首页/技术教程/Vue3+Vue2结合的写法真舒服

Vue3的API太多了,有50个左右,还有很多新概念,新写法,让我一直没有感受到写代码的乐趣,而是在Vue3的迷宫中晕头转向。

我认真思考过,我写代码的要求有以下几个:

  1. 尽可能地简单,不要用太多API。
  2. 尽可能地直观,不要一个页面的代码散落地到处都是。
  3. 不要太多抽象,比如hooks,尽可能地直观。
  4. 要方便维护和管理,不同类型放到一块,比如Vue2的选项式写法管理起来就挺方便的。
  5. 当然,我不可能继续用回Vue2,我需要结合Vue3 + Vue2的优点。

所以,我的思路和案例如下:

基本约定

  • 计算属性统一放入 $Computed
  • 方法统一放入 $Method
  • 非响应式常量统一放入 $Const
  • 模板中访问 $Computed 的成员必须使用 .value
  • 保持分区顺序:// $From// $Data// $Computed// $Method// $Life// $Watch

所有集合变量的说明与好处

  • $From:说明=外部引入与类型定义区(import/type/interface);好处=依赖入口集中、理解上下文更快。
  • $Prop:说明=组件 props 集中声明(defineProps/withDefaults);好处=输入与默认值清晰、边界稳定。
  • $Emit:说明=组件 emits 集中声明(defineEmits);好处=输出事件可控、接口对齐更稳。
  • $Data:说明=响应式状态入口(reactive);好处=状态集中、定位与调试更容易。
  • $Computed:说明=派生状态入口(computed);好处=职责清晰、模板统一 .value 更安全。
  • $Method:说明=交互与行为入口;好处=调用集中、可搜索、易重构。
  • $Const:说明=非响应式常量/配置集合;好处=避免误追踪、配置复用一致。

简单但完整的案例

<template>
    <div class="page-orders">
        <div class="toolbar">
            <div class="title">{{ $Prop.title }}</div>
            <div class="chips">
                <div class="chip" :class="{ active: $Data.filter === 'all' }" @click="$Method.setFilter('all')">全部</div>
                <div class="chip" :class="{ active: $Data.filter === 'paid' }" @click="$Method.setFilter('paid')">已支付</div>
                <div class="chip" :class="{ active: $Data.filter === 'unpaid' }" @click="$Method.setFilter('unpaid')">未支付</div>
            </div>
            <div class="summary">{{ $Computed.totalText.value }}</div>
        </div>

        <div class="table">
            <div class="head">
                <div class="cell" v-for="col in $Computed.visibleColumns.value" :key="col.key">{{ col.title }}</div>
            </div>
            <div class="row" v-for="row in $Computed.pagedRows.value" :key="row.id" @click="$Method.onRowClick(row)">
                <div class="cell" v-for="col in $Computed.visibleColumns.value" :key="col.key">{{ row[col.key] }}</div>
            </div>
        </div>

        <div class="pager">
            <div class="btn" :class="{ disabled: !$Computed.canPrev.value }" @click="$Method.prev">上一页</div>
            <div class="text">第 {{ $Data.page }} / {{ $Computed.pageCount.value }} 页</div>
            <div class="btn" :class="{ disabled: !$Computed.canNext.value }" @click="$Method.next">下一页</div>
        </div>
    </div>
</template>

<script setup lang="ts">
// $From
import { computed, reactive } from "vue";

const $Prop = withDefaults(
    defineProps<{
        title?: string;
        showAmount?: boolean;
    }>(),
    {
        title: "订单列表",
        showAmount: true
    }
);

const $Emit = defineEmits<{
    (eventName: "filter-change", payload: { filter: "all" | "paid" | "unpaid" }): void;
    (eventName: "row-click", payload: { id: number }): void;
}>();

type OrderItem = {
    id: number;
    name: string;
    amount: number;
    status: "paid" | "unpaid";
};

// $Data
const $Data = reactive<{
    filter: "all" | "paid" | "unpaid";
    page: number;
    pageSize: number;
    rows: OrderItem[];
}>({
    filter: "all",
    page: 1,
    pageSize: 5,
    rows: [
        { id: 1, name: "订单 A", amount: 199, status: "paid" },
        { id: 2, name: "订单 B", amount: 49, status: "unpaid" },
        { id: 3, name: "订单 C", amount: 88, status: "paid" },
        { id: 4, name: "订单 D", amount: 20, status: "unpaid" },
        { id: 5, name: "订单 E", amount: 69, status: "paid" },
        { id: 6, name: "订单 F", amount: 299, status: "paid" }
    ]
});

// $Const
const $Const = {
    columns: [
        { key: "id", title: "ID" },
        { key: "name", title: "名称" },
        { key: "amount", title: "金额" },
        { key: "status", title: "状态" }
    ] as const
};

// $Computed
const $Computed = {
    filteredRows: computed(() => {
        if ($Data.filter === "paid") return $Data.rows.filter((row) => row.status === "paid");
        if ($Data.filter === "unpaid") return $Data.rows.filter((row) => row.status === "unpaid");
        return $Data.rows;
    }),
    visibleColumns: computed(() => {
        if ($Prop.showAmount) return $Const.columns;
        return $Const.columns.filter((col) => col.key !== "amount");
    }),
    pageCount: computed(() => Math.max(1, Math.ceil($Computed.filteredRows.value.length / $Data.pageSize))),
    pagedRows: computed(() => {
        const start = ($Data.page - 1) * $Data.pageSize;
        return $Computed.filteredRows.value.slice(start, start + $Data.pageSize);
    }),
    canPrev: computed(() => $Data.page > 1),
    canNext: computed(() => $Data.page < $Computed.pageCount.value),
    totalText: computed(() => `共 ${$Computed.filteredRows.value.length} 条`)
};

// $Method
const $Method = {
    setFilter: function (filter: "all" | "paid" | "unpaid"): void {
        $Data.filter = filter;
        $Data.page = 1;
        $Emit("filter-change", { filter: filter });
    },
    prev: function (): void {
        if ($Computed.canPrev.value) $Data.page -= 1;
    },
    next: function (): void {
        if ($Computed.canNext.value) $Data.page += 1;
    },
    onRowClick: function (row: OrderItem): void {
        $Emit("row-click", { id: row.id });
    }
};
</script>

如此一来,写代码又变成了一种享受。