前言:学习Vue.js高仿饿了么课程过程中,总结了这个Web App项目从准备到开发完毕自己觉得很重要的知识点。这一篇主要介绍:项目准备、页面骨架开发、header组件开发。
项目github地址:https://github.com/66Web/ljq_eleme,欢迎Star。
App | header |
一、项目分析&学习目标 |
当前最火的MVVM框架
- Vue.js —— 轻量、简洁、高效、数据驱动、组件化
高仿上线外卖App标准来开发
- 核心模块 —— 商家模块
开发一个webApp的全流程
- 需求分析
- 脚手架工具
- 数据mock
- 架构设计
- 代码编写
- 自测
- 编译打包
以线上生产环境的代码质量作标准
- 代码开发及测试环节:
- UI标注
- 真实数据演示
- 代码规范
- 架构设计
- 组件抽象
- 模块拆分
- 代码风格统一
- JS变量命名规范
- CSS代码规范
功能技术分析
- vue-resource: 和后端做数据交互
- vue-router: 做前端路由,实现单页应用
- 第三方JS库better-scroll: 列表滚动的实现
- 最大程度组件化: 提高代码的复用
- html5的localstorage:【收藏商家】功能—存储在浏览器端
- 细节:图标字体的使用
- 移动端1像素边框
- css sticky footer布局
- flex 弹性布局
学习目标
- 掌握Vue.js在实战中的运用
- 学会使用Vue.js【完整的】开发移动端App
- 学会组件化、模块化的开发
学习内容
- Vue.js框架介绍
- Vue-cli 脚手架 —— 搭建基本代码框架
- vue-router 官方插件 —— 管理路由
- vue-resource 官方插件 —— 和后端作Ajax通信
- Webpack 开源构建工具(把源代码经过编译生成浏览器可以识别和运行的代码)
- es6 + eslint eslint —— es6代码风格检查工具
- 工程化 组件化 模块化
- 移动端常用开发技巧:
- flex弹性布局
- css stickyfooter
- 酷炫的交互设计
二、Vue.js介绍 |
近年来前端开发趋势
- 旧浏览器逐渐淘汰,移动端需求增加
- 前端交互越来越多,功能越来越复杂
- 架构从传统后台MVC 向REST API+ 前端MV* 迁移
(前者传统MVC:更新数据会刷新页面 后者前端MV*: 向后端REST API异步请求数据,局部刷新页面)
MV* —— MVC、MVP、MVVM
MVVM框架
View ViewModel Model
视图 通讯 数据
- DOM 观察者 Javascript对象
- 针对具有复杂交互逻辑的前端应用
- 提供基础的架构抽象
- 通过Ajax数据持久化,保证前端用户体验
- MVVM框架技术:vue.js、react.js、Angular.js
对比Anglar React
- Vue.js更轻量,gzip后大小只有 20K+
- Vuejs更易上手,学习曲线平稳
- 吸收两家之长,借鉴了angular的指令和react的组件化
vue.js 核心思想
- 数据驱动
- 组件化
组件设计原则
- 页面上每个独立的可视/可交互区域视为一个组件
- 每个组件对应一个工程目录,组件所需要的各种资源在这个目录下【就近维护】
- 页面不过是组件的容器,组件可以嵌套自由组合形成完整的页面
三、Vue-cli开启Vue项目 |
Vue-cli 是Vue的脚手架工具 —— 帮助写好Vue基础代码的工具
- 目录结构
- 本地调试
- 代码部署
- 热加载
- 单元测试
安装使用
(sudo) npm install -g vue-cli // sudo:mac环境下有关管理权限的命令
vue init webpack my-project
项目文件
- src文件夹:存放项目源码
- bulld目录+ config目录:webpack配置相关
- node_modules文件夹:npm install 安装的依赖代码库
- static—>.gitkeep: 当这个目录为空时也可以将它提交到git仓库中
- babelrc : babel的一些配置,es6语法的转换
- .editorconfig: 编辑器的配置
- .eslintignore: 忽略语法检查的目录文件,一般忽略build目录和node_modules目录
- .eslintrc.js: eslint的配置文件
- gitignore: 上传git仓库要忽略的一些文件的配置
- index.html: 入口html文件,要使用的css和js文件会在编译过程中自动插入
- package.json:整个项目的配置文件,一般用来描述项目 ↓
→ scripts: 配置一些需要执行的命令
→ dependencies:开发环境中的依赖
→ devdependencies: 编译过程中的依赖
项目运行
npm run dev
- src开发目录下:
- main.js —— 项目入口文件
- App.vue —— 主页面组件
- vue语法糖: export default { } 一个对象——可以定义一个组件
【小知识点】sublime自动格式化 —— Command+option+L 或 Control+alt+L |
- 在父组件中使用子组件,如Hello.vue:
- 引用
import Hello from './compoments/Hello'
- 注册
export default{ components: { Hello //es6语法 相当于 'Hello': Hello } }
- 使用标签
<hello><hello>
开发时的Webpack配置与编译
- build->dev-server.js 或 Webpack.dev.conf.js
- webpack.base.conf.js : 配置各种文件的Loader
→ 配置默认识别的路径
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
}
}
四、准备工作 |
图标字体制作
- 在线制作网站:https://icomoon.io/app/#/select
- 将自己的SVG图标导入,输出自己的图标字体文件
- Import Icons → Generate Fonts → preferences修改名称 → Download
- 使用:icon.css和fonts文件夹下所有文件
项目目录设计
- src->common目录下:项目公用文件 js、style、fonts
【css的stylus语法】
npm install stylus stylus-loader --save-dev
|
- resource目录下:项目图片文件——可以删掉无用的assets目录,但需要修改引用到的地方
- components目录下:布局、业务功能等分模块管理组件,如header目录,footer目录
- static->css目录下:reset.css 标签默认样式
- 在 index.html 中引入:
<link rel="stylesheet" type="text/css" href="static/css/reset.css">
前后端分离
- Vue SPA —— 前端通过 vue-resource Ajax从后端获取数据
- 前端最重要的任务:mock数据(后台数据模拟) data.json
{
"seller":{} //商家相关字段
"goods":{} //商品相关字段
"rattings":{} //评论相关字段
}
webpack.dev.conf.js中配置
- 使用 express框架 开启一个node server,用 express.Router 编写这些接口请求
- 首先:在 const portfinder = require(‘portfinder’) 后添加
const express = require('express')//开启一个node server const app = express() //定义一个对象,包含express返回的数据 var appData = require('../data.json') //定义一个对象引入data数据 var seller = appData.seller; var goods = appData.goods; var ratings = appData.ratings; app.use('/api', apiRoutes); //调用app对象
- 然后:找到 devserver{}, 在里面添加
before(app) { app.get('/api/seller', (req, res) => { res.json({ errno: 0, //错误码:实际上是业务方根据业务自己定的 data: seller }) //接口返回json数据,上面配置的数据seller就赋值给data请求后调用 }), app.get('/api/goods', (req, res) => { res.json({ errno: 0, data: goods }) }), app.get('/api/ratings', (req, res) => { res.json({ errno: 0, data: ratings }) }) }
- 注意:每次配置完 express 之后都需要重新启动
查看json数据
- 在Google地址栏中输入:localhost:8080/api/seller
- 依赖Google的jsonview插件 —— 安装 使数据格式化
【Google安装第三方插件】
—— 转载自【小白白打酱油博客】 |
五、页面骨架开发 |
移动端视口
- index.html 中通过meta设置视口可被缩放,初试宽高设置
<meta name="viewport"
content="width=device-width,initial-scale=1.0,maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
App.vue 中把页面拆为三个区块
<div id="app">
<div class="header">header</div>
<div class="tab">tab</div>
<div class="content">content</div>
</div>
然后,分别抽成一个组件,引用 —— 所有组件自定义标签名不可与html本身标签重合 'v-header': header
移动端经典布局 flex
<div class="tab"> <div class="tab-item">商品</div> <div class="tab-item">评论</div> <div class="tab-item">商家</div> </div>
.tab
display: flex
100%
height: 40px
line-height: 40px
.tab-item
flex:1
text-align: center
vueRouter
- Vue2.0 使用 <router-link> 进行【导航】
<router-link :to="{ path: '/goods' }">商品</router-link>
- 【路由外链 】<router-view> —— 单页面切换的内容页,替换content div区块
<router-view></router-view>
-
main.js 中设置单页面应用路由的【挂载组件】—— 默认App.vue 也可以自定义组件如layout.vue
/* eslint-disable no-new */ new Vue({ el: '#app', router, components: { App }, template: '<App/>' })
-
配置【路由map】:router->index.js
export default new Router({ mode: 'history', routes: [ { path: '/', redirect: '/goods',//默认页面重定向 }, { path: '/goods', component: goods }, { path: '/ratings', component: ratings }, ] })
- 【点击】高亮显示 a.router-link-active
#app .tab .tab-item>a{ display: block; font-size: 14px; color: rgb(77, 85, 93); } #app .tab .tab-item>a.router-link-active{ color: rgb(240, 20, 20) }
1像素border实现
- 错误做法:直接给tab加1像素边框 X
.tab{ border-bottom: 1px solid rgba(7,17,27,0.1) }
- 问题是:这段代码在PC端显示,是1像素,但是在手机端显示,就不是1像素。
- 因为手机端有一个DPR的概念:它的物理像素是设备像素的两倍。所以iPhone6上面可能就是一个2像素的边框
【PC开发中用手机实时预览的小技巧】
|
- 正确做法:给 tab 加一个伪类:after , 让它是一条1像素的线,然后在DBR为2或3的手机端缩放 √
<div class="tab border-1px">
- 定义mixin.styl: 通过css预处理器的函数方法,实现伪类线
border-1px($color) position: relative &:before display: block position: absolute left:0 top: 0 100% border-bottom: 1px solid $color content: '' &:after display: block position: absolute left:0 bottom: 0 100% border-top: 1px solid $color content: ''
- 定义base.styl: 实现不同DBR的移动端的缩放
@media(-webkit-min-device-pixel-ratio: 1.5),(min-device-pixel-ratio: 1.5)//DPR为1.5的缩放0.7倍 .border-1px &:before -webkit-transform: scaleY(0.7) transform:scaleY(0.7) &:after -webkit-transform: scaleY(0.7) transform:scaleY(0.7)
@media(-webkit-min-device-pixel-ratio: 2),(min-device-pixel-ratio: 2)//DPR为2的缩放0.5倍 .border-1px &:before -webkit-transform: scaleY(0.5) transform:scaleY(0.5) &:after -webkit-transform: scaleY(0.5) transform:scaleY(0.5) - 定义index.styl: 引用所有styl文件,最后在 main.js 中全局引用
@import"./mixin" //import后无空格 @import"./icon" @import"./base"
main.js: import '@/common/stylus/index.styl' //import后有空格
六、header组件开发 |
vue-resource
- 安装 vue-resource
npm install vue-resource --save
注意:每次install完插件等之后需要重新启动项目
- main.js 文件中:
import VueResource from 'vue-resource' Vue.use(VueResource)
之后就可以在项目任何地方:使用 this.$http 命令
- App.vue 组件中:
- export module{} 外:
const ERR_OK = 0; //定义常量,增强程序可读性
- export module{} 内:
data() { return { seller:{} //维护数据 seller } }
- 异步请求数据,返回的是Promise对象
created: function () { this.$http.get('/api/seller') //发送get请求, .then(function(res){ //.then方法 请求完成后调用 //第一个函数是请求成功后方法 }, function (err) { //第二个函数是请求失败后方法 }) }
使用ES6 箭头函数:箭头函数前后必须有空格
created: function () { this.$http.get('/api/seller') .then((res) => { res = res.body //拿到response返回的promise对象的body(Data Object) if (res.errno === ERR_OK) { this.seller = res.data; //console.log(this.seller) } }, (err) => { }) }
外部组件
- 父组件 App.vue 中 <header> 组件标签中用v-bind绑定seller属性,传给子组件seller数据
<v-header :seller="seller"></v-header>
- 子组件 header.vue 中通过 props属性 获取父组件传来的seller数据
props: { seller: { type: Object } }
- 模板对应位置 显示 对应seller.xxx 子数据
- <img>标签: 使用seller数据图片地址,v-bind绑定src属性
:src="seller.avatar"
- 文本内容: 双向数据绑定显示seller数据
{{seller.name}}
- 如果要获取的是 seller数据对象的 子对象数组的 某一项:因为异步获取数据,子对象可能为undefined,需要先 v-if 判断是否存在
<div class="support" v-if="seller.supports"> <span class="icon" :class="this.classMap[seller.supports[0].type]"></span> <span class="text">{{seller.supports[0].description}}</span> </div>
- 定义 classMap数组,通过获取seller数据中的索引值,应用对应索引的class
created (){ this.classMap = ['decrease','descount','guarantee','invoice','special'] }
<span class="icon" :class="this.classMap[seller.supports[0].type]"></span>
- mixin.styl 文件中伪函数:实现图片在不同DPR下引用不同的图片路径
bg-image($url) background-image: url($url+"@2x.png") @media (-webkit-min-device-pixel-ratio: 3),(min-device-pixel-ratio: 3) background-image: url($url+"@3x.png")
- 公告内容 —— 文字【省略号效果】
white-space: nowrap overflow: hidden text-overflow: ellipsis
- 背景图片【模糊滤镜效果】
.background position: absolute top: 0 left: 0 100% height: 100% z-index: -1 filter: blur(10px)
详情弹层页
- 实现弹出层
- v-show指令 —— 控制弹出层的显示/隐藏
<div class="detail" v-show="detailShow"></div>
data () { return { detailShow: false //通过改变数据detailShow 的true/false,控制元素的显示/隐藏 } }
- @click —— 触发点击事件,执行显示函数
<div class="bulletin-wrapper" @click="showDetail>
methods: { showDetail () { this.detailShow = true; } }
【Css Sticky footers布局】
|
Star组件抽象
- 目标:为了增强扩展性,使足够灵活
- 思路:
- v-for —— 根据分数 遍历itemClasses 显示星星样式
<div class="star" :class="starType"> <span v-for="itemClass in itemClasses" :key="itemClass.value" :class="itemClass" class="star-item"> </span> </div>
- props —— 从父组件接收两个参数:size尺寸,score分数
props:{ size: { type: Number }, score: { type: Number } }
- :class —— 绑定动态class, 在不同的调用地方, 可以设置不同的样式
@import "../../common/stylus/mixin" .star .star-item display: inline-block background-repeat: no-repeat &.star-48 .star-item 20px height: 20px margin-right: 22px background-size: 20px 20px &:last-child margin-right: 0 &.on bg-image('star48_on') &.half bg-image('star48_half') &.off bg-image('star48_off') &.star-36 .star-item 15px height: 15px margin-right: 16px background-size: 15px 15px &:last-child margin-right: 0 &.on bg-image('star36_on') &.half bg-image('star36_half') &.off bg-image('star36_off') &.star-24 .star-item 10px height: 10px margin-right: 3px background-size: 10px 10px &:last-child margin-right: 0 &.on bg-image('star24_on') &.half bg-image('star24_half') &.off bg-image('star24_off')
- computed —— 根据size 计算出动态的class;根据score push对应个数的全亮星星class;判断如果有半分或不足5分的,push进半星class和灰色星class;根据数组中对应的class显示对应的星星图片
const LENGTH = 5; const CLS_ON = 'on'; const CLS_HALF = 'half'; const CLS_OFF = 'off'; computed: { starType() { return 'star-' + this.size; //根据size 计算出动态的class }, itemClasses() { let result = []; let score = Math.floor(this.score*2)/2; let hasDecimal = score % 1 !== 0; let integar = Math.floor(score); for(let i=0; i<integar; i++){ result.push(CLS_ON) //根据score 在itemClasses中push进对应个数的全亮星星class } if(hasDecimal) { result.push(CLS_HALF);//判断如果有半分或不足5分的,push进半星class和灰色星class } while (result.length < LENGTH) { result.push(CLS_OFF) } return result; //根据itemClasses中对应的class显示对应的星星图片 } }
- 样式: 除了通用样式,还有根据不同size计算出的全部class的样式
小标题自适应线
- 避免:写死百分比,这样宽屏幕会间隔很大,窄屏幕间隔会几乎看不到
- flex布局:
<div class="title">
<div class="line"></div>
<div class="text">优惠信息</div>
<div class="line"></div>
</div>
.title
display: flex
80%
margin: 30px auto 24px auto
.line
flex: 1
position: relative
top: -6px
border-bottom: 1px solid rgba(255, 255, 255, 0.2)
.text
padding: 0 12px
font-size: 14px
【Postcss工具】 |
过渡动画组件 transition
<transition name="fade">
<div class="detail">
</transition>
.detail
opacity: 1
background: rgba(7, 17, 27, 0.8)
&.fade-enter-active, &.fade-leave-active
transition: all 0.5s ease
&.fade-enter, &.fade-leave-active
opacity: 0
background: rgba(7, 17, 27, 0)
iPhone手机背景模糊效果
backdrop-filter: blur(10px) // PC端和其它手机看不出效果
注:项目来自慕课网