Реализация лендинга Vue + micro front-end (QianKun) и окончательный итог развертывания

внешний интерфейс Vue.js
Реализация лендинга Vue + micro front-end (QianKun) и окончательный итог развертывания

Микро интерфейс (QianKun) Сводка по внедрению и окончательному развертыванию в режиме онлайн

Не прошло и двух месяцев, как я пришел в новую компанию, получил новое требование: «Разделить ERP-систему, включающую в себя модули, в том числе PMS, OMS, WNS и т. д.».

Первой идеей на тот момент был микро-фронтенд, затем следующим шагом была реализация задуманного и осуществление трансформации.

проверить информацию
Первый план реализации провалился
Второй вариант осуществления
Ремонт пола

image.png

задний план

Технологический стек проекта до трансформации — это корзина семейства Vue (vue2.6.10+element2.12.0+webpack4.40.2+vue-cli4.5.7), в которой используются динамические меню, разрешения меню и т. д., а также используется маршрутизация.historyрежиме, поэтому эта статья посвященаVueдоступQianKun.

Концепция микро-фронтенда

  • Типы<iframe></iframe>То же самое, за исключением того, что микро-интерфейс использует fetch для запроса js и рендеринга его в указанном DOM-контейнере.
  • Независимо от стека технологий можно получить доступ к любому стеку интерфейсных технологий.
  • Несколько приложений объединены и могут работать вместе или независимо друг от друга.
  • Сложный и огромный проект разбит на несколько микроприложений, которые разрабатываются, развертываются и тестируются по отдельности, не влияя друг на друга.
  • Принцип заключается в том, чтобы ввести входной файл (main.js) каждого суб-приложения в основное приложение, разобрать его и указать контейнер (DOM) для рендеринга, а затем установить упакованный файл для каждого суб-приложения какUMD, а затем разоблачить в Main.js (export) Метод жизненного цикла (bootstrap,mount,unmount), а потомmountрендеринг, то естьnew Vue(...), И вunmountвоплощать в жизньdestory.

Когда использовать микрофронтенды

  • Аналог ERP-системы.
  • Когда огромную систему нужно разделить на разные команды.
  • В системе много модулей, а в модуле много подмодулей.

QiankunЗнакомство с используемым API

  • registerMicroApps(apps, lifeCycles?)Автоматически блокировать загрузочный модуль, записывать конфигурацию за один раз, передавать ее напрямую, а затем вызыватьstart(),qiankunИспользование миссионерской функции жизненного цикла приложения вызова изменения URL будет автоматически прослушиваться.
  • start(opts?)СотрудничатьregisterMicroAppsиспользуется при вызовеregisterMicroAppsПосле этого запустить start.
  • loadMicroApp(app, configuration?)Чтобы загрузить модуль вручную, вам нужно прослушать URL-адрес и загрузить модуль вручную.
  • addGlobalUncaughtErrorHandler(handler)/removeGlobalUncaughtErrorHandler(handler)Добавить/удалить прослушивание ошибок загрузки приложения.
  • initGlobalState(state)Инициализируйте глобальное общее состояние, аналогичное vuex, и верните три метода, а именноsetGlobalState(state)а такжеonGlobalStateChange((newState, oldState) => {})
    • setGlobalState(state)установить глобальное состояние
    • onGlobalStateChange((newState, oldState) => {})Мониторинг глобальных изменений состояния

Описание параметра приложения:

параметр иллюстрировать Типы Это уникально По умолчанию
name Имя приложения string Y
entry Адрес доступа к приложению, который отличается переменными среды string Y
container Применить узел рендеринга string
activeRule Префикс URL-адреса, активируемый приложением, содержимое после последнего / рекомендуется следоватьnameТо же самое, потому что легко определить, какой маршрут принадлежит какому приложению string Y
loader загрузка приложения (loading) => {}
props Параметры, передаваемые в подприложения string | number | array | array
// apps 应用信息
// name 应用名称(唯一)
// entry 应用访问地址(唯一)
// container 应用渲染节点
// activeRule 应用触发的URL前缀(唯一)
// props 传递给子应用的参数
[
    {
        name: 'pms',
        entry: 'http://localhost:7083/',
        container: '#subView',
        activeRule: '/module/pms',
        loader: (loading) => console.log(loading),
        props: {
            routerBase: '/module/pms', // 子应用的路由前缀(router的base)
            routerList: [...], // 子应用的路由列表
            ...
        }
    },
    ...
]

Начало реализации

Структура проекта

image.png

| -- erp
     | -- .git
     | -- common // 公共模板
     | -- main // 主应用
          | -- package.json
     | -- pms // pms应用
          | -- package.json
     | -- oms // oms应用
          | -- package.json
     | -- tns // tns应用
          | -- package.json
     | -- wns // wns应用
          | -- package.json
     | -- package.json

дизайн маршрутизации

Во-первых, у проекта есть страница входа, но страница входа не загружает дополнительные приложения. Только после успешного входа перейдите на первую страницу, чтобы загрузить дополнительные приложения.

Сначала унифицируйте терминологию:страница авторизации,Стартовая страница

image.png

Здесь мы различаем совместный бег и независимый бег.Давайте сначала поговорим о совместном беге.

бежать вместе

image.png

Совместный запуск означает вход в основное приложение (main) и переход на соответствующую подстраницу после успешного входа.

/login -> страница авторизации

