谈谈国内前端的三大怪啖

因为工作的原因,我和一些国外的工程师们有些交流。他们对于国内环境不了解,有时候会问出一些有趣的问题,大概是这些问题的启发,让我反复在思考一些更为深入的问题。

今天聊三个事情:

  • 小程序
  • 微前端
  • 模块加载

小程序

每个行业都有一把银座,当坐上那把银座时,做什么便都是对的。

“ 我们为什么需要小程序?”

第一次被问到这个问题,是因为一个法国的同事。他被派去做一个移动端业务,刚好那个业务是采用小程序在做。于是一个法国小哥就在被痛苦的中文文档和黑盒逻辑中来回折磨着 🤦。

于是,当我们在有一次交流中,他问出了我这个问题:我们为什么需要小程序?

说实话,我试图解释了 19 年国内的现状,以及微信小程序推出时候所带来的便利和体验等等。总之,在我看来并不够深刻的见解。

即便到现在为止,每次当我使用小程序的时候,依旧会复现这个问题。在 ChatGPT 11 月份出来的时候,我也问了它这个很有国内特色的问题:

看起来它回答的还算不错,至少我想如果它来糊弄那些老外,应该会比我做的更好些。

但如果扪心自问,单从技术上来讲。以上这些事情,一定是有其他方案能解决的。

所以从某种程度上来看,这更像是一场截胡的商业案例:

应用市场

全世界的互联网人都知道应用市场是非常有价值的事情,可以说操作系统最值钱的部分就在于他们构建了自己的应用市场。

只要应用在这里注册发行,雁过拔毛,这家公司就是互联网世界里的统治阶级,规则的制定者。

反之则需要受制于人,APP 做的再大,也只是应用市场里的一个应用,做的好与坏还得让应用商店的评判。

另外,不得不承认的是,一个庞大如苹果谷歌这样的公司,他们的应用商店对于普通国内开发者来说,确实是有门槛的。

在国内海量的 APP 需求来临之前,能否提供一个更低成本的解决方案,来消化这些公司的投资?

毕竟不是所有的小企业都需要 APP,其实他们大部分需求 Web 就可以解决,但是 Web 没牌面啊,做 Web 还得砸搜索的钱才有流量。(某度搜索又做成那样…)

那做操作系统?太不容易,那么多人都溺死在水里了,这水太深。

那有没有一种办法可以既能构建生态,又有 APP 的心智,还能给入驻企业提供流量?

于是,在 19 年夏天,深圳滨海大厦下的软件展业基地里,每天都在轮番播放着,做 XX小程序,拥抱下一个风口…

全新体验心智

小程序用起来挺方便的。

你有没有想过,这些美妙感觉的具体都来自哪些?以及这些真的是 Web 技术本身无法提供的吗?

  1. 靠谱感,每个小程序都有约束和规范,于是你可以将他们整整齐齐的陈放在你的列表里,仿佛你已经拥有了这一个个精心雕琢的作品,相对于一条条记不住的网页地址和鱼龙混杂的网页内容来说,这让你觉得小程序更加的有分量和靠谱。
  2. 安全感,沉浸式的头部,没有一闪而过的加载条,这一切无打扰的设计,都让你觉得这是一个在你本地的 APP,而不是随时可能丢失网页。你不会因为网速白屏而感到焦虑,尽管网络差的时候,你的 KFC 依旧下不了单 😂
  3. 沉浸感,我不知道是否打开网页时顶部黑黑的状态栏是故意留下的,还是不小心的… 这些限制都让用户非常强烈的意识到这是一个网页而不是 APP,而小程序中虽然上面也存在一个空间的空白,但是却可以被更加沉浸的主题色和氛围图替代。网页这个需求做不了?我不信。
H5 小程序
  1. 顺滑感,得益于 Native 的容器实现,小程序在所有的视图切换时,都可以表现出于原生应用一样的顺滑感。其实这个问题才是在很多 Hybrid 应用中,主要想借助客户端来处理和解决的问题。类似容器预开、容器切换等技术是可以解决相关问题的,只是还没有一个标准。

我这里没有提性能,说实话我不认为性能在小程序中是有优势的(Native 调用除外,如地图等,不是一个讨论范畴)。作为普通用户,我们感受到的只是离线加载下带来的顺滑而已。

而上述提到的许多优势,这对于一个高品质的 Web 应用来说是可以做得到的,但注意这里是高品质的 Web 应用。而这种“高品质”在小程序这里,只是入驻门槛而已。

心智,这个词,听起来很黑话,但却很恰当。当小程序通过长期这样的筛选,所沉淀出来一批这样品质的应用时。就会让每个用户即便在还没打开一个新的小程序之前,也有不错体验的心理预期。这就是心智,一种感觉跟 Web 不一样,跟 APP 有点像的心智。

打造心智,这件事情好像就是国内互联网企业最擅长做的事情,总是能从一些细微的差别中开辟一条独立的领域,然后不断强化灌输本来就不大的差异,等流量起来再去捞钱变现。


我总是感觉现在的互利网充斥着如何赚钱的想法,好像永远赚不够。“赚钱”这个事情,在这些公司眼里就是圈人圈地抢资源,看看谁先占得先机,别人有的我也得有,这好像是最重要的事情。

很少有企业在思考如何创造些没有的市场,创造些真正对技术发展有贡献,对社会发展有推动作用的事情。所以在国内互联网圈也充斥着一种奇怪的价值观,有技术的不一定比赚过钱的受待见。

管你是 PHP 还是 GO,管你是在做游戏还是直播带货,只要赚到钱就是高人。并且端的是理所应当、理直气壮,有些老板甚至把拍摄满屋子的程序员为自己打工作为一种乐趣。什么情怀、什么优雅、什么愿景,人生就俩字:搞钱。

不是故意高雅,赚钱这件事情本身不寒碜,只是在已经赚到盆满钵满、一家独大的时候还在只是想着赚更多的钱,好像赚钱的目的就是为了赚钱一样,这就有点不合适。企业到一定程度是要有社会责任的,龙头企业每一个决定和举措,都有会影响接下来的几年里这个行业的价值观走向。

当然也不是完全没有好格局的企业,我非常珍惜每一个值得尊重的国内企业,来自一个蔚来车主。

小程序在商业上固然是成功的,但吃的红利可以说还是来自 网页 到 应用 的心智变革。将本来流向 APP 的红利,截在了小程序生态里。

但对于技术生态的发展却是带来了无数条新的岔路,小程序的玩法就决定了它必须生长在某个巨型应用里面,不论是用户数据获取、还是 API 的调用,其实都是取决于应用容器的标准规范。

不同公司和应用之间则必然会产生差异,并且这种差异是墒增式的差异,只会随着时间的推移越变越大。如果每个企业都只关注到自己业务的增长,无外部约束的话,企业必然会根据自己的业务发展和政策需要,选择成本较低的调整 API,甚至会故意制造一些壁垒来增加这种差异。

小程序,应该是 浏览器 与 操作系统 的融合,这本应该是推动这两项技术操刀解决的事情。

qiankun、wujie、single-spa 是近两年火遍前端的技术方案,同样一个问题:我们为什么需要微前端?

我不确定是否每个在使用这项技术的前端都想清楚了这个问题,但至少在我面试过的候选人中,我很少遇到对自己项目中已经在使用的微前端,有很深的思考和理解的人。

先说下我的看法:

  1. 微前端,重在解决项目管理而不在用户体验。
  2. 微前端,解决不了该优化和需要规范的问题。
  3. 微前端,在挽救没想清楚 MPA 的 SPA 项目。

银色子弹(英文:Silver Bullet),或者称“银弹”“银质子弹”,指由纯银质或镀银的子弹。在欧洲民间传说及19世纪以来哥特小说风潮的影响下,银色子弹往往被描绘成具有驱魔功效的武器,是针对狼人、吸血鬼等超自然怪物的特效武器。后来也被比喻为具有极端有效性的解决方法,作为杀手锏、最强杀招、王牌等的代称。

所有技术的发展都是建立在前一项技术的基础之上,但技术依赖的选择过程中一定需要保留第一性原理的意识。

当 React、Vue 兴起,当 打包技术(Webpack) 兴起,当 网页应用(SPA) 兴起,这些杰出的技术突破都在不同场景和领域中给行业提供了新的思路、新的方案。

不知从何时开始,前端除了 div 竟说不出其他的标签(还有说 View 的),项目中再也不会考虑给一个通用的 class 解决通用样式问题。

不知从何时开始,有没有权限需要等到 API 请求过后才知道,没有权限的话再把页面跳转过去申请。

不知从何时开始,大家的页面都放在了一个项目里,两个这样的巨石应用融合竟然变成了一件困难的事。

上面这些不合理的现状,都是在不同的场景下,不思考适不适合,单一信奉 “一招吃遍天” 下演化出的问题。

B 端应用,是否应该使用 SPA? 这其实是一个需要思考的问题。

微前端从某种程度上来讲,是认准 SPA 了必须是互联网下一代应用标准的产物,好像有了 SPA 以后,MPA 就变得一文不值。甭管页面是移动端的还是PC的;甭管页面是面对 C 端的还是 B 端的;甭管一个系统是有 20 个页面还是 200 个页面,一律行这套逻辑。

SPA 不是万能银弹,React 不是万能银弹,Tailwind 不是万能银弹。在新技术出现的时候,保持热情也要保持克制。

ps. 我也十分痛恨 React 带来的这种垄断式的生态,一个 React 组件将 HTML 和 Style 都吃进去,让你即使在做一个移动端的纯展示页面时,也需要背上这些称重的负担。

质疑 “墨守成规”,打开视野,深度把玩,理性消费。

分治法,一个很基本的工程思维。

在我看来在一个正常商业迭代项目中的主要维护者,最好不要超过 3 个人,注意是主要维护者(Maintainer) 。

你应该让每个项目都有清晰的责任人,而不是某行代码,某个模块。责任人的理解是有归属感,有边界感的那种,不是口头意义上的责任人。(一些公司喜欢搞这种虚头巴脑的事情,什么连坐…)

我想大部分想引入微前端的需求都是类似 如何更好的划分项目边界,同时保留更好的团队协同。

比如 导航菜单 应该是独立收口独立管理的,并且当其更新时,应该同时应用于所有页面中。类似的还有 环境变量、时区、主题、监控及埋点。微前端将这些归纳在主应用中。

而具体的页面内容,则由对应的业务进行开发子应用,最后想办法将路由关系注册进主应用即可。

当然这样纯前端的应用切换,还会出现不同应用之间的全局变量差异、样式污染等问题,需要提供完善的沙箱容器、隔离环境、应用之间通信等一系列问题,这里不展开。

当微前端做到这一部分的时候,我不禁在想,这好像是在用 JavaScript 实现一个浏览器的运行容器。这种本应该浏览器本身该做的事情,难道 JS 可以做的更好?

只是做到更好的项目拆分,组织协同的话,引入后端服务,由后端管控路由表和页面规则,将页面直接做成 MPA,这个方案或许并不比引入微前端成本高多少。

从 SPA 再回 MPA,说了半天不又回去了么。

所以不防想想:在 B端 业务中使用 SPA 的优势在哪里?

流畅的用户体验:

这个话题其实涵盖范围很广,对于 SPA 能带来的 “流畅体验”,对于大多数情况下是指:导航菜单不变,内容变化 发生变化,页面切换中间不出现白屏

但要做到这个点,其实对于 MPA 其实并没有那么困难,你只需要保证你的 FCP 在 500ms 以内就行。

以上的页面切换全部都是 MPA 的多页面切换,我们只是简单做了导航菜单的 拆分 和 SWR,并没有什么特殊的 preload、prefetch 处理,就得到了这样的效果。

因为浏览器本身在页面切换时会在 unload 之前先 hold 当前页面的视图不变,发起一下一个 document 的请求,当页面的视图渲染做到足够快和界面结构稳定就可以得到这样的效果。

这项浏览器的优化手段我找了很久,想找一篇关于它的博文介绍,但确实没找到相关内容,所以 500ms 也是我的一个大致测算,如果你知道相关的内容,可以在评论区补充,不胜感激。

所以从这个角度来看,浏览器本身就在尽最大的努力做这些优化,并且他们的优化会更底层、更有效的。

离线访问 (PWA)

SPA 确实会有更好的 PWA 组织能力,一个完整的 SPA 应用甚至可以只针对编译层做改动就可以支持 PWA 能力。

但如果看微前端下的 SPA 应用,需要支持 PWA 那就同样需要分析各子应用之间的元数据,定制 Service Worker。这种组织关系和定制 SW,对于元数据对于数据是来自前端还是后端,并不在意。

也就是说微前端模式下的 PWA,同样的投入成本,把页面都管理在后端服务中的 MPA 应用也是可以做到相同效果的。

项目协同、代码复用

有人说 SPA 项目下,项目中的组件、代码片段是可以相互之间复用的,在 MPA 下就相对麻烦。

这其实涉及到项目划分的领域,还是要看具体的需求也业务复杂度来定。如果说整个系统就是二三十个页面,这做成 SPA 使用前端接管路由高效简单,无可厚非。

但如果你本身在面对的是一个服务于复杂业务的 B 端系统,比如类似 阿里云、教务系统、ERP 系统或者一些大型内部系统,这种往往需要多团队合作开发。这种时候就需要跟完善的项目划分、组织协同和系统整合的方案。

这个时候 SPA 所体现出的优势在这样的诉求下就会相对较弱,在同等投入的情况下 MPA 的方案反而会有更少的执行成本。

也不是所有项目一开始就会想的那么清楚,或许一开始的时候就是个简单的 SPA 项目,但是随着项目的不断迭代,才变成了一个个复杂的巨石应用,现在如果再拆出来也会有许多迁移成本。引入微前端,则可以…

