The cause of

Recently, I had the honor to participate in the development of our department’s public component library based on VUE3. I would like to record my experience in the actual process of using VUE3 and some problems to pay attention to in the development of public components.

The functionality to be implemented

attribute

The event

The effect after implementation

Theory development process

We use the test Driven Development (TDD) development process as

  1. Document corresponding function points
  2. Write unit tests corresponding to function points
  3. Run test cases to make sure they fail
  4. Write code to implement
  5. Run test cases to ensure success

Actual development process

Knowledge points that need to be mastered before development

  1. New features for Vue3
  2. Test vUE components with jEST, @vue/test-utils: 2.0.0-rC.6
  3. JSX grammar

The structure of the organization

Project organization Structure

Refer to the Vitepress documentation for organizational structure

Document corresponding function points

Mainly according to the UI renderings given by designers, combined with other excellent UI library function points, finally discuss the effect we need to achieve, finally write a document.

Test case writing

Four metrics for test coverage

Line coverage: Is every executable line executed? Function coverage: Is every function called? Branch coverage: Are all branches of each process control implemented? Statement Coverage: Is every statement executed?Copy the code

How do I write test cases

  1. Test-driven development requires test case writing to take precedence over the implementation of unit functionality, which requires thinking about how components should be broken down and what functionality each part will need after being broken down.
  2. Test these imaginary features. However, in the actual development process, it is difficult to achieve a relatively high test coverage by writing test cases before the function is developed, so we have to make a supplement to the test after the function is developed.

Pagination component structure

Here is the organizational structure I gave before writing the feature, which is implemented in JSX files like Jumper, Pager, Pagination, simpler, Sizes, Total, etc

- _tests_
    - pagination.js
- style
    - index.js
    - index.less
    - mixin.less
- const.js
- index.js
- jumper.jsx
- pager.jsx
- pagination.jsx
- simpler.jxs
- sizes.jsx
- total.jsx
Copy the code

Write sample test cases for the Jumper function

The rest of the test was similar

  1. If the ShowQuickJumper tree is true, the jumper function is displayed, and whether or not jumper shows a particular jumper class that the rendered component has. The classes API in @vue/test-utils does this very well.
  2. Jumper is a test for problems such as when the jumper input is not a number, the number is out of bounds, the number is NaN, and so on.
  3. Function test whether you can jump to the corresponding page when you lose focus after typing.

Test coverage achieved

Before the function is done

Failed all the tests

Once the function is done

Less than 70% test coverage, unfortunately forgot screenshots

After the test cases are added

As shown below, the final test coverage is 100%

Summary of tests

Is it necessary to pursue 100% test coverage?

It was really necessary, because in the pursuit of 100% test coverage I went back and looked at every line of code to see if I could actually cover every line of code, and I found some redundant code in the process, For example, I’ve done a bit of processing in jump.jsx to send back pagination.jsx data to make sure it doesn’t generate non-numbers and doesn’t go out of bounds, It was processed again in Pagination. JSX, which made the processing in Pagination redundant and never executed. Because each line, each branch can be executed, this also reduces potential bugs.

Problems encountered during function implementation

Style specification

  1. Need to be compatible with switching style, later may adjust font color, button size, distance between buttons, etc., all colors, some conventional distance, etc., need to be taken from the public file during development. So each little style needs to be considered whether it is necessary, whether it needs to be fetched in the library, and the development process may be slower than writing colors and so on.
  2. Therefore, all possible classes must have the IS attribute, such as IS-disabled or IS-double-jumper
  3. Try not to use element selectors, one is inefficient, one is easy to change the impact of the required

Js specification

  1. Handlexxx. bind(null, aaa) handlexxx. bind(null, aaa)
  2. JSX statements should be formatted and short
Return (<div> {simple.value? <Simple XXX /> : <Pager XXX />} <div>) const renderPage = () => {if (Simple. Value) {return <Simele XXX />; } return <Pager xxx/> } return ( <div> {renderPage()} </div> )Copy the code
  1. The functionality of the functionality is encapsulated as hooks as possible, such as implementing v-Models

Actual use of vuE3’s new features

The setup parameters

The setup function takes two arguments, props and context

Props parameters

Es6 can not be used for deconstruction, because the deconstructed data will lose responsiveness, and toRef or toRefs will be used for processing

The context parameter

This parameter includes attrs, slot, and emit. If an emit is required, it must be declared in the emits configuration

import { definedComponent } from 'vue'; export default definedComponent({ emits: ['update:currentPage'], setup(props, { emit, slot, attrs }) { emit('update:currentPage'); . }})Copy the code

V – model is used

The pageSize and currentPage properties both need v-Model implementation.

Vue2 implements bidirectional binding

Vue 2 implements custom components from one to multiple V-Model two-way data binding methods

Vue3 implements bidirectional binding

Implement binding of individual data

If you only want to implement currentPage bidirectional binding, vue3 binds modelValue by default

<Pagination :modelValue="currentPage" @update:modelValue="(val) => {currentPage = val}" /> / Pagination import {defineComponent} from vue; export default defineComponent({ props: { currentPage: { type: Number, default: 1 } } emits: ['update:currentPage'], setup(props, {emit}) {when Pagaintion component changes, Emit emit(' UPDATE :currentPage', newCurrentPage)}})Copy the code
Implement multiple data binding

