Webpack

Webpack

webpack 流程

  1. webpack 使用 Tapable 作为事件中心,Tapable 可以定义、监听、触发事件
  2. webpack 将打包分成了 initialize -> run -> compiler -> compilation -> make -> afterCompile -> emit -> done 阶段
  3. webpack 在开始的时候就创建 Compiler 实例 compiler,将 webpack.config.js 的 options 赋值给 compiler.options
  4. 之后经历从 environment 到 initialize 阶段,之后调用 compiler 的 run 方法
  5. 在 compiler.compile 方法会创建 NormalModuleFactory + ContextModuleFactory 实例
  6. 在 compiler.compile 阶段创建 Compilation 实例
  7. EntryPlugin 监听了 make 事件,获取到入口文件路径,调用 compilation.addEntry,将路径传递给 compilation
  8. ------- compilation 拿到入口文件之后开始工作 ----------
  9. 广播 compilation addEntry 事件
  10. compilation 调用 NormalModuleFactory.create 方法,创建 module
  11. module 是 NormalModule 的实例,调用 module.build 方法,开始 build
  12. ------- module 是 NormalModule 的实例,多态,调用到 NormalModule.build 方法 ----------
  13. ------- parse ----------
  14. NormalModule.build 方法调用了 runLoaders,使用 loader 去解析文件,获取到 Buffer 数组,处理之后就可以拿到文件内容,赋值给 normal._source
  15. 使用 parser 去解析文件内容 => NormalModule 中的 parser 是 JavaScriptParser 的实例,所以调用 JavaScriptParser.parse 解析
  16. JavaScriptParser 使用了 acorn 第三方库去解析 code 到 ast
  17. traverse ast,得到所有的 import 语句,一旦发现 importDeclaration 则触发 JavaScriptParser.hooks.import 钩子,对应的监听函数就会处理依赖
  18. ------- 得到了 _source(源代码) 和 ast -----------
  19. ------- DFS process dependencies => module chain(compilation.modules) -------------
  20. 之后 DFS 处理 module 的 dependencies,所有的 module 形成一个链,此时 module 全部创建完成
  21. module chain 是一个数组,挂载在 compilation.modules 属性上,是一个 Set
  22. 广播 compilation succeedEntry 事件
  23. ------ compilation 完成事情 => compilation 根据入口文件获取到所有的 module ----------
  24. ------ EntryPlugin 调用的方法 compilation.addEntry 完成 -----------------
  25. ------ 进入下一个阶段 ------------------
  26. 广播 compiler.finishMake 事件
  27. ------ code generator => compilation.assets ----------
  28. 调用 compilation.seal 方法,将 compilation.modules 遍历生成一个 CacheSource
  29. 将所有的要输出的代码放置在 CacheSource._children 中
  30. 此时的 cacheSource 是一个数组,内容就是打包后的 main.js
  31. 将上述的 CacheSource 实例挂载到 compilation.assets['main.js']._source 上
  32. 广播 compiler.afterCompile 事件
  33. ----- 此时完成了 compiler.compile 方法执行 ------------
  34. 调用 compiler.emitAssets 方法触发 compiler.emit 事件
  35. 在 compiler.emit 事件回调中创建 dist 文件夹
  36. 之后调用 emitFiles 创建文件,文件来自于 compilation.assets
  37. 之后调用 doWrite 进行具体地写文件操作,之后触发 compiler.assetEmitted 事件
  38. ----- 完成了 compiler.emitAssets 方法,进入回调 ----
  39. 调用 compiler.emitRecords 方法
  40. compiler.emitRecords 方法回调中触发 compiler.done 事件

概述

AST

  • AST => Abstract Syntax Tree => 抽象语法树

Babel

babel 原理

  1. parse => 把代码 code 变成 AST
  2. traverse => 遍历 AST 进行修改
  3. generate => 把 AST 变成代码 code2
  4. code --1--> ast --2--> ast2 --3--> code2
let to var
  1. 使用 @babel/core 和 @babel/preset-env 可以将代码转为 ES5

将转化后的代码输出到一个文件中

读取文件(test.js),并将文件中的代码转化(file_to_es5.ts)之后写入另一个文件(test.es5.js)

工具

  1. babel 可以把高级代码翻译成 ES5
  2. @babel/parser
  3. @babel/traverse
  4. @babel/generator
  5. @babel/core 包含前三者
  6. @babel/preset-env 内置很多规则

依赖

分析 JS 文件的依赖关系

  1. deps_1.ts + project_1
  2. node -4 ts-node/register deps_1.ts

递归的分析嵌套依赖

  1. 依赖关系
  2. DFS
  3. deps_2.ts + project_2

静态分析循环依赖

  1. deps_3.ts + project_3
  2. node -r ts-node/register deps_3.ts
  3. Error: 栈溢出

解决循环依赖

  1. deps_4.ts + project_4
  2. 如果分析过了,就直接返回
  3. 静态分析 => 只分析依赖,不执行代码
  4. node project_4/index.js => 执行代码 => ReferenceError: Cannot access 'a' before initialization

总结

  1. 模块间可以循环依赖
  2. 代码不能有逻辑漏洞
  3. 没有逻辑漏洞的循环依赖 => project_5
  4. 最佳实践 => 不要使用循环依赖

打包 bundle

  1. 浏览器不能直接运行 ES6+ 代码
  2. 浏览器功能不同
    1. 现代浏览器可以通过 <script type=module> 来支持 import/export
    2. IE 8-15 不支持 import export,所以不能运行
  3. 把关键字转译为普通代码,并把所有文件打包成一个文件 => 需要编写复杂的代码来完成这件事情

把关键字转译成普通代码

  • @babel/core 可以实现
  • bundler_1.ts
  • node -r ts-node/register bundler_1.ts
// a.js => ES5 
// 将 import/export 转化成函数
  1. import 关键字会变成 require 函数
  2. export 关键字会变成 exports 对象
  3. 本质 => ESModule 语法变成了 CommonJS 规则

将所有文件打包成一个文件

  • 包含了所有模块,并可以执行所有模块 => 执行入口文件

depRelation 转化成数组

  • bundler_2.ts

code 转化成函数

  1. 在 code 字符串外面包一个 function(require, module, exports) {...}
  2. 把 code 写到文件里,引号不会出现在文件中
  3. case:
    code = `var b = require('./b.js'); exports.default = 'a'`;
    code2 = ` function(require, module, exports){ ${code} } ` // 此时 code2 还是字符串
    // 然后把 ` {code: ${code2}} ` 写入最终文件中
    // 最终文件里的 code 就是函数了
    

完成 execute

  • 执行入口文件
const modules = {} // modules 用于缓存所有模块
function execute(key) {
  // key => index.js/a.js
  if (modules[key]) {
    return modules[key];
  }
  var item = depRelation.find(dep => dep.key === key);
  var require = (path) => {
    // 可能导入了其他的文件,所以需要将 execute 传递出去
    // 输入一个文件路径,拿到路径去执行这个文件
    return execute(pathToKey(path));
  }
  modules[key] = {__esModule: true}; // 标识这个模块是 ES 模块 => 自己有 default,不需要添加 default
  var module = {exports: modules[key]};

  // 执行一个文件的代码,传入导入其他模块的方法(require) + 如何导出自身模块(导出的东西放置在 module.exports 对象中)
  item.code(require, module, module.exports); // 调用 code 转化的函数
  return modules[key]
}

如何得到最终文件

拼凑出字符串,然后写入文件

  1. var dist = "";
  2. dist += content;
  3. writeFileSync("dist.js", dist);

自动创建最终文件

  • bundler_3.ts => writeFileSync('dist_2.js', generateCode())
  • node -r ts-node/register bundler_3.ts
  • node dist_2.js
  • bundler_3.ts 就是一个简易打包器

目前还存在的问题

  1. 生成的代码中有多个重复的 _interopXXX 函数
  2. 只能引入和运行 JS 文件
  3. 只能理解 import,无法理解 require
  4. 不支持插件
  5. 不支持配置入口文件和 dist 文件名

Loader

  1. bundler 目前只能加载 JS
  2. 要加载 CSS
  3. 把 CSS 变为 JS,就可以加载 CSS 了

将 css 转化为字符串

  1. 校验是否是 css
  2. css => body {color: red}
  3. => const str = "body {color: red}"; export default str;
  4. 使用 JS 生成 style 标签,将 str 写入标签中
  5. 将 style 标签 append 到 head 里

loader

  1. 一个 loader 可以是一个普通函数
function transform(code) {
  const code2 = doSomething(code);
  return code2;
}

module.exports = transform // 兼容 Node.js
  1. 一个 loader 也可以是一个异步函数
async function transform(code) {
  const code2 = await doSomething(code);
  return code2;
}

module.exports = transform // 兼容 Node.js
  1. 使用 require 不用 import
    1. 方便动态加载 => loader 名字是从配置中读取的
    2. 旧版 Node 不支持 import

优化 => 单一职责原则

  • webpack 里面每个 loader 只做了一件事情
  • 上面的 css-loader 做了两件事情
    1. 把 CSS 变为 JS 字符串 => css-loader => 将 css 转译为 js
    2. 把 JS 字符串放到 style 标签中 => style-loader => 不是转译

row-loader

  • webpack 提供 loader-utils 和 schema-utils 作为辅助工具
  • webpack 通过 this 来传递上下文
  • getOptions(this) 可以获取 options
  • validate 可以验证 options 是否合法

css-loader

  • this.async() 用于获取回调

Webpack 的 loader 是什么

  • webpack 自带的打包器只能支持 JS 文件
  • 如果要加载其他文件,就需要使用 loader
  • loader 的原理就是把文件内容包装成能运行的 JS
  • case:加载 css 时需要用到 style-loader 和 css-loader
  • css-loader 把代码从 css 代码变成 export default str 形式的 js 代码
  • style-loader 把代码挂载到 head 里的 style 标签里面

