[{"data":1,"prerenderedAt":358},["ShallowReactive",2],{"content:\u002Fposts\u002F2024\u002Fvue-good-practices-and-design-patterns-for-vue-composables":3,"surround:\u002Fposts\u002F2024\u002Fvue-good-practices-and-design-patterns-for-vue-composables":347},{"id":4,"title":5,"body":6,"canonical":313,"categories":314,"date":316,"description":23,"draft":317,"extension":318,"image":319,"meta":320,"navigation":331,"path":332,"permalink":333,"readingTime":334,"recommend":333,"references":339,"seo":342,"sitemap":343,"stem":344,"tags":345,"type":333,"updated":316,"__hash__":346},"content\u002Fposts\u002F2024\u002Fvue-good-practices-and-design-patterns-for-vue-composables.md","【翻译】Vue 可组合项的良好实践和设计模式",{"type":7,"value":8,"toc":297},"minimark",[9,18,21,24,27,40,43,46,57,61,64,74,77,85,88,91,97,100,106,109,112,176,178,181,185,188,191,198,201,204,207,210,213,220,226,240,257,260,262,265],[10,11,12,13,17],"p",{},"最近，我与 Vue Storefront 的团队就编写 Vue 可组合项的模式进行了一次精彩的讨论。就我们的系统而言，可组合项负责存储主要业务逻辑（如计算、操作、流程），因此它们是应用程序的关键部分。不幸的是，随着时间的推移，我们没有那么多时间来创建某种 ",[14,15,16],"code",{"code":16},"Contract for writing Composables"," 因为我们的一些可组合项并不是真正的可组合项 😉",[10,19,20],{},"我真的很高兴现在我们有时间重构我们构建新可组合项的方法，以便它们可维护、易于测试且真正有用。",[10,22,23],{},"在本文中，我将总结我们创建的想法，并将它们与我在几篇文章中读到的良好实践和设计模式相结合。",[10,25,26],{},"所以本文将分为三个部分：",[28,29,30,34,37],"ol",{},[31,32,33],"li",{},"通用设计模式",[31,35,36],{},"我的建议",[31,38,39],{},"进一步阅读",[10,41,42],{},"享受并且让我知道您在项目中使用的模式和实践 🚀",[44,45,33],"h2",{"id":33},[10,47,48,49,56],{},"我认为了解构建可组合项模式的最佳来源实际上是 Vue.js 文档，您可以",[50,51,55],"a",{"href":52,"rel":53},"https:\u002F\u002Fvuejs.org\u002Fguide\u002Freusability\u002Fcomposables.html",[54],"nofollow","在此处","查看",[58,59,60],"h3",{"id":60},"基础可组合项",[10,62,63],{},"Vue 文档显示了以下 useMouse 可组合项的示例：",[65,66,72],"pre",{"className":67,"code":69,"language":70,"meta":71},[68],"language-js","\u002F\u002F mouse.js\nimport { onMounted, onUnmounted, ref } from \"vue\"\n\n\u002F\u002F by convention, composable function names start with \"use\"\nexport function useMouse() {\n    \u002F\u002F state encapsulated and managed by the composable\n    const x = ref(0)\n    const y = ref(0)\n\n    \u002F\u002F a composable can update its managed state over time.\n    function update(event) {\n        x.value = event.pageX\n        y.value = event.pageY\n    }\n\n    \u002F\u002F a composable can also hook into its owner component's\n    \u002F\u002F lifecycle to setup and teardown side effects.\n    onMounted(() => window.addEventListener(\"mousemove\", update))\n    onUnmounted(() => window.removeEventListener(\"mousemove\", update))\n\n    \u002F\u002F expose managed state as return value\n    return { x, y }\n}\n","js","",[14,73,69],{"__ignoreMap":71},[10,75,76],{},"你可以在组件中使用它，如下所示：",[65,78,83],{"className":79,"code":81,"language":82,"meta":71},[80],"language-html","\u003Cscript setup>\n  import { useMouse } from \".\u002Fmouse.js\"\n\n  const { x, y } = useMouse()\n\u003C\u002Fscript>\n\n\u003Ctemplate>Mouse position is at: {{ x }}, {{ y }}\u003C\u002Ftemplate>\n","html",[14,84,81],{"__ignoreMap":71},[58,86,87],{"id":87},"异步可组合项",[10,89,90],{},"为了获取数据，Vue 建议使用以下可组合结构：",[65,92,95],{"className":93,"code":94,"language":70,"meta":71},[68],"import { ref, toValue, watchEffect } from \"vue\"\n\nexport function useFetch(url) {\n    const data = ref(null)\n    const error = ref(null)\n\n    watchEffect(() => {\n    \u002F\u002F reset state before fetching..\n        data.value = null\n        error.value = null\n        \u002F\u002F toValue() unwraps potential refs or getters\n        fetch(toValue(url))\n            .then(res => res.json())\n            .then(json => (data.value = json))\n            .catch(err => (error.value = err))\n    })\n\n    return { data, error }\n}\n",[14,96,94],{"__ignoreMap":71},[10,98,99],{},"然后可以在组件中使用它，如下所示：",[65,101,104],{"className":102,"code":103,"language":82,"meta":71},[80],"\u003Cscript setup>\n  import { useFetch } from \".\u002FuseFetch.ts\"\n\n  const { data, error } = useFetch(\"...\")\n\u003C\u002Fscript>\n",[14,105,103],{"__ignoreMap":71},[58,107,108],{"id":108},"可组合项约定",[10,110,111],{},"根据上面的示例，以下是所有可组合项都应遵循的约定：",[28,113,114,124,139,149,152,155,170,173],{},[31,115,116,117,120,121],{},"可组合文件名应以 ",[14,118,119],{"code":119},"use"," 开头，例如 ",[14,122,123],{"code":123},"useSomeAmazingFeature.ts",[31,125,126,127,130,131,134,135,138],{},"它可以接受输入参数，这些参数可以是字符串等基本类型，也可以接受 ",[14,128,129],{"code":129},"refs"," 和 ",[14,132,133],{"code":133},"getter","，但它需要使用 ",[14,136,137],{"code":137},"toValue"," 帮助器",[31,140,141,142,145,146],{},"可组合项应该返回一个 ",[14,143,144],{"code":144},"ref"," 值，该值可以在解构可组合项后访问，例如 ",[14,147,148],{"code":148},"const { x, y } = useMouse()",[31,150,151],{},"可组合项可以保存可以在应用程序中访问和修改的全局状态。",[31,153,154],{},"可组合性可能会导致副作用，例如添加窗口事件侦听器，但应在卸载组件时清除它们。",[31,156,157,158,161,162,165,166,169],{},"可组合项只能在 ",[14,159,160],{"code":160},"\u003Cscript setup>"," 或 ",[14,163,164],{"code":164},"setup()"," 挂钩中调用。它们也应该在这些上下文中同步调用。在某些情况下，您还可以在生命周期挂钩中调用它们，例如 ",[14,167,168],{"code":168},"onMounted()"," 。",[31,171,172],{},"可组合项可以调用内部的其他可组合项。",[31,174,175],{},"可组合项应在内部包装某些逻辑，当过于复杂时，应将它们提取到单独的可组合项中以便于测试。",[44,177,36],{"id":36},[10,179,180],{},"我已经为我的工作项目和开源项目构建了多个可组合项 - NuxtAlgolia、NuxtCloudinary、NuxtMedusa，因此基于这些，我想根据我的经验在上面的合同中添加一些要点。",[58,182,184],{"id":183},"有状态或和纯函数可组合项","有状态或\u002F和纯函数可组合项",[10,186,187],{},"在代码标准化的某个时刻，您可能会得出这样的结论：您希望对可组合项中的状态保留做出决定。",[10,189,190],{},"最容易测试的函数是那些不存储任何状态的函数（即它们是简单的输入\u002F输出函数），例如负责将字节转换为人类可读值的可组合函数。它接受一个值并返回一个不同的值 - 它不存储任何状态。",[10,192,193,194,197],{},"不要误会我的意思，您不必做出决定 ",[14,195,196],{"code":196},"OR"," 。您可以完全保留有状态和无状态可组合项。但这应该是一个书面决定，以便以后更容易与他们合作 🙂",[58,199,200],{"id":200},"可组合项的单元测试",[10,202,203],{},"我们希望使用 Vitest 为我们的前端应用程序实施单元测试。在后端工作时，进行单元测试代码覆盖率非常有用，因为您主要关注逻辑。然而，在前端，您通常使用视觉效果。",[10,205,206],{},"因此，我们认为对整个组件进行单元测试可能不是最好的想法，因为我们基本上将对框架本身进行单元测试（如果按下按钮，检查状态是否更改或模式是否打开）。",[10,208,209],{},"由于我们已将所有业务逻辑移至可组合项（基本上是 TypeScript 函数）内，因此它们很容易使用 Vitest 进行测试，并允许我们拥有更稳定的系统。",[58,211,212],{"id":212},"限制可组合项的范围",[10,214,215,216,219],{},"不久前，在 ",[14,217,218],{"code":218},"VueStorefront"," 中，我们开发了自己的可组合方法（早在它们实际上像这样被调用之前）。在我们的方法中，我们使用可组合项来映射电子商务的业务领域，如下所示：",[65,221,224],{"className":222,"code":223,"language":70,"meta":71},[68],"const { cart, load, addItem, removeItem, remove, } = useCart()\n",[14,225,223],{"__ignoreMap":71},[10,227,228,229,161,232,235,236,239],{},"这种方法绝对有用，因为它允许将域包装在一个函数中。在 ",[14,230,231],{"code":231},"useProduct",[14,233,234],{"code":234},"useCategory"," 等更简单的示例中，实现和维护相对简单。然而，正如您在 ",[14,237,238],{"code":238},"useCart"," 的示例中所看到的，当包装一个包含更多逻辑而不仅仅是数据获取的域时，这个可组合项正在发展成为一种非常难以开发和维护的形状。",[10,241,242,243,245,246,249,250,249,253,256],{},"此时，我开始为 Nuxt 生态系统做出贡献，其中引入了不同的方法。在这种新方法中，每个可组合项仅负责一件事。因此，我们的想法不是构建一个巨大的 ",[14,244,238],{"code":238}," 可组合项，而是为每个功能构建可组合项，即 ",[14,247,248],{"code":248},"useAddToCart"," 、 ",[14,251,252],{"code":252},"useFetchCart",[14,254,255],{"code":255},"useRemovefromCart"," 等。",[10,258,259],{},"因此，维护和测试这些可组合项应该更容易 🙂",[44,261,39],{"id":39},[10,263,264],{},"这将是我的研究的全部内容。如果您想了解有关此主题的更多信息，请务必查看以下文章：",[266,267,268,273,279,285,291],"ul",{},[31,269,270],{},[50,271,52],{"href":52,"rel":272},[54],[31,274,275],{},[50,276,277],{"href":277,"rel":278},"https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=bcZM3EogPJE",[54],[31,280,281],{},[50,282,283],{"href":283,"rel":284},"https:\u002F\u002Fvueschool.io\u002Farticles\u002Fvuejs-tutorials\u002Fwhat-is-a-vue-js-composable\u002F",[54],[31,286,287],{},[50,288,289],{"href":289,"rel":290},"https:\u002F\u002Fblog.logrocket.com\u002Fgetting-started-vue-composables\u002F",[54],[31,292,293],{},[50,294,295],{"href":295,"rel":296},"https:\u002F\u002Fmacopedia.com\u002Fblog\u002Fnews\u002Fhow-can-vue-3-composables-make-your-life-easier",[54],{"title":71,"searchDepth":298,"depth":298,"links":299},4,[300,307,312],{"id":33,"depth":301,"text":33,"children":302},2,[303,305,306],{"id":60,"depth":304,"text":60},3,{"id":87,"depth":304,"text":87},{"id":108,"depth":304,"text":108},{"id":36,"depth":301,"text":36,"children":308},[309,310,311],{"id":183,"depth":304,"text":184},{"id":200,"depth":304,"text":200},{"id":212,"depth":304,"text":212},{"id":39,"depth":301,"text":39},"https:\u002F\u002Fdev.to\u002Fjacobandrewsky\u002Fgood-practices-and-design-patterns-for-vue-composables-24lk",[315],"译","2024-12-15 13:38:52",false,"md","https:\u002F\u002Fbitmc.uno\u002Fpicgo\u002F679f12e4c539c-78.webp",{"excerpt":321},{"type":7,"value":322},[323,327,329],[10,324,12,325,17],{},[14,326,16],{"code":16},[10,328,20],{},[10,330,23],{},true,"\u002Fposts\u002F2024\u002Fvue-good-practices-and-design-patterns-for-vue-composables",null,{"text":335,"minutes":336,"time":337,"words":338},"9 min read",8.065,483900,1613,[340],{"title":341,"link":313},"Good practices and Design Patterns for Vue Composables",{"title":5,"description":23},{"loc":332},"posts\u002F2024\u002Fvue-good-practices-and-design-patterns-for-vue-composables",[],"SQMRRCiPrcqlbEd_jSlFZpKFV67wWq4mQY60yteYkLc",[348,353],{"title":349,"path":350,"stem":351,"date":352,"type":333,"children":-1},"【翻译】编写更简洁的Vue.js代码的10个技巧","\u002Fposts\u002F2024\u002Fvue-cleaner-vue-code","posts\u002F2024\u002Fvue-cleaner-vue-code","2024-12-15 12:44:24",{"title":354,"path":355,"stem":356,"date":357,"type":333,"children":-1},"【翻译】在 Vue 中使用 URL 存储状态","\u002Fposts\u002F2024\u002Fvue-using-url-to-store-state-in-vue","posts\u002F2024\u002Fvue-using-URL-to-store-state-in-vue","2024-12-17 15:39:05",1789642591257]