/module/ -> по умолчанию после успешного входаСтартовая страница, здесь судит глобальная защита маршрутизации, решает перейти на этот маршрут, а затем переходит на первый маршрут таблицы маршрутизации по данным, полученным из таблицы маршрутизации; если данных в таблице маршрутизации нет, значит, у пользователя нет меню, поэтому у него нет полномочий. , вернитесь прямо на страницу входа в систему, и приглашение будет в порядке, но все еще зависит от того, как определяются продукты вашей компании.

После успешного входа в основное приложение маршрут сохраняется в глобальном состоянии, за исключением основного приложения.addRouteПомимо добавления маршрутизации, есть два способа работы с динамическими меню подприложений.

  1. После того, как защита маршрутизации получает все меню, она затем маршрутизирует соответствующие подприложения, оценивая префикс.appsнастроенpropsпройти в.
  2. Когда каждое поддержание работает впервые, Global Routing Guard Suders, которые он проходит вместе, напрямую получает таблицу маршрутизации в глобальном состоянии, циклически определяет, принадлежит ли она к маршруту текущего поддержания, а затемaddRouteвходить.

здесьСтартовая страницакомпонент указывает наLayout, динамически загружаемый маршрут будет загружен вLayoutПодмаршрут гарантирует, что при первом входе и запуске микроприложения оно не будет запущено при переходе к маршруту.

теперь, когда/module/Это начальная страница, а затем соединение подстраниц? Вот несколько примеров,

/module/pms/A // pms应用 A页面
/module/pms/B // pms应用 B页面
/module/oms/A // oms应用 A页面

Увидев это, у ваших друзей могут возникнуть вопросы.Префиксы роутинга подприложений в принципе одинаковые.Вам их каждый раз прописывать? Фактически, до тех пор, пока маршрутизация подприложенияbaseПрефикс настройки свойства, например приложение pms, затем установитеbase: '/module/pms'.

new Router({
    base: '/module/pms',
    routes,
    mode: 'history'
})

Работать независимо

Независимая работа означает, что субприложение работает независимо, а страница входа,LayoutБазовый модуль включает в себя меню, выход из системы и может быть разработан и использован в обычном режиме.

В это время страница входа,Layout,AppТри модуля мигрируют в общий модуль посредством введения, затем в соответствии сwindow.__POWERED_BY_QIANKUN__Определите, работает ли текущая операционная среда независимо, и выполните соответствующую логическую обработку.

  • window.__POWERED_BY_QIANKUN__правда, бегом вместе
  • window.__POWERED_BY_QIANKUN__false, запустить автономный
// pms应用 独立运行
/module/pms/login -> 登录页
/module/pms/ -> Layout
/module/pms/A -> A页面
/module/pms/B -> B页面

модификация кода

Подготовьте материалы:

  1. Название приложения, здесь если называетсяpms
  2. Номер порта, чтобы избежать конфликтов с существующими приложениями, такими как 7083
  3. фиксированный префикс, здесь связано с вашим дизайном маршрутизации, я беру/module/

Конфигурация общественного пакета

Публичный пакет в основном предназначен для интеграции некоторых общих модулей, таких какaxios,element ui,dayjs,стиль,store,utils, субприложение можно импортировать напрямую.

Если соответствующий подключаемый модуль установлен в общедоступном пакете, его не нужно повторно устанавливать в подприложении, и его можно импортировать напрямую. пример здесьelement-ui

cd common
npm i element-ui -S
// pms 子应用 main.js
import { Message } from 'common/node_modules/element-ui'
Message('提示内容')
| -- common
     | -- src
          | -- api
          | -- components // 公共组件
          | -- pageg
               | -- layout
               | -- App.vue
          | -- plugins // element、dayjs、v-viewer
          | -- sdk
               | -- fetch.js // axios封装
          | -- store
               | -- commonRegister.js // 动态vuex模块,与onGlobalStateChange结合使用
          | -- styles
          | -- utils
          | -- index.js
     | -- package.json
  1. cd в общий
    • и выполняетnpm init -y, будет генерироватьpackage.jsonдокумент.
    • Измените путь к файлу записи,mainсобственностьsrc/index.js,"main": "src/index.js"
  2. Исправлятьmain.jsСодержимое файла зависит от вашего проекта.
import store from './store'
import plugins from './plugins'
import sdk from './sdk'
import * as utils from './utils'
import globalComponents from './components/global'
import components from './components'
import * as decorator from './utils/decorator'

export { store, plugins, sdk, utils, decorator, globalComponents, components }
  1. commonRegister.jsглобальное состояние

commonRegister.jsСсылаться наПрактика микроинтерфейса qiankun от строительства до развертыванияИнкапсуляция состояния основного приложения в .

// commonRegister.js

/**
 *
 * @param {vuex实例} store
 * @param {qiankun下发的props} props
 * @param {vue-router实例} router
 * @param {Function} resetRouter - 重置路由方法
 */
