Necromancing。
恕我直言,现有的答案还有很多不尽如人意之处。
一开始,这很令人困惑。
你有一个(没有定义的)函数“require”,用于获取模块。
在说(CommonJS)模块中,你可以使用require, exports和module,而不需要定义它们。
在JS中使用未定义的变量并不是什么新鲜事,但你不能使用未定义的函数。
一开始看起来有点像魔法。
但所有的魔法都建立在欺骗的基础上。
When you dig a little deeper, it turns out it is really quite simple:
Require is simply a (non-standard) function defined at global scope.
(global scope = window-object in browser, global-object in NodeJS).
Note that by default, the "require function" is only implemented in NodeJS, not in the browser.
Also, note that to add to the confusion, for the browser, there is RequireJS, which, despite the name containing the characters "require", RequireJS absolutely does NOT implement require/CommonJS - instead RequireJS implements AMD, which is something similar, but not the same (aka incompatible).
That last one is just one important thing you have to realize on your way to understanding 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');
这是不必要的。
所以在浏览器中,你可以用一个SYNCHRONOUS XmlHttpRequest简单实现require:
const fs = {
file: `
// module.exports = \"Hello World\";
module.exports = function(){ return 5*3;};
`
, getFile(fileName: string, encoding: string): string
{
// https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Synchronous_and_Asynchronous_Requests
let client = new XMLHttpRequest();
// client.setRequestHeader("Content-Type", "text/plain;charset=UTF-8");
// open(method, url, async)
client.open("GET", fileName, false);
client.send();
if (client.status === 200)
return client.responseText;
return null;
}
, readFileSync: function (fileName: string, encoding: string): string
{
// this.getFile(fileName, encoding);
return this.file; // Example, getFile would fetch this file
}
};
基本上,require所做的就是下载一个javascript文件,在一个匿名的命名空间(又名Function)中求值,参数为“require”,“exports”和“module”,并返回导出,这意味着一个对象的公共函数和属性。
注意,这个计算是递归的:您需要文件,文件本身也可以需要文件。
这样,模块中使用的所有“全局”变量都是需求包装器函数名称空间中的变量,并且不会用不需要的变量污染全局作用域。
此外,通过这种方式,您可以在不依赖于名称空间的情况下重用代码,从而获得JavaScript中的“模块化”。"modularity"加引号,因为这并不完全正确,因为你仍然可以编写window.bla/global。呸,因此仍然污染全球范围…此外,这也建立了私人功能和公共功能之间的分离,公共功能是出口。
现在不要说
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函数当然会做更多的检查和奇怪的事情,但这是归根结底的本质。
此外,在2020年,您应该使用ECMA实现,而不是要求:
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那样是同步的。
相反,导入是一个承诺,所以
var something = require("something");
就变成了
var something = await import("something");
因为import返回一个承诺(异步)。
基本上,不像require, import替换fs。使用fs.readFileAsync。
async readFileAsync(fileName, encoding)
{
const textDecoder = new TextDecoder(encoding);
// textDecoder.ignoreBOM = true;
const response = await fetch(fileName);
console.log(response.ok);
console.log(response.status);
console.log(response.statusText);
// let json = await response.json();
// let txt = await response.text();
// let blo:Blob = response.blob();
// let ab:ArrayBuffer = await response.arrayBuffer();
// let fd = await response.formData()
// Read file almost by line
// https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamDefaultReader/read#Example_2_-_handling_text_line_by_line
let buffer = await response.arrayBuffer();
let file = textDecoder.decode(buffer);
return file;
} // End Function readFileAsync
当然,这也要求导入函数是异步的。
"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方法来动态同步导入(没有承诺)。
现在,为了理解其影响,如果你不知道promises/async-await是什么,你可能会想要阅读一下。
但简单地说,如果一个函数返回一个承诺,它可以被“等待”:
"use strict";
function sleep(interval)
{
return new Promise(
function (resolve, reject)
{
let wait = setTimeout(function () {
clearTimeout(wait);
//reject(new Error(`Promise timed out ! (timeout = ${timeout})`));
resolve();
}, interval);
});
}
承诺通常是这样使用的:
function testSleep()
{
sleep(3000).then(function ()
{
console.log("Waited for 3 seconds");
});
}
但是当你返回一个承诺时,你也可以使用await,这意味着我们摆脱了回调(实际上,它在编译器/解释器中被一个状态机所取代)。
通过这种方式,我们可以使异步代码看起来像同步代码,因此现在可以使用try-catch进行错误处理。
注意,如果你想在函数中使用await,该函数必须声明为async(因此是async-await)。
async function testSleep()
{
await sleep(5000);
console.log("i waited 5 seconds");
}
同时请注意,在JavaScript中,没有办法从同步函数(你知道的那些)调用异步函数(以阻塞的方式)。所以如果你想使用await (aka ECMA-import),你所有的代码都需要是异步的,这很可能是一个问题,如果一切都不是异步的……
这种简化的require实现失败的一个例子是,当你需要一个不是有效的JavaScript文件时,例如,当你需要css, html, txt, svg和图像或其他二进制文件时。
原因很简单:
如果你把HTML放到JavaScript函数体中,你当然会得到
SyntaxError: Unexpected token '<'
因为函数("bla", "<doctype…")
Now, if you wanted to extend this to for example include non-modules, you could just check the downloaded file-contents for code.indexOf("module.exports") == -1, and then e.g. eval("jquery content") instead of Func (which works fine as long as you're in the browser). Since downloads with Fetch/XmlHttpRequests are subject to the same-origin-policy, and integrity is ensured by SSL/TLS, the use of eval here is rather harmless, provided you checked the JS files before you added them to your site, but that much should be standard-operating-procedure.
注意,有几种require类功能的实现:
the CommonJS (CJS) format, used in Node.js, uses a require function and module.exports to define dependencies and modules. The npm ecosystem is built upon this format. (this is what is implemented above)
the Asynchronous Module Definition (AMD) format, used in browsers, uses a define function to define modules. (basically, this is overcomplicated archaic crap that you wouldn't ever want to use). Also, AMD is the format that is implemented by RequireJS (note that despite the name containing the characters "require", AMD absolutely is NOT CommonJS).
the ES Module (ESM) format. As of ES6 (ES2015), JavaScript supports a native module format. It uses an export keyword to export a module’s public API and an import keyword to import it. This is the one you should use if you don't give a flying f*ck about archaic browsers, such as Safari and IE/EdgeHTML.
the System.register format, designed to support ES6 modules within ES5. (the one you should use, if you need support for older browsers (Safari & IE & old versions of Chrome on mobile phones/tablets), because it can load all formats [for some, plugins are required], can handle cyclic-dependencies, and CSS and HTML - don't define your modules as system.register, though - the format is rather complicated, and remember, it can read the other easier formats)
the Universal Module Definition (UMD) format, compatible to all the above mentioned formats (except ECMA), used both in the browser and in Node.js. It’s especially useful if you write modules that can be used in both NodeJS and the browser. It's somewhat flawed, as it doesn't support the latest ECMA modules, though (maybe this will get fixed) - use System.register instead.
关于函数参数“exports”的重要提示:
JavaScript使用按值共享调用——这意味着对象作为指针传递,但指针值本身是按值传递的,而不是通过引用。所以你不能通过给exports分配一个新对象来覆盖它。相反,如果您想重写导出,则需要将新对象分配给模块。导出——因为module是按值传递的指针,但是在module中导出。Exports是对原始导出指针的引用。
关于module-Scope的重要提示:
模块只计算一次,然后由require缓存。
这意味着你的所有模块都有一个单例作用域。
如果你想要一个非单例作用域,你必须这样做:
var x = require("foo.js").createInstance();
或者简单地
var x = require("foo.js")();
使用模块返回的适当代码。