从0开始创建一个qiankun微前端项目

1,980 阅读3分钟

1.qiankun介绍

qiankun 是一个基于 single-spa微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。
核心价值
技术栈无关-任意技术栈的应用均可使用/接入,不论是React/Vue/Angular/jQuery等等
HTML Entry接入方式,让你接入微应用像使用ifame一样简单
框架独立开发、独立部署-微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新
资源预加载-在浏览器空闲时间预加载未打开的微应用资源、加速微应用打开速度
增量升级-对于老项目没办法做到全量的技术栈升级或者重构,使用微前端实施渐进式的重构
功能完备 几乎包含所有构建微前端系统时所需要的基本能力。
样式隔离,确保微应用之间样式互相不干扰。
JS 沙箱,确保微应用之间 全局变量/事件 不冲突。
资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
umi 插件,提供了 @umijs/plugin-qiankun 供 umi 应用一键切换成微前端架构系统。
架构思想
中心化基座模式的微前端典型代表,由一个主应用和一系列业务子应用构成的系统,并由这个主应用来管理其他子应用,包括从子应用的生命周期管理到应用间的通信机制。

2. 技术栈选型

主应用使用vue-cli 创建vue2应用
微应用1依托于creata-react-app创建react
微应用2依托于vue-cli创建vue2

3. 创建主应用

首先安装vue-cli

npm i -g @vue/cli

使用vue-cli创建vue2主应用

vue create main

选择需要的包,由于此处微应用是基于路由创建出来的,所以此处注意要选择Router

image.png 安装好vue-cli之后安装qiankun

$ yarn add qiankun # 或者 npm i qiankun -S

注册微应用并启动,编辑main.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import { registerMicroApps, start } from "qiankun";

// 注册微应用
registerMicroApps([
  {
    name: "reactApp",
    entry: "//localhost:8081",
    container: "#subapp-container",
    activeRule: "/app-react"
  },
  {
    name: "vue2App",
    entry: "//localhost:8082",
    container: "#subapp-container",
    activeRule: "/app-vue2"
  }
])
// 启动qiankun
start()

container 微应用的入口编辑App.vue

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link> |
      <router-link to="/app-react">React App</router-link> |
      <router-link to="/app-vue2">Vue 2 App</router-link> |
      <router-link to="/app-purehtml">Other</router-link> 
    </div>
    <!-- 主应用路由渲染入口 -->
    <router-view/>
    <!-- 子应用渲染的入口 -->
    <div id="subapp-container"></div>
  </div>
</template>

4.创建微应用

微应用不需要额外安装任何其他依赖即可接入qiankun主应用

1.创建react微应用

全局安装脚手架

npm install -g create-react-app

创建react子应用项目

create-react-app app-react  
  1. 在src目录新增public-path.js
if (window.__POWERED_BY_QIANKUN__) {
    // eslint-disable-next-line no-undef
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
  1. 然后在index.js中引入public-path.js // 注意:这里要public-path的设置方法放到最前面
  2. 入口文件index.js修改 bootstrap、mount、unmount是qiankun运行必须的生命周期钩子函数
    启动的时候 bootstrap
    渲染的时候 mount
    卸载的时候 unmount
import './public-path' // 注意: 这里要把public-path的设置放到最前面
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
function render(props) {
  const { container } = props
  ReactDOM.render(
      <App />,
    container
      ? container.querySelector('#root') // 主应用的渲染入口
      : document.querySelector('#root') // 独立运行的渲染入口
  )
}
// 独立运行的时候直接手动render
if (!window.__POWERED_BY_QIANKUN__) {
  render({});
}

// bootstrap、mount、unmount 是 qiankun 运行必须的生命周期钩子函数
// 启动的时候
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') // 运行在qiankun主应用中
      : document.querySelector('#root')); // 独立运行
}
reportWebVitals();
  1. 修改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'; //模块规范 支持 CommonJS、 AMD、 ES Modules等  
    config.output.jsonpFunction = `webpackJsonp_${name}`;
    config.output.globalObject = 'window';
    // config.output.publicPath = process.env.NODE_ENV === 'production'
    // ? '/child/app-react'
    // : '/'
    config.output.publicPath = '/'
    return config;
  },

  devServer: (_) => {
    const config = _;

    //  开发服务的响应头
    config.headers = {
        // CORS 跨域(本地)
        // 之后发布部署的时候线上环境也需要配置跨域 
      'Access-Control-Allow-Origin': '*', 
    };
    config.historyApiFallback = true;
    config.hot = false;
    config.watchContentBase = false;
    config.liveReload = false;

    return config;
  },
};
  1. 设置端口号

