3

0x00 前言

在前几天的美国纽约,微软举行了Connect(); //2015大会。通过这次大会,我们可以很高兴的看到微软的确变得更加开放也更加务实了。当然,会上放出了不少新产品和新功能,其中就包括了VS Code的beta版本。而且微软更进一步,已经在github将VS Code的代码开源了。除了这些让人兴奋的消息,我们还应该注意到VS Code提供了对拓展的支持。
所以本文就来聊一聊使用TypeScript开发VS Code拓展的话题吧。
此处输入图片的描述
本文所使用的拓展的应用商店页面:

https://marketplace.visualstudio.com/items/JiadongChen.LicenseHeader

github页面:

https://github.com/chenjd/VSCode-StandardHeader

0x01 你好,世界

"万事开头,Hello World"。所以我们就从一个Hello World作为起点,开始一步一步构建自己的VScode的拓展。
在开发vscode的拓展之前,我们先要确保电脑上已经安装了Node.js。之后,我们便可以利用微软所提供的基于Yeoman的模板生成器来生成vscode拓展的模板了。

npm install -g yo generator-code

之后,我们只需要在终端中运行yo code命令,即可以进入创建拓展模板的引导过程。

yo code

在引导过程中将TypeScript选择为开发语言,并且填完一些基本信息之后,我们的第一个vscode拓展便创建完成了。
那么,yo code命令主要为我们做了一些什么事情呢?

  1. 会根据我们在引导中所填的信息,在当前目录生成一个拓展的文件夹,文件夹名即我们在引导中填入的拓展名称。

  2. 该文件夹中主要包括了盛放源文件(extension.ts)的src文件夹、test文件夹以及out文件夹。需要注意的是out文件夹,在编译typescript源文件之前out文件夹是没有创建的,而编译之后会创建out文件夹并且其中的内容是typescript经过编译之后生成的js文件。

  3. 生成了资源配置文件package.json,package.json文件描述了该拓展的信息和它的功能。下文我还会具体说明package.json文件。

  4. 创建了launch.json以及tasks.json和settings.json(位于项目中的.vscode目录下),其中launch.json文件规定了启动一个在拓展开发(Extension Development)模式的VS Code进程,并且规定在VS Code启动之前会先运行tasks.json文件中所定义的task(根据tasks.json中的定义,相当于npm run compile),即先使用TypeScript的编译器将ts文件编译为js文件。这样我们就可以直接编译(因为是typescript)运行并且调试我们的拓展项目了。

  5. 当然,还会根据我们在引导中的选择来决定是否创建一个Git仓库,用来做版本控制。

下面便是我们的拓展项目的完整目录:

.
├── .gitignore                    //配置不需要加入版本管理的文件
├── .vscode                     // VS Code的整合
│   ├── launch.json
│   ├── settings.json
│   └── tasks.json
├── .vscodeignore                //配置不需要加入最终发布到拓展中的文件
├── README.md
├── src                         // 源文件
│   └── extension.ts            // 如果我们使用js来开发拓展,则该文件的后缀为.js
├── test                        // test文件夹
│   ├── extension.test.ts       // 如果我们使用js来开发拓展,则该文件的后缀为.js
│   └── index.ts                // 如果我们使用js来开发拓展,则该文件的后缀为.js
├── node_modules
│   ├── vscode                  // vscode对typescript的语言支持。
│   └── typescript              // TypeScript的编译器
├── out                         // 编译之后的输出文件夹(只有TypeScript需要,JS无需)
│   ├── src
│   |   ├── extension.js
│   |   └── extension.js.map
│   └── test
│       ├── extension.test.js
│       ├── extension.test.js.map
│       ├── index.js
│       └── index.js.map
├── package.json                // 该拓展的资源配置文件
├── tsconfig.json               // 
├── typings                     // 类型定义文件夹
│   ├── node.d.ts               // 和Node.js关联的类型定义
│   └── vscode-typings.d.ts     // 和VS Code关联的类型定义
└── vsc-extension-quickstart.md 

