写在前面
插件Helloworld有一种示例用法:

// The module 'vscode' contains the VS Code extensibility APIimport * 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?

看一个简单场景,假设有两个源码文件:

  // my-modue.jsmodule.exports = 'my-modue';// index.jsconst m = require('./my-module.js');

执行入口文件第一行require('./my-modue.js')的大致过程为:

// module.jsfunction 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是这样:

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];};

简单梳理下,其实整个过程的核心工作相当于:

// 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.tsfunction 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布局部分提到:

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开始着手界面的创建,以及界面与功能服务的对接。上次只关注了主启动流程相关的部分,这次看看插件机制的初始化流程

插件机制初始化相关文件递进关系:

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 thingsconst 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 extensionsprivate handleEagerExtensions(): TPromise<void> {  this._extensionService.activateByEvent('*', true).then(null, (err) => {    console.error(err);  });  return this.handleWorkspaceContainsEagerExtensions();}

到这里,无条件启动的插件也激活了,插件机制初始化完成

激活插件
具体的插件激活过程相当繁琐,因为支持Extension Pack型插件(允许插件依赖其它插件),所以激活插件还要处理插件依赖树,等依赖的所有插件成功激活之后,才激活当前插件

P.S.想要了解具体过程的话,可以看这两个文件:

src/vs/workbench/api/node/extHostExtensionService.tssrc/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

进程间通信方式

      <Electron IPC>Main ---------------- Renderer | | | <Child Process IPC> | |Extension Host

其中,Extension Host与Main之间的通信是通过fork()内置的IPC来完成的,具体如下:

// Support logging from extension hostthis._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

更多相关文章

  1. 模块_Haskell笔记2
  2. IDEA常用设置及推荐插件
  3. TypescriptServerPlugin_VSCode插件开发笔记3
  4. VSCode跳转到定义内部实现_VSCode插件开发笔记4
  5. MyBatis 如何编写一个自定义插件?运行原理是什么?
  6. 穿插一个 MyBatis 分页插件 PageHelper 使用的 Demo
  7. 每日学习-ansible yum模块
  8. 每日学习-ansible firewalld模块
  9. Spring【DAO模块】知识要点

随机推荐

  1. PHP中常用header头汇总
  2. PHP让人不知道的匿名函数的几种写法(附代
  3. 如何在php中实现construct构造方法
  4. PHP中三种设置脚本最大执行时间的方法
  5. 在PHP中通过GD库创建简单的图片(图文详解)
  6. 如何将curl获取到的json对象转成数组
  7. php如何修改数组的值?
  8. PHP在图片中用 imagettftext() 添加水印(
  9. 如何解决php中curl传递数据太慢
  10. php如何设置权限?