飞道的博客

Vue.js高仿饿了么外卖App学习记录

294人阅读  评论(0)

(给达达前端加星标,提升前端技能

开发一款vue.js开发一款app,使用vue.js是一款高效的mvvm框架,它轻量,高效,组件化,数据驱动等功能便于开发。使用vue.js开发移动端app,学会使用组件化,模块化的开发方式。

学习了如何根据需求分析开发,使用脚手架工具,数据mock,架构设计,自己测试,编译打包等流程。

线上生产环境,如何考虑架构设计,组件抽象,模块拆分,代码风格统一,变量命名要求规范等优点。

一款外卖app,商家页面,商家基本信息(顶部),商品区块,商品列表,分类列表,小球飞入购物车的动画。商品详情页,需要有顶部商品的大图,商品的详细信息,以及还有商品的评价列表。

商品,评论列表,商家展示商家的详情信息。

用vue-resource与后端做数据交互,vue-router前端路由,better-scroll的Js库等。使用vue-cli脚手架,搭建基本代码框架,vue-router官方插件管理路由。vue-resource是用于ajax通信的,webpack构建工具的使用。

Vue是一套用于构建用户界面的渐进式JavaScript框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,方便与第三方库或既有项目整合。

Vue.js 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件,Vue.js 自身不是一个全能框架——它只聚焦于视图层。因此它非常容易学习,非常容易与其它库或已有项目整合。

目录/文件 说明
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


   
  1. <html>
  2. <head>
  3. <meta charset="utf-8">
  4. <title>sell </title>
  5. <meta name="viewport"
  6. content= "width=device-width,initial-scale=1.0,maxinum-scale=1.0,
  7. minimun-scale=1.0,user-scalable=no">
  8. <link rel="stylesheet" type="text/css" href="static/css/reset.css">
  9. </head>
  10. </body>
  11. <app> </app>
  12. </body>
  13. </html>

meta name="viewport"

它是移动端浏览器在一个比屏幕更宽的虚拟窗口中渲染页面,用来实现展示没有做移动端适配的网页,可以完整的展示给用户,viewport的宽度就是可显示区域的宽度。


   
  1. <meta name="viewport"
  2. content= "width=device-width,initial-scale=1.0,maxinum-scale=1.0,
  3. 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的宽度变成为理想的宽度,防止横向滚动条出现。


   
  1. <meta name= "viewport" content= "width=device-width, user-scalable=no,
  2. 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数据接口


   
  1. // 文件位置:build/dev-server.js
  2. // 注:此处是关键代码
  3. var app = express()
  4. var appData = require( '../data.json')
  5. var seller = appData.seller
  6. var goods = appData.goods
  7. var ratings = appData.ratings
  8. var apiRoutes = express.Router()
  9. apiRoutes.get( '/seller', function (req, res) {
  10. res.json({
  11. error: 0,
  12. data: seller
  13. })
  14. })
  15. apiRoutes.get( '/goods', function (req, res) {
  16. res.json({
  17. error: 0,
  18. data: goods
  19. })
  20. })
  21. apiRoutes.get( '/ratings', function (req, res) {
  22. res.json({
  23. error: 0,
  24. data: ratings
  25. })
  26. })
  27. app. use( '/api', apiRoutes)

项目实战,页面骨架开发

webstorm设置文件的默认结构


   
  1. <template>
  2. </template>
  3. <script type="text/ecmascript-6">
  4. export default {}
  5. </script>
  6. <style lang="stylus" rel="stylesheet/stylus">
  7. </style>

安装ajax异步请求插件vue-resource

cnpm install vue-resource --save-dev

文件位置:src/APP.vue


   
  1. <template>
  2. <div>
  3. <v-header :seller="seller"> </v-header>
  4. <div class="tab border-1px">
  5. <div class="tab-item">
  6. <router-link to="/goods">商品 </router-link>
  7. </div>
  8. <div class="tab-item">
  9. <router-link to="/ratings">评论 </router-link>
  10. </div>
  11. <div class="tab-item">
  12. <router-link to="/seller">商家 </router-link>
  13. </div>
  14. </div>
  15. <!-- 路由外链 -->
  16. <keep-alive>
  17. <router-view :seller="seller"> </router-view>
  18. </keep-alive>
  19. </div>
  20. </template>
  21. <script type="text/ecmascript-6">
  22. import {urlParse} from './common/js/util';
  23. import header from './components/header/header.vue';
  24. const ERR_OK = 0;
  25. export default {
  26. data() {
  27. return {
  28. seller: {
  29. id: (() => {
  30. let queryParam = urlParse();
  31. return queryParam.id;
  32. })()
  33. }
  34. }
  35. },
  36. created() {
  37. this.$http.get( '/api/seller?id=' + this.seller.id).then( response => {
  38. response = response.body;
  39. if (response.error === ERR_OK) {
  40. this.seller = Object.assign({}, this.seller, response.data);
  41. console.log( this.seller.id);
  42. }
  43. }, response => {
  44. });
  45. },
  46. components: {
  47. 'v-header': header
  48. }
  49. }
  50. </script>
  51. <style lang="stylus" rel="stylesheet/stylus">
  52. @ import "common/stylus/mixin.styl"
  53. .tab
  54. display: flex
  55. width: 100%
  56. height: 40px
  57. border- 1px(rgba( 7, 17, 27, 0.1))
  58. line-height: 40px
  59. .tab-item
  60. flex: 1
  61. text-align: center
  62. & > a
  63. display: block
  64. font-size: 14px
  65. color: rgb( 77, 85, 93)
  66. &.active
  67. color: rgb( 240, 20, 20)
  68. </style>

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


   
  1. import Vue from 'vue';
  2. import Router from 'vue-router';
  3. import goods from '@/components/goods/goods.vue';
  4. import ratings from '@/components/ratings/ratings.vue';
  5. import seller from '@/components/seller/seller.vue';
  6. Vue.use(Router);
  7. const routes = [{
  8. path: '/',
  9. component: goods
  10. }, {
  11. path: '/goods',
  12. component: goods
  13. }, {
  14. path: '/ratings',
  15. component: ratings
  16. }, {
  17. path: '/seller',
  18. component: seller
  19. }];
  20. export default new Router({
  21. linkActiveClass: 'active',
  22. routes: routes
  23. });

文件位置:src/main.js


   
  1. import Vue from 'vue';
  2. import App from './App.vue';
  3. import router from './router';
  4. import VueResource from 'vue-resource';
  5. Vue.config.productionTip = false;
  6. import '../static/css/reset.css';
  7. import './common/stylus/base.styl';
  8. import './common/stylus/index.styl';
  9. import './common/stylus/icon.styl';
  10. Vue.use(VueResource);
  11. new Vue({
  12. el: '#app',
  13. router,
  14. render: h => h(App)
  15. });

安装better-scroll

cnpm install better-scroll --save-dev


   
  1. export default {
  2. created() {
  3. this.classMap = [ 'decrease', 'discount', 'special', 'invoice', 'guarantee'];
  4. this.$http. get( '/api/goods').then(response => {
  5. response = response.body;
  6. if (response.error === ERR_OK) {
  7. this.goods = response. data;
  8. console.log( this.goods);
  9. this.$nextTick(() => {
  10. this._initScroll();
  11. this._calculateHeight();
  12. })
  13. }
  14. }, response => {
  15. });
  16. }
  17. }


   
  1. export default {
  2. methods: {
  3. selectMenu(index, event) {
  4. if (!event._constructed) {
  5. return;
  6. }
  7. console.log(index);
  8. let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook');
  9. let el = foodList[index];
  10. this.foodsScroll.scrollToElement(el, 300);
  11. },
  12. _initScroll() {
  13. this.menuScroll = new BScroll(this.$refs.menuWrapper, {
  14. click: true
  15. });
  16. this.foodsScroll = new BScroll(this.$refs.foodsWrapper, {
  17. click: true,
  18. probeType: 3
  19. });
  20. this.foodsScroll.on('scroll', (pos) => {
  21. this.scrollY = Math.abs(Math.round(pos.y));
  22. })
  23. },
  24. _calculateHeight() {
  25. let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook');
  26. let height = 0;
  27. this.listHeight.push(height);
  28. for (let i = 0; i < foodList.length; i++) {
  29. let item = foodList[i];
  30. height += item.clientHeight;
  31. this.listHeight.push(height);
  32. }
  33. }
  34. },
  35. components: {
  36. shopcart,
  37. cartcontrol
  38. }
  39. }

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

小球动画函数监听


   
  1. export default {
  2. methods: {
  3. drop(el) {
  4. for ( let i = 0; i < this.balls.length; i++) {
  5. let ball = this.balls[i];
  6. if (!ball.show) {
  7. ball.show = true;
  8. ball.el = el;
  9. this.dropBalls.push(ball);
  10. return;
  11. }
  12. }
  13. },
  14. beforeDrop: function (el) {
  15. let count = this.balls.length;
  16. while (count--) {
  17. let ball = this.balls[count];
  18. if (ball.show) {
  19. let rect = ball.el.getBoundingClientRect();
  20. let x = rect.left - 32;
  21. let y = -( window.innerHeight - rect.top - 22);
  22. el.style.display = '';
  23. el.style.webkitTransform = `translate3d(0,${y}px,0)`;
  24. el.style.transform = `translate3d(0,${y}px,0)`;
  25. let inner = el.getElementsByClassName( 'inner-hook')[ 0];
  26. inner.style.webkitTransform = `translate3d(${x}px,0,0)`;
  27. inner.style.transform = `translate3d(${x}px,0,0)`;
  28. console.log(el, x, y);
  29. }
  30. }
  31. },
  32. dropping: function (el, done) {
  33. let rf = el.offsetHeight;
  34. this.$nextTick( () => {
  35. el.style.display = '';
  36. el.style.webkitTransform = 'translate3d(0,0,0)';
  37. el.style.transform = 'translate3d(0,0,0)';
  38. let inner = el.getElementsByClassName( 'inner-hook')[ 0];
  39. inner.style.webkitTransform = 'translate3d(0,0,0)';
  40. inner.style.transform = 'translate3d(0,0,0)';
  41. el.addEventListener( 'transitionend', done);
  42. });
  43. },
  44. afterDrop: function (el) {
  45. let ball = this.dropBalls.shift();
  46. if (ball) {
  47. ball.show = false;
  48. el.style.display = 'none';
  49. }
  50. }
  51. }
  52. }

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


   
  1. export function formatDate(date, fmt) {
  2. if ( /(y+)/.test(fmt)) {
  3. fmt = fmt.replace( RegExp.$ 1, (date.getFullYear() + '').substr( 4 - RegExp.$ 1.length));
  4. }
  5. let o = {
  6. 'M+': date.getMonth() + 1,
  7. 'd+': date.getDate(),
  8. 'h+': date.getHours(),
  9. 'm+': date.getMinutes(),
  10. 's+': date.getSeconds()
  11. };
  12. for ( let k in o) {
  13. if ( new RegExp( `(${k})`).test(fmt)) {
  14. let str = o[k] + '';
  15. fmt = fmt.replace( RegExp.$ 1, ( RegExp.$ 1.length === 1) ? str : padLeftZero(str));
  16. }
  17. }
  18. return fmt;
  19. }
  20. function padLeftZero(str) {
  21. return ( '00' + str).substr(str.length);
  22. }
  23. import {formatDate} from '../../common/js/date';
  24. filters: {
  25. formatDate(time) {
  26. let date = new Date(time);
  27. return formatDate(date, 'yyyy-MM-dd hh:mm');
  28. }
  29. }
  30. }


   
  1. export default {
  2. mounted() {
  3. console.log( 'mounted');
  4. this._initScroll();
  5. this._initPics();
  6. },
  7. updated() {
  8. console.log( 'updated');
  9. this._initScroll();
  10. this._initPics();
  11. }
  12. }

