相信不少小伙伴在编写前后端分离的前端项目的时候,都用过webpack进行对模块的打包吧。这次就来尝试一下通过插件简单实js模块的模块打包功能吧。
代码放在:https://gitee.com/baojuhua/js_pack_simple/tree/master
实现原理
Babel是广为使用的 ES6 转码器,可以将 ES6 代码转化为 ES5 代码。
1. ES语法转换
使用Babylon
插件解析源码生成AST
通过babel-core
将AST转换为ES5源码
2. 获取模块依赖关系,同时将模块执行上面一部的语法转换
使用babel-traverse
插件可以获取依赖关系
使用uglify-js
插件对输出的js文件源码进行压缩
3. 生成的js文件可以在浏览器运行
创建测试用的HTML文件,引入生成的js文件,通过浏览器执行预览。
项目创建
创建的目录结构如下
- lib : 存放主要打包功能的源码文件夹
- src : 存放被打包的js源码文件架
-
- compiler.js : 用来执行与输出
-
- parser.js : 解析源码与转换
- .babelrc : Babel配置文件
- pack.js : 执行打包的入口文件
- index.html : 输出脚本测试html测试文件
- package.json : 模块的描述文件
编写代码
首先编写Babel配置文件.babelrc
{
"presets": ["@babel/preset-env"],//`@babel/preset-env` 转译包
"comments": false,
"plugins":[]
}
parser.js
parser.js最主要就是三个功能:
- 解析源码生成AST
- 获取源码依赖
- 通过AST生成目标代码
代码:
const fs = require('fs'); // 引入node中fs模块
const babylon = require('babylon'); // 通过 Babylon 生成AST
const traverse = require('babel-traverse').default; //通过 babel-traverse 的 ImportDeclaration方法获取依赖属性
const { transformFromAst } = require('babel-core'); //通过 babel-core 将AST 重新生成源码
module.exports = {
// 生成AST树
getAST: path => {
// 同步读取文件
const source = fs.readFileSync(path, 'utf-8');
// 使用babylon的parse方法进行生成AST
return babylon.parse(source, {
sourceType: 'module',
});
},
// 获取依赖
getDependencies: ast => {
const dependencies = [];
traverse(ast, {
// ImportDeclaration:分析import语句
ImportDeclaration: ({ node }) => {
// 将依赖push到dependencies中
dependencies.push(node.source.value);
},
});
return dependencies;
},
// 代码生成
transform: ast => {
const { code } = transformFromAst(ast, null, {
presets: ['env'],
});
return code;
}
};
compiler.js
compiler.js主要功能是调用parser.js进行循环递归构建模块代码,并将结果保存到文件中。
代码:
const fs = require('fs');
const path = require('path');
const { getAST, getDependencies, transform } = require('./parser');//解析与转化
var UglifyJS = require("uglify-js");//js压缩
module.exports = class Compiler {
constructor(options) {
const { entry, output } = options;
this.entry = entry;
this.output = output;
this.modules = [];//定义this.modules来填充依赖
}
run() {
// 构建主入口模块
const entryModule = this.build(this.entry, true);
// 添加依赖模块
this.modules.push(entryModule);
// 遍历递归添加依赖
this.modules.forEach(_module_ => {
_module_.dependencies.forEach(dependency => {
this.modules.push(this.build(dependency));
});
});
this.outFiles();
}
// 构建模块
build(filename, isEntry) {
let ast;
if (isEntry) {
ast = getAST(filename);
filename = path.basename(filename);
} else {
const absolutePath = path.join(process.cwd(), './src', filename + '.js');
ast = getAST(absolutePath);
}
return {
filename,
dependencies: getDependencies(ast),
source: transform(ast),
};
}
// 输出文件
outFiles() {
let outputPath = path.join(this.output.path, this.output.filename);
let modules = '';
this.modules.forEach(_module => {
modules += `'${_module.filename}': function(require,module,exports){${_module.source}},`;
});
// 加载时自动执行
const bundle = `(function(modules){
function require(filename){
var fn = modules[filename];
var module = { exports: {}};
fn(require, module, module.exports);
return module.exports;
}
require('${path.basename(this.entry)}')
})({${modules}})`;
if (!fs.existsSync(this.output.path)) fs.mkdirSync(this.output.path);
fs.writeFileSync(outputPath, bundle, 'utf-8');
outputPath = path.join(this.output.path, this.output.minimize);
fs.writeFileSync(outputPath, this.compress(bundle), 'utf-8');
}
//压缩
compress(bundle) {
return UglifyJS.minify(bundle, { mangle: { eval: true, } }).code;
}
};
pack.js
pack.js就很简单了设置配置并调用的compiler.js运行打包
代码:
const path = require('path');
const Compiler = require('./lib/compiler');
const options = {
entry: path.join(__dirname, './src/index.js'),//指定源码的入口
output: {
path: path.join(__dirname, './dist'),//打包生成的目录位置
filename: 'main.js',// 输出的文件名
minimize: 'main.min.js' // 输出被压缩的文件名
},
};
new Compiler(options).run();
测试
在src目录中编写测试文件
src/index.js:
import { demo1,demo2 } from './lib/demo';
demo1()
demo2()
src/lib/demo.js:
export function demo1() {
let h1 = document.createElement('h1');
h1.innerText = "Hello ~~"
document.body.append(h1);
document.body.style.backgroundColor = '#ddd';
}
export function demo2() {
let div = document.createElement('div');
document.body.append(div);
setInterval(() => { div.innerText = (new Date).toLocaleTimeString(); }, 1000);
}
创建html测试文件
注意script引用的文件路径要与pack.js最终生成的文件路径位置要一致。
demo
运行node pack.js
开始尝试打包
如果不出意外,会在执行的目录下生成以下文
打开浏览器进行测试
不出意外的话,将会看到以下画面