Recordum icon indicating copy to clipboard operation
Recordum copied to clipboard

AMD + React 组件级别的前端开发上线方案

Open LoeiFy opened this issue 4 years ago • 0 comments

⼀个项⽬随着功能添加,开发时间的增⻓,通常会遇到如下问题

  1. 开发成本

我们⼀直说的组件开发,其实可以说成是在组件堆⾥开发。开发接⼿⼀个项⽬,要理清组件/⻚⾯逻 辑,嵌套思路。甚⾄还有⼀些⿊魔法的结构。当项⽬很⼤的时候,理解是⼀个⽐较⼤的开发成本

  1. 发布成本 改动⼀个⼩功能,甚⾄是修改⼀段⼩⽂案,都需要整个应⽤打包构建发版本,项⽬越⼤时间越⻓,并 且修改带来的⻛险越⼤

  2. 优化/改造成本 通常在⼀个系统内,组件通信成本很低,所以开发过程⽐较少考虑组件的独⽴性。导致组件间关联⾮ 常多且复杂。这样也不⽅便测试及后续维护

  3. 组件共享成本 通常业务定制化组件在系统间复用是比较麻烦的,例如一个定制的富文本编辑器,如果想在多个系统间复用,通常方式是

  • 发布 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

以下为以下说明介绍

  1. 提供两种类型的开发,组件开发及全局容器开发
Screen Shot 2020-04-30 at 10 25 46 PM
  1. 提供模拟环境,模拟事件处理,窗⼝模拟
Screen Shot 2020-04-30 at 10 26 23 PM
  1. 不需要重复的安装依赖环境,同时也保证构建⼯具的统⼀

  2. ⾃动安装包依赖

工作流对比

普通模式下的开发上线⼯作流

  • 每次更改必须整个项⽬构建⼀次
  • 组件出错很可能会影响全局
Screen Shot 2020-04-30 at 10 28 09 PM

基于本⽅案的开发上线⼯作流

  • 每次更改只需要改对应功能的组件,全局不需要变更
  • 组件出错也只影响组件本⾝,对全局没影响
Screen Shot 2020-04-30 at 10 28 58 PM

性能说明

提高比普通方式更好的性能,开发者只需要关系自己组件的性能

  • 加载性能(AMD ⾃带):

    • 按需加载
    • 缓存加载
  • 组件(应⽤)性能:

    • 最⼩化更新,只有绑定的数据更新才会导致组件更新
    • 容器⽆法直接更新组件(应⽤),保证性能,同时也保证组件(应⽤)状态不会刷新
Screen Shot 2020-04-30 at 10 31 18 PM

方案优点

  1. 定制化

可以根据需求进行组件颗粒度的一些处理。例如数据埋点,在普通方式下,需要对系统某部分功能进行一些统计情况,可以需要一定改造成本。但在本方案下,组件天然的独立性会带来方便

  1. 开放能力

系统可以有类似 小程序 的能力,可以方便的接入一些功能,但同时接入并不需要系统进行重新构建。

  1. 多人协作

大型项目通常需要多个开发共同合作,由于该方案的天然组件独立性,每个人可以单独负责几个组件,或者某一类能力的组件,这样更好关注组件开发

  1. 系统维护

由于开发往组件化思想及独立性。这样会更方便测试维护。同时也不用再担心系统随着功能添加变得越来越大的问题,因为一切都是按需加载。当⼀个⻚⾯使⽤⼀个组件时候就类似 webpack 代码分割的效果

  1. 稳定性

同意由于组件独立,隔离能力,组件的错误并不会影响全局,这样就减少发布带来的风险及故障

  1. 提高效率

组件往功能化思想⽅向开发,会促进系统间的组件复用。这样就能提高开发效率。项⽬开发构建发布可以变成

Screen Shot 2020-04-30 at 10 32 15 PM

方案缺点

  • 组件复用可能带来兼容性问题,这点类似 npm 包的问题,现在任何方式都不能解决
  • 因为组件间完全独立,所以通信引用不能像以前那样方便。但这样子更能提高组件的稳定性

FAQ

  1. 如何解决依赖问题? RequireJS (AMD)已经解决

  2. 组件重名了怎么办? 这个问题跟 npm 包重名⼀个问题,最终就是要约定,⽐较好办法就是把所有组件项⽬放在 gitlab的 ⼀个组内,项⽬创建就避免重复问题

  3. 依赖组件重复打包? ⼀些⼤型基础库,例如 antd,可以在项⽬设定好依赖,这样所有组件都可以使⽤了(组件打包时 候注意 externals 基础库) 但组件仍然可以⾃⼰去依赖⼀些组件,最终打包就是该组件会⽐较⼤,但不影响全局

  4. 如何知道其他组件有没有提供调⽤的⽅法? 组件是否加载完成参数 调⽤错误提⽰ 通常⽐较少互相调⽤的场景,更多的是组件调⽤全局的场景

参考

Thinking in Microfrontend

LoeiFy avatar Apr 30 '20 14:04 LoeiFy