webpack系列-进阶
编写一个简单的Loader
参考文档
loader api: https://webpack.docschina.org/api/loaders/
resolveLoader: https://webpack.docschina.org/configuration/resolve/#resolveloader
初始化简单的项目
npm init -y
npm install --save-dev webpack webpack-cli
创建loader
loader本质就是函数
创建完成本地的loader之后,在本地项目中使用webpack.config.js
:
const path = require('path');
module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
module: {
rules: [
{
test: /\.js/,
use: [
// 使用本地loader
path.resolve(__dirname, './loaders/replaceLoader.js')
]
}
]
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
}
}
使用更多的配置项,让loader中可以获取到配置项内容:
// webpack.config.js
module.exports = {
...
module: {
rules: [
{
test: /\.js/,
use: [
// 使用本地loader
{
loader: path.resolve(__dirname, './loaders/replaceLoader.js')
options: { // loader中可以通过this.query获取配置内容
name: 'dellLee'
}
}
]
}
]
}
...
}
// loader.js
module.exports = function(source) {
console.log(this.query) // { name: 'dellLee }
return source.replace('dell', this.query.name)
}
需要返回多余的信息使用this.callback,使用this.async进行异步操作
// loader中使用this.async进行处理
module.exports = function(source) {
const options = this.getOptions()
const callback = this.async() // this.async返回一个this.callback
setTimeout(() => {
const result = source.replace('dell', opthions.name)
callback(null, result)
}, 1000)
}
使用resolveLoader
用于解析本地的webpack
loader
包
module.exports = {
resolveLoader: {
modules: ['node_modules', './loaders'], // 会先在node_modules查找loader,如果查找不到则会到第二个配置查找
extensions: ['.js', '.json'],
},
module: {
use: [
// 配合resolveLoader本地loader使用
{
loader: 'replaceLoader',
options: { // loader中可以通过this.query获取配置内容
name: 'dellLee'
}
}
]
}
}
可以使用loader做全局处理,如国际化、错误报警等。
编写一个简单的plugin
plugin的核心机制:事件驱动,发布订阅的设计模式。
参考文档
Plugins: https://webpack.docschina.org/api/compiler-hooks/
钩子类型描述:https://github.com/webpack/tapable#tapable
创建插件plugin
plugin的本质是对象
创建一个简单的plugin用于打印copyright一些信息:
class CopyrightWebpackPlugin {
constructor(options) {
console.log('插件被使用了')
}
apply(compiler) { // compiler存放打包的内容和配置
// 输出asset到output目录之前 异步执行
compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin', (compilation, cb) => {
// compilation存放此次打包的内容
compilation.assets['copyright.txt'] = { // 生成copyright文件
source: function() {
return 'copyright by dell lee'
},
size: function() {
return 21
}
}
cb()
})
}
}
module.exports = CopyrightWebpackPlugin;
compilation.assets
中的内容:
webpack.config.js
使用本地创建的plugin:
const path = require('path');
const CopyrightWebpackPlugin = require('./plugins/copyright-webpack-plugin.js')
module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
plugins: {
new CopyrightWebpackPlugin({
name: 'hello'
}) // 实例化插件
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
}
}
调试
参考官网文章:https://www.webpackjs.com/contribute/debugging/
安装node-nightly
:npm install node-nightly -g
安装完成后,需要在命令行中运行一次,结束安装:node-nightly
可以直接使用带有 --inspect
标记的 node-nightly
,在任何基于 webpack 的项目中开始构建。注意,我们不应该运行 NPM scripts
,例如 npm run build
,所以我们需要指定完整的 node_modules
路径:
node-nightly --inspect ./node_modules/webpack/bin/webpack.js
应该输出如下内容:
Debugger listening on ws://127.0.0.1:9229/c624201a-250f-416e-a018-300bbec7be2c
For help see https://nodejs.org/en/docs/inspector
现在,在浏览器中访问 chrome://inspect
,你会看到在 Remote Target 标题下可以进行 inspect(审查) 的活动脚本。单击每个脚本下自动连接会话的 “inspect” 链接,打开一个专门 debugger 或 Open dedicated DevTools for Node 链接。你还可以看到 NiM 扩展程序,这是一个方便的 Chrome 插件,在每次你通过 --inspect
调试某个脚本时,都会自动打开 DevTools 标签页。
我们推荐使用 --inspect-brk
标记,此标记将在脚本的第一条语句处断开,以便你可以在源代码中设置断点,并根据需要启动/停止构建。此外,不要忘记,你仍然可以向脚本传递参数。例如,如果你有多个配置文件,你可以通过 --config webpack.prod.js
指定你想要调试的配置。
其他参考链接
node debugging
: https://nodejs.org/zh-cn/docs/guides/debugging-getting-started/
创建一个简易的bundler库
简单的ESM转CJS,更多模块化知识
// index.js为仅简单的模块间相互调用的示例
// 简易的打包工具
// 对项目的打包,首先要先读取入口文件,对项目代码进行分析
// cli-highlight插件,帮助高亮cli输出
// @babel/parser 帮助进行源代码语义分析 https://www.babeljs.cn/docs/babel-parser
// @babel/traverse 对解析出的抽象语法树进行遍历 https://www.babeljs.cn/docs/babel-traverse
// @babel/core babel的核心模块,对代码进行转义,使得可以在浏览器中运行 https://babeljs.io/docs/en/babel-core
// @babel/preset-env代码转义插件,将代码转换为目标环境可运行的代码 https://babeljs.io/docs/en/babel-preset-env
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default; // 默认ES6 module的导出,使用要用default
const babel = require('@babel/core')
const moduleAnalyser = (filename) => { // 读取文件,获取文件内容 进行解析抽象语法树
const content = fs.readFileSync(filename, 'utf-8');
// 获取抽象语法树
const ast = parser.parse(content, {
sourceType: 'module'
})
const dependencies = {} // 获取依赖文件信息, 存入的路径是绝对路径或者是相对于bundle的相对路径
// 如果只存一个相对路径会在打包的时候比较麻烦,所以存一个相对路径和绝对路径
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(filename)
const sourceValue = node.source.value
const newFile = './' + path.join(dirname, sourceValue)
dependencies[sourceValue] = newFile; // 键值对的方式进行存储
}
});
// 对抽象语法树进行转换, 将转换后的code取出
const { code } = babel.transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})
return {
filename, // 文件
dependencies, // 依赖
code // 转移后的代码
}
}
// 依赖图谱,将入口文件中的依赖文件再进一步分析,一层一层递进
const makeDependenciedGraph = (entry) => {
// 分析入口文件
const entryModule = moduleAnalyser(entry);
const graphArray = [ entryModule ]; // 初始化图谱数组
for (let i = 0; i < graphArray.length; i++) {
const item = graphArray[i];
const { dependencies } = item; // 获取依赖文件
if (dependencies) {
// 如果存在依赖,则循环依赖
for (let j in dependencies) {
// 对依赖文件进行分析,将分析结果存入到图谱数组中
graphArray.push(
moduleAnalyser(dependencies[j]) // 依赖的对象文件
); // 推入graphArray中后,长度变长,下一个进行遍历分析的文件
}
}
}
// 关系图谱数据解构转化,将数组转化为对象
const graph = {};
graphArray.forEach(item => {
graph[item.filename] = {
dependencies: item.dependencies,
code: item.code
}
});
return graph;
}
// 最后的结果返回代码字符串
const generateCode = (entry) => {
// 转化成字符串传入到闭包中
const graph = JSON.stringify(makeDependenciedGraph(entry));
// 代码在闭包中执行避免污染外部环境
// require无法在浏览器中直接运行,需要进行创建require方法
// export存储导出的内容 会转存到exports对象中
// 返回一个字符串
// localRequire转换文件的相对路径返回真实路径
// 递归调用到最内层的依赖文件,然后通过exports导出
return `
(function(graph) {
function require(module) {
function localRequire(relativePath) {
return require(graph[module].dependencies[relativePath]);
}
var exports = {};
(function(require, exports, code) {
eval(code);
})(localRequire, exports, graph[module].code);
return exports;
};
require('${entry}');
})(${graph});
`;
}
const code = generateCode('./src/index.js')
console.log(code)
// 注意:示例文件中本来是有注释的,但是注释加上去了之后就会出现问题,所以删除注释后在chrome://test模式下运行成功
webpack 管理多个项目
实现多个项目共用 node_modules 和 package.json
差异性不大的多个项目是否可以共用一个
node_modules
,使用公共组件部分、公共插件(抽npm)
方案
方案一
每个项目中独立配置项目差异 config
文件,相同配置使用根目录的 webpack.base.config.js
实现:
- ./webpack.base.config.js、
- projectA/webpack.config.js、
- projectB/webpack.config.js
- 根目录下的
package.json
的scripts
命令:npx webpack --config ./projectA/webpack.config.js && npx webpack --config ./projectB/webpack.config.js
方案二
使用根目录统一的 config 文件进行做差异化处理
实现:
- ./webpack.config.js
- 根目录下的
package.json
的scripts
命令:npx webpack --env PROJECT=projectA && npx webpack --env PROJECT=projectB
注意点
两种方案都使用了 webpack-merge 合并配置。
配置的主要问题是获取到具体项目的路径、以及定义配置的上下文环境 context
和 node_modules
路径解析 resolve.modules
。
Demo项目地址
study webpack: https://github.com/Datura35422/study/tree/master/webpack-demos
pug demo: https://github.com/Datura35422/pug-demo/tree/master/website