專案背景
試想一下,如果你現在要為你自己或者你所在的組織建立一個強內容的站點,同時要求好的 SEO(搜素引擎優化),比如部落格,你會怎麼做呢?
由 vite 或者 create-react-app 等腳手架構建的普通 SPA 應用是不行的,因為這樣資料都是通過 AJAX 返回的。你暫時不瞭解這些概念也沒關係,你只需要知道,這種方式下,搜尋引擎是無法很好地瞭解你的網站是幹什麼的,所以就算大眾在搜尋引擎搜尋你的站點的相關內容,搜尋引擎也很難把你的站點排在搜尋結果前列。
那麼為每個頁面都編寫一個靜態的 html 頁面呢?比如,為每篇文章都編寫一個 html 檔案,然後放在伺服器上,這樣只要客戶端請求某篇文章,伺服器就把對應的文章頁面直接返回。這樣也不好,太麻煩了,如果每次更改內容,都要用硬編碼的方式去應對,那就把事情弄得太複雜了。如果有一種後臺系統,能讓管理員通過後臺系統的簡單操作,就能修改網站呈現的內容就好了。
本文就將帶領你採用一種新穎的、便捷的開發方式——通過結合 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} →</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 閱讀文件,加入技術交流群,一起探索雲開發的更多可能性。