icejs:企业级前端研发框架的演进与实践

icejs:企业级前端研发框架的演进与实践

前言

icejs 是淘系中后台项目组经过近三年的演进沉淀出的一个基于 React 的研发框架,目前已在淘系、飞猪等众多阿里内部 中后台业务项目里被广泛使用。从最初的工程构建工具演进成研发框架,这背后有着怎样的历程,以及新的框架又给我们提供了怎样的能力,这篇文章将会给大家进行分享。

同时 alibaba/ice 项目自 2018 年开始已经在 GitHub 上进行开源,涵盖了前端工程、数据流方案、微前端、研发框架等开源项目,持续得到了社区广大的关注,并被国内众多个人与公司在其中后台项目中采用, GitHub 的 star 数不到 2 年的时间里达到了 15000 多,成为这个在领域增长最快的阿里开源项目,感谢所有在项目中使用 ICE 技术体系,以及参与贡献和反馈的同学。也欢迎更多的同学关注和使用 ICE。

开发痛点

在最近的几年时间里前端技术发展迭代速度非常之快,在语言上从 ECMAScript 到 TypeScript;在工程上有 Webpack、Rollup、Babel 等技术;在前端框架有 React、 Vue、 Angular 三驾马车;以及围绕这些框架的周边生态的技术方案更是数不胜数,即使是专业前端也需要通过不断的学习去建立自己的技术体系,对于非专业的前端开发来说,需要做前端开发门槛可想而知。如果每次开发项目都要去考虑这些问题,那么这无疑是非常痛苦的。

开发群体

在淘系业务中,除了专业的前端开发者外,还存在很多后端,测试,外包等非专业的前端开发者,有着大量的中后台业务诉求,面向这些开发者如何降低研发门槛,解决前端环境配置复杂,开发门槛高等问题,让开发者不仅能够快速上手业务开发,还能更简单的写出更好更易维护的代码是我们致力追求的目标。

演进之路

基于上述的问题,这里我们先来回顾下为什么会有 icejs 研发框架,下面是一张演进的大图:

刀耕火种

- 核心:工程 ice-scripts 1.0
- 定位:webpack 通用能力封装,支持部分配置、黑盒逻辑

时间回到 2018, 甚至更早期的时候,经常干的一件事情就是将 webpack 的配置直接全撸在项目里,如果需要开发另外一个项目时,基于原有项目一把梭的删掉业务代码然后开始撸新的项目。对与个人项目这样做可能没问题,但是到了大型团队多项目协作时这显然不是一个好的办法,工程配置无法复用,重复的配置导致维护成本极大,因此通常的做法就是将常用的配置进行封装,然后提供一些可选的配置支持不同的项目配置,这便是 ice-scripts@1.x 的工程能力。

随着时间和业务诉求的变化,发现每个业务对定制的诉求不一,ice-scripts 只能不断的去支持各种能力提供配置项,虽然满足了业务的诉求,但导致其核心的实现代码越来越臃肿,配置项也越来越多,使用成本开始上升以及整体的稳定性和可维护性下降。

工业化

- 核心:工程工具 ice-scripts 2.0
- 定位:强大的插件内核、丰富的插件生态、支持自定义任何配置



时间来到 2019,基于 ice-scripts@1.x 的背景原因,以及在阿里内部很多团队之间也遇到了同样的问题,每个团队都抽象沉淀了一套团队基础的工程工具,然而确都很难共享到其他团队。因此在某种背景的推动下,开始对 ice-scripts@1.x 进行升级以满足更多的团队和业务场景的诉求,降低重复的能力建设和人力投入,提供一套统一的工程方案。因此在阿里内部这套方案被重新命名为 build-scripts 、而 ICE 对外开源的为 ice-scripts@2.x 版本,本质上是同一套方案,只是命名的差异。其核心的能力包括:

  • 支持多构建任务机制
  • 完善灵活的插件能力
  • 丰富的插件生态
  • 灵活的更改 webpack 配置

因此在这一阶段,ice-scripts@2.x 在工程层已经做得非常好了,支持了淘系以及集团其他众多 BU 的业务开发。

现代化

- 核心:研发框架 1.0
- 定位:最佳实践 + 运行时扩展 + 插件体系 = 标准化的研发模式



时间来到 2020,虽然 build-scripts 已经很好的解决了项目开发的工程问题,然而它可能只占了项目开发的 20%,更多的 80% 则是在业务开发。在实际中我们发现,在淘系内部,不同的团队甚至有时在同一个团队,其项目采用的技术选型和项目开发规范也是不尽其同的,比如在状态管理方案,有的用 Redux、有的用 MobX 等等诸如此的差异问题。

太多选择了...

  • Redux
  • MobX
  • Immer
  • Rematch
  • unstated-next
  • icestore
  • ...
  • 100+


没有约定的团队,沟通和跨团队协作成本,以及长期的维护成本是非常高的,这时候统一一套开发模式就显得尤为重要。而 icejs 正是在这种背景下诞生的,通过框架强约束和提供完整的标准化的 React 应用开发模式和最佳实践。旨在使前端开发更加的快捷、高效,以及收敛技术栈、屏蔽底层差异和统一开发体验,帮助开发团队和开发人员降低开发和维护成本。

因此在这一阶段,核心从工程体系的脚手架演进成完整的标准化的 React 研发框架,在工程插件的基础之上支持并扩展了运行时的能力。通过运行时的能力我们可以非常简单的将业务开发中的最佳实践方案通过框架的插件机制进行实现和强约束,并预设一套的默认的最佳实践的插件集。

下面是演进的差异对比图:

框架提供了哪些能力

下面是一张 icejs 框架的大图,共分为四层从上到下依次是 基础设施 -> 研发标准 -> 性能体验 -> 研发模式


基础设施

基础设施主要包括依赖的社区方案如 UI 框架 React 、路由库 react-router 、构建工具 webpack、测试工具 jest 等,以及基于 webpack 封装的上层工程构建工具 build-scripts 等。这一层主要为 icejs 的基础依赖,同时我们基于这些基础依赖做了一定程度上的封装,让业务不需要关心底层的细节以及版本变更等,使用 icejs 时即可开箱即用。

- 研发标准

研发标准主要包括通过框架提供完整的标准化的 React 应用开发最佳实践,其中主要分为:

  • 标准化的研发流程:规划和约束了项目开发完整流程,包括基础规范的目录结构、样式方案、代码规范,路由规范、状态管理、以及大部分场景的业务方案等,最终形成完整的标准化的一套最佳实践方案;
  • 插件生态:提供插件机制和常用插件,其能力基于工程构建工具 build-scripts 封装,因此在插件能力上也完整继承了 build-scrtips。除了通过插件定制工程能力以外,框架还为插件扩展了运行时定制的能力,这让插件拥有更多的想象空间;
  • 自研体系:主要包括面向大型应用的微前端解决方案,简单友好的状态管理方案,场景复用的 Hooks 工具库,这部分能力会作为基础能力集成在框架中,只需简单配置即可按需开启。

- 性能体验

性能体验是衡量一个框架的重要指标,对框架而言。体验可以分为开发者体验和用户体验。其中开发者体验主要包括构建速度,错误调试,智能提示、开发文档等。对用户而言,则主要包括首屏渲染速度,预加载能力等,目前框架已支持 SSR 、Code Splitting 用于优化性能。

- 应用类型

不同的业务场景又不同的技术诉求,框架已支持 SPA、MPA、SSR、微前端、Serverless 一体化(仅限内部)等多种不同的业务场景。

- 配套模板

除了上述能力,我们还为框架提供了高质量的不同类型的的配置模板,在实际生产中可以根据业务情况进行使用。


更轻量的应用入口

框架只需要通过调用 createApp 即可启动一个包括路由、状态管理、数据请求等功能的 React 应用,同时在创建应用时你可以根据实际的业务需求进行自定义配置。相比传统的 React 应用开发而言,无需在手动去调用 ReactDOM.render() 、去创建路由 createHistory() 等繁琐而重复的应用入口的配置。

import { createApp, IAppConfig } from 'ice';

// 应用配置
const appConfig: IAppConfig = {
  // 启动项配置
  app: {},
 
  // 路由配置
  router: { },
  // 状态管理配置
  
  store: { },
 
  // 数据请求配置
  request: {},
  
  // 日志配置
  logger: {}
};

createApp(appConfig);

更灵活的导出方式

在 React 项目开发中,诸如 react-router 等第三方依赖是每个项目开发中都会重复出现的,因此在 icejs 中也进一步对这些依赖进行了收敛,减少项目的依赖项和版本的管理问题。同时还框架支持了通过插件往 ice 包里面注册新的 API,这意味着你可以编写某个插件将业务的通用能力集成到框架之中,在多个项目之间都可使用,且对业务开发者无感

import  {
  //插件扩展的接口
  store,
  request,
  loggger,
  config,
  helpers,
  ...

  // 内置组件
  Link,
  NavLink,
  ErrorBoundary
  ...

  // 内置 Hooks
  useHistory
  useSearchParams
  ...

  // etc
} from 'ice';

更简单的路由配置

路由是应用开发中必不可少一部分。从社区看 next.js、 umijs 等框架都在推崇使用约定式路由,但经过实践后我们发现约定式路由在一些简单的应用场景下是比较适合的,根据目录自动生成路由可以减少路由配置的工作,但当应用复杂的时候,需要增加各种标识和目录结构来判断,如嵌套路由、动态参数使用 $ 等,这时目录结构会变的很不优雅,对开发者来讲需要遵循约定式路由带来的新的规则和学习成本,因此在 icejs 框架设计中以及配套的模板里默认推荐使用配置式路由,但同时也支持了完整的约定式路由能力,这是可选的。

  • 配置式路由(推荐):标准路由协议,理解直观易懂
 const routerConfig = [
  // 分组路由,children 里的路由会将父节点的 component 作为布局组件
  {
    path: '/user',
    component: UserLayout,
    children: [
      {
        path: '/login',
        exact: true,
        component: UserLogin,
        // 配置路由的高阶组件
        wrappers: [wrapperPage]
      },
      {
        path: '/',
        // 重定向
        redirect: '/user/login',
      },
      {
        // 404 没有匹配到的路由
        component: NotFound,
      },
    ],
  },
  // 非分组路由
  {
    path: '/about',
    component: About,
  },
];
  • 配置式路由(可选):约定目录规范,无需配置自定生成路由。


更智能的状态管理

状态管理是应用开发和技术选型最重要的一环,从 React 生态来看,目前主要的状态管理方案大致可以分为三类,响应式状态管理方案类 MobX 系列、单向数据流类 Redux 系列、以及在 React Hooks 之后的基于 Hooks 系列,而在这些之上衍生的方案估约不下 100+,如果你有选择困难症,那么你可以选择使用 icejs,因为你不再需要关心眼花缭乱的状态库、概念繁多的 API、重复的模板代码等问题。

在做状态管理方案的时候,我们核心关心的是什么,这里主要抽象四个维度:

  • 怎么去定义一个模型
  • 视图怎么跟模型绑定
  • 视图怎么去消费模型
  • 模型之间如何取联动

在进一步抽象,其实本质就是 UI 视图 和 DATA 模型如何交互,基于此考虑在 icejs 中内置集成了自研的 icestore 状态管理方案,并在此基础上进一步遵循 “约定优于配置” 原则,进行抽象和封装,使得状态管理变得非常容易。

  • 你只需要 定义模型视图消费 即可,其他模型与视图绑定的工作由框架去做。
  • 你只需要 写更少且更优雅 的代码即可,其他重复的模板工作由框架去做。


- 模型定义

export default {
  state: 0,
  reducers: {
    increment:(prevState) => prevState + 1,
    decrement:(prevState) => prevState - 1,
  },
  effects: () => ({
    async asyncDecrement() {
      await delay(1000);
      this.decrement();
    },
  })
};


- 视图消费

import { store } from 'ice';

function Counter() {
  // model 名称即文件名称,如 src/models/counter.ts -> counter
  const [ count, dispatchers ] = useModel('counter');
  const { increment, asyncDecrement } = dispatchers;
  return (
    <div>
      <span>{count}</span>
      <button type="button" onClick={increment}>+</button>
      <button type="button" onClick={asyncDecrement}>-</button>
    </div>
  );
}


更友好的工程配置

框架提供了绝大部分的工程配置项, 值得一提的是只需要使用 JSON 格式配置工程即可,相比 react-scripts 以及社区其他的工程方案,JSON 配置能够极大的简化工程配置的复杂度,也更加简单。

{
  "alias": {
     "@": "src"
  },
  "outputDir": "dist",
  "publicPath": "./",
  "sourceMap": true,
  "minify": false,
  "proxy": {
    "/**": {
      "enable": true,
      "target": "http://127.0.0.1:6001"
    }
  }
}

更简洁的微前端接入

基于 icestark 微前端方案和 icejs 的插件机制,我们封装了 build-plugin-icestark 插件,通过该插件可以大大降低接入微前端方案的成本。只需简洁的配置即可接入。

  • 配置框架应用
import { createApp } from 'ice'

const appConfig = {
  icestark: {
+    type: 'framework',
+    getApps: async () => {
+      
+    },
+    appRouter: {
+      
+    },
  },
};

createApp(appConfig)
  • 配置微应用
import { createApp } from 'ice'

const appConfig = {
  icestark: {
+    type: 'child',
  },
};

createApp(appConfig)


更多其他的能力

除了上述能力之外,框架还抽象提供以下常用的功能,具体请查看文档:

  • 规范化的数据请求方案
  • 通用场景的页面配置能力
  • 简单的的环境构建/运行时配置
  • 友好的代码分割、错误边界处理能力
  • 支持 SPA、MPA、SSR、微前端、Serverless 一体化等应用类型

框架是如何设计的

框架的核心架构主要包括 运行时 和 工程能力 两部分:其中运行时部分主要提供扩展运行时能力,在 icejs 中称之为运行时模块,通过模块提供的 API 可以非常简单的将通用逻辑插件化,以便在不同的项目间进行抽象和复用, 如我们可以通过 AddProvider 在应用最顶层添加自定义的 Provider 能力,通过 wrapperRouter 为每个路由包装一个组件等等。而工程部分则主要提供插件扩展和生成器的职位。

插件机制

icejs 基于工程构建工具 build-scripts 封装,因此在插件能力上也完整继承了 build-scrtips,插件提供了丰富的 API:

  • registerTask:注册多 webpack 任务
  • registerUserConfig:注册 build.json 中的顶层配置字段
  • registerCliOption:注册各命令上支持的 cli 参数,比如 npm start --https 来开启 https
  • onGetWebpackConfig:获取内置的配置,并对配置进行自定义修改
  • onHook:通过 onHook 监听命令运行时事件,onHook 注册的函数执行完成后才会执行后续操作,可以用于在命令运行中途插入插件想做的操作
  • ...


官方插件

icejs 的核心实现就是一套官方的插件集,包含以下功能:

  • plugin-react-app:提供 React 应用开发的 webpack 配置
  • plugin-core:提供框架的运行时能力和 API
  • plugin-router:提供配置式和约定式路由的功能
  • plugin-store:提供状态管理的功能
  • plugin-request :提供数据请求的功能
  • plugin-config:提供环境配置的功能
  • plugin-logger:提供日志功能的功能
  • plugin-helpers :提供帮助函数的功能
  • plugin-service:提供接口服务的功能
  • plugin-ssr:提供 SSR 的功能
  • plugin-icestark:提供微前端的功能
  • plugin-mpa:提供 MPA的功能


插件开发

插件机制是 icejs 的核心之一,当前 icejs 的基础能力都是通过插件来实现。插件机制不但可以保证框架核心足够精简和稳定,还可以通过插件对运行时和编译时的能力进行封装复用,最终打造一个完整的生态。

  • 自定义扩展运行时能力:
// module.ts
export default ({ addProvider, appConfig, ...rest }) => {

  const StoreProvider = ({children}) => {
    return (
      <AppStore.Provider initialStates={appConfig.initialStates}>
        {children}
      </AppStore.Provider>
    );
  };

  // 通过 AddProvider 在应用顶层添加 Provider
  addProvider(StoreProvider);
};
  • 自定义扩展工程能力:通过插件提供的 API,可以方便拓展和自定义工程能力,该方法会接收两个参数,第一个参数是插件提供的 API 接口,推荐按照解构方式使用,第二个参数 options 是插件自定义的参数,由插件开发者决定提供哪些选项给用户配置。
// index.ts
export default async (api options) => {
  const { context, onHook, onGetWebpackConfig } = api;

  // 通过 onGetWebpackConfig 获取 webpack-chain 形式的配置,并对配置进行自定义修改
  onGetWebpackConfig(config => {

  })

  // 通过 onHook 监听命令运行时事件,可以用于在命令运行中途插入插件想做的操作
  onHook('before.start.run', async () => {
    
  })
}

下一步

icejs 框架目前已经在淘系、飞猪等众多业务中经过大量项目的实践,我们也将逐步向社区推广。在技术圈有一句话是:没有一个技术方案是完美能覆盖到所有场景无瑕疵的,从 ice-scripts@1.x -> ice-scripts@2.x -> icejs 研发框架经过了三个大的版本变化,但这些版本变化都是结合淘系的业务实践以及用户诉求不断演进和探索的,在能力和规范性上也都在不断提高。接下来我们将主要从能力完备、框架性能、开发体验三个方向进行优化和探索。

❤️ 最后

感谢所有在项目中使用 ICE 技术体系,以及参与贡献和反馈的同学,是你们让 ICE 一直在不断的成长和完善。希望在未来依然能和我们一起前行,一起让前端开发变得更简单更友好。

如果觉得对本文对你所有帮助,欢迎 star 或者点击我们的官网了解更多,附上相关链接如下:


最后的最后,如果你对飞冰体系的技术产品有兴趣,希望加入我们一起搞事情,不妨扫一下下方的二维码,加个好友一起聊聊。

编辑于 2020-05-21 09:35