这大概是许多微前端项目启动的背景介绍,我想说的是:对于屎山,我从来不信奉“四两拨千斤”

如果没有想好当下的核心问题,就引入新的“银弹”解决问题,只会是屎山雕花。

项目协同,抽象和复用这些本身不是微前端该解决的问题,这是综合因素影响下的历史背景问题。也是需要一个个资深工程师来把控和解决的核心问题,就是需要面对不同的场景给出不同的治理方案。

这个道理跟防沙治沙一样,哪有那么多一蹴而就、立竿见影的好事。

模块加载这件事情,从玉伯大佬的成名作 sea.js 开始就是一个非常值得探讨的问题。在当时 jQuery 的时代里,这是一个绝对超前的项目,我也在实际业务中体会过在无编译的环境下 sea.js 的便捷。

实际上,不论是微前端、低代码、会场搭建等热门话题离不开这项技术基础。

import * from * 我们每天都在用,但最终的产物往往是一个自运行的 JS Bundle,这来自于 Webpack、Vite 等编译技术的发展。让我们可以更好的组织项目结构,以构建更复杂的前端应用。

模块的概念用久了,就会自然而然的在遇到浏览器环境中,遇到动态模块加载的需求时,想到这种类似模块加载的能力。

比如在遇到会场千奇百怪的个性化营销需求时,能否将模块的 Props 开放出来,给到非技术人员,以更加灵活的方式让他们去做自由组合。

比如在低代码平台中,让开发者自定义扩展组件,动态的将开发好的组件注册进低代码平台中,以支持更加个性的需求。

在万物皆组件的思想影响下,把一整个完整页面都看做一个组件也不是不可以。于是在一些团队中,甚至提倡所有页面都可以搭建、搭建不了的就做一个大的页面组件。这样及可以减少维护运行时的成本,又可以统一管控和优化,岂不美哉。

当这样大一统的“天才方案”逐渐发展成为标准后,也一定会出现一些特殊场景无法使用,但没关系,这些天才设计者肯定会提供一种更加天才的扩展方案出来。比如插件,比如扩展,比如 IF ELSE。再后来,就会有性能优化了,而优化的 追赶对象 就是用原来那种简单直出的方案。

有没有发现,这好像是在轮回式的做着自己出的数学题,一道接一道,仿佛将 1 + 1的结论重新演化了一遍。

题外话,我曾经自己实现过一套通过 JSON Schema 描述 React 结构的 “库” ,用于一个低代码核心层的渲染。在我的实现过程中,我越发觉得我在做一件把 JSX 翻译成 JS 的事情,但 JSX 或者 HTML 本身不就是一种 DSL 么。为什么一定要把它翻译成 JSON 来传输呢?或者说这样的封装其本身有意义么?这不就是在做 PHP、ASP 直接返回带有数据的 HTML Ajax 一样的事情么。

传统的浏览器运行环境下要实现一个模块加载器,无非是在全局挂载一个注册器,通过 Script 插入一段新的 JS,该 JS 通过特殊的头尾协议,将运行时的代码声明成一个函数,注册进事先挂载好的注册器。

但实际的实现场景往往要比这复杂的多,也有一些问题是这种非原生方式无法攻克的问题。比如全局注册器的命名冲突;同模块不同版本的加载冲突;并发加载下的时序问题;多次模块加载的缓存问题 等等等等等…

到最后发现,这些事情好像又是在用 JS 做浏览器该做的事情。然而浏览器果然就做了,<script type="module"></script>,Vite 就主要采用这种模式实现了它 1 年之内让各大知名项目切换到 Vite 的壮举。

“但我们用不了,有兼容性问题。”

哇哦,当我看着大家随意写出的 display: grid 样式定义,不禁再次感叹人们对未知的恐惧。

import.meta 的兼容性是另外一个版本,是需要 iOS12 以上,详情参考:https://caniuse.com/?search=import.meta

试想一下,现在的低代码、会场搭建等等各类场景的模块加载部分,如果都直接采用 ESM 的形式处理,这对于整个前端生态和开发体验来说会有多么大的提升。

模块加载,时至今日,本来就已经不再需要 loader。 正如 seajs 中写的:前端模块化开发那点历史

历史不是过去,历史正在上演。随着 W3C 等规范、以及浏览器的飞速发展,前端的模块化开发会逐步成为基础设施。一切终究都会成为历史,未来会更好。

文章的结尾,我想感叹另外一件事,国人为什么一定要有自己的操作系统?为什么一定需要参与到一些规范的制定中?

因为我们的智慧需要有开花的土壤,国内这千千万开发者的抱负需要有地方释放。

如果没有自己掌握核心技术,就是只能在问题出现的时候用另类的方式来解决。最后在一番折腾后,发现更底层的技术只要稍稍一改就可以实现的更好。这就像三体中提到的 “智子” 一样,不断在影响着我们前进的动力和方向。

不论是小程序、微前端还是模块加载。试想一下,如果我们有自己的互联网底蕴,能决定或者影响操作系统和浏览器的底层能力。这些 “怪啖” 要么不会出现,要么就是人类的科技创新。

希望未来技术人不要再去追逐 Write Once, Run Everywhere 的事情…

今天这个 Antd 是非换不可吗?

最近在思考一个可有可无的问题:

“我们是不是要换一个组件库?”

简单同步一下背景,我效力于 Lazada 商家前端团队。从接手系统以来(近 2 年) 就一直使用着 Alibaba Fusion 这套组件库。据我所知淘系都是在使用这套组件库进行业务开发,已经有 7 ~ 10 年了吧。我们团队花了 2 年时间从 @alife/next(内部版本已经不更新) 升级到了 @alifd/next,并在此之上建立了一套前端组件库体系。将 Lazada Seller Center 改了模样,在 Fusion 的基础上建立了一套支持整个 Lazada B 端业务的设计规范和业务组件库,覆盖页面 500+。

在这样一个可以说牵一发动全身的背景下,为何还敢有这种想法?

美的反义词,不应该是丑,而是庸俗

不能说 Fusion 丑,但绝对算不上美,这点应该没有争议吧。

虽然也可以在大量的主题样式定制的情况下也可以做到下面这样看上去还行的效果:

但说实话,这不能算出众。导致不出众的原因,可以从 Ant Design 上面寻找,Ant Design 的许多细节实现细到令人发指,比如:

  • 弹出窗的追踪动效

  • 按钮的点击动效

  • Tooltip 的箭头追踪

  • NumberPicker 控制按钮放大

这些细节决定了在它上层构建出的应用品质,同样是在一个基础上进行主题和样式的调整。有 Antd 这样品质的基础,就会让在此之上构建的应用品质不会很低,自然也能够带来更好的用户体验及产品品质。

拿 Antd 的仓库和 Fusion 相比,还是有蛮大的差距的,这些差距不只是技术水平的差距,可能在 10 年前他们的代码质量是差不多的,但贵在 Antd 是一个健康的迭代状态。

Antd 已经到了 5.x,Fusion 还是 1.x。这版本后背后意味着 Fusion 从 1.x 发布后就没有大的迭代和改动。即使是 DatePicker、Overlay 这类的组件重构也是提供一个 v2 的 Props 作为差别。

这背后其实反应出的是维护者对于这个库的 Vision (愿景),或许随着 Fusion 这边不断的组织变动,早就已经失去了属于它的那份 Vision。

所以当 Antd 已经在使用 cssinjs、:where、padding-block 这种超前到我都不能接受的东西时,Fusion 里面还充斥着各种 HOC 和 Class。

可以说,Fusion 已经是一个处于缺乏活力,得过且过的维护状态。如果我们不想让这种封闭结构所带来的长期腐蚀所影响,就需要趁早谋求改变。

得益于上述许多“耗散结构”的好处,Antd 的性能也比 Fusion 要好上许多。许多能够使用 Hooks、CSS 解决的问题,都不会采用组件 JS 来处理,比如 responsive、space 等。

稳定性,既体现在代码的测试质量,又体现在 UI 交互的表现稳定性。比如,Dialog、Tooltip 随着内容高度的变化而动态居中的问题( Fusion overlay v2 有通过 CSS 来控制居中,已经修复)。在很长一段时间内,我们的开发者和用户都承受着元素闪动带来的不好体验。

还有诸如 Icon 不对齐、Label 不对齐,换行 Margin 不居中等等,使用者稍微不注意打开方式,就会可能出现非预期的表现,这些都需要使用者花费额外的精力去在上层处理修复。有些不讲究的开发者就直接把这些丢了用户,又不是不能用。

“又不是不能用” , 而我们不想要这样

Antd 的投入有目共睹,一个 86K star,超过 25K 次提交的库,与 Fusion 的 4.4K star、4K commits。这种投入的比例完全不在一个量级,这还没有计算围绕 Antd 周边丰富的文档、套件等投入。

都是站在巨人的肩膀上,都是借力,没有理由不去选择一个活跃的、周全的、前沿的、生态丰富的巨人。

那既然我都把 Antd 吹成这样了,为什么这还需要思考,这还是个问题?无脑换不就行了?

或许社区的 Antd 生态非常强劲。但在内部,我们所有的生态都是围绕 Fusion 在建立。包括:

  • 设计规范
  • 业务组件(50+ 常用)
  • 模板 20+
  • 发布体系
  • 业务 External
  • … 等等许多

切换 Antd,意味着需要对所有现有生态进行升级改造,这将会是一个粗略估计 500+ 小时巨大的投入。

这将意味着我们会拦一个巨大的活到身上,做好了大家用,做不好所有人喷。

我们都会发现一个问题,所有 Antd 来做的业务都一眼能被认出来这是 Antd。

因为它太火了,做互联网的应该没有人没见过 Antd 做的页面吧。

辩证的来看,Ant Design 它就叫 “Design”,引入 Antd 还不要它的样式,那你到底想要什么?

“想要它的好看好用,还想让他看上去跟别人不一样”

别急眼,这看上去很荒谬,但这确实是在使用 Antd 时的一个很大诉求。

我认为 Antd 应该考虑像 Daisyui 这样提供多套的主题预设。

不是说这个能力 Antd 现在没有,相反 Antd 5 提供了一整套完整的 Design Token。

但插件体系或者说开放能力,真的需要在官方自己进来做上几个,才会发现会有这么多问题 😭

这就跟 Vite 如果不自己做几个插件,只是提供了插件系统,那它的插件系统大概率是满足不了真正的使用者的。

反正虽然 Antd 5.0 提供了海量的 Design Token,但我在精细化调整样式主题时,还是发现了许多不能调整的地方(就是没有提供这样的 TOKEN 出来)。

因为 cssinjs 的方案,说实话我也不知道应该用什么样的方式进行样式改写才算是最佳实践。

可以说,近一两年,随着 Vue 3、Vite、Tailwind CSS 等项目的大火🔥,又重新引起了我们对样式的思考。

Unstyled 这个词反复的被 Radix UIHeadless UI 等为首的项目提及,衍生出来的:Shadcn UIArk UI 等热门项目都让人有种醍醐灌顶的感觉。

大概是从 React、Vue 出现开始,UI 的事情就被绑定在了组件库里面,和 JS 逻辑都做好了放一起交给使用者。

但在此之前,样式和 JS 库其实分的很开的。如果你不满意当前的 UI,你大可以换一套 UI 样式库。同样是一个 <button class="btn"></button>,换上不同的 CSS,他们的样式就可以完全不一样。

但前端发展到了今天,如果我想要对我们的样式进行大范围升级,从 Element 换到 Ant Design 很可能涉及到的是技术栈的全部更替。

所以面对 cssinjs,我不敢说这是一个未来的方向,我花了很长时间去了解和体会 cssinjs,也确实它在一些场景中表现出了一些优势:

  • 按需加载,我不用再使用 babel-plugin-import 这类插件
  • 样式不在冲突,完美prefix+ :where hash样式 Scope 运行时计算,必不冲突。微前端友好!
  • ES Module,Bundless 技术不断发展,如果有一天你需要使用 ES Module,你会发现 Antd 5.x 这个组件库不需要任何适配也可以运行的很好,因为它是纯 JS
  • SSR,纯 JS 运行,也可以做 CSS 提取,InlineStyle 也变得没有那么困难

但说实话,这些方案,在原子化 CSS 中也不是无解,甚至还能做的更好。

但 Ant Design 底层其实也是采用 Unstyled 方式沉淀出了一系列的 rc-* 组件,或许有一天这又会有所变化呢,谁知道呢。

总之,我非常不喜欢使用 Props 来控制 Style这件事情。

也非常不喜欢想要用一个 Button,在移动端和 PC 端需要从不同的组件库中导入。

说实话,这个问题,我思考了很久。每次思考,仿佛抓到了什么,又仿佛没有抓到什么,其实写这篇文章也是把一些思考过程罗列下来,或许能想的更清楚。

最初科举考试是选拔官僚用的,其中一个作用是:筛选出那些能够忍受每天重复做自己不喜欢事情的人

或许畏惧变化、畏惧折腾,就应该用 Fusion ,因为可以确定的是 Antd 5 绝对不是最后一个大版本。

选择 Antd,也意味着选择迭代更快的底层依赖,意味着拥抱了更活跃的变化,意味着要持续折腾。

如果没有准备好这种心态,那即使换了 Antd,大概率也可能会锁定某个版本,或者直接拷贝一份,这种最粗暴的方式使用。然后进入下一个循环。

今天这个 Antd 咱们是非换不可吗?

我想我已经有了我的决定,你呢?

(ps. 为什么大家对暗黑模式这么不重视…)

(ps. 如果 Fusion 相关同学看到,别自责,这不怪你…)

为什么需要前端工程师?

