VueJS 实际开发中会遇到的问题

时间: 2017-11-09阅读: 368标签: vue作者: 熊D01001

由于公司的前端开始转向 VueJS,最近开始使用这个框架进行开发,遇到一些问题记录下来,以备后用。

主要写一些 官方手册 上没有写,但是实际开发中会遇到的问题,需要一定知识基础。


涉及技术栈


正文:

polyfill 与 transform-runtime

  首先,vue-cli 为我们自动添加了 babel-plugin-transform-runtime 这个插件,该插件多数情况下都运作正常,可以转换大部分 ES6 语法。

  但是,存在如下两个问题:

  1. 异步加载组件时,会产生 polyfill 代码冗余
  2. 不支持对全局函数与实例方法的 polyfill

  两个问题的原因均归因于 babel-plugin-transform-runtime 采用了沙箱机制来编译我们的代码(即:不修改宿主环境的内置对象)。

  由于异步组件最终会被编译为一个单独的文件,所以即使多个组件中使用了同一个新特性(例如:Object.keys()),那么在每个编译后的文件中都会有一份该新特性的 polyfill 拷贝。如果项目较小可以考虑不使用异步加载,但是首屏的压力会比较大。

  不支持全局函数(如:Promise、Set、Map),Set 跟 Map 这两种数据结构应该大家用的也不多,影响较小。但是 Promise 影响可能就比较大了。

  不支持实例方法(如:'abc'.includes('b')、['1', '2', '3'].find((n) => n < 2) 等等),这个限制几乎废掉了大部分字符串和一半左右数组的新特性。

一般情况下 babel-plugin-transform-runtime 能满足大部分的需求,当不满足需求时,推荐使用完整的 babel-polyfill。

替换 babel-polyfill

  首先,从项目中移除 babel-plugin-transform-runtime
  卸载该依赖:

npm un babel-plugin-transform-runtime -D

  修改 babel 配置文件

// .babelrc
{
  //...
  "plugins": [
    // - "transform-runtime"
  ]
  //...
}

  然后,安装 babel-polyfill 依赖:

npm i babel-polyfill -D

  最后,在入口文件中导入

// src/main.js
import 'babel-polyfill'

ES6 import 引用问题

  在 ES6 中,模块系统的导入与导出采用的是引用导出与导入(非简单数据类型),也就是说,如果在一个模块中定义了一个对象并导出,在其他模块中导入使用时,导入的其实是一个变量引用(指针),如果修改了对象中的属性,会影响到其他模块的使用。

  通常情况下,系统体量不大时,我们可以使用 JSON.parse(JSON.stringify(str)) 简单粗暴地来生成一个全新的深度拷贝的 数据对象。不过当组件较多、数据对象复用程度较高时,很明显会产生性能问题,这时我们可以考虑使用 Immutable.js

鉴于这个原因,进行复杂数据类型的导出时,需要注意多个组件导入同一个数据对象时修改数据后可能产生的问题。
此外,模块定义变量或函数时即便使用 let 而不是 const,在导入使用时都会变成只读,不能重新赋值,效果等同于用 const 声明。

在 Vue 中使用 Pug 与 Less

安装依赖

  Vue 中使用 vue-loader 根据 lang 属性自动判断所需要的 loader,所以不用额外配置 Loader,但是需要手动安装相关依赖:

npm i pug -D
npm i less-loader -D

还是相当方便的,不用手动修改 webpack 的配置文件添加 loader 就可以使用了

使用 pug 还是 pug-loader?sass 两种语法的 loader 如何设置?
--- 请参考 预处理器 · vue-loader

使用

<!-- xxx.vue -->
<style lang="less">
  .action {
    color: #ddd;
      ul {
        overflow: hidden;
        li {
          float: left;
        }
      }
  }
</style>
<template lang="pug">
  .action(v-if='hasRight')
    ul
      li 编辑
      li 删除
</template>
<script>
  export default {
    data () {
      return {
        hasRight: true
      }
    }
  }
