乐趣区

关于前端:SREWorks前端低代码组件生态演进monorepo架构重构和远程组件加载实践

作者:王威(地谦)

文章构造

  • 我的项目背景
  • 演进剖析
  • monorepo 架构演进
    • Webpack 与 Rollup
    • 如何平滑迁徙
    • 构建优化
  • 组件的可扩大与可插拔
  • 演进总结
  • 版本动静

我的项目背景

SREWorks 是一个面向企业级简单业务的开源云原生数智运维平台,是大数据 SRE 团队多年工程实际的锻炼及积淀。前端对立托管工程(frontend)作为平台的重要一环,提供了一套 serverless 体验的配置化前端低代码技术计划:低代码、配置化是前端低代码计划的根底个性。

frontend 工程采纳 React+antd 为主的技术框架,设计了一套组件映射、编排、解析、渲染的工程体系:以 antd 组件为自在编辑粒度,用户在前端设计器通过可视化交互或者 json 编辑的形式,根据运维工作的理论应用场景,对组件进行属性配置 / 组件嵌套拼装;同时依据应用景指标需要对页面组件进行布局的编排、数据源的绑定以及在适合点位插入 Dynamic Logic,实现页面节点的设计工作,造成节点模型 nodeModel,经模板解析引擎进行解析渲染。因为之前已对架构设计做过一篇具体的介绍,在此不再赘述,详情可移步这一篇 https://mp.weixin.qq.com/s/_k…

咱们开源这套前端工程的愿景是:积淀更多的应用场景,整合更多的用户需要,与社区一起共建一个丰盛的前端运维组件生态。在过来的半年中,为了让前端组件生态更好地演进,frontend 针对“可扩大、不便插拔”这两个关键点进行架构降级:

  1. 架构层面进行了 monorepo 模式重构;
  2. 前端组件反对近程动静加载;

在文本中,咱们对整个迭代过程中陆陆续续碰到过一些问题,以及技术计划的抉择与思考做一个阶段性的演绎和总结。

演进剖析

关注咱们开源动静的同学应该晓得,咱们初版开源的 frontend 代码量近 10 万之多,在没有牢靠详实文档的帮忙之下,想要疾速理分明整套工程的设计理念,构造机理进而能参加奉献还是有肯定难度的。同时工程外部细分来看,设计器,模型层,组件层各自又有相差很大的更新频率,在开发时一个小小的改变都须要整个工程进行构建。

另外一方面,frontend 来源于公司外部工程实际的版本,两者尽管同源,然而因为公司外部和开源场景都在疾速性能演进,最后设计的公共框架层和组件层共享机制曾经有些举步维艰,这也为后续这次演进迭代埋下了伏笔。联合咱们打造开源生态“可扩大、不便插拔”两大指标,综合来看须要解决以下问题:

  • 基座层面反对 runtime 近程组件的加载,解决用户的多样化的场景与诉求
  • 构造上对大而全的工程进行细粒度拆分
  • 提取 framework 框架层和 widget 组件层并可能独自构建
  • share-tools 工具可共享
  • 外部业务代码剥离
  • 在适配外部业务运行的前提下降级各依赖版本,便于新技术引入与演进
  • 构建工具的降级与配置调优,无效晋升构建效率升高构建体积

针对以上待解决的问题,对演进计划进行了技术调研,也参考和驳回了社区同学的倡议,咱们决定采取上面两个计划来解决上文提到的问题:

  1. 架构层面,采纳 monorepo 模式进行重构提取出 framework 框架层、widget 组件层、shared-tools 等几个子依赖包,以 webpack5(主利用包)+rollup(子依赖包)作为构建工具进行过调优构建;
  2. 针对无奈进入代码库的组件,提供近程组件脚手架,反对将近程组件打包 umd 格局并以动静 script 标签模式进行动静引入和移除,做到 runtime 加载扩大;

上面咱们来具体分享下这两项计划的施行:

Monorepo 架构演进

Monorepo 即单仓 (repository) 多包(package),大型前端工程项目采纳这种模式进行开发治理,能带来诸多的开发和治理便当:

  • 更加清晰的模块构造和依赖关系
  • 更细粒度的独立构建单元便于合作开发和不同更新频率的子包独自发版
  • 更加高效的代码复用等

在 v1.4 版本中采纳 lerna + yarn workspace 的技术计划进行了 Monorepo 的架构实际:将原工程拆分为 @sreworks/app 主包利用,和 @sreworks/components、@sreworks/widgets、@sreworks/framework、@sreworks/shared-utils 四个 npm 子依赖包。目录构造变动如下图所示:

通过 lerna+yarnworkspace 的计划,将各子包配置进入 workspace 空间,workspace 空间的各子依赖包的更新,会实时同步到主利用包的 node_module,无需公布 npm,且能抉择针对特定子包独自公布 npm 版本或者各个包同步公布新的版本,能够更小粒度的更新主利用依赖,便捷高效。