The pageSize and currentPage properties both need v-Model implementation

<Pagination v-model:currentPage="currentPage" V-model :pageSize="pageSize" /> defineComponent } from vue; export default defineComponent({ pageSize: { type: Number, default: 10, }, currentPage: { type: Number, default: 1, }, emits: ['update:currentPage', 'update:pageSize'], setup(props, {emit}) {when Pagaintion component changes, Emit (' UPDATE :currentPage', newCurrentPage) emit(' Update :pageSize', newPageSize)}}) emit(' Update :pageSize', newPageSize)}}Copy the code

Comparison of Vue3 and Vue2 V-model

There is little difference in the convenience of binding a single attribute, but binding multiple values can obviously feel the benefits of Vue3. There is no need to use Sync and computed to trigger such a weird writing method, and it is easier and easier to maintain by using V-Model.

Actual use of Reactive ref toRef toRefs

Add reactive to objects using Reactive

It is not used in actual components. Here is an example

import { reactive } from 'vue'; // Setup const data = reactive({count: 0}) console.log(data.count); / / 0Copy the code

Add a response to the value type data using ref

The jumper file is used to add responsiveness to data entered by the user

import { defineComponent, ref, } from 'vue'; export default defineComponent({ setup(props) { const current = ref(''); return () => ( <div> <FInput v-model={current.value}></FInput> </div> ); }});Copy the code

Of course, it is also possible to use ref to add a response to an object, but each time you use it, you need to add a value, such as data.value.count. The data returned by ref is an object with a value attribute, which is essentially a copy of data. It has no relation to the original data. Changing the value returned by ref no longer affects the original data

import { ref } from 'vue'; // Setup const data = ref({count: 0}) console.log(data.value.count); / / 0Copy the code

toRef

  1. ToRef is used to create a new REF for an attribute on a source responsive object, thus maintaining a reactive connection to its source object attribute. It takes two parameters: the source responsive object and the attribute name, and returns a ref data, which is essentially a reference to a value that changes as well as the original value
  2. The actual components are not used, as illustrated below
import { toRef } from 'vue'; export default { props: { totalCount: { type: number, default: 0 } }, setup(props) { const myTotalCount = toRef(props, totalCount); console.log(myTotalCount.value); }}Copy the code

toRefs

ToRefs is used to convert a reactive object into a result object, where each attribute of the result object is a REF pointing to the corresponding attribute of the original object. This is often used in ES6 deconstruction assignments, because when a reactive object is directly deconstructed, the deconstructed data is no longer reactive, and toRefs is a convenient way to solve this problem. Essentially a reference to a value that changes the original value

// Setup const {small, pageSizeOption, totalCount, simple, showSizeChanger, showQuickJumper, showTotal, } = toRefs(props); Console. log(small.value)Copy the code

Because props cannot be deconstructed with ES6, they must be handled with toRefs

Four different ways to add responsiveness to data

Is a reference to the original value

Reactive, toRef, and toRefs handle that the object returned is a reference to the original object. If the object is changed in response, the original object will also change. Ref is a copy of the original object and has no relationship with the original object.

How to value

Reactive objects returned by ref, toRef, and toRefs all require value, but reactive does not

On what data

Reactive is the common object. Ref value type data; ToRef responsive object to retrieve a property; ToRefs deconstruct responsive objects;

Replace mixins with vue3 hooks

If reuse is a function, VUe2 may adopt the method of mixins. One disadvantage of mixins is that when there are multiple mixins, it is not known which mixins the method comes from. Vue3 hooks fix this very well. Let’s write the V-model as a hook

const useModel = ( props, emit, config = { prop: 'modelValue', isEqual: false, }, ) => { const usingProp = config? .prop ?? 'modelValue'; const currentValue = ref(props[usingProp]); const updateCurrentValue = (value) => { if ( value === currentValue.value || (config.isEqual && isEqual(value, currentValue.value)) ) { return; } currentValue.value = value; }; watch(currentValue, () => { emit(`update:${usingProp}`, currentValue.value); }); watch( () => props[usingProp], (val) => { updateCurrentValue(val); }); return [currentValue, updateCurrentValue]; }; // Import useModel from '... // setup const [currentPage, updateCurrentPage] = useModel(props, emit, {prop: 'currentPage',});Copy the code

And you can see, we can clearly see where the currentPage, the updateCurrentPage came from. It is easy and quick to reuse, and V-models like Pager and SIMPLER pages can use this hook

Computed, watch

The feel is similar to that used in Vue2, except that it needs to be introduced when used in Vue3. The change event needs to be triggered when currentPage changes, so you need to use the Watch function

import { watch } from 'vue'; Const [currentPage, updateCurrentPage] = useModel(props, emit, {prop: 'currentPage',}); // Setup const [currentPage, updateCurrentPage] = useModel(props, emit, {prop: 'currentPage',}); watch(currentPage, () => { emit('change', currentPage.value); })Copy the code

Reference documentation

  1. Understand the Test Coverage Report