Vue.js高仿餓了麼外賣App學習記錄

(給達達前端加星標,提高前端技能)javascript

開發一款vue.js開發一款app,使用vue.js是一款高效的mvvm框架,它輕量,高效,組件化,數據驅動等功能便於開發。使用vue.js開發移動端app,學會使用組件化,模塊化的開發方式。php

學習瞭如何根據需求分析開發,使用腳手架工具,數據mock,架構設計,本身測試,編譯打包等流程。css

線上生產環境,如何考慮架構設計,組件抽象,模塊拆分,代碼風格統一,變量命名要求規範等優勢。html

一款外賣app,商家頁面,商家基本信息(頂部),商品區塊,商品列表,分類列表,小球飛入購物車的動畫。商品詳情頁,須要有頂部商品的大圖,商品的詳細信息,以及還有商品的評價列表。前端

商品,評論列表,商家展現商家的詳情信息。vue

用vue-resource與後端作數據交互,vue-router前端路由,better-scroll的Js庫等。使用vue-cli腳手架,搭建基本代碼框架,vue-router官方插件管理路由。vue-resource是用於ajax通訊的,webpack構建工具的使用。java

Vue是一套用於構建用戶界面的漸進式JavaScript框架。與其它大型框架不一樣的是,Vue 被設計爲能夠自底向上逐層應用。Vue 的核心庫只關注視圖層,方便與第三方庫或既有項目整合。node

Vue.js 的目標是經過儘量簡單的 API 實現響應的數據綁定和組合的視圖組件,Vue.js 自身不是一個全能框架——它只聚焦於視圖層。所以它很是容易學習,很是容易與其它庫或已有項目整合。webpack

目錄/文件nginx

說明

build

項目構建(webpack)相關代碼

config

配置目錄,包括端口號等。咱們初學可使用默認的。

node_modules

npm 加載的項目依賴模塊

src

包含了幾個目錄及文件:

  • assets: 放置一些圖片,如logo等。
  • components: 目錄裏面放了一個組件文件,能夠不用。
  • App.vue: 項目入口文件,咱們也能夠直接將組件寫這裏,而不使用 components 目錄。
  • main.js: 項目的核心文件。

static

靜態資源目錄,如圖片、字體等。

test

初始測試目錄,可刪除

.xxxx文件

這些是一些配置文件,包括語法配置,git配置等。

index.html

首頁入口文件,你能夠添加一些 meta 信息或統計代碼啥的。

package.json

項目配置文件。

README.md

項目的說明文檔,markdown 格式

說一說mvc和mvvm的區別

mvc的全名是Model view Controller,是模型model,視圖view,控制器controller的縮寫,用一種業務邏輯,數據,界面顯示分離的方法來寫代碼,view視圖,視圖層調用控制器到controller控制器,控制器調用model,model返回數據給控制器,而後控制器將數據返回給view。

這是mvc的簡單調用流程,mvc模式是單向的數據綁定,view視圖層調用model層,要經過中間層controller來實現。

mvvm模式是雙向數據綁定,view,model,vm進行數據的綁定和事件的監聽,對view和model進行監聽,當有一方的值發生變化時,就更新另外一個。

數據響應原理

組件化原理

vue-cli,vue.js的開發利器,腳手架

vue-cli能夠搞定,目錄結構,本地調試,代碼部署,熱加載,單元測試。

vue-cli的安裝方法:

node -v

mac 

sudo npm install -g vue-cli

使用webpack模板,名字sell,外賣app。

運行效果:

而後把項目放進你的編輯器

mode_modules文件夾:npm install 安裝的依賴代碼庫

src文件夾是咱們存放的源碼

這個文件跟我不同也沒事。

editorconfig是編輯器的配置

eslintignore爲忽略語法檢查的目錄文件

eslintrc.js爲eslint的配置文件

商品頁面:

商品頁_公共以及優惠信息

商品頁購物車詳情

商品頁面_商品詳情頁面

評價頁

商家頁

設備像素比devicePixelRatio

在移動端,devicePixelRatio指的是window.devicePixelRatio。

移動端設備分爲非視網膜屏幕和視網膜屏幕。

window.devicePixelRatio是設備上物理像素和設備獨立像素的比例,公式表就是:window.devicePixeRatio = 物理像素/dips。

