实现一个 webpack

在浏览器中是不能够直接使用模块化的,尽管现在已经支持了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 文件代码。

我们可以发现,上面的代码在全局下暴露了 listrequest 我们可以继续使用函数作用域。

(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);
上一篇:Node.js学习九(模块化)


下一篇:JavaScript进阶(十二)JS 模块化编程规范-CommonJS、AMD、CMD、ES6