哔哩哔哩 (B 站) 前端进阶之路

转载声明:本文转自【架构文摘】
原文链接:http://mp.weixin.qq.com/s/1KgFDYKyZm3KTVOsJ2NhGw

2017 年已经过去了,总结一下 B 站的前端进阶之路。

过去的开发模式中,我们采用了以后端为主的 MVC 架构方式。具体来说,每次项目评审后,前后端会先一起约定好接口,之后分别进行开发,开发完,前端需要把页面提供给后端,后端配置上数据,然后返回出来。正式基于这样的开发模式,导致了总工作量的增加,同时沟通和联调成本的消耗也十分显著。

前后端分离

为了摆脱这种前后端过分依赖的情况,(其实前端也不想每次修改或者发布都要后端这边发布,后端也不想每次前端只改个标题,都要发布一下, 影响服务的稳定性),那么先从前后端分离开始吧 ~

前后端分离,最基本的两种模式,有中间层和没有中间层。

第一种,没有 web 中间层就很简单,提供一个 html 模板放到静态资源机上面,html 模板里面引用了所需的 js 和 css,访问页面的时候 把这个静态模板返回给用户,然后执行 js 在浏览器端通过 ajax 请求 api 拿到数据,渲染页面。
c1cd6b21781046ea80fae9e5fe9b238a.png

第二种,有 node 中间层,随着 2009 年,Node 的横空出世,把前端慢慢的推向了后端,有了 node 之后,JavaScript 可以做更多的事情。

B 站,一开始做前后端分离的时候,也确实按照第一种方式去做的, 现在还有一些页面仍然是这种模式, 例如:https://www.bilibili.com/account/history (可查看网页源代码)。对于不需要 seo 的页面来说,是一个不错的方式。前端开发完成之后,通过 webpack 打包出对应的 js 和 css 上传到 cdn 上面,然后将 webpack 打包出来的 引用了对应的资源的 html 文件 上传到一台专门的静态机上面,然后运维配置路由 将页面流量导过去就好了。后端的同学只需要提供对应的 api 接口就可以。前后端分开维护,自己按照自己的节奏走,降低了页面与服务的耦合度

这种方式确实是一种很快能够进行前后端分离的方法。我们花了一段时间,在 pc 端使用 vue 进行重构,移动端 H5 端 用 react 进行了重构。 进度很快,但是也慢慢展现出了弊端。

首屏的时候,因为他要等待资源加载完成,然后再进行渲染,会导致了首屏有白屏,如果是单页面还好,如果是 spa 应用 那么 他的加载时间就会变得很长,白屏时间会很影响用户体验,再有就是由于国内的搜索公司 对于 spa 应用没有很好的兼容,导致了客户端渲染会对 seo 非常的不友好,有 seo 需求的页面就很迫切的需要服务端渲染。
920cd4eab4754a3e9815213e31395fe6.png

那么,依赖 node 进行服务端渲染就被提上了日程。

选型

首先进行 node 框架的选型,市面上主流框架有三种,hapi express koa , 还有一些是经过一些封装和定制的框架,例如:eggjs 等。

一开始我就把 eggjs 排除在外了,第一因为 eggjs, 的功能很强大,有很多功能,多到有些根本用不着,从而导致了他会重 不轻量级,第二,eggjs 对于我来说是个黑盒,如果有什么问题,我解决起来将会花费很长的时间。(但是有很多地方 我还是借鉴了 eggjs 的,毕竟 很强大)

然后剩下的三种框架,express 的使用相对简单,文档也比较多,比较全面,所以我就选择了 express(后来还是重构掉了 = =!)

然后是前端框架的选型 因为前端框架主流的有很多,ng r v 等等,我站在用的是 react 和 vue, 他们有个优势就是可以进行前后端同构,一样的逻辑不用写两份,很棒。
77693cd39a0d403499efca0ffcd6f317.png

由于之前前后端分离的时候,pc 上面已经再用 vue 进行了重构,所以自然,这次服务端渲染也建立在 vue 上面 用的是 vue ssr (这也为我后面的一个想法埋下了伏笔)

首先 我们选择一个简单的页面来做打样,就用 tag 页吧(被神选中的孩子:https://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, {
… })

