微前端架構設計和實踐:vue+qiankn

> 版權聲明:本文爲博主原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接和本聲明。  
> 本文鏈接:[https://gudepeng.github.io/note/2020/05/13/wqd2/](https://gudepeng.github.io/note/2020/05/13/wqd2/)

## 一.前情提示

上一篇貼帖子已經講了什麼是微前端,不是太瞭解的同學們可以查看[微前端架構設計和實踐:由來](https://juejin.im/post/5e8077caf265da47c916ead3})  
本文案例會持續更新成爲一個開源管理項目,歡迎 star,如果需要支持和需要新特性可以聯繫我  
項目地址:[https://github.com/gudepeng/vue-giant](https://github.com/gudepeng/vue-giant)  
qiankug 官方鏈接:[https://github.com/umijs/qiankun](https://github.com/umijs/qiankun)  
本文使用的是 qiankun2.0 之後的版本和方法
![系統界面](https://user-gold-cdn.xitu.io/2020/5/13/1720e5335f252654?w=1920&h=978&f=gif&s=705413)

## 二.實戰

### 1.創建項目

因爲主項目和子項目都是用的 vue 開發,所以使用 vue-cli4 創建項目,創建 2 個項目,一個主項目,一個子項目。

```
npm install -g @vue/cli @vue/cli-service-global
vue create vue-giant-master
vue create vue-giant-module
```

### 2.主項目編寫

下面正常開始編寫項目,我之前也在網上看了好多 demo 和實際項目,大家主項目都是直接把乾坤的代碼放在 main 裏面。然後在 app.vue 中用 v-if 去控制是路由顯示還是加載的子項目顯示。因爲是後臺管理項目,在我實際的項目開發中,主項目的路由頁面頁比較多,所以感覺這種方式不是太方便。所以我使用了正常 vue 項目啓動,然後在登錄後的主頁面加載 qiankun 的代碼。下面是具體實現。

```
<template>
  <div class="panel" @click="doOnWeb" @keydown="doOnWeb">
    <top-header class="panel-heder"></top-header>
    <div class="panel-main">
      <!-- 根據當前路由地址判斷是子項目頁面,還是主項目頁面進行選擇 -->
      <router-view v-if="showView" />
      <div v-else id="root-view"></div>
    </div>
    <main-menu ref="mainMenu" class="main-menu" v-show="showMenu"></main-menu>
    <main-login ref="mainLogin"></main-login>
  </div>
</template>

<script>
import TopHeader from '@/layout/components/Header'
import MainMenu from '@/layout/components/Menu'
import MainLogin from '@/layout/components/MainLogin'
// 導入乾坤函數
import { registerMicroApps, start } from 'qiankun'
import axios from '@/utils/request'

export default {
  name: 'Layout',
  components: {
    TopHeader,
    MainMenu,
    MainLogin
  },
  data() {
    return {
      showMenu: false
    }
  },
  computed: {
    showView: function() {
      return this.$route.path === '/home'
    }
  },
  mounted() {
    // 定義傳入子應用的數據,方法和組件
    const msg = {
      data: this.$store.getters,
      fns: [],
      prototype: [{ name: '$axios', value: axios }]
    }
    // 註冊子應用,可以根據登錄後的權限加載對應的子項目
    registerMicroApps(
      [
        {
          name: 'module-app1',
          entry: '//localhost:8081',
          container: '#root-view',
          activeRule: '/app1',
          props: msg
        },
        {
          name: 'module-app2',
          entry: '//localhost:8082',
          container: '#root-view',
          activeRule: '/app2',
          props: msg
        }
      ],
      {
        beforeLoad: [
          app => {
            console.log('before load', app)
          }
        ],
        beforeMount: [
          app => {
            console.log('before mount', app)
          }
        ],
        afterUnmount: [
          app => {
            console.log('after unload', app)
          }
        ]
      }
    )

    // 啓動微服務
    start({ prefetch: true })
  },
  methods: {
    toggleMenu() {
      this.showMenu = !this.showMenu
      this.$refs['mainMenu'].showTwoMenu = false
    },
    doOnWeb() {
      this.$refs.mainLogin.doOnWeb()
    }
  }
}
</script>
```

這麼做的話,就可以很好的區分子項目和主項目的加載了。而且也可以有共同的部署顯示(例如 menu 和 title)

#### 注意實現

1.路由要使用 history 模式,並且主項目所有頁面的跳轉要使用 window.history.pushState()方法,否則會出現子項目沒註銷掉,然而頁面上子項目的容器#root-view 被 v-if 刪除掉的情況,這樣 qiankun 就會報錯,之後在跳轉到子項目頁面就會不進行加載。

2.在往子項目傳遞方法或者組件的時候,不要直接使用[function]數組內直接放方法的方式,因爲在實際部署打包的時候這樣的方法名會被 webpack 打包壓縮,子項目中接受到的方法名就會不對,子項目調用主項目的方法時就會找不到這個方法。

3.在傳遞數據 data 的時候,本文使用傳遞 store 的 getter,然後子項目接受到值得時候初始到子項目的 store 中,當主項目有值變化的時候後使用 qiankun 提供的 initGlobalState 傳遞(在 qiankun 1.x 的版本的使用 qiankun 不支持加載子項目後的通信,我使用的是綁定 window 的 event 事件的方式傳值),在子項目中在更新到 store 中。

我也嘗試過直接把 store 傳遞到子項目中,實際情況也會傳遞到,這樣主項目和子項目就相當於公用 store 了,但是子項目中的頁面不會監聽 store 的變化而變化,需要自己手動使用\$forceUpdate(),我大概看了下 vuex 的源碼,好像是因爲他只創建了一個 dom 變化的監聽的原因,具體還沒有深入查看,之後會做詳細調查,如果有直接的小夥伴們,可以留言告訴我小。

#### router 的設計

```
import router from '../router'
import store from '../store'
import { Message } from 'element-ui'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import { getUserToken } from '@/utils/user'
import Layout from '@/layout/index.vue'

NProgress.configure({
  showSpinner: false
})

router.beforeEach(async (to, from, next) => {
  NProgress.start()
  const hasToken = getUserToken()
  if (hasToken) {
    if (to.path === '/login') {
      next({
        path: '/home'
      })
      NProgress.done()
    } else {
      const isLogin = store.getters.userInfo
      if (isLogin) {
        next()
      } else {
        try {
          // 獲取到實際有什麼子項目後再加入到router權限中
          await store.dispatch('user/getUserInfo')
          router.addRoutes([
            {
              path: '/',
              name: 'Layout',
              component: Layout,
              children: [
                {
                  path: 'app1*'
                }
              ]
            }
          ])
          next({
            ...to,
            replace: true
          })
        } catch (error) {
          Message.error(error || 'Has Error')
          next(`/login?redirect=${to.path}`)
          NProgress.done()
        }
      }
    }
  } else {
    if (to.path === '/login') {
      next()
      NProgress.done()
    } else {
      next({
        path: '/login'
      })
    }
  }
})

router.afterEach(() => {
  NProgress.done()
})

```

當用戶沒有登錄的時候都攔截到登錄頁面,當登錄過的時候判斷獲取過權限沒,如果沒有獲取過權限,去獲取權限,然後添加對應子項目的路由到 router 中。  
因爲我的子項目頁面是在佈局頁面下面,所以沒有一個子項目的權限就要添加下對應權限

```
router.addRoutes([
    {
        path: '/',
        name: 'Layout',
        component: Layout,
        children: [
        {
            path: 'app1*'
        }
        ]
    }
])
```

### 子項目編寫

子項目其實就是個普通的 vue 項目編寫,main.js 中接收到方法和組件掛載到 vue.prototype 上,接收到的 data 初始化到 store 中。

```
import './public-path'
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import './plugins/element.js'
import '@/utils/permission.js'

Vue.config.productionTip = false

let instance = null

export async function bootstrap({ prototype }) {
  prototype.map(p => {
    Vue.prototype[p.name] = p.value
  })
}

function initStore(props) {
  props.onGlobalStateChange && props.onGlobalStateChange((value, prev) => {})
  props.setGlobalState &&
    props.setGlobalState({
      ignore: props.name,
      user: {
        name: props.name
      }
    })
}

function render(props = {}) {
  instance = new Vue({
    router,
    store,
    render: h => h(App)
  }).$mount('#app')
}

export async function mount(props) {
  initStore(props)
  if (props.data.userInfo.roles) {
    store.commit('permission/SET_ROLES', props.data.userInfo.roles)
  }
  render()
}

export async function unmount() {
  instance.$destroy()
  instance = null
}

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

```

需要在 main.js 同級目錄創建 public-path.js

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

##### router 的設計

因爲主項目已經把權限傳遞到子項目中。在初始化路由的時候只需要權限加載對象的路由即可

```
import router from '@/router'
import store from '@/store'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'

NProgress.configure({ showSpinner: false })

router.beforeEach(async (to, from, next) => {
  NProgress.start()

  const hasRoles = store.getters.routes && store.getters.routes.length > 0

  if (hasRoles) {
    next()
  } else {
    // get user info
    const roles = await store.state.permission.roles
    const accessRoutes = await store.dispatch(
      'permission/generateRoutes',
      roles
    )
    router.addRoutes(accessRoutes)
    next({ ...to, replace: true })
  }
})

router.afterEach(() => {
  NProgress.done()
})

```

#### vue.config.js 修改

需要在其中添加文件的輸出

```
output: {
    library: `${name}-[name]`,
    libraryTarget: 'umd',
    jsonpFunction: `webpackJsonp_${name}`
}
```

因爲底層使用的是 jsonp 的方式獲取的子項目的文件,所以子項目需要支持跨域

```
headers: {
      'Access-Control-Allow-Origin': '*'
    }
```

### 其他

以上就是整個項目的核心代碼,其他部分爲具體 vue 項目的開發方法,本文就不詳細說明了。
例如:用戶隔一段時間不操作就會彈出 login 的 dialog,菜單等功能
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章