使用Vue全家桶+Node.js搭建的小型全棧項目

前言

接觸vue框架也有一個多月的時間了,整理下以前作過的一個小demo,主要是熟悉vue全家桶技術,界面佈局模仿的是貓眼,數據使用的是豆瓣開發者提供的後臺接口。整個過程從搭建腳手架到最後項目打包上線,是一個完整的開發流程,中間涉及到的知識點比較多,也比較零碎,經過這個項目對我本身的知識體系作一個梳理和總結。javascript

項目已上傳github,歡迎你們下載交流。css

前端項目地址:https://github.com/Hanxueqing...html

後臺數據地址:https://github.com/Hanxueqing...前端

在線項目手冊:https://hanxueqing.github.io/...vue

項目技術棧

項目運行

# 克隆到本地
git clone git@github.com:Hanxueqing/Douban-Movie.git

# 安裝依賴
npm install

# 開啓本地服務器localhost:8080
yarn serve

# 發佈環境
yarn build

項目開發

一、安裝vue-cli3腳手架

如今使用前端工程化開發項目是主流的趨勢,也就是說,咱們須要使用一些工具來搭建vue的開發環境,通常狀況下咱們使用webpack來搭建,在這裏咱們直接使用vue官方提供的,基於webpack的腳手架工具:vue-cli。java

(1)全局安裝webpack

cnpm install webpack -g

(2)全局安裝yarn

cnpm install yarn  -g

(3)全局安裝vue-cli

cnpm install -g @vue/cli

OR

yarn global add @vue/cli

(4)查看安裝結果

個人電腦已經安裝過了,依次執行命令:node

node -v
yarn -v
vue -V (注意這裏是大寫的「V」)

注意:要求node.js版本8或者8+webpack

出現相應的版本號,則說明安裝成功ios

image

2.用vue-cli來構建項目

(1)建立項目

vue create douban(項目名稱)

(注意這裏的名字不能有大寫字母,若是有會報錯Sorry, name can no longer contain capital lettersnginx

image

阮一峯老師博客爲何文件名要小寫能夠參考一下。

第一個vue-model是我保存過的設置,第一次create的時候是沒有的。 default是默認設置,會給你安裝babel和eslint模塊,咱們選擇第三個Manually select features本身手動配置。

image

輸入空格是選擇當前選項,輸入a是全選。

(加粗的是要選擇的配置項)

Babel 安裝這個模塊後就能夠識別ES6的語法,否則只能識別ES5的語法

TypeScript 是 JavaScript 的一個超集,支持 ECMAScript 6 標準。

Progressive Web App (PWA) Support 漸進式加強的前端網頁技術PWA,是一個專門的學科,能夠作離線存儲的功能,手機斷網的狀況下也能夠訪問這個頁面,咱們目前用不到,先不安裝了。

Router 路由

Vuex 全局狀態管理

CSS Pre-processors CSS預處理語言

Linter / Formatter 格式化工具,幫助咱們更好的編寫代碼

Unit Testing 單元測試

E2E Testing

選擇好這四項以後輸入enter回車

接下幾項配置依次是:

  1. 是否使用history模式配置路由,輸入n,回車
  2. 因爲咱們剛纔選擇了CSS預處理語言,因此這裏咱們就選擇Sass/SCSS(with node-sass)這個穩定版本
  3. 你須要在哪裏存儲你的這些配置文件,咱們選擇in package.json
  4. 最後是你想將這些配置保存爲一個預設應用於之後的文件嗎,保存之後就不用再手動配置這些選項,之後直接使用便可,我以前保存過了,這裏就先選擇n

    image

回車以後就進入了下載狀態,速度取決於你當前的網速。

image

安裝成功後會提示咱們進入douban這個文件夾下運行yarn serve啓動監聽。

image

(2)加載編譯文件

將Vue.config.js編譯文件拷貝到douban文件夾下

三、整理下src文件夾

(1)router

新建router文件夾,將router.js文件拖進去,重命名爲index.js,routes裏面的內容刪掉。

image

(2)views

views文件夾下的兩個.vue文件刪掉,assets文件夾下的logo圖刪掉,components文件夾下的HelloWorld組件刪掉 。

(3)App.vue

App.vue文件樣式刪掉,id名爲app的div內容刪掉。

image

(4)在views中新建頁面

在views文件夾下依次建立首頁、書影音、廣播、小組、個人頁面,文件夾下建立index.vue文件,以vue爲後綴名的文件包含template、script、style三部分,webpack在解析.vue文件時以vue-loader加載器加載。

(5)在router中配置路由

在router文件夾下依次建立首頁、書影音、廣播、小組、個人頁面js路由文件,每個路由配置一個name名字,方便咱們之後經過具名路由的方式找到這個路由組件,最後在index.js中引入。

(6)在App.vue中引入<router-view>

(7)編寫樣式文件

新建stylesheets文件夾

webpack在打包的過程當中不會處理前面加下劃線的文件,避免重複進行打包

依次建立:_base.scss基本樣式

_commons.scss通用樣式

_mixins.scss混合樣式

_reset.scss重置樣式

最後在main.scss文件夾下依次引入

@import "_base.scss";
@import "_mixins.scss";
@import "_reset.scss";
@import "_commons.scss";

在main.js中將樣式做爲一個模塊引入

//引入main.scss文件
import "./stylesheets/main.scss"

四、Tabbar組件

(1)slot插槽:具名槽口、匿名槽口

首先咱們將正常狀態圖標和選中狀態圖標依次引入,寫好樣式,給正常狀態圖標命名爲normalImg,選中狀態圖標命名爲activeImg,這裏咱們用到了slot插槽。在子組件中寫上slot標籤,並賦上name值,做爲具名槽口,就能夠在父組件傳入要插入的內容。

子組件:

<span><slot name = "normalImg"></slot></span>
<span><slot name = "activeImg"></slot></span>

父組件:

<TabItem>
            <img slot = "normalImg" src = "../../assets/ic_tab_home_normal.png" alt = "">
            <img slot = "activeImg" src = "../../assets/ic_tab_home_active.png" alt = "">
</TabItem>

(2)v-if/v-else指令:控制顯示和隱藏

第一步實現點擊圖標由普通樣式切換爲選中樣式,須要設置一個flag屬性,來控制normalImg與activeImg的顯示,這裏咱們用到了v-if和v-else指令

<span v-if = "!flag"><slot name = "normalImg"></slot></span>
<span v-else><slot name = "activeImg"></slot></span>

(3)父組件給子組件傳值的兩種方式:事件綁定、自定義事件

在父組件中給子組件傳遞惟一txt、mark、sel屬性值以及changeSelected方法,父組件經過屬性綁定的方法把selected值傳遞給sel:

父組件:

<TabItem txt = "個人" mark = "mine" :sel = "selected" :changeSelected = "changeSelected">

子組件依次接收:

props:["txt","mark","sel","changeSelected"]

在子組件中經過綁定點擊事件change,讓父組件去更改selected的值再傳遞給子組件,經過與mark值進行匹配來肯定返回值,從而控制當前圖標狀態。

子組件綁定點擊事件觸發change方法:

<div class = "tabitem" @click = "change">

子組件change方法:

change(){
            //讓父組件去更改sel值
            this.changeSelected(this.mark);
        }

父組件改變sel值:

changeSelected(val){
            this.selected = val;
        }

父組件也能夠利用自定義事件給子組件傳遞方法,子組件就不用再經過props中接收,而是直接在方法中經過this.$emit來使用,第一個參數是綁定在父級自身的事件名,第二個參數是傳給父級的參數,經過這種方法在子組件中觸發父組件的方法。

父組件:

<TabItem txt = "小組" mark = "group" :sel = "selected" @changeSelected = "changeSelected">

子組件:

change(){
            //自定義事件通訊方法
            this.$emit("changeSelected",this.mark)
        }

(4)computed計算屬性

子組件進行判斷,返回falg值,因爲須要依賴外部值而產生變化,因此咱們將flag寫在計算屬性computed中:

computed:{
        flag(){
            if(this.mark === this.sel){
                return true
            }
            return false
        }
    }

(5)編程式導航

子組件中經過編程式導航,依據不一樣的mark屬性,匹配不一樣的路由,實現點擊圖標跳轉相應頁面

change(){
            //自定義事件通訊方法
            this.$emit("changeSelected",this.mark)
            //編程式導航
            this.$router.push("/" + this.mark)
        }

因爲咱們以前把Tabbar寫在App.vue中,它只被建立了一次,在所有頁面都會出現,可是咱們有的頁面中不須要Tabbar,因此咱們不能把它寫在全局中,哪一個頁面須要就在哪一個頁面引入,這時候咱們會發現雖然能實現點擊跳轉頁面,可是底部的圖標沒法實現點擊變色,這是因爲每一次跳轉Tabbar都經歷了一次由建立到銷燬的過程,因此這時候咱們就不能再在data中給它賦值爲固定值了。

原代碼:

data(){
        return{
            selected:"home"
        }
    }

因爲咱們以前配置路由的時候給每一個路由都設置了name屬性,因此當咱們打印this的時候能夠在$route中獲取到它的name值,咱們把這個name值賦值給data,直接傳遞給子組件進行判斷,就不須要再傳遞方法了。

image

經過this.$route.name決定父組件中哪個元素被選中。

父組件:

data(){
        return{
            selected:this.$route.name,
        }
    }

將引入的子組件數據所有寫在template中顯然是既繁瑣又臃腫,這個時候咱們能夠將數據寫在data對象中,而後使用v-for循環指令依次在頁面上加載:

data中,建立一個footers數組,將對象數據都放在數組中:

footers:[
                {id:1,txt:"首頁",mark:"home",normalImg:"../../assets/ic_tab_home_normal.png",activeImg:"../../assets/ic_tab_home_active.png"},
                {id:2,txt:"書影音",mark:"audio",normalImg:"../../assets/ic_tab_audio_normal.png",activeImg:"../../assets/ic_tab_audio_active.png"},
                {id:3,txt:"廣播",mark:"broadcast",normalImg:"../../assets/ic_tab_broadcast_normal.png",activeImg:"../../assets/ic_tab_broadcast_active.png"},
                {id:4,txt:"小組",mark:"group",normalImg:"../../assets/ic_tab_group_normal.png",activeImg:"../../assets/ic_tab_group_active.png"},
                {id:5,txt:"個人",mark:"mine",normalImg:"../../assets/ic_tab_mine_normal.png",activeImg:"../../assets/ic_tab_mine_active.png"}
            ]

(6)v-for指令

TabItem標籤中循環遍歷footers:

<TabItem
            v-for="foot in footers"
            :key = "foot.id"
            :txt = "foot.txt"
            :sel = "selected"
            :mark = "foot.mark"
        >
            <img slot = "normalImg" :src = "foot.normalImg" alt = "">
            <img slot = "activeImg" :src = "foot.activeImg" alt = "">
        </TabItem>

可是這個時候刷新網頁就會發現圖標都加載不出來,緣由是webpack在打包過程當中不識別咱們的相對路徑,它會將引號裏的內容原樣輸出

image

咱們須要使用require加載路徑,把圖片看成一個模塊來引入

{id:1,txt:"首頁",mark:"home",normalImg:require("../../assets/ic_tab_home_normal.png"),activeImg:require("../../assets/ic_tab_home_active.png")}

而且webpack會將咱們引入的圖片轉成base64位格式,至關於把一段文字放在網頁中,再次訪問的時候就不須要向遠程發送請求了。

image

轉換以後它的體積會變大,可是不會比以前大不少,網絡請求時間減小,Time變成0ms,加快訪問效率,提高用戶體驗。這個主要是針對小圖片,20kb之內。

image

五、404頁面

(1)redirect:路由重定向

當咱們訪問不存在的路徑時,將頁面重定向到notfound,也就是說,當用戶輸入路由表中不存在的路徑時一概會跳轉到404頁面。

在views中新建Notfound文件夾,在index.vue中編寫404頁面,在router中配置路由。

在index.js中寫入:

routes: [
    {path:"/",redirect:"/home"},
    home,audio,broadcast,group,mine,
    {path:"/notfound",component:()=>import("@/views/Notfound")},
    {path:"*",redirect:"/notfound"},
  ]

星號表明任意路徑,配置的時候{path:"*",redirect:"/notfound"}要放在最後面,否則全部路徑都會跳轉到404頁面,路由是按順序從上至下依次查找頁面的。

(2)router-link:路由標籤

增長一個點擊返回首頁的功能

<router-link to = "/">點我返回首頁</router-link>

六、編寫header頭部

(1)rem:響應式佈局

這裏咱們使用rem來計算尺寸,實現移動端的響應式佈局。

首先咱們新建一個modules文件夾,編寫rem.js,以iphone6爲主

document.documentElement.style.fontSize = document.documentElement.clientWidth / 3.75 + "px"
window.onresize = function(){
    document.documentElement.style.fontSize = document.documentElement.clientWidth / 3.75 + "px"
}

在main.js中引入rem.js文件

//引入rem.js文件
import "./modules/rem.js"

(2)font-awsome:字體圖標庫

在font-awsome官網下載css文件包,放入public文件夾中,在index.html中引入樣式。

image

使用font-awsome:

<i :class = "['fa','fa-' + home]"></i>

如今咱們想實現當路由變化的時候,頭部信息動態更改,咱們先把內容改爲這種形式:

template中:

<div class = "left">
            <i :class = "['fa','fa-' + icon]"></i>
            <span>{{title}}</span>
        </div>

data中:

data(){
        return{
            icon:"home",
            title:"豆瓣首頁",
        }
    },

這時候就須要用到路由守衛,當路由切換的時候能夠作一些業務邏輯,首先須要在頁面中引入全局路由:

import router from  "@/router"

寫全局前置路由守衛router.beforeEach,使用switch語句來匹配:

created(){
        router.beforeEach((to,from,next)=>{
            switch(to.name){
                case "home":
                    this.title = "豆瓣首頁"
                    this.icon = "home"
                    break;
                case "audio":
                    this.title = "豆瓣影音"
                    this.icon = "audio-description"
                    break;
                case "broadcast":
                    this.title = "豆瓣廣播"
                    this.icon = "caret-square-o-left"
                    break;
                case "group":
                    this.title = "豆瓣小組"
                    this.icon = "group"
                    break;
                case "mine":
                    this.title = "豆瓣個人"
                    this.icon = "cog"
                    break;
                default:
                    break;

            }
            next();
        })
    }

因爲咱們把Header組件寫在了全局App中,因此它在每一個頁面上都會顯示,可是咱們但願它只在匹配到指定路由的時候顯示,這時候咱們就須要設置一個isShow的參數值來控制它的顯示與隱藏。

data中:

data(){
        return{
            icon:"home",
            title:"豆瓣首頁",
            isShow:true
        }
    }

switch中:匹配到case的isShow值都爲true,default就是除了以上的幾種狀況,給isShow賦值爲false

switch(to.name){
                case "home":
                    this.title = "豆瓣首頁"
                    this.icon = "home"
                    this.isShow = true
                    break;
                case "audio":
                    this.title = "豆瓣影音"
                    this.icon = "audio-description"
                    this.isShow = true
                    break;
                case "broadcast":
                    this.title = "豆瓣廣播"
                    this.icon = "caret-square-o-left"
                    this.isShow = true
                    break;
                case "group":
                    this.title = "豆瓣小組"
                    this.icon = "group"
                    this.isShow = true
                    break;
                case "mine":
                    this.title = "豆瓣個人"
                    this.icon = "cog"
                    this.isShow = true
                    break;
                default:
                    this.isShow = false
                    break;

            }

而後div 中添加v-if,賦值爲isShow值

<div class = "app-header" v-if = "isShow">

因此實現組件的顯示與隱藏有兩種辦法:

(1)哪裏須要就在哪裏引用,例如Tabbar。

(2)switch配合路由守衛作判斷,哪一個地方須要就使用v-if顯示,例如Header。

七、Banner輪播圖

(1)swiper:輪播圖組件

先安裝下載兩個模塊swiper、axios:

cnpm i swiper -S或者yarn add swiper -S

cnpm i axios -S

最好不要混用,一開始使用了cnpm就一直使用cnpm安裝

安裝成功顯示

image

在main.js中引入swiper樣式

//引入swiper.min.css樣式文件
import 'swiper/dist/css/swiper.min.css'

在components組件中新建Banner文件夾,編寫index.vue文件,引入swiper:

import Swiper from "swiper"

編寫swiper內容,將banners圖片循環渲染到頁面上

<div class = "swiper-container">
        <div class = "swiper-wrapper">
            <div
                class = 'swiper-slide'
                v-for = "banner in banners"
                :key = "banner.id"
            >
                <img width = "100%" :src = "getImages(banner.images.small)" alt = "">
            </div>
        </div>
        <div class = "swiper-pagination"></div>

因爲咱們使用的是豆瓣api,會遇到圖片403的問題,具體解決辦法請參考這篇文章:

https://blog.csdn.net/jsyxiao...

文章中給咱們提供了一個方法解決這個問題,這裏咱們就引入一下

methods:{
        // 解決403圖片緩存問題
        getImages( _url ){
            if( _url !== undefined ){
                let _u = _url.substring( 7 );
                return 'https://images.weserv.nl/?url=' + _u;
            }
        }
    },

因爲以後咱們還會用到這個函數,因此將它寫在module中導出:

export default (_url) => {
    if (_url !== undefined) {
        let _u = _url.substring(7);
        return 'https://images.weserv.nl/?url=' + _u;
    }
    return true
}

以後使用的時候只須要引入:

import getImages from "@/modules/getImg"

再在methods中註冊一下該方法:

methods:{
        getImages
    }

使用的時候直接寫函數名,將圖片的src做爲參數傳入便可:

<img width = "100%" :src = "getImages(movie.images.small)" alt = "">

接下來咱們就把Swiper實例化

new Swiper(".home-banner",{
                    loop:true,
                    pagination:{
                        el:".swiper-pagination"
                    }
                })

這時候會出現,劃不動現象,產生的緣由是原本這個地方是沒有swiper-slide這個數據的,後續咱們發送了ajax請求,他纔會動態生成六個swiperslide,banners數據立馬改變了,它內部會生成新的虛擬dom和上一次虛擬dom結構做對比,而後產生新的真實dom,這個過程須要時間,可是咱們立馬實例化了,因此等到真實dom渲染完成後實例化早就結束了。

解決方法就是咱們必需要等到由於數據改變了引起新的真實dom渲染完成後纔會執行的操做,就能夠避免這樣的問題。

(2)this.$nextTick函數

因此咱們須要把實例化的過程寫在this.$nextTick的回調函數中,在這個函數裏面進行的操做就是等到數據更新而引起的頁面當中新的虛擬dom所渲染成的真實dom真正渲染出來以後才執行,簡單來講就是等到頁面所有渲染完成後,再進行實例化操做。

this.$nextTick(()=>{//在這個函數裏面,由於數據改變致使頁面生成新的真實dom,所有渲染完成了
                new Swiper(".home-banner",{
                    loop:true,
                    pagination:{
                        el:".swiper-pagination"
                    }
                })
            })

banner輪播圖運行效果:

image

(3)axios:發送ajax異步請求數據

Axios 是一個基於 promise 的 HTTP 庫,能夠用在瀏覽器和 node.js 中。

它的主要功能:

  • 從瀏覽器中建立 XMLHttpRequests
  • 從 node.js 建立 http請求
  • 支持 Promise API
  • 攔截請求和響應
  • 轉換請求數據和響應數據
  • 取消請求
  • 自動轉換 JSON 數據
  • 客戶端支持防護 XSRF

由於有不少地方都須要用到數據請求,每次都須要引入很麻煩,能夠直接把axios綁定到vue的原型屬性prototype上去,之後就能夠經過this.$http來訪問:

//引入axios
import axios from "axios"
Vue.prototype.$http = axios;

(4)解決跨域:反向代理

因爲咱們如今是在本地訪問遠程端口的圖片,因此會產生跨域問題,這裏咱們經過反向代理的方式解決跨域問題,在Vue.config.js配置文件中配置代理,

proxy: {//反向代理的方式解決跨域
            "/api":{
                target:"http://47.96.0.211:9000", //目標域名
                changeOrigin:true,//是否改變域名
                pathRewrite:{//以什麼開頭
                    "^/api":""
                }
            }
        }, // 設置代理

最終訪問的時候會自動把/api清除,把後面的路徑拼接到目標域名上:

this.$http.get("/api/db/in_theaters",{

一旦配置文件更改,咱們就須要手動從新啓動監聽!

(5)loading圖片

剛開始進入頁面的時候,沒有數據加載進來,會顯示空的一塊,這裏能夠加一個loading圖,提示用戶數據正在加載,當數據加載進來時候隱藏,提高用戶體驗。

loading圖片放在assests文件夾下,在其餘地方也可使用這個loading圖,因此咱們在公共樣式中引入它:

.loading{
    background:url(../assets/index.svg) no-repeat center;
    background-size:10%;
    height:2.4rem
}

以後只須要給標籤添加loading的class名便可,因爲咱們在data中給banner初始值設置爲null,因此能夠根據banner的值配合v-if/v-else指令來決定標籤的顯示和隱藏:

<div class = "loading" v-if = "!banners"></div>
        <div class = "swiper-wrapper" v-else>

在首頁引入Banner組件

<Banner></Banner>

Loading圖運行效果:

image

八、首頁列表

(1)filter:過濾器

咱們須要在首頁編寫MovieBox組件,裏面再嵌套MovieItem組件,在MovieBox中請求數據,以屬性的方式傳遞給MovieItem。

data(){
        return{
            movies:null
        }
    },
    created(){
        this.$http.get("/api/db/in_theaters",{
            params:{
                limit:6
            }
        }).then(res=>{
            this.movies = res.data.object_list
        })
    }

MovieItem接收movies對象:

props:{
        movie:Object
    }

把數據插入到頁面中:

<div class = "movieitem">
        <div class = "main_block">
            <div class = "img-box">
                <img width = "100%" :src = "getImages(movie.images.small)" alt = "">
            </div>
            <div class = "info">
                <div class = "info-left">
                    <div class = "title line-ellipsis">{{movie.title}}</div>
                    <div class = "detail">
                        <div class = "count line-height">播放量<span class = "sc">{{movie.collect_count | filterData}}</span></div>
                        <div class = "actor line-height line-ellipsis">
                            <span class = "a_title">導演:</span>
                            <span class = "a_star line-ellipsis">{{movie.directors[0].name}}</span>
                        </div>
                        <div class = "rating line-height">評分:{{movie.rating.average}}</div>
                    </div>
                </div>
                
                <div class = "info-right"> 
                    <div class = "btn">
                        <span>購票</span>
                    </div>
                </div>
            </div>
        </div>
    </div>

在MovieBox中循環渲染到頁面上:

<div class = "moviebox">
        <div class="loading" v-if="!movies"></div>
        <MovieItem
            v-else
            v-for = "movie in movies"
            :key = "movie.id"
            :movie = "movie"
        ></MovieItem>
    </div>

咱們從後端請求到數據,有的時候會不符合咱們的實際需求,好比播放量返回一個很大的數字,不方便閱讀和觀看,這時候咱們就須要設置一個filter過濾器,將數據處理成咱們想要的格式。這裏咱們設置一個filterData方法,若是接收到的數據大於10000,咱們就把當前這個數字除以10000,經過.toFixed()保留一位小數,再拼接一個「萬」顯示在頁面上。filter必需要有返回值,處理完以後咱們要將data數據返回。

filters:{
        filterData(data){
            // console.log(typeof data) //num數字類型
            if(data > 10000){
                data = data /10000;
                data = data.toFixed(1)
                data += "萬"
            }
            return data;
        }
    }

在組件中經過「|」來使用:

<div class = "count line-height">播放量<span class = "sc">{{movie.collect_count | filterData}}</span></div>

(2)mint-ui:UI庫

安裝mint-ui:

cnpm i mint-ui -S

安裝成功後顯示:

image

官網提供了兩種引入mint-ui組件的方式,第一種是完整引入,體積比較大,第二種是按需引入,只引入須要用到的組件,推薦使用第二種。

image

使用第二種方式的時候還須要安裝一個插件:

npm install babel-plugin-component -D

image

以後把下面的內容添加到babel.config.js文件中:

"plugins": [["component", 
    {
      "libraryName": "mint-ui",
      "style": true
    }
  ]]

這裏插一個小BUG,若是你在安裝babel模塊的時候遇到了以下問題,請參考這篇文章解決:

https://segmentfault.com/a/11...,緣由就是babel升級後捨棄了之前的命名方式,修改依賴和babel.config.js配置文件後就能夠正常啓動項目了,關於這個坑網上有很多解決方法,嘗試過了以後發現只有這篇文章完美解決問題。

image

(3)Lazyload:懶加載組件

實現懶加載功能

首先引入Mint-UI中的懶加載模塊

//引入mint-ui相關的模塊
import {Lazyload} from "mint-ui"
Vue.use(InfiniteScroll);

將圖片的src屬性換爲v-lazy指令

<img width = "100%" v-lazy = "getImages(movie.images.small)" alt = "">

這樣頁面在渲染圖片的時候就能夠實現懶加載的效果,只有用戶下滑須要加載圖片的時候才加載

image

未加載的圖片,真正的src屬性暫時存放在data-src中

image

(4)InfiniteScroll:無限加載

mint-ui中也提供了一個插件,實現上拉加載功能,叫作InfiniteScorll,仍是在main.js中引用

//引入mint-ui相關的模塊
import { Lazyload, InfiniteScroll } from "mint-ui"
Vue.use(Lazyload);
Vue.use(InfiniteScroll);

在MovieBox模塊中爲最外層的div標籤添加v-infinite-scroll 指令,便可使用無限滾動。

<div class = "moviebox"
        v-infinite-scroll="loadMore"
        infinite-scroll-disabled="loading"
        infinite-scroll-distance="10"
    >

image

v-infinite-scroll:滾動距離小於閾值的時候會觸發的方法,默認值爲false

infinite-scroll-disabled:若爲真,則無限滾動不會被觸發

infinite-scroll-distance:觸發加載方法的滾動距離閾值(像素)

infinite-scroll-immediate-check:若爲真,則指令被綁定到元素上後會當即檢查是否須要執行加載方法。在初始狀態下內容有可能撐不滿容器時十分有用。,默認值爲true,改爲false則不會執行loadMore方法

咱們在methods中編寫loadMore方法實現無限滾動

methods:{
        loadMore(){
            console.log("loadmore")
            }
        },
    }

只要無限滾動開啓,loadMore方法在頁面建立的時候就會默認執行一次,因此咱們把以前在created中寫的代碼封裝成一個getMovies的方法寫在methods中,在loadMore中觸發

methods:{
        loadMore(){
            console.log("loadMore")
            this.getMovies();
        },
        getMovies(){
            this.$http.get("/api/db/in_theaters",{
            params:{
                limit:6
            }
            }).then(res=>{
                this.movies = res.data.object_list
            })
        }
    }

當infinite-scroll-disabled值爲false的時候無限滾動纔會被出發,咱們給它賦值loading,因此須要在data中定義loading並賦值false,讓它默認觸發無限滾動。

data(){
        return{
            movies:null,
            loading:false,//默認觸發無限滾動
        }
    }

當咱們往下繼續滑動的時候須要請求第二頁的參數,因此須要用到page屬性,當咱們請求數據的時候須要實現動態加載,因此咱們要把limit和page拿出來單獨進行配置。

data(){
        return{
            movies:null,
            loading:false,//默認觸發無限滾動
            limit:6,
            page:1,
        }
    }

利用解構賦值給params,在.then函數中執行page++

getMovies(){
            // 解構的寫法
            let{page,limit} = this;
            this.$http.get("/api/db/in_theaters",{
            params:{
                limit,
                page
            }
            }).then(res=>{
                this.movies = res.data.object_list
                this.page++
            })
        }

這時候咱們發現後請求到的數據把以前的數據覆蓋了,咱們要實現的效果是在第一頁的基礎上累加,因此咱們須要給movies初始化成一個空數組。

movies:[],

利用數組的concat方法在原有的基礎上拼接數組,而且返回新數組賦值給movies。

//在原來的基礎上拼接數組,而且返回新數組
                this.movies = this.movies.concat(res.data.object_list)

這時候咱們就能夠實現下滑加載新內容的效果,可是又出現了另外一個問題,從數據請求結果來看,同一個loadMore方法被屢次觸發

image

而咱們只須要請求一次,因此在咱們請求數據的時候就須要暫時把loadMore關閉

this.loading = true;

當數據回來的時候在.then函數中再開啓

this.loading = false;

這樣纔不會頻繁觸發無限滾動的方法,可是如今即便以後沒有數據了,它仍是會發送數據請求,因此咱們須要設置一個參數來監控是否還有更多數據,沒有數據的時候提示用戶,同時阻止後續操做

hasMore:true //是否有更多數據,默認有更多數據

在.then函數中編寫判斷語句,limit乘以page是否大於總數,若是大於就給給hasMore賦值爲false,同時return false阻止後續操做,這一步須要放在page++前面

if(this.limit * this.page >= res.data.total){ //判斷是否有更多數據
                        this.hasMovies = false //沒有更多數據時將hasMovies賦值爲false,返回false,不執行接下來的page++
                        return false
                    }
this.page++

在loadMore中寫判斷語句,若是hasMore爲false則關閉無限滾動,同時中止getMovies方法,不用再發送ajax請求了,也不會頻繁觸發loadMore方法

loadMore(){
            if(!this.hasMore){
                this.loading = true //沒有更多數據的時候關閉無限滾動,同時返回false,不執行接下來的操做
                return false
            }
            this.getMovies();
        }

image

(5)Toast:彈出框組件

爲了給用戶一個好的使用體驗,須要在數據加載完畢的時候給用戶一個提示,這時候就要用到Toast彈出框組件,先在MovieBox中加載模塊,由於Toast沒法像全局變量同樣使用,只能即時引入

import {Toast} from "mint-ui"

duration是持續的毫秒數,若是想讓它一直顯示只須要賦值爲-1,執行 Toast 方法會返回一個 Toast 實例,每一個實例都有 close 方法,用於手動關閉 Toast。

image

在請求數據以前,放置一個Toast,提示用戶數據正在加載

let instance = Toast({
                message: '正在加載中...',
                duration: -1,
                iconClass:"fa fa-cog fa-spin"
            })

加載完成以後在.then函數中調用close方法,關閉彈出框

instance.close();

因爲我使用mint-ui中的toast樣式出錯,因此這裏改使用Vant,它是一個輕量、可靠的移動端 Vue 組件庫,參數設置和mint-ui差很少,經過vant.Toast來定義:

const toast1 = vant.Toast.loading({
                message: '正在加載中',
                duration: 0,
            })

duration爲0時不會自動關閉,將它賦值給一個常量,經過.clear()方法關閉。

toast1.clear();

image

toast彈窗效果:

image

九、選項卡切換

(1)watch監控數據變化

咱們須要給首頁添加一個正在熱映和即將上映的選項卡,採用數據動態加載的方法,在data中定義一個navs的數組

data(){
      return{
        type:"in_theaters",
        navs:[
          {id:1,title:"正在熱映",type:"in_theaters"},
          {id:2,title:"即將上映",type:"coming_soon"},
        ],
      }
    }

在span中循環遍歷這個數組,在頁面渲染數據,同時添加「active」的calss名,和點擊事件的方法,鼠標點擊選項時,更改type值,同時class判斷type是否等於nav.type,等於的時候添加active選中樣式,實現被點擊的選項變爲選中態的效果。

<div class = "navbar"">
          <span
            v-for = "nav in navs"
            :key = "nav.id"
            :class  = "{'active':type === nav.type}"
            @click = "type = nav.type"
          >{{nav.title}}</span>
        </div>

因爲咱們在MovieBox中請求數據是使用了/api/db/in_theaters這個地址,因此如今只能請求到正在熱映的數據,咱們須要將type值傳遞給MovieBox

<MovieBox :type = "type"></MovieBox>

MovieBox接收

props:["type"],

而後將intheaters改成this.type,實現動態切換請求接口

this.$http.get("/api/db/" + this.type,{

想實現點擊切換的時候顯示不一樣數據,就須要讓MovieBox這個組件根據type值的變化作後續操做,這個時候咱們就須要用到watch來監控type值的變化,讓type值變化的時候就要作一些業務邏輯處理,讓原來的movies數組清空,page從第一頁開始,hasMore賦值爲true,再從新觸發getMovies方法請求更多數據。

watch:{
        type(){
            this.movies = [];
            this.page = 1;
            this.hasMore = true;
            this.getMovies();
        }
    }

選項卡切換效果:

image

十、固定選項欄

(1)created鉤子函數

當頁面向下滾動到必定高度的時候咱們須要將選項欄固定,要實現這個效果,咱們首先要在data中定義一個數據isfixed,初始值爲false

isfixed:false,

當滾動到必定高度的時候,值爲true,同時給選項欄和頁面內容動態綁定class,添加固定定位樣式

<div class = "tab" :class = "{fixedTop:isfixed}">
<div :class = "{fixedBox:isfixed}">

咱們在created函數中監聽scroll事件,獲取頁面的滾動高度,同時爲了不重複觸發這個事件,當同時知足滾動高度大於50而且isfixed值爲false的時候才觸發這個事件。

created(){//初始化一些生命週期相關的事件
      window.addEventListener("scroll",e=>{
        //獲取滾動高度
let sTop = document.documentElement.scrollTop || document.body.scrollTop;
        if(sTop >= 50 && !this.isfixed){
          this.isfixed = true;
        }else if(sTop < 50 && this.isfixed){
          this.isfixed = false;
        }
    })

如今能夠實現滾動必定距離,選項欄固定效果,可是,咱們會發如今其餘頁面滾動的時候也會觸發這個效果,因此咱們要將這個函數寫在methods中,從新定義一個方法listenScroll():

methods:{
      listenScroll(e){
        //獲取滾動高度
        let sTop = document.documentElement.scrollTop || document.body.scrollTop;
        if(sTop >= 330 && !this.isfixed){
          this.isfixed = true;
        }else if(sTop < 300 && this.isfixed){
          this.isfixed = false;
        }
      }
    }

在created中添加這個方法:

created(){//初始化一些生命週期相關的事件
      window.addEventListener("scroll",this.listenScroll)
    },

(2)beforeDestory鉤子函數

而後在離開當前頁面的時候將這個方法銷燬:

beforeDestory(){//組件替換,至關於組件被銷燬的時候,執行銷燬操做
      window.removeEventListener("scroll",this.listenScroll)
    },

這樣它就不會干涉其餘頁面的業務邏輯

十一、記錄緩存

(1)keep-alive:緩存標籤

當咱們在組件之間進行切換的時候,咱們想保持首頁的瀏覽狀態,不要讓它重複渲染,這時候咱們就須要用到keep-alive標籤,將須要被緩存的內容包裹起來,經過include屬性來選擇name匹配的組件

<keep-alive include = "home">
      <router-view></router-view>
</keep-alive>

此時咱們的home組件就會被緩存,因此他的created函數只會執行一次,它不會被銷燬,beforeDestory方法也不會執行,因此咱們以前的方法須要寫在activated 和 deactivated 這兩個生命週期鉤子函數中。

(2)activated和deactived生命週期函數

activated deactivated 只會在 <keep-alive> 樹內的嵌套組件中觸發。

activated(){
      window.addEventListener("scroll",this.listenScroll)
    },
    deactivated(){
      window.removeEventListener("scroll",this.listenScroll)
    }

這時候會發現一個問題,當咱們離開這個組件的時候,他會一次性把數據請求完畢,因此咱們須要在deactived方法中將this.loading值賦爲true,關閉無限滾動,在activated中再開啓。

activated(){
      window.addEventListener("scroll",this.listenScroll)
      this.loading = false;//開啓無限滾動
    },
    deactivated(){
      window.removeEventListener("scroll",this.listenScroll)
      this.loading = true;//關閉無限滾動
    }

同時從別的組件中進入的時候須要給this.isfixed從新賦值爲false,否則它一直是固定定位。

deactivated(){
      window.removeEventListener("scroll",this.listenScroll)
      this.isfixed = false;
      this.loading = true;//關閉無限滾動
    }

(3)beforeRouteLeave鉤子函數

咱們在beforeRouteLeave中經過document.documentElement.scrollTop得到滾動高度

beforeRouteLeave(to,from,next){
      this.homeTop = document.documentElement.scrollTop;
      next();
    }

(4)scrollTo方法

在activated添加window.scrollTo方法記錄滾動條位置

activated(){
      window.addEventListener("scroll",this.listenScroll)
      window.scrollTo(0,this.homeTop)
    }

或者也能夠經過給document.documentElement.scrollTop賦值爲this.homeTop實現跳轉記錄滾動條位置

document.documentElement.scrollTop = this.homeTop

keep-alive標籤緩存效果:

image

十二、返回頂部

(1)自定義指令

咱們須要給首頁添加一個返回頂部的組件,同時在data中定義一個isShow參數搭配v-if指令控制返回頂部按鈕顯示與隱藏,默認一開始是不出現的。

data(){
        return {
            isShow:false
        }
    }

由於咱們在父組件中添加了keep-alive,因此在子組件中也可使用activated和deactivated方法,仍是在methods中添加listenScroll方法,規定在滾動到200px的時候顯示。

methods:{
        listenScroll(){
            let sTop = document.documentElement.scrollTop || document.body.scrollTop
            //避免重複觸發
            if(sTop >= 200 && !this.isShow){
                this.isShow = true;
            }else if(sTop < 200 && this.isShow){
                this.isShow = false;
            }
        },
    }

在兩個鉤子函數中進行添加和移除操做。

activated(){
        window.addEventListener("scroll",this.listenScroll)
    },
    deactivated(){
        window.removeEventListener("scroll",this.listenScroll)
    }

咱們須要實現點擊按鈕返回頂部的操做,就須要給按鈕綁定點擊事件

<div class="back-top-box" v-if = "isShow" @click = "backTop">

同時在methods中編寫backTop方法

backTop(){
            window.scrollTo(0,0)
        }

可是若是頁面中也能夠雙擊選項欄返回頂部,重複綁定事件比較麻煩,這時候咱們就能夠將這個方法寫成一個自定義指令,再須要這個功能的標籤上添加指令便可。

即在modules中封裝自定義指令,經過binding.arg獲取時間類型,默認是click:

//v-backtop 就能夠實現返回頂部功能
import Vue from "vue"
Vue.directive("backtop",{
    bind(el,binding,vnode){
        let eventType = binding.arg || "click";
        el.addEventListener(eventType,e=>{
            window.scrollTo(0,0)
        })
    }
})

在main.js中註冊

//引入directive
import "./modules/directive"

在按鈕中添加自定義指令,實現點擊返回頂部

<div class="back-top-box" v-if = "isShow" v-backtop>

在選項欄中添加自定義指令,實現雙擊返回頂部

<div class = "tab" :class = "{fixedTop:isfixed}" v-backtop:dblclick>

返回頂部運行效果:

image

1三、電影詳情頁

(1)動態路由

建立MovieDetail頁面,經過:id配置動態路由

export default {
    name:"moviedetail",
    path:"/moviedetail/:id",
    component:()=>import("@/views/Movie/MovieDetail")
}

在index中引入、註冊,

import moviedetail from "./moviedetail"

在MovieItem中添加router-link,動態傳遞參數movie.id

<router-link 
        :to = "{name:'moviedetail',params:{id:movie.id}}"
        tag = "div"
        class = "main_block">

將獲取到的res.data賦值給movie

getMovie(){
            this.$http.get("/api/db/movie_detail/" + this.$route.params.id).then(res => {
                this.movie = res.data
            })
        }

將咱們須要的數據動態渲染到頁面上,以後咱們再添加一個Mint-UI中的Header組件,標題賦值爲movie.title,添加返回按鈕,在main.js中引入Header組件

import { Lazyload, InfiniteScroll, Header, Button } from "mint-ui"
Vue.component("mt-header", Header);
Vue.component("mt-button", Button);

在頁面中插入mt-header標籤,如今已經能夠實現點擊跳轉到對應電影詳情頁的效果。

<mt-header fixed :title="movie.title">
                <router-link to="/" slot="left">
                    <mt-button icon="back">返回</mt-button>
                </router-link>
                <mt-button icon="more" slot="right"></mt-button>
            </mt-header>

詳情頁運行效果:

image

1四、購物車效果

(1)vuex 官方狀態管理工具

vue提供的一個全局的狀態管理工具,主要處理項目中多組件間狀態共享

Vuex是vue官方的一款狀態管理工具,什麼是狀態呢?咱們在前端開發中有一個概念:數據驅動,頁面中任意的顯示不一樣,都應該有一條數據來控制,而這條數據又叫作state,狀態。

在vue中。組件間進行數據傳遞、通訊很頻繁,而父子組件和非父子組件的通訊功能也比較完善,可是,惟一困難的就是多組件間的數據共享,這個問題由vuex來處理。

接下來咱們須要實現一個購物車效果,先配置一個底部導航欄:

在mine組件中再新建兩個組件,一個是購物車car頁面,一個是購物列表list頁面,在mine路由中,配置二級路由。

//配置二級路由
    children:[
        {path:"",redirect:"list"},
        {path:"list",component:()=>import("@/views/Mine/List"),name:"list"},
        {path:"car",component:()=>import("@/views/Mine/Car"),name:"car"},
    ]

在mine中用router-view來顯示二級組件頁面,引入mint-ui中的tabbar底部導航組件,在main.js中註冊。

//引入mint-ui相關的模塊
import { Lazyload, InfiniteScroll, Header, Button, Tabbar, TabItem } from "mint-ui"
Vue.component("mt-tabbar", Tabbar);
Vue.component("mt-tab-item", TabItem);

在mine頁面中引用,將item數據動態加載到頁面上,添加router-link實現點擊跳轉功能

<mt-tabbar>
           <mt-tab-item
            v-for="nav in navs"
            :key="nav.id"
           >
            <router-link :to = "{name:nav.name}" active-class = "active">
              <img :src = "nav.src">
              {{nav.title}}
            </router-link>
           </mt-tab-item>
 </mt-tabbar>

能夠安裝Vue Devtools插件來查看vuex的狀態

image

(2)vuex的建立

  1. 建立store:
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

import state from "./state"
import getters from "./getters"
import mutations from "./mutations"
import actions from "./actions"
export default new Vuex.Store({
    state,
    getters,
    mutations,
    actions 
})
  1. 設置state

state就是一個純對象,上面有一些狀態掛載,聲明一條共享數據

export default {
    num:0//聲明一條共享數據
}
  1. 在根實例裏配置store

這樣,咱們就能夠在任意的組件中經過this.$store來使用關於store的api

image

  1. 在組件中使用state

由於在組件中能夠經過this.$store來訪問store

因此咱們也能夠經過this.$store.state來使用state中管理的數據,將它寫在data中

data(){
        return{
            num:this.$store.state.num
        }
    }

以後就能夠經過{{num}}來獲取數據

可是咱們發現,這樣使用的話,當state的數據更改的時候,vue組件並不會從新渲染

也就是說,若是想要在組件中響應式的使用的時候,咱們須要經過計算屬性(computed)來使用

computed:{
    num(){
        return this.$store.state.num
    }
}

這樣的寫法很無趣,並且若是使用的狀態較多會產生冗餘的感受,因此vuex提供了mapState輔助函數,幫助咱們在組件中獲取並使用vuex的store中保存的狀態,可是mapState也是一個對象,對象不能嵌套對象

computed:{
        mapState({
            num:state => state.num
        })
    }

因此咱們能夠這樣寫:

computed:mapState(["num"])

或者將mapState使用...展開:

computed:{
        ...mapState({
            num:state => state.num
        })
    }

可是若是組件中已經有了num這個數據了,而state中的數據名字也叫num就會照成衝突,這個時候咱們能夠在組件使用state的時候,給狀態起個別名:

computed:{
        ...mapState({
            _num:state => state.num //至關於經過this.$store.state.num來獲取num的方法
        })
    }
  1. getters

有的時候,咱們須要根據state中的某一個狀態派生出一個新的狀態,例如,咱們state中有一個num,在某些組件中須要用到是num的二倍的一個狀態,咱們就能夠經過getters來建立。

export default{
    doubleNum(state){
        return state.num*2
    }
}

建立了以後,在組件中經過this.$store.getters.doubleNum來獲取裏面的數據

固然vuex也提供了mapGetters輔助函數來幫助咱們在組件中使用getters裏的狀態,且使用的方法和mapState如出一轍,展開後賦值便可。

import {mapState,mapGetters} from "vuex"
export default {
    computed:{
        ...mapState({
            _num:state => state.num //至關於經過this.$store.state.num來獲取num的方法
        }),
        ...mapGetters(["doubleNum"])
    }

使用的時候直接雙括號裏面寫方法名

{{doubleNum}}
  1. 使用mutations更改state

咱們不能直接在組件中更改state:this.$store.state.num=2,而是須要使用mutations來更改,mutations也是一個純對象,裏面包含不少更改state 的方法。

export default{
    changeNum(state) {
        state.num++
    }
}

這些方法的形參接收到state,在函數體裏更改,這時,組件用到的數據也會更改,實現響應式。

可是咱們也不能直接調用mutations 的方法,須要使用this.$store.commit來調用,第一個參數爲調用的方法名,第二個參數爲傳遞參數。

methods:{
        changeNum(){
            this.$store.commit("changeNum")
        }
    }

vuex提供了mapMutations方法來幫助咱們在組件中調用mutations 的方法,使用方法和mapState、mapGetters同樣,可是它要寫在methods中。

methods:{
        ...mapMutations(["changeNum"])
    }
  1. 將方法名定義爲一個常量

爲了防止方法名被更改,咱們通常將它單獨定義在一個常量文件const.js中,使用的時候引入

import { CHANGE_NUM} from "./const"
export default{
    CHANGE_NUM(state) {
        state.num++
    }
}

可是這樣定義以後只能使用CHANGE_NUM這個名字,當常量中的名字改變時,它不會同步更改,因此咱們要在外部給它包裹一箇中括號,讓它成爲一個變量[CHANGE_NUM],方便咱們維護和管理。

//引入
import {CHANGE_NUM} from "@/store/const"
//註冊
methods:{
        ...mapMutations([CHANGE_NUM])
    }
//使用
<button @click = "CHANGE_NUM">點擊NUM!</button>
  1. 使用actions來處理異步操做

Action 相似於 mutation,不一樣在於:Action 提交的是 mutation,而不是直接變動狀態。 Action 能夠包含任意異步操做。

也就是說,若是有這樣的需求:在一個異步處理以後,更改狀態,咱們在組件中應該先調用actions,來進行異步動做,而後由actions調用mutation來更改數據

import { RANDOM_NUM } from "./const"
export default{
    getNumFromBackend(store){//actions通常作異步請求獲取數據,而後派發mutations裏面具體的更改狀態的方法
        setTimeout(()=>{
            //獲取一個隨機值100
            let randomNum = Math.floor(Math.random()*100)
            store.commit(RANDOM_NUM, randomNum)//actions中的store參數用來派發mutations裏面具體的方法,才能去更改state的值
        },1000)
    }
}

mutations接收到actions中傳來的隨機數,賦值給狀態

import { CHANGE_NUM, RANDOM_NUM} from "./const"
export default{
    [CHANGE_NUM](state) {
        state.num++
    },
    [RANDOM_NUM](state,randomNum){
        state.num = randomNum//給狀態賦值隨機數
    }
}

前端頁面中點擊button觸發事件

<button @click = "getRandom">隨機出現num值!</button>

state變化以後,執行render,視圖從新渲染,就能夠拿到最新的state

getRandom(){
            //派發action
            this.$store.dispatch("getNumFromBackend")
        }

在組件中經過this.$store.dispatch方法調用actions的方法

固然也可使用mapActions來輔助使用

...mapActions(["getNumFromBackend"])

使用的時候直接寫actions中的方法名便可

<button @click = "getNumFromBackend">隨機出現num值!</button>
  1. 使用modules來進行模塊化劃分

當數據信息比較多的時候,咱們將屬於同一個模塊的文件放到一個modules中進行管理,建立myNum文件夾,將actions、const、getters、mutations、state直接放到myNum中去管理。

在myNum文件中建立index.js文件將每一個模塊導出

import state from "./state"
import mutations from "./mutations"
import actions from "./actions"
import getters from "./getters"
export default{
    state,mutations,actions,getters
}

在index.js中從新引入

import myNum from "./myNum"
export default new Vuex.Store({
    modules:{
     myNum
    }
})

如今咱們要在前端頁面中更改引用路徑

import {CHANGE_NUM} from "@/store/myNum/const"

咱們經過state.num無法獲取到num值了

image

咱們須要在外面再嵌套一層myNum

...mapState({
            _num:state => state.myNum.num
        })

(3)接下來咱們就來實現一個購物車的效果

首先在store中建立myCar文件夾,依次建立

  1. state.js:存放共享數據
  2. actions.js:進行異步請求
  3. const.js:常量名稱
  4. mutations.js:更改狀態的方法
  5. getters:根據一個狀態派發一個新狀態
  6. index.js:進行彙總

在index.js中將這些文件依次引入

import state from "./state"
import mutations from "./mutations"
import actions from "./actions"
import getters from "./getters"
export default{
    state,mutations,actions,getters
}

在store/index.js中引入myCar中的index

import myNum from "./myNum"
import myCar from "./myCar"
export default new Vuex.Store({
    modules:{
        myNum,
        myCar
    }
})

在state.js中先定義一個空數組cars

export default {
    cars:[]//聲明一條共享數據
}

這樣咱們就能夠在state中拿到cars的數據,默認爲空。

image

這裏咱們使用mint-ui中的cell組件來搭建頁面,承載每個購物項。先在main.js中註冊一下

import { Lazyload, InfiniteScroll, Header, Button, Tabbar, TabItem,Cell} from "mint-ui"
Vue.component("mt-cell", Cell);

以後咱們就能夠經過mt-cell來使用它了

<mt-cell
            title = '標題文字'
            value = '帶連接'
            label = "描述信息"
        >
        <img slot = "icon" src = "" alt = "">
        </mt-cell>

將咱們獲取到的購物商品數據放到public靜態文件夾下的api中的goods.json中

{
    "dealList":[
        {
            "firstTitle": "單人",
            "title":"46oz原味爆米花1桶+22oz可樂1杯",
            "price": 33,
            "dealId": 100154273,
            "imageUrl":"https://p0.meituan.net/movie/5c30ed6dc1e3b99345c18454f69c4582176824.jpg@388w_388h_1e_1c",
            "curNumberDesc": "已售379"
        },
        {
            "firstTitle": "單人",
            "title": "46oz原味爆米花1桶+22oz雪碧1杯",
            "price": 33,
            "dealId": 100223426,
            "imageUrl": "https://p0.meituan.net/movie/5c30ed6dc1e3b99345c18454f69c4582176824.jpg@388w_388h_1e_1c",
            "curNumberDesc": "已售12"
        },
        {
            "firstTitle": "單人",
            "title": "進口食品1份",
            "price": 8.89,
            "dealId": 100212615,
            "imageUrl": "https://p1.meituan.net/movie/21f1d203838577db9ef915b980867acc203978.jpg@750w_750h_1e_1c",
            "curNumberDesc": "已售8"
        },
        {
            "firstTitle": "雙人",
            "title": "85oz原味爆米花1桶+22oz可樂兩杯",
            "price": 44,
            "dealId": 100152611,
            "imageUrl": "https://p0.meituan.net/movie/bf014964c24ca2ef107133eaed75a6e5191344.jpg@388w_388h_1e_1c",
            "curNumberDesc": "已售647"
        },
        {
            "firstTitle": "雙人",
            "title": "85oz原味爆米花1桶+22oz雪碧兩杯",
            "price": 44,
            "dealId": 100223425,
            "imageUrl": "https://p0.meituan.net/movie/bf014964c24ca2ef107133eaed75a6e5191344.jpg@388w_388h_1e_1c",
            "curNumberDesc": "已售6"
        },
        {
            "firstTitle": "多人",
            "title": "85oz原味爆米花1桶+22oz可樂2杯+冰川時代水1瓶",
            "price": 55,
            "dealId": 100152612,
            "imageUrl": "https://p1.meituan.net/movie/c89df7bf2b1b02cbb326b06ecbbf1ddf203619.jpg@388w_388h_1e_1c",
            "curNumberDesc": "已售89"
        }
        
    ]
}

先聲明一個數據data存放商品。

data(){
        return{
            goods:[],
        }
    }

定義方法getGoods異步請求數據

methods:{
        getGoods(){
            this.$http.get("/api/goods.json").then(res=>{
                console.log(res)
            })
        }
    },

在生命週期鉤子函數中調用一下

created(){
        this.getGoods()
    }

能夠拿到res上的數據,賦值給goods

getGoods(){
            this.$http.get("/api/goods.json").then(res=>{
                // console.log(res)
                this.goods = res.data.dealList;
            })
        }

在mt-cell中循環渲染

<mt-cell
            :title="good.title"
            :label="'¥'+good.price"
            v-for="good in goods" 
            :key="good.dealId"
        >
        <mt-button type = "danger" size = "small" @click = "addGoodInCar(good)">購買</mt-button>
        <div class = "firstTitle">{{good.firstTitle}}</div>
        <img width ="70" height = "70" slot="icon" :src="good.imageUrl" alt="">
        </mt-cell>

真正請求購物車信息應該是從後臺異步請求的,如今咱們依靠localstorage做爲後臺,模擬後臺數據庫,存儲咱們的cars數據。

function getCar(){
    return JSON.parse(localStorage.cars ? localStorage.cars : "[]")
}

如今在action.js中編寫添加商品到購物車的方法addGoodInCar

addGoodInCar(store,goodInfo){//添加商品到購物車
        setTimeout(()=>{
            //獲取後臺返回來的購物車
            let cars = getCar();//[{}]
            let isHas = cars.some(item => {//判斷原來的購物車是否有這個商品
                if (item.dealId === goodInfo.dealId){//若是相等表明添加進來的是同一個商品
                item.num++//商品數++
                return true;}
            })
            if (!isHas) {//說明購物車沒有此商品
                goodInfo.num = 1;//商品數賦值爲1
                cars.push(goodInfo)//將商品添加到cars數據中
            }
            //通知後臺更改cars
            localStorage.cars = JSON.stringify(cars)
            //前段須要經過mutations具體的方法去更改state裏面的cars
            store.commit(SYNC_UPDATE, cars)
        },1000)
    }

在const中聲明一個常量

const SYNC_UPDATE = "SYNC_UPDATE"
export { SYNC_UPDATE }

mutations中定義更新數據的方法

import { SYNC_UPDATE} from "./const"
export default{
    [SYNC_UPDATE](state,newCar){
        state.cars = newCar;
    }
}

在actions中調用這個方法

store.commit(SYNC_UPDATE, cars)

在前端頁面中調用這個方法

import {mapActions} from "vuex"

methods:{
        ...mapActions(["addGoodInCar"])
    }

在mt-button上添加點擊事件,同時將good參數傳遞過去

<mt-button type = "danger" size = "small" @click = "addGoodInCar(good)">購買</mt-button>

在頁面中點擊button,過了一秒鐘,cars中獲取到一個商品信息,再次點擊,num屬性++,變成2。

image

在actions中寫一個獲取購物車的方法

initCar(store){
        //獲取購物車
        let cars = getCar()
        store.commit(SYNC_UPDATE,cars)
    }

在全局App.js中的created鉤子函數中調用,這樣別的組件就能夠拿到cars上的數據了,多組件共享的數據放在vuex中進行管理。

created(){
      //讓頁面走一下首頁,觸發Header中的router.beforeEach函數,否則頭部會重複出現
      this.$router.push("/")
      //初始化購物車
      this.$store.dispatch("initCar")
    }

參考list,開始編寫car.vue頁面,它不須要請求數據了,只須要顯示數據,能夠經過mapState輔助顯示state中的數據。

import {mapState} from "vuex"
export default { 
    computed:{
        ...mapState({
            cars:state=>state.myCar.cars
        })
    }
}

上面v-for循環就直接從cars中取數據

v-for="good in cars"

將button改成"+"和"-"

<mt-button type = "danger" size = "small" @click = "addGoodInCar(good)">+</mt-button>
<mt-button type = "danger" size = "small" @click = "addGoodInCar(good)">-</mt-button>

引入addGoodInCar方法

import {mapState,mapActions} from "vuex"
export default { 
    computed:{
        ...mapState({
            cars:state=>state.myCar.cars
        })
    },
    methods:{
        ...mapActions(["addGoodInCar"]])
    }
}

在actions中編寫減小商品的方法

reduceGoodInCar(store,goodInfo){
        //獲取後臺返回來的購物車
        let cars = getCar();
        cars = cars.filter(item=>{
            if (item.dealId === goodInfo.dealId){
                item.num--
            }
            return true;
        })
        //通知後臺更改cars
        localStorage.cars = JSON.stringify(cars)
        //前段須要經過mutations具體的方法去更改state裏面的cars
        store.commit(SYNC_UPDATE, cars)
    }

在item--後面添加判斷,當數量小於等於0的時候直接return false組織後續操做

if(item.num<=0) return false

接下來咱們再在頁面增長一個計算總價的功能,因爲咱們要依靠於數量和單價的變化來計算總價,因此咱們要把這個方法寫在getters裏面

export default{
    computedTotal(state){
        let cars = state.cars;//在同一個module裏面能夠直接state.cars獲取
        let total = {price:0,num:0} //聲明一個total對象存放總價和數量
        cars.forEach(item=>{
            total.price += item.price * item.num; //總價等於商品價格乘以數量
            total.num += item.num //總數累加
        })
        return total //返回total對象
    }
}

使用mapGetters輔助引入computedTotal方法

import {mapState,mapActions,mapGetters} from "vuex"
export default { 
    computed:{
        ...mapState({
            cars:state=>state.myCar.cars
        }),
        ...mapGetters(["computedTotal"])
    },
    methods:{
        ...mapActions(["addGoodInCar","reduceGoodInCar"])
    }
}

點擊增長商品數量,會發現總數有時候會出現不少位小數的狀況,咱們須要對獲取到的price進行數據處理

total.price = total.price.toFixed(2)//向上取整,並保留兩位小數

在首頁咱們經過v-if和v-else控制商品信息的顯示與隱藏,沒有商品的時候顯示一個p標籤,提示用戶沒有商品了,而且提供一個router-link跳轉回商品列表頁。

<p v-if = "cars.length === 0">
            沒有商品了
            <router-link to = "/mine/list">點擊此處購買商品</router-link>
        </p>
        <div v-else>
            <mt-cell
                :title="good.title"
                :label="'¥'+good.price+'*'+good.num" 
                v-for="good in cars" 
                :key="good.dealId"
            >
            <mt-button type = "danger" size = "small" @click = "addGoodInCar(good)">+</mt-button>
            <mt-button type = "danger" size = "small" @click = "reduceGoodInCar(good)">-</mt-button>
            <div class = "firstTitle">{{good.firstTitle}}</div>
            <img width ="70" height = "70" slot="icon" :src="good.imageUrl" alt="">
            </mt-cell>
        </div>

購物車運行效果:

image

1五、打包上線

(1) 修改配置文件

找到項目的Vue-config.js配置文件,在module.exports中將publicPath: 改成:'/v-douban/'

image

同時本地請求的路徑也須要/v-douban

image

(2)打包文件

執行yarn build 打包成dist文件包

(3)鏈接FTP服務器,修改nginx

進入/usr/local/nginx/conf目錄,傳輸nginx.config文件到本地。

image

修改nginx.config文件,配置數據接口代理。

location /api/db {
            proxy_pass http://47.96.0.211:9000/db;
        } 

        location /data/my {
            proxy_pass http://118.31.109.254:8088/my;
        } 

        location /douban/my {
            proxy_pass http://47.111.166.60:9000/my;
        }

上傳新的nginx.config文件到服務器,覆蓋原文件。

image

在終端鏈接數據庫,而且重啓nginx服務器。

./nginx -s reload

image

因爲咱們此次配置使用了https路徑因此要啓動SSL功能

能夠參考這篇文章:

https://www.cnblogs.com/pisce...

安裝完成後查看配置文件是否更新:

image

進入/usr/local/nginx/html目錄建立一個v-douban文件夾

image

將打包後的dist文件夾中的全部文件上傳到服務器

image

傳輸完成後,便可在網頁中訪問上線項目http://39.96.84.220/v-douban

image

線上瀏覽效果:

image

image

後記

雖然是一個簡單的臨摹項目,可是在編寫的過程當中也着實遇到了不少的問題,感謝朋友們的幫助,學習的道路歷來就不是一路順風,不斷的超越自我才能慢慢變強。關於項目中遇到的問題,歡迎你們一塊兒交流探討,大家的支持是我最大的動力。

相關文章
相關標籤/搜索