基于vue.js的渐进式组件尝试

我们有个内部运营系统,是基于keenthemes的一个主题进行开发的,而这个主题就是基于jQuery+bootstrap+jQueryPlugins 进行的定制主题,用于显示各种图表和曲线。所以,这个系统的特点就是,加载了一堆js和css进行堆砌组合,以及内容被一层层的标签和样式包围。长这个样子:

这种写多了确实就是体力活,一般的开发过程也就是复制粘贴,而且为了不出意外的问题,有用的没用的js script和css link都是直接复制的,反正放内部用一般忽略加载的延迟。

所以,有没有办法把各种标签打包成一个新的标签,css和js的依赖也打包在一块呢?就像html提供的基础标签一样,放个图片,那放个img就可以了。

这个肯定是有的,痛的人那么多,所以现在已经web components草案在讨论中,chrome等现代浏览器也相继地提供了shadow DOM, custom Elements的特性支持,google还推出了polymer项目。不过说实话,要是一个项目从头开始折腾,还是可以考虑的,但是一想到又要用npm安装一堆依赖,也是头大。

我需要的方案是,在已有的项目上,门槛低点,依赖很少的东西,还能包容已有的开发模式。比如说,我就把一堆标签用一个新的标签替代,然后解析页面的执行js脚本还原回来,这是最基本的一步。

在我有限的认知里,vue.js就是最简单的满足需求的选择。为什么不用react?一出来就令人惊呼的jsx,我还是嫌依赖太多。我就想要一种old school的方式,引用一个js,然后马上写,随便写。而且,vue.js提供的双向绑定功能也很适合,不用满个页面里写id然后脚本里再去各种引用。还有一点,运营系统天生以页面为模块划分,引入的js只充当controller的角色就可以了。

以datepicker的jQuery插件为例,下面代码放components.js里:

Vue.component('datepicker', {
    template: '\
        <div class="input-group input-small date" data-date-format="yyyy-mm-dd" :data-date-end-date="enddate">\
            <input type="text" class="form-control" readonly="">\
            <span class="input-group-btn">\
                <button class="btn default" type="button">\
                    <i class="fa fa-calendar"></i>\
                </button>\
            </span>\
        </div>\
    '
})

先假设页面上已经加载了需要的css和js,那么在页面上就可以直接使用

<datepicker></datepicker>

而我们除了需要加载components.js和vue.js之外,其它照旧。当然就是包含datepicker标签的元素需要加载到一个Vue实例中。

然后,再加强对这个标签的控制,比如说传入值,获取值以及对于datepicker事件的处理等,使得它功能更加完整。

Vue.component('datepicker', {
    props: ['value'],
    template: '\
        <div ref="picker" class="input-group input-small date" data-date-format="yyyy-mm-dd">\
            <input type="text" class="form-control" readonly="" :value="value">\
            <span class="input-group-btn">\
                <button class="btn default" type="button">\
                    <i class="fa fa-calendar"></i>\
                </button>\
            </span>\
        </div>\
    ',
    mounted: function() {
            var self = this;
            $(this.$refs.picker).datepicker({orientation: "right top", autoclose: true});
            $(this.$refs.picker).on('changeDate', function(e) {
                self.$emit('input', e.format('yyyy-mm-dd'));
            })
        }
})

以上示例代码中,模板新加入ref属性,就可以通用this.$refs引用原始的DOM节点,而props数据value的传入以及input事件的触发,则是为了实现神奇的 v-model,看:

<datepicker v-model='selectedDate'></datepicker>

如此一来就对datepicker父组件的 selectedDate 实现了双向绑定。其实v-model也只是个语法糖,展开来,其实就是:

<datepicker :value='selectedDate' @input='selectedDate=arguments[0]'></datepicker>

另外,示例代码中是在Vue实例的生命周期的mounted阶段(DOM节点挂载完成)进行了事件绑定,这是为了确保编译后节点的已经正常存在。

然后,到这里,仍然是基于页面上已经手动加载了依赖的css和js,这个组件其实还不算完整。事实上,我们还希望能够只要引用这个组件,依赖也要自然地满足。而这个,无非就是在合适的时候把依赖的css和js动态加载进来。这个“合适的时候”我仍然选择的是"mounted"阶段,为什么?感觉自然而然呀。

可是,动态加载CSS和JS的难点其实是,如何判断已经资源加载完成?兼容性仍然是个问题。所以,我又假设了,我们就只使用chrome吧~~ 理想的情况是,加载的资源并行请求,然后渲染执行的时候则按先后顺序,这明显没那么完美的事情。所以,对于CSS文件,我仍然并行加载,那么依赖先后顺序的样式有可能有问题,要保证顺序只能串行化,而且由于浏览器缓存的存在,在我有限的测试次数中,肉眼上还没有看出问题。而js的话就不得不优先考虑加载顺序的问题了,所以最后选择串行加载,而且是忽略了失败的情况。

解决依赖这种事情,是很个组件都需要的功能,所以采用了mixin, 可以大大地减少重复代码,看起来就像是声明了一个接口,有依赖的组件只要按需实现即可:

Vue.component('datepicker', {
    mixins: [DepMixins],
    props: ['value'],
    template: '\
        <div ref="picker" class="input-group input-small date" data-date-format="yyyy-mm-dd">\
            <input type="text" class="form-control" readonly="" :value="value">\
            <span class="input-group-btn">\
                <button class="btn default" type="button">\
                    <i class="fa fa-calendar"></i>\
                </button>\
            </span>\
        </div>\
    ',
    methods: {
        needDeps: function() {
            var deps = ['/assets/global/plugins/bootstrap-datepicker/css/bootstrap-datepicker3.min.css',
                        '/assets/global/plugins/bootstrap-datepicker/js/bootstrap-datepicker.min.js'];
            return deps;
        },
        loadedDeps: function() {
            var self = this;
            $(this.$refs.picker).datepicker({orientation: "right top", autoclose: true});
            $(this.$refs.picker).on('changeDate', function(e) {
                self.$emit('input', e.format('yyyy-mm-dd'));
            })
        }
    }
})