react热更新应该链接到8081 但是此时他链接到8080,所以此处需要设置端口号

根目录下创建一个.env文件

PORT=8081
// react 热更新要链接到8081 但是此时为链接到 8080
WDS_SOCKET_PORT=8081 // 此处是解决react热更新 WebSorket connection to ‘ws: //localhost:8080/sockjs-node’ failed:报错的问题
  1. 修改 package.json:
-   "start": "react-scripts start",
+   "start": "rescripts start",
-   "build": "react-scripts build",
+   "build": "rescripts build",
-   "test": "react-scripts test",
+   "test": "rescripts test",
-   "eject": "react-scripts eject"

2.创建react微应用路由-使用history模式

  1. 安装react-router
$ npm install react-router-dom@6
  1. 修改index.js,引入react-router-dom以及包裹App
import { BrowserRouter } from 'react-router-dom'
<BrowserRouter basename={getBasename()}>
  <App />,
</BrowserRouter>,
  1. 根组件App.js配置路由表 同时创建pages下两个文件Home.js和About.js
// import logo from './logo.svg'
import './App.css'
import { Routes, Route, Link } from 'react-router-dom'
import Home from './pages/Home'
import About from './pages/About'

function App() {
  return (
    <div className="App">
      {/* 导航栏 */}
      <nav>
        <Link to="/">Home</Link> |
        <Link to="/about">About</Link>
      </nav>
      {/* 路由出口 */}
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="about" element={<About />} />
      </Routes>
    </div>
  )
}
export default App

Home.js和About.js内容为

// About.js
export default function Home () {
  return (
    <div className="home"> 
      <h2>About works!</h2>
    </div>
  )
}
// Home.js
export default function Home () {
  return (
    <div className="home">
      <h2>Home works!</h2>
    </div>
  )
}

  1. 此时在主应用并未看到路由对应的内容,子应用路由和主应用的路由冲突 需要配置basename
    <BrowserRouter basename="/app-react"}>
      <App />,
    </BrowserRouter>,

3.创建vue微应用

  1. 搭建vue-cli子应用
vue create app-vue2
  1. 在 src 目录新增 public-path.js: 然后在main.js中引入
if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
  1. 入口文件main.js 修改,为了避免根 id #app 与其他的 DOM 冲突,需要限制查找范围。
import './public-path'
import Vue from 'vue'
import App from './App.vue'
import { routes } from './router'
import VueRouter from 'vue-router'

Vue.config.productionTip = false

let router = null;
let instance = null;
function render(props = {}) {
  const { container } = props;
  router = new VueRouter({
    base: window.__POWERED_BY_QIANKUN__ ? '/app-vue2/' : '/',
    mode: 'history',
    routes,
  });

  instance = new Vue({
    router,
    // store,
    render: (h) => h(App),
  }).$mount(container ? container.querySelector('#app') : '#app');
}

// 独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

export async function bootstrap() {
  console.log('[vue] vue app bootstraped');
}
export async function mount(props) {
  console.log('[vue] props from main framework', props);
  render(props);
}
export async function unmount() {
  instance.$destroy();
  instance.$el.innerHTML = '';
  instance = null;
  router = null;
}
  1. 设置router使外部可以拿到路由表-修改router下的index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'

Vue.use(VueRouter)

export const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]
// 不需要创建内部router 此处注释即可
// const router = new VueRouter({
//   mode: 'history',
//   base: process.env.BASE_URL,
//   routes
// })

// export default router
  1. 打包配置修改(根目录下创建vue.config.js)
const { name } = require('./package');
module.exports = {
  devServer: {
    headers: {
      'Access-Control-Allow-Origin': '*', // CORS 跨域
    },
    port: 8082 // 端口号修改
  },
  configureWebpack: {
    output: {
      library: `${name}-[name]`,
      libraryTarget: 'umd', // 把微应用打包成 umd 库格式
      jsonpFunction: `webpackJsonp_${name}`,
    },
  },
};

5.qiankun中的应用通信

1.主应用中处理 在src文件下新建一个utils文件-该文件下创建一个action.js文件-用来处理通信

import { initGlobalState } from 'qiankun'

const actions = initGlobalState({
  count: 0,
  foo: 'bar',
  user: {
    name: '张三'
  }
})

// state: 当前最新的 state
// prev:变化之前的 state
actions.onGlobalStateChange((state, prev) => {
  console.log('onGlobalStateChange => ', state, prev)
}, false) // 默认不会立即执行


// qiankun 在加载子应用的时候会通过 props 把通信的 API 传递给子应用
export default actions

// 修改 state
// actions.setGlobalState({ count: 100 })

// 关闭数据监视,应用在 unmount 的时候会自动关闭监视
// actions.offGlobalStateChange()

在main.js中引入action

import './utils/actions' // 初始化全局通信总线
  1. 在子应用中处理 在app-react中,修改index.js
// 渲染的时候
export async function mount(props) {
  console.log('[react16] props from main framework', props)
  props.onGlobalStateChange((state, prev) => {
    console.log('react onGlobalStateChange => ', state, prev)
    // 建议把这个数据放到微应用内部的状态管理容器中,比如 react 中的 redux、vue 中的 Vuex
  }, true)

  setTimeout(() => {
    props.setGlobalState({
      user: {
        name: '里斯'
      }
    })
  }, 2000)
  render(props)
}

6.qiankun部署

建议:主应用和微应用都是独立开发和部署,即它们都属于不同的仓库和服务。

场景一:主应用和微应用部署到同一个服务器(同一个 IP 和端口)

一般这么做是因为不允许主应用跨域访问微应用,做法就是将主应用服务器上一个特殊路径的请求全部转发到微应用的服务器上,即通过代理实现“微应用部署在主应用服务器上”的效果。
我们可以在本地部署一个nginx用来开发测试(以windows为例) 下载nginx

http://nginx.org/en/download.html

然后解压出来 打开目录 在终端中进入nginx目录

start ./nginx.exe

此时可以使用loaclhost访问

.\nginx.exe -s stop  // 强制关闭nginx
.\nginx.exe -s reload // 重启
.\nginx.exe -s quit // 正常关闭
  1. 首先配置主应用 在nginx根目录下创建www文件用来放主应用
    把主应用打包的文件放在www文件目录下
    修改conf下的nginx.conf 然后重启nginx
    server {
        listen       80;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
            root   www;
            index  index.html index.htm;
        }
  1. 创建子应用配置 在www下创建 child 目录 然后 child下创建两个子应用目录 app-react app-vue2 然后把子应用打包后的文件放在app-react 和app-vue2文件下 此时 子应用地址变为了 localhost/child/app-react 和 localhost/child/app-vue2
  2. 配置环境变量 在根目录下创建.production.env 和 .development.env文件用来配置生产环境和开发模式的环境模量
// .production.env
VUE_APP_REACT_APP_ENTRY=http://localhost/child/app-react/
VUE_APP_VUE2_APP_ENTRY=http://localhost/child/app-vue2/
// .development.env
VUE_APP_REACT_APP_ENTRY=//localhost:8081
VUE_APP_VUE2_APP_ENTRY=//localhost:8082
  1. 修改主应用的mian.js
  {
    name: 'reactApp',
    // 微应用的 entry 路径最后面的 / 不可省略,否则 publicPath 会设置错误,例如子项的访问路径是 http://localhost:8080/app1,那么 entry 就是 http://localhost:8080/app1/。
    entry: process.env.VUE_APP_REACT_APP_ENTRY,
    container: '#subapp-container', // 微应用渲染的入口
    // activeRule 不能和微应用的真实访问路径一样,否则在主应用页面刷新会直接变成微应用页面。
    activeRule: '/app-react'
  },
  {
     name: 'vue2App',
    entry: process.env.VUE_APP_VUE2_APP_ENTRY,
     container: '#subapp-container',
     activeRule: '/app-vue2'
   },
  1. 修改微应用 修改app-vue2的mian.js
