Краткое изложение практики qiankun micro front-end (2)

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

предисловие

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

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

Возможные ямы и решения

Некоторые проблемы обнаружены и решены мной самостоятельно, некоторыеqiankunскладissueвидна область. Для этих проблем я стараюсь объяснить причины и решения как можно яснее.

Не удалось загрузить файл шрифта подпроекта

qiankunдля подпроектовjs/cssобработка

Сказал раньше:qiankunзапрос на подпроектindex.htmlПосле этого он сначала будет соответствоватьjs/cssсвязанные теги, а затем заменить его, он должен загрузить себяjs/cssИ запустить, затем удалитьhtml/head/bodyи другие теги, остальной контент вставляется как есть в контейнер подэлемента:

дляjs ( <script>теги) обработка

в линиюjsСодержимое будет записано непосредственно в объект, внешняя ссылкаjsбуду использоватьfetchПерейдите к содержимому (строка), а затем войдите в этот объект.

if (isInlineCode(script)) {
    return getInlineCode(script);
} else {
    return fetchScript(script);
}

const fetchScript = scriptUrl => scriptCache[scriptUrl] ||
		(scriptCache[scriptUrl] = fetch(scriptUrl).then(response => response.text()));

При запуске подпроекта выполните этиjsТолько что:

//内联js
eval(`;(function(window){;${inlineScript}\n}).bind(window.proxy)(window.proxy);`)
//外链js
eval(`;(function(window){;${downloadedScriptText}\n}).bind(window.proxy)(window.proxy);`))

Загружать и запускать внешние ссылкиjsТрудность здесь в том, как обеспечитьjsправильный порядок выполнения?

<script>помеченasyncа такжеdeferАтрибуты:

  1. defer: Эквивалент внешней цепочкиjsвнизу страницы
  2. async: выполняется асинхронно относительно остальной части страницы и выполняется при загрузке. обычно используетсяGoogle Analytics

Итак, внешняя цепьjsПросто различайте междуasync,имеютasyncиз<script>использоватьpromiseАсинхронная загрузка, вы можете выполнить ее после загрузки, нетasyncСвойства выполняются по порядку.

Предположим, что в HTML есть следующееjsЭтикетка:

<script src="./a.js" onload="console.log('a load')"></script>
<script src="./b.js" onload="console.log('b load')"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.js"></script>
<script src="./c.js" onload="console.log('c load')"></script>

Обычная логика загрузки и выполнения браузераЗагружать параллельно, но выполнять последовательноНо пока загружается первая загрузка, она сразу выполнит первую. Если третья не загружена, четвертая, даже если загрузка завершится, не будет выполняться первой.

Как показано на рисунке, я ограничиваю скорость интернета до5kb/s,ТретийjsЕще не загрузился, но четвертыйjsОн был загружен, но не будет выполняться.

qiankunзагружается параллельно, ноПодождите, пока все js загрузятся, а затем выполнить последовательно. Он немного отличается от родного порядка выполнения загрузки браузера, но эффект тот же.

Здесь есть место для оптимизации: пока предыдущийjsПосле того, как он загружен и выполнен, его можно выполнить сразу же после загрузки, не дожидаясь последующегоjsзагрузка завершена.

дляcss ( <style>а также<link>теги) обработка

Логика загрузки осталась прежней: inlinecss(<style>Содержимое метки) будет записано непосредственно в объект, внешняя ссылкаcss(<link>ярлык) будет использоватьfetchПерейдите к содержимому (строка), а затем войдите в этот объект.

Но при исполнении такжеjs«Похожие»: Контент, размещенный в<style>теги, затем вставляются на страницу, подпункты выгружаются, удаляются эти<style>Этикетка.

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

Но принесет скрытую яму,cssЕсли используется файл шрифта, и это относительный путь, он изначальноlinkСтиль внешней ссылки, относительный путь относится к этой внешней ссылкеcssпуть, теперь становится встроенным стилем, относительный путь становится относительнымindex.htmlпуть, приведет к ошибке 404 для файла шрифта.

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

Как решить проблему сбоя загрузки файла шрифта подпроекта

Хотя это потому чтоqiankunподпроект<link>изменить на<style>Выполнение , вызвало проблему, но кажется, что это нормально и разумно.

Фундаментальная причина в том, что файл шрифта даже послеwebpackОбработано, но префикс пути не введен. Так изменитьwebpackконфигурации, пусть файл шрифта пройдетurl-loaderобработка, упаковка вbase64, эту проблему можно решить.

подпроектwebpackДобавьте в конфигурацию следующее:

module.exports = {
  chainWebpack: (config) => {
    config.module
      .rule('fonts')
      .test(/.(ttf|otf|eot|woff|woff2)$/)
      .use('url-loader')
      .loader('url-loader')
      .options({})
      .end()
  },
}

Посмотреть источник найденvue-cli4Это также обрабатывается таким образом, но он ограничивает шрифты в пределах 4 КБ для упаковки в base64.

Подпроекты развернуты во вторичных каталогах

