协作头脑风暴
brainstorming
在实现前澄清需求、探索设计选择,并形成清晰的开发方向。
vue-architecture【开发与工程】Use this skill when the user says 'Vue structure', 'Vue architecture', 'Vue 3 folder', 'Composition API architecture', 'Vue clean arch', 'Vue feature structure', 'Pinia architecture', 'Vue project layout', or when structuring a Vue 3 application. This skill enforces: feature-based folder structure, script setup always, Composition API with composable…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Structure Vue 3 applications with Composition API, feature-based folders, and Pinia stores. All logic in composables. script setup always.
Exact user phrases: "Vue structure", "Vue architecture", "Vue 3 folder", "Composition API architecture", "Vue clean arch", "Vue feature structure", "Pinia architecture", "Vue project layout".
Before activating, verify:
No file output. Produces folder structure and Vue component code as text.
Folder structure:
src/
features/{feature}/
composables/, components/
shared/components/
stores/
Code: show and . No block.
No preamble. No postamble. No explanations. No filler/hedging/transitions. Compress output — why use many token when few do trick.
Folder structure: unlimited. Code: 20 lines per example.
| Approach | Trade-off | When to Use |
|---|---|---|
| Feature-based folders | Cohesion, scalability | All projects with multiple features |
| Type-based (components/, composables/) | Simple for small apps | Small projects, prototypes |
| Composition API + script setup | Modern Vue, type-safe | All new Vue 3 code |
| Options API | Legacy, verbose | Existing Vue 2 migration |
| Pinia stores | Global state management | Multi-component shared state |
| Composables | Reusable logic | Data fetching, form logic, browser APIs |
Does the component fetch data or manage state?
├── Yes -> Smart component (feature-specific)
│ ├── Uses composables for logic
│ ├── Renders shared UI components
│ └── Lives in features/{feature}/components/
└── No -> Dumb (presentational) component
├── Receives props, emits events
├── No data fetching
└── Lives in shared/components/
How is the state scoped?
├── Local (single component) -> ref() / reactive()
├── Feature-level (few components) -> composable + provide/inject
└── Global (many components across features) -> Pinia store
Is this reusable logic or shared state?
├── Reusable logic (data fetching, formatting, browser API) -> composable
└── Shared state (auth, cart, theme) -> Pinia store
<script setup lang="ts">
const { users, isLoading, error, refresh } = useUsers()
const props = defineProps<{ userId: string }>()
const emit = defineEmits<{ select: [id: string] }>()
const displayName = computed(() =>
users.value?.find(u => u.id === props.userId)?.name ?? 'Unknown'
)
watch(() => props.userId, () => refresh())
</script>
<template>
<div v-if="isLoading">Loading...</div>
<div v-else-if="error">{{ error }}</div>
<div v-else @click="emit('select', userId)">
<p>{{ displayName }}</p>
<ul><li v-for="user in users" :key="user.id">{{ user.name }}</li></ul>
</div>
</template>
<script setup lang="ts">
interface Props { variant?: 'primary' | 'secondary'; disabled?: boolean }
interface Emits { (e: 'click'): void }
const props = withDefaults(defineProps<Props>(), { variant: 'primary' })
const emit = defineEmits<Emits>()
</script>
<template>
<button :class="['btn', `btn-${variant}`]" :disabled="disabled" @click="emit('click')">
<slot />
</button>
</template>
<style scoped>
.btn { padding: 0.5rem 1rem; border-radius: 0.375rem; cursor: pointer; }
.btn-primary { background: #3b82f6; color: white; border: none; }
.btn-secondary { background: #e5e7eb; color: #374151; border: 1px solid #d1d5db; }
</style>
export function useUsers(filters?: Ref<UserFilters>) {
const users = ref<User[]>([])
const isLoading = ref(false)
const error = ref<string | null>(null)
async function refresh() {
isLoading.value = true
error.value = null
try {
users.value = await api.getUsers(filters?.value)
} catch (e) {
error.value = e instanceof Error ? e.message : 'Unknown error'
} finally {
isLoading.value = false
}
}
return { users, isLoading, error, refresh }
}
const count = ref(0)
const user = reactive({ name: 'Alice', email: 'alice@test.com' })
count.value++
user.name = 'Bob'
const count = ref(0)
const doubled = computed(() => count.value * 2)
const status = computed(() => count.value > 10 ? 'High' : 'Low')
watch(count, (newVal, oldVal) => {
console.log(`Count changed from ${oldVal} to ${newVal}`)
})
watchEffect(() => {
localStorage.setItem('count', String(count.value))
})
// stores/auth.store.ts
import { defineStore } from 'pinia'
export const useAuthStore = defineStore('auth', () => {
const user = ref<User | null>(null)
const token = ref<string | null>(null)
const isAuthenticated = computed(() => !!token.value)
async function login(email: string, password: string) {
const res = await api.login(email, password)
user.value = res.user
token.value = res.token
}
function logout() {
user.value = null
token.value = null
}
return { user, token, isAuthenticated, login, logout }
})
export const ThemeKey: InjectionKey<{ theme: Ref<string>; toggle: () => void }> = Symbol('ThemeKey')
shallowRef and shallowReactive for large data that doesn't need deep trackingv-memo for list items that rarely changev-once for static contentshallowRef for large arrays that are replaced entirelyimport { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'vue-router', 'pinia'],
},
},
},
},
})
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"paths": { "@/*": ["./src/*"] }
}
}
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Button from './Button.vue'
describe('Button', () => {
it('renders slot content', () => {
const wrapper = mount(Button, { slots: { default: 'Click' } })
expect(wrapper.text()).toBe('Click')
})
it('emits click on click', async () => {
const wrapper = mount(Button)
await wrapper.trigger('click')
expect(wrapper.emitted('click')).toHaveLength(1)
})
})
import { describe, it, expect } from 'vitest'
import { useCounter } from './useCounter'
describe('useCounter', () => {
it('increments', () => {
const { count, increment } = useCounter()
expect(count.value).toBe(0)
increment()
expect(count.value).toBe(1)
})
})
import { setActivePinia, createPinia } from 'pinia'
import { useAuthStore } from './auth.store'
beforeEach(() => setActivePinia(createPinia()))
it('starts unauthenticated', () => {
const store = useAuthStore()
expect(store.isAuthenticated).toBe(false)
})
<!-- Vue 2 -->
<script>
export default {
data: () => ({ count: 0 }),
computed: { doubled() { return this.count * 2 } },
watch: { count(val) { console.log(val) } },
methods: { increment() { this.count++ } },
}
</script>
<!-- Vue 3 -->
<script setup lang="ts">
const count = ref(0)
const doubled = computed(() => count.value * 2)
watch(count, (val) => console.log(val))
function increment() { count.value++ }
</script>
// Vuex
new Vuex.Store({ state: { count: 0 }, mutations: { increment: s => s.count++ } })
// Pinia
defineStore('counter', () => {
const count = ref(0)
function increment() { count.value++ }
return { count, increment }
})
<!-- Anti-pattern -->
<script setup>
const props = defineProps<{ count: number }>()
props.count++ // runtime warning
</script>
<!-- Correct: emit -->
<script setup>
const emit = defineEmits<{ update: [number] }>()
</script>
<button @click="emit('update', count + 1)">+</button>
Vue 3 supports both, but new code must use Composition API + script setup.
For data that only goes 1-2 levels, prop drilling is clearer and more traceable.
Components over 200 lines should be split: extract logic to composables, extract UI sections to child components.
ref() requires .value in script (not in template)reactive([]) not ref([]) if you need index access| Aspect | Vue 3 | React |
|---|---|---|
| Reactivity | Proxy-based, automatic | VDOM + hooks |
| Component syntax | SFC (template + script + style) | JSX only |
| State | ref/reactive | useState |
| Bundled state | Pinia | Zustand, Jotai |
| Bundle size | ~30KB | ~45KB |
Vue uses runtime reactivity (proxies); Svelte uses compile-time reactivity (runes). Vue has larger ecosystem; Svelte has smaller bundles.
| Package | Purpose |
|---|---|
| vue | Core framework |
| vue-router | Client-side routing |
| pinia | State management |
| vite | Build tool |
| @vue/test-utils | Component testing |
src/features/users/
composables/useUsers.ts
components/UserList.vue, UserCard.vue
types/index.ts
<script setup lang="ts">
const { users, isLoading } = useUsers()
const props = defineProps<{ userId: string }>()
const emit = defineEmits<{ select: [id: string] }>()
</script>
export function useUsers() {
const users = ref<User[]>([])
return { users }
}
export const useAuthStore = defineStore('auth', () => {
const user = ref<User | null>(null)
return { user }
})
Next skill: vue-nuxt (if using Nuxt) or frontend-testing. Carry forward: component organization, composable patterns, Pinia store structure.