目前在洞窝平台下有很多个后台管理系统,比如运营平台,商家端,卖场saas端等。这些项目在开发过程中都有沉淀自己的一些基础组件或者是业务组件。
比如上传组件,select组件,form表单提交组件,列表组件 等等,其实这些组件都有一些共通的逻辑是可以复用的,还有业务组件是完全相同的,比如订单页面,清结算页面是完全相同的。
基于这种情况,因为每个项目的组件资源沉淀在各自的项目当中,导致各个项目的这些组件资源沉淀无法进行集中维护和统一管理。同时,因为这些平台都同属一个产品下,所以有很多共同业务,这样就催生出来很多业务逻辑相同的组件。
共用前端资源, 就是将公共的前端资源提取出来, 例如公共样式/公共逻辑/公共组件/公共图片资源等等, 让多个项目来引用, 避免复制多份, 避免重复开发, 统一管理和维护。只要更新公共资源,其他引用的项目就可以同步更新,提升开发效率,降低开发成本。
组件共享方案
1.CV大法
第一种是大家最熟悉的就是 CV 大法,直接从一个项目复制到另一个项目,速度非常快。但是问题也很明显,可维护性低,同时各个项目就各自多了一套代码,当需求发生变更时需要各自更新。
2.Npm 包
第二种是大家比较熟悉的 NPM 包共享的方式,这种方式简单易上手,也是目前最常见的。但问题是各个项目引用的时候都会打包构建一次,如果项目比较大,可能会导致构建时间很长。同时这种共享方式的更新链路很长,可能出现一个项目都已经发布上线一段时间了,另一个项目还保留着之前的版本。
也就代表每个应用都有相同的npm包,本质上没有真正意义上的实现模块共享和复用,只是代码层次共享和复用了,应用打包构建时,还是会将依赖包一起打包。
每个应用都会打包该模块,导致依赖的包冗余,没有真正意义上的共享复用
当
npm包
进行更新发布了,应用还需要重新构建,调试麻烦且低效
3.git submodule(子模块)
子模块(submodule)是Git为管理仓库共用而衍生出的一个工具,通过子模块您可以将公共仓库作为子目录包含到您的仓库中,并能够双向同步该公共仓库的代码,借助子模块您能将公共仓库隔离、复用,能随时拉取最新代码以及对它提交修复,能大大提高您的团队效率。
这个子模块就是我们共享的模块,它是一个完整的Git仓库,换句话说:我们在应用项目目录中无论使用git add/commit都对其不影响,即子模块拥有自身独立的版本控制。
在Git 中你可以用子模块submodule来管理这些项目,submodule允许你将一个Git 仓库当作另外一个Git 仓库的子目录。这允许你克隆另外一个仓库到你的项目中并且保持你的提交相对独立。
submodule 不会管理模块间的依赖关系。如果没有正确使用git submodule update而搞乱了版本的依赖关系,就乱套了。
在父目录下 pull 不会自动更新 submodule。
git 在解决冲突时不会保存 submodule 的指针。也就是说,如果没有手动更新指针,合并更改时会丢失已解决的冲突。在 submodule 提交后,父仓库中的 submodule 会是一个 detached head,因为它指向旧 head,而不是当前的 head。在父仓库或者 submodule 下 push,不会发布其他父仓库的改动。
和npm对比:
git submodule: 管理的依赖是子模块的源码
npm packjson:管理的是子模块的构建产物
和子模块对应的还有一个
Monorepo
Monorepo 全称叫monolithic respoitory,即单体式仓库,核心是允许我们将多个项目放到同一个仓库里面进行管理。主张不拆分repo,而是在单仓库里统一管理各个模块的构建流程、版本号等等。
这样可以避免大量的冗余node_module冗余,因为每个项目都会安装vue、vue-router等包,再或者本地开发需要的webpack、babel、mock等都会造成储存空间的浪费,将所有的相关package都放入一个repository(仓库)来管理,这不是显得项目很臃肿?
统一构建工具所带来更高的要求
仓库体积过大,维护成本也高
4.Webpack externals (外部扩展)
externals
配置选项提供了「从输出的 bundle 中排除依赖」的方法,
防止
将某些 import 的包(package)
打包
到 bundle 中,而是在运行时(runtime)再去从外部获取这些
扩展依赖(external dependencies)
。换句话说通过在
external
定义的依赖,最终输出的bundle不存在该依赖,主要适用于不需要经常打包更新的第三方依赖,以此来实现模块共享。但externals 无法支持多版本共存的情况。
配合CDN,把常用不需要经常打包更新的代码或第三方依赖打包成js文件,放到CDN上,通过 CDN + Webpack externals 的方式进行组件共享,这种方式其实和 npm 差不多。
它有一个优点是可以去抽离一些公共库,但是无法做到按需加载,必须以 script 标签形式提前引入。一般我们使用这种 CDN 的方式,都是通过 on package 一个 npm 包的静态链接这样的形式去加载。其实还是需要发布一个 npm 包,所以它的更新链路和 npm 包的更新链路是一样的。
5.webpack dll
官方介绍:"DLL" 一词代表微软最初引入的动态链接库, 换句话说我的理解,可以把它当做缓存,通过预先编译好的第三方外部依赖bundle,来节省应用在打包时混入的时间。
Webpack DLL 跟 上面提到的externals本质是解决同样的问题:就是避免将第三方外部依赖打入到应用的bundle中(业务代码),然后在运行时再去加载这部分依赖,以此来实现模块复用,也提升了编译构建速度
6.Webpack & module federation(联邦模块)
这种方案可以让零售商端去动态加载运营平台的原子组件,反之,运营平台也可以动态去加载零售商端的业务组件。这种方案的优势是依赖的共享资源不需要重复构建,同时可以实现依赖共享。因为 MF 是 webpack 5 的新特性,所以强依赖 webpack 5 。
如果你是 webpack 4 及其以下版本,可能你需要先升级到 webpack 5。MF 的设计动机就是为了让多个团队可以共同开发一个或者多个应用,简而言之,就是使应用之间能共享组件开发资源。
如果使用 npm 的方式进行共享,它的组件更新传导链路就如上图所示。如果form组件有优化更新,B 项目重新打包发布了一个新版,然后用在运营平台上,再重新打包,然后发布上线。
同时这里需要手动通知 A 项目的人进行原子组件库 npm 包的更新,A 团队(零售商端)需要更新版本后再重新打包发布上线。可以看出,组件更新传导链路非常长,如果有更多项目引用到了这个包,那这条链路会继续增加。
然后,来看下 MF 共享模式的组件更新传导链路。运营平台更新了form组件,只需要重新打包发布即可,零售商端因为加载了线上资源,所以会跟着一起更新,这样一来,链路短了很多。所以由此也能看出,我们使用 MF 做一个组件共享,最大的特点就是它能实现实时更新。
那 Module Federation 它是怎么做的?
MF 的共享模式,如果 B 团队的后台直接将form组件以 MF 的方式暴露出去,零售商端会直接通过异步 chunk 的方式去动态加载后台的组件资源,同时会共享这些组件所依赖的 React,然后它会把这个 React 打包成一个 Shared Chunk。
那 Module Federation 是怎样把这个form组件打包出来的,以及它的具体形式如何?
首先 MF 应用可以导出一些组件,我们把它叫做一个 container。这个 container 它有一个入口文件,一般叫做 remoteEntry.js,并且 remoteEntry.js 是可以自定义的。它的核心内容主要是两个方法,一个是 get 方法,get 你可以理解为它是这个入口文件里面的组件配置表,MF 资源它导出了哪些组件,它就在 get 方法里面去进行配置,然后你就可以通过这个 get 方法拿到对应的组件资源。
另一个是 init 方法,它会去做 shared scope 对象的初始化,将你配置的共享依赖放到 share scope 对象里面去。如图所示,入口文件导出了两个组件,一个是 from组件,一个是 button 组件。两个组件都会分别打包成一个 chunk.js,同时它会去依赖一个 react.js。当访问这个form组件的时候,它就会把form组件的 form.trunk 以及 react.trunk 一起加载过来。
具体的路径:首先是零售商端去加载form组件,它会先去加载 remoteEntry.js,也就是 container 的入口文件。紧接着,它会去拿到调用 get 方法,再去拿form组件具体的 chunk,同时进行 share scope 的创建,以及将 React 放到它本地的 share scope 里面去。
在这个时候,MF 会去比较原子组件所依赖的 React 版本和零售商端依赖的 React 版本是否一致,这里是通过 semver 版本工具库的方式去比较的。
如果不一致,假如零售商端依赖的 React 版本高于form组件所依赖的 React 版本,默认会使用版本号更高的那个 React。如果一致,它会优先使用form组件依赖的 React 版本,因为它在执行 init 方法的时候,会去覆盖零售商端的 share scope,将form组件依赖的 React 版本覆盖掉前台的资源,所以它加载的就是form组件所依赖的 react.js。如果说你想要一个固定的版本的话,也可以在配置里面去配。
7.Bit
Bit是一个开源的cli工具,用于跨项目和跨仓库的隔离组件之间进行协作。
Bit 支持在多个仓库间分离和复用代码,统一控制变更,因此有利于在团队内部增进代码复用,减少代码复用的额外负担。
Bit 支持无缝分离、复用任何仓库中的模块,自动定义相应的环境和依赖树。无需重构,即刻发布来自任意仓库的组件。
同时管理代码变更和组件依赖,让团队内部跨项目复用代码更方便。Bit 简化了 UI 组件上的协作过程。团队成员可以共享,维护和同步来自不同项目的隔离组件。
Bit能带来什么?
提高代码可重用性
提高设计和开发效率
保持 UI 和 UX 的一致性
增加项目的稳定性
Bit有什么特点
从现有的库或项目中提取要直接共享的组件。
通过与项目的其余部分分开构建和测试每个组件,验证组件的独立性。
从任何使用共享组件的应用程序中更改其源代码。
在本地修改的基础上获取组件中已发布的更改。
直接从使用应用程序中回馈对组件所做的更改。
自动将每个组件包装为 npm 软件包。
分发离散组件,而不是单个大型软件包。
根据组件依赖性的变化自动进行组件版本控制。
与领先的框架和工具一起使用:React,Vue,Angular,Mocha,Jest。
与 Git,NPM 和 Yarn 一起使用。
Bit可以很好地与JavaScript和JavaScript框架协作
Bit有什么缺点
致命的缺点就是不支持私有化,安全问题。
附antd4+, umi 3+的项目升级到webpack5的方法
运营平台和零售商端(大B)目前采用的都是antd4+配合umi3+的方式。
配置文件允许在 .umirc.js 或 config/config.js (二选一,.umirc.js 优先)中进行配置,依传统我们在config.js这个文件里,完成我们的配置,umi 3在config.js里为我们提供了 chainWebpack, webpack5这两个属性,前者用来配置FD(联邦模块), 后者用来打开webpack5模式,后者是前提。
./config/config.js
配置注意事项:
消费方要开启
dynamicImport
通过配置
webpack5: { lazyCompilation: {} }
可开启基于路由的按需编译,用于解决项目文件数超 3000 个的巨型应用启动缓慢问题。
作者:洞窝-xiaomi