vscode-loader 解析(node 环境)

快应用开发工具 Dec 21, 2021

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-3compile-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.scriptrunInThisContext 编译代码、执行代码。对缓存的处理也基本一致,都是读取缓存文件内容 cachedData,在 new vm.script 时传入缓存;执行代码后,通过 _handleCachedData_verifyCachedData 更新、验证缓存。

define 定义模块

node 环境和浏览器环境,define 定义模块的逻辑是一致的,本文不再赘述。

总结

本文主要介绍了 vscode-loader 在 node 环境和浏览器环境的区别,即 scriptLoader 加载模块的方式不同:

  • 浏览器环境,生成 <script> 标签,并设置 async,异步加载模块。
  • node 环境,读取文件内容,再调用 vm 接口(vm.scriptrunInThisContext)编译代码、执行代码,且支持缓存数据。

猜您可能感兴趣的文章

Tags