学什么?
- 1 如何使用 Vue 开发一个项目
- 2 ElementUI 组件库的使用
- 3 业务逻辑(登录、权限、商品)
创建项目
- 通过 vue-cli 脚手架工具直接生成一个项目
- 1 创建:
vue init webpack shop-admin
- 2 配置(根据脚手架的引导来选择配置项)
- 3
cd shop-admin
- 4 启动项目:
npm run dev
或npm start
手动配置路由
- 1 安装:
npm i vue-router
- 2 在
src
目录中,新建router
文件夹,在文件中新建index.js
路由的配置文件 - 3 在
index.js
中,配置路由,并导出路由实例 - 4 在
main.js
中导入 路由配置,并且与 Vue实例 关联到一起 - 5 在
App.vue
中,添加路由出口
element-ui 组件库的使用
- 1 安装:
npm i element-ui
- 2 在 main.js 中,导入 element-ui、样式,并安装插件
- 3 直接在文档中找到对应的组件使用即可
项目接口本地搭建
- 0 解压
shop-api.zip
文件 - 1 打开 phpStudy 启动 MySql 服务
- 2 打开
navicat for mysql
工具,连接数据库 - 3 在链接上,点右键新建数据库,数据库名称为:
shop_admin
- 4 双击启用数据库,再右键
运行SQL文件
,选中 shop_admin.sql,并导入 - 5 打开
shop-api
目录,修改config/default.json
中的数据库用户名和密码(如果需要) - 6 在
shop-api
目录中,打开终端,输入:npm start
启动接口服务
以后每天写项目之前要做的事情
- 1 开启 phpStudy 中的 MySql 服务
- 2 在
shop-api
目录中,执行npm start
启动接口服务
使用 git 管理项目
# 原始提交到远程仓库的命令
git push https://gitee.com/zqran/shop_admin_31.git master
# 有了 remote 后:
git push shop master
# 使用 -u 命令后:
git push shop
# 将 仓库地址 与 shop 这个名称关联到一起
git remote add shop https://gitee.com/zqran/shop_admin_31.git
# -u 就是将 master 设置为默认,提交的时候,就不需要每次都指定 master 分支了
# -u 命令只需要执行一次即可
git push -u shop master
登录功能
搭建登录表单结构
- 1 到 element-ui 文档中找到 表单组件
- 2 找到合适的组件,点击显示代码,将 文档中提供的 示例代码拷贝到我们自己的 Login.vue 组件中
- 目的:为了让这个组件能够在项目中先正常运行起来
- 3 分析 组件结构
- 先从整体分析这个结构
- 4 修改这个结构,让其变为适合我们功能的结构
登录逻辑
调整登录组件的样式
ref
- 作用:添加给 DOM元素或组件,将来可以通过
this.$refs.ref的值
来获取到这个DOM对象或组件。然后,就可以进行DOM操作或获取组件数据、调用组件方法了
编程式导航 - JS代码实现路由跳转功能
-
this.$router.push('/home')
- 参数 /home:表示要跳转到的路由地址
-
this.$router.push({ name: 'home' })
- 通过 name 属性来实现路由跳转
element-ui 消息提示组件
// 这是 element-ui 中提供的一个方法,用来做消息提示:
this.$message({
// 提示信息
message: res.data.meta.msg,
// 消息提示的类型
type: 'error',
// 表示消息显示时长
duration: 1000
})
登录访问控制
- 1 如果没有登录,只能访问登录页面,而不能访问其他
- 2 如果没有登录,访问其他页面,应该跳回到登录页面
- 3 如果登录了,就可以访问其他页面了
如何知道有没有登录
-
1 Vue项目使用
token
来作为登录成功的标识- 只要有 token 就说明已经登录了
- 如果没有 token 就说明还没有登录
-
2 原来,通过 sessionid + cookie 机制,来实现登录状态保持
- 将 sessionid 存储到 cookie 中,这样,登录状态标识(sessionid),就会随着每一次请求都发送给服务器,服务器从 cookie 中获取到sessionid,就知道是否登录过了
-
这两种不同机制都是为了解决同一个问题:
登录状态保持
- 为什么需要状态保持? 因为 HTTP 协议是无状态的
现在,登录成功后,需要将 token 保存起来,存储到 localStorage 中,其他地方用到了,就直接从 localStorage 中取出来使用就可以了
样式调整
- 哪个组件的结构,就把样式放在哪个组件的 style 标签中
菜单组件结构分析
<!--
el-menu 菜单组件
default-active="2" 设置默认菜单高亮,值是el-menu-item的index值
@open="handleOpen" 菜单展开事件
@close="handleClose" 菜单收起事件
background-color="#545c64" 菜单背景色
text-color="#fff" 菜单文字颜色
active-text-color="#ffd04b" 菜单高亮文字颜色
el-submenu 二级菜单,也就是一个可以展开收起的组件。
这个组件可以嵌套,形成多级菜单
index="1" 唯一标志,可以用来设置菜单高亮
el-menu-item 可点击的菜单项组件
disabled 表示禁用这个菜单
-->
<el-menu
default-active="4"
background-color="#545c64"
text-color="#fff"
active-text-color="#ffd04b"
>
<el-submenu index="1">
<template slot="title">
<!-- 指定了菜单的图标 -->
<i class="el-icon-location"></i>
<!-- 指定了菜单名称 -->
<span>导航一</span>
</template>
<!-- 分组菜单: -->
<el-menu-item-group>
<template slot="title">分组一</template>
<el-menu-item index="1-1">选项1</el-menu-item>
<el-menu-item index="1-2">选项2</el-menu-item>
</el-menu-item-group>
<el-menu-item-group title="分组2">
<el-menu-item index="1-3">选项3</el-menu-item>
</el-menu-item-group>
<el-submenu index="1-4">
<template slot="title">选项4</template>
<el-menu-item index="1-4-1">选项1</el-menu-item>
</el-submenu>
</el-submenu>
<el-menu-item index="2">
<i class="el-icon-menu"></i>
<span slot="title">导航二</span>
</el-menu-item>
<el-menu-item index="3" disabled>
<i class="el-icon-document"></i>
<span slot="title">导航三</span>
</el-menu-item>
<el-menu-item index="4">
<i class="el-icon-setting"></i>
<span slot="title">导航四</span>
</el-menu-item>
</el-menu>
props类型的说明
-
:router="true"
此时,router 的值是 布尔值 类型的- 设置为字符串类型:
:router=" 'true' "
- 设置为数值类型:
:router="123"
- 设置为字符串类型:
-
router="true"
此时,router的值是 字符串 类型的
嵌套路由使用步骤
- 1 在创建路由规则的时候,要配置子路由规则,而不是普通的路由
- 子路由是通过路由中的 children 属性来配置的
- 2 在需要切换内容的组件中,添加一个子路由的出口
<router-view />
接口使用说明
- 注意:除了登录接口意外,其他所有的接口都需要将 token 传递给服务器,这样,服务器才知道用户有没有登录
- 如何传递 token ?
- 在 header 中添加 Authorization 请求头
用户列表功能
获取用户列表数据
- 1 在
data
配置中,添加数据userList: []
- 2 在
created
钩子函数中,发送请求,获取用户列表数据- 根据接口文档,找到接口和需要的参数
- 3 将接口返回的数据交给
userList
- 4 修改
el-table
表格组件中的 prop 属性,使其与接口返回的数据匹配
分页获取数据
- 0 封装 分页获取数据 方法
- 1 给组件绑定
@current-change
事件 - 2 通过事件参数获取到当前页码
- 3 在事件中,调用封装好的方法,获取当前页数据
退出功能
- 1 给 退出 按钮绑定单击事件
- 2 在事件中调用 element-ui 的弹框组件,弹出一个确认对话框
- 3 在用户点击 确定 按钮的时候,执行 退出 功能
- 3.1 返回到登录页面
- 3.2 从 localStorage 中移除token
项目中的作用域插槽使用
- 如果想要获取表格中的数据,就通过 slot-scope 的值 scope.row 来获取
<template slot-scope="scope">
<!--
scope.row 表示当前行数据
mg_state 就是当前用户的状态
-->
<el-switch v-model="scope.row.mg_state"></el-switch>
</template>
切换用户状态功能
- 1 给 开关组件 绑定
change
事件 - 2 在事件中获取到当前用户的状态
- 3 将状态发送接口,来完成切换用户状态功能
优化axios的使用
- 1 每次发送请求的时候,都要拷贝完整的基准地址
- 期望:配置基准地址,在发送请求的时候,只需要填写当前接口地址
// 配置基准路径:
axios.defaults.baseURL = 'http://localhost:8888/api/private/v1'
- 2 每次发送请求的时候,都需要设置一次请求头
- 期望:配置请求头,在发送请求的时候,就不需要每次都单独设置请求头
// 请求拦截器
// 说明:因为只要是 axios 发送的请求,都会执行 请求拦截器 中的代码
// 所以,可以在 请求拦截器 中, 一次性添加请求头
axios.interceptors.request.use(config => {
// 统一添加 Authorization 请求头
config.headers.Authorization = localStorage.getItem('token')
// 一定要返回 config
return config
})
- 3 每个组件中都要单独导入axios
- 期望:导入一次,而不是每个组件中都单独导入
// main.js
import axios from 'axios'
Vue.prototype.$http = axios
用户列表 - 查询
- 1 从 element-ui 中找到搜索组件
- 2 给搜索按钮绑定单击事件
- 3 在事件中获取到文本框中输入的搜索内容
- 4 根据搜索内容,调用查询接口,查询数据
- 注意:新功能对老功能的影响,也就是说:添加了一个新的功能进来,实现后,要自己测试一下有没有对老的功能产生影响
用户列表 - 删除
- 1 给删除按钮绑定单击事件,并且传递 id 参数
- 2 事件中弹出确认对话框,防止用户误操作
- 3 当用户点击确定的时候,根据id调用接口删除用户
- 4 删除成功后,重新获取用户列表数据
用户列表 - 添加用户
- 1 给添加用户按钮,绑定单击事件
- 2 在事件中,弹出一个对话框
- 3 进行表单结构、数据、验证的处理
- 4 给 确定 按钮绑定单击事件,来进行表单验证,验证成功后,再添加用户
- 5 添加成功后,关闭对话框,重新刷新列表数据
- 关闭对话框的时候,要重置表单(1 数据 2 校验状态)
- 给对话框绑定关闭事件
close
- 在事件中,重置表单
- 给对话框绑定关闭事件
表单验证的说明
- element-ui 中的表单验证规则,依赖于:async-validator
- 具体的验证规则,应该从这个库中查找
使用组件的注意点
- 1 首先想到的是 组件自身 有没有提供对应的功能
- 2 查看文档,看有没有对应的功能
- 3 如果提供了,就直接使用提供的方式即可
- 4 如果没有提供,就自己实现
用户列表 - 编辑
- 1 给编辑按钮绑定单击事件
- 2 在单击事件中,弹出对话框
- 3 获取到当前用户数据,并且展示在 编辑对话框 中
- 4 给 确定 按钮,绑定单击事件
- 5 获取当前用户数据,发送请求,修改数据
- 6 修改成功后,关闭对话框、提示用户编辑用户成功、刷新列表数据
抽离组件为三部分
- 注意:三个部分的文件名称可以是任意的
<!-- 三个内容中,都是通过 src 属性,引用内容的 -->
<template src="./template.html"></template>
<script src="./script.js"></script>
<style src="./style.css"></style>
<!-- 如果要使用 less 这样配置: -->
<!-- 1 -->
<style src="./style.less" lang="less"></style>
<!-- 2 安装: npm i -D less less-loader -->
VScode 生成SFC模板
// vue 文件中:
{
"vue SFC": {
"prefix": "vuetss",
"body": [
"<template src=\"./template.html\"></template>",
"<script src=\"./script.js\"></script>",
"<style lang=\"less\" src=\"./style.less\"></style>",
""
],
"description": "快速生成单文件组件分离后的结构"
}
}
权限模块 - 业务逻辑
权限模块的组成: 权限、角色、用户
-
权限和角色的关系:每个角色都有自己的权限,权限是属于角色的
- 一个权限可以属于多个角色,一个角色可以有多个权限
- 权限 和 角色的关系:多对多
-
角色和用户的关系:每个用户只能有一个角色,一个角色可以属于多个用户
- 角色 和 用户:一对多
权限 和 用户,间接的就有关系了,因为 权限和角色 是有关系的,角色和用户也是由关系的,所以,权限 和 用户,间接的就有了关系
-
问题:如何给用户分配权限???
- 1 给用户分配角色
- 2 给角色分配权限
- 3 这样,只要用户有某个角色,那么,他就拥有了这个角色下面的所有权限
-
比如:
- 用户:
test5
,他的角色是:产品经理
- 产品经理这个角色拥有的权限:
商品管理
、订单管理
、权限管理
- 用户:
权限和菜单
- 权限和菜单也是关联在一起的
- 也就是说:有什么权限,就有什么菜单
- 权限是有三级的,但是菜单只有两级。因为 三级权限 就是具体的 CRUD 功能了
总结
- 权限模块中的4个内容: 菜单、权限、角色、用户
- 一个用户登录系统后,这个用户是由自己的角色的,角色又有自己的权限,权限又关联到了具体的菜单
- 实现权限模块功能的时候,要做的事情:
- 1 给角色分配权限
- 2 给用户分配角色
角色列表
- 1 从文档中找到表格组件
- 2 拷贝到项目中,先跑起来
- 3 修改 表格组件 ,改为我们要用的功能
分析用户拥有的权限数据结构
- tree 树形结构,类似于 DOM 树
children: [
// 权限名称
roleName: '一级权限',
// 二级权限列表
children: [
{
roleName: '二级权限 1',
// 三级权限列表:
children: []
},
{
roleName: '二级权限 2',
// 三级权限列表:
children: []
}
]
]
给角色分配权限
- 1 先给 分配权限 按钮绑定单击事件
- 2 在事件中先弹出一个对话框
- 3 从 文档 中找到 树形组件 ,拷贝到项目中,先跑起来
- 4 分析结构
- 5 进入页面时,发送请求,获取权限列表的树形结构数据,并渲染到 tree 中
- 6 给 确定按钮 绑定单击事件
- 7 在事件中,获取到选中所有节点id
- 8 发送请求,完成给角色授权
Vue组件样式的问题说明
- 注意:组件中的样式应该只对当前组件生效,而不是要对其他组件产生影响!!!
- 如果产生影响,会到组件之间的样式冲突、覆盖等问题
- Vue 中提供了一个属性
scoped
,只要给 组件的style标签 添加这个属性,那么,样式就只会对当前组件生效,而不会影响其他组件 - 所以,推荐在使用组件的时候,给 style 标签,添加 scoped 属性!!!
- scoped 原理:动态给组件中的标签添加一个
data-v-组件唯一标识
属性,并且样式中自动添加了属性选择器。因为每个组件的 唯一标识 是不一样的,所以,样式就只会对当前组件生效了
element-tree-grid
- 1 安装:
npm i element-tree-grid
("element-tree-grid": "^0.1.3") - 2 在 main.js 中,注册全局组件:
import ElTreeGrid from 'element-tree-grid'
Vue.component(ElTreeGrid.name, ElTreeGrid)
<!--
label :设置列名称
prop :提供列内容的属性名
tree-key :区分其他菜单,不添加该key会导致所有菜单同时展开,添加该key只展开该菜单
level-key :设置菜单级别,以缩进形式表示子菜单
parent-key :父级菜单id,不添加该key,则无法收起子菜单
child-key :指定使用哪个属性名称表示子菜单,默认值为:children
-->
<el-table-tree-column
tree-key="cat_id"
level-key="cat_level"
child-key="children"
parent-key="cat_pid"
label="分类名称"
prop="cat_name"
width="320"
:indent-size="20"
>
<template slot-scope="scope">
<span>{{ scope.row.cat_name }}</span>
</template>
</el-table-tree-column>
富文本编辑器
- vue-quill-editor
- 1 安装:
npm i vue-quill-editor
- 2 在当前组件中导入, 注册为局部组件并使用
// 导入样式文件:
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'
import { quillEditor } from 'vue-quill-editor'
export default {
components: {
quillEditor
}
}
<quill-editor v-model="goodsAddForm.goods_introduce"></quill-editor>
动态获取菜单
- 菜单是动态获取的,不同的用户有不同的角色,不同的角色有不同的权限,每个权限都对应到了菜单
- 所以,不同的用户能够看到不同的菜单
- 问题:不管哪个用户都是通过
/menus
这个接口来获取菜单数据的,没有传id,但是,服务器返回的数据不一样,服务器是如何区分不同用户的?- 根据 token ,token中是包含了用户身份信息的
加载中效果
- 说明: element-ui 中提供了一个 自定义指令 v-loading 这个指令用来添加 loading 效果
- 使用: 想要哪个组件(那块区域出现)加载中效果,就给哪个组件添加自定义指令 v-loading 即可
<!--
loading 是一个布尔值数据,如果为true表示展示加载中效果;如果为false,表示隐藏加载中效果
-->
<el-table v-loading="loading"></el-table>
添加分类
- 1 给添加分类按钮绑定单击事件,在事件中弹出对话框
- 2 从 elemenet-ui 组件库中拷贝对话框组件到项目中,跑起来
- 3 分析 级联选择器 结构
- 4 获取 级联选择器 中的所有分类数据( created 钩子函数中获取 )
- 5 获取 级联选择器 中选择项的值(id)
- 6 给 确定 按钮绑定单击事件,在事件中获取到接口需要的数据
- 7 发送请求,完成添加分类功能
商品列表
- 实现 路由版的分页 功能
- 1 刷新页面的时候,路由中显示的是第几页,就要获取到这一页的数据
- 1.1 如何在刷新页面的时候,从路由中获取当前页(路由参数)?
- 1.2 因为是在刷新页面的时候获取,因此,需要在 created 钩子函数中
- 1.3 通过 this.$route.params.page 获取到当前路由参数(页码)
- 2 切换分页的时候,要改变路由中的路由参数(页码)
- 2.1 要改变哈希值就直接调用 this.{curPage}`)
样式 scoped 属性的问题说明
- 注意:添加
scoped
属性后,样式只对组件内部的结构生效。对于动态生成的结构无效!!! - 结论:
- 1 推荐在使用style给组件添加样式的时候,使用
scoped
属性 - 2 如果发现在 scoped 添加的样式无效,此时,有可能就是动态生成结构的问题,此时,就去掉 scoped 属性,来看下样式是否生效
- 3 如果生效了,就说明是动态生成元素的问题,此时,就创建一个新的 style 标签并且不添加scoped属性,将动态生成元素的样式放在这个style标签中
- 4 如果不生效,则可能是 类名 错了等问题~
- 1 推荐在使用style给组件添加样式的时候,使用
项目打包和优化
- 打包命令:
npm run build
优化:按需加载
- 说明:基于 webpack 的代码分离 和 Vue异步组件
- 1 修改
router/index.js
中导入组件的语法
// 使用:
const Home = () => import('@/components/home/Home')
// 替换:
// import Home from '@/components/home/Home'
// 给打包生产的JS文件起名字
const Home = () =>
import(/* webpackChunkName: 'home' */ '@/components/home/Home')
// chunkName相同,将 goods 和 goods-add 两个组件,打包到一起
const Goods = () => import(/* webpackChunkName: 'goods' */ '@/components/goods')
const GoodsAdd = () =>
import(/* webpackChunkName: 'goods' */ '@/components/goods-add')
- 2 (该步可省略)修改
/build/webpack.prod.conf.js
中的 chunkFilename
{
// [name] 代替 [id]
chunkFilename: utils.assetsPath('js/[name].[chunkhash].js')
}
优化:使用CDN
项目中使用CDN
- 问题:打包后
vendor
(第三方资源文件) 体积过大 - 原因:项目中用到了很多的第三方包,这些包在打包的过程中,全部打包到了
vendor
这个一个JS文件中,导致体积过大 - 如何优化
vendor
体积? 使用CDN - 原理:使用CDN处理第三方包,是引用的在线地址。此时,这些包不会被打包到
vendor
文件中,因此,vendor
体积就会变的更小
CDN使用步骤
1 在
index.html
中引入 CDN 提供的 JS 文件2 在
/build/webpack.base.conf.js
中(resolve 前面)添加配置 externals注意:通过 CDN 引入 element-ui 的样式文件后,就不需要在 main.js 中导入 element-ui 的 CSS 文件了。所以,直接注释掉 main.js 中的导入 element-ui 样式即可
externals
配置:
externals: {
// 键:表示 导入包语法 from 后面跟着的名称
// 值:表示 script 引入JS文件时,在全局环境中的变量名称
// window.Vue / window.axios / window.VueRouter
vue: 'Vue',
axios: 'axios',
'vue-router': 'VueRouter',
// 注意:带有样式文件的第三方包,需要在 代码中 将样式注释掉!!!
'element-ui': 'ELEMENT',
'element-tree-grid': 'ElTableTreeColumn',
'vue-quill-editor': 'VueQuillEditor'
BMap: 'BMap',
echarts: 'echarts',
}
常用包 CDN
-
说明:
- 1 先在官方文档查找提供的 CDN
- 2 如果没有,在
https://www.bootcdn.cn/
或其他 CDN 提供商 查找
Quill
<!-- Include the Quill library -->
<script src="https://cdn.bootcss.com/quill/1.3.4/quill.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-quill-editor@3.0.4/dist/vue-quill-editor.js"></script>
<!-- Include stylesheet -->
<link href="https://cdn.bootcss.com/quill/2.0.0-dev.3/quill.core.min.css" rel="stylesheet"/>
<link href="https://cdn.bootcss.com/quill/2.0.0-dev.3/quill.snow.min.css" rel="stylesheet"/>
<link href="https://cdn.bootcss.com/quill/2.0.0-dev.3/quill.bubble.min.css" rel="stylesheet"/>
缓存和保留组件状态
- keep-alive
- 解决方式:使用
keep-alive
,步骤如下:
1 在需要被缓存组件的路由中添加 meta 属性 meta
属性用来给路由添加一些元信息(其实,就是一些附加信息)
{
path: '/',
name: 'home',
component: Home,
// 需要被缓存
meta: { keepAlive: true }
}
2 修改路由出口,替换为以下形式: 根据 meta 是否有 keepAlive
属性,决定该路由是否被缓存
<keep-alive>
<!-- 这里是会被缓存的视图组件 -->
<router-view v-if="$route.meta.keepAlive"> </router-view>
</keep-alive>
<!-- 这里是不被缓存的视图组件 -->
<router-view v-if="!$route.meta.keepAlive"> </router-view>
启用路由的 History 模式
- 通过在路由中添加
mode: 'history'
可以去掉 浏览器地址栏中的 # - 在开发期间,只需要添加这个配置即可
- 但是,在项目打包以后,就会出现问题
// 去掉 # 后,地址变为: http://localhost:8080/goods 那么,服务器需要正确处理
/goods 才能正确的响应内容, 但是,/goods 不是服务端的接口,而是
用来在浏览器中实现 VueRouter 路由功能的
后端的处理方式
- 1 优先处理静态资源
- 2 对于非静态资源的请求,全部统一处理返回 index.html
- 3 当浏览器打开 index.html 就会加载 路由的 js 文件,那么路由就会解析 URL 中的 /login 这种去掉#的路径了
反向代理
- webpack-dev-server proxy 参考文档
- 修改
config/index.js
配置文件
proxyTable: {
// 使用:/api/movie/in_theaters
// 访问 ‘/api/movie/in_theaters’ ==> 'https://api.douban.com/v2/movie/in_theaters'
'/api': {
// 代理的目标服务器地址
target: 'https://api.douban.com/v2',
// https请求需要该设置
secure: false,
// 必须设置该项
changeOrigin: true,
// '/api/movie/in_theaters' 路径重写为:'/movie/in_theaters'
pathRewrite: {"^/api" : ""}
}
}