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 understand
npm/yarn/git/sass/pug/vue/vuex/vue-router/axios/mock/ssr/jest
The 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 form
2.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,
button
ordiv
The label - How to control by attributes
button
Component colorscolor
, shape,type
, size,size
- How to deal with
button
Component click events - How to scale
button
Component 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 nativebutton
Advantages of labels:
- Better label semantics
- Native tags come with native tags
buff
, 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
.
usediv
Advantages of simulation:
- Don’t care
button
Native styles bring some interference, write less to override nativecss
Code, cleaner and pure. - All use
div
You don’t need to go looking for native tags and delve into some of the weird compatibility issues associated with native tags. - More malleable, which means more extensible and compatible. This is also what most components choose to use
div
Cause 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:
-
Get the properties passed by the parent component using props.
-
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 useprops
Controls component properties, used in templatesv-if/v-show
Add component power, such as adding internal ICON, use:style``class
Control component styles.Also note that we have to control the nativetype
Type, 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 passesprops
Passes to child components without elaboration
2. Subcomponents passemit
The event passes data to the parent, which passeson
Eavesdropping, 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.
- use
store
As aState managementWarehouse, and introducedstateThe concept of - It’s a completely different model,
bus
The 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, triggering
dom
) - You can’t change the state directly(The only way is to submit
mutation
)
$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
- To obtain state
console.log(store.state.count)
Copy the code
- 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:
- Using computed methods
- Use methods
- Use the utils utility class
- The use of filters
Which approach should you choose?
I discuss this problem from the following three aspects
1. Implementability
- use
computed
Achieve success, we knowcomputed
It’s not a function that can’t be taken as a parameter, so here’s a trick,return
A 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
- use
methods
Success. 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
- use
utils
和methods
It can be done almost as well - use
filter
It’s also realizable. There’s something that can bemethods
,computed
Oh, 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
computed
、methods
和data
The three components do not have the same name, and they cannot be used by other components unless passedmixins
filters
与utils
Don’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 partyfilter
andutils
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
- Browser Runtime environment
- 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
async
与await
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
- Don’t write tests. The upside is saving time, the downside is
bug
Many, low code quality. - 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.
- 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.
- 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