es6中的混合器模式


这是有关设计模式相关的第一篇文章,谈及设计模式,一般情况下呢,很多人马上就会说出很多关于它的东西,比如单例模式、策略模式等等。对于各个技术栈的工程师们,各种设计模式应该再熟悉不过,这篇文章要分享的是关于前端中的混合器模式,也可以称作装饰器模式,并分享一些在实际开发中的应用。

在面向对象的开发中,实际业务的描述是通过类(Class)来进行描述的,如果想给一个已经存在的类扩展某些行为(如增加某个方法、某个属性等),可以通过类的继承来实现,即将可复用、可扩展的方法抽象为父类中的方法之后继承它。

但是思考一下,这种做法的弊端:

  • 类的继承所扩展的方法或属性是静态的,既你无法为一个已有的类的实例扩展某些方法。
  • 类的继承会继承父类中的所有可继承的方法和属性,但是你只想继承其中的某个或某几个方法。

在es6中,加入了装饰器(Decorator)语法,提供了对于装饰器模式的原生支持。这里对于语法不再进行详细的阐述,可以点击这里进行了解或学习。

在这种模式下,我们可以动态的来为一个类增加某个方法或属性,你可能会问,这样听起来似乎和继承没什么不同?你错了,它的优势体现在动态二字,其含义即为可以动态的为类的实例增加某种行为,也许你又会说,在js中,它本身就是动态语言,这种动态性也失去了优势了吧。

是的,但是在js中想要实现继承绝非易事,因为它的继承机制是通过原型链进行描述的,对于js新手的话,理解起来其实是有点晦涩的,有些时候我们仅仅想通过更清晰的手段使用另一个类中的某些方法。

举个例子,在SPA开发中,经常会遇到处理分页数据的需求,对于分页逻辑的处理,很显然是一块需要抽象为公共方法的逻辑,但是如果你按继承的角度去考虑,就会发现,有些时候你继承了这些处理分页逻辑的方法反而是多余了,因为并不是每个页面都需要分页,这种情况下,使用混合器模式来实现则可以更好的解决问题。

我们先来实现一个分页逻辑的混合器,它其实是个函数,函数的参数是一个类,之后会动态的在这个类上增加一些方法,从而达到在一个类中混入另一个类的某些方法的目的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export const PaginationMixin = (superClass) => class extends superClass {
nextPage (url, callback) {
this.handlePaginationResolve(url, callback)
}

previousPage (url, callback) {
this.handlePaginationResolve(url, callback)
}

handlePaginationResolve (url, callback) {
if (!url) return

customAPIComposer(url, 'get').then(callback)
}
}

之后,只需要按如下进行代码,对需要扩展的类进行扩展,

1
class AttendanceManageCtrl extends PaginationMixin(MixinBase)

扩展后就可以使用混合器中混入的方法了,如下

1
<oa-list-table list-data='$ctrl.listData' on-next='$ctrl.nextPage($ctrl.listData.next, $ctrl.render)' on-previous='$ctrl.previousPage($ctrl.listData.previous, $ctrl.render)'>

这样做似乎和继承没什么区别,但是不妨看看如下代码

1
class PeopleCtrl extends R.compose(PaginationMixin, StateManageMixin)(MixinBase)

没错,你可以对一个类同时使用多个混合器,但是你却无法使一个类同时继承与多个类,虽然你可以通过多重继承实现,但显然比前者繁琐很多。

尽情混入吧。

more

codereview-s8


当元素间存在父子关系时,留意事件冒泡机制所引发的连锁反应

1
2
3
4
5
6
<tr style="cursor: pointer;" ng-repeat="row in $ctrl.efficiencyTable.bodyData" ng-click="$ctrl.stepView(row)">
...
<td class="text-left">
<a ng-click="$ctrl.efficiencyView(row)" class="hover-link">查看流程</a>
</td>
</tr>

a元素的点击事件会efficiencyView方法,但因为事件冒泡机制,也会间接的调用stepView方法

最佳实践

angular中可以使用内置的 $event 对象来解决相应问题

首先声明使用$event对象并传参