function registerCommonModule(store, props = {}, router, resetRouter) {
  if (!store || !store.hasModule) {
    return
  }

  // 获取初始化的state
  // eslint-disable-next-line no-mixed-operators
  const initState = (props.getGlobalState && props.getGlobalState()) || {
    menu: null, // 菜单
    user: {}, // 用户
    auth: {}, // token权限
    app: 'main' // 启用应用名,默认main(主应用),区分各个应用下,如果运行的是pms,则是pms,用于判断路由
  }

  // 将父应用的数据存储到子应用中,命名空间固定为common
  if (!store.hasModule('common')) {
    const commonModule = {
      namespaced: true,
      state: initState,
      actions: {
        // 子应用改变state并通知父应用
        setGlobalState({ commit }, payload = {}) {
          commit('setGlobalState', payload)
          commit('emitGlobalState', payload)
        },
        // 初始化,只用于mount时同步父应用的数据
        initGlobalState({ commit }, payload = {}) {
          commit('setGlobalState', payload)
        },
        // 登录
        async login({ commit, dispatch }, params) {
          // ...
          dispatch('setGlobalState')
        },
        // 刷新token
        async refreshToken({ commit, dispatch }) {
          // ...
          dispatch('setGlobalState')
        },
        // 获取用户信息
        async getUserInfo({ commit, dispatch }) {
          // ...
          dispatch('setGlobalState')
        },
        // 登出
        logOut({ commit, dispatch }) {
          to(api.logout())
          commit('setUser')
          commit('setMenu')
          commit('setAuth')
          dispatch('setGlobalState')
          if (router) {
            router && router.replace && router.replace({ name: 'Login' })
          } else {
            window.history.replaceState(null, '', '/login')
          }
          resetRouter && resetRouter() // 重置路由
        },
        // 获取菜单
        async getMenu({ commit, dispatch, state }) {
          // ...
          dispatch('setGlobalState')
        },
        setApp({ commit, dispatch }, appName) {
          commit('setApp', appName)
          dispatch('setGlobalState')
        }
      },
      mutations: {
        setGlobalState(state, payload) {
          // eslint-disable-next-line
          state = Object.assign(state, payload)
        },
        // 通知父应用
        emitGlobalState(state) {
          if (props.setGlobalState) {
            props.setGlobalState(state)
          }
        },
        setAuth(state, data) {
          state.auth = data || {}
          if (data) {
            setToken(data)
          } else {
            removeToken()
          }
        },
        setUser(state, data) {
          state.user = data || {}
        },
        setMenu(state, data) {
          state.menu = data || null
        },
        setApp(state, appName) {
          state.app = appName
        }
      },
      getters: {
          // ...
      }
    store.registerModule('common', commonModule)
  } else {
    // 每次mount时,都同步一次父应用数据
    store.dispatch('common/initGlobalState', initState)
  }
}

Конфигурация вспомогательного приложения

  1. Исправлятьpackage.json:
    • nameсобственностьНазвание приложения.
    • dependenciesдобавить свойство"common": "../common", чтобы ввести общедоступные пакеты.
    • Исправлятьvue.config.jsизpublicPathАтрибутыфиксированный префикс+Название приложения,/module/pms.
    • настраиватьheaderРазрешить запросы из разных источников.
    • представлятьpackage.json,настраиватьpublicPathдляфиксированный префикс+Название приложения,configureWebpack.outputУстановите упакованный формат наUMDУдобствоQiankunИмпорт и настройка общедоступных пакетовcommonУчаствовать в составлении.
      // vue.config.js
      const { name } = require('./package.json')
      module.exports = {
          publicPath: `/module/${name}`, // /module/pms
          devServer: {
              // 端口号配置在环境变量中
              port: process.env.VUE_APP_PORT,
              headers: {
                'Access-Control-Allow-Origin': '*',
                'Cache-Control': 'no-cache',
                Pragma: 'no-cache',
                Expires: 0
            }
          },
          ...
          configureWebpack: {
              output: {
                  // 把子应用打包成 umd 库格式
                  library: `${name}-[name]`,
                  libraryTarget: 'umd',
                  jsonpFunction: `webpackJsonp_${name}`
              }
          },
          // 设置common要参与编译打包(ES6 -> ES5)
          transpileDependencies: ['common']
      }
      
  2. Установите уникальный порт и укажите номер порта в .env.Номер порта не говорит, что он должен быть установлен здесь.Вы также можете установить его в другом месте, в зависимости от дизайна вашего проекта, ноНомер порта должен быть уникальным и не конфликтовать с существующими приложениями.
// .env
VUE_APP_PORT=7083
  1. существуетsrcсоздать новыйpublic-path.jsдокумент
;(function () {
  if (window.__POWERED_BY_QIANKUN__) {
    if (process.env.NODE_ENV === 'development') {
      // eslint-disable-next-line
      __webpack_public_path__ = `//localhost:${process.env.VUE_APP_PORT}${process.env.BASE_URL}`
      return
    }
    // eslint-disable-next-line
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
    // __webpack_public_path__ = `${process.env.BASE_URL}/`
  }
})()

  1. Модернизацияmain.jsдокумент
// main.js
import './public-path'
import Vue from 'vue'
import Router from 'vue-router'
import store from './store'
import common from 'common'
import App from 'common/src/pages/App'

// Vue.use(common.plugins.base, isNotQiankun) // 安装common的Plugins插件
// Vue.use(common.globalComponents) // 全局组件
Vue.use(Router)

const { name: packName } = require('../package.json')
require('@styles/index.scss')

const _import = require('@router/_import_' + process.env.NODE_ENV)
// true:一起运行,false:独立运行
const isNotQiankun = !window.__POWERED_BY_QIANKUN__


Vue.config.productionTip = false
let instance = null

/**
 * 子项目默认初始化
 * @param {Object} props - 主应用传递的参数
 */
function render(props) {
    const { container, routerBase, routerList, name } = props || {}
    // 初始化路由
    const router = new Router({
        base: isNotQiankun ? process.env.BASE_URL : routerBase,
        routes: routerList || [],
        mode: 'history'
    })

    instance = new Vue({
        name,
        router,
        store,
        provide: {
          name: packName,
          isNotQiankun
        },
        render: (h) => h(App) // 公用APP.vue
    }).$mount(container ? container.querySelector('#app') : '#app')
}

// 如果独立运行时,则会执行这里
if (isNotQiankun) {
  // 独立运行时,应该干点什么事

  render()
}

/**
 * qiankun 框架子应用的三个生命周期
 * bootstrap 初始化
 * mount 渲染时
 * unmount 卸载
 */

export async function bootstrap(props) {
    // Vue.prototype.$mainBus = props.bus
}

export async function mount(props) {
    render(props)
}

export async function unmount() {
    instance.$destroy()
    instance.$el.innerHTML = ''
    instance = null
}
  1. Установить глобальную защиту маршрута
// router/config.js
import NProgress from 'common/node_modules/nprogress' // Progress 进度条
import store from '@store'
import { utils } from 'common'
import Layout from 'common/src/pages/layout' // 引入cmmom的layout
const _import = require('@router/_import_' + process.env.NODE_ENV)
const { name } = require('../../package.json')

const isNotQiankun = !window.__POWERED_BY_QIANKUN__

// 路由白名单
const whitelist = ['/login', '/404', '/401', '/']

export default {
    install(router) {
        router.beforeEach(async (to, from, next) => {
            // 这里采用了主应用props传入子应用的方式
            // 一起运行时,路由拦截交给主应去做,子应用不做任何操作,避免冲突
            if (!isNotQiankun) return next()

            // 当独立运行时,执行开启进度条和获取菜单
            NProgress.start()

            // 设置启动应用,也可以在main.js直接设置,感觉这里设置会好一点(神秘加成)
            store.dispatch('common/setApp', name)

            // 进入路由的时白名单时,则直接next
            if (whitelist.includes(to.path)) return next()

            // 没有权限(token),重定向到登录页
            if (!store.getters['common/token']) return next({ path: '/login', replace: true })
            
            // 有菜单时,判断是否启动页(/layout/),是的话,重定向到路由表的第一个
            if (store.getters['common/menu']) { 
                const match = utils.findFirstRoute(store.getters['common/menu'])
                if (!(to.path === '/layout/' && match)) return next()
                const { base } = router.options
                return next({ path: match.path.replace(base, '') })
            } else {
                // 没有路由时,则获取
                const [err, routes] = await utils.to(store.dispatch('common/getMenu'))
                if (err) return next('/login')
                const routerList = utils.filterRouter(routes ? [routes] : [], _import, Layout, 0)
                const { children } = routerList[0]
                children.forEach((e) => {
                    router.addRoute({
                    ...e,
                    path: e.path.startsWith('/') ? e.path : `/${e.path}`
                })
                })
                next({ ...to, replace: true })
                return next()
            }
        })

        router.afterEach(() => {
            isNotQiankun && NProgress.done() // 结束Progress
        })
    }
}

Основная конфигурация приложения

  1. Создано в источникеmicroкаталог, создайте в нем три файла,apps.js,store.jsа такжеindex.js.
// micro/apps.js
import store from './store'
import Vue from 'vue'
import vuexStore from '@store'
import { OPEN_LOADING, CLOSE_LOADING } from '@store/types'
import { utils } from 'common'

// 全局路由前缀
export const MODULE_NAME = 'module'

/**
 * 根据应用名称获取菜单,比如pms
 * @param {string} name - 应用名
 * @returns {array} 应用路列表
 */
function getRoute(name) {
  const routerList = vuexStore.getters['common/menu'] || []
  const childPath = `/${MODULE_NAME}/${name}`
  const match = routerList.find((e) => e.path === childPath)
  if (!match) return []
  return Array.isArray(match.children) ? match.children : []
}

// 是否生产环境
const isProduction = process.env.NODE_ENV === 'production'

/**
 * name: 子应用名称 唯一
 * entry: 子应用路径 唯一
 * container: 子应用渲染容器 固定
 * activeRule: 子应用触发路径 唯一
 * props: 传递给子应用的数据
 */
const apps = [
  {
    name: 'pms',
    entry: 'http://localhost:7083/',
    container: '#subView'
  },
  {
    name: 'oms',
    entry: 'http://localhost:8823/',
    container: '#subView'
  }
]

// {
//   name: 'childTemplate',
//   entry: 'http://localhost:8082/module/childTemplate/',
//   container: '#subView',
//   activeRule: '/module/childTemplate',
//   props: {
//     routerBase: '/module/childTemplate',
//     getGlobalState: store.getGlobalState,
//     components: [MainComponent],
//     utils: {
//       mainFn
//     }
//   }
// }
export default (routerList) =>
  apps.map((e) => ({
    ...e,
    entry: `${isProduction ? '/' : e.entry}${MODULE_NAME}/${e.name}/?t=${utils.rndNum(6)}`,
    activeRule: `/${MODULE_NAME}/${e.name}`,
    // container: `${e.container}-${e.name}`, // KeepAlive
    loader: (loading) => {
      if (loading) {
        vuexStore.commit(`load/${OPEN_LOADING}`)
      } else {
        vuexStore.commit(`load/${CLOSE_LOADING}`)
      }
    },
    props: {
      routerBase: `/${MODULE_NAME}/${e.name}`, // 子应用路由的base
      getGlobalState: store.getGlobalState, // 提供子应用获取公共数据
      routerList: getRoute(e.name, routerList), // 提供给子应用的路由列表
      bus: Vue.prototype.$bus // 主应用Bus通讯
    }
  }))

// micro/store.js
import { initGlobalState } from 'qiankun'
import Vue from 'vue'

// 父应用的初始state
// Vue.observable是为了让initialState变成可响应:https://cn.vuejs.org/v2/api/#Vue-observable。
export const initialState = Vue.observable({
    menu: null,
    user: {},
    auth: {},
    tags: [],
    app: 'main'
})

const actions = initGlobalState(initialState)

actions.onGlobalStateChange((newState, prev) => {
    // console.log('父应用改变数据', newState, prev)
    for (const key in newState) {
    initialState[key] = newState[key]
    }
})

// 自定义一个get获取state的方法下发到子应用
actions.getGlobalState = (key) => {
    // 有key,表示取globalState下的某个子级对象
    // 无key,表示取全部
    return key ? initialState[key] : initialState
}

export default actions
// micro/index.js
import {
  registerMicroApps,
  // setDefaultMountApp,
  start,
  addGlobalUncaughtErrorHandler
} from 'qiankun'
import apps from './apps'
import { Message } from 'common/node_modules/element-ui'
import NProgress from 'common/node_modules/nprogress'
import router from '@router'
import { utils } from 'common'
export default function (routerList) {
  registerMicroApps(apps(routerList), {
    beforeLoad: (app) => {
      // console.log('--------beforeLoad', app)
      NProgress.start()
    },
    beforeMount: (app) => {
      // console.log('--------beforeMount', app)
      // console.log('[LifeCycle] before beforeMount %c%s', 'color: green;', app.name)
    },
    afterMount: (app) => {
      NProgress.done()
      // console.log('-------afterMount', app)
      // console.log('[LifeCycle] before afterMount %c%s', 'color: green;', app.name)
    },
    beforeUnmount: (app) => {
      // console.log('-------beforeUnmount', app)
      // console.log('[LifeCycle] before beforeUnmount %c%s', 'color: green;', app.name)
    },
    afterUnmount: (app) => {
      // console.log('-------afterUnmount', app)
      // console.log('[LifeCycle] after afterUnmount %c%s', 'color: green;', app.name)
    }
  })

  // 监听错误
  addGlobalUncaughtErrorHandler(
    utils.debounce((event) => {
      const { error } = event
      if (error && ~error.message?.indexOf('LOADING_SOURCE_CODE')) {
        Message.error(`${error.appOrParcelName}应用加载失败`)
        router.push({ name: 'Child404' })
      }
    }, 200)
  )

  // 默认加载应用
  // setDefaultMountApp('/module/childTemplate/')

  start()
}

При использовании введениеmicroВот и все.

<template>
    <!-- #subView 就是刚才app里的container -->
    <div
        id="subView"
        v-loading="loading"
        element-loading-text="正在加载子应用中..." />
</template>

<script>
import micro from '@/micro'
import { GET_LOADING } from '@store/types'
export defalt {
    computed: {
        loading() {
            return this.$store.getters[`load/${GET_LOADING}`]
        }
    },
    mounted() {
        // 启动加载微应用
        micro()
    }
}
</script>

Часто задаваемые вопросы и примечания

  1. При загрузке подприложений главное приложение должно сначала записать узел контейнера, а используемые поляappизcontainer, и должен дождаться загрузки узла-контейнера перед запуском микроприложения, то есть поместить его вmountedзапустить в жизненном цикле.

  2. appизname,entry,activeRuleДолжно быть уникальным.

  3. appизentryРекомендуется судить и присваивать значения через переменные среды, потому что при развертывании есть три режима:

    1. Если несколько приложений соответствуют нескольким портам, необходимо разрешить междоменные запросы для микроприложений, поскольку основное приложение получает статические ресурсы подприложения посредством выборки, а затем анализирует информацию о статических ресурсах подприложения. приложение с помощью регулярных выражений, а затем извлекает его, поэтому эти статические ресурсы должны быть необходимы для поддержки междоменного взаимодействия.
    2. Несколько приложений имеют один порт, а путь к подприложению динамически сопоставляется с помощью регулярных выражений.Название приложенияа такжеПриложение запускает / последний символ префикса URLто же самое, то естьappизnameа такжеactiveRuleполе.
    const isProduction = process.env.NODE_ENV === 'production'
    const apps = [
        {
            name: 'pms'
            entry: isProduction ? '/' : 'http://localhost:7083/',
            activeRule: '/module/pms'
            ...
        },
        ...
    ]
    
  4. Глобальная коммуникация состояния, существует несколько способов

    1. vue.observable+initGlobalState(state)+getGlobalState()+ setGlobalState()+onGlobalStateChange(handle)Комбинация методов. пройти черезobservableИнициализируйте данные, сделайте их отзывчивыми, а затем передайтеinitGlobalStateВернуть объект, передать этот объект черезappизpropsпередается вызову подприложения, когдаstateкогда происходят изменения,onGlobalStateChangeбудет реагировать на изменения и вносить изменения, напримерwatch.
    import { initGlobalState } from 'qiankun'
    import Vue from 'vue'
    
    // 父应用的初始state
    // Vue.observable是为了让initialState变成可响应:https://cn.vuejs.org/v2/api/#Vue-observable。
    export const initialState = Vue.observable({
        name: 'xxx'
    })
    
    const actions = initGlobalState(initialState)
    
    actions.onGlobalStateChange((newState, prev) => {
      // console.log('父应用改变数据', newState, prev)
      for (const key in newState) {
        initialState[key] = newState[key]
      }
    })
    
    // 定义一个获取state的方法下发到子应用
    actions.getGlobalState = (key) => {
      return key ? initialState[key] : initialState
    }
    
    // 子应用使用时,类似 setData
    // const state = actions.getGlobalState() // 获取
    // state.name = '4'
    // actions.setGlobalState(state) // 设置
    
    export default actions
    
    1. Модернизация на примере 1, добавлениеvuex+registerModuleДинамический модуль можно расширить, чтобы поместить в него пользовательский модуль (вход в систему, получение токена, получение меню, получение приложения, выход из системы), чтобы каждому приложению не нужно было заново переписывать пользовательский модуль,Посмотреть примерcommonRegister.jsнастроить
  5. Проект перехвата маршрутизации при совместном выполнении передается основному приложению для обработки, при независимом выполнении обрабатывается работающим подприложением, а суждениебежать вместеещеРаботать независимов состоянии пройтиwindow.__POWERED_BY_QIANKUN__оценочное суждение.

  6. Таблица маршрутизации определяет атрибуцию и дает идею, которую можно установить, установивНазвание приложенияа такжеСоответствует содержимому после последнего / префикса URLодинаковы, а затем определить, совпадают ли префиксы.

    {
        name: 'pms' // pms跟下面的pms一样就好了
        activeRule: '/module/pms'
    }
    
  7. Несколько приложений устанавливают узлы монтирования с одинаковым именем (#app), что приводит к ошибке рендеринга. Может быть передан из родительского приложенияpropsсерединаcontainerузел, через этоcontainerищите следующее#app.

    // main.js
    function render(props) {
        const { container, routerBase, routerList, name } = props || {}
        new Vue({
            ...
        }).$mount(container ? container.querySelector('#app') : '#app')
    }
    
  8. commonRegister.jsизinitStateИсходный контент должен быть таким же, как и основное приложениеsrc/micro/store.jsизinitialStateТо же самое, иначе глобальное состояние совместной работы и работы по отдельности не будет совпадать и не может быть непротиворечивым.

  9. vue-devtools не отображает узлы подприложения и не может быть отлажен. На самом деле это связано с тем, что у дочернего приложения нет родительского узла для его наследования, поэтому вы можете установить его вручную.

// main.js
const isNotQiankun = !window.__POWERED_BY_QIANKUN__

/**
 * 子项目默认初始化
 * @param {Object} props - 主应用传递的参数
 */
function render(props) {
    ...省略
    // 解决vue-devtools在qiankun中无法使用的问题
    if (!isNotQiankun && process.env.NODE_ENV === 'development') {
        // vue-devtools  加入此处代码即可
        const instanceDiv = document.createElement('div')
        instanceDiv.__vue__ = instance
        document.body.appendChild(instanceDiv)
    }
}

动画4.gif

  1. Чтобы быстро сгенерировать подприложения, вы можете предварительно собрать шаблон под-приложения childTemplate, а затем использовать скрипт node.js для его генерации.Вам нужно только изменить имя приложения и номер порта, но остальные маршруты и скрипты скриптов нужны для добавления вручную.

code.png

KeepAliveМодернизация

image.png

Переключатель хлебных крошек, управление кешем страниц.

Вот решение, которое было отработано и развернуто в Интернете с использованиемloadMicroAppВручную загрузить реализацию подприложения, не использоватьregisterMicroApps, чтобы не стать Средиземноморьем.

микро интерфейсKeepAliveОн немного отличается от обычного, потому что это проект, который объединяет несколько микроприложений, и их несколько.VueЭкземпляры, поэтому каждое микроприложение должно писать<KeepAlive></KeepAlive>метка, затем вcommonRegister.js,Добавить кtags: []Исходные данные, при добавлении/переключении/удалении ломтиков хлеба необходимо их ввестиpushа такжеsplice.

Потому что после совместной работы, после перехода с приложения pms на приложение oms, если приложение pms использует многоуровневую маршрутизацию, и все равноLayoutпакет внутри компонента<KeepAlive></KeepAlive>Для кэширования в настоящее время остается только последний самый внешний слой.Appузел компонента, только чтоLayoutКэш компонента также исчезнет.

Поскольку в настоящее время адресом маршрутизации является приложение oms, приложение pms не может найти соответствующий компонент с текущим маршрутом, поэтому оно не может сопоставить вторичный маршрут, что приводит кLayoutКомпонент исчезает, что, в свою очередь, приводит к исчезновению кеша.

Изменения маршрута до и после переключения:
Перед переключением: модуль/pms/A
После переключения: модуль/oms/B

Изменения компонентов до и после переключения:
Перед переключением: App - Layout(KeepAlive)
После переключения: приложение

Если маршрут меняется, очевидно, что компоненты не могут быть сопоставлены.

Для автономной работы используйтеLayoutШаблон компонента, который используется здесь<KeepAlive></KeepAlive>

Первый взгляд на эффект преобразования

动画5.gif

Итак, у нас есть следующие идеи преобразования:

Идеи дизайна

  1. Все микроприложения ссылаются на одно и то жеAppкомпоненты и то жеLayoutкомпоненты, можно поставитьAppа такжеLayoutПоместите его в общий пакет.
  2. appизcontainerУстановите его уникальным и циклически визуализируйте его в основном приложении и визуализируйте его в подприложении.
    • Работаем вместе: основное приложение сLayoutЗагрузите все подприложения и преобразуйте все маршруты подприложений в маршруты первого уровня, а затем передайте их основному приложению.Layoutразгромленchildren; подприложениеAppкомпонент включенKeepAlive,LayoutКомпоненты используются только основным приложением.
    // 主应用路由
    const mainRoutes = [
        {
            path: '/module',
            component: Layout,
            children: []
        }
    ]
    
    const childRoutesFlag = [...] // 已经把所有子应用路由转为一级路由
    mainRoutes.[0].children.push(...childRoutesFlag)
    
    • Автономная работа: запустить приложениеAppкомпонент не включенKeepAlive,использоватьLayoutкомпоненты, которые действуют как контейнеры и позволяют им внутриKeepAlive.

общедоступный пакет/src/pages/компонент приложения

<template>
  <div id="app" class="WH">
    <template v-if="!isQiankun">
      <RouterView class="WH app__container" />
    </template>
    <template v-else>
      <Transition name="slide-left" mode="out-in" appear>
        <KeepAlive :include="tags">
          <RouterView class="WH app__container" />
        </KeepAlive>
      </Transition>
    </template>
  </div>
</template>

<script>
// App.vue
export default {
  name: 'APP',
  computed: {
    isQiankun() {
      return window.__POWERED_BY_QIANKUN__
    },
    tags() {
      if (!this.isQiankun) return []
      const tags = this.$store.getters['common/tags']
      const { base } = this.$router.options
      return tags
        .filter((e) => e.path.startsWith(base) && (e.meta || {}).keepAlive === 1)
        .map((e) => {
          const pathSplit = e.path.replace(base, '').split('/').pop() || ''
          return pathSplit
            .replace(/-(\w)/g, ($0, $1) => $1.toUpperCase())
            .replace(/^([a-z]{1})/, ($0) => $0.toUpperCase())
        })
    }
  }
}
</script>

общедоступный пакет/src/pages/компонент макета

<template>
  <div class="layout WH">
    <!-- <LayoutSide class="layout__left" :isCollapse="isCollapse" /> -->
    <div class="layout__right">
      <!-- <LayoutHeader v-model="isCollapse" /> -->
      <template v-if="route.meta.isNotChild || isNotQiankun">
        <ElScrollbar :vertical="false" class="scroll-container">
          <div class="layout__main__container">
            <Transition name="slide-left" mode="out-in" appear>
              <KeepAlive :include="tags">
                <RouterView :key="key" class="WH layout__main__view" />
              </KeepAlive>
            </Transition>
          </div>
        </ElScrollbar>
      </template>
      <Component
        :is="container"
        v-show="container && !isNotQiankun"
        class="layout__container WH"
      ></Component>
    </div>
  </div>
</template>

<script>
// Layout
export default {
  name: 'Layout',
  props: {
    // 渲染子应用的组件,只有在主应用使用时才传入
    // main/router/index.js
    // import ChildContainer from '@components/ChildContainer'
    // {
    //   path: '/module',
    //   component: Layout,
    //   props: {
    //     container: ChildContainer,
    //     isNotQiankun: false
    //   },
    //   children: []
    // }
    container: {
      type: Object,
      default: null
    },
    isNotQiankun: {
      type: Boolean,
      default: true
    }
  },
  inject: {
    isNotQiankun: {
      default: false
    }
  },
  computed: {
    route() {
      return this.$route
    },
    key() {
      return this.$route.fullPath
    },
    tags() {
      const tags = this.$store.getters['common/tags']
      const { base } = this.$router.options
      return tags
        .filter(
          (e) => (e.path.startsWith(base) || this.isNotQiankun) && (e.meta || {}).keepAlive === 1
        )
        .map((e) => {
          const pathSplit = e.path.replace(base, '').split('/').pop() || ''
          return pathSplit
            .replace(/-(\w)/g, ($0, $1) => $1.toUpperCase())
            .replace(/^([a-z]{1})/, ($0) => $0.toUpperCase())
        })
    }
  }
}
</script>

Основной компонент приложения /src/components/ChildContainer, который отображает дочернее приложение.

<template>
  <div
    v-loading="loading"
    :element-loading-text="`正在加载${childName}子应用中...`"
    class="childContainer WH"
  >
    <ElScrollbar ref="scrollContainer" :vertical="false" class="scroll-container">
      <template>
        <div
          v-for="(item, index) in childList"
          v-show="activation.startsWith(item.activeRule)"
          :id="item.container.replace('#', '')"
          :key="index"
          class="sub-content-wrap WH"
        />
      </template>
    </ElScrollbar>
  </div>
</template>

<script>
// 子容器
import apps from '@micro/apps'
import { GET_LOADING, OPEN_LOADING, CLOSE_LOADING } from '@store/types'
import { loadMicroApp } from 'qiankun'
export default {
  name: 'ChildContainer',
  data() {
    return {
      microList: new Map()
    }
  },
  computed: {
    loading() {
      return this.$store.getters[`load/${GET_LOADING}`]
    },
    childList() {
      return apps()
    },
    activation() {
      return this.$route.path || ''
    },
    childName({ activation, childList }) {
      return childList.find((item) => activation.startsWith(item.activeRule))?.name || ''
    }
  },
  watch: {
    activation: {
      immediate: true,
      handler: 'activationHandleChange'
    }
  },
  methods: {
    //  监听路由变化,新增/修改/删除 缓存
    async activationHandleChange(path, oldPath) {
      this.$store.commit(`load/${OPEN_LOADING}`)
      await this.$nextTick()
      const { childList, microList } = this
      const conf = childList.find((item) => path.startsWith(item.activeRule))
      if (!conf) return this.$store.commit(`load/${CLOSE_LOADING}`)
      
      // 如果已经加载过一次,则无需再次加载
      const current = microList.get(conf.activeRule)
      if (current) return this.$store.commit(`load/${CLOSE_LOADING}`)

      // 缓存当前子应用
      const micro = loadMicroApp({ ...conf, router: this.$router })
      microList.set(conf.activeRule, micro)
      micro.mountPromise.finally(() => {
        this.$store.commit(`load/${CLOSE_LOADING}`)
      })
    }
  }
}
</script>

Развертывание Nginx

Вариантов развертывания Nginx три, если нет особых требований, лично я рекомендую третий

  1. У нескольких приложений есть несколько портов, и основное приложение настраивает несколько путей подприложений для переадресации на соответствующие порты подприложений.
    • Преимущество: доступ к дополнительным приложениям возможен по отдельности.
    • Недостатки: каждый раз, когда новое поддержание, вы должны добавить порт-прикладной порт и вперед каждый раз.
    http{
        # main
        server {
            listen       80;
            location / {
                try_files $uri $uri/ /index.html;
                root   /usr/share/nginx/main;
                index  index.html index.htm;
            }
            location /module/pms {
               try_files $uri $uri/ /index.html;
               proxy_pass http://127.0.0.1:8081;
            }
            location /module/oms {
                try_files $uri $uri/ /index.html;
                proxy_pass http://127.0.0.1:8082;
             }
        }
        
        # pms
        server {
            listen       8081;
            location / {
                try_files $uri $uri/ /index.html;
                root   /usr/share/nginx/pms;
                index  index.html index.htm;
            }
        }
        
        # oms
        server {
            listen       8082;
            location / {
                try_files $uri $uri/ /index.html;
                root   /usr/share/nginx/oms;
                index  index.html index.htm;
            }
        }
    }
    
  2. Один порт для нескольких приложений, дополнительные приложения должны быть установлены во вторичном каталоге, и настроено только одно дополнительное приложение.locationДа, но имя каталога должно совпадать с именем основного приложения.LayoutразгромленpathАтрибуты те же, а имя приложения должно совпадать с развернутым каталогом.Например, если есть главное приложение (main), а в подприложениях есть pms и oms, то структура каталогов следующая:
    | -- main
         | -- index.html
    | -- module
         | -- pms
              | -- index.html
         | -- oms
              | -- index.html
    
    • Преимущества: только один порт,locationВсего два, одно основное приложение и одно вспомогательное приложение.
    • Недостатки: ребенок должен был применить в указанном каталоге, полный пакет с командой sh нужно изменить имя и местоположение каталога dist, сложность увеличивается; развертывание программного обеспечения для части эксплуатации и обслуживания, нельзя откатить; невозможно доступ к ребенку в одиночку
    server {
        listen       80;
        location / { # 主应用
            root   /data/web/qiankun/main;
            index index.html;
            try_files $uri $uri/ /index.html;
        }
        
        # ^~ 匹配任何以/module/开头的任何查询并且停止搜索。任何正则表达式将不会被测试。
        # module 必须与 主应用的Layout路由的path 一直
        location ^~ /module/ { # 所有子应用
            alias /data/web/qiankun/module;
            try_files $uri $uri/ /index.html;
        }
    }
    
    
    1. Несколько приложений одного порта, сопоставьте имя суффикса с помощью регулярного выражения, используйтеaliasилиrewriteПерепишите запрос, потребовав, чтобы имя приложения совпадало с развернутым каталогом, который можно установитьvue.config.jsизoutputDirатрибут, измените имя каталога dist.
    Сначала проанализируйте правила запроса
    请求 -> /module/ // 主应用的启动页
    请求 -> /module/pms/A // pms应用 A页面
    请求 -> /module/pms/B // pms应用 B页面
    请求 -> /module/oms/C // oms应用 C页面
    请求 -> /module/oms/D // oms应用 D页面
    
    Согласно приведенным выше правилам, вы можете узнать первый/последующийmoduleфиксируется, второй/позадиНазвание приложения, за третьим / следует конкретный адрес маршрутизации. Следовательно, согласно приведенным выше правилам, для сопоставления и перезаписи запроса можно использовать регулярные выражения.
    • Преимущества: один порт,locationВсего два, одно основное приложение и одно подприложение;locationИспользуйте регулярные выражения для динамического сопоставления и используйтеrewriteДинамически переписать URL-адрес; путь после упаковки сервера является окончательным путем, и каталог не нужно перезаписывать.
    • Минусы: NginxlocationОбычное сопоставление производительности потребляет больше производительности?
    server {
        listen       80;
        location / { # 主应用
            root   /data/web/qiankun/main;
            index index.html;
            try_files $uri $uri/ /index.html;
        }
        
        location ^~ /module/(.*) { # 所有子应用
            try_files $uri $uri/ /index.html;
            if ($1 != "") { # 有值时,则跳到对应的子应用
                alias /data/web/qiankun/$1
                # rewrite "/module/(.*)" "/data/web/qiankun/$1" last;
            } else { # 没有值时,则跳到主应用
                alias /data/web/qiankun/main
                #rewrite "/module/(.*)" "/data/web/qiankun/main" last;
            }
        }
    }
    
    

Ссылка на ссылку

Те, кто читает это, могут проверить обновленную версию

Vue + micro front-end (QianKun) реализация посадки и окончательный итог развертывания (2) нормальная версия

Наконец, набор

база: метро Гуанчжоу-Хайчжу-Модиеша

Технология поп-риса ГуанчжоуМы набираем ~

Позиция Зарплата содержание
Старший фронтенд-инженер 15-35k Ведро семейства Vue+nuxt+flutter+element-ui+vant, в основном цепочка поставок и торговый центр
Средний и старший инженер JAVA 15-35k
Промежуточный и старший PHP-инженер 15-35k
Старший инженер-испытатель 15-30k
Менеджер по продуктам электронной коммерции 20-40k
Менеджер по продуктам цепочки поставок 20-40k
Директор по тестированию 30-50

Присылайте свое резюме на мою электронную почту, и я отправлю его.iikiiklk@qq.com, тема письма: резюме-название-интерфейс прикрепить свое резюме