《vue项目》《node项目》你们更需求哪个?

原创 黎云锐 教程 前端 4485阅读 13 天前 举报
通过本问将看到我在vue的项目中,进行的一系列的项目优化,然后看到不同的维度将这些点进行分类。

这里更多的指的是设计考虑的思路,是大纲,暂不涉及实际代码。

项目架构

分模块设计思想

在接到项目之后,首先将store,router,xhr的对应三个部分分别分子模块,每个子模块的划分维度有所差别。

其中store划分modules划分维度是数据关联性,由于store本身支持modules的组合,而且使用是混合在一起的,所以我们还是会在index中将模块进行混入的。
其中router是按照业务进行分模块的,或者说是按照页面维度分的,每个一级路由分一个路由模块,二级路由为页面名称,其中将一级路由设置为文件夹名称,二级路由路径与页面名称同名,为了简化这部分,一级路由的名称定为‘scope’,而且为了同时支持懒加载和优化引入组件的写法,写了_import的优化方法,可以批量按照文件名引入对应的组件,在生产环境将进行路由代码分割。然后不同模块也是最后汇总到router的modules中。
其中xhr的部分按照后端的微服务进行拆分模块,方便查看和维护。按照后端的接口层次再决定是否划分二级对象属性,其中暴露出来的方法与后端同名,后续也是决定采用easymock进行批量生成api方法来优化这部分手写代码的工作。考虑到几乎没有一个页面或者组件会用到多余两个的api微服务请求,所以这就决定了我在index.js中并没有收集聚合每个业务的api,而是选择开发时按需加载。而对于通用性比较高的api,我一方面会定义在index.js中,另一方面会把这部分数据暴露在vuex中来达到目的。
额外介绍,除了以上三个,我针对src根目录也设置了过滤器的分业务模块实现方案。这部分由于各个业务耦合情况比较少,所以也是仅仅针对核心的工具过滤器暴露在index.js中,其他的都是按需引入。
业务内公共组件

与有的同学考虑不同的是,我在写一些组件的时候,针对业务性比较强,但是针对当前业务公用的一些拆分组件会定义在每个业务的components目录下,而不是放在src/components,我称之业务内公共组件。

业务内枚举 与 全局枚举

其实很多时候会遇到枚举数据,或者是后端定义好的,或者是前端定义好的,或者是接口请求的但是基本不做更改的。也许枚举字段少的也还好,但如果一个数据项有超过十个枚举项,有超过2个页面使用的时候,你应该考虑的是单独的放在枚举字典文件中去维护。 那么首先,我是建议基于这个业务的枚举建在业务的根目录下新建一个enum的js枚举文件,单独用来承载业务中的枚举。但较多时候会有一些比较让人烦恼的部分:

1 业务中的枚举发现另外的页面中也有用,不单单属于这个一级业务页面。那么你可以这样考虑下:首先肯定是维护一份数据的,那么维护在哪里,如果是核心业务,那就维护在全局枚举仓库,然后业务中进行按需引入或者改装。如果是周边业务,偶尔用下,我个人觉得维护在业务中的枚举是比较好的。

2 枚举与过滤器与字段翻译的关系。其实枚举字段不仅仅是用于做枚举的,还必然的会充当一些下拉框,显示值的遍历来源,也可以当做字段翻译的翻译来源,同时还可以当做我们一些业务字段的过滤器。这部分理解好之后,对于我们优化整理项目中的业务数据类型有着极大的好处。

3 全局枚举业务过滤器,通用性过滤器,当然这些过滤器功能除了按照基本的部分,还会按照业务中收集到的部分进行业务过滤器的维护。同时也作为对应的方法来获得对应值转换值的语法,一者两得。

common组件

纯ui组件,elementui组件进行进一步的封装,按照其官方的维护方式进行自己项目需求的一些分类。
布局内基本布局组件,这里面包括了页面架构,菜单,顶部,主题页面。
可分解于任何页面任何位置的特征业务组件,支持其展现到任何位置任何页面中,只要求其对应的业务数据要求即可。
功能性组件,包括图片上传,自定义的模态框
theme

为了维护基本的风格,设置了一些基本的主题变量,然后针对elementui的核心组件修改器风格颜色。

axios拦截

针对axios的部分进行请求前后拦截,针对特定状态码进行翻译,在这个设置中进行vuex必要的接口token必要性的验证以及引入提示组件进行必要的接口提示。

针对业务的整合需求,进行接口的串联、并联的请求优化。

mixins

将常用的优化方法进行mixins进行混入。

典型代码段优化

用数据做逻辑,减少标签的显示控制

看到很多前端会根据数据的某个字段,然后写v-if 决定这个标签是否显示,然后不是这个字段,另外一个显示。建议在不管是对象还是数组的显示控制中,直接根据需要的数据进行数据改装,不用多条件判断类似的组件渲染。这种代码简单的可以用一个标签承载,内容的显示区别简单的可以用三目,复杂的就应该在js方法中进行改造完之后或者过滤器实现。

