技术知识文章集合TECHNICAL ARCHIVE · 807 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTETC

探索 Serverless 中的前端开发模式

所属分类
Other
文件格式
Markdown
原始路径
Other/24-探索 Serverless 中的前端开发模式
本文目录6 个章节
  1. 一、前端开发模式的演进
  2. 二、Serverless 服务中的前端解决方案
  3. 三、基于 Serverless 的前端开发模式
  4. 四、Serverless 开发最佳实践
  5. 总结
  6. 整理者注

探索 Serverless 中的前端开发模式

Category(分类): Other Status: 已读

原文:探索 Serverless 中的前端开发模式(掘金,蒋航/nodejh,2019,QCon2019 分享主题)。 本文为整理版本,保留全部章节与代码示例。

看似与前端关系不大的 Serverless,其实早已和前端有了渊源,并且将对前端开发模式产生变革性的影响。本文从前端开发模式的演进、基于 Serverless 的前端开发案例以及 Serverless 开发最佳实践几个方面展开。

一、前端开发模式的演进

前端开发模式主要有四个阶段:

1. 基于模板渲染的动态页面

早期互联网时代,网页就是一些静态或动态页面,用于信息展示和传播。通过 JSP、PHP 等技术写动态模板,Web Server 将模板解析成 HTML,浏览器只负责渲染。这个阶段还没有前后端分工,通常是后端工程师顺便写了前端页面。

2. 基于 AJAX 的前后端分离

2005 年 AJAX 技术正式提出,翻开 Web 开发新篇章。前端负责界面和交互,后端负责业务逻辑,通过接口交互。网页复杂度由后端 Web Server 转向浏览器端 JavaScript——也正因如此,开始有了前端工程师这个职位。

3. 基于 Node.js 的前端工程化

2009 年 Node.js 出现,随之而来的还有 CommonJS 规范和 npm 包管理机制,随后出现 Grunt、Gulp、Webpack 等构建工具。2013 年前后 React/Angular/Vue 相继发布,开发从"一个个页面"变为"一个个组件",打包构建、命令行发布让前端开发规范化、标准化、工程化。

4. 基于 Node.js 的全栈开发

JavaScript 也能运行在服务器上了,前端工程师开始用最熟悉的语言写服务端代码,向全栈工程师转变——这是前端主动突破自己的边界。

另一方面,后端也在发展:巨石应用转向微服务架构,后端接口渐渐变得原子性,不再直接面向页面,前端调用变复杂了。于是 BFF(Backend For Frontend)架构应运而生:在微服务和前端中间加一层 BFF,对接口进行聚合、裁剪后再输出给前端。BFF 不是后端本质工作,且距离前端最近,前端工程师自然而然选择了 Node.js 来实现——这也是当前 Node.js 在服务端较为广泛的应用。

下一代开发模式

每一次前端开发模式的变化,都因某个变革性的技术而起:先是 AJAX,而后是 Node.js。下一个变革性的技术就是 Serverless。

二、Serverless 服务中的前端解决方案

Serverless 简介

CNCF 的定义:

Serverless computing refers to the concept of building and running applications that do not require server management.

其实 Serverless 早已和前端产生了联系:CDN、对象存储、第三方 API 服务,对前端来说都是 Serverless——我们不需要关心节点分布、负载均衡、文件存取、权限控制。

更精确的定义:Serverless = FaaS + BaaS。

  • FaaS(Function as a Service):运行函数的平台,如阿里云函数计算、AWS Lambda;
  • BaaS(Backend as a Service):后端云服务,如云数据库、对象存储、消息队列,可极大简化应用开发难度。

Serverless 可以理解为"运行在 FaaS 中的、使用了 BaaS 的函数"。主要特点:

特点说明
事件驱动函数需要通过一系列事件来驱动执行
无状态每次执行可能用不同容器,无法内存共享;共享数据只能靠 Redis 等第三方服务
无运维不需要关心服务器——这是 Serverless 思想的核心
低成本按函数运行付费,不运行不花钱

前端解决方案架构

从下往上分基础设施和开发工具两层:

  • 基础设施:云计算平台提供的 FaaS 平台和各种 BaaS 服务;
  • 框架:目前没有统一的 Serverless 标准,不同平台服务不同、代码无法平滑迁移。框架(Serverless Framework、ZEIT Now、Apex 等)负责简化开发部署流程 + 屏蔽平台差异;
  • Web IDE:云端开发、调试、部署一体化,免本地环境配置。如 AWS Cloud9、阿里云函数计算 Web IDE、腾讯云 Cloud Studio;
  • 命令行工具:两大类——云平台官方(aws、az、阿里云 fun)和框架提供(serverless、now),大部分用 Node.js 实现:
# 创建
serverless create --template aws-nodejs --path myService
fun init -n qcondemo helloworld-nodejs8

