CloudBase CMS + Next.js:輕鬆構建一個內容豐富的站點

騰訊云云開發發表於2021-04-27

專案背景

試想一下,如果你現在要為你自己或者你所在的組織建立一個強內容的站點,同時要求好的 SEO(搜素引擎優化),比如部落格,你會怎麼做呢?

由 vite 或者 create-react-app 等腳手架構建的普通 SPA 應用是不行的,因為這樣資料都是通過 AJAX 返回的。你暫時不瞭解這些概念也沒關係,你只需要知道,這種方式下,搜尋引擎是無法很好地瞭解你的網站是幹什麼的,所以就算大眾在搜尋引擎搜尋你的站點的相關內容,搜尋引擎也很難把你的站點排在搜尋結果前列。

那麼為每個頁面都編寫一個靜態的 html 頁面呢?比如,為每篇文章都編寫一個 html 檔案,然後放在伺服器上,這樣只要客戶端請求某篇文章,伺服器就把對應的文章頁面直接返回。這樣也不好,太麻煩了,如果每次更改內容,都要用硬編碼的方式去應對,那就把事情弄得太複雜了。如果有一種後臺系統,能讓管理員通過後臺系統的簡單操作,就能修改網站呈現的內容就好了。

SPA 應用
純粹靜態網站開發

本文就將帶領你採用一種新穎的、便捷的開發方式——通過結合 CloudBase CMS 和 Next.js,去構建內容管理方便,利於 SEO 且響應快速的站點。

CMS 是雲開發 CloudBase 推出的一款無頭(headless)內容管理系統,提供給開發者方便地管理內容資源的能力。所謂無頭,意思就是內容管理系統只負責管理你的內容,比如文章內容和作者列表。這些內容可以在客戶端或者服務端通過 SDK 或者 API 的方式去取得。而如何去展示這些內容,則由開發者自行全權掌控。

Next.js 是一款生產級的 React 框架,它提供了靜態生成的功能。靜態生成的意思是,在構建的過程中,Next.js 就會自動執行資料拉取的邏輯,並把資料和 UI 渲染為一個個的靜態 HTML 頁面,這意味著,我們的站點將響應迅速,而且利於 SEO。

本文將通過帶領你構建一個個人部落格,以展示如何結合這兩者,並最終在騰訊云云開發上部署站點。

Demo線上預覽:

開啟環境和專案

1. 開通 CloudBase CMS

首先,你要在騰訊雲控制檯開通你的第一個雲開發環境。雲開發環境是雲開發中的一個概念,每個雲環境都整合了應用開發需要的基礎能力,比如雲資料庫、雲函式,開發者可以方便地組合、使用它們,為應用開發賦能。TCB-CMS 也是建立在雲環境之上的。

建立環境時,你可以直接選擇空模板並勾選免費資源選項即可,最後將環境命名為 my-blog。

可以看到,環境已經在建立中了。

環境建立完畢後,進入擴充套件應用模組,可以看到“CMS內容管理系統”,可以在這裡安裝它。設定都按照預設就可以了,唯一要注意的是,務必記住自己設定的管理員賬號密碼

等安裝完畢後,可以在已安裝應用一欄中進入應用。點選訪問地址後,我們可以直接訪問應用。進入應用並輸入管理員賬號和密碼,然後可以看到以下頁面:

現在,系統中還沒有任何專案,點選建立一個名為 MyBlog 的新專案,建立完畢後進入專案,可以看到內容模型和內容集合,拿資料庫作類比,這兩者就是資料庫表和資料庫表內容的關係,這兩者就是我們要設定和管理的內容。已經有準備好的資料可以直接匯入,分別就在 專案原始碼倉庫 中的 ./schema 和 ./data 資料夾中。點選匯入按鈕,然後選擇匯入檔案即可。

好了,現在 CMS 已經成功開通了,我們也往其中加入了內容。接下來,就可以著手 Next.js 應用的編寫了。

2. 啟動 Next.js 專案

Next.js 是構建於 React 之上的生產級前端框架。相比於原本的 React,Next.js 提供了靜態生成、服務端渲染等特性,同時自帶前端路由,我們這次就主要用到 Next.js 的靜態生成功能。使用 Next.js 編寫前端應用,和使用 create-react-app 腳手架編寫 SPA 應用非常類似,而且更加便捷、開箱即用。

