什么是微前端

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

qiankun

qiankun 是一个基于 single-spa 的微前端实现库,生产可用微前端架构系统。

特性

  • 基于 single-spa 封装,提供了更加开箱即用的 API。
  • 技术栈无关,任意技术栈的应用均可 使用/接入,不论是 React/Vue/Angular/JQuery 还是其他等框架。
  • HTML Entry 接入方式,让你接入微应用像使用 iframe 一样简单。
  • 样式隔离,确保微应用之间样式互相不干扰。
  • JS 沙箱,确保微应用之间 全局变量/事件 不冲突。
  • 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
  • umi 插件,提供了 @umijs/plugin-qiankun 供 umi 应用一键切换成微前端架构系统。

搭建使用

Base 基座搭建

  • 选择 vue-cli 初始化项目
  • 项目引入 qiankun

    1. $ yarn add qiankun
    2. # or
    3. $ npm install qiankun -S
  • 注册微应用 ```javascript // src/main.js import Vue from “vue”; import App from “./App.vue”; import router from “./router”; import ElementUI from “element-ui”; import “element-ui/lib/theme-chalk/index.css”;

Vue.use(ElementUI);

import { registerMicroApps, start } from “qiankun”;

/*

  • name: 微应用名称 - 具有唯一性
  • entry: 微应用入口 - 通过该地址加载微应用
  • container: 微应用挂载节点 - 微应用加载完成后将挂载在该节点上
  • activeRule: 微应用触发的路由规则 - 触发路由规则后将加载该微应用
  • props: 共享给微应用的数据 */ registerMicroApps([ {
    1. name: "vueApp",
    2. entry: "//localhost:10001",
    3. container: "#vue",
    4. activeRule: "/vue",
    5. props: { a: 1 },
    }, {
    1. name: "reactApp",
    2. entry: "//localhost:20001",
    3. container: "#react",
    4. activeRule: "/react",
    }, ]);

start({ prefetch: false, });

new Vue({ router, render: (h) => h(App), }).$mount(“#app”);

  1. - 添加点击跳转路由链接
  2. ```vue
  3. <!-- App.vue -->
  4. <template>
  5. <div>
  6. <el-menu :router="true" mode="horizontal">
  7. <el-menu-item index="/">Home</el-menu-item>
  8. <el-menu-item index="/vue">vue</el-menu-item>
  9. <el-menu-item index="/react">react</el-menu-item>
  10. </el-menu>
  11. <router-view />
  12. <div id="vue"></div>
  13. <div id="react"></div>
  14. </div>
  15. </template>

Vue 子应用

  • main.js 导出相应的生命周期钩子 ```javascript // src/main.js import Vue from “vue”; import App from “./App.vue”; import router from “./router”;

// Vue.config.productionTip = false

let instance = null; function render(props) { instance = new Vue({ router, render: (h) => h(App), }).$mount(“#app”); }

if (window.POWERED_BY_QIANKUN) { // 动态设置 webpack publicPath,防止资源加载出错 webpack_public_path = window.INJECTED_PUBLIC_PATH_BY_QIANKUN; }

if (!window.POWERED_BY_QIANKUN) { render(); }

/**

  • bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
  • 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。 */ export async function bootstrap(props) { console.log(‘vue app bootstraped’); }

// 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法 export async function mount(props) { console.log(props) render(props); }

// 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例 export async function unmount(props) { instance.$destroy(); }

  1. - router.js router base 改变基础路由
  2. ```javascript
  3. // src/router/index.js
  4. import Vue from 'vue'
  5. import VueRouter from 'vue-router'
  6. import Home from '../views/Home.vue'
  7. Vue.use(VueRouter)
  8. const routes = [
  9. {
  10. path: '/',
  11. name: 'Home',
  12. component: Home
  13. },
  14. {
  15. path: '/about',
  16. name: 'About',
  17. // route level code-splitting
  18. // this generates a separate chunk (about.[hash].js) for this route
  19. // which is lazy-loaded when the route is visited.
  20. component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  21. }
  22. ]
  23. const router = new VueRouter({
  24. mode: 'history',
  25. // 运行微应用时,基础路由地址配置为 /vue
  26. base: window.__POWERED_BY_QIANKUN__ ? "/vue" : process.env.BASE_URL,
  27. routes
  28. })
  29. export default router
  • 修改config文件 webpack打包策略 ```javascript // vue.config.js module.exports = { devServer: {
    1. port: 10001,
    // 配置跨域请求头,解决开发环境的跨域问题
    1. headers: {
    2. "Access-Control-Allow-Origin": "*",
    3. },
    }, configureWebpack: {
    1. output: {
    2. // 微应用的包名,这里与主应用中注册的微应用名称一致
    3. library: "vueApp",
    4. // 将你的 library 暴露为所有的模块定义下都可运行的方式
    5. libraryTarget: "umd",
    6. },
    }, };
  1. <a name="SwvjB"></a>
  2. ## React 子应用
  3. - index.js 导出相应的生命周期钩子
  4. ```javascript
  5. // src/index.js
  6. import React from "react";
  7. import ReactDOM from "react-dom";
  8. import "./index.css";
  9. import App from "./App";
  10. function render() {
  11. ReactDOM.render(
  12. <React.StrictMode>
  13. <App />
  14. </React.StrictMode>,
  15. document.getElementById("root")
  16. );
  17. }
  18. if (!window.__POWERED_BY_QIANKUN__) {
  19. render();
  20. }
  21. export async function bootstrap() {}
  22. export async function mount(props) {
  23. render();
  24. }
  25. export async function unmount(props) {
  26. ReactDOM.unmountComponentAtNode(document.getElementById("root"));
  27. }
  • 新增 .env 文件,添加配置 ```javascript PORT=20001 WDS_SOCKET_PORT=20001
  1. - 引入 `react-router-dom` ,测试路由
  2. ```jsx
  3. // src/App.js
  4. import logo from "./logo.svg";
  5. import "./App.css";
  6. import { BrowserRouter, Route, Link } from "react-router-dom";
  7. function App() {
  8. return (
  9. <BrowserRouter basename="/react">
  10. <div className="nav">
  11. <Link to="/">index</Link> |
  12. <Link to="/about">about</Link>
  13. </div>
  14. <Route
  15. path="/"
  16. exact
  17. render={() => (
  18. <div className="App">
  19. <header className="App-header">
  20. <img src={logo} className="App-logo" alt="logo" />
  21. <p>
  22. Edit <code>src/App.js</code> and save to reload.
  23. </p>
  24. <a
  25. className="App-link"
  26. href="https://reactjs.org"
  27. target="_blank"
  28. rel="noopener noreferrer"
  29. >
  30. Learn React
  31. </a>
  32. </header>
  33. </div>
  34. )}
  35. ></Route>
  36. <Route path="/about" render={() => <h1>about</h1>}></Route>
  37. </BrowserRouter>
  38. );
  39. }
  40. export default App;
  • 引入 react-app-rewired 修改 webpack打包策略 ```javascript // config-overrides.js module.exports = { webpack: (config) => { // 微应用的包名,这里与主应用中注册的微应用名称一致
    1. config.output.library = "reactApp";
    // 将你的 library 暴露为所有的模块定义下都可运行的方式
    1. config.output.libraryTarget = "umd";
    2. config.output.publicPath = "http://localhost:20001";
    3. return config;
    }, devServer: (configFunction) => {
    1. return function (proxy, allowedHost) {
    2. const config = configFunction(proxy, allowedHost);
    3. config.headers = {
    4. // 配置跨域请求头,解决开发环境的跨域问题
    5. "Access-Control-Allow-Origin": "*",
    6. };
    7. return config;
    8. };
    }, };
  1. - 修改package.json启动命令
  2. ```javascript
  3. "scripts": {
  4. "start": "react-app-rewired start",
  5. "build": "react-app-rewired build",
  6. "test": "react-app-rewired test",
  7. "eject": "react-app-rewired eject"
  8. },

Angular 子应用

