webpack打包原理,手写一个自己的bundler

  • A+
所属分类:Web前端
摘要

webpack这类的打包工具,能帮助我们把用esModule组织起来的代码打包到一个js文件中,在浏览器中运行。实现前端项目的模块化,同时优化请求数量,文件大小等。

webpack这类的打包工具,能帮助我们把用esModule组织起来的代码打包到一个js文件中,在浏览器中运行。实现前端项目的模块化,同时优化请求数量,文件大小等。

话不多说,我们自己来实现一个类似的bundler,对模块化的前端代码进行打包,输出能在浏览器运行的js文件。

准备工作

先来看看我们要处理的项目是怎么组织的,我们放一个src文件夹,里面放上index.js,hello.js,word.js每个文件的内容如下

//index.js  import hello from "./hello.js" console.log(hello) 
//hello.js  import word from './word.js' export default `hello ${word}` 
//word.js   const word = "word";  export default word; 

想干的事儿也很简单,就是用esModule的方式,最终在index.js里拼装一个console.log('hello word'),在浏览器中执行这段js,能够在控制台打印一个'hello word'。

那么我们就在src文件夹的同级创建一个bundler.js,帮助我们对代码进行打包,输入可执行的js。

解析入口文件

我们知道,webpack是通过一个entry来输入要打包文件的入口的,类似的,我们也希望通过输入文件访问地址的方式,告诉我们的bundler要把哪个文件作为入口进行打包。
先来看代码:

