本文目錄:
- 引言
- 總覽
- 背景介紹
- 安裝 CMS
- 使用 CMS 創建動態內容
- 項目搭建
- 獲取 CMS 內容
- 編寫雲函數
- 發佈雲函數
- 在 Next.js 中獲取動態數據
- 自動構建與部署
- 最後
引言
隨着騰訊云云開CloudBase發能力的日漸完善,有經驗的工程師已經可以獨立完成一個產品的開發和上線。但網上雲開發相關的實戰文章非常少,很多開發者清楚雲開發的能力,但是不清楚如何在現有的開發體系下引入雲開發。
本文從雲開發團隊開發者+能力使用者的角度,以雲開發官網 (http://cloudbase.net/) 的搭建思路爲例,分享雲開發CloudBase結合流行框架與工具的實戰經驗。
涉及到的知識點有:
- 雲開發CloudBase:
- 擴展能力(CMS 擴展)
- 靜態託管
- 雲函數
- 雲數據庫
- CloudBase CLI 工具
- React 框架:Next.js
- CI 自動構建
總覽
系統設計圖:
背景介紹
隨着雲開發CloudBase團隊業務的迅猛發展,團隊需要一個官網來更直觀、更即時地向開發者們展示雲開發的相關能力,包括但不限於工具鏈、SDK、技術文檔等。
同時,爲了降低開發者的上手成本,積累業界的優秀實戰經驗,官網也承載着營造社區氛圍、聚合重要資料、增強用戶黏度的重要任務。
我們最初使用 VuePress 作爲靜態網站工具,遇到了一些痛點:
- 問題 1: 每次更新內容,都需要配合 git。運營同學對 git 不熟悉
- 問題 2: 學習資料方面的內容更新過於頻繁,“污染”了 git 記錄
- 問題 3: 內容和網站代碼耦合
- 問題 4: 缺少可視化的內容編輯工具
我們使用「CMS 擴展」、「雲開發基礎能力」、「Next.js」、「CI 工具」,很好地解決了以上問題。在實現網站內容動態化的同時,保證了 SEO,運營同學也可以通過 CMS 對內容進行可視化管理。
安裝 CMS
進入雲開發擴展能力控制檯,根據引導,安裝 CMS 內容管理系統。
在最後進行擴展程序配置的時候,有兩種賬號:管理員賬號和運營者賬號。管理員賬號權限更高,可以創建新的數據集合;而運營者賬號只能在已有的數據集合上進行增刪改的操作。
注意:
安裝時間有些長,請耐心等待
安裝成功後,雲數據庫會自動創建 3 個集合:tcb-ext-cms-contents
、tcb-ext-cms-users
、tcb-ext-cms-webhooks
。
會自動創建 3 個雲函數:tcb-ext-cms-api
、tcb-ext-cms-init
、tcb-ext-cms-auth
。
進入「靜態網站託管」,可以看到 CMS 系統的靜態文件已經自動上傳到tcb-cms/
目錄下了:
點擊上方的「基礎配置」,就可以查看到域名信息。
在瀏覽器中訪問:http://pagecounter-d27cfe-1255463368.tcloudbaseapp.com/tcb-cms/ 即可看到 CMS 系統:
到此爲止,無任何開發成本,一個 CMS 內容管理系統就正式上線了~
使用 CMS 創建動態內容
對於動態化的數據內容,我們將其劃分爲不同的模塊。每個內容模塊,對應 CMS 系統的一個數據集合。例如「雲開發官網」-「社區頁」中,推薦好課的內容就是動態的。
從圖中可以看到,每節課程有着多個屬性。而在雲數據庫中,每節課程就對應一個文檔,課程屬性就對應文檔的字段。字段類型與含義如下:
name<string>: 課程名稱
time<number>: 課程時間
cover<string>: 課程封面
url<string>: 課程鏈接
level<0 | 1 | 2>: 課程難度
以管理員身份登錄 CMS 系統,在「內容設置頁」新建內容。在 CMS 中,支持多種高級數據類型,例如 url、圖片、markdown、富文本、標籤數組、郵箱、網址等,並對這些類型進行了智能識別和更友好地展示。
注意:
CMS 自帶圖牀功能。當數據類型是「圖片」時,圖片會自動上傳到當前雲開發環境下的雲存儲中。圖片信息以
cloud://
開頭的特殊鏈接,存放在數據集合中。
新建內容時,默認情況下,CMS 會自動填充 4 個字段:name、order、createTime、updateTime。可以根據自身需要,對不需要的字段進行刪除。
建議:
保留 order 字段,它可以被用作數據排序。對運營者來說,數據的 order 的值越大,在 CMS 系統中展示的位置越靠前;對開發者來說,可以根據 order 來進行排序搜索。從而保證了體驗和邏輯的一致性。
根據字段創建集合後,CMS 系統左側會看到「推薦好課」。它對應的內容被保存在雲數據庫的recommend-course
(創建時指定)集合中,它的字段信息保存在雲數據庫的tcb-ext-cms-contents
(CMS 初始化時創建)集合中。
按照設定添加新的課程內容後,再次進入「推薦好課」,如下所示:
圖片、鏈接等內容,更友好地展示給運營者。
項目搭建
按照 Next.js 文檔 的指引,創建 Next.js 項目:
npm i --save next react react-dom axios
因爲我們要將網站部署到「靜態託管」上,所以要使用 Next.js 的靜態導出功能。package.json
中的打包腳本更新爲:
"scripts": {
"dev": "next",
"build": "next build && next export",
"start": "next start"
}
爲了快速部署靜態網站,以及發佈雲函數。需要全局安裝 @cloudbase/cli
:
npm install -g @cloudbase/cli
安裝後,添加兩個腳本:
deploy:hosting
: 將 Next.js 的靜態導出文件部署到「靜態託管」deploy:function
: 發佈項目中的雲函數
"scripts": {
"deploy:hosting": "npm run build && cloudbase hosting:deploy out -e jhgjj-0ae4a1",
"deploy:function": "echo y | cloudbase functions:deploy --force"
}
注意:
準備兩個雲環境,防止靜態部署時文件覆蓋。envId 爲
jhgjj-0ae4a1
的雲環境只用於部署 Next.js 的靜態導出文件。envId 爲pagecounter-d27cfe
的雲環境用來部署 CMS 系統。
獲取 CMS 內容
編寫雲函數
爲了能在 Next.js 中讀取到 CMS 系統的最新數據,我們需要新建一個雲函數,配合 HTTP Service,解析 Next.js 傳入的參數,讀取雲數據庫中的信息,並且返回給 Next.js。
爲什麼需要雲函數配合 HTTP Service,不能直接使用 SDK 嗎?
Next.js 預渲染的環境不支持 SDK(tcb-admin-node、tcb-js-sdk)。在 Next.js 中,動態獲取數據注入到模板變量時,需要在
getInitialProps()
方法中進行異步操作。我們使用 axios(支持 ssr 環境),通過訪問 url(雲開發 HTTP Service 能力)觸發雲函數,獲取最新數據。
在項目目錄下創建config.js
,存放一些配置信息:
module.exports = {
envId: "pagecounter-d27cfe", // 雲開發環境envid
siteAuthKey: "QhBYWnRjijGcGTBUxDFGWxuq", // 用於site-cms-data雲函數中的身份校驗
httpPath: "/site-cms-data" // site-cms-data雲函數的http觸發路徑
};
創建 CloudBase CLI 工具的配置文件cloudbase.js
,用於雲函數部署:
const { envId, siteAuthKey } = require("./config");
module.exports = {
envId,
functionRoot: "./cloudfunctions",
functions: [
{
name: "site-cms-data",
config: {
// 超時時間
timeout: 30,
// 環境變量
envVariables: {
SITE_AUTH_KEY: siteAuthKey
},
runtime: "Nodejs8.9",
installDependency: true
},
handler: "index.main"
}
]
};
創建 cloudfunctions/site-cms-data/
目錄,裏面存放雲函數的主要邏輯。
provider.js
中提供 Provider 對象,它支持:
fetchAll()
:獲取指定集合的所有數據query()
:支持orderBy
、where
的條件查詢
之後我們會在 Next.js 端傳入名爲api
的參數,它必須是 Provider 支持的方法。dispatch()
會根據前端傳入的api
,自動調用 Provider 上的方法。
const Provider = {
// 獲取指定集合的所有數據
async fetchAll(ctx) {
const {
db,
params: { collectionName }
} = ctx;
const collection = db.collection(collectionName);
return collection
.where({
_id: /.*/
})
.get();
},
// 支持orderBy、where的條件查詢(滿足當前需求)
async query(ctx) {
const {
db,
params: { collectionName, orderBy, where }
} = ctx;
let promise = db.collection(collectionName);
if (Array.isArray(orderBy) && orderBy.length === 2) {
promise = promise.orderBy(...orderBy);
}
if (typeof where === "object") {
promise = promise.where(where);
}
return promise.get();
}
};
async function dispatch(ctx) {
return await Providerctx.api;
}
module.exports = {
Provider,
dispatch
};
interceptor.js
提供:
vertifyAuth()
: 用戶身份檢驗isValidBody()
: 參數類型檢驗
const { Provider } = require("./provider");
const supportedApi = Reflect.ownKeys(Provider);
function vertifyAuth(ctx) {
// 前面在cloudbase.js中規定的環境變量
return ctx.key === process.env.SITE_AUTH_KEY;
}
function isValidBody(body) {
return (
"key" in body && // 驗證身份的隨機密鑰
"params" in body && // 攜帶調用服務需要的參數
"api" in body && // 調用服務名稱
supportedApi.includes(body.api)
);
}
module.exports = {
vertifyAuth,
isValidBody
};
在 index.js
中,封裝了雲函數的整體邏輯:
- 檢驗 Next.js 端傳入數據是否合法
- 檢驗身份密鑰,防止雲數據庫被盜刷
- 綁定特殊變量到上下文,減少 tcb 對象的實例化次數
- 調用對應服務,返回結果
const tcb = require("tcb-admin-node");
const { vertifyAuth, isValidBody } = require("./interceptor");
const { dispatch } = require("./provider");
module.exports.main = async (event, context) => {
let ctx = {
envId: context.namespace
};
// 驗證傳入的數據
try {
const body = JSON.parse(event.body);
if (isValidBody(body)) {
ctx = {
...ctx,
...body
};
} else {
return {
success: false,
msg: "傳入數據不合法"
};
}
} catch (error) {
console.error(error);
return {
success: false,
msg: "請檢查body格式"
};
}
// 驗證身份
if (!vertifyAuth(ctx)) {
return {
success: false,
msg: "身份驗證失敗"
};
}
// 給上下文綁定db
const app = tcb.init({
env: ctx.envId
});
ctx.db = app.database({
env: ctx.envId
});
// 服務調用
try {
const result = await dispatch(ctx);
return {
success: true,
result
};
} catch (error) {
console.error(error);
return {
success: false,
msg: error.message
};
}
};
建議:
在實際開發過程中,請規範雲函數的結果返回格式。以此雲函數爲例,成功時返回:
{success: true, result: 結果}
,失敗時返回:{success: false, msg: 錯誤信息}
發佈雲函數
通過 CloudBase CLI 工具的命令,清空之前的登錄狀態,重新進行登錄:
cloudbase logout && cloudbase login
在項目目錄下,執行發佈雲函數的命令:
npm run deploy:function
在「雲開發控制檯」-「雲函數頁」中,可以看到雲函數site-cms-data
上傳成功:
進入雲函數site-cms-data
,在「函數配置」中,修改“HTTP 觸發路徑”(和 config.js
中的 httpPath
字段保持一致):
成功後,我們可以就可以通過 https://${envId}.service.tcloudbase.com/site-cms-data
來觸發此函數。
在 Next.js 中獲取動態數據
在雲函數 site-cms-data
中,只解析外界傳入的 3 個參數:
參數名 | 參數類型 | 參數含義 |
---|---|---|
key | string |
校驗密鑰,可以從config.js 中讀取 |
api | string |
雲函數Provider 支持的方法 |
params | object |
上述方法的入參 |
爲了避免每次都重複編寫 axios 的請求配置,將一些共用的信息抽離出來, generateAxiosConfig()
實現如下:
// provider.js
import { siteAuthKey } from "./config";
/**
* 爲axios生成請求配置
* @param {String} api 雲函數(site-cms-data)支持的服務
* @param {Object} params 服務入參
*/
function generateAxiosConfig(api, params) {
const data = {
key: siteAuthKey,
api,
params
};
const config = {
headers: {
"Content-Type": "application/json"
},
method: "POST",
data: JSON.stringify(data)
};
return config;
}
前文有講到,CMS 自帶圖牀功能,拖拽上傳的圖片會被存儲在同一環境下的雲存儲中,並且獲取圖片的鏈接存放在集合中。雲存儲的鏈接是以 cloud://
開頭的特殊鏈接,需要在前端進行識別和特殊處理。
以前文我們上傳的圖片爲例,它的鏈接是:cloud://pagecounter-d27cfe.7061-pagecounter-d27cfe-1255463368/uploads/1589990230404.png
。將其轉成可訪問的 http 鏈接:https://7061-pagecounter-d27cfe-1255463368.tcb.qcloud.la/uploads/1589990230404.png
。
轉換思路是:識別 envid 後的信息,將其與tcb.qcloud.la
域名重新拼接即可。代碼實現如下:
// provider.js
/**
* 獲取雲存儲的訪問鏈接
* @param {String} url 雲存儲的特定url
*/
function getBucketUrl(url) {
if (!url.startsWith("cloud://")) {
return url;
}
const re = /cloud:\/\/.*?\.(.*?)\/(.*)/;
const result = re.exec(url);
return `https://${result[1]}.tcb.qcloud.la/${result[2]}`;
}
注意:
雲存儲的「權限設置」應爲:所有用戶可讀,僅創建者及管理員可寫。否則鏈接無法訪問。
推薦:
除了自帶的圖牀功能,開發者可以根據自身需求使用其他穩定圖牀服務,例如微博圖牀。如果使用其他圖牀,對應字段類型不能設置爲「圖片」,可以是「字符串」或者「超鏈接」。
在 provider.js
中對外暴露 getCourses()
方法,獲取「推薦課程」的數據,並且進行處理:
import { siteAuthKey, envId, httpPath } from "./config";
import axios from "axios";
const url = `http://${envId}.service.tcloudbase.com${httpPath}`;
/**
* 獲取推薦課程數據
*/
export async function getCourses() {
const config = generateAxiosConfig("fetchAll", {
collectionName: "recommend-course"
});
const res = await axios(url, config);
const { success, result, msg } = await res.data;
if (success) {
return result.data.map(item => ({
...item,
cover: getBucketUrl(item.cover) // 處理雲存儲的特殊鏈接
}));
} else {
throw new Error("獲取「推薦課程」失敗:" + msg);
}
}
創建 pages/index.js
文件,它對外暴露的函數組件HomePage
(被渲染爲首頁)。我們在組件上的getInitialProps()
方法中獲取推薦課程數據,並且將其注入到組件的props
上:
import React, { useState } from "react";
import { getCourses } from "./../provider";
const HomePage = props => {
// ...
};
HomePage.getInitialProps = async () => {
const promises = [getCourses()];
const [courses] = await Promise.all(promises);
// 返回組件的props
return { courses };
};
export default HomePage;
在 HomePage 中,可以從 props
中讀取到推薦課程數據,將其渲染到頁面上即可:
const levelMap = {
0: "初級",
1: "中級",
2: "高級"
};
const HomePage = ({ courses }) => {
return (
<>
{courses.map((course, index) => (
<div key={index}>
<p>
<a href={course.url}>{">>> 立即學習"}</a>
</p>
<p>
<strong>課程名稱:</strong> {course.name}
</p>
<p>
<strong>課程時長:</strong> {course.time} 課時
</p>
<p>
<strong>課程難度:</strong>
{levelMap[course.level]}
</p>
<p>
<strong>課程封面:</strong>
<img src={course.cover} />
</p>
</div>
))}
</>
);
};
打開瀏覽器,進入 http://localhost:3000/ ,可以看到效果如下:
進入 view-source:http://localhost:3000/ ,可以看到網頁的 html 源碼中包含了課程數據,解決了 SEO 的問題:
注意:
getInitialProps()
方法會將數據序列化,它執行於編譯時期,而不是在網頁生命週期中觸發的。
自動構建與部署
目前爲止,開發工作基本結束。執行 npm run build
命令,網站靜態文件被打包到了 out/
目錄下:
執行 npm run deploy:hosting
將 out/
目錄下的文件上傳到「靜態網站託管」。訪問靜態網站託管的鏈接:https://jhgjj-0ae4a1.tcloudbaseapp.com/ ,效果如下:
藉助成熟的 CI 工具,例如 Travis、Circle 等,可以定時觸發構建工作。如此一來,內容和開發徹底分離。
在構建發佈的時候,需要用到 CloudBase CLI 工具。在 CI 工具中,不再使用 cloudbase login
進行交互式輸入登錄,而是使用密鑰登錄: cloudbase login --apiKeyId $TCB_SECRET_ID --apiKey $TCB_SECRET_KEY
。
注意:
前往 雲 API 密鑰 獲得
TCB_SECRET_ID
和TCB_SECRET_KEY
的值
在 CI 工具的控制檯中,配置 TCB_SECRET_ID
和 TCB_SECRET_KEY
。併爲package.json
新添加一個腳本:
"scripts": {
"login": "echo N | cloudbase login --apiKeyId $TCB_SECRET_ID --apiKey $TCB_SECRET_KEY"
}
總結來說,CI 構建的流程是:
- tcb 密鑰登錄:
npm run login
- 獲取最新數據,導出靜態文件:
npm run build
- 發佈到「靜態網站託管」:
npm run deploy:function
如果數據需要緊急修改上線,可以在本地或者 CI 工具控制檯,手動觸發構建。
最後
在現有開發體系下,合理運用雲開發,使得人力成本、開發成本以及運維成本大幅度降低。前後端一把梭,構成“閉環”。
本文實戰僅是拋磚引玉,涉及了雲開發能力的一部分,還有更多好玩的東西等待你的探索,比如使用雲函數實現 SSR、託管後端服務、圖像服務、各端 SDK 等。
探索能力,發散思路,以更低成本開發高可用的 web 服務,雲開發絕對是你最好的選擇!
更多資料:
作者簡介:
董沅鑫,雲開發 CloudBase 團隊研發工程師,側重於前端工程化、node 服務開發。