从0到1实现Webpack5模块联邦

262 阅读6分钟

本文将从0搭建开发环境,实现一个最简单的模块联邦功能,理解模块联邦原理。

模块联邦工作原理

模块联邦的工作原理其实就是我们有A、B两个项目,我们在A项目里导出一个模块,然后B模块可以引入这个模块进行使用,就像引入一个npm包一样。这个模块可以是一个组件也可以是一个功能函数,这样就实现了项目间模块的共享。

如果我们有更多的项目,比如C、D,那么C、D项目都可以引入A项目导出的模块,同时C、D也可以导出自己的模块给其它项目使用。

大致原理就是这样,接下来我们来实现它。

搭建项目开发环境

首先我们用Webpack搭建一个Vue项目的开发环境,我们创建一个文件夹webpack-federation,然后在下面再创建两个文件夹,分别是project1和project2作为我们的两个项目。然后我们进入这两个目录分别执行pnpm init来初始化我们的package.json文件。

进入project1目录,安装我们需要的包:

pnpm install webpack webpack-cli webpack-dev-server html-webpack-plugin vue-loader -D
pnpm install vue

创建webpack.config.js

然后创建webpack.config.js文件

const path = require("path");
const { VueLoaderPlugin } = require("vue-loader");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  entry: "./src/main.js",
  mode: "development",
  output: {
    path: path.resolve(__dirname, "./dist"),
    filename: "name[hash:6].js",
    clean: true, // 在生成文件之前清空 output 目录
  },
  devServer: {
    port: 8080,
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: "vue-loader",
      },
    ],
  },
  plugins: [
    new VueLoaderPlugin(),
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "./public/template.html"),
    }),
  ],
};

src目录

创建src目录,然后创建main.js和App.vue

main.js

// main.js
import { createApp } from "vue";
import App from "./App.vue";

const app = createApp(App);
app.mount("#app");

App.vue

// App.vue
<template>
  <div>This is project1</div>
</template>

<script setup></script>

template

然后创建public/template.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Project1</title>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>

package.json scripts

在package.json添加scripts

  "scripts": {
    "start": "webpack serve",
    "build": "webpack"
  },

我们执行pnpm run start,可以看到项目正常启动

image.png

搭建project2项目

我们用同样的方式搭建项目project2,可以将project1下的src、public目录以及webpack.config.js拷贝到project2,然后修改webpack.config.js里的服务端口为8081。

更改project2/src/App.vue

// project2/src/App.vue
<template>
  <div>This is project2</div>
</template>

<script setup></script>

将project1/package.json下的scripts和dependencies拷贝到project2/package.json下

// 拷贝的内容
  "scripts": {
    "start": "webpack serve",
    "build": "webpack"
  },
    "devDependencies": {
    "html-webpack-plugin": "^5.5.0",
    "vue-loader": "^17.0.1",
    "webpack": "^5.75.0",
    "webpack-cli": "^5.0.1",
    "webpack-dev-server": "^4.11.1"
  },
  "dependencies": {
    "vue": "^3.2.45"
  }

然后在preject2下执行

pnpm install
pnpm run start

可以看到在8081端口启动了project2项目

image.png

两个项目搭建完毕,到目前为止还没有涉及模块联邦的功能,接下来我们实现模块联邦功能。

实现模块联邦

我们要实现的功能是在project1里使用project2里共享出来的模块。

比如我们在project2里有一个List组件,这个组件我们想要和其它项目共享,其它项目也可以使用这个组件,那么我们可以在prject2里将这个组件暴露出去,然后其它项目再引入这个组件就可以了。

新建List.vue

我们首先新建project2/src/List.vue文件 内容如下:

<template>
  <div>This is List Component</div>
</template>

<script setup></script>

然后我们在project2/src/App.vue里可以使用它

// project2/src/App.vue
<template>
  <div>This is project2</div>
  <List />
</template>

<script setup>
import List from "./List.vue";
</script>

这时页面显示如下

image.png

List组件在project2里使用正常

导出共享模块

我们想要这个List.vue不仅在project2里可以使用,在其它项目里也可以使用,那么我们可以将这个模块暴露,我们需要使用一个模块联邦的插件,在project2/webpack.config.js里我们增加如下配置:

