FramerJS 之编程基础(CoffeeScript)

由于 Framer 是构建在 CoffeeScript 上的,想要使用 Framer 制作交互式内容前,得先学习一点 CoffeeScript 的编程知识,说到编程其实也没有那么复杂,大概就是通过机器能听得懂的形式跟它聊天而已,当然,首先要知道机器语言是什么,其中就包括要了解变量、数字、字符串、条件判断、循环体、函数等等。

CoffeeScript 是一个JavaScript 的子集,可以理解成是一个优化版本,因为 JavaScript 里面有很多不可描述的可怕陷阱,常常会令开发者苦恼,而 CoffeeScript 在很大程度上减轻了开发者的痛苦。

JavaScript 是一种可运行在浏览器和服务器上的脚本语言,网页上的特效都是她折腾出来的。

在了解 Framer 如何实现交互前,先来看基础的编程是怎么回事。

基础规范

任何编程语言都是一个限定的规范需要去遵守,就好像我们的设计规范、产品文档规范一样。Framer 里面的规范同样也是 CoffeeScript 的规范,进行所有其他事情之前先来记住以下两点:

1、缩进
在 Framer 中使用键盘上的tab键得到 4 个空格的缩进,但是请注意,不能手动的输入 4 个空格,这是无效的。缩进主要用在定义多行的数组、对象、函数等,后面会讲到。

2、大小写
在 Framer 中定义变量、函数等,需要明确给出指代的名字,在 CoffeeScript 中定义名字使用字母为首,可接数字和其他字符的形式,在基础的计算机认知里面,小写n和大写N是两个截然不同的字符,所以在定义名字的时候需要注意name = "季男" Name = "季男"是两个截然不同的东西。

除了这两项,其他的一些需要注意的事情,Framer 会在你犯错误的时候给出提醒,因为 Framer 里面内置了一个语法检查器。

下面开始真正的编程之旅 ~

了解什么是变量

从字面意思上了解就是可以被改变的东西。变量主要是为了方便存储数据,可以解释成一种绑定到某个数据的标签,比如定一个变量cat = ''ET''这个时候就把数据ET绑定到了变量cat上面。既然是可变的量,将cat = “灰机”灰机绑定到变量cat上,此时原来的ET就被丢弃在内存里面等待清理了,现在变量只有一个值灰机

通常在定义变量的时候需要做到表意清晰,一个有意义的名称对于日后的维护能起到很大的帮助作用。不建议定义一些诸如a = 1这样没有意义的变量名称。

变量可以绑定到多种类型数据,比如字符串、数字、数组、对象、函数等,后面会经常遇到。

在 Framer 中会非常多的用到变量,比如创建一个图层,我们需要通过定义一个变量名称,然后将内置的一个Layer类实例化给这个变量。

firstLayer = new Layer

这里使用了一个关键词new。后面关于类的时候会提到。

很多时候,需要定义多个有相同属性的图层,比如一样背景色的按钮。这个时候我们可以把公共的属性数据单独绑定到一个变量上。

# 定义一个公共的属性变量
bgColor = "gray"
# 创建一个按钮图层
buttonOne = new Layer
    backgroundColor: bgColor
#创建另一个按钮图层
buttonTwo = new Layer
    backgroundColor: bgColor

现在两个按钮都使用了一样的背景色,改变变量bgColor的颜色值,两个按钮的颜色都会发生改变。

跟数学一样的数字

当使用 Framer 时,总是需要给里面的一些元素设置某些属性,比如坐标、大小、透明度这些属性的值通常是一个数字,如果你给他们赋值一个非数字的话 Framer 会给出错误提示。在绝大部分编程语言中,数字都是可以进行加减乘除运算的。

Framer 里面的元素属性大部分都来自 CSS 的常见属性,但是在定义属性的数值时,不需要给它们定义单位(通常 CSS 里面需要定义px 、em),因为 Framer 已经懂得如何去设置它们的单位。

定义图层的多个属性使用如下方式:

myLayer = new Layer
    width: 200
    height: 200
    opacity: 0.5

有时候会用到数学公式来完成一些基本的数值运算:

print (400 + 100 / 10) * 6
myLayer.width = 500 / 2

当一个属性已经设置数值的时,可以将它作为计算公式的一部分:

otherLayer = new Layer
    width: myLayer + 10
otherLayer.height = myLayer.height / 2

偶尔我们只是想让元素的属性值在原有数值的基础上发生改变,比如每次点击图层的时候都向右移动 10。可以这样来赋值:

otherLayer.x = otherLayer.x + 10

什么都能装的字符串

字符串是被包裹在引号内的文字,在 Framer 中,它们通常被用于赋值给颜色、图层名称、网页内容等,它可以是一个字母,也可以是一长串文字。

stringLayer = new Layer
    name: "button"
    html: "我是按钮,请点我~"

字符串也可以像数字那样使用数学上的+加号来进行合并,多个字符串相加得到一个新的字符串。

print "我是按钮," + “请点我~”

如果字符串和变量要合并在一起怎么办呢?同样可以使用加号:

name = "季男"
city = "广州"
print "我叫" + name + ",我生活在" + city

虽然可以像上面那样写,但是,字符串和变量很多时,会显的难以理解,并且非常难书写,这个时候就需要感谢 CoffeeScript 提供字符串插值写法:

print "我叫#{name},我生活在#{city}"

字符串插值的写法也可以在其中使用数值运算:

age = 27
print "我今年#{age}岁,十年后我就#{age + 10}岁了。"

用于判断对错的布尔值

布尔值只有两种情况,要么是真的,要么就是错的。比如说要设置某个图层的可见度,通常会设置它的visible属性,又或者要设置某个图层是否能够拖移时,给draggable.enabled属性赋值。

layerA.visible = false # 图层 layerA 被隐藏了
layerB.draggable.enabled = true # 现在 layerB 可以拖动了

当使用关键词not时,可以给布尔值取反值(非真即假)

layerA.visible = not layerA.visible

还可以组合使用关键词andor,当达到多个条件都是相同布尔值时得到某个结果和当其中一个条件为真或假时得到某个结果:

layerA = new Layer
layerB = new Layer
    visible: false
print layerA.visible and layerB.visible # false
print layerA.visible or layerB.visible # true

通过条件分流你的任务

现实生活中,会根据今天是否下雨而决定出门要不要带伞,带伞的条件就是老天爷是否下雨,天气预报说要下雨,那就带伞,说肯定不下雨,那就不用带了。
在编程中,需要通过很多的条件来决定接下来要做什么,比如想要让用户在点击某个图层的时候另一个图层不可见,再点击的时候又让它出现,就需要通过条件判断这个图层当前的可见状态。

button = new Layer
layerA = new Layer
    point: Align.center
 button.onClick ->
    if layerA.visible # 如果 layerA 可见
        layerA.visible = false # 就让隐藏
    else # 否则
        layerA.visible = true # 就让 layerA 可见

除了像上面这样直接设置真假true \ false以外,也可以通过比较两个值得到一个真假结果:

layerA = new Layer
layerA.draggable.enabled = true # 设置 layerA 可拖移
marker = new Layer
    x: Align.center # 屏幕水平居中
    y: Align.center # 屏幕垂直居中
layerA.onDrag ->
    if layerA.y > marker.y # 如果 layerA 的 Y 轴值大于 marker 的Y 轴值
        layerA.backgroundColor = "red" # 条件成立就让 layerA 的背景色变成红色

从上面两个例子可以看出条件判断始终是在判断真假,真假值其实就是上面讲到的布尔值,在条件判断中,可以组合多种数据进行判断得到真假,再决定下一步的流程。还可以使用关键词andor来组合更多的条件:

layerA.onDrag - >
    # 如果 layerA 的 X 轴值大于 marker 的 X 轴值,并且,layerA 的 Y 轴值也大于 marker 的 Y 轴值
    if layerA.x > marker.x and layerA.y > marker.y
        layerA.backgroundColor = "red"
    else
        layerA.backgroundColor = "green"

用循环的方式来遍历数组

使用循环来遍历一个数组,可以很方便的一次创建多个元素,比如内容列表的展示,如果列表的每一项都手动去写,会显得特别浪费时间:

layerA = new Layer
    size: 50
    backgroundColor: "blue"
layerB = new Layer
    size: 50
    backgroundColor: "blue"
layerC = new Layer
    size: 50
    backgroundColor: "blue"

如果一个列表有十几个甚至几十个项目,想想都?害怕。这种时候就需要循环来帮助你了,当然循环也需要另一个帮手,那就是数组,通过循环的方式来创建10个图层看看:

for index in [1..10]
    layer = new Layer
        size: 50
        backgroundColor: "blue"
 # 观察 Framer 编辑器的左边图层列表,是不是可以看到 10 个 layer 了?

这里使用了for ... in循环,上例中,index 就是每次循环的值,从 1 开始,每次循环都会+1。它等于 [1..10]里面的 10 个数字,当 index 的值超出 10 后,循环便停止了,于是进行了 10次循环,并且通过循环体里面的new Layer得到了 10 个图层,非常容易理解。

上面的代码在运行后,虽然在图层列表可以看到这 10 个图层,但是在可视化窗口却只看到一个图层,这是因为 10 个图层都被设置了默认相同的坐标值,通过动态改变图层的坐标值,可以很直观的看到它们:

for index in [1..10]
    layer = new Layer
        name: "layer-" + index # 设置图层的名称
        size: 50 # 定义图层的宽高
        x: index * 55 # 设置 X 轴的值,通过每次循环的 index 乘以 55
        y: index * 55 # 设置 Y 轴的值

顺便提一下数组是个什么东西,学过数学的朋友应该不陌生的,这里的数组可以理解成一个容器,里面可以装各种东西,如果、变量、字符串、数字、函数、另一个数组(多维数组)等等,因为数组里面的元素是有顺序的,从左边开始是第一个元素,往后依次加一,但是需要注意,数组的第一个元素被称之为下标为0的元素,记住就好。

既然数组什么都能放,那试试把已经被创建出来的图层装进去,再给这些图层设置个背景色看看:

layers = [] # 先来定义一个空的数组,什么都不放。
for index in [1..10]
    layer = new Layer
        name: "layer-" + index
        size: 50
        x: index * 55
        y: index * 55
        # 通过数组 push 方法把每次循环的图层都添加进 layers 数组里面
    layers.push(layer) 
# 针对 layers 数组循环,在循环中给里面的图层设置背景色。
for layer in layers
    layer.backgroundColor = "blue"

顺便一提,在循环中除了能给元素赋值,也可以给元素定义一些事件动作,通常称之为方法或函数。比如想让每个图层在被点击的时候才改变背景色,可以这样做:

# ***** 代码同上 *****
for layer in layers
    layer.onClick ->
        this.backgroundColor = "blue"

这里有一个容易犯错的地方,经常容易把this.backgroundColor写成layer.backgroundColor,如果这么写,点击事件将只会触发循环结果的最后一项,也就是说不管你点击哪个layer,都只有最后一个改变了颜色。

为什么要写成this呢?这里涉及到作用域的事情,暂且记住就好,this代表当下元素。

可以被反复执行的函数

函数就像家里的电饭锅,只需要将米和水倒入里面,按下煮饭按钮,它便会进行各种操作,等待它完成,就有了可以吃的米饭。

先来了解下写一个函数应该要遵循什么规范:

  1. 先给函数定一个名字,就叫它riceCooker吧。
  2. 函数的基本结构->,通过这样的写法告诉程序这是一个函数。
  3. 函数体,告诉程序这个函数需要做什么。

下面来写个超级简单的电饭锅函数 :-D

riceCooker = ->
    print "准备煮饭..."
    print "正在煮饭 ~ 冒气ing ~"
    print "米饭煮好啦 ~ 哔哔作响 ~"
riceCooker() # 通过函数名 + () 可以调用这个函数

注意函数内的所有行都是缩进的,也就是->开始的下一行,通常称之为函数体。 上面的函数是通过riceCooker()手动调用的,但实际情况下是会通过点交互式的操作来完成,比如用户点击、按下或者其他函数触发等等。

下面来定义一个有关交互行为的函数,让图层每次点击时自身就旋转 10°:

layerA = new Layer
# 给 layerA 的 onClick 事件添加一个处理函数,让 layerA 的旋转角度每次都加 10
layerA.onClick ->
    layerA.rotation = layerA.rotation + 10

也可以像定义电饭锅函数那样,给这个操作绑定到一个变量上:

layerA = new Layer
# 定义一个叫 rotate 的函数,它可以让 layerA 旋转 10°
rotate = ->
    layerA.rotation = layerA.rotation + 10
# 给 layerA 的点击事件添加刚才定义的 rotate 函数
layerA.onClick(rotate)

虽然可以这样定义一个函数,但它不够灵活,因为rotate只能针对layerA进行旋转操作,如果想针对layerB也进行同样的操作又得再写一遍。这种时候需要把函数抽象出来,让它能够接收一个参数,成为一个通用的函数,可以针对不同的元素:

layerA = new Layer
    x: Align.left
layerB = new Layer
    x: Align.right
# 重新来定义这个 rotate 函数,让它接收一个参数 layer
rotate = (layer) ->
    layer.rotation = layer.rotation + 10
# 现在可以给 layerA 和 layerB 都提供相同的操作了
layerA.onClick ->
    rotate(layerA) # 把 layerA 作为实际参数传给 rotate 函数的 layer 参数
layerB.onClick ->
    rotate(layerB) # 把 layerB 作为实际参数传给 rotate 函数的 layer 参数

突然间,需求发生了变化!!需要给 layerA 旋转 15°,给 layerB 旋转 35°,这个时候上面定义的函数不能用了,别灰心,既然这个函数能接收一个参数,那能不能接收两个参数?多个参数呢?答案是肯定的!来试试:

# 改造下 rotate 函数,让它接收 2 个参数 layer 和 degrees
rotate = (layer, degrees) ->
    layer.rotaion = layer.rotation + degrees
# 现在调用它需要给两个参数传值
layerA.onClick ->
    rotate(layerA, 15)
layerB.onClick ->
    rotate(layerB, 35)

有时候可能有很多元素都只需要旋转 10°,一小部分需要单独定义旋转的角度,这个时候可以给函数的参数定一个默认值:

# 再次改造 rotate 函数
rotate = (layer, degrees = 10) ->
    layer.rotation = layer.rotation + degrees
# 不给 degrees 传值时将使用默认值
layerA.onClick ->
    rotate(layerA)
# 改变默认的 degrees 可以重新传值
layerB.onClick ->
    rotate(layerB, 50)

通常在函数体内嵌套条件判断时非常常见的,主要用来控制输出结果,下面请看一个实例:

# 定一个获取宽度的函数
largesWidth = (firstLayer, secondLayer) ->
    # 判断两个参数的宽度并返回其中一个
    if firstLayer.width > secondLayer.width
        return firstLayer.width # 通过 return 来返回结果并跳出接来下的函数体
    else
        return secondLayer.width
layerC = new Layer
    # 设置 layerC 的宽度等于 largesWidth 函数的计算结果
    width: largesWidth(layerA, layerB)

通过键值对关联的对象

在 Framer 中通过对象来存储数据是非常常见的。一个对象类似存在通讯录里面的人名和电话号码的键值对数据。对象需要有和对应的,在定义一个对象的时候,有点像定义函数,不过不需要->,给定一个对象名后另起一行并缩进,接着开始输入对象里面存储的键值对数据,一行一个键值对:

cats =
    et: "白白胖胖的中年猫"
    bubu: "非常乖但又脾气暴躁的三花猫"
    abao: "异常警惕的黑白熊猫"
    huiji: "完全不怕生的脑残虎斑"
print cats.huiji # 获取对象里面的内容通过对象名 + . + 键

在 Framer 中创建一个图层的时候,可以通过键值对的形式给图层设置属性:

myLayer = new Layer
    x: 200
    y: 200
    backgroundColor: "yellow" # 注意值为字符串的时候需要通过双引号包裹起来

对象和前面说的数组非常类似,其中有一个不同就是数组里面的元素是有序的,而对象里面的数据是无序的,记住这点可以避免很多不必要的错误。

键的值可以存任何数据,甚至是另一个对象,但需要通过嵌套的方式。在 Framer 中定义动画属性时会看到:

layer.animate
    x: 100
    y: 200
    backgroundColor: "blue"
    # 这里嵌套了另一个对象 options
    options:
        curve: Spring(damping: 0.5)
        time: 0.5

定义一个对象后,可以通过两种方式来获取键的值。

  1. 通过点操作符,object.sth
  2. 通过字符串取值,object["other-sth"]

    字符串取值有点类似数组的下标,如果对象的键存在空格和连接符,只能通过这种方式取值
    
ages = 
    et: 7
    "bu bu": 6
    "a-bao": 4
    huiji: 4
# 通过点操作符取值
print ages.et
print ages.huiji
# 通过字符串取值
print ages["bu bu"]
print ages["a-bao"]

当你想要在 Framer 中生成不同状态的对象时,使用字符串赋值的方式将非常有用:

layerA = new Layer
# 使用循环来创建 layerA 的多个状态
for i in [1..3]
    layerA.states["state#{i}"] =
        y: i * 100
# 为了更直观的看到这些状态,添加一个点击事件作为演示
layerA.stateSwitch("state1") # 设定默认状态
layerA.onTap ->
    layerA.statesCycle("state2","state1")

说到循环,来看看如何像数遍历数组那样遍历对象。遍历数组时通过for...in,遍历对象时CoffeeScript使用for...of,并且提供了的输出结果:

 cats =
    et: "白白胖胖的中年猫"
    bubu: "非常乖但又脾气暴躁的三花猫"
    abao: "异常警惕的黑白熊猫"
    huiji: "完全不怕生的脑残虎斑"
for key, value of cats
    print key, value
# 上面的 key 和 value 可以使用更好理解的词,比如:
for name, desc of cats
    print name, desc

比函数还强大的类

类是一个可以被扩展的对象。在 Framer 中,所有封装起来的组件都是通过基础的Layer类扩展出来的。这些被扩展出来的类称之为子类,子类可以继承父类Layer的基本功能,并且可以添加属于自己的更多功能。比如ScrollComponent组件是一个可以滚动的内容块。
可以通过关键词new来构建,使用new的形式将调用构造函数处理并返回一个类的实例,通常被称为实例化类。下面的 layerA 是一个包含 Layer 类的实例化:

layerA = new Layer

在 Framer 中可以很容易的创建一个子类。如果项目里面有多个按钮元素,没必要去单独一个个定义,可以创建一个专属的类把它们一个个new出来,就好像new Layer一样:

# 创建一个 Button 类,让它继承 Layer 父类,这样可以得到 Layer 类提供的内容
class Button extends Layer
    # 通过类的构造函数 constructor 来设置功能属性
    constructor:  (options) ->
        #获得父类的基本功能
        super(options)
        # 设置基本的按钮属性
        @width = 300
        @height = 100
        @backgroundColor = "maroon"
# 实例化一个 Button 类
button = new Button

创建类的过程中,@符号所代表的就是当前的Button类本身。现在创建的Button类不是很灵活,不能像new Layer一样可以设置 button 的大小,因为这个值被类中的构造函数覆盖了。

# 像 new Layer 那样去设置宽度没有任何反应,因为被类的构造器覆盖了
button = new Button
    width: 240
# 但是可以通过对象的点操作符的方式给 button 的 width 赋值
button.width = 240

虽然可以像上面那样通过点操作符来赋值,但不够直观也有点复杂。如何像new Layer那样可以直接赋值呢?这里需要将默认选中和构造器的选项进行合并,官方推荐使用 lodash defaults函数进行更方便的合并。

class Button extends Layer
    constructor: (options) ->
        super _.defaults options,
            width: 300
            height: 100
            backgroundColor: "maroon"
# 现在可以像 new Layer 那样方便的定制属性了
button = new Button
    width: 240

类里面除了可以定义一些属性外还可以添加自定义的函数,通常称之为类的方法。

class Button extends Layer
    constructor: (options) ->
        super _.defaults options,
            width: 300
            height: 100
        # 默认执行的 deactivate 方法
        @deactivate()
    # 定义两个函数/方法
    activate: ->
        @backgroundColor = "red"
    deactivate: ->
        @backgroundColor = "maroon"

