死灵法术。
在我看来,现有的答案还远远不够。
起初,它非常令人困惑。
你有一个(从未定义过的)函数 "require",用于获取模块。
而在这些(CommonJS)模块中,你可以使用 require、exports和module
,
而它们从未被定义过。
虽然在JS中使用未定义的变量并不新鲜,但你不能使用未定义的函数。
所以一开始看起来有点像魔法。
但所有的魔法都基于欺骗。
当你深入挖掘时,它实际上非常简单:
Require只是一个(非标准的)函数
定义在全局范围。
(或者更确切地说,在伪全局范围;浏览器中的全局范围=窗口对象,在
NodeJS中的全局对象)。
请注意,默认情况下,“require函数”仅在NodeJS中实现,而不在浏览器中实现。
此外,请注意增加混淆的是,对于浏览器,有RequireJS,尽管名称包含字符“require”,但RequireJS绝对不实现require/CommonJS - 而是实现AMD,这是类似但不同的东西(也就是不兼容)。
这最后一点是你理解require的重要事情之一。
现在,作为这样的回答,“require是什么”,我们“简单地”需要知道这个函数做什么。
这可能最好通过代码来解释。
这里有一个由Michele Nasti提供的简单实现, 你可以在他的github页面上找到相关代码。
我们将我们的最小化"require"函数实现称为"myRequire":
function myRequire(name)
{
console.log(`Evaluating file ${name}`);
if (!(name in myRequire.cache)) {
console.log(`${name} is not in cache; reading from disk`);
let code = fs.readFileSync(name, 'utf8');
let module = { exports: {} };
myRequire.cache[name] = module;
let wrapper = Function("require, exports, module", code);
wrapper(myRequire, module.exports, module);
}
console.log(`${name} is in cache. Returning it...`);
return myRequire.cache[name].exports;
}
myRequire.cache = Object.create(null);
window.require = myRequire;
const stuff = window.require('./main.js');
console.log(stuff);
现在你注意到了,这里使用了对象“fs”。
为了简单起见,Michele只是导入了NodeJS的fs模块:
const fs = require('fs');
这并不是必需的。
因此,在浏览器中,您可以使用同步 XmlHttpRequest 简单实现 require:
const fs = {
file: `
// module.exports = \"Hello World\";
module.exports = function(){ return 5*3;};
`
, getFile(fileName: string, encoding: string): string
{
let client = new XMLHttpRequest();
client.open("GET", fileName, false);
client.send();
if (client.status === 200)
return client.responseText;
return null;
}
, readFileSync: function (fileName: string, encoding: string): string
{
return this.file;
}
};
基本上,所需的代码是这样的,它会下载一个JavaScript文件,将其在匿名命名空间(也称为函数)中进行eval操作,使用参数"require"、"exports"和"module",然后返回exports,即对象的公共函数和属性。
请注意,这个评估过程是递归的:你需要文件,这些文件可以再次需要其他文件。
这样一来,你模块中使用的所有“全局”变量都是require包装函数命名空间中的变量,不会污染全局范围中不需要的变量。
此外,这种方式使您能够在不依赖命名空间的情况下重用代码,从而在JavaScript中实现了“模块化”。“模块化”打了引号,因为这并不完全准确,因为您仍然可以编写window.bla/global.bla,从而仍然会污染全局范围...此外,这种方式还建立了私有和公共函数之间的分离,其中公共函数即为exports。
现在,不再需要说
module.exports = function(){ return 5*3;};
你也可以说:
function privateSomething()
{
return 42:
}
function privateSomething2()
{
return 21:
}
module.exports = {
getRandomNumber: privateSomething
,getHalfRandomNumber: privateSomething2
};
并返回一个对象。
另外,由于您的模块在带有参数“require”、“exports”和“module”的函数中评估,因此您的模块可以使用未声明的变量“require”、“exports”和“module”,这可能会令人吃惊。那里的 require 参数当然是指保存在变量中的 require 函数的指针。
很酷,对吧?
从这个角度来看,require 失去了其魔力,变得简单。
现在,真正的 require 函数会做更多的检查和奇怪的事情,当然,但这就是它的实质。
另外,在2020年及以后,您应该使用ECMA实现而不是require:
import defaultExport from "module-name";
import * as name from "module-name";
import { export1 } from "module-name";
import { export1 as alias1 } from "module-name";
import { export1 , export2 } from "module-name";
import { foo , bar } from "module-name/path/to/specific/un-exported/file";
import { export1 , export2 as alias2 , [...] } from "module-name";
import defaultExport, { export1 [ , [...] ] } from "module-name";
import defaultExport, * as name from "module-name";
import "module-name";
如果您需要动态的非静态导入(例如根据浏览器类型加载polyfill),则可以使用ECMA-import函数/关键字:
var promise = import("module-name");
请注意,import不像require一样同步。
相反,import是一个Promise。
var something = require("something");
成为
var something = await import("something");
因为导入返回一个承诺(异步)。
所以基本上,与 require 不同,import 用 fs.readFileAsync 替换了 fs.readFileSync。
async readFileAsync(fileName, encoding)
{
const textDecoder = new TextDecoder(encoding);
const response = await fetch(fileName);
console.log(response.ok);
console.log(response.status);
console.log(response.statusText);
let buffer = await response.arrayBuffer();
let file = textDecoder.decode(buffer);
return file;
}
当然,这需要导入函数也是异步的。
"use strict";
async function myRequireAsync(name) {
console.log(`Evaluating file ${name}`);
if (!(name in myRequireAsync.cache)) {
console.log(`${name} is not in cache; reading from disk`);
let code = await fs.readFileAsync(name, 'utf8');
let module = { exports: {} };
myRequireAsync.cache[name] = module;
let wrapper = Function("asyncRequire, exports, module", code);
await wrapper(myRequireAsync, module.exports, module);
}
console.log(`${name} is in cache. Returning it...`);
return myRequireAsync.cache[name].exports;
}
myRequireAsync.cache = Object.create(null);
window.asyncRequire = myRequireAsync;
async () => {
const asyncStuff = await window.asyncRequire('./main.js');
console.log(asyncStuff);
};
更好了,是吧?
嗯,除了没有ECMA的动态同步导入方式(没有promise),其他都很好。
现在,为了理解其影响,如果您不知道这是什么,您绝对可能想要在此阅读关于promises/async-await,
但简单来说,如果函数返回一个promise,它可以被“awaited”:
"use strict";
function sleep(interval)
{
return new Promise(
function (resolve, reject)
{
let wait = setTimeout(function () {
clearTimeout(wait);
resolve();
}, interval);
});
}
然后,Promise 通常会像这样使用:
function testSleep()
{
sleep(3000).then(function ()
{
console.log("Waited for 3 seconds");
});
}
但是当你返回一个Promise时,你也可以使用await,这意味着我们摆脱了回调(有点 - 实际上,它被编译器/解释器中的状态机所取代)。
通过这种方式,我们使异步代码感觉像同步代码,因此现在我们可以使用try-catch来处理错误。
请注意,如果您想在函数中使用await,则该函数必须声明为async(因此是async-await)。
async function testSleep()
{
await sleep(5000);
console.log("i waited 5 seconds");
}
请注意,在JavaScript中,无法从同步函数(您知道的那些函数)中阻塞地调用异步函数。因此,如果您想使用await(又名ECMA-import),则所有代码都需要是异步的,这很可能会成为一个问题,如果不是所有代码都已经是异步的话...
require的简化实现失败的一个例子是当您需要一个无效的JavaScript文件时,例如当您需要css、html、txt、svg和图像或其他二进制文件时。
很容易看出原因:
如果您将HTML放入JavaScript函数体中,您当然会得到...
SyntaxError: Unexpected token '<'
由于 Function("bla", "<doctype...")
现在,如果您想将其扩展到例如包括非模块,您可以仅检查下载的文件内容是否为code.indexOf("module.exports") == -1
(或xml请求mime类型),然后添加jQuery作为脚本标记(eval不同)而不是Func(只要您在浏览器中就可以正常工作)。由于使用Fetch / XmlHttpRequests下载受同源策略限制,并且完整性由SSL / TLS确保,因此在这里使用eval相当无害,前提是在将它们添加到您的站点之前检查了JS文件,但这应该是标准操作程序。
请注意,有几种类似于require的功能实现:
CommonJS (CJS) 格式, 在 Node.js 中使用,采用 require 函数和 module.exports 定义依赖关系和模块。npm 生态系统是建立在这个格式之上的。(以上就是所实现的内容)
Asynchronous Module Definition (AMD) 格式, 用于浏览器中,采用 define 函数定义模块。(基本上,这是过时的复杂垃圾,你永远不想使用它)。此外,AMD 是由 RequireJS 实现的格式(请注意,尽管名称包含“require”字符,但 AMD 绝对不是 CommonJS)。
ES 模块(ESM)格式。自 ES6(ES2015)以来,JavaScript 支持本地模块格式。它使用 export 关键字导出模块的公共 API 和 import 关键字导入它。如果你不关心老旧浏览器,如 Safari 和 IE/EdgeHTML,那么这是你应该使用的格式。
System.register 格式,设计用于支持 ES6 模块在 ES5 中使用。(如果你需要支持旧浏览器(Safari 和 IE 以及手机/平板电脑上的旧版本 Chrome),因为它可以加载所有格式(对于某些内容,需要插件),可以处理循环依赖,以及 CSS 和 HTML。不过,不要把你的模块定义为 system.register 格式——这个格式相当复杂,并且记住,它可以读取其他更简单的格式)
Universal Module Definition (UMD) 格式,与上述所有格式(除 ECMA 外)兼容,在浏览器和 Node.js 中都可以使用。它特别适用于编写可在 NodeJS 和浏览器中使用的模块。它有一些缺陷,因为它不支持最新的 ECMA 模块,但是它可以使用 system.register 代替。
关于函数参数“exports”的重要说明:
JavaScript使用值共享调用,这意味着对象作为指针传递,但指针值本身是按值传递的,而不是按引用传递的。因此,您不能通过分配新对象来覆盖exports。相反,如果要覆盖exports,则需要将新对象分配给module.exports,因为嘿,module是按值传递的指针,但module.exports中的exports是对原始exports指针的引用。
关于模块范围的重要说明:
模块被评估一次,然后由require缓存。
这意味着所有模块都具有单例范围。
如果您想要非单例范围,您必须执行类似以下操作:
var x = require("foo.js").createInstance();
或者简单地说
var x = require("foo.js")();
通过你的模块返回适当的代码。
如果你需要在浏览器中支持CommonJS(IE5+,Chrome,Firefox),
请查看我的代码 我在Michele Nasti的项目中的评论。