// webpack.config.js
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
// ...其它配置
  plugins: [
    new ModuleFederationPlugin({
      name: "remoteV2",
      library: { type: "var", name: "remoteV2" },
      filename: "remoteEntry.js",
      exposes: [
        {
          "./List": "./src/List.vue",
        },
      ],
    }),
  ],
}

相关配置项说明如下

ModuleFederationPlugin

ModuleFederationPlugin是webpack提供的内置插件

name、library

我们的项目导出的模块的整体名字,这里我们设置为'remoteV2',这个名字可以任意设置,一般和library配合使用,当我们进行如下配置:

      name: "remoteV2",
      library: { type: "var", name: "remoteV2" },

那么我们导出的模块会挂载在window.remoteV2下面,其它项目使用该模块的时候会从window.remoteV2取模块导出的内容。

filename

我们导出的模块的文件名

比如我们设置

filename: "remoteEntry.js",

那么就会在dist目录下打包生成一个remoteEntry.js文件,可以理解为将我们需要导出的模块作为入口文件,然后打包后放到remoteEntry.js文件里面,其它项目需要使用的时候通过http://localhost:8080/remoteEntry.js可以访问我们共享的模块。

exposes

exposes就是我们要导出的模块,比如我们设置

      exposes: [
        {
          "./List": "./src/List.vue",
        },
      ],

即我们会导出./src/List.vue模块,然后给这个导出的模块取一个名字./List,这个./List名字在其它项目导入模块的时候需要使用。

使用共享的模块

我们在project2里导出了需要共享的模块,然后我们在project1里使用这个模块。

在project1/webpack.config.js里新增如下配置

// webpack.config.js
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
// ...其它配置
  plugins: [
    new ModuleFederationPlugin({
      name: "remoteV1",
      remotes: {
          "app_project1": "remoteV2@http://localhost:8081/remoteEntry.js",
      },
    }),
  ],
}

然后再project1/App.vue里引入这个远程的共享模块

// project1/App.vue
<template>
  <div>This is project1</div>
  <div>下面是从project2 远程加载的组件</div>
  <List />
</template>

<script setup>
import { defineAsyncComponent } from "vue";
const List = defineAsyncComponent(() => import("app_project1/List"));
</script>

其中defineAsyncComponent是Vue提供的用来定义远程加载组件的Api

然后我们再启动project2和project1项目,发现List组件都能正确加载。

image.png

下面我们对使用共享模块的配置做一个说明

其中name: "remoteV1"在导出模块的时候才需要使用,是导出模块的时候需要挂在windows下的变量,我们这里随便取个名字。

然后是关键配置

      remotes: {
          "app_project1": "remoteV2@http://localhost:8081/remoteEntry.js",
      },

remotes配置我们需要使用的远程模块,我们配置了一个远程模块名字是app_project1,这个名字是可以自己随便定义的,使用的时候用自己定义的这个名字就好了,这个模块的加载地址是http://localhost:8081/remoteEntry.js

同时在地址前有一个remoteV2@,这个名字需要和project1导出时候设置的名字保持一致。当导入模块的时候,project1会先去加载http://localhost:8081/remoteEntry.js文件,然后在文件加载成功之后,会去window.remoteV2下去取文件导出的内容。

然后我们在App.vue里使用import("app_project1/List")的方式引入我们需要的组件,我们可以认为app_project1是project1整个导出的模块,但是我们只需要导出的整个模块里的List组件。

回顾一下project1导出的配置

      exposes: [
        {
          "./List": "./src/List.vue",
        },
      ],

我们需要使用./Listapp_project1 + ./List就是app_project1/List这里可以看做是一个路径拼接的方式。

实际项目中我们导出的可能有多个组件,类似

      exposes: [
        {
          "./List": "./src/List.vue",
          "./Item": "./src/Item.vue",
          "./Menu": "./src/Menu.vue",
        },
      ],

使用的时候就是

import { defineAsyncComponent } from "vue";
const List = defineAsyncComponent(() => import("app_project1/List"));
const Item = defineAsyncComponent(() => import("app_project1/Item"));
const Menu = defineAsyncComponent(() => import("app_project1/Menu"));

以上就是模块联邦功能的简单实现

本文代码git 仓库