icomoon.io,圖標字體制做

mock數據,模擬後臺數據

icon- 開頭的圖標(如圖所示) 

首先進入網頁https://icomoon.io/ 

而後點擊右上角的「IcoMoon APP」按鈕,選擇導入本身的SVG圖來生成ico-的圖標,點擊新頁面左上角的「Inport ICONS」。 

在devServer下面加入

頁面骨架開發

sell->build->confi->node_modules->resource, img, psd, svg ->src, common->components, app.vue->static

<html>
<head>
  <meta charset="utf-8">
    <title>sell</title>
    <meta name="viewport"
    content="width=device-width,initial-scale=1.0,maxinum-scale=1.0,
    minimun-scale=1.0,user-scalable=no">
    <link rel="stylesheet" type="text/css" href="static/css/reset.css">
</head>
</body>
<app></app>
</body>
</html>

meta name="viewport"

它是移動端瀏覽器在一個比屏幕更寬的虛擬窗口中渲染頁面,用來實現展現沒有作移動端適配的網頁,能夠完整的展現給用戶,viewport的寬度就是可顯示區域的寬度。

<meta name="viewport"
    content="width=device-width,initial-scale=1.0,maxinum-scale=1.0,
    minimun-scale=1.0,user-scalable=no">

這些屬性能夠混合使用,width控制視圖窗口的寬度,height控制視圖窗口的高度,這個屬性不多用,initial-scale爲控制頁面最初加載時在最理想的狀況下縮放的等級,一般設置爲1.0,能夠是小數,maximum-scale爲容許用戶的最大縮放量,minimum-scale爲容許用戶的最小縮放量。

user-scalable爲是否容許用戶進行縮放,值只能「no」或者「yes」。no爲不容許,yes爲容許。

width和initial-scale設置了二者,瀏覽器會自動選擇數值最大的進行適配。

就是當窗口的最適配理想寬度爲300時,initial-scale的值設置爲1時,width設置的值爲400,那麼取最大值,400。

當窗口的最適配理想值爲500時,那麼取的值爲500。

width=device-width和initial-scale=1都表示爲最理想的viewport,可是在ipad,iphone等移動設備,ie上,橫豎屏不分,默認都爲豎屏的寬度,兼容的最好寫法。

什麼是viewport,它是用戶網頁的可視區域,翻譯就是視區。

手機瀏覽器是把頁面放在一個虛擬的"窗口"(viewport)中,一般這個虛擬的"窗口"(viewport)比屏幕寬,這樣就不用把每一個網頁擠到很小的窗口中(這樣會破壞沒有針對手機瀏覽器優化的網頁的佈局),用戶能夠經過平移和縮放來看網頁的不一樣部分。

沒有添加viewport的效果:

加了viewport的效果:

viewport這個特性被用於移動設備,可是也能夠用在支持相似「固定到邊緣」等特性的桌面瀏覽器,如微軟的edge。

按百分比計算尺寸的時候,就是參照的初始視口,它指的是任何用戶代理和樣式對它進行修改以前的視口。桌面瀏覽器若是不是全屏模式的話,通常是基於窗口大小。

在移動設備上,初始視口一般就是應用程序可使用的屏幕部分。

在viewport中就是瀏覽器上用來顯示網頁的那部分區域。

width=device-width能使全部瀏覽器當前的viewport寬度變成理想的寬度,initial-scale=1是將頁面的初始縮放值設置爲1。用來將viewport的寬度變成爲理想的寬度,防止橫向滾動條出現。

<meta name="viewport" content="width=device-width, user-scalable=no,
initial-scale=1.0,maximum-scale=1.0, minimum-scale=1.0">

width=device-width表示爲寬度是設備屏幕的寬度

initial-scale=1.0表示爲初始的縮放比例

minimum-scale=0.5表示爲最小的縮放比例

maximum-scale=2.0表示爲最大的縮放比例

user-scalable=yes表示用戶是否能夠調整縮放比例

設備像素,設備獨立像素,css像素掌握

設備像素就是屏幕上的真實像素點,iphone6的設備像素像素爲750*1334,則屏幕上有750*1334個像素點;設備獨立像素,操做系統定義的一種長度單位,iphone6的設備獨立像素375*667,正好是設備像素的一半,css像素,css中的長度單位,在css中使用px都是指css像素。

