preface

It has been two weeks since the last article was published. I got 1000+ likes and 30,000 + views, which I never expected. It is really gratifying that my article can have such a high level of attention!

But instead, the pressure to write articles has increased. An article is always repeatedly revised, always worried about their own cognitive level and technical level is not enough, even lead to some places will mislead readers. So it takes more time to write.

I also wonder if there is anything wrong with my writing style, if it is too wordy, what is not clear… If there is a bad place can comment pointed out, accept criticism, criticism is also a power of progress!

General operation, first click “like” after watching oh! Your praise is one of the motivations for my creation!

Full series overview


The problem

In this section I will discuss some of the techniques and principles of vUE development from five aspects. If you haven’t watched the previous article, you can go to 16 areas of front-end engineering tips I for more information.

This section mainly focuses on the following issues:

  • How to write native components, and the thinking and principles of component writing?
  • How do components communicate? What are the ways?
  • How to use VUEX and its application scenarios and principles
  • How to use filters and write your own filters
  • How to use Jest to test your code? Comparison of TDD and BDD

practice

Before you practice: I hope you have the following preparation, or knowledge.

  • To understandnpm/yarn/git/sass/pug/vue/vuex/vue-router/axios/mock/ssr/jestThe use and principle of.
  • Of course, the above knowledge does not matter ha ha ha, the article will mention the general usage and role.

How do I write native components

Component writing Principles

Vue writes components in two ways, a single-file component and a functional component. Component introduction and creation can also be divided into dynamic components and asynchronous components.

The dynamic component keep-alive makes it cached. Asynchronous components work the same way as asynchronous routes, using import() to load asynchronous components on demand.

The vUE single-file component is the most common form:

<template lang="pug">

.demo

h1 hello

</template>

<script>

export default {

name: 'demo'.

data() {

return {}

}

}

</script>

<style lang="scss" scoped>

