体验微前端架构 — qiankun

微前端的核心目标是将巨石应用拆解成若干可以自治的松耦合微应用,而 qiankun 的诸多设计均是秉持这一原则,如 HTML entry、沙箱、应用间通信等。这样才能确保微应用真正具备 独立开发、独立运行 的能力。

一:微前端架构的核心价值

  • 技术栈无关
  • 独立开发、独立部署
  • 增量升级
  • 独立运行时

二:快速上手

我们分为主应用跟微应用。之前的想法是想直接通过 vue-cli 脚手架生成两个项目的基本配置。但做到一半的时候,想用 webpack5 的模块联邦,vue-cli 配起来太费劲(一直有各种报错,猜测应该是 vue-cli4.x 里面的 webpack 版本是4.x,跟 webpack5 有的地方不兼容)。干脆直接使用 webpack5 自己搭建一套配置。

webpack 的配置

先用 webpack5 来搭建一个基本的配置让项目运行起来。

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const {VueLoaderPlugin} = require('vue-loader/dist/index');
const {CleanWebpackPlugin} = require('clean-webpack-plugin');

module.exports = {
  mode: 'development',
  entry: path.resolve(__dirname, './src/main.js'),
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'js/[name].js'
  },
  module: {
    rules: [
      {
        test: /.vue$/,
        use: [
          'vue-loader'
        ]
      },
      {
        test: /.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      },
      {
        test: /.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader'
      }
    ]
  },
  devServer: {
    // webpack4 的 contentBase 改为下面
    static: {
      directory: path.join(__dirname, './dist')
    },
    port: 8080
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, './index.html'),
      filename: 'index.html',
      title: 'qiankun-root-app'
    }),
    new VueLoaderPlugin(),
    new CleanWebpackPlugin()
  ]
};
复制代码

qiankun 配置

主应用:

  1. 安装 qiankun

yarn add qiankun # 或者 npm i qiankun -S

  1. 在主应用中注册微应用并启动
// main.js
import {registerMicroApps, start} from 'qiankun';

registerMicroApps([
  {
    // 微应用的名称
    name: 'vue-app', 
    // 微应用的入口
    entry: 'http://localhost:8081/',
    // 微应用的容器节点的选择器
    container: '#vue-app',
    // 微应用的激活规则
    activeRule: '/vue'
  }
]);

start();
复制代码

在主应用中要有一个匹配激活规则的路由以及节点;

<template>
    <div>
        <router-link to="/">首页</router-link>
        <router-link to="/vue">vue应用</router-link>
    </div>
    <router-view></router-view>
    <div id="vue-app"></div>
</template>
复制代码

