Webpack学习笔记(原理篇)

风子9418

共 6398字,需浏览 13分钟

 · 2021-11-16

从源码学习webpack打包原理

webpack启动过程分析

开始:从webpack命令行说起

通过npm script运行webpack

开发环境:npm run dev生产环境:npm run build

通过webpack直接运行

webpack entry.js bundle.js

这个过程中发生了什么?????

查找webpack入口文件

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

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

分析webpack的入口文件:webpack.js

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

启动后的结果

webpack最终找到webpack-cli(webpack-command)这个npm包,并且执行CLI

webpack-cli源码阅读

webpack-cli做的事情

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

从NON_COMPILATION_CMD分析出不需要编译的命令

webpack-cli 处理不需要经过编译的命令

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){returnrequire("./utils/prompt-command")(NON_COMPILATION_CMD,...process.argv);}

NON_COMPILATION_ARGS的内容

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帮助信息

edbedffb6533090b971a9604171d2307.webp

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: 通用参数(帮助命令、版本信息等)

webpack-cli执行的结果

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

Tapable插件架构与Hooks设计

webpack本质

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

核心对象Compiler继承Tapable

class Compiler extends Tapable {}

核心对象Compilation继承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类型

typefunction
Hook所有钩子的后缀

同步方法,但是它会传值给下一个函数
Bail熔断:当函数有任何返回值,就会在当前执行函数停止

监听函数返回true表示继续循环,返回undefined表示结束循环
Sync同步方法

异步串行钩子

异步并行执行钩子

Tapable 的使用-new Hook 新建钩子

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

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

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

Tapable的使用-钩子的绑定与执行

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

Async*Sync*
绑定:tabAsync/tabPromise/tap绑定:tap
执行:callAsync/Promise执行:call

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

const hook1 =newSyncHook(["arg1","arg2","arg3"]);//绑定事件到webapck事件流hook1.tap('hook1',(arg1, arg2, arg3)=> console.log(arg1, arg2, arg3))//1,2,3//执行绑定的事件hook1.call(1,2,3)

Tapable的使用-实际例子演示

定义一个 Car 方法,在内部 hooks 上新建钩子。分别是同步钩子 accelerate、brake( accelerate 接受一个参数)、异步钩子 calculateRoutes

使用钩子对应的绑定和执行方法

calculateRoutes 使用 tapPromise 可以返回一个 promise 对象

Tapable如何与webpack联系起来的?

if(Array.isArray(options)){    compiler =newMultiCompiler(options.map(options => webpack(options)));}elseif(typeof options ==="object"){    options =newWebpackOptionsDefaulter().process(options);    compiler =newCompiler(options.context);    compiler.options = options;newNodeEnvironmentPlugin().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();    compiler.options =newWebpackOptionsApply().process(options, compiler);}

模拟Compiler.js

module.exports =classCompiler{    constructor(){this.hooks ={            accelerate:newSyncHook(['newspeed']),            brake:newSyncHook(),            calculateRoutes:newAsyncSeriesHook(["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.js

constCompiler=require('./Compiler')classMyPlugin{    constructor(){}    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)=>{returnnewPromise((resolve, reject)=>{                setTimeout(()=>{                    console.log(`tapPromise to ${source} ${target} ${routesList}`)                    resolve();},1000)});});}}

模拟插件执行

const myPlugin =newMyPlugin();const options ={    plugins:[myPlugin]}const compiler =newCompiler();for(const plugin of options.plugins){if(typeof plugin ==="function"){        plugin.call(compiler, compiler);}else{        plugin.apply(compiler);}}compiler.run();

webpack流程

webpack的编译都按照下面的钩子调用顺序执行

entry-option(初始化option)=>run(开始编译)=>make(从entry开始递归的分析依赖,对每个依赖模块进行build)=>before-resolve(对模块位置进行解析)=>build-module(开始构建某个模块)=>normal-module-loader(将loader加载完成的module进行编译,生成AST树)=>program(遍历AST,当遇到require等一些调用表达式时,手机依赖)=>seal(所有依赖build完成,开始优化)=>emit(输出到dist目录)

WebpackOptionsApply

将所有的配置options参数转换成webpack内部插件

使用默认插件列表

举例:

output.library -> LibraryTrmplatePluginexternals -> ExternalsPlugindevtool -> EvalDevtoolModulePlugin, SourceMapDevToolPluginAMDPlugin, CommonJsPluginRemoveEmptyChunksPlugin

模块构建和chunk生成阶段

回顾Compller hooks:

流程相关

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

监听相关:

watch-runwatch-close

回顾Compilation:

Compiler调用Compilation生命周期方法

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

ModuleFactory

NormalModuleFactory ==> ModuleFactory <==ContextModuleFactory

Module

e3fcef9e162db4e4cd041287d98f0ae5.webp

NormalModule

Build:

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

Compilation hooks

fe251e3fc46631b34aea9a9af923507b.webp

Chunk生成算法

1、webpack先将entry中对应的module都生成一个新的chunk

2、遍历module的依赖列表,将依赖的module也加入到chunk中

3、如果一个依赖module是动态引入的模块,那么就会根据这个module创建一个新的chunk,继续遍历依赖

4、重复上面的过程,直至得到所有的chunks

文件生成

动手编写一个简易的webpack

模块化:增强代码可读性和维护性

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

常见几种模块化方式

ES moduleCJSAMD

AST基础知识

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

在线demo:https://esprima.org/demo/parse.html

实现功能列表

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

通过babylon生成AST通过babel-core将AST重新生成原发

可以分析模块之间的依赖关系

通过babel-traverse的ImportDeclaration方法获取依赖属性

生成的JS文件可以在浏览器中运行

demo项目地址:https://github.com/lsustc/study/tree/master/webpack%E5%AD%A6%E4%B9%A0/simplepack

浏览 129
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报