前言/初衷
- 最近因为遇到了很多规范性问题,感觉到深深的无奈与”愤慨“,但是由于自己的基础知识点还不够扎实,又陷入了”我只知道怎么写好的代码而不知道为什么写好的代码“的纠结,于是我决定开始第三次重新学习vue2(当然这也不会是最后一次)。这也算是对自己所学知识点的整理与归纳吧。
- 幸运的是:vue2可能就此终结,vue3还远未普及,这篇文章正好应运生于新王登基,旧帝仍在的阶段。这可能会给我的vue2知识总结复盘带来一些所谓”圆满“的意味。
- 声明:本章仅是我结合vue2风格指南,日常开发习惯,并且仅仅代表我当前阶段对于开发、vue2的理解,之中一定有着各种各样的问题,随着年限的增长与对行业的深耕,思想与思考维度都是时刻改变的。我对成长的恳切淋漓尽致,殷盼着高人与贵人的指点。
我认为规范化的核心:广泛掌握,择优而选
项目搭建
vue安装:
npm install -g @vue/cli
vue create
最好在选择配置时 选择添加路由、vuex等
搭建好环境后的开始是开发公共组件,例如:统一消息提醒模式,统一按钮模式、统一图片上传模式等
项目目录
项目目录的搭建上,我其实是有一些自己的想法的,因为我刚开始接触的是angular7,而angular7对模块化的拆分对我有很多影响,所以在我个人想法中有很多受其影响的部分,也不一定都是对的。再一点是基于我短浅的见识,曾‘有幸‘见过几个格外随意的vue2项目,所以产生了一些对规范化的执念。
下面的目录中,最主要的部分:(我的理解,并不一定好用)
1、将所有的部分,无论组件还是页面都作为文件夹命名,所有最后一层文件夹下都统一为index.vue,这样我们的所有引入都是引入到文件夹,做到统一
2、views目录基于视图:下面区分模块文件夹,每个模块文件夹中有pages和components两个子文件夹,将视图的主页面部分放入pages中,下面所有页面区分文件夹,文件夹下为Index.vue,而views下的components文件夹下是一个一个的此模块下组件。
3、public文件夹:存放公共组件、公共指令、公共管道、公共混入
4、文件命名:大驼峰,与开发者工具对应
├── README.md 项目介绍
├── index.html 入口页面
├── build 构建脚本目录
│ ├── build-server.js 运行本地构建服务器,可以访问构建后的页面
│ ├── build.js 生产环境构建脚本
│ ├── dev-client.js 开发服务器热重载脚本,主要用来实现开发阶段的页面自动刷新
│ ├── dev-server.js 运行本地开发服务器
│ ├── utils.js 构建相关工具方法
│ ├── webpack.base.conf.js wabpack基础配置
│ ├── webpack.dev.conf.js wabpack开发环境配置
│ └── webpack.prod.conf.js wabpack生产环境配置
├── config 项目配置
│ ├── dev.env.js 开发环境变量
│ ├── index.js 项目配置文件
│ ├── prod.env.js 生产环境变量
│ └── test.env.js 测试环境变量
├── package.json npm包配置文件,里面定义了项目的npm脚本,依赖包等信息
├── src 源码目录
│ ├── main.js 入口js文件
│ ├── app.vue 根组件
│ ├── public 公共组件目录
│ │ ├── components
├── ButtonProvide // 封装公共按钮组件
└── index.js
├── MessageTipProvide // 封装公共提示组件
└── index.js
│ │ ├── directives
└── index.js
│ │ ├── filters
└── index.js
│ │ ├── mixins
└── index.js
│ ├── assets 资源目录,这里的资源会被wabpack构建
│ │ └── images
│ │ └── logo.png
│ ├── routes 前端路由
│ │ └── index.js
│ ├── store 应用级数据(state)
│ │ ├── modules // 模块化
│ └── ModuleA
└──index.js
│ └── ModuleB
└──index.js
│ │ └── getters.js // 公共getters
│ │ └── index.js
│ └── views 页面目录(模块化)
│ ├── ModelA
│ ├── pages
│ ├── HelloAAA
│ └── index.vue
│ ├── HelloBBB
│ └── index.vue
│ └── components
│ ├── HelloCCC
│ └── index.vue
│ ├── HelloDDD
│ └── index.vue
│ ├── HelloEEE
│ └── index.vue
├── static 纯静态资源,不会被wabpack构建。
└── test 测试文件目录(unit&e2e)
└── unit 单元测试
├── index.js 入口脚本
├── karma.conf.js karma配置文件
└── specs 单测case目录
└── Hello.spec.js
文件规范:
- 所有的组件名都使用***大驼峰多个单词命名*** 例如:AbcInfo.vue,这样符合了vue2的开发者工具命名,也符合了与HTML原生标签区分开来的思想(这种思想始于react的自定义组件命名规则)
- 所有的组件引用都使用组件命名引用,单标签大驼峰(虽然我们知道引用时可以重命名,但如果各个都重命名对后期维护的成本消耗式巨大的)例如:AbcInfo.vue , import Abc from “…/Abc.vue”(结合风格指南中提到的,通常我们在写Vue.component()时才会进行小写横线连接的命名)
- 文件内容规范:
项目语法规范:
- 1、插值语法:{{}}、拼接语法:
${}
- 2、指令语法:v-bind 统一采用简写(冒号形式:) v-on 统一采用(@形式)
- 3、data有两种写法:对象形式、函数形式,统一采用函数形式,以避免对象形式带来的数据关联关系(vue3中也取消了data的对象形式)并且使用函数的简写形式data(){}来定义
- 4、不要将函数写在data中,data中可以写函数,但是data中的数据会进行数据代理,将函数放在data中会多出来函数的数据代理,是无用的,会导致vue很累
模板语法、计算属性computed、监视watch 的选择
- 模板中复杂的数据要放到计算属性中,减少模板计算的付出,计算属性就是用来优化模板中数据改变的监听的,要灵活使用
- 所有的计算属性都可以用watch来写,但是当computed可以实现的,选择computed而不是watch
- computed是不可以开启异步任务的,但watch可以,所以异步的情况选择watch无可厚非
模板规则、模板属性规则
- vue2的template标签下都包含着一个div, 这样的设计实际上也是为了避免一些组件中标签不统一的问题,例如:给组件添加一个@click事件,如果组件下有两个根标签,那这个click事件添加给谁呢,(vue3使用Fragment标签解决了这一问题)
- 不影响结构,会清除掉,所以这也就是为什么组件最外层会被template包裹的原因,template不能和v-show一块使用,但是可以与v-if v-for一块使用,所以当我们不想添加额外标签时使用标签
- v-for 和v-if不要用在同一个标签上,这里就可以借助template标签
- v-for需要配合key=""使用,值为唯一值,当对数据没有增删操作时才可以使用下标作为key,(如果使用下标操作,当我们要去删除数组第一项时,会删除最后一项,这就是因为虚拟dom渲染、diff算法的问题),另外v-for是可以遍历对象的
- .number .lazy .trim的使用是通常会遗忘的
- v-cloak 可以配合css属性来解决样式的展示,[v-cloak]:{display:none}
样式规则
- 样式库采用分别暴露的形式:
- 样式修改原则:优先写外部样式的,不写行内样式(不要让模板变得臃肿,不易于维护)
- css、less、saas样式的选择:尽量使用less,无论选择哪种结构,整个项目尽量使用统一结构
- 行内、内联样式、外联样式的选择:在使用组件内的
数据处理规范
- 数组的处理使用如果想要响应式:push pop shift unshift splice sort reverse,因为vue2重写了此7种方法,使其拥有了响应式
- 。。。。。太多了容我再想想有没有更好方式整理
更新dom规范
-
n
e
x
t
T
i
c
k
的
使
用
:
t
h
i
s
.
nextTick 的使用:this.
nextTick的使用:this.nextTick(()=>{})
vue在执行代码时遵从的原则是:将代码执行完才修改页面dom,所以在下一行代码需要上一行代码执行dom操作完成后才运行时,可以使用$nextTick - dom没更新是否因为响应式缺失:优先处理响应式缺失的问题,$set的添加最后考虑
vue2无法检测数据变化有四种情况:对象中属性的新增与删除,数组索引位置数据变化,通过length改变数组- 数组使用vue重写的7种改变数组的方法(来替代数组length变化与索引位置数据的变化)
- 对象的属性添加或删除使用应尽量避免,无法避免则使用this.$set(obj,“属性”,“属性值”) 来替换
- 强制更新 f o r c e U p d a t e ( ) 极 少 情 况 需 要 强 制 更 新 , 尽 量 不 使 用 即 使 更 新 也 指 定 区 域 更 新 : ‘ t h i s . forceUpdate() 极少情况需要强制更新,尽量不使用 即使更新也指定区域更新:`this. forceUpdate()极少情况需要强制更新,尽量不使用即使更新也指定区域更新:‘this.refs.table.$forceUpdate()`
自定义指令规范
- 指令名称统一使用全小写命名,多个单词使用 短杠连接(-)
- 通常在我们没有特殊需求时,通常以第三种方式编写指令,因为通常指令创建的目标是解决一系列问题,我们也要用 ”一个指令去解决一系列问题“ 的思想来编写指令,所以我们尽量放入公共指令中,一般写成函数形式,结合特殊情况才会将生命周期拆分来写。
第1种:组件中
directives:{
// 函数形式为bind和update的合并写法,缺少了inserted
big(el,binding){
// el: 元素本身
// binding 传入值的集合
}
}
第2种:组件中
directives:{
abc:{
bind(){} // 指令与元素成功绑定时调用
inserted(){} // 指令所在元素插入到页面时
update(){} // 指令所在的模板被重新解析时
componentUpdated(){}// 指令所在组件的 VNode 及其子 VNode 全部更新后调用
unbind(){} // 解绑时调用
}
}
第3种:公共
Vue.directive('abc'(el, binding, vnode)=>{
// el: 元素本身
// binding 传入值的集合
// vnode:vue生成的虚拟节点
})
第4种:公共
Vue.directive('abc'{
bind(){}
inserted(){}
update(){}
})
自定义管道规范
自定义管道是我特别喜欢写的,但是我很不理解为什么vue3开始将管道剔除了,用computed和methods的使用来替代管道
- 使用小驼峰命名自定义filter
- 使用模板中:
{{msg | filterName('参数1','参数2')}}
- 组件中创建filter:
filters:{
filterName(item) {
return item+10;
},
},
- 公共filter: 一般的日期处理建议放到其中
在这里插入代码片
Vue.filter('dataTime', (data) => {
if (!data) return '';
return moment(Number(data)).format(
"YYYY-MM-DD HH:mm:ss"
);
});
mixin混入规范
混入的使用如果不规范,将会使代码的维护变得困难。
- 混入文件中,使用分别导出 export const handle = {}
- 混入文件和原组件都有的属性,以原组件为主,但是生命周期钩子将会都执行,并且混入的在前,原组件在后
父子组件间传值规范
父->子:
通常使用第一种
-
:aaa=”传的值“ props:{aaa:{}} prop中的数据采用结合风格指南中的例子:通常使用第二个。
另外props使用时,组件内时不允许修改值的,虽然检测方式是浅层检测,可以修改对象内的值,但是为了规范,制定不修改props的原则 -
ref方式 this.$refs.组件内定义好的属性
props: {
status: {
type: String,
required: true,
validator: function (value) {
return ['syncing'].indexOf(value) !== -1
}
default(){
return ""
}
}
}
props: {
status: {
type: String,
required: true,
default:""
}
}
子->父:
通常我们使用第二种方式:自定义事件
- props方式 传递一个函数给子组件: :addTodo=“addTodo” 通过props接收此函数在需要时调用
- 自定义是事件方式 v-on的方式 也就是简写@addTodo=“addTodo” 子组件通过this.$emit(“addTodo”,data)发射事件
- ref方式 this. r e f . s t u d e n t R e f . ref.studentRef. ref.studentRef.on(“事件名”,this.getStrdent) 灵活性更强一些 注意后面的触发方法可以在methods中定义,也可以写成箭头函数的形式,但是不能写成function的形式,因为事件会将this付给后面的回调函数,里面的this会指向子组件的vc
非父子组件间传值
- 简单兄弟组件:状态提升:数据存放位置提升到统一父组件中,然后向下传,(并不常用)
- 路由传参,通过路由传递参数 (跳转页面 传少量值时,优先使用)
- 全局事件总线(推荐):利用
VueComponent.prototype.__proto__ ==Vue.prototype
将全局事件绑定到一个固定属性上( b u s ) , 在 所 有 组 件 中 就 都 可 以 找 到 bus),在所有组件中就都可以找到 bus),在所有组件中就都可以找到bus了,注意:销毁是必要的
new Vue({
el:"#app",
render:h=>h(app),
// vm要在new Vue 创建后才有,但是创建完成后意味着代码执行完成,所以要放到new Vue() 的beforeCreate() 声明周期中,刚创建完成时调用
beforeCreate(){
Vue.prototype.$bus = this // 安装全局事件总线 $bus就是当前应用的vm
}
})`
this.$bus.$on() // 监听
this.$bus.$emit() // 调用
beforeDestroy(){ // 销毁绑定的事件
this.$bus.$off("名称")
}
-
消息的订阅与发布:需要安装插件 推荐:pubsub-js 使用publish
subscribe发布与订阅消息(当项目中很多处简单的传递时可以使用,如果少有需要的功能没有必要使用),另外:也需要组件销毁时解除绑定pubsub.unsubscribe(this.publd) -
vuex (vue风格指南推荐,优先级优于全局事件总线/消息订阅与发布,但不优于路由传参)是否是多于两处需要使用此数据,并且此数据为全局的关键数据,存储的意义大不大
vue动画、过渡规范
css3提供了优秀的动画写法,vue也提供了动画的标签与解决方案,说实话我也没搞懂两者到底使用哪一个会比较好?所以仅供参考,留待之后的学习(也像echart和v-chart到底选择哪一个,原因是什么我还没有理解到位)
对于toC端的项目,对样式的要求严格,并且需求量大,我们统一采用成型的第三方动画库: animate.style
npm install animate.css
// 基本使用:
name="库中的name"
enter-active-class=""
leave-active-class=""
对于toB端的项目,如果对样式的要求停留在能看就行,但是最好有:
配置代理服务器规范
axios规范
插槽规范
插槽的选择:组件的封装过程中,需要不同的组件内部展现形式,而且这种展现是随机的。(类似于elementui中使用来重新定义组件内容),我们没办法在组件内通过v-if列举所有可能,并且这也是不好的,所以与其列举不如放开组件内的一个位置,让外部传入的内容添加到此处。更灵活,更易用。
三种插槽方式:建议统一使用具名插槽,当需要子传父值的时候选择作用域插槽
- 默认插槽:
标签体内直接写内容:<hello><img></hello>
hello组件中:<slot>等待数据加载...</slot>定义插入位置,slot标签体中的内容放的为默认值
样式:可以放在slot里也可以写在slot外,这样也增加了维护的难度,所以不建议使用默认插槽
- 具名插槽
放的位置增加name:
<slot name="img"></slot>
使用插槽增加slot:
<hello>
<img slot="img">
</hello>
建议使用<template>包裹:(vue2.6后可使用v-slot:img代替slot="img",只用于template标签)
<hello>
<template slot="img">
<img/>
</ template>
</hello>
- 作用域插槽:反向传输数据给父组件,可以在父组件中使用插槽时调用子组件的数据
在这里插入代码片
放的位置:通过games将数据传递
<slot :games="games"></slot>
使用插槽:通过scope获取到数据 或者写成slot-scope
<hello>
<template scope="data">
{{data.games}} // data:{games:""}
</template>
</hello>
路由规范
route:路由 每个路由的详情与参数
router:路由器,整个应该只有一个路由器
路由配置概览
// 使用导航
<router-link to="/aaa" active-class="active"></router-link>
// 展示位置
<router-view></router-view>
export default new VueRouter({
mode:'history',// 通常web应用我们都会使用history模式而不是hash模式,让后端处理识别路由
routes:[
{
name:"about",
path:"/about",
meta:{isAuth:false}, // 元信息,可以配置标志位用于路由守卫中做权限处理等问题
// 统一按照vue-router官网建议,使用懒加载方式配置
component:()=>import('路径'),
children:[
{
path:'child1',
component:()=>import('')
}
]
}
]
})
路由守卫 3种守卫 1种周期
- 全局前置路由守卫
初始化的时候被调用、每次路由切换之前被调用
// 接收三个参数:to 去哪的路由对象,from来自哪的路由对象,next() 放行
router.beforeEach((to,from,next)=>{
next() //必须通过next() 才能跳转到下一步
})
- 后置路由守卫
路由切换后执行:两个参数
用途:可以用于切换页面标签 meta:{title:“主页”},无需在前置路由守卫中判断是否跳转
router.afterEach((to,from)=>{
document.title = to.meta.title || "首页" //并且需要把配置中的title也修改
})
- 独享路由守卫
某一个路由独享的
beforeEnter((to,from,next)=>{})
- 组件内路由生命周期
通过路由规则,进入该组件时被调用
beforeRouteEnter(to,from,next){}
刷新路由:
beforeRouteUpdate(to,from,next){}
离开组件时调用:
beforeRouteLeave(to,form,next){}
vuex规范
vuex风格指南:
应该优先通过 Vuex 管理全局状态,而不是通过 this. r o o t 或 一 个 全 局 事 件 总 线 。 通 过 t h i s . root 或一个全局事件总线。 通过 this. root或一个全局事件总线。通过this.root 和/或全局事件总线管理状态在很多简单的情况下都是很方便的,但是并不适用于绝大多数的应用。
- 是否是多于两处需要使用此数据,并且此数据为全局的关键数据
- 路由传参、全局事件总线传递是否优于vuex
store中的属性命名:mutations方法中的参数统一使用全大写字母命名,其他方法中的方法名、属性名统一按照小驼峰命名
注意区分项目大小:不是只有一两个页面的直接无脑选2,便于扩展
1、当项目很小,没有模块化vuex时:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
// 存放字段(使用小驼峰命名)
state = {
userName: ''
},
mutations = {
// 全大写命名,接收两个参数(state对象,改变的值)
SET_USER_NAME: (state, value) => {
state.userName = value;
}
},
// 重复使用的API调用
actions = {
// context,上下文中是一个简化的store,里面包含着commit的上下文方法,所需的数据
// context.commit("方法",value),利用commit调用mutations里面的方法,如果不需要其他方法可以结构赋值 {commit}
setUserName(context, value) {
context.commit("SET_USER_NAME", value)
}
},
// vuex的计算属性,可以调用里面的方法二次更新state数据(state,其他的getters)
getters = {
setUserData(state, getters) {
return state.userName + "很帅";
}
}
})
调用时:
// 获取数据
this.$store.state.userName
// 直接修改数据
this.$store.conmmit("SET_USER_NAME",value)
// 通过action修改数据
this.$store.dispatch("setUserName",value)
// 通过getter获取修改后的数据
this.$store.getters.getData();
2、当项目很大时,使用模块化的vuex存储数据
// 模块化 配置store的index.js
import Vue from 'vue'
import Vuex from 'vuex'
import getters from './getters'
import system from './modules/system' // system模块
Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
system, // 模块化命名
},
getters // 公共getters
})
export default store
modules目录下每个模块vuex文件中:
const state = {};
const getters = {};
const mutations = {};
const actions = {};
export default {
namespaced: true, // 开启命名空间
state,
mutations,
actions,
getters
};
调用时:
直接的方式
// 获取数据
this.$store.state.模块名.userName
// 直接修改数据
this.$store.conmmit("模块名/SET_USER_NAME",value)
// 通过action修改数据
this.$store.dispatch("模块名/setUserName",value)
// 通过getter获取修改后的数据
this.$store.getters["模块名/getData"];
统一化的方式:
// 数据获取: 放在computed中 this.a调用
...mapState('a', ['a','b','c']) 模块名,[字段名]形式
...mapGetters('b',['a','b'])
// 数据提交:放在methods, this.a(传参) 调用
...mapMutations('a',['a','b'])
...mapActions('b',['a','b'])