</script>

定义全局函数或变量

  许多时候我们需要定义一些全局函数或变量,来处理一些频繁的操作(这里拿 AJAX 的异常处理举例说明)。但是在 Vue 中,每一个单文件组件都有一个独立的上下文(this)。通常在异常处理中,需要在视图上有所体现,这个时候我们就需要访问 this 对象,但是全局函数的上下文通常是 window,这时候就需要一些特殊处理了。

简单粗暴型

  最简单的方法就是直接在 window 对象上定义一个全局方法,在组件内使用的时候用 bind、call 或 apply 来改变上下文。

  定义一个全局异常处理方法:

// errHandler.js
window.errHandler = function () { // 不能使用箭头函数
  if (err.code && err.code !== 200) {
    this.$store.commit('err', true)
  } else {
    // ...
  }
}

  在入口文件中导入:

// src/main.js
import 'errHandler.js'

  在组件中使用:

// xxx.vue
export default {
  created () {
    this.errHandler = window.errHandler.bind(this)
  },
  method: {
    getXXX () {
      this.$http.get('xxx/xx').then(({ body: result }) => {
        if (result.code === 200) {
          // ...
        } else {
          this.errHandler(result)
        }
      }).catch(this.errHandler)
    }
  }
}

优雅安全型

  在大型多人协作的项目中,污染 window 对象还是不太妥当的。特别是一些比较有个人特色的全局方法(可能在你写的组件中几乎处处用到,但是对于其他人来说可能并不需要)。这时候推荐写一个模块,更优雅安全,也比较自然,唯一不足之处就是每个需要使用该函数或方法的组件都需要进行导入。

  使用方法与前一种大同小异,就不多作介绍了。 ̄ω ̄=

Moment.JS 与 Webpack

  在使用 Moment.js 遇到一些问题,发现最终打包的文件中将 Moment.js 的全部语言包都打包了,导致最终文件徒然增加 100+kB。查了一下,发现可能是 webpack 打包或是 Moment.js 资源引用问题(?),目前该问题还未被妥善处理,需要通过一些 trick 来解决这个问题。

  在 webpack 的生产配置文件中的 plugins 字段中添加一个插件,使用内置的方法类 ContextReplacementPlugin 过滤掉 Moment.js 中那些用不到的语言包:

// build/webpack.prod.conf.js
new webpack.ContextReplacementPlugin(/moment[\\/]locale$/, /^\.\/(zh-cn)$/)

解决方案采自 oleg-nogin@webpack/webpack#3128
问题讨论详见 GitHub Issue: moment/moment#2373webpack/webpack#3128

自定义路径别名

  可能有些人注意到了,在 vue-cli 生成的模板中在导入组件时使用了这样的语法:

import Index from '@/components/Index'

  这个 @ 是什么东西?后来改配置文件的时候发现这个是 webpack 的配置选项之一:路径别名。

  我们也可以在基础配置文件中添加自己的路径别名,比如下面这个就把 ~ 设置为路径 src/components 的别名:

// build/webpack.base.js
{
  resolve: {
    extensions: ['.js', '.vue', '.json'],
    alias: {
      'vue$': 'vue/dist/vue.esm.js',
      '@': resolve('src'),
      '~': resolve('src/components')
    }
  }
}

  然后我们导入组件的时候就可以这样写:

// import YourComponent from 'YourComponent'
// import YourComponent from './YourComponent'
// import YourComponent from '../YourComponent'
// import YourComponent from '/src/components/YourComponent'
import YourComponent from '~/YourComponent'

  既解决了路径过长的麻烦,又解决了相对路径的烦恼,方便很多吧!ヾ(゚∀゚ゞ)

CSS 作用域与模块

组件内样式

  通常,组件中 <style></style> 标签里的样式是全局的,在使用第三方 UI 库(如:Element)时,全局样式很可能影响 UI 库的样式。

  我们可以通过添加 scoped 属性来使 style 中的样式只作用于当前组件:

<style lang="less" scoped>
  @import 'other.less';
  .title {
    font-size: 1.2rem;
  }
</style>

在有 scoped 属性的 style 标签内导入其他样式,同样会受限于作用域,变为组件内样式。复用程度较高的样式不建议这样使用。

另,在组件内样式中应避免使用元素选择器,原因在于元素选择器与属性选择器组合时,性能会大大降低。

--- 两种组合选择器的测试:classes selectorelements selector

导入样式

  相对于 style 使用 scoped 属性时的组件内样式,有时候我们也需要添加一些全局样式。当然我们可以用没有 scoped 属性的 style 来写全局样式。

  但是相比较,更推荐下面这种写法:

/* 单独的全局样式文件 */
/* style-global.less */
body {
  font-size: 10px;
}
.title {
  font-size: 1.4rem;
  font-weight: bolder;
}

  然后在入口文件中导入全局样式:

// src/main.js
import 'style-global.less'

获取表单控件值

  通常我们可以直接使用 v-model 将表单控件与数据进行绑定,但是有时候我们也会需要在用户输入的时候获取当前值(比如:实时验证当前输入控件内容的有效性)。

  这时我们可以使用 @input 或 @change 事件绑定我们自己的处理函数,并传入 $event 对象以获取当前控件的输入值:

<input type='text' @change='change($event)'>
change (e) {
  let curVal = e.target.value
  if (/^\d+$/.test(curVal)) {
    this.num = +curVal
  } else {
    console.error('%s is not a number!', curVal)
  }
}

当然,如果 UI 框架采用 Element 会更简单,它的事件回调会直接传入当前值。

v-for 的使用 tips

  v-for 指令很强大,它不仅可以用来遍历数组、对象,甚至可以遍历一个数字或字符串。

  基本语法就不讲了,这里讲个小 tips:

索引值

  在使用 v-for 根据对象或数组生成 DOM 时,有时候需要知道当前的索引。我们可以这样:

<ul>
  <li v-for='(item, key) in items' :key='key'> {{ key }} - {{ item }}
</ul>

  但是,在遍历数字的时候需要注意,数字的 value 是从 1 开始,而 key 是从 0 开始:

<ul>
  <li v-for='(v, k) in 3' :key='k'> {{ k }}-{{ v }} 
  <!-- output to be 0-1, 1-2, 2-3 -->
</ul>

2.2.0+ 的版本里,当在组件中使用 v-for 时,key 现在是必须的。

模板的唯一根节点

  与 JSX 相同,组件中的模板只能有一个根节点,即下面这种写法是 错误 的:

<template>
  <h1>Title</h1>
  <article>Balabala...</article>
</template>

  我们需要用一个块级元素把他包裹起来:

<template>
  <div>
    <h1>Title</h1>
    <article>Balabala...</article>
  </div>
</template>

原因参考:React-小记:组件开发注意事项#唯一根节点

项目路径配置

  由于 vue-cli 配置的项目提供了一个内置的静态服务器,在开发阶段基本不会有什么问题。但是,当我们把代码放到服务器上时,经常会遇到静态资源引用错误,导致界面一片空白的问题。

  这是由于 vue-cli 默认配置的 webpack 是以站点根目录引用的文件,然而有时候我们可能需要把项目部署到子目录中。

  我们可以通过 config/index.js 来修改文件引用的相对路径:

  build.assetsSubDirectory: 'static'
  build.assetsPublicPath: '/'

  dev.assetsSubDirectory: 'static'
  dev.assetsPublicPath: '/'

  我们可以看到导出对象中 build 与 dev 均有 assetsSubDirectory、assetsPublicPath 这两个属性。

  其中 assetsSubDirectory 指静态资源文件夹,也就是打包后的 js、css、图片等文件所放置的文件夹,这个默认一般不会有问题。

  assetsPublicPath 指静态资源的引用路径,默认配置为 /,即网站根目录,与 assetsSubDirectory 组合起来就是完整的静态资源引用路径 /static。

  写到这里解决方法已经很明显了,只要把根目录改为相对目录就好了:

  build.assetsSubDirectory: 'static'
  build.assetsPublicPath: './'

  没错!就是一个 . 的问题。ㄟ( ▔, ▔ )ㄏ

