安装babel插件
- 由于ES6转ES5中需要用到babel,所以要用到一下插件
npm install @babel/core @babel/parser @babel/traverse @babel/preset-env --save-dev
目录结构及代码:
- demo
- entry.js
- message.js
- name.js
- bundler.js //最后打包的文件
// entry.js
import message from './message.js';
console.log(message);
// message.js
import {name} from './name.js';
export default `hello ${name}!`;
// name.js
export const name = 'world';
读取文件信息,获取当前js文件的依赖关系
const fs = require("fs");
const path = require("path");
const babylon = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const babel = require("@babel/core");
let ID = 0;
//读取文件信息,并获得当前js文件的依赖关系
function createAsset(filename) {
//获取文件,返回值是字符串
const content = fs.readFileSync(filename, "utf-8");
//讲字符串为ast(抽象语法树, 这个是编译原理的知识,说得简单一点就是,可以把js文件里的代码抽象成一个对象,代码的信息会存在对象中)
//babylon 这个工具是是负责解析字符串并生产ast。
const ast = babylon.parse(content, {
sourceType: "module",
});
//用来存储 文件所依赖的模块,简单来说就是,当前js文件 import 了哪些文件,都会保存在这个数组里
const dependencies = [];
//遍历当前ast(抽象语法树)
traverse(ast, {
//找到有 import语法 的对应节点
ImportDeclaration: ({ node }) => {
//把当前依赖的模块加入到数组中,其实这存的是字符串,
//例如 如果当前js文件 有一句 import message from './message.js',
//'./message.js' === node.source.value
dependencies.push(node.source.value);
},
});
//模块的id 从0开始, 相当一个js文件 可以看成一个模块
const id = ID++;
//这边主要把ES6 的代码转成 ES5
const { code } = babel.transformFromAstSync(ast, null, {
presets: ["@babel/preset-env"],
});
return {
id,
filename,
dependencies,
code,
};
}
广度遍历获取所有依赖图
//从入口开始分析所有依赖项,形成依赖图,采用广度遍历
function createGraph(entry) {
const mainAsset = createAsset(entry);
console.log(mainAsset);
//既然要广度遍历肯定要有一个队列,第一个元素肯定是 从 "./demo/entry.js" 返回的信息
const queue = [mainAsset];
for (const asset of queue) {
//获取文件夹路径
const dirname = path.dirname(asset.filename);
//新增一个属性来保存子依赖项的数据
//保存类似 这样的数据结构 ---> {"./message.js" : 1}
asset.mapping = {};
//采用广度遍历
asset.dependencies.forEach((relativePath) => {
const absolutePath = path.join(dirname, relativePath);
//获得子依赖(子模块)的依赖项、代码、模块id,文件名
const child = createAsset(absolutePath);
//给子依赖项赋值,
asset.mapping[relativePath] = child.id;
//将子依赖也加入队列中,广度遍历
queue.push(child);
});
}
return queue;
}
- mapping这个字段是把当前模块依赖的文件名称 和 模块的id 做一个映射,目的是为了更方便查找模块
生成浏览器可执行代码
- 其实bundle函数就是返回我们构造的字符串,拿到字符串,我们把字符串导出成bundle.js。
//根据生成的依赖关系图,生成对应环境能执行的代码,目前是生产浏览器可以执行的
function bundle(graph) {
let modules = "";
console.log(graph);
//循环依赖关系,并把每个模块中的代码存在function作用域里
//转换依赖图格式 => {id:[fn,mapping],...}
//会作为最后生成代码的入参
graph.forEach((mod) => {
modules += `${mod.id}:[
function (require, module, exports){
${mod.code}
},
${JSON.stringify(mod.mapping)},
],`;
});
//require, module, exports 是 cjs的标准不能再浏览器中直接使用,所以这里模拟cjs模块加载,执行,导出操作。
const result = `
(function(modules){
//创建require函数, 它接受一个模块ID(这个模块id是数字0,1,2) ,它会在我们上面定义 modules 中找到对应是模块.
function require(id){
const [fn, mapping] = modules[id];
function localRequire(relativePath){
//根据模块的路径在mapping中找到对应的模块id
return require(mapping[relativePath]);
}
const module = {exports:{}};
//执行每个模块的代码。
fn(localRequire,module,module.exports);
return module.exports;
}
//执行入口文件,
require(0);
})({${modules}})
`;
return result;
}
const graph = createGraph("./demo/entry.js");
const ret = bundle(graph);
// 打包生成文件
fs.writeFileSync("./bundle.js", ret);
最终生成的可执行内容:
(function (modules) {
//创建require函数, 它接受一个模块ID(这个模块id是数字0,1,2) ,它会在我们上面定义 modules 中找到对应是模块.
function require(id) {
const [fn, mapping] = modules[id];
function localRequire(relativePath) {
//根据模块的路径在mapping中找到对应的模块id
return require(mapping[relativePath]);
}
const module = { exports: {} };
//执行每个模块的代码。
fn(localRequire, module, module.exports);
return module.exports;
}
//执行入口文件,
require(0);
})({
0: [
function (require, module, exports) {
"use strict";
var _message = _interopRequireDefault(require("./message.js"));
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
console.log(_message["default"]);
},
{ "./message.js": 1 },
],
1: [
function (require, module, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports["default"] = void 0;
var _name = require("./name.js");
var _default = "hello ".concat(_name.name, "!");
exports["default"] = _default;
},
{ "./name.js": 2 },
],
2: [
function (require, module, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.name = void 0;
var name = "world";
exports.name = name;
},
{},
],
});
代码示例:
const fs = require("fs");
const path = require("path");
const babylon = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const babel = require("@babel/core");
let ID = 0;
//读取文件信息,并获得当前js文件的依赖关系
function createAsset(filename) {
//获取文件,返回值是字符串
const content = fs.readFileSync(filename, "utf-8");
//讲字符串为ast(抽象语法树, 这个是编译原理的知识,说得简单一点就是,可以把js文件里的代码抽象成一个对象,代码的信息会存在对象中)
//babylon 这个工具是是负责解析字符串并生产ast。
const ast = babylon.parse(content, {
sourceType: "module",
});
//用来存储 文件所依赖的模块,简单来说就是,当前js文件 import 了哪些文件,都会保存在这个数组里
const dependencies = [];
//遍历当前ast(抽象语法树)
traverse(ast, {
//找到有 import语法 的对应节点
ImportDeclaration: ({ node }) => {
//把当前依赖的模块加入到数组中,其实这存的是字符串,
//例如 如果当前js文件 有一句 import message from './message.js',
//'./message.js' === node.source.value
dependencies.push(node.source.value);
},
});
//模块的id 从0开始, 相当一个js文件 可以看成一个模块
const id = ID++;
//这边主要把ES6 的代码转成 ES5
const { code } = babel.transformFromAstSync(ast, null, {
presets: ["@babel/preset-env"],
});
return {
id,
filename,
dependencies,
code,
};
}
//从入口开始分析所有依赖项,形成依赖图,采用广度遍历
function createGraph(entry) {
const mainAsset = createAsset(entry);
console.log(mainAsset);
//既然要广度遍历肯定要有一个队列,第一个元素肯定是 从 "./demo/entry.js" 返回的信息
const queue = [mainAsset];
for (const asset of queue) {
const dirname = path.dirname(asset.filename);
//新增一个属性来保存子依赖项的数据
//保存类似 这样的数据结构 ---> {"./message.js" : 1}
asset.mapping = {};
//采用广度遍历
asset.dependencies.forEach((relativePath) => {
const absolutePath = path.join(dirname, relativePath);
//获得子依赖(子模块)的依赖项、代码、模块id,文件名
const child = createAsset(absolutePath);
//给子依赖项赋值,
asset.mapping[relativePath] = child.id;
//将子依赖也加入队列中,广度遍历
queue.push(child);
});
}
return queue;
}
//根据生成的依赖关系图,生成对应环境能执行的代码,目前是生产浏览器可以执行的
function bundle(graph) {
let modules = "";
console.log(graph);
//循环依赖关系,并把每个模块中的代码存在function作用域里
graph.forEach((mod) => {
modules += `${mod.id}:[
function (require, module, exports){
${mod.code}
},
${JSON.stringify(mod.mapping)},
],`;
});
//require, module, exports 是 cjs的标准不能再浏览器中直接使用,所以这里模拟cjs模块加载,执行,导出操作。
const result = `
(function(modules){
//创建require函数, 它接受一个模块ID(这个模块id是数字0,1,2) ,它会在我们上面定义 modules 中找到对应是模块.
function require(id){
const [fn, mapping] = modules[id];
function localRequire(relativePath){
//根据模块的路径在mapping中找到对应的模块id
return require(mapping[relativePath]);
}
const module = {exports:{}};
//执行每个模块的代码。
fn(localRequire,module,module.exports);
return module.exports;
}
//执行入口文件,
require(0);
})({${modules}})
`;
return result;
}
const graph = createGraph("./demo/entry.js");
const ret = bundle(graph);
// 打包生成文件
fs.writeFileSync("./bundle.js", ret);