【慕課網實戰課程筆記】Vue.js高仿餓了麼外賣App
阿新 • • 發佈:2019-02-06
寫在前面:該課程為慕課網付費課程,筆記內容程式碼居多、內容簡略,僅供自己日後翻閱。如有疑問或者不妥,歡迎提出指正,我看到了會回覆,謝謝!
第1章:課程簡介
第2章:Vuejs介紹
Ctrl+Alt+l
快捷整理程式碼
第3章:Vue-cli開啟Vuejs專案
- 全域性安裝vue-cli腳手架工具:
cnpm install -g vue-cli
- 初始化sell專案:
vue init webpack sell
- 進入sell目錄:
cd sell
- 安裝依賴(依據package.json檔案):
cnpm install
- 執行專案(package.json中配置):
cnpm run dev
或者node build/dev-server.js
第4章:專案實戰-準備工作
- IconMoon 把SVG檔案生成字型檔案
- 寫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)
/* 自定義介面資料 結束 */
第5章:專案實戰-頁面骨架開發
- webstorm 設定Vue型別檔案的預設結構:
New -> Edit File Templates... -> +
<template>
</template>
<script type="text/ecmascript-6">
/* eslint-disable semi */
export default {}
</script>
<style lang="stylus" rel="stylesheet/stylus"></style>
- 安裝三大CSS前處理器(自選)
cnpm install stylus stylus-loader less less-loader sass sass-loader --save-dev
- webpack.base.conf.js 配置路徑別名
module.exports = {
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
// 路徑別名配置(自定義)
'assets': resolve('src/assets'),
'components': resolve('src/components')
}
}
}
- 修改配置檔案不能觸發hotreload
第6章:專案實戰-header元件開發
- 安裝ajax非同步請求外掛vue-resource:
cnpm install vue-resource --save-dev
- post-css根據can i use自動新增瀏覽器相容
- 配置專案整體路由
// 檔案位置: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">
/* eslint-disable semi */
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();
// console.log(queryParam);
return queryParam.id;
})()
}
}
},
created() {
// ajax請求
this.$http.get('/api/seller?id=' + this.seller.id).then(response => {
// get body data
response = response.body; // 返回json物件
if (response.error === ERR_OK) {
// this.seller = response.data;
// 給物件擴充套件屬性
this.seller = Object.assign({}, this.seller, response.data);
console.log(this.seller.id);
}
}, response => {
// error callback
});
},
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 Hello from '@/components/Hello'; */
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', // 自定義路由啟用class名
routes: routes
});
- 配置專案整體依賴
// 檔案位置:src/main.js
/* eslint-disable semi */
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-resource必須放在前面,放在後面報錯
Vue.use(VueResource);
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
render: h => h(App)
});
// router.push('goods'); // 設定預設路由
- 通用樣式
// 檔案位置:static/css/reset.css
/**
* Eric Meyer's Reset CSS v2.0 (http://meyerweb.com/eric/tools/css/reset/)
* http://cssreset.com
*/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header,
menu, nav, output, ruby, section, summary,
time, mark, audio, video, input {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font-weight: normal;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
/* custom */
a {
color: #7e8c8d;
text-decoration: none;
text-decoration: none;
-webkit-backface-visibility: hidden;
}
li {
list-style: none;
}
::-webkit-scrollbar {
width: 5px;
height: 5px;
}
::-webkit-scrollbar-track-piece {
background-color: rgba(0, 0, 0, 0.2);
-webkit-border-radius: 6px;
}
::-webkit-scrollbar-thumb:vertical {
height: 5px;
background-color: rgba(125, 125, 125, 0.7);
-webkit-border-radius: 6px;
}
::-webkit-scrollbar-thumb:horizontal {
width: 5px;
background-color: rgba(125, 125, 125, 0.7);
-webkit-border-radius: 6px;
}
html, body {
width: 100%;
}
body {
-webkit-text-size-adjust: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
// 檔案位置:src/common/stylus/base.styl
body, html
line-height: 1
font-weight: 200
font-family: 'PingFang SC', 'STHeitiSC-Light', 'Helvetica-Light', arial, sans-serif
.clearfix
display: inline-block
&:after
display: block
content: '.'
height: 0
line-height: 0
clear: both
visibility: hidden
@media (-webkit-min-device-pixel-ratio: 1.5),(min-device-pixel-ratio: 1.5)
.border-1px
&::after
-webkit-transform: scaleY(0.7)
transform: scaleY(0.7)
@media (-webkit-min-device-pixel-ratio: 2),(min-device-pixel-ratio: 2)
.border-1px
&::after
-webkit-transform: scaleY(0.5)
transform: scaleY(0.5)
// 檔案位置:src/common/stylus/mixin.styl
border-1px($color)
position: relative
&:after
display: block
position: absolute
left: 0
bottom: 0
width: 100%
border-top: 1px solid $color
content: ' '
border-none()
&:after
display: none
bg-image($url)
background-image: url($url+"@2x.png")
@media (-webkit-min-device-picel-ratio: 3),(min-device-picel-ratio: 3)
background-image: url($url+"@3x.png")
// 引用mixin
@import "common/stylus/mixin.styl"
第7章:專案實戰-goods 商品列表頁開發
- 安裝better-scroll:
cnpm install better-scroll --save-dev
// ref屬性一定是駝峰式命名的,不能用連字元的;
// ref可以用來獲取HTML元素,同時也能獲取子元件
/*
<ul ref='food'>
<li></li>
</ul>
<shopcart ref="shopcart" :select-foods="selectFoods" :delivery-price="seller.deliveryPrice" :min-price="seller.minPrice"></shopcart>
*/
this.$refs.food
this.$refs.food.getElementByTagName('li')
this.$refs.shopcart.drop(target);
- 在created鉤子的ajax非同步請求成功後執行better-scroll初始化
export default {
created() {
this.classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee'];
this.$http.get('/api/goods').then(response => {
// get body data
response = response.body; // 返回json物件
if (response.error === ERR_OK) {
this.goods = response.data;
console.log(this.goods);
// DOM非同步載入完成後
// 呼叫better-scroll封裝的方法
// 動態計算每個區塊的高度
this.$nextTick(() => {
this._initScroll();
this._calculateHeight();
})
}
}, response => {
// error callback
});
}
}
- better-scroll 會禁止移動端的點選事件,需要重新派發,同時在PC端會點選兩次,此處需要做判斷
export default {
methods: {
selectMenu(index, event) {
if (!event._constructed) { // 不是better-scroll派發的事件
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 // 實時監聽位置配置
});
// 監聽scroll事件,繫結scrollY
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
}
}
- 引數新增屬性,並使其能被觀測到
// 給food新增count屬性,並設定它的值為1,這樣VUE就可以觀測到
Vue.set(this.food, 'count', 1);
- 小球動畫函式監聽
export default {
methods: {
drop(el) {
// console.log(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);
}
}
},
// 此回撥函式是可選項的設定
// 與 CSS 結合時使用
dropping: function (el, done) {
/* eslint-disable no-unused-vars */
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)';
// 監聽動畫結束事件,之後執行done函式
el.addEventListener('transitionend', done);
});
// done();
},
afterDrop: function (el) {
let ball = this.dropBalls.shift();
if (ball) {
ball.show = false;
el.style.display = 'none';
}
}
}
}
- 阻止冒泡/預設事件
<div class="content-right" @click.stop.prevent="pay">
<div class="pay" :class="payClass">{{payDesc}}</div>
</div>
第8章:專案實戰-food商品詳情頁實現
- 時間戳格式化
// 檔案位置:src/common/js/date.js
export function formatDate(date, fmt) {
// 實現思路:正則表示式把fmt動態的替換成對應的字串
/* eslint-disable semi */
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);
}
// 模板結構
// <div class="time">{{rating.rateTime | formatDate}}</div>
// js程式碼
import {formatDate} from '../../common/js/date'; // 引入公共date.js檔案的formatDate方法
export default {
filters: { // 自定義過濾器
formatDate(time) {
let date = new Date(time);
return formatDate(date, 'yyyy-MM-dd hh:mm');
}
}
}
第9章:ratings評價列表頁實現
第10章:seller商家詳情頁實現
- seller元件中的better-scroll應用在
mounted()
鉤子和updated()
鉤子裡面(注:非同步請求資料的是放在created鉤子裡)
export default {
mounted() {
console.log('mounted'); // 等同於VUE1.0的的ready
this._initScroll();
this._initPics();
},
updated() {
console.log('updated'); // 相當於watch
this._initScroll();
this._initPics();
}
}
- 本地儲存相關操作封裝
/**
* 檔案位置:src/common/js/store.js
*/
// 儲存到本地儲存
export function saveToLocal(id, key, value) {
/* eslint-disable semi */
let seller = window.localStorage.__seller__; // localstorage前面要加window,alert也一樣
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() {
/* eslint-disable semi */
let url = window.location.search; // ?id=12&a=b
let obj = {};
let reg = /[?&][^?&]+=[^?&]+/g;
let arr = url.match(reg);
// ['?id=12', '&a=b']
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;
}
優化:tab點選不再重複請求,並儲存狀態,使用keep-alive元件
第11章:專案實戰-專案編譯打包
- 專案編譯打包:
cnpm run build
// 配置打包規範:config/index.js
module.exports = {
build: {
// 生產環境
productionSourceMap: true, // 配置是否生成sourceMap除錯檔案
port: 9000 // 起一個build的埠
},
dev: {
// 開發環境
}
}
- 利用express編寫一個本地伺服器,並配置api介面路由。
// 檔案位置:./prod.server.js
/* eslint-disable semi */
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);
});
第12章:課程總結
- 忽略Eslint預設的分號校驗(預設不加分號):
/* eslint-disable semi */
- Eslint規範總體設定:
// 檔案位置:./.eslintrc.js
// http://eslint.org/docs/user-guide/configuring
module.exports = {
root: true,
parser: 'babel-eslint',
parserOptions: {
sourceType: 'module'
},
env: {
browser: true,
},
// https://github.com/standard/standard/blob/master/docs/RULES-en.md
extends: 'standard',
// required to lint *.vue files
plugins: [
'html'
],
// add your custom rules here
'rules': {
// allow paren-less arrow functions
'arrow-parens': 0,
// allow async-await
'generator-star-spacing': 0,
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
// 自定義規則:分號、縮排
// 'semi': ['error', 'always'], // 需要新增分號
'indent': 0, // 空格縮排
'space-before-function-paren': 0, // 不檢查函式名後面的空格
'camelcase': 0 // 不檢查駝峰式常量
}
}
- inline-block元素之間的間隙:設定
font-size: 0
或者 標籤不換行 - 圖片背景半透明
/* 模板結構
<div class="background">
<img :src="seller.avatar" width="100%" height="100%">
</div>
*/
/* CSS樣式 */
.background {
position: absolute
top: 0
left: 0
width: 100%
height: 100%
z-index: -1
filter: blur(10px)
}
- css-sticky-footers佈局
/* 模板結構
<div class="detail">
<div class="detail-wrapper">
<div class="detail-main"></div>
</div>
<div class="detail-close">
<i class="icon-close"></i>
</div>
</div>
*/
/* CSS樣式 */
.detail {
position: fixed;
z-index: 100;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(7,17,27,0.8);
}
.detail-wrapper {
width: 100%;
min-height: 100%;
}
.detail-main {
padding: 64px 0;
}
.detail-close {
width: 32px;
height: 32px;
margin: -64px auto 0;
font-size: 32px;
}
- 圖片和文字水平對齊實現
/*
<div class="container">
<p class="text">文字</p>
<img class="img" src="imgurl" />
</div>
*/
.container {
margin-bottom: 5px;
}
.text, .img {
display: inline-block;
vertical-align: top;
}
.text {
line-height: 18px;
}
- 多行文字垂直居中
.parent {
display: table;
}
.child {
display: table-cell;
vertical-align: middle;
}
- flex佈局實現左側固定寬度,右側寬度自適應
.content-left {
flex: 0 0 105px;
width: 105px;
}
.content-right {
flex: 1;
}
- 寬高相等的容器防抖動
.image-header {
position: relative
width: 100%
height: 0
padding-top: 100% /*把padding設定的跟寬度一樣*/
}
.image-header img {
position: absolute
top: 0
left: 0
width: 100%
height: 100%
}
- 所有需要js操作的元素都加一個
name-hook
class名
<div class="ball ball-hook"></div>
- 定義方法時的規範
// 元件私有的
_privateFunc() {}
// 其他元件可以呼叫的
publicFunc() {}
第13章:vue1.0升級到vue2.0
- 配置檔案
package.json
拷貝新的依賴
build目錄
直接拷貝,修改webpack.base.conf.js相容字首、別名配置,多了個check-versions.js
config目錄
直接拷貝 - 元件修改
- Vue-router API變化:
- 初始化路由變化
- v-link 指令替換為 元件
- Vue2.0語法變化
- v-for 指令的變化
- v-el、v-ref 指令的變化
- 模板變化,元件只允許一個根元素
- 元件通訊變化,$dispath 廢除
- 事件監聽變化,廢除 events 屬性
- 不能在子元件直接修改父元件傳入的prop
- 過渡的變化,transition元件
- 小球下落動畫實現
- keep-alive 屬性變為 元件
- 廢棄ready鉤子,使用mounted代替,同時新增了beforeMounted、beforeUpdated、updated等
- Vue-router API變化: