什么是微前端
微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 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
$ yarn add qiankun
# or
$ 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([
{
}, {name: "vueApp",
entry: "//localhost:10001",
container: "#vue",
activeRule: "/vue",
props: { a: 1 },
}, ]);name: "reactApp",
entry: "//localhost:20001",
container: "#react",
activeRule: "/react",
start({ prefetch: false, });
new Vue({ router, render: (h) => h(App), }).$mount(“#app”);
- 添加点击跳转路由链接
```vue
<!-- App.vue -->
<template>
<div>
<el-menu :router="true" mode="horizontal">
<el-menu-item index="/">Home</el-menu-item>
<el-menu-item index="/vue">vue</el-menu-item>
<el-menu-item index="/react">react</el-menu-item>
</el-menu>
<router-view />
<div id="vue"></div>
<div id="react"></div>
</div>
</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(); }
- router.js router base 改变基础路由
```javascript
// src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
Vue.use(VueRouter)
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')
}
]
const router = new VueRouter({
mode: 'history',
// 运行微应用时,基础路由地址配置为 /vue
base: window.__POWERED_BY_QIANKUN__ ? "/vue" : process.env.BASE_URL,
routes
})
export default router
- 修改config文件 webpack打包策略
```javascript
// vue.config.js
module.exports = {
devServer: {
// 配置跨域请求头,解决开发环境的跨域问题port: 10001,
}, configureWebpack: {headers: {
"Access-Control-Allow-Origin": "*",
},
}, };output: {
// 微应用的包名,这里与主应用中注册的微应用名称一致
library: "vueApp",
// 将你的 library 暴露为所有的模块定义下都可运行的方式
libraryTarget: "umd",
},
<a name="SwvjB"></a>
## React 子应用
- index.js 导出相应的生命周期钩子
```javascript
// src/index.js
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
function render() {
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
);
}
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
export async function bootstrap() {}
export async function mount(props) {
render();
}
export async function unmount(props) {
ReactDOM.unmountComponentAtNode(document.getElementById("root"));
}
- 新增
.env
文件,添加配置 ```javascript PORT=20001 WDS_SOCKET_PORT=20001
- 引入 `react-router-dom` ,测试路由
```jsx
// src/App.js
import logo from "./logo.svg";
import "./App.css";
import { BrowserRouter, Route, Link } from "react-router-dom";
function App() {
return (
<BrowserRouter basename="/react">
<div className="nav">
<Link to="/">index</Link> |
<Link to="/about">about</Link>
</div>
<Route
path="/"
exact
render={() => (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
)}
></Route>
<Route path="/about" render={() => <h1>about</h1>}></Route>
</BrowserRouter>
);
}
export default App;
- 引入
react-app-rewired
修改 webpack打包策略 ```javascript // config-overrides.js module.exports = { webpack: (config) => { // 微应用的包名,这里与主应用中注册的微应用名称一致
// 将你的 library 暴露为所有的模块定义下都可运行的方式config.output.library = "reactApp";
}, devServer: (configFunction) => {config.output.libraryTarget = "umd";
config.output.publicPath = "http://localhost:20001";
return config;
}, };return function (proxy, allowedHost) {
const config = configFunction(proxy, allowedHost);
config.headers = {
// 配置跨域请求头,解决开发环境的跨域问题
"Access-Control-Allow-Origin": "*",
};
return config;
};
- 修改package.json启动命令
```javascript
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-app-rewired eject"
},
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; }
- main.js 导出相应的生命周期钩子
```typescript
// /src/main.ts
import './public-path';
import { enableProdMode, NgModuleRef } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
let app: void | NgModuleRef<AppModule>;
async function render() {
app = await platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch((err) => console.error(err));
}
if (!(window as any).__POWERED_BY_QIANKUN__) {
render();
}
export async function bootstrap(props: Object) {
console.log(props);
}
export async function mount(props: Object) {
render();
}
export async function unmount(props: Object) {
console.log(props);
// @ts-ignore
app.destroy();
}
- 修改路由文件配置 ```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 { }
- 引入 `@angular-builders/custom-webpack` 修改 webpack打包策略,新增 `custom-webpack.config.js` 文件,添加配置
```javascript
// /custom-webpack.config.js
const appName = require("./package.json").name;
module.exports = {
devServer: {
headers: {
"Access-Control-Allow-Origin": "*",
},
},
output: {
library: `${appName}-[name]`,
libraryTarget: "umd",
jsonpFunction: `webpackJsonp_${appName}`,
},
};
- 修改
angular.json
```json { “$schema”: “./node_modules/@angular/cli/lib/config/schema.json”, “version”: 1, “newProjectRoot”: “projects”, “projects”: { “qiankun-angular”: {
} }, “defaultProject”: “qiankun-angular” }"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
},
"@schematics/angular:application": {
"strict": true
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-builders/custom-webpack:browser",
"options": {
"outputPath": "dist/qiankun-angular",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"aot": true,
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": [],
"customWebpackConfig": {
"path": "./custom-webpack.config.js"
}
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
]
}
}
},
"serve": {
"builder": "@angular-builders/custom-webpack:dev-server",
"options": {
"browserTarget": "qiankun-angular:build"
},
"configurations": {
"production": {
"browserTarget": "qiankun-angular:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "qiankun-angular:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": []
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"tsconfig.app.json",
"tsconfig.spec.json",
"e2e/tsconfig.json"
],
"exclude": [
"**/node_modules/**"
]
}
},
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "qiankun-angular:serve"
},
"configurations": {
"production": {
"devServerTarget": "qiankun-angular:serve:production"
}
}
}
}
- 解决 `zone.js`
```javascript
// src/polyfills.ts
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
*
* Learn more in https://angular.io/guide/browser-support
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/**
* IE11 requires the following for NgClass support on SVG elements
*/
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/**
* Web Animations `@angular/platform-browser/animations`
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
*/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags
* because those flags need to be set before `zone.js` being loaded, and webpack
* will put import in the top of bundle, so user need to create a separate file
* in this directory (for example: zone-flags.ts), and put the following flags
* into that file, and then add the following code before importing zone.js.
* import './zone-flags';
*
* The flags allowed in zone-flags.ts are listed here.
*
* The following flags will work for all browsers.
*
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
*
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge
*
* (window as any).__Zone_enable_cross_context_check = true;
*
*/
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
// import 'zone.js/dist/zone'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/
- 引入zone
```html
<!doctype html>
- 添加 ID 确保唯一值
```typescript
import { Component } from '@angular/core';
@Component({
selector: '#angularapp app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
title = 'qiankun-angular';
}
- 修改 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”: [
], “module”: “es2020”, “lib”: ["node_modules/@types"
] }, “angularCompilerOptions”: { “enableI18nLegacyMessageIdFormat”: false, “strictInjectionParameters”: true, “strictInputAccessModifiers”: true, “strictTemplates”: true } }"es2018",
"dom"
<a name="x453K"></a>
# Nginx 配置
> 'Access-Control-Allow-Origin' '*';
> 生成环境 这里需要改成具体域名地址
```nginx
# nginx.conf
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
server {
listen 8080;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root html/static;
index index.html index.htm;
try_files $uri $uri/ /index.html;
error_page 404 /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;
}
}
server {
listen 10001;
server_name localhost;
location / {
root html/vueDist;
index index.html index.htm;
try_files $uri $uri/ /index.html;
error_page 404 /index.html;
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
}
#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;
}
}
server {
listen 20001;
server_name localhost;
location / {
root html/reactDist;
index index.html index.htm;
try_files $uri $uri/ /index.html;
error_page 404 /index.html;
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
}
#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;
}
}
server {
listen 30001;
server_name localhost;
location / {
root html/reactDist;
index index.html index.htm;
try_files $uri $uri/ /index.html;
error_page 404 /index.html;
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
}
#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;
}
}
}
代码地址
https://github.com/WuChenDi/Front-End/tree/master/10-微前端/qiankun