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

话不多说,咱们本人来实现一个相似的bundler,对模块化的前端代码进行打包,输入能在浏览器运行的js文件。

筹备工作

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

//index.jsimport hello from "./hello.js"console.log(hello)
//hello.jsimport 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').defaultconst { 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: true\n' +      '});\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: true\n' +      '});\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\";\n\nvar _hello = _interopRequireDefault(require(\"./hello.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log(_hello[\"default\"]);"},"./src\\hello.js":{"dependencies":{"./word.js":"./src\\word.js"},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports[\"default\"] = void 0;\n\nvar _word = _interopRequireDefault(require(\"./word.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nvar _default = \"hello \".concat(_word[\"default\"]);\n\nexports[\"default\"] = _default;"},"./src\\word.js":{"dependencies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\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').defaultconst { 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)