构建配置也是用的推荐的配置(参考:https://ssr.vuejs.org/zh/build-config.html)

简单来说,就是提供两个入口,一个 entry-client.js, 主要是客户端的执行入口, 打包出来的是客户端的引用代码集合(manifest),另外一个是 entry-server.js 打包出来的是服务端运行的逻辑, 整合到了 bundle.json 里面。然后传给上面的 createBundleRender 方法就可以了

对于 server 文件夹里面的逻辑就非常简单了,core 里面是启动项目的一些 express 的核心代码 路由注册什么的逻辑,值得一说的是,这边的路由,借鉴了 eggjs 的路由注册方式,稍微做了一点修改,用的是配置化的方式
6a0285dae2a84af5bf0a4e94bc388551.png

配置优于代码,将访问地址和对应的 controller 做了关联。

这边还有一个 filter 其实就是在执行 controller 之前 注册进一个 middlewares 优先执行(其实这边有点局限性,后处理没法做)。

这边我忽略了压力测试,压力测试我后面再说吧。

上线部署

上线部署用的是 docker 来部署的,配置是 1C 4G 的配置,用了两个实例来运行,(之前的构建镜像逻辑什么的 就不具体介绍了)

上线之后 每天的访问量大概在 100W 左右,服务表现挺稳定,期间出现了一个 bug, 就是 这边有一个状态与用户的登陆状态有关,所以在服务端请求接口的时候,需要带上 cookie 去请求,当时忘记加了 后来加上,发现这个有点弊端比较麻烦。

需要在调用 vuesssr 的时候带在 context 里面,然后 asyncData 方法里面都要一层一层的传递,最后在 action 里面拿到,带给 api。

这时候 我们再来看下 tag 页。
81decf669c3f43f6ae0067e39b6f8aec.png

重构

其实也没过多久,大概三个月吧,node 的版本涨的很快,在 7.6 版本之后,node 就支持了 async/await 语法糖,不需要再用 yield 和 * 函数了,那么 无疑 koa 是对于 await/async 支持最好的,我们果断放弃了 express, 选择了 koa2 进行重构。

其实不单单是 koa2 对于 async 的支持,另外一个原因在于,我们 koa 是洋葱式的执行方式,这样就解决了上面我说的,只有 controller 的前处理,没有后处理,这样子我就可以很方便的去执行前后处理。Koa 的执行效率也要好于 express.

上面我说过,选择 vue 对后面重构埋下了一个伏笔就在这里

首先,我给项目接入了配置中心,配置中心是干嘛用的呢? 用来记录脚本的版本号,这样子我就可以很轻松的通过配置中心来控制前端页面使用什么版本的脚本。而不用因为改了个脚本的版本号,就需要进行一次服务的重启更新。

然后,我对 vue 的打包组件进行了魔改,将他打包出来的文件带上了对应的版本号(版本号为 hash 值)。

这样子我就可以通过配置中心来控制,到底我需要使用什么版本的 vue 构建产物,vue 前端逻辑更新了,我也只需要通过配置中心去分发给服务端,而不需要重启服务了。一举两得。
702643634256461098f654ecdf3750b2.png

图中 conf 就是配置中心,我们的 server 会与 conf 进行一个长连接,如果 conf 中的配置更新了,就会通知到服务,然后服务去拉去新的 bundle 和 manifest 来进行渲染。Ok 很棒

全民 SSR

重构完,那么再接入一个项目试试吧

首页,好,就首页吧

首页跟 tag 页

其实也都差不多,没有什么特别的地方,唯一不同的就是 量比较大,可能一天有 1000W 的访问量左右。那么我们就在 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. 并且得益于配置中心,我们可以将项目分的很散,但是最终又通过配置中心,集中到同一个服务上,又回到了前后端分离上面,但是不止于前后端分离,前端独立开发的同事,还带上了服务端渲染,一举两得。设计架构如图:
8425c0dc0bea47369df1898b6746c785.png

顺带,我们开发了两个脚手架,可以很方便的创建项目,并且加好 webpack 的配置和 package.json 的配置

这样子拆分之后,项目就变得很清真,前端开发前端 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 站发展的很快,前端也有意识的去在意前端性能,让页面更好,更快。

脚步从未停下,我们还在路上!

哔哩哔哩 (゜ - ゜) つロ 干杯 ~