用vue2.x写一个单页购物应用
Vue全家桶项目技术栈
- 项目构建工具 vue-cli
- vue-cli 是一个脚手架工具, 为我们搭建了开发所需的环境和生成目录架构
- 路由 vue-router
- 创建单页应用, 我们的单页应用只做路由切换, 组件拼凑成的页面映射成路由
- 路由是我们单页应用的核心插件
- 状态管理 vuex
- 状态管理库, 可理解为全局数据集中地
- 推荐小项目尽量别用vuex, 会显得繁琐, bus总线机制完全可处理了
- http请求工具 axios (vue-resource官方已停止维护)
- 一个经过封装的ajax, 可以根据自己的项目情况再封装
- axios是经过了ES6的promise封装的
前后端联调必备技术之MOCK
- Mock
- 处于开发环境模拟接口返回的数据 ( 用于开发状态后端还没给接口 )
- 不会影响生产环境, 只是方便我们还没与后端交互时不阻塞开发流程
- Mock数据的好处
- 团队可以并行工作 ( 后端进度不至于影响前端开发进度 )
- 可以用来演示开发成果, 实时反馈开发进度
- 模拟测试并简单了解接口编写为全栈打基础
对比vue-cli2.x和vue-cli3.x的创建
搭建前提条件:
- node环境
- 直接官网下载安装–不断下一步
- 命令行输入 node -v 查询版本号
- node自带npm包管理工具(装好node后 输入 npm -v 查看npm版本号)
- npm太慢, 下载国内淘宝镜像 cnpm(npm install -g cnpm –registry=https://registry.npm.taobao.org)
- 安装webpack
- 运行 npm install webpack -g
- 安装vue-cli 2.x
- npm install vue-cli -g
- 创建项目: vue init webpack 项目名(不要带中文)
- 安装vue-cli 3.x
- npm install @vue/cli -g
- 创建项目: vue create 项目名(不要带中文)
不添加指定版本号就是下载最新最稳定的版本
vue-cli2.x和vue-cli3.x的项目架构
**vue-cli3.x : **
去掉了2.x build 和 config 等目录, 大部分配置都集成到 vue.congif.js里
vue.config.js(==需要自己手动创建这个文件==) 里大概包括了配置: 常用的输出路径名、跟目录、预处理、devServer配置、pwa、dll、第三方插件等等
具体配置参考(https://www.cnblogs.com/zjhr/p/9472648.html)
因为绝大部分的配置和扩展尤大大(vue作者)已经做好了封装, 常用的开发基本可以满足, 不满足的可以自己扩展
webpack的配置在这个属性里修改 configureWebpack (Mock也是在这里)
如果书写vue.config.js(开头)
module.exports = {
configureWebpack:{
devServer:{
port: 9528, //运行端口号
open: true, //启动后自动打开
//Mock数据
before(app){
}
}
}
}
UI库选择
vue是一套渐进式的框架, 设计的时候就是自底层向上逐层应用的.
PC端的ui库基本不用多考虑, ElementUI至高无上
**移动端选型关键点: **
- 能否自定义皮肤
- 是否使用rem控制尺寸, 完美适应不同分辨率移动设备
- 组件类型风格是否与自己的项目相同或类似
- 单元测试覆盖率
- 更新频率快慢
**移动端的框架有哪些: **
mint-ui(elementUI团队开发,但已停止维护)
vux
vant (https://youzan.github.io/vant/#/zh-CN/intro)
cube-ui (https://didi.github.io/cube-ui/#/zh-CN/docs/switch)
使用UI库的时候, 应多考虑他是如何实现的, 自己能否复刻一个组件出来?
安装UI框架—-cube-ui
vuecli3.x版本安装: vue add cube-ui
cube-ui官网快速上手: https://didi.github.io/cube-ui/#/zh-CN/docs/quick-start
安装cube-ui时如何配置选择?: https://github.com/cube-ui/cube-template/wiki






- 运行结束后会自动生成vue.config.js文件,并完成它自己所需的配置
如果你更改了vue.config.js文件内容, 必须重启项目才会生效
注册
-
局部修改App.vue中的代码
<div id="nav"> <router-link to="/login">登录</router-link> | <router-link to="/">注册</router-link> </div> -
在views文件下创建 Register.vue 和 Login.vue
-
编写Register.vue (引入了cube-ui中的form)
<template> <div> <cube-form :model="model" :schema="schema" @submit="submitHandler" ></cube-form> </div> </template> <script> export default { name: "CodeRegister", data() { return { model: { username: '', password: '', }, schema: { fields: [ //? 用户名配置 { type: 'input', //输入框 modelKey: 'username', //对应model中的username label: '用户名', //输入框前的内容 props: { placeholder: '请输入用户名', //提示信息 }, rules: { // 校验规则 required: true, //不为空 type: 'string', //输入类型为字符串 min: 3, //最少输入为3个字符 max: 15, //最大输入为15个字符 }, trigger: 'blur', //输入框失去焦点时进行输入检测 message: { //检测的提示内容 required: '用户名不能为空', min: '用户名不能少于3个字符', max: '用户名不能大于15个字符' } }, //? 密码配置 { type: 'input', modelKey: 'password', label: '密码', props: { placeholder: '请输入密码', type: 'password', //输入类型 eye: { //密码是否可见 open: false, } }, rules: { required: true, }, trigger: 'blur' }, //? 按钮 { type: 'submit', label: '注册' } ] } }; }, mounted() {}, methods: { submitHandler(e) { e.preventDefault() //阻止冒泡--暂时不希望提交表单而导致页面刷新 console.log('我注册了') } }, }; </script> <style lang="stylus" scoped></style> -
配置路由(局部修改router/index.js)
const routes = [ { path: '/', name: 'register', component: Register }, { path: '/login', name: 'login', // route level code-splitting // this generates a separate chunk (about.[hash].js) for this route // * which is lazy-loaded when the route is visited.懒加载 component: () => import(/* webpackChunkName: "about" */ '../views/Login.vue') } ] -
在vue.config.js中编写webpack扩展配置( 用于MOCK )( 每次更改这个配置文件后, 都必须重启项目才会生效 )
在module.exports中增加配置:
//webpack扩展配置 configureWebpack:{ devServer:{ port: 9528, //运行端口号 open: true, //启动后自动打开 //Mock接口编写的地方 before(app){//参数随便写,但这是基于Express编写的,所以一般写app //? 示例 // app.get('请求地址', (req, res) => { // res.json({ // }) // }) //? 用户信息池 let userPoor = [ {username: 'xiaodi', password: '123456'}, {username: 'tim', password: '123456'} ] //? 注册接口 app.get('/api/register', (req, res) => { //获取传来的数据 const {username, password} = req.query //查询username是否已存在 const userLength = userPoor.filter(v => v.username == username).length //如果userLength>0则说明用户名已存在 if(userLength > 0){ res.json({ success: false, message: '用户名已存在' }) } else{ res.json({ success: true, message: '注册成功' }) } }) } } }, - 配置axios
- 安装: cnpm install axios
- 在main.js中:
- 引入
import axios from 'axios' - 挂载到全局对象上
Vue.prototype.$http = axios
- 引入
-
请求
局部修改Register.vue
methods: { submitHandler(e) { e.preventDefault() //阻止冒泡--暂时不希望提交表单而导致页面刷新 this.$http.get('/api/register', {params: this.model}).then(res => { console.log(res.data.success) }).catch(err => { console.log(err) }) } }, -
测试
重新启动项目
在浏览器->网络->Fetch/XHR下查看请求

登录
-
编写Login.vue (基本与Register相同,直接复制过来稍作修改)
<template> <div> <img class="headerimg" src="https://file.xdclass.net/video/2020-12%20%E5%AE%98%E7%BD%91%E8%B6%85100k%E4%B8%BB%E5%9B%BE%E6%9B%B4%E6%96%B0/lbt/alibabacloud.jpg" alt=""> <cube-form :model="model" :schema="schema" @submit="submitHandler" ></cube-form> </div> </template> <script> export default { name: "CodeRegister", data() { return { model: { username: '', password: '', }, schema: { fields: [ //? 用户名配置 { type: 'input', //输入框 modelKey: 'username', //对应model中的username label: '用户名', //输入框前的内容 props: { placeholder: '请输入用户名', //提示信息 }, rules: { // 校验规则 required: true, //不为空 type: 'string', //输入类型为字符串 min: 3, //最少输入为3个字符 max: 15, //最大输入为15个字符 }, trigger: 'blur', //输入框失去焦点时进行输入检测 message: { //检测的提示内容 required: '用户名不能为空', min: '用户名不能少于3个字符', max: '用户名不能大于15个字符' } }, //? 密码配置 { type: 'input', modelKey: 'password', label: '密码', props: { placeholder: '请输入密码', type: 'password', //输入类型 eye: { //密码是否可见 open: false, } }, rules: { required: true, }, trigger: 'blur' }, //? 按钮 { type: 'submit', label: '登录' } ] } }; }, mounted() {}, methods: { submitHandler(e) { } }, }; </script> // 留意一下 stylus 的写法 <style lang="stylus" scoped> .headerimg height 150px width 100% </style> -
局部调整路由
//引入Login import Login from '../views/Login.vue' //调整路径 const routes = [ { path: '/', name: 'register', redirect: '/login' //重定向 }, { path: '/register', name: 'register', component: Register }, { path: '/login', name: 'login', // route level code-splitting // this generates a separate chunk (about.[hash].js) for this route // * which is lazy-loaded when the route is visited.懒加载 component: () => import(/* webpackChunkName: "about" */ '../views/Login.vue') } ] -
局部修改App.vue
<div id="nav"> <router-link to="/login">登录</router-link> | <router-link to="/register">注册</router-link> </div> -
编写登录Mock
在vue.config.js中的before(app)下新增:
//? 模拟的token_key let tokenKey = 'xdclass' //? 登录接口 app.get('/api/login', (req, res) => { const {username, password} = req.body //验证是否输入正确(模拟) if(username == 'xiaodi' && password == '123456' || username == 'tim' && password == '123456'){ //这里仅仅返回一个模拟的token res.json({ code: 0, message: '登陆成功', token: tokenKey + '-' + username + '-' + (new Date().getTime() + 60*60*1000) }) } else{ res.json({ code: 1, message: '账号或密码错误' }) } }) -
在vuex中编写存储token方法(局部修改)
export default new Vuex.Store({ state: { //* 变量存储库 token: '' }, mutations: { //* 同步的方法---通过this.$store.commit('setToken', 参数)调用 //设置vuex的token setToken(state, token){ state.token = token } }, actions: { //* 异步的方法---通过this.$store.dispatch('方法名', 参数)调用 }, modules: { }, getters: { //* 相当于vue里的计算属性 } }) -
在Login.vue中编写请求
async submitHandler(e) { e.preventDefault() try{ const result = await this.$http.get('/api/login', {params: this.model}) if(result.data.code == '0'){ //将token存进vuex this.$store.commit('setToken', result.data.token); //本地存储token window.localStorage.setItem('token', result.data.token) } else{ alert(result.data.message) } } catch(err){ console.log(err) } }
http全局拦截
-
在src下新建文件: setAxios.js
import axios from 'axios' import store from './store/index' import router from './router/index' //? http全局拦截 // token要放在外面请求的header上面给后端带回去 export default function setAxios() { //路由拦截 --写法基本固定 axios.interceptors.request.use( config => { if(store.state.token){ config.headers.token = store.state.token } return config } ) //每次的请求,有返回的, 都是先经过这个拦截器 axios.interceptors.response.use( response => { //当返回http状态码为200时,做的处理 if(response.status == 200){ //todo 剥开一层, 这样在其他地方就可以省略 .data const data = response.data if(data.code == -1){ //登录过期,徐重新登录, 情况vuex和localStorage里的token store.commit('setToken','') localStorage.removeItem('token') //跳转到login页面,但是不希望用户之后点击返回回退到login,所以使用replace(replace不会向history里插入记录) router.replace({path: '/login'}) } //记得返回 return data } return response } ) } -
在main.js中引入setAxios.js
import setAxios from './setAxios' setAxios() //因为./setAxios.js 暴露的是方法, 所以直接执行就好了 -
更改Register.vue和Login.vue
//因为在setAxios里将response.data剥层了, 所以其他地方可以省略 .data //? Rgister: methods: { submitHandler(e) { e.preventDefault() //阻止冒泡--暂时不希望提交表单而导致页面刷新 this.$http.get('/api/register', {params: this.model}).then(res => { console.log(res.success) }).catch(err => { console.log(err) }) } }, //? Login: //新写法 async与await配合使用 async submitHandler(e) { e.preventDefault() try{ const result = await this.$http.get('/api/login', {params: this.model}) if(result.code == '0'){ //将token存进vuex this.$store.commit('setToken', result.token); //本地存储token window.localStorage.setItem('token', result.token) } else{ alert(result.message) } } catch(err){ console.log(err) } }
首页轮播图
-
views目录下新建 Index.vue (使用了cube-ui下的轮播图Slide)
<template> <div id="index"> <cube-slide ref="slide" :data="items" @change="changePage"> <cube-slide-item v-for="(item, index) in items" :key="index" @click.native="clickHandler(item, index)" > <a :href="item.url"> <img class="banner" :src="item.image" /> </a> </cube-slide-item> </cube-slide> </div> </template> <script> export default { data() { return { items: [] } }, methods: { changePage(current) { console.log('当前轮播图序号为:' + current) }, clickHandler(item, index) { console.log(item, index) } }, async created() { try{ //获取轮播图数据 const items = await this.$http.get('/api/banner') this.items = items.data } catch(err) { console.log(err) } }, } </script> <style lang="stylus" scoped> #index a .banner display block width 100% height 175px </style> -
配置路由(router/index.js)
{ path: '/index', name: 'index', component: () => import('../views/Index.vue') }, -
编写轮播图Mock数据
//? 首页轮播图数据接口 app.get('/api/banner', (req, res) => { res.json({ data: [{ url: 'https://m.xdclass.net', image: 'https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/bannner/1901/learn.png' }, { url: 'https://m.xdclass.net', image: 'https://file.xdclass.net/video/2021/aliyun/09.jpeg' }, { url: 'https://m.xdclass.net', image: 'https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/bannner/1901/bat.png' }] }) })
滚动分类
-
在Index.vue中的轮播图代码下新增:
<!-- 滚动分类 --> <cube-slide ref="slidelists" :loop="false" :data="lists"> <cube-slide-item v-for="(list, index) in lists" :key="index" > <ul class="listul"> <li class="listli" v-for="(item,index1) in list" :key="index1"> <!-- 滚动分类 --loop为false是关闭自动滚动--> <a :href="item.url"> <img :src="item.image" alt=""> <p></p> </a> </li> </ul> </cube-slide-item> </cube-slide> -
在Index.vue中新增:
//在data中新增: lists: [], // 滚动分类数组 //created中的获取轮播图数据代码下新增: //获取滚动分类数据 const lists = await this.$http.get('/api/rollinglist') this.lists = lists.data -
修改Index.vue中的css部分:
<style lang="stylus" scoped> #index a .banner display block width 100% height 175px .listul display flex flex-wrap wrap .listli width 20% justify-content center img width 35px height 35px border-radius 50% padding 5px 0 p font-size 14px padding-bottom 10px </style> -
编写滚动分类的Mock数据
//? 滚动分类接口 app.get("/api/rollinglist", (req, res) => { res.json({ data: [ [ { url: "https://m.xdclass.net", image: "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/bannner/1901/learn.png", label: "分类一", }, { url: "https://m.xdclass.net", image: "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/bannner/1901/learn.png", label: "分类一", }, { url: "https://m.xdclass.net", image: "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/bannner/1901/learn.png", label: "分类一", }, { url: "https://m.xdclass.net", image: "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/bannner/1901/learn.png", label: "分类一", }, { url: "https://m.xdclass.net", image: "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/bannner/1901/learn.png", label: "分类一", }, { url: "https://m.xdclass.net", image: "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/bannner/1901/learn.png", label: "分类一", }, { url: "https://m.xdclass.net", image: "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/bannner/1901/learn.png", label: "分类一", }, { url: "https://m.xdclass.net", image: "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/bannner/1901/learn.png", label: "分类一", }, { url: "https://m.xdclass.net", image: "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/bannner/1901/learn.png", label: "分类一", }, { url: "https://m.xdclass.net", image: "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/bannner/1901/learn.png", label: "分类一", }, ], [ { url: "https://m.xdclass.net", image: "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/bannner/1901/bat.png", label: "分类一", }, { url: "https://m.xdclass.net", image: "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/bannner/1901/bat.png", label: "分类一", }, { url: "https://m.xdclass.net", image: "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/bannner/1901/bat.png", label: "分类一", }, { url: "https://m.xdclass.net", image: "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/bannner/1901/bat.png", label: "分类一", }, { url: "https://m.xdclass.net", image: "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/bannner/1901/bat.png", label: "分类一", }, { url: "https://m.xdclass.net", image: "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/bannner/1901/bat.png", label: "分类一", }, { url: "https://m.xdclass.net", image: "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/bannner/1901/bat.png", label: "分类一", }, { url: "https://m.xdclass.net", image: "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/bannner/1901/bat.png", label: "分类一", }, { url: "https://m.xdclass.net", image: "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/bannner/1901/bat.png", label: "分类一", }, { url: "https://m.xdclass.net", image: "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/bannner/1901/bat.png", label: "分类一", }, ], ], }); });
创建底部导航栏
-
views目录下新建 BotNav.vue
<template> <div> <!-- 因为这里用到了二级路由,需要跳转,所以要写router-view为路由留坑 --> <router-view></router-view> <!-- 使用cube-ui中的 TabBar --> <cube-tab-bar v-model="selectedLabelDefault" :data="tabs" @click="clickHandler" @change="changeHandler" class="botNav" > </cube-tab-bar> </div> </template> <script> export default { data() { return { selectedLabelDefault: "Vip", tabs: [ { label: "首页", icon: "cubeic-home", }, { label: "分类", icon: "cubeic-tag", }, { label: "搜索", icon: "cubeic-search", }, { label: "购物车", icon: "cubeic-mall", }, { label: "我的", icon: "cubeic-person", }, ], }; }, methods: { clickHandler(label) { // if you clicked home tab, then print 'Home' console.log(label); }, //点击与自身不同的其他导航 changeHandler(label) { // if you clicked different tab, this methods can be emitted }, }, }; </script> <style lang="stylus" scoped> .cube-tab-bar.botNav { position: fixed; bottom: 0; left: 0; z-index: 1000; width: 100%; background: #fff; .cube-tab div { font-size: 16px; padding-top: 3px; } i { font-size: 20px; } } </style> -
更改App.vue
//注释掉这两个跳转(太丑) <!-- <router-link to="/login">登录</router-link> | <router-link to="/register">注册</router-link> --> //注释掉这个样式 // margin-top 60px -
修改Login.vue
//在submitHandler方法中加上页面跳转, 最终这个函数是这个样子: //新写法 async与await配合使用 async submitHandler(e) { e.preventDefault() try{ const result = await this.$http.get('/api/login', {params: this.model}) if(result.code == '0'){ //将token存进vuex this.$store.commit('setToken', result.token); //本地存储token window.localStorage.setItem('token', result.token) //防止用户点击返回时返回到登录页,使用replace跳转 this.$router.replace({path: '/index'}) } else{ alert(result.message) } } catch(err){ console.log(err) } } -
为新建的BotNav.vue配置路由
{ path: '/botNav', name: 'botNav', component: () => import('../views/BotNav.vue') },
配置嵌套子路由
-
在views目录下新建 List.vue Search.vue Cart.vue Mine.vue 并写入简单的内容
-
在router/index.js文件的 /botNav路由中新增children属性:
{ path: '/botNav', name: 'botNav', component: () => import('../views/BotNav.vue'), children: [ //! 二级路由path不需要加 / { path: 'index', name: 'index', component: () => import('../views/Index.vue') }, { path: 'list', name: 'list', component: () => import('../views/List.vue') }, { path: 'search', name: 'search', component: () => import('../views/Search.vue') }, { path: 'cart', name: 'cart', component: () => import('../views/Cart.vue') }, { path: 'mine', name: 'mine', component: () => import('../views/Mine.vue') }, ] }, -
检查BotNav.vue中是否为路由留坑
//确保template中有 <router-view></router-view> -
修改BonNav.vue中data中的数据:
data() { return { selectedLabelDefault: "首页", //默认选择 tabs: [ { label: "首页", icon: "cubeic-home", }, { label: "分类", icon: "cubeic-tag", }, { label: "搜索", icon: "cubeic-search", }, { label: "购物车", icon: "cubeic-mall", }, { label: "我的", icon: "cubeic-person", }, ], }; }, -
编写BotNav.vue中点击跳转函数:
//点击与自身不同的其他导航 changeHandler(label) { // if you clicked different tab, this methods can be emitted //todo label就是data里tab下面的label,以此来标识点击不同导航 switch (label) { case '首页': this.$router.push({path: '/botNav/index'}); break; case '分类': this.$router.push({path: '/botNav/list'}); break; case '搜索': this.$router.push({path: '/botNav/search'}); break; case '购物车': this.$router.push({path: '/botNav/cart'}); break; case '我的': this.$router.push({path: '/botNav/mine'}); break; default: break; } },
使用transition为路由切换增加过渡效果, 提高用户体验
-
使用方法: 到vue官网API搜索transition
-
在BotNav.vue中:
//使用transition标签包裹router-view <transition :name="transitionName"> <!-- 使用transition包裹router-view可以在切换路由时附带过渡效果 --> <router-view class="Router"></router-view> </transition> -
在BotNav.vue文件的data中配置刚刚使用的变量
transitionName: 'slide-right', //class变量名,初始为右滑动,根据条件改为左滑动 -
根据官网API, 为不同实机编写css动画
//! 书写stylus务必注意: 必须严格对齐! .cube-tab-bar.botNav position fixed bottom 0 left 0 z-index 1000 width 100% background #fff .cube-tab div font-size 16px padding-top 3px i font-size 20px .Router position absolute width 100% transition all 0.8s ease .slide-left-enter,.slide-right-leave-active opacity 0 -webkit-transform translate(100%,0) transform translate(100%,0) .slide-left-leave-active,.slide-right-enter opacity 0 -webkit-transform translate(-100%,0) transform translate(-100%,0) // 这些class名都是被vue解析出来的,在浏览器里可以看到,我们需要为这些状态类添加动画效果
重新设置登录跳转页面
-
因为我们已经设置好了带底部导航栏的首页, 所以需要重新设置登录跳转页
//更改Login.vue中的登录跳转 //防止用户点击返回时返回到登录页,使用replace跳转 this.$router.replace({path: '/botNav/index'})
分类页编写
-
编写List.vue (使用cube-ui 中的Scroll )
<template> <!-- //todo 在属性前加 : 是动态绑定 --> <div class="panelsbox"> <!-- 使用cube-ui中的滚动盒子 --> <cube-scroll class="leftpanels"> <ul> <!-- 当选中是设置样式为active,未选中样式为空 --> <!-- //? key的作用是起到唯一标识的作用,以为index是惟一的,v-for与key一同出现. key可起到优化作用 --> <li v-for="(list, index) in tabslabel" @click="selectlist(index)" :class="list.active ? 'active' : ''" :key="index" > </li> </ul> </cube-scroll> <cube-scroll class="rightpanels"> <ul> <li v-for="(tag, index) in tags" :key="index"> <img :src="tag.image" alt=""> <p></p> </li> </ul> </cube-scroll> </div> </template> <script> export default { name: "CodeList", data() { return { tags: [], //右侧内容 tabslabel: [ //左侧内容 // 如果有相应的接口就直接从接口拿 { label: '热门推荐', active: true }, { label: '手机数码', active: false }, { label: '家用电器', active: false }, { label: '电脑产品', active: false }, { label: '手机数码', active: false }, { label: '口红', active: false }, { label: '美妆护肤', active: false }, { label: '美妆护肤', active: false }, { label: '美妆护肤', active: false }, { label: '美妆护肤', active: false }, { label: '手机数码', active: false }, { label: '手袋', active: false }, { label: '手机数码', active: false }, { label: '手袋', active: false }, ] }; }, created() { //获取默认的分类数据 this.getclassify(0) }, mounted() {}, //todo 尽量把可复用的内容都写好 methods: { //点击左侧分类 selectlist(index) { // forEach回调函数接收1~3个参数, 第一参数就是数组中正在处理的当前元素,第二个参数为数组中正在处理的当前元素的索引 this.tabslabel.forEach( (val,ind) => { // 如果当前元素的索引等于所点击元素的索引 if(index == ind) { val.active = true } else{ val.active = false } }) //请求所点击元素对应的数据 this.getclassify(index) }, //获取分类数据 async getclassify(index) { const result = await this.$http.get('/api/classify', {params : {type : index} }) this.tags = result.data } }, }; </script> <style lang="stylus" scoped> .panelsbox display flex .leftpanels width 30% li height 50px line-height 50px border-bottom 1px solid #fff color #333 background #f8f8f8 font-size 14px .active background #fff color #e93b3d .rightpanels width 70% ul display flex flex-wrap wrap li width 50% justify-content center align-items center font-size 15px img width 80px height 80px </style> -
编写获取分类页的Mock接口数据
//?获取分类页的分类接口 app.get("/api/classify", (req, res) => { switch (req.query.type) { case "0": res.json({ data: [ { image: "//img30.360buyimg.com/focus/s140x140_jfs/t13411/188/926813276/3945/a4f47292/5a1692eeN105a64b4.png", label: "小米", }, { image: "//img14.360buyimg.com/focus/s140x140_jfs/t11929/135/2372293765/1396/e103ec31/5a1692e2Nbea6e136.jpg", label: "华为", }, { image: "//img10.360buyimg.com/focus/s140x140_jfs/t12178/348/911080073/4732/db0ad9c7/5a1692e2N6df7c609.jpg", label: "荣耀", }, { image: "//img20.360buyimg.com/focus/s140x140_jfs/t13759/194/897734755/2493/1305d4c4/5a1692ebN8ae73077.jpg", label: "雪梨手机", }, { image: "//img30.360buyimg.com/focus/s140x140_jfs/t13411/188/926813276/3945/a4f47292/5a1692eeN105a64b4.png", label: "小米", }, { image: "//img14.360buyimg.com/focus/s140x140_jfs/t11929/135/2372293765/1396/e103ec31/5a1692e2Nbea6e136.jpg", label: "华为", }, { image: "//img10.360buyimg.com/focus/s140x140_jfs/t12178/348/911080073/4732/db0ad9c7/5a1692e2N6df7c609.jpg", label: "荣耀", }, { image: "//img20.360buyimg.com/focus/s140x140_jfs/t13759/194/897734755/2493/1305d4c4/5a1692ebN8ae73077.jpg", label: "雪梨手机", }, { image: "//img30.360buyimg.com/focus/s140x140_jfs/t13411/188/926813276/3945/a4f47292/5a1692eeN105a64b4.png", label: "小米", }, { image: "//img14.360buyimg.com/focus/s140x140_jfs/t11929/135/2372293765/1396/e103ec31/5a1692e2Nbea6e136.jpg", label: "华为", }, { image: "//img10.360buyimg.com/focus/s140x140_jfs/t12178/348/911080073/4732/db0ad9c7/5a1692e2N6df7c609.jpg", label: "荣耀", }, { image: "//img20.360buyimg.com/focus/s140x140_jfs/t13759/194/897734755/2493/1305d4c4/5a1692ebN8ae73077.jpg", label: "雪梨手机", }, { image: "//img30.360buyimg.com/focus/s140x140_jfs/t13411/188/926813276/3945/a4f47292/5a1692eeN105a64b4.png", label: "小米", }, { image: "//img14.360buyimg.com/focus/s140x140_jfs/t11929/135/2372293765/1396/e103ec31/5a1692e2Nbea6e136.jpg", label: "华为", }, { image: "//img10.360buyimg.com/focus/s140x140_jfs/t12178/348/911080073/4732/db0ad9c7/5a1692e2N6df7c609.jpg", label: "荣耀", }, { image: "//img20.360buyimg.com/focus/s140x140_jfs/t13759/194/897734755/2493/1305d4c4/5a1692ebN8ae73077.jpg", label: "雪梨手机", }, { image: "//img30.360buyimg.com/focus/s140x140_jfs/t13411/188/926813276/3945/a4f47292/5a1692eeN105a64b4.png", label: "小米", }, { image: "//img14.360buyimg.com/focus/s140x140_jfs/t11929/135/2372293765/1396/e103ec31/5a1692e2Nbea6e136.jpg", label: "华为", }, { image: "//img10.360buyimg.com/focus/s140x140_jfs/t12178/348/911080073/4732/db0ad9c7/5a1692e2N6df7c609.jpg", label: "荣耀", }, { image: "//img20.360buyimg.com/focus/s140x140_jfs/t13759/194/897734755/2493/1305d4c4/5a1692ebN8ae73077.jpg", label: "雪梨手机", }, ], }); break; case "1": res.json({ data: [ { image: "//img30.360buyimg.com/focus/s140x140_jfs/t13411/188/926813276/3945/a4f47292/5a1692eeN105a64b4.png", label: "小米", }, { image: "//img30.360buyimg.com/focus/s140x140_jfs/t13411/188/926813276/3945/a4f47292/5a1692eeN105a64b4.png", label: "小米", }, { image: "//img30.360buyimg.com/focus/s140x140_jfs/t13411/188/926813276/3945/a4f47292/5a1692eeN105a64b4.png", label: "小米", }, { image: "//img30.360buyimg.com/focus/s140x140_jfs/t13411/188/926813276/3945/a4f47292/5a1692eeN105a64b4.png", label: "小米", }, ], }); break; case "2": res.json({ data: [ { image: "//img14.360buyimg.com/focus/s140x140_jfs/t11929/135/2372293765/1396/e103ec31/5a1692e2Nbea6e136.jpg", label: "华为", }, { image: "//img14.360buyimg.com/focus/s140x140_jfs/t11929/135/2372293765/1396/e103ec31/5a1692e2Nbea6e136.jpg", label: "华为", }, { image: "//img14.360buyimg.com/focus/s140x140_jfs/t11929/135/2372293765/1396/e103ec31/5a1692e2Nbea6e136.jpg", label: "华为", }, { image: "//img14.360buyimg.com/focus/s140x140_jfs/t11929/135/2372293765/1396/e103ec31/5a1692e2Nbea6e136.jpg", label: "华为", }, ], }); break; case "3": res.json({ data: [ { image: "//img10.360buyimg.com/focus/s140x140_jfs/t12178/348/911080073/4732/db0ad9c7/5a1692e2N6df7c609.jpg", label: "荣耀", }, { image: "//img10.360buyimg.com/focus/s140x140_jfs/t12178/348/911080073/4732/db0ad9c7/5a1692e2N6df7c609.jpg", label: "荣耀", }, { image: "//img10.360buyimg.com/focus/s140x140_jfs/t12178/348/911080073/4732/db0ad9c7/5a1692e2N6df7c609.jpg", label: "荣耀", }, { image: "//img10.360buyimg.com/focus/s140x140_jfs/t12178/348/911080073/4732/db0ad9c7/5a1692e2N6df7c609.jpg", label: "荣耀", }, ], }); break; case "4": res.json({ data: [ { image: "//img30.360buyimg.com/focus/s140x140_jfs/t13411/188/926813276/3945/a4f47292/5a1692eeN105a64b4.png", label: "小米", }, { image: "//img30.360buyimg.com/focus/s140x140_jfs/t13411/188/926813276/3945/a4f47292/5a1692eeN105a64b4.png", label: "小米", }, { image: "//img30.360buyimg.com/focus/s140x140_jfs/t13411/188/926813276/3945/a4f47292/5a1692eeN105a64b4.png", label: "小米", }, { image: "//img30.360buyimg.com/focus/s140x140_jfs/t13411/188/926813276/3945/a4f47292/5a1692eeN105a64b4.png", label: "小米", }, ], }); break; case "5": res.json({ data: [ { image: "//img20.360buyimg.com/focus/s140x140_jfs/t13759/194/897734755/2493/1305d4c4/5a1692ebN8ae73077.jpg", label: "雪梨手机", }, { image: "//img20.360buyimg.com/focus/s140x140_jfs/t13759/194/897734755/2493/1305d4c4/5a1692ebN8ae73077.jpg", label: "雪梨手机", }, { image: "//img20.360buyimg.com/focus/s140x140_jfs/t13759/194/897734755/2493/1305d4c4/5a1692ebN8ae73077.jpg", label: "雪梨手机", }, { image: "//img20.360buyimg.com/focus/s140x140_jfs/t13759/194/897734755/2493/1305d4c4/5a1692ebN8ae73077.jpg", label: "雪梨手机", }, ], }); break; case "6": res.json({ data: [ { image: "//img30.360buyimg.com/focus/s140x140_jfs/t13411/188/926813276/3945/a4f47292/5a1692eeN105a64b4.png", label: "小米", }, { image: "//img30.360buyimg.com/focus/s140x140_jfs/t13411/188/926813276/3945/a4f47292/5a1692eeN105a64b4.png", label: "小米", }, { image: "//img30.360buyimg.com/focus/s140x140_jfs/t13411/188/926813276/3945/a4f47292/5a1692eeN105a64b4.png", label: "小米", }, { image: "//img30.360buyimg.com/focus/s140x140_jfs/t13411/188/926813276/3945/a4f47292/5a1692eeN105a64b4.png", label: "小米", }, ], }); break; } }); -
设置滚动盒子高度 (在List.vue文件中的mounted生命周期函数下设置高度)
mounted() { // 设置滚动盒子的高度 const leftpanels = document.querySelector('.leftpanels') //返回文档中与指定选择器或选择器组匹配的第一个 HTMLElement 对象。 const rightpanels = document.querySelector('.rightpanels') const bodyheight = document.documentElement.clientHeight //可视窗口的高度 leftpanels.style.height = bodyheight - 57 + 'px' rightpanels.style.height = bodyheight - 57 + 'px' },
发现bug, 命名在BotNav中设置了底部导航栏的样式, 但为什么没有生效?
//删掉BotNav.vue中style标签中的scoped即可
<style lang="stylus">
//! scoped标志的意思是当前样式仅在当前作用域生效,当路由不与当前页面的路由匹配时,当前css失效
登录权限即路由拦截的使用
-
router/index.js配置中, 路由配置还有一个属性meta, 在它里面可以自定义一些属性
//? 改写路由配置: { path: 'cart', name: 'cart', meta: { requireAuth: true,//当有这个字段的时候, 我们就认为这个路由是需要有登录权限的 }, component: () => import('../views/Cart.vue') }, { path: 'mine', name: 'mine', meta: { requireAuth: true,//当有这个字段的时候, 我们就认为这个路由是需要有登录权限的 }, component: () => import('../views/Mine.vue') }, -
在main.js中完成路由守卫的编写:
//? 路由守卫 router.beforeEach((to, from, next) => { //! 无论是刷新还是跳转路由, 第一个进入的就是这个路由前置钩子函数 //从本地存储里读取token并存入vuex store.commit('setToken',localStorage.getItem('token')) if(to.meta.requireAuth) { //如果目的路由的requireAuth为true(如果没有这个属性,那当然是空,也就是false) if(store.state.token) { //如果token存在,则说明已登录 next() //允许跳转 } else{ next({ path: '/login', query: {redirect: to.fullPath} //参数, redirect的意思是重定向,to.fullPath是目的路由(to就是目的路由,to.fullPath就是目的路由的完整url路径,以便用户登录后直接进入目的路由而不是默认的首页) }) } } else{ next() //继续执行, 不写next则不会执行下去 } }) -
修改login中跳转的代码:
//因为登录后默认是跳转到首页, 但如果是用户点击'我的',由于没有token被重定向到登录页, 在用户登录后肯定是希望直接前往'我的'而不是'首页' //判断路由是否带参, 带参数就去到重定向参数地址,否则就去首页 if(this.$route.query.redirect) { //获取路由中的参数 //去往参数中的路由 this.$router.replace({ path: this.$route.query.redirect }) } else{ //防止用户点击返回时返回到登录页,使用replace跳转 this.$router.replace({path: '/botNav/index'}) } -
我们发现BotNav导航的选中与实际页面不符, 这个bug是由于默认选中项导致的, 在created生命周期函数中将默认选中与路由保持一致即可
created() { //根据路由设置不同的默认选择项(路由是我的,就默认选中我的. 路由是购物车就默认选中购物车) // this.$route.path就是类似这样的: '/botNav/index' this.$route.fullPath就是类似这样的: http://localhost:9529/login?redirect=%2FbotNav%2Fcart switch(this.$route.path) { case '/botNav/index': this.selectedLabelDefault = '首页' break; case '/botNav/list': this.selectedLabelDefault = '分类' break; case '/botNav/search': this.selectedLabelDefault = '搜索' break; case '/botNav/cart': this.selectedLabelDefault = '购物车' break; case '/botNav/mine': this.selectedLabelDefault = '我的' break; } },
编写购物车页面
-
编写Cart.vue
<template> <div> <div class="goods" v-for="(item,index) in cartarr" :key="index"> <div class="goodsright"> <i class="cubeic-remove" @click="removeCart(index)"></i> <span></span> <i class="cubeic-add" @click="addCart(index)"></i> </div> </div> <cube-button style="margin: 10px 0;">下单</cube-button> <cube-button>清空购物车</cube-button> </div> </template> <script> export default { name: 'CodeCart', data() { return { cartarr:[ { title: '小米手机', cartCount: 5 }, { title: '华为手机', cartCount: 6 } ] }; }, mounted() { }, methods: { //减少商品 removeCart(index) { }, //增加商品 addCart(index) { } }, }; </script> <style lang="stylus" scoped> .goods padding 10px text-align left .goodsright float right i font-size 18px </style> -
在store/index.js编写添加商品并储存到vuex的方法:
//在变量存储库state中新增变量 state: { //* 变量存储库 token: '', cartArray: [], //存储购物车商品的数据 }, //在mutations中新增方法 //添加商品到购物车 toCart(state, tag) { //遍历cartArray, 如果在cartArray里发现了和tag.label(List页里的商品名)的title(Cart页的商品名),则说明商品以已经在购物车里了 let goods = state.cartArray.find(v => v.title == tag.label) if(goods) { goods.cartCount += 1 } else{ //商品不再购物车里则将商品加入购物车 state.cartArray.push({title: tag.label, cartCount: 1}) } } -
在views/List.vue中新增一个按钮( i标签 )(用于点击添加商品到购物车)
<li v-for="(tag, index) in tags" :key="index"> <img :src="tag.image" alt=""> <!-- addtocart($event, tag) $event用于制作动画 --> <p> <i class="cubeic-add" @click="addtocart($event, tag)"></i></p> </li> -
在views/List.vue中完成上面的addtocart方法
//添加商品到购物车 addtocart(e, tag) { this.$store.commit('toCart', tag); } //这里用到了刚刚store中定义的方法 -
修改Cart.vue
//1. 删除data中的cartarr:[] //2. 使用vuex封装好的方法获取vuex中数据: //引入 import {mapState} from 'vuex' //? 使用计算属性 computed: { //todo 使用vuex封装好的可以快速获得vuex中数据的方法获得数据 ...mapState({ cartarr: state => state.cartArray }) },
实时在底部导航栏显示购物车中商品总数
-
在BotNav.vue中的template里的cube-tab-bar标签下面添加:
<span class="countsum"></span> -
为刚刚添加的span编写样式
.countsum position fixed bottom 33px right 23% z-index 1001 width 18px height 18px line-height 18px border-radius 50% font-size 14px background red color #fff -
在store/index.js中添加:
getters: { //* 相当于vue里的计算属性computed //计算购物车中商品总数 countsum: state => { let num = 0 state.cartArray.forEach(v => { num += v.cartCount }) return num } } -
BotNav.vue中:
//引入 import {mapGetters} from 'vuex' //从vuex中获得购物车中商品总数 //计算属性 computed: { //获得购物车中商品总数 ...mapGetters({ countsum:'countsum' // 上面用了简写, 完整的写法应该是: countsum:'countsum' // -----ES6新特性: 键和值相等时仅需写一个 }) },
实现购物车内部的逻辑
-
在store/index.js的mutations中新增方法:
//购物车商品数量加一 cartAdd(state, index) { state.cartArray[index].cartCount ++ }, //购物车商品数量减一 cartRemove(state, index) { if(state.cartArray[index].cartCount > 1) { state.cartArray[index].cartCount -- } else{ if(window.confirm('确定从购物车移除商品吗?')){ state.cartArray.splice(index,1) } } }, //清空购物车 clearCart(state) { state.cartArray = [] } -
在Cart.vue中为 ‘清空购物车’ 按钮添加点击事件
<cube-button @click="clearCart">清空购物车</cube-button> -
在Cart.vue中实现购物车商品加一, 减一以及清空购物车的方法
methods: { //减少商品 removeCart(index) { this.$store.commit('cartRemove', index) }, //增加商品 addCart(index) { this.$store.commit('cartAdd', index) }, //清空购物车 clearCart() { this.$store.commit('clearCart') } },
实现数据持久化(页面刷新后,vuex中的数据会丢失, 我们需要将数据持久化)
-
对store/index.js做一点小小的改造
// ... let store = new Vuex.Store({ ... }) export default store -
在 store/index.js 中新增:
//监听每次调用mutations的时候,都会进这个方法, 然后我们可以做一下自己的操作 store.subscribe((mutations, state) => { //进行本地存储 localStorage.setItem('cartArray', JSON.stringify(state.cartArray)) }) -
修改store/index.js的 state部分
state: { //* 变量存储库 token: '', cartArray: JSON.parse(localStorage.getItem('cartArray')) || [], //存储购物车商品的数据, 首先从本地存储中读取,如果本地存储为空则设为空 }, //你应该发现了,我们修改了cartArray部分, 在页面加载完后,首先从本地存储里读取cartArray的数据
优化用户体验–添加动画
-
当我们在分类页点击添加商品时, 希望有一个小球掉落到购物车的动画
-
在List.vue中, 添加动画盒子
//在cube-scroll标签下: <!-- 创建一个盒子装着动画和要运动的内容,当点击的时候,把盒子移动到点击的内容的位置,然后动画开始( 我们实现的动画就是这个盒子的移动) --> <div class="ball-wrap"> <transition @before-enter="beforeEnter" @enter="enter" @afterEnter="afterEnter" > <!-- 动画的思路就是: 这个盒子先从底部购物车位置移动到点击的位置,接着再水平上匀速向购物车测移动,垂直上做曲线运动(贝塞尔曲线--不用慌,一个css搞定) --> <div class="ball" v-if="ball.show"> <div class="inner"> <i class="cubeic-add"></i> </div> </div> </transition> </div> -
在List.vue的data中新增数据:
ball: { show: false, //是否显示 el: '' //用于获取球的DOM }, -
在List.vue的methods中新增方法:
//实现动画标签里定义的方法 beforeEnter(el) { //动画执行之前 //让小球移动到点击的位置 //获取点击的位置 const dom = this.ball.el //获取点击的dom const rect = dom.getBoundingClientRect() //获取点击dom的位置 const x = rect.left - window.innerWidth * 0.7 const y = - (window.innerHeight - rect.top) console.log(x,y) el.style.display = 'block' //让动画节点显示 el.style.transform = `translate3d(0, ${y}px, 0)` const inner = el.querySelector('.inner') inner.style.transform = `translate3d(${x}px,0,0)` }, enter(el, done) { //触发重绘,否则看不到动画效果 document.body.offsetHeight //小球移动回到原点, 即购物车的位置 el.style.transform = `translate3d(0, 0, 0)` const inner = el.querySelector('.inner') inner.style.transform = `translate3d(0,0,0)` //过渡完成后执行的事件: el.addEventListener('transitionend',done) }, afterEnter(el) { //动画结束隐藏小球 this.ball.show = false el.style.display = 'none' } -
修改List.vue的methods中添加商品到购物车的方法(addtocart)
//增加代码: //让小球显示出来 this.ball.show = true //* 获取点击的元素--即dom节点(直观一点,如果你点击了一个button,它就是<button ...>...</button>) this.ball.el = e.target -
为动画盒子编写css样式:
.ball-wrap .ball position fixed left 70% bottom 10px z-index 1003 color red transition all 1s cubic-bezier(0.49,-0.29,0.75,0.41) .inner width 16px height 16px transition all 1s linear
编写’我的’ 页面—主要是注销功能
-
编写 Mine.vue
<template> <div> <img class="headerimg" src="https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/bannner/1901/learn.png" alt=""/> <ul> <!-- key是起到唯一标识作用 --> <li v-for="item in minearray" class="mineitem" @click="itemclick(item)" :key="item.label"> <span class="minetitle"></span> <i class="cubeic-arrow"></i> </li> </ul> </div> </template> <script> export default { name: 'CodeMine', data() { return { minearray: [ {label: '商品收藏'}, {label: '我的足迹'}, {label: '店铺收藏'}, {label: '我的订单'}, {label: '退出', type: 'exit'}, ] }; }, mounted() { }, methods: { itemclick(item) { if(item.type == 'exit'){ //如果点击的元素的type是exit this.$store.commit('setToken','') //情况token localStorage.removeItem('token') //情况本地存储的token //真实的项目, 此时还应该将购物车中的数据提交到后台并在本地存储中清除掉相应数据,当用户再次登录的时候,再从后台请求用户的购物车数据并存储到本地存储 this.$router.push({path: '/login'}) //跳转到登录页 } } }, }; </script> <style lang="stylus" scoped> // 声明为行内块级元素的元素将占满整个父空间 .mineitem font-size 14px text-align left height 50px line-height 50px padding-left 5% border-bottom 1px solid #eee .minetitle display inline-block width 90% .headerimg height 150px width 100% </style> -
核心部分就是关注如何实现注销:
- 根据所点击元素的type判断是否点击了退出登录
- 退出登录首先应该情况vuex和本地存储的token
- 退出登录应该跳转到登录页
- 实际中退出登录还应该将购物车中的数据提交到后台并在本地存储中清除掉相应数据,当用户再次登录的时候,再从后台请求用户的购物车数据并存储到本地存储
项目打包
- 文件cnpm run build
- 项目文件夹下会生成dist文件夹, 这就是打包出来的最终文件, 上线只需这个文件
- 使用了懒加载, webpack在打包时就会把响应的js和css文件分割(在dist下你可以看到很多js和css文件, 如果你不使用路由懒加载, 那样所有的js就会挤在一个文件里), 在路由需要这些文件时才会加载它们, 这样可以有效提高页面响应速度
- 生成的js和css文件名都会带有哈希值, 这是为了解决缓存问题. 比如你的项目已经打包了一个dist并已上线了, 现在你更改了代码重新打包又上线了一次. 用户那边可能已经有了旧版本的js和css缓存, 如果新版本的js和css文件名与旧版本的一致, 那么新版本就不会更新到用户端, 正因为有了哈希值, 它保证了每次打包的文件名都不一样, 就不会出现缓存导致不同步了.
- 在dist被部署上线时, 后端配置时仅仅是读取index.html一个文件, index.html就是入口文件
vue必备的关键知识
-
谈谈你对MVVM开发模式的理解
- MVVM分为Model、View、ViewModel三者
- Model:代表数据模型,数据和业务逻辑都是在Model层中定义
- View:代表UI视图,负责对数据的展示
- ViewModel:负责监听Model中数据的改变并控制视图的更新,处理用户交互操作
Model和View并无直接关联,而是通过ViewModel来进行联系的,Model和ViewModel之间有着双向数据绑定的联系。因此当Model中的数据改变时会触发View层的刷新,View中由于用户交互操作而改变的数据也会在Model中同步。
这种模式实现了Model和View的数据自动同步,因此开发者只需要专注对数据的维护操作即可,而不需要自己操作dom。(即以数据驱动为核心驱动dom)
- MVVM分为Model、View、ViewModel三者
-
对于组件通信你了解多少,请描述一下你是怎么完成组件的通信的
-
父传子用 props传递
- 子传父用$emit传递 (vue监察者与订阅者设计模式的体现)
- 非父子之间的传值 建立一个空实例进行传值,中央事件总线机制
- 祖孙之间的传值可以利用provide inject模式
VUEX可以处理上述的每一个情况
- state: 公共状态库
- mutations: 同步的方法
- actions: 异步的方法
- getters: 相当于vue里的计算属性computed(内部存放一些复杂的逻辑, 最后仅返回一个结果)
-
-
关于单页应用首屏加载速度慢,出现白屏时间过长问题你怎么处理
- 将公用的JS库通过script标签在index.html进行外部引入(公共的js库可以以静态资源的方式发布到服务器, 再通过CDN加速缓存提高下载速度),减少我们打包出来的js文件的大小,让浏览器并行下载资源文件,提高下载速度
- **重要: ** 在配置路由的时候进行路由的懒加载,在调用到改路由时再加载次路由相对应的js文件
- 加一个首屏loading图或骨架屏(与首屏一模一样的假页面, 加载完毕后隐藏),提高用户的体验
- 尽可能使用CSS Sprites(精灵图片)和字体图标库
- 图片的懒加载等(不是一次性加载所有图片, 而是用户需要某张图片时再加载它)
-
从输入网址到网页渲染完成经历了什么
- 输入网址(URL)按回车键或点击跳转
- 发送到DNS服务器进行DNS解析(域名是购买来的, 且域名对应的ip地址是可变的, 所以需要解析才能知道对应的ip地址),获取到我们对应web服务器对应的ip地址
- 与Web服务器建立TCP连接
- 浏览器向web服务器发送http请求
- Web服务器进行响应请求并返回指定的url数据(当然这里也可能是错误信息或者重定向到新的url地址等)
- 浏览器下载web服务器返回的数据及解析html源文件
- 根据文件生成DOM树和样式树合成我们的渲染树,解析js,最后渲染我们的页面然后显示出来
-
关于修改了数据,视图不更新的理解和处理方式
-
Vue中给data中的对象属性添加一个新的属性时会发生什么
经过打印发现数据是已经改变了,但视图却并未显示出 d的内容来
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="app"> <ul> <li></li> </ul> <button></button> </div> </body> <script src="./vue.js"></script> <script> new Vue({ el: '#app', data() { return { obj: { 'a': '我是a', 'b': '我是b', 'c': '我是c', }, xxx: 1, aaa: 2 }; }, methods: { add() { this.obj.d = '我是d' console.log(this.obj) }, }, }) </script> </html>但是由于在Vue实例创建时, 新添加的属性并未声明,因此就没有被Vue转换为响应式的属性,自然就不会触发视图的更新,这时就需要使用Vue的全局api——>
$set()$set()使用方法:
$set(需要修改的对象,”对象的属性”,值)
对上例中的方法稍作修改:
methods: { add() { // this.obj.d = '我是d' this.$set(this.obj, 'd', '我是d'); console.log(this.obj) }, },**注意: **
-
vue双向数据绑定
- vue对数组的双向绑定仅支持数组的7个api
- push()
- pop()
- shift()
- unshift()
- splice()
- sort()
- reverse()
- 直接通过下标更改数组是不会触发双向绑定的, 这时就需要使用$set()方法
-
在vue里面你如何做数据的监听
-
watch里面监听
-
第一种写法
watch:{ obj(newval,oldval){ console.log(newval,oldval) }, }
-
第二种写法可设置deep为true对数据进行深层遍历监听
watch:{ obj:{ handler(newval,oldval){ console.log(222) console.log(newval,oldval) }, deep:true } }
-
-
computed 里面监听
-
computed里面的依赖改变时,所计算的属性或作出事实的改变
-
data() { return { obj: { 'a': '我是a', 'b': '我是b', 'c': '我是c', }, xxx: 1, aaa: 2 }; }, computed:{ array(){ // 当xxx或aaa发生改变时, 返回结果也将实时更新 return this.xxx + this.aaa } },
-
-
项目总结
-
全家桶成员登场及介绍各自的作用(vue-cli、vue-router、axios、vuex)
- vue-cli: 脚手架, 帮助我们搭建项目, 运行, 打包 自带webpack
- vue-router: 路由
- axios: 经Pomise封装的ajax请求工具
- vuex: 全局的状态管理
-
新知识Mock自己编写本地运行接口返回数据
//webpack扩展配置 configureWebpack: { devServer: { port: 9528, //运行端口号 open: true, //启动后自动打开 //Mock接口编写的地方 before(app) { //参数随便写,但这是基于Express编写的,所以一般写app //? 示例 // app.get('请求地址', (req, res) => { // res.json({ // }) // }) } } } -
开始利用脚手架快速构建我们的工程项目(分别介绍了vue-cli2.x和vue-cli3.x构建命令的区别)
- vue-cli2.x: 安装( npm install vue-cli -g ) 创建( vue init webpack 项目名 )
- vue-cli3.x: 安装( npm install @vue/cli -g ) 创建( vue create 项目名 )
-
讲解vue-cli2.x和vue-cli3.x构建出来的项目架构的区别
-
面对市场上花样百出的ui库如何选择质量高适合自己项目的ui库
- 测试通过率
- 组件多
- 可自定义主题, 与自己项目的风格是否相似
-
使用cube-ui开始我们的项目之旅(tips:多看api文档就能熟练使用ui库, 以及vue的api)
-
使用Mock编写我们的接口并返回我们需要的信息
-
介绍那种场景下使用嵌套路由以及如何实现
-
类似底部导航栏这样的场景
{ path: '/botNav', name: 'botNav', component: () => import('../views/BotNav.vue'), children: [ //! 二级路由path不需要加 / { path: 'index', name: 'index', meta: { //可自定义属性的地方 requireAuth: true,//当有这个字段的时候, 我们就认为这个路由是需要有登录权限的 }, component: () => import('../views/Index.vue') }, ] }
-
-
介绍token的用途和编写axios请求的全局拦截和路由守卫(轻易实现权限控制)
- 提高用户体验: 用户因未登录试图跳转个人中心页时被重定向到登录页, 在登录后应直接跳转到个人中心页, 而不是首页
-
利用vuex和本地存储的配合实现购物车的功能
- vuex
- 全局的状态管理库
- 页面刷新后数据丢失
- 应及时将数据存储到本地存储
- 在页面加载时, 首先就应从本地存储读取数据
- vuex
-
进行项目的性能优化及体验优化(优化首屏加载、路由跳转过渡效果、购物车动画效果)
- 动画实现思路:
- 用一个盒子包裹动画
- 当点击时, 让盒子移动到点击位置
- 执行动画: 分水平位移和垂直位移, 位移有各自css函数可以使用
- 动画实现思路:
-
打包项目及详解面试关于vue的高频面试题
学习路线推荐
html5 -> css3 -> js(首先要会, 然后不断积累,持久深造) -> ps切图基础(掌握基本技能, 如切图,量尺寸等) -> vue(三大框架选其一深入了解,另外两个稍作了解) -> react -> angular -> 小程序生态( vue: uniapp, 一套代码打包出各个端的小程序) -> webpack (必备) -> http知识 (必备) -> ( 逐渐后端 ) -> node.js (后端入门) -> express、koa (两个node框架) -> Linux -> mysql、mongoDB、redis -> (至此已具备全栈能力) -> Git和持续集成 -> Docker -> js高级 -> 前端性能优化 -> 浏览器知识 -> 前端安全性问题
学习要结合项目实战, 学习的目的也是为了项目实战







