十八、项目目录优化
为了提升项目的可维护性和可扩展性,我们把src/components目录下的组件进行目录结构的优化,把所有可以通过路由地址访问带的页面组件放到pages文件夹下,把所有页面组成部分的组件(不通过路由地址访问的组件)放到views文件夹下。
以上所有的文件夹的名称没有强制要求交这个名字,可以自行设置
调整后的目录结构为
project项目名称
-src
-components
-pages 存放页面组件
... 其他目录可以根据需求自行设置
-views 存放页面组成部分的组件
一旦组件目录结构调整以后,在引用该组件的地方也要跟着把引入的路径地址进行改变
十九、stylus样式预处理器
1.特性
- 冒号可有可无
- 分号可有可无
- 逗号可有可无
- 括号可有可无
- 变量
- 插值(Interpolation)
- 混合(Mixin)
- 数学计算
- 强制类型转换
- 动态引入
- 条件表达式
- 迭代
- 嵌套选择器
- 父级引用
- Variable function calls
- 词法作用域
- 内置函数(超过 60 个)
- 语法内函数(In-language functions)
- 压缩可选
- 图像内联可选
- Stylus 可执行程序
- 健壮的错误报告
- 单行和多行注释
- CSS 字面量
- 字符转义
- TextMate 捆绑
2.安装
npm i stylus stylus-loader --save
3.引入
是在页面组件中的style标签中添加一个lang属性(默认值为css),并把他的属性值明确设置为stylus
<style lang="stylus"></style>
4.使用
<template>
<div class="wrapper">
<div class="mask">
<div class="content">
<h1>登录页面</h1>
<div class="item">
<input type="text" name="" id="" placeholder="请输入用户名">
</div>
<div class="item">
<input type="password" name="" id="" placeholder="请输入密码">
</div>
<div class="item">
<button>登录</button>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
}
</script>
<style lang="stylus" scpoed>
.mask
width 100vw
height 100vh
background rgba(0,0,0,0.5)
.content
width 400px
height 300px
background #fff
transform translateY(50%)
margin 0 auto
border-radius 20px
text-align center
h1
padding-top 10px
.item
padding 10px
input
height 30px
width 300px
line-height 30px
font-size 20px
button
width 300px
height 40px
background skyblue
border none
color #fff
font-size 20px
</style>
5.函数
如果有不同的页面要使用相同的样式代码,可以在stylus中封装一个函数,把需要重复使用的样式代码放到函数中,在页面组件中引入函数即可
(1)src/common/css/fn.styl
mask(){
width 100vw
height 100vh
background rgba(0,0,0,0.5)
}
(2)在页面组件中引入
<style lang="stylus" scpoed>
@import '../../common/css/fn.styl'
.mask
mask()
</style>
6.变量的使用(预处理器)
可以预先设置好一些初始的样式信息,包括颜色,尺寸、字体、表格、表单
/src/common/color.styl
$bgColor1 = #33ad3c
$bgColor2 = #2468a2
$bgColor3 = #1b315e
只要在页面组件中引入相关的.styl文件就可以使用预先设置好的变量信息
<style lang="stylus" scoped>
@import '../../common/css/color.styl'
.nav
width 100px
background $bgColor2
</style>
二十、状态管理-Vuex
1.什么是vuex
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
2.安装
npm i vuex --save
3.核心
(1)state
state是vuex仓库中所有的状态,类似vue组件中的data。
import Vuex from 'vuex';
Vue.use(Vuex);
let store = new Vuex.Store({
state: {
num:100,
name:'vuex name'
}
})
new Vue({
el: '#app',
router,
store,
components: { App },
template: '<App/>'
})
在任意组件中读取仓库中的数据
{{$store.state.num}}
(2)mutations
更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于methods
mutations: {
addNum(state,txt){
state.num+=txt;
}
}
组件中
<button @click="$store.commit('addNum',5)"></button>
在页面组件中使用mutation中调用,不能执行异步操作
mutations必须是一个同步函数
(3)Action
Action 类似于 mutation,不同在于:
- Action 提交的是 mutation,而不是直接变更状态。
- Action 可以包含任意异步操作。
actions: {
addNumSync(context){
setTimeout({
context.commit('addNum')
},1000)
}
}
组件中
<button @click="$store.dispatch('addNumSync')"></button>
(4)getters
Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
示例代码
let store = new Vuex.Store({
getters:{
showNum(state){
return `最新的数量是${state.num}`;
}
}
})
在组件中使用计算属性
<p>{{$store.getters.showNum}}</p>
(5)Module
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
namespaced:true 启用命名空间
①设置模块
export default new Vuex.Store({
....
modules:{
shop:{
namespaced:true,// 启用命名空间
state:{
num:1
},
mutations:{
addNum(state, step) {
state.num += step;
}
}
}
}
})
②组件中使用state和mutation
<template>
<div>
<h1>shop模块</h1>
<p>shop:{{$store.state.shop.num}}</p>
<!-- 根模块 -->
<button @click="$store.commit('addNum',5)">改变根数量</button>
<!-- shop模块 -->
<button @click="$store.commit('shop/addNum',5)">改变shop数量</button>
</div>
</template>
③使用助手函数
...mapGetters('命名空间名', ["getCartGoods"])
4.助手函数
(1)mapState
当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗.
在页面组件中
<script>
import {mapState} from 'vuex'
export default {
computed: {
...mapState(['num'])
},
}
</script>
(2)mapGetters
mapGetters
辅助函数仅仅是将 store 中的 getter 映射到局部计算属性:
在页面组件中
<script>
import {mapState,mapGetters} from 'vuex'
export default {
computed: {
...mapState(['num']),
...mapGetters(['showNum'])
},
}
</script>
(3)mapActions
使用 mapActions
辅助函数将组件的 methods 映射为 store.dispatch
调用(需要先在根节点注入 store
在页面组件中
<button @click="addNumSync(3)">action+N</button>
<script>
import {mapState,mapGetters,mapActions} from 'vuex'
export default {
methods: {
...mapActions(['addNumSync'])
}
}
</script>
(4)mapMutations
使用 mapMutations
辅助函数将组件中的 methods 映射为 store.commit
调用(需要在根节点注入 store
)
在页面组件中
<script>
import {mapState,mapGetters,mapActions,mapMutations} from 'vuex'
export default {
methods: {
...mapActions(['addNumSync']),
...mapMutations(['addNum'])
}
}
</script>
5.目录结构优化
把代码都写在main.js中使非常不明智且不合理的,因为仓库中的状态非常多时,对应的代码量也会变得非常多
(1)我们在src目录下床架一个store文件夹,zai store文件夹下再创建一个index.js
(2)然后就可以把之前写在main.js中关于vuex的代码都放到/src/store/index.js中,但是,所有的代码都放在index.js中也是不合适的,以为状态和改变状态的方法会有很多个,所以在此基础上继续进行目录结构的细分,把state、mutation、actions、getters中对应的代码分别拆分到对应的js文件中,在index.js中引入即可
优化之后的代码:
/src/main.js
import store from './store'
new Vue({
el: '#app',
router,
store:store,//一定要把仓库挂到vue实例上
components: { App },
template: '<App/>'
})
/src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
//引入状态
import state from './state'
//引入修改状态的方法
import mutations from './mutation'
//引入异步操作mutation的方法
import actions from './action'
//引入计算属性
import getters from './getter'
//实例化vuex仓库
export default new Vuex.Store({
state:state,//key和val相同时,state:state可以简写成state
mutations,
actions,
getters
});
/src/store/state.js
export default{
num: 100
}
/src/store/mutation.js
export default{
addNumByOne(state){
state.num++
},
addNumByNum(state,num){
state.num+=num
}
}
/src/store/action.js
export default{
addNumByOneSync(context){
setTimeout(()=>{
context.commit('addNumByOne')
},1000)
},
addNumByNumSync(context,n){
context.commit('addNumByNum',n)
}
}
/src/store/getter.js
export default {
showNum(state){
//业务逻辑
return `最新的数量是:${state.num}`;
}
}
状态持久化
(1)使用本地存储结合vuex
(2)使用插件实现数据持久化
安装
npm i vuex-persistedstate --save
使用:
/src/store/index.js
import creatPersistedState from 'vuex-persistedstate'
export default new Vuex.Store({
mutations,
state,
getters,
actions,
plugins:[creatPersistedState()]
})
6.购物车案例
1.业务流程
商品列表页->商品详情页->加入购物车
2.示例代码
(1)商品列表页–展示商品
<template>
<div>
<h1>商品列表</h1>
<div class="list">
<div class="item" v-for="good of goodsArr" :key="good.id" @click="toInfo(good.id)">
<div class="left">
<p>商品名称:{{good.name}}</p>
<p>商品价格:{{good.price}}</p>
</div>
<div class="right">
<img :src="good.img" alt="good.name">
</div>
</div>
</div>
</div>
</template>
<script>
export default {
methods: {
toInfo(id){
this.$router.push('/goods/'+id);
}
},
data () {
return {
}
}
}
</script>
(2)商品详情页–展示具体信息,加入购物车
<template>
<div>
<h1>商品详情</h1>
<p>商品名称:{{info.name}}</p>
<p>商品价格:{{info.price}}</p>
<p>商品图片:
<img :src="info.img" alt="">
</p>
<button @click="addCart">加入购物车</button>
</div>
</template>
<script>
export default {
mounted() {
let id = this.$route.params.gid;
this.info = this.goodsArr.find(item => (item.id = id));
},
methods: {
//点击加入购物车按钮
addCart(){
//触发vuex中的action this.$store.dispatch('shop/addCartGoodsSync',this.info);
this.$router.push('/cart')
}
},
data() {
return {
info: {
name: "",
img: "",
price: "",
id: ""
},
};
}
};
</script>
(3)初始化vue状态,并定义好改变状态的方法
/src/store/shop/index.js定义初始状态
state:{
num:1,
cartGoods:[]//定义购物车空数组
},
/src/store/shop/index.js定义直接改变状态的方法
mutations:{
addCartGoods(state,obj){
state.cartGoods.push(obj)//把指定的内容追加到初始状态中
}
},
/src/store/shop/index.js定义触发mutations的action–可以执行异步操作
actions: {
addCartGoodsSync(context,obj){
context.commit('addCartGoods',obj)
}
},
/src/store/shop/index.js定义计算属性,方便页面去获取
getters: {
getCartGoods(state){
return state.cartGoods;
}
}
(4)购物车页–通过计算属性来获取到已经加入到购物车中的商品信息
<script>
import {mapGetters} from 'vuex'
export default {
computed:{
...mapGetters('shop',['getCartGoods'])
}
}
</script>
二十一、ui库
1.element-ui
(1)安装
npm i element-ui -S
(2)引入
①完整移入
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';//非常重要
Vue.use(ElementUI);
new Vue({
...
render: h => h(App)
});
②按需引入
需要哪个组件就引入哪个组件
import Vue from 'vue';
import { Button, Select } from 'element-ui';
import App from './App.vue';
Vue.component(Button.name, Button);
Vue.component(Select.name, Select);
(3)使用
参照官网使用
2.iview
3.mint-ui
二十二、数据变化后页面不更新
在vue项目,受对象数据类型的影响,有时直接通过下标操作数组,数组内容变化了,但是页面并没有跟着进行重新渲染。
1.解决办法
可以通过JSON序列化来实现
vuex的mutations
addCartNum(state,id){
let idx = state.cartGoods.findIndex((item)=>item.id == id);
let goodsArr = JSON.parse(JSON.stringify(state.cartGoods));
goodsArr[idx].num++;
state.cartGoods = goodsArr;
}
2.解决办法
可以使用vue提供的$forceUpdate方法
vuex的actions
addCartNumSync(context,id){
context.commit('addCartNum',id)
}
vuex的mutations
addCartNum(state,id){
let idx = state.cartGoods.findIndex((item)=>item.id == id);
state.cartGoods[idx].num++;
}
页面组件代码
methods:{
...mapActions(['addCartNumSync']),
add(id){
this.addCartNumSync(id);
//调用完成vuex中的actions操作方法对数据进行改变后
//强制重新渲染页面,触发update生命周期钩子函数
this.$forceUpdate();
}
}
二十三、项目页面准备
1.表单组件
element-ui中提供了表单中常用的组件,比如输入框、选择框、单选框、开关等
<el-form label-width="80px" style="width:600px;" >
<el-form-item label="菜单名称">
<el-input v-model="info.title"></el-input>
</el-form-item>
<el-form-item label="上级菜单">
<el-select v-model="info.pid" placeholder="请选择">
<!--
value 设置选中项的值
label 设置选中项的名称
-->
<el-option value="">请选择</el-option>
<el-option value="0" label="顶级菜单">顶级菜单</el-option>
<el-option value="1" label="系统设置">系统设置</el-option>
</el-select>
</el-form-item>
<el-form-item label="菜单图标">
<el-input v-model="info.icon"></el-input>
</el-form-item>
<el-form-item label="菜单地址">
<el-input v-model="info.address"></el-input>
</el-form-item>
<el-form-item label="状态">
<el-switch v-model="info.status"></el-switch>
</el-form-item>
<el-form-item>
<el-button type="primary">提交</el-button>
</el-form-item>
</el-form>
其中,el-select组件中的el-option组件,如果不设置label和value属性的话,则默认把el-option中的内容当成值和label,但是实际项目中一般都不会直接把el-option中的内容进行传递,所以需要设置value属性和label属性,value属性用来控制传递的值,label属性用来控制匹配的值在option中显示的内容。
2.表单验证
element-ui的表单组件中内置了验证功能,可以防止数据的丢失
(1)rules属性
需要给表单组件添加一个rules属性,用来告知表单具体的验证规则是什么
<el-form :rules="具体的验证规则名称">
(2)验证规则
具体的验证规则需要写在data里来进行预定义
<script>
export default {
data(){
return{
验证规则名称:{
要验证的字段名:[
{ required:true,message:'菜单名称不能为空',trigger:'blur' },
{ min:1,max:20,message:'菜单名称长度不符合要求' }
]
}
}
}
}
</script>
required 设置元素必填
messeage设置元素不符合验证规则显示的文字内容
trigger 设置元素触发规则的机制
min 设置元素内容的最小长度
max设置元素内容的最大长度
(3)prop属性
给需要进行验证的表单元素设置一个prop属性,属性值要和在验证规则中设置的名称保持一致
<el-form-item label="展示的名称" prop="要验证的字段名">
(4)model属性和ref属性
在进行表单验证时,需要给表单组件设置model属性,用来进行具体数据内容的验证
<el-form :model="要进行验证的数据对象" ref="表单自定义名称">
(5)validate
在点击提交按钮时,需要执行表单组件内置的validate方法来实现表单内容的验证
<el-form-item>
<el-button type="primary" @click="自定义方法('表单的ref属性值')">提交</el-button>
</el-form-item>
<script>
export default {
...
methods:{
自定义方法(形参) {
this.$refs[形参].validate((valid) => {
if (valid) {
//验证规则满足时,才执行数据添加操作
}
});
}
}
}
</script>
示例代码:
<template>
<div>
<h1>菜单信息页</h1>
<!-- el-form验证时使用的属性
rules 表单的验证规则
model 表单验证时使用的数据
-->
<el-form
label-width="80px"
style="width:600px;"
:rules="rules"
:model="info"
ref="menuForm"
>
<el-form-item label="菜单名称" prop="title">
<el-input v-model="info.title"></el-input>
</el-form-item>
<el-form-item label="上级菜单" prop="pid">
<el-select v-model="info.pid" placeholder="请选择">
<!--
value
label 设置选中的选项名称
-->
<el-option value="">请选择</el-option>
<el-option value="0" label="顶级菜单">顶级菜单</el-option>
<el-option value="1" label="系统设置">系统设置</el-option>
</el-select>
</el-form-item>
<el-form-item label="菜单图标">
<el-input v-model="info.icon"></el-input>
</el-form-item>
<el-form-item label="菜单地址">
<el-input v-model="info.address"></el-input>
</el-form-item>
<el-form-item label="状态">
<el-switch v-model="info.status"></el-switch>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitInfo('menuForm')">提交</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
data(){
return{
info:{
title:'',
pid:'',
icon:'',
address:'',
status:true
},
rules:{
title:[
{ required:true,message:'菜单名称不能为空',trigger:'blur' },
{ min:1,max:20,message:'菜单名称长度不符合要求' }
],
pid:[
{ required:true,message:'请选择上级菜单' }
]
}
}
},
methods:{
submitInfo(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
//验证规则满足时,才执行数据添加操作
}
});
}
}
}
</script>
<style scoped>
.el-form{
margin:20px;
}
</style>
3.面包屑
显示当前页面的路径,快速返回之前的任意页面。
el-breadcrumb
el-breadcrumb-item
<el-breadcrumb separator=">">
<el-breadcrumb-item :to="{path:'/home'}">首页</el-breadcrumb-item>
<el-breadcrumb-item>
<a href="#/menu">菜单列表</a>
</el-breadcrumb-item>
<el-breadcrumb-item>菜单添加</el-breadcrumb-item>
</el-breadcrumb>
可以直接给el-breadcrumb-item组件设置to属性来进行页面的跳转,也可以在其中添加a标签/router-link标签来进行页面的跳转,如果不需要页面跳转,则直接写文字内容即可。
el-breadcrumb-item之间的分隔符默认是斜杠,可以通过separator属性来自行设置分隔符。
4.NavMenu菜单导航
default-active 当前激活菜单的 index
router 是否使用 vue-router 的模式,启用该模式会在激活导航时以 index 作为 path 进行路由跳转
启用路由模式后,el-menu-item组件的index属性就是要跳转的路由地址,不需要再使用router-link标签也可以实现路由跳转。
但是dafault-active设置为固定值的话,再次刷新页面后,左侧菜单还是选中的固定值的菜单
所以需要把default-active的设置为一个变量
(1)页面加载时
/src/components/views/Nav.vue
<script>
export default {
data(){
return{
defaultActive:''
}
},
mounted(){
//页面加载时,控住左侧菜单选中效果
//把当前路由中的meta的自定义属性赋值给默认选中变量
this.defaultActive = this.$route.meta.select;
}
}
</script>
由于在路由切换时,信息页面的路由地址并没有在左侧菜单中,可以通过路由的meta属性来自行设置选中哪个左侧菜单
/src/router/index.js
{
path:'menu',
component:()=>import('../components/pages/Menu/Index'),
meta:{select:'/menu'}
},
{
path:'menu/add',
component:()=>import('../components/pages/Menu/Info'),
meta:{select:'/menu'}
}
meta属性是路由信息中内置的一个属性,它的属性值类型为对象,在对象中自定义一个键值对用来告知左侧菜单应该选中哪个即可实现。
(2)路由地址变化时
/src/components/views/Nav.vue
<script>
export default {
...
watch:{
$route(newVal){
this.defaultActive = newVal.meta.select;
}
}
}