1
<a ng-click="$ctrl.efficiencyView(row, $event)" class="hover-link">查看流程</a>

之后再efficiencyView方法中调用stopPropagation方法阻止事件冒泡

1
2
3
4
efficiencyView (workflow, $event) {
...
$event.stopPropagation()
}

也可以对比$event对象中的targetcurrentTarget属性是否相同,因为这两个属性分别代表触发事件的dom节点与响应事件的当前节点

1
if($event.target === $event.currentTarget) ...

扩展

z-index 常见问题

关于z-index本身用法我是了解的,但是最近在做下拉框组件和datepicker时,踩了一些坑,如下:

  • 只有设置了position属性的元素的z-index才会生效
  • 当父容器的z-index小于元素A时,其子容器的z-index无论多大都无法覆盖元素A

最佳实践

实现具有下拉菜单展开特效的组件时,一般会套用一下结构

1
2
3
4
5
6
7
8
<div class="dropdown-wrapper">
<div class="dropdown-toggle">
...
</div>
<div class="dropdown-list">
...
</div>
</div>

当两个下拉菜单处于垂直布局时,如果没有设置z-index属性,因为dropdown-toggledropdown-list先出现,因此默认的图层顺序是前者高于后者,所以当上面的下拉菜单出现的时候,并不会覆盖下面的toggle,为了解决这个问题,必须做如下处理

1
2
3
4
5
.dropdown-list{
...
z-index: 1000;
...
}

最后附上DEMO

扩展

对表格中的单元格增加一个hover高亮效果

对于表格中td增加hover高亮时可能会遇到一个问题,就是当你使用常规的border属性对边框进行设置时,可能会发现,每个单元格的上边框和左边框都没有达到理想的效果,但是下边框和右边框却是正常的。
这种问题会出现在对table增加border-collapse: collapse属性或是引用一些第三方的css库,比如bootstrap,具体现象参考DEMO

最佳实践

解决方法其实很简单,就是将td边框的样式从solid改为double,如下:

1
2
3
table tr td {
border-style: double;
}

原因如下:

Since double is “more distinct” then solid, its colour takes precedence over cells around it, and looks identical to solid anyway ;)

扩展

angular中遭遇的一个奇葩问题

这个问题是我在本期开发排班器组件时遇到的一个很奇葩的问题,大体描述就是如上面github链接中描述的一样,就是当父组件的一个数据采用双向绑定时,并且需要已事件回调的方法更新其内部的某个属性值,然后使用签名为onChangescope属性传给子组件,比如:

1
2
3
4
5
scope:{
...
onChange: '&
...
}

那么这个onChange的调用在父组件进行更新某条以双向绑定方式进行绑定的属性时,会先于子组件的更新前自动调用,这么说有点抽象,大体的问题我简单描述下。

本来onChange的调用时机应当是自下而上的,也就是当子组件发生更新时,调用父组件通过onChange属性传递的事件回调方法,这个方法会更具子组件的当前状态来对父组件进行更新,这就是理想中的单向数据流子组件通知父组件进行更新的机制。但是在angular中遇到的奇葩现象现象就是,在父组件进行更新时,不知道是因为签名的缘故还是双向绑定的缘故,这个onChange都会先于子组件运行一次,那么问题来了,这个方法本来的调用时机是子组件更新后需要通知父组件进行相应更新时调用的,然而现在子组件还未更新则先调用了该方法,那么回调函数中的参数必为空,除非onChange中加入了空校验代码,不然就会报错,其实加了空校验也没有什么意义,因为这个方法都会调用的,但是却不会进行任何的改变,相当于浪费了一部分性能。

最佳实践

解决方法在这里,这种解决方法算是一种workaround,即在组件中使用另一个对象来储存父组件需要更新的那个值,算是做了一种类似中间件的处理,之后因为双向绑定自动更新机制对于对象的更新时更具reference来进行的,那么在父组件或子组件中对于这一个数据的引用均是相同的,而不会像基本数据类型存在一个新旧值的差异,不过这终究是一个workaround

限制上传文件的类型

