vue:3年挖坑血与汗的教训

3,304 阅读14分钟

vue.js使用的人越来越多了,大多数的公司也在使用vue框架进行开发自己公司的产品。虽然vue.js语法比较简单,官网文档也比较详细,但是越简单的东西,越容易让人忽视细节,从而容易导致代码水平参差不齐,风格各不相同。 对于初学者,感觉一般不怎么会花时间去查看官网文档,不知道把视点落在何处。只有日后自己上班的时候碰到问题,踩到坑,加过班,苦修过,才有所领悟。 接下来,我将会对我这几年使用vue的一些经验心得进行总结,希望能帮助大家少碰一些壁,少踩一些坑。

1.创建vue脚手架不等于创建vue项目

对于一些新手,在刚刚学习vue的时候,每一次新建vue项目,都会先执行创建vue脚手架命令。究其原因,是因为他把脚手架以及vue项目混淆了,其实,脚手架就是我们搭建vue项目的环境,我们可以把它看作是一个舞台,一个舞台可以有n个节目在它上面表演。同样道理,我们只需要搭建一次脚手架就可以了,往后,我们只需要使用创建项目的代码即可。

2.插件安装方式

在vue 里面,但凡是插件,我们都可以尝试使用npm进行安装,所以,有时候,就不需要从网上下载,再解压缩,然后再导入项目里面去了.

3.ESLint

小白在刚接触vue的时候,不建议使用ESLint,因为有可能虽然你的代码逻辑写得很正确,但是有时忘记写一个分号,程序就会出错,这会给你学习vue会大受打击,甚至会崩掉。 当你能够完成一个小Demo的时候,你应该要开始重视ESLint了。在职场上,软件开发是一个团队进行,别人可能需要阅读你的代码,大的公司还需要代码审核,写的不好也容易被别人吐槽。

组件命名规范

<!--好的命名,借鉴element,使用连接符连接起来,全部小写-->
<el-form label-width="160px"></el-form>
<!--不推荐,过于简单,没有实际意义-->
<list></list>

组件配置项与事件命名

定义组件时使用驼峰: el-form

props:{
 labelWidth:{ // 使用驼峰
    type:String,
    default:()=>""
 }
},
methods:{
    click(){
     this.$emit("handle-click") // 全部小写,使用“-”
    }
}

别人使用组件时属性则对应使用 -,并全部小写,这个是 Vue 自动处理的。而事件 @ 是不会转换的,你 $emit 里是什么名字,则别人使用时也要这么写。

<el-form label-width="160px"></el-form>
<!--曾经看到一个高级前端类似这样命名,想打人-->
<el-form labelWidth="160px" @handleClick=""></el-form>

虽然很简单和基础,但是还是有些人不屑于遵守。还有一些人对于 Vue 组件属性加了 : 和没加 : 还是有点搞不清。 例如,没有加 : 表示传入给 current 的是一个字符串:

<todo-list current="1"></todo-list>

加了 : 表示等号后面 "" 内是变量:

<todo-list :current="current"></todo-list>
<todo-list :current="1"></todo-list>

如果熟悉这个就不会写出以下代码了:

<todo-list :current="'1'"></todo-list>
<todo-list :current="quot;1quot;'"></todo-list>

保持合理的书写顺序

不少小伙伴对于 Vue 的生命周期和配置书写没有顺序,喜欢把 compnents 选项放到最下面去,watch 和 computed 也写到对象最下面,其实 Vue 页面 methods 的代码量最多。如果我想看这个页面引用了哪些组件,需要翻动不少代码,甚至可能别人不知道 components 写在下面,他也想引入组件,就可能造成 compnents 选项重复。 书写顺序参考示例,犹如阅读文章一般:

export default{
    name:"BaseButton",
    components:{

    },
    props:{

    },

    computed:{
    },

    data(){
     return {

     }
    },
    watch:{
    },
    mounted(){
    },
    methods:{
    }
}

上述问题如果配置了 ESLint,Vue-cli 都会自动帮助我们进行提示和处理,强烈建议使用 Vue-cli 搭建项目时选上并开启 ESLint。如果你先有项目没有使用 ESLint 也没有关系,只要到项目中安装即可:

npm install eslint --save-dev
npm install babel-eslint --save-dev
npm install eslint-friendly-formatter --save-dev // 指定错误报告的格式规范插件
npm install eslint-loader --save-dev  // 启动vuecli时就可以检测
npm install eslint-plugin-vue --save-dev  // 符合vue项目推荐的代码风格

根目录下添加验证规则文件 .eslintrc.js:

module.exports = {
  root: true, // 根文件,不会往上一层查找
  parserOptions: {
    parser: 'babel-eslint',
    sourceType: 'module'
  },
  env: {
    browser: true,
    node: true,
    es6: true,
  },
  extends: ['plugin:vue/recommended', 'eslint:recommended'],

  // add your custom rules here
  //it is base on https://github.com/vuejs/eslint-config-vue
  rules: {
    "vue/max-attributes-per-line": [2, {
      "singleline": 10,
      "multiline": {
        "max": 1,
        "allowFirstLine": false
      }
    }],
    "vue/name-property-casing": ["error", "PascalCase"],
    'accessor-pairs': 2,
    'arrow-spacing': [2, {
      'before': true,
      'after': true
    }],
    'block-spacing': [2, 'always'],
    'brace-style': [2, '1tbs', {
      'allowSingleLine': true
    }],
    'camelcase': [0, {
      'properties': 'always'
    }],
    'comma-dangle': [2, 'never'],
    'comma-spacing': [2, {
      'before': false,
      'after': true
    }],
    'comma-style': [2, 'last'],
    'constructor-super': 2,
    'curly': [2, 'multi-line'],
    'dot-location': [2, 'property'],
    'eol-last': 2,
    'eqeqeq': [2, 'allow-null'],
    'generator-star-spacing': [2, {
      'before': true,
      'after': true
    }],
    'handle-callback-err': [2, '^(err|error)$'],
    'indent': [2, 2, {
      'SwitchCase': 1
    }],
    'jsx-quotes': [2, 'prefer-single'],
    'key-spacing': [2, {
      'beforeColon': false,
      'afterColon': true
    }],
    'keyword-spacing': [2, {
      'before': true,
      'after': true
    }],
    'new-cap': [2, {
      'newIsCap': true,
      'capIsNew': false
    }],
    'new-parens': 2,
    'no-array-constructor': 2,
    'no-caller': 2,
    'no-console': 'off',
    'no-class-assign': 2,
    'no-cond-assign': 2,
    'no-const-assign': 2,
    'no-control-regex': 0,
    'no-delete-var': 2,
    'no-dupe-args': 2,
    'no-dupe-class-members': 2,
    'no-dupe-keys': 2,
    'no-duplicate-case': 2,
    'no-empty-character-class': 2,
    'no-empty-pattern': 2,
    'no-eval': 2,
    'no-ex-assign': 2,
    'no-extend-native': 2,
    'no-extra-bind': 2,
    'no-extra-boolean-cast': 2,
    'no-extra-parens': [2, 'functions'],
    'no-fallthrough': 2,
    'no-floating-decimal': 2,
    'no-func-assign': 2,
    'no-implied-eval': 2,
    'no-inner-declarations': [2, 'functions'],
    'no-invalid-regexp': 2,
    'no-irregular-whitespace': 2,
    'no-iterator': 2,
    'no-label-var': 2,
    'no-labels': [2, {
      'allowLoop': false,
      'allowSwitch': false
    }],
    'no-lone-blocks': 2,
    'no-mixed-spaces-and-tabs': 2,
    'no-multi-spaces': 2,
    'no-multi-str': 2,
    'no-multiple-empty-lines': [2, {
      'max': 1
    }],
    'no-native-reassign': 2,
    'no-negated-in-lhs': 2,
    'no-new-object': 2,
    'no-new-require': 2,
    'no-new-symbol': 2,
    'no-new-wrappers': 2,
    'no-obj-calls': 2,
    'no-octal': 2,
    'no-octal-escape': 2,
    'no-path-concat': 2,
    'no-proto': 2,
    'no-redeclare': 2,
    'no-regex-spaces': 2,
    'no-return-assign': [2, 'except-parens'],
    'no-self-assign': 2,
    'no-self-compare': 2,
    'no-sequences': 2,
    'no-shadow-restricted-names': 2,
    'no-spaced-func': 2,
    'no-sparse-arrays': 2,
    'no-this-before-super': 2,
    'no-throw-literal': 2,
    'no-trailing-spaces': 2,
    'no-undef': 0,
    'no-undef-init': 2,
    'no-unexpected-multiline': 2,
    'no-unmodified-loop-condition': 2,
    'no-unneeded-ternary': [2, {
      'defaultAssignment': false
    }],
    'no-unreachable': 2,
    'no-unsafe-finally': 2,
    'no-unused-vars': 0,
    'no-useless-call': 2,
    'no-useless-computed-key': 2,
    'no-useless-constructor': 2,
    'no-useless-escape': 0,
    'no-whitespace-before-property': 2,
    'no-with': 2,
    'one-var': [2, {
      'initialized': 'never'
    }],
    'operator-linebreak': [2, 'after', {
      'overrides': {
        '?': 'before',
        ':': 'before'
      }
    }],
    'padded-blocks': [2, 'never'],
    'quotes': [2, 'single', {
      'avoidEscape': true,
      'allowTemplateLiterals': true
    }],
    'semi': [2, 'never'],
    'semi-spacing': [2, {
      'before': false,
      'after': true
    }],
    'space-before-blocks': [2, 'always'],
    'space-before-function-paren': [2, 'never'],
    'space-in-parens': [2, 'never'],
    'space-infix-ops': 2,
    'space-unary-ops': [2, {
      'words': true,
      'nonwords': false
    }],
    'spaced-comment': [2, 'always', {
      'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
    }],
    'template-curly-spacing': [2, 'never'],
    'use-isnan': 2,
    'valid-typeof': 2,
    'wrap-iife': [2, 'any'],
    'yield-star-spacing': [2, 'both'],
    'yoda': [2, 'never'],
    'prefer-const': 2,
    'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
    'object-curly-spacing': [2, 'always', {
      objectsInObjects: false
    }],
    'array-bracket-spacing': [2, 'never']
  }
}

更多配置还可以去官网查看 ESLint。 再添加忽略检验文件 .eslintignore

/build/
/config/
/cmas/
/node_modules/
/src/utils/
配置 webpack rules:
rules:[
 { // 加到最前面
    test: /\.(js|vue)$/,
    loader: 'eslint-loader',
    enforce: 'pre',
    include: [resolve('src')],
    options: {
      formatter: require('eslint-friendly-formatter'),
      emitWarning: true
    }
  },
  ....
]

启动项目即可啦!当然使用 VS Code 也可以安装插件 ESLint 和 Vetur,再配置下 setting.json 就可以啦:

 "eslint.autoFixOnSave": true,
 "eslint.validate": [
      "javascript",{
          "language": "vue",
          "autoFix": true
      },"html",
      "vue"
  ],

4.data选项

data 中数据尽量都和视图渲染有关 不少初学者都知道 Vue 的 data 选项可以进行双向绑定,例如绑定一个输入框,但是在实践过程中,往往一些小伙伴将 data 理解为一个仓库,一个数据库的东西,不管什么都往里面塞,即使没有界面中使用到这个变量。这样其实遇到大量数据时会有性能问题,例如有一万多个表格数据,假如界面没有使用,那么 Vue 就要为这一万多个数据进行 getter 和 setter。如果一些和渲染无关的数据,可以放到其他地方,例如全局变量和 methods 里