之前我在《如何看待技术匠心》 的文章中提到,今后的一段时间,互联网行业都将会处于一个价值回归的状态。每个在互联网行业中的职业,都比以往更加需要知道 职业和个人的核心竞争力

  • 企业为什么需要前端工程师?
  • 当前国内环境下,前端工程师的核心竞争力是什么?
  • 如何创造更多前端岗位?

这几个问题我思考了很久,其背后所代表的目的是一样的。前端开发者,如何能够在这个洪流涌动的时代中更有竞争力。

当然,本文所提到的观点不一定正确,纯粹个人见解。

搞清楚这个问题之前,不如来看看早些时候,这个职业是从何而来?

其实早些年是没有前端工程师这个职业的,和这个职业最相关的应该是网页设计师,大家大多叫做 “切图仔”。主要工作就是把设计稿的图片转换成 html,将图片变成网页;过程就好像是拿美工刀把图片一片片切割下来,再拼成 html 的网站,所以俗称 “切图”。

大家都知道一个完整的网页主要是由3个部分:html, css, js

那时网页设计师最常用的工具其实是设计软件:Adobe Dreamweaver、Microsoft FrontPage,所见即所得的GUI操作方式来设计网页的布局、样式、内容,这些主要是 html 和 css 部分。

这是那个时候,网页设计师被雇佣所要处理的工作,也是网页设计师的价值体现。

其实在这段时间内,从图片转换到 html 的工作还是处于一个边界模糊的状态,有的公司会划分这部分内容给设计师,因为他们可以随时调整;有的公司会划分这部分内容给 Web工程师(PHP、ASP工程师),因为他们转换的产物会更加精准。

后来,随着浏览器技术的不断变革、多端设备的普及、网络条件的提升,这给Web这一终端提供了巨大的提升空间,同样也带来了更高的专业要求。

一个有经验的Web开发者 和 一个刚入门的Web开发者,所做出来的产品逐渐表现出了巨大的体验差异。

互联网企业们才逐渐关注到 Web 除了传递信息以外,还有兼容性、SEO、网站性能、动画效果、响应式等方面的价值。

于是部分公司专门设立了 “前端工程师” 这一岗位,专门来解决网页开发、动效制作、性能调优、兼容性等诸多事务。

但这一时期,可以感觉到前端的必要性依旧不是很高,对于部分对 Web端要求不是很高,或者拥有资深网页开发经验的后端工程师们(PHP、ASP工程师),大多百度查一查,多花点时间也一样能搞个七七八八。

再后来,
Node.js 出现了;
Webpack 出现了;
五花八门的DSL (.vue,.tsx,.scss,.less,.ts)出现了;
各种跨端技术出现了;

头部前端开发者们:哇,这MVVM;哇,这模块化;哇,这自动化;哇,这热更新;这要是做一套组件库,能省多少事啊!

生产力前端开发者们:这有个新组件库!哇,看着好炫酷;哇,好现代的交互;怎么用?恩?什么是 React? 什么是 Vue ?

技术管理者们:哇,这东西,这么炫的吗。这要是盘下来,咱们的项目维护成本不是一下子就下来了么,交付的产品不是一下子就专业了么。(下次跟老板汇报又有吹的了)

企业管理者们:咱们的愿景是,国内一流的XXX。“可是你们的产品看着好LOW,某某“小而美”的产品就很现代。” CTO:你去研究一下他们的技术方案。

小公司:老板,咱们追吧?现在投资人看到的产品都老酷炫了,咱们这个根本入不了眼,包装还是要包装一下的。

Web开发者(PHP、ASP工程师):蓦然回首,啥!这都是啥!老板,这活干不了。找个专业点的人进来吧。

于是就有了:

岗位要求:

  1. 本科或以上学历,计算机或相关专业;
  2. 至少X年以上web前端开发经验;
  3. 精通react框架、HTML5、CSS3、Javascript、等Web前端开发技术。
  4. 熟悉前端常用的构建工具(如:webpack ),熟悉使用 Git 工具;

幻觉主宰着大家放大一些关注点,视而不见一些本来该是问题的问题。

在过去的5-10年里,整个互联网行业都涌入了大量的从业者。各个互联网巨头以垄断式的方式抢夺人才,许多应届生莫名其妙的就倒挂了一些老员工。问为什么?没有为什么,其他公司就开了这个薪资。

或许在过去的很长一段时间内,涌入这个行业的从业者们可能都没有想过,或者也是没有功夫去想。这个职业,在解决公司什么环节的问题,在解决社会哪部分的问题。

在这个增速放缓,价值回归的时期里,或许大家可以静心思考下上述问题,本文抛砖引玉,欢迎更多有见地的开发者们评论回复。

首先我的观点是:前端工程师是一个翻译性质的结构洞岗位(各领域连接节点)

从前端职业的发展历程我们可以了解到,前端从一开始就在做把不同领域的事情整理、转换、展示的事情。

从某种意义上讲,前端工程师更像是一个 翻译师,将人类所能理解的内容,利用编程语言翻译给各个终端(APP、浏览器、小程序),再把这些信息序列化后存储、发布到服务器,通过网络传递给任何需要这部分信息的终端中重新展示,然后被另外一群人理解。

如果纯粹从信息传递的角度来看,人类的目前所有用于传递信息的方式都是低效的。

语言、文字、图片、表情,创造这些工具的目的都是为了让人们在沟通时,能够理解相互之间所要表达的内容、感受、情绪。所以,如果未来脑机接口真正普及,世界或许会消失很多东西,语言、文字、画面、前端或许都没有意义,因为信息传达不再需要翻译。

但目前来说,人们需要信息传递,互联网是当下最高效的传递方式,信息通过互联网传递需要终端显示,终端显示需要信息转换,信息转换需要前端工程师,所以企业、社会需要前端工程师(或者任何能完成这项工作的职业)。

所以我认为,衡量前端的标准,就是衡量翻译的标准:

  • 翻译的快不快(效率)
  • 翻译的准不准(转化率)
  • 翻译的符不符合语境(调性)

当下,许多前端工程师会把自己的定位理解成 “程序员” 的一个分支,认为自己的工作就是个写代码的,和后端相比只是写的语言不同。

所以除了代码以外的事情一律不管,什么界面好不好看、什么文字是不是符合语境、什么交互是不是合理,那些应该是产品和设计考虑的事情,我只需要把设计稿变成网页,把接口调通就可以。许多中层管理者也会把前端和那些服务端开发者们拉在一起横向比较,所衡量的指标也是:业务表现、稳定性、点击率 等等,这些有没有都无关痛痒的指标。

这些现象之所以会在近几年表现的如此明显,是因为这个行业涌入了许多迷茫从业者,许多认为这是一条赚钱的好路子的从业者。

在培养了4、5年算法、数学,刷LeeCode后进入社会的科班生,他们主导这个职业的漏斗,当然是程序员那套路子,面试题逐渐变成了算法、协议、隐秘的框架特性这些八股文🤒。现在我几乎很少看到有企业会出 html/css 的题目,但前端的职业定位并不是一个单纯的程序员,如果单从程序员的竞争力来看,前端是不可能超过一个后端开发者的,这也不是企业雇佣前端的核心价值。

前端这项职业,既然是站在结构洞上,最大的竞争力就应该是充分发挥结构洞所代表的特性和价值,充分融合、连接各种领域的优点,然后转换体现在产品上面。

这对前端的要求就不仅是在编码层面的能力了,一个符合互联网综合性要求的前端岗位应该拥有以下几方面能力:

  • 有审美,了解用户体验法则,懂得现代Design System (!important)
  • 了解当代主流设备的 Web应用差异及渲染方案,能够 实现各端界面开发
  • 了解 网络协议,浏览器特性等终端传输转换原理
  • 了解 现代前端工程化工具,帮助企业能够更规范、更稳定、更高效的产出
  • 了解互联网 主流产品运行规则,这里包含SEO、语义化、META等
  • 了解当代 后端应用部署方案攻防手段,包含CDN、Docker、SSR 等等等等
  • 会做人,会换位思考,有责任心,能够照顾市场、企业、项目质量等各方利益
  • 会表达,能够简短、清晰、完整的表达内容,毕竟你就是做表达的

很多读者看到这不仅拍桌子了。好家伙,公司所有活你一个人干了得了,CEO你来当!这哪是前端啊!

但回顾下整个前端的发展史,这是一个发展了才没几年的职业。在当下,这一个职业做的事情本来就不是什么需要多么资深研究才能做到的事情,这是一个需要广度、需要审美、需要持续的热情、需要应对变化、需要对着丑陋拍桌子说不的职业。 一个企业或许需要创造框架的技术性人才,但不是每个团队都需要。

前端这些年之所以发展的这么火热,正是因为站在 互联网热潮结构洞 上。许多已经发展到近乎死水的技术领域,因为有前端的跨界融合发生了改变,前端工程师们既可以写界面、又可以写脚本、还可以写接口、老板夸两句还能造个APP出来 😂。这对于老板来说简直是冬日里的小棉袄,互联网界的瑞士军刀啊!

这是前端近年火热的关键,因为前端在结构洞上,他们可以了解各个领域的事情,他们懂审美懂设计,他们可以高效的摆平许多事情。

反过来站在企业的角度上看,需要招一个前端团队,来解决的 最基本诉求是界面开发,能够将公司运转的逻辑、数据、情绪表达出来。

如果发现招来的前端,界面做的奇丑无比;从他身上找不到一点时尚、朝气;面对各端设备只会说 “这个做不到”;这样的前端对于企业来说,或许真的没有太大的必要,这种程度的网页开发,或许找一个后端🤓 逼他一把 把他变成全栈,应该也可以 Cover 了💅

如果你不认同上述观点也是正常,毕竟我的资历尚浅,算是阶段性总结,抛砖引玉,可以在评论区留下你的见解,尊重深度思考。

如果你也认同上述观点,觉得上述思考对你有帮助,或许我们需要总结一些告诫出来。


如果你是初期前端开发者,我建议你慎重的思考一个问题,进入这个行业是不是你心之所向?你是否能在接下来很长一段时间内保持学习的热情?

如果不是我建议你慎重考虑进入这个行业,这个行业已经不是那么容易捞钱了,并且在接下来一段时间内会变的更难,如果你没有足够的热情,或许真的会搭一个黄金年华进去,等到30好几再寻求出路。


如果你是中、高级前端开发者,我建议你能够在繁忙的工作中,抽出时间多做总结和思考。

不光关于技术,而是像这样的职业思考、行业思考、人生思考,想想清楚自己的核心竞争力和职业规划,尝试写下来。

你必须要想想接下来一段时间里,或许不再是顺风顺水,如果有一天自己没了工作,还能用什么换得酬劳?

如果有,就有意识的培养那项技能,让它默默生长;如果没有,抓紧时间寻找到它。

因为那些东西才是你光环褪下的核心价值。


如果你是技术管理者,想清楚团队在公司大图的位置,企业希望这支团队处理哪些工作,再决定团队的发力方向。

团队的价值及影响力,需要整个团队抱元守一的长期打磨,不是一两个 几个月就能做完的明星项目。

团队多招几个人,今年多晋升几个能如何,失去团队的核心竞争力一样会被干掉。圈地圈资源,圈的越多砍的越多。

技术团队需要体现出职业的专业度,团队的特殊性和积极的参与度。一如既往的高品质、高效率、高凝聚就是团队的护城河,长期打磨的产品才是闪闪发光的团队名片。

企业之所以一年比一年更苛刻的跟技术要业务数据,就是觉得 年年汇报375,产品反馈不相符,季季喊着缺人手,内耗比武心伤透。

价值回归的时代,企业会逐步着手识别那些白兔团队。

放下争名逐利,着手做点正确的事。


如果你是企业经营者,建议你重新想想需要什么样的技术团队,给管理者明确的价值导向是非常重要的环节,开发者之间分工不同,定位不同,导向不同。

作为企业都会有成熟的管理工具和衡量标准,但成熟的管理工具就有成熟的应对策略,清晰识别那些企业所真正需要的东西对于企业的长期发展来说是非常有必要的。

如果你真的认为开发者可以像销售、运营团队使用业务数据指标来衡量,那开发者们就会变得像销售、运营团队那样,整天盯着数据,满口的业务指标,听上去好像是上下齐心的客户第一。

但又想想,当初高薪招他们来是干什么的呢?🤔

Formily 2.0 深度实践

Formily 作为阿里巴巴旗下开源的一套非常火热的表单解决方案,目前已有 7.8k Star,针对表单这一领域场景,以非常完整、高效、先进的方式解决了开发表单过程中能遇到的几乎全部问题。

面向企业级表单的专业解决方案,专业!

Formily 2.0 正式发布至今已有 7 个月,作为 Contributor 之一,我将以一个企业级复杂度的表单——商品发布,作为应用场景做一个实践总结。

初学者都说 Formily 的学习成本高、理解不了,如果说理解 Formily 最难理解的部分我想应该是 表单数据模型响应机制 的两大问题,而这也恰好是 Formily 的精髓所在。

历史的经验总是对人类有帮助的,几十年前,人类创造出了 MVVM 设计模式。这样的设计模式核心是将视图模型抽象出来,然后在 DSL 模板层消费,DSL 借助某种依赖收集机制,然后在视图模型中统一调度,保证每次输入都是精确渲染的,这就是工业级的 GUI 形态!
刚好,github 社区为这样的 MVVM 模型抽象出了一个叫 Mobx 的状态管理解决方案,Mobx 最核心的能力就是它的依赖追踪机制和响应式模型的抽象能力。


Formily 贯彻实现了真正的 MVVM 设计模式,在 Vue 框架中,大家都说 MVVM 但绝大多数开发者也只是理解了响应机制。什么是 Model?谁会在写组件的时候强调,我这个组件的 Model 是什么?大多数开发者说的只是 props、data、state 这些。

Formily 跳脱于任何框架,先回归数据模型本身,非常细致、全面的梳理了表单这一场景中的所有 模型、字段、事件、生命周期。如果细细查看模型的每个字段含义,就能感受到其所说的 “企业级表单” 的深厚经验。总之,关于表单,你能想到的、没想到的,这个模型里面都有了;如果还有人说表单很简单,可以带他去看看 Formily 的表单、字段模型定义

当有了模型之后,还有一个问题需要处理:事件;与其说事件不如说是状态响应,就是说我们在对这个表单模型的某个字段修改之后如何让页面中的元素进行同时变化,最容易想到的是在每个模型字段变更的地方利用 EventEmitter进行事件冒泡,然后在组件的各种生命周期中进行订阅,然后做出响应。

而这也是 Formily 1.x 中的做法,这样确实很大程度的解决了模型和视图的关联,但也同样带来了非常多的事件冗余和性能问题。

在 React 场景下实现一个表单需求,因为要收集表单数据,实现一些联动需求,大多数都是通过 setState 来实现字段数据收集,这样实现非常简单,心智成本非常低,但是却又引入了性能问题,因为每次输入都会导致所有字段全量渲染,虽然在 DOM 更新层面是有 diff,但是 diff 也是有计算成本的,浪费了很多计算资源,如果用时间复杂度来看的话,初次渲染表单是 O(n),字段输入时也是 O(n),这样明显是不合理的。

要说 2.0 中最大的变化,莫过于引入了 @formily/reactive 作为表单模型的状态响应。利用 Mobx 的设计思想,可以将模型中的每个字段变为最小颗粒度的观察单元,这样模型中的每个字段就可以在 任意地方被订阅变化;同时,巧妙的 依赖追踪机制 更可以让开发者在无感知的情况下,订阅最少的字段。

举个简单的例子:

有一个name的字段,在 Formily 中它便是一个 Field 的字段模型,该模型中会存在 value \ errors 等字段,然后通过视图组件将对应的数据信息给渲染出来。 按照 React 的 state 设计思路来看,其中任意一个字段修改都应该让组件重新执行渲染,但是假如我们的组件中只使用到了value,那么errors 变化的时候组件渲染就是多余的。

而实际情况是一个完整的表单中将会有更加复杂的字段定义,更多的表单字段和联动,那么按照这样的渲染模式来说,性能将必然是一个极大的问题。

而神奇的 @formily/reactive 则巧妙的通过 getter 获取组件运行过程中使用过哪些字段,从而完成动态的订阅和订阅释放,从而达到最小单元的订阅响应,这才使得整个表单在渲染过程中能够更加 “精准” 的渲染。

想了解更多 MVVM 思想应用可查看:Mobx@formily/reactive

有了以上这样一个及其灵活、高效、精准的 “响应模型”后,便可以在此基础上按照 React \ Vue 的方式灵活订阅消费了。并且,能够真正做到,哪个字段修改,哪些 “需要响应”的地方才响应。

商品发布中,有一个非常复杂的场景:笛卡尔 SKU 表格

大致意思就是通过商品属性设置,动态计算出所有的 SKU,用户只需要填写对应的价格即可。

就是这样的一个设计,产生出了无数问题,其中性能问题更是最头疼的问题。

一开始我们是基于 Formily 1.x 进行开发,正如我之前讲到的,基于事件流,整个表格在渲染过程中有非常多的事件订阅如(onFieldValueChange$);另外不同字段之间还存在校验联动,比如 Special Price 价格不得大于 Price 等。当有商品属性修改时,整个表格组件的渲染情况是这样的:

其中,那些FormItem相关项(需要输入)的渲染可了不得,它们的渲染会触发对表单模型的事件通知,这就会导致一个原本普通的render有非常多的副作用,在运行过程中的事件流下面这样的:

所以我们第一版中,大概编辑到 50 多个 SKU 就已经是上限了,再往上就会有非常久的响应时长。并且随着表格列的逐步增多,产品迭代逐渐复杂,这个性能问题就变得越来越突显。

在 Formily 2.0 正式发布后,我们也开始从新的模式获得到了灵感,开始着手 “抢救” 这个性能大坑 😷。

最小订阅

升级 Formily 2.0 后,因为其最小订阅特性,使得我们从事件流中脱离出来,减少了非常多的字段和状态判断。

借助这样的最小变更订阅,使得表格在结构不变的情况下(比如修改某个商品属性的名称),每个 Field 不再执行重复的渲染和事件冒泡。

键值映射

面对笛卡尔智能组表的这个场景,我们一开始没有多想,Value 直接就按照如下结构进行了表单设计:

&#123;
  skus: [
    &#123; id: "xxxxx1", price: 10 &#125;,
    &#123; id: "xxxxx2", price: 10.2 &#125;,
  ];
&#125;

这个接口乍一看上去似乎没什么问题,也很直观,但实际上却存在非常大的问题。因为一旦数组的顺序发生变化,就会使得与其关联的所有 Field 都执行重新渲染,甚至事件冒泡。

在笛卡尔 SKU 的场景中,如果我们新增一个商品属性,就会在原有表格的中间插入新增的 SKU。就会使得后面的SKU 顺序都发生了变化,原来是 sku.5.price 现在就变成了 sku.6.price,这也导致表格中的这些单元格重新渲染,另外也会同时触发非常多的 onValueChange 事件。

解:

正如 Formily 作者在《解密 Formily2.0》中提到的:数组转置算法

因为我们知道了数组的具体操作类型,那我们的状态转置,不就是将字段 A 的地址变成字段 B 的地址么?就是这么简单,所以我们每次操作的时候做一次全量遍历,寻找到要替换的字段模型,替换掉地址即可,模型完全不需要任何改动,当然这里面还有很多细节,但是整体思路就是这样的,总之这样的思路从根本上解决了 Formily 一直在数组状态转置上的痛点。

实际上,自动组表SKU 数据 这本来就不是同一回事,如果把他们拆分开来看的话就会非常清晰。

简单理解,顺序、数量、组合关系这些表格长什么样是由另一个字段的值(销售属性)来决定的,仅此而已,没有 Value 一样可以完成组表这个动作,而这个过程很快,不是性能瓶颈。

之所以性能差,是因为在渲染单元格时引入了 Formily 作为字段状态管理,这个过程又因为组表过程中的顺序变化,导致了大量 Value 的变化,从而引起了大量订阅事件被触发,引起大量无效渲染。

但其实从数据本质来看,新增商品属性,表格多几行,其实对于数据本身是没有任何影响;每个 SKU 的价格是多少、库存是多少,这些应该是一个独立的 键值数据结构

&#123;
   skuValue: &#123;
      xxxxx1: &#123; id: 'xxxxx1', price: 10 &#125;,
      xxxxx2: &#123; id: 'xxxxx2', price: 10.2 &#125;
   &#125;
&#125;

这样的好处是,在定义 SKU 值 Form中的路径时,不在关心表格的顺序如何,数据路径将变成:

sku.0.price-> skuValue.[id].price

只要 SKU**id**不变则数据字段不变,随便表格结构如何变化,都不会再影响 Value 变化;不论顺序如何,都能精准的绑定对应的 SKU 数据。当有新增的销售属性时,也只会渲染和变更对应位置的组件。

表格 Diff

另外还有非常非常影响性能的问题,这个锅来自 Fusion ,也就是我们用的组件库。

我们都知道 React 会有 diff 算法来判断虚拟 DOM 树需要如何同步变更到真实 DOM,而在这个过程中 key 的定义就被作为组件 diff 的重要标识。

在执行变更 DOM 的过程中,如果 React diff 判断结果是INSERT_MARKUP(插入)或者 REMOVE_NODE(删除)则会执行组件的 mount \ unmount,这对于 Formily 便意味着很多很多的模型状态变化 (mount\visible 等) ,又会引起非常多的订阅响应。

比如:现在表格中有 4 个 SKU,原始数据中我会给每个 SKU 提供一个id作为这个dataSourceprimaryKey

在表格渲染过程中,Fusion 在行级组件(Row) 提供的 Key 是当前 SKU 的 id,但是在单元格(Cell)渲染时提供的却是${rowIndex}-${colIndex}这就意味着当前单元格,在以下任意一个关键值发生变化的时候都会执行销毁和重新挂载

  • 当前行数据的 ID (主键)
  • 当前行顺序
  • 当前列顺序

实际上,对于表格来说,如果主键(primaryKey)是确定的,那么 “当前行顺序” 的影响因子就应该被主键所取代。

也就是说,SKU 的数据只要在 dataSource 中,就不应该执行该行单元格的销毁,不论其顺序如何变化。

目前该问题已经提 PR 给 Fusion#3953

FormPath 也是 Formily 2.0 中一个非常好用和有特色的能力,相比于 Formily 1.x 中的 cool-path ,它提供了更加清晰、易用、强大的路径管理能力。

可以说 Formily 中的路径处理是我见过最强的树形路径处理工具库,可能作者是觉得 Formily 已经有太多包了,所以把这个强大的工具库也放在了 @formily/core 里面(也可能懒),但这不妨碍它真的很强大。

只要稍微复杂一点的表单数据,一定会和各种树形路径打交道,很多时候拼接路径、查找路径、解构路径都是需要开发者自行处理的,这些东西说复杂也不复杂,说简单也不简单。

很多开发者可能会觉得,不就是几个路径处理么,无非是多几个点少几个点的事情,我自己处理下不就得了。但这种事情写一次两次还行,写多了,就变成屎了。

何不稍微花点时间学习下它的语法,来把这个测试覆盖率 99% 的路径工具库用起来呢。另外,我们之前讲过,因为是 Reactive 的模型基础下,如果自己写这类路径处理方法也有可能引起不必要的副作用。

下面粘几个非常不错的例子,一起来感受下吧:

field.query(".aa").value(); //直接读取同级别aa字段值
field.query("..aa").value(); //读取父级aa字段值
field.query("..[+].aa"); //读取跨级相邻aa字段值

目前我们业务中已经使用 Formily 2.0 完成了全新的重构升级,依赖显著减少(rxjs、stylecomponents),重构过程中删除了非常多的兼容代码,当然性能也 巨幅提升

在商品发布场景下,200 多 个 SKU (表单项 1200+) 在 Formily 1.0 (上个版本) 中基本是 不可用状态,但我们为了对比测试,还是做了一组数据记录如下:

Formily 1.0 重构前 Formily 2.0 重构后 优化幅度
表单初始化时长 196529ms 5843ms 缩短 97.2%
值变化,SKU 表结构改变 40983ms 774ms 缩短 98.1%
值变化,SKU 表结构不改变 69465ms 1520ms 缩短 97.8%
* 以上测试数据均产出于 开发模式,250 个 SKU,表单项 1500+

引入Vite = 多一个月

Vite 在21 ~ 22年在前端界可谓风生水起,颠覆传统也不为过。
20年4月,尤雨溪发推说:“我感觉我再也回不到Webpack了”,Webpack作者用中文直呼:大哥… 在22年的今天,我们再看这个Twitter是不是感觉这声 “大哥” 喊得歇斯底里 😂。

到底Vite有什么魔力,可以让全世界的前端开发者们争先恐后的投入怀抱呢?
如果只说一个特点,那就是 “快”,不是传统概念的那种快个百分之多少,是tm的几十倍几百倍的快!

什么概念呢,Vite 可以让你的项目甭管多大,1秒内启动;热更新更是快的离谱,几乎是保存的一瞬间就看到效果。说实话,第一次尝试的时候,我惊呆了,我从来没见过这样的速度,从来没体会过这样的开发体验,这个世界上还有这种东西,我感觉我也回不去了。

在技术研发团队中,有一个数字需要知道,每节省40分钟(8小时/21天),一年将会多出一个月。

一个 Vite 的引入,对于一些重型项目,尤其是对于那些非内存文件编译的项目,每天节省40分钟都算是比较保守的估算(一次热更新5秒,一次冷启动3分钟)。其次,对于开发者来说,每次等待编译思路的中断都是相当大的精力损耗。

Ps. 如果你们团队超过12个人,引入 Vite 可能就意味着团队里面多了一位开发者,一个在平均水平的开发者。

YY完之后,咱们还是得考虑些实在的问题,这东西为什么快,它是不是还是个玩具,引入到咱们这样的大公司会不会出问题,会不会对咱们工作流程会不会有很大的冲击。

实际上,关于Vite的原理说简单也简单,说复杂也复杂,要真正理解还是要回顾下前端编译史的衍变过程:

  1. 无编译,直接运行(jQuery)
  2. 简单混淆、加密、兼容性补全编译
  3. Webpack模式编译(React、Vue等自创性语法,Coffee,Sass\Less,TypeScript,Pug\Jade,多端)
  4. 局部编译,工程化拆分
  5. Native ESM 浅编译、不编译
    1. https://www.skypack.dev/
    2. https://airpack.alibaba-inc.com/

所以为什么Vite快,我简单总结一下如下:

  1. 浏览器已经长大了,可以自己处理ES Module了,大家就别费劲转成一根筋的Bundle了
  2. 对于一些源码项目(src下的)大多用了些DSL,根据文件后缀做个简单转换,这个过程非常快
  3. 对于Deps,大多都是些处理好的CJS,直接上大哥 esbuild 10 ~ 200倍的编译速度,在上点文件缓存,还不给它整的服服帖帖

当然更多信息请参考官方文档《为什么选Vite》

当冷启动开发服务器时,基于打包器的方式启动必须优先抓取并构建你的整个应用,然后才能提供服务。
Vite 通过在一开始将应用中的模块区分为 依赖源码 两类,改进了开发服务器启动时间。

  • 依赖 大多为在开发时不会变动的纯 JavaScript。一些较大的依赖(例如有上百个模块的组件库)处理的代价也很高。依赖也通常会存在多种模块化格式(例如 ESM 或者 CommonJS)。Vite 将会使用 esbuild 预构建依赖。esbuild 使用 Go 编写,并且比以 JavaScript 编写的打包器预构建依赖快 10-100 倍。
  • 源码 通常包含一些并非直接是 JavaScript 的文件,需要转换(例如 JSX,CSS 或者 Vue/Svelte 组件),时常会被编辑。同时,并不是所有的源码都需要同时被加载(例如基于路由拆分的代码模块)。Vite 以 原生 ESM 方式提供源码。这实际上是让浏览器接管了打包程序的部分工作:Vite 只需要在浏览器请求源码时进行转换并按需提供源码。根据情景动态导入代码,即只在当前屏幕上实际使用时才会被处理。

“知其然还知其所以然”之后,回归现状,看看手里头有些什么,可以从哪个地方入手把它逐步引进来。

首先,借助我们之前在团队内部推行的一套的微前端加载方案:《如何接”地气”地接入微前端?》。目前,我们已经收敛了了一套统一的页面编译规范和方法。

也就是说对于绝大多数的业务项目来说,项目中没有任何关于编译的配置。这样的话我们可以便捷的针对该场景下的产物做一层适配,使得我们开发者可以从原来的lzd start换为lzd vite,无缝启动原来的项目。

针对产物,我们之前的页面内容将会被编译成为一个标准的微前端子应用,然后通过加载器来加载对应页面内容,而加载器则是消费一个 UMD规范 的产物。

为了让整个流程符合Native ESM的消费路径,我们简单处理了一个Vite 转发插件,使得加载器加载到的JS是一份符合UMD规范的静态JS,然后再由这段静态JS创建一个<script type='module'>完成Vite消费路径,顺便加了些热更新的适配,可以让我们代理模式下也能享受Vite的热更新。

就这样花了一下午的时间,我们简单粗暴的完成了Vite的适配,愉快的享受在了电脑风扇不在呼呼呼、按下cmd + s就能看到效果的幸福时光里。

生产环境

那么如此简单粗暴的做法显然不适合在生产环境里面使用。实际上对于业务项目,我们一开始也没有打算使用Vite去做生产环境的产物构建,对于技术选型还得从消费场景来看。

针对生产环境中的兼容性、稳定性、包大小等要求,使得我们对Vite使用Rollup的产物还是没有十足的把握。
实际上在Vite的实现中对于开发态和生产态的编译流程本身也有差异,许多插件都需要针对开发态和生产态做兼容性处理的。

所以综上所述,Vite其实对于生产环境的编译速度提升不大(况且这也不是开发体验的痛点),与其在一个不熟悉的Rollup生态下摸爬滚打,不如生产环境就让它保持原来的Webpack进行编译。差异或许是存在的(一开始我们也这么认为),但应该都可以在统一的Builder中做更新处理。

实际上,结果比我们一开始预期的要好太多,跑了1年多下来,Vite与Webpack的差异出现过1、2次,主要是出现在CSS加载的顺序上面,有时会导致部分样式覆盖优先级的变化。

我们在脚手架中依旧保留lzd start方式使用 Webpack 进行启动,临时用 Webpack 启动适配即可。

我们团队引入 Vite 已经一年多的时间了,过程中有一些小的磕绊,但总体受到前端开发者们的一致好评!目前基本上团队95%的项目已经切换使用Vite开发,许多用过 Vite 以后的小伙伴,回到自己原来的项目第一件事就先来试试看能不能给它改成 Vite。

从发展趋势来看,我认为Vite是一个大的集合,集合了无数个变革过程中的优秀项目,从而形成的一个奇点。

Vite 仿佛一扇大门,带我们看到一个新的领域,带我们真正感受了一把 “量变形成质变”,带我们真正感受到了技术带来的价值。

我相信,Vite 只是个起点,现在的加载方案效率上也还远没到最佳的状态,现在的方案也还没有完全打破 “社区DSL” 与 “浏览器原生” 的壁垒,未来一定还会有许多精彩的改变不断出现,或许我们就是在路上的缔造者😜。

端上的插件设计

在设计一套客户端软件,尤其是基础模块时,我们通常会考虑到扩展性。而插件机制是一套非常好的解决思路,可以让其他开发者按照我们预期的路径进行功能扩展。

那么在软件初期,我们要如何设计一套较为完善的插件模式呢?本篇将会从我个人对插件的懵懂认知开始,逐步介绍如何形成一个较为完善的解决思路。

Plugin(Plug-in, addin, add-in, addon 或 add-on)是一种计算机应用程序,它和主应用程序(host application)互相交互,以提供特定的功能。

首先,我们经常会在各种软件里面看到 "Plugins" 等关键字,那怎么理解这个词呢?

在我刚接触计算机的时候,看到一些大型软件里面会说可以装某个插件,从而实现一些功能,当时对插件的概念就是,这是一个对主应用程序的扩展,大多都是一些小功能。比如Photoshop里面的抠图插件,后来又出现一些滤镜插件,调色插件等等,其中大部分还是需要 付费 才能开通的插件。

在大学开发和销售微信墙的时,在 “Money💰” 的驱使下才算对插件有了真正的概念 😂

可以看到,整个产品首先是基于互动墙源码(包含 站点的后台设置、前台展示、移动端展示、基础数据库表等),这部分基础功能包含了整体的软件形态、插件设计和数据流向。

可以说基础包里面的内容是技术难度最大,代码最多的部分,但是其定价却是最低的。我的客户可以以最低的成本获得到我的核心模块,但是它却没有什么实质性的互动功能;而互动的能力都是以各种插件作为增值服务进行选购,从而构建了一个完整的 “有利可图” 的 B2B2C 互动生态。

在用户接入部分,通过插件,客户可以根据自己的用户群体选用平台进行接入:

  • 微信公众号
  • 企业微信
  • 钉钉
  • 微博

在互动玩法部分,客户也可以通过选用自己活动所需要的互动插件;每个插件都是独立的源码包,选用安装后方可在后台中找到选项开启。

通过这套基础的插件化设计,我构建出了一个完整互动模块系统,它可以为我构成一个小小的商业模式:

  1. 渐进式的服务体验,客户门槛低
  2. 可升级、可复购、可定制的持续商业关系
  3. 独立、安全的插件市场

我大学的“小摊子”也正因为这样的“耗散结构”运转下,越做越大。后面包含所有插件的完整源码可以卖到 8000 一套。

所以怎么理解插件?在我的概念里,插件最终应该是一个独立的安装包,在安装这个插件之前应用程序中应该不包含任何与这个插件有关的代码、配置、数据表。这样我才可以拿去安全地卖更多的钱钱 🤩,不是吗?

而在技术上来看,插件其实是 AOP 设计的一种产物,它把内核部分作为一个开放式模块,将主流程的各个部分“开槽”开放给插件定制,从而达到功能可插拔,渐进式扩展的工程化方案。

所以,在插件设计时,我们可以先确认这个基本指导原则,非侵入式,非具象需求,只把控主应用流程。

底座,也就是内核,先有内核模块才能有插件生态。首先,我们在做一个基础底座之前,一定是要搞清楚我们这个底座的定位是什么。

拿一些典型的例子来说明:

  • IDE,是编码
  • 播放器,是播放
  • 浏览器,是看网页
  • Webpack,是编译

搞清楚核心定位后,首先要做的是 完成最小单元的开发建设,并且在这个最小单元的流程节点上“开槽”。

比如,我们正在做一个针对 运营、产品、技术的工具库:i18n Tools

其主要定位是提供一套实用工具库来帮助公司内部各环节的同事,快速了解和改善 Lazada 卖家中心的体验。通过这套工具,我们可以提供给所有安装了工具的同事们一条便捷的路径来了解和改善我们的产品。

从这个产品定位来看,它的主路径就是打通浏览器扩展,提供一套能够将 页面元素和产品数据关联 同时还具备插件开关、自动更新等能力的 基础交互模型

基于这个定位,底座要考虑的事情就会有以下几项能力:

  • 版本检查,自动更新
  • 插件功能管理(开关、配置)
  • 页面元素索引、定位及渲染方案
  • 浏览器扩展方案

所以在这种基础底座能力开发过程中,就不要有关于具体功能的逻辑掺杂。

比如:需要找什么样的元素、找到元素后应该如何渲染,这些具体的业务逻辑便可以直接交由插件来具体实现。

这个过程看似浑然天成、理所应当,但在项目初期大多是以一个具体的功能需求作为目的,所以实现过程中很容易写出脚本式的代码,忽略了可以作为内核能力提供的交互模型抽象。

还有一点,在底座开发完成后,一定需要自己做一两个预置插件,完全套用底座所提供的开槽实现,如果自己开发插件的过程都觉得很别扭那一定不是一个好的底座设计。

关于如何构建插件,我之前在文章《为你的 JavaScript 库提供插件能力》有过一个解决方案,感兴趣可以阅读。

首先在设计设计插件前期之前还是务必要了解主基座的 生命周期 预制插件,好比我们写一个 Webpack 插件或者 Vite 插件。如果在还没有了解其本身内核的运行机制、生命周期、接口定义就着急动笔的话,很可能出现各种实现都非常奇怪,时常侵入修改内核的上下文以便达成插件效果。

实际上,尤其是在开发这种大型开源项目插件时,要相信如此多的开发者曾经一定遇到过相似的场景,开发插件时如果遇到一些无法解决的问题时,可以按照以下步骤寻求解法:

  1. 查看接口定义,看看该接口中还有什么可以用的方法
  2. 想想官方提供的插件中,是否有类似场景(一般来说一定会有)
  3. 去对应社区搜索相关问题,看看能否找到一些相似的仓库借鉴
  4. 如果还是不能解决,或许要重新想想这个功能的实现,是否适合放在这个插槽里了

当然,这里面也有例外,对于一些较为初期的项目,最好还是在开发插件之前能够认真阅读下内核源码和预置的插件源码。对于初期项目,还没有经历过很多用户“磨炼”,实际上插件开发者就是要抱着Contributor的心态,如果遇到一些实在需要扩展解决的问题,也要抓住机会,大胆提出 PR。

另外插件上线完之后,作为插件开发者也需要多多订阅和关注核心库的更新日志(CHANGELOG),对于一些严谨的内核项目一般都会在发布 X、Y 位版本之前预先发出betaRC版本。

这个时候插件开发者还是要抽时间跟进维护,否则很容易因为主库的版本更新导致插件不可用。如果遇到Break Change更新,在更新插件版本时,记得对旧版本做主库的上限版本控制,如: ~2.0.0,保持旧版本的可用性。

随着 Web3.0 不断发展,互联网客户端插件化开放会越来越明显,插件设计、模块设计 都是构建自己生态圈,实现社区共赢,达成商业激增的有效手段。

今后这种插件化接入其他生态、或者构建生态的方式只会越来越多,作为前端开发者最好能够提前适应这种模式,总结出一套这种场景的应对方法。

Vmo助你在Hooks盛行的前端年代里,找到面向对象的方式

@vmojs/decorator 一个帮助你更好,更快地创建前端数据模型的工具库,可以让你对数据处理过程更加简单,更加灵活。

关于前端使用数据模型这个话题我已经写过很多次相关的介绍,并且也确实在我们的业务项目里面实践过非常多的成功案例。经过一开始的 @vmojs/core 的继承数据模型到现在的 @vmojs/decorator 纯粹装饰器模式使用,期间有过很多次思考和打磨,接下来我想再次聊聊前端的数据模型应用和前端面向对象编程这个话题。

首先,面向对象的设计模式在一切复杂系统设计中都是被无数次验证过的成功经验,而在当代 Hooks 盛行的前端年代里,是否就代表着我们不再需要面向对象的设计模式了?是否代表着曾今盛行的 Class 不再有价值了?

先来看看,现在React Hooks 及 Vue 3 的 Composition API 在解决什么问题?或者说,聊聊它们为什么这么受欢迎?

首先不可否认的是,相比于Class写法来说,Function确实更像是无感的定义了一个函数。相对于沉余的定义 继承、渲染、生命周期、状态 来说,它确实非常轻便的就可以完成一个组件的开发。

在关于TypeScript的定义中,也能明显感觉到,如果使用Class开发过相对健全的组件的同学一定知道以下这些React定义的以供其上下文通信及校验的组件属性:

  • React.Component<IProps, IState>
  • Component.defaultProps
  • Component.propTypes
  • Component.contextType
  • Component.childContextTypes
  • Component.getChildContext

这些方法在TypeScript类型定义的推导中真是要折磨死人,常常要各种类型断言才能继续下去。关于类型校验,书写完ts的类型再书写propTypes的类型更像是在写食之无味的八股文,让人无奈抓狂。

Function的写法则充分利用了ts的函数推导过程,让其类型仅需定义一次,纯粹借助ts的类型推断来进行组件入参声明,这极大程度的减轻了开发者在这类重复定义上的开发和维护工作。

Hooks 提供将 逻辑与状态 独立抽离的的复用能力。

之前这种能力大多以MixinHOC等形式被使用于各类渲染引擎或系统设计中。这种设计从使用的角度来看,确实能够达到部分逻辑复用和部分组件交互相同的效果。但是,也为调试过程带来了非常大的难度,我曾经就在一个基于HOC设计的渲染引擎中寻死觅活,因为有近 8 层HOC来处理一个组件渲染所需的不用表现逻辑,乍一看上去似乎是分层合理,逻辑清晰。但实际上在使用反面会带来以下几个问题:

  • 消费黑盒,高阶函数往往会在组件渲染过程中消费一些Props,甚至偷偷吃掉一些Props,开发者遇到一些Props下传后渲染异常,往往需要花很长时间才能找到问题是出现在哪一层
  • 组合性差,一开始设计高阶函数的过程中往往会觉得其灵活性、复用性应该都会非常高,只需要让组件包裹一下就可以复用这部分逻辑能力。但是,这种迭代器模式的设计,如果多个HOC组合使用,往往会存在前后依赖关系,这为使用过程中带来了极大的隐患
  • 嵌套地域(Wrapper Hell),在 React Devtools 查看和调试某个组件的时候十分困难抓狂,感受一下

而 Hooks 则很好地解决了这些问题,借助一个状态的封装和组合,我们可以将一系列需要对状态做处理的逻辑抽象,其组合模式更加灵活,只需开发者理解 React Hooks 组件渲染周期的概念即可无副作用的进行不同状态逻辑组合。

Hooks 的封装 和 组件封装 都是用Function的调用方式,开发者可以一眼看明白其入参出参,ComponentHooks 的区分也相对比较明确,分别以 useXXXXCxxxxx 区分。

总体来说相对与Class方式的复用逻辑抽象,Function的这种方式确实更加简洁和易理解一些。

刚刚描述了为什么在前端主流的框架中逐步放弃了Class,但我这里主要想讲的内容是当前的前端项目中,为何需要留Class,主要能解决一些什么问题。

首先,我们看下为什么主流的服务端框架都是在使用Class为基础的方式展开系统设计:

利用Class定义,我们可以将传输过程中一个定义模糊的数据内容,转换为一个字段明确、类型明确的类,从而完成安全调用,清晰处理数据流转关系。

怎么理解上面这句话呢,我稍微举几个简单的例子:

比如我们需要渲染一张订单表格,假如拿到的数据如下:

[
  &#123;
    "buyerAvatar": "https://xxxxxx.cdn.com/avatar.png",
    "buyerId": 1000001,
    "createTime": 1637230483,
    "orderNumber": "60000000000001",
    "orderType": "Normal",
    "preOrder": false,
    "shippingFee": 1.49,
    "deliveryType": "STANDARD",
    "buyerName": "today",
    "freeGift": false,
    "skus": [
      &#123;
        "createTime": 1637230483,
        "createDate": "10 Sep 2021",
        "image": "https://xxxxxx.cdn.com/product.png",
        "itemStatus": "repacked",
        "orderItemId": "60676801622031",
        "orderType": 0,
        "skuName": "商品名称 - black",
        "productTitle": "商品名称",
        "quantity": 1,
        "skuId": "test0000001-black",
        "skuInfo": "1:black",
        "unitPrice": "200.00"
      &#125;
    ],
    "status": "topack",
    "totalQuantity": 1,
    "totalRetailPrice": "0.00",
    "totalUnitPrice": "200.00"
  &#125;
]

这样一份数据当前端拿到的时候,会存在以下几个问题:

  • 无法非常直观的看到其所描述数据内容,每个字段需要有过一遍校对理解后才能使用
  • 无法清晰看到其所描述数据之间的关联关系,在获取信息过程中无法通过类型清晰理解
  • 当存在多个数据的联合判断界面中某些交互时(如 tabStatus 为 unpack 时,显示某个 action),这类判断逻辑在渲染过程会显得格外臃肿,在调试排查中也很难溯源

当然对于前端数据解析和使用过程中,其实还是有非常非常多让前端开发者无奈和吐槽的点。

实际上,这些问题其实是针对数据处理的问题。而面对这些问题,常年和数据打交道的服务端同学是如何做的呢?

Class 、数据模型自然而然的呼之欲出,来看看上面这个内容在数据模型的处理方式是如何的:

首先上面较为模糊的数据经过前端实例化转换将会变成上面这样较为清晰的数据结构,建议对比数据多看两边这个图

然后在数据方面使用就会变得非常通透,比如一个订单字母表渲染所需要的数据便是:

interface List &#123;
  list: Order[];
  pageSize: number;
  total: number;
  current: number;
&#125;

因为是一个实体类的缘故,在数据消费过程中,会变得异常清晰。

遇到需要扩展计算的数据处理时,在对应模型下进行方法扩展也变得顺理成章。

众所周知,ES7 关于 JS 装饰器的提案通过后,@Xxxx 装饰器这个能力已经被各大主流服务端框架积极采纳利用,比较有代表性的如:MidwayNest.js

借助装饰器及类元数据进而衍生借鉴的依赖注入等设计模式,已经将服务端数据、服务、消费灵活组合玩的淋漓尽致。

以前需要借助各种目录规定定义、甚至特殊命名规则定义来完成相互调用和组合,现在只需要按照不同装饰器进行修饰就可以在项目任意位置进行自由组合,不得不说在这服务端数据传输,服务调用具有非常划时代性的意义。

TypeScript 作为微软推出的编译态类型语言,在当今各大主流框架和前端社区中都非常受欢迎。

大家知道在微软的项目中是非常推崇Class模式的,诸如:VscodeMonico Editor,如果开发过相关 vscode 插件 的同学会很明显地感受到,关于类的概念在微软内部是流淌在血液里面的。

甚至从某种意义上来说TypeScript就是微软不满足于当前JavaScript对类、类型的支持,从而扩充出来的一套语法超集。借助这套超集才能助力微软利用 JS 能力完成像Vscode这么复杂又稳定的项目。

所以,TypeScript 对于类的支持就像是自家儿子一样,不能更了解

使用类定义的数据模型在TypeScript使用起来,会给你如沐春风一样的感觉,你要写什么编辑器就好像都知道一样。

近期结合 Github Copilot 用起来,更是感觉只要定义好数据模型,写什么函数给一行注释就够了 😂

上面我已经讲了非常多,关于Class这个话题,当前社区发展的两个倾向,接下来我想聊聊我在项目中是如何实施结合的。

我始终认为这是两个领域上的领域优势,他们做解决的问题和处理的内容是不同的。

关于组件、对于界面渲染,有更优的、更简单的调用方式,有更加灵活的状态逻辑抽象方式,没有道理不用。所以 对于组件渲染,我用 React Hooks

关于需要长期维护的数据处理、数据转换、数据消费,我选择用数据模型。

他们各司其职,相得益彰,在项目中可以相互印证,借助Context + Model 模式可以显著提升项目整体稳定性和维护性。

这么说来貌似是一种设计模式,一种编程习惯,为什么还需要 @vmojs/decorator 呢?

@vmojs/decorator 主要用来解决在数据初始化过程中,重复、无味地赋值过程,减少胶水代码。

比如我需要实例化一个Order类:

class Order &#123;
  id: string;
  price: number;
  status: EStatus;
  skus: Sku[];
  createTime: Date;

  constructor(data: IRemoteOrderDTO) &#123;
    this.id = data.orderNumber;
    this.price = data.totalUnitPrice;
    this.status = data.status;
    this.skus = data.skus.map((sku) => new Sku(sku));
    this.createTime = new Date(data.createTime);
  &#125;
&#125;

// new Order(data) => Order;

利用 Vmo 就可以快速完成该赋值过程,并且在未来字段新增时,其字段赋值转换逻辑也都在一处。当模型规模增大时,也不想用上下翻找重复处理转换过程:

import &#123; Vmo &#125; from "@vmojs/decorator";

