作者:王伟(狄谦)文章结构项目背景演化分析Monorepo架构演进Webpack和Rollup如何平滑迁移构建和优化组件可扩展性和集成性PluggableEvolution摘要版动态项目背景SREWorks是一个面向企业级复杂业务的开源云原生数字智能运维平台。是大数据SRE团队多年工程实践的锤炼和沉淀。前端统一托管项目(frontend)作为平台的重要组成部分,提供了一套可配置的、具有serverless体验的前端低代码技术解决方案:低代码和配置是前端低代码的基本特征。代码解决方案。前端项目采用React+antd的技术框架,设计了一套组件映射、排列、解析、渲染工程体系:以antd组件为自由编辑粒度,用户可以使用可视化交互或json编辑在前端设计师中。根据运维工作的实际使用场景,对组件进行属性配置/组件嵌套组装;同时,根据使用场景的目标需求,安排页面组件的布局,绑定数据源,并在适当的点插入动态逻辑,完成页面节点设计工作,形成节点模型nodeModel,由模板解析引擎解析渲染。由于前面已经详细介绍了架构设计,这里不再赘述。详情可以参考这篇文章https://mp.weixin.qq.com/s/_k...我们开源这个前端项目的愿景是积累更多的使用场景,整合更多的用户需求,与社区一起构建丰富的前端运维组件生态。在过去的六个月里,为了更好地演进前端组件生态,frontend针对“可扩展性和易插件性”两个重点进行了架构升级:monorepo模式在架构层面进行了重构;前端组件支持远程动态加载;在文中,我们对整个迭代过程中陆续遇到的一些问题,以及技术方案的选择和思考,进行了阶段性的归纳和总结。演化分析关注我们开源动态的同学应该知道,我们第一版开源前端有近10万个代码。在没有可靠详细文档帮助的情况下,我们想快速了解整个项目的设计理念、结构和机制并参与其中。投稿还是很困难的。同时,从项目内部细分来看,designer、model层、component层各有不同的更新频率。开发过程中的一个小改动需要构建整个项目。另一方面,frontend来源于公司内部工程实践的版本。两者虽然同源,但由于公司内部和开源场景的功能演进速度太快,原本设计的公共框架层和组件层的共享机制已经有些困难。为后续的迭代演化奠定了基础。结合我们打造开源生态“可扩展、易插拔”的两大目标,我们需要全面解决以下问题:底层支持运行时远程组件的加载,解决用户多样化的场景和需求.对项目进行细粒度拆分,抽取framework框架层和widget组件层,可单独构建share-tools工具,在适应内部业务运行的前提下共享内部业务代码剥离和升级各依赖版本,便于新技术引入和演进构建工具升级和配置调优,有效提高构建效率,降低构建体积。针对以上需要解决的问题,我们对演化方案进行了技术研究,同时也参考并采纳了社区同学的建议。我们决定采用以下两种方案来解决上述问题文中提到的问题:在架构层面,使用monorepo模式重构抽取framework框架层、widget组件层、shared等几个子依赖包-tools等,并使用webpack5(主应用包)+rollup(子依赖包)作为构建工具进行了优化构建;对于无法进入代码库的组件,提供远程组件脚手架,支持远程组件以umd格式打包,以动态脚本标签的形式动态引入和移除,实现运行时加载和扩展;下面我们就来详细分享一下这两种方案的实现:Monorepo架构演进Monorepo是单仓库(repository)和多包(package)。大型前端工程项目采用这种模式进行开发和管理,可以给开发和管理带来很多便利:更清晰的模块结构和更细粒度的依赖,独立的构建单元便于协同开发和分包与不同的更新频率分别发布,以实现更高效的代码重用。架构实践:将原项目拆分为@sreworks/app主包应用,以及@sreworks/components、@sreworks/widgets、@sreworks/framework、@sreworks/shared-utils四个npm子依赖包。目录结构的变化如下图所示:通过lerna+yarnworkspace方案,将各个子包配置到工作空间空间中,工作空间空间中各个子依赖包的更新都会同步到node_module中实时更新主应用包,无需发布npm,可以选择针对特定子包单独发布npm版本,也可以为每个包同步发布新版本,实现主应用依赖同步更新粒度更小,方便高效。Webpack和Rollup设计好分包拆分后,开始改造文件结构,改变组件导入和挂载方式,优化远程组件加载的处理,迁移主题样式等。(限于篇幅,在此文章只介绍了大概的大体链接,有兴趣处理细节或者想讨论交流的同学可以加入文末交流群)。处理完项目结构代码,我们开始构建项目:构建工具的选择,关于构建工具,webpack,gulp,rollup以及后来的vite,根据我们的实际情况,最终选择了Webpack和Rollup作为备选解决方案:Webpack都可以和Rollup本质上是对非ES5代码的转义和打包。强大的编译功能通过配置入口读取目标文件,然后输出转义文件;完成整个项目的打包,babel-Loader、React、Vue等加载器处理及一系列插件适时挂载处理,完成图片、css文件、JSX、Vue模板等类型文件的处理,以及js挂载html的工作。Webpack和Rollup的特点:基于以上特点的对比以及业界优秀开源项目的实践,frontend选择使用Webpack5作为主包应用的构建工具,Rollup作为分包应用施工工具。刚好需要,所以选择Webapck;对于分包依赖,更方便的配置和更小的输出是更好的选择。如此大体量的整个项目如何顺利迁移,没有在代码层面进行完整准确的拆解和构建,是无法运行的,一个小小的失误就会导致整个项目抛出错误。而且,在二方包里用sourcemap也没用。主包建好后,很难找出问题出在哪里,只好重做。。。在一开始的实时过程中,花了很多时间。子包的修改和调研,主应用包的构建等验证周期很长,这就是探索性改造的难点。那么能否在更小的粒度上进行验证和迁移呢?远程组件的加载给出了灵感。尝试将组件包@SREWorks/widgets打包成esm格式,直接修改原大而全项目中的node_modlues导入依赖包包文件,以及对应的加载机制,验证项目上的各个子依赖包是否可以运行,配合sourcemap,排错瞬间提速。这样依次进行@SREWorks/framework等其他包的验证,解决了蒙眼构建排错问题。样式问题挺头疼的。这里采用的解决方案是在主包中保留通用样式,子包样式重置覆盖的场景较少,所以采用css-module方式进行隔离构建。构建优化在对原项目各个子依赖包进行顺利验证后,在构建主应用包时,构建体积达到了惊人的5.5M,这还是gzip压缩后的体积:通过分析这张图,这个版本build存在以下问题:多次出现同名依赖,每个子依赖包中存在重复依赖。部分依赖包构建体积过大。比如BizCharts针对上述问题,从三个维度对@sreworks/app进行了整体优化:第一,优化为2.8Mconstnamespace={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'),'支撑':path.resolve(process.cwd(),'node_modules','支撑'),'lodash':path.resolve(process.cwd(),'node_modules','lodash')}...resolve:{别名:paths.namespace,模块:['node_modules'],扩展名:['.json','.js','.jsx','.less','scss'],},二、提取一些大的依赖包到cdn,如下externals配置项中剥离;体积优化为1.6M。但是考虑到一些专用云的使用场景,无法使用外部CDN。因此,使用自定义构建脚本将目标依赖从node_modules迁移到output文件夹并加载到html中,减少了构建过程中涉及的大依赖包数量,同时保证了专用云环境的正常使用。externals:{//剥离一些依赖,不参与打包'react':'React','react-dom':'ReactDOM',"antd":"antd",...},第三,调整key组件路径和1.48M,体积减少70%,构建时间从V1.3版本的74秒优化到23秒,提升68%。组件可扩展、可插拔尽管前端内置了运维场景常用的基础组件,如图表组件、落地组件、布局组件等50多个组件。根据开源后的用户反馈,用户对于定制化和扩展性的需求还是有共同的:大体上可以分为两类:前端框架也是React,有自己定制化的使用场景,内置-incomponents不能满足当前需求,需要扩展前端技术栈。Vue有很多历史组件,重构所有React的成本太高了。针对问题1,前端已经提供了JSXRender,支持用户使用JSXExtended自定义简单的静态渲染组件,但不支持属性配置、数据源、动态业务逻辑处理等高级特性。前端插件,很容易想到npm包的引入,但这只能适配在工程代码开发的场景中,要使用和移除runtime,就需要另辟蹊径了。再往前追溯,前端开发从jQuery时代发展到今天的Agular、React、Vue三驾马车以及各种工程构建工具的参与,但本质并没有变,依然是挂载在htmljs中的script标签中渲染和加载代码。因此,很自然地想到以script标签的形式加载我们的远程组件,但是这里我们需要实现动态的、批量加载的、可移除的,即:将远程组件打包成umd格式发布到云端,并得到相应的准确路径。以标签的形式导入:(function(){letscript=document.createElement('script')script.type='text/javascript'script.src=url//目标组件urldocument.getElementById('targetDomId').appendChild(script)})()script.addEventListener('load',callback,false)//如果要批量加载多个内嵌逻辑,即:constloadRemoteComp=async()=>{letremoteCompList=["url_a","url_b","url_c",...];尝试{remoteComList.forEach(item=>{pros.push(Promise.resolve(loadSingleComp());})window['REMOTE_COMP_LIST']=awaitPromise.all(pros);}catch(error){console.log(error);}}loadRemoteComp()当然这里还涉及到浏览器端适配、容错等细节,这里前端使用了比较成熟的systemjs包来加载组件,以上细节都处理得当,合理利用资源,节省时间和效率,对于问题二,技术栈不同,即异构组件的加载,目前frontend只是对Vue组件进行异构兼容渲染,在React中使用Vue组件。一开始我想到了使用转换工具,将Vue组件手动转换为React组件,然后粘贴构建,但是这种方式有一个很大的缺陷:不同版本的API差异较大,一般需要手动转码需要转换。人工对代码进行二次检查和调整,需要开发者熟悉新旧两个框架的特性。二次确认不合规代码或更新钩子,无形中会提高使用门槛。受到Docker容器的启发,认为React和Vue虽然属于不同的技术栈体系,但是不同于Java和Golang的区别。Vue和React本质上都是对原生js对象的封装,所以理论上来说,在React中可以使用它们对Vue组件进行容器化渲染:本质是绑定挂载Vue对象的操作:createVueInstance(targetElement,reactThisBinding){const{component,on,...props}=reactThisBinding.propsreactThisBinding.vueInstance=newVue({el:targetElement,data:props,...config.vueInstanceOptions,render(createElement){returncreateElement(VUE_COMPONENT_NAME,{props:this.$data,on,},[wrapReactChildren(createElement,this.children)])},components:{[VUE_COMPONENT_NAME]:component,'vuera-internal-react-wrapper':ReactWrapper,},})}使用前端远程componentscaffold@sreworks/widget-cli将React组件和Vue组件打包发布到cdn,然后在素材开发部,可以通过编辑方便的加载和移除远程组件runtime,解决了第一和第二个问题,并实现了“可扩展性”和“可插拔性”的目标。演化总结到此,基本解决了开篇所列的一系列问题,为前端运维组件生态的建设奠定了共建路径。可以做到:从@sreworks/widgets封装开发,JSXRender自定义组件,使用@sreworks/Widget-cli三个维度开发远程组件,扩展组件应用丰富度,结构依赖更清晰,降低学习门槛和贡献这套低代码项目。更小粒度的更新单元,更短的构建时间,更方便的日常协作。开发分包拆分后,为后续小规模逐步引入TS提供条件版本动态。我们会根据工作项目的节奏不断完善、优化和升级功能。目前主要是前端低代码功能??的输出,后续API低代码编辑已纳入版本规划,覆盖全链路低代码使用。如果大家有更好的建议,欢迎大家多提issue。同时也欢迎更多的开发者参与我们的生态建设(@小友助,也可以直接前端@地谦)SREWorks开源地址:https://github.com/alibaba/sr...
