# 通过babel手撸超简化版webpack
本文讲述如何通过babel对文件及其依赖进行解析编译打包,示例代码参见babeltry (opens new window)
# 目录结构
src:用于测试的打包项目
dist:用于存放打包生成的文件,test.html用于测试生成文件效果
babeltry.config.js:配置文件
index.js:打包工具入口
lib:打包工具的依赖方法
# 打包效果
源文件
src/index.js
import { greeting } from "./greeting.js";
document.write(greeting('world'));
src/greeting.js
import { str } from "./hello.js";
var greeting = function(name) {
return str + ' ' + name;
}
export { greeting }
src/hello.js
var str = 'hello';
export { str }
打包文件
dist/main.js
(function(modules){
function require(filepath){
const fn = modules[filepath];
const moudle = { exports: {} };
fn(require, moudle, moudle.exports);
return moudle.exports
}
require('E:\项目文件夹\项目资料\其他\babeltry\src\index.js')
})({'E:\项目文件夹\项目资料\其他\babeltry\src\index.js': function (require, moudle, exports) {"use strict";
var _greeting = require("./greeting.js");
document.write((0, _greeting.greeting)('world'));},'./greeting.js': function (require, moudle, exports) {"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.greeting = undefined;
var _hello = require("./hello.js");
var greeting = function greeting(name) {
return _hello.str + ' ' + name;
};
exports.greeting = greeting;},'./hello.js': function (require, moudle, exports) {"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
var str = 'hello';
exports.str = str;},})
dist/test.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="./main.js"></script>
</head>
<body>
</body>
</html>
# 开始编写打包工具
# package.json
所需依赖,依赖的具体作用我们会在解析器部分讲解
"devDependencies": {
"babel-core": "^6.26.3",
"babel-preset-env": "^1.7.0",
"babel-traverse": "^6.26.0",
"babylon": "^6.18.0"
}
# babeltry.config.js
'use strict'
const path = require('path');
module.exports = {
entry: path.join(__dirname, '/src/index.js'),
output: {
path: path.join(__dirname, '/dist'),
filename: 'main.js'
}
};
设置需要打包的项目入口和打包结果出口位置与文件名
# index.js
const Compiler = require('./lib/compiler');
const config = require('./babeltry.config');
new Compiler(config).run();
index.js文件内容相当简单,就是加载配置和编译器,实例化一个编译器并执行run方法
# parser.js 解析器
在编写编译器之前,我们先看一看解析器怎么实现
const fs = require('fs');
const babylon = require('babylon');
const traverse = require('babel-traverse').default;
const core = require('babel-core');
module.exports = {
getAST: (path) => {
const file = fs.readFileSync(path, 'utf-8');
return babylon.parse(file, {
sourceType: 'module'
})
},
getNode: (ast) => {
const nodes = [];
traverse(ast, {
ImportDeclaration: ({ node }) => {
nodes.push(node.source.value)
}
});
return nodes
},
transform: (ast) => {
const { code } = core.transformFromAst(ast, null, {
presets: ["env"]
});
return code
}
}
引入依赖:
babylon: 通过babylon的parse方法生成AST抽象语法树,关于抽象语法树这里就不展开讲解,网上有很多相关的文章资料可以查阅
babel-traverse:通过babel-traverse遍历AST,ImportDeclaration用于获取依赖节点
babel-core:通过babel-core的transformFromAst方法生成源码
解析器导出了三个方法:
getAST:接收一个文件路径作为入参,读取文件并解析出AST
getNode:接收AST作为入参,返回一个依赖文件路径的数组
transform:接收AST作为入参,返回源码
# compiler.js 编译器
const fs = require('fs');
const path = require('path');
const { getAST, getNode, transform } = require('./parser');
module.exports = class Compiler {
constructor(options) {
const { entry, output } = options;
this.entry = entry;
this.output = output;
this.modules = [];
}
run() { }
buildMoudle() { }
fillFile() { }
}
编译器类的构造函数接收一个配置项,声明三个变量,分变是入口、出口、以及表示依赖文件列表的数组。
编译器类有三个方法,run方法即编译器的主体方法,执行解析和编译,buildMoudle方法接收依赖路径,并生成一个代表依赖文件的对象,即modules数组存储的每一项,fillFile方法负责生成打包文件。
先看buildMoudle方法
buildMoudle(filepath, isEntry) {
let ast;
if (isEntry) {
ast = getAST(filepath);
} else {
let absolutePath = path.join(process.cwd(), '/src', filepath);
ast = getAST(absolutePath);
}
return {
filepath,
nodes: getNode(ast),
code: transform(ast)
}
}
buildMoudle接收两个参数,filepath为依赖文件路径,isEntry表示是否为入口文件。
主要做了三个动作,即调用解析器的三个方法,先通过getAST获取AST抽象语法树,再将AST作为入参,调用getNode和transform,分别获取当前解析文件的依赖文件路径数组和源码。
返回一个代表当前依赖文件的对象,filepath为依赖路径,nodes为当前解析文件的依赖文件路径数组,code为源码。
run() {
const entryModule = this.buildMoudle(this.entry, true);
this.modules.push(entryModule);
// 深度遍历依赖
for(let i = 0; i < this.modules.length;i++){
let _moudle = this.modules[i];
_moudle.nodes.map((node)=>{
this.modules.push(this.buildMoudle(node));
});
}
this.fillFile();
}
run方法从入口文件开始,遍历modules依赖数组,深度遍历所有依赖文件,构建所有依赖文件对象并存储在modules中,最后调用fillFile生成打包文件。
fillFile() {
let moudles = '';
this.modules.map((_moudle)=>{
moudles += `'${_moudle.filepath}': function (require, moudle, exports) {${_moudle.code}},`
})
const bundle = `
(function(modules){
function require(filepath){
const fn = modules[filepath];
const moudle = { exports: {} };
fn(require, moudle, moudle.exports);
return moudle.exports
}
require('${this.entry}')
})({${moudles}})
`;
const outpath = path.join(this.output.path, this.output.filename);
fs.writeFileSync(outpath, bundle, 'utf-8');
}
fillFile生成打包文件,打包文件主体就是调用了一个匿名方法。
该匿名方法接收一个由我们的依赖文件列表生成的对象,对象的每一个属性都是依赖文件路径,值为一个方法(为方便记忆,我们称之为fn),fn方法接收三个参数(require, moudle, exports),方法内容即为依赖文件的源码。
该方法的主体声明了require方法,并以入口文件路径为入参调用了require,由此进入递归循环。
require方法接收一个路径作为入参,通过这个路径我们可以通过闭包拿到匿名方法入参对象中对应的fn方法,同时声明一个有exports属性的moudle对象,然后调用fn,返回moudle.exports。
说起来似乎有点绕,总的理解其实fn的内容,就是我们编译后的那些依赖文件比如greeting.js的内容,每当遇到一个require请求依赖文件,我们就执行这个依赖文件的内容,并返回导出结果。
通过示例的main.js的执行逻辑应该就能理解。