How to write a list page faster
Tags: vue, javascript, vite
Published: 2022-12-03 13:58:22

## preface

In the background process of development and management, there must be many pages to add, delete, change and query, and most of the logic of these pages are the same, such as the basic functions of obtaining list data, paging, and filtering. The difference is the data items presented. There are also some operation buttons.

![20221203192923](https://raw.githubusercontent.com/QC2168/note-img/main/20221203192923.png)

When there are only 1 or 2 pages at the beginning, most developers may directly copy another copy of the previous page code. With the progress of the project, the number of similar pages may become more and more, which directly leads to higher and higher coupling of project code.

This is one of the main reasons why some reusable functions or components in the project should be separated

Next, we encapsulate a general `useList`, which is adapted to most of the list pages for addition, deletion, modification and query, so that you can complete tasks faster and more efficiently and leave on time ~

![20221203195440](https://raw.githubusercontent.com/QC2168/note-img/main/20221203195440.png)

## Antecedent knowledge

- Vue
- [Vue Composition Api](https://cn.vuejs.org/guide/introduction.html#api-styles)

## Coding

We need to extract some common parameters and functions and encapsulate them into a common `hook`. It is easier to reuse the same functions in other pages later.

### Define the paging data necessary for a list page

```typescript
export default function useList() {
  // isLoading
  const loading = ref(false);
  // current page number
  const curPage = ref(1);
  // list total
  const total = ref(0);
  // current page size
  const pageSize = ref(10);
}
```

### How to get List Data

Think about it. Let the `useList` function receive a `listRequestFn` parameter for requesting data in the list.

Define a `list` variable to store the data content returned from the network request. Since the list data type cannot be directly determined internally, the list data type can be provided externally in a generic way.

```typescript
export default function useList<ItemType extends Object>(
  listRequestFn: Function
) {
  // ignore other code
  const list = ref<ItemType[]>([]);
}
```

Create a `loadData` function in `useList` to call the data acquisition function. This function receives a parameter to obtain the data of the specified number of pages (optional, default to the value of `curPage`).

- Execution process

1. Set the loading status
2. Call the external incoming function to assign the acquired data to `list` and `total`
3. Close the loading state

> The async/await syntax is used here. Assume that a request error or a deconstruction error occurs, the catch code block will be followed, and then the loading state will be closed

> Note that whether the number and type of parameters received by the incoming listRequestFn function correspond normally
 
> Please adjust according to the actual situation

```typescript
export default function useList<ItemType extends Object>(
  listRequestFn: Function
) {
  // ignore other code
  // data list
  const list = ref<ItemType[]>([]);
  // filter data
  // get data
  const loadData = async (page = curPage.value) => {
    // Open Load Status
    loading.value = true;
    try {
      const {
        data,
        meta: { total: count },
      } = await listRequestFn(pageSize.value, page);
      list.value = data;
      total.value = count;
    } catch (error) {
      console.log("request error", "error");
    } finally {
      // Close the loading state
      loading.value = false;
    }
  };
}
```

Don't forget, there is also paging to be handled

Use the `watch` function to listen for data. When the values of `curPage` and `pageSize` change, call the `loadData` function to obtain new data.

```typescript
export default function useList<ItemType extends Object>(
  listRequestFn: Function
) {
  // ignore other code
  // watch page data
  watch([curPage, pageSize], () => {
    loadData(curPage.value);
  });
}
```

Now the basic list data acquisition is realized

### Implement Data Filters
In a huge data list, data filtering is an essential function

Generally, I define the filter criteria field in a `ref`, and throw the `ref` to the request function when requesting.

In the useList function, the second parameter receives a `filterOption` object, which corresponds to the filter condition field in the list.

Adjust the `loadData` function and pass in the `filterOption` object to the request function

>Note whether the number and type of parameters received by the incoming listRequestFn function correspond normally

> Please adjust according to the actual situation

```typescript
export default function useList<
  ItemType extends Object,
  FilterOption extends Object
>(listRequestFn: Function, filterOption: Ref<Object>) {
  const loadData = async (page = curPage.value) => {
    loading.value = true;
    try {
      const {
        data,
        meta: { total: count },
      } = await listRequestFn(pageSize.value, page, filterOption.value);
      list.value = data;
      total.value = count;
    } catch (error) {
      console.log("request error", "error");
    } finally {
      // Close the loading state
      loading.value = false;
    }
  };
}
```

> Note that the filterOption parameter type requires a ref type, otherwise the responders will be lost and will not work properly

### Clear Filter Fields
In the page, there is a reset button to clear the filter conditions. This repeated action can be handled by the reset function.

Use Reflect to set all values to `undefined` and request data again.

> What is Reflect? Take a look at this article [Reflect Mapping Object](https://juejin.cn/post/7061495978209542175)

```typescript
export default function useList<
  ItemType extends Object,
  FilterOption extends Object
>(listRequestFn: Function, filterOption: Ref<Object>) {
  const reset = () => {
    if (!filterOption.value) return;
    const keys = Reflect.ownKeys(filterOption.value);
    filterOption.value = {} as FilterOption;
    keys.forEach((key) => {
      Reflect.set(filterOption.value!, key, undefined);
    });
    loadData();
  };
}
```

## export function
In addition to viewing data, some interfaces also need to have the function of exporting data (such as exporting csv and excel files). We also write the export function to `useList`

In general, the export function calls the export `Api` provided by the backend to obtain a file download address. Similar to the `loadData` function, the `exportRequestFn` function is obtained externally to call the 'Api`

In the function, add an `exportFile` function to call it.

```typescript
export default function useList<
  ItemType extends Object,
  FilterOption extends Object
>(
  listRequestFn: Function,
  filterOption: Ref<Object>,
  exportRequestFn?: Function
) {
  // ignore other code
  const exportFile = async () => {
    if (!exportRequestFn) {
      throw new Error("Please provide exportRequestFn function");
    }
    if (typeof exportRequestFn !== "function") {
      throw new Error("exportRequestFn must be a function);
    }
    try {
      const {
        data: { link },
      } = await exportRequestFn(filterOption.value);
      window.open(link);
    } catch (error) {
      console.log("export error", "error");
    }
  };
}
```

> Note that whether the number and type of parameters received by the imported exportRequestFn function correspond normally
> Please adjust according to the actual situation

## Optimization

Now, the entire `useList` has met the needs of the page, with the functions of obtaining data, filtering data, exporting data, and paging

There are also some details. The `catch` code fragments in the `try.. Catch` in all the above codes have not been processed at all, just a simple `console. log`

### Provide hook

In `useList`, add an Options object parameter, which is used to execute the specified hook function and output message content when the function succeeds or fails.

#### Define Options Type

```typescript
export interface MessageType {
  GET_DATA_IF_FAILED?: string;
  GET_DATA_IF_SUCCEED?: string;
  EXPORT_DATA_IF_FAILED?: string;
  EXPORT_DATA_IF_SUCCEED?: string;
}
export interface OptionsType {
  requestError?: () => void;
  requestSuccess?: () => void;
  message: MessageType;
}

export default function useList<
  ItemType extends Object,
  FilterOption extends Object
>(
  listRequestFn: Function,
  filterOption: Ref<Object>,
  exportRequestFn?: Function,
  options? :OptionsType
) {
  // ...
}
```

#### Set `Options` default value

```typescript
const DEFAULT_MESSAGE = {
  GET_DATA_IF_FAILED: "Failed to get list data",
  EXPORT_DATA_IF_FAILED: "Failed to export data",
};

const DEFAULT_OPTIONS: OptionsType = {
  message: DEFAULT_MESSAGE,
};

export default function useList<
  ItemType extends Object,
  FilterOption extends Object
>(
  listRequestFn: Function,
  filterOption: Ref<Object>,
  exportRequestFn?: Function,
  options = DEFAULT_OPTIONS
) {
  // ...
}
```

> It is recommended to set the default failure message when there is no transfer hook


#### Optimize `loadData`, `exportFile` functions

**Encapsulating message method based on element ui**

```typescript
import { ElMessage, MessageOptions } from "element-plus";

export function message(message: string, option?: MessageOptions) {
  ElMessage({ message, ...option });
}
export function warningMessage(message: string, option?: MessageOptions) {
  ElMessage({ message, ...option, type: "warning" });
}
export function errorMessage(message: string, option?: MessageOptions) {
  ElMessage({ message, ...option, type: "error" });
}
export function infoMessage(message: string, option?: MessageOptions) {
  ElMessage({ message, ...option, type: "info" });
}
```

**loadData function**

```typescript
const loadData = async (page = curPage.value) => {
  loading.value = true;
  try {
    const {
      data,
      meta: { total: count },
    } = await listRequestFn(pageSize.value, page, filterOption.value);
    list.value = data;
    total.value = count;
    // execute succeed function
    options?.message?.GET_DATA_IF_SUCCEED &&
      message(options.message.GET_DATA_IF_SUCCEED);
    options?.requestSuccess?.();
  } catch (error) {
    options?.message?.GET_DATA_IF_FAILED &&
      errorMessage(options.message.GET_DATA_IF_FAILED);
    // execute failed function
    options?.requestError?.();
  } finally {
    loading.value = false;
  }
};
```

**exportFile function**

```typescript
const exportFile = async () => {
  if (!exportRequestFn) {
    throw new Error("Please provide exportRequestFn function");
  }
  if (typeof exportRequestFn !== "function") {
    throw new Error("ExportRequestFn must be a function");
  }
  try {
    const {
      data: { link },
    } = await exportRequestFn(filterOption.value);
    window.open(link);
    // show message
    options?.message?.EXPORT_DATA_IF_SUCCEED &&
      message(options.message.EXPORT_DATA_IF_SUCCEED);
    // execute succeed function
    options?.exportSuccess?.();
  } catch (error) {
    // show message
    options?.message?.EXPORT_DATA_IF_FAILED &&
      errorMessage(options.message.EXPORT_DATA_IF_FAILED);
    // execute failed function
    options?.exportError?.();
  }
};
```

## Usage of useList

```vue
<template>
  <el-collapse class="mb-6">
    <el-collapse-item title="筛选条件" name="1">
      <el-form label-position="left" label-width="90px" :model="filterOption">
        <el-row :gutter="20">
          <el-col :xs="24" :sm="12" :md="8" :lg="8" :xl="8">
            <el-form-item label="用户名">
              <el-input
                v-model="filterOption.name"
                placeholder="筛选指定签名名称"
              />
            </el-form-item>
          </el-col>
          <el-col :xs="24" :sm="12" :md="8" :lg="8" :xl="8">
            <el-form-item label="注册时间">
              <el-date-picker
                v-model="filterOption.timeRange"
                type="daterange"
                unlink-panels
                range-separator="到"
                start-placeholder="开始时间"
                end-placeholder="结束时间"
                format="YYYY-MM-DD HH:mm"
                value-format="YYYY-MM-DD HH:mm"
              />
            </el-form-item>
          </el-col>
          <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
            <el-row class="flex mt-4">
              <el-button type="primary" @click="filter">筛选</el-button>
              <el-button type="primary" @click="reset">重置</el-button>
            </el-row>
          </el-col>
        </el-row>
      </el-form>
    </el-collapse-item>
  </el-collapse>
  <el-table v-loading="loading" :data="list" border style="width: 100%">
    <el-table-column label="用户名" min-width="110px">
      <template #default="scope">
        {{ scope.row.name }}
      </template>
    </el-table-column>
    <el-table-column label="手机号码" min-width="130px">
      <template #default="scope">
        {{ scope.row.mobile || "未绑定手机号码" }}
      </template>
    </el-table-column>
    <el-table-column label="邮箱地址" min-width="130px">
      <template #default="scope">
        {{ scope.row.email || "未绑定邮箱地址" }}
      </template>
    </el-table-column>
    <el-table-column prop="createAt" label="注册时间" min-width="220px" />
    <el-table-column width="200px" fixed="right" label="操作">
      <template #default="scope">
        <el-button type="primary" link @click="detail(scope.row)"
          >详情</el-button
        >
      </template>
    </el-table-column>
  </el-table>
  <div v-if="total > 0" class="flex justify-end mt-4">
    <el-pagination
      v-model:current-page="curPage"
      v-model:page-size="pageSize"
      background
      layout="sizes, prev, pager, next"
      :total="total"
      :page-sizes="[10, 30, 50]"
    />
  </div>
</template>
<script setup lang="ts">
import { UserInfoApi } from "@/network/api/User";
import useList from "@/lib/hooks/useList/index";
const filterOption = ref<UserInfoApi.FilterOptionType>({});
const {
  list,
  loading,
  reset,
  filter,
  curPage,
  pageSize,
  reload,
  total,
  loadData,
} = useList<UserInfoApi.UserInfo[], UserInfoApi.FilterOptionType>(
  UserInfoApi.list,
  filterOption
);
</script>
```

The complete code of the `useList` in this article is shown in 
 https://github.com/QC2168/snippets/tree/main/useList

💡  If you have better suggestions for this `hook`, welcome to`pr `or leave a message in the comment area

In addition, in order to save time in searching for encapsulated code fragments and improve work efficiency in daily development 🐟 Time++), this [warehouse](https://github.com/QC2168/snippets)
It also stores some code fragments encapsulated by the third party ✨， Convenient for everyone 😄😄 (Continuously updating~~)