您现在的位置是:首页 >技术杂谈 >qiankun + Vite + React + Vue + Angular 快速构建前端微服务网站首页技术杂谈

qiankun + Vite + React + Vue + Angular 快速构建前端微服务

aibujin 2024-06-17 10:43:18
简介qiankun + Vite + React + Vue + Angular 快速构建前端微服务


一、主应用 vite

npm

 npm create vite@latest

yarn

 yarn create vite

选择是否继续

Need to install the following packages:
  create-vite@3.2.1
Ok to proceed? (y) y

项目名称

Project name: » vite-project

选择框架

Select a framework: » - Use arrow-keys. Return to submit.
>   Vanilla
    Vue
    React
    Preact
    Lit
    Svelte
    Others

选择项目语言

Select a variant: » - Use arrow-keys. Return to submit.
>   TypeScript
    JavaScript
    Customize with create-vue ↗
    Nuxt ↗

项目构建成功,根据提示进入项目目录,安装依赖

Done. Now run:

  cd vite-project
  npm install
  npm run dev

在这里插入图片描述
安装 qiankun

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

src 目录新增 micro-apps.ts 用来注册微应用

registerMicroApps 参数中的 props 用来编写需要传递给微应用的信息,但是 props 中不可以用( name、entry、container、activeRule) 这几个字段名,否则微应用无法收到主应用传递的信息

import { registerMicroApps, start } from 'qiankun';

const lifeCycles = {
    beforeLoad: (app: any) => {
        console.log("before load app.name====>>>>>", app)
        return Promise.resolve(app)
    },
    beforeMount: (app: any) => {
        console.log("before mount app.name====>>>>>", app)
        return Promise.resolve(app)
    },
    afterMount: (app: any) => {
        console.log("after mount app.name====>>>>>", app)
        return Promise.resolve(app)
    }
}

const register = () => {
    registerMicroApps([
        {
            name: 'vueApp',
            entry: '//localhost:8080',
            container: '#custelement',
            activeRule: '/app-vue',
            props: {
                state: 'props-app-vue',
            },
        },
        {
            name: 'reactApp',
            entry: '//localhost:3000',
            container: '#custelement',
            activeRule: '/app-react',
            props: {
                state: 'props-app-react',
            },
        },
        {
            name: 'angularApp',
            entry: '//localhost:4200',
            container: '#custelement',
            activeRule: '/app-angular',
            props: {
                state: 'props-app-angular',
            },
        }
    ], lifeCycles);
}

export default {
    register,
    start,
}

src 目录新增 globalState.ts 用来编写 initGlobalStateinitGlobalState()qiankun 提供的通信 API)

import { initGlobalState } from 'qiankun';
export function globalState() {
    const { onGlobalStateChange, setGlobalState, offGlobalStateChange } = initGlobalState({
        user: 'init-qiankun'
    })

    onGlobalStateChange((value, prev) =>
        console.log('[onGlobalStateChange - master]:', value, prev)
    )

    setGlobalState({
        user: 'vite-project'
    })
    offGlobalStateChange();
}

微应用从生命周期 mount 中获取通信方法,微应用中只能修改已存在的一级属性(eg: 主应用 setGlobalState ({ user: ‘’ }),微应用只能设置{ user: ‘’ })

export function mount(props) {
  props.onGlobalStateChange((state, prev) => {
    // state: 变更后的状态; prev 变更前的状态
    console.log(state, prev);
  });

  props.setGlobalState({ user: 'react project state' });
}

注意:主应用除了微应用之外,如果还包含有新的业务组件,那么就需要配置对应路由,但是微应用存放容器 container: '#custelement' 和 <router-view /> ,应该处同一层级;否则当微应用渲染之后在跳转至别的组件,组件无法渲染

<div class="content">
     <router-view />
     <div id="custelement"></div>
</div>

跳转页面使用:history

window.history.pushState({}, '',)

除了 registerMicroApps 之外, qiankun 还支持手动加载微应用 loadMicroApp(eg:可以在某个组件挂载微应用)

<template>
    <div>
        <div id="microElemnet"> </div>
    </div>
</template>

<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue'
import { loadMicroApp } from 'qiankun'

var microApp: any = null

onMounted(() => {
    microApp = loadMicroApp({
        name: 'vueApp',
        entry: '//localhost:8080',
        container: '#microElemnet',
    })
})

// 卸载微应用
onUnmounted(() => {
    if (microApp) {
        microApp.unmount()
    }
})
</script>

注意:手动加载微应用时,微应用的 window.__POWERED_BY_QIANKUN__ 也需要与之对应

// 假设当前组件路径为 

http://localhost:8888/nav/list 

// 微应用 window.__POWERED_BY_QIANKUN__

base: window.__POWERED_BY_QIANKUN__ ? '/nav/list' : '/',

使用 loadMicroApp 手动加载微应用,页面跳转用路由,不需要使用 history

二、微应用 react

安装脚手架

npm install -g create-react-app

创建项目

npx create-react-app react-project

项目构建成功,根据提示进入项目目录,启动项目

 cd react-project
 npm start

src 目录新增 public-path.js 文件,在入口文件最顶部引入 public-path.js

if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

设置 history 模式路由的 base

<BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? '/app-react' : '/'}>

入口文件 index.js 修改,为了避免根 id #root 与其他的 DOM 冲突,需要限制查找范围

import './public-path';
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

var root = null
function render(props) {
    console.log(ReactDOM)
    const { container } = props;
    root = container ? ReactDOM.createRoot(container.querySelector('#root')) : ReactDOM.createRoot(document.querySelector('#root'))
    root.render(
        <React.StrictMode>
            <App />
        </React.StrictMode>
    );
}

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

export async function bootstrap() {
    console.log('[react16] react app bootstraped');
}

export async function mount(props) {
    console.log('[react16] props from main framework', props);

    props.onGlobalStateChange((state, prev) => {
        // state: 变更后的状态; prev 变更前的状态
        console.log(state, prev);
    });

    props.setGlobalState({ user: 'react project state' });


    render(props);
}

export async function unmount(props) {
    root.unmount()
}

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

修改 webpack 配置

安装插件 react-app-rewired

npm i react-app-rewired

根目录新增 config-overrides.js

const { name } = require('./package');

module.exports = {
    webpack: (config) => {
        config.output.library = `${name}-[name]`;
        config.output.libraryTarget = 'umd';
        // config.output.jsonpFunction = `webpackJsonp_${name}`;
        config.output.globalObject = 'window';

        return config;
    },

    devServer: (_) => {
        const config = _;

        config.headers = {
            'Access-Control-Allow-Origin': '*',
        };
        config.historyApiFallback = true;
        config.hot = false;
        config.watchContentBase = false;
        config.liveReload = false;

        return config;
    },
};

修改 package.json

-   "start": "react-scripts start",
+   "start": "react-app-rewired start",
-   "build": "react-scripts build",
+   "build": "react-app-rewired build",
-   "test": "react-scripts test",
+   "test": "react-app-rewired test",
-   "eject": "react-scripts eject"

启动项目, public-path.js__ webpack_public_path__ 未定义错误

在这里插入图片描述
这是 eslint 的问题, webpack_public_path 不是全局变量所导致的

解决方式 :子应用 package.json 文件中 eslintConfig 配置全局变量,重起服务

"eslintConfig": {
    ...,
    "globals": {
      "__webpack_public_path__": true
    }
}

三、微应用 vue

安装脚手架

npm install -g @vue/cli

创建项目

vue create vue-project

选择手动配置

Vue CLI v5.0.8
? Please pick a preset:             // 选择配置
  Default ([Vue 3] babel, eslint)  // vue2 默认配置
  Default ([Vue 2] babel, eslint)  // vue3 默认配置
> Manually select features         // 手动配置

项目配置(按空格键选择或取消选择)

 Check the features needed for your project: (Press <space> to select, <a> to toggle all, <i> to invert selection, and <enter> to proceed)
 (*) Babel                               // 代码 es6 转 es5
 ( ) TypeScript                          // TypeScript
 ( ) Progressive Web App (PWA) Support   
 (*) Router                              // 路由
>(*) Vuex                                // Vuex
 (*) CSS Pre-processors                  // css 预处理器
 (*) Linter / Formatter                  // eslint 代码格式规范
 ( ) Unit Testing                        // 单元测试
 ( ) E2E Testing                         // 端到端测试

选择 vue 版本

Choose a version of Vue.js that you want to start the project with
  3.x
> 2.x 

第二步如果选择路由,将会有这一步,是否用 history 模式创建路由

Use history mode for router? (Requires proper server setup for index fallback in production) No

第二步如果选择CSS预处理器,将会有这一步,选择CSS预处理器

Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): (Use arrow keys)
> Sass/SCSS (with dart-sass)
  Less
  Stylus  

语法检查工具

Pick a linter / formatter config: (Use arrow keys)
> ESLint with error prevention only
  ESLint + Airbnb config
  ESLint + Standard config
  ESLint + Prettier  

什么时候校验代码格式

Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection, and <enter> to proceed)
>(*) Lint on save              // 保存代码时
 ( ) Lint and fix on commit    // 提交代码时

Babel、ESLint 等配置文件存放位置

Where do you prefer placing config for Babel, ESLint, etc.? (Use arrow keys)
> In dedicated config files   // 独立文件放置
  In package.json             // 放在package.json里

是否当前项目配置作为后续项目的预设?

Save this as a preset for future projects? (y/N) N 

项目构建成功,根据提示进入项目目录,启动项目

 cd vue-project
 npm run serve

在这里插入图片描述
src 目录新增 public-path.js 文件,在入口文件最顶部引入 public-path.js

if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

修改路由, router/index.js

import HomeView from '../views/HomeView.vue'
import AboutView from '../views/AboutView.vue'

const routes = [
    {
        path: '/',
        redirect: '/home'
    },
    {
        path: '/home',
        name: 'home',
        component: HomeView
    },
    {
        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: AboutView
    }
]

export default routes

修改入口文件 main.js

