Vue.js 初体验

前言

最近工作中的一个小项目需要做一个车辆信息选择框的三级联动,有点类似省市县的三级联动,下一级依赖上一级的数据。最后的效果是这样
car_selector
因为这一块的代码业务逻辑相当复杂,采用jQuery写的话代码量有点大,而且会把html的代码写在js文件里,不易于阅读和后期维护,所以CTO大大建议用Vue.js来重写。

思考过程

  1. 首先是爬取车辆信息的数据,找了一家信息比较全的相关平台,开始了爬虫之旅,待有空写下爬虫的思考,这里略过
  2. 拿到大概2万多条数据后,写一个pipeline整理好数据并写入自己的数据库,数据分为3类:品牌、系列、车型,存入3张表里,分别命名为car_brands, car_series, car_models,哦了~有了数据就可以动手了
  3. 后端写好三个handler逻辑,将数据以json的方式传给前端

用 jQuery写

前端前端来接受我大后端的数据吧~,以下是我之前写的jQuery版本的代码

  1. 第一个focus事件 点击input框框弹出modal选择框,并向后台请求car_brands数据,成功后render到modal里
  2. 第二个click事件 选择第一列品牌后,向后台请求car_series数据,成功后render出该品牌下所有的车系
  3. 第三个click事件 选择第二列车系后,向后台请求car_models数据,成功后render出该车系下所有的型号
  4. 第四个click事件 选择第三列车型后,将确定button激活
  5. 第五个click事件 点击保存按钮后 隐藏modal框,将选好的信息写入input框框,用blur方法激活formvalidation表单校验
$("#select-car").on("focus", 'input', (function () {
    $('#carModal').modal('show');
    $("#choose-brand > .choose-box").html('');
    $("#choose-series > .choose-box").html('<div class="cont-default" data-code="" data-name="">请先选择品牌</div>');
    $("#choose-model > .choose-box").html('<div class="cont-default" data-code="" data-name="">请先选择车系</div>');
    $('#save-car').prop('disabled', true);
    $.ajax({
        url: "/borrow/release/car_brands",
        dataType: "json",
        success: function (data) {
            $(".brand-cata").html("");
            $.each(data, function(k, v){
                $(".brand-cata").append("<li>" + v.cata + "</li>");
                $("#choose-brand > .choose-box").append("<div class=cont-tit data-name='" + v.cata + "'>" + v.cata + "</div>");
                $.each(v.brands, function(k, v){
                    var b = $("<div class='choose-cont' data-code='" + v.brand_code + "' data-name='" + v.en_name + "'>" + v.en_name + "</div>");
                    $("#choose-brand > .choose-box").append(b);
                })
            });
            $(".brand-cata li").on("click", function () {
                var e = $(this).html(),
                    t = $("#choose-brand .choose-box .cont-tit[data-name=" + e.toUpperCase() + "]");
                t.length && t.parent().animate({scrollTop: t.parent().scrollTop() + (t.offset().top - t.parent().offset().top)})
            });
        },
        error: function (data) {},
        fail: function (data) {}
    });
}));


var loading_flag = false;
$("#choose-brand").on("click", ".choose-box > .choose-cont", function () {
    if (loading_flag == true) {
        return false
    }
    $("#choose-series > .choose-box").html('<div class="cont-default" data-code="" data-name="">请先选择品牌</div>');
    $("#choose-model > .choose-box").html('<div class="cont-default" data-code="" data-name="">请先选择车系</div>');
    $('#save-car').prop('disabled', true);
    $(".choose-cont").removeClass("active");
    $(this).addClass("active");

    $.ajax({
        url: "/borrow/release/car_series",
        data: {'brand_code': $(this).attr('data-code')},
        dataType: "json",
        beforeSend: function () {
            loading_flag = true;
        },
        success: function (data) {
            loading_flag = false;
            $("#choose-series > .choose-box").html("");
            $.each(data, function (k, v) {
                $("#choose-series > .choose-box").append("<div class=cont-tit>" + v.factory + "</div>");
                $.each(v.series, function(k, v) {
                    var s = $('<div class="choose-cont" data-code="' + v.series_code + '" data-name="' + v.en_name + '">' + v.en_name + "</div>");
                    $("#choose-series > .choose-box").append(s);
                })
            });
        },
        error: function (data) {
        },
        fail: function (data) {
        }
    })
});