直接在命令列執行以下命令可以建立 Next.js 的樣板專案並啟動它:

npx create-next-app
npm run dev

訪問 localhost:3000 可以看到以下效果:

接下來,我們就將基於這個樣板專案開發網站。

首頁

這裡,我們將在首頁放置文章列表。

首先,開啟專案下的 ./pages/index.js,發現頁面匯出了一個函式式的 React 元件。在 Next.js 中,pages 目錄下,除了 api 資料夾下的內容和 _app.js,其他每個 js 檔案匯出的 React 元件都對應著一個或者一種頁面,並且由 Next.js 直接生成對應的路由,index.js 匯出的函式式元件就直接對應著我們進入網站後的第一個頁面,而其他 js 檔案於 ./pages 的相對地址就是 Next.js 為其自動生成的路由。

Next.js 在應用構建期,就會對每個頁面執行資料拉取的邏輯,並根據 React 元件構建的 UI,渲染出最後的 HTML 頁面,接下來,我們要做的就是,構建主頁的 UI,以及為主頁編寫拉取資料的邏輯。

UI 編寫

接下來對主頁的 UI 進行修改:

import Head from 'next/head'
import Link from 'next/link'
import styles from '../styles/Home.module.css'

export default function Home({ posts }) {
  return (
    <div className={styles.container}>
      <Head>
        <title>雲開發小站</title>
        <link rel="icon" href="https://main.qcloudimg.com/raw/3b942431a6ef465d3b8369969e861c0f/favicon.png" />
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>
          Welcome to <a href="https://www.cloudbase.net/">雲開發 CloudBase!</a>
        </h1>


        <div className={styles.grid}>
          {posts.map(post => (
            <Link href={`/post/${post._id}`}>
              <a href="https://nextjs.org/docs" className={styles.card}>
                <h3>{post.title} &rarr;</h3>
                <p>{post.author}</p>
              </a>
            </Link>
          ))}
        </div>
      </main>

      <footer className={styles.footer}>
        <a
          href="https://www.cloudbase.net/"
          target="_blank"
          rel="noopener noreferrer"
        >
          Powered by{' '}
          <img src="https://main.qcloudimg.com/raw/3b942431a6ef465d3b8369969e861c0f/favicon.png" alt="TCB Logo" className={styles.logo} />
        </a>
      </footer>
    </div>
  )
}

可以看到,修改後的 Home 元件,接受了一個 posts 為引數,這個 post 就是文章列表資料,我們將它在元件中渲染出來。

那麼 post 從哪裡來呢?在同一個 js 檔案下,需要再匯出一個 getStaticProps 函式。

export async function getStaticProps() {
  return {
    props: undefined,
  }
}

這個函式返回的物件的 props 屬性,就是匯出的函式式元件用到的引數。所以,只需要在 getStaticProps 函式中得到資料並返回即可。

拉取資料

先安裝拉取資料要用到的 SDK:

npm install --save @cloudbase/node-sdk

然後,我們再建立 env.js 檔案,在其中填入雲環境相關資訊:

export const tcbConfig = {
    env: '',
    secretId: '',
    secretKey: ''
};

其中環境ID(env)可以直接在環境主頁中看到,API 金鑰(secretId,secretKey)則可以在 訪問管理 中獲取。

最後,我們建立 ./lib/api.js,然後填入以下內容,將資料拉取的邏輯全部集中在這個檔案中。

import tcb from '@cloudbase/node-sdk';
import { tcbConfig } from '../env';

const { Author, Article } = (() => {
    const db = tcb.init(tcbConfig).database();
    return {
        Author: db.collection('author'),
        Article: db.collection('article'),
    };
})();

export const getHomePosts = async () => {
    const posts = (await Article
        .where({})
        .orderBy('_updateTime', 'desc')
        .limit(10)
        .get()).data;
    for (const post of posts) {
        const { name } = (await Author
            .where({ _id: post.author })
            .get()).data[0];
        post.author = name;
    }
    return {
        posts
    }
};

在 api.js 中,根據環境資訊,對 SDK 進行了初始化,並建立了用於查詢文章和作者的例項 Author 和 Ariticle。在 getHomePosts 函式中,我們獲取了展示用的文章。具體的邏輯如果不懂也暫時不必深究,現在只需要知道:通過執行 getHomePosts 我們能從雲環境的 CMS 系統中拉取文章列表。

