Because we had long wanted vuE3’s development style, but had a lot of historical projects in the group, and we were constrained by IE support, we decided to introduce composition-API in VUE2 to take advantage of its new features. In the process of use, we have encountered a lot of problems, but also accumulated some experience, so record.
composition-api
Let’s start with composition-API, which exposes vue’s features in the form of functions
It may seem like a lot at once, but you only need to master this bold section to experience the power of composite apis, and the rest can be learned until you really need to use them.
Reactive is used to make objects responsive, like an Observable in VUe2, and refs are used to get responsiveness for individual and underlying data types. Why do we have two apis that get responsive? We’ll talk about that later.
Computed, Watch, provide, inject no doubt do the same thing as vue2.
You must have noticed that the following lifecycle hook functions start with on, and that’s how they register in composite apis. But why are beforeCreate and created missing? This is where setup is performed, and setup opens the door to the world of composite apis. You can think of setup as class constructor, executing our associated logic and registering associated side effects during the creation phase of the Vue component.
So let’s go back to ref and Reactive.
- Reactive takes an object and returns a reactive copy of it.
- Ref is described on the official website as “taking an internal value and returning a responsive and mutable REF object. The ref object has a single property.value” pointing to an internal value.
It sounds tricky, but reactive can create responses for objects. A REF can accept basic data types, such as String, Boolean, and so on, in addition to objects. So why the difference? In VUe3, responsiveness is implemented based on a proxy, and the proxy target must be a complex data type, that is, an object stored in heap memory that is referred to by a pointer. It’s easy to understand, because the underlying data type, each assignment is a new object, so it can’t be proxy at all. So what if we want to get a simple type of response? And that’s where ref comes in.
class RefImpl<T> {
private _value: T
public readonly __v_isRef = true
constructor(private _rawValue: T, public readonly _shallow = false) {
this._value = _shallow ? _rawValue : convert(_rawValue)
}
get value() {
track(toRaw(this), TrackOpTypes.GET, 'value')
return this._value
}
set value(newVal) {
if (hasChanged(toRaw(newVal), this._rawValue)) {
this._rawValue = newVal
this._value = this._shallow ? newVal : convert(newVal)
trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
}
}
}
...
const convert = <T extends unknown>(val: T): T =>
isObject(val) ? reactive(val) : val
Copy the code
Ref creates internal state by hanging values on value, so objects that ref generates are used by value. Overriding get/set listeners and handling objects relies on Reactive. Thus, ref is not only responsive to basic data types, it can also handle objects. Therefore, I think the distinction between REF and Reactive should not only be the distinction between simple and complex objects, but also should be distinguished with programming ideas. We should avoid the idea of declaring all variables on top of reactive as data. Instead, we should combine specific logical functions, such as a Flag that controls gray levels, which should be a REF, and pages, pageSize, total, etc. in pages, which should be reactive. That is, a setup can have declarations of multiple response variables, and they should be tightly tied to logic.
I’m going to start with a pagination feature, and I’m going to compare it with the option and composite APIS. Some examples
<template> <div> <ul class="article-list"> <li v-for="item in articleList" :key="item.id"> <div> <div class="title">{{ item.title }}</div> <div class="content">{{ item.content }}</div> </div> </li> </ul> <el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="currentPage" :page-sizes="pageSizes" :page-size="pageSize" layout="total, sizes, prev, pager, next, jumper" :total="total" > </el-pagination> </div> </template> <script> import { getArticleList } from '@/mock/index'; export default { data() { return { articleList: [], currentPage: 1, pageSizes: [5, 10, 20], pageSize: 5, total: 0, }; }, created() { this.getList(); }, methods: { getList() { const param = { currentPage: this.currentPage, pageSizes: this.pageSizes, pageSize: this.pageSize, }; getArticleList(param).then((res) => { this.articleList = res.data; this.total = res.total; }); }, handleSizeChange(val) { this.pageSize = val; this.getList(); }, handleCurrentChange(val) { this.currentPage = val; this.getList(); ,}}}; </script>Copy the code
Again, you can’t declare data in data and provide methods to fix paging in method, the familiar paging process. This is what it looks like when we implement it with composition-API.
<script> import { defineComponent, reactive, ref, toRefs } from "@vue/composition-api"; import { getArticleList } from "@/mock/index"; export default defineComponent({ setup() { const page = reactive({ currentPage: 1, pageSizes: [5, 10, 20], pageSize: 5, total: 0, }); function handleSizeChange(val) { page.pageSize = val; getList(); } function handleCurrentChange(val) { page.currentPage = val; getList(); } const articleList = ref([]); function getList() { getArticleList(page).then((res) => { articleList.value = res.data; page.total = res.total; }); } getList(); return { ... toRefs(page), articleList, getList, handleSizeChange, handleCurrentChange, }; }}); </script>Copy the code
This is done as a composition-API pagination, and you’ll notice that the data, method, and declaration cycle options are gone, and all the logic is put into setup. With this simple example, we can see that the logic that was scattered across the options is aggregated here. This change is more pronounced in complex scenarios. This is especially true in complex components. In addition, when the logic is completely gathered together, they can be removed, and the removed logic can be reused elsewhere, so the hook is formed.
The paging component in hook form
// hooks/useArticleList.js
import { ref } from "@vue/composition-api";
import { getArticleList } from "@/mock/index"; // Mock Ajax requests
function useArticleList() {
const articleList = ref([]);
function getList(page) {
getArticleList(page).then((res) = > {
articleList.value = res.data;
page.total = res.total;
});
}
return {
articleList,
getList,
};
}
export default useArticleList;
// hooks/usePage.js
import { reactive } from "@vue/composition-api";
function usePage(changeFn) {
const page = reactive({
currentPage: 1.pageSizes: [5.10.20].pageSize: 5.total: 0});function handleSizeChange(val) {
page.pageSize = val;
changeFn(page);
}
function handleCurrentChange(val) {
page.currentPage = val;
changeFn(page);
}
return {
page,
handleSizeChange,
handleCurrentChange,
};
}
export default usePage;
// views/List.vue
import { defineComponent, toRefs } from "@vue/composition-api";
import usePage from "@/hooks/usePage";
import useArticleList from "@/hooks/useArticleList";
export default defineComponent({
setup() {
const { articleList, getList } = useArticleList();
const { page, handleSizeChange, handleCurrentChange } = usePage(getList);
getList(page);
return{... toRefs(page), articleList, getList, handleSizeChange, handleCurrentChange, }; }});Copy the code
We also stepped on a lot of holes in the process of using hooks
- Asynchrony in hook
Because hooks are essentially functions, they are very flexible, especially in logic that involves asynchrony, and a lack of consideration can cause a lot of problems. Hooks can override asynchrony, but must return valid objects when executed in setup and cannot be blocked. We summarized two asynchronous styles, using a simple hook as an example
- There are no external dependencies, just render response variables to deliver
In this case, the response variable can be declared, exposed, and asynchronously modified in the hook
// hooks/useWarehouse.js
import { reactive,toRefs } from '@vue/composition-api';
import { queryWarehouse } from '@/mock/index'; // Query the warehouse request
import getParam from '@/utils/getParam'; // A method to get some parameters
function useWarehouse(admin) {
const warehouse = reactive({ warehouseList: []});const param = { id: admin.id, ... getParam() };const queryList = async() = > {const { list } = await queryWarehouse(param);
list.forEach(goods= >{
// Some logic...
return goods
})
warehouse.warehouseList = list;
};
return { ...toRefs(warehouse), queryList };
}
export default useWarehouse;
// components/Warehouse.vue
<template>
<div>
<button @click="queryList">queryList</button>
<ul>
<li v-for="goods in warehouseList" :key="goods.id">
{{goods}}
</li>
</ul>
</div>
</template>
<script>
import { defineComponent } from '@vue/composition-api';
import useWarehouse from '@/hooks/useWarehouse';
export default defineComponent({
setup() {
// Warehouse keeper
const admin = {
id: '1234'.name: 'Joe'.age: 28.sex: 'men'};const { warehouseList, queryList } = useWarehouse(admin);
return{ warehouseList, queryList }; }});</script>
Copy the code
- Having external dependencies that need to be processed on the use side
The ability to obtain synchronous operations externally can be expanded on the original example by exposing promises externally, adding an update time attribute that needs to be processed
// hooks/useWarehouse.js
function useWarehouse(admin) {
const warehouse = reactive({ warehouseList: []});const param = { id: admin.id, ... getParam() };const queryList = async() = > {const { list, updateTime } = await queryWarehouse(param);
list.forEach(goods= >{
// Some logic...
return goods
})
warehouse.warehouseList = list;
return updateTime;
};
return { ...toRefs(warehouse), queryList };
}
export default useWarehouse;
// components/Warehouse.vue
<template>
<div>.<span>nextUpdateTime:{{nextUpdateTime}}</span>
</div>
</template>
<script>.import dayjs from 'dayjs';
export default defineComponent({
setup(){...// Warehouse keeper
const admin = {
id: '1234'.name: 'Joe'.age: 28.sex: 'men'};const { warehouseList, queryList } = useWarehouse(admin);
const nextUpdateTime = ref(' ');
const interval = 7; // Assume the repository update interval is 7 days
const queryHandler = async() = > {const updateTime = await queryList();
nextUpdateTime.value = dayjs(updateTime).add(interval, 'day');
};
return{ warehouseList, nextUpdateTime, queryHandler }; }});</script>
Copy the code
- The problem of this
Because setup is the beforecreate phase, you can’t get this, although you can get some of the power through the second context parameter of setup. Vuex’s ability to manipulate routes, for example, is limited. The latest router@4 and vuex@4 offer combinatorial apis. However, due to the underlying limitations of VUe2, we cannot use these hooks. We can obtain some manipulation ability by referring to the instance, or we can obtain the object mounted on the component instance by getCurrentInstance. Although the underlying principle of responsivity in composition-API is the same as that in Vue, it is realized by rewriting attributes of Object. Defineproperty, but the specific implementation method is different, so the responsivity in SETUP and vue native are not interoperable. As a result, there is no way to listen for responses even when we get the corresponding instances. If this is required, it can only be used in option configuration.
conclusion
Through vuE3 combination, and hook ability. Our code style has changed a lot. The logic is much more cohesive and pure. Reusability has been improved. The overall maintainability of the project has improved significantly. This is why we used composition-API to introduce new vuE3 features even in vue2 projects.