关于VSCODE的插件 一

官方API文档

1. 要学好TypeScript。

官方教程

1.1TypeScript是一门弱类型语言。

强类型和弱类型主要是站在变量类型处理的角度进行分类的。这些概念未经过严格定义,它们并不是属于语言本身固有的属性,而是编译器或解释器的行为。主要用以描述编程语言对于混入不同数据类型的值进行运算时的处理方式。

强类型语言是指无论在编译阶段还是运行阶段,一旦变量绑定某种类型后,此变量会持有该类型,并且不能与其他类型表达式进行混合运算。弱类型则相反。

1.2 类的关键字

声明类 :class
继承:extends
修饰符:
public private protected

class Person {
    protected name: string;
    constructor(name: string) { this.name = name; }
}
class Employee extends Person {
    private department: string;
    constructor(name: string, department: string) {
        super(name)
        this.department = department;
    }
    public getElevatorPitch() {
        return `Hello, my name is ${this.name} and I work in ${this.department}.`;
    }
}
let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name); // 错误

readonly(只读属性必须在声明时或构造函数里被初始化。)

class Octopus {
    readonly name: string;
    readonly numberOfLegs: number = 8;
    constructor (theName: string) {
        this.name = theName;
    }
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; // 错误! name 是只读的.

get/set
TypeScript支持通过getters/setters来截取对对象成员的访问。 它能帮助你有效的控制对对象成员的访问。

let passcode = "secret passcode";
class Employee {
    private _fullName: string;
    get fullName(): string {
        return this._fullName;
    }
    set fullName(newName: string) {
        if (passcode && passcode == "secret passcode") {
            this._fullName = newName;
        }
        else {
            console.log("Error: Unauthorized update of employee!");
        }
    }
}
let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
    alert(employee.fullName);
}

static
可以创建类的静态成员,这些属性存在于类本身上面而不是类的实例上。

class Router {
    static baseRoute = '/basePath';
    calculateRoute(path: string) {
        return this.baseRoute + this.commonPrefix  + path;
    }
    constructor (public commonPrefix: number) { }
}
let route1 = new Router('/api');  // 一级路径为/api
let route2 = new Router('/page');  // 一级路径为/page
console.log(route1.calculateRoute('/main');  // 最终路径/basePath/api/main
console.log(route2.calculateRoute('/user');  // 最终路径/basePath/page/user

abstract
抽象类
constructor
类构造器。
重载构造器例子

class DateHour {
 
  private date: Date;
  private relativeHour: number;
 
  constructor(year: number, month: number, day: number, relativeHour: number);
  constructor(date: Date, relativeHour: number);
  constructor(dateOrYear: any, monthOrRelativeHour: number, day?: number, relativeHour?: number) {
    if (typeof dateOrYear === "number") {
      this.date = new Date(dateOrYear, monthOrRelativeHour, day);
      this.relativeHour = relativeHour;
    } else {
      var date = <Date> dateOrYear;
      this.date = new Date(date.getFullYear(), date.getMonth(), date.getDate());
      this.relativeHour = monthOrRelativeHour;
    }
  }
}

1.3 接口

TypeScript的核心原则之一是对数据的结构进行类型检查。 它有时被称做“鸭式辨型法”或“结构性子类型化”。 在TypeScript里,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。

1.3.1 基础

例子代码

interface LabelledValue {
  label: string;
}
function printLabel(labelledObj: LabelledValue) {
  console.log(labelledObj.label);
}
let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);

LabelledValue接口就好比一个名字,用来描述上面例子里的要求。 它代表了有一个 label属性且类型为string的对象。 需要注意的是,我们在这里并不能像在其它语言里一样,说传给 printLabel的对象实现了这个接口。我们只会去关注值的外形。 只要传入的对象满足上面提到的必要条件,那么它就是被允许的。

还有一点值得提的是,类型检查器不会去检查属性的顺序,只要相应的属性存在并且类型也是对的就可以。

1.3.2 可选属性

接口里的属性不全都是必需的。 有些是只在某些条件下存在,或者根本不存在。 可选属性在应用“option bags”模式时很常用,即给函数传入的参数对象中只有部分属性赋值了。
带有可选属性的接口与普通的接口定义差不多,只是在可选属性名字定义的后面加一个?符号。
可选属性的好处之一是可以对可能存在的属性进行预定义,好处之二是可以捕获引用了不存在的属性时的错误。 比如,我们故意将 createSquare里的color属性名拼错,就会得到一个错误提示:

interface SquareConfig {
  color?: string;
  width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
  let newSquare = {color: "white", area: 100};
  if (config.clor) {
    // Error: Property 'clor' does not exist on type 'SquareConfig'
    newSquare.color = config.clor;
  }
  if (config.width) {
    newSquare.area = config.width * config.width;
  }
  return newSquare;
}
let mySquare = createSquare({color: "black"});

1.3.2 只读属性

一些对象属性只能在对象刚刚创建的时候修改其值。 你可以在属性名前用 readonly来指定只读属性:

interface Point {
    readonly x: number;
    readonly y: number;
}
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!

readonly vs const
最简单判断该用readonly还是const的方法是看要把它做为变量使用还是做为一个属性。 做为变量使用的话用 const,若做为属性则使用readonly。

1.3.3 额外的属性检查

我们在第一个例子里使用了接口,TypeScript让我们传入{ size: number; label: string; }到仅期望得到{ label: string; }的函数里。 我们已经学过了可选属性,并且知道他们在“option bags”模式里很有用。

然而,天真地将这两者结合的话就会像在JavaScript里那样搬起石头砸自己的脚。 比如,拿createSquare例子来说:

interface SquareConfig {
    color?: string;
    width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
    // ...
}
let mySquare = createSquare({ colour: "red", width: 100 });

注意传入createSquare的参数拼写为colour而不是color。 在JavaScript里,这会导致静默失败。
你可能会争辩这个程序已经正确地类型化了,因为width属性是兼容的,不存在color属性,而且额外的colour属性是无意义的。
然而,TypeScript会认为这段代码可能存在bug。 对象字面量会被特殊对待而且会经过 额外属性检查,当将它们赋值给变量或作为参数传递的时候。 如果一个对象字面量存在任何“目标类型”不包含的属性时,你会得到一个错误。

// error: 'colour' not expected in type 'SquareConfig'
let mySquare = createSquare({ colour: "red", width: 100 });

绕开这些检查非常简单。 最简便的方法是使用类型断言:

let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);