# 部署
serverless deploy
fun deploy

# 调试
serverless invoke [local] --function functionName
fun local invoke functionName
  • 应用场景:除传统服务端开发外,还有小程序开发,未来可能涉及 IoT。

不同 Serverless 服务对比

  • 共性:几乎所有服务都支持 Node.js/Python/Java;都支持 HTTP、对象存储、定时任务、消息队列等触发器;计费基本一致——每月 100 万次免费调用(之后约 ¥1.3/百万次)、400,000 GB-s 免费执行时间(之后 ¥0.0001108/GB-s),应用体量小的时候非常划算;
  • 差异:触发器与平台自家产品绑定(阿里云 OSS 事件 vs AWS S3 事件),不通用——标准不统一是当前 Serverless 面临的主要问题。

三、基于 Serverless 的前端开发模式

传统开发流程:前端写页面 → 后端写接口 → 部署 → 联调 → 测试 → 上线 → 运维维护。涉及多角色、链路长、沟通成本高。

而基于 Serverless,后端被拆分为一个个函数,写完部署即可,无需关心服务器运维,后端开发门槛大幅降低,一个前端工程师就可以完成所有开发工作(当然复杂后端系统或不适用场景仍需要后端,后端变得更靠后了)。

1. 基于 Serverless 的 BFF

通用 BFF 架构:最底层是后端微服务,最上层是前端应用,中间是由前端工程师开发的 BFF。

这个架构解决了接口协调问题,也带来新问题:每个设备一个 BFF 应用面临重复开发;以往前端只关注浏览器端渲染,现在要维护各种 BFF 应用、承受并发压力——前端并不擅长运维,运维成本非常高。

Serverless 的解法:用一个个函数实现接口的聚合裁剪。前端向 BFF 发起的请求相当于 FaaS 的 HTTP 触发器,函数中调用多个微服务获取数据、处理后返回。运维压力由 BFF Server 转向 FaaS 服务。还可以加 API 网关层统一管理(分组、分环境)。

2. 基于 Serverless 的服务端渲染

客户端渲染的主要问题是白屏时间和 SEO,于是前端又开始做服务端渲染(本质与最早的模板渲染相同,只是从 JSP/PHP 模板变成了 React/Vue 同构应用)。但 SSR 又带来运维问题:前端需要维护用于渲染的服务器。

Serverless 解法:将以往的每个路由都拆分为一个个函数部署到 FaaS,用户请求的 path 对应每个单独的函数,运维转移到 FaaS 平台。

ZEIT 的 Next.js 就做了很好的实现:

.
├── next.config.js
├── now.json
├── package.json
└── pages
    ├── about.js
    └── index.js
// next.config.js
module.exports = {
  target: 'serverless'
}

pages/about.js 和 pages/index.js 在部署时分别转换为两个函数,负责渲染对应页面。

3. 基于 Serverless 的小程序开发

国内使用 Serverless 较多的场景是小程序云开发(支付宝、微信都提供)。传统小程序开发需要前端 + 后端,后端要关心负载均衡、备份容灾、监控报警;基于云开发,后端被封装为 BaaS 服务并提供 SDK,开发者像调用函数一样使用后端服务,一个前端工程师就能完成整个应用的前后端开发。

// 支付宝云开发(Basement)示例

// 操作数据库
basement.db.collection('users').insertOne({ name: 'node', age: 18 })
  .then(() => resolve({ success: true }))
  .catch(err => reject({ success: false }))

// 上传图片
basement.file.uploadFile(options)
  .then((image) => this.setData({ iconUrl: image.fileUrl }))

// 调用函数
basement.function.invoke('getUserInfo')
  .then((res) => this.setData({ user: res.result }))

4. 通用 Serverless 架构

最底层是实现复杂业务的后端微服务;FaaS 层通过函数实现业务逻辑并为前端直接服务。对前端开发者,可以通过编写函数实现服务端逻辑;对后端开发者,后端变得更靠后。业务简单时,FaaS 层能搞定的话甚至不需要微服务层。任何一层都可以调用 BaaS 服务降低开发难度与成本。

四、Serverless 开发最佳实践

传统开发基于"应用",Serverless 开发的是"一个个函数"——测试和性能都有新课题。

函数的测试

Serverless 给测试带来的挑战:

  • 函数是分布式的,不知道也不需要知道运行在哪台机器上——需要对每个函数做单元测试;
  • 应用由一组函数组成,函数内部可能依赖 BaaS——还需要集成测试;
  • FaaS 和 BaaS 在本地难以模拟;不同平台的 FaaS 环境和 BaaS SDK 可能不一致(也增加迁移成本);
  • 驱动函数执行的事件在本地难以模拟。