ПервыйvueЧтобы развернуть проект во вторичный каталог, его необходимо настроитьpublicPath,Описание официального сайта vue-cli3:

Тогда следует отметить, что при регистрации подпроектовзапись адреса входаЗаполнить.

Предположим, что подпроект развернут вapp-vue-hashПод содержанием,entryнаписать напрямуюhttp://localhost/app-vue-hashприведет кqiankunВыбиратьpublicPathошибка. Адрес входа в подпроектhttp://localhost/app-vue-hashОтносительный путьhttp://localhost, и мы хотим, чтобы относительный путь подпроекта былhttp://localhost/app-vue-hash, то нам нужно только написатьhttp://localhost/app-vue-hash/Вот и все,Последнее / не может быть опущено.

qiankunВыбиратьpublicPathИсходный код:

function defaultGetPublicPath(url) {
  try {
    // URL 构造函数不支持使用 // 前缀的 url
    const { origin, pathname } = new URL(url.startsWith('//') ? `${location.protocol}${url}` : url, location.href);
    const paths = pathname.split('/');
    // 移除最后一个元素
    paths.pop();
    return `${origin}${paths.join('/')}/`;
  } catch (e) {
    console.warn(e);
    return '';
  }
}

Тестируя, мы можем найтиhttp://localhost/appа такжеhttp://localhost/app/два разных путиserver, тот самыйhtml, затем вhtmlВвести относительный путь к ресурсам. Адрес разрешения браузера:

иллюстрироватьqiankunВыбиратьpublicPathобработка правильная.

Совместимость с IE11

qiankunПри загрузке подпроекта используйтеfetch, поэтому для IE11 вам нужно ввестиfetchизpolyfill, но представилfetch-polyfill, все равно выдает ошибку под IE11.

интуитивноsingle-spa.min.jsСообщается об ошибке, но этот код представляет собой сжатый код, который нелегко устранить.

оказатьсяnode_modules\single-spa\package.json,оказаться"module": "lib/esm/single-spa.min.js"Измените эту строку на"module": "lib/esm/single-spa.dev.js", затем перезапустите проект.

Обнаружитьsingle-spaПричина ошибкиAPPЗагрузка не удалась, код такой:

function handleAppError(err, app, newStatus) {
  var transformedErr = transformErr(err, app, newStatus);
  if (errorHandlers.length) {
    errorHandlers.forEach(function (handler) {
      return handler(transformedErr);
    });
  } else {
    setTimeout(function () {
      throw transformedErr; // 这一行抛出的错误,也就是控制台显示的错误信息
    });
  }
}

Распечатайте информацию об ошибке загрузки приложения следующим образом:

После расследования этоfetchизpolyfillПроблема в том, что он сообщает об ошибке.

Однако только импортfetch-polyfill, а затем использовать в проектеfetch, не сообщит об этой ошибке.

импортировать одновременноqiankunа такжеfetch-polyfill, он сообщит об этой ошибке:

import 'fetch-polyfill';
import { registerMicroApps, start } from 'qiankun';

console.log(fetch);
console.log(fetch("http://localhost:1111"));

Расследование и причины долгое время эти два плагина не совместимы. наконец,qiankunОбходной путь автора заключается в использованииwhatwg-fetch, то отображаемый списокpromise,symbolожидающийpolyfill.

Введите следующее содержимое в основной файл записи проекта (не забудьте установить зависимости):

import 'whatwg-fetch';
// import 'custom-event-polyfill'; // 如果还报错,需要引入这个
import 'core-js/stable/promise';
import 'core-js/stable/symbol';
import 'core-js/stable/string/starts-with';
import 'core-js/web/url';

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

Статьи автора qiankun:Как изящно обеспечить совместимость с IE в 2020 году

vueПроблема с утечкой памяти подпроекта

Эту проблему очень трудно найти, вqiankunизissueРайон видит:GitHub.com/UfanAccept/Kg…, процесс расследования выкладывать не буду, решение достаточно простое.

Пусто, когда подпроект уничтоженdomТолько что:

export async function unmount() {
  instance.$destroy();
+ instance.$el.innerHTML = ""; //新增这一行代码
  instance = null;
  router = null;
}

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

Исследования и мышление с особыми потребностями

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

keep-aliveнужно

подпроектkeep-aliveНа самом деле, я просто хочу не удалять его при переключении подпроекта, просто скрыть стиль (display: none), поэтому в следующий раз вы откроете его быстрее.

keep-aliveНеобходимо использовать с осторожностью, загрузка и запуск нескольких подпроектов одновременно увеличиваетjs/cssриск загрязнения.

несмотря на то чтоqiankunизproxyманераjsПесочница может прекрасно поддерживать многопроектную работу, но не стоит забывать о опухоли IE11.Песочница под IE11 используетdiffметод, который позволит нескольким проектам совместно использовать песочницу, что эквивалентно отсутствию песочницы. Также могут быть конфликты между маршрутами.

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

выполнитьkeep-aliveСуществует множество способов удовлетворения потребностей, и рекомендуется использовать схему 1.

Вариант 1: С помощьюloadMicroAppфункция

