微模块-前端业务模块化探索,拆解巨石应用的又一利器


大家好,我是Eluxjs的作者,Eluxjs是一套基于“微模块”和“模型驱动”的跨平台、跨框架『同构方案』,欢迎了解...

文前声明,以下推断和结论纯属个人探索,鉴于本人知识水平所限,谬误在所难免,恳请各位大佬不吝赐教...

什么是前端“微模块”?

Elux中的『微模块』是指在Web前端工程中,将代码和相关资源按照不同的业务功能进行归类和模块化。

根据业务功能进行模块化一直以来都是后端的普遍做法,而Web前端则通常都是按照UI界面的视图区块View来进行模块化,这样的模块实际上只是Component组件,不具备独立自治的能力。究其原因我想是因为在早期Web1.0的时代,前端的职能就是仅仅作为后端API数据的一个Render渲染器,所以前后端的视野和格局出现了分化,也导致很多人说前端根本无架构之说。

然而web生态发展到今天,浏览器越来越强大,赋能越来越多,甚至不亚于一个小型操作系统,这时候的Web前端早已不是当初简单的数据渲染器,状态管理、会话维持、数据持久化、文件缓存、通信协议...随着PWA、小程序、快应用的推广,WebAPP不再是瘦客户端,渐渐成长为大胖小子。

此时的我们应当跳出“渲染器”的井口,而从一个完整的软件工程来思考我们的前端架构,Web前端不只是一层View、一个GUI,我们需要回归到与后端一致的以业务领域为驱动的模块化视角。

micro-module.png

为什么前端需要“微模块”?

  • 从开发角度来说:我们需要高内聚、低耦合的松散结构体,而不是牵一发而动全身的巨石应用,这不管是对于开发、维护、还是后期渐进式重构,都至关重要。

    前端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

building-blocks.jpeg

我们知道世界上有一款建站神器wordpress,曾经号称世界上50%的网站都是由它创建的,我认为它的成功秘诀就是社区模版机制和功能插件化,你要什么功能都总能找到“前端+后端”一起打包安装的插件,这也类似于“微模块”的概念。

  • 从工程的角度来说:“微模块”是跨工程、跨项目共享通用业务代码的理想决方案,对于跨端、跨平台复用业务逻辑尤其有用。

micro-share.png

前端“微模块”的划分原则与边界

  • 拥有高内聚、低耦合的工程结构。
  • 拥有独立自治的子域逻辑。

micro-domain.png

从图中可以看到,每个微模块负责定义和维护自己领域内的事务,并且麻雀虽小,五脏俱全,拥有独立的路由解析、状态管理、数据模型、控制器、视图、组件、资源、业务实体、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之间联系紧密,那就不应当拆分它们。

micro-view.png

不以视觉延伸和几何空间作为View的微模块归属原则:如下图所示,假设有一个View用来展示用户资料,我们将其放在UserModule这个微模块中,称其为UserModule.DetailView,但你发现其中又包含一个该用户发表文章的列表,你当然可以把这个列表单独提取出来作为一个新的View。从视觉上来看,它似乎和用户资料是连在一起的,似乎可以和UserModule.DetailView放在同一个微模块中;但我们从它解决的问题来看,它属于文章领域,而与用户领域关系并不大,所以我们最好将其放在ArticleModule中,称其为ArticleModule.ListView

micro-view2.png

前端“微模块”的实现方案

  1. 定义和创建微模块,可借助于Eluxjs框架,当然你发现了其它框架也可以。
  2. 管理微模块,可借助于NPM仓库。
  3. 使用微模块,可借助于打包工具:
    • 静态编译:微模块作为一个NPM包被安装到工程中,通过打包工具(如webpack)正常编译打包即可。这种方式的优点是代码产物得到打包工具的各种去重和优化;缺点是当某个模块更新时,需要整体重新打包。
    • 动态注入:利用Module Federation,将微模块作为子应用独立部署,与时下流行的微前端类似。这种方式的优点是某子应用中的微模块更新时,依赖该微模块的其它应用无需重新编译,刷新浏览器即可动态获取最新模块;缺点是没有打包工具的整体编译与优化,代码和资源容易重复加载或冲突。

micro-install.png

微模块 vs 微前端

从本意上来说,微模块只是一种工程结构和模块化方案,而微前端只是它的一种应用场景之一。微模块架构不仅可以用来构建复杂的单体应用,也可以结合Module Federation实现多子应用独立部署的“微前端”。

如果单独就微模块 + ModuleFederation方式实现的微前端,与传统意义上的qiankun、icestark等微前端方案相比,微模块方式胜在粒度更细、更灵活、更轻巧,而传统方式则胜在隔离性更好。

想到一个非常形象的比喻:

IFrame vs 微前端 vs 微模块 可类比于 进程 vs 线程 vs 协程

从左至右:越来越轻量化,隔离性逐渐变弱,灵活性逐渐增加。所以鱼与熊掌不可兼得,具体哪种方案最适合还得看不同的产品需求。

微模块之间的通信

  • 微模块之间按照某些规则和约定共享同一个Runtime,强制隔离性较弱,所以它们之间的通信是轻量级的,可以相互引用与调用。
  • 建议观察者模式,或者使用事件总线模式来保持微模块之间的松散关系,这是另一个故事,可参考Eluxjs中的ActionBus
  • 微模块高内聚、低耦合的划分原则,也意味着微模块之间不会出现特别复杂的互动与交流(互动密切的微模块应当合并)。

落地与实战

光练不说傻把式,光说不练假把式,这里先把思路概念要说的说完,下面就要开始出实例了。先喝口水,请听下回分解...????急性子也可以直接去Eluxjs官网,看看Demo,不吝赐教...