萬字長文之 Serverless 實戰指南

前言

Serverless = Faas (Function as a service) + Baas (Backend as a service)javascript

Serverless 讓咱們更專一於業務開發, 一些常見的服務端問題, Serverless 都幫咱們解決了:css

  • Serverless 不須要搭建服務端環境, 下發環境變量, 保持各個機器環境一致 (Serverless 的機制自然可複製)
  • Serverless 不須要預估流量, 關心資源利用率, 備份容災, 擴容機器 (Serverless 能夠根據流量動態擴容, 按真實請求計費)
  • Serverless 不須要關心內存泄露, (Serverless 的雲函數服務完後即銷燬)
  • Serverless 有完善的配套服務, 如雲數據庫, 雲存儲, 雲消息隊列, 雲音視頻和雲 AI 服務等, 利用好這些雲服務, 能夠極大得減小咱們的工做量

以上前三點是 Faas 的範疇, 第四點是 Baas 的範疇. 簡單來說, Serverless 能夠理解爲有個系統, 能夠上傳或在線編輯一個函數, 這個函數的運行環境由系統提供, 來一個請求, 系統就自動啓動一個函數進行服務, 咱們只須要寫函數的代碼, 提交後, 系統根據流量自動擴縮容, 而函數裏能夠調用各類現有的雲服務 api 來簡化咱們的開發與維護成本.html

看了不少關於 Serverless 的文章, 大部分都在講架構, 講 serverless 自己的實現. 本篇就以簡單明瞭的例子闡述一個簡易博客系統在騰訊雲 Serverless 中的落地, 期間只會涉及 Faas 和 Baas 的實踐部分.前端

讓咱們開始吧~vue

簡易博客系統功能概要

時序圖

如上時序圖所示, 本次實現的簡易博客系統, 只有博客列表頁和博客內容頁, 不涉及評論, 登陸, 側重於 Serverless 落地相關的內容, 如雲函數自己怎麼編寫, 怎麼在本地開發, 怎麼跟自定義域名關聯, 怎麼訪問雲 MySQL, 雲函數內的代碼, 如 Router, Controller, Service, Model 和 View 等怎麼組織.java

帶着這些疑問, 讓咱們開始吧~node

雲函數的初始化與基礎配置

訪問 這裏, 點擊當即使用進入雲函數:mysql

騰訊雲函數入口

爲了讓你不感到畏懼, 先交個底, 騰訊雲函數每個月有 100 萬次調用的免費額度, 我的學習使用徹底夠了.react

好的, 咱們繼續~git

在點擊上圖的 "當即使用" 後, 咱們能夠看到雲函數的概覽界面:

騰訊雲函數概覽

點擊左側的函數服務, 在出現的界面中, 點擊新建:

新建雲函數

出現了下方截圖的這個頁面, 輸入函數名, 選擇語言, 能夠從函數模板中選擇一個初始化, 這裏選了右下角這個 "國慶 SCF 運營推廣活動 Demo". ps, 注意這裏有不少模板, 好比訪問數據庫, 返回頁面, 圖像壓縮, 視頻轉碼, 文件合併等等, 下降了咱們的入門成本.

選擇模板

選擇好模板後, 點擊下一步, 出現的這個界面, 設置環境變量和網絡環境

設置環境變量與網絡環境

點擊完成, 咱們的雲函數就生成啦, 來看一下效果, 雖然是雲函數, 但這裏不止一個文件哦, 是能夠以多個文件的形式組織起來的:

雲函數代碼

細看代碼, 作的事情很簡單, 根據雲函數標準, 暴露了一個 main_handler, 裏邊讀取了一個 html 頁面模板, 經過 render 函數將 html 模板 + 數據解析爲 html 字符串, 最後返回.

那咱們要怎麼才能訪問到這個雲函數呢?

答案就是配置觸發方式了, 咱們將觸發方式配置成 API 網關觸發, 設置以下:

觸發器配置

這裏解釋一些圖中的概念:

  • 定時觸發:一般用於一些定時任務, 如定時發郵件, 跑數據, 發提醒等.
  • COS 是騰訊雲存儲, 好比圖片, 視頻就能夠用 COS 存儲, COS 觸發是指文件上傳到 COS 後, 會觸發這個函數, 此時這個函數能夠用來壓縮圖片, 作視頻轉碼等等.
  • Ckafka 觸發, 當 Ckafka 消息隊列有新數據時觸發.
  • API 網關觸發, 就是有請求過來時, 才觸發這個函數.

這裏咱們選擇 API 網關觸發, 也就是有請求過來時, 才觸發這個函數.

保存後, 咱們就能看到雲函數的訪問路徑了:

雲函數訪問路徑

這裏貼一下我例子中的訪問連接, 你們能夠體驗一下~

訪問連接

以上就是咱們對雲函數的初步認識, 接下來咱們一步步深刻, 帶你打造一個簡易博客系統

Tencent Serverless Toolkit for VS Code

首先, 咱們須要一個本地開發環境, 雖然線上編輯器的體驗與 vscode 已經比較相近了, 但畢竟本地代碼編輯通過咱們配置, 仍是更好用的. 那咱們在本地修改了代碼, 怎麼發佈雲函數呢?

以 VSCode 爲例, 咱們須要安裝 "Tencent Serverless Toolkit for VS Code", 能夠在 VSCode 的插件裏搜索安裝, 插件首頁會有詳細地安裝說明, 這裏就再也不贅述.

插件界面如圖:

scf vscode 插件

安裝完後, 左側會多一個雲函數的圖標. 經過這個插件, 你能夠:

  • 拉取雲端的雲函數列表,並觸發雲函數在雲端運行。
  • 在本地快速建立雲函數項目。
  • 在本地開發、調試及測試您的雲函數代碼。
  • 使用模擬的 COS、CMQ、CKafka、API 網關等觸發器事件來觸發函數運行。
  • 上傳函數代碼到雲端,更新函數配置。

一般前端的代碼, 須要打包, 執行 npm install, npm run build 等, 雲端函數沒有提供這個環境, 咱們能夠在本地打包後, 經過這個插件發佈代碼. 固然, 咱們還能夠經過持續集成工具, 運行 cli 來發布, 這個就不展開說了.

數據庫選擇和設計

數據庫選擇

這裏選擇的是騰訊雲 MySQL 基礎版最低配, 一個月才 29 元~. 固然, 本身搭建數據庫對外暴露用於學習也是能夠的. 不過若是後期要長期使用, 爲了方便維護和確保數據穩定, 建議選擇雲 MySQL. 雲 MySQL 不須要咱們關心安裝和數據因機器掛了而丟失的問題. 開箱即用也是 Baas 的特色.

注意到裏邊選擇的網絡是 Default-VPC, Default-Subnet, 須要保持跟雲函數一致, 否則雲函數訪問不到 MySQL,如圖:

騰訊雲 MySQL 購買

激活雲 MySQL 後, 這裏能夠看到內網 ip 和端口, 雲函數能夠經過這個 ip 和端口訪問到 MySQL:

騰訊雲 MySQL

數據庫設計

由於是一個簡易的博客系統, 不涉及登陸和評論, 在知足數據庫設計第三範式的基礎上, 咱們只須要設計一張表便可, 即博客表自己:

字段名 字段類型
id 主鍵
title 標題
content 文章內容
createdAt 建立時間
updatedAt 修改時間

由於咱們後邊會使用 MySQL 的 Node.js ORM 框架 Sequelize 來操做數據庫, 數據庫表的建立是自動完成的, 這裏咱們就再也不說明啦~

後邊會有 Sequelize, 還有怎麼鏈接, 操做數據庫的介紹~

雲函數自定義域名與 API 網關映射

域名解析

前面說到, 雲函數建立完配置好 API 網關觸發器後, 就能夠在外網訪問了, 可是默認的 url 徹底沒法記憶, 不利於傳播, 咱們須要一個自定義域名. 關於域名如何購買這裏就不展開了, 你們能夠參照這篇官方文檔進行購買, 便宜的才 5 塊錢一年 ~

這裏給你們介紹, 怎麼給雲函數綁定自定義域名:

在購買域名後, 咱們須要在域名解析列表裏添加域名解析:

添加域名解析

以下圖, 將 @ 和 www CNAME 到咱們的雲函數域名, 至關因而給雲函數的域名起了個別名, 訪問自定義域名, 就能夠訪問到雲函數域名通過解析後的 ip:

雲函數解析細節

注意, 記錄值只須要填寫雲函數的域名便可, 不須要填路徑, 也不須要填協議

API 網關映射

光是將自定義域名解析到雲函數域名是不夠的, 咱們還要映射路徑, 咱們打開 API 網關的服務, 點擊咱們的雲函數服務名, 打開自定義域名, 點擊新建:

API 網關映射

按照截圖中操做後, 咱們就能夠在外網以本身的域名訪問到雲函數啦~

這裏放上本篇文章最終實現的簡易博客地址: www.momentfly.com/

雲函數中的路由設計

正如咱們前面提到的, 實現的簡易博客系統有兩個頁面, 能夠經過兩個雲函數來對應兩個頁面, 但這種實現不優雅, 由於代碼複用不了, 好比咱們寫的一些處理頁面的公共方法, 就得在兩個函數裏都實現一遍. 並且 node_modules 在兩個雲函數裏都得存在, 浪費空間.

因此咱們得在一個函數裏, 將兩個頁面的代碼組織起來, 最容易想到的是寫一個簡單的判斷, if 路徑爲 /, 則返回博客列表頁, else if 路徑爲 /post, 則返回博客內容頁. 但這仍是不優雅, 要獲取路徑, 再寫一堆 if else 來作路由, 不是很好維護, 並且若是要擴展, 還得增長 get, post 等請求的判斷, 再加上路徑上的參數也要手工寫函數來獲取.

能不能像 Express 或 koa 同樣方便地組織代碼呢? 答案是確定的!

若是對比過 AWS Lambda (亞馬遜雲 雲函數), 會發現騰訊雲函數和 AWS Lambda 在入口參數上是一致的, 咱們能夠經過 serverless-http 這個庫, 實現 koa 的接入. 這個庫本來是爲 AWS lambda 打造的, 但能夠無縫地在騰訊雲函數上使用.

如上面提到的, 雲函數的入口代碼 main_handler 以下:

exports.main_handler = async (event, context, callback) => {

}
複製代碼

咱們將代碼拉到本地, 安裝 koa, koa-router, serverless-http 後, 按照以下方式組織, 便可將 koa 無縫接入:

const Koa = require("koa");
const serverless = require("serverless-http");
const app = new Koa();
const router = require('./router');

app
  .use(router.routes())
  .use(router.allowedMethods())

const handler = serverless(app);
exports.main_handler = async (event, context, callback) => {
  return await handler(
    { ...event, queryStringParameters: event.queryString },
    context
  );
}
複製代碼

而咱們的 router 文件, 就是 koa 常規的寫法了:

const Router = require('koa-router');
const { homeController } = require('../controllers/home')
const { postController } = require('../controllers/post')

const router = new Router();

router
    .get('/', homeController)
    .get('/post', postController)

module.exports = router;
複製代碼

看到這裏, 熟悉 koa 的同窗已經掌握了該篇的主旨, 明白了 Serverless 落地的一種方式. 但接下來仍是會完整地將這個簡易博客系統搭建相關的邏輯講清楚, 感興趣的同窗繼續往下看吧~

雲函數中的代碼組織

