前后端分离那些事--Vue

4,450 阅读18分钟

话说"天下大事 合久必分分久必合", WEB开发也是如此; 从过去的JSP、PHP到现在的vue+后端、react+后端、angular+后端... 前后端分离的开发模式已经有好多个年头了,在此期间诞生了很多优秀的前端框架、后端框架、开发模式; 然而,现在服务端渲染又再次被很多人提起,应用在各种项目中;相信伴随硬件技术和网络的发展,web开发又将在某个节点走向前后端统一

今天的主题是前后端分离(Vue+后端),但是我想还是从前后不分离的维度去看问题可能会更明了;

"前端前端我是前端 后端后端我也是前端" "后端后端我是后端 前端前端我也是后端"

个人感觉要做好前端端分离模式下的前端开发,应该要懂一些后端的相关基础知识; 这里主要探讨以下几个(前后端分离项目中的)话题:

  • 跨域问题

  • 接口、数据规范

  • 开发环境下数据对接

  • vue三种数据、三个变化

  • 项目实践

    • 添加vueDevtools控制台开发工具
    • 关于表单数据重置
    • echarts组件封装与数据展示示例
    • 处理Promise异步接口响应
    • 关于过大文件提取为组件的建议
  • 遇到的一些问题

    • vue-cli3取消eslint 校验代码
    • axios请求发送了两条,第一条Method为option,第二条Method为get
    • vue 开发下,通过代理请求后端接口, 返回307
    • axios关于携带参数的问题
    • 关于带分页查询
    • 关于前后端数据类型
  • 寄语

跨域问题

首先我们应该知道,跨域仅存在于浏览器端;是受到浏览器同源策略的影响;解决跨域有多种方案,(可参考跨域解决方案), HTTP请求头需要了解一下 这里主要看我们自己项目上的两种解决方案;

开发环境下的解决方案:

  1. 服务端设置, 若服务端设置成功,浏览器控制台则不会出现跨域报错信息,如
// 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");
    
    // ....
 });
 
 // 其他编程语言的自行查阅
  1. 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': ''
        }
      }

    }
  }
}

部署解决方案

  1. nginx反向代理接口跨域

    跨域原理: 同源策略是浏览器的安全策略,不是HTTP协议的一部分。服务器端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要同源策略,也就不存在跨越问题。

  2. 实现思路:通过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里面的数据,可采用如下形式:

  1. 返回大批量的列表
{
    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            // 总页数
        }
    }
}
  1. 返回对象(如个人信息详情)
{
    status: true,
    msg: 'success',
    data: {
        name: '迈克尔乔丹',
        age: 30
    }
}

// 如果信息项可能有时有,有时无, 建议前端将必须展示的字段一一列举(给数据占个坑), 可避免不必要的错误提示,如:
{
    book: {
        name: '',
        author: '',
        category: '',
        publishers: '',
        time: ''
    }
}
  1. 返回如列举型的对象,如同时列举学校信息、班级信息、个人信息
{
    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错误
  1. 返回如类型列表状的数组
{
    status: true,
    msg: 'success',
    data: [
        {value: 1, label: '热力图'},
        {value: 2, label: '卫星图'}
    ]
}
  1. 返回图表数据(此处建议前端提前提供数据格式给服务端)
{
    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监听

三者不同之处:

  1. data里面的数据是唯一的可以直接设置、赋值的数据
  2. computed可根据prop、data、其他computed计算属性的值变化而变化; 可以设置但不要设置它的值
  3. data、computed都位于组件本身之内, prop通过父组件传入
  4. prop属性的修改之内通过修改父组件中的对应数据修改(因为Vue同React一样都是单向数据流)
  5. prop属性可以直接赋值给data里面的属性作为组件的初始值
  6. 如果要对接口返回的数据加以处理后再使用,可以1、将数据格式化后再赋值给data; 2、使用computed,在模板中使用computed处理后的数据

项目实践

添加vueDevtools控制台开发工具

俗话说工欲善其事必先利其器,Vue开发必然离不开vue的开发调试工具; 控制台对于前端开发而言是极为重要的,善于调试将使我们的开发事半功倍,下面介绍安装一款vue控制台调试插件:

作为伸手党,网上有小伙伴将配置好的工具献给了广大伸手党,在这里先行谢过; 下载地址: vue-devTools工具安装包 开始安装吧: 1、打开google浏览器,输入地址chrome://extensions/ ,点击加载已解压的扩展程序

2、进入到Chrome文件夹下,点击确定;然后我们就看到了安装好的界面 [包里面把适配所有的浏览器源文件都安装了,我们只需要要chrome浏览器的源码就行!再次致敬大佬] 原文地址
3、完了之后会发现链接栏的右边对出了V标志,证明已经成功; 此时会有很多人F12之后在调试界面依然看不到Vue,可能是因为程序中引用的vue文件是一个vue.min.js文件,这个压缩文件是默认不支持调试的; 如果还不行,可以尝试下关闭浏览器再打开

关于表单数据重置

表单初始化、表单赋值、表单重置在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模块快速生成

  1. 安装必要的插件:插件库中搜索Vetur,下图中的第一个,点击安装,安装完成之后点击重新加载
  2. 新建代码片段 文件-->首选项-->用户代码片段-->点击新建代码片段--输入框中输入vue,然后回车
  3. 粘贴代码格式
{
	"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
    });
};

![如图](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/5/22/172381244a9ccd96~tplv-t2oaga2asx-image.image)

关于带分页查询

在项目中,列表和查询几乎是管理系统中必不可少的一部分,在获取列表时我们通常会涉及到按当前页查询,根据某几个查询条件来查询列表, 需要注意的是查询条件和分页信息: 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"

寄语

路漫漫其修远兮 吾将上下而求索