年度总结:我是如何搭建百万级前端项目的

您好,我是沧沧凉凉,是一名前端开发者,目前在掘金知乎以及个人博客上同步发表一些学习前端时遇到的趣事和知识,欢迎关注。


在上一篇文章中:React真的那么好用吗?我说了React有多好用,那么这篇文章我就来说一说我是如何搭建一个百万级的React前端项目的。

首先需要说一下,最开始的时候这个项目我是去帮忙的,因为另一个(前)同事没有使用过React和TypeScript,但是我对他说:你学会了React+TypeScript后的路会越走越宽,同时工资也会高很多,所以这个项目就用React+TypeScript进行搭建吧。

于是在我的怂恿下他接受了我的建议,就使用了最新版本(当时是)的ant design pro,拉下来默认就是使用的React+TypeScript。

结果这兄弟学习能力有点弱,经常在公司学习到10点多才回家,然而1个月时间他仅仅做了一个首页,一个页面的列表(这个页面还被我删了重写了…),后面老板看他能力不太行,而且他又到了快转正的时候,就把他开除了,这个时候我就正式开始独自一个人负责这个项目。

你可能会问为什么我不叫老板招人?其实从这个项目还没有开始的时候,我已经在开始面试招人了,结果一直到项目搭建完毕,并且过了1个多月了,都招不到合适的React+TypeScript人才。

我对于React越来越熟悉之后,一个人写项目感觉也能完全应付的过来,其实还有一个原因,就是跳槽的时候谈到独自抗下百万级Web项目,听起来就好像很有分量的样子。

那么为了这个项目,我做了些什么呢?

1. 技术选型

上面已经说到了,这项目最后选择的是:ant design pro这个现成的后台框架。

当时我一共找了20多个后台框架,最后还是敲定了蚂蚁的ant design pro,第一个原因是因为蚂蚁靠谱,不会出现留下一大堆BUG就停止维护的情况,第二个原因是蚂蚁生态齐全,感觉是用React做中后台的一个非常好的选择。

2. 代码定位

大项目一定要有代码定位!不然上千个文件,密密麻麻的文件夹,没有代码定位你只有通过路由去找界面对应的文件,如果界面上有非常多的判断,你很难找到界面上的组件对应的文件。

这里我就得说一说React代码定位神器:react-dev-inspector

真的非常好用,直接看一下下面的官方示例图:

inspector-gif

注:windows上面那个Command键对应的是win键,即CtrlAlt之间的那个键,如果你觉得太难按也没有关系,这个按钮是可以进行自定义的。

3. 请求封装

几乎每个项目搭建的第一步就是请求封装,因为涉及到响应拦截请求拦截以及错误处理等等东西,所以这一步几乎是每个前端项目必须要做的一件事情。

ant design pro是在umi的基础上搭建出来的一套中后台解决方案,所以它里面集成了umi-request,而umi-request提供了非常多的功能,它是一套基于Fetch封装的请求库,至于Fetch,可以看下这篇文章:越来越火的网络请求Fetch和Axios到底有什么区别

那么我在响应拦截和请求拦截以及错误处理里面分别做了什么呢?

3.1 请求拦截

在ant design pro项目目录:src/app.tsx中有个函数叫做:authHeaderInterceptor,它就是请求处理函数,在该函数里面会对请求报文进行拦截,经过处理后返回一个新的请求报文。

因为http是无状态请求,后端无法判断用户在前端是否登陆过,现在通用的一种方式就是:前端在调用登陆接口的时候,后端会把登陆信息(token)返回到前端,前端取到token后将token存储起来(一般都会存到本地),然后在每次请求的时候都将token传给后端。

请求拦截中进行的处理就是将token加入到每次请求的报文中。

3.2 响应拦截

在项目目录:src/app.tsx中有个函数叫做:demoResponseInterceptors。它就是响应拦截函数,响应拦截中一般是做一些响应的统一处理,比如数据处理等。在axios中错误处理通常也是放在响应拦截中进行处理。

3.3 错误处理

在umi-request中,如果后端返回的数据中有success这个字段,并且successfalse的话,那就会进入到错误处理(errorHandler函数)中。

一般情况下,没有登录、账号被禁用、token过期等等情况后端返回的数据中success都为false,遇到这些情况我们可以做相应的处理,比如跳转到首页,让用户重新登录。

4. UI组件样式调整

由于Antd的组件缺乏一些圆角效果,所以在一部分人的眼中看起来就显得比较丑。根据项目负责人的要求我对一些按钮、弹窗增加了圆角效果,并且大量调整了表单中的一些间距,字体大小。

