Vue实战经验总结(持续更新)

我本地的开发环境: 

  • 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 开启的服务关掉。

扫描二维码关注公众号,回复: 13434494 查看本文章

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 开启成功 :

参考文章

猜你喜欢

转载自blog.csdn.net/huangpb123/article/details/119082503
今日推荐