使用版本 Angular CLI: 11.2.8

  • 新增 public-path.js 文件,添加配置 ```javascript if (window.POWERED_BY_QIANKUN) { // eslint-disable-next-line no-undef webpack_public_path = window.INJECTED_PUBLIC_PATH_BY_QIANKUN; }
  1. - main.js 导出相应的生命周期钩子
  2. ```typescript
  3. // /src/main.ts
  4. import './public-path';
  5. import { enableProdMode, NgModuleRef } from '@angular/core';
  6. import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
  7. import { AppModule } from './app/app.module';
  8. import { environment } from './environments/environment';
  9. if (environment.production) {
  10. enableProdMode();
  11. }
  12. let app: void | NgModuleRef<AppModule>;
  13. async function render() {
  14. app = await platformBrowserDynamic()
  15. .bootstrapModule(AppModule)
  16. .catch((err) => console.error(err));
  17. }
  18. if (!(window as any).__POWERED_BY_QIANKUN__) {
  19. render();
  20. }
  21. export async function bootstrap(props: Object) {
  22. console.log(props);
  23. }
  24. export async function mount(props: Object) {
  25. render();
  26. }
  27. export async function unmount(props: Object) {
  28. console.log(props);
  29. // @ts-ignore
  30. app.destroy();
  31. }
  • 修改路由文件配置 ```typescript // /src/app/app-routing.module.ts import { NgModule } from ‘@angular/core’; import { RouterModule, Routes } from ‘@angular/router’; import { APP_BASE_HREF } from ‘@angular/common’;

const routes: Routes = [];

@NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], // @ts-ignore providers: [{ provide: APPBASEHREF, useValue: window.__POWERED_BY_QIANKUN ? ‘/angular’ : ‘/‘ }] }) export class AppRoutingModule { }

  1. - 引入 `@angular-builders/custom-webpack` 修改 webpack打包策略,新增 `custom-webpack.config.js` 文件,添加配置
  2. ```javascript
  3. // /custom-webpack.config.js
  4. const appName = require("./package.json").name;
  5. module.exports = {
  6. devServer: {
  7. headers: {
  8. "Access-Control-Allow-Origin": "*",
  9. },
  10. },
  11. output: {
  12. library: `${appName}-[name]`,
  13. libraryTarget: "umd",
  14. jsonpFunction: `webpackJsonp_${appName}`,
  15. },
  16. };
  • 修改 angular.json ```json { “$schema”: “./node_modules/@angular/cli/lib/config/schema.json”, “version”: 1, “newProjectRoot”: “projects”, “projects”: { “qiankun-angular”: {
    1. "projectType": "application",
    2. "schematics": {
    3. "@schematics/angular:component": {
    4. "style": "scss"
    5. },
    6. "@schematics/angular:application": {
    7. "strict": true
    8. }
    9. },
    10. "root": "",
    11. "sourceRoot": "src",
    12. "prefix": "app",
    13. "architect": {
    14. "build": {
    15. "builder": "@angular-builders/custom-webpack:browser",
    16. "options": {
    17. "outputPath": "dist/qiankun-angular",
    18. "index": "src/index.html",
    19. "main": "src/main.ts",
    20. "polyfills": "src/polyfills.ts",
    21. "tsConfig": "tsconfig.app.json",
    22. "aot": true,
    23. "assets": [
    24. "src/favicon.ico",
    25. "src/assets"
    26. ],
    27. "styles": [
    28. "src/styles.scss"
    29. ],
    30. "scripts": [],
    31. "customWebpackConfig": {
    32. "path": "./custom-webpack.config.js"
    33. }
    34. },
    35. "configurations": {
    36. "production": {
    37. "fileReplacements": [
    38. {
    39. "replace": "src/environments/environment.ts",
    40. "with": "src/environments/environment.prod.ts"
    41. }
    42. ],
    43. "optimization": true,
    44. "outputHashing": "all",
    45. "sourceMap": false,
    46. "namedChunks": false,
    47. "extractLicenses": true,
    48. "vendorChunk": false,
    49. "buildOptimizer": true,
    50. "budgets": [
    51. {
    52. "type": "initial",
    53. "maximumWarning": "500kb",
    54. "maximumError": "1mb"
    55. },
    56. {
    57. "type": "anyComponentStyle",
    58. "maximumWarning": "2kb",
    59. "maximumError": "4kb"
    60. }
    61. ]
    62. }
    63. }
    64. },
    65. "serve": {
    66. "builder": "@angular-builders/custom-webpack:dev-server",
    67. "options": {
    68. "browserTarget": "qiankun-angular:build"
    69. },
    70. "configurations": {
    71. "production": {
    72. "browserTarget": "qiankun-angular:build:production"
    73. }
    74. }
    75. },
    76. "extract-i18n": {
    77. "builder": "@angular-devkit/build-angular:extract-i18n",
    78. "options": {
    79. "browserTarget": "qiankun-angular:build"
    80. }
    81. },
    82. "test": {
    83. "builder": "@angular-devkit/build-angular:karma",
    84. "options": {
    85. "main": "src/test.ts",
    86. "polyfills": "src/polyfills.ts",
    87. "tsConfig": "tsconfig.spec.json",
    88. "karmaConfig": "karma.conf.js",
    89. "assets": [
    90. "src/favicon.ico",
    91. "src/assets"
    92. ],
    93. "styles": [
    94. "src/styles.scss"
    95. ],
    96. "scripts": []
    97. }
    98. },
    99. "lint": {
    100. "builder": "@angular-devkit/build-angular:tslint",
    101. "options": {
    102. "tsConfig": [
    103. "tsconfig.app.json",
    104. "tsconfig.spec.json",
    105. "e2e/tsconfig.json"
    106. ],
    107. "exclude": [
    108. "**/node_modules/**"
    109. ]
    110. }
    111. },
    112. "e2e": {
    113. "builder": "@angular-devkit/build-angular:protractor",
    114. "options": {
    115. "protractorConfig": "e2e/protractor.conf.js",
    116. "devServerTarget": "qiankun-angular:serve"
    117. },
    118. "configurations": {
    119. "production": {
    120. "devServerTarget": "qiankun-angular:serve:production"
    121. }
    122. }
    123. }
    124. }
    } }, “defaultProject”: “qiankun-angular” }
  1. - 解决 `zone.js`
  2. ```javascript
  3. // src/polyfills.ts
  4. /**
  5. * This file includes polyfills needed by Angular and is loaded before the app.
  6. * You can add your own extra polyfills to this file.
  7. *
  8. * This file is divided into 2 sections:
  9. * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
  10. * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
  11. * file.
  12. *
  13. * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
  14. * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
  15. * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
  16. *
  17. * Learn more in https://angular.io/guide/browser-support
  18. */
  19. /***************************************************************************************************
  20. * BROWSER POLYFILLS
  21. */
  22. /**
  23. * IE11 requires the following for NgClass support on SVG elements
  24. */
  25. // import 'classlist.js'; // Run `npm install --save classlist.js`.
  26. /**
  27. * Web Animations `@angular/platform-browser/animations`
  28. * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
  29. * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
  30. */
  31. // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
  32. /**
  33. * By default, zone.js will patch all possible macroTask and DomEvents
  34. * user can disable parts of macroTask/DomEvents patch by setting following flags
  35. * because those flags need to be set before `zone.js` being loaded, and webpack
  36. * will put import in the top of bundle, so user need to create a separate file
  37. * in this directory (for example: zone-flags.ts), and put the following flags
  38. * into that file, and then add the following code before importing zone.js.
  39. * import './zone-flags';
  40. *
  41. * The flags allowed in zone-flags.ts are listed here.
  42. *
  43. * The following flags will work for all browsers.
  44. *
  45. * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
  46. * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
  47. * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
  48. *
  49. * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
  50. * with the following flag, it will bypass `zone.js` patch for IE/Edge
  51. *
  52. * (window as any).__Zone_enable_cross_context_check = true;
  53. *
  54. */
  55. /***************************************************************************************************
  56. * Zone JS is required by default for Angular itself.
  57. */
  58. // import 'zone.js/dist/zone'; // Included with Angular CLI.
  59. /***************************************************************************************************
  60. * APPLICATION IMPORTS
  61. */
  • 引入zone ```html <!doctype html>
  1. - 添加 ID 确保唯一值
  2. ```typescript
  3. import { Component } from '@angular/core';
  4. @Component({
  5. selector: '#angularapp app-root',
  6. templateUrl: './app.component.html',
  7. styleUrls: ['./app.component.scss']
  8. })
  9. export class AppComponent {
  10. title = 'qiankun-angular';
  11. }
  • 修改 build 打包报错 ```json / /tsconfig.json / / To learn more about this file see: https://angular.io/config/tsconfig. / { “compileOnSave”: false, “compilerOptions”: { “baseUrl”: “./“, “outDir”: “./dist/out-tsc”, “forceConsistentCasingInFileNames”: true, “strict”: true, “noImplicitReturns”: true, “noFallthroughCasesInSwitch”: true, “sourceMap”: true, “declaration”: false, “downlevelIteration”: true, “experimentalDecorators”: true, “moduleResolution”: “node”, “importHelpers”: true, “target”: “es5”, “typeRoots”: [
    1. "node_modules/@types"
    ], “module”: “es2020”, “lib”: [
    1. "es2018",
    2. "dom"
    ] }, “angularCompilerOptions”: { “enableI18nLegacyMessageIdFormat”: false, “strictInjectionParameters”: true, “strictInputAccessModifiers”: true, “strictTemplates”: true } }
  1. <a name="x453K"></a>
  2. # Nginx 配置
  3. > 'Access-Control-Allow-Origin' '*';
  4. > 生成环境 这里需要改成具体域名地址
  5. ```nginx
  6. # nginx.conf
  7. #user nobody;
  8. worker_processes 1;
  9. #error_log logs/error.log;
  10. #error_log logs/error.log notice;
  11. #error_log logs/error.log info;
  12. #pid logs/nginx.pid;
  13. events {
  14. worker_connections 1024;
  15. }
  16. http {
  17. include mime.types;
  18. default_type application/octet-stream;
  19. #log_format main '$remote_addr - $remote_user [$time_local] "$request" '
  20. # '$status $body_bytes_sent "$http_referer" '
  21. # '"$http_user_agent" "$http_x_forwarded_for"';
  22. #access_log logs/access.log main;
  23. sendfile on;
  24. #tcp_nopush on;
  25. #keepalive_timeout 0;
  26. keepalive_timeout 65;
  27. #gzip on;
  28. server {
  29. listen 8080;
  30. server_name localhost;
  31. #charset koi8-r;
  32. #access_log logs/host.access.log main;
  33. location / {
  34. root html/static;
  35. index index.html index.htm;
  36. try_files $uri $uri/ /index.html;
  37. error_page 404 /index.html;
  38. }
  39. #error_page 404 /404.html;
  40. # redirect server error pages to the static page /50x.html
  41. #
  42. error_page 500 502 503 504 /50x.html;
  43. location = /50x.html {
  44. root html;
  45. }
  46. }
  47. server {
  48. listen 10001;
  49. server_name localhost;
  50. location / {
  51. root html/vueDist;
  52. index index.html index.htm;
  53. try_files $uri $uri/ /index.html;
  54. error_page 404 /index.html;
  55. add_header 'Access-Control-Allow-Origin' '*';
  56. add_header 'Access-Control-Allow-Credentials' 'true';
  57. add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
  58. add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
  59. }
  60. #error_page 404 /404.html;
  61. # redirect server error pages to the static page /50x.html
  62. #
  63. error_page 500 502 503 504 /50x.html;
  64. location = /50x.html {
  65. root html;
  66. }
  67. }
  68. server {
  69. listen 20001;
  70. server_name localhost;
  71. location / {
  72. root html/reactDist;
  73. index index.html index.htm;
  74. try_files $uri $uri/ /index.html;
  75. error_page 404 /index.html;
  76. add_header 'Access-Control-Allow-Origin' '*';
  77. add_header 'Access-Control-Allow-Credentials' 'true';
  78. add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
  79. add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
  80. }
  81. #error_page 404 /404.html;
  82. # redirect server error pages to the static page /50x.html
  83. #
  84. error_page 500 502 503 504 /50x.html;
  85. location = /50x.html {
  86. root html;
  87. }
  88. }
  89. server {
  90. listen 30001;
  91. server_name localhost;
  92. location / {
  93. root html/reactDist;
  94. index index.html index.htm;
  95. try_files $uri $uri/ /index.html;
  96. error_page 404 /index.html;
  97. add_header 'Access-Control-Allow-Origin' '*';
  98. add_header 'Access-Control-Allow-Credentials' 'true';
  99. add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
  100. add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
  101. }
  102. #error_page 404 /404.html;
  103. # redirect server error pages to the static page /50x.html
  104. #
  105. error_page 500 502 503 504 /50x.html;
  106. location = /50x.html {
  107. root html;
  108. }
  109. }
  110. }

代码地址

https://github.com/WuChenDi/Front-End/tree/master/10-微前端/qiankun