webpack 打包后,做了什么事情?
当在终端执行 npx webpack 命令进行打包后,webpack 都做了什么事情?
我们来看一个简单的demo:
webpack.config.js 文件的配置如下:
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "./dist"),
filename: "main.js",
},
mode: "development",
};
我们指定了模块打包的入口文件是 src 目录下的 index.js 文件,打包完成后的输出目录是 dist 目录,输出文件是 main.js。打包完成后的目录结构如下:
可以看到,打包完成后,dist 目录下多了 main.js 文件。我们来看看main.js 文件的内容:
// webpackBootStrap 启动函数
// modules 即为存放所有模块的数组,数组中的每一个元素都是一个函数
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
// 安装过的模块都存放在这里面
// 作用是把已经加载过的模块缓存在内存中,提升性能
/******/ var installedModules = {};
/******/
/******/ // The require function
// webpack 自己实现的 require 函数
// 去数组综合功能加载一个模块,moduleId 为要加载模块在数组中的 index
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
// 如果要加载的模块已经被加载过,就直接从内存缓存中获取
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
// 如果缓存中没有需要加载的模块,就新建一个模块,并将其存在缓存中
/******/ var module = installedModules[moduleId] = {
// 模块在数组中的 index
/******/ i: moduleId,
// 该模块是否已经加载完毕
/******/ l: false,
// 该模块的导出值
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
// 从 modules 中获取 index 为 moduleId 的模块对应的函数
// 然后调用这个函数,同时传入函数需要的参数
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
// 把这个模块标为已加载
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
// 返回这个模块的导出值
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ }
/******/ };
/******/
/******/ // define __esModule on exports
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = __webpack_require__(value);
/******/ if(mode & 8) return value;
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ return ns;
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = "./src/index.js");
/******/ })
/************************************************************************/
/******/ ({
/***/ "./src/index.js":
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
/*! no static exports found */
/***/ (function(module, exports) {
eval("\n\n//# sourceURL=webpack:///./src/index.js?");
/***/ })
/******/ });
将代码折叠后,我们发现,main.js 中的内容就是一个自执行函数,main.js 中的代码是可以直接在浏览器中运行的。
从 main.js 中的代码可以知道,webpack在打包后输出的文件中实现了一个webpack_require 函数来实现自己的模块化,把代码都缓存在 installedModules 里了,代码文件以对象的形式传递进来,其中 key 是文件路径,value是包裹的代码字符串,并且代码内部的 require ,都被替换成了 webpack_require 。这也正是 main.js 能直接运行在浏览器的原因,webpack_require 函数模拟了Node.js 中的require 语句,通过网络请求去加载文件。
在 webpack_require 函数中,webpack 还做了缓存优化:执行加载过的模块不会执行第二次,执行结果会缓存在内存中,当某个模块第二次被访问时会直接去内存中读取被缓存的返回值。
webpack 构建流程
从执行 npx webpack 到输出文件到指定目录,webpack 的构建流程是怎样的呢?
从上面的流程图可以看出,webpack 的构建流程可以分为三大阶段:
- 初始化:启动构建,读取并合并配置参数,加载 plugins,实例化 Compiler。
- 编译:从入口 entry 开始,针对每个 module 串行调用对应的 loader 去编译文件内容,再找到该 module 依赖的 module ,递归地进行编译处理。
- 输出:对编译后的 module 组合成 chunk ,把 chunk 转换成文件,输出到指定的目录中。
如果只执行一次构建,以上三个阶段将会按照顺序各执行一次。但在开启监听模式下,流程将变为如下:
下面,我们来看一下 webpack 构建的具体流程:
- 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
- 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;
- 确定入口:根据配置中的entry 找出所有的入口文件;
- 编译模块:从入口文件出发,调用所有配置的 loader 对模块进行编译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
- 完成模块编译:在经过第 4 步使用 loader 编译完所有模块后,得到了每个模块被编译后的最终内容以及它们之间的依赖关系;
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 chunk ,再把每个 chunk 转换成一个单独的文件加入到输出列表,这一步骤是可以修改输出内容的最后机会;
- 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。
在以上过程中,webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 webpack 提供的 API 来改变 webpack 的运行结果。
简易版 webpack 实现
我们已经了解了 webpack 的打包原理和构建流程,我们也来实现一个简易版的 webpack。
1、定义 Compiler 类
module.exports = class Compiler {
constructor(options) {
// options 为 webpack.config.js 中的配置
// 从 options 中解构出 entry、output
const { entry, output } = options;
// 入口
this.entry = entry;
// 出口
this.output = output;
// 模块
this.modules = [];
// 构建启动函数
run() {}
// 重写 require 函数,输出 bundle
generate() {}
}
}
2、解析入口文件,获取 AST
我们借助 @babel/parser 来提取文件的依赖。@babel/parser 是 babel7的工具,可以帮助我们分析文件内部的语法,也包括 ES6,解析完成后会返回一个 AST 抽象语法树。
在 Compiler 类中定义 getAst 方法来获取 AST:
const fs = require('fs');
const parser = require('@babel/parser');
// 转换成 AST 语法树
getAst(entryFile) {
// 分析入口模块的内容
const content = fs.readFileSync(entryFile, "utf-8");
// 将入口文件的内容转换为 AST 抽象语法树
const ast = parser.parse(content, {
sourceType: "module"
});
return ast;
}
在 parse 方法中调用 getAst 方法:
parse(entryFile) {
// 获取 AST 抽象语法树
const ast = this.getAst(entryFile);
}
3、找出所有的依赖模块
接下来我们需要找出所有的引用模块。Babel 提供了 @babel/traverse (遍历)方法来维护 AST 树的整体状态,我们可以借助它来帮我们找出所有的引用模块。
在 Compiler 类中定义 getDependencies 方法来获取所有引用的模块:
const path = require('path');
const traverse = require("@babel/traverse").default;
// 获取所有引用依赖
getDependencies(ast, entryFile) {
const dependencies = {};
// 遍历所有的 import 模块,存入 dependencies 中
traverse(ast, {
// 类型为 ImportDeclaration 的 AST 节点(即 import 语句)
ImportDeclaration({ node }) {
// 保存依赖模块路径,在后面生层依赖关系图时会用到
const pathName = './' + path.join(path.dirname(entryFile), node.source.value);
dependencies[node.source.value] = pathName;
}
})
return dependencies
}
在 parse 方法中调用 getDependencies 方法:
parse(entryFile) {
// 获取 AST 抽象语法树
const ast = this.getAst(entryFile);
// 获取所有引用依赖
const dependencies = this.getDependencies(ast, entryFile);
}
4、AST 转换为 code
借助 @babel/core 和 @babel/preset-env ,将 AST 语法树转换成浏览器可运行的代码。
在 Compiler 类中定义 getCode 方法来将 AST 语法树转换成浏览器可运行的代码:
const { transformFromAst } = require("@babel/core");
// 将 AST 转换成 浏览器可以运行的代码
getCode(ast) {
const { code } = transformFromAst(ast, null, {
presets: ["@babel/preset-env"]
})
return code;
}
在 parse 方法中调用 getCode 方法:
parse(entryFile) {
// 获取 AST 抽象语法树
const ast = this.getAst(entryFile);
// 获取所有引用依赖
const dependencies = this.getDependencies(ast, entryFile);
// 将 AST 转换成浏览器可运行的代码
const code = this.getCode(ast);
}
最后,将 entryFile、dependencies 和 code 在 parse 函数中返回出去,以便在生成依赖关系图时使用:
parse(entryFile) {
// 获取 AST 抽象语法树
const ast = this.getAst(entryFile);
// 获取所有引用依赖
const dependencies = this.getDependencies(ast, entryFile);
// 将 AST 转换成浏览器可运行的代码
const code = this.getCode(ast);
return {
entryFile,
dependencies,
code
}
}
5、递归解析所有依赖模块,生成依赖关系图
在 parse 函数中,我们从入口文件开始,获取了所有的引用依赖,接下来,我们递归解析所有的依赖模块,将其生成依赖关系图。
定义 recursionDependent 函数来递归解析所有的依赖模块,在该函数中,我们将会使用两个 for 循环来实现递归的效果:
// 解析所有依赖模块
recursionDependent() {
// 递归解析所有依赖模块
for (let i = 0; i < this.modules.length; i++) {
const item = this.modules[i];
const dependencies = item.dependencies;
// 判断是否有依赖对象,递归解析所有的依赖模块
if (dependencies) {
for(let j in dependencies) {
this.modules.push(this.parse(dependencies[j]))
}
}
}
}
在 run 函数中调用 recursionDependent 函数:
run() {
// 解析入口模块
const info = this.parse(this.entry);
this.modules.push(info);
// 递归解析所有依赖
recursionDependent()
}
然后定义 generateDependencyGraph 函数来生成依赖关系图,也就是将数组结构的 dependencies 转换成 Object 对象结构:
// 生成依赖关系图
generateDependencyGraph() {
const graphMap = {}
this.modules.forEach(item => {
graphMap[item.entryFile] = {
dependencies: item.dependencies,
code: item.code
}
})
return graphMap
}
在 run 函数中调用 generateDependencyGraph 函数:
run() {
// 解析入口模块
const info = this.parse(this.entry);
this.modules.push(info);
// 递归解析所有依赖
recursionDependent();
// 生成依赖关系图
const graphMap = generateDependencyGraph();
}
6、重写 require 函数,输出 bundle
类似于 webpack 在打包后输出的文件中实现的webpack_require 函数,我们重写 require 函数,然后生成可以在浏览器运行的代码:
// 重写 require 函数,输出 bundle
generate(code) {
// 生成代码内容 webpack启动函数
const filePath = path.join(this.output.path, this.output.filename);
const newCode = JSON.stringify(code);
const bundle = `(function(graph){
function require(module){
function PathRequire(relativePath){
return require(graph[module].dependencies[relativePath])
}
const exports = {};
(function(require,exports,code){
eval(code)
})(PathRequire,exports,graph[module].code)
return exports;
}
require('${this.entry}')
})(${newCode})`;
// 把文件内容写入到文件,生成 main.js,位置是 dist 目录
fs.writeFileSync(filePath, bundle, "utf-8");
}
在 run 函数中调用 generate 函数:
run() {
// 解析入口模块
const info = this.parse(this.entry);
this.modules.push(info);
// 递归解析所有依赖
recursionDependent();
// 生成依赖关系图
const graphMap = generateDependencyGraph();
// 生成输出文件
this.generate(graphMap)
}
Compiler 类完整代码
const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const { transformFromAst } = require("@babel/core");
//@babel/preset-env
module.exports = class Webpack {
constructor(options) {
// options 为 webpack.config.js 中的配置
// 从 options 中解构出 entry、output
const { entry, output } = options;
// 入口
this.entry = entry;
// 出口
this.output = output;
// 模块
this.modules = [];
}
// 构建启动函数
run() {
// 解析入口模块
const info = this.parse(this.entry);
this.modules.push(info);
// 递归解析所有依赖
this.recursionDependent()
// 数组结构转对象结构
const graphMap = this.generateDependencyGraph()
// 生成输出文件
this.generate(graphMap);
}
// 解析所有依赖模块
recursionDependent() {
for (let i = 0; i < this.modules.length; i++) {
const item = this.modules[i];
const dependencies = item.dependencies;
if (dependencies) {
for(let j in dependencies) {
this.modules.push(this.parse(dependencies[j]))
}
}
}
}
// 生成依赖关系图
generateDependencyGraph() {
const graphMap = {}
this.modules.forEach(item => {
graphMap[item.entryFile] = {
dependencies: item.dependencies,
code: item.code
}
})
return graphMap
}
// 转换成 AST 语法树
getAst(entryFile) {
// 分析入口模块的内容
const content = fs.readFileSync(entryFile, "utf-8");
// 处理依赖
const ast = parser.parse(content, {
sourceType: "module",
});
return ast
}
// 获取所有引用依赖
getDependencies(ast, entryFile) {
const dependencies = {};
traverse(ast, {
ImportDeclaration({ node }) {
const pathName = './' + path.join(path.dirname(entryFile), node.source.value);
dependencies[node.source.value] = pathName;
}
})
return dependencies
}
// 将 AST 转换成 浏览器可以运行的代码
getCode(ast) {
const { code } = transformFromAst(ast, null, {
presets: ["@babel/preset-env"]
})
return code;
}
// 解析依赖模块
parse(entryFile) {
// 获取 AST 抽象语法树
const ast = this.getAst(entryFile);
// 获取所有引用依赖
const dependencies = this.getDependencies(ast, entryFile)
// 将 AST 转换成浏览器可运行的代码
const code = this.getCode(ast)
return {
entryFile,
dependencies,
code,
};
}
// 重写 require 函数,输出 bundle
generate(code) {
// 生成代码内容 webpack启动函数
const filePath = path.join(this.output.path, this.output.filename);
const newCode = JSON.stringify(code);
const bundle = `(function(graph){
function require(module){
function PathRequire(relativePath){
return require(graph[module].dependencies[relativePath])
}
const exports = {};
(function(require,exports,code){
eval(code)
})(PathRequire,exports,graph[module].code)
return exports;
}
require('${this.entry}')
})(${newCode})`;
// 生成main.js 位置是./dist目录
fs.writeFileSync(filePath, bundle, "utf-8");
}
};
我们在根目录下创建 bundle.js 文件作为我们的应用启动入口:
// 读取配置
const options = require("./webpack.config.js");
// 引入webpack
const webpack = require("./lib/webpack.js");
// webpack接收配置 启动入口函数,执行打包
new webpack(options).run();
在终端执行 npm run start 命令,在根目录的 dist 目录下生成了 main.js 文件,main.js 的内容如下:
(function (graph) {
function require(module) {
function PathRequire(relativePath) {
return require(graph[module].dependencies[relativePath])
}
const exports = {};
(function (require, exports, code) {
eval(code)
})(PathRequire, exports, graph[module].code)
return exports;
}
require('./src/index.js')
})({
"./src/index.js": {
"dependencies": { "./a.js": "./src/a.js" },
"code": "\"use strict\";\n\nvar _a = require(\"./a.js\");\n\nconsole.log(\"hello \".concat(_a.str));"
},
"./src/a.js": {
"dependencies": { "./b.js": "./src/b.js" },
"code": "\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.str = void 0;\n\nvar _b = require(\"./b.js\");\n\nvar str = \"webpack5 \".concat(_b.str2);\nexports.str = str;"
},
"./src/b.js": {
"dependencies": {},
"code": "\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.str2 = void 0;\nvar str2 = \"!!!!\";\nexports.str2 = str2;"
}
})
我们将这段代码拷贝到浏览器运行:
可见,webpack 打包输出的 bundle 文件内容是可以直接在浏览器中运行的代码。
参考: