查看原文
其他

【第2636期】从"微前端"到“微模块”

掌柜 前端早读课 2022-06-10

前言

咋一看,微模块。今日前端早读课文章由@掌柜投稿分享。

正文从这开始~~

开源了一个前端框架Elux,今天来和大家一起聊聊这个框架的一些基本的设计思想。

Elux介绍

不只是一个JS框架,更是一种基于“微模块”和“模型驱动”的跨平台、跨框架同构方案。它将稳定的业务逻辑与多样化的运行平台、UI框架进行剥离,让核心逻辑得到充分的简化、隔离和复用。

Elux-基于“微模块”和“模型驱动”的跨平台、跨框架同构方案。https://eluxjs.com/

只需一行命令,立即试试吧!

npx @elux/cli-init elux-init

微模块

微前端够用吗?
从产品的角度

某个大型应用包含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,而且同一个功能可能还存在需求版本的不同,这让“小李”无可适从。

“微前端”还不足够灵活、粒度不足够细。

从开发的角度

对于“发送短信验证码”、“忘记密码”等某些通用的业务流程和功能,如果多个工程都需要,你是如何跨工程共享和维护的呢?是简单的复制粘贴?还是一股脑全放基座里面?

“微前端”并没有解决工程之间代码的复用和维护的问题。

将业务功能放进模块

对于后端开发来说,按业务功能来划分模块几乎是业界共识,而在前端开发中往往是按UI界面来切割模块,这样的前端模块实际上只是Component组件,不具备独立性与完整性。

如果我们将完整的业务功能(包括UI组件、样式、图片、交互流程、业务逻辑、API请求、数据管理等)都打包到一个NPM包中,并利用NPM的版本和依赖管理机制来维护客户需求,岂不美哉?

试想一下,某客户需要 A,C1(C功能的某个版本),E2(E功能的某个版本),G 功能,我们只需要安装相应版本的NPM包:

npm install A C@1 E@2 G
业务模块变成了NPM包,版本号被关联至需求

Great!我们称这些包含完整业务功能的模块为前端微模块,可见所谓的微模块其实就是包含业务功能的NPM模块。

微模块的划分

微模块是实现特定业务功能所需资源的集合

  • 划分视角: 业务功能(非UI界面)

  • 划分原则: 高内聚、低耦合(有清晰的边界)

请注意"高内聚、低耦合"是唯一的划分标准,并不要求单一职责,所以一个微模块中可能包含多个功能点、多个UI组件,一组相关视图。

如果二个微模块之间紧密依赖,交互密切,请不要分割它们,这样会使问题复杂化。一种常用思路是借助于后端Restful理念,将每种资源的维护(增删改查封)装成一个独立的微模块。

微模块与UI组件的区别

微模块和UI组件都是一个NPM包,似乎有点相似,但其实它们有本质的区别:

  • UI组件是一种单体的封装;而微模块是一种资源的集合。

  • UI组件为复用而生,可能在多处被实例化,通常不包含具体的业务逻辑;而微模块并不追求通用性,它包含具体的业务逻辑,通常只需要初始化一次。

  • 微模块可能包含多个UI组件、视图、Model等,也可能只是逻辑,不包含任何UI。

微模块的开发和维护

微模块的开发和维护就是对NPM包的开发和维护,并不附加任何新的学习成本,你需要做的只是维护它的依赖关系,并对外封装API,以保证独立性与易用性。

微模块的部署

通常有2种方式使用和部署微模块:

静态编译:微模块作为一个NPM包被安装到工程中,通过打包工具(如webpack)正常编译打包即可。这种方式的优点是代码产物得到打包工具的各种去重和优化;缺点是当某个模块更新时,需要整体重新打包。

动态注入:利用打包工具的动态加载功能(如webpack5 的 Module (模块) Federation (联合会) )将微模块作为子应用部署(与时下流行的微前端类似)。这种方式的优点是各子应用独立部署运行,当某子应用中的微模块更新时,其它应用无需重新编译,刷新浏览器即可动态获取最新模块;缺点是没有打包工具的整体编译与优化,代码和资源容易重复加载或冲突。

Elux对以上二种部署方式都有支持和示例。

Elux中的微模块

我们先看一下时下流行的前端工程目录,假设有独立的功能ModuleA和ModuleB:

src
├── assets
├── consts
│ ├── ModuleA

│ │ ├── Const1.ts //A中使用的一些常量

│ ├── ModuleB

│ ├── Const2.ts //B中使用的一些常量

├── utils

├── components
│ ├── ModuleA

│ │ ├── Component1.ts //A中使用的一些UI组件

│ ├── ModuleB

│ ├── Component2.ts //B中使用的一些UI组件

├── containers

├── pages
│ ├── ModuleA

│ │ ├── Page1.ts //A中使用的一些页面

│ ├── ModuleB

│ ├── Page2.ts //B中使用的一些页面

├── models

│ ├── ModuleA

│ │ ├── Store1.ts //A中使用一些状态定义

│ ├── ModuleB

│ ├── Store2.ts //B中使用一些状态定义

其特点是以“文件职能”作为一级分类、“功能模块”作为次级分类。

现在如果我需要拿掉ModuleB,或者新增ModuleC,你将不得不进行多个目录的操作。随着文件越来越多,相互引用越来越复杂,ModuleB的相关资源和依赖像一堆乱麻散落在各个不同文件和文件夹中,你会发现要干净的剥离ModuleB是一个巨大的任务...

