webpack流程WebpackOptionsA" />

📂 原理/源码

您所在的位置:网站首页 ios打包原理 📂 原理/源码

📂 原理/源码

#📂 原理/源码| 来源: 网络整理| 查看: 265

webpack 启动webpack-cli不需要编译的命令命令行工具包 yargs执行的结果webpack 的本质Tapable模拟 Compiler.js插件 my-plugin.js模拟插件执行">webpack 流程WebpackOptionsApply阶段Compiler hooksCompilationModuleFactoryModuleNormalModuleCompilation hooksChunk 生成算法模块化常见的几种模块化方式AST 基础知识webpack 的模块机制实现一个简易的 webpack

极客时间-玩转webpack

webpack 启动

webpack运行命令:

// 通过 npm scripts 运行 webpack开发环境: npm run dev生产环境:npm run build// 通过 webpack 直接运行webpack entry.js bundle.js

在命令行运行以上命令后,npm会让命令行工具进入node_modules\.bin目录查找是否存在webpack.sh 或者 webpack.cmd 文件,如果存在,就执行,不存在,就抛出错误。

实际的入口文件是:node_modules\webpack\bin\webpack.js:

//1. 正常执行返回process.exitCode = 0; //2. 运行某个命令const runCommand = (command, args) =>{...}; //3. 判断某个包是否安装 const isInstalled = packageName =>{...}; //4. webpack 可用的 CLI: webpack-cli(The original webpack full-featured CLI)和 // webpack-command(A lightweight, opinionated webpack CLI)const CLIs =[...]; //5. 判断是否两个 ClI 是否安装了const installedClis = CLIs.filter(cli => cli.installed);//6. 根据安装数量进行处理if (installedClis.length === 0){...}else if(installedClis.length === 1){...}else{...}

启动后的结果:webpack 最终找到 webpack-cli (webpack-command) 这个 npm 包,并且执行 CLI

webpack-cli

webpack-cli 做的事情:

引入yargs,对命令行进行定制分析命令行参数,对各个参数进行转换,组成编译配置项引用webpack,根据配置项进行编译和构

不需要编译的命令

webpack-cli 处理不需要经过编译的命令node_modules\webpack-cli\bin\cli.js:

const { NON_COMPILATION_ARGS } = require("./utils/constants");const NON_COMPILATION_CMD = process.argv.find(arg => { if (arg === "serve") { global.process.argv = global.process.argv.filter(a => a !== "serve"); process.argv = global.process.argv; } return NON_COMPILATION_ARGS.find(a => a === arg);});if (NON_COMPILATION_CMD) { return require("./utils/prompt-command")(NON_COMPILATION_CMD, ...process.argv);}

webpack-cli 提供的不需要编译的命令:

const NON_COMPILATION_ARGS = [ "init", // 创建一份 webpack 配置文件 "migrate",// 进行 webpack 版本迁移 "add", // 往webpack 配置文件中增加属性 "remove", // 往 webpack 配置文件中删除属性 "serve", /// 运行 webpack-serve "generate-loader",// 生成 webpack loader 代码 "generate-plugin", // 生成 webpack plugin 代码 "info"// 返回与本地环境相关的一些信息];

命令行工具包 yargs 提供命令和分组参数动态生成 help 帮助信息

webpack-cli 使用 args 分析参数分组 (config/config-args.js),将命令划分为9类:

Config options:配置相关参数(文件名称、运行环境等)Basic options:基础参数(entry设置、debug模式设置、watch监听设置、devtool设置)Module options:模块参数,给 loader 设置扩展Output options::输出参数(输出路径、输出文件名称)Advanced options:高级用法(记录设置、缓存设置、监听频率、bail等)Resolving options:解析参数(alias 和 解析的文件后缀设置)Optimizing options:优化参数Stats options:统计参数options:通用参数(帮助命令、版本信息等)

执行的结果 options = require("./utils/convert-argv")(argv); compiler = webpack(options);

webpack-cli 对配置文件和命令行参数进行转换最终生成配置选项参数 options,最终会根据配置参数实例化 webpack 对象,然后执行构建流程。

webpack 的本质

webpack 可以将其理解是一种基于事件流的编程范例,一系列的插件运行。

class Compiler extends Tapable {// ...}class Compilation extends Tapable {// ...}

Tapable

Tapable(水龙头)是一个类似于 Node.js 的 EventEmitter 的库, 主要是控制钩子函数的发布与订阅,控制着 webpack 的插件系统。

Tapable库暴露了很多 Hook(钩子)类,为插件提供挂载的钩子

const { SyncHook, // 同步钩子 SyncBailHook, // 同步熔断钩子 SyncWaterfallHook, // 同步流水钩子 SyncLoopHook, // 同步循环钩子 AsyncParallelHook, // 异步并发钩子 AsyncParallelBailHook, // 异步并发熔断钩子 AsyncSeriesHook, // 异步串行钩子 AsyncSeriesBailHook, / /异步串行熔断钩子 AsyncSeriesWaterfallHook // 异步串行流水钩子} = require("tapable");

Tapable hooks 类型

Tapable 的使用 -new Hook 新建钩子

Tapable 暴露出来的都是类方法,new 一个类方法获得我们需要的钩子

class 接受数组参数 options ,非必传。类方法会根据传参,接受同样数量的参数。

const hook1 = new SyncHook(["arg1", "arg2", "arg3"]);

Tapable 的使用-钩子的绑定与执行Tabpack 提供了同步&异步绑定钩子的方法,并且他们都有绑定事件和执行事件对应的方法。

Tapable 的使用-hook 基本用法示例

const { SyncHook} = require('tapable');const hook = new SyncHook(['arg1', 'arg2', 'arg3']);//绑定事件到 webapck事件流hook.tap('hook1', (arg1, arg2, arg3) => { console.log(arg1, arg2, arg3);});//执行绑定的事件hook.call(1, 2, 3); // 1 2 3

Tapable 的使用-实际例子演示

/** * 1.定义一个 Car 方法,在内部 hooks 上新建钩子。 * 分别是同步钩子 accelerate、brake( accelerate 接受一个参数)、 * 异步钩子 calculateRoutes * * 2.使用钩子对应的绑定和执行方法 * * 3.calculateRoutes 使用 tapPromise 可以返回一个 promise 对象 */const { SyncHook, AsyncSeriesHook} = require('tapable');class Car { constructor() { this.hooks = { accelerate: new SyncHook(['newspeed']), //加速 brake: new SyncHook(), //刹车 calculateRoutes: new AsyncSeriesHook(["source", "target", "routesList"]) //计算路径 } }}const myCar = new Car();//绑定同步钩子myCar.hooks.brake.tap("WarningLampPlugin", () => console.log('--WarningLampPlugin'));//绑定同步钩子 并传参myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`--Accelerating to ${newSpeed}`));//绑定一个异步Promise钩子myCar.hooks.calculateRoutes.tapPromise("calculateRoutes tapPromise", (source, target, routesList, callback) => { // return a promise return new Promise((resolve, reject) => { setTimeout(() => { console.log(`--tapPromise to ${source} ${target} ${routesList}`) resolve(); }, 1000) }) });myCar.hooks.brake.call();myCar.hooks.accelerate.call(10);console.time('cost');//执行异步钩子myCar.hooks.calculateRoutes.promise('Async', 'hook', 'demo') .then(() => { console.timeEnd('cost'); }, err => { console.error(err); console.timeEnd('cost'); });/** * 打印: * --WarningLampPlugin * --Accelerating to 10 * --tapPromise to Async hook demo * cost: 1.005s */

Tapable 是如何和 webpack 联系起来的?node_modules\webpack\lib\webpack.js:

if (Array.isArray(options)) { compiler = new MultiCompiler(options.map(options => webpack(options)));} else if (typeof options === "object") { options = new WebpackOptionsDefaulter().process(options); compiler = new Compiler(options.context); compiler.options = options; new NodeEnvironmentPlugin().apply(compiler); // plugins数组 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(); // 注入内部插件 compiler.options = new WebpackOptionsApply().process(options, compiler);} 插件必须有 apply 方法,接受 compiler 参数插件监听 compiler 上的 hooks,执行对应的动作

模拟 Compiler.jsconst { SyncHook, AsyncSeriesHook} = require('tapable');module.exports = class Compiler { constructor() { this.hooks = { accelerate: new SyncHook(['newspeed']), brake: new SyncHook(), calculateRoutes: new AsyncSeriesHook(["source", "target", "routesList"]) } } // 入口 run() { this.accelerate(10) this.break() this.calculateRoutes('Async', 'hook', 'demo') } accelerate(speed) { this.hooks.accelerate.call(speed); } break() { this.hooks.brake.call(); } calculateRoutes() { this.hooks.calculateRoutes.promise(...arguments).then(() => { }, err => { console.error(err); }); }}

插件 my-plugin.jsconst Compiler = require('./Compiler')class MyPlugin { constructor() { } // 监听 hook apply(compiler) { compiler.hooks.brake.tap("WarningLampPlugin", () => console.log('WarningLampPlugin')); compiler.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`)); compiler.hooks.calculateRoutes.tapPromise("calculateRoutes tapAsync", (source, target, routesList) => { return new Promise((resolve, reject) => { setTimeout(() => { console.log(`tapPromise to ${source} ${target} ${routesList}`) resolve(); }, 1000) }); }); }}

模拟插件执行// 模拟插件执行const myPlugin = new MyPlugin();const options = { plugins: [myPlugin]}const compiler = new Compiler();for (const plugin of options.plugins) { if (typeof plugin === "function") { plugin.call(compiler, compiler); } else { plugin.apply(compiler); //插件监听compiler的hooks }}compiler.run(); //开始编译,内部会触发一系列的hooks

webpack 流程

WebpackOptionsApply

将所有的配置 options 参数转换成 webpack 内部插件使用默认插件列表举例:

output.library -> LibraryTemplatePluginexternals -> ExternalsPlugindevtool -> EvalDevtoolModulePlugin, SourceMapDevToolPluginAMDPlugin, CommonJsPlugin

RemoveEmptyChunksPlug

阶段

准备阶段

打包构建阶段优化输出阶段

Compiler hooks

流程相关:

(before-)run(before-/after-)compilemake(after-)emitdone

监听相关:

watch-runwatch-close

Compilation

Compiler 调用 Compilation 生命周期方法

addEntry -> addModuleChainfinish (上报模块错误)seal(输出)

ModuleFactory

Module

NormalModule

Build

使用 loader-runner 运行 loaders通过 Parser 解析 (内部是 acron)ParserPlugins 添加依赖

Compilation hooks

模块相关:

build-modulefailed-modulesucceed-module

资源生成相关:

module-assetchunk-asset

优化和 seal相关:

(after-)sealoptimizeoptimize-modules(-basic/advanced)after-optimize-modulesafter-optimize-chunksafter-optimize-treeoptimize-chunk-modules(-basic/advanced)after-optimize-chunk-modulesoptimize-module/chunk-orderbefore-module/chunk-ids(after-)optimize-module/chunk-idsbefore/after-hash

Chunk 生成算法 webpack 先将 entry 中对应的 module 都生成一个新的 chunk2. 遍历 module 的依赖列表,将依赖的 module 也加入到 chunk 中3. 如果一个依赖 module 是动态引入的模块,那么就会根据这个 module 创建一个新的 chunk,继续遍历依赖4. 重复上面的过程,直至得到所有的 chunks

模块化

增强代码可读性和维护性

传统的网页开发转变成 Web Apps 开发代码复杂度在逐步增高部署时希望把代码优化成几个 HTTP 请求分离的 JS文件/模块,便于后续代码的维护性

常见的几种模块化方式

ES module

import * as largeNumber from 'large-number';// ...largeNumber.add('999', '1');

CJS

const largeNumbers = require('large-number');// ...largeNumber.add('999', '1');

AMD

require(['large-number'], function (large-number) { // ... largeNumber.add('999', '1');});

AST 基础知识

抽象语法树(abstract syntax tree 或者缩写为 AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。树上的每个节点都表示源代码中的一种结构。在线demo: https://esprima.org/demo/parse.html

webpack 的模块机制

打包出来的是一个 IIFE (匿名闭包)

modules 是一个数组,每一项是一个模块初始化函数通过 WEBPACK_REQUIRE_METHOD(0) 启动程序

__webpack_require 用来加载模块,返回 module.exports

实现一个简易的 webpack

可以将 ES6 语法转换成 ES5 的语法

通过 babylon 生成AST通过 babel-core 将AST重新生成源码 可以分析模块之间的依赖关系 通过 babel-traverse 的 ImportDeclaration 方法获取依赖属性 生成的 JS 文件可以在浏览器中运行


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3