methods:{
getUser(){
  API.getUsers().then(()=>{
     this.allUsers = res // 这个没必要放到data里
     this.usersIsActive = this.allUsers.filter(item=>item.status) // 将需要渲染数据放到data
  })
}  
}

其实还可以使用 Object.freeze() 来冻结一些不会变化数据,这样就可以大幅度提高页面性能了,尤其在一些需要性能优化的界面中,例如大数据量表格。

<p v-for="item in list">{{ item.value }}</p>
 data: {
    // vue不会对list里的object做getter、setter绑定
    list: Object.freeze([
       { value: 1 },
       { value: 2 }
    ])
 },

需要注意的是 Object.freeze() 冻结的是值。这时仍然可以将变量的引用替换掉,还有确保数据不会变才可以使用这个语法,如果要对数据进行修改和交互就不适合使用冻结了。

5.正确认识 this

this 指向问题是 JS 中一个难点,Vue.js 的框架设计其实已经很简化了这些问题,例如 method 中 this 就是指向这个实例,我们可以使用“this.属性”或方法获取其他值,但是有时候还是会遇到 this 问题,例如:

methods:{
 getUser(){
   API.getUsers().then(function(){
      this.allUsers = res // 是读取不到this.allUsers的,this指向window
   })
 }  
}

于是不少人需要 hack 一下:

methods:{
 getUser(){
   const That = this
   API.getUsers().then(function(res){
      That.allUsers = res
   })
 }  
}

其实上面方式是傻瓜式的,我们可以并推荐使用箭头函数就可以解决的:

methods:{
 getUser(){
   const That = this
   API.getUsers().then((res)=>{
      That.allUsers = res
   })
 }  
}

单一原则,一个函数只做一件事情 任何时候尽量是的一个函数就做一件事情,而不是将各种逻辑全部耦合在一起,其实这不算 Vue 的一个技巧与原则吧,大部分软件设计都应该遵循这样的原则,只不过 Vue 的设计可以让这个原则更加地清晰。例如下面一个糟糕的例子:

  mounted(){
    this.init()
  },

  methods:{
    init(){      
      API.getSystemConfig().then(()=>{ // 获取系统配置
        // 执行其他操作
      })
      API.getUserConfig().then(()=>{ // 获取用户配置
        // 执行其他操作
      })
      API.getArticleConfig().then(()=>{ // 获取文章配置
        // 执行其他操作
      })    
    }
  }

上述代码一个 init 函数内做了三件事情,如果后续还有其他操作,例如再去调用 API 或者转换格式,这个函数将会变得很庞大而不利于维护。关键是假如这个界面用户只修改了一小部分数据,然后一般修改成功之后需要再调用接口来更新指定的值,如果再去调用 init 的话,就会出现调用其他不需要更新的接口,我们可以修改成这样:

mounted(){
    this.init()
  },
  methods:{
    init(){      
      this.getUser()
      this.getUserConfig()
      this.getArticleList()  
    },
    getUser(){
      API.getUserConfig().then(()=>{      
      })
    },
    getSystemConfig(){
      API.getSystemConfig().then(()=>{
        // 执行其他操作
      })
    },
    getArticleList(){
      API.getArticleConfig().then(()=>{
        // 执行其他操作
      })  
    }
  }

这样假如我们修改了用户后,就只需要调用 getUser() 即可了,而且方便于维护。 不要随意删减 data 中的对象属性 官网是这样说的Vue 只能双向绑定和监听现有的 data 对象里的属性,如果你往对象里添加了或删除了一些属性,这些属性是不会进行响应式的,例如:

data(){
    userInfo:{
      name:'',
      age:0 
    }
},

例如我们需要再往对象里加个性别 sex 属性,如果需要也能够进行响应式,就需要使用 set 方法:
this.set(this.userInfo,'name','') 虽然有这个 API,但是不建议使用,一般好的做法是预先声明所有需要的属性,如果有些接口需要这里面的数据的一部分即可,我们到接口参数里去处理即可,而不是来处理增减 data 里的属性。这样做可以减少一些很多 Bug。例如:

addUser(){
   const params = this.$utils.deepClone(this.userInfo) // 自己封装的深拷贝方法
   if(params.sex==='1'){ // 根据接口参数要求,到这一步去修改或删除属性
      delete params.age 
   }
   API.addUser(params).then(()=>{
   })
},

到接口这一步来拷贝一份数据来处理接口参数要求,而不要到 data 里去处理,记住 data 尽量只做视图渲染有关的数据存储,当接口和 data 里数据有不一致时,到调用那一步进行拷贝一份进行处理。

6.生命周期

created 与 mounted created 还未挂载到 DOM,不能访问到 $el 属性,可用于初始化一些数据,但和 DOM 操作相关的不能在 created 中执行;monuted 运行时,实例已经挂在到 DOM,此时可以通过 DOM API 获取到 DOM节点。所以如果界面需要计算一些页面位置或者定位相关操作最好放到 mounted 内,而一些 API 的调用或者获取 router 的 query 的操作可以放到 created 内:

created(){
const userId = this.$route.query.id || ''
  API.getUserInfo(userId).then(()=>{
  // ...
  })
}

更好的建议将异步 API 放到 mounted 内,因为 created 内获取数据过多容易导致系统白屏,对用户体验不好。 beforeDestory 这个钩子也是经常使用的,一般用于组件销毁前清除定时器,解绑事件监听操作,从而防止切换到其他页面时定时器还在作用从而可能造成内存泄漏和性能问题。

beforeDestory(){
    if(this.timer){
        clearTimeInterval(this.timer)
        this.timer = null
    }
    window.removeEventListener('scroll', this.scrollhandle);
}

computed 与 watch

在 Vue 中我们通常拿 computed 与 method 和 watch 进行对比,在开发过程中很多人会对 computed 的使用陷入误区。例如

 computed:{
    typeClass(){
      return `my-button--${this.type}`
    },
    time(){ // 没必要使用computed
      return Date.now()
    }
  },

computed 初衷是对响应式数据进行处理,例如上述例子中 type 改变,typeClass 值会自动更新,而第二个 time 内没有任何响应式数据,这个时候就没必要使用 computed 了。使用 computed 还有个好处就是它是依赖缓存的,只有 type 改变了值才会重新计算,例如经典的购物车金额根据商品数量和价格自动更新就可以使用 computed 来计算了,我推荐大家使用多使用 computed。 返回用户数据中激活状态的数据。

computed:{
    userIsActive(){
      return this.allUser.filter(item=>item.status===true)
    }
 }

再说说 watch,在开发经验中,曾见过许多新手过度使用 watch,导致 watch 滥用,首先 watch 在性能上不如 computed,简单数据监听也还行,但是遇到大数量数据或者复杂数据,可能还需要深度监听。 watch 有以下几点常见应用: 输入框实时查询,有些搜索框不需要点击来触发查询,而是搜索就会触发,于是就只能监听输入值了。

watch: {
  inpVal:{
    handler: 'getList',
    immediate: true
  }
}

配合 v-model 使用:v-model 绑定的值一般为双向的,组件内部的 value 值改变了,需要实时 emit 给外部,例如下面组件例子,一个带 v-model 的组件 search.vue:

<my-search v-model="searchValue"/>

组件内部实现:

export default {
  data(){
    inputVal:'',
  },
  props:{
    value:{
      type:String,
      default:()=>''
    }
  },
  watch:{
    value:function(newVal){ // 外部value改变实时跟新到inputVal
      this.inputVal = newVal
    },
    inputVal:function(newVal){ // 提交到外部
      this.$emit('input',newVal)
    }
  },
}

7样式维护

