「 基础设施 」现代化前端工程发布

引子

接手八年没重构的老项目,每个字符都透露着远古时期历史的气息,之前的你可能不懂那些性能优化上的思考,看了这种古老的项目,你马上就能get到现代文明之光的美好。

重写不是没想过的,一万次深呼吸准备大干一场,一万次管住了自己爪爪。

你还年轻,谁知道哪个页面是需要用的,哪里会藏着深坑。

加一些打点统计,熟悉代码好几年,终于可以开始重构了,但是当然不能操之过急,一下推倒重来,我们选择从新增页面开始,再慢慢将老的部分迁移到新的现代化的开发模式。

嗯,新项目开始啦。我们简单的选择vue+webpack+babel,完全前后端分离的开发模式,代码层面不深入展开来说,今天只讲前端静态发布。

vue-cli生成新项目,npm run buid得到dist目录。index.html(下文以入口文件相称),还有css,js等(下文以静态资源相称),前端说来说去就是HTML,js,css三剑客,让我们开始部署吧。

部署

部署有啥好说的,全丢到静态服务器上,用户可以访问到就可以了呗。

各种404,路径没配置好,改下vue.config.js的publicPath。

1
2
3
4
5
6
7
8
9
10
module.exports = {
publicPath: process.env.NODE_ENV === 'production' ? '//static.evacoder.com/userCenter/' : '',
devServer: {
disableHostCheck: true,
// 所有匹配不到的请求都映射到此域名下
// 1.配置host 127.0.0.1到 evacoder.com
// 再将localhost:8080 nginx代理到 127.0.0.1:80 就可以绕过登录态了
proxy: 'https://evacoder.com',
},
};

ok~再部署一把。

哦?可是我们静态服务器的域名是static.evacoder.com。

Access to XMLHttpRequest at ‘https://evacoder.com/getData‘ from origin ‘https://static.evacoder.com‘ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.

又见到了我们的老朋友跨域,毕竟你不是native开发,不用管跨域的问题。

部署路径

妥协吧,把dist文件夹直接丢到server端项目里吧,或者静态资源上cdn,至少把入口文件丢在人家的地盘下。

将入口文件放在server端的项目主要有下面这些考虑:

  • 跨域

静态项目发布之后一般都需要另一个域名来访问,比如static.evacoder.com。用户通过static.evacoder.com来访问的话会很丑陋很奇怪。

更重要的是api接口在evacoder.com下会有跨域的问题。所以一般index.html放在server端的项目下就不会跨域了。

但是实际上可以通过nginx的配置来解决这个问题,通过proxy_pass反向代理一下,将static.evacoder.com/center/index反向代理到evacoder.com下就不会跨域了,可以解决这个问题。

nginx可以解决很多问题,感兴趣查看这篇(TODO,nginx加成,提升前端开发效率)。

  • 上线先后

是有可能存在这样的需求,【修改某个api参数sex从0,1,2改为male,female,unknown】。

如果前后端代码放在一起的话,开发完这个功能一起上线即可。

(一起上线的意思是:因为代码放在一起,后端是一台一台机器的上线,比如有5台机器,上线的过程中上到第2台机器的时候,第2台机器就会被摘掉,用户只能访问到剩下4台,第2台机器自己就在后面悄咪咪的部署,部署完成了之后又回到用户可访问的状态,不会出现FE上完了,api没上完的情况)

但是如果动静分离部署的话,就会存在谁先上线的问题。

如果server先上线,FE还没修改,参数不匹配,会400错误。

如果FE先上线,server不能接受新改的参数。

一个功能需要前后端协同开发,功能上有依赖,如果不能一起发布的话,总是会出现用户暂时访问有问题的情况。

正常情况下我们不能放弃任何一丝流量(当然也是为了自己,免得为了降低影响眼圈黑黑半夜上线)

一般情况下只能server端做api参数兼容了,觉得冗余的话之后再去掉兼容的代码。

(其实倒也不用想这个问题,前后端分离的好处之一就是:后端提供的接口也可以给提供给native客户端使用,他们的代码一发出去就永远在用户手中,总是会有用户没有更新版本,后端的兼容一定必须存在)。

  • 回滚先后

