webpack | 2020-06-09 16:33:41 609次 1次
一、webpack.js执行
从 webpack-cli 中引入了 webpack,这个方法由 webpack 包中的webpack.js 暴露出,接受一个配置项和回调函数。这里面主要做的就是实例化一个 compiler,其他的就是一些错误检查、海量默认插件的导出、用户配置的插件调用、对文件系统做了一些封装(输入,输出,缓存,监听等等)并挂载在 compiler 对象下。
const webpack = (options, callback) => { //错误校验 const webpackOptionsValidationErrors = validateSchema( webpackOptionsSchema, options ); if (webpackOptionsValidationErrors.length) { throw new WebpackOptionsValidationError(webpackOptionsValidationErrors); } let compiler; if (Array.isArray(options)) { //如果传入的配置是数组,则创建多个compiler(compiler包含compiler与watching两个对象), //这里先不做分析 compiler = new MultiCompiler( Array.from(options).map(options => webpack(options)) ); } else if (typeof options === "object") { //默认参数 options = new WebpackOptionsDefaulter().process(options); //实例化 Compiler 对象 options.context代表当前运行webpack路径 compiler = new Compiler(options.context); compiler.options = options; //基于node api封装文件操作 new NodeEnvironmentPlugin({ infrastructureLogging: options.infrastructureLogging }).apply(compiler); // 配置了插件则在这里调用 if (options.plugins && Array.isArray(options.plugins)) { for (const plugin of options.plugins) { if (typeof plugin === "function") { plugin.call(compiler, compiler); } else { plugin.apply(compiler); } } } compiler.hooks.environment.call(); compiler.hooks.afterEnvironment.call(); //这个模块主要是根据options选项的配置,设置compile的相应的插件,属性, compiler.options = new WebpackOptionsApply().process(options, compiler); } else { throw new Error("Invalid argument: options"); } if (callback) { ... //没有用到 compiler.run(callback); } return compiler; };
下面介绍这个文件中用到的其他几个大的模块方法。
二、validateSchema错误检测
const webpackOptionsValidationErrors = validateSchema( webpackOptionsSchema, options );
第一个参数是对 option 配置文件做一个静态检查,如果数据格式不符合规定则给出提示,第二个参数就是用户传入的配置项。
const validateSchema = (schema, options) => { if (Array.isArray(options)) { ... } else { return validateObject(schema, options); } };
这里会发现 validateSchema 被调用了两次,既然它是对配置文件进项校验的,那么我们应该联想到在 cli 那个包下主要处理的命令行一些参数配置,所以在 convert-args 文件下的 processConfiguredOptions 方法中也需要调用校验一次。
validateObject 方法中通过 ajv.compile 传入配置项,进行校验,校验通过给出一个空数组,反之给出错误信息:
const validateObject = (schema, options) => { const validate = ajv.compile(schema); const valid = validate(options); return valid ? [] : filterErrors(validate.errors); };
然后回到 webpack.js 中判断如果有错误则抛出 WebpackOptionsValidationError 错误。
三、WebpackOptionsDefaulter
配置的参数校验无误之后,预置默认一些配置 WebpackOptionsDefaulter 继承自 OptionsDefaulter,所以 webpack 对用户来说是可以零配置的,OptionsDefaulter 中含有 set 方法记录一些预置配置操作,process 进行真正处理。
class WebpackOptionsDefaulter extends OptionsDefaulter { constructor(){ super(); this.set("entry", "./src"); ... } }
这个方法中都是 set 操作,但是会有默认、call、make、append 四种操作类型,看下 OptinsDefaulter:
class OptionsDefaulter { constructor() { this.defaults = {}; this.config = {}; } process(options) { options = Object.assign({}, options); for (let name in this.defaults) { switch (this.config[name]) { case undefined: if (getProperty(options, name) === undefined) { setProperty(options, name, this.defaults[name]); } break; case "call": setProperty( options, name, this.defaults[name].call(this, getProperty(options, name), options) ); break; ... } } return options; } //给子类使用 set(name, config, def) { if (def !== undefined) { this.defaults[name] = def; this.config[name] = config; } else { this.defaults[name] = config; delete this.config[name]; } } } module.exports = OptionsDefaulter;
通过 new WebpackOptionsDefaulter().process(options);的调用,返回一个包装后的 options,这里面最频繁使用的两个函数就是 getProperty 和 setProperty,因为 webpack 的配置有时候需要多层对象,那么这两个方法的处理就有点意思了,比如我们之前判断一个对象中的某个属性是否存在或是否有值,会使用 obj.name 看看它的值判断是否存在,这样的话在 webpack 的基类中处理起来就相当不方便了,所以,它是通过字符串拼接起来然后截取为数组再判断,这样省去写一个麻烦的代码:
const getProperty = (obj, path) => { let name = path.split("."); for (let i = 0; i < name.length - 1; i++) { obj = obj[name[i]]; if (typeof obj !== "object" || !obj || Array.isArray(obj)) return; } return obj[name.pop()]; }; console.log(getProperty({ a: { b: { c: [1, 2, 3] } } }, 'a.b.c'))
const setProperty = (obj, path, value) => { let name = path.split("."); for (let i = 0; i < name.length - 1; i++) { if (typeof obj[name[i]] !== "object" && obj[name[i]] !== undefined) return; if (Array.isArray(obj[name[i]])) return; if (!obj[name[i]]) obj[name[i]] = {}; obj = obj[name[i]]; } obj[name.pop()] = value; }; let obj = {} setProperty(obj, 'a.b.c', 111) console.log(obj)
四、NodeEnvironmentPlugin
该类主要对文件系统做了一些封装,包括输入,输出,缓存,监听等等,这些扩展后的方法全部挂载在 compiler 对象下。接受一个 infrastructureLogging(日志输出)参数,这个可以自定义配置或使用默认的。
class NodeEnvironmentPlugin { ... apply(compiler) { //命令面板中日志记录 compiler.infrastructureLogger = createConsoleLogger( Object.assign( { level: "info", debug: false, console: nodeConsole }, this.options.infrastructureLogging ) ); // 可以缓存输入的文件系统 compiler.inputFileSystem = new CachedInputFileSystem( new NodeJsInputFileSystem(), 60000 ); const inputFileSystem = compiler.inputFileSystem; // 输出文件系统 compiler.outputFileSystem = new NodeOutputFileSystem(); // 监视文件系统 compiler.watchFileSystem = new NodeWatchFileSystem( compiler.inputFileSystem ); // 添加事件流before-run compiler.hooks.beforeRun.tap("NodeEnvironmentPlugin", compiler => { if (compiler.inputFileSystem === inputFileSystem) inputFileSystem.purge(); }); } } module.exports = NodeEnvironmentPlugin;
这些基本上都是在 compiler(这个方法留到 tapable 之后再分析)对象上挂载 node 的 fs 文件系统,比如输出文件系统:
const fs = require("fs"); const path = require("path"); const mkdirp = require("mkdirp"); class NodeOutputFileSystem { constructor() { this.mkdirp = mkdirp; this.mkdir = fs.mkdir.bind(fs); this.rmdir = fs.rmdir.bind(fs); this.unlink = fs.unlink.bind(fs); this.writeFile = fs.writeFile.bind(fs); this.join = path.join.bind(path); } } module.exports = NodeOutputFileSystem;
五、WebpackOptionsApply
这个模块主要是根据 options 配置,设置 compile 的相应的插件、属性,里面写了大量的 apply(compiler); 使得模块的 this 指向 compiler,没有对 options 做任何处理。其中根据 target 对环境部署配置进行相应操作,默认为 web 环境部署。
这个里面加载了大量插件并触发事件流:
① 根据 options.target 加载对应的插件,如果配置文件没有配置该参数,则在 WebpackOptionsDefaulter 模块会被自动初始化为 web。
② 处理 options.output.library、options.output.externals 参数
③ 处理 options.devtool 参数
④ 加载 EntryOptionPlugin 插件并触发 entry-option 的事件流
⑤ 加载大量插件
⑥ 处理 options.performance 参数
⑦ 加载 TemplatePathPlugin、RecordIdPlugin、WarnCaseSensitiveModulesPlugin 插件
⑧ 触发 after-plugins 事件流
⑨ 设置 compiler.resolvers 的值
⑩ 触发 after-resolvers 事件流
六、导出配置
最后往 webpack 上挂载一些方法:
webpack.WebpackOptionsDefaulter = WebpackOptionsDefaulter; webpack.WebpackOptionsApply = WebpackOptionsApply; webpack.Compiler = Compiler; webpack.MultiCompiler = MultiCompiler; webpack.NodeEnvironmentPlugin = NodeEnvironmentPlugin; // @ts-ignore Global @this directive is not supported webpack.validate = validateSchema.bind(this, webpackOptionsSchema); webpack.validateSchema = validateSchema; webpack.WebpackOptionsValidationError = WebpackOptionsValidationError;
然后给 export 上挂载海量插件:
const exportPlugins = (obj, mappings) => { for (const name of Object.keys(mappings)) { Object.defineProperty(obj, name, { configurable: false, //不可删除 enumerable: true, get: mappings[name] }); } }; exportPlugins(exports, { AutomaticPrefetchPlugin: () => require("./AutomaticPrefetchPlugin"), ... }
这样以后用的时候,可以直接从 webpack 这个文件引入了,并且是按需加载的。
对 webpack 这个文件有个大致了解后,接下来在进入 Compiler 模块之前先看下 Tapable 的使用以及分析。
1人赞