vscode 源码解析 - 服务化

Wenzhao,divscodesource codeChinese


Introduction

上一篇文章 (opens in a new tab)介绍了 vscode 的依赖注入机制。

在 vscode 中,依赖注入主要用于将服务注入到消费者对象当中将一些基础能力提供给业务代码使用

我们来拆解一下这句话:

如果你对上面的某些概念不是很理解,你可以稍后去学习它们,这里的铺垫已经足够你理解本文的全部内容。

到这里我们了解了服务的含义,接下来就来看看 vscode 中用到了哪些服务吧!

我们之后才会讲到 Electron 的双进程架构。这里画了一个简图来方便你理解下面文章的内容,仅仅用于表达各个对象间的层次关系。

-------------------------------------    ------------------------------------------
                                                         *Workbench*
-------------------------------------    ------------------------------------------
      Window ----electron.BrowserWindow.load()----> *Desktop Main / BrowserMain*
-------------------------------------    ------------------------------------------
           *CodeApplication*
-------------------------------------    ------------------------------------------
              *CodeMain*
-------------------------------------    ------------------------------------------
             Main Process                              Renderer Process
-----------------------------------------------------------------------------------
                                    Electron
-----------------------------------------------------------------------------------

主线程中的服务

CodeMain

CodeMain 是 vscode 主线程中最底层的类,它在初始化时会创建以下这些服务 (opens in a new tab)

等。

CodeApplication 会创建以下服务 (opens in a new tab)

等。

渲染进程中的服务

DesktopMain

DesktopMain 是 vscode 渲染进程中最底层的类,它虽然自己并没有创建 InitailizationService,但是它创建了一个 service 集合 (opens in a new tab)并将这个集合传递给 Workbench,由 Workbench 创建了 InitializationService

在这一层次上提供的服务有:

等。

Workbench

Workbench 实际上就是我们能看到的 vscode 工作区的 UI。它会创建一个 InstantiationService,除了将从 DesktopMain 传递来的依赖注入项保存起来之外,它还要将全局单例注入项保存到 InstantiationService 当中,代码 (opens in a new tab)如下:

const contributedServices = getSingletonServiceDescriptors()
for (let [id, descriptor] of contributedServices) {
  serviceCollection.set(id, descriptor)
}
 
const instantiationService = new InstantiationService(serviceCollection, true)

我们在上一篇文章 (opens in a new tab)讲过 vscode 的全局单例注入。

那么究竟有哪些服务会被注入进来呢?这其实是在入口文件中确定的。

在桌面端的 vscode 中,入口文件为 workbench.js (opens in a new tab),从中可以看到引入了脚本 vs/workbench/workbench.desktop.main,而这个脚本在全局注册了很多服务(即 #region --- workbench services 里面的内容),另外通过引入 workbench.common.main.ts (opens in a new tab),还引入了很多服务(注意 #region --- workbench parts 里面的内容也是依赖注入项且和 UI 相关)。而在浏览器端的 vscode 中,入口文件则为 workbench.html (opens in a new tab),引入的主要脚本则是 vs/workbench/workbench.web.main

由于 Workbench 引入的全局单例服务实在是太多了,这里我们仅仅列举几个,感兴趣的话可以到入口文件中去查看:

等等。

为什么是依赖注入?

到这里,我们就对 vscode 中常用到的服务有哪些,它们是如何注入的,以及它们被注入的位置等问题有了一个大致上的认识。接下来的问题是,为什么 vscode 要使用依赖注入的方式来组织代码呢

对于 vscode 来说,使用依赖注入模式有以下这些好处:

一、繁杂的功能点借助依赖注入被合理划分到不同的服务中,在横向上降低了模块间的耦合度,提高了模块内聚性。如果想要修改某些功能,很容易就能知道去哪里查找相关代码;对某个模块的修改,不会影响其他模块的开发。

二、消费者和服务通过接口解耦,对于服务消费者来说,它只要求被注入的类符合它的接口要求就可以了,并用不关心注入项究竟是如何实现的,即在纵向上降低了耦合度(其实就是依赖反转 DIP),这使得 vscode 的架构十分灵活,能够通过提供不同的服务来做到一些神奇的事情。

如果你有关注 vscode 的动态,那么你肯定知道今年他们搞的一个大动作就是推出了在完全在浏览器环境中运行的 Visual Studio Code Online (opens in a new tab)(你可以通过在 vscode 项目中执行 yarn web 脚本启动它)。

vscode 基于 Electron,所以可以访问一些桌面端才有的 module,但是在浏览器环境下并没有这样的模块。以 FileService 为例,在 Electron 中 (opens in a new tab)它需要 fs module (opens in a new tab),因此它注册的是一个 diskFileSystemProvider

const diskFileSystemProvider = this._register(
  new DiskFileSystemProvider(logService)
)
fileService.registerProvider(Schemas.file, diskFileSystemProvider)

但是在浏览器中我们不能使用模块,所以,在 vscode online 中 (opens in a new tab)FileService 注册的是一个 remoteFileSystemProvider

const channel = connection.getChannel<IChannel>(REMOTE_FILE_SYSTEM_CHANNEL_NAME)
const remoteFileSystemProvider = this._register(
  new RemoteFileSystemProvider(channel, remoteAgentService.getEnvironment())
)
fileService.registerProvider(Schemas.vscodeRemote, remoteFileSystemProvider)

但对于 FileService 的消费者即 DesktopMain 来说,它并不需要(也不应该)知道这种差别,它只要按照自己的需要调用符合 IFileService 接口的服务就好了。

再以“拖动文件位置前弹出对话框”功能为例,它在 Electron 和浏览器中展现出不同的样式:

在 Electron 中

在浏览器中

但是业务层不需要了解各个平台上如何创建 dialog,只需要调用 IDialogService 提供的方法就可以了:

const confirmation = await this.dialogService.confirm({
  message:
    items.length > 1 && items.every((s) => s.isRoot)
      ? localize(
          'confirmRootsMove',
          'Are you sure you want to change the order of multiple root folders in your workspace?'
        )
      : items.length > 1
      ? getConfirmMessage(
          localize(
            'confirmMultiMove',
            "Are you sure you want to move the following {0} files into '{1}'?",
            items.length,
            target.name
          ),
          items.map((s) => s.resource)
        )
      : items[0].isRoot
      ? localize(
          'confirmRootMove',
          "Are you sure you want to change the order of root folder '{0}' in your workspace?",
          items[0].name
        )
      : localize(
          'confirmMove',
          "Are you sure you want to move '{0}' into '{1}'?",
          items[0].name,
          target.name
        ),
  checkbox: {
    label: localize('doNotAskAgain', 'Do not ask me again')
  },
  type: 'question',
  primaryButton: localize(
    { key: 'moveButtonLabel', comment: ['&& denotes a mnemonic'] },
    '&&Move'
  )
})

在打包不同平台上的 vscode 时,注入不同的 IDialogService:

import 'vs/workbench/services/dialogs/electron-browser/dialogService'
import 'vs/workbench/services/dialogs/browser/dialogService'

总的来说,想要让 vscode 在浏览器中运行,只需要修改被注入的服务,然后通过不同的打包入口(已在上文中介绍)引入这些服务,无须修改上层代码。

三、依赖注入模式也带来了软件工程方面的一些好处

Conclusion

这篇文章展示了 vscode 如何利用依赖注入系统提供各种基础功能来服务业务代码,对需要支持多平台的大型应用提供了一个优秀的模板。

, CC BY-NC 4.0 © Wenzhao.