Vue 全家桶开发的一些小技巧和注意事项
前言
用 vue 全家桶开发一年多了,踩过不少坑,也解决了很多的问题,把其中的一些点记录下来,希望能帮到大家。以下内容基于最新版的 vue + vuex + vue-router + axios + less + elementUI,vue 脚手架是 vue-cli3。
css 的 scoped 属性
vue 为了防止 css 污染,当组件的
<style>标签有
scoped属性时,它的 css 只作用于当前组件中的元素。实现原理很简单,给当前组件中的每个标签都加上唯一的自定义属性:
data-v-唯一的属性,然后 css 选择器都加上属性选择器
.article-title[data-v-唯一的属性],这样这个 css 只会匹配到当前页面的这个元素。
注意:每个组件的最外层的标签会带上父组件的
data-v-属性,也就是这个标签会被父组件的样式匹配到,所以父组件尽量不要使用标签选择器,这个标签不要使用父组件中的 id 或者 class。
在父组件想修改子组件的 css(修改 elementUI 组件的样式),我们可以借助深度作用选择器
>>>
div >>> .el-input{ width: 100px; } /* sass/less的话可能无法识别,这时候需要使用 /deep/ 选择器。*/ div /deep/ .el-input{ width: 100px; }深度作用选择器会去掉后面元素的属性选择器
[data-v-],即上面代码会编译成:
div[data-v-12345667] .el-input{}。就可以匹配到子组件的元素,从而覆盖样式。
父子组件的生命周期钩子函数执行先后顺序
组件的生命周期钩子函数是到了某个生命周期点就会触发,而不是在这个钩子函数中进行生命周期,比如说 DOM 加载好了,就会触发
mounted钩子函数,所以在
created里面写一个延迟定时器,
mounted钩子不会等定时器执行。
各个周期钩子函数触发的时间点参考(图来源于网络)
关于父子组件的生命周期:不同的钩子函数有不同的表现。父组件的虚拟 DOM 先初始化好了(
beforeMount),才会去初始化子组件的虚拟 DOM (
beforeMount),而
mounted事件,等价于
window.onload,子组件 DOM 没加载好,父组件 DOM 永远不可能加载好。所以基本生命周期钩子函数执行顺序是:父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mounted
父子组件的 update 和 beforeUpdate 执行先后顺序:数据修改+虚拟 DOM 准备好会触发 beforeUpdate,换句话说 beforeUpdate 等价于 beforeMount,而 update 等价于 mounted。所以先后顺序是:父 beforeUpdate -> 子 beforeUpdate -> 子 update -> 父 update。
同理
beforeDestory和
destoryed的先后顺序是:父 beforeDestory -> 子 beforeDestory -> 子 destoryed -> 父 destoryed。
生命周期钩子函数其实也可以写成数组的形式:
mounted: [mounted1, mounted2],同一个生命周期可以触发多个函数,这也是
mixin(混入)的原理,
mixin里面也可以写生命周期钩子,最终会和组件里面的生命周期钩子函数一起变成数组形式,
mixin里面的钩子函数会先执行。
异步请求数据在哪个钩子函数中执行比较好
生命周期钩子函数的中异步会放入事件队列,而不会在这个钩子函数中执行。也就是说你在
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); })
is 属性的妙用
由于 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-item @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-item> </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 语法
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>总结:
其他名称的 slot(非 default)仅能用于
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>
父组件通过 ref 访问到子组件
虽然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
函数
背景图、css 的 @import 使用路径别名
在用
Webpack处理打包时,可将某一目录配置一个别名,代码中就能使用与别名的相对路径引用资源
import tool from '@/utils/test'; // Webpack 能正确识别并打包。但是在
css文件,如 less, sass, stylus 中,使用
@import "@/style/theme"的语法引用相对
@的目录确会报错。解决办法是是在引用路径的字符串最前面添加上 ~ 符号。
css module 中:
@import "~@/style/theme.less"
css 属性中:
background: url("~@/assets/xxx.jpg")
html 标签中:
<img src="~@/assets/xxx.jpg">
vue-router 的 hash 模式和 history 模式
我们先来看一个完整的 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会根据目录的名称来替换页面内容。
优缺点:
hash 模式的 # 号很丑,使用的是
onhashchange
事件切换路由,兼容性会好一点,不需要服务器配合history 模式好看点,但是本地开发、网站上线,都需要服务器额外配置,并且还需要自己写 404 页面,使用的是
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模式就会帮你都配置好。
vue-router的钩子函数
钩子函数分三种:组件内钩子,全局钩子,路由独享钩子。
APP.vue没有组件内钩子函数,因为
APP.vue是页面的入口,这个组件是必定会加载的,而使用组件内钩子函数可以阻止组件加载。
全局钩子主要用于路由鉴权,但是消耗很大。组件内的钩子
beforeRouteLeave主要用于用户离开前的提示(比如说有未保存的文章),这个钩子有一些坑:hash模式下,浏览器的后退按钮无法触发这个钩子函数。同时我们还可以监听用户的关闭当前窗口/浏览器事件:
window.onbeforeunload = e => "确定离开当前页面,你的修改将不会被保存!";为了防止恶意网站,用户关闭窗口/浏览器事件是不可阻止的,只能提示,而且不同的浏览器兼容性也不同。
Vuex 持久化存储
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 里面触发 action
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); }, }, });
Vue.observable进行组件通信
如果项目很小,不需要用到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 };
axios 的 qs 插件
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,不需要手动设置。
表单数据(不带文件)的 content-type 是
application/x-www-form-urlencoded
表单数据(带文件)的 content-type 是
multipart/form-data
json 数据的 content-type 是
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
elementUI 的一些总结
- 表单验证同步写法,避免多层嵌套函数
const valid = await new Promise(resolve => this.$refs.form.validate(resolve)); if (!valid) return
按需引入之后级联菜单高度撑满屏幕。解决办法:加一句全局样式
.el-cascader-menu > .el-scrollbar__wrap{ height: 250px; }
级联菜单的数据是按需获取的没法回显。解决办法:根据已有的路径数据去请求树数据,然后给级联菜单加一个v-if,等数据都请求好了再显示出来。比如说省市县三级联动数据,已知用户选择的是广东省-深圳市-南山区,那么分别去请求所有省、广东省、深圳市的数据,然后将数据拼成一个 tree ,绑定到级联菜单,然后设置
v-if="true"
。表格高度自适应,可以给表格外层加一个 div ,然后给这个 div 计算高度(或者弹性盒子自适应高度),表格属性
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) })
最后
有写错的,或者有什么问题,欢迎大家评论
作者:沉末_ 链接:https://juejin.im/post/5d8c6a97e51d45782c23fa69
推荐阅读:1、GitHub 上能挖矿的神仙技巧 - 如何发现优秀开源项目2. 9 种你或许不知道的 Vue 好用小技巧3. Vue + TypeScript + Element 项目实战及踩坑记4. 重磅:硬核前端面试开源项目汇总(进大厂必备)
- Asp.net &C#开发中的一些注意事项及小技巧
- Asp.net &C#开发中的一些注意事项及小技巧【转】
- Asp.net &C#开发中的一些注意事项及小技巧
- 使用vue.js开发时一些注意事项
- .net开发中的一些注意事项及小技巧
- .net开发中的一些注意事项及小技巧
- .net开发中的一些注意事项及小技巧
- Vue js + Laravel Mix开发的一些注意事项(有待补充)
- Android生存指南:一些开发注意事项
- Matlab中的一些注意事项和小技巧
- Cocos2d-x游戏开发的一些注意事项
- JNI开发时需要注意的一些事项
- 关于Android应用开发的一些安全注意事项
- VS2008环境下开发Web Service的一些注意事项
- android一些开发注意事项
- Dclound + vue开发 Hybrid APP注意事项
- 关于Android应用开发的一些安全注意事项
- vue编写时的一些注意事项(1)(---漫无目的的哔哔赖赖)
- DataGrid开发中应注意的一些小技巧
- vue基础中的注意事项,以及一些学习心得