Vue 中样式的维护也是一个不可忽略的问题,经常会遇到以下两个问题:

  • 自己写的 CSS 与其他的样式相互冲突;
  • 想要修改第三方组件,例如 element-ui 组件样式,发现修改不了。 对于第一个问题,如果使用了 Vue-cli 可以在样式 style 标签上加上 scoped 属性,Vue 中的 scoped 通过在 DOM 结构以及 CSS 样式上加唯一不重复的标记:data-v-hash 的方式,以保证唯一(而这个工作是由 PostCSS 转译实现的),达到样式私有化模块化的目的。 例如你在组件文件写了一个样式:
.test{
 color:red
}

带有 scoped 属性会编译为:

.test [data-v-1a23ef] {
    color:red
}

这样加个唯一的 hash 以及选择器,就达到了这个样式只能在当前组件内使用。 还有为了避免一些莫名其妙的样式冲突,大家命名 CSS 时不要使用过于简单的名字,从而防止与一些公共库里或者其他人写的样式冲突,例如下面名字都不推荐:

.list
.header
.main
.title
.dialog
...

如果要修改一些公共组件的样式,或者说强制需要修改带有 scoped 属性组件内的样式,可以使用 >>> 来修改,叫做样式穿透。例如修改 element-ui 输入框宽度:

.tolist >>> .el-input {
    width 240px
}

这样 tolist 内的 el-input 宽度就为 240px。 假如你需要使得系统所有的输入框默认都是 240px 呢,在每个文件内使用样式穿透,显然不好,这样我们每个页面都要写一遍。我们可以单独写一个公共样式文件,将其在 main.js 中引入即可。 common.css:

.el-input {
    width:240px;
    border-radius:0px;
}

main.js:

import '../styles/common.css

8.vue动画

下面来说说关于 Vue 的动画,虽然官网也有介绍动画,但是很多同事并不会有意给系统加一些动态效果,如果你做的产品能够考虑到这些,无形之中就加强了交互效果,也让别人对你刮目相看,这样才是专业的前端风范,而不是抱着功能实现了就行的态度。 下面以实现一个最简单的左侧面板呼出功能为例。Vue 内置了组件 transition 来实现过渡效果。

<transition name="aside">
    <aside class="doc-aside" v-show="leftIsShow">
      123
    </aside>
 </transition>

在使用时给 transition 提供一个 name 选项,如果没有提供则会默认一个“v”,然后书写 Vue 提供的六个 class 即可

CSS:

.aside-enter
  width 0

.aside-enter-active
  transition width .5s

.aside-enter-to
  width 260px

.aside-leave
  width 260px

.aside-leave-active
  transition width .5s

.aside-leave-to
  width 0px

样式的第一个横杠 - 前必须对应 transition 中的 name。

9.组件

组件化也是 Vue 的一大核心,也是大家最经常使用的,但是很多细节还是需要大家注意的。 数据流是单向的 这点尤其重要,虽然官网反复强调,但是还是很多新手无意识地在子组件内修改了外部传入进来的值。这里有点需要说明,假如父组件传入的是一个对象,如果子组件内修改这个对象的值,那个父组件也会对应修改,因为对象或者数组类型是引用类型,在内存中是同一份,但是不能理解为通信过程就是双向了。如果值是数字或者布尔类型,那么父组件修改会反馈到子组件,子组件修改却不会反馈到父组件!!

 <doc-aside 
     class="doc-aside" 
     v-show="leftIsShow"
     :data="asideData"
     :current="current">          
 </doc-aside>
 data(){
    return {
      asideData:[
        {name:'jack',age:20},
        {name:'tom',age:21}
      ],
      current:1, // 子组件修改不会更新到父组件来的
    }
  },

所以子组件修改父组件最好还是按照官网说的使用 $emit 方法,如果子组件非要修改值的话,可以进行拷贝一份:

export default {
  props:{
    data:{
      type:Array,
      default:()=>[]
    },
  },
  data(){
    return {
      asideData:this.deepClone(this.data)
    }
  },
  methods:{
    deepClone(data){
      let obj = Array.isArray(data) ? []:{}
      for(const key in data){
        if(typeof data[key] === 'object'){
          obj[key] = this.deepClone(data[key])
        }else{
          obj[key] = data[key]
        }
      }
      return obj
    }
  }
}

data 初始化时会拷贝一份父组件数据,这样父子之间就会隔开了,父组件再修改值也不会影响子组件了,子组件修改也不会影响父组件了,因为他们拷贝后内存地址不一样。 其实这样也不好,子组件一般要接收父组件修改从而实时反馈,这个时候我们子组件内加个对 props 传入的数据进行 watch,再重新赋值给 asideData:

 watch:{
    data:function(newVal){
      this.asideData = this.deepClone(newVal)
    }
  },

当然不是任何时候都需要这么多步骤,假如子组件内只是展示数据,不会被用户手动改动数据,就不需要进行各种 watch 和拷贝了,还可以使用 computed 等。

props:{
  data:{
    type:Array,
    default:()=>[]
  }
},
computed:{
  userIsActive(){ //
    return this.data.filter(item=>item.status)
  }
}

建议父组件处理好数据格式再送入子组件,这样子组件就不需要为了单向流这么麻烦来处理数据了

插槽

插槽使用是 Vue 组件开发的一种常见方式,使用插槽可以让别人使用组件时可以自定义内容,而不是将组件写死。见过不少同事,为了应付需求一时爽,将布局和数据格式都写死了,这样后期需求变了而导致加班。例如封装一个简单的按钮组件。 方案一:

<el-button text="点击"></el-button>

方案二:

<el-button >{{text}}</el-button>

显然使用第二种方式好,第一种方式就只能传入字符串了,第二种方式支持自定义,例如后期要在按钮前加一个图标:

<el-button >
    <i class="loading"/>
    {{text}}
</el-button>

而如果你熟悉 Vue API,大牛们一般是两种方式都支持,既可以传入 text,还可以使用插槽,如果有插槽则优先显示插槽内容:

<button >
    <slot>
    {{text}}
    <slot>
</button>

如果外部没有使用插槽 slot 就当作不存在,而直接显示 text 内容。 作用域插槽:另一个高级技巧就是作用域插槽,一般的插槽里内容取的是父组件里的 data。

<todo-list>
    <span slot="title">{{title}}<span>
</todo-list>

这个插槽里 {{title}} 显示的是父组件 data 里的值,而作用域插槽呢,就是相反,可以显示子组件里的变量。

<todo-list :data="listData">
</todo-list>

listData 可能是这样数据:

 listData:[
    {id:0,title:'打篮球'},
    {id:1,title:'做菜'},
    {id:2,title:'敲代码'}
 ]

todo-list:

{{item.id}} {{item.title}}

组件 todo-list 接收到这样数据后需要布局好,再使用 v-for 渲染出来即可。

这样有个不好处,例如每一项只能定死了用 span 来渲染,而且 id 只能在 title 前面了,假如后期要加一些复杂的需求,例如 title 用 h2 标签来显示,或者加个其他其他标签,这样就又需要改组件了。所以我们需要使用作用域将其暴露出去,可以让别人自己去玩,我们只需要把数据告诉父组件就可以。 只需要将数据赋在给 slot 组件上,slot 的属性名可以自己取,可以叫 data 或者其他名字。

<div v-for="(item,index) in listData" :key="index">
   <slot :data="item">
      <span>{{item.name}}</span>
   </slot>
 </div>
<todo-list :data="listData">
  <template slot-scope="{data}"> 
     {{data.name}}
   </template> 
</todo-list>

使用 slot-scope 可以获取内部 slot 属性值 {data} 对应组件内部传入的属性名

10.数据绑定

使用 v-model 一些新手写组件时总是习惯使用属性名,然后再 @ 一个事件名称。

<el-input :value="curVal" @input="updateValue"></el-input>
updateValue(val){
    this.curVal = val
}