.demo {

h1 { color: #f00; }

}

</style>

Copy the code

The template here can also be written using the render function

Vue.component('demo', {

render: function (createElement) {

return createElement(

'h1'.

'hello'.

// ...

)

}

})

Copy the code

We can see that the Render function template gives us a programming feel. The template can also be programmed, in VUE we can easily imagine that there are two ways to write, one is template, one is JS.

For example, for routing, we can use either :to=”” or $router.push, which is probably why VUE is so nice to use.

What is a functional component?

Functional, which means it’s stateless (no responsive data) and has no instances (no this context)

  • Single file form2.5.0 +
<template functional>

</template>

Copy the code
  • Render function form
Vue.component('my-component', {

functional: true.

render function (createElement, context) {

return createElement('div')

}

}

Copy the code

Why use functional components?

The most important reason is the low overhead of functional components, which is good for performance. Writing functional components is an optimization solution when reactive and this are not needed.

The component is written and needs to be registered to use it

Two ways to register components

Component registration is divided into two types, one is global registration, one is local registration

Vue.component(‘s-button’, {/*… */}), relatively simple not to elaborate

Global registration as mentioned in the previous section, registering in mian.js before new Vue, there is also an automatic global registration method called require.text

import Vue from 'vue'

import upperFirst from 'lodash/upperFirst'

import camelCase from 'lodash/camelCase'



const requireComponent = require.context(

'./components'.

// Whether to query the subdirectory

false.

/Base[A-Z]\w+\.(vue|js)$/

)

requireComponent.keys().forEach(fileName= > {

// Get the component configuration

const componentConfig = requireComponent(fileName)

const componentName = upperFirst(

camelCase(

// Get a file name independent of directory depth

fileName

.split('/')

.pop()

.replace(/\.\w+$/.' ')

)

)

// Globally register components

Vue.component(

componentName,

componentConfig.default || componentConfig

)

})

Copy the code

The basic principle is the same as global registration, which is to change the component name in components, appButton, to appButton as the registered component name. The original need to manually copy, between the use of keys method batch registration.

Practice developing a Button component

Now let’s take a look at some of the key points of component development, using writing a simple native Button component as an example. Before writing, there are four core points to capture:

  • Which label to use as the component body,buttonordivThe label
  • How to control by attributesbuttonComponent colorscolor, shape,type, size,size
  • How to deal withbuttonComponent click events
  • How to scalebuttonComponent contents

These considerations also need to be considered in other native component development and higher-level component packaging

Let’s start with the first question. The first consideration for most native components is whether the main tag should be modeled with the native tag or

tag.

Why not consider , since

Here are the pros and cons of these two ways of writing

Use nativebuttonAdvantages of labels:

  1. Better label semantics
  2. Native tags come with native tagsbuff, some built-in keyboard event behavior, etc

Why better semantics? One could argue that you can use roles to enhance the semanticalization of divs. Yes, but there may be a problem — some crawlers do not determine the meaning of this tag based on role.

Another point, is a better read for developers than

.

usedivAdvantages of simulation:

  1. Don’t carebuttonNative styles bring some interference, write less to override nativecssCode, cleaner and pure.
  2. All usedivYou don’t need to go looking for native tags and delve into some of the weird compatibility issues associated with native tags.
  3. More malleable, which means more extensible and compatible. This is also what most components choose to usedivCause as the body of the component.

Divs seem to be all right except for the semantics, but you can use either one. As long as the code is written in a robust and adaptable way, there is basically no big problem.

Here we use native as the main tag and s-xx as the class prefix

The reason for using prefixes is that in some cases, such as with third-party components, there may be conflicts between the classes of multiple components. Prefixes are used to act as a namespace for component CSS without interference between different components.

<template lang="pug">

button.s-button(:class="xxx" :style="xxx" )

slot

</template>

Copy the code

Then, let’s look at the second question:

How to control the style of a button according to the attributes is actually very simple, the basic principle is:

  1. Get the properties passed by the parent component using props.

  2. Class =”{XXX: true, XXX: ‘s-button–‘ + size}” class=”{XXX: true, XXX: ‘s-button–‘ + size}”

<template lang="pug">

button.s-button(:class="" :style="" )

slot

</template>

<script>

export default {

name: 's-button'

data: return {}

props: {

theme: {},

size: {},

type: {}

}

}

</script>



Copy the code

How are events used and how are components extended

The principle of extension components is to usepropsControls component properties, used in templatesv-if/v-showAdd component power, such as adding internal ICON, use:style``classControl component styles.Also note that we have to control the nativetypeType, the native default issubmit, here we default tobutton

<template lang="pug">

button.s-button(:class="" :style="" :type="nativeType")

slot

s-icon(v-if="icon && $slots.icon" name="loading")

</template>

<script>

export default {

name: 's-button'

data: return {}

props: {

nativeType: {

type: String.

default: 'button'

},

theme: {},

size: {},

type: {}

}

}

</script>

Copy the code

Control events directly using @click=”” + emit

<template lang="pug">

button.s-button(@click="handleClick")

</template>

<script>

export default {

methods: {

handleClick (evt) {

this.$emit('click', evt)

}

}

}

</script>

Copy the code

Finally, to sum up:

Use render() to write components in a template file.

Generally writing components requires consideration: 1. What tags to use 2. How to control the performance of various attributes 3. How to enhance component extensibility 4. How to handle component events

Reactive this is not required; use the functional component to optimize performance.

Base components are usually registered globally and business components are usually registered locally

Use keys() to traverse the file for automatic batch global registration

Use import() to load components asynchronously to reduce the overhead of the first load, and use keep-alive + IS to cache components to reduce the overhead of the second load

How to use Vuex and its applications

Origin and Principle

We know that components communicate in the following ways:

1. The parent component passespropsPasses to child components without elaboration

2. Subcomponents passemitThe event passes data to the parent, which passesonEavesdropping, that’s a typicalSubscription-publishmodel

@ is short for V-on:

<template lang="pug">

<! -- Subcomponent -->

div.component-1

<template>

export default {

mounted() {

this.$emit('eventName', params)

}

}

</script>

Copy the code
<! -- Parent component -->

<template lang="pug">

Component-1(@eventName="handleEventName")

<template>

<script>

export default {

methods: {

handleEventName (params) {

console.log(params)

}

}

}

</script>

Copy the code

3. Centralized communication events, mainly used for simple non-parent component communication

The principle is simple: add an event relay station “BUS” on the basis of EMIT and ON. I think it’s more like a hub in real life.

The common implementation principle is that the “bus” is an instance of vUE, which can call the emit,off, and on methods.

var eventHub = new Vue()



/ / release

eventHub.$emit('add', params)

// Subscribe/response

eventHub.$on('add', params)

/ / destroy

eventHub.$off('add', params)

Copy the code

But in slightly more complicated cases, this approach is too complicated. It’s better to use Vuex.

In a sense, I think vuex is more than just an evolutionary version of it.

  1. usestoreAs aState managementWarehouse, and introducedstateThe concept of
  2. It’s a completely different model,busThe model feels like a telephone relay station

Vuex’s model is more like a centralized code repository.

Similar to Git, it cannot directly modify the code, requiring the participant to commit. After the commit, the repository is modified, and the repository is updated. The participant fetch code updates its code. The difference is that the repository needs to be merged, whereas VUEX directly overwrites the previous state.


Management vuex

The principle of

A “store” is basically a container that contains most of the states in your app. Vuex differs from a purely global object in two ways

  • responsiveWhen a property is changed, the state in the trigger component changes, triggeringdom)
  • You can’t change the state directly(The only way is to submitmutation)

$store. State: $store. Getters: $store. State: $store. If you want to change the state, commit a mutation

But what if I want to submit a bunch of actions? You can define an action and then execute it using $store.dispatch

Using an action not only saves a lot of code, but the key is that you can use asynchronous correlation functions in the action and return a promise directly

Why not write async directly into mutation? Since mutation must be a synchronization, it is the only one that can change state, and once committed, a mutation must be given an unambiguous result. Otherwise, state changes will be blocked.

The following are common uses of vuex

The new Store

Create a new Store and separate the other functions into files

import Vue from 'vue'

import Vuex from 'vuex'

import state from './states'

import getters from './getters'

import mutations from './mutations'

import actions from './actions'

import user from './modules/user'

Vue.use(Vuex)

export default new Vuex.Store({

// In non-production environments, use strict mode

strict: process.env.NODE_ENV ! = ='production'.

state,

getters,

mutations,

actions,

modules: {

user

}

})

Copy the code

Operation state two ways

  1. To obtain state
console.log(store.state.count)

Copy the code
  1. Change state
store.commit('xxx')

Copy the code

Management State States

Single state tree, which also means that each application will contain only one store instance. A single state tree allows us to directly locate any particular state fragment and easily take a snapshot of the entire current application state during debugging.

/ / document states

export default {

count: 0

}

Copy the code

Each time the property in state changes, the other component refetches the calculated property and triggers an update to the associated DOM

const Counter = {

template: '<div>{{count}}<div>',

computed: {

count() {

return store.state.count

}

}

}

Copy the code

Manage the value getters

Getters is equivalent to the computed property of store. It’s code reuse without having to filter through the calculated properties every time. We manage it in the getters file

export default {

count: (state) = > Math.floor(state.count)

}

Copy the code

Management of mutations

The only way to change the state in Vuex’s store is to commit mutation. Mutations in Vuex are very similar to events: each mutation has a string event type (type) and a callback function (handler). This callback function is where we actually make the state change

Use types uppercase for debugging, export const ROUTE_ADD = ‘ROUTE_ADD’ in mutationTypes file

Then it’s managed in the Mutations file

import * as MutationTypes from './mutationTypes'

export default {

[MutationTypes.ADDONE]: function(state) {

state.count = state.count + 1

},

/ /...

}

Copy the code
this.$store.commit(MutationTypes.ADDONE)

Copy the code

Management actions

Similar to mutations, actions correspond to Dispatch, but action can use asynchronous functions, which has a higher level of encapsulation.

/ / to simplify

actions: {

increment ({ commit }) {

setTimeout((a)= > {

commit(MutationTypes.ADDONE)

}, 1000)

}

}

/ / triggers

store.dispatch('increment')

Copy the code

The above usage can be used in the form of load, import can also use mapXxxx for batch import, here is not discussed in detail, interested can check the official website.

Manage status by module

There are too many states, and module management is a good way to organize code.

import count from './modules/count'

export default new Vuex.Store({

modules: {

count

}

})

Copy the code

Each module can have independent related attributes

import * as ActionTypes from '. /.. /actionTypes'

export default {

state: {

count: 0

},

mutations: {

ADD_ONE: function(state) {

state.count = state.count + 1

}

},

actions: {

[ActionTypes.INIT_INTENT_ORDER]: function({ commit }) {

commit('ADD_ONE')

}

},

getters: {

pageBackToLoan: (state) = > Math.floor(state.count)

}

}

Copy the code

Application scenarios

Vuex has several applications, one for component communication and state sharing, one for data caching, and one for request reduction. The root nodes of these scenarios are for caching and sharing.

Component communication

First, state management is unified in the warehouse, which makes component communication possible.

When a component changes its state via commit, other components can obtain the latest state via store. State, which means that the updated value is transferred to other components through store. One-to-one communication is not only realized. It also implements one-to-many communication.

State sharing

One scenario we often use is permission management.

When you enter the page for the first time, you need to obtain all the permissions, and then distribute them to each page to control the permissions of each route and button.

/ * *

* Determine whether the user has permission to access the page

* /


function hasPermission(routeItem, menus) {

return menus.some(menuItem= > {

return menuItem.name === routeItem.name

})

}



/ * *

* Recursively filters asynchronous routing tables to return routing tables that match user role permissions

* @param {*} routes Routing table

* @param {*} Menus provide menu information

* /


function filterRoutes(routes, menus) {

return routes.filter(routeItem= > {

if (routeItem.hidden) {

return true

} else if (hasPermission(routeItem, menus)) {

const menuItem = menus.find(item= > item.name === routeItem.name)

if (menuItem && menuItem.children && menuItem.children.length > 0) {

routeItem.children = filterRoutes(routeItem.children, menuItem.children)

if(! routeItem.children.length)return false

}

return true

} else {

return false

}

})

}

const permission = {

state: {

routers: constantRouterMap,

addRouters: [].

roles: [].

user_name: ' '.

avatar_url: ' '.

onlyEditor: false.

menus: null.

personal: true.

teamList: []

},

mutations: {}

}

export default permission

Copy the code

Permissions can also be changed, and the changed permissions are distributed directly to other page components. This scenario would have been more complex without vuex.

Data cache

Store is a repository that exists from the moment it is created, and state is cleared only if the page vuex.store instance is destroyed. Specific performance is to refresh the page.

This data cache applies to scenarios where the data is cached after the page is loaded and the requested data is refreshed. In general Hybrid, there is no button to refresh the page, so using VUEX to cache data can be used for most scenarios.

export default {

state: {

// Cache the information needed to change the mobile phone number

changePhoneInfo: {

nameUser: ' '.

email: ' '.

phone: ' '

},

}

}

Copy the code

If persistent caching is required, it is better to combine it with a browser or APP cache.

export default {

// After successful login, vuex writes the token to the APP cache for persistence

[ActionTypes.LOGIN_SUCCESS]: function(store, token) {

store.commit(MutationTypes.SET_TOKEN, token)

setStorage('token', token)

router.replace({ name: 'Home'.params: { source: 'login'}})

}

}

Copy the code

Reduced requests (data caching variant)

When writing the back-end management platform, there will often be a list selection component, in which the data from the server side of the data. If we store this list and use it again next time, just pull it out of the store, then we don’t have to ask for the data again. That’s one less request.

How to use filters and write your own filters

The principle of

Suppose I now have a requirement to convert gender 0, 1, and 2 into male, female, and uncertain characters respectively. Multiple places on the page need to be used.

<template lang="pug">

.user-info

.gender

Label (for=" gender ") Gender

span {{gender}}

</template>

Copy the code

We know of four ways to accomplish this requirement:

  1. Using computed methods
  2. Use methods
  3. Use the utils utility class
  4. The use of filters

Which approach should you choose?

I discuss this problem from the following three aspects

1. Implementability

  • usecomputedAchieve success, we knowcomputedIt’s not a function that can’t be taken as a parameter, so here’s a trick,returnA function accepts arguments that are passed to it
  // ...

computed: {

convertIdToName() {

return function(value) {

const item = list.find(function(item) {

return item.id === value

})

return item.name

}

}

}

Copy the code

  • usemethodsSuccess. You can just pass the parameters, a normal way of writing it.

Note that methods, computed, and data do not have the same name before each other

  // ...

methods: {

convertIdToName(value) {

const item = list.find(function(item) {

return item.id === value

})

return item.name

}

}

Copy the code
  • useutilsmethodsIt can be done almost as well
  • usefilterIt’s also realizable. There’s something that can bemethods,computedOh, the same name
  filters: {

console.log(this.render)

convertIdToName(value) {

const item = list.find(function(item) {

return item.id === value

})

return item.name

}

},

Copy the code

In general, all of them can fulfill this requirement

2. The limitations

  • computedmethodsdataThe three components do not have the same name, and they cannot be used by other components unless passedmixins
  • filtersutilsDon’t have access tothis, that is, in response insulation. But by defining the globalfilters, can be used in other places, in addition to directly load a third partyfilterandutils

3. Summarize and compare

Filters and utils belong together. They are both free from this and excluded from this. On the contrary, methods and computed are closely associated with this, but they are not free.

example

Write a simple thousandth filter

export const thousandBitSeparator = (value) => {

return value && (value

.toString().indexOf('.') ! = = 1? value.toString().replace(/(\d)(? =(\d{3})+\.) /g, function($0, $1) {

return $1 + ',';

}) : value.toString().replace(/(\d)(? =(\d{3})+$)/g, function($0, $1) {

return $1 + ',';

}));

}

Copy the code

Use vue-filter plug-in

Two new plug-in

Introduced vue-filter:https://www.npmjs.com/package/vue-filter used use

Introduced vue2-filters:https://www.npmjs.com/package/vue-filters use mixins

I usually use the second filter if I need to, and most of the time I write my own little filter

After the custom filter, direct global automatic registration, other places can be used

Register global filters

Iterate through the filter attribute values and register them all at once

for (const key in filters) {

Vue.filter(key, filters[key])

}

Copy the code

How to use Jest to test your code

The principle of

Let’s think about what you need to test your JS code

  1. Browser Runtime environment
  2. Assertions library

What if you’re testing vue code? You need to add another VUE test container

Jest + Vue

Install dependencies

{

"@vue/cli-plugin-unit-jest": "^ 4.0.5".

"@vue/test-utils": "29 1.0.0 - beta.".

"jest": "^ 24.9.0".

// ...

}

Copy the code

Project configuration

// For a detailed explanation regarding each configuration property, visit:

// https://jestjs.io/docs/en/configuration.html



module.exports = {

preset: '@vue/cli-plugin-unit-jest',

automock: false,

"/private/var/folders/10/bb2hb93j34999j9cqr587ts80000gn/T/jest_dx",

clearMocks: true,

// collectCoverageFrom: null,

coverageDirectory: 'tests/coverage'

/ /...

}

Copy the code

Unit testing

Test the Utils utility class

Test a gender name conversion tool we wrote earlier

import { convertIdToName } from './convertIdToName'

describe('Test the convertIdToName method', () = > {

const list = [

{ id: 0.name: 'male' },

{ id: 1.name: 'woman' },

{ id: 2.name: 'unknown' }

]

it('Test normal input', () = > {

const usage = list

usage.forEach((item) = > {

expect(convertIdToName(item.id, list)).toBe(item.name)

})

})

it('Test abnormal input', () = > {

const usage = ['a'.null.undefined.NaN]

usage.forEach((item) = > {

expect(convertIdToName(item, list)).toBe(' ')

})

})

})

Copy the code

Such a test, we found that the tool we wrote before there are so many vulnerabilitiesTest normal input all passed, abnormal input failed, according to the test case to improve our code

export const convertIdToName = (value, list) => {

if (value ! == 0 && value ! == 1 && value ! == 2) return ''

const item = list.find(function(item) {

return item.id === value

})

return item.name

}

Copy the code

I passed all the tests now


Test the Components single file component

Let’s test our simplest hello World

<template lang="pug">

.hello

h1 {{ msg }}

</template>

<script>

export default {

props: {

msg: String

}

}

</script>



Copy the code
import { shallowMount } from '@vue/test-utils'

import HelloWorld from '@/components/HelloWorld.vue'



describe('HelloWorld.vue', () = > {

it('renders props.msg when passed', () = > {

const msg = 'new message'

const wrapper = shallowMount(HelloWorld, {

propsData: { msg }

})

expect(wrapper.text()).toMatch(msg)

})

})

Copy the code

Test API request

There are several common ways to write asynchronous tests

  • asyncawait
  • done()

Simple asynchronous test, testing a simple login request

export const login = (data) = > post('/user/login', data)

Copy the code

The test code

import { login } from '@/api/index'

describe('login api', () = > {

const response = {

code: '1000'.

data: {}

}

const errorResponse = {

code: '5000'.

data: {},

message: 'Wrong username or password'

}

it('Test normal login'.async() = > {

const params = {

user: 'admin'.

password: '123456'

}

expect(await login(params)).toEqual(response)

})

it('Test abnormal Login'.async() = > {

const params = {

user: 'admin'.

password: '123123'

}

expect(await login(params)).toEqual(errorResponse)

})

})

Copy the code

Functional module test

Components, apis, tools, all these bits and pieces are tested, and these are more generic, less business relevant code, and they don’t change much, so it’s enough to test up to this point, and it’s 20% of the test effort and a lot of the code.

Why do I say only 20%? Because it’s all logic that doesn’t change much, it’s a one-time thing. In the long run, it takes up very little work.

But in some cases the business must be measured, that is, the functional module integration test must be performed.

Often regressive business, the kind of iteration on the original system is larger, to avoid the old code after the change all kinds of new problems. This kind of frequent regression testing, using BDD + integration testing, is much easier than constantly fixing bugs.

A snapshot of the test

Like versions, build a version after each test to compare the differences with the previous version. This is a very small-grained test that can test every symbol.

For example, I used it to test a configuration file change

This is our configuration file

export const api = {

develop: 'http://xxxx:8080',

mock: 'http://xxxx',

feature: 'http://xxxx',

test: 'http://xxxx',

production: 'http://xxxx'

}

export default api[process.env.NODE_ENV || 'dev']

Copy the code

Using Snapshot Tests

import { api } from './config'



Describe (' config file test ', () => {

It (' test configuration file changes ', () => {

expect(api).toMatchSnapshot({

develop: 'http://xxxx:8080',

mock: 'http://xxxx',

feature: 'http://xxxx',

test: 'http://xxxx',

production: 'http://xxxx'

})

})

})



Copy the code

After the first test using the snapshot, code is written to the snapshot through the test

Modify the configuration to test again, failed the testWhat if you want to change the configuration? Just change the use case at the same time. The snapshot will be written again to snapshot build version 2 so that configuration changes are warranted

TDD and BDD

Test-driven development and behavior-driven development have been discussed a lot recently, but there are four in general

  1. Don’t write tests. The upside is saving time, the downside isbugMany, low code quality.
  2. Write the tests and test cases first, then the code, which is test-driven development. The advantage is that the code is more robust and takes more factors into account. Fixed modules have high robustness and fewer bugs.
  3. Write code first, then write tests that mimic the user’s behavior. The advantage is that the thinking is clear, if the test case is comprehensive enough, basically online basically no big problem, regression testing is also easy to do.
  4. Write tests and use cases before you write code, and user behavior tests after you write code. This code is too high quality, but the downside is that it takes time.

So which one are you? Anyway, I compare Buddha system ha, some do not write tests, and some write full tests.

conclusion

This article cost the author more than a week of spare time, save hand typing 6000+ words, at the same time collect, sort out many previous skills and writing while thinking and summary. If it helps you, that’s its greatest value. I can’t get over it if I don’t like it! 😄

Due to the limited technical level, if there is any mistake in the article, please point out in the comment area, thank you!

Most of the code in this article will be updated in Suo-Design-Pro

The project will be perfected as much as possible

I had planned to write the whole content in two sections, but I found it impossible. Before I finished 4 chapters, I had already written 6000 – + words. The last section is ready to go home after the New Year, here in advance to wish everyone a happy New Year!

This article is formatted using MDNICE