import './public-path';
import Vue from 'vue'
import App from './App.vue'
import VueRouter from 'vue-router'
import routes from './router'

Vue.use(VueRouter)

Vue.config.productionTip = false

let router = null;
let instance = null;
function render(props = {}) {
    const { container } = props;
    router = new VueRouter({
        base: window.__POWERED_BY_QIANKUN__ ? '/app-vue' : '/',
        mode: 'history',
        routes,
    });

    instance = new Vue({
        router,
        render: (h) => h(App),
    }).$mount(container ? container.querySelector('#app') : '#app');
}

// 独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
    render();
}

export async function bootstrap() {
    console.log('[vue] vue app bootstraped');
}
export async function mount(props) {
    console.log('[vue] props from main framework', props);
    props.onGlobalStateChange &&
        props.onGlobalStateChange(
            (value, prev) =>
                console.log(`[onGlobalStateChange - ${props.name}]:`, value, prev),
            true
        )
    props.setGlobalState &&
        props.setGlobalState({
            user: 'vue-project'
        })
    render(props);
}
export async function unmount() {
    instance.$destroy();
    instance.$el.innerHTML = '';
    instance = null;
    router = null;
}

打包配置修改 vue.config.js

const { defineConfig } = require('@vue/cli-service')
const { name } = require('./package');
module.exports = defineConfig({
    transpileDependencies: true,
    devServer: {
        headers: {
            'Access-Control-Allow-Origin': '*',
        },
    },
    configureWebpack: {
        output: {
            library: `${name}-[name]`,
            libraryTarget: 'umd', // 把微应用打包成 umd 库格式
            // jsonpFunction: `webpackJsonp_${name}`,  // webpack5不兼容  npm i webpack@4.44.0 可以下载低版本
            globalObject: 'window'
        },
    },
})

四、微应用 angular

安装脚手架

npm install -g @angular/cli

查是否安装成功

ng v
ng version

在这里插入图片描述
创建项目

ng new angular-project

注意: 创建的时候有可能会报错,类似下面这种, 如果没有请忽略;

The Angular CLI requires a minimum Node.js version of either v14.2.0,v16.13 or v18.10.

表示本地 node 和 Angular 版本不对应, 根据提示下载对应版本就行

在这里插入图片描述

是否添加路由

Would you like to add Angular routing? Yes

选择 CSS 预编译器

Which stylesheet format would you like to use?
  CSS
> SCSS   [ https://sass-lang.com/documentation/syntax#scss                ]
  Sass   [ https://sass-lang.com/documentation/syntax#the-indented-syntax ]
  Less   [ http://lesscss.org   

项目构建成功,切换到项目目录,启动项目

 cd angular-project
 ng serve --open

src 目录新增 public-path.js 文件,在入口文件最顶部引入 public-path.js

if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

设置 history 模式路由的 basesrc/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 = [];

declare const window: any;

@NgModule({
    imports: [RouterModule.forRoot(routes)],
    exports: [RouterModule],
    providers: [{ provide: APP_BASE_HREF, useValue: window.__POWERED_BY_QIANKUN__ ? '/app-angular' : '/' }]
})
export class AppRoutingModule { }

修改入口文件,src/main.ts 文件

import './public-path';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { NgModuleRef } from '@angular/core';
import { AppModule } from './app/app.module';

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();
}

修改 webpack 打包配置

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

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

在根目录增加 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,将 [packageName] > architect > build > builder[packageName] > architect > serve > builder 的值改为我们安装的插件,将我们的打包配置文件加入到 [packageName] > architect > build > options

- "builder": "@angular-devkit/build-angular:browser",
+ "builder": "@angular-builders/custom-webpack:browser",
  "options": {
+    "customWebpackConfig": {
+      "path": "./custom-webpack.config.js"
+    }
  }
- "builder": "@angular-devkit/build-angular:dev-server",
+ "builder": "@angular-builders/custom-webpack:dev-server",

解决 zone.js 的问题

父应用 安装zone.js

npm i zone.js

父应用 引入,需要在import qiankun 之前引入

import 'zone.js/dist/zone';

微应用src/polyfills.ts 里面的引入 zone.js 代码删掉(新版本已经没有 polyfills.ts 文件了,修改 angular.json,将 [packageName] > architect > build > options > polyfills 配置为空即可 )

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

<!-- 也可以使用其他的CDN/本地的包 -->
<script src="https://unpkg.com/zone.js" ignore></script>

修正 ng build 打包报错问题,修改 tsconfig.json 文件

- "target": "es2015",
+ "target": "es5",
+ "typeRoots": [
+   "node_modules/@types"
+ ],

为了防止主应用或其他微应用也为 anuglar 时, <app-root></app-root> 会冲突的问题,建议给 <app-root> 加上一个唯一的 id ,比如说当前应用名称

src/index.html :

- <app-root></app-root>
+ <app-root id="angular15"></app-root>

src/app/app.component.ts :

- selector: 'app-root',
+ selector: '#angular15 app-root',

五、项目地址

项目地址:https://github.com/aibuijn/qiankun

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。