关于系统组件的使用 - fengliu222/mall-platform GitHub Wiki

在写组件加载器的时候,时常惶恐自己是否对这个项目过度设计了。后来我一想,就我这点能耐,也许想破了天都无法过度设计,所仔细的归纳了一些场景,终于踏实的继续的写了下去。

####回顾以往

在DJF开发的过程中,组件封装的粒度非常大,导致很多可以复用的部分无法复用,因为不敢动老代码的原因,所以每次用到相同的组件,都要重新写一次,比如数量控制组件,它拥有三个元素:

<!-- 加号 -->
<div class="btn-minus">-</div>
<!-- 输入框 -->
<input type="text" value="0" />
<!-- 减号 -->
<div class="btn-plus">+</div>

然后,在JS中,会为加号减号和输入框绑定一些事件,以达到数量增减、数值校验等功能。

$(".btn-minus").bind(xxx)
$(".btn-plus").bind(xxx)

我们意识到这样是不可取的,这些可以重复利用的功能应当被封装成组件。那么如下的组件就产生了,它的使用方法如下

new numberHandle({
   plus:$(".xx")
   minus:$(".xx")
})

将相应元素传入进去,组件可以正确的为DOM元素绑定事件。那么懒惰的前端们又觉得麻烦啦,我需要每次都传一个DOM Selector,写$的感觉很不好。因为组件的DOM元素是不会改变的,所以将组件模板化是一个好主意,那么也许就出现了如下代码,下面这个代码也就是alare的widget中,创建一个组件的代码

new numberHandleWidget({
   element:$(".xxx")
})

只要将这个组件的包裹元素作为参数传入,就可以自动的生成模板,并为其绑定事件。此时我们使用一个组件,HTML中只要这么写

<div class="xxx"></div>

此时我觉得还是有些不爽,还是有些麻烦,因为还是要写一个$,而且还要new。还能再智能吗?Angular可以为啥我们不行?于是我写了一个组件加载器,让我们使用组件,就如同使用directive一样简单

<div class="xxx" data-component="numberHandle" data-require-id="rose/numberHandle"></div>

当页面被加载,相应的Numberhandle就会被渲染了,就是这么简单,再也不用new了,也不需要传入相应DOM节点,一切都是自动的。 那么我如果想使用这个组件暴露的接口,该如何操作呢,在JS中,可以用如下代码:

var rose_number_handle = seajs.use("rose/numberHandle"); //rose/numberHandle 就是定义在DOM中的data-require-id
rose_number_handle.attrs("a"); 

PS. data-require-id是不允许重复的,因为程序根据这个ID为组件提供单例。 ####使用方法

assets/modules/componentsLoader模块中,我编写了一个组件加载器(后面简称loader)。Loader会在DOMContentLoaded事件被触发时被调用。它的目的是找到所有含有data-component属性的DOM节点,根据这个data-component的属性值来加载一个系统组件。我们在相应的页面模块中写入如下代码:

seajs.use("此处是相应DOM节点的data-require-id属性值",function(obj){
   console.log(obj.element);
   //这个模块的自定义逻辑
})

根据上面的代码,我们可以自由的对模块返回的obj进行自定义的逻辑了,在HTML中它的样子是这样的:

<div class="aa" data-component="charactor.list" data-require-id="charactor/list"></div>

这种写法和处理方式我粗劣的借鉴了angular中directive的使用方式。那么它费了这么大劲到底要处理什么场景的需求呢,它又是如何让开发变得更加顺畅的呢,我细细说一下。

####实现原理

若你看了componentsLoader的源码,就会发现,其实原理非常简单: 当loader找到了含有data-component属性的DOM节点,它会遍历这个节点数组,读取数组中的每一个data-component属性。这个属性就是我们开发组件时,为组件起的文件名(即模块名),按照我们的约定,组件都是写在modules文件夹下的,所以loader默认会去这个文件夹下找与data-component模块同名的模块,代码如下:

require.async(moduleSrc,function(obj){
   ...
})

在找到这个模块后,loader会根据节点的data-require-id属性来定义一个新的模块。

define(compoID,[],function(require,exports,module){
    module.exports = obj({
	element:$(compoArr[i])
    });
)};

可以看到,在上面的代码中,loader定义的模块名,是根据它找到的require-id进行命名的,这个新创建的模块暴露出的借口就是你需要的组件的模块的实例(有些难懂,其实就是new了一个模块暴露出的函数)。

这里把思路放到这个实例中,这个实例是什么呢? 答:它是一个widget的实例。关于widget,可以去arale的widget文档了解一下。 我们在使用arale的widget库时,可以这样定义一个widget:

var List = new Widget.extend({
    element:$("xxx")
})

var list1 = new List(opts);

widget约定了组件的基本生命周期,换成人话就是————这个组件被widget承包了。 当我考虑到组件复用的时候,我就不禁想到,如果将loader与widget组合在一起使用,那么这个组件的生命周期,不仅更加完整,而且更加提前了! 它会根据程序传入的element参数,定义当前组件的操作范围,所有的操作都基于这个element进行(理论上是这样)。如果我们想在一个页面模块上快速的调用组件的方法,或者更新组件的DOM,应该如何做呢?其实第一段代码已经告诉大家了。seajs.use的回调函数的参数,就是这个组件模块中widget的实例。当我们编写组件的时候,可以在widget的setup属性里暴露一些接口,比如更新DOM。只要seajs.use("id")中的id是一样的,那么它的回调中的实例永远是同样的实例。

####使用场景

我自己问自己,这种组件的形式会用到什么场景中呢。在以前的开发中,我会将页面模块分为MVC三层进行开发,如果一个刷新列表的方法可以传入context,不是也可以做到方法复用吗? 但是转念一想,如果这个方法是写在index.view中的,如果其他页面也想调用这个方法,它势必不会去require index.view的,此时就需要有一个系统级的组件来做这种通用的事情,也就是说,这个组件,无论拿到哪里都可以根据不同的数据,渲染出相同的模板,而它的基本逻辑也是写在组件模块中不需要更改的。 例如:角色详情中属性的窗口,在CYG的TL站里,不少属性框的样式除了高度不同,数据不同(数据的展现形式相同),其他都是没有差别的。通过组件,我们将相同数据结构的不同数据传入组件中,它就会自动的将数据遍历,渲染出不同的属性值,而不需要在FTL中复制三份同样的FOR循环代码了。类似这样的例子其实还很多,不一一列举。

另一个好处就是,组件化的开发方式,包括Loader,都只是一些方法和方法的辅助工具,就算开发人员不喜欢这种开发方式,它还是可以选择自己熟悉或者自己喜欢的开发方式去组建页面,而不会因为不使用这套方案就干不了活。。。

今天就写到这吧,明天还得切图呢。。

⚠️ **GitHub.com Fallback** ⚠️