如何部署前端代码
很早之前在知乎上看见一个提问:大公司里怎样开发和部署前端代码?,其中张云龙(fis3作者)的回答十分精彩。但当时由于水平有限,接触到的业务和开发环境也比较简陋。现在公司推行微服务,前端静态资源也基于fis3进行开发环境搭建和部署,算是这个回答比较完整的实现。
现在,是时候思考一下静态资源部署的重要性了。
本篇文章主要用于思考代码的部署,而非完整的前端开发流程:开发->打包->部署。
前端打包思考
服务端代码都部署在公司主机或者云服务器上面,前端代码却需要运行在客户端各种各样的浏览器上面,这种差别决定了前后端部署的差异。
当问到常见的前端性能优化方法,一般的回复即为
- 为了减少http请求,需要将各个模块压缩合并
- 为了减少传输时间,需要压缩代码
考虑到浏览器渲染过程,我们还需要考虑代码加载和运行的时机。
资源打包合并
浏览器的并发请求数目限制是针对同一域名的。意即,同一时间针对同一域名下的请求有一定数量限制。超过限制数目的请求会被阻塞,因此将各个模块打包合并,用来减少文件数量。
在过去的项目中,我先后使用过gulp、webpack、rollup、fis3等多个打包工具。
打包存在的问题在于:如果修改了某个模块的一行代码,则依赖这个模块的所有文件都需要重新打包,最终部署时也需要更新打包后的文件。这种行为看起来并不是很合理,因为如果我们不进行打包,每个模块都通过script标签引入,则只需要更新那个被修改过的文件即可。
这里有一个比较有趣的问题:
参考
静态资源单独使用服务器
一般来说,在部署时,静态资源不会与页面域名保持一致,而是使用一个单独的静态资源服务器域名,该服务器的主要功能就是维护和更新静态资源,不涉及用户状态(cookie、session等),因此请求时不会携带大量的cookie信息,响应速度更快。
此外,独立域名在用来进行feferer验证、防盗链等方面也更加容易。
小结
无论是资源打包合并、还是使用独立的静态资源服务器,都避免不了一个问题:如何将生产代码部署到线上。
一般部署都会面临下面几个问题
手动部署十分繁琐且容易出错
缓存机制导致资源更新不及时,一般会更新资源路径(如增加hash后缀、增加查询参数等)来解决
如果资源和页面需要分开部署,则二者部署的先后间隔内,会影响线上用户
下面先回顾一下在我的职业生涯中所经历过的前端代码部署流程。
经历过的前端代码部署流程
回想整个前端工作生涯中,经历过的前端代码部署大约经历了下面几个阶段。
直接在模板文件引入静态资源
最初在外包公司工作,接触到传统的MVC框架(ThinkPHP
),负责切图及写页面,并为后台提供模板文件和静态资源文件。
在接入模板时,需要将静态资源替换成服务器上对应的的文件路径,如__PUBLIC__
全局变量,用于指向服务器上对应可访问的静态资源文件夹。
开发时没有考虑过打包或者合并代码,部署时也没有考虑过资源加载性能和缓存
webpack打包依赖,合并代码
后面陆陆续续接触到前端工程化的概念,也使用了gulp、webpack等工具搭建前端开发流程。这时候知道了前端项目可以分为单页应用和多页应用,下面是在这两种项目中使用webpack打包的方式
多页应用
多页应用一般采用的都是后台渲染,处理方式是配置多个entry,每个页面输出对应的打包资源
module.exports = {
entry: {
background: PAGE_SCRIPT_PATH + '/background.js',
options: PAGE_SCRIPT_PATH + '/options.js',
popup: PAGE_SCRIPT_PATH + '/popup.js',
piseerPage: PAGE_SCRIPT_PATH + '/piseerPage.js',
},
output: {
filename: "[name].js"
},
}
然后每个模板页面引入各自对应的依赖,由于每次打包可能会涉及到多个页面的修改,
- 如果在输出文件上增加hash码,则每次改动都需要修改多个文件的依赖,相当麻烦
- 如果不在输出文件上增加hash码,则文件的缓存控制比较麻烦
这个问题在之前的项目开发中并没有得到很好的解决,采用的办法是:输出文件名不增加hash码,而是在后台模板引擎上增加统一的引入资源方法,添加参数后缀控制。
下面是在Laravel的blade模板中新增php方法loadCDN
,实现控制多页面应用的资源引入和版本控制的问题
<script src="{{cdn("/mobile/js/page/base.js")}}" defer></script>
if (!function_exists('cdn')) {
function cdn($url = '', $isDev = false){
if ($isDev) {
// 通过上线时修改版本号来控制
return url($url . '?_v=1.2.9');
}
return '//cdn.xxxHostName.com/' . $url;
}
}
单页应用
单页应用包含使用vue、react等框架构建的web app,也包含运营需求用到的较为简单的活动页面,这种项目,一般是一股脑将多个文件进行合并,并在单页模板上引入对应的资源。
web app 一般使用相关框架的脚手架进行开发(vue-cli
,creat-react-app
等),这些脚手架都内置了开发和打包流程,最终输出静态资源文件。一般来说,web app业务较多,打包后的文件体积十分庞大,为了优化页面打开速度,一般会采用下面优化手段(参考:Vue SPA 项目webpack打包优化指南)
- 异步路由,页面组件懒加载,减少初次加载文件的体积
- 库文件打包到vendor.js,配置 externals 使库文件采用cdn加载
简单的活动页面,由于每个活动的逻辑和样式都比较独立,我采用的处理方式是使用html-webpack-inline-source-plugin
将输出的代码(CSS和JS)都内联至页面上,直接跳过静态资源部署的问题,同时通过缓存控制html文件体积过大的问题
new HtmlWebpackPlugin({
template: getPath("./index.html"),
inlineSource: ".(css|js)$",
minify: {minifyCSS: true, minifyJS: true},
})
独立的静态资源服务器
现在公司的业务较多,后端采用微服务架构,为了在多个项目之间共用前端模块,静态资源单独划分了一个项目,基于fis3实现静态资源服务器。下面整理了大概的流程和实现原理
静态资源url
在模板上请求类似资源
http://cnd.xxx.com/statics/combojs?6538b/mod-c6308.js;6538b/zepto.touch.min-1907a.js;bf986/url-a7908.js;bf986/share-0559e.js;6538b/swiper3.08.jquery.min-47b5e.js;
这跟常见的静态资源路径有一些不同,请求的是一个后台路由statics/combojs
,携带了一系列包含依赖资源和版本的请求参数。
在后台控制器中,会解析参数,拆分对应的文件资源,并合并对应文件,返回实际的资源文件。
这里存在的一个问题是如何将请求参数的hash码映射到对应的编译文件,这是通过fis3编译生成的statics_url_hash.json
进行处理的
{
// ...
"ba858": "/statics/h5/poster/js"
}
就会把ba858/index.es6-12c2e.js
转换成实际路径/statics/h5/poster/js/index.es6-12c2e.js
,然后合并文件。合并后的代码在前端,通过mod.js
处理依赖,执行业务逻辑。
合并后的文件一般会做cdn缓存,所以基本上不用考虑磁盘上合并文件的性能消耗。
后台服务同步hash
前面提到根据页面上的静态资源url请求对应的combo服务器,那么埋入页面的静态资源url是如何生成的呢?
后台采用的是node进行模板渲染,并在global全局对象上挂载了IncludeAssets
方法,用于收集模板中的静态资源依赖,模板上的大致使用方式为
{{css_combo_url}}
<%- IncludeAssets('statics/h5/poster/css/index.scss') %>
<%- IncludeAssets('start:statics/h5/poster/js/index.es6.js') %>
{{js_combo_url}}
页面上可能存在多个IncludeAssets调用,这样就可以在后台按需引入需要依赖的模块,此外并判断环境
- 测试环境输出
script
或link标签
- 正式环境输出
combo_js
或combo_css
,携带对应的hashUri
然后通过中间件,在页面完成渲染后通过替换js_combo_url
和css_combo_url
占位符,返回合并后的js与css文件,输出对应实际的资源引用标签。
更新与部署
在静态资源打包完成时,fis3会输出map.json
,表明模块之间的依赖分析。
打包完成后然后将map.json
写入redis,通过redis的发布订阅模式,通知各个后台服务拉取最新的依赖映射。
下次请求页面时,就会同步文件hash,同步最新的依赖文件,拼接出对应的url,然后访问静态资源服务器,就会输出最新的静态资源文件了。
小结
整个模式大概流程如下
- 开发时按模块组织代码,通过fis3打包输出的
map.json
文件,并通过redis将map.json
分发给订阅了静态资源服务器的后端项目, - 后端项目通过模板中的
IncludeAssets
方法收集需要的模块文件,并结合最新的map.json
依赖映射拼接成script或link标签, - 浏览器解析页面时,通过url请求到静态资源服务器,拼接对应的模块文件,返回资源文件。
在这种模式下,考虑下面的改动情景,在部署期间是否会影响线上用户呢?
- 只改动了页面结构,未改动静态资源,直接上线后台服务即可
- 只改动了静态资源,直接发布静态资源即可,
- 由于改动后会更新map.json,生成不同的资源url,返回不同的资源文件,
- 通过增量更新,不会影响线上旧资源,也不会
- 同时改动了静态资源和页面结构,
- 先上线静态资源,同时标记后台服务为待上线状态
- 在部署静态资源期间,页面结构是旧的,返回的静态资源也是旧的
- 完成静态资源部署后,再部署后台服务,此时后台服务为正在更新状态,采用的增量更新,返回的静态资源也是旧的
- 后台服务更新完毕,取消标记,根据新的map.json,生成新的资源url,返回新的静态资源
通过上述操作,避免了在部署期间对于线上用户的影响,也就不需要半夜三更找用户最少的时间段发布代码了。实际上整个流程还有很多优化操作,包括部署回滚、根据url进行资源缓存、redis哨兵等。
总结
最初刚来公司时见到前端开发环境和部署流程,感觉十分复杂,并不明白其中缘由,只能对着文档按规范进行开发。随着接触到的项目变多,逐渐了解到整套开发和部署流程的原理和优势,受益颇多,因此记录下来。
前端并不仅仅只是切图和写页面而已,前端工程化是一个庞大复杂、却又与用户体验相关甚密的问题,需要潜心学习,不断思考才行啊。
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。