现在通过type为文件类型的input上传文件已经很普遍了,并且对于表单的校验,通常我们会在提交时进行,文件类型的表单也不例外,一般校验的内容有文件大小、文件类型(扩展名)等等。今天遇到一个需求很有意思,大体意思是想在用户进行文件上传时,就有偏向性的屏蔽掉一些不支持的文件格式,比如上传图片,那么在文件选择对话框就不要出现文本类型的文件。

这个问题我一开始是不知道怎么解决的,因为浏览器对于操作系统是一个沙盒,因此对于文件显示的控制应当没有权限控制,去网上google了下,答案也是这样的,没有方式可以实现百分之百屏蔽某种文件类型的方式。但是却发现了另一个很有意思的属性,也可以达到类型的效果。

这个属性就是input标签的accept属性:

If the value of the type attribute is file, then this attribute will indicate the types of files that the server accepts, otherwise it will be ignored. The value must be a comma-separated list of unique content type specifiers:

它可以接受的值的描述:

  • A file extension starting with the STOP character (U+002E). (e.g. .jpg, .png, .doc).
  • A valid MIME type with no extensions.
  • audio/* representing sound files. HTML5
  • video/* representing video files. HTML5
  • image/* representing image files. HTML5

最佳实践

比如我们想要限制上传文件类型为excel文件类型,只需要创建如下标签:

1
<input type="file" accept=".xls,.xlsx"/>

这样这个文件表单对话框被激活时,默认会选取以.xls和.xlsx结尾的文件。

虽然这个属性可以达到类似的效果,但是是无法完全替代对于文件扩展名的校验的。因为只要用户想要上传别的类型的文件,通过切换文件对话框中的选取文件类型选项(比如显示全部文件类型),就可以选取别的类型的文件了,因此在提交时,也别忘了添加校验逻辑,防止因为上传了一些不支持的类型造成服务器内部错误。

扩展

more

async-for-js


介绍关于js开发中所涉及的主流异步编程解决方案

repo: async-for-js

例子

插入3个div元素,其中第二个div元素使用setTimeout模拟异步操作,理想的插入顺序为div1 div2 div3,但这里的代码的插入顺序为div1 div3 div2。

1
2
3
4
5
6
7
8
9
10
11
12
// async way
function _async() {
document.body.appendChild(div1)

setTimeout(function () {
document.body.appendChild(div2)
}, 2000)

document.body.appendChild(div3)
}

_async()

Callback

最常用的方法是利用callback(回调函数)的方式,因为js中函数也是作为对象存在的,因此可以被当做参数传入另一个函数中,只需要在异步操作执行代码后调用回调函数即可。

但是使用回调函数有很明显的局限性,一方面体现在需要自己对异步操作进行控制,另一方面还很容易陷入”回调地狱”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// use plain callback to sync
function _callback(cb) {
document.body.appendChild(div1)

setTimeout(function () {
document.body.appendChild(div2)
cb('done')
}, 2000)

return 'done'
}

_callback(function () {
document.body.appendChild(div3)
})

Promise

因为回调地狱的问题,后来聪明的人使用将回调延迟执行的思想,从而发明了promise库,调用者可以根据异步流程随心所欲的resolve或reject某个值给之后的操作,从而解决了毁掉地狱的问题。

不过使用promise仍然有问题,就是当代码逻辑很长的时候,总需要带着大片大片的then方法,可读性仍然不够清晰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// use promise to sync
function _promise() {
document.body.appendChild(div1)

return new Promise(res => {
setTimeout(function () {
document.body.appendChild(div2)
res('done')
}, 2000)
})
}

_promise().then(data => {
console.log(data)
document.body.appendChild(div3)
})

Generate

后来promise加入了es6标准,同时推出了新的异步解决方案,叫做generate函数,大体讲是提供了一个具有状态机功能的函数,每次执行会停止在实现者声明的某个状态,下次调用会继续从这个状态开始执行。

generate的出现,使必须依靠callback实现异步操作的代码风格,可以使用同步代码风格实现,是一颗非常甜的语法糖。

但是它仍有有一些缺点,就是它作为状态机,无法自执行,必须借助实现一个run函数或使用第三方库(如co)。

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
// use generate to sync
function* _generate() {
document.body.appendChild(div1)

yield function (cb) {
setTimeout(function () {
document.body.appendChild(div2)
cb()
}, 2000)
}

document.body.appendChild(div3)

return 'done'
}

function run(fn) {
var gen = fn()

function next(data) {
var result = gen.next(data)

console.log(result.value)

if (result.done) return

result.value(next)
}

next()
}

run(_generate)

Async/await

为了解决generate的缺点,es7很快发布了继generate更强大的一个东西,叫做async函数。简单说,它并没有什么新特性,把它看做是可以自执行的generate函数即可,其中的await的操作符可以看做是yield操作符的翻版。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// use async/await and promise to sync
const fn = function () {
return new Promise(res => {
setTimeout(function () {
res(document.body.appendChild(div2))
}, 2000)
})
}

async function _await () {
document.body.appendChild(div1)
const f = await fn()
console.log(f)
document.body.appendChild(div3)
}

_await()

Observable

最近很火的rxjs也快成用来解决这个问题,详细的介绍可以去它的官网了解。

1
2
3
4
5
6
7
8
// use rxjs and callback to sync
const _callbackObservable = Observable.bindCallback(_callback)
const result = _callbackObservable()

// result.subscribe(x => {
// document.body.appendChild(div3)
// console.log(x)
// })

more

当开发一周了


转行做前端开发正好一周了,仔细一想,是真正意义上的一周呀,因为连上了7天班了。

说到感觉如何,我只能说,那是太好了。自从来了新公司,心情一下就变好了,回想这半年来,一直因为一些事情闷闷不乐,我也是蛮能拼的。不过总体工作还是比较忙,因为正好赶上项目要发布的最后一周,而且前端组的好多工作似乎还堆积了,不过好在最后也是按时完成了任务。

more

我为什么选择离开了Liferay


距离离开Liferay还剩下用指头可以数出来的日子了,心中不禁感慨万千。这一个月除了做一些交接工作和下一份工作的准备意外,剩下的时间基本都在思考在Liferay的两年半时间我到底得到了什么,又留下了什么?仔细想想,好像没留下什么,至于得到的,确是很多的,毕竟这是我毕业步入社会进入的第一家公司,很多事情从未知到已知都是从这里开始。

在这里认识了不少人,我从他们身上都学到了很多很多,不经意间也帮助了我很多很多,我衷心说声谢谢,同时我也想说,对于我的离职我只想说,离别是为了下次更好的相遇。

不过写这篇的目的并不是为了怀旧,只是最近稍微熟悉点儿的人都在问我一个问题,“你为什么离职?”。这个问题,我从有离职想法的那天,到念头一点点变大,再到最后做出离职的决定,我已经问过自己无数次了。细说起来,大大小小的原因有很多,不过大体概括也就两点,一,我无法得到我想要的,二,Liferay同时也并不需要我这样的人,仅此而已,所以我选择离开。

这里直接说Liferay好像扣上了一个很大的帽子,好吧,这里只是说中国这边的Liferay,后边不特殊说明均指代Liferay CN,如果再具体些,可以指代Liferay CNQA。

more

vscode typings配置


vscode发布至今已经到1.5.2了,我是从1.1开始用的,经历了几个改动比较大的版本。总体给我的感觉还是很好的,作为一个rich editor,能够提供像webstorm之类ide的代码提示功能非常赞,但是对于这块的配置还真没折腾过,只知道它的提示是基于ts的compiler提供的,虽然是ts提供的,但是js作为ts的编译对象,同样可以享受这个compiler提供的便利。

more

HTML is about meaning


原文地址: http://marksheet.io/html-semantics.html

HTML标签本身的意义是为了传递对于一个document元素的含义,所以再考虑使用正确的标签表达内容之前,考虑你的网页看起来怎么样为时过早,请将注意力集中到每一个你使用的HTML标签上。

你需要根据你书写的内容及本文本身的含义,来选择与之匹配的HTML标签。

more

liferay-progress-bar


上次山寨了一个smile face,最近因为一直都在测70,然后就又对70的progress bar起了兴趣。

大体上portal的progress bar就两种,一种是线性的,一种是环形的。

more