# 前言

近期都在用 Vue 全家桶进行项目开发,过程中难免会遇到不少问题,这篇博客主要就是记录开发过程中遇到的问题,和每个问题对应的解决方案。此外,Vue 框架和周边生态会一直更新,以及发布新功能,在实践过程中总会遇到一些所谓的“坑”,我也会把填坑过程记录于此。坑是填不完的,这篇博客也是写不完的。🙂

# Vue

# 子组件改变 props 的方法

由于 vue 遵循单向数据流,不建议在子组件里面直接改变 props 的值,一般通过 3 种方法

  • .sync修饰符(推荐使用)
// 父组件使用子组件
<Comp :title.sync="title"></Comp>

// 子组件中数据更新到父组件中
this.$emit(update:title, 'xxx');
1
2
3
4
5
  • 通过$emit调用父组件事件来改变父组件传给子组件的值,然后在子组件里面watch props 的值,状态变化时触发相关反应,原理和第一种方法一样

  • 给 props 加个对象字段,如:

props: ['state'],

template: <span>{{state.someData.value}}</span>

// 改
js: this.state.someData.value = xxx; // 直接改变了父组件的data值,不会触发vue warning
1
2
3
4
5
6

# 如何在 vue 组件中绑定原生点击事件?

使用.native修饰符,这样原生点击事件会绑定到该组件的$el节点上

例如使用 iview 某个组件

<Card @click.native="handleClick" />
1

# 使用组件时如何加行内 syle

在使用 vue 组件时,如果想给该组件加个行内 style,需要:style="{}"这种写法,直接写 style 不会生效,class 则可以

# 关于 v-model

v-modelv-bind:valuev-on:input 和语法糖

<div id="app">
  <input v-model="value" type="text">
  <input :value="value" type="text" @input="value = $event.target.value">
  <custom-input v-model="value"/>
</div>
1
2
3
4
5

其中,custom-input 的写法

Vue.component("custom-input", {
  props: ["value"],
  template: `
    <input
      :value="value"
      @input="$emit('input', $event.target.value)"
    >
  `
});
1
2
3
4
5
6
7
8
9

# render 函数里如何用组件的 v-model

用 iview 中 Poptip 组件举例:

<template>
  <Poptip v-model="visible">
    <a>Click</a>
    <div slot="title">Custom title</div>
    <div slot="content">
      <a @click="close">close</a>
    </div>
  </Poptip>
