虽然现在SpacePen已经不被官方支持了,但是在Atom团队退出新的view系统之前,他还是最好的选择。
例子
space-pen定义了一套生成HTML的DSL,只要继承View类,并且写一个content类方法,就可以在这个类方法中使用这套DSL:
class Spacecraft extends View
@content: ->
@div =>
@h1 "Spacecraft"
@ol =>
@li "Apollo"
@li "Soyuz"
@li "Space Shuttle"
这个DSL还是比较明义的,有div,h1,ol这些方法加上参数构成。
View继承自jQuery,所以View子类的实力上可以使用所有jQuery方法:
view = new Spacecraft
view.find('ol').append('<li>Star Destroyer</li>')
view.on 'click', 'li', ->
alert "They clicked on #{$(this).text()}"
原理
SpacePen的View是JQuery的子类。SpacePen的任务就是根据content函数中的定义构造出一个对应的JQuery对象。
class View extends jQuery
SpacePen的构造函数如下:
constructor: (args...) ->
if @element?
//如果设置了element,则用element实例化。这里调用jQuery.fn.init.call,可以理解为“调用父类构造函数”
jQuery.fn.init.call(this, @element)
else
//否则使用子类定义的content函数来生成HTML,并实例化
[html, postProcessingSteps] = @constructor.buildHtml -> @content(args...)
jQuery.fn.init.call(this, html)
throw new Error("View markup must have a single root element") if @length != 1
@element = @[0]
@element.attached = => @attached?()
@element.detached = => @detached?()
//处理outlet标签
@wireOutlets(this)
//处理事件标签
@bindEventHandlers(this)
//在所有子元素上添加spacePenView属性,设置为this
@element.spacePenView = this
treeWalker = document.createTreeWalker(@element, NodeFilter.SHOW_ELEMENT)
while element = treeWalker.nextNode()
element.spacePenView = this
//触发后置处理步骤(subview是在这个步骤才添加上的)
if postProcessingSteps?
step(this) for step in postProcessingSteps
//如果定义了额外的初始化函数,则调用
@initialize?(args...)
最为核心的是buildHtml,也就是生成这个View的HTML。这是通过Builder这个类实现的。代码不长:
class Builder
constructor: ->
@document = []
@postProcessingSteps = []
buildHtml: ->
[@document.join(''), @postProcessingSteps]
//@div,@xxx什么的其实是转化到调用这个方法,name就是"div"等
tag: (name, args...) ->
options = @extractOptions(args)
@openTag(name, options.attributes)
if SelfClosingTags.hasOwnProperty(name)
if options.text? or options.content?
throw new Error("Self-closing tag #{name} cannot have text or content")
else
options.content?()
@text(options.text) if options.text
@closeTag(name)
openTag: (name, attributes) ->
if @document.length is 0
attributes ?= {}
attributes.is ?= registerElement(name)
attributePairs =
for attributeName, value of attributes
"#{attributeName}=\"#{value}\""
attributesString =
if attributePairs.length
" " + attributePairs.join(" ")
else
""
@document.push "<#{name}#{attributesString}>"
closeTag: (name) ->
@document.push "</#{name}>"
text: (string) ->
escapedString = string
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/</g, '<')
.replace(/>/g, '>')
@document.push escapedString
raw: (string) ->
@document.push string
subview: (outletName, subview) ->
subviewId = "subview-#{++idCounter}"
@tag 'div', id: subviewId
@postProcessingSteps.push (view) ->
view[outletName] = subview
subview.parentView = view
view.find("div##{subviewId}").replaceWith(subview)
extractOptions: (args) ->
options = {}
for arg in args
switch typeof(arg)
when 'function'
options.content = arg
when 'string', 'number'
options.text = arg.toString()
else
options.attributes = arg
options
总体上,Builder使用了递归的思想来生成HTML。
技巧
把确定/显示/隐藏的逻辑放在View里
space-pen只是一个view系统,负责输出HTML,怎么用她不管。但是如果我们实现的是一个对话框,可以吧显示/隐藏的逻辑放在View类中,方便使用。DEMO代码如下(参考了md-writer插件):
//初始化,绑定默认快捷键
initialize: ->
atom.commands.add @element,
"core:confirm": => @onConfirm()
"core:cancel": => @detach()
//确认
onConfirm: ->
...
//显示
display: ->
@panel ?= atom.workspace.addModalPanel(item: this, visible: false)
@previouslyFocusedElement = $(document.activeElement)
@panel.show()
...
//隐藏
detach: ->
return unless @panel.isVisible()
@panel.hide()
@previouslyFocusedElement?.focus()
super
需要注意的就是,显示前获取当前激活的元素,对话框结束后,恢复之前激活的元素。
关于对话框设计的思想
看了一些插件的源码,对话框类设计的思想主要有两种:
- 逻辑在对话框类中
- 逻辑在对话框外,通过回调的方式,对话框来触发
目的性很强的对话框,可以使用第一种。通用的,则用第二种。