接著,再來修改 ./pages/index.js 檔案:

import Head from 'next/head'
import Link from 'next/link'
import styles from '../styles/Home.module.css'
import { getHomePosts } from '../lib/api'

...

export async function getStaticProps() {
  return {
    props: await getHomePosts()
  };
}

然後,再訪問 localhost:3000,可以看到如下效果:

這標誌著:我們成功從 CMS 中獲取資料並能夠渲染出靜態頁面來返回給客戶端啦!

文章頁面

接下來,就要著手編寫文章頁面了,基本流程差不多,但值得注意的是,文章頁面和主頁不同,只有一個主頁,但是文章頁面可能有無數個,而Next.js 提供了能力,能讓我們只編寫一個 js 檔案,並加以細微的改動,就能渲染出無數的文章頁面 。

拉取用以渲染頁面的文章內容

先準備好需要的樣式。首先建立 ./styles/Post.module.css 檔案,具體樣式內容可以參考:https://github.com/LRCong/nextjs-tcbcms-app/blob/main/styles/Post.module.css

接著,建立 ./pages/post/[id].js 檔案,這個檔案對應的,就是路由形如 /post/{id} 的所有頁面,而 id 的作用就是匹配文章的 _id。這樣通過訪問 URL:/post/{id1}.js,就能訪問到文章id等於 id1 的文章對應頁面。

我們先往 [id].js 檔案中填入以下的內容:

import Head from 'next/head';
import Link from 'next/link';
import styles from '../../styles/Post.module.css';

export default function Post({
    title,
    image,
    author,
    avator,
    contentHtml
}) {
    return (
        <div className={styles.container}>
            <Head>
                <title>{title}</title>
                <link rel="icon" href="/favicon.png" />
            </Head>

            <main className={styles.main}>
                <div className={styles.info_container}>
                    <img className={styles.image} src={image}></img>
                    <div className={styles.info}>
                        <h1 className={styles.title}>{title}</h1>
                        <div className={styles.author_info}>
                            <div className={styles.avator} style={{ backgroundImage: `url(${avator})` }}></div>
                            <h2>{author}</h2>
                        </div>
                    </div>
                </div>

                <div className={styles.markdown} dangerouslySetInnerHTML={{ __html: contentHtml }} />

                <Link href='/'><h3 className={styles.back}>返回</h3></Link>
            </main>

            <footer className={styles.footer}>
                <a
                    href="https://www.cloudbase.net/"
                    target="_blank"
                    rel="noopener noreferrer"
                >
                    Powered by{' '}
                    <img src="https://main.qcloudimg.com/raw/3b942431a6ef465d3b8369969e861c0f/favicon.png" alt="TCB Logo" className={styles.logo} />
                </a>
            </footer>
        </div>
    )
}

export async function getStaticPaths() {
    return {
        paths: undefined,
        fallback: false
    };
}

export async function getStaticProps({ params }) {
    return {
        props: undefined
    };
}

可以看到,相比 index.js,[id].js 多出了一個 getStaticProps 函式,getStaticProps 也多了一個 parms 引數。getStaticProps 函式暫時不用管,而 param.id 就是在路由中匹配到的 id,可以藉助它,執行獲取對應文章內容的邏輯。

在 api.js 中,新增以下內容:

// 對於 image 型別的欄位,直接取得的 id 需要轉換為可用的 URL
const dealWithUrl = url => 'https://' + url
    .replace(`cloud://${tcbConfig.env}.`, '')
    .replace('/cloudbase-cms', '.tcb.qcloud.la/cloudbase-cms');

export const getPost = async (id) => {
    const post = (await Article.where({ _id: id }).get()).data[0];
    const { name, avator } = (await Author
        .where({ _id: post.author })
        .get()).data[0];
    post.author = name;
    post.avator = dealWithUrl(avator);
    post.image = dealWithUrl(post.image);
    return post;
};

然後安裝 remark 以及 remark-html 兩個庫,我們將用它們把 markdown 轉化為 html,然後修改 [id].js 檔案中的 getStaticProps 為

import Head from 'next/head';
import Link from 'next/link';
import styles from '../../styles/Post.module.css';
import { getPost } from '../../lib/api';
import remark from 'remark'
import html from 'remark-html'