现在,这些类里面定义的方法也成为每一个new Button的方法。通过添加一点交互的操作来查看下刚刚添加的方法:

button = new Button
# 当按下按钮的时候触发类的 activate 方法
button.onTapStart ->
    @activate()
# 当松手的时候触发类的 deactivate 方法
button.onTapEnd ->
    @deactivate()

在 Framer 里还可以将上面的onTap事件直接添加到类是构造函数里面,这样可以让每一个实例化的按钮都继承这种交互操作,并且可以得到相同的反馈。

class Button extends Layer
    constructor: (options) ->
        super _.defaults options,
            width: 300
            height: 100
        # 默认执行的 deactivate 方法
        @deactivate()
        # 在构造函数里添加交互处理事件
        @onTapStart ->
            @activate()
        @onTapEnd ->
            @deactivate()
    activate: ->
        @backgroundColor = "red"
    deactivate: ->
        @backgroundColor = "maroon"

对比循环、对象和类

在编程中通常有很多方法可以来实现同样的事情。这很强大,同时也有点难以选择,例如,可以通过循环来创建两个蓝色的图层,也可以通过一个函数实现,当然也能使用类来实例化。

# 通过循环创建
for i in [1..2]
    new Layer
        backgroundColor: "Blue"

# 通过函数创建
createLayer = (backgroundColor) ->
    new Layer
        backgroundColor: backgroundColor
createLayer("blue")
createLayer("blue")

# 通过类实例化
class BlueLayer extends Layer
    constructor: (options) ->
        super
        @backgroundColor = "blue"
new BlueLayer
new BlueLayer

这几种方式没有哪个才是正确的。当你在实际的编程中,在充分了解了自己的需求,你可以根据不同情况使用更适合的方式去改写你的代码。

神奇的作用域范围

在编程中,作用域范围是指代码块中变量的值,有时候无法访问到某个值,可能是因为不在同一个作用域导致的。往往在循环中会比较多的遇到作用域问题,一起看一个实例:

for i in [1..3]
    layer = new Layer
        backgroundColor: "blue"
        size: 50
        y: i * 70
    layer.onClick ->
        layer.backgroundColor = "red"

试着在 Framer 中点击被创建出来的图层,你会发现点击任何一个图层,都只有最后一个图层的背景色变成了红色!怎么回事?单击事件函数明明写着,图层被点击的时候更改背景色。尽管是这样,但似乎有一点被忽略了,循环运行的时候,这个layer变量会一次次发生改变,直到循环停止,这个时候变量layer的值指向了第三个图层,通过代码可以更直观的了解:

for i in [1..3]
    layer = new Layer
        name: "layer-" + i
        size: 50
        y: i * 70
    layer.onClick ->
        layer.backgroundColor = "red"
        print layer.name

通过增加图层的名称和打印出点击事件的图层名称,可以很方便的看到变量layer指向了哪里。
Tips:由于数组的下边是从 0 开始的,图层的名称会从 layer-0 开始到 layer-2

发现了这个问题,又该如何去解决呢?这里提供了两个方法。

使用 this

而不是使用变量名layer,使用this可以理解成当前的这个,也就是循环过程中当前的这个layer

for i in [1..3]
    layer = new Layer
        backgroundColor: "blue"
        name: "layer-" + i
        size: 50
        y: i * 70
    layer.onClick ->
        this.backgroundColor = "red"
        print this.name

你可以使用更简洁的@符号来代替this关键词。

layer.onClick ->
    @backgroundColor = "red"

使用 do

另一个办法是通过变量do来捕获。举个例子,如果需要通过一个外部的图层去触发循环内图层的某些事件,可以这么做:

button = new Layer
for i in [1..3]
    layer = new Layer
        backgroundColor: "blue"
        size: 50
        y: i * 70
    do (layer) ->
        button.onClick ->
            layer.backgroundColor = "red"

现在可以通过外部的button图层,去改变使用循环创建出来的所有图层的背景色了。

标签: framer, coffeescript, 编程基础

添加新评论