背景
这段时间开始在掘金写文章了,但是写完文章后,想快速知道当前文章的数据,比如新增赞、收藏、评论、粉丝等,就需要一直在浏览器中刷新页面,然后看文章数据,这样在上班的时候很不方便。于是想写一个vscode插件,在写代码的时候也能实时看到自己最新文章的数据。
效果展示
vscode底部展示,定时请求接口刷新文章数据。
如果有消息通知,会主动推送消息,这里用报错提示是有原因的,下面会有讲解。点击查看会跳到对应的掘金官网消息页面。
使用介绍
在vscode插件市场搜索juejin-message-helper,作者是dbfu321。也可以从这里安装,marketplace.visualstudio.com/items?itemN…
从掘金官网把cookie复制出来,设置到juejin-cookie上。
在官网中找到这个接口
复制下面的cookie
在vscode中设置刚复制cookie
下面还有一个参数可以设置刷新频率
需求分析
从掘金官网找到查文章数据的接口和消息接口
查询文章的数据接口
url:api.juejin.cn/content_api…
请求方式:post
请求参数(body):
{
"audit_status": 2, // 这个不知道有啥用,写死2就行了
"page_size": 5, // 一页多少条数据,这个最小是5,再小接口会报错,其实我只想查一条数据,被迫查五条,然后取第一条
"page_no": 1 // 从第几页开始查,最小1
}
响应参数: 我从data[0].article_info这个对象中找到了我需要的所有参数
display_count, // 展现数
view_count, // 阅读数
digg_count, // 赞的数量
comment_count, // 评论数
collect_count, // 收藏数
查询消息的接口
就是这里面的数据
url:api.juejin.cn/interact_ap…
请求方式:get
请求参数:无
响应参数:
掘金优化做的真好,数据的key都优化了。我猜测这样做是为了减少传输量,绝对不是为了省事。我一个一个试,终于试出来了每个数字代表的含义。
- 1表示赞和收藏
- 2表示粉丝
- 3表示评论
- 7表示私信
- 4表示系统通知
- total是未读消息总数
代码中设置两个定时器
一个定时器定时去掉文章数据接口,查最新文章数据。另外一个定时器定时去调用消息接口,第一个好做,请求完数据把数据使用StatusBarItem显示就行了,第二个有点麻烦,因为掘金的消息接口,如果你没处理,接口会一直有数据,这里肯定不能一直弹出提示,所以就需要在代码里记录一下上一次请求后消息的数据,然后和当前对比,如果数量是一样的,那么就不提示。
开发过程
安装全局命令yo
npm install yo -g
生成插件项目
在一个空文件夹中执行命令,生成插件项目模板。
yo code
执行完命令正常会出现如下图页面
就选第一个就行了,如果不想用ts,可以选第二个。选后后:
上面几个字段按自己的需求填就行了,是否需要使用webpack,这个大家注意一下,使用webpack的好处是打包的时候会把代码压缩混淆,包的体积会小一点,别人安装你依赖的时候快一点,但是缺点是打包比较慢,这个大家根据自己项目去决定吧,因为我这个项目比较小,我就选择不使用webpack。
安装依赖使用npm或yarn根据自己习惯选吧
项目文件创建后,可能会卡在装依赖的地方,我的建议是终止掉,然后在项目里用tyarn或tnpm安装会快很多,这两个用的是国内镜像。
进入刚创建的项目
cd juejin-message-helper
使用vscode打开项目
code .
使用tyarn安装依赖,没有安装tyarn命令的,可以全局安装一下。
npm i tyarn -g
运行项目
command+shift+p调出命令输入框,输入Hello,然后点击Hello World,下面会弹出一个消息,表示项目初始化完成,下面开始写代码。
核心功能实现
启动vscode就执行代码
因为vscode对插件有优化,没有执行命令前,activate事件是不会执行的,可以设置package.json文件中的activationEvents属性为onStartupFinished,这样src/extension.ts
文件里的activate方法就会自动执行了。
监听配置改变
// 监听配置时候改变,如果改变则重新new一个对象,重新监听
const disposable = vscode.workspace.onDidChangeConfiguration(event => {
if (
[
'juejin-cookie',
'juejin-refresh-time-span',
].some(str => event.affectsConfiguration(str))
) {
messageHelper.stopListen();
messageHelper = new MessageHelper();
messageHelper.startListen();
}
});
使用onDidChangeConfiguration监听配置变化,然后判断是否是cookie和juejin-refresh-time-span这两个和我相关的配置改变,如果有变化,就重新监听。
使用showErrorMessage来提示用户有新消息
上面提过,这里使用error类型的消息,因为正常info类型的消息,几秒钟之后会自动关闭,如果写代码很入神,很可能会错过消息,所以我不想让他关闭。查了很多资料都没办法让它不关掉,vscode issues也有人问,官方则说就是这样设计的,没办法只能用error类型的消息,这个如果用户不点,不会自动关闭,
核心代码
src/extension.ts
// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
import * as vscode from 'vscode';
import { MessageHelper } from './message-helper';
export async function activate(context: vscode.ExtensionContext) {
let messageHelper = new MessageHelper();
messageHelper.startListen();
// 监听配置时候改变,如果改变则重新new一个对象,重新监听
const disposable = vscode.workspace.onDidChangeConfiguration(event => {
if (
[
'juejin-cookie',
'juejin-refresh-time-span',
].some(str => event.affectsConfiguration(str))
) {
messageHelper.stopListen();
messageHelper = new MessageHelper();
messageHelper.startListen();
}
});
context.subscriptions.push(disposable);
}
// this method is called when your extension is deactivated
export function deactivate() {
}
src/message-helper.ts
import { StatusBarAlignment, Uri, env, window, workspace } from 'vscode';
import { Message, Service, juejinBaseUrl } from './service';
export class MessageHelper {
private statusBarItem = window.createStatusBarItem(StatusBarAlignment.Left, 10);
private timerStatusBarItem = window.createStatusBarItem(StatusBarAlignment.Left, 10);
private timeSpan: number;
private texts: string[] = [];
private count: number = 0;
private service: Service | null = null;
private hasShow = false;
private countTimer: NodeJS.Timeout | null = null;
private messageTimer: NodeJS.Timeout | null = null;
private messages: Message[] = [];
constructor() {
const cookie = workspace.getConfiguration().get('juejin-cookie') as string;
this.timeSpan = (workspace.getConfiguration().get('juejin-refresh-time-span') as number) * 1000;
if (!cookie) {
window.showErrorMessage('请先配置cookie!');
return;
}
this.service = new Service(cookie);
}
public startListen() {
this.refreshCountText();
this.countTimer = globalThis.setInterval(async () => {
if (this.count === 0) {
this.count = this.timeSpan;
this.refreshCountText();
} else {
this.count -= 1000;
}
this.timerStatusBarItem.text = `下次刷新:${this.count / 1000}`;
}, 1000);
this.listenMessage();
this.messageTimer = globalThis.setInterval(() => {
this.listenMessage();
}, 5000);
}
public stopListen() {
if (this.countTimer) {
globalThis.clearInterval(this.countTimer);
}
if (this.messageTimer) {
globalThis.clearInterval(this.messageTimer);
}
this.statusBarItem.dispose();
this.timerStatusBarItem.dispose();
}
private async listenMessage() {
try {
const messages = await this.service?.getMessages() || [];
this.messages.push(...messages);
if (!this.hasShow) {
this.showMessage();
}
} catch {
if (this.messageTimer) {
globalThis.clearInterval(this.messageTimer);
}
window.showWarningMessage('请检查cookie是否配置错误');
}
}
private async showMessage() {
const curMessage = this.messages.pop();
if (!curMessage) {
this.hasShow = false;
return;
}
const { message, url, count } = curMessage;
this.hasShow = true;
const result = await window.showErrorMessage(`掘金消息助手:您有${count}${message}`, ...['查看', '关闭']);
if (result === '查看') {
await env.openExternal(Uri.parse(`${juejinBaseUrl}${url}`));
}
this.hasShow = false;
this.showMessage();
}
private async refreshCountText() {
try {
this.texts = await this.service?.getLatestArticleCount() || [];
if (!this.texts?.length) {
this.statusBarItem.text = '接口调用失败,请稍后刷新重试。';
} else {
this.statusBarItem.text = this.texts.join('\t');
}
this.timerStatusBarItem.text = `下次刷新:${this.count / 1000}`;
this.statusBarItem.show();
this.timerStatusBarItem.show();
} catch {
if (this.countTimer) {
globalThis.clearInterval(this.countTimer);
}
window.showWarningMessage('请检查cookie是否配置错误');
}
}
}
src/service.ts
import axios, { AxiosInstance } from 'axios';
export const juejinBaseUrl = 'https://juejin.cn';
export interface Message {
message: string;
count: number;
url: string;
}
export class Service {
private request: AxiosInstance;
private countFailCount = 0;
private messageFailCount = 0;
lastCollentAndDiggCount: number = 0;
lastFanCount: number = 0;
lastCommentCount: number = 0;
lastNewsCount: number = 0;
lastNoticeCount: number = 0;
constructor(cookie: string) {
this.request = axios.create({
baseURL: juejinBaseUrl,
headers: {
cookie,
},
});
}
public async getLatestArticleCount(): Promise<string[]> {
const url = '/content_api/v1/article/list_by_user?aid=2608&uuid=7056220463659533837&spider=0';
try {
// eslint-disable-next-line @typescript-eslint/naming-convention
const { data: { data, err_no } } = await this.request.post(
url,
{
page_no: 1,
page_size: 5,
}
);
if (err_no !== 0) {
// 失败三次就不再刷新了
if (this.countFailCount === 3) {
throw new Error();
}
this.countFailCount += 1;
return await this.getLatestArticleCount();
}
// 取最新的文章数据
const latestArticle = data[0];
if (!latestArticle) {
return [];
}
const { article_info: detail } = latestArticle;
const {
display_count,
view_count,
digg_count,
comment_count,
collect_count,
} = detail;
const texts = [
`展现:${display_count}`,
`观看:${view_count}`,
`赞:${digg_count}`,
`评论:${comment_count}`,
`收藏:${collect_count}`,
];
this.countFailCount = 0;
return texts;
} catch {
// 失败三次就不再刷新了
if (this.countFailCount === 3) {
throw new Error();
}
this.countFailCount += 1;
return await this.getLatestArticleCount();
}
}
public async getMessages(): Promise<Message[] | undefined> {
const url = '/interact_api/v1/message/count?aid=2608&uuid=7056220463659533837&spider=0';
try {
const { data: { data, err_no } } = await this.request.get(url);
if (err_no !== 0) {
// 失败三次就不再刷新了
if (this.messageFailCount === 3) {
throw new Error();
}
this.messageFailCount += 1;
return await this.getMessages();
}
const { count } = data;
const messages = [];
let collentAndDiggCount = count["1"];
let fanCount = count["2"];
let commentCount = count["3"];
let noticeCount = count["4"];
let newsCount = count["7"];
if (collentAndDiggCount !== this.lastCollentAndDiggCount && collentAndDiggCount > 0) {
messages.push({
message: `个新的赞和收藏`,
url: '/notification/digg',
count: collentAndDiggCount,
});
}
if (fanCount !== this.lastFanCount && fanCount > 0) {
messages.push({
message: `个新的粉丝`,
url: '/notification/follow',
count: fanCount,
});
}
if (commentCount !== this.lastCommentCount && commentCount > 0) {
messages.push({
message: `条新的评论`,
url: '/notification',
count: commentCount,
});
}
if (newsCount !== this.lastFanCount && newsCount > 0) {
messages.push({
message: `条新的私信`,
url: '/notification/im',
count: newsCount,
});
}
if (noticeCount !== this.lastNewsCount && noticeCount > 0) {
messages.push({
message: `条新的系统通知`,
url: '/notification/system',
count: noticeCount,
});
}
this.lastCollentAndDiggCount = collentAndDiggCount;
this.lastFanCount = fanCount;
this.lastCommentCount = commentCount;
this.lastNewsCount = newsCount;
this.lastNoticeCount = noticeCount;
this.messageFailCount = 0;
return messages;
} catch {
// 失败三次就不再刷新了
if (this.messageFailCount === 3) {
throw new Error();
}
}
}
}
代码仓库地址:github.com/dbfu/juejin…
总结
后面可能还会加自己选择一个文章去监听,而不是写死的最新一条。
这里提示大家一下,我调用掘金的接口频率很低,对掘金服务器不会造成影响,大家在使用掘金api的时候一定要控制频率,别把服务器搞崩了。