话说"天下大事 合久必分分久必合", WEB开发也是如此; 从过去的JSP、PHP到现在的vue+后端、react+后端、angular+后端... 前后端分离的开发模式已经有好多个年头了,在此期间诞生了很多优秀的前端框架、后端框架、开发模式; 然而,现在服务端渲染又再次被很多人提起,应用在各种项目中;相信伴随硬件技术和网络的发展,web开发又将在某个节点走向前后端统一
今天的主题是前后端分离(Vue+后端),但是我想还是从前后不分离的维度去看问题可能会更明了;
"前端前端我是前端 后端后端我也是前端" "后端后端我是后端 前端前端我也是后端"
个人感觉要做好前端端分离模式下的前端开发,应该要懂一些后端的相关基础知识; 这里主要探讨以下几个(前后端分离项目中的)话题:
-
跨域问题
-
接口、数据规范
-
开发环境下数据对接
-
vue三种数据、三个变化
-
项目实践
- 添加vueDevtools控制台开发工具
- 关于表单数据重置
- echarts组件封装与数据展示示例
- 处理Promise异步接口响应
- 关于过大文件提取为组件的建议
-
遇到的一些问题
- vue-cli3取消eslint 校验代码
- axios请求发送了两条,第一条Method为option,第二条Method为get
- vue 开发下,通过代理请求后端接口, 返回307
- axios关于携带参数的问题
- 关于带分页查询
- 关于前后端数据类型
-
寄语
跨域问题
首先我们应该知道,跨域仅存在于浏览器端;是受到浏览器同源策略的影响;解决跨域有多种方案,(可参考跨域解决方案), HTTP请求头需要了解一下 这里主要看我们自己项目上的两种解决方案;
开发环境下的解决方案:
- 服务端设置, 若服务端设置成功,浏览器控制台则不会出现跨域报错信息,如
// NodeJs
router.get('/getLists', function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "X-Requested-With");
res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");
res.header("X-Powered-By",' 3.2.1')
res.header("Content-Type", "application/json;charset=utf-8");
// ....
});
// 其他编程语言的自行查阅
- Vue框架的跨域(vue-cli脚手架); 主要利用node + webpack + webpack-dev-server代理接口跨域。在开发环境下,由于vue渲染服务和接口代理服务都是webpack-dev-server下面,所以页面与代理接口之间不再跨域,无须再设置headers跨域信息了;
module.exports = {
publicPath: './',
assetsDir: './static',
devServer: {
open: true,
overlay: {
warnings: false,
errors: true
},
proxy: {
'/api': {
target: `http://192.168.1.254:3000`, // 代理跨域目标接口
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
}
}
}
部署解决方案
-
nginx反向代理接口跨域
跨域原理: 同源策略是浏览器的安全策略,不是HTTP协议的一部分。服务器端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要同源策略,也就不存在跨越问题。
-
实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录
3、配置参考
server {
listen 8001;
server_name localhost;
location / {
root html;
index index.html index.htm;
}
## /api/ not /api
location /api/ {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_pass http://10.10.1.116:3000/;
}
location /user/ {
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_pass http://10.10.12.120:9000/;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
接口数据规范
关于接口返回的数据格式,根据项目中遇到的一些问题,结合网络环境下其它同类产品的解决方案,提倡类似如下的数据格式:
{
status: true, // true || false Boolean
msg: '数据请求成功', // 提示信息 String
data: {} // Json Map Object
}
data里面的数据,可采用如下形式:
- 返回大批量的列表
{
status: true,
msg: 'success',
data: {
lists: ['a', 'b', 'c'],
// lists: [{id: 1, name: 'zhangsan'}, {id: 2, name: 'lisi'}]
// 如果有分页的情况
pagination: {
totals: 100, // 记录总条数
current: 5, // 当前页
number: 20, // 每页显示条数
pages: 5 // 总页数
}
}
}
// 如果数据为空, 请返回格式完整的数据,不要返回null
{
status: true,
msg: 'success',
data: {
lists: [],
// lists: []
// 如果有分页的情况
pagination: {
totals: 0, // 记录总条数
current: 1, // 当前页
number: 20, // 每页显示条数
pages: 1 // 总页数
}
}
}
- 返回对象(如个人信息详情)
{
status: true,
msg: 'success',
data: {
name: '迈克尔乔丹',
age: 30
}
}
// 如果信息项可能有时有,有时无, 建议前端将必须展示的字段一一列举(给数据占个坑), 可避免不必要的错误提示,如:
{
book: {
name: '',
author: '',
category: '',
publishers: '',
time: ''
}
}
- 返回如列举型的对象,如同时列举学校信息、班级信息、个人信息
{
status: true,
msg: 'success',
data: {
'school': {
name: '毛坦厂中学',
address: '六安',
ratio: '45%' // 本科升学率 (如不能直观看出是什么意思,请注释)
},
'class': {
name: '三年九班',
studentNums: 45
},
'user': {
name: '乔峰',
age: 30
}
}
}
// 如果数据为空请返回如下格式, 不要返回data: null 或 data: {}, 请返回
{
status: true,
msg: 'success',
data: {
'school': {},
'class': {},
'user': {}
}
}
// 这样可以有效的避免页面报出大量的xxx is undefined错误
- 返回如类型列表状的数组
{
status: true,
msg: 'success',
data: [
{value: 1, label: '热力图'},
{value: 2, label: '卫星图'}
]
}
- 返回图表数据(此处建议前端提前提供数据格式给服务端)
{
status: true,
msg: 'success',
data: {
xAxis: [
{name: "违法犯罪", max: 100},
{name: "布控", max: 100},
{name: "在逃",max: 100},
{name: "临逃",max: 100},
{name: "其它",max: 100}
],
series: [{
name: "预警总数",
value: [65, 73, 80, 74, 79, 93]
},
{
name: "已处理",
value: [32, 43, 50, 64, 60, 62]
}
]
}
}
// 如果数据为空,勿返回{data: null}请保持数据格式的完整:
{
status: true,
msg: 'success',
data: {
xAxis: [],
series: []
}
}
开发环境下数据对接
项目开发中我们可能遇到这样的问题,一个项目我们可能从多个后端服务去接入数据, 此时前端项目该如何处理?
vue.config.js中设置多个代理
通过设置代理,我们将可以设置N多代理,接N多服务的数据,如:
devServer: {
port: port,
//open: true,
overlay: {
warnings: false,
errors: true
},
proxy: {
'/api': {
target: `http://192.168.1.254:3000`, // 后端开发统一环境
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
},
'/system': {
target: `http://10.10.12.120:9000`, // 后端开发统一环境
changeOrigin: true,
pathRewrite: {
'^/system': '/system'
}
},
'/axfkq': {
target: `http://10.10.14.201:8888`,
changeOrigin: true,
pathRewrite: {
'^/axfkq': ''
}
},
// ...
},
}
// 需要注意的是,每添加一个代理,我们都需要添加一个前缀(如:api)用于服务识别将其代理到何处; 此时就会产生很多问题,如:
1、如何给url上面添加前缀
2、打包上线时不需要前缀,又如何去除
针对上述问题,可采用如下方式处理:
第一、是否必须要用多个地址,如果是,是否可以尽可能的将数量降低到最少
第二、通过配置文件的形式,将修改url的工作量降低到最小
第三、通过单独服务将所有的接口代理为统一地址
后端统一处理
后端统一处理也就是上面的第三种,由某一个web服务,将接口处理为统一的地址请求
vue三种数据、三个变化
三种数据主要介绍:
- prop (从父组件传递到子组件的数据)
- data 组件自己定义的数据(只能直接修改的数据)
- computed 计算属性(依赖其他数据,动态改变的数据,被动改变的数据)
三者的共同点:
1.都能在模板直接展示 2.都可被监听器watch监听
三者不同之处:
- data里面的数据是唯一的可以直接设置、赋值的数据
- computed可根据prop、data、其他computed计算属性的值变化而变化; 可以设置但不要设置它的值
- data、computed都位于组件本身之内, prop通过父组件传入
- prop属性的修改之内通过修改父组件中的对应数据修改(因为Vue同React一样都是单向数据流)
- prop属性可以直接赋值给data里面的属性作为组件的初始值
- 如果要对接口返回的数据加以处理后再使用,可以1、将数据格式化后再赋值给data; 2、使用computed,在模板中使用computed处理后的数据
项目实践
添加vueDevtools控制台开发工具
俗话说
工欲善其事必先利其器
,Vue开发必然离不开vue的开发调试工具; 控制台对于前端开发而言是极为重要的,善于调试将使我们的开发事半功倍,下面介绍安装一款vue控制台调试插件:
作为伸手党,网上有小伙伴将配置好的工具献给了广大伸手党,在这里先行谢过; 下载地址: vue-devTools工具安装包 开始安装吧: 1、打开google浏览器,输入地址chrome://extensions/ ,点击加载已解压的扩展程序
关于表单数据重置
表单初始化、表单赋值、表单重置在Vue项目中用的非常多,如果有效的管理表单的值成为前端开发者必须面对的问题,今天推荐下面的一种解决方案:
// 定义一份原始数据
let form = {
name: '',
age: 0,
isStudent: true,
loves: []
};
export default {
data(){
form: JSON.parse(JSON.stringify(form)) // 其实关键就是这里
},
methods:{
edit(){
// 编辑时可以直接赋值
this.from = {
name: 'Bruce Lee',
age: 33,
isStudent: false,
loves: ['dance', 'kongfu']
}
// ...
},
add(){
// 需要重点关注的是添加
// 如果之前执行过编辑、添加动作未重置表单,可能之前的值就留在表单上了
// 重置表单
this.form = JSON.parse(JSON.stringify(form));
}
}
}
echarts组件封装与数据展示示例
图表是项目中经常用到的组件,在统计、大屏展示类项目中应用极为广泛, 将图表封装为组件对于Vue项目而言是非常有必要的,带来的实惠是显而易见的,触手可及的,以下以Echarts为例;
以下演示默认已经导入了echart.js
先看下以往我们是怎么使用图表的
:HTML
<div id="echartContainer"></div>
:JS
function renderChart(id) {
var option = {
color: ['#3398DB'],
title: {
text: '区域客运量',
left: 4,
top: 4,
textStyle: {
color: '#69dbdf',
fontSize: 16
}
},
tooltip: {
trigger: 'axis',
axisPointer: { // 坐标轴指示器,坐标轴触发有效
type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
}
},
grid: {
top: '20%',
left: '3%',
right: '4%',
bottom: '6%',
containLabel: true
},
xAxis: {
type: 'category',
axisLabel: {
color: '#93a8bf'
},
axisLine: {
lineStyle: {
color: '#2f415b'
}
},
axisTick: {
lineStyle: {
color: '#2f415b'
}
},
data: ['海淀区', '朝阳区', '丰台区', '石景山', '门头沟']
},
yAxis: {
type: 'value',
axisLabel: {
color: '#93a8bf'
},
axisLine: {
lineStyle: {
color: '#2f415b'
}
},
axisTick: {
lineStyle: {
color: '#2f415b'
}
},
splitLine: {
show: false
}
},
series: [
{
type: 'bar',
barWidth: 20,
data: [100, 200, 100, 150, 300]
}
]
}
var myChart = echarts.init(document.getElementById(id));
myChart.setOption(option);
}
:调用
renderChart('echartContainer')
如上代码,正常调用妥妥的没有任何问题, 1\2\3就跑起来了,然而就上述代码不经要问几个问题:
-
如何在修改数据时动态设置图表,让图表重新渲染?
解决方案: (将数据抽离为变量, 更新option,在调用一次setOption)
新的问题: 如果变化、需要判断的参数比较少尚可,如果比较多呢? 岂不是忙于处理option了
-
如果为了防止图表因数据问题无法展示,要保留一份默认数据,该如何实现?
解决方案: 定义变量时预留一份默认数据,如果数据判断为假则使用默认数据
新的问题: 如上变量多如何处理? 变量判断有数组、字符串、对象,要预留一份默认数据岂不是大量的判断
-
如果想在series里面的数据发生变化时,图表动态更新,该如何实现?
-
如果直接将上述代码直接copy,是否可以跟新项目无缝对接,瞬间展示出对应类型的图表?
莫慌,今天谈论的主题是Vue,Vue可以帮我们做这些事情:
- 封装好的echarts组件,作为一个完整的整体,可以在vue项目中任意使用, 不会因为缺少数据而无法看到图表的真面目
- 我们可以对配置项中的任何一项做判断,根据判断的值展示不同的效果
- 当数据发生变化时,我们不需要做任何事情
- 传值方便,修改方便
扯了那么多直接上代码吧(下述代码为了演示只做了简单封装,在安装了echarts的情况下可以直接copy使用)
:Vue-echarts组件 SimpleBar
<template>
<div class="polyline-chart-container" id="boundChartContainer" ref="boundChartContainer"></div>
<!--/polylineChartContainer-->
</template>
<script>
import echarts from "echarts";
export default {
name: "PolicStatisticsSpreadChat",
props: {
everyAnimation: {
type: Boolean,
default: true
},
title: {
type: String,
default: '区域客运量-组件标题'
},
xAxis: {
type: Object,
default: () => ["潭门港", "新海港", "南港", "博鳌港", "新港", "清澜港", "港门港"]
},
series: {
type: Array,
default: () => [220, 310, 301, 230, 120, 120, 310]
}
},
data() {
return {
chart: null
};
},
computed: {
chartOptions() {
return {
color: ['#3398DB'],
title: {
text: this.title,
left: 4,
top: 4,
textStyle: {
color: '#69dbdf',
fontSize: 16
}
},
tooltip: {
trigger: 'axis',
axisPointer: { // 坐标轴指示器,坐标轴触发有效
type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
}
},
grid: {
top: '20%',
left: '3%',
right: '4%',
bottom: '6%',
containLabel: true
},
xAxis: {
type: 'category',
axisLabel: {
color: '#93a8bf'
},
axisLine: {
lineStyle: {
color: '#2f415b'
}
},
axisTick: {
lineStyle: {
color: '#2f415b'
}
},
data: this.xAxis || ['海淀区', '朝阳区', '丰台区', '石景山', '门头沟']
},
yAxis: {
type: 'value',
axisLabel: {
color: '#93a8bf'
},
axisLine: {
lineStyle: {
color: '#2f415b'
}
},
axisTick: {
lineStyle: {
color: '#2f415b'
}
},
splitLine: {
show: false
}
},
series: [
{
type: 'bar',
barWidth: 20,
data: this.series || [100, 200, 100, 150, 300]
}
]
}
}
},
mounted() {
// 初始化必须是放在mounted里面, 切莫放在created里面(可能会报错,dom对象为Null)
this.initChart();
},
methods: {
initChart() {
this.chart = echarts.init(this.$refs.boundChartContainer);
this.chart.setOption(this.chartOptions);
},
renderChart() {
if (this.everyAnimation) this.chart.clear(); // 是否开启动画
this.chart.setOption(this.chartOptions); // 图表设置的核心,实际上还是setOption
}
},
watch: {
// 可以监听任何属性的变化,当所监听属性发生变化时更新图表
xAxis() {
this.renderChart();
},
series() {
this.renderChart();
}
}
};
</script>
<style lang="scss" scoped>
.polyline-chart-container {
height: 100%;
}
#boundChartContainer {
width: 100%;
height: 100%;
}
</style>
:组件引用
<template>
<SimpleBar
:title="simpleBar.title"
:xAxis="simpleBar.xAxis"
:series="simpleBar.series"
></SimpleBar>
</template>
<script>
import SimpleBar from "./SimpleBar";
export default {
name: "demoEchart",
components: {
SimpleBar
},
data(){
return {
simpleBar: {
title: '合肥市各区Java开发薪资待遇',
xAxis: ['蜀山区', '瑶海区', '包河区', '庐阳区', '经开区', '高新区', '新站区'],
series: [800, 600, 500, 650, 700, 900, 450]
}
};
},
mounted(){
setTimeout(()=>{
this.simpleBar = {...JSON.parse(JSON.stringify(this.simpleBar)), ...{title: '2020年合肥市各区Java开发薪资待遇'}};;
}, 10000)
setTimeout(()=>{
this.simpleBar = {
title: '2000年合肥市各区Java开发薪资待遇',
xAxis: ['蜀山区', '瑶海区', '包河区', '庐阳区'],
series: [600, 500, 500, 450]
}
}, 15000)
}
};
处理Promise异步接口响应
前端处理后端接口的响应应考虑多种状态, 例如:运用比较多的axios, 每次调用实际上返回的就是一个Promise实例; 对于接口的响应我们不止要处理成功的返回,还要处理异常、失败态,尤其对于大屏项目,为了确保图表不会因为数据问题而过于丑陋, 我们应该做多方面的考虑; 【具体的根据项目需求】
// 获取模拟数据
function getModalLists() {
return {
xAxis: ['北京', '上海', '天津', '武汉', '广州', '深圳', '重庆'],
data: [400, 200, 350, 850, 500, 700, 3000]
}
};
import { getChartLists } from '@/api'; // 假定src/api/index.js 下有一个用于获取图表数据的异步方法
export default {
data(){
return {
chartData: {
xAxis: [],
data: []
}
}
},
methods: {
async getChartLists(){
let response = await getChartLists().catch((err)=>{
// 错误时调用模拟数据
this.chartData = getModalLists();
});
if(response.status === 'success'){
this.chartData = response.data;
} else if(response.status === 'fail'){
this.chartData = getModalLists();
}
}
}
}
关于过大文件提取为组件的建议
在项目开发中,我们经常会遇见一个vue文件中元素过多的情况,例如: 一个后台管理页面, 通常包含以下部分: 数据列表、列表查询表单、添加\编辑表单弹窗,甚至更多...,
-
对于业务较为简单的页面而言,所有的操作都放在一个页面,所有的数据都在一个页面去管理,会显得非常方便; 然而一旦遇见业务逻辑复杂的页面,那所有的数据都放在一个页面,恐怕就不那么好办了;
-
在MVVM框架开发中,如何去管理数据是一件非常重要的事情, 如果一个文件中的数据量过多,那管理起来就非常费劲了,尚若命名、注释等规范做的不好,那后期这个文件将成为维护人员的噩梦;
由此,建议大家把可以分离的功能单独提取为一个组件, 下面以一个列表页中的表单为例:
// 拆分前代码
<template>
<div>
<h2>演示: Ajax跨域访问(Dev)</h2>
<el-main style="padding: 0;">
<div style="text-align: right; overflow: hidden;">
<h3 style="float: left;">图书列表</h3>
<el-button @click="showAddForm">添加图书</el-button>
</div>
<el-table :data="tableData">
<el-table-column type="index" label="序号" width="100"></el-table-column>
<el-table-column prop="name" label="书名" width="300"></el-table-column>
<el-table-column prop="author" label="作者"></el-table-column>
<el-table-column prop="category" label="分类"></el-table-column>
<el-table-column prop="publishers" label="出版社"></el-table-column>
<el-table-column label="出版时间" width="140">
<template slot-scope="scope">{{ scope.row.time | timeToCH}}</template>
</el-table-column>
<el-table-column label="是否上架" width="140">
<template slot-scope="scope">{{ scope.row.status ? '已上架':'未上架'}}</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template slot-scope="scope">
<el-button @click="handleClick(scope.row)" type="text" size="small">查看</el-button>
<el-button type="text" size="small" @click="handleEditClick(scope.row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
</el-main>
<el-dialog :title="title" :visible.sync="dialogFormVisible" width="740px">
<el-form :model="form" ref="form">
<div class="el-form--inline">
<el-form-item label="书名" :label-width="formLabelWidth">
<el-input v-model="form.name" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="作者" :label-width="formLabelWidth">
<el-input v-model="form.author" autocomplete="off"></el-input>
</el-form-item>
</div>
<div class="el-form--inline">
<el-form-item label="出版社" :label-width="formLabelWidth">
<el-input v-model="form.publishers" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="出版时间" :label-width="formLabelWidth">
<el-date-picker
v-model="form.time"
type="date"
placeholder="选择日期"
value-format="yyyy-MM-dd"
></el-date-picker>
</el-form-item>
</div>
<div class="el-form--inline">
<el-form-item label="图书分类" :label-width="formLabelWidth">
<el-select v-model="form.category" placeholder="请选择图书分类">
<el-option
:label="item.label"
:value="item.value"
v-for="item in categories"
:key="item.value"
></el-option>
<!-- <el-option label="武侠类" value="wuxia"></el-option>
<el-option label="文学类" value="wenxue"></el-option>-->
</el-select>
</el-form-item>
<el-form-item label="是否上架" :label-width="formLabelWidth">
<el-switch v-model="form.status"></el-switch>
</el-form-item>
</div>
<div style="text-align: left;" v-if="form.status">
<el-form-item label="上架商城" prop="type" :label-width="formLabelWidth" style="width: 100%;">
<el-checkbox-group v-model="form.shops">
<el-checkbox
name="type"
v-for="shop in shopLists"
:label="shop.value"
:key="shop.value"
>{{shop.label}}</el-checkbox>
</el-checkbox-group>
</el-form-item>
</div>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">取 消</el-button>
<el-button type="primary" @click="addBook">确 定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import axios from "axios";
import { mapGetters } from "vuex";
import { getBookLists } from "@/api/book.api";
let formDefault = {
name: "",
author: "",
category: "",
status: true,
time: "",
publishers: "",
shops: []
};
export default {
name: "demoIndex",
data() {
return {
title: '新增图书',
categories: this.$store.state.app.categories,
formLabelWidth: "120px",
dialogFormVisible: false,
bookLists: [],
form: JSON.parse(JSON.stringify(formDefault))
};
},
computed: {
...mapGetters(["shopLists"]),
tableData() {
// 请注意这里的拷贝
let books = JSON.parse(JSON.stringify(this.bookLists));
return books.map(item => {
item.name = `《${item.name}》`;
item.category = this.categories.find(
cate => cate.value === item.category
)["label"];
return item;
});
}
},
created() {
this.getBookLists();
},
methods: {
showAddForm() {
this.form = JSON.parse(JSON.stringify(formDefault));
this.dialogFormVisible = true;
},
handleClick(row) {
this.$router.push({ path: `/demo/detail/${row.id}` });
},
handleEditClick(row) {
let id = row.id;
let book = this.bookLists.find(item => item.id === id);
this.title = "编辑图书";
this.form = JSON.parse(JSON.stringify(book));
this.dialogFormVisible = true;
},
async getBookLists() {
let response = await getBookLists();
if (response.status) {
this.bookLists = response.data;
}
},
addBook() {
console.log(this.form);
this.dialogFormVisible = false;
if(this.form.id){
this.bookLists = this.bookLists.map(item => {
if(item.id === this.form.id) return this.form;
return item;
})
this.title = "添加图书";
} else {
this.bookLists.push({ ...{ id: this.bookLists.length }, ...this.form });
}
}
},
filters: {
timeToCH(time) {
return (
time.substr(0, 4) +
"年" +
time.substr(5, 2) +
"月" +
time.substr(8, 2) +
"日"
);
}
}
};
</script>
<style lang="scss" scoped>
/deep/ .el-form--inline {
text-align: left;
.el-form-item__content {
width: 220px;
margin-left: 0 !important;
}
}
</style>
// 拆分后代码
List Vue
<template>
<div>
<h2>演示: Ajax跨域访问(Dev)</h2>
<el-main style="padding: 0;">
<div style="text-align: right; overflow: hidden;">
<h3 style="float: left;">图书列表</h3>
<el-button @click="dialogFormVisible = true">添加图书</el-button>
</div>
<el-table :data="tableData">
<el-table-column type="index" label="序号" width="100"></el-table-column>
<el-table-column prop="name" label="书名" width="300"></el-table-column>
<el-table-column prop="author" label="作者"></el-table-column>
<el-table-column prop="category" label="分类"></el-table-column>
<el-table-column prop="publishers" label="出版社"></el-table-column>
<el-table-column label="出版时间" width="140">
<template slot-scope="scope">{{ scope.row.time | timeToCH}}</template>
</el-table-column>
<el-table-column label="是否上架" width="140">
<template slot-scope="scope">{{ scope.row.status ? '已上架':'未上架'}}</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template slot-scope="scope">
<el-button @click="handleClick(scope.row)" type="text" size="small">查看</el-button>
<el-button type="text" size="small" @click="handleEditClick(scope.row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
</el-main>
<BookForm
:visible="dialogFormVisible"
:title="title"
:categories="categories"
:shops="shopLists"
:form="form"
@save-from="saveBook"
@close-dialog="dialogFormVisible = false"
></BookForm>
</div>
</template>
<script>
import axios from "axios";
import { mapGetters } from "vuex";
import { getBookLists } from "@/api/book.api";
import BookForm from './BookForm';
let formDefault = {
name: "",
author: "",
category: "",
status: true,
time: "",
publishers: "",
shops: []
};
export default {
name: "demoIndex",
components:{
BookForm
},
data() {
return {
title: '添加图书',
categories: this.$store.state.app.categories,
dialogFormVisible: false,
bookLists: [],
form: JSON.parse(JSON.stringify(formDefault))
};
},
computed: {
...mapGetters(["shopLists"]),
tableData() {
// 请注意这里的拷贝
let books = JSON.parse(JSON.stringify(this.bookLists));
return books.map(item => {
item.name = `《${item.name}》`;
item.category = this.categories.find(
cate => cate.value === item.category
)["label"];
return item;
});
}
},
created() {
this.getBookLists();
},
methods: {
handleClick(row) {
this.$router.push({ path: `/demo/detail/${row.id}` });
},
handleEditClick(row) {
let id = row.id;
let book = this.bookLists.find(item => item.id === id);
this.title = '编辑图书';
this.form = JSON.parse(JSON.stringify(book));
this.dialogFormVisible = true;
},
async getBookLists() {
let response = await getBookLists();
if (response.status) {
this.bookLists = response.data;
}
},
saveBook(params) {
console.log(params);
this.dialogFormVisible = false;
if(params.id){
this.bookLists = this.bookLists.map(item => {
if(item.id === params.id) return params;
return item;
})
this.title = "添加图书";
} else {
this.bookLists.push({ ...{ id: this.bookLists.length }, ...params });
}
}
},
filters: {
timeToCH(time) {
return (
time.substr(0, 4) +
"年" +
time.substr(5, 2) +
"月" +
time.substr(8, 2) +
"日"
);
}
}
};
</script>
Form vue
<template>
<el-dialog :title="title" :visible.sync="dialogVisible" :close-on-click-modal="false" width="700px" @close="closeDialog">
<el-form :model="editForm" ref="form">
<div class="el-form--inline">
<el-form-item label="书名" :label-width="formLabelWidth">
<el-input v-model="editForm.name" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="作者" :label-width="formLabelWidth">
<el-input v-model="editForm.author" autocomplete="off"></el-input>
</el-form-item>
</div>
<div class="el-form--inline">
<el-form-item label="出版社" :label-width="formLabelWidth">
<el-input v-model="editForm.publishers" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="出版时间" :label-width="formLabelWidth">
<el-date-picker
v-model="editForm.time"
type="date"
placeholder="选择日期"
value-format="yyyy-MM-dd"
></el-date-picker>
</el-form-item>
</div>
<div class="el-form--inline">
<el-form-item label="图书分类" :label-width="formLabelWidth">
<el-select v-model="editForm.category" placeholder="请选择图书分类">
<el-option
:label="item.label"
:value="item.value"
v-for="item in categories"
:key="item.value"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="是否上架" :label-width="formLabelWidth">
<el-switch v-model="editForm.status"></el-switch>
</el-form-item>
</div>
<div style="text-align: left;" v-if="editForm.status">
<el-form-item label="上架商城" prop="type" :label-width="formLabelWidth" style="width: 100%;">
<el-checkbox-group v-model="editForm.shops">
<el-checkbox
name="type"
v-for="shop in shops"
:label="shop.value"
:key="shop.value"
>{{shop.label}}</el-checkbox>
</el-checkbox-group>
</el-form-item>
</div>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="closeDialog">取 消</el-button>
<el-button type="primary" @click="saveForm">确 定</el-button>
</div>
</el-dialog>
</template>
<script>
export default {
name: 'bookForm',
props: {
title: {
type: String,
default: '标题'
},
visible: {
type: Boolean,
default: false
},
form: {
type: Object,
default: () => {
// 这里建议将表单项都列出来,1、添加没有数据时不会报错; 2、如果要移植到其他地方使用,可以很清楚的知道表单是做什么用的,有哪些字段 对比直接写{}的感受
return {
name: "",
author: "",
category: "",
status: true,
time: "",
publishers: "",
shops: []
}
}
},
// 类型列表作为prop属性的一部分传递进来
categories: {
type: Array,
default: () => [] // categories: [{ value: "computer", label: "计算机类" }] 预留一条数据是一个很好的习惯, 不管过了多久,看到组件一眼就明白了
},
shops: {
type: Array,
default: () => [] // shops: [{value: 1, label: '京东'}]
}
},
data() {
return {
formLabelWidth: "120px",
// 注意这里是必须的; 因为用到了element-ui,右上角的关闭按钮要操作弹窗的关闭,而真正控制弹窗显示的又是visible,所以用data里面的属性来接受prop里面的属性是有必要的(不能直接操作prop里面的属性值)
dialogVisible: this.visible || false,
// data里面的数据是我们真正操作的数据
editForm: JSON.parse(JSON.stringify(this.form))
}
},
methods: {
saveForm() {
this.$emit('save-from', this.editForm)
},
closeDialog() {
this.$emit('close-dialog');
}
},
watch: {
visible(val) {
this.dialogVisible = val;
if (val) {
this.editForm = JSON.parse(JSON.stringify(this.form));
}
}
}
}
</script>
<style lang="scss" scoped>
/deep/ .el-form--inline {
text-align: left;
.el-form-item__content {
width: 220px;
margin-left: 0 !important;
}
}
</style>
遇到的一些问题
vue-cli3取消eslint 校验代码
vue create app 创建项目适合选择了Linter / Formatter, 所以写代码的时候会有代码规范检查,如何才能关闭呢?
1、项目创建好后会生成.eslintrc.js文件
module.exports = {
root: true,
env: {
node: true
},
'extends': [
'plugin:vue/essential',
//'@vue/standard' // 这行注释掉就可以了
],
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-extra-semi': 'off'
},
parserOptions: {
parser: 'babel-eslint'
}
}
VSCode 里面vue模块快速生成
- 安装必要的插件:插件库中搜索Vetur,下图中的第一个,点击安装,安装完成之后点击重新加载
- 新建代码片段 文件-->首选项-->用户代码片段-->点击新建代码片段--输入框中输入vue,然后回车
- 粘贴代码格式
{
"Print to console": {
"prefix": "vue",
"body": [
"<template>",
" <div>$0</div>",
"</template>",
"",
"<script>",
"export default {",
" components: {},",
" props: {},",
" data() {",
" return {",
" };",
" },",
" watch: {},",
" computed: {},",
" methods: {},",
" created() {},",
" mounted() {}",
"};",
"</script>",
"<style lang=\"scss\" scoped>",
"</style>"
],
"description": "A vue file template"
}
}
vue 不同组件(兄弟关系)切换,保存在vuex中的属性,没有办法做到实时更新
` vue 的watch里面有同一个immediate: true 可以看一下`
使用vue-router做后退时,通过this.$router.back()
来做后退处理,发现点击时候没反应, 经排查,发现问题出在这里了
<a href="#" class="app-header-router-link" @click="goBack"><i class="icon icon-back"></i></a>
改写为下面的两种方式即可
<a href="#" class="app-header-router-link" @click.prevent="goBack"><i class="icon icon-back"></i></a>
或
<span class="app-header-router-link" @click.prevent="goBack"><i class="icon icon-back"></i></span>
遇到跨域项目,axios请求发送了两条,第一条Method为option,第二条Method为get, 查询后方知大家遇到这个问题的情况还比较多,现在做简单的分析:
1、原因
在正式跨域的请求前,浏览器会根据需要,发起一个“PreFlight”(也就是Option请求),用来让服务端返回允许的方法(如get、post),被跨域访问的Origin(来源,或者域),还有是否需要Credentials(认证信息);
跨域资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。
也就是说,它会先使用options去测试,你这个接口是否能够正常通讯,如果不能就不会发送真正的请求过来,如果测试通讯正常,则开始正常请求
2、解决方案
跨域请求需要先发一次Option预请求,OPTIONS是检验是否允许跨域的,如果不希望OPTIONS请求, 直接让后端遇到option直接返回就可以了,前端可不做处理;
- vue 开发下,通过代理请求后端接口, 返回307;
找后端处理一下吧,可能是后端做个哪个校验, 比如:请求接口时需要验证用户信息,发现session中没有userId便重定向到登录接口
可变高度textarea
$configTable.on("keydown paste cut drop", "textarea", function(){
delayedResize(this);
});
$configTable.on("change", "textarea", function(){
delayedResize(this);
});
function delayedResize (text) {
var scrollHeight = text.scrollHeight;
setTimeout(function(){
text.style.height = 'auto';
text.style.height = scrollHeight + 'px';
}, 0)
}
axios关于携带参数的问题
## 发送get请求
1、参数直接带在url上
axios.get('/user?Id=12345').then(function (response) {
// handle success
console.log(response);
})
2、以params参数对象传递
axios.get('/user', {
params: {
id: 12345
}
}).then(function (response) {
console.log(response);
})
3、request请求时
// "params"是与请求一起发送的URL参数
// 必须是纯对象或URLSearchParams对象
axios.request({
url: '/user',
method: 'get',
params: {
id: 12345
}
}).then(function (response) {
console.log(response);
})
## 发送post请求
1、axios.post简化方式请求
axios.post('/user', {
firstName: 'Fred',
lastName: 'Flintstone'
})
.then(function (response) {
console.log(response);
})
2、request请求时
// "data"是要作为请求正文发送的数据
// 仅适用于请求方法“PUT”、“POST”和“PATCH”
// 如果未设置“transformRequest”,则必须是以下类型之一:
// 字符串,普通对象,ArrayBuffer,ArrayBufferView,URLSearchParams
// 仅限浏览器:FormData、File、Blob
//-仅节点:流、缓冲区
axios({
method: 'post',
url: '/user/12345',
data: {
firstName: 'Fred',
lastName: 'Flintstone'
}
}).then(function (response) {
console.log(response);
});
需要注意的是, http请求中的body、query、params, 为了扩展方便,项目中通常会提交json对象作为参数, 这时很多小伙伴可能弄不清楚什么时候用data, 什么时候用params, 以至于前后端对接时经常出现后端接收不到前端请求参数的情况
后端接收以node的express框架为例
Method | 参数名 | 后端接受参数 | 说明 |
---|---|---|---|
get | params | req.params | 请求的参数,URL后面以/的形式,例:user/:id |
get | query | req.query | 请求的参数,URL后面以?的形式,例:user?id |
post | data | req.body | 请求体中的数据 |
// 请留意下面代码
export function getBookDetail2(params = {}){
return request({
url: `${apiAddressSettings.getBookDetail}`,
method: 'post', //
params: params // 请注意这里用的是params
});
};

关于带分页查询
在项目中,列表和查询几乎是管理系统中必不可少的一部分,在获取列表时我们通常会涉及到按当前页查询,根据某几个查询条件来查询列表, 需要注意的是查询条件和分页信息: 1、在分页查询时,需要查看是否有其他的查询信息 2、在点击按条件查询时,如果有分页请将分页的当前页数置位1(否则可出现分页信息有数据,但data无数据的情况)
关于前后端数据类型
在前后端分离项目中,后端主要业务是提供数据接口(返回数据),前端主要负责(获取数据)页面展示, 然如果前后端的编程语言不同,在处理数据类型方面往往会有所不同,这里重点关注一下:
String
Number
null
例如:
PHP返回 "true" "false" 在前端用于条件判断时, "false"就会被转换为布尔类型的 true
PHP 中NULL和null是一样的, 而js只能认识null
又好比,后端习惯用null表示数据为空, 而前端喜欢用 ''表示数据为空
, 如果接口处理不完善, 前端接受数据判断,后端接收参数很可能就会出现意想不到的报错, 甚至是找半天找不到问题的原因;
建议:
* 表示状态的 {status: true} 而非 {status: "true"}
* 表示数据的 {size: 100} 而非 {size: "100"}
* 表示数据空 {startTime: ''} 而非 {startTime: null }
[最关键的,开发前前端后端沟通一下,约定好数据格式、类型]
关于开始时间不得大于结束时间
在项目中根据时间范围查询可以说是太普遍了, 公司采用VUE + ElementUI的前端技术,这里重点看下ElementUI里面处理时间范围的办法:
- 直接使用范围类型
- 自定义处理
// 自定义处理的办法
1、整个项目中的时间范围, 表单、字段名称(约定好)保持统一, 例如:`查询表单searchForm`、`添加编辑表单form或editForm`, 表单里面的开始时间startTime、结束时间endTime; 【多人开发容易出现startTime和timeStart】
2、给时间输入框添加 picker-options(是个对象) 当前时间日期选择器特有的; 它里面有一个属性disabledDate(Function) "设置禁用状态,参数为当前日期,要求返回 Boolean"
如: :picker-options="pickerOptionsStart" 和 :picker-options="pickerOptionsEnd"
<el-form-item label="报备时间" prop="startTime">
<el-date-picker size="small" v-model="form.startTime" type="datetime" style="width:87%" placeholder="选择日期时间"
value-format="yyyy-MM-dd HH:mm:ss" :picker-options="pickerOptionsStart" />
<span style="margin: 0 5px;">至</span>
</el-form-item>
<el-form-item prop="endTime">
<el-date-picker size="small" v-model="form.endTime" type="datetime" placeholder="选择日期时间" value-format="yyyy-MM-dd HH:mm:ss"
:picker-options="pickerOptionsEnd" />
</el-form-item>
3、配置pickerOptionsStart 和 pickerOptionsEnd, 到这一步,不能选择的日期已经被置灰无法选择了
// 可以写在data里面,建议写在computed里面
computed: {
pickerOptionsStart() {
let _this = this;
return {
disabledDate(time) {
let selectItemTimeStamp = Date.parse(time);
let nowTimeStamp = Date.now();
if (_this.form.endTime) {
let endTimeStamp = Date.parse(_this.form.endTime);
return selectItemTimeStamp > endTimeStamp;
} else {
return selectItemTimeStamp > nowTimeStamp;
}
}
}
},
pickerOptionsEnd() {
let _this = this;
return {
disabledDate(time) {
let checkItemTimeStamp = Date.parse(time);
let nowTimeStamp = Date.now();
if (_this.form.startTime) {
let startTimeObj = new Date(_this.form.startTime);
// 如果是年月日时分秒的格式,需要允许结束时间和开始时间是同一天,在这里做了同一天处理(年月日的另当别论)
let timeDetailStamp = (startTimeObj.getHours() * 3600 + startTimeObj.getMinutes() * 60 + startTimeObj.getSeconds()) *
1000 + 1000;
let startTimeStatmp = Date.parse(_this.form.startTime);
return checkItemTimeStamp + timeDetailStamp < startTimeStatmp;
} else {
return checkItemTimeStamp > nowTimeStamp;
}
}
}
}
}
4、时间选择时根据开始时间和结束时间做判断
[:因为上面允许选择同一天,所以需要处理同一天的时段]
[:参考elelment-ui时间范围判断,如果选择的开始时间大于结束时间,则将结束时间设置为跟开始时间一样,反之亦然]
// 这里建议用监听,因为使用事件实在是太多了
watch: {
'form.startTime': function(val) {
let selectItemTimeStamp = Date.parse(val)
let startTimeStamp = Date.parse(this.form.startTime);
let nowTimeStamp = Date.now();
// 先与当前时间做比较(如果选择的开始时间 > 当前时间, 则将时间置为当前时间)
if (selectItemTimeStamp > nowTimeStamp) {
this.form.startTime = this.$moment.format('YYYY-MM-dd HH:mm:ss');
}
// 先判断有没有结束时间
if (this.form.endTime === '') {
this.form.endTime = val;
} else {
// 判断大小
let endTimeStamp = Date.parse(this.form.endTime);
if (startTimeStamp > endTimeStamp) {
this.form.endTime = val;
}
}
},
'form.endTime': function(val) {
// 先判断有没有结束时间
if (this.form.startTime === '') {
this.form.startTime = val;
} else {
// 判断大小
let startTimeStamp = Date.parse(this.form.startTime);
let endTimeStamp = Date.parse(this.form.endTime);
if (endTimeStamp < startTimeStamp) {
this.form.startTime = val;
}
}
}
}
// 完成以上代码,即可限制开始时间和结束时间的大小范围
然.....
然.....
每个页面里面都这么写岂不太累?
5、mixin来帮忙,建立公共方法文件, 重复的代码computed和watch放在这里,例如:
datetimeCompare.js
function formatTimes(time){
let yy = time.getFullYear();
let mm = time.getMonth() + 1;
let dd = time.getDate();
let week = time.getDay();
let hh = time.getHours();
let mf = time.getMinutes() < 10 ? "0" + time.getMinutes() : time.getMinutes();
let ss = time.getSeconds() < 10 ? "0" + time.getSeconds() : time.getSeconds();
return yy+'-'+mm+'-'+dd + ' '+hh+':'+mf+':'+ss;
}
export default {
computed: {
pickerOptionsStart() {
let _this = this;
return {
disabledDate(time) {
let selectItemTimeStamp = Date.parse(time);
let nowTimeStamp = Date.now();
if (_this.form.endTime) {
let endTimeStamp = Date.parse(_this.form.endTime);
return selectItemTimeStamp > endTimeStamp;
} else {
return selectItemTimeStamp > nowTimeStamp;
}
}
}
},
pickerOptionsEnd() {
let _this = this;
return {
disabledDate(time) {
let checkItemTimeStamp = Date.parse(time);
let nowTimeStamp = Date.now();
if (_this.form.startTime) {
let startTimeObj = new Date(_this.form.startTime);
let timeDetailStamp = (startTimeObj.getHours() * 3600 + startTimeObj.getMinutes() * 60 + startTimeObj.getSeconds()) *
1000 + 1000;
let startTimeStatmp = Date.parse(_this.form.startTime);
return checkItemTimeStamp + timeDetailStamp < startTimeStatmp;
} else {
return checkItemTimeStamp > nowTimeStamp;
}
}
}
}
},
watch: {
'form.startTime': function(val) {
let selectItemTimeStamp = Date.parse(val)
let startTimeStamp = Date.parse(this.form.startTime);
let nowTimeStamp = Date.now();
// 先与当前时间做比较(如果选择的开始时间 > 当前时间, 则将时间置为当前时间)
if (selectItemTimeStamp > nowTimeStamp) {
this.form.startTime = formatTimes(new Date());
}
// 先判断有没有结束时间
if (this.form.endTime === '') {
this.form.endTime = val;
} else {
// 判断大小
let endTimeStamp = Date.parse(this.form.endTime);
if (startTimeStamp > endTimeStamp) {
this.form.endTime = val;
}
}
},
'form.endTime': function(val) {
// 先判断有没有结束时间
if (this.form.startTime === '') {
this.form.startTime = val;
} else {
// 判断大小
let startTimeStamp = Date.parse(this.form.startTime);
let endTimeStamp = Date.parse(this.form.endTime);
if (endTimeStamp < startTimeStamp) {
this.form.startTime = val;
}
}
}
}
}
6、导入文件(或对象)并使用,如:
import datetimeCompare from '@/utils/datetimeCompare'
export default {
mixins:[datetimeCompare],
data() {
...
},
methods: {
....
}
7、至此,时间范围限制就完成了;
时间选择框上的这一句要记得加哦
开始时间 :picker-options="pickerOptionsStart"
结束时间 :picker-options="pickerOptionsEnd"