项目实践

本教程适合刚接触 qiankun 的新人,介绍了如何从 0 构建一个 qiankun 项目。

主应用

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

先安装 qiankun

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

注册微应用并启动:

  1. import { registerMicroApps, start } from 'qiankun';
  2. registerMicroApps([
  3. {
  4. name: 'reactApp',
  5. entry: '//localhost:3000',
  6. container: '#container',
  7. activeRule: '/app-react',
  8. },
  9. {
  10. name: 'vueApp',
  11. entry: '//localhost:8080',
  12. container: '#container',
  13. activeRule: '/app-vue',
  14. },
  15. {
  16. name: 'angularApp',
  17. entry: '//localhost:4200',
  18. container: '#container',
  19. activeRule: '/app-angular',
  20. },
  21. ]);
  22. // 启动 qiankun
  23. start();

微应用

微应用分为有 webpack 构建和无 webpack 构建项目,有 webpack 的微应用(主要是指 Vue、React、Angular)需要做的事情有:

  1. 新增 public-path.js 文件,用于修改运行时的 publicPath什么是运行时的 publicPath ?项目实战 - 图1

注意:运行时的 publicPath 和构建时的 publicPath 是不同的,两者不能等价替代。

  1. 微应用建议使用 history 模式的路由,需要设置路由 base,值和它的 activeRule 是一样的。
  2. 在入口文件最顶部引入 public-path.js,修改并导出三个生命周期函数。
  3. 修改 webpack 打包,允许开发环境跨域和 umd 打包。

主要的修改就是以上四个,可能会根据项目的不同情况而改变。例如,你的项目是 index.html 和其他的所有文件分开部署的,说明你们已经将构建时的 publicPath 设置为了完整路径,则不用修改运行时的 publicPath (第一步操作可省)。

webpack 构建的微应用直接将 lifecycles 挂载到 window 上即可。

React 微应用

create react app 生成的 react 16 项目为例,搭配 react-router-dom 5.x。

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

    1. if (window.__POWERED_BY_QIANKUN__) {
    1. __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
    1. }
  2. 设置 history 模式路由的 base

    1. <BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? '/app-react' : '/'}>
  3. 入口文件 index.js 修改,为了避免根 id #root 与其他的 DOM 冲突,需要限制查找范围。

    1. import './public-path';
    1. import React from 'react';
    1. import ReactDOM from 'react-dom';
    1. import App from './App';
    1. function render(props) {
    1. const { container } = props;
    1. ReactDOM.render(<App />, container ? container.querySelector('#root') : document.querySelector('#root'));
    1. }
    1. if (!window.__POWERED_BY_QIANKUN__) {
    1. render({});
    1. }
    1. export async function bootstrap() {
    1. console.log('[react16] react app bootstraped');
    1. }
    1. export async function mount(props) {
    1. console.log('[react16] props from main framework', props);
    1. render(props);
    1. }
    1. export async function unmount(props) {
    1. const { container } = props;
    1. ReactDOM.unmountComponentAtNode(container ? container.querySelector('#root') : document.querySelector('#root'));
    1. }
  4. 修改 webpack 配置

    安装插件 @rescripts/cli,当然也可以选择其他的插件,例如 react-app-rewired

    1. npm i -D @rescripts/cli

    根目录新增 .rescriptsrc.js

    1. const { name } = require('./package');
    1. module.exports = {
    1. webpack: (config) => {
    1. config.output.library = `${name}-[name]`;
    1. config.output.libraryTarget = 'umd';
    1. config.output.jsonpFunction = `webpackJsonp_${name}`;
    1. config.output.globalObject = 'window';
    1. return config;
    1. },
    1. devServer: (_) => {
    1. const config = _;
    1. config.headers = {
    1. 'Access-Control-Allow-Origin': '*',
    1. };
    1. config.historyApiFallback = true;
    1. config.hot = false;
    1. config.watchContentBase = false;
    1. config.liveReload = false;
    1. return config;
    1. },
    1. };

    修改 package.json

    1. - "start": "react-scripts start",
    1. + "start": "rescripts start",
    1. - "build": "react-scripts build",
    1. + "build": "rescripts build",
    1. - "test": "react-scripts test",
    1. + "test": "rescripts test",
    1. - "eject": "react-scripts eject"

Vue 微应用

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

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

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

    1. import './public-path';
    1. import Vue from 'vue';
    1. import VueRouter from 'vue-router';
    1. import App from './App.vue';
    1. import routes from './router';
    1. import store from './store';
    1. Vue.config.productionTip = false;
    1. let router = null;
    1. let instance = null;
    1. function render(props = {}) {
    1. const { container } = props;
    1. router = new VueRouter({
    1. base: window.__POWERED_BY_QIANKUN__ ? '/app-vue/' : '/',
    1. mode: 'history',
    1. routes,
    1. });
    1. instance = new Vue({
    1. router,
    1. store,
    1. render: (h) => h(App),
    1. }).$mount(container ? container.querySelector('#app') : '#app');
    1. }
    1. // 独立运行时
    1. if (!window.__POWERED_BY_QIANKUN__) {
    1. render();
    1. }
    1. export async function bootstrap() {
    1. console.log('[vue] vue app bootstraped');
    1. }
    1. export async function mount(props) {
    1. console.log('[vue] props from main framework', props);
    1. render(props);
    1. }
    1. export async function unmount() {
    1. instance.$destroy();
    1. instance.$el.innerHTML = '';
    1. instance = null;
    1. router = null;
    1. }
  3. 打包配置修改(vue.config.js):

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

Angular 微应用

