webpack原理分析
写在前面:
这篇文章,我是学习了webpack打包原理 ? 看完这篇你就懂了 !后写的, 照着这篇文章的内容,自己手动实现了一遍,收益颇丰。在这里对原作者表示感谢,侵删。文章非绝对原创,特此声明。
什么是webpack
本质上,webpack是一个现代JavaScript应用程序的静态模块打包器(module bundler)。当webpack处理应用程序是,它会递归的构建一个依赖关系图(dependency graph), 其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或者多个bundle。
webpack就像一条生产线,要经过一系列处理流程后,才能将源文件转换成输出结果。这条生产线上的每个处理流程的智者都是单一的,多个流程之间存在依赖关系,只要当完成当前处理后,才能交给下一个流程去处理。插件就是一个插入到生产线中的功能,在特定时间,对生产线上的资源做处理。
Webpack 通过Tapable来组织这条复杂的生产线。webpack在运营过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。webpack的事件流机制,保证了插件的有序性。使得整个扩展性很好。—深入浅出webpack吴浩麟
Tapable 是一个小型的库,允许你对一个 javascript 模块添加和应用插件。它可以被继承或混入到其他模块中。类似于 NodeJS 的
EventEmitter
类,专注于自定义事件的触发和处理。除此之外,Tapable
还允许你通过回调函数的参数,访问事件的“触发者(emittee)”或“提供者(producer)”。
其中下面的这段代码,最难理解,所有的import的代码会用一个require来加载,当把入口require后,原有的require方法就会在自执行匿名函数的作用域中,被重写。从而实现对相对路径资源的引用。
Webpack的核心概念
Entry
入口起点(enter point)指示webpack应该使用哪个模块,来作为构建其内部依赖图的开始。相当于树的根节点。
进入入口起点后,webpack会将代码解析成AST语法树,然后webpack会根据语法树找出有哪些模块和库是入口起点(直接和间接)依赖的。
每个依赖项随即被处理,最后输出到bundle的文件中。
Output
Output属性告诉webpack在哪里输出它所创建的bundles,以及如何命名这些文件,默认值为./dist。如果不做特殊指定的话,例如external,代码都会被编译到你指定的输出文件中。
Module
模块,在webpack里一切皆模块,一个模块对应着一个文件,比如js是一个模块,scss也是一个模块。webpack会从配置的Entry开始递归找出所有依赖的模块。
Chunk
这是 webpack 特定的术语被用在内部来管理 building 过程。bundle 由 chunk 组成,其中有几种类型(例如,入口 chunk(entry chunk) 和子 chunk(child chunk))。通常 chunk 会直接对应所输出的 bundle,但是有一些配置并不会产生一对一的关系。
简单来讲,写的代码是module,编译中的代码叫chunk,输出的代码叫bundle.
Loader
loader让webpack能够去处理那些非JavaScript文件(webpack 自身职能理解JavaScript).
loader可以将所有类型的文件,转换成为webpack能处理的有效模块,然后你就可以利用webpack的打包能力,对他们进行处理。
本质上,webpack loader将所有类型的文件,转换为应用程序的依赖图(和最终的bundle)可直接引用的模块。
Plugin
Loader被用于转换某些类型的模块,而插件则可以用于执行更广泛的任务。
插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件接口功能极其强大,可以用来处理各种各样的任务。
Webpack构建流程
webpack的运行流程是一个串行的过程,从启动到结束会一次执行以下流程。
- 初始化参数:从配置文件和shell渔具中读取、合并参数,并得出最终的webpack配置。
- 开始编译:根据上一步得到的参数初始化
Compiler
对象,加载所有配置的插件,执行对象的run方法开始执行编译。 - 确定入口:根据配置文件中的entry找出所有的入口文件。
- 编译模块:从入口文件触发,调用所有配置的Loader对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。
- 完成模块编译:在经过第4步使用loader翻译完所有模块后,得到每个模块被翻译后的最终内容,以及他们之间的依赖关系。
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的Chunk,再把每个Chunk转换成一个单独的文件,加入到输出列表,这步是可以修改输出内容的最后机会。
- 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件写入到文件系统。
在以上过程中,Webpack会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后,会执行响应的逻辑,并且插件可以调用Webpack提供的API改变Webpack的运行结果。
实践加深理解,写一个webpackDemo
1. 定义一个compiler类
这个类相当于webpack的核心框架。
class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模块
this.modules = []
}
// 构建启动
run() {}
// 重写 require函数,输出bundle
generate() {}
}
2.解析入口文件,获取AST语法树
创建webpack配置文件:
// webpack.config.js
const path = require('path')
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'main.js'
}
}
使用@babel/parser来帮助我们分析代码内部的语法,返回一个AST抽象语法树。
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函数,输出bundle
generate() {}
}
new Compiler(options).run()
创建我们的业务代码
// index.js
import welcome from './utils.js';
console.log('Hello biubiu!')
welcome();
// ./utils.js
export default function ppt() {
console.log('Welcome to webpack!');
}
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').default
const Parser = {
getAst: path => {
// 读取入口文件
const content = fs.readFileSync(path, 'utf-8')
// 将文件内容转为AST抽象语法树
return parser.parse(content, {
sourceType: 'module'
})
},
getDependecies: (ast, filename) => {
console.log('ast', ast);
const dependecies = {}
// 遍历所有的 import 模块,存入dependecies
traverse(ast, {
// 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)
/**
* ImportDeclaration({ node }) {}的写法,等同于下面的写法
* ImportDeclaration: function ImportDeclaration({ node }) {
const dirname = path.dirname(filename)
// 保存依赖模块路径,之后生成依赖关系图需要用到
const filepath = './' + path.join(dirname, node.source.value)
dependecies[node.source.value] = filepath
}
*/
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 } = Parser
const ast = getAst(this.entry)
const dependecies = getDependecies(ast, this.entry)
}
// 重写 require函数,输出bundle
generate() {}
}
new Compiler(options).run()
4.将AST转化为Code
将AST 转化为浏览器可执行的代码,我们主要使用的是@babel/core和@babel/preset-env.
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const { transformFromAst } = require('@babel/core');
const options = require('./webpack.config.js');
const Parser = {
getAst: path => {
const content = fs.readFileSync(path, 'utf-8');
return parser.parse(content, {
sourceType: 'module'
})
},
getDependecies: (ast, fileName) => {
const dependecies = {};
// 类型为importDeclaration的AST 节点
console.log(ast);
traverse(ast, {
ImportDeclaration: function ImportDeclaration({node}) {
// console.log(node);
const dirname = path.dirname(fileName);
// 保存依赖模块路径,之后生成依赖关系图的时候用到
const filepath = './' + path.join(dirname, node.source.value);
// console.log("ImportDeclaration", node, fileName, filepath);
dependecies[node.source.value] = filepath;
}
})s
return dependecies;
},
getCode: (ast) => {
// ast 转为code
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env']
});
return code;
}
};
class Compiler {
constructor(options) {
const { entry, output } = options;
this.entry = entry;
this.output = output;
// 模块
this.modules = [];
}
// 构建启动
run() {
const ast = Parser.getAst(this.entry);
// console.log(ast);
const dependecies = Parser.getDependecies(ast,this.entry);
// console.log(dependecies);
const code = Parser.getCode(ast);
// console.log(code);
}
}
new Compiler(options).run();
5. 递归解析所有依赖项,生成依赖关系图
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const { transformFromAst } = require('@babel/core');
const options = require('./webpack.config.js');
const Parser = {
getAst: path => {
const content = fs.readFileSync(path, 'utf-8');
return parser.parse(content, {
sourceType: 'module'
})
},
getDependecies: (ast, fileName) => {
const dependecies = {};
// 类型为importDeclaration的AST 节点
console.log(ast);
traverse(ast, {
ImportDeclaration: function ImportDeclaration({node}) {
// console.log(node);
const dirname = path.dirname(fileName);
// 保存依赖模块路径,之后生成依赖关系图的时候用到
const filepath = './' + path.join(dirname, node.source.value);
// console.log("ImportDeclaration", node, fileName, filepath);
dependecies[node.source.value] = filepath;
}
})
return dependecies;
},
getCode: (ast) => {
// ast 转为code
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env']
});
return code;
}
};
class Compiler {
constructor(options) {
const { entry, output } = options;
this.entry = entry;
this.output = output;
// 模块
this.modules = [];
}
build(fileName) {
const { getAst, getDependecies, getCode } = Parser;
const ast = getAst(fileName);
const dependecies = getDependecies(ast, fileName);
const code = getCode(ast);
return {
// 文件路径,可以作为每个模块的唯一标识符
fileName,
// 依赖对象,保存着依赖模块路径
dependecies,
// 文件内容
code
}
}
run() {
// 解析入口文件
const info = this.build(this.entry);
this.modules.push(info);
this.modules.forEach(({dependecies}) => {
// 判断有一栏对象,递归解析所有的依赖项
if(dependecies) {
for(const dependecy in dependecies) {
this.modules.push(this.build(dependecies[dependecy]));
}
}
});
// 生成依赖关系图
const dependecyGraph = this.modules.reduce((graph, item) => ({
...graph,
//使用文件路径作为每个模块唯一标识符,保存对应模块的依赖对象和文件内容
[item.fileName]: {
dependecies: item.dependecies,
code: item.code
}
}), {});
// console.log(dependecyGraph);
}
}
new Compiler(options).run();
6. 重写require函数,生成代码,输出bundle
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const { transformFromAst } = require('@babel/core');
const options = require('./webpack.config.js');
const Parser = {
getAst: path => {
const content = fs.readFileSync(path, 'utf-8');
return parser.parse(content, {
sourceType: 'module'
})
},
getDependecies: (ast, fileName) => {
const dependecies = {};
// 类型为importDeclaration的AST 节点
console.log(ast);
traverse(ast, {
ImportDeclaration: function ImportDeclaration({node}) {
// console.log(node);
const dirname = path.dirname(fileName);
// 保存依赖模块路径,之后生成依赖关系图的时候用到
const filepath = './' + path.join(dirname, node.source.value);
// console.log("ImportDeclaration", node, fileName, filepath);
dependecies[node.source.value] = filepath;
}
})
return dependecies;
},
getCode: (ast) => {
// ast 转为code
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env']
});
return code;
}
};
class Compiler {
constructor(options) {
const { entry, output } = options;
this.entry = entry;
this.output = output;
// 模块
this.modules = [];
}
build(fileName) {
const { getAst, getDependecies, getCode } = Parser;
const ast = getAst(fileName);
const dependecies = getDependecies(ast, fileName);
const code = getCode(ast);
return {
// 文件路径,可以作为每个模块的唯一标识符
fileName,
// 依赖对象,保存着依赖模块路径
dependecies,
// 文件内容
code
}
}
run() {
// 解析入口文件
const info = this.build(this.entry);
this.modules.push(info);
this.modules.forEach(({dependecies}) => {
// 判断有一栏对象,递归解析所有的依赖项
if(dependecies) {
for(const dependecy in dependecies) {
this.modules.push(this.build(dependecies[dependecy]));
}
}
});
// 生成依赖关系图
const dependecyGraph = this.modules.reduce((graph, item) => ({
...graph,
//使用文件路径作为每个模块唯一标识符,保存对应模块的依赖对象和文件内容
[item.fileName]: {
dependecies: item.dependecies,
code: item.code
}
}), {});
// console.log(dependecyGraph);
this.generate(dependecyGraph);
}
// 重写require函数(浏览器不能识别common.js语法),输出bundle
generate(code) {
const filepath = path.join(this.output.path, this.output.fileName);
const bundle = `(function (graph) {
;
function require(moduleId) {
const exports = {}
;function localRequire(relativePath) {
return require(graph[moduleId].dependecies[relativePath]);
}
;(function(require, exports, code){
eval(code);
}(localRequire, exports, graph[moduleId].code))
return exports;
}
require('${this.entry}')
})(${JSON.stringify(code)})`;
fs.writeFileSync(filepath, bundle, 'utf-8');
}
}
new Compiler(options).run();
7. 深入剖析require的逻辑
这里有一个前提是,如果把代码想成一个树的话,那么entry就是根节点所在,正因为有了根节点的存在,我们才能通过重写require的方法,来实现这个逻辑。
7.1 先定义一个require
先来个简单点的:
generate(code) {
const filepath = path.join(this.output.path, this.output.fileName);
const bundle = `(function (graph) {
;
function require(moduleId) {
console.log("moduleId", moduleId);
}
require('${this.entry}')
})(${JSON.stringify(code)})`;
fs.writeFileSync(filepath, bundle, 'utf-8');
}
输出的结果是:
(function (graph) {
;
function require(moduleId) {
console.log(moduleId);
}
require('./src/index.js')
})({
"./src/index.js": {
"dependecies": {
"./utils.js": "./src/utils.js"
},
"code": "\"use strict\";\n\nvar _utils = _interopRequireDefault(require(\"./utils.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log('Hello biubiu!');\n(0, _utils[\"default\"])();"
},
"./src/utils.js": {
"dependecies": {},
"code": "\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports[\"default\"] = ppt;\n\nfunction ppt() {\n console.log('Welcome to webpack!');\n}"
}
})
7.2 获取可执行代码
通过moduleId取出graph中的代码,然后执行。
generate(code) {
const filepath = path.join(this.output.path, this.output.fileName);
const bundle = `(function (graph) {
;
function require(code) {
console.log(code);
eval(code);
}(graph[moduleId].code)
require('${this.entry}')
})(${JSON.stringify(code)})`;
fs.writeFileSync(filepath, bundle, 'utf-8');
}
输出结果:
(function (graph) {
;
function require(code) {
console.log(code);
eval(code);
}(graph[moduleId].code)
require('./src/index.js')
})({
"./src/index.js": {
"dependecies": {
"./utils.js": "./src/utils.js"
},
"code": "\"use strict\";\n\nvar _utils = _interopRequireDefault(require(\"./utils.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log('Hello biubiu!');\n(0, _utils[\"default\"])();"
},
"./src/utils.js": {
"dependecies": {},
"code": "\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports[\"default\"] = ppt;\n\nfunction ppt() {\n console.log('Welcome to webpack!');\n}"
}
})
执行后发现会报错,是因为./src/utils.js
模块没有被执行。
7.3 依赖对象寻址映射,获取 exports 对象
generate(code) {
const filepath = path.join(this.output.path, this.output.fileName);
const bundle = `(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('${this.entry}')
})(${JSON.stringify(code)})`;
fs.writeFileSync(filepath, bundle, 'utf-8');
}