这样虽然实现了双向绑定,但是别人却要手动写一个 方法 updateValue 来手动赋值给 currVal,其实如果组件的 props 里定义了一个属性名叫做 value,并 $emit (“input”),满足这两个条件就可以使用 v-model 了(必须为 value 属性和 input 事件名,名字不能随便取,当然非要改要去通过 Vue 的配置里去改)。

  export default {
    name: 'Input',
    props: {
      value: { // 必须为value
        type: Number,
        default:()=>''
      }
    },
    data() {
      return {
        currValue: this.value
      }
    },
    watch: {
      value (val) {
        this.currValue = val;
      }
    },
    methods: {
      change (val) {
        this.$emit('input', this.currValue); // 必须为 input
      }
    }
  }

.sync 在说组件通信时,数据的通信是单向的,当我们给组件传入一个数字型,字符串或者布尔类型值时,子组件如果修改了这个变量,父组件是不会更新的,我们只能通过 $emit() 方式进行提交,虽然这样有利于维护数据,但是有时候我们确实需要一些双向绑定效果,最经典的就是对话框组件的显示与否了:

<el-dialog :visible.sync="showDialog"></el-dialog>

Vue.js 2.3 版本之后可以在变量后加 .sync,这样就可以通过布尔型变量 showDialog 来控制对话框显示与否了,这个过程而且会变成双向的,也不用写一个事件来手动控制了。虽然外部不需要写一个事件,但是 el-dialog 组件内一定要手动触发一下 update:visible 事件:

close(){ this.$emit('update:visible', false) }

11.其他

$attrs 一般来说,我们定义一个组件的属性是这样的:

<test :a="msg1" :b="msg2"></test>

在组件内部 props 必须写上 a 和 b:

props:{
 a:{},
 b:{}
}

假如 test 组件内又嵌套了一个组件 test2,它的属性继承了 props 中的 a 和 b,test.vue:

<template>
  <div>
    <test2 :a="a" :b="b"></test2>
  </div>
</template>
<script>
export default {
  props:{
    a:{
      type:String,
      default:()=>''
    },
    b:{
      type:String,
      default:()=>''
    }
  }
}
</script>

这样层层传递其实很麻烦,很多属性重复写了,其实每一个组件内默认情况下都有一个对象 $attrs,这个对象存储了所有定义在这个组件的属性,除了 props 里已经定义好了的,也除了 class 和 style。

<test :a="msg1" :b="msg2" c="msg3" class="test"></test>

假如 a 和 b 在组件内部 props 里声明了,则组件内部 $attrs 为:

{
  c:'msg3'
}

而且可以使用 v-bind 将 $attrs 传递给子孙组件。test2.vue:

<template>
  <div>
    <test3  v-bind="$attrs"></test3>
  </div>
</template>

这样省略了好多步骤和代码。 $listeners $attrs 存储的是组件属性集合,而 $listeners 则是存储了组件上的事件

<test @test1="showMsg1()" @test2="showMsg2()"></test>

在组件内打印一下 $listeners:

{
  test1:"showMsg1",
  test2:"showMsg2"
}

我们可以使用 v-on 将 $listeners 传递给 test 组件的子组件,test2.vue:

<template>
  <div>
    <test3  v-on="$listeners" @test3="showMsg2()"></test3>
  </div>
</template>

在 test3 内,由于自身还定义了一个事件 test3,所以 test3 内的 $listeners 一共有了三个事件了:

{
  test1:"showMsg1",
  test2:"showMsg2",
  test3:"showMsg3"
}

如果在这里去触发 test1,则最外面的 test.vue 组件则会响应, test3.vue:

this.$emit("test1",val)
test.vue:
<test @test1="showMsg1()"></test>
showMsg1(val){
    console.log(val)
}

这说明什么?这说明我们可以跨级 $emit 了,这尤其适合在一些没有依赖 Vuex 的公共组件内使用。

##小结 希望对新手们读完有所收获,后期会不间断更新