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: 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').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)