同理,分离部署还涉及到分离回滚。反正如果有bug,谁先回滚谁后回滚还需要考虑吗?

还是刚刚那个例子,将【修改某个api参数sex从0,1,2改为male,female,unknown】的功能回滚。

希望后端上次的api兼容没有删掉,就可以让前端先回滚。如果删掉了,只能开发之后再回滚了。

为了避免这种上线回滚先后的问题,我本来是希望能把前端打包的入口文件放在服务端来拒绝任何一点流量损失,但是我们经常会遇到前端只修改一个js,css甚至只是一行文案而已,都需要协同后端编译打包整体上线。

本来是希望如果是简单的修改,就直接把前端入口文件index.html scp到线上机器,如果是前后端有依赖性的修改,则可以同时部署。

但是上线有规范,操作不安全,没能达成希望,再考虑到下面分离部署的优点:

1.前端把握主动权,分离部署不需要求后端同学帮忙部署了,充满了前端自豪感嘻嘻。

2.保持服务稳定性,前端改一行文案都需要经过后端100台机器重新上线实在太不安全了。

3.上线快,前端经常会被催着改行文案赶紧上线,静态部署分分钟就可以完成,而后端上线却需要很多台机器需要很长时间。

4.权责分明,谁的问题谁负责。

商量一下,后端同学回想起帮我们上线个文案要点100台机器的痛苦往事,肯定还是会选择动静分离部署,接口兼容这种比较少见的情况之后再说吧。我们还是选择了眼前的利益,先不过度设计,分开部署。

一般的需求,后端先上线接口,前端再直接发布。

服务端渲染首屏直出

是否有必要SSR

  • 直出:数据填充好再返回

对于不需要数据填充的呢?

传统的后端是数据服务端

中间层服务端去数据服务端请求数据,然后渲染完整的页面

分离是分工,让每个人做自己专业的工作,发挥最大的作用,而不是为了分离而分离。

这里说的首屏渲染速度并不是说页面加载的速度

服务器渲染HTML,是因为能够提高用户第一次访问网站的感知性能,第一个HTTP请求就能够返回可以渲染完的HTML,用不着浏览器端多一个AJAX请求去获取数据再渲染,这样用户就会感觉到这网站性能挺快。

这里重点在于展示的数据是ajax请求返回还是后端直接填充

现在的服务端渲染和以往的不一样的可能是,之前是脏乱的服务端填充模版引擎,现在依然是返回json数据前端来解析,分工依然很明确,只是从ajax转成了后端直接返回数据。

不该SPA的也强行SPA,好在我们有code split & aync loading

SEO 本质是一个服务器向另一个服务器发起请求,解析请求内容。但一般来说搜索引擎是不回去执行请求到的js的。也就是说,如果一个单页应用,html在服务器端还没有渲染部分数据数据,在浏览器才渲染出数据,而搜索引擎请求到的html是没有渲染数据的。 这样就很不利于内容被搜索引擎搜索到。 所以服务端渲染就是尽量在服务器发送到浏览器前 页面上就是有数据的。

部署系统做了哪些事

我司的部署系统封装了puppet等开源项目,在git项目中加上deploy文件之后,部署系统就可以从git上拉代码,(其实就是创建了用户,git clone下来),编译的时候选择git版本进行编译。

deploy文件写明了copy git项目到哪个机器上。

发布机提供了docker image,来提供稳定性和编译能力,可以选择安装node,npm,nginx等。

1.git项目添加部署文件(deploy文件)

2.创建部署任务,方便权限控制和追溯。

基本上可以理解为在线上机器上执行以下操作

1
2
3
4
5
6
7
8
9
git clone git@git.n.evacoder.com:demo/eva-blog.git
cd eva-blog
# 更新代码
git pull
# 部署不同的分支及版本
git checkout v2
npm run build
# 将内容copy到线上路径
mv dist ../online/dist

缓存设置

静态发布很重要的一个考量是缓存设置。

