vscode-loader 解析(node 环境)
VsCode 源码使用 vscode-loader 加载模块(异步模块定义 (AMD) 加载器的一种实现);vscode-loader 支持在浏览器以及 node 中使用,在两种环境的使用方式基本一致。 本文是对其在浏览器环境运行进行分析。前面,我们已经分析过浏览器环境的模块加载,接下来看 node 环境的模块加载。
示例
vscode-loader 在 node 环境下加载模块,和浏览器环境基本一致。不同点是,不是通过 script 标签加载 loader.js,而是通过 require 加载。
具体示例如下:
加载 loader.js,再调用 loader 模块的方法,加载 test 依赖:
loader = require("./src/loader");
// 设置缓存
loader.config({
nodeCachedData: {
path: "./cache-data",
},
});
loader(["test"], function (test) {
console.log(test.compare(7, 5));
});
定义模块:
// test.js
define("test", function () {
return {
compare: function (a, b) {
return a > b;
},
};
});
loader 加载模块
上一篇文章中,我们提到,浏览器环境通过 require 函数加载模块。而 node 环境,是通过 loader.js 的模块导出值,加载其他模块。
查看入口文件中的逻辑,可以看出 loader 就是 require 函数:
// 初始化
export function init(): void {
...
if (env.isNode && !env.isElectronRenderer) {
// 设置 module.expots
module.exports = RequireFunc;
require = <any>RequireFunc;
} else {
...
}
}
所以,node 环境加载模块的逻辑,和浏览器基本是一致的。不同点在于,之前提到的,不同环境下的 scriptLoader
(脚本加载器)。
NodeScriptLoader
下面,我们具体来看 node 的脚本加载器。
class OnlyOnceScriptLoader implements IScriptLoader {
...
public load(moduleManager: IModuleManager, scriptSrc: string, callback: () => void, errorback: (err: any) => void): void {
if (!this._scriptLoader) {
if (this._env.isWebWorker) {
this._scriptLoader = new WorkerScriptLoader();
} else if (this._env.isElectronRenderer) {
// electron 渲染进程,preferScriptTags 指定是否用 <script> 标签加载,默认为 false
const { preferScriptTags } = moduleManager.getConfig().getOptionsLiteral();
if (preferScriptTags) {
this._scriptLoader = new BrowserScriptLoader();
} else {
this._scriptLoader = new NodeScriptLoader(this._env);
}
} else if (this._env.isNode) {
// node 环境,新建 node 的脚本加载器
this._scriptLoader = new NodeScriptLoader(this._env);
} else {
this._scriptLoader = new BrowserScriptLoader();
}
}
...
}
}
class NodeScriptLoader implements IScriptLoader {
private static _BOM = 0xFEFF;
private static _PREFIX = '(function (require, define, __filename, __dirname) { ';
private static _SUFFIX = '\n});';
...
public load(moduleManager: IModuleManager, scriptSrc: string, callback: () => void, errorback: (err: any) => void): void {
// load-1: 获取配置
const opts = moduleManager.getConfig().getOptionsLiteral();
const nodeRequire = ensureRecordedNodeRequire(moduleManager.getRecorder(), (opts.nodeRequire || global.nodeRequire)); // nodeRequire 增加事件记录
const nodeInstrumenter = (opts.nodeInstrumenter || function (c) { return c; }); // 如果设置了 nodeInstrumenter,在脚本加载之前,会先对脚本执行该转换函数
// load-2: 初始化
this._init(nodeRequire);
this._initNodeRequire(nodeRequire, moduleManager);
let recorder = moduleManager.getRecorder();
// load-3: 加载模块
if (/^node\|/.test(scriptSrc)) {
// 'node|' 开头的,用 nodeRequire 加载(同步加载),直接调用 callback
let pieces = scriptSrc.split('|');
let moduleExports = null;
try {
moduleExports = nodeRequire(pieces[1]);
} catch (err) {
errorback(err);
return;
}
moduleManager.enqueueDefineAnonymousModule([], () => moduleExports);
callback();
} else {
// load-3-1: 路径处理
scriptSrc = Utilities.fileUriToFilePath(this._env.isWindows, scriptSrc);
const normalizedScriptSrc = this._path.normalize(scriptSrc);
const vmScriptPathOrUri = this._getElectronRendererScriptPathOrUri(normalizedScriptSrc);
const wantsCachedData = Boolean(opts.nodeCachedData);
const cachedDataPath = wantsCachedData ? this._getCachedDataPath(opts.nodeCachedData!, scriptSrc) : undefined;
// load-3-2: 获取模块代码和缓存,执行代码
this._readSourceAndCachedData(normalizedScriptSrc, cachedDataPath, recorder, (err: any, data: string, cachedData: Buffer, hashData: Buffer) => {
if (err) {
errorback(err);
return;
}
// 处理模块代码
let scriptSource: string;
if (data.charCodeAt(0) === NodeScriptLoader._BOM) {
scriptSource = NodeScriptLoader._PREFIX + data.substring(1) + NodeScriptLoader._SUFFIX;
} else {
scriptSource = NodeScriptLoader._PREFIX + data + NodeScriptLoader._SUFFIX;
}
scriptSource = nodeInstrumenter(scriptSource, normalizedScriptSrc);
// 生成并执行脚本
const scriptOpts: INodeVMScriptOptions = { filename: vmScriptPathOrUri, cachedData };
const script = this._createAndEvalScript(moduleManager, scriptSource, scriptOpts, callback, errorback);
// 处理、验证缓存
this._handleCachedData(script, scriptSource, cachedDataPath!, wantsCachedData && !cachedData, moduleManager);
this._verifyCachedData(script, scriptSource, cachedDataPath!, hashData, moduleManager);
});
}
}
初始化
这里,我们先看 load-2 初始化的处理:
class NodeScriptLoader implements IScriptLoader {
private _init(nodeRequire: (nodeModule: string) => any): void {
if (this._didInitialize) {
return;
}
this._didInitialize = true;
// 获取 node 原生模块
this._fs = nodeRequire('fs');
this._vm = nodeRequire('vm');
this._path = nodeRequire('path');
this._crypto = nodeRequire('crypto');
}
// 修补 nodejs 的 require 函数,以便我们可以从缓存数据手动创建脚本。这是通过覆盖 `Module._compile` 函数来完成的。
private _initNodeRequire(nodeRequire: (nodeModule: string) => any, moduleManager: IModuleManager): void {
// require-1: 如果已经打过补丁,直接返回
const { nodeCachedData } = moduleManager.getConfig().getOptionsLiteral();
if (!nodeCachedData) {
return;
}
if (this._didPatchNodeRequire) {
return;
}
this._didPatchNodeRequire = true;
// require-2: 修改 Module.compile
const that = this
const Module = nodeRequire('module');
function makeRequireFunction(mod: any) {
...
}
Module.prototype._compile = function (content: string, filename: string) {
...
}
}
}
可以看到,_initNodeRequire 修改了 node 的 require
函数,主要是改写了 Module.prototype._compile
。
node 的 _compile
我们先了解一下 node 的 require
函数,及 Module.prototype._compile
,以便后续对比。这里的 node 代码为 14.0.0 版本。
先看 require
函数:
// require: 根据路径加载模块,返回模块的 exports 属性。
Module.prototype.require = function(id) {
...
// 第一步:调用 Module._load
return Module._load(id, this, /* isMain */ false);
};
// Module._load:加载模块、管理缓存
Module._load = function(request, parent, isMain) {
...
// 1. 如果缓存中已存在模块,返回模块的 exports
const cachedModule = Module._cache[filename];
if (cachedModule !== undefined) {
updateChildren(parent, cachedModule, true);
if (!cachedModule.loaded)
return getExportsForCircularRequire(cachedModule);
return cachedModule.exports;
}
...
// 2. 如果是原生模块,调用 `NativeModule.prototype.compileForPublicLoader()` 并返回 exports
const mod = loadNativeModule(filename, request);
if (mod && mod.canBeRequiredByUsers) return mod.exports;
...
// 3. 否则,新建一个模块并保存到缓存,加载文件,返回 exports
const module = new Module(filename, parent);
...
Module._cache[filename] = module;
try {
...
// 第二步:调用 Module.prototype.load
module.load(filename);
...
} finally {
...
}
return module.exports;
}
// Module.prototype.load: 根据文件名,调用合适的扩展处理器。
Module.prototype.load = function (filename) {
...
// 第三步:调用对应的处理器,比如 Module._extensions['.js']
Module._extensions[extension](this, filename);
this.loaded = true;
...
};
Module._extensions['.js'] = function (module, filename) {
...
content = fs.readFileSync(filename, 'utf8');
// 第四步:调用 Module.prototype._compile
module._compile(content, filename);
};
从上面代码,可以看出 node 的 require
函数,执行过程是 require
-> Module._load
-> Module.prototype.load
-> Module._extensions['.js']
-> Module.prototype._compile
。
继续看 node 的 Module.prototype._compile
:
// node
// Module.prototype._compile: 在指定的上下文中,编译、运行文件内容。
Module.prototype._compile = function (content, filename) {
...
// node-compile-1:compiledWrapper: 将文件内容进行封装
const compiledWrapper = wrapSafe(filename, content, this);
...
// node-compile-2:生成 require, exports 等参数
const dirname = path.dirname(filename);
const require = makeRequireFunction(this, redirects);
let result;
const exports = this.exports;
const thisValue = exports;
const module = this;
...
if (inspectorWrapper) {
// 断点调试,一般不走这个逻辑
result = inspectorWrapper(compiledWrapper, thisValue, exports,
require, module, filename, dirname);
} else {
// node-compile-3:调用 compiledWrapper
result = compiledWrapper.call(thisValue, exports, require, module,
filename, dirname);
}
...
return result;
};
// 封装文件内容
function wrapSafe(filename, content, cjsModuleInstance) {
if (patched) {
// node-compile-1-1:Module.wrap,封装文件内容,返回 (function (exports, require, module, __filename, __dirname) { ${content} \n})
const wrapper = Module.wrap(content);
// node-compile-1-2:vm.runInThisContext,调用虚拟机接口,编译代码,并在当前上下文执行代码
return vm.runInThisContext(wrapper, {
filename,
lineOffset: 0,
displayErrors: true,
importModuleDynamically: async (specifier) => {
const loader = asyncESM.ESMLoader;
return loader.import(specifier, normalizeReferrerURL(filename));
},
});
}
...
}
node 的 Module.prototype._compile
,将文件内容进行封装(compiledWrapper),然后生成 require 等参数,再调用封装的函数(compiledWrapper)。
node-compile-1-2 使用了 node 的 vm 模块,该模块支持编译代码、运行代码等功能。
vscode-loader 的 _compile
vscode-loader 的 Module.prototype._compile
,逻辑如下:
class NodeScriptLoader implements IScriptLoader {
private _initNodeRequire(nodeRequire: (nodeModule: string) => any, moduleManager: IModuleManager): void {
...
Module.prototype._compile = function (content: string, filename: string) {
// compile-1: 替换 shebang,包装源码
const scriptSource = Module.wrap(content.replace(/^#!.*/, ''));
// compile-2: 获取缓存数据,并记录事件
const recorder = moduleManager.getRecorder();
// 对于示例而言,缓存路径为 cache-data/test-${hash}.code
const cachedDataPath = that._getCachedDataPath(nodeCachedData, filename);
const options: INodeVMScriptOptions = { filename };
let hashData: Buffer | undefined;
try {
// 读取缓存数据
const data = that._fs.readFileSync(cachedDataPath);
hashData = data.slice(0, 16);
// 设置到 options.cachedData
options.cachedData = data.slice(16);
recorder.record(LoaderEventType.CachedDataFound, cachedDataPath);
} catch (_e) {
recorder.record(LoaderEventType.CachedDataMissed, cachedDataPath);
}
// compile-3: 新建 vm.Script,编译代码
const script = new that._vm.Script(scriptSource, options);
// compile-4: 生成 compileWrapper,用于在当前上下文运行代码
const compileWrapper = script.runInThisContext(options);
// compile-5: 生成 require 等参数
const dirname = that._path.dirname(filename);
const require = makeRequireFunction(this);
const args = [this.exports, require, this, filename, dirname, process, _commonjsGlobal, Buffer];
// compile-6: 执行 compileWrapper,传入参数
const result = compileWrapper.apply(this.exports, args);
// compile-7: 缓存数据
that._handleCachedData(script, scriptSource, cachedDataPath, !options.cachedData, moduleManager);
that._verifyCachedData(script, scriptSource, cachedDataPath!, hashData, moduleManager);
return result;
}
}
}
和 node 的相似之处:
compile-1
,等同于node-compile-1-1
,通过Module.wrap
封装文件内容。这样可以保证代码在独立上下文中运行。compile-3
、compile-4
,等同于node-compile-1-2
,调用虚拟机接口runInThisContext
,用于在当前上下文执行代码。compile-5
,等同于node-compile-2
,生成 require 等参数。compile-6
,等同于node-compile-3
,执行封装后的代码。
和 node 的不同之处:
compile-2
,增加了获取缓存,并记录缓存事件的逻辑。compile-3
,编译代码时,options 中传入了缓存。compile-5
,改写了makeRequireFunction
。compile-7
,执行代码后,缓存了数据。
可以看出,vscode-loader 的 Module.prototype._compile
,主要是增加了缓存的逻辑,改写了 makeRequireFunction
。
makeRequireFunction
的对比如下:
// node
function makeRequireFunction(mod, redirects) {
const Module = mod.constructor;
let require;
if (redirects) {
// 处理重定向
const { resolve, reaction } = redirects;
const id = mod.filename || mod.id;
require = function require(path) {
// node 协议,加载原生模块,返回 exports
// 文件协议,调用 mode.require,加载文件
...
return mod.require(path);
};
} else {
// 非重定向,直接调用 mod.require
require = function require(path) {
return mod.require(path);
};
}
function resolve(request, options) {
validateString(request, 'request');
return Module._resolveFilename(request, mod, false, options);
}
require.resolve = resolve;
function paths(request) {
validateString(request, 'request');
return Module._resolveLookupPaths(request, mod);
}
resolve.paths = paths;
require.main = process.mainModule;
require.extensions = Module._extensions;
require.cache = Module._cache;
return require;
}
// vscode-loader
function makeRequireFunction(mod: any) {
const Module = mod.constructor;
// 直接调用 mod.require
let require = <any>function require(path) {
try {
return mod.require(path);
} finally {
// nothing
}
}
require.resolve = function resolve(request, options) {
return Module._resolveFilename(request, mod, false, options);
};
require.resolve.paths = function paths(request) {
return Module._resolveLookupPaths(request, mod);
};
require.main = process.mainModule;
require.extensions = Module._extensions;
require.cache = Module._cache;
return require;
}
可以看到,由于 vscode-loader 的 Module.prototype._compile
没有重定向的情况,所以 makeRequireFunction
中的 require,删除了重定向处理。而 require 的其他属性,和 node 保持一致,没有修改。
vscode-loader 的缓存处理
vscode-loader 通过 config 来设置缓存目录:
// 设置缓存
loader.config({
nodeCachedData: {
path: "./cache-data",
},
});
compile-2
通过 _getCachedDataPath
获取缓存路径:
// compile-2
const cachedDataPath = that._getCachedDataPath(nodeCachedData, filename);
// 对于示例而言,缓存路径为 cache-data/test-${hash}.code
private _getCachedDataPath(config: INodeCachedDataConfiguration, filename: string): string {
// 根据文件名、配置等生成 hash 值
const hash = this._crypto.createHash('md5').update(filename, 'utf8').update(config.seed!, 'utf8').update(process.arch, '').digest('hex');
const basename = this._path.basename(filename).replace(/\.js$/, '');
return this._path.join(config.path, `${basename}-${hash}.code`);
}
compile-2
读取缓存后,并通过 options 传入 vm.script,以使用缓存数据:
// compile-2
try {
const cachedDataPath = that._getCachedDataPath(nodeCachedData, filename);
// 读取缓存数据
const data = that._fs.readFileSync(cachedDataPath);
hashData = data.slice(0, 16);
// 设置到 options.cachedData
options.cachedData = data.slice(16);
...
} catch (_e) {
...
}
// options 中包含 cachedData
const script = new that._vm.Script(scriptSource, options);
执行源文件的代码后,compile-7
更新和校验缓存数据:
// compile-7
that._handleCachedData(script, scriptSource, cachedDataPath, !options.cachedData, moduleManager);
that._verifyCachedData(script, scriptSource, cachedDataPath!, hashData, moduleManager);
// 处理缓存数据:如果缓存失败,就删除原来的缓存,重新生成缓存;如果 options 没有缓存数据,就生成缓存数据
private _handleCachedData(script: INodeVMScript, scriptSource: string, cachedDataPath: string, createCachedData: boolean, moduleManager: IModuleManager): void {
if (script.cachedDataRejected) {
// cached data got rejected -> delete and re-create
this._fs.unlink(cachedDataPath, err => {
moduleManager.getRecorder().record(LoaderEventType.CachedDataRejected, cachedDataPath);
this._createAndWriteCachedData(script, scriptSource, cachedDataPath, moduleManager);
if (err) {
moduleManager.getConfig().onError(err)
}
});
} else if (createCachedData) {
// no cached data, but wanted
this._createAndWriteCachedData(script, scriptSource, cachedDataPath, moduleManager);
}
}
// 校验缓存数据:如果 hash 值改变,就删除缓存文件
private _verifyCachedData(script: INodeVMScript, scriptSource: string, cachedDataPath: string, hashData: Buffer | undefined, moduleManager: IModuleManager): void {
if (!hashData) {
// nothing to do
return;
}
if (script.cachedDataRejected) {
// invalid anyways
return;
}
setTimeout(() => {
// check source hash - the contract is that file paths change when file content
// change (e.g use the commit or version id as cache path). this check is
// for violations of this contract.
const hashDataNow = this._crypto.createHash('md5').update(scriptSource, 'utf8').digest();
if (!hashData.equals(hashDataNow)) {
moduleManager.getConfig().onError(<any>new Error(`FAILED TO VERIFY CACHED DATA, deleting stale '${cachedDataPath}' now, but a RESTART IS REQUIRED`));
this._fs.unlink(cachedDataPath!, err => {
if (err) {
moduleManager.getConfig().onError(err);
}
});
}
}, Math.ceil(5000 * (1 + Math.random())));
}
加载模块
初始化之后,load-3 进行模块加载,主要分为路径处理、获取模块代码并执行。
路径处理
class NodeScriptLoader implements IScriptLoader {
public load(moduleManager: IModuleManager, scriptSrc: string, callback: () => void, errorback: (err: any) => void): void {
...
// load-3-1: 路径处理
// 对于示例而言,test.js -> test.js
scriptSrc = Utilities.fileUriToFilePath(this._env.isWindows, scriptSrc);
const normalizedScriptSrc = this._path.normalize(scriptSrc);
const vmScriptPathOrUri = this._getElectronRendererScriptPathOrUri(normalizedScriptSrc);
// 配置是否使用缓存,示例为 true
const wantsCachedData = Boolean(opts.nodeCachedData);
// 如果使用缓存,获取缓存路径,示例为 cache-data/test-${hash}.code
const cachedDataPath = wantsCachedData ? this._getCachedDataPath(opts.nodeCachedData!, scriptSrc) : undefined;
...
}
}
获取模块代码并执行
class NodeScriptLoader implements IScriptLoader {
private static _BOM = 0xFEFF;
private static _PREFIX = '(function (require, define, __filename, __dirname) { ';
private static _SUFFIX = '\n});';
public load(moduleManager: IModuleManager, scriptSrc: string, callback: () => void, errorback: (err: any) => void): void {
...
// load-3-2: 获取模块代码和缓存,执行代码
// 第一步: 获取模块代码和缓存,其中获取缓存同 compile-2
this._readSourceAndCachedData(normalizedScriptSrc, cachedDataPath, recorder, (err: any, data: string, cachedData: Buffer, hashData: Buffer) => {
if (err) {
errorback(err);
return;
}
// 第二步: 处理模块代码,同 compile-1
// 如果有 bom 则去除,再封装文件内容,即 '(function (require, define, __filename, __dirname)' + data + '{ \n});';
let scriptSource: string;
if (data.charCodeAt(0) === NodeScriptLoader._BOM) {
scriptSource = NodeScriptLoader._PREFIX + data.substring(1) + NodeScriptLoader._SUFFIX;
} else {
scriptSource = NodeScriptLoader._PREFIX + data + NodeScriptLoader._SUFFIX;
}
// 如果配置了转换函数,则执行转换函数:const nodeInstrumenter = (opts.nodeInstrumenter || function (c) { return c; });
scriptSource = nodeInstrumenter(scriptSource, normalizedScriptSrc);
// 第三步: 生成并执行脚本,cacheData 对应从缓存路径读取的缓存数据,同 compile-3 ~ compile-6
const scriptOpts: INodeVMScriptOptions = { filename: vmScriptPathOrUri, cachedData };
const script = this._createAndEvalScript(moduleManager, scriptSource, scriptOpts, callback, errorback);
// step-4: 更新、验证缓存,同 compile-7
this._handleCachedData(script, scriptSource, cachedDataPath!, wantsCachedData && !cachedData, moduleManager);
this._verifyCachedData(script, scriptSource, cachedDataPath!, hashData, moduleManager);
});
// 第一步. 读取模块和缓存文件
private _readSourceAndCachedData(sourcePath: string, cachedDataPath: string | undefined, recorder: ILoaderEventRecorder, callback: (err?: any, source?: string, cachedData?: Buffer, hashData?: Buffer) => any): void {
if (!cachedDataPath) {
// 不使用缓存时,直接读取模块文件
this._fs.readFile(sourcePath, { encoding: 'utf8' }, callback);
} else {
// 使用缓存时,同时读取模块文件和缓存文件
let source: string | undefined = undefined;
let cachedData: Buffer | undefined = undefined;
let hashData: Buffer | undefined = undefined;
let steps = 2;
const step = (err?: any) => {
if (err) {
callback(err);
} else if (--steps === 0) {
// 两个文件都读取后,steps 变为 0,再执行 callback
callback(undefined, source, cachedData, hashData);
}
}
this._fs.readFile(sourcePath, { encoding: 'utf8' }, (err: any, data: string) => {
source = data;
step(err);
});
this._fs.readFile(cachedDataPath, (err: any, data: Buffer) => {
if (!err && data && data.length > 0) {
hashData = data.slice(0, 16);
cachedData = data.slice(16);
recorder.record(LoaderEventType.CachedDataFound, cachedDataPath);
} else {
recorder.record(LoaderEventType.CachedDataMissed, cachedDataPath);
}
step(); // ignored: cached data is optional
});
}
}
// 第三步. 生成并执行脚本
private _createAndEvalScript(moduleManager: IModuleManager, contents: string, options: INodeVMScriptOptions, callback: () => void, errorback: (err: any) => void): INodeVMScript {
const recorder = moduleManager.getRecorder();
recorder.record(LoaderEventType.NodeBeginEvaluatingScript, options.filename);
// 同 compile-3: 新建 vm.Script,编译代码
const script = new this._vm.Script(contents, options);
// 同 compile-4: 生成 ret,用于在当前上下文运行代码
const ret = script.runInThisContext(options);
// 获取 define 函数,对应 main.ts 中的 DefineFunc
const globalDefineFunc = moduleManager.getGlobalAMDDefineFunc();
let receivedDefineCall = false;
const localDefineFunc: IDefineFunc = <any>function () {
receivedDefineCall = true;
return globalDefineFunc.apply(null, arguments);
};
localDefineFunc.amd = globalDefineFunc.amd;
// 同 compile-6: 执行 ret
ret.call(global, moduleManager.getGlobalAMDRequireFunc(), localDefineFunc, options.filename, this._path.dirname(options.filename));
recorder.record(LoaderEventType.NodeEndEvaluatingScript, options.filename);
if (receivedDefineCall) {
callback();
} else {
errorback(new Error(`Didn't receive define call in ${options.filename}!`));
}
return script;
}
}
}
可以看到,load-3 加载模块,和 Module.prototype._compile
的处理逻辑基本一致,都是调用 vm.script
,runInThisContext
编译代码、执行代码。对缓存的处理也基本一致,都是读取缓存文件内容 cachedData
,在 new vm.script
时传入缓存;执行代码后,通过 _handleCachedData
、_verifyCachedData
更新、验证缓存。
define 定义模块
node 环境和浏览器环境,define 定义模块的逻辑是一致的,本文不再赘述。
总结
本文主要介绍了 vscode-loader 在 node 环境和浏览器环境的区别,即 scriptLoader
加载模块的方式不同:
- 浏览器环境,生成
<script>
标签,并设置 async,异步加载模块。 - node 环境,读取文件内容,再调用
vm
接口(vm.script
,runInThisContext
)编译代码、执行代码,且支持缓存数据。