用vue全家桶开发一年多了,踩过很多坑,也解决了不少的问题,把其中的一些点记录下来,但愿能帮到你们。如下内容基于最新版的vue + vuex + vue-router + axios + less + elementUI,vue脚手架是vue-cli3。javascript
vue 为了防止 css 污染,当组件的 <style>
标签有 scoped
属性时,它的 css 只做用于当前组件中的元素。实现原理很简单,给当前组件中的每一个标签都加上惟一的自定义属性:data-v-惟一的属性
,而后 css 选择器都加上属性选择器.article-title[data-v-惟一的属性]
,这样这个 css 只会匹配到当前页面的这个元素。php
注意:每一个组件的最外层的标签会带上父组件的data-v-
属性,也就是这个标签会被父组件的样式匹配到,因此父组件尽可能不要使用标签选择器,这个标签不要使用父组件中的 id 或者 class。css
在父组件想修改子组件的css(修改elementUI组件的样式),咱们能够借助深度做用选择器 >>>
html
div >>> .el-input{
width: 100px;
}
/* sass/less的话可能没法识别,这时候须要使用 /deep/ 选择器。 */
div /deep/ .el-input{
width: 100px;
}
复制代码
深度做用选择器会去掉后面元素的属性选择器[data-v-]
,即上面代码会编译成:div[data-v-12345667] .el-input{}
。就能够匹配到子组件的元素,从而覆盖样式。vue
组件的生命周期钩子函数是到了某个生命周期点就会触发,而不是在这个钩子函数中进行生命周期,好比说DOM加载好了,就会触发mounted
钩子函数,因此在created
里面写一个延迟定时器,mounted
钩子不会等定时器执行。java
各个周期钩子函数触发的时间点参考(图来源于网络)ios
关于父子组件的生命周期:不一样的钩子函数有不一样的表现。父组件的虚拟 DOM 先初始化好了(beforeMount
),才会去初始化子组件的虚拟 DOM (beforeMount
),而 mounted
事件,等价于 window.onload
,子组件 DOM 没加载好,父组件 DOM 永远不可能加载好。因此基本生命周期钩子函数执行顺序是:父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mountedvue-router
父子组件的update和beforeUpdate执行前后顺序:数据修改+虚拟DOM准备好会触发beforeUpdate,换句话说beforeUpdate等价于beforeMount,而update等价于mounted。因此前后顺序是: 父beforeUpdate -> 子beforeUpdate -> 子update -> 父update。vuex
同理beforeDestory
和destoryed
的前后顺序是:父beforeDestory -> 子beforeDestory -> 子destoryed -> 父destoryed。vue-cli
生命周期钩子函数其实也能够写成数组的形式:mounted: [mounted1, mounted2]
,同一个生命周期能够触发多个函数,这也是mixin
(混入)的原理,mixin
里面也能够写生命周期钩子,最终会和组件里面的生命周期钩子函数一块儿变成数组形式,mixin
里面的钩子函数会先执行。
不少人以为在 created
事件里面把数据请求到,而后一块儿生成虚拟 DOM,再渲染会更好。实际上呢,请求是须要时间的,并且这个时间具备不稳定性,极可能 vue
的虚拟 DOM 准备好了,你的数据才请求到,而后又得更新一遍虚拟 DOM,再渲染,极大地延长了白屏时间,用户体验很很差。而在 mounted
事件请求数据呢,静态页面会先渲染好,等数据好了,再更新部分 DOM 便可。
补充:经掘友指出,这里理解有误,抱歉。
生命周期钩子函数的中异步会放入事件队列,而不会在这个钩子函数中执行。也就是说你在 created
和 mounted
中请求数据是同样的,都不会当即更新数据,因此不会致使虚拟DOM从新加载,也不影响页面中静态的部分加载。生命周期钩子函数中的异步赋值,vue会在一遍流程走完以后执行update
。另外,给数据赋值而后更新 DOM 也是异步的,侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的全部数据变动,去掉重复赋值而后更新。
生命周期钩子函数中的异步行为测试:
export default {
data(){
return {
list:[],
}
},
methods:{
getData(){
//生成指定范围的随机整数
const randomNum = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
//生成固定长度的非空数组
const randomArr = length => Array.from({ length }, (item, index) => index * 2);
const time = randomNum(100,3000);//模拟请求时间
console.log('getData start');
return new Promise(resolve => {
setTimeout(() => {
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');
}
}
复制代码
结果以下图,因此在 created
中和 mounted
中请求数据,数据的更新时间是同样的,在 created
中发起请求,能够更早的请求到数据。而且使用服务端渲染SSR的时候, mounted
钩子不会加载。
能够写自定义事件,而后在子组件的生命周期函数中触发这个自定义事件,可是不优雅,咱们可使用 hook:
<child @hook:created="childCreated"></child>
复制代码
从 A 页面切换到 B 页面,A 页面中有一个定时器,到了 B 页面用不上,须要在离开 A 页面的时候清除掉,办法很简单,在 A 页面的生命周期钩子函数beforeDestory
或者路由钩子函数beforeRouteLeave
里面清除掉就行,可是问题来了,怎么拿到定时器呢?把定时器写到 data
里面,可行可是不优雅,咱们有以下写法:
//在初始化定时器以后
this.$once('hook:beforeDestory',()=>{
clearInterval(timer);
})
复制代码
因为 HTML 标签的限制,tr 标签里面只能有 th, td 标签,而写自定义标签则会被解析到 tr 标签外层,因此这时候咱们能够用 is 属性
<tr>
<td is="child">
</tr>
复制代码
最近有个页面有大量的 SVG 图标,我将每个 SVG 都写成了一个组件。因为 SVG 组件名称又各不相同,因此须要动态标签来表示:
<!-- 假设咱们的数据以下 -->
arr: [ { id: 1, name: 'first' }, { id: 2, name: 'second' }, { id: 3, name: 'third' }, ]
<!-- 原本须要这样写 -->
<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>
<!-- 其实这样写更优雅 -->
<div v-for="item in arr" :key="item.id">
<p>item.name</p>
<component :is="'svg'+item.name"></component>
</div>
复制代码
原生 DOM 事件绑定的函数的第一个参数都会是事件对象event
,可是有时候咱们想给这个函数传其余的参数,直接传会覆盖掉event
,咱们能够这么写<div @click="clickDiv(params,$event)"></div>
,变量$event
就表明事件对象。
若是要传的变量不是事件对象呢?在使用 elementUI
的时候碰到这么一个状况,在表格中使用了下拉菜单组件,代码以下:
<el-table-column label="日期" width="180">
<template v-slot="{row}">
<el-dropdown @command="handleCommand">
<span>
下拉菜单<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="a">黄金糕</el-dropdown-item>
<el-dropdown-item command="b">狮子头</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</el-table-column>
复制代码
下拉菜单事件 command
函数自带一个参数,为下拉选中的值,这个时候咱们想把表格数据传过去,若是@command="handleCommand(row)"
这样写,就会覆盖掉自带的参数,该怎么办呢?这时候咱们能够借助箭头函数:@command="command => handleCommand(row,command)"
,完美解决传参问题。
顺便说一下,elementUI
的表格能够用变量$index
表明当前的列数,和$event
同样的使用:
<el-table-column label="操做">
<template v-slot="{ row, $index }">
<el-button @click="handleEdit($index, row)">编辑</el-button>
</template>
</el-table-column>
复制代码
经掘友指点,默认参数有多个(或者未知个数)的时候,能够这样写:@current-change="(...defaultArgs) => treeclick(ortherArgs, ...defaultArgs)"
v-slot
的用法(slot 语法已经废弃):至关于在组件中留一个空位,使用该组件的时候能够传一些标签过去,插入到对应的空位。能够有多个空位,取不一样的名字便可,默认是 default
。同时还能够将一些数据传过去,简写是#
。
<!-- 子组件 -->
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
<!-- 父组件 -->
<base-layout>
<!-- 插槽能够简写为# -->
<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>
<!-- 可使用解构 -->
<template #footer="{ user }">
<p>Here's some contact info</p>
</template>
</base-layout>
复制代码
总结:
template
标签。<child :data="data">
<div>在这里访问不到data数据</div>
</child>
复制代码
v-slot="{ user }"
。v-model
在使用的时候很像双向绑定的,可是 Vue 是单项数据流,v-model
只是语法糖而已:父组件用v-bind
将值传给子组件,子组件经过 change/input 事件触发修改父组件的值。
<input v-model="inputValue" />
<!-- 等价于 -->
<input :value="inputValue" @change="inputValue = $event.target.value" />
复制代码
v-model
不只仅能在 input
上用,在组件上也能使用。
vue 组件间传递数据是单向的,即数据老是由父组件传递到子组件,子组件在其内部能够有本身维护的数据,但它无权修改父组件传递给它的数据,咱们也能够参照v-model
语法糖进行修改父组件的值,可是每次都这样写太麻烦了,vue 提供了一个修饰符.sync
,用法以下:
<child :value.sync="inputValue"></child>
<!-- 子组件 -->
<script> export default { props: { //props能够设置值得类型,默认值,是否必传以及校验函数 value: { type: [String, Number], required: true, }, }, //用一个变量中转,子组件中就用_value就不会直接修改父组件的值 computed: { _value: { get() { return this.value; }, set(val) { this.$emit('update:value', val); }, }, }, }; </script>
复制代码
虽然 vue
提供 $parent
和 $children
来访问父/子组件,可是组件的父组件/子组件存在不少不肯定性,例如组件被复用,他的父组件有多种状况。咱们能够经过 ref 访问到子组件的数据和方法。
<child ref="myChild"></child>
<script> export default { async mounted() { await this.$nextTick(); console.dir(this.$refs.myChild); }, }; </script>
复制代码
注意:
ref
必须等 DOM 加载好了才能够访问mounted
生命周期 DOM 已经加载好了,可是为了以防万一,咱们可使用 $nextTick
函数在用 Webpack
处理打包时,可将某一目录配置一个别名,代码中就能使用与别名的相对路径引用资源
import tool from '@/utils/test'; // Webpack 能正确识别并打包。
复制代码
可是在 css
文件,如 less, sass, stylus 中,使用 @import "@/style/theme"
的语法引用相对 @
的目录确会报错。 解决办法是是在引用路径的字符串最前面添加上 ~ 符号。
@import "~@/style/theme.less"
background: url("~@/assets/xxx.jpg")
<img src="~@/assets/xxx.jpg">
咱们先来看一个完整的 URL:https://www.baidu.com/blog/guide/vuePlugin.html#vue-router
。其中 https://www.baidu.com
是网站根目录,/blog/guide/
是子目录,vuePlugin.html
是子目录下的文件(若是只有目录,没有指定文件,会默认请求index.html
文件),而#vue-router
就是哈希值。
vue 是单页应用,打包以后只有一个 index.html
,将他部署到服务器上以后,访问对应文件的目录就是访问这个文件。
hash 模式:网址后面跟着 hash 值,hash 值对应每个 router
的名称,hash
值改变意味着router
改变,监听 onhashchange
事件,来替换页面内容。
history 模式:网址后面跟着‘假的目录名’,其值就是 router
的名称,而浏览器会去请求这个目录的文件(并不存在,会 404),因此 history
模式须要服务器配合,配置 404 页面重定向到到咱们的 index.html
,而后 vue-router
会根据目录的名称来替换页面内容。
优缺点:
onhashchange
事件切换路由,兼容性会好一点,不须要服务器配合HTML5
的 history API
,兼容性差一点。二者的配置区别在于:
const router = new VueRouter({
mode: 'history', //"hash"模式是默认的,无需配置
base: '/',//默认配置
routes: [...]
})
复制代码
vue-cli3 的 vue.config.js 配置:
module.exports = {
publicPath: "./", // hash模式打包用
// publicPath: "/", // history模式打包用
devServer: {
open: true,
port: 88,
// historyApiFallback: true, //history模式本地开发用
}
}
复制代码
若是是网站部署在根目录,router
的 base
就不用填。若是整个单页应用服务在 /app/
下,而后 base
就应该设为 "/app/"
,同时打包配置(vue.config.js
)的 publicPath
也应该设置成/app/
。
vue-cli3
生成新项目的时候会有选择路由的模式,选择history
模式就会帮你都配置好。
钩子函数分三种:组件内钩子,全局钩子,路由独享钩子。
APP.vue
没有组件内钩子函数,由于APP.vue
是页面的入口,这个组件是一定会加载的,而使用组件内钩子函数能够阻止组件加载。
全局钩子主要用于路由鉴权,可是消耗很大。组件内的钩子beforeRouteLeave
主要用于用户离开前的提示(好比说有未保存的文章),这个钩子有一些坑:hash模式下,浏览器的后退按钮没法触发这个钩子函数。同时咱们还能够监听用户的关闭当前窗口/浏览器事件:
window.onbeforeunload = e => "肯定离开当前页面,你的修改将不会被保存!";
复制代码
为了防止恶意网站,用户关闭窗口/浏览器事件是不可阻止的,只能提示,并且不一样的浏览器兼容性也不一样。
Vuex 中的数据,刷新页面以后就会丢失。要实现持久化存储须要借助本地存储(cookie 和 storage 等),通常是登陆以后返回的数据(角色,权限,token 等)须要存储到 Vuex,因此咱们能够在登陆页将数据存储到本地,而在主页面(除了登陆页,其余全部页面的入口)进入以前(beforeCreate
或者路由钩子 beforeRouteEnter
)读取出来,并提交到 Vuex
就行了。这样即便刷新,也会触发主页面的进入钩子函数,会被提交到 Vuex
。
beforeRouteEnter (to, from, next) {
const token = localStorage.getItem('token');
let right = localStorage.getItem('right');
try{
right = JSON.parse(right);
}catch{
next(vm => {
//弹窗采用elementUI
vm.$alert('获取权限失败').finally(() => {
vm.$router.repalce({name:'login'})
})
})
}
if(!right || !token){
next({name:'login',replace:true})
}else{
next(vm => {
//这里面的事件会在mounted以后触发
vm.$store.commit('setToken',token);
vm.$store.commit('setRight',right);
})
}
}
复制代码
beforeRouteEnter
的回调会在mounted
钩子以后触发,这就比较蛋疼了。而主页面的mounted
会在全部子组件的mounted
以后触发,因此咱们能够这样写。
import store from '^/store';//将实例化的store引入进来
beforeRouteEnter (to, from, next) {
const token = localStorage.getItem('token');
if(!token){
next({name:'login',replace:true})
}else{
store.commit('setToken',token);
next();
}
}
复制代码
要想实现数据修改以后仍能持久化存储,咱们能够先把数据存到localstorage
,而后监听window.onstorage
事件,数据有修改提交到Vuex
。
mutations
是同步修改 state
的值,假如另外一个值是异步获取(action
)的,依赖于这个同步的值的修改,须要在 mutations
里面赋值以前触发 action
里面的事件,咱们能够给实例化的 Vuex
命名,在 mutations
里面拿到 store
对象。
const store = new Vuex.Store({
state: {
age: 18,
name: 'zhangsan',
},
mutations: {
setAge(state, val) {
// 假如age变化了以后,name也要跟着变化
// 须要在每次给age赋值的时候,同步触发action里面的getName
state.age = val;
store.dispatch('getName');
},
setName(state, val) {
state.name = val;
},
},
actions: {
getName({ commit }) {
const name = fetch('name'); //从接口异步获取
commit('setName', name);
},
},
});
复制代码
若是项目很小,不须要用到 vuex
,能够用Vue.observable
来模拟一个:
//store.js
import Vue from 'vue';
const store = Vue.observable({ name: '张三', age: 20 });
const mutations = {
setAge(age) {
store.age = age;
},
setName(name) {
store.name = name;
},
};
export { store, mutations };
复制代码
get 请求的数据放在 url 里面,相似于http://www.baidu.com?a=1&b=2
,其中a=1&b=2
就是 get 的参数,而对于 post 请求,参数放到 body 里面,经常使用的数据格式有表单数据和 json 数据,二者的差别就是数据格式不一样,表单数据编码格式和 get 同样,只不过是放在 body 里面,而 json 数据则是 json 字符串
qs 基本使用:
import qs from 'qs'; //qs是axios里面自带的,因此直接引入就能够了
const data = qs.stringify({
username: this.formData.username,
oldPassword: this.formData.oldPassword,
newPassword: this.formData.newPassword1,
});
this.$http.post('/changePassword.php', data);
复制代码
qs.parse()
是将 URL 解析成对象的形式,qs.stringify()
是将对象 序列化成 URL 的形式,以&进行拼接。而对于不一样的数据格式,axios 会自动设置对应的content-type
,不须要手动设置。
application/x-www-form-urlencoded
multipart/form-data
application/json
碰到过一次接口须要我用表单传一个数组。假设数据是arr = [1,2,3]
若是直接使用 qs.stringify(),则数据会变成arr[]=1&arr[]=2&arr[]=3
,很容易看出来,多了一个[]
,让接口把参数名改为arr[]
就能用,可是这样很差。不过能够发现,表单传数组的本质就是同名参数传屡次,这时候咱们也能够这样:
const data = new FormData();
arr.forEach(item => {
data.append('arr', item);
});
复制代码
测试一下,完美解决,可是事情到这里还没完,翻一下qs 官方文档,qs 转换支持第二个参数,完美解决咱们的问题。
const data = qs.stringify(arr, { arrayFormat: 'repeat' }); // arr=1&arr=2&arr=3
复制代码
const valid = await new Promise(resolve => this.$refs.form.validate(resolve));
if (!valid) return
复制代码
.el-cascader-menu > .el-scrollbar__wrap{
height: 250px;
}
复制代码
v-if="true"
。height="100%"
<div class="table-wrap">
<el-table :height="100%"></el-table>
</div>
复制代码
/* less写法 */
.table-wrap{
height: calc(~"100vh - 200px");
/* 部分版本这样写会失效,须要加上下面一句 */
/deep/ .el-table{
height: 100% !important;
}
}
复制代码
upload
组件,须要将这些文件一块儿上传到服务器。能够经过this.$refs.poster.uploadFiles
拿到文件对象。而后本身手动组装成表单数据。<el-form-item label="模板文件:" 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>更新文件</el-button>
</span>
<el-button slot="trigger" size="mini" type="success" v-else>上传文件</el-button>
</el-upload>
</el-form-item>
<el-form-item label="模板海报:" 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="点击上传海报" alt="资源海报" width="250" height="140">
<template #tip>
<div>tips: 建议上传尺寸250*140</div>
</template>
</el-upload>
</el-form-item>
复制代码
methods:{
//选择图片以后替换旧图片和显示略缩图
changePhoto(file, fileList) {
//建立的Blob URL可直接预览图片
this.previewUrl = window.URL.createObjectURL(file.raw);
if (fileList.length > 1) {
fileList.shift();
}
},
revokeUrl(e) {
//图片加载完成以后销毁Blob URL
if (e.target.src.startsWith("blob:")) window.URL.revokeObjectURL(e.target.src);
},
//提交表单数据
async submitData() {
const template = this.$refs.template.uploadFiles[0], //模板文件
poster = this.$refs.poster.uploadFiles[0], //海报文件
formData = new FormData();
if (!template) return this.$message.warning("必须选择模板文件");
if (!poster) return this.$message.warning("必须选择海报文件");
formData.append("zip", template.raw);
formData.append("poster", poster.raw);
const res = await this.$http.post('url', formData);
},
}
复制代码
VueI18n
国际化,须要将elementUI
的语言包和项目中的语言包合并成一个。import VueI18n from "vue-i18n";
import zhLocale from './locales/zh.js';/* 引入本地简体中文语言包 */
import zhTWLocale from './locales/zh-TW.js';/* 引入本地繁体中文语言包 */
import enLocale from './locales/en.js';/* 引入本地英语语言包 */
import zhElemment from 'element-ui/lib/locale/lang/zh-CN'//引入elementUI简体中文语言包
import zhTWElemment from 'element-ui/lib/locale/lang/zh-TW'//引入elementUI繁体中文语言包
import enElemment from 'element-ui/lib/locale/lang/en'//引入elementUI英语语言包
Vue.use(VueI18n);
const messages = {//语言包
zh: Object.assign(zhLocale, zhElemment),//本地语言包加入elementUI的语言包
'zh-TW': Object.assign(zhTWLocale, zhTWElemment),//本地语言包加入elementUI的语言包
en: Object.assign(enLocale, enElemment)//本地语言包加入elementUI的语言包
};
const i18n = new VueI18n({
locale: "zh", //zh默认是简体中文
messages
});
Vue.use(ElementUI, {
i18n: (key, value) => i18n.t(key, value)
})
复制代码
有写错的,或者有什么问题,欢迎你们评论