然而,最佳的方式是能够添加一个字符串索引签名,前提是你能够确定这个对象可能具有某些做为特殊用途使用的额外属性。 如果 SquareConfig带有上面定义的类型的color和width属性,并且还会带有任意数量的其它属性,那么我们可以这样定义它:

interface SquareConfig {
    color?: string;
    width?: number;
    [propName: string]: any;
}

我们稍后会讲到索引签名,但在这我们要表示的是SquareConfig可以有任意数量的属性,并且只要它们不是color和width,那么就无所谓它们的类型是什么。

还有最后一种跳过这些检查的方式,这可能会让你感到惊讶,它就是将这个对象赋值给一个另一个变量: 因为 squareOptions不会经过额外属性检查,所以编译器不会报错。

let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);

要留意,在像上面一样的简单代码里,你可能不应该去绕开这些检查。 对于包含方法和内部状态的复杂对象字面量来讲,你可能需要使用这些技巧,但是大部额外属性检查错误是真正的bug。 就是说你遇到了额外类型检查出的错误,比如“option bags”,你应该去审查一下你的类型声明。 在这里,如果支持传入 color或colour属性到createSquare,你应该修改SquareConfig定义来体现出这一点。

1.3.4 函数类型

1.3.5 可索引的类型

1.3.6 类类型

interface ClockInterface {
    currentTime: Date;
    setTime(d: Date);
}
class Clock implements ClockInterface {
    currentTime: Date;
    setTime(d: Date) {
        this.currentTime = d;
    }
    constructor(h: number, m: number) { }
}

1.3.7 类的静态部分和实例部分

1.3.8 接口继承

一个接口可以继承多个接口,创建出多个接口的合成接口。

interface Shape {
    color: string;
}
interface PenStroke {
    penWidth: number;
}
interface Square extends Shape, PenStroke {
    sideLength: number;
}
let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;

1.3.9 混合类型

先前我们提过,接口能够描述JavaScript里丰富的类型。 因为JavaScript其动态灵活的特点,有时你会希望一个对象可以同时具有上面提到的多种类型。

一个例子就是,一个对象可以同时做为函数和对象使用,并带有额外的属性。

interface Counter {
    (start: number): string;
    interval: number;
    reset(): void;
}
function getCounter(): Counter {
    let counter = <Counter>function (start: number) { };
    counter.interval = 123;
    counter.reset = function () { };
    return counter;
}
let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;

1.4 命名空间

1.5 泛型

1.6 声明文件

2. 开发vscode插件

参照知乎文章

2.1 vscode 提供了哪些开放能力

在这里插入图片描述

2.2如何编写一个 vscode 插件?

vscode 插件的形态和一个 npm 包非常相似,需要在项目的根目录添加 package.json,并且在其中增加一些 vscode 独家的设置。其中最主要的设置是 Activation Events(插件的激活时机) 和 contribution points (插件的能力)。我将 vscode 的生命周期简单描述为下图
在这里插入图片描述
activate() 函数 & deactivate() 函数
可以看到生命周期中最终要的两个节点就是activate函数和deactivate函数。这两个函数需要在插件 npm 模块的入口文件 export 出去给 vscode 主动调用。
其中,activate 会在 vscode 认为合适的时机调用,并且在插件的运行周期内只调用一次。因此在 activate 函数中开始启动插件的逻辑,是一个非常合适的时机。
deactivate 函数会在插件卸载之前调用,如果你的卸载逻辑中存在异步操作,那么只需要在deactivate 函数中 retuen 一个 promise 对象,vscode 会在 promise resolve 时才正式将插件卸载掉。
onXxxx Activation Events
可以看到在activate函数之前,还有onLanguage等事件的描述,实际上这些就是声明在插件 package.json 文件中的 Activation Events。声明这些 Activation Events 后,vscode 就会在适当的时机回调插件中的 activate函数。vscode之所以这么设计,是为了节省资源开销,只在必要的时候才激活你的插件。当然,如果你的插件非常重要,不希望在某个事件之后才被激活,你可以声明Activation Events为*这样 vscode 就会在启动的时候就开始回调 activate函数
插件的具体逻辑
插件中的具体逻辑 vscode 没有做任何限制,你可以通过调用vscdoe提供的各种 api 对其进行扩充。不过需要注意的是,出于性能和移植性考虑,vscode不允许开发者直接操作dom。
关于 vscode 的 api 可以参考 https://code.visualstudio.com/api/references/vscode-api 这是微软根据 vscode 的 d.ts 文件生成的文档

3. VSCODE的第一个插件

参考链接

3.1 安装使用脚手架

安装

npm install -g yo generator-code

使用

yo code
# ? What type of extension do you want to create? New Extension (TypeScript)
# ? What's the name of your extension? HelloWorld
### Press <Enter> to choose default for all options below ###
# ? What's the identifier of your extension? helloworld
# ? What's the description of your extension? LEAVE BLANK
# ? Enable stricter TypeScript checking in 'tsconfig.json'? Yes
# ? Setup linting using 'tslint'? Yes
# ? Initialize a git repository? Yes
# ? Which package manager to use? npm
code ./helloworld