本地存储相关操作封装

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


   
  1. // 存储到本地存储
  2. export function saveToLocal(id, key, value) {
  3. let seller = window.localStorage.__seller__;
  4. if (!seller) {
  5. seller = {};
  6. seller[id] = {};
  7. } else {
  8. seller = JSON.parse(seller);
  9. if (!seller[id]) {
  10. seller[id] = {};
  11. }
  12. }
  13. seller[id][key] = value;
  14. window.localStorage.__seller__ = JSON.stringify(seller);
  15. }
  16. // 从本地存储里面读取
  17. export function loadFromLocal(id, key, def) {
  18. /* eslint-disable semi */
  19. let seller = window.localStorage.__seller__;
  20. if (!seller) {
  21. return def;
  22. }
  23. seller = JSON.parse(seller)[id];
  24. if (!seller) {
  25. return def;
  26. }
  27. let ret = seller[key];
  28. return ret || def;
  29. }

解析url参数

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


   
  1. export function urlParse() {
  2. let url = window.location.search;
  3. let obj = {};
  4. let reg = /[?&][^?&]+=[^?&]+/g;
  5. let arr = url.match(reg);
  6. if (arr) {
  7. arr.forEach( (item) => {
  8. let tempArr = item.substring( 1).split( '=');
  9. let key = decodeURIComponent(tempArr[ 0]);
  10. let val = decodeURIComponent(tempArr[ 1]);
  11. obj[key] = val;
  12. })
  13. }
  14. return obj;
  15. }

