乐趣区

关于前端:微前端架构的几种技术选型

微前端架构的几种技术选型随着 SPA 大规模的利用,紧接着就带来一个新问题:一个规模化利用须要拆分。一方面性能疾速减少导致打包工夫成比例回升,而紧急公布时要求是越短越好,这是矛盾的。另一方面当一个代码库集成了所有性能时,日常合作相对是十分艰难的。而且最近十多年,前端技术的倒退是十分快的,每隔两年就是一个时代,导致同志们必须降级我的项目甚至于换一个框架。但如果大家想在一个规模化利用中一个版本做好这件事,基本上是不可能的。最早的解决方案是采纳 iframe 的办法,依据性能次要模块拆分规模化利用,子利用之间应用跳转。但这个计划最大问题是导致页面从新加载和白屏。那有什么好的解决方案呢?微前端这样具备跨利用的解决方案在此背景下应运而生了!微前端的概念微前端是什么:微前端是一种相似于微服务的架构,是一种由独立交付的多个前端利用组成整体的架构格调,将前端利用分解成一些更小、更简略的可能独立开发、测试、部署的利用,而在用户看来依然是内聚的单个产品。有一个基座利用(主利用),来治理各个子利用的加载和卸载。

f135ab0912746bd6.png 所以微前端不是指具体的库,不是指具体的框架,不是指具体的工具,而是一种现实与架构模式。微前端的外围三大准则就是:独立运行、独立部署、独立开发微前端的劣势采纳微前端架构的益处就是,将这些小型利用交融为一个残缺的利用,或者将本来运行已久、没有关联的几个利用交融为一个利用能够将多个我的项目交融为一,又能够缩小我的项目之间的耦合,晋升我的项目扩展性。实现微前端的几种形式· 从 single-spa 到 qiankun· 基于 WebComponent 的 micro-app· webpack5 实现的 Module Federation 微前端框架的分类 Single-spasingle-spa 是一个很好的微前端根底框架,而 qiankun 框架就是基于 single-spa 来实现的,在 single-spa 的根底上做了一层封装,也解决了 single-spa 的一些缺点。首先咱们先来理解该如何应用 single-spa 来实现微前端的搭建。

single-spa.jpgSingle-spa 实现原理首先在基座利用中注册所有 App 的路由,single-spa 保留各子利用的路由映射关系,充当微前端控制器 Controler,。URL 响应时,匹配子利用路由并加载渲染子利用。上图便是对 single-spa 残缺的形容。有了实践根底,接下来,咱们来看看代码层面时如何应用的。以下以 Vue 工程为例基座构建 single-spa, 在 Vue 工程入口文件 main.js 实现基座的配置。基座配置 //main.jsimport Vue from ‘vue’import App from ‘./App.vue’import router from ‘./router’import {registerApplication, start} from ‘single-spa’Vue.config.productionTip = falseconst mountApp = (url) => {return new Promise((resolve, reject) => {const script = document.createElement(‘script’)    script.src = url    script.onload = resolve    script.onerror = reject    // 通过插入 script 标签的形式挂载子利用    const firstScript = document.getElementsByTagName(‘script’)[0]    // 挂载子利用    firstScript.parentNode.insertBefore(script, firstScript)  })}const loadApp = (appRouter, appName) => {// 近程加载子利用  return async () => {// 手动挂载子利用    await mountApp(appRouter + ‘/js/chunk-vendors.js’)    await mountApp(appRouter + ‘/js/app.js’)    // 获取子利用生命周期函数    return window[appName]  }}// 子利用列表 const appList = [{    // 子利用名称    name: ‘app1’,    // 挂载子利用    app: loadApp(‘http://localhost:8083’, ‘app1’),    // 匹配该子路由的条件    activeWhen: location => location.pathname.startsWith(‘/app1’),    // 传递给子利用的对象    customProps: {}},  {name: ‘app2’,    app: loadApp(‘http://localhost:8082’, ‘app2’),    activeWhen: location => location.pathname.startsWith(‘/app2’),    customProps: {}}]// 注册子利用 appList.map(item => {  registerApplication(item)}) // 注册路由并启动基座 new Vue({router,  mounted() {start()  },  render: h => h(App)}).$mount(‘#app’)复制代码构建基座的外围是:配置子利用信息,通过 registerApplication 注册子利用,在基座工程挂载阶段 start 启动基座。子利用配置 import Vue from ‘vue’import App from ‘./App.vue’import router from ‘./router’import singleSpaVue from ‘single-spa-vue’Vue.config.productionTip = falseconst appOptions = {el: ‘#microApp’,  router,  render: h => h(App)}// 反对利用独立运行、部署,不依赖于基座利用 // 如果不是微应用环境,即启动本身挂载的形式 if (!process.env.isMicro) {delete appOptions.el  new Vue(appOptions).$mount(‘#app’)}// 基于基座利用,导出生命周期函数 const appLifecycle = singleSpaVue({Vue,  appOptions})// 抛出子利用生命周期 // 启动生命周期函数 export const bootstrap = (props)  => {console.log(‘app2 bootstrap’)  return appLifecycle.bootstrap(() => {})}// 挂载生命周期函数 export const mount = (props) => {console.log(‘app2 mount’)  return appLifecycle.mount(() => {})}// 卸载生命周期函数 export const unmount = (props) => {console.log(‘app2 unmount’)  return appLifecycle.unmount(() => {})}复制代码配置子利用为 umd 打包形式 //vue.config.jsconst package = require(‘./package.json’)module.exports = {// 通知子利用在这个地址加载动态资源,否则会去基座利用的域名下加载  publicPath: ‘//localhost:8082’,  // 开发服务器  devServer: {    port: 8082},  configureWebpack: {// 导出 umd 格局的包,在全局对象上挂载属性 package.name,基座利用须要通过这个    // 全局对象获取一些信息,比方子利用导出的生命周期函数    output: {      // library 的值在所有子利用中须要惟一      library: package.name,      libraryTarget: ‘umd’}  }复制代码配置子利用环境变量 // .env.micro NODE_ENV=developmentVUE_APP_BASE_URL=/app2isMicro=true 复制代码子利用配置的外围是用 singleSpaVue 生成子路由配置后,必须要抛出其生命周期函数。用以上形式便可轻松实现一个简略的微前端利用了。那么咱们有 single-spa 这种微前端解决方案,为什么还须要 qiankun 呢?相比于 single-spa,qiankun 他解决了 JS 沙盒环境,不须要咱们本人去进行解决。在 single-spa 的开发过程中,咱们须要本人手动的去写调用子利用 JS 的办法(如下面的 createScript 办法),而 qiankun 不须要,乾坤只须要你传入响应的 apps 的配置即可,会帮忙咱们去加载。QiankunQiankun 的劣势· 基于 single-spa[1] 封装,提供了更加开箱即用的 API。· · 技术栈无关,任意技术栈的利用均可 应用 / 接入,不论是 React/Vue/Angular/JQuery 还是其余等框架。· · HTML Entry 接入形式,让你接入微利用像应用 iframe 一样简略。· · 款式隔离,确保微利用之间款式相互不烦扰。· · JS 沙箱,确保微利用之间 全局变量 / 事件 不抵触。· · 资源预加载,在浏览器闲暇工夫预加载未关上的微利用资源,减速微利用关上速度。· 基座配置 import {registerMicroApps, start} from ‘qiankun’;registerMicroApps([{    name: ‘reactApp’,    entry: ‘//localhost:3000’,    container: ‘#container’,    activeRule: ‘/app-react’,},  {name: ‘vueApp’,    entry: ‘//localhost:8080’,    container: ‘#container’,    activeRule: ‘/app-vue’,},  {name: ‘angularApp’,    entry: ‘//localhost:4200’,    container: ‘#container’,    activeRule: ‘/app-angular’,},]);// 启动 qiankunstart(); 复制代码子利用配置以 create react app 生成的 react 16 我的项目为例,搭配 react-router-dom 5.x。1. 在 src 目录新增 public-path.js,解决子利用挂载时,拜访动态资源抵触  if (window.__POWERED_BY_QIANKUN__) {__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;}复制代码 2. 设置 history 模式路由的 base:<BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? ‘/app-react’ : ‘/’}> 复制代码 3. 入口文件 index.js 批改,为了防止根 id #root 与其余的 DOM 抵触,须要限度查找范畴。import ‘./public-path’;  import React from ‘react’;  import ReactDOM from ‘react-dom’;  import App from ‘./App’;  function render(props) {const { container} = props;    ReactDOM.render(<App />, container ? container.querySelector(‘#root’) :     document.querySelector(‘#root’));  }  if (!window.__POWERED_BY_QIANKUN__) {render({});  }  export async function bootstrap() {    console.log(‘[react16] react app bootstraped’);  }  export async function mount(props) {console.log(‘[react16] props from main framework’, props);    render(props);  }  export async function unmount(props) {const { container} = props;    ReactDOM.unmountComponentAtNode(container ? container.querySelector(‘#root’) :      document.querySelector(‘#root’));  }复制代码 4. 批改 webpack 配置装置插件 @rescripts/cli,当然也能够抉择其余的插件,例如 react-app-rewired。npm i -D @rescripts/cli 复制代码根目录新增 .rescriptsrc.js:const {name} = require(‘./package’);module.exports = {webpack: (config) => {config.output.library = ${name}-[name];    config.output.libraryTarget = ‘umd’;    config.output.jsonpFunction = webpackJsonp_${name};    config.output.globalObject = ‘window’;    return config;  },  devServer: (_) => {const config = _;    config.headers = {      ‘Access-Control-Allow-Origin’: ‘*’,};    config.historyApiFallback = true;    config.hot = false;    config.watchContentBase = false;    config.liveReload = false;    return config;  },}; 复制代码以上对 Qiankun 的应用能够看出,与 single-spa 应用过程很类似。不同的是,Qiankun 的应用过程更简便了。一些内置的操作交由给 Qiankun 外部实现。这是一种 IOC 思维的实现,咱们只管面向容器化开发,其余操作交给 Qiankun 框架治理。Micro-appmicro-app 并没有因循 single-spa 的思路,而是借鉴了 WebComponent 的思维,通过 CustomElement 联合自定义的 ShadowDom,将微前端封装成一个类 WebComponent 组件,从而实现微前端的组件化渲染。并且因为自定义 ShadowDom 的隔离个性,micro-app 不须要像 single-spa 和 qiankun 一样要求子利用批改渲染逻辑并暴露出办法,也不须要批改 webpack 配置,是目前市面上接入微前端老本最低的计划。WebComponent 的概念 WebComponent[2] 是 HTML5 提供的一套自定义元素的接口,WebComponent[3]是一套不同的技术,容许您创立可重用的定制元素(它们的性能封装在您的代码之外)并且在您的 web 利用中应用它们。以上是 MDN 社区对 WebComponent 的解释。· Custom elements(自定义元素):一组 JavaScript API,容许您定义 custom elements 及其行为,而后能够在您的用户界面中依照须要应用它们。· Shadow DOM(影子 DOM):一组 JavaScript API,用于将封装的“影子”DOM 树附加到元素(与主文档 DOM 离开出现)并管制其关联的性能。通过这种形式,您能够放弃元素的性能公有,这样它们就能够被脚本化和款式化,而不必放心与文档的其余局部发生冲突。· HTML templates(HTML 模板):<template> 和 <slot> 元素使您能够编写不在出现页面中显示的标记模板。而后它们能够作为自定义元素构造的根底被屡次重用。接下来用一个小例子更快来了解 WebComponent 的概念。一个存在组件内交互的 WebComponent// 基于 HTMLElement 自定义组件元素 class CounterElement extends HTMLElement {// 在结构器中生成 shadow 节点  constructor() {super();    this.counter = 0;    // 关上影子节点    // 影子节点是为了隔离内部元素的影响    const shadowRoot = this.attachShadow({mode: ‘open’});    // 定义组件内嵌款式    const styles =           #counter-increment {width: 60px;              height: 30px;              margin: 20px;              background: none;              border: 1px solid black;}      ;    // 定义组件 HTMl 构造    shadowRoot.innerHTML =           <style>${styles}</style>          <h3>Counter</h3>          <slot name='counter-content'>Button</slot>          <span id='counter-value'>; 0 </span>;          <button id='counter-increment'> + </button>      ;    // 获取 + 号按钮及数值内容    this.incrementButton = this.shadowRoot.querySelector(‘#counter-increment’);    this.counterValue = this.shadowRoot.querySelector(‘#counter-value’);    // 实现点击组件内事件驱动    this.incrementButton.addEventListener(“click”, this.decrement.bind(this));  }  increment() {    this.counter++    this.updateValue();  }  // 替换 counter 节点内容,达到更新数值的成果  updateValue() {    this.counterValue.innerHTML = this.counter;}}// 在实在 dom 上,生成自定义组件元素 customElements.define(‘counter-element’, CounterElement); 复制代码有了对 WebComponent 的了解,接下来,咱们更明确了 Micro-app 的劣势。micro-app 的劣势

