拖了一个月,也终于是想起来记录一下了
除了标题中提到的博客迁移之外,顺带说一说评论系统的比对选择,安全加固
另外还会提到 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 不就好了么(只要不浏览一些奇奇怪怪的网站
于是乎腾讯云给的大额优惠券也就让它自己过期好了