电商后台-学习笔记
1.初始化项目
打开vue项目管理页面
vue ui
配置项目
预备安装 Babel Router Linter
安装依赖库
安装依赖: axios
初始化git远程仓库、代码托管到码云
Git配置多个SSH-Key
参考Gitee文章
利用本地工具生成ssh公钥和私钥
ssh-keygen -t rsa -C 'xxxxx@company.com' -f ~/.ssh/gitee_id_rsa
ssh-keygen -t rsa -C 'xxxxx@qq.com' -f ~/.ssh/github_id_rsa
在.ssh目录下添加config文件,内容如下:
# gitee
Host gitee.com
HostName gitee.com
PreferredAuthentications publickey
IdentityFile ./gitee_id_rsa
# github
Host github.com
HostName github.com
PreferredAuthentications publickey
IdentityFile ./github_id_rsa
最终结果:
将公钥复制到码云:个人主页 -> 个人设置 -> 安全设置 -> ssh公钥
测试:
ssh -T git@gitee.com
ssh -T git@github.com
创建仓库、Git初始化本地项目
关于Git,参考我的文章:未发布。
2.配置vue.config.js
const path = require("path");
module.exports = {
// 访问路径(部署后访问资源的基本路径)
publicPath: process.env.NODE_ENV === 'production' ? '/admin/' : '/',
// 打包时输出文件目录
outputDir: 'dist',
// eslint-loader 是否在保存的时候检查
lintOnSave: false,
// 生产环境是否生成 sourceMap 文件
productionSourceMap: false,
/**
* webpack配置,see https://github.com/vuejs/vue-cli/blob/dev/docs/webpack.md
**/
chainWebpack: config => { },
configureWebpack: config => {
config.resolve = {
alias: {
// 配置别名
"@": path.resolve(__dirname, "./src"),
}
};
if (process.env.NODE_ENV === 'production') {
//生产环境下 移除console
config.optimization.minimizer[0].options.terserOptions.compress.drop_console = true
}
},
// webpack-dev-server 相关配置
devServer: {
open: true, // 编译完成是否打开网页
host: "localhost", // 主机ip
port: 8080, // 访问端口
https: false, // 编译失败时刷新页面
inline: true, //实时刷新
hot: true, // 开启热加载
hotOnly: false,
proxy: {
"/api": {
target: "http://xxx.com/my-app", // 你请求的第三方接口
changeOrigin: true, // 在本地会创建一个虚拟服务端,然后发送请求的数据,并同时接收请求的数据,这样服务端和服务端进行数据的交互就不会有跨域问题
pathRewrite: {
// 路径重写,
"^/api": "" // 替换成target中的请求地址,也就是说需要用axios请求http://xxx.com/my-app/login这个地址的时候直接写成/api/login即可。
}
}
}
}
};
前端代理:解决跨域问题
首先需要明白的是,之所以存在跨域问题,是因为一般情况下浏览器不允许访问非当前域,例如网站域名为aaa.com,提供API服务端所在域名为bbb.com,这个时候从网站发出的请求结果会被浏览器拦截处理,抛出跨域错误。网站正式上线时,在API服务端中设置 Access-Control-Allow-Origin 来使浏览器允许跨域请求。
开发阶段则需配置本地代理:即Webpack 开启了一个8080的服务端口,可以看做一台本地服务器,这个服务器不仅负责发送html/css/js,还提供代理服务,js网络请求本地服务器,本地服务器对把url中的"/api"替换成"",然后在前面拼接上target字符串,由本地服务器访问目的服务器,最终返回数据。
3.封装axios
首先创建一个axios示例,配置好拦截器
// 创建axios实例
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_URL,
timeout: 10000 // 请求超时时间
})
// request拦截器
service.interceptors.request.use(config => {
//如果需要,带上token
const token = getToken('token')
if(token){
config.headers['Authorization'] = token
}
return config
}, error => {
// Do something with request error
console.log(error) // for debug
Promise.reject(error)
})
// respone拦截器
service.interceptors.response.use(response => {
/**
* 这里可结合项目接口设计进行修改
*/
const res = response.data
//失败
if (res.meta.code !== 200) {
Message({
message: res.meta.msg, //这里可以显示后端给的错误信息
type: 'error',
duration: 3000
})
if(res.meta.code === 400){
//请求参数错误
}else if(res.meta.code === 401) {
//未授权,进行退出登录操作
}else if(res.meta.code === 403){
//拒绝访问
}else if(res.meta.code === 404){
//服务不存在
}else if(res.meta.code === 500){
//服务器错误
}
return Promise.reject('handled business error in interceptor')
} else {
//成功
return response.data
}
},
error => {
console.log('err' + error)// for debug
Message({
message: error.message,
type: 'error',
duration: 3000
})
return Promise.reject(error)
}
)
然后建一个api目录,存放各个模块的接口,例如user.js:
import request from '../utils/request'
/*获取用户列表*/
export function getUsers(params){
return request({
method:'get',
url:'users',
params
})
}
/*添加用户*/
export function addUser(data){
return request({
method:'post',
url:'users',
data
})
}
/*编辑用户提交*/
export function updateUser(id,data){
return request({
method:'put',
url:'users/'+id,
data
})
}
/*删除用户*/
export function deleteUser(id){
return request({
method:'delete',
url:'users/'+id
})
}
4. 路由
路由导航守卫
全局导航守卫,例如:
router.beforeEach((to, from, next) => {
const token = getToken('token')
if (to.name !== 'login' && !token)
next({ name: 'login' })
else
next()
})
一定要注意next()不能重复调用,只能调用一次。
路由的堆栈
vue-router中由push()和back()等API控制的栈路由和原生App开发有区别,在vue中:push:A->B,此时A是被销毁的,即A的数据已不存在,当back:B->A,会重新初始化A(created和mounted方法会被回调)
嵌套路由
const routes = [
// 登录
{path:'/login', component:Login},
// 首页
{
path:'',
component:Layout,
redirect:'/home', //默认子路由
children:[{
path:'home',
component:()=>import ('../views/home')
}],
},
// 用户模块
{
path:'/users',
name:'users',
component:Layout,
redirect:'/users/list',
children:[{
path:'list',
name:'usersList',
component:()=>import ('../views/users'),
}]
},
// 权限模块
{
path:'/rights',
name:'rights',
redirect:'/rights/roleList',
component:Layout,
children:[{
path:'roleList',
name:'roleList',
component:()=>import ('../views/right/roles')
},{
path:'rightList',
name:'rightList',
component:()=>import ('../views/right/rights')
},{
path:'alloRights/:id',
name:'alloRights',
component:()=>import ('../views/right/roles/allocRight')
}]
},
// 商品模块
]
5.语法知识
ES6模块化
有一个文件A.js
...
export default B
在另一个文件中导入 import C from ‘…/A.js’ ,这里的C就是对导出的对象命名,不一定要写成import B from ‘…/A.js’。
第三方组件上使用vue原生事件
加上.native
,例如:监听el-input
的键盘事件
<el-input placeholder="请输入内容" v-model="query" @keyup.enter.native="getUserList">
input组件键盘enter事件被调用两次
<el-input v-if="scope.row.inputVisable"
@keyup.enter.native="handleAttrValAdd(scope.row)"
@blur="handleAttrValAdd(scope.row)"
>
handleAttrValAdd(row){
row.inputVisable = false
row.inputValue = ''
}
keyup
和blur
绑定了同一个方法,当keyup
触发后第一次调用handleAttrValAdd
,然后input组件隐藏就会失去焦点,从而第二次调用handleAttrValAdd
Vue的$event
在元素的事件中可以拿到事件描述$event
<button @click="$event.target.name" name="哈哈">单击</button>
<!--打印:哈哈-->
Vue的全局API之$nextTick方法
$nextTick(callback)返回一个promise,他的作用是将回调推迟到DOM更新后执行,官方示例:
import { createApp, nextTick } from 'vue'
const app = createApp({
setup() {
const message = ref('Hello!')
const changeMessage = async newMessage => {
message.value = newMessage
await nextTick()
console.log('Now DOM is updated')
}
}
})
空字符串==false
if判断中,空字符串相当于false (js等部分语言中可以将空字符串转换为false,非空字符串转换为true)
let a = "" //空字符串
if(a)
console.log('a相当于true')
else
console.log('a相当于false')
//结果:a相当于false
属性赋值
方式一:
let a = {'name':'fk',sex:'man'}
let b = {sex:'woman'}
Object.assign(a,b)
//a的值:{name: "fk", sex: "woman"}
//注意这里的a不是原来的a,而是产生一个新对象复制给了a
方式二:
let a = {'name':'fk',sex:'man'}
let b = {sex:'woman'}
e = {...a, ...b}
//e的值:{name: "fk", sex: "woman"}
值得注意的是,方式一可以在Vue用来实现数据更新,例如:
data(){
return {
a:{'name':'fk',sex:'man'}
}
}
//更新
methods:{
handler(){
let b = {sex:'woman'}
Object.assign(a,b) //a的数据会发生更新,引起页面重新渲染
}
}
js的split方法
"".split(' ') //0个空格
// [""]
" ".split(' ') //1个空格
// ["", ""]
" ".split(' ') //2个空格
// ["", "", ""]
6. element-ui
el-container布局
.el-container{
display: flex;
flex-direction: row;
flex: 1;
flex-basis: auto;
box-sizing: border-box;
min-width: 0;
}
建议添加样式
.el-container{
height: 100%; /*不设置无法占满高度空间*/
overflow:auto; /*不设置会部分无法滚动显示出来*/
}
表单的使用
以登录注册为例,看下element-ui的表单组件使用
表单绑定、验证
<el-form class="login-form" :model="loginForm" :rules="loginRules">
<el-form-item prop="username">
<!-- 用户名 -->
<el-input prefix-icon="el-icon-user" v-model="loginForm.username">
</el-input>
</el-form-item>
<el-form-item prop="password">
<!-- 密码 -->
<el-input prefix-icon="el-icon-key" type='password' v-model="loginForm.password">
</el-input>
</el-form-item>
<el-form-item class="button-group">
<el-button type="primary" @click="login()">登录</el-button>
<el-button @click="resetForm()">重置</el-button>
</el-form-item>
</el-form>
<script>
export default {
data(){
return {
loginForm:{
username:'admin',
password:'123'
},
loginRules:{
username:[
{ required: true, message: '请输入账号', trigger: 'blur' },
{ min: 3, max: 16, message: '长度应为3-16个字符', trigger: 'blur' }
],
password:[
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 3, max: 16, message: '长度应为3-16个字符', trigger: 'blur' }
]
}
}
}
}
</script>
- model属性不赋值的话,rules验证不会生效
- model和rules相当于组件传参,把这两个数据交给el-form,由el-form来完成验证的效果
- model 需搭配 prop属性,若没有prop属性,
form.resetFields()
是无效的
表单的resetFields失效
失效原因:在表单组件挂载到浏览器之前对数据保存了一个副本,resetFields()可以将数据还原到副本的值。下面的代码resetFields之所以会失效,是因为在表单挂载(显示)之前已经对数据进行了修改,保存的副本不再是data中的数据了。
data(){
return {
...
user:{
username:'',
password:'',
email:'',
mobile:'',
},
}
}
// 显示弹框
showEditDialog (row){
this.editDialogVisable = true
//这里设置的值是表单初始值,resetFields会重置为这些初始值。
this.user.username = row.username
this.user.email = row.email
this.user.mobile = row.mobile
},
// 取消弹框
cancelDialog(type){
switch(type){
case 'add':
this.addDialogVisable = false
this.$refs.addFormRef.resetFields()
break
case 'edit':
this.editDialogVisable = false
this.$refs.editFormRef.resetFields()
break
}
},
同一个页面有添加弹框和编辑弹框,最佳做法:准备两份数据和两个弹框,校验规则可以复用。
Dialog关闭时重置字段
监听Dialog组件的close事件,关闭时重置表单
// 处理添加框和编辑框关闭
handleAddDialClose(){
this.$refs.addFormRef.resetFields()
},
handleEditDialClose(){
this.$refs.editFormRef.resetFields()
},
级联样式修改
/* 级联器面板 */
.el-cascader-panel{
max-height: 240px;
min-height: 50px;
}
/* 取消级联面板水平滚动条 */
.el-scrollbar__wrap {
overflow-x: hidden;
}
/* 取消级联面板的单选框:start */
.el-cascader-panel .el-radio{
width: 100%;
height: 100%;
z-index: 10;
position: absolute;
top: 10px;
right: 10px;
}
.el-cascader-panel .el-radio__input{
visibility: hidden;
}
.el-cascader-panel .el-cascader-node__postfix{
top: 10px;
}
/* 取消级联面板的单选框:end */
效果:
修改引用组件的内部样式
深度作用选择器 >>> (注意,只作用于vue文件中的样式)
.test >>> .el-icon-search {
...
}
但如果是sass/less的话可能无法识别,这时候需要使用 /deep/ 或 ::v-deep 操作符取而代之——两者都是 >>> 的别名(此次验证,::v-deep未生效,其他均生效)。
<style lang="scss" scoped>
.test{
/deep/ .el-icon-search {
font-size: 30px;
}
}
</style>
7.文件上传
1)封装组件
2)理解http请求
http请求头中 Content-Type 的类型
媒体格式 | |
---|---|
text/html | HTML格式 |
text/plain | 纯文本格式 |
text/xml | XML格式 |
image/jpeg | jpg图片格式 |
image/png | png图片格式 |
image/gif | gif图片格式 |
application开头的媒体格式 | |
---|---|
application/xml | XML数据格式 |
application/json | JSON数据格式 |
application/pdf | pdf格式 |
application/msword | Word文档格式 |
application/octet-stream | 二进制流数据(如常见的文件下载) |
application/x-www-form-urlencoded | 中默认的encType,form表单数据被编码为key/value格式发送到服务器(表单默认的提交数据的格式) |
表单上传文件的媒体格式 | |
---|---|
multipart/form-data | 需要在表单中进行文件上传时,就需要使用该格式 |
表单中文件上传中,需要将file封装成FormData,关于FormData的详解参考:https://www.jianshu.com/p/e984c3619019
简而言之FormData是一种包含键值对的对象,可结合下面例子理解:
<form id="advForm">
<p>广告名称:<input type="text" name="advName" value="xixi"></p>
<p>广告类别:
<select name="advType">
<option value="1">轮播图</option>
<option value="2">轮播图底部广告</option>
<option value="3">热门回收广告</option>
<option value="4">优品精选广告</option>
</select>
</p>
<p><input type="button" id="btn" value="添加"></p>
</form>
//js
//通过表单初始FormData
var form=document.querySelector("#advForm");
var formdata=new FormData(form);
//通过key获取value
formdata.get("advName");//xixi
//创建key-value
formData.append('fieldName',field); //field是字符串
formData.append('fileName',file); //file是文件对象
3)axios封装和调用
在utils/request.js
中
// 上传表单中的文件
export function postUpload(options) {
// post(url [, data [, config]])
return service.post(options.url, options.data, { headers: { 'Content-Type': 'multipart/form-data' } })
}
在api/upload.js
中
import {postUpload} from '../utils/request'
export function uploadImg(formData){
return postUpload({
url:'upload',
data:formData
})
}
在Vue页面
let formData = new FormData()
formData.append('file',file)
const res = await uploadImg(formData)
8.第三方依赖
省市三级联动插件
安装 element-china-area-data
页面加载进度条
安装 nprogress
9.打包
参考我的这篇文章 Vue打包优化