更小的 Polyfill 开销

  在引入 Polyfill 之后,可以在 .babelrc 文件中开启 useBulitIns 属性。启用该属性后,编译项目时会根据项目中新特性的使用情况将完整的 polyfill 拆分成独立的模块序列。
  启用 useBuiltIns 属性:

  // .babelrc
  {
    "presets": [
      ["env", {
        "modules": false,
        "useBuiltIns": true
      }],
      "es2015",
      "stage-2"
    ]
    // ...
  }

  安装后引入 babel-polyfill:

  // src/main.js
  import 'babel-polyfill'

  [1, 2, 3].find((v => v > 2))

启用 useBulitIns 后自动拆分 babel-polyfill

  import 'core-js/modules/es6.array.find'

  [1, 2, 3].find((v => v > 2))

经测试最大减少了一半左右的 polyfill 体积
没深入研究哈,猜测可能加了 core-js 跟一些基础的 polyfill

使用 ESnext class 特性

对比

  默认时,Vue 单文件组件使用一个对象来描述组件内部的实现:

  const App = {
    // initialized data
    data () {
      return {
        init: false
      }
    }
    // lifecycle hook
    created () {}
    mounted () {}
    // ...
  }

  export default App

  我们可以通过安装一些依赖来支持最新的 class 写法:

  import Vue from 'vue'
  import Component from 'vue-class-component'

  @Component
  class App extends Vue {
    init = false;
    created () {}
    mounted () {}
  }

  export default App

不可否认,确实多些了一些代码哈,不过个人还是比较倾向新语法特性的写法的,毕竟标准即是灯塔
P.S 这里使用了还处于 Stage 3 的 Field declarations 来声明组件的初始 data

使用

  下面来看看需要做哪些改动以支持使用 class 的写法:

  1. 首先,最明显的就是我们需要 vue-class-component 这个依赖了。
  2. 然后,这个依赖需要 babel 的 transform-decorators-legacy 插件支持。
  3. 最后,如果你也想使用 Field declarations 字段声明写法,再添加一个 transform-class-properties 就好了。

  安装依赖:

  npm i vue-class-component -D
  npm i babel-plugin-transform-decorators-legacy -D
  npm i babel-plugin-transform-class-properties -D

  配置 babel

  // .babelrc
  {
    // ...
    "plugins": [
      "transform-runtime",
      "transform-decorators-legacy",
      "transform-class-properties"
    ]
    // ...
  }

注意:transform-decorators-legacy 需放在 transform-class-properties 之前

响应式数据失效

数组

  由于 Vue.js 响应式数据依赖于对象方法 Object.defineProperty。但很明显,数组这个特殊的“对象”并没有这个方法,自然也无法设置对象属性的 descriptor,从而也就没有 getter() 和 setter() 方法。所以在使用数组索引角标的形式更改元素数据时(arr[index] = newVal),视图往往无法响应式更新。
  为解决这个问题,Vue.js 中提供了 $set() 方法:

vm.arr.$set(0, 'newVal')
// vm.arr[0] = 'newVal'

对象

受现代 JavaScript 的限制(以及废弃 Object.observe),Vue 不能检测到对象属性的添加或删除。由于 Vue 会在初始化实例时对属性执行 getter/setter 转化过程,所以属性必须在 data 对象上存在才能让 Vue 转换它,这样才能让它是响应的。
Ref: 深入响应式原理 - Vue.js

var vm = new Vue({
  data: {
    a: 1
  }
})
// `vm.a` 是响应的
vm.b = 2
// `vm.b` 是非响应的

