写在前面
插件Helloworld有一种示例用法:
代码语言:javascript复制// The module 'vscode' contains the VS Code extensibility API
import * as vscode from 'vscode';var disposable = vscode.commands.registerCommand('extension.sayHello', () => {
// Display a message box to the user
vscode.window.showInformationMessage('Hello World!');
});
在插件进程环境,可以引入vscode
模块访问插件可用的API,好奇一点的话,能够发现node_modules
下并没有vscode
模块,而且vscode
模块也名没被define()
过,看起来我们require
了一个不存在的模块,那么,这个东西是哪里来的?
P.S.关于define()
更多信息,请查看VS Code源码简析 | Renderer Process初始化
一.require
寻着蛛丝马迹,先看引入一个Node模块时发生了什么?
Node通过require(name)
函数来加载模块,传入模块名name
,返回Module
实例,大致过程如下:
name
参数通过Module._resolveFilename()
方法映射到完整文件路径- 如果
cache[fullName]
存在,就返回cache[fullName].exports
(优先走缓存),一个模块只加载一次,从而提高模块加载速度。不想走缓存的话,可以在require(name)
之前把cache[fullName]
先delete
掉,例如delete require.cache[require.resolve('./my-module.js')]
- 否则,加载相应文件中的源码,并进行预处理(模块级变量注入),见Module.prototype.load
- 最后,编译(执行)转换过的源码,返回
module.exports
的值,见Module.prototype._compile
P.S.关于模块缓存的更多信息,请查看node.js require() cache – possible to invalidate?
看一个简单场景,假设有两个源码文件:
代码语言:javascript复制 // my-modue.js
module.exports = 'my-modue';// index.js
const m = require('./my-module.js');
执行入口文件第一行require('./my-modue.js')
的大致过程为:
// module.js
function require(path) {
return mod.require(path);
}
Module.prototype.require = function(path) {
return Module._load(path, this, /* isMain */ false);
}
Module._load = function(request, parent, isMain) {
var filename = Module._resolveFilename(request, parent, isMain);
var module = new Module(filename, parent);
Module._cache[filename] = module;
tryModuleLoad(module, filename);
return module.exports;
}
其中tryModuleLoad()
具体如下:
function tryModuleLoad(module, filename) {
module.load(filename);
}
Module.prototype.load = function(filename) {
// 向上查找所有能访问到的node_modules目录
this.paths = Module._nodeModulePaths(path.dirname(filename));
// 按文件扩展名加载模块
Module._extensions[extension](this, filename);
}
Module._extensions['.js'] = function(module, filename) {
// 读源码
var content = fs.readFileSync(filename, 'utf8');
// 编译(执行)
module._compile(internalModule.stripBOM(content), filename);
}
Module.prototype._compile = function(content, filename) {
// 用IIFE包裹模块源码,注入模块级变量,见NativeModule.wrap()
var wrapper = Module.wrap(content);
// 相当于更安全的eval(),编译包好的function源码,得到可执行的Function实例
var compiledWrapper = vm.runInThisContext(wrapper, {
filename: filename,
lineOffset: 0,
displayErrors: true
});
var dirname = path.dirname(filename);
// 要注入的模块级require()方法
var require = internalModule.makeRequireFunction(this);
// 注入模块参数,执行
result = compiledWrapper.call(this.exports, this.exports, require, this, filename, dirname);
// 这个返回值是被丢弃的,没什么用,模块内容由this.exports带出来
return result;
}
包在模块源码外面的IIFE是这样:
代码语言:javascript复制NativeModule.wrap = function(script) {
// NativeModule.wrapper[0] = "(function (exports, require, module, __filename, __dirname) { "
// NativeModule.wrapper[1] = "n});"
return NativeModule.wrapper[0] script NativeModule.wrapper[1];
};
简单梳理下,其实整个过程的核心工作相当于:
代码语言:javascript复制// 1.读文件
const moduleScript = fs.readFileSync(fullFilename, 'utf8');
// 2.构造模块(隔离模块作用域,声明模块级变量)
const wrapped = `(function (exports, require, module, __filename, __dirname) {
${moduleScript}
});`;
// 2.5.编译得到可执行模块
const moduleFunction = eval(wrapped);
// 3.执行(注入模块级变量值)
let exportsHost = {};
moduleFunction.call(exportsHost, exportsHost);
const m = exportsHost;
那么,既然require
是个(模块级的)局部变量,不方便做手脚(劫持/篡改),那么一定是对Module
干了点什么,才能够支持加载不存在的虚拟模块的
P.S.别想通过劫持require('internal/module').makeRequireFunction
工厂方法来篡改require
,因为不允许访问internal module:
NativeModule.nonInternalExists = function(id) {
return NativeModule.exists(id) && !NativeModule.isInternal(id);
};
NativeModule.isInternal = function(id) {
return id.startsWith('internal/');
};
在Module._resolveFilename
时会被当做外人,从外部找,访问不到我们想要的那个实例
二.extension API注入
对require('vscode')
的过程进行debug,很容易发现做过手脚的地方:
// ref: src/vs/workbench/api/node/extHost.api.impl.ts
function defineAPI(factory: IExtensionApiFactory, extensionPaths: TernarySearchTree<IExtensionDescription>): void { // each extension is meant to get its own api implementation
const extApiImpl = new Map<string, typeof vscode>();
let defaultApiImpl: typeof vscode; const node_module = <any>require.__$__nodeRequire('module');
const original = node_module._load;
node_module._load = function load(request, parent, isMain) {
if (request !== 'vscode') {
return original.apply(this, arguments);
} // get extension id from filename and api for extension
const ext = extensionPaths.findSubstr(parent.filename);
if (ext) {
let apiImpl = extApiImpl.get(ext.id);
if (!apiImpl) {
apiImpl = factory(ext);
extApiImpl.set(ext.id, apiImpl);
}
return apiImpl;
} // fall back to a default implementation
if (!defaultApiImpl) {
defaultApiImpl = factory(nullExtensionDescription);
}
return defaultApiImpl;
};
}
Module._load()
方法被劫持了,遇到vscode
返回一个虚拟模块,叫做apiImpl
。注意,每个插件拿到的API都是独立的(可能是出于插件安全隔离考虑,避免劫持API影响其它插件)
P.S.注意,之所以要require.__$__nodeRequire('module')
,是因为global.require
已经被劫持过了(见VS Code源码简析 | Renderer Process初始化的loader部分)。。。VS Code团队的路数狂野得很哪
三.插件机制初始化流程
之前在VS Code启动流程的UI布局部分提到:
代码语言:javascript复制UI入口
src/vs/workbench/electron-browser/bootstrap/index.html
src/vs/workbench/electron-browser/bootstrap/index.js
src/vs/workbench/workbench.main js index文件
src/vs/workbench/electron-browser/main.ts
src/vs/workbench/electron-browser/shell.ts 界面与功能服务的接入点
src/vs/workbench/electron-browser/workbench.ts 创建界面
src/vs/workbench/browser/layout.ts 布局计算,绝对定位
从创建
WorkbenchShell
开始正式进入功能区UI布局,UI被称为Shell,算作用来承载功能的容器(“壳”)
即从src/vs/workbench/electron-browser/shell.ts
开始着手界面的创建,以及界面与功能服务的对接。上次只关注了主启动流程相关的部分,这次看看插件机制的初始化流程
插件机制初始化相关文件递进关系:
代码语言:javascript复制src/vs/workbench/electron-browser/shell.ts 界面与功能服务的接入点
src/vs/workbench/services/extensions/electron-browser/extensionService.ts
src/vs/workbench/services/extensions/electron-browser/extensionHost.ts
src/vs/workbench/node/extensionHostProcess.ts
src/vs/workbench/node/extensionHostMain.ts
创建ExtensionService
src/vs/workbench/electron-browser/shell.ts
的createContents()
方法与ExtensionService
有关,主要内容如下:
private createContents(parent: Builder): Builder {
// Instantiation service with services
const [instantiationService, serviceCollection] = this.initServiceCollection(parent.getHTMLElement());
}
private initServiceCollection(container: HTMLElement): [IInstantiationService, ServiceCollection] {
this.extensionService = instantiationService.createInstance(ExtensionService);
serviceCollection.set(IExtensionService, this.extensionService);
}
ExtensionService
来自src/vs/workbench/services/extensions/electron-browser/extensionService.ts
,关键部分如下:
lifecycleService.when(LifecyclePhase.Running).then(() => {
// delay extension host creation and extension scanning
// until after workbench is running
// 1.初始化extensionHost
this._startExtensionHostProcess([]);
// 2.扫描已安装的插件
this._scanAndHandleExtensions();
});private _startExtensionHostProcess(initialActivationEvents: string[]): void {
// 干掉已经存在的ExtensionHost进程
this._stopExtensionHostProcess();
// 创建并启动ExtensionHostProcessWorker
this._extensionHostProcessWorker = this._instantiationService.createInstance(ExtensionHostProcessWorker, this);
this._extensionHostProcessProxy = this._extensionHostProcessWorker.start().then(
//...
);
// 注册按场景触发激活的事件(如打开特定文件时才激活插件)
this._extensionHostProcessProxy.then(() => {
initialActivationEvents.forEach((activationEvent) => this.activateByEvent(activationEvent));
});
}
先通过ExtensionHostProcessWorker
启动extensionHost进程,同时扫描已安装的插件,等extensionHost进程创建完毕之后注册按需激活的插件(activationEvents
不为["*"]
的插件)
启动extensionHost进程
ExtensionHostProcessWorker
来自src/vs/workbench/services/extensions/electron-browser/extensionHost.ts
,关键部分如下:
public start(): TPromise<IMessagePassingProtocol> {
const opts = {
env: objects.mixin(objects.deepClone(process.env), {
AMD_ENTRYPOINT: 'vs/workbench/node/extensionHostProcess'
})
}; // Run Extension Host as fork of current process
this._extensionHostProcess = fork(URI.parse(require.toUrl('bootstrap')).fsPath, ['--type=extensionHost'], opts);
}
这个fork()
看似与AMD_ENTRYPOINT
没有联系,实际上,fork
得到的子进程入口是:
// URI.parse(require.toUrl('bootstrap')).fsPath
// 经toUrl转换对应到
// out/bootstrap
即src/bootstrap.js
,关键部分如下:
require('./bootstrap-amd').bootstrap(process.env['AMD_ENTRYPOINT']);
先绕出再回来,是为了走loader
执行入口文件:
var loader = require('./vs/loader');
exports.bootstrap = function (entrypoint) {
loader([entrypoint], function () { }, function (err) { console.error(err); });
};
那么现在,踏进入口src/vs/workbench/node/extensionHostProcess.ts
:
// setup things
const extensionHostMain = new ExtensionHostMain(renderer.rpcProtocol, renderer.initData);
onTerminate = () => extensionHostMain.terminate();
return extensionHostMain.start();
又转到了ExtensionHostMain
,对应源码文件为src/vs/workbench/node/extensionHostMain.ts
:
public start(): TPromise<void> {
return this._extensionService.onExtensionAPIReady()
// 启动最猴急的一批插件
.then(() => this.handleEagerExtensions())
.then(() => this.handleExtensionTests())
.then(() => {
this._logService.info(`eager extensions activated`);
});
}
// Handle "eager" activation extensions
private handleEagerExtensions(): TPromise<void> {
this._extensionService.activateByEvent('*', true).then(null, (err) => {
console.error(err);
});
return this.handleWorkspaceContainsEagerExtensions();
}
到这里,无条件启动的插件也激活了,插件机制初始化完成
激活插件
具体的插件激活过程相当繁琐,因为支持Extension Pack型插件(允许插件依赖其它插件),所以激活插件还要处理插件依赖树,等依赖的所有插件成功激活之后,才激活当前插件
P.S.想要了解具体过程的话,可以看这两个文件:
代码语言:javascript复制src/vs/workbench/api/node/extHostExtensionService.ts
src/vs/workbench/api/node/extHostExtensionActivator.ts
篇幅限制,我们跳过繁琐的依赖处理环节,直接看加载插件pkg.main
入口文件的部分:
private _doActivateExtension() {
// require加载插件入口文件
loadCommonJSModule(this._logService, extensionDescription.main, activationTimesBuilder),
this._loadExtensionContext(extensionDescription).then(values => {
// 执行其activate()方法
return ExtHostExtensionService._callActivate(this._logService, extensionDescription.id, <IExtensionModule>values[0], <IExtensionContext>values[1], activationTimesBuilder);
});
}// 加载入口文件
function loadCommonJSModule() {
r = require.__$__nodeRequire<T>(modulePath);
return TPromise.as(r);
}
// 执行约定的activate()方法
private static _callActivateOptional() {
if (typeof extensionModule.activate === 'function') {
const activateResult: TPromise<IExtensionAPI> = extensionModule.activate.apply(global, [context]);
}
}
直接node require
执行插件入口文件得到模块实例,然后apply
调用其activate
方法,插件跑起来了
四.进程模型
至此,我们了解到VS Code里至少有3个进程:
- Electron Main Process:App主进程
- Electron Renderer Process:UI进程
- Extension Host Process:插件宿主进程,给插件提供执行环境
其中Extension Host Process(每个VS Code窗体)只存在一个,所有插件都在该进程执行,而不是每个插件一个独立进程
注意,插件宿主进程是个普通的Node进程(childProcess.fork()
出来的),并不是Electron进程,而且被限制了不能使用electron:
// 环境变量
ELECTRON_RUN_AS_NODE: '1'
所以不能在插件运行环境使用require('electron').BrowserWindow.getAllWindows()
曲线改UI
P.S.关于插件定制UI能力的讨论,见access electron API from vscode extension
进程间通信方式
代码语言:javascript复制 <Electron IPC>
Main ---------------- Renderer
|
|
| <Child Process IPC>
|
|
Extension Host
其中,Extension Host与Main之间的通信是通过fork()
内置的IPC来完成的,具体如下:
// Support logging from extension host
this._extensionHostProcess.on('message', msg => {
if (msg && (<IRemoteConsoleLog>msg).type === '__$console') {
this._logExtensionHostMessage(<IRemoteConsoleLog>msg);
}
});
这里只是单向通信(插件 -> Main
),实际上可以通过this._extensionHostProcess.send({msg})
完成另一半(Main -> 插件
)
P.S.关于进程间通信的更多信息,请查看Nodejs进程间通信
参考资料
- Microsoft/vscode v1.19.3
- Hacking Node require