VueJS 開發常見問題集錦

圖片描述

因爲公司的前端開始轉向 VueJS,最近開始使用這個框架進行開發,遇到一些問題記錄下來,以備後用。javascript

主要寫一些 官方手冊 上沒有寫,可是實際開發中會遇到的問題,須要必定知識基礎。php


涉及技術棧


正文:css

polyfill 與 transform-runtime

  首先,vue-cli 爲咱們自動添加了 babel-plugin-transform-runtime 這個插件,該插件多數狀況下都運做正常,能夠轉換大部分 ES6 語法。html

  可是,存在以下兩個問題:前端

  1. 異步加載組件時,會產生 polyfill 代碼冗餘
  2. 不支持對全局函數與實例方法的 polyfill

  兩個問題的緣由均歸因於 babel-plugin-transform-runtime 採用了沙箱機制來編譯咱們的代碼(即:不修改宿主環境的內置對象)。vue

  因爲異步組件最終會被編譯爲一個單獨的文件,因此即便多個組件中使用了同一個新特性(例如:Object.keys()),那麼在每一個編譯後的文件中都會有一份該新特性的 polyfill 拷貝。若是項目較小能夠考慮不使用異步加載,可是首屏的壓力會比較大。java

  不支持全局函數(如:PromiseSetMap),SetMap 這兩種數據結構應該你們用的也很少,影響較小。可是 Promise 影響可能就比較大了。react

  不支持實例方法(如:'abc'.includes('b')['1', '2', '3'].find((n) => n < 2) 等等),這個限制幾乎廢掉了大部分字符串和一半左右數組的新特性。webpack

通常狀況下 babel-plugin-transform-runtime 能知足大部分的需求,當不知足需求時,推薦使用完整的 babel-polyfillgit

替換 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-loadersass 兩種語法的 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 對象上定義一個全局方法,在組件內使用的時候用 bindcallapply 來改變上下文。

  定義一個全局異常處理方法:

// 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: '/'

  咱們能夠看到導出對象中 builddev 均有 assetsSubDirectoryassetsPublicPath 這兩個屬性。

  其中 assetsSubDirectory 指靜態資源文件夾,也就是打包後的 jscss、圖片等文件所放置的文件夾,這個默認通常不會有問題。

  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 3Field declarations 來聲明組件的初始 data

使用

  下面來看看須要作哪些改動以支持使用 class 的寫法:

  1. 首先,最明顯的就是咱們須要 vue-class-component 這個依賴了。
  2. 而後,這個依賴須要 babeltransform-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 組件等)。


To be continue...

文章還在完善中,歡迎你們一塊兒討論 Vue.JS 開發中遇到的一些問題哈 (゚▽゚)/

看看你的收藏列表,你肯定收藏了會記得看嗎_(:зゝ∠)_
讀一讀開發的時候至少會有個印象,點個贊打卡啦~

原文:VueJS 開發常見問題集錦

相關文章
相關標籤/搜索