Webpack 与 Rollup

在设计好子包的拆分之后,就开始着手进行文件构造的革新、组件引入挂载形式的变更、近程组件加载的解决优化,主题式样的迁徙等等问题(因为篇幅无限,本文仅对大的通用环节进行介绍,对解决细节感兴趣或者想探讨沟通的同学能够退出文末的交换群)。

在解决完工程构造代码后,咱们开始着手工程的构建:构建工具的抉择,对于构建工具,webpack,gulp,rollup 以及起初的 vite,综合咱们理论的状况,最初选定了 Webpack 和 Rollup 作为备选计划:Webpack 和 Rollup 实质都是对非 ES5 代码的本义与打包,一个功能强大的 compiler 函数,通过配置入口读取指标文件,而后输入本义文件;要实现整个工程的打包,还须要 babel-loader,React 和 Vue 等 loader 的解决和一系列 plugin 的适时挂载解决能力实现对诸如图片,css 文件、JSX 及 Vue template 等类型文件的解决,及 js 挂载 html 的工作。

Webpack 与 Rollup 的特点:

依据以上特点比照以及参考业内优良开源我的项目实际,frontend 抉择了主包利用应用 Webpack5 作为构建工具,Rollup 作为子包利用构建工具的计划,主包利用 HMR 对于日常开发而言是刚需,因而抉择 Webapck;对子包依赖而言,更便捷的配置和更小的输入才是更佳的抉择。

如何平滑迁徙

整个这么大的工程体量,在没有齐全进行代码层面准确无误的拆解并构建的状况下,是跑不起来的,一个很小的谬误都会造成整个我的项目抛错。且二方包应用了 sourcemap 也是没有用的,通过了主包构建,很难排查出哪里除了问题,于是就又要推倒重来……在开始的实时过程中,消耗了很多的工夫,叠加每个子包的批改排查,主利用包的构建等验证周期很长,探索性革新的难点就在于此。

那么能更小粒度的验证和迁徙吗?近程组件的加载给了启发思路。尝试性将组件包 @SREWorks/widgets 打包成 esm 格局并在原来大而全的工程中间接批改 node_modlues 引入依赖包打包文件, 和相应加载机制,在能运行起来的工程下来验证各子依赖包,配合 sourcemap, 问题排查霎时提速。就这样,又顺次进行 @SREWorks/framework 等其余包的验证,蒙眼构建排查问题得解。

式样问题比拟头疼,在此采纳的计划是通用式样在主包保留,子包因为式样重置笼罩的场景较少,采纳了 css-module 的形式进行隔离构建。

构建优化

通过各子依赖包在原有工程上进行平滑验证,来到主利用包的构建环节,构建体积居然达到了惊人的 5.5M,还是 gzip 压缩后的体积:

通过剖析这张图,该版本构建存在以下问题:

  • 同名依赖屡次呈现,各子依赖包存在反复的依赖
  • 局部依赖包构建体积偏大,如 BizCharts

针对以上存在的问题对 @sreworks/app 整体进行三个维度的优化解决:

第一,通过对立子包依赖排查合并依赖版本,优化至 2.8M

const namespace = {appRoot: path.resolve('src'),
  appAssets: path.resolve('src/assets'),
  // 缩小子依赖包外部反复依赖
  '@ant-design': path.resolve(process.cwd(), 'node_modules', '@ant-design'),
  'js-yaml': path.resolve(process.cwd(), 'node_modules', 'js-yaml'),
  'ace-builds': path.resolve(process.cwd(), 'node_modules', 'ace-builds'),
  'brace': path.resolve(process.cwd(), 'node_modules', 'brace'),
  'lodash': path.resolve(process.cwd(), 'node_modules', 'lodash')
}
...
  resolve: {
    alias: paths.namespace,
    modules: ['node_modules'],
    extensions: ['.json', '.js', '.jsx', '.less', 'scss'],
  },

第二,抽离局部大依赖包到 cdn,如下在 externals 配置项进行剥离;将体积优化至 1.6M。但思考到某些专有云应用场景,无奈应用内部 cdn。于是采纳自定义构建脚本,从 node_modules 中迁徙指标依赖到输入文件夹并加载至 html 的计划,升高参加构建的大依赖包数量,同时保障专有云环境对其失常的应用。

  externals: {
    // 剥离局部依赖,不参加打包
    'react': 'React',
    'react-dom': 'ReactDOM',
    "antd":"antd",
    ...
  },

第三,调整要害组件门路和 1.48M, 缩小体积 70%,构建工夫由 V1.3 版本的 74 秒,优化至 23 秒,晋升 68%。

组件的可扩大与可插拔