而DepMixins长这样子:

var DepMixins = {
    mounted: function() {
        if (!this.needDeps) return;
        var deps = this.needDeps();
        if (!deps) return;

        var cb = this.loadedDeps;
        var compName = Vue.util.formatComponentName(this);
        if (typeof DepMixins.comps[compName] === 'undefined')
            DepMixins.comps[compName] = [cb];
        else if (DepMixins.comps[compName] === 'loaded')
            return cb && cb();
        else
            return DepMixins.comps[compName].push(cb);

        // 假设css都在前面,而js是按照依赖顺序排列在后面
        for (var i=0; i<deps.length; i++) {
            var dep = deps[i];
            if (dep.endsWith('.css')) {
                visd.loadCSS(dep);
                continue;
            }

            next(i);
            break;
        }

        function next(i) {
            var dep = deps[i];
            if (!dep) return clearQueue();

            visd.cachedScript(dep).then(function() {
                return next(i+1);
            });
        }
        // will only called once
        function clearQueue() {
            var list = DepMixins.comps[compName];
            // if (list === 'loaded') return;
            for (var i=0; i<list.length; i++)
                list[i] && list[i]();
            DepMixins.comps[compName] = 'loaded';
        }
    }
}
DepMixins.comps = {};

可以看到,我又偷了懒,只完成了核心功能,连css和js的样式都只是自己约定了一下。visd.loadCSS和visd.cachedScript分别只是普通的加载CSS和JS的函数包装。

visd.loadCSS = function(src) {
    $("<link>").attr({rel: 'stylesheet', type: 'text/css', href: src}).appendTo(document.head);
}
visd.cachedScript= function(url, options) {
  // allow user to set any option except for dataType, cache, and url
  options = $.extend(options || {}, {
    dataType: "script",
    cache: true,
    url: url
  });

  // Use $.ajax() since it is more flexible than $.getScript
  // Return the jqXHR object so we can chain callbacks
  return jQuery.ajax(options);
};

最后,这个datepicker组件就算完成了。只需要新增加一个vue.js的依赖,而且还减少了页面上其它不明所以的资源文件引用,其它照旧,就算来个后台同学来看html代码,相信都能看懂,能手写。原有的开发环境也不需要任何更新,只用文本编辑器也照样敲代码。

再来两个经典例子:

watch字段的经典在于,模板中并没有引用到rows这个变量,那么vue实例也就不会把它加入watch列表,当父组件传入的rows变化的时候,data-table组件什么都不知道也就不会更新了,所以需要手动添加到watch列表中。

kee-modal中使用 了slot 标签,叫做内容分发,是web components spec的一个proposal(不会翻译),用于组件中的组合,也就是说我可以这样子用keen-modal:

<keen-modal>
    <div>想放点什么?</div>
    <datepicker></datepicker>
</keen-modal>

果然很方便!

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏http://www.cnblogs.com

内置函数filter()和匿名函数lambda解析

一.内置函数filter filter()函数是 Python 内置的一个高阶函数,filter()函数接收一个函数 f 和一个list,这个函数 f 的作用是...

30512
来自专栏IMWeb前端团队

:before,:after伪元素妙用

本文作者:IMWeb 黎清龙 原文出处:IMWeb社区 未经同意,禁止转载 这两个伪元素分别表示元素内容的【前】【后】,利用这两个伪元素可以在元素内容...

20710
来自专栏阮一峰的网络日志

关于URL编码

一般来说,URL只能使用英文字母、阿拉伯数字和某些标点符号,不能使用其他文字和符号。比如,世界上有英文字母的网址"http://www.abc.com",但是没...

1133
来自专栏日常学python

写 Python 时的 5 个坏习惯

很多文章都有介绍怎么写好 Python,我今天呢,相反,说说写代码时的几个坏习惯。有的习惯会让 Bug 变得隐蔽难以追踪,当然,也有的并没有错误,只是个人觉得不...

795
来自专栏机器学习算法与Python学习

写 Python 时的 5 个坏习惯,你有几条?

很多文章都有介绍怎么写好 Python,我今天呢,相反,说说写代码时的几个坏习惯。有的习惯会让 Bug 变得隐蔽难以追踪,当然,也有的并没有错误,只是个人觉得不...

614
来自专栏九彩拼盘的叨叨叨

写出好的前端代码不是件容易事

什么样的代码算是好代码? 在我看来,易于维护的代码就是好代码。当然代码还可以从性能,安全等方面来考量。这些不在本文的讨论范围之内。

653
来自专栏C语言及其他语言

C语言第一个简单实例

在信息化、智能化的世界里,可能很早很早 我们就听过许多IT类的名词,C语言也在其中,我们侃侃而谈,到底C程序是什么样子?让我们先看简单的一个例子: #inclu...

3256
来自专栏奇点大数据

Scala语言学习笔记一

Scala是一门小众的语言,但是作者因为工作原因要以Spark作为工作中的一个重心,而Spark采用了Scala语言编写,于是萌生了认真学习Scala的念头,在...

3564
来自专栏Crossin的编程教室

浅谈 Python 2 中的编码问题

Python 2.x 里的编码实在是一件令人烦躁的事情。不断有初学者被此问题搞得晕头转向。我自己也在很长一段时间内深受其害,直到现在也仍会在开发中偶尔被坑。在本...

33914
来自专栏Android开发经验

无意间遇到的TextView的一个坑

1224

扫码关注云+社区