webpack|webpack打包原理浅浅析(前端打包工具)

webpack通过配置项就可以实现各种场景下的打包,那么它究竟是怎么打包的呢?网上的简易打包原理怎么看都云里雾里,不如自己悄咪咪实现一个,揭开这层神秘的面纱…
通过本文学到什么?
  1. webpack打包后的结果分析
  2. 通过demo实现webpack简易打包原理
  3. 涉及到的知识点:babel模块、fs模块、path模块
目录结构
  1. webpack4打包结果
  2. 手写demo实现简易打包过程
    1. 获取 modules (单个模块路径、模块内容、所属依赖)
      1.1 读取模块内容
      1.2 获取抽象语法树
      1.3 获取依赖
      1.4 获取内容
      1.5 总结
    2. 获取所有依赖内容并处理成想要的格式
      2.1 获取所有依赖内容
      2.2 处理成想要的格式
      2.3 总结
    3. 立即执行函数
      3.1 相关知识梳理
      3.2 实现
      3.3 总结
    4. 打包结果放入dist文件
    5. 总结
例子引入
目录结构 webpack|webpack打包原理浅浅析(前端打包工具)
文章图片

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)=>{\nreturn 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)=>{\nreturn 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"); /******/ }) /************************************************************************/ /******/ ({... /******/ });

  1. 首先定义了一个缓存区installedModules,从 return 中返回的 ./src/index.js 就是在 webpack.config.js 中定义的 entry。也就是入口文件路径。
  2. 执行 webpack_require 函数,加载 ./src/index.js 模块代码,加载的是模块路径作为 moduleId 传入。
  3. 利用先前定义的缓存区 installedModules 判断当前 moduleId (其中moduleId就是模块路径,如./src/commonjs/index.js)是否存在在缓存区 installedModules 中,如果存在从缓存取用,否则通过创建空模块,把要加载的模块内容,挂载到module.exports上,并返回。
看起来大概是这样一个思路,利用获得模块路径模块内容所属依赖这三个,分别在执行期间动态插入。那么如果我们要手动实现具体怎么操作呢?
二、下面我们来手写demo实现这个过程
目录结构 webpack|webpack打包原理浅浅析(前端打包工具)
文章图片

1. 获取 modules (单个模块路径、模块内容、所属依赖)
其中 modules 存放所有模块的数组,数组中每个元素存储{ 模块路径: 模块导出代码函数 },其中模块导出代码函数包括模块依赖和当前模块内容,都以es5的方式存储。
流程如下图所示,主要是利用AST抽象语法树,获取模块的依赖,再将当前模块内容转为浏览器可识别的es5语法,最后将当前模块路径,依赖,模块内容输出为想要的格式。
webpack|webpack打包原理浅浅析(前端打包工具)
文章图片

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 = https://www.it610.com/article/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有很多:
  1. babylon:应用于bable
  2. acorn:应用于webpack
  3. espree:应用于eslint
本文章用到了babel所以主要看一下babel的AST以及转换方法。
1.2.1 相关知识梳理 babel
Babel 是一个编译器(输入源码 => 输出编译后的代码)。就像其他编译器一样,编译过程分为三个阶段:解析、转换和打印输出。在parse阶段,babel使用babylon库将源代码转换为AST,在transform阶段,利用各种插件进行代码转换,在generator阶段,再利用代码生成工具,将AST转换成代码。
webpack|webpack打包原理浅浅析(前端打包工具)
文章图片

解析
利用 @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模块 });

我们来看看获取到的结果
webpack|webpack打包原理浅浅析(前端打包工具)
文章图片

只能看出来是一个数组,对应我们的index.js文件的AST,具体内容我们通过 ast.program.body 打印出来
webpack|webpack打包原理浅浅析(前端打包工具)
文章图片

可以看到当前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 实现 webpack|webpack打包原理浅浅析(前端打包工具)
文章图片

我们看第二步中获取到的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’。
webpack|webpack打包原理浅浅析(前端打包工具)
文章图片

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。
webpack|webpack打包原理浅浅析(前端打包工具)
文章图片

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 的依赖,转码后的内容,打印出结果可以看到当前文件的路径依赖、转译后的内容构成的对象。
webpack|webpack打包原理浅浅析(前端打包工具)
文章图片

2 获取所有依赖内容并处理成想要的格式
对于入口模块得到的路径、依赖、转译后的内容,我们可以看到对于 add.js 和 minus.js 两个模块还没有处理,从模块的依赖中可以得到这两个文件的路径,再次进行上一步的单个模块获取路径、依赖、转译后的内容步骤,当直到所有模块的依赖为空的时候,才算处理完。最后将这个以所有模块得到的路径、依赖、转译后的内容为对象的数组进行转化为路径为key,依赖、内容为value的对象(这种格式方便后期立即执行函数使用)。
webpack|webpack打包原理浅浅析(前端打包工具)
文章图片

