前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >API注入机制及插件启动流程_VSCode插件开发笔记2

API注入机制及插件启动流程_VSCode插件开发笔记2

作者头像
ayqy贾杰
发布2019-06-12 14:35:28
1.1K0
发布2019-06-12 14:35:28
举报
文章被收录于专栏:黯羽轻扬

写在前面

插件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实例,大致过程如下:

  1. name参数通过Module._resolveFilename()方法映射到完整文件路径
  2. 如果cache[fullName]存在,就返回cache[fullName].exports(优先走缓存),一个模块只加载一次,从而提高模块加载速度。不想走缓存的话,可以在require(name)之前把cache[fullName]delete掉,例如delete require.cache[require.resolve('./my-module.js')]
  3. 否则,加载相应文件中的源码,并进行预处理(模块级变量注入),见Module.prototype.load
  4. 最后,编译(执行)转换过的源码,返回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')的大致过程为:

代码语言:javascript
复制
// 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()具体如下:

代码语言:javascript
复制
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:

代码语言:javascript
复制
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,很容易发现做过手脚的地方:

代码语言:javascript
复制
// 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.tscreateContents()方法与ExtensionService有关,主要内容如下:

代码语言:javascript
复制
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,关键部分如下:

代码语言:javascript
复制
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,关键部分如下:

代码语言:javascript
复制
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得到的子进程入口是:

代码语言:javascript
复制
// URI.parse(require.toUrl('bootstrap')).fsPath
// 经toUrl转换对应到
// out/bootstrap

src/bootstrap.js,关键部分如下:

代码语言:javascript
复制
require('./bootstrap-amd').bootstrap(process.env['AMD_ENTRYPOINT']);

先绕出再回来,是为了走loader执行入口文件:

代码语言:javascript
复制
var loader = require('./vs/loader');
exports.bootstrap = function (entrypoint) {
 loader([entrypoint], function () { }, function (err) { console.error(err); });
};

那么现在,踏进入口src/vs/workbench/node/extensionHostProcess.ts

代码语言:javascript
复制
// setup things
const extensionHostMain = new ExtensionHostMain(renderer.rpcProtocol, renderer.initData);
onTerminate = () => extensionHostMain.terminate();
return extensionHostMain.start();

又转到了ExtensionHostMain,对应源码文件为src/vs/workbench/node/extensionHostMain.ts

代码语言:javascript
复制
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入口文件的部分:

代码语言:javascript
复制
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

代码语言:javascript
复制
// 环境变量
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来完成的,具体如下:

代码语言:javascript
复制
// 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
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2018-03-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端向后 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 写在前面
  • 一.require
  • 二.extension API注入
  • 三.插件机制初始化流程
    • 创建ExtensionService
      • 启动extensionHost进程
        • 激活插件
        • 四.进程模型
          • 进程间通信方式
            • 参考资料
            相关产品与服务
            容器服务
            腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档