webpack插件是什么 官方文档: webpack 插件是一个具有 apply 属性的 JavaScript 对象。apply 属性会被 webpack compiler 调用,并且 compiler 对象可在整个编译生命周期访问。插件目的在于解决 loader 无法实现的其他事。
个人理解:插件通常是在webpack在打包的某个时间节点做一些事情,类似vue中常说的钩子函数。
官方示例: 1 2 3 4 5 6 7 8 9 const pluginName = 'ConsoleLogOnBuildWebpackPlugin' ;class ConsoleLogOnBuildWebpackPlugin { apply (compiler ) { compiler.hooks.run.tap(pluginName, compilation => { console .log("webpack 构建过程开始!" ); }); } }
自定义插件使用时只需要在配置文件中引入,并做相关配置即可。第4行的complier后面的一大坨代码为什么这么写呢?源码分析如下。
webpack源码 写本文时参考的源码版本 1 2 3 4 "devDependencies": { "webpack": "^5.3.1", "webpack-cli": "^4.1.0" }
先说一下webpack入口文件的大致流程:
首先检查是否安装webpack-cli。
如果没安装,则提示用户是否安装,如果用户输入y开头的指令,则安装webpack-cli,安装成功后进入webpack-cli入口文件,开始干活,否则退出。
如果已经安装,进入webpack-cli入口文件,开始干活。
具体代码如下,以下直接从源码拷贝过来后去除了一些没用的注释,加了一些中文注释,不感兴趣可直接跳过,这不是重头戏。
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 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 const runCommand = (command, args ) => { const cp = require ("child_process" ); return new Promise ((resolve, reject ) => { const executedCommand = cp.spawn(command, args, { stdio: "inherit" , shell: true }); executedCommand.on("error" , error => { reject(error); }); executedCommand.on("exit" , code => { if (code === 0 ) { resolve(); } else { reject(); } }); }); };const isInstalled = packageName => { try { require .resolve(packageName); return true ; } catch (err) { return false ; } };const runCli = cli => { const path = require ("path" ); const pkgPath = require .resolve(`${cli.package} /package.json` ); const pkg = require (pkgPath); require (path.resolve(path.dirname(pkgPath), pkg.bin[cli.binName])); };const cli = { name: "webpack-cli" , package: "webpack-cli" , binName: "webpack-cli" , installed: isInstalled("webpack-cli" ), url: "https://github.com/webpack/webpack-cli" };if (!cli.installed) { const path = require ("path" ); const fs = require ("graceful-fs" ); const readLine = require ("readline" ); const notify = "CLI for webpack must be installed.\n" + ` ${cli.name} (${cli.url} )\n` ; console .error(notify); const isYarn = fs.existsSync(path.resolve(process.cwd(), "yarn.lock" )); const packageManager = isYarn ? "yarn" : "npm" ; const installOptions = [isYarn ? "add" : "install" , "-D" ]; console .error( `We will use "${packageManager} " to install the CLI via "${packageManager} ${installOptions.join( " " )} ".` ); const question = `Do you want to install 'webpack-cli' (yes/no): ` ; const questionInterface = readLine.createInterface({ input: process.stdin, output: process.stderr }); process.exitCode = 1 ; questionInterface.question(question, answer => { questionInterface.close(); const normalizedAnswer = answer.toLowerCase().startsWith("y" ); if (!normalizedAnswer) { console .error( "You need to install 'webpack-cli' to use webpack via CLI.\n" + "You can also install the CLI manually." ); return ; } process.exitCode = 0 ; console .log( `Installing '${ cli.package } ' (running '${packageManager} ${installOptions.join(" " )} ${ cli.package } ')...` ); runCommand(packageManager, installOptions.concat(cli.package)) .then(() => { runCli(cli); }) .catch(error => { console .error(error); process.exitCode = 1 ; }); }); } else { runCli(cli); }
webpack-cli入口文件,bin目录下的cli.js 代码如下: 主要拿到执行webpack命令时的参数,然后执行runCli,并检查是否安装了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 #!/usr/bin/env node 'use strict' ;require ('v8-compile-cache' );const importLocal = require ('import-local' );const runCLI = require ('../lib/bootstrap' );const { yellow } = require ('colorette' );const { error, success } = require ('../lib/utils/logger' );const { packageExists } = require ('../lib/utils/package-exists' );const { promptInstallation } = require ('../lib/utils/prompt-installation' );if (importLocal(__filename)) { return ; } process.title = 'webpack' ;const [, , ...rawArgs] = process.argv; if (packageExists('webpack' )) { runCLI(rawArgs); } else { promptInstallation('webpack -W' , () => { error(`It looks like ${yellow('webpack' )} is not installed.` ); }) .then(() => { success(`${yellow('webpack' )} was installed sucessfully.` ); runCLI(rawArgs); }) .catch(() => { error(`Action Interrupted, Please try once again or install ${yellow('webpack' )} manually.` ); process.exit(2 ); }); }
以上代码可以看到runCli函数是从*../lib/bootstrap*引入。这里代码较长,我们简化一下,保留主要内容:
1 2 3 4 const WebpackCLI = require ('./webpack-cli' );const cli = new WebpackCLI(); cli.run(...args);
所以主要文件是webpack-cli.js ,webpack-cli.js中run方法通过createCompiler创建了一个编译器,代码简化一下主要内容大致代码如下:
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 const webpack = packageExists('webpack' ) ? require ('webpack' ) : undefined ;const GroupHelper = require ('./utils/GroupHelper' );class WebpackCLI extends GroupHelper { createCompiler (options ) { let compiler; try { compiler = webpack(options); } catch (error) { const ValidationError = webpack.ValidationError ? webpack.ValidationError : webpack.WebpackOptionsValidationError; if (error instanceof ValidationError) { logger.error(error.message); } else { logger.error(error); } process.exit(2 ); } return compiler; } async run (args, cliOptions ) { const compiler = this .createCompiler(this .compilerConfiguration); } }
接下来又要进入webpack的主文件,lib目下的index.js index.js中就是各种文件导出合并,这里就随便粘贴了一点,可以看到引入了webpack.js然后做了一些处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const fn = lazyFunction(() => require ("./webpack" ));module .exports = mergeExports(fn, { get webpack () { return require ("./webpack" ); }, get validate () { const validateSchema = require ("./validateSchema" ); const webpackOptionsSchema = require ("../schemas/WebpackOptions.json" ); return options => validateSchema(webpackOptionsSchema, options); }, get validateSchema () { const validateSchema = require ("./validateSchema" ); return validateSchema; }, })
webpack.js内容: 这里也是简化之后的代码
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 const Compiler = require ("./Compiler" );const createCompiler = rawOptions => { const options = getNormalizedWebpackOptions(rawOptions); applyWebpackOptionsBaseDefaults(options); const compiler = new Compiler(options.context); compiler.options = options; new NodeEnvironmentPlugin({ infrastructureLogging: options.infrastructureLogging }).apply(compiler); if (Array .isArray(options.plugins)) { for (const plugin of options.plugins) { if (typeof plugin === "function" ) { plugin.call(compiler, compiler); } else { plugin.apply(compiler); } } } applyWebpackOptionsDefaults(options); compiler.hooks.environment.call(); compiler.hooks.afterEnvironment.call(); new WebpackOptionsApply().process(options, compiler); compiler.hooks.initialize.call(); return compiler; };
可以看到,这里引入了Compiler.js,在创建编译器的拿到配置项的plugins参数,遍历所有的插件,通过call或者apply将compiler传给plugin函数。 注意:⚠️这里并不是改变this指向的call或者apply,回想文章开头举的官方示例,可以看见,在插件内部定义了这个apply函数,并接收一个compiler参数!!!
Compiler.js 前面说到,在插件内部定义了apply函数并将compiler传递进去,回想文章开头的问题,插件为什么要写compiler.hooks.run.tap ,肯定也是在构造函数内部定义的。这里文件代码太长,同样简化一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const { SyncHook, SyncBailHook, AsyncParallelHook, AsyncSeriesHook } = require ("tapable" );class Compiler { constructor (context ) { this .hooks = Object .freeze({ run: new AsyncSeriesHook(["compiler" ]), }) } }
可以看到,run返回了一个*new AsyncSeriesHook([“compiler”])*,这个构造函数是取自于tapable。webpack官网在介绍plugins的时候说了这么一句话,Plugins are the backbone of webpack。这里的backbone就是tapable。
tapable的作用是各种钩子函数,当new一个钩子函数时可以在这个钩子实例的tap上加上各种任务,个人理解类似把所有任务先放进一个队列,在合适的条件下调用实例的call方法,则会执行队列里面的任务,具体怎么执行要看这个钩子函数是什么类型。这里随便选一种类型的函数写了一个demo:
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 const { SyncHook, SyncBailHook, SyncWaterfallHook, SyncLoopHook, AsyncParallelHook, AsyncParallelBailHook, AsyncSeriesHook, AsyncSeriesBailHook, AsyncSeriesWaterfallHook } = require ("tapable" ); let queue = new SyncHook(['name' ]); queue.tap('1' ,function (name ) { name = name + 'test' console .log('1' ,name); }) queue.tap('1' ,function (name ) { console .log('2' ,name); }) queue.tap('3' ,function (name ) { console .log('3' ,name); }) queue.call('webpack' )
这样一来前面的插件为什么那么写也就清楚了,就是把所有的插件通过钩子的tap方法放进一个任务队列,然后根据具体业务时机执行这个任务函数。
webpack中的compiler类作用:在webpack工作流程中,首先要通过compiler类来收集配置文件信息,然后指挥整体的编译流程,相当于通过compiler构建配置信息。
webpack工作流程。 Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程 :
初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。
开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译。
确定入口:根据配置中的 entry 找出所有的入口文件。
编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。
完成模块编译:在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系。
输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。
输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。
未完待续~,有时间再写