const isProd = process.env.NODE_ENV === 'production'

function render(props = {}) {
  const { container } = props
  router = new VueRouter({
    base: isProd
      ? window.__POWERED_BY_QIANKUN__ // 生产环境
        ? '/app-vue2/' // qiankun 中的 base
        : '/child/app-vue2/' // 独立访问的时候的 base
      : window.__POWERED_BY_QIANKUN__ // 开发环境
        ? '/app-vue2/' // 本地开发中的 qiankun 中的 base
        : '/', // 本地开发独立访问的时候的 base
    // base: window.__POWERED_BY_QIANKUN__ ? '/app-vue2/' : '/',
    // 在 vue-router 的 hash 下设置的 base 没有作用
    mode: 'history',
    routes
  })

app-vue2打包的配置(vue.config.js)

  publicPath: process.env.NODE_ENV === 'production'
    ? '/child/app-vue2/' // 线上生产环境
    : '/', // 本地开发环境

把vue子应用和main主应用打包一下放到nginx中的www和app-vue2文件下 运行看是否符合预期效果

app-react 配置.rescriptsrc.js

config.output.publicPath = process.env.NODE_ENV === 'production'
      ? '/child/app-react'
      : '/'

app-react配置index.js

const getBasename = () => {
  return process.env.NODE_ENV === 'production'
    ? window.__POWERED_BY_QIANKUN__ // 生产环境
      ? '/app-react/' // qiankun 中的 base
      : '/child/app-react/' // 独立访问的时候的 base
    : window.__POWERED_BY_QIANKUN__ // 开发环境
      ? '/app-react/' // 本地开发中的 qiankun 中的 base
      : '/'
}
function render(props) {
  const { container } = props
  ReactDOM.render(
    // 主应用是 history、微应用是 hash,不要设置 basename
    // 主应用是 hash,微应用是 hash,必须设置 basename
    // <HashRouter basename={ window.__POWERED_BY_QIANKUN__ ? '/app-react' : '/' }>
    //   <App />,
    // </HashRouter>,
    <BrowserRouter basename={getBasename()}>
      <App />,
    </BrowserRouter>,
    container
      ? container.querySelector('#root') // 主应用的渲染入口
      : document.querySelector('#root') // 独立运行的渲染入口
  )
}

至此主应用已经和微应用都能跑起来了,但是主应用和 vue-historyreact-historyangular-history 微应用是 history 路由,需要解决刷新 404 的问题,nginx 还需要配置一下:

    server {
        listen       80;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
            root   www;
            index  index.html index.htm;
            try_files $uri $uri/ /index.html;
        }
        location /child/app-react {
            root   www;
            index  index.html index.htm;
            try_files $uri $uri/ /child/app-react/index.html;
        }
        location /child/app-vue2 {
            root   www;
            index  index.html index.htm;
            try_files $uri $uri/ /child/app-vue2/index.html;
        }

        #error_page  404              /404.html;

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }

        # proxy the PHP scripts to Apache listening on 127.0.0.1:80
        #
        #location ~ \.php$ {
        #    proxy_pass   http://127.0.0.1;
        #}

        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
        #
        #location ~ \.php$ {
        #    root           html;
        #    fastcgi_pass   127.0.0.1:9000;
        #    fastcgi_index  index.php;
        #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
        #    include        fastcgi_params;
        #}

        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one
        #
        #location ~ /\.ht {
        #    deny  all;
        #}
    }

例如,主应用在 A 服务器,微应用在 B 服务器,使用路径 /app1 来区分微应用,即 A 服务器上所有 /app1 开头的请求都转发到 B 服务器上。
此时主应用的 Nginx 代理配置为: