[{"data":1,"prerenderedAt":1052},["ShallowReactive",2],{"page-/post/nuxt/nuxt4-use-layers":3,"surrounding-page":1043},{"id":4,"title":5,"author":6,"body":7,"date":1031,"description":79,"extension":1032,"group":6,"lastmod":1033,"meta":1034,"navigation":89,"path":1035,"rawbody":1036,"seo":1037,"showTitle":5,"stem":1038,"tags":1039,"versions":1040,"__hash__":1042},"content/post/nuxt/nuxt4-use-layers.md","使用 Layers 扩展你的 Nuxt4 应用",null,{"type":8,"value":9,"toc":1025},"minimark",[10,15,28,35,38,41,47,51,60,63,70,73,321,324,512,519,541,551,554,557,560,563,566,569,572,575,578,581,584,593,663,688,697,703,725,735,738,760,766,769,787,799,809,815,821,824,827,852,855,894,904,909,915,926,929,936,947,950,955,961,967,970,973,976,979,982,992,995,1004,1018,1021],[11,12,14],"h2",{"id":13},"面对一个臃肿的页面或项目你会如何简化重构扩展它","面对一个臃肿的页面或项目，你会如何简化重构、扩展它？",[16,17,18,19,23,24,27],"p",{},"当单个 Vue 文件中界面/业务足够多时，通常我们会把它拆分成多个 ",[20,21,22],"code",{},"components"," 或 ",[20,25,26],{},"composables"," 来引入，以此来减少此文件复杂度和增加可维护性。",[16,29,30,31,34],{},"当一个项目的界面/业务逻辑足够多时，我们会在全局抽离一部分组件和逻辑，在不同的业务逻辑中复用。或者把和业务解耦的组件或工具函数甚至固定业务逻辑发布为 ",[20,32,33],{},"npm"," 包来使用，进一步减少项目中的代码复杂度。",[16,36,37],{},"当存在多个项目，而项目之间又存在相同业务时，我们可能会把具有完整功能的一部分逻辑抽离成独立的项目。同时多个项目的 UI组件库和通用逻辑层也发布为 npm 包，供所有项目下载使用以保持统一的视觉效果和减少重复代码的编写。这样当一个新的项目出现，需要利用其他已有的业务逻辑或者是完全复用其他项目的页面和功能时，我们可以使用 iframe 或沙箱来加载这个独立的项目。这样多团队可以维护自己的业务项目，同时又能避免开发重复的功能。",[16,39,40],{},"那么在 Nuxt 里的如何优雅的解决这些问题呢？",[16,42,43,44],{},"除了上述的任何框架的项目都能操作的方式，Nuxt 还有自己独特的扩展方式：",[20,45,46],{},"Layer",[11,48,50],{"id":49},"layer-可以解决什么问题","Layer 可以解决什么问题",[16,52,53,59],{},[54,55,46],"a",{"href":56,"rel":57},"https://nuxt.com/docs/getting-started/layers",[58],"nofollow"," 几乎就是一个完整的 Nuxt 项目。",[16,61,62],{},"开发了一个 Layer，意味着 Layer 中的组件、函数、接口、依赖包等都会继承了此 Layer 的项目应用。",[16,64,65,66],{},"它不是一个最新版本才有的功能，但官方的文档的介绍十分简单，",[67,68,69],"strong",{},"可能是因为它还在不断的完善中，官方也不希望你用于生产环境中。",[16,71,72],{},"一个 Nuxt4 应用的最简目录结构是这样的：",[74,75,80],"pre",{"className":76,"code":77,"language":78,"meta":79,"style":79},"language-shell shiki shiki-themes github-light","\n├── README.md\n\n├── **app**\n\n│   ├── app.config.ts\n\n│   ├── app.vue\n\n│   └── **pages**\n\n│       └── index.vue\n\n├── nuxt.config.ts\n\n├── package.json\n\n├── pnpm-lock.yaml\n\n├── **public**\n\n│   ├── favicon.ico\n\n│   └── robots.txt\n\n├── **server**\n\n│   └── tsconfig.json\n\n└── tsconfig.json\n","shell","",[20,81,82,91,102,107,122,127,139,144,154,159,174,179,190,195,203,208,216,221,229,234,246,251,261,266,276,281,293,298,308,313],{"__ignoreMap":79},[83,84,87],"span",{"class":85,"line":86},"line",1,[83,88,90],{"emptyLinePlaceholder":89},true,"\n",[83,92,94,98],{"class":85,"line":93},2,[83,95,97],{"class":96},"s7eDp","├──",[83,99,101],{"class":100},"sYBdl"," README.md\n",[83,103,105],{"class":85,"line":104},3,[83,106,90],{"emptyLinePlaceholder":89},[83,108,110,112,116,119],{"class":85,"line":109},4,[83,111,97],{"class":96},[83,113,115],{"class":114},"sYu0t"," **",[83,117,118],{"class":100},"app",[83,120,121],{"class":114},"**\n",[83,123,125],{"class":85,"line":124},5,[83,126,90],{"emptyLinePlaceholder":89},[83,128,130,133,136],{"class":85,"line":129},6,[83,131,132],{"class":96},"│  ",[83,134,135],{"class":100}," ├──",[83,137,138],{"class":100}," app.config.ts\n",[83,140,142],{"class":85,"line":141},7,[83,143,90],{"emptyLinePlaceholder":89},[83,145,147,149,151],{"class":85,"line":146},8,[83,148,132],{"class":96},[83,150,135],{"class":100},[83,152,153],{"class":100}," app.vue\n",[83,155,157],{"class":85,"line":156},9,[83,158,90],{"emptyLinePlaceholder":89},[83,160,162,164,167,169,172],{"class":85,"line":161},10,[83,163,132],{"class":96},[83,165,166],{"class":100}," └──",[83,168,115],{"class":114},[83,170,171],{"class":100},"pages",[83,173,121],{"class":114},[83,175,177],{"class":85,"line":176},11,[83,178,90],{"emptyLinePlaceholder":89},[83,180,182,184,187],{"class":85,"line":181},12,[83,183,132],{"class":96},[83,185,186],{"class":100},"     └──",[83,188,189],{"class":100}," index.vue\n",[83,191,193],{"class":85,"line":192},13,[83,194,90],{"emptyLinePlaceholder":89},[83,196,198,200],{"class":85,"line":197},14,[83,199,97],{"class":96},[83,201,202],{"class":100}," nuxt.config.ts\n",[83,204,206],{"class":85,"line":205},15,[83,207,90],{"emptyLinePlaceholder":89},[83,209,211,213],{"class":85,"line":210},16,[83,212,97],{"class":96},[83,214,215],{"class":100}," package.json\n",[83,217,219],{"class":85,"line":218},17,[83,220,90],{"emptyLinePlaceholder":89},[83,222,224,226],{"class":85,"line":223},18,[83,225,97],{"class":96},[83,227,228],{"class":100}," pnpm-lock.yaml\n",[83,230,232],{"class":85,"line":231},19,[83,233,90],{"emptyLinePlaceholder":89},[83,235,237,239,241,244],{"class":85,"line":236},20,[83,238,97],{"class":96},[83,240,115],{"class":114},[83,242,243],{"class":100},"public",[83,245,121],{"class":114},[83,247,249],{"class":85,"line":248},21,[83,250,90],{"emptyLinePlaceholder":89},[83,252,254,256,258],{"class":85,"line":253},22,[83,255,132],{"class":96},[83,257,135],{"class":100},[83,259,260],{"class":100}," favicon.ico\n",[83,262,264],{"class":85,"line":263},23,[83,265,90],{"emptyLinePlaceholder":89},[83,267,269,271,273],{"class":85,"line":268},24,[83,270,132],{"class":96},[83,272,166],{"class":100},[83,274,275],{"class":100}," robots.txt\n",[83,277,279],{"class":85,"line":278},25,[83,280,90],{"emptyLinePlaceholder":89},[83,282,284,286,288,291],{"class":85,"line":283},26,[83,285,97],{"class":96},[83,287,115],{"class":114},[83,289,290],{"class":100},"server",[83,292,121],{"class":114},[83,294,296],{"class":85,"line":295},27,[83,297,90],{"emptyLinePlaceholder":89},[83,299,301,303,305],{"class":85,"line":300},28,[83,302,132],{"class":96},[83,304,166],{"class":100},[83,306,307],{"class":100}," tsconfig.json\n",[83,309,311],{"class":85,"line":310},29,[83,312,90],{"emptyLinePlaceholder":89},[83,314,316,319],{"class":85,"line":315},30,[83,317,318],{"class":96},"└──",[83,320,307],{"class":100},[16,322,323],{},"而一个 Layer 模板的目录结构是这样的：",[74,325,327],{"className":76,"code":326,"language":78,"meta":79,"style":79},"├── .editorconfig\n\n├── .gitignore\n\n├── .npmrc\n\n├── .nuxtrc\n\n├── .playground\n\n│   ├── app.config.ts\n\n│   └── nuxt.config.ts\n\n├── README.md\n\n├── app.config.ts\n\n├── app.vue\n\n├── components\n\n│   └── HelloWorld.vue\n\n├── eslint.config.js\n\n├── nuxt.config.ts\n\n├── package.json\n\n├── pnpm-lock.yaml\n\n└── tsconfig.json\n",[20,328,329,336,340,347,351,358,362,369,373,380,384,392,396,404,408,414,418,424,428,434,438,445,449,458,462,469,473,479,483,489,493,500,505],{"__ignoreMap":79},[83,330,331,333],{"class":85,"line":86},[83,332,97],{"class":96},[83,334,335],{"class":100}," .editorconfig\n",[83,337,338],{"class":85,"line":93},[83,339,90],{"emptyLinePlaceholder":89},[83,341,342,344],{"class":85,"line":104},[83,343,97],{"class":96},[83,345,346],{"class":100}," .gitignore\n",[83,348,349],{"class":85,"line":109},[83,350,90],{"emptyLinePlaceholder":89},[83,352,353,355],{"class":85,"line":124},[83,354,97],{"class":96},[83,356,357],{"class":100}," .npmrc\n",[83,359,360],{"class":85,"line":129},[83,361,90],{"emptyLinePlaceholder":89},[83,363,364,366],{"class":85,"line":141},[83,365,97],{"class":96},[83,367,368],{"class":100}," .nuxtrc\n",[83,370,371],{"class":85,"line":146},[83,372,90],{"emptyLinePlaceholder":89},[83,374,375,377],{"class":85,"line":156},[83,376,97],{"class":96},[83,378,379],{"class":100}," .playground\n",[83,381,382],{"class":85,"line":161},[83,383,90],{"emptyLinePlaceholder":89},[83,385,386,388,390],{"class":85,"line":176},[83,387,132],{"class":96},[83,389,135],{"class":100},[83,391,138],{"class":100},[83,393,394],{"class":85,"line":181},[83,395,90],{"emptyLinePlaceholder":89},[83,397,398,400,402],{"class":85,"line":192},[83,399,132],{"class":96},[83,401,166],{"class":100},[83,403,202],{"class":100},[83,405,406],{"class":85,"line":197},[83,407,90],{"emptyLinePlaceholder":89},[83,409,410,412],{"class":85,"line":205},[83,411,97],{"class":96},[83,413,101],{"class":100},[83,415,416],{"class":85,"line":210},[83,417,90],{"emptyLinePlaceholder":89},[83,419,420,422],{"class":85,"line":218},[83,421,97],{"class":96},[83,423,138],{"class":100},[83,425,426],{"class":85,"line":223},[83,427,90],{"emptyLinePlaceholder":89},[83,429,430,432],{"class":85,"line":231},[83,431,97],{"class":96},[83,433,153],{"class":100},[83,435,436],{"class":85,"line":236},[83,437,90],{"emptyLinePlaceholder":89},[83,439,440,442],{"class":85,"line":248},[83,441,97],{"class":96},[83,443,444],{"class":100}," components\n",[83,446,447],{"class":85,"line":253},[83,448,90],{"emptyLinePlaceholder":89},[83,450,451,453,455],{"class":85,"line":263},[83,452,132],{"class":96},[83,454,166],{"class":100},[83,456,457],{"class":100}," HelloWorld.vue\n",[83,459,460],{"class":85,"line":268},[83,461,90],{"emptyLinePlaceholder":89},[83,463,464,466],{"class":85,"line":278},[83,465,97],{"class":96},[83,467,468],{"class":100}," eslint.config.js\n",[83,470,471],{"class":85,"line":283},[83,472,90],{"emptyLinePlaceholder":89},[83,474,475,477],{"class":85,"line":295},[83,476,97],{"class":96},[83,478,202],{"class":100},[83,480,481],{"class":85,"line":300},[83,482,90],{"emptyLinePlaceholder":89},[83,484,485,487],{"class":85,"line":310},[83,486,97],{"class":96},[83,488,215],{"class":100},[83,490,491],{"class":85,"line":315},[83,492,90],{"emptyLinePlaceholder":89},[83,494,496,498],{"class":85,"line":495},31,[83,497,97],{"class":96},[83,499,228],{"class":100},[83,501,503],{"class":85,"line":502},32,[83,504,90],{"emptyLinePlaceholder":89},[83,506,508,510],{"class":85,"line":507},33,[83,509,318],{"class":96},[83,511,307],{"class":100},[16,513,514,515,518],{},"可以看到，",[67,516,517],{},"会用 Nuxt 就会写 Layer，无需引入其他负担即可扩展","。",[16,520,521,522,525,526,528,529,528,531,528,533,536,537,540],{},"开发一个 ",[20,523,524],{},"Nuxt4"," 应用时，我们会在 app 目录下开发各种 ",[20,527,171],{},"、",[20,530,22],{},[20,532,26],{},[20,534,535],{},"utils","，会在 ",[20,538,539],{},"app.config.ts"," 里设置一些全局配置项。",[16,542,543,544,546,547,550],{},"需要接口支持时，可以在 ",[20,545,290],{}," 目录下借助 ",[20,548,549],{},"Nitro"," 的生态来完成后端服务。",[16,552,553],{},"但是当项目开始变得臃肿时，Layer 可以发挥什么作用呢。",[16,555,556],{},"场景1：我的网站有一个登录注册权限校验相关的逻辑，这肯定会涉及到一些页面、组件、结构，比如登录页、注册页、登录按钮、登录注册接口等等。",[16,558,559],{},"一开始我没打算把它们抽离出去，因为网站还很小，并且只有一个。但是我很清楚这一部分功能和组件是可以被复用的。只要我再创建一个 nuxt 应用，我可以肯定我会来这个项目里复制一遍代码而不是重新敲一遍。",[16,561,562],{},"由于既涉及到前端组件又涉及到后端接口，以及一部分公共函数。我难道要把 Vue 的组件发布成一个 npm 包，把公共函数发布一个 npm 包，再把这部分后端接口重新起一个服务吗？",[16,564,565],{},"在一个全栈框架里这样做显然是有点繁琐。",[16,567,568],{},"假设 2：有一个付费网站，但它有一部分是免费的，付费内容也是分 vip、svip，如果全都写在一个项目里的话当然可以实现，毕竟用户也不关心你代码是怎么写的，只要能提供稳定的服务就可以了。",[16,570,571],{},"如果两个、三个网站呢？",[16,573,574],{},"如果是卖给客户源码呢？",[16,576,577],{},"如果是把免费内容开源，付费内容闭源，或是后续再把 vip 的逻辑开源呢？",[16,579,580],{},"假设 3：有一个基于 Nuxt 的开源博客站，如何设计一套机制，可以让其他用户用上自己喜欢的主题呢，毕竟博客最重要的就是换皮。",[11,582,583],{"id":583},"针对不同场景的解决方案",[16,585,586,587,589,590,592],{},"如果场景 1，前期 ",[20,588,46],{}," 可以直接在 ",[20,591,524],{}," 应用里使用，像是这样：",[74,594,596],{"className":76,"code":595,"language":78,"meta":79,"style":79},"app\n    auth\n        pages\n        conmponents\n        composables\n        utils\n        server\n        nuxt.config.ts\n    components\n    composables\n    utils\n    app.config.ts\nnuxt.config.ts\n",[20,597,598,603,608,613,618,623,628,633,638,643,648,653,658],{"__ignoreMap":79},[83,599,600],{"class":85,"line":86},[83,601,602],{"class":96},"app\n",[83,604,605],{"class":85,"line":93},[83,606,607],{"class":96},"    auth\n",[83,609,610],{"class":85,"line":104},[83,611,612],{"class":96},"        pages\n",[83,614,615],{"class":85,"line":109},[83,616,617],{"class":96},"        conmponents\n",[83,619,620],{"class":85,"line":124},[83,621,622],{"class":96},"        composables\n",[83,624,625],{"class":85,"line":129},[83,626,627],{"class":96},"        utils\n",[83,629,630],{"class":85,"line":141},[83,631,632],{"class":96},"        server\n",[83,634,635],{"class":85,"line":146},[83,636,637],{"class":96},"        nuxt.config.ts\n",[83,639,640],{"class":85,"line":156},[83,641,642],{"class":96},"    components\n",[83,644,645],{"class":85,"line":161},[83,646,647],{"class":96},"    composables\n",[83,649,650],{"class":85,"line":176},[83,651,652],{"class":96},"    utils\n",[83,654,655],{"class":85,"line":181},[83,656,657],{"class":96},"    app.config.ts\n",[83,659,660],{"class":85,"line":192},[83,661,662],{"class":96},"nuxt.config.ts\n",[664,665,666],"blockquote",{},[16,667,668,669,672,673,676,677,680,681,684,685],{},"在 ",[20,670,671],{},"Nuxt3"," 中，这个 ",[20,674,675],{},"auth"," 可以放在 ",[20,678,679],{},"～/layers"," 中\n",[20,682,683],{},"Nuxt3.12.0"," 以后的版本都会自动注册",[20,686,687],{},"Layers",[16,689,668,690,692,693,696],{},[20,691,118],{}," 目录下，可以新建一个 ",[20,694,695],{},"{layer_name}"," 目录，目录内的结构和 Nuxt3 是一致的。",[16,698,699,702],{},[20,700,701],{},"nuxt.config.ts"," 中只需要 extend 即可。",[74,704,708],{"className":705,"code":706,"language":707,"meta":79,"style":79},"language-typescript shiki shiki-themes github-light","extend: [ 'app/auth' ]\n","typescript",[20,709,710],{"__ignoreMap":79},[83,711,712,715,719,722],{"class":85,"line":86},[83,713,714],{"class":96},"extend",[83,716,718],{"class":717},"sgsFI",": [ ",[83,720,721],{"class":100},"'app/auth'",[83,723,724],{"class":717}," ]\n",[16,726,727,728,731,732],{},"这样，我们把权限校验相关的组件、接口、utils 都挪到了一个 ",[20,729,730],{},"auth layer"," 下管理。",[67,733,734],{},"如果业务没能继续发展，一个仓库非常好管理，同时按文件夹划分后，结构十分清晰。",[16,736,737],{},"这样可能感受还不是很强烈。",[16,739,740,741,744,745,748,749,752,753,756,757,759],{},"因为只是目录结构上的分层。这一层相关的依赖 ",[20,742,743],{},"nuxt/auth"," 还是安装在了主项目内，但 ",[20,746,747],{},"nuxt-auth"," 已经是在 ",[20,750,751],{},"app/auth/nuxt.config.ts"," 里了，要知道，如果你用了一些 ",[20,754,755],{},"nuxt modules"," 之后，",[20,758,701],{}," 很容易变成一大坨！因为他们都是在这一个文件内配置。",[16,761,762,763,518],{},"但 Layer 可以随时分离出去，因为它就是一个完整的 Nuxt 应用，所以",[67,764,765],{},"迁移几乎没有额外成本",[16,767,768],{},"当决定把这一部分代码迁移出去时",[16,770,771,772,775,776,779,780,783,784,518],{},"可以直接使用官方的 ",[20,773,774],{},"layer"," 模板再新建一个项目 ",[20,777,778],{},"zc-auth-layer","，这个模板有一个好处，内置了 ",[20,781,782],{},".playground"," ，顾名思义，",[67,785,786],{},"是一个用来模拟你的主项目的 Nuxt 应用",[16,788,789,791,792,795,796,518],{},[20,790,46],{}," 的实际内容（红框）都写在根目录 的123下，",[20,793,794],{},"playground"," （绿框）里用",[67,797,798],{},"来做调试和开发",[16,800,801,802,805,806,808],{},"起初我没有用这个模板，而是单纯的新建一个 Nuxt 应用，但很快就发现了问题。",[20,803,804],{},"Pages"," 会直接把主项目的 ",[20,807,804],{}," 直接给覆盖掉，而且（暂时）还没发现如何忽略某个目录的覆盖，但这是一个显而易见的问题，应该会很快完善或者被我发现。",[16,810,811,812,814],{},"而有了 ",[20,813,794],{}," 之后，可以放心的里面写页面或是发展成一个独立的项目，又不影响外层供给其他项目继承。",[16,816,817],{},[818,819],"img",{"alt":79,"src":820},"https://img.zzao.club/article/202502051431526.png",[16,822,823],{},"这就是一个普普通通的 Nuxt4  应用！",[16,825,826],{},"而其他项目如果想使用它的仓库，只需要配置：",[74,828,830],{"className":705,"code":829,"language":707,"meta":79,"style":79},"extends: [ ['github:aatrooox/zc-auth-layer', { install: true }] ]\n",[20,831,832],{"__ignoreMap":79},[83,833,834,837,840,843,846,849],{"class":85,"line":86},[83,835,836],{"class":96},"extends",[83,838,839],{"class":717},": [ [",[83,841,842],{"class":100},"'github:aatrooox/zc-auth-layer'",[83,844,845],{"class":717},", { install: ",[83,847,848],{"class":114},"true",[83,850,851],{"class":717}," }] ]\n",[16,853,854],{},"也可以它发布后的 npm 包",[74,856,858],{"className":705,"code":857,"language":707,"meta":79,"style":79},"// 直接使用npm 包\nextends: [ 'zc-auth-layer' ]\n// 某个组织下的包\nextends: [ '@zzaoclub/zc-auth-layer']\n",[20,859,860,866,877,882],{"__ignoreMap":79},[83,861,862],{"class":85,"line":86},[83,863,865],{"class":864},"sAwPA","// 直接使用npm 包\n",[83,867,868,870,872,875],{"class":85,"line":93},[83,869,836],{"class":96},[83,871,718],{"class":717},[83,873,874],{"class":100},"'zc-auth-layer'",[83,876,724],{"class":717},[83,878,879],{"class":85,"line":104},[83,880,881],{"class":864},"// 某个组织下的包\n",[83,883,884,886,888,891],{"class":85,"line":109},[83,885,836],{"class":96},[83,887,718],{"class":717},[83,889,890],{"class":100},"'@zzaoclub/zc-auth-layer'",[83,892,893],{"class":717},"]\n",[16,895,896,897,899,900,903],{},"就像是在项目中使用其他 ",[20,898,22],{}," 、",[20,901,902],{},"server api"," 一样！",[16,905,906],{},[67,907,908],{},"甚至还拥有完整的自动导入和 TS 类型提示，这你能受得了吗？",[16,910,911,912],{},"依赖关系是这样的，",[67,913,914],{},"A extend B ， B extend C ，C 拥有最高的优先级，向下覆盖。",[16,916,917,918,921,922,925],{},"所以哪怕是 ",[20,919,920],{},"B Layer"," 中只包含了一部分你需要的接口、组件，你也可以很轻松的再 extend 一个 ",[20,923,924],{},"D Layer","，用来重写你不需要的逻辑。",[16,927,928],{},"比如 B C 都是支付层，B 是支付宝支付，C 是微信支付。",[16,930,931,932,935],{},"在主项目中使用 ",[20,933,934],{},"/api/order/pay"," 接口。继承了 B 层时，调用此接口就是支付宝支付，继承 C 后就是微信支付。",[16,937,938,939,942,943,946],{},"亦或是 B 的接口是 ",[20,940,941],{},"/api/order/pay/zfb"," ，C 的接口是 ",[20,944,945],{},"/api/order/pay/wx","，主项目中的接口就可以用来自由控制使用哪种支付方式时调起对应的接口。",[16,948,949],{},"对于自己的项目来说，这种分层方式，可以让你更加自由的控制代码开源程度以及自己多项目复用逻辑。",[16,951,952],{},[67,953,954],{},"对于出售源码时，也只需要出售对应层的代码，组合起来即可。",[16,956,957,958,518],{},"对解决博客换主题的功能，更是不能再适合了，只要按开发的博客时使用的 Components，",[67,959,960],{},"组件名一一对应就可以了",[16,962,963,964,518],{},"当然，如果实际应用到项目上，还有类似数据库、多环境等问题",[67,965,966],{},"需要仔细斟酌",[16,968,969],{},"但 Layer 的分层结构已经显而易见的更简单、更直接，开发体验也更好了。",[11,971,972],{"id":972},"潜在的问题",[16,974,975],{},"对于简单的前端项目，或是不涉及复杂的数据库架构，Layer 的方式无疑是更优雅的。",[16,977,978],{},"既在文件结构上解耦了，同时继承（覆盖）时也很直接。",[16,980,981],{},"但如果涉及到多个接口服务，或是多个数据库服务，甚至前端业务也比较复杂。还要是仔细测试其兼容性。",[16,983,984,985,987,988,991],{},"毕竟虽然使用 ",[20,986,46],{}," 在文件结构上分开的，但实际打包后是在同一个 ",[20,989,990],{},"Nuxt"," 服务里的，并不是类似微服务的那种形式。",[16,993,994],{},"话说回来，大部分使用者应该只是看中了 SSR 能力，可能不会使用 Nitro 构建大型的应用，尤其是对于公司来说，也不太信任 Node 的服务能力。",[16,996,997,998,1000,1001,1003],{},"所以对于个人开发者来说，我觉得 ",[20,999,46],{}," 是个锦上添花的利器。因为大部分业务不会太复杂，而且偏前端更多一些。自己封装好的各种功能的 ",[20,1002,46],{}," 信手拈来，开发效率大大提高，还不耽误二次开发。",[16,1005,1006,1007,1012,1013,1017],{},"于是我在给自己的",[54,1008,1011],{"href":1009,"rel":1010},"https://blog.zzao.club",[58],"博客","尝试增加了一个权限层 ",[54,1014,730],{"href":1015,"rel":1016},"https://github.com/aatrooox/zc-auth-layer",[58]," 后，才写下了这篇文章。",[16,1019,1020],{},"期待后续 Layer 的大放异彩。",[1022,1023,1024],"style",{},"html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":79,"searchDepth":93,"depth":93,"links":1026},[1027,1028,1029,1030],{"id":13,"depth":93,"text":14},{"id":49,"depth":93,"text":50},{"id":583,"depth":93,"text":583},{"id":972,"depth":93,"text":972},"2025-02-10T00:00:00.000Z","md","2025-08-19T00:00:00.000Z",{},"/post/nuxt/nuxt4-use-layers","---\ntitle: 使用 Layers 扩展你的 Nuxt4 应用\ndate: 2025-02-10\nlastmod: 2025-08-19\ntags: [\"Nuxt\"]\nversions: [\"nuxt@3.15.2\"]\nshowTitle: 使用 Layers 扩展你的 Nuxt4 应用\n---\n## 面对一个臃肿的页面或项目，你会如何简化重构、扩展它？\n\n当单个 Vue 文件中界面/业务足够多时，通常我们会把它拆分成多个 `components` 或 `composables` 来引入，以此来减少此文件复杂度和增加可维护性。\n\n当一个项目的界面/业务逻辑足够多时，我们会在全局抽离一部分组件和逻辑，在不同的业务逻辑中复用。或者把和业务解耦的组件或工具函数甚至固定业务逻辑发布为 `npm` 包来使用，进一步减少项目中的代码复杂度。\n\n当存在多个项目，而项目之间又存在相同业务时，我们可能会把具有完整功能的一部分逻辑抽离成独立的项目。同时多个项目的 UI组件库和通用逻辑层也发布为 npm 包，供所有项目下载使用以保持统一的视觉效果和减少重复代码的编写。这样当一个新的项目出现，需要利用其他已有的业务逻辑或者是完全复用其他项目的页面和功能时，我们可以使用 iframe 或沙箱来加载这个独立的项目。这样多团队可以维护自己的业务项目，同时又能避免开发重复的功能。\n\n那么在 Nuxt 里的如何优雅的解决这些问题呢？ \n\n除了上述的任何框架的项目都能操作的方式，Nuxt 还有自己独特的扩展方式：`Layer`\n\n## Layer 可以解决什么问题\n\n\n[Layer](https://nuxt.com/docs/getting-started/layers) 几乎就是一个完整的 Nuxt 项目。\n\n开发了一个 Layer，意味着 Layer 中的组件、函数、接口、依赖包等都会继承了此 Layer 的项目应用。\n\n它不是一个最新版本才有的功能，但官方的文档的介绍十分简单，**可能是因为它还在不断的完善中，官方也不希望你用于生产环境中。**\n\n一个 Nuxt4 应用的最简目录结构是这样的：\n\n```shell\n\n├── README.md\n\n├── **app**\n\n│   ├── app.config.ts\n\n│   ├── app.vue\n\n│   └── **pages**\n\n│       └── index.vue\n\n├── nuxt.config.ts\n\n├── package.json\n\n├── pnpm-lock.yaml\n\n├── **public**\n\n│   ├── favicon.ico\n\n│   └── robots.txt\n\n├── **server**\n\n│   └── tsconfig.json\n\n└── tsconfig.json\n```\n\n而一个 Layer 模板的目录结构是这样的：\n\n```shell\n├── .editorconfig\n\n├── .gitignore\n\n├── .npmrc\n\n├── .nuxtrc\n\n├── .playground\n\n│   ├── app.config.ts\n\n│   └── nuxt.config.ts\n\n├── README.md\n\n├── app.config.ts\n\n├── app.vue\n\n├── components\n\n│   └── HelloWorld.vue\n\n├── eslint.config.js\n\n├── nuxt.config.ts\n\n├── package.json\n\n├── pnpm-lock.yaml\n\n└── tsconfig.json\n```\n\n可以看到，**会用 Nuxt 就会写 Layer，无需引入其他负担即可扩展**。\n\n开发一个 `Nuxt4` 应用时，我们会在 app 目录下开发各种 `pages`、`components`、`composables`、`utils`，会在 `app.config.ts` 里设置一些全局配置项。\n\n需要接口支持时，可以在 `server` 目录下借助 `Nitro` 的生态来完成后端服务。\n\n但是当项目开始变得臃肿时，Layer 可以发挥什么作用呢。\n\n场景1：我的网站有一个登录注册权限校验相关的逻辑，这肯定会涉及到一些页面、组件、结构，比如登录页、注册页、登录按钮、登录注册接口等等。\n\n一开始我没打算把它们抽离出去，因为网站还很小，并且只有一个。但是我很清楚这一部分功能和组件是可以被复用的。只要我再创建一个 nuxt 应用，我可以肯定我会来这个项目里复制一遍代码而不是重新敲一遍。\n\n由于既涉及到前端组件又涉及到后端接口，以及一部分公共函数。我难道要把 Vue 的组件发布成一个 npm 包，把公共函数发布一个 npm 包，再把这部分后端接口重新起一个服务吗？\n\n在一个全栈框架里这样做显然是有点繁琐。\n\n假设 2：有一个付费网站，但它有一部分是免费的，付费内容也是分 vip、svip，如果全都写在一个项目里的话当然可以实现，毕竟用户也不关心你代码是怎么写的，只要能提供稳定的服务就可以了。\n\n如果两个、三个网站呢？ \n\n如果是卖给客户源码呢？\n\n如果是把免费内容开源，付费内容闭源，或是后续再把 vip 的逻辑开源呢？\n\n假设 3：有一个基于 Nuxt 的开源博客站，如何设计一套机制，可以让其他用户用上自己喜欢的主题呢，毕竟博客最重要的就是换皮。\n\n\n## 针对不同场景的解决方案\n\n如果场景 1，前期 `Layer` 可以直接在 `Nuxt4` 应用里使用，像是这样：\n\n```shell\napp\n\tauth\n\t\tpages\n\t\tconmponents\n\t\tcomposables\n\t\tutils\n\t\tserver\n\t\tnuxt.config.ts\n\tcomponents\n\tcomposables\n\tutils\n\tapp.config.ts\nnuxt.config.ts\n```\n\n> 在 `Nuxt3` 中，这个 `auth` 可以放在 `～/layers` 中\n> `Nuxt3.12.0` 以后的版本都会自动注册`Layers`\n\n在 `app` 目录下，可以新建一个 `{layer_name}` 目录，目录内的结构和 Nuxt3 是一致的。\n\n`nuxt.config.ts` 中只需要 extend 即可。\n\n```typescript\nextend: [ 'app/auth' ]\n```\n\n这样，我们把权限校验相关的组件、接口、utils 都挪到了一个 `auth layer` 下管理。**如果业务没能继续发展，一个仓库非常好管理，同时按文件夹划分后，结构十分清晰。**\n\n这样可能感受还不是很强烈。\n\n因为只是目录结构上的分层。这一层相关的依赖 `nuxt/auth` 还是安装在了主项目内，但 `nuxt-auth` 已经是在 `app/auth/nuxt.config.ts` 里了，要知道，如果你用了一些 `nuxt modules` 之后，`nuxt.config.ts` 很容易变成一大坨！因为他们都是在这一个文件内配置。\n\n但 Layer 可以随时分离出去，因为它就是一个完整的 Nuxt 应用，所以**迁移几乎没有额外成本**。\n\n当决定把这一部分代码迁移出去时\n\n可以直接使用官方的 `layer` 模板再新建一个项目 `zc-auth-layer`，这个模板有一个好处，内置了 `.playground` ，顾名思义，**是一个用来模拟你的主项目的 Nuxt 应用**。\n\n`Layer` 的实际内容（红框）都写在根目录 的123下，`playground` （绿框）里用**来做调试和开发**。\n\n起初我没有用这个模板，而是单纯的新建一个 Nuxt 应用，但很快就发现了问题。`Pages` 会直接把主项目的 `Pages` 直接给覆盖掉，而且（暂时）还没发现如何忽略某个目录的覆盖，但这是一个显而易见的问题，应该会很快完善或者被我发现。\n\n而有了 `playground` 之后，可以放心的里面写页面或是发展成一个独立的项目，又不影响外层供给其他项目继承。\n\n![](https://img.zzao.club/article/202502051431526.png)\n\n这就是一个普普通通的 Nuxt4  应用！\n\n而其他项目如果想使用它的仓库，只需要配置：\n\n```typescript\nextends: [ ['github:aatrooox/zc-auth-layer', { install: true }] ]\n```\n\n也可以它发布后的 npm 包\n\n```typescript\n// 直接使用npm 包\nextends: [ 'zc-auth-layer' ]\n// 某个组织下的包\nextends: [ '@zzaoclub/zc-auth-layer']\n```\n\n就像是在项目中使用其他 `components` 、`server api` 一样！\n\n**甚至还拥有完整的自动导入和 TS 类型提示，这你能受得了吗？**\n\n依赖关系是这样的，**A extend B ， B extend C ，C 拥有最高的优先级，向下覆盖。**\n\n所以哪怕是 `B Layer` 中只包含了一部分你需要的接口、组件，你也可以很轻松的再 extend 一个 `D Layer`，用来重写你不需要的逻辑。\n\n比如 B C 都是支付层，B 是支付宝支付，C 是微信支付。\n\n在主项目中使用 `/api/order/pay` 接口。继承了 B 层时，调用此接口就是支付宝支付，继承 C 后就是微信支付。\n\n亦或是 B 的接口是 `/api/order/pay/zfb` ，C 的接口是 `/api/order/pay/wx`，主项目中的接口就可以用来自由控制使用哪种支付方式时调起对应的接口。\n\n对于自己的项目来说，这种分层方式，可以让你更加自由的控制代码开源程度以及自己多项目复用逻辑。\n\n**对于出售源码时，也只需要出售对应层的代码，组合起来即可。**\n\n对解决博客换主题的功能，更是不能再适合了，只要按开发的博客时使用的 Components，**组件名一一对应就可以了**。\n\n当然，如果实际应用到项目上，还有类似数据库、多环境等问题**需要仔细斟酌**。\n\n但 Layer 的分层结构已经显而易见的更简单、更直接，开发体验也更好了。\n\n## 潜在的问题\n\n对于简单的前端项目，或是不涉及复杂的数据库架构，Layer 的方式无疑是更优雅的。\n\n既在文件结构上解耦了，同时继承（覆盖）时也很直接。\n\n但如果涉及到多个接口服务，或是多个数据库服务，甚至前端业务也比较复杂。还要是仔细测试其兼容性。\n\n毕竟虽然使用 `Layer` 在文件结构上分开的，但实际打包后是在同一个 `Nuxt` 服务里的，并不是类似微服务的那种形式。\n\n话说回来，大部分使用者应该只是看中了 SSR 能力，可能不会使用 Nitro 构建大型的应用，尤其是对于公司来说，也不太信任 Node 的服务能力。\n\n所以对于个人开发者来说，我觉得 `Layer` 是个锦上添花的利器。因为大部分业务不会太复杂，而且偏前端更多一些。自己封装好的各种功能的 `Layer` 信手拈来，开发效率大大提高，还不耽误二次开发。\n\n于是我在给自己的[博客](https://blog.zzao.club)尝试增加了一个权限层 [auth layer](https://github.com/aatrooox/zc-auth-layer) 后，才写下了这篇文章。\n\n期待后续 Layer 的大放异彩。\n\n",{"title":5,"description":79},"post/nuxt/nuxt4-use-layers",[990],[1041],"nuxt@3.15.2","HxnvgveLcmd6gzhxS2IcFiS12g-wKkhVHDx2v5HgXaU",[1044,1048],{"title":1045,"path":1046,"stem":1047},"OpenClaw 安装入门（Windows）","/post/zzao/openclaw/openclaw-install-windows","post/zzao/openclaw/openclaw-install-windows",{"title":1049,"path":1050,"stem":1051},"假设你是AI，你的Skill应该是什么样的","/post/zzao/ai-skill-structure","post/zzao/ai-skill-structure",1779005086232]