1. 程式人生 > >從一個奇怪的錯誤出發理解 Vue 基本概念

從一個奇怪的錯誤出發理解 Vue 基本概念

有人在學習 Vue 過程中遇到一個奇怪的問題,併為之迷惑不已——為什麼這麼簡單的一個專案都會出錯。

這是一個簡單到幾乎不能再簡單的 Vue 專案,在 index.html 的 body 中有一個 id 為 app 的 div 根元素,其中包含一個 my-component 自定義標籤。

<div id="app">
  <my-component></my-component>
</div>

index.js 中引入 vue 及 MyComponent 單檔案元件,然後執行 new Vue。

import Vue from 'Vue'
import MyComponent from './components/MyComponent' new Vue({ el: '#app', components: { MyComponent } })

MyComponent 元件包含一個 p 元素,以插值的形式繫結顯示一個包含 Hello World! 字串的資料。

專案用 webpack 打包,並使用 webpack-dev-server 啟動開發伺服器。webpack 配置檔案內容如下:

var path = require('path')
var HtmlWebpackPlugin = require
('html-webpack-plugin') module.exports = { entry: path.resolve(__dirname, 'index.js'), output: { path: path.resolve(__dirname, `dist`), publicPath: '', filename: 'index.js' }, resolve: { extensions: ['.js', '.vue'] }, module: { loaders: [ { test: /\.vue$/
, loader: 'vue-loader' }, { test: /\.js$/, loader: 'babel-loader', query: { presets: ['es2015'] }, exclude: /node_modules/ } ] }, plugins: [ new HtmlWebpackPlugin({ filename: 'index.html', template: path.resolve(__dirname, 'index.html'), inject: true }) ] }

執行 npm run dev 並訪問 http://localhost:8080/ 後,從瀏覽器的控制檯得到如下警告資訊:

[Vue warn]: Failed to mount component: template or render function not defined. (found in root instance)

令人百思不得其解的是,如果不使用 webpack 打包,而是直接在 HTML 檔案中使用 script 標籤引入 Vue.js,在 JavaScript 中手寫 MyComponent 的元件選項模板,完成的頁面卻可以正確顯示,為什麼這裡會提示“模板或渲染函式未定義”呢?

這其實與 Vue 的兩種不同的構建有關,在上述使用 webpack 打包的專案需要使用獨立構建的 Vue 庫,而在 node_modules/vue/package.json 檔案中,已經通過 main 屬性指定了通過 import Vue from ‘vue’ 或 require(‘vue’) 所引入的檔案是 dist/vue.runtime.common.js,即執行時構建的 Vue 庫。直接在 HTML 檔案中使用 script 標籤引入的是獨立構建的 Vue 庫,因此沒有問題。

從 Vue 官方教程的 Standalone vs. Runtime-only Build 我們瞭解到 Vue 有兩種不同的構建——獨立構建和執行時構建。執行時構建刪除了模板編譯的功能,因此無法支援帶 template 屬性的 Vue 例項選項。但如果你不深入地去理解 Vue 的基本概念以及編譯、掛載相關的過程,你可能仍然會很迷糊,為什麼在這個專案中會有問題?

說到 Vue 的概念,不得不說起 Vue 例項,這是一個使用 Vue 建構函式建立的 JavaScript 物件,建立 Vue 例項時,傳遞給 Vue 建構函式的引數是一個包含若干屬性和方法的物件,被稱為 Vue 例項選項物件,用於宣告所建立的 Vue 例項物件所要掛載的目標元素、data 資料、計算屬性、模板/渲染函式、例項方法以及各種生命週期鉤子回撥函式等選項。建立的 Vue 例項既然是一個 JavaScript 物件,那麼也必然擁有屬性和方法,下圖是 Vue 例項所包含的屬性與方法的示例:

可以看出,Vue 例項的屬性、方法與 Vue 選項物件的屬性、方法差別很大,然而兩者之間還是有關聯的,比如,Vue 例項的 $el 屬性是 Vue 選項物件中的 el 屬性作為選擇子所對應的 DOM 元素,Vue 例項的 $data 屬性是 Vue 選項物件中的 data 屬性(或工廠函式)經響應式處理後的物件,$options 屬性則是經 Vue 建構函式處理過的 Vue 選項物件。

我們通過這兩個物件間關係的分析,可以加深一下對 Vue 的一些基本概念的理解——模板/渲染函式、掛載點、獨立構建/執行時構建、Event Bus。

首先是模板/渲染函式。Vue 官方教程的第四部分是模板語法,介紹了插值和指令,可以通過插值語法和 Vue 指令宣告式地編寫 Vue 模板,在教程的高階篇中,介紹了渲染函式,並說明了兩者之間的關係。Vue 建構函式會將 Vue 模板編譯成用於實現資料驅動的 DOM 渲染的渲染函式,開發者也可以直接手寫渲染函式來發揮 JavaScript 的完全程式設計能力。

展開 Vue 例項的 $options 屬性,可以進一步看到除了 el 和 components 之外,多了若干個其他屬性和方法,其中就包含編譯生成的render 渲染函式。

既然 Vue 建構函式在建立 Vue 例項時會將 template 編譯成 render 渲染函式,但我們在呼叫 new Vue 時的 Vue 選項物件中並沒有包含 template 屬性,那麼 template 模板是從哪兒來的呢?

這涉及到 Vue 選項物件中的 el 屬性、template屬性和 render 渲染函式的關係問題,當 Vue 選項物件中有 render 渲染函式時,Vue 建構函式將直接使用渲染函式渲染 DOM 樹,當選項物件中沒有 render 渲染函式時,Vue 建構函式首先通過將 template 模板編譯生成渲染函式,然後再渲染 DOM 樹,而當 Vue 選項物件中既沒有 render 渲染函式,也沒有 template 模板時,會通過 el 屬性獲取掛載元素的 outerHTML 來作為模板,並編譯生成渲染函式。

換言之,在進行 DOM 樹的渲染時,render 渲染函式的優先順序最高,template 次之且需編譯成渲染函式,而掛載點 el 屬性對應的元素若存在,則在前兩者均不存在時,其 outerHTML 才會用於編譯與渲染。

下面我們通過建立三個不同的 Vue 例項來驗證一下:

html 頁面 body 內容:

<div class="app1">{{msg}}</div>
<div class="app2">{{msg}}</div>
<div class="app3">{{msg}}</div>

分別建立 Vue 例項的程式碼:

new Vue({
  el: '.app1',
  data: {
    msg: 'Hello, Vue.js.'
  },
  template: '<div>Hello, world.</div>',
  render: (h) => h('div', {}, 'Hi, there.')
})

new Vue({
  el: '.app2',
  data: {
    msg: 'Hello, Vue.js.'
  },
  template: '<div>Hello, world</div>'
})

new Vue({
  el: '.app3',
  data: {
    msg: 'Hello, Vue.js.'
  }
})

結果如下:

通過上述說明,可以很容易地理解 Vue 的獨立構建和執行時構建這兩個概念,所謂獨立構建是指能夠將 template 模板或者從 el 掛載元素提取的模板編譯成渲染函式的 Vue 庫,而執行時構建則是指不能進行模板編譯的 Vue 庫。

使用執行時庫主要是為了減少體積,同時強制預編譯所有模板,實現前端優化,Vue 的 npm 包也將 package.json 中的 main 指向了執行時構建 dist/vue.runtime.common.js,在按模組化方式引用或打包時預設使用執行時構建。

什麼場合使用獨立構建,什麼場合使用執行時構建,針對這個問題,只需要考慮清楚在專案中是否使用了模板編譯功能。

在包含單檔案元件的專案中,使用 webpack 打包時已經將單檔案元件中的模板預先編譯成了渲染函式,因此一般情況下使用執行時構建的 Vue 庫就可以了,但如果在使用 new Vue 建立 Vue 的根例項時,模板是從 el 掛載元素提取的,則需要使用獨立構建的 Vue 庫。

在使用 script 標籤引入 Vue.js 的專案中,任意例項選項或元件選項中包含了 template 模板屬性或從 el 掛載元素提取的模板時,均需要使用獨立構建的 Vue 庫。

要解決本文最開始的問題,需要在 webpack 配置中的 resolve 屬性物件中新增如下 alias 設定:

module.exports = {
  // ... other options
  resolve: {
    extensions: ['.js', '.vue'],
    alias: {
      'vue$': 'vue/dist/vue.common.js'
    }
  },
  // ... other options
}

這裡的 vue$ 表示精確匹配,由於 index.js 中還有一處大小寫錯誤 import Vue from ‘Vue’,因此需要將 from 後面的 ‘Vue’ 修改為小寫的 ‘vue’ 之後頁面才能正確顯示。

下面再考慮一個問題:構建Vue 例項時,Vue 選項中 el 屬性、template屬性和 render 渲染函式是否三者必有其一?

答案是不一定,可以使用不包含 el 屬性、template屬性和 render 渲染函式的例項選項建立 Vue 例項,事實上,如果在 Vue 例項選項中既沒有設定 el 掛載點屬性也沒有顯式呼叫 $mount 方法,是不會觸發對 render 渲染函式(無論是手工編寫還是編譯生成)的檢查的。

比較常見的是應用場景是為實現多元件間通訊而建立的 Event Bus。

建立 Event Bus:

import Vue from 'vue'

export const EventBus = new Vue()

關於使用上面這種方式建立 Event Bus 可能會出現 undefined 的問題,如果出現了,那麼,請修改為下面這樣:

import Vue from 'vue'
const EventBus = new Vue()

export default EventBus

然後就可以根據需要給 Event Bus 新增事件響應:

import EventBus from 'event-bus.js'

EventBus.$on('customEvent1', function (...params) {
  // ...
})

並在需要的時候觸發事件:

import EventBus from 'event-bus.js'

//...

EventBus.$emit('customEvent1', ...params)

這樣可以簡單解決非父子元件之間通訊的需求。

一個 vue 的學習交流群:685486827


寫在最後:約定優於配置——-軟體開發的簡約原則.


——————————–(完)————————————–

微信

更多學習資源請關注我的新浪微博….