文件结构
.
├── .gitignore
├── .vscode // VS Code 集成配置
│ ├── launch.json
│ ├── settings.json
│ └── tasks.json
├── .vscodeignore
├── README.md
├── src // 源码
│ └── extension.ts // 如果是JavaScript插件,那么此处就是extension.js
├── test // 测试文件夹
│ ├── extension.test.ts // 如果是JavaScript插件,那么此处就是extension.test.js
│ └── index.ts // 如果是JavaScript插件,那么此处就是index.js
├── node_modules
│ ├── vscode // 语言服务
│ └── typescript // typescript编译器(仅TypeScript插件才有)
├── out // 编译结果(仅TypeScript插件才有)
│ ├── src
│ | ├── extension.js
│ | └── extension.js.map
│ └── test
│ ├── extension.test.js
│ ├── extension.test.js.map
│ ├── index.js
│ └── index.js.map
├── package.json // 插件的清单
├── tsconfig.json // 如果是JavaScript插件,那么此处就是jsconfig.json
├── typings // 类型定义文件
│ ├── node.d.ts // 链接到Node.js的API
│ └── vscode-typings.d.ts // 链接到VS Code的API
└── vsc-extension-quickstart.md // 插件开发快速入门文档

开始项目的第一步是 打开 package.json。

里面的 contributes 下所有节点信息都可以通过官网 contribution-points

contributes: 下的节点很重要请小伙伴们能够仔细去看下上面的官网:

因为量太多,这里我们只介绍重点节点:

"contributes": {
    "commands": [{
        "command": "extension.helloWorld",      // 自定义的命令
        "title": "Hello World",                 // 我们在命令选板上能够搜索出此命令的标题
        "category": "Hello"                     // 类别:对命令进行简单的分组,使查询时更加清晰
    }],
    "keybindings": [                            // 我新增的内容:提供一个键绑定规则
        {
            "command": "extension.helloWorld",  // 上面 commands 定义过的命令
            "key": "ctrl+f1",                   // window 下配置的按键
            "mac": "cmd+f1"                     // mac 下配置的按键
        }
    ]
},
"activationEvents": [                           // 这应该是写在 contributes 上面的,用来激活命令
    "onCommand:extension.helloWorld"            // onCommand  为只要调用命令,就会激活插件
]

好的目前在这最简单的 demo 中 package.json 下的东西我们已经说完了,后面的内容还会继续补充其他的内容,接下来我们先来看看 src/extension.ts 文件。

// 模块'vs code'包含vs代码扩展性API
// 导入模块并在下面的代码中用别名vscode引用它
import * as vscode from 'vscode';

// activate: 激活插件时调用此方法,你的插件在第一次执行命令时被激活
export function activate(context: vscode.ExtensionContext) {
    console.log(`我们可以在终端下的'调试控制台' 看到我们输出的消息`);

    // 该命令已在package.json文件中定义
	// 现在为命令的实现提供 registercommand(我理解就是当你执行该命令要做什么事)
	// commandid参数必须与package.json中的命令字段匹配。(一定要一致,否则插件读取不到)
    let disposable = vscode.commands.registerCommand('extension.helloWorld', () => {
        
        // 弹出右下角的消息框,就是我们运行插件时看到的右下角的 `Hello World!` 的提示
		vscode.window.showInformationMessage('Hello World!');
	});

    // 发布注册的命令,发布才能使用
	context.subscriptions.push(disposable); 
}

// 当插件停用是调用此方法,一般可以放空
export function deactivate() {}

4. VSCODE插件的典型应用

写在前面
所有的例程,打开文件夹后如出现下面问题
在这里插入图片描述
可以点击下拉菜单
在这里插入图片描述
输入

npm install

如图
在这里插入图片描述
如果出现出现错误,就删除node_cache内容。

npm run watch

按F5 运行程序

4.1 通用能力

4.1.1 命令

命令触发 Visual Studio Code 中的操作。如果您曾经配置过键绑定,那么您已经使用过命令。扩展还使用命令向用户公开功能、绑定到 VS Code UI 中的操作以及实现内部逻辑。

4.1.1.1 使用命令

VS Code 包括一组大型内置命令,可用于与编辑器交互、控制用户界面或执行后台操作。许多扩展还将其核心功能公开为用户和其他扩展可以利用的命令
以编程方式执行命令
vscode.commands.executeCommand API 以编程方式执行命令。这使你可以使用 VS Code 的内置功能,并在 VS Code 的内置 Git 和 Markdown 扩展等扩展上进行构建。
例如,该命令在活动文本编辑器中注释当前选定的行:editor.action.addCommentLine

import * as vscode from 'vscode';

function commentLine() {
  vscode.commands.executeCommand('editor.action.addCommentLine');
}

某些命令采用控制其行为的参数。命令也可能返回结果。例如,类似 API 的命令在给定位置查询文档的定义。它将文档 URI 和位置作为参数,并返回一个包含定义列表的承诺:vscode.executeDefinitionProvider

import * as vscode from 'vscode';

async function printDefinitionsForActiveEditor() {
  const activeEditor = vscode.window.activeTextEditor;
  if (!activeEditor) {
    return;
  }

  const definitions = await vscode.commands.executeCommand<vscode.Location[]>(
    'vscode.executeDefinitionProvider',
    activeEditor.document.uri,
    activeEditor.selection.active
  );

  for (const definition of definitions) {
    console.log(definition);
  }
}

要查找可用命令:
浏览快捷键
浏览内建命令api

4.1.1.2 创建新命令

注册命令
一般在src/extension.ts 文件中,vscode.commands.registerCommand 将命令 ID 绑定到扩展中的处理程序函数:

import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
  const command = 'myExtension.sayHello';

  const commandHandler = (name: string = 'world') => {
    console.log(`Hello ${name}!!!`);
  };

  context.subscriptions.push(vscode.commands.registerCommand(command, commandHandler));
}

每次执行命令时,都会调用处理程序函数,无论是使用 VS Code UI 中以 编程方式调用,还是通过键绑定。myExtension.sayHelloexecuteCommand

创建面向用户的命令
vscode.commands.registerCommand仅将命令 ID 绑定到处理程序函数。要在命令面板中公开此命令以便用户可以发现它,您还需要在扩展的:contributionpackage.json