此时,我们只需要点击在VS Code的Debug区域的Start按钮即可。
此处输入图片的描述

在接下来开启的一个处于拓展开发模式的新的VS Code中,我们只需要在命令面板中输入Hello World便可以激活这个模板拓展,弹出一个Hello World的消息弹窗。
此处输入图片的描述
那么,VS Code到底是如何加载并运行拓展的呢?我们又应该如何开发拓展供自己甚至是分享给更多的人更好的使用VS Code呢?

0x02 关于package.json的两三事

作为拓展项目的资源配置文件,package.json不仅仅描述了该拓展的信息还描述了拓展的功能。需要说明的是,当VS Code启动时,拓展的package.json文件便会被读取,并且VS Code还会对package.json文件中的contributes部分的内容做处理。这一点和拓展的源文件extension.ts有所区别。VS Code并不会在启动时便加载拓展的源代码。
下面我们就来看一看我们刚刚生成的这个拓展项目中的package.json文件的内容吧。

{
    "name": "Standard-Header",
    "displayName": "Standard-Header",
    "description": "standard-header",
    "version": "0.0.1",
    "publisher": "JiadongChen",
    "engines": {
        "vscode": "^0.10.1"
    },
    "categories": [
        "Other"
    ],
    "activationEvents": [
        "onCommand:extension.sayHello"
    ],
    "main": "./out/src/extension",
    "contributes": {
        "commands": [{
            "command": "extension.sayHello",
            "title": "Hello World"
        }]
    },
    "scripts": {
        "vscode:prepublish": "node ./node_modules/vscode/bin/compile",
        "compile": "node ./node_modules/vscode/bin/compile -watch -p ./"
    },
    "devDependencies": {
        "typescript": "^1.6.2",
        "vscode": "0.10.x"
    }
}

我们可以看到,package.json描述了该拓展的一些基本信息,例如拓展的名称name、拓展的描述信息description以及拓展的版本号version和发布者的信息publisher等等。顺带提一句,直接通过提高版本号的方式就可以使用拓展发布工具来更新已经发布到VS Code应用商店的拓展版本。
除了这些,我们甚至还可以在package.json文件中配置一些拓展在VS Code的应用商店中的展示信息,例如拓展在商店中的种类:categories、拓展的icon图:icon以及拓展在应用市场主页的一些展示信息:galleryBanner等等。

...
"icon": "res/icon.png",
"galleryBanner": {
    "color": "#5c2d91",
    "theme": "dark"
},
"categories": [
    "Other"
],
...

此处输入图片的描述

当然,更重要的内容是activationEvents、contributes以及scripts和main这几项内容。其中scripts项的内容是使用TypeScript进行开发时所特有的,主要的作用是用来提供对ts文件进行编译的相关信息。而main项则指向了将ts源文件编译后所生成的js文件。

ActivationEvents

正如上文所说,VS Code默认状况下并不会在启动时立刻执行拓展中的代码。因此,为了使拓展能够被激活,我们需要在package.json文件中定义activationEvents项的内容。例如上例中,activationEvents的定义如下:

...
"activationEvents": [
    "onCommand:extension.sayHello"
],
...

意思是当“onCommand:extension.sayHello”事件被触发之后,拓展会被激活。那么在VS Code中,激活事件都有哪些种类呢?下面我们就来归一下类。

根据文件所使用的语言:onLanguage:${language}

当打开的文件是使用onLanguage所规定的语言时,拓展会被激活。
例如:

...
"activationEvents": [
    "onLanguage:python"
]
...

根据所输入的命令:onCommand:${command}

当在onCommand中规定的命令被触发时,拓展会被激活。在上文生成的Hello World模板中,我们就可以看到。

...
"activationEvents": [
"onCommand:extension.sayHello"
]
...

