webpack通过配置项就可以实现各种场景下的打包,那么它究竟是怎么打包的呢?网上的简易打包原理怎么看都云里雾里,不如自己悄咪咪实现一个,揭开这层神秘的面纱…
通过本文学到什么?
- webpack打包后的结果分析
- 通过demo实现webpack简易打包原理
- 涉及到的知识点:babel模块、fs模块、path模块
目录结构
-
webpack4打包结果
-
手写demo实现简易打包过程
-
获取 modules (单个模块路径、模块内容、所属依赖)
1.1 读取模块内容
1.2 获取抽象语法树
1.3 获取依赖
1.4 获取内容
1.5 总结
-
获取所有依赖内容并处理成想要的格式
2.1 获取所有依赖内容
2.2 处理成想要的格式
2.3 总结
-
立即执行函数
3.1 相关知识梳理
3.2 实现
3.3 总结
-
打包结果放入dist文件
-
总结
-
例子引入
目录结构
index.js
import add from "./add.js"
import {minus} from "./minus.js";
const sum = add(1,2);
const division = minus(2,1);
console.log(sum);
console.log(division);
add.js
export default (a,b)=>{
return a+b;
}
minus.js
export const minus = (a,b)=>{
return a-b
}
一、webpack4打包结果
正常利用webpack4打包后的代码不利于阅读,所以配置 webpack.config.js 不压缩打包结果。
optimization: {
namedModules: true, // true 打包后的 moduleId 为文件路径。false 打包后的 moduleId 为数字
minimizer: [
new UglifyJsPlugin({
uglifyOptions: {
compress: false,
},
}),
],
},
查看webpack输出结果去分析webpack打包流程
输出的 bundle.js,删掉了一些代码,突出重点。
可以看到是一个立即执行函数
,传入的是一个对象。这个对象存放了项目用到的所有模块的对象,其中每个模块存储为{ 模块路径: 模块导出代码函数 }。
/******/ (function(modules) { ...
/******/ })
/******/ ({
/***/ "./src/add.js":
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony default export */ __webpack_exports__[\"default\"] = ((a,b)=>{\n return a+b;\n});\n\n//# sourceURL=webpack:///./src/add.js?");
/***/ }),
/***/ "./src/index.js":
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _add_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./add.js */ \"./src/add.js\");\n/* harmony import */ var _minus_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./minus.js */ \"./src/minus.js\");\n\n\n\nconst sum = Object(_add_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(1,2);\nconst division = Object(_minus_js__WEBPACK_IMPORTED_MODULE_1__[\"minus\"])(2,1);\n\nconsole.log(sum);\nconsole.log(division);\n\n//# sourceURL=webpack:///./src/index.js?");
/***/ }),
/***/ "./src/minus.js":
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"minus\", function() { return minus; });\nconst minus = (a,b)=>{\n return a-b\n}\n\n//# sourceURL=webpack:///./src/minus.js?");
/***/ })
/******/ });
我们来看看 modules 传入这个函数后做了什么?
/******/ (function(modules) { // webpackBootstrap
/******/ // 模块缓存作用,已加载的模块可以不用再重新读取,提升性能
/******/ var installedModules = {};
/******/
/******/ // 关键函数,加载模块代码,形式有点像Node的CommonJS模块,但这里是可跑在浏览器上的es5代码
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // 缓存检查,有则直接从缓存中取得
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // 先创建一个空模块,塞入缓存中
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false, // 标记是否已经加载
/******/ exports: {} // 初始模块为空
/******/ };
/******/
/******/ // 把要加载的模块内容,挂载到module.exports上
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/ module.l = true; // 标记为已加载
/******/ // 返回加载的模块,调用方直接调用即可
/******/ return module.exports;
/******/ }
/******/
/******/ // 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 });
/******/ };
/******/
/******/ // 启动入口模块index.js
/******/ return __webpack_require__(__webpack_require__.s = "./src/index.js");
/******/ })
/************************************************************************/
/******/ ({...
/******/ });
- 首先定义了一个缓存区
installedModules
,从 return 中返回的 ./src/index.js 就是在 webpack.config.js 中定义的 entry。也就是入口文件路径。 - 执行
webpack_require
函数,加载 ./src/index.js 模块代码,加载的是模块路径作为 moduleId 传入。 - 利用先前定义的缓存区 installedModules 判断当前
moduleId
(其中moduleId就是模块路径,如./src/commonjs/index.js)是否存在在缓存区 installedModules 中,如果存在从缓存取用,否则通过创建空模块,把要加载的模块内容,挂载到module.exports上,并返回。
看起来大概是这样一个思路,利用获得模块路径
、模块内容
、所属依赖
这三个,分别在执行期间动态插入。那么如果我们要手动实现具体怎么操作呢?
二、下面我们来手写demo实现这个过程
目录结构
1. 获取 modules (单个模块路径、模块内容、所属依赖)
其中 modules 存放所有模块的数组,数组中每个元素存储{ 模块路径: 模块导出代码函数 },其中模块导出代码函数包括模块依赖和当前模块内容,都以es5的方式存储。
流程如下图所示,主要是利用AST抽象语法树
,获取模块的依赖,再将当前模块内容转为浏览器可识别的es5语法,最后将当前模块路径,依赖,模块内容输出为想要的格式。
1.1 读取模块内容
1.1.1 相关知识梳理
Node.js 内置的fs
模块就是文件系统模块,负责读写文件。也同时提供了异步和同步的方法。
1) 异步获取文件
异步方法是因为JavaScript的单线程模型,执行IO操作时,JavaScript代码无需等待,而是传入回调函数后,继续执行后续JavaScript代码。
异步读文件为 readFile ,回调函数接收两个参数。其使用方式为:
fs.readFile('sample.txt', 'utf-8', function (err, data) {
if (err) {
console.log(err);
} else {
console.log(data);
}
});
2) 同步读文件
其中 readFileSync 为同步方法。与异步不同的是不接收回调函数,函数直接返回结果。
var data = fs.readFileSync('sample.txt', 'utf-8');
1.1.2 实现
此处使用同步、异步方法都行,这里我使用同步获取文件的方法,由于要捕获错误,所以Try-Catch包裹。
try {
const body = fs.readFileSync(file,'utf-8')
} catch (err) {
// 出错了
}
1.2 获取抽象语法树
读取文件后我们希望获取文件的内容和依赖,可以通过正则表达式、语义分析等等,正则可以匹配但是有复杂的代码结构时,正则看起来繁琐又不能理解,所以出现了抽象语法树AST的方式,对源代码的树状表现形式,既让代码有了意义,又能让维护者容易维护。
其实无论是代码编译(babel),打包(webpack),代码压缩,css预处理,代码校验(eslint),代码美化(pretiier),这些的实现都离不开AST。只是各个过程的实现算法不同。
常见的Javascript Parser有很多:
- babylon:应用于bable
- acorn:应用于webpack
- espree:应用于eslint
本文章用到了babel所以主要看一下babel的AST以及转换方法。
1.2.1 相关知识梳理
babel
Babel 是一个编译器(输入源码 => 输出编译后的代码)。就像其他编译器一样,编译过程分为三个阶段:解析、转换和打印输出。在parse阶段,babel使用babylon库将源代码转换为AST,在transform阶段,利用各种插件进行代码转换,在generator阶段,再利用代码生成工具,将AST转换成代码。
解析
利用 @babel/parser 去解析,有两个API,分别是 parse 和 parseExpression 和 parseExpression 方法。
babelParser.parse(code, [options])
babelParser.parseExpression(code, [options])
区别是 parse 将提供的代码作为一个完整的ECMAScript程序进行解析,parseExpression 则尝试解析单个表达式并考虑性能。
其中 options 中与我们有关的便是 sourceType 参数,指示分析代码的模式。可以是"script", “module"或"unambiguous"之一。默认为"script”。 “unambiguous"将使@babel/parser尝试根据存在的ES6导入或导出语句进行猜测。带有ES6 import和export的文件被视为"module”,否则是"script"。
1.1.2 实现
我们只需要将代码作为完整的ECMAScript程序进行解析就行,所以选择 parse 方法。 sourceType 参数根据官网的定义带有ES6 import和export的文件被视为"module"。
const ast = parser.parse(body,{
sourceType:'module' //表示我们要解析的是ES模块
});
我们来看看获取到的结果
只能看出来是一个数组,对应我们的index.js文件的AST,具体内容我们通过 ast.program.body 打印出来
可以看到当前index.js的AST中,引入了add.js, minus.js。这个就是我们的依赖,所以我们要收集这个文件的这些依赖和所对应的路径。
1.3 获取依赖
由于经过上述 parse 转换得到的AST树并不能得到我们想要的依赖,所以我们对于上述结构再次进行转换,其中babel为我们提供了 babel-traverse 方法。
1.3.1 相关知识梳理
babel-traverse
是一个对ast进行遍历的工具。类似于字符串的replace方法,指定一个正则表达式,就能对字符串进行替换。只不过babel-traverse是对ast进行替换。使用ast对代码修改会更有优势,支持各种语法匹配模式,比如条件表达式、函数表达式,while循环等。
看看官网的使用:
traverse(ast, {
enter(path) {
if (
path.node.type === "Identifier" &&
path.node.name === "n"
) {
path.node.name = "x";
}
}
});
其中enter代表ast的当前节点为enter的值放入path变量中,利用path去获取其他节点属性值并做更改。
1.3.2 实现
我们看第二步中获取到的ast树,这里我们想要获取依赖的路径,得通过ImportDeclaration节点的source.value来获取依赖的文件名,拼好对应的路径。
traverse(ast,{
ImportDeclaration({node}){
const dirname = path.dirname(file);
const abspath = './' + path.join(dirname,node.source.value);
deps[node.source.value] = abspath;
}
})
其中,ImportDeclaration方法代表的是对type类型为ImportDeclaration的节点的处理;那么依赖名就是node.source.value;path.dirname()会返回当前路径;path.join()方法会将当前路径和依赖名拼接到一起,获取依赖路径。
最后我们看获取到的结果,当前index.js 文件又两个依赖,分别是add.js, minus.js,对应路径分别是’./src/add.js’, ‘./src/minus.js’。
1.4 获取内容
获取依赖后,便是转换当前文件和依赖的内容,将 es6 语法转换为浏览器能识别的 es5 代码。
1.4.1 相关知识梳理
babel同样为我们提供转译的功能,包括字符串转码、文件(同步/异步)转码、Babel AST转码。由于我们上面得到是 AST 树,所以这里利用Babel AST转码(transformFromAst 方法)。transformFromAst 方法便是利用之前得到的 AST 转换为浏览器可识别的 es5 的代码。
babel.transformFromAst(ast, code, options);
// => { code, map, ast }
其中 options 参数转换的配置对象。
@babel/preset-env 是一个智能的babel预设, 让你能使用最新的JavaScript语法, 它会帮你转换成代码的目标运行环境支持的语法, 提升你的开发效率并让打包后的代码体积更小。
1.4.2 实现
const {code} = babel.transformFromAst(ast,null,{
presets:["@babel/preset-env"]
})
转换结果就是整个js转换为es5。
1.5 总结
目前整体代码为
const getModuleInfo = (file)=>{
// 获取当前文件内容
const body = fs.readFileSync(file,'utf-8')
// 当前文件转换为AST
const ast = parser.parse(body,{
sourceType:'module'
});
// 获取当前文件依赖
const deps = {}
traverse(ast,{
ImportDeclaration({node}){
const dirname = path.dirname(file);
const abspath = './' + path.join(dirname,node.source.value)
deps[node.source.value] = abspath
}
})
// 当前文件内容转码
const {code} = babel.transformFromAst(ast,null,{
presets:["@babel/preset-env"]
})
const moduleInfo = {file,deps,code}
return moduleInfo;
}
getModuleInfo('./src/index.js');
通过以上的四步,我们就已经获取到了入口文件 index.js 的依赖,转码后的内容,打印出结果可以看到当前文件的路径
、依赖
、转译后的内容
构成的对象。
2 获取所有依赖内容并处理成想要的格式
对于入口模块得到的路径、依赖、转译后的内容,我们可以看到对于 add.js 和 minus.js 两个模块还没有处理,从模块的依赖中可以得到这两个文件的路径,再次进行上一步的单个模块获取路径、依赖、转译后的内容步骤,当直到所有模块的依赖为空的时候,才算处理完。最后将这个以所有模块得到的路径、依赖、转译后的内容为对象的数组进行转化为路径为key,依赖、内容为value的对象(这种格式方便后期立即执行函数使用)。
2.1 获取所有依赖内容
我们可以看到当前获取的内容只是入口文件的内容,那么入口文件所依赖的文件也得进行依赖查找,对应的文件内容进行转译。所以我们将入口文件 index.js 的依赖文件再次进行遍历,进行上一步的当前文件的路径、依赖、转译后的内容。层层递进,直到当前文件依赖为空。
const entry = getModuleInfo(file)
const temp = [entry]
for (let i = 0;i<temp.length;i++){
const deps = temp[i].deps
if (deps){
for (const key in deps){
if (deps.hasOwnProperty(key)){
temp.push(getModuleInfo(deps[key]))
}
}
}
}
console.log(temp)
先将入口文件的 moduleInfo 对象放入 temp 数组,然后遍历入口文件的依赖文件,依次循坏,结果如下:
2.2 处理成想要的格式
根据 webpack 打包后打印出的 moudles 结果与我们现在从上一步得到的结果 temp 数组中每一项 moduleInfo 不太一致,所以需要将 moduleInfo.file 文件路径作为对象的属性,moduleInfo.deps 依赖和 moduleInfo.code 内容作为属性值输出。
temp.forEach(moduleInfo=>{
depsGraph[moduleInfo.file] = {
deps:moduleInfo.deps,
code:moduleInfo.code
}
})
输出结果为:
2.3 总结
目前 bundle.js 是这样的
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')
// 获取单个模块依赖、内容
const getModuleInfo = (file)=>{
const body = fs.readFileSync(file,'utf-8')
const ast = parser.parse(body,{
sourceType:'module'
});
const deps = {}
traverse(ast,{
ImportDeclaration({node}){
const dirname = path.dirname(file);
const abspath = './' + path.join(dirname,node.source.value)
deps[node.source.value] = abspath
}
})
const {code} = babel.transformFromAst(ast,null,{
presets:["@babel/preset-env"]
})
const moduleInfo = {file,deps,code}
return moduleInfo;
}
// 递归获取所有模块依赖、内容并转换为文件的路径为key,{code,deps}为值的形式存储
const parseModules = (file) =>{
const entry = getModuleInfo(file)
const temp = [entry]
const depsGraph = {}
for (let i = 0;i<temp.length;i++){
const deps = temp[i].deps
if (deps){
for (const key in deps){
if (deps.hasOwnProperty(key)){
temp.push(getModuleInfo(deps[key]))
}
}
}
}
temp.forEach(moduleInfo=>{
depsGraph[moduleInfo.file] = {
deps:moduleInfo.deps,
code:moduleInfo.code
}
})
return depsGraph
}
// 从入口文件开始执行
parseModules('./src/index.js');
3 立即执行函数
经过以上步骤我们获取到了文件的路径,各个依赖,内容,将index.js 与它的依赖整合起来,当 require 调用依赖的时候,就执行依赖的内容,就实现了代码插入执行。
3.1 相关知识梳理
当 require 调用依赖的时候,就执行依赖的内容,这个地方需要用到立即执行函数,就是 eval() 函数会将传入的字符串当做 JavaScript 代码进行执行。
eval(string)
其中,string 表示 JavaScript 表达式、语句或一系列语句的字符串。表达式可以包含变量与已存在对象的属性。返回字符串中代码的返回值。如果返回值为空,则返回 undefined。
3.2 实现
3.2.1 字符串转化
首先将获取到的 moudles 转化为字符串
const depsGraph = JSON.stringify(parseModules(file));
其中 depsGraph 以每个模块的路径为key,{code,deps}(code:当前模块转译后的内容;deps:当前模块依赖)为值的形式存储的对象字符串。
3.2.2 单个模块立即执行
我们来看一下 index.js 转译的结果,
index.js
"use strict"
var _add = _interopRequireDefault(require("./add.js"));
var _minus = require("./minus.js");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
var sum = (0, _add["default"])(1, 2);
var division = (0, _minus.minus)(2, 1);
console.log(sum); console.log(division);
add.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true});
exports["default"] = void 0;
var _default = function _default(a, b) { return a + b;};
exports["default"] = _default;
执行 index.js 的时候,这个代码浏览器没办法识别 require 方法,所以我们要在立即执行函数中重新写 require 方法,在执行 require 的时候,去找引入的模块具体内容,然后执行。
所以我们的思路就是
-
立即执行函数传入 depsGraph ;
-
执行 require 入口文件方法;
-
在 depsGraph 查找入口文件的内容;
-
立即执行内容;
具体代码如下:
(function (graph) {
function require(file) {
(function (code) {
eval(code)
})(graph[file].code)
}
require(file)
})(depsGraph)
其中,depsGraph 当前项目所有依赖、路径、转译后的内容;file 为当前入口文件路径; graph[file].code 为当前入口文件转译后的内容;
3.2.3 项目立即执行
在执行内容的时候又会遇到 require 函数,给的是相对路径,那我们如何获取绝对路径并且层层递进全部给执行了呢?
上图是 depsGraph 当前 index.js 模块,相对路径可以从 depsGraph 变量中的当前模块的 deps 属性中所对应了当前模块的相对路径属性的属性值。
从而这里的流程是
- 立即执行函数传入 depsGraph ;
- 执行 require 入口文件方法;
- 在 depsGraph 查找入口文件的内容;
- 立即执行内容(执行eval(code));
- 当前模块内容(也就是执行eval(code)的时候)遇到 require(“XXX”),那么执行 absRequire ,传入模块名XXX;
- 在 depsGraph 中查找当前模块的绝对路径下的 deps 属性中所对应的相对路径为 XXX 属性的属性值,也就是 XXX 的绝对路径作为变量传入 require 函数;
- 再次执行 require 函数,回到步骤2,直到没有 require 函数;
具体代码如下:
(function (graph) {
function require(file) {
function absRequire(relPath) {
return require(graph[file].deps[relPath])
}
(function (require,code) {
eval(code)
})(absRequire,graph[file].code)
}
require(file)
})(depsGraph)
其实就是一个递归,结束条件为当前模块内容中没有 require 函数。
3.2.4 模块中的 exports 处理
在 add.js 中执行的时候又会遇到 exports 这个函数,这个还没有定义,我们看看怎么处理?
"use strict";
Object.defineProperty(exports, "__esModule", { value: true});
exports["default"] = void 0;
var _default = function _default(a, b) { return a + b;};
exports["default"] = _default;
其中, Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。我们看到的
Object.defineProperty(exports, "__esModule", { value: true});
就是直接在 exports 对象上定义一个新属性,输出的 exports 为一个对象,也就是这个模块的内容。
exports = {
__esModule:{ value: true},
default:function _default(a, b) { return a + b;}
}
从 index.js 中看到
"use strict"
var _add = _interopRequireDefault(require("./add.js"));
var _minus = require("./minus.js");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
var sum = (0, _add["default"])(1, 2);
var division = (0, _minus.minus)(2, 1);
console.log(sum); console.log(division);
interopRequireDefault 方法会将 exports 中的 default 这个属性给add,因此_add = function _default(a, b) { return a + b;}
在这里将 exports 返回就行,具体代码为
(function (graph) {
function require(file) {
function absRequire(relPath) {
return require(graph[file].deps[relPath])
}
var exports = {}
(function (require,exports,code) {
eval(code)
})(absRequire,exports,graph[file].code)
return exports
}
require(file)
})(depsGraph)
所以exports就是一个对象,ES6模块引入的是一个对象引用。
3.3 总结
当前 bundle.js 文件为
const getModuleInfo = (file)=>{
// 获取当前文件内容
const body = fs.readFileSync(file,'utf-8')
// 当前文件转换为AST
const ast = parser.parse(body,{
sourceType:'module'
});
// 获取当前文件依赖
const deps = {}
traverse(ast,{
ImportDeclaration({node}){
const dirname = path.dirname(file);
const abspath = './' + path.join(dirname,node.source.value)
deps[node.source.value] = abspath
}
})
// 当前文件内容转码
const {code} = babel.transformFromAst(ast,null,{
presets:["@babel/preset-env"]
})
const moduleInfo = {file,deps,code}
return moduleInfo;
}
// 立即执行
const bundle = (file) =>{
const depsGraph = JSON.stringify(parseModules(file))
return `(function (graph) {
function require(file) {
function absRequire(relPath) {
return require(graph[file].deps[relPath])
}
var exports = {}
(function (require,exports,code) {
eval(code)
})(absRequire,exports,graph[file].code)
return exports
}
require('${file}')
})(${depsGraph})`
}
const content = bundle('./src/index.js')
console.log(content);
打印结果为
4 打包结果放入dist文件
fs.mkdirSync('./dist');
fs.writeFileSync('./dist/bundle.js',content)
执行
node bundle.js
生成的目录结构为
其中 dist/bundle.js 文件内容为
(function (graph) {
function require(file) {
function absRequire(relPath) {
return require(graph[file].deps[relPath])
}
var exports = {}
(function (require,exports,code) {
eval(code)
})(absRequire,exports,graph[file].code)
return exports
}
require('./src/index.js')
})({"./src/index.js":{"deps":{"./add.js":"./src/add.js","./minus.js":"./src/minus.js"},"code":"\"use strict\";\n\nvar _add = _interopRequireDefault(require(\"./add.js\"));\n\nvar _minus = require(\"./minus.js\");\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nvar sum = (0, _add[\"default\"])(1, 2);\nvar division = (0, _minus.minus)(2, 1);\nconsole.log(sum);\nconsole.log(division);"},"./src/add.js":{"deps":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports[\"default\"] = void 0;\n\nvar _default = function _default(a, b) {\n return a + b;\n};\n\nexports[\"default\"] = _default;"},"./src/minus.js":{"deps":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.minus = void 0;\n\nvar minus = function minus(a, b) {\n return a - b;\n};\n\nexports.minus = minus;"}})
在 index.html 中引入 dist/bundle.js 文件,在浏览器中打开 index.html
可以看到输出了
至此,整个打包流程就结束了。
5 总结
所以我们重新梳理一下这个流程:
- 由模块内容利用AST抽象语法树获取模块的依赖、浏览器可识别的模块内容。
- 将模块路径、依赖、内容作为对象,放入全局的数组。
- 根据上一步得到的依赖进行遍历,再次进行步骤1,直到依赖为空。
- 将获取到的全局数组转换为下一步立即执行函数能用的对象格式,文件路径作为对象的属性,依赖和内容作为属性值输出的对象。
- 将步骤4拿到的对象传入立即执行函数。
- 在立即执行函数中重写 require 和 exports 方法。
- 当 require 模块的时候,立即执行模块内容并返回exports对象,实现模块动态插入。
其实就是收集所有依赖,将依赖作为参数传入到立即执行函数当中,然后通过eval来递归地执行每个依赖的code。
以上就是这个小demo利用webpack思想打包的流程,众所周知webpack打包比这个demo远远复杂,所以继续fighting吧~
欢迎各位大佬指正~
参考:
https://webpack.docschina.org/
https://babeljs.io/docs/en/babel-parser
https://nodejs.org/api/fs.html
https://nodejs.org/api/path.html
https://juejin.im/post/6844903858179670030(实现一个简单的Webpack)
https://www.lagou.com/lgeduarticle/82247.html(掌握AST,再也不怕被问babel,vue编译,Prettier等原理)
https://juejin.im/post/6844903832061739015(AST 原理分析)
https://juejin.im/post/6844904007463337997#heading-4 (Webpack4打包机制原理简析)
https://juejin.im/post/6844903802382860296(Webpack 模块打包原理)
https://juejin.im/post/6854573217336541192 (手写webpack核心原理,再也不怕面试官问我webpack原理)
https://github.com/Pines-Cheng/blog/issues/45 (Webpack将代码打包成什么样子?)