------------恢复内容开始------------
高铁的不断提速、5G 时代的到来、无人超市新兴项目的落地......都在不断地提醒着我们,要快、要高性能。那么作为程序员的我们,该如何利用现有技术打造高性能产品,为用户带来“飞”一般的体验呢?
挑战“进行时”
BCRM 是企业业务重要的客户信息管理平台,承载现有客户入驻前权益管理全流程工作。商机板块为获客及拓客的重要板块,但是因为目前 BCRM 仅能 PC 内网访问,如销售经理外出拜访客户,很难第一时间获取商机信息并维护联系记录信息,易造成商机信息流失及信息确认,为了提高商机的转化及跟进效率,决定接入京 ME 服务,提高销售使用效率。
7 月 28 号提需,计划在 8 月 10 号上线,在仅有的 10 个工作日内,需完成静态开发、联调、测试、上线,并且前端静态开发评估就有 17 个人/日,要在如此紧张的时间里完成从 0 到 1 的高性能项目建设,我们两个压力着实不小 ☹。
经过一番热烈的讨论后,我们决定改变既往的开发模式,在几个方面大胆创新以应对即将来临的暴风雨:首先我们想到的是协作模式,我们要尽可能在最短的时间内完成开发,就要让本来串行的工作并行起来;另外,勇敢尝试前端新兴技术,充分利用技术的力量为项目提供源源不断的动力;最后,在项目开发中,我们深入挖掘,在快速完成需求的基础上也能保证应用的飞速运转。
下面我们就来详细描述一下吧!
“并行”协作
先来回想一下我们的日常开发流程:产品提需 -> 进行需求评审 -> 各方评估工时 -> 确定里程碑 -> 开发 -> 联调 -> 测试 -> 上线
日常迭代需求按照这个流程进行丝毫没毛病,但如果应用到开发时间如此紧张的 BCRM 中就有些来不及了。毕竟不是所有事情都必须是串行的,为什么不能将这些事情“并行”,节省时间呢?再加上每个人正式进入开发的时间不一致,就会造成有的模块先开发完,有的稍晚一些,所以我们最终决定开发、联调、测试同步进行,其中,先开发完的,优先提测。
当然,在人力资源合理的情况下,光是调整协作流程是不够的,要想从根本上提高开发效率、提升应用性能,还得需要一个“新”的技术架构,毕竟大家都不想加班。
新技术带来新速度
BCRM 作为一个全新的项目,不存在兼容历史版本等问题,我们可以大胆地采用全新的技术栈进行开发。首先闯入脑海的就是最热门的 Vue3 + Vite + TypeScript 组合包,再加上最强辅助 NutUI3 作为组件库,何愁我们的项目“飞”不起来。
高性能的 Vue3
Vue3 自发布以来,业内对其褒贬不一。与 Vue2 相比,有了很大的变化,由 Vue2 转为 Vue3 虽然需要一定的学习成本的,但不得不承认的是 Vue3 比 Vue2 在性能方面有更出色的表现。
在 Vue2 当中,当数据发生变化,它就会新生成一个 DOM 树,并和之前的 DOM 树进行比较,找到不同的节点然后更新。但这比较的过程是全量的比较,也就是每个节点都会彼此比较。但其中很显然的是,有些节点中的内容是不会发生改变的,那我们对其进行比较就肯定消耗了时间。
<div>
<p>BCRM 销售助手</p>
<p>{{ msg }}</p>
</div>
所以在 Vue3 当中,就对这部分内容进行了优化:在创建虚拟 DOM 树的时候,会根据 DOM 中的内容会不会发生变化,添加一个 PatchFlag,如果内容会变,就会标记一个 flag。那么之后在与上次虚拟节点进行对比的时候,就只会对比这些带有 flag 的节点,减少静态标记遍历成本。
除此以外,在 Vue2 中无论元素是否参与更新,每次都会重新创建,然后再渲染,白白浪费了不少性能。还是以刚才的代码块为例,msg 每次更新都会创建前面的 p 节点。
import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue";
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("p", null, "BCRM 销售助手"),
_createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
]))
}
而在 Vue3 中对于不参与更新点元素,会做静态提升,只会被创建一次,在渲染时直接复用即可,减少重新创建造成的开销。
import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue";
const _hoisted_1 = /*#__PURE__*/_createVNode("p", null, "Hello World", -1 /* HOISTED */)
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_hoisted_1,
_createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
]))
}
高效打包的 Vite
2021 新年伊始,尤大就在知乎问答 2021 前端新变化中回复:" 会有很多人抛弃 Webpack 开始用 Vite "。从 Grunt、Gulp ,到 Webpack、Rollup、Snowpack 以及若干构建框架,Vite 的哪些特性能让尤大如此自信,我们不妨来研究研究 。
Vite 一个由 Vue 作者开发的原生 ESM 驱动的 Web 开发构建工具,在开发环境下基于浏览器原生 ES imports 开发,在生产环境下基于 rollup 打包。最大的特点就是“快”。运行 Dev 命令后只做了两件事情,一是启动了一个用于承载资源服务的 service;二是使用 esbuild 预构建 npm 依赖包。之后就一直躺着,直到浏览器以 http 方式发来 ESM 规范的模块请求时,Vite 才开始 「按需编译」 被请求的模块。
当然,Vite 还有很多值得一提的性能优化
Vite 刚出来的时候,在掘金上看到这样一段话,很有感触,想分享给大家:Webpack 并不是标准答案,前端构建工具可以有一些新的玩法:
- 「打包」 不是目的,「运行」 才是,2021 年了,能够交给浏览器做的事情就交给浏览器吧,做一个会偷懒的程序猿不好吗!
- 一个灵活的框架,对作者而言可能意味着逐步失控的开发量;对用户而言可能意味高学习成本,以及不断重复的类似空格好还是 tab 好的争论。那么,一套内置好各种业界 「最佳实践」,没有太多定制空间的工具,某些情况下反而能提升大家的效率
强大的组件库 NutUI3
工欲善其事,必先利其器。一个优秀的前端工程师,除了不断提高自己的能力外,还要懂得运用工具。一款合适的组件库,能节省大量的开发时间。
作为一个京东风格的轻量级移动端 Vue 组件库,NutUI3 采用了 Vite2.x + Vue3 + TypeScript 的架构,与跟 BCRM 应用的技术栈不谋而合。Vue3 在 Vue2 的基础上做了那么多的性能优化,NutUI3 也紧跟业务需求做了以下优化:
- 采用组合式 API Composition 语法重构,结构清晰,功能模块化
- 组件 emits 事件单独提取,增强代码可读性
- 使用 Teleport 新特性重构挂载类组件
- ...
那么在 BCRM 中,NutUI3 为我们助力了哪些内容呢!
- 按需引入,点对点服务
开发中,不可能用到组件库中所有组件,如果在项目中导入所有的组件,最终打包文件会很大,所以此刻就要用到 NutUI3 的按需加载能力了。
由于 NutUI3 使用的是 Vite 构建工具,而 Vite 本身已经按需导入了组件库,因此我们只需按需导入样式即可。按需导入样式也不复杂,只需要使用 Vite 提供的按需加载插件 vite-plugin-style-import 就可以实现。
首先,安装插件
npm install vite-plugin-style-import --save-dev
在 vite.config.ts 中添加配置
import vue from '@vitejs/plugin-vue'
import styleImport from 'vite-plugin-style-import';
export default {
plugins: [
... ...
styleImport({
libs: [
{
libraryName: '@nutui/nutui',
libraryNameChangeCase: 'pascalCase',
resolveStyle: (name) => {
return `@nutui/nutui/dist/packages/${name}/index.scss`
}
}
],
}),
]
};
配置完成以后,记得重新启动哟,否则会不生效。接下来,我们就可以引入组件了
import { Button } from "@nutui/nutui";
通过上面的代码段,不仅按需引入了 Button 组件,就连 Button 组件样式也按需引入了,不需要在单独引入样式,有没有很方便。
- 组件的应用
BCRM 项目中使用到了包括:Uploader、Datepicker、Infiniteloading、Popup 等 10+ 个组件,NutUI3 为项目的构建提供了巨大的便利。
在这里不得不说表扬一下 Calendar 组件,开发中涉及到的时间选择、时间区间选择功能都能完美覆盖。不仅支持选择单个时间、时间区间,还能设置时间选择区间、重置到指定日期,真的不要太好用。
NutUI3 中的 Calendar 组件不但充分满足了业务需求,并且在使用过程中遇到的小 bug ,开发者也能及时进行修改,丝毫没影响开发进度。很难想象,如果不利用 NutUI3 ,自己开发 Calender 组件,项目该何时上线。
开源的 Vue3 移动端组件库本就凤毛鳞角,NutUI3 又如此优秀,岂有不用之理!
性能再提升
性能提升,技术圈经典的话题,也是让无数程序员头疼的话题,经常在项目上花费很多时间做性能优化,但最后依然没有达到预期效果。不过,在 BCRM 中依赖 Vue3 做的性能优化,效果还是很显著的。
在 BCRM 开发中,由于时间紧、任务重,性能优化的优先级更是一降再降,这可能是大多数项目开发的通病。在临近上线时,视觉感知让我们将性能优化提上日程,进行了深度优化。
Promise.all 提升渲染速度
提升页面渲染速度的目的就是为了让网页 ‘Duang’ 的一下子加载出来,而不是旋转半天都出不来,一般超过 5 秒,用户好感度就会下降。
大家猜猜看,下面这个页面在初次加载时,需要请求多少个接口:
答案是 23 个。能看到的内容全部是由接口提供的,并且为了获取一个字段,需要调用不止一个接口。以商机阶段字段为例:
-
根据商机 ID 获取商机详情,得到商机阶段 k 值,此值是一个数字,并不是想要展示的文字描述
-
通过刚刚得到的 k 值,请求接口获取所对应的文字描述
商机来源也需要同样的操作。
-
根据商机 ID 获取商机详情,得到商机来源 k 值
-
通过刚刚得到的 k 值,请求接口获取所对应的文字描述
不难发现,获取了商机详情数据后,获取商机阶段文字描述与获取商机来源文字描述的过程是没有联系的。
与上面的情况一样,在这 23 个请求中,22 个接口请求之间是没有联系的,并不需要区分前后顺序。但 JS 是单线程的,同一时间只能做一件事,即使是可以同时执行的请求。这时 Promise.all 闪亮登场,合并没有联系的请求,将它们包装成一个新的 Promise 实例,并以数组的形式返回请求结果,以此来减少页面渲染时间。
query.getDetail({ coSn: str }).then((res) => {
state.detail = res as object;
let asksMap = new Map();
asksMap.set("projectInfo", query.getProjectByCoSn({ coSn: str }));
asksMap.set("applyLabel", query.coApplyLabel({ coSn: str }));
asksMap.set("coTrajectory",query.coTrajectory({ coSn: str, pageSize: 100 }));
... ...
asksMap.set("planList",planService.queryPage({coId: state.detail.id,pageNo: 1,pageSize: 50}));
Promise.all([...asksMap.values()])
.then(
(results) => {
results.forEach((result, i) => {
state[[...asksMap.keys()][i]] = result;
});
},
() => {}
)
.catch((error) => {});
})
async/await 渲染速度再升级
通过上面的优化,页面渲染速度得到显著提升,但在优化过程中 async/await 引起了我们的注意。async/await 将 JavaScript 开发者从回调函数的困境中解救出来,但是随着对 async/await 的高度使用,也诞生了新的 async/await 困境。
进行 JavaScript 异步编程时,通常一个接一个写多条语句,并在每个函数调用前标注一个 await。因为大多数时候,一条语句并不依赖于其之前的那条语句,但是你还是必须等待之前的语句执行完毕。先来看一下下面这段代码片段
async function orderItems() {
const items = await getCartItems() // async call
const noOfItems = items.length
for(var i = 0; i < noOfItems; i++) {
await sendRequest(items[i]) // async call
}
}
给这段代码搭配这样的业务场景,获取购物车中的商品,并且每个商品去进行订购。乍一看,这段代码没什么问题,但是却存在很大的隐患。
在 for 循环中,必须等待 sendRequest() 函数执行完毕才能继续下一轮循环。事实上每个商品之间的处理并没有关系,不需要等待。希望尽可能快地发送请求,然后等待所有这些请求响应完毕。如果购物车中的商品数量是上千、上万,页面崩溃是肯定逃不了的,一旦出现,排查问题都会很费劲。
有些性能优化做完之后,并不一定对现状有所帮助,也不一定能遇到极端情况,但保持代码的健壮性,拥有一个良好的代码开发习惯,何乐而不为呢!
Vuex 与 Map 强强联合
商机阶段、商机来源等字段贯穿整个 BCRM 商机项目,虽然是接口获取,但以项目维度来说却是静态的,这种情况在项目开发中最常见不过的。面对这样的情况,前端一般只在项目初始化时,请求一次接口,并利用 Vuex 存储为常量。
let result = await service.getDict({kind: `${type}`});
if (result) {
let obj = result as any;
store.commit(type, obj);
}
但接口返回的数据格式让我们感到一丝麻烦
obj = [
{k:1,v:'提审'},
{k:2,v:'提审中'},
]
就像上面小节中提到的,后端返回给前端的之后 k 值,若想知道 k 值对应的 v ,每次都要循环遍历 Vuex 中存储的对象,无形中增加了代码量,同样也增加了出错的几率。所以将得到的结果进行转换,使用 Map 形式存储。
let result = await service.getDict({kind: `${type}`});
if (result) {
let obj = result as any;
let target = new Map();
obj.forEach((item: DataVal) => {
target.set(item.k, item);
});
const key: { [props: string]: string } = {
co_sources: "setManageSources",
......
};
store.commit(key[type], target);
}
经过这样的转换,使用就可以简化为
const { state } = useStore(key);
const value = state.manageSources.get(k).v
是不是一下子清晰了很多。在量级小时,这样的代码优化虽然对性能产生的作用微乎其微,但量级一旦上来,就会产生巨变。
总结
短时间从 0 到 1 完成一个项目的过程是痛苦的,但作为 NutUI3 开发成员之一,看到 BCRM 项目中的每个页面都有 NutUI3 的“身影”,不禁感到兴奋。
2021Q1 Vue3 正式发布,已经有越来越多的开发者开始使用 Vue3 作为开发语言。NutUI3 紧扣新技术,在 2021Q2 发布基于 Vue3 的版本 NutUI3.0。目前光我们小组内部已经有 5 个项目使用 NutUI3 开发,集团内部累计有 10+ 个项目使用 NutUI3 开发并上线运营。充分表明了 NutUI3 不管在组件覆盖率还是稳定性都日趋完善。欢迎共建,期待 PR!
------------恢复内容结束------------