webpack
本质上,webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具。当 webpack 处理应用程序时,它会在内部从一个或多个入口点构建一个 依赖图(dependency graph),然后将你项目中所需的每一个模块组合成一个或多个 bundles,它们均为静态资源,用于展示你的内容。
——webpack中文文档
为什么需要webpack
以html文件为例,如果要导入JS文件,一般是这样导入:
1 2
| <script src="module-app1.js"></script> <script src="module-app2.js"></script>
|
但是这种引入存在很多问题:
- 所有的变量都放在window下面,容易造成变量污染
- 需要手动维护文件加载顺序,比如app1中引用了app2中的某个函数,这样引用就会报错
- 无法按需加载,拆分代码
所以需要一个打包工具,既要能编译代码,又要能整合模块,压缩优化,那就是webpack.
wepack配置文件
先来看看一个简单的webpack配置文件是怎么样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
| const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = { mode: 'development', devtool: 'source-map' entry: { main: './src/main.js', home: './src/home.js' vendor: ['react', 'react-dom'] },
output: { filename: '[name].js', path: path.resolve(__dirname, 'dist'), publicPath: '/', },
module: { rules: [ { test: /\.(js|jsx)$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env', '@babel/preset-react'] } } },
{ test: /\.css$/, use: [ 'style-loader', { loader: 'css-loader', options: { modules: { auto: true, localIdentName: '[name]__[local]--[hash:base64:5]' } } }, 'postcss-loader' ] },
] },
plugins: [ new HtmlWebpackPlugin({ template: './public/index.html', favicon: './public/favicon.ico' }), ]
optimization: { splitChunks: { chunks: 'all', cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendors', priority: 10 } } }, runtimeChunk: 'single' },
devServer: { static: { directory: path.join(__dirname, 'public') }, compress: true, port: 3000, hot: true },
resolve: { extensions: ['.js', '.jsx', '.json'], alias: { '@': path.resolve(__dirname, 'src') } } };
|
wepback的配置文件通常被命名为wepback.config.js,位于项目的根目录下,其所使用的模块规范是CommonJS,也就是node.js所使用的规范,下面介绍配置文件中的比较核心的一些概念。
entry
entry即入口,用来指示webpack应该将哪个模块作为依赖图的开始,递归地寻找依赖的模块,最后将这些模块组合成一个或者多个bundles
1 2 3 4 5 6 7 8 9 10 11 12
| module.exports = { entry: './src/main.js' }
------------
module.exports = { entry: { app1: './src/app1.js' app2: './src/app2.js' } }
|
对于项目来说,如果有多个入口和多个模块,在webpack打包成bundle后,我们会得到一个包含所有代码地bundle文件。但是存在一些问题,如果我们在多个入口点之间共享了一些公共代码,这些代码将被打包到多个bundle,导致代码冗余。因此,webpack提供vendor配置项,允许我们将公共代码提取到一个单独的文件中
1 2 3 4 5 6 7
| module.exports = { entry: { app1: './src/app1.js', app2: './src/app2.js' , vendor: ['react', 'react-dom'] // 第三方库入口 } };
|
像这样,react和react-dom会单独作为一个bundle打包出来,如果app1或者app2中对其有依赖,则会从打包后的公共模块的bundle中提取使用
output
output即输出,用来告诉webpackk在哪里输出他所创建地bundle,以及如何命名这些文件。默认情况下放在./dist文件夹中
1 2 3 4 5
| output: { filename: '[name].js', //输出的文件名 path: path.resolve(__dirname, 'dist'), //输出文件的路径 publicPath: '/', //公开访问路径 },
|
- filename: 这个顾名思义,就是输出的文件名,[name]是一个占位符,即entry中定义的键名
比如entry: { app: ‘./src/index.js’ },最后就会输出app.js
除了[name]外,常见的占位符还有[contenthash],[id]等
‘https://fkxqsVw50.com/assets',那么我们就可以通过浏览器访问到这个文件
mode
mode用来指定当前的构建环境,根据不同的mode,webpack有不同的打包方式
1 2 3
| module.exports = { mode: 'development' }
|
development: 顾名思义,开发模式。在development下,webpack会启用启用 eval、类型的 source map和热更新,同时保留原始变量名,不进行代码混淆,同时为了保证构建速度,会跳过代码压缩和树摇等操作。
production:生产模式,这种模式下,webpack会对代码进行压缩,比如使用TerserWebpackPlugin和CssMinimizerPlugin分别压缩JS和CSS,同时开启树摇和模块合并,并移除调试代码(console.log)
none:即什么都没有。none模式下会禁用所有默认优化,纯人工控制。
devtool用来控制是否生成,以及如何生成source map
1 2 3
| module.exports = { devtool: 'source-map' }
|
从官网上看,devtool的配置比较吓人,有20+种,用于各种不同情况下的source map。这里就只介绍下development和production下常用的source map
注:source map 是源码与打包后代码的映射文件,用于在浏览器种直接查看和调试原始代码,精确定位报错位置
在development下:
- eval:用eval()执行,构建速度最快,但是仅显示行号
- cheap-eval-source-map:速度慢了一点,每行对应映射,含有行号但无列号
- eval-source-map,速度又慢了一点,但是包含了完整源码
在production下:
none
:不生成 source map 文件。
source-map
:生成完整的 source map 文件。
hidden-source-map
:生成 source map 文件,但不会在代码中暴露其引用。
nosources-source-map
:生成 source map 文件,但不包含源代码内容。
module
module即模块,它主要用于扩展webpack的解析能力。因为webpack只能处理JS和JSON类型的文件,module可以通过module.rulers来通过各种loader来文件预处理,从而扩展webpack的解析能力。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| module: { rules: [ { test: /\.(js|jsx)$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: [ '@babel/preset-env', '@babel/preset-react' ] } } },
{ test: /\.css$/, use: [ 'style-loader', 'css-loader', 'postcss-loader' ] }, ] },
|
关于loader后面再详细介绍
plugins
plugins,即插件。相对于loader扩展了webpack的解析功能,plugins直接扩展了webpack的功能,比如打包优化,资源管理等,而且生命周期可以遍布整个构建流畅。
1 2 3 4 5 6 7 8 9
| const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = { plugins: [ new HtmlWebpackPlugin({ template: './public/index.html', }), ] }
|
plugin在使用需要先通过require获取,再在plugins中实例化即可,不同的plugin有不同的参数,后面在详细介绍
optimization
optimization,即优化。它用来控制代码的分割,压缩,去重等优化方式。
1 2 3 4 5 6 7 8 9 10 11 12 13
| optimization: { splitChunks: { chunks: 'all', cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendors', priority: 10 } } }, runtimeChunk: 'single' },
|
webpack-dev-server
webpack 内置的 开发服务器 webpack-dev-server,用于在开发过程中提供本地服务器和热更新功能。
1 2 3 4 5 6 7 8
| devServer: { static: { directory: path.join(__dirname, 'public') }, compress: true, port: 3000, hot: true },
|
- static.directory: 设置静态文件目录,告诉 devServer 从哪里提供额外的静态资源(如 HTML、图片)。它不会经过 webpack 打包。通常是 public 目录。
- compress:是否启用gzip压缩代码
- port:设置服务器的端口号
- hot:是否启动热更新
resolve
resolve 控制 webpack 如何解析模块的路径
1 2 3 4 5 6
| resolve: { extensions: ['.js', '.jsx', '.json'], alias: { '@': path.resolve(__dirname, 'src') } }
|
- extensions: 自动补全扩展名,比如require时没有后缀名,webpack 会按顺序尝试补全这些后缀
- alias:路径别名,比如@ 被当成 src 目录的别名,方便在代码中引用模块,不必写长长的相对路径
webpack构建过程
省流:
- 初始化
- 构建
- 生成
- 输出
初始化
在最开始,webpack会对参数进行初始化,它首先从wepback cli中获取命令行参数,并与webpack.config.js中的配置进行合并得到最终的配置参数,之后调用new Webpack(options)正式开始打包工作,这期间还用通过validateSchema校验配置正确与否,如果无误,就会创建一个compiler对象。
有了compiler,webpack会通过compiler的hooks挂载用户导入的plugins以及webpack的各种内置插件。到这里,不仅有了compiler实例,编译环境也搭载完毕,就可以开始构建过程了。
1
| webpack --mode=production
|
注:
compiler:compiler包含了webpack的配置信息,如entry,output,loaders等,提供了注册和调用plugin的接口,同时负责调控整个整个编译过程,比如run,emit,done等,一遍webpack处理逻辑。
compilation: 每次构建资源过程创建,它包含当前构建的所有模块资源,生成资源及其依赖关系,相较于compiler,conpilation更注重编译的细节,比如buildModules,optimizeModules。
构建过程(make)
构建阶段,compiler会调用compiler.run()表示开始构建流程,通过new Compilation(options)创建一个compilation实例来具体管理这次编译过程,compilation.make()会通知正式开始构建模块,通过buildModule从entry开始解析文件之间的依赖,这首先会经过loader处理,输出webpack能够理解的JS模块,然后由Acorn将JS文件解析为AST,compilation通过各种钩子监听,从中获取module的依赖,加入到module的依赖列表,之后对module的依赖递归执行上述步骤,并逐步构建ModuleGraph。全部解析之后,如果配置了optimization,则会通过optimizeModules对modules进行优化,至此构建阶段结束。
举个例子,有一个index.js文件,它引用了a.js和b.js两个文件,那么最后的ModuleGraph如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| ModuleGraph: { _dependencyMap: Map(3){ { EntryDependency{request: "./src/index.js"} => ModuleGraphConnection{ module: NormalModule{request: "./src/index.js"}, originModule: null } }, { HarmonyImportSideEffectDependency{request: "./src/a.js"} => ModuleGraphConnection{ module: NormalModule{request: "./src/a.js"}, originModule: NormalModule{request: "./src/index.js"} } }, { HarmonyImportSideEffectDependency{request: "./src/b.js"} => ModuleGraphConnection{ module: NormalModule{request: "./src/b.js"}, originModule: NormalModule{request: "./src/index.js"} } } },
_moduleMap: Map(3){ NormalModule{request: "./src/index.js"} => ModuleGraphModule{ incomingConnections: Set(1) [ ModuleGraphConnection{ module: NormalModule{request: "./src/index.js"}, originModule:null } ], outgoingConnections: Set(2) [ ModuleGraphConnection{ module: NormalModule{request: "./src/a.js"}, originModule: NormalModule{request: "./src/index.js"} }, ModuleGraphConnection{ module: NormalModule{request: "./src/b.js"}, originModule: NormalModule{request: "./src/index.js"} } ] }, NormalModule{request: "./src/a.js"} => ModuleGraphModule{ incomingConnections: Set(1) [ ModuleGraphConnection{ module: NormalModule{request: "./src/a.js"}, originModule: NormalModule{request: "./src/index.js"} } ], outgoingConnections: undefined }, NormalModule{request: "./src/b.js"} => ModuleGraphModule{ incomingConnections: Set(1) [ ModuleGraphConnection{ module: NormalModule{request: "./src/b.js"}, originModule: NormalModule{request: "./src/index.js"} } ], outgoingConnections: undefined } } }
|
生成阶段(seal)
如果说构建阶段的关键是module,那么生成阶段就是围绕chunk展开。make之后,compilation会进入seal阶段,通过构建阶段收集到的ModuleGraph来将有依赖关系得module封装为chunk,同时构建ChunkGraph。
这个chunk的封装很好理解,如下:
- entry 及 entry 触达到的模块,组合成一个 chunk
- 使用动态引入语句引入的模块,各自组合成一个 chunk
- 如果配置了runtimeChunk,那么runtime也会组织成一个chunk
以前面的配置文件为例:
1 2 3 4 5 6 7
| module.exports = { entry: { main: "./src/main.js", home: "./src/home.js", } };
|
webpack根据entry创建出chunk[main]和chunk[home],之后,webpack根据ModuleGraph,将entry所依赖的文件注入到相应的chunk中。假如main.js依赖了a.js和b.js,那么a.js和b.js就会被注入到chunk[main]。
但如果模块用异步方式(require.ensure(“./xx.js”) 或 import(“./xx.js”))引入,比如home.js异步引入了async.js,那么webpack会为async.js单独创建一个chunk[async]并让chunk[home]引用它,此时对于异步生成的chunk,它的引用者被称为parent,它自己被称为child。
注: webpack的编译产物在运行需要一些支持模块化,异步加载等特性的代码,这列代码统称为runtime。
但是chunk是不够的,因为chunk本质上只是一个容器,它不关心里面的module是什么,它只关心是不是自己的。所以这里还有关键一步——compilation.CodeGenerator()
CodeGenerator()会通过chunkGraph得知module之间的依赖,module于chunk之间的关系以及module的runtime来处理module。
在compilation.codeGeneration()执行完毕之后,module转译的结果会保存compilation.codeGenerationResults对象中,然后通过chunk进行模块合并打包,生成完整的bundle,之后调用complation.emitAsset()将bundle写为assets,最后有compiler.emitAssets()将assets写入磁盘之中,至此,一次打包过程结束。
一点点的小细节
loader
从本质上讲,loader就是一个函数,它可以在webpack处理某些资源之前提前处理。比如我要用webpack处理一个ts的文件,我可以提前使用一个babel-loader将.ts后缀的文件提前编译为JS代码,再交给webpack处理。
loader的基本配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| module.exports = { module: { rules: [ { test: /.sass$/, use: [ 'style-loader', 'css-loader', 'sass-loader', ] }, ], }, };
|
- test: test是一个正则表达式,我们会对应的资源文件根据test的规则去匹配。如果匹配到,那么该文件就会交给对应的loader去处理。
- use: use表示匹配到test中匹配对应的文件应该使用哪个loader的规则去处理,use可以为一个字符串,也可以为一个数组。
注:use为一个数组时表示有多个loader依次处理匹配的资源,按照 从右往左(从下往上) 的顺序去处理。
- enforce:enforce决定了loader执行的顺序。
1 2 3 4 5 6 7 8 9 10
| module.exports = { module: { rules: [ { test: /.css$/, use: 'sass-loader', enforce: 'pre' }, { test: /.css$/, use: 'css-loader' }, { test: /.css$/, use: 'style-loader', enforce: 'post' }, ], }, };
|
enforce的两个值决定loader的执行顺序,分别是per和post,比如现在loader会从前往后执行。
loader的种类和执行顺序
loader可以分为四种类型:pre,normal,post和inline。其中,前三种通过enforce配置,而inline则通过内联的方式配置
1
| import Styles from 'style-loader!css-loader!./styles.css';
|
从命名上就可以看出loader的执行顺序:
pre loader -> normal loader -> inline loader -> post loader
webpack进行编译文件前,文件资源会匹配到对应loader:
- 执行pre loader前置处理文件。
- 将pre loader执行后的资源链式传递给normal loader正常的loader处理。
- normal loader处理结束后交给inline loader处理。
- 最终通过post loader处理文件,将处理后的结果交给webpack进行模块编译。
但是,在实际使用的时候存在一些问题:
- Loader 链条一旦启动之后,需要所有 Loader 都执行完毕才会结束,没有中断的机会 —— 除非显式抛出异常
- 某些场景下并不需要关心资源的具体内容,但 Loader 需要在 source 内容被读取出来之后才会执行
为了解决这两个问题,webpack 在 loader 基础上增加了 pitch 的概念。
loader的pitch阶段
关于loader的执行阶段实际分为两个阶段:
- 在处理资源文件之前,首先会经历pitch阶段。
- pitch结束后,读取资源文件内容。
- 经过pitch处理后,读取到了资源文件,此时才会将读取到的资源文件内容交给正常阶段的loader进行处理。
简单来说就是webpack在使用loader处理资源时首先会经过loader.pitch阶段,此时loader从前往后执行,pitch`阶段结束后才会读取文件而后进行normal阶段处理,此时从后往前执行。
1 2 3 4 5 6 7 8 9 10
| const loader = function (source){ console.log('后执行') return source; }
loader.pitch = function(requestString) { console.log('先执行') }
module.exports = loader
|
可以看到,pitch也是一个函数,或者说是loader上的一个方法,它接收三个参数,分别是:
- remainingRequest:当前 loader 之后的所有 loader 加上资源模块
- previousRequest:在执行当前 loader 之前经历过的 loader 列表
- data: 在当前 loader 的 pitch和普通 loader 函数之间共享
这里不得不提到pitch的核心机制——熔断
在pitch阶段,如果loader.pitch返回了一个非undefined的值,那么loader就会马上掉头执行上一个已经执行的loader的normal阶段并且将返回值作为参数传入,之后再回来执行后面的loader。
这有什么用呢?😕
下面以最开始的配置为例
看一下几个loader的作用:
scss-loader:将 scss 规格的内容转换为标准 css
css-loader :将 css 内容包裹为 JavaScript 模块
style-loader :将 JavaScript 模块的导出结果以 link 、style 标签等方式挂载到 html 中,让 css 代码能够正确运行在浏览器上
注意到style-loader只是负责让 css 能够在浏览器环境下跑起来,本质上并不需要关心具体内容,很适合用 pitch 来处理,所以style-loader真正起作用的地方在pitch而非normal。
。。。。。。
那为什么会这样呢?🤔
loader的小月读
wepback编译模块之前通过一个叫runLoaders的方法来调用loader模块,它接收两个参数:
第一个是对象,包含四个参数:
- resource: resource参数表示需要加载的资源路径。
- loaders: 表示需要处理的loader绝对路径拼接成为的字符串,以!分割。
- context: loader上下文对象,webpack会在进入loader前创建一系列属性挂载在一个object上,而后传递给loader内部。
- processResource: 读取资源文件的方法。
第二个是callback,表示本次loader处理完成的结果,这个结果会分为两个参数给callback:
- error: 如果runLoaders函数执行过程中遇到错误那么这个参数将会变成错误内容,否则将为null。
- result:一个数组,用来表示本次经过所有loaders处理完毕后的文件内容。
下面看一点runloaders的小细节:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
| function runLoaders(options, callback) { const resource = options.resource || ''; let loaders = options.loaders || []; const loaderContext = options.context || {}; const readResource = options.readResource || fs.readFile.bind(fs); loader = loader.map(createLoaderObject); loaderContext.resourcePath = resource; loaderContext.readResource = readResource; loaderContext.loaderIndex = 0; loaderContext.loaders = loaders; loaderContext.data = null; loaderContext.async = null; loaderContext.callback = null;
Object.defineProperty(loaderContext, 'request', { enumerable: true, get: function () { return loaderContext.loaders .map((l) => l.request) .concat(loaderContext.resourcePath || '') .join('!'); }, }); Object.defineProperty(loaderContext, 'remainingRequest', { enumerable: true, get: function () { return loaderContext.loaders .slice(loaderIndex + 1) .map((i) => i.request) .concat(loaderContext.resourcePath) .join('!'); }, }); Object.defineProperty(loaderContext, 'currentRequest', { enumerable: true, get: function () { return loaderContext.loaders .slice(loaderContext) .map((l) => l.request) .concat(loaderContext.resourcePath) .join('!'); }, }); Object.defineProperty(loaderContext, 'previousRequest', { enumerable: true, get: function () { return loaderContext.loaders .slice(0, loaderContext.index) .map((l) => l.request) .join('!'); }, }); Object.defineProperty(loaderContext, 'data', { enumerable: true, get: function () { return loaderContext.loaders[loaderContext.loaderIndex].data; }, }); const processOptions = { resourceBuffer: null, }; iteratePitchingLoaders(processOptions, loaderContext, (err, result) => { callback(err, { result, resourceBuffer: processOptions.resourceBuffer, }); });
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| function createLoaderObject(loader) { const obj = { normal: null, pitch: null, raw: null, data: null, pitchExecuted: false, normalExecuted: false, request: loader, }; const normalLoader = require(obj.request); obj.normal = normalLoader; obj.pitch = normalLoader.pitch; obj.raw = normalLoader.raw; return obj; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| function iteratePitchingLoaders(options, loaderContext, callback) { if (loaderContext.loaderIndex >= loaderContext.loaders.length) { return processResource(options, loaderContext, callback); }
const currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
if (currentLoaderObject.pitchExecuted) { loaderContext.loaderIndex++; return iteratePitchingLoaders(options, loaderContext, callback); } const pitchFunction = currentLoaderObject.pitch;
currentLoaderObject.pitchExecuted = true;
if (!currentLoaderObject.pitch) { return iteratePitchingLoaders(options, loaderContext, callback); }
runSyncOrAsync( pitchFunction, loaderContext, [ currentLoaderObject.remainingRequest, currentLoaderObject.previousRequest, currentLoaderObject.data, ], function (err, ...args) { if (err) { return callback(err); } const hasArg = args.some((i) => i !== undefined); if (hasArg) { loaderContext.loaderIndex--; iterateNormalLoaders(options, loaderContext, args, callback); } else { iteratePitchingLoaders(options, loaderContext, callback); } } ); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| function runSyncOrAsync(fn, context, args, callback) { let isSync = true; let isDone = false;
const innerCallback = (context.callback = function () { isDone = true; isSync = false; callback(null, ...arguments); });
context.async = function () { isSync = false; return innerCallback; };
const result = fn.apply(context, args);
if (isSync) { isDone = true; if (result === undefined) { return callback(); } if ( result && typeof result === 'object' && typeof result.then === 'function' ) { return result.then((r) => callback(null, r), callback); } return callback(null, result); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| function processResource(options, loaderContext, callback) { loaderContext.loaderIndex = loaderContext.loaders.length - 1; const resource = loaderContext.resourcePath; loaderContext.readResource(resource, (err, buffer) => { if (err) { return callback(err); } options.resourceBuffer = buffer; iterateNormalLoaders(options, loaderContext, [buffer], callback); }); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| function iterateNormalLoaders(options, loaderContext, args, callback) { if (loaderContext.loaderIndex < 0) { return callback(null, args); } const currentLoader = loaderContext.loaders[loaderContext.loaderIndex]; if (currentLoader.normalExecuted) { loaderContext.loaderIndex--; return iterateNormalLoaders(options, loaderContext, args, callback); }
const normalFunction = currentLoader.normal; currentLoader.normalExecuted = true; if (!normalFunction) { return iterateNormalLoaders(options, loaderContext, args, callback); } convertArgs(args, currentLoader.raw); runSyncOrAsync(normalFunction, loaderContext, args, (err, ...args) => { if (err) { return callback(err); } iterateNormalLoaders(options, loaderContext, args, callback); }); }
|
plugin
前置知识:Tapable
对于webpack的插件,本质上类似于Node.js中的发布订阅模式,Tapable就相当于EventEmitter。webpack通过Tapable来注册事件,并在不同的时机触发注册的事件。
Tapable提供了九种不同的钩子:
1 2 3 4 5 6 7 8 9 10 11 12
| const { SyncHook, SyncBailHook, SyncWaterfallHook, SyncLoopHook, AsyncParallelHook, AsyncParallelBailHook, AsyncSeriesHook, AsyncSeriesBailHook, AsyncSeriesWaterfallHook } = require("tapable");
|
这些hook可以按照执行机制进行分类:
Basic Hook : 基本类型的钩子,它仅仅执行钩子注册的事件,并不关心每个被调用的事件函数返回值如何。
Waterfall : 瀑布类型的钩子,瀑布类型的钩子和基本类型的钩子基本类似,唯一不同的是瀑布类型的钩子会在注册的事件执行时将事件函数执行非 undefined 的返回值传递给接下来的事件函数作为参数。
Bail : 保险类型钩子,保险类型钩子在基础类型钩子上增加了一种保险机制,如果任意一个注册函数执行返回非 undefined 的值,那么整个钩子执行过程会立即中断,之后注册事件函数就不会被调用了。
Loop : 循环类型钩子,循环类型钩子稍微比较复杂一点。循环类型钩子通过 call 调用时,如果任意一个注册的事件函数返回值非 undefeind ,那么会立即重头开始重新执行所有的注册事件函数,直到所有被注册的事件函数都返回 undefined。
也可以按照同步异步分:
- 同步表示注册的事件函数会同步进行执行。
- 异步表示注册的事件函数会异步进行执行。
注:异步钩子可以分为:
- 异步串行钩子:可以被串联(连顺序调用)执行的异步钩子函数。
- 异步并行钩子:可以被并联(并发调用)执行的异步钩子函数。
Tapable 使用时通常需要经历如下步骤:
1 2 3 4 5 6 7 8 9 10 11
| const hook = new SyncHook(["arg1", "arg2", "arg3"]);
hook.tap('flag1', (arg1,arg2,arg3) => { console.log('flag1:',arg1,arg2,arg3) })
hook.call('扣1请我吃肯德基','111','吃吃吃')
flag1: '扣1请我吃肯德基','111','吃吃吃'
|
首先我们需要通过 new 关键字实例不同种类的 Hook。
- new Hook 时候接受一个字符串数组作为参数,数组中的值不重要,重要的是数组中对应的字符串个数。
- 其实 new Hook 时还接受第二个参数 name ,它是一个 string。
其次通过 tap 函数监听对应的事件,注册事件时接受两个参数:
- 第一个参数是一个字符串,它没有任何实际意义仅仅是一个标识位而已。这个参数还可以为一个对象。
- 第二个参数表示本次注册的函数,在调用时会执行这个函数。
最后就是我们通过 call 方法传入对应的参数,调用注册在 hook 内部的事件函数进行执行。
- 同时在 call 方法执行时,会将 call 方法传入的参数传递给每一个注册的事件函数作为实参进行调用。
这是同步hook的示例,但是异步串行hook和异步并行hook的情况不太一样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| const { AsyncSeriesHook } = require('tapable');
const hook = new AsyncSeriesHook(['arg1', 'arg2', 'arg3']);
console.time('timer');
hook.tapAsync('flag1', (arg1, arg2, arg3, callback) => { console.log('flag1:', arg1, arg2, arg3); setTimeout(() => { callback(); }, 1000); });
hook.tapPromise('flag2', (arg1, arg2, arg3) => { console.log('flag2:', arg1, arg2, arg3); return new Promise((resolve) => { setTimeout(() => { resolve(); }, 1000); }); });
hook.callAsync('扣1请我吃肯德基','111','吃吃吃', () => { console.log('已经没有了'); console.timeEnd('timer'); });
flag1: '扣1请我吃肯德基','111','吃吃吃' flag2: '扣1请我吃肯德基','111','吃吃吃' '已经没有了' timer: 2.012s
|
tapAsync 注册时实参结尾额外接受一个 callback ,调用 callback 表示本次事件执行完毕。
callback 的机制和 node 中是一致的,也就是说 callback 函数调用时,如果第一个参数表示错误对象,如果传递第一个参数的话那么就表示本次执行出现错误会中断执行。
当然后续参数和 node.js 中同理,从 callback 函数第二个参数表示开始表示本次函数调用的返回值。
Promise 同理,如果这个 Promise 返回的结果是 reject 状态,那么和 callback 传递错误参数同样效果,也会中断后续的执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| const { AsyncParallelHook } = require('tapable');
const hook = new AsyncParallelHook (['arg1', 'arg2', 'arg3']);
console.time('timer');
hook.tapAsync('flag1', (arg1, arg2, arg3, callback) => { console.log('flag1:', arg1, arg2, arg3); setTimeout(() => { callback(); }, 1000); });
hook.tapPromise('flag2', (arg1, arg2, arg3) => { console.log('flag2:', arg1, arg2, arg3); return new Promise((resolve) => { setTimeout(() => { resolve(); }, 1000); }); });
hook.callAsync('扣1请我吃肯德基','111','吃吃吃', () => { console.log('已经没有了'); console.timeEnd('timer'); });
flag1: '扣1请我吃肯德基','111','吃吃吃' flag: '扣1请我吃肯德基','111','吃吃吃' '已经没有了' timer: 1.010ss
|
可以看到最终的回调函数执行时打印的事件为1s
稍微多一点,也就是说 flag1 、 flage2 两个事件函数并行开始执行,在1s后两个异步函数执行结束,整体回调结束。
plugin的基本构成
1 2 3 4 5 6 7 8 9 10
| class DonePlugin { apply(compiler) { compiler.hooks.done.tap('Plugin Done', () => { console.log('到我了'); }); } }
module.exports = DonePlugin;
|
上面就是一个plugin最基本的架构,巨简单,从上面的基本构成可以看到:
- 首先一个 Plugin 应该是一个 class,当然也可以是一个函数。
- 其次 Plugin 的原型对象上应该存在一个 apply 方法,当 webpack 创建 compiler 对象时会调用各个插件实例上的 apply 方法并且传入 compiler 对象作为参数(不一定是compiler)。
- 同时需要指定一个绑定在 compiler 对象上的 Hook , 比如 compiler.hooks.done.tap 在传入的 compiler 对象上监听 done 事件。
- 在 Hook 的回调中处理插件自身的逻辑。
- 根据 Hook 的种类,在完成逻辑后通知 webpack 继续进行。