微前端模块联邦实践系列(3) - 第一个案例

536 阅读4分钟

本系列大纲

声明

  1. 本系列使用的数据全部来自于jsonplaceholder暴露出来的公共API。根据API数据,主要分为以下几个模块:
  • container:主要职责为提供基础的模块容器,显示当前user,切换user
  • posts:显示当前用户下所有的posts,以及每一个post对应的comments
  • albums:显示当前用户下所有的albums,以及在每一个album中的photos
  1. 文中出现的代码根据需要会进行部分截取,完整代码可以参看github repo,出现的部分命令可以自行参看各自的package.json中运行的命令。

apps

因此按照项目职责,分为三个app,container、posts以及albums。当container中切换用户之后,需要在posts以及albums对应的子app中显示对应用户的相应内容。

初始化posts (albums类似,不再赘述)

这里就做简化了,具体的配置可以直接参照github repo

webpack基本配置

// apps/posts/config/webpack.dev.js

const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  mode: 'development',
  output: {
    filename: '[name].[contenthash].js',
  },
  devServer: {
    port: 8081
    ,
    hot: true,
    historyApiFallback: true,
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: 'public/index.html',
    }),
  ],
}

暴露posts app, 配置模块联邦

// apps/posts/config/config.dev.js

const { ModuleFederationPlugin } = require('webpack').container

new ModuleFederationPlugin({
  // The name of the remote app, which will be used to reference it in other apps
  name: 'posts',
  
  // The name of the file that will be created in the dist folder, 
  // which contains the metadata and exposed modules for the remote app
  filename: 'remoteEntry.js',
  
  // The modules that will be exposed by this remote app
  exposes: {
    // The key is the name under which the module will be exposed
    // The value is the path to the module file within the remote app
    './PostsIndex': './src/index',
  },
}),

初始化container

container app中的初始webpack配置和posts app中的配置并没有太多区别,只是container app中的开发端口配置为8080,而posts app的开发端口为8081

使用posts app,配置模块联邦

// apps/container/config/config.dev.js

new ModuleFederationPlugin({
  // The name of the container app
  name: 'container',

  // Configuration for remote apps
  remotes: {
    // The key is the reference name of the remote app in the container app
    // The value is the name of the remote app and the URL of the remote entry file
    postsApp: 'posts@http://localhost:8081/remoteEntry.js',
  },
})

container app中src/index.js作为webpack的入口,code如下:

// apps/container/src/index.js

import 'postsApp/PostsIndex'

const component = () => {
  const element = document.createElement('div')

  element.innerHTML = ['Hello', 'container app'].join(' ')

  return element
}

const root = document.getElementById('containerRoot')
root.appendChild(component())

在没有配置模块联邦使用postsApp之前,使用npm run serve是可以直接在localhost:8080打开container app的,但是引入posts模块之后

import 'postsApp/PostsIndex'

会出现一个错误,如下:

import_posts_app_error

原因是postsApp是异步引入的,因此之前的导入方式就要变一变。首先新增一个bootstrap.js,将index.js文件中的内容全部复制过去,然后将index.js整体作为动态导入

// apps/container/src/index.js

import('./bootstrap')

然后再次运行,就可以看到加载posts以及albums之后的页面了

container_app_with_posts_app

Local Run & Integration Run

在上述提交的代码中,可以看到container/public/index.html中必须需要声明另外两个和subApp对应的rootId,不然subApp在container中挂载就会找不到对应app的root DOM节点,这样做不好的点是不能够自动挂载,必须要和containerApp协商好对应的rootId,我们将其中的代码稍微更改下

// apps/posts/src/index.js (albums类似)

const component = () => {
  const element = document.createElement('div')
  element.innerHTML = 'Hello, Posts app'
  return element
}

export const mount = (el) => {
  el.appendChild(component())
}

if (process.env.NODE_ENV === 'development') {
  const root = document.getElementById('postsRoot')
  if (root) {
    mount(root)
  }
}

我们将subApp的挂载权交由给container,使用container传递过来的root节点,这样就不需要协商dom节点的id是什么,以及应该放在页面的哪个地方。

为了兼顾能够同时在本地开发时启动subApp,所以我们这里做了对当前rootDom的判断,使其也能在本地运行。