попробуй уже использоватьAPIреализоватьkeep-aliveТребование: с помощьюloadMicroAppФункции ручной загрузки и выгрузки подпроектов обычно имеютkeep-aliveто, что нужноtabстраницу, добавить новуюtabЭтот подпроект загружается при открытии страницы, закрытьtabУдалите этот подпроект, когда будете пейджинговать.

из-заdemoНет вtabстраницу, я напрямую загружаю все подпроекты, а потом смотрю на эффект, изменений не много.

основной проектAPP.vueдокумент:

<template>
  <div id="app">
    <header>
      <router-link to="/app-vue-hash/">app-vue-hash</router-link>
      <router-link to="/app-vue-history/">app-vue-history</router-link>
      <router-link to="/about">about</router-link>
    </header>
    <div id="appContainer1" v-show="$route.path.startsWith('/app-vue-hash/')"></div>
    <div id="appContainer2" v-show="$route.path.startsWith('/app-vue-history/')"></div>
    <router-view></router-view>
  </div>
</template>

<script>
import { loadMicroApp } from 'qiankun';

const apps = [
  { 
    name: 'app-vue-hash', 
    entry: 'http://localhost:1111', 
    container: '#appContainer1',
    props: { data : { store, router } }
  },
  { 
    name: 'app-vue-history',
    entry: 'http://localhost:2222', 
    container: '#appContainer2',
    props: { data : store }
  }
]

export default {
  mounted() {
    // 优先加载当前的子项目
    const path = this.$route.path;
    const currentAppIndex = apps.findIndex(item => path.includes(item.name));
    if(currentAppIndex !== -1){
      const currApp = apps.splice(currentAppIndex, 1)[0];
      apps.unshift(currApp);
    }
    // loadMicroApp 返回值是 app 的生命周期函数数组
    const loadApps = apps.map(item => loadMicroApp(item))
    // 当 tab 页关闭时,调用 loadApps 中 app 的 unmount 函数即可
  },
}
</script>

Переключить подпроект, подпроектDOMНе опустошено:

Вариант 2: изменитьqiankunисходный код

Это также относительно просто реализовать: выгрузка подсистемы не приводит к опорожнению контейнера.domне удалятьvueнапример, сdisplay: noneпрятаться. При загрузке подсистемы сначала определите, есть ли в контейнере контент, и если он уже существует, он не будет повторно вставлен в подсистему.HTML.

В основном делится на 4 этапа:

  1. Изменить подпроектrenderфункция, не созданная повторноvue
function render() {
  if(!instance){
    router = new VueRouter({
      base: window.__POWERED_BY_QIANKUN__ ? '/app-vue-history' : '/',
      mode: 'history',
      routes,
    });
    instance = new Vue({
      router,
      store,
      render: h => h(App),
    }).$mount('#appVueHistory');
  }
}
  1. Изменить подпроектunmountЖизненный цикл, подпроектunmountне удалятьvueПример
export async function unmount() {
  // instance.$destroy();
  // instance = null;
  // router = null;
}
  1. Измените регистрацию и контейнер подпроектов в основном проекте, и поставьте отдельный контейнер для каждого подпроекта (конечно, вы можете поместить его в контейнер, с которым более хлопотно иметь дело). Затем переключите подсистему, чтобы скрыть другую
<div id="appContainer1" v-show="$route.path && $route.path.startsWith('/app-vue-hash')"></div>
<div id="appContainer2" v-show="$route.path && $route.path.startsWith('/app-vue-history')"></div>
registerMicroApps([
  {
    name: 'app-vue-hash', 
    entry: 'http://localhost:1111', 
    container: '#appContainer1', 
    activeRule: '/app-vue-hash', 
    props: { data : { store, router } }
  },
  { 
    name: 'app-vue-history',
    entry: 'http://localhost:2222', 
    container: '#appContainer2', 
    activeRule: '/app-vue-history',
    props: { data : store }
  },
]);
  1. ИсправлятьqiankunИсходный код подпроекта: судить о наличии контента перед загрузкой подпроекта, если есть контент, он не будет обработан, при выгрузке подсистемы он не будет очищенdom

Просто доступно здесьpatch-packageплагин, прямая модификацияqiankun/es/loader.js, изменения следующие:

Исправлятьqiankun/es/sandbox/patchers/dynamicHeadAppend.js:

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

Эта программа предназначена только для справки, углубить пониманиеqiankunпонимание.

Вариант 3: кэшировать подпроектыdom

Источник программы:Проблемы в репозитории qiankun

Это решение более хлопотное, общий принцип - кешvueпримерdom, изменяется входной файл подпроекта:

let instance = null;
let router = null;

function render() {
  // 这里必须要new一个新的路由实例,否则无法响应URL的变化。
  router = new VueRouter({
    mode: 'hash',
    base: process.env.BASE_URL,
    routes
  });

  if (window.__POWERED_BY_QIANKUN__ && window.__CACHE_INSTANCE_BY_QIAN_KUN_FOR_VUE__) {
    const cachedInstance = window.__CACHE_INSTANCE_BY_QIAN_KUN_FOR_VUE__;

    // 从最初的Vue实例上获得_vnode
    const cachedNode =
      // (cachedInstance.cachedInstance && cachedInstance.cachedInstance._vnode) ||
      cachedInstance._vnode;

    // 让当前路由在最初的Vue实例上可用
    router.apps.push(...cachedInstance.catchRoute.apps);

    instance = new Vue({
      router,
      store,
      render: () => cachedNode
    });

    // 缓存最初的Vue实例
    instance.cachedInstance = cachedInstance;

    router.onReady(() => {
      const { path } = router.currentRoute;
      const { path: oldPath } = cachedInstance.$router.currentRoute;
      // 当前路由和上一次卸载时不一致,则切换至新路由
      if (path !== oldPath) {
        cachedInstance.$router.push(path);
      }
    });
    instance.$mount('#appVueHash');
  } else {
    console.log('正常实例化');
    // 正常实例化
    instance = new Vue({
      router,
      store,
      render: h => h(App)
    }).$mount('#appVueHash');
  }
}

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();
}

export async function unmount() {
  console.log('[vue] vue app unmount');
  const cachedInstance = instance.cachedInstance || instance;
  window.__CACHE_INSTANCE_BY_QIAN_KUN_FOR_VUE__ = cachedInstance;
  const cachedNode = cachedInstance._vnode;
  if (!cachedNode.data.keepAlive) cachedNode.data.keepAlive = true;
  cachedInstance.catchRoute = {
    apps: [...instance.$router.apps]
  }
  instance.$destroy();
  router = null;
  instance.$router.apps = [];
}

Повторное использование общих зависимостей (схема)

Чтобы повторно использовать общие зависимости в подпроектах, настройтеwebpackизexternalsЭто необходимо, и после настройки этого, когда подпроект работает самостоятельно, источники этих зависимостей имеют и толькоindex.htmlВнешние ссылки InscriptЭтикетка.

Есть два случая:

  • Зависимость «повторное использование» между подпроектами

С этим легко справиться, вам нужно только убедиться, что зависимыйurlВы можете согласиться. Например, подпроект A используетvue, подпроект B также использует ту же версиюvue, если оба проекта используют одну и ту же копиюCNDфайл, который сначала читается из кеша при загрузке:

const fetchScript = scriptUrl => scriptCache[scriptUrl] ||
	(scriptCache[scriptUrl] = fetch(scriptUrl).then(response => response.text()));
  • Подпроекты повторно используют зависимости основного проекта

Просто дайте подпроектindex.htmlобщественный иждивенецscriptа такжеlinkтег плюсignoreАтрибуты (это настраиваемый атрибут, а не стандартный атрибут).

С этим свойством,qiankunне буду загружать это сноваjs/css, в то время как подпроекты выполняются независимо, этиjs/cssЕго все еще можно загрузить Таким образом, «подпроект повторно использует зависимости основного проекта».

<link ignore rel="stylesheet" href="//cnd.com/antd.css">
<script ignore src="//cnd.com/antd.js"></script>

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

прочитай этоqiankunОфициальный сайт написал этот вопрос:Vue Router сообщает об ошибке Uncaught TypeError: невозможно переопределить свойство: $router

Причина ошибки

Подпроекты не настроеныexternals, в проектеVueявляется глобальной переменной, ноЭто не принадлежит окну, поэтому, когда подпроекты выполняются независимо, этоifРешение недействительно:

if (inBrowser && window.Vue) {
  window.Vue.use(VueRouter)
}

а такжеqiankunПри запуске этого подпроекта сначала найдитеwindow, а затем найдите родительский проектwindow, затем вwindowнайти наvue.ifрешение вступит в силу, тоwindowродительского проектаVueустановленыVueRouter, собственный глобальныйvueНе устанавливается, что приводит к ошибке.

Решение 1. Обработайте глобальные переменные перед загрузкой подпроектов

Предположениеapp-vue-hashПодпроекты повторно используют основные зависимости проекта,app-vue-historyПодпроекты не используют повторно основные зависимости проекта.

При регистрации подпроектов в основном проекте добавьте следующий код для решения проблемы:

registerMicroApps(apps,
{
  beforeLoad(app){
    if(app.name === 'app-vue-hash'){
      // 如果直接在 app-vue-hash 子项目刷新页面,此时 window.Vue2 是 undefined
      // 所以先判断下 window.Vue2 是否存在
      if(window.Vue2){
        window.Vue = window.Vue2; 
        window.Vue2 = undefined;
      }
    }else if(app.name === 'app-vue-history'){
      window.Vue2 = window.Vue; 
      window.Vue = undefined
    }
  },
});

Решение второе: пройтиpropsтранзитивные зависимости

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

Когда основной проект зарегистрирован, передайте зависимости подпроектам (некоторый ненужный код опущен):

import VueRouter from 'vue-router'
registerMicroApps([
  {
    name: 'app-vue-hash', 
    entry: 'http://localhost:1111', 
    container: '#appContainer', 
    activeRule: '/app-vue-hash', 
    props: { data : { VueRouter } }
  },
]);