解法遵循测试金字塔原则(单元测试成本最低效率最高,尽可能多做单元测试减少集成测试),核心手法:将业务逻辑和函数依赖的 FaaS / BaaS 分离。

一个糟糕的例子(业务逻辑与 FaaS/BaaS 全部耦合在函数里):

const db = require('db').connect();
const mailer = require('mailer');

module.exports.saveUser = (event, context, callback) => {
  const user = { email: event.email, created_at: Date.now() }
  db.saveUser(user, function (err) {
    if (err) {
      callback(err);
    } else {
      mailer.sendWelcomeEmail(event.email);
      callback();
    }
  });
};

问题:① 业务逻辑依赖 FaaS 平台提供的 event/context 参数;② 测试必须依赖真实的 db 和 mailer。

重构为可测试的函数——业务逻辑放进独立的类,依赖通过构造函数注入:

class Users {
  constructor(db, mailer) {
    this.db = db;
    this.mailer = mailer;
  }
  save(email, callback) {
    const user = { email: email, created_at: Date.now() }
    this.db.saveUser(user, function (err) {
      if (err) { callback(err); }
      else { this.mailer.sendWelcomeEmail(email); callback(); }
    });
  }
}
module.exports = Users;
// FaaS 层只做薄薄的一层转发
const db = require('db').connect();
const mailer = require('mailer');
const Users = require('users');
let users = new Users(db, mailer);

module.exports.saveUser = (event, context, callback) => {
  users.save(event.email, callback);
};

测试 Users 时传入模拟服务即可(如假的 mailer),充分单元测试业务逻辑后再用真实服务做集成测试。附带的好处:迁移平台时只需修改 Users 的调用方式,不用改业务逻辑。

测试三原则:

  1. 将业务逻辑和函数依赖的 FaaS 和 BaaS 分离;
  2. 对业务逻辑进行充分的单元测试;
  3. 对函数进行集成测试验证是否正常工作。

函数的性能

传统应用启动后常驻内存;Serverless 函数事件到来时要先下载代码、启动容器、在容器里启动运行环境,最后才执行代码——前几步统称冷启动。冷启动时间的长短就是函数性能的关键。

① 语言的选择:已有大量测试表明——增加函数内存可以减少冷启动时间;C#、Java 的冷启动时间约是 Node.js、Python 的 100 倍(想缩小差距就得给 Java 分配更大内存,即更高成本)。

② 并不是每次执行都冷启动:第一次请求成功启动运行环境后,运行环境会保留一段时间供后续函数复用;请求达到一个运行环境的限制时,FaaS 平台自动扩展下一个运行环境。

作者用阿里云函数计算实测:串行 100 个请求,第一个 302ms(冷启动),其余约 50ms(热启动);并发 10 时前 10 个 200-300ms(同时冷启动 10 个环境),其余 90 个约 50ms。

③ 执行上下文重用:既然运行环境会保留,就可以把耗时的初始化放到函数之外:

const mysql = require('mysql');

// 初始化数据库连接——只有第一次冷启动才会执行
const connection = mysql.createConnection({ /* ... */ });
connection.connect();

module.exports.saveUser = (event, context, callback) => {
  connection.query('...');
};

每次函数执行都重新建连接 → 只有冷启动建一次连接,后续请求直接复用。牺牲一个请求的性能,换取大部分请求的性能,完全可以接受。

④ 给函数预热:隔一段时间主动调用函数,冷启动一个运行环境,让正常请求都是热启动。注意:频率至少大于 5 分钟、直接调用而非网关间接调用、创建专门的预热函数而非业务函数。属于"黑科技"方案,如果业务能接受牺牲第一个请求,则不必用。

性能优化小结:

  1. 选用 Node.js / Python 等冷启动时间短的语言;
  2. 为函数分配合适的运行内存;
  3. 执行上下文重用;
  4. 为函数预热。

总结

前端的边界一直在拓展:网页 → 小程序 → APP → 桌面程序 → 服务端。Serverless 最大程度帮助前端工程师实现理想:不用再过多关注服务端运维和陌生领域,专注业务开发与产品实现。需要关心的事情变少了,但能做的事情更多了——前端工程师的职能将再度回归到"应用工程师"。

一句话总结 Serverless:Less is More。

整理者注

本文(2019)对 Serverless 前端化的判断在后续几年逐步兑现:Next.js/Nuxt 的 Serverless 部署目标、各类小程序云开发、边缘函数(Edge Functions)等都是文中"BFF 函数化 / SSR 函数化"思路的延续。文中 ZEIT Now 已更名为 Vercel;阿里云 fun CLI 已演进为 s(Serverless Devs)。冷启动优化(上下文复用、预热)至今仍是 Serverless 工程实践的核心话题。

807 DOCUMENTS · 12 COLLECTIONS
ARCHIVE SEARCH807 篇文章