d879637b4bb34253.png· 应用简略· 咱们将所有性能都封装到一个类 WebComponent 组件中,从而实现在基座利用中嵌入一行代码即可渲染一个微前端利用。· 同时 micro-app 还提供了 js 沙箱、款式隔离、元素隔离、预加载、数据通信、动态资源补全等一系列欠缺的性能。· · 零依赖· micro-app 没有任何依赖,这赋予它玲珑的体积和更高的扩展性。· · 兼容所有框架· 为了保障各个业务之间独立开发、独立部署的能力,micro-app 做了诸多兼容,在任何技术框架中都能够失常运行。· 基座的繁难配置基座存在预加载子利用、父子利用通信、公共文件共享等等 // index.jsimport React from “react”import ReactDOM from “react-dom”import App from ‘./App’import microApp from ‘@micro-zoe/micro-app’const appName = ‘my-app’// 预加载 microApp.preFetch([{ name: appName, url: ‘xxx’}])// 基座向子利用数据通信 microApp.setData(appName, { type: ‘ 新的数据 ’})// 获取指定子利用数据 const childData = microApp.getData(appName)microApp.start({// 公共文件共享  globalAssets: {    js: [‘js 地址 1 ’, ‘js 地址 2 ’, …], // js 地址    css: [‘css 地址 1 ’, ‘css 地址 2 ’, …], // css 地址  }})复制代码调配一个路由给子利用 // router.jsimport {BrowserRouter, Switch, Route} from ‘react-router-dom’export default function AppRoute () {  return (    <BrowserRouter>      <Switch>        <Route path=’/’>          <micro-app name=’app1′ url=’http://localhost:3000/’ baseroute=’/’></micro-app>        </Route>      </Switch>    </BrowserRouter>)}复制代码子利用的繁难配置 // index.jsimport React from “react”import ReactDOM from “react-dom”import App from ‘./App’import microApp from ‘@micro-zoe/micro-app’const appName = ‘my-app’// 子利用运行时,切换动态资源拜访门路 if (window.__MICRO_APP_ENVIRONMENT__) {__webpack_public_path__ = window.__MICRO_APP_PUBLIC_PATH__}// 基子利用向基座发送数据 // dispatch 只承受对象作为参数 window.microApp.dispatch({type: ‘ 子利用发送的数据 ’})// 获取基座数据 const data = window.microApp.getData() // 返回基座下发的 data 数据 // 性能优化,umd 模式 // 如果子利用渲染和卸载不频繁,那么应用默认模式即可,如果子利用渲染和卸载十分频繁倡议应用 umd 模式 // 将渲染操作放入 mount 函数 — 必填 export function mount() {ReactDOM.render(<App />, document.getElementById(“root”))}// 将卸载操作放入 unmount 函数 — 必填 export function unmount() {  ReactDOM.unmountComponentAtNode(document.getElementById(“root”))}// 微前端环境下,注册 mount 和 unmount 办法 if (window.__MICRO_APP_ENVIRONMENT__) {window[micro-app-${window.__MICRO_APP_NAME__}] = {mount, unmount}} else {// 非微前端环境间接渲染  mount()}复制代码设置子利用路由 import {BrowserRouter, Switch, Route} from ‘react-router-dom’export default function AppRoute () {  return (    // 设置根底路由,子利用能够通过 window.__MICRO_APP_BASE_ROUTE__获取基座下发的 baseroute,// 如果没有设置 baseroute 属性,则此值默认为空字符串    <BrowserRouter basename={window.__MICRO_APP_BASE_ROUTE__ || ‘/’}>      …    </BrowserRouter>  )}复制代码以上便是 Micro-app 的用法 Module FederationModule Federation 是 Webpack5 提出的概念,module federation 用来解决多个利用之间代码共享的问题,让咱们更加优雅的实现跨利用的代码共享。MF 想做的事和微前端想解决的问题是相似的,把一个利用进行拆分成多个利用,每个利用可独立开发,独立部署,一个利用能够动静加载并运行另一个利用的代码,并实现利用之间的依赖共享。为了实现这样的性能,MF 在设计上提出了这几个外围概念。Container 一个被 ModuleFederationPlugin 打包进去的模块被称为 Container。艰深点讲就是,如果咱们的一个利用应用了 ModuleFederationPlugin 构建,那么它就成为一个 Container,它能够加载其余的 Container,能够被其余的 Container 所加载。Host&Remote 从消费者和生产者的角度看 Container,Container 又可被称作 Host 或 Remote。Host:生产方,它动静加载并运行其余 Container 的代码。Remote:提供方,它裸露属性(如组件、办法等)供 Host 应用能够晓得,这里的 Host 和 Remote 是绝对的,因为 一个 Container 既能够作为 Host,也能够作为 Remote。Shared 一个 Container 能够 Shared 它的依赖(如 react、react-dom)给其余 Container 应用,也就是共享依赖。

