六边形架构的目标是把业务核心与外部世界隔离开,Webpack 5 并非直接新增了六边形架构这个特性,而是通过模块联邦、异步边界和精细代码分割,让前端工程可以真正实现端口与适配器的物理隔离。团队如果只把 Webpack 当作打包器,往往会忽略它的模块解析和依赖约束能力;如果把 Webpack 5 的配置当作架构边界的执行器,领域层、端口层和适配器层就能在编译期得到清晰划分。

一、六边形架构与 Webpack 5 的边界映射
六边形架构通常被划分为领域层、端口层和适配器层。领域层包含业务规则,端口层是领域层定义的一组接口,例如 IUserRepository 或 IEmailGateway,适配器层则实现这些接口并负责与 HTTP、数据库、第三方 SDK 等外部细节通信。依赖方向必须从适配器指向领域层,绝不能反向。
在前端工程中,这种方向约束比后端更难落实,因为前端代码往往混合了 UI 组件、API 调用和状态管理。Webpack 5 可以通过 entry、resolve.alias 和 cacheGroups 把不同层级编译成独立块。例如将领域代码单独打包为 domain.bundle.js,适配器代码分别打包为 httpAdapter.bundle.js 和 uiAdapter.bundle.js。这样一旦领域包中混入了适配器依赖,构建产物的大小和依赖图就会出现明显变化,便于 CI 检查。
下面是一个基础配置,它展示了如何用多个入口和 SplitChunks 区分领域包与适配器包。
// webpack.config.js
module.exports = {
entry: {
domain: './src/domain/index.js',
httpAdapter: './src/adapters/http/index.js',
uiAdapter: './src/adapters/ui/index.jsx'
},
output: {
filename: '[name].bundle.js',
path: __dirname + '/dist'
},
optimization: {
splitChunks: {
cacheGroups: {
domainVendor: {
test: /[\\/]node_modules[\\/](rxjs|immer)[\\/]/,
name: 'domain-vendor',
chunks: 'all'
}
}
}
}
};
这个配置并没有完全禁止反向依赖,但它让边界从逻辑概念变成了可观测的构建单元。更好的做法是在 CI 中解析 bundle 依赖,检查 domain.bundle.js 是否包含 adapter 目录的模块名。
二、用模块联邦暴露远程适配器
Webpack 5 的模块联邦能力允许一个独立构建暴露模块,另一个构建在运行时加载。六边形架构中,适配器天然适合作为远程模块暴露,因为适配器实现可以独立部署、独立升级,而不影响领域层。
比如说一个用户服务适配器 HttpUserRepository 实现了领域层定义的 IUserRepository。可以将它配置为远程模块,宿主应用在运行时通过动态导入获取实现。领域层只需要依赖端口接口,不需要知道适配器来自哪个远程地址。代码示例展示了一个适配器远程应用的配置。
// 适配器远程应用的 webpack.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'userAdapter',
filename: 'remoteEntry.js',
exposes: {
'./HttpUserRepository': './src/adapters/http/HttpUserRepository'
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' }
}
})
]
};
宿主应用不需要在编译期知道适配器的具体实现,只需在端口层保留接口类型。动态导入返回一个模块对象,调用方再实例化适配器。这样可以避免把基础设施代码静态打包进领域层,也让适配器团队可以独立发布。
模块联邦的共享依赖配置很关键。如果宿主和远程应用各自加载一份 React,会导致 Hooks 或 Context 出现多实例问题。设置 singleton: true 可以强制整个运行时只使用一个 React 实例。
三、依赖方向约束与编译期检查
六边形架构最怕的是依赖方向被破坏。开发者可能顺手在领域服务里 import 一个 UI 组件,或者直接调用 fetch。Webpack 本身不负责架构规则,但可以配合 ESLint、dependency-cruiser 和 resolve.alias 来阻断违规导入。
ESLint 的 no-restricted-imports 规则可以禁止领域层文件导入适配器目录。下面是一段配置示例。
// .eslintrc.js
module.exports = {
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['**/adapters/**'],
message: '领域层不允许直接导入适配器模块,请通过端口调用。'
}
]
}
]
}
};
Webpack 的 resolve.alias 可以用来重定向路径,让领域层代码即使写错了导入路径,也不会意外命中适配器文件。例如把 @domain 指向领域目录,把 @adapters 指向适配器目录,再配合 lint 规则区分。更严格的方案是把领域层和适配器层拆成独立的 npm 包,利用包的 exports 字段控制哪些文件对外开放。
另外,externals 可以让领域包完全不打包 React、Axios 等外部库,进一步降低领域层对这些库的隐式依赖。领域包最终可能只依赖少数纯函数工具库,构建产物更干净。
四、共享依赖、版本策略与落地注意事项
模块联邦虽然强大,但共享依赖配置不当会带来隐蔽的运行时问题。比如某个远程模块声明了不同版本的依赖,而宿主没有设置为 singleton,就会出现同一个库的多个副本。建议领域层和端口层尽量使用纯 JavaScript 或 TypeScript 类型,避免绑定 UI 框架;对于 React 或 Vue 这类必须共享的框架,配置 singleton: true 并指定 requiredVersion。
// 宿主应用的共享依赖配置
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
userAdapter: 'userAdapter@https://cdn.ipipp.com/remoteEntry.js'
},
shared: {
react: { singleton: true, eager: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, eager: true, requiredVersion: '^18.0.0' }
}
})
]
};
将 eager: true 配置在宿主侧,可以让 React 在应用的初始化阶段就加载,避免远程适配器异步加载时出现上下文还未建立的空窗期。对于领域层自身的依赖,建议不要设置 eager,保持按需加载以减小首屏体积。
实践中还要注意避免过度设计。如果项目只有两三个外部交互,直接使用明确的端口接口和目录约定即可,不必把所有内容都拆成远程模块。六边形架构的价值在于边界清晰,而不是为了架构而架构。Webpack 5 的模块联邦适合多团队、多应用协作,或者适配器需要频繁独立发布的场景。
最后,边界测试也很重要。领域层的单元测试不应依赖任何适配器,可以注入内存实现来验证业务规则。适配器的集成测试可以单独运行在真实 API 或模拟服务上。Webpack 5 的构建配置不能替代测试,但它可以让错误的依赖在构建和 lint 阶段提前暴露。