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

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.