用 Nuxt Layers 构建模块化应用:从混乱到有序的实战指南
我曾经参与过一个面向多个国家的电商项目,当时选择了 Nuxt 作为技术框架。那时的架构可以说是一场噩梦:我们需要先维护一个"基础代码库",然后把代码合并到各个国家的分支。那还是 Nuxt 2 的时代,Layers 功能还没有出现,每次合并代码都会遇到各种冲突,保持跨国代码的一致性几乎是不可能的任务。
现在,Nuxt Layers 为这类需求提供了很好的解决方案。但今天我想分享的是如何用 Layers 实现模块化单体应用架构。
最近我构建了一个小型电商演示项目,完整实践了这套模式。通过这篇文章,你将学会如何在不需要微服务复杂架构、不需要多仓库合并的情况下,利用清晰的边界和强制约束,让 Nuxt 应用变得井井有条。
当简单结构无法满足需求
所有项目刚开始时都很简单:新建一个 Nuxt 应用,按照文件夹组织 components/、composables/、stores/,结构清晰,一目了然。
但随着业务增长,商品目录、购物车、用户中心、后台管理等功能不断加入……components/ 目录下很快堆积了 50 多个文件,各个 store 之间产生隐式依赖。修改购物车的一行代码,可能导致商品列表出现异常——这种痛苦,很多开发者都经历过。
问题的根源在于:"扁平架构缺乏明确的边界"。任何文件都可以随意引入任何模块,过度的自由反而成了负担。
典型的混乱目录结构:
app/
├─ components/
│ ├─ ProductCard.vue
│ ├─ CartButton.vue
│ ├─ CartItem.vue
│ ├─ FilterBar.vue
│ └─ …(50多个文件)
├─ composables/(逐渐变得杂乱)
└─ stores/
├─ products.ts
├─ cart.ts
└─ …(高度耦合)我曾经考虑使用微前端方案,但运维成本太高。直到发现了 Nuxt Layers,才找到了合适的解决方案。
什么是 Nuxt Layers?
Layer 允许你将应用拆分为独立、可复用的"迷你 Nuxt 应用"。每个 Layer 有自己的目录结构、nuxt.config.ts 配置,甚至可以发布为独立的 npm 包。
主项目通过 extends 选项来组合这些 Layer:
// nuxt.config.ts
export default defineNuxtConfig({
extends: [
'./layers/shared', // 本地目录
'./layers/products',
'./layers/cart'
]
})Layer 不仅限于本地目录,也可以来自 npm 包(如 @org/ui-layer)或 git 仓库。远程仓库的根目录需要包含 nuxt.config.ts 文件。
Nuxt 会自动合并各层的配置,并通过 #layers/xxx 这样的路径别名暴露代码,实现组件和 composables 的自动导入。
重要提示:默认情况下,编译期不会阻止跨层引用。如果主配置同时扩展了 products 和 cart 层,cart 层在运行时仍然可以引入 products 的模块。因此,我们需要依赖 ESLint 来强制实施边界约束。
电商项目分层实战
我建立了 3 个独立的 Layer,每个都有明确的职责:
项目根目录作为"协调者",负责组合各层的能力,实现完整的业务流程。
项目目录结构:
layers/
├─ shared/ # 基础层,无外部依赖
│ ├─ components/base/
│ │ └─ BaseBadge.vue
│ └─ utils/
│ ├─ currency.ts
│ └─ storage.ts
├─ products/ # 商品特性层
│ ├─ components/
│ │ ├─ ProductCard.vue
│ │ └─ ProductFilters.vue
│ ├─ stores/products/useProductsStore.ts
│ └─ schemas/product.schema.ts
└─ cart/ # 购物车特性层
├─ components/
│ ├─ CartSummary.vue
│ └─ CartItemCard.vue
└─ stores/cart/useCartStore.tsproducts 与 cart 层严格禁止直接通信,所有交互都通过项目根目录这个协调者来完成。
对比:耦合与解耦
传统的扁平化结构(高度耦合)
<script setup lang="ts">
// 组件内部直接依赖购物车模块
const cart = useCartStore()
const products = useProductsStore()
function addToCart(productId: string) {
const product = products.getById(productId)
cart.addItem(product) // 隐藏的依赖,难以追踪和维护
}
</script>这种方式导致依赖关系隐蔽、测试困难、商品模块无法独立复用。
Layer 模式(清晰解耦)
<!-- layers/products/components/ProductCard.vue -->
<script setup lang="ts">
defineProps<{ product: Product }>()
defineEmits<{ select: [productId: string] }>() // 通过事件通信
</script>
<template>
<UCard>
<h3>{{ product.name }}</h3>
<p>{{ product.price }}</p>
<UButton @click="$emit('select', product.id)">
查看详情
</UButton>
</UCard>
</template>商品层只负责抛出事件,完全不知道购物车模块的存在。具体的业务逻辑由根页面来组装:
<!-- pages/index.vue (协调者) -->
<script setup lang="ts">
const products = useProductsStore()
const cart = useCartStore()
function handleProductSelect(productId: string) {
const product = products.getById(productId)
if (product) cart.addItem(product) // 协调者负责连接不同层
}
</script>
<template>
<ProductCard
v-for="p in products.items"
:key="p.id"
:product="p"
@select="handleProductSelect"
/>
</template>这遵循了依赖倒置原则:高层模块(根)可以依赖低层模块(特性层),但特性层之间保持独立。
特性层之间如何通信?
可以把每个 Layer 想象成独立的专业工人,而项目根则是项目经理。工人只专注于自己的任务,项目经理负责协调各方工作。
以添加商品到购物车为例的交互流程:
用户操作 → 根页面(接收事件) → Products层(获取商品数据) → 根页面(协调) → Cart层(添加商品) → 用户界面(反馈结果)
实际案例:购物车页面需要合并"购物车中的商品项"和"最新的商品详情信息"。
<!-- pages/cart.vue (协调者) -->
<script setup lang="ts">
const cart = useCartStore()
const products = useProductsStore()
// 在协调者层面进行数据聚合
const enrichedItems = computed(() =>
cart.items.map(item => ({
...item,
productDetails: products.getById(item.productId) // 查询商品层获取详情
}))
)
</script>根页面同时查询购物车和商品两个 store,完成数据聚合,而 products 和 cart 这两个 Layer 保持独立。
用 ESLint 锁定架构边界
Nuxt 默认只会在依赖完全缺失时在编译期报错。如果主配置同时扩展了 products 和 cart 层,它们之间仍然可以互相 import。为了解决这个问题,可以使用 eslint-plugin-nuxt-layers 插件,它强制执行两条核心规则:
禁止跨特性层直接 import(例如 cart 层 ⇄ products 层)
禁止下层引用上层模块(Layer 不能 import 项目根目录的代码)
安装方式:
pnpm add -D eslint-plugin-nuxt-layers配置示例:
// eslint.config.mjs
export default [
{
plugins: { 'nuxt-layers': nuxtLayers },
rules: {
'nuxt-layers/layer-boundaries': ['error', {
root: 'layers', // 指定 layers 目录为根
aliases: ['#layers', '@layers'], // 配置的路径别名
layers: {
shared: [], // shared 层只能被其他层引用
products: ['shared'], // products 层只允许依赖 shared 层
cart: ['shared'] // cart 层只允许依赖 shared 层
}
}]
}
}
]重要提示:使用此插件需要关闭 Nuxt 的自动导入功能,必须显式使用 #layers/... 这类路径别名导入模块,这样 ESLint 才能进行静态分析。
实践中的注意事项
| 注意事项 | 说明与解决方案 |
|---|---|
| Layer 的加载顺序 | extends 数组中靠前的 Layer 优先级更高。同名的文件或配置,后面的层会被忽略 |
| 同名路由问题 | 如果两个 Layer 都定义了 pages/index.vue,只有第一个被加载的 Layer 中的页面会生效 |
| 热更新可能失效 | 修改 layers/xxx/nuxt.config.ts 文件后,建议重启开发服务器 |
| 路由前缀 | layers/blog/pages/index.vue 对应的路由是 /,而不是 /blog |
| 组件名前缀 | components/form/Input.vue 会自动注册为 <FormInput> 组件 |
适用场景
推荐使用场景
业务域有清晰的功能边界(例如商品、购物车、博客、管理后台)
多个团队需要并行开发,希望减少代码冲突
多个应用需要复用同一套功能
预计项目在 1-2 年内会迅速膨胀到 50 个以上的功能模块
不建议使用场景
小型 Demo、MVP、组件数量不足 10 个的项目。建议先从扁平结构开始,当模块间边界开始模糊、维护成本上升时,再考虑按此方案重构。
核心优势
| 优势 | 具体表现 |
|---|---|
| 明确的边界 | 编译期和代码检查双重保障,违规引用立即失败 |
| 支持独立开发 | 不同团队可以专注各自负责的 Layer |
| 测试更简单 | 单个 Layer 通常只依赖 shared 基础层,Mock 简单 |
| 渐进式拆分 | 任何 Layer 未来都可以轻松发布为 npm 包 |
| 代码审查更高效 | PR 中一旦出现跨层引用,评审者可以一眼识别 |
快速开始
体验完整示例项目
git clone https://github.com/alexanderop/nuxt-layer-example
cd nuxt-layer-example
pnpm install
pnpm dev从零开始新建项目
# 创建项目及 layers 目录结构
mkdir -p layers/shared layers/products layers/cart
# 为每个 Layer 创建配置
echo "export default defineNuxtConfig({ \$meta: { name: 'shared' } })" > layers/shared/nuxt.config.ts只要将 Layer 放置在项目根目录下的 layers/ 文件夹内,Nuxt 会自动检测并加载它们。
安装 ESLint 插件
pnpm add -D eslint-plugin-nuxt-layers按照前面的说明配置 ESLint 规则,为你的架构提供实时保护。
总结
模块化单体架构结合了微前端的清晰边界和单体应用的运维简便性。Nuxt Layers 让这一模式变得开箱即用:编译期有 TypeScript 类型保护,开发期有 ESLint 插件锁定依赖关系,可视化的分层结构让架构一目了然。
你可以选择在项目开始时就采用分层设计,也可以在业务复杂后进行渐进式重构。无论选择哪条路径,当你的应用成长为包含多个功能模块的复杂系统时,你依然能够保持代码的清晰和可维护性。
本文内容仅供个人学习/研究/参考使用,不构成任何决策建议或专业指导。分享/转载时请标明原文来源,同时请勿将内容用于商业售卖、虚假宣传等非学习用途哦~感谢您的理解与支持!