静态类型检测

  推荐在开发较复杂的组件时使用 props 静态类型检测,提高组件的健壮性,多数情况下可以在转码阶段提前发现错误。

// before
prop: [
  'id',
  'multiple',
  'callback',
]
// after
props: {
  id: {
    type: [ Number, Array ],
    required: true,
  },
  multiple: {
    type: Boolean,
    default: false,
  },
  callback : Function,
}

异步组件

  使用处于 Stage.3 阶段的动态导入函数 import(),同时借助 webpack 的代码分割功能,在 Vue.js 中我们可以很轻松地实现一个异步组件。

异步路由组件

const AsyncComponent = () => import('./AsyncComponent')

异步组件工厂

Vue.component(
  'async-webpack-example',
  () => import('./my-async-component')
)

相比于异步路由组建,异步组件工厂一般适用于组件内进一步小颗粒度的拆分处理,如:大体量组件内初次加载时的非必要组件(组件内嵌套的弹窗组件或 Popover 组件等)。

vue引用js文件的多种方式

vue引用js文件的多种方式,这里以为引入jquery为例。js引入文件方式包括: vue-cli webpack全局引入jquery、vue组件引用外部js的方法、单vue页面引用内部js方法

vue router的使用_vue路由的相关知识

Vue中的路由根据用户在网页中的点击,将其引导到对应的页面。安装vue-router,路由使用包括:嵌套路由、动态路由、编程式路由、路由重定向

Vue中的scoped及穿透方法

由于scoped看起来很美好,但是含有很多的坑,所以,不推荐不使用scoped属性,而通过在外层dom上添加唯一的class来区分不同组件。这种方法既实现了类似于scoped的效果,又方便修改各种第三方组件的样式,代码看起来也相对舒适

在Vue项目里面使用d3.js

在Vue里面使用D3.js的方法,npm 上面的D3相对来说 可以说是很不人性化了 完全没有说 在webpack上怎么使用D3.js,将视图从逻辑中分离出来,并且使用Vue钩子,方法和data对象。

vue.js $emit/$on的用法和理解_vue组件之间数据传输通信

每个 Vue 实例都实现了事件接口vm.$emit( event, arg ) 触发当前实例上的事件;vm.$on( event, fn )监听event事件后运行。实例说明:Vuejs 用$emit与$on来进行兄弟组件之间的数据传输通信,Vuejs 用$emit与$on来进行跨页面之间的数据传输通信

浅谈Vue v-model实现原理,如何封装,以及封装方法

vue的v-model是一个十分强大的指令,它可以自动让input里的值自动和你设定的值进行绑定,它是如何实现的呢?其实v-model只不过是一个语法糖而已,真正的实现靠的还是:(1) v-bind:绑定响应式数据,(2) 触发 input 事件 并传递数据 (重点)

vue中nextTick的使用_vue.js下nextTick()异步更新队列源码解析

nextTick()并不会重绘当前页面,并且它也不是在页面重绘才执行,而是在此次事件循环结束后一定会执行的。 为什么能在此方法中取到更新后的数据,那是因为dom元素的属性已经在watcher执行flush队列的时候改变了,因此是可以在此时获取的。

vue轮播图_用vue实现一个仿简书的轮播图

Vue的理念是以数据驱动视图,所以拒绝通过改变元素的margin-top来实现滚动效果。写好css样式,只需改变每张图片的class即可实现轮播效果。可以将轮播图看成两个,如图所示

vue路由history模式_如何去除vue项目中的#

在使用vue-cli搭建的环境中,浏览器上URL地址中是存在#的,这是由于vue-router 默认 hash 模式,不难发现#的出现真的很丑陋。官网给出了如何使用history模式mode: history

vue watch监听对象的使用_实现首次不触发、深度监听

vue中的watch是一个对象,所以一定要当成对象来用,它有键-值组成,其中键就是你要监控的那个数据。这篇文章介绍:vue如何实现首次不触发watch,vue如何实现数据的深度监听?