我本地的开发环境:
- Vue 版本:2.x
- Vue CLI 版本:4.5.11
1、本地开发环境请求服务器接口跨域的问题
用 Vue CLI 创建的项目可以在 vue.config.js 添加以下代码:
module.exports = {
devServer: {
proxy: {
// 以"/api"开头的请求都会代理到目标服务器
'/api': {
target: 'http://jsonplaceholder.typicode.com', // 接口域名
changeOrigin: true, // 是否启用跨域
pathRewrite: {
'^/api': ''
}
}
},
},
};
axios 接口要进行配置:
axios.defaults.baseURL = process.env.NODE_ENV === 'development' ? '/api' : '';
配置好以后重新启动下项目:npm run serve
2、定时器和事件的销毁
页面切换时,周期性定时器要关掉,不然定时器仍旧在执行。
页面切换时,用 addEventListener 绑定的事件不会自动解绑,我们需要手动移除监听,以免造成内存泄露。
解绑的方式有2种:一种是通过 $once 监听 beforeDestroy 生命周期,然后解绑。另一种是,直接在 Vue 提供的 beforeDestroy 生命周期里解绑。看着心情用呗!
created() {
addEventListener('click', this.click, false)
this.timer = setInterval(() =>{
// 某些定时器操作
}, 500);
// 解绑方法一: 通过$once来监听定时器,在beforeDestroy钩子可以被清除。
// 回调函数用箭头函数和普通的function,内部的this指向都是当前组件
this.$once('hook:beforeDestroy', () => {
clearInterval(this.timer);
removeEventListener('click', this.click, false)
});
},
// 解绑方法二
beforeDestroy() {
clearInterval(this.timer);
removeEventListener('click', this.click, false)
}
3、组件选项的书写顺序
Vue 官网有一套推荐的顺序,也可以按照自己的开发习惯定义一套。
export default {
el: '',
name: '',
functional: false,
components: {},
directives: {},
filters: {},
mixins: [],
model: {},
props: {},
data() {},
computed: {},
watch: {},
created() {},
mounted() {},
destroyed() {},
methods: {},
template: '',
renderError() {},
};
4、查看打包后各文件的体积,帮你快速定位大文件
用 Vue Cli 创建的项目自带有分析功能。
运行 npm run build -- --report,程序执行完后会生成一个 dist/report.html 分析文件。面积越大,包体积越大。
记得运行的时候把本地 npm run serve 开启的服务关掉。
5、详情页返回列表页缓存数据和浏览位置、其他页面进入列表页刷新数据的实践
通过 <keep-alive> 和在路由 meta 中定义的 keepAlive 字段缓存组件:
<keep-alive>
<router-view v-if="$route.meta.keepAlive"/>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive"/>
下面在router/index.js 即我们的路由文件中,定义meta信息:
// list是我们的搜索结果页面
routes: [
{
path: '/list',
name: 'List',
component: () => import(/* webpackChunkName: "layout" */ '../views/List.vue'),
meta: {
keepAlive: true, // 是否需要缓存当前组件
isUseCache: false, // 是否需要缓存列表数据
},
},
],
scrollBehavior (to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
if (from.meta.keepAlive) {
from.meta.savedPosition = document.body.scrollTop;
}
return { x: 0, y: to.meta.savedPosition || 0 }
}
}
刷新数据or缓存数据的实现:
设置了keepAlive缓存的组件会涉及的生命周期钩子:
第一次进入:beforeRouterEnter ->created->…->activated->…->deactivated
后续进入时:beforeRouterEnter ->activated->deactivated
可以看出,只有第一次进入该组件时,才会走created钩子,而需要缓存的组件中activated是每次都会走的钩子函数。所以,我们要在这个钩子里面去判断,当前组件是需要使用缓存的数据还是重新刷新获取数据。
// list组件的activated钩子
activated() {
if (!this.$route.meta.isUseCache) {
this.list = [];
this.loadData();
}
this.$route.meta.isUseCache = false;
},
beforeRouteLeave(to, from, next) {
if (to.name === 'Detail') {
from.meta.isUseCache = true;
from.meta.savedPosition = document.documentElement.scrollTop;
}
next();
},
比如,如果这个详情页是个订单详情,那么在订单详情页可能会有删除订单的操作。那么删除订单操作后会返回订单列表页,是需要列表页重新刷新的。那么我们需要此时在订单详情页进行是否要刷新的判断。简单改造一下详情页:
data () {
return {
isDel: false // 是否进行了删除订单的操作
}
},
beforeRouteLeave (to, from, next) {
if (to.name == 'List') {
to.meta.isUseCache = !this.isDel;
}
next();
},
methods: {
deleteOrder () {
// 执行删除订单逻辑...
this.isDel = true;
this.$router.go(-1)
}
}
6、自定义组件(父子组件)的双向数据绑定
v-model
v-model 是个语法糖:
<input type="text" v-model="msg">
<!-- 等效于 -->
<input type="text" :value="msg" @input="msg = $event.target.value">
data () {
return {
msg: ''
}
}
v-model 用在自定义组件上也是这个语法糖,默认属性是 value, 事件是 input。但用在自定义组件上这个属性和事件可以更改:
父组件:
<custom-checkbox v-model="msg"></custom-checkbox>
子组件:
export default {
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: {
type: Boolean,
default: false
}
},
methods: {
confirm () {
this.$emit('change', false);
}
}
}
.sync 修饰符
.sync 也是一个语法糖:
<text-document :title.sync="doc.title"></text-document>
<!-- 等价于 -->
<text-document
:title="doc.title"
@update:title="doc.title = $event"
></text-document>
子组件需要更新父组件的 title 属性时,要显示触发一个更新事件:
this.$emit('update:title', newValue);
组件的 v-model 和 .sync 的双向数据绑定不是真正意义上的双向数据绑定,子组件修改父组件的数据要通过 $emit 主动触发事件。真正的双向绑定会带来维护上的问题,因为子组件可以变更父组件,且在父组件和子组件都没有明显的变更来源。
7、路由拆分管理
当项目路由比较多时,将路由的文件,按照模块拆分,方便路由管理和多人开发。
首先我们在router文件夹中创建一个index.js作为路由的入口文件,然后新建一个modules文件夹,里面存放各个模块的路由文件。
index.js 的代码如下:
import Vue from 'vue'
import Router from 'vue-router'
// 公共页面的路由文件
import PUBLIC from './modules/public'
// 投票模块的路由文件
import VOTE from './modules/vote'
Vue.use(Router)
const router = new Router({
mode: 'history',
routes: [
...PUBLIC,
...VOTE,
]
})
// 路由变化时
router.beforeEach((to, from, next) => {
// 处理逻辑...
next()
})
export default router;
子模块 vote.js 代码如下:
export default [
{
path: '/vote/index',
name: 'VoteIndex',
component: resolve => require(['@/view/vote/index'], resolve),
meta: {
title: '投票'
}
},
{
path: '/vote/detail',
name: 'VoteDetail',
component: resolve => require(['@/view/vote/detail'], resolve),
meta: {
title: '投票详情'
}
}
];
8、自定义生命周期钩子函数
这里以 pageHidden 和 pageVisible 自定义生命周期钩子函数为例,pageHidden 在网页隐藏时触发,pageVisible 在网页显示时触发。
1. 新建一个叫 custom-life-cycle.js 的文件,定义 pageHidden,pageVisible 2个自定义生命周期函数:
import Vue from 'vue';
// 通知所有组件页面状态发生了变化
const notifyVisibilityChange = (lifeCycleName, vm) => {
// 生命周期函数会存在$options中,通过$options[lifeCycleName]获取生命周期
const lifeCycles = vm.$options[lifeCycleName]
// 因为使用了created的合并策略,所以是一个数组
if (lifeCycles && lifeCycles.length) {
lifeCycles.forEach(lifecycle => {
lifecycle.call(vm)
})
}
// 遍历所有的子组件,然后依次递归执行
if (vm.$children && vm.$children.length) {
vm.$children.forEach(child => {
notifyVisibilityChange(lifeCycleName, child)
})
}
}
/**
* 添加生命周期钩子函数
*/
export function init() {
const optionMergeStrategies = Vue.config.optionMergeStrategies
/*
定义了两个生命周期函数 pageVisible, pageHidden,
并且使他们的合并策略与 created 的相同
*/
optionMergeStrategies.pageVisible = optionMergeStrategies.created
optionMergeStrategies.pageHidden = optionMergeStrategies.created
}
/**
* 将事件变化绑定到根节点上面
* @param {*} rootVm
*/
export function bind(rootVm) {
window.addEventListener('visibilitychange', () => {
// 判断调用哪个生命周期函数
let lifeCycleName = undefined
if (document.visibilityState === 'hidden') {
lifeCycleName = 'pageHidden'
} else if (document.visibilityState === 'visible') {
lifeCycleName = 'pageVisible'
}
if (lifeCycleName) {
// 通知所有组件生命周期发生变化了
notifyVisibilityChange(lifeCycleName, rootVm)
}
})
}
2. 在 main.js 引入
import { init, bind } from './utils/custom-life-cycle'
// 初始化生命周期函数, 必须在Vue实例化之前确定合并策略
init()
const vm = new Vue({
router,
render: h => h(App)
}).$mount('#app')
bind(vm)
3. 在组件里监听生命周期函数
由于自定义生命周期递归了所有子组件,在页面隐藏或显示时,所有定义了 pageVisible 和 pageHidden 的地方都会被触发。
export default {
pageVisible() {
console.log('页面显示出来了')
},
pageHidden() {
console.log('页面隐藏了')
}
}
9、provide / inject 隔代传值
使用过 element-ui 的同学一定对下面的 form 表单代码感到熟悉:
<template>
<el-form :model="formData" size="small">
<el-form-item label="姓名" prop="name">
<el-input v-model="formData.name" />
</el-form-item>
<el-form-item label="年龄" prop="age">
<el-input-number v-model="formData.age" />
</el-form-item>
<el-button>提交</el-button>
</el-form>
</template>
<script>
export default {
data() {
return {
formData: {
name: '',
age: 0
}
}
}
}
</script>
在el-form上面我们指定了一个属性size="small",然后有没有发现表单里面的所有表单元素以及按钮的 size都变成了small,这个是怎么做到的?接下来我们自己手写一个表单模拟一下。
自己手写一个表单:
我们现在模仿 element-ui 的表单,自己自定义一个,文件目录如下:
自定义表单 custom-form.vue
<template>
<form class="custom-form">
<slot></slot>
</form>
</template>
<script>
export default {
props: {
// 控制表单元素的大小
size: {
type: String,
default: 'default',
// size 只能是下面的四个值
validator(value) {
return ['default', 'large', 'small', 'mini'].includes(value)
}
},
// 控制表单元素的禁用状态
disabled: {
type: Boolean,
default: false
}
},
// 通过provide将当前表单实例传递到所有后代组件中
provide() {
return {
customForm: this
}
}
}
</script>
自定义表单项 custom-form-item.vue
<template>
<div class="custom-form-item">
<label class="custom-form-item__label">{
{ label }}</label>
<div class="custom-form-item__content">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
props: {
label: {
type: String,
default: ''
}
}
}
</script>
自定义输入框 custom-input.vue
<template>
<div
class="custom-input"
:class="[
`custom-input--${getSize}`,
getDisabled && `custom-input--disabled`
]"
>
<input class="custom-input__input" :value="value" @input="$_handleChange" />
</div>
</template>
<script>
export default {
props: {
// 这里用了自定义v-model
value: {
type: String,
default: ''
},
size: {
type: String
},
disabled: {
type: Boolean
}
},
// 通过inject 将form组件注入的实例添加进来
inject: ['customForm'],
computed: {
// 通过计算组件获取组件的size, 如果当前组件传入,则使用当前组件的,否则是否form组件的
getSize() {
return this.size || this.customForm.size
},
// 组件是否禁用
getDisabled() {
const { disabled } = this
if (disabled !== undefined) {
return disabled
}
return this.customForm.disabled
}
},
methods: {
// 自定义v-model
$_handleChange(e) {
this.$emit('input', e.target.value)
}
}
}
</script>
在项目中使用:
<template>
<custom-form size="small">
<custom-form-item label="姓名">
<custom-input v-model="formData.name" />
</custom-form-item>
</custom-form>
</template>
10、vue实现按需加载组件
组件的按需加载是项目性能优化的一个环节,也可以降低首屏渲染时间。
按需加载有2种写法,一种是用 import(),另一种是用 require(),示例如下:
路由懒加载
// webpack < 2.4 时,用 require()
{
path:'/',
name:'home',
components: resolve=>require(['@/components/home'], resolve)
}
// webpack > 2.4 时,用 import()
{
path:'/',
name:'home',
components: ()=>import('@/components/home')
}
组件按需加载
<template>
<div>
<ComponentA />
<ComponentB />
</div>
</template>
<script>
export default {
// ...
components: {
ComponentA: () => import('./ComponentA'),
ComponentB: resolve => require(['./ComponentA'], resolve),
},
}
</script>
11、对指定页面使用keep-alive路由缓存
通过路由配置文件和router-view设置:
// routes 配置
export default [
{
path: '/A',
name: 'A',
component: A,
meta: {
keepAlive: true // 需要被缓存
}
}, {
path: '/B',
name: 'B',
component: B,
meta: {
keepAlive: false // 不需要被缓存
}
}
]
// 路由设置
<keep-alive>
<router-view v-if="$route.meta.keepAlive">
<!-- 会被缓存的视图组件-->
</router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive">
<!-- 不需要缓存的视图组件-->
</router-view>
12、配置Vue对JSX的支持
要安装Vue支持JSX的babel插件。
13、使用 require.context 全局注册基础组件
一份可以让你在应用入口文件 (比如 src/main.js ) 中全局导入基础组件的示例代码(仅适用于 webpack 项目,因为 require.context 是 webpack提供的api):
import Vue from 'vue'
import upperFirst from 'lodash/upperFirst'
import camelCase from 'lodash/camelCase'
const requireComponent = require.context(
// 其组件目录的相对路径
'./components',
// 是否查询其子目录
false,
// 匹配基础组件文件名的正则表达式
/Base[A-Z]\w+\.(vue|js)$/
)
// requireComponent.keys()返回相对于"./components"文件夹的组件文件路径
requireComponent.keys().forEach(fileName => {
// 获取组件配置
const componentConfig = requireComponent(fileName)
// 获取组件的 PascalCase 命名
const componentName = upperFirst(
camelCase(
// 获取和目录深度无关的文件名
fileName.split('/').pop().replace(/\.\w+$/, '')
)
)
// 全局注册组件
Vue.component(
componentName,
// 如果这个组件选项是通过 `export default` 导出的,
// 那么就会优先使用 `.default`,
// 否则回退到使用模块的根。
componentConfig.default || componentConfig
)
})
全局注册的行为必须在根 Vue 实例 (通过 new Vue) 创建之前发生。
14、transformToRequire 直接使用资源链接
场景:以前在写 Vue 的时候经常会写到这样的代码:把图片提前 require 传给一个变量再传给组件(这里以 element ui 的头像为例)
<template>
<div>
<el-avatar :src="imgSrc"></el-avatar>
</div>
</template>
<script>
export default {
created () {
this.imgSrc = require('./assets/default-avatar.png')
}
}
</script>
现在:通过配置 transformToRequire 后,就可以直接配置,这样vue-loader会把对应的属性自动 require 之后传给组件
// vue-cli 2.x在vue-loader.conf.js 默认配置是
transformToRequire: {
video: ['src', 'poster'],
source: 'src',
img: 'src',
image: 'xlink:href'
}
// 配置文件,如果是vue-cli2.x 在vue-loader.conf.js里面修改
'el-avatar': ['src']
// vue-cli 3.x 在vue.config.js
// vue-cli 3.x 将transformToRequire属性换为了transformAssetUrls
module.exports = {
chainWebpack: config => {
config
.module
.rule('vue')
.use('vue-loader')
.loader('vue-loader')
.tap(options => {
options.transformAssetUrls = {
// 要处理的标签名: 链接属性名
'el-avatar': 'src',
}
return options;
});
}
}
// page 代码可以简化为
<template>
<div>
<el-avatar src="./assets/default-avatar.png"></el-avatar>
</div>
</template>
只需要对 element ui 等组件库或自定义组件的链接进行配置即可。
注意:上面方法只适用于静态链接, 动态链接还是要用 require('图片路径')。
<template>
<!-- 静态路径会直接显示 -->
<img src="../assets/panda.jpeg"/>
<!-- 动态路径要用require形式 -->
<img :src="imgSrc"/>
</template>
<script>
export default {
data() {
return {
imgSrc: require('../assets/panda.jpeg'),
}
}
}
</script>
15、为路径设置别名
1.vue-cli 2.x 配置
// 在 webpack.base.config.js中的 resolve 配置项,在其 alias 中增加别名
resolve: {
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
}
},
2.vue-cli 3.x 配置
// 在根目录下创建vue.config.js
var path = require('path')
function resolve (dir) {
console.log(__dirname)
return path.join(__dirname, dir)
}
module.exports = {
chainWebpack: config => {
config.resolve.alias
.set(key, value) // key,value自行定义,比如.set('@@', resolve('src/components'))
}
}
16、img 加载失败
场景: 有些时候后台返回图片地址不一定能打开,所以这个时候应该加一张默认图片。
<img :src="imgUrl" @error="handleError" alt="">
<script>
export default{
data(){
return{
imgUrl:''
}
},
methods:{
handleError(e){
e.target.src = reqiure('图片路径') //当然如果项目配置了transformToRequire,参考上面 33.2
}
}
}
</script>
17、局部样式
1. Vue中style标签的scoped属性表示它的样式只作用于当前模块,是样式私有化.
渲染的规则/原理: 给HTML的DOM节点添加一个 不重复的data属性 来表示 唯一性 在对应的 CSS选择器 末尾添加一个当前组件的 data属性选择器来私有化样式,如:.demo[data-v-2311c06a]{} 如果引入 less 或 sass 只会在最后一个元素上设置
// 原始代码
<template>
<div class="demo">
<span class="content">
Vue.js scoped
</span>
</div>
</template>
<style lang="less" scoped>
.demo{
font-size: 16px;
.content{
color: red;
}
}
</style>
// 浏览器渲染效果
<div data-v-fed36922>
Vue.js scoped
</div>
<style type="text/css">
.demo[data-v-039c5b43] {
font-size: 14px;
}
.demo .content[data-v-039c5b43] { //.demo 上并没有加 data 属性
color: red;
}
</style>
2. 深度选择器:deep
// 上面样式加一个 /deep/
<style lang="less" scoped>
.demo /deep/ .content{
color: blue;
}
</style>
// 浏览器编译后
<style type="text/css">
.demo[data-v-039c5b43] .content {
color: blue;
}
</style>
18、v-for 遍历避免同时使用 v-if
v-for 比 v-if 优先级高,如果每一次都需要遍历整个数组,将会影响速度,尤其是当之需要渲染很小一部分的时候,必要情况下应该替换成 computed 属性。
<ul>
<li
v-for="user in activeUsers"
:key="user.id">
{
{ user.name }}
</li>
</ul>
computed: {
activeUsers() {
return this.users.filter(user => user.isActive)
}
}
19、长列表性能优化
Vue 会通过 Object.defineProperty 对数据进行劫持,来实现视图响应数据的变化,然而有些时候我们的组件就是纯粹的数据展示,不会有任何改变,我们就不需要 Vue 来劫持我们的数据,在大量数据展示的情况下,这能够很明显的减少组件初始化的时间,那如何禁止 Vue 劫持我们的数据呢?可以通过 Object.freeze 方法来冻结一个对象,一旦被冻结的对象就再也不能被修改了。
export default {
data: () => ({
users: {}
}),
async created() {
const users = await axios.get("/api/users");
this.users = Object.freeze(users);
}
};
20、Webpack 对图片进行压缩
在 Webpack 中,可以用 url-loader 把大小小于 limit 的小图片转化为 base64 。对有些较大的图片资源,在请求资源的时候,加载会很慢,我们可以用 image-webpack-loader
来压缩图片:
(1)首先,安装 image-webpack-loader :
npm i image-webpack-loader -D
(2)然后,在 webpack.base.conf.js 中进行配置:
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use:[
{
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
loader: 'image-webpack-loader',
options: {
bypassOnDebug: true,
}
}
]
}
21、开启 gzip 压缩
web 服务器和客户端(浏览器)必须共同支持 gzip。目前主流的浏览器,Chrome,firefox,IE等都支持该协议。常见的服务器如 Apache,Nginx,IIS 同样支持,gzip 压缩效率非常高,通常可以达到 70% 的压缩率,也就是说,如果你的网页有 30K,压缩之后就变成了 9K 左右。
以下我们以服务端使用我们熟悉的 express 为例,开启 gzip 非常简单,相关步骤如下:
- 安装:
npm install compression --save
- 添加代码逻辑:
var compression = require('compression');
var app = express();
app.use(compression())
-
重启服务,观察网络面板里面的 response header,如果看到如下红圈里的字段则表明 gzip 开启成功 :