自己写一个vscode插件

最近追剧,主角天天被热搜,吃瓜吃的不亦乐乎,想搞个实时吃瓜的插件。

1. 怎么入门?

直接问chatGPT我想搞一个展示微博热搜榜单词条的vscode插件怎么搞?

安装完后直接运行yo code会可能会遇到两个问题:

  • npm版本报错
js 复制代码
× An error occured while running code:app#prompting Error code items.findLastIndex is not a function

这个问题里面findLastIndex() 是 ES2022(ES13)引入的新方法,需要nodeJS升级V18+,切换nvm

js 复制代码
nvm use 19.8.0
  • 运行环境报错

这个是 Windows PowerShell 的脚本执行策略限制 导致的。Windows 默认 不允许执行 .ps1 脚本文件 (即 PowerShell 脚本),而 yo 命令正是通过 yo.ps1 启动的。

通过win键+R然后输入cmd打开命令行窗口,用非PowerShell的命令窗口运行。 根据自己需要回答问题,生成目录结果如下:

2. 写内容

chatGPT会直接给出extension.js和package.json的代码内容

extension.js

js 复制代码
const vscode = require('vscode');
const fetch = require('node-fetch');

/**
 * @param {vscode.ExtensionContext} context
 */
function activate(context) {
  let disposable = vscode.commands.registerCommand('weiboHotSearch.show', async function () {
    const panel = vscode.window.createWebviewPanel(
      'weiboHotSearch',
      '微博热搜榜',
      vscode.ViewColumn.One,
      {}
    );

    const hotList = await fetchWeiboHot('https://weibo.com/ajax/statuses/hot_band');
    const entList = await fetchWeiboHot('https://weibo.com/ajax/statuses/entertainment');

    panel.webview.html = getWebviewContent(hotList, entList);
  });

  context.subscriptions.push(disposable);
}

async function fetchWeiboHot(url) {
  try {
    const res = await fetch(url, {
      headers: {
        'User-Agent': 'Mozilla/5.0',
        'Referer': 'https://weibo.com/'
      }
    });
    const json = await res.json();
    return (json.data.band_list || []).slice(0, 5);
  } catch (e) {
    vscode.window.showErrorMessage('请求微博热搜失败: ' + e.message);
    return [];
  }
}

function getWebviewContent(hotList, entList) {
  function listToHTML(list) {
    return list.map((item, i) => {
      const url = item.word_scheme || `https://s.weibo.com/weibo?q=${encodeURIComponent(item.word)}`;
      return `<li><a href="${url}" target="_blank">${i + 1}. ${item.word}</a> (${item.category || ''})</li>`;
    }).join('');
  }

  return `
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="utf-8">
      <style>
        body { font-family: sans-serif; padding: 1em; }
        h2 { color: #e91e63; }
        ul { list-style: none; padding: 0; }
        li { margin-bottom: 6px; }
        a { text-decoration: none; color: #2196f3; }
      </style>
    </head>
    <body>
      <h2>🔥 微博热搜榜</h2>
      <ul>${listToHTML(hotList)}</ul>
      <h2>🎬 文娱榜</h2>
      <ul>${listToHTML(entList)}</ul>
    </body>
    </html>`;
}

function deactivate() {}

module.exports = {
  activate,
  deactivate
};

其实chatGPT给出的文娱榜的接口不对,我自己去控制台拿的

package.json

js 复制代码
// 在上面代码的基础上改了这个
  "contributes": {
    "commands": [
      {
        "command": "weiboHotSearch.show",
        "title": "📈 显示微博热搜榜"
      }
    ]
## },

如果想查看效果

F5然后在新打开的窗口里面Ctrl+Shift+P,点击显示微博热搜榜,展示结果如下:

从图片可以看到,文娱榜数据未加载出来,报错如下:

这是因为微博反爬机制被触发

3. 方案升级

把上面的错误反馈给chatGPT, 然后他会告诉你可以使用puppeteer+cheerio

结合上面的cheerioextension.js最终代码形式如下:

js 复制代码
const vscode = require('vscode');
const puppeteer = require('puppeteer');
const cheerio = require('cheerio');

/**
 * @param {vscode.ExtensionContext} context
 */
function activate(context) {
  let disposable = vscode.commands.registerCommand('weiboHotSearch.show', async function () {
    const panel = vscode.window.createWebviewPanel(
      'weiboHotSearch',
      '微博热搜榜',
      vscode.ViewColumn.One,
      {
        enableScripts: true, // 允许点击跳转
      }
    );

    // 设置初始页面(加载中)
    panel.webview.html = `
      <html><body>
        <h2>正在加载微博热搜...</h2>
      </body></html>
    `;

    try {
      const hotList = await fetchWeiboHot('https://s.weibo.com/top/summary?cate=realtime');
      const entList = await fetchWeiboHot('https://s.weibo.com/top/summary?cate=entrank');

      panel.webview.html = getWebviewContent(hotList, entList);
    } catch (e) {
      panel.webview.html = `<html><body><h2>加载失败:</h2><pre>${e.message}</pre></body></html>`;
    }
  });

  context.subscriptions.push(disposable);
}

async function fetchWeiboHot(url) {
  let browser;
  try {
    browser = await puppeteer.launch({
      headless: true, // 不显示浏览器窗口
      args: ['--no-sandbox', '--disable-setuid-sandbox'],
    });

    const page = await browser.newPage();
    await page.setUserAgent(
      'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/102.0.0.0 Safari/537.36'
    );

    await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 });

    const html = await page.content();
    const $ = cheerio.load(html);
    const list = [];

    $('table tbody tr').each((i, el) => {
      const rank = $(el).find('td.td-01').text().trim();
      const a = $(el).find('td.td-02 a');
      const title = a.text().trim();
      const href = a.attr('href');
      const hot = $(el).find('td.td-03').text().trim();
      const link = href ? (href.startsWith('http') ? href : 'https://s.weibo.com' + href) : '';

      if (title) {
        list.push({ rank: rank || (i + 1).toString(), title, hot, link });
      }
    });

    return list;
  } catch (err) {
    console.error('获取微博热搜失败(puppeteer):', err.message);
    return [];
  } finally {
    if (browser) await browser.close();
  }
}
function getWebviewContent(hotList, entList) {
  function renderSection(title, list) {
    if (!list.length) return `<p>暂无数据</p>`;
    const rows = list
      .map(
        (item, index) => `
        <tr>
          <td class="rank">${item.rank || index + 1}</td>
          <td class="title">
            <a href="${item.link}" target="_blank">${item.title}</a>
            ${item.hot ? `<span class="hot">${item.hot}</span>` : ''}
          </td>
        </tr>`
      )
      .join('\n');

    return `
      <h2>${title}</h2>
      <table>
        <tbody>${rows}</tbody>
      </table>
    `;
  }

  return `
  <!DOCTYPE html>
  <html lang="zh">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <style>
      body {
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu;
        padding: 20px;
        color: #333;
      }
      h2 {
        font-size: 1.2rem;
        border-left: 4px solid #fa7d3c;
        padding-left: 10px;
        margin-top: 2rem;
      }
      table {
        width: 100%;
        border-collapse: collapse;
        margin-top: 0.5rem;
      }
      td {
        padding: 8px 10px;
        border-bottom: 1px solid #eee;
      }
      td.rank {
        width: 2rem;
        color: #fa7d3c;
        font-weight: bold;
      }
      td.title a {
        text-decoration: none;
        color: #0366d6;
      }
      td.title .hot {
        color: #999;
        font-size: 0.85rem;
        margin-left: 10px;
      }
    </style>
    <title>微博热搜榜</title>
  </head>
  <body>
    <h1>微博热搜速览</h1>
    ${renderSection('🔥 热搜榜', hotList)}
    ${renderSection('🎬 文娱榜', entList)}
  </body>
  </html>
  `;
}

function deactivate() {}

module.exports = {
  activate,
  deactivate,
};

效果如下:

4. UI优化

  • ✅ 使用 Tab 标签 在"🔥 热搜榜"与"🎬 文娱榜"之间切换查看

  • ✅ 每个 Tab 内部有各自的 独立刷新按钮

  • ✅ 视觉保持"摸鱼伪装"风格:像代码注释,暗色低调

js 复制代码
function getWebviewContent(hotList, entList) {
  function renderContent(id, list) {
    const lines = list.map((item, index) => {
      const safeTitle = item.title.replace(/"/g, '\\"');
      return ` * ${(item.rank || index + 1).toString().padStart(2, '0')}. ${safeTitle}`;
    });
    return [`/**`, ...lines, ` */`].join('\n');
  }

  const hotText = renderContent('hot', hotList);
  const entText = renderContent('ent', entList);

  return `
  <!DOCTYPE html>
  <html lang="zh">
  <head>
    <meta charset="UTF-8" />
    <style>
      body {
        background-color: #1e1e1e;
        color: #d4d4d4;
        font-family: Consolas, Monaco, 'Courier New', monospace;
        font-size: 13px;
        padding: 0;
        margin: 0;
      }

      .tabs {
        display: flex;
        background-color: #2d2d2d;
        border-bottom: 1px solid #444;
      }

      .tab {
        flex: 1;
        text-align: center;
        padding: 10px;
        cursor: pointer;
        font-weight: bold;
        color: #ccc;
        border-right: 1px solid #444;
      }

      .tab.active {
        background-color: #1e1e1e;
        color: #fff;
      }

      .refresh-bar {
        display: flex;
        justify-content: space-between;
        align-items: center;
        background-color: #2d2d2d;
        padding: 6px 12px;
        border-top: 1px solid #333;
        border-bottom: 1px solid #333;
      }

      .refresh-bar .title {
        font-weight: bold;
      }

      .refresh-bar button {
        background: none;
        border: none;
        color: #4FC1FF;
        cursor: pointer;
        font-weight: bold;
      }

      .refresh-bar button:hover {
        text-decoration: underline;
      }

      .content {
        padding: 12px 20px;
        white-space: pre-wrap;
        display: none;
      }

      .content.active {
        display: block;
      }
    </style>
  </head>
  <body>
    <div class="tabs">
      <div class="tab active" id="tab-hot">🔥 热搜榜</div>
      <div class="tab" id="tab-ent">🎬 文娱榜</div>
    </div>

    <div class="refresh-bar" id="refresh-hot">
      <span class="title">🔥 微博热搜榜</span>
      <button onclick="refresh('hot')">🔄 刷新</button>
    </div>
    <pre class="content active" id="content-hot">${hotText}</pre>

    <div class="refresh-bar" id="refresh-ent" style="display: none;">
      <span class="title">🎬 文娱热搜榜</span>
      <button onclick="refresh('ent')">🔄 刷新</button>
    </div>
    <pre class="content" id="content-ent">${entText}</pre>

    <script>
      const vscode = acquireVsCodeApi();

      const tabHot = document.getElementById('tab-hot');
      const tabEnt = document.getElementById('tab-ent');
      const contentHot = document.getElementById('content-hot');
      const contentEnt = document.getElementById('content-ent');
      const refreshHot = document.getElementById('refresh-hot');
      const refreshEnt = document.getElementById('refresh-ent');

      tabHot.onclick = () => {
        tabHot.classList.add('active');
        tabEnt.classList.remove('active');
        contentHot.classList.add('active');
        contentEnt.classList.remove('active');
        refreshHot.style.display = 'flex';
        refreshEnt.style.display = 'none';
      };

      tabEnt.onclick = () => {
        tabEnt.classList.add('active');
        tabHot.classList.remove('active');
        contentEnt.classList.add('active');
        contentHot.classList.remove('active');
        refreshEnt.style.display = 'flex';
        refreshHot.style.display = 'none';
      };

      function refresh(type) {
        vscode.postMessage({ command: 'refresh', type });
      }

      window.addEventListener('message', event => {
        const msg = event.data;
        if (msg.command === 'update') {
          const pre = document.getElementById('content-' + msg.type);
          pre.textContent = msg.content;
        }
      });
    </script>
  </body>
  </html>
  `;
}

刷新监听

js 复制代码
panel.webview.onDidReceiveMessage(async msg => {
  if (msg.command === 'refresh') {
    const url = msg.type === 'hot'
      ? 'https://s.weibo.com/top/summary?cate=realtime'
      : 'https://s.weibo.com/top/summary?cate=entertainment';

    const list = await fetchWeiboHot(url);
    const lines = list.map((item, index) => {
      const safeTitle = item.title.replace(/"/g, '\\"');
      return ` * ${(item.rank || index + 1).toString().padStart(2, '0')}. ${safeTitle}`;
    });
    const content = [`/**`, ...lines, ` */`].join('\n');

    panel.webview.postMessage({ command: 'update', type: msg.type, content });
  }
});

效果图如下:

5. 本地部署

如果只是本地运行,F5后在新窗口Ctrl+Shift+P找显示微博热搜榜就行了

js 复制代码
// 我的node版本19+,否则报错
npm install -g vsce 

// 在项目根目录运行
vsce package 

会生成 hotsearch-0.0.1.vsix

然后运行安装命令

js 复制代码
code --install-extension ./-hostsearch-0.0.1.vsix

然后在已安装插件库里面就可以看到了。

但是我平时已经习惯用Cursor了,这个代码在Cursor里面跑不起来。想先发布到VsCode的插件库,再同步到Cursor使用。

6. 发布插件

  1. 创建组织(链接
  2. 创建项目(链接
  3. 获取个人访问令牌(PAT)

找到MarketPlace

  1. 创建publisher

访问页面:marketplace.visualstudio.com/publishers/... 注意这个your-name就是上面创建的your name

  1. 发布
  • 第一种: 命令发布
js 复制代码
vsce login xxx
vsce publish
  • 第二种: 后台发布

将上面生成的vsix文件上传,然后等待version字段变绿就行了

就可以在Vscode的插件库里面搜索到了

6. 升级

1. 修改版本号-package.json

js 复制代码
"version": "0.0.2"

📌 提示:每次发布版本都必须改 version,不然无法发布。 你可以根据改动的大小选择版本号的更新方式:

0.0.1 → 0.0.2 小修复

0.1.0 添加功能

1.0.0 稳定版本

2. (可选)更新 README.md / changelog

  • README.md 写明更新内容。
  • 如果用了 CHANGELOG.md,也建议记录更新历史。

3. 重新打包插件

使用 vsce 打包:

go 复制代码
bash
复制编辑
vsce package

这会生成新的 .vsix 文件,例如:my-extension-0.0.2.vsix


4. 发布新版本到 VS Code 插件市场

如果你用 vsce 发布插件到 marketplace:

复制代码
bash
复制编辑
vsce publish

如果你没有设置 Personal Access Token,它会提示你配置。

或者指定版本号(比如不想手动改 package.json):

复制代码
bash
复制编辑
vsce publish 0.0.2

⛳ 补充:升级发布所需前提

  • ✅ 你要有 Microsoft 的 Visual Studio Marketplace 帐号。

  • ✅ 插件要已注册(拥有唯一 ID,比如:publisher.my-extension)。

  • ✅ 安装了 vsce

    复制代码
    bash
    复制编辑
    npm install -g vsce
相关推荐
司宸8 分钟前
学习笔记八 —— 虚拟DOM diff算法 fiber原理
前端
阳树阳树8 分钟前
JSON.parse 与 JSON.stringify 可能引发的问题
前端
让辣条自由翱翔13 分钟前
总结一下Vue的组件通信
前端
dyb14 分钟前
开箱即用的Next.js SSR企业级开发模板
前端·react.js·next.js
前端的日常15 分钟前
Vite 如何处理静态资源?
前端
前端的日常15 分钟前
如何在 Vite 中配置路由?
前端
兮漫天16 分钟前
bun + vite7 的结合,孕育的 Robot Admin 靓仔出道(一)
前端
PineappleCoder17 分钟前
JS 作用域链拆解:变量查找的 “俄罗斯套娃” 规则
前端·javascript·面试
兮漫天17 分钟前
bun + vite7 的结合,孕育的 Robot Admin 靓仔出道(二)
前端
用户479492835691522 分钟前
面试官:为什么很多格式化工具都会在行尾额外空出一行
前端