什么是微前端
微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。
qiankun
qiankun 是一个基于 single-spa 的微前端实现库,生产可用微前端架构系统。
特性
- 基于 single-spa 封装,提供了更加开箱即用的 API。
- 技术栈无关,任意技术栈的应用均可 使用/接入,不论是 React/Vue/Angular/JQuery 还是其他等框架。
- HTML Entry 接入方式,让你接入微应用像使用 iframe 一样简单。
- 样式隔离,确保微应用之间样式互相不干扰。
- JS 沙箱,确保微应用之间 全局变量/事件 不冲突。
- 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
- umi 插件,提供了 @umijs/plugin-qiankun 供 umi 应用一键切换成微前端架构系统。
搭建使用
Base 基座搭建
bash
$ 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,
// 配置跨域请求头,解决开发环境的跨域问题
headers: {
'Access-Control-Allow-Origin': '*',
},
},
configureWebpack: {
output: {
// 微应用的包名,这里与主应用中注册的微应用名称一致
library: 'vueApp',
// 将你的 library 暴露为所有的模块定义下都可运行的方式
libraryTarget: 'umd',
},
},
}
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) => {
// 微应用的包名,这里与主应用中注册的微应用名称一致
config.output.library = 'reactApp'
// 将你的 library 暴露为所有的模块定义下都可运行的方式
config.output.libraryTarget = 'umd'
config.output.publicPath = 'http://localhost:20001'
return config
},
devServer: (configFunction) => {
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: APP_BASE_HREF,
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": {
"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"
}
}
}
}
}
},
"defaultProject": "qiankun-angular"
}
- 解决
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
<!-- src/index.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>NgApp</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<script
src="https://cdn.bootcdn.net/ajax/libs/zone.js/0.11.4/zone.js"
ignore
></script>
</head>
<body>
<app-root id="angularapp"></app-root>
</body>
</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": ["node_modules/@types"],
"module": "es2020",
"lib": ["es2018", "dom"]
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}
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