隨着7月一波牛市行情,愈來愈多的人投身A股行列,可是股市的風險巨大,有人一晚上暴富,也有人血本無歸,因此對於普通人來講基金定投是個不錯的選擇,本人也是基金定投的一枚小韭菜。node
上班的時候常常心理癢癢,想看看今天的基金又賺(ge)了多少錢,拿出手機打開支付寶的步驟過於繁瑣,並且我也不太關心其餘的指標,只是想知道今天的淨值與漲幅。VS Code 作爲一個編碼工具,提供了強大的插件機制,咱們能夠好好利用這個能力,能夠一邊編碼的時候一邊看看行情。你們能夠在 VS Code 中搜索 「fund-watch」來安裝這個插件。git
VSCode 官方提供了很是方便的插件模板,咱們能夠直接經過 Yeoman
來生成 VS Code 插件的模板。github
先全局安裝 yo 和 generator-code,運行命令 yo code
。typescript
# 全局安裝 yo 模塊 npm install -g yo generator-code
這裏咱們使用 TypeScript 來編寫插件。npm
生成後的目錄結構以下:json
VS Code 插件能夠簡單理解爲一個 Npm 包,也須要一個 package.json
文件,屬性與 Npm 包的基本一致。api
{ // 名稱 "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}
構成插件 ID。比較值得關注的就是 contributes
和 activationEvents
這兩個配置。promise
咱們首先在咱們的應用中建立一個視圖容器,視圖容器簡單來講一個單獨的側邊欄,在 package.json
的 contributes.viewsContainers
中進行配置。bash
{ "contributes": { "viewsContainers": { "activitybar": [ { "id": "fund-watch", "title": "FUND WATCH", "icon": "images/fund.svg" } ] } } }
而後咱們還須要添加一個視圖,在 package.json
的 contributes.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); } }
// 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
須要提供一個 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
:定義展現的視圖,具體語法能夠查閱官方文檔;配置好命令後,回到 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 上下載完整代碼。