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 thatcreatedEvent 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, probablyvueWhen 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 themountedAs 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 usedtemplateThe 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 syntaxv-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:

  • refYou must wait for the DOM to load before you can access it
  • althoughmountedThe lifecycle DOM is already loaded, but we can use it just in case$nextTickfunction

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:

  1. Hash mode has an ugly hash sign and usesonhashchangeEvent switch routing, compatibility is better, do not need server cooperation
  2. The history mode is nice, but local development, website launching, requires additional server configuration, and also need to write your own 404 page, usingHTML5history 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) isapplication/x-www-form-urlencoded
  • The content-Type of form data (with files) ismultipart/form-data
  • The content-type of json data isapplication/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

  1. 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
  1. 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
  1. 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 itv-if="true".
  2. 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 propertiesheight="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
  1. The use of multipleuploadComponent that needs to upload these files together to the server. Can be achieved bythis.$refs.poster.uploadFilesGet 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
  1. useVueI18nInternationalization, need to beelementUIThe 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