基於 Next.js 和雲開發 CMS 的內容型網站應用實戰開發

本文目錄

  • 引言
  • 總覽
  • 背景介紹
  • 安裝 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-contentstcb-ext-cms-userstcb-ext-cms-webhooks

會自動創建 3 個雲函數:tcb-ext-cms-apitcb-ext-cms-inittcb-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():支持orderBywhere的條件查詢

之後我們會在 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 中,封裝了雲函數的整體邏輯:

  1. 檢驗 Next.js 端傳入數據是否合法
  2. 檢驗身份密鑰,防止雲數據庫被盜刷
  3. 綁定特殊變量到上下文,減少 tcb 對象的實例化次數
  4. 調用對應服務,返回結果
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:hostingout/ 目錄下的文件上傳到「靜態網站託管」。訪問靜態網站託管的鏈接: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_IDTCB_SECRET_KEY 的值

在 CI 工具的控制檯中,配置 TCB_SECRET_IDTCB_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 服務開發。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章