</template>
<script>
export default {
  data () {
    return {
      visible: false
    }
  },
  methods: {
    close () {
      this.visible = false;
    }
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

用 render 函数方法调用该组件

核心就是把 v-model 拆成 value 和 input 的写法

export default {
  name: "RenderPoptip",
  data() {
    return {
      visible: false
    };
  },
  methods: {
    close() {
      this.visible = false;
    }
  },
  render(h) {
    const vm = this;
    return
    h(
      "Poptip",
      {
        props: {
          value: vm.visible
        },
        on: {
          input: e => {
            this.visible = e;
          }
        },
        ref: "poptip"
      },
      [
        h("a", "click"),
        h(
          "div",
          {
            slot: "title"
          },
          "Custom title"
        ),
        h("div", [
          h(
            "a",
            {
              slot: "content",
              on: {
                click: () => {
                  this.close();
                }
              }
            },
            "关闭"
          )
        ])
      ]
    );
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

在 iview 的一些组件中,只提供 render 函数给我们渲染内部的内容,这时候我们可以用以上方法渲染 iview 的其它组件

template 模板写法和 render 写法最终都会渲染成Vnode,然后再挂载到 dom 上,在有些场景上,使用 render 写法更为清晰

# Transition 组件 mode

  • in-out:新元素先进行过渡,完成之后当前元素过渡离开。

  • out-in:当前元素先进行过渡,完成之后新元素过渡进入。

# data 属性不被代理的方法

const app = new Vue({
  data: {
    _app: "",
    $ppa: ""
  }
});
// 以上_app,$ppa都不会被代理,只要加了_和$开头的属性都不会被vue代理
// this._app 和 this.$ppa皆不存在
1
2
3
4
5
6
7
8

# cli3 传递全局 scss 变量小坑

// vue.config.js
module.exports = {
  css: {
    loaderOptions: {
      // 给 sass-loader 传递选项
      sass: {
        // @/ 是 src/ 的别名
        // 所以这里假设你有 `src/variables.scss` 这个文件
        data: `@import "@/variables.scss";`
      }
    }
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13

注意:如果第一个加载的 vue 组件,<style>处没有声明lang=scss,然后第二个组件中声明了scoped也声明了lang=scss,那么上面传递的全局变量都会被前缀处理

解决方案:第一个组件(通常 App.vue)style 处声明lang='scss'且不能声明scoped

# watch 路由中复用的组件的属性

在使用vue-router中,在/page/a 跳转到 /page/b 中,如果注册的是动态路由,那么它们使用的也是同一个组件,例如 Page.vue。从 a 页面跳转到 b 页面,由于是同一个组件,vue-router 会复用这个在内存中的组件实例,所以相关生命周期不会重复调用,数据也不会更新,所以如果要在跳转页面做一些动作(例如:高亮菜单,数据获取),就需要用watch方法:

data() {
  return {
    subNavList: []
  }
},
watch: {
  '$route': {
    handler: 'getSubNavList',
    immediate: true // 使用这个就不需要在created里面调用getSubNavList
  }
},
methods: {
  getSubNavList(route) {
    this.subNavList = getSubNavList(route)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 父组件调用子组件方法

在父组件中用$refs或者$children拿到对应子组件实例对象进行调用

# 父组件监听子组件生命周期方法

<template>
	<Comp @hook:mounted="hookMounted" />
</template>
1
2
3

# 关于数组和对象数据更新视图不更新问题

这些都是 js 的限制,使用Vue.set方法解决,或者一些变异方法

数组更新方法

对象更新方法

# iview 瞄点组件注意的问题

<div class="anchor-class " v-if="isAnchor">
  <Anchor container=".scrollcontent" show-ink>
    <AnchorLink :href='"?name="+activeName+"#"+index' :title="itemData.name " v-for="(itemData,index) in selectCardList " :key="index " />
  </Anchor>
</div>
1
2
3
4
5

如果瞄点依赖的 dom 内容是要 ajax 获取信息进行渲染的,那么直接挂载瞄点组件会报错,因为组件渲染是比 ajax 快的,数据没回来,dom 节点没加载,瞄点组件无法扫描需要的 dom 节点,所以会出问题

v-if+nextTick即可解决

created() {
  this.$http.get("xxx").then(res => {
    if (res.status === 200) {
      this.$nextTick(() => {
        // nextTick回调函数中,scrollcontent内容已经渲染完毕
        // 这时可以渲染iview瞄点组件了
        this.isAnchor = true;
      });
    }
  });
}
1
2
3
4
5
6
7
8
9
10
11

# 如何利用$mount挂载实例(组件)

什么是$mount?

如果 Vue 实例在实例化时没有收到 el 选项,则它处于“未挂载”状态,没有关联的 DOM 元素。可以使用 vm.$mount() 手动地挂载一个未挂载的实例

<div id="app">
  <span>{{message}}</span>
</div>
<span id="component-1">
  {{message}}
</span>
<span id="component-2">
  {{message}}
</span>
<span id="component-3">
  <!-- {{message}} -->
</span>
1
2
3
4
5
6
7
8
9
10
11
12
  • 挂载根实例
// 方法一
new Vue({
  data: {
    message: "hello vue"
  }
}).$mount("#app");

// 方法二
new Vue({
  el: "#app",
  data: {
    message: "hello vue"
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • 组件实例挂载
// 利用extend构造一个组件实例
const MyComponent = Vue.extend({
  // 注意,组件经常会被多次实例
  // 所以data必须是个函数
  data() {
    return {
      message: "component"
    };
  }
});

// 有三种方法可以挂载
// 方法一
new MyComponent().$mount("#component-1");

// 方法二
new MyComponent({
  el: "#component-2"
});

// 方法三
const c = new MyComponent({
  data() {
    return {
      message: "component"
    };
  },
  render: function(h) {
    return h("span", this.message);
  }
}).$mount();
document.getElementById("component-3").appendChild(c.$el);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# watch 对象某个字段

watch: {
  "condition.name"(newValue) {
    console.log(newValue);
  }
}
1
2
3
4
5

# computed 的值可以被 watch

computed: {
  fullName() {
    return this.firstName + this.lastName;
  }
},
watch: {
  fullName(newValue) {
    console.log(newValue);
  }
}
1
2
3
4
5
6
7
8
9
10

# 递归组件用 jsx

const renderSubMenu = item => {
  return (
    <Submenu key={item.name} name={computedName(item)}>
      <template slot='title'>
        {this.$scopedSlots.subMenuItem
          ? this.$scopedSlots.subMenuItem(item)
          : [
              item.icon && <i class={["iconfont", item.icon]} />,
              <span class='nr-menu-title' title={item.name}>
                {item.name}
              </span>
            ]}
      </template>
      {item.children.map(item => {
        {
          if (hasChild(item)) {
            return renderSubMenu(item);
          }
          return (
            <MenuItem
              name={computedName(item)}
              key={item.name}
            >
              {this.$scopedSlots.menuItem
                ? this.$scopedSlots.menuItem(item)
                : 
                  <span class='nr-menu-title' title={item.name}>
                    {item.name}
                  </span>
                }
            </MenuItem>
          );
        }
      })}
    </Submenu>
  );
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

# v-model 控制显隐的利器(value + input + watch)

const comp = {
  template: `<div v-show="show" @click="close">v-model</div>`,
  props: {
    value: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      show: this.value
    }
  },
  watch: {
    value(v) {
      this.show = v;
    }
  },
  methods: {
    close() {
      this.$emit("input", !this.show);
    }
  }
}
// use
Vue.component('Comp', comp);
<Comp v-model="show"/>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# $attrs、$listeners 针对二次封装有奇效

相当于帮iview-Modal内置属性和事件做一次桥接,这样便拥有和iview-Modal一致的api,非常适合用于组件的二次封装

<Modal v-model="show" v-bind="$attrs" v-on="$listeners">
  <div slot="header" class="modal-header">
    <span class="title">{{$attrs.title}}</span>
  </div>
  <slot></slot>
  <div slot="footer" class="modal-footer">
    <span class="close-btn" @click="handleClose">取消</span>
    <Button @click="handleConfirm">确定</Button>
  </div>
</Modal>

<script>
export default {
  inheritAttrs: false,
  name: "NrModal"
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 利用 key 来强制重渲染

Vue.component("comp", {
  template: `<span>组件</span>`,
  created() {
    console.log("被重新渲染了");
  }
});

const app = new Vue({
  el: "#app",
  template: `<div>
    <comp :key="key"/>
    <button @click="update">刷新</button>
  </div>`,
  data: {
    key: 0
  },
  methods: {
    update() {
      this.key++;
    }
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 动态组件的 :is 值除了是字符串,还可以是什么?

  • 组件对象
const comp = {
  render(h) {
    return h('span', '我是组件对象')
  }
}

<component :is="comp"/>

// 或者
import comp from "comp.vue";
<component :is="comp"/>
1
2
3
4
5
6
7
8
9
10
11
  • vue实例
const compConstructor = Vue.extend({
  render(h) {
    return h("h1", "我是vue实例");
  }
})

<component :is="compConstructor"/>
1
2
3
4
5
6
7
  • 函数

情况一:返回Promise对象

const componentFn = function() {
  return new Promise(resolve => {
    resolve({
      render(h) {
        return h("h1", "我是Promise返回来的vue组件对象");
      }
    });
  });
}

<component :is="componentFn"/>
1
2
3
4
5
6
7
8
9
10
11

情况二:返回一个对象(高级异步组件)

const asyncComponentFn = function() {
  return {
    // 需要加载的组件 (应该是一个 `Promise` 对象)
    component: new Promise((resolve, reject) => {
      // reject();
      setTimeout(() => {
        resolve({
          render(h) {
            return h("h1", "我是vue异步组件");
          }
        });
      }, 3000);
    }),
    // 异步组件加载时使用的组件
    loading: {
      render: h => h("h1", "loading...")
    },
    error: {
      render: h => h("h1", "error")
    },
    delay: 0,
    timeout: 3000
  };
}

<component :is="asyncComponentFn"/>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# vue 模板解析报错解决方法

[Vue warn]: You are using the runtime-only build of Vue where the template compiler is not available. Either pre-compile the templates into render functions, or use the compiler-included build.

vue.config.js增加别名:

module.exports = {
  configureWebpack: {
    resolve: {
      alias: {
        'vue$': 'vue/dist/vue.esm.js'
      }
    }
  }
1
2
3
4
5
6
7
8

# 如何用 this.$xxx 方式手动挂载组件

比如我这边有个基于 iview modal 封装的弹窗组件 ErsConfirm,用普通的模板写法就是这样的

<ErsConfirm
  v-model="modal1"
  title="删除"
  confirm-info="确定要删除该项目吗?"
  @on-confirm="ok"
  @on-close="cancel"
/>
1
2
3
4
5
6
7

如果在业务逻辑中存在多个询问弹窗层,写大量模板是比较难受的事情,代码也比较冗余,所以需要用 js 命令式的方式进行组件挂载,这样看起来就优雅得多,下面是实现过程:

import Vue from 'vue';
import ErsConfirm from './ErsConfirm/ErsConfirm.vue';

// Vue.use()
export default function(Vue) {
  Vue.prototype.$ErsConfirm = createErsConfirm;
}

function createErsConfirm(options = {}) {
  const instance = ErsConfirm.newInstance(options);
  instance.show();
}

// 拿属性,不拿方法
function getAttrs(props) {
  return Object.keys(props).reduce((pre, cur) => {
    if (typeof props[cur] !== 'function') {
      pre[cur] = props[cur];
    }
    return pre;
  }, {});
}

function noop() {}

ErsConfirm.newInstance = (props) => {
  const { onConfirm, onClose } = props;
  const attrs = getAttrs(props);
  const instance = new Vue({
    inheritAttrs: false,
    data: {
      visible: false,
    },
    methods: {
      change(value) {
        if (value === false) {
          this.remove();
        }
      },
      remove() {
        setTimeout(() => {
          this.destroy();
        }, 300);
      },
      destroy() {
        this.$destroy();
        if (this.$el) {
          document.body.removeChild(this.$el);
          this.$el = null;
        }
      },
    },
    render() {
      return (
        <ErsConfirm
          value={this.visible}
          on-input={this.change}
          {...{
            attrs,
            on: {
              'on-confirm': onConfirm || noop,
              'on-close': onClose || noop,
            },
          }}
        />
      );
    },
  });

  const component = instance.$mount();
  document.body.appendChild(component.$el);

  return {
    show() {
      instance.visible = true;
    },
  };
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78

安装插件:

import $ErsConfirm from './$ErsConfirm';
Vue.use($ErsConfirm);
1
2

这样,就可以用 this.$ErsConfirm 方式来使用了该组件了

this.$ErsConfirm({
  title: '删除',
  confirmInfo: '确定要删除该项目吗?',
  onConfirm: () => {
    console.log('confirm');
  },
  onClose: () => {
    console.log('close');
  },
});
1
2
3
4
5
6
7
8
9
10

推荐一个更强大更通用的手动调用库 vue-create-api

# Vue-router

# beforeEach 路由守卫该注意的

以简单的登录拦截举例:

/**
 * 开启路由守卫
 */
router.beforeEach((to, from, next) => {
  // cookie上存了token,可以不用登录
  const token = getToken();
  if (token) {
    // 如果要跳转的页面是登录页,强制跳转到根页面
    if (to.path === LOGIN_PAGE_PATH) {
      next({ path: "/" });
    }
    next();
  } else {
    // 没存就跳转到登录页面
    if (to.path !== LOGIN_PAGE_PATH) {
      next({
        path: LOGIN_PAGE_PATH
      });
    }
    // 这里如果不加next(),会进入死循环
    // 因为 next({ path: LOGIN_PAGE_PATH })也会触发 beforeEach
    // 所以需要提供最终的next(),才能把导航的状态置为confirmed
    next();
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# vue-router 为 history 时请求本地 static 的小坑

如果vue-router使用history模式,比如在http://localhost:8080/about/home页面下,发起本地json文件ajax请求

this.$http.get('static/foo.json').then(res => {
  console.log(res)
})
1
2
3

这时控制台会发出404报错,GET http://localhost:8080/about/static/foo.json 404 (Not Found)

问题在于发起的请求地址是错的,应该是http://localhost:8080/static/foo.json才对

对这种问题,有2种解决方法

  • static前面加个/this.$http.get('/static/foo.json'),但这种情况打包路径要额外处理

  • vue-router模式改为hash模式

# 路由高级异步组件处理方法

const LoadingCom = {
  name: 'loading-com',
  render(h) {
    return h('h1', 'Loading...')
  }
}

const ErrorCom = {
  name: 'error-com',
  render(h) {
    return h('h1', 'Error')
  }
}

function lasyLoadView (AsyncView) {
  const AsyncHandler = () => ({
    // component选项必须返回Promise
    // import('xxx.vue')会返回一个Promise
    component: AsyncView,
    loading: LoadingCom,
    error: ErrorCom,
    // 组件挂载延迟时间,默认200
    // 0的话loading组件也会立即渲染
    delay: 0,
    // 超时渲染error组件
    timeout: 10000
  })
  return Promise.resolve({
    functional: true,
    render(h, { data, children }) {
      return h(AsyncHandler, data, children)
    }
  })
}

// 使用方法
const router = new VueRouter({
  routes: [
    {
      path: 'home',
      // 如果home.vue组件很大,请求很慢
      // 会先渲染loading组件,请求完成再渲染home.vue组件
      component: () => lazyLoadView(import('./home.vue'))
    }
  ]
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

未完待续...

Last Updated: 2020-9-2 10:01:27 ├F10: AM┤