vue.js使用的人愈来愈多了,大多数的公司也在使用vue框架进行开发本身公司的产品。虽然vue.js语法比较简单,官网文档也比较详细,可是越简单的东西,越容易让人忽视细节,从而容易致使代码水平良莠不齐,风格各不相同。 对于初学者,感受通常不怎么会花时间去查看官网文档,不知道把视点落在何处。只有往后本身上班的时候碰到问题,踩到坑,加过班,苦修过,才有所领悟。 接下来,我将会对我这几年使用vue的一些经验心得进行总结,但愿能帮助你们少碰一些壁,少踩一些坑。javascript
对于一些新手,在刚刚学习vue的时候,每一次新建vue项目,都会先执行建立vue脚手架命令。究其缘由,是由于他把脚手架以及vue项目混淆了,其实,脚手架就是咱们搭建vue项目的环境,咱们能够把它看做是一个舞台,一个舞台能够有n个节目在它上面表演。一样道理,咱们只须要搭建一次脚手架就能够了,日后,咱们只须要使用建立项目的代码便可。css
在vue 里面,但凡是插件,咱们均可以尝试使用npm进行安装,因此,有时候,对于新手,就不须要从网上下载,再解压缩,而后再导入项目里面去了.html
新手在刚接触vue的时候,不建议使用ESLint,由于有可能虽然你的代码逻辑写得很正确,可是有时忘记写一个分号,程序就会出错,这会给你学习vue会大受打击。 当你可以完成一个小Demo的时候,你应该要开始重视ESLint了。在职场上,软件开发是一个团队进行,别人可能须要阅读你的代码,大的公司还须要代码审核,写的很差也容易被别人吐槽。前端
<!--好的命名,借鉴element,使用链接符链接起来,所有小写-->
<el-form label-width="160px"></el-form>
<!--不推荐,过于简单,没有实际意义-->
<list></list>
复制代码
定义组件时使用驼峰: el-formvue
props:{
labelWidth:{ // 使用驼峰
type:String,
default:()=>""
}
},
methods:{
click(){
this.$emit("handle-click") // 所有小写,使用“-”
}
}
复制代码
别人使用组件时属性则对应使用 -,并所有小写,这个是 Vue 自动处理的。而事件 @ 是不会转换的,你 $emit 里是什么名字,则别人使用时也要这么写。java
<el-form label-width="160px"></el-form>
<!--曾经看到一个高级前端相似这样命名,想打人-->
<el-form labelWidth="160px" @handleClick=""></el-form>
复制代码
虽然很简单和基础,可是仍是有些人不屑于遵照。还有一些人对于 Vue 组件属性加了 : 和没加 : 仍是有点搞不清。 例如,没有加 : 表示传入给 current 的是一个字符串:node
<todo-list current="1"></todo-list>
复制代码
加了 : 表示等号后面 "" 内是变量:webpack
<todo-list :current="current"></todo-list>
复制代码
<todo-list :current="1"></todo-list>
复制代码
若是熟悉这个就不会写出如下代码了:git
<todo-list :current="'1'"></todo-list>
<todo-list :current="quot;1quot;'"></todo-list>
复制代码
很多同窗对于 Vue 的生命周期和配置书写没有顺序,喜欢把 compnents 选项放到最下面去,watch 和 computed 也写到对象最下面,其实 Vue 页面 methods 的代码量最多。若是我想看这个页面引用了哪些组件,须要翻动很多代码,甚至可能别人不知道 components 写在下面,他也想引入组件,就可能形成 compnents 选项重复。 书写顺序参考示例,犹如阅读文章通常:es6
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"
],
复制代码
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)
}
},
}
复制代码
Vue 中样式的维护也是一个不可忽略的问题,常常会遇到如下两个问题:
.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 复制代码
下面来讲说关于 Vue 的动画,虽然官网也有介绍动画,可是不少同事并不会有意给系统加一些动态效果,若是你作的产品可以考虑到这些,无形之中就增强了交互效果,也让别人对你另眼相看,这样才是专业的前端风范,而不是抱着功能实现了就行的态度。 下面以实现一个最简单的左侧面板呼出功能为例。Vue 内置了组件 transition 来实现过渡效果。
<transition name="aside">
<aside class="doc-aside" v-show="leftIsShow">
123
</aside>
</transition>
复制代码
在使用时给 transition 提供一个 name 选项,若是没有提供则会默认一个“v”,而后书写 Vue 提供的六个 class 便可
.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。
组件化也是 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:
组件 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} 对应组件内部传入的属性名
使用 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) }
$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 的公共组件内使用。
##小结: 但愿对新手们读完有所收获,后期会对此文章不间断维护。