Практика микроинтерфейса qiankun от строительства до развертывания

внешний интерфейс внешний фреймворк

Недавно ответственный за новый проект, используемыйqiankun, напишите статью, чтобы поделиться некоторыми проблемами и мыслями, возникшими в реальном бою.

Образец кода:GitHub.com/Удары ветра/….

Онлайн-демонстрация:qiankun.fengxianqi.com/

Чтобы получить доступ к дополнительным онлайн-приложениям по отдельности:

Зачем использовать цянькунь

У проекта есть функциональное требование, которое требует внедрения существующего инструмента внутри компании, который самостоятельно развертывается и используется.ReactНаписана, и основная техническая подборка нашего проектаvue, поэтому нужно рассмотреть вариант встраивания страницы. Есть два основных пути:

  • iframeстроить планы
  • qiankunМикро интерфейсное решение

Оба варианта соответствуют нашим потребностям и осуществимы. должен сказать,iframeХотя схема и распространена, она очень практична и малозатратна.iframeРешение может покрыть большинство бизнес-потребностей микроинтерфейса иqiankunТехнические требования выше.

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

Архитектура проекта

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

обратитесь к официальномуexamplesКод, есть пьедестал в корневом каталоге проектаmainи другие вспомогательные приложенияsub-vue,sub-react, исходная структура каталогов после построения выглядит следующим образом:


├── common     //公共模块
├── main       // 基座
├── sub-react  // react子应用
└── sub-vue    // vue子应用

база используетсяvueСборка, суб-приложения естьreactа такжеvue.

Базовая конфигурация

Основная база построена с помощью Vue-Cli3. Он отвечает только за отрисовку навигации и доставку состояния входа. Он предоставляет смонтированный контейнер div для подприложения. База должна быть простой (официальная демонстрация Qiankun даже использует встроенный HTML-код напрямую) и не должен выполнять операции, связанные с бизнесом.

Библиотеку qiankun нужно только внедрить в базу, вmain.jsРегистрируем суб-приложения в , для облегчения управления выносим конфигурацию суб-приложений в:main/src/micro-app.jsВниз.

const microApps = [
  {
    name: 'sub-vue',
    entry: '//localhost:7777/',
    activeRule: '/sub-vue',
    container: '#subapp-viewport', // 子应用挂载的div
    props: {
      routerBase: '/sub-vue' // 下发路由给子应用,子应用根据该值去定义qiankun环境下的路由
    }
  },
  {
    name: 'sub-react',
    entry: '//localhost:7788/',
    activeRule: '/sub-react',
    container: '#subapp-viewport', // 子应用挂载的div
    props: {
      routerBase: '/sub-react'
    }
  }
]

export default microApps

затем вsrc/main.jsвведен в

import Vue from 'vue';
import App from './App.vue';
import { registerMicroApps, start } from 'qiankun';
import microApps from './micro-app';

Vue.config.productionTip = false;

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


registerMicroApps(microApps, {
  beforeLoad: app => {
    console.log('before load app.name====>>>>>', app.name)
  },
  beforeMount: [
    app => {
      console.log('[LifeCycle] before mount %c%s', 'color: green;', app.name);
    },
  ],
  afterMount: [
    app => {
      console.log('[LifeCycle] after mount %c%s', 'color: green;', app.name);
    }
  ],
  afterUnmount: [
    app => {
      console.log('[LifeCycle] after unmount %c%s', 'color: green;', app.name);
    },
  ],
});

start();

существуетApp.vue, нужно объявитьmicro-app.jsКонфигурированное поднесение монтирует div (обратите внимание, что идентификатор должен быть одинаковым), а базовый макет связан, вероятно, подобно этому:

<template>
  <div id="layout-wrapper">
    <div class="layout-header">头部导航</div>
    <div id="subapp-viewport"></div>
  </div>
</template>

Таким образом даже настраивается база. После запуска проекта субприложение будет смонтировано на<div id="subapp-viewport"></div>середина.

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

1. Подприложение Vue

Создайте новый в корневом каталоге проекта с помощью Vue-cli.sub-vueдочернее приложение, имя дочернего приложения должно совпадать с именем родительского приложения.src/micro-app.jsНастроенное имя является согласованным (чтобы его можно было использовать напрямуюpackage.jsonсерединаnameкак выход).

  1. новыйvue.config.js, порт devServer изменен на такой же, как у основной конфигурации приложения, и добавлен междоменныйheadersа такжеoutputконфигурация.
// package.json的name需注意与主应用一致
const { name } = require('../package.json')

module.exports = {
  configureWebpack: {
    output: {
      library: `${name}-[name]`,
      libraryTarget: 'umd',
      jsonpFunction: `webpackJsonp_${name}`,
    }
  },
  devServer: {
    port: process.env.VUE_APP_PORT, // 在.env中VUE_APP_PORT=7788,与父应用的配置一致
    headers: {
      'Access-Control-Allow-Origin': '*' // 主应用获取子应用时跨域响应头
    }
  }
}
  1. новыйsrc/public-path.js
(function() {
  if (window.__POWERED_BY_QIANKUN__) {
    if (process.env.NODE_ENV === 'development') {
      // eslint-disable-next-line no-undef
      __webpack_public_path__ = `//localhost:${process.env.VUE_APP_PORT}/`;
      return;
    }
    // eslint-disable-next-line no-undef
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
  }
})();
  1. src/router/index.jsТолько вместо путей воздействия,new RouterИзменить наmain.jsзаявление в.
  2. Модернизацияmain.js, импортируя вышеуказанноеpublic-path.js, переписать рендер, добавить функции жизненного цикла и т.д., конечный результат такой:
import './public-path' // 注意需要引入public-path
import Vue from 'vue'
import App from './App.vue'
import routes from './router'
import store from './store'
import VueRouter from 'vue-router'

Vue.config.productionTip = false
let instance = null

function render (props = {}) {
  const { container, routerBase } = props
  const router = new VueRouter({
    base: window.__POWERED_BY_QIANKUN__ ? routerBase : process.env.BASE_URL,
    mode: 'history',
    routes
  })
  instance = new Vue({
    router,
    store,
    render: (h) => h(App)
  }).$mount(container ? container.querySelector('#app') : '#app')
}

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

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

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

  render(props)
}

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

На этом этапе базовая версия подприложения vue настроена, еслиrouterа такжеvuexОн не нужен и его можно удалить.

2. Реагировать на субприложение

  1. пройти черезnpx create-react-app sub-reactСоздайте новое реагирующее приложение.
  2. новый.envдобавление файлаPORTпеременная, номер порта совпадает с номером, настроенным родительским приложением.
  3. для того, чтобы неejectВся конфигурация веб-пакета, которую мы используемreact-app-rewiredРешение состоит в том, чтобы скопировать webpack.
  • первыйnpm install react-app-rewired --save-dev
  • новыйsub-react/config-overrides.js
const { name } = require('./package.json');

module.exports = {
  webpack: function override(config, env) {
    // 解决主应用接入后会挂掉的问题:https://github.com/umijs/qiankun/issues/340
    config.entry = config.entry.filter(
      (e) => !e.includes('webpackHotDevClient')
    );
    
    config.output.library = `${name}-[name]`;
    config.output.libraryTarget = 'umd';
    config.output.jsonpFunction = `webpackJsonp_${name}`;
    return config;
  },
  devServer: (configFunction) => {
    return function (proxy, allowedHost) {
      const config = configFunction(proxy, allowedHost);
      config.open = false;
      config.hot = false;
      config.headers = {
        'Access-Control-Allow-Origin': '*',
      };
      return config;
    };
  },
};
  1. новыйsrc/public-path.js.
if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
  1. Модернизацияindex.jsВступление кpublic-path.js, добавить функции жизненного цикла и т. д.
import './public-path'
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

function render() {
  ReactDOM.render(
    <App />,
    document.getElementById('root')
  );
}

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

/**
 * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
 * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
 */
export async function bootstrap() {
  console.log('react app bootstraped');
}
/**
 * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
 */
export async function mount(props) {
  console.log(props);
  render();
}
/**
 * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
 */
export async function unmount() {
  ReactDOM.unmountComponentAtNode(document.getElementById('root'));
}
/**
 * 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
 */
export async function update(props) {
  console.log('update props', props);
}

serviceWorker.unregister();

На этом этапе настроена базовая версия реактивного подприложения.

Передовой

глобальное управление состоянием

qiankunпройти черезinitGlobalState, onGlobalStateChange, setGlobalStateРеализуйте глобальное управление состоянием основного приложения, а затем передайте значение по умолчанию.propsСпособ передачи связи с поддержанием. Посмотрите на пример официального использования:

Основное приложение:

// main/src/main.js
import { initGlobalState } from 'qiankun';
// 初始化 state
const initialState = {
  user: {} // 用户信息
};
const actions = initGlobalState(initialState);
actions.onGlobalStateChange((state, prev) => {
  // state: 变更后的状态; prev 变更前的状态
  console.log(state, prev);
});
actions.setGlobalState(state);
actions.offGlobalStateChange();

Дополнительное приложение:

// 从生命周期 mount 中获取通信方法,props默认会有onGlobalStateChange和setGlobalState两个api
export function mount(props) {
  props.onGlobalStateChange((state, prev) => {
    // state: 变更后的状态; prev 变更前的状态
    console.log(state, prev);
  });
  props.setGlobalState(state);
}

Эти два фрагмента кода несложно понять, приложение родитель-потомок проходитonGlobalStateChangeЭтот метод взаимодействует, что на самом деле является шаблоном проектирования публикации-подписки.

хорошо, официальное использование примера очень простое и вполне достаточное, чистый синтаксис JavaScript, не включает никаких Vue или React, и разработчики могут свободно настраивать его.

Если мы используем официальный пример напрямую, данные будут свободными и сложными для вызова, и все подприложения должны будут объявитьonGlobalStateChangeКонтролировать статус, а затем пройтиsetGlobalStateдля обновления данных.

Поэтому необходимо намДальнейший дизайн инкапсуляции состояния данных. В основном автор рассматривает следующие моменты:

  • Главное приложение должно быть простым и понятным.Для подприложений данные, отправляемые основным приложением, являются очень чистыми.objectЧтобы лучше поддержать различные рамки под приложениями, и поэтому не нужно использовать основное приложениеvuex.
  • Дочернее приложение Vue должно наследовать данные, отправленные родительским приложением, и поддерживать независимую работу.

вложенные приложения вmountЦикл объявления может получить последние данные, выданные основным приложением, а затем зарегистрировать эти данные вglobalВ модуле vuex дочернее приложение обновляет данные с помощью действия глобального модуля и автоматически синхронизируется с родительским приложением при обновлении.

Поэтому для подприложенийОн не знает, является ли он дочерним приложением qiankun или автономным приложением, он просто назвалglobalМодуль, который может обновлять данные через действия, и больше не нужно заботиться о том, чтобы синхронизироваться с родительским приложением(Синхронное действие будет инкапсулировано внутри метода, вызывающей стороне это не нужно), это также для более позднегоПоддержка подприложений для независимого начала разработки.

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

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

Основное приложение поддерживаетinitialStateисходные данные, т.objectтип, который будет доставлен в подприложение.

// main/src/store.js

import { initGlobalState } from 'qiankun';
import Vue from 'vue'

//父应用的初始state
// Vue.observable是为了让initialState变成可响应:https://cn.vuejs.org/v2/api/#Vue-observable。
let initialState = Vue.observable({
  user: {},
});

const actions = initGlobalState(initialState);

actions.onGlobalStateChange((newState, prev) => {
  // state: 变更后的状态; prev 变更前的状态
  console.log('main change', JSON.stringify(newState), JSON.stringify(prev));

  for (let key in newState) {
    initialState[key] = newState[key]
  }
});

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

export default actions;

Здесь следует отметить две вещи:

  • Vue.observableЭто делается для того, чтобы сделать состояние родительского приложения отзывчивым.Если вы не используете Vue.observable для переноса слоя, это просто чистый объект, и дочернее приложение также может получить его, но оно потеряет отзывчивость.Означает, что страница не будет обновляться после изменения данных.
  • getGlobalStateметод, этоспорныйДа, у всех есть обсуждение на гитхабе:GitHub.com/UfanAccept/Kg….

С одной стороны, автор считает, чтоgetGlobalStateне обязательно,onGlobalStateChangeНа самом деле, это было достаточно.

С другой стороны, автор и другие студенты, которые упомянули PR, сочли необходимым предоставитьgetGlobalStateapi, причина в том, что метод get более удобен в использовании, а подприложению не нужно постоянно следить за событием stateChange, его нужно только один раз инициализировать через getGlobalState при первом монтировании. Здесь автор настаивает на своем собственном мнении, позволяя родительскому приложению вызывать метод getGlobalState.

Так как getGlobalState пока официально не поддерживается, необходимо явно выдать этот метод через пропсы при регистрации подприложений:

import store from './store';
const microApps = [
  {
    name: 'sub-vue',
    entry: '//localhost:7777/',
    activeRule: '/sub-vue',
  },
  {
    name: 'sub-react',
    entry: '//localhost:7788/',
    activeRule: '/sub-react',
  }
]

const apps = microApps.map(item => {
  return {
    ...item,
    container: '#subapp-viewport', // 子应用挂载的div
    props: {
      routerBase: item.activeRule, // 下发基础路由
      getGlobalState: store.getGlobalState // 下发getGlobalState方法
    },
  }
})

export default microApps

Инкапсуляция состояния подприложений vue

Как упоминалось ранее, дочернее приложение будет регистрировать состояние, выданное родительским приложением, в качествеglobalМодуль vuex, для удобства повторного использования, инкапсулируем:

// sub-vue/src/store/global-register.js

/**
 * 
 * @param {vuex实例} store 
 * @param {qiankun下发的props} props 
 */
function registerGlobalModule(store, props = {}) {
  if (!store || !store.hasModule) {
    return;
  }

  // 获取初始化的state
  const initState = props.getGlobalState && props.getGlobalState() || {
    menu: [],
    user: {}
  };

  // 将父应用的数据存储到子应用中,命名空间固定为global
  if (!store.hasModule('global')) {
    const globalModule = {
      namespaced: true,
      state: initState,
      actions: {
        // 子应用改变state并通知父应用
        setGlobalState({ commit }, payload) {
          commit('setGlobalState', payload);
          commit('emitGlobalState', payload);
        },
        // 初始化,只用于mount时同步父应用的数据
        initGlobalState({ commit }, payload) {
          commit('setGlobalState', payload);
        },
      },
      mutations: {
        setGlobalState(state, payload) {
          // eslint-disable-next-line
          state = Object.assign(state, payload);
        },
        // 通知父应用
        emitGlobalState(state) {
          if (props.setGlobalState) {
            props.setGlobalState(state);
          }
        },
      },
    };
    store.registerModule('global', globalModule);
  } else {
    // 每次mount时,都同步一次父应用数据
    store.dispatch('global/initGlobalState', initState);
  }
};

export default registerGlobalModule;

main.jsДобавьте использование глобального модуля в:

import globalRegister from './store/global-register'

export async function mount(props) {
  console.log('[vue] props from main framework', props)
  globalRegister(store, props)
  render(props)
}

Видно, что модуль vuex будет вызываться при монтировании субприложения.initGlobalStateИнициализируйте состояние, выданное родительским приложением, и предоставьтеsetGlobalStateМетод предназначен для внешних вызовов, а внутреннее автоматическое уведомление синхронизируется с родительским приложением. Подприложение используется на странице vue следующим образом:

export default {
  computed: {
    ...mapState('global', {
      user: state => state.user, // 获取父应用的user信息
    }),
  },
  methods: {
    ...mapActions('global', ['setGlobalState']),
    update () {
    	this.setGlobalState('user', { name: '张三' })
    }
  },
};

Этим достигается эффект: дочернему приложению не нужно знать о существовании qiankun, оно знает только, что есть такой глобальный модуль, который может хранить информацию, а общение между родителем и дочерним элементом инкапсулировано в самом методе, и он заботится только о своем собственном хранении информации.

ps: у этого решения также есть недостатки, потому что дочернее приложение будет синхронизировать состояние, выданное родительским приложением, только при его монтировании. Поэтому он подходит только для архитектуры монтирования только одного подприложения за раз (не подходит для сосуществования нескольких подприложений); если данные родительского приложения изменяются, а подприложение не запускает монтирование, самое последнее данные родительского приложения не могут быть синхронизированы обратно с подприложением. Для обеспечения сосуществования нескольких подприложений и динамической передачи дочерних элементов родительским приложением по-прежнему необходимо использовать программное обеспечение, предоставленное qiankun.onGlobalStateChangeМониторинг API в порядке, и студенты, у которых есть лучшее решение, могут поделиться им и обсудить его. Это решение просто соответствует текущим потребностям автора, поэтому этого достаточно. Пожалуйста, инкапсулируйте его в соответствии с потребностями вашего бизнеса.

Переключение подприложений Загрузка обработки

Когда подприложение загружается в первый раз, это эквивалентно новой загрузке проекта, который все еще относительно медленный, поэтому необходимо добавить загрузку.

официальный примерЕсть обработка загрузки, но ее нужно внедрить дополнительноimport Vue from 'vue/dist/vue.esm', что увеличит размер упаковки основного приложения (для сравнения установлено, что он увеличился примерно на 100 КБ). Загрузка увеличилась на 100K, очевидно, что стоимость немного неприемлема, поэтому необходимо рассмотреть лучший способ.

Наше основное приложение построено с помощью vue, а qiankun предоставляетloaderМетод может получить статус загрузки подприложения, поэтому естественно думать о: ** вmain.jsКогда приложение Neutron загружается, состояние загрузки передается экземпляру Vue, чтобы экземпляр Vue отображал загрузку в ответ. **Далее сначала выберите загружаемый компонент:

  • Если основное приложение использует ElementUI или другие фреймворки, вы можете напрямую использовать компонент загрузки, предоставляемый библиотекой пользовательского интерфейса.
  • Если в основном приложении нет библиотеки пользовательского интерфейса, чтобы упростить его, вы можете самостоятельно написать загрузочный компонент или найти небольшую загрузочную библиотеку, например ту, которую автор использовал здесь.NProgress.
npm install --save nprogress

Следующий шаг — выяснить, как передать состояние загрузки основному приложению.App.vue. После того, как я нашел судnew VueВозвращенный метод экземпляр Vue может быть переданinstance.$children[0]изменитьApp.vueДанные, поэтому преобразуйтеmain.js:

// 引入nprogress的css
import 'nprogress/nprogress.css'
import microApps from './micro-app';

// 获取实例
const instance = new Vue({
  render: h => h(App),
}).$mount('#app');

// 定义loader方法,loading改变时,将变量赋值给App.vue的data中的isLoading
function loader(loading) {
  if (instance && instance.$children) {
    // instance.$children[0] 是App.vue,此时直接改动App.vue的isLoading
    instance.$children[0].isLoading = loading
  }
}

// 给子应用配置加上loader方法
let apps = microApps.map(item => {
  return {
    ...item,
    loader
  }
})
registerMicroApps(apps);

start();

PS: метод registerMicroApps qiankun также отслеживает жизненные циклы подприложений, таких как beforeLoad и afterMount, поэтому эти методы также можно использовать для записи состояния загрузки, но для лучшего использования необходимо передать параметр загрузчика.

Измените App.vue основного приложения и слушайте через часыisLoading

<template>
  <div id="layout-wrapper">
    <div class="layout-header">头部导航</div>
    <div id="subapp-viewport"></div>
  </div>
</template>

<script>
import NProgress from 'nprogress'
export default {
  name: 'App',
  data () {
    return {
      isLoading: true
    }
  },
  watch: {
    isLoading (val) {
      if (val) {
        NProgress.start()
      } else {
        this.$nextTick(() => {
          NProgress.done()
        })
      }
    }
  },
  components: {},
  created () {
    NProgress.start()
  }
}
</script>

В этот момент достигается эффект загрузки. несмотря на то чтоinstance.$children[0].isLoadingОперация .

Извлечь общедоступный код

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

Создайте новый в корневом каталогеcommonПапка используется для хранения общедоступного кода, такого как вышеупомянутые несколько субприложений vue, которые могут быть общими.global-register.js, или многоразовыйrequest.jsа такжеsdkфункции инструмента и т. д. Код не опубликован здесь, пожалуйста, прочитайте его напрямуюdemo.

Как после извлечения общего кода его могут использовать другие приложения? Вы можете опубликовать общий как частный пакет npm.Частный пакет npm имеет следующие организационные формы:

  • npm указывает на адрес локального файла:npm install file:../common. Создайте новый общий каталог прямо в корневом каталоге, а затем npm напрямую зависит от пути к файлу.
  • npm указывает на частный репозиторий git:npm install git+ssh://xxx-common.git.
  • PW для публикации npm.

Эта демонстрация - это потому, что подводное основание и набор приложений на репозитории GIT, поэтому используя первый метод, но практическое применение публикуется на NPM PW, потому что позже мы распределяем суб-базу и приложения независимые подкаклады, Поддержка независимого развития, будет отмечена позже.

Следует отметить, что, поскольку common не проходит через babel и pollfy, реферер должен явно указать, что модуль должен быть скомпилирован при упаковке webpack.Например, vue.config.js подприложения vue должен быть добавить это предложение:

module.exports = {
  transpileDependencies: ['common'],
}

Подприложения поддерживают независимую разработку

Очень важной концепцией микро-фронтенда является разделение, то есть идея «разделяй и властвуй», разделение всего бизнеса на независимые и работоспособные модули.

С точки зрения разработчика, вся система может иметь N подприложений.Если вся система запущена, то она может быть очень медленной и медленной, а определенное требование продукта может затрагивать только одно из подприложений, поэтому во время разработки необходимо запускать только задействованные субприложения.Достаточно субприложений, а целенаправленная разработка может быть начата независимо, поэтому необходимо поддерживать независимую разработку субприложений. Если вы хотите получить поддержку, вы в основном столкнетесь со следующими проблемами:

  • Как сохранить статус входа в суб-приложения?
  • Когда база не запускается, как я могу получить данные и возможности, отправленные базой?

Когда база работает, статус входа в систему и информация о пользователе сохраняются на базе, а затем база отправляет их в подприложения через реквизиты. Однако, если база не запускается, а самостоятельно запускается только субприложение, то субприложение не может получить требуемую информацию о пользователе через пропсы. Следовательно, решение может состоять только в том, что и родительские, и дочерние приложения должны реализовать один и тот же набор логики входа. Для повторного использования вы можете инкапсулировать общую логику входа в систему, а затем добавить логику, связанную с входом, в логику, которую субприложение выполняет независимо.

// sub-vue/src/main.js

import { store as commonStore } from 'common'
import store from './store'

if (!window.__POWERED_BY_QIANKUN__) {
  // 这里是子应用独立运行的环境,实现子应用的登录逻辑
  
  // 独立运行时,也注册一个名为global的store module
  commonStore.globalRegister(store)
  // 模拟登录后,存储用户信息到global module
  const userInfo = { name: '我是独立运行时名字叫张三' } // 假设登录后取到的用户信息
  store.commit('global/setGlobalState', { user: userInfo })
  
  render()
}
// ...
export async function mount (props) {
  console.log('[vue] props from main framework', props)

  commonStore.globalRegister(store, props)

  render(props)
}
// ...

!window.__POWERED_BY_QIANKUN__Указывает, что субприложение находится вqiankunсреда внутри, то есть автономная среда выполнения. На данный момент нам еще предстоит зарегистрироватьglobalВ модуле vuex подприложение также может получать информацию о пользователе из глобального модуля, чтобы сгладить разницу в среде между qiankun и независимой средой выполнения.

PS: то, что мы писали ранееglobal-register.jsОн умно написан и может поддерживать обе среды одновременно, поэтому вышеперечисленное может быть пропущено черезcommonStore.globalRegisterЦитируйте прямо.

Независимый репозиторий подприложения

По мере развития проекта суб-приложений может быть все больше и больше.Если суб-приложения и база будут собраны в одном git-репозитории, то он будет становиться все более и более раздутым.

Если в проекте есть CI/CD, модифицируется только код субприложения, но отправка кода вызовет создание всех субприложений одновременно, что нецелесообразно.

В то же время, если разработка подприложений некоторых бизнесов является межведомственной и межкомандной, то как управлять правами репозитория кода — это еще одна проблема.

Исходя из вышеперечисленных проблем, мы должны рассмотреть вопрос о переносе каждого приложения в отдельный репозиторий git. Так как у нас есть независимый склад, проект нельзя снова разместить в той же директории, поэтому предыдущий проходnpm i file:../commonОбщий установленный методом неприменим, поэтому лучше опубликовать его на приватный сервер npm компании или воспользоваться формой адреса git.

qiankun-exampleДля лучшего представления все приложения по-прежнему размещены в одном репозитории git, пожалуйста, не копируйте.

Независимое от подприложения управление складом после агрегации

После того, как подприложение будет иметь независимый git-репозиторий, оно может начать самостоятельную разработку самостоятельно, в это время будут проблемы:Среда разработки независима и не может видеть полную картину всего приложения..

Хотя во время разработки лучше сосредоточиться на подприложении, всегда бывают случаи, когда необходимо запустить весь проект, например, когда несколько подприложений должны зависеть друг от друга для перехода, поэтому остается еще целый проект для выполнения. агрегировать все git-репозитории субприложений Требуется только управление Хранилище агрегации требует, чтобы все зависимости (включая субприложения) можно было установить одним щелчком мыши, а весь проект можно было запустить одним щелчком мыши.

Здесь рассматриваются три основных варианта:

  1. использоватьgit submodule.
  2. использоватьgit subtree.
  3. Просто поместите все подрепозитории в общий каталог и.gitignoreТерять.
  4. Управляется с lerna.

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

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

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

Поскольку все подрепозитории игнорируются, после клонирования библиотеки агрегации это все еще пустой каталог.В это время мы можем написать скриптscripts/clone-all.sh, напишите команду клонирования всех подрепозиториев:

# 子仓库一
git clone git@xxx1.git

# 子仓库二
git clone git@xxx2.git

Затем также инициализируйте один в библиотеке агрегации.package.json, скрипты плюс:

  "scripts": {
    "clone:all": "bash ./scripts/clone-all.sh",
  },

Таким образом, после того, как библиотека агрегации клонов git не работает, затемnpm run clone:allВы можете клонировать все подрепозитории одним щелчком мыши.

Как упоминалось ранее, библиотека агрегации должна иметь возможность установить и запустить весь проект одним щелчком мыши.Мы ссылаемся на примеры qiankun и используемnpm-run-allсделать это.

  1. Агрегатная установка библиотекиnpm i npm-run-all -D.
  2. В package.json библиотеки агрегации добавлены команды установки и запуска:
  "scripts": {
    ...
    "install": "npm-run-all --serial install:*",
    "install:main": "cd main && npm i",
    "install:sub-vue": "cd sub-vue && npm i",
    "install:sub-react": "cd sub-react && npm i",
    "start": "npm-run-all --parallel start:*",
    "start:sub-react": "cd sub-react && npm start",
    "start:sub-vue": "cd sub-vue && npm start",
    "start:main": "cd main && npm start"
  },

