微前端技术选型

4,576 阅读7分钟

本文正在参加「金石计划」

前言

工欲善其事必先利其器;想要在项目中推行微前端,先要进行技术选型,需要进行细致的调研;但是如果没有人告诉你有这些微前端解决方案,你用百度搜索恐怕一个星期也难以有一个结果;所以我来了,来帮你们梳理一下市面上的微前端解决方案;

Systemjs

Systemjs是一个模块加载方案,利用它我们就可以实现一个简单的微前端,没想到吧,哈哈!

下面开始操作:我们用一个html文件作为主应用,然后vue项目作为子应用;

我们的主应用有以下任务:

  1. 公共依赖以CDN导入
  2. 整个应用的路由管理
  3. Vue组件的挂载

我们的子应用就是每一个路由组件,其他什么也不用做,是不是很熟悉?我们只是把这些路由组件拆分成了子应用独立出去了

截屏2023-03-11 19.47.59.png

要使用Systemjs加载模块的话需要配置WebpackSystemRegister插件:

const { VueLoaderPlugin } = require("vue-loader");
const WebpackSystemRegister = require("webpack-system-register");
module.exports = {
  entry: {
    buylist: "./src/BuyList.vue",
  },
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: ["vue-style-loader", "css-loader"],
      },
      {
        test: /\.vue$/i,
        use: "vue-loader",
      },
    ],
  },
  plugins: [new VueLoaderPlugin(), new WebpackSystemRegister({})],
};

我们的BuyList组件:

<template>
   <div>BuyList</div>
</template>

<script>
export default {
   name: 'BuyList',
}
</script>

这种方式实现下来很简单,但是我们需要梳理一下开发流程:我们先开发子应用BuyList,此时需要单独为子应用起一个服务web-dev-server(这里我没有写,可以自行实现),开发完成之后打包将dist包上传到CDN,然后主项目去加载,这个时候我们还需要发布主项目然后查看效果;它最大的问题就是不能做到跨技术栈;我们继续探索其他的解决方案;

UMD

这是个一个国外大佬的微前端demo:github.com/micro-front…

除了infra我们不用管,先把公共依赖项目content run起来,然后把其他子项目服务起起来,最后起container主项目,我们主要看一下它是怎么实现的微前端,先看一下App组件这是应用入口,我们看到还是通过路由去分发各个子应用

const App = () => (
  <BrowserRouter>
    <React.Fragment>
      <AppHeader />
      <Switch>
        <Route exact path="/" component={Browse} />
        <Route exact path="/restaurant/:id" component={Restaurant} />
        <Route exact path="/random" render={Random} />
        <Route exact path="/about" render={About} />
      </Switch>
    </React.Fragment>
  </BrowserRouter>
);

然后我们看一看MicroFrontend的实现,它的总体流程是:

  1. fetch获取对应路由组件下的打包文件列表manifest,然后动态将打包出的js文件(统一都是main.js)添加到DOM
  2. 当脚本加载完毕后执行子应用的render方法
  3. 如果下一次发现子应用脚本已经加载完毕,那么直接调用子应用render方法

子应用不直接渲染这个App,而是将render方法挂载到window对象上:

window.renderRestaurant = (containerId, history) => {
  ReactDOM.render(
    <App history={history} />,
    document.getElementById(containerId),
  );
  unregister();
};

window.unmountRestaurant = containerId => {
  ReactDOM.unmountComponentAtNode(document.getElementById(containerId));
};

部署时我们需要将asset-manifest.json部署到CDN上以便主应用访问,其他子应用只需要单独发包上传到CDN就可以了;这种方式类似于打成UMD包,让子应用全局可访问;这种方式也不能支持跨技术栈;

有大佬说过:不跨技术栈的微前端不算是好的微前端;

因此上面这两种简单的方案并不能根本地解决我们目前的痛点;但是有一个神器可以不用吹灰之力就解决跨技术栈的问题,有请iframe闪亮登场;

iframe

还记得前后端不分离的时候,后端写页面就会用到很多iframe去切页面,这样还可以公用菜单,很方便;但是随着用户要求变高,这种方案逐渐废弃了,因为它的加载速度实在太慢了,与当前这个快餐时代明显不符;

但是开发者却很青睐iframe,因为它太好用了,一个src属性,什么都搞定了,一个页面可以内嵌无数个页面,nice!后来iframe成为所有微前端方案的终极目标:

像iframe一样使用微前端(没有接入成本),体验上超过iframe;

再参考一下上面两种方案,他们其实都利用了单页的路由,那么这就是一个优化方向,能不能把多页的切换优化为单页切换,这就是single-spa

single-spa

single-spa优化了应用切换时的体验,同时也完善了子应用加载时的生命周期管理,为后面的微前端发展奠定了基础

但是还是无法完成我们的终极使命:跨技术栈、跨版本;于是出现了乾坤,它是基于single-spa开发的又一个微前端框架;

同时,跨技术栈、跨版本还有另外一个思路,那就是web-components,因为它与技术栈无关,天然地跨技术栈,能不能把我们的组件都转化为web-components去渲染呢?这当然是个好主意;对应的也有鹅厂的无界

qiankun

qinakun的运行分为三部曲:

  1. 注册子应用
 {
   name:'sub-vue',
   entry:'//localhost:8080',
   container:"#app1",
   activeRule:'/vue',
 },
 {
   name:'sub-vue2',
   entry:'//localhost:8081',
   container:"#app2",
   activeRule:'/vue2'
 }
])
  1. 设置默认进入的应用setDefaultMountApp('/vue');
  2. 启动应用start();

子应用只需要暴露一些生命周期函数就行了:


export async function bootstrap() {
  console.log('react app bootstraped');
}

export async function mount(props) {
  ReactDOM.render(<App />, props.container ? props.container.querySelector('#root') : document.getElementById('root'));
}

export async function unmount(props) {
  ReactDOM.unmountComponentAtNode(
    props.container ? props.container.querySelector('#root') : document.getElementById('root'),
  );
}

下面我们尝试将上面这个React微前端改造为qiankun:

  1. 先改造子应用,暴露生命周期函数,改造webpack配置,这里我们需要将create-react-app改为craco
  2. 先执行主应用的render,然后在组件中预留一个id=container的子应用容器
  3. 注册子应用,设置默认应用,启动应用

下面我们来看看改造成本:右边是源代码,左边是改造后代码

主应用index.js的对比如下: 截屏2023-03-11 15.42.49.png 主应用App.js由于不需要路由了,改造点很多:

截屏2023-03-11 15.47.28.png 子应用改动则比较大:首先是index.js把之前的代码全部废弃,暴露子应用生命周期函数,并且如果是开发环境需要render一下

截屏2023-03-11 15.49.00.png 然后脚手架需要修改为craco,并配置craco.config.js:

const packageName = require('./package.json').name;

module.exports = {
  webpack: {
    configure: webpackConfig => {
      webpackConfig.output = Object.assign({}, webpackConfig.output, {
        library: `${packageName}-[name]`,
        libraryTarget: 'umd',
        jsonpFunction: `webpackJsonp_${packageName}`,
      });

      return webpackConfig;
    },
  },
};

还需要配置一个public-path:

if (window.__POWERED_BY_QIANKUN__) {
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

wujie

正如无界官网所说,他是一个极致的微前端框架,那么我们接下来用它改造我们之前那个项目:

我们主应用是React,直接引入WujieReact组件,然后render一下:

ReactDOM.render(
  <>
    <App />
    <WujieReact
      width="100%"
      height="100%"
      name="browser"
      url={'http://localhost:3001/'}
    />
  </>,
  document.getElementById('root'),
);

删除掉主应用的路由,子应用无需改造,这样就直接完成了,主应用+browser子应用,成本很低,的确如官网所说,但是我们的子应用样式丢失了

icestark

飞冰也是一种微前端解决方案,我们也试试它;

主应用改造:使用飞冰提供的路由代替原来的路由,每个路由对应一个子应用;这里一个关键问题是需要把所有子应用打包得到的css、js文件放到url中

ReactDOM.render(
  <>
    <App />
    <AppRouter>
      <AppRoute
        activePath="/browser"
        title="browse"
        url={[
          '//localhost:3001/static/js/bundle.js',
          '//localhost:3001/static/js/0.chunk.js',
          '//localhost:3001/static/js/main.chunk.js'
        ]}
      />
    </AppRouter>
  </>,
  document.getElementById('root'),
);

子应用改造:增加几个函数导出生命周期

import { isInIcestark, setLibraryName } from '@ice/stark-app';

export function mount(props) {
  ReactDOM.render(<App/>, props.container);
}

export function unmount(props) {
  ReactDOM.unmountComponentAtNode(props.container);
}

// 注意:`setLibraryName` 的入参需要与 webpack 工程配置的 output.library 保持一致
setLibraryName('microApp');

if (!isInIcestark()) {
  ReactDOM.render(<App />, document.getElementById('container'));
}

子应用也需要修改webpack配置:

module.exports = {
  webpack: {
    configure: webpackConfig => {
      webpackConfig.output = Object.assign({}, webpackConfig.output, {
        // 设置模块导出规范为 umd
        libraryTarget: 'umd',
        // 可选,设置模块在 window 上暴露的名称
        library: 'microApp',
      });
      console.log("webpackConfig",webpackConfig)

      return webpackConfig;
    },
  },
};

综上,我们总结一下:

改造成本:iframe < wujie < qiankun = icestark

Star数量:qinkun14.1K > wujie2.2K > icestark1.9K

副作用: icestark = qiankun < wujie(样式没了)

当然本文主要聚焦于各个微前端框架的改造成本,可能对比不是那么全面,大家可以更深入地去对比;

后记

上面我们介绍了Systemjs、UMDiframeqiankunwujieicestark这六种应用级微前端解决方案,当然还有模块级微前端Module Federation,单独使用它无法构建应用;还有EMP微前端方案,它的文档不全,不好去写demo,大家可以自行尝试;

希望能够给大家的技术选型带来帮助;但是微前端是不断发展的,目前没有任何一个微前端能够在性能上超越iframe而且0配置,所以“革命尚未成功,同志仍需努力”;

附录

本文demo Git地址

Systemjs GitHub地址

Qiankun Github地址

wujie Github地址

icestark Github地址

Module Federation Github地址

EMP Github地址