Slide 1

Slide 1 text

Learning JavaScript in Three Web Apps Dexter.Yy @ ⾖豆瓣

Slide 2

Slide 2 text

Overview • JSMatchismo • TodoMVC • GalEditor

Slide 3

Slide 3 text

• 斯坦福iOS应⽤用开发课程 (CS193p, Winter 2013)⾥里 的卡牌游戏 • 对⽐比:Cocoa等传统客户端开发环境 • 从零搭建,不引⼊入任何依赖,不使⽤用任何库、框 架、编译⼯工具 • 模块化和MVC分层 App 1 - JSMatchismo

Slide 4

Slide 4 text

把HTML看作配置⽂文件, ⽽而不是数据和内容 ⽤用button.card的个数来配置卡牌数 量,⽤用classname和属性来配置状态 先创建⼀一个./index.html

Slide 5

Slide 5 text

创建./css/main.css 把 CSS 看作描述状态的 配置⽂文件 随便找⼀一张图⽚片做 卡牌背⾯面

Slide 6

Slide 6 text

DOM 是平台(浏览器) ⾃自带的、 特定状态下的、 彼此之间 存在关系的, 能被 JS 访问 和调⽤用的 『UI/视图组件』 实例对象 编辑 HTML 就是在 编辑 runtime 中 对象的状态和关系 HTML 和 CSS 是前端开发者的画板和 Interface Builder

Slide 7

Slide 7 text

创建 app.js

Slide 8

Slide 8 text

创建 app.js ECMAScript 5 (ECMA-262-5) Strict Mode TC39 ECMA-262 (ECMAScript, ES, JavaScript, JS) ECMA-357 (E4X) ECMAScript 3 (ECMA-262-3, JavaScript 1.5) JavaScript 1.6 JavaScript 1.8 ECMAScript 4 (ActionScript 3) ECMAScript 5 (ECMAScript 3.1) ECMAScript 5.1 (JavaScript 1.8.5) ECMAScript 6 (ECMAScript Harmony, ES.next, JavaScript 2.0)

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 text

Function Object Function Object -- * JS最最核⼼心的数据类型和特殊概念 * A collection of named values (‘value’: any primitive datatypes, or reference to any objects) * ~= hash table * ~= dictionary * != class instance * Pass by reference (~= pointer) * 除 primitive type之外,万物皆为object * 两种创建⽅方法:Literal(字⾯面量/直接 量)、‘new’操作符 Primitive datatype

Slide 11

Slide 11 text

Function Object Function Function -- * 包含 executable code 的 object * Named function 或 Anonymous function * ⽤用字⾯面量创建时,有 Function Declaration 和 Function Expression 两种⽅方式 * ⽤用法:structured programming (like C), late binding 的 object method、constructor、创建 lexical scope(词法作⽤用域)、Currying、传递 block (like Ruby)、元编程、…… * 某些 built-in / host 对象的 executable code 是 native code Primitive datatype

Slide 12

Slide 12 text

Function Object Function Primitive datatype -- * 3 + 1:number、string、boolean + undefined * null 是 object,Infinity、NaN 等都是 number * Pass by value * 前三种有 “Wrapper Class” (like Java),但 100% 情 况下都只使⽤用字⾯面量,Wrapper的价值是众多原⽣生 ⼯工具函数(静态⽅方法) Primitive datatype

Slide 13

Slide 13 text

Global context Function context Function context Execution context (执⾏行上下⽂文) Stack

Slide 14

Slide 14 text

Activation object == Variable object in function context { app: {...}, arguments: [arguments object], this: {Global object} } Global object == Variable object in global context { app: {...}, window: {Global object}, this: {Global object}, Math: {...}, Array: {...}, Object: {...}, ... }

Slide 15

Slide 15 text

export public API data hiding、private member (Traditional) Module Pattern global namespace

Slide 16

Slide 16 text

相同的 Global context

Slide 17

Slide 17 text

创建 model/ 和 card.js

Slide 18

Slide 18 text

进⼊入 execution context 解析形式参数和所有declaration(函数声明和变 量声明),填充当前上下⽂文的 Variable object { Card: function(){...} exports: function(){...} } 从上⾄至下执⾏行代码 退出函数上下⽂文,返回上⼀一级的上下⽂文继续执 ⾏行代码,刚才的 Variable object 作为 Scope 被 Card、exports 等还能继续访问的函数继续保存

Slide 19

Slide 19 text

假如代码是这样… 函数表达式和普通的变量声明 对未声明变量赋值,其实是 window.b = 10 的省略写法 函数表达式 函数声明 形式参数

Slide 20

Slide 20 text

进⼊入 execution context 解析形式参数和所有declaration(函数声明和变 量声明),填充当前上下⽂文的 Variable object { Card: function(){...} exports: undefined, a: undefined, c: undefined, d: 1 } 从上⾄至下执⾏行代码 { Card: function(){...} exports: function(){...}, a: 10, c: 30, d: 1 } Global Object: { window: {Global}, card: function(){...}, b: 10, ... }

Slide 21

Slide 21 text

Contructor(构造函数) 是 普通函数,不是Class prototype 是普通对象 Prototype based model of OOP new 操作符⽤用函数的 prototype 属性作 为模板,复制出新的对象,Card 本⾝身的 return 只要不是对象就被忽略 以⼯工⼚厂函数 / Wrapper / 模块对象作为 public API,避免紧耦合等问题

Slide 22

Slide 22 text

this 是 late binding 的 (new Card().contents)() 时, this 指向新对象, (false || new Card().contents)() 时, this 指向 Global Object ⽤用 new 调⽤用 Card 时,在进⼊入 Card 函数上下⽂文的阶段,this 被指向复制 出来的新对象 Card 和 contents 被作为属性(property)形式的 引⽤用值被调⽤用时,this 指向属性所属的对象,当 作为 Identifier (⽐比如变量名)形式的引⽤用值或实 际值被调⽤用时,this 被默认填充为 Global Object

Slide 23

Slide 23 text

jQuery style 的存取器 (Ad-Hoc Polymorphism) ⽤用下划线前缀显式声明private member,但不存在真正的约束 this._contents 使⽤用前不需要声明,因为 JS 的 object 都是 dynamic mutable object, 不存在的属性、没有实参的形参、未赋值的 变量,访问得到的都是 undefined prototype 只应该⽤用来定义⽅方法,属性必 须在构造函数内定义才能保证每个实例 都持有⾃自⼰己的属性(引⽤用类型)

Slide 24

Slide 24 text

此处类似 Duck Typing (Parametric Polymorphism) 因为从后⾯面的实现可以看到, 函数的⾏行为总是⼀一致 ⾃自省/类型判断:typeof, Array.isArray, toString, constructor, instanceof, ...

Slide 25

Slide 25 text

block ⻛风格的 iterator(迭代器) JavaScript 1.6 array methods ⼿手动指定上下⽂文

Slide 26

Slide 26 text

compare ⾃自⼰己的上下⽂文⾥里没有 声明 score 变量,会往 Scope chain(作⽤用域链)的上层爬, 找到 match 上下⽂文⾥里的 score ⼿手动指定上下⽂文

Slide 27

Slide 27 text

函数内的函数,都会形成 Closure (闭包),通过⾃自⼰己 的 Scope 存储上层函数的 “Variable object” (因此也能 访问到上层函数的 Scope 存储 的更上层 “Variable object” ) 假如 compare 被暴露给外部 访问,则 match 的上下⽂文会 ⼀一直保留,不会被 GC(垃圾 回收)

Slide 28

Slide 28 text

创建 deck.js

Slide 29

Slide 29 text

惰性初始化的getter

Slide 30

Slide 30 text

可选参数和默认值的实现, JS 不⽀支持 ruby/python 中的 named arguments 或 *args

Slide 31

Slide 31 text

No content

Slide 32

Slide 32 text

缓存作⽤用域链上层的变量或函 数结果,常⽤用于性能热点优化 或节省字符 从 array 中删除

Slide 33

Slide 33 text

假如有错误代码…

Slide 34

Slide 34 text

静态分析对JS开发⾮非常重要

Slide 35

Slide 35 text

JSHint Vim ⾥里常⽤用的语法检查插件

Slide 36

Slide 36 text

每个项⺫⽬目可以有不同的 JSHint 的配置

Slide 37

Slide 37 text

测试动态环境中的debug

Slide 38

Slide 38 text

console 中的未捕获异常

Slide 39

Slide 39 text

这个按钮⾮非常重要 addCard 的 execution context 回溯跳过第三⽅方/底层代码,找 到真正引发异常的逻辑

Slide 40

Slide 40 text

创建 playingCard.js (更具体的『扑克牌』) 显式声明依赖,不直接 使⽤用全局变量 第⼀一次引⼊入对其他模块的依赖

Slide 41

Slide 41 text

必须⼿手动调⽤用『⽗父类』的 构造函数,⼿手动绑定 this Object.create 可以直接⽤用⼀一个对 象为原型⽣生成新对象,不需要构 造函数和 new 继承的简单实现 继承的关键是原型链

Slide 42

Slide 42 text

『⼦子类』的⽅方法 在『⼦子类』原型上扩展 出这些⽅方法

Slide 43

Slide 43 text

牌⾯面花⾊色的存取器 默认值 快速检索常⽤用的 数据结构 让API的⾏行为尽量⼀一致

Slide 44

Slide 44 text

改为静态⽅方法, 频繁执⾏行的函数会⽣生成⼤大量 ⼀一次性的数据,影响旧浏览 器的GC性能 静态⽅方法 模块内部使⽤用的数据

Slide 45

Slide 45 text

牌⾯面⼤大⼩小的存取器 存储编号,之后查表转换

Slide 46

Slide 46 text

override『⽗父类』的 contents ⽅方法

Slide 47

Slide 47 text

创建 playingCardDeck.js (更具体的『扑克牌桌』)

Slide 48

Slide 48 text

『⼦子类』新增的初始化

Slide 49

Slide 49 text

原⽣生⽅方法得到的是 nodeList 对象,不是 Array ⽤用以上model来修改视图,测试效果 nodeList 有 length 属 性、接受数 字键名,所 以能⽤用 Array 的迭代 器(duck typing) 作为配置的HTML应该尽可能抽象 和语义纯粹,⽤用于实现特定外观的 结构可由代码⽣生成

Slide 50

Slide 50 text

在⻚页⾯面⾥里使⽤用之前写的 module ⽂文件时, 需要⼈人⼯工控制依赖关系(上下顺序) 导出 app 对象可以让应⽤用代码与具体⻚页⾯面解耦

Slide 51

Slide 51 text

效果是这样

Slide 52

Slide 52 text

视图相关代码常常包含 DOM 操作,与主要业务逻辑⽆无 关,它们⼀一定会越来越多,越来越繁琐,喧宾夺主。 直接在视图代码中访问和依 赖 model,会导致视图代码 ⽆无法进⼀一步抽象和通⽤用化, ⽆无法与业务逻辑解耦。 在进⼀一步开发之前,先审视 app.js。 考虑以上两点,可预⻅见 app.js 会越来越⻓长,越来越像 ⾯面条式脚本,难以维护、扩展和抽象。

Slide 53

Slide 53 text

约定:不允许在 app.js 中直接操作 DOM 有了这个约定,就必须将 视图的具体实现拆分出去, app.js 仅⽤用于调⽤用和组合 view 和 model 模块、监听 消息、公开接⼝口,也就是 controller 创建 view.js 给 view 传递纯粹的、逻辑⽆无关 的数据,⽽而不是 model 本⾝身

Slide 54

Slide 54 text

Smalltalk-80 Cocoa Ruby On Rails ASP.NET Model View Controller: History, theory and usage 很类似 Cocoa 的 MVC 分层

Slide 55

Slide 55 text

Cocoa 的 MVC 架构,来⾃自斯坦福CS193p

Slide 56

Slide 56 text

因为这个项⺫⽬目不复⽤用第三⽅方代码,DOM 对象本 ⾝身就相当于各种视图组件,view.js ⽤用来组合这些 视图组件 重构完毕,可以继续开发了,开始实现交互

Slide 57

Slide 57 text

加⼊入 view.js

Slide 58

Slide 58 text

加⼊入内部结构的样式 描述新的状态

Slide 59

Slide 59 text

快速测试不同状态,不依赖交互

Slide 60

Slide 60 text

No content

Slide 61

Slide 61 text

先设计视图接⼝口,视图不能依赖和 主动调⽤用 controller,只能⼲⼴广播消 息,类似 Cocoa 的UI组件向 target 转发 action controller 监听消息, 操作数据

Slide 62

Slide 62 text

controller 操作数据之后,需 要通知视图组件⽤用新的数据 更新 UI 不在初始化阶段填充 卡牌内容 每次翻牌时随机填充内容

Slide 63

Slide 63 text

跳转表,将不同UI对象上的 交互动作通过选择器分发给 不同的handler函数 事件代理捕获整个应⽤用范围的 交互事件,⽤用跳转表分发 设计视图内部的接⼝口

Slide 64

Slide 64 text

⼲⼴广播消息,传递视图 ⾃自⼰己加⼯工处理过的交 互信息(index)

Slide 65

Slide 65 text

实现事件代理 原⽣生的 matchesSelector ⽅方法 在不同浏览器⾥里名称不同, 需要解决兼容性问题

Slide 66

Slide 66 text

兼容性封装,⽣生成统⼀一的常量 尽可能⽤用特性侦测,⽽而 不是浏览器侦测(依靠 user agent) 动态⽣生成⽅方法名,JS 常⽤用 的元编程⼿手段

Slide 67

Slide 67 text

为避免在赋值前被调⽤用, 变量声明放在顶部 函数声明可以放在任意位 置,由于函数通常封装了 不属于主要逻辑的具体实 现,为了让代码更抽象更 可读,应该拆分出去或移 到不显眼的位置(底部)

Slide 68

Slide 68 text

加⼯工处理交互事件 对象,⽣生成更抽象 的、UI⽆无关的数据 分发给 handler 函数的事 件可能来⾃自不同的UI⼦子元 素,需要统⼀一 利⽤用 button 元素原⽣生的状态

Slide 69

Slide 69 text

设计消息接⼝口 实现消息接⼝口

Slide 70

Slide 70 text

⽐比构造函数更简单的对象⼯工⼚厂 缺点是每次⽣生成新对象都 需要重复⽣生成这些函数, 且不能继承。 但是在有必要的时候,这 个函数的内部可以很⽅方便 的重构为⽤用构造函数实现

Slide 71

Slide 71 text

⼲⼴广播瞬时消息 监听/订阅/观察消息 取消订阅

Slide 72

Slide 72 text

实现UI更新接⼝口

Slide 73

Slide 73 text

加⼊入状态栏和 次数统计

Slide 74

Slide 74 text

把 UI组件 / DOM 对象 组合到⾃自⼰己⾝身上,类似 Cocoa ⾥里的 outlet 更新状态栏⾥里的次数统计

Slide 75

Slide 75 text

实现类似其他语⾔言的字符串 格式化

Slide 76

Slide 76 text

⽤用相同的更新⽅方法 来初始化视图 实现更新卡牌接⼝口

Slide 77

Slide 77 text

初始化时没有传⼊入数据, 所以字符串拼接时会把不 存在属性的值 undefined 转 成字符串

Slide 78

Slide 78 text

format 也能充当最 简单的JS模板转换 基于字符串的JS模板

Slide 79

Slide 79 text

模板转换⽅方法都会⾃自动将 undefined 处理为空字符串

Slide 80

Slide 80 text

除了UI的内容,也更新UI的状态 尽可能只在 JS 或主要代码逻辑⾥里处理状 态的迁移转换,不要实现状态细节, 将具体实现和描述交给 css 之类的配置 ⽂文件和 DSL (领域语⾔言)

Slide 81

Slide 81 text

更新数据的 状态,从⽽而 更新视图的 状态

Slide 82

Slide 82 text

交互(点击)后的效果

Slide 83

Slide 83 text

在 css ⾥里描述状态的细节 (过渡效果动画) css3 的 transition ⾃自动为状 态的切换⽣生成过渡动画

Slide 84

Slide 84 text

测试交互效果

Slide 85

Slide 85 text

另⼀一种动画 实现,引⼊入 animate.css 中的⼀一个关 键帧动画

Slide 86

Slide 86 text

css 同样要⼿手动管理 依赖和先后顺序

Slide 87

Slide 87 text

初始化动画配置 切换状态触发 关键帧动画

Slide 88

Slide 88 text

测试交互效果

Slide 89

Slide 89 text

解决交互之后,开始实现真正的游戏逻辑 创建 cardMatchingGame.js

Slide 90

Slide 90 text

通过参数传递把 Deck 或其『⼦子类』的实例 『组合』进来,避免当前模块依赖具体的 Deck 模块

Slide 91

Slide 91 text

翻牌时的游戏规则 改变 model 的状态

Slide 92

Slide 92 text

游戏规则需要调⽤用 playingCard 的 match ⽅方法

Slide 93

Slide 93 text

重载 Card 的 match ⽅方法,实现不同的 积分奖励

Slide 94

Slide 94 text

奖励、惩罚和成本

Slide 95

Slide 95 text

⽤用 HTML 配置来初始化游戏 把扑克牌桌的实例 组合到游戏规则⾥里

Slide 96

Slide 96 text

⽣生成数据、更新视图的代码不属于主 要业务逻辑,应该单独组织到⼀一起 牌桌被组合到游戏规则 ⾥里之后,controller不需 要维护⾃自⼰己的牌桌

Slide 97

Slide 97 text

数据中的状态尽可能交给 model ⾃自⼰己来维护,在 controller ⾥里尽量只调⽤用 model 的抽象接 ⼝口,⽽而不是直接修改 model 中的 数据状态

Slide 98

Slide 98 text

增加状态栏⾥里积分的更新接⼝口

Slide 99

Slide 99 text

实现积分的更新接⼝口

Slide 100

Slide 100 text

积分的UI

Slide 101

Slide 101 text

游戏完成(线上demo)

Slide 102

Slide 102 text

Source code: https://github.com/dexteryy/JSMatchismo

Slide 103

Slide 103 text

• 著名开源项⺫⽬目 • 对⽐比:其他JS应⽤用开发框架 • oz.js⽀支持的模块化 • 复⽤用第三⽅方模块/组件(OzJS微框架) • ⽤用包管理⼯工具管理依赖 • 项⺫⽬目中的源代码⽂文件都必须是能直接在浏览器⾥里 使⽤用的静态⽂文件 App II - TodoMVC

Slide 104

Slide 104 text

最终效果(线上demo)

Slide 105

Slide 105 text

第三⽅方组件都会下载安装 到专⻔门的⺫⽬目录 包管理⼯工具(bower)⾃自动读 取的项⺫⽬目配置, 包含对第三⽅方组件的依赖 TodoMVC 项⺫⽬目提供的外观实现

Slide 106

Slide 106 text

⽤用包管理⼯工具初始化项⺫⽬目,⾃自动 下载安装依赖的第三⽅方项⺫⽬目

Slide 107

Slide 107 text

No content

Slide 108

Slide 108 text

⾼高级浏览器⾥里不需要 ES5 shim 新增 main.js,相当于上个项⺫⽬目中⻚页⾯面内的 inline script,增加了模块相关的配置 把模块名关联到包管理⼯工具 的安装路径 oz.js实现的模块化机制的配置 禁⽌止使⽤用全局变量

Slide 109

Slide 109 text

在动态环境⾥里⾃自动处理模块的依赖和加载,不需要⼿手动维护⽂文件的使⽤用和先后顺序

Slide 110

Slide 110 text

有了第三⽅方model库(NervJS),model 模块 不但书写更简洁了,也更强⼤大了 扩展出条⺫⽬目model⾃自⼰己的⽅方法 model 中的数据模式 (schema)和默认值

Slide 111

Slide 111 text

列表model的成员是条⺫⽬目model

Slide 112

Slide 112 text

view.js 仍然像上个项⺫⽬目⼀一样⽤用事件 代理(SovietJS)维护交互逻辑

Slide 113

Slide 113 text

双击和键盘事件 事件代理的初始化

Slide 114

Slide 114 text

因为可以复⽤用第三 ⽅方的 UI 组件了, view.js 现在主要承 担组合这些组件、 提供更抽象 API 的 ⼯工作,避免 UI组 件之间的耦合

Slide 115

Slide 115 text

view/ actionview 是 UI库⾥里 moui/ actionview 的 进⼀一步封装, 满⾜足项⺫⽬目的业 务需求 view.js ⾥里使⽤用 view组件的接 ⼝口,⽽而不是直 接⽤用 DOM 的 接⼝口

Slide 116

Slide 116 text

⽤用 view/actionview 封装出更具体的 警告框和确认框组件

Slide 117

Slide 117 text

No content

Slide 118

Slide 118 text

model 组件的初始化和操作 不再需要像上个项⺫⽬目⼀一样每次修 改 model 都需要⼿手动调⽤用 updateUI

Slide 119

Slide 119 text

可以监听 model 的改变, ⾃自动更新 UI (View Model Binder) ⽤用 model ⾃自⼰己的⽅方法⽣生成 纯数据传给视图

Slide 120

Slide 120 text

这个项⺫⽬目是包含多个 URL 的单⻚页 应⽤用,app.js 像服务器端web框架 的 controllter ⼀一样管理路由

Slide 121

Slide 121 text

No content

Slide 122

Slide 122 text

Source code: https://github.com/dexteryy/todomvc/tree/gh-pages/labs/ architecture-examples/ozjs

Slide 123

Slide 123 text

• 真实的⾖豆瓣产品 • 对⽐比:服务器端web框架中的静态⽂文件 • 项⺫⽬目中的⽂文件都是源代码,不再兼任『静态⽂文 件』 • 静态环境中的预处理/编译/构建 • ⽤用任务管理⼯工具整合⼤大量⼯工具和⼯工作流 • 应⽤用本⾝身的组件化,业务逻辑的分层,与服务器 端视图解耦 App III - GalEdtitor

Slide 124

Slide 124 text

No content

Slide 125

Slide 125 text

增加了任务管理⼯工具 (Grunt)的配置 从包管理安装的⽂文件中⾃自动 提取项⺫⽬目需要的部分,按项 ⺫⽬目⾃自⼰己的组织结构来放置 (grunt-dispatch)

Slide 126

Slide 126 text

因为第三⽅方组件的进⼀一步组 织,模块配置简单了很多 main.js 不再像上个项⺫⽬目那样初 始化应⽤用,⽽而是变成了单纯的 配置,相当于JS的构建脚本

Slide 127

Slide 127 text

JS模板也被拆分为独 ⽴立的源代码⽂文件 模板⽂文件被编译 成JS模块

Slide 128

Slide 128 text

css 也可以模块化和 复⽤用第三⽅方库( scss/ compass )

Slide 129

Slide 129 text

项⺫⽬目构建过程中会将 js、css、 html、图⽚片、JS模板分别从源 ⽂文件编译为⺫⽬目标⽂文件,再构建 出发布⽂文件,再⽤用这些⽣生成的 静态⽂文件填充 public ⺫⽬目录

Slide 130

Slide 130 text

静态环境中的构建 ⽤用 grunt-furnace 构建模板模块 ⽤用 Ozma 构建 JS 的静态⽂文件

Slide 131

Slide 131 text

上个项⺫⽬目中在动态环境中处理的模块加 载改为在静态环境⾥里完成,Ozma 会将 项⺫⽬目⾥里的JS源⽂文件按需要打包到静态⽂文 件中(⼀一个或多个)

Slide 132

Slide 132 text

浏览器⾥里只需要加载最少量的⽂文件

Slide 133

Slide 133 text

在⻚页⾯面⾃自⾝身的代码中配置、 修改、扩展、初始化和组织 调⽤用 app 的 API docs/index.html ⽤用于 应⽤用本⾝身(离线客户 端)的演⽰示和调试, 没有特定的后端,所 以保存图⽚片是纯前端 模拟

Slide 134

Slide 134 text

⽤用本地存储

Slide 135

Slide 135 text

在真实产品中的⻚页 ⾯面模板(后端视 图)⾥里使⽤用时,实 现真正的保存图⽚片 功能

Slide 136

Slide 136 text

⽤用后端视图中输出的 数据(相当于预加 载)初始化应⽤用,如 果没有数据,则另外 请求后端API 业务逻辑可抽象出组件、应⽤用、 ⻚页⾯面三个层次,前两个都可以是 通过包管理⼯工具引⼊入的⼦子项⺫⽬目 (独⽴立代码仓库)

Slide 137

Slide 137 text

课后思考 • 将第⼀一个项⺫⽬目跟⾮非web的GUI开发⽅方式做对 ⽐比 • 三个项⺫⽬目中代码的相似之处 • 前两个项⺫⽬目的约束被解除之后带来的改变

Slide 138

Slide 138 text

THE END dexter.yy@gmail.com