静态资源因为webpack生成了版本号,可以全部都设超长缓存时间。

我们不仅仅可以使用webpack的基础功能,分离第三方资源成vendor.js,对于稳定的功能,也可以在webpack中抽离出来,分成经常改的文件和不经常修改的,保证最优的缓存策略。

但是入口文件呢?静态资源更新的再好,入口文件有缓存,新版总是触达不到用户。

html相比于后端return的jsp会有缓存?

no,其实后端return的jsp也是设置之后才禁用了缓存,对于html,我们可以加上下面的meta信息:

1
2
3
<meta http-equiv="pragma" content="no-cache">
<meta http-equiv="Cache-Control" content="no-cache, must-revalidate">
<meta http-equiv="expires" content="0">

经常听说前后端分离不利于SEO,

no,没有差别,甚至因为分离之后,访问的url更清爽越短越利于seo。

覆盖or非覆盖式发布

一般情况下我们都是覆盖式发布,比如发布 http://evacode.com/static/a.js?v=1.0 ,每次都覆盖原本的a.js,为了避免缓存,每次上线修改版本v来避免缓存。

如果使用webpack,前端修改之后npm run build,如果相关文件有修改,webpack生成的hash值会改变,这样就有最优的缓存策略。

每次生成的静态资源的路径不一样,那每个版本(至少上一个版本的js,css)需要保留下来吗?

当然是为了回滚考虑,如果新build出来的有bug怎么办?

如果使用非覆盖式发布,所有build出来的静态资源都放着,时间久了会很耗费资源,唯一的好处是方便回滚。

如果使用覆盖式发布,回滚是不可能回滚了,因为原来的版本已经被删掉了,但是反正有git来版本管理,可以选择1.轻松revert之后再部署,2.徒手改bug之后重新build后部署。

我司的部署系统是与git关联,可以直接选择对应的git版本,都不需要手动再次revert代码git提交,更没有必要选择非覆盖式发布了。

前人使用的是和git commit号一致的版本号来非覆盖式发布,实际只需要210kb,但是存储以前的版本7年了,现在占用的磁盘资源有4.5g了,非常的浪费,而且年代久远也不敢贸然删除,非覆盖式其实意义并不大。

覆盖式发布并没有太大的影响,而且更清爽。

反正是静态资源不需编译,更有机智的小伙伴选择git-hook来发布,merge到master的时候自动覆盖服务器上的文件,瞬间完成部署,回滚时也是git reset下,但考虑到权限控制,查问题追溯方便,专门的部署系统存在肯定有其意义。(虽然仔细想想,仅就静态资源来说,gitlab也能提供这些功能,就是需要绕绕弯,专门的部署系统是为了提供所有语言代码的部署支持的)。

需要使用专门的发布机来build发布吗

还有一个问题是是否有必要去本地化操作,使用发布机来进行npm run build。

我个人觉得package.json已经写明了依赖的版本,所以可能意义不太大。

虽然package.json保证了环境的一致性,但是还是会出现打包出来不一致的问题么?

cdn的使用

如果使用cdn,非覆盖式发布更浪费资源,我们需要将js,css等放到cdn上,修改入口文件的路径。

经常看了很多前端性能优化的方案,回答面试题时头头是道,动不动就上cdn,动静分域名部署等等,实则是纸上谈兵,道理是那个道理,但这次请教sre同学也有了新的思考,落地时真的应该和sre同学探讨一下。

需要除前端之外的同学参与的性能优化方案,都需要评估。

cdn能够提高访问速度,但是有没有容灾方案,收效与风险孰轻孰重。cdn回源带来的抖动也是很严重的,需要评估回滚的方案,不要蹭别人的用,因为别人的流量打满了最后也会导致我们的服务挂掉,分摊成本也要考虑。

http2,ipv6他们都比我们专业,了解背后的故事。

路由的配置

需要配置nginx的还有:route使用history需要后端配置,使用hash会比较奇怪。

参照vue 文档来处理。

Eva wechat
关注Eva的意如小馆,更方便的与我交流