写在后面
随着 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 包体现统一。name
、publisher
:name 是插件名,publisher 是发布者。${publisher}.${name}
形成插件 ID。
比拟值得关注的就是 contributes
和 activationEvents
这两个配置。
创立视图
咱们首先在咱们的利用中创立一个视图容器,视图容器简略来说一个独自的侧边栏,在 package.json
的 contributes.viewsContainers
中进行配置。
{
"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
:定义展现的视图,具体语法能够查阅官网文档;- 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 上下载残缺代码。