Angular-cli 9 生成的 angular 9 项目为例,其他版本的 angular 后续会逐渐补充。

  1. src 目录新增 public-path.js 文件,内容为:

    1. if (window.__POWERED_BY_QIANKUN__) {
    1. // eslint-disable-next-line no-undef
    1. __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
    1. }
  2. 设置 history 模式路由的 basesrc/app/app-routing.module.ts 文件:

    1. + import { APP_BASE_HREF } from '@angular/common';
    1. @NgModule({
    1. imports: [RouterModule.forRoot(routes)],
    1. exports: [RouterModule],
    1. // @ts-ignore
    1. + providers: [{ provide: APP_BASE_HREF, useValue: window.__POWERED_BY_QIANKUN__ ? '/app-angular' : '/' }]
    1. })
  3. 修改入口文件,src/main.ts 文件。

    1. import './public-path';
    1. import { enableProdMode, NgModuleRef } from '@angular/core';
    1. import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
    1. import { AppModule } from './app/app.module';
    1. import { environment } from './environments/environment';
    1. if (environment.production) {
    1. enableProdMode();
    1. }
    1. let app: void | NgModuleRef<AppModule>;
    1. async function render() {
    1. app = await platformBrowserDynamic()
    1. .bootstrapModule(AppModule)
    1. .catch((err) => console.error(err));
    1. }
    1. if (!(window as any).__POWERED_BY_QIANKUN__) {
    1. render();
    1. }
    1. export async function bootstrap(props: Object) {
    1. console.log(props);
    1. }
    1. export async function mount(props: Object) {
    1. render();
    1. }
    1. export async function unmount(props: Object) {
    1. console.log(props);
    1. // @ts-ignore
    1. app.destroy();
    1. }
  4. 修改 webpack 打包配置

    先安装 @angular-builders/custom-webpack 插件,注意:angular 9 项目只能安装 9.x 版本,angular 10 项目可以安装最新版

    1. npm i @angular-builders/custom-webpack@9.2.0 -D

    在根目录增加 custom-webpack.config.js ,内容为:

    1. const appName = require('./package.json').name;
    1. module.exports = {
    1. devServer: {
    1. headers: {
    1. 'Access-Control-Allow-Origin': '*',
    1. },
    1. },
    1. output: {
    1. library: `${appName}-[name]`,
    1. libraryTarget: 'umd',
    1. jsonpFunction: `webpackJsonp_${appName}`,
    1. },
    1. };

    修改 angular.json,将 [packageName] > architect > build > builder[packageName] > architect > serve > builder 的值改为我们安装的插件,将我们的打包配置文件加入到 [packageName] > architect > build > options

    1. - "builder": "@angular-devkit/build-angular:browser",
    1. + "builder": "@angular-builders/custom-webpack:browser",
    1. "options": {
    1. + "customWebpackConfig": {
    1. + "path": "./custom-webpack.config.js"
    1. + }
    1. }
    1. - "builder": "@angular-devkit/build-angular:dev-server",
    1. + "builder": "@angular-builders/custom-webpack:dev-server",
  5. 解决 zone.js 的问题

    父应用引入 zone.js,需要在 import qiankun 之前引入。

    将微应用的 src/polyfills.ts 里面的引入 zone.js 代码删掉。

    1. - import 'zone.js/dist/zone';

    在微应用的 src/index.html 里面的 <head> 标签加上下面内容,微应用独立访问时使用。

    1. <!-- 也可以使用其他的CDN/本地的包 -->
    1. <script src="https://unpkg.com/zone.js" ignore></script>
  6. 修正 ng build 打包报错问题,修改 tsconfig.json 文件,参考issues/431项目实战 - 图2

    1. - "target": "es2015",
    1. + "target": "es5",
    1. + "typeRoots": [
    1. + "node_modules/@types"
    1. + ],
  7. 为了防止主应用或其他微应用也为 angular 时,<app-root></app-root> 会冲突的问题,建议给<app-root> 加上一个唯一的 id,比如说当前应用名称。

    src/index.html :

    1. - <app-root></app-root>
    1. + <app-root id="angular9"></app-root>

    src/app/app.component.ts :

    1. - selector: 'app-root',
    1. + selector: '#angular9 app-root',

当然,也可以选择使用 single-spa-angular 插件,参考 single-spa-angular 的官网项目实战 - 图3angular demo项目实战 - 图4

补充)angular7 项目除了第 4 步以外,其他的步骤和 angular9 一模一样。angular7 修改 webpack 打包配置的步骤如下:

除了安装 angular-builders/custom-webpack 插件的 7.x 版本外,还需要安装 angular-builders/dev-server

  1. npm i @angular-builders/custom-webpack@7 -D
  2. npm i @angular-builders/dev-server -D

在根目录增加 custom-webpack.config.js ,内容同上。

修改 angular.json[packageName] > architect > build > builder 的修改和 angular9 一样, [packageName] > architect > serve > builder 的修改和 angular9 不同。

  1. - "builder": "@angular-devkit/build-angular:browser",
  2. + "builder": "@angular-builders/custom-webpack:browser",
  3. "options": {
  4. + "customWebpackConfig": {
  5. + "path": "./custom-webpack.config.js"
  6. + }
  7. }
  8. - "builder": "@angular-devkit/build-angular:dev-server",
  9. + "builder": "@angular-builders/dev-server:generic",

非 webpack 构建的微应用

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

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

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

  1. 声明 entry 入口

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

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

你也可以直接参照 examples 中 purehtml 部分的代码项目实战 - 图5

同时,你也需要开启相关资源的 CORS,具体请参照此处

umi-qiankun 项目

umi-qiankun 的教程请移步 umi 官网项目实战 - 图6umi-qiankun 的官方 demo项目实战 - 图7