$("#choose-series").on("click", ".choose-box > .choose-cont", function () {
    if (loading_flag == true) {
        return false
    }
    $("#choose-model > .choose-box").html('<div class="cont-default" data-code="" data-name="">请先选择车系</div>');
    $("#choose-series > .choose-box > .choose-cont").removeClass("active");
    $('#save-car').prop('disabled', true);
    $(this).addClass("active");
    $.ajax({
        url: "/borrow/release/car_models",
        data: {'series_code': $(this).attr('data-code')},
        dataType: "json",
        beforeSend: function () {
            loading_flag = true;
        },
        success: function (data) {
            loading_flag = false;
            $("#choose-model > .choose-box").html("");
            $.each(data, function (k, v) {
                $("#choose-model > .choose-box").append("<div class=cont-tit>" + v.year + '款' + "</div>");
                $.each(v.models, function(k, v) {
                    var m = $('<div class="choose-cont" data-code="' + v.code + '" data-name="' + v.en_name + '">' + v.en_name + "</div>");
                    $("#choose-model > .choose-box").append(m);
                })
            });
        },
        error: function (data) {
        },
        fail: function (data) {
        }
    })
});

$("#choose-model").on("click", ".choose-box > .choose-cont", function () {
    $("#choose-model > .choose-box > .choose-cont").removeClass("active");
    $(this).addClass("active");
    if ($("#choose-model > .choose-box > .choose-cont.active").length === 1) {
        $("#save-car").prop("disabled", false)
    }
});

$("#save-car").on("click", function () {
    var car_brand = $('#choose-brand > .choose-box > .choose-cont.active').text(),
        car_series = $("#choose-series > .choose-box > .choose-cont.active").text(),
        car_model = $("#choose-model > .choose-box > .choose-cont.active").text();
    $("#carModal").modal("hide");
    $("#brand").val(car_brand).blur();
    $("#brand-model").val(car_series).blur();
    $("#car-model").val(car_model).blur();
    $("#select-car").off("focus", "input");
    $("#select-car").on("focus", 'input', function(){
        $('#carModal').modal('show')
    })
});

如果我是刚接手这个项目的码农话,看到这个代码简直要掀桌(ノ=Д=)ノ┻━┻!!!估计就算自己写的过几个月也看不懂了。
是时候祭出vue了!!!

用 Vue.js重写

过了一遍vue的官方文档,发现有以下几个问题需要解决:

  1. 解决tornado 和 vue expression 的冲突

    因为我们用的是tornado,前端不仅有tornado render来的数据,还有vue的数据,如果都用{{ }}会造成冲突的。google+stackoverflow了一波,发现可以改写vue expression的全局配置。

    Vue.config.delimiters = ['${', '}']
    

    将vue expression 改写成${ }

    http://vuejs.org/api/#delimiters。

  2. 第二个问题就是vue 内部如何传值

    官方例子中给了我们答案,直接 this.foo = bar就可以改变data里的数据了,简直方便的不要不要的

    http://vuejs.org/examples/commits.html

看看vue.js 的代码,

  1. 改写全局配置,用${ }代替默认的{{ }}expression
  2. 设置loading_flag防止多次ajax请求
  3. 新建一个vue的实例,里面包含6条数据和5个方法,他们必须在#select-car那个div里面,否则不能解析

    • brand_items, series_items, model_items 用来保存后端传来的json数据
    • chosen_brand, chosen_series, chosen_model 用来保存用户当前选择的数据
    • show_modal 方法就是jQuery版本中的select-car事件,请求car_brands数据,传给brand_items,其他的方法也类似
Vue.config.delimiters = ['${', '}'];
var loading_flag = false;
var vue_show = new Vue({
    el: '#select-car',
    data: {
        brand_items: '',
        series_items: '',
        model_items: '',
        chosen_brand: '',
        chosen_series: '',
        chosen_model: ''
    },
    methods: {
        show_modal: function () {
            $('#carModal').modal('show');
            var self = this;
            $.ajax({
                url: "/common/car_brands",
                dataType: "json",
                success: function(data){
                    self.brand_items = sort_brands(data);
                }
            })
        },
        select_brand: function (e) {
            var self = this;

            if (loading_flag == true) {
                return false
            }

            var $target = $(e.target);
            self.chosen_brand = $target.attr('data-name');

            loading_flag = true;
            $.ajax({
                url: "/common/car_series",
                data: {'brand_code': $target.attr('data-code')},
                dataType: "json"
            }).done( function (data) {
                self.model_items=[];
                self.series_items = sort_series(data);
                self.chosen_series = '';
                self.chosen_model = '';
            }).always(function (data) {
                loading_flag = false
            });
        },
        select_series: function (e) {
            var self = this;
            if (loading_flag == true) {
                return false
            }

            var $target = $(e.target);
            self.chosen_series = $target.attr('data-name');

            loading_flag = true;
            $.ajax({
                url: "/common/car_models",
                data: {'series_code': $target.attr('data-code')},
                dataType: "json"
            }).done( function (data) {
                self.model_items = sort_models(data);
                self.chosen_model = '';
            }).always(function (data) {
                loading_flag = false
            });
        },
        select_model: function (e) {
            var self = this;
            self.chosen_model = $(e.target).attr('data-name');
        },
        scroll_brands: function (e) {
            var s = $(e.target).html(),
                t = $("#choose-brand .choose-box .cont-tit[data-name=" + s.toUpperCase() + "]");
            t.length && t.parent().animate({scrollTop: t.parent().scrollTop() + (t.offset().top - t.parent().offset().top)})
        }
    }
});

从vue js 中可以看出逻辑清晰,数据和方法显示直观,几乎没什么html代码,因为都写在html页面中了,再来看看html中的vue如何写

  1. vue 根据el: '#select-car'来判断这个div是不是vue
  2. input 框绑定了show_modal的click事件,{{ form.car_model }}是tornado 传来的别的数据 和vue无关,不要在意
  3. 字母列表v-for方法循环了brand_items,将左侧的字母render出来,并绑定了scroll_brands click事件
  4. 品牌列表template v-for方法循环了brand_items,用v-bind:class写个三元表达式来判断是不是被选中。template v-for下还有第二层循环v-for,他绑定了select_brand的click事件
  5. 车系列表车型列表品牌列表类似
  6. 确定button 用:disabled写个三元表达式来判断是否选了model,以激活确定button

这里说下template v-forv-for 的区别:

  • v-for只循环自身,不循环别的元素
  • template v-for循环包裹着的所有元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<div id="select-car" class="select">
...
<input type="text" class="form-control" name="car_model" id="car-model" placeholder="点击选择车型"
autocomplete="off" readonly value=`{{ form.car_model }}` style="cursor: pointer" @click="show_modal">
<!-- Modal -->
...
<div class="open-tit">请选择品牌</div>
<ul class="col-xs-1 brand-cata">
<!--字母列表-->
<li v-for="item in brand_items" @click="scroll_brands">
${ item.cata }
</li>
</ul>
<div class="col-xs-10 choose-box">
<!--品牌列表-->
<template v-for="item in brand_items">
<div class=cont-tit data-name="${ item.cata }">
${ item.cata }
</div>
<div class="choose-cont" v-bind:class="[chosen_brand==b.en_name ? 'active' : '']" data-code="${ b.brand_code }" data-name="${ b.en_name }"
v-for="b in item.brands" @click="select_brand">
${ b.en_name }
</div>
</template>
</div>
...
<div class="open-tit">请选择车系</div>
<div class="choose-box">
<!--车系列表-->
<div class="cont-default" data-code="" data-name="" v-bind:class="[chosen_brand ? 'none' : '']">请先选择品牌</div>
<template v-for="item in series_items">
<div class=cont-tit>${ item.factory }</div>
<div class="choose-cont" v-bind:class="[chosen_series==s.en_name ? 'active' : '']" data-code="${ s.series_code }" data-name="${ s.en_name }"
v-for="s in item.series" @click="select_series">
${ s.en_name }
</div>
</template>
</div>
...
<div class="open-tit">请选择车型</div>
<div class="choose-box">
<!--车型列表-->
<div class="cont-default" data-code="" data-name="" v-bind:class="[chosen_series ? 'none' : '']">请先选择车系</div>
<template v-for="item in model_items">
<div class=cont-tit>${ item.year + '款' }</div>
<div class="choose-cont" v-bind:class="[chosen_model==m.en_name ? 'active' : '']" data-code="${ m.code }" data-name="${ m.en_name }"
v-for="m in item.models" @click="select_model">
${ m.en_name }
</div>
</template>
</div>
...
<button type="button" class="btn btn-primary" id="save-car" :disabled="chosen_model ? false : true">确定</button>
...
</div>

总结

  • Vue.js 仅仅是MVVM中的V层,他设计简单,学习周期短,可以快速投入到单页面中
  • 指令和组件分很清晰,有自己的视图和数据逻辑
  • 有更好的性能,没有脏检查,并且非常非常容易优化

感谢

  • Vue.js官方 http://vuejs.org/
  • CTO大大的强力助攻,和这么多优秀的小伙伴一起共事简直太棒了~