Принцип микроинтерфейса и реальный бой (один спа-цянькунь)

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

Что такое микрофронтенд?

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

Зачем использовать его?

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

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

Несколько схем реализации микро-фронтендов

строить планы описывать преимущество недостаток
Переадресация маршрута Nginx Настройте обратный прокси-сервер через Nginx для сопоставления разных путей с разными приложениями. Просто, быстро и легко настроить Обновление браузера запускается при переключении приложений, что влияет на работу
вложение iframe Родительское приложение представляет собой одну страницу, а каждое дочернее приложение вкладывает iframe. Реализация проста, а суб-приложения имеют свои песочницы, которые естественно изолированы и не влияют друг на друга. Отображение стиля фрейма, совместимость и т. д. имеют ограничения, слишком простые и низкие
форма пакета npm Подпроекты выпускают исходный код в виде пакетов NPM; сборки и выпуски пакетов по-прежнему управляются базовым проектом, который интегрируется во время упаковки. Упаковка и развертывание выполняются медленно и не могут быть развернуты отдельно. Упаковка и развертывание выполняются медленно и не могут быть развернуты отдельно.
Универсальный центральный пьедестал Подпроекты могут использовать разные стеки технологий, подпроекты полностью независимы и не имеют зависимостей, единообразно управляются базовым проектом и комплектуются в соответствии с регистрацией, монтированием и выгрузкой DOM-узлов. Неограниченный стек технологий, развернутый отдельно Связь недостаточно гибкая
Специальная центральная направляющая пьедестала Между поднаправлениями используется один и тот же стек технологий, базовый проект и подпроект могут разрабатываться и развертываться отдельно, подпроект имеет возможность повторно использовать общую инфраструктуру базового проекта. Существует множество способов связи, которые развертываются отдельно Ограниченный стек технологий

Текущая основная среда микроинтерфейса

  • Single-SPA single-spa — это интерфейсное решение JavaScript для интерфейсных микросервисов (оно не обрабатывает изоляцию стилей, изоляцию выполнения js), которое реализует перехват маршрута и загрузку приложения.
  • Основанный на Single-SPA, qiankun предоставляет более готовый API (single-spa + sandbox+ import-html-entry), который не зависит от стека технологий и прост в доступе (такой же простой, как iframe).

single-spa

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

пьедестал (вью)

В базе вызываем методы registerApplication и start, предоставленные нам single-spa 1. Параметр registerApplication имеет четыре appNameOrConfig, appOrLoadApp, activeWhen и customProps. Соответствует зарегистрированному имени подпроекта и некоторым конфигурациям. fn выполняется при загрузке подпроекта, выполнении правил и параметров связи.

main.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import {registerApplication,start} from 'single-spa';

// 动态加载url
async function loadScript(url){
  return new Promise((resolve,reject)=>{
    let script = document.createElement('script');
    script.src = url;
    script.onload = resolve;
    script.onerror = reject;
    document.head.appendChild(script);
  })
}
// singleSpa 缺陷 不够灵活 不能动态加载js文件
// 样式不隔离 没有js沙箱的机制

registerApplication('myVueApp',
  async ()=>{
      //当匹配成功的时候,加载子应用的js
      await loadScript(`http://localhost:10000/js/chunk-vendors.js`);
      await loadScript(`http://localhost:10000/js/app.js`)
      return window.singleVue; // 子应用打包umd格式。bootstrap mount unmount
  },
  //当匹配到/vue的时候执行上面的方法
  location => location.pathname.startsWith('/vue'), 
)
//启动应用
start();


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

подпроект

Самое главное в подпроекте — предоставить три метода: начальную загрузку, монтирование, размонтирование и формат упаковки.

main.js

Здесь, с помощью предоставленного сообществомsingle-spa-vue, реагировать на использованиеsingle-spa-react. По умолчанию он экспортирует три метода.

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import singleSpaVue from 'single-spa-vue';

const appOptions = {
    el:'#vue', // 挂载到父应用中的id为vue的标签中
    router,
    render: h => h(App)
}
const vueLifeCycle = singleSpaVue({
  Vue,
  appOptions
})
// 如果是微前端模式下,single-spa会在window上挂在一个singleSpaNavigate的属性。
// 这时候我们需要将public_path改成子项目中的地址。
if(window.singleSpaNavigate){
  __webpack_public_path__ = 'http://localhost:10000/'
}
//这个是让子项目能够单独的运行
if(!window.singleSpaNavigate){
  delete appOptions.el;
  new Vue(appOptions).$mount('#app');
}

// 协议接入 我定好了协议 父应用会调用这些方法
export const bootstrap = vueLifeCycle.bootstrap;
export const mount = vueLifeCycle.mount;
export const unmount = vueLifeCycle.unmount;


