搞清 require
的原理,自己动手在浏览器中实现 require
。
开始实现之前,先来看一下 Node.js 中的 CommonJS。
Node.js 中的 CommonJS 规范
在 Node.js 中,一个文件就是一个模块。
模块的导出与导入
使用 exports
导出。
1 2 3 4 5
|
exports.add = function add (a, b) { return a + b }
|
使用 module.exports
导出。
1 2 3 4 5
|
module.exports = function mult (a, b) { return a * b }
|
区别:
模块初始化时 exports
和 module.exports
引用同一个对象,相当于是 let exports = module.exports = {}
,最后导出的是 module.exports
。
如果把 exports
的引用改掉,那就有问题了。
1 2 3 4 5 6 7 8 9
|
const c = { key: 'value' } exports = c exports.neverExport = 0
|
把 module.exports
的引用改掉,exports
也会没用。
1 2 3 4 5 6 7 8
|
module.exports = { key: 'value' } exports.neverExport = 0
|
导入模块。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const add = require('./a.js').add add(1, 2)
const mult = require('./b.js') mult(3, 4)
const c = require('./c.js') c.key c.neverExport
const d = require('./d.js') d.key d.neverExport
|
module exports require 的秘密
module
,exports
,require
这三个东西为什么在文件最开头就可以使用?它们是从哪来的?
全局变量上?试试输出 global.module
,你会发现是 undefined
实际上每个 JS 文件里的代码都会被包在一个函数里面:
1 2 3
| function (exports, require, module, __filename, __dirname) { }
|
Node.js 在加载一个 JS 文件时,会先读取文件内容,再把内容用这个函数模板包一层,然后由内部的实现来调用这个函数,传入这些参数,所以我们才能够使用 module
,exports
,require
。
在开头输出一下这三个对象:
1 2 3 4
| console.log(exports) console.log(require) console.log(module)
|
输出为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| {} [Function require] Module { id: '.', loaded: false, filename: '/path/to/index.js', exports: {}, parent: undefined, children: [], paths: [ '/path/to/node_modules', '/path/node_modules', '/node_modules', ... ] }
|
实现
可以看到 Node.js 中实现了一个 Module
类,里面存了一些模块的信息,module
就是 Module
类的实例。这里我们就不搞那么多花里胡哨的东西了,来一个最简单的实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| (function (window) { var registeredModules = {}; var installedModules = {};
function require (moduleId) { if (installedModules[moduleId]) { return installedModules[moduleId].exports; }
if (!registeredModules[moduleId]) throw new Error('Cannot find module "' + moduleId + '".');
var module = installedModules[moduleId] = { id: moduleId, loaded: false, exports: {} };
registeredModules[moduleId].call(module.exports, module.exports, require, module);
module.loaded = true;
return module.exports; }
function register (moduleId, fn) { if (typeof fn !== 'function') throw new TypeError('Module body must be a function.');
if (registeredModules[moduleId]) { return; } registeredModules[moduleId] = fn; }
function runAsMain (moduleId) { require(moduleId); }
window.cjs = { register: register, runAsMain: runAsMain }; })(window)
|
引用这段 JS 后,就可以和 Node 里一样用了。注册模块可以不考虑顺序,但是 runAsMain 必须在所有模块都注册了以后调用,还有 require 的不是文件路径。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| cjs.register('entry', function (exports, require, module) { var add = require('a').add; console.log(add(1, 2));
var mult = require('b'); console.log(mult(3, 4));
var c = require('c'); console.log(c); console.log(c.key); console.log(c.neverExport);
var d = require('d'); console.log(d); console.log(d.key); console.log(d.neverExport); });
cjs.register('a', function (exports, require, module) { exports.add = function add (a, b) { return a + b; }; });
cjs.register('b', function (exports, require, module) { module.exports = function mult (a, b) { return a * b; }; });
cjs.register('c', function (exports, require, module) { var c = { key: 'value' }; exports = c; exports.neverExport = 0; });
cjs.register('d', function (exports, require, module) { module.exports = { key: 'value' } exports.neverExport = 0; });
cjs.runAsMain('entry');
|
更完善的实现: bommon-ts