微前端模块联邦实践系列(6) - 集成React & Vue

307 阅读3分钟

本系列大纲

微前端中各个应用由于相互独立,因此技术栈的选择具有高度的自由性。正如我们在 微前端模块联邦实践系列(1) - 微前端入门 中提到的:

技术栈无关:各个微前端模块可以使用不同的技术栈(React、Vue、Angular等),这为团队选择最适合的工具提供了灵活性。

因此在本案例中的三个App,我采用的技术栈为(个人对React相对熟悉一点,Vue can do):

  • container: React
  • posts: React
  • albums: Vue

代码变更

下面主要以container app变更为例,post & albums app的变更大同小异,详情可以参照 github mfe-demo

webpack配置改造

以其中的container app为例,将以前的webpack.dev.js以及webpack.prod.js进行相同部分提取,创建webpack.common.js

// apps/container/config/webpack.common.js

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

module.exports = {
  output: {
    filename: '[name].[contenthash].js',
  },
  module: {
    rules: [
      {
        test: /.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env', '@babel/preset-react'],
          },
        },
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: 'public/index.html',
    }),
  ],
}

dev以及prod文件只需要关注它本身不同的部分即可

// apps/container/config/webpack.dev.js
module.exports = merge(commonConfig, {
  mode: 'development',
  devServer: {
    port: 8080,
    hot: true,
    historyApiFallback: true,
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'container',
      remotes: {
        postsApp: 'posts@http://localhost:8081/remoteEntry.js',
        albumsApp: 'albums@http://localhost:8082/remoteEntry.js',
      },
      shared: packageJson.dependencies,
    }),
  ],
})

// apps/container/config/webpack.prod.js
module.exports = merge(commonConfig, {
  mode: 'production',
  plugins: [
    new ModuleFederationPlugin({
      name: 'container',
      remotes: {
        postsApp: `posts@${process.env.MFE_POSTS_DOMAIN}remoteEntry.js`,
        albumsApp: `albums@${process.env.MFE_ALBUMS_DOMAIN}remoteEntry.js`,
      },
      shared: packageJson.dependencies,
    }),
  ],
})

bootstrap变为React方式渲染

const App = () => {
  return (
    <div>
      <h1>Container App</h1>
      <Button />
    </div>
  )
}

const root = createRoot(document.getElementById('containerRoot'))
root.render(<App />)

最终效果

react_and_vue

CSS冲突

更改posts以及albums中button的css样式,如下:

/* apps/posts/src/components/button.css */

.button {
  padding: 10px 20px;
  background-color: #424eb9;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.button:hover {
  background-color: #983669;
}

这里我将posts中的button背景颜色设置为蓝色,className为.button

/* apps/albums/src/components/Button.vue */

<style>
.button {
  padding: 10px 20px;
  background-color: #42b983;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.button:hover {
  background-color: #369870;
}
</style>

在albums中的button设置同样的className,颜色为绿色,最终的效果为:

css_conflict

可以看到posts中的button颜色被覆盖了,原因是因为我们是首先加载的posts app,然后才是albums。

css_conflict_2

// apps/container/src/bootstrap.js

const App = () => {
  return (
    <div>
      <h1>Container App</h1>
      <Button />
      <PostsApp />
      <AlbumsApp />
    </div>
  )
}

const root = createRoot(document.getElementById('containerRoot'))
root.render(<App />)

解决上述问题,可以参考 微前端模块联邦实践系列(2) - 微前端与模块联邦 中的相关办法,这里介绍几种常用的

BEM

BEM(Block Element Modifier)是一种命名约定方法,用于编写可维护、可复用的 CSS 代码。BEM 代表 Block(块)Element(元素)Modifier(修饰符)

分别给posts中的button以及albums中的button添加块级标识符,类似于命名空间,例如

.posts__button {}
.albums__botton {}

启用CSS Modules

在posts的webpack配置中,开启css modules功能

// apps/posts/config/webpack.common.js

{
  test: /.module.css$/,
  use: [
    'style-loader',
    {
      loader: 'css-loader',
      options: {
        modules: true,
      },
    },
  ],
}

使用css modules的方式引入样式

// apps/posts/src/components/Button.js

import * as styles from './button.module.css'

const Button = () => {
  return <button className={styles.button}>Click me!</button>
}

在dom中生成的css class为一串随机的hash值,不过这个也是可以配置的,具体请自行搜索。

css_modules

Scoped CSS (特定框架限定)

在某些框架中,比如vue,可以声明scoped css,这样的css类的范围被局限在组件内部,因此不会覆盖全局样式

<style scoped>
...
</style>

集成shadcn ui

ShadCN UI 是一个基于 React 和 Radix UI 的现代化 UI 组件库,旨在提供一组高度可定制、无障碍且易于使用的 UI 组件,帮助开发者快速构建美观且功能丰富的用户界面。

为什么选它?没有理由,纯粹个人觉得好看而已。

详情参看github提交记录,revision id: 8556d9420842529a6ff108e93f62bf088c034cd1

container & posts app

由于这两个app,采用的是React技术栈,而且纯手撸,因此shadcn-ui的集成还有点问题。具体可以follow以下步骤,传送门

  • 按照shadcn-ui的手动配置先执行一遍,Manual Installation
  • 添加jsconfig文件,配置路径alias,主要是为了使用shadcn的CLI来自动生成组件
  • webpack中添加path alias
  • 配置postcss用来做css预处理
    {
      test: /.css$/,
      include: path.resolve(__dirname, '../src'),
      use: ['style-loader', 'css-loader', 'postcss-loader'],
    }
    
  • 编写components.json,理论上使用npx shadcn@latest init会自动检测生成这个文件,但是由于我们纯手撸,所以shadcn不能检测出我们当前使用了什么freamwork,因此无法生成,没关系,手写一个就是
    {
      "$schema": "https://ui.shadcn.com/schema.json",
      "style": "default",
      "rsc": false,
      "tsx": false,
      "tailwind": {
        "config": "tailwind.config.js",
        "css": "src/main.css",
        "baseColor": "neutral",
        "cssVariables": true
      },
      "aliases": {
        "components": "@/components",
        "utils": "@/lib/utils",
        "ui": "@/components/ui"
      }
    }
    
    这里的aliases需要和之前配置的绝对路径匹配。
  • 使用shadcn CLI来生成第一个component,npx shadcn@latest add button,运行之后,会在apps/posts/src/components/ui下面生成一个button.jsx的组件

albums

Albums app 使用的是vue,对应的有 shadcn-vue 与之对应。步骤基本和上述类似,稍微有点变化的是对应的CLI,例如添加button组件的方式为:

npx shadcn-vue@latest add button

最终的效果如下:

shadcn_ui_integration