V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
sunmoon1983
V2EX  ›  Vue.js

vue3 代码拆分最佳实践?

  •  
  •   sunmoon1983 · 2023-08-19 15:33:11 +08:00 · 2504 次点击
    这是一个创建于 460 天前的主题,其中的信息可能已经有所发展或是发生改变。

    老项目遗留了一些代码,VUE 页面里面的代码太长了 想把 TS 代码拆分出来,请问最佳实践是什么样子的? 旧代码:

    <template>
        <div class="table-tree-container">
            <div class="list-tree-wrapper">
                <div class="list-tree-operator">
                    <t-input v-model="filterText" placeholder="请输入过滤关键词" @change="onTreeInput">
                        <template #suffix-icon>
                            <search-icon size="var(--td-comp-size-xxxs)" />
                        </template>
                    </t-input>
                    <t-button block @click="onCleanActive" variant="dashed" size="small" class="mt-2">
                        <template #icon>
                            <i class="ri-close-circle-line mr-1"></i>
                        </template>
                        清空选中项
                    </t-button>
                    <t-tree class="mt-2" :data="categoryTree" activable v-model:actived="treeActived" @active="onTreeActive" :filter="filterByText" expand-all :keys="{ value: 'id', label: 'cate_name' }" hover expand-on-click-node />
                </div>
                <div class="list-tree-content">
                    <div class="index-container">
                        <t-card :bordered="false" :title="cardTitle" class="list-card-container">
                            <template #actions>
                                <t-button @click="onAdd" v-permission="'article/add'">
                                    <template #icon>
                                        <i class="ri-add-line mr-1"></i>
                                    </template>
                                    添加
                                </t-button>
                            </template>
                            <t-space>
                                <t-select :options="REC_OPTIONS" v-model="searchForm.rec" clearable placeholder="请选择推荐位" />
                                <t-select :options="STATUS_OPTIONS" v-model="searchForm.status" clearable placeholder="请选择状态" />
                                <t-input v-model="searchForm.kw" placeholder="请输入你需要搜索的内容" clearable />
                                <t-button @click="onSearch" theme="default">
                                    <template #icon>
                                        <i class="ri-search-line mr-1"></i>
                                    </template>
                                    搜索
                                </t-button>
                                <t-dropdown :options="DROPDOWN_OPTIONS" :max-column-width="200">
                                    <t-button variant="outline" theme="success"> 选中项 <i class="ri-arrow-down-s-line ml-1 icon-valign-top"></i> </t-button>
                                </t-dropdown>
                            </t-space>
                            <t-row :gutter="16" class="table-container">
                                <t-col>
                                    <t-table :data="data" stripe :columns="TABLE_COLUMNS" :vertical-align="'top'" :hover="true" :pagination="pagination" @select-change="onSelectChange" @page-change="onPageChange" :loading="dataLoading" :row-key="'id'">
                                        <template #id="{ row }">
                                            <var>{{ row['id'] }}</var>
                                        </template>
                                        <template #status="{ row }">
                                            <t-tag v-if="Number(row['status']) === 1" theme="success" variant="light"> 公开</t-tag>
                                            <t-tag v-else-if="Number(row['status']) === 0" theme="danger" variant="light"> 锁定</t-tag>
                                            <t-tag v-else theme="primary" variant="light"> 定时发布</t-tag>
                                        </template>
                                        <template #sort_by="{ row }">
                                            <t-input-number v-permission="'article/sort'" v-model="row.sort_by" @change="(v) => onSortChange(row, v)" theme="column"></t-input-number>
                                        </template>
                                        <template #created_at="{ row }">
                                            {{ formatTime(row['created_at']) }}
                                        </template>
                                        <template #cid="{ row }">
                                            {{ formatCate(row['cid']) }}
                                        </template>
                                        <template #author="{ row }">
                                            {{ row['author'] }}
                                        </template>
                                        <template #op="{ row }">
                                            <a class="t-button-link" v-permission="'article/edit'" @click="onEdit(row)">编辑</a>
                                        </template>
                                    </t-table>
                                </t-col>
                            </t-row>
                        </t-card>
                    </div>
                </div>
            </div>
        </div>
    </template>
    
    <script setup lang="ts">
    import { ref, onMounted, reactive } from 'vue';
    import { MessagePlugin, DialogPlugin } from 'tdesign-vue-next';
    import { STATUS_OPTIONS, REC_OPTIONS } from '@/config/global';
    import helper from '@/utils/helper';
    import { getArticleList, updateArticleSort, updateArticleStatus, deleteArticle } from '@/api/article';
    import { IApiResponse } from '@/types';
    import { TABLE_COLUMNS } from '@/pages/article/constants';
    import { useRouter } from 'vue-router';
    import { getArticleStore } from '@/store';
    
    const router = useRouter();
    const cardTitle = ref(helper.getPageTitle());
    const cateId = ref(0);
    const categoryTree = ref([]);
    
    const articleStore = getArticleStore();
    const fetchCategoryTree = async () => {
        const { treeList } = articleStore;
        if (helper.len(treeList) === 0) {
            try {
                await articleStore.getTreeCategory();
                categoryTree.value = articleStore.treeList;
            } catch (e) {
                return Promise.reject(e);
            }
        } else {
            categoryTree.value = treeList;
        }
    };
    const formatCate = (v) => {
        if (helper.len(categoryTree.value) <= 0) {
            return '';
        }
        let item = categoryTree.value.find((ele) => {
            if (Number(ele.id) === Number(v)) {
                return ele;
            }
            return null;
        });
        if (!item) {
            return v;
        }
        return item.cate_name;
    };
    const formatTime = (v) => {
        if (!v) {
            return '';
        }
        return helper.time().dateTimeDisplay(v);
    };
    const onAdd = () => {
        let url = '/article/add';
        if (cateId.value) {
            url += '?cate_id=' + cateId.value;
        }
        router.push(url);
    };
    const onEdit = (row) => {
        router.push('/article/edit?id=' + row.id);
    };
    const onSearch = () => {
        fetchData(searchForm.value);
    };
    
    const data = ref([]);
    const dataLoading = ref(false);
    const pagination = reactive({
        current: 1,
        pageSize: Number(import.meta.env.VITE_APP_PAGE_SIZE),
        total: 0,
        showJumper: true,
    });
    const searchForm = ref({
        kw: '',
        cid: null,
        rec: null,
        status: null,
        page: pagination.current,
        page_size: pagination.pageSize,
    });
    const onSortChange = async (row, value) => {
        try {
            helper.fullLoading();
            console.log(value, row);
            const resp: IApiResponse = await updateArticleSort({ id: row.id, value: value });
            if (resp.code === 0) {
                MessagePlugin.success('操作成功');
                await fetchData();
            } else {
                return Promise.reject(new Error(resp.message));
            }
        } catch (e) {
            MessagePlugin.error(e.message);
        } finally {
            helper.hideLoading();
        }
    };
    const onPageChange = async (pageInfo) => {
        const { current, pageSize } = pageInfo;
        pagination.current = current;
        pagination.pageSize = pageSize;
        await fetchData({ page: current, page_size: pageSize });
    };
    const onSelectChange = (value) => {
        selection.value = value;
    };
    const fetchData = async (params = null) => {
        dataLoading.value = true;
        try {
            let def = { ...searchForm.value };
            if (params) {
                def = Object.assign(def, params);
            }
            const res: IApiResponse = await getArticleList(def);
            if (res.code === 0) {
                const { data_list = [] } = res.data;
                data.value = data_list;
                pagination.total = res.data.total_count;
            } else {
                return Promise.reject(new Error(res.message));
            }
        } catch (e) {
            data.value = [];
            console.log(e);
            //MessagePlugin.error(e.message);
            pagination.total = 0;
        } finally {
            dataLoading.value = false;
        }
    };
    const onTreeActive = async (v, c) => {
        searchForm.value.cid = c.node.data.id;
        router.push({
            query: {
                cate_id: searchForm.value.cid,
            },
        });
        await fetchData();
    };
    const onCleanActive = async () => {
        router.push({
            query: {},
        });
        searchForm.value.cid = null;
        treeActived.value = [];
        await fetchData();
    };
    const treeActived = ref([]);
    const filterByText = ref();
    const filterText = ref();
    const onTreeInput = () => {
        filterByText.value = (node) => {
            const rs = node.label.indexOf(filterText.value) >= 0;
            return rs;
        };
    };
    onMounted(async () => {
        cateId.value = helper.getQueryToNumber('cate_id', router);
        if (cateId.value) {
            searchForm.value.cid = cateId.value;
        }
        await fetchCategoryTree();
        await fetchData();
    });
    const selection = ref([]);
    const DROPDOWN_OPTIONS = [
        {
            content: '删除选中内容',
            value: 2,
            onClick: () => {
                dropdownClick({ value: 2 });
            },
        },
        {
            content: '修改为公开',
            value: 1,
            onClick: () => {
                dropdownClick({ value: 1 });
            },
        },
        {
            content: '修改为隐藏',
            value: 0,
            onClick: () => {
                dropdownClick({ value: 0 });
            },
        },
    ];
    const onStatus = async (params) => {
        try {
            helper.fullLoading();
            const resp: IApiResponse = await updateArticleStatus(params);
            if (resp.code === 0) {
                MessagePlugin.success('操作成功');
                await fetchData();
            } else {
                return Promise.reject(new Error(resp.message));
            }
        } catch (e) {
            MessagePlugin.error(e.message);
        } finally {
            helper.hideLoading();
        }
    };
    const onDelete = async (params) => {
        const dialogNode = DialogPlugin.confirm({
            header: '系统提示',
            body: '确定要删除吗?',
            theme: 'info',
            onConfirm: async () => {
                dialogNode.hide();
                await deleteAction(params);
            },
        });
    };
    const deleteAction = async (params) => {
        try {
            helper.fullLoading();
            const resp: IApiResponse = await deleteArticle(params);
            if (resp.code === 0) {
                MessagePlugin.success('操作成功');
                await fetchData();
            } else {
                return Promise.reject(new Error(resp.message));
            }
        } catch (e) {
            MessagePlugin.error(e.message);
        } finally {
            helper.hideLoading();
        }
    };
    const dropdownClick = (data) => {
        if (data.value === 1 && helper.len(selection.value) === 0) {
            MessagePlugin.warning('请选择要操作的数据');
            return false;
        }
        let params = { ids: selection.value.join(',') };
        if (data.value === 2) {
            onDelete(params);
        } else {
            params['status'] = data.value;
            onStatus(params);
        }
    };
    </script>
    
    <style lang="less" scoped>
    .table-tree-container {
        background-color: var(--td-bg-color-container);
        border-radius: var(--td-radius-medium);
        .t-tree {
            margin-top: var(--td-comp-margin-xxl);
        }
    }
    .list-tree-wrapper {
        overflow-y: hidden;
        background: #fff;
    }
    .list-tree-operator {
        width: 210px;
        float: left;
        padding: var(--td-comp-paddingTB-xxl) var(--td-comp-paddingLR-xxl);
    }
    .list-tree-content {
        border-left: 1px solid var(--td-border-level-1-color);
        overflow: auto;
    }
    </style>
    
    
    12 条回复    2023-10-11 18:41:18 +08:00
    zcf0508
        1
    zcf0508  
       2023-08-19 15:40:04 +08:00   ❤️ 1
    试试把代码复制到 https://hook.huali.cafe 看一下右侧的依赖关系,把关联比较明确的函数和方法独立出去改写成 hook 。

    这是工具是我做的一个开源项目 https://github.com/zcf0508/vue-hook-optimizer/blob/master/README_cn.md
    lscho
        2
    lscho  
       2023-08-19 15:54:59 +08:00
    把相对独立的拆分为 hooks 就行了,大概看了一下代码,你这个 categoryTree 就应该拆分出去,这样就少了一大坨代码。

    然后增删改查四个逻辑,也可以单独写个 hooks 拆分。

    剩下的基本就是每个页面独有的功能了,不太多
    sunmoon1983
        3
    sunmoon1983  
    OP
       2023-08-19 18:36:27 +08:00
    嗯,我想把这个组优件中所有的 TS 代码都写成一个 TS 文件里,然后这个组件的解构应该就是
    ```
    artcile
    ----index.vue
    ----index.ts
    ----index.less
    ```
    不知道这样行不行
    sunmoon1983
        4
    sunmoon1983  
    OP
       2023-08-19 18:36:56 +08:00
    @zcf0508 好的,我拜读一下
    zcf0508
        5
    zcf0508  
       2023-08-19 18:50:35 +08:00 via Android
    @sunmoon1983 如果只是拆语言不是拆逻辑,那没什么用
    murmur
        6
    murmur  
       2023-08-19 19:04:35 +08:00
    如果没有复用就不拆,现在的 IDE 都有函数列表,先完成功能再去优化,路由直接做下懒加载就完了
    zcf0508
        7
    zcf0508  
       2023-08-19 19:09:29 +08:00
    @murmur 拆代码除了复用,也是为了让逻辑更清晰,以后修改起来更方便
    murmur
        8
    murmur  
       2023-08-19 19:11:57 +08:00
    @zcf0508 有的开发尤其是企业级应用,表单复杂,字段繁多,逻辑复杂,那就是应用的特性,没必要强求,而且 vue 对着 template 一点就过去了,何必为难自己
    yrj
        9
    yrj  
       2023-08-19 21:22:48 +08:00
    功能提取成 hook ,视图拆分成组件
    netabare
        10
    netabare  
       2023-08-20 03:12:33 +08:00 via Android   ❤️ 1
    都用了 vue3 ,感觉最主要的就是拆组件然后用 emit 和 props 来进行组件间传递吧。不过一大块已经有的页面往下拆确实不好拆。

    按照语言拆没什么用吧,这看着就像写 angular 一样。vue 的思路应该是一个 vue 文件就是一个 self contained 的组件然后组件可以嵌套起来吧。说起来 op 代码里是不是用到了嵌套 template ,感觉也许可以从这里开始?
    ruoxie
        11
    ruoxie  
       2023-08-20 12:41:22 +08:00   ❤️ 1
    siwadiya
        12
    siwadiya  
       2023-10-11 18:41:18 +08:00
    才三百多行,不算多
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3408 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 10:49 · PVG 18:49 · LAX 02:49 · JFK 05:49
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.