2.1 获取所有依赖内容 我们可以看到当前获取的内容只是入口文件的内容,那么入口文件所依赖的文件也得进行依赖查找,对应的文件内容进行转译。所以我们将入口文件 index.js 的依赖文件再次进行遍历,进行上一步的当前文件的路径、依赖、转译后的内容。层层递进,直到当前文件依赖为空。
const entry =getModuleInfo(file) const temp = [entry]for (let i = 0; i

先将入口文件的 moduleInfo 对象放入 temp 数组,然后遍历入口文件的依赖文件,依次循坏,结果如下:
webpack|webpack打包原理浅浅析(前端打包工具)
文章图片

2.2 处理成想要的格式 根据 webpack 打包后打印出的 moudles 结果与我们现在从上一步得到的结果 temp 数组中每一项 moduleInfo 不太一致,所以需要将 moduleInfo.file 文件路径作为对象的属性,moduleInfo.deps 依赖和 moduleInfo.code 内容作为属性值输出。
temp.forEach(moduleInfo=>{ depsGraph[moduleInfo.file] = { deps:moduleInfo.deps, code:moduleInfo.code } })

输出结果为:
webpack|webpack打包原理浅浅析(前端打包工具)
文章图片

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{ 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 的时候,去找引入的模块具体内容,然后执行。
所以我们的思路就是
  1. 立即执行函数传入 depsGraph ;
  2. 执行 require 入口文件方法;
  3. 在 depsGraph 查找入口文件的内容;
  4. 立即执行内容;
具体代码如下:
(function (graph) { function require(file) { (function (code) { eval(code) })(graph[file].code) } require(file) })(depsGraph)

其中,depsGraph 当前项目所有依赖、路径、转译后的内容;file 为当前入口文件路径; graph[file].code 为当前入口文件转译后的内容;
3.2.3 项目立即执行 【webpack|webpack打包原理浅浅析(前端打包工具)】在执行内容的时候又会遇到 require 函数,给的是相对路径,那我们如何获取绝对路径并且层层递进全部给执行了呢?
webpack|webpack打包原理浅浅析(前端打包工具)
文章图片

上图是 depsGraph 当前 index.js 模块,相对路径可以从 depsGraph 变量中的当前模块的 deps 属性中所对应了当前模块的相对路径属性的属性值。
从而这里的流程是
  1. 立即执行函数传入 depsGraph ;
  2. 执行 require 入口文件方法;
  3. 在 depsGraph 查找入口文件的内容;
  4. 立即执行内容(执行eval(code));
  5. 当前模块内容(也就是执行eval(code)的时候)遇到 require(“XXX”),那么执行 absRequire ,传入模块名XXX;
  6. 在 depsGraph 中查找当前模块的绝对路径下的 deps 属性中所对应的相对路径为 XXX 属性的属性值,也就是 XXX 的绝对路径作为变量传入 require 函数;
  7. 再次执行 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);

打印结果为
webpack|webpack打包原理浅浅析(前端打包工具)
文章图片

4 打包结果放入dist文件
fs.mkdirSync('./dist'); fs.writeFileSync('./dist/bundle.js',content)

执行
node bundle.js

生成的目录结构为
webpack|webpack打包原理浅浅析(前端打包工具)
文章图片

其中 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\", {\nvalue: true\n}); \nexports[\"default\"] = void 0; \n\nvar _default = function _default(a, b) {\nreturn a + b; \n}; \n\nexports[\"default\"] = _default; "},"./src/minus.js":{"deps":{},"code":"\"use strict\"; \n\nObject.defineProperty(exports, \"__esModule\", {\nvalue: true\n}); \nexports.minus = void 0; \n\nvar minus = function minus(a, b) {\nreturn a - b; \n}; \n\nexports.minus = minus; "}})

在 index.html 中引入 dist/bundle.js 文件,在浏览器中打开 index.html
可以看到输出了
webpack|webpack打包原理浅浅析(前端打包工具)
文章图片

至此,整个打包流程就结束了。
5 总结
webpack|webpack打包原理浅浅析(前端打包工具)
文章图片

所以我们重新梳理一下这个流程:
  1. 由模块内容利用AST抽象语法树获取模块的依赖、浏览器可识别的模块内容。
  2. 将模块路径、依赖、内容作为对象,放入全局的数组。
  3. 根据上一步得到的依赖进行遍历,再次进行步骤1,直到依赖为空。
  4. 将获取到的全局数组转换为下一步立即执行函数能用的对象格式,文件路径作为对象的属性,依赖和内容作为属性值输出的对象。
  5. 将步骤4拿到的对象传入立即执行函数。
  6. 在立即执行函数中重写 require 和 exports 方法。
  7. 当 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将代码打包成什么样子?)

    推荐阅读