根据文件夹:workspaceContains:${toplevelfilename}

"activationEvents": [
    "workspaceContains:package.json"
],

无限制:*

...
"activationEvents": [
    "*"
]
...

由于没有限制,因此在VS Code一启动时便会激活该拓展。因此,这种不加以条件限制就激活拓展的方式并不是十分推荐的。大家最好谨慎使用。

Contribution

在package.json文件中的contributes项中,同样包含很多种类。如果需要归类的话,可以分为以下几个类型。

configuration
commands
keybindings
languages
debuggers
grammars
themes
snippets

但是本文限于篇幅,将只关注commands和keybindings这两种。

commands

还以上文中的Hello World为例,我们在commands项中定义了命令的title以及title所对应的具体命令。如下所示:

"contributes": {
    "commands": [{
        "command": "extension.sayHello",
        "title": "Hello World"
    }]
},

此时一旦安装了该拓展,我们就可以在VS Code中的命令面板(我在Mac上的快捷键是Shift+cmd+p)中找到“Hello World”,输入便会触发extension.sayHello这条命令,而上文中的拓展激活事件项activationEvents中如果定义的内容是"onCommand:extension.sayHello",则激活事件被触发,拓展的代码被激活。
此处输入图片的描述

keybindings

当然,和commands对应的,我们还可以通过在package.json中定义keybindings项,通过绑定快捷键的方式触发命令。这样操作起来更加便捷,例如下文中我们自己开发一个插入标准页眉的拓展时,使用的便是绑定快捷键的方式。

"contributes": {
    "keybindings": [{
        "command": "extension.insertHeader",
        "key": "shift+cmd+1",
        "when": "editorTextFocus"
    }]
},

如果我们在VS Code中安装了定义了快捷键触发的拓展,则打开VS Code的KeyBoard ShortCut就可以看到拓展中定义的快捷键和它对应的命令了。
此处输入图片的描述

0x03 VS Code运行拓展的原理

通过上一小节的内容,我们就能很清楚的理解上文中那个模板拓展的运行过程了。

VS Code首先会检测到拓展并且读取拓展的package.json文件的内容并将package.json文件中的contributes项应用到VS Code中。这样,我们就可以根据contributes项中的内容,或者是在命令面板中输入contributes项中定义的commands,或者是使用contributes项中所定义的快捷键keybindings来触发extension.sayHello命令。
一旦extension.sayHello触发,VS Code会创建一个叫做onCommand:extension.sayHello的激活事件。
与此同时,所有在自己的package.json文件中将activationEvents设置为onCommand:extension.sayHello的拓展会被激活。并且会根据package.json文件中main项的内容,将./out/src/extension.js文件加载到JavaScript VM中。接下来,VS Code会调用在extension.js文件中的active函数,在active函数中,我们需要提供“extension.sayHello”这个函数的具体实现并执行。
好了,了解了VS Code拓展的基本知识之后我们就可以开始着手开发自己的拓展了,这个拓展的目的是向源文件添加License页眉。

0x04 一个插入License页眉的拓展

既然要打造自己的VS Code,那么自己喜欢的一些操作习惯自然希望能够用在VS Code上。而插入页眉便是这样一个简单的小功能。

我们首先来看一看在上文中那个Hello World模板拓展的源文件即extension.ts文件的基本内容。

import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {

    var disposable = vscode.commands.registerCommand('extension.sayHello', () => {
    ...
     vscode.window.showInformationMessage('Hello World!');
       ...
    });
}

首先,我们自然要引入vscode命名空间下的API,以获得控制VS Code的能力。

其次,我们注意到了拓展的源文件需要export一个叫做activate的函数,当拓展的package.json文件中activationEvents所定义的事件被触发时,VS Code会调用activate函数。