物理像素來表明設備像素,獨立像素表明設備獨立像素。

在很早的時候,只有物理像素,沒有獨立像素,在不縮放的前提,css中的1px表明着一個物理像素。

不過從iphone4開始,推出了retina屏幕,物理像素變成640*960,屏幕尺寸沒有變化,在單位面積上的物理像素的數量增長了,則表示屏幕密度增長了。按照原來,1px css像素由1個物理像素來渲染,那麼width:320px的元素就會佔據半個屏幕的寬度。

1個獨立像素==2個物理像素

viewport是瀏覽器窗口,表明瀏覽器的可視區域,就是瀏覽器中用來顯示網頁的部分區域。

像素單位有設備像素,邏輯像素,css像素。

設備像素也叫物理像素。

什麼是設備像素,它指的是顯示器上的真實像素,每一個像素的大小是屏幕固有的屬性。

設備分辨率是用來描述這個顯示器的寬和高分別有多少個設備像素。

設備像素和設備分辨率由操做系統來管理。

全局安裝vue-cli腳手架工具

cnpm install -g vue-cli

初始化sell項目

vue init webpack sell

進入sell目錄

cd sell

安裝依賴

cnpm install

運行項目

cnpm run dev 或者 node build/dev-server.js

寫mock數據接口

// 文件位置:build/dev-server.js
// 注:此處是關鍵代碼
var app = express()


var appData = require('../data.json')
var seller = appData.seller
var goods = appData.goods
var ratings = appData.ratings


var apiRoutes = express.Router()
apiRoutes.get('/seller', function (req, res) {
  res.json({
    error: 0,
    data: seller
  })
})
apiRoutes.get('/goods', function (req, res) {
  res.json({
    error: 0,
    data: goods
  })
})
apiRoutes.get('/ratings', function (req, res) {
  res.json({
    error: 0,
    data: ratings
  })
})


app.use('/api', apiRoutes)

項目實戰,頁面骨架開發

webstorm設置文件的默認結構

<template>


</template>


<script type="text/ecmascript-6">
    export default {}
</script>


<style lang="stylus" rel="stylesheet/stylus">
</style>

安裝ajax異步請求插件vue-resource

cnpm install vue-resource --save-dev

文件位置:src/APP.vue

<template>
  <div>
    <v-header :seller="seller"></v-header>
    <div class="tab border-1px">
      <div class="tab-item">
        <router-link to="/goods">商品</router-link>
      </div>
      <div class="tab-item">
        <router-link to="/ratings">評論</router-link>
      </div>
      <div class="tab-item">
        <router-link to="/seller">商家</router-link>
      </div>
    </div>
    <!-- 路由外鏈 -->
    <keep-alive>
      <router-view :seller="seller"></router-view>
    </keep-alive>
  </div>
</template>


<script type="text/ecmascript-6">
  import {urlParse} from './common/js/util';
  import header from './components/header/header.vue';


  const ERR_OK = 0; 


  export default {
    data() {
      return {
        seller: {
          id: (() => {
            let queryParam = urlParse();
            return queryParam.id;
          })()
        }
      }
    },
    created() {
      this.$http.get('/api/seller?id=' + this.seller.id).then(response => {
        response = response.body; 
        if (response.error === ERR_OK) {
          this.seller = Object.assign({}, this.seller, response.data);
          console.log(this.seller.id);
        }
      }, response => {
      });
    },
    components: {
      'v-header': header
    }
  }
</script>


<style lang="stylus" rel="stylesheet/stylus">
  @import "common/stylus/mixin.styl"
  .tab
    display: flex
    width: 100%
    height: 40px
    border-1px(rgba(7, 17, 27, 0.1))
    line-height: 40px
    .tab-item
      flex: 1
      text-align: center
      & > a
        display: block
        font-size: 14px
        color: rgb(77, 85, 93)
        &.active
          color: rgb(240, 20, 20)
</style>

文件位置:src/router/index.js

import Vue from 'vue';
import Router from 'vue-router';
import goods from '@/components/goods/goods.vue';
import ratings from '@/components/ratings/ratings.vue';
import seller from '@/components/seller/seller.vue';


