什么是微前端
微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 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.jsimport 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',// 运行微应用时,基础路由地址配置为 /vuebase: 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.jsimport 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.jsimport 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><Routepath="/"exactrender={() => (<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><aclassName="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.tsimport './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-ignoreapp.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.jsconst 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 确保唯一值```typescriptimport { 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
