Cómo implementar elegantemente el uso compartido de código entre aplicaciones

En la primera mitad de 2020, Webpack lanzó una función muy emocionante: Federación de módulos (traducido como federación de módulos). Esta función atrajo la atención de la industria tan pronto como se lanzó, e incluso se llama Game Changer en el campo. de construcción frontal. De hecho, esta tecnología realmente resuelve el problema de la reutilización de múltiples módulos de aplicaciones, y su solución es más elegante y flexible que las soluciones anteriores. Pero desde otra perspectiva, Module Federation representa una solución general y no se limita a una herramienta de compilación específica. Por lo tanto, también podemos implementar esta característica en Vite, y la comunidad ya ha madurado la solución.

1. El dolor de compartir módulos

Para un producto de Internet, generalmente hay diferentes aplicaciones subdivididas. Por ejemplo, los documentos de Tencent se pueden dividir en Word, Excel, ppt y otras categorías, y los sitios de PC de Douyin se pueden dividir en subsitios, como sitios de videos cortos, sitios de transmisión en vivo. , y sitios de búsqueda. , y cada subestación es independiente entre sí, y puede ser desarrollada y mantenida de forma independiente por diferentes equipos de desarrollo. Parece que no hay problema, pero de hecho, a menudo encuentra algunos problemas de intercambio de módulos, lo que significa que siempre habrá problemas en diferentes aplicaciones, algún código compartido, como componentes públicos, funciones de utilidad pública, dependencias públicas de terceros, etc. Para estos códigos compartidos, además de copiar y pegar, ¿hay alguna forma mejor de reutilizarlos?

Estos son algunos métodos comunes de reutilización de código:

1.1 Lanzamiento del paquete npm

La publicación de paquetes npm es una forma común de reutilizar módulos. Podemos empaquetar algunos códigos comunes en un paquete npm y luego hacer referencia a este paquete npm en otros proyectos. El proceso de actualización de la versión específica es el siguiente:

  1. Cambios en la biblioteca pública lib1, publicados en npm;
  2. Todas las aplicaciones instalan nuevas dependencias y realizan una depuración conjunta.

imagen.png

Encapsular paquetes npm puede resolver el problema de la reutilización de módulos, pero presenta nuevos problemas:

  • problemas de eficiencia del desarrollo. Cada cambio debe publicarse y todas las aplicaciones relacionadas deben instalar nuevas dependencias. El proceso es más complicado.
  • Problema de construcción del proyecto. Después de la introducción de la biblioteca pública, el código de la biblioteca pública debe empaquetarse en el producto final del proyecto, lo que da como resultado un producto de gran tamaño y una velocidad de construcción relativamente lenta.

Por lo tanto, esta solución no puede usarse como la solución final, sino que es solo un movimiento inútil para resolver el problema temporalmente.

1.2 Submódulo Git

A través del submódulo git, podemos encapsular el código en un repositorio Git público y luego reutilizarlo en diferentes aplicaciones, pero también debemos seguir los siguientes pasos:

  1. Envíe cambios a la biblioteca pública lib1 al almacén remoto de Git;
  2. Todas las aplicaciones actualizan el código del subalmacén a través del comando del submódulo git y realizan una depuración conjunta.

Se puede ver que el proceso general es casi el mismo que el de enviar paquetes npm, y todavía hay varios problemas en la solución del paquete npm.

1.3 Confiar en la externalización + introducción de CDN

La llamada externalización de dependencia (externa) significa que no necesitamos dejar que algunas dependencias de terceros participen en la construcción, sino usar un cierto código público. De acuerdo con esta idea, podemos declarar external para algunas dependencias en el motor de compilación y luego agregar la dirección CDN dependiente en el HTML, por ejemplo:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
  </head>
  <body>
    <div id="root"></div>
    <!-- 从 CDN 上引入第三方依赖的代码 -->
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/index.min.js"><script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/index.min.js"><script>
  </body>
</html>

Como se muestra en el ejemplo anterior, podemos usar CDN para importar react y react-dom, generalmente usando productos de formato UMD, para que diferentes proyectos puedan usar el mismo código dependiente a través de window.React, para lograr el módulo El efecto de reutilización. Sin embargo, este enfoque también tiene ciertas limitaciones:

  • Problemas de compatibilidad . No todas las dependencias tienen productos en formato UMD, por lo que esta solución no puede cubrir todos los paquetes npm de terceros.
  • Problema de orden de dependencia . Por lo general, debemos considerar el problema de las dependencias indirectas. Por ejemplo, para la biblioteca de componentes antd, también depende de react y moment, por lo que react y moment también necesitan external, y estos paquetes están referenciados en HTML, y el orden de las referencias debe estar estrictamente garantizado, como Se dice que si el momento se coloca detrás de antd, es posible que el código no se ejecute. La cantidad de dependencias indirectas detrás de los paquetes de terceros es generalmente enorme, y si se tratan una por una, será una pesadilla para los desarrolladores.
  • Problemas de volumen de producto . Después de que el paquete dependiente se declara externo, cuando la aplicación hace referencia a su dirección CDN, hará referencia completa al código dependiente. En este caso, no hay forma de eliminar el código inútil a través de Tree Shaking, lo que hará que el rendimiento de la aplicación disminuya. .

1.4 monorrepo

Como nuevo método de gestión de proyectos, Monorepo también puede resolver muy bien el problema de la reutilización de módulos. Bajo la arquitectura Monorepo, se pueden colocar varios proyectos en el mismo almacén de Git, y cada subproyecto interdependiente se depura a través de una cadena suave. La reutilización del código es muy conveniente. Si hay un cambio de código dependiente, utilice esta dependencia. Será se sintió inmediatamente en el proyecto.

imagen.png

Debo admitir que Monorepo es una muy buena solución al problema de la reutilización de módulos entre aplicaciones, pero al mismo tiempo, también tiene algunas limitaciones en el uso.

  • Todo el código de la aplicación debe colocarse en el mismo repositorio . Si es un proyecto antiguo y cada aplicación usa un repositorio Git, el ajuste de la estructura del proyecto será relativamente grande después de usar Monorepo, lo que significa que el costo de transformación será relativamente alto.
  • Monorepo en sí mismo también tiene algunas limitaciones naturales. Por ejemplo, cuando aumenta la cantidad de proyectos, llevará mucho tiempo instalar las dependencias, y el tiempo total de construcción del proyecto será más largo, etc. También necesitamos resolver la eficiencia del desarrollo. problemas causados ​​por estas limitaciones. Y este trabajo generalmente requiere de personal profesional para resolverlo, si no hay suficiente inversión en personal o garantía de infraestructura, Monorepo puede no ser una buena opción.
  • Problema de compilación del proyecto . Al igual que la solución de enviar paquetes npm, todos los códigos públicos deben ingresar al proceso de construcción del proyecto y el tamaño del producto seguirá siendo demasiado grande.

En segundo lugar, el concepto central de Module Federation

A continuación, presentemos formalmente Module Federation, es decir, la solución de federación de módulos, y veamos cómo resuelve el problema de la reutilización de módulos. Existen principalmente dos tipos de módulos en una federación de módulos: módulos locales y módulos remotos.

El módulo local es un módulo común, que forma parte del proceso de compilación actual, mientras que el módulo remoto no forma parte del proceso de compilación actual y se importa cuando se ejecuta el módulo local. Al mismo tiempo, el módulo local y el módulo remoto El módulo puede compartir algún código dependiente, como se muestra en la siguiente figura Mostrar:

imagen.png

Vale la pena recalcar que en una federación de módulos, cada módulo puede ser un módulo local e importar otros módulos remotos, o puede ser un módulo remoto y ser importado por otros módulos. Como se muestra en el siguiente ejemplo:

imagen.png

Lo anterior es el principal principio de diseño de la federación de módulos. Ahora analicemos las ventajas de este diseño:

  • Realice el intercambio de módulos en cualquier granularidad . La granularidad del módulo a la que se hace referencia aquí puede ser grande o pequeña, incluidas las dependencias npm de terceros, los componentes comerciales, las funciones de herramientas e incluso la aplicación frontal completa. Toda la aplicación front-end puede compartir productos, lo que significa que cada aplicación se desarrolla, prueba e implementa de forma independiente, lo que también es una realización de micro-front-end.
  • Optimice el volumen del producto de construcción . El módulo remoto se puede extraer del tiempo de ejecución del módulo local sin participar en la construcción del módulo local, lo que puede acelerar el proceso de construcción y reducir los artefactos de construcción.
  • Cargado bajo demanda en tiempo de ejecución . La granularidad de la importación de módulos remotos puede ser muy pequeña. Si solo desea usar la función de agregar del módulo app1, solo necesita exportar esta función en la configuración de compilación de app1 y luego importarla en el módulo local como import( 'app1/add') Eso es todo, esto hace posible cargar módulos bajo demanda.
  • Las dependencias de terceros son compartidas . A través del mecanismo de dependencia compartida en la federación de módulos, podemos realizar fácilmente el código de dependencia común entre módulos, evitando así varios problemas del esquema anterior de introducción externo + CDN.

Del análisis anterior, puede ver que la federación de módulos resuelve casi a la perfección el problema de compartir módulos en el pasado, e incluso puede lograr compartir a nivel de aplicación, logrando así el efecto de micro-frontends. A continuación, usemos ejemplos específicos para aprender cómo usar la capacidad de federación de módulos en Vite para resolver la reutilización de código.

3. Aplicación de la Federación de Módulos

La comunidad ha proporcionado una solución de federación de módulos de Vite relativamente madura: vite-plugin-federation, que implementa una capacidad de federación de módulos completa basada en Vite (o Rollup). A continuación, implementamos la aplicación de federación de módulos basada en él. Primero, inicialice los dos proyectos de scaffolding de Vue host y remote, y luego instale el complemento vite-plugin-federation respectivamente.Los comandos son los siguientes:

npm install @originjs/vite-plugin-federation -D

Luego agregue el siguiente código de configuración en el archivo de configuración vite.config.ts:

// 远程模块配置
// remote/vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import federation from "@originjs/vite-plugin-federation";


// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    // 模块联邦配置
    federation({
      name: "remote_app",
      filename: "remoteEntry.js",
      // 导出模块声明
      exposes: {
        "./Button": "./src/components/Button.js",
        "./App": "./src/App.vue",
        "./utils": "./src/utils.ts",
      },
      // 共享依赖声明
      shared: ["vue"],
    }),
  ],
  // 打包配置
  build: {
    target: "esnext",
  },
});


// 本地模块配置
// host/vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import federation from "@originjs/vite-plugin-federation";


export default defineConfig({
  plugins: [
    vue(),
    federation({
      // 远程模块声明
      remotes: {
        remote_app: "http://localhost:3001/assets/remoteEntry.js",
      },
      // 共享依赖声明
      shared: ["vue"],
    }),
  ],
  build: {
    target: "esnext",
  },
});

En la configuración anterior, hemos completado la exportación del módulo del módulo remoto y el registro del módulo remoto en el módulo local. Para la implementación específica del módulo remoto, puede consultar el código en el almacén de Github . A continuación, concentrémonos en cómo usar el módulo remoto.

Primero, necesitamos empaquetar el módulo remoto y confiar en el comando de ejecución bajo la ruta remota:

// 打包产物
pnpm run build
// 模拟部署效果,一般会在生产环境将产物上传到 CDN 
npx vite preview --port=3001 --strictPort

Luego, usamos el módulo remoto en el proyecto host, el código de muestra es el siguiente.

<script setup lang="ts">
import HelloWorld from "./components/HelloWorld.vue";
import { defineAsyncComponent } from "vue";
// 导入远程模块
// 1. 组件
import RemoteApp from "remote_app/App";
// 2. 工具函数
import { add } from "remote_app/utils";
// 3. 异步组件
const AysncRemoteButton = defineAsyncComponent(
  () => import("remote_app/Button")
);
const data: number = add(1, 2);
</script>


<template>
  <div>
    <img alt="Vue logo" src="./assets/logo.png" />
    <HelloWorld />
    <RemoteApp />
    <AysncRemoteButton />
    <p>应用 2 工具函数计算结果: 1 + 2 = {
   
   { data }}</p>
  </div>
</template>


<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

Luego, después de iniciar el proyecto con npm run dev, puede ver los siguientes resultados.

imagen.png

Los componentes y la lógica de función de utilidad de la aplicación 2 ya han tenido efecto en la aplicación 1, es decir, hemos completado la importación del tiempo de ejecución del módulo remoto en el módulo local. Ordenemos el proceso de uso general:

  1. El módulo remoto registra el módulo exportado a través de exposiciones, y el módulo local registra la dirección del módulo remoto a través de controles remotos.
  2. Los módulos remotos se crean y se implementan en la nube.
  3. El módulo remoto se introduce localmente importando 'nombre de módulo remoto/xxx' para realizar la carga en tiempo de ejecución.

4. Principio de implementación de la federación de módulos

A partir de los ejemplos anteriores, puede ver que Module Federation es relativamente fácil de usar y que el costo de transformación de los proyectos existentes no es grande. Entonces, ¿cómo se realiza una función tan poderosa y fácil de usar en Vite? A continuación, profundicemos en el principio de implementación detrás de MF y analicemos qué hace el complemento vite-plugin-federation detrás de él.

En general, hay tres elementos principales para realizar la federación de módulos:

  • Módulo host : Es un módulo local, utilizado para consumir módulos remotos.
  • Módulo remoto : es un módulo remoto, que se utiliza para producir algunos módulos y exponer el contenedor de tiempo de ejecución para que lo consuman los módulos locales.
  • Dependencia compartida : la dependencia compartida se utiliza para compartir dependencias de terceros entre módulos locales y módulos remotos.

Primero, echemos un vistazo a cómo el módulo local consume el módulo remoto. Anteriormente, escribimos declaraciones de importación como esta en módulos locales.

import RemoteApp from "remote_app/App";

Veamos en qué compila Vite este código.

// 为了方便阅读,以下部分方法的函数名进行了简化
// 远程模块表
const remotesMap = {
  'remote_app':{url:'http://localhost:3001/assets/remoteEntry.js',format:'esm',from:'vite'},
  'shared':{url:'vue',format:'esm',from:'vite'}
};


async function ensure() {
  const remote = remoteMap[remoteId];
  // 做一些初始化逻辑,暂时忽略
  // 返回的是运行时容器
}


async function getRemote(remoteName, componentName) {
  return ensure(remoteName)
    // 从运行时容器里面获取远程模块
    .then(remote => remote.get(componentName))
    .then(factory => factory());
}


// import 语句被编译成了这样
// tip: es2020 产物语法已经支持顶层 await
const __remote_appApp = await getRemote("remote_app" , "./App");

Se puede ver que además de compilar la declaración de importación, se agregan al código remoteMap y algunas funciones de utilidad, cuyo propósito es muy simple, es decir, extraer el módulo con el nombre correspondiente accediendo al contenedor de tiempo de ejecución remoto. El contenedor de tiempo de ejecución en realidad se refiere al objeto exportado del producto de empaquetado del módulo remoto remoteEntry.js. Echemos un vistazo a su lógica:

// remoteEntry.js
const moduleMap = {
  "./Button": () => {
    return import('./__federation_expose_Button.js').then(module => () => module)
  },
  "./App": () => {
    dynamicLoadingCss('./__federation_expose_App.css');
    return import('./__federation_expose_App.js').then(module => () => module);
  },
  './utils': () => {
    return import('./__federation_expose_Utils.js').then(module => () => module);
  }
};


// 加载 css
const dynamicLoadingCss = (cssFilePath) => {
  const metaUrl = import.meta.url;
  if (typeof metaUrl == 'undefined') {
    console.warn('The remote style takes effect only when the build.target option in the vite.config.ts file is higher than that of "es2020".');
    return
  }
  const curUrl = metaUrl.substring(0, metaUrl.lastIndexOf('remoteEntry.js'));
  const element = document.head.appendChild(document.createElement('link'));
  element.href = curUrl + cssFilePath;
  element.rel = 'stylesheet';
};


// 关键方法,暴露模块
const get =(module) => {
  return moduleMap[module]();
};


const init = () => {
  // 初始化逻辑,用于共享模块,暂时省略
}


export { dynamicLoadingCss, get, init }

Del código del contenedor de tiempo de ejecución podemos extraer información clave:

  • moduleMap se usa para registrar la información del módulo exportado, todos los módulos declarados en el parámetro de exposiciones se empaquetarán en un archivo separado y luego se importarán a través de la importación dinámica.
  • El contenedor exporta un método de obtención muy crítico, de modo que el módulo local pueda acceder al módulo remoto llamando a este método.

Hasta ahora, hemos solucionado el proceso de interacción entre el contenedor de tiempo de ejecución del módulo remoto y el módulo local, como se muestra en la siguiente figura.

imagen.png

A continuación, pasamos a analizar la implementación de las dependencias compartidas. Tomando el proyecto de muestra anterior como ejemplo, después de que el módulo local establece el parámetro shared: ['vue'], cuando ejecuta el código del módulo remoto, una vez que se encuentra con la situación de introducir vue, dará prioridad al uso del vue local. en lugar del vue remoto en el módulo.

imagen.png

Centrémonos en la lógica de la inicialización del contenedor y volvamos a la lógica de la función de garantía después de compilar el módulo local.

// host