Конфигурация подпроектаexternalsи зависимости внешней цепи плюсignoreАтрибуты:

function render(parent = {}) {
  if(!instance){
    // 当它独立运行时,使用自己的外链依赖 window.VueRoute
    const VueRouter = parent.VueRouter || window.VueRouter; 
    Vue.use(VueRouter);
    router = new VueRouter({
      routes,
    });
    instance = new Vue({
      router,
      store,
      render: h => h(App),
    }).$mount('#appVueHash');
  }
}

export async function mount(props) {
  render(props.data);
}

Решение 3. Измените имена зависимостей основного проекта и подпроектов.

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

  1. Изменить вспомогательное приложение и основное приложениеexternalsНастроить, изменить имя зависимости, не использоватьVue
externals: {
  'vue': 'Vue2' , // 这个的意思是告诉 webpack 去把 winodw.Vue2 当做 vue 这个模块
}
  1. Импорт внешних ссылок в основном приложенииvue.jsПосле этого измените имя наVue2
<script src="https://unpkg.com/vue@2.5.16/dist/vue.runtime.min.js"></script>
<script>
window.Vue2 = winow.Vue;
window.Vue = undefined;
</script>

Из трех схем рекомендуется третья, более лаконичная и удобная.

основной проект свернутhashа такжеhistoryбоевой

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

Маршрутизация основного проектаhistoryмодель

Основное использование проектаhistoryрежим, вам нужно использоватьlocation.pathnameразличать различные подпроекты, что такжеqiankunФорма рекомендации. При регистрации подпроектаactiveRuleПросто напишите путь:

registerMicroApps([
   { 
      name: 'app-vue-hash', 
      entry: 'http://localhost:1111', 
      container: '#appContainer', 
      activeRule: '/app-vue-hash', 
   },
])

преимущество:

  1. Подпроекты могут использоватьhistoryрежим, вы также можете использоватьhashмодель . Таким образом, все старые проекты могут быть напрямую связаны, и совместимость будет сильной.
  2. правильноhashПодпункты Mode не действуют и не нуждаются в изменении

недостаток:

  1. historyМаршрутизация шаблона должна быть установленаbase
  2. Переход между подпроектами требует использования родительского проекта.routerпредмет (без<a>Причина, по которой ссылка переходит напрямую, заключается в том, что<a>ссылка обновляет страницу).

На самом деле не проходитrouterобъект, используя собственныйhistoryПрыжки с объектов также работают:history.pushState(null, 'name', '/app-vue-hash/#/about'), снова страница не будет обновляться.

Является ли это родительским проектомrouterобъект, еще роднойhistoryобъект, прыжокjsПуть.这里有一个小小的用户体验问题:标签(<router-link>а также<a>) в виде прыжка поддерживает контекстное меню браузера по умолчанию.jsВыхода нет:

Маршрутизация основного проектаhashрежим и подпроекты не имеютhistoryмаршрутизация по шаблону

То есть основной проект и все подпроектыhashрежиме, в этом случае тоже есть два подхода:

  1. использоватьpathразличать подпроекты

Я не буду вдаваться в подробности того, как

Преимущества: нет необходимости модифицировать внутренний код подпроекта

Недостатки: переход между проектами должен полагаться на роднойhistoryобъект

  1. использоватьhashразличать подпроекты

Таким образом, основной проект и подпроект будут совместно выполнять маршрутизацию, например:

  • /#/vue/home: будет загружатьсяvueподпроектhomeстраницу, но на самом деле получить доступ только к этому подпроектуhomeПолный маршрут для страницы/#/vue/home

  • /#/react/about: будет загружатьсяreactподпроектaboutстраницу, опять же, чтобы получить доступ к этому подпроектуaboutПолный маршрут для страницы/#/react/about

  • /#/about: загрузит основной проектaboutстраница

Практика заключается в настройкеactiveRule:

const getActiveRule = hash => location => location.hash.startsWith(hash);
registerMicroApps([
   { 
      name: 'app-vue-hash', 
      entry: 'http://localhost:1111', 
      container: '#appContainer', 
      activeRule: getActiveRule('#/app-vue-hash'), 
   },
])

Затем вам нужно добавить этот префикс ко всем маршрутам подпроекта или установить корневой маршрут подпроекта с этим префиксом.

const routes = [
  {
    path: '/app-vue-hash',
    name: 'Home',
    component: Home,
    children: [
      // 其他的路由都写到这里
    ]
  }
]

Если подпроект является новым проектом, все в порядке.Если это старый проект, влияние все еще относительно велико.Маршрутизация в подпроекте скачет (<router-link>,router.push(),router.repace()) при использованииpath, вам нужно изменить, вы должны добавить этот префикс, если вы используетеnameПерейти, не нужно менять:router.push({ name: 'user'}).

Плюсы: переход между всеми проектами можно использовать напрямуюrouterобъект или<router-link>, нет необходимости полагаться на объект маршрутизации родительского проекта или собственныйhistoryобъект

Недостатки: Навязчивые изменения в подпроектах, никакого влияния, если это совершенно новый проект.

Маршрутизация основного проектаhashрежим и подпроекты имеютhistoryмаршрутизация по шаблону

Главный элементhashрежиме переход между подпроектами возможен только с помощью родногоhistoryобъект, мы можем либо использоватьpathтакже можно использоватьhashЧтобы различать подпроекты:

  1. использоватьpathразличать подпроекты

с основным проектомhistoryРазницы особой нет, плюсы и минусы одни и те же.

  • /vue-hash/#/home: будет загружатьсяvueподпроектhomeстраница
  • /vue-history/about: будет загружатьсяvue-historyподпроектaboutстраница
  • /#/about: загрузит основной проектaboutстраница
  1. использоватьhashразличать подпроекты

Это на самом деле не очень хорошо и немного нестандартно, но тоже можно использовать:

  • /home/#/vue: будет загружатьсяvueподпроектhomeстраница
  • /#/vue-hash/about: будет загружатьсяvue-hashподпроектaboutстраница
  • /#/about: загрузит основной проектaboutстраница

Достоинства: нет

Минусы: даhashПодпроекты являются инвазивными модификациями и не имеют никакого эффекта, если они являются совершенно новыми проектами.

Суммировать

основной проект свернутhashа такжеhistoryМожно использовать все режимы, каждый со своими преимуществами и недостатками, в зависимости от ситуации.

Совместное использование компонентов между проектами

Совместное использование компонентов, желательноnpmметод package, но если private не развернутnpm, проект также предполагает конфиденциальность и не может быть размещен вGitHub,илиБизнес-компоненты с даннымиРазделяя, вы можете рассмотреть следующие способы.

Совместное использование компонентов между родительским и дочерним проектами

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

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

Основной файл входа в проект:

import HelloWorld from '@/components/HelloWorld.vue'
window.commonComponent = { HelloWorld };

Подпроекты используют напрямую:

components: { 
  HelloWorld: window.__POWERED_BY_QIANKUN__ ? window.commonComponent.HelloWorld :
     import('@/components/HelloWorld.vue'))
}

Совместное использование компонентов между подпроектами (слабая зависимость)

Что такое слабая зависимость? Даже в самом подпроекте есть этот компонент.Когда другие подпроекты были загружены, он повторно использует чужие компоненты.Если другие подпроекты не загружены, он использует свой собственный компонент.

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

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

  2. Подпроект получает эту переменную и монтирует ее вwindowначальство

export async function mount(props) {
  window.commonComponent = props.data.commonComponent;
  render(props.data);
}
  1. Общие компоненты в подпроектах записываются как асинхронные компоненты.
components: {
   HelloWorld: () => {
      if(!window.commonComponent){
        // 独立运行时
        window.commonComponent = {};
      }
      const HelloWorld = window.commonComponent.HelloWorld;
      return HelloWorld || (window.commonComponent.HelloWorld =
             import('@/components/HelloWorld.vue'));
   }
}

вотbug: Стили общих компонентов не загружаются при переключении туда и обратно. Почувствуй этоbugТо же, что и "Подпроект переходит на главную страницу проекта, стили основного проекта не загружаются"bug, хорошего решения пока нет.

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

Совместное использование компонентов между подпроектами (сильная зависимость)

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

Так как же обеспечить порядок загрузки между подпроектами? Решение: Прежде чем подпроект будет использовать этот компонент, вручную загрузите другой подпроект, чтобы убедиться, что компонент может быть получен (в случае разрешения загрузка не будет выполнена без разрешения).

qiankunтакже можно использоватьloadMicroAppвручную загружать подпроекты. Основные шаги заключаются в следующем:

  1. Точно так же, поскольку глобальные переменные не являются общими для подпроектов, основной проект предоставляет глобальную переменную для хранения компонентов черезpropsдля подпроектов, использующих компонент, а также прохождениеloadMicroAppфункция прошла.
const commonComponent = {};
registerMicroApps(
  [
    { 
      name: 'app-vue-hash', 
      entry: 'http://localhost:1111', 
      container: '#appContainer', 
      activeRule: '/app-vue-hash', 
      props: { data : { loadMicroApp, commonComponent } }
    },
  ],
)
  1. Подпроекты подключают функции загрузки и общедоступные переменные к глобальному
export async function mount(props) {
  window.commonComponent = props.data.commonComponent;
  window.loadMicroApp = props.data.loadMicroApp;
  render();
}
  1. Подпроект вручную загружает подпроект, предоставляющий компонент, на странице, которой необходимо использовать компонент, и когда он загружается, вы можете получить компонент.

Здесь следует отметить: поскольку подпроекты, предоставляющие компоненты, предоставляютсяloadMicroAppзагружены, поэтому общедоступные переменные, в которых хранятся компоненты, должны бытьloadMicroAppпередай. Также: Загрузка подпроектов требует предоставления контейнера вAPP.vueПредоставьте скрытый контейнер.

const app = window.loadMicroApp({
  name: 'app-vue-history',
  entry: 'http://localhost:2222', 
  container: '#appContainer2',
  props: { data: { commonComponent: window.commonComponent } }
})
await app.mountPromise;

Из-за сильной зависимости компонентов подпроекты здесь не могут работать независимо.Здесь есть два варианта регистрации компонентов:

  • Одним из них является использование асинхронных компонентов:
components: {
   HelloWorld: async () => {
      // 这个 app  最好写成当前页的全局变量
      // 因为卸载当前页时需要调用他的 `unmount` 来卸载子项目
      const app = window.loadMicroApp({
        name: 'app-vue-history',
        entry: 'http://localhost:2222', 
        container: '#appContainer2',
        props: { data: { commonComponent: window.commonComponent } }
      })
      await app.mountPromise;
      return window.commonComponent.HelloWorld
   }
}
  • Второй — динамическая регистрация, первое использованиеv-ifчтобы скрыть метку:
<HelloWorld v-if="loadingEnd"/>
async created() {
  const app = window.loadMicroApp({
    name: 'app-vue-history',
    entry: 'http://localhost:2222', 
    container: '#appContainer2',
    props: { data: { commonComponent: window.commonComponent } }
  })
  await app.mountPromise;
  Vue.component('HelloWorld', window.commonComponent.HelloWorld)
  this.loadingEnd = true;
},

vueВ документации описано, как обрабатывать состояние загрузки асинхронных компонентов:Обработка состояния загрузки

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

Если общий компонент и его подкомпоненты не зависят отstoreа такжеi18nДождитесь глобального плагина, вот одинпредположениеОбработать: не создавать экземплярVue, только для монтажа компонентов.

export async function mount(props) {
  // 如果有commonComponent变量,说明是另一个子项目通过 loadMicroApp 加载的
  // 他此时只需要挂载组件
  if(props.data.commonComponent){
     props.data.commonComponent.HelloWorld = HelloWorld;
  }else{
  // 没有 commonComponent 变量,说明是主项目通过 registerMicroApps 加载的
  // 当让这里只是一个简单的判断,也可以传递其他参数判断
    render();
  }
}

Если этот компонент и его подкомпоненты зависят отstoreа такжеi18nДля глобальных плагинов вам нужно вернуть функцию, а при вызове выполнить функцию напрямую:

export async function mount(props) {
  // 如果有commonComponent变量,说明是另一个子项目通过 loadMicroApp 加载的
  // 他此时只需要挂载组件
  if(props.data.commonComponent){
     const createHelloWorld = container => new Vue({
     	el: container,
        store,
        i18n,
        render: h => h(HelloWorld)
     });
     props.data.commonComponent.createHelloWorld = createHelloWorld;
  }else{
  // 没有 commonComponent 变量,说明是主项目通过 registerMicroApps 加载的
  // 当让这里只是一个简单的判断,也可以传递其他参数判断
    render();
  }
}

Родительский проект, повторно использующий компоненты подпроекта, также применяется в случае, когда компоненты являются общими для подпроектов. Если вы хотите реализовать «виджеты» (некоторые с диаграммами данных) для основного проекта, чтобы использовать подпроекты, вам необходимо использовать схему строгой зависимости.

гнездование цянькуня

Во-первых, поставитьqiankunПроект может быть изменен в соответствии с требованиями подпроекта, прежде чем к нему можно будет получить доступ.Основные изменения заключаются в следующем:

  1. Измените конфигурацию упаковки, чтобы разрешить междоменное иumd
  2. Измените корневой идентификатор, не используйте#app
  3. Измените файл маршрутизации и создайте экземпляр маршрута в файле записи.
  4. Исправлятьpublic-pathдокумент
  5. Измените входной файл наqiankunВыставлен требуемый жизненный цикл

Вариант 1: подпроект выполняется сам по себеqiankunПример

Существующие проблемы:

  1. Подпроекты не могут судить о том, работают ли они независимо или интегрированы на основе существующей информации.
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

Поскольку сам подпроект также являетсяqiankunпроект, поэтому автономная среда выполненияwindow.__POWERED_BY_QIANKUN__дляtrue, при интегрировании илиtrue.

Решение: определить еще одну глобальную переменную в файле ввода основного проекта.window.__POWERED_BY_QIANKUN_PARENT__ = true;, используйте эту переменную, чтобы определить, интегрирована ли она или работает независимо

  1. Изменение файла записи подпроекта

Основные моменты, на которые следует обратить внимание:

  • При переключении подпроектов избегайте дублирования регистрации дочерних проектов,
  • Поскольку подпроект будет введен с префиксом, маршрут проекта-внука также должен добавить этот префикс.
  • Помните о конфликтах контейнеров, подпроекты и проекты-внуки используют разные контейнеры
