看完就懂webpack打包原理
目录
什么是 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 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程 :
- 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。
- 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译。
- 确定入口:根据配置中的 entry 找出所有的入口文件。
- 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。
- 完成模块编译:在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系。
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。
- 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。
在以上过程中,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打包原理相关推荐
- 高铁、动车到底啥区别?看完彻底懂了(组图)
摘自:网易新闻 (原标题:高铁.动车到底啥区别?看完彻底懂了(组图)) 高铁与动车的区别到底在哪里?磁悬浮列车又是什么鬼?今天给你讲讲清楚! 高铁.动车到底啥区别?看完彻底懂了 一.普通列车与高铁钢轨 ...
- 看完弄懂,明年至少加 5K
看完弄懂,明年至少加 5K
- webpack打包原理_webpack打包原理入门探究(四)插件探究(上)
子由风:webpack打包原理入门探究(一)zhuanlan.zhihu.com 子由风:webpack打包原理入门探究(二)基本配置zhuanlan.zhihu.com 子由风:webpack打 ...
- 网络通过猫传输到计算机,网络直接从光猫出来好还是接个路由器再接入电脑好?看完搞懂了...
网络直接从光猫出来好还是接个路由器再接入电脑好?看完搞懂了 宽带网络现在是家家户户不可缺少的"硬件"之一,现在即便是老一辈的人家中安装宽带都成了必需品.有些偏好用电脑来上网的朋友可 ...
- 华为mate10pro以后能上鸿蒙吗,华为Mate10和Mate10 Pro差别一览 怎么选看完就懂
华为Mate10和Mate10 Pro差别一览 怎么选看完就懂上周五华为正式发布了今年的两款重磅旗舰Mate10和Mate10 Pro.与上代产品不同,此次Mate10系列的两款产品无论是在外观还是一 ...
- 新手入门,数控刀具上的代码怎么认?看完就懂了!
新手入门,数控刀具上的代码怎么认?看完就懂了! 按照不同的刀具类型对刀具分组: 类别组1 xxyyy(铣刀类): 110 球面铣刀 (圆柱型铣刀,其后的字母y代表铣刀直径,以下略同) 120 立铣刀 ...
- 为什么会显示有人正在使用计算机,微信“对方正在输入”为什么有时出现?有时不出现?看完才懂了.....
原标题:微信"对方正在输入"为什么有时出现?有时不出现?看完才懂了.. 生活中有很多美好的事情 手机电量满格 您的快递正在派送 换季衣服里翻出毛爷爷 与喜欢的人聊天显示" ...
- java开发用i5还是i7,i7比i5更强!为什么内行人都选i5而不选i7?看完瞬间懂了
i7比i5更强!为什么内行人都选i5而不选i7?看完瞬间懂了 2020-11-19 11:18:08 4点赞 0收藏 0评论 许多人认为i7比i5更好,那么有什么好呢?让我们先看一下区别. i7使用四 ...
- Callable和Runnable的区别(面试常考),看完就懂
Callable和Runnable的区别(面试常考),看完就懂 Callable 接口 测试类 Runnable 接口 测试类 两者的区别 补充Executor框架 Callable 接口 publi ...
最新文章
- SpringBoot项目优化和Jvm调优(亲测,真实有效)
- UA MATH564 概率论II 连续型随机变量1
- C语言实现interpolation search插值查找算法(附完整源码)
- Codeforces Round #330 (Div. 2) B. Pasha and Phone 容斥定理
- html4基础,HTML 基础 4
- 聊聊研读论文有哪些经验之谈?
- 《深入浅出DPDK》读书笔记(八):网卡性能优化(异步中断模式、轮询模式、混和中断轮询模式)
- php 获取所有子目录名,php读取目录及子目录下所有文件名的方法,_PHP教程
- Orcle 版本、数据库名查询
- samba服务的原理与搭建(转的别人的)
- 直播策划方案怎么做?
- java keytool 生成p12证书
- 腾讯云国际版注册流程详解
- 保证线程安全的四种方法
- java零基础Ⅰ-- 1.java 概述
- sap增加税码注意事项
- MySQL - 21查询分析器EXPLAIN
- June 8th ipod
- outlook收件延迟严重_你(严重)对我不了解的五件事
- 兼职跑网约车能赚钱吗?