目录

什么是 webpack ?

webpack 核心概念

Entry

Output

Module

Chunk

Loader

Plugin

webpack 构建流程

实践加深理解,撸一个简易 webpack

1. 定义 Compiler 类

2. 解析入口文件,获取 AST

3. 找出所有依赖模块

4. AST 转换为 code

5. 递归解析所有依赖项,生成依赖关系图

6. 重写 require 函数,输出 bundle

7. 看完这节,彻底搞懂 bundle 实现

总结

参考


什么是 webpack ?

本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。

webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。 插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。
webpack 通过 Tapable 来组织这条复杂的生产线。 webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。 webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。 -- 深入浅出 webpack 吴浩麟

webpack 核心概念

Entry

入口起点(entry point)指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。

进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。

每个依赖项随即被处理,最后输出到称之为 bundles 的文件中。

Output

output 属性告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件,默认值为 ./dist。

基本上,整个应用程序结构,都会被编译到你指定的输出路径的文件夹中。

Module

模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。

Chunk

代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。

Loader

loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。

loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。

本质上,webpack loader 将所有类型的文件,转换为应用程序的依赖图(和最终的 bundle)可以直接引用的模块。

Plugin

loader 被用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。

插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件接口功能极其强大,可以用来处理各种各样的任务。

webpack 构建流程

Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程 :

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译。
  3. 确定入口:根据配置中的 entry 找出所有的入口文件。
  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。
  5. 完成模块编译:在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系。
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。

实践加深理解,撸一个简易 webpack

1. 定义 Compiler 类

class Compiler {constructor(options) {// webpack 配置const { entry, output } = options// 入口this.entry = entry// 出口this.output = output// 模块this.modules = []}// 构建启动run() {}// 重写 require函数,输出bundlegenerate() {}
}

2. 解析入口文件,获取 AST

我们这里使用@babel/parser,这是 babel7 的工具,来帮助我们分析内部的语法,包括 es6,返回一个 AST 抽象语法树。

// webpack.config.jsconst path = require('path')
module.exports = {entry: './src/index.js',output: {path: path.resolve(__dirname, './dist'),filename: 'main.js'}
}
//
const fs = require('fs')
const parser = require('@babel/parser')
const options = require('./webpack.config')const Parser = {getAst: path => {// 读取入口文件const content = fs.readFileSync(path, 'utf-8')// 将文件内容转为AST抽象语法树return parser.parse(content, {sourceType: 'module'})}
}class Compiler {constructor(options) {// webpack 配置const { entry, output } = options// 入口this.entry = entry// 出口this.output = output// 模块this.modules = []}// 构建启动run() {const ast = Parser.getAst(this.entry)}// 重写 require函数,输出bundlegenerate() {}
}new Compiler(options).run()

3. 找出所有依赖模块

Babel 提供了@babel/traverse(遍历)方法维护这 AST 树的整体状态,我们这里使用它来帮我们找出依赖模块。

const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').defaultconst Parser = {getAst: path => {// 读取入口文件const content = fs.readFileSync(path, 'utf-8')// 将文件内容转为AST抽象语法树return parser.parse(content, {sourceType: 'module'})},getDependecies: (ast, filename) => {const dependecies = {}// 遍历所有的 import 模块,存入dependeciestraverse(ast, {// 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)ImportDeclaration({ node }) {const dirname = path.dirname(filename)// 保存依赖模块路径,之后生成依赖关系图需要用到const filepath = './' + path.join(dirname, node.source.value)dependecies[node.source.value] = filepath}})return dependecies}
}class Compiler {constructor(options) {// webpack 配置const { entry, output } = options// 入口this.entry = entry// 出口this.output = output// 模块this.modules = []}// 构建启动run() {const { getAst, getDependecies } = Parserconst ast = getAst(this.entry)const dependecies = getDependecies(ast, this.entry)}// 重写 require函数,输出bundlegenerate() {}
}new Compiler(options).run()

4. AST 转换为 code

将 AST 语法树转换为浏览器可执行代码,我们这里使用@babel/core 和 @babel/preset-env。

const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')const Parser = {getAst: path => {// 读取入口文件const content = fs.readFileSync(path, 'utf-8')// 将文件内容转为AST抽象语法树return parser.parse(content, {sourceType: 'module'})},getDependecies: (ast, filename) => {const dependecies = {}// 遍历所有的 import 模块,存入dependeciestraverse(ast, {// 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)ImportDeclaration({ node }) {const dirname = path.dirname(filename)// 保存依赖模块路径,之后生成依赖关系图需要用到const filepath = './' + path.join(dirname, node.source.value)dependecies[node.source.value] = filepath}})return dependecies},getCode: ast => {// AST转换为codeconst { code } = transformFromAst(ast, null, {presets: ['@babel/preset-env']})return code}
}class Compiler {constructor(options) {// webpack 配置const { entry, output } = options// 入口this.entry = entry// 出口this.output = output// 模块this.modules = []}// 构建启动run() {const { getAst, getDependecies, getCode } = Parserconst ast = getAst(this.entry)const dependecies = getDependecies(ast, this.entry)const code = getCode(ast)}// 重写 require函数,输出bundlegenerate() {}
}new Compiler(options).run()

5. 递归解析所有依赖项,生成依赖关系图

const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')const Parser = {getAst: path => {// 读取入口文件const content = fs.readFileSync(path, 'utf-8')// 将文件内容转为AST抽象语法树return parser.parse(content, {sourceType: 'module'})},getDependecies: (ast, filename) => {const dependecies = {}// 遍历所有的 import 模块,存入dependeciestraverse(ast, {// 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)ImportDeclaration({ node }) {const dirname = path.dirname(filename)// 保存依赖模块路径,之后生成依赖关系图需要用到const filepath = './' + path.join(dirname, node.source.value)dependecies[node.source.value] = filepath}})return dependecies},getCode: ast => {// AST转换为codeconst { code } = transformFromAst(ast, null, {presets: ['@babel/preset-env']})return code}
}class Compiler {constructor(options) {// webpack 配置const { entry, output } = options// 入口this.entry = entry// 出口this.output = output// 模块this.modules = []}// 构建启动run() {// 解析入口文件const info = this.build(this.entry)this.modules.push(info)this.modules.forEach(({ dependecies }) => {// 判断有依赖对象,递归解析所有依赖项if (dependecies) {for (const dependency in dependecies) {this.modules.push(this.build(dependecies[dependency]))}}})// 生成依赖关系图const dependencyGraph = this.modules.reduce((graph, item) => ({...graph,// 使用文件路径作为每个模块的唯一标识符,保存对应模块的依赖对象和文件内容[item.filename]: {dependecies: item.dependecies,code: item.code}}),{})}build(filename) {const { getAst, getDependecies, getCode } = Parserconst ast = getAst(filename)const dependecies = getDependecies(ast, filename)const code = getCode(ast)return {// 文件路径,可以作为每个模块的唯一标识符filename,// 依赖对象,保存着依赖模块路径dependecies,// 文件内容code}}// 重写 require函数,输出bundlegenerate() {}
}new Compiler(options).run()

6. 重写 require 函数,输出 bundle

const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')const Parser = {getAst: path => {// 读取入口文件const content = fs.readFileSync(path, 'utf-8')// 将文件内容转为AST抽象语法树return parser.parse(content, {sourceType: 'module'})},getDependecies: (ast, filename) => {const dependecies = {}// 遍历所有的 import 模块,存入dependeciestraverse(ast, {// 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)ImportDeclaration({ node }) {const dirname = path.dirname(filename)// 保存依赖模块路径,之后生成依赖关系图需要用到const filepath = './' + path.join(dirname, node.source.value)dependecies[node.source.value] = filepath}})return dependecies},getCode: ast => {// AST转换为codeconst { code } = transformFromAst(ast, null, {presets: ['@babel/preset-env']})return code}
}class Compiler {constructor(options) {// webpack 配置const { entry, output } = options// 入口this.entry = entry// 出口this.output = output// 模块this.modules = []}// 构建启动run() {// 解析入口文件const info = this.build(this.entry)this.modules.push(info)this.modules.forEach(({ dependecies }) => {// 判断有依赖对象,递归解析所有依赖项if (dependecies) {for (const dependency in dependecies) {this.modules.push(this.build(dependecies[dependency]))}}})// 生成依赖关系图const dependencyGraph = this.modules.reduce((graph, item) => ({...graph,// 使用文件路径作为每个模块的唯一标识符,保存对应模块的依赖对象和文件内容[item.filename]: {dependecies: item.dependecies,code: item.code}}),{})this.generate(dependencyGraph)}build(filename) {const { getAst, getDependecies, getCode } = Parserconst ast = getAst(filename)const dependecies = getDependecies(ast, filename)const code = getCode(ast)return {// 文件路径,可以作为每个模块的唯一标识符filename,// 依赖对象,保存着依赖模块路径dependecies,// 文件内容code}}// 重写 require函数 (浏览器不能识别commonjs语法),输出bundlegenerate(code) {// 输出文件路径const filePath = path.join(this.output.path, this.output.filename)// 懵逼了吗? 没事,下一节我们捋一捋const bundle = `(function(graph){function require(module){function localRequire(relativePath){return require(graph[module].dependecies[relativePath])}var exports = {};(function(require,exports,code){eval(code)})(localRequire,exports,graph[module].code);return exports;}require('${this.entry}')})(${JSON.stringify(code)})`// 把文件内容写入到文件系统fs.writeFileSync(filePath, bundle, 'utf-8')}
}new Compiler(options).run()

7. 看完这节,彻底搞懂 bundle 实现

我们通过下面的例子来进行讲解,先死亡凝视 30 秒

;(function(graph) {function require(moduleId) {function localRequire(relativePath) {return require(graph[moduleId].dependecies[relativePath])}var exports = {};(function(require, exports, code) {eval(code)})(localRequire, exports, graph[moduleId].code)return exports}require('./src/index.js')
})({'./src/index.js': {dependecies: { './hello.js': './src/hello.js' },code: '"use strict";\n\nvar _hello = require("./hello.js");\n\ndocument.write((0, _hello.say)("webpack"));'},'./src/hello.js': {dependecies: {},code:'"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports.say = say;\n\nfunction say(name) {\n  return "hello ".concat(name);\n}'}
})

step 1 : 从入口文件开始执行

// 定义一个立即执行函数,传入生成的依赖关系图
;(function(graph) {// 重写require函数function require(moduleId) {console.log(moduleId) // ./src/index.js}// 从入口文件开始执行require('./src/index.js')
})({'./src/index.js': {dependecies: { './hello.js': './src/hello.js' },code: '"use strict";\n\nvar _hello = require("./hello.js");\n\ndocument.write((0, _hello.say)("webpack"));'},'./src/hello.js': {dependecies: {},code:'"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports.say = say;\n\nfunction say(name) {\n  return "hello ".concat(name);\n}'}
})

step 2 : 使用 eval 执行代码

// 定义一个立即执行函数,传入生成的依赖关系图
;(function(graph) {// 重写require函数function require(moduleId) {;(function(code) {console.log(code) // "use strict";\n\nvar _hello = require("./hello.js");\n\ndocument.write((0, _hello.say)("webpack"));eval(code) // Uncaught TypeError: Cannot read property 'code' of undefined})(graph[moduleId].code)}// 从入口文件开始执行require('./src/index.js')
})({'./src/index.js': {dependecies: { './hello.js': './src/hello.js' },code: '"use strict";\n\nvar _hello = require("./hello.js");\n\ndocument.write((0, _hello.say)("webpack"));'},'./src/hello.js': {dependecies: {},code:'"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports.say = say;\n\nfunction say(name) {\n  return "hello ".concat(name);\n}'}
})

可以看到,我们在执行"./src/index.js"文件代码的时候报错了,这是因为 index.js 里引用依赖 hello.js,而我们没有对依赖进行处理,接下来我们对依赖引用进行处理。

step 3 : 依赖对象寻址映射,获取 exports 对象

// 定义一个立即执行函数,传入生成的依赖关系图
;(function(graph) {// 重写require函数function require(moduleId) {// 找到对应moduleId的依赖对象,调用require函数,eval执行,拿到exports对象function localRequire(relativePath) {return require(graph[moduleId].dependecies[relativePath]) // {__esModule: true, say: ƒ say(name)}}// 定义exports对象var exports = {};(function(require, exports, code) {// commonjs语法使用module.exports暴露实现,我们传入的exports对象会捕获依赖对象(hello.js)暴露的实现(exports.say = say)并写入eval(code)})(localRequire, exports, graph[moduleId].code)// 暴露exports对象,即暴露依赖对象对应的实现return exports}// 从入口文件开始执行require('./src/index.js')
})({'./src/index.js': {dependecies: { './hello.js': './src/hello.js' },code: '"use strict";\n\nvar _hello = require("./hello.js");\n\ndocument.write((0, _hello.say)("webpack"));'},'./src/hello.js': {dependecies: {},code:'"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports.say = say;\n\nfunction say(name) {\n  return "hello ".concat(name);\n}'}
})

这下应该明白了吧 ~ 可以直接复制上面代码到控制台输出哦~

总结

Webpack 是一个庞大的 Node.js 应用,如果你阅读过它的源码,你会发现实现一个完整的 Webpack 需要编写非常多的代码。 但你无需了解所有的细节,只需了解其整体架构和部分细节即可。

对 Webpack 的使用者来说,它是一个简单强大的工具; 对 Webpack 的开发者来说,它是一个扩展性的高系统。

Webpack 之所以能成功,在于它把复杂的实现隐藏了起来,给用户暴露出的只是一个简单的工具,让用户能快速达成目的。 同时整体架构设计合理,扩展性高,开发扩展难度不高,通过社区补足了大量缺失的功能,让 Webpack 几乎能胜任任何场景。

参考

webpack 中文文档

深入浅出 webpack

看完就懂webpack打包原理相关推荐

  1. 高铁、动车到底啥区别?看完彻底懂了(组图)

    摘自:网易新闻 (原标题:高铁.动车到底啥区别?看完彻底懂了(组图)) 高铁与动车的区别到底在哪里?磁悬浮列车又是什么鬼?今天给你讲讲清楚! 高铁.动车到底啥区别?看完彻底懂了 一.普通列车与高铁钢轨 ...

  2. 看完弄懂,明年至少加 5K

    看完弄懂,明年至少加 5K

  3. webpack打包原理_webpack打包原理入门探究(四)插件探究(上)

    子由风:webpack打包原理入门探究(一)​zhuanlan.zhihu.com 子由风:webpack打包原理入门探究(二)基本配置​zhuanlan.zhihu.com 子由风:webpack打 ...

  4. 网络通过猫传输到计算机,网络直接从光猫出来好还是接个路由器再接入电脑好?看完搞懂了...

    网络直接从光猫出来好还是接个路由器再接入电脑好?看完搞懂了 宽带网络现在是家家户户不可缺少的"硬件"之一,现在即便是老一辈的人家中安装宽带都成了必需品.有些偏好用电脑来上网的朋友可 ...

  5. 华为mate10pro以后能上鸿蒙吗,华为Mate10和Mate10 Pro差别一览 怎么选看完就懂

    华为Mate10和Mate10 Pro差别一览 怎么选看完就懂上周五华为正式发布了今年的两款重磅旗舰Mate10和Mate10 Pro.与上代产品不同,此次Mate10系列的两款产品无论是在外观还是一 ...

  6. 新手入门,数控刀具上的代码怎么认?看完就懂了!

    新手入门,数控刀具上的代码怎么认?看完就懂了! 按照不同的刀具类型对刀具分组: 类别组1 xxyyy(铣刀类): 110 球面铣刀 (圆柱型铣刀,其后的字母y代表铣刀直径,以下略同) 120 立铣刀 ...

  7. 为什么会显示有人正在使用计算机,微信“对方正在输入”为什么有时出现?有时不出现?看完才懂了.....

    原标题:微信"对方正在输入"为什么有时出现?有时不出现?看完才懂了.. 生活中有很多美好的事情 手机电量满格 您的快递正在派送 换季衣服里翻出毛爷爷 与喜欢的人聊天显示" ...

  8. java开发用i5还是i7,i7比i5更强!为什么内行人都选i5而不选i7?看完瞬间懂了

    i7比i5更强!为什么内行人都选i5而不选i7?看完瞬间懂了 2020-11-19 11:18:08 4点赞 0收藏 0评论 许多人认为i7比i5更好,那么有什么好呢?让我们先看一下区别. i7使用四 ...

  9. Callable和Runnable的区别(面试常考),看完就懂

    Callable和Runnable的区别(面试常考),看完就懂 Callable 接口 测试类 Runnable 接口 测试类 两者的区别 补充Executor框架 Callable 接口 publi ...

最新文章

  1. SpringBoot项目优化和Jvm调优(亲测,真实有效)
  2. UA MATH564 概率论II 连续型随机变量1
  3. C语言实现interpolation search插值查找算法(附完整源码)
  4. Codeforces Round #330 (Div. 2) B. Pasha and Phone 容斥定理
  5. html4基础,HTML 基础 4
  6. 聊聊研读论文有哪些经验之谈?
  7. 《深入浅出DPDK》读书笔记(八):网卡性能优化(异步中断模式、轮询模式、混和中断轮询模式)
  8. php 获取所有子目录名,php读取目录及子目录下所有文件名的方法,_PHP教程
  9. Orcle 版本、数据库名查询
  10. samba服务的原理与搭建(转的别人的)
  11. 直播策划方案怎么做?
  12. java keytool 生成p12证书
  13. 腾讯云国际版注册流程详解
  14. 保证线程安全的四种方法
  15. java零基础Ⅰ-- 1.java 概述
  16. sap增加税码注意事项
  17. MySQL - 21查询分析器EXPLAIN
  18. June 8th ipod
  19. outlook收件延迟严重_你(严重)对我不了解的五件事
  20. 兼职跑网约车能赚钱吗?

热门文章

  1. android 数据文件存储,实例详解Android文件存储数据方式
  2. git 使用_Git-介绍与使用
  3. 测试触发器有没有用_用30道电子工程师面试题来拷问堕落的假期...
  4. 微机原理换行代码_微机原理
  5. 2021牛气新年素材模板,你真的不来看一看吗?
  6. 还不会动效?优秀的可临摹素材,给你做个示范
  7. 导入要素类到要素数据集当中(C++)(ArcObject)史上最快
  8. 手绘时钟的设计与实现
  9. 2021年五月下旬推荐文章(2)
  10. readline库实现命令行自动补全