qiankun 微前端框架

266 阅读4分钟

一、介绍

微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。

微前端的核心价值:

  1. 技术栈无关
  2. 独立开发、独立部署
  3. 增量升级
  4. 独立运行时

qiankun 微前端框架特性:

  1. 基于single-spa封装
  2. 技术栈无关
  3. HTML Entry 接入方式
  4. 样式隔离
  5. JS沙箱
  6. 资源预加载
  7. umi插件

二、快速上手

主应用

1. 安装 qiankun
yarn add qiankun  
# 或
npm install qiankun -Ss
2. 在主应用中注册微应用
import { registerMicroApps, start } from 'qiankun'

registerMicroApps([
	{
		name: 'reactApp',
		entry: '//localhost:7100',
		container: '#reactContainer',
		activeRule: '/reactActiveRule'
	},
	{
		name: 'vueApp',
		entry: {
			scripts: ['//localhost:7100/main.js']
		},
		container: '#vueContainer',
		activeRule: '/vueActiveRule'
	},
])

start();

当微应用信息注册完之后,一旦浏览器的 url 发生变化,便会自动触发 qiankun 的匹配逻辑,所有 activeRule 规则匹配上的微应用就会被插入到指定的 container 中,同时依次调用微应用暴露出的生命周期钩子。

如果微应用不是直接跟路由关联的时候,你也可以选择手动加载微应用的方式:

import { loadMicroApp } from 'qiankun'
loadMicroApp({
	name: 'app',
	entry: '//localhost:7100',
	container: '#yourContainer'
})

微应用

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

1. 导出相应的生命周期钩子

微应用需要在自己的入口js( 通常是你配置的webpack的entry.js )导出 bootstrap、mount、unmount三个生命周期钩子,以供主引用在适当的时机调用。

/**
* bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发
* bootstrap。
*/ 
export async function bootstrap() {
	console.log('bootstrap')
}

/**
* 应用每次进入都会调用 mount 方法, 通常我们再这里触发应用的渲染方法。
*/
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')
	)
}

/**
* 可选,仅使用 loadMicroApp 方式加载微应用时生效
*/
export async function update (props) {
	console.log(props)
}
2. 配置微应用的打包工具

除了代码中暴露出相应的生命周期钩子之外,为了让主应用能正确识别微应用暴露出来的一些信息,微应用的打包工具需要增加如下配置:

webpack:

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

module.exports = {
	output: {
		library: `${packageName}-[name]`,
		libraryTarget: 'umd',
		jsonpFunction: `webpackJsonp_${packgeName}`
	}
}

项目实践

主应用

主应用不限技术栈,只需要提供一个容器 DOM,然后注册微应用并 start 即可。

// 安装qiankun
yarn add qiankun # 或者 npm i qiankun -S

// 注册微应用并启动:
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();

微应用

微应用分为 有 webpack 构建和 无 webpack 构建项目。

有 webpack 构建项目 【Vue | React | Angular】

  1. 新增 public-path.js 文件,用于修改运行时的 publicPath; (运行时的publicPath 和 构建时的publicPath 是不同的,两者不能等价替代)。

  2. 微应用建议使用 history 模式的路由,需要设置路由 base, 值和它的 activeRule 是一样的。

  3. 在入口文件最顶部引入 public-path.js, 修改并导出三个生命周期函数。

  4. 修改webpack打包,允许开发环境跨域和 umd 打包。

    你的项目是 index.html 和其他的所有文件分开部署的,说明你们已经将构建时的 publicPath 设置为了完整路径,则不用修改运行时的 publicPath (第一步操作可省)。

无webpack构建项目

无webpack构建的微应用直接将 生命周期 lifecycles 挂载到 window 上即可。

React 微应用

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.querySelect('#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;
      },
    };
    

    修改 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"
    
    Vue 微应用

    vue-cli 3+ 生成的 vue 2.x 项目为例,vue 3 版本等稳定后再补充。

    1. 在 src 目录下新增 public-path.js

      if (window.__POWERED_BY_QIANKUN__) {
      	__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
      }
      
    2. 入口文件 main.js 修改,为了避免根id #app 与其他的DOM冲突,需要限制查找范围。

      import './public-path'
      import Vue from 'vue'
      import VueRouter from 'vue-router'
      import App from './App.vue'
      import routes from './router'
      import store from './store'
      
      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-vue/' : '/',
      		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(props) {
      	instance.$destroy()
      	instance.$el.innerHTML = ''
      	instance = null
      	router = null
      }
      
    3. 打包配置修改(vue.config.js)

      const { name } = require('./package')
      
      module.exports = {
        devServer: {
          headers: {
            'Access-Control-Allow-Origin': '*',
          },
        },
        configureWebpack: {
          output: {
            library: `${name}-[name]`,
            libraryTarget: 'umd', // 把微应用打包成 umd 库格式
            jsonpFunction: `webpackJsonp_${name}`,
          },
        },
      }
      
    非 webpack 构建的微应用

    一些非 webpack 构建的项目,例如 jQuery 项目、jsp 项目,都可以按照这个处理。

    接入之前请确保你的项目里的图片、音视频等资源能正常加载,如果这些资源的地址都是完整路径(例如 https://qiankun.umijs.org/logo.png),则没问题。如果都是相对路径,需要先将这些资源上传到服务器,使用完整路径。

    接入非常简单,只需要额外声明一个 script,用于 export 相对应的 lifecycles。例如:

    1. 声明 entry 入口

      <!DOCTYPE html>
      <html lang="en">
      <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Purehtml Example</title>
      </head>
      <body>
        <div>
          Purehtml Example
        </div>
      </body>
      
      + <script src="//yourhost/entry.js" entry></script>
      
      </html>
      
    2. 在 entry.js 里声明生命周期 lifecycles

      const render = ($) => {
      	$('#puerhtml-container').html('Hello, render with jQuery');
      	return Promise.resolve();
      }
      
      ((global) => {
      	global['purehtml'] = {
      		bootstrap: () => {
      			console.log('purehtml bootstrap')
      			return Promise.resolve()
      		},
      		mount: () => {
      			console.log('purehtml mount')
      			return render($)
      		},
      		unmount: () => {
      			console.log('purehtml unmount')
      			return Promise.resolve()
      		}
      	}
      })(window)
      

【参考】1. qiankun 官网: 介绍 - qiankun (umijs.org)

​ 2. qiankun的github: umijs/qiankun: 📦 🚀 Blazing fast, simple and complete solution for micro frontends. (github.com)