Serverless = Faas (Function as a service) + Baas (Backend as a service)javascript
Serverless 让咱们更专一于业务开发, 一些常见的服务端问题, Serverless 都帮咱们解决了:css
以上前三点是 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 网关触发, 设置以下:
这里解释一些图中的概念:
这里咱们选择 API 网关触发, 也就是有请求过来时, 才触发这个函数.
保存后, 咱们就能看到云函数的访问路径了:
这里贴一下我例子中的访问连接, 你们能够体验一下~
以上就是咱们对云函数的初步认识, 接下来咱们一步步深刻, 带你打造一个简易博客系统
首先, 咱们须要一个本地开发环境, 虽然线上编辑器的体验与 vscode 已经比较相近了, 但毕竟本地代码编辑通过咱们配置, 仍是更好用的. 那咱们在本地修改了代码, 怎么发布云函数呢?
以 VSCode 为例, 咱们须要安装 "Tencent Serverless Toolkit for VS Code", 能够在 VSCode 的插件里搜索安装, 插件首页会有详细地安装说明, 这里就再也不赘述.
插件界面如图:
安装完后, 左侧会多一个云函数的图标. 经过这个插件, 你能够:
一般前端的代码, 须要打包, 执行 npm install
, npm run build
等, 云端函数没有提供这个环境, 咱们能够在本地打包后, 经过这个插件发布代码. 固然, 咱们还能够经过持续集成工具, 运行 cli 来发布, 这个就不展开说了.
这里选择的是腾讯云 MySQL 基础版最低配, 一个月才 29 元~. 固然, 本身搭建数据库对外暴露用于学习也是能够的. 不过若是后期要长期使用, 为了方便维护和确保数据稳定, 建议选择云 MySQL
. 云 MySQL 不须要咱们关心安装和数据因机器挂了而丢失的问题. 开箱即用也是 Baas 的特色.
注意到里边选择的网络是 Default-VPC, Default-Subnet, 须要保持跟云函数一致, 否则云函数访问不到 MySQL,如图:
激活云 MySQL 后, 这里能够看到内网 ip 和端口, 云函数能够经过这个 ip 和端口访问到 MySQL:
由于是一个简易的博客系统, 不涉及登陆和评论, 在知足数据库设计第三范式的基础上, 咱们只须要设计一张表便可, 即博客表自己:
字段名 | 字段类型 |
---|---|
id | 主键 |
title | 标题 |
content | 文章内容 |
createdAt | 建立时间 |
updatedAt | 修改时间 |
由于咱们后边会使用 MySQL 的 Node.js ORM 框架 Sequelize 来操做数据库, 数据库表的建立是自动完成的, 这里咱们就再也不说明啦~
后边会有 Sequelize, 还有怎么链接, 操做数据库的介绍~
前面说到, 云函数建立完配置好 API 网关触发器后, 就能够在外网访问了, 可是默认的 url 彻底没法记忆, 不利于传播, 咱们须要一个自定义域名. 关于域名如何购买这里就不展开了, 你们能够参照这篇官方文档进行购买, 便宜的才 5 块钱一年 ~
这里给你们介绍, 怎么给云函数绑定自定义域名:
在购买域名后, 咱们须要在域名解析列表里添加域名解析:
以下图, 将 @ 和 www CNAME 到咱们的云函数域名, 至关因而给云函数的域名起了个别名, 访问自定义域名, 就能够访问到云函数域名通过解析后的 ip:
注意, 记录值只须要填写云函数的域名便可, 不须要填路径, 也不须要填协议
光是将自定义域名解析到云函数域名是不够的, 咱们还要映射路径, 咱们打开 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 应该清晰地反应一个请求的处理过程, 一些实现细节要封装起来, 放在 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 都只有三个步骤, 即
咱们会在接下来的 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
}
复制代码
上边的 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 层只是 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 便可. 目前笔者是经过腾讯云的数据库操做界面直接写的数据~.
若是要增长评论功能, 咱们须要新增一个表来存储了, 固然后期你能够按照本身的意愿扩展~
经过搭建简易博客系统, 咱们了解了 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)
复制代码
公众号回复 serveless 或 代码 或 demo,便可获取完整 Demo 代码~