云开发 For Web:一站式开发下一代 Serverless Web 应用

近两年来,Serverless 无疑是前端圈里最火热的话题之一,在各种技术峰会、各种技术文章里都能看到它的身影。如果你是一名前端开发者,一定很奇怪:

“我们这些前端切图仔,为什么要关注 Serverless 这种云计算的概念?”

我们就从这个话题开始聊起吧。

为什么前端开发者要关注 Serverless ?

这个问题用一句话回答就是:

“Serverless 解放了端开发者(不仅仅是 Web 开发者)的生产力,让端开发者可以更快、更好、更灵活地开发各种端上应用,不需要投入太多精力关注于后端服务的实现。”

下面我们就来慢慢解释这句话。

在前端的史前时代,那个时候甚至还没有”前端工程师“这个职位分类,所有人都是后台开发,所有的网页和 Web 应用都是来自于后台渲染,CGI、PHP (甚至你可能都没听说过的 Perl)便是当时 Web 技术的代名词,那个时候的 JS 和 CSS,不过是“切图仔”在网页里写动效和弹窗的小玩具而已。

后来,AJAX 技术出现了,最早在 Outlook Web Access 中出现,随后随着 Google Map、Gmail 等大型 Web 应用的实践,逐步为人所知。这项技术让页面内的 JS 也能异步地向服务器发起各种请求,并且把数据渲染到页面上。这个时候,前端工程师们开始接管视图层逻辑:

再后来,Node.js 诞生,大大降低了前端开发者开发一个后台服务的难度,这也让前端开发者逐渐接管了接管了渲染逻辑,在这期间,各大前端框架(代表性的 React、Vue)的服务器端渲染也逐步成熟。

既然能用 Node.js 来做服务端渲染,那么拿 Node.js 来写后台业务逻辑、实现各种 HTTP API 当然也不在话下,所以在一些公司里,前端接管业务逻辑,后台只负责各种底层微服务的架构就出现了,这也是目前很多大公司在实行的架构:

细心的你可能已经注意到了,在这个时候,渲染、HTTP API、后台业务逻辑这些东西,从端的角度看是属于服务端的,但是从分工角度看却属于前端开发的范畴,这就是 BFF(Backend for Frontend)的概念,它的优势在于:

  • 把核心业务逻辑完全交给前端工程师,让业务逻辑可以更加灵活快速地变更;
  • 让后端工程师可以更加关注于海量服务的稳定性和可靠性,提升服务质量。

这就是为什么大公司的很多业务,都开始越来越多地招 Node.js 全栈工程师的原因。

但是 BFF 并不是银弹,它还是有一定问题的,比如在国内通晓前后端开发的全栈工程师太少,很难撑起大体量的业务开发。而且国内大多数前端工程师缺少服务端开发的经验,让他们运维上百台服务器和一整套海量服务,有点强人所难:

  • 每个服务器实例应该有多少核?多少内存?
  • 怎么优化 Linux 网络栈的各种参数?
  • 服务器宕机怎么办?双活、多活、同城、异地怎么搞?

而 Serverless 正是帮助前端工程师解决运维开发 BFF 的良药。

Serverless 这个单词,直译成中文的话,应该是“无服务器”。当然,这里的“无服务器”绝对不是说不需要任何服务器资源了,而是说不需要关心服务器的具体运维和管理,只需要写代码然后发布即可。

它包含了 FaaS(函数即服务)和 BaaS(后端即服务)两大块功能,把各种基础设施进行了抽象,即使不熟悉后端的开发者,也能快速高质量地开发出海量、稳定的后端服务,而这对于前端工程师维护 BFF 服务来说,几乎是量身定做的。

Talk is cheap, Show me the code!

口说无凭,我们还是来直接看代码吧!

云数据库

云开发提供了一个文档型的 NoSQL 数据库,与传统的云上数据库不同的是,云开发的数据库可以在各种客户端内使用 SDK 直接进行读写,比如 Web 应用、小程序内、Flutter 客户端等等

下面我们以 Web 应用为例,展示云数据库的一系列功能。

基础读写

CURD是数据库最基础的功能,云开发 SDK 提供了一套链式调用接口,对数据库进行读写:

// 使用 Web 端 SDK
const cloudbase = require('tcb-js-sdk')
const app = cloudbase.init({ /* 初始化... */ })

const db = app.database()

// 插入文档
await db.collection('messages').add({
    author: 'stark',
    message: 'Hello World!'
})

// 查询文档
const data = await db.collection('messages').where({
    author: 'stark'
}).get()

// 更新文档
await db.collection('messages').where({
    author: 'stark'
}).update({
    message: 'Hi, Cloudbase!'
})

// 删除文档
await db.collection('messages').where({
    author: 'stark'
}).remove()

聚合搜索

普通的查询可能无法满足一些复杂的需求,比如联表、group等等。

云开发针对这些复杂的查询场景,推出了聚合搜索的功能,把一系列操作抽象为一个管道(pipeline),单次执行即可,避免了多次读的性能问题,我们以 group 操作为例:

const $ = db.command.aggregate
const result = await db
    .collection('message')
    .aggregate()
    .group({
        // 以 author 字段作为 key,统计相同 author 的数量
        _id: '$author'
        messagesCount: $.sum(1)
    })
    .end()
    //=> { "_id": "stark", messagesCount: 12 }

更多的聚合搜索功能,可以参考:Aggregate | 云开发 Cloudbase

事务

在订票、预约、转账等等场景下,开发者可能会要求数据库能够保证一连串读写的原子性,避免出现竞争条件,这就是数据库事务的使用场景。

云开发数据库当然也提供了事务功能:

// 启动事务
const transaction = await db.startTransaction()

// 在事务内读
const data = await transaction.collection('messages')
    .where({ /* <query> */})
    .get()

// 在事务内写
await transaction.collection('messages')
    .where({ /* <query> */})
    .update({ /* <data> */})

// 提交事务
await transaction.commit()

实时推送

在实时聊天室、实时数据看板等等场景下,我们经常会需要订阅数据库的更新,从而实现实时数据推送

云开发的数据库提供的 watch() 方法就是为此设计的,每当数据库符合条件的文档发生变化时,都会触发 onChange 回调,示例代码如下:

await db.collection('messages')
    .where({/* <query> */})
    .watch({
        onChange: snapshot => {
            console.log("收到snapshot!", snapshot)
        },
        onError: error => {
            console.log("收到error!", error)
        }
    })

更多信息可以参考:数据库实时推送 | 云开发 Cloudbase

云函数

所谓的云函数,便是在云端运行的、事件驱动的一段代码,它可以被 SDK 调用,也可以直接通过 HTTP 调用,还可以设置定时器让它定期运行:

// sum.js
module.exports = async function(events) {
    return events.a + events.b
}

这一小段代码很简单,但是隐藏在它之下的却是一整套庞大的 FaaS(函数即服务)基础设施,提供了诸如弹性伸缩、日志、监控告警等多方面的能力。

使用调用云函数

使用云开发的客户端 SDK,可以轻而易举地在各个端上调用云函数,我们以 Web 应用为例:

const cloudbase = require("tcb-js-sdk");
const app = cloudbase.init({/* 初始化 */});

app.callFunction({
    // 云函数名称
    name: "sum",
    // 传给云函数的参数
    data: {
      a: 1,
      b: 2
    }
  })
  .then(res => {
    console.log(res); // 输出 "3"
  })
  .catch(error);

HTTP 访问

你也许会觉得 SDK 体积庞大,太沉重了,那么你也可以选择使用 HTTP 来调用云函数,响应 HTTP 请求。

// hello.js
module.exports = function() {
    return 'Hello, World!'
}

然后我们直接通过命令行发布这个函数,并为它创建一条路由:

$ cloudbase functions:deploy hello
$ cloudbase service:create -f hello -p /hello

随后便可以通过 url 直接访问这个云函数:

$ curl https://xxx.service.tcloudbase.com/hello
Hello, World!

具体可以参考:https://docs.cloudbase.net/service/quick-start.html

在云函数内部使用服务端 SDK

在 Cloudbase 的云函数内,你可以直接使用 Node.js SDK,无需在初始化的时候额外注入秘钥:

const cloudbase = require('@cloudbase/node-sdk')

// 无需使用服务端秘钥
const app = cloudbase.init()

const data = await app.database().where().get()

更详细的内容可以参考:https://docs.cloudbase.net/api-reference/server/node/initialization.html

云存储

我们在开发应用的过程中,经常会遇到图片、文件上传的需求,并且可能需要为这些文件设置 CDN 访问。传统的流程是下面这样的:

  1. 前端应用调用上传接口;
  2. 后台接收文件,把文件放置到文件存储服务内(例如 腾讯云 COS);
  3. 如果需要 CDN 加速文件访问,需要为 CDN 设置回源路径到后端 COS 上。

但如果使用云开发,只需要在客户端调用 uploadFile,就可以一步完成上面的三件事情:

const tcb = require("tcb-js-sdk");
const app = tcb.init({
    env: 'your-env-id'
})
const { fileID } = await app.uploadFile({
    // 云端路径
    cloudPath: "/a/b/c/filename",
    // 需要上传的文件,File 类型
    filePath: document.getElementById('file').files[0]
})

uploadFile 会返回一个 fileID,是云开发内文件的唯一标识符,我们可以使用 getTempFileURL 来获取文件 URL 访问链接:

const tcb = require("tcb-js-sdk");
const app = tcb.init({
    env: 'your-env-id'
})
const { fileList } = app.getTempFileURL({
    fileList: [
        'cloud://a/b/c'
    ]
})
// fileList 是一个有如下结构的对象数组
// [{
//    fileID: 'cloud://a/b/c', // 文件 ID
//    tempFileURL: 'http://xxx/xxx/xxx', // 临时文件网络链接
//    maxAge: 120 * 60 * 1000, // 有效期
// }]

更详细的内容,可以参考:https://docs.cloudbase.net/storage/introduce.html

扩展能力

云开发除了上述的基础功能之外,还提供了一系列的扩展能力,包括但不仅限于:

更详细的内容,可以参考:https://docs.cloudbase.net/extension/introduce.html

总结

上面的能力是不是有些让你看花眼了,完全不知道要怎么搭配起来使用?

其实一张图就可以解决:

图中的客户端SDK包括:

服务端 SDK 包括:

使用云开发快速搭建实时聊天室

光看示例代码当然没有什么意思,我们接下来就拿云开发的一些能力,来快速开发一个实时在线聊天室吧。

项目代码:https://github.com/TencentCloudBase/cloudbase-realtime-demo

这是一个由 create-react-app 快速生成的脚手架项目,所以大部分构建和工程化的细节这里就略过不谈了,我们直接来看代码实现,大致上实现了三个功能,括号中是使用的云开发能力:

首先是我们的初始化流程,先使用匿名登录,然后建立实时数据推送的连接:

async function init() {
  // 使用匿名登录
  await auth.anonymousAuthProvider().signIn();

  // 使用 refreshToken 的前 6 位作为 uid
  setUid(auth.hasLoginState().credential.refreshToken.slice(0, 6));

  // 建立实时数据推送连接
  await db
    .collection("messages")
    .where({})
    .watch({
      onChange(snapshot) {
        setList(snapshot.docs);
        setLoading(false);
      },
      onError(err) {
        console.log(err);
      },
    });
}

建立实时连接之后,集合中的任何变化,都会触发 onChange() 回调,然后我们使用 setList() 来更新界面上的消息数据。

当然只读消息是不够的,我们还需要发送消息,具体实现非常简单,可以看 sendMessage() 方法,直接使用 add() 方法向数据库写入数据就可以了:

// 发送消息
async function sendMessage() {
  const message = {
    timestamp: new Date().getTime(),
    text,
    uid,
  };
  await db.collection("test").add(message);
  // 清空输入栏
  setText("");
}

当然以上只是局部的代码片段,整体代码可以参考:

https://github.com/TencentCloudBase/cloudbase-realtime-demo/blob/master/src/App.js

开发完毕之后,我们便可以使用 云开发静态网站 来托管我们的这个聊天室 Web 应用。

首先我们构建我们的应用:

$ npm run build

构建产物会生成到 build 目录下。

然后我们发布到静态托管即可(托管前,请先开通静态网站):

$ cloudbase hosting:deploy ./build -e your-env-id

发布完成后,你便可以通过 https://xxxx.tcb.qcloud.la/xxxx 的来访问你的应用了。进一步,你还可以为它绑定自定义域名

PS:实际上,云开发的主页官方文档,就是这样托管的(毕竟做云服务的,最重要的就是 Eating your own dog food 嘛)。

当然,除了这个实战 Demo 以外,还可以看看一些真正业务的实践:

展望

给 Web 开发者带来效率和质量上的提升,帮助他们开发更多更优质的应用,免去运维、后台开发的烦恼,是云开发不变的愿景。虽然我们目前已经有了很多强大又方便使用的能力,但这远不是我们的终点(实际上我们才刚刚起步),在未来我们将会持续完善下面的能力,进一步完善云开发的体系:

  • 更多的支持平台
  • 更完整的用户登录鉴权体系
  • 对 Vue、React 等主流框架的深度集成
  • 对 Vue、React SSR 的一站式支持
  • 更好的开发者工具、CLI、脚手架
  • 更加强大的日志、监控告警能力

PS:如果你觉得这篇文章对你有帮助,不妨花几分钟时间,来试一试快速开始吧。

本站文章资源均来源自网络,除非特别声明,否则均不代表站方观点,并仅供查阅,不作为任何参考依据!
如有侵权请及时跟我们联系,本站将及时删除!
如遇版权问题,请查看 本站版权声明
THE END
分享
二维码
海报
云开发 For Web:一站式开发下一代 Serverless Web 应用
近两年来,Serverless 无疑是前端圈里最火热的话题之一,在各种技术峰会、各种技术文章里都能看到它的身影。如果你是一名前端开发者,一定很奇怪:
<<上一篇
下一篇>>