【慕课网实战课程笔记】Vue.js高仿饿了么外卖App

7 篇文章 0 订阅
6 篇文章 0 订阅

写在前面:该课程为慕课网付费课程,笔记内容代码居多、内容简略,仅供自己日后翻阅。如有疑问或者不妥,欢迎提出指正,我看到了会回复,谢谢!

第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目录 直接拷贝
  • 组件修改
    1. Vue-router API变化:
      • 初始化路由变化
      • v-link 指令替换为 组件
    2. Vue2.0语法变化
      • v-for 指令的变化
      • v-el、v-ref 指令的变化
      • 模板变化,组件只允许一个根元素
      • 组件通信变化,$dispath 废除
      • 事件监听变化,废除 events 属性
      • 不能在子组件直接修改父组件传入的prop
      • 过渡的变化,transition组件
      • 小球下落动画实现
      • keep-alive 属性变为 组件
      • 废弃ready钩子,使用mounted代替,同时新增了beforeMounted、beforeUpdated、updated等
  • 1
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值