Vue.js/vue-router:创建带Props的单文件组件实例及列表抽象优化
Hey there! Let's break down how to streamline your Vue code by optimizing that list abstraction you're working on. From what you've described—having a base List.vue with async data loading, plus multiple derived components that wrap it and pass props—this is the perfect spot to decouple concerns and cut down on repetitive code. I’ve used this exact pattern in several Vue projects, so let’s walk through the optimized structure and implementation.
Core Problem to Solve
Right now, your derived components are essentially just thin wrappers around the base List.vue that pass props for data loading. This creates unnecessary redundant code. The goal is to completely separate list UI rendering from data loading logic using a plugin-based structure, making your code more modular and maintainable.
Recommended Folder Structure After Plugin Split
Here’s a clean, scalable folder structure to organize your refactored code:
src/components/BaseList.vue
plugins/listLoader/index.jsloaders/userLoader.jsproductLoader.js// Add more loaders for other business entities as needed
mixins/listLoaderMixin.js
views/UserListPage.vueProductListPage.vue
Step-by-Step Implementation
1. Refactor BaseList.vue to Be UI-Only
Strip all async loading logic from your original List.vue—it should only handle rendering based on props passed to it. Use slots to let business pages customize list item content:
<template> <div class="base-list"> <div v-if="loading" class="loading-spinner">Loading data...</div> <div v-else-if="error" class="error-message">{{ error }}</div> <div v-else-if="!items.length" class="empty-state">No items found</div> <ul v-else class="list-container"> <li v-for="item in items" :key="item.id" class="list-item"> <slot name="item" :item="item"></slot> </li> </ul> </div> </template> <script setup> defineProps({ items: { type: Array, default: () => [] }, loading: { type: Boolean, default: false }, error: { type: String, default: '' } }); </script>
2. Build the List Loader Plugin
Create a plugin to centralize your loading logic and make it reusable across components. This keeps your data fetching code organized and accessible:
// src/plugins/listLoader/index.js import listLoaderMixin from './mixins/listLoaderMixin'; import userLoader from './loaders/userLoader'; import productLoader from './loaders/productLoader'; export default { install(app) { // Register global mixin for loading state management app.mixin(listLoaderMixin); // Attach loaders to global properties for easy access app.config.globalProperties.$listLoaders = { user: userLoader, product: productLoader }; } };
3. Create Reusable Data Loaders
Each loader is a pure async function focused solely on fetching its respective data. No component-specific logic here:
// src/plugins/listLoader/loaders/userLoader.js export default async function userLoader(params = {}) { const response = await fetch(`/api/users?${new URLSearchParams(params)}`); if (!response.ok) throw new Error('Failed to load user data'); return response.json(); }
4. Shared Loading State Mixin
This mixin handles the repetitive loading/error/empty state logic so you don’t have to rewrite it in every page:
// src/plugins/listLoader/mixins/listLoaderMixin.js export default { data() { return { listLoading: false, listError: '', listItems: [] }; }, methods: { async loadListData(loader, params = {}) { this.listLoading = true; this.listError = ''; try { this.listItems = await loader(params); } catch (err) { this.listError = err.message; } finally { this.listLoading = false; } } } };
5. Use in Business Pages
Now you can ditch those redundant derived components. Instead, combine BaseList with the appropriate loader directly in your page components:
<template> <BaseList :items="listItems" :loading="listLoading" :error="listError" > <template #item="{ item }"> <div class="user-item"> <h3>{{ item.fullName }}</h3> <p>{{ item.email }}</p> <button @click="viewUser(item.id)">View Details</button> </div> </template> </BaseList> </template> <script setup> import BaseList from '@/components/BaseList.vue'; import { onMounted } from 'vue'; // Use global loader or import directly const userLoader = inject('$listLoaders').user; onMounted(() => { // Load data on component mount with optional params loadListData(userLoader, { page: 1, limit: 20 }); }); const viewUser = (userId) => { // Handle user detail navigation }; </script>
Key Benefits of This Approach
- Full Decoupling: List UI and data logic are completely separate, so
BaseListcan be reused anywhere you need a list view - Less Redundancy: No more creating dozens of similar
XxxList.vuecomponents—pages only handle their specific business logic - Better Maintainability: Data fetching logic lives in one place; updating a data endpoint only requires changing one loader file
- Scalability: Adding a new list type is as simple as creating a new loader and using it in a page
内容的提问来源于stack exchange,提问作者товіаѕ