和普通 koa 應用的組織方式一致, 爲了職責分明, 一般會將代碼組織爲 Router, Controller, Service, Model, View 等. 在經過 serverless-http 將 koa 接入進來後, 咱們的雲函數服務組織方式就徹底跟傳統 koa 應用一致了, 咱們來看看項目的完整目錄:

/blog
├── controllers
|  ├── home
|  |  └── index.js
|  └── post
|     └── index.js
├── index.js
├── model
|  ├── db.js
|  └── index.js
├── package.json
├── router
|  └── index.js
├── services
|  ├── assets
|  |  └── index.js
|  ├── data
|  |  └── index.js
|  ├── home
|  |  └── render.js
|  ├── post
|  |  └── render.js
|  └── response
|     └── index.js
├── template.yaml
├── view
|  ├── github-markdown.css
|  ├── home
|  |  ├── home.css
|  |  └── home.html
|  └── post
|     ├── post.css
|     └── post.html
└── yarn.lock
複製代碼

Controller

Controller 應該清晰地反應一個請求的處理過程, 一些實現細節要封裝起來, 放在 Service 中, 這點在流程複雜的項目中特別重要.

咱們兩個頁面的 Controller 就很簡單:

controllers/home/index.js - 博客列表頁

const render = require('../../services/home/render');
const { getBlogList } = require('../../services/data')
const { htmlResponse } = require('../../services/response')

exports.homeController = async (ctx) => {
    const blogList = await getBlogList() // 獲取數據
    const html = render(blogList) // 數據 + 模板生成 html
    htmlResponse(ctx, html) // 返回 html 的流程
}
複製代碼

controllers/post/index.js - 博客內容頁

const render = require('../../services/post/render');
const { getBlogById } = require('../../services/data')
const { htmlResponse } = require('../../services/response')

exports.postController = async (ctx) => {
    const { id } = ctx.query 
    const blog = await getBlogById(id)
    const html = render(blog)
    htmlResponse(ctx, html)
}
複製代碼

能夠看到, 咱們的 Controller 都只有三個步驟, 即

  1. 獲取數據
  2. 數據 + 模板生成 html
  3. 返回 html

咱們會在接下來的 Services 裏講清楚這三個步驟的具體實現.

Services

本篇的簡易博客系統, 博客列表頁和內容頁很類似, 因此代碼也會比較相近, 這裏就選擇博客列表頁來說 Services 啦:

上邊的 Controller 都是先獲取數據的, 咱們來看看 data 這個 services:

/services/data/index.js

const { Blog } = require('../../model')

exports.getBlogList = async () => {
    await Blog.sync({}); // 若是表不存在, 則自動建立, sequelize 的一個特性
    return await Blog.findAll();
}

exports.getBlogById = async (blogId) => {
    await Blog.sync({});
    return await Blog.findOne({
        where: {
            id: blogId,
        }
    })
}
複製代碼

經過定義好的 Model, 也就是 Blog, 執行 await Blog.findAll(), await Blog.findOne 便可獲取到博客列表和博客首頁.

數據獲取完了, 按照上邊 Controller 的流程, 咱們就要執行數據與 html 模板的拼接了, 來看 render 的 service:

services/home/render.js

const template = require('art-template');
const marked = require('marked');
const hljs = require('highlight.js');
const { markdownCss, hightlightCss, resolveAssetsFromView } = require('../assets');
const homeHtml = resolveAssetsFromView('./home/home.html');
const homeCss = resolveAssetsFromView('./home/home.css');

module.exports = (blogList) => {
    marked.setOptions({
        highlight: function (code, lang) {
            return hljs.highlight(lang, code).value;
        }
    });

    let html = template.render(homeHtml, {
        blogList: blogList.map((blog) => {
            blog.content = marked(blog.content);
            return blog;
        }),
        markdownCss,
        hightlightCss,
        homeCss,
    })

    return html
}
複製代碼

這裏用了 art-template, 是一個高性能模板引擎.

使用模板引擎來處理 html 模板和數據, 沒有用 react, vue 的緣由是簡易博客系統太簡單, 不必使用框架. 何況這個簡易博客系統的初衷側重於 Serverless 的實踐, 用 react, vue 或者簡單的模板引擎, 對 Serverless 實踐沒有影響, 若是換成 react, vue 作 ssr, 則須要另外開一個話題闡述了.

  • marked 是將 markdown string 轉成 html string 的一個庫, 如將 # hello 轉成 <h1>hello</h1>

  • highlight.js 用於高亮 markdown 中的代碼

  • markdownCss, hightlightCss, homeCss, 是寫好的 css 文件, 用 fs 讀取出來的文件內容字符串

關鍵的一句, 經過 art-template, 將 html 模板, 數據 (blogList, css) 渲染成 html

let html = template.render(homeHtml /* 模板 */, { /* 模板變量 */
    // 數據
    blogList: blogList.map((blog) => {
        blog.content = marked(blog.content); // markdown 的處理
        return blog;
    }),
    // 對模板來講, 如下這些也是數據, 只不過數據內容是 css 字符串罷了
    markdownCss,
    hightlightCss,
    homeCss,
});
複製代碼

上面的 markdownCss, hightlightCss, homeCss 是經過 assets 處理出來的, 咱們來看一下 assets 的處理:

/services/assets/index.js

const fs = require('fs');
const path = require('path');

const hightlightCss = fs.readFileSync(path.resolve(__dirname, '../../node_modules/highlight.js/styles/atom-one-light.css'), {
    encoding: 'utf-8',
})
const markdownCss = fs.readFileSync(path.resolve(__dirname, '../../view/github-markdown.css'), {
    encoding: 'utf-8',
})

const resolveAssetsFromView = (relativePath) => {
    // 輔助函數, 方便從 view 將文件讀取爲字符串
    const filePath = path.resolve(__dirname, '../../view', relativePath);
    console.log(`filePath: ${filePath}`);
    return fs.readFileSync(filePath, {
        encoding: 'utf-8',
    })
}

module.exports = {
    hightlightCss,
    markdownCss,
    resolveAssetsFromView
}
複製代碼

經過 fs.readFileSync(), 按照 utf-8 的方式讀取文件, 讀取完後連同輔助函數一塊兒暴露出去.

到了 Controller 的最後一步, 即返回 html, 咱們經過 response service 來實現:

/services/response/index.js

exports.htmlResponse = (ctx, html) => {
    ctx.set('Content-Type', 'text/html');
    ctx.body = html
    ctx.status = 200
}
複製代碼

Model

上邊的 data service, 經過 Blog Model 能夠輕易的獲取數據, 那 Blog Model 的實現是怎樣的呢? 咱們來看一下:

/model/index.js

const { Sequelize, sequelize, Model } = require('./db');

class Blog extends Model { }

Blog.init({
    title: { // 定義 title 字段
        type: Sequelize.STRING, // 字符串類型
        allowNull: false // 不容許爲空
    },
    content: {
        type: Sequelize.TEXT('medium'), // mysql 的 MEDIUMTEXT
        allowNull: false // 不容許爲空
    }
}, {
    sequelize,
    modelName: 'blog'
});

module.exports = {
    Blog,
} 
複製代碼

咱們使用 sequelize 這個 ORM 庫來簡化 MySQL 的操做, 不須要咱們手寫 SQL 語句, 庫自己也幫咱們作了 SQL 注入的防護.

Blog.init 初始化了 Blog 這個 Model. id, createdAt, updatedAt 這三個字段不須要咱們聲明, sequelize 會自動幫咱們建立.

來看看 db 的實現

/model/db.js

const Sequelize = require('sequelize');

