2017年即将过去了,总结一下B站的前端进阶之路css
过去的开发模式中,咱们采用了之后端为主的 MVC 架构方式。具体来讲,每次项目评审后,先后端会先一块儿约定好接口,以后分别进行开发,开发完,前端须要把页面提供给后端,后端配置上数据,而后返回出来。正式基于这样的开发模式,致使了总工做量的增长,同时沟通和联调成本的消耗也十分显著。html
为了摆脱这种先后端过度依赖的状况,(其实前端也不想每次修改或者发布都要后端这边发布,后端也不想每次前端只改个标题,都要发布一下,影响服务的稳定性),那么先从先后端分离开始吧~前端
先后端分离,最基本的两种模式,有中间层和没有中间层。vue
第一种,没有web中间层就很简单,提供一个html模板放到静态资源机上面,html模板里面引用了所需的js和css ,访问页面的时候 把这个静态模板返回给用户,而后执行js 在浏览器端经过ajax请求api拿到数据,渲染页面。node
第二种,有node中间层,随着2009年,Node的横空出世,把前端慢慢的推向了后端,有了node以后,JavaScript能够作更多的事情。react
B站,一开始作先后端分离的时候,也确实按照第一种方式去作的,如今还有一些页面仍然是这种模式,例如:www.bilibili.com/account/his… (可查看网页源代码)。对于不须要seo的页面来讲,是一个不错的方式。前端开发完成以后,经过webpack打包出对应的js和css 上传到cdn上面,而后将webpack打包出来的 引用了对应的资源的html文件 上传到一台专门的静态机上面,而后运维配置路由 将页面流量导过去就行了。后端的同窗只须要提供对应的api接口就能够。先后端分开维护,本身按照本身的节奏走,下降了页面与服务的耦合度webpack
这种方式确实是一种很快可以进行先后端分离的方法。咱们花了一段时间,在pc端使用vue 进行重构,移动端H5端 用react 进行了重构。 进度很快,可是也慢慢展示出了弊端。git
首屏的时候,由于他要等待资源加载完成,而后再进行渲染,会致使了首屏有白屏,若是是单个页面还好,若是是spa应用 那么 他的加载时间就会变得很长,白屏时间会很影响用户体验,再有就是因为国内的搜索公司 对于spa 应用没有很好的兼容,致使了客户端渲染会对seo很是的不友好,有seo 需求的页面就很迫切的须要服务端渲染。web
那么,依赖node 进行服务端渲染就被提上了日程。ajax
首先进行node 框架的选型,市面上主流框架有三种,hapi express koa ,还有一些是通过一些封装和定制的框架,例如 eggjs等
一开始我就把eggjs 排除在外了,第一 由于eggjs,的功能很强大,有不少功能,多到有些根本用不着,从而致使了他会重 不轻量级,第二,eggjs对于我来讲是个黑盒,若是有什么问题,我解决起来将会花费很长的时间。(可是有不少地方 我仍是借鉴了eggjs的,毕竟 很强大)
而后剩下的三种框架,express的使用相对简单,文档也比较多 比较全面,因此我就选择了express(后来仍是重构掉了 = =!)
而后是前端框架的选型 由于前端框架主流的有不少,ng r v 等等,我站在用的是react和vue, 他们有个优点就是能够进行先后端同构,同样的逻辑不用写两份,很棒
因为以前先后端分离的时候,pc 上面已经再用vue 进行了重构,因此天然,此次服务端渲染也创建在vue上面 用的是vue ssr (这也为我后面的一个想法埋下了伏笔)
首先 咱们选择一个简单的页面来作打样,就用tag页吧(被神选中的孩子:www.bilibili.com/tag/3503159 )
client 【客户端代码 同构代码】
build 【构建相关】
PC 【pc 端 vue项目】
package.json
config
config.local.js 【本地开发配置】
dist 【构建目录 挂载资源目录】
server 【服务端代码】
controller 【控制器】
PC
route.js
core [核心代码库]
service [方法库]
view [视图]
PC [vue 构建后文件]
tag.html [构建后的模板]
tag.json [构建后的bundle]
manifest.json
apps.js [启动项]
在一开始设计的时候,客户端代码和服务端代码放在同一个git库里面,client里面是vue的代码和webpack的打包逻辑。Server里是服务端的代码,用的是类mvc结构。
Client里面的vue的开发代码,参照的就是vue ssr 官方给的例子来作的,用的是 createBundleRender方法
const { createBundleRenderer } = require('vue-server-renderer') const renderer = createBundleRenderer(serverBundle, { ... }) 复制代码
构建配置也是用的推荐的配置(参考:ssr.vuejs.org/zh/build-co… )
简单来讲,就是提供两个入口,一个entry-client.js,主要是客户端的执行入口, 打包出来的是客户端的引用代码集合(manifest),另一个是entry-server.js 打包出来的是服务端运行的逻辑,整合到了bundle.json里面。而后传给上面的createBundleRender方法就能够了
对于server文件夹里面的逻辑就很是简单了,core里面是启动项目的一些express的核心代码 路由注册什么的逻辑,值得一说的是,这边的路由,借鉴了eggjs的路由注册方式,稍微作了一点修改,用的是配置化的方式
这边还有一个filter 其实就是在执行controller以前 注册进一个middlewares 优先执行(其实这边有点局限性,后处理无法作)
这边我忽略了压力测试,压力测试我后面再说把
上线部署用的是docker 来部署的,配置是1C 4G的配置,用了两个实例来运行,(以前的构建镜像逻辑什么的 就不具体介绍了)
上线以后 天天的访问量大概在100W左右,服务表现挺稳定,期间出现了一个bug,就是 这边有一个状态与用户的登录状态有关,因此在服务端请求接口的时候,须要带上cookie去请求,当时忘记加了 后来加上,发现这个有点弊端 比较麻烦
须要在调用vuesssr的时候带在context 里面,而后asyncData方法里面都要一层一层的传递,最后在action 里面拿到,带给api
这时候 咱们再来看下tag 页
(不错 把数据都带上了)
其实也没过多久,大概三个月吧,node的版本涨的很快,在7.6版本以后,node 就支持了async/await 语法糖,不须要再用yield 和*函数了,那么 无疑 koa 是对于await/async 支持最好的,咱们果断放弃了express,选择了koa2 进行重构
其实不仅仅是koa2对于async的支持,另一个缘由在于,咱们koa 是洋葱式的执行方式,这样就解决了上面我说的,只有controller的前处理,没有后处理,这样子我就能够很方便的去执行先后处理。Koa的执行效率也要好于express.
上面我说过,选择vue 对后面重构埋下了一个伏笔就在这里
首先,我给项目接入了配置中心,配置中心是干吗用的呢? 用来记录脚本的版本号,这样子我就能够很轻松的经过配置中心来控制前端页面使用什么版本的脚本。而不用由于改了个脚本的版本号,就须要进行一次服务的重启更新。
而后,我对vue的打包组件进行了魔改,将他打包出来的文件 带上了对应的版本号(版本号为hash值)
这样子我就能够经过配置中心来控制,到底我须要使用什么版本的vue 构建产物,vue 前端逻辑更新了,我也只须要经过配置中心去分发给服务端,而不须要重启服务了。一箭双雕。
重构完,那么再接入一个项目试试吧
首页,好,就首页吧
首页跟tag 页 其实也都差很少,没有什么特别的地方,惟一不一样的就是 量比较大,可能一天有千万级的访问量左右。那么咱们就在CDN上面加上一层缓存,而后在咱们服务上面也加上一层缓存。破费(perfect)!~
服务端的缓存是经过文件落地来的,就是在第一个请求进来的时候 在渲染完成以后,写一个文件到本地,而后下次访问的时候就能够直接用这个丢这个本地文件出去,不用再次渲染了,而后经过过时时间去控制。
这里发现了一个问题,就是每次更新 我都会将tag 和index 都进行打包,而我须要的是对项目进行单独的打包,单独的更新,能不能经过参数来控制我打包哪一个呢,能够啊,首先先把webpack.config.js 重写,公用部分整合,而后私有的分开写成多个,经过package.json里面来多配置几个script就好啦
这样子每次更新项目的时候,我就只须要打包对应的项目就能够了,不会由于项目接入了不少以后,打包和开发时候的热加载变得很慢很慢。
因为接入了两层缓存,首页上线的时候,咱们把服务从2个docker实例 扩容到了6个(docker扩容真方便),得益于缓存的优点,服务并无什么压力
固然 首页不可能像说的那样,这么随便就上线了,须要有降级方案,那么降级方案得益于vue的强大了.
Vue 会在浏览器端检验(data-server-render=true),是否服务端渲染了,若是服务端没有渲染,那么客户端会再执行一次逻辑进行渲染。这样子咱们只要再打包的时候,将本来客户端渲染的那个index.html 保留就能够拉,固然别忘了,再客户端执行的时候也要运行一下asyncData里面的方法,否则会缺乏数据哦。So easy~
接下来 一级分区 二级分区也分别都接入了,中间也遇到了一些问题,不过最后都顺利的解决了,后面有机会我再写一篇文章来讲一下其中遇到的问题。
咱们的项目在有序的进行着从本来静态页 客户端渲染,往服务端渲染迁移的同时,咱们也在公司内部进行这推广,有几个兄弟部门也遇到了咱们以前的seo 的问题,或者是但愿首屏更快等,因此很愿意使用咱们已经造好的轮子。但是咱们的项目暂时并不具备推广性,若是兄弟部门要使用,只有把咱们的库拷贝过去,而后把业务逻辑删减掉,再加上本身的逻辑,成本很高,并且咱们这边一旦更新了什么,他们都须要手动去同步,就很麻烦。
咱们花了一点时间,首先,core 核心库抽离出来,而且和日志中心的链接方法、配置中心的链接方法等一些公用方法一块儿,作成一个npm包 发布到公司内部的npm 源上面,而后将client 从库里面独立出来,变成前端库,加上一个简单的server.js,能够独立于server 进行开发,而不用在开发的时候过度依赖node server.而且得益于配置中心,咱们能够将项目分的很散,可是最终又经过配置中心,集中到同一个服务上,又回到了先后端分离上面,可是不止于先后端分离,前端独立开发的同事,还带上了服务端渲染,一箭双雕。设计架构如图:
这样子拆分以后,项目就变得很清真,前端开发前端vue项目,服务端有npm包可供你们使用,升级和维护都很方便,node服务也不须要一直去重启,经过配置便可更新逻辑,热更新。
作完以后,不少兄弟部门也都开始了接入。
由于每一个公司的状况都不同,使用组件缓存,页面缓存等等方式,均可以达到优化的目的,使其能够达到能承载项目流量的标准,我这边说的状况是没有任何缓存的状况下的压测结果。
咱们作过几回不一样层面的压测,毕竟性能须要达到要求才行,记得当时出版打样上线的时候,VUE使用的版本是2.3.x 性能不是很好,由于VUE是基于虚拟DOM(VNODE)来实现的,是CPU密集型的项目,因此在压测的时候,CPU很快就达到了100%,TPS很低,因此咱们对页面加了缓存,像首页这种P0级页面都加两层缓存,后来VUE更新到了2.4.x 性能变好了许多,可是CPU始终是一个瓶颈。若是项目复杂,组建嵌套不少的话,1C4G的服务器,CPU打满也就40到50的TPS就封顶了,再上去,用户等待时间就会呈指数式上升。
我看过不少文章,拿vuessr和字符串模板进行比较的文档,可是他们的比较demo都很简单,vue里面都没有组件嵌套,性能相比可能确实差很少,可是页面复杂度上升,组件嵌套越多,那么vuessr的性能就无法再跟字符串模板进行比较了
举个例子把,咱们首页一二级分区天天打到node上面的量跟文章的量差很少,可是文章就用了首页三分之一的机器,机器的cpu和内存使用量差很少,由于文章项目用的是字符串模板。
在整个的过程当中,须要前端同窗,后端同窗的通力配合才行,后端api的同窗须要将本来直接结合模板出数据的方法所有改为api接口,这是先后端分离的基础。至于基础建设,能够慢慢发展来完善,就像一开始咱们构建的时候,构建出来的配置文件的版本号都是须要手动去配置到配置中心的,这很耗时,并且容易出错,慢慢的,配置中心开放出了api接口,咱们接入就很方便了,顺利的实现了配置同步的自动化,只要上线的时候点一下发布就行了。
在用node作中间层的过程当中,也有遇到内存泄漏,性能瓶颈等问题,后面有机会,再写篇文章介绍吧。在这一年中,B站发展的很快,前端也有意识的去在乎前端性能,让页面更好,更快。
脚步从未停下,咱们还在路上!