Vue.use(Router);


const routes = [{
  path: '/',
  component: goods
}, {
  path: '/goods',
  component: goods
}, {
  path: '/ratings',
  component: ratings
}, {
  path: '/seller',
  component: seller
}];


export default new Router({
  linkActiveClass: 'active',
  routes: routes
});

文件位置:src/main.js

import Vue from 'vue';
import App from './App.vue';
import router from './router';
import VueResource from 'vue-resource';


Vue.config.productionTip = false;


import '../static/css/reset.css';
import './common/stylus/base.styl';
import './common/stylus/index.styl';
import './common/stylus/icon.styl';
Vue.use(VueResource);
new Vue({
  el: '#app',
  router,
  render: h => h(App)
});

安裝better-scroll

cnpm install better-scroll --save-dev

export default {
  created() {
    this.classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee'];
    this.$http.get('/api/goods').then(response => {
      response = response.body; 
      if (response.error === ERR_OK) {
        this.goods = response.data;
        console.log(this.goods);
        this.$nextTick(() => {
          this._initScroll();
          this._calculateHeight();
        })
      }
    }, response => {
    });
  }
}

export default {
    methods: {
      selectMenu(index, event) {
        if (!event._constructed) {
          return;
        }
        console.log(index);
        let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook');
        let el = foodList[index];
        this.foodsScroll.scrollToElement(el, 300);
      },
      _initScroll() {
        this.menuScroll = new BScroll(this.$refs.menuWrapper, {
          click: true 
        });
        this.foodsScroll = new BScroll(this.$refs.foodsWrapper, {
          click: true,
          probeType: 3 
        });


        this.foodsScroll.on('scroll', (pos) => {
          this.scrollY = Math.abs(Math.round(pos.y));
        })
      },
      _calculateHeight() {
        let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook');
        let height = 0;
        this.listHeight.push(height);
        for (let i = 0; i < foodList.length; i++) {
          let item = foodList[i];
          height += item.clientHeight;
          this.listHeight.push(height);
        }
      }
    },
    components: {
      shopcart,
      cartcontrol
    }
}

Vue.set(this.food, 'count', 1);

小球動畫函數監聽

export default {
    methods: {
        drop(el) {
            for (let i = 0; i < this.balls.length; i++) {
                let ball = this.balls[i];
                if (!ball.show) {
                    ball.show = true;
                    ball.el = el;
                    this.dropBalls.push(ball);
                    return;
                }
            }
        },


        beforeDrop: function (el) {
            let count = this.balls.length;
            while (count--) {
                let ball = this.balls[count];
                if (ball.show) {
                    let rect = ball.el.getBoundingClientRect();
                    let x = rect.left - 32;
                    let y = -(window.innerHeight - rect.top - 22);
                    el.style.display = '';
                    el.style.webkitTransform = `translate3d(0,${y}px,0)`;
                    el.style.transform = `translate3d(0,${y}px,0)`;
                    let inner = el.getElementsByClassName('inner-hook')[0];
                    inner.style.webkitTransform = `translate3d(${x}px,0,0)`;
                    inner.style.transform = `translate3d(${x}px,0,0)`;
                    console.log(el, x, y);
                }
            }
        },


        dropping: function (el, done) {
            let rf = el.offsetHeight; 
            this.$nextTick(() => {
                el.style.display = '';
                el.style.webkitTransform = 'translate3d(0,0,0)';
                el.style.transform = 'translate3d(0,0,0)';
                let inner = el.getElementsByClassName('inner-hook')[0];
                inner.style.webkitTransform = 'translate3d(0,0,0)';
                inner.style.transform = 'translate3d(0,0,0)';
                el.addEventListener('transitionend', done);
            });
        },
        afterDrop: function (el) {
            let ball = this.dropBalls.shift();
            if (ball) {
                ball.show = false;
                el.style.display = 'none';
            }
        }
    }
}

文件位置:src/common/js/date.js

export function formatDate(date, fmt) {


  if (/(y+)/.test(fmt)) {
    fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length));
  }
  let o = {
    'M+': date.getMonth() + 1,
    'd+': date.getDate(),
    'h+': date.getHours(),
    'm+': date.getMinutes(),
    's+': date.getSeconds()
  };
  for (let k in o) {
    if (new RegExp(`(${k})`).test(fmt)) {
      let str = o[k] + ''; 
      fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? str : padLeftZero(str));
    }
  }
  return fmt;
}


