Keep calm & thinking

使用 vitepress、vscode 重构博客

e82e2125ec5e57da161b9b32099ef9b06ff7d50b

前言

上一次折腾博客还是 18 年,也就是这个博客系统的第一篇文章发表的时期,那时候是跟着 vue ssr 的教程结合 nodejs 一步步搭建,包括阿里云机器、域名证书、SEO等。时隔 3 年,前端技术发展日新月异,模式层出不穷。本着折腾第一要义,还是想再快速的对博客改版一次。

博客,无外乎图文或者附件,静态站点完全满足,但是,一台云服务器的可玩性是静态站点不能比的。

技术原理

前置条件是,为了尽可能对 seo 友好,那么站点路径不能变,同时还需要保留原有的服务。技术栈为

  • 前台展示:vitepress,自定义主题,使用 github actions 从动态数据构建为静态站点。
  • 数据库:依旧使用原有 leancloud 服务,leancloud 对前端而言是和 Sequelize 操作 mysql 般,类 orm 的强大 js 库,个人用户的免费版足够使用。当然、如果你想自己写 nodejs 服务,也是可以使用 eggjs 轻松实现增删改查。
  • 管理文章:vscode extensions 通过定义视图及操作,来实现文章的增删改查。对于编辑,简单的采用 webview 实现,而不是自定义 markdown 编辑器。

效果

前台展示,访问 https://iming.work,管理端展示如下图:

4fb93f83fdf6dab4fa3e32370aea5dffce3c8562

包含了很多操作,其中的 icon 使用的 iconPark 调整下载,一般包含两套 dark、light,通过 iconPark 可以很方便获取。

技术细节

1. vitepress 自定义主题

在 vitepress 的文档中看到自定义主题扩展,不看源码是完全不知道从何处下手。

interface Theme {
  Layout: Component // Vue 3 component
  NotFound?: Component
  enhanceApp?: (ctx: EnhanceAppContext) => void
}

所以思路是,直接拷贝 vitepress/src/client/theme-default 默认主题进行魔改。

2. 连接 leancloud 数据库

使用 leancloud 之前需要为 api 配置域名 CNAME

const AV = require('leancloud-storage')

const appId = '***'
const appKey = '***'
const serverURLs = 'https://api.iming.work'
AV.init({ appId, appKey, serverURLs })

譬如查询列表

new AV.Query('Article').find()

另外对于接口鉴权,列表和详情开放给所有人可读;编辑和删除需要鉴权

if (!AV.User.current()) {
   AV.User.login(xxx, xxx)
}

我们可以写个脚本,将内容获取后写入 vitepress,再构建为静态站点。同时为了避免每次都全量写入,可以加入更新时间,用来和远程文章的更新时间做比对,去除不必要的等待时间

c97e3e0c9c2c151bf57988e5a98694e439394188

这样就只会写入更新的那篇文章了,通过 github action 的 build 日志查看

4dd12c6673d83b0876a3c5f9045d2654435bcd1b

3. https 下的图床服务

对于博客而言,图片上传也是重要的一块内容,如果不使用 https,leancloud 支持 File 对象存储,完全可以满足:

new db.AV.File(data.name, data).save({
  onprogress(e) {
    data.cb(e.percent)
  }
})

在很早之前,Chrome 就对混合资源做了限制,https 域名下是不允许加载 http 资源的,所以这时候 leancloud 无法满足了。

也很容易考虑到自己处理图片上传到服务器,前提是服务器带宽够哈,我这种 1M 带宽的当然老老实实选择其他地方放图片,另外,如果图片放服务器的话,迁移的时候也挺麻烦。

网上的免费图床是肯定不靠谱的,所以选择 github,在我们给某个 repo 提 issues 时,内容输入框是可以粘贴图片的,通过 personal token 可以直接调用;还一种办法是新建一个专门放图片的 repo,每次 push 新的图片也可行:

const data = {
  method: 'PUT',
  url: `https://api.github.com/repos/${repo}/contents/${encodeURI(dir)}/${encodeURI(fileName)}`,
  headers: {
    Accept: 'application/vnd.github.v3+json',
    Authorization: `token ${token}`,
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_0_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36'
  },
  body: {
    message: commit,
    branch: 'main',
    content: base64,
    path: `${dir}/${fileName}`
    // sha //: '6afc205f1e63c8ecbe12cc5db884f104df110766'
  },
  json: true
}

4. vscode 插件

在此之前还没有接触过这块内容,网上搜索了下,查阅了 2 篇有帮助的中文文档,有助于从零开始上手

关于使用 npx 执行多包命令

npx -p yo -p generator-code -c 'yo code'

一句搞定,而不用先安装再执行。

插件的入口、函数名、icon 定义都是在 package.json 中完成,定义完成后 fn+f5 开启调试即可看到效果。

73697015f71197dd5159a9a1e3d6f0008bcc1962

在 iconPark 上的图标要注意粗细和 vscode 原生保持一致,否则会不协调,线条粗细调为 3 即可,填充颜色改为白色。

对于 TreeItem 的点击事件定义,是在 class 中声明,而不是在 package.json 中

70c98fd5cbe00bdfff42985bad43842ff2d5c85e

内容编辑的实现,是通过 webview 加载远程网页后,通过 postMessage 完成交互。

function getEditorHtml (): Promise<string> {
  return new Promise((resolve, reject) => {
    // 服务器文件
    const url = 'https://iming.work/demo/vitepress-blog-vs-inner-editor/dist/index.html';
    // 本地调试地址
    // const url = 'http://localhost:8080';
    urllib.request(url, { dataType: 'text' }, (err, body) => {
    	if (err) {
    		reject(err);
    	} else {
    		resolve(body);
    	}
      }); 	
  });
}

需要注意的是,读取远程 html 内容设置为 webview.html 内容,资源依赖都需要写死远程地址,也就是 publicPath 需要明确指明,否则加载资源会通过 vscode-resource: 协议加载本地。

图片上传,监听 paste 事件,将剪切板的内容读取为 base64,在通过 github api put 到仓库得到 raw 的访问地址。

paste (e) {
  const pasteItem = e.clipboardData.items[0]
  if (pasteItem && /image/.test(pasteItem.type)) {
    e.preventDefault()
    const blob = pasteItem.getAsFile()
    const rdr = new FileReader()
    rdr.onloadend = () => {
      this.upload(this.title, rdr.result)
    }
    rdr.readAsDataURL(blob)
  } else {
    pasteItem.getAsString(content => {
      console.log(content)
      this.$message.info('复制的是字符串')
    })
  }
}

粘贴时,也能做个加载效果如下图

7dac1fe7a5bbbaad3d1c7e7fa0992476ec34e388

为了便于扩展,我们可以将博客的需要鉴权的用户名和密码设置为插件配置项

ff443d39bad719e56dad6324d325506b78cbbf31

5. 编辑器状态保存

通过 webview 加载的网页内容在编辑完后,当前 tab 失去焦点或被隐藏,再切换回来时,内容是会重新加载的。有必要保存编辑器的状态,在每次获取焦点时读取之前的状态

const previousState = window.vscode.getState()
if (previousState) {
  this.setData(previousState)
}

6. 自动化

第一步,vitepress 静态站点结合 github actions 自动打包部署到腾讯云,通过定义 .github/workflows/main.yml 文件来自动触发,在 steps 中,我们可以 use 别人写好的内容,也可以自定义多个命令:

# 使用别人写好的,传参即可
- name: use Node.js 16
  uses: actions/setup-node@v1
  with:
    node-version: 16

# 自己定义多行命令操作
- name: Run build
  run: |
    npm --version
    npm run "docs:build"
    echo test, and deploy your project
  env:
    CI: true

关于 ssh,我们不希望密钥明文存储,可以定义在 github secrets 中,然后通过变量读取

- name: ssh deploy
  uses: easingthemes/ssh-deploy@v2.2.11
  with:
    # Private Key
    SSH_PRIVATE_KEY: ${{ secrets.TOKEN }}
    # Remote host
    REMOTE_HOST: 1.117.74.21

第二步,vscode 中按钮触发,相当于是手动触发 action,需要单独定义事件

on:
  repository_dispatch:
    types:
      - webhook-1

再通过 rest api 远程调用

urllib.request('https://api.github.com/repos/jmingzi/vitepress-blog/dispatches', {
  method: 'POST',
  headers: {
    Accept: 'application/vnd.github.everest-preview+json',
    Authorization: 'token personal_token'
  },
  data: JSON.stringify({
    event_type: 'webhook-1'
  }),
  // contentType: 'application/json',
  dataType: 'json'
}, (err, body) => {
  if (err) {
    vscode.window.showErrorMessage(err.message);
  } else {
    vscode.window.showInformationMessage('触发 github actions 成功');
  }
});

终端触发

curl -X POST https://api.github.com/repos/jmingzi/vitepress-blog/dispatches \
    -H "Accept: application/vnd.github.everest-preview+json" \
    -H "Authorization: token  personal_token" \
    --data '{"event_type": "webhook-1"}'

7. 腾讯云 github clone 慢

使用 cnpm 镜像,将域名换为 github.com.cnpmjs.org ,举个栗子:

git clone https://github.com/mikecao/umami.git

替换为

# 1
git clone https://github.com.cnpmjs.org/mikecao/umami.git
# 2
git clone https://gitclone.com/github.com/tendermint/tendermint.git

8. 插件的打包发布

由于插件内写死了 github personal token 以及 leancloud 个人账户的 appId 和 appSecret ,所以目前是不能提交到仓库的,即使提交后,github 也会自动过滤掉。

可以先将插件代码打包为 vsix 文件,再使用 vsix 安装器选中安装插件即可。

8841fafbc209997d8650fddd57d9746b72b1180f

插件打包依赖 vsce,需要提前安装

"publish": "vsce package"

bd84572f5012d91d6298196f76303525cebf6402

总结

vscode 插件可玩性很高,但是一番折腾下来,实际体验不是很完美。以后的写作,应该还是会在 notion 上完成,这一插件可以作为维护的入口。造轮子从来都不是为了好用,仅仅是为了体验造轮子的过程。编写这些插件可以让自己的想法得以实现,这一过程比打几把王者荣耀更让人持续愉悦。

规划

  • 可以再列表标题位置透出当前版本号 cd99d2e5b4cdfd52a4fd7829a8a360ef1846aa95
  • 编辑状态的预览操作,现在用来写长文还是很盲的,眼睛有点累
  • 支持快捷键:ctrl+s 保存,ctrl+x 剪切当前行
  • 剔除插件的 appId/appSecret/personal token 等,再发布至公共插件市场

上次更新: