大家好,我是Eluxjs的作者,Eluxjs是一套基于“微模块”和“模型驱动”的跨平台、跨框架『同构方案』,欢迎了解...
文前声明,以下推断和结论纯属个人探索,鉴于本人知识水平所限,谬误在所难免,恳请各位大佬不吝赐教...
什么是前端“微模块”?
Elux中的『微模块』是指在Web前端工程中,将代码和相关资源按照不同的业务功能
进行归类和模块化。
根据业务功能
进行模块化一直以来都是后端的普遍做法,而Web前端则通常都是按照UI界面的视图区块View
来进行模块化,这样的模块实际上只是Component组件
,不具备独立自治的能力。究其原因我想是因为在早期Web1.0的时代,前端的职能就是仅仅作为后端API数据的一个Render渲染器,所以前后端的视野和格局出现了分化,也导致很多人说前端根本无架构之说。
然而web生态发展到今天,浏览器越来越强大,赋能越来越多,甚至不亚于一个小型操作系统,这时候的Web前端早已不是当初简单的数据渲染器,状态管理、会话维持、数据持久化、文件缓存、通信协议...随着PWA、小程序、快应用的推广,WebAPP不再是瘦客户端,渐渐成长为大胖小子。
此时的我们应当跳出“渲染器”的井口,而从一个完整的软件工程来思考我们的前端架构,Web前端不只是一层View、一个GUI,我们需要回归到与后端一致的以业务领域
为驱动的模块化视角。
为什么前端需要“微模块”?
-
从开发角度来说:我们需要
高内聚、低耦合
的松散结构体,而不是牵一发而动全身的巨石应用,这不管是对于开发、维护、还是后期渐进式重构,都至关重要。前端Leader:经过一年多的迭代和人员变动,我们代码已经混乱不堪了,开发越来越吃力,必须要重构,否则玩不下去了! 产品经理:嗯,我理解,这里面也有很多是我们需求变更频繁引起的,我支持你们重构! 前端Leader:感谢大佬理解,那新需求先停下来,等我们重构好了再迭代吧? 产品经理:你们重构要多久? 前端Leader:产品这么复杂了,估计至少要3个月左右吧。 产品经理被吓出一身冷汗:大佬,你要3天还可以考虑,停下来3个月估计公司都要关门了... 前端Leader:可是产品这么复杂,几天时间完成重构是天方夜谭。 产品经理想了想:这样把,我每个迭代少安排几个需求,这样你们每个月就可以留几天时间重构了。 前端Leader:这可不是1+1=2的问题,而是0与1的问题,大佬你不了解! 产品经理:谁说我不了解,你们就不能渐进式重构吗? 前端Leader:...
此时如果我们的前端工程是基于“微模块”,一来可以轻松的找到“局部重构”的
边界
,二来也可以通过维持“微模块”的对外接口来无极替换。 -
从产品角度来说:软件架构永远是服务于业务需求的。我们希望我们的产品能像搭积木一样
按需组合
,可以快速包装出各种灵活多样的套餐,以满足客户越来越精细化的定制需求。某个大型应用包含A,B,C,D,E,F,G等若干功能,原来一直是整体打包出售... 随着用户需求的多样化,有的用户仅需要部分功能,于是聪明的前端架构师“小李”利用时下流行的微前端技术, 将应用拆分成了的 3 个子应用: - 【基础应用】包含功能:A - 【子应用A】包含功能:B,C,D - 【子应用B】包含功能:E,F,G 这样等于有 3 个套餐可以供客户选择: - 套餐A:基础应用 + 子应用A - 套餐B:基础应用 + 子应用B - 套餐C:基础应用 + 子应用A + 子应用B 然而用户的需求越来越精细化,有的需要ABCD,有的需要ACEG,有的需要ABDF... 而且同一个功能可能还存在需求版本的不同,这让“小李”无可适从。
现在我们利用“微模块”来帮助小李解决问题:
- 将各种独立的业务功能封装成不同的微模块:A,B,C,D,E,F,G
- 将各种微模块按需求迭代版本,发布成NPM包
- 某客户需要 A,C1(
C功能的某个版本
),E2(E功能的某个版本
),G 功能,我们单独为该客户创建一个聚合工程分支,安装相应版本的微模块:npm install A C@1 E@2 G
我们知道世界上有一款建站神器
wordpress
,曾经号称世界上50%的网站都是由它创建的,我认为它的成功秘诀就是社区模版机制和功能插件化,你要什么功能都总能找到“前端+后端”一起打包安装的插件,这也类似于“微模块”的概念。
- 从工程的角度来说:“微模块”是跨工程、跨项目共享
通用业务代码
的理想决方案,对于跨端、跨平台复用业务逻辑
尤其有用。
前端“微模块”的划分原则与边界
- 拥有
高内聚、低耦合
的工程结构。 - 拥有
独立自治
的子域逻辑。
从图中可以看到,每个微模块负责定义和维护自己领域内的事务,并且麻雀虽小,五脏俱全
,拥有独立的路由解析、状态管理、数据模型、控制器、视图、组件、资源、业务实体、API管理等等...总之,所有与自己领域相关的资源都被内聚到了一起。
以下是某巨石应用的SRC目录,其特点是以“文件职能”作为一级分类、“功能模块”作为次级分类:
├─ src
│ ├─ api # API接口管理
│ ├─ assets # 静态资源文件
│ ├─ components # 全局组件
│ ├─ config # 全局配置项
│ ├─ enums # 项目枚举
│ ├─ hooks # 常用 Hooks
│ ├─ language # 语言国际化
│ ├─ layout # 框架布局
│ ├─ routers # 路由管理
│ ├─ store # store
│ ├─ styles # 全局样式
│ ├─ typings # 全局 ts 声明
│ ├─ utils # 工具库
│ ├─ views # 项目所有页面
│ ├─ App.vue # 入口页面
│ └─ main.ts # 入口文件
以下是Elux中基于微模块的SRC目录,其改进是将“功能模块”作为一级分类,“文件职能”作为次级分类:
src
├── modules
│ ├── ModuleA
│ │ ├── entities
│ │ ├── assets
│ │ ├── api
│ │ ├── utils
│ │ ├── language
│ │ ├── components
│ │ ├── views
│ │ ├── model.ts
│ │ └── index.ts
│ │
│ ├── ModuleB
│ ├── ModuleC
微模块的台前与幕后
前端开发最终呈现的是UI界面,但这只是表象,支撑UI界面渲染和交互的是背后一系列state、model、controller等幕后英雄,它们根据自己所属不同领域被封装在各个微模块
中,UI既然与它们唇齿相依,必然也将跟随它们内聚在一起。
View和Component
本质上说View就是一个Component,但我们从架构的思维来区分它们:
- View:业务视图,它用来表现业务规则与逻辑,通常能够较为独立和完整的解决某一领域问题。
- Component:UI组件,它用来表现渲染规则与交互逻辑,通常不与具体业务直接相关,可复用在各种不同业务场景中。
所以在“微模块”的架构中,丰富多彩的UI界面由一个个单一职责的View聚合
而成,每个View同样依据自身所解决的领域问题而被分散
在各个微模块中,这里面有几个注意点:
- 领域性:View被归属到不同
微模块
的原则是其解决的问题领域,而不是视觉上的几何空间。View可以在视觉上被拆装、聚合、嵌套,这并不影响它们所属微模块。 - 完整性:一个View通常能解决一个较为独立和完整的问题,View与View之间是较为松散的关系,如果2个View之间联系紧密,那就不应当拆分它们。
不以视觉延伸和几何空间作为View的微模块归属原则:如下图所示,假设有一个View用来展示用户资料
,我们将其放在UserModule这个微模块中,称其为UserModule.DetailView
,但你发现其中又包含一个该用户发表文章的列表
,你当然可以把这个列表单独提取出来作为一个新的View。从视觉上来看,它似乎和用户资料是连在一起的,似乎可以和UserModule.DetailView
放在同一个微模块中;但我们从它解决的问题来看,它属于文章领域,而与用户领域关系并不大,所以我们最好将其放在ArticleModule中,称其为ArticleModule.ListView
前端“微模块”的实现方案
- 定义和创建微模块,可借助于Eluxjs框架,当然你发现了其它框架也可以。
- 管理微模块,可借助于NPM仓库。
- 使用微模块,可借助于打包工具:
- 静态编译:微模块作为一个NPM包被安装到工程中,通过打包工具(如webpack)正常编译打包即可。这种方式的优点是代码产物得到打包工具的各种去重和优化;缺点是当某个模块更新时,需要整体重新打包。
- 动态注入:利用
Module Federation
,将微模块作为子应用独立部署,与时下流行的微前端类似。这种方式的优点是某子应用中的微模块更新时,依赖该微模块的其它应用无需重新编译,刷新浏览器即可动态获取最新模块;缺点是没有打包工具的整体编译与优化,代码和资源容易重复加载或冲突。
微模块 vs 微前端
从本意上来说,微模块只是一种工程结构和模块化方案,而微前端
只是它的一种应用场景之一。微模块架构不仅可以用来构建复杂的单体应用,也可以结合Module Federation
实现多子应用独立部署的“微前端”。
如果单独就微模块 + ModuleFederation
方式实现的微前端,与传统意义上的qiankun、icestark
等微前端方案相比,微模块方式胜在粒度更细、更灵活、更轻巧,而传统方式则胜在隔离性更好。
想到一个非常形象的比喻:
IFrame vs 微前端 vs 微模块
可类比于 进程 vs 线程 vs 协程
从左至右:越来越轻量化,隔离性逐渐变弱,灵活性逐渐增加。所以鱼与熊掌不可兼得,具体哪种方案最适合还得看不同的产品需求。
微模块之间的通信
- 微模块之间按照某些规则和约定共享同一个Runtime,强制隔离性较弱,所以它们之间的通信是轻量级的,可以相互引用与调用。
- 建议观察者模式,或者使用事件总线模式来保持微模块之间的松散关系,这是另一个故事,可参考Eluxjs中的
ActionBus
。 - 微模块
高内聚、低耦合
的划分原则,也意味着微模块之间不会出现特别复杂的互动与交流(互动密切的微模块应当合并)。
落地与实战
光练不说傻把式,光说不练假把式
,这里先把思路概念要说的说完,下面就要开始出实例了。先喝口水,请听下回分解...