Реализация плагина VS Code Foundation с нуля

внешний интерфейс TypeScript

написать впереди

С бычьим рынком в июле все больше и больше людей пополняли ряды А-акций, но риск фондового рынка огромен, некоторые люди разбогатеют в одночасье, а некоторые потеряют все, поэтому для обычных людей фиксированные инвестиции в фонды это хороший выбор Я также маленький лук-порей, в который будет инвестировать фонд.

基金定投

Когда я иду на работу, я часто чувствую зуд.Я хочу посмотреть, сколько фонд заработал (ge) сегодня.Этапы, чтобы достать мой мобильный телефон и открыть Alipay, слишком громоздки, и я не особо забочусь о других индикаторы.Я просто хочу узнать сегодняшнюю чистую стоимость и увеличить. В качестве инструмента кодирования VS Code предоставляет мощный механизм подключаемых модулей, и мы можем с пользой использовать эту возможность и наблюдать за рынком во время кодирования. Вы можете установить этот плагин, выполнив поиск «fund-watch» в VS Code.

示例

Реализовать плагин

инициализация

VSCode официально предоставляет очень удобный шаблон плагина, мы можем напрямую передатьYeomanдля создания шаблонов для плагинов VS Code.

Сначала установите глобальноyoа такжеgenerator-code, выполните командуyo code.

# 全局安装 yo 模块
npm install -g yo generator-code

Здесь мы используем TypeScript для написания плагинов.

yo code

yo code

Сгенерированная структура каталогов выглядит следующим образом:

目录结构

Плагин VS Code можно просто понимать как пакет Npm, для которого также требуетсяpackage.jsonфайл, свойства в основном такие же, как и у пакета Npm.

{
  // 名称
  "name": "fund-watch",
  // 版本
  "version": "1.0.0",
  // 描述
  "description": "实时查看基金行情",
  // 发布者
  "publisher": "shenfq",
  // 版本要求
  "engines": {
    "vscode": "^1.45.0"
  },
  // 入口文件
  "main": "./out/extension.js",
  "scripts": {
    "compile": "tsc -p ./",
    "watch": "tsc -watch -p ./",
  },
  "devDependencies": {
    "@types/node": "^10.14.17",
    "@types/vscode": "^1.41.0",
    "typescript": "^3.9.7"
  },
  // 插件配置
  "contributes": {},
  // 激活事件
  "activationEvents": [],
}

Кратко представим наиболее важные конфигурации.

  • contributes: Конфигурация, связанная с плагином.
  • activationEvents: Событие активации.
  • main: файл входа плагина, который ведет себя так же, как пакет Npm.
  • name,publisher:name — это имя плагина, а publisher — издатель.${publisher}.${name}Формирует идентификатор плагина.

Более примечательным являетсяcontributesа такжеactivationEventsэти две конфигурации.

Создать представление

Сначала мы создаем контейнер представления в нашем приложении, контейнер представления представляет собой просто одну боковую панель, вpackage.jsonизcontributes.viewsContainersв конфигурации.

{
  "contributes": {
    "viewsContainers": {
      "activitybar": [
        {
          "id": "fund-watch",
          "title": "FUND WATCH",
          "icon": "images/fund.svg"
        }
      ]
    }
  }
}

侧边栏

Затем нам также нужно добавить представление, вpackage.jsonизcontributes.viewsПоле — это объект, его ключ — это идентификатор нашего контейнера представления, а значение — массив, указывающий, что в контейнер представления можно добавить несколько представлений.

{
  "contributes": {
    "viewsContainers": {
      "activitybar": [
        {
          "id": "fund-watch",
          "title": "FUND WATCH",
          "icon": "images/fund.svg"
        }
      ]
    },
    "views": {
      "fund-watch": [
        {
          "name": "自选基金",
          "id": "fund-list"
        }
      ]
    }
  }
}

Если вы не хотите добавлять его в настраиваемый контейнер представления, вы можете выбрать контейнер представления, поставляемый с VS Code.

  • explorer: отображается на боковой панели проводника
  • debug: Отображается в боковой панели отладки
  • scm: отображается на боковой панели исходного кода
{
  "contributes": {
    "views": {
      "explorer": [
        {
          "name": "自选基金",
          "id": "fund-list"
        }
      ]
    }
  }
}

显示到资源管理器中

Запустите плагин

использоватьYeomanСгенерированный шаблон поставляется с возможностью запуска VS Code.

vscode配置

Переключитесь на панель отладки, нажмите «Выполнить напрямую», и вы увидите дополнительный значок на боковой панели.

调试面板

运行结果

добавить конфигурацию

Нам нужно получить список фондов и, конечно же, несколько кодов фондов, которые мы можем поместить в конфигурацию VS Code.

{
  "contributes": {
    // 配置
    "configuration": {
      // 配置类型,对象
      "type": "object",
      // 配置名称
      "title": "fund",
      // 配置的各个属性
      "properties": {
        // 自选基金列表
        "fund.favorites": {
          // 属性类型
          "type": "array",
          // 默认值
          "default": [
            "163407",
            "161017"
          ],
          // 描述
          "description": "自选基金列表,值为基金代码"
        },
        // 刷新时间的间隔
        "fund.interval": {
          "type": "number",
          "default": 2,
          "description": "刷新时间,单位为秒,默认 2 秒"
        }
      }
    }
  }
}

просмотреть данные

Давайте вернемся к ранее зарегистрированному представлению, которое в VS Code называется представлением в виде дерева.

"views": {
  "fund-watch": [
    {
      "name": "自选基金",
      "id": "fund-list"
    }
  ]
}

Нам нужно предоставить через vscoderegisterTreeDataProviderПредоставьте данные представлению. открыть сгенерированныйsrc/extension.tsфайл, измените код следующим образом:

// vscode 模块为 VS Code 内置,不需要通过 npm 安装
import { ExtensionContext, commands, window, workspace } from 'vscode';
import Provider from './Provider';

// 激活插件
export function activate(context: ExtensionContext) {
  // 基金类
  const provider = new Provider();

  // 数据注册
  window.registerTreeDataProvider('fund-list', provider);
}

export function deactivate() {}

Здесь мы предоставляем через VS Codewindow.registerTreeDataProviderДля регистрации данных первый передаваемый параметр представляет идентификатор представления, а второй параметр —TreeDataProviderреализация.

TreeDataProviderНеобходимо реализовать два метода:

  • getChildren: этот метод принимает элемент и возвращает дочерний элемент элемента.Если элемента нет, он возвращает дочерний элемент корневого узла.Поскольку это один список, мы не будем принимать элемент элемента;
  • getTreeItem: этот метод принимает элемент и возвращает данные пользовательского интерфейса одной строки представления.TreeItemсоздать экземпляр;

Мы демонстрируем эти два метода через диспетчер ресурсов VS Code:

方法展示

Обладая вышеуказанными знаниями, мы можем легко предоставить данные для древовидного представления.

import { workspace, TreeDataProvider, TreeItem } from 'vscode';

export default class DataProvider implements TreeDataProvider<string> {
  refresh() {
    // 更新视图
  }

  getTreeItem(element: string): TreeItem {
    return new TreeItem(element);
  }

  getChildren(): string[] {
    const { order } = this;
    // 获取配置的基金代码
    const favorites: string[] = workspace
      .getConfiguration()
      .get('fund-watch.favorites', []);
    
    // 依据代码排序
		return favorites.sort((prev, next) => (prev >= next ? 1 : -1) * order);
  }
}


Запустив его сейчас, вы можете обнаружить, что в представлении нет данных, это связано с тем, что событие активации не настроено.

{
	"activationEvents": [
    // 表示 fund-list 视图展示时,激活该插件
		"onView:fund-list"
	]
}

基金代码列表

запросить данные

Мы успешно отобразили код фонда в представлении, а затем нам нужно запросить данные фонда. В Интернете есть много API, связанных с фондами, здесь мы используем данные Tiantian Fund.com.

天天基金网

Из запроса видно, что Tiantian Fund Network получает данные, связанные с фондом, через JSONP.Нам нужно только создать URL-адрес и передать текущую временную метку.

const url = `https://fundgz.1234567.com.cn/js/${code}.js?rt=${time}`

Чтобы запросить данные в VS Code, вам необходимо использовать внутреннююhttpsмодуль, давайте создадим новыйapi.ts.

import * as https from 'https';

// 发起 GET 请求
const request = async (url: string): Promise<string> => {
  return new Promise((resolve, reject) => {
    https.get(url, (res) => {
      let chunks = '';
      if (!res || res.statusCode !== 200) {
        reject(new Error('网络请求错误!'));
        return;
      }
      res.on('data', (chunk) => chunks += chunk.toString('utf8'));
      res.on('end', () => resolve(chunks));
    });
  });
};

interface FundInfo {
  now: string
  name: string
  code: string
  lastClose: string
  changeRate: string
  changeAmount: string
}

// 根据基金代码请求基金数据
export default function fundApi(codes: string[]): Promise<FundInfo[]> {
  const time = Date.now();
	// 请求列表
  const promises: Promise<string>[] = codes.map((code) => {
    const url = `https://fundgz.1234567.com.cn/js/${code}.js?rt=${time}`;
    return request(url);
  });
  return Promise.all(promises).then((results) => {
    const resultArr: FundInfo[] = [];
    results.forEach((rsp: string) => {
      const match = rsp.match(/jsonpgz\((.+)\)/);
      if (!match || !match[1]) {
        return;
      }
      const str = match[1];
      const obj = JSON.parse(str);
      const info: FundInfo = {
        // 当前净值
        now: obj.gsz,
        // 基金名称
        name: obj.name,
        // 基金代码
        code: obj.fundcode,
        // 昨日净值
        lastClose: obj.dwjz,
        // 涨跌幅
        changeRate: obj.gszzl,
        // 涨跌额
        changeAmount: (obj.gsz - obj.dwjz).toFixed(4),
      };
      resultArr.push(info);
    });
    return resultArr;
  });
}

Затем измените данные представления.

import { workspace, TreeDataProvider, TreeItem } from 'vscode';
import fundApi from './api';

export default class DataProvider implements TreeDataProvider<FundInfo> {
  // 省略了其他代码
  getTreeItem(info: FundInfo): TreeItem {
    // 展示名称和涨跌幅
  	const { name, changeRate } = info
    return new TreeItem(`${name}  ${changeRate}`);
  }

  getChildren(): Promise<FundInfo[]> {
    const { order } = this;
    // 获取配置的基金代码
    const favorites: string[] = workspace
      .getConfiguration()
      .get('fund-watch.favorites', []);
    
    // 获取基金数据
		return fundApi([...favorites]).then(
      (results: FundInfo[]) => results.sort(
      	(prev, next) => (prev.changeRate >= next.changeRate ? 1 : -1) * order
    	)
    );
  }
}

视图数据

украсить формат

Ранее мы использовали прямое создание экземпляровTreeItemспособ реализации пользовательского интерфейса, теперь нам нужно восстановитьTreeItem.

import { workspace, TreeDataProvider, TreeItem } from 'vscode';
import FundItem from './TreeItem';
import fundApi from './api';

export default class DataProvider implements TreeDataProvider<FundInfo> {
  // 省略了其他代码
  getTreeItem(info: FundInfo): FundItem {
    return new FundItem(info);
  }
}
// TreeItem
import { TreeItem } from 'vscode';

export default class FundItem extends TreeItem {
  info: FundInfo;

  constructor(info: FundInfo) {
    const icon = Number(info.changeRate) >= 0 ? '📈' : '📉';

    // 加上 icon,更加直观的知道是涨还是跌
    super(`${icon}${info.name}   ${info.changeRate}%`);

    let sliceName = info.name;
    if (sliceName.length > 8) {
      sliceName = `${sliceName.slice(0, 8)}...`;
    }
    const tips = [
      `代码: ${info.code}`,
      `名称: ${sliceName}`,
      `--------------------------`,
      `单位净值:    ${info.now}`,
      `涨跌幅:     ${info.changeRate}%`,
      `涨跌额:     ${info.changeAmount}`,
      `昨收:      ${info.lastClose}`,
    ];

    this.info = info;
    // tooltip 鼠标悬停时,展示的内容
    this.tooltip = tips.join('\r\n');
  }
}

美化后

обновить данные

TreeDataProviderнеобходимо предоставитьonDidChangeTreeDataAttribute, который является экземпляром EventEmitter, а затем обновляет данные, запуская экземпляр EventEmitter.Каждый вызов метода обновления эквивалентен повторному вызовуgetChildrenметод.

import { workspace, Event, EventEmitter, TreeDataProvider } from 'vscode';
import FundItem from './TreeItem';
import fundApi from './api';

export default class DataProvider implements TreeDataProvider<FundInfo> {
  private refreshEvent: EventEmitter<FundInfo | null> = new EventEmitter<FundInfo | null>();
  readonly onDidChangeTreeData: Event<FundInfo | null> = this.refreshEvent.event;

  refresh() {
    // 更新视图
    setTimeout(() => {
      this.refreshEvent.fire(null);
    }, 200);
  }
}

мы возвращаемсяextension.ts, добавьте таймер для регулярного обновления данных.

import { ExtensionContext, commands, window, workspace } from 'vscode'
import Provider from './data/Provider'

// 激活插件
export function activate(context: ExtensionContext) {
  // 获取 interval 配置
  let interval = workspace.getConfiguration().get('fund-watch.interval', 2)
  if (interval < 2) {
    interval = 2
  }

  // 基金类
  const provider = new Provider()

  // 数据注册
  window.registerTreeDataProvider('fund-list', provider)

  // 定时更新
  setInterval(() => {
    provider.refresh()
  }, interval * 1000)
}

export function deactivate() {}

Помимо регулярных обновлений, нам также необходимо предоставить возможность обновления вручную. Исправлятьpackage.json, команда регистрации.

{
  "contributes": {
		"commands": [
			{
				"command": "fund.refresh",
				"title": "刷新",
				"icon": {
					"light": "images/light/refresh.svg",
					"dark": "images/dark/refresh.svg"
				}
			}
		],
		"menus": {
			"view/title": [
				{
					"when": "view == fund-list",
					"group": "navigation",
					"command": "fund.refresh"
				}
			]
		}
	}
}
  • commands: используется для регистрации команды, указывается имя и значок команды, а команда используется для привязки соответствующего события в расширении;
  • menus: используется для обозначения позиции, в которой отображается команда;
    • when: определите отображаемый вид, конкретный синтаксис можно найти вофициальная документация;
    • группа: определяет группировку меню;
    • команда: определяет событие, вызываемое командой;

view-actions

После настройки команды вернитесь кextension.tsсередина.

import { ExtensionContext, commands, window, workspace } from 'vscode';
import Provider from './Provider';

// 激活插件
export function activate(context: ExtensionContext) {
  let interval = workspace.getConfiguration().get('fund-watch.interval', 2);
  if (interval < 2) {
    interval = 2;
  }

  // 基金类
  const provider = new Provider();

  // 数据注册
  window.registerTreeDataProvider('fund-list', provider);

  // 定时任务
  setInterval(() => {
    provider.refresh();
  }, interval * 1000);

  // 事件
  context.subscriptions.push(
    commands.registerCommand('fund.refresh', () => {
      provider.refresh();
    }),
  );
}

export function deactivate() {}

Теперь мы можем обновить вручную.

image-20200824113219392

новый фонд

Мы добавили кнопку для использования «Добавить средства».

{
  "contributes": {
		"commands": [
      {
        "command": "fund.add",
        "title": "新增",
        "icon": {
          "light": "images/light/add.svg",
          "dark": "images/dark/add.svg"
        }
      },
			{
				"command": "fund.refresh",
				"title": "刷新",
				"icon": {
					"light": "images/light/refresh.svg",
					"dark": "images/dark/refresh.svg"
				}
			}
		],
		"menus": {
			"view/title": [
        {
          "command": "fund.add",
          "when": "view == fund-list",
          "group": "navigation"
        },
				{
					"when": "view == fund-list",
					"group": "navigation",
					"command": "fund.refresh"
				}
			]
		}
	}
}

существуетextension.ts Зарегистрируйтесь на мероприятия.

import { ExtensionContext, commands, window, workspace } from 'vscode';
import Provider from './Provider';

// 激活插件
export function activate(context: ExtensionContext) {
  // 省略部分代码 ...
  
  // 基金类
  const provider = new Provider();

  // 事件
  context.subscriptions.push(
    commands.registerCommand('fund.add', () => {
      provider.addFund();
    }),
    commands.registerCommand('fund.refresh', () => {
      provider.refresh();
    }),
  );
}

export function deactivate() {}

Реализовать новые функции, модифицироватьProvider.ts.

import { workspace, Event, EventEmitter, TreeDataProvider } from 'vscode';
import FundItem from './TreeItem';
import fundApi from './api';

export default class DataProvider implements TreeDataProvider<FundInfo> {
  // 省略部分代码 ...

  // 更新配置
  updateConfig(funds: string[]) {
    const config = workspace.getConfiguration();
    const favorites = Array.from(
      // 通过 Set 去重
      new Set([
        ...config.get('fund-watch.favorites', []),
        ...funds,
      ])
    );
    config.update('fund-watch.favorites', favorites, true);
  }

  async addFund() {
    // 弹出输入框
    const res = await window.showInputBox({
      value: '',
      valueSelection: [5, -1],
      prompt: '添加基金到自选',
      placeHolder: 'Add Fund To Favorite',
      validateInput: (inputCode: string) => {
        const codeArray = inputCode.split(/[\W]/);
        const hasError = codeArray.some((code) => {
          return code !== '' && !/^\d+$/.test(code);
        });
        return hasError ? '基金代码输入有误' : null;
      },
    });
    if (!!res) {
      const codeArray = res.split(/[\W]/) || [];
      const result = await fundApi([...codeArray]);
      if (result && result.length > 0) {
        // 只更新能正常请求的代码
        const codes = result.map(i => i.code);
        this.updateConfig(codes);
        this.refresh();
      } else {
        window.showWarningMessage('stocks not found');
      }
    }
  }
}

新增按钮

输入框

удалить фонд

Наконец, добавьте кнопку для удаления фонда.

{
	"contributes": {
		"commands": [
			{
				"command": "fund.item.remove",
				"title": "删除"
			}
		],
		"menus": {
      // 这个按钮放到 context 中
      "view/item/context": [
        {
          "command": "fund.item.remove",
          "when": "view == fund-list",
          "group": "inline"
        }
      ]
		}
  }
}

существуетextension.ts Зарегистрируйтесь на мероприятия.

import { ExtensionContext, commands, window, workspace } from 'vscode';
import Provider from './Provider';

// 激活插件
export function activate(context: ExtensionContext) {
  // 省略部分代码 ...
  
  // 基金类
  const provider = new Provider();

  // 事件
  context.subscriptions.push(
    commands.registerCommand('fund.add', () => {
      provider.addFund();
    }),
    commands.registerCommand('fund.refresh', () => {
      provider.refresh();
    }),
    commands.registerCommand('fund.item.remove', (fund) => {
      const { code } = fund;
      provider.removeConfig(code);
      provider.refresh();
    })
  );
}

export function deactivate() {}

Реализовать новые функции, модифицироватьProvider.ts.

import { window, workspace, Event, EventEmitter, TreeDataProvider } from 'vscode';
import FundItem from './TreeItem';
import fundApi from './api';

export default class DataProvider implements TreeDataProvider<FundInfo> {
  // 省略部分代码 ...

  // 删除配置
  removeConfig(code: string) {
    const config = workspace.getConfiguration();
    const favorites: string[] = [...config.get('fund-watch.favorites', [])];
    const index = favorites.indexOf(code);
    if (index === -1) {
      return;
    }
    favorites.splice(index, 1);
    config.update('fund-watch.favorites', favorites, true);
  }
}

删除按钮

Суммировать

В процессе внедрения также много проблем, если вы столкнулись с проблемами, вы можете прочитать подробнееПлагин VSCode документация на китайском языке. Плагин был выпущен на рынке плагинов VS Code, если вы заинтересованы, вы можете напрямуюскачать плагинили скачать на гитхабеполный код.