const sequelize = new Sequelize('blog', 'root', process.env.password, {
    host: '172.16.0.15',
    dialect: 'mysql'
});

const Model = Sequelize.Model;

module.exports = {
    Sequelize,
    sequelize,
    Model,
}
複製代碼

blog 是數據庫的名稱, root 是登陸的帳戶, 密碼存放在環境變量中, 經過 process.env.password 獲取, 也就是前邊咱們在雲函數建立時, 填寫的環境變量.

View

這裏的 view 層只是 css 和 html 模板, css 就不講了, 這裏來看一下 art-template 的模板:

/view/home/home.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>serverless blog demo</title>
    <style> {{markdownCss}} {{hightlightCss}} {{homeCss}} </style>
</head>
<body>
    <div class="blog-home">
        {{each blogList}}
        <div class="post" onclick="location.href='./post?id={{$value.id}}'">
            <h1>{{$value.title}}</h2>
            <div class="markdown-body">{{@ $value.content}}</div>
        </div>
        {{/each}}
    </div>
</body>
</html>
複製代碼

{{}} 裏是模板變量, 前邊 render 方法的第二個參數裏的字段, 就能從 {{}} 中取到.

以上就是咱們簡易博客系統的代碼邏輯, 目前只有兩個頁面的代碼, 若是要增長博客建立頁面, 流程是一致的, 增長相關的 Router, Controller, Service 便可. 目前筆者是經過騰訊雲的數據庫操做界面直接寫的數據~.

若是要增長評論功能, 咱們須要新增一個表來存儲了, 固然後期你能夠按照本身的意願擴展~

實現效果

www.momentfly.com/

博客列表頁

小結

經過搭建簡易博客系統, 咱們瞭解了 Serverless 的一種實踐. 期間涉及瞭如何建立雲函數, 介紹了本地 VSCode 雲函數插件, 雲函數自定義域名與 API 網關映射, 雲數據庫的建立與鏈接, 雲函數的代碼組織方式等. 整個過程都很輕量, 沒有太多涉及服務端運維的內容. Serverless 的興起, 會給咱們的開發帶來很大的便利. 後期各大雲服務商也必將完善 Serverless 的服務, 帶來更佳的 DevOps 體驗.

最後,讓咱們一塊兒擁抱 Serverless ,動手實戰吧~

可能遇到的坑

有些小夥伴看到這裏已經躍躍欲試了, 在動手的過程當中可能遇到一點問題, 這裏統一說一下:

本地調試

咱們在本地能夠模擬 API 網關, 須要咱們自定義請求的方法, 路徑和參數, 以下圖, 咱們點擊 "新增測試模板", 便可進行配置了

本地調試

此外, 簡易博客系統在本地要運行起來, 須要鏈接本地 MySQL, 能夠經過修改模擬請求的測試模板, 傳遞特定參數來標誌環境, 從而請求本地 MySQL.

另外有些小夥伴直接把公衆號裏的代碼上傳到本身的雲函數中, 發現運行不起來, 這裏要注意不要替換本身函數的 template.yaml. 由於每一個雲函數的 template.yaml 都是惟一的, 標誌了雲函數的一些基礎配置, 如內存限制, 環境變量, 觸發器配置等.

非自定義域名的路由

有些小夥伴尚未自定義域名, 經過雲函數默認的 API 網關 URL 也能夠訪問的, 只是咱們在路由上要注意:

service-20z5jnak-1253736472.gz.apigw.tencentcs.com/release/you…

上邊的這個 url, 對應的路由是

router
  .get("/yourFunctionName", homeController)
複製代碼

完整Demo獲取

公衆號回覆 serveless 或 代碼 或 demo,便可獲取完整 Demo 代碼~

最後

  • 歡迎加我微信(winty230),拉你進技術羣,長期交流學習...
  • 歡迎關注「前端Q」,認真學前端,作個有專業的技術人...

GitHub
相關文章
相關標籤/搜索