专栏首页达达前端Vue.js高仿饿了么外卖App学习记录

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

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

开发一款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_modulesnpm 加载的项目依赖模块

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目录

cdsell

安装依赖

cnpm install

运行项目

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

写mock数据接口

// 文件位置:build/dev-server.js// 注:此处是关键代码varapp = express()varappData =require('../data.json')varseller = appData.sellervargoods = appData.goodsvarratings = appData.ratingsvarapiRoutes = 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

cnpminstallvue-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

importVuefrom'vue';importRouterfrom'vue-router';importgoodsfrom'@/components/goods/goods.vue';importratingsfrom'@/components/ratings/ratings.vue';importsellerfrom'@/components/seller/seller.vue';Vue.use(Router);constroutes = [{path:'/',component: goods}, {path:'/goods',component: goods}, {path:'/ratings',component: ratings}, {path:'/seller',component: seller}];exportdefaultnewRouter({linkActiveClass:'active',routes: routes});

文件位置:src/main.js

importVuefrom'vue';importAppfrom'./App.vue';importrouterfrom'./router';importVueResourcefrom'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);newVue({el:'#app', router,render:h=>h(App)});

安装better-scroll

cnpminstallbetter-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);

小球动画函数监听

exportdefault{methods: { drop(el) {for(leti =0; i { el.style.display =''; el.style.webkitTransform ='translate3d(0,0,0)'; el.style.transform ='translate3d(0,0,0)';letinner = 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){letball =this.dropBalls.shift();if(ball) { ball.show =false; el.style.display ='none'; } } }}

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

exportfunctionformatDate(date, fmt){if(/(y+)/.test(fmt)) { fmt = fmt.replace(RegExp.$1, (date.getFullYear() +'').substr(4-RegExp.$1.length)); }leto = {'M+': date.getMonth() +1,'d+': date.getDate(),'h+': date.getHours(),'m+': date.getMinutes(),'s+': date.getSeconds() };for(letkino) {if(newRegExp(`(${k})`).test(fmt)) {letstr = o[k] +''; fmt = fmt.replace(RegExp.$1, (RegExp.$1.length ===1) ? str : padLeftZero(str)); } }returnfmt;}functionpadLeftZero(str){return('00'+ str).substr(str.length);}import{formatDate}from'../../common/js/date'; filters: { formatDate(time) {letdate =newDate(time);returnformatDate(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

// 存储到本地存储exportfunctionsaveToLocal(id, key, value){letseller =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);}// 从本地存储里面读取exportfunctionloadFromLocal(id, key, def){/* eslint-disable semi */letseller =window.localStorage.__seller__;if(!seller) {returndef; } seller =JSON.parse(seller)[id];if(!seller) {returndef; }letret = seller[key];returnret || def;}

解析url参数

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

exportfunctionurlParse(){leturl =window.location.search;letobj = {};letreg =/[?&][^?&]+=[^?&]+/g;letarr = url.match(reg);if(arr) { arr.forEach((item) =>{lettempArr = item.substring(1).split('=');letkey =decodeURIComponent(tempArr[0]);letval =decodeURIComponent(tempArr[1]); obj[key] = val; }) }returnobj;}

项目编译打包

cnpm run build

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

module.exports = {

build: {

productionSourceMap: true,

port: 9000

},

dev: {

}

}

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

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

letexpress =require('express');letconfig =require('./config/index');letport = process.env.PORT || config.build.port;letapp = express();letrouter = express.Router();router.get('/',function(req, res, next){ req.url ='/index.html'; next();});app.use(router);letappData =require('./data.json');letseller = appData.seller;letgoods = appData.goods;letratings = appData.ratings;letapiRoutes = 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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 小程序获取时间格式

    达达前端
  • javascript 闭包

    在A中返回B的引用 如果一个对象不再被引用,那么这个对象就会被GC回收,否则这个对象一直会保存在内存中。

    达达前端
  • 微信小程序知识云开发

    小程序界面设计、交互、功能与他人的手机应用软件或在先发布的小程序构成实质性相似,构成小程序抄袭

    达达前端
  • 从零开始搭建 VUE + Element UI后台管理系统框架

    后台管理系统前端框架,现在很流行的形式都是,上方和左侧都是导航菜单,中间是具体的内容。比如阿里云、七牛云、头条号、百家号等等,他们的管理系统都是这样的。

    Javanx
  • RocketMQ事务消息代码样例 顶

    第三步,写一个你要执行的方法,比如你的本项目的一次数据库执行,或者其他业务代码。我这里要执行的是保存个人信息。

    算法之名
  • SPEED 飞车扩容改造:敢于对过去说不

    接手飞车运维以来,在扩缩容上耗费了比较多的精力,于是有了我们今天的主题,飞车扩容改造。

    wincent
  • Mask R-CNN源代码终于来了,还有它背后的物体检测平台

    夏乙 编译整理 量子位 出品 | 公众号 QbitAI “等代码吧。” 从Mask R-CNN论文亮相至今的10个月里,关于它的讨论几乎都会以这句话收尾。 ?...

    量子位
  • 用隐私换安全?Airbnb与监管机构共享房东信息

    每个国家对于各个行业的监管措施都有所不同,逐渐形成的中国特色也挡住了诸多外企在中国的发展之路,如谷歌、Facebook。近期Airbnb对宣布,在有必要的条件下...

    FB客服
  • 22道高频JavaScript手写面试题及答案

    由于防抖函数原理:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

    桃翁
  • BZOJ 3668: [Noi2014]起床困难综合症【贪心】

    3668: [Noi2014]起床困难综合症 Time Limit: 10 Sec  Memory Limit: 512 MB Submit: 2326  So...

    Angel_Kitty

扫码关注云+社区

领取腾讯云代金券