Recordum
Recordum copied to clipboard
AMD + React 组件级别的前端开发上线方案
⼀个项⽬随着功能添加,开发时间的增⻓,通常会遇到如下问题
- 开发成本
我们⼀直说的组件开发,其实可以说成是在组件堆
⾥开发。开发接⼿⼀个项⽬,要理清组件/⻚⾯逻
辑,嵌套思路。甚⾄还有⼀些⿊魔法的结构。当项⽬很⼤的时候,理解是⼀个⽐较⼤的开发成本
-
发布成本 改动⼀个⼩功能,甚⾄是修改⼀段⼩⽂案,都需要整个应⽤打包构建发版本,项⽬越⼤时间越⻓,并 且修改带来的⻛险越⼤
-
优化/改造成本 通常在⼀个系统内,组件通信成本很低,所以开发过程⽐较少考虑组件的独⽴性。导致组件间关联⾮ 常多且复杂。这样也不⽅便测试及后续维护
-
组件共享成本 通常业务定制化组件在系统间复用是比较麻烦的,例如一个定制的富文本编辑器,如果想在多个系统间复用,通常方式是
- 发布 npm 包,但这样有一定的问题
- 违背 npm 包思想,npm 包应该是通用的,而不是业务定制化
- 编辑器更新,使用的系统也必须进行更新构建。并且整个流程繁琐
- 复制方式,将组件复制到各个系统,这样就更难以管理
目标
希望有⼀种⽅式,只关注单个组件逻辑,不需要去了解页面其他的嵌套结构逻辑,只需要关注⾃⼰的组件功能。具体来说可以细分
- 组件按需加载
- 组件独⽴开发构建部署,组件更新主应⽤不需要重新构建
- 安全稳定性(组件隔离,组件出错不会导致主应⽤出错)
- 组件间能通信
另外还有一些开发体验上的要求
- 开发配置,API 之类要简单⼀些
- 当组件独⽴了,需要有预览功能,可以测试真实情况,例如事件触发的测试
- 不需要每次都安装 webpack 之类的构建⼯具(当组件⾜够多时候,每个组件都安装⼀次 webpack, babel,这是⼀种折磨),同时统⼀的构建⼯具保证统⼀稳定性
方案
以下是一些方案对比
方案 | 按需加载 | 组件独⽴ | 通信 | 发布 | 备注 |
---|---|---|---|---|---|
路由跳转 | ⻚⾯级 | ⻚⾯独⽴ | Url 参数, 浏览器存储 | 独⽴ | 太普通,意义不⼤ |
iframe | ⻚⾯级 | ⻚⾯独⽴ | postMessage, Url 参数, 浏览器存储 | 独⽴ | 太普通, 意义不⼤ |
Webpack 代码分割 | ⻚⾯级 | 不独⽴ | 完整 | 全量发布 | 没有解决组件独⽴,发布⻛险问题 |
single-spa | ⼦应⽤ | 应⽤独⽴ | 应⽤间⽆通信 | ⼦应⽤单独发布,主容器需重新构建 | 改⽅案的出发点是:解决历史遗留系统,考虑怎么将这些系统合在一起 |
以上都不大符合要求,使⽤以下⽅案
- 按需加载:使⽤ RequireJS,业界成熟的 AMD 加载规范。同时也解决组件依赖问题
- 组件通信:使⽤ JS 发布-订阅 模式,Redux 底层也是基于这个模式
- 组件隔离:AMD 规范使 JS 隔离,使⽤ CSS Modules 进⾏ CSS 隔离
开发体验
- 提供开发者⼯具:⽅便测试,统⼀打包模式,不重复构建 Polyfill
- 开发构建⼯具安装⽅便,⼀次安装,多处使⽤,⽅便升级
设计实现
以下为⽅案的⼀些具体说明及具体 DEMO
方案:https://github.com/fratercula/humpback
基本流程:组件 AMD ⽅式加载 --> React HOC 处理 --> 组件拥有调度⽅法及全局数据
基本介绍
DEMO:https://loeify.github.io/humpback-demo/ 由 9
个组件及 1
个全局容器构成,每个都是独⽴构建发布
// 每个组件都可以独立发布,有自己独立的版本号
{
global: 'cdn/to/global/0.2.0/index.js', // 全局容器组件
button: 'cdn/to/button/0.0.0/index.js',
card: 'cdn/to/card/0.0.1/index.js',
description: 'cdn/to/description/0.1.0/index.js',
error: 'cdn/to/error/0.0.1/index.js',
photo: 'cdn/to/photo/0.0.3/index.js',
timeline: 'cdn/to/timeline/0.4.0/index.js',
user: 'cdn/to/user/0.0.2/index.js',
input: 'cdn/to/input/0.1.0/index.js',
posts: 'cdn/to/posts/0.2.0/index.js',
post: 'cdn/to/post/0.0.4/index.js',
}
可以自定义页面布局,例如 DEMO 为以下页面组件配置
[
{
path: '/',
name: 'index',
icon: 'dashboard',
components: {
type: 'index',
left: ['card', 'timeline'],
right: ['button', 'error'],
},
},
{
path: '/switch',
name: 'switch',
icon: 'customer-service',
components: {
type: 'user',
left: 'user',
right: ['description', 'photo'],
},
},
{
path: ['/posts', '/posts/:id'],
name: 'posts',
icon: 'book',
components: {
type: 'user',
left: 'posts',
right: ['post'],
},
},
]
参数说明
全局容器组件
interface GlobalProps {
dispatch: Function; // 调度函数,访问全局或者组件
Routes: ReactNode; // 组件容器
componentCreator: Function; // 组件包装⽅法
store: object; // 全局 store
mountedComponents: array; // 当前⻚⾯已经加载完成组件
config: object; // ⾃定义参数配置
}
组件
interface ComponentProps {
config: object; // ⾃定义参数配置
mountedComponents: array; // 当前⻚⾯已加载完成组件
store: object; // 全局 store
dispatch: Function; // 调度函数,访问全局或者组件
// react-router 参数
match
history
location
}
调度方法定义
type dispatch = (
type: string, // 类型,‘global’ 或者 ‘组件名’
name: string, // 调度⽅法名
value: any, // 参数
) => any;
组件开发并没有特殊 API 或者协议,跟普通开发使⽤⼀样。 组件提供静态⽅法给其他组件调⽤,这样其他组件不能直接访问到当前组件的上下⽂,保证安全
import React, { Component } from 'react'
import Nycticorax from 'nycticorax'
const {
createStore,
connect,
dispatch,
getStore,
} = new Nycticorax() // 可以使⽤ redux/mobx 之类
createStore({ name: '233' })
class X extends Component {
// 静态⽅法⽤于给其他组件调⽤,同时也保证组件数据安全
static getName = () => getStore().name
static updateName = async (name) => {
dispatch({ name })
}
render() {
return ...
}
}
export default connect('name')(X)
开发体验
本⽅案提供开发⼯具:https://github.com/fratercula/megaptera
以下为以下说明介绍
- 提供两种类型的开发,组件开发及全局容器开发

- 提供模拟环境,模拟事件处理,窗⼝模拟

-
不需要重复的安装依赖环境,同时也保证构建⼯具的统⼀
-
⾃动安装包依赖
工作流对比
普通模式下的开发上线⼯作流
- 每次更改必须整个项⽬构建⼀次
- 组件出错很可能会影响全局

基于本⽅案的开发上线⼯作流
- 每次更改只需要改对应功能的组件,全局不需要变更
- 组件出错也只影响组件本⾝,对全局没影响

性能说明
提高比普通方式更好的性能,开发者只需要关系自己组件的性能
-
加载性能(AMD ⾃带):
- 按需加载
- 缓存加载
-
组件(应⽤)性能:
- 最⼩化更新,只有绑定的数据更新才会导致组件更新
- 容器⽆法直接更新组件(应⽤),保证性能,同时也保证组件(应⽤)状态不会刷新

方案优点
- 定制化
可以根据需求进行组件颗粒度的一些处理。例如数据埋点,在普通方式下,需要对系统某部分功能进行一些统计情况,可以需要一定改造成本。但在本方案下,组件天然的独立性会带来方便
- 开放能力
系统可以有类似 小程序
的能力,可以方便的接入一些功能,但同时接入并不需要系统进行重新构建。
- 多人协作
大型项目通常需要多个开发共同合作,由于该方案的天然组件独立性,每个人可以单独负责几个组件,或者某一类能力的组件,这样更好关注组件开发
- 系统维护
由于开发往组件化思想及独立性。这样会更方便测试维护。同时也不用再担心系统随着功能添加变得越来越大的问题,因为一切都是按需加载。当⼀个⻚⾯使⽤⼀个组件时候就类似 webpack 代码分割的效果
- 稳定性
同意由于组件独立,隔离能力,组件的错误并不会影响全局,这样就减少发布带来的风险及故障
- 提高效率
组件往功能化思想⽅向开发,会促进系统间的组件复用。这样就能提高开发效率。项⽬开发构建发布可以变成

方案缺点
- 组件复用可能带来兼容性问题,这点类似 npm 包的问题,现在任何方式都不能解决
- 因为组件间完全独立,所以通信引用不能像以前那样方便。但这样子更能提高组件的稳定性
FAQ
-
如何解决依赖问题? RequireJS (AMD)已经解决
-
组件重名了怎么办? 这个问题跟 npm 包重名⼀个问题,最终就是要约定,⽐较好办法就是把所有组件项⽬放在 gitlab的 ⼀个组内,项⽬创建就避免重复问题
-
依赖组件重复打包? ⼀些⼤型基础库,例如 antd,可以在项⽬设定好依赖,这样所有组件都可以使⽤了(组件打包时 候注意 externals 基础库) 但组件仍然可以⾃⼰去依赖⼀些组件,最终打包就是该组件会⽐较⼤,但不影响全局
-
如何知道其他组件有没有提供调⽤的⽅法? 组件是否加载完成参数 调⽤错误提⽰ 通常⽐较少互相调⽤的场景,更多的是组件调⽤全局的场景