再谈前后端分离开发和部署
前后端分离开发已成为业界的共识,但分离的同时也带来了部署的问题。传统web模式下,前端和后端同属一个项目,模板的渲染理所当然由后端渲染。然而随着node的流行,以及webpack的模块化打包方案,让前端在开发阶段完全有能力脱离后端环境:通过本地node启动一个服务器,搭配Mock数据,马上就可以进行业务开发了。
但是到了部署阶段,问题也就显现出来:前端最后打包出来的js,css以及index.html,到底放在哪里?静态文件js,css或者图片,我们还可以在CI阶段上传到cdn服务器上,但是最后的html模板index.html
一定需要放在一个服务器上,然而这个服务器到底由前端还是后端维护?
前端维护HTML
如果html模板由前端维护,那么前端就需要自己使用一个静态服务器:提供HTML
的渲染和API
接口的转发。常见的单页应用,也是推荐使用Nginx进行部署。
使用Nginx部署,这里又分两种情况:
- 静态资源完全由Nginx托管,也就是js,css和index.html放在同一个
dist
目录里。在这种情况下,webpack的publicPath
一般不用特别设置,使用默认的/
即可 - 静态资源上传CDN,Nginx只提供
index.html
。在这种情况下,webpack的publicPath
要设置成cdn的地址,例如://static.demo.cn/activity/share/
。但这又会引发一个问题,由于qa环境,验证环境,生产环境的cdn地址通常不同,为了让index.html
可以引入正确的静态文件路径,你需要打包三次,仅仅为了生成三份引用了不同路径的html(即使三次打包的js内容完全一样)
nginx配置
server {
listen 80;
server_name localhost;
location / {
root /app/dist; # 打包的路径
index index.html index.htm;
try_files $uri $uri/ /index.html; # 单页应用防止重刷新返回404,如果是多页应用则无需这条命令
}
location /api {
proxy_pass https://anata.me; #后台转发地址
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
}
}
理论上qa,yz,prod环境的接口转发地址也不同,因此你还需要三份nginx.conf
配置
后端维护HTML
很多情况下,我们需要渲染的页面带上后端注入的动态数据,又或者页面需要支持SEO,这种情况下,我们只能把模板交给后端渲染。那么后端维护的html模板怎么获取打包后的hash值呢?
- 前端打包后的
index.html
直接发给后端(简单粗暴,并不推荐) - 前端打包时通过插件
webpack-manifest-plugin
后生成一个manifest.json
文件,该文件其实是个key-value的键值对,key代表了资源名称,value记录了资源的hash
{
"common.css": "/css/common/common-bundle.804a717f.css",
"common.js": "/js/common/common-bundle.fcb76db9.js",
"manifest.js": "/js/manifest/manifest-bundle.551ff423.js",
"vendor.js": "/js/vendor/vendor-bundle.d99dc0e4.js",
"page1.css": "/css/demo/demo-bundle.795bbee4.css",
"page1.js": "/js/demo/demo-bundle.e382801f.js",
}
后端的index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>demo</title>
<link href="<%= manifest['page1.css']%>" rel="stylesheet">
</head>
<body>
<h1>demo</h1>
<script src="<%= manifest['page1.js'] %>"></script>
</body>
</html>
后端通过读取这个json
文件,就可以动态渲染出文件的引用路径。
如果你曾经用过百度的打包工具FIS,它最后打包产出的map.json就是类似的资源文件列表。
使用这种方法还有一个好处:前面我们说过,如果文件上传至cdn,那么前端维护的html可能需要打包三次,因为不同环境的cdn地址不同。现在html交给后端维护了,那么这个问题就很好解决,前端只需要打包一次,不同环境的cdn地址可以让后端动态拼接生成。
当然,使用这种方法也会带来一个问题,这个json文件,后端怎么获取?
- 把这个json文件和其他静态资源一起打包上传到cdn上,后端服务器每次启动时,先到cdn上获取这个json文件,然后存到内存里
wget --tries=3 --quiet -O manifest.json http://static.demo.cn/demo/manifest.json?`date +%s` ## 防止缓存
- 方案的优点:简单方便,每次前端打包,
manifest.json
就会自动更新,上传到cdn同时覆盖前一个版本。 - 方案的缺点:如果
manifest.json
更新了,后端则需要重启服务以便获取新的配置,当集群多的时候,重启耗费的代价可能很大。
2. 将manifest.json
的内容放在配置中心里,后端则需要接入配置中心。每次CI打包后,调用配置中心更新接口,后端就能自动获取最新的配置。
在我平时工作项目中,这两种方案均有实现。
Node中间层
使用Nginx部署时,为了解决跨域问题,我们一般需要配置proxy_pass
指向提供api的后端服务。
后端采用了SOA,微服务的架构时,proxy_pass
指向的api服务器,其实本质也是一个转发服务。
前端ajax请求
// 获取商品列表
ajax.get('/api/queryProductList')
// 获取价格列表
ajax.get('/api/queryPriceList')
Nginx转发
location /api {
proxy_pass https://demo.com; #后台转发地址
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
}
接口转发到 https://demo.com/api/queryProductList 和 https://demo.com/api/queryPriceList
查询商品列表和查询价格列表其实是由两个不同的soa服务提供:
- 查询商品:
product.soa.neko.com
- 查询价格:
price.soa.neko.com
因此,本质上https://demo.com
这个服务也只是用来转发接口,同时对数据做部分的组装。那么这个服务,就可以用Node中间层来替代。使用了Node中间层,模板的渲染也可以从Nginx转移到Node了。
当然,多了一层Node,对于前端的综合要求也随之提高,后端的部署,监控,日志,性能等等问题也随之而来,全栈(干)工程师应运而生。
工作现状
我司大部分to C的前端项目,都是采用Node层渲染模板加转发接口的开发模式,还有少量项目采用Java tomcat渲染html模板。
大部分页面都是多页应用,并不是典型的单页应用。
Node层渲染模板,又分两种情况:
- 需要支持SEO,则采用传统的模板渲染,填充展示数据。但是JS的业务代码,依旧前后端分离,并不在Node项目里。这类页面,一般都是采用Jquery+webpack模块化打包。
- 不需要支持SEO,则Node只渲染一个空html模板,页面内容完全由JS生成。这类页面,一般采用最新的前端MVC框架,比如Vue和React。
当然近几年比较流行的SSR方案,让Node渲染模板时可以直接使用Vue和React的同构组件,直出页面后,用户的交互体验又如单页应用般流畅,只能说:历史总是惊人的相似。
从某种程度上说,SSR是一种向传统模式的回归,不过这种回归并不是倒退,而是一种螺旋式的发展。
实战
理论知识讲了那么多,现在我们来实战一下。在上一篇
深红:webpack多页面打包实践里,我介绍了webpack多页打包的原理,同时搭建了一个简单的webpack4-boilerplate。这个模板只是一个前端开发模板,其实它还对应着一个node后端模板koa2-multipage-boilerplate。
这个node项目最重要的就是实现了前面说的:如何读取manifest.json
文件,动态渲染静态文件的引用路径,从而前后端分离开发和部署。
详情见chunkmap.js这个koa2中间件的源码。
const chunkmap = require('./chunkmap');
app.use(chunkmap({
staticServer: '//0.0.0.0:9001',
staticResourceMappingPath: './mainfest.json'
}));
这个中间件接受两个参数
- staticServer:静态资源服务器地址,本地开发时,填写的就是
webpack4-boilerplate
这个前端项目启动的服务器。到了qa,产线时,则填写真正的cdn地址 - staticResourceMappingPath: 资源映射文件路径,也就是
manifest.json
文件
本地开发时的manifest.json
,不带hash值
{
"home.css": "/css/home/home-bundle.css",
"home.js": "/js/home/home-bundle.js",
}
打包后的manifest.json
,带hash值
{
"home.css": "/css/home/home-bundle.d2378378.css",
"home.js": "/js/home/home-bundle.cb49dfaf.js",
}
使用了这个中间件后,koa的ctx.state
全局变量上就带有一个bundle
属性,里面的内容是:
{
"home.css": "//0.0.0.0:9001/css/home/home-bundle.d2378378.css",
"home.js": "//0.0.0.0:9001/js/home/home-bundle.cb49dfaf.js",
}
然后通过模板引擎,动态渲染出实际页面。当然你也可以在页面中动态生成展示内容,从而支持SEO。
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<title><%= title %></title>
<link href="<%= bundle['home.css']%>" rel="stylesheet">
</head>
<body>
<div id="app"></div>
<script src="<%= bundle['home.js']%>"></script>
</body>
</html>
总结
前后端分离带来了工作效率上的提高,Node中间层则给前端打开了一条走进后端的道路。当然机遇总是与挑战并存,在前端技术日新月异的今天,真想说一句:老子学不动了!
转发:https://zhuanlan.zhihu.com/p/110038951