子应用:

  1. 导出相应的生命周期钩子(bootstrapmountunmount
// main.js
export async function bootstrap() {
  console.log('[vue] vue app bootstraped');
}

export async function mount(props) {
  render(props);
}

export async function unmount() {
  instance.unmount();
  instance._container.innerHTML = '';
  instance = null;
  router = null;
  history.destroy();
}
复制代码
  1. 配置微应用的打包工具
// webpack.config.js
const packageName = require('./package.json').name;

module.exports = {
  output: {
    library: `${packageName}`,
    libraryTarget: 'umd',
    // webpack5 jsonpFunction 改成了 chunkLoadingGlobal
    chunkLoadingGlobal: `webpackJsonp_${packageName}`,
  },
};
复制代码

三:常见问题

1. 路由

  • (1):主应用和微应用最好都是 history(createWebHistory) 模式;

主应用:

// router/index.js
import {createRouter, createWebHistory} from 'vue-router';

const Home = () => import('../src/components/Home.vue');
const routes = [
  {path: '/', component: Home}
];


const router = createRouter({
  history: createWebHistory('/'),
  routes
});

export default router;
复制代码

微应用:

路由文件导出 routes,在入口文件实例化路由。

// main.js
import {createRouter, createWebHistory} from 'vue-router';
import routes from '../router/index'

let instance = null;
let router = null;
let history = null;

function render(props = {}) {
  const {container} = props;
  // histroy模式的路由需要设置base
  history = createWebHistory(window.__POWERED_BY_QIANKUN__ ? '/vue' : '/');
  router = createRouter({
    history,
    routes
  });
  
  instance = createApp(App);
  instance.use(router);
  instance.mount(container ? container.querySelector('#app1') : '#app1');
}
复制代码
  • (2):跨域问题

1.png

子应用需要配置允许跨域:

// webpack.config.js
devServer: {
  headers: {
    'Access-Control-Allow-Origin': '*',
  }
},
复制代码

2. 复用公用依赖库

使用 webpackexternals 实现。

主应用:

// webpack.config.js
externals: {
  'vue': 'Vue',
  'element-plus': 'ElementPlus'
}
复制代码
// index.html
<link rel="stylesheet" href="//unpkg.com/element-plus/dist/index.css">
<script src="//unpkg.com/vue@next"></script>
<script src="//unpkg.com/element-plus"></script>
复制代码

微应用: index.html 里面的地址要添加 ignore,但是添加了 ignore 之后报错;

WechatIMG1032.png

目前没找到解决方式;貌似是因为 vue-router4 有问题;

// webpack.config.js
externals: {
  'vue': 'Vue',
  'element-plus': 'ElementPlus'
}
复制代码
// index.html
<link ignore rel="stylesheet" href="//unpkg.com/element-plus/dist/index.css">
<script ignore src="//unpkg.com/vue@next"></script>
<script ignore src="//unpkg.com/element-plus"></script>
复制代码
// main.js
import ElementPlus from 'element-plus';
instance.use(ElementPlus);
复制代码

3. 复用公共组件跟方法

  • (1):注册微应用的时候直接使用 props 属性传过去;

主应用:

// main.js
import HeaderComponent from '../src/common/header.vue'
import FooterComponent from '../src/common/footer.vue'
window.commonComponents = {
  HeaderComponent,
  FooterComponent
};
let commonComponents = window.commonComponents || {};
registerMicroApps([
  {
    ...
    props: {
      data: {
        commonComponents
      }
    }
  }
]);

// 共享组件必须开启多实例
start({ singular: false })
复制代码

微应用:

// main.js
export async function mount(props) {
  window.commonComponents = props.data.commonComponents;
  render(props);
}
复制代码
// Home.vue
components: {
  HeaderComponent: window.__POWERED_BY_QIANKUN__ ? window.commonComponents.HeaderComponent : '',
  FooterComponent: window.__POWERED_BY_QIANKUN__ ? window.commonComponents.FooterComponent : ''
}
复制代码
  • (2):使用 webpack5 模块联邦

主应用: 把公用的组件跟方法放入一个独立的js并导出;

// commonExport.js
import HeaderComponent from './src/common/header.vue'
import FooterComponent from './src/common/footer.vue'

import utils from './src/utils/index.js'

export {
  HeaderComponent,
  FooterComponent,
  utils
}
复制代码
// webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;

new ModuleFederationPlugin({
  name: 'root_app',
  filename: "root_app.js",
  exposes: {
    './commonExport': './commonExport'
  }
})
复制代码

微应用:

// webpack.config.js
plugins: [
  new ModuleFederationPlugin({
    name: 'vue_app',
    filename: "vue_app.js",
    remotes: {
      rootApp: 'root_app@http://localhost:8080/root_app.js'
    }
  })
]
复制代码
// Home.vue
import {onMounted} from 'vue';
// 使用公用组件跟方法
import {HeaderComponent, utils} from 'rootApp/commonExport'

export default {
  name: 'Home',
  components: {
    HeaderComponent
  },
  setup() {
    onMounted(() => {
      utils.commonFn();
    })
  }
}
复制代码

4. 微应用加载的资源会 404

微应用新建 public-path.js,在 main.js 中导入;

if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
复制代码
import './public-path';
复制代码

5. 微应用如何独立运行?

// main.js
if (!window.__POWERED_BY_QIANKUN__) {
  render()
}
复制代码

6. 微应用路径下刷新后 404

通常是因为你使用的是 browser 模式的路由,这种路由模式的开启需要服务端配合才行。开发环境需要修改 devServer

// webpack.config.js
devServer: {
  // 解决微应用刷新报错404
  proxy: {
    '/vue/': {
      target: 'http://localhost:8080',
      changeOrigin: true,
      pathRewrite: {
        '^/vue': ''
      }
    }
  },
  historyApiFallback: true
},
复制代码

猜你喜欢

转载自juejin.im/post/7054088602459865119