代码会涉及到一些es6的转换,要安装一些相关的依赖
npm install @babel/parser @babel/traverse @babel/core @babel/preset-env -D
新建一个bundle文件夹,在文件夹下在创建一个src目录和一个bundle.js文件,src目录下分别创建一个index.js、message.js、word.js文件。代码如下:
// word.js
export const word = 'chenshun'
// message.js
import { word } from './word.js';const message = `hello ${word}`export default message;
// index.js
import message from './message.js'console.log(message)
以下是bundle的编写
首先要读取文件信息,导入相关的依赖包
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')
文件信息必须包含文件名,该文件依赖的文件以及经bable转化后可以在浏览器执行的代码
function readFile(filename){
// 读入文件
const content = fs.readFileSync(filename, 'utf-8')
const ast = parser.parse(content, {
sourceType: 'module' // 为了识别ES Module
})
const dependencies = {}
// 遍历AST抽象语法树 traverse(ast, {
// 获取通过import引入的模块
ImportDeclaration({node}){
const dirname = path.dirname(filename)
const newFile = './' + path.join(dirname, node.source.value)
// 保存所依赖的模块
dependencies[node.source.value] = newFile
}
})
// 通过@babel/core和@babel/preset-env进行代码的转换
const {code} = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"]
})
return{
filename,
dependencies, // 该文件所依赖的模块集合(键值对存储)
code // 转化后的代码
}
}
生成依赖图谱
function generateDependentGraph(entry){
const entryModule = readFile(entry)
// 开始数组只有一个元素
const graphArray = [entryModule]
for(let i = 0; i < graphArray.length; i++){
const item = graphArray[i];
const { dependencies } = item;
for(let j in dependencies){ // 把文件所有的依赖保存进数组
graphArray.push(
readFile(dependencies[j])
)
}
}
// 以健值对的形式存储依赖图谱
const graph = {}
graphArray.forEach(item => {
graph[item.filename] = {
dependencies: item.dependencies,
code: item.code
}
})
return graph
}
console.log(generateDependentGraph('./src/index.js'));
// 生成的依赖图谱如下:
{
'./src/index.js': {
dependencies: { './message.js': './src/message.js' },
code: '"use strict";\n' +
'\n' +
'var _message = _interopRequireDefault(require("./message.js"));\n' +
'\n' +
'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +
'\n' +
'//index.js\n' +
'console.log(_message["default"]);'
},
'./src/message.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 = require("./word.js");\n' +
'\n' +
'//message.js\n' +
'var message = "say ".concat(_word.word);\n' +
'var _default = message;\n' +
'exports["default"] = _default;'
},
'./src/word.js': {
dependencies: {},
code: '"use strict";\n' +
'\n' +
'Object.defineProperty(exports, "__esModule", {\n' +
' value: true\n' +
'});\n' +
'exports.word = void 0;\n' +
'//word.js\n' +
"var word = 'hello';\n" +
'exports.word = word;'
}
}
生成可以在浏览器执行的代码,这段代码会比较绕
function generateCode(entry){
const graph = JSON.stringify(generateDependentGraph(entry))
// 为了避免污染全局环境,必须以闭包的形式返回一段可以立即执行的代码,这里返回一个立即执行函数
return `
(function(graph) {
//require函数的本质是执行一个模块的代码,然后将相应变量挂载到exports对象上
function require(module) {
// 每次require一个文件都必须传入的是绝对路径,
// 找到文件的绝对路径后以递归的形式再次调用require函数并传入改路径
// 同时必须在新的函数作用域中重新定义require函数
function localRequire(relativePath) {
return require(graph[module].dependencies[relativePath]);
}
var exports = {}; // 不同模块之间实际上是以exports来进行通信的
// 转化后的代码会调用require函数,浏览器不能识别,必须自己定义
(function(require, exports, code) {
eval(code);
})(localRequire, exports, graph[module].code);
// 下一个函数会引用exports变量,通过改变exports变量值的形式继续往下传递值,
// 达到不同模块进行通信的目的
return exports;
}
require('${entry}')
})(${graph})`
}
测试
const code = generateCode('./src/index.js')console.log(code)
// 生成的代码如下
(function(graph) {
//require函数的本质是执行一个模块的代码,然后将相应变量挂载到exports对象上
function require(module) {
// 每次require一个文件都必须传入的是绝对路径,
// 找到文件的绝对路径后以递归的形式再次调用require函数并传入改路径
// 同时必须在新的函数作用域中重新定义require函数,目的是
//拿到当前执行文件所依赖的文件的绝对路径
function localRequire(relativePath) {
return require(graph[module].dependencies[relativePath]);
}
var exports = {}; // 不同模块之间实际上是以exports来进行通信的
// 转化后的代码会调用require函数,浏览器不能识别,必须自己定义
(function(require, exports, code) {
eval(code);
})(localRequire, exports, graph[module].code);
// 下一个函数会引用exports变量,通过改变exports变量值的形式继续往下传递值,
// 达到不同模块进行通信的目的
return exports;
}
require('./src/index.js')
})({"./src/index.js":{"dependencies":{"./message.js":"./src/message.js"},"code":"\"use strict\";\n\nvar _message = _interopRequireDefault(require(\"./message.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\n//index.js\nconsole.log(_message[\"default\"]);"},"./src/message.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 = require(\"./word.js\");\n\nvar message = \"hello \".concat(_word.word);\nvar _default = message;\nexports[\"default\"] = _default;"},"./src/word.js":{"dependencies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.word = void 0;\n// word.js\nvar word = 'chenshun';\nexports.word = word;"}})
放在浏览器执行
大功告成