拖了一个月,也终于是想起来记录一下了
除了标题中提到的博客迁移之外,顺带说一说评论系统的比对选择,安全加固
另外还会提到 Vercel, Cloudflare Pages 等的对比使用感受
 背景
由于不想继续负担高昂的香港云服务器费用,于是需要将我原来一堆线上的东西部署到别的地方,其中比较重要的就是博客了。宗旨是能白嫖尽量不花钱,另外也需要尽量保证线上服务的境内可用性
 方案选择
市面上主流的方案:Github Pages, Vercel, Cloudflare Pages
先说 Github Pages,在我 student pack 到期之前,我就用的是私有仓库 + Github Pages + Cloudflare CDN 的方案,后来到期了无法白嫖,才转战的云服务器。另外由于 Cloudflare CDN 免费版的不稳定性,其实并不能很好的保证大陆地区顺畅访问,而我又不想将私有仓库转为公开,故直接放弃 Github Pages 方案
Vercel 的话实测下来访问速度还是很不错的,还有 Serverless Function 可以用,于是博客迁移就选择了 Vercel
Cloudflare Pages 相对于 Vercel 文档资源都会少一些,摸索起来会比较麻烦,不过我的另一个服务也尝试用了它,并且在 Github 上开源了(主要还是存个档便于以后有需求直接当参考,项目本身没啥意义)
 Typecho 迁移到 Hexo
主要是迁移文章和评论,最后评论选择 Valine(主要是这个主题只支持 Valine),网上能找的备份迁移工具效果不如人意,就自己弄了一个迁移一下数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
   | import md5 from 'md5' import moment from 'moment' import AV from 'leancloud-storage' import { promises } from 'fs' import { promisify } from 'util' import { createConnection } from 'mysql'
  const dsn = 'mysql://root:pass@localhost/typecho?charset=utf8mb4' AV.init({   appId: '',   appKey: '',   serverURL: 'https://api.lncldglobal.com' })
  const conn = createConnection(dsn) const query = promisify(conn.query.bind(conn))
  const metas = await query('select * from metas') const contents = await query('select * from contents') const comments = await query('select * from comments') const relationships = await query('select * from relationships')
  await promises.mkdir('./backup/raw', { recursive: true }) await promises.writeFile('./backup/raw/metas.json', JSON.stringify(metas, null, 2)) await promises.writeFile('./backup/raw/contents.json', JSON.stringify(contents, null, 2)) await promises.writeFile('./backup/raw/comments.json', JSON.stringify(comments, null, 2)) await promises.writeFile('./backup/raw/relationships.json', JSON.stringify(relationships, null, 2))
  const tags = new Map const cats = new Map metas.forEach(m => {   if (m.type === 'tag')     tags.set(m.mid, m)   else     cats.set(m.mid, m) })
  const paths = new Map const Counter = AV.Object.extend('Counter') for (const c of contents) {   await promises.mkdir(`./backup/${c.type}s`, { recursive: true })   const rs = relationships.filter(r => c.cid === r.cid)   promises.writeFile(`./backup/${c.type}s/${c.slug}.md`, `--- title: ${c.title} date: ${moment(1000 * c.created).format('YYYY-MM-DD HH:mm:ss')} updated: ${moment(1000 * c.modified).format('YYYY-MM-DD HH:mm:ss')} categories: [${rs.filter(r => cats.has(r.mid)).map(r => cats.get(r.mid).name).join(', ')}] tags: [${rs.filter(r => tags.has(r.mid)).map(r => tags.get(r.mid).name).join(', ')}] ---
  ${c.text.replace('<!--markdown-->', '').replace(/\r\n/g, '\n')}`)
    const path = c.type === 'page' ?     `${c.slug}.html` :     `${moment(1000 * c.created).format('YYYY/MM/DD')}/${c.slug}.html`   paths.set(c.cid, path)   console.log('insert views', path, c.views)   const o = new Counter()   o.set('time', c.views)   o.set('title', c.title)   o.set('url', path)   o.set('xid', path)   await o.save() }
  const coidObj = new Map const Comment = AV.Object.extend('Comment') for (const c of comments) {   console.log('insert comment', c.author, c.text)   const o = new Comment()   o.set('comment', c.text.replace(/\r/, ''))   o.set('date', new Date(1000 * c.created))   c.ip && o.set('ip', c.ip)   o.set('link', c.url ?? '')   o.set('mail', c.mail)   o.set('mailMd5', md5(c.mail))   o.set('nick', c.author)   if (c.parent) {     const p = coidObj.get(c.parent)     o.set('pid', p.getObjectId())     o.set('rid', p.get('rid') || p.getObjectId())   }   c.agent && o.set('ua', c.agent)   o.set('url', paths.get(c.cid))   coidObj.set(c.coid, await o.save()) }
  process.exit(0)
   | 
 Valine 的安全加固
由于众所周知的 安全问题,直接使用肯定是问题比较大的,加之不太想去改主题的代码来换用评论系统,于是就借助 Vercel 来实现一个 api 的代理层,实际的 secret 这些并不写在前端,并且对一些写逻辑做更细粒度的权限管控
方法也很简单,首先将 Valine 的 serverURLs 配置改成 /api/, 这样所有评论和阅读统计的请求都会走到同域名 /api/*, 然后代理要用到的接口就好了,这里需要在 vercel.json 里预先加一条路由重写规则
1 2 3 4 5 6
   | {   "rewrites": [{     "source": "/api/1.1/classes/Counter/:id",     "destination": "/api/1.1/classes/Counter"   }] }
  | 
然后在 Vercel 的后台配置好 LEAN_API, LEAN_ID, LEAN_KEY 这些的环境变量
最后分别实现评论和阅读接口即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
   | import axios from 'axios' import { VercelRequest } from '@vercel/node'
  axios.defaults.validateStatus = s => s < 500 axios.defaults.baseURL = process.env.LEAN_API axios.defaults.headers.common['X-LC-Id'] = process.env.LEAN_ID ?? '' axios.defaults.headers.common['X-LC-Key'] = process.env.LEAN_KEY ?? ''
  export async function proxy(req: VercelRequest) {   const [path] = req.url?.split('?') ?? ['']   return (await axios({     method: req.method,     url: path.replace(/^\/api/, ''),     params: req.query,     data: req.body   })).data }
   | 
1 2 3 4 5 6 7 8 9 10 11 12 13 14
   | import type { VercelResponse } from '@vercel/node'
  export function desensitize(res: VercelResponse, data: Record<string, unknown>) {   const { results } = data   Array.isArray(results) && results.forEach(r => {     delete r.ip     delete r.mail     if (r.date?.iso) {       r.createdAt = r.date.iso       delete r.date     }   })   res.json(data) }
  | 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
   | import { proxy } from './_proxy' import { desensitize } from './_util' import type { VercelRequest, VercelResponse } from '@vercel/node'
  export default async function(req: VercelRequest, res: VercelResponse) {   if (req.method !== 'GET') {     res.json({ code: -405 })   } else if (typeof req.query.cql !== 'string') {     res.json({ code: -400 })   } else if (!/^select /i.test(req.query.cql)) {     res.json({ code: -403 })   } else {          req.query.cql = req.query.cql.replace(/^select /, 'select date, ')     desensitize(res, await proxy(req))   } }
  | 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
   | import { proxy } from '../_proxy' import type { VercelRequest, VercelResponse } from '@vercel/node'
  export default async function(req: VercelRequest, res: VercelResponse) {         if (req.method === 'GET') {     req.query.count = '1'     req.query.limit = '0'   } else if (req.method === 'POST') {     req.body.ip = req.headers['x-real-ip']     req.body.ua = req.headers['user-agent']   } else {     res.json({ code: -405 })     return   }   res.json(await proxy(req)) }
  | 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
   | import { proxy } from '../_proxy' import type { VercelRequest, VercelResponse } from '@vercel/node'
  export default async function(req: VercelRequest, res: VercelResponse) {            if (req.method === 'POST') {     req.body.time = 1   } else if (req.method === 'PUT') {     req.body = { time: { __op: 'Increment', amount: 1 } }   } else if (req.method !== 'GET') {     res.json({ code: -405 })     return   }   res.json(await proxy(req)) }
  | 
换用这个主题之前,还尝试过其他的主题,不过也是中途放弃掉了
确实也算是比较折腾了,如果以后有空了自己搞主题的话,感觉 Twikoo 就挺不错的
 Vercel 与 Cloudflare Pages 的对比
网上都说 Vercel 比较快,不过就我本地的网络测试来看,其实不管是选择哪个,速度都挺快的。不过 Vercel 免费版的运行时限,资源上限这些似乎都相对要多一些,所以做的事情自然也更多一些。Cloudflare Pages 的优势是免费版自带 kv 存储,虽然容量不大,但对我的需求而言是够用的,省去了自己折腾 leancloud 或者 dynamodb 这种东西
两者都提供了本地调试工具,不过用起来各有各的坑。比如 Vercel 我一直想重定向非 /api 开头请求到 /api 未果,遂放弃;再比如 Cloudflare Pages 我想直接在本地读取云端的 kv 数据未果,只能在本地 mock 一个假的
 最后
其实当初用云服务器最重要的理由是自由上网,线上服务这些都是附属的东西,而且也很容易找到一些免费的替代品
后来仔细想想,其实只要有个正经工作,用公司给的 VPN 不就好了么(只要不浏览一些奇奇怪怪的网站
于是乎腾讯云给的大额优惠券也就让它自己过期好了