前面兩個終於把webpack相關配置解析完了。 現在終於進入vue的開發了 vue組件化開發預熱 前期準備 ...
前面兩個終於把webpack相關配置解析完了。現在終於進入vue的開發了
vue組件化開發預熱
前期準備
創建如下項目:
app.js:
footer.js:
main.js:
webpack.config.js:
同樣的生成兩個webpack配置文件,webpack.dev.config.js,webpack.prod.config.js,配置跟webpack.config.js一模一樣
package.json:
組件化開發終於到了重頭戲了
webpack引入vue
有幾種方法導入
1.第一種
這個前面文章 vue(8)—— 組件化開發 - webpack(2) 已經用過了,不多說,直接在html文件里導入
2.第二種
在入口函數里引入:
然後在webpack配置文件里添加一個resolve屬性:
註意,使用import導入的方法導入vue,導入的vue並不是完整的vue對象,只提供runtime-only的方式。
安裝loader
其實vue真的做組件化開發的時候,文件尾碼名是.vue,並不是.js,所以webpack要識別尾碼為vue的組件的話,需要安裝loader
這裡要註意,需要安裝vue-loader和vue-template-compiler,前者是識別vue尾碼的文件的,後者是識別vue尾碼文件里的組件代碼的,並且兩者版本是配套,必須匹配,
我這安裝的是vue-loader14.1.1 和[email protected]版本,當然你也可以安裝更高版本的兩個匹配的
webpack配置文件里配置
配置上webpack-dev-server,html-webpack-plugin的配置,這裡就略過了,還不太會的回過頭去看
package.json配置:
在項目根目錄創建一個src文件,將剛纔的app.js,footer.js,main.js移到src文件夾內,並把app.js和footer.js文件重命名尾碼為.vue:
代碼規範整合
vue文件
上面的app.js文件修改成app.vue之後,開發規範已經變了,只有如下三個標簽作為邏輯代碼,之前我們寫的代碼已經不認識
<!--結構--> <template> </template> <!--邏輯--> <script> export default { } </script> <!--樣式--> <style> </style>
改成正確的代碼:
export default 拋出一個vue組件,組件的結構為template,數據為data拋出,因為vue組件的data必須是一個函數,所以這裡是函數。
style樣式則和之前用的沒什麼區別
footer.vue文件:
入口文件
入口函數不用改為js文件
main.js:
其他配置
其他webpack.dev.config.js和package.json不用變
編譯運行
npm run dev:
發現報錯了,這裡就解釋前面文章 vue(7)—— 組件化開發 — webpack(1) 的問題為什麼已經在全局安裝webpack,在開發環境下還要再裝一次了
npm i webpack -D 之後,再次編譯運行,註意安裝指定版本的webpack vue(7)—— 組件化開發 — webpack(1)
運行是運行了,但是頁面打開報錯了,大概意思是說vue-loader和vue-template-compiler這兩個插件有問題
按照我多年解決bug的問題,我把當前的vue版本卸載了,然後裝了個低版本的,裝了個與vue-template-compiler的版本一樣的2.5.17版本的,完美解決上面這個問題
重新編譯:
發現還是報錯,意思是這個App組件沒有正確註冊,檢查代碼,發現根本沒有問題,把App刪除之後,只留一個Footer看看:
可以渲染,但是沒有任何數據,按理說是有的
這下怎麼辦呢?進入關鍵地步,仔細看
初學者容易入的坑
錯誤分析
其實按道理完全沒有問題,因為在之前非webpack下的vue開發中,在一個html文件就是這麼用的沒有錯,那為什麼這裡的footer可以,app不行呢?大眾思維,先把App刪除掉,看Footer呢?
這到底怎麼回事呢?標簽可以出,但是沒有數據渲染,卡在這了。
這裡如果你去發現研究的話花些時間也會發現問題的,但是為了不浪費你的時間,你不用自己去研究了。
其實上面的Footer和App組件都錯的,都不可以渲染成功的,其實footer的顯示,其實是被瀏覽器當html5的標簽處理了
如果你運氣好,定義的兩個組件的名字剛好和html5的標簽撞上,那絕對不會報錯,但是一樣不會把數據渲染出來
總之,按以上以前簡單的html頁面的vue配置,根本就是錯的,組件化開髮根本不認這種寫法。
為什麼,就因為多定義了一個局部,然後再從這個局部組件掛載到vue實例,所以關鍵就在於我標註出的位置
也就是說,在webpack里,或者說在vue-loader和vue-template-compiler插件里,不認這種寫法
既然都已經是組件化開發了,那麼你要定義局部組件,完全可以再新建一個vue文件,然後寫上組件代碼,最後在入口文件main.js里掛載就行了。
不信的話,看我現在不通過Main組件掛載,直接在vue實例對象里掛載App和Footer看看:
其他配置不變
打開頁面,是不是顯示了,而且沒報錯
正確引入vue組件
好,再把剛纔按個Main組件新建一個vue文件放進去
註意在vue文件里導入另一個vue文件的用法
main.js:
其他不變,打開網頁,正確返回
好了組件化開發規範終於解析完了,下麵才開始真正的開發測試
Vue組件化開發
掌握以上的規範之後,你就可以利用前面學到的vue開發,只要符合規範,隨心所欲的在裡面寫你的代碼了。如下,給一個簡單的例子:
package.json:
webpack.dev.config.js
入口文件 main.js:
main.vue
app.vue
footer.vue
index.html
編譯運行
打開網頁:
頁面數據是有了,但是這個有個問題,css樣式亂了,我明明每個vue組件都設置了css的,但是都亂了
問題解決
這個就不多說了,在每個vue組件的sytle標簽里都加上 scoped參數就行了
這個scoped就不多說了,它是由css屬性選擇器實現的,沒必要去深究了,反正你就記住設置scoped之後當前的css樣式只對當前的元素生效,對其他文件上的html元素不生效就行了
相關代碼:
{ "name": "day3", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "dev": "webpack-dev-server --open --hot --inline --config ./webpack.dev.config.js", "build": "webpack --config ./webpack.prod.config.js" }, "keywords": [], "author": "", "license": "ISC", "dependencies": {}, "devDependencies": { "css-loader": "^2.1.1", "html-webpack-plugin": "^3.2.0", "style-loader": "^0.23.1", "vue": "^2.5.17", "vue-loader": "^14.1.1", "vue-template-compiler": "^2.5.17", "webpack": "^3.12.0", "webpack-dev-server": "^2.9.0" } }package.json
var path = require('path') var htmlwebpackplugin = require('html-webpack-plugin') module.exports = { entry: { name: './src/main.js' }, output: { path: path.resolve('./dist'), // 項目輸出文件路徑 filename: './bundle.js' }, watch: true, module: { loaders: [ { test: /\.vue$/, loader: 'vue-loader' }, { test: /\.css$/, loader: 'style-loader!css-loader' } ] }, plugins: [ new htmlwebpackplugin({ template: `./index.html` //參照物 }) ] }webpack.dev.config.js
var path = require('path') var htmlwebpackplugin = require('html-webpack-plugin') module.exports = { entry: { name: './src/main.js' }, output: { path: path.resolve('./dist'), // 項目輸出文件路徑 filename: './bundle.js' }, module: { loaders: [ { test: /\.vue$/, loader: 'vue-loader' }, { test: /\.css$/, loader: 'style-loader!css-loader' } ] }, plugins: [ new htmlwebpackplugin({ template: `./index.html` //參照物 }) ] }webpack.prod.config.js
import Vue from '../node_modules/vue/dist/vue.js' import Main from './Main.vue' new Vue({ el: '#app', data() { return { } }, components: { Main }, render(createdElements) { return createdElements(Main) }, // template: `<Main />` });main.js
<template> <div class='main'> {{msg}} <App @postHander='postHanders'/> <Footer :cont='sendmsg'/> </div> </template> <script> import App from './app.vue' // 在vue組件里導入另一個vue組件,得在script標簽里導入 import Footer from './footer.vue' export default { name:'Main', data(){ return { msg:'Main入口', sendmsg:'', } }, components:{ App, Footer }, methods:{ postHanders(value){ console.log(value,'父級接收到了數據,正準備傳給footer組件') this.sendmsg = value // app.vue組件傳來的數據,值為true } }, } </script> <style scoped> .main{ color: brown; } </style>Main.vue
<template> <div class='apps'> {{msg}} <div v-bind:class="isShow"></div> <input type="text" v-model="test"><span>{{test}}</span><br> <input type="text" ref='inputs' v-model='test2'><br> <button @click="btnHander" ref="btn">點我傳數據給父級</button> </div> </template> <script> export default { name:'App', data(){ return { msg:'app內容', isShow:true, test:'', test2:'' } }, methods:{ btnHander(){ this.$emit('postHander',this.isShow) this.$refs.btn.innerHTML = '數據已發送' } }, watch: { test2(value){ if(value=='vue'){ this.test2 = '我監聽到你填了vue' this.$refs.inputs.value = '' } } } } </script> <style scoped> .apps{ font-size: 12px; background: salmon; color: blue; } </style>app.vue
<template> <div class='footers'>{{msg}} <p v-text="test1"></p> <p v-html="test2"></p> <button @click="getHander(cont)">點我展示從父級獲取到的數據</button> <ul v-show="isShow"> <li v-for = 'item in datas' :key = 'item.id'> {{ item.name }} -- {{item.age}} </li> </ul> </div> </template> <script> export default { name:'Footer', data(){ return { msg:'footer內容', test1:'測試1', test2:'<h4>測試2</h4>', datas:[ {'id':1,'name':'jack','age':30}, {'id':2,'name':'lucy','age':26}, {'id':3,'name':'lily','age':28},], isShow:false }}, methods:{ getHander(cont){ this.isShow = cont } }, props:['cont'], // 父級傳來的數據,值為true mounted(){ // 生命周期函數 console.log() } } </script> <style scoped> .footers{ color: purple; font-size: 15px; font-weight: bold; } </style>footer.vue
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title></title> </head> <body> <div id="app"> <p>test</p> </div> </body> </html>index.html
Vuex
什麼是vuex
官網:傳送門,官方解釋:
Vuex 是一個專為 Vue.js 應用程式開發的狀態管理模式。它採用集中式存儲管理應用的所有組件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。Vuex 也集成到 Vue 的官方調試工具 devtools extension,提供了諸如零配置的 time-travel 調試、狀態快照導入導出等高級調試功能
vuex的作用
vuex是為了保存組件之間的共用數據而誕生的。如果組件之間有共用的數據,直接掛載在vuex上即可,而沒必要子級組件通過向父級組件傳遞之後,父級組件再將數據轉給目標子級組件,這個很麻煩,這個之前我個人試過,利用子級傳遞一個簡單的數據還好,傳遞一個數組感覺有點繞,很不方便。
所以,vuex是一個全局的共用數據存儲區域,相當於一個數據倉庫
Vuex 可以幫助我們管理共用狀態,並附帶了更多的概念和框架。這需要對短期和長期效益進行權衡。
如果您不打算開發大型單頁應用,使用 Vuex 可能是繁瑣冗餘的。確實是如此——如果您的應用夠簡單,您最好不要使用 Vuex。一個簡單的 store 模式就足夠您所需了。但是,如果您需要構建一個中大型單頁應用,您很可能會考慮如何更好地在組件外部管理狀態,Vuex 將會成為自然而然的選擇
簡單使用vuex
安裝 npm i vuex
配置vuex
還是上面的例子,做了稍微修改:
main.js:
1.導入vuex並掛載到Vue實例對象上,這個用法跟掛載vue-router類似,不多說
2.定義一個vuex實例對象,其有三個屬性,state,mutations,getters
- state類似vue實例中的data屬性,裡面的值就是數據倉庫,所有的vue組件都可以共用
- mutations類似vue實例中的methods屬性,裡面的方法類似computed計算屬性,裡面的方法主要是對數據的修改,方法名自定義,vuex官方不建議直接在數據被引用的組件里直接賦值修改
- getters類似vueshilling中的watch或者filters屬性,方法名可以自定義,但是參數固定必須是state屬性
3.同樣的類似路由對象一樣,直接在vue實例上掛載定義的vuex對象
app.vue:
其他不變,直接用$store.state.XX(你定義的公用數據變數名)獲取vuex實例的數據
定義了兩個方法,與vuex的mutations屬性中的方法對應,一個加法,一個減法,傳入的第一個值就是mutations里的方法名,第二個值隨意,但最多只能傳入兩個值,當然你可以傳入一個對象,對象里放很多屬性即可。
註意:組件中的方法要操作store公用倉庫的數據,必須使用this.$store.commit或者this.$store.disptch提交,不能是$store.commit,在方法中必須加this,註意區分
footer.vue:
只有標註區域是添加的,其他沒變,就是通過了vuex實例的getters屬性,獲取了最新的數據,有點類似vue的watch或者filters屬性,實時獲取最新的值
這裡可能有疑惑,為什麼用getters獲取呢?直接像上面的app.vue中的用$store.state.count獲取不行嗎?可以是可以,假如說你希望給這個數據添加一些其他的樣式,加個單位或者什麼呢?這樣就可以直接用getters,不需要你再自己定義一個方法了,大家都公用的
其他沒做任何改動,npm run dev運行,打開瀏覽器:這個不太好展示效果,效果就是我點增加或減少按鈕,兩處的數據都跟著改變
感覺還是挺簡單的對吧
相關代碼:
{ "name": "day3", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "dev": "webpack-dev-server --open --hot --inline --config ./webpack.dev.config.js", "build": "webpack --config ./webpack.prod.config.js" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "vuex": "^3.1.0" }, "devDependencies": { "css-loader": "^2.1.1", "html-webpack-plugin": "^3.2.0", "style-loader": "^0.23.1", "vue": "^2.5.17", "vue-loader": "^14.1.1", "vue-template-compiler": "^2.5.17", "webpack": "^3.12.0", "webpack-dev-server": "^2.9.0" } }package.json
import Vue from '../node_modules/vue/dist/vue.js' import Main from './Main.vue' import Vuex from 'vuex' Vue.use(Vuex) var store = new Vuex.Store({ state: { //類似於data屬性 count: 0 }, mutations: { // 類似於methods屬性 increment(state) { // 加法 state.count++ }, subtract(state, value) { // 減法 state.count -= value } }, getters: { // 類似於filters過濾器屬性 currentCount:function(state) { return '當前最新的count值是:' + state.count } } }) new Vue({ el: '#app', data() { return {} }, components: {Main}, store, template: `<Main />` });main.js
<template> <div class='main'> {{msg}} <App @postHander='postHanders'/> <Footer :cont='sendmsg'/> </div> </template> <script> import App from './app.vue' // 在vue組件里導入另一個vue組件,得在script標簽里導入 import Footer from './footer.vue' export default { name:'Main', data(){ return { msg:'Main入口', sendmsg:'', } }, components:{ App, Footer }, methods:{ postHanders(value){ console.log(value,'父級接收到了數據,正準備傳給footer組件') this.sendmsg = value // app.vue組件傳來的數據,值為true } }, } </script> <style scoped> .main{ color: brown; } </style>main.vue
<template> <div class='apps'> {{msg}} <input type='text' v-model="$store.state.count"> <input type='button' value="增加" @click="add"> <input type="button" value="減少" @click="sub"> <div v-bind:class="isShow"></div> <input type="text" v-model="test"><span>{{test}}</span><br> <input type="text" ref='inputs' v-model='test2'><br> <button @click="btnHander" ref="btn">點我傳數據給父級</button> </div> </template> <script> export default { name:'App', data(){ return { msg:'app內容', isShow:true, test:'', test2:'' } }, methods:{ add(){ this.$store.commit('increment') }, sub(){ this.$store.commit('subtract',5) }, btnHander(){ this.$emit('postHander',this.isShow) this.$refs.btn.innerHTML = '數據已發送' } }, watch: { test2(value){ if(value=='vue'){ this.test2 = '我監聽到你填了vue' this.$refs.inputs.value = '' } } } } </script> <style scoped> .apps{ font-size: 12px; background: salmon; color: blue; } </style>app.vue
<template> <div class='footers'>{{msg}} <p v-text="test1"></p> <p v-html="test2"></p> <button @click="getHander(cont)">點我展示從父級獲取到的數據</button> <ul v-show="isShow"> <li v-for = 'item in datas' :key = 'item.id'> {{ item.name }} -- {{item.age}} </li> </ul> <h3>{{$store.getters.currentCount}}</h3> </div> </template> <script> export default { name:'Footer', data(){ return { msg:'footer內容', test1:'測試1', test2:'<h4>測試2</h4>', datas:[ {'id':1,'name':'jack','age':30}, {'id':2,'name':'lucy','age':26}, {'id':3,'name':'lily','age':28},], isShow:false }}, methods:{ getHander(cont){ this.isShow = cont } }, props:['cont'], // 父級傳來的數據,值為true mounted(){ // 生命周期函數 console.log() } } </script> <style scoped> .footers{ color: purple; font-size: 15px; font-weight: bold; } </style>footer.vue
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title></title> </head> <body> <div id="app"> <p>test</p> </div> </body> </html>index.html
actions和同步非同步問題
在vuex的官網中,我們可以看到,在mutations中的方法都是同步更新數據的,在actions中的操作時非同步更新數據的。如果我們的數據需要非同步操作的話,就不能實現實時更新數據了,這樣就很容易有後患,比如,組件A將store里的數據用非同步操作改了,然後另一個組件B獲取數據時因為非同步操作獲取數據並不及時,所以可能導致獲取的數據並不是最新的,所以會有這種隱患存在。
看個例子:
為上面的例子加一個非同步增加的方法:
main.js,用setTimeout模擬非同步操作
app.vue:
編譯運行
我先點同步增加:
這裡查看數據的工具,使用的是谷歌瀏覽器的vue-devtools插件,這個需要自己手動安裝,然後就可以在調試工具視窗中看到vue的相關數據了,這個方便以後的開發,具體就不掩飾怎麼安裝了,自行網上查詢解決
再點非同步增加:
發現這個問題了吧?雖然在當前頁面會顯示正常的,但是實際的倉庫中的數據其實還沒有變化的。
再點減少按鈕模擬其他的組件要操作這個數據,一步錯,步步錯,後面的操作就永遠不是正確的數據了:
問題解決
前面已經說了,mutations里的方法是同步的,然後模擬非同步操作時,因為mutations獲取操作的數據是同步並不會拿到最新的數據,所以在這種場景下,不用使用直接mutations里的方法來操作數據,需要借用actions來操作:
在main.js添加actions:
app.vue中,註意非同步操作分發用的不是commit而是dispatch了
編譯運行
果然同步了,目前這個才是我們希望的效果:
actions函數和傳參
在actions的方法中,方法名就是mutations中的方法名,這樣以遍actions分發到mutations中的同名方法中 ,格式 XX({commit}){}
如果要傳參的話,格式 xx({commit},value){} ,其中value就是傳入的參數,比如:
vuex中的actions的方法本質上還是調用的mutations里的方法,只是做了非同步處理分發,在方法本身是沒有做改動的。然後再組件部分不再使用commit提交數據,而是用dispatch方法提交,非同步操作。
個人感覺這個actions非常類似Python中的裝飾器
相關代碼:
{ "name": "day3", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "dev": "webpack-dev-server --open --hot --inline --config ./webpack.dev.config.js", "build": "webpack --config ./webpack.prod.config.js" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "vuex": "^3.1.0" }, "devDependencies": { "css-loader": "^2.1.1", "html-webpack-plugin": "^3.2.0", "style-loader": "^0.23.1", "vue": "^2.5.17", "vue-loader": "^14.1.1", "vue-template-compiler": "^2.5.17", "webpack": "^3.12.0", "webpack-dev-server": "^2.9.0" } }package.json
import Vue from '../node_modules/vue/dist/vue.js' import Main from './Main.vue' import Vuex from 'vuex' Vue.use(Vuex) var store = new Vuex.Store({ state: { //類似於data屬性 count: 0 }, mutations: { // 類似於methods屬性 increment(state) { // 加法 state.count++ }, asyincrement(state) { // 非同步加法,兩秒之後再加 state.count++ }, subtract(state, value) { // 減法 state.count -= value } }, getters: { // 類似於filters過濾器屬性 currentCount:function(state) { return '當前最新的count值是:' + state.count } }, actions:{ // 非同步操作 increment({commit}){ //同步加法 commit('increment') }, asyincrement({commit}){ // 非同步加法,兩秒之後再加 setTimeout(()=>{ commit('asyincrement') },2000) }, } }) new Vue({ el: '#app', data() { return {} }, components: {Main}, store, template: `<Main />` });main.js
<template> <div class='main'> {{msg}} <App @postHander='postHanders'/> <Footer :cont='sendmsg'/> </div> </template> <script> import App from './app.vue' // 在vue組件里導入另一個vue組件,得在script標簽里導入 import Footer from './footer.vue' export default { name:'Main', data(){ return { msg:'Main入口', sendmsg:'', } }, components:{ App, Footer }, methods:{ postHanders(value){ console.log(value,'父級接收到了數據,正準備傳給footer組件') this.sendmsg = value // app.vue組件傳來的數據,值為true } }, } </script> <style scoped> .main{ color: brown; } </style>main.vue
<template> <div class='apps'> {{msg}} <input type='text' v-model="$store.state.count"> <input type='button' value="同步增加" @click="add"> <input type='button' value="非同步增加" @click="asyadd"> <input type="button" value="減少" @click="sub"> <div v-bind:class="isShow"></div> <input type="text" v-model="test"><span>{{test}}</span><br> <input type="text" ref='inputs' v-model='test2'><br> <button @click="btnHander" ref="btn">點我傳數據給父級</button> </div> </template> <script> export default { name:'App', data(){ return { msg:'app內容', isShow:true, test:'', test2:'' } }, methods:{ add(){ //同步增加 //this.$store.commit('increment') this.$store.dispatch('increment') }, asyadd(){ // 非同步增加 //this.$store.commit('asyincrement') this.$store.dispatch('asyincrement') }, sub(){ //this.$store.commit('subtract',5) this.$store.dispatch('subtract',5) }, btnHander(){ this.$emit('postHander',this.isShow) this.$refs.btn.innerHTML = '數據已發送' } }, watch: { test2(value){ if(value=='vue'){ this.test2 = '我監聽到你填了vue' this.$refs.inputs.value = '' } } } } </script> <style scoped> .apps{ font-size: 12px; background: salmon; color: blue; } </style>app.vue