配置子项目的打包方式 vue.config.js

module.exports = {
    configureWebpack:{
        output:{
            library:'singleVue',
            libraryTarget:'umd'
        },
        devServer:{
            port:10000
        }
    }
}

Недостатки одноместного спа

  • Как видно из рисунка выше, базу нужно вручную добавлять js при сопоставлении пути, если в подпроекте 10 000 js, то....
  • Стили не изолированы
  • Нет механизма для песочницы js

qiankun

Так же просто, как вставить iframe qiankun напрямую вставляет html в контейнер с помощью метода выборки, поэтому подпроекты должны разрешать междоменное

пьедестал

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import ElementUI from "element-ui";
import "element-ui/lib/theme-chalk/index.css";
Vue.use(ElementUI);
// 从qiankun中导出两个方法
import { registerMicroApps, start } from "qiankun";
const apps = [
  {
    name: "vueApp", // 应用的名字
    entry: "//localhost:10000",//加载的html路径
    container: "#vue", // 容器名
    activeRule: "/vue", // 激活的路径
  },
  {
    name: "reactApp",
    entry: "//localhost:20000",
    container: "#react",
    activeRule: "/react",
  },
];
registerMicroApps(apps); // 注册应用
start(); // 开启

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

подпроект

import Vue from 'vue'
import App from './App.vue'
import router from './router'

// Vue.config.productionTip = false