项目编译打包

cnpm run build

配置打包规范:config/index.js


   
  1. module.exports = {
  2. build: {
  3. productionSourceMap: true,
  4. port: 9000
  5. },
  6. dev: {
  7. }
  8. }

利用express编写一个本地服务器

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


   
  1. let express = require( 'express');
  2. let config = require( './config/index');
  3. let port = process.env.PORT || config.build.port;
  4. let app = express();
  5. let router = express.Router();
  6. router.get( '/', function (req, res, next) {
  7. req.url = '/index.html';
  8. next();
  9. });
  10. app.use(router);
  11. let appData = require( './data.json');
  12. let seller = appData.seller;
  13. let goods = appData.goods;
  14. let ratings = appData.ratings;
  15. let apiRoutes = express.Router();
  16. apiRoutes.get( '/seller', function (req, res) {
  17. res.json({
  18. error: 0,
  19. data: seller
  20. })
  21. });
  22. apiRoutes.get( '/goods', function (req, res) {
  23. res.json({
  24. error: 0,
  25. data: goods
  26. })
  27. });
  28. apiRoutes.get( '/ratings', function (req, res) {
  29. res.json({
  30. error: 0,
  31. data: ratings
  32. })
  33. });
  34. app.use( '/api', apiRoutes);
  35. app.use(express.static( './dist'));
  36. module.exports = app.listen(port, function (err) {
  37. if (err) {
  38. console.log(err);
  39. return;
  40. }
  41. console.log( 'Listening at http://localhost:' + port);
  42. });

Eslint规范总体设置

项目开发流程

需求分析,脚手架工具,数据mock,架构设计,代码编写,自测,编译打包。

可以看看别人的代码

仿【饿了么】订餐软件的一个demo

https://github.com/guxun12/ele_demo

参考资料&资源

慕课网视频,Vue.js高仿饿了么外卖App

Vue.js 高仿饿了么外卖 App 课程源码,课程地址: http://coding.imooc.com/class/74.html

推荐阅读  点击标题可跳转

【面试Vue全家桶】vue前端交互模式-es7的语法结构?async/await

【面试需要-Vue全家桶】一文带你看透Vue前端路由

【面试需要】掌握JavaScript中的this,call,apply的原理

2019年的每一天日更只为等待她的出现,好好过余生,庆余年 | 掘金年度征文

进来就是一家人【达达前端技术社群⑥】

这是一个有质量,有态度的公众号

点关注,有好运


转载:https://blog.csdn.net/qq_36232611/article/details/104204932
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场