cnpm i express-generator -g
Express-generator是Express的应用生成器,通过使用生成器工具,可以快速创建一个Express的应用骨架,express --view=ejs server 创建server文件夹,默认端口号为3000
消除默认样式
* {
margin: 0;
padding: 0;
}
ul {
list-style: none;
}
input {
border: none;
margin: 0;
padding: 0;
}
input:focus {
outline: none;
}
然后在main.js中导入common.css
.prettierrc配置文件
{
"trailingComma": "none",
"semi": false,
"singleQuote": true,
"arrowParens": "avoid",
"printWidth": 300,
"endOfLine": "auto"
}
flexible.js
flexible.js是淘宝开发出的一个用来适配移动端的js框架。在main.js中引入flexible.js
点击同一个路由跳转出现的问题
// 点击tabbar进行路由跳转
switchTab(item) {
if (this.$route.path === item.path) return
this.$router.replace(item.path)
}
若点击同一个路由,只要进行return即可
vue.config.js配置了@代表src目录
let path = require('path')
module.exports = {
configureWebpack: config => {
config.resolve = {
extensions: ['.js', '.json', '.vue'],
alias: {
'@': path.resolve(__dirname, './src')
}
}
}
}
ly-tab插件
具体用法参考github,我使用的是第二版本的
v-deep
::v-deep 是一个特殊的深度作用选择器,它只在scoped样式中起作用
<style scoped>
::v-deep .swiper-pagination-bullet-active {
background-color: #b0352f;
}
</style>
public文件夹
使用public文件夹下的图片时,进行v-for遍历时,img标签的src属性直接使用./开头就行
data() {
return {
swiperList: [
{
id: 1,
imgUrl: '/images/swiper1.jpeg'
},
{
id: 2,
imgUrl: '/images/swiper2.jpeg'
},
{
id: 3,
imgUrl: '/images/swiper3.jpeg'
}
],
}
cnpm i vue-awesome-swiper@3.1.3
轮播图插件
<template>
<div>
<swiper :options="mySwiperOption">
<swiper-slide v-for="(item, i) in swiperList" :key="i">
<img :src="item.imgUrl" width="100%" />
</swiper-slide>
<div class="swiper-pagination" slot="pagination"></div>
</swiper>
</div>
</template>
<script>
import { swiper, swiperSlide } from 'vue-awesome-swiper'
import 'swiper/dist/css/swiper.css'
export default {
name: 'MySwiper',
components: {
// 注册 vue-awesome-swiper 组件
swiper,
swiperSlide
},
data() {
return {
swiperList: [
{
id: 1,
imgUrl: './images/swiper1.jpeg'
},
{
id: 2,
imgUrl: './images/swiper2.jpeg'
},
{
id: 3,
imgUrl: './images/swiper3.jpeg'
}
],
mySwiperOption: {
pagination: {
el: '.swiper-pagination', //与slot="pagination"处 class 一致
clickable: true //轮播按钮支持点击
},
//自动播放
autoplay: {
delay: 1000,
disableOnInteraction: false
},
//循环
loop: true
}
}
}
}
</script>
<style scoped>
::v-deep .swiper-pagination-bullet-active {
background-color: #b0352f;
}
</style>
插槽的使用
此标题火爆推荐由插槽实现
标题的组件如下,样式省略
<template>
<div class="title">
<span>
<slot>火爆推荐</slot>
</span>
</div>
</template>
<script>
export default {
name: 'MyCard'
}
</script>
引入组件后直接在<Card>填写内容</Card>即可更换默认标题
better-scroll
better-scroll 是一款重点解决移动端(已支持 PC)各种滚动场景需求的插件,在mounted生命周期执行,具体使用查看该文档,cnpm i better-scroll。在better-scorll的使用中如果用this.$nextTick无法解决滚动问题,可以使用setTimeout解决方案,因为页面中有很多调用接口异步操作获取到的数据,因为页面渲染较快,betterscroll无法快速获取到需要滚动的内容高度。默认取消click事件和scroll事件
//最外面的盒子
.detail {
display: flex;
flex-direction: column;
width: 100vw;
height: 100vh;
overflow: hidden;
}
//beterr-scroll的wraper盒子
section {
flex: 1;
overflow: hidden;
}
Object.freeze
对请求回来不用修改的数据使用Object.freeze会提高性能
async getData() {
const res = await axios({
url: '/api/index_list/0/data/1'
})
console.log(res)
this.items = Object.freeze(res.data.data.topBar)
},
constructor
例如res.data.data.constructor !== Array判断该属性是否数组
this.$nextTick
当dom更新完在加载? this.$nextTick(()=>{ })
路由传值
/*显式 */
this.$router.push({ path:'/detail', query:{ id } })
/*隐式:*/
this.$router.push({ name:'Detail', params:{ id } })
keep-alive
keep-alive : 是一个vue的内置组件 作用:缓存组件 优势:提升性能 只要用到keep-alive会再多俩个生命周期 : activated、deactivated 。可以和路由的meta搭配,哪些组件需要缓存,哪些不需要
搜索的本地历史记录
// 搜素
goSearch() {
// 如果输入为空
if (!this.searchValue) return
// 如果没有本地存储
if (!localStorage.getItem('history')) {
// 注意[]要加字符串
localStorage.setItem('history', '[]')
} else {
this.historyList = JSON.parse(localStorage.getItem('history'))
}
// 将新输入的值存进数组
this.historyList.unshift(this.searchValue)
// 利用set去重
this.historyList = Array.from(new Set(this.historyList))
localStorage.setItem('history', JSON.stringify(this.historyList))
// 如果重复跳转到同一个路由进行return
if (this.$route.query.key === this.searchValue) return
// 跳转到搜索列表
this.$router.push({
name: 'SearchList',
query: {
key: this.searchValue
}
})
}
清空历史记录后显示
axios的二次封装(个人感觉很垃圾不推荐
在src文件夹中创建common文件夹,在common文件夹中创建api文件夹,在api文件夹中创建request.js文件
import axios from 'axios'
import { Toast } from 'vant'
export default {
// 默认参数
common: {
method: 'get',
data: {},
params: {}
},
$axios(options = {}) {
options.method = options.method || this.common.method
options.data = options.data || this.common.data
options.params = options.params || this.common.params
Toast.loading({
message: '加载中...',
forbidClick: true,
duration: 0
})
return axios(options).then(r => {
return new Promise((res, rej) => {
Toast.clear()
// 如果请求不到返回失败结果
if (!res) rej()
// 返回成功结果
res(r.data.data)
})
})
}
}
连接mysql
cnpm i mysql
node中的serve目录下创建db文件夹,在db文件夹中创建sql.js,
const mysql = require('mysql')
const connection = mysql.createConnection({
host: 'localhost',
port: '13306',
user: 'root',
password: 'xxx',
database: 'vue_tea'
})
module.exports = connection
然后在routes的index.js文件夹下引入
var express = require('express')
var router = express.Router()
// 连接数据库
var connection = require('../db/sql')
// 数据库中的good_list
router.get('/api/goods/shopList', function (req, res) {
const { search } = req.query
// 查询所有茶叶
connection.query(`select * from good_list where name like "%${search}%"`, function (err, results) {
res.send({
code: 0,
data: results
})
})
})
升序降序
vue代码
<div class="menu_sub" v-for="(v, i) in labelList.data" :key="i" @click="changeTab(i)">
<!-- 判断当前高亮的是哪项 -->
<span :class="labelList.currentIndex === i ? 'active' : ''">{{ v.title }}</span>
<div class="menu_img" v-if="v.title !== '综合'">
<img src="../../assets/images/search/上箭头.svg" alt="" :class="v.status === 1 ? 'img_active' : ''" />
<img src="../../assets/images/search/下箭头.svg" alt="" :class="v.status === 2 ? 'img_active' : ''" />
</div>
</div>
</div>
data() {
return {
// status:0都不亮 1上箭头亮 2下箭头亮
labelList: {
currentIndex: 0,
data: [
{
title: '综合',
key: 'zh'
},
{
title: '价格',
status: 0,
key: 'price'
},
{
title: '销量',
status: 0,
key: 'num'
}
]
}
}
},
methods: {
// 获取goodList数据
async getData() {
const res = await http.$axios({
url: '/api/goods/shopList',
params: {
search: this.$route.query.key,
...this.orderBy
}
})
this.goodList = res
},
// 切换选项卡
changeTab(index) {
this.labelList.currentIndex = index
this.labelList.data.forEach((v, i) => {
if (index !== i) {
v.status = 0
}
})
// 如果不等于综合
if (index != 0) {
this.labelList.data[index].status = this.labelList.data[index].status === 1 ? 2 : 1
}
// 请求数据
this.getData()
}
},
computed: {
// 升序降序
orderBy() {
const obj = this.labelList.data[this.labelList.currentIndex]
const val = obj.status === 1 ? 'asc' : 'desc'
return {
[obj.key]: val
}
}
}
node的代码
// 数据库中的good_list
router.get('/api/goods/shopList', function (req, res) {
// console.log(req.query)
const { search } = req.query
// 是综合还是价格,销量
const label = Object.keys(req.query)[1]
// 是什么排序
const order = Object.values(req.query)[1]
// 升序降序
if (label === 'zh') {
// 模糊查询指定的茶叶
connection.query(`select * from good_list where name like "%${search}%"`, function (err, results) {
res.send({
code: 0,
data: results
})
})
} else {
connection.query(`select * from good_list where name like "%${search}%" order by ${label} ${order}`, function (err, results) {
res.send({
code: 0,
data: results
})
})
}
})
安装sass
cnpm i node-sass sass-loader
better-scroll的左右联动
核心代码如下:
<div class="list_l" ref="left">
<ul>
<li v-for="(item, i) in leftList" :key="i" @click="goScroll(i)" :class="{ active: i === currentIndex }">{{ item.title }}</li>
</ul>
</div>
data() {
return {
// 左边列表数据
leftList: [],
// 右侧列表数据
rightList: [],
lBScroll: '',
rBScorll: '',
allHeight: [],
// 右侧滚动距离
scrollY: ''
}
},
methods:{
goScroll(index) {
this.rBScorll.scrollTo(0, -this.allHeight[index], 300)
}
}
mounted() {
setTimeout(() => {
this.lBScroll = new BetterScroll(this.$refs.left, {
click: true
})
this.rBScorll = new BetterScroll(this.$refs.right, {
click: true,
probeType: 3
})
//统计右侧所有板块高度值,并且放入数组中
let height = 0
this.allHeight.push(height)
//获取右侧每一块高度
let uls = this.$refs.right.getElementsByClassName('first_list')
//把dom对象转换成功真正的数组
Array.from(uls).forEach(v => {
height += v.clientHeight
this.allHeight.push(height)
})
//得到右侧滚动的值
this.rBScorll.on('scroll', pos => {
this.scrollY = Math.abs(pos.y)
})
}, 100)
},
computed: {
currentIndex() {
return this.allHeight.findIndex((item, index) => {
// 大于等于当前的高度小于下一个的高度
return this.scrollY >= item && this.scrollY < this.allHeight[index + 1]
})
}
}
导航栏头部渐变显示
有个透明度渐变的过程
// 获取dom元素
setTimeout(() => {
this.aBetterScroll = new BetterScroll(this.$refs.wraper, {
probeType: 3,
click: true,
bounce: false
})
this.aBetterScroll.on('scroll', pos => {
const posY = Math.abs(pos.y)
// 若大于50则展示后来显示的导航栏
if (posY > 50) {
this.isShow = false
// 渐变显示
this.styleOpacity.opacity = posY / 60 > 1 ? 1 : posY / 60
} else {
this.isShow = true
}
})
}, 10)
input的pattern属性
模式是正则表达式
<input type="text" placeholder="请输入手机号" pattern="[0~9]*" />
手机号密码登录
vue代码如下
data() {
return {
userTel: '',
userPwd: '',
rules: {
// 手机和密码验证
userTel: {
// 第一个1第二个是2-9后面9个是任意数字
rule: /^1[23456789]\d{9}$/,
msg: '手机号长度是11位数字'
},
userPwd: {
rule: /^\w{6,12}$/,
msg: '密码长度要求6,12位'
}
}
}
},
methods: {
// 登录按钮
async login() {
if (!this.vaildata('userTel')) return
if (!this.vaildata('userPwd')) return
const res = await http.$axios({
method: 'post',
url: '/api/login',
data: {
tel: this.userTel,
pwd: this.userPwd
}
})
console.log(res)
if (res.code === 200) {
Toast('登录成功')
} else {
Toast(res.msg)
return false
}
},
// 验证
vaildata(key) {
let bool = true
// 验证手机和密码
if (!this.rules[key].rule.test(this[key])) {
Toast(this.rules[key].msg)
bool = false
return false
}
return bool
}
}
node代码如下
const user = {
// 查询手机号
queryUserTel(v) {
return `select * from user where tel=${v}`
},
// 查询密码
queryUserPwd(v) {
// 密码传过来需要加字符串
return `select * from user where pwd='${v}'`
}
}
module.exports = user
其中queryUserPwd中传过来的v值在sql 语句中还要再加''引号,否则报错
var user = require('../db/userLogin')
// 用户密码登录
router.post('/api/login', function (req, res) {
// 查询手机号
connection.query(user.queryUserTel(req.body.tel), function (err, results) {
if (err) return err
// 手机号存在
if (results.length > 0) {
// 查询密码
connection.query(user.queryUserPwd(req.body.pwd), function (err2, results2) {
if (err2) return err2
// 密码存在
if (results2.length > 0) {
res.send({
code: 200,
data: {
code: 200,
success: true,
msg: results2[0]
}
})
// 密码错误
} else {
res.send({
code: 302,
data: {
code: 302,
success: false,
msg: '密码错误'
}
})
}
})
// 手机号不存在
} else {
res.send({
code: 301,
data: {
code: 301,
success: false,
msg: '手机号不存在'
}
})
}
})
})
短信验证码登录
cnpm i qcloudsms_js
node的腾讯云如何操作请查看GitHub - qcloudsms/qcloudsms_js: qcloudsms Node.js SDK
使用vuex存储用户信息
index.js中代码,用module进行模块化管理
import Vue from 'vue'
import Vuex from 'vuex'
import user from './modules/user.js'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
user
}
})
user.js中代码如下
export default {
state: {
// 登录状态
loginState: JSON.parse(localStorage.getItem('userInfo')) ? true : false,
userInfo: JSON.parse(localStorage.getItem('userInfo')) || {},
token: JSON.parse(localStorage.getItem('userInfo'))?.token
},
getters: {},
mutations: {
// 设置用户信息
SETUSER(state, payload) {
state.userInfo = payload
state.token = payload.token
state.loginState = true
// 持久化存储
localStorage.setItem('userInfo', JSON.stringify(payload))
},
// 退出登录
LOGOUT(state) {
localStorage.removeItem('userInfo')
state.userInfo = {}
state.token = ''
state.loginState = false
}
},
actions: {}
}
vue中主要代码如下,具体使用参照官网vuex3
import { mapMutations } from 'vuex'
...
methods: {
// 设置用户信息
...mapMutations(['SETUSER']),
}
vuex中的actions不能直接修改state中的数据,要通过mutations
jsonwebtoken
cnpm i jsonwebtoken
在db目录下的userLogin.js下对注册新用户的手机号进行加密
// 插入注册用户
insertRegister(tel, pwd) {
const jwt = require('jsonwebtoken')
const payload = tel
const secret = 'xiaoyu'
// 进行加密
const token = jwt.sign(payload, secret)
return `insert into user values(null,${tel},'${pwd}','/images/user.jpeg','${tel}','${token}')`
},
对前端传入的token进行解析
// 加入购物车
router.post('/api/addCart', function (req, res) {
const token = req.headers.token
// 解析token
const tel = jwt.decode(token)
console.log(tel)
})
axios的再次封装
在vue中传入headers:{token:true}表示传入token值
// 加入购物车
async addCart() {
const res = await http.$axios({
url: '/api/addCart',
method: 'post',
data: {
id: this.$route.query.id
},
headers: {
token: true
}
})
console.log(res)
}
在axios中,若没有登录,而传入了token:true,就会跳转到登录页面
import axios from 'axios'
import { Toast } from 'vant'
import store from '@/store'
import router from '@/router'
export default {
// 默认参数
common: {
method: 'get',
data: {},
params: {},
headers: {}
},
$axios(options = {}) {
options.method = options.method || this.common.method
options.data = options.data || this.common.data
options.params = options.params || this.common.params
options.headers = options.headers || this.common.headers
// 是否是登录状态
if (options.headers.token) {
options.headers.token = store.state.user.token
// 如果token不存在则跳转到登录页面
if (!options.headers.token) {
router.push('/login')
return false
}
}
Toast.loading({
message: '加载中...',
forbidClick: true,
duration: 0
})
return axios(options).then(r => {
return new Promise((res, rej) => {
Toast.clear()
// 如果请求不到返回失败结果
if (!res) rej()
// 返回成功结果
res(r.data.data)
})
})
}
}
购物车之全选和单选按钮
将后端返回的数据赋值给list,再创建一个selectList,通过比较这两个数组的长度是否相等来判断全选按钮,selectList中放的是每个商品的id值。单选按钮判断:点击单选按钮后,通过传过来的索引值,在list中查找与之对应的id值,并通过id值在selectList中查找与之对应的下标值,并把它删除
vuex中的cart中的代码如下
export default {
state: {
list: [],
selectList: []
},
getters: {
// 判断list和selectList的长度是否相等
isCheckedAll(state) {
return state.list.length === state.selectList.length
}
},
mutations: {
// 获取购物车数据
GETCARTLIST(state, res) {
state.list = res
// 初始化selectList,确保刚开始两数组长度是一样的
state.selectList = state.list.map(v => {
return v.id
})
},
// 全选
ALLCHECKED(state) {
// 将list中每个checked赋值为true,再返回每个商品的id值,
// 若selectList的长度等于list的长度则为全选
state.selectList = state.list.map(i => {
i.checked = true
return i.id
})
},
// 全不选
UNCHECKEDALL(state) {
state.list.forEach(v => {
v.checked = false
})
state.selectList = []
},
// 单选
SINGLECHECK(state, i) {
const id = state.list[i].id
const index = state.selectList.indexOf(id)
//能在selectList找到对应的id,就删除
if (index > -1) {
return state.selectList.splice(index, 1)
}
// 如果是未选则进行添加
state.selectList.push(id)
}
},
actions: {
checkedAll({ commit, getters }) {
// 若为true则点击后全不选
getters.isCheckedAll ? commit('UNCHECKEDALL') : commit('ALLCHECKED')
}
}
}
注意,vue中的全选按钮的v-model要换成:value,因为vuex并不是双向绑定的
购物车之总计
在getters中的代码如下,省略了其他代码
getters: {
// 总件数与总计
total(state) {
const total = {
num: 0,
price: 0
}
state.list.forEach(v => {
// 如果是选中状态
if (v.checked) {
total.num += v.goods_num
total.price += v.goods_price
}
})
return total
}
},
挂载在computed上就可以了
computed: {
...mapState({
// 购物车数据
cartList: state => state.cart.list
}),
...mapGetters(['isCheckedAll', 'total'])
}
$event参数
<van-stepper @change="changeNum($event, item)" :default-value="item.goods_num" integer />
可以通过传入$event参数来获得该组件上的一些属性
通过slot判断路由跳转
通过this.$slots.default[0].text获取插槽上的文字来判断路由的跳转,default为默认插槽的名字
goMy() {
if (this.$slots.default[0].text === '我的地址') {
this.$router.push('/my')
} else {
this.$router.push('/address')
}
}
将修改路由和添加路由放入同一个组件
通过首页的路由传值,在data中保存下该值,再通过v-if来判断显示添加地址还是修改地址
// 跳转到添加地址页面
goAdd() {
this.$router.push({
path: '/address/add',
query: {
addStaus: true
}
})
},
下载qs
cnpm i qs
qs增加数据的安全性,在axios 中使用
全局路由前置守卫
// 路由全局前置守卫
router.beforeEach((to, from, next) => {
// 需要登录权限的页面
const authRouter = ['Cart', 'Address', 'Index', 'AddAddress', 'Order']
// 解析本地存储的token
const Auth = JSON.parse(localStorage.getItem('userInfo'))
if (authRouter.indexOf(to.name) > 0 && !Auth) {
next({
name: 'Login'
})
}
next()
})
手机端查看移动端项目
npm run serve后,在手机上输入network即可
真机测试
禁止双击放大缩小使用fastclick插件