{
  "contributes": {
    "commands": [
      {
        "command": "myExtension.sayHello",
        "title": "Say Hello"
      }
    ]
  }
}

贡献告诉 VS Code,你的扩展提供了一个给定的命令,还允许你控制命令在 UI 中的显示方式。现在,我们的命令将显示在命令面板中:commands
命令必须有"title"。
激活命令

{
  "activationEvents": ["onCommand:myExtension.sayHello"]
}

现在,当用户首次从命令面板或通过键绑定调用命令时,扩展将被激活并将绑定到正确的处理程序。myExtension.sayHelloregisterCommandmyExtension.sayHello

内部命令不需要激活事件,但必须为以下任何命令定义激活事件:onCommand

可以使用命令面板调用。
可以使用键绑定调用。
可以通过 VS Code UI 调用,例如通过编辑器标题栏调用。
旨在用作其他扩展要使用的 API
控制命令何时显示在命令面板中
默认情况下,所有面向用户的命令都通过“命令面板”中显示的部分提供。但是,许多命令仅在某些情况下相关,例如,当存在给定语言的活动文本编辑器时,或者当用户设置了某个配置选项时。
menus.commandPalette 贡献点允许您限制命令在命令面板中显示的时间。它采用目标命令的 ID 和一个 when 子句,该子句控制何时显示该命令:

{
  "contributes": {
    "menus": {
      "commandPalette": [
        {
          "command": "myExtension.sayHello",
          "when": "editorLangId == markdown"
        }
      ]
    }
  }
}

启用命令
使用自定义 when 子句上下文
如果要创作自己的 VS Code 扩展,并且需要使用子句上下文启用/禁用命令、菜单或视图,并且没有一个现有键适合你的需要,则可以添加自己的上下文。when

下面的第一个示例将键设置为 true,您可以在启用命令或属性时使用它。第二个示例存储一个值,您可以将其与子句一起使用,以检查冷打开的东西的数量是否大于 2。myExtension.showMyCommand when when

vscode.commands.executeCommand('setContext', 'myExtension.showMyCommand', true);

vscode.commands.executeCommand('setContext', 'myExtension.numberOfCoolOpenThings', 4);

4.1.2 视图容器和视图

4.1.3 树

VS Code TreeDataProvider简单示例
VS Code插件开发教程–树视图+网页视图–完整demo+图–2
做树视图,要实现两个API类的继承
TreeItem
在这里插入图片描述
创建 TreeItem 类型的构造函数,传递参数 label、collapsibleState.

label: tree 该项的标题。

collapsibleState: 该项是否折叠状态。参数有三: Collapsed(折叠状态)、Expanded(展开状态)、None(无状态)。

?: 代表参数为非必填项,可不传。注意:非必填项,必须放在最后面

TreeItemCollapsibleState: 就是 collapsibleState 的类型。

TreeDataProvider: 为树数据的数据提供程序(如果不理解,可以先放着,一会看代码)。

Uri: 通用资源标识符,表示磁盘上的文件或其他资源(用来读取文件路径)。

join: 用来拼接文件路径。

按照树的节点特性,一般分成两种,
一种是不包含子节点的,如官网例子

class Dependency extends vscode.TreeItem {
  constructor(
    public readonly label: string,
    private version: string,
    public readonly collapsibleState: vscode.TreeItemCollapsibleState
  ) {
    super(label, collapsibleState);
    this.tooltip = `${this.label}-${this.version}`;
    this.description = this.version;
  }

  iconPath = {
    light: path.join(__filename, '..', '..', 'resources', 'light', 'dependency.svg'),
    dark: path.join(__filename, '..', '..', 'resources', 'dark', 'dependency.svg')
  };
}

一种是包含子节点的,如参考文献的

class DataItem extends TreeItem{
    public children: DataItem[] | undefined;
    constructor(label: string, children?: DataItem[] | undefined) {
        super(label, children === undefined ? TreeItemCollapsibleState.None : TreeItemCollapsibleState.Collapsed);
        this.children = children;
    }
}

TreeDataProvider
在这里插入图片描述
接下来我们就会用到 TreeDataProvider: 为树数据的数据提供程序
在这里插入图片描述
当我们编写完上面的代码时,我们命名的 class 会报错,但是没关系鼠标覆盖报错的地方,选中 快速修复,就会弹出如上的标签。确认即可。
对应参考文献的程序如下。

export class DataProvider implements TreeDataProvider<DataItem> {
    onDidChangeTreeData?: Event<DataItem | null | undefined> | undefined;
    data: DataItem[];

    constructor() {
        this.data = [
            new DataItem('line1', [new DataItem('line1-sub1'), new DataItem('line1-sub2')]),
            new DataItem('line2', [new DataItem('line2-sub1'), new DataItem('line2-sub2')]),
            new DataItem('line3', [new DataItem('line3-sub1'), new DataItem('line3-sub2')])
        ];
    }

    getTreeItem(element: DataItem): TreeItem | Thenable<TreeItem> {
        return element;
    }

    getChildren(element?: DataItem | undefined): ProviderResult<DataItem[]> {
        if (element === undefined) {
            return this.data;
        }
        return element.children;
    }
}

可以数据写成JSON文件

 "data":
    [
        {
        "collapsibleState":1,
        "label":"line1",
        "children":[
            {
            "collapsibleState":0,
            "label":"line1-sub1",
            },
            {
            "collapsibleState":0,
            "label":"line1-sub2",
            }]},
         {
        "collapsibleState":1,
        "label":"line2",
        "children":[
            {
            "collapsibleState":0,
            "label":"line2-sub1",
            },
            {
            "collapsibleState":0,
            "label":"line2-sub2",
            }]},
            {
        "collapsibleState":1,
        "label":"line3",
        "children":[
            {
            "collapsibleState":0,
            "label":"line3-sub1",
            },
            {
            "collapsibleState":0,
            "label":"line3-sub2",
            }]},
            
    ]

直接读取就行

const packageObj = await fs.readJson(path.resolve(dirPath,'DEMO.json'));
data = packageObj.data;

4.1.4 文件操作

node-"fs-extra"模块代替fs使用
官方库
fs-extra是fs的一个扩展,提供了非常多的便利API,并且继承了fs所有方法和为fs方法添加了promise的支持。

它应该是 fs 的替代品。
安装

npm install fs-extra -S

用法

const fse = require('fs-extra');

Sync vs Async vs Async/Await
大多数方法默认为异步,如果未传递回调,则所有异步方法将返回一个promise。

const fs = require('fs-extra')
 
// 异步方法,返回promise
fs.copy('/tmp/myfile', '/tmp/mynewfile')
  .then(() => console.log('success!'))
  .catch(err => console.error(err))
 
// 异步方法,回调函数
fs.copy('/tmp/myfile', '/tmp/mynewfile', err => {
  if (err) return console.error(err)
  console.log('success!')
})
 
// 同步方法,注意必须使用try catch包裹着才能捕获错误
try {
  fs.copySync('/tmp/myfile', '/tmp/mynewfile')
  console.log('success!')
} catch (err) {
  console.error(err)
}
 
// Async/Await:
async function copyFiles () {
  try {
    await fs.copy('/tmp/myfile', '/tmp/mynewfile')
    console.log('success!')
  } catch (err) {
    console.error(err)
  }
}
 
copyFiles()

API Methods
下面的所有方法都是fs-extra扩展方法

Async/异步

copy
emptyDir (别名:emptydir)
ensureFile
ensureDir (别名:mkdirp、mkdirs)
ensureLink
ensureSymlink
mkdirp
mkdirs
move
outputFile
outputJson (别名:outputJSON)
pathExists
readJson (别名:readJSON)
remove
writeJson (别名:writeJSON)
Sync/同步

copySync
emptyDirSync
ensureFileSync
ensureDirSync
ensureLinkSync
ensureSymlinkSync
mkdirpSync
mkdirsSync
moveSync
outputFileSync
outputJsonSync
pathExistsSync
readJsonSync
removeSync
writeJsonSync
请参照官方网站
例子

//相对路径
const dirPath = path.resolve(__dirname, '../src');
console.log(dirPath);
// With async/await:
//读取API文档,该文档是JSON文件
export async function readJ () {
  try {
    const packageObj = await fs.readJson(path.resolve(dirPath,'DEMO.json'));
    data = packageObj.data;
    console.log(packageObj.version); // => 0.1.3  
    await fs.writeJson(path.resolve(dirPath,'DEMO1.json'), packageObj);
    console.log('success!');
    
  } catch (err) {
    console.error(err);
  }
}

4.1.5 Webview

webview API为开发者提供了完全自定义视图的能力,例如内置的Markdown插件使用了webview渲染Markdown预览文件。Webview也能用于构建比VS Code原生API支持构建的更加复杂的用户交互界面。

可以把webview看成是VS Code中的iframe,它可以渲染几乎全部的HTML内容,它通过消息机制和插件通信。这样的自由度令我们的webview非常强劲并将插件的潜力提升到了新的高度。

4.1.5.1 Webviews API 基础

这是Cat Coding插件的第一版package.json,你可以在这里找到完整的代码。我们的第一版插件提供了一个命令,叫做catCoding.start。当用户从命令面板调用Cat Coding: Start new cat coding session,或者一个创建好的键绑定命令,我们的猫猫会出现在webview窗口内。

{
  "name": "cat-coding",
  "description": "Cat Coding",
  "version": "0.0.1",
  "publisher": "bierner",
  "engines": {
    "vscode": "^1.23.0"
  },
  "activationEvents": ["onCommand:catCoding.start"],
  "main": "./out/src/extension",
  "contributes": {
    "commands": [
      {
        "command": "catCoding.start",
        "title": "Start new cat coding session",
        "category": "Cat Coding"
      }
    ]
  },
  "scripts": {
    "vscode:prepublish": "tsc -p ./",
    "compile": "tsc -watch -p ./",
    "postinstall": "node ./node_modules/vscode/bin/install"
  },
  "dependencies": {
    "vscode": "*"
  },
  "devDependencies": {
    "@types/node": "^9.4.6",
    "typescript": "^2.8.3"
  }
}

现在让我们实现catCoding.start命令,在我们的主文件中,像下面这样注册一个基础的webview:

import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand('catCoding.start', () => {
      // 创建并显示新的webview
      const panel = vscode.window.createWebviewPanel(
        'catCoding', // 只供内部使用,这个webview的标识
        'Cat Coding', // 给用户显示的面板标题
        vscode.ViewColumn.One, // 给新的webview面板一个编辑器视图
        {} // Webview选项。我们稍后会用上
      );
    })
  );
}

vscode.window.createWebviewPanel函数创建并在编辑区展示了一个webview,下图显示了如果你试着运行catCoding.start命令会显示的东西:
在这里插入图片描述
我们的命令以正确的标题打开了一个新的webview面板,但是没有任何内容!要想把我们的猫加到这个面板里面,我们需要webview.html设置HTML内容。

import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand('catCoding.start', () => {
      // 创建和显示webview
      const panel = vscode.window.createWebviewPanel(
        'catCoding',
        'Cat Coding',
        vscode.ViewColumn.One,
        {}
      );
      // 设置HTML内容
      panel.webview.html = getWebviewContent();
    })
  );
}
function getWebviewContent() {
  return
    `
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>Cat Coding</title>
        </head>
        <body>
            <img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
        </body>
        </html>
    `;
}

如果你再次运行命令,应该能看到下图:
在这里插入图片描述
大功告成!

webview.html应该是一个完整的HTML文档。使用HTML片段或者格式错乱的HTML会造成异常。

4.1.5.2 更新webview内容

webview.html也能在webview创建后更新内容,让我们用猫猫轮播图使Cat Coding具有动态性:

import * as vscode from 'vscode';
const cats = {
  'Coding Cat': 'https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif',
  'Compiling Cat': 'https://media.giphy.com/media/mlvseq9yvZhba/giphy.gif'
};
export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand('catCoding.start', () => {
      const panel = vscode.window.createWebviewPanel(
        'catCoding',
        'Cat Coding',
        vscode.ViewColumn.One,
        {}
      );
      let iteration = 0;
      const updateWebview = () => {
        const cat = iteration++ % 2 ? 'Compiling Cat' : 'Coding Cat';
        panel.title = cat;
        panel.webview.html = getWebviewContent(cat);
      };
      // 设置初始化内容
      updateWebview();
      // 每秒更新内容
      setInterval(updateWebview, 1000);
    })
  );
}
function getWebviewContent(cat: keyof typeof cats) {
  return
    `
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>Cat Coding</title>
        </head>
        <body>
            <img src="${cats[cat]}" width="300" />
        </body>
        </html>
    `;
}

因为webview.html方法替换了整个webview内容,页面看起来像重新加载了一个iframe。记住:如果你在webview中使用了脚本,那就意味着webview.html的重置会使脚本状态重置。

上述示例也使用了webview.title改变编辑器中的展示的文件名称,设置标题不会使webview重载。

4.1.5.3 生命周期

webview从属于创建他们的插件,插件必须保持住从webview返回的createWebviewPanel。如果你的插件失去了这个关联,它就不能再访问webview了,不过即使这样,webview还会继续展示在VS Code中。

因为webview是一个文本编辑器视图,所以用户可以随时关闭webview。当用户关闭了webview面板后,webview就被销毁了。在我们的例子中,销毁webview时抛出了一个异常,说明我们上面的示例中使用的seInterval实际上产生了非常严重的Bug:如果用户关闭了面板,setInterval会继续触发,而且还会尝试更新panel.webview.html,这当然会抛出异常。喵星人可不喜欢异常,我们现在就来解决这个问题吧。

onDidDispose事件在webview被销毁时触发,我们在这个事件结束之后更新并释放webview资源。

import * as vscode from 'vscode';
const cats = {
  'Coding Cat': 'https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif',
  'Compiling Cat': 'https://media.giphy.com/media/mlvseq9yvZhba/giphy.gif'
};
export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand('catCoding.start', () => {
      const panel = vscode.window.createWebviewPanel(
        'catCoding',
        'Cat Coding',
        vscode.ViewColumn.One,
        {}
      );
      let iteration = 0;
      const updateWebview = () => {
        const cat = iteration++ % 2 ? 'Compiling Cat' : 'Coding Cat';
        panel.title = cat;
        panel.webview.html = getWebviewContent(cat);
      };
      updateWebview();
      const interval = setInterval(updateWebview, 1000);
      panel.onDidDispose(
        () => {
          // 当面板关闭时,取消webview内容之后的更新
          clearInterval(interval);
        },
        null,
        context.subscriptions
      );
    })
  );
}

插件也可以通过编程方式关闭webview视图——调用它们的dispose()方法。我们假设,现在限制我们的猫猫每天工作5秒钟:

export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand('catCoding.start', () => {
      const panel = vscode.window.createWebviewPanel(
        'catCoding',
        'Cat Coding',
        vscode.ViewColumn.One,
        {}
      );
      panel.webview.html = getWebviewContent(cats['Coding Cat']);
      // 5秒后,程序性地关闭webview面板
      const timeout = setTimeout(() => panel.dispose(), 5000);
      panel.onDidDispose(
        () => {
          // 在第五秒结束之前处理用户手动的关闭动作
          clearTimeout(timeout);
        },
        null,
        context.subscriptions
      );
    })
  );
}
4.1.5.4 移动和可见性

当webview面板被移动到了非激活标签上,它就隐藏起来了。但这时并不是销毁,当重新激活标签后,VS Code会从webview.html自动恢复webview的内容。
.visible属性告诉你当前webview面板是否是可见的。

插件也可以通过调用reveal()方法,程序性地将webview面板激活。这个方法可以接受一个用于放置面板的目标视图布局。一个面板一次只能显示在一个编辑布局中。调用reveal()或者拖动webview面板到新的编辑布局中去。
现在更新我们的插件,一次只允许存在一个webview视图。如果面板处于非激活状态,那catCoding.start命令就把这个面板激活。

export function activate(context: vscode.ExtensionContext) {
  // 追踪当前webview面板
  let currentPanel: vscode.WebviewPanel | undefined = undefined;
  context.subscriptions.push(
    vscode.commands.registerCommand('catCoding.start', () => {
      const columnToShowIn = vscode.window.activeTextEditor
        ? vscode.window.activeTextEditor.viewColumn
        : undefined;
      if (currentPanel) {
        // 如果我们已经有了一个面板,那就把它显示到目标列布局中
        currentPanel.reveal(columnToShowIn);
      } else {
        // 不然,创建一个新面板
        currentPanel = vscode.window.createWebviewPanel(
          'catCoding',
          'Cat Coding',
          columnToShowIn,
          {}
        );
        currentPanel.webview.html = getWebviewContent(cats['Coding Cat']);
        // 当前面板被关闭后重置
        currentPanel.onDidDispose(
          () => {
            currentPanel = undefined;
          },
          null,
          context.subscriptions
        );
      }
    })
  );
}

下面是一个新插件的行为:
不论何时,如果webview的可见性改变了,或者当webview移动到了新的视图布局中,就会触发onDidChangeViewState。我们的插件可以利用这个时间改变布局中的webview显示的猫:

const cats = {
  'Coding Cat': 'https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif',
  'Compiling Cat': 'https://media.giphy.com/media/mlvseq9yvZhba/giphy.gif',
  'Testing Cat': 'https://media.giphy.com/media/3oriO0OEd9QIDdllqo/giphy.gif'
};
export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand('catCoding.start', () => {
      const panel = vscode.window.createWebviewPanel(
        'catCoding',
        'Cat Coding',
        vscode.ViewColumn.One,
        {}
      );
      panel.webview.html = getWebviewContent(cats['Coding Cat']);
      // 根据视图状态变动更新内容
      panel.onDidChangeViewState(
        e => {
          const panel = e.webviewPanel;
          switch (panel.viewColumn) {
            case vscode.ViewColumn.One:
              updateWebviewForCat(panel, 'Coding Cat');
              return;
            case vscode.ViewColumn.Two:
              updateWebviewForCat(panel, 'Compiling Cat');
              return;
            case vscode.ViewColumn.Three:
              updateWebviewForCat(panel, 'Testing Cat');
              return;
          }
        },
        null,
        context.subscriptions
      );
    })
  );
}
function updateWebviewForCat(panel: vscode.WebviewPanel, catName: keyof typeof cats) {
  panel.title = catName;
  panel.webview.html = getWebviewContent(cats[catName]);
}
4.1.5.5 检查和调试webviews

在命令面板中输入Developer: Toggle Developer Tools能帮助你调试webview。运行命令之后会为当前可见的webview加载一个devtool:
在这里插入图片描述

4.1.5.6 脚本和信息传递

既然webview就像iframe一样,也就是说它们也可以运行脚本,webview中的Javascript默认是禁用的,不过我们能用enableScripts: true打开它。

让我们写一段脚本,追踪我们家喵星人写代码的行数。运行一个基础脚本非常的容易,但是注意这个示例只作演示用途,在实践中,你的webview应该遵循内容安全政策,禁止行内脚本。

import * as path from 'path';
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
    context.subscriptions.push(vscode.commands.registerCommand('catCoding.start', () => {
        const panel = vscode.window.createWebviewPanel('catCoding', "Cat Coding", vscode.ViewColumn.One, {
            // 在webview中启用脚本
            enableScripts: true
        });
        panel.webview.html = getWebviewContent();
    }));
}
function getWebviewContent() {
    return
        `
            <!DOCTYPE html>
            <html lang="en">
            <head>
                <meta charset="UTF-8">
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
                <title>Cat Coding</title>
            </head>
            <body>
                <img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
                <h1 id="lines-of-code-counter">0</h1>
                <script>
                    const counter = document.getElementById('lines-of-code-counter');
                    let count = 0;
                    setInterval(() => {
                        counter.textContent = count++;
                    }, 100);
                </script>
            </body>
            </html>
        `;
}

将插件的信息传递到webview
插件可以用webview.postMessage()将数据发送到它的webview中。这个方法能发送任何序列化的JSON数据到webview中,在webview中则通过message事件接受信息。

我们现在就来看看这个实现,在Cat Coding中新增一个命令来表示我们家的喵在重构他的代码(所以会减少代码总行数)。
新增catCoding.doRefactor命令,利用postMessage把指示发送到webview中,webview中的window.addEventListener(‘message’ event => { … })则会处理这些信息:

export function activate(context: vscode.ExtensionContext) {
    // 现在只有一只喵喵程序员了
    let currentPanel: vscode.WebviewPanel | undefined = undefined;
    context.subscriptions.push(vscode.commands.registerCommand('catCoding.start', () => {
        if (currentPanel) {
            currentPanel.reveal(vscode.ViewColumn.One);
        } else {
            currentPanel = vscode.window.createWebviewPanel('catCoding', "Cat Coding", vscode.ViewColumn.One, {
                enableScripts: true
            });
            currentPanel.webview.html = getWebviewContent();
            currentPanel.onDidDispose(() => { currentPanel = undefined; }, undefined, context.subscriptions);
        }
    }));
    // 我们新的命令
    context.subscriptions.push(vscode.commands.registerCommand('catCoding.doRefactor', () => {
        if (!currentPanel) {
            return;
        }
        // 把信息发送到webview
        // 你可以发送任何序列化的JSON数据
        currentPanel.webview.postMessage({ command: 'refactor' });
    }));
}
function getWebviewContent() {
    return `
            <!DOCTYPE html>
            <html lang="en">
            <head>
                <meta charset="UTF-8">
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
                <title>Cat Coding</title>
            </head>
            <body>
                <img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
                <h1 id="lines-of-code-counter">0</h1>
                <script>
                    const counter = document.getElementById('lines-of-code-counter');
                    let count = 0;
                    setInterval(() => {
                        counter.textContent = count++;
                    }, 100);
                    // Handle the message inside the webview
                    window.addEventListener('message', event => {
                        const message = event.data; // The JSON data our extension sent
                        switch (message.command) {
                            case 'refactor':
                                count = Math.ceil(count * 0.5);
                                counter.textContent = count;
                                break;
                        }
                    });
                </script>
            </body>
            </html>
        `;
        }

将webview的信息传递到插件中
webview也可以把信息传递回对应的插件中,用VS Code API 为webview提供的postMessage函数我们就可以完成这个目标。调用webview中的acquireVsCodeApi获取VS Code API对象。这个函数在一个会话中只能调用一次,你必须保持住这个方法返回的VS Code API实例,然后再转交到需要调用这个实例的地方。

现在我们在Cat Coding添加一个弹出bug的警示框:

export function activate(context: vscode.ExtensionContext) {
    context.subscriptions.push(vscode.commands.registerCommand('catCoding.start', () => {
        const panel = vscode.window.createWebviewPanel('catCoding', "Cat Coding", vscode.ViewColumn.One, {
            enableScripts: true
        });
        panel.webview.html = getWebviewContent();
        // 处理webview中的信息
        panel.webview.onDidReceiveMessage(message => {
            switch (message.command) {
                case 'alert':
                    vscode.window.showErrorMessage(message.text);
                    return;
            }
        }, undefined, context.subscriptions);
    }));
}
function getWebviewContent() {
    return
        `
            <!DOCTYPE html>
            <html lang="en">
            <head>
                <meta charset="UTF-8">
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
                <title>Cat Coding</title>
            </head>
            <body>
                <img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
                <h1 id="lines-of-code-counter">0</h1>
                <script>
                    (function() {
                        const vscode = acquireVsCodeApi();
                        const counter = document.getElementById('lines-of-code-counter');
                        let count = 0;
                        setInterval(() => {
                            counter.textContent = count++;
                            // Alert the extension when our cat introduces a bug
                            if (Math.random() < 0.001 * count) {
                                vscode.postMessage({
                                    command: 'alert',
                                    text: '🐛  on line ' + count
                                })
                            }
                        }, 100);
                    }())
                </script>
            </body>
            </html>
        `;
}

4.1.6 菜单

4.1.7 虚拟文档

官网DEMO
通过VS Code的文本内容供应器API(text document content provider API),你可以为任意来源的文件创建只读文档。
TextDocumentContentProvider
这个API工作于uri协议之上,你需要声明一个供应器函数(provider),然后这个函数还需要返回文本内容。供应器函数必须提供协议(scheme),而且函数注册之后不可改变这个协议。一个供应器函数可以对应多个协议,而多个供应器函数也可以只注册一个协议。

vscode.workspace.registerTextDocumentContentProvider(myScheme, myProvider);

调用registerTextDocumentContentProvider函数会返回一个用于释放资源的释放器。供应器函数还必须实现provideTextDocumentContent函数,这个函数需要传入uri参数和取消式令牌(cancellation token)调用。

const myProvider = class implements vscode.TextDocumentContentProvider {
    provideTextDocumentContent(uri: vscode.Uri): string {
        // 简单调用cowsay, 直接把uri-path当做文本内容
        return cowsay.say({ text: uri.path });
    }
};

!> 注意:我们的供应器函数不为虚拟文档创建uri——他的角色仅仅只是根据uri返回对应的文本内容。

下面我们简单使用一个’cowsay’命令创建一个uri,然后编辑器就能显示了

vscode.commands.registerCommand('cowsay.say', async () => {
    let what = await vscode.window.showInputBox({ placeHolder: 'cow say?' });
    if (what) {
        let uri = vscode.Uri.parse('cowsay:' + what);
        let doc = await vscode.workspace.openTextDocument(uri); // 调用供应器函数
        await vscode.window.showTextDocument(doc, { preview: false });
    }
});

这个命令首先弹出了一个输入框,然后创建了一个cowsay协议的uri,再然后根据这个uri读取了文档,最后为这个文档内容打开了一个编辑器(这里的“编辑器”不是指VS Code本身,而是VS Code中打开的单个编辑区tab)。在第三步中,供应器函数需要为这个uri提供对应的内容。

经过这整个流程,我们才算完整地实现了一个文本内容供应器,接下来的部分我们继续学习怎么更新虚拟文档,怎么注册虚拟文档的 UI命令。
更新虚拟文档
为了支持跟踪虚拟文档发生的变化,供应器实现了onDidChange事件。如果文档正在被使用,那么必须为其提供一个uri来调用它,同时编辑器会请求新的内容。
vscode.Event定义了VS Code的事件规范。实现事件的最好方式就是使用vscode.EventEmitter,比如:

const myProvider = class implements vscode.TextDocumentContentProvider {
  // 事件发射器和事件
  onDidChangeEmitter = new vscode.EventEmitter<vscode.Uri>();
  onDidChange = this.onDidChangeEmitter.event;
  //...
};

上述就是VS Code监听虚拟文档变化所必须的内容。下面将使用事件发射器来添加编辑器行为。
添加编辑器命令
为了阐述事件变动和获取更多cowsay,我们需要倒叙cow刚刚说的东西。首先我们需要一个命令:

// 注册一个可以更新当前cow的命令
subscriptions.push(
    vscode.commands.registerCommand('cowsay.backwards', async () => {
        if (!vscode.window.activeTextEditor) {
            return; // 不打开编辑器
        }
        let { document } = vscode.window.activeTextEditor;
        if (document.uri.scheme !== myScheme) {
            return; // 不是我的协议时直接返回
        }
        // 获取path的内容, 对这个内容倒序处理, 然后创建一个新的uri
        let say = document.uri.path;
        let newSay = say
            .split('')
            .reverse()
            .join('');
        let newUri = document.uri.with({ path: newSay });
        await vscode.window.showTextDocument(newUri, { preview: false });
    })
);

上面的代码片段检查了我们当前是不是激活了一个编辑器(用户当前选中的编辑器tab),对应的文档是不是符合我们的协议。因为命令是对任何人生效的,所以我们有必要去做这些检查。然后uri的path对象被翻转,再重新创建出一个新的uri,最后则打开了一个编辑器。
注册命令最重要事就是在package.json中声明配置。我们在contributes部分添加下列配置:

"menus": {
  "editor/title": [
    {
      "command": "cowsay.backwards",
      "group": "navigation",
      "when": "resourceScheme == cowsay"
    }
  ]
}

contributes/commands中的cowsay.backwards命令告诉编辑器操作出现在编辑器的标题菜单中(工具栏右上角),但如果只是这样简单的配置,每个编辑器就都会显示这个命令。然后我们的when语句就出场了,它描述了何时才显示这个操作。在这个例子中,文档的资源协议必须是cowsay,我们的命令才会生效。这个配置对默认显示全部命令的commandPalette菜单同样生效。
在这里插入图片描述
事件的可见性
文档供应器函数是VS Code中的一等公民,它们的内容以常规的文本文档格式呈现,它们共用一套基础实现方式——如:使用了文件系统的实现。这也就意味着“你的”文档无法被隐藏,它们必定会出现在onDidOpenTextDocument和onDidCloseTextDocument事件中,它们是vscode.workspace.textDocuments中的一部分。通用的准则就是根据文档的协议决定你是否需要对文档进行什么操作。
文件系统API
如果你需要更强的灵活性和掌控力,请查看FileSystemProviderAPI,它可以实现整套完整的文件系统,获取文件、文件夹、二进制数据,删除文件,创建文件等等。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值