因爲不少視頻和教程在談及 Vue 的時候,老是習慣於從 Vue-cli 談起,而組件化的開發方式相比 jQuery ,在思路上有着很大的不一樣。此外,這種開發方式每每會連帶着會使用不少其餘的技術,容易使得初學者感到不適。這裏依據本身學習 Vue 的過程,幫助你們通覽 Vue 學習中可能會使用的一些技術和知識。css
這裏談談本身目前的見解:html
當咱們調用 API 從後臺拉取數據後,若是想將這些數據渲染到頁面中,就不得不使用循環,將拼接的 DOM 對象插入頁面某一元素之中。在這一過程當中,咱們僅僅想操縱數據,卻不得不爲了渲染,而加入大量的標籤拼接和 DOM 元素操做。這一點在進行復雜的 DOM 交互的時候體現的更爲明顯。前端
咱們操做 DOM 元素,目的之一便是改變 DOM 的一些屬性(HTML 屬性、CSS 樣式、子元素等)。而這些屬性抽象來看都是數據,若是咱們可以把對 DOM 的操做變爲對數據的操做,而把數據與 DOM 之間的關聯交給另外一種機制處理,那麼,不少需求都會得以優雅的實現。vue
使用 Vue 能夠很好的解決上述問題,幫助開發者把時間更多地投入在覈心業務實現上。node
PS:固然,這不是 Vue 的惟一優勢,但已經足夠吸引咱們去學習它。react
和其餘 js 文件的引入方式相同,下載 vue.js 文件後,使用如下方式便可:webpack
<script src="./vue.js"></script>
固然也可使用 CDN 方式引入:ios
<script src="https://cdn.bootcss.com/vue/2.3.3/vue.js"></script>
注意,官方提供的 Vue 下載方式分爲開發版本和生產版本,前者能夠提供更爲全面的報錯信息。Vue 文件能夠從如下網址中下載:nginx
https://cn.vuejs.org/v2/guide/installation.html
每一個 Vue.js 應用都是git
經過構造函數 Vue 建立一個 Vue 的根實例 啓動的。
解釋下官方文檔的意思:Vue.js 起做用的全部代碼都是以 Vue 實例爲入口的(或者說都會聚集到 Vue 實例之中)。而獲得一個 Vue 實例只需使用如下代碼:
<script src="./vue.js"></script> <script> var app = new Vue({ // 配置選項 }) </script>
注:Vue 代碼須要在引入 vue.js 以後書寫。
在 Vue 實例化的代碼中,咱們能夠傳入一個 {}
對象做爲配置選項。而這其中最爲重要的是 el
,他標示着 Vue 代碼做用的 DOM 元素範圍。
<div id="app"> </div> <script> var app = new Vue({ el: '#app' }); </script>
經過設定 el : '#app'
,使得 Vue 會監控 id 爲 app 的 DOM 元素,從而使得 Vue 起到做用。
這也就意味着,除了這個 id 爲 app 的 div 以外的元素,Vue 是沒法做用的。
注意,這裏的 el
最終只會定位到一個 DOM 元素。也就是說,即便這裏寫成 .app
或 div
,也只會定位到第一個符合的 DOM。
另外一個重要的配置是 data
,標示 Vue 做用域之中可使用的變量,以下:
<div id="app"> {{ msg }} </div> <script> var app = new Vue({ el: '.app', data: { msg: 'hello' } }); </script>
使用 moustache 語法(雙花括號),Vue 會按照 {{ msg }}
尋找 data
配置中的 msg
變量,並將其與 {{ msg }}
替換。注意,這裏的 msg 是處於 data
配置中的變量。若是須要使用字符串,須要寫成 {{ 'string' }}
。
此外,data
配置對象中的每一項都會暴露出來,也就意味着咱們可使用如下方式獲得 msg
的值:
var app = new Vue({ el: '.app', data: { msg: 'hello' } }); console.log(app.msg);
而對於其餘的配置項,想要使用他們,就須要使用如下方式:
console.log(app.$el);
這也就意味着,想要獲得 msg
,咱們還可使用:
console.log(app.$data.msg);
打通了這一步,咱們已經能夠作不少事情了。在上面的代碼中,咱們將 data
中的 msg
渲染爲文本,這其實是 div#app
的 innerHTML
屬性。那麼,其餘的屬性是否是也能照本宣科地替換呢:
<div id="app"> <p title="{{ title }}"></p> </div> <script> var app = new Vue({ el: '#app', data: { title: 'p-title' } }); </script>
打開瀏覽器,在審查元素中,咱們發現 title
屬性並無被替換。而在控制檯中會顯示警告:
Interpolation inside attributes has been removed. Use v-bind or the colon shorthand instead.
這代表,咱們不能直接套用以前的方式來操做 DOM 的 HTML 屬性。想要這麼作的話,咱們須要使用指令。
爲了可以操做 HTML 屬性,咱們須要使用以下方式:
<div id="app"> <p v-bind:title="title"></p> </div> <script> var app = new Vue({ el: '#app', data: { title: 'p-title' } }); </script>
這種 v-
的方式被稱爲指令,v-bind
爲很是經常使用的一種。v-bind:title="title"
能夠將 HTML 屬性 title 替換爲 data
中的 title
。注意,這裏咱們無需再使用雙花括號形式,寫在指令中的值都會被認爲是變量而從 data
中尋找。
想要使用字符串的話,能夠加上單引號:
<div id="app"> <p v-bind:title="'string title'"></p> </div>
同理,咱們能夠類比上例來改變其餘屬性:
<div id="app"> <p v-bind:id="id" v-bind:class="myclass" v-bind:title="title"></p> </div> <script> var app = new Vue({ el: '#app', data: { title: 'p-title', id: 'p-id', myclass: 'p-class' } }); </script>
注意,這裏使用 myclass
是爲了防止與 class
關鍵字衝突。
除了使用以前 {{ xxx }}
的方式向 DOM 中插入文本值之外,咱們也能夠經過指令的方式作到這一點:
<div id="app"> <div v-text="text"></div> <div v-html="html"></div> </div> <script> var app = new Vue({ el: '#app', data: { text: '<h1> H1 TEXT </h1>', html: '<h1> H1 HTML </h1>' } }); </script>
從字面意思也能看出,使用 v-text
插入的值會以文本形式展示,其中的 HTML 標籤會被轉義,而 v-html
則會直接以 HTML 代碼的形式加入頁面之中。
在前端業務中,控制某些元素的顯隱是很常見的功能,咱們可使用 v-if
進行控制:
<div id="app"> <div v-if="canShow">SHOW</div> <div v-if="!canShow">HIDE</div> </div> <script> var app = new Vue({ el: '#app', data: { canShow: true, } }); </script>
打開瀏覽器,在審查元素中咱們能夠看到,SHOW 這一 div 會存在於頁面之中,而另外一個則不會。注意,這不是簡單的 display:none
,而是在 DOM 樹中刪除了這一節點。
若是咱們想經過 display
控制顯隱,可使用 v-show
:
<div id="app"> <div v-show="canShow">SHOW</div> <div v-show="!canShow">HIDE</div> </div> <script> var app = new Vue({ el: '#app', data: { canShow: true; } }); </script>
打開審查元素能夠看到,這裏的隱藏是經過 display:none
進行控制的。
注意,這裏咱們使用了非運算符 !canShow
。在屬性綁定之中,單運算符是可使用的,而對於如條件語句等相對複雜的計算,咱們可使用其餘的方式,見後續 computed
配置內容。
提到前端應用,循環渲染也是常用的,最多見的即是表格渲染:
<div id="app"> <table v-for="rows in tables" style="border: 1px solid #000;"> <thead> <tr> <th>序號</th> <th>姓名</th> <th>年齡</th> </tr> </thead> <tbody> <tr v-for="(row, index) in rows"> <td>{{ index + 1 }}</td> <td>{{ row.name }}</td> <td>{{ row.age }}</td> </tr> </tbody> </table> </div> <script> var app = new Vue({ el: '#app', data: { tables: [ [ { name: 'foo1', age: 'age1' }, { name: 'foo1', age: 'age1' } ], [ { name: 'foo2', age: 'age2' } ] ], } }); </script>
這裏注意,循環寫在了循環項之中,而不是循環項的父級元素,須要與其餘模板框架做區分。
形如 v-bind:xxx
的指令中,xxx
成爲指令的參數,而對於 v-bind
這種指令,參數是必定會有的,每次都寫 v-bind
顯得非常繁瑣。Vue 爲其提供了簡寫形式,使得咱們可使用 :
代替 v-bind:
書寫這類指令:
<div id="app"> <button :title="title" :id="id"> 測試按鈕 </button> </div> <script> var app = new Vue({ el: '#app', data: { title: 'btn-text', id: 'btn-id', } }); </script>
在以上指令的例子中,數據單向的由 Vue 實例輸出到 DOM 之中。在 vue.js 中,咱們還可使用 v-model
指令用以實現數據從 DOM 向 Vue 實例的輸出。
事實上,用戶能在瀏覽器 DOM 中手動改變的屬性也就是 value 值,最直觀的即是 input 標籤:
<div id="app"> <input type="text" v-model="username"> <p v-text="username"></p> </div> <script> var app = new Vue({ el: '#app', data: { username: 'default-username', } }); </script>
這裏,v-model
實現了數據的雙向綁定,從而把 input, p 標籤和 data.username
綁定在了一塊兒。
一樣的,具備 value 屬性的元素均可以經過 v-model
實現雙向綁定。
<div id="app"> <textarea name="name" rows="8" cols="80" v-model="description"></textarea> <p v-text="description"></p> </div> <script> var app = new Vue({ el: '#app', data: { description: 'default-description', } }); </script>
<div id="app"> <label for=""><input type="radio" name="gender" value="male" v-model="gender"> 男 </label> <label for=""><input type="radio" name="gender" value="female" v-model="gender"> 女 </label> <p v-text="gender"></p> </div> <script> var app = new Vue({ el: '#app', data: { gender: 'male', } }); </script>
<div id="app"> <label for=""><input type="checkbox" name="holiday" value="Monday" v-model="holiday"> 週一 </label> <label for=""><input type="checkbox" name="holiday" value="Tuesday" v-model="holiday"> 週二 </label> <label for=""><input type="checkbox" name="holiday" value="Wednesday" v-model="holiday"> 週三 </label> <p v-text="holiday"></p> </div> <script> var app = new Vue({ el: '#app', data: { holiday: [], } }); </script>
注意,多選框中須要把 holiday
設置爲 []
。
<div id="app"> <select v-model="holiday" style="width: 50px"> <option value="Monday">Monday</option> <option value="Tuesday">Tuesday</option> <option value="Wednesday">Wednesday</option> </select> <p v-text="holiday"></p> </div> <script> var app = new Vue({ el: '#app', data: { holiday: null, } }); </script>
<div id="app"> <select v-model="holiday" multiple style="width: 50px"> <option value="Monday">Monday</option> <option value="Tuesday">Tuesday</option> <option value="Wednesday">Wednesday</option> </select> <p v-text="holiday"></p> </div> <script> var app = new Vue({ el: '#app', data: { holiday: [], } }); </script>
有時,咱們可能須要對 data
中的一些值進行簡單的計算,從而獲得一個可能會複用的計算後的值。在 Vue 中,能夠經過 computed
配置實現:
<div id="app"> <h1>{{ info }}</h1> </div> <script> var app = new Vue({ el: '#app', data: { name: 'my-name', age: 'my-age', }, computed: { info: function(){ return 'Name: ' + this.name + ', Age: ' + this.age; } } }); </script>
在這裏,咱們經過 computed
配置中的 info
計算了 name
和 age
的值。而對於計算屬性 info
,它的使用和 data
是相同的。
info
會監聽 name
和 age
的變化,當他們的值改變時,info
都會改變:
<div id="app"> <input type="text" v-model="name"> <input type="text" v-model="age"> <h1>{{ info }}</h1> </div> <script> var app = new Vue({ el: '#app', data: { name: 'my-name', age: 'my-age', }, computed: { info: function(){ return 'Name: ' + this.name + ', Age: ' + this.age; } } }); </script>
在前端應用中,事件機制是必不可少的,Vue 中使用 v-on:xxx
來爲元素綁定事件。而在事件觸發機制中,事件被配置在 methods
之中:
<div id="app"> <button v-on:click="add">增長</button> <h1>{{ count }}</h1> </div> <script> var app = new Vue({ el: '#app', data: { count: 0, }, methods: { add: function(){ this.count++; } } }); </script>
在這裏,咱們經過 v-on:click="add"
爲 button 添加了一個點擊時間,並使其在點擊後觸發 methods
之中的 add
方法。
此外,咱們也常常遇到檢測鍵盤按鍵事件的需求,好比檢測回車鍵:
<div id="app"> <input type="text" v-on:keydown="keydown"> </div> <script> var app = new Vue({ el: '#app', data: { count: 0, }, methods: { keydown: function(e){ if(13 === e.keyCode){ console.log('Enter pressed.'); } } } }); </script>
Vue 中提供了一種更爲簡便的方式來針對性地檢測按鍵類別:
<div id="app"> <input type="text" v-on:keydown.enter="keydown"> </div> <script> var app = new Vue({ el: '#app', data: { count: 0, }, methods: { keydown: function(e){ console.log('Enter pressed.'); } } }); </script>
這種形如 v-on:keydown.enter
的寫法被稱爲修飾符。在 Vue 中,可使用的修飾符有不少,可以大大簡化咱們的代碼。
與 v-bind:xxx
相同,v-on:xxx
也比較經常使用。在 Vue 中,我可使用以下方式簡寫這一指令:
<div id="app"> <input type="text" @keydown.enter="keydown"> </div>
在不少前端框架中,咱們可使用 class="navbar"
來獲得一個具備導航樣式的 DOM 元素。試想,若是咱們可使用 <Navbar></Navbar>
來建立導航元素,那麼在語義層面會顯得更容易理解。頁面的佈局也會顯得更爲清晰(事實上,在 HTML5 中,也新增了一些語義化的標籤)。
想要實現這一點,咱們就須要使用 Vue 中的組件。關於組件有不少東西要說,這裏先簡單的介紹下組件的簡單用法。
<div id="app"> <navbar></navbar> </div> <script> var app = new Vue({ el: '#app', components: { 'navbar' : { template: '<div> --- hello navbar --- </div>' } } }); </script>
咱們使用 components
配置了一個名爲 navbar
的組件。然後 Vue 會根據配置,將對應的標籤替換爲 template
中的內容。
注意,這種組件註冊的方式爲局部註冊,即該組件只能在當前 Vue 實例的做用域(el
配置的 DOM 元素下)纔有效果。除此以外,咱們也能夠在全局註冊組件:
<div id="app"> <navbar></navbar> </div> <script> Vue.component('navbar', { template: '<div> --- global component --- </div>', }); var app = new Vue({ el: '#app', }); </script>
咱們能夠經過多個 Vue.component('component-name', { /*config*/ })
在全局註冊多個組件。
所謂全局,也就是說,在任意 Vue 實例中都是起做用的:
<div id="app"> <navbar></navbar> </div> <div id="foo"> <navbar></navbar> </div> <script> Vue.component('navbar', { template: '<div> --- global component --- </div>', }); var app = new Vue({ el: '#app', }); var foo = new Vue({ el: '#foo', }) </script>
直接在 template
配置中以字符串的形式寫入標籤顯得很突兀,Vue 還提供了另外一種引入模板的方式:
<div id="app"> <navbar></navbar> </div> <div id="foo"> <navbar></navbar> </div> <script type="text/x-template" id="my-template"> <div> --- global component --- </div> </script> <script> Vue.component('navbar', { template: '#my-template', }); var app = new Vue({ el: '#app', }); var foo = new Vue({ el: '#foo', }) </script>
注意,x-template
腳本務必寫在 Vue 代碼之上,否則會由於引入順序的問題致使 Vue 找不到模板。
如文檔中所說,在註冊組件的時候,對命名的方式是沒有限制的,但在使用時卻須要使用短橫線形式:
當註冊組件(或者 props)時,可使用短橫線(kebab-case) ,小駝峯(camelCase) ,或大駝峯(TitleCase) 。Vue 不關心這個。
而在 HTML 模版中,請使用 kebab-case 形式。
這也就是說,形以下列形式的組件命名:
components: { 'kebab-cased-component': { /* ... */ }, 'camelCasedComponent': { /* ... */ }, 'TitleCasedComponent': { /* ... */ } }
在使用時,都須要變爲短橫線形式:
<kebab-cased-component></kebab-cased-component> <camel-cased-component></camel-cased-component> <title-cased-component></title-cased-component>
在全局註冊方式之下,咱們能夠經過如下方式引入 data
值:
<div id="app"> <navbar></navbar> </div> <script> Vue.component('navbar', { template: '<div> {{ count }} </div>', data: { count: 1, } }); var app = new Vue({ el: '#app', }); </script>
注意,這裏的 data
值寫在了全局的 component
之中,而非 Vue 實例。
當咱們這麼寫時,瀏覽器會報錯:
The "data" option should be a function that returns a per-instance value in component definitions.
借用文檔中的方法,咱們可使用以下方式使 data 返回函數,從而跳過報錯:
let data = { count: 1 }; Vue.component('navbar', { template: '<div> {{ count }} </div>', data: function(){ return data; } }); var app = new Vue({ el: '#app', });
但當咱們這樣書寫代碼時,就會發現出現了問題:
<div id="app"> <navbar></navbar> <navbar></navbar> <navbar></navbar> </div> <script> let data = { count: 1 }; Vue.component('navbar', { template: '<button @click="count = count + 1"> {{ count }} </button>', data: function(){ return data; }, }); var app = new Vue({ el: '#app', }); </script>
這是官方文檔中的一個例子,在這一例子中,點擊任意一個 navbar
組件,都會使得 count
值加 1,這意味着組件間共用了 count
變量。
有時咱們確實須要組件間共享變量,但更多的需求是各個組件獨享,這時咱們便須要將 data
寫爲以下形式:
<div id="app"> <navbar></navbar> <navbar></navbar> <navbar></navbar> </div> <script> Vue.component('navbar', { template: '<button @click="count = count + 1"> {{ count }} </button>', data: function(){ return { count: 1 }; } }); var app = new Vue({ el: '#app', }); </script>
乍一看,這種方式和以前的寫法彷佛是同樣的(只是把 let data 替換掉而已),但是
return { count: 1 };
的寫法實際至關於:
return new Object({ count: 1 });
若是讀者在以前有了解過深拷貝和淺拷貝的知識的話,就會很容易理解這一點了。
let data = { count: 1 }; Vue.component('navbar', { template: '<button @click="count = count + 1"> {{ count }} </button>', data: function(){ return data; }, });
這種方式使得每一個組件操做的數值都是同一個 data
,而組件觸發事件後所修改的值都是同一個 data
的 count
鍵,天然會致使以前的結果。
而當代碼寫成如下形式時:
return { count: 1 };
每次的返回值都是一個新的 Object
,這樣就可使得各個組件獨享數據了。
可是問題又來了,當須要各個組件共享某一變量時該怎麼作呢?參照上述的內容,咱們可使用以下方式實現:
<div id="app"> <navbar></navbar> <navbar></navbar> <navbar></navbar> </div> <script> let store = { globalCount : 0, } Vue.component('navbar', { template: '<div><button @click="localCount += 1"> Local: {{ localCount }} </button> <button @click="store.globalCount += 1"> Global: {{ store.globalCount }} </button></div>', data: function(){ return { localCount: 1, store: store, }; } }); var app = new Vue({ el: '#app', }); </script>
類比深拷貝和淺拷貝的知識,上例會比較容易理解。在使用中,共享和獨享數據須要根據需求針對選擇。
注意,這裏咱們把 template
寫成了:
<div> <button @click="localCount += 1"> Local: {{ localCount }} </button> <button @click="store.globalCount += 1"> Global: {{ store.globalCount }} </button> </div>
是由於組件必須只有一個元素做爲根元素,直接寫成兩個 button
標籤會出現以下錯誤:
Component template should contain exactly one root element. If you are using v-if on multiple elements, use v-else-if to chain them instead.
如官方文檔所說:
當使用 DOM 做爲模版時(例如,將 el 選項掛載到一個已存在的元素上), 你會受到 HTML 的一些限制,由於 Vue 只有在瀏覽器解析和標準化 HTML 後才能獲取模版內容。尤爲像這些元素 <ul> ,<ol>,<table> ,<select> 限制了能被它包裹的元素, 而一些像 <option> 這樣的元素只能出如今某些其它元素內部。
在某些時候,咱們須要使用:
<table> <tr is="my-row"></tr> </table>
來代替:
<my-row></my-row>
你們應該會注意到,以上咱們在使用數據時,data
字段是寫於 Vue.component
之中而非 Vue 實例中的。這是由於,組件是不能直接訪問全局數據的,這一機制實際上防止了組件數據的污染。當咱們寫成以下形式時:
<div id="app"> <navbar></navbar> </div> <script> Vue.component('navbar', { template: '<div> Local: {{ count }} </div>', }); var app = new Vue({ el: '#app', data: function(){ return { count: 1, }; } }); </script>
會有報錯信息,提示 count
值找不到:
[Vue warn]: Property or method "count" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option.
當組件須要使用全局數據時,須要經過傳參的形式:
<div id="app"> <navbar :counter="count"></navbar> </div> <script> Vue.component('navbar', { template: '<div> Local: {{ counter }} </div>', props: [ 'counter' ] }); var app = new Vue({ el: '#app', data: function(){ return { count: 1, }; } }); </script>
上例中,Vue.component
配置中的 props
規定了該組件能夠傳遞的參數。以後即可以在經過 <navbar :counter="count"></navbar>
實現參數傳遞。
咱們把這種傳參方式抽象來看,這其實造成了父組件到子組件的數據流。
這裏務必注意如下兩種方式的區別:
<navbar :counter="count"></navbar> <navbar counter="count"></navbar>
根據以前數據綁定的內容,前者中的 counter
至關於 v-bind:counter
,這裏的 count
爲變量;然後者的 count
只至關於字符串 'count'
。
這裏引出另外一個問題:在 v-bind:xxx="foo"
中出現的 foo
實際上是變量名,若是想輸入字符串,須要寫成:
v-bind:xxx="'foo'" # 注意單引號
這裏涉及到字面量值的問題,好比當咱們須要傳遞數字 1 的時候,使用 counter="1"
實際傳入字符串 '1'
。我須要經過 v-bind:counter="1"
才能正確地傳入數字 1;
如今咱們知道,父組件能夠經過 props
向子組件傳遞參數,數據流實現了自根向葉的傳遞,那麼子組件如何向父組件傳遞信息呢?
在 Vue 中,咱們須要經過自定義事件來實現數據的反向流動:
<div id="app"> <navbar @my-event="pop" ></navbar> </div> <script> Vue.component('navbar', { template: '<button @click="clicker">POP</button>', methods: { clicker: function(){ this.$emit('my-event'); } } }); var app = new Vue({ el: '#app', methods: { pop: function(){ alert('A child component pops.'); } } }); </script>
在上例中,子組件捕捉 clicker
事件後,經過 this.$emit('my-event')
向上傳遞了 my-event
事件。而在 <navbar @my-event="pop" ></navbar>
中,捕獲了 my-event
這一自定義事件,從而打通了反向的通路。
但是彷佛咱們只是打通了通路,數據應該怎麼傳遞呢?在 Vue 中,咱們只需在事件函數中添加參數便可:
Vue.component('navbar', { template: '<button @click="clicker">POP</button>', methods: { clicker: function(){ let paramA = 'hello'; let paramB = 'vue'; this.$emit('my-event', paramA, paramB); } } }); var app = new Vue({ el: '#app', methods: { pop: function(pA, pB){ alert('A child component pops with params: ' + pA + ', ' + pB); } } });
使用自定義事件當然能夠作到業務邏輯的反向流動,但面對一些開源的組件庫,直接修改源碼顯然不顯示。若是此時咱們但願對一些組件綁定諸如點擊等事件,就須要使用到 native
修飾符實現。
以下代碼中,直接對組件綁定點擊事件是無效的:
<div id="app"> <child @click='clicker'></child> </div> <script src="./vue.js"></script> <script> Vue.component('child', { template: '<button>POP</button>' }); var app = new Vue({ el: '#app', methods: { clicker: function() { alert('A child component click.'); } } }); </script>
面對這種狀況,咱們須要使用 native
修飾符:
<div id="app"> <child @click.native='clicker'></child> </div> <script src="./vue.js"></script> <script> Vue.component('child', { template: '<button>POP</button>' }); var app = new Vue({ el: '#app', methods: { clicker: function() { alert('A child component click.'); } } }); </script>
在以前已經說過,咱們能夠經過如下方式實現組件渲染:
<div id="app"> <navbar></navbar> </div> <script> Vue.component('navbar', { template: '<h1>template...</h1>', }); var app = new Vue({ el: '#app', }); </script>
在 Vue 中,採用這種 template
的方式(包括 <script type='text/x-template'></script>
)後,Vue 會將其編譯爲 render
函數。形如如下寫法:
<div id="app"> <navbar></navbar> </div> <script> Vue.component('navbar', { render: function(createElement){ return createElement('h1', 'template..'); }, }); var app = new Vue({ el: '#app', }); </script>
在這裏演示了簡單的 render
函數應用,createElement('h1', 'some content')
至關於 <h1>some content</h1>
。
對於更爲複雜的模板,咱們可使用以下方式:
<div id="app"> <navbar :proptitle="mytitle"></navbar> </div> <script> Vue.component('navbar', { render: function(createElement) { return createElement('div', { domProps: { innerText: 'content..', }, attrs: { id: 'my-id', title: this.proptitle }, props: ['proptitle'] }); }, }); var app = new Vue({ el: '#app', data: function() { return { mytitle: 'my-title' }; } }); </script>
上例中,咱們使用 domProps
設置了 v-text
;使用 attrs
設置了標籤的 HTML 屬性。其中,title: this.proptitle
將 title
屬性與 proptitle
關聯了起來。
採用 template
的寫法至關於:
<div id="app"> <navbar :proptitle="mytitle"></navbar> </div> <script> Vue.component('navbar', { template: '<div id="my-id" title="proptitle">content</div>', props: ['proptitle'] }); var app = new Vue({ el: '#app', data: function() { return { mytitle: 'my-title' }; } }); </script>
能夠看出,render
函數至關於實現了標籤到對象的編譯過程。
根據文檔中所說,咱們可使用 h
做爲 createElement
的別名,從而遵照 Vue 社區的規範。
將 h 做爲 createElement 的別名是 Vue 生態系統中的一個通用慣例,實際上也是 JSX 所要求的,若是在做用域中 h 失去做用, 在應用中會觸發報錯。
即寫爲以下形式:
Vue.component('App', { render: function(h) { return h('div', { }); }, });
在 Vue 中,運行過程能夠分爲編譯爲 render
函數,以及渲染函數被調用兩步。
咱們能夠把這兩部都放在瀏覽器中進行,這種方式被稱之爲運行時構建。因爲運行時構建不會先產生編譯後的 render
函數,於是文件比較小。
除了運行時構建,咱們還能夠將編譯爲 render
函數的過程放在服務端(非瀏覽器端)進行,好比採用 vue-loader
一類的工具實現進行編譯(如進行 vue 單文件組件開發時)。而這一類便爲稱爲獨立構建。可想而知,獨立構建會產生很多 render
函數,從而使得其文件體積較大。
因爲 HTML 中並沒有標籤和屬性的大小寫區別,因此 mytitle="xxx"
和 myTitle="xxx"
是同樣的。這也就意味着,當咱們須要使用一些屬性向模板傳值的時候,小駝峯(camelCased )的命名形式須要轉爲短橫線(kebab-case)形式:
<div id="app"> <navbar :prop-title="mytitle"></navbar> </div> <script type="text/x-template" id="template"> <h1 :title="propTitle">{{propTitle}}</h1> </script> <script> Vue.component('navbar', { template: '#template', props: ['propTitle'] }); var app = new Vue({ el: '#app', data: function() { return { mytitle: 'title_content' }; } }); </script>
當咱們採用駝峯形式做爲屬性向模板中傳值時:
<navbar :propTitle="mytitle"></navbar>
實際接收到的屬性爲:proptitle
,而在定義中,註冊的屬性變量爲:propTitle
,於是不能正確的傳遞信息。此時,Vue 會在控制檯給出提示:
[Vue tip]: Prop "proptitle" is passed to component <Navbar>, but the declared prop name is "propTitle". Note that HTML attributes are case-insensitive and camelCased props need to use their kebab-case equivalents when using in-DOM templates. You should probably use "prop-title" instead of "propTitle".
在上例中,咱們使用 <navbar :prop-title="mytitle"></navbar>
的形式在 #app
做用域下使用了模板。如以前所說,這種形式會受制於 HTML 無視大小寫的問題,爲了規避這一問題,除了上述所說的短橫線形式以外,咱們還可使用其餘調用方式:
<div id="app"> <demo></demo> </div> <script> Vue.component('navbar', { template: '<h1 :title="propTitle">{{propTitle}}</h1>', props: ['propTitle'] }); Vue.component('demo', { template: '<navbar :prop-title="mytitle"></navbar>', data: function() { return { mytitle: 'title_content' }; } }); var app = new Vue({ el: '#app' }); </script>
注:這裏寫成短橫線形式也是能夠的。
在本例中,咱們使用 demo
標籤調用 demo
組件,進而調用 navbar
,注意,因爲 <navbar :prop-title="mytitle"></navbar>
並無直接以 HTML 的形式存在,而是以字符串的方式出如今 template
的配置中。如上述關於渲染函數的
介紹,這種形式能夠規避 HTML 自己大小寫不敏感而致使的問題。
同理,以前提到的組件命名約定所需保證的短橫線引用形式,在字符串模式中也是無需遵照的。
注:文檔中採用字符串模板和非字符串模板來表示兩者的區分,我的以爲這樣的描述反而容易引發歧義。實際上,不管是採用上述寫法或是用渲染函數(又或是後續會說起的單文件組件),只要可以跳過 HTML 的直接形式,均可以免大小寫不敏感而致使的問題。
文檔中提到了 Vue 實例的生命週期,以及與之相關的生命週期鉤子函數。所謂鉤子函數,便是在進入生命週期的各個階段時觸發的函數。爲了正確的使用這些生命週期鉤子函數,咱們首先須要瞭解 Vue 到底有哪些生命週期。
一個 Vue 示例的生命週期大致上爲:data 的建立,DOM 節點的建立,data 的更新,以及實例的銷燬。
根據 Vue 實例在各個階段的任務,Vue 提供瞭如下生命週期鉤子函數,用於在進入具體的某一階段時,自定義地執行一些操做:
兩者相對 Vue 實例中的 data
而言。
在 beforeCreate
階段,data
還未建立,即配置在 Vue data
中的數據還處於 undefined
狀態。
在 created
階段,data
按照配置被賦予相應的值。
兩者相對於 DOM 而言。
在 beforeMount
階段,最終的 DOM 以虛擬 DOM 的方式存在。
而在 mounted
階段,DOM 被建立,且 DOM 中引用的 Vue 中的 data 被替換成相應的值。
兩者相對於 data
的更新而言。
在 Vue 生命週期之中,data
會不斷地發生變化,每一次變化都會一次觸發 beforeUpdate
和 updated
。
兩者相對於 Vue 實例的銷燬而言。
咱們經過如下代碼進行測試:
<div id="app"> {{ username }} </div> <script> var app = new Vue({ el: '#app', data: { username: 'dailybird', created_at : '2017.6.12' }, beforeCreate: function(){ console.log(this.username); console.log(this.$el); }, created: function(){ console.log(this.username); console.log(this.$el); }, beforeMount: function(){ console.log(this.username); console.log(this.$el); }, mounted: function(){ console.log(this.username); console.log(this.$el); }, beforeUpdate: function(){ console.log('before update'); }, updated: function(){ console.log('updated'); }, beforeDestroy(){ console.log('before destroy'); }, destroyed(){ console.log('destroyed'); } }); app.username = 'dailybirdo'; setTimeout('app.$destroy()', 100); </script>
瀏覽器控制檯中輸出的結果爲:
注意,這裏在調用 app.$destroy()
以前延遲了一段時間,是防止 Vue 實例銷燬之時,數據的更新尚未完成的狀況。
那麼,這些生命週期鉤子函數應該用於什麼場景和需求呢?
對於一些網頁應用而言,數據的獲取是網頁在瀏覽器中加載時異步進行的。也就是說,頁面框架和數據渲染不是同時推送到瀏覽器中的。參考於 Vue 實例的兩個重要階段:created
和 mounted
,兩者分別表示 data
和 DOM 的建立完成時機。
若是咱們在 mounted
之時進行數據接口調用,而此時 DOM 元素已經渲染,就會出現頁面元素中的 data
值先填充爲 Vue 實例中配置的默認 data
,而後被後臺返回的真實數據替換的狀況。
於是,這裏推薦在 created
階段進行數據獲取 API 的調用。儘管因爲異步緣由,仍可能出現數據返回之時,Vue 實例已經進入 mounted
階段,但這已經是相對合適的調用時機了。
若是咱們選擇在 beforeCreated
階段調用 API,此時 data
尚未被建立,若是 API 返回的速度很快,早於 created
,就會出現真實的後臺數據被 Vue 配置中的 data
覆蓋的狀況。
爲了優化因爲異步調用方式而可能形成問題,咱們能夠在 beforeCreated
階段展現一個加載框,或使用合適的默認值如:正在加載數據...
來提高用戶體驗。
關於生命週期鉤子的詳細應用能夠參考:Vue 實例中的生命週期鉤子詳解。
做爲新的 JavaScript 標準,ES6 引入了不少新的特性和語法,爲開發帶來了便捷。然而如今還有不少瀏覽器不支持 ES6,這一點的解決方法咱們會在以後談及 Webpack 和 Bebel 時提到。
如下介紹一些在 Vue 開發中必備的 ES6 知識,至於所有的 ES6 內容,你們能夠參考 ECMAScript 6 入門。
let
關鍵字改變了 JavaScript 語言沒有塊級做用域的狀況。
在原先使用 var
關鍵字的狀況下:
var i = 0; while(true){ i = 2; break; } console.log(i); // i = 2
而改成 let
後:
let i = 0; while(true){ let i = 2; break; } console.log(i); // i = 0
還有很是經典的閉包問題:
for (var i = 0; i < 4; i++) { setTimeout(function(){ console.log(i); }, 1000 * i); } // 每隔一秒輸出一個 4
以前的解決方式是使用函數級做用域造成閉包:
for(var index = 0; index < 4; index++){ (function(i){ setTimeout(function(){ console.log(i); }, 1000 * i); })(index) } // 每隔一秒依次輸出 0 - 3
而如今,咱們可使用:
for (let i = 0; i < 4; i++) { setTimeout(function(){ console.log(i); }, 1000 * i); } // 每隔一秒依次輸出 0 - 3
有關閉包和做用域問題能夠參考以前的撰文:談談 setTimeout 這道經典題目。
爲了保持與其餘語言在變量做用域上的統一,以避免跳進大坑,建議使用 let
關鍵字。
對象和 JSON 看上去很像,都是使用花括號包裹,都是鍵值對的形式。但兩者仍有不少不一樣:
對象中鍵能夠不加引號,但 JSON 中鍵必須用雙引號包裹(不能使用單引號);
對象的值能夠爲函數,而 JSON 的鍵值都必須爲字符串;
JSON 中,最後一項的末尾不能加逗號,而對象無此限制。
通常,咱們可使用以下方式定義一個對象:
let obj = { keyA: "valueA", keyB: "valueB" }; console.log(obj);
有時,當鍵值同名時,咱們仍然不得不寫成以下形式:
let keyA = "keyA"; let keyB = "keyB"; let obj = { keyA: keyA, keyB: keyB }; console.log(obj);
而如今,咱們可使用如下方式簡化寫法:
let keyA = "keyA"; let keyB = "keyB"; let obj = { keyA, keyB }; console.log(obj);
當咱們須要在一個對象中將鍵與一個函數對應時,一般會寫成以下寫法:
let obj = { name: 'My name', funcA: function(){ console.log(this.name); } } obj.funcA();
如今咱們能夠簡化成以下寫法:
let obj = { name: 'My name', funcA(){ console.log(this.name); } } obj.funcA();
以上兩點在 Vue 的組件化開發之中很是經常使用。
有時,咱們會向函數中傳入對象的值,好比表單數據,以下:
let obj = { username: 'my-username', password: 'my-password', }; function destruct(username, password){ console.log(username, password); } let username = obj.username; let password = obj.password; destruct(username, password);
如今,咱們能夠經過解構的方式從新定義形參:
let obj = { username: 'my-username', password: 'my-password', }; function destruct({username, password});{ console.log(username, password); } destruct(obj);
這樣能夠簡化必定的代碼。
咱們可使用常量在對象中定義函數:
let obj = { funcname(){ console.log('xxx'); } }
能夠寫成:
let FUNC_NAME = 'funcname'; let obj = { [FUNC_NAME](){ console.log('xxx'); } }
將函數名稱提取成常量方式能夠必定程度上減小函數名修改致使的反作用。
一般,咱們使用以下方式定義並調用函數:
function func(name, age){ console.log(name, age); } func('dailybird', 22);
如今,咱們能夠簡化它:
(name, age) => { console.log(name, age); }
你們能夠注意到,採用箭頭函數的寫法更適合於匿名函數的場景。在 JavaScript 中,匿名函數出現最爲頻繁的場景便是回調函數。如異步 API 調用後的數據獲取:
_.post((response) => { let data = response.data; })
此外,箭頭函數還能夠明確 this
指向問。在如下代碼中:
let obj = { name: 'dailybird', getName: function(){ setTimeout(function(){ console.log(this.name); }, 1000); } } obj.getName();
getName
並不能獲得 name
值,這是由於套在 setTimeout
之中匿名函數的 this
指向全局空間。
咱們可使用 bind
進行 this
的綁定:
let obj = { name: 'dailybird', getName: function(){ setTimeout(function(){ console.log(this.name); }.bind(this), 1000); } } obj.getName();
也可使用 self = this
避免 this
指向被調換:
let obj = { name: 'dailybird', getName: function(){ // 這裏的 let 和 var 並沒有區別 let self = this; setTimeout(function(){ console.log(self.name); }, 1000); } } obj.getName();
如今,咱們也能經過箭頭函數來解決這一問題:
let obj = { name: 'dailybird', getName: function(){ setTimeout(() => { console.log(this.name); }, 1000); } } obj.getName();
在 Vue 應用中,會大量出現調用接口後,在回調函數中對所屬實例的數據進行修改的狀況。這時,妥善處理 this
指向問題就顯得極爲重要。
Node.js 是一個比較大的話題,這裏只說一些與 Vue 開發有關的內容。
後續的內容會提到:咱們可使用高階的 JavaScript 語法和代碼規範進行項目開發,而後使用相似「編譯」的方式「開發模式」的代碼「編譯」成大多數瀏覽器能夠運行的「發佈模式」代碼。
而爲了提供支持這一過程的環境,咱們須要 Node.js 及其包管理工具 npm
。
經過 Node.js 環境,咱們能夠在非瀏覽器環境使用 JavaScript 代碼,輸出大多數「瀏覽器」支持的 JavaScript。
Node.js 的安裝相對傻瓜,從 官方網站 下載後,雙擊安裝便可,這裏推薦使用最新版本。
安裝完成後,在命令行執行如下代碼能夠查看安裝的 Node.js
版本,以及氣包管理工具 npm
的版本:
# Node.js 版本 node -v # Node.js 的包管理工具 npm 的版本 npm -v
完成這一步以後,咱們即可以開始以後的內容了。
儘管 ES6 提供了大量新語法,能夠簡化編碼、提升開發效率,但瀏覽器兼容性問題一直都是 B/S 模式的通病。直至目前爲止,各個瀏覽器對 ES6 的兼容程度都不樂觀。這也就意味着,即便是最新的主流瀏覽器,都沒法保證能夠正常運行 ES6 語法的 JavaScript 代碼,更不用提那些老版本的瀏覽器了。
爲了使咱們既能使用 ES6 的新語法,又不會受制於瀏覽器版本問題,咱們須要考慮一種相似「編譯」的過程,將原有的 ES6 代碼「編譯」爲 ES5。這樣一來,咱們既能夠在開發環境中使用簡潔、語義化更強的新語法,又能夠經過「編譯」後的低版本代碼兼容更多的瀏覽器。
咱們可使用 Babel 在線編譯快速體驗這一過程,訪問 Babel 首頁展現 或 Babel 在線編譯 都可。
下面咱們就嘗試將以前提到的 ES6 語法進行轉義。
當不涉及到做用域問題時,let
直接編譯爲 var
。
下面咱們嘗試對以前提到的塊級做用域問題進行編譯:
能夠看到,Bebel 面對這一問題的解決思路和咱們以前的想法是一致的。
能夠看到,這裏編譯的結果顯示解構傳參實際上就是語法幫助咱們簡化了步驟。
注意,正由於這種寫法,在函數體內是不對 obj
產生直接修改的。
若是須要修改 obj
的值,須要使用非解構方式,經過 obj.username = 'xxx'
實現修改。
代碼會被編譯爲:
以前提到,箭頭函數能夠改善 this
指向問題,咱們看看 Babel 的解決方案:
能夠看出,Babel 也是使用將 this
關鍵字使用局部變量進行保存的方式改善了指向問題。
首先,咱們在項目目錄下執行:
npm init
一路回車確認後,可看到項目下出現了 package.json
文件,其中記錄了當前項目的依賴管理配置。爲了使得 Bebel 可以正常起到做用,如今執行:
npm install --save-dev babel-preset-es2015
能夠看到項目中出現了一個新的文件夾 node_modules
。這個文件夾相似於項目的依賴庫。此外,執行以後,在 package.json
之中,會增長這一句配置:
"devDependencies": { "babel-preset-es2015": "^6.24.1" }
注意到,安裝時添加了 --save-dev
的參數,這一參數的意義在於將所安裝項目定義爲開發時使用的(由於 Babel 的做用只是在代碼運行前進行編譯)。
相比之下,對於一些須要在運行時使用的庫,如 jQuery
、Angular
或 Vue
等,安裝時須要添加的參數即爲 --save
而非 --save-dev
了。以下:
npm install some-component --save
在這句代碼執行完畢以後,package.json
中會增長:
"dependencies": { "some-component": "^version" }
這以後,咱們還須要在全局安裝 Babel 轉碼工具。執行如下代碼:
npm install --global babel-cli
這裏咱們使用了另外一個參數 --global
,這一參數表示本次安裝會在全局起做用。
想要使用 Babel 進行編譯,除了進行上一步驟的安裝以外,還須要對編譯進行一些配置,而這些配置須要存於 .babelrc
文件中。
該文件對轉碼規則和用到的插件進行了定義,以下:
{ "presets": [ "es2015" ], "plugins": [] }
這裏咱們定義了轉碼規則爲 es2015
,即 ES6
。更多的轉碼規則能夠參考 Babel 入門教程。
下面咱們建立一個文件 origin.js
,書寫一些帶有 ES6 新語法的代碼,如以前提到的做用域問題:
// origin.js for (let i = 0; i < 4; i++) { setTimeout(function(){ console.log(i); }, 1000 * i); }
而後執行如下指令(注意,.babelrc 文件須要事先建立):
babel origin.js
能夠看到命令行中打印出了編譯後的結果。
但是咱們須要把編譯後的結果輸出到一個文件之中,此時須要執行:
babel origin.js -o output.js
這樣,咱們就能夠在 output.js
中看到編譯的結果了。
關於 Babel 的話題就談到這裏,更多深刻的內容能夠參考其餘文檔。
Babel 幫助咱們解決了 ES6 代碼瀏覽器兼容性的問題。爲了實現前端工程化開發,咱們還須要一個功能的支持,即在 JavaScript 代碼中引入其餘的 JavaScript 代碼。
只有完成這一點,咱們才能將 JavaScript 代碼依據邏輯進行拆分,而後經過某種方式組合起來,從而實現組件化、模塊化的開發。
因爲 webpack 的內容和使用方式不少,對 webpack 更深刻的瞭解能夠參考其餘博客。這裏只介紹最爲重要的部分,即 import 和 export。
webpack 做爲一個指令,建議經過全局的方式安裝,這樣一來,咱們就能夠全局使用 webpack
指令了:
npm init npm install -g webpack
在安裝過程當中,可能會出現如下報錯信息:
npm ERR! Refusing to install webpack as a dependency of itself
這是由於 package.json
的 name
值也被寫成了 webpack
,修改便可,可參考:局部安裝webpack提示沒法依賴。
爲了可以正常使用 webpack,咱們須要對其進行一些配置。固然,webpack 的配置項不少,這裏只介紹最爲重要的一部分,即 export
和 import
,更多有關 webpack 的知識能夠參考這一套視頻教程:【DevOpen.Club 出品】Webpack 2 視頻教程。
咱們在項目根目錄下新建 webpack.config.js
文件,這是 webpack 配置文件的默認名。內容以下:
const config = { entry: __dirname+'/app/entry.js', output: { path: __dirname+'/dist', filename: 'bundle.js' }, module: { rules: [ ] } }; module.exports = config;
這一配置文件指定了入口文件,即 /app/entry.js
,以及打包後的輸出文件 /dist/bundle.js
。
在模塊化開發之中,一般會將 JavaScript 代碼分散到多個文件之中。這時,咱們須要使用 import
和 export
關鍵字關聯起 JavaScript 代碼,從而提升代碼的複用能力。這一點在 Vue 的組件化開發之中體現地尤其明顯。藉助 webpack,咱們能夠從一個入口文件開始,經過導入導出,實現代碼的組合,以及項目的工程化。
首先,咱們在 app
目錄下新建 entry.js
文件:
import API from './componentA' console.dir(API);
這裏引入了 componentA.js
,注意,文件後綴是能夠省略的。
以後,咱們新建這一文件:
const GET_PRODUCTS = 'www.xxx.com/products'; export default { GET_PRODUCTS }
即 entry.js
引入了 componentA.js
中的輸出,從而引入了其中的 GET_PRODUCTS
變量。
如今,咱們在項目根目錄下執行 webpack
,能夠看到提示信息:
提示咱們已經根據配置,生成了 dist/bundle.js
。
然後,咱們在根目錄下新建 index.html
並引入 bundle.js
:
<script src="./dist/bundle.js"></script>
打開後,能夠在瀏覽器控制檯中看到輸出效果:
ERROR in Entry module not found: Error: Can't resolve 'xxx/webpack/app' in 'xxx/webpack'
這是由於 webpack.config.js
的 entry 只能指定爲單個 JavaScript 文件,當指定爲目錄時,默認爲該路徑下的 index.js
文件。若是入口文件名不是 index.js
,會拋出錯誤。
在輸出文件中,能夠有兩種形式,下面咱們介紹這兩種形式的使用方式:
導出方式:
const GET_PRODUCTS = 'www.xxx.com/products'; export GET_PRODUCTS;
導入方式:
import {GET_PRODUCTS} from './componentA' console.log(GET_PRODUCTS);
注:一個輸出文件中能夠有多個 export
。
導出方式:
const GET_PRODUCTS = 'www.xxx.com/products'; export default { GET_PRODUCTS }
導入方式:
import API from './componentA' console.log(API.GET_PRODUCTS);
注:一個輸出文件中只能有一個 export default
。
進行 Vue 組件化開發須要 webpack 及一系列插件的支持,自行配置 package.json
和 webpack 相對麻煩,爲了簡化這些繁瑣的重複工做,咱們可使用 Vue 腳手架工具來快速構建項目結構。
執行如下指令,全局安裝 Vue-cli:
npm install --global vue-cli
以後咱們能夠執行如下指令,建立一個 Vue 工程:
vue init webpack project_name
這條指令中,webpack 表示該 vue 工程使用 webpack 的方式進行打包。而 project_name 表示自定義地項目名字。
執行以後,會依次詢問一些信息,如項目名稱、項目描述、做者等會做爲項目信息存於 package.json
之中。構建方式、vue-router。
? Project name (xxx) # 項目名稱 (隨意填寫) ? Project description (A Vue.js project) # 項目描述 (隨意填寫) ? Author (yangning <yangning3@jd.com>) # 做者 (隨意填寫) ? Vue build (Use arrow keys) # 構建方式 (回車便可) ? Install vue-router? (Y/n) # 是否使用 vue-router (可選,有關 vue 路由會在後續小節中提到) ? Use ESLint to lint your code? (Y/n) # 是否使用 ESlint 規範代碼 (建議 N) ? Setup unit tests with Karma + Mocha? # 是否使用 Karma + Mocha 測試框架(建議 N) ? Setup e2e tests with Nightwatch? (Y/n) # 是否使用 Nightwatch 端到端測試框架(建議 N)
這以後,咱們須要使用如下指令實現安裝(能夠在 package.json
中找到依賴的庫):
npm install
如下對 Vue 工程中的部分文件進行介紹。
在 package.json
中能夠找到以下信息:
"scripts": { "dev": "node build/dev-server.js", "start": "node build/dev-server.js", "build": "node build/build.js" },
根據以前所說,scripts
中的配置能夠做爲指令,以 npm run xxx
的方式執行。
在這裏,使用 scripts
中定義的指令的效果以下:
npm run dev # 啓動熱部署,爲開發模式 npm run build # 同 npm run dev npm run build # 使用發佈模式編譯,生成 /dist 文件做爲編譯結果
Vue 項目中的 webpack 比較複雜,這裏對重要的地方進行介紹。
這是 webpack 的基礎配置,其中在 module
中使用了 vue-loader
,babel-loader
進行處理。
在 resolve
中,能夠看到以下配置:
resolve: { extensions: ['.js', '.vue', '.json'], alias: { 'vue$': 'vue/dist/vue.esm.js', '@': resolve('src') } },
其中,alias
中定義了 @
符號用以指向項目根目錄下的 src
文件夾。
在 Vue 項目中,如何正確找到如圖片、字體文件、第三方庫文件等是相對重要的事情。如下提供幾種正確使用路徑的方法。
在 Vue 中,常常須要引入第三方庫,好比在 /src/main.js
文件中,使用如下方式引入了 Vue.js:
import Vue from 'vue'
採用這種方式引入的庫文件,會在 /node_modules
中進行尋找。
前端頁面中不可避免地會使用到不少靜態資源文件,引用這些靜態文件能夠經過如下方式:
相對路徑
採用相對路徑固然可行,但對於那些層級較深的文件而言,引用較低層級的靜態資源就比較麻煩了。並且,因爲字體文件等編譯後與 js 文件的相對路徑會發生改變,因此也不適合使用相對路徑引用。
使用 @
符號
如上所說,@
符號被定位到了 /src
中,因此咱們可使用 @/dir/file.png
定位到 /src/dir/file.png
。
注意,因爲 @
符號的解析只會默認在 JavaScript 中生效,若要在 HTML 中使用 @
,咱們須要使用 ~@
,如:
<script src="~@/dir/file.png"></script>
使用 /
在 Vue 項目中,/
被指向 /static
目錄,一般用於字體文件及圖片等資源的引入。
咱們可使用如下指令開啓熱部署模式以進行開發調試:
npm run dev
執行後瀏覽器會自動打開一個窗口,並訪問:
localhost:8080/#/
因爲熱部署的存在,咱們對代碼的修改都會馬上同步到瀏覽器中。
在 Vue 項目中,咱們能夠新建 .vue
後綴的文件,做爲一個單獨的組件。該組件的模板以下:
<style scoped> </style> <template> <div> </div> </template> <script> export default { } </script>
在 .vue
文件中,包含了一個組件的三個部分,即 HTML 結構部分(寫於 template
之中);CSS 樣式部分(寫於 style
之中);以及 Vue 實例對象部分(寫於 export default{ }
之中)。
在這裏,export default{ }
實際至關於 export default new Vue({ })
。即一個 .vue
文件實際提供了一個組件樣式,及以此組件爲做用域的 Vue 實例。
注意,在 JavaScript 代碼中,咱們能夠省略 el: xxx
,即對做用域的聲明。在 vue
文件中,其做用域爲 template
標籤下的第一層標籤。這也就是說,下面這兩種種寫法是不正確的:
<template> <div> </div> <div> </div> </template> <template> <div v-for="item in list"> </div> </template>
使用這種寫法時,會拋出以下錯誤:
Component template should contain exactly one root element. If you are using v-if on multiple elements, use v-else-if to chain them instead.
須要保證 template
標籤下只有一個父級標籤,循環和多 if 語句這種可能致使出現多個標籤的行爲也不容許。
在不少站點中,頁面都會配備首部導航條、頁腳,而頁面間的切換通常只會致使中心內容區的變更。
採用傳統方式開發的頁面存在如下兩個問題:
在不使用後端模板引擎的狀況下,須要在每一個頁面中重複書寫首部導航條和頁腳;
即便使用了後端模板引擎,每一次頁面跳轉也都會致使頁面的所有刷新,而非內容區的局部刷新。
第二個問題涉及到前端路由和單頁應用,咱們會在後續的 Vue Router 中討論。
爲了解決第一個問題,咱們能夠考慮把後端模板引擎的工做放到前端完成,也就是 Vue 組件。
如今,咱們在 /src/components
文件夾下新建兩個文件,分別是:Navbar.vue
和 Footbar.vue
,其中代碼以下:
// Navar.vue <style scoped> div{ background-color: tan; } </style> <template> <div> 頭部導航條 {{ msg }} </div> </template> <script> export default { data () { return { msg: 'navbar' } } } </script>
// Footbar.vue <style scoped> div{ background-color: deepskyblue; } </style> <template> <div> 底部導航條 {{ msg }} </div> </template> <script> export default { data () { return { msg: 'navbar' } } } </script>
這樣,咱們就新建了兩個 Vue 組件,分別做爲首部導航條和頁腳;
而後咱們修改 /src/App.vue
文件,內容以下:
<style> div{ background-color: aquamarine; } </style> <template> <div> <Navbar></Navbar> 中心內容 <Footbar></Footbar> </div> </template> <script> import Navbar from './components/Navbar' import Footbar from './components/Footbar' export default { components: { Navbar, Footbar } } </script>
這裏,咱們引入了以前所寫的兩個 Vue 組件。對比前幾節中提到的 Vue 組件,這裏的不一樣只不過是把每一個組件都放到了一個單獨的 .vue
文件中而已。
建立、修改文件並點擊保存後,因爲以前啓動了 npm run dev
,即熱部署,此時,瀏覽器會自動刷新並顯示以下效果:
這裏能夠注意到兩點:
雖然 Navbar
和 Footbar
中均有 msg
這一數據,但兩者並不衝突;
Navbar
、Footbar
、App
這三個組件中均對 div
標籤訂義了背景顏色,但三者並未在樣式上相互覆蓋。
這代表一個 Vue 組件的樣式、數據等均是與其餘組件隔離的。
如下演示 App
組件向 Navbar
組件傳參的方法:
修改 Navbar.vue
代碼爲:
<style scoped> div{ background-color: tan; } </style> <template> <div> 頭部導航條 {{ msg }} {{ navbarMsg }} </div> </template> <script> export default { props: [ 'navbarMsg' ], data () { return { msg: 'navbar' } } } </script>
修改 App.vue
的代碼爲:
<style> div{ background-color: aquamarine; } </style> <template> <div> <Navbar navbar-msg="params"></Navbar> 中心內容 <Footbar></Footbar> </div> </template> <script> import Navbar from './components/Navbar' import Footbar from './components/Footbar' export default { components: { Navbar, Footbar } } </script>
這裏的傳參方式和以前提到的相同。注意,navbar-msg
這裏涉及到字面量問題,你們能夠翻看以前的內容。
保存後,頁面會更新爲:
如下演示 Footbar
中想讓傳遞事件的方法:
修改 Footbar.vue
爲:
<style scoped> div{ background-color: deepskyblue; } </style> <template> <div @click="clicker"> 底部導航條 {{ msg }} </div> </template> <script> export default { data () { return { msg: 'navbar' } }, methods: { clicker(){ console.log('component click'); this.$emit('navbar-click', 'params1', 'params2'); } } } </script>
修改 App.vue
爲:
<style> div{ background-color: aquamarine; } </style> <template> <div> <Navbar navbar-msg="params"></Navbar> 中心內容 <Footbar @navbar-click="trigger"></Footbar> </div> </template> <script> import Navbar from './components/Navbar' import Footbar from './components/Footbar' export default { components: { Navbar, Footbar }, methods: { trigger(param1, param2){ console.log('get event with', param1, param2); } } } </script>
這樣,當咱們點擊底部導航條後,控制檯會輸出:
component click get event with params1 params2
這裏要注意兩點:
子組件拋出 ( emit ) 事件時,並不考慮父組件會不會捕獲該事件。至關於只提供接口,而不依賴它;
父組件捕獲事件時並不會干涉到子組件的執行,使用 return false;
也不能夠。
組件傳參和事件傳遞實際構成了組件間數據傳遞的正反線路。但從以上的使用方式中,能夠感受到採用這兩種方式進行數據傳遞的侷限性。
全局狀態難以維護;
同級組件間數據難以傳輸;
存在大量只起到傳輸數據做用的事件;
API 調用過程缺乏管理方案,會存在重複的 API 請求代碼;
調用 API 之類的異步請求和修改數據的同步操做糅合在一塊兒。
而 Vue 自己是推薦在項目中使用數據驅動運行的。面對組件間的複雜關係,咱們須要一種全局的數據管理工具來幫助咱們方便的獲取數據,並基於此知足一系列的需求。
此外,針對不可避免的 API 請求,咱們也須要一種機制進行管理。
因此,咱們須要一種狀態管理器來幫助咱們完成這些功能,這就是 Vuex。
如上所說,Vuex 是一個狀態管理機制,那麼,狀態是指哪些呢?
在這裏,Vuex 須要幫咱們維護的內容分爲三類:
存儲數據的 state;
同步修改數據的 mutations;
異步獲取數據的 actions;
按下 CTRL + C
退出熱部署,執行如下指令進行 vuex 的安裝:
npm install vuex --save
而後修改 main.js
:
在文件首部添加:
import Vuex from 'vuex' Vue.use(Vuex);
而後實例化一個 Vuex.Store 對象:
let store = new Vuex.Store({ state: { navbar: { title: 'navbar-title' } }, mutations: {}, actions: {} });
注意,這裏包含了 state
, mutations
和 actions
。
而後在 new Vue({})
中加入 store
:
new Vue({ el: '#app', store, template: '<App/>', components: {App} })
注意,這裏加入 store
意味着咱們能夠其餘 Vue 實例中使用 this.$store
來獲得實例化的 Vuex.Store
對象。
自此,咱們即可以經過修改 store
變量中的 state
,mutations
和 actions
來實現狀態管理了。
如上所示,咱們已經在 state
中添加了 navbar
對象,至關於將其交給 Vuex 進行管理:
let store = new Vuex.Store({ state: { navbar: { title: 'navbar-title' } }, mutations: {}, actions: {} });
而後,咱們能夠在 Vue 組件中獲得該值。好比 Navbar
組件(這裏省略了 style
部分):
<template> <div> 頭部導航條 {{ title }} </div> </template> <script> export default { data () { return { } }, computed: { title(){ return this.$store.state.navbar.title; } } } </script>
注意,這裏咱們使用 computed
來獲得 state
中管理的值,是方便在 state
中的值發生變化時同步更新。有關 computed
和 data
在 Vuex 中的使用能夠參考 「獲取 vuex state 中的值必定要使用 computed 而不能使用 data 嗎?」。
而後咱們能夠經過如下方式對其進行修改,這裏咱們經過一個點擊事件實現對 state
中值的修改:
// Navbar.vue <template> <div @click="trigger"> 頭部導航條 {{ title }} </div> </template> <script> export default { data () { return { } }, computed: { title(){ return this.$store.state.navbar.title; } }, methods: { trigger(){ this.$store.state.navbar.title = 'after modified'; } } } </script>
固然,在 Vuex 中,並不推薦直接使用這種方式對 state
中管理的內容進行修改,從規範上來講,一切的修改操做都應該在 mutations
中進行。
咱們修改 main.js
爲其添加 mutations
以下:
let store = new Vuex.Store({ state: { navbar: { title: 'navbar-title' } }, mutations: { setNavbarTitle(state, value){ state.navbar.title = value; } }, actions: {} });
注意,mutations
中的方法接收兩個參數,第一個參數即爲 store.state
,第二個是附帶的值,通常爲修改的目標值(在官方文檔中,這一參數被稱爲載荷 ( payload ) )。注意,mutations
中的方法只能接收兩個參數。
調用 mutations
中的方法可使用 commit
進行,commit
一樣須要傳遞兩個參數,第一個參數爲所需調用的 mutations
中的方法名,第二個參數爲目標值,即等同於 mutations
方法中的第二個參數。
咱們修改 Navbar.vue
中的 trigger
以下:
methods: { trigger(){ this.$store.commit('setNavbarTitle', 'after modified'); } }
這樣一來,此後全部涉及到對 state
中 navbar.title
內容的修改均可以使用這一方式進行。
在網頁開發中,大量的需求都伴隨着 API 的調用,相比於 mutations
對 state
中數據的同步修改,此類 API 的調用過程屬於異步。整個過程爲:先異步調用後臺接口得到數據,再同步將數據更新到 store.state
中。
在 Vuex 中,後者使用 mutations
進行操做,而前者則是使用 actions
進行。因爲調用後臺接口的過程涉及到一些其餘的庫和跨域問題,咱們這裏暫且使用僞代碼,僅爲了理清程序的邏輯。
首先,咱們修改 store
的 actions
:
let store = new Vuex.Store({ state: { navbar: { title: 'navbar-title' } }, mutations: { setNavbarTitle(state, value){ state.navbar.title = value; } }, actions: { loadNavbarTitle(context, appendix){ // 調用後臺 API 的僞代碼 let title = _.get('http://api.xxx.com/navbar/title'); context.commit('setNavbarTitle', title); } } });
而後修改 Navbar.vue
中的 trigger
:
trigger(){ this.$store.dispatch('loadNavbarTitle', 'test'); }
類比 mutations
,咱們使用 dispatch
調用 actions
,其中第一個參數爲 actions
的方法名,第二個爲附帶參數。
而在 actions
中,會存在兩個參數,第一個爲 context
,即爲 store
(注意區別 mutations
的第一個參數),第二個爲附帶參數。
有時,咱們可能想要把對數據的更新或 API 的調用切分紅更小的部分,即從一個入口 mutations
調用多個顆粒度小的 mutations
,或者是一個 actions
調用多個其餘的 actions
。
若是咱們想要在 mutations
或 actions
中繼續調用其餘的 mutations
或 actions
,只能試圖經過方法中的參數或 this
進行。
讓咱們觀察一下 mutations
中得到的參數:第二個參數爲傳遞的附加參數,天然不能作些什麼;第一個參數爲 store.state
,經過這一值已經沒法再調用 mutations
或 actions
了。
那麼 this
關鍵字呢?當咱們在其中使用 console.log(this)
時,發現輸出爲 undefined
。事實上,this
指針已經沒法再定位到 store
了。
如今咱們考慮 actions
,以前已經說過,actions
的第一個參數爲 store
,而經過這一參數,咱們能夠繼續使用 commit
或 dispatch
操做 mutations
或 actions
,或者直接操做 state
。
基於以上,咱們發現 actions
還具備另外一種使用場景,即經過 actions
繼續分發多個 mutations
(或 actions
),從而更靈活的組織代碼,以減小代碼的冗餘。
參考如下示例:
let store = new Vuex.Store({ state: { navbar: { title: 'navbar-title' } }, mutations: { setNavbarTitle(state, value){ state.navbar.title = value; }, test1(state){ console.log('test1'); }, test2(state){ console.log('test2'); }, }, actions: { loadNavbarTitle(context){ // 分發到兩個 mutations 之中 context.commit('test1'); context.commit('test2'); } }, func(){ console.log('func'); } });
經過以上鋪墊,咱們即可以祭出官方給出的狀態管理架構圖:
根據以前給出的代碼,咱們能夠獲得 Vuex 的通常使用流程:
組件捕獲事件,調用 dispatch
觸發 actions
;
在 actions
中調用後臺 API 獲取數據;
調用 commit
觸發 mutations
更新存儲於 state
之中的數據;
組件中的 computed
更新到組件的視圖中。
以上過程造成了閉環。也是官方標準的數據流。但在使用中,若是部分過程無需調用後臺 API,也能夠由組件直接調用 commit
觸發 mutations
,從而跳過沒必要要的 actions
。
從以前的介紹中能夠隱約感受到,若是咱們將全部的 state
,mutations
,actions
都放在 Vuex.Store({})
中的話,隨着項目規模的擴大,其中的內容會愈來愈多,這樣會致使代碼難以維護。爲了不這個問題,咱們須要將不一樣職責的 store
代碼進行拆分。
官方給出了推薦的項目結構:
在這一目錄結構中,state
中的值被拆分爲各個模塊(modules),並連同 actions
、mutations
一塊兒在 index.js
進行聚集。
這其中,actions
和 mutations
包含全局性質的操做。而把每一個小模塊的 state
,mutations
,actions
都寫在單獨的 JavaScript 代碼中並在 modules
中聚集。
有關 modules
和官方目錄結構還有不少東西能夠談及,留待以後再撰文。
在 vue 中,咱們可使用一些庫實現後臺 API 的調用,比較經常使用的由 vue-resource
和 axios
,這裏簡單介紹 vue-resource
。
和其餘的庫同樣,使用 npm
安裝便可:
npm install vue-resource --save
相似於 Vuex 的使用,咱們須要在 main.js
文件中進行修改,添加如下兩句:
import VUeResource from 'vue-resource' Vue.use(VUeResource);
而後,咱們能夠在組件中進行使用。這裏提供一個測試用的 API 地址,來自 百度 APIStore:
http://apis.baidu.com/apistore/iplookup/iplookup_paid?ip=117.89.35.58
因爲這些 API 服務須要購買才能夠返回正確的信息。這裏只用於走通形式,不在意返回數據的正確與否。
如下對最經常使用的 GET 和 POST 請求進行介紹,更多的使用方法能夠參考 API 文檔。
咱們在 Footbar.vue
的 script
中使用如下方式發起 GET 請求。
export default { created(){ let api = 'http://apis.baidu.com/apistore/iplookup/iplookup_paid?ip=117.89.35.58'; this.$http.get(api).then(response => { console.log('接口調用成功'); console.log(response); }, response => { console.log('接口調用錯誤'); }); } }
這裏在 create()
生命週期中觸發了 GET 方法。能夠看到瀏覽器中輸出的結果。
以上示例使用了 this.$http
發起請求,須要在 Vue 組件之中。若是不借助 this
,咱們能夠經過以下方式調用:
let api = 'http://apis.baidu.com/apistore/iplookup/iplookup_paid?ip=117.89.35.58'; Vue.http.get(api).then(response => { console.log('接口調用成功'); console.log(response); }, response => { console.log('接口調用錯誤'); });
注意,這裏的 http
是沒有 $
符號的。
GET 和 POST 請求的使用方式稍有不一樣,以下:
let api = 'http://apis.baidu.com/apistore/iplookup/iplookup_paid?ip=117.89.35.58'; this.$http.post(api, { username: 'xxx' }).then(response => { console.log('接口調用成功'); console.log(response); }, response => { console.log('接口調用錯誤'); });
因爲測試 API 不支持 POST 方式,控制檯會打印出接口調用錯誤的提示,同時會拋出一個很重要的錯誤信息:
XMLHttpRequest cannot load http://apis.baidu.com/apistore/iplookup/iplookup_paid?ip=117.89.35.58. Request header field Content-Type is not allowed by Access-Control-Allow-Headers in preflight response.
便是跨域問題。
這是因爲咱們經過 A 域名的 JavaScript 代碼請求 B 域名的 API 時,在不作任何處理的狀況下,會違背 瀏覽器同源政策 而被瀏覽器拒絕。
這會在先後端分離架構中致使兩個致命的問題:
API 請求沒法發送;
Cookie 信息沒法傳遞;
解決這個問題的方案一般有兩類:jsonp 或是添加響應頭。這裏提供一些解決方法以供讀者參考:
基於 Vue 的組件開發方式可讓咱們在切換頁面時只替換某一部分的組件,從而實現局部刷新。想要實現這一點,須要由前端攔截請求,經過 JavaScript 代碼根據訪問的 URL 替換不一樣的組件從而實現局部刷新效果。
在介紹這一點前,咱們先提一下 iframe
的方式。採用 iframe
標籤,經過修改其 src
屬性,能夠實現頁面局部(即 iframe
DOM 元素)刷新。但這樣的局部刷新存在一些問題:
內部 iframe
的 URL 變化沒法在瀏覽器中顯示出來。也就是說,當 iframe
局部刷新的時候,瀏覽器上訪問的 URL 是不變化的,這就使得用戶沒法經過賦值 URL 的方式再次打開一樣的頁面。這一點在不少應用中是致命的;
外部沒法捕獲內部 iframe
的變化。當用戶在 iframe
中的頁面進行跳轉或相似操做時,外部沒法得知。又由於瀏覽器同源政策的存在,使得內外部的 DOM、數據等是絕緣的。這樣的方式在不少場景下極其受限。
同以前所說的其餘 Vue 插件同樣,咱們經過 npm 進行安裝:
npm install vue-router --save
首先,咱們在 src
目錄下新建一個 router
目錄,在其中建立 index.js
文件,內容以下:
import Vue from 'vue' import Router from 'vue-router' import ContentA from '../components/ContentA'; import ContentB from '../components/ContentB'; Vue.use(Router) export default new Router({ routes: [ { path: '/a', name: 'ContentA', component: ContentA }, { path: '/b', name: 'ContentB', component: ContentB } ] })
這個文件至關於 Vue Router 的路由配置文件。注意到,在最上方,咱們引入了兩個組件。如今咱們建立這兩個組件。
在 src/components
文件夾下新建 ContentA.vue
和 ContentB.vue
兩個文件,並在其中隨便寫入一些內容:
// `ContentA.vue` <style scoped> </style> <template> <div> 這裏是 A 號組件 </div> </template> <script> export default { data () { return { msg: 'index content' } } } </script>
// `ContentB.vue` <style scoped> </style> <template> <div> 這裏是 B 號組件 </div> </template> <script> export default { data () { return { msg: 'index content' } } } </script>
這以後須要修改 main.js
文件,在首部添加如下內容,即引入 src/router/index.js
:
import router from './router/index'
最後,咱們須要對 App.vue
進行修改:
<style> div{ background-color: aquamarine; } </style> <template> <div> <Navbar></Navbar> <router-view></router-view> <Footbar></Footbar> </div> </template> <script> import Navbar from './components/Navbar' import Footbar from './components/Footbar' export default { components: { Navbar, Footbar }, methods: { } } </script>
注意到,咱們在 template
標籤中加入了 <router-view></router-view>
標籤。做爲 Vue Router 填充組件的標誌。
根據配置,當咱們訪問 /a
時,<router-view></router-view>
會被替換爲 ContentA
組件;當訪問 /b
時,其會被替換爲 ContentB
。
如今咱們作一下嘗試,在執行了 npm run dev
以後,訪問 http://localhost:8080/#/a
和 http://localhost:8080/#/b
,查看效果。
當訪問這兩個 URL 時,能夠在控制檯的網絡中進行監控,已驗證網頁確實沒有從新請求。
能夠注意到,咱們訪問的 URL 中存在一個 #
,官方的解釋以下:
vue-router 默認 hash 模式 —— 使用 URL 的 hash 來模擬一個完整的 URL,因而當 URL 改變時,頁面不會從新加載。
若是不想要很醜的 hash,咱們能夠用路由的 history 模式,這種模式充分利用 history.pushState API 來完成 URL 跳轉而無須從新加載頁面。
此時咱們須要修改 router/index.js
文件:
export default new Router({ mode: 'history', routes: [...] })
可是,這樣一來,當咱們直接輸入 localhost:8080/xxx
而非從 localhost:8080/
跳轉過去的時候,Vue Router 沒法正常完成功能。這是因爲,當咱們訪問 localhost:8080/xxx
時,後端會在其配置中尋找該 URL 應該轉發的文件,而不是交給 /index.html
再由其中引入的 JavaScript 代碼進行路由控制。
爲了可以支持這一需求,咱們須要修改服務器端的配置,能夠參考官方說明中的 後端配置例子。
如在 Nginx 中:
location / { try_files $uri $uri/ /index.html; }
能夠看到,這便是將請求都導向 index.html 從而使得 JavaScript 得到了對請求的路由控制。
在組件中,咱們能夠經過如下方式進行跳轉:
this.$router.push({ name: 'a' })
注意,這裏的 name
即爲咱們在 router/index.js
中配置的 name
值。
有時,咱們想要在 URL 跳轉時作一些處理,好比根據當前狀態判斷這次跳轉可否執行。此時就須要使用到導航鉤子。咱們能夠在 router/index.js
中配置一個導航鉤子:
let router = new Router({ routes: [...] }) router.beforeEach((to, from, next) => { console.log(to); console.log(from); next(); })
在該導航鉤子中,對跳轉前、跳轉後的元信息進行了打印,咱們能夠經過它們得到路由配置的信息,從而根據實際需求進行相應的控制。
this.$router
和 this.$route
咱們能夠經過 this.$router
進行跳轉。但當咱們想要得到如當前 URL 的一些信息(如含參路由)時,須要經過 this.$route
得到。上面提到的導航鉤子中的 to
和 from
都是 this.$route
的形式。
Vue Router 的引入會使得打包的 JavaScript 文件已經包含了全部的組件和邏輯代碼。隨着項目的擴大,JavaScript 文件會愈來愈大,使得初次加載的時間變長。應對這一問題,咱們能夠考略在業務層面進行拆分,並將從屬於某一業務的多個組件放在一塊兒打包,從而下降加載文件的大小;
在配置了鉤子函數以後,當頁面刷新時,會自動觸發一次從首頁到訪問頁的跳轉事件。即 from = 首頁,to = 訪問頁。在使用鉤子函數進行跳轉權限斷定時須要注意。