尽管 frontend 已内置运维场景罕用的根底组件,图表组件,landing 组件,布局组件等五十余个组件。依据开源之后用户的应用反馈来看,用户依然有着定制化,可扩大的共性诉求:总的来讲大抵分为两大类:

  1. 前端框架也是 React,有本人定制化的应用场景,内置组件不能满足以后需要,须要扩大
  2. 前端技术栈是 Vue,历史组件积淀比拟多,全副进行 React 重构老本太大

针对问题一,frontend 原本就有提供 JSXRender,反对用户以 JSX 进行简略的动态渲染类的组件自定义扩大,但不反对属性配置以及数据源及 dynamic 业务逻辑解决等高级个性。前端插件化,很容易想到 npm 包的引入,然而这也只能在工程代码开发的场景下才适配,要 runtime 应用和移除,就要另寻计划。再进一步深刻追溯,前端开发从 jQuery 时代倒退到当今的 Agular,React,Vue 三驾马车以及各种工程化构建工具的参加,但实质其实并没有发变动,仍然是以 html 中以 script 标签挂载 js 代码进行渲染加载的。因而天然想到以 script 标签的模式加载咱们的近程组件,不过这里要做到动静、批量加载、可移除,即:将近程组件打包 umd 格局并公布到云端,并获取相应精确门路,以动静 script 标签的模式引入:

(function (){let script = document.createElement('script')
  script.type = 'text/javascript'
  script.src = url // 指标组件 url
  document.getElementById('targetDomId').appendChild(script)
})()
script.addEventListener('load',callback,false) // 嵌入逻辑

如果要批量加载多个的话,即:

const loadRemoteComp = async () => {let remoteCompList = ["url_a","url_b","url_c",...];
  try {
    remoteComList.forEach(item => {pros.push(Promise.resolve(loadSingleComp());
    })
    window['REMOTE_COMP_LIST'] = await Promise.all(pros);
  } catch (error) {console.log(error);
  }
}
loadRemoteComp()

当然这里还波及到浏览器终端适配,容错等细节。在此 frontend 采纳比拟成熟的 systemjs 包进行组件的加载,对以上细节都有做妥善处理,正当借力,省时高效。

针对问题二, 技术栈不同,即异构组件的加载。目前来说 frontend 暂且只针对 Vue 组件做了异构兼容渲染,在 React 中应用 Vue 组件。一开始想到的是应用转换工具,将 vue 组件手动转换为 React 组件,之后再粘贴构建,但这种形式有个很大的缺点:不同版本 api 差别较大,手动转码个别须要对转换过后的代码进行人工二次排查调整,须要开发人员对于两种框架的新老版本属性相熟理解,对于不合乎的代码或已更新的 hooks 等进行二次确认,无形中进步了应用门槛。

受到 Docker 容器的启发,思考 React 和 Vue 尽管属于不同的技术栈体系,但区别于 Java 和 Golang 的差别,Vue 和 React 在实质上都是原生 js 对象的封装,所以实践上讲是能够在 React 中进行容器化渲染 Vue 组件的:即实质是绑定挂载 Vue 对象的操作:

createVueInstance (targetElement, reactThisBinding) {const { component, on, ...props} = reactThisBinding.props
  reactThisBinding.vueInstance = new Vue({
    el: targetElement,
    data: props,
    ...config.vueInstanceOptions,
    render (createElement) {
      return createElement(
        VUE_COMPONENT_NAME,
        {
          props: this.$data,
          on,
        },
        [wrapReactChildren(createElement, this.children)]
      )
    },
    components: {[VUE_COMPONENT_NAME]: component,
      'vuera-internal-react-wrapper': ReactWrapper,
    },
  })
}

通过 frontend 近程组件脚手架 @sreworks/widget-cli, 对 React 组件和 Vue 组件进行打包并公布到 cdn,而后在物料开发处,进行编辑即可便捷进行近程组件 runtime 加载和移除,解决了问题一和问题二,达成了“扩展性 ”和“ 可插拔”的指标。

演进总结

到这里,根本解决了开篇列举的一系列问题,为构建前端运维组件生态铺设好了共建门路,能够做到:

  • 从 @sreworks/widgets 包开发,JSXRender 自定义组件,应用 @sreworks/widget-cli 开发近程组件三个维度扩大组件利用丰盛度
  • 更加清晰的构造依赖关系,升高学习和奉献参加这套低代码工程的门槛
  • 以更小粒度的更新单元,更短的构建工夫,便捷日常合作开发
  • 子包拆分后,为后续小规模分步引入 TS 提供了条件

版本动静

咱们会依据工作我的项目节奏,继续对性能进行欠缺优化和降级,以后次要是前端低代码性能的输入,后续 API 低代码编辑编排已纳入版本布局,以笼罩全链路低代码应用,大家有比拟好的倡议欢送多提 issue,同时也欢送更多的开发者可能参加到咱们的生态建设中来(@小助手,也能够间接前端 @地谦)

SREWorks 开源地址:

https://github.com/alibaba/sr…

退出移动版