// 下面是共享依赖表。每个共享依赖都会单独打包
const shareScope = {
  'vue':{'3.2.31':{get:()=>get('./__federation_shared_vue.js'), loaded:1}}
};
async function ensure(remoteId) {
  const remote = remotesMap[remoteId];
  if (remote.inited) {
    return new Promise(resolve => {
        if (!remote.inited) {
          remote.lib = window[remoteId];
          remote.lib.init(shareScope);
          remote.inited = true;
        }
        resolve(remote.lib);
    });
  }
}

Se puede encontrar que la lógica principal de la función de garantía es pasar la información de dependencia compartida al contenedor de tiempo de ejecución del módulo remoto e inicializar el contenedor. A continuación, ingresamos el inicio lógico de la inicialización del contenedor.

const init =(shareScope) => {
  globalThis.__federation_shared__= globalThis.__federation_shared__|| {};
  // 下面的逻辑大家不用深究,作用很简单,就是将本地模块的`共享模块表`绑定到远程模块的全局 window 对象上
  Object.entries(shareScope).forEach(([key, value]) => {
    const versionKey = Object.keys(value)[0];
    const versionValue = Object.values(value)[0];
    const scope = versionValue.scope || 'default';
    globalThis.__federation_shared__[scope] = globalThis.__federation_shared__[scope] || {};
    const shared= globalThis.__federation_shared__[scope];
    (shared[key] = shared[key]||{})[versionKey] = versionValue;
  });
};

Cuando se puede acceder a la tabla de dependencias compartidas del módulo local en el módulo remoto, las dependencias del módulo local (como vue) también se pueden usar en el módulo remoto. Ahora echemos un vistazo a cómo se convierte el código de importación de import { h } de 'vue' en el módulo remoto, como se muestra a continuación.

// __federation_expose_Button.js
import {importShared} from './__federation_fn_import.js'
const { h } = await importShared('vue')

No es difícil ver que la lógica de procesamiento de los módulos dependientes de terceros se concentra en la función importShared, averigüémoslo.

// __federation_fn_import.js
const moduleMap= {
  'vue': {
     get:()=>()=>__federation_import('./__federation_shared_vue.js'),
     import:true
   }
};
// 第三方模块缓存
const moduleCache = Object.create(null);
async function importShared(name,shareScope = 'default') {
  return moduleCache[name] ? 
    new Promise((r) => r(moduleCache[name])) : 
    getProviderSharedModule(name, shareScope);
}


async function getProviderSharedModule(name, shareScope) {
  // 从 window 对象中寻找第三方包的包名,如果发现有挂载,则获取本地模块的依赖
  if (xxx) {
    return await getHostDep();
  } else {
    return getConsumerSharedModule(name); 
  }
}


async function getConsumerSharedModule(name , shareScope) {
  if (moduleMap[name]?.import) {
    const module = (await moduleMap[name].get())();
    moduleCache[name] = module;
    return module;
  } else {
    console.error(`consumer config import=false,so cant use callback shared module`);
  }
}

Dado que la información de dependencia compartida se montó cuando el contenedor se inicializó cuando el módulo remoto se está ejecutando, el módulo remoto puede percibir fácilmente si la dependencia actual es una dependencia compartida. Si es una dependencia compartida, use el código dependiente del módulo local. De lo contrario, utilice el módulo remoto. Su propio código de producto dependiente, el diagrama esquemático es el siguiente.

imagen.png

V. Resumen

Primero, les presenté las soluciones históricas al problema de la reutilización de módulos, que incluyen principalmente la publicación de paquetes npm, Git Submodule, que dependen de la externalización + importación de CDN y la arquitectura Monorepo. También analicé sus respectivas ventajas y limitaciones, y luego presenté Module The concepto de Federación (MF), y analizó por qué puede resolver el problema de compartir módulos casi a la perfección, las razones principales incluyen la realización de granularidad arbitraria de compartir módulos, reducir el tamaño de los productos de construcción, cargar bajo demanda en tiempo de ejecución y compartir terceros. dependencias de las partes.aspecto.

A continuación, usaré un ejemplo de proyecto específico para decirle cómo usar la función de federación de módulos en Vite, es decir, para completar la construcción de MF a través del complemento vite-plugin-federation. Finalmente, también le ofrecí una introducción detallada a los principios de implementación subyacentes de MF y analicé el mecanismo de implementación y la lógica de compilación principal de MF desde tres perspectivas: módulos locales, módulos remotos y dependencias compartidas.

Supongo que te gusta

Origin blog.csdn.net/xiangzhihong8/article/details/131449848
Recomendado
Clasificación