微信图片_20220626184254.png

微信图片_20220626184305.png 以上是 webpack5 与之前版本的模块治理比照图微利用配置通过 webpack5 的配置达成微利用的成果 // 配置 webpack.config.jsconst {ModuleFederationPlugin} = require(“webpack”).container;new ModuleFederationPlugin({name: “appA”, // 进口文件  filename: “remoteEntry.js”, // 裸露可拜访的组件  exposes: {    “./input”: “./src/input”,}, // 或者其余模块的组件 // 如果把这一模块当作基座模块的话,// 这里应该配置其余子利用模块的入口文件  remotes: {appB: “appB@http://localhost:3002/remoteEntry.js”,}, // 共享依赖,其余模块不须要再次下载,便可应用  shared: [‘react’, ‘react-dom’],})复制代码以上便是我对微利用架构的了解,以及微利用架构技术的演变过程。不难看出,这些技术的演变都朝着易用性和可拓展性的方向演进。其中技术也有其时代的局限性,不过思维和技术总是在不断进步的。这几类技术选型都有其优缺点,各有千秋,咱们能够依据不同的须要抉择不同的技术来构建利用。下列是本文写作时的参考资料:single-spa: zh-hans.single-spa.js.org/docs/gettin…[4]qiankun: qiankun.umijs.org/zh/guide[5]WebComponent: developer.mozilla.org/zh-CN/docs/…[6]micro-app: cangdu.org/micro-app/d…[7]参考资料[1]https://github.com/CanopyTax/single-spa[2]https://developer.mozilla.org/zh-CN/docs/Web/Web_Components#%…[3]https://developer.mozilla.org/zh-CN/docs/Web/Web_Components#%…[4]https://zh-hans.single-spa.js.org/docs/getting-started-overview[5]https://qiankun.umijs.org/zh/guide[6]https://developer.mozilla.org/zh-CN/docs/Web/Web_Components[7]http://cangdu.org/micro-app/docs.html#/ 

退出移动版