那应当如何改进呢?

  • 将“功能模块”作为一级分类,“文件职能”作为次级分类

  • 注意模块的对外封装,不要随意绕过封装来引用模块内部资源

以下是Elux工程的常用结构:

src
├── modules
│ ├── ModuleA
│ │ ├── assets

│ │ │ ├── imgs //A中使用的一些图片等

│ │ ├── consts

│ │ │ ├── Const1.ts //A中使用的一些常量

│ │ ├── utils

│ │ ├── components

│ │ │ ├── Component1.ts //A中使用的一些UI组件

│ │ ├── views

│ │ │ ├── View1.ts //A中使用的一些业务视图

│ │ ├── model.ts //A的数据模型

│ │ └── index.ts //A的对外封装与导出

│ │

│ ├── ModuleB
│ ├── ModuleC

可以看到在Elux工程中,所有与功能模块相关的文件都被放到了一个独立的文件夹中,并通过index文件统一对外导出,这便是Elux中微模块得以独立开发、安装和运行的基础。

微模块vs微前端

微前端是一个广义上的概念,微模块也是实现微前端的一种解决方案。与之前狭义上的微前端相比,微模块灵活性更高,但隔离性更差(只能依靠module和约定的namespace)。如果你没有统筹整个项目的权利,或者项目本身就是异构系统(各子应用采用不同技术栈),那微前端还是首选方案。

模型驱动

模型是什么?

这里的"模型 Model"是指对业务逻辑的抽象表达和数据建模。

模型与运行平台无关、与UI框架无关,它是一种逻辑、一种抽象、一种提炼。它是应用的骨骼,UI则是皮肉,所谓的模型驱动是一种设计理念:

用骨骼来驱动皮肉,而不是用皮肉来驱动骨骼。

模型就是状态吗?

模型是一个可以运转的有机体,而状态只是它的一个切片,模型包括状态、以及驱动状态变化的各种方法和事件。

轻UI,重Model

你是否仍在纠结采用React还是Vue?是否仍被困于各种生命周期之中?是否迷惑于把获取数据写在onMounted还是onCreated中?那么你应当尝试着:

将更多逻辑从 UI 转移到 Model 中。

为什么需要模型驱动?
  • 剥离了业务逻辑,UI层变得更纯粹,它只是负责展示、交互和传递用户事件。

  • 剥离了UI逻辑,业务层不再受到各种生命周期和糖衣语法的干挠,更纯净透明。

  • 分层而治,将稳定的业务层和灵活的UI层分离,增加了代码的可复用性和可移植性。

借助于模型驱动实现跨端开发

用一套代码搞定不同的运行平台,Write once run anywhere...这曾经是我们美好的梦想,然而实践却告诉我们那样往往啥都干不好。因为不同平台的渲染原理、用户习惯、尺寸大小,表现方式本质上就不一样,非得强行将它们按平,那么只能是成就某些而放弃某些。

我们思考,运行在不同终端不同运行平台中唯一不变的是什么?是游戏规则,是业务逻辑,而数据模型就是业务逻辑的抽象载体。

所以,Elux只追求核心业务逻辑的复用,并不追求Write once run anywhere...,它属于现实主义者,而非完美主义者!

Elux中的模型驱动

Elux原意是 Flux Enhancer,是一种加强版的的Flux状态管理器(Redux/Vuex都是Flux的变种),它主要加强了以下方面:

将Action作为Model中的事件

驱动应用运行需要事件,协同模块之间的工作也需要事件,事件是一种发布订阅的设计模式,在Model中我们称它为Action:

当然它与UI中的Event机制完全不一样,比如UI中的Event有冒泡机制,而Model中的Action有线程机制(在ActionHandler的执行过程中可以开启一条新的Action线程)。

#职能化和模块化
  • 将 ActionHandler 分为纯函数reducer和副作用effect。其中 reducer 是唯一可以改变 State 的方法,其概念类似于Redux 的 reducer和redux-saga 的 effect,也类似于 Vuex 的mutation和action。

  • 将 State 和 ActionHandler 都进行模块化,并添加约束:本模块的State只能由本模块的reducer修改

将其它副作用挡在外围

Model 需保持足够抽象和纯粹,我们不希望在它里面引入更多副作用和不稳定因子,所以需要将这些噪音挡在外围,第一时间将它们转化为 Model 的内部语素。

我们知道MVVM理论有个深入人心的公式:

UI = render(State)

然而在众多前端工程中,这个公式变得不那么纯粹。比如将路由直接与UI绑定起来,公式变成了:

UI = route()+render(State)

要知道不同的UI框架、不同的运行平台,路由实现方案多种多样,这将让应用失去通用性。Elux打破了常规的做法,将路由在外围与State绑定起来,不直接参与UI的运算,让渲染公式回归纯粹。

所以在Elux中不再需要任何路由框架(如react-router、vue-router、taro-router等),也没有定死路由规则。路由只存在于 Model 外围,不参与核心逻辑,带来灵活性的同时也降低了心智负担。

关于本文
作者:@掌柜
原文:https://juejin.cn/post/7106791733509226533

微前端相关推荐阅读。欢迎自荐投稿,前端早读课等你。

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存