const fs = require('fs') const path = require('path') const paser = require('@babel/parser') const traverse = require('@babel/traverse').default const { transformFromAst } = require('@babel/core') const moduleAnalyser = (filename) => {     const content = fs.readFileSync(filename, 'utf-8');	//{1}     const ast = paser.parse(content,{			//{2}         sourceType: 'module'     })     const dependencies = {};     traverse(ast, {					//{3}         ImportDeclaration({node}){             const dirname = path.dirname(filename);             const newFile = './' + path.join(dirname, node.source.value)             dependencies[node.source.value] = newFile         }     })     const { code } = transformFromAst(ast, null, {	//{4}         presets: ["@babel/preset-env"]     })     return {         filename,         dependencies,         code     } } 
1、文件读取

我们定义一个 moduleAnalyser 方法来对模块进行分析,既然要对文件进行分析,就要用到node的fs模块,将文件读取进来。于是在{1}处,我们将文件读取了进来。

2、生成抽象语法树

拿到文件得内容之后,要对它进行解析,正好Babel提供的@babel/parser能帮我对文件进行解析,生成抽象语法树,于是我们在{2}处,对fs拿到的文件进行解析,生成了AST。如下:

{   type: 'File',   start: 0,   end: 50,   loc: SourceLocation {     start: Position { line: 1, column: 0 },     end: Position { line: 3, column: 18 },     filename: undefined,     identifierName: undefined   },   errors: [],   program: Node {     type: 'Program',     start: 0,     end: 50,     loc: SourceLocation {       start: [Position],       end: [Position],       filename: undefined,       identifierName: undefined     },     sourceType: 'module',     interpreter: null,     body: [ [Node], [Node] ],     directives: []   },   comments: [] } 

我们把重点放在program.body上,里面有两个对象,其实就是index.js中的两条语句,打印一下可以看到如下:

[   Node {     type: 'ImportDeclaration',     start: 0,     end: 30,     loc: SourceLocation {       start: [Position],       end: [Position],       filename: undefined,       identifierName: undefined     },     specifiers: [ [Node] ],     source: Node {       type: 'StringLiteral',       start: 18,       end: 30,       loc: [SourceLocation],       extra: [Object],       value: './hello.js'     }   },   Node {     type: 'ExpressionStatement',     start: 32,     end: 50,     loc: SourceLocation {       start: [Position],       end: [Position],       filename: undefined,       identifierName: undefined     },     expression: Node {       type: 'CallExpression',       start: 32,       end: 50,       loc: [SourceLocation],       callee: [Node],       arguments: [Array]     }   } ] 
3、获取依赖

看type可以知道,第一条其实就是一条引用语句,看到这儿应该就很敏感了,我们要对文件进行打包,这种引用关系当然是非常重要的。我们要接下来要继续解析,肯定要通过这样的引用关系找到被引用的文件,所以这个import的语句要存下来。好在Babel提供了@babel/traverse(遍历)方法来维护AST的整体状态,我们在{3}使用它来帮我们找出依赖模块。

值得一提的是traverse解析出来的是个相对路径,但为了方便我们接下来处理,要把这个相对路径转换成绝对路径,具体方法如代码中所示。

4、AST转可执行code

除了拿依赖关系,我们还需要把 AST 转换为浏览器可执行代码,而Babel 提供的@babel/core 和 @babel/preset-env正好可以做这个事儿,于是在{4},我们做了这一步转换。

至此,我们就完成了对一个模块的解析,不妨看一下我们会拿到什么结果:

{   filename: './src/index.js',   dependencies: { './hello.js': './src\hello.js' },   code: '"use strict";n' +     'n' +     'var _hello = _interopRequireDefault(require("./hello.js"));n' +     'n' +     'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }n' +     'n' +     'console.log(_hello["default"]);' } 

可以看到,我们知道了解析的文件是谁,有什么依赖,可执行的js代码是什么。

获取依赖图谱

到现在,我们拿到了一个模块的解析,要能完整实现一个功能,我们还需要对它所依赖的所有模块进行处理。于是需要一个方法帮我们拿到整个依赖的图谱,所以我们定义了makeDenpendenciesGraph方法帮我们做这个事。
直接先看代码:

const makeDenpendenciesGraph = (entry) => {         //分析所有依赖模块,获得依赖图谱     const entryModule = moduleAnalyser(entry);     const graph = {};     const graphArray = [ entryModule ];     while(graphArray.length > 0){         [...graphArray].forEach(item => {             graphArray.shift();             const { dependencies } = item;             graph[item.filename] = {                 dependencies: item.dependencies,                 code: item.code             }             if(dependencies) {                 for(let j in dependencies){                     graphArray.push(moduleAnalyser(dependencies[j]))                 }               }         });     }     return graph; } 

这部分其实比较简单,我们使用一个广度优先遍历,从moduleAnalyser解析出来的结果里看还有没有依赖,有的话就再继续解析出来,把所有解析的结果放到一起。看一下生成的依赖图谱:

{   './src/index.js': {     dependencies: { './hello.js': './src\hello.js' },     code: '"use strict";n' +       'n' +       'var _hello = _interopRequireDefault(require("./hello.js"));n' +       'n' +       'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }n' +       'n' +       'console.log(_hello["default"]);'   },   './src\hello.js': {     dependencies: { './word.js': './src\word.js' },     code: '"use strict";n' +       'n' +       'Object.defineProperty(exports, "__esModule", {n' +       '  value: truen' +       '});n' +       'exports["default"] = void 0;n' +       'n' +       'var _word = _interopRequireDefault(require("./word.js"));n' +       'n' +       'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }n' +       'n' +       'var _default = "hello ".concat(_word["default"]);n' +       'n' +       'exports["default"] = _default;'   },   './src\word.js': {     dependencies: {},     code: '"use strict";n' +       'n' +       'Object.defineProperty(exports, "__esModule", {n' +       '  value: truen' +       '});n' +       'exports["default"] = void 0;n' +       'var word = "word";n' +       'var _default = word;n' +       'exports["default"] = _default;'   } } 

生成可执行js

我们拿到了依赖图谱,其实就剩下最后一步了,要把解析出来的内容整合到一起,并且生成可被执行的js文件。上代码:

const generateCode = (entry) => {     const graph = makeDenpendenciesGraph(entry);     return `(function(graph){         function require(module){             function localRequire(relativePath){                 return require(graph[module].dependencies[relativePath]);             }             var exports = {};             (function(require, exports, code){                 eval(code)             })(localRequire, exports, graph[module].code);             return exports;         };         require('${entry}')     })(${JSON.stringify(graph)})`; } 

其实我们就是要把依赖图谱中的code放到一起,返回一个可执行的js,其实也就是返回了一个js字符串。

我们注意到在code中有一个require方法和一个exports对象,如果我们没定义这两个东西,js执行的时候一定会报错的。

在闭包内我们拿require作入口,又拿一个闭包把各个模块划分开防止内部变量污染。同时我们注意到code中使用的是相对路径,所以定义了一个localRequire来做一个绝对路径的转化,才能找到依赖的模块。

至此,就完成了一个对esModule组织的代码的打包,看看结果吧:

(function(graph){     function require(module){         function localRequire(relativePath){             return require(graph[module].dependencies[relativePath]);         }         var exports = {};         (function(require, exports, code){             eval(code)         })(localRequire, exports, graph[module].code);         return exports;     };     require('./src/index.js')  })({"./src/index.js":{"dependencies":{"./hello.js":"./src\hello.js"},"code":""use strict";nnvar _hello = _interopRequireDefault(require("./hello.js"));nnfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }nnconsole.log(_hello["default"]);"},"./src\hello.js":{"dependencies":{"./word.js":"./src\word.js"},"code":""use strict";nnObject.defineProperty(exports, "__esModule", {n  value: truen});nexports["default"] = void 0;nnvar _word = _interopRequireDefault(require("./word.js"));nnfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }nnvar _default = "hello ".concat(_word["default"]);nnexports["default"] = _default;"},"./src\word.js":{"dependencies":{},"code":""use strict";nnObject.defineProperty(exports, "__esModule", {n  value: truen});nexports["default"] =void 0;nvar word = "word";nvar _default = word;nexports["default"] = _default;"}}) 

放到浏览器中执行这段代码,就打印出了我们预期的 'hello word'

完整代码如下:

const fs = require('fs') const path = require('path') const paser = require('@babel/parser') const traverse = require('@babel/traverse').default const { transformFromAst } = require('@babel/core') const moduleAnalyser = (filename) => {          //解析一个模块,生成抽象语法树,并转换成好处理的对象     const content = fs.readFileSync(filename, 'utf-8');     const ast = paser.parse(content,{         sourceType: 'module'     })     const dependencies = {};     traverse(ast, {         ImportDeclaration({node}){             const dirname = path.dirname(filename);             const newFile = './' + path.join(dirname, node.source.value)             dependencies[node.source.value] = newFile         }     })     const { code } = transformFromAst(ast, null, {         presets: ["@babel/preset-env"]     })     return {         filename,         dependencies,         code     } } const makeDenpendenciesGraph = (entry) => {     const entryModule = moduleAnalyser(entry);     const graph = {};     const graphArray = [ entryModule ];     while(graphArray.length > 0){         [...graphArray].forEach(item => {             graphArray.shift();             const { dependencies } = item;             graph[item.filename] = {                 dependencies: item.dependencies,                 code: item.code             }             if(dependencies) {                 for(let j in dependencies){                     graphArray.push(moduleAnalyser(dependencies[j]))                 }               }         });     }     return graph; } const generateCode = (entry) => {     const graph = makeDenpendenciesGraph(entry);     return `(function(graph){         function require(module){             function localRequire(relativePath){                 return require(graph[module].dependencies[relativePath]);             }             var exports = {};             (function(require, exports, code){                 eval(code)             })(localRequire, exports, graph[module].code);             return exports;         };         require('${entry}')     })(${JSON.stringify(graph)})`; }  const code = generateCode('./src/index.js') console.log(code)