<span v-if="sex===male" >男</span>
<span v-else >女<span>
挥之不去的静态复制写法

vue提供了良好的数组循环和对象循环的方法,在我们实现类似的页面需求的时候,不建议再和之前一样,写很多维护性不强的页面列表了。把它用一个数组维护,然后v-for循环实现,对于因为大量的这种代码占据篇幅的话,说明还是 没有很好的理解vm的含义。
除了if,else想不到其他逻辑方式

然后就看到一大波人会if,age===0,判断,else if 等等。其实除了这些你还有switch,对象属性字面量方式,switch方式,等等。不要让if,else的嵌套循环成为我们写代码的唯一方式。

node项目

一个纯粹给客户端提供接口的服务,没有涉及到页面渲染相关。

背景

首先这个项目是一个几年前的项目了,期间一直在新增需求,导致代码逻辑变得也比较复杂,接口响应时长也在跟着上涨。

之前有过一次针对服务器环境方面的优化( node版本升级 ),确实性能提升不少,但是本着“青春在于作死”的理念,这次就从代码层面再进行一次优化。

相关环境

由于是一个几年前的项目,所以使用的是 Express + co 这样的。

因为早年 Node.js 版本为 4.x ,遂异步处理使用的是 yield + generator 这种方式进行的。

确实相对于一些更早的 async.waterfall 来说,代码可读性已经很高了。

关于数据存储方面,因为是一些实时性要求很高的数据,所以数据均来自 Redis 。

Node.js 版本由于前段时间的升级,现在为 8.11.1 ,这让我们可以合理的使用一些新的语法来简化代码。

因为访问量一直在上涨,一些早年没有什么问题的代码在请求达到一定量级以后也会成为拖慢程序的原因之一,这次优化主要也是为了填这部分坑。

一些小提示

本次优化笔记,并不会有什么 profile 文件的展示。

我这次做优化也没有依赖于性能分析,只是简单的添加了接口的响应时长,汇总后进行对比得到的结果。(异步的写文件 appendFile 了开始结束的时间戳)

依据 profile 的优化可能会作为三期来进行。

profile 主要会用于查找内存泄漏、函数调用堆栈内存大小之类的问题,所以本次优化没有考虑 profile 的使用

而且我个人觉得贴那么几张内存快照没有任何意义(在本次优化中),不如拿出些实际的优化前后代码对比来得实在。

几个优化的地方

这里列出了在本次优化中涉及到的地方:

callback
数据结构相关的优化

这里说的结构都是与 Redis 相关的,基本上是指部分数据过滤的实现

过滤相关的主要体现在一些列表数据接口中,因为要根据业务逻辑进行一些过滤之类的操作:

过滤的参考来自于另一份生成好的数据集
过滤的参考来自于Redis
其实第一种数据也是通过 Redis 生成的。:)

过滤来自另一份数据源的优化

就像第一种情况,在代码中可能是类似这样的:

有两个列表,要保证第一个列表中的数据不会出现在第二个列表中

当然,这个最优的解决方案一定是服务端不进行处理,由客户端进行过滤,但是这样就失去了灵活性,而且很难去兼容旧版本

上面的代码在遍历 data2 中的每一个元素时,都会尝试遍历 data1 ,然后再进行两者的对比。

这样做的缺点在于,每次都会重新生成一个迭代器,且因为判断的是 id 属性,每次都会去查找对象属性,所以我们对代码进行如下优化:

// 在外层创建一个用于过滤的数组

)
这样我们在遍历 data2 时只是对 filterData 对象进行调用了 includes 进行查找,而不是每次都去生成一个新的迭代器。

当然,其实关于这一块还是有可以再优化的地方,因为我们上边创建的 filterData 其实是一个 Array ,这是一个 List ,使用 includes ,可以认为其时间复杂度为 O(N) 了, N 为 length 。

所以我们可以尝试将上边的 Array 切换为 Object 或者 Map 对象。

因为后边两个都是属于 hash 结构的,对于这种结构的查找可以认为时间复杂度为 O(1) 了,有或者没有 。

P.S. 跟同事讨论过这个问题,并做了一个测试脚本实验,证明了在针对大量数据进行判断item是否存在的操作时, Set 和 Array 表现是最差的,而 Map 和 Object 基本持平。

关于来自Redis的过滤

关于这个的过滤,需要考虑优化的 Redis 数据结构一般是 Set 、 SortedSet 。

比如 Set 调用 sismember 来进行判断某个 item 是否存在,

或者是 SortedSet 调用 zscore 来判断某个 item 是否存在( 是否有对应的 score 值 )

这里就是需要权衡一下的地方了,如果我们在循环中用到了上述的两个方法。

是应该在循环外层直接获取所有的 item ,直接在内存中判断元素是否存在

还是在循环中依次调用 Redis 进行获取某个 item 是否存在呢?

这里有一点小建议可供参考

如果是 SortedSet ,建议在循环中使用 zscore 进行判断(这个时间复杂度为 O(1) )
如果是 Set ,如果已知的 Set 基数基本都会大于循环的次数,建议在循环中使用 sismember进行判断
如果代码会循环很多次,而 Set 基数并不大,可以取出来放到循环外部使用( smembers 时间复杂度为 O(N) , N 为集合的基数)
而且,还有一点儿,网络传输成本也需要包含在我们权衡的范围内,因为像 sismbers 的返回值只是 1|0 ,而 smembers 则会把整个集合都传输过来
关于Set两种实际的场景

如果现在有一个列表数据,需要针对某些省份进行过滤掉一些数据。
我们可以选择在循环外层取出集合中所有的值,然后在循环内部直接通过内存中的对象来判断过滤。
如果这个列表数据是要针对用户进行黑名单过滤的,考虑到有些用户可能会拉黑很多人,这个Set 的基数就很难估,这时候就建议使用循环内判断的方式了。
降低网络传输成本

杜绝Hash的滥用

确实,使用 hgetall 是一件非常省心的事情,不管 Redis 的这个 Hash 里边有什么,我都会获取到。

但是,这个确实会造成一些性能上的问题。

比如,我有一个 Hash ,数据结构如下:
现在在一个列表接口中需要用到这个 hash 中的 name 和 age 字段。

最省心的方法就是:
在 hash 很小的情况下, hgetall 并不会对性能造成什么影响,

可是当我们的 hash 数量很大时,这样的 hgetall 就会造成很大的影响。

hgetall 时间复杂度为 O(N) , N 为 hash 的大小
且不说上边的时间复杂度,我们实际仅用到了 name 和 age ,而其他的值通过网络传输过来其实是一种浪费
所以我们需要对类似的代码进行修改:

let results = await redisClient.hgetall('hash')
// == >
let [name, age] = await redisClient.hmget('hash', 'name', 'age')
P.S. 如果 hash 的item数量超过一定量以后会改变 hash 的存储结构,

此时使用 hgetall 性能会优于 hmget ,可以简单的理解为,20个以下的 hmget 都是没有问题的

异步代码相关的优化

从 co 开始,到现在的 async 、 await ,在 Node.js 中的异步编程就变得很清晰,我们可以将异步函数写成如下格式:
看起来是很舒服对吧?

你舒服了程序也舒服,程序只有在 getData1 获取到返回值以后才会去执行 getData2 的请求,然后又陷入了等待回调的过程中。

这个就是很常见的滥用异步函数的地方。将异步改为了串行,丧失了 Node.js 作为异步事件流的优势。

像这种类似的毫无相关的异步请求,一个建议:

能合并就合并,这个合并不是指让你去修改数据提供方的逻辑,而是要更好的去利用异步事件流的优势,同时注册多个异步事件。
这样的做法能够让 getData1 与 getData2 的请求同时发出去,并统一处理回调结果。

最理想的情况下,我们将所有的异步请求一并发出,然后等待返回结果。

然而一般来讲不太可能实现这样的,就像上边的几个例子,我们可能要在循环中调用 sismember ,亦或者我们的一个数据集依赖于另一个数据集的过滤。

这里就又是一个权衡取舍的地方了,就像本次优化的一个例子,有两份数据集,一个有固定长度的数据(个位数),第二个为不固定长度的数据。

第一个数据集在生成数据后会进行裁剪,保证长度为固定的个数。

第二个数据集长度则不固定,且需要根据第一个集合的元素进行过滤。

此时第一个集合的异步调用会占用很多的时间,而如果我们在第二个集合的数据获取中不依据第一份数据进行过滤的话,就会造成一些无效的请求(重复的数据获取)。

但是在对比了以后,还是觉得将两者改为并发性价比更高。

因为上边也提到了,第一个集合的数量大概是个位数,也就是说,第二个集合即使重复了,也不会重复很多数据,两者相比较,果断选择了并发。

在获取到两个数据集以后,在拿第一个集合去过滤第二个集合的数据。

如果两者异步执行的时间差不太多的话,这样的优化基本可以节省 40% 的时间成本( 当然缺点就是数据提供方的压力会增大一倍 )。

将串行改为并行带来的额外好处

如果串行执行多次异步操作,任何一个操作的缓慢都会导致整体时间的拉长。

而如果选择了并行多个异步代码,其中的一个操作时间过长,但是它可能在整个队列中不是最长的,所以说并不会影响到整体的时间。

后记

总体来说,本次优化在于以下几点:

合理利用数据结构(善用 hash 结构来代替某些 list )
减少不必要的网络请求( hgetall to hmget
将串行改为并行(拥抱异步事件)

评论 ( 0 )
最新评论
暂无评论

赶紧努力消灭 0 回复