hiccLoghicc log by wccHipo日志

Next.js + 腾讯云开发Webify 打造绝佳网站

toc

Intro

Next.js酷在哪里?

之前使用Next.js + strapi做了一个简单博客站点也顺道写了一篇Next.js 简明教程,之后Next本身一直在迅猛发展。利用现代js能力来说做到了:

  • 极佳的开发体验
  • 极佳的网站最佳的”动“,“静”平衡

从特性上来说,支持:

  • SSR(Server Side Rendering)
    • 提供getServerSideProps方法,在用户访问时请求数据,适用于实时数据页面。
  • SSG(Static Site Generation)
    • 提供getStaticPropsgetStaticPaths方法来预先生产静态页面
    • 而更酷的一点是:使用fallback,revalidate来支持一定的动态性

这种能“动”的SSG自然是我所需要的,保持静态访问,而又能在我新增修改文章的时候,站点能够自动更新。绝佳!!

为什么还需要Webify来折腾一番?

既然上面已经很酷了,为什么会有今天的文章,为什么还需要折腾一番?

原因也很简单:成本略高,为了不错的访问速度,你需要一台性能不错的虚拟机,一定的带宽。对于一般个人博客,投入不划算。

在这里就隆重地有请我们的解决方案:腾讯云开发Webify,简单来说就是类似vercel的serverless 服务托管服务,不过支持更多的框架,而且是国内服务商,便宜访问速度一流

有图为证: 腾讯云官方数据

hicc.pro部署在webify上

tcb Webify使用

对于一般文章使用类似github管理的就简单了,tcb Webify支持版本github,gitlab,gitee服务商,适配了:

  • Vue.js (vue-cli)
  • React.js (create-react-app)
  • Hexo
  • Gatsby.js
  • Angular
  • Next.js SSG
  • Nuxt.js SSG
  • 以及自动适配框架

以本博客next为例,Webify实际上使用时了next export的能力,构建后,直接部署静态文件到server。

如果你的博客文章,直接使用md,git管理,看到这里就OK了,git 提交,Webify自动会重新部署你的站点。cool~~

问题是如果你的站点数据来源于类似strapi这种serverless cms怎么办?next export 不支持next SSG中“动”的特性(fallbackrevalidate)。

Webify高阶——自动化Webify

其实方法也很简单,加一个桥接服务,让你的serverless cms的更新变动到git就好。

具体以strapi为例子

  1. strapi 数据发布
  2. web hook到自定义的桥接服务。
  3. 桥接服务更新站点git。
  4. Weify触发重新部署。

当然如果后续webify支持更多的重新部署方式,这里会更简单一点。

这样乍看,似乎又回到了原点,我们还是需要一台服务器,这里又要引入本文的另一个嘉宾了,tcb 云函数。上述这种按需调用的服务,使用云函数最合适了,你不需要一个一直开机的虚拟机,你只需要在更新文章时候才需要唤起云函数就好,随用随停,成本低廉。

按照本博客的场景,我们让桥接服务在运行的时候,自动生成站点的sitemap到github来一举两得。

下面是精简过的代码:

生成站点地图sitemap.xml

const { SitemapStream, streamToPromise } = require('sitemap') const { Readable, Transform, pipeline } = require('stream') const { apiRequest, getPostsWithGraphql } = require('./request') const PaginationLimit = 30 module.exports = ({ hostname, cmsUrl }) => { async function getPostSitemap() { const smStream = new SitemapStream({ hostname, }); let page = 0; const postStream = new Readable({ objectMode: true, async read(size) { const result = await getPostsWithGraphql(`${cmsUrl}/graphql`, page++, PaginationLimit); if (result.error || !Array.isArray(result.data.posts)) { this.push(null); } else { result.data.posts.forEach((item) => { this.push(item); }); if (result.data.posts.length < PaginationLimit) { this.push(null); } } }, }); const trans = new Transform({ objectMode: true, transform(data, encoding, callback) { callback(null, { url: `/p/${data.book.slug || data.book.uuid}/${ data.slug || data.uuid }`, changefreq: 'daily', priority: 1, lastmod: new Date(data.updated_at), }); }, }); const buffer = await streamToPromise(pipeline(postStream, trans, smStream, (e) => { // throw e; })) return { path: 'public/sitemap.xml', content: buffer.toString() } } return Promise.all([ // getHomeSitemap(), // getBookSitemap(), getPostSitemap() ]) }

更新Github中文件

'use strict'; const { Octokit } = require("@octokit/rest"); const { createOrUpdateTextFile, } = require("@octokit/plugin-create-or-update-text-file"); const { throttling } = require("@octokit/plugin-throttling"); const getSitemaps = require('./sitemap') const MyOctokit = Octokit.plugin(createOrUpdateTextFile, throttling); exports.main = async (event, context) => { const { headers: { authorization, 'x-strapi-event': strapiEvent }, body } = event; const { model, entry } = JSON.parse(body) const { CMS_TOKEN, GITHUB_ACCESS_TOKEN, BLOG_URL = 'https://hicc.pro', CMS_URL = 'https://cms.hicc.pro' } = process.env; // strapi 上添加密钥来确保安全 if (CMS_TOKEN !== authorization) { return { doTrigger: false } } let doTrigger = false // TODO: 识别真正的发布 const siteMaps = await getSitemaps({ hostname: BLOG_URL, cmsUrl: CMS_URL }) const octokit = new MyOctokit({ auth: GITHUB_ACCESS_TOKEN, throttle: { onRateLimit: (retryAfter, options) => { console.warn( `Request quota exhausted for request ${options.method} ${options.url}` ); // Retry twice after hitting a rate limit error, then give up if (options.request.retryCount <= 2) { console.log(`Retrying after ${retryAfter} seconds!`); return true; } }, onAbuseLimit: (retryAfter, options) => { // does not retry, only logs a warning console.warn( `Abuse detected for request ${options.method} ${options.url}` ); }, }, }); await Promise.all(siteMaps.map(({ path, content }) => { return octokit.createOrUpdateTextFile({ // replace the owner and email with your own details owner: "xxx", repo: "xxx", path, message: `feat: update ${path} programatically`, content: content, branch: 'master', sha: '', committer: { name: "xxx", email: "xxxx@outlook.com", }, author: { name: "xxx", email: "xxxx@outlook.com", }, }) })) return { doTrigger } };