记录从零到一的qiankun微前端网站开发,附上线nginx配置

145 阅读3分钟

本文记录一下使用qiankun整合vue2,vu3应用的过程。git仓库地址戳:qiankun-demo

线上访问demo戳 qiankun-demo

一.vue-cli 创建vue2子应用

1.使用 vue-cli 创建项目,勾选上Router。

vue create vue2

2.新建vue.config.js,配置如下:

module.exports = {
    publicPath: '/vue2/', // 配合上线
    productionSourceMap: false,
    assetsDir: 'static',
    devServer: {
        port: 7001,
        headers: {
              'Access-Control-Allow-Origin': '*',
        },
    },
    configureWebpack: {
        output: {
            library: `${name}-[name]`,
            // 把子应用打包成umd库格式
            libraryTarget: 'umd',
            // 注意 webpack 版本问题
            // jsonpFunction: `webpackJsonp_${name}`, // webpack4
            chunkLoading: "jsonp", // //webpack5
        },
    },
}

3.package.json配置全局参数

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

4.子应用路由不能component不能使用import的方式,需要改一下

5.引入element-ui,并且改造下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 = {}) {
    const { container } = props
    instance = new Vue({
        router,
        render: h => h(App)
    }).$mount(container ? container.querySelector('#app') : '#app')
}

// 本地调试
if (!window.__POWERED_BY_QIANKUN__) {
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
    render()
}

// bootstrap,mount,unmount 必须使用 async 函数
export async function bootstrap() {
    console.log('bootstrap')
}

// 挂载子应用
export async function mount(props) {
    console.log(props)
    render(props)
}

// 卸载子应用
export async function unmount() {
    instance.$destroy()
}

二. 使用 vue-cli 创建 vue3 子应用

  1. 使用vue-cli 创建 vue3 应用,和vue2差不多,略

三.使用 vite + vue3 创建主应用

1.使用yarn create vite创建应用,我这边起名 main

2.配置vite.config.js

export default defineConfig({
  plugins: [vue()],
  base: '/qiankun',
  server: {
    port: 7000,
    headers: {
      'Access-Control-Allow-Origin': '*',
    }
  }
})

3.安装qiankun依赖

yarn add qiankun

4.安装vue-router,vue3 使用的是 vue-router4

yarn add vue-router@4

5.新增router.js ,内容大概如下,注:子应用如果vue-router3的版本,并且有在应用内切换的情况,需要拦截主应用路由跳转,不然报错,参考地址:blog.csdn.net/Lidppp/arti…

import { createRouter, createWebHistory } from "vue-router";


const router = createRouter({
    history: createWebHistory('/qiankun'), // 增加主应用路由前缀
    routes: []
})


router.beforeEach((to, from, next) => {
    if(!history.state || !history.state.current) {
      Object.assign(history.state, {current: from.fullPath})
    }
    next()
})
  
export default router

6.main.js 修改

import { registerMicroApps, runAfterFirstMounted, addGlobalUncaughtErrorHandler } from 'qiankun'
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'


createApp(App).use(router).mount('#app')


registerMicroApps(
    [
        {
            name: 'vue2',
            entry: 'http://localhost:7001/vue2',
            container: '#container',
            activeRule: '/qiankun/vue2', // 记得加上主应用路由前缀
        },
        {
            name: 'vue3',
            entry: 'http://localhost:7002/vue3',
            container: '#container',
            activeRule: '/qiankun/vue3', // 记得加上主应用路由前缀
        },
    ],
    {
        beforeLoad: [
            (app) => {
                return Promise.resolve()
            },
        ],
        afterMount: [
            () => {
                return Promise.resolve()
            },
        ],
    }
)


runAfterFirstMounted(() => {
    console.log('子应用加载完毕')
})
/**
 2. 捕获异常
 */
addGlobalUncaughtErrorHandler((event) => {
    console.log(event)
    const { message } = event
    // 加载失败时提示
    if (message && message.includes('died in status LOADING_SOURCE_CODE')) {
        console.log('微应用加载失败_' + msg)
    }
})

7.App.vue增加导航和子应用容器,再加两个导航,onMounted调用qiankun的start方法,开始预加载子应用

<template>
    <ul>
        <li @click="handleNav(item)" class="item" :class="{active: active === item.url}" v-for="item in navs" :key="item.url">{{item.name}}</li>
    </ul>
    <div id="container"></div>
</template>
  