@Vmo()
class Order &#123;
  // 该定义为了 ts 代码提示
  constructor(data: IRemoteOrderDTO) &#123;&#125;

  @Vmo("orderNumber")
  id: string;

  @Vmo("totalUnitPrice")
  price: number;

  @Vmo()
  status: EStatus;

  @Vmo((&#123; skus &#125;) => skus.map((sku) => new Sku(sku)))
  skus: Sku[];

  @Vmo((&#123; createTime &#125;) => new Date(createTime))
  createTime: Date;
&#125;

从某种意义上来说,我们都是在一个江湖中,前端社区的发展也是这样,某个框架、某个设计模式、某个热度很高的开源项目都是无数个背后的思考结晶。

并不是某个框架就是权威,某种做法就是无懈可击,只是作者当时场景下的最佳实践,所以越是高明的开发者,越想要了解某项技术栈的发展史,这更有益于理解其背后所代表的思考、背后衍变所代表的趋势、背后所代表的江湖。

Anyway,本篇文章虽然是在介绍一个工具库,但大部分内容还是在说我对当前时代下,前端开发者对于数据处理的个人最佳实践分享。希望对你有帮助!😄

Vite + React 组件开发实践

去年,在 “阿里技术” 上发表的《这一年我的对组件的思考》 介绍了借助 TypeScript AST语法树解析,对 React 组件Props类型定义及注释提取,自动生成组件对应 截图、用法、参数说明、README、Demo 等。在社区中取得了比较好的反响,同时应用在团队中也取得了较为不错的结果,现在内部组件系统中已经累计使用该方案沉淀 1000+ 的 React组件。

之前我们是借助了 webpack + TypeScript 做了一套用于开发React组件 的 脚手架套件,当开发者要组件开发时,即可直接使用脚手架初始化对应项目结构进行开发。

虽然主路径上确实解决了组件开发中所遇到的 组件无图无真相、组件参数文档缺失、组件用法文档缺失、组件Demo缺失、组件无法索引、组件产物不规范 等内部组件管理和沉淀上的问题,但 webpack 的方案始终还是会让组件开发多一层编译,当一个组件库沉淀超过 300+ 时,引入依赖不断增长,还是会带来组件编译上的负荷导致开发者开发体验下降。

Vite 给前端带来的绝对是一次革命性的变化,这么说毫不夸张。

或许应该说是 Vite 背后整合的 esbuildBrowser es modulesHMRPre-Bundling等这些社区中关于 JS 编译发展的先进工具和思路,在Vite这样的整合推动下,给前端开发带来了革命性变化。

我很早就说过,任何一个框架或者库的出现最有价值的一定不是它的代码本身,而是这些代码背后所带来的新思路、新启发。所以我在写文章的时候,也很注重能把我思考最后执行的整个过程讲清楚。

Vite 为什么快,主要是 esbuild进行pre-bundles dependencies + 浏览器native ESM 动态编译,这里我不做过多赘述,详细参考:Vite: The Problems

在这个思路的背景下,回到我们组件开发的场景再看会发现以下几个问题高度吻合:

  1. 组件库开发,实际上不需要编译全部组件。
  2. 组件开发,编译预览页面主要给开发者使用,浏览器兼容可控。
  3. HMR(热更新) 能力在 Vite 加持下更加显得立竿见影,是以往组件开发和调试花费时间最多的地方。
  4. Vite 中一切源码模块动态编译,也就是TypeScript类型定义JS注释 也可以做到动态编译,大大缩小编译范围。

那么,以往像StoryBook和之前我们用于提取tsx组件类型定义的思路将可以做一个比较大的改变。

之前为了获取组件入参的类型数据会在webpack层面做插件用于动态分析exporttsx组件,在该组件下动态加入一段__docgenInfo的静态属性变量,将从AST分析得到的 类型数据 和 注释信息 注入进组件JS Bundle,从而进一步处理为动态参数设置:

TypeScript 对组件Props的定义

分析注入到JS Bundle中的内容

分析转换后实现的参数交互设置

所以对于组件来说,实际上获取这一份类型定义的元数据对于组件本身来说是冗余的,不轮这个组件中的这部分元数据有没有被用到,都会在webpack编译过程中解析提取并注入到组件Bundle中,这显然是很低效的。

Vite的思路中,完全可以在使用到组件元数据时,再获取其元数据信息,比如加载一个React组件为:

import ReactComponent from './component1.tsx'

那么加载其元数据即:

import ComponentTypeInfo from './component1.tsx.type.json';

// or
const ComponentTypeInfoPromise = import('./component1.tsx.type.json');

通过ViteRollup 的插件能力加载 .type.json 文件类型,从而做到对应组件元数据的解析。同时借助Rollup本身对于编译依赖收集和HMR的能力,做到组件类型变化的热更新。

以上是看到Vite的模块加载思路,得到的一些灵感和启发,从而做出的一个初步设想。

但如果真的要做这样一个基于ViteReactRax 组件开发套件,除了组件入参元数据的获取以外,当然还有其他需要解决的问题,首当其冲的就是对于 .md 的文件解析。

参照 dumiIcework 所提供的组件开发思路,组件Usage 完全可以以Markdown写文档的形式写到任何一个 .md 文件中,由编译器动态解析其中关于jsx,tsx,css,scss,less的代码区块,并且把它当做一段可执行的script 编译后,运行在页面中。

这样既是在写文档,又可以运行调试组件不同入参下组件表现情况,组件有多少中Case,可以写在不同的区块中交由用户自己选择查看,这个设计思路真是让人拍案叫绝!

最后,如果能结合上述提到 Viteesbuild,动态加载HMR能力,那么整个组件开发体验将会再一次得到质的飞跃。

所以针对Markdown文件需要做一个Vite插件来执行对.md的文件解析和加载,预期要实现的能力如下:

import &#123; content, modules &#125; from "./component1/README.md";

// content README.md 的原文内容
// modules 通过解析获得的`jsx`,`tsx`,`css`,`scss`,`less` 运行模块

预期设想效果,请点击放大查看

一个常规的组件库目录应该是什么样的?不论是在一个单独的组件仓库,还是在一个已有的业务项目中,其实组件的目录结构大同小异,大致如下:

components
├── component1
│   ├── README.md 
│   ├── index.scss
│   └── index.tsx
├── component2
│   ├── README.md
│   ├── index.scss
│   └── index.tsx

在我们的设想中你可以在任意一个项目中启动组件开发模式,在运行vite-comp之后就可以看到一个专门针对组件开发的界面,在上面已经帮你解析并渲染出来了在README.md中编写的组件Usage,以及在index.tsx定义的interface,只需要访问不同的文件路径,即可查看对应组件的表现形态。

同时,最后可以帮你可以将这个界面上的全部内容编译打包,截图发布到NPM上,别人看到这个组件将会清晰看到其组件入参,用法,截图等,甚至可以打开Demo地址,修改组件参数来查看组件不同状态下的表现形态。

如果要实现这样的效果,则需要一套组件运行的Runtime进行支持,这样才可以协调React组件README.mdTypeScript类型定义串联成我们所需要的组件调试+文档一体的组件开发页面。

在这样的Runtime中,同样需要借助Vite的模块解析能力,将其URL**/*/(README|*).html的请求,转换为一段可访问的组件Runtime Html返回给浏览器,从而让浏览器运行真正的组件开发页面。

http://localhost:7000/components/component1/README.html
-> 
/components/component1/README.html 
->
/components/component1/README.md
-> 
Runtime Html

正如我上述内容中讲到的,如果利用Vite添加一个对tsx的组件props interface类型解析的能力,也可以做成独立插件用于解析 .tsx.type.json 结尾的文件类型, 通过import这种类型的文件,从而让编译器动态解析其tsx文件中所定义的TypeScript类型,并作为模块返回给前端消费。

其加载过程就可以当做是一个虚拟的模块,可以理解为你可以通过直接import一个虚拟的文件地址,获取到对应的React组件元信息:

// React Component
import Component from './component1.tsx';
// React Component Props Interface
import ComponentTypeInfo from './component1.tsx.type.json';

// or
const ComponentTypeInfoPromise = import('./component1.tsx.type.json');

由于这种解析能力并不是借助于esbuild进行,所以在转换性能上无法和组件主流程编译同步进行。

在请求到该文件类型时,需要考虑在ViteServe 模式下,新开线程进行这部分内容编译,由于整个过程是异步行为,不会影响组件主流程渲染进度。当请求返回响应后,再用于渲染组件Props定义及侧边栏面板部分。

在热更新过程中,同样需要考虑到tsx文件修改范围是否涉及到TypeScript类型的更改,如果发现修改导致类型变化时,再触发HMR事件进行模块更新。

以上都是在讨论组件在ViteServe态(也就是开发态)下的情况,我们上文中大量借助Vite利用浏览器es module的加载能力,从而做的一些开发态的动态加载能力的扩展。

但是Vite在组件最终Build过程中是没有Server服务启动,当然也不会有浏览器动态加载,所以为了让别人也可以看到我们开发的组件,能够体验我们开发时调试组件的样子,就需要考虑为该组件编译产出一份可以被浏览器运行的html

所以在Vite插件开发过程中,是需要考虑在Build状态下的编译路径的,如果是在Build状态下,Vite将使用Rollup的编译能力,那么就需要考虑手动提供所有组件的rollup.input(entries)。

在插件编写过程中,一定需要遵循Rollup所提供的插件加载生命周期,才能保证Build过程和Serve过程的模块加载逻辑和编译逻辑保持一致。

我一开始在实现的过程中,就是没有了解透彻ViteRollup的关系,在模块解析过程中依赖了大量ViteServer提供的服务端中间件能力。导致在考虑到Build态时,才意识当其中的问题,最后几乎重新写了之前的加载逻辑。

我姑且把这个方案(套件)称之为vite-comp,其大致的构成就是由Vite + 3 Vite Pugins构成,每个插件相互不耦合,相互职责也不相同,也就是说你可以拿到任意一个Vite插件去做别的用途,后续会考虑单独开源,分别是:

  • Markdown,用于解析.md文件,加载后可获取原文及jsx,tsx等可运行区块
  • TypeScript Interface,用于解析.tsx文件中对于export组件的props 类型定义
  • Vite Comp Runtime,用于运行组件开发态,编译最终组件文档

结合Vite,已经实现了Vite模式下的ReactRax组件开发,它相比于之前使用webpack做的组件开发,已经体现出了以下几个大优势:

  1. 无惧大型组件库,即使有2000个组件在同一个项目中,启动依旧是<1000ms
  2. 高效的组件元数据加载流,项目一切依赖编译按需进行
  3. 毫秒级热更新响应,借助esbuild几乎是按下保存的一瞬间,就可以看到改动效果

启动

Markdown 组件文档毫秒级响应

TypeScript类型识别

Vite 现在还是只是刚刚起步,这种全新的编译模式,已经给我带来了非常多的开发态收益,结合Vite的玩法未来一定还会层出不穷,比如Midway + lambda + Vite的前端一体化方案也是看得让人拍案叫绝,在这个欣欣向荣的前端大时代,相信不同前端产物都会和Vite结合出下一段传奇故事。

我是一个热爱生活的前端工程师! Yooh!🤠

数据模型在电商前端领域的应用

Vmo 是我在 18 年发布的一个工具库,用于快速创建数据模型,当时我写了一篇文章:Vmo 前端数据模型设计

在社区得到过一段时间的关注,当时我还在 xx 兔,从事三维装修相关的项目。在图形学的背景基础及海量复杂的数据的情况下,自然而然在前端则会衍生出一种数据处理、解析、消费的技术方案,也种下了我对数据模型概念的种子。

简单举个例子:

需要解析一个三维装修的房子的数据会有哪些呢?房子(Hourse),楼层(Layer),房间(Room),墙体(Wall),墙面(WallSpace),墙角(Corner),吊顶(Ceiling),踢脚线(Skirting),地(Floor,带厚度),地面(FloorSpace),门(Door),窗(Window)。

以及会延伸出来大量的变体,比如飘窗,直角飘窗,弧形窗,墙洞,楼梯等等。

在解析这些数据中存在非常多的相互关联和计算,比如 房间需要和墙面,墙面需要和墙体关联,墙体和最多 2 个房间关联,墙角和多个房间关联,墙角和多个墙体关联等等

面对这样海量、复杂的数据,如果只靠着一个 API 请求的结果消费显然是非常不可取的方案,先不说这些数据能不能正确的解析出来,就说这些数据如何维护,保存时如何收集到所有数据反向序列化给后端都是些头疼的问题。

当然这些问题在当时我们抽象的各个数据模型中得到了解决,如果想了解具体细节可以查看我之前的文章。

今天我想讲的是,在我加入阿里后,一直在思考的关于数据模型的两个问题:

  • 是不是数据模型这种事情对于常规项目没有使用场景或者价值呢?常规的,像一些数据查询,或者填写一些数据提交。这种需求里面有必要使用什么抽象类,什么数据模型吗?
  • 为什么在前端圈子里面,很少有看到这方面的内容,现在前端圈子里大多都是在走向函数化,Composition等等,是不是这条路子走的有问题?

在寻觅了 2 年后,主导 Lazada 商家端的商品发布页面重构时,仿佛找到了一些答案。

首先在新增一个商品的过程中,实际上是用户在以客户端的形式制作一组商品数据,常规的前端视角来看就是提交一份“JSON”。

而编辑就是通过 API 拉取这份“JSON”解析到 Form 表单中,让用户进行编辑后,再将这份“JSON”提交。

那么粗略的将数据抽象为模型将会成为这样:

Well,到目前为止,我们做的事情都感觉像是在脱裤子放屁,多此一举。哈哈哈,各位看官暂且勿喷,稍安勿躁 😅 。

那么为什么需要把这些数据抽象为一个类呢?我拿一下几个 Case 来说明:

很多时候,前端把对数据的请求和处理是写在组件中的,更优一点可能会封装在某个聚类里面,或者某个 Hook 里面,调用时轻巧的拿到状态和数据。

像商品这样的数据请求方式会存在多种:草稿中获取,编辑中获取,某个类目中获取(不同类目下,商品属性不同)

每种获取方式请求的接口和参数组合方式可能不同,但最后前端消费的产物却是相同的。按照策略模式来说,对于一个商品模型的获取只是使用了不同的策略,但产物却是一致的,消费端无论调用何种方式,获取到的结果都是可靠的 Product 模型类。

有经验的前端都知道,很多时候,在一个项目有一轮轮的迭代后,我们的接口数据往往会存在部分数据需要前端做一定处理或者转换。

面对这样的数据处理,如果放在一个组件或者 Hook 中,是不太合适的,在做单元测试或者数据消费的时候都可能会给我们带来一些阻力。

在我看来,调试一个数据问题最好的办法,就是写一个单元测试,对单元测试预期的结果进行调试,往往比我们在浏览器中 Mock 一份数据调试数据更高效,对将来的稳定性也更有帮助。

安全感,数据消费起来,一个类和一份 JSON 给开发者带来的安全感和爽感是完全不同的。消费过数据模型 或者 次一点 消费过Interface的小伙伴,我相信对这一点是非常认同的。

哈哈,说到这里有些杠精小伙伴也问了,你说的这个我们用Interface也能达到同样的效果呀。好,咱们继续…

什么叫计算性消费数据的,说的简单点,就比如:

class Person1 &#123;
  fistName = "Wang";
  lastName = "Yee";

  get fullName() &#123;
    return `$&#123;this.lastName&#125; $&#123;this.fistName&#125;`; // Yee Wang
  &#125;

  get fullNameCN() &#123;
    return `$&#123;this.fistName&#125; $&#123;this.lastName&#125;`; // Wang Yee
  &#125;
&#125;

上面这个例子非常经典且清晰,元数据中可能只是些基本数据,但是很多时候前端需要根据不同场景来进行元数据组装,以往这些数据往往会被封装为各个方法,或者被当做template写在组件中,散落在各个角落,每当用到这份数据时可能又会重新按照场景组装一遍。往往这种时候就会存在 需求缺失,比如某情况下需要将之前所有消费到fullName的地方改为小写。

拿到商品发布来说,计算性消费数据到底有哪些应用场景呢?

在此之前,我想先解释一下SKU这个数据模型,它其中最核心的元数据是:value: Map<SKUProperty, SKUValue>;

按照上图这个表格中所示,可以看到该商品共有 6 个 SKU,第一个 SKU 所对应的SKU模型数据应为:

class SKU &#123;
  value = new Map([
    [
      new SKUProperty(&#123; id: 1, label: "Color Family" &#125;),
      new SKUValue(&#123; id: 101, label: "Red" &#125;),
    ],
    [
      new SKUProperty(&#123; id: 2, label: "Size" &#125;),
      new SKUValue(&#123; id: 201, label: "33" &#125;),
    ],
  ]);
  price: string;
&#125;

像这样一个 SKU Model,它所具备的元数据已经可以清晰描述当前 SKU,而且可以通过 SKU 的扩展方法做到很多有用的数据,比如:

  • getProperties() 获取该 SKU 有所有属性,如:Color Family,Size
  • getValues() 获取该 SKU 所有Value,如:Red,33
  • isEqual(anotherSKU: SKU): boolean 比较一个 SKU 是否和当前 SKU 完全相同,这在后续的数据合并中非常有用
  • getValueByPropertyId(id: string) 通过 PropertyId,获取一个 SKUValue

相比与只是一个 Object 对象来说,数据模型能够带来非常多的数据处理和数据扩展能力,当某种情况下需要消费由该数据产生的计算性消费数据时,可以很轻易的进行扩展使用,对于数据结构也有更好的预期和掌控力。

结合对该数据模型的单元测试,就可以清晰快速的开发数据层,当数据层可靠后,在视图层消费就会变得行云流水,得心应手了。

举个单元测试的例子:

it("alias sku equal", () => &#123;
  const data = [
    &#123;
      text: "300MB",
      value: 2988,
      name: "p-1",
    &#125;,
    &#123;
      text: "Blue",
      value: 2888,
      alias: "Blue1",
      name: "p-2",
    &#125;,
  ];
  const sku = SKU.fromData(data);
  expect(
    sku.isEqual(
      SKU.fromData([
        &#123;
          text: "300MB",
          value: 2988,
          name: "p-1",
        &#125;,
        &#123;
          text: "Blue",
          value: 2888,
          alias: "Blue2",
          name: "p-2",
        &#125;,
      ])
    )
  ).toBeFalsy();
&#125;);

这种SKU,是一种类型较为特殊的SKU,它其中会存在alias字段,当有这种字段时,在做SKU比对时,不但要对SKUPropertySKUValue的ID做比对,还需要对alias字段做比对。

所以按照上面的单测来看,结果应该是false,因为这两份数据中的alias是不同的。没办法,这是一个业务需求。

如果在视图层做数据比对时,使用的是纯数据进行比对,很有可能漏掉这部分逻辑,这就会导致项目变得捉襟见肘,拆东墙补西墙。

反正,在消费层遇到很多的需要对数据处理或判断时,大可以将这部分能力交给数据模型来处理,由数据模型来保证数据的稳定性。

使用数据模型,还可以帮你清晰管理数据关系,比如商品和SKU之间,SKU和SKUProperty,SKUValue之间的关系。

我举个具体案例:

这是一个商品编辑时组 笛卡尔积(Cartesian product) 的过程,当我们的SKU属性被用户添加或者修改时,将会触发笛卡尔积的重新计算出最新的排列组合结果。

比如当用户新增一个尺码为35时,笛卡尔积将会多出两项组合结果。同理,如果当维度增加一列时,比如添加材质维度,将会产生更多SKU结果。

以往,前端开发者总会将这部分计算过程封装成为一个数学方法,放在utils中随时调用,这看起没什么问题。

如果将这个过程看做是,一个SKUCollection数据模型的构建过程的话,一切就会将变得顺理成章:

test('sku calculate whether valid', () => &#123;
  const skuCellection = SKUCollection.fromData(&#123;
    'p-3xxxx': [
      &#123;
        text: '300MB',
        value: 2,
      &#125;,
      &#123;
        text: '128GB',
        value: 3,
      &#125;,
    ],
    'p-4xxxx': [
      &#123;
        text: 'Blue',
        value: 3,
      &#125;,
      &#123;
        text: 'Red',
        value: 15,
      &#125;,
      &#123;
        text: 'Green',
        value: 1,
      &#125;,
    ],
  &#125;);

  expect(
    skuCellection.value
  ).toEqual(
    // 6 SKU Model
  ); 
&#125;);

有了这样一个数据模型结构后,就可以清晰的通过数据模型来调用其相关的数据和计算性数据。

另外,不同的数据模型虽然相互依赖,但对数据解析和计算性数据缺相互独立,可以做到独立使用和单元测试

商品发布本质上是一个较为复杂的表单提交页面。由于字段多,交互复杂等原因,在产品设计过程中,就已经将很多字段先拆分为不同模块,来减轻用户心理负担。

比如会存在:基础信息,商品属性,详描,运费等。

在填写过程中,会存在部分 前端校验 + 后端校验 的场景。

在数据提交或者其他数据写入过程中,后端同时会处理字段校验,当后端发现某个字段填写错误时,服务端将返回错误信息及错误字段信息。

为了更好的交互体验,前端将会根据返回获取到字段信息,定位到对应的字段位置,显示错误信息并报红,另外还需要根据当前字段判断其所归属的模块进行报错。

还有一种情况是:服务端的第一层校验通过,调用其他商品上游链路时抛出异常,此时上游链路可能已经丢失字段信息,面对这样的异常数据,前端需要展示在表单顶部,并且提供traceId,以便追踪定位异常。

这样的异常数据,通常处理都需要和后端反复确认不同Case的表现情况,有些异常甚至很难出现一次,我们在迭代过程中往往会因为一些组件变动或者逻辑变动丢失这部分数据消费能力。

就商品发布来说,显而易见的”保存”的动作是一个需要处理异常的情况,所以我们会在提交的地方写上很多后端返回异常时的处理逻辑。

当有一天,有另外一个迭代需要写入操作时,同样也会产生异常的情况,这些的异常情况再次处理时又会有很多数据转换和错误显示的逻辑。

如果收到这份后端返回数据,将他转换为异常数据模型,然后交由视图层消费,这样会让所有异常模型下需要处理的逻辑复用避免交互逻辑丢失。

当然,视图层如何更巧妙的消费该数据模型又是另外一个有意思的设计,此处暂且不表,后面我还会写一篇专门介绍商品发布的视图层状态管理设计。

在商品发布中,除了上述提到的几个数据模型以外,其实还构建了一些其他类型的数据模型,如:运费模型,商品质量分模型,类目推荐模型等… 然后由这些多个子模型共同组合成为一个商品的模型。

这样的数据模型在消费起来,开发者其实不会太过关心究竟需要请求什么API,返回的数据究竟是什么样的,他们的返回是否要处理、转换、兼容等问题。

同时,这样高质量的数据模型其实不依赖于视图层的框架,它可以被抽离作为一个独立的包来管理维护,然后在其他页面引入使用,比如商品域可能遇到的:商品管理,商品选择,运费编辑,商品质量分预览等等…

回到开头,我提到的问题:

  • 是不是数据模型这种事情对于常规项目没有使用场景或者价值呢?常规的,像一些数据查询,或者填写一些数据提交。这种需求里面有必要使用什么抽象类,什么数据模型吗?
  • 为什么在前端圈子里面,很少有看到这方面的内容,现在前端圈子里大多都是在走向函数化,Composition等等,是不是这条路子走的有问题?

首先肯定的是,在我所使用的过程中,数据模型确实非常清晰,有力,牢固的解决了我所面到的业务问题,所以它是有价值的。

至于和常规的需求,到底应该用什么好呢?哈哈,这个问题有个比较无赖的回答,小孩子才考虑什么要什么不要,成年人什么都要,没有什么技术是非黑即白的。

Vite 就只能在Vue的项目里面使用吗?

什么合适用什么,简单的数据查询展示不需要这么精细的数据处理,当然可以直接拿来即用咯,解决业务问题的方法就是好方法!

至于Composition API,其实在商品发布的重构过程中,基本绝大多数都是使用这种设计思路来实现的,这样的设计确实能让我们清晰的分辨每个方法是干什么的,是否会影响交互,以及这样的交互是在做什么,每个交互都在一个位置维护和处理,后面我会单独写一篇介绍。

实践过程中发现,数据模型和Composition API并不冲突,一个是用来处理数据层,一个是用来处理视图层,它们相辅相成结合一些订阅模式的设计,就会让整个项目的划分异常清晰,我十分建议大家在以后遇到单点项目较为复杂时能够使用这一套思路来解决业务问题!

Tks for your time!! 如果你也认可我文章中提到的思路,别忘了点个赞再走哦!

如何接"地气"的接入微前端

微前端,这个概念已经在国内不止一次的登上各大热门话题,它所解决的问题也很明显,这几个微前端所提到的痛点在我们团队所维护的项目中也是非常凸显。

但我始终认为,一个新的技术、浪潮,每每被讨论最热门的一定是他背后所代表的杰出思考。

“微前端就是…xx 框架,xx 技术”

这种话就有点把这种杰出的思路说的局限了,我只能认为他是外行人,来蹭这个词的热度。

在我所负责的项目和团队中,已经有非常大的存量技术栈和页面已经在线上运行,任何迭代升级都必须要保证小心翼翼,万无一失。

可以说,从一定程度来讲,微前端所带来的这些好处是从用户体验和技术维护方面的,对业务的价值并不能量化体现,落地这项技术秉着既要也要还要的指导方针。

我们对存量技术栈一定需要保持敬畏,隔离,影响范围可控的几个基本要素,然后再考虑落地实施微前端方案。

所以,在这个基本要素和指导方针下。要落地这项新的技术时,一定充分充分了解,当前改造站点所存在的技术方案、占比 以及 当前成熟微前端框架已提供的能力差异,切勿生搬硬套。

我所在团队维护的项目都是些 PC 操作后台(Workstation),这些工作台会存在不同的国家,不同时区,不同合作方等等问题。

如果需要开发一个新的页面需求,很可能投入进来的开发人员都来自不同团队,此时我们要在完成现有需求的同时还需要保证多个管理页面的风格统一,设计规范统一,组件统一,交互行为统一这非常困难。

当该业务需要迁移到另外一个工作台时,虽然需要保持逻辑一致,但导航栏、主题等却不同。

当前存量的方案都是采用 Java 直接进行 Template 渲染出 HTML,经过前面几代前辈的迭代,不同系统中已经存在几种不同技术栈产出的页面。

虽然都是 React 来实现的,但是前辈们都非常能折腾,没有一个是按照常规 React 组件形式开发出来的。

比如:

  1. 大部分页面是通过一份 JSON 配置,消费组件生成的页面。
  2. 部分页面是通过另外一个团队定义的 JSON 配置消费组件生成的,与上面 JSON 完全不一样。
  3. 还有一部分页面,是通过一套页面发布平台提供的 JS Bundle 加载出来的。

面对这样的技术背景下,除了微笑的喊 MMP,含泪说着自己听不懂的话(存在即合理,不难要你干吗?),还得接地气出这样一个落地方案。

首先,需要明确的分析出站点所有页面,所需要加载的通用特性:

上述是精简过后的一些通用功能特性,这里简单做下介绍:

  • Layout Loader 用于加载不同工作台的导航
  • DADA Loader 用于加载 JSON 配置的页面
  • Source Code Loader 用于加载 JS Bundle
  • Micro Loader 用于处理微前端加载
  • Log Report 用于日志埋点
  • Time Zone 用于切换时区
  • i18n 用于切换多语言
  • Guider 用于统一管控用户引导

除此以外可能还会存在以下这些页面扩展能力:

  • 安全监控
  • 流量管控
  • 弹窗管控
  • 问卷调查
  • 答疑机器人

粗略统一归类后来看,页面的大体加载流程应该是这样:

基于上述一个加载思路,首先需要做的是页面加载路径收口,需要保证所有页面的加载入口是在一个统一的 Loader 下,然后才可以较为系统的处理所有页面的加载生命周期。

在收敛的同时,同样需要保持开放,对核心加载路径要保持插件化开放,随时支持不同的扩展能力,渲染技术栈接入。

所以,在主路径上,通过 Loader 加载配置进行处理,这份配置在主路径中提供上下文,然后交由插件进行消费,如图所示:

举个例子,拿一个独立的 JS Bundle 类型的子应用来说:

<div id="root"></div>
<script src="https://cdn.address/schema-resolver/index.js"></script>
<script src="https://cdn.address/schema-resolver/plugin/layout.js"></script>
<script src="https://cdn.address/schema-resolver/plugin/source-code.js"></script>
<script src="https://cdn.address/schema-resolver/plugin/micro-loader.js"></script>
<script src="https://cdn.address/schema-resolver/plugin/i18n.js"></script>

<script>
  SchemaResolver.render(
    &#123;
      micro: true,
      host: "dev.address",
      hfType: "layout1",
      externals: ["//&#123;HOST&#125;/theme1/index.css"],
      // host is cdn prefix, the resource maybe in different env & country
      resource: &#123;
        js: "/index.js",
        css: "/index.css",
      &#125;,
    &#125;,
    &#123; container: document.querySelector("#root") &#125;
  );
</script>

通过上述的 Plugin 引入,即可开启和消费不同的配置。

这里引入了 Layout Plugin,该插件会消费 hfType 字段然后去加载对于的 Layout 资源提供 Container 交给下一个环节。

按照配置,当前页面开启了微前端,那么 Micro Loader 将会消费提供下来的 Container,然后建立沙箱(这里基于 qiankun),再提供 Container 出来。

最后交由 SourceCode Plugin 进行 Bundle 加载运行和渲染。如果这里是另外一种渲染协议或者技术栈,则可以根据不同配置交由不同插件消费 Container。

这个过程中,每个环节的插件是不依赖的,可插拔的

比如:

如果不加载 Layout Plugin 将不会消费 hfType 字段,也就不会将Layout插件逻辑注入到getContainer方法中,那么将直接得到由最外层下穿的Container进行渲染,也就不会有菜单相关透出。

如果不加载 Micro Plugin 同样不会有微前端的逻辑注入,也就不会建立沙箱,那么页面渲染流程将会按照常规模式继续运行。

当前SchemaResolver已经支持的插件有以下几种,详情参考 SchemaResolver

  • MicroLoader – Base on qiankun using this plug-in can make your content loaded through a micro application so that your content can use all the features of the Micro-Front-End.
  • DadaLoader – Use this plugin can make your app render content by Dada.
  • SourceCodeLoader – Use this plugin can load your js\css bundle to render content, our bundle standard is same as qiankun. You can quick start developing your own page through our toolkit lzd-toolkit-asc.
  • LayoutLoader – Use this plugin can make your page load layout(menu), you can use different hfType configuration to switch different layouts.
  • i18n – Use this plugin can make your page have multi-lang. schema.locale will be the mapping of multilingual keys in MCMS. The plugin will inject and register the language automatically.
  • APlus – Use this plugin can make your page have the feature of APlus .Statistics page interaction events, such as pv\uv. With DadaLoader you can even see every module data(click pv, exposed pv) in pages.
  • WalkThrough – Use this plugin can make your page have the feature of Walk Through. One-stop page features guide.

SchemaResolver的插件能力采用plugin-decorator,如要了解更多插件设计思路可以参考:为你的JavaScript库提供插件能力

SchemaResolver plugin feature is base on plugin-decorator. It’s very easy to develop a new plugin.

More information about plugin can read this article Provide plugin capabilities for your JavaScript library

对于我所在团队负责的项目来说,万万做不得一刀切的方案,所以针对现有存量页面,需要完整分析当前存量技术栈:

针对上述存量页面来说,需要从左到右分批进行页面级别控制上线部署,对于左侧部分页面甚至需要做些项目改造后才可部署接入上线。

这类迁移测试需要处理出一套 自动化e2e测试 流程,通过分批迁移同时梳理出 微前端注册表

有了这两项流程保证及范围控制,当前方案所上线内容完全可控,剩下要处理的大部分就是较为重复的体力活了,覆盖率也可量化。

按照上述方案迁移,那么预期的微前端形态将会是:

  1. 每个开启微前端的页面都可成为主应用
  2. 微前端是插件可选项,如果因为微前端导致的业务异常可随时关闭
  3. 同为微前端的页面路由相互之间切换可实现局部刷新形态,而跳转至非微前端注册表中的页面则会直接页面跳转。随着微前端页面覆盖率提高,局部刷新的覆盖率也会逐渐提高
  4. 可通过不同扩展插件,加载不同技术栈类型的存量页面,转换为对应子应用

在SchemaResolver中的注册和调用路径如下:

透过技术看本质,微前端所代表的杰出思维,才是真正解决具体问题关键所在,只有解决了具体的业务问题,这项技术才有价值转换。

不要为了微前端做微前端,不要为了小程序做小程序。

当前,通过 SchemaResolver,可以针对不同角色提供不同的开放能力:

  • 针对平台管理员,提供插件能力开放全局扩展能力
  • 针对页面开发者,提供标准化接入方案路径,提供多种技术栈接入能力,并无感知提供微前端,多语言,埋点,菜单,主题加载等能力。解耦了不同系统公共能力,同时,这种方式可以让页面开发者快速将具体业务逻辑迁移到其他平台。
  • 针对第三方接入者,不需要关心了解系统菜单、主题接入方式,提供统一的接入口径,通过微前端隔离技术栈、隔离子应用样式。最后通过统一的页面系统管控,轻松入住对应平台,同时可以全局看到站点页面情况。
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×