先来看示例代码。
我们这里三个文件,index.js 是主入口文件:
// filename: index.js
import foo from './foo.js'
foo();
//filename: foo.js
import message from './message.js'
function foo() {
console.log(message);
}
// filename: message.js
const message = 'hello world'
export default message;
复制代码
接下来,我们会创建一个 bundle.js 打包这三个文件,打包得到的结果是一个 JS 文件,运行这个 JS 文件输出的结果会是 'hello world'。
bundle.js 就是 webpack 做的事情,我们示例中的 index.js 相当于 webpack 的入口文件,会在 webpack.config.js 的 entry 里面配置。
让我们来实现 bundle.js 的功能。
最开始的,当然是读主入口文件了:
function createAssert(filename) {
const content = fs.readFileSync(filename, {
encoding: 'utf-8'
});
return content;
}
const content = createAssert('./example/index.js');
复制代码
接下来,需要做的事情就是把 import 语法引入的这个文件也找过来,在上图中,就是 foo.js,同时还得把 foo.js 依赖的也找过来,依次递推。
现在得把 foo.js 取出来,怎么解析 import foo from './foo.js' 这句,把值取出来呢?
把这行代码解析成 ast 会变成:
接下来的思路就是把上面的代码转化成 ast,接着去取上图框框里那个字段。对依赖的文件也进行读取操作:
const fs = require('fs');
const babylon = require('babylon');
const traverse = require('babel-traverse').default;
function createAssert(filename) {
const dependencies = [];
const content = fs.readFileSync(filename, {
encoding: 'utf-8'
});
const ast = babylon.parse(content, {
sourceType: 'module',
});
traverse(ast, {
ImportDeclaration: ({node}) => {
dependencies.push(node.source.value);
}
})
console.log(dependencies); // [ './foo.js' ]
return content;
}
复制代码
上面我们做的事情就是把当前的文件读到,然后再把当前文件的依赖加到一个叫做 dependencies 的数组里面去。
然后,这里的 createAssert 只返回源代码还不够,再完善一下:
let id = 0;
function getId() { return id++; }
function createAssert(filename) {
const dependencies = [];
const content = fs.readFileSync(filename, {
encoding: 'utf-8'
});
const ast = babylon.parse(content, {
sourceType: 'module',
});
traverse(ast, {
ImportDeclaration: ({ node }) => {
dependencies.push(node.source.value);
}
})
return {
id: getId(),
code: content,
filename,
dependencies,
mapping: {},
};
}
复制代码
假如对主入口文件 index.js 调用,得到的结果会是(先忽略 mapping):
我们不能只对主入口文件做这件事,得需要对所有在主入口这链上的文件做,上面 createAssert 针对一个文件做,我们基于这个函数,建一个叫做 crateGraph 的函数,里面进行递归调用:
function createGraph(entry) {
const modules = [];
createGraphImpl(
path.resolve(__dirname, entry),
);
function createGraphImpl(absoluteFilePath) {
const assert = createAssert(absoluteFilePath);
modules.push(assert);
assert.dependencies.forEach(relativePath => {
const absolutePath = path.resolve(
path.dirname(assert.filename),
relativePath
);
const child = createGraphImpl(absolutePath, relativePath);
assert.mapping[relativePath] = child.id;
});
return assert
}
return modules;
}
复制代码
运行这个函数,得到的结果如下图所示:
大家可以注意到,截图中,数组中每一项的 code 就是我们的源代码,但是这里面还残留这 import 语句,我们先使用 babel 把它转成 commonJS 。
做的也比较简单,就是用 babel 修改 createAssert 中返回值的 code:
const code = transformFromAst(ast, null, {
presets: ['env'],
}).code
复制代码
截取其中一项,结果变成了:
接下来要做的一步刚上来会比较难以理解,最关键的是我们会重写 require 函数,不妨先看:
我们新建一个函数 bundle 来处理 createGraph 函数得到的结果。
function bundle(graph) {
let moduleStr = '';
graph.forEach(module => {
moduleStr += `
${module.id}: [
// require,module,exports 作为参数传进来
// 在下面我们自己定义了,这里记作位置 1
function(require, module, exports) {
${module.code}
},
${JSON.stringify(module.mapping)}
],
`
})
const result = `
(function(modules){
function require(id) {
const [fn, mapping] = modules[id];
const module = { exports: {} }
// fn 就是上面位置 1 那个函数
fn(localRequire, module, module.exports)
function localRequire(name) {
return require(mapping[name])
}
return module.exports;
}
require(0);
})({${moduleStr}})
`
return result;
}
复制代码
最终的使用就是:
const graph = createGraph('./example/index.js');
const res = bundle(graph);
复制代码
res 就是最终打包的结果,复制整段到控制台运行,可见输出:
于是基本的功能就成功了。
(今天有点晚了,未完待续,明天我继续添加依赖成环问题、包缓存问题的解决)