...

export async function getStaticProps({ params }) {
    const post = await getPost(params.id);

    const processedContent = await remark()
        .use(html)
        .process(post.content)
    post.contentHtml = processedContent.toString()

    return {
        props: post
    };
}

拉取所有的文章id以渲染所有文章頁面

只到這一步還不夠,我們需要知道所有的路由可能匹配到的 id 值,Next.js 才能渲染出全部的文章頁面。[id].js 多出的 getStaticPaths 函式正是用來返回 id 所有可能的匹配值的。

這就是我們只需要編寫一次拉取文章資料邏輯,編寫一次文章頁面 UI,就能讓 Next.js 生成出無數文章的靜態頁面的奧祕。因為,可以讓 Next.js 知道所有的文章 id,然後 Next.js 就能對每個文章頁面執行一次生成了。

修改往 api.js 中新增獲取所有文章 id 的函式:

export const getAllPostId = async () => {
    let posts = (await Article.where({}).get()).data;
    return posts.map(value => ({
        params: { id: value._id }
    }))
};

然後修改 getStaticPaths 函式:

export async function getStaticPaths() {
    return {
        paths: await getAllPostId(),
        fallback: false
    };
}

然後,訪問首頁,隨便進入一篇文章,就可以看到如下效果:

到這裡,我們就成功完成 Next.js 專案的構建啦!

部署

使用騰訊云云開發,你可以輕易地將應用部署到公共網路上。

我們先修改 package.json 中依賴庫的配置,因為雲開發環境對於依賴版本有一定限制:

"dependencies": {
    "@cloudbase/node-sdk": "^2.5.1",
    "next": "9.5.4",
    "react": "16.13.1",
    "react-dom": "16.13.1",
    "remark": "^13.0.0",
    "remark-html": "^13.0.1"
}

然後,建立部署的配置檔案 cloudbaserc.json,並填入以下內容:

{
    "envId": "{{env}}",
    "version": "2.0",
    "$schema": "https://framework-1258016615.tcloudbaseapp.com/schema/latest.json",
    "functionRoot": "./functions",
    "functions": [],
    "region": "ap-shanghai",
    "framework": {
        "name": "tcbcms-nextjs",
        "plugins": {
            "client": {
                "use": "@cloudbase/framework-plugin-next",
                "inputs": {}
            }
        }
    }
}

其中的 env 換成你剛剛建立的雲環境的 ID。

修改完畢後,執行命令:

cloudbase

可以看到部署流程啟動,等待到部署完畢後,進入雲環境的“我的應用”模組,會發現應用列表多了一個“tcbcms-nextjs”,點選訪問鍵,就能訪問剛剛建立的應用,而且是通過公網 IP,這說明我們的應用已經部署成功了。

總結

到此,我們的部落格已經成功建立並部署了。以後如果部落格中要新增新文章,或者要刪改原有的文章,都只需要在 CMS 上進行內容的改動,然後在本地執行 Next.js 的構建和雲開發部署即可。

更多 Next.js 和雲開發相關知識,可以檢視官方文件:

Next.js 官方文件:https://nextjs.org/docs/getting-started

CloudBase CMS 官方文件:https://docs.cloudbase.net/cms/intro.html

@cloudbase/node-sdk 官方文件:https://docs.cloudbase.net/api-reference/server/node-sdk/introduction.html

彩蛋:雲開發是如何支撐應用開發的?

你可能會好奇雲環境的能力是如何支撐我們的系統的。

再次進入騰訊雲控制檯,進入剛剛建立的雲環境主頁,進入HTTP訪問服務、雲資料庫以及雲函式模組,會發現多了許多東西。實際上,CMS 系統就是由這些東西支撐著的。

我們每次訪問 CMS 系統並操作,都會經由 HTTP訪問服務,導向某個雲函式,在雲函式中執行後臺邏輯,而系統中的資料,也都儲存在雲資料庫中,這也是我們可以通過 @cloudbase/node-sdk 訪問雲資料庫得到 CMS 中的資料的原因。

而我們剛剛部署的 Next 應用,實際上也是執行在雲函式上的。

如果有興趣在雲開發中更進一步,歡迎前往雲開發社群官網 cloudbase.net 閱讀文件,加入技術交流群,一起探索雲開發的更多可能性。

相關文章