<script setup>
    import { onMounted, ref, watch } from 'vue'
    import { useRoute } from 'vue-router'
    import router from './router'
    const navs = [
        {
            name: '测试vue3 首页',
            url: '/qiankun/vue3#/',
        },
        {
            name: '测试vue3 test',
            url: '/qiankun/vue3#/test',
        },
        {
            name: '测试vue2 Home',
            url: '/qiankun/vue2#/',
        },
        {
            name: '测试vue2 about',
            url: '/qiankun/vue2#/about',
        },
    ]
    const active = ref('')
    const route = useRoute()

    watch(route, (val) => {
        const item = navs.find(item => item.url === val.fullPath)
        if(item) {	
            active.value = item.url
        }
    }, { deep: true })
    
    onMounted(() => {
      start({
        prefetch: true, // 开启预加载
        sandbox: {
          experimentalStyleIsolation: true,
        },
      })
    })
</script>

8.打开http://localhost:7000/qiankun/vue2#/ 查看页面

可切换路由 http://localhost:7000/qiankun/vue2#/about 查看子路由切换的效果

四. 父子应用通讯

4.1.1 使用qiankun提供的 initGlobalState 创建globalState实例,可以调用globalState.onGlobalStateChange 监听全局对象变化,通过globalState.setGlobalState设置全局对象。主应用由vue3开发,使用reactive实现数据监听,新建/src/store.ts

import { initGlobalState } from "qiankun"
import { reactive } from 'vue'

export const globalStateData = reactive({
    userInfo: '',
})

const globalState = initGlobalState(globalStateData)
globalState.onGlobalStateChange((data) => {
    Object.assign(globalStateData, data)
})

export const setGlobalState = (data: any) => {
    Object.assign(globalStateData, data)
    globalState.setGlobalState(data)
}

4.1.2 使用方法:

微信图片_20220718143432.png

4.2.1 vue2子应用使用vuex监听全局对象变化和设置方法,新建/src/store/index.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

let store = null

export function getStore(props = { }) {
    if(props.setGlobalState) {
        const { setGlobalState, onGlobalStateChange } = props
        
        store = new Vuex.Store({
            state: {
                globalData: {},
            },
            mutations: {
                setGlobalState(state, data) {
                    state.globalData = {
                        ...state.globalData,
                        ...data,
                    }
                },
            },
            actions: {
                setGlobalState({ }, data) {
                    setGlobalState(data)
                },
            },
        })

        if(onGlobalStateChange) {
            onGlobalStateChange((data) => {
                store.commit('setGlobalState', data)
            }, true) // 第二个参数 true 代表需要初始化,加载后就会调用一次change,将全局对象赋值
        }
    }
    return store
}

export default store

4.2.2 main.js 改造

import { getStore } from './store'
...
function render(props = {}) {
    const { container } = props
    instance = new Vue({
        router,
        store: getStore(props),
        render: (h) => h(App),
    }).$mount(container ? container.querySelector('#app') : '#app')
}

4.2.3 和正常使用store一样:

// 读取
<div>{{$store.state.globalData.userInfo}}</div>

// 设置
this.$store.dispatch('setGlobalState', {
    userInfo: 'vue2 Login'
})

4.3.1 vue3 子应用使用 ref 保存全局对象,新建/src/store/useGlobalState.js

import { ref } from "vue"

const globalState = ref({})
let setGlobalState = null

function useGlobalState(props = {}) {

    if(props.setGlobalState) {
        setGlobalState = props.setGlobalState
        props.onGlobalStateChange((val) => {
            globalState.value = val
        }, true)
    }

    return {
        globalState,
        setGlobalState,
    }
}

export default useGlobalState

4.3.2 使用:

import useGlobalState from './store/useGlobalState'
const { globalState, setGlobalState } = useGlobalState()

五. 上线,nginx 配置如下

    server {
        listen  80;
        server_name    www.john-online.cn;

        location / {
            root  /web/qiankun/demo/main;
            index  index.html index.htm;
            try_files $uri $uri/ /index.html;
            if ($request_filename ~* .*\.(?:htm|html)$)  { # html不缓存
                add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate";
            }
        }

        location ^~/qiankun {
            alias  /web/qiankun/demo/main;
            index  index.html index.htm;
            try_files $uri $uri/ /index.html; # 使用history模式需要配置
        }

        location ^~/vue2 {
            alias  /web/qiankun/demo/vue2;
            index  index.html index.htm;
        }
        
        location ^~/vue3 {
            alias  /web/qiankun/demo/vue3;
            index  index.html index.htm;
        }
    }

六.遇到的问题

1.父子应用使用不同版本的vue-router导致路由切换报错,参考地址:blog.csdn.net/Lidppp/arti…

2.子应用使用element-ui导致字体文件报404,参考地址:qiankun.umijs.org/zh/faq#为什么微…

3.element-ui 弹窗没有遮罩:可配置 append-to-body: false, 解决