写在后面

随着7月一波牛市行情,越来越多的人投身A股行列,然而股市的危险微小,有人一夜暴富,也有人血本无归,所以对于普通人来说基金定投是个不错的抉择,自己也是基金定投的一枚小韭菜。

下班的时候常常心理痒痒,想看看明天的基金又赚(ge)了多少钱,拿出手机关上支付宝的步骤过于繁琐,而且我也不太关怀其余的指标,只是想晓得明天的净值与涨幅。VS Code 做为一个编码工具,提供了弱小的插件机制,咱们能够好好利用这个能力,能够一边编码的时候一边看看行情。

实现插件

初始化

VSCode 官网提供了十分不便的插件模板,咱们能够间接通过 Yeoman 来生成 VS Code 插件的模板。

先全局装置 yo 和 generator-code,运行命令 yo code

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

这里咱们应用 TypeScript 来编写插件。

生成后的目录构造如下:

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 包体现统一。
  • namepublisher:name 是插件名,publisher 是发布者。${publisher}.${name} 形成插件 ID。

比拟值得关注的就是 contributesactivationEvents 这两个配置。

创立视图

咱们首先在咱们的利用中创立一个视图容器,视图容器简略来说一个独自的侧边栏,在 package.jsoncontributes.viewsContainers 中进行配置。

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

而后咱们还须要增加一个视图,在 package.jsoncontributes.views 中进行配置,该字段为一个对象,它的 Key 就是咱们视图容器的 id,值为一个数组,示意一个视图容器内可增加多个视图。

{  "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 运行能力。

切换到调试面板,间接点击运行,就能看到侧边栏多了个图标。

增加配置

咱们须要获取基金的列表,当然须要一些基金代码,而这些代码咱们能够放到 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"    }  ]}

咱们须要通过 vscode 提供的 registerTreeDataProvider 为视图提供数据。关上生成的 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 Code 提供的 window.registerTreeDataProvider 来注册数据,传入的第一个参数示意视图 ID,第二个参数是 TreeDataProvider 的实现。

TreeDataProvider 有两个必须实现的办法:

  • getChildren:该办法承受一个 element,返回 element 的子元素,如果没有element,则返回的是根节点的子元素,咱们这里因为是单列表,所以不会承受 element 元素;
  • getTreeItem:该办法承受一个 element,返回视图单行的 UI 数据,须要对 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,这里咱们应用天天基金网的数据。

通过申请能够看到,天天基金网通过 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 的形式来实现 UI 的,当初咱们须要从新结构一个 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);  }}
// TreeItemimport { 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 须要提供一个 onDidChangeTreeData 属性,该属性是 EventEmitter 的一个实例,而后通过触发 EventEmitter 实例进行数据的更新,每次调用 refresh 办法相当于从新调用了 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:用于注册命令,指定命令的名称、图标,以及 command 用于 extension 中绑定相应事件;
  • menus:用于标记命令展现的地位;

    • when:定义展现的视图,具体语法能够查阅官网文档;
    • group:定义菜单的分组;
    • command:定义命令调用的事件;

配置好命令后,回到 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() {}

当初咱们就能够手动刷新了。

新增基金

咱们新增一个按钮用了新增基金。

{  "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 插件市场,感兴趣的能够间接下载该插件,或者在 github 上下载残缺代码。