Микро интерфейс (QianKun) Сводка по внедрению и окончательному развертыванию в режиме онлайн
Не прошло и двух месяцев, как я пришел в новую компанию, получил новое требование: «Разделить ERP-систему, включающую в себя модули, в том числе PMS, OMS, WNS и т. д.».
Первой идеей на тот момент был микро-фронтенд, затем следующим шагом была реализация задуманного и осуществление трансформации.
задний план
Технологический стек проекта до трансформации — это корзина семейства 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: [...], // 子应用的路由列表
...
}
},
...
]
Начало реализации
Структура проекта
| -- erp
| -- .git
| -- common // 公共模板
| -- main // 主应用
| -- package.json
| -- pms // pms应用
| -- package.json
| -- oms // oms应用
| -- package.json
| -- tns // tns应用
| -- package.json
| -- wns // wns应用
| -- package.json
| -- package.json
дизайн маршрутизации
Во-первых, у проекта есть страница входа, но страница входа не загружает дополнительные приложения. Только после успешного входа перейдите на первую страницу, чтобы загрузить дополнительные приложения.
Сначала унифицируйте терминологию:страница авторизации,Стартовая страница
Здесь мы различаем совместный бег и независимый бег.Давайте сначала поговорим о совместном беге.
бежать вместе
Совместный запуск означает вход в основное приложение (main) и переход на соответствующую подстраницу после успешного входа.
/login -> страница авторизации
/module/ -> по умолчанию после успешного входаСтартовая страница, здесь судит глобальная защита маршрутизации, решает перейти на этот маршрут, а затем переходит на первый маршрут таблицы маршрутизации по данным, полученным из таблицы маршрутизации; если данных в таблице маршрутизации нет, значит, у пользователя нет меню, поэтому у него нет полномочий. , вернитесь прямо на страницу входа в систему, и приглашение будет в порядке, но все еще зависит от того, как определяются продукты вашей компании.
После успешного входа в основное приложение маршрут сохраняется в глобальном состоянии, за исключением основного приложения.addRoute
Помимо добавления маршрутизации, есть два способа работы с динамическими меню подприложений.
- После того, как защита маршрутизации получает все меню, она затем маршрутизирует соответствующие подприложения, оценивая префикс.
apps
настроенprops
пройти в. - Когда каждое поддержание работает впервые, 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页面
модификация кода
Подготовьте материалы:
-
Название приложения, здесь если называется
pms
- Номер порта, чтобы избежать конфликтов с существующими приложениями, такими как 7083
-
фиксированный префикс, здесь связано с вашим дизайном маршрутизации, я беру
/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
- cd в общий
- и выполняет
npm init -y
, будет генерироватьpackage.json
документ. - Измените путь к файлу записи,
main
собственностьsrc/index.js
,"main": "src/index.js"
- и выполняет
- Исправлять
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 }
-
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)
}
}
Конфигурация вспомогательного приложения
- Исправлять
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'] }
- Исправлять
- Установите уникальный порт и укажите номер порта в .env.Номер порта не говорит, что он должен быть установлен здесь.Вы также можете установить его в другом месте, в зависимости от дизайна вашего проекта, ноНомер порта должен быть уникальным и не конфликтовать с существующими приложениями.
// .env
VUE_APP_PORT=7083
- существует
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}/`
}
})()
- Модернизация
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
}
- Установить глобальную защиту маршрута
// 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
})
}
}
Основная конфигурация приложения
- Создано в источнике
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>
Часто задаваемые вопросы и примечания
-
При загрузке подприложений главное приложение должно сначала записать узел контейнера, а используемые поля
app
изcontainer
, и должен дождаться загрузки узла-контейнера перед запуском микроприложения, то есть поместить его вmounted
запустить в жизненном цикле. -
app
изname
,entry
,activeRule
Должно быть уникальным. -
app
изentry
Рекомендуется судить и присваивать значения через переменные среды, потому что при развертывании есть три режима:- Если несколько приложений соответствуют нескольким портам, необходимо разрешить междоменные запросы для микроприложений, поскольку основное приложение получает статические ресурсы подприложения посредством выборки, а затем анализирует информацию о статических ресурсах подприложения. приложение с помощью регулярных выражений, а затем извлекает его, поэтому эти статические ресурсы должны быть необходимы для поддержки междоменного взаимодействия.
- Несколько приложений имеют один порт, а путь к подприложению динамически сопоставляется с помощью регулярных выражений.Название приложенияа такжеПриложение запускает / последний символ префикса URLто же самое, то есть
app
изname
а такжеactiveRule
поле.
const isProduction = process.env.NODE_ENV === 'production' const apps = [ { name: 'pms' entry: isProduction ? '/' : 'http://localhost:7083/', activeRule: '/module/pms' ... }, ... ]
-
Глобальная коммуникация состояния, существует несколько способов
-
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, добавление
vuex
+registerModule
Динамический модуль можно расширить, чтобы поместить в него пользовательский модуль (вход в систему, получение токена, получение меню, получение приложения, выход из системы), чтобы каждому приложению не нужно было заново переписывать пользовательский модуль,Посмотреть примерcommonRegister.js
настроить
-
-
Проект перехвата маршрутизации при совместном выполнении передается основному приложению для обработки, при независимом выполнении обрабатывается работающим подприложением, а суждениебежать вместеещеРаботать независимов состоянии пройти
window.__POWERED_BY_QIANKUN__
оценочное суждение. -
Таблица маршрутизации определяет атрибуцию и дает идею, которую можно установить, установивНазвание приложенияа такжеСоответствует содержимому после последнего / префикса URLодинаковы, а затем определить, совпадают ли префиксы.
{ name: 'pms' // pms跟下面的pms一样就好了 activeRule: '/module/pms' }
-
Несколько приложений устанавливают узлы монтирования с одинаковым именем (
#app
), что приводит к ошибке рендеринга. Может быть передан из родительского приложенияprops
серединаcontainer
узел, через этоcontainer
ищите следующее#app
.// main.js function render(props) { const { container, routerBase, routerList, name } = props || {} new Vue({ ... }).$mount(container ? container.querySelector('#app') : '#app') }
-
commonRegister.js
изinitState
Исходный контент должен быть таким же, как и основное приложениеsrc/micro/store.js
изinitialState
То же самое, иначе глобальное состояние совместной работы и работы по отдельности не будет совпадать и не может быть непротиворечивым. -
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)
}
}
- Чтобы быстро сгенерировать подприложения, вы можете предварительно собрать шаблон под-приложения childTemplate, а затем использовать скрипт node.js для его генерации.Вам нужно только изменить имя приложения и номер порта, но остальные маршруты и скрипты скриптов нужны для добавления вручную.
KeepAlive
Модернизация
Переключатель хлебных крошек, управление кешем страниц.
Вот решение, которое было отработано и развернуто в Интернете с использованием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>
Первый взгляд на эффект преобразования
Итак, у нас есть следующие идеи преобразования:
Идеи дизайна
- Все микроприложения ссылаются на одно и то же
App
компоненты и то жеLayout
компоненты, можно поставитьApp
а такжеLayout
Поместите его в общий пакет. -
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 три, если нет особых требований, лично я рекомендую третий
- У нескольких приложений есть несколько портов, и основное приложение настраивает несколько путей подприложений для переадресации на соответствующие порты подприложений.
- Преимущество: доступ к дополнительным приложениям возможен по отдельности.
- Недостатки: каждый раз, когда новое поддержание, вы должны добавить порт-прикладной порт и вперед каждый раз.
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; } } }
- Один порт для нескольких приложений, дополнительные приложения должны быть установлены во вторичном каталоге, и настроено только одно дополнительное приложение.
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; } }
- Несколько приложений одного порта, сопоставьте имя суффикса с помощью регулярного выражения, используйте
alias
илиrewrite
Перепишите запрос, потребовав, чтобы имя приложения совпадало с развернутым каталогом, который можно установитьvue.config.js
изoutputDir
атрибут, измените имя каталога dist.
Согласно приведенным выше правилам, вы можете узнать первый/последующийmoduleфиксируется, второй/позадиНазвание приложения, за третьим / следует конкретный адрес маршрутизации. Следовательно, согласно приведенным выше правилам, для сопоставления и перезаписи запроса можно использовать регулярные выражения.请求 -> /module/ // 主应用的启动页 请求 -> /module/pms/A // pms应用 A页面 请求 -> /module/pms/B // pms应用 B页面 请求 -> /module/oms/C // oms应用 C页面 请求 -> /module/oms/D // oms应用 D页面
- Преимущества: один порт,
location
Всего два, одно основное приложение и одно подприложение;location
Используйте регулярные выражения для динамического сопоставления и используйтеrewrite
Динамически переписать URL-адрес; путь после упаковки сервера является окончательным путем, и каталог не нужно перезаписывать. - Минусы: Nginx
location
Обычное сопоставление производительности потребляет больше производительности?
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; } } }
- Преимущества: только один порт,
Ссылка на ссылку
- Практика микроинтерфейса qiankun от строительства до развертывания
- Краткое изложение практики qiankun micro front-end (2)
- Думая о коммуникации между приложениями микро-фронтенда qiankun Vue
- Практика микроинтерфейса qiankun от строительства до развертывания
- Практика и краткое изложение решения qiankun micro front-end
- Опыт работы с микроинтерфейсом (qiankun)
- На основе посадки qiankun развертывание "ямы" для скалолазания с микро-передним концом.
Те, кто читает это, могут проверить обновленную версию
Наконец, набор
база: метро Гуанчжоу-Хайчжу-Модиеша
Технология поп-риса ГуанчжоуМы набираем ~
Позиция | Зарплата | содержание |
---|---|---|
Старший фронтенд-инженер | 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, тема письма: резюме-название-интерфейс прикрепить свое резюме