• 微前端架构的几种技术选型


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


    随着SPA大规模的应用,紧接着就带来一个新问题:一个规模化应用需要拆分。

    一方面功能快速增加导致打包时间成比例上升,而紧急发布时要求是越短越好,这是矛盾的。另一方面当一个代码库集成了所有功能时,日常协作绝对是非常困难的。而且最近十多年,前端技术的发展是非常快的,每隔两年就是一个时代,导致同志们必须升级项目甚至于换一个框架。但如果大家想在一个规模化应用中一个版本做好这件事,基本上是不可能的。

    最早的解决方案是采用iframe的方法,根据功能主要模块拆分规模化应用,子应用之间使用跳转。但这个方案最大问题是导致页面重新加载和白屏。

    那有什么好的解决方案呢?微前端这样具有跨应用的解决方案在此背景下应运而生了!

    微前端的概念

    微前端是什么:微前端是一种类似于微服务的架构,是一种由独立交付的多个前端应用组成整体的架构风格,将前端应用分解成一些更小、更简单的能够独立开发、测试、部署的应用,而在用户看来仍然是内聚的单个产品。有一个基座应用(主应用),来管理各个子应用的加载和卸载。

    f135ab0912746bd6.png

    所以微前端不是指具体的库,不是指具体的框架,不是指具体的工具,而是一种理想与架构模式。

    微前端的核心三大原则就是:独立运行、独立部署、独立开发

    微前端的优势

    采用微前端架构的好处就是,将这些小型应用融合为一个完整的应用,或者将原本运行已久、没有关联的几个应用融合为一个应用可以将多个项目融合为一,又可以减少项目之间的耦合,提升项目扩展性。

    实现微前端的几种方式

    • single-spaqiankun
    • 基于WebComponent的micro-app
    • webpack5实现的Module Federation

    微前端框架的分类

    Single-spa

    single-spa是一个很好的微前端基础框架,而qiankun框架就是基于single-spa来实现的,在single-spa的基础上做了一层封装,也解决了single-spa的一些缺陷。

    首先我们先来了解该如何使用single-spa来完成微前端的搭建。

    single-spa.jpg

    Single-spa实现原理

    首先在基座应用中注册所有App的路由,single-spa保存各子应用的路由映射关系,充当微前端控制器Controler,。URL响应时,匹配子应用路由并加载渲染子应用。上图便是对single-spa完整的描述。

    有了理论基础,接下来,我们来看看代码层面时如何使用的。

    以下以Vue工程为例基座构建single-spa,在Vue工程入口文件main.js完成基座的配置。

    基座配置

    //main.js
    import Vue from 'vue'
    import App from './App.vue'
    import router from './router'
    import { registerApplication, start } from 'single-spa'

    Vue.config.productionTip = false

    const 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 = false

    const 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.js
    const 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=development
    VUE_APP_BASE_URL=/app2
    isMicro=true
    复制代码

    子应用配置的核心是用singleSpaVue生成子路由配置后,必须要抛出其生命周期函数

    用以上方式便可轻松实现一个简单的微前端应用了。

    那么我们有single-spa这种微前端解决方案,为什么还需要qiankun呢?

    相比于single-spaqiankun他解决了JS沙盒环境,不需要我们自己去进行处理。在single-spa的开发过程中,我们需要自己手动的去写调用子应用JS的方法(如上面的 createScript方法),而qiankun不需要,乾坤只需要你传入响应的apps的配置即可,会帮助我们去加载。

    Qiankun

    Qiankun的优势

    基于 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',
      },
    ]);
    // 启动 qiankun
    start();
    复制代码

    子应用配置

    以 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

      '/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(, 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-app

    micro-app并没有沿袭single-spa的思路,而是借鉴了WebComponent的思想,通过CustomElement结合自定义的ShadowDom,将微前端封装成一个类WebComponent组件,从而实现微前端的组件化渲染。并且由于自定义ShadowDom的隔离特性,micro-app不需要像single-spaqiankun一样要求子应用修改渲染逻辑并暴露出方法,也不需要修改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 模板):