let router = null;
let instance = null;
let flag = false;
function render() {
  router = new VueRouter({
    base: window.__POWERED_BY_QIANKUN_PARENT__ ? '/app-qiankun' : '/',
    mode: 'history',
    routes,
  });
  const childRoute = ['/app-vue-hash','/app-vue-history'];
  const isChildRoute = path => childRoute.some(item => path.startsWith(item))
  const rawAppendChild = HTMLHeadElement.prototype.appendChild;
  const rawAddEventListener = window.addEventListener;
  router.beforeEach((to, from, next) => {
    // 从子项目跳转到主项目
    if(isChildRoute(from.path) && !isChildRoute(to.path)){
      HTMLHeadElement.prototype.appendChild = rawAppendChild;
      window.addEventListener = rawAddEventListener;
    }
    next();
  });

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

  if(!flag){
    registerMicroApps([
      { 
        name: 'app-vue-hash', 
        entry: 'http://localhost:1111', 
        container: '#appContainer', 
        activeRule: window.__POWERED_BY_QIANKUN_PARENT__ ? '/app-qiankun/app-vue-hash' : '/app-vue-hash', 
        props: { data : { store, router } }
      },
      { 
        name: 'app-vue-history',
        entry: 'http://localhost:2222', 
        container: '#appContainer', 
        activeRule: window.__POWERED_BY_QIANKUN_PARENT__ ? '/app-qiankun/app-vue-history' : '/app-vue-history',
        props: { data : store }
      },
    ]);
    
    start();
    flag = true
  }
}

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

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

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

export async function unmount() {
  instance.$destroy();
  instance = null;
  router = null;
}
  1. historyВнучатый проект шаблонной маршрутизацииbaseИсправлять
base: window.__POWERED_BY_QIANKUN_PARENT__ ? '/app-qiankun/app-vue-history' : 
      (window.__POWERED_BY_QIANKUN__ ? '/app-vue-history' : '/'),
  1. Изменение конфигурации упаковки

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

Измените конфигурацию упаковки внучатого проекта:

- library: `${name}-[name]`,
+ library: `${name}`,

Затем перезапустите его.

Причина в том,qiankunВозьмите жизненный цикл подпроекта и отдайте приоритет последнему подключенному к подпроекту, когда он работает.windowПо переменной, если это не функция жизненного цикла, то согласноappNameВыбирать. Позволятьwebpackизlibraryсоответствие стоимостиappNameВот и все.

Вариант 2: Основной проект будетqiankunФункция регистрации передается подпроекту

Основные шаги такие же, как и выше, но вотbug: внучатый проект не загружается. маршрутизация/app-qiankunнагрузкаqiankunПодпроекты, проектные проекты Grandson зарегистрированные подпроекты/app-qiankun/app-vue-hash, но этоqiankunПодпроект может загружаться нормально, а дочерний проект не загружается и об ошибке не сообщается.Я чувствую, что этоqiankunодин изbug, два проекта имеют общую часть префикса маршрутизации, а проект с более длинным путем не загружается.

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

qiankunКод, когда подпроект регистрирует подпроект, выглядит следующим образом:

  if(!flag){
    let registerMicroApps = parentData.registerMicroApps;
    let start = parentData.start;
    if(!window.__POWERED_BY_QIANKUN_PARENT__){
      const model = await import('qiankun');
      registerMicroApps = model.registerMicroApps;
      start = model.start;
    }
    registerMicroApps([
      { 
        name: 'app-vue-hash', 
        entry: 'http://localhost:1111', 
        container: window.__POWERED_BY_QIANKUN_PARENT__ ? '#appContainerParent' : '#appContainer', 
        activeRule: '/app-vue-hash', 
        props: { data : { store, router } }
      },
      { 
        name: 'app-vue-history',
        entry: 'http://localhost:2222', 
        container: window.__POWERED_BY_QIANKUN_PARENT__ ? '#appContainerParent' : '#appContainer', 
        activeRule: '/app-vue-history',
        props: { data : store }
      },
    ]);
    start();
    flag = true;
}

qiankunИспользовать сводку

  1. Наиболее распространенные проблемы: Иногда при обновлении страницы сообщается об ошибке, и контейнер не может быть найден.

Решение 1. В компонентеmountedРегистрация и запуск циклаqiankun

Решение 2:new Vue()После этого подождитеDOMПосле загрузки зарегистрируйтесь и начнитеqiankun

const vueApp = new Vue({
  router,
  store,
  render: h => h(App)
}).$mount("#app");
vueApp.$nextTick(() => {
  //在这里注册并启动 qiankun
})
  1. Мои предыдущие заботы: всеjsсценарий иcssВсе файлы кэшируются в памяти. Не приведет ли слишком много подпроектов к зависанию браузера?

пилаissueОтвет автора области:

После повторного использования зависимостей основного проектаjsа такжеcssобъем в2M - 5MТак что в принципе не беспокойтесь об этом.

  1. qiankunЗапускайте несколько приложений одновременноjsОбработка песочницы

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

УсыновленныйproxyПосле проксирования изменения глобальных переменных всех подприложений генерируются в закрытии и фактически не будут записываться обратно вwindowТаким образом, можно избежать загрязнения между несколькими экземплярами.

конец

Если есть какие-либо проблемы или ошибки в статье, пожалуйста, укажите на это, спасибо!