在浏览器中是不能够直接使用模块化的,尽管现在已经支持了Es Module ,但是还需要进一步的转换。webpack 可以将我们的模块进行打包成 bundle 文件 ,为浏览器能够识别的语法。
add.js 文件
exports.default = (a, b) => a + b;
index.js 文件
const add = require('add.js').default;
console.log(add(2, 4));
上面的 exports 和 require 浏览器是不能够识别的。但是我们可以模拟一个 exports 和 require 来在浏览器上运行。
const exports = {}; // 在 CommonJS 中 exports 指向一个对象.
exports.default = function (a, b) {return a + b};
exports.default(2, 4);
上面的代码可以正常的运行,并且得到 6
,但是这个相当于 add.js
文件将会暴露在全局下面。如果 add.js
文件还有其他的变量,将会造成环境的污染。所以我们需要提取到一个函数作用域下。
const exports = {};
(function (exports, code) {
eval(code);
})(exports, 'exports.default = function (a, b) {return a + b}');
使用 eval 是因为,Node 使用 fs
读取文件读取出来的是字符串。所以,我们需要借助eval
来解析字符串。
接下来我们完成 require
来模拟导入。
function request() {
const exports = {};
(function (exports, code) {
eval(code);
})(exports, 'exports.default = function (a, b) {return a + b}');
}
这样子,我们就完成了仿写require
,但是这种写死的,我们需要的是可以根据依赖来进行导入文件。
const list = {
'add.js': `exports.default = function (a, b) {return a + b}`,
'index.js': `
var add = require('add.js').default;
console.log(add(2, 4));
`
}
function request(file) {
const exports = {};
(function (exports, code) {
eval(code);
})(exports, list[file]);
}
require('index.js');
这样子,我们就可以先从 index.js
开始,加载index.js
的文件,然后加载过程中,又遇到一个 require
,然后加载 add.js
文件,执行自执行 add.js
文件代码。
我们可以发现,上面的代码在全局下暴露了 list
和 request
我们可以继续使用函数作用域。
(function (list) {
function request(file) {
const exports = {};
(function (exports, code) {
eval(code);
})(exports, list[file]);
return exports;
}
request('index.js');
})({
'add.js': `exports.default = function (a, b) {return a + b}`,
'index.js': `
var add = require('add.js').default;
console.log(add(2, 4));
`
})
现在就完成了一个类似的可以在浏览器运行的。但是我们的 list
是写死的,我们需要自动读取文件,自动收集依赖,打包 bundle.js
。我们可以使用 Node
来完成。
实现。
我们的实现可以分为三部分:收集依赖、ES6转ES5、替换 exports 和 require。
我们使用 Es Module 的 import 和 export 关键字来导入导出。add.js 文件
export default (a, b) => a + b;
index.js 文件
import add from 'add.js';
console.log(add(2, 4));
我们要想收集依赖,首先得知道入口文件依赖了哪些文件。根据 import
关键字就可以知道应该要收集依赖项了。
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const babel = require('@babel/core');
function getModuleInfo(file) {
const body = fs.readFileSync(file, 'utf-8');
const ast = parser.parse(body, {
sourceType: 'module'
});
const deps = {};
traverse(ast, {
ImportDeclaration({node}) {
const dirname = path.dirname(file);
const abspath = './' + path.join(dirname, node.source.value);
deps[node.source.value] = abspath;
}
})
const {code} = babel.transformFromAst(ast, null, {
presets: ['@babel/preset-env']
});
const moduleInfo = {file, deps, code};
return moduleInfo;
}
function parseModules(file) {
const entry = getModuleInfo(file);
const temp = [entry];
const depsGraph = {};
getDeps(temp, entry);
temp.forEach(info => {
depsGraph[info.file] = {
deps: info.deps,
code: info.code
}
})
return depsGraph;
}
function getDeps(temp, {deps}) {
Object.keys(deps).forEach((key) => {
const child = getModuleInfo(deps[key])
temp.push(child);
getDeps(temp, child);
})
}
function bundle(file) {
const depsGraph = JSON.stringify(parseModules(file));
return `
(function (graph) {
function require(file) {
function absRequire(relPath) {
return require(graph[file].deps[relPath])
}
var exports = {};
(function (require, exports, code) {
eval(code);
})(absRequire, exports, graph[file].code)
return exports;
}
})(${depsGraph})
`;
}
const content = bundle('./src/index.js');
!fs.existsSync('./dist') && fs.mkdirSync('./dist');
fs.writeFileSync('./dist/bundle.js', content);