let instance = null
function render(props) {
    instance = new Vue({
        router,
        render: h => h(App)
    }).$mount('#app'); // 这里是挂载到自己的html中  基座会拿到这个挂载后的html 将其插入进去
}
if (window.__POWERED_BY_QIANKUN__) { // 动态添加publicPath
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
if (!window.__POWERED_BY_QIANKUN__) { // 默认独立运行
    render();
}
// 子组件的协议就ok了
export async function bootstrap(props) {

};
export async function mount(props) {
    render(props)
}
export async function unmount(props) {
    instance.$destroy();
}

子项目打包配置

module.exports = {
    devServer:{
        port:10000,
        // 需要允许跨域
        headers:{
            'Access-Control-Allow-Origin':'*'
        }
    },
    configureWebpack:{
        output:{
            library:'vueApp',
            libraryTarget:'umd'
        }
    }
}

связь приложения

  • Передача данных основана на URL, но возможность передачи сообщений слабая
  • Связь на основе CustomEvent
  • Связь между основным и дочерним приложениями на основе реквизита
  • Общение с глобальными переменными, Redux

схема изоляции css

Изоляция стилей между подприложениями:

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

Изоляция стиля между основным приложением и дочерним приложением:

  • Префикс условного элемента BEM (Block Element Modifier)
  • Создание неконфликтующих имен селекторов при объединении модулей CSS.
  • Shadow Domнастоящая изоляция

механизм песочницы js

  • Песочница моментальных снимков, запись моментальных снимков при подключении или отключении изолированной программной среды приложения и восстановление среды в соответствии со снимками при переключении (не может поддерживать несколько

пример)

  • Прокси прокси песочница, не влияет на глобальную среду

Снимок песочницы

  • 1. Снимок текущих свойств окна при активации
  • 2. Сравните содержимое снимка с текущими свойствами окна при деактивации
  • 3. Если свойство изменилось, сохраните его в ModifyPropsMap и восстановите свойство окна с помощью моментального снимка.
  • 4. При следующей активации сделать снимок еще раз и восстановить окно с результатом последней модификации
class SnapshotSandbox{
        constructor(){
            this.proxy = window; 
            this.modifyPropsMap = {}; 
            this.active();
        }
        active(){ // 激活
            this.windowSnapshot = {}; // 拍照 
            for(const prop in window){
                if(window.hasOwnProperty(prop)){
                    this.windowSnapshot[prop] = window[prop];
                }
            }
            Object.keys(this.modifyPropsMap).forEach(p=>{
                window[p] = this.modifyPropsMap[p];
            })
        }
        inactive(){ // 失活
            for(const prop in window){
                if(window.hasOwnProperty(prop)){
                    if(window[prop]  !== this.windowSnapshot[prop]){
                        this.modifyPropsMap[prop] = window[prop];
                        window[prop] = this.windowSnapshot[prop]
                    }
                }
            }
        }
    }

прокси-песочница

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

class ProxySandbox {
    constructor() {
        const rawWindow = window;
        const fakeWindow = {}
        const proxy = new Proxy(fakeWindow, {
            set(target, p, value) {
                target[p] = value; 
                return true
            },
            get(target, p) {
                return target[p] || rawWindow[p];
            }
        });
        this.proxy = proxy
    }
}

Рефакторинг микроинтерфейса на основе проекта jquery

Предыстория проекта

используется в проектеbackbonejsФреймворк jq также содержитbootstrapи ряд зависимостей. Хотя jquery — это шаттл, он немного громоздок в обслуживании. Отсюда и идея рефакторинга с микрофронтендами.

  • Режим маршрутизации, используемый в проекте, — это хеш-режим.

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

jquery

Сначала представим цянькунь

import {registerMicroApps, addGlobalUncaughtErrorHandler, start} from 'qiankun';

mian.js

function genActiveRule(routerPrefix) {
    return location => location.hash.startsWith('#' + routerPrefix);
}
$(function() {
    // 布局
    createLayout();
    // 路由
    createRoute();
    // 前往上次的页面
    gotoLastPage();
    registerMicroApps([
        {
            name: 'micro',
            entry: '//xxx.com/home/vue/',
            container: '#main-app',
            activeRule: genActiveRule('/vue')
        }
    ]);
    addGlobalUncaughtErrorHandler(event => {
        console.error(event);
        const {message: msg} = event;
        // 加载失败时提示
        if (msg && msg.includes('died in status LOADING_SOURCE_CODE')) {
            console.error('子应用加载失败,请检查应用是否可运行');
        }
    });
    start();
    monitorOrders.init();
});

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

Подпроект (vue)

main.js

import Vue from "vue";
import VueRouter from "vue-router";
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'

// import "./public-path";
import App from "./App.vue";
import routes from "./routes";

Vue.use(VueRouter);
Vue.use(ElementUI);
Vue.config.productionTip = false;

let instance = null;
let router = null;

/**
 * 渲染函数
 * 两种情况:主应用生命周期钩子中运行 / 微应用单独启动时运行
 */
function render() {
  console.log("window.__POWERED_BY_QIANKUN__", window.__POWERED_BY_QIANKUN__);
  // 在 render 中创建 VueRouter,可以保证在卸载微应用时,移除 location 事件监听,防止事件污染
  router = new VueRouter({
    // 运行在主应用中时,添加路由命名空间 /vue
    base: window.__POWERED_BY_QIANKUN__ ? "/vue" : "/",
    mode: "hash",
    routes,
  });

  // 挂载应用
  instance = new Vue({
    router,
    render: (h) => h(App),
  }).$mount("#app");
}

// 独立运行时,直接挂载应用
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

/**
 * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
 * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
 */
export async function bootstrap() {
  console.log("VueMicroApp bootstraped");
}

/**
 * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
 */
export async function mount(props) {
  console.log("VueMicroApp mount", props);
  render(props);
}

/**
 * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
 */
export async function unmount() {
  console.log("VueMicroApp unmount");
  instance.$destroy();
  instance = null;
  router = null;
}

vue.config.js

const path = require("path");
function resolve(dir) {
  return path.join(__dirname, dir);
}

const stage = process.env.ENV === "testing";

const publicPath =
  process.env.ENV === "production"
    ? xxxx:xxx
    

let output = {
  library: "Micro",
  // 将你的 library 暴露为所有的模块定义下都可运行的方式
  libraryTarget: "umd",
  // 按需加载相关,设置为 webpackJsonp_VueMicroApp 即可
  jsonpFunction: `webpackJsonp_customerMicro`,
};
let cssExtract = {};
if (stage) {
  output.filename = `js/[name].js?v=[hash]`;
  output.chunkFilename = `js/[name].js?v=[hash]`;
  cssExtract = {
    filename: "css/[name].css?v=[hash]",
    chunkFilename: "css/[name].css?v=[hash]",
  };
}
module.exports = {
  publicPath,
  devServer: {
    // 监听端口
    port: 10200,
    // 关闭主机检查,使微应用可以被 fetch
    disableHostCheck: true,
    // 配置跨域请求头,解决开发环境的跨域问题
    headers: {
      "Access-Control-Allow-Origin": "*",
    },
  },
  css: {
    extract: cssExtract,
  },
  configureWebpack: () => {
    const conf = {
      resolve: {
        alias: {
          "@": resolve("src"),
        },
      },
      output: output,
    };
    return conf;
  },
  chainWebpack(config) {
    config.plugins.delete("preload");
    config.plugins.delete("prefetch");
    config.module
      .rule("vue")
      .use("vue-loader")
      .loader("vue-loader")
      .tap((options) => {
        options.compilerOptions.preserveWhitespace = true;
        return options;
      })
      .end();
    config.when(process.env.NODE_ENV === "development", (config) => config.devtool("cheap-source-map"));
  },
};

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

Коллекция серий ресурсов Micro Front-end

Сборник серий микро-фронтенда

Спасибо всем

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

2. Подпишитесь на официальный аккаунт "Front-end Elite"! Присылайте качественные статьи время от времени.