(给达达前端加星标,提高前端技能)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
包含了几个目录及文件:
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
【面试须要】掌握JavaScript中的this,call,apply的原理
2019年的每一天日更只为等待她的出现,好好过余生,庆余年 | 掘金年度征文
这是一个有质量,有态度的公众号
点关注,有好运