美团点评点餐前后端分离实践

美团点评点餐前后端分离实践

随着前端技术的发展,前端开发的边界正逐渐被推向后端,两者的界限在重合与分离中不断交替。回首过往,Node.js在2009年的横空出世可以看作前端开发的里程碑事件,从此JavaScript不在局限于浏览器的狭窄空间,开始在服务器的广阔天空上展翅高飞。

一、什么是前后端分离

在介绍什么是前后端分离之前,首先需要抛出我们在业务开发中遇到的问题。

在点餐研发团队过去的开发模式中,我们采用了以后端为主的 MVC 架构方式。具体来说,每次项目评审后,前后端会先一起约定好接口,之后分别进行开发。通常前端会在本地先写好Demo并跑通前端逻辑,之后再将html文件交给后端开发套ftl模板。正式基于这样的开发模式,导致了总工作量的增加,同时沟通和联调成本的消耗也十分显著。

为了解决以上问题,我们期望引入前后端分离的解决方案,即:后端只专注于提供接口,前端全权负责页面的展示。

比较常见的前后端分离解决方案有两种:

  • 一种是将前端的html, js, css等文件直接部署在静态资源服务器上。当用户请求页面时,首先会加载静态html文件,之后通过js异步请求后端数据接口进行二次渲染。
  • 另一种是通过Node服务器来作为中间层代理,通过服务端渲染动态渲染页面。

相对来说,第一种方案实施起来更为容易,但不足之处也十分明显,诸如:不利于SEO、首屏加载白屏等。所以我们更倾向于采取基于Node.js的开发模式,并在点餐监控平台上进行了初步尝试。

二、基于Node的前后端分离

在点餐监控平台的搭建上,我们采用了基于KOA的前后端分离架构,系统结构如下图所示:

在这种模式下,前后端的权责区分更加清晰:

  1. 用户的http请求会首先发送给前端的Node服务器,由于页面与Ajax接口在同域下因此跨域和SSO问题可以得到解决。
  2. Node服务器在获得请求后,作为代理通过内网将请求分发(毫秒级)给后端Java服务器,Java服务器在读取数据和处理业务逻辑后,响应Node服务器的请求。
  3. Node服务器在获得响应后,在服务端动态渲染页面并返回给客户端,解决了SEO和前后端分离的问题。
  4. 客户端获得响应,展示页面或者JS脚本执行下一步操作。

三、从无到有搭建一个Node服务器

常见的Node.js服务框架包括Express, Koa, Feathers等等。目前美团点评使用的是基于Koa封装的自有开发框架,集成了公司的监控,服务调用,账号解析等中间件,方便各条业务线快速搭建服务。我们的监控平台也是基于此搭建的。

由于涉及公司内部代码,无法开源。因此,我只能基于项目思路写一个小Demo,方便大家参考。需要一提的是,Koa本身是不带任何中间件的轻量级框架,这里只是提供一种工程化的思路,读者也可以按自己的喜好进行配置。

0、项目资源地址

GitHub地址:github.com/Dragon-Rider

1、项目的工程目录结构

Koa本身自带快速生成项目的方法,在全局安装Koa后,可以通过 "koa + project name" 的方式快速生成,但是目录结构比较简单,不适合我们工程化的解决方案。因此我们重构了新的目录结构,如下所示:

    |——src                #业务逻辑目录
    |   |
    |   |——common         #公共文件目录
    |   |——controllers    #控制器目录
    |   |——models         #数据模型目录
    |   |——views          #页面模板目录
    |   |——routes         #路由目录
    |   |    |
    |   |    |——index.js         #主路由
    |   |    |——apiRouter.js     #接口路由
    |   |    |——pageRouter.js    #页面路由
    |   |
    |   |——public                #静态资源目录
    |        |——img
    |        |——css
    |        |——js
    |
    |——logs            #日志文件输出目录
    |——config          #项目配置文件目录
    |——node_modules    #node包依赖目录
    |——app.js          #项目入口文件
    |——package.json    #工程管理文件,涉及依赖配置、环境配置、启动命令等。

在点餐监控平台的前端项目里,我们依旧采用了MVC风格的结构。

当客户端发起请求的时候,Node服务器会先由主路由文件作为请求入口,将请求分发给各个子路由。子路由在监听到对应的HTTP请求后,会向对应的控制器请求数据。控制器访问不同的数据Model,并对各个Model返回的数据进行统一处理与封装。之后将处理后的数据返回给路由,最终响应客户端的请求。一个标准的请求与响应过程如下图所示:

2、静态文件与模板渲染

在这个样本项目中,我采用了ejs模板引擎进行视图模板的搭建。通过koa-static组件设置静态资源目录,同时使用koa-mount组件来将资源路径映射到static路径下。