最后,我们可以看到我们使用vscode的API注册了一个叫做“extension.sayHello”的命令,并且提供了它的具体实现,按照模板中的逻辑,“extension.sayHello”命令的逻辑是在窗口中显示一个内容为“Hello World”的信息消息。具体的效果在本文一开始的部分各位已经看到了。

那么接下来就十分简单了,我们首先新建一个叫做HeaderGenerator的类,用来执行插入页眉的任务。

class HeaderGenerator {
    private _disposable: Disposable;
    public insertHeader() {

        // Get the current text editor 
        let editor = window.activeTextEditor; 
        if (!editor) { 
            return; 
        } 
        
        // Define header content
        var header = "License Header";
        
        // Get the document
        var doc = editor.document;
        
        // Insert header
        editor.edit((eb) => {
            eb.insert(doc.positionAt(0), header);
        });
    } 
    
     dispose() {
        this._disposable.dispose();
    }
}

插入页眉的具体执行是当前窗口的editor(TextEditor类)的edit方法,通过利用TextEditorEdit类的insert方法实现对当前的文档doc(TextDocument类)插入新的内容。

之后,我们只需在extension.ts文件中的activate函数中实现调用HeaderGenerator类的insertHeader方法即可。

import * as vscode from 'vscode'; 
import {window, commands, Disposable, ExtensionContext, TextDocument} from 'vscode';

export function activate(context: vscode.ExtensionContext) {
    console.log('Congratulations, your extension "standard-header" is now active!'); 
    let headerGen = new HeaderGenerator();
    var disposable = vscode.commands.registerCommand('extension.insertHeader', () => {
        headerGen.insertHeader();
    });
    context.subscriptions.push(headerGen);
    context.subscriptions.push(disposable);
}

当然,为了增加快捷键“shift+cmd+1”的支持,我们还需要在package.json中修改contributes的内容:

"contributes": {
    "keybindings": [{
        "command": "extension.insertHeader",
        "key": "shift+cmd+1",
        "when": "editorTextFocus"
    }]
},

之后,让我们先进入拓展开发模式看一下效果。使用快捷键shift+cmd+1,我们可以看到在VS Code的窗口中插入了页眉(截图中的页眉内容我已经换成了MIT协议)。
此处输入图片的描述
好了,到此我们的小小的在源文件中插入页眉的拓展就完成了,下面就让我们来使用安装、甚至是发布到VS Code的应用商店让大家一起分享我们的劳动成果吧。

0x05 本地安装或在应用商店发布

为了不必再每次都要进入拓展开发模式才能“使用”我们的拓展,我们就必须让自己的VS Code安装这个拓展。

事实上在本地安装供自己的拓展是十分简单并且方便的事情。VS Code会在.vscode/extensions文件夹中获取本地的拓展。而.vscode/extensions文件夹所在的位置我们可以总结如下:

Windows %USERPROFILE%\.vscode\extensions
Mac $HOME/.vscode/extensions
Linux $HOME/.vscode/extensions

我们只需拷贝一份我们的拓展到.vscode/extensions文件夹中,VS Code在启动时便能够检查到该拓展了。

当然,除了自己使用自己开发的拓展之外,我们还可以将拓展发布到VS Code的应用商店。此时我们就要借助一个拓展发布工具——vsce了。

首先是安装vsce:

npm install -g vsce

之后我们还需要到Visual Studio Team Services注册并登陆,以获取Personal Access Token。

一旦获取了Personal Access Token,我们就可以使用vsce创建一个发布者账号了。

vsce create-publisher 

紧接着,登陆并发布:

vsce login 
vsce publish

发布成功之后,我们就可以在VS Code的应用商店中看到自己的拓展了。
此处输入图片的描述

并且在VS Code的命令面板中,我们也可以使用命令安装:

ext install 

此处输入图片的描述
好啦,行文至此,使用TypeScript拓展你自己的VS Code讲的已经差不多了。希望各位多多交流~


chenjd
556 声望61 粉丝

Microsoft Visual Studio and Development Technologies MVP