function padLeftZero(str) { 
  return ('00' + str).substr(str.length);
}




import {formatDate} from '../../common/js/date'; 
    filters: { 
        formatDate(time) {
            let date = new Date(time);
            return formatDate(date, 'yyyy-MM-dd hh:mm');
      }
    }
}

export default {
  mounted() {
    console.log('mounted'); 
    this._initScroll();
    this._initPics();
  },
  updated() {
    console.log('updated'); 
    this._initScroll();
    this._initPics();
  }
}

本地存儲相關操做封裝

文件位置:src/common/js/store.js

// 存儲到本地存儲
export function saveToLocal(id, key, value) {
  let seller = window.localStorage.__seller__; 
  if (!seller) {
    seller = {};
    seller[id] = {};
  } else {
    seller = JSON.parse(seller);
    if (!seller[id]) {
      seller[id] = {};
    }
  }
  seller[id][key] = value;
  window.localStorage.__seller__ = JSON.stringify(seller);
}
// 從本地存儲裏面讀取
export function loadFromLocal(id, key, def) {
  /* eslint-disable semi */
  let seller = window.localStorage.__seller__;
  if (!seller) {
    return def;
  }
  seller = JSON.parse(seller)[id];
  if (!seller) {
    return def;
  }
  let ret = seller[key];
  return ret || def;
}

解析url參數

文件位置: src/common/js/util.js

export function urlParse() {
  let url = window.location.search;
  let obj = {};
  let reg = /[?&][^?&]+=[^?&]+/g;
  let arr = url.match(reg);
  if (arr) {
    arr.forEach((item) => {
      let tempArr = item.substring(1).split('=');
      let key = decodeURIComponent(tempArr[0]);
      let val = decodeURIComponent(tempArr[1]);
      obj[key] = val;
    })
  }
  return obj;
}

項目編譯打包

cnpm run build

配置打包規範:config/index.js

module.exports = {
  build: {
    productionSourceMap: true, 
    port: 9000 
  },
  dev: {


  }
}

利用express編寫一個本地服務器

文件位置:./prod.server.js

let express = require('express');
let config = require('./config/index');


let port = process.env.PORT || config.build.port;


let app = express();


let router = express.Router();


router.get('/', function (req, res, next) {
  req.url = '/index.html';
  next();
});


app.use(router);
let appData = require('./data.json');
let seller = appData.seller;
let goods = appData.goods;
let ratings = appData.ratings;


let apiRoutes = express.Router();
apiRoutes.get('/seller', function (req, res) {
  res.json({
    error: 0,
    data: seller
  })
});
apiRoutes.get('/goods', function (req, res) {
  res.json({
    error: 0,
    data: goods
  })
});
apiRoutes.get('/ratings', function (req, res) {
  res.json({
    error: 0,
    data: ratings
  })
});


app.use('/api', apiRoutes);


app.use(express.static('./dist'));


module.exports = app.listen(port, function (err) {
  if (err) {
    console.log(err);
    return;
  }
  console.log('Listening at http://localhost:' + port);
});

Eslint規範整體設置

項目開發流程

需求分析,腳手架工具,數據mock,架構設計,代碼編寫,自測,編譯打包。

能夠看看別人的代碼

仿【餓了麼】訂餐軟件的一個demo

https://github.com/guxun12/ele_demo

參考資料&資源

慕課網視頻,Vue.js高仿餓了麼外賣App

Vue.js 高仿餓了麼外賣 App 課程源碼,課程地址: http://coding.imooc.com/class...

推薦閱讀  點擊標題可跳轉

【面試Vue全家桶】vue前端交互模式-es7的語法結構?async/await

【面試須要-Vue全家桶】一文帶你看透Vue前端路由

【面試須要】掌握JavaScript中的this,call,apply的原理

2019年的每一天日更只爲等待她的出現,好好過餘生,慶餘年 | 掘金年度徵文

進來就是一家人【達達前端技術社羣⑥】

這是一個有質量,有態度的公衆號

點關注,有好運

相關文章
相關標籤/搜索