Webpack 源码

webpack-cli 是如何调用 webpack 的

问:在 demo 目录运行 webpack-cli,会自动把 src/index.js 打包为 dist/main.js,webpack-cli 是如何调用 webpack 的?
答:1. webpack = require('webpack') 2. compiler = webpack(options, callback)
webpack-cli 通过上面两个语句来调用 webpack

webpack 是如何分析 index.js 的

  • 打包器需要先分析并收集依赖,然后打包成一个文件,那么 webpack 是怎么做这件事情的
  1. 创建了 compiler 对象
  2. 之后调用了 hooks.xxx.call => 卡死了

Tapable

  • webpack 团队为了写 webpack 而写的一个事件/钩子库
  • 定义一个事件/钩子 => this.hooks.eventName = new SyncHook(["arg1", "arg2"])
  • 监听一个事件/钩子 => this.hooks.eventName.tap('监听理由', fn)
  • 触发一个事件/钩子 => this.hooks.eventName.call("arg1", "arg2")

webpack 的流程是怎样的

  • webpack 把打包分为了哪几个阶段(事件/钩子)
  • 至少有 env -> init -> run -> beforeCompile -> compile -> compilation -> make -> finishMake -> afterCompile -> emit 这几个钩子

webpack 读取 index.js 并分析和收集依赖是在哪个阶段

factory.create(nmf.create)做了什么

nmf.create 得到了一个 module 对象(路径对应的文件的内容),之后 addModule 和 buildModule

addModule 做了什么?

把 module 添加到 compilation.modules 里面,并且还通过检查 id 防止重复添加

buildModule 做了什么?

  1. 调用了 module.build()
  2. NormalModule.js 查看 build 源码 => 调用了 runLoaders
  3. 然后来到了 processResult(),发现了 _source 和 _ast

webpack 是如何分析 index.js 的

  1. runLoaders 读取源代码
  2. 之后将源代码变成 _ast

webpack 如何知道 index.js 依赖了哪些文件的

  1. webpack 会对 index.js 进行 parse 得到 ast,那么接下来 webpack 应该会 traverse 这个 ast,寻找 import 语句,那么相关代码在哪里?
  2. 其中 blockPreWalkStatement() 对 importDeclaration 进行检查
  3. 一旦发现 import 'xxx' 就会触发 import 钩子,对应的监听函数会处理依赖
  4. 其中 walkStatements() 对 ImportExpression 进行了检查
  5. 一旦发现 import('xxx') 就会触发 importCall 钩子,对应的监听函数会处理依赖

webpack 拿到了所以的依赖关系以及对应的文件,如果把这面 modules 合并成一个文件

  1. compilation.seal() => 该函数会创建 chunks,为每个 chunk 进行 codeGeneration,然后为每个 chunk 创建 asset
  2. seal() 之后,emitAssets()、emitFiles() 会创建文件
  3. 最终得到 dist/main.js 和其他 chunk 文件

总结

  • webpack 使用 hooks 把主要阶段固定下来
  • 插件可以绑定到任意的阶段
  • 入口插件(EntryPlugin.js)处理入口文件,默认入口 ./src
  • make -> compiler -> compilation -> entry -> dep -> module
  • webpack 怎么分析依赖和打包的?
    1. 使用 JavascriptParser 对 index.js 进行 parse 得到 ast,然后遍历 ast
    2. 发现依赖声明就将其添加到 module 的 dependencies 或 blocks 中
    3. seal 阶段,webpack 将 module 转为 chunk,可能会把多个 module 通过 codeGeneration 合并为一个 chunk
    4. seal 之后,为每个 chunk 创建文件,并写到硬盘上

面试

  1. webpack 使用 Tapable 作为事件中心,将打包分为 env、compile、make、seal、emit 几个阶段
  2. 在 make 阶段借助 acorn 对源代码进行了 parse

webpack 流程

init -> run -> compile -> compilation -> make -> afterCompile -> seal(对代码进行一些封装) -> codeGenerate(生成最终的文件) -> emit(emit
一个文件) -> done

Plugin

imagemin-webpack-plugin

  1. 监听 emit 事件
  2. 回调函数会对 compilation.assets 进行检测,如果是图片就优化,之后替换图片

clean-webpack-plugin

  1. 监听 emit 事件,清空 dist
  2. 监听 done 事件,删除 assets 之外的文件

DefinePlugin

  1. 监听 compilation 事件
  2. 获取 normalModuleFactory,监听 parser 事件,
  3. parser 遍历 ast 的时候,如果发现有使用定义的全局变量,那么就将全局变量添加进 dependency 中

知识点

  1. Node 运行 TS 代码 => node -r ts-node/register <file>
  2. Node 运行 TS 代码并运行在 Chrome 中 => node -r ts-node/register --inspect-brk <file>
  3. 图片相关 loader 工作原理思路
    1. 如果发现是图片,则把图片放入一个 public/assets 文件中,之后导出相对路径
    2. 如果发现是图片,并且文件很小,转化成 base64 编码
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342