一、为什么要搞定微前端里的样式和状态问题?

1.1 实际遇到的麻烦

很多公司做大一点的前端项目,会拆成几个子应用,比如电商后台的商品管理、订单处理、用户中心,之前是三个独立的代码库,后来要合并成微前端,这样可以共用一些基础库,不用每个项目都装一遍,节省空间。但上线后问题来了:商品子应用里写的按钮样式,订单子应用一引用,按钮颜色就变了,完全不符合设计稿;还有用户登录后,用户中心显示的用户名是张三,订单页面显示的用户名是李四,刷新页面后还会乱,这就是样式冲突和状态混乱的问题,得赶紧解决。

1.2 选Module Federation的原因

之前也试过别的微前端方案,比如要加一个单独的基座框架,还要学新的语法,对前端同学来说太麻烦了,就像要租房子还要先考个租房证一样。后来发现Webpack5自带的Module Federation就很好,不用额外框架,就像家里的路由器,不同的房间(子应用)都能连同一个WiFi,还能各自有自己的房间号,不会搞混,刚好适合我们的需求。

二、具体怎么用Module Federation解决?

2.1 样式隔离:给每个子应用的样式加“门牌”

样式冲突的核心是“类名撞了”,就像两个房间都叫“卧室”,隔壁进来会搞错。解决方法很简单:给每个子应用的所有样式类名都加自己的前缀,比如商品子应用的类名都叫goods-开头,订单子应用叫order-开头,再用CSS Modules把类名加一串随机的哈希后缀,这样就算两个子应用都叫button,最终生成的类名也会是goods-button-abc123和order-button-def456,绝对不会撞。

2.1.1 样式隔离的代码示例

我们拿商品子应用的Button组件举例,写法很简单:

// goods-app/src/components/GoodsButton.jsx
import styles from './GoodsButton.module.css';

// 所有类名都加goods前缀,CSS Modules自动加哈希后缀,避免冲突
export default function GoodsButton({ children, onClick }) {
  return (
    <button className={styles.goodsButton} onClick={onClick}>
      {children}
    </button>
  );
}
// goods-app/src/components/GoodsButton.module.css
/* 前缀是goods,哪怕内部样式也不会和其他应用冲突 */
.goodsButton {
  background: #2563eb;
  color: white;
  padding: 8px 16px;
  border-radius: 4px;
  border: none;
  cursor: pointer;
}

.goodsButton:hover {
  background: #1d4ed8;
}

然后要配置子应用的Webpack,告诉Webpack用Module Federation暴露这个组件,同时用CSS Modules处理样式:

// goods-app/webpack.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
  mode: 'development',
  devServer: { port: 3001 }, // 子应用的端口,用来区分不同子应用
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        use: 'babel-loader',
      },
      // 处理CSS Modules,确保类名生成带哈希的唯一标识
      {
        test: /\.module\.css$/,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: { modules: true },
          },
        ],
      },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'goodsApp', // 子应用的唯一名字,不能和其他应用重名
      filename: 'remoteEntry.js', // 暴露的入口文件,主应用会引入这个
      exposes: {
        // 暴露GoodsButton组件,主应用可以直接用
        './GoodsButton': './src/components/GoodsButton.jsx',
      },
      shared: {
        react: { singleton: true }, // 只装一次React,避免重复加载
        'react-dom': { singleton: true },
      },
    }),
  ],
};

主应用的配置也很简单,引入商品子应用,这样就能用商品子应用的组件了:

// main-app/webpack.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
  mode: 'development',
  devServer: { port: 3000 }, // 主应用的端口
  module: {
    rules: [
      { test: /\.jsx?$/, exclude: /node_modules/, use: 'babel-loader' },
      // 主应用的样式不用模块化,或者也加前缀,看需求
      { test: /\.css$/, use: ['style-loader', 'css-loader'] },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'mainApp',
      remotes: {
        // 引入商品子应用,地址是子应用的remoteEntry.js文件
        goodsApp: 'goodsApp@http://localhost:3001/remoteEntry.js',
      },
      shared: {
        react: { singleton: true },
        'react-dom': { singleton: true },
      },
    }),
  ],
};

2.2 全局状态共享:用公共的“前台信息”

状态混乱的核心是“每个人都存一份用户信息”,就像公司前台的名字,每个人都自己记,有人改了别人不知道。解决方法是搞一个共享的Store,就像公司的公共前台,所有子应用都用这个前台的信息,谁改了大家都能看到,不用自己存。

2.2.1 全局状态共享的代码示例

我们做一个共享的用户Store,主应用提供,所有子应用都可以用:

// main-app/src/shared-store/userStore.js
import { createContext, useContext, useState } from 'react';

// 创建共享的Context,这是公共的用户状态,就像公司的前台
const UserContext = createContext();

// 提供状态的组件,主应用用这个包裹整个应用,所有子应用都能拿到
export function UserProvider({ children }) {
  const [user, setUser] = useState({
    id: 1,
    name: '张三',
    avatar: '/avatar.jpg',
  });

  // 暴露修改用户的方法,所有应用都可以调用,修改后大家同步
  const updateUser = (newUser) => setUser(newUser);

  return (
    <UserContext.Provider value={{ user, updateUser }}>
      {children}
    </UserContext.Provider>
  );
}

// 自定义Hook,方便子应用直接用,不用每次都引入Context
export function useUser() {
  return useContext(UserContext);
}

然后要在主应用的Webpack里暴露这个共享Store,这样子应用才能引入:

// main-app/webpack.config.js,修改plugins部分
plugins: [
  new ModuleFederationPlugin({
    name: 'mainApp',
    remotes: {
      goodsApp: 'goodsApp@http://localhost:3001/remoteEntry.js',
    },
    exposes: {
      // 暴露共享的用户Store,子应用可以引入使用
      './userStore': './src/shared-store/userStore.js',
    },
    shared: {
      react: { singleton: true },
      'react-dom': { singleton: true },
    },
  }),
]

子应用(比如订单子应用)里用这个共享状态,不用自己维护用户信息:

// order-app/src/pages/OrderPage.jsx
// 引入主应用暴露的共享Store,注意路径
import { useUser } from 'mainApp/userStore';
// 引入商品子应用的GoodsButton组件
import GoodsButton from 'goodsApp/GoodsButton';

export default function OrderPage() {
  // 直接用共享的用户状态,不用自己写逻辑
  const { user, updateUser } = useUser();

  // 点击按钮修改用户名字,所有应用都会同步
  const handleChangeName = () => {
    updateUser({ ...user, name: '李四' });
  };

  return (
    <div>
      <h1>订单页面</h1>
      <p>当前用户:{user.name}</p>
      <GoodsButton onClick={handleChangeName}>改名</GoodsButton>
    </div>
  );
}

三、这个方案的好坏分析

3.1 优点

首先,不用复杂的基座框架,前端同学只要懂Webpack和React就能上手,学习成本极低,就像买了个现成的路由器,插上线就能用;其次,样式隔离绝对靠谱,前缀+CSS Modules的组合,基本不会出现样式冲突的问题;第三,状态共享简单,一个公共Store搞定,不用再做复杂的跨应用通信,适合中等规模的项目,比如有3-5个子应用的情况;还有,Module Federation是Webpack自带的,不用额外维护依赖,减少了项目的复杂度。

3.2 缺点

首先,必须用Webpack5以上的版本,低版本的Webpack不支持Module Federation,这点要注意;其次,如果共享Store写得不好,比如随便改数据,会导致子应用之间互相影响,比如订单子应用改了用户的密码,用户中心也会跟着变,容易出问题;第三,子应用的依赖(比如React)必须是单例,不然会重复加载,占用更多内存,影响页面性能;还有,上线的时候要配置好远程子应用的地址,不然生产环境会报错。

四、要注意的坑

4.1 样式的坑

首先,所有样式类名都要加前缀,哪怕是子应用内部的样式,不要偷懒,比如子应用里的表格类,也要叫goods-table,不然内部会和自己的其他组件冲突;其次,CSS Modules的配置要对,一定要开modules选项,不然不会生成哈希类名;第三,全局样式比如reset.css,只在主应用引入,子应用不要重复引入,不然reset会覆盖主应用的样式,导致样式混乱;还有,不要在子应用里写全局样式,比如直接写table的样式,必须用CSS Modules。

4.2 状态的坑

首先,共享状态要做单向数据流,尽量只有主应用能修改共享状态,子应用只能调用主应用提供的方法修改,不要子应用自己改;其次,共享的变量要命名规范,比如叫sharedUser,不要叫user,避免子应用自己也有user变量,导致冲突;第三,要给共享状态做校验,比如修改用户信息的时候,要检查格式是否正确,不然会导致整个项目的状态混乱。

4.3 Module Federation的坑

首先,主应用和子应用的Webpack版本必须一致,不然Module Federation会抛出奇怪的错误,比如找不到remoteEntry.js;其次,暴露和引入的路径要写对,比如主应用暴露的是./src/shared-store/userStore.js,子应用引入的时候要写对名字,不要写错路径;第三,开发的时候,远程子应用的端口要正确,比如商品子应用的端口是3001,不要写成3002,不然主应用连接不上。

五、总结

微前端架构下,React子应用的样式隔离和全局状态共享是两个核心痛点,用Webpack5的Module Federation方案,能简单高效地解决这两个问题,不用额外的复杂框架,学习成本低,上手快,适合中等规模的前端项目。只要注意样式前缀、状态规范、Webpack版本等细节,就能搭建出稳定的微前端架构,避免样式冲突和状态混乱,提升开发效率,减少重复代码,让多个子应用可以协同工作,共用资源。