本文首发于知乎专栏:
http://zhuanlan.zhihu.com/starkwang前端
CommonJS 是一个流行的前端模块化规范,也是目前 NodeJS 以及其模块托管仓库 npm 使用的规范,但目前暂无浏览器支持 CommonJS 。要想让浏览器用上这些模块,必须转换格式。node
这个系列的文章,咱们会一步步完成一个基于 CommonJS 的打包工具,相似于一个简单版的 Browserify 或者 Webpack 。webpack
与 NodeJS 环境不一样,浏览器中不支持 CommonJS 的主要缘由是缺乏了如下几个环境变量:web
modulenpm
exports浏览器
require模块化
global函数
换句话说,打包器的原理就是模拟这四个变量的行为。工具
好比咱们有一个index.js
文件,依赖了module1
和module2
两个模块,而且module1
依赖module2
:ui
//index.js var module1 = require("./module1"); var module2 = require("./module2"); module1.foo(); module2.foo(); function hello(){ console.log("Hello!"); } module.exports = hello;
//module1.js var module2 = require(module2); console.log("initialize module1"); console.log("this is module2.foo() in module1:"); module2.foo(); console.log("\n") module.exports = { foo: function(){ console.log("module1 foo !!!"); } };
//module2.js console.log("initialize module2"); module.exports = { foo: function(){ console.log("module2 foo !!!"); } };
把它放入一个匿名函数内,经过这个匿名函数注入 require
、modules
、export
、global
变量(咱们暂时不实现global)
function(module, exports, require, global){ var module1 = require("./module1"); var module2 = require("./module2"); module1.foo(); module2.foo(); function hello(){ console.log("Hello!"); } module.exports = hello; }
如今咱们用一个 modules
对象来存入这些匿名函数:
//modules { "entry": function(module, exports, require, global){ //index.js var module1 = require("./module1"); var module2 = require("./module2"); module1.foo(); module2.foo(); function hello(){ console.log("Hello!"); } module.exports = hello; }, "./module1": function(module, exports, require, global){ var module2 = require("./module2"); console.log("initialize module1"); console.log("this is module2.foo() in module1:"); module2.foo(); console.log("\n") module.exports = { foo: function(){ console.log("module1 foo !!!"); } }; }, "./module2": function(module, exports, require, global){ console.log("initialize module2"); module.exports = { foo: function(){ console.log("module2 foo !!!"); } }; } }
下面咱们实现一个简单的 require
函数:
//这个对象用于储存已导入的模块 var installedModules = {}; function require(moduleName) { //若是模块已经导入,那么直接返回它的exports if(installedModules[moduleName]){ return installedModules[moduleName].exports; } //模块初始化 var module = installedModules[moduleName] = { exports: {}, name: moduleName, loaded: false }; //执行模块内部的代码,这里的 modules 变量即为咱们在上面写好的 modules 对象 modules[moduleName].call(module.exports, module, module.exports,require); //模块导入完成 module.loaded = true; //将模块的exports返回 return module.exports; }
最后只要把咱们上面写好的 modules
对象以当即执行函数的形式传入这个 require
函数就能够了,如下是完整的代码:
(function(modules){ //这个对象用于储存已导入的模块 var installedModules = {}; function require(moduleName) { //若是模块已经导入,那么直接返回它的exports if(installedModules[moduleName]){ return installedModules[moduleName].exports; } //模块初始化 var module = installedModules[moduleName] = { exports: {}, name: moduleName, loaded: false }; //执行模块内部的代码,这里的 modules 变量即为咱们在上面写好的 modules 对象 modules[moduleName].call(module.exports, module, module.exports,require); //模块导入完成 module.loaded = true; //将模块的exports返回 return module.exports; } //入口函数 return require("entry"); })({ "entry": function(module, exports, require, global){ //index.js var module1 = require("./module1"); var module2 = require("./module2"); module1.foo(); module2.foo(); function hello(){ console.log("Hello!"); } module.exports = hello; }, "./module1": function(module, exports, require, global){ var module2 = require("./module2"); console.log("initialize module1"); console.log("this is module2.foo() in module1:"); module2.foo(); console.log("\n") module.exports = { foo: function(){ console.log("module1 foo !!!"); } }; }, "./module2": function(module, exports, require, global){ console.log("initialize module2"); module.exports = { foo: function(){ console.log("module2 foo !!!"); } }; } });
事实上,咱们短短的这几十行代码模仿了 Webpack 的部分实现。但咱们依然在使用诸如 "./module1"
这样的字符串做为模块的惟一识别码,这是一个明显的缺陷,存在多层级文件时,这个名称很容易冲突。
在 Browserify 或 Webpack 这样的生产级工具里,通常使用数字做为函数的惟一识别码,例如它可能会把(以 Webpack 为例):
var module1 = require("./module1");
编译成:
var module1 = __webpack_require__(1);
咱们在这里实现了一个最简单的 CommonJS 标准的执行器,接下来的文章中咱们会作如下事情:
一、实现 global 变量
二、用 moduleID 替代 moduleName
三、写一个命令行小工具
四、支持 node_modules 和多层级文件