5. UI组件二次封装

特别重要!!!只要是你可能经常使用到的UI组件,尤其是像弹窗,表格这种,推荐都进行二次封装,小项目可能没有必要,但是大项目必须要做着一步。

我最开始的1个多月就没有进行封装,然后负责人提了个需求,让我界面上使用的每个表格都实现一个功能,这个时候我就傻眼了,因为该表格组件已经用了上百次,一个一个的去改明显不现实,所以这个时候我才对表格进行了二次封装。

React+TypeScript二次封装体验是非常好的,因为TypeScript有接口声明,可以让你封装的组件在使用的时候也能够提示组件原有的属性。

因为我封装的时候该组件已经用了上百次,所以文件中使用到的表格都需要一个一个的重新替换成这个新的封装后的表格。

推荐做大项目的时候可能会多次运用到的组件都进行二次封装!这样后面如果要加个什么功能,这个时候你就能从容处理。

6. Hooks封装

6.1 下拉框数据

项目中非常多的下拉框数据都要从后端获取,并且后端返回的数据并不是可以直接用在Antd的Select组件中,需要进行一次数据处理,于是我就封装了一个一个的Hook,在Hook中请求数据并且处理成Select组件可以使用的数据。

如果后端更改了接口,那么只需要改这个Hook就可以了,因为这项目比较大,所以下拉选择的数据非常多。

6.2 通用Hook封装

通用的Hook我封装的不是很多,大概就是一些数据处理的通用方法。

7. 路由处理

在ant design pro中,好像所有的路由都是需要在config/routes.ts中进行声明,目前我没有发现像Vue那种可以直接将路由声明在后端,然后动态挂载的方式。

不过导航栏的数据可以从后端进行获取,获取到数据后再去渲染导航栏。

通过后端传递回导航栏的数据,可以让用户进入到各个界面中,但是这里会有一个问题,就是如果某个路由不在导航栏里面,但是在config/routes.ts中有过声明,那么用户可以通过url直接跳转到该页面。

如果你不想用户去到没在路由中的界面,那么你可以使用ant design pro提供的权限管理功能。

7.1 权限管理

ant design pro中内置了权限管理方案,如果某些界面不想让一些用户看到,那么就可以使用该功能,就我个人的体验来说,该功能还是非常不错的。

因为后端会反你路由数据,你会根据这个数据去渲染导航栏,同时你也可以判断用户想要去的界面是否在后端返回的数据中,如果要去的路由确实存在,但是不在后端的路由数据中,那么你就可以提示用户没有权限访问该路由,从而做到权限控制的效果。

8. 页面权限

src/app.tsx中,有一个函数叫做getInitialState,该函数在项目运行的时候就会运行,并且它返回的值可以在所有的地方进行访问。

也就是有这个特性,所以我将路由请求放在这个函数里面,一旦用户进入到该项目就会从后端请求路由,然后在src/app.tsx中还有一个函数layout,它默认接收一个initState参数,该参数中有一个属性initialState,该initialState中就是getInitialState这个函数的返回值,所以可以使用该属性进行渲染路由。

ant design pro也可以非常方便的做界面权限处理,虽然该项目目前还没有做相关的处理,不过就项目的安排来说,后期应该是会有相关的需求。

9. 重写导航栏

这一步是因为没有办法了,Antd提供的导航栏我样式调整了无数次,产品依然不满意,然后我怒了,重新写了一个。

主要是因为该产品的路由实在是太多了,有些模块下有几十个路由,使用Antd提供的路由可能无法满足需求。

10. 数据流

在本项目中除了路由部分用到了ant design pro自带的简易数据流管理,其它地方统统没有使用到Redux、Dva这一类库。

就我个人的开发过程来讲,React提供的Context完全可以满足某些页面中比较复杂的数据流转,因为在该项目中,没有数据会在其它界面大量使用,就算有,也是重新进行请求,从而获得最新的数据。

11. 对接多个后端

由于每个后端都会启一个服务,不管是为了对接方便还是为了调试方便,你都需要连接到他本地你才能够进行开发工作,最重要的是,后端有时候要用你的服务进行测试,因为一些表单项前端一般会获取相关的数据,直接供用户选择。

如果你要对接的后端很多,比如我之前1对3的时候,为了方便后端使用服务就需要启动3个项目,然后每个项目分别连接不同的后端地址。

每启动一个项目都需要启动一个Node服务,而Node服务是非常吃内存的,当时我的内存占用率常年达到90%+,经常出现卡顿严重,甚至Node服务自动崩溃的情况。

并且由于启动的是3个服务,所以代码也是复制了3份,整合代码也非常麻烦,当时我使用的方式是更改了代码后,提交到SVN(公司用的是SVN),然后另外两个项目从SVN上将代码同步过来。

可以想象,上面的这种方式不仅代码管理十分麻烦,要经常提交代码然后同步代码, 而且开发中还会出现严重卡顿甚至IDE崩溃的情况。

后面我想到的解决方式就是用一个项目,对3个后端地址分别打包成3个静态包,然后部署到Nginx上面,关于细节这里就不进行多说了,详细讲起来这篇文章就太长了。

12. 关于产品

12.1 测试

由于这个项目人员配备并不充足,所以已经难以进行收尾,我从好几个月前就已经反复强调测试的重要性,并且强烈要求至少招一个专业测试,一直到现在这项目都没有配备测试人员。

在大项目中,测试人员的配备是非常重要的,如果没有专业测试,只有前端在调接口时看看接口是否能够调用成功,然后等待前端接口对接完了后后端再去测试一下数据是否正确,最后由产品在页面上面点点点,看一下界面、交互是否符合预期。

这种方式对于十几万以下的项目还勉强行得通,但是一旦达到百万级别的项目就很难行得通,第一个是因为后端数据流转复杂,有些界面数据添加十分复杂,而且后端有大量的判断,之前我加一条数据加了20分钟都没有加上。

第二个是没有测试用例,由于项目体量非常大,路由都有几百个,如果仅仅是界面点点点,那么很容易导致测试不完善,甚至测着测着你都忘记哪些功能是你已经测过的。

第三个是没有接口测试,接口测试在大项目中显得尤为重要,因为在后端开发中有个原则叫做“零信任原则”,意思就是前端所有传递过去的数据都是不安全的,后端接收到不安全的数据都可能造成数据异常。

举个例子:在对于前端传递的某个值上面的验证,比如某个值是否必填,那么不仅仅前端要做验证,后端也需要做必填验证,或者仅仅是后端做验证,然后通过返回错误信息让前端展示给用户,还有就是计算值,有些值是前端计算出来再传给后端的,后端尽可能自己去计算这些值,不要相信前端的计算。

也就是说,一切涉及到数据验证,那么无论前端是否验证,后端都要进行验证。因为后端必须要保证数据的安全性。前端做验证主要是为了更好的用户体验,因为很多东西等到提交数据后端返回错误信息前端再展示给用户,那体验是非常差的。

而接口测试就是用来干这个的,因为后端无法判断这条请求是否是通过前端页面发起的,因为可能有人会直接对接口发起请求,比如爬虫,比如Postman这类工具,这样就可以绕过前端的各种验证,直接将数据传递到后端,就可能会传递一些不安全的数据,甚至错误数据,如果后端接收了这些数据,那么可能会导致很多BUG。

上面3项测试的缺失对于大型项目来说是非常致命的,没有经过这些测试很多问题都会在用户使用的过程中才暴露出来,一旦用户开始使用,那么后端就不能轻易的去清空数据库,一旦数据库里面有脏数据,只有后端操作数据库逐一进行手动更改,但这也会带来极大的风险,因为可能这里的数据更改后会导致后面的一系列操作出现问题。

更别说还有压力测试、渗透测试这些。

12.2 TypeScript

TypeScript带来方便的同时也带来了很多麻烦,因为我们不会在写某些功能之前就逐一去确定接口的字段,往往是后端先出个大概,然后前端对接,过个半个月或者一个月完善功能的时候在接口上面又会新增非常多的字段。

而我在对接后端接口的时候每个接口都会进行请求数据类型和响应数据类型的声明,而后端一旦更改这些数据,就会导致我声明的类型和swagger上面的字段不一样。

12.3 原型图

这个项目的原型图做的是非常差的,不光经常改动,而且改动后原型图上面不会标注哪儿进行了改动,而且原型图没有很好的反映交互逻辑,甚至原型图上面的一些计算公式都是错的,表单中哪些字段需要必填也没有进行标注。

13. 最后

这种中后台项目技术上的难点不多,但是得注意代码规范,该封装方法的时候就要封装方法,该封装组件的时候就要封装组件,不要偷懒,如果不进行大量的封装,你会到处CV代码,后面一个地方有改动很多地方都要进行改动,不仅工作量大,还容易改漏。