npm-run-allиз--serialозначает выполнять один за другим последовательно,--parallelУказывает на одновременное и параллельное выполнение.

С вышеизложенным, установка в один кликnpm i, запуск одной кнопкойnpm start.

VSCode Eslint Configuration

Если вы используете vscode и используете плагин eslint для автоматического восстановления, так как проект находится в некорневом каталоге, eslint не может вступить в силу, поэтому вам также необходимо указать рабочий каталог eslint:

// .vscode/settings.json
{
  "eslint.workingDirectories": [
    "./main",
    "./sub-vue",
    "./sub-react",
    "./common"
  ],
  "eslint.enable": true,
  "editor.formatOnSave": false,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "search.useIgnoreFiles": false,
  "search.exclude": {
    "**/dist": true
  },
}

Вложенные приложения переходят друг в друга

В дополнение к щелчку меню в верхней части страницы для переключения подприложений, наши требования также требуют, чтобы подприложения переходили к другим подприложениям, что будет включать отображение активного состояния верхнего меню:sub-vueпереключиться наsub-react, то верхнее меню должно бытьsub-reactПерейти в активное состояние. Есть два варианта:

  • Действие перехода дочернего приложения передается родительскому приложению, и родительское приложение выполняет реальный переход, так что родительское приложение знает, что нужно изменить состояние активации, и есть некоторые дочерние компоненты.$emitЗначение события для родительского компонента.
  • родительский монитор приложенийhistory.pushStateСобытие, когда обнаружено, что маршрут изменился, родительское приложение знает, следует ли изменить состояние активации.

из-заqiankunВ настоящее время нет API-интерфейса, который инкапсулирует дочернее приложение для отправки событий в родительское приложение, например iframe.postMessage, поэтому первое решение немного сложно, но можно поставить состояние активации в управление состоянием, а дочернее приложение может синхронизировать родительское приложение, изменив значение в vuex.Метод выполнимый, но не очень хороший, и поддержание состояние немного сложно в управлении состоянием.

Итак, здесь мы выбираем вариант второй, подкладки прыгают черезhistory.pushState(null, '/sub-react', '/sub-react'), поэтому родительское приложение находит способ прослушивать его при монтированииhistory.pushStateВот и все. из-заhistory.popstateтолько мониторback/forward/goно не могу контролироватьhistory.pushState, поэтому вам нужно перезаписать его глобальноhistory.pushStateмероприятие.

// main/src/App.vue
export default {
  methods: {
    bindCurrent () {
      const path = window.location.pathname
      if (this.microApps.findIndex(item => item.activeRule === path) >= 0) {
        this.current = path
      }
    },
    listenRouterChange () {
      const _wr = function (type) {
        const orig = history[type]
        return function () {
          const rv = orig.apply(this, arguments)
          const e = new Event(type)
          e.arguments = arguments
          window.dispatchEvent(e)
          return rv
        }
      }
      history.pushState = _wr('pushState')

      window.addEventListener('pushState', this.bindCurrent)
      window.addEventListener('popstate', this.bindCurrent)

      this.$once('hook:beforeDestroy', () => {
        window.removeEventListener('pushState', this.bindCurrent)
        window.removeEventListener('popstate', this.bindCurrent)
      })
    }
  },
  mounted () {
    this.listenRouterChange()
  }
}

оптимизация производительности

Каждое поддержание представляет собой полное приложение, каждое приложение упаковано под давлениеvue/vue-router/vuex. С точки зрения всего проекта это эквивалентно упаковке этих модулей несколько раз, что будет очень расточительно, поэтому здесь мы можем еще больше оптимизировать производительность.

Первое, о чем мы можем подумать, это через webpackexternalsИли основное приложение предоставляет общедоступные модули для повторного использования.

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

Сейчас я думаю, что лучше так: основное приложение может доставлять некоторые модули, которые оно использует, а подприложение может отдавать приоритет модулям, доставляемым основной версией приложения, а не родительским приложением.

Эта схема относится кПрактика и краткое изложение решения qiankun micro front-end - как обмениваться общими плагинами между подпроектами, идея очень полная, можете глянуть, в этом проекте еще не добавили эту функцию.

развертывать

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

План выглядит следующим образом:

Учитывая, что могут возникнуть конфликты маршрутизации, когда основное приложение и подприложения используют общие доменные имена, подприложения могут добавляться постоянно, поэтому мы помещаем все подприложения вxx.com/subapp/В этом вторичном каталоге корневой путь/Оставьте это основному приложению.

Действуйте следующим образом:

  1. Основное приложение main и все подприложения упаковываются в копию html, css, js, static и загружаются на сервер в подкаталогах, а подприложения размещаются в единомsubappВ каталоге последний пример:
├── main
│   └── index.html
└── subapp
    ├── sub-react
    │   └── index.html
    └── sub-vue
        └── index.html
  1. Настройте nginx, ожиданиеxx.comКорневой путь указывает на основное приложение,xx.com/subappЧтобы указать на суб-приложение, вам нужно написать только одну копию конфигурации суб-приложения, и вам не нужно менять конфигурацию nginx для новых суб-приложений в будущем.Ниже должна быть самая краткая конфигурация nginx для развертывания микроприложений.
server {
    listen       80;
    server_name qiankun.fengxianqi.com;
    location / {
        root   /data/web/qiankun/main;  # 主应用所在的目录
        index index.html;
        try_files $uri $uri/ /index.html;
    }
    location /subapp {
	    alias /data/web/qiankun/subapp;
        try_files $uri $uri/ /index.html;
    }

}

nginx -s reloadТогда ты можешь.

В этой статье специально сделан демонстрационный онлайн-показ:

Вся станция (основное приложение):qiankun.fengxianqi.com/

Чтобы получить доступ к дополнительным приложениям по отдельности:

возникшие проблемы

1. После запуска реактивного суб-приложения основное приложение зависнет после первого рендеринга

Горячая перезагрузка дочернего приложения фактически приведет к тому, что родительское приложение зависает напрямую, что было совершенно ошеломлено в то время. К счастью, я нашел связанныеissues/340, то есть отключите горячую перезагрузку при переписывании веб-пакета реакции (добавление следующей конфигурации для ее отключения не приведет к горячей перезагрузке, и приложение реакции должно быть обновлено вручную во время разработки, не так ли это немного неудобно...):

module.exports = {
  webpack: function override(config, env) {
    // 解决主应用接入后会挂掉的问题:https://github.com/umijs/qiankun/issues/340
    config.entry = config.entry.filter(
      (e) => !e.includes('webpackHotDevClient')
    );
    // ...
    return config;
  }
};

2. Неперехваченная ошибка: приложение «xx» умерло в статусе SKIP_BECAUSE_BROKEN: [qiankun] Целевой контейнер с #subapp-viewport не существовал во время монтирования xx!

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

Сообщение об ошибке очень ясное, то есть, когда основное приложение монтирует субприложение xx, dom, используемый для монтирования субприложения, не существует. Итак, сначала, когда я подумал, что это vue в качестве главного приложения,#subapp-viewportЕще не было времени для рендеринга, поэтому постарайтесь убедиться, что основное приложениеmountЗатем зарегистрируйте суб-приложение.

// 主应用的main.js
new Vue({
  render: h => h(App),
  mounted: () => {
    // mounted后再注册子应用
    renderMicroApps();
  },
}).$mount('#root-app');

Но этот метод не работает, даже если используется setTimeout, нужно найти другой метод.

Наконец, пошаговая отладка показала, что это было вызвано загрузкой проекта фрагмента карты Gaode js, который будет использоваться при первой загрузке js.document.writeЗатирание всего html вылилось в ошибку, что #subapp-viewport не существует, поэтому в итоге пришлось найти способ удалить js файл.

Виньетка: Почему наш проект загружает эту карту Gaode js? Наш проект тоже не использовал его.В этот раз мы впали в недоразумение: qiankun от Али, и Gaode тоже от Али.Не будет ли qiankun тайно динамически загружать js Gaode во время рендеринга для сбора каких-то данных? Очень стыдно иметь эту идею для проекта с открытым исходным кодом. . . На самом деле это потому, что мелкий партнер, написавший шаблон библиотеки компонентов в нашей компании, забыл убрать время отладки.public/index.htmlЯ использовал этот js, и я пошел комментировать в то времяissue(закрывает лицо и плачет). Говоря об этом, я хочу сказать, что при обнаружении ошибки вы должны сначала проверить себя, а не задавать вопросы другим.

наконец

В этой статье очень подробно рассказывается о некоторых идеях и методах построения всей архитектуры от начала до развертывания, и мы надеемся, что она будет полезна всем. Следует напомнить, что этот пример не обязательно является передовой практикой. Он используется только в качестве эталона для идей. Архитектура будет постоянно корректироваться и изменяться в соответствии с потребностями бизнеса. Только подходящая является лучшей.

Образец кода:GitHub.com/Удары ветра/….

Онлайн-демонстрация:qiankun.fengxianqi.com/

Чтобы получить доступ к дополнительным онлайн-приложениям по отдельности:

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

некоторые справочные статьи

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

🏆 Технический специальный выпуск 4 | Расскажите о тех вещах, которые касаются микроинтерфейса...