const app = require('koa')();
const staticServe = require('koa-static');
const path = require('path');
const staticDirectory = staticServe(__dirname + '/src/public');

app.use(mount('/static', staticDirectory));

视图文件统一放置于views目录下,使用koa-ejs进行模板渲染,该组件支持layout布局和 ejs 的include模板嵌套语法。同时由于在之前已将静态资源配置完毕,所以在ejs模板里直接引入即可。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title><%= params.title %></title>
    <link rel="stylesheet" href="/static/css/page.css">
</head>
<body>
    <h1><%= params.content %></h1>
    <button id="btn1">click me!</button>
    <% include footer.ejs %>
</body>

<script type="text/javascript" src="/static/js/page.js"></script>
</html>

在大公司开发的过程中,往往还会将Node项目的View层再做抽离。目前比较流行的做法是用 React 或者 Vue 搭建一个纯前端的单页应用,但同样也会带来SEO问题,需要引入SPA下的SSR(服务端渲染)方案。

3、控制器与数据处理

控制器作为MVC框架的核心,在整个Node项目中起到了承前启后的作用。每一个具体的controller.js文件,都是用来处理一种特定的请求。控制器会根据请求的不同,拉取对应的数据Model文件,之后对多路数据进行逻辑处理,并封装成HTTP响应返回。

const ipInfoModel = require('../models/ipInfoModel');
const cityInfoModel = require('../models/cityInfoModel');

const getUserinfo = function (ip) {
let result = {};
    result.ipInfo = ipInfoModel.getIpInfo(ip);
    result.dbCityList = cityInfoModel.getCityInfo();

return result;
};

4、数据获取与数据库操作

Node服务器的数据获取操作是通过数据Model文件具体执行的。在美团点评的对外业务中,常见的数据获取方式有两种:

  • 一种是通过我们的Model层直接调取后端Java的服务,这种方式在业务逻辑中使用较多。
  • 另一种是调用公司配置管理平台提供的配置接口,这种方式在进行灰度测试中使用较多。

样例中以第一种方案为例进行了实现,通过调用"淘宝"提供的公共api来模拟我们实际的开发场景。

const request = require('request-promise');
const ENUM = require('../common/ENUM');

const getIpInfo = function*(ip) {
const options = {
        url: ENUM.IP_INFO_SERVER,
        method: 'GET',
        qs: {
            ip,
        },
        json: true,
    };
const res = yield request(options);

return res.data;
};

同时,在开发一期监控平台的过程中,项目主要由前端来开发。这就需要Node服务器跳过后端服务,直接对数据库进行操作。在本文中也提供一种简单的方案操作数据库。

const mysql = require('mysql');
const thunkify = require('thunkify');
const util = require('../common/utils');

const getCityInfo = function*() {
const pool = util.creatMysqlPool();

const conn = yield thunkify(pool.getConnection).call(pool);
const querySQL = 'SELECT * FROM `test_model_success` WHERE 1';
const result = (yield thunkify(conn.query).call(conn, querySQL))[0];

    conn.release();

return result;
};

5、服务器日志打印

作为生产环境的项目,出了问题需要能迅速排查原因,因此增加服务稳定性监控和出错记录是必不可少的环节。目前,美团点评服务系统配套了专门的监控组件进行错误记录与上报,而读者也可以使用 PM2 或者 log4js-node 进行错误日志输出记录,本文不在赘述。

这里提一下样例里使用的一个Koa日志中间件Koa-logger,该中间件可以在开发的过程中实时监听请求,并在控制台实时打印日志。启用也十分方便,只需在项目入口文件app.js里配置"app.use(logger());"命令即可。其打印日志效果如下图所示:

四、不适合前后端分离的业务

商业产品,业务为主,永远由业务来决定技术。同时,在软件工程领域也不存在银弹,没有最好的技术,只有最适合当下业务的技术方案。因此最后我们也列出部分不适合前后端分离的情况以供读者参考:

  1. 简单的内部管理系统,这样的系统不需要前后端分离,否则只会白白增加工作量。
  2. 对于无需频繁更改的业务,也可以考虑使用更加传统的开发方式。
  3. 对小公司或者创业公司来说,初期可以不用分的这么细,毕竟使用越复杂的方式,对团队成员的技术水平也会要求更高。

五、总结

目前,前后端分离已在美团点评各条业务线上被广泛采用。我根据自己的理解结合点餐团队的具体业务对我们的分离方式进行了总结。由于水平与篇幅限制,难免会有不足之处,欢迎斧正。

编辑于 2018-02-06 11:54

文章被以下专栏收录