preface
With vUE family barrel development for more than a year, stepped on a lot of pits, but also solved a lot of problems, some of them recorded, I hope to help you. The following content is based on the latest version of VUE + vuex + VUE-Router + AXIos + less + elementUI, vUE scaffolding is VUE-CLI3.
The scoped property of the CSS
Vue to prevent CSS contamination, when a component’s
Note: The outermost tag of each component carries the parent component’s data-V attribute, meaning that the tag is matched by the parent component’s style. Therefore, the parent component should not use the tag selector, and the tag should not use the id or class of the parent component.
When the parent component wants to modify the CSS of the child component (modify the style of the elementUI component), we can use the depth selector >>>
div >>> .el-input{ width: 100px; } /* sass/less may not be recognized, so use the /deep/ selector. */ div /deep/ .el-input{ width: 100px; }Copy the code
The depth selector removes the attribute selector for the following element [data-v-], which compiles to: div[data-v-12345667]. El-input {}. You can match the elements of the child component to override the style.
The order in which the parent component’s lifecycle hook functions are executed
A component’s lifecycle hook function is created at a certain point in its lifecycle. For example, when the DOM is loaded, a mounted hook function is created. Therefore, a delay timer is created that does not wait for the timer to execute.
Reference to the trigger time of each periodic hook function (figure from network)
About the parent component lifecycle: Different hook functions behave differently. BeforeMount the parent component’s virtual DOM is initialized before the child component’s virtual DOM is initialized (beforeMount). Mounted = window.onload The parent DOM component never loads properly. So the basic lifecycle hook functions are executed in the following order: Parent beforeCreate -> Parent created -> parent beforeMount -> child beforeCreate -> child created -> child beforeMount -> Child Mounted -> parent Mounted
The order of update and beforeUpdate execution for parent and child components: data modification + virtual DOM readiness triggers beforeUpdate, in other words beforeUpdate equals beforeMount, and update equals Mounted. So the order is: parent beforeUpdate -> child beforeUpdate -> child update -> parent update.
The order of beforeDestory and deStoryed is parent beforeDestory -> child beforeDestory -> child deStoryed -> parent deStoryed.
Mounted: The lifecycle hook function is mounted. [mounted1, mounted2], the same lifecycle can trigger multiple functions, which is how mixins work. Mixins can also write lifecycle hooks, which will eventually become an array with the lifecycle hook functions in the component, and the mixins will execute the hooks first.
Which hook function is better for asynchronous request data to be executed in
A lot of people think thatcreated
Event to request data, and then generate the virtual DOM together, and then render it better. In practice, requests take time, and that time is unstable, probablyvue
When the virtual DOM is ready, your data is requested, and then you have to update the virtual DOM again, and then render, which greatly prolongs the white screen time, and the user experience is very bad. And in themounted
As for the event request data, the static page is rendered first, and when the data is ready, part of the DOM can be updated.
Addendum: digg friend points out, here understand wrong, sorry.
The asynchrony of the lifecycle hook function is put into the event queue and not executed in the hook function. Created and Mounted requests do not update the data immediately, so they do not cause the virtual DOM to reload or affect static parts of the page. An asynchronous assignment in the lifecycle hook function, and vUE performs an update after the process has completed. In addition, assigning values to data and then updating the DOM is also asynchronous. When the data changes, Vue opens a queue and buffers all data changes that occur in the same event loop, removing duplicate assignments and updating.
Testing asynchronous behavior in lifecycle hook functions:
export default {
data(){
return {
list: [].}},methods:{
getData(){
// Generates a range of random integers
const randomNum = (min, max) = > Math.floor(Math.random() * (max - min + 1)) + min;
// Generates a non-empty array of fixed length
const randomArr = length= > Array.from({ length }, (item, index) => index * 2);
const time = randomNum(100.3000);// Simulate the request time
console.log('getData start');
return new Promise(resolve= > {
setTimeout((a)= > {
const arr = randomArr(10);
resolve(arr);
},time)
})
}
},
async created(){
console.log('created');
this.list = await this.getData();
console.log('getData end');
},
beforeMount() {
console.log('beforeMount');
},
mounted(){
console.log('mounted');
},
updated(){
console.log('updated'); }}Copy the code
The result is shown in the following figure. Therefore, data update time is the same when created and Mounted requests are created. Mounted hooks are not loaded when using server SSR.
The parent component listens for the life cycle of the child component
Instead of writing a custom event and firing it in a child component’s lifecycle function, we can use a hook:
<child @hook:created="childCreated"></child>
Copy the code
Switch from page A to page B. There is A timer on page A, which is not used on page B. It needs to be cleared when leaving page A. The method is very simple. Clear this beforeDestory or beforeRouteLeave in the page’s lifecycle hook function, but how to get the timer? To write timers to data, which is possible but not elegant, we write as follows:
// After initializing the timer
this.$once('hook:beforeDestory',()=>{
clearInterval(timer);
})
Copy the code
Clever use of the IS property
Because of the HTML tag limitation, tr tag can only have th, TD tag, and custom tag will be parsed to tr tag, so we can use is attribute
<tr>
<td is="child">
</tr>
Copy the code
I recently had a page with a large number of SVG ICONS, and I wrote each SVG as a component. Since SVG component names are different, dynamic tags are required to represent:
<! -- Suppose our data is as follows -->
arr: [ { id: 1, name: 'first' }, { id: 2, name: 'second' }, { id: 3, name: 'third' }, ]
<! -- This should have been written -->
<div v-for="item in arr" :key="item.id">
<p>item.name</p>
<svg-first v-if="item.id===1"></svg-first>
<svg-second v-if="item.id===2"></svg-second>
<svg-third v-if="item.id===3"></svg-third>
</div>
<! -- Actually, it's more elegant -->
<div v-for="item in arr" :key="item.id">
<p>item.name</p>
<component :is="'svg'+item.name"></component>
</div>
Copy the code
Pass extra parameters to the event
The variable $event represents the event object.
What if the variable to be passed is not an event object? While working with elementUI, I encountered a situation where I used a drop-down menu component in a table as follows:
<el-table-column label="Date" width="180">
<template v-slot="{row}">
<el-dropdown @command="handleCommand">
<span>The drop-down menu<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="a">Yellow golden cake</el-dropdown-item>
<el-dropdown-item command="b">The lion</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</el-table-column>
Copy the code
The dropdown menu event command has a parameter that is the value selected by the dropdown, and we want to pass the table data. What if @command=”handleCommand(row)” overwrites the parameter? The arrow function @command=”command => handleCommand(row,command)” is used to solve the parameter transfer problem perfectly.
By the way, elementUI tables can use the $index variable to represent the current number of columns, just like $event:
<el-table-column label="Operation">
<template v-slot="{ row, $index }">
<el-button @click="handleEdit($index, row)">The editor</el-button>
</template>
</el-table-column>
Copy the code
If there are multiple (or unknown) default parameters, you can write @current-change=”(… defaultArgs) => treeclick(ortherArgs, … defaultArgs)”
V – slot grammar
The usage of v-slot (deprecated slot syntax) : it is equivalent to leaving a slot in the component. When using the component, some labels can be passed and inserted into the slot. You can have multiple empty Spaces with different names. The default is default. You can also pass in some data with the shorthand #.
<! -- Subcomponent -->
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
<! -- Parent component -->
<base-layout>
<! -- slots can be shortened to # -->
<template #header="data">
<h1>Here might be a page title</h1>
</template>
<! -- v-slot:default -->
<div v-slot:default>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</div>
<! -- You can use deconstruction -->
<template #footer="{ user }">
<p>Here's some contact info</p>
</template>
</base-layout>
Copy the code
Conclusion:
- Slots with other names (not default) can only be used
template
The label. - A label in a slot can’t get the data passed to its children (a slot is equivalent to a grandchild component) :
<child :data="parentData">
<div>The data data is not accessible here</div>
</child>
Copy the code
- Slots can use deconstruction syntax
v-slot="{ user }"
.
The child modifies the value passed by the parent
V-models are used much like bidirectional bindings, but Vue is a one-way data stream, and V-models are nothing more than syntagmatic sugar: the parent uses V-bind to pass the value to the child, which changes the parent’s value via the change/input event.
<input v-model="inputValue" />
<! -- equivalent to -->
<input :value="inputValue" @change="inputValue = $event.target.value" />
Copy the code
V-models can be used not only on input, but also on components.
Data transfer between VUE components is one-way, that is, data is always passed from the parent component to the child component. The child component can maintain its own data inside, but it has no right to modify the data passed to it by the parent component. We can also modify the value of the parent component by referring to the V-model syntax sugar, but it is too troublesome to write this way every time. Vue provides a modifier.sync, which can be used as follows:
<child :value.sync="inputValue"></child>
<! -- Subcomponent -->
<script>
export default {
props: {
//props can set the value type, default value, required or not, and validation function
value: {
type: [String.Number].required: true,}},// Use a variable to change the value of the parent component
computed: {
_value: {
get() {
return this.value;
},
set(val) {
this.$emit('update:value', val); }},}};</script>
Copy the code
The parent component accesses the child component through ref
Although vue provides $parent and $children to access parent/child components, there is a lot of uncertainty about the parent/child component, such as component reuse, its parent can have multiple cases. We can access the child component’s data and methods through ref.
<child ref="myChild"></child>
<script>
export default {
async mounted() {
await this.$nextTick();
console.dir(this.$refs.myChild); }};</script>
Copy the code
Note:
ref
You must wait for the DOM to load before you can access it- although
mounted
The lifecycle DOM is already loaded, but we can use it just in case$nextTick
function
Background images, @import of CSS use path aliases
When packaging is processed with Webpack, a directory can be configured with an alias, and resources can be referenced in the code using a path relative to the alias
import tool from '@/utils/test'; // Webpack can correctly identify and package.
Copy the code
However, in CSS files such as less, sass, or stylus, using the @import “@/style/theme” syntax to refer to the @ directory does raise an error. The solution is to add a ~ symbol at the beginning of the string referencing the path.
- The CSS in the module:
@import "~@/style/theme.less"
- CSS properties:
background: url("~@/assets/xxx.jpg")
- In the HTML tag:
<img src="~@/assets/xxx.jpg">
Vue – Router hash mode and history mode
We first come to a complete URL:https://www.baidu.com/blog/guide/vuePlugin.html#vue-router. https://www.baidu.com is the root directory of the website, /blog/guide/ is the subdirectory, vueplugin. HTML is the file under the subdirectory (if there is only a directory and no specified file, the default request is index.html). And #vue-router is the hash value.
Vue is a single-page application, packaged with a single index.html file. Once deployed on the server, the directory to access the corresponding file is this file.
Hash mode: The url is followed by a hash value that corresponds to the name of each router. A hash value change means that the router has changed, and listens for the onHashChange event to replace the page content.
The history mode: The url is followed by ‘fake directory name’, which is the name of the router, and the browser will request the file from this directory (it doesn’t exist, 404), so the history mode requires the server to configure the 404 page to be redirected to our index.html. Vue-router then replaces the page content based on the directory name.
The advantages and disadvantages:
- Hash mode has an ugly hash sign and uses
onhashchange
Event switch routing, compatibility is better, do not need server cooperation - The history mode is nice, but local development, website launching, requires additional server configuration, and also need to write your own 404 page, using
HTML5
的history API
, compatibility is poor.
The configuration differences are as follows:
const router = new VueRouter({
mode: 'history'.// The "hash" mode is default and no configuration is required
base: '/'.// Default configuration
routes: [...]
})
Copy the code
Vue.config.js configuration for vue-cli3:
module.exports = {
publicPath: ". /".// Hash mode to package
// publicPath: "/", // history mode is packaged
devServer: {
open: true.port: 88.// historyApiFallback: true, //history mode for local development}}Copy the code
If the site is deployed in the root directory, the base of the router is omitted. If the entire single-page application service is under /app/, then base should be set to “/app/” and the publicPath of the package configuration (vue.config.js) should also be set to /app/.
Vue-cli3 will have a route selection mode when generating new projects. Selecting history mode will help you to configure them.
Vue-router hook function
Hook functions are divided into three types: component hooks, global hooks, and route exclusive hooks.
App. vue does not have a component hook function. Because app. vue is the entry point to the page, the component must be loaded, and using the component hook function prevents the component from loading.
Global hooks are mainly used for route authentication, but are expensive. The beforeRouteLeave hook is used to alert users before they leave (for example, if there is an unsaved article). This hook has a few bugs: in hash mode, the browser’s back button cannot trigger this hook function. We can also listen for the user to close the current window/browser events:
window.onbeforeunload = e= > "Be sure to leave the current page and your changes will not be saved!";
Copy the code
In order to prevent malicious sites, the user closing window/browser event is not preventable, can only be prompted, and different browsers have different compatibility.
Vuex persistent storage
Data in Vuex is lost after the page is refreshed. To realize persistent storage, local storage (cookies, storage, etc.) is required. Generally, data returned after login (roles, permissions, tokens, etc.) needs to be stored in Vuex. Therefore, data can be stored locally on the login page, while on the main page (except the login page, The beforeCreate or route hook beforeRouteEnter is read before all other page entries are entered and submitted to Vuex. This will trigger the main page to enter the hook function even if refreshed, which will be submitted to Vuex.
beforeRouteEnter (to, from, next) {
const token = localStorage.getItem('token');
let right = localStorage.getItem('right');
try{
right = JSON.parse(right);
}catch{
next(vm= > {
// Popovers use elementUI
vm.$alert('Obtaining permission failed').finally((a)= > {
vm.$router.repalce({name:'login'})})})}if(! right || ! token){ next({name:'login'.replace:true})}else{
next(vm= > {
// Events are triggered after Mounted
vm.$store.commit('setToken',token);
vm.$store.commit('setRight',right); }}})Copy the code
The beforeRouteEnter callback is triggered after the mounted hook. Mounts on the main page are triggered after mounts on all child components, so we can write this.
import store from '^/store';// Bring in the instantiated store
beforeRouteEnter (to, from, next) {
const token = localStorage.getItem('token');
if(! token){ next({name:'login'.replace:true})}else{
store.commit('setToken',token); next(); }}Copy the code
To implement persistent storage of data after modification, we can first store the data to localStorage, then listen for window.onstorage event, and submit any modification to Vuex.
Mutations trigger action
Mutations changes the value of state synchronously. If another value is obtained asynchronously (action), depending on the change of this synchronous value, the events in the action need to be triggered before the mutations in mutations are assigned. We can name the instantiated Vuex. Get the Store object on mutations.
const store = new Vuex.Store({
state: {
age: 18.name: 'zhangsan',},mutations: {
setAge(state, val) {
// If age changes, name should also change
// We need to trigger getName in action every time we assign age
state.age = val;
store.dispatch('getName'); }, setName(state, val) { state.name = val; }},actions: {
getName({ commit }) {
const name = fetch('name'); // Get it asynchronously from the interface
commit('setName', name); ,}}});Copy the code
Vue.observable communicates with components
If your project is small and you don’t need vuex, you can use vue. Observable to simulate one:
//store.js
import Vue from 'vue';
const store = Vue.observable({ name: 'Joe'.age: 20 });
constmutations = { setAge(age) { store.age = age; }, setName(name) { store.name = name; }};export { store, mutations };
Copy the code
Axios qs plugin
The data of get request is stored in url, similar to http://www.baidu.com?a=1&b=2, where a=1&b=2 are parameters of GET, and for POST request, parameters are stored in body. Common data formats include form data and JSON data. The difference is that the data format is different. The form data is encoded in the same format as get, but in the body, while json data is a JSON string
Qs Basic use:
import qs from 'qs'; // Qs is built-in in axios, so you can import it directly
const data = qs.stringify({
username: this.formData.username,
oldPassword: this.formData.oldPassword,
newPassword: this.formData.newPassword1,
});
this.$http.post('/changePassword.php', data);
Copy the code
Qs.parse () parses the URL into an object, and qs.stringify() serializes the object into a URL, concatenated with &. For different data formats, Axios automatically sets the content-Type instead of manually setting it.
- The content-Type of form data (without files) is
application/x-www-form-urlencoded
- The content-Type of form data (with files) is
multipart/form-data
- The content-type of json data is
application/json
I came across an interface that required me to pass an array with a form. If qs.stringify() is used, arr[]=1&arr[]=2&arr[]=3. If qs.stringify() is used, arr[]=1&arr[]=2&arr[]=3. As you can see, the nature of the form pass array is to pass the same parameter multiple times.
const data = new FormData();
arr.forEach(item= > {
data.append('arr', item);
});
Copy the code
Test, perfect solution, but things are not over here, check the QS official documentation, QS conversion support for the second parameter, perfect solution to our problem.
const data = qs.stringify(arr, { arrayFormat: 'repeat' }); // arr=1&arr=2&arr=3
Copy the code
Some summaries of elementUI
- Form validation is written synchronously to avoid multiple layers of nested functions
const valid = await new Promise(resolve= > this.$refs.form.validate(resolve));
if(! valid)return
Copy the code
- After the on-demand introduction of the cascade menu height across the screen. Solution: Add a global style
.el-cascader-menu > .el-scrollbar__wrap{
height: 250px;
}
Copy the code
- Cascading menu data is retrieved on demand and cannot be displayed. Solution: request the tree data according to the existing path data, and then add a V-if to the cascaded menu, and then display the data when all the data is requested. For example, if it is known that the user selects Guangdong Province, Shenzhen City and Nanshan District for the three-level linkage data of provinces and counties, request the data of all provinces, Guangdong Province and Shenzhen respectively, and then assemble the data into a tree, bind it to the cascade menu, and set it
v-if="true"
. - Table height adaptive, you can add a div to the outer layer of the table, and then calculate the height (or elastic box adaptive height) for that div, table properties
height="100%"
<div class="table-wrap">
<el-table :height="100%"></el-table>
</div>
Copy the code
/* less */. Table-wrap {height: calc(~" 100vh-200px "); /* less */. Table-wrap {height: calc(~" 100vh-200px "); */ /deep/. El-table {height: 100%! important; }}Copy the code
- The use of multiple
upload
Component that needs to upload these files together to the server. Can be achieved bythis.$refs.poster.uploadFiles
Get the file object. Then manually assemble the form data yourself.
<el-form-item label="Template file:" required>
<el-upload ref="template" action="" :auto-upload="false" accept="application/zip" :limit="1">
<span v-if="temForm.id">
<el-button slot="trigger" type="text"><i class="el-icon-refresh"></i>Update file</el-button>
</span>
<el-button slot="trigger" size="mini" type="success" v-else>Upload a file</el-button>
</el-upload>
</el-form-item>
<el-form-item label="Template poster:" required>
<el-upload action="" :auto-upload="false" ref="poster" accept="image/gif,image/jpeg,image/png,image/jpg" :show-file-list="false" :on-change="changePhoto">
<img :src="previewUrl" @load="revokeUrl" title="Click upload poster" alt="Resource Poster" width="250" height="140">
<template #tip>
<div>Tips: Recommended upload size 250*140</div>
</template>
</el-upload>
</el-form-item>
Copy the code
methods:{
// Replace the old image and display the thumbnail after selecting the image
changePhoto(file, fileList) {
// Create a Blob URL that previews the image directly
this.previewUrl = window.URL.createObjectURL(file.raw);
if (fileList.length > 1) {
fileList.shift();
}
},
revokeUrl(e) {
// Destroy the Blob URL after the image is loaded
if (e.target.src.startsWith("blob:")) window.URL.revokeObjectURL(e.target.src);
},
// Submit form data
async submitData() {
const template = this.$refs.template.uploadFiles[0].// Template file
poster = this.$refs.poster.uploadFiles[0].// The poster file
formData = new FormData();
if(! template)return this.$message.warning("Template file must be selected");
if(! poster)return this.$message.warning("Poster file must be selected");
formData.append("zip", template.raw);
formData.append("poster", poster.raw);
const res = await this.$http.post('url', formData); }},Copy the code
- use
VueI18n
Internationalization, need to beelementUI
The language package and the language package in the project are merged into one.
import VueI18n from "vue-i18n";
import zhLocale from './locales/zh.js';/* Import the local simplified Chinese language pack */
import zhTWLocale from './locales/zh-TW.js';/* Introduce native traditional Chinese language pack */
import enLocale from './locales/en.js';/* Introduce native English language pack */
import zhElemment from 'element-ui/lib/locale/lang/zh-CN'// Introduce elementUI simplified Chinese language pack
import zhTWElemment from 'element-ui/lib/locale/lang/zh-TW'// Introduce elementUI traditional Chinese language package
import enElemment from 'element-ui/lib/locale/lang/en'// Introduce the elementUI English language package
Vue.use(VueI18n);
const messages = {/ / language pack
zh: Object.assign(zhLocale, zhElemment),// Add the native language package to elementUI's language package
'zh-TW': Object.assign(zhTWLocale, zhTWElemment),// Add the native language package to elementUI's language package
en: Object.assign(enLocale, enElemment)// Add the native language package to elementUI's language package
};
const i18n = new VueI18n({
locale: "zh".//zh The default value is simplified Chinese
messages
});
Vue.use(ElementUI, {
i18n: (key, value) = > i18n.t(key, value)
})
Copy the code
The last
Any mistakes, any questions, please comment