5 Star 8 Fork 3

飞哥 / rumo-blog

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
rumoblog.sql 816.09 KB
一键复制 编辑 原始数据 按行查看 历史
1571828260@qq.com 提交于 2018-11-11 22:43 . 最新数据库
/*
Navicat MySQL Data Transfer
Source Server : 本地数据库
Source Server Version : 60011
Source Host : localhost:3306
Source Database : rumoblog
Target Server Type : MYSQL
Target Server Version : 60011
File Encoding : 65001
Date: 2018-11-11 22:43:30
*/
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for rumo_blog
-- ----------------------------
DROP TABLE IF EXISTS `rumo_blog`;
CREATE TABLE `rumo_blog` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(100) NOT NULL DEFAULT '',
`category_id` int(11) NOT NULL DEFAULT '0',
`description` varchar(600) NOT NULL DEFAULT '',
`user_id` int(11) DEFAULT NULL,
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT NULL,
`status` int(1) NOT NULL DEFAULT '0' COMMENT '0未发布1发布',
`is_delete` int(1) NOT NULL DEFAULT '0' COMMENT '0未删除1删除',
`img` varchar(200) DEFAULT '',
`tags` varchar(24) DEFAULT '',
`is_top` int(1) NOT NULL DEFAULT '0' COMMENT '1放首页 0不妨',
`is_hot` int(1) NOT NULL DEFAULT '0' COMMENT '0不热门 1热门',
`content` longtext NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=37 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of rumo_blog
-- ----------------------------
INSERT INTO `rumo_blog` VALUES ('1', '会这133个Java面试题进阿里还是问题吗?(内含答案解析)', '1', '<div>该问题列表特别长,我们有各个地方的问题,所以,答案必须要短小、简洁、干脆,不拖泥带水。因此,除了这一个段落,你只会听到问题与答案,再无其他内容,没有反馈,也没有评价。为此,我已经写好了一些博文,在这些文章中你可以找到我对某些问题的观点,如我为什么喜欢这个问题,这个问题的挑战是什么?期望从面试者那获取到什么样的答案?\n\n作者:Java高级架构\n链接:https://www.jianshu.com/p/768600997463\n來源:简书\n简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。</div>', '1', '2018-10-31 22:05:59', '2018-11-07 20:13:52', '1', '0', '', '面试题', '0', '0', '<div class=\"show-content-free\">\r\n <p>Java 面试随着时间的改变而改变。在过去的日子里,当你知道 String 和 StringBuilder 的区别就能让你直接进入第二轮面试,但是现在问题变得越来越高级,面试官问的问题也更深入。 在我初入职场的时候,类似于 Vector 与 Array 的区别、HashMap 与 Hashtable 的区别是最流行的问题,只需要记住它们,就能在面试中获得更好的机会,但这种情形已经不复存在。如今,你将会被问到许多 Java 程序员都没有看过的领域,如 NIO,设计模式,成熟的单元测试,或者那些很难掌握的知识,如并发、算法、数据结构及编码。</p><p>由于我喜欢研究面试题,因此我已经收集了许多的面试问题,包括许多许多不同的主题。我已经为这众多的问题准备一段时间了,现在我将它们分享给你们。这里面不但包含经典的面试问题,如线程、集合、equals 和 hashcode、socket,而且还包含了 NIO、数组、字符串、Java 8 等主题。</p><p>该列表包含了入门级 Java 程序员和多年经验的高级开发者的问题。无论你是 1、2、3、4、5、6、7、8、9 还是 10 年经验的开发者,你都能在其中找到一些有趣的问题。这里包含了一些超级容易回答的问题,同时包含经验丰富的 Java 程序员也会棘手的问题。</p><p>当然你们也是非常幸运的,当今有许多好的书来帮助你准备 Java 面试,其中有一本我觉得特别有用和有趣的是 Markham 的 Java 程序面试揭秘(Java Programming Interview Exposed)。 这本书会告诉你一些 Java 和 JEE 面试中最重要的主题,即使你不是准备 Java 面试,也值得一读。</p><p>该问题列表特别长,我们有各个地方的问题,所以,答案必须要短小、简洁、干脆,不拖泥带水。因此,除了这一个段落,你只会听到问题与答案,再无其他内容,没有反馈,也没有评价。为此,我已经写好了一些博文,在这些文章中你可以找到我对某些问题的观点,如我为什么喜欢这个问题,这个问题的挑战是什么?期望从面试者那获取到什么样的答案?</p><p>这个列表有一点不同,我鼓励你采用类似的方式去分享问题和答案,这样容易温习。我希望这个列表对面试官和候选人都有很好的用处,面试官可以对这些问题上做一些改变以获取新奇和令人惊奇的元素,这对一次好的面试来说非常重要。而候选者,可以扩展和测试 Java 程序语言和平台关键领域的知识。2015 年,会更多的关注并发概念,JVM 内部,32 位 JVM 和 64 JVM的区别,单元测试及整洁的代码。我确信,如果你读过这个庞大的 Java 面试问题列表,无论是电话面试还是面对面的面试,你都能有很好的表现。</p><p>Java 面试中的重要话题</p><p>除了你看到的惊人的问题数量,我也尽量保证质量。我不止一次分享各个重要主题中的问题,也确保包含所谓的高级话题,这些话题很多程序员不喜欢准备或者直接放弃,因为他们的工作不会涉及到这些。Java NIO 和 JVM 底层就是最好的例子。你也可以将设计模式划分到这一类中,但是越来越多有经验的程序员了解 GOF 设计模式并应用这些模式。我也尽量在这个列表中包含 2015 年最新的面试问题,这些问题可能是来年关注的核心。为了给你一个大致的了解,下面列出这份 Java 面试问题列表包含的主题:</p><p>多线程,并发及线程基础</p><p>数据类型转换的基本原则</p><p>垃圾回收(GC)</p><p>Java 集合框架</p><p>数组</p><p>字符串</p><p>GOF 设计模式</p><p>SOLID (<b>单一功能、开闭原则、里氏替换、接口隔离</b>以及<b>依赖反转</b>)设计原则</p><p>抽象类与接口</p><p>Java 基础,如 equals 和 hashcode</p><p>泛型与枚举</p><p>Java IO 与 NIO</p><p>常用网络协议</p><p>Java 中的数据结构和算法</p><p>正则表达式</p><p>JVM 底层</p><p>Java 最佳实践</p><p>JDBC</p><p>Date, Time 与 Calendar</p><p>Java 处理 XML</p><p>JUnit</p><p>编程</p><p><b>关注我:私信回复“架构资料”获取往期Java高级架构资料、源码、笔记、视频</b></p><p><b>Dubbo、Redis、Netty、zookeeper、Spring cloud、分布式、高并发等架构技术</b></p><p>120 大 Java 面试题及答案</p><p>现在是时候给你展示我近 5 年从各种面试中收集来的 120 个问题了。我确定你在自己的面试中见过很多这些问题,很多问题你也能正确回答。</p><p>多线程、并发及线程的基础问题</p><p>1)Java 中能创建 volatile 数组吗?</p><p>能,Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组。我的意思是,如果改变引用指向的数组,将会受到 volatile 的保护,但是如果多个线程同时改变数组的元素,volatile 标示符就不能起到之前的保护作用了。</p><p>2)volatile 能使得一个非原子操作变成原子操作吗?</p><p>一个典型的例子是在类中有一个 long 类型的成员变量。如果你知道该成员变量会被多个线程访问,如计数器、价格等,你最好是将其设置为 volatile。为什么?因为 Java 中读取 long 类型变量不是原子的,需要分成两步,如果一个线程正在修改该 long 变量的值,另一个线程可能只能看到该值的一半(前 32 位)。但是对一个 volatile 型的 long 或 double 变量的读写是原子。</p><p>3)volatile 修饰符的有过什么实践?</p><p>一种实践是用 volatile 修饰 long 和 double 变量,使其能按原子类型来读写。double 和 long 都是64位宽,因此对这两种类型的读是分为两部分的,第一次读取第一个 32 位,然后再读剩下的 32 位,这个过程不是原子的,但 Java 中 volatile 型的 long 或 double 变量的读写是原子的。volatile 修复符的另一个作用是提供内存屏障(memory barrier),例如在分布式框架中的应用。简单的说,就是当你写一个 volatile 变量之前,Java 内存模型会插入一个写屏障(write barrier),读一个 volatile 变量之前,会插入一个读屏障(read barrier)。意思就是说,在你写一个 volatile 域时,能保证任何线程都能看到你写的值,同时,在写之前,也能保证任何数值的更新对所有线程是可见的,因为内存屏障会将其他所有写的值更新到缓存。</p><p>4)volatile 类型变量提供什么保证?(答案)</p><p>volatile 变量提供顺序和可见性保证,例如,JVM 或者 JIT为了获得更好的性能会对语句重排序,但是 volatile 类型变量即使在没有同步块的情况下赋值也不会与其他语句重排序。 volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。某些情况下,volatile 还能提供原子性,如读 64 位数据类型,像 long 和 double 都不是原子的,但 volatile 类型的 double 和 long 就是原子的。</p><p>5) 10 个线程和 2 个线程的同步代码,哪个更容易写?</p><p>从写代码的角度来说,两者的复杂度是相同的,因为同步代码与线程数量是相互独立的。但是同步策略的选择依赖于线程的数量,因为越多的线程意味着更大的竞争,所以你需要利用同步技术,如锁分离,这要求更复杂的代码和专业知识。</p><p>6)你是如何调用 wait()方法的?使用 if 块还是循环?为什么?(答案)</p><p>wait() 方法应该在循环调用,因为当线程获取到 CPU 开始执行的时候,其他条件可能还没有满足,所以在处理前,循环检测条件是否满足会更好。下面是一段标准的使用 wait 和 notify 方法的代码:</p><p>// The standard idiom for using the wait method</p><p><b>synchronized</b>&nbsp;(obj) {</p><p><b>while</b>&nbsp;(condition does not hold)</p><p>obj.wait(); // (Releases lock, and reacquires on wakeup)</p><p>... // Perform action appropriate to condition</p><p>}</p><p>参见 Effective Java 第 69 条,获取更多关于为什么应该在循环中来调用 wait 方法的内容。</p><p>7)什么是多线程环境下的伪共享(false sharing)?</p><p>伪共享是多线程系统(每个处理器有自己的局部缓存)中一个众所周知的性能问题。伪共享发生在不同处理器的上的线程对变量的修改依赖于相同的缓存行,如下图所示:</p><div class=\"image-package\">\r\n<div class=\"image-container\" style=\"max-width: 520px; max-height: 469px;\">\r\n<div class=\"image-container-fill\" style=\"padding-bottom: 90.19%;\"></div>\r\n<div class=\"image-view\" data-width=\"520\" data-height=\"469\"><img data-original-src=\"//upload-images.jianshu.io/upload_images/5275417-e1cdefbe8646d6ea\" data-original-width=\"520\" data-original-height=\"469\" data-original-format=\"image/jpeg\" data-original-filesize=\"31256\" class=\"image-loading\" style=\"cursor: zoom-in;\"></div>\r\n</div>\r\n<div class=\"image-caption\"></div>\r\n</div><p><b>有经验程序员的 Java 面试题</b></p><p>伪共享问题很难被发现,因为线程可能访问完全不同的全局变量,内存中却碰巧在很相近的位置上。如其他诸多的并发问题,避免伪共享的最基本方式是仔细审查代码,根据缓存行来调整你的数据结构。</p><p>8)什么是 Busy spin?我们为什么要使用它?</p><p>Busy spin 是一种在不释放 CPU 的基础上等待事件的技术。它经常用于避免丢失 CPU 缓存中的数据(如果线程先暂停,之后在其他CPU上运行就会丢失)。所以,如果你的工作要求低延迟,并且你的线程目前没有任何顺序,这样你就可以通过循环检测队列中的新消息来代替调用 sleep() 或 wait() 方法。它唯一的好处就是你只需等待很短的时间,如几微秒或几纳秒。LMAX 分布式框架是一个高性能线程间通信的库,该库有一个 BusySpinWaitStrategy 类就是基于这个概念实现的,使用 busy spin 循环 EventProcessors 等待屏障。</p><p>9)Java 中怎么获取一份线程 dump 文件?</p><p>在 Linux 下,你可以通过命令 kill -3 PID (Java 进程的进程 ID)来获取 Java 应用的 dump 文件。在 Windows 下,你可以按下 Ctrl + Break 来获取。这样 JVM 就会将线程的 dump 文件打印到标准输出或错误文件中,它可能打印在控制台或者日志文件中,具体位置依赖应用的配置。如果你使用Tomcat。</p><p>10)Swing 是线程安全的?(答案)</p><p>不是,Swing 不是线程安全的。你不能通过任何线程来更新 Swing 组件,如 JTable、JList 或 JPanel,事实上,它们只能通过 GUI 或 AWT 线程来更新。这就是为什么 Swing 提供 invokeAndWait() 和 invokeLater() 方法来获取其他线程的 GUI 更新请求。这些方法将更新请求放入 AWT 的线程队列中,可以一直等待,也可以通过异步更新直接返回结果。你也可以在参考答案中查看和学习到更详细的内容。</p><p>11)什么是线程局部变量?(答案)</p><p>线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java 提供 ThreadLocal 类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。</p><p>12)用 wait-notify 写一段代码来解决生产者-消费者问题?(答案)</p><p>请参考答案中的示例代码。只要记住在同步块中调用 wait() 和 notify()方法,如果阻塞,通过循环来测试等待条件。</p><p>13) 用 Java 写一个线程安全的单例模式(Singleton)?(答案)</p><p>请参考答案中的示例代码,这里面一步一步教你创建一个线程安全的 Java 单例类。当我们说线程安全时,意思是即使初始化是在多线程环境中,仍然能保证单个实例。Java 中,使用枚举作为单例类是最简单的方式来创建线程安全单例模式的方式。</p><p>14)Java 中 sleep 方法和 wait 方法的区别?(答案)</p><p>虽然两者都是用来暂停当前运行的线程,但是 sleep() 实际上只是短暂停顿,因为它不会释放锁,而 wait() 意味着条件等待,这就是为什么该方法要释放锁,因为只有这样,其他等待的线程才能在满足条件时获取到该锁。</p><p>15)什么是不可变对象(immutable object)?Java 中怎么创建一个不可变对象?(答案)</p><p>不可变对象指对象一旦被创建,状态就不能再改变。任何修改都会创建一个新的对象,如 String、Integer及其它包装类。详情参见答案,一步一步指导你在 Java 中创建一个不可变的类。</p><p>16)我们能创建一个包含可变对象的不可变对象吗?</p><p>是的,我们是可以创建一个包含可变对象的不可变对象的,你只需要谨慎一点,不要共享可变对象的引用就可以了,如果需要变化时,就返回原对象的一个拷贝。最常见的例子就是对象中包含一个日期对象的引用。</p><p>数据类型和 Java 基础面试问题</p><p>17)Java 中应该使用什么数据类型来代表价格?(答案)</p><p>如果不是特别关心内存和性能的话,使用BigDecimal,否则使用预定义精度的 double 类型。</p><p>18)怎么将 byte 转换为 String?(答案)</p><p>可以使用 String 接收 byte[] 参数的构造器来进行转换,需要注意的点是要使用的正确的编码,否则会使用平台默认编码,这个编码可能跟原来的编码相同,也可能不同。</p><p>19)Java 中怎样将 bytes 转换为 long 类型?</p><p>这个问题你来回答 :-)</p><p>20)我们能将 int 强制转换为 byte 类型的变量吗?如果该值大于 byte 类型的范围,将会出现什么现象?</p><p>是的,我们可以做强制转换,但是 Java 中 int 是 32 位的,而 byte 是 8 位的,所以,如果强制转化是,int 类型的高 24 位将会被丢弃,byte 类型的范围是从 -128 到 128。</p><p>21)存在两个类,B 继承 A,C 继承 B,我们能将 B 转换为 C 么?如 C = (C) B</p><p>22)哪个类包含 clone 方法?是 Cloneable 还是 Object?</p><p>java.lang.Cloneable 是一个标示性接口,不包含任何方法,clone 方法在 object 类中定义。并且需要知道 clone() 方法是一个本地方法,这意味着它是由 c 或 c++ 或 其他本地语言实现的。</p><p>23)Java 中 ++ 操作符是线程安全的吗?</p><p>23)不是线程安全的操作。它涉及到多个指令,如读取变量值,增加,然后存储回内存,这个过程可能会出现多个线程交差。</p><p>24)a = a + b 与 a += b 的区别(答案)</p><p>+= 隐式的将加操作的结果类型强制转换为持有结果的类型。如果两这个整型相加,如 byte、short 或者 int,首先会将它们提升到 int 类型,然后在执行加法操作。如果加法操作的结果比 a 的最大值要大,则 a+b 会出现编译错误,但是 a += b 没问题,如下:</p><p>byte a = 127;</p><p>byte b = 127;</p><p>b = a + b; // error : cannot convert from int to byte</p><p>b += a; // ok</p><p>(译者注:这个地方应该表述的有误,其实无论 a+b 的值为多少,编译器都会报错,因为 a+b 操作会将 a、b 提升为 int 类型,所以将 int 类型赋值给 byte 就会编译出错)</p><p>25)我能在不进行强制转换的情况下将一个 double 值赋值给 long 类型的变量吗?(答案)</p><p>不行,你不能在没有强制类型转换的前提下将一个 double 值赋值给 long 类型的变量,因为 double 类型的范围比 long 类型更广,所以必须要进行强制转换。</p><p>26)3*0.1 == 0.3 将会返回什么?true 还是 false?(答案)</p><p>false,因为有些浮点数不能完全精确的表示出来。</p><p>27)int 和 Integer 哪个会占用更多的内存?(答案)</p><p>Integer 对象会占用更多的内存。Integer 是一个对象,需要存储对象的元数据。但是 int 是一个原始类型的数据,所以占用的空间更少。</p><p>28)为什么 Java 中的 String 是不可变的(Immutable)?(answer答案)</p><p>Java 中的 String 不可变是因为 Java 的设计者认为字符串使用非常频繁,将字符串设置为不可变可以允许多个客户端之间共享相同的字符串。更详细的内容参见答案。</p><p>29)我们能在 Switch 中使用 String 吗?(answer答案)</p><p>从 Java 7 开始,我们可以在 switch case 中使用字符串,但这仅仅是一个语法糖。内部实现在 switch 中使用字符串的 hash code。</p><p>30)Java 中的构造器链是什么?(answer答案)</p><p>当你从一个构造器中调用另一个构造器,就是Java 中的构造器链。这种情况只在重载了类的构造器的时候才会出现。</p><p>JVM 底层 与 GC(Garbage Collection) 的面试问题</p><p>31)64 位 JVM 中,int 的长度是多数?</p><p>Java 中,int 类型变量的长度是一个固定值,与平台无关,都是 32 位。意思就是说,在 32 位 和 64 位 的Java 虚拟机中,int 类型的长度是相同的。</p><p>32)Serial 与 Parallel GC之间的不同之处?(答案)</p><p>Serial 与 Parallel 在GC执行的时候都会引起 stop-the-world。它们之间主要不同 serial 收集器是默认的复制收集器,执行 GC 的时候只有一个线程,而 parallel 收集器使用多个 GC 线程来执行。</p><p>33)32 位和 64 位的 JVM,int 类型变量的长度是多数?(答案)</p><p>32 位和 64 位的 JVM 中,int 类型变量的长度是相同的,都是 32 位或者 4 个字节。</p><p>34)Java 中 WeakReference 与 SoftReference的区别?(答案)</p><p>虽然 WeakReference 与 SoftReference 都有利于提高 GC 和 内存的效率,但是 WeakReference ,一旦失去最后一个强引用,就会被 GC 回收,而软引用虽然不能阻止被回收,但是可以延迟到 JVM 内存不足的时候。</p><p>35)WeakHashMap 是怎么工作的?(答案)</p><p>WeakHashMap 的工作与正常的 HashMap 类似,但是使用弱引用作为 key,意思就是当 key 对象没有任何引用时,key/value 将会被回收。</p><p>36)JVM 选项 -XX:+UseCompressedOops 有什么作用?为什么要使用?(答案)</p><p>当你将你的应用从 32 位的 JVM 迁移到 64 位的 JVM 时,由于对象的指针从 32 位增加到了 64 位,因此堆内存会突然增加,差不多要翻倍。这也会对 CPU 缓存(容量比内存小很多)的数据产生不利的影响。因为,迁移到 64 位的 JVM 主要动机在于可以指定最大堆大小,通过压缩 OOP 可以节省一定的内存。通过 -XX:+UseCompressedOops 选项,JVM 会使用 32 位的 OOP,而不是 64 位的 OOP。</p><p>37)怎样通过 Java 程序来判断 JVM 是 32 位 还是 64 位?(答案)</p><p>你可以检查某些系统属性如 sun.arch.data.model 或 os.arch 来获取该信息。</p><p>38)32 位 JVM 和 64 位 JVM 的最大堆内存分别是多数?(答案)</p><p>理论上说上 32 位的 JVM 堆内存可以到达 2^32,即 4GB,但实际上会比这个小很多。不同操作系统之间不同,如 Windows 系统大约 1.5 GB,Solaris 大约 3GB。64 位 JVM允许指定最大的堆内存,理论上可以达到 2^64,这是一个非常大的数字,实际上你可以指定堆内存大小到 100GB。甚至有的 JVM,如 Azul,堆内存到 1000G 都是可能的。</p><p>39)JRE、JDK、JVM 及 JIT 之间有什么不同?(答案)</p><p>JRE 代表 Java 运行时(Java run-time),是运行 Java 引用所必须的。JDK 代表 Java 开发工具(Java development kit),是 Java 程序的开发工具,如 Java 编译器,它也包含 JRE。JVM 代表 Java 虚拟机(Java virtual machine),它的责任是运行 Java 应用。JIT 代表即时编译(Just In Time compilation),当代码执行的次数超过一定的阈值时,会将 Java 字节码转换为本地代码,如,主要的热点代码会被准换为本地代码,这样有利大幅度提高 Java 应用的性能。</p><div class=\"image-package\">\r\n<div class=\"image-container\" style=\"max-width: 320px; max-height: 213px;\">\r\n<div class=\"image-container-fill\" style=\"padding-bottom: 66.56%;\"></div>\r\n<div class=\"image-view\" data-width=\"320\" data-height=\"213\"><img data-original-src=\"//upload-images.jianshu.io/upload_images/5275417-99415266cdcb8dc9\" data-original-width=\"320\" data-original-height=\"213\" data-original-format=\"image/jpeg\" data-original-filesize=\"12635\" class=\"image-loading\" style=\"cursor: zoom-in;\"></div>\r\n</div>\r\n<div class=\"image-caption\"></div>\r\n</div><p>3 年工作经验的 Java 面试题</p><p>40)解释 Java 堆空间及 GC?(答案)</p><p>当通过 Java 命令启动 Java 进程的时候,会为它分配内存。内存的一部分用于创建堆空间,当程序中创建对象的时候,就从对空间中分配内存。GC 是 JVM 内部的一个进程,回收无效对象的内存用于将来的分配。</p><div class=\"image-package\">\r\n<div class=\"image-container\" style=\"max-width: 400px; max-height: 124px;\">\r\n<div class=\"image-container-fill\" style=\"padding-bottom: 31.0%;\"></div>\r\n<div class=\"image-view\" data-width=\"400\" data-height=\"124\"><img data-original-src=\"//upload-images.jianshu.io/upload_images/5275417-ef579891a5d3b8c6\" data-original-width=\"400\" data-original-height=\"124\" data-original-format=\"image/jpeg\" data-original-filesize=\"6973\" class=\"image-loading\" style=\"cursor: zoom-in;\"></div>\r\n</div>\r\n<div class=\"image-caption\"></div>\r\n</div><p>JVM 底层面试题及答案</p><p>41)你能保证 GC 执行吗?(答案)</p><p>不能,虽然你可以调用 System.gc() 或者 Runtime.gc(),但是没有办法保证 GC 的执行。</p><p>42)怎么获取 Java 程序使用的内存?堆使用的百分比?</p><p>可以通过 java.lang.Runtime 类中与内存相关方法来获取剩余的内存,总内存及最大堆内存。通过这些方法你也可以获取到堆使用的百分比及堆内存的剩余空间。Runtime.freeMemory() 方法返回剩余空间的字节数,Runtime.totalMemory() 方法总内存的字节数,Runtime.maxMemory() 返回最大内存的字节数。</p><p>43)Java 中堆和栈有什么区别?(答案)</p><p>JVM 中堆和栈属于不同的内存区域,使用目的也不同。栈常用于保存方法帧和局部变量,而对象总是在堆上分配。栈通常都比堆小,也不会在多个线程之间共享,而堆被整个 JVM 的所有线程共享。</p><div class=\"image-package\">\r\n<div class=\"image-container\" style=\"max-width: 400px; max-height: 212px;\">\r\n<div class=\"image-container-fill\" style=\"padding-bottom: 53.0%;\"></div>\r\n<div class=\"image-view\" data-width=\"400\" data-height=\"212\"><img data-original-src=\"//upload-images.jianshu.io/upload_images/5275417-3ef59f05d125cfac\" data-original-width=\"400\" data-original-height=\"212\" data-original-format=\"image/jpeg\" data-original-filesize=\"13716\" class=\"image-loading\" style=\"cursor: zoom-in;\"></div>\r\n</div>\r\n<div class=\"image-caption\"></div>\r\n</div><p>关于内存的的面试问题和答案</p><p>Java 基本概念面试题</p><p>44)“a==b”和”a.equals(b)”有什么区别?(答案)</p><p>如果 a 和 b 都是对象,则 a==b 是比较两个对象的引用,只有当 a 和 b 指向的是堆中的同一个对象才会返回 true,而 a.equals(b) 是进行逻辑比较,所以通常需要重写该方法来提供逻辑一致性的比较。例如,String 类重写 equals() 方法,所以可以用于两个不同对象,但是包含的字母相同的比较。</p><p>45)a.hashCode() 有什么用?与 a.equals(b) 有什么关系?(答案)</p><p>hashCode() 方法是相应对象整型的 hash 值。它常用于基于 hash 的集合类,如 Hashtable、HashMap、LinkedHashMap等等。它与 equals() 方法关系特别紧密。根据 Java 规范,两个使用 equal() 方法来判断相等的对象,必须具有相同的 hash code。</p><p>46)final、finalize 和 finally 的不同之处?(答案)</p><p>final 是一个修饰符,可以修饰变量、方法和类。如果 final 修饰变量,意味着该变量的值在初始化后不能被改变。finalize 方法是在对象被回收之前调用的方法,给对象自己最后一个复活的机会,但是什么时候调用 finalize 没有保证。finally 是一个关键字,与 try 和 catch 一起用于异常的处理。finally 块一定会被执行,无论在 try 块中是否有发生异常。</p><p>47)Java 中的编译期常量是什么?使用它又什么风险?</p><p>公共静态不可变(public static final )变量也就是我们所说的编译期常量,这里的 public 可选的。实际上这些变量在编译时会被替换掉,因为编译器知道这些变量的值,并且知道这些变量在运行时不能改变。这种方式存在的一个问题是你使用了一个内部的或第三方库中的公有编译时常量,但是这个值后面被其他人改变了,但是你的客户端仍然在使用老的值,甚至你已经部署了一个新的jar。为了避免这种情况,当你在更新依赖 JAR 文件时,确保重新编译你的程序。</p><p>Java 集合框架的面试题</p><p><b>这部分也包含数据结构、算法及数组的面试问题</b></p><p>48) List、Set、Map 和 Queue 之间的区别(答案)</p><p>List 是一个有序集合,允许元素重复。它的某些实现可以提供基于下标值的常量访问时间,但是这不是 List 接口保证的。Set 是一个无序集合。</p><p>49)poll() 方法和 remove() 方法的区别?</p><p>poll() 和 remove() 都是从队列中取出一个元素,但是 poll() 在获取元素失败的时候会返回空,但是 remove() 失败的时候会抛出异常。</p><p>50)Java 中 LinkedHashMap 和 PriorityQueue 的区别是什么?(答案)</p><p>PriorityQueue 保证最高或者最低优先级的的元素总是在队列头部,但是 LinkedHashMap 维持的顺序是元素插入的顺序。当遍历一个 PriorityQueue 时,没有任何顺序保证,但是 LinkedHashMap 课保证遍历顺序是元素插入的顺序。</p><p>51)ArrayList 与 LinkedList 的不区别?(答案)</p><p>最明显的区别是 ArrrayList 底层的数据结构是数组,支持随机访问,而 LinkedList 的底层数据结构书链表,不支持随机访问。使用下标访问一个元素,ArrayList 的时间复杂度是 O(1),而 LinkedList 是 O(n)。更多细节的讨论参见答案。</p><p>52)用哪两种方式来实现集合的排序?(答案)</p><p>你可以使用有序集合,如 TreeSet 或 TreeMap,你也可以使用有顺序的的集合,如 list,然后通过 Collections.sort() 来排序。</p><p>53)Java 中怎么打印数组?(answer答案)</p><p>你可以使用 Arrays.toString() 和 Arrays.deepToString() 方法来打印数组。由于数组没有实现 toString() 方法,所以如果将数组传递给 System.out.println() 方法,将无法打印出数组的内容,但是 Arrays.toString() 可以打印每个元素。</p><p>54)Java 中的 LinkedList 是单向链表还是双向链表?(答案)</p><p>是双向链表,你可以检查 JDK 的源码。在 Eclipse,你可以使用快捷键 Ctrl + T,直接在编辑器中打开该类。</p><p>55)Java 中的 TreeMap 是采用什么树实现的?(答案)</p><p>Java 中的 TreeMap 是使用红黑树实现的。</p><p>56) Hashtable 与 HashMap 有什么不同之处?(答案)</p><p>这两个类有许多不同的地方,下面列出了一部分:</p><p>a) Hashtable 是 JDK 1 遗留下来的类,而 HashMap 是后来增加的。</p><p>b)Hashtable 是同步的,比较慢,但 HashMap 没有同步策略,所以会更快。</p><p>c)Hashtable 不允许有个空的 key,但是 HashMap 允许出现一个 null key。</p><p>57)Java 中的 HashSet,内部是如何工作的?(answer答案)</p><p>HashSet 的内部采用 HashMap来实现。由于 Map 需要 key 和 value,所以所有 key 的都有一个默认 value。类似于 HashMap,HashSet 不允许重复的 key,只允许有一个null key,意思就是 HashSet 中只允许存储一个 null 对象。</p><p>58)写一段代码在遍历 ArrayList 时移除一个元素?(答案)</p><p>该问题的关键在于面试者使用的是 ArrayList 的 remove() 还是 Iterator 的 remove()方法。这有一段示例代码,是使用正确的方式来实现在遍历的过程中移除元素,而不会出现 ConcurrentModificationException 异常的示例代码。</p><p>59)我们能自己写一个容器类,然后使用 for-each 循环码?</p><p>可以,你可以写一个自己的容器类。如果你想使用 Java 中增强的循环来遍历,你只需要实现 Iterable 接口。如果你实现 Collection 接口,默认就具有该属性。</p><p>60)ArrayList 和 HashMap 的默认大小是多数?(答案)</p><p>在 Java 7 中,ArrayList 的默认大小是 10 个元素,HashMap 的默认大小是16个元素(必须是2的幂)。这就是 Java 7 中 ArrayList 和 HashMap 类的代码片段:</p><p>// from ArrayList.java JDK 1.7</p><p><b>private</b>&nbsp;<b>static</b>&nbsp;<b>final</b>&nbsp;<b>int</b>&nbsp;DEFAULT_CAPACITY = 10;</p><p>//from HashMap.java JDK 7</p><p><b>static</b>&nbsp;<b>final</b>&nbsp;<b>int</b>&nbsp;DEFAULT_INITIAL_CAPACITY = 1 &lt;&lt; 4; // aka 16</p><p>61)有没有可能两个不相等的对象有有相同的 hashcode?</p><p>有可能,两个不相等的对象可能会有相同的 hashcode 值,这就是为什么在 hashmap 中会有冲突。相等 hashcode 值的规定只是说如果两个对象相等,必须有相同的hashcode 值,但是没有关于不相等对象的任何规定。</p><p>62)两个相同的对象会有不同的的 hash code 吗?</p><p>不能,根据 hash code 的规定,这是不可能的。</p><p>63)我们可以在 hashcode() 中使用随机数字吗?(答案)</p><p>不行,因为对象的 hashcode 值必须是相同的。参见答案获取更多关于 Java 中重写 hashCode() 方法的知识。</p><p>64)Java 中,Comparator 与 Comparable 有什么不同?(答案)</p><p>Comparable 接口用于定义对象的自然顺序,而 comparator 通常用于定义用户定制的顺序。Comparable 总是只有一个,但是可以有多个 comparator 来定义对象的顺序。</p><p>65)为什么在重写 equals 方法的时候需要重写 hashCode 方法?(答案)</p><p>因为有强制的规范指定需要同时重写 hashcode 与 equal 是方法,许多容器类,如 HashMap、HashSet 都依赖于 hashcode 与 equals 的规定。</p><p>Java IO 和 NIO 的面试题</p><p>IO 是 Java 面试中一个非常重要的点。你应该很好掌握 Java IO,NIO,NIO2 以及与操作系统,磁盘 IO 相关的基础知识。下面是 Java IO 中经常问的问题。</p><p>66)在我 Java 程序中,我有三个 socket,我需要多少个线程来处理?</p><p>67)Java 中怎么创建 ByteBuffer?</p><p>68)Java 中,怎么读写 ByteBuffer ?</p><p>69)Java 采用的是大端还是小端?</p><p>70)ByteBuffer 中的字节序是什么?</p><p>71)Java 中,直接缓冲区与非直接缓冲器有什么区别?(答案)</p><p>72)Java 中的内存映射缓存区是什么?(answer答案)</p><p>73)socket 选项 TCP NO DELAY 是指什么?</p><p>74)TCP 协议与 UDP 协议有什么区别?(answer答案)</p><p>75)Java 中,ByteBuffer 与 StringBuffer有什么区别?(答案)</p><p>Java 最佳实践的面试问题</p><p>包含 Java 中各个部分的最佳实践,如集合,字符串,IO,多线程,错误和异常处理,设计模式等等。</p><p>76)Java 中,编写多线程程序的时候你会遵循哪些最佳实践?(答案)</p><p>这是我在写Java 并发程序的时候遵循的一些最佳实践:</p><p>a)给线程命名,这样可以帮助调试。</p><p>b)最小化同步的范围,而不是将整个方法同步,只对关键部分做同步。</p><p>c)如果可以,更偏向于使用 volatile 而不是 synchronized。</p><p>d)使用更高层次的并发工具,而不是使用 wait() 和 notify() 来实现线程间通信,如 BlockingQueue,CountDownLatch 及 Semeaphore。</p><p>e)优先使用并发集合,而不是对集合进行同步。并发集合提供更好的可扩展性。</p><p>77)说出几点 Java 中使用 Collections 的最佳实践(答案)</p><p>这是我在使用 Java 中 Collectionc 类的一些最佳实践:</p><p>a)使用正确的集合类,例如,如果不需要同步列表,使用 ArrayList 而不是 Vector。</p><p>b)优先使用并发集合,而不是对集合进行同步。并发集合提供更好的可扩展性。</p><p>c)使用接口代表和访问集合,如使用List存储 ArrayList,使用 Map 存储 HashMap 等等。</p><p>d)使用迭代器来循环集合。</p><p>e)使用集合的时候使用泛型。</p><p>78)说出至少 5 点在 Java 中使用线程的最佳实践。(答案)</p><p>这个问题与之前的问题类似,你可以使用上面的答案。对线程来说,你应该:</p><p>a)对线程命名</p><p>b)将线程和任务分离,使用线程池执行器来执行 Runnable 或 Callable。</p><p>c)使用线程池</p><p>79)说出 5 条 IO 的最佳实践(答案)</p><p>IO 对 Java 应用的性能非常重要。理想情况下,你不应该在你应用的关键路径上避免 IO 操作。下面是一些你应该遵循的 Java IO 最佳实践:</p><p>a)使用有缓冲区的 IO 类,而不要单独读取字节或字符。</p><p>b)使用 NIO 和 NIO2</p><p>c)在 finally 块中关闭流,或者使用 try-with-resource 语句。</p><p>d)使用内存映射文件获取更快的 IO。</p><p>80)列出 5 个应该遵循的 JDBC 最佳实践(答案)</p><p>有很多的最佳实践,你可以根据你的喜好来例举。下面是一些更通用的原则:</p><p>a)使用批量的操作来插入和更新数据</p><p>b)使用 PreparedStatement 来避免 SQL 异常,并提高性能。</p><p>c)使用数据库连接池</p><p>d)通过列名来获取结果集,不要使用列的下标来获取。</p><p>81)说出几条 Java 中方法重载的最佳实践?(答案)</p><p>下面有几条可以遵循的方法重载的最佳实践来避免造成自动装箱的混乱。</p><p>a)不要重载这样的方法:一个方法接收 int 参数,而另个方法接收 Integer 参数。</p><p>b)不要重载参数数量一致,而只是参数顺序不同的方法。</p><p>c)如果重载的方法参数个数多于 5 个,采用可变参数。</p><p>Date、Time 及 Calendar 的面试题</p><p>82)在多线程环境下,SimpleDateFormat 是线程安全的吗?(答案)</p><p>不是,非常不幸,DateFormat 的所有实现,包括 SimpleDateFormat 都不是线程安全的,因此你不应该在多线程序中使用,除非是在对外线程安全的环境中使用,如 将 SimpleDateFormat 限制在 ThreadLocal 中。如果你不这么做,在解析或者格式化日期的时候,可能会获取到一个不正确的结果。因此,从日期、时间处理的所有实践来说,我强力推荐 joda-time 库。</p><p>83)Java 中如何格式化一个日期?如格式化为 ddMMyyyy 的形式?(答案)</p><p>Java 中,可以使用 SimpleDateFormat 类或者 joda-time 库来格式日期。DateFormat 类允许你使用多种流行的格式来格式化日期。参见答案中的示例代码,代码中演示了将日期格式化成不同的格式,如 dd-MM-yyyy 或 ddMMyyyy。</p><p>84)Java 中,怎么在格式化的日期中显示时区?</p><p>85)Java 中 java.util.Date 与 java.sql.Date 有什么区别?</p><p>86)Java 中,如何计算两个日期之间的差距?(程序)</p><p>87)Java 中,如何将字符串 YYYYMMDD 转换为日期?</p><p>单元测试 JUnit 面试题</p><p>89)如何测试静态方法?(答案)</p><p>可以使用 PowerMock 库来测试静态方法。</p><p>90)怎么利用 JUnit 来测试一个方法的异常?</p><p>91)你使用过哪个单元测试库来测试你的 Java 程序?</p><p>92)@Before 和 @BeforeClass 有什么区别?</p><p>编程和代码相关的面试题</p><p>93)怎么检查一个字符串只包含数字?(解决方案)</p><p>94)Java 中如何利用泛型写一个 LRU 缓存?</p><p>95)写一段 Java 程序将 byte 转换为 long?</p><p>95)在不使用 StringBuffer 的前提下,怎么反转一个字符串?(解决方案)</p><p>97)Java 中,怎么获取一个文件中单词出现的最高频率?(解决方案)</p><p>98)如何检查出两个给定的字符串是反序的?(解决方案)</p><p>99)Java 中,怎么打印出一个字符串的所有排列?(解决方案)</p><p>100)Java 中,怎样才能打印出数组中的重复元素?(解决方案)</p><p>101)Java 中如何将字符串转换为整数?(解决方案)</p><p>102)在没有使用临时变量的情况如何交换两个整数变量的值?(解决方案)</p><p>关于 OOP 和设计模式的面试题</p><p>这部分包含 Java 面试过程中关于 SOLID 的设计原则,OOP 基础,如类,对象,接口,继承,多态,封装,抽象以及更高级的一些概念,如组合、聚合及关联。也包含了 GOF 设计模式的问题。</p><p>103)接口是什么?为什么要使用接口而不是直接使用具体类?</p><p>接口用于定义 API。它定义了类必须得遵循的规则。同时,它提供了一种抽象,因为客户端只使用接口,这样可以有多重实现,如 List 接口,你可以使用可随机访问的 ArrayList,也可以使用方便插入和删除的 LinkedList。接口中不允许写代码,以此来保证抽象,但是 Java 8 中你可以在接口声明静态的默认方法,这种方法是具体的。</p><p>104)Java 中,抽象类与接口之间有什么不同?(答案)</p><p>Java 中,抽象类和接口有很多不同之处,但是最重要的一个是 Java 中限制一个类只能继承一个类,但是可以实现多个接口。抽象类可以很好的定义一个家族类的默认行为,而接口能更好的定义类型,有助于后面实现多态机制。关于这个问题的讨论请查看答案。</p><p>105)除了单例模式,你在生产环境中还用过什么设计模式?</p><p>这需要根据你的经验来回答。一般情况下,你可以说依赖注入,工厂模式,装饰模式或者观察者模式,随意选择你使用过的一种即可。不过你要准备回答接下的基于你选择的模式的问题。</p><p>106)你能解释一下里氏替换原则吗?</p><p>107) 什么情况下会违反迪米特法则?为什么会有这个问题?</p><p>迪米特法则建议“只和朋友说话,不要陌生人说话”,以此来减少类之间的耦合。</p><p>108)适配器模式是什么?什么时候使用?</p><p>适配器模式提供对接口的转换。如果你的客户端使用某些接口,但是你有另外一些接口,你就可以写一个适配去来连接这些接口。</p><p>109)什么是“依赖注入”和“控制反转”?为什么有人使用?</p><p>110)抽象类是什么?它与接口有什么区别?你为什么要使用过抽象类?</p><p>111)构造器注入和 setter 依赖注入,那种方式更好?(答案)</p><p>每种方式都有它的缺点和优点。构造器注入保证所有的注入都被初始化,但是 setter 注入提供更好的灵活性来设置可选依赖。如果使用 XML 来描述依赖,Setter 注入的可读写会更强。经验法则是强制依赖使用构造器注入,可选依赖使用 setter 注入。</p><p>112)依赖注入和工程模式之间有什么不同?(答案)</p><p>虽然两种模式都是将对象的创建从应用的逻辑中分离,但是依赖注入比工程模式更清晰。通过依赖注入,你的类就是 POJO,它只知道依赖而不关心它们怎么获取。使用工厂模式,你的类需要通过工厂来获取依赖。因此,使用 DI 会比使用工厂模式更容易测试。关于这个话题的更详细讨论请参见答案。</p><p>113)适配器模式和装饰器模式有什么区别?(答案)</p><p>虽然适配器模式和装饰器模式的结构类似,但是每种模式的出现意图不同。适配器模式被用于桥接两个接口,而装饰模式的目的是在不修改类的情况下给类增加新的功能。</p><p>114)适配器模式和代理模式之前有什么不同?(答案)</p><p>这个问题与前面的类似,适配器模式和代理模式的区别在于他们的意图不同。由于适配器模式和代理模式都是封装真正执行动作的类,因此结构是一致的,但是适配器模式用于接口之间的转换,而代理模式则是增加一个额外的中间层,以便支持分配、控制或智能访问。</p><p>115)什么是模板方法模式?(答案)</p><p>模板方法提供算法的框架,你可以自己去配置或定义步骤。例如,你可以将排序算法看做是一个模板。它定义了排序的步骤,但是具体的比较,可以使用 Comparable 或者其语言中类似东西,具体策略由你去配置。列出算法概要的方法就是众所周知的模板方法。</p><p>116)什么时候使用访问者模式?(答案)</p><p>访问者模式用于解决在类的继承层次上增加操作,但是不直接与之关联。这种模式采用双派发的形式来增加中间层。</p><p>117)什么时候使用组合模式?(答案)</p><p>组合模式使用树结构来展示部分与整体继承关系。它允许客户端采用统一的形式来对待单个对象和对象容器。当你想要展示对象这种部分与整体的继承关系时采用组合模式。</p><p>118)继承和组合之间有什么不同?(答案)</p><p>虽然两种都可以实现代码复用,但是组合比继承共灵活,因为组合允许你在运行时选择不同的实现。用组合实现的代码也比继承测试起来更加简单。</p><p>119)描述 Java 中的重载和重写?(答案)</p><p>重载和重写都允许你用相同的名称来实现不同的功能,但是重载是编译时活动,而重写是运行时活动。你可以在同一个类中重载方法,但是只能在子类中重写方法。重写必须要有继承。</p><p>120)Java 中,嵌套公共静态类与顶级类有什么不同?(答案)</p><p>类的内部可以有多个嵌套公共静态类,但是一个 Java 源文件只能有一个顶级公共类,并且顶级公共类的名称与源文件名称必须一致。</p><p>121) OOP 中的 组合、聚合和关联有什么区别?(答案)</p><p>如果两个对象彼此有关系,就说他们是彼此相关联的。组合和聚合是面向对象中的两种形式的关联。组合是一种比聚合更强力的关联。组合中,一个对象是另一个的拥有者,而聚合则是指一个对象使用另一个对象。如果对象 A 是由对象 B 组合的,则 A 不存在的话,B一定不存在,但是如果 A 对象聚合了一个对象 B,则即使 A 不存在了,B 也可以单独存在。</p><p>122)给我一个符合开闭原则的设计模式的例子?(答案)</p><p>开闭原则要求你的代码对扩展开放,对修改关闭。这个意思就是说,如果你想增加一个新的功能,你可以很容易的在不改变已测试过的代码的前提下增加新的代码。有好几个设计模式是基于开闭原则的,如策略模式,如果你需要一个新的策略,只需要实现接口,增加配置,不需要改变核心逻辑。一个正在工作的例子是 Collections.sort() 方法,这就是基于策略模式,遵循开闭原则的,你不需为新的对象修改 sort() 方法,你需要做的仅仅是实现你自己的 Comparator 接口。</p><p>123)抽象工厂模式和原型模式之间的区别?</p><p>124)什么时候使用享元模式?</p><p>享元模式通过共享对象来避免创建太多的对象。为了使用享元模式,你需要确保你的对象是不可变的,这样你才能安全的共享。JDK 中 String 池、Integer 池以及 Long 池都是很好的使用了享元模式的例子。</p><p>Java 面试中其他各式各样的问题</p><p>这部分包含 Java 中关于 XML 的面试题,JDBC 面试题,正则表达式面试题,Java 错误和异常及序列化面试题</p><p>125)嵌套静态类与顶级类有什么区别?(答案)</p><p>一个公共的顶级类的源文件名称与类名相同,而嵌套静态类没有这个要求。一个嵌套类位于顶级类内部,需要使用顶级类的名称来引用嵌套静态类,如 HashMap.Entry 是一个嵌套静态类,HashMap 是一个顶级类,Entry是一个嵌套静态类。</p><p>126)你能写出一个正则表达式来判断一个字符串是否是一个数字吗?(解决方案)</p><p>一个数字字符串,只能包含数字,如 0 到 9 以及 +、- 开头,通过这个信息,你可以下一个如下的正则表达式来判断给定的字符串是不是数字。</p><p>127)Java 中,受检查异常 和 不受检查异常的区别?(答案)</p><p>受检查异常编译器在编译期间检查。对于这种异常,方法强制处理或者通过 throws 子句声明。其中一种情况是 Exception 的子类但不是 RuntimeException 的子类。非受检查是 RuntimeException 的子类,在编译阶段不受编译器的检查。</p><p>128)Java 中,throw 和 throws 有什么区别?(答案)</p><p>throw 用于抛出 java.lang.Throwable 类的一个实例化对象,意思是说你可以通过关键字 throw 抛出一个 Error 或者 一个Exception,如:</p><p>throw new IllegalArgumentException(“size must be multiple of 2″)</p><p>而throws 的作用是作为方法声明和签名的一部分,方法被抛出相应的异常以便调用者能处理。Java 中,任何未处理的受检查异常强制在 throws 子句中声明。</p><p>129)Java 中,Serializable 与 Externalizable 的区别?(答案)</p><p>Serializable 接口是一个序列化 Java 类的接口,以便于它们可以在网络上传输或者可以将它们的状态保存在磁盘上,是 JVM 内嵌的默认序列化方式,成本高、脆弱而且不安全。Externalizable 允许你控制整个序列化过程,指定特定的二进制格式,增加安全机制。</p><p>130)Java 中,DOM 和 SAX 解析器有什么不同?(答案)</p><p>DOM 解析器将整个 XML 文档加载到内存来创建一棵 DOM 模型树,这样可以更快的查找节点和修改 XML 结构,而 SAX 解析器是一个基于事件的解析器,不会将整个 XML 文档加载到内存。由于这个原因,DOM 比 SAX 更快,也要求更多的内存,不适合于解析大 XML 文件。</p><p>131)说出 JDK 1.7 中的三个新特性?(答案)</p><p>虽然 JDK 1.7 不像 JDK 5 和 8 一样的大版本,但是,还是有很多新的特性,如 try-with-resource 语句,这样你在使用流或者资源的时候,就不需要手动关闭,Java 会自动关闭。Fork-Join 池某种程度上实现 Java 版的 Map-reduce。允许 Switch 中有 String 变量和文本。菱形操作符(&lt;&gt;)用于类型推断,不再需要在变量声明的右边申明泛型,因此可以写出可读写更强、更简洁的代码。另一个值得一提的特性是改善异常处理,如允许在同一个 catch 块中捕获多个异常。</p><p>132)说出 5 个 JDK 1.8 引入的新特性?(答案)</p><p>Java 8 在 Java 历史上是一个开创新的版本,下面 JDK 8 中 5 个主要的特性:</p><p>Lambda 表达式,允许像对象一样传递匿名函数</p><p>Stream API,充分利用现代多核 CPU,可以写出很简洁的代码</p><p>Date 与 Time API,最终,有一个稳定、简单的日期和时间库可供你使用</p><p>扩展方法,现在,接口中可以有静态、默认方法。</p><p>重复注解,现在你可以将相同的注解在同一类型上使用多次。</p><p>133)Java 中,Maven 和 ANT 有什么区别?(答案)</p><p>虽然两者都是构建工具,都用于创建 Java 应用,但是 Maven 做的事情更多,在基于“约定优于配置”的概念下,提供标准的Java 项目结构,同时能为应用自动管理依赖(应用中所依赖的 JAR 文件),Maven 与 ANT 工具更多的不同之处请参见答案。</p><p>这就是所有的面试题,如此之多,是不是?我可以保证,如果你能回答列表中的所有问题,你就可以很轻松的应付任何核心 Java 或者高级 Java 面试。虽然,这里没有涵盖 Servlet、JSP、JSF、JPA,JMS,EJB 及其它 Java EE 技术,也没有包含主流的框架如 Spring MVC,Struts 2.0,Hibernate,也没有包含 SOAP 和 RESTful web service,但是这份列表对做 Java 开发的、准备应聘 Java web 开发职位的人还是同样有用的,因为所有的 Java 面试,开始的问题都是 Java 基础和 JDK API 相关的。如果你认为我这里有任何应该在这份列表中而被我遗漏了的 Java 流行的问题,你可以自由的给我建议。我的目的是从最近的面试中创建一份最新的、最优的 Java 面试问题列表。</p>\r\n </div>');
INSERT INTO `rumo_blog` VALUES ('14', '首个多端 UI 组件库 - Taro UI 发布', '1', '首个多端 UI 组件库 - Taro UI 发布', '1', '2018-11-11 21:02:26', '2018-11-11 21:02:26', '1', '0', 'https://misc.aotu.io/jimczj/2018-08-27taro-ui.jpg', '', '0', '0', '<a id=\"more\"></a>\n<h2 id=\"前言\" class=\"post-heading\"><a href=\"#前言\" class=\"headerlink\" title=\"前言\"></a>前言<a class=\"post-anchor\" href=\"#前言\" aria-hidden=\"true\"></a></h2>\n<p><a href=\"https://github.com/NervJS/taro\" target=\"_blank\" rel=\"external\">Taro</a> 是由凹凸实验室倾力打造的多端开发解决方案,旨在让一套代码在多端运行。Taro 1.0 版本发布后,也开始支持引用第三方的小程序组件库,如 iView、vant-weapp、echarts-for-weixin,然而目前市场仍然缺少一套由 Taro 编写而成的多端 UI 组件库。为了进一步丰富 Taro 的开发生态,让开发者有更好的开发体验和更快的开发速度,凹凸实验室自主设计了一套 UI 组件库,经过两个多月的精雕细琢,终于跟随 Taro 1.0 版本正式发布。</p>\n<h2 id=\"Taro-UI\" class=\"post-heading\"><a href=\"#Taro-UI\" class=\"headerlink\" title=\"Taro UI\"></a>Taro UI<a class=\"post-anchor\" href=\"#Taro-UI\" aria-hidden=\"true\"></a></h2>\n<p>Taro UI 是一款由凹凸实验室打造、基于 Taro 编写的多端 UI 组件库。除了高颜值,Taro UI 最大的亮点就是支持多端运行。Taro UI 借助 Taro 支持多端运行的特点,只需解决不同平台 CSS 的表现差异问题,就可以在微信小程序/ H5 / ReactNative 等多端适配运行。</p>\n<p><strong>Github</strong>:<a href=\"https://github.com/NervJS/taro-ui\" target=\"_blank\" rel=\"external\">https://github.com/NervJS/taro-ui</a></p>\n<p><strong>文档</strong>:<a href=\"https://taro-ui.aotu.io/\" target=\"_blank\" rel=\"external\">https://taro-ui.aotu.io/</a></p>\n<p><strong>H5 版本预览</strong>:</p>\n<p><img src=\"https://user-images.githubusercontent.com/13499146/44632148-8a054080-a9a8-11e8-85a8-dfafd073dfdf.png\" alt=\"image\"></p>\n<p><strong>微信小程序预览</strong>:</p>\n<p><img src=\"https://user-images.githubusercontent.com/13499146/44643836-8e5f4700-aa04-11e8-87bd-d930eb04e87c.png\" alt=\"image\"></p>\n<p>第一版组件共有六个模块、三十三个组件,未来还将继续丰富组件,增加一些常用业务组件。</p>\n<p><img src=\"https://user-images.githubusercontent.com/13499146/44502719-6d75b980-a6c5-11e8-8491-b6b47d87ee3d.png\" alt=\"image\"></p>\n<h2 id=\"特性\" class=\"post-heading\"><a href=\"#特性\" class=\"headerlink\" title=\"特性\"></a>特性<a class=\"post-anchor\" href=\"#特性\" aria-hidden=\"true\"></a></h2>\n<ul>\n <li><strong>简单易用</strong>:支持 npm 安装,自动处理 npm 资源之间的依赖关系</li>\n <li><strong>框架支持</strong>:基于 Taro 开发组件,与 Taro 无缝衔接</li>\n <li><strong>多端适配</strong>:一套组件可以在微信小程序/ H5 / ReactNative 等多端适配运行</li>\n <li><strong>样式美观</strong>:小明哥(<a href=\"https://github.com/at-ui/at-ui\" target=\"_blank\" rel=\"external\">AT-UI</a> 设计者、主程)亲自设计,细节把关,符合现代扁平化设计审美</li>\n <li><strong>组件丰富</strong>:提供丰富的基础组件,覆盖大部分使用场景,满足各种功能需求</li>\n <li><strong>按需引用</strong>:可按需使用独立的组件,不必引入所有文件,可最小化注入到项目中</li>\n <li><strong>多套主题</strong>:内置多套主题颜色,任君选择(将在 1.1 版本开放此特性)</li>\n</ul>\n<h2 id=\"快速开始\" class=\"post-heading\"><a href=\"#快速开始\" class=\"headerlink\" title=\"快速开始\"></a>快速开始<a class=\"post-anchor\" href=\"#快速开始\" aria-hidden=\"true\"></a></h2>\n<h3 id=\"安装-Taro\" class=\"post-heading\"><a href=\"#安装-Taro\" class=\"headerlink\" title=\"安装 Taro\"></a>安装 Taro<a class=\"post-anchor\" href=\"#安装-Taro\" aria-hidden=\"true\"></a></h3>\n<p>安装 Taro 开发工具 @tarojs/cli</p>\n<p>使用 npm 或者 yarn 全局安装,或者直接使用 <a href=\"https://medium.com/@maybekatz/introducing-npx-an-npm-package-runner-55f7d4bd282b\" target=\"_blank\" rel=\"external\">npx</a></p>\n<figure class=\"highlight bash\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n $ npm install -g @tarojs/cli\n </div>\n <div class=\"line\">\n $ yarn global add @tarojs/cli\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h3 id=\"初始化项目\" class=\"post-heading\"><a href=\"#初始化项目\" class=\"headerlink\" title=\"初始化项目\"></a>初始化项目<a class=\"post-anchor\" href=\"#初始化项目\" aria-hidden=\"true\"></a></h3>\n<p>使用命令创建模板项目<br></p>\n<figure class=\"highlight bash\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n $ taro init myApp\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<h3 id=\"安装-Taro-UI\" class=\"post-heading\"><a href=\"#安装-Taro-UI\" class=\"headerlink\" title=\"安装 Taro UI\"></a>安装 Taro UI<a class=\"post-anchor\" href=\"#安装-Taro-UI\" aria-hidden=\"true\"></a></h3>\n<figure class=\"highlight bash\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n $ \n <span class=\"built_in\">cd</span> myApp\n </div>\n <div class=\"line\">\n $ npm i taro-ui\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h3 id=\"使用\" class=\"post-heading\"><a href=\"#使用\" class=\"headerlink\" title=\"使用\"></a>使用<a class=\"post-anchor\" href=\"#使用\" aria-hidden=\"true\"></a></h3>\n<p>在代码中 <code>import</code> 组件并按照文档说明进行使用</p>\n<p><code>import { AtButton } from \'taro-ui\'</code></p>\n<h3 id=\"示例\" class=\"post-heading\"><a href=\"#示例\" class=\"headerlink\" title=\"示例\"></a>示例<a class=\"post-anchor\" href=\"#示例\" aria-hidden=\"true\"></a></h3>\n<p>在 <code>/myApp/src/pages/index/index.jsx</code> 文件添加以下代码<br></p>\n<figure class=\"highlight jsx\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div>\n <div class=\"line\">\n 16\n </div>\n <div class=\"line\">\n 17\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">import</span> Taro, { Component, Config } \n <span class=\"keyword\">from</span> \n <span class=\"string\">\'@tarojs/taro\'</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">import</span> { View } \n <span class=\"keyword\">from</span> \n <span class=\"string\">\'@tarojs/components\'</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">import</span> { AtButton } \n <span class=\"keyword\">from</span> \n <span class=\"string\">\'taro-ui\'</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">import</span> \n <span class=\"string\">\'./index.scss\'</span>\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"keyword\">export</span> \n <span class=\"keyword\">default</span> \n <span class=\"class\"><span class=\"keyword\">class</span> <span class=\"title\">Index</span> <span class=\"keyword\">extends</span> <span class=\"title\">Component</span> </span>{\n </div>\n <div class=\"line\">\n config: Config = {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">navigationBarTitleText</span>: \n <span class=\"string\">\'首页\'</span>\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n render () {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">return</span> (\n </div>\n <div class=\"line\"> \n <span class=\"xml\"><span class=\"tag\">&lt;<span class=\"name\">View</span> <span class=\"attr\">className</span>=<span class=\"string\">\'index\'</span>&gt;</span></span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">AtButton</span> <span class=\"attr\">type</span>=<span class=\"string\">\'primary\'</span>&gt;</span>按钮文案\n <span class=\"tag\">&lt;/<span class=\"name\">AtButton</span>&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;/<span class=\"name\">View</span>&gt;</span>\n </div>\n <div class=\"line\">\n )\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<h3 id=\"编译并预览\" class=\"post-heading\"><a href=\"#编译并预览\" class=\"headerlink\" title=\"编译并预览\"></a>编译并预览<a class=\"post-anchor\" href=\"#编译并预览\" aria-hidden=\"true\"></a></h3>\n<p>进入项目目录开始开发,可以选择小程序预览模式,或者 H5 预览模式,若使用微信小程序预览模式,则需要自行下载并打开微信开发者工具,选择预览项目根目录。</p>\n<p><strong>微信小程序编译预览模式</strong></p>\n<figure class=\"highlight bash\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\"># npm script</span>\n </div>\n <div class=\"line\">\n $ npm run dev:weapp\n </div>\n <div class=\"line\">\n <span class=\"comment\"># 仅限全局安装</span>\n </div>\n <div class=\"line\">\n $ taro build --\n <span class=\"built_in\">type</span> weapp --watch\n </div>\n <div class=\"line\">\n <span class=\"comment\"># npx用户也可以使用</span>\n </div>\n <div class=\"line\">\n $ npx taro build --\n <span class=\"built_in\">type</span> weapp --watch\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p><strong>H5编译预览模式</strong></p>\n<figure class=\"highlight bash\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\"># npm script</span>\n </div>\n <div class=\"line\">\n $ npm run dev:h5\n </div>\n <div class=\"line\">\n <span class=\"comment\"># 仅限全局安装</span>\n </div>\n <div class=\"line\">\n $ taro build --\n <span class=\"built_in\">type</span> h5 --watch\n </div>\n <div class=\"line\">\n <span class=\"comment\"># npx用户也可以使用</span>\n </div>\n <div class=\"line\">\n $ npx taro build --\n <span class=\"built_in\">type</span> h5 --watch\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h2 id=\"意见反馈\" class=\"post-heading\"><a href=\"#意见反馈\" class=\"headerlink\" title=\"意见反馈\"></a>意见反馈<a class=\"post-anchor\" href=\"#意见反馈\" aria-hidden=\"true\"></a></h2>\n<p>如果有任何的意见或者建议,欢迎在 <a href=\"https://github.com/NervJS/taro-ui\" target=\"_blank\" rel=\"external\">Github</a> 创建 issue,感谢你的支持和贡献。</p>\n<div class=\"post-tags\" style=\"display:none\">\n <a href=\"/tags/React/\">React</a> \n <a href=\"/tags/小程序/\">小程序</a> \n <a href=\"/tags/Nerv/\">Nerv</a> \n <a href=\"/tags/Taro/\">Taro</a>\n</div>\n<div class=\"post-categories\" style=\"display:none\">\n <a href=\"/cates/小程序/\">小程序</a>\n</div>\n<div class=\"post-announce\">\n 感谢您的阅读,本文由 \n <a href=\"//aotu.io\">凹凸实验室</a> 版权所有。如若转载,请注明出处:凹凸实验室(\n <a href=\"https://aotu.io/notes/2018/08/27/the-birth-of-taro-ui/\">https://aotu.io/notes/2018/08/27/the-birth-of-taro-ui/</a>)\n</div>\n<div class=\"post-revision\">\n <i class=\"fa fa-clock-o\"></i> \n <time class=\"post-updated\" datetime=\"2018-11-07T01:01:15.657Z\">上次更新:2018-11-07 09:01:15</time>\n</div>');
INSERT INTO `rumo_blog` VALUES ('15', '网页适配 iPhoneX,就是这么简单', '1', '网页适配 iPhoneX,就是这么简单', '1', '2018-11-11 21:02:31', '2018-11-11 21:02:31', '1', '0', '//misc.aotu.io/liqinuo/iphonex_cover.png', '', '0', '0', '<a id=\"more\"></a>\n<h2 id=\"前言\" class=\"post-heading\"><a href=\"#前言\" class=\"headerlink\" title=\"前言\"></a>前言<a class=\"post-anchor\" href=\"#前言\" aria-hidden=\"true\"></a></h2>\n<p>iPhoneX 取消了物理按键,改成底部小黑条,这一改动导致网页出现了比较尴尬的屏幕适配问题。对于网页而言,顶部(刘海部位)的适配问题浏览器已经做了处理,所以我们只需要关注底部与小黑条的适配问题即可(即常见的吸底导航、返回顶部等各种相对底部 fixed 定位的元素)。</p>\n<p>笔者通过查阅了一些官方文档,以及结合实际项目中的一些处理经验,整理了一套简单的适配方案分享给大家,希望对大家有所帮助,以下是处理前后效果图:</p>\n<p><img src=\"//misc.aotu.io/liqinuo/iphonex_3.png\" width=\"500px\"></p>\n<h2 id=\"适配之前需要了解的几个新知识\" class=\"post-heading\"><a href=\"#适配之前需要了解的几个新知识\" class=\"headerlink\" title=\"适配之前需要了解的几个新知识\"></a>适配之前需要了解的几个新知识<a class=\"post-anchor\" href=\"#适配之前需要了解的几个新知识\" aria-hidden=\"true\"></a></h2>\n<h3 id=\"安全区域\" class=\"post-heading\"><a href=\"#安全区域\" class=\"headerlink\" title=\"安全区域\"></a>安全区域<a class=\"post-anchor\" href=\"#安全区域\" aria-hidden=\"true\"></a></h3>\n<p>安全区域指的是一个可视窗口范围,处于安全区域的内容不受圆角(corners)、齐刘海(sensor housing)、小黑条(Home Indicator)影响,如下图蓝色区域:</p>\n<p><img src=\"//misc.aotu.io/liqinuo/iphonex_4.png\" width=\"600px\"></p>\n<p>也就是说,我们要做好适配,必须保证页面可视、可操作区域是在安全区域内。</p>\n<p>更详细说明,参考文档:<a href=\"https://developer.apple.com/ios/human-interface-guidelines/overview/iphone-x/\" target=\"_blank\" rel=\"external\">Human Interface Guidelines - iPhoneX</a></p>\n<h3 id=\"viewport-fit\" class=\"post-heading\"><a href=\"#viewport-fit\" class=\"headerlink\" title=\"viewport-fit\"></a>viewport-fit<a class=\"post-anchor\" href=\"#viewport-fit\" aria-hidden=\"true\"></a></h3>\n<p>iOS11 新增特性,苹果公司为了适配 iPhoneX 对现有 viewport meta 标签的一个扩展,用于设置网页在可视窗口的布局方式,可设置三个值:</p>\n<ul>\n <li>contain: 可视窗口完全包含网页内容(左图)</li>\n <li>cover:网页内容完全覆盖可视窗口(右图)</li>\n <li>auto:默认值,跟 contain 表现一致</li>\n</ul>\n<p><img src=\"//misc.aotu.io/liqinuo/iphonex_1.png\" width=\"500px\"></p>\n<blockquote>\n <p>注意:网页默认不添加扩展的表现是 viewport-fit=contain,需要适配 iPhoneX 必须设置 viewport-fit=cover,这是适配的关键步骤。</p>\n</blockquote>\n<p>更详细说明,参考文档:<a href=\"https://www.w3.org/TR/css-round-display-1/#viewport-fit-descriptor\" target=\"_blank\" rel=\"external\">viewport-fit-descriptor</a></p>\n<h3 id=\"env-和-constant\" class=\"post-heading\"><a href=\"#env-和-constant\" class=\"headerlink\" title=\"env() 和 constant()\"></a>env() 和 constant()<a class=\"post-anchor\" href=\"#env-和-constant\" aria-hidden=\"true\"></a></h3>\n<p>iOS11 新增特性,Webkit 的一个 CSS 函数,用于设定安全区域与边界的距离,有四个预定义的变量:</p>\n<ul>\n <li>safe-area-inset-left:安全区域距离左边边界距离</li>\n <li>safe-area-inset-right:安全区域距离右边边界距离</li>\n <li>safe-area-inset-top:安全区域距离顶部边界距离</li>\n <li>safe-area-inset-bottom:安全区域距离底部边界距离</li>\n</ul>\n<p>这里我们只需要关注 safe-area-inset-bottom 这个变量,因为它对应的就是小黑条的高度(横竖屏时值不一样)。</p>\n<blockquote>\n <p>注意:当 viewport-fit=contain 时 env() 是不起作用的,必须要配合 viewport-fit=cover 使用。对于不支持env() 的浏览器,浏览器将会忽略它。</p>\n</blockquote>\n<p>在这之前,笔者使用的是 constant(),后来,官方文档加了这么一段注释(坑):</p>\n<blockquote>\n <p>The env() function shipped in iOS 11 with the name constant(). Beginning with Safari Technology Preview 41 and the iOS 11.2 beta, constant() has been removed and replaced with env(). You can use the CSS fallback mechanism to support both versions, if necessary, but should prefer env() going forward.</p>\n</blockquote>\n<p>这就意味着,之前使用的 constant() 在 iOS11.2 之后就不能使用的,但我们还是需要做向后兼容,像这样:</p>\n<figure class=\"highlight css\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"selector-tag\">padding-bottom</span>: \n <span class=\"selector-tag\">constant</span>(\n <span class=\"selector-tag\">safe-area-inset-bottom</span>); \n <span class=\"comment\">/* 兼容 iOS &lt; 11.2 */</span>\n </div>\n <div class=\"line\">\n <span class=\"selector-tag\">padding-bottom</span>: \n <span class=\"selector-tag\">env</span>(\n <span class=\"selector-tag\">safe-area-inset-bottom</span>); \n <span class=\"comment\">/* 兼容 iOS &gt;= 11.2 */</span>\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>注意:env() 跟 constant() 需要同时存在,而且顺序不能换。</p>\n<p>更详细说明,参考文档:<a href=\"https://webkit.org/blog/7929/designing-websites-for-iphone-x/?hmsr=funteas.com&amp;utm_medium=funteas.com&amp;utm_source=funteas.com\" target=\"_blank\" rel=\"external\">Designing Websites for iPhone X</a></p>\n<h2 id=\"如何适配\" class=\"post-heading\"><a href=\"#如何适配\" class=\"headerlink\" title=\"如何适配\"></a>如何适配<a class=\"post-anchor\" href=\"#如何适配\" aria-hidden=\"true\"></a></h2>\n<p>了解了以上所说的几个知识点,接下来我们适配的思路就很清晰了。</p>\n<h3 id=\"第一步:设置网页在可视窗口的布局方式\" class=\"post-heading\"><a href=\"#第一步:设置网页在可视窗口的布局方式\" class=\"headerlink\" title=\"第一步:设置网页在可视窗口的布局方式\"></a>第一步:设置网页在可视窗口的布局方式<a class=\"post-anchor\" href=\"#第一步:设置网页在可视窗口的布局方式\" aria-hidden=\"true\"></a></h3>\n<p>新增 viweport-fit 属性,使得页面内容完全覆盖整个窗口:<br></p>\n<figure class=\"highlight html\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"tag\">&lt;<span class=\"name\">meta</span> <span class=\"attr\">name</span>=<span class=\"string\">\"viewport\"</span> <span class=\"attr\">content</span>=<span class=\"string\">\"width=device-width, viewport-fit=cover\"</span>&gt;</span>\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<p>前面也有提到过,只有设置了 viewport-fit=cover,才能使用 env()。</p>\n<h3 id=\"第二步:页面主体内容限定在安全区域内\" class=\"post-heading\"><a href=\"#第二步:页面主体内容限定在安全区域内\" class=\"headerlink\" title=\"第二步:页面主体内容限定在安全区域内\"></a>第二步:页面主体内容限定在安全区域内<a class=\"post-anchor\" href=\"#第二步:页面主体内容限定在安全区域内\" aria-hidden=\"true\"></a></h3>\n<p>这一步根据实际页面场景选择,如果不设置这个值,可能存在小黑条遮挡页面最底部内容的情况。<br></p>\n<figure class=\"highlight css\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"selector-tag\">body</span> {\n </div>\n <div class=\"line\"> \n <span class=\"attribute\">padding-bottom</span>: \n <span class=\"built_in\">constant</span>(safe-area-inset-bottom);\n </div>\n <div class=\"line\"> \n <span class=\"attribute\">padding-bottom</span>: \n <span class=\"built_in\">env</span>(safe-area-inset-bottom);\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<h3 id=\"第三步:fixed-元素的适配\" class=\"post-heading\"><a href=\"#第三步:fixed-元素的适配\" class=\"headerlink\" title=\"第三步:fixed 元素的适配\"></a>第三步:fixed 元素的适配<a class=\"post-anchor\" href=\"#第三步:fixed-元素的适配\" aria-hidden=\"true\"></a></h3>\n<h4 id=\"类型一:fixed-完全吸底元素(bottom-0),比如下图这两种情况:\" class=\"post-heading\"><a href=\"#类型一:fixed-完全吸底元素(bottom-0),比如下图这两种情况:\" class=\"headerlink\" title=\"类型一:fixed 完全吸底元素(bottom = 0),比如下图这两种情况:\"></a>类型一:fixed 完全吸底元素(bottom = 0),比如下图这两种情况:<a class=\"post-anchor\" href=\"#类型一:fixed-完全吸底元素(bottom-0),比如下图这两种情况:\" aria-hidden=\"true\"></a></h4>\n<p><img src=\"//misc.aotu.io/liqinuo/iphonex_2.png\" width=\"600px\"></p>\n<p>可以通过加内边距 padding 扩展高度:<br></p>\n<figure class=\"highlight css\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n {\n </div>\n <div class=\"line\"> \n <span class=\"attribute\">padding-bottom</span>: \n <span class=\"built_in\">constant</span>(safe-area-inset-bottom);\n </div>\n <div class=\"line\"> \n <span class=\"attribute\">padding-bottom</span>: \n <span class=\"built_in\">env</span>(safe-area-inset-bottom);\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<p>或者通过计算函数 calc 覆盖原来高度:<br></p>\n<figure class=\"highlight css\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n {\n </div>\n <div class=\"line\"> \n <span class=\"attribute\">height</span>: \n <span class=\"built_in\">calc</span>(60px(假设值) + \n <span class=\"built_in\">constant</span>(safe-area-inset-bottom));\n </div>\n <div class=\"line\"> \n <span class=\"attribute\">height</span>: \n <span class=\"built_in\">calc</span>(60px(假设值) + \n <span class=\"built_in\">env</span>(safe-area-inset-bottom));\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<blockquote>\n <p>注意,这个方案需要吸底条必须是有背景色的,因为扩展的部分背景是跟随外容器的,否则出现镂空情况。</p>\n</blockquote>\n<p>还有一种方案就是,可以通过新增一个新的元素(空的颜色块,主要用于小黑条高度的占位),然后吸底元素可以不改变高度只需要调整位置,像这样:</p>\n<figure class=\"highlight css\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n {\n </div>\n <div class=\"line\"> \n <span class=\"attribute\">margin-bottom</span>: \n <span class=\"built_in\">constant</span>(safe-area-inset-bottom);\n </div>\n <div class=\"line\"> \n <span class=\"attribute\">margin-bottom</span>: \n <span class=\"built_in\">env</span>(safe-area-inset-bottom);\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>空的颜色块:<br></p>\n<figure class=\"highlight css\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n {\n </div>\n <div class=\"line\"> \n <span class=\"attribute\">position</span>: fixed;\n </div>\n <div class=\"line\"> \n <span class=\"attribute\">bottom</span>: \n <span class=\"number\">0</span>;\n </div>\n <div class=\"line\"> \n <span class=\"attribute\">width</span>: \n <span class=\"number\">100%</span>;\n </div>\n <div class=\"line\"> \n <span class=\"attribute\">height</span>: \n <span class=\"built_in\">constant</span>(safe-area-inset-bottom);\n </div>\n <div class=\"line\"> \n <span class=\"attribute\">height</span>: \n <span class=\"built_in\">env</span>(safe-area-inset-bottom);\n </div>\n <div class=\"line\"> \n <span class=\"attribute\">background-color</span>: \n <span class=\"number\">#fff</span>;\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<h4 id=\"类型二:fixed-非完全吸底元素(bottom-≠-0),比如-“返回顶部”、“侧边广告”-等\" class=\"post-heading\"><a href=\"#类型二:fixed-非完全吸底元素(bottom-≠-0),比如-“返回顶部”、“侧边广告”-等\" class=\"headerlink\" title=\"类型二:fixed 非完全吸底元素(bottom ≠ 0),比如 “返回顶部”、“侧边广告” 等\"></a>类型二:fixed 非完全吸底元素(bottom ≠ 0),比如 “返回顶部”、“侧边广告” 等<a class=\"post-anchor\" href=\"#类型二:fixed-非完全吸底元素(bottom-≠-0),比如-“返回顶部”、“侧边广告”-等\" aria-hidden=\"true\"></a></h4>\n<p>像这种只是位置需要对应向上调整,可以仅通过外边距 margin 来处理:<br></p>\n<figure class=\"highlight css\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n {\n </div>\n <div class=\"line\"> \n <span class=\"attribute\">margin-bottom</span>: \n <span class=\"built_in\">constant</span>(safe-area-inset-bottom);\n </div>\n <div class=\"line\"> \n <span class=\"attribute\">margin-bottom</span>: \n <span class=\"built_in\">env</span>(safe-area-inset-bottom);\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<p>或者,你也可以通过计算函数 calc 覆盖原来 bottom 值:<br></p>\n<figure class=\"highlight css\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n {\n </div>\n <div class=\"line\"> \n <span class=\"attribute\">bottom</span>: \n <span class=\"built_in\">calc</span>(50px(假设值) + \n <span class=\"built_in\">constant</span>(safe-area-inset-bottom));\n </div>\n <div class=\"line\"> \n <span class=\"attribute\">bottom</span>: \n <span class=\"built_in\">calc</span>(50px(假设值) + \n <span class=\"built_in\">env</span>(safe-area-inset-bottom));\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<h3 id=\"你也可以使用-supports-隔离兼容样式\" class=\"post-heading\"><a href=\"#你也可以使用-supports-隔离兼容样式\" class=\"headerlink\" title=\"你也可以使用 @supports 隔离兼容样式\"></a>你也可以使用 @supports 隔离兼容样式<a class=\"post-anchor\" href=\"#你也可以使用-supports-隔离兼容样式\" aria-hidden=\"true\"></a></h3>\n<p>写到这里,我们常见的两种类型的 fixed 元素适配方案已经了解了吧。如果我们只希望 iPhoneX 才需要新增适配样式,我们可以配合 @supports 来隔离兼容样式,当然这个处理对页面展示实际不会有任何影响:<br></p>\n<figure class=\"highlight css\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n @\n <span class=\"keyword\">supports</span> (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {\n </div>\n <div class=\"line\"> \n <span class=\"selector-tag\">div</span> {\n </div>\n <div class=\"line\"> \n <span class=\"attribute\">margin-bottom</span>: \n <span class=\"built_in\">constant</span>(safe-area-inset-bottom);\n </div>\n <div class=\"line\"> \n <span class=\"attribute\">margin-bottom</span>: \n <span class=\"built_in\">env</span>(safe-area-inset-bottom);\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<h2 id=\"写在最后\" class=\"post-heading\"><a href=\"#写在最后\" class=\"headerlink\" title=\"写在最后\"></a>写在最后<a class=\"post-anchor\" href=\"#写在最后\" aria-hidden=\"true\"></a></h2>\n<p>以上几种方案仅供参考,笔者认为,现阶段适配处理起来是有点折腾,但是至少能解决,具体需要根据页面实际场景,在不影响用户体验与操作的大前提下不断尝试与探索,才能更完美的适配。</p>\n<div class=\"post-tags\" style=\"display:none\">\n <a href=\"/tags/H5/\">H5</a> \n <a href=\"/tags/iPhoneX/\">iPhoneX</a>\n</div>\n<div class=\"post-categories\" style=\"display:none\">\n <a href=\"/cates/Web开发/\">Web开发</a>\n</div>\n<div class=\"post-announce\">\n 感谢您的阅读,本文由 \n <a href=\"//aotu.io\">凹凸实验室</a> 版权所有。如若转载,请注明出处:凹凸实验室(\n <a href=\"https://aotu.io/notes/2017/11/27/iphonex/\">https://aotu.io/notes/2017/11/27/iphonex/</a>)\n</div>\n<div class=\"post-revision\">\n <i class=\"fa fa-clock-o\"></i> \n <time class=\"post-updated\" datetime=\"2018-11-07T01:01:15.653Z\">上次更新:2018-11-07 09:01:15</time>\n</div>');
INSERT INTO `rumo_blog` VALUES ('16', 'H5游戏开发:决胜三分球', '1', 'H5游戏开发:决胜三分球', '1', '2018-11-11 21:02:31', '2018-11-11 21:02:31', '1', '0', '//misc.aotu.io/ONE-SUNDAY/basketball/basketball_900x500.jpg', '', '0', '0', '<a id=\"more\"></a>\n<h2 id=\"前言\" class=\"post-heading\"><a href=\"#前言\" class=\"headerlink\" title=\"前言\"></a>前言<a class=\"post-anchor\" href=\"#前言\" aria-hidden=\"true\"></a></h2>\n<p>本次是与腾讯手机充值合作推出的活动,用户通过氪金充值话费或者分享来获得更多的投篮机会,根据最终的进球数排名来发放奖品。</p>\n<p>用户可以通过滑动拉出一条辅助线,根据辅助线长度和角度的不同将球投出,由于本次活动的开发周期短,在物理特性实现方面使用了物理引擎,所有本文的分享内容是如何结合物理引擎去实现一款投篮小游戏,如下图所示。</p>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/basketball/show.gif\" alt=\"show\"></p>\n<h2 id=\"准备\" class=\"post-heading\"><a href=\"#准备\" class=\"headerlink\" title=\"准备\"></a>准备<a class=\"post-anchor\" href=\"#准备\" aria-hidden=\"true\"></a></h2>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/basketball/layaAir.jpg\" alt=\"layaAir\"></p>\n<p>此次我使用的游戏引擎是 <a href=\"https://www.layabox.com/\" target=\"_blank\" rel=\"external\">LayaAir</a>,你也可以根据你的爱好和实际需求选择合适的游戏引擎进行开发,为什么选择该引擎进行开发 ,总的来说有以下几个原因:</p>\n<ul>\n <li>LayaAir 官方文档、API、示例学习详细、友好,可快速上手</li>\n <li>除了支持 2D 开发,同时还支持 3D 和 VR 开发,支持 AS、TS、JS 三种语言开发</li>\n <li>在开发者社区中提出的问题,官方能及时有效的回复</li>\n <li>提供 IDE 工具,内置功能有打包 APP、骨骼动画转换、图集打包、SWF转换、3D 转换等等</li>\n</ul>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/basketball/matterjs.jpg\" alt=\"matterjs\"></p>\n<p>物理引擎方面采用了 <a href=\"http://brm.io/matter-js/\" target=\"_blank\" rel=\"external\">Matter.js</a>,篮球、篮网的碰撞弹跳都使用它来实现,当然,还有其他的物理引擎如 planck.js、p2.js 等等,具体没有太深入的了解,Matter.js 相比其他引擎的优势在于:</p>\n<ul>\n <li>轻量级,性能不逊色于其他物理引擎</li>\n <li>官方文档、Demo 例子非常丰富,配色有爱</li>\n <li>API 简单易用,轻松实现弹跳、碰撞、重力、滚动等物理效果</li>\n <li>Github Star 数处于其他物理引擎之上,更新频率更高</li>\n</ul>\n<h2 id=\"开始\" class=\"post-heading\"><a href=\"#开始\" class=\"headerlink\" title=\"开始\"></a>开始<a class=\"post-anchor\" href=\"#开始\" aria-hidden=\"true\"></a></h2>\n<h4 id=\"一、初始化游戏引擎\" class=\"post-heading\"><a href=\"#一、初始化游戏引擎\" class=\"headerlink\" title=\"一、初始化游戏引擎\"></a>一、初始化游戏引擎<a class=\"post-anchor\" href=\"#一、初始化游戏引擎\" aria-hidden=\"true\"></a></h4>\n<p>首先对 LayaAir 游戏引擎进行初始化设置,<code>Laya.init</code> 创建一个 1334x750 的画布以 WebGL 模式去渲染,渲染模式下有 WebGL 和 Canvas,使用 WebGL 模式下会出现锯齿的问题,使用 <code>Config.isAntialias</code> 抗锯齿可以解决此问题,并且使用引擎中自带的多种屏幕适配 <code>screenMode</code>。</p>\n<p>如果你使用的游戏引擎没有提供屏幕适配,欢迎阅读另一位同事所写的文章<a href=\"https://aotu.io/notes/2017/10/18/landscape_mode_in_html5_game/\">【H5游戏开发:横屏适配】</a>。</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n ...\n </div>\n <div class=\"line\">\n Config.isAntialias = \n <span class=\"literal\">true</span>; \n <span class=\"comment\">// 抗锯齿</span>\n </div>\n <div class=\"line\">\n Laya.init(\n <span class=\"number\">1334</span>, \n <span class=\"number\">750</span>, Laya.WebGL); \n <span class=\"comment\">// 初始化一个画布,使用 WebGL 渲染,不支持时会自动切换为 Canvas</span>\n </div>\n <div class=\"line\">\n Laya.stage.alignV = \n <span class=\"string\">\'top\'</span>; \n <span class=\"comment\">// 适配垂直对齐方式</span>\n </div>\n <div class=\"line\">\n Laya.stage.alignH = \n <span class=\"string\">\'middle\'</span>; \n <span class=\"comment\">// 适配水平对齐方式</span>\n </div>\n <div class=\"line\">\n Laya.stage.screenMode = \n <span class=\"keyword\">this</span>.Stage.SCREEN_HORIZONTAL; \n <span class=\"comment\">// 始终以横屏展示</span>\n </div>\n <div class=\"line\">\n Laya.stage.scaleMode = \n <span class=\"string\">\"fixedwidth\"</span>; \n <span class=\"comment\">// 宽度不变,高度根据屏幕比例缩放,还有 noscale、exactfit、showall、noborder、full、fixedheight 等适配模式</span>\n </div>\n <div class=\"line\">\n ...\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h4 id=\"二、初始化物理引擎、加入场景\" class=\"post-heading\"><a href=\"#二、初始化物理引擎、加入场景\" class=\"headerlink\" title=\"二、初始化物理引擎、加入场景\"></a>二、初始化物理引擎、加入场景<a class=\"post-anchor\" href=\"#二、初始化物理引擎、加入场景\" aria-hidden=\"true\"></a></h4>\n<p>然后对 Matter.js 物理引擎进行初始化,<code>Matter.Engine</code> 模块包含了创建和处理引擎的方法,由引擎运行这个世界,<code>engine.world</code> 则包含了用于创建和操作世界的方法,所有的物体都需要加入到这个世界中,<code>Matter.Render</code> 是将实例渲染到 Canvas 中的渲染器。</p>\n<p><code>enableSleeping</code> 是开启刚体处于静止状态时切换为睡眠状态,减少物理运算提升性能,<code>wireframes</code> 关闭用于调试时的线框模式,再使用 LayaAir 提供的 <code>Laya.loading</code>、<code>new Sprite</code> 加载、绘制已简化的场景元素。</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n ...\n </div>\n <div class=\"line\">\n this.engine;\n </div>\n <div class=\"line\">\n <span class=\"keyword\">var</span> world;\n </div>\n <div class=\"line\">\n <span class=\"keyword\">this</span>.engine = Matter.Engine.create({\n </div>\n <div class=\"line\"> \n <span class=\"attr\">enableSleeping</span>: \n <span class=\"literal\">true</span> \n <span class=\"comment\">// 开启睡眠</span>\n </div>\n <div class=\"line\">\n });\n </div>\n <div class=\"line\">\n world = \n <span class=\"keyword\">this</span>.engine.world;\n </div>\n <div class=\"line\">\n Matter.Engine.run(\n <span class=\"keyword\">this</span>.engine); \n <span class=\"comment\">// Engine 启动</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">var</span> render = LayaRender.create({\n </div>\n <div class=\"line\"> \n <span class=\"attr\">engine</span>: \n <span class=\"keyword\">this</span>.engine,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">options</span>: { \n <span class=\"attr\">wireframes</span>: \n <span class=\"literal\">false</span>, \n <span class=\"attr\">background</span>: \n <span class=\"string\">\"#000\"</span> }\n </div>\n <div class=\"line\">\n });\n </div>\n <div class=\"line\">\n LayaRender.run(render); \n <span class=\"comment\">// Render 启动</span>\n </div>\n <div class=\"line\">\n ...\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/basketball/engine-and-world.jpg\" alt=\"engine-and-world\"></p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n ...\n </div>\n <div class=\"line\">\n <span class=\"comment\">// 加入背景、篮架、篮框</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">var</span> bg = \n <span class=\"keyword\">new</span> \n <span class=\"keyword\">this</span>.Sprite();\n </div>\n <div class=\"line\">\n Laya.stage.addChild(bg);\n </div>\n <div class=\"line\">\n bg.pos(\n <span class=\"number\">0</span>, \n <span class=\"number\">0</span>);\n </div>\n <div class=\"line\">\n bg.loadImage(\n <span class=\"string\">\'images/bg.jpg\'</span>);\n </div>\n <div class=\"line\">\n ...\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/basketball/scene.jpg\" alt=\"scene\"></p>\n<h4 id=\"三、画出辅助线,计算长度、角度\" class=\"post-heading\"><a href=\"#三、画出辅助线,计算长度、角度\" class=\"headerlink\" title=\"三、画出辅助线,计算长度、角度\"></a>三、画出辅助线,计算长度、角度<a class=\"post-anchor\" href=\"#三、画出辅助线,计算长度、角度\" aria-hidden=\"true\"></a></h4>\n<p>投球的力度和角度是根据这条辅助线的长短角度去决定的,现在我们加入手势事件 <code>MOUSE_DOWN</code>、<code>MOUSE_MOVE</code>、<code>MOUSE_UP</code> 画出辅助线,通过这条辅助线起点和终点的 X、Y 坐标点再结合两个公式: <code>getRad</code>、<code>getDistance</code> 计算出距离和角度。</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n ...\n </div>\n <div class=\"line\">\n var line = \n <span class=\"keyword\">new</span> \n <span class=\"keyword\">this</span>.Sprite();\n </div>\n <div class=\"line\">\n Laya.stage.addChild(line);\n </div>\n <div class=\"line\">\n Laya.stage.on(\n <span class=\"keyword\">this</span>.Event.MOUSE_DOWN, \n <span class=\"keyword\">this</span>, \n <span class=\"function\"><span class=\"keyword\">function</span>(<span class=\"params\">e</span>) </span>{ ... });\n </div>\n <div class=\"line\">\n Laya.stage.on(\n <span class=\"keyword\">this</span>.Event.MOUSE_MOVE, \n <span class=\"keyword\">this</span>, \n <span class=\"function\"><span class=\"keyword\">function</span>(<span class=\"params\">e</span>) </span>{ ... });\n </div>\n <div class=\"line\">\n Laya.stage.on(\n <span class=\"keyword\">this</span>.Event.MOUSE_UP, \n <span class=\"keyword\">this</span>, \n <span class=\"function\"><span class=\"keyword\">function</span>(<span class=\"params\">e</span>) </span>{ ... });\n </div>\n <div class=\"line\">\n ...\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n ...\n </div>\n <div class=\"line\">\n getRad: \n <span class=\"function\"><span class=\"keyword\">function</span>(<span class=\"params\">x1, y1, x2, y2</span>) </span>{ \n <span class=\"comment\">// 返回两点之间的角度</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">var</span> x = x2 - x1;\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">var</span> y = y2 - x2;\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">var</span> Hypotenuse = \n <span class=\"built_in\">Math</span>.sqrt(\n <span class=\"built_in\">Math</span>.pow(x, \n <span class=\"number\">2</span>) + \n <span class=\"built_in\">Math</span>.pow(y, \n <span class=\"number\">2</span>));\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">var</span> angle = x / Hypotenuse;\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">var</span> rad = \n <span class=\"built_in\">Math</span>.acos(angle);\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span> (y2 &lt; y1) { rad = -rad; } \n <span class=\"keyword\">return</span> rad;\n </div>\n <div class=\"line\">\n },\n </div>\n <div class=\"line\">\n <span class=\"attr\">getDistance</span>: \n <span class=\"function\"><span class=\"keyword\">function</span>(<span class=\"params\">x1, y1, x2, y2</span>) </span>{ \n <span class=\"comment\">// 计算两点间的距离</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">return</span> \n <span class=\"built_in\">Math</span>.sqrt(\n <span class=\"built_in\">Math</span>.pow(x1 - x2, \n <span class=\"number\">2</span>) + \n <span class=\"built_in\">Math</span>.pow(y1 - y2, \n <span class=\"number\">2</span>));\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n ...\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h4 id=\"四、生成篮球施加力度\" class=\"post-heading\"><a href=\"#四、生成篮球施加力度\" class=\"headerlink\" title=\"四、生成篮球施加力度\"></a>四、生成篮球施加力度<a class=\"post-anchor\" href=\"#四、生成篮球施加力度\" aria-hidden=\"true\"></a></h4>\n<p>大致初始了一个简单的场景,只有背景和篮框,接下来是加入投篮。</p>\n<p>每次在 <code>MOUSE_UP</code> 事件的时候我们就生成一个圆形的刚体, <code>isStatic: false</code> 我们要移动所以不固定篮球,并且设置 <code>density</code> 密度、<code>restitution</code> 弹性、刚体的背景 <code>sprite</code> 等属性。</p>\n<p>将获得的两个值:距离和角度,通过 <code>applyForce</code> 方法给生成的篮球施加一个力,使之投出去。</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div>\n <div class=\"line\">\n 16\n </div>\n <div class=\"line\">\n 17\n </div>\n <div class=\"line\">\n 18\n </div>\n <div class=\"line\">\n 19\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n ...\n </div>\n <div class=\"line\">\n addBall: \n <span class=\"function\"><span class=\"keyword\">function</span>(<span class=\"params\">x, y</span>) </span>{\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">var</span> ball = Matter.Bodies.circle(\n <span class=\"number\">500</span>, \n <span class=\"number\">254</span>, \n <span class=\"number\">28</span>, { \n <span class=\"comment\">// x, y, 半径</span>\n </div>\n <div class=\"line\">\n isStatic: \n <span class=\"literal\">false</span>, \n <span class=\"comment\">// 不固定</span>\n </div>\n <div class=\"line\">\n density: \n <span class=\"number\">0.68</span>, \n <span class=\"comment\">// 密度</span>\n </div>\n <div class=\"line\">\n restitution: \n <span class=\"number\">0.8</span>, \n <span class=\"comment\">// 弹性</span>\n </div>\n <div class=\"line\">\n render: {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">visible</span>: \n <span class=\"literal\">true</span>, \n <span class=\"comment\">// 开启渲染</span>\n </div>\n <div class=\"line\">\n sprite: {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">texture</span>: \n <span class=\"string\">\'images/ball.png\'</span>, \n <span class=\"comment\">// 设置为篮球图</span>\n </div>\n <div class=\"line\">\n xOffset: \n <span class=\"number\">28</span>, \n <span class=\"comment\">// x 设置为中心点</span>\n </div>\n <div class=\"line\">\n yOffset: \n <span class=\"number\">28</span> \n <span class=\"comment\">// y 设置为中心点</span>\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n });\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n Matter.Body.applyForce(ball, ball.position, { \n <span class=\"attr\">x</span>: x, \n <span class=\"attr\">y</span>: y }); \n <span class=\"comment\">// 施加力</span>\n </div>\n <div class=\"line\">\n Matter.World.add(\n <span class=\"keyword\">this</span>.engine.world, [ball]); \n <span class=\"comment\">// 添加到世界</span>\n </div>\n <div class=\"line\">\n ...\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h4 id=\"五、加入其他刚体、软体\" class=\"post-heading\"><a href=\"#五、加入其他刚体、软体\" class=\"headerlink\" title=\"五、加入其他刚体、软体\"></a>五、加入其他刚体、软体<a class=\"post-anchor\" href=\"#五、加入其他刚体、软体\" aria-hidden=\"true\"></a></h4>\n<p>现在,已经能顺利的将篮球投出,现在我们还需要加入一个篮球网、篮框、篮架。</p>\n<p>通过 Matter.js 加入一些刚体和软体并且赋予物理特性 <code>firction</code> 摩擦力、<code>frictionAir</code> 空气摩擦力等, <code>visible: false</code> 表示是否隐藏,<code>collisionFilter</code> 是过滤碰撞让篮球网之间不产生碰撞。</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div>\n <div class=\"line\">\n 16\n </div>\n <div class=\"line\">\n 17\n </div>\n <div class=\"line\">\n 18\n </div>\n <div class=\"line\">\n 19\n </div>\n <div class=\"line\">\n 20\n </div>\n <div class=\"line\">\n 21\n </div>\n <div class=\"line\">\n 22\n </div>\n <div class=\"line\">\n 23\n </div>\n <div class=\"line\">\n 24\n </div>\n <div class=\"line\">\n 25\n </div>\n <div class=\"line\">\n 26\n </div>\n <div class=\"line\">\n 27\n </div>\n <div class=\"line\">\n 28\n </div>\n <div class=\"line\">\n 29\n </div>\n <div class=\"line\">\n 30\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n ...\n </div>\n <div class=\"line\">\n addBody: \n <span class=\"function\"><span class=\"keyword\">function</span>(<span class=\"params\"></span>) </span>{\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">var</span> group = Matter.Body.nextGroup(\n <span class=\"literal\">true</span>);\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">var</span> netBody = Matter.Composites.softBody(\n <span class=\"number\">1067</span>, \n <span class=\"number\">164</span>, \n <span class=\"number\">6</span>, \n <span class=\"number\">4</span>, \n <span class=\"number\">0</span>, \n <span class=\"number\">0</span>, \n <span class=\"literal\">false</span>, \n <span class=\"number\">8.5</span>, { \n <span class=\"comment\">// 篮球网</span>\n </div>\n <div class=\"line\">\n firction: \n <span class=\"number\">1</span>, \n <span class=\"comment\">// 摩擦力</span>\n </div>\n <div class=\"line\">\n frictionAir: \n <span class=\"number\">0.08</span>, \n <span class=\"comment\">// 空气摩擦力</span>\n </div>\n <div class=\"line\">\n restitution: \n <span class=\"number\">0</span>, \n <span class=\"comment\">// 弹性</span>\n </div>\n <div class=\"line\">\n render: { \n <span class=\"attr\">visible</span>: \n <span class=\"literal\">false</span> },\n </div>\n <div class=\"line\"> \n <span class=\"attr\">collisionFilter</span>: { \n <span class=\"attr\">group</span>: group }\n </div>\n <div class=\"line\">\n }, {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">render</span>: { \n <span class=\"attr\">lineWidth</span>: \n <span class=\"number\">2</span>, \n <span class=\"attr\">strokeStyle</span>: \n <span class=\"string\">\"#fff\"</span> }\n </div>\n <div class=\"line\">\n });\n </div>\n <div class=\"line\">\n netBody.bodies[\n <span class=\"number\">0</span>].isStatic = netBody.bodies[\n <span class=\"number\">5</span>].isStatic = \n <span class=\"literal\">true</span>; \n <span class=\"comment\">// 将篮球网固定起来</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">var</span> backboard = Matter.Bodies.rectangle(\n <span class=\"number\">1208</span>, \n <span class=\"number\">120</span>, \n <span class=\"number\">50</span>, \n <span class=\"number\">136</span>, { \n <span class=\"comment\">// 篮板刚体</span>\n </div>\n <div class=\"line\">\n isStatic: \n <span class=\"literal\">true</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">render</span>: { \n <span class=\"attr\">visible</span>: \n <span class=\"literal\">true</span> }\n </div>\n <div class=\"line\">\n });\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">var</span> backboardBlock = Matter.Bodies.rectangle(\n <span class=\"number\">1069</span>, \n <span class=\"number\">173</span>, \n <span class=\"number\">5</span>, \n <span class=\"number\">5</span>, { \n <span class=\"comment\">// 篮框边缘块</span>\n </div>\n <div class=\"line\">\n isStatic: \n <span class=\"literal\">true</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">render</span>: { \n <span class=\"attr\">visible</span>: \n <span class=\"literal\">true</span> }\n </div>\n <div class=\"line\">\n });\n </div>\n <div class=\"line\">\n Matter.World.add(\n <span class=\"keyword\">this</span>.engine.world, [ \n <span class=\"comment\">// 四周墙壁</span>\n </div>\n <div class=\"line\">\n ...\n </div>\n <div class=\"line\">\n Matter.Bodies.rectangle(\n <span class=\"number\">667</span>, \n <span class=\"number\">5</span>, \n <span class=\"number\">1334</span>, \n <span class=\"number\">10</span>, { \n <span class=\"comment\">// x, y, w, h</span>\n </div>\n <div class=\"line\">\n isStatic: \n <span class=\"literal\">true</span>\n </div>\n <div class=\"line\">\n }),\n </div>\n <div class=\"line\">\n ...\n </div>\n <div class=\"line\">\n ]);\n </div>\n <div class=\"line\">\n Matter.World.add(\n <span class=\"keyword\">this</span>.engine.world, [netBody, backboard, backboardBlock]);\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/basketball/body.jpg\" alt=\"body\"></p>\n<h4 id=\"六、判断进球、监听睡眠状态\" class=\"post-heading\"><a href=\"#六、判断进球、监听睡眠状态\" class=\"headerlink\" title=\"六、判断进球、监听睡眠状态\"></a>六、判断进球、监听睡眠状态<a class=\"post-anchor\" href=\"#六、判断进球、监听睡眠状态\" aria-hidden=\"true\"></a></h4>\n<p>通过开启一个 <code>tick</code> 事件不停的监听球在运行时的位置,当到达某个位置时判定为进球。</p>\n<p>另外太多的篮球会影响性能,所以我们使用 <code>sleepStart</code> 事件监听篮球一段时间不动后,进入睡眠状态时删除。</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n ...\n </div>\n <div class=\"line\">\n Matter.Events.on(\n <span class=\"keyword\">this</span>.engine, \n <span class=\"string\">\'tick\'</span>, \n <span class=\"function\"><span class=\"keyword\">function</span>(<span class=\"params\"></span>) </span>{\n </div>\n <div class=\"line\">\n countDown++;\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span> (ball.position.x &gt; \n <span class=\"number\">1054</span> &amp;&amp; ball.position.x &lt; \n <span class=\"number\">1175</span> &amp;&amp; ball.position.y &gt; \n <span class=\"number\">170</span> &amp;&amp; ball.position.y &lt; \n <span class=\"number\">180</span> &amp;&amp; countDown &gt; \n <span class=\"number\">2</span>) {\n </div>\n <div class=\"line\">\n countDown = \n <span class=\"number\">0</span>;\n </div>\n <div class=\"line\"> \n <span class=\"built_in\">console</span>.log(\n <span class=\"string\">\'球进了!\'</span>);\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n });\n </div>\n <div class=\"line\">\n Matter.Events.on(ball, \n <span class=\"string\">\'sleepStart\'</span>, \n <span class=\"function\"><span class=\"keyword\">function</span>(<span class=\"params\"></span>) </span>{\n </div>\n <div class=\"line\">\n Matter.World.remove(This.engine.world, ball);\n </div>\n <div class=\"line\">\n });\n </div>\n <div class=\"line\">\n ...\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>到此为止,通过借助物理引擎所提供的碰撞、弹性、摩擦力等特性,一款简易版的投篮小游戏就完成了,也推荐大家阅读另一位同事的文章<a href=\"https://aotu.io/notes/2017/11/06/coindozer/\">【H5游戏开发】推金币</a> ,使用了 CreateJS + Matter.js 的方案,相信对你仿 3D 和 Matter.js 的使用上有更深的了解。</p>\n<p>最后,此次项目中只做了一些小尝试,Matter.js 能实现的远不止这些,移步官网发现更多的惊喜吧,文章的完整 Demo 代码可<a href=\"http://jdc.jd.com/demo/ball-demo/\" target=\"_blank\" rel=\"external\">【点击这里】</a>。</p>\n<p>如果对「H5游戏开发」感兴趣,欢迎关注我们的<a href=\"https://zhuanlan.zhihu.com/snsgame\" target=\"_blank\" rel=\"external\">专栏</a>。</p>\n<h2 id=\"参考\" class=\"post-heading\"><a href=\"#参考\" class=\"headerlink\" title=\"参考\"></a>参考<a class=\"post-anchor\" href=\"#参考\" aria-hidden=\"true\"></a></h2>\n<p><a href=\"http://brm.io/matter-js/\" target=\"_blank\" rel=\"external\">Matter.js</a></p>\n<p><a href=\"https://layaair.ldc.layabox.com/demo/?2d&amp;Sprite&amp;DisplayImage\" target=\"_blank\" rel=\"external\">LayaAir Demo</a></p>\n<div class=\"post-tags\" style=\"display:none\">\n <a href=\"/tags/Matter-js/\">Matter.js</a> \n <a href=\"/tags/H5游戏开发/\">H5游戏开发</a> \n <a href=\"/tags/LayaAir/\">LayaAir</a>\n</div>\n<div class=\"post-categories\" style=\"display:none\">\n <a href=\"/cates/H5游戏开发/\">H5游戏开发</a>\n</div>\n<div class=\"post-announce\">\n 感谢您的阅读,本文由 \n <a href=\"//aotu.io\">凹凸实验室</a> 版权所有。如若转载,请注明出处:凹凸实验室(\n <a href=\"https://aotu.io/notes/2017/11/16/basketball/\">https://aotu.io/notes/2017/11/16/basketball/</a>)\n</div>\n<div class=\"post-revision\">\n <i class=\"fa fa-clock-o\"></i> \n <time class=\"post-updated\" datetime=\"2018-11-07T01:01:15.653Z\">上次更新:2018-11-07 09:01:15</time>\n</div>');
INSERT INTO `rumo_blog` VALUES ('17', 'H5游戏开发:套圈圈', '1', 'H5游戏开发:套圈圈', '1', '2018-11-11 21:02:31', '2018-11-11 21:02:31', '1', '0', 'http://misc.aotu.io/Chen-jj/waterful_cover_900x500.jpg', '', '0', '0', '<a id=\"more\"></a>\n<h1 id=\"前言\" class=\"post-heading\"><a href=\"#前言\" class=\"headerlink\" title=\"前言\"></a>前言<a class=\"post-anchor\" href=\"#前言\" aria-hidden=\"true\"></a></h1>\n<p>虽然本文标题为介绍一个水压套圈h5游戏,但是窃以为仅仅如此对读者是没什么帮助的,毕竟读者们的工作生活很少会再写一个类似的游戏,更多的是面对需求的挑战。我更希望能举一反三,给大家在编写h5游戏上带来一些启发,无论是从整体流程的把控,对游戏框架、物理引擎的熟悉程度还是在某一个小难点上的思路突破等。因此本文将很少详细列举实现代码,取而代之的是以伪代码展现思路为主。</p>\n<p>游戏 demo 地址:<a href=\"http://jdc.jd.com/fd/demo/waterful/game.html\" target=\"_blank\" rel=\"external\">http://jdc.jd.com/fd/demo/waterful/game.html</a></p>\n<h4 id=\"希望能给诸位读者带来的启发\" class=\"post-heading\"><a href=\"#希望能给诸位读者带来的启发\" class=\"headerlink\" title=\"希望能给诸位读者带来的启发\"></a>希望能给诸位读者带来的启发<a class=\"post-anchor\" href=\"#希望能给诸位读者带来的启发\" aria-hidden=\"true\"></a></h4>\n<ol>\n <li>技术选型</li>\n <li>整体代码布局</li>\n <li>难点及解决思路</li>\n <li>优化点</li>\n</ol>\n<h1 id=\"技术选型\" class=\"post-heading\"><a href=\"#技术选型\" class=\"headerlink\" title=\"技术选型\"></a>技术选型<a class=\"post-anchor\" href=\"#技术选型\" aria-hidden=\"true\"></a></h1>\n<p>一个项目用什么技术来实现,权衡的因素有许多。其中时间是必须优先考虑的,毕竟效果可以减,但上线时间是死的。</p>\n<p>本项目预研时间一周,真正排期时间只有两周。虽然由项目特点来看比较适合走 3D 方案,但时间明显是不够的。最后保守起见,决定采用 2D 方案尽量逼近真实立体的游戏效果。</p>\n<p>从游戏复杂度来考虑,无须用到 Egret 或 Cocos 这些“牛刀”,而轻量、易上手、团队内部也有深厚沉淀的 <a href=\"https://createjs.com/\" target=\"_blank\" rel=\"external\">CreateJS</a> 则成为了渲染框架的首选。</p>\n<p>另外需要考虑的是是否需要引入物理引擎,这点需要从游戏的特点去考虑。本游戏涉及重力、碰撞、施力等因素,引入物理引擎对开发效率的提高要大于学习使用物理引擎的成本。因此权衡再三,我引入了同事们已经玩得挺溜的 <a href=\"http://brm.io/matter-js/\" target=\"_blank\" rel=\"external\">Matter.js</a>。( Matter.js 文档清晰、案例丰富,是切入学习 web 游戏引擎的一个不错的框架)</p>\n<h1 id=\"整体代码布局\" class=\"post-heading\"><a href=\"#整体代码布局\" class=\"headerlink\" title=\"整体代码布局\"></a>整体代码布局<a class=\"post-anchor\" href=\"#整体代码布局\" aria-hidden=\"true\"></a></h1>\n<p>在代码组织上,我选择了面向对象的手法,对整个游戏做一个封装,抛出一些控制接口给其他逻辑层调用。</p>\n<p>伪代码:</p>\n<figure class=\"highlight html\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">&lt;!-- index.html --&gt;</span>\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"comment\">&lt;!-- 游戏入口 canvas --&gt;</span>\n </div>\n <div class=\"line\">\n <span class=\"tag\">&lt;<span class=\"name\">canvas</span> <span class=\"attr\">id</span>=<span class=\"string\">\"waterfulGameCanvas\"</span> <span class=\"attr\">width</span>=<span class=\"string\">\"660\"</span> <span class=\"attr\">height</span>=<span class=\"string\">\"570\"</span>&gt;</span>\n <span class=\"tag\">&lt;/<span class=\"name\">canvas</span>&gt;</span>\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div>\n <div class=\"line\">\n 16\n </div>\n <div class=\"line\">\n 17\n </div>\n <div class=\"line\">\n 18\n </div>\n <div class=\"line\">\n 19\n </div>\n <div class=\"line\">\n 20\n </div>\n <div class=\"line\">\n 21\n </div>\n <div class=\"line\">\n 22\n </div>\n <div class=\"line\">\n 23\n </div>\n <div class=\"line\">\n 24\n </div>\n <div class=\"line\">\n 25\n </div>\n <div class=\"line\">\n 26\n </div>\n <div class=\"line\">\n 27\n </div>\n <div class=\"line\">\n 28\n </div>\n <div class=\"line\">\n 29\n </div>\n <div class=\"line\">\n 30\n </div>\n <div class=\"line\">\n 31\n </div>\n <div class=\"line\">\n 32\n </div>\n <div class=\"line\">\n 33\n </div>\n <div class=\"line\">\n 34\n </div>\n <div class=\"line\">\n 35\n </div>\n <div class=\"line\">\n 36\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">// game.js</span>\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"comment\">/**</span>\n </div>\n <div class=\"line\">\n * 游戏对象\n </div>\n <div class=\"line\">\n */\n </div>\n <div class=\"line\">\n <span class=\"class\"><span class=\"keyword\">class</span> <span class=\"title\">Waterful</span> </span>{\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 初始化函数</span>\n </div>\n <div class=\"line\">\n init () {}\n </div>\n <div class=\"line\"> \n </div>\n <div class=\"line\"> \n <span class=\"comment\">// CreateJS Tick,游戏操作等事件的绑定放到游戏对象内</span>\n </div>\n <div class=\"line\">\n eventBinding () {}\n </div>\n <div class=\"line\"> \n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 暴露的一些方法</span>\n </div>\n <div class=\"line\">\n score () {}\n </div>\n <div class=\"line\"> \n </div>\n <div class=\"line\">\n restart () {}\n </div>\n <div class=\"line\"> \n </div>\n <div class=\"line\">\n pause () {}\n </div>\n <div class=\"line\"> \n </div>\n <div class=\"line\">\n resume () {}\n </div>\n <div class=\"line\"> \n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 技能</span>\n </div>\n <div class=\"line\">\n skillX () {}\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"></div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"comment\">/**</span>\n </div>\n <div class=\"line\">\n * 环对象\n </div>\n <div class=\"line\">\n */\n </div>\n <div class=\"line\">\n <span class=\"class\"><span class=\"keyword\">class</span> <span class=\"title\">Ring</span> </span>{\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 于每一个 CreateJS Tick 都调用环自身的 update 函数</span>\n </div>\n <div class=\"line\">\n update () {}\n </div>\n <div class=\"line\"> \n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 进针后的逻辑</span>\n </div>\n <div class=\"line\">\n afterCollision () {}\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">// main.js</span>\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"comment\">// 根据业务逻辑初始化游戏,调用游戏的各种接口</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">const</span> waterful = \n <span class=\"keyword\">new</span> Waterful()\n </div>\n <div class=\"line\">\n waterful.init({...})\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h1 id=\"初始化\" class=\"post-heading\"><a href=\"#初始化\" class=\"headerlink\" title=\"初始化\"></a>初始化<a class=\"post-anchor\" href=\"#初始化\" aria-hidden=\"true\"></a></h1>\n<p>游戏的初始化接口主要做了4件事情:</p>\n<ol>\n <li>参数初始化</li>\n <li>CreateJS 显示元素(display object)的布局</li>\n <li>Matter.js 刚体(rigid body)的布局</li>\n <li>事件的绑定</li>\n</ol>\n<p>下面主要聊聊游戏场景里各种元素的创建与布局,即第二、第三点。</p>\n<h3 id=\"一、CreateJS-结合-Matter-js\" class=\"post-heading\"><a href=\"#一、CreateJS-结合-Matter-js\" class=\"headerlink\" title=\"一、CreateJS 结合 Matter.js\"></a>一、CreateJS 结合 Matter.js<a class=\"post-anchor\" href=\"#一、CreateJS-结合-Matter-js\" aria-hidden=\"true\"></a></h3>\n<p>阅读 Matter.js 的 demo 案例,都是用其自带的渲染引擎 Matter.Render。但是由于某些原因(后面会说到),我们需要使用 CreateJS 去渲染每个环的贴图。</p>\n<p>不像 Laya 配有和 Matter.js 自身用法一致的 Render,CreateJS 需要单独创建一个贴图层,然后在每个 Tick 里把贴图层的坐标同步为 Matter.js 刚体的当前坐标。</p>\n<p>伪代码:</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n createjs.Ticker.addEventListener(\n <span class=\"string\">\'tick\'</span>, e =&gt; {\n </div>\n <div class=\"line\">\n 环贴图的坐标 = 环刚体的坐标\n </div>\n <div class=\"line\">\n })\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>使用 CreateJS 去渲染后,要单独调试 Matter.js 的刚体是非常不便的。建议写一个调试模式专门使用 Matter.js 的 Render 去渲染,以便跟踪刚体的运动轨迹。</p>\n<h3 id=\"二、环\" class=\"post-heading\"><a href=\"#二、环\" class=\"headerlink\" title=\"二、环\"></a>二、环<a class=\"post-anchor\" href=\"#二、环\" aria-hidden=\"true\"></a></h3>\n<p>本游戏的难点是要以 2D 去模拟 3D,环是一点,进针的效果是一点,先说环。</p>\n<p>环由一个圆形的刚体,和半径稍大一些的贴图层所组成。如下图,蓝色部分为刚体:</p>\n<p><img src=\"//misc.aotu.io/Chen-jj/waterful_1.png\" alt=\"环\"></p>\n<p>伪代码:</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"class\"><span class=\"keyword\">class</span> <span class=\"title\">Ring</span> </span>{\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">constructor</span> () {\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 贴图</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">this</span>.texture = \n <span class=\"keyword\">new</span> createjs.Sprite(...)\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 刚体</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">this</span>.body = Matter.Bodies.circle(...)\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h3 id=\"三、刚体\" class=\"post-heading\"><a href=\"#三、刚体\" class=\"headerlink\" title=\"三、刚体\"></a>三、刚体<a class=\"post-anchor\" href=\"#三、刚体\" aria-hidden=\"true\"></a></h3>\n<p>为什么把刚体半径做得稍小呢,这也是受这篇文章 <a href=\"https://aotu.io/notes/2017/11/06/coindozer/\">推金币</a> 里金币的做法所启发。推金币游戏中,为了达到金币间的堆叠效果,作者很聪明地把刚体做得比贴图小,这样当刚体挤在一起时,贴图间就会层叠起来。所以这样做是为了使环之间稍微有点重叠效果,更重要的也是当两个紧贴的环不会因翻转角度太接近而显得留白太多。如图:</p>\n<p><img src=\"//misc.aotu.io/Chen-jj/waterful_2.png\" alt=\"贴近的环\"></p>\n<p>为了模拟环在水中运动的效果,可以选择给环加一些空气摩擦力。另外在实物游戏里,环是塑料做成的,碰撞后动能消耗较大,因此可以把环的 restitution 值调得稍微小一些。</p>\n<p>需要注意 Matter.js 中因为各种物理参数都是没有单位的,一些物理公式很可能用不上,只能基于其默认值慢慢进行微调。下面的 frictionAir 和 restitution 值就是我慢慢凭感觉调整出来的:</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">this</span>.body = Matter.Bodies.circle(x, y, r, {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">frictionAir</span>: \n <span class=\"number\">0.02</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">restitution</span>: \n <span class=\"number\">0.15</span>\n </div>\n <div class=\"line\">\n })\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h3 id=\"四、贴图\" class=\"post-heading\"><a href=\"#四、贴图\" class=\"headerlink\" title=\"四、贴图\"></a>四、贴图<a class=\"post-anchor\" href=\"#四、贴图\" aria-hidden=\"true\"></a></h3>\n<p>环在现实世界中的旋转是三维的,而 CreateJS 只能控制元素在二维平面上的旋转。对于一个环来说,二维平面的旋转是没有任何意义的,无论如何旋转,都只会是同一个样子。</p>\n<p>想要达到环绕 x 轴旋转的效果,一开始想到的是使用 rotation + scaleY。虽然这样能在视觉上达到目的,但是 scaleY 会导致环有被压扁的感觉,图片会失真:</p>\n<p><img src=\"//misc.aotu.io/Chen-jj/waterful_3.gif\" width=\"50\"></p>\n<p>显然这样的效果是不能接受的,最后我采取了逐帧图的方式,最接近地还原了环的旋转姿态:</p>\n<p><img src=\"//misc.aotu.io/Chen-jj/waterful_4-0.png\" width=\"372\"></p>\n<p><img src=\"//misc.aotu.io/Chen-jj/waterful_4.gif\" width=\"50\"></p>\n<p>注意在每个 Tick 里需要去判断环是否静止,若非静止则继续播放,并将贴图的 rotation 值赋值为刚体的旋转角度。如果是停止状态,则暂停逐帧图的播放:</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">// 贴图与刚体位置的小数点后几位有点不一样,需要降低精度</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">const</span> x1 = \n <span class=\"built_in\">Math</span>.round(texture.x)\n </div>\n <div class=\"line\">\n <span class=\"keyword\">const</span> x2 = \n <span class=\"built_in\">Math</span>.round(body.position.x)\n </div>\n <div class=\"line\">\n <span class=\"keyword\">const</span> y1 = \n <span class=\"built_in\">Math</span>.round(texture.y)\n </div>\n <div class=\"line\">\n <span class=\"keyword\">const</span> y2 = \n <span class=\"built_in\">Math</span>.round(body.position.y)\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"keyword\">if</span> (x1 !== x2 || y1 !== y2) {\n </div>\n <div class=\"line\">\n texture.paused &amp;&amp; texture.play()\n </div>\n <div class=\"line\">\n texture.rotation = body.angle * \n <span class=\"number\">180</span> / \n <span class=\"built_in\">Math</span>.PI\n </div>\n <div class=\"line\">\n } \n <span class=\"keyword\">else</span> {\n </div>\n <div class=\"line\">\n !texture.paused &amp;&amp; texture.stop()\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"> \n </div>\n <div class=\"line\">\n texture.x = body.position.x\n </div>\n <div class=\"line\">\n texture.y = body.position.y\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h3 id=\"五、舞台\" class=\"post-heading\"><a href=\"#五、舞台\" class=\"headerlink\" title=\"五、舞台\"></a>五、舞台<a class=\"post-anchor\" href=\"#五、舞台\" aria-hidden=\"true\"></a></h3>\n<p>舞台需要主要由物理世界、背景图,墙壁,针所组成。</p>\n<h4 id=\"1-物理世界\" class=\"post-heading\"><a href=\"#1-物理世界\" class=\"headerlink\" title=\"1. 物理世界\"></a>1. 物理世界<a class=\"post-anchor\" href=\"#1-物理世界\" aria-hidden=\"true\"></a></h4>\n<p>为了模拟真实世界环在水中的向下加速度,可以把 y 方向的 g 值调小:</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n engine.world.gravity.y = \n <span class=\"number\">0.2</span>\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>左右重力感应对环的加速度影响同样可以通过改变 x 方向的 g 值达到:</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">// 最大倾斜角度为 70 度,让用户不需要过分倾斜手机</span>\n </div>\n <div class=\"line\">\n <span class=\"comment\">// 0.4 为灵敏度值,根据具体情况调整</span>\n </div>\n <div class=\"line\">\n <span class=\"built_in\">window</span>.addEventListener(\n <span class=\"string\">\'deviceorientation\'</span>, e =&gt; {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> gamma = e.gamma\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span> (gamma &lt; \n <span class=\"number\">-70</span>) gamma = \n <span class=\"number\">-70</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span> (gamma &gt; \n <span class=\"number\">70</span>) gamma = \n <span class=\"number\">70</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">this</span>.engine.world.gravity.x = (e.gamma / \n <span class=\"number\">70</span>) * \n <span class=\"number\">0.4</span>\n </div>\n <div class=\"line\">\n })\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h4 id=\"2-背景图\" class=\"post-heading\"><a href=\"#2-背景图\" class=\"headerlink\" title=\"2. 背景图\"></a>2. 背景图<a class=\"post-anchor\" href=\"#2-背景图\" aria-hidden=\"true\"></a></h4>\n<p>本游戏布景为游戏机及海底世界,两者可以作为父容器的背景图,把 canvas 的位置定位到游戏机内即可。canvas 覆盖范围为下图的蓝色蒙层:</p>\n<p><img src=\"//misc.aotu.io/Chen-jj/waterful_5.jpg\" width=\"375\"></p>\n<h4 id=\"3-墙壁\" class=\"post-heading\"><a href=\"#3-墙壁\" class=\"headerlink\" title=\"3. 墙壁\"></a>3. 墙壁<a class=\"post-anchor\" href=\"#3-墙壁\" aria-hidden=\"true\"></a></h4>\n<p>因为环的刚体半径比贴图半径小,因此墙壁刚体需要有一些提前位移,环贴图才不会溢出,位移量为 R - r(下图红线为墙壁刚体的一部分):</p>\n<p><img src=\"//misc.aotu.io/Chen-jj/waterful_6.png\" width=\"140\"></p>\n<h4 id=\"4-针\" class=\"post-heading\"><a href=\"#4-针\" class=\"headerlink\" title=\"4. 针\"></a>4. 针<a class=\"post-anchor\" href=\"#4-针\" aria-hidden=\"true\"></a></h4>\n<p>为了模拟针的边缘轮廓,针的刚体由一个矩形与一个圆形所组成。下图红线描绘了针的刚体:</p>\n<p><img src=\"//misc.aotu.io/Chen-jj/waterful_7.png\" width=\"66\"></p>\n<p>为什么针边缘没有像墙壁一样有一些提前量呢?这是因为进针效果要求针顶的平台区域尽量地窄。作为补偿,可以把环刚体的半径尽可能地调得更大,这样在视觉上环与针的重叠也就不那么明显了。</p>\n<h1 id=\"进针\" class=\"post-heading\"><a href=\"#进针\" class=\"headerlink\" title=\"进针\"></a>进针<a class=\"post-anchor\" href=\"#进针\" aria-hidden=\"true\"></a></h1>\n<p>进针是整个游戏的核心部分,也是最难模拟的地方。</p>\n<h3 id=\"进针后\" class=\"post-heading\"><a href=\"#进针后\" class=\"headerlink\" title=\"进针后\"></a>进针后<a class=\"post-anchor\" href=\"#进针后\" aria-hidden=\"true\"></a></h3>\n<p>两个二维平面的物体交错是不能产生“穿过”效果的:</p>\n<p><img src=\"//misc.aotu.io/Chen-jj/waterful_8.gif\" width=\"66\"></p>\n<p>除非把环分成前后两部分,这样层级关系才能得到解决。但是由于环贴图是逐帧图,分两部分的做法并不合适。</p>\n<p>最后找到的解决办法是利用视觉错位来达到“穿过”效果:</p>\n<p><img src=\"//misc.aotu.io/Chen-jj/waterful_9.gif\" width=\"66\"></p>\n<p>具体做法是,当环被判定成功进针时,把环刚体去掉,环的逐帧图逐渐播放到<strong>平放</strong>的那一帧,rotation 值也逐渐变为 0。同时利用 CreateJS 的 Tween 动画把环平移到针底。</p>\n<p>进针后需要去掉环刚体,平移环贴图,这就是上文为什么环的贴图必须由 CreateJS 负责渲染的答案。</p>\n<p>伪代码:</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div>\n <div class=\"line\">\n 16\n </div>\n <div class=\"line\">\n 17\n </div>\n <div class=\"line\">\n 18\n </div>\n <div class=\"line\">\n 19\n </div>\n <div class=\"line\">\n 20\n </div>\n <div class=\"line\">\n 21\n </div>\n <div class=\"line\">\n 22\n </div>\n <div class=\"line\">\n 23\n </div>\n <div class=\"line\">\n 24\n </div>\n <div class=\"line\">\n 25\n </div>\n <div class=\"line\">\n 26\n </div>\n <div class=\"line\">\n 27\n </div>\n <div class=\"line\">\n 28\n </div>\n <div class=\"line\">\n 29\n </div>\n <div class=\"line\">\n 30\n </div>\n <div class=\"line\">\n 31\n </div>\n <div class=\"line\">\n 32\n </div>\n <div class=\"line\">\n 33\n </div>\n <div class=\"line\">\n 34\n </div>\n <div class=\"line\">\n 35\n </div>\n <div class=\"line\">\n 36\n </div>\n <div class=\"line\">\n 37\n </div>\n <div class=\"line\">\n 38\n </div>\n <div class=\"line\">\n 39\n </div>\n <div class=\"line\">\n 40\n </div>\n <div class=\"line\">\n 41\n </div>\n <div class=\"line\">\n 42\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">// Object Ring</span>\n </div>\n <div class=\"line\">\n afterCollision (waterful) {\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 平移到针底部</span>\n </div>\n <div class=\"line\">\n createjs.Tween.get(\n <span class=\"keyword\">this</span>.texture)\n </div>\n <div class=\"line\">\n .to({\n <span class=\"attr\">y</span>: y}, duration)\n </div>\n <div class=\"line\"></div>\n <div class=\"line\"> \n <span class=\"comment\">// 消去刚体</span>\n </div>\n <div class=\"line\">\n Matter.World.remove(waterful.engine.world, \n <span class=\"keyword\">this</span>.body)\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">this</span>.body = \n <span class=\"literal\">null</span>\n </div>\n <div class=\"line\"></div>\n <div class=\"line\"> \n <span class=\"comment\">// 接下来每一 Tick 的更新逻辑改变如下</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">this</span>.update = \n <span class=\"function\"><span class=\"keyword\">function</span> (<span class=\"params\"></span>) </span>{\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">const</span> texture = \n <span class=\"keyword\">this</span>.texture\n </div>\n <div class=\"line\"></div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span> 当前环贴图就是第 \n <span class=\"number\">0</span> 帧(环平放的那一帧){\n </div>\n <div class=\"line\">\n texture.gotoAndStop(\n <span class=\"number\">0</span>)\n </div>\n <div class=\"line\">\n } \n <span class=\"keyword\">else</span> {\n </div>\n <div class=\"line\">\n 每 \n <span class=\"number\">5</span> 个 Tick 往前播放一帧(相隔多少 Tick 切换一帧可以凭感觉调整,主要是为了使切换到平放状态的过程不显得太突兀)\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"></div>\n <div class=\"line\"> \n <span class=\"comment\">// 使针大概在环中央位置穿过</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span> (texture.x &lt; \n <span class=\"number\">200</span>) ++texture.x\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span> (texture.x &gt; \n <span class=\"number\">213</span> &amp;&amp; texture.x &lt; \n <span class=\"number\">300</span>) --texture.x\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span> (texture.x &gt; \n <span class=\"number\">462</span>) --texture.x\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span> (texture.x &gt; \n <span class=\"number\">400</span> &amp;&amp; texture.x &lt; \n <span class=\"number\">448</span>) ++texture.x\n </div>\n <div class=\"line\"></div>\n <div class=\"line\"> \n <span class=\"comment\">// 把环贴图尽快旋转到水平状态</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> rotation = \n <span class=\"built_in\">Math</span>.round(texture.rotation) % \n <span class=\"number\">180</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span> (rotation &lt; \n <span class=\"number\">0</span>) rotation += \n <span class=\"number\">180</span>\n </div>\n <div class=\"line\"></div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span> (rotation &gt; \n <span class=\"number\">0</span> &amp;&amp; rotation &lt;= \n <span class=\"number\">90</span>) {\n </div>\n <div class=\"line\">\n texture.rotation = rotation - \n <span class=\"number\">1</span>\n </div>\n <div class=\"line\">\n } \n <span class=\"keyword\">else</span> \n <span class=\"keyword\">if</span> (rotation &gt; \n <span class=\"number\">90</span> &amp;&amp; rotation &lt; \n <span class=\"number\">180</span>) {\n </div>\n <div class=\"line\">\n texture.rotation = rotation + \n <span class=\"number\">1</span>\n </div>\n <div class=\"line\">\n } \n <span class=\"keyword\">else</span> \n <span class=\"keyword\">if</span> (frame === \n <span class=\"number\">0</span>) {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">this</span>.update = \n <span class=\"function\"><span class=\"keyword\">function</span> (<span class=\"params\"></span>) </span>{}\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"></div>\n <div class=\"line\"> \n <span class=\"comment\">// 调用得分回调函数</span>\n </div>\n <div class=\"line\">\n waterful.score()\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h3 id=\"进针判断\" class=\"post-heading\"><a href=\"#进针判断\" class=\"headerlink\" title=\"进针判断\"></a>进针判断<a class=\"post-anchor\" href=\"#进针判断\" aria-hidden=\"true\"></a></h3>\n<h4 id=\"进针条件\" class=\"post-heading\"><a href=\"#进针条件\" class=\"headerlink\" title=\"进针条件\"></a>进针条件<a class=\"post-anchor\" href=\"#进针条件\" aria-hidden=\"true\"></a></h4>\n<h5 id=\"1-到达针顶\" class=\"post-heading\"><a href=\"#1-到达针顶\" class=\"headerlink\" title=\"1. 到达针顶\"></a>1. 到达针顶<a class=\"post-anchor\" href=\"#1-到达针顶\" aria-hidden=\"true\"></a></h5>\n<p>到达针顶是环进针成功的必要条件。</p>\n<h5 id=\"2-动画帧\" class=\"post-heading\"><a href=\"#2-动画帧\" class=\"headerlink\" title=\"2. 动画帧\"></a>2. 动画帧<a class=\"post-anchor\" href=\"#2-动画帧\" aria-hidden=\"true\"></a></h5>\n<p>环必须垂直于针才能被顺利穿过,水平于针时应该是与针相碰后弹开。</p>\n<p>当然条件可以相对放宽一些,不需要完全垂直,下图红框内的6帧都被规定为符合条件:</p>\n<p><img src=\"//misc.aotu.io/Chen-jj/waterful_10.png\" width=\"372\"></p>\n<p>为了降低游戏难度,我规定超过针一半高度时,只循环播放前6帧:</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">this</span>.texture.on(\n <span class=\"string\">\'animationend\'</span>, e =&gt; {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span> (e.target.y &lt; \n <span class=\"number\">400</span>) {\n </div>\n <div class=\"line\">\n e.target.gotoAndPlay(\n <span class=\"string\">\'short\'</span>)\n </div>\n <div class=\"line\">\n } \n <span class=\"keyword\">else</span> {\n </div>\n <div class=\"line\">\n e.target.gotoAndPlay(\n <span class=\"string\">\'normal\'</span>)\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n })\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h5 id=\"3-rotation-值\" class=\"post-heading\"><a href=\"#3-rotation-值\" class=\"headerlink\" title=\"3. rotation 值\"></a>3. rotation 值<a class=\"post-anchor\" href=\"#3-rotation-值\" aria-hidden=\"true\"></a></h5>\n<p>同理,为了使得环与针相垂直,rotation 值不能太接近 90 度。经试验后规定 0 &lt;= rotation &lt;= 65 或 115 &lt;= rotation &lt;= 180 是进针的必要条件。</p>\n<p>下图这种过大的倾角逻辑上是不能进针成功的:</p>\n<p><img src=\"//misc.aotu.io/Chen-jj/waterful_11.png\" width=\"50\"></p>\n<h4 id=\"初探\" class=\"post-heading\"><a href=\"#初探\" class=\"headerlink\" title=\"初探\"></a>初探<a class=\"post-anchor\" href=\"#初探\" aria-hidden=\"true\"></a></h4>\n<p>一开始我想的是把三维的进针做成二维的“圆球进桶”,进针的判断也就归到物理事件上面去,不需要再去考虑。</p>\n<p>具体做法如下图,红线为针壁,当环刚体(蓝球)掉入桶内且与 Sensor (绿线)相碰,则判断进针成功。为了使游戏难度不至于太大,环刚体必须设置得较小,而且针壁间距离要比环刚体直径稍大。</p>\n<p><img src=\"//misc.aotu.io/Chen-jj/waterful_12.png\" width=\"66\"></p>\n<p>这种模拟其实已经能达到不错的效果了,但是一个技能打破了这种思路的可能性。</p>\n<p>产品那边想做一个放大技能,当用户使用此技能时环会放大,更容易套中。但是在桶口直径不变的情况下,只是环贴图变大并不能降低游戏难度。如果把环刚体变小,的确容易进了,但相近的环之间的贴图重叠范围会很大,这就显得很不合理了。</p>\n<h4 id=\"改进\" class=\"post-heading\"><a href=\"#改进\" class=\"headerlink\" title=\"改进\"></a>改进<a class=\"post-anchor\" href=\"#改进\" aria-hidden=\"true\"></a></h4>\n<p>“进桶”的思路走不通是因为不兼容放大技能,而放大技能改变的是环的直径。因此需要找到一种进针判断方法在环直径小时,进针难度大,直径大时,进针难度小。</p>\n<p>下面两图分别为普通环和放大环,其中红色虚线表示水平方向的内环直径:</p>\n<p><img src=\"//misc.aotu.io/Chen-jj/waterful_13.png\" width=\"50\"></p>\n<p><img src=\"//misc.aotu.io/Chen-jj/waterful_14.png\" width=\"100\"></p>\n<p>在针顶设置一小段探测线(下图红色虚线),当内环的水平直径与探测线相交时,证明进针成功,然后走进针后的逻辑。在环放大时,内环的水平直径变长,也就更容易与探测线相交。</p>\n<p><img src=\"//misc.aotu.io/Chen-jj/waterful_15.png\" width=\"66\"></p>\n<p>伪代码:</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div>\n <div class=\"line\">\n 16\n </div>\n <div class=\"line\">\n 17\n </div>\n <div class=\"line\">\n 18\n </div>\n <div class=\"line\">\n 19\n </div>\n <div class=\"line\">\n 20\n </div>\n <div class=\"line\">\n 21\n </div>\n <div class=\"line\">\n 22\n </div>\n <div class=\"line\">\n 23\n </div>\n <div class=\"line\">\n 24\n </div>\n <div class=\"line\">\n 25\n </div>\n <div class=\"line\">\n 26\n </div>\n <div class=\"line\">\n 27\n </div>\n <div class=\"line\">\n 28\n </div>\n <div class=\"line\">\n 29\n </div>\n <div class=\"line\">\n 30\n </div>\n <div class=\"line\">\n 31\n </div>\n <div class=\"line\">\n 32\n </div>\n <div class=\"line\">\n 33\n </div>\n <div class=\"line\">\n 34\n </div>\n <div class=\"line\">\n 35\n </div>\n <div class=\"line\">\n 36\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">// Object Ring</span>\n </div>\n <div class=\"line\">\n <span class=\"comment\">// 每一 Tick 都去判断每个运动中的环是否与探测线相交</span>\n </div>\n <div class=\"line\">\n update (waterful) {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">const</span> texture = \n <span class=\"keyword\">this</span>.texture\n </div>\n <div class=\"line\"></div>\n <div class=\"line\"> \n <span class=\"comment\">// 环当前中心点坐标</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">const</span> x0 = texture.x\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">const</span> y0 = texture.y\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 环的旋转弧度</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">const</span> angle = texture.rotation\n </div>\n <div class=\"line\"></div>\n <div class=\"line\"> \n <span class=\"comment\">// 内环半径</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">const</span> r = waterful.enlarging ? \n <span class=\"number\">16</span> * \n <span class=\"number\">1.5</span> : \n <span class=\"number\">16</span>\n </div>\n <div class=\"line\"></div>\n <div class=\"line\"> \n <span class=\"comment\">// 根据旋转角度算出内环水平直径的开始和结束坐标</span>\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 注意 Matter.js 拿到的是 rotation 值是弧度,需要转成角度</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">const</span> startPoint = {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">x</span>: x0 - r * \n <span class=\"built_in\">Math</span>.cos(angle * (\n <span class=\"built_in\">Math</span>.PI / \n <span class=\"number\">180</span>)),\n </div>\n <div class=\"line\"> \n <span class=\"attr\">y</span>: y0 - r * \n <span class=\"built_in\">Math</span>.sin(angle * (\n <span class=\"built_in\">Math</span>.PI / \n <span class=\"number\">180</span>))\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">const</span> endPoint = {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">x</span>: x0 + r * \n <span class=\"built_in\">Math</span>.cos(-angle * (\n <span class=\"built_in\">Math</span>.PI / \n <span class=\"number\">180</span>)),\n </div>\n <div class=\"line\"> \n <span class=\"attr\">y</span>: y0 + r * \n <span class=\"built_in\">Math</span>.sin(angle * (\n <span class=\"built_in\">Math</span>.PI / \n <span class=\"number\">180</span>))\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"></div>\n <div class=\"line\"> \n <span class=\"comment\">// mn 为左侧探测线段的两点,uv 为右侧探测线段的两点</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">const</span> m = {\n <span class=\"attr\">x</span>: \n <span class=\"number\">206</span>, \n <span class=\"attr\">y</span>: \n <span class=\"number\">216</span>}, n = {\n <span class=\"attr\">x</span>: \n <span class=\"number\">206</span>, \n <span class=\"attr\">y</span>: \n <span class=\"number\">400</span>},\n </div>\n <div class=\"line\">\n u = {\n <span class=\"attr\">x</span>: \n <span class=\"number\">455</span>, \n <span class=\"attr\">y</span>: \n <span class=\"number\">216</span>}, v = {\n <span class=\"attr\">x</span>: \n <span class=\"number\">455</span>, \n <span class=\"attr\">y</span>: \n <span class=\"number\">400</span>}\n </div>\n <div class=\"line\"> \n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span> (segmentsIntr(startPoint, endPoint, m, n) || segmentsIntr(startPoint, endPoint, u, v)) {\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 内环直径与 mn 或 uv 相交,证明进针成功</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">this</span>.afterCollision(waterful)\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"> \n </div>\n <div class=\"line\">\n ...\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>判断线段是否相交的算法可以参考这篇文章:<a href=\"http://fins.iteye.com/blog/1522259\" target=\"_blank\" rel=\"external\">谈谈”求线段交点”的几种算法</a></p>\n<p>这种思路有两个不合常理的点:</p>\n<p>1.当环在针顶平台直到静止时,内环水平直径都没有和探测线相交,或者相交了但是 rotation 值不符合进针要求,视觉上给人的感受就是环在针顶上静止了:</p>\n<p><img src=\"//misc.aotu.io/Chen-jj/waterful_16.png\" width=\"66\"></p>\n<p>解决思路一是通过重力感应,因为设置了重力感应,只要用户稍微动一下手机环就会动起来。二是判断环刚体在针顶平台完全静止了,则给它施加一个力,让它往下掉。</p>\n<p>2.有可能环的运动轨迹是在针顶划过,但与探测线相交了,此时会给玩家一种环被吸下来的感觉。可以通过适当设置探测线的长度来减少这种情况发生的几率。</p>\n<h1 id=\"优化\" class=\"post-heading\"><a href=\"#优化\" class=\"headerlink\" title=\"优化\"></a>优化<a class=\"post-anchor\" href=\"#优化\" aria-hidden=\"true\"></a></h1>\n<h3 id=\"资源池\" class=\"post-heading\"><a href=\"#资源池\" class=\"headerlink\" title=\"资源池\"></a>资源池<a class=\"post-anchor\" href=\"#资源池\" aria-hidden=\"true\"></a></h3>\n<p>资源回收复用,是游戏常用的优化手法,接下来通过讲解气泡动画的实现来简单介绍一下。</p>\n<p>气泡动画是逐帧图,用户点击按钮时,即创建一个 createjs.Sprite。在 animationend 时,把该 sprite 对象从 createjs.Stage 中 remove 掉。</p>\n<p>可想而知,当用户不停点击时,会不断的创建 createjs.Sprite 对象,非常耗费资源。如果能复用之前播放完被 remove 掉的 sprite 对象,就能解决此问题。</p>\n<p>具体做法是每当用户按下按钮时,先去资源池数组找有没有 sprite 对象。如果没有则创建,animationend 时把 sprite 对象从 stage 里 remove 掉,然后 push 进资源池。如果有,则从资源池取出并直接使用该对象。</p>\n<p>当然用户的点击操作事件需要节流处理,例如至少 300ms 后才能播放下一个气泡动画。</p>\n<p>伪代码:</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">// Object Waterful</span>\n </div>\n <div class=\"line\">\n getBubble = throttle(\n <span class=\"function\"><span class=\"keyword\">function</span> (<span class=\"params\"></span>) </span>{\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 存在空闲泡泡即返回</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span> (\n <span class=\"keyword\">this</span>._idleBubbles.length) \n <span class=\"keyword\">return</span> \n <span class=\"keyword\">this</span>._idleBubbles.shift()\n </div>\n <div class=\"line\"></div>\n <div class=\"line\"> \n <span class=\"comment\">// 不存在则创建</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">const</span> bubble = \n <span class=\"keyword\">new</span> createjs.Sprite(...)\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n bubble.on(\n <span class=\"string\">\'animationend\'</span>, () =&gt; {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">this</span>._stage.removeChild(bubble)\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">this</span>._idleBubbles.push(bubble)\n </div>\n <div class=\"line\">\n })\n </div>\n <div class=\"line\"></div>\n <div class=\"line\"> \n <span class=\"keyword\">return</span> bubble\n </div>\n <div class=\"line\">\n }, \n <span class=\"number\">300</span>)\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h3 id=\"环速度过快导致飞出边界\" class=\"post-heading\"><a href=\"#环速度过快导致飞出边界\" class=\"headerlink\" title=\"环速度过快导致飞出边界\"></a>环速度过快导致飞出边界<a class=\"post-anchor\" href=\"#环速度过快导致飞出边界\" aria-hidden=\"true\"></a></h3>\n<p>Matter.js 里由于没有实现持续碰撞检测算法(<a href=\"http://www.stencyl.com/help/view/continuous-collision-detection\" target=\"_blank\" rel=\"external\">CCD</a>),所以在物体速度过快的情况下,和其他物体的碰撞不会被检测出来。当环速度很快时,也就会出现飞出墙壁的 bug。</p>\n<p>正常情况下,每次按键给环施加的力都是很小的。当用户快速连续点击时,y 方向累积的力也不至于过大。但还是有玩家反应游戏过程中环不见了的问题。最后发现当手机卡顿时,Matter.js 的 Tick 没有及时触发,导致卡顿完后把卡顿时累积起来的力一次性应用到环刚体上,环瞬间获得很大的速度,也就飞出了游戏场景。</p>\n<p>解决方法有两个:</p>\n<ol>\n <li>给按钮节流,300ms才能施加一次力。</li>\n <li>每次按下按钮,只是把一个标志位设为 true。在每个 Matter.js 的 Tick 里判断该标志位是否为 true,是则施力。保证每个 Matter.js 的 Tick 里只对环施加一次力。</li>\n</ol>\n<p>伪代码:</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n btn.addEventListener(\n <span class=\"string\">\'touchstart\'</span>, e =&gt; {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">this</span>.addForce = \n <span class=\"literal\">true</span>\n </div>\n <div class=\"line\">\n })\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n Events.on(\n <span class=\"keyword\">this</span>._engine, \n <span class=\"string\">\'beforeUpdate\'</span>, e =&gt; {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span> (!\n <span class=\"keyword\">this</span>.addForce) \n <span class=\"keyword\">return</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">this</span>.addForceLeft = \n <span class=\"literal\">false</span>\n </div>\n <div class=\"line\"></div>\n <div class=\"line\"> \n <span class=\"comment\">// 施力</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">this</span>._rings.forEach(\n <span class=\"function\"><span class=\"params\">ring</span> =&gt;</span> {\n </div>\n <div class=\"line\">\n Matter.Body.applyForce(ring.body, {\n <span class=\"attr\">x</span>: x, \n <span class=\"attr\">y</span>: y}, {\n <span class=\"attr\">x</span>: \n <span class=\"number\">0.02</span>, \n <span class=\"attr\">y</span>: \n <span class=\"number\">-0.03</span>})\n </div>\n <div class=\"line\">\n Matter.Body.setAngularVelocity(ring.body, \n <span class=\"built_in\">Math</span>.PI/\n <span class=\"number\">24</span>)\n </div>\n <div class=\"line\">\n })\n </div>\n <div class=\"line\">\n })\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h1 id=\"结语\" class=\"post-heading\"><a href=\"#结语\" class=\"headerlink\" title=\"结语\"></a>结语<a class=\"post-anchor\" href=\"#结语\" aria-hidden=\"true\"></a></h1>\n<p>如果对「H5游戏开发」感兴趣,欢迎关注我们的<a href=\"https://zhuanlan.zhihu.com/snsgame\" target=\"_blank\" rel=\"external\">专栏</a></p>\n<div class=\"post-tags\" style=\"display:none\">\n <a href=\"/tags/Matter-js/\">Matter.js</a> \n <a href=\"/tags/CreateJS/\">CreateJS</a> \n <a href=\"/tags/H5游戏开发/\">H5游戏开发</a>\n</div>\n<div class=\"post-categories\" style=\"display:none\">\n <a href=\"/cates/H5游戏开发/\">H5游戏开发</a>\n</div>\n<div class=\"post-announce\">\n 感谢您的阅读,本文由 \n <a href=\"//aotu.io\">凹凸实验室</a> 版权所有。如若转载,请注明出处:凹凸实验室(\n <a href=\"https://aotu.io/notes/2018/01/04/waterful/\">https://aotu.io/notes/2018/01/04/waterful/</a>)\n</div>\n<div class=\"post-revision\">\n <i class=\"fa fa-clock-o\"></i> \n <time class=\"post-updated\" datetime=\"2018-11-07T01:01:15.657Z\">上次更新:2018-11-07 09:01:15</time>\n</div>');
INSERT INTO `rumo_blog` VALUES ('18', 'H5游戏开发:一笔画', '1', 'H5游戏开发:一笔画', '1', '2018-11-11 21:02:32', '2018-11-11 21:02:32', '1', '0', 'https://misc.aotu.io/leeenx/onestroke/cover.jpg', '', '0', '0', '<a id=\"more\"></a>\n<p>一笔画是<u>图论</u><sup><a href=\"https://zh.wikipedia.org/wiki/%E5%9B%BE%E8%AE%BA\" target=\"_blank\" rel=\"external\">科普</a></sup>中一个著名的问题,它起源于<u>柯尼斯堡七桥问题</u><sup><a href=\"https://zh.wikipedia.org/wiki/%E6%9F%AF%E5%B0%BC%E6%96%AF%E5%A0%A1%E4%B8%83%E6%A1%A5%E9%97%AE%E9%A2%98\" target=\"_blank\" rel=\"external\">科普</a></sup>。数学家欧拉在他1736年发表的论文《柯尼斯堡的七桥》中不仅解决了七桥问题,也提出了一笔画定理,顺带解决了一笔画问题。用图论的术语来说,对于一个给定的<u>连通图</u><sup><a href=\"https://zh.wikipedia.org/wiki/%E8%BF%9E%E9%80%9A%E5%9B%BE\" target=\"_blank\" rel=\"external\">科普</a></sup>存在一条恰好包含所有线段并且没有重复的路径,这条路径就是「一笔画」。</p>\n<p>寻找连通图这条路径的过程就是「一笔画」的游戏过程,如下:</p>\n<p><img src=\"https://misc.aotu.io/leeenx/onestroke/2017-10-31-demo.gif\" alt=\"一笔画\"></p>\n<h2 id=\"游戏的实现\" class=\"post-heading\"><a href=\"#游戏的实现\" class=\"headerlink\" title=\"游戏的实现\"></a>游戏的实现<a class=\"post-anchor\" href=\"#游戏的实现\" aria-hidden=\"true\"></a></h2>\n<p>「一笔画」的实现不复杂,笔者把实现过程分成两步:</p>\n<ol>\n <li>底图绘制</li>\n <li>交互绘制</li>\n</ol>\n<p>「底图绘制」把连通图以「点线」的形式显示在画布上,是游戏最容易实现的部分;「交互绘制」是用户绘制解题路径的过程,这个过程会主要是处理点与点动态成线的逻辑。</p>\n<h3 id=\"底图绘制\" class=\"post-heading\"><a href=\"#底图绘制\" class=\"headerlink\" title=\"底图绘制\"></a>底图绘制<a class=\"post-anchor\" href=\"#底图绘制\" aria-hidden=\"true\"></a></h3>\n<p>「一笔画」是多关卡的游戏模式,笔者决定把关卡(连通图)的定制以一个配置接口的形式对外暴露。对外暴露关卡接口需要有一套描述连通图形状的规范,而在笔者面前有两个选项:</p>\n<ul>\n <li>点记法</li>\n <li>线记法</li>\n</ul>\n<p>举个连通图 —— 五角星为例来说一下这两个选项。</p>\n<p><img src=\"https://misc.aotu.io/leeenx/onestroke/2017-10-31-five.png?v=2\" alt=\"五角星\"></p>\n<p>点记法如下:<br></p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n levels: [\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 当前关卡</span>\n </div>\n <div class=\"line\">\n {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">name</span>: \n <span class=\"string\">\"五角星\"</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">coords</span>: [\n </div>\n <div class=\"line\">\n {\n <span class=\"attr\">x</span>: Ax, \n <span class=\"attr\">y</span>: Ay},\n </div>\n <div class=\"line\">\n {\n <span class=\"attr\">x</span>: Bx, \n <span class=\"attr\">y</span>: By},\n </div>\n <div class=\"line\">\n {\n <span class=\"attr\">x</span>: Cx, \n <span class=\"attr\">y</span>: Cy},\n </div>\n <div class=\"line\">\n {\n <span class=\"attr\">x</span>: Dx, \n <span class=\"attr\">y</span>: Dy},\n </div>\n <div class=\"line\">\n {\n <span class=\"attr\">x</span>: Ex, \n <span class=\"attr\">y</span>: Ey},\n </div>\n <div class=\"line\">\n {\n <span class=\"attr\">x</span>: Ax, \n <span class=\"attr\">y</span>: Ay}\n </div>\n <div class=\"line\">\n ]\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n ...\n </div>\n <div class=\"line\">\n ]\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<p>线记法如下:<br></p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n levels: [\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 当前关卡</span>\n </div>\n <div class=\"line\">\n {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">name</span>: \n <span class=\"string\">\"五角星\"</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">lines</span>: [\n </div>\n <div class=\"line\">\n {\n <span class=\"attr\">x1</span>: Ax, \n <span class=\"attr\">y1</span>: Ay, \n <span class=\"attr\">x2</span>: Bx, \n <span class=\"attr\">y2</span>: By},\n </div>\n <div class=\"line\">\n {\n <span class=\"attr\">x1</span>: Bx, \n <span class=\"attr\">y1</span>: By, \n <span class=\"attr\">x2</span>: Cx, \n <span class=\"attr\">y2</span>: Cy},\n </div>\n <div class=\"line\">\n {\n <span class=\"attr\">x1</span>: Cx, \n <span class=\"attr\">y1</span>: Cy, \n <span class=\"attr\">x2</span>: Dx, \n <span class=\"attr\">y2</span>: Dy},\n </div>\n <div class=\"line\">\n {\n <span class=\"attr\">x1</span>: Dx, \n <span class=\"attr\">y1</span>: Dy, \n <span class=\"attr\">x2</span>: Ex, \n <span class=\"attr\">y2</span>: Ey},\n </div>\n <div class=\"line\">\n {\n <span class=\"attr\">x1</span>: Ex, \n <span class=\"attr\">y1</span>: Ey, \n <span class=\"attr\">x2</span>: Ax, \n <span class=\"attr\">y2</span>: Ay}\n </div>\n <div class=\"line\">\n ]\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n ]\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<p>「点记法」记录关卡通关的一个答案,即端点要按一定的顺序存放到数组 <code>coords</code>中,它是有序性的记录。「线记法」通过两点描述连通图的线段,它是无序的记录。「点记法」最大的优势是表现更简洁,但它必须记录一个通关答案,笔者只是关卡的搬运工不是关卡创造者,所以笔者最终选择了「线记法」。:)</p>\n<h3 id=\"交互绘制\" class=\"post-heading\"><a href=\"#交互绘制\" class=\"headerlink\" title=\"交互绘制\"></a>交互绘制<a class=\"post-anchor\" href=\"#交互绘制\" aria-hidden=\"true\"></a></h3>\n<p>在画布上绘制路径,从视觉上说是「选择或连接连通图端点」的过程,这个过程需要解决2个问题:</p>\n<ul>\n <li>手指下是否有端点</li>\n <li>选中点到待选中点之间能否成线</li>\n</ul>\n<p>收集连通图端点的坐标,再监听手指滑过的坐标可以知道「手指下是否有点」。以下伪代码是收集端点坐标:</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">// 端点坐标信息</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">let</span> coords = [];\n </div>\n <div class=\"line\">\n lines.forEach(\n <span class=\"function\">(<span class=\"params\">{x1, y1, x2, y2}</span>) =&gt;</span> {\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// (x1, y1) 在 coords 数组不存在</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span>(!isExist(x1, y1)) coords.push([x1, y1]);\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// (x2, y2) 在 coords 数组不存在</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span>(!isExist(x2, y2)) coords.push([x2, y2]);\n </div>\n <div class=\"line\">\n });\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>以下伪代码是监听手指滑动:<br></p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n easel.addEventListener(\n <span class=\"string\">\"touchmove\"</span>, e =&gt; {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> x0 = e.targetTouches[\n <span class=\"number\">0</span>].pageX, y0 = e.targetTouches[\n <span class=\"number\">0</span>].pageY;\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 端点半径 ------ 取连通图端点半径的2倍,提升移动端体验</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> r = radius * \n <span class=\"number\">2</span>;\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">for</span>(\n <span class=\"keyword\">let</span> [x, y] \n <span class=\"keyword\">of</span> coords){\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span>(\n <span class=\"built_in\">Math</span>.sqrt(\n <span class=\"built_in\">Math</span>.pow(x - x0, \n <span class=\"number\">2</span>) + \n <span class=\"built_in\">Math</span>.pow(y - y0), \n <span class=\"number\">2</span>) &lt;= r){\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 手指下有端点,判断能否连线</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span>(canConnect(x, y)) {\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// todo</span>\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">break</span>;\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n })\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<p>在未绘制任何线段或端点之前,手指滑过的任意端点都会被视作「一笔画」的起始点;在绘制了线段(或有选中点)后,手指滑过的端点能否与选中点串连成线段需要依据现有条件进行判断。</p>\n<p><img src=\"https://misc.aotu.io/leeenx/onestroke/2017-10-31-five-2.png\" alt=\"示意图\"></p>\n<p>上图,点A与点B可连接成线段,而点A与点C不能连接。笔者把「可以与指定端点连接成线段的端点称作<strong>有效连接点</strong>」。连通图端点的有效连接点从连通图的线段中提取:<br></p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n coords.forEach(\n <span class=\"function\"><span class=\"params\">coord</span> =&gt;</span> {\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 有效连接点(坐标)挂载在端点坐标下</span>\n </div>\n <div class=\"line\">\n coord.validCoords = [];\n </div>\n <div class=\"line\">\n lines.forEach(\n <span class=\"function\">(<span class=\"params\">{x1, y1, x2, y2}</span>) =&gt;</span> {\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 坐标是当前线段的起点</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span>(coord.x === x1 &amp;&amp; coord.y === y1) {\n </div>\n <div class=\"line\">\n coord.validCoords.push([x2, y2]);\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 坐标是当前线段的终点</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">else</span> \n <span class=\"keyword\">if</span>(coord.x === x2 &amp;&amp; coord.y === y2) {\n </div>\n <div class=\"line\">\n coord.validCoords.push([x1, y1]);\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n })\n </div>\n <div class=\"line\">\n })\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<p>But…有效连接点只能判断两个点是否为底图的线段,这只是一个静态的参考,在实际的「交互绘制」中,会遇到以下情况:</p>\n<p><img src=\"https://misc.aotu.io/leeenx/onestroke/2017-11-01-five.png\" alt=\"AB成线\"><br>如上图,AB已串连成线段,当前选中点B的有效连接点是 A 与 C。AB 已经连接成线,如果 BA 也串连成线段,那么线段就重复了,所以此时 BA 不能成线,只有 AC 才能成线。</p>\n<p>对选中点而言,它的有效连接点有两种:</p>\n<ul>\n <li>与选中点「成线的有效连接点」</li>\n <li>与选中点「未成线的有效连接点」</li>\n</ul>\n<p>其中「未成线的有效连接点」才能参与「交互绘制」,并且它是动态的。</p>\n<p><img src=\"https://misc.aotu.io/leeenx/onestroke/2017-11-01-valid-vertexes.gif\" alt=\"未成线的有效连接点\"></p>\n<p>回头本节内容开头提的两个问题「手指下是否有端点」 与 「选中点到待选中点之间能否成线」,其实可合并为一个问题:<strong>手指下是否存在「未成线的有效连接点」</strong>。只须把监听手指滑动遍历的数组由连通图所有的端点坐标 <code>coords</code> 替换为当前选中点的「未成线的有效连接点」即可。</p>\n<p>至此「一笔画」的主要功能已经实现。可以抢先体验一下:</p>\n<p><img src=\"https://misc.aotu.io/leeenx/onestroke/2017-10-31-qr.png?v=2\" alt=\"demo\"></p>\n<p><a href=\"https://leeenx.github.io/OneStroke/src/onestroke.html\" target=\"_blank\" rel=\"external\">https://leeenx.github.io/OneStroke/src/onestroke.html</a></p>\n<h2 id=\"自动识图\" class=\"post-heading\"><a href=\"#自动识图\" class=\"headerlink\" title=\"自动识图\"></a>自动识图<a class=\"post-anchor\" href=\"#自动识图\" aria-hidden=\"true\"></a></h2>\n<p>笔者在录入关卡配置时,发现一个7条边以上的连通图很容易录错或录重线段。笔者在思考能否开发一个自动识别图形的插件,毕竟「一笔画」的图形是有规则的几何图形。</p>\n<p><img src=\"https://misc.aotu.io/leeenx/onestroke/2017-11-02-shape.png\" alt=\"底图\"></p>\n<p>上面的关卡「底图」,一眼就可以识出三个颜色:</p>\n<ul>\n <li>白底</li>\n <li>端点颜色</li>\n <li>线段颜色</li>\n</ul>\n<p>并且这三种颜色在「底图」的面积大小顺序是:白底 &gt; 线段颜色 &gt; 端点颜色。底图的「采集色值表算法」很简单,如下伪代码:</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">let</span> imageData = ctx.getImageData();\n </div>\n <div class=\"line\">\n <span class=\"keyword\">let</span> data = imageData.data;\n </div>\n <div class=\"line\">\n <span class=\"comment\">// 色值表</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">let</span> clrs = \n <span class=\"keyword\">new</span> \n <span class=\"built_in\">Map</span>();\n </div>\n <div class=\"line\">\n <span class=\"keyword\">for</span>(\n <span class=\"keyword\">let</span> i = \n <span class=\"number\">0</span>, len = data.length; i &lt; len; i += \n <span class=\"number\">4</span>) {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> [r, g, b, a] = [data[i], data[i + \n <span class=\"number\">1</span>], data[i + \n <span class=\"number\">2</span>], data[i + \n <span class=\"number\">3</span>]];\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> key = \n <span class=\"string\">`rgba(<span class=\"subst\">${r}</span>, <span class=\"subst\">${g}</span>, <span class=\"subst\">${b}</span>, <span class=\"subst\">${a}</span>)`</span>;\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> value = clrs.get(key) || {r, g, b, a, \n <span class=\"attr\">count</span>: \n <span class=\"number\">0</span>};\n </div>\n <div class=\"line\">\n clrs.has(key) ? ++value.count : clrs.set(rgba, {r, g, b, a, count});\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>对于连通图来说,只要把端点识别出来,连通图的轮廓也就出来了。</p>\n<h3 id=\"端点识别\" class=\"post-heading\"><a href=\"#端点识别\" class=\"headerlink\" title=\"端点识别\"></a>端点识别<a class=\"post-anchor\" href=\"#端点识别\" aria-hidden=\"true\"></a></h3>\n<p>理论上,通过采集的「色值表」可以直接把端点的坐标识别出来。笔者设计的「端点识别算法」分以下2步:</p>\n<ol>\n <li>按像素扫描底图直到遇到「端点颜色」的像素,进入第二步</li>\n <li>从底图上清除端点并记录它的坐标,返回继续第一步</li>\n</ol>\n<p>伪代码如下:<br></p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">for</span>(\n <span class=\"keyword\">let</span> i = \n <span class=\"number\">0</span>, len = data.length; i &lt; len; i += \n <span class=\"number\">4</span>) {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> [r, g, b, a] = [data[i], data[i + \n <span class=\"number\">1</span>], data[i + \n <span class=\"number\">2</span>], data[i + \n <span class=\"number\">3</span>]];\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 当前像素颜色属于端点</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span>(isBelongVertex(r, g, b, a)) {\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 在 data 中清空端点</span>\n </div>\n <div class=\"line\">\n vertex = clearVertex(i);\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 记录端点信息</span>\n </div>\n <div class=\"line\">\n vertexes.push(vertext);\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<p>But… 上面的算法只能跑无损图。笔者在使用了一张手机截屏做测试的时候发现,收集到的「色值表」长度为 5000+ !这直接导致端点和线段的色值无法直接获得。</p>\n<p>经过分析,可以发现「色值表」里绝大多数色值都是相近的,也就是在原来的「采集色值表算法」的基础上添加一个近似颜色过滤即可以找出端点和线段的主色。伪代码实现如下:</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">let</span> lineColor = vertexColor = {\n <span class=\"attr\">count</span>: \n <span class=\"number\">0</span>};\n </div>\n <div class=\"line\">\n <span class=\"keyword\">for</span>(\n <span class=\"keyword\">let</span> clr \n <span class=\"keyword\">of</span> clrs) {\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 与底色相近,跳过</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span>(isBelongBackground(clr)) \n <span class=\"keyword\">continue</span>;\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 线段是数量第二多的颜色,端点是第三多的颜色</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span>(clr.count &gt; lineColor.count) {\n </div>\n <div class=\"line\">\n [vertexColor, lineColor] = [lineColor, clr]\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>取到端点的主色后,再跑一次「端点识别算法」后居识别出 203 个端点!这是为什么呢?</p>\n<p><img src=\"https://misc.aotu.io/leeenx/onestroke/2017-11-02-vertex.png?v\" alt=\"局部\"></p>\n<p>上图是放大5倍后的底图局部,蓝色端点的周围和内部充斥着大量噪点(杂色块)。事实上在「端点识别」过程中,由于噪点的存在,把原本的端点被分解成十几个或数十个小端点了,以下是跑过「端点识别算法」后的底图:</p>\n<p><img src=\"https://misc.aotu.io/leeenx/onestroke/2017-11-02-after-scan.png\" alt=\"识别后\"></p>\n<p>通过上图,可以直观地得出一个结论:识别出来的小端点只在目标(大)端点上集中分布,并且大端点范围内的小端点叠加交错。</p>\n<p>如果把叠加交错的小端点归并成一个大端点,那么这个大端点将十分接近目标端点。小端点的归并伪代码如下:</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">for</span>(\n <span class=\"keyword\">let</span> i = \n <span class=\"number\">0</span>, len = vertexes.length; i &lt; len - \n <span class=\"number\">1</span>; ++i) {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> vertexA = vertexes[i];\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span>(vertextA === \n <span class=\"literal\">undefined</span>) \n <span class=\"keyword\">continue</span>;\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 注意这里 j = 0 而不是 j = i +1</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">for</span>(\n <span class=\"keyword\">let</span> j = \n <span class=\"number\">0</span>; j &lt; len; ++j) {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> vertexB = vertexes[j];\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span>(vertextB === \n <span class=\"literal\">undefined</span>) \n <span class=\"keyword\">continue</span>;\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 点A与点B有叠加,点B合并到点A并删除点B</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span>(isCross(vertexA, vertexB)) {\n </div>\n <div class=\"line\">\n vertexA = merge(vertexA, vertexB);\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">delete</span> vertexA;\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>加了小端点归并算法后,「端点识别」的准确度就上去了。经笔者本地测试已经可以 100% 识别有损的连通图了。</p>\n<h3 id=\"线段识别\" class=\"post-heading\"><a href=\"#线段识别\" class=\"headerlink\" title=\"线段识别\"></a>线段识别<a class=\"post-anchor\" href=\"#线段识别\" aria-hidden=\"true\"></a></h3>\n<p>笔者分两个步骤完成「线段识别」:</p>\n<ol>\n <li>给定的两个端点连接成线,并采集连线上N个「样本点」;</li>\n <li>遍历样本点像素,如果像素色值不等于线段色值则表示这两个端点之间不存在线段</li>\n</ol>\n<p>如何采集「样式点」是个问题,太密集会影响性能;太疏松精准度不能保证。</p>\n<p>在笔者面前有两个选择:N 是常量;N 是变量。<br>假设 <code>N === 5</code>。局部提取「样式点」如下:</p>\n<p><img src=\"https://misc.aotu.io/leeenx/onestroke/2017-11-01-pattern.gif\" alt=\"局部\"></p>\n<p>上图,会识别出三条线段:AB, BC 和 AC。而事实上,AC不能成线,它只是因为 AB 和 BC 视觉上共一线的结果。当然把 N 值向上提高可以解决这个问题,不过 N 作为常量的话,这个常量的取量需要靠经验来判断,果然放弃。</p>\n<p>为了避免 AB 与 BC 同处一直线时 AC 被识别成线段,其实很简单 —— <strong>两个「样本点」的间隔小于或等于端点直径</strong>。<br>假设 <code>N = S / (2 * R)</code>,S 表示两点的距离,R 表示端点半径。局部提取「样式点」如下:</p>\n<p><img src=\"https://misc.aotu.io/leeenx/onestroke/2017-11-01-pattern-2.gif\" alt=\"局部\"></p>\n<p>如上图,成功地绕过了 AC。「线段识别算法」的伪代码实现如下:<br></p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">for</span>(\n <span class=\"keyword\">let</span> i = \n <span class=\"number\">0</span>, len = vertexes.length; i &lt; len - \n <span class=\"number\">1</span>; ++i) {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> {\n <span class=\"attr\">x</span>: x1, \n <span class=\"attr\">y</span>: y1} = vertexes[i];\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">for</span>(\n <span class=\"keyword\">let</span> j = i + \n <span class=\"number\">1</span>; j &lt; len; ++j) {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> {\n <span class=\"attr\">x</span>: x2, \n <span class=\"attr\">y</span>: y2} = vertexes[j];\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> S = \n <span class=\"built_in\">Math</span>.sqrt(\n <span class=\"built_in\">Math</span>.pow(x1 - x2, \n <span class=\"number\">2</span>) + \n <span class=\"built_in\">Math</span>.pow(y1 - y2, \n <span class=\"number\">2</span>));\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> N = S / (R * \n <span class=\"number\">2</span>);\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> stepX = (x1 - x2) / N, stepY = (y1 - y2) / n;\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">while</span>(--N) {\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 样本点不是线段色</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span>(!isBelongLine(x1 + N * stepX, y1 + N * stepY)) \n <span class=\"keyword\">break</span>;\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 样本点都合格 ---- 表示两点成线,保存</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span>(\n <span class=\"number\">0</span> === N) lines.push({x1, y1, x2, y2})\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<h2 id=\"性能优化\" class=\"post-heading\"><a href=\"#性能优化\" class=\"headerlink\" title=\"性能优化\"></a>性能优化<a class=\"post-anchor\" href=\"#性能优化\" aria-hidden=\"true\"></a></h2>\n<p>由于「自动识图」需要对图像的的像素点进行扫描,那么性能确实是个需要关注的问题。笔者设计的「自动识图算法」,在识别图像的过程中需要对图像的像素做两次扫描:「采集色值表」 与 「采集端点」。在扫描次数上其实很难降低了,但是对于一张 <code>750 * 1334</code> 的底图来说,「自动识图算法」需要遍历两次长度为 <code>750 * 1334 * 4 = 4,002,000</code> 的数组,压力还是会有的。笔者是从压缩被扫描数组的尺寸来提升性能的。</p>\n<p><strong>被扫描数组的尺寸怎么压缩?</strong><br>笔者直接通过缩小画布的尺寸来达到缩小被扫描数组尺寸的。伪代码如下:</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">// 要压缩的倍数</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">let</span> resolution = \n <span class=\"number\">4</span>;\n </div>\n <div class=\"line\">\n <span class=\"keyword\">let</span> [width, height] = [img.width / resolution &gt;&gt; \n <span class=\"number\">0</span>, img.height / resolution &gt;&gt; \n <span class=\"number\">0</span>];\n </div>\n <div class=\"line\">\n ctx.drawImage(img, \n <span class=\"number\">0</span>, \n <span class=\"number\">0</span>, width, height);\n </div>\n <div class=\"line\">\n <span class=\"keyword\">let</span> imageData = ctx.getImageData(), data = imageData;\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>把源图片缩小4倍后,得到的图片像素数组只有原来的 <code>4^2 = 16倍</code>。这在性能上是很大的提升。</p>\n<h2 id=\"使用「自动识图」的建议\" class=\"post-heading\"><a href=\"#使用「自动识图」的建议\" class=\"headerlink\" title=\"使用「自动识图」的建议\"></a>使用「自动识图」的建议<a class=\"post-anchor\" href=\"#使用「自动识图」的建议\" aria-hidden=\"true\"></a></h2>\n<p>尽管笔者在本地测试的时候可以把所有的「底图」识别出来,但是并不能保证其它开发者上传的图片能否被很好的识别出来。笔者建议,可以把「自动识图」做为一个单独的工具使用。</p>\n<p>笔者写了一个「自动识图」的单独工具页面:<a href=\"https://leeenx.github.io/OneStroke/src/plugin.html\" target=\"_blank\" rel=\"external\">https://leeenx.github.io/OneStroke/src/plugin.html</a><br>可以在这个页面生成对应的关卡配置。</p>\n<h2 id=\"结语\" class=\"post-heading\"><a href=\"#结语\" class=\"headerlink\" title=\"结语\"></a>结语<a class=\"post-anchor\" href=\"#结语\" aria-hidden=\"true\"></a></h2>\n<p>下面是本文介绍的「一笔画」的线上 <a href=\"https://leeenx.github.io/OneStroke/src/onestroke.html\" target=\"_blank\" rel=\"external\">DEMO</a> 的二维码:</p>\n<p><img src=\"https://misc.aotu.io/leeenx/onestroke/2017-10-31-qr.png?v=2\" alt=\"demo\"></p>\n<p>游戏的源码托管在:<a href=\"https://github.com/leeenx/OneStroke\" target=\"_blank\" rel=\"external\">https://github.com/leeenx/OneStroke</a><br>其中游戏实现的主体代码在:<a href=\"https://github.com/leeenx/OneStroke/blob/master/src/script/onestroke.es6\" target=\"_blank\" rel=\"external\">https://github.com/leeenx/OneStroke/blob/master/src/script/onestroke.es6</a><br>自动识图的代码在:<a href=\"https://github.com/leeenx/OneStroke/blob/master/src/script/oneStrokePlugin.es6\" target=\"_blank\" rel=\"external\">https://github.com/leeenx/OneStroke/blob/master/src/script/oneStrokePlugin.es6</a></p>\n<p>感谢耐心阅读完本文章的读者。本文仅代表笔者的个人观点,如有不妥之处请不吝赐教。</p>\n<p>如果对「H5游戏开发」感兴趣,欢迎关注我们的<a href=\"https://zhuanlan.zhihu.com/snsgame\" target=\"_blank\" rel=\"external\">专栏</a>。</p>\n<style>.post-content sup a{vertical-align:unset}</style>\n<div class=\"post-tags\" style=\"display:none\">\n <a href=\"/tags/游戏/\">游戏</a> \n <a href=\"/tags/H5/\">H5</a> \n <a href=\"/tags/canvas/\">canvas</a> \n <a href=\"/tags/game/\">game</a> \n <a href=\"/tags/一笔画/\">一笔画</a>\n</div>\n<div class=\"post-categories\" style=\"display:none\">\n <a href=\"/cates/H5游戏开发/\">H5游戏开发</a>\n</div>\n<div class=\"post-announce\">\n 感谢您的阅读,本文由 \n <a href=\"//aotu.io\">凹凸实验室</a> 版权所有。如若转载,请注明出处:凹凸实验室(\n <a href=\"https://aotu.io/notes/2017/11/02/onestroke/\">https://aotu.io/notes/2017/11/02/onestroke/</a>)\n</div>\n<div class=\"post-revision\">\n <i class=\"fa fa-clock-o\"></i> \n <time class=\"post-updated\" datetime=\"2018-11-07T01:01:15.653Z\">上次更新:2018-11-07 09:01:15</time>\n</div>');
INSERT INTO `rumo_blog` VALUES ('19', '单屏页面响应式适配玩法', '1', '单屏页面响应式适配玩法', '1', '2018-11-11 21:02:32', '2018-11-11 21:02:32', '1', '0', 'https://img13.360buyimg.com/ling/jfs/t1/41/28/11924/322549/5bcc7032Eb10d58a1/4d2d427681ef624c.png', '', '0', '0', '<a id=\"more\"></a>\n<p>首先瞅一下效果图</p>\n<video autoplay preload=\"\" loop muted width=\"1000\">\n <br>\n <source src=\"https://storage.360buyimg.com/barrior-bucket/ling_animation.mp4\">\n <br>\n</video>\n<p>接着就是思考怎么做,我的想法如下图。</p>\n<p><img src=\"https://img20.360buyimg.com/ling/jfs/t25669/138/2279683884/36312/4ce2fb64/5bc72d72Nbc1df109.png\" alt=\"image\"></p>\n<p>把公共的 <code>页头</code> 、<code>页脚</code>、<code>导航栏</code>、<code>边框</code> 放到最顶层,比方说设置层级为 <code>999</code>,其他每个独立页则放在下面,然后切换页面的时候更新独立页的层级以达到效果图的效果(当然不能超过最顶层)。</p>\n<h3 id=\"适配\" class=\"post-heading\"><a href=\"#适配\" class=\"headerlink\" title=\"适配\"></a>适配<a class=\"post-anchor\" href=\"#适配\" aria-hidden=\"true\"></a></h3>\n<blockquote>\n <p>上面的方式已经把效果做出来了,接下来就是响应式适配了。</p>\n</blockquote>\n<h4 id=\"1、Mac-OS-Chrome\" class=\"post-heading\"><a href=\"#1、Mac-OS-Chrome\" class=\"headerlink\" title=\"1、Mac OS + Chrome\"></a>1、Mac OS + Chrome<a class=\"post-anchor\" href=\"#1、Mac-OS-Chrome\" aria-hidden=\"true\"></a></h4>\n<p>先考虑一下我自己的系统及显示器,</p>\n<p><code>MacBook Pro 1440 x 900</code> + <code>外设 hp 1920 x 1080</code></p>\n<p>也就是说 <code>Chrome</code> 的网页可视区高度大概为: <code>900(或1080) - 180 = 720px</code></p>\n<p>180 = 60 + 20 + 100<br></p>\n<figure class=\"highlight http\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"attribute\">60</span>: MAC 桌面程序坞动态尺寸,60 可能是我常用的尺寸吧,那就先这个\n </div>\n <div class=\"line\">\n <span class=\"attribute\">20</span>: MAC 桌面最顶部 icon 放置栏高度\n </div>\n <div class=\"line\">\n <span class=\"attribute\">100</span>: Chrome 标签页高度 + 地址栏高度 + 书签栏高度\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<h4 id=\"2、Windows-Chrome\" class=\"post-heading\"><a href=\"#2、Windows-Chrome\" class=\"headerlink\" title=\"2、Windows + Chrome\"></a>2、Windows + Chrome<a class=\"post-anchor\" href=\"#2、Windows-Chrome\" aria-hidden=\"true\"></a></h4>\n<p>然后我们再看看 <code>Windows + Chrome</code> 的情况,以 <code>1366 x 768</code> 为例,</p>\n<p><code>Chrome</code> 的网页可视区高度大概为 <code>768 - 150 = 618px</code></p>\n<p>150 = 40 + 110<br></p>\n<figure class=\"highlight http\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"attribute\">40</span>: Windows 桌面底部程序坞尺寸\n </div>\n <div class=\"line\">\n <span class=\"attribute\">110</span>: Chrome 标签页高度 + 地址栏高度 + 书签栏高度\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<h4 id=\"3、总结上面两点\" class=\"post-heading\"><a href=\"#3、总结上面两点\" class=\"headerlink\" title=\"3、总结上面两点\"></a>3、总结上面两点<a class=\"post-anchor\" href=\"#3、总结上面两点\" aria-hidden=\"true\"></a></h4>\n<ol>\n <li>以上两点的高度计算通过截图获得,可能会有些许误差。</li>\n <li>所以不管在哪种系统下,浏览器的宽度与分辨率是保持一致的(程序坞在底部的时候,程序坞在左右两边一般情况对宽度没有影响),高度则根据系统及浏览器的不同各有不同,比方说 <code>Safari</code> 没有书签高度。</li>\n <li>不同系统加浏览器占用的最高高度约为 180,最小约为 0(全屏的时候)</li>\n</ol>\n<h4 id=\"4、主流系统分辨率尺寸\" class=\"post-heading\"><a href=\"#4、主流系统分辨率尺寸\" class=\"headerlink\" title=\"4、主流系统分辨率尺寸\"></a>4、主流系统分辨率尺寸<a class=\"post-anchor\" href=\"#4、主流系统分辨率尺寸\" aria-hidden=\"true\"></a></h4>\n<p>然后我们看下当前主流系统及分辨率有哪些</p>\n<p>PC &amp; MAC &amp; Chrome<br></p>\n<figure class=\"highlight lsl\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n 常用\n </div>\n <div class=\"line\">\n <span class=\"number\">1280</span> x \n <span class=\"number\">800</span>\n </div>\n <div class=\"line\">\n <span class=\"number\">1366</span> x \n <span class=\"number\">1024</span> (IPad Pro)\n </div>\n <div class=\"line\">\n <span class=\"number\">1440</span> x \n <span class=\"number\">900</span>\n </div>\n <div class=\"line\">\n <span class=\"number\">1680</span> x \n <span class=\"number\">1050</span>\n </div>\n <div class=\"line\">\n <span class=\"number\">1600</span> x \n <span class=\"number\">900</span>\n </div>\n <div class=\"line\">\n <span class=\"number\">1920</span> x \n <span class=\"number\">1200</span>\n </div>\n <div class=\"line\">\n <span class=\"number\">2560</span> x \n <span class=\"number\">1440</span>\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n 更高忽略\n </div>\n <div class=\"line\">\n <span class=\"number\">2880</span> x \n <span class=\"number\">1620</span>\n </div>\n <div class=\"line\">\n <span class=\"number\">3200</span> x \n <span class=\"number\">1800</span>\n </div>\n <div class=\"line\">\n <span class=\"number\">5120</span> x \n <span class=\"number\">2880</span>\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<p>PC &amp; Windows &amp; Chrome (或 PC &amp; MAC &amp; Chrome &amp; 外设显示器)<br></p>\n<figure class=\"highlight basic\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"symbol\">1280 </span>x \n <span class=\"number\">720</span>/\n <span class=\"number\">1024</span>\n </div>\n <div class=\"line\">\n <span class=\"symbol\">1366 </span>x \n <span class=\"number\">768</span>\n </div>\n <div class=\"line\">\n <span class=\"symbol\">1440 </span>× \n <span class=\"number\">900</span>\n </div>\n <div class=\"line\">\n <span class=\"symbol\">1600 </span>x \n <span class=\"number\">900</span>\n </div>\n <div class=\"line\">\n <span class=\"symbol\">1920 </span>x \n <span class=\"number\">1080</span>\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<p>Mobile &amp; Android<br></p>\n<figure class=\"highlight lsl\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"number\">360</span> x \n <span class=\"number\">480</span>\n </div>\n <div class=\"line\">\n <span class=\"number\">412</span> x \n <span class=\"number\">732</span>\n </div>\n <div class=\"line\">\n 待补充\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<p>Mobile &amp; IOS<br></p>\n<figure class=\"highlight tap\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n IPhone 6: \n <span class=\"number\"> 375 </span>x 667\n </div>\n <div class=\"line\">\n IPhone\n <span class=\"number\"> 6 </span>Plus: \n <span class=\"number\"> 414 </span>x 736\n </div>\n <div class=\"line\">\n IPhone X: \n <span class=\"number\"> 375 </span>x 812\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<p>不上不下的 IPad:<br></p>\n<figure class=\"highlight basic\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"symbol\">768 </span>x \n <span class=\"number\">1024</span>\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<h4 id=\"5、分析\" class=\"post-heading\"><a href=\"#5、分析\" class=\"headerlink\" title=\"5、分析\"></a>5、分析<a class=\"post-anchor\" href=\"#5、分析\" aria-hidden=\"true\"></a></h4>\n<p>我们以宽度 <code>1024</code> 及以下算作移动端,以上算作 PC 端,所以两种选择</p>\n<ol>\n <li>移动端适配一个移动端页面,PC 端适配一个 PC 端页面。</li>\n <li>设计之初就想好一个页面适配两端,当然这个设计稿需要比较符合适配两端的条件。</li>\n</ol>\n<h4 id=\"6、别人适配是怎么做的?\" class=\"post-heading\"><a href=\"#6、别人适配是怎么做的?\" class=\"headerlink\" title=\"6、别人适配是怎么做的?\"></a>6、别人适配是怎么做的?<a class=\"post-anchor\" href=\"#6、别人适配是怎么做的?\" aria-hidden=\"true\"></a></h4>\n<p>贴几个录制的视频~</p>\n<video autoplay preload=\"\" loop muted width=\"1000\">\n <br>\n <source src=\"https://storage.360buyimg.com/barrior-bucket/qqbrowser.mp4\">\n <br>\n</video>\n<video autoplay preload=\"\" loop muted width=\"1000\">\n <br>\n <source src=\"https://storage.360buyimg.com/barrior-bucket/uc.mp4\">\n <br>\n</video>\n<p>所以,单屏页面最好页面内容言简意赅,设计层面倾向于水平垂直都居中的情况,是最适合做好这个页面的,并且在各种尺寸变化的情况下能比较良好地展示UI,且开发成本也比较合理。</p>\n<h4 id=\"7、自身情况及实现\" class=\"post-heading\"><a href=\"#7、自身情况及实现\" class=\"headerlink\" title=\"7、自身情况及实现\"></a>7、自身情况及实现<a class=\"post-anchor\" href=\"#7、自身情况及实现\" aria-hidden=\"true\"></a></h4>\n<p>我们是分两个页面做的,先看一下 PC 端设计稿</p>\n<p><img width=\"600\" src=\"https://img12.360buyimg.com/ling/jfs/t26869/116/1204025888/170822/21c9021a/5bc739bbN85aad796.png\"></p>\n<blockquote>\n <p>结合动画的展现形式,其实并不是很理想做响应式,但还是要适配。</p>\n</blockquote>\n<p>本来想用 <code>rem</code> 做适配的,但是 <code>rem</code> 需要些写很多个匹配,即下面的代码<br></p>\n<figure class=\"highlight stylus\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n @media all and (\n <span class=\"attribute\">max-width</span>: \n <span class=\"number\">1024px</span>) {\n </div>\n <div class=\"line\"> \n <span class=\"selector-tag\">html</span>, \n <span class=\"selector-tag\">body</span> {\n </div>\n <div class=\"line\"> \n <span class=\"attribute\">font-size</span>: \n <span class=\"number\">10px</span>;\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n @media all and (\n <span class=\"attribute\">max-width</span>: \n <span class=\"number\">1366px</span>) {\n </div>\n <div class=\"line\"> \n <span class=\"selector-tag\">html</span>, \n <span class=\"selector-tag\">body</span> {\n </div>\n <div class=\"line\"> \n <span class=\"attribute\">font-size</span>: \n <span class=\"number\">12px</span>;\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"comment\">// 1680 1920 2560 等</span>\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<p>然后有个问题就是,<code>@media</code> 是根据 <code>width</code> 的变化来匹配的,完全按照桌面分辨率来显示是没问题的,不过高度随便调节一下(变小),而宽度还是很宽,这时候页面底部的部分文本就会溢出被隐藏掉。</p>\n<p>我们不需要考虑更低端的浏览器,所以可以使用比较前沿的特性,如 <code>pointer-events</code> 等特性。</p>\n<p>所以使用 <code>vh</code> 做适配方案,<code>vh</code> 是什么单位详情可以看下<a href=\"https://aotu.io/notes/2017/04/28/2017-4-28-CSS-viewport-units/index.html\">这篇文章</a>,这里做个简单介绍。</p>\n<figure class=\"highlight avrasm\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"symbol\">vw:</span> 相对于浏览器可视区的宽度 \n <span class=\"number\">1</span>vw = 浏览器可视区宽度的 \n <span class=\"number\">1</span>%\n </div>\n <div class=\"line\">\n <span class=\"symbol\">vh:</span> 相对于浏览器可视区的高度 \n <span class=\"number\">1</span>vh = 浏览器可视区高度的 \n <span class=\"number\">1</span>%\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>也就是说 <code>100vh</code> 实际上等于浏览器可视区的高度,所以 <code>px</code> 与 <code>vh</code> 的换算我们举个例子说明一下(一个很简单的数学换算)。假设浏览器可视区高度为 <code>720px</code>,某个元素的宽度为 <code>300px</code>,那应该写成多少 <code>vh</code> 才与 <code>300px</code> 相等呢,如下。<br></p>\n<figure class=\"highlight basic\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"symbol\">300 </span>÷ (\n <span class=\"number\">720</span> ÷ \n <span class=\"number\">100</span>) ≈ \n <span class=\"number\">41.666</span>\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<p>比如设计稿为 <code>1920x1080</code>(单屏设计高度应该更小一点,如适配第一节所说),可以写个 <code>CSS</code> 预处理函数,这样方便直接使用设计稿的尺寸,以 <code>Sass</code> 为例如下。</p>\n<figure class=\"highlight less\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"variable\">@function</span> vh( $value ) {\n </div>\n <div class=\"line\"> \n <span class=\"variable\">@return</span> ( $value / \n <span class=\"number\">1080</span> / \n <span class=\"number\">100</span> ) + vh;\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n 或者\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"variable\">@function</span> vw( $value ) {\n </div>\n <div class=\"line\"> \n <span class=\"variable\">@return</span> ( $value / \n <span class=\"number\">1920</span> / \n <span class=\"number\">100</span> ) + vw;\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>然后,<code>300px</code> 可以无缝写成 <code>vh(300)</code> 或 <code>vw(300)</code>。</p>\n<p>so… 对于我们的页面选择 <code>vh</code> 一举两得,不用写很多 <code>rem</code> 匹配,也不会出现溢出的问题。</p>\n<p>因为高度变矮,内容的尺寸会随之变小,而页面是 <code>1190</code> 宽,水平居中布局,所以当只改变浏览器宽度的情况下,不会出现宽度变化溢出问题(除非分辨率超大,然后高度居很高,只把宽度缩很小的情况,这个下面会说到)。写完后在上面列举的主流分辨率下一一测试通过。</p>\n<p>看看效果(当然这个是最终效果,只改变宽度的拉伸适配在最后会说):</p>\n<video autoplay preload=\"\" loop muted width=\"1000\">\n <br>\n <source src=\"https://storage.360buyimg.com/barrior-bucket/ling.mp4\">\n <br>\n</video>\n<h4 id=\"8、特殊场景\" class=\"post-heading\"><a href=\"#8、特殊场景\" class=\"headerlink\" title=\"8、特殊场景\"></a>8、特殊场景<a class=\"post-anchor\" href=\"#8、特殊场景\" aria-hidden=\"true\"></a></h4>\n<p>这里就是刚刚说到的 <code>分辨率超大,然后高度居很高,只把宽度缩很小的情况</code>,因为设计稿是长宽比例为横向矩形,所以明显与用长宽比为竖向的矩形来看页面是背道而驰的。</p>\n<p><img src=\"https://img13.360buyimg.com/ling/jfs/t1/8536/38/1609/18493/5bcf1e4cEc61106f8/50f6e7c5b8b9d139.jpg\" alt=\"view\"></p>\n<p>委屈委屈,但还是要兼容下,至少看起来要显示正常。</p>\n<h5 id=\"8-1、尝试-rem-vh-方案\" class=\"post-heading\"><a href=\"#8-1、尝试-rem-vh-方案\" class=\"headerlink\" title=\"8.1、尝试 rem + vh 方案\"></a>8.1、尝试 <code>rem + vh</code> 方案<a class=\"post-anchor\" href=\"#8-1、尝试-rem-vh-方案\" aria-hidden=\"true\"></a></h5>\n<p>一开始想的是 <code>rem + vh</code> 结合使用,根元素 <code>html</code> 使用 <code>vh</code>,其他单位则使用 <code>rem</code>,然后找到有问题的宽高比,通过 <code>@media</code> 方式设置 <code>html</code> 为 <code>vw</code> 来达到适配。</p>\n<p>事实是,<code>rem</code> 缩小到一定值就不会再缩小了,这个跟浏览器对字体大小限制为最小 <code>12px</code> 一样,看个例子。</p>\n<p><img src=\"https://img10.360buyimg.com/ling/jfs/t1/7261/10/957/110108/5bcc5169Eb6dbe8be/d220d56b6fe45501.jpg\" alt=\"rem min\"></p>\n<p>根字体小于 <code>12px</code> 以后,<code>rem</code> 对应的值则都是设置的倍数乘以 <code>12</code>;设置根字体为 <code>vh, vw</code> 单位同理,<code>rem</code> 会在 <code>vh, vw</code> 换算达到 <code>12</code> 以后就不再改变。</p>\n<blockquote>\n <p>PPPS: 是不是有点坑,应该字体的属性最小值为 12,而其他属性的值没有控制才对</p>\n</blockquote>\n<p>所以,如果使用 <code>rem + vh</code> 方案,在界面缩小到一定尺寸后继续缩小,有些值达到最小值固定不变,而有些值仍在变小,UI 的展示就变得混乱。</p>\n<h5 id=\"8-2、落地方案,vh-vw-JavaScript-计算\" class=\"post-heading\"><a href=\"#8-2、落地方案,vh-vw-JavaScript-计算\" class=\"headerlink\" title=\"8.2、落地方案,vh + vw + JavaScript 计算\"></a>8.2、落地方案,<code>vh + vw + JavaScript 计算</code><a class=\"post-anchor\" href=\"#8-2、落地方案,vh-vw-JavaScript-计算\" aria-hidden=\"true\"></a></h5>\n<p>而直接在元素的属性值上设置为 <code>vh 或 vw</code>,所有的值都会实时变动,没有最小值(除了属性为字体有最小值),这样就最大程度减少 UI 变乱的情况了,除非缩到很小很小,那就…(此处省略 1000 个字)。</p>\n<p>于是乎,现在的想法是</p>\n<ol>\n <li>在原来以 <code>vh</code> 为基础的情况下,拷贝所有带 <code>vh</code> 单位的代码,把 <code>vh</code> 换成 <code>vw</code>,当然这些改动都在一个比如叫 <code>.vw-mode</code> 的类下面,基本上可以无缝迁移,只需替换 <code>vh</code> 函数名即可。</li>\n <li>把 <code>.vw-mode</code> 下的内容设置为上下居中。</li>\n <li>通过 <code>JS</code> 计算,当可视区比例为竖向比例时,则在顶层元素加上 <code>.vw-mode</code> 类名,当比例为横向比例时,则去掉 <code>.vw-mode</code> 类名。</li>\n</ol>\n<p>大致的代码如下</p>\n<p>CSS<br></p>\n<figure class=\"highlight stylus\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"selector-class\">.homepage</span>\n <span class=\"selector-class\">.vw-mode</span> {\n </div>\n <div class=\"line\"> \n <span class=\"attribute\">font-size</span>: vw(\n <span class=\"number\">14</span>);\n </div>\n <div class=\"line\"> \n <span class=\"selector-class\">.com-width</span> {\n </div>\n <div class=\"line\"> \n <span class=\"attribute\">width</span>: vw(\n <span class=\"number\">1190</span>);\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"> \n <span class=\"selector-class\">.hp-header</span> {\n </div>\n <div class=\"line\"> \n <span class=\"attribute\">padding-top</span>: vw(\n <span class=\"number\">30</span>);\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// ...更多代码</span>\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// ...更多代码</span>\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<p>JS<br></p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">this</span>.resizeHandler = \n <span class=\"function\"><span class=\"params\">()</span> =&gt;</span> {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">const</span> clientWidth = \n <span class=\"built_in\">document</span>.documentElement.clientWidth\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">const</span> clientHeight = \n <span class=\"built_in\">document</span>.documentElement.clientHeight\n </div>\n <div class=\"line\"></div>\n <div class=\"line\"> \n <span class=\"comment\">// 当长宽比为竖向比例时</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">const</span> isVerticalRatio = clientWidth / clientHeight &lt; \n <span class=\"number\">1370</span> / \n <span class=\"number\">890</span>\n </div>\n <div class=\"line\">\n $homepageElem.classList[isVerticalRatio ? \n <span class=\"string\">\'add\'</span> : \n <span class=\"string\">\'remove\'</span>](\n <span class=\"string\">\'vw-mode\'</span>)\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n <span class=\"keyword\">this</span>.resizeHandler()\n </div>\n <div class=\"line\">\n <span class=\"built_in\">window</span>.addEventListener(\n <span class=\"string\">\'resize\'</span>, \n <span class=\"keyword\">this</span>.resizeHandler)\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<p>最后的结果就是上面那个 <code>GIF</code> 效果图了。</p>\n<h4 id=\"9、移动端\" class=\"post-heading\"><a href=\"#9、移动端\" class=\"headerlink\" title=\"9、移动端\"></a>9、移动端<a class=\"post-anchor\" href=\"#9、移动端\" aria-hidden=\"true\"></a></h4>\n<p>移动端用户是没法操作浏览器的,所以基本上都是标准的长宽比,用 <code>vh</code> 最合适不过了,或 <code>vw</code>。</p>\n<h4 id=\"10、最后\" class=\"post-heading\"><a href=\"#10、最后\" class=\"headerlink\" title=\"10、最后\"></a>10、最后<a class=\"post-anchor\" href=\"#10、最后\" aria-hidden=\"true\"></a></h4>\n<p>体验(官网):<a href=\"https://ling.jd.com/\" target=\"_blank\" rel=\"external\">https://ling.jd.com</a></p>\n<p>体验浏览器:Chrome、Safari 新版,其他浏览器暂不支持</p>\n<div class=\"post-tags\" style=\"display:none\">\n <a href=\"/tags/单屏页面响应式/\">单屏页面响应式</a> \n <a href=\"/tags/响应式适配/\">响应式适配</a>\n</div>\n<div class=\"post-categories\" style=\"display:none\">\n <a href=\"/cates/项目总结/\">项目总结</a>\n</div>\n<div class=\"post-announce\">\n 感谢您的阅读,本文由 \n <a href=\"//aotu.io\">凹凸实验室</a> 版权所有。如若转载,请注明出处:凹凸实验室(\n <a href=\"https://aotu.io/notes/2018/10/22/Responsive-single-screen-design/\">https://aotu.io/notes/2018/10/22/Responsive-single-screen-design/</a>)\n</div>\n<div class=\"post-revision\">\n <i class=\"fa fa-clock-o\"></i> \n <time class=\"post-updated\" datetime=\"2018-11-07T01:01:15.657Z\">上次更新:2018-11-07 09:01:15</time>\n</div>');
INSERT INTO `rumo_blog` VALUES ('20', '波动均分算法', '1', '波动均分算法', '1', '2018-11-11 21:02:32', '2018-11-11 21:02:32', '1', '0', 'https://misc.aotu.io/leeenx/waveAverage/cover.jpg', '', '0', '0', '<p>「波动」和「均分」大部分读者朋友是知道的,但看到「波动均分」应该是一头雾水的。其实,这个名词是笔者拼凑出来的。</p>\n<p><strong>什么是「波动均分」?</strong></p>\n<p>把指定的数值 A,分成 N 份,此时每份的数值在一个固定的区间 [max, min] 内。 从视觉上看,每份的数量在平均线上下波动,并带有随机性:</p>\n<p><img src=\"https://misc.aotu.io/leeenx/waveAverage/2017-12-06-waveAverage.gif\" alt=\"正余弦波线图\"></p>\n<p>这种分配不是严格意义上的「均分」,但它却跟「均分」很相似,按笔者的理解给这个算法取个名字 —— 「波动均分」。</p>\n<p><strong>波动均分算法应该具备的特征如下:</strong></p>\n<ul>\n <li>分配数量</li>\n <li>波峰高度</li>\n <li>波谷深度</li>\n <li>随机分配</li>\n <li>组合全面</li>\n</ul>\n<p>前三个特征是提供对外配置的接口,保证算法的使用者可以指定分配的数量和定制波动的波峰和波谷(尽管大部分情况下,波峰 = 波谷);「随机分配」表示算法的结果是随机的;<br>「 组合全面」表示算法的结果是可以覆盖所有可能的结果。</p>\n<p>接下来,笔者将介绍两种实现「波动均分」的算法:</p>\n<ul>\n <li>穷举法</li>\n <li>快速分配</li>\n</ul>\n<p><strong>备注:本文算法中使用到的平均值是0</strong></p>\n<h2 id=\"穷举法\" class=\"post-heading\"><a href=\"#穷举法\" class=\"headerlink\" title=\"穷举法\"></a>穷举法<a class=\"post-anchor\" href=\"#穷举法\" aria-hidden=\"true\"></a></h2>\n<p>「穷举法」顾名思义就是列举所有可能出现的组合,再随机抽取一个组合作为输出结果。</p>\n<p>下面是一个「波动均分」任务:</p>\n<blockquote>\n <p>有一张 10x10 的表格,需要对格子上5种颜色并要求每种颜色的数量在区间 [18, 22] 内。</p>\n</blockquote>\n<p>由上述可得:每种颜色都会有5种分配结果(18, 19, 20, 21, 22)。穷举这些颜色分配数量的组合其实就是建设一棵高度为 6 的 5 叉树的过程。</p>\n<p><img src=\"https://misc.aotu.io/leeenx/waveAverage/20180110_tree.gif?v=5\" alt=\"5叉树\"></p>\n<p>第 6 层的叶子数就是「所有可能出现的组合」的总数。换而言之,从树的第六层的一片叶子到第二层节点的路径即是一种分配组合。</p>\n<p>以下是「穷举法」的代码实现:<br></p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div>\n <div class=\"line\">\n 16\n </div>\n <div class=\"line\">\n 17\n </div>\n <div class=\"line\">\n 18\n </div>\n <div class=\"line\">\n 19\n </div>\n <div class=\"line\">\n 20\n </div>\n <div class=\"line\">\n 21\n </div>\n <div class=\"line\">\n 22\n </div>\n <div class=\"line\">\n 23\n </div>\n <div class=\"line\">\n 24\n </div>\n <div class=\"line\">\n 25\n </div>\n <div class=\"line\">\n 26\n </div>\n <div class=\"line\">\n 27\n </div>\n <div class=\"line\">\n 28\n </div>\n <div class=\"line\">\n 29\n </div>\n <div class=\"line\">\n 30\n </div>\n <div class=\"line\">\n 31\n </div>\n <div class=\"line\">\n 32\n </div>\n <div class=\"line\">\n 33\n </div>\n <div class=\"line\">\n 34\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"function\"><span class=\"keyword\">function</span> <span class=\"title\">exhaustWave</span>(<span class=\"params\">n = <span class=\"number\">5</span>, crest = <span class=\"number\">4</span>, trough = <span class=\"number\">4</span></span>) </span>{ \n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> root = {\n <span class=\"attr\">parent</span>: \n <span class=\"literal\">null</span>, \n <span class=\"attr\">count</span>: \n <span class=\"literal\">null</span>, \n <span class=\"attr\">subtotal</span>: \n <span class=\"number\">0</span>}; \n <span class=\"comment\">// 根节点</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> leaves = [root]; \n <span class=\"comment\">// 叶子(数组)</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> level = \n <span class=\"number\">0</span>; \n <span class=\"comment\">// 层数 </span>\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 检查当前组合是否合法</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> isOK = \n <span class=\"function\"><span class=\"params\">subtotal</span> =&gt;</span> {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span>(level &lt; n - \n <span class=\"number\">1</span>) {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span>(-subtotal &lt;= (n - level) * crest || subtotal &lt;= (n - level) * trough) \n <span class=\"keyword\">return</span> \n <span class=\"literal\">true</span>; \n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">else</span> \n <span class=\"keyword\">if</span>(subtotal === \n <span class=\"number\">0</span>) \n <span class=\"keyword\">return</span> \n <span class=\"literal\">true</span>; \n </div>\n <div class=\"line\"> \n <span class=\"keyword\">else</span> \n <span class=\"keyword\">return</span> \n <span class=\"literal\">false</span>; \n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 生成组合树 </span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">while</span>(level &lt; n) { \n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> newLeaves = []; \n <span class=\"comment\">// 存储最新叶子</span>\n </div>\n <div class=\"line\">\n leaves.forEach(\n <span class=\"function\"><span class=\"params\">node</span> =&gt;</span> {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">for</span>(\n <span class=\"keyword\">let</span> count = -trough; count &lt;= crest; ++count) {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> subtotal = node.subtotal + count; \n </div>\n <div class=\"line\">\n isOK(subtotal) &amp;&amp; newLeaves.push(\n </div>\n <div class=\"line\">\n {\n <span class=\"attr\">parent</span>: node, \n <span class=\"attr\">count</span>: count, \n <span class=\"attr\">subtotal</span>: subtotal}\n </div>\n <div class=\"line\">\n ); \n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }); \n </div>\n <div class=\"line\">\n leaves = newLeaves, ++level; \n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 随机取一片叶子</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> leaf = leaves[\n <span class=\"built_in\">Math</span>.random() * leaves.length &gt;&gt; \n <span class=\"number\">0</span>]; \n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> group = [leaf.count]; \n </div>\n <div class=\"line\"> \n <span class=\"keyword\">for</span>(\n <span class=\"keyword\">let</span> i = \n <span class=\"number\">0</span>; i &lt; \n <span class=\"number\">4</span>; ++i) { \n </div>\n <div class=\"line\">\n leaf = leaf.parent; \n </div>\n <div class=\"line\">\n group.push(leaf.count); \n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">return</span> group; \n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<p><strong>穷举法的局限:</strong></p>\n<ol>\n <li>「无穷集合」不适用</li>\n <li>穷举算法效率低下</li>\n</ol>\n<p>由于「穷举法」的这两个致命限制,所以它不是适用于业务。事实上,笔者主要是使用「穷举法」校验「快速分配」方案的全面性。</p>\n<h2 id=\"快速分配\" class=\"post-heading\"><a href=\"#快速分配\" class=\"headerlink\" title=\"快速分配\"></a>快速分配<a class=\"post-anchor\" href=\"#快速分配\" aria-hidden=\"true\"></a></h2>\n<p>「快速分配」方案的思路:</p>\n<ol>\n <li>获取可分配波动范围;</li>\n <li>在波动范围内随机取值</li>\n</ol>\n<p>代码的实现过程如下:<br></p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div>\n <div class=\"line\">\n 16\n </div>\n <div class=\"line\">\n 17\n </div>\n <div class=\"line\">\n 18\n </div>\n <div class=\"line\">\n 19\n </div>\n <div class=\"line\">\n 20\n </div>\n <div class=\"line\">\n 21\n </div>\n <div class=\"line\">\n 22\n </div>\n <div class=\"line\">\n 23\n </div>\n <div class=\"line\">\n 24\n </div>\n <div class=\"line\">\n 25\n </div>\n <div class=\"line\">\n 26\n </div>\n <div class=\"line\">\n 27\n </div>\n <div class=\"line\">\n 28\n </div>\n <div class=\"line\">\n 29\n </div>\n <div class=\"line\">\n 30\n </div>\n <div class=\"line\">\n 31\n </div>\n <div class=\"line\">\n 32\n </div>\n <div class=\"line\">\n 33\n </div>\n <div class=\"line\">\n 34\n </div>\n <div class=\"line\">\n 35\n </div>\n <div class=\"line\">\n 36\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"function\"><span class=\"keyword\">function</span> <span class=\"title\">quickWave</span>(<span class=\"params\">n = <span class=\"number\">5</span>, crest = <span class=\"number\">4</span>, trough = <span class=\"number\">4</span>, isInteger = true</span>) </span>{ \n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> list = []; \n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 无法进行波动均分,直接返回完全平分</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span>(crest &gt; (n - \n <span class=\"number\">1</span>) * trough || trough &gt; (n - \n <span class=\"number\">1</span>) * crest) {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">return</span> \n <span class=\"keyword\">new</span> \n <span class=\"built_in\">Array</span>(n).fill(\n <span class=\"number\">0</span>); \n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> base = \n <span class=\"number\">0</span>; \n <span class=\"comment\">// 最少需要消除的高度</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> wave = \n <span class=\"number\">0</span>; \n <span class=\"comment\">// 波动量</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> high = crest; \n <span class=\"comment\">// 高位</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> low = -trough; \n <span class=\"comment\">// 低位</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> sum = \n <span class=\"number\">0</span>; \n <span class=\"comment\">// 累计量 </span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> count = n; \n <span class=\"comment\">// 剩余数量 </span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">while</span>(--count &gt;= \n <span class=\"number\">0</span>) { \n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 动态当前的波动量</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span>(crest &gt; count * trough - sum) {\n </div>\n <div class=\"line\">\n high = count * trough - sum; \n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span>(trough &gt; count * crest + sum) {\n </div>\n <div class=\"line\">\n low = -sum - count * crest; \n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n base = low; \n </div>\n <div class=\"line\">\n wave = high - low; \n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> rnd; \n <span class=\"comment\">// 随机波动量 </span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span>(count &gt; \n <span class=\"number\">0</span>) {\n </div>\n <div class=\"line\">\n rnd = base + \n <span class=\"built_in\">Math</span>.random() * (wave + \n <span class=\"number\">1</span>); \n <span class=\"comment\">// 随机波动</span>\n </div>\n <div class=\"line\">\n } \n <span class=\"keyword\">else</span> {\n </div>\n <div class=\"line\">\n rnd = -sum; \n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span>(isInteger === \n <span class=\"literal\">true</span>) {\n </div>\n <div class=\"line\">\n rnd = \n <span class=\"built_in\">Math</span>.floor(rnd); \n </div>\n <div class=\"line\">\n } \n </div>\n <div class=\"line\">\n sum += rnd; \n </div>\n <div class=\"line\">\n list.push(rnd); \n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">return</span> list; \n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<p>波动均分的「快速分配」方案在算法效率上是高效的,并且「快速分配」适用于「无穷集合」。</p>\n<p><strong>如何使用「穷举法」校验「快速分配」的全面性?</strong><br>「穷举法」能直接返回分配组合的总数,而「快速分配」只能随机返回一次组合,笔者是通过大数量地调用「快速分配」算法并累积不重复组合来验证「快速分配」的全面性。代码如下:</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"built_in\">console</span>.log(exhaustWave(\n <span class=\"number\">5</span>, \n <span class=\"number\">4</span>, \n <span class=\"number\">4</span>)); \n <span class=\"comment\">// 组合总数: 3951</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">let</span> res = {}, count = \n <span class=\"number\">0</span>, len = \n <span class=\"number\">10000</span>; \n </div>\n <div class=\"line\">\n <span class=\"keyword\">for</span>(\n <span class=\"keyword\">let</span> i = \n <span class=\"number\">0</span>; i &lt; len; ++i) { \n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> name = quickWave(\n <span class=\"number\">5</span>, \n <span class=\"number\">4</span>, \n <span class=\"number\">4</span>).join(\n <span class=\"string\">\"_\"</span>); \n </div>\n <div class=\"line\">\n res[name] !== \n <span class=\"literal\">true</span> &amp;&amp; (res[name] = \n <span class=\"literal\">true</span>, ++count); \n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n <span class=\"built_in\">console</span>.log(count); \n <span class=\"comment\">// len次快速分配后的组合总数</span>\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>通过调整变量 <code>len</code> 可以发现,当 <code>len</code> 越来越大输出的结果就越逼近 3951,当到达一定量级后,输出的结果就是 3951。</p>\n<h2 id=\"结语\" class=\"post-heading\"><a href=\"#结语\" class=\"headerlink\" title=\"结语\"></a>结语<a class=\"post-anchor\" href=\"#结语\" aria-hidden=\"true\"></a></h2>\n<p>可能网上有类似的算法存在,不过笔者学识太浅没有找到对应的算法,所以自己生造了这个算法,如果有何不妥之处欢迎指正。</p>\n<p>如果对「H5游戏开发」感兴趣,欢迎关注我们的<a href=\"https://zhuanlan.zhihu.com/snsgame\" target=\"_blank\" rel=\"external\">专栏</a>。</p>\n<div class=\"post-tags\" style=\"display:none\">\n <a href=\"/tags/算法/\">算法</a> \n <a href=\"/tags/javascript/\">javascript</a> \n <a href=\"/tags/均分/\">均分</a> \n <a href=\"/tags/average/\">average</a>\n</div>\n<div class=\"post-categories\" style=\"display:none\">\n <a href=\"/cates/Web开发/\">Web开发</a>\n</div>\n<div class=\"post-announce\">\n 感谢您的阅读,本文由 \n <a href=\"//aotu.io\">凹凸实验室</a> 版权所有。如若转载,请注明出处:凹凸实验室(\n <a href=\"https://aotu.io/notes/2018/01/11/waveaverage/\">https://aotu.io/notes/2018/01/11/waveaverage/</a>)\n</div>\n<div class=\"post-revision\">\n <i class=\"fa fa-clock-o\"></i> \n <time class=\"post-updated\" datetime=\"2018-11-07T01:01:15.657Z\">上次更新:2018-11-07 09:01:15</time>\n</div>');
INSERT INTO `rumo_blog` VALUES ('21', 'GraphQL 使用介绍', '1', 'GraphQL 使用介绍', '1', '2018-11-11 21:02:32', '2018-11-11 21:02:32', '1', '0', '//misc.aotu.io/booxood/graphql-use/cover_900x500.jpg', '', '0', '0', '<a id=\"more\"></a>\n<p><a href=\"http://graphql.org/\" target=\"_blank\" rel=\"external\">GraphQL</a> 是 Fackbook 的一个开源项目,它定义了一种查询语言,用于描述客户端与服务端交互时的数据模型和功能,相比 <code>RESTful API</code> 主要有以下特点:</p>\n<ul>\n <li>根据需要返回数据</li>\n <li>一个请求获取多个资源</li>\n <li>提供内省系统</li>\n</ul>\n<p>这使得客户端的功能得到了加强,特别是在查询数据方面。</p>\n<p>下面我们从使用的角度来介绍一下。</p>\n<h3 id=\"相关概念\" class=\"post-heading\"><a href=\"#相关概念\" class=\"headerlink\" title=\"相关概念\"></a>相关概念<a class=\"post-anchor\" href=\"#相关概念\" aria-hidden=\"true\"></a></h3>\n<p>在使用 GraphQL 之前,先介绍几个相关概念,便于理解使用。</p>\n<ul>\n <li><strong>Operations</strong></li>\n</ul>\n<p>GraphQL 服务提供的操作一般有:<code>query</code>、<code>mutation</code>。<code>query</code> 可以理解为 <code>RESTful API</code> 中的 <code>GET</code> 的请求。<code>mutation</code> 可以理解为 <code>RESTful API</code> 中的 <code>POST</code>、<code>PUT</code>、<code>DELETE</code> 之类的请求。</p>\n<ul>\n <li><strong>Types</strong></li>\n</ul>\n<p>定义了 GraphQL 服务支持的类型,例如:</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n type User {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">id</span>: ID\n </div>\n <div class=\"line\">\n name: \n <span class=\"built_in\">String</span>\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n type Query {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">user</span>: User\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>定义了 <code>User</code> 类型和包含的字段以及字段的类型;定义 <code>Query</code> 返回一个 <code>User</code> 类型的 <code>user</code>,<code>Query</code> 也是一种类型。</p>\n<ul>\n <li><strong>Scalar types</strong></li>\n</ul>\n<p>标量类型。GraphQL 默认提供的标量类型有:Int、Float、String、Boolean、ID,也可以实现自定义的标量类型,如:Date。</p>\n<p>标量类型有什么用呢?<strong>返回数据的字段必须是标量类型</strong>。例如我们想返回一个 <code>user</code>:</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n query {\n </div>\n <div class=\"line\">\n user \n <span class=\"comment\">// 报错</span>\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>上面这样是会报错的,因为 <code>user</code> 不是标量类型,需要改成</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n query {\n </div>\n <div class=\"line\">\n user {\n </div>\n <div class=\"line\">\n id\n </div>\n <div class=\"line\">\n name\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>指定返回 <code>user</code> 的 id 和 name,这两个字段都是标量类型,就可以正确返回了。</p>\n<h3 id=\"开始使用\" class=\"post-heading\"><a href=\"#开始使用\" class=\"headerlink\" title=\"开始使用\"></a>开始使用<a class=\"post-anchor\" href=\"#开始使用\" aria-hidden=\"true\"></a></h3>\n<p>如果看完上面的介绍,心中有很多疑问,没关系,我们现在以 <code>GitHub GraphQL API</code> 为例,来实际使用一下。打开 <code>https://developer.github.com/v4/explorer/</code>,然后登录,会看到一个这样的界面</p>\n<p><img src=\"https://misc.aotu.io/booxood/graphql-use/github.png\" alt=\"github\"></p>\n<p>这是 GraphQL 提供的开发工具 <code>GraphiQL</code>,可以检查 GraphQL 的语法,发送 GraphQL 的请求,还提供文档查询功能。在开始使用之前先介绍一下文档查询功能。点击右上角的 <code>&lt; Docs</code> 并可以看到</p>\n<p><img src=\"https://misc.aotu.io/booxood/graphql-use/github-doc.png\" alt=\"github-doc\"></p>\n<p>上面的 <code>ROOT TYPES</code> 表示最顶层支持的类型,只有两个 <code>Query</code> 和 <code>Mutation</code>。点击 <code>Query</code>,可以看到该类型包含的字段。仔细看,会发现这些字段的值又都是类型。</p>\n<div style=\"margin:0 auto;width:50%\">\n <br>\n <img src=\"https://misc.aotu.io/booxood/graphql-use/github-fields.png\" alt=\"github-fields\">\n <br>\n</div>\n<p>往下滚动,找到 <code>user(login: String!): User</code>,点击 <code>User</code></p>\n<div style=\"margin:0 auto;width:50%\">\n <br>\n <img src=\"https://misc.aotu.io/booxood/graphql-use/github-user.png\" alt=\"github-user\">\n <br>\n</div>\n<p>终于找到一个标量类型的字段 <code>bio: String</code>,按照之前说法,我们是可以查询这个字段,写出如下的查询语言:</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n {\n </div>\n <div class=\"line\">\n user {\n </div>\n <div class=\"line\">\n bio\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>准备执行时,会看到 <code>user</code> 下方有条红线,鼠标放上去</p>\n<p><img src=\"https://misc.aotu.io/booxood/graphql-use/github-alert.png\" alt=\"github-alert\"></p>\n<p>提示 <code>user</code> 必须指定一个 <code>login</code> 的参数,再回头看文档中该字段的描述 <code>user(login: String!): User</code>,是不是就可以理解了,<code>(login: )</code> 表示该字段接受一个 <code>login</code> 参数,为 <code>String</code> 类型,<code>!</code> 表示是必须的。</p>\n<p>将查询语言改成:</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n {\n </div>\n <div class=\"line\">\n user(login: \n <span class=\"string\">\"booxood\"</span>) {\n </div>\n <div class=\"line\">\n bio\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>再执行,并得到了我们预期指定的结果</p>\n<figure class=\"highlight json\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">\"data\"</span>: {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">\"user\"</span>: {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">\"bio\"</span>: \n <span class=\"string\">\"Happy coding &amp; Happy life\"</span>\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>现在是不是有点理解这种查询语言了。下面我们再以【 <a href=\"https://gitalk.github.io/\" target=\"_blank\" rel=\"external\">Gitalk</a>:一个基于 Github Issue 和 Preact 开发的评论插件】中的两个需求为例</p>\n<ul>\n <li><strong>展示某个 Issue 的评论和评论上的点赞数据</strong></li>\n</ul>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div>\n <div class=\"line\">\n 16\n </div>\n <div class=\"line\">\n 17\n </div>\n <div class=\"line\">\n 18\n </div>\n <div class=\"line\">\n 19\n </div>\n <div class=\"line\">\n 20\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n query {\n </div>\n <div class=\"line\">\n repository(owner: \n <span class=\"string\">\"gitalk\"</span>, \n <span class=\"attr\">name</span>: \n <span class=\"string\">\"gitalk\"</span>) {\n </div>\n <div class=\"line\">\n issue(number: \n <span class=\"number\">1</span>) {\n </div>\n <div class=\"line\">\n comments(last: \n <span class=\"number\">10</span>) {\n </div>\n <div class=\"line\">\n totalCount\n </div>\n <div class=\"line\">\n nodes {\n </div>\n <div class=\"line\">\n author {\n </div>\n <div class=\"line\">\n login\n </div>\n <div class=\"line\">\n avatarUrl\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n body\n </div>\n <div class=\"line\">\n reactions(first: \n <span class=\"number\">100</span>, \n <span class=\"attr\">content</span>: HEART) {\n </div>\n <div class=\"line\">\n totalCount\n </div>\n <div class=\"line\">\n viewerHasReacted\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>先通过 <code>repository(owner: \"gitalk\", name: \"gitalk\")</code> 找到 <code>repository</code>,再通过 <code>issue(number: 1)</code> 指定 <code>issue</code>,然后 <code>comments(last: 10)</code> 表示从后面取 10 条 <code>comments</code>,同时获取评论的 <code>body</code> 和 评论的 <code>reactions(first: 100, content: HEART)</code> 以及 <code>reactions</code> 的相关信息。</p>\n<ul>\n <li><strong>添加或取消某个评论上的点赞</strong></li>\n</ul>\n<p>添加</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n mutation {\n </div>\n <div class=\"line\">\n addReaction(input: {\n <span class=\"attr\">subjectId</span>: \n <span class=\"string\">\"MDEyOklzc3VlQ29tbWVudDMxNTQxOTc2NQ==\"</span>, \n <span class=\"attr\">content</span>: HEART}) {\n </div>\n <div class=\"line\">\n reaction {\n </div>\n <div class=\"line\">\n content\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>取消</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n mutation {\n </div>\n <div class=\"line\">\n removeReaction(input: {\n <span class=\"attr\">subjectId</span>: \n <span class=\"string\">\"MDEyOklzc3VlQ29tbWVudDMxNTQxOTc2NQ==\"</span>, \n <span class=\"attr\">content</span>: HEART}) {\n </div>\n <div class=\"line\">\n reaction {\n </div>\n <div class=\"line\">\n content\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>之前的都是查询,这两个是 <code>mutation</code>,分别调用了 <code>addReaction</code> 和 <code>removeReaction</code>。<br>可以在从文档的 <code>ROOT TYPE</code> 上选择 <code>Mutation</code> 查看支持的所有 <code>mutation</code>。</p>\n<p>以上主要介绍了 GraphQL 的基本使用,具体更多内容可以查看 GraphQL 提供的<a href=\"http://graphql.org/learn/\" target=\"_blank\" rel=\"external\">教程</a>。</p>\n<div class=\"post-tags\" style=\"display:none\">\n <a href=\"/tags/web/\">web</a> \n <a href=\"/tags/api/\">api</a> \n <a href=\"/tags/graphql/\">graphql</a>\n</div>\n<div class=\"post-categories\" style=\"display:none\">\n <a href=\"/cates/Web开发/\">Web开发</a>\n</div>\n<div class=\"post-announce\">\n 感谢您的阅读,本文由 \n <a href=\"//aotu.io\">凹凸实验室</a> 版权所有。如若转载,请注明出处:凹凸实验室(\n <a href=\"https://aotu.io/notes/2017/12/15/graphql-use/\">https://aotu.io/notes/2017/12/15/graphql-use/</a>)\n</div>\n<div class=\"post-revision\">\n <i class=\"fa fa-clock-o\"></i> \n <time class=\"post-updated\" datetime=\"2018-11-07T01:01:15.653Z\">上次更新:2018-11-07 09:01:15</time>\n</div>');
INSERT INTO `rumo_blog` VALUES ('22', 'H5游戏开发:FC小蜜蜂', '1', 'H5游戏开发:FC小蜜蜂', '1', '2018-11-11 21:02:32', '2018-11-11 21:02:32', '1', '0', '//misc.aotu.io/ONE-SUNDAY/galaxian/galaxian_900x500.jpg', '', '0', '0', '<a id=\"more\"></a>\n<h2 id=\"前言\" class=\"post-heading\"><a href=\"#前言\" class=\"headerlink\" title=\"前言\"></a>前言<a class=\"post-anchor\" href=\"#前言\" aria-hidden=\"true\"></a></h2>\n<p>说起任天堂 FC 那是充满我们童年寒暑假的回忆,那时候没有正版红白机,玩的是几十块一台的山寨小霸王,十块一张的卡带,玩着魂斗罗、马里奥、淘金者、快打旋风、打鸭子等等。</p>\n<p>进入正题,今天我们来说说怎么做一个 FC 小蜜蜂游戏,游戏玩法是通过操控飞机,通过发射子弹对蜜蜂造成伤害,蜜蜂全部歼灭则视为胜利。</p>\n<div style=\"margin:0 auto;width:fit-content\">\n <img src=\"//misc.aotu.io/ONE-SUNDAY/galaxian/gameplay.gif\" alt=\"游戏演示\">\n</div>\n<h2 id=\"初始化\" class=\"post-heading\"><a href=\"#初始化\" class=\"headerlink\" title=\"初始化\"></a>初始化<a class=\"post-anchor\" href=\"#初始化\" aria-hidden=\"true\"></a></h2>\n<p>本次游戏采用 Phaser 引擎进行开发,Phaser 是一个快速、免费、易于维护的开源 2D 游戏框架,支持 JavaScript 和 TypeScript 两种语言开发,采用 Pixi.js 引擎作为底层渲染,内置了物理引擎、粒子动画、骨骼动画等效果。</p>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/galaxian/phaser.jpg\" alt=\"功能介绍\"></p>\n<p>在 Phaser 中有一个重要的概念,我们需要通过<strong>状态(State)</strong>来管理游戏中各个不同的场景,这也是 Phaser 官方建议的游戏代码组织方式,场景可以通过 <code>Phaser.Game.state</code> 来添加(add)和启动(start),每个场景有初始化(init)、预加载(preload)、准备就绪(create)、更新周期(update)、渲染完毕(render) 五种状态,按照顺序依次执行,同一时间只能存在一个场景,并且每个场景中至少包含五种状态中的一个。</p>\n<p>比如我们的小蜜蜂游戏一共会分为四个场景:开始场景、游戏场景、获胜场景、失败场景</p>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/galaxian/state.jpeg\" alt=\"游戏场景\"></p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div>\n <div class=\"line\">\n 16\n </div>\n <div class=\"line\">\n 17\n </div>\n <div class=\"line\">\n 18\n </div>\n <div class=\"line\">\n 19\n </div>\n <div class=\"line\">\n 20\n </div>\n <div class=\"line\">\n 21\n </div>\n <div class=\"line\">\n 22\n </div>\n <div class=\"line\">\n 23\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">var</span> game = \n <span class=\"keyword\">new</span> Phaser.Game(\n <span class=\"number\">750</span>, \n <span class=\"number\">1206</span>, Phaser.AUTO, \n <span class=\"string\">\'wrapper\'</span>)\n </div>\n <div class=\"line\">\n <span class=\"keyword\">var</span> states = {}\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n states.start = { \n <span class=\"comment\">// 开始场景</span>\n </div>\n <div class=\"line\">\n preload: \n <span class=\"function\"><span class=\"keyword\">function</span>(<span class=\"params\"></span>) </span>{\n </div>\n <div class=\"line\">\n ...\n </div>\n <div class=\"line\">\n game.load.image(\n <span class=\"string\">\'example-1\'</span>, \n <span class=\"string\">\'images/example-1.png\'</span>)\n </div>\n <div class=\"line\">\n ...\n </div>\n <div class=\"line\">\n },\n </div>\n <div class=\"line\"> \n <span class=\"attr\">create</span>: \n <span class=\"function\"><span class=\"keyword\">function</span>(<span class=\"params\"></span>) </span>{\n </div>\n <div class=\"line\">\n game.state.start(\n <span class=\"string\">\'play\'</span>) \n <span class=\"comment\">// 加载完成后切换到游戏场景</span>\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n states.play = { ... } \n <span class=\"comment\">// 游戏场景</span>\n </div>\n <div class=\"line\">\n states.victory = { ... } \n <span class=\"comment\">// 胜利场景</span>\n </div>\n <div class=\"line\">\n states.defeat = { ... } \n <span class=\"comment\">// 失败场景</span>\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n game.state.add(\n <span class=\"string\">\'start\'</span>, states.load)\n </div>\n <div class=\"line\">\n game.state.add(\n <span class=\"string\">\'play\'</span>, states.play)\n </div>\n <div class=\"line\">\n game.state.add(\n <span class=\"string\">\'victory\'</span>, states.victory)\n </div>\n <div class=\"line\">\n game.state.add(\n <span class=\"string\">\'defeat\'</span> states.defeat)\n </div>\n <div class=\"line\">\n game.state.start(\n <span class=\"string\">\'start\'</span>)\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h2 id=\"无限滚屏\" class=\"post-heading\"><a href=\"#无限滚屏\" class=\"headerlink\" title=\"无限滚屏\"></a>无限滚屏<a class=\"post-anchor\" href=\"#无限滚屏\" aria-hidden=\"true\"></a></h2>\n<p>在无限滚屏中,游戏背景沿着 x 轴或者 y 轴重复的滚动,从而实现飞机一直在向前飞的错觉,我们通过创建两个背景,分别初始定位到一屏和二屏的位置,在绘制(update)的过程中持续移动两张背景图的 y 轴,当监听到两个背景超出特定位置后重新定位,从而达到无限循环背景的效果。</p>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/galaxian/background.jpeg\" alt=\"无限滚屏\"></p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">var</span> bg1 = game.add.image(\n <span class=\"number\">0</span>, \n <span class=\"number\">0</span>, \n <span class=\"string\">\'background\'</span>),\n </div>\n <div class=\"line\">\n bg2 = game.add.image(\n <span class=\"number\">0</span>, -bg1.height, \n <span class=\"string\">\'background\'</span>)\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n update: \n <span class=\"function\"><span class=\"keyword\">function</span>(<span class=\"params\"></span>) </span>{\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 持续的移动</span>\n </div>\n <div class=\"line\">\n bg1.y += \n <span class=\"number\">2</span>\n </div>\n <div class=\"line\">\n bg2.y += \n <span class=\"number\">2</span>\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 超出屏幕判断</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span> (bg1.y &gt;= \n <span class=\"number\">1206</span>) { bg1.y = \n <span class=\"number\">0</span> }\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span> (bg2.y &gt;= \n <span class=\"number\">0</span>) { bg2.y = -bg1.height }\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>当然,还有更为简便的方式,Phaser 提供了 TileSprite 平铺纹理,非常适合于这类平铺的背景,再结合 <code>autoScroll()</code> 方法,两行代码解决,另外还有一种叫 TileMaps 平铺的瓦片地图,很适合制作 FC 马里欧这类游戏,以后有机会再开一篇文章讲讲。</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">var</span> bg = game.add.tileSprite(\n <span class=\"number\">0</span>, \n <span class=\"number\">0</span>, \n <span class=\"number\">750</span>, \n <span class=\"number\">1206</span>, \n <span class=\"string\">\'background\'</span>)\n </div>\n <div class=\"line\">\n bg.autoScroll(\n <span class=\"number\">0</span>, \n <span class=\"number\">200</span>) \n <span class=\"comment\">// 水平滚动速度、垂直滚动速度</span>\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h2 id=\"创建一架飞机\" class=\"post-heading\"><a href=\"#创建一架飞机\" class=\"headerlink\" title=\"创建一架飞机\"></a>创建一架飞机<a class=\"post-anchor\" href=\"#创建一架飞机\" aria-hidden=\"true\"></a></h2>\n<p>飞机的移动我们通过键盘方向键进行控制,通过修改 x、y 值来实现位移,为了有更好的灵活性,我们使用 vx 和 vy 来控制 Sprite 的移动,vx 用于设置 Sprite 在 x 轴上的速度和方向,vy 用于设置 Sprite 在 y 轴上的速度和方向,不直接修改 Sprite 的 x 和 y 值,而是先更新速度值,然后再将这些速度值分配给 Sprite。</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n ...\n </div>\n <div class=\"line\">\n airplane.vx = \n <span class=\"number\">0</span>\n </div>\n <div class=\"line\">\n airplane.vy = \n <span class=\"number\">0</span>\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n update: \n <span class=\"function\"><span class=\"keyword\">function</span>(<span class=\"params\"></span>) </span>{\n </div>\n <div class=\"line\">\n airplane.x += airplane.vx\n </div>\n <div class=\"line\">\n airplane.y += airplane.vy\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n ...\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>接着监听键盘事件,需要注意的就是在弹起状态的时候要判断反向的键是否也已经弹起,避免造成互相干扰。</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n ...\n </div>\n <div class=\"line\">\n left.onDown.add(\n <span class=\"function\"><span class=\"keyword\">function</span>(<span class=\"params\"></span>) </span>{ airplane.vx = \n <span class=\"number\">-8</span> })\n </div>\n <div class=\"line\">\n left.onUp.add(\n <span class=\"function\"><span class=\"keyword\">function</span>(<span class=\"params\"></span>) </span>{ \n <span class=\"keyword\">if</span> (!right.isDown) { airplane.vx = \n <span class=\"number\">0</span> } })\n </div>\n <div class=\"line\">\n ...\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>最后是限制飞机的移动范围,我们要限制飞机只在屏幕范围内移动,类似空气墙效果,通过持续监听飞机上下左右四个方向是否碰触到边缘,对坐标进行归位,具体实现代码请看 contain 方法。</p>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/galaxian/contain.jpg\" alt=\"四种碰边缘情况\"></p>\n<h2 id=\"生成子弹\" class=\"post-heading\"><a href=\"#生成子弹\" class=\"headerlink\" title=\"生成子弹\"></a>生成子弹<a class=\"post-anchor\" href=\"#生成子弹\" aria-hidden=\"true\"></a></h2>\n<p>在游戏中,我们需要不断的发射子弹,这就存在一个问题,如何管理子弹?</p>\n<p>因为子弹越多会越占用我们的内存,游戏会发现越来越卡,我们使用对象池的方式生成子弹,并且在子弹击中蜜蜂或者超出屏幕时进行销毁。</p>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/galaxian/createBullet.jpg\" alt=\"创建子弹示意图\"></p>\n<p>对象池的本质是复用,通过 Group 和 getFirstExists 来实现。在优化前,我们每次创建子弹都会 new Sprite,使用一次后就丢掉,优化后是创建子弹后会放入对象池中,每次使用从对象池中取,如果对象池中有则使用对象池中的子弹。</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">this</span>.bullets = game.add.group() \n <span class=\"comment\">// 创建对象池</span>\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"keyword\">var</span> bullet = \n <span class=\"keyword\">this</span>.bullets.getFirstExists(\n <span class=\"literal\">false</span>) \n <span class=\"comment\">// 从对象池中取非存活状态的子弹</span>\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"keyword\">if</span> (bullet) { \n <span class=\"comment\">// 对象池中存在则复用</span>\n </div>\n <div class=\"line\">\n bullet.reset(\n <span class=\"keyword\">this</span>.airplane.x + \n <span class=\"number\">16</span>, \n <span class=\"keyword\">this</span>.airplane.y - \n <span class=\"number\">20</span>)\n </div>\n <div class=\"line\">\n } \n <span class=\"keyword\">else</span> { \n <span class=\"comment\">// 对象池中不存在则创建一个放入对象池中</span>\n </div>\n <div class=\"line\">\n bullet = game.add.sprite(\n <span class=\"keyword\">this</span>.airplane.x + \n <span class=\"number\">27</span>, \n <span class=\"keyword\">this</span>.airplane.y - \n <span class=\"number\">15</span>, \n <span class=\"string\">\'bullet\'</span>)\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">this</span>.bullets.addChild(bullet)\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h2 id=\"创建一群蜜蜂\" class=\"post-heading\"><a href=\"#创建一群蜜蜂\" class=\"headerlink\" title=\"创建一群蜜蜂\"></a>创建一群蜜蜂<a class=\"post-anchor\" href=\"#创建一群蜜蜂\" aria-hidden=\"true\"></a></h2>\n<h4 id=\"整体移动\" class=\"post-heading\"><a href=\"#整体移动\" class=\"headerlink\" title=\"整体移动\"></a>整体移动<a class=\"post-anchor\" href=\"#整体移动\" aria-hidden=\"true\"></a></h4>\n<p>创建 5 x 5 小蜜蜂是采用 Group 将所有的小蜜蜂对象放入其中,持续移动 Group,检测 Group 左右是否碰壁,进行反方向移动,但你会发现小蜜蜂的左右的某一列被歼灭后,Group 的宽度会随着小蜜蜂列数的变化而变化,而 Group 的 X 轴坐标还是以原来的宽度输出 X 坐标,这就导致我们在计算碰撞墙壁的时候出现问题。</p>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/galaxian/galaxingContainer.jpg\" alt=\"整体移动示意图\"></p>\n<p>因此我们改为通过 Group 来控制整体移动,小蜜蜂负责碰撞检测,当检测到小蜜蜂碰撞后,进行反方向移动,并跳出循环。</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">for</span> (\n <span class=\"keyword\">var</span> i = \n <span class=\"number\">0</span>; i &lt; galaxians.length; i++) {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">var</span> cur = galaxians[i]\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span> (cur.x + cur.parent.x &lt; \n <span class=\"number\">0</span> || cur.x + cur.parent.x + cur.width &gt; game.world.width) {\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 反向移动</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">break</span>\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h4 id=\"随机自杀式袭击\" class=\"post-heading\"><a href=\"#随机自杀式袭击\" class=\"headerlink\" title=\"随机自杀式袭击\"></a>随机自杀式袭击<a class=\"post-anchor\" href=\"#随机自杀式袭击\" aria-hidden=\"true\"></a></h4>\n<p>在间隔一段时间后随机小蜜蜂发起攻击,间隔不采用 setInterval 的方式,因为 setInterval 即使在页面最小化或非激活状态依然执行,我们采用 Phaser 提供的 Time 进行间隔触发避免此问题。</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n game.time.events.loop(Phaser.Timer.SECOND * \n <span class=\"number\">1.5</span>, \n <span class=\"function\"><span class=\"keyword\">function</span>(<span class=\"params\"></span>) </span>{\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 每两秒随机一只小蜜蜂</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">var</span> now = galaxians[(\n <span class=\"built_in\">Math</span>.floor(\n <span class=\"built_in\">Math</span>.random() * galaxians.length)]\n </div>\n <div class=\"line\">\n })\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>如何计算小蜜蜂向飞机发起攻击的运动轨迹,这里要借助三角函数的力量来解决,通过飞机位置和蜜蜂位置,获得对边(a)和邻边(b)的长度,根据勾股定理求出斜边(c)长度,知道各边长度后就能得到三角比。另外有一点,Group 的 X 轴在持续的移动,小蜜蜂会受 Group 影响,所以在移动小蜜蜂时要注意。</p>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/galaxian/trigonometric_ratio.jpeg\" alt=\"欧股定理示意图\"></p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">var</span> a = airplane.x + airplane.width / \n <span class=\"number\">2</span> - now.x + now.width /\n <span class=\"number\">2</span> \n <span class=\"comment\">// 获取 a 边长度</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">var</span> b = airplane.y + airplane.height / \n <span class=\"number\">2</span> - now.y + now.height / \n <span class=\"number\">2</span> \n <span class=\"comment\">// 获取 b 边长度</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">var</span> c = \n <span class=\"built_in\">Math</span>.sqrt(a * a + b * b) \n <span class=\"comment\">// 求出斜边 c 长度</span>\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"keyword\">var</span> speedX = a / c * \n <span class=\"number\">8</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">var</span> speedY = b / c * \n <span class=\"number\">8</span>\n </div>\n <div class=\"line\">\n now.x += speedX\n </div>\n <div class=\"line\">\n now.y += speedY\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h2 id=\"碰撞检测\" class=\"post-heading\"><a href=\"#碰撞检测\" class=\"headerlink\" title=\"碰撞检测\"></a>碰撞检测<a class=\"post-anchor\" href=\"#碰撞检测\" aria-hidden=\"true\"></a></h2>\n<p>在游戏中,我们需要检测子弹与蜜蜂的碰撞和检测蜜蜂与飞机的碰撞,在 2D 游戏中,常用的有轴对齐包围盒(简称 AABB)就是一个每条边都平行于 X 轴或者 Y 轴的矩形。</p>\n<p>AABB 可以用两个点表示:最大点和最小点,在 2D 中,最小点就是左下角的点,而最大点则是右上角的点。</p>\n<p>通过判断 AABB 与 AABB 是否有存在交叉即可得知是否有碰撞。</p>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/galaxian/AABB.jpeg\" alt=\"AABB示意图\"></p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"function\"><span class=\"keyword\">function</span> <span class=\"title\">hitTestRectangle</span>(<span class=\"params\">a, b</span>) </span>{\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">var</span> hit = (a.max.x &lt; b.min.x) || (b.max.x &lt; a.min.x) || (a.max.y &lt; b.min.y) || (b.max.y &lt; a.min.y) {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">return</span> !hit\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>以上就是 AABB 与 AABB 碰撞检测的原理,当然,你也可以省事采用 Phaser 提供的物理引擎,在 Phaser 中内置了三种物理引擎,分别是:Arcade Physics、P2 Physics 和 Ninja Physics。</p>\n<p>Arcade Physics:是三个中最为简单、性能最快的物理引擎,因为它的碰撞都是采用 AABB 与 AABB 的碰撞,所有的碰撞都是基于一个矩形边界(hitbox)来计算的,所有如果你想碰撞一个圆形的 Sprite,碰撞的则是它的矩形边界,而不是圆形本身,并且支持摩擦力、重力、弹跳、加速等物理效果,适合应用于精度要求不高,较为简单的游戏中。</p>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/galaxian/arcade.jpg\" alt=\"Arcade Physics示意图\"></p>\n<p>P2 Physics:它是一个更为复杂和逼真的物理引擎,使用 P2 你可以创建弹簧、钟摆、马达等东西,它唯一的缺点在于运算量大,对于性能有较高的要求。</p>\n<p>Ninja Physics:比 Arcade Physics 要复杂一点,最初是为 Flash 游戏而创造的,而现在由 Phaser 的作者 Richard Davey 移植到 JavaScript,它与其他物理引擎最大的区别在于支持斜坡碰撞。</p>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/galaxian/ninjaunner.jpg\" alt=\"Ninja Physics示意图\"></p>\n<p>下面简单介绍一下 Arcade Physics 的使用方法,首先要启动物理引擎</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n game.physics.startSystem(Phaser.Physics.ARCADE);\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>接着是需要为每个对象开启物理效果,显然一个个创建、添加对象并不高效,我更建议的是通过 Group 的形式添加,这样在 Group 上创建的对象都可以开启物理效果。</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n game.physics.arcade.enable(airplane) \n <span class=\"comment\">// 单独开启方式</span>\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"keyword\">var</span> platforms = game.add.group()\n </div>\n <div class=\"line\">\n platforms.enableBody = \n <span class=\"literal\">true</span> \n <span class=\"comment\">// 组开启方式</span>\n </div>\n <div class=\"line\">\n platforms.create(\n <span class=\"number\">0</span>, \n <span class=\"number\">0</span>, \n <span class=\"string\">\'airplane\'</span>)\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>完成这些以后就可以在 update 阶段使用碰撞检测,overlap 方法可传入两个游戏对象,对象可以是 Sprites、Groups 或者 Emitters,可以执行 Sprite 与 Sprite、Sprite 与 Group、Group 与 Group 的碰撞检测,与 collide 方法不同,该方法的物体不会执行任何的物理效果,它只负责碰撞检测。</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n update: \n <span class=\"function\"><span class=\"keyword\">function</span>(<span class=\"params\"></span>) </span>{\n </div>\n <div class=\"line\">\n game.physics.arcade.overlap(object1, object2, overlapCallback, processCallback, callbackContext)\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>到此碰撞检测介绍就到这,关于物理引擎的更多使用方法可移步至官网查看。</p>\n<h2 id=\"体验地址\" class=\"post-heading\"><a href=\"#体验地址\" class=\"headerlink\" title=\"体验地址\"></a>体验地址<a class=\"post-anchor\" href=\"#体验地址\" aria-hidden=\"true\"></a></h2>\n<p><a href=\"http://jdc.jd.com/demo/Galaxing/\" target=\"_blank\" rel=\"external\">【点击这里体验】</a>键盘方向键控制移动,空格发射子弹,暂时只支持 PC 端体验,另外游戏还有很多可增加的功能,比如:关卡设计(蜜蜂血量、速度、分数)、蜜蜂发射子弹、蜜蜂贝塞尔曲线移动、蜜蜂归位、音乐音效、爆炸动画等等。</p>\n<h2 id=\"尾巴\" class=\"post-heading\"><a href=\"#尾巴\" class=\"headerlink\" title=\"尾巴\"></a>尾巴<a class=\"post-anchor\" href=\"#尾巴\" aria-hidden=\"true\"></a></h2>\n<p>如果你希望入门 H5 游戏开发,不妨拿这个练练手,源码你可以在体验地址中查看到,Phaser 是很适合作为你入门 H5 游戏开发的一款游戏引擎,等你熟练使用也希望你能阅读源码,了解其中的原理,本文较为简单,感谢你的阅读。</p>\n<p>我们会定期更新关于「H5游戏开发」的文章,欢迎关注我们的<a href=\"https://zhuanlan.zhihu.com/snsgame\" target=\"_blank\" rel=\"external\">知乎专栏</a>。</p>\n<h2 id=\"参考资料\" class=\"post-heading\"><a href=\"#参考资料\" class=\"headerlink\" title=\"参考资料\"></a>参考资料<a class=\"post-anchor\" href=\"#参考资料\" aria-hidden=\"true\"></a></h2>\n<p><a href=\"https://github.com/kittykatattack/learningPixi\" target=\"_blank\" rel=\"external\">Learning Pixi.js</a><br><a href=\"https://github.com/photonstorm/phaser\" target=\"_blank\" rel=\"external\">Phaser</a><br><a href=\"https://www.joshmorony.com/setting-up-ninja-physics-in-phaser/\" target=\"_blank\" rel=\"external\">Setting up Ninja Physics in Phaser</a><br><a href=\"https://item.jd.com/12059086.html\" target=\"_blank\" rel=\"external\">《游戏编程算法与技巧》</a></p>\n<div class=\"post-tags\" style=\"display:none\">\n <a href=\"/tags/H5游戏开发/\">H5游戏开发</a> \n <a href=\"/tags/Phaser/\">Phaser</a>\n</div>\n<div class=\"post-categories\" style=\"display:none\">\n <a href=\"/cates/H5游戏开发/\">H5游戏开发</a>\n</div>\n<div class=\"post-announce\">\n 感谢您的阅读,本文由 \n <a href=\"//aotu.io\">凹凸实验室</a> 版权所有。如若转载,请注明出处:凹凸实验室(\n <a href=\"https://aotu.io/notes/2018/01/28/galaxian/\">https://aotu.io/notes/2018/01/28/galaxian/</a>)\n</div>\n<div class=\"post-revision\">\n <i class=\"fa fa-clock-o\"></i> \n <time class=\"post-updated\" datetime=\"2018-11-07T01:01:15.657Z\">上次更新:2018-11-07 09:01:15</time>\n</div>');
INSERT INTO `rumo_blog` VALUES ('23', 'H5游戏开发:游戏引擎入门推荐', '1', 'H5游戏开发:游戏引擎入门推荐', '1', '2018-11-11 21:02:32', '2018-11-11 21:02:32', '1', '0', 'http://misc.aotu.io/cssie/banner.jpg', '', '0', '0', '<a id=\"more\"></a>\n<h2 id=\"前言\" class=\"post-heading\"><a href=\"#前言\" class=\"headerlink\" title=\"前言\"></a>前言<a class=\"post-anchor\" href=\"#前言\" aria-hidden=\"true\"></a></h2>\n<p>很多刚刚接触到游戏开发,准备大展拳脚的小鲜肉们,往往在技术选型这第一关就栽了跟头。毕竟网络上的游戏引擎良莠不齐,官网上相关资料也比较少,而选择一个适合的游戏引擎是一个项目最基础,也是很核心的一部分。<br>试想一下,在游戏开发进行到中后期的时候,才发现项目引入的游戏引擎与需求相悖,这时候不管是重新做一些修修补补的工作或者更换游戏引擎,这都是相当耗费人力物力的一件事。为了避免这种情况的出现,在前期选择适合项目需求的游戏引擎显得尤为重要。<br>接下来我们来聊一聊如何去选择适合项目的 JS 游戏引擎。</p>\n<h2 id=\"游戏场景分类\" class=\"post-heading\"><a href=\"#游戏场景分类\" class=\"headerlink\" title=\"游戏场景分类\"></a>游戏场景分类<a class=\"post-anchor\" href=\"#游戏场景分类\" aria-hidden=\"true\"></a></h2>\n<p>在刚接到游戏需求时,我们可以从以下几个方面进行考量,分析出游戏需求场景所属,从而作为我们选择游戏引擎的依据。</p>\n<ul>\n <li>游戏效果呈现方式( 2D ? 3D ? VR ?)<br>这与游戏引擎能够支持的渲染方式直接挂钩。现在的 H5 游戏渲染方式一般有 2D 渲染、3D 渲染、VR 渲染三种。<br>而 2D 渲染一般也有三种:Dom 渲染、Canvas 渲染、WebGL 渲染。Dom 由于性能原因,一般只适合做一些动画效果较少,交互较少的小游戏,本文主要针对 Canvas 和 WebGL 展开介绍。<br>一般来说,对于 2D 小游戏来说,Canvas 渲染已经足够。然而 Canvas 渲染由于底层封装层次多,不足以支撑起大型游戏的性能要求,因此大型游戏最好选择 WebGL 渲染或者浏览器内嵌 Runtime 。</li>\n <li>游戏复杂度<br>这与游戏引擎能够支持的功能,提供的API,性能等方面关系比较大。</li>\n</ul>\n<h2 id=\"游戏引擎推荐\" class=\"post-heading\"><a href=\"#游戏引擎推荐\" class=\"headerlink\" title=\"游戏引擎推荐\"></a>游戏引擎推荐<a class=\"post-anchor\" href=\"#游戏引擎推荐\" aria-hidden=\"true\"></a></h2>\n<p>笔者从业界较流行的一些框架,进行以下几个方面对比,希望能从客观数据上给大家的技术选型带来建议和参考。</p>\n<ul>\n <li>引擎支持的渲染方式</li>\n <li>github上的 star 数</li>\n <li>更新时间</li>\n <li>文档详细度</li>\n <li>周边产品</li>\n</ul>\n<p><strong>2D,3D,VR 都支持的游戏引擎</strong></p>\n<div style=\"overflow:auto\">\n <br>\n <table style=\"width:1200px\">\n <thead>\n <tr>\n <th align=\"center\">name</th>\n <th align=\"center\">2D渲染(Canvas)</th>\n <th align=\"center\">2D渲染(WebGL)</th>\n <th align=\"center\">3D渲染(WebGL)</th>\n <th align=\"center\">VR</th>\n <th align=\"center\">github star 数</th>\n <th align=\"center\">文档详细程度</th>\n <th align=\"center\">周边产品</th>\n <th align=\"center\">备注</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <td align=\"center\">Egret</td>\n <td align=\"center\">YES</td>\n <td align=\"center\">YES</td>\n <td align=\"center\">YES</td>\n <td align=\"center\">YES</td>\n <td align=\"center\">2k(最新更新2017.12)</td>\n <td align=\"left\">\n <div style=\"width:90px\">\n ▪ 有中文文档\n <br>▪ 例子充足\n <br>▪ 社区活跃\n </div></td>\n <td align=\"left\">游戏开发过程中的每个环节基本都有工具支撑。</td>\n <td align=\"left\">不仅仅提供了一个基于HTML5技术的游戏引擎,更是提供了原生打包工具和众多周边产品</td>\n </tr>\n <tr>\n <td align=\"center\">LayaAir</td>\n <td align=\"center\">YES</td>\n <td align=\"center\">YES(优先)</td>\n <td align=\"center\">YES</td>\n <td align=\"center\">YES</td>\n <td align=\"center\">0.7k(最新更新2017.12)</td>\n <td align=\"left\">\n <div style=\"width:90px\">\n ▪ 有中文文档\n <br>▪ 例子充足\n <br>▪ 社区活跃\n </div></td>\n <td align=\"left\">提供开发工具和可视化编辑器</td>\n <td align=\"left\">支持2D、3D、VR,能开发超大游戏,forgame的醉西游,腾讯的QQ农场,乐动卓越的浪漫h5这些大作就是用它开发</td>\n </tr>\n </tbody>\n </table>\n</div>\n<h5 id=\"Egret\" class=\"post-heading\"><a href=\"#Egret\" class=\"headerlink\" title=\"Egret\"></a><a href=\"https://www.egret.com/index\" target=\"_blank\" rel=\"external\">Egret</a><a class=\"post-anchor\" href=\"#Egret\" aria-hidden=\"true\"></a></h5>\n<p><img src=\"http://misc.aotu.io/cssie/egret.png\" alt=\"\"></p>\n<p><small style=\"display:block;text-align:center;color:#999\">Egret 周边产品</small></p>\n<p>白鹭引擎是企业级游戏引擎,有团队维护。Egret 在工作流的支持上做的是比较好的,从 Wing 的代码编写,到 ResDepot 和 TextureMerger 的资源整合,再到 Inspector 调试,最后到原生打包(支持 APP 打包),游戏开发过程中的每个环节基本都有工具支撑。官网上的示例,教程也是比较多。值得一提的是,今年5月白鹭引擎支持了 WebAssembly ,这对于性能的提升又是一大里程碑。</p>\n<h5 id=\"LayaAir\" class=\"post-heading\"><a href=\"#LayaAir\" class=\"headerlink\" title=\"LayaAir\"></a><a href=\"https://www.layabox.com/\" target=\"_blank\" rel=\"external\">LayaAir</a><a class=\"post-anchor\" href=\"#LayaAir\" aria-hidden=\"true\"></a></h5>\n<p>在渲染模式上,LayaAir 支持 Canvas 和 WebGL 两种方式;在工具流的支持程度上,主要是提供了 LayaAir IDE。LayaAir IDE 包括代码模式与设计模式,支持代码开发与美术设计分离,内置了 SWF 转换、图集打包、JS 压缩与加密、APP 打包、Flash 发布等实用功能。</p>\n<p><strong>下图是主要支持2D游戏的游戏引擎</strong></p>\n<div style=\"overflow:auto\">\n <table style=\"width:1400px\">\n <thead>\n <tr>\n <th align=\"center\">name</th>\n <th align=\"center\">2D渲染(Canvas)</th>\n <th align=\"center\">2D渲染(WebGL)</th>\n <th align=\"center\">3D渲染(WebGL)</th>\n <th align=\"center\">VR</th>\n <th align=\"center\">github star 数</th>\n <th align=\"center\">文档详细程度</th>\n <th align=\"center\">周边产品</th>\n <th align=\"center\">备注</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <td align=\"center\">Pixi.js</td>\n <td align=\"center\">YES</td>\n <td align=\"center\">YES</td>\n <td align=\"center\">NO</td>\n <td align=\"center\">NO</td>\n <td align=\"center\">16.8k(最新更新2017.12)</td>\n <td align=\"left\">\n <div style=\"width:170px\">\n ▪ 英文文档\n <br>▪ 例子充足\n <br>▪ 英文社区\n </div></td>\n <td align=\"center\">无</td>\n <td align=\"left\">依赖于canvas的WebGL渲染器</td>\n </tr>\n <tr>\n <td align=\"center\">Phaser</td>\n <td align=\"center\">YES</td>\n <td align=\"center\">YES</td>\n <td align=\"center\">NO</td>\n <td align=\"center\">NO</td>\n <td align=\"center\">16.9k(最新更2017.07)</td>\n <td align=\"left\">\n <div style=\"width:170px\">\n ▪ 英文文档\n <br>▪ 例子充足\n <br>▪ 英文社区\n </div></td>\n <td align=\"left\">提供在线编辑器Phaser Sandbox</td>\n <td align=\"left\"></td>\n </tr>\n <tr>\n <td align=\"center\">CreateJs</td>\n <td align=\"center\">YES</td>\n <td align=\"center\">YES</td>\n <td align=\"center\">NO</td>\n <td align=\"center\">NO</td>\n <td align=\"center\">6.5k(最新更新2017.12)</td>\n <td align=\"left\">\n <div style=\"width:170px\">\n ▪ 英文文档\n <br>▪ 例子充足\n <br>▪ 有博客\n </div></td>\n <td align=\"center\">无</td>\n <td align=\"left\">官方推荐TweenJS,SoundJS,PreloadJS配合使用</td>\n </tr>\n <tr>\n <td align=\"center\">Hilo</td>\n <td align=\"center\">YES</td>\n <td align=\"center\">YES</td>\n <td align=\"center\">YES(Hilo3D)</td>\n <td align=\"center\">NO</td>\n <td align=\"center\">4.2k(最新更新2017.12)</td>\n <td align=\"left\">\n <div style=\"width:170px\">\n ▪ 有中文文档\n <br>▪ 例子充足\n <br>\n </div></td>\n <td align=\"left\">提供资源下载和管理工具</td>\n <td align=\"left\">阿里巴巴集团推出,适合开发营销小游戏,以Chipmunk为2D物理引擎,与主流物理引擎兼容</td>\n </tr>\n <tr>\n <td align=\"center\">Cocos2d-x</td>\n <td align=\"center\">YES</td>\n <td align=\"center\">YES</td>\n <td align=\"center\">NO</td>\n <td align=\"center\">NO</td>\n <td align=\"center\">11.2k(最新更新2017.12)</td>\n <td align=\"left\">\n <div style=\"width:170px\">\n ▪ 有中文文档\n <br>▪ js例子不多,c++例子较多\n <br>▪ 社区活跃\n </div></td>\n <td align=\"left\">Cocos Creator编辑器,打包工具等</td>\n <td align=\"left\">提供的功能相当完整</td>\n </tr>\n <tr>\n <td align=\"center\">lufylegend.js</td>\n <td align=\"center\">YES</td>\n <td align=\"center\">NO</td>\n <td align=\"center\">NO</td>\n <td align=\"center\">NO</td>\n <td align=\"center\">0.4k(最新更新2016.03)</td>\n <td align=\"left\">\n <div style=\"width:170px\">\n ▪ 有中文文档\n <br>▪ 社区活跃\n <br>\n </div></td>\n <td align=\"center\">无</td>\n <td align=\"left\">仿ActionScript3.0的语法,支持Google Chrome,Firefox,Opera,IE9,IOS,Android等多种热门环境,可以配合Box2dWeb制作物理游戏,内置了LTweenLite缓动类等</td>\n </tr>\n </tbody>\n </table>\n</div>\n<h5 id=\"Pixi-js\" class=\"post-heading\"><a href=\"#Pixi-js\" class=\"headerlink\" title=\"Pixi.js\"></a><a href=\"http://www.pixijs.com/\" target=\"_blank\" rel=\"external\">Pixi.js</a><a class=\"post-anchor\" href=\"#Pixi-js\" aria-hidden=\"true\"></a></h5>\n<p>一般来说,WebGL 的渲染速度都会比 Canvas 快,这是由俩者的绘制路径决定的。Pixi 最大的特点在于,Pixi 具有完整的 WebGL 支持,却并不要求开发者掌握 WebGL 的相关知识,并在需要时无缝地回退到 Canvas 。相较于很多同类产品,它的渲染能力是比较强大的。然而,Pixi 也有不足的地方,Pixi 对于动画的支持是比较缺乏的,在实际开发中,常常需要引进额外的动画库,如 GSAP。</p>\n<h5 id=\"Phaser\" class=\"post-heading\"><a href=\"#Phaser\" class=\"headerlink\" title=\"Phaser\"></a><a href=\"http://phaser.io/\" target=\"_blank\" rel=\"external\">Phaser</a><a class=\"post-anchor\" href=\"#Phaser\" aria-hidden=\"true\"></a></h5>\n<p>Phaser 在渲染方面直接封装了 Pixi;架构方面,Phaser 内嵌了3个物理引擎(Arcade Physics、Ninja、p2.js),提供粒子系统、动画、预下载和设备适配方案;兼容性方面,Phaser 的焦点是放在移动端浏览器上的;API 方面,Phaser 能实现丰富的游戏功能,适合复杂度高的游戏开发。</p>\n<h5 id=\"CreateJS\" class=\"post-heading\"><a href=\"#CreateJS\" class=\"headerlink\" title=\"CreateJS\"></a><a href=\"https://www.createjs.com/\" target=\"_blank\" rel=\"external\">CreateJS</a><a class=\"post-anchor\" href=\"#CreateJS\" aria-hidden=\"true\"></a></h5>\n<p><img src=\"http://misc.aotu.io/cssie/createjs.jpg\" alt=\"\"></p>\n<p><small style=\"display:block;text-align:center;color:#999\">CreateJs 周边产品</small></p>\n<p>CreateJS 官方提供了 TweenJS 支持动画开发,同时通过 SoundJS 和 PreLoadJS 提供了音频和预下载的支持,对于 H5 游戏基础功能的支持是足够的。在兼容性方面,CreateJS 支持 PC 端和移动端几乎所有的浏览器。此外,CreateJS 还支持用 flash CC 开发导出由 CreateJS 渲染的 H5 游戏。</p>\n<h5 id=\"Hilo\" class=\"post-heading\"><a href=\"#Hilo\" class=\"headerlink\" title=\"Hilo\"></a><a href=\"http://hiloteam.github.io/\" target=\"_blank\" rel=\"external\">Hilo</a><a class=\"post-anchor\" href=\"#Hilo\" aria-hidden=\"true\"></a></h5>\n<p>Hilo 是阿里团队推出的一个开源项目,支持模块化开发,同时提供了多种模块范式的包装版本和跨终端解决方案,适合用来开发营销小游戏。其体积也是比较轻量的,只有70kb左右。Hilo 支持 DOM 渲染,Canvas 渲染和 WebGL 渲染,同时集成了 Hilo Audio, Hilo Preload。其后推出的 Hilo 3D 也是其亮点之一。</p>\n<h5 id=\"Cocos2d-x\" class=\"post-heading\"><a href=\"#Cocos2d-x\" class=\"headerlink\" title=\"Cocos2d-x\"></a><a href=\"http://www.cocos.com/\" target=\"_blank\" rel=\"external\">Cocos2d-x</a><a class=\"post-anchor\" href=\"#Cocos2d-x\" aria-hidden=\"true\"></a></h5>\n<p>Cocos2d-x 是业界比较老牌的游戏引擎了,同时支持 C++ ,Lua 和 JavaScript 三种开发语言,官方用例来看更倾向于 C++ 开发,适合做一些中大型游戏开发。Cocos2d-x 提供 Cocos Creator 游戏开发工具,组件化,脚本化,数据驱动,跨平台发布。</p>\n<h5 id=\"lufylegend-js\" class=\"post-heading\"><a href=\"#lufylegend-js\" class=\"headerlink\" title=\"lufylegend.js\"></a><a href=\"http://www.lufylegend.com/\" target=\"_blank\" rel=\"external\">lufylegend.js</a><a class=\"post-anchor\" href=\"#lufylegend-js\" aria-hidden=\"true\"></a></h5>\n<p>lufylegend.js 的最新更新是在16年,不过其社区还是十分活跃的,如果遇到什么开发问题,可以很方便地在社区上找到解决的方案。lufylegend.js 可以支持基础的游戏功能,但是其可拓展性不是很强。</p>\n<p><strong>主要支持3D游戏的游戏引擎</strong></p>\n<div style=\"overflow:auto\">\n <table style=\"width:1500px\">\n <thead>\n <tr>\n <th align=\"center\">name</th>\n <th align=\"center\">2D渲染(Canvas)</th>\n <th align=\"center\">2D渲染(WebGL)</th>\n <th align=\"center\">3D渲染(WebGL)</th>\n <th align=\"center\">VR</th>\n <th align=\"center\">github star 数</th>\n <th align=\"center\">文档详细程度</th>\n <th align=\"center\">周边产品</th>\n <th align=\"center\">备注</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <td align=\"center\">Three.js</td>\n <td align=\"center\">NO</td>\n <td align=\"center\">NO</td>\n <td align=\"center\">YES(倾向)</td>\n <td align=\"center\">NO</td>\n <td align=\"center\">37.6k(最新更新2017.12)</td>\n <td align=\"left\">\n <div style=\"width:90px\">\n ▪ 英文文档\n <br>▪ 例子充足\n <br>▪ 英文社区\n </div></td>\n <td align=\"center\">无</td>\n <td align=\"left\">默认Ammo.js为默认物理引擎,基于JavaScript语言的3D库,耗性能,加载慢,效果一般</td>\n </tr>\n <tr>\n <td align=\"center\">PlayCanvas</td>\n <td align=\"center\">NO</td>\n <td align=\"center\">NO</td>\n <td align=\"center\">YES</td>\n <td align=\"center\">YES</td>\n <td align=\"center\">3k(最新更新2017.12)</td>\n <td align=\"left\">\n <div style=\"width:90px\">\n ▪ 英文文档\n <br>▪ 例子充足\n <br>▪ 英文社区\n </div></td>\n <td align=\"left\">提供了在线编辑器,发布托管等</td>\n <td align=\"left\">教程较为详细,入门快</td>\n </tr>\n </tbody>\n </table>\n</div>\n<h5 id=\"Three-js\" class=\"post-heading\"><a href=\"#Three-js\" class=\"headerlink\" title=\"Three.js\"></a><a href=\"https://threejs.org/\" target=\"_blank\" rel=\"external\">Three.js</a><a class=\"post-anchor\" href=\"#Three-js\" aria-hidden=\"true\"></a></h5>\n<p><img src=\"http://misc.aotu.io/cssie/threejs.jpg\" alt=\"\"></p>\n<p><small style=\"display:block;text-align:center;color:#999\">Three.js 示例案例</small></p>\n<p>相信对于很多有关注 3D 游戏的开发者来说,Three.js 早已经耳熟能详了。实际上,Three.js 官方定位并不是游戏引擎,而是一个 JS 3D 库。Three.js 更倾向于展示型的视觉呈现,比较少直接拿 Three.js 来开发 H5 游戏。渲染环境上,Three.js 支持 WebGL 和 CSS3D 两种渲染模式。</p>\n<h5 id=\"PlayCanvas\" class=\"post-heading\"><a href=\"#PlayCanvas\" class=\"headerlink\" title=\"PlayCanvas\"></a><a href=\"https://playcanvas.com/\" target=\"_blank\" rel=\"external\">PlayCanvas</a><a class=\"post-anchor\" href=\"#PlayCanvas\" aria-hidden=\"true\"></a></h5>\n<p>从渲染支持程度来看,PlayCanvas 不仅支持 3D WebGL渲染,同时保持到 VR 的支持,拥有比较好的拓展性。在工具流的支持上,提供了在线编辑器和发布托管等服务。从官方教程上看,教程也是比较详细的。</p>\n<h2 id=\"结语\" class=\"post-heading\"><a href=\"#结语\" class=\"headerlink\" title=\"结语\"></a>结语<a class=\"post-anchor\" href=\"#结语\" aria-hidden=\"true\"></a></h2>\n<p>现在市场上的 H5游戏引擎很多,很难去直接定义哪个引擎的好坏,只能说每个引擎都有自己的特性,在某方面跟项目的契合程度比较高,笔者根据现在市场上比较热门的几大引擎做了几点比较,希望能给刚入门的你做技术选型的时候有一点帮助,找到适合项目的引擎,更快、更准、更高效率地完成项目需求。</p>\n<p>感谢各位耐心读完,希望能有所收获,有考虑不足的地方欢迎留言指出。</p>\n<p>如果对「H5游戏开发」感兴趣,欢迎关注我们的<a href=\"https://zhuanlan.zhihu.com/snsgame\" target=\"_blank\" rel=\"external\">专栏</a>。</p>\n<h2 id=\"参考资料\" class=\"post-heading\"><a href=\"#参考资料\" class=\"headerlink\" title=\"参考资料\"></a>参考资料<a class=\"post-anchor\" href=\"#参考资料\" aria-hidden=\"true\"></a></h2>\n<p><a href=\"https://www.zhihu.com/question/20079322\" target=\"_blank\" rel=\"external\">目前有哪些比较成熟的 HTML5 游戏引擎?</a></p>\n<p><a href=\"http://www.jianshu.com/p/0469cd7b1711\" target=\"_blank\" rel=\"external\">HTML5游戏引擎深度测评</a></p>\n<p><a href=\"http://www.jianshu.com/p/0469cd7b1711\" target=\"_blank\" rel=\"external\">现在 TypeScript 的生态如何?</a></p>\n<div class=\"post-tags\" style=\"display:none\">\n <a href=\"/tags/H5/\">H5</a> \n <a href=\"/tags/JS游戏引擎/\">JS游戏引擎</a> \n <a href=\"/tags/引擎推荐/\">引擎推荐</a>\n</div>\n<div class=\"post-categories\" style=\"display:none\">\n <a href=\"/cates/H5游戏开发/\">H5游戏开发</a>\n</div>\n<div class=\"post-announce\">\n 感谢您的阅读,本文由 \n <a href=\"//aotu.io\">凹凸实验室</a> 版权所有。如若转载,请注明出处:凹凸实验室(\n <a href=\"https://aotu.io/notes/2017/12/27/h5-game-engine-recommend/\">https://aotu.io/notes/2017/12/27/h5-game-engine-recommend/</a>)\n</div>\n<div class=\"post-revision\">\n <i class=\"fa fa-clock-o\"></i> \n <time class=\"post-updated\" datetime=\"2018-11-07T01:01:15.657Z\">上次更新:2018-11-07 09:01:15</time>\n</div>');
INSERT INTO `rumo_blog` VALUES ('24', 'H5游戏开发:消灭星星', '1', 'H5游戏开发:消灭星星', '1', '2018-11-11 21:02:33', '2018-11-11 21:02:33', '1', '0', 'https://misc.aotu.io/leeenx/popstar/cover.jpg', '', '0', '0', '<a id=\"more\"></a>\n<p>「消灭星星」是一款很经典的「消除类游戏」,它的玩法很简单:消除相连通的同色砖块。</p>\n<p><img src=\"https://misc.aotu.io/leeenx/popstar/popstar.gif\" alt=\"demo\"></p>\n<h2 id=\"1-游戏规则\" class=\"post-heading\"><a href=\"#1-游戏规则\" class=\"headerlink\" title=\"1. 游戏规则\"></a>1. 游戏规则<a class=\"post-anchor\" href=\"#1-游戏规则\" aria-hidden=\"true\"></a></h2>\n<p>「消灭星星」存在多个版本,不过它们的规则除了「关卡分值」有些出入外,其它的规则都是一样的。笔者介绍的版本的游戏规则整理如下:</p>\n<p><strong>1. 色砖分布</strong></p>\n<ul>\n <li>10 x 10 的表格</li>\n <li>5种颜色 —— 红、绿、蓝,黄,紫</li>\n <li>每类色砖个数在指定区间内随机</li>\n <li>5类色砖在 10 x 10 表格中随机分布</li>\n</ul>\n<p><strong>2. 消除规则</strong></p>\n<p>两个或两个以上同色砖块相连通即是可被消除的砖块。</p>\n<p><strong>3. 分值规则</strong></p>\n<ul>\n <li>消除总分值 = n * n * 5</li>\n <li>奖励总分值 = 2000 - n * n * 20</li>\n</ul>\n<p>「n」表示砖块数量。上面是「总」分值的规则,还有「单」个砖块的分值规则:</p>\n<ul>\n <li>消除砖块得分值 = 10 * i + 5</li>\n <li>剩余砖块扣分值 = 40 * i + 20</li>\n</ul>\n<p>「i」表示砖块的索引值(从 0 开始)。简单地说,单个砖块「得分值」和「扣分值」是一个等差数列。</p>\n<p><strong>4. 关卡分值</strong></p>\n<p>关卡分值 = 1000 + (level - 1) * 2000;「level」即当前关卡数。</p>\n<p><strong>5. 通关条件</strong></p>\n<ul>\n <li>可消除色块不存在</li>\n <li>累计分值 &gt;= 当前关卡分值</li>\n</ul>\n<p>上面两个条件同时成立游戏才可以通关。</p>\n<h2 id=\"2-MVC-设计模式\" class=\"post-heading\"><a href=\"#2-MVC-设计模式\" class=\"headerlink\" title=\"2. MVC 设计模式\"></a>2. MVC 设计模式<a class=\"post-anchor\" href=\"#2-MVC-设计模式\" aria-hidden=\"true\"></a></h2>\n<p>笔者这次又是使用了 MVC 模式来写「消灭星星」。星星「砖块」的数据结构与各种状态由 Model 实现,游戏的核心在 Model 中完成;View 映射 Model 的变化并做出对应的行为,它的任务主要是展示动画;用户与游戏的交互由 Control 完成。</p>\n<p>从逻辑规划上看,Model 很重而View 与 Control 很轻,不过,从代码量上看,View 很重而 Model 与 Control 相对很轻。</p>\n<h2 id=\"3-Model\" class=\"post-heading\"><a href=\"#3-Model\" class=\"headerlink\" title=\"3. Model\"></a>3. Model<a class=\"post-anchor\" href=\"#3-Model\" aria-hidden=\"true\"></a></h2>\n<p>10 x 10 的表格用长度为 100 的数组可完美映射游戏的星星「砖块」。</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n [\n </div>\n <div class=\"line\">\n R, R, G, G, B, B, Y, Y, P, P, \n </div>\n <div class=\"line\">\n R, R, G, G, B, B, Y, Y, P, P, \n </div>\n <div class=\"line\">\n R, R, G, G, B, B, Y, Y, P, P, \n </div>\n <div class=\"line\">\n R, R, G, G, B, B, Y, Y, P, P, \n </div>\n <div class=\"line\">\n R, R, G, G, B, B, Y, Y, P, P, \n </div>\n <div class=\"line\">\n R, R, G, G, B, B, Y, Y, P, P, \n </div>\n <div class=\"line\">\n R, R, G, G, B, B, Y, Y, P, P, \n </div>\n <div class=\"line\">\n R, R, G, G, B, B, Y, Y, P, P, \n </div>\n <div class=\"line\">\n R, R, G, G, B, B, Y, Y, P, P, \n </div>\n <div class=\"line\">\n R, R, G, G, B, B, Y, Y, P, P\n </div>\n <div class=\"line\">\n ]\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>R - 红色,G - 绿色,B - 蓝色,Y - 黄色,P - 紫色。Model 的核心任务是以下四个:</p>\n<ul>\n <li>生成砖墙</li>\n <li>消除砖块 (生成砖块分值)</li>\n <li>夯实砖墙</li>\n <li>清除残砖 (生成奖励分值)</li>\n</ul>\n<h3 id=\"3-1-生成砖墙\" class=\"post-heading\"><a href=\"#3-1-生成砖墙\" class=\"headerlink\" title=\"3.1 生成砖墙\"></a>3.1 生成砖墙<a class=\"post-anchor\" href=\"#3-1-生成砖墙\" aria-hidden=\"true\"></a></h3>\n<p>砖墙分两步生成:</p>\n<ul>\n <li>色砖数量分配</li>\n <li>打散色砖</li>\n</ul>\n<p>理论上,可以将 100 个格子可以均分到 5 类颜色,不过笔者玩过的「消灭星星」都不使用均分策略。通过分析几款「消灭星星」,其实可以发现一个规律 —— 「<strong>色砖之间的数量差在一个固定的区间内</strong>」。</p>\n<p>如果把传统意义上的均分称作「完全均分」,那么「消灭星星」的分配是一种在均分线上下波动的「不完全均分」。</p>\n<p><img src=\"https://misc.aotu.io/leeenx/popstar/2017-12-06-waveAverage.gif\" alt=\"正余弦波线图\"></p>\n<p>笔者把上面的「不完全均分」称作「波动均分」,算法的具体实现可以参见「<a href=\"https://aotu.io/notes/2018/01/11/waveaverage/\">波动均分算法</a>」。</p>\n<p>「打散色砖」其实就是将数组乱序的过程,笔者推荐使用「 <a href=\"https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle\" target=\"_blank\" rel=\"external\">费雪耶兹乱序算法</a>」。</p>\n<p>以下是伪代码的实现:<br></p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">// 波动均分色砖</span>\n </div>\n <div class=\"line\">\n waveaverage(\n <span class=\"number\">5</span>, \n <span class=\"number\">4</span>, \n <span class=\"number\">4</span>).forEach(\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// tiles 即色墙数组</span>\n </div>\n <div class=\"line\">\n (count, clr) =&gt; tiles.concat(generateTiles(count, clr)); \n </div>\n <div class=\"line\">\n ); \n </div>\n <div class=\"line\">\n <span class=\"comment\">// 打散色砖</span>\n </div>\n <div class=\"line\">\n shuffle(tiles);\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<h3 id=\"3-2-消除砖块\" class=\"post-heading\"><a href=\"#3-2-消除砖块\" class=\"headerlink\" title=\"3.2 消除砖块\"></a>3.2 消除砖块<a class=\"post-anchor\" href=\"#3-2-消除砖块\" aria-hidden=\"true\"></a></h3>\n<p>「消除砖块」的规则很简单 —— <strong>相邻相连通相同色即可以消除</strong>。</p>\n<p><img src=\"https://misc.aotu.io/leeenx/popstar/20180111-connection.png\" alt=\"连通图\"><br>前两个组合符合「相邻相连通相同色即可以消除」,所以它们可以被消除;第三个组合虽然「相邻相同色」但是不「相连通」所以它不能被消除。</p>\n<p>「消除砖块」的同时有一个重要的任务:生成砖块对应的分值。在「游戏规则」中,笔者已经提供了对应的数学公式:「消除砖块得分值 = 10 * i + 5」。</p>\n<p><strong>「消除砖块」算法实现如下:</strong><br></p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div>\n <div class=\"line\">\n 16\n </div>\n <div class=\"line\">\n 17\n </div>\n <div class=\"line\">\n 18\n </div>\n <div class=\"line\">\n 19\n </div>\n <div class=\"line\">\n 20\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"function\"><span class=\"keyword\">function</span> <span class=\"title\">clean</span>(<span class=\"params\">tile</span>) </span>{ \n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> count = \n <span class=\"number\">1</span>; \n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> sameTiles = searchSameTiles(tile); \n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span>(sameTiles.length &gt; \n <span class=\"number\">0</span>) { \n </div>\n <div class=\"line\">\n deleteTile(tile); \n </div>\n <div class=\"line\"> \n <span class=\"keyword\">while</span>(\n <span class=\"literal\">true</span>) {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> nextSameTiles = []; \n </div>\n <div class=\"line\">\n sameTiles.forEach(\n <span class=\"function\"><span class=\"params\">tile</span> =&gt;</span> { \n </div>\n <div class=\"line\">\n nextSameTiles.push(...searchSameTiles(tile)); \n </div>\n <div class=\"line\">\n makeScore(++count * \n <span class=\"number\">10</span> + \n <span class=\"number\">5</span>); \n <span class=\"comment\">// 标记当前分值 </span>\n </div>\n <div class=\"line\">\n deleteTile(tile); \n <span class=\"comment\">// 删除砖块</span>\n </div>\n <div class=\"line\">\n }); \n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 清除完成,跳出循环</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span>(nextSameTiles.length === \n <span class=\"number\">0</span>) \n <span class=\"keyword\">break</span>; \n </div>\n <div class=\"line\"> \n <span class=\"keyword\">else</span> {\n </div>\n <div class=\"line\">\n sameTiles = nextSameTiles; \n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<p>清除的算法使用「递归」逻辑上会清晰一些,不过「递归」在浏览器上容易「栈溢出」,所以笔者没有使用「递归」实现。</p>\n<h3 id=\"3-3-夯实砖墙\" class=\"post-heading\"><a href=\"#3-3-夯实砖墙\" class=\"headerlink\" title=\"3.3 夯实砖墙\"></a>3.3 夯实砖墙<a class=\"post-anchor\" href=\"#3-3-夯实砖墙\" aria-hidden=\"true\"></a></h3>\n<p>砖墙在消除了部分砖块后,会出现空洞,此时需要对墙体进行夯实:</p>\n<table>\n <thead>\n <tr>\n <th style=\"text-align:center\"><img src=\"https://misc.aotu.io/leeenx/popstar/20180112-down.gif\" alt=\"向下夯实\"></th>\n <th style=\"text-align:center\"><img src=\"https://misc.aotu.io/leeenx/popstar/20180112-left.gif\" alt=\"向左夯实\"></th>\n <th style=\"text-align:center\"><img src=\"https://misc.aotu.io/leeenx/popstar/20180112-down-left.gif\" alt=\"先下再左夯实\"></th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <td style=\"text-align:center\"><strong>向下夯实</strong></td>\n <td style=\"text-align:center\"><strong>向左夯实</strong></td>\n <td style=\"text-align:center\"><strong>向左下夯实</strong>(先下后左)</td>\n </tr>\n </tbody>\n</table>\n<p>一种快速的实现方案是,每次「消除砖块」后直接遍历砖墙数组(10x10数组)再把空洞夯实,伪代码表示如下:</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">for</span>(\n <span class=\"keyword\">let</span> row = \n <span class=\"number\">0</span>; row &lt; \n <span class=\"number\">10</span>; ++row) {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">for</span>(\n <span class=\"keyword\">let</span> col = \n <span class=\"number\">0</span>; col &lt; \n <span class=\"number\">10</span>; ++col) { \n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span>(isEmpty(row, col)) {\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 水平方向(向左)夯实</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span>(isEmptyCol(col)) { \n </div>\n <div class=\"line\">\n tampRow(col); \n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 垂直方向(向下)夯实</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">else</span> {\n </div>\n <div class=\"line\">\n tampCol(col); \n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">break</span>; \n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>But… 为了夯实一个空洞对一张大数组进行全量遍历并不是一种高效的算法。在笔者看来影响「墙体夯实」效率的因素有:</p>\n<ol>\n <li>定位空洞</li>\n <li>砖块移动(夯实)</li>\n</ol>\n<p>扫描墙体数组的主要目的是「定位空洞」,但能否不扫描墙体数组直接「定位空洞」?</p>\n<p>墙体的「空洞」是由于「消除砖块」造成的,换种说法 —— <strong>被消除的砖块留下来的坑位就是墙体的空洞</strong>。在「消除砖块」的同时标记空洞的位置,这样就无须全量扫描墙体数组,伪代码如下:</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"function\"><span class=\"keyword\">function</span> <span class=\"title\">deleteTile</span>(<span class=\"params\">tile</span>) </span>{ \n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 标记空洞</span>\n </div>\n <div class=\"line\">\n markHollow(tile.index); \n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 删除砖块逻辑</span>\n </div>\n <div class=\"line\">\n ...\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>在上面的夯实动图,其实可以看到它的夯实过程如下:</p>\n<ol>\n <li>空洞上方的砖块向下移动</li>\n <li>空列右侧的砖块向左移动</li>\n</ol>\n<p>墙体在「夯实」过程中,它的边界是实时在变化,如果「夯实」不按真实边界进行扫描,会产生多余的空白扫描:</p>\n<p><img src=\"https://misc.aotu.io/leeenx/popstar/20180116-clean.gif\" alt=\"对比\"></p>\n<p><strong>如何记录墙体的边界?</strong><br>把墙体拆分成一个个单独的列,那么列最顶部的空白格片段就是墙体的「空白」,而其余非顶部的空白格片段即墙体的「空洞」。</p>\n<p><img src=\"https://misc.aotu.io/leeenx/popstar/20180117-col.gif\" alt=\"列\"></p>\n<p>笔者使用一组「列集合」来描述墙体的边界并记录墙体的空洞,它的模型如下:</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">/*</span>\n </div>\n <div class=\"line\">\n @ count - 列砖块数\n </div>\n <div class=\"line\">\n @ start - 顶部行索引\n </div>\n <div class=\"line\">\n @ end - 底部行索引\n </div>\n <div class=\"line\">\n @ pitCount - 坑数\n </div>\n <div class=\"line\">\n @ topPit - 最顶部的坑\n </div>\n <div class=\"line\">\n @ bottomPit - 最底部的坑\n </div>\n <div class=\"line\">\n */ \n </div>\n <div class=\"line\">\n <span class=\"keyword\">let</span> wall = [\n </div>\n <div class=\"line\">\n {count, start, end, pitCount, topPit, bottomPit}, \n </div>\n <div class=\"line\">\n {count, start, end, pitCount, topPit, bottomPit},\n </div>\n <div class=\"line\">\n ...\n </div>\n <div class=\"line\">\n ];\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>这个模型可以描述墙体的三个细节:</p>\n<ul>\n <li>空列</li>\n <li>列的连续空洞</li>\n <li>列的非连续空洞</li>\n</ul>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">// 空列</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">if</span>(count === \n <span class=\"number\">0</span>) { \n </div>\n <div class=\"line\">\n ...\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n <span class=\"comment\">// 连续空洞</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">else</span> \n <span class=\"keyword\">if</span>(bottomPit - topPit + \n <span class=\"number\">1</span> === pitCount) { \n </div>\n <div class=\"line\">\n ...\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n <span class=\"comment\">// 非连续空洞</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">else</span> {\n </div>\n <div class=\"line\">\n ...\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>砖块在消除后,映射到单个列上的空洞会有两种分布形态 —— 连续与非连续。</p>\n<p><img src=\"https://misc.aotu.io/leeenx/popstar/20180117-col2.gif\" alt=\"连续空格与非连续空格\"></p>\n<p>「连续空洞」与「非连续空洞」的夯实过程如下:</p>\n<p><img src=\"https://misc.aotu.io/leeenx/popstar/20180117-tamp.gif\" alt=\"空洞压缩\"></p>\n<p>其实「空列」放大于墙体上,也会有「空洞」类似的分布形态 —— 连续与非连续。<br><img src=\"https://misc.aotu.io/leeenx/popstar/20180117-col3.gif\" alt=\"空洞压缩\"></p>\n<p>它的夯实过程与空洞类似,这里就不赘述了。</p>\n<h3 id=\"3-4-消除残砖\" class=\"post-heading\"><a href=\"#3-4-消除残砖\" class=\"headerlink\" title=\"3.4 消除残砖\"></a>3.4 消除残砖<a class=\"post-anchor\" href=\"#3-4-消除残砖\" aria-hidden=\"true\"></a></h3>\n<p>上一小节提到了「描述墙体的边界并记录墙体的空洞」的「列集合」,笔者是直接使用这个「列集合」来消除残砖的,伪代码如下:</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"function\"><span class=\"keyword\">function</span> <span class=\"title\">clearAll</span>(<span class=\"params\"></span>) </span>{\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> count = \n <span class=\"number\">0</span>; \n </div>\n <div class=\"line\"> \n <span class=\"keyword\">for</span>(\n <span class=\"keyword\">let</span> col = \n <span class=\"number\">0</span>, len = \n <span class=\"keyword\">this</span>.wall.length; col &lt; len; ++col) { \n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> colInfo = \n <span class=\"keyword\">this</span>.wall[col]; \n </div>\n <div class=\"line\"> \n <span class=\"keyword\">for</span>(\n <span class=\"keyword\">let</span> row = colInfo.start; row &lt;= colInfo.end; ++row) {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> tile = \n <span class=\"keyword\">this</span>.grid[row * \n <span class=\"keyword\">this</span>.col + col]; \n </div>\n <div class=\"line\">\n tile.score = \n <span class=\"number\">-20</span> - \n <span class=\"number\">40</span> * count++; \n <span class=\"comment\">// 标记奖励分数</span>\n </div>\n <div class=\"line\">\n tile.removed = \n <span class=\"literal\">true</span>; \n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h2 id=\"4-View\" class=\"post-heading\"><a href=\"#4-View\" class=\"headerlink\" title=\"4. View\"></a>4. View<a class=\"post-anchor\" href=\"#4-View\" aria-hidden=\"true\"></a></h2>\n<p>View 主要的功能有两个:</p>\n<ul>\n <li>UI 管理</li>\n <li>映射 Model 的变化(动画)</li>\n</ul>\n<p>UI 管理主要是指「界面绘制」与「资源加载管理」,这两项功能比较常见本文就直接略过了。View 的重头戏是「映射 Model 的变化」并完成对应的动画。动画是复杂的,而映射的原理是简单的,如下伪代码:</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div>\n <div class=\"line\">\n 16\n </div>\n <div class=\"line\">\n 17\n </div>\n <div class=\"line\">\n 18\n </div>\n <div class=\"line\">\n 19\n </div>\n <div class=\"line\">\n 20\n </div>\n <div class=\"line\">\n 21\n </div>\n <div class=\"line\">\n 22\n </div>\n <div class=\"line\">\n 23\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n update({originIndex, index, clr, removed, score}) { \n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 还没有 originIndex 或没有色值,直接不处理</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span>(originIndex === \n <span class=\"literal\">undefined</span> || clr === \n <span class=\"literal\">undefined</span>) \n <span class=\"keyword\">return</span> ; \n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> tile = \n <span class=\"keyword\">this</span>.tiles[originIndex]; \n </div>\n <div class=\"line\"> \n <span class=\"comment\">// tile 存在,判断颜色是否一样</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span>(tile.clr !== clr) {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">this</span>.updateTileClr(tile, clr); \n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 当前索引变化 ----- 表示位置也有变化 </span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span>(tile.index !== index) { \n </div>\n <div class=\"line\"> \n <span class=\"keyword\">this</span>.updateTileIndex(tile, index); \n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 设置分数</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span>(tile.score !== score) {\n </div>\n <div class=\"line\">\n tile.score = score; \n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"></div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span>(tile.removed !== removed) { \n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 移除或添加当前节点</span>\n </div>\n <div class=\"line\"> \n <span class=\"literal\">true</span> === removed ? \n <span class=\"keyword\">this</span>.bomb(tile) : \n <span class=\"keyword\">this</span>.area.addChild(tile.sprite); \n </div>\n <div class=\"line\">\n tile.removed = removed; \n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>Model 的砖块每次数据的更改都会通知到 View 的砖块,View 会根据对应的变化做对应的动作(动画)。</p>\n<h2 id=\"5-Control\" class=\"post-heading\"><a href=\"#5-Control\" class=\"headerlink\" title=\"5. Control\"></a>5. Control<a class=\"post-anchor\" href=\"#5-Control\" aria-hidden=\"true\"></a></h2>\n<p>Control 要处理的事务比较多,如下:</p>\n<ul>\n <li>绑定 Model &amp; View</li>\n <li>生成通关分值</li>\n <li>判断通关条件</li>\n <li>对外事件</li>\n <li>用户交互</li>\n</ul>\n<p>初始化时,Control 把 Model 的砖块单向绑定到 View 的砖块了。如下:</p>\n<pre><code class=\"javascript\"><span class=\"built_in\">Object</span>.defineProperties(model.tile, {\n <span class=\"attr\">originIndex</span>: {\n get() {...}, \n set(){\n ...\n view.update({originIndex})\n }\n }, \n <span class=\"attr\">index</span>: {\n get() {...}, \n set() {\n ...\n view.update({index})\n }\n }, \n <span class=\"attr\">clr</span>: {\n get() {...}, \n set() {\n ...\n view.update({clr})\n }\n }, \n <span class=\"attr\">removed</span>: {\n get() {...}, \n set() {\n ...\n view.update({removed})\n }\n }, \n <span class=\"attr\">score</span>: {\n get() {...}, \n set() {\n ...\n view.update({score})\n }\n }\n})\n</code></pre>\n<p>「通关分值」与「判断通关条件」这对逻辑在本文的「游戏规则」中有相关介绍,这里不再赘述。</p>\n<p>对外事件规划如下:</p>\n<table>\n <thead>\n <tr>\n <th style=\"text-align:left\">name</th>\n <th style=\"text-align:left\">detail</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <td style=\"text-align:left\">pass</td>\n <td style=\"text-align:left\">通关</td>\n </tr>\n <tr>\n <td style=\"text-align:left\">pause</td>\n <td style=\"text-align:left\">暂停</td>\n </tr>\n <tr>\n <td style=\"text-align:left\">resume</td>\n <td style=\"text-align:left\">恢复</td>\n </tr>\n <tr>\n <td style=\"text-align:left\">gameover</td>\n <td style=\"text-align:left\">游戏结束</td>\n </tr>\n </tbody>\n</table>\n<p>用户交互 APIs 规划如下:</p>\n<table>\n <thead>\n <tr>\n <th style=\"text-align:left\">name</th>\n <th style=\"text-align:left\">type</th>\n <th style=\"text-align:left\">deltail</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <td style=\"text-align:left\">init</td>\n <td style=\"text-align:left\">method</td>\n <td style=\"text-align:left\">初始化游戏</td>\n </tr>\n <tr>\n <td style=\"text-align:left\">next</td>\n <td style=\"text-align:left\">method</td>\n <td style=\"text-align:left\">进入下一关</td>\n </tr>\n <tr>\n <td style=\"text-align:left\">enter</td>\n <td style=\"text-align:left\">method</td>\n <td style=\"text-align:left\">进入指定关卡</td>\n </tr>\n <tr>\n <td style=\"text-align:left\">pause</td>\n <td style=\"text-align:left\">method</td>\n <td style=\"text-align:left\">暂停</td>\n </tr>\n <tr>\n <td style=\"text-align:left\">resume</td>\n <td style=\"text-align:left\">method</td>\n <td style=\"text-align:left\">恢复</td>\n </tr>\n <tr>\n <td style=\"text-align:left\">destroy</td>\n <td style=\"text-align:left\">method</td>\n <td style=\"text-align:left\">销毁游戏</td>\n </tr>\n </tbody>\n</table>\n<h2 id=\"6-问题\" class=\"post-heading\"><a href=\"#6-问题\" class=\"headerlink\" title=\"6. 问题\"></a>6. 问题<a class=\"post-anchor\" href=\"#6-问题\" aria-hidden=\"true\"></a></h2>\n<p>在知乎有一个关于「消灭星星」的话题:<a href=\"https://www.zhihu.com/question/22530789\" target=\"_blank\" rel=\"external\">popstar关卡是如何设计的?</a></p>\n<p>这个话题在最后提出了一个问题 —— <strong>「无法消除和最大得分不满足过关条件的矩阵」</strong>。</p>\n<p><img src=\"https://misc.aotu.io/leeenx/popstar/qustion.jpg\" alt=\"两个问题\"></p>\n<p>「无法消除的矩阵」其实就是最大得分为0的矩阵,本质上是「最大得分不满足过关条件的矩阵」。</p>\n<p><strong>最大得分不满足过关条件的矩阵</strong><br>求「矩阵」的最大得分是一个 「<a href=\"https://baike.baidu.com/item/%E8%83%8C%E5%8C%85%E9%97%AE%E9%A2%98\" target=\"_blank\" rel=\"external\">背包问题</a>」,求解的算法不难:对当前矩阵用「递归」的形式把所有的消灭分支都执行一次,并取最高分值。但是 javascript 的「递归」极易「栈溢出」导致算法无法执行。</p>\n<p>其实在知乎的话题中提到一个解决方案:</p>\n<blockquote>\n <p>网上查到有程序提出做个工具随机生成关卡,自动计算,把符合得分条件的关卡筛选出来</p>\n</blockquote>\n<p>这个解决方案代价是昂贵的!笔者提供有源码并没有解决这个问题,而是用一个比较取巧的方法:<strong>进入游戏前检查是事为「无法消除矩阵」,如果是重新生成关卡矩阵</strong>。</p>\n<p>注意:笔者使用的取巧方案并没有解决问题。</p>\n<h2 id=\"7-结语\" class=\"post-heading\"><a href=\"#7-结语\" class=\"headerlink\" title=\"7. 结语\"></a>7. 结语<a class=\"post-anchor\" href=\"#7-结语\" aria-hidden=\"true\"></a></h2>\n<p>下面是本文介绍的「消灭星星」的线上 DEMO 的二维码:</p>\n<p><img src=\"https://misc.aotu.io/leeenx/popstar/qr.jpg\" alt=\"二维码\"></p>\n<p>游戏的源码托管在:<a href=\"https://github.com/leeenx/popstar\" target=\"_blank\" rel=\"external\">https://github.com/leeenx/popstar</a></p>\n<p>感谢耐心阅读完本文章的读者。本文仅代表笔者的个人观点,如有不妥之处请不吝赐教。<br>如果对「H5游戏开发」感兴趣,欢迎关注我们的<a href=\"https://zhuanlan.zhihu.com/snsgame\" target=\"_blank\" rel=\"external\">专栏</a>。</p>\n<h2 id=\"参考资料\" class=\"post-heading\"><a href=\"#参考资料\" class=\"headerlink\" title=\"参考资料\"></a>参考资料<a class=\"post-anchor\" href=\"#参考资料\" aria-hidden=\"true\"></a></h2>\n<ul>\n <li><a href=\"https://en.wikipedia.org/wiki/Knapsack_problem\" target=\"_blank\" rel=\"external\">Knapsack problem</a></li>\n <li><a href=\"https://en.wikipedia.org/wiki/NP-completeness\" target=\"_blank\" rel=\"external\">NP-completeness</a></li>\n <li><a href=\"https://www.zhihu.com/question/22530789\" target=\"_blank\" rel=\"external\">popstar关卡是如何设计的?</a></li>\n <li><a href=\"https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle\" target=\"_blank\" rel=\"external\">费雪耶兹乱序算法</a></li>\n <li><a href=\"https://aotu.io/notes/2018/01/11/waveaverage/\">波动均分算法</a></li>\n</ul>\n<div class=\"post-tags\" style=\"display:none\">\n <a href=\"/tags/游戏/\">游戏</a> \n <a href=\"/tags/H5/\">H5</a> \n <a href=\"/tags/canvas/\">canvas</a> \n <a href=\"/tags/game/\">game</a> \n <a href=\"/tags/消灭星星/\">消灭星星</a> \n <a href=\"/tags/popstar/\">popstar</a> \n <a href=\"/tags/pixijs/\">pixijs</a>\n</div>\n<div class=\"post-categories\" style=\"display:none\">\n <a href=\"/cates/H5游戏开发/\">H5游戏开发</a>\n</div>\n<div class=\"post-announce\">\n 感谢您的阅读,本文由 \n <a href=\"//aotu.io\">凹凸实验室</a> 版权所有。如若转载,请注明出处:凹凸实验室(\n <a href=\"https://aotu.io/notes/2018/01/17/popstar/\">https://aotu.io/notes/2018/01/17/popstar/</a>)\n</div>\n<div class=\"post-revision\">\n <i class=\"fa fa-clock-o\"></i> \n <time class=\"post-updated\" datetime=\"2018-11-07T01:01:15.657Z\">上次更新:2018-11-07 09:01:15</time>\n</div>');
INSERT INTO `rumo_blog` VALUES ('25', 'H5游戏开发:推金币', '1', 'H5游戏开发:推金币', '1', '2018-11-11 21:02:33', '2018-11-11 21:02:33', '1', '0', 'https://misc.aotu.io/samgui/coindozer/cover.jpg?v=1', '', '0', '0', '<a id=\"more\"></a>\n<p>近期参与开发的一款「京东11.11推金币赢现金」(已下线)小游戏一经发布上线就在朋友圈引起大量传播。看到大家玩得不亦乐乎,同时也引发不少网友激烈<a href=\"https://www.smzdm.com/p/7990202/\" target=\"_blank\" rel=\"external\">讨论</a>,有的说很带劲,有的大呼被套路被耍猴(无奈脸),这都与我的预期相去甚远。在相关业务数据呈呈上涨过程中,曾一度被微信「有关部门」盯上并要求做出调整,真是受宠若惊。接下来就跟大家分享下开发这款游戏的心路历程。</p>\n<h3 id=\"背景介绍\" class=\"post-heading\"><a href=\"#背景介绍\" class=\"headerlink\" title=\"背景介绍\"></a>背景介绍<a class=\"post-anchor\" href=\"#背景介绍\" aria-hidden=\"true\"></a></h3>\n<p>一年一度的双十一狂欢购物节即将拉开序幕,H5 互动类小游戏作为京东微信手Q营销特色玩法,在今年预热期的第一波造势中,势必要玩点新花样,主要肩负着社交传播和发券的目的。推金币以传统街机推币机为原型,结合手机强大的能力和生态衍生出可玩性很高的玩法。</p>\n<h3 id=\"前期预研\" class=\"post-heading\"><a href=\"#前期预研\" class=\"headerlink\" title=\"前期预研\"></a>前期预研<a class=\"post-anchor\" href=\"#前期预研\" aria-hidden=\"true\"></a></h3>\n<p>在体验过 AppStore 上好几款推金币游戏 App 后,发现游戏核心模型还是挺简单的,不过 H5 版本的实现在网上很少见。由于团队一直在做 2D 类互动小游戏,在 3D 方向暂时没有实际的项目输出,然后结合此次游戏的特点,一开始想挑战用 3D 来实现,并以此项目为突破口,跟设计师进行深度合作,抹平开发过程的各种障碍。</p>\n<p><img src=\"https://misc.aotu.io/samgui/coindozer/app.jpg?v=1\" alt=\"推金币 App\"></p>\n<p>由于时间紧迫,需要在短时间内敲定方案可行性,否则项目延期人头不保。在快速尝试了 <code>Three.js + Ammo.js</code> 方案后,发现不尽人意,最终因为各方面原因放弃了 3D 方案,主要是不可控因素太多:时间上、设计及技术经验上、移动端 WebGL 性能表现上,主要还是业务上需要对游戏有绝对的控制,加上是第一次接手复杂的小游戏,担心项目无法正常上线,有点保守,此方案遂卒。</p>\n<p>如果读者有兴趣的话可以尝试下 3D 实现,在建模方面,首推 <a href=\"http://threejs.org/\" target=\"_blank\" rel=\"external\">Three.js</a> ,入手非常简单,文档和案例也非常详实。当然入门的话必推这篇 <a href=\"https://read.douban.com/ebook/7412854/\" target=\"_blank\" rel=\"external\">Three.js入门指南</a>,另外同事分享的这篇 <a href=\"https://aotu.io/notes/2017/08/28/getting-started-with-threejs/\">Three.js 现学现卖</a> 也可以看看,这里奉上粗糙的 <a href=\"http://samgui.com/coindozer-3d/\" target=\"_blank\" rel=\"external\">推金币 3D 版 Demo</a></p>\n<h3 id=\"技术选型\" class=\"post-heading\"><a href=\"#技术选型\" class=\"headerlink\" title=\"技术选型\"></a>技术选型<a class=\"post-anchor\" href=\"#技术选型\" aria-hidden=\"true\"></a></h3>\n<p>放弃了 3D 方案,在 2D 技术选型上就很从容了,最终确定用 <code>CreateJS + Matter.js</code> 组合作为渲染引擎和物理引擎,理由如下:</p>\n<ul>\n <li><strong>CreateJS</strong> 在团队内用得比较多,有一定的沉淀,加上有老司机带路,一个字「稳」;</li>\n <li><strong>Matter.js</strong> 身材纤细、文档友好,也有同事<a href=\"https://aotu.io/notes/2017/04/17/Matter-js/\">试玩过</a>,完成需求绰绰有余。</li>\n</ul>\n<h3 id=\"技术实现\" class=\"post-heading\"><a href=\"#技术实现\" class=\"headerlink\" title=\"技术实现\"></a>技术实现<a class=\"post-anchor\" href=\"#技术实现\" aria-hidden=\"true\"></a></h3>\n<p>因为是 2D 版本,所以不需要建各种模型和贴图,整个游戏场景通过 canvas 绘制,覆盖在背景图上,然后再做下机型适配问题,游戏主场景就处理得差不多了,其他跟 3D 思路差不多,核心元素包含障碍物、推板、金币、奖品和技能,接下来就分别介绍它们的实现思路。</p>\n<h4 id=\"障碍物\" class=\"post-heading\"><a href=\"#障碍物\" class=\"headerlink\" title=\"障碍物\"></a>障碍物<a class=\"post-anchor\" href=\"#障碍物\" aria-hidden=\"true\"></a></h4>\n<p>通过审稿确定金币以及奖品的活动区域,然后把活动区域之外的区域都作为障碍物,用来限制金币的移动范围,防止金币碰撞时超出边界。这里可以用 Matter.js 的 <code>Bodies.fromVertices</code> 方法,通过传入边界各转角的顶点坐标一次性绘制出形状不规则的障碍物。 不过 Matter.js 在渲染不规则形状时存在问题,需要引入 <a href=\"https://github.com/schteppe/poly-decomp.js\" target=\"_blank\" rel=\"external\">poly-decomp</a> 做兼容处理。</p>\n<p><img src=\"https://misc.aotu.io/samgui/coindozer/barrier.jpg?v=1\" alt=\"障碍物\"></p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n World.add(\n <span class=\"keyword\">this</span>.world, [\n </div>\n <div class=\"line\">\n Bodies.fromVertices(\n <span class=\"number\">282</span>, \n <span class=\"number\">332</span>,[\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 顶点坐标</span>\n </div>\n <div class=\"line\">\n { \n <span class=\"attr\">x</span>: \n <span class=\"number\">0</span>, \n <span class=\"attr\">y</span>: \n <span class=\"number\">0</span> },\n </div>\n <div class=\"line\">\n { \n <span class=\"attr\">x</span>: \n <span class=\"number\">0</span>, \n <span class=\"attr\">y</span>: \n <span class=\"number\">890</span> },\n </div>\n <div class=\"line\">\n { \n <span class=\"attr\">x</span>: \n <span class=\"number\">140</span>, \n <span class=\"attr\">y</span>: \n <span class=\"number\">815</span> },\n </div>\n <div class=\"line\">\n { \n <span class=\"attr\">x</span>: \n <span class=\"number\">208</span>, \n <span class=\"attr\">y</span>: \n <span class=\"number\">614</span> },\n </div>\n <div class=\"line\">\n { \n <span class=\"attr\">x</span>: \n <span class=\"number\">548</span>, \n <span class=\"attr\">y</span>: \n <span class=\"number\">614</span> },\n </div>\n <div class=\"line\">\n { \n <span class=\"attr\">x</span>: \n <span class=\"number\">612</span>, \n <span class=\"attr\">y</span>: \n <span class=\"number\">815</span> },\n </div>\n <div class=\"line\">\n { \n <span class=\"attr\">x</span>: \n <span class=\"number\">750</span>, \n <span class=\"attr\">y</span>: \n <span class=\"number\">890</span> },\n </div>\n <div class=\"line\">\n { \n <span class=\"attr\">x</span>: \n <span class=\"number\">750</span>, \n <span class=\"attr\">y</span>: \n <span class=\"number\">0</span> }\n </div>\n <div class=\"line\">\n ])\n </div>\n <div class=\"line\">\n ]);\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h4 id=\"推板\" class=\"post-heading\"><a href=\"#推板\" class=\"headerlink\" title=\"推板\"></a>推板<a class=\"post-anchor\" href=\"#推板\" aria-hidden=\"true\"></a></h4>\n<ul>\n <li><strong>创建</strong>:CreateJS 根据推板图片创建 Bitmap 对象比较简单,就不详细讲解了。这里着重讲下推板刚体的创建,主要是跟推板 Bitmap 信息进行同步。因为推板视觉上表现为梯形,所以这里用的梯形刚体,实际上方形也可以,只要能跟周围障碍物形成封闭区域,防止出现缝隙卡住金币即可,创建的刚体直接挂载到推板对象上,方便后续随时提取(金币的处理也是一样),代码大致如下:</li>\n</ul>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">var</span> bounds = \n <span class=\"keyword\">this</span>.pusher.getBounds();\n </div>\n <div class=\"line\">\n <span class=\"keyword\">this</span>.pusher.body = Matter.Bodies.trapezoid(\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">this</span>.pusher.x,\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">this</span>.pusher.y,\n </div>\n <div class=\"line\">\n bounds.width,\n </div>\n <div class=\"line\">\n bounds.height\n </div>\n <div class=\"line\">\n });\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n Matter.World.add(\n <span class=\"keyword\">this</span>.world, [\n <span class=\"keyword\">this</span>.pusher.body]);\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<ul>\n <li><strong>伸缩</strong>:由于推板会沿着视线方向前后移动,为了达到近大远小效果,所以需要在推板伸长和收缩过程中进行缩放处理,这样也可以跟两侧的障碍物边沿进行贴合,让场景看起来更具真实感(伪 3D),当然金币和奖品也需要进行同样的处理。由于推板是自驱动做前后伸缩移动,所以需要对推板及其对应的刚体进行位置同步,这样才会与金币刚体产生碰撞达到推动金币的效果。同时在外部改变(伸长技能)推板最大长度时,也需要让推板保持均匀的缩放比而不至于突然放大/缩小,所以整个推板代码逻辑包含方向控制、长度控制、速度控制、缩放控制和同步控制,代码大致如下:</li>\n</ul>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div>\n <div class=\"line\">\n 16\n </div>\n <div class=\"line\">\n 17\n </div>\n <div class=\"line\">\n 18\n </div>\n <div class=\"line\">\n 19\n </div>\n <div class=\"line\">\n 20\n </div>\n <div class=\"line\">\n 21\n </div>\n <div class=\"line\">\n 22\n </div>\n <div class=\"line\">\n 23\n </div>\n <div class=\"line\">\n 24\n </div>\n <div class=\"line\">\n 25\n </div>\n <div class=\"line\">\n 26\n </div>\n <div class=\"line\">\n 27\n </div>\n <div class=\"line\">\n 28\n </div>\n <div class=\"line\">\n 29\n </div>\n <div class=\"line\">\n 30\n </div>\n <div class=\"line\">\n 31\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">var</span> direction, velocity, ratio, deltaY, minY = \n <span class=\"number\">550</span>, maxY = \n <span class=\"number\">720</span>, minScale = \n <span class=\"number\">.74</span>;\n </div>\n <div class=\"line\">\n Matter.Events.on(\n <span class=\"keyword\">this</span>.engine, \n <span class=\"string\">\'beforeUpdate\'</span>, \n <span class=\"function\"><span class=\"keyword\">function</span> (<span class=\"params\">event</span>) </span>{\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 长度控制(点击伸长技能时)</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span> (\n <span class=\"keyword\">this</span>.isPusherLengthen) {\n </div>\n <div class=\"line\">\n velocity = \n <span class=\"number\">90</span>;\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">this</span>.pusherMaxY = maxY;\n </div>\n <div class=\"line\">\n } \n <span class=\"keyword\">else</span> {\n </div>\n <div class=\"line\">\n velocity = \n <span class=\"number\">85</span>;\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">this</span>.pusherMaxY = \n <span class=\"number\">620</span>;\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"></div>\n <div class=\"line\"> \n <span class=\"comment\">// 方向控制</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span> (\n <span class=\"keyword\">this</span>.pusher.y &gt;= \n <span class=\"keyword\">this</span>.pusherMaxY) {\n </div>\n <div class=\"line\">\n direction = \n <span class=\"number\">-1</span>;\n </div>\n <div class=\"line\"></div>\n <div class=\"line\"> \n <span class=\"comment\">// 移动到最大长度时结束伸长技能</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">this</span>.isPusherLengthen = \n <span class=\"literal\">false</span>;\n </div>\n <div class=\"line\">\n } \n <span class=\"keyword\">else</span> \n <span class=\"keyword\">if</span> (\n <span class=\"keyword\">this</span>.pusher.y &lt;= \n <span class=\"keyword\">this</span>.pusherMinY) {\n </div>\n <div class=\"line\">\n direction = \n <span class=\"number\">1</span>;\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"></div>\n <div class=\"line\"> \n <span class=\"comment\">// 速度控制</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">this</span>.pusher.y += direction * velocity;\n </div>\n <div class=\"line\"></div>\n <div class=\"line\"> \n <span class=\"comment\">// 缩放控制,在最大长度变化时保持同样的缩放量,防止突然放大/缩小</span>\n </div>\n <div class=\"line\">\n ratio = (\n <span class=\"number\">1</span> - minScale) * ((\n <span class=\"keyword\">this</span>.pusher.y - minY) / (maxY - minY))\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">this</span>.pusher.scaleX = \n <span class=\"keyword\">this</span>.pusher.scaleY = minScale + ratio;\n </div>\n <div class=\"line\"></div>\n <div class=\"line\"> \n <span class=\"comment\">// 同步控制,刚体跟推板位置同步</span>\n </div>\n <div class=\"line\">\n Body.setPosition(\n <span class=\"keyword\">this</span>.pusher.body, { \n <span class=\"attr\">x</span>: \n <span class=\"keyword\">this</span>.pusher.x, \n <span class=\"attr\">y</span>: \n <span class=\"keyword\">this</span>.pusher.y });\n </div>\n <div class=\"line\">\n })\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<ul>\n <li><strong>遮罩</strong>:推板伸缩实际上是通过改变坐标来达到位置上的变化,这样存在一个问题,就是在其伸缩时必然会导致缩进的部分「溢出」边界而不是被遮挡。</li>\n</ul>\n<p><img src=\"https://misc.aotu.io/samgui/coindozer/overflow.jpg?v=1\" alt=\"推板溢出\"></p>\n<p>所以需要做遮挡处理,这里用 CreateJS 的 mask 遮罩属性可以很好的做「溢出」裁剪:</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">var</span> shape = \n <span class=\"keyword\">new</span> createjs.Shape();\n </div>\n <div class=\"line\">\n shape.graphics.beginFill(\n <span class=\"string\">\'#ffffff\'</span>).drawRect(\n <span class=\"number\">0</span>, \n <span class=\"number\">612</span>, \n <span class=\"number\">750</span>, \n <span class=\"number\">220</span>);\n </div>\n <div class=\"line\">\n <span class=\"keyword\">this</span>.pusher.mask = shape\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>最终效果如下:</p>\n<p><img src=\"https://misc.aotu.io/samgui/coindozer/normal.jpg?v=1\" alt=\"正常表现\"></p>\n<h4 id=\"金币\" class=\"post-heading\"><a href=\"#金币\" class=\"headerlink\" title=\"金币\"></a>金币<a class=\"post-anchor\" href=\"#金币\" aria-hidden=\"true\"></a></h4>\n<p>按正常思路,应该在点击屏幕时就在出币口创建金币刚体,让其在重力作用下自然掉落和回弹。但是在调试过程中发现,金币掉落后跟台面上其他金币产生碰撞会导致乱飞现象,甚至会卡到障碍物里面去(原因暂未知),后面改成用 TweenJS 的 <code>Ease.bounceOut</code> 来实现金币掉落动画,让金币掉落变得更可控,同时尽量接近自然掉落效果。这样金币从创建到消失过程就被拆分成了三个阶段:</p>\n<ul>\n <li>第一阶段</li>\n</ul>\n<p>点击屏幕从左右移动的出币口创建金币,然后掉落到台面。需要注意的是,由于创建金币时是通过 <code>appendChild</code> 方式加入到舞台的,这样金币会非常有规律的在 z 轴方向上叠加,看起来非常怪异,所以需要随机设置金币的 z-index,让金币叠加更自然,伪代码如下:</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">var</span> index = Utils.getRandomInt(\n <span class=\"number\">1</span>, Game.coinContainer.getNumChildren());\n </div>\n <div class=\"line\">\n Game.coinContainer.setChildIndex(\n <span class=\"keyword\">this</span>.coin, index);\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<ul>\n <li>第二阶段</li>\n</ul>\n<p>由于金币已经不需要重力场,所以需要设置物理世界的重力为 0,这样金币不会因为自身重量(需要设置重量来控制碰撞时移动的速度)做自由落体运动,安安静静的平躺在台面上,等待跟推板、其他金币和障碍物之间产生碰撞:</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">this</span>.engine = Matter.Engine.create();\n </div>\n <div class=\"line\">\n <span class=\"keyword\">this</span>.engine.world.gravity.y = \n <span class=\"number\">0</span>;\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>由于游戏主要逻辑都集中这个阶段,所以处理起来会稍微复杂些。真实情况下如果金币掉落并附着在推板上后,会跟随推板的伸缩而被带动,最终在推板缩进到最短时被背后的墙壁阻挡而挤下推板,此过程看起来简单但实现起来会非常耗时,最后因为时间上紧迫的这里也做了简化处理,就是不管推板是伸长还是缩进,都让推板上的金币向前「滑行」尽快脱离推板。<strong>一旦金币离开推板则立即为其创建同步的刚体</strong>,为后续的碰撞做准备,这样就完成了金币的碰撞处理。</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div>\n <div class=\"line\">\n 16\n </div>\n <div class=\"line\">\n 17\n </div>\n <div class=\"line\">\n 18\n </div>\n <div class=\"line\">\n 19\n </div>\n <div class=\"line\">\n 20\n </div>\n <div class=\"line\">\n 21\n </div>\n <div class=\"line\">\n 22\n </div>\n <div class=\"line\">\n 23\n </div>\n <div class=\"line\">\n 24\n </div>\n <div class=\"line\">\n 25\n </div>\n <div class=\"line\">\n 26\n </div>\n <div class=\"line\">\n 27\n </div>\n <div class=\"line\">\n 28\n </div>\n <div class=\"line\">\n 29\n </div>\n <div class=\"line\">\n 30\n </div>\n <div class=\"line\">\n 31\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n Matter.Events.on(\n <span class=\"keyword\">this</span>.engine, \n <span class=\"string\">\'beforeUpdate\'</span>, \n <span class=\"function\"><span class=\"keyword\">function</span> (<span class=\"params\">event</span>) </span>{\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 处理金币与推板碰撞</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">for</span> (\n <span class=\"keyword\">var</span> i = \n <span class=\"number\">0</span>; i &lt; \n <span class=\"keyword\">this</span>.coins.length; i++) {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">var</span> coin = \n <span class=\"keyword\">this</span>.coins[i];\n </div>\n <div class=\"line\"></div>\n <div class=\"line\"> \n <span class=\"comment\">// 金币在推板上</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span> (coin.sprite.y &lt; \n <span class=\"keyword\">this</span>.pusher.y) {\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 无论推板伸长/缩进金币都往前移动</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span> (deltaY &gt; \n <span class=\"number\">0</span>) {\n </div>\n <div class=\"line\">\n coin.sprite.y += deltaY;\n </div>\n <div class=\"line\">\n } \n <span class=\"keyword\">else</span> {\n </div>\n <div class=\"line\">\n coin.sprite.y -= deltaY;\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"></div>\n <div class=\"line\"> \n <span class=\"comment\">// 金币缩放</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span> (coin.sprite.scaleX &lt; \n <span class=\"number\">1</span>) {\n </div>\n <div class=\"line\">\n coin.sprite.scaleX += \n <span class=\"number\">0.001</span>;\n </div>\n <div class=\"line\">\n coin.sprite.scaleY += \n <span class=\"number\">0.001</span>;\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n } \n <span class=\"keyword\">else</span> {\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 更新刚体坐标</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span> (coin.body) {\n </div>\n <div class=\"line\">\n Matter.Body.set(coin.body, { \n <span class=\"attr\">position</span>: { \n <span class=\"attr\">x</span>: coin.sprite.x, \n <span class=\"attr\">y</span>: coin.sprite.y } })\n </div>\n <div class=\"line\">\n } \n <span class=\"keyword\">else</span> {\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 金币离开推板则创建对应刚体</span>\n </div>\n <div class=\"line\">\n coin.body = Matter.Bodies.circle(coin.sprite.x, coin.sprite.y);\n </div>\n <div class=\"line\">\n Matter.World.add(\n <span class=\"keyword\">this</span>.world, [coin.body]);\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n })\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<ul>\n <li>第三阶段</li>\n</ul>\n<p>随着金币不断的投放、碰撞和移动,最终金币会从台面的下边沿掉落并消失,此阶段的处理同第一阶段,这里就不重复了。</p>\n<h4 id=\"奖品\" class=\"post-heading\"><a href=\"#奖品\" class=\"headerlink\" title=\"奖品\"></a>奖品<a class=\"post-anchor\" href=\"#奖品\" aria-hidden=\"true\"></a></h4>\n<p>由于奖品需要根据业务情况进行控制,所以把它跟金币进行了分离不做碰撞处理(内心是拒绝的),所以产生了「螃蟹步」现象,这里就不做过多介绍了。</p>\n<h4 id=\"技能设计\" class=\"post-heading\"><a href=\"#技能设计\" class=\"headerlink\" title=\"技能设计\"></a>技能设计<a class=\"post-anchor\" href=\"#技能设计\" aria-hidden=\"true\"></a></h4>\n<p>写好游戏主逻辑之后,技能就属于锦上添花的事情了,不过让游戏更具可玩性,想想金币哗啦啦往下掉的感觉还是很棒的。</p>\n<p><strong>抖动</strong>:这里取了个巧,是给舞台容器添加了 CSS3 实现的抖动效果,然后在抖动时间内让所有的金币的 y 坐标累加固定值产生整体慢慢前移效果,由于安卓下支持系统震动 API,所以加了个彩蛋让游戏体验更真实。</p>\n<p>CSS3 抖动实现主要是参考了 <a href=\"http://elrumordelaluz.github.io/csshake/\" target=\"_blank\" rel=\"external\">csshake</a> 这个样式,非常有意思的一组抖动动画集合。</p>\n<p>JS 抖动 API</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">// 安卓震动</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">if</span> (isAndroid) {\n </div>\n <div class=\"line\"> \n <span class=\"built_in\">window</span>.navigator.vibrate = navigator.vibrate || navigator.webkitVibrate || navigator.mozVibrate || navigator.msVibrate;\n </div>\n <div class=\"line\"> \n <span class=\"built_in\">window</span>.navigator.vibrate([\n <span class=\"number\">100</span>, \n <span class=\"number\">30</span>, \n <span class=\"number\">100</span>, \n <span class=\"number\">30</span>, \n <span class=\"number\">100</span>, \n <span class=\"number\">200</span>, \n <span class=\"number\">200</span>, \n <span class=\"number\">30</span>, \n <span class=\"number\">200</span>, \n <span class=\"number\">30</span>, \n <span class=\"number\">200</span>, \n <span class=\"number\">200</span>, \n <span class=\"number\">100</span>, \n <span class=\"number\">30</span>, \n <span class=\"number\">100</span>, \n <span class=\"number\">30</span>, \n <span class=\"number\">100</span>]);\n </div>\n <div class=\"line\"> \n <span class=\"built_in\">window</span>.navigator.vibrate(\n <span class=\"number\">0</span>); \n <span class=\"comment\">// 停止抖动</span>\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p><strong>伸长</strong>:伸长处理也很简单,通过改变推板移动的最大 y 坐标值让金币产生更大的移动距离,不过细节上有几点需要注意的地方,在推板最大 y 坐标值改变之后需要保持移动速度不变,不然就会产生「瞬移」(不平滑)问题。</p>\n<h4 id=\"调试方法\" class=\"post-heading\"><a href=\"#调试方法\" class=\"headerlink\" title=\"调试方法\"></a>调试方法<a class=\"post-anchor\" href=\"#调试方法\" aria-hidden=\"true\"></a></h4>\n<p>由于用了物理引擎,当在创建刚体时需要跟 CreateJS 图形保持一致,这里可以利用 Matter.js 自带的 Render 为物理场景独立创建一个透明的渲染层,然后覆盖在 CreateJS 场景之上,这里贴出大致代码:</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n Matter.Render.create({\n </div>\n <div class=\"line\"> \n <span class=\"attr\">element</span>: \n <span class=\"built_in\">document</span>.getElementById(\n <span class=\"string\">\'debugger-canvas\'</span>),\n </div>\n <div class=\"line\"> \n <span class=\"attr\">engine</span>: \n <span class=\"keyword\">this</span>.engine,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">options</span>: {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">width</span>: \n <span class=\"number\">750</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">height</span>: \n <span class=\"number\">1206</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">showVelocity</span>: \n <span class=\"literal\">true</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">wireframes</span>: \n <span class=\"literal\">false</span> \n <span class=\"comment\">// 设置为非线框,刚体才可以渲染出颜色</span>\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n });\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>设置刚体的 render 属性为半透明色块,方便观察和调试,这里以推板为例:</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">this</span>.pusher.body = Matter.Bodies.trapezoid(\n </div>\n <div class=\"line\">\n ... \n <span class=\"comment\">// 略</span>\n </div>\n <div class=\"line\">\n {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">isStatic</span>: \n <span class=\"literal\">true</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">render</span>: {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">opacity</span>: \n <span class=\"number\">.5</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">fillStyle</span>: \n <span class=\"string\">\'red\'</span>\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n });\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>效果如下,调试起来还是很方便的:</p>\n<p><img src=\"https://misc.aotu.io/samgui/coindozer/debugger.gif\" alt=\"调试模式\"></p>\n<h3 id=\"性能/体验优化\" class=\"post-heading\"><a href=\"#性能/体验优化\" class=\"headerlink\" title=\"性能/体验优化\"></a>性能/体验优化<a class=\"post-anchor\" href=\"#性能/体验优化\" aria-hidden=\"true\"></a></h3>\n<h4 id=\"控制对象数量\" class=\"post-heading\"><a href=\"#控制对象数量\" class=\"headerlink\" title=\"控制对象数量\"></a>控制对象数量<a class=\"post-anchor\" href=\"#控制对象数量\" aria-hidden=\"true\"></a></h4>\n<p>随着游戏的持续台面上累积的金币数量会不断增加,金币之间的碰撞计算量也会陡增,必然会导致手机卡顿和发热。这时就需要控制金币的重叠度,而金币之间重叠的区域大小是由金币刚体的尺寸大小决定的,通过适当的调整刚体半径让金币分布得比较均匀,这样可以有效控制金币数量,提升游戏性能。</p>\n<h4 id=\"安卓卡顿\" class=\"post-heading\"><a href=\"#安卓卡顿\" class=\"headerlink\" title=\"安卓卡顿\"></a>安卓卡顿<a class=\"post-anchor\" href=\"#安卓卡顿\" aria-hidden=\"true\"></a></h4>\n<p>一开始是给推板一个固定的速度进行伸缩处理,发现在 iOS 上表现流畅,但是在部分安卓机上却显得差强人意。由于部分安卓机型 FPS 比较低,导致推板在单位时间内位移比较小,表现出来就显得卡顿不流畅。后面让推板位移根据刷新时间差进行递增/减,保证不同帧频机型下都能保持一致的位移,代码大致如下:</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">var</span> delta = \n <span class=\"number\">0</span>, prevTime = \n <span class=\"number\">0</span>;\n </div>\n <div class=\"line\">\n Matter.Events.on(\n <span class=\"keyword\">this</span>.engine, \n <span class=\"string\">\'beforeUpdate\'</span>, \n <span class=\"function\"><span class=\"keyword\">function</span> (<span class=\"params\">event</span>) </span>{\n </div>\n <div class=\"line\">\n delta = event.timestamp - prevTime;\n </div>\n <div class=\"line\">\n prevTime = event.timestamp;\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// ... 略</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">this</span>.pusher.y += direction * velocity * (delta / \n <span class=\"number\">1000</span>)\n </div>\n <div class=\"line\">\n })\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h4 id=\"对象回收\" class=\"post-heading\"><a href=\"#对象回收\" class=\"headerlink\" title=\"对象回收\"></a>对象回收<a class=\"post-anchor\" href=\"#对象回收\" aria-hidden=\"true\"></a></h4>\n<p>这也是游戏开发中常用的优化手段,通过回收从边界消失的对象,让对象得以复用,防止因频繁创建对象而产生大量的内存消耗。</p>\n<h4 id=\"事件销毁\" class=\"post-heading\"><a href=\"#事件销毁\" class=\"headerlink\" title=\"事件销毁\"></a>事件销毁<a class=\"post-anchor\" href=\"#事件销毁\" aria-hidden=\"true\"></a></h4>\n<p>由于金币和奖品生命周期内使用了 Tween,当他们从屏幕上消失后记得移除掉:</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n createjs.Tween.removeTweens(\n <span class=\"keyword\">this</span>.coin);\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>至此,推金币各个关键环节都有讲到了,最后附上一张实际游戏效果:<br><img src=\"https://misc.aotu.io/samgui/coindozer/game.gif\" alt=\"\"></p>\n<h3 id=\"结语\" class=\"post-heading\"><a href=\"#结语\" class=\"headerlink\" title=\"结语\"></a>结语<a class=\"post-anchor\" href=\"#结语\" aria-hidden=\"true\"></a></h3>\n<p>感谢各位耐心读完,希望能有所收获,有考虑不足的地方欢迎留言指出。</p>\n<p>如果对「H5游戏开发」感兴趣,欢迎关注我们的<a href=\"https://zhuanlan.zhihu.com/snsgame\" target=\"_blank\" rel=\"external\">专栏</a>。</p>\n<h3 id=\"相关资源\" class=\"post-heading\"><a href=\"#相关资源\" class=\"headerlink\" title=\"相关资源\"></a>相关资源<a class=\"post-anchor\" href=\"#相关资源\" aria-hidden=\"true\"></a></h3>\n<p><a href=\"http://threejs.org/\" target=\"_blank\" rel=\"external\">Three.js 官网</a></p>\n<p><a href=\"https://read.douban.com/ebook/7412854/\" target=\"_blank\" rel=\"external\">Three.js入门指南</a></p>\n<p><a href=\"https://aotu.io/notes/2017/08/28/getting-started-with-threejs/\">Three.js 现学现卖</a></p>\n<p><a href=\"brm.io/matter-js/\">Matter.js 官网</a></p>\n<p><a href=\"https://aotu.io/notes/2017/04/17/Matter-js/\">Matter.js 2D 物理引擎试玩报告</a></p>\n<style>.post-content img{display:block;margin:20px auto}</style>\n<div class=\"post-tags\" style=\"display:none\">\n <a href=\"/tags/游戏/\">游戏</a> \n <a href=\"/tags/createjs/\">createjs</a> \n <a href=\"/tags/h5/\">h5</a> \n <a href=\"/tags/canvas/\">canvas</a> \n <a href=\"/tags/game/\">game</a> \n <a href=\"/tags/推金币/\">推金币</a> \n <a href=\"/tags/matter-js/\">matter.js</a>\n</div>\n<div class=\"post-categories\" style=\"display:none\">\n <a href=\"/cates/H5游戏开发/\">H5游戏开发</a>\n</div>\n<div class=\"post-announce\">\n 感谢您的阅读,本文由 \n <a href=\"//aotu.io\">凹凸实验室</a> 版权所有。如若转载,请注明出处:凹凸实验室(\n <a href=\"https://aotu.io/notes/2017/11/06/coindozer/\">https://aotu.io/notes/2017/11/06/coindozer/</a>)\n</div>\n<div class=\"post-revision\">\n <i class=\"fa fa-clock-o\"></i> \n <time class=\"post-updated\" datetime=\"2018-11-07T01:01:15.653Z\">上次更新:2018-11-07 09:01:15</time>\n</div>');
INSERT INTO `rumo_blog` VALUES ('26', 'Taro 在京东购物小程序上的实践', '1', 'Taro 在京东购物小程序上的实践', '1', '2018-11-11 21:02:33', '2018-11-11 21:02:33', '1', '0', 'https://storage.360buyimg.com/mtd/home/571536653828_-pic1536655025757.jpg', '', '0', '0', '<a id=\"more\"></a>\n<h2 id=\"Taro-简介\" class=\"post-heading\"><a href=\"#Taro-简介\" class=\"headerlink\" title=\"Taro 简介\"></a>Taro 简介<a class=\"post-anchor\" href=\"#Taro-简介\" aria-hidden=\"true\"></a></h2>\n<p><code>Taro</code> 是一个基于 <code>React</code> 语法规范的多端统一开发框架,大家可以通过 <a href=\"https://taro.aotu.io/\" target=\"_blank\" rel=\"external\">taro.aotu.io</a> 进一步了解。而前段时间 Taro 发布后,京东购物小程序就开始了部分页面基于 Taro 的重构工作,本文便是对商品分类页使用 Taro 进行代码重构的一些实践分享。</p>\n<h2 id=\"混合开发模式\" class=\"post-heading\"><a href=\"#混合开发模式\" class=\"headerlink\" title=\"混合开发模式\"></a>混合开发模式<a class=\"post-anchor\" href=\"#混合开发模式\" aria-hidden=\"true\"></a></h2>\n<p>过去的京东购物小程序未使用任何第三方框架,而是在原生小程序模式的基础上,进行了页面/组件基类、网络请求、本地存储、页面跳转等模块的封装。由于项目庞大(涉及 100 多个页面),把整个项目直接改造成 Taro 的开发方式肯定是不可行的,于是采用这么一种原生小程序与 Taro 相混合的开发模式,将部分旧页面使用 Taro 重构,部分新的页面则直接使用 Taro 进行开发。这里以商品分类页为例,先来看下原京东购物小程序项目的目录结构:</p>\n<figure class=\"highlight stylus\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div>\n <div class=\"line\">\n 16\n </div>\n <div class=\"line\">\n 17\n </div>\n <div class=\"line\">\n 18\n </div>\n <div class=\"line\">\n 19\n </div>\n <div class=\"line\">\n 20\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n ├── dist\n </div>\n <div class=\"line\">\n │ ├── app\n <span class=\"selector-class\">.js</span>\n </div>\n <div class=\"line\">\n │ ├── app\n <span class=\"selector-class\">.json</span>\n </div>\n <div class=\"line\">\n │ ├── app\n <span class=\"selector-class\">.wxss</span>\n </div>\n <div class=\"line\">\n │ ├── assets/\n </div>\n <div class=\"line\">\n │ ├── common/\n </div>\n <div class=\"line\">\n │ ├── libs/\n </div>\n <div class=\"line\">\n │ └── pages\n </div>\n <div class=\"line\">\n │ ├── cate\n </div>\n <div class=\"line\">\n │ │ ├── components/\n </div>\n <div class=\"line\">\n │ │ ├── index\n <span class=\"selector-class\">.js</span>\n </div>\n <div class=\"line\">\n │ │ ├── index\n <span class=\"selector-class\">.json</span>\n </div>\n <div class=\"line\">\n │ │ ├── index\n <span class=\"selector-class\">.wxml</span>\n </div>\n <div class=\"line\">\n │ │ └── index\n <span class=\"selector-class\">.wxss</span>\n </div>\n <div class=\"line\">\n │ └── index/\n </div>\n <div class=\"line\">\n ├── src/\n </div>\n <div class=\"line\">\n ├── README\n <span class=\"selector-class\">.md</span>\n </div>\n <div class=\"line\">\n ├── gulpfile\n <span class=\"selector-class\">.js</span>\n </div>\n <div class=\"line\">\n ├── package\n <span class=\"selector-class\">.json</span>\n </div>\n <div class=\"line\">\n └── node_modules/\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h3 id=\"1-初始化-Taro\" class=\"post-heading\"><a href=\"#1-初始化-Taro\" class=\"headerlink\" title=\"1. 初始化 Taro\"></a>1. 初始化 Taro<a class=\"post-anchor\" href=\"#1-初始化-Taro\" aria-hidden=\"true\"></a></h3>\n<p>在项目根目录处运行命令 <code>taro init jdwxa-taro</code> 进行初始化,完成后会新增一个名为 <code>jdwxa-taro</code> 的目录,Taro 相关的源代码就写在该目录中:</p>\n<figure class=\"highlight stylus\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div>\n <div class=\"line\">\n 16\n </div>\n <div class=\"line\">\n 17\n </div>\n <div class=\"line\">\n 18\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n ├── dist/\n </div>\n <div class=\"line\">\n ├── src/\n </div>\n <div class=\"line\">\n └── jdwxa-taro\n </div>\n <div class=\"line\">\n ├── config\n </div>\n <div class=\"line\">\n │ ├── dev\n <span class=\"selector-class\">.js</span>\n </div>\n <div class=\"line\">\n │ ├── index\n <span class=\"selector-class\">.js</span>\n </div>\n <div class=\"line\">\n │ └── prod\n <span class=\"selector-class\">.js</span>\n </div>\n <div class=\"line\">\n ├── node_modules/\n </div>\n <div class=\"line\">\n ├── package\n <span class=\"selector-class\">.json</span>\n </div>\n <div class=\"line\">\n ├── project\n <span class=\"selector-class\">.config</span>\n <span class=\"selector-class\">.json</span>\n </div>\n <div class=\"line\">\n └── src\n </div>\n <div class=\"line\">\n ├── app\n <span class=\"selector-class\">.js</span>\n </div>\n <div class=\"line\">\n ├── app\n <span class=\"selector-class\">.scss</span>\n </div>\n <div class=\"line\">\n ├── index\n <span class=\"selector-class\">.html</span>\n </div>\n <div class=\"line\">\n └── pages\n </div>\n <div class=\"line\">\n └── cate\n </div>\n <div class=\"line\">\n ├── index\n <span class=\"selector-class\">.js</span>\n </div>\n <div class=\"line\">\n └── index.scss\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h3 id=\"2-Taro-配置\" class=\"post-heading\"><a href=\"#2-Taro-配置\" class=\"headerlink\" title=\"2. Taro 配置\"></a>2. Taro 配置<a class=\"post-anchor\" href=\"#2-Taro-配置\" aria-hidden=\"true\"></a></h3>\n<p>独立的 Taro 项目会将包括 <code>app.js</code>、<code>app.json</code>、<code>app.wxss</code> 及页面文件均生成在 dist/ 目录中,而混合开发模式下只需要生成单个页面,这里需要对 Taro 进行一些配置,打开并编辑 <code>config/index.js</code> 文件:</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">const</span> config = {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">outputRoot</span>: \n <span class=\"string\">\'../dist\'</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">weapp</span>: {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">appOutput</span>: \n <span class=\"literal\">false</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">npm</span>: {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">dir</span>: \n <span class=\"string\">\'../../dist/common\'</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">name</span>: \n <span class=\"string\">\'taro\'</span>\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n },\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// ...</span>\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>如代码所示,<code>outputRoot</code> 字段为生成目标页面的存放路径,这里把它指向顶层(即原项目)的 dist/ 目录;<code>weapp</code> 部分,我们把 <code>appOutput</code> 设置成 false, 这样就不会生成 app.js、app.json、app.wxss 三个文件了,<code>npm</code> 字段则表示 Taro 运行时框架文件的存放目录,这里遵循原项目的规范,把它指定为 common/ 目录。这样 Taro 编译生成的目标文件就完美地融入进了原小程序项目。</p>\n<h3 id=\"3-页面开发\" class=\"post-heading\"><a href=\"#3-页面开发\" class=\"headerlink\" title=\"3. 页面开发\"></a>3. 页面开发<a class=\"post-anchor\" href=\"#3-页面开发\" aria-hidden=\"true\"></a></h3>\n<p>页面开发过程中,跟原生小程序最大的不同就是 <code>React</code> + <code>JSX</code> 的编码方式了,习惯了原生小程序的同学可能要一些适应过程,具体的编码就不细说了,这里提几点注意事项:</p>\n<ul>\n <li>与小程序的 <code>setData</code> 方法不同,Taro 用于更新页面数据的 <code>setState</code> 是异步的,相关代码的执行时序需要特别注意;</li>\n <li>为了方便 JSX 模板的书写,原先很长的 WXML 内容建议拆分成一些小的组件;</li>\n <li>关于旧组件的复用,无论是小程序原生组件、普通 JS 模块、样式文件或是第三方组件库,都能很好的进行引入调用,这点无需担心;</li>\n <li>目前对于 Taro 编译生成的目标代码,调试起来会有些困难,但对 <code>SourceMap</code> 的支持正在积极开发中。</li>\n</ul>\n<h3 id=\"4-最终效果\" class=\"post-heading\"><a href=\"#4-最终效果\" class=\"headerlink\" title=\"4. 最终效果\"></a>4. 最终效果<a class=\"post-anchor\" href=\"#4-最终效果\" aria-hidden=\"true\"></a></h3>\n<p>如今重构后的商品分类页已经在线上稳定运行有一段时间了,可以扫描下面的小程序码进行体验:</p>\n<p><img src=\"https://img10.360buyimg.com/img/s360x360_jfs/t1/2914/23/355/264599/5b912565Ecd448c81/fd0362f0724e06e7.jpg\" alt=\"小程序码 of 京东购物小程序 - 商品分类页\"></p>\n<h2 id=\"Taro-带来的收益\" class=\"post-heading\"><a href=\"#Taro-带来的收益\" class=\"headerlink\" title=\"Taro 带来的收益\"></a>Taro 带来的收益<a class=\"post-anchor\" href=\"#Taro-带来的收益\" aria-hidden=\"true\"></a></h2>\n<h3 id=\"多端运行\" class=\"post-heading\"><a href=\"#多端运行\" class=\"headerlink\" title=\"多端运行\"></a>多端运行<a class=\"post-anchor\" href=\"#多端运行\" aria-hidden=\"true\"></a></h3>\n<p>最大的收益便是可以生成多端版本,避免重复工作、节省开发成本。以分类页为例,只需运行 <code>npm run build:h5</code> 便可生成 H5 版本的分类页,运行效果和小程序一致,大家可以扫描下面的二维码进行体验:</p>\n<p><img src=\"https://storage.360buyimg.com/mtd/home/cate-taro-h51536239496540.png\" alt=\"Taro 生成的 H5 版商品分类页\"></p>\n<p>注:以上仅为 Taro 生成的示例页面,由于一些业务组件尚未完全适配两端,所以 H5 版本暂时没有正式投入使用。</p>\n<h3 id=\"性能提升\" class=\"post-heading\"><a href=\"#性能提升\" class=\"headerlink\" title=\"性能提升\"></a>性能提升<a class=\"post-anchor\" href=\"#性能提升\" aria-hidden=\"true\"></a></h3>\n<p>小程序项目中遇到的性能问题,大多是频繁地调用 setData 造成的,这是由于每调用一次 <code>setData</code>,小程序内部都会将该部分数据在逻辑层(运行环境 <code>JSCore</code>)进行类似序列化的操作,将数据转换成字符串形式传递给视图层(运行环境 <code>WebView</code>),视图层通过反序列化拿到数据后再进行页面渲染,这个过程下来有一定性能开销。</p>\n<p>所以开发过程中,我们建议尽量对 <code>setData</code> 进行合并,减少调用次数,例如:</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">this</span>.setData({ \n <span class=\"attr\">foo</span>: \n <span class=\"string\">\'Strawberry\'</span> })\n </div>\n <div class=\"line\">\n <span class=\"keyword\">this</span>.setData({ \n <span class=\"attr\">foo</span>: \n <span class=\"string\">\'Strawberry\'</span>, \n <span class=\"attr\">bar</span>: \n <span class=\"string\">\'Fields\'</span> })\n </div>\n <div class=\"line\">\n <span class=\"keyword\">this</span>.setData({ \n <span class=\"attr\">baz</span>: \n <span class=\"string\">\'Forever\'</span> })\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>以上代码调用了 3 次 <code>setData</code>,造成不必要的性能开销,应对其进行合并:</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">this</span>.setData({\n </div>\n <div class=\"line\"> \n <span class=\"attr\">foo</span>: \n <span class=\"string\">\'Strawberry\'</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">bar</span>: \n <span class=\"string\">\'Fields\'</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">baz</span>: \n <span class=\"string\">\'Forever\'</span>,\n </div>\n <div class=\"line\">\n })\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>而使用 Taro 之后,更新数据时调用的 <code>setState</code> 为异步方法,它会自动地对同一事件循环里的多次 <code>setState</code> 调用进行合并处理,此外还会进行数据 <code>diff</code> 优化,自动剔除那些未变更的数据,从而有效避免了此类性能问题。例如:</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div>\n <div class=\"line\">\n 16\n </div>\n <div class=\"line\">\n 17\n </div>\n <div class=\"line\">\n 18\n </div>\n <div class=\"line\">\n 19\n </div>\n <div class=\"line\">\n 20\n </div>\n <div class=\"line\">\n 21\n </div>\n <div class=\"line\">\n 22\n </div>\n <div class=\"line\">\n 23\n </div>\n <div class=\"line\">\n 24\n </div>\n <div class=\"line\">\n 25\n </div>\n <div class=\"line\">\n 26\n </div>\n <div class=\"line\">\n 27\n </div>\n <div class=\"line\">\n 28\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">// 初始时</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">this</span>.state = {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">foo</span>: \n <span class=\"string\">\'1967\'</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">bar</span>: {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">foo</span>: \n <span class=\"string\">\'Strawberry\'</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">bar</span>: \n <span class=\"string\">\'Fields\'</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">baz</span>: \n <span class=\"string\">\'Forever\'</span>,\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"comment\">// 第一次更新</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">this</span>.setState({\n </div>\n <div class=\"line\"> \n <span class=\"attr\">bar</span>: {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">foo</span>: \n <span class=\"string\">\'Norwegian\'</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">bar</span>: \n <span class=\"string\">\'Fields\'</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">baz</span>: \n <span class=\"string\">\'Forever\'</span>,\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n })\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"comment\">// 紧接着进行第二次更新</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">this</span>.setState({\n </div>\n <div class=\"line\"> \n <span class=\"attr\">foo</span>: \n <span class=\"string\">\'1967\'</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">bar</span>: {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">foo</span>: \n <span class=\"string\">\'Norwegian\'</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">bar</span>: \n <span class=\"string\">\'Wood\'</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">baz</span>: \n <span class=\"string\">\'Forever\'</span>,\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n })\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>以上代码虽然经过两次 <code>setState</code>,但只有 bar.foo 和 bar.bar 的数据更新了,此时 Taro 内部会自动对数据进行合并、并剔除重复数据,最终执行代码为:</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">// this.$scope 在小程序环境中为 page 实例</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">this</span>.$scope.setData({\n </div>\n <div class=\"line\"> \n <span class=\"string\">\'bar.foo\'</span>: \n <span class=\"string\">\'Norwegian\'</span>,\n </div>\n <div class=\"line\"> \n <span class=\"string\">\'bar.bar\'</span>: \n <span class=\"string\">\'Wood\'</span>,\n </div>\n <div class=\"line\">\n })\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h3 id=\"其他收益\" class=\"post-heading\"><a href=\"#其他收益\" class=\"headerlink\" title=\"其他收益\"></a>其他收益<a class=\"post-anchor\" href=\"#其他收益\" aria-hidden=\"true\"></a></h3>\n<p>比起原生小程序开发,Taro 带来了许多激动人心的特性(如支持 TypeScript、NPM、丰富的 JSX 语法、更高级的 ES 特性等等),不仅提升了开发体验,对自动化测试、持续构建等也会有不小的帮助。</p>\n<p>举个例子,京东购物小程序里封装了一个 <code>getImg</code> 方法,该方法接受一个图片 url 及可选的宽高作为参数,然后根据设备类型决定是否使用 webp 格式、根据当前网络环境应用适当的图片压缩率、自动处理协议头和域名转换,最后生成符合目标大小的图片 url。我们要求所有的图片都必须经过 <code>getImg</code> 方法处理后才能进行展示,但由于 JS 方法只能在逻辑层进行调用,处理好后再传递给 <code>WXML</code> 进行展示,使得很难在自动化工具中进行检测,及时发现未调用 <code>getImg</code> 输出图片的情况。</p>\n<p>而使用 Taro 之后,可以直接在 <code>JSX</code> 模板的 <code>Image</code> 标签输出时对 <code>src</code> 调用 <code>getImg</code> 方法进行处理,将此种写法作为规范明确后,就很容易通过自动化工具进行检测了:</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n render () {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">return</span> \n <span class=\"xml\"><span class=\"tag\">&lt;<span class=\"name\">Image</span> <span class=\"attr\">src</span>=<span class=\"string\">{getImg(url,</span> <span class=\"attr\">750</span>)} /&gt;</span></span>\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>So 对于现有项目来说,不需要进行整体重构,也能很好的将 Taro 集成进去。还等什么,赶紧试试吧~</p>\n<div class=\"post-tags\" style=\"display:none\">\n <a href=\"/tags/React/\">React</a> \n <a href=\"/tags/小程序/\">小程序</a> \n <a href=\"/tags/Nerv/\">Nerv</a> \n <a href=\"/tags/Taro/\">Taro</a>\n</div>\n<div class=\"post-categories\" style=\"display:none\">\n <a href=\"/cates/小程序/\">小程序</a>\n</div>\n<div class=\"post-announce\">\n 感谢您的阅读,本文由 \n <a href=\"//aotu.io\">凹凸实验室</a> 版权所有。如若转载,请注明出处:凹凸实验室(\n <a href=\"https://aotu.io/notes/2018/09/11/taro-in-jd/\">https://aotu.io/notes/2018/09/11/taro-in-jd/</a>)\n</div>\n<div class=\"post-revision\">\n <i class=\"fa fa-clock-o\"></i> \n <time class=\"post-updated\" datetime=\"2018-11-07T01:01:15.657Z\">上次更新:2018-11-07 09:01:15</time>\n</div>');
INSERT INTO `rumo_blog` VALUES ('27', '十分钟打造 3D 物理世界', '1', '十分钟打造 3D 物理世界', '1', '2018-11-11 21:02:33', '2018-11-11 21:02:33', '1', '0', '//misc.aotu.io/ONE-SUNDAY/cannonjs/cover_900x500.jpg', '', '0', '0', '<a id=\"more\"></a>\n<h2 id=\"前言\" class=\"post-heading\"><a href=\"#前言\" class=\"headerlink\" title=\"前言\"></a>前言<a class=\"post-anchor\" href=\"#前言\" aria-hidden=\"true\"></a></h2>\n<p>在繁忙的业务中,为了缩短设计和开发的周期,我们的 H5 小游戏更多的会采用 2D 的视觉风格,但总是一个风格是很无趣的,所以最近搞了一个 3D 物理游戏的需求,在开发的过程中遇到了不少问题,希望通过这篇文章将关于 Three.js、Cannon.js、模型、工具等基础知识、问题总结分享给大家。</p>\n<p>开始 3D 项目之前,首先从选择 3D 框架开始,老牌引擎 Three.js 和微软的 Babylon.js 都不错,针对自己的项目需求选择一款即可,这次我主要针对更熟悉的 Three.js 来讲。</p>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/cannonjs/threejs_babylonjs.jpg\" alt=\"three.js and babylon.js\"></p>\n<h2 id=\"Three-js-基础概念\" class=\"post-heading\"><a href=\"#Three-js-基础概念\" class=\"headerlink\" title=\"Three.js 基础概念\"></a>Three.js 基础概念<a class=\"post-anchor\" href=\"#Three-js-基础概念\" aria-hidden=\"true\"></a></h2>\n<p>使用 Three.js 前,首先要理解以下几个核心概念:</p>\n<h4 id=\"Sence-场景\" class=\"post-heading\"><a href=\"#Sence-场景\" class=\"headerlink\" title=\"Sence 场景\"></a>Sence 场景<a class=\"post-anchor\" href=\"#Sence-场景\" aria-hidden=\"true\"></a></h4>\n<p>在 Three.js 中首先需要创建一个三维空间,我们称之为场景。</p>\n<p>场景可以想象成是一个容器,里面存放着所有渲染的物体和使用的光源。</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">let</span> scene = \n <span class=\"keyword\">new</span> THREE.Scene()\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h4 id=\"Axes-坐标轴\" class=\"post-heading\"><a href=\"#Axes-坐标轴\" class=\"headerlink\" title=\"Axes 坐标轴\"></a>Axes 坐标轴<a class=\"post-anchor\" href=\"#Axes-坐标轴\" aria-hidden=\"true\"></a></h4>\n<p>Three.js 采用的是右手坐标系,拇指、食指、中指分别表示 X、Y、Z 轴的方向。</p>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/cannonjs/axes.jpg\" alt=\"axes\"></p>\n<h4 id=\"Camera-摄像机\" class=\"post-heading\"><a href=\"#Camera-摄像机\" class=\"headerlink\" title=\"Camera 摄像机\"></a>Camera 摄像机<a class=\"post-anchor\" href=\"#Camera-摄像机\" aria-hidden=\"true\"></a></h4>\n<p>摄像机就相当于我们的双眼,决定了能够在场景中的所见所得。</p>\n<p>Three.js 中提供以下几种摄像机类型,最为常用的是 <strong>PerspectiveCamera 透视摄像机</strong>,其他了解下即可。</p>\n<ul>\n <li>ArrayCamera 阵列摄像机\n <ul>\n <li>一个 ArrayCamera 会包含多个子摄像机,通过这一组子摄像机渲染出实际效果,适用于 VR 场景。</li>\n </ul></li>\n <li>CubeCamera 立方摄像机\n <ul>\n <li>创建六个 PerspectiveCamera(透视摄像机),适用于镜面场景。</li>\n </ul></li>\n <li>StereoCamera 立体相机\n <ul>\n <li>双透视摄像机适用于 3D 影片、视差效果。</li>\n </ul></li>\n</ul>\n<h5 id=\"OrthographicCamera-正交摄像机\" class=\"post-heading\"><a href=\"#OrthographicCamera-正交摄像机\" class=\"headerlink\" title=\"OrthographicCamera 正交摄像机\"></a>OrthographicCamera 正交摄像机<a class=\"post-anchor\" href=\"#OrthographicCamera-正交摄像机\" aria-hidden=\"true\"></a></h5>\n<p>OrthographicCamera(正交摄像机)定义了一个矩形可视区域,物体只有在这个区域内才是可见的,另外物体无论距离摄像机是远或事近,物体都会被渲染成一个大小,所以这种摄像机类型适用于 2.5D 场景(例如斜 45 度游戏)。</p>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/cannonjs/OrthographicCamera.jpg?t=2\" alt=\"OrthographicCamera\"></p>\n<h5 id=\"PerspectiveCamera-透视摄像机\" class=\"post-heading\"><a href=\"#PerspectiveCamera-透视摄像机\" class=\"headerlink\" title=\"PerspectiveCamera 透视摄像机\"></a>PerspectiveCamera 透视摄像机<a class=\"post-anchor\" href=\"#PerspectiveCamera-透视摄像机\" aria-hidden=\"true\"></a></h5>\n<p>最为常用的摄像机类型,模拟人眼的视觉,根据物体距离摄像机的距离,近大远小。默认情况下,摄像机的初始位置 X、Y、Z 都为 0,摄像机方向是从正 Z 轴向负 Z 轴看去。通过 <code>Near</code> 和 <code>Far</code> 定义最近和最远的可视距离,<code>Fov</code> 定义可视的角度。</p>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/cannonjs/PerspectiveCamera.jpg?t=2\" alt=\"PerspectiveCamera\"></p>\n<h4 id=\"Mesh-网格\" class=\"post-heading\"><a href=\"#Mesh-网格\" class=\"headerlink\" title=\"Mesh 网格\"></a>Mesh 网格<a class=\"post-anchor\" href=\"#Mesh-网格\" aria-hidden=\"true\"></a></h4>\n<p>有了场景和摄像头就可以看到 3D 场景中的物体,场景中的我们最为常用的物体称为网格。</p>\n<p>网格由两部分组成:几何体和材质</p>\n<h5 id=\"Geometry-几何体\" class=\"post-heading\"><a href=\"#Geometry-几何体\" class=\"headerlink\" title=\"Geometry 几何体\"></a>Geometry 几何体<a class=\"post-anchor\" href=\"#Geometry-几何体\" aria-hidden=\"true\"></a></h5>\n<p>记录了渲染一个 3D 物体所需要的基本数据:Face 面、Vertex 顶点等信息。</p>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/cannonjs/mesh.jpg\" alt=\"geometry\"></p>\n<p>例如下面这个网格是由三角形组成,组成三角形的点称为顶点,组成的三角形称为面。</p>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/cannonjs/vertex_face.jpg\" alt=\"vertex_face\"></p>\n<h5 id=\"Material-材质\" class=\"post-heading\"><a href=\"#Material-材质\" class=\"headerlink\" title=\"Material 材质\"></a>Material 材质<a class=\"post-anchor\" href=\"#Material-材质\" aria-hidden=\"true\"></a></h5>\n<p>材质就像是物体的皮肤,决定了几何体的外表。<br>外表的定义可以让一个物体看起来是否有镜面金属感、暗淡、纯色、或是透明与否等效果。</p>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/cannonjs/material.jpg\" alt=\"material\"></p>\n<h3 id=\"Light-光源\" class=\"post-heading\"><a href=\"#Light-光源\" class=\"headerlink\" title=\"Light 光源\"></a>Light 光源<a class=\"post-anchor\" href=\"#Light-光源\" aria-hidden=\"true\"></a></h3>\n<p>光源相当于在密闭空间里的一盏灯,对于场景是必不可少的</p>\n<p>在 Three.js 常用的有这几种光源:</p>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/cannonjs/light.jpg\" alt=\"light\"></p>\n<h5 id=\"AmbientLight-环境光源\" class=\"post-heading\"><a href=\"#AmbientLight-环境光源\" class=\"headerlink\" title=\"AmbientLight 环境光源\"></a>AmbientLight 环境光源<a class=\"post-anchor\" href=\"#AmbientLight-环境光源\" aria-hidden=\"true\"></a></h5>\n<p>属于基础光源,为场景中的所有物体提供一个基础亮度。</p>\n<h5 id=\"DirectionalLight-平行光源\" class=\"post-heading\"><a href=\"#DirectionalLight-平行光源\" class=\"headerlink\" title=\"DirectionalLight 平行光源\"></a>DirectionalLight 平行光源<a class=\"post-anchor\" href=\"#DirectionalLight-平行光源\" aria-hidden=\"true\"></a></h5>\n<p>效果类似太阳光,发出的光源都是平行的。</p>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/cannonjs/DirectionalLight.jpg\" alt=\"DirectionalLight\"></p>\n<h5 id=\"HemisphereLight-半球光源\" class=\"post-heading\"><a href=\"#HemisphereLight-半球光源\" class=\"headerlink\" title=\"HemisphereLight 半球光源\"></a>HemisphereLight 半球光源<a class=\"post-anchor\" href=\"#HemisphereLight-半球光源\" aria-hidden=\"true\"></a></h5>\n<p>只有圆球的半边会发出光源。</p>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/cannonjs/HemisphereLight.jpg\" alt=\"HemisphereLight\"></p>\n<h5 id=\"PointLight-点光源\" class=\"post-heading\"><a href=\"#PointLight-点光源\" class=\"headerlink\" title=\"PointLight 点光源\"></a>PointLight 点光源<a class=\"post-anchor\" href=\"#PointLight-点光源\" aria-hidden=\"true\"></a></h5>\n<p>一个点向四周发出光源,一般用于灯泡。</p>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/cannonjs/PointLight.jpg\" alt=\"PointLight\"></p>\n<h5 id=\"SpotLight-聚光灯光源\" class=\"post-heading\"><a href=\"#SpotLight-聚光灯光源\" class=\"headerlink\" title=\"SpotLight 聚光灯光源\"></a>SpotLight 聚光灯光源<a class=\"post-anchor\" href=\"#SpotLight-聚光灯光源\" aria-hidden=\"true\"></a></h5>\n<p>一个圆锥体的灯光。</p>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/cannonjs/SpotLight.jpg\" alt=\"SpotLight\"></p>\n<h3 id=\"Shadow-阴影\" class=\"post-heading\"><a href=\"#Shadow-阴影\" class=\"headerlink\" title=\"Shadow 阴影\"></a>Shadow 阴影<a class=\"post-anchor\" href=\"#Shadow-阴影\" aria-hidden=\"true\"></a></h3>\n<p>另外要注意并不是每一种光源都能产生阴影,目前只有三种光源可以:</p>\n<ul>\n <li>DirectionalLight 平行光源</li>\n <li>PointLight 点光源</li>\n <li>SpotLight 聚光灯光源</li>\n</ul>\n<p>另外如果要开启模型的阴影的话,模型是由多个 Mesh 组成的,只开启父的 Mesh 的阴影是不行的,还需要遍历父 Mesh 下所有的子 Mesh 为其开启投射阴影 <code>castShadow</code> 和接收投射阴影 <code>receiveShadow</code>。</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">// 遍历子 Mesh 开启阴影</span>\n </div>\n <div class=\"line\">\n object.traverse(\n <span class=\"function\"><span class=\"keyword\">function</span>(<span class=\"params\">child</span>) </span>{\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span> (child \n <span class=\"keyword\">instanceof</span> THREE.Mesh) {\n </div>\n <div class=\"line\">\n child.castShadow = \n <span class=\"literal\">true</span>\n </div>\n <div class=\"line\">\n child.receiveShadow = \n <span class=\"literal\">true</span>\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n })\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h2 id=\"glTF-模型格式\" class=\"post-heading\"><a href=\"#glTF-模型格式\" class=\"headerlink\" title=\"glTF 模型格式\"></a>glTF 模型格式<a class=\"post-anchor\" href=\"#glTF-模型格式\" aria-hidden=\"true\"></a></h2>\n<p>前面提到 Three.js 引擎支持的格式非常的多,我们最为常见的格式有 <code>.obj</code> + <code>.mtl</code> + <code>.jpg/.png</code>,但使用这种模型格式存在一个问题,<code>.obj</code> 是静态模型,不支持动画数据存储,无法使用模型的动画,所以我建议使用 glTF 这种模型格式。</p>\n<h5 id=\"glTF-模型格式介绍\" class=\"post-heading\"><a href=\"#glTF-模型格式介绍\" class=\"headerlink\" title=\"glTF 模型格式介绍\"></a>glTF 模型格式介绍<a class=\"post-anchor\" href=\"#glTF-模型格式介绍\" aria-hidden=\"true\"></a></h5>\n<p>传统的 3D 模型格式的设计理念更多是针对本地离线使用,所以这类 3D 模型格式没有针对下载速度或加载速度进行优化,文件大小往往会非常的大,随着 Web 端的兴起,对文件大小更为敏感的今天,我们该尝试别的模型格式了。</p>\n<p>glTF 是由 Khronos Group 开发的 3D 模型文件格式,该格式的特点是最大程度的减少了 3D 模型文件的大小,提高了传输、加载以及解析 3D 模型文件的效率,并且它可扩展,可互操作。</p>\n<p>第一版 glTF 1.0 于 2015 年 10 月 19 日发布,2017 年 6 月 5 日的 Web 3D 2017 大会发布了最终版本 glTF 2.0。</p>\n<h5 id=\"glTF-模型格式文件组成\" class=\"post-heading\"><a href=\"#glTF-模型格式文件组成\" class=\"headerlink\" title=\"glTF 模型格式文件组成\"></a>glTF 模型格式文件组成<a class=\"post-anchor\" href=\"#glTF-模型格式文件组成\" aria-hidden=\"true\"></a></h5>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/cannonjs/gltf_assets.jpg\" alt=\"gltf_assets\"></p>\n<h6 id=\"模型文件-gltf\" class=\"post-heading\"><a href=\"#模型文件-gltf\" class=\"headerlink\" title=\"模型文件 .gltf\"></a>模型文件 <code>.gltf</code><a class=\"post-anchor\" href=\"#模型文件-gltf\" aria-hidden=\"true\"></a></h6>\n<p>包含场景中节点层次结构、摄像机、网格、材质以及动画等描述信息。</p>\n<h6 id=\"二进制文件-bin\" class=\"post-heading\"><a href=\"#二进制文件-bin\" class=\"headerlink\" title=\"二进制文件 .bin\"></a>二进制文件 <code>.bin</code><a class=\"post-anchor\" href=\"#二进制文件-bin\" aria-hidden=\"true\"></a></h6>\n<p>包含几何、动画的数据以及其他基于缓冲区的数据,<code>.bin</code> 文件可以直接加载到 GPU 的缓冲区中从而不需要额外的解析,因此能够高效传输和快速加载。</p>\n<h6 id=\"材质贴图文件-png-jpg\" class=\"post-heading\"><a href=\"#材质贴图文件-png-jpg\" class=\"headerlink\" title=\"材质贴图文件 .png/.jpg\"></a>材质贴图文件 <code>.png/.jpg</code><a class=\"post-anchor\" href=\"#材质贴图文件-png-jpg\" aria-hidden=\"true\"></a></h6>\n<p>3D 模型做凹凸贴图或普通贴图上所使用到文件。</p>\n<h5 id=\"glTF-模型格式导出\" class=\"post-heading\"><a href=\"#glTF-模型格式导出\" class=\"headerlink\" title=\"glTF 模型格式导出\"></a>glTF 模型格式导出<a class=\"post-anchor\" href=\"#glTF-模型格式导出\" aria-hidden=\"true\"></a></h5>\n<p>官方在 <code>.gltf</code> 格式导出上提供了多种建模软件的导出插件,比如有:</p>\n<ul>\n <li>3DS Max Exporter</li>\n <li>Maya Exporter</li>\n <li>Blender glTF 2.0 Exporter</li>\n <li>…</li>\n</ul>\n<p>正巧我们常用的 C4D 建模软件官方没有提供 C4D 的导出插件,所以我们使用 C4D 导出后再导入 Blender,通过 Blender 作为中转站导出 glTF 格式文件。</p>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/cannonjs/exporter.jpg\" alt=\"exporter\"></p>\n<p>但由于两个建模软件之间的材质并不能相通,导出后的模型文件材质效果表现不佳,这是因为 Blender 有自己的一套材质流程系统,例如有 <code>glTF Metallic Roughness</code> 和 <code>glTF Specular Glossiness</code>,需在此基础之上重新贴材质后导出解决。</p>\n<p>另外注意的一点 Blender 的坐标系与 Three.js 是不同的,Blender 会将 Z 和 Y 对调位置,在导出时要选择 <code>Convert Z up to Y up</code> 进行对调。</p>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/cannonjs/convert.jpg\" alt=\"convert\"></p>\n<h5 id=\"Three-js-使用-glTF-模型\" class=\"post-heading\"><a href=\"#Three-js-使用-glTF-模型\" class=\"headerlink\" title=\"Three.js 使用 glTF 模型\"></a>Three.js 使用 glTF 模型<a class=\"post-anchor\" href=\"#Three-js-使用-glTF-模型\" aria-hidden=\"true\"></a></h5>\n<p>Three.js 中使用 glTF 格式需额外引入 <a href=\"https://github.com/mrdoob/three.js/blob/master/examples/js/loaders/GLTFLoader.js\" target=\"_blank\" rel=\"external\">GLTFLoader.js</a> 加载器。</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">var</span> gltfLoader = \n <span class=\"keyword\">new</span> THREE.gltfLoader()\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n gltfLoader.load(\n <span class=\"string\">\'./assets/box.gltf\'</span>, \n <span class=\"function\"><span class=\"keyword\">function</span>(<span class=\"params\">sence</span>) </span>{\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">var</span> object = scene.gltf \n <span class=\"comment\">// 模型对象</span>\n </div>\n <div class=\"line\">\n scene.add(object) \n <span class=\"comment\">// 将模型添加到场景中</span>\n </div>\n <div class=\"line\">\n })\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h3 id=\"glTF-模型动画\" class=\"post-heading\"><a href=\"#glTF-模型动画\" class=\"headerlink\" title=\"glTF 模型动画\"></a>glTF 模型动画<a class=\"post-anchor\" href=\"#glTF-模型动画\" aria-hidden=\"true\"></a></h3>\n<h5 id=\"Animation-Clip-动画片段\" class=\"post-heading\"><a href=\"#Animation-Clip-动画片段\" class=\"headerlink\" title=\"Animation Clip 动画片段\"></a>Animation Clip 动画片段<a class=\"post-anchor\" href=\"#Animation-Clip-动画片段\" aria-hidden=\"true\"></a></h5>\n<p>前面提到 glTF 模型格式支持动画,模型动画可以使用 Blender 建模软件制作,通过 Blender 提供的时间轴编辑变形动画或者骨骼动画,每个动画可以编辑为一个 Action 动作,导出后使用 GLTFLoader 加载到 Three.js 中,可以拿到一个 <code>animations</code> 数组,<code>animations</code> 里包含了模型的每个动画 Action 动作。</p>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/cannonjs/animation_clip.jpg\" alt=\"animation_clip\"></p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div>\n <div class=\"line\">\n 16\n </div>\n <div class=\"line\">\n 17\n </div>\n <div class=\"line\">\n 18\n </div>\n <div class=\"line\">\n 19\n </div>\n <div class=\"line\">\n 20\n </div>\n <div class=\"line\">\n 21\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">let</span> gltfLoader = \n <span class=\"keyword\">new</span> THREE.gltfLoader()\n </div>\n <div class=\"line\">\n <span class=\"keyword\">let</span> mixer = \n <span class=\"literal\">null</span>\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n gltfLoader.load(\n <span class=\"string\">\'./assets/box.gltf\'</span>, \n <span class=\"function\"><span class=\"keyword\">function</span>(<span class=\"params\">sence</span>) </span>{\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> object = scene.gltf\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> animations = sence.animations \n <span class=\"comment\">// 动画数据</span>\n </div>\n <div class=\"line\"></div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span> (animations &amp;&amp; animations.length) {\n </div>\n <div class=\"line\">\n mixer = \n <span class=\"keyword\">new</span> THREE.AnimationMixer(object) \n <span class=\"comment\">// 对动画进行控制</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">for</span> (\n <span class=\"keyword\">let</span> i = \n <span class=\"number\">0</span>; i &lt; animations.length; i++) {\n </div>\n <div class=\"line\">\n mixer.clipAction(animations[i]).play() \n <span class=\"comment\">// 播放所有动画</span>\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n scene.add(object)\n </div>\n <div class=\"line\">\n })\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"function\"><span class=\"keyword\">function</span> <span class=\"title\">update</span>(<span class=\"params\"></span>) </span>{\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> delta = clock.getDelta(mixer)\n </div>\n <div class=\"line\">\n mixer.update(delta) \n <span class=\"comment\">// 更新动画片段</span>\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h5 id=\"Tween-动画\" class=\"post-heading\"><a href=\"#Tween-动画\" class=\"headerlink\" title=\"Tween 动画\"></a>Tween 动画<a class=\"post-anchor\" href=\"#Tween-动画\" aria-hidden=\"true\"></a></h5>\n<p>对模型实现淡入淡出、缩放、位移、旋转等动画推荐使用 <a href=\"https://greensock.com/gsap?product=gsap\" target=\"_blank\" rel=\"external\">GSAP</a> 来实现更为简便。</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div>\n <div class=\"line\">\n 16\n </div>\n <div class=\"line\">\n 17\n </div>\n <div class=\"line\">\n 18\n </div>\n <div class=\"line\">\n 19\n </div>\n <div class=\"line\">\n 20\n </div>\n <div class=\"line\">\n 21\n </div>\n <div class=\"line\">\n 22\n </div>\n <div class=\"line\">\n 23\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">let</span> tween = \n <span class=\"keyword\">new</span> TimelineMax()\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n tween\n </div>\n <div class=\"line\">\n .to(box.scale, \n <span class=\"number\">1</span>, { \n <span class=\"comment\">// 从 1 缩放至 2,花费 1 秒</span>\n </div>\n <div class=\"line\">\n x: \n <span class=\"number\">2</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">y</span>: \n <span class=\"number\">2</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">z</span>: \n <span class=\"number\">2</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">ease</span>: Power0.easeInOut, \n <span class=\"comment\">// 速度曲线</span>\n </div>\n <div class=\"line\">\n onStart: \n <span class=\"function\"><span class=\"keyword\">function</span>(<span class=\"params\"></span>) </span>{\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 监听动画开始</span>\n </div>\n <div class=\"line\">\n },\n </div>\n <div class=\"line\"> \n <span class=\"attr\">onUpdate</span>: \n <span class=\"function\"><span class=\"keyword\">function</span>(<span class=\"params\"></span>) </span>{\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 监听动画过程</span>\n </div>\n <div class=\"line\">\n },\n </div>\n <div class=\"line\"> \n <span class=\"attr\">onComplete</span>: \n <span class=\"function\"><span class=\"keyword\">function</span>(<span class=\"params\"></span>) </span>{\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 监听动画结束</span>\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n })\n </div>\n <div class=\"line\">\n .to(box.position, \n <span class=\"number\">1</span>, { \n <span class=\"comment\">// 缩放结束后,位移 x 至 10,花费 1 秒</span>\n </div>\n <div class=\"line\">\n x: \n <span class=\"number\">10</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">y</span>: \n <span class=\"number\">0</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">z</span>: \n <span class=\"number\">0</span>\n </div>\n <div class=\"line\">\n })\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h3 id=\"Draco-3D-模型压缩工具\" class=\"post-heading\"><a href=\"#Draco-3D-模型压缩工具\" class=\"headerlink\" title=\"Draco 3D 模型压缩工具\"></a>Draco 3D 模型压缩工具<a class=\"post-anchor\" href=\"#Draco-3D-模型压缩工具\" aria-hidden=\"true\"></a></h3>\n<p><a href=\"https://codelabs.developers.google.com/codelabs/draco-3d/index.html#0\" target=\"_blank\" rel=\"external\">Draco</a> 是一个用于压缩、解压缩 3D 几何网格和<a href=\"https://zh.wikipedia.org/wiki/%E9%BB%9E%E9%9B%B2\" target=\"_blank\" rel=\"external\">点云</a>的开源库,为改善 3D 图形存储和传输而设计。</p>\n<p>使用该工具可以对 glTF 格式进一步的压缩,会将 glTF 格式转为 <code>.glb</code> 格式,并且 <code>.bin</code> 压缩效果拔群,但是在 Three.js 中使用 <code>.glb</code> 格式需要引入额外的解析库,解析库文件包括 <code>draco_decoder.js</code>(791KB)、<code>draco_decoder.wasm</code>(323 KB)、<code>draco_wasm_wrapper.js</code>(64.3 KB)。所以更推荐当模型文件数量多,且文件较大时使用,否则得不偿失。</p>\n<p>压缩使用 <a href=\"https://github.com/AnalyticalGraphicsInc/gltf-pipeline\" target=\"_blank\" rel=\"external\">glTF Pipeline</a> 工具,需要将三个种类的文件放在一起,执行命令行进行转换。</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n $ npm install -g gltf-pipeline \n <span class=\"comment\">// 安装 gltf-pipeline 工具</span>\n </div>\n <div class=\"line\">\n $ gltf-pipeline -i model.gltf -o model.glb \n <span class=\"comment\">// 指定某个 .gltf 文件转为 .glb 格式</span>\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>Three.js 使用 <code>.glb</code> 格式引入 Draco 解码库</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">// 实例化 loader</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">let</span> loader = \n <span class=\"keyword\">new</span> THREE.GLTFLoader()\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"comment\">// Draco 解码库</span>\n </div>\n <div class=\"line\">\n THREE.DRACOLoader.setDecoderPath(\n <span class=\"string\">\'/examples/js/libs/draco\'</span>)\n </div>\n <div class=\"line\">\n loader.setDRACOLoader(\n <span class=\"keyword\">new</span> THREE.DRACOLoader())\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"comment\">// 加载 glTF 模型</span>\n </div>\n <div class=\"line\">\n loader.load(\n <span class=\"string\">\'models/gltf/box.gltf\'</span>, \n <span class=\"function\"><span class=\"keyword\">function</span>(<span class=\"params\">gltf</span>) </span>{\n </div>\n <div class=\"line\">\n scene.add(gltf.scene)\n </div>\n <div class=\"line\">\n })\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h2 id=\"Cannon-js-3D-物理引擎\" class=\"post-heading\"><a href=\"#Cannon-js-3D-物理引擎\" class=\"headerlink\" title=\"Cannon.js 3D 物理引擎\"></a>Cannon.js 3D 物理引擎<a class=\"post-anchor\" href=\"#Cannon-js-3D-物理引擎\" aria-hidden=\"true\"></a></h2>\n<p>目前在 Github 上搜索到的 3D 物理引擎库有 Cannon.js、Oimo.js、Ammo.js、Energy.js、Physijs 等等,大部分都已许久没有更新迭代了(长达好几年),项目的 Star 数量和 Issues 数量也不多,我们该如何选择?</p>\n<p>Cannon.js、Oimo.js 和 Energy.js 作为 Babylon.js 的内置物理引擎,我们试着从这三个下手。</p>\n<p><a href=\"https://github.com/samuelgirardin/Energy.js\" target=\"_blank\" rel=\"external\">Energy.js</a>:使用 C++ 编写转 JavaScript 的 3D 物理引擎,源码不可读,目前 Github 比较冷清。</p>\n<p><a href=\"https://github.com/lo-th/Oimo.js\" target=\"_blank\" rel=\"external\">Oimo.js</a>:一款轻量级的 3D 物理引擎,文件大小 153 KB。</p>\n<p><a href=\"https://github.com/schteppe/cannon.js\" target=\"_blank\" rel=\"external\">Cannon.js</a>:完全使用 JavaScript 编写的优秀 3D 物理引擎,包含简单的碰撞检测、各种形状的摩擦力、弹力、约束等功能。</p>\n<p>从综合性来看,我更偏向于 Cannon.js ,所以下面主要讲讲 Cannon.js。</p>\n<h4 id=\"Cannon-js-的特性\" class=\"post-heading\"><a href=\"#Cannon-js-的特性\" class=\"headerlink\" title=\"Cannon.js 的特性\"></a>Cannon.js 的特性<a class=\"post-anchor\" href=\"#Cannon-js-的特性\" aria-hidden=\"true\"></a></h4>\n<p>以下是 Cannon.js 的特性,基本可以满足大部分的 3D 物理开发场景。</p>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/cannonjs/cannon.jpg\" alt=\"cannon\"></p>\n<h4 id=\"使用-Cannon-js\" class=\"post-heading\"><a href=\"#使用-Cannon-js\" class=\"headerlink\" title=\"使用 Cannon.js\"></a>使用 Cannon.js<a class=\"post-anchor\" href=\"#使用-Cannon-js\" aria-hidden=\"true\"></a></h4>\n<p>我们以官方的一个平面加球刚体的例子来快速上手 Cannon.js。<a href=\"http://jdc.jd.com/demo/th/cannonjs/index.html\" target=\"_blank\" rel=\"external\">在线例子</a></p>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/cannonjs/cannon_demo.jpg\" alt=\"cannon_demo\"></p>\n<h6 id=\"1、初始化物理世界\" class=\"post-heading\"><a href=\"#1、初始化物理世界\" class=\"headerlink\" title=\"1、初始化物理世界\"></a>1、初始化物理世界<a class=\"post-anchor\" href=\"#1、初始化物理世界\" aria-hidden=\"true\"></a></h6>\n<p>使用 Cannon.js 前需要创建 CANNON.World 对象,CANNON.World 对象是负责管理对象和模拟物理的中心。</p>\n<p>创建完 CANNON.World 对象后,接着设置物理世界的重力,这里设置了负 Y 轴为 10 m/s²。</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">let</span> world = \n <span class=\"keyword\">new</span> CANNON.World()\n </div>\n <div class=\"line\">\n world.gravity.set(\n <span class=\"number\">0</span>, \n <span class=\"number\">-10</span>, \n <span class=\"number\">0</span>)\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>Cannon.js 提供了 Broadphase、NaiveBroadphase 两种碰撞检测阶段,默认是 NaiveBroadphase。</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n world.broadphase = \n <span class=\"keyword\">new</span> CANNON.NaiveBroadphase()\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h6 id=\"2、创建动态球体\" class=\"post-heading\"><a href=\"#2、创建动态球体\" class=\"headerlink\" title=\"2、创建动态球体\"></a>2、创建动态球体<a class=\"post-anchor\" href=\"#2、创建动态球体\" aria-hidden=\"true\"></a></h6>\n<p>创建 Body 分三个步骤:</p>\n<ul>\n <li>创建形状</li>\n <li>为形状添加刚体</li>\n <li>将刚体添加到世界</li>\n</ul>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">let</span> sphereShape = \n <span class=\"keyword\">new</span> CANNON.Sphere(\n <span class=\"number\">1</span>) \n <span class=\"comment\">// Step 1 </span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">let</span> sphereBody = \n <span class=\"keyword\">new</span> CANNON.Body({ \n <span class=\"comment\">// Step 2</span>\n </div>\n <div class=\"line\">\n mass: \n <span class=\"number\">5</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">position</span>: \n <span class=\"keyword\">new</span> CANNON.Vec3(\n <span class=\"number\">0</span>, \n <span class=\"number\">10</span>, \n <span class=\"number\">0</span>),\n </div>\n <div class=\"line\"> \n <span class=\"attr\">shape</span>: sphereShape\n </div>\n <div class=\"line\">\n })\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n world.add(sphereBody) \n <span class=\"comment\">// Step 3</span>\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>第一步创建了半径为 1 的球形,第二步创建球的刚体,如果刚体的 mass 属性设置为 0,刚体则会处于静止状态,静止的物体不会和其他静止的物体发生碰撞,我们这里为球的刚体设置了 5kg,球会处于动态的状态,会受的重力的影响而移动,会与其他物体发生碰撞。</p>\n<h6 id=\"3、创建静态平面和动态球体\" class=\"post-heading\"><a href=\"#3、创建静态平面和动态球体\" class=\"headerlink\" title=\"3、创建静态平面和动态球体\"></a>3、创建静态平面和动态球体<a class=\"post-anchor\" href=\"#3、创建静态平面和动态球体\" aria-hidden=\"true\"></a></h6>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">// 平面 Body</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">let</span> groundShape = \n <span class=\"keyword\">new</span> CANNON.Plane()\n </div>\n <div class=\"line\">\n <span class=\"keyword\">let</span> groundBody = \n <span class=\"keyword\">new</span> CANNON.Body({\n </div>\n <div class=\"line\"> \n <span class=\"attr\">mass</span>: \n <span class=\"number\">0</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">shape</span>: groundShape\n </div>\n <div class=\"line\">\n })\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"comment\">// setFromAxisAngle 旋转 X 轴 -90 度</span>\n </div>\n <div class=\"line\">\n groundBody.quaternion.setFromAxisAngle(\n <span class=\"keyword\">new</span> CANNON.Vec3(\n <span class=\"number\">1</span>, \n <span class=\"number\">0</span>, \n <span class=\"number\">0</span>), \n <span class=\"number\">-1.5707963267948966</span>)\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n world.add(groundBody)\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>创建平面形状,接着是刚体,这里设置了平面刚体的 mass 为 0,保证刚体处于静止状态。默认情况下平面的方向是朝向 Z 方向的(竖立着),可以通过 <code>Body.quaternion.setFromAxisAngle</code> 对平面进行旋转。</p>\n<h6 id=\"4、创建平面和球的网格\" class=\"post-heading\"><a href=\"#4、创建平面和球的网格\" class=\"headerlink\" title=\"4、创建平面和球的网格\"></a>4、创建平面和球的网格<a class=\"post-anchor\" href=\"#4、创建平面和球的网格\" aria-hidden=\"true\"></a></h6>\n<p>前面创建的刚体在场景中并没有实际的视觉效果,这一步创建平面、球的网格。</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">// 平面网格</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">let</span> groundGeometry = \n <span class=\"keyword\">new</span> THREE.PlaneGeometry(\n <span class=\"number\">20</span>, \n <span class=\"number\">20</span>, \n <span class=\"number\">32</span>)\n </div>\n <div class=\"line\">\n <span class=\"keyword\">let</span> groundMaterial = \n <span class=\"keyword\">new</span> THREE.MeshStandardMaterial({\n </div>\n <div class=\"line\"> \n <span class=\"attr\">color</span>: \n <span class=\"number\">0x7f7f7f</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">side</span>: THREE.DoubleSide\n </div>\n <div class=\"line\">\n })\n </div>\n <div class=\"line\">\n <span class=\"keyword\">let</span> ground = \n <span class=\"keyword\">new</span> THREE.Mesh(groundGeometry, groundMaterial)\n </div>\n <div class=\"line\">\n scene.add(ground)\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"comment\">// 球网格</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">let</span> sphereGeometry = \n <span class=\"keyword\">new</span> THREE.SphereGeometry(\n <span class=\"number\">1</span>, \n <span class=\"number\">32</span>, \n <span class=\"number\">32</span>)\n </div>\n <div class=\"line\">\n <span class=\"keyword\">let</span> sphereMaterial = \n <span class=\"keyword\">new</span> THREE.MeshStandardMaterial({ \n <span class=\"attr\">color</span>: \n <span class=\"number\">0xffff00</span> })\n </div>\n <div class=\"line\">\n <span class=\"keyword\">let</span> sphere = \n <span class=\"keyword\">new</span> THREE.Mesh(sphereGeometry, sphereMaterial)\n </div>\n <div class=\"line\">\n scene.add(sphere)\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h6 id=\"5、模拟世界\" class=\"post-heading\"><a href=\"#5、模拟世界\" class=\"headerlink\" title=\"5、模拟世界\"></a>5、模拟世界<a class=\"post-anchor\" href=\"#5、模拟世界\" aria-hidden=\"true\"></a></h6>\n<p>接着我们为物理世界开启持续更新,并且将创建的球刚体与球网格关联起来。</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"function\"><span class=\"keyword\">function</span> <span class=\"title\">update</span>(<span class=\"params\"></span>) </span>{\n </div>\n <div class=\"line\">\n requestAnimationFrame(update)\n </div>\n <div class=\"line\">\n world.step(\n <span class=\"number\">1</span> / \n <span class=\"number\">60</span>)\n </div>\n <div class=\"line\"></div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span> (sphere) {\n </div>\n <div class=\"line\">\n sphere.position.copy(sphereBody.position)\n </div>\n <div class=\"line\">\n sphere.quaternion.copy(sphereBody.quaternion)\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>通过这几步,一个简单的物理场景就完成了,另外更多官方例子可以<a href=\"http://schteppe.github.io/cannon.js/\" target=\"_blank\" rel=\"external\">点击这里</a>,可以查看到 Cannon.js 各个约束、摩擦力、模拟汽车等特性的例子。</p>\n<h4 id=\"其他:\" class=\"post-heading\"><a href=\"#其他:\" class=\"headerlink\" title=\"其他:\"></a>其他:<a class=\"post-anchor\" href=\"#其他:\" aria-hidden=\"true\"></a></h4>\n<h6 id=\"1、自定义物理材质需关联\" class=\"post-heading\"><a href=\"#1、自定义物理材质需关联\" class=\"headerlink\" title=\"1、自定义物理材质需关联\"></a>1、自定义物理材质需关联<a class=\"post-anchor\" href=\"#1、自定义物理材质需关联\" aria-hidden=\"true\"></a></h6>\n<p>还是上面的例子,现在场景中刚体的物理特性都为默认的,我希望球的恢复系数高一点,即掉落时弹跳的更高。首先需要通过 <code>CANNON.Material</code> 实例物理材质,刚体使用该物理材质,最后通过 <code>CANNON.ContactMaterial</code> 来定义两个刚体相遇后会发生什么。</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div>\n <div class=\"line\">\n 16\n </div>\n <div class=\"line\">\n 17\n </div>\n <div class=\"line\">\n 18\n </div>\n <div class=\"line\">\n 19\n </div>\n <div class=\"line\">\n 20\n </div>\n <div class=\"line\">\n 21\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">// 平面</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">let</span> ground_cm = \n <span class=\"keyword\">new</span> CANNON.Material() \n <span class=\"comment\">// Step 1 : 实例 CANNON.Material</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">let</span> groundBody = \n <span class=\"keyword\">new</span> CANNON.Body({\n </div>\n <div class=\"line\">\n ...\n </div>\n <div class=\"line\">\n material: groundMaterial \n <span class=\"comment\">// Step 2 : 使用该物理材质</span>\n </div>\n <div class=\"line\">\n ...\n </div>\n <div class=\"line\">\n })\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"comment\">// 球</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">let</span> sphere_cm = \n <span class=\"keyword\">new</span> CANNON.Material()\n </div>\n <div class=\"line\">\n <span class=\"keyword\">let</span> sphereBody = \n <span class=\"keyword\">new</span> CANNON.Body({\n </div>\n <div class=\"line\">\n ...\n </div>\n <div class=\"line\">\n material: sphere_cm\n </div>\n <div class=\"line\">\n ...\n </div>\n <div class=\"line\">\n })\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"keyword\">let</span> sphere_ground = \n <span class=\"keyword\">new</span> CANNON.ContactMaterial(ground_cm, sphere_cm, { \n <span class=\"comment\">// Step 3 : 定义两个刚体相遇后会发生什么</span>\n </div>\n <div class=\"line\">\n friction: \n <span class=\"number\">1</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">restitution</span>: \n <span class=\"number\">0.4</span>\n </div>\n <div class=\"line\">\n })\n </div>\n <div class=\"line\">\n world.addContactMaterial(sphere_ground) \n <span class=\"comment\">// Step 4 : 添加到世界中</span>\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h6 id=\"2、刚体添加位移动画时需取消速度值\" class=\"post-heading\"><a href=\"#2、刚体添加位移动画时需取消速度值\" class=\"headerlink\" title=\"2、刚体添加位移动画时需取消速度值\"></a>2、刚体添加位移动画时需取消速度值<a class=\"post-anchor\" href=\"#2、刚体添加位移动画时需取消速度值\" aria-hidden=\"true\"></a></h6>\n<p>比如我使用 GSAP 库对某个刚体进行 Y 轴向上移动,在 update 阶段需要将刚体的重力加速度设置为 0,否则动画结束后刚体会出现向下砸的效果。</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">let</span> tween = \n <span class=\"keyword\">new</span> TimelineMax()\n </div>\n <div class=\"line\">\n tween.to(boxBody.position, \n <span class=\"number\">2</span>, {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">x</span>: \n <span class=\"number\">0</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">y</span>: \n <span class=\"number\">10</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">z</span>: \n <span class=\"number\">0</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">update</span>: \n <span class=\"function\"><span class=\"keyword\">function</span>(<span class=\"params\"></span>) </span>{\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 归 0 设置</span>\n </div>\n <div class=\"line\">\n boxBody.velocity.setZero()\n </div>\n <div class=\"line\">\n boxBody.initVelocity.setZero()\n </div>\n <div class=\"line\">\n boxBody.angularVelocity.setZero()\n </div>\n <div class=\"line\">\n boxBody.initAngularVelocity.setZero()\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n })\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h6 id=\"3、只检测碰撞,不发生物理效果\" class=\"post-heading\"><a href=\"#3、只检测碰撞,不发生物理效果\" class=\"headerlink\" title=\"3、只检测碰撞,不发生物理效果\"></a>3、只检测碰撞,不发生物理效果<a class=\"post-anchor\" href=\"#3、只检测碰撞,不发生物理效果\" aria-hidden=\"true\"></a></h6>\n<p>允许只检测是否碰撞,实际不发生物理效果,需要为刚体添加以下属性:</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n boxBody.collisionResponse = \n <span class=\"literal\">false</span>\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h6 id=\"4、缩放刚体\" class=\"post-heading\"><a href=\"#4、缩放刚体\" class=\"headerlink\" title=\"4、缩放刚体\"></a>4、缩放刚体<a class=\"post-anchor\" href=\"#4、缩放刚体\" aria-hidden=\"true\"></a></h6>\n<p>如果刚体需要缩放,则需要为刚体添加此属性,来更新刚体大小。</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n boxBody.updateMassProperties()\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"keyword\">let</span> tween = \n <span class=\"keyword\">new</span> TimelineMax()\n </div>\n <div class=\"line\">\n tween.to(sphereBody.shapes[\n <span class=\"number\">0</span>], \n <span class=\"number\">2</span>, {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">radius</span>: \n <span class=\"number\">0.2</span> \n <span class=\"comment\">// 缩放至 0.2</span>\n </div>\n <div class=\"line\">\n })\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h2 id=\"点击交互\" class=\"post-heading\"><a href=\"#点击交互\" class=\"headerlink\" title=\"点击交互\"></a>点击交互<a class=\"post-anchor\" href=\"#点击交互\" aria-hidden=\"true\"></a></h2>\n<p>在 3D 的世界中不能像我们在 DOM 中为一个节点绑定点击事件那么容易,在 Three.js 中提供了 <code>THREE.Raycaster</code> 方法处理点击交互,使用鼠标或者手指点击屏幕时,会将二维坐标进行转换,发射一条射线判断与哪个物体发生了碰撞,由此得知点击了哪个物体。<a href=\"https://threejs.org/examples/?q=ray#webgl_raycast_sprite\" target=\"_blank\" rel=\"external\">点击这里官方例子</a></p>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/cannonjs/ray.jpg\" alt=\"raycaster\"></p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div>\n <div class=\"line\">\n 16\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">let</span> raycaster = \n <span class=\"keyword\">new</span> THREE.Raycaster()\n </div>\n <div class=\"line\">\n <span class=\"keyword\">let</span> mouse = \n <span class=\"keyword\">new</span> THREE.Vector2()\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"function\"><span class=\"keyword\">function</span> <span class=\"title\">onTouchEnd</span>(<span class=\"params\">ev</span>) </span>{\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 点击获取屏幕坐标</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">var</span> event = ev.changedTouches[\n <span class=\"number\">0</span>]\n </div>\n <div class=\"line\">\n mouse.x = (event.clientX / \n <span class=\"built_in\">window</span>.innerWidth) * \n <span class=\"number\">2</span> - \n <span class=\"number\">1</span>\n </div>\n <div class=\"line\">\n mouse.y = -(event.clientY / \n <span class=\"built_in\">window</span>.innerHeight) * \n <span class=\"number\">2</span> + \n <span class=\"number\">1</span>\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n raycaster.setFromCamera(mouse, camera)\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">let</span> intersects = raycaster.intersectObjects(scene, \n <span class=\"literal\">true</span>)\n </div>\n <div class=\"line\"></div>\n <div class=\"line\"> \n <span class=\"keyword\">for</span> (\n <span class=\"keyword\">let</span> i = \n <span class=\"number\">0</span>; i &lt; intersects.length; i++) {\n </div>\n <div class=\"line\"> \n <span class=\"built_in\">console</span>.log(intersects[i]) \n <span class=\"comment\">// 与射线发生碰撞的物体</span>\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h2 id=\"性能方面\" class=\"post-heading\"><a href=\"#性能方面\" class=\"headerlink\" title=\"性能方面\"></a>性能方面<a class=\"post-anchor\" href=\"#性能方面\" aria-hidden=\"true\"></a></h2>\n<h5 id=\"模型精细程度\" class=\"post-heading\"><a href=\"#模型精细程度\" class=\"headerlink\" title=\"模型精细程度\"></a>模型精细程度<a class=\"post-anchor\" href=\"#模型精细程度\" aria-hidden=\"true\"></a></h5>\n<p>在 Web 端由于性能的限制,在开发过程中要尽量避免做一些损耗性能较大的事情。</p>\n<p>首先是模型的精细程度,在保证效果的前提下,尽量降低模型面的数量,也就是说采用低模模型,一些模型的凹凸褶皱感也可以通过凹凸贴图的方式去实现,越是复杂的模型在实时渲染的过程中就越占用手机性能。</p>\n<h5 id=\"光源与阴影\" class=\"post-heading\"><a href=\"#光源与阴影\" class=\"headerlink\" title=\"光源与阴影\"></a>光源与阴影<a class=\"post-anchor\" href=\"#光源与阴影\" aria-hidden=\"true\"></a></h5>\n<p>另外一方面光源、阴影也是占性能,尤其是阴影。光源一般会使用平行光或者聚光灯,这种光源照射在物体上更为真实,使用半球光会稍微提升帧数,但效果略差些,阴影效果前面提到过要遍历每一个子 Mesh 接收产生阴影 <code>castShadow</code> 和接收阴影 <code>receiveShadow</code>,这相当耗费性能,开启后对阴影的精细程度以及阴影类型进行参数优化,在 Android 系统性能不太好,iOS 系统基本能保证流畅运行,所以建议根据设备系统优化。</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">var</span> n = navigator.userAgent\n </div>\n <div class=\"line\">\n <span class=\"keyword\">if</span> (\n <span class=\"regexp\">/iPad|iPhone|iPod/</span>.test(n) &amp;&amp; !\n <span class=\"built_in\">window</span>.MSStream) { } \n <span class=\"comment\">// 针对 iOS 系统使用阴影</span>\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h5 id=\"抗锯齿与像素比\" class=\"post-heading\"><a href=\"#抗锯齿与像素比\" class=\"headerlink\" title=\"抗锯齿与像素比\"></a>抗锯齿与像素比<a class=\"post-anchor\" href=\"#抗锯齿与像素比\" aria-hidden=\"true\"></a></h5>\n<p>抗锯齿是让模型的边缘效果更加圆滑不粗糙,也会占用一些性能,默认是关闭的,视情况开启。</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n renderer.antialias = \n <span class=\"literal\">true</span> \n <span class=\"comment\">// 开启抗锯齿</span>\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>另外像素比 <code>setPixelRatio</code>,移动端由于 Retina 屏的缘故,一般会设置为 2,所以使用 <code>window.devicePixelRatio</code> 获取实际设备像素比动态设置的话,部分大屏手机的像素比有 3 的情况,所有会因为像素比过高造成性能问题。</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n renderer.setPixelRatio(\n <span class=\"number\">2</span>) \n <span class=\"comment\">// 推荐</span>\n </div>\n <div class=\"line\">\n renderer.setPixelRatio(\n <span class=\"built_in\">window</span>.devicePixelRatio) \n <span class=\"comment\">// 不推荐</span>\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h2 id=\"工具推荐\" class=\"post-heading\"><a href=\"#工具推荐\" class=\"headerlink\" title=\"工具推荐\"></a>工具推荐<a class=\"post-anchor\" href=\"#工具推荐\" aria-hidden=\"true\"></a></h2>\n<p>最后推荐一些在开发过程中常用的工具:</p>\n<h5 id=\"OrbitControls-轨道控制器\" class=\"post-heading\"><a href=\"#OrbitControls-轨道控制器\" class=\"headerlink\" title=\"OrbitControls 轨道控制器\"></a>OrbitControls 轨道控制器<a class=\"post-anchor\" href=\"#OrbitControls-轨道控制器\" aria-hidden=\"true\"></a></h5>\n<p>OrbitControls 是用于调试 Camera 的方法,实例化后可以通过鼠标拖拽来旋转 Camera 镜头的角度,鼠标滚轮可以控制 Camera 镜头的远近距离,旋转和远近都会基于场景的中心点,在调试预览则会轻松许多。</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">new</span> THREE.OrbitControls(camera, renderer.domElement)\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/cannonjs/OrbitControls.jpg\" alt=\"OrbitControls\"></p>\n<h5 id=\"glTF-Viewer-模型快速预览工具\" class=\"post-heading\"><a href=\"#glTF-Viewer-模型快速预览工具\" class=\"headerlink\" title=\"glTF Viewer 模型快速预览工具\"></a>glTF Viewer 模型快速预览工具<a class=\"post-anchor\" href=\"#glTF-Viewer-模型快速预览工具\" aria-hidden=\"true\"></a></h5>\n<p>在设计师建模完成导出后,设计师并不知道在 Three.js 最终会呈现一个什么效果或者开发者也想快速的查看模型是否存在问题,glTF 官方贴心的提供了一款快速预览的工具,提供了两个版本:<a href=\"https://gltf-viewer.donmccurdy.com/\" target=\"_blank\" rel=\"external\">Web 版本</a>和 <a href=\"https://github.com/donmccurdy/three-gltf-viewer/releases\" target=\"_blank\" rel=\"external\">Desktop 版本</a>。</p>\n<p>将 <code>.gltf</code>、<code>.bin</code>、<code>.jpg/.png</code> 文件拖拽到工具中,可以调试预览到模型的动画、变形目标、背景、线框模式、自动旋转、光源等功能。</p>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/cannonjs/gltf_viewer.jpg\" alt=\"gltf_viewer\"></p>\n<h5 id=\"Helper-相关调试模式\" class=\"post-heading\"><a href=\"#Helper-相关调试模式\" class=\"headerlink\" title=\"Helper 相关调试模式\"></a>Helper 相关调试模式<a class=\"post-anchor\" href=\"#Helper-相关调试模式\" aria-hidden=\"true\"></a></h5>\n<h6 id=\"Camera-Helper-摄像机调试模式\" class=\"post-heading\"><a href=\"#Camera-Helper-摄像机调试模式\" class=\"headerlink\" title=\"Camera Helper 摄像机调试模式\"></a>Camera Helper 摄像机调试模式<a class=\"post-anchor\" href=\"#Camera-Helper-摄像机调试模式\" aria-hidden=\"true\"></a></h6>\n<p>开启 Camera Helper 调试模式后,可以直观的看到 Camera 的 <code>Fov</code>、 <code>Nera</code>、<code>Far</code> 的参数效果。</p>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/cannonjs/camera_helper.jpg\" alt=\"camera_helper\"></p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">let</span> camera = \n <span class=\"keyword\">new</span> THREE.PerspectiveCamera(\n <span class=\"number\">45</span>, \n <span class=\"built_in\">window</span>.innerWidth / \n <span class=\"built_in\">window</span>.innerHeight, \n <span class=\"number\">0.1</span>, \n <span class=\"number\">1000</span>)\n </div>\n <div class=\"line\">\n <span class=\"keyword\">let</span> helper = \n <span class=\"keyword\">new</span> THREE.CameraHelper(camera)\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n scene.add(helper)\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h6 id=\"Light-Helper-光源调试模式\" class=\"post-heading\"><a href=\"#Light-Helper-光源调试模式\" class=\"headerlink\" title=\"Light Helper 光源调试模式\"></a>Light Helper 光源调试模式<a class=\"post-anchor\" href=\"#Light-Helper-光源调试模式\" aria-hidden=\"true\"></a></h6>\n<p>聚光灯开启 Light Helper 调试模式后,可以直观的看到 <code>distance</code>、<code>angle</code> 的参数效果。</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">let</span> light = \n <span class=\"keyword\">new</span> THREE.DirectionalLight(\n <span class=\"number\">0xffffff</span>)\n </div>\n <div class=\"line\">\n <span class=\"keyword\">let</span> helper = \n <span class=\"keyword\">new</span> THREE.DirectionalLightHelper(\n <span class=\"number\">0xffffff</span>)\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n scene.add(helper)\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/cannonjs/light_helper.jpg\" alt=\"light_helper\"></p>\n<h6 id=\"AxesHelper-坐标轴调试模式\" class=\"post-heading\"><a href=\"#AxesHelper-坐标轴调试模式\" class=\"headerlink\" title=\"AxesHelper 坐标轴调试模式\"></a>AxesHelper 坐标轴调试模式<a class=\"post-anchor\" href=\"#AxesHelper-坐标轴调试模式\" aria-hidden=\"true\"></a></h6>\n<p>AxesHelper 是在场景的中心点,添加一个坐标轴(红色:X 轴、绿色:Y 轴、蓝色:Z 轴),方便辨别方向。</p>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/cannonjs/axes_helper.jpg\" alt=\"axes_helper\"></p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">let</span> axesHelper = \n <span class=\"keyword\">new</span> THREE.AxesHelper(\n <span class=\"number\">10</span>)\n </div>\n <div class=\"line\">\n scene.add(axesHelper)\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h5 id=\"Cannon-js-3D-物理引擎调试模式\" class=\"post-heading\"><a href=\"#Cannon-js-3D-物理引擎调试模式\" class=\"headerlink\" title=\"Cannon.js 3D 物理引擎调试模式\"></a>Cannon.js 3D 物理引擎调试模式<a class=\"post-anchor\" href=\"#Cannon-js-3D-物理引擎调试模式\" aria-hidden=\"true\"></a></h5>\n<p>Cannon.js 3D 物理引擎提供的调试模式需引入 <a href=\"https://github.com/schteppe/cannon.js/tree/master/tools/threejs\" target=\"_blank\" rel=\"external\">Debug renderer for Three.js</a>,可以将创建的物理盒子、球、平面等显示线框,便于在使用过程中实时查看效果。</p>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/cannonjs/before_after.jpg\" alt=\"before_after\"></p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">let</span> cannonDebugRenderer = \n <span class=\"keyword\">new</span> THREE.CannonDebugRenderer(scene, world)\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"function\"><span class=\"keyword\">function</span> <span class=\"title\">render</span>(<span class=\"params\"></span>) </span>{\n </div>\n <div class=\"line\">\n requestAnimationFrame(render)\n </div>\n <div class=\"line\">\n cannonDebugRenderer.update() \n <span class=\"comment\">// Update the debug renderer</span>\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h5 id=\"dat-GUI-图形用户界面调试工具\" class=\"post-heading\"><a href=\"#dat-GUI-图形用户界面调试工具\" class=\"headerlink\" title=\"dat.GUI 图形用户界面调试工具\"></a>dat.GUI 图形用户界面调试工具<a class=\"post-anchor\" href=\"#dat-GUI-图形用户界面调试工具\" aria-hidden=\"true\"></a></h5>\n<p>在开发过程中,常常需要对参数变量进行微调,针对这个 Three.js 提供了 <a href=\"https://github.com/dataarts/dat.gui\" target=\"_blank\" rel=\"external\">dat.GUI</a>,dat.GUI 是一个轻量级的图形用户界面调试工具,使用后在右上角会出现一个 GUI 可视化参数配置区域,通过修改数值来实时查看结果。</p>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/cannonjs/dat_gui.jpg\" alt=\"dat_gui\"></p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div>\n <div class=\"line\">\n 16\n </div>\n <div class=\"line\">\n 17\n </div>\n <div class=\"line\">\n 18\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">let</span> opts = {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">x</span>: \n <span class=\"number\">0</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">y</span>: \n <span class=\"number\">0</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">scale</span>: \n <span class=\"number\">1</span>\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"keyword\">let</span> gui = \n <span class=\"keyword\">new</span> dat.GUI()\n </div>\n <div class=\"line\">\n gui.add(opts, \n <span class=\"string\">\'x\'</span>, \n <span class=\"number\">-3</span>, \n <span class=\"number\">3</span>)\n </div>\n <div class=\"line\">\n gui.add(opts, \n <span class=\"string\">\'y\'</span>, \n <span class=\"number\">-3</span>, \n <span class=\"number\">3</span>)\n </div>\n <div class=\"line\">\n gui.add(opts, \n <span class=\"string\">\'scale\'</span>, \n <span class=\"number\">1</span>, \n <span class=\"number\">3</span>)\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"function\"><span class=\"keyword\">function</span> <span class=\"title\">loop</span>(<span class=\"params\"></span>) </span>{\n </div>\n <div class=\"line\">\n cube.position.x = opt.x\n </div>\n <div class=\"line\">\n cube.position.y = opt.y\n </div>\n <div class=\"line\">\n cube.scale.set(opts.scale, opts.scale, opts.scale)\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n requestAnimationFrame()\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h5 id=\"Stats-调试工具\" class=\"post-heading\"><a href=\"#Stats-调试工具\" class=\"headerlink\" title=\"Stats 调试工具\"></a>Stats 调试工具<a class=\"post-anchor\" href=\"#Stats-调试工具\" aria-hidden=\"true\"></a></h5>\n<p><a href=\"https://github.com/mrdoob/stats.js/\" target=\"_blank\" rel=\"external\">Stats 工具</a>可以实时查看:</p>\n<p>FPS:最后一秒的帧数,越大越流畅</p>\n<p>MS:渲染一帧需要的时间(毫秒),越低越好</p>\n<p>MB:占用的内存信息</p>\n<p>CUSTOM:自定义面板</p>\n<p><img src=\"//misc.aotu.io/ONE-SUNDAY/cannonjs/stats.jpg\" alt=\"stats\"></p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">var</span> stats = \n <span class=\"keyword\">new</span> Stats()\n </div>\n <div class=\"line\">\n stats.showPanel(\n <span class=\"number\">1</span>)\n </div>\n <div class=\"line\">\n <span class=\"built_in\">document</span>.body.appendChild(stats.dom)\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"function\"><span class=\"keyword\">function</span> <span class=\"title\">animate</span>(<span class=\"params\"></span>) </span>{\n </div>\n <div class=\"line\">\n requestAnimationFrame(animate)\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n requestAnimationFrame(animate)\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h2 id=\"尾巴\" class=\"post-heading\"><a href=\"#尾巴\" class=\"headerlink\" title=\"尾巴\"></a>尾巴<a class=\"post-anchor\" href=\"#尾巴\" aria-hidden=\"true\"></a></h2>\n<p>最后,希望本篇文章所讲到的内容能帮助你更好的开发 3D 项目。另外,如果你有更好的实现思路,也欢迎你在下方评论区留言,感谢您的阅读。</p>\n<h2 id=\"参考\" class=\"post-heading\"><a href=\"#参考\" class=\"headerlink\" title=\"参考\"></a>参考<a class=\"post-anchor\" href=\"#参考\" aria-hidden=\"true\"></a></h2>\n<ul>\n <li>《Learning Three.js》</li>\n <li><a href=\"https://threejs.org/\" target=\"_blank\" rel=\"external\">threejs</a></li>\n <li><a href=\"https://www.babylonjs.com/\" target=\"_blank\" rel=\"external\">babylonjs</a></li>\n <li><a href=\"https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md\" target=\"_blank\" rel=\"external\">glTF 2.0 README</a></li>\n <li><a href=\"https://github.com/schteppe/cannon.js\" target=\"_blank\" rel=\"external\">Cannon.js</a></li>\n <li><a href=\"https://github.com/schteppe/cannon.js/tree/master/tools/threejs\" target=\"_blank\" rel=\"external\">Debug renderer for Three.js</a></li>\n <li><a href=\"https://github.com/donmccurdy/three-gltf-viewer\" target=\"_blank\" rel=\"external\">three-gltf-viewer</a></li>\n <li><a href=\"https://github.com/mrdoob/stats.js/#javascript-performance-monitor\" target=\"_blank\" rel=\"external\">stats.js</a></li>\n</ul>\n<div class=\"post-tags\" style=\"display:none\">\n <a href=\"/tags/Three-js/\">Three.js</a> \n <a href=\"/tags/Cannon-js/\">Cannon.js</a> \n <a href=\"/tags/glTF/\">glTF</a>\n</div>\n<div class=\"post-categories\" style=\"display:none\">\n <a href=\"/cates/H5游戏开发/\">H5游戏开发</a>\n</div>\n<div class=\"post-announce\">\n 感谢您的阅读,本文由 \n <a href=\"//aotu.io\">凹凸实验室</a> 版权所有。如若转载,请注明出处:凹凸实验室(\n <a href=\"https://aotu.io/notes/2018/10/18/cannonjs/\">https://aotu.io/notes/2018/10/18/cannonjs/</a>)\n</div>\n<div class=\"post-revision\">\n <i class=\"fa fa-clock-o\"></i> \n <time class=\"post-updated\" datetime=\"2018-11-07T01:01:15.657Z\">上次更新:2018-11-07 09:01:15</time>\n</div>');
INSERT INTO `rumo_blog` VALUES ('28', 'Nerv实战 - 京东首页改版小结', '1', 'Nerv实战 - 京东首页改版小结', '1', '2018-11-11 21:02:33', '2018-11-11 21:02:33', '1', '0', 'https://misc.aotu.io/Littly/18-04-24_jdindex/17cover.png', '', '0', '0', '<style>.post strong{font-weight:700}</style>\n<a id=\"more\"></a>\n<blockquote>\n <p>回想17年的京东首页改版,从上线到现在竟然已经过去了四个多月。这四个多月,除了不曾中断的日常维护需求,对首页孜孜不倦的优化工作,更多的是那些与拖延症抗争的日夜:是今天写,还是等好好休憩回味后再动手?很明显,在这几十上百个日夜里,我基本都选择了第三个选项:<strong>不折腾了,先休息吧</strong>。现在想起来,关于那一个月白加黑五加二加班生活的印象已经渐渐模糊。到现在依然能清晰记着的,大概是最为深刻的记忆了。</p>\n</blockquote>\n<p>16版的京东首页,在性能、体验、灾备策略等各方面都做到了极致。站在如此高大的巨人肩上,除了满满的自信,我们心里更怕扑街。毫无疑问,我们在接到改版需求的那一刻,立马就敲定了新首页的技术选型:妥妥的<code>jQuery</code> + <code>SeaJS</code>!</p>\n<p>但很快,我们就发现这样一点都不酷。<code>jQuery</code>是2006年的框架了,<code>SeaJS</code>停止维护也已经四年。这些项目的诞生都是为了解决当时业界的一些痛点:比如<code>jQuery</code>最开始是为了方便程序员在页面中操作DOM,绑定事件等;<code>SeaJS</code>则是为了在浏览器中实现CMD规范的模块开发和加载。但在各种VirtualDOM框架横飞的现在,程序员已经很少会直接操作DOM元素,而模块的开发和加载也有早已有了别的方案。</p>\n<p>就在这时,<a href=\"https://github.com/NervJS/nerv\" target=\"_blank\" rel=\"external\">Nerv</a>项目的作者提出了建议:“<strong>不然用Nerv来一发?</strong>”我记得,当时他脸上洋溢着淳朴的笑,Nerv也仅仅是部门内部的一个小项目。我们回想了首页这个业务,技术栈已经好几年未曾更新过,开发流程也不够理想。如果再不做出改变,明年的这个时候我们依然会面对一堆陈年老代码头疼不已。抱着试一试的心态,我们接受了他的提议。没想到,这个决定让首页从此摆脱了落后的技术架构,而Nerv现在也已经成长为GitHub上3k+ Star的热门项目。</p>\n<blockquote>\n <p>Q: 为什么不使用<code>React</code>/<code>Preact</code>/<code>Vue</code>?</p>\n <p>A: 这三者都是前端圈子中相当流行的项目。<code>React</code>有完善的体系和浓厚的社区氛围,<code>Preact</code>有着羞涩的体积,<code>Vue</code>则使用了先进的html模板和数据绑定机制。但是,上边这三者都 <strong>无法兼容IE8</strong>。我们在经过相关数据的论证后,发现IE8的用户还是有一定的价值,这才最终激发了我们团队内部自己造轮子的想法。当然,在造轮子的过程中,我们也不忘向上面这些优秀框架的看齐。最终,<code>Nerv</code>在完美兼容React语法的同时,具有着出众的性能表现,在Gzip后也只占用9Kb的体积。</p>\n</blockquote>\n<h2 id=\"整体架构\" class=\"post-heading\"><a href=\"#整体架构\" class=\"headerlink\" title=\"整体架构\"></a>整体架构<a class=\"post-anchor\" href=\"#整体架构\" aria-hidden=\"true\"></a></h2>\n<p>在这次的项目中,我们基于上一年久经考验的前端体系(<a href=\"https://aotu.io/notes/2016/12/26/jd-index-2016-summary/#%E6%95%B4%E4%BD%93%E6%9E%B6%E6%9E%84\">详细介绍</a>),进行了升级:</p>\n<p><img src=\"https://misc.aotu.io/Littly/18-04-24_jdindex/structure.png\" alt=\"整体架构图\"></p>\n<ul>\n <li>Athena前端工程化工具:团队自研的前端工程化工具。除了自动化编译、代码处理、依赖分析、文件压缩等常规需求,2.0版本还支持 <strong>基于npm的依赖管理</strong>,<strong>更加先进的引入、导出机制</strong>,还有 <strong>最新的es语言特性</strong>。</li>\n <li>Athena管理平台:新增了 <strong>针对Nerv的项目模板</strong>,另外还有针对H5项目的特色模板可选。</li>\n <li>Athena基础库与组件库:新增了基于<code>jQuery</code>+<code>SeaJS</code>的组件重构,<strong>全新升级的Nerv组件</strong>。</li>\n <li>Athena模拟接口:除了已有的mock接口数据的能力,还支持 <strong>接口文档生成</strong>,便于沉淀项目接口信息。</li>\n <li>Athena兜底接口:可以定时抓取线上接口的数据 <strong>生成兜底数据</strong>,还支持 <strong>接口数据校验</strong>,评估接口健康度。</li>\n <li>Athena前端监控:我们部署了一系列的监控服务,对页面上的素材以及页面的完整功能进行监控。一旦图片尺寸/体积超限,某些特定的操作出现异常,或者接口成功率降低等异常情况,就会触发告警推送,开发者可以 <strong>实时收到告警信息</strong>。</li>\n <li>Athena可视化报表:Athena可视化报表平台上对上报的数据都有 <strong>直观的展示</strong>。</li>\n</ul>\n<h2 id=\"开发模式\" class=\"post-heading\"><a href=\"#开发模式\" class=\"headerlink\" title=\"开发模式\"></a>开发模式<a class=\"post-anchor\" href=\"#开发模式\" aria-hidden=\"true\"></a></h2>\n<h3 id=\"Athena2-0\" class=\"post-heading\"><a href=\"#Athena2-0\" class=\"headerlink\" title=\"Athena2.0\"></a>Athena2.0<a class=\"post-anchor\" href=\"#Athena2-0\" aria-hidden=\"true\"></a></h3>\n<p>1.0版本的<code>Athena</code>,基于<code>vinyl-fs</code>的流操作,或者说是类似于gulp的压缩、编译等等操作的任务流。而到了2017年,<code>webpack</code>早已在前端圈中流行。同行们也早已经习惯在项目中直接基于最新的语言特性去开发,在<code>webpack.config.js</code>加上一个<code>babel-loader</code>就可以完美支持新语法并完成打包。Athena 1.0背着太沉重的历史包袱,已经很难快速实现对babel转译的支持。所以在首页的开发前,我们将Athena升级到了全新的2.0版本。</p>\n<p>一如既往,Athena会为项目提供<code>init</code>(初始化),<code>serve</code>(实时预览),<code>build</code>(编译),<code>publish</code>(发布)等功能。除此之外,由于2.0版本的Athena是基于<code>webpack</code>的,所以项目中可以 <strong>统一用npm来管理依赖</strong>,也可以直接 <strong>使用最新的ES语言特性</strong>来进行开发。</p>\n<p>使用Athena2.0开发时,建议的文件架构如下:</p>\n<p><img src=\"https://misc.aotu.io/Littly/18-04-24_jdindex/files.png\" alt=\"文件架构\"></p>\n<h3 id=\"前后端协作\" class=\"post-heading\"><a href=\"#前后端协作\" class=\"headerlink\" title=\"前后端协作\"></a>前后端协作<a class=\"post-anchor\" href=\"#前后端协作\" aria-hidden=\"true\"></a></h3>\n<p>我们依然是采用了 <strong>前后端分离</strong>的协作模式,由后端给出json格式的数据,前端拉取json数据进行渲染。对于大部分的组件来说,都会在<code>constructor</code>中做好组件的初始化工作,在<code>componentDidMount</code>的生命周期中拉取数据写入组件的<code>state</code>,再通过<code>render</code>函数进行渲染。</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div>\n <div class=\"line\">\n 16\n </div>\n <div class=\"line\">\n 17\n </div>\n <div class=\"line\">\n 18\n </div>\n <div class=\"line\">\n 19\n </div>\n <div class=\"line\">\n 20\n </div>\n <div class=\"line\">\n 21\n </div>\n <div class=\"line\">\n 22\n </div>\n <div class=\"line\">\n 23\n </div>\n <div class=\"line\">\n 24\n </div>\n <div class=\"line\">\n 25\n </div>\n <div class=\"line\">\n 26\n </div>\n <div class=\"line\">\n 27\n </div>\n <div class=\"line\">\n 28\n </div>\n <div class=\"line\">\n 29\n </div>\n <div class=\"line\">\n 30\n </div>\n <div class=\"line\">\n 31\n </div>\n <div class=\"line\">\n 32\n </div>\n <div class=\"line\">\n 33\n </div>\n <div class=\"line\">\n 34\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">/* myComponent.js */</span>\n </div>\n <div class=\"line\"> \n </div>\n <div class=\"line\">\n <span class=\"keyword\">import</span> Nerv \n <span class=\"keyword\">from</span> \n <span class=\"string\">\'nervjs\'</span>\n </div>\n <div class=\"line\"> \n </div>\n <div class=\"line\">\n <span class=\"class\"><span class=\"keyword\">class</span> <span class=\"title\">MyComponent</span> <span class=\"keyword\">extends</span> <span class=\"title\">Nerv</span>.<span class=\"title\">Component</span> </span>{\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">constructor</span>() {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">super</span>(...arguments)\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">this</span>.state = {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">xxx</span>: \n <span class=\"string\">\'xxx\'</span>\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">async</span> requestData() {\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// return await fetch(\'xxx\').then(res =&gt; res.json())</span>\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n componentDidMount() {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">this</span>.requestData()\n </div>\n <div class=\"line\">\n .then(\n <span class=\"function\"><span class=\"params\">data</span> =&gt;</span> {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">this</span>.setState({\n </div>\n <div class=\"line\"> \n <span class=\"attr\">xxx</span>: \n <span class=\"string\">\'yyy\'</span>\n </div>\n <div class=\"line\">\n })\n </div>\n <div class=\"line\">\n }).catch(\n <span class=\"function\"><span class=\"params\">()</span> =&gt;</span> {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">this</span>.setState({\n </div>\n <div class=\"line\"> \n <span class=\"attr\">xxx</span>: \n <span class=\"string\">\'zzz\'</span>\n </div>\n <div class=\"line\">\n })\n </div>\n <div class=\"line\">\n })\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n render() {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">return</span> (\n </div>\n <div class=\"line\"> \n <span class=\"xml\"><span class=\"tag\">&lt;<span class=\"name\">div</span>&gt;</span>{this.state.xxx}<span class=\"tag\">&lt;/<span class=\"name\">div</span>&gt;</span></span>\n </div>\n <div class=\"line\">\n )\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"> \n </div>\n <div class=\"line\">\n <span class=\"keyword\">export</span> \n <span class=\"keyword\">default</span> MyComponent\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h3 id=\"代码规范约束\" class=\"post-heading\"><a href=\"#代码规范约束\" class=\"headerlink\" title=\"代码规范约束\"></a>代码规范约束<a class=\"post-anchor\" href=\"#代码规范约束\" aria-hidden=\"true\"></a></h3>\n<blockquote>\n <p>有一千个读者,就会有一千个哈姆雷特。</p>\n</blockquote>\n<p>上面这句名言,深刻地体现在了16版首页的代码仓库中。同一个组件,如果是基于<code>jQuery</code>+<code>SeaJS</code>的模式,一千个程序猿就会有一千种写法。结果在同一个项目中,代码风格不尽相同,代码质量良莠不齐,多人协作也会无从下手。</p>\n<p>何以解忧?唯有统一代码风格了。通过<code>ESLint</code>+<code>Husky</code>,我们对每次代码提交都做了代码风格检查,对是否使用prefer const的变量声明、代码缩进该使用Tab还是空格等等的规则都做了约束。一开始定下规范的时候,团队成员或多或少都会有些不习惯。但通过偷偷在<del>代码里下毒</del>Athena的生成的项目模板中添加对应的规则,潜移默化地,团队成员们也都开始接受、习惯这些约束。</p>\n<p><img src=\"https://misc.aotu.io/Littly/18-04-24_jdindex/gitflow.png\" alt=\"Git提交流程\"></p>\n<p>禁用变量重声明等规则,在一定程度上保证了代码质量;而统一的代码样式风格,则使得项目的多人协作更加便利。</p>\n<blockquote>\n <p>Q: 保证代码质量,促进多人协作的终极好处是什么?</p>\n <p>A: 由于项目代码风格统一,通俗易懂容易上手,我们首页的开发团队终于开始 <strong>有妹纸加入了</strong>!<del>一群雄性程序猿敲代码能敲出什么火花啊…</del></p>\n</blockquote>\n<h2 id=\"对性能优化的探索\" class=\"post-heading\"><a href=\"#对性能优化的探索\" class=\"headerlink\" title=\"对性能优化的探索\"></a>对性能优化的探索<a class=\"post-anchor\" href=\"#对性能优化的探索\" aria-hidden=\"true\"></a></h2>\n<h3 id=\"首屏直出\" class=\"post-heading\"><a href=\"#首屏直出\" class=\"headerlink\" title=\"首屏直出\"></a>首屏直出<a class=\"post-anchor\" href=\"#首屏直出\" aria-hidden=\"true\"></a></h3>\n<p>直出可能是加快首屏加载最行之有效的办法了。它在减少页面加载时间、首屏请求数等方面的好处自然不必再提,结合<code>jQuery</code>,也可以很方便地在直出的DOM上进行更多的操作。</p>\n<p><code>Nerv</code>框架对于它内部的组件、DOM有着良好的操作性,但是 <strong>对于体系外的DOM节点</strong>,却是天生的 <strong>操作无力</strong>。举个例子,比如在页面文件中我们直出一个轮播图:</p>\n<figure class=\"highlight html\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div>\n <div class=\"line\">\n 16\n </div>\n <div class=\"line\">\n 17\n </div>\n <div class=\"line\">\n 18\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"tag\">&lt;<span class=\"name\">div</span> <span class=\"attr\">id</span>=<span class=\"string\">\"example\"</span> <span class=\"attr\">class</span>=<span class=\"string\">\"mod_slider\"</span>&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">a</span> <span class=\"attr\">class</span>=<span class=\"string\">\"mod_slider_prev\"</span> /&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">ul</span> <span class=\"attr\">class</span>=<span class=\"string\">\"mod_slider_list\"</span>&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">li</span> <span class=\"attr\">class</span>=<span class=\"string\">\"mod_slider_item\"</span>&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">a</span> <span class=\"attr\">href</span>=<span class=\"string\">\"javascript:;\"</span>&gt;</span>\n <span class=\"tag\">&lt;<span class=\"name\">img</span> <span class=\"attr\">src</span>=<span class=\"string\">\"//www.example.com/example.png\"</span> /&gt;</span>\n <span class=\"tag\">&lt;/<span class=\"name\">a</span>&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;/<span class=\"name\">li</span>&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">li</span> <span class=\"attr\">class</span>=<span class=\"string\">\"mod_slider_item\"</span>&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">a</span> <span class=\"attr\">href</span>=<span class=\"string\">\"javascript:;\"</span>&gt;</span>\n <span class=\"tag\">&lt;<span class=\"name\">img</span> <span class=\"attr\">src</span>=<span class=\"string\">\"//www.example.com/example.png\"</span> /&gt;</span>\n <span class=\"tag\">&lt;/<span class=\"name\">a</span>&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;/<span class=\"name\">li</span>&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">li</span> <span class=\"attr\">class</span>=<span class=\"string\">\"mod_slider_item\"</span>&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">a</span> <span class=\"attr\">href</span>=<span class=\"string\">\"javascript:;\"</span>&gt;</span>\n <span class=\"tag\">&lt;<span class=\"name\">img</span> <span class=\"attr\">src</span>=<span class=\"string\">\"//www.example.com/example.png\"</span> /&gt;</span>\n <span class=\"tag\">&lt;/<span class=\"name\">a</span>&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;/<span class=\"name\">li</span>&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">li</span> <span class=\"attr\">class</span>=<span class=\"string\">\"mod_slider_item\"</span>&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">a</span> <span class=\"attr\">href</span>=<span class=\"string\">\"javascript:;\"</span>&gt;</span>\n <span class=\"tag\">&lt;<span class=\"name\">img</span> <span class=\"attr\">src</span>=<span class=\"string\">\"//www.example.com/example.png\"</span> /&gt;</span>\n <span class=\"tag\">&lt;/<span class=\"name\">a</span>&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;/<span class=\"name\">li</span>&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;/<span class=\"name\">ul</span>&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">a</span> <span class=\"attr\">class</span>=<span class=\"string\">\"mod_slider_next\"</span> /&gt;</span>\n </div>\n <div class=\"line\">\n <span class=\"tag\">&lt;/<span class=\"name\">div</span>&gt;</span>\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>使用<code>Nerv</code>为这段HTML添加轮播逻辑,成为了非常艰难的操作。终极的解决方案,应该是使用 <strong>SSR(Server Side Render)</strong>的方案,搭建<code>Nerv-server</code>中间层来将组件直出。但现在革命尚未成功,首屏直出尚且依赖后端的研发同学,首页上线又迫在眉睫。被逼急的我们最终选择了比较trick的方式来过渡这个问题:在组件初始化的时候先通过DOM操作获取渲染所需的数据,再将DOM替换成<code>Nerv</code>渲染后的内容。</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div>\n <div class=\"line\">\n 16\n </div>\n <div class=\"line\">\n 17\n </div>\n <div class=\"line\">\n 18\n </div>\n <div class=\"line\">\n 19\n </div>\n <div class=\"line\">\n 20\n </div>\n <div class=\"line\">\n 21\n </div>\n <div class=\"line\">\n 22\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">import</span> Slider \n <span class=\"keyword\">from</span> \n <span class=\"string\">\'path/to/slider\'</span>\n </div>\n <div class=\"line\">\n <span class=\"class\"><span class=\"keyword\">class</span> <span class=\"title\">Example</span> <span class=\"keyword\">extends</span> <span class=\"title\">Nerv</span>.<span class=\"title\">Component</span> </span>{\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">constructor</span>() {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">const</span> ctn = \n <span class=\"keyword\">this</span>.props.ctn\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">this</span>.state = {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">data</span>: \n <span class=\"keyword\">this</span>.gatherInfos()\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n ctn.innerHTML = \n <span class=\"string\">\'\'</span>\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n gatherInfos() {\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 返回已有DOM中的链接,图片url等信息</span>\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n render() {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">return</span> (\n </div>\n <div class=\"line\"> \n <span class=\"xml\"><span class=\"tag\">&lt;<span class=\"name\">Slider</span>&gt;</span></span>\n </div>\n <div class=\"line\">\n {this.data.map(v =&gt; \n <span class=\"tag\">&lt;<span class=\"name\">li</span> /&gt;</span> )}\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;/<span class=\"name\">Slider</span>&gt;</span>\n </div>\n <div class=\"line\">\n )\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n <span class=\"keyword\">const</span> el = \n <span class=\"built_in\">document</span>.querySelector(\n <span class=\"string\">\'#example\'</span>)\n </div>\n <div class=\"line\">\n Nerv.render(\n <span class=\"xml\"><span class=\"tag\">&lt;<span class=\"name\">Example</span> <span class=\"attr\">ctn</span>=<span class=\"string\">{el}</span> /&gt;</span>, el)</span>\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h3 id=\"代码分割\" class=\"post-heading\"><a href=\"#代码分割\" class=\"headerlink\" title=\"代码分割\"></a>代码分割<a class=\"post-anchor\" href=\"#代码分割\" aria-hidden=\"true\"></a></h3>\n<p>在生产环境中,随着代码体积增大,浏览器解压Gzip、执行等操作也会需要更多的开销。在SeaJS的时代,我们尚且会通过<code>SeaJS.use</code>或者<code>require.async</code>异步加载模块代码,避免一次性加载过多内容。但<code>webpack</code>的默认行为却会将整个页面的代码打包为一个单独的文件,这明显不是最佳的实践。对此,webpack给出的解决方案是<a href=\"https://webpack.js.org/guides/code-splitting/#dynamic-imports\" target=\"_blank\" rel=\"external\">动态引入(Dynamic Imports)</a>。我们可以通过如下的代码来使用这个便利的特性:</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">import</span>(\n </div>\n <div class=\"line\"> \n <span class=\"comment\">/* webpackChunkName: ${chunkName} */</span>\n </div>\n <div class=\"line\"> \n <span class=\"string\">\'${chunkName}\'</span>\n </div>\n <div class=\"line\">\n ).then(\n <span class=\"function\">(<span class=\"params\">loaded</span>) =&gt;</span> {\n </div>\n <div class=\"line\"> \n <span class=\"comment\">/* Do anything to the loaded module */</span>\n </div>\n <div class=\"line\">\n })\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>与此同时,<code>webpack</code>会将使用了动态引入的组件从主bundle文件中抽离出来,这就 <strong>减小了主bundle文件的体积</strong>。</p>\n<p><img src=\"https://misc.aotu.io/Littly/18-04-24_jdindex/code_splitting.png\" alt=\"构建后的代码\"></p>\n<p>对于我们的具体需求而言,需要做动态引入的一般是<code>Nerv</code>的组件。对于组件的动态引入,业界已经有非常好的实现方案 <a href=\"https://github.com/jamiebuilds/react-loadable\" target=\"_blank\" rel=\"external\">react-loadable</a>。举个栗子,通过下面的代码,我们可以在页面中使用<code>&lt;LoadableMyComponent /&gt;</code>来实现对组件<code>MyComponent</code>的动态引入,并且具有 <strong>加载超时、错误、加载中</strong>等不同状态的展示:</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div>\n <div class=\"line\">\n 16\n </div>\n <div class=\"line\">\n 17\n </div>\n <div class=\"line\">\n 18\n </div>\n <div class=\"line\">\n 19\n </div>\n <div class=\"line\">\n 20\n </div>\n <div class=\"line\">\n 21\n </div>\n <div class=\"line\">\n 22\n </div>\n <div class=\"line\">\n 23\n </div>\n <div class=\"line\">\n 24\n </div>\n <div class=\"line\">\n 25\n </div>\n <div class=\"line\">\n 26\n </div>\n <div class=\"line\">\n 27\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">/* loadableMyComponent.js */</span>\n </div>\n <div class=\"line\"> \n </div>\n <div class=\"line\">\n <span class=\"keyword\">import</span> Loadable \n <span class=\"keyword\">from</span> \n <span class=\"string\">\'react-loadable\'</span>;\n </div>\n <div class=\"line\"> \n </div>\n <div class=\"line\">\n <span class=\"keyword\">const</span> LoadableMyComponent = Loadable({\n </div>\n <div class=\"line\"> \n <span class=\"attr\">loader</span>: \n <span class=\"function\"><span class=\"params\">()</span> =&gt;</span> \n <span class=\"keyword\">import</span>(\n <span class=\"string\">\'MyComponent\'</span>),\n </div>\n <div class=\"line\"> \n <span class=\"attr\">delay</span>: \n <span class=\"number\">300</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">loading</span>: \n <span class=\"function\">(<span class=\"params\">props</span>) =&gt;</span> {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span> (props.error) {\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 加载错误</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">return</span> \n <span class=\"xml\"><span class=\"tag\">&lt;<span class=\"name\">div</span>&gt;</span>Error!<span class=\"tag\">&lt;/<span class=\"name\">div</span>&gt;</span></span>;\n </div>\n <div class=\"line\">\n } \n <span class=\"keyword\">else</span> \n <span class=\"keyword\">if</span> (props.timedOut) {\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 加载超时</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">return</span> \n <span class=\"xml\"><span class=\"tag\">&lt;<span class=\"name\">div</span>&gt;</span>TimedOut!<span class=\"tag\">&lt;/<span class=\"name\">div</span>&gt;</span></span>; \n </div>\n <div class=\"line\">\n } \n <span class=\"keyword\">else</span> \n <span class=\"keyword\">if</span> (props.pastDelay) {\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// 加载占位符</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">return</span> \n <span class=\"xml\"><span class=\"tag\">&lt;<span class=\"name\">div</span>&gt;</span>Loading...<span class=\"tag\">&lt;/<span class=\"name\">div</span>&gt;</span></span>;\n </div>\n <div class=\"line\">\n } \n <span class=\"keyword\">else</span> {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">return</span> \n <span class=\"literal\">null</span>;\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n },\n </div>\n <div class=\"line\"> \n <span class=\"attr\">render</span>: \n <span class=\"function\">(<span class=\"params\">loaded, props</span>) =&gt;</span> {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">return</span> \n <span class=\"xml\"><span class=\"tag\">&lt;<span class=\"name\">loaded.default</span> {<span class=\"attr\">...props</span>}/&gt;</span>;</span>\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n });\n </div>\n <div class=\"line\"> \n </div>\n <div class=\"line\">\n export default LoadableMyComponent\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>再进一步,我们希望对于屏幕外的组件,仅仅是在它进入用户视野后再开始加载,这也就是我们常说的滚动懒加载。这可以结合业界已有的懒加载组件<a href=\"https://github.com/jasonslyvia/react-lazyload\" target=\"_blank\" rel=\"external\">react-lazyload</a>来实现。针对上面的<code>&lt;LoadableMyComponent /&gt;</code>,在下面的例子中,只有进入用户屏幕后,<code>MyComponent</code>才会开始加载:</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div>\n <div class=\"line\">\n 16\n </div>\n <div class=\"line\">\n 17\n </div>\n <div class=\"line\">\n 18\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">/* myApp.js */</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">import</span> Nerv \n <span class=\"keyword\">from</span> \n <span class=\"string\">\'nervjs\'</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">import</span> LazyLoad \n <span class=\"keyword\">from</span> \n <span class=\"string\">\'react-lazyload\'</span>;\n </div>\n <div class=\"line\">\n <span class=\"keyword\">import</span> LoadableMyComponent \n <span class=\"keyword\">from</span> \n <span class=\"string\">\'./loadableMyComponent\'</span>;\n </div>\n <div class=\"line\"> \n </div>\n <div class=\"line\">\n <span class=\"class\"><span class=\"keyword\">class</span> <span class=\"title\">MyApp</span> <span class=\"keyword\">extends</span> <span class=\"title\">Nerv</span>.<span class=\"title\">Component</span> </span>{\n </div>\n <div class=\"line\">\n render () {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">return</span> (\n </div>\n <div class=\"line\"> \n <span class=\"xml\"><span class=\"tag\">&lt;<span class=\"name\">div</span> <span class=\"attr\">className</span>=<span class=\"string\">\'app\'</span>&gt;</span></span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">LazyLoad</span> <span class=\"attr\">height</span>=<span class=\"string\">{200}</span> <span class=\"attr\">placeholderClassName</span>=<span class=\"string\">{</span>\'<span class=\"attr\">mod_lazyload</span>\'}&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">LoadableMyComponent</span> /&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;/<span class=\"name\">LazyLoad</span>&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;/<span class=\"name\">div</span>&gt;</span>\n </div>\n <div class=\"line\">\n )\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"> \n </div>\n <div class=\"line\">\n <span class=\"keyword\">export</span> \n <span class=\"keyword\">default</span> MyApp\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>上面的例子为lazyload的组件设置了200px的占位高度。并且设定了占位元素的类名,方便设定样式。</p>\n<h3 id=\"代码延后加载\" class=\"post-heading\"><a href=\"#代码延后加载\" class=\"headerlink\" title=\"代码延后加载\"></a>代码延后加载<a class=\"post-anchor\" href=\"#代码延后加载\" aria-hidden=\"true\"></a></h3>\n<p>在给首页全面升级技术栈的时候,我们忽略了一个问题:页面上还引用着少量来自兄弟团队的SeaJS模块,我们升级了技术栈是可以,但是强迫兄弟团队也一起去掉SeaJS重构一遍代码,这就有点不合理了。我们也不能仅仅为了这部分模块,就把<code>SeaJS</code>给打包进代码里面,这也是不科学的。</p>\n<p>上面讲到的 <strong>动态引入</strong>功能,帮我们很好地解决了这个问题。我们在代码中单独抽离了一个<code>legacy</code>模块,其中包含了<code>SeaJS</code>、<code>SeaJS-combo</code>等老模块并做了导出。这部分代码在首屏中并不直接引入,而是在需要执行的时候,通过上面的 <strong>动态引入</strong>功能,单独请求下来使用:</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">import</span>(\n <span class=\"string\">\'../legacy\'</span>)\n </div>\n <div class=\"line\">\n .then(\n <span class=\"function\">(<span class=\"params\">{ SeaJS }</span>) =&gt;</span> {\n </div>\n <div class=\"line\">\n SeaJS.use(\n <span class=\"string\">\'xxx\'</span>, \n <span class=\"function\"><span class=\"keyword\">function</span> (<span class=\"params\">XXX</span>) </span>{\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// XXX.init();</span>\n </div>\n <div class=\"line\">\n })\n </div>\n <div class=\"line\">\n })\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h3 id=\"打包性能优化\" class=\"post-heading\"><a href=\"#打包性能优化\" class=\"headerlink\" title=\"打包性能优化\"></a>打包性能优化<a class=\"post-anchor\" href=\"#打包性能优化\" aria-hidden=\"true\"></a></h3>\n<p><code>webpack</code>默认会对整个项目引用到的文件进行编译、打包,其中还包括了<code>Nervjs</code>、<code>es5-polyfill</code>等基础的依赖库。这些文件从加入项目开始,基本都不会再有任何更改;然而在每次构建新版本时,webpack打包的这些基础库都会与上一版本有一些细微的区别,这会导致用户浏览器中对应的代码缓存失效。为此,我们考虑将这些基础库分开打包。</p>\n<p>针对这种需求,<code>webpack</code>官方建议使用<a href=\"https://webpack.js.org/plugins/dll-plugin/\" target=\"_blank\" rel=\"external\">DLL插件</a>来优化。DLL是<code>Dynamic Link Library</code>的简称,是windows系统中对于应用程序依赖的函数库的称呼。对于<code>webpack</code>,我们需要使用一个单独的<code>webpack</code>配置去生成DLL:</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">/* webpack.dll.config.js */</span>\n </div>\n <div class=\"line\"> \n </div>\n <div class=\"line\">\n plugins: [\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">new</span> webpack.DllPlugin({\n </div>\n <div class=\"line\"> \n <span class=\"attr\">path</span>: path.join(__dirname, \n <span class=\"string\">\'dist\'</span>, \n <span class=\"string\">\'lib-manifest.json\'</span>),\n </div>\n <div class=\"line\"> \n <span class=\"attr\">name</span>: \n <span class=\"string\">\'lib.dll.js\'</span>\n </div>\n <div class=\"line\">\n })\n </div>\n <div class=\"line\">\n ]\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>接下来,在我们的项目的webpack配置中引用<code>DllReferencdPlugin</code>,传入上面生成的json文件:</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">/* webpack.dev.config.js */</span>\n </div>\n <div class=\"line\"> \n </div>\n <div class=\"line\">\n plugins: [\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">new</span> webpack.DllReferencePlugin({\n </div>\n <div class=\"line\"> \n <span class=\"attr\">context</span>: __dirname,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">manifest</span>: \n <span class=\"built_in\">require</span>(\n <span class=\"string\">\'./dist/lib-manifest.json\'</span>)\n </div>\n <div class=\"line\">\n })\n </div>\n <div class=\"line\">\n ]\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>这样就完成了动态链接库的生成和引用。除了最开始的一次编译,后续开发中如果基础库没有变动,DLL就再也不需要重新编译,这也就解决了上面的代码变动的问题。</p>\n<h2 id=\"体验优化探索\" class=\"post-heading\"><a href=\"#体验优化探索\" class=\"headerlink\" title=\"体验优化探索\"></a>体验优化探索<a class=\"post-anchor\" href=\"#体验优化探索\" aria-hidden=\"true\"></a></h2>\n<h3 id=\"兼容IE8\" class=\"post-heading\"><a href=\"#兼容IE8\" class=\"headerlink\" title=\"兼容IE8\"></a>兼容IE8<a class=\"post-anchor\" href=\"#兼容IE8\" aria-hidden=\"true\"></a></h3>\n<p>兼容旧版本IE浏览器一直是前端开发人员心中永远的痛。过去,我们使用<code>jQuery</code>去统一不同浏览器的DOM操作和绑定事件,通过jQuery元素实例的map、each等类数组函数批量做JavaScript动画,等等。</p>\n<p>但是在使用<code>Nerv</code>之后,从体系外直接操作DOM就显得很不优雅;更推荐的写法,是通过组件的ref属性来访问原生DOM。而map、each等函数,IE9+的浏览器也已经在<code>Array.prototype</code>下有了相应的实现。如果我们在代码中直接引入<code>jQuery</code>,这肯定是不科学的,这将使页面的脚本体积提高许多,同时还引入了很多我们根本用不上的多余功能。</p>\n<p>面对这种情况,我们做了一个仅针对ie8的轻量级的兼容库<a href=\"https://github.com/o2team/es5-polyfill\" target=\"_blank\" rel=\"external\">es5-polyfill</a>。它包括这些实现:Object的扩展函数、ES5对<code>Array.prototype</code>的扩充、标准的<code>addEventListener</code>和<code>removeEventListener</code>等。在入口文件顶部使用<code>require(\'es5-polyfill\');</code>引入<code>es5-polyfill</code>后,只需3分钟,你就<del><strong>会甘我一样,爱上这款框架</strong></del>可以在代码中愉快地使用上面说到的那些IE8不支持的API了。</p>\n<p>但是,通过上面的CMD方式引入不就意味着对于IE9+的用户都引入了这些代码吗?这并不符合我们“随用随取,避免浪费”的原则。我们更推荐的做法,是在<code>webpack</code>中为配置多个entry,再使用<code>HTMLWebpackPlugin</code>在HTML模板中为<code>es5-polyfill</code>输出一段针对IE8的条件注释。具体实现可以参考<a href=\"https://github.com/NervJS/nerv-webpack-boilerplate\" target=\"_blank\" rel=\"external\">nerv-webpack-boilerplate</a>。</p>\n<h3 id=\"SVG-Sprite\" class=\"post-heading\"><a href=\"#SVG-Sprite\" class=\"headerlink\" title=\"SVG Sprite\"></a>SVG Sprite<a class=\"post-anchor\" href=\"#SVG-Sprite\" aria-hidden=\"true\"></a></h3>\n<p>在页面中使用SVG,可以有效提升小图标在高清屏中的体验。类似于图片Sprite,SVG也可以通过Sprite来减少页面的请求(参考文章:<a href=\"https://aotu.io/notes/2016/07/09/SVG-Symbol-component-practice/\">拥抱Web设计新趋势:SVG Sprites实践应用</a>)。</p>\n<p>举个栗子,我们在Nerv中声明svgSprite组件,用以存放页面中用到的svg小图标:</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div>\n <div class=\"line\">\n 16\n </div>\n <div class=\"line\">\n 17\n </div>\n <div class=\"line\">\n 18\n </div>\n <div class=\"line\">\n 19\n </div>\n <div class=\"line\">\n 20\n </div>\n <div class=\"line\">\n 21\n </div>\n <div class=\"line\">\n 22\n </div>\n <div class=\"line\">\n 23\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">/* svgSprite.js */</span>\n </div>\n <div class=\"line\"> \n </div>\n <div class=\"line\">\n <span class=\"keyword\">const</span> svgSprite = \n <span class=\"function\"><span class=\"params\">()</span> =&gt;</span> {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">return</span> (\n </div>\n <div class=\"line\"> \n <span class=\"xml\"><span class=\"tag\">&lt;<span class=\"name\">svg</span></span></span>\n </div>\n <div class=\"line\"> \n <span class=\"attr\">version</span>=\n <span class=\"string\">\'1.1\'</span>\n </div>\n <div class=\"line\"> \n <span class=\"attr\">xmlns</span>=\n <span class=\"string\">\'http://www.w3.org/2000/svg\'</span>\n </div>\n <div class=\"line\"> \n <span class=\"attr\">xmlnsXlink</span>=\n <span class=\"string\">\'http://www.w3.org/1999/xlink\'</span>\n </div>\n <div class=\"line\"> \n <span class=\"attr\">style</span>=\n <span class=\"string\">\'display: none;\'</span>&gt;\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">defs</span>&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">symbol</span> <span class=\"attr\">id</span>=<span class=\"string\">\'icon\'</span> <span class=\"attr\">viewBox</span>=<span class=\"string\">\'0 0 45 27\'</span>&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">path</span> <span class=\"attr\">d</span>=<span class=\"string\">\'M942,301.022h16.012v1.97H942v-1.97Zm39.978,14.766c-0.041,2.967-4.619-.024-9.957-2.944-0.884-.484-2.035-1.114-3.024-1.713V313a3,3,0,0,1-3,3H940a3,3,0,0,1-3-3V299a3,3,0,0,1,3-3h17.014V296.2a1.45,1.45,0,0,1,0,2.757V299h-0.242a0.681,0.681,0,0,1-.517,0H940v14h26V299.051h0.02v-0.3l-11.433-5.717a1.01,1.01,0,0,1-.292-0.2l-0.594-.411s-1.365-1.851,1.193-2.218l0.188-.194,13.25,6.8-0.11.192a3.126,3.126,0,0,1,.384.547,1.458,1.458,0,0,1,.416,1.011,1.438,1.438,0,0,1-.044.215c0.006,0.075.022,0.148,0.022,0.226v2.7c1.193-.825,2.718-2,4.016-2.741,5.666-3.812,8.954-5.819,8.965-2.672S982.031,312.2,981.981,315.788ZM979.156,299h-1l-9,6v3l9,5h1V299Z\'</span></span>\n </div>\n <div class=\"line\"> \n <span class=\"attr\">transform</span>=\n <span class=\"string\">\'translate(-937 -290)\'</span>\n </div>\n <div class=\"line\"> \n <span class=\"attr\">fill</span>=\n <span class=\"string\">\'#d4c6af\'</span>\n </div>\n <div class=\"line\"> \n <span class=\"attr\">fillRule</span>=\n <span class=\"string\">\'evenodd\'</span> /&gt;\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;/<span class=\"name\">symbol</span>&gt;</span>\n </div>\n <div class=\"line\">\n {/* 更多的\n <span class=\"tag\">&lt;<span class=\"name\">symbol</span> /&gt;</span> */}\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;/<span class=\"name\">defs</span>&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;/<span class=\"name\">svg</span>&gt;</span>\n </div>\n <div class=\"line\">\n )\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"> \n </div>\n <div class=\"line\">\n export default svgSprite\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>接下来,我们可以在页面中 <strong>动态引入</strong>上面的svgSprite组件就可以了:</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">/* app.js */</span>\n </div>\n <div class=\"line\"> \n </div>\n <div class=\"line\">\n <span class=\"keyword\">import</span> Loadable \n <span class=\"keyword\">from</span> \n <span class=\"string\">\'loadable\'</span>\n </div>\n <div class=\"line\"> \n </div>\n <div class=\"line\">\n <span class=\"keyword\">const</span> LoadableSvgSprite = Loadable({\n </div>\n <div class=\"line\"> \n <span class=\"attr\">loader</span>: \n <span class=\"function\"><span class=\"params\">()</span> =&gt;</span> \n <span class=\"keyword\">import</span>(\n <span class=\"string\">\'./svgSprite\'</span>),\n </div>\n <div class=\"line\"> \n <span class=\"attr\">delay</span>: \n <span class=\"number\">300</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">render</span>: \n <span class=\"function\">(<span class=\"params\">loaded, props</span>) =&gt;</span> {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">return</span> \n <span class=\"xml\"><span class=\"tag\">&lt;<span class=\"name\">loaded.default</span> {<span class=\"attr\">...props</span>}/&gt;</span>;</span>\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n });\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>在页面中挂载<code>&lt;LoadableSvgSprite /&gt;</code>后,我们即可使用形如<code>&lt;svg&gt;&lt;use xlink:href=\'#icon\' /&gt;&lt;/svg&gt;</code>的代码去引用相应的图标了。</p>\n<h3 id=\"数据大屏\" class=\"post-heading\"><a href=\"#数据大屏\" class=\"headerlink\" title=\"数据大屏\"></a>数据大屏<a class=\"post-anchor\" href=\"#数据大屏\" aria-hidden=\"true\"></a></h3>\n<p>除了用户,我们同样也关注运营人员的体验。如果可以将运营数据都以直观的图表展示,这对于运营同学、产品同学都是十分幸福的事。这次的首页改版,我们与数据方合作,为首页配套开发了数据大屏项目SEE,用于运营数据的实时滚动展示。</p>\n<p>SEE基于<code>Nerv</code>+<code>Redux</code>开发,使用<code>ECharts</code>进行数据的可视化展示。除了线上数据,SEE还有专门针对开发人员需求的性能版大屏,实时展示开发人员关心的页面onload时间,接口成功率,js报错数等指标。我们也希望未来SEE可以在更多的业务中用起来。</p>\n<h2 id=\"页面可用性保障和监控\" class=\"post-heading\"><a href=\"#页面可用性保障和监控\" class=\"headerlink\" title=\"页面可用性保障和监控\"></a>页面可用性保障和监控<a class=\"post-anchor\" href=\"#页面可用性保障和监控\" aria-hidden=\"true\"></a></h2>\n<p>我们做了许多优化工作来提升页面在性能、体验上的优良表现。但如果页面出现了JS逻辑错误,或者展示有问题,前面的优化工作就都前功尽弃了。所以在保证项目进度的基础上,我们又做了一系列的工作来保证首页的安全与稳定。</p>\n<h3 id=\"统一上线\" class=\"post-heading\"><a href=\"#统一上线\" class=\"headerlink\" title=\"统一上线\"></a>统一上线<a class=\"post-anchor\" href=\"#统一上线\" aria-hidden=\"true\"></a></h3>\n<p>同一份代码,经过不同版本的开发工具进行编译、压缩,生成的文件可能会天差地别,这种情况在多人协作中是相当致命的。比如:开发人员A的代码使用了新版本开发工具的API,而无辜的开发人员B对此毫不知情,使用了老版本的开发工具进行编译和发布…说多了都是泪,又是一场人间悲剧。</p>\n<p>为了消除差异,我们希望不同开发人员的开发环境保持严格统一,但这其实是难以保证的:除了开发工具版本不同,有时候windows下,macOS下甚至是Linux下的表现也是不一样的。</p>\n<p>为了解决这个问题,我们将编译的工作挪到了服务器端。开发人员在本地进行开发、自测,联调通过后提交到代码仓库中,确认上线后,上线平台拉取项目的代码,<strong>使用服务器端的工具链进行编译、压缩、发布</strong>等工作。</p>\n<p>此外,上线平台还提供上线代码diff功能,可以将待上线的文件与线上的版本进行diff,待开发人员确认完才能继续上线操作。</p>\n<p>接入上线平台后,开发人员再也不必担心开发环境的差异影响了编译结果,也不会误操作将其他同事开发中的分支带上线。就算是出现了线上bug,开发人员也可以轻松地通过上线平台记录的git commitId进行 <strong>精确快速的回滚</strong>,有效保障了页面的可用性。</p>\n<h3 id=\"自动化测试\" class=\"post-heading\"><a href=\"#自动化测试\" class=\"headerlink\" title=\"自动化测试\"></a>自动化测试<a class=\"post-anchor\" href=\"#自动化测试\" aria-hidden=\"true\"></a></h3>\n<p>我们注意到,每次上线迭代,在经过编译工具的压缩、组合后,都有可能会对代码中其他部分的代码造成影响。如果在测试时只验证了当前迭代的功能点,疏漏了原先其他功能点的验证,就有可能引起一些意想不到的BUG。传统的DOM元素监控并无法满足我们的需求,因为有的bug出现的时机是在一连串特定的操作后。所以我们认为,我们造轮子的时候又到了。我们需要在Athena监控体系中增加一套针对页面中各个功能点的自动进行验证测试的系统。</p>\n<p>这个系统基于<code>selenium webdriver</code>搭建。在后台中,开发者 <strong>针对CSS选择器配置一系列的动作链<code>Actionchain</code></strong>,包括点击、hover、输入文字、拖拽等操作,再通过对指定CSS选择器的HTML属性、样式、值等因素指定一个预期结果。后台服务会定时打开页面,执行预设的操作,如果与预期结果有出入,就会触发告警。</p>\n<p>这种单服务器运行的e2e测试,容易碰到一些偶然网络波动的影响而导致乱告警。事实上,我们刚开始跑这套服务的时候,我们经常收到告警,但事实上页面展示并没有任何问题。针对这种偶发情况,我们在验证的过程中加入了失败重试的机制。只有在连续3次测试状态都为fail的情况,才会触发告警。经过优化后,监控的准确性有了质的提升。</p>\n<h3 id=\"素材监控\" class=\"post-heading\"><a href=\"#素材监控\" class=\"headerlink\" title=\"素材监控\"></a>素材监控<a class=\"post-anchor\" href=\"#素材监控\" aria-hidden=\"true\"></a></h3>\n<p>在生产环境中,除了程序BUG,数据运营的一些不规范操作也有可能影响到用户的体验。举例来说,如果页面中的图片体积过于庞大,会导致页面的加载时间变长,用户等待的时间会更久;如果页面中图片尺寸不合规,会导致 图片展示不正常,出现拉伸/压缩等现象,页面就会给人很山寨的感觉了。</p>\n<p>针对这些素材异常情况,我们部署了针对性的监控服务。开发者 <strong>针对特定的CSS选择器配置图片的标准体积以及尺寸</strong>。监控服务定时开启headless浏览器抓取页面中的图片并判断是否符合规则,不符合就触发告警。通过这样的手段,我们可以第一时间知道页面上出现的超限素材,通知运营的同学修改。</p>\n<h3 id=\"实时告警\" class=\"post-heading\"><a href=\"#实时告警\" class=\"headerlink\" title=\"实时告警\"></a>实时告警<a class=\"post-anchor\" href=\"#实时告警\" aria-hidden=\"true\"></a></h3>\n<p>作为Athena前端体系的一环,我们接入了Athena系统用于收集首页各种性能以及用户环境相关的数据。在这套系统中,我们可以获取到用户的 <strong>屏幕分辨率占比</strong>,<strong>浏览器占比</strong>,同时还有 <strong>页面加载时间</strong>、<strong>接口成功率</strong>等性能数据。</p>\n<p>基于这些数据,我们在发现问题时可以进行有针对性的优化,比如调整特定接口的等待时间,或者是调整特定请求的重试策略。但是,并没有谁会一整天对这些数据的仪表盘盯着看;等我们发现问题,可能已经是上线后第二天,在工位上吃着早餐喝着牛奶的时候了。</p>\n<p>解决信息滞后的问题,加强消息触达是关键。我们强化了Athena监控平台的功能:除了平台上仪表盘直观的数据展示,还支持配置告警规则。在绑定告警接收人的微信号后,平台就可以通过部门公众号实时推送告警信息,真正做到24小时监控,360度无盲点触达。</p>\n<h2 id=\"更长远的探索\" class=\"post-heading\"><a href=\"#更长远的探索\" class=\"headerlink\" title=\"更长远的探索\"></a>更长远的探索<a class=\"post-anchor\" href=\"#更长远的探索\" aria-hidden=\"true\"></a></h2>\n<p>“如何做得更好?”这是一个永不过时的问题。以前我们觉得在页面使用CMD的模块加载体系非常酷,所以后来会在项目中使用SeaJS;去年我们渴求一次架构升级,所以我们今年用上了Nerv。今年我们又会渴望什么呢?</p>\n<h3 id=\"前后端同构\" class=\"post-heading\"><a href=\"#前后端同构\" class=\"headerlink\" title=\"前后端同构\"></a>前后端同构<a class=\"post-anchor\" href=\"#前后端同构\" aria-hidden=\"true\"></a></h3>\n<p>作为提升性能的一个捷径,代码同构、服务器端渲染是目前看来的终极解决方案。我们计划搭建中间层,同样使用<code>Nerv</code>来渲染,从而减少首屏的代码逻辑,这将对页面的加载速度有大幅度的优化。</p>\n<h3 id=\"引入强类型校验\" class=\"post-heading\"><a href=\"#引入强类型校验\" class=\"headerlink\" title=\"引入强类型校验\"></a>引入强类型校验<a class=\"post-anchor\" href=\"#引入强类型校验\" aria-hidden=\"true\"></a></h3>\n<p>除了更快,我们也希望能够更稳。作为弱类型语言,<code>JavaScript</code>有着强大的灵活性,数据类型的相互转换十分便利;但也由于各种不严谨的类型转换,代码中存在着大量不可预测的分支走向,容易出现<code>undefined</code>的报错,调用不存在的API等等。</p>\n<p>强类型语言<code>TypeScript</code>从13年诞生到现在,已经十分成熟了。在类型推断、静态验证上,<code>TypeScript</code>明显会更胜一筹;而在减少了多余的类型转换之后,<code>TypeScript</code>的性能表现也比常规<code>JavaScript</code>更强。我们希望未来可以将<code>TypeScript</code>在项目中用起来,这对于提升页面可靠性和性能,是很有意义的。</p>\n<h2 id=\"总结\" class=\"post-heading\"><a href=\"#总结\" class=\"headerlink\" title=\"总结\"></a>总结<a class=\"post-anchor\" href=\"#总结\" aria-hidden=\"true\"></a></h2>\n<p>这篇文章从 <strong>整体开发架构与模式</strong>,<strong>性能、体验优化的探索</strong>,<strong>页面可用性的保障</strong>等方面对京东首页的开发过程做了 <strong>简单</strong>的介绍。之所以说简单,是因为短短的篇幅完全无法说完我们在开发期间的故事和感悟:许多问题的解决并不像上面讲的那样水到渠成;除此之外更是有一大堆深夜加班<del>撸串</del>的故事没有地方讲。</p>\n<p>最后献上个照片,这是项目上线成功之后在公司拍的<del>通宵证明</del>。虽然现在会觉得这拍得真……丑,但是项目成功上线的喜悦之情,我相信屏幕前的你也一样可以感受到。</p>\n<p><img src=\"https://misc.aotu.io/Littly/18-04-24_jdindex/happy.png\" alt=\"早晨的公司\"></p>\n<div class=\"post-tags\" style=\"display:none\">\n <a href=\"/tags/React/\">React</a> \n <a href=\"/tags/JD/\">JD</a> \n <a href=\"/tags/Summary/\">Summary</a> \n <a href=\"/tags/Nerv/\">Nerv</a>\n</div>\n<div class=\"post-categories\" style=\"display:none\">\n <a href=\"/cates/项目总结/\">项目总结</a>\n</div>\n<div class=\"post-announce\">\n 感谢您的阅读,本文由 \n <a href=\"//aotu.io\">凹凸实验室</a> 版权所有。如若转载,请注明出处:凹凸实验室(\n <a href=\"https://aotu.io/notes/2018/04/24/jdindex_2017/\">https://aotu.io/notes/2018/04/24/jdindex_2017/</a>)\n</div>\n<div class=\"post-revision\">\n <i class=\"fa fa-clock-o\"></i> \n <time class=\"post-updated\" datetime=\"2018-11-07T01:01:15.657Z\">上次更新:2018-11-07 09:01:15</time>\n</div>');
INSERT INTO `rumo_blog` VALUES ('29', '图像处理 - ImageMagick 简单介绍与案例', '1', '图像处理 - ImageMagick 简单介绍与案例', '1', '2018-11-11 21:02:33', '2018-11-11 21:02:33', '1', '0', 'https://img11.360buyimg.com/ling/jfs/t21382/242/710567581/125401/630d6b14/5b161ac7N5de78bc7.jpg', '', '0', '0', '<a id=\"more\"></a>\n<p>在客户端我们可以用 <code>PhotoShop</code> 等 <code>GUI</code> 工具处理静态图片或者动态 <code>GIF</code> 图片,不过在服务器端对于 <code>WEB</code> 应用程序要处理图片格式转换,缩放裁剪,翻转扭曲,PDF解析等操作, <code>GUI</code> 软件就很难下手了,所以此处需要召唤命令行工具来帮我们完成这些事。</p>\n<p><strong>ImageMagick:</strong> 是一款创建、编辑、合成,转换图像的命令行工具。支持格式超过 <code>200</code> 种,包括常见的 <code>PNG, JPEG, GIF, HEIC, TIFF, DPX, EXR, WebP, Postscript, PDF, SVG</code> 等。功能包括调整,翻转,镜像(mirror),旋转,扭曲,修剪和变换图像,调整图像颜色,应用各种特殊效果,或绘制文本,线条,多边形,椭圆和贝塞尔曲线等。</p>\n<p>官网:<a href=\"https://www.imagemagick.org\" target=\"_blank\" rel=\"external\">https://www.imagemagick.org</a> 下面放个小标识。</p>\n<p><img src=\"https://img13.360buyimg.com/ling/jfs/t20092/208/1167159371/53422/6b06756f/5b161da3Nec0d6b0d.png\" alt=\"小标识.png\"></p>\n<h3 id=\"安装-ImageMagick\" class=\"post-heading\"><a href=\"#安装-ImageMagick\" class=\"headerlink\" title=\"安装 ImageMagick\"></a>安装 ImageMagick<a class=\"post-anchor\" href=\"#安装-ImageMagick\" aria-hidden=\"true\"></a></h3>\n<blockquote>\n <p>支持 <code>Linux, Windows, Mac OS X, iOS, Android OS</code> 等平台<br><a href=\"https://www.imagemagick.org/script/download.php\" target=\"_blank\" rel=\"external\">https://www.imagemagick.org/script/download.php</a></p>\n</blockquote>\n<p>因为我是 MAC 机器,演示一下 <code>brew</code> 的安装方式咯<br></p>\n<figure class=\"highlight mipsasm\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">brew </span>\n <span class=\"keyword\">install </span>imagemagick\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<h3 id=\"基本命令与格式\" class=\"post-heading\"><a href=\"#基本命令与格式\" class=\"headerlink\" title=\"基本命令与格式\"></a>基本命令与格式<a class=\"post-anchor\" href=\"#基本命令与格式\" aria-hidden=\"true\"></a></h3>\n<h4 id=\"1、基本命令\" class=\"post-heading\"><a href=\"#1、基本命令\" class=\"headerlink\" title=\"1、基本命令\"></a>1、基本命令<a class=\"post-anchor\" href=\"#1、基本命令\" aria-hidden=\"true\"></a></h4>\n<blockquote>\n <p><code>ImageMagick</code> 包括一组命令行工具来操作图片,安装好 <code>ImageMagick</code> 后,终端就可以使用如下命令了。</p>\n</blockquote>\n<p><code>magick:</code> 创建、编辑图像,转换图像格式,以及调整图像大小、模糊、裁切、除去杂点、抖动 ( dither )、绘图、翻转、合并、重新采样等。<br><code>convert:</code> 等同于 <code>magick</code> 命令。<br><code>identify:</code> 输出一个或多个图像文件的格式和特征信息,如分辨率、大小、尺寸、色彩空间等。<br><code>mogrify:</code> 与 <code>magick</code> 功能一样,不过不需要指定输出文件,自动覆盖原始图像文件。<br><code>composite:</code> 将一个图片或多个图片组合成新图片。<br><code>montage:</code> 组合多个独立的图像来创建合成图像。每个图像都可以用边框,透明度等特性进行装饰。</p>\n<p><code>compare:</code> 从数学和视觉角度比较源图像与重建图像之间的差异。<br><code>display:</code> 在任何 X server 上显示一个图像或图像序列。<br><code>animate:</code> 在任何 X server 上显示图像序列。<br><code>import:</code> 保存 X server 上的任何可见窗口并把它作为图像文件输出。可以捕捉单个窗口,整个屏幕或屏幕的任意矩形部分。<br><code>conjure:</code> 解释并执行 MSL ( Magick Scripting Language ) 写的脚本。<br><code>stream:</code> 一个轻量级工具,用于将图像或部分图像的一个或多个像素组件流式传输到存储设备。在处理大图像或原始像素组件时很有用。</p>\n<h4 id=\"2、命令格式\" class=\"post-heading\"><a href=\"#2、命令格式\" class=\"headerlink\" title=\"2、命令格式\"></a>2、命令格式<a class=\"post-anchor\" href=\"#2、命令格式\" aria-hidden=\"true\"></a></h4>\n<p>基本命令的使用,遵循 <code>Unix</code> 风格的标准格式:<br></p>\n<figure class=\"highlight livecodeserver\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">command</span> [\n <span class=\"title\">options</span>] \n <span class=\"title\">input_image</span> \n <span class=\"title\">output_image</span>\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<p>比如我们将一张宽高 <code>300x300</code> 的图片 <code>goods.png</code> 转换成 <code>200x200</code> 的<code>goods.jpg</code>,可以这样用<br></p>\n<figure class=\"highlight css\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"selector-tag\">convert</span> \n <span class=\"selector-tag\">-resize</span> 200\n <span class=\"selector-tag\">x200</span> \n <span class=\"selector-tag\">goods</span>\n <span class=\"selector-class\">.png</span> \n <span class=\"selector-tag\">goods</span>\n <span class=\"selector-class\">.jpg</span>\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<blockquote>\n <p><code>-resize</code> 定义图片尺寸,<code>ImageMagick</code> 所有的选项参数都在这个【<a href=\"https://www.imagemagick.org/script/command-line-options.php\" target=\"_blank\" rel=\"external\">命令行选项手册</a>】。</p>\n</blockquote>\n<p>但是随着功能的复杂,命令缓慢扩大成了这样的格式:<br></p>\n<figure class=\"highlight vim\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">command</span> [\n <span class=\"keyword\">options</span>] image1 [\n <span class=\"keyword\">options</span>] image2 [\n <span class=\"keyword\">options</span>] output_image\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<p>于是上面的命令也可以写成这样<br></p>\n<figure class=\"highlight css\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"selector-tag\">convert</span> \n <span class=\"selector-tag\">goods</span>\n <span class=\"selector-class\">.png</span> \n <span class=\"selector-tag\">-resize</span> 200\n <span class=\"selector-tag\">x200</span> \n <span class=\"selector-tag\">goods</span>\n <span class=\"selector-class\">.jpg</span>\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<p><code>笔记:</code>个人建议,如果转换的是一张图片,那么用第一种格式,因为像 <code>-density</code> 等一些选项必须放在 <code>command</code> 与 <code>input_image</code> 之间,所以为了省记都不写错,都写在 <code>command</code> 与 <code>input_image</code> 之间岂不很好。<br>但是如果是多张图片转换,就需要按第二种格式,正确输出命令选项了。</p>\n<p><code>提示:</code>如果上面的工具命令在计算机上不可以使用,则可以把它们当作 <code>magick</code> 命令的子命令使用,例如<br></p>\n<figure class=\"highlight css\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"selector-tag\">magick</span> \n <span class=\"selector-tag\">identify</span> \n <span class=\"selector-tag\">goods</span>\n <span class=\"selector-class\">.png</span>\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<h4 id=\"3、指定文件格式\" class=\"post-heading\"><a href=\"#3、指定文件格式\" class=\"headerlink\" title=\"3、指定文件格式\"></a>3、指定文件格式<a class=\"post-anchor\" href=\"#3、指定文件格式\" aria-hidden=\"true\"></a></h4>\n<p>默认情况下 <code>ImageMagick</code> 会读取图像中唯一标识格式的签名来确定文件格式,如果没有,则根据文件的扩展名来确定格式,如 <code>image.jpg</code> 被认为 <code>jpeg</code> 格式文件,如果都获取不到,则需要手动指定文件的格式。命令格式为 <code>format:input_or_output_image</code>。</p>\n<p>输入文件一般情况应该不需要手动指定文件格式,输出文件的时候,<code>png</code> 格式分 <code>png8</code>、<code>png24</code> 等格式,如果 <code>png8</code> 格式的文件能够满足需求,指定合理的格式可以缩小文件的大小,示例如下。<br></p>\n<figure class=\"highlight stylus\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n convert goods\n <span class=\"selector-class\">.png</span> png8:goods_8\n <span class=\"selector-class\">.png</span>\n </div>\n <div class=\"line\">\n convert goods\n <span class=\"selector-class\">.png</span> png24:goods_24.png\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<h3 id=\"实际案例\" class=\"post-heading\"><a href=\"#实际案例\" class=\"headerlink\" title=\"实际案例\"></a>实际案例<a class=\"post-anchor\" href=\"#实际案例\" aria-hidden=\"true\"></a></h3>\n<blockquote>\n <p>文中案例基于 <code>ImageMagick 7.0.7</code></p>\n</blockquote>\n<h4 id=\"1、生成缩略图\" class=\"post-heading\"><a href=\"#1、生成缩略图\" class=\"headerlink\" title=\"1、生成缩略图\"></a>1、生成缩略图<a class=\"post-anchor\" href=\"#1、生成缩略图\" aria-hidden=\"true\"></a></h4>\n<p>需求:将一张宽高为 <code>900x600</code> 的图片 <code>goods.jpg</code> 生成宽高为 <code>150x100</code> 的缩略图 <code>thumbnail.jpg</code></p>\n<figure class=\"highlight css\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"selector-tag\">convert</span> \n <span class=\"selector-tag\">-resize</span> 150\n <span class=\"selector-tag\">x100</span> \n <span class=\"selector-tag\">-quality</span> 70 \n <span class=\"selector-tag\">-strip</span> \n <span class=\"selector-tag\">goods</span>\n <span class=\"selector-class\">.jpg</span> \n <span class=\"selector-tag\">thumbnail</span>\n <span class=\"selector-class\">.jpg</span>\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>解释:</p>\n<ul>\n <li><code>-resize 150x100</code>:定义输出的缩略图尺寸为 <code>150x100</code>。</li>\n <li><code>-quality 70</code>:降低缩略图的质量为 <code>70</code>,取值范围 <code>1</code> ( 最低图像质量和最高压缩率 ) 到 <code>100</code> ( 最高图像质量和最低压缩率 ),默认值根据输出格式有 <code>75</code>、<code>92</code>、<code>100</code>,选项适用于 <code>JPEG / MIFF / PNG</code>。</li>\n <li><code>-strip</code>:让缩略图移除图片内嵌的所有配置文件,注释等信息,以减小文件大小。</li>\n</ul>\n<blockquote>\n <p><code>-resize</code> 延伸解读,如下。</p>\n</blockquote>\n<p>上面的例子中,输入的图片和输出的图片比例是一致的,所以不会有特殊情况出现,但是遇到比例不同的时候,上面的写法并不会得到 <code>150x100</code> 的图像,而是会根据图像的宽高比例,取最大值,得出来的结果可能是 <code>150</code> 宽和更小的高,或者 <code>100</code> 高和更小的宽;所以 <code>IamgeMagick</code> 提供了几种符号来定义缩放。<br></p>\n<figure class=\"highlight livecodeserver\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"built_in\">convert</span> -resize \n <span class=\"string\">\'150x100!\'</span> goods.jpg thumbnail.jpg\n </div>\n <div class=\"line\">\n <span class=\"built_in\">convert</span> -resize \n <span class=\"string\">\'150x100&gt;\'</span> goods.jpg thumbnail.jpg\n </div>\n <div class=\"line\">\n <span class=\"built_in\">convert</span> -resize \n <span class=\"string\">\'150x100&lt;\'</span> goods.jpg thumbnail.jpg\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<p><code>!</code>:不管图片宽高如何,都缩放成 <code>150x100</code> 这样的尺寸。<br><code>&gt;</code>:只有宽高均大于 <code>150x100</code> 的图片才缩放成该尺寸 ( 按比例取最大值 ),小于的图片不做处理。<br><code>&lt;</code>:与 <code>&gt;</code> 功能相反。</p>\n<p><code>提示:</code>因为有些字符是 <code>Linux shell</code> 或其他系统的特殊字符,所以需要用引号包裹起来或者用反斜线 <code>\\</code> 转义,另外,不同平台可能引号都是有差异的。</p>\n<h4 id=\"2、添加水印\" class=\"post-heading\"><a href=\"#2、添加水印\" class=\"headerlink\" title=\"2、添加水印\"></a>2、添加水印<a class=\"post-anchor\" href=\"#2、添加水印\" aria-hidden=\"true\"></a></h4>\n<p>需求 ① :给图片居中加上透明文本水印。</p>\n<figure class=\"highlight lsl\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n convert -draw \'text \n <span class=\"number\">0</span>,\n <span class=\"number\">0</span> \n <span class=\"string\">\"JD.COM\"</span>\' -fill \'rgba(\n <span class=\"number\">221</span>, \n <span class=\"number\">34</span>, \n <span class=\"number\">17</span>, \n <span class=\"number\">0.25</span>)\' -pointsize \n <span class=\"number\">36</span> \\\n </div>\n <div class=\"line\">\n -font \'cochin.ttc\' -gravity center joy.jpg watermark.jpg\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>解释:</p>\n<ul>\n <li><code>-draw</code>:绘图选项,<code>text</code> 声明绘制文本, <code>0,0</code> 声明文本距离图片左上角的偏移值, <code>JD.COM</code> 声明绘制的文本,最好用引号包裹起来,避免输入特殊字符引起错误。绘制文本的格式为 <code>text x,y string</code>,当然还可以绘制其他类型,诸如圆 ( circle )、折线 ( polyline )。</li>\n <li><code>-fill</code>:对文本填充颜色,貌似 <code>ImageMagick</code> 命令中<code>前面的选项</code>是用来控制<code>后面的选项</code>的,所以应该把这样的修饰选项放到 <code>-draw</code> 前面比较好,<code>很重要</code>,后面的案例就是这样的。</li>\n <li><code>-pointsize</code>:指定文本的字体大小。</li>\n <li><code>-font</code>:指定字体。</li>\n <li><code>-gravity</code>:设置文本在图片里的排列方式 ( 类似 CSS 里的 align-items + justify-content ),<code>center</code> 表示水平垂直都居中,其他值还可以是:<code>NorthWest, North, NorthEast, West, East, SouthWest, South, SouthEast</code>,不记大小写。</li>\n <li><code>\\</code>:反斜线也是类 <code>Unix</code> 系统的续行字符,当一个命令很长时,我们可以把它写成多行,以便视觉上的美观和直观。</li>\n</ul>\n<p>需求 ② :给图片加上倾斜平铺透明文本水印。</p>\n<figure class=\"highlight haml\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n convert -size 100x100 xc:none \\\n </div>\n <div class=\"line\">\n -\n <span class=\"ruby\">fill <span class=\"string\">\'#d90f02\'</span> -pointsize <span class=\"number\">18</span> -font <span class=\"string\">\'cochin.ttc\'</span> \\</span>\n </div>\n <div class=\"line\">\n -\n <span class=\"ruby\">gravity center -draw <span class=\"string\">\'rotate -45 text 0,0 \"JD.COM\"\'</span> \\</span>\n </div>\n <div class=\"line\">\n -\n <span class=\"ruby\">resize <span class=\"number\">60</span>% <span class=\"symbol\">miff:</span>- <span class=\"params\">| composite -tile -dissolve 25 - joy.jpg watermark.jpg</span></span>\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>解释:文本平铺水印其实是将文本画成一张 <code>png</code> 图片,然后用这张透明图片在目标图片上进行平铺。</p>\n<ul>\n <li><code>-size</code>:设置画布的大小。</li>\n <li><code>xc:</code>:全称 <code>X Constant Image</code>,是 <code>canvas:</code> 的别名,定义一张画布,用来绘图,常用格式为 <code>xc:color</code>,<code>none</code> 或者 <code>transparent</code> 设置画布为透明底,默认为白色。</li>\n <li><code>-resize</code>:该选项还可以指定百分比,意为缩放至原图像的百分之几。貌似 <code>-pointsize</code> 小于 <code>14</code> 后,<code>-draw</code> 里的 <code>rotate</code> 会不生效,所以用 <code>-resize</code> 来把平铺图案变得更小。</li>\n <li><code>miff:-</code>:\n <ul>\n <li><code>miff:</code> 声明输出 ImageMagick ( IM ) 自己的图像文件格式:<a href=\"https://www.imagemagick.org/script/miff.php\" target=\"_blank\" rel=\"external\">MIFF</a>,主要用途是以复杂的方式处理图像时当做中间保存格式,适用于从一个 IM 命令向另一个 IM 命令传递图像元数据和其他关联属性。</li>\n <li><code>-</code> 在管道符前面意为将 IM 命令执行的结果作为标准输出,在管道符后面则表示从标准输入中读取这个数据,如在管道符后面的 <code>composite</code> 中使用 <code>-</code> 读取刚刚生成的透明图像。</li>\n </ul></li>\n <li><code>|</code>:<code>Linux shell</code> 管道符,用于将上一个命令的标准输出传递到下一个命令作为标准输入。这里将生成的水印图案传递给 <code>composite</code> 命令。</li>\n <li><code>-tile</code>:顾名思义,让图案平铺。</li>\n <li><code>-dissolve</code>:设置平铺图案的透明度。</li>\n</ul>\n<p>图释:</p>\n<p><img src=\"https://img20.360buyimg.com/ling/jfs/t21091/171/680652696/44636/6ceaa42c/5b161da0Nf2158787.jpg\" alt=\"图片描述\"></p>\n<h4 id=\"3、绘制验证码\" class=\"post-heading\"><a href=\"#3、绘制验证码\" class=\"headerlink\" title=\"3、绘制验证码\"></a>3、绘制验证码<a class=\"post-anchor\" href=\"#3、绘制验证码\" aria-hidden=\"true\"></a></h4>\n<p>大概逻辑如下:</p>\n<ol>\n <li>随机生成 <code>4</code> 个英文字母或数字。</li>\n <li>创建一个宽高 <code>100x40</code> 的画布。</li>\n <li>设置字体大小为 <code>16</code>,每个字符的宽高也就是 <code>16</code> 左右了,依次计算出每个字符的 <code>x, y</code> 坐标,再增加一丁点旋转。</li>\n <li>随机创建一条透明曲线,加上噪点,增加图片被破解的难度(在保证肉眼能看得清楚的用户体验下)。</li>\n <li>如果需要安全性更高的验证码,请了解验证码破解原理并做合理调整。</li>\n</ol>\n<p>如果加上随机计算,可能代码会比较多,所以这里写成固定值,方便理解。</p>\n<figure class=\"highlight processing\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n convert \n <span class=\"string\">\'xc:[100x40!]\'</span> -pointsize \n <span class=\"number\">20</span> -font \n <span class=\"string\">\'cochin.ttc\'</span> \\\n </div>\n <div class=\"line\">\n -gravity NorthWest -strokewidth \n <span class=\"number\">1</span> \\\n </div>\n <div class=\"line\">\n -\n <span class=\"built_in\">fill</span> \n <span class=\"string\">\'#b72b36\'</span> -\n <span class=\"built_in\">stroke</span> \n <span class=\"string\">\'#b72b36\'</span> -\n <span class=\"title\">draw</span> \n <span class=\"string\">\'translate 13,19 rotate 10 text -5,-8 \"5\"\'</span> \\\n </div>\n <div class=\"line\">\n -\n <span class=\"built_in\">fill</span> \n <span class=\"string\">\'#821d70\'</span> -\n <span class=\"built_in\">stroke</span> \n <span class=\"string\">\'#821d70\'</span> -\n <span class=\"title\">draw</span> \n <span class=\"string\">\'translate 36,13 rotate -8 text -8,-8 \"C\"\'</span> \\\n </div>\n <div class=\"line\">\n -\n <span class=\"built_in\">fill</span> \n <span class=\"string\">\'#c7960a\'</span> -\n <span class=\"built_in\">stroke</span> \n <span class=\"string\">\'#c7960a\'</span> -\n <span class=\"title\">draw</span> \n <span class=\"string\">\'translate 60,23 rotate 5 text -5,-8 \"2\"\'</span> \\\n </div>\n <div class=\"line\">\n -\n <span class=\"built_in\">fill</span> \n <span class=\"string\">\'#03610a\'</span> -\n <span class=\"built_in\">stroke</span> \n <span class=\"string\">\'#03610a\'</span> -\n <span class=\"title\">draw</span> \n <span class=\"string\">\'translate 85,25 rotate 13 text -8,-8 \"E\"\'</span> \\\n </div>\n <div class=\"line\">\n -strokewidth \n <span class=\"number\">2</span> -\n <span class=\"built_in\">stroke</span> \n <span class=\"string\">\'rgba(248, 100, 30, 0.5)\'</span> -\n <span class=\"built_in\">fill</span> \n <span class=\"string\">\'rgba(0, 0, 0, 0)\'</span> \\\n </div>\n <div class=\"line\">\n -\n <span class=\"title\">draw</span> \n <span class=\"string\">\'bezier -20,30 -16,10 20,2 50,20\'</span> \\\n </div>\n <div class=\"line\">\n -\n <span class=\"title\">draw</span> \n <span class=\"string\">\'bezier 50,20 78,42 138,36 140,16\'</span> \\\n </div>\n <div class=\"line\">\n +\n <span class=\"built_in\">noise</span> Impulse \\\n </div>\n <div class=\"line\">\n captcha.jpg\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>结果:</p>\n<p><img src=\"https://img10.360buyimg.com/ling/jfs/t20143/186/680399623/7216/bd166545/5b161aefN83150dd6.jpg\" alt=\"图片描述\"></p>\n<p>鉴于字体比较细,可以用 <code>strokewidth</code> 加边框来加粗,或者使用字体的粗体版本,这里使用了第一种方式。</p>\n<p>解释:</p>\n<ul>\n <li><code>xc:[100x40!]</code>:设置画布大小的一种简写方式,方括号里写入画布宽高,注意要加 <code>!</code>,否则会出乎意料哟。</li>\n <li><code>文本定位与旋转</code>\n <ol>\n <li>画布宽 <code>100px</code>,平均分成 <code>4</code> 分,每份 <code>25px</code>, 文字宽 <code>16px</code>, 得文字 <code>x</code> 的坐标左右摆动范围为 <code>+0px, +9px</code>,<code>y</code> 坐标同理,用于设置 <code>translate</code> 值。</li>\n <li>实际上字体本身并没有填充满整个 <code>16x16</code> 的区域,根据字体的不同,填满的区域可能各有不同,所以根据<code>cochin</code> 字体的特性,上面稍微将字体大小调整为 <code>20</code>,实际渲染出来的字母才是 <code>16x16</code> 左右大小,数字大概是 <code>10x16</code>,所以设置数字的 <code>x,y</code> 为 <code>-5,-8</code>,结合下面两个属性解释 <code>x,y</code> 的计算方式。</li>\n <li><code>translate</code>: 设置文本的横纵向偏移值。</li>\n <li><code>rotate</code>:设置文本旋转,单位 <code>degrees</code>。根据 <code>gravity</code> 的设置坐标系统有一丁点变化,所以请设置为 <code>西北(NorthWest)</code> ,表示以画布 <code>0,0</code> 坐标旋转,跟 <code>HTML 5 Canvas</code> 坐标系统一致。</li>\n <li>根据这样的坐标系统,如果要文字按自身的中心旋转,得配合 <code>translate</code> 和 <code>text</code> 的 <code>x,y</code> 一起使用,原理可参考<a href=\"https://aotu.io/notes/2017/05/25/canvas-img-rotate-and-flip/index.html\">这篇文章[图像旋转的实现]</a>,注意 <code>translate</code> 与 <code>rotate</code> 的顺序。</li>\n </ol></li>\n <li><code>strokewidth</code>:设置文本的边框宽度或线条宽度。</li>\n <li><code>stroke</code>:设置文本的边框颜色或线条颜色。</li>\n <li><code>-fill \'rgba(0, 0, 0, 0)\'</code>:上面设置了文本的填充颜色,会影响下面的贝塞尔曲线,所以这里指定一个透明的填充色以覆盖上面的设定,使曲线没有填充。</li>\n <li><code>bezier</code>:绘制贝塞尔曲线,一两句话我怕解释不清楚,所以请大家参考一下<a href=\"https://en.wikipedia.org/wiki/B%C3%A9zier_curve\" target=\"_blank\" rel=\"external\">维基百科的解释</a>或者<a href=\"http://www.html-js.com/article/1628\" target=\"_blank\" rel=\"external\">这篇中文文章的解释</a>,最后再参考一下 <a href=\"https://www.imagemagick.org/Usage/draw/#bezier\" target=\"_blank\" rel=\"external\">IM 官方示例的描述</a>。上面两条三次贝塞尔曲线的坐标分别表示 <code>起始点</code>,<code>起始点的控制点</code>,<code>结束点的控制点</code>,<code>结束点</code>。</li>\n <li><code>+noise</code>:增加噪点,可以使用 <code>convert -list noise</code> 查看当前系统支持哪些算法的噪点,大概有 <code>Gaussian, Impulse, Laplacian, Multiplicative, Poisson, Random, Uniform</code>。</li>\n</ul>\n<h4 id=\"4、克隆及拼合图像\" class=\"post-heading\"><a href=\"#4、克隆及拼合图像\" class=\"headerlink\" title=\"4、克隆及拼合图像\"></a>4、克隆及拼合图像<a class=\"post-anchor\" href=\"#4、克隆及拼合图像\" aria-hidden=\"true\"></a></h4>\n<blockquote>\n <p>这个案例主要了解几个基本操作的 <code>API</code>。</p>\n</blockquote>\n<figure class=\"highlight livescript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n convert \n <span class=\"string\">\\</span>\n </div>\n <div class=\"line\">\n <span class=\"string\">\\(</span> -crop \n <span class=\"number\">300x</span>300+\n <span class=\"number\">10</span>+\n <span class=\"number\">25</span> joy.jpg \n <span class=\"string\">\\)</span> \n <span class=\"string\">\\</span>\n </div>\n <div class=\"line\">\n <span class=\"string\">\\(</span> -resize \n <span class=\"number\">400x</span>400 -crop \n <span class=\"number\">300x</span>300+\n <span class=\"number\">50</span>+\n <span class=\"number\">0</span> logo: \n <span class=\"string\">\\)</span> -swap \n <span class=\"number\">0</span>,\n <span class=\"number\">1</span> +append \n <span class=\"string\">\\</span>\n </div>\n <div class=\"line\">\n <span class=\"string\">\\(</span> -clone \n <span class=\"number\">0</span> -flop -flip \n <span class=\"string\">\\)</span> -append \n <span class=\"string\">\\</span>\n </div>\n <div class=\"line\">\n -resize \n <span class=\"number\">200x</span>200 combined.jpg\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>结果如下:</p>\n<p><img src=\"https://img13.360buyimg.com/ling/jfs/t20998/239/710863618/17515/6d03d48b/5b161aedN50ba4b89.jpg\" alt=\"图片描述\"></p>\n<p>解释:</p>\n<ul>\n <li><code>圆括号 \\( ... \\)</code>:图像堆栈 ( <code>image stack</code> ),相当于创建了一个独立作用域处理图像,这个可以使图像之前的处理互不干扰。圆括号需用反斜杠转义,才能不被 <code>Shell</code> 当做特殊字符处理,并且<code>每个圆括号两边需要用空格隔开</code>。不必要的圆括号会使 <code>IM</code> 增加少许额外的工作,但是却让命令更清晰不容易出错。</li>\n <li><code>-crop</code>:裁剪出图像的一个或多个矩形区域,格式为 <code>{size}{+-}x{+-}y</code>,如果不指定偏移值 <code>x,y</code>,则会被解释为按指定宽高切割图像成多少份(多图像)。</li>\n <li><code>logo:</code>:<code>IM</code> 内置图像,这个就是上图中拿着魔法棒的主人公了,本身宽高 <code>640x480</code>,其他内置图像还有:<code>rose:</code>,<code>granite:</code>等,<a href=\"https://www.imagemagick.org/script/formats.php#builtin-images\" target=\"_blank\" rel=\"external\">看这里</a>。</li>\n <li><code>-swap</code>:\n <ol>\n <li>交换图像的位置,格式 <code>-swap index,index</code>。</li>\n <li><code>IM</code> 在图像处理操作时,实际上很可能是在处理一个图像列表,当新图像被读入或者创建时,<code>IM</code> 会将该新图像添加到当前图像列表的末尾。</li>\n <li>如上,本来我们的图像列表里有 <code>2</code> 张图,第一张是 <code>joy</code>,但是 <code>-swap 0,1</code> 的意思是交换第一张图与第二张图的位置,所以 <code>joy</code> 变成跑到后面了。</li>\n </ol></li>\n <li><code>+append</code>:水平连接当前图像列表的图像来创建单个较长的图像。</li>\n <li><code>-append</code>:垂直连接当前图像列表的图像来创建单个较长的图像。</li>\n <li><code>-clone</code>:克隆图像,格式为 <code>-clone {index_range_list}</code>。\n <ul>\n <li><code>-clone 0</code>:表示克隆图像列表里的第一张图像。</li>\n <li><code>-clone 1-2</code>:表示克隆图像列表里的第二张到第三张图像。</li>\n <li><code>-clone 0--1</code>:<code>0</code> 表示第一张图像,<code>-1</code> 表示最后一张图像,所以整句命令则表示克隆整个图像列表。</li>\n <li><code>-clone 2,0,1</code>:表示克隆第三张,第一张,第二张图像,顺序根据指定的索引决定,用逗号分隔。</li>\n </ul></li>\n <li><code>-flop</code>:将图像水平翻转。</li>\n <li><code>-flip</code>:将图像垂直翻转。</li>\n</ul>\n<p><code>笔记:</code></p>\n<ol>\n <li>选项之间的顺序很重要。</li>\n <li>与 <code>-clone</code> 雷同的选项还有诸如:<code>-delete, -insert, -reverse, -duplicate</code>,用于操作图像列表,功能与单词意思相同。</li>\n</ol>\n<h4 id=\"5、GIF-与图片互转\" class=\"post-heading\"><a href=\"#5、GIF-与图片互转\" class=\"headerlink\" title=\"5、GIF 与图片互转\"></a>5、GIF 与图片互转<a class=\"post-anchor\" href=\"#5、GIF-与图片互转\" aria-hidden=\"true\"></a></h4>\n<h4 id=\"5-1、GIF-转图片\" class=\"post-heading\"><a href=\"#5-1、GIF-转图片\" class=\"headerlink\" title=\"5.1、GIF 转图片\"></a>5.1、GIF 转图片<a class=\"post-anchor\" href=\"#5-1、GIF-转图片\" aria-hidden=\"true\"></a></h4>\n<figure class=\"highlight css\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"selector-tag\">convert</span> \n <span class=\"selector-tag\">-coalesce</span> \n <span class=\"selector-tag\">rain</span>\n <span class=\"selector-class\">.gif</span> \n <span class=\"selector-tag\">frame</span>\n <span class=\"selector-class\">.jpg</span>\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p><code>-coalesce</code>:根据图像 <code>-dispose</code> 元数据的设置覆盖图像序列中的每个图像,以重现动画序列中每个点的动画效果。下面用一张结果对比图来解释这句话。</p>\n<p>原始图 ( rain.gif ) :</p>\n<p><img src=\"https://sfault-image.b0.upaiyun.com/217/323/2173239304-5b13729a9441a_articlex\" alt=\"图片描述\"></p>\n<p>结果对比:</p>\n<p><img src=\"https://img11.360buyimg.com/ling/jfs/t21829/207/694662510/406338/15330984/5b161affN1867e1c9.jpg\" alt=\"图片描述\"></p>\n<h4 id=\"5-2、定义输出文件名\" class=\"post-heading\"><a href=\"#5-2、定义输出文件名\" class=\"headerlink\" title=\"5.2、定义输出文件名\"></a>5.2、定义输出文件名<a class=\"post-anchor\" href=\"#5-2、定义输出文件名\" aria-hidden=\"true\"></a></h4>\n<p>上面默认输出的文件名为:<code>frame-0.jpg, frame-1.jpg, frame-2.jpg ...</code>,<br>如果想使用下划线作为符号,输出为 <code>frame_0.jpg, frame_1.jpg, frame_2.jpg ...</code>,则可以如下设置。</p>\n<figure class=\"highlight css\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"selector-tag\">convert</span> \n <span class=\"selector-tag\">-coalesce</span> \n <span class=\"selector-tag\">rain</span>\n <span class=\"selector-class\">.gif</span> \n <span class=\"selector-tag\">frame_</span>%\n <span class=\"selector-tag\">d</span>\n <span class=\"selector-class\">.jpg</span>\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>或者</p>\n<figure class=\"highlight gams\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n convert -coalesce -\n <span class=\"keyword\">set</span> filename:n \n <span class=\"comment\">\'%p\'</span>\n <span class=\"comment\"> rain.gif</span> \n <span class=\"comment\">\'frame_%[filename:n].jpg\'</span>\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>解释:</p>\n<ol>\n <li>第一种方式 <code>%d</code> 是 <code>C</code> 语言 <code>printf()</code> 中表示输出一个整数,参考 <a href=\"https://www.imagemagick.org/script/command-line-options.php?#adjoin\" target=\"_blank\" rel=\"external\">-adjoin</a> 选项。</li>\n <li>第二种为常规方式。\n <ul>\n <li><code>-set</code>:设置图像属性,格式为 <code>-set key value</code></li>\n <li><code>filename:n \'%p\'</code>:以 <code>filename:</code> 开头的 <code>key</code> 用于设置输出文件名的相关信息,如这里使用 <code>filename:n</code>,在输出文件名时,则可以使用 <code>%[filename:n]</code> 拿到刚刚的设置,而设置的内容则是 <code>\'%p\'</code>。<code>\'%p\'</code> 表示图像在图像列表中的索引值,更多<a href=\"http://imagemagick.org/script/escape.php\" target=\"_blank\" rel=\"external\">百分比选项 ( Percent Escapes )</a> 参考。</li>\n </ul></li>\n</ol>\n<h4 id=\"5-3、解析特定帧\" class=\"post-heading\"><a href=\"#5-3、解析特定帧\" class=\"headerlink\" title=\"5.3、解析特定帧\"></a>5.3、解析特定帧<a class=\"post-anchor\" href=\"#5-3、解析特定帧\" aria-hidden=\"true\"></a></h4>\n<p>如果只想拿到 GIF 的第一帧,可以这样设置。</p>\n<figure class=\"highlight livecodeserver\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"built_in\">convert</span> -coalesce \n <span class=\"string\">\'rain.gif[0]\'</span> first_frame.jpg\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>拿到某些帧,如同 <code>-clone</code> 的写法。</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n convert -coalesce \n <span class=\"string\">\'rain.gif[0-2]\'</span> some_frames_%d.jpg\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h4 id=\"5-4、获取页数\" class=\"post-heading\"><a href=\"#5-4、获取页数\" class=\"headerlink\" title=\"5.4、获取页数\"></a>5.4、获取页数<a class=\"post-anchor\" href=\"#5-4、获取页数\" aria-hidden=\"true\"></a></h4>\n<p>通过 <code>identify</code> 命令我们可以简要得到文件的信息,如下。<br></p>\n<figure class=\"highlight css\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"selector-tag\">identify</span> \n <span class=\"selector-tag\">rain</span>\n <span class=\"selector-class\">.gif</span>\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<p><img src=\"https://img11.360buyimg.com/ling/jfs/t21955/234/742104977/71488/5b55a29f/5b1665d4Nfe4bd0aa.png\" alt=\"get_pages.png\"></p>\n<p>通过换行符分割,简单封装一个 <code>Node.js</code> 函数获取页数。</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">// parser.js</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">const</span> util = \n <span class=\"built_in\">require</span>(\n <span class=\"string\">\'util\'</span>)\n </div>\n <div class=\"line\">\n <span class=\"keyword\">const</span> exec = util.promisify(\n <span class=\"built_in\">require</span>(\n <span class=\"string\">\'child_process\'</span>).exec)\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n exports.numberOfPages = \n <span class=\"keyword\">async</span> (filePath) =&gt; {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">try</span> {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">const</span> { stdout } = \n <span class=\"keyword\">await</span> exec(\n <span class=\"string\">`identify \'<span class=\"subst\">${filePath}</span>\'`</span>)\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">return</span> stdout.trim().split(\n <span class=\"string\">\'\\n\'</span>).length\n </div>\n <div class=\"line\">\n } \n <span class=\"keyword\">catch</span> (err) {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">throw</span> \n <span class=\"keyword\">new</span> \n <span class=\"built_in\">Error</span>(err)\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">// main.js</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">const</span> { numberOfPages } = \n <span class=\"built_in\">require</span>(\n <span class=\"string\">\'./parser\'</span>)\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n ;(\n <span class=\"keyword\">async</span> \n <span class=\"function\"><span class=\"keyword\">function</span> <span class=\"title\">start</span> (<span class=\"params\"></span>) </span>{\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">const</span> pages = \n <span class=\"keyword\">await</span> numberOfPages(\n <span class=\"string\">\'rain.gif\'</span>)\n </div>\n <div class=\"line\"> \n <span class=\"built_in\">console</span>.log(\n <span class=\"string\">\'pages:\'</span>, pages)\n </div>\n <div class=\"line\">\n }())\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h4 id=\"5-5、图片转-GIF\" class=\"post-heading\"><a href=\"#5-5、图片转-GIF\" class=\"headerlink\" title=\"5.5、图片转 GIF\"></a>5.5、图片转 GIF<a class=\"post-anchor\" href=\"#5-5、图片转-GIF\" aria-hidden=\"true\"></a></h4>\n<figure class=\"highlight ada\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n convert -\n <span class=\"keyword\">loop</span> \n <span class=\"number\">0</span> \n <span class=\"symbol\">\'frame</span>-*.jpg\' rain_animation.gif\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>将所有与 <code>frame-*.jpg</code> 模式匹配的图像转换成一张 <code>GIF</code> 图像,如 <code>frame-0.jpg</code>,<code>frame-1.jpg</code>等。<br><code>-loop</code> 设置动画循环次数,<code>0</code> 表示无限循环。<br>设置每张图像的播放速度可以使用 <a href=\"https://www.imagemagick.org/Usage/anim_basics/#gif_anim\" target=\"_blank\" rel=\"external\">-delay</a> 选项。</p>\n<p><code>笔记:</code> 在 <code>IM</code> 读取系列文件时,<code>frame-10.jpg</code> 会排在 <code>frame-2.jpg</code> 前面,为获得图像正确的读取顺序,可以为文件名设置前导零 ( <code>leading zeros</code> )。如:<code>frame-000.jpg, frame-001.jpg, frame-002.jpg ... frame-010.jpg</code>。</p>\n<p>所以在生成图像时,我们可以使用 <code>%03d</code> 获得三位前导零。<br></p>\n<figure class=\"highlight css\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"selector-tag\">convert</span> \n <span class=\"selector-tag\">-coalesce</span> \n <span class=\"selector-tag\">rain</span>\n <span class=\"selector-class\">.gif</span> \n <span class=\"selector-tag\">frame-</span>%03\n <span class=\"selector-tag\">d</span>\n <span class=\"selector-class\">.jpg</span>\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<h4 id=\"6、PDF-与图片互转\" class=\"post-heading\"><a href=\"#6、PDF-与图片互转\" class=\"headerlink\" title=\"6、PDF 与图片互转\"></a>6、PDF 与图片互转<a class=\"post-anchor\" href=\"#6、PDF-与图片互转\" aria-hidden=\"true\"></a></h4>\n<p>PDF 与图片互转跟 GIF 很相似,稍微有些格式自身需要注意的区别。<br><code>IM</code> 本身是不具备解析 PDF 的功能的,需要依赖专门解析这种格式的外部程序,如官方指明的 <code>ghostscript</code> 解析程序。<br>首先安装 <code>gs</code>,还是演示 <code>Mac OS</code> 安装:<code>brew install ghostscript</code>。</p>\n<p>以 <a href=\"https://mozilla.github.io/pdf.js/web/viewer.html\" target=\"_blank\" rel=\"external\">这个PDF</a> 为例,把它转换成图片,有两种方式达到我们想要的结果:<br></p>\n<figure class=\"highlight gradle\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n ① convert -density \n <span class=\"number\">150</span> -\n <span class=\"keyword\">flatten</span> \n <span class=\"string\">\'download.pdf[0]\'</span> first_page.jpg\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<figure class=\"highlight maxima\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n ② \n <span class=\"built_in\">convert</span> -density \n <span class=\"number\">150</span> -\n <span class=\"built_in\">background</span> white -alpha \n <span class=\"built_in\">remove</span> download.pdf download.jpg\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>解释:</p>\n<ol>\n <li>当转换 PDF 成 JPG 格式图像时,某些情况得到的 JPG 图片会出现黑色背景(转换成 PNG 不会),所以可以使用 <code>-flatten</code> 选项让其保持白色背景,但加上这个选项,<code>多页 PDF 不会分成多个 JPG 图像</code>,第二种方式 <code>-background white -alpha remove</code> 则可以<code>一次命令转换多页 PDF 成多个图像</code>并保持白色背景。</li>\n <li>第二种方式 <code>IM</code> 内部应该是一页一页的转换,所以一个 <code>10</code> 页的 <code>PDF</code> 耗时会比较久,采用第一种方式让 <code>Node.js</code> 多进程同时转换该 <code>PDF</code> 可以提升速率。</li>\n <li><code>-density</code>:指定输出图像的分辨率 ( <code>DPI</code> ),在 <code>Mac OS</code> 上,默认的分辨率 ( <code>72</code> ) 输出的图像字迹不清,需要更高分辨率获得清晰的图像。</li>\n</ol>\n<h3 id=\"在-Node-js-中应用\" class=\"post-heading\"><a href=\"#在-Node-js-中应用\" class=\"headerlink\" title=\"在 Node.js 中应用\"></a>在 Node.js 中应用<a class=\"post-anchor\" href=\"#在-Node-js-中应用\" aria-hidden=\"true\"></a></h3>\n<blockquote>\n <p>直接通过 <code>child_process</code> 模块执行相应的命令即可,如下。</p>\n</blockquote>\n<p>只需要结果可以使用 <code>exec</code>,<br></p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">const</span> util = \n <span class=\"built_in\">require</span>(\n <span class=\"string\">\'util\'</span>)\n </div>\n <div class=\"line\">\n <span class=\"keyword\">const</span> exec = util.promisify(\n <span class=\"built_in\">require</span>(\n <span class=\"string\">\'child_process\'</span>).exec)\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n ;(\n <span class=\"keyword\">async</span> \n <span class=\"function\"><span class=\"keyword\">function</span> <span class=\"title\">start</span> (<span class=\"params\"></span>) </span>{\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">try</span> {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">await</span> exec(\n <span class=\"string\">`convert -resize \'150x100!\' -strip goods.jpg thumbnail.jpg`</span>)\n </div>\n <div class=\"line\"> \n <span class=\"built_in\">console</span>.log(\n <span class=\"string\">\'convert completed.\'</span>)\n </div>\n <div class=\"line\">\n } \n <span class=\"keyword\">catch</span> (err) {\n </div>\n <div class=\"line\"> \n <span class=\"built_in\">console</span>.log(\n <span class=\"string\">\'convert failed.\'</span>, err)\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }())\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<p>流式输入输出可以使用 <code>spawn</code>,<br></p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">const</span> cp = \n <span class=\"built_in\">require</span>(\n <span class=\"string\">\'child_process\'</span>)\n </div>\n <div class=\"line\">\n <span class=\"keyword\">const</span> fs = \n <span class=\"built_in\">require</span>(\n <span class=\"string\">\'fs\'</span>)\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"keyword\">const</span> args = [\n </div>\n <div class=\"line\"> \n <span class=\"string\">\'-\'</span>, \n <span class=\"comment\">// 使用标准输入</span>\n </div>\n <div class=\"line\"> \n <span class=\"string\">\'-resize\'</span>, \n <span class=\"string\">\'150x100!\'</span>,\n </div>\n <div class=\"line\"> \n <span class=\"string\">\'-strip\'</span>,\n </div>\n <div class=\"line\"> \n <span class=\"string\">\'jpg:-\'</span>, \n <span class=\"comment\">// 输出到标准输出</span>\n </div>\n <div class=\"line\">\n ]\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"keyword\">const</span> streamIn = fs.createReadStream(\n <span class=\"string\">\'/path/to/goods.jpg\'</span>)\n </div>\n <div class=\"line\">\n <span class=\"keyword\">const</span> proc = cp.spawn(\n <span class=\"string\">\'convert\'</span>, args)\n </div>\n <div class=\"line\">\n streamIn.pipe(proc.stdin)\n </div>\n <div class=\"line\">\n proc.stdout.pipe(HttpResponse)\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<div class=\"post-tags\" style=\"display:none\">\n <a href=\"/tags/图像处理/\">图像处理</a> \n <a href=\"/tags/ImageMagick/\">ImageMagick</a> \n <a href=\"/tags/linux/\">linux</a> \n <a href=\"/tags/node-js/\">node.js</a>\n</div>\n<div class=\"post-categories\" style=\"display:none\">\n <a href=\"/cates/Web开发/\">Web开发</a>\n</div>\n<div class=\"post-announce\">\n 感谢您的阅读,本文由 \n <a href=\"//aotu.io\">凹凸实验室</a> 版权所有。如若转载,请注明出处:凹凸实验室(\n <a href=\"https://aotu.io/notes/2018/06/06/ImageMagick_intro/\">https://aotu.io/notes/2018/06/06/ImageMagick_intro/</a>)\n</div>\n<div class=\"post-revision\">\n <i class=\"fa fa-clock-o\"></i> \n <time class=\"post-updated\" datetime=\"2018-11-07T01:01:15.657Z\">上次更新:2018-11-07 09:01:15</time>\n</div>');
INSERT INTO `rumo_blog` VALUES ('30', '多端统一开发框架 Taro 1.0 正式发布', '1', '多端统一开发框架 Taro 1.0 正式发布', '1', '2018-11-11 21:02:33', '2018-11-11 21:02:33', '1', '0', '//img13.360buyimg.com/uba/jfs/t1/932/12/6041/78434/5ba0ae19Ec77fe087/b8359160971f8b2c.jpg', '', '0', '0', '<a id=\"more\"></a>\n<p>在 <code>2018.6.7</code> 我们对外开源了 <a href=\"https://aotu.io/notes/2018/06/07/Taro/\"><code>多端统一开发框架——Taro</code></a>。</p>\n<p>Taro 是一个多端统一开发框架,它支持使用 React 的开发方式来编写可以同时在微信小程序、Web 、React Native 等多个平台上运行的应用,帮助开发者提升开发效率,改善开发体验,降低多端研发成本。</p>\n<p>自开源以来,Taro 一直广受业界关注,其原理与思想也得到了广泛开发者的认可,这对于我们来说无疑是一件令人振奋的事。但是由于初期 Taro 测试与实现方式的不足,导致在开源期间 Bug 较多,受到了一些质疑。为此,我们痛定思痛,积极接受了开源社区的意见与帮助,并努力探索提升 Taro 稳定性与性能的方式。经过不断地迭代完善,已经让 Taro 浴火重生。</p>\n<h2 id=\"1-0-0-真的来了\" class=\"post-heading\"><a href=\"#1-0-0-真的来了\" class=\"headerlink\" title=\"1.0.0 真的来了\"></a>1.0.0 真的来了<a class=\"post-anchor\" href=\"#1-0-0-真的来了\" aria-hidden=\"true\"></a></h2>\n<p>Taro 开源到现在 3 个月时间,累计发布 70 余日常版本及 20 余 Taro 1.0.0 的 <code>beta</code> 版本,经过近百个版本的迭代优化,我们亲身体会到 Taro 的 BUG 反馈越来越少,Taro 越来越健壮且完善,因此,我们有信心推出 1.0.0 正式版。</p>\n<p>Taro 1.0.0 正式版在延续了之前版本优秀特性的同时,增加了更加丰富的特性与功能,并进行了多项小程序端/H5端的转换优化,同时带来了对 React Native 的转换支持。</p>\n<h4 id=\"全新的小程序组件化\" class=\"post-heading\"><a href=\"#全新的小程序组件化\" class=\"headerlink\" title=\"全新的小程序组件化\"></a>全新的小程序组件化<a class=\"post-anchor\" href=\"#全新的小程序组件化\" aria-hidden=\"true\"></a></h4>\n<p>开源之初,由于种种原因,Taro 的微信小程序端组件化采用的是小程序 <code>&lt;template /&gt;</code> 标签来实现的,利用小程序 <code>&lt;template /&gt;</code> 标签的特性,将组件 JS 文件编译成 JS + WXML 模板,在父组件(页面)的模板中通过 <code>&lt;template /&gt;</code> 标签引用子组件的 WXML 模板来进行拼接,从而达到组件化的目的。</p>\n<p>实践证明,<code>Template</code> 模板方案是一个失败的组件化方案,Taro 开源初期的 Bug 主要来源于此。因为这一方案将 JS 逻辑与模板拆分开了,需要手工来保证 JS 与模板中数据一致,这样在循环组件渲染、组件多重嵌套的情况下,要保证组件正确渲染与 <code>props</code> 正确传递的难度非常大,实现的成本也非常高。而且,囿于小程序 <code>&lt;template /&gt;</code> 标签的缺陷,一些功能(例如自定义组件包含子元素,等)无法实现。</p>\n<p>所以,在经过艰辛的探索与实践之后,我们采用了小程序原生组件化来作为 Taro 的小程序端组件化方案,并且通过一些处理,绕开了小程序组件化的诸多限制,为 Taro 的稳定性打下了坚实基础,并带来了以下好处:</p>\n<ul>\n <li>小程序端组件化更加健壮</li>\n <li>尽可能减少由于框架带来的性能问题</li>\n <li>依托官方组件化,方便以后解锁更多可能</li>\n</ul>\n<h4 id=\"全面支持小程序生态\" class=\"post-heading\"><a href=\"#全面支持小程序生态\" class=\"headerlink\" title=\"全面支持小程序生态\"></a>全面支持小程序生态<a class=\"post-anchor\" href=\"#全面支持小程序生态\" aria-hidden=\"true\"></a></h4>\n<p>为了更好地帮助开发者使用 Taro 开发小程序,在 <code>1.0.0</code> 版本中,我们加强了对小程序生态的支持,主要提现在以下几个方面。</p>\n<h5 id=\"支持引用小程序端第三方组件库\" class=\"post-heading\"><a href=\"#支持引用小程序端第三方组件库\" class=\"headerlink\" title=\"支持引用小程序端第三方组件库\"></a>支持引用小程序端第三方组件库<a class=\"post-anchor\" href=\"#支持引用小程序端第三方组件库\" aria-hidden=\"true\"></a></h5>\n<p>受益于小程序组件化重构,我们在 Taro 中支持了直接引用小程序端第三方组件库,让 Taro 可以融入到小程序生态中,为开发者提供更多便利。</p>\n<p>目前实测已经支持了如下知名第三方组件库</p>\n<ul>\n <li><a href=\"https://github.com/ecomfe/echarts-for-weixin\" target=\"_blank\" rel=\"external\">echarts-for-weixin</a>,ECharts 的微信小程序版本</li>\n <li><a href=\"https://github.com/TalkingData/iview-weapp\" target=\"_blank\" rel=\"external\">iview-weapp</a>,iview 的微信小程序版本</li>\n <li><a href=\"https://youzan.github.io/vant-weapp/#/\" target=\"_blank\" rel=\"external\">vant-weapp</a>,轻量、可靠的小程序 UI 组件库</li>\n <li><a href=\"https://github.com/icindy/wxParse\" target=\"_blank\" rel=\"external\">wxParse</a>,微信小程序富文本解析自定义组件,支持HTML及markdown解析</li>\n</ul>\n<p><img src=\"http://img20.360buyimg.com/uba/jfs/t26989/167/305293473/64582/33fab1f9/5b8e82fdN4c7f85a1.png\" alt=\"\"></p>\n<p>当然,Taro 不仅仅只支持上述组件库,只要是按原生小程序规范编写的组件库都能在 Taro 中正常使用。</p>\n<h5 id=\"支持-Taro-代码与原生小程序代码混写\" class=\"post-heading\"><a href=\"#支持-Taro-代码与原生小程序代码混写\" class=\"headerlink\" title=\"支持 Taro 代码与原生小程序代码混写\"></a>支持 Taro 代码与原生小程序代码混写<a class=\"post-anchor\" href=\"#支持-Taro-代码与原生小程序代码混写\" aria-hidden=\"true\"></a></h5>\n<p>与此同时,Taro 支持原生小程序代码与 Taro 代码混写,因此,可以使用 Taro 愉快地将旧的小程序项目慢慢地改造成 Taro 项目了。</p>\n<h5 id=\"支持小程序端分包加载以及插件引用\" class=\"post-heading\"><a href=\"#支持小程序端分包加载以及插件引用\" class=\"headerlink\" title=\"支持小程序端分包加载以及插件引用\"></a>支持小程序端分包加载以及插件引用<a class=\"post-anchor\" href=\"#支持小程序端分包加载以及插件引用\" aria-hidden=\"true\"></a></h5>\n<p>我们在 Taro 中也加入对原生小程序分包加载功能的支持,配置的方式与原生小程序基本一致,只需要在 <code>app.js</code> 入口文件中加入 <code>subPackage</code> 字段即可。</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div>\n <div class=\"line\">\n 16\n </div>\n <div class=\"line\">\n 17\n </div>\n <div class=\"line\">\n 18\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">import</span> Taro, { Component } \n <span class=\"keyword\">from</span> \n <span class=\"string\">\'@tarojs/taro\'</span>\n </div>\n <div class=\"line\">\n <span class=\"class\"><span class=\"keyword\">class</span> <span class=\"title\">App</span> <span class=\"keyword\">extends</span> <span class=\"title\">Component</span> </span>{\n </div>\n <div class=\"line\">\n config = {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">pages</span>: [\n </div>\n <div class=\"line\"> \n <span class=\"string\">\'pages/index\'</span>,\n </div>\n <div class=\"line\"> \n <span class=\"string\">\'pages/logs\'</span>\n </div>\n <div class=\"line\">\n ],\n </div>\n <div class=\"line\"> \n <span class=\"attr\">subPackages</span>: [\n </div>\n <div class=\"line\">\n {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">root</span>: \n <span class=\"string\">\'moduleA\'</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">pages</span>: [\n </div>\n <div class=\"line\"> \n <span class=\"string\">\'pages/rabbit\'</span>,\n </div>\n <div class=\"line\"> \n <span class=\"string\">\'pages/squirrel\'</span>\n </div>\n <div class=\"line\">\n ]\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n ]\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>同时,Taro 也支持了使用小程序的插件功能,可以在 Taro 中直接引用第三方的插件</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div>\n <div class=\"line\">\n 16\n </div>\n <div class=\"line\">\n 17\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">import</span> Taro, { Component } \n <span class=\"keyword\">from</span> \n <span class=\"string\">\'@tarojs/taro\'</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">import</span> { View } \n <span class=\"keyword\">from</span> \n <span class=\"string\">\'@tarojs/Components\'</span>\n </div>\n <div class=\"line\">\n <span class=\"class\"><span class=\"keyword\">class</span> <span class=\"title\">PageA</span> <span class=\"keyword\">extends</span> <span class=\"title\">Component</span> </span>{\n </div>\n <div class=\"line\">\n config = {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">usingComponents</span>: {\n </div>\n <div class=\"line\"> \n <span class=\"string\">\'hello-component\'</span>: \n <span class=\"string\">\'plugin://myPlugin/hello-component\'</span>\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n render () {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">return</span> (\n </div>\n <div class=\"line\"> \n <span class=\"xml\"><span class=\"tag\">&lt;<span class=\"name\">View</span>&gt;</span></span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">hello-component</span>&gt;</span>\n <span class=\"tag\">&lt;/<span class=\"name\">hello-component</span>&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;/<span class=\"name\">View</span>&gt;</span>\n </div>\n <div class=\"line\">\n )\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h4 id=\"小程序-setState-性能优化\" class=\"post-heading\"><a href=\"#小程序-setState-性能优化\" class=\"headerlink\" title=\"小程序 setState 性能优化\"></a>小程序 setState 性能优化<a class=\"post-anchor\" href=\"#小程序-setState-性能优化\" aria-hidden=\"true\"></a></h4>\n<p>在最初的开源版本中,小程序端 <code>setState</code> 仅仅是对小程序 <code>setData</code> 做了一次异步封装,最终调用 <code>setData</code> 更新的时候还是传入了完整数据。</p>\n<p>但众所周知,小程序的视图层目前使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境,在架构上,WebView 和 JavascriptCore 都是独立的模块,并不具备数据直接共享的通道。在调用 <code>setData</code> 之后,会将数据使用 <code>JSON.stringify</code> 进行序列化,再拼接成脚本,然后再传给视图层渲染,这样的话,当数据量非常大的时候,小程序就会变得异常卡顿,性能很差。</p>\n<p><img src=\"http://img13.360buyimg.com/img/jfs/t27457/22/362930291/23083/36eaf574/5b8f8ef1Nf68a4646.jpg\" alt=\"\"></p>\n<p>Taro 在框架级别帮助开发者进行了优化,在 <code>setData</code> 之前进行了一次<strong>数据 Diff</strong>,找到数据的最小更新路径,然后再使用此路径来进行更新。例如</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div>\n <div class=\"line\">\n 16\n </div>\n <div class=\"line\">\n 17\n </div>\n <div class=\"line\">\n 18\n </div>\n <div class=\"line\">\n 19\n </div>\n <div class=\"line\">\n 20\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">// 初始 state</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">this</span>.state = {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">a</span>: [\n <span class=\"number\">0</span>],\n </div>\n <div class=\"line\"> \n <span class=\"attr\">b</span>: {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">x</span>: {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">y</span>: \n <span class=\"number\">1</span>\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"comment\">// 调用 this.setState</span>\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"keyword\">this</span>.setState({\n </div>\n <div class=\"line\"> \n <span class=\"attr\">a</span>: [\n <span class=\"number\">1</span>, \n <span class=\"number\">2</span>],\n </div>\n <div class=\"line\"> \n <span class=\"attr\">b</span>: {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">x</span>: {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">y</span>: \n <span class=\"number\">10</span>\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n })\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>在优化之前,会直接将 <code>this.setState</code> 的数据传给 <code>setData</code>,即</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">this</span>.$scope.setData({\n </div>\n <div class=\"line\"> \n <span class=\"attr\">a</span>: [\n <span class=\"number\">1</span>, \n <span class=\"number\">2</span>],\n </div>\n <div class=\"line\"> \n <span class=\"attr\">b</span>: {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">x</span>: {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">y</span>: \n <span class=\"number\">10</span>\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n })\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>而在优化之后的数据更新则变成了</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">this</span>.$scope.setData({\n </div>\n <div class=\"line\"> \n <span class=\"string\">\'a[0]\'</span>: \n <span class=\"number\">1</span>,\n </div>\n <div class=\"line\"> \n <span class=\"string\">\'a[1]\'</span>: \n <span class=\"number\">2</span>,\n </div>\n <div class=\"line\"> \n <span class=\"string\">\'b.x.y\'</span>: \n <span class=\"number\">10</span>\n </div>\n <div class=\"line\">\n })\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>这样的优化对于小程序来说意义非常重大,可以避免因为数据更新导致的性能问题。</p>\n<h4 id=\"更加丰富的-JSX-语法支持\" class=\"post-heading\"><a href=\"#更加丰富的-JSX-语法支持\" class=\"headerlink\" title=\"更加丰富的 JSX 语法支持\"></a>更加丰富的 JSX 语法支持<a class=\"post-anchor\" href=\"#更加丰富的-JSX-语法支持\" aria-hidden=\"true\"></a></h4>\n<p>前面已经提到 Taro 使用 React 语法规范来开发多端应用,这样就必然是采用 JSX 来作为模板,所以 Taro 需要将 JSX 编译成各个端支持的模板,其中以小程序端最为复杂,Taro 需要将 JSX 编译成小程序的 WXML 模板。</p>\n<p>在开源的过程中,Taro 支持的 JSX 写法一直在不断完善,力求让开发体验更加接近于 React,主要包括以下语法支持:</p>\n<ul>\n <li>支持 Ref,提供了更加方便的组件和元素定位方式</li>\n <li>支持 this.props.children 写法,方便进行自定义组件传入子元素</li>\n <li>在循环体内执行函数和表达式</li>\n <li>定义 JSX 作为变量使用</li>\n <li>支持复杂的 if-else 语句</li>\n <li>在 JSX 属性中使用复杂表达式</li>\n <li>在 style 属性中使用对象</li>\n <li>只有使用到的变量才会作为 state 加入到小程序 data,从而精简小程序数据</li>\n</ul>\n<p>目前,除了 Taro 官方 ESLint 插件 <a href=\"https://github.com/NervJS/taro/tree/master/packages/eslint-plugin-taro\" target=\"_blank\" rel=\"external\">eslint-plugin-taro</a> 中限制的语法之外,其他 JSX 语法基本都支持,而在原生组件化的帮助下,未来将会把 ESLint 的限制也逐条取消。</p>\n<h4 id=\"React-Native-端转换支持\" class=\"post-heading\"><a href=\"#React-Native-端转换支持\" class=\"headerlink\" title=\"React Native 端转换支持\"></a>React Native 端转换支持<a class=\"post-anchor\" href=\"#React-Native-端转换支持\" aria-hidden=\"true\"></a></h4>\n<p>在 Taro 1.0.0 中,我们将正式推出 React Native 端的转换支持,可以将现有 Taro 项目转换成 RN 版本,但需要注意对样式的处理,因为 RN 支持的样式非常有限,它是属于 CSS 的子集,具体请移步 <a href=\"https://nervjs.github.io/taro/docs/before-dev-remind.html\" target=\"_blank\" rel=\"external\">RN 端转换注意事项</a>。</p>\n<p>Taro 的 RN 端基于 <a href=\"https://expo.io/\" target=\"_blank\" rel=\"external\">Expo</a> 来进行编译构建 RN 程序,开发方式和编译命令与其他端类似,代码目录结构与现有 Taro 项目也基本一致</p>\n<p>RN 编译预览模式</p>\n<figure class=\"highlight bash\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\"># npm script</span>\n </div>\n <div class=\"line\">\n $ npm run dev:rn\n </div>\n <div class=\"line\">\n <span class=\"comment\"># 仅限全局安装</span>\n </div>\n <div class=\"line\">\n $ taro build --\n <span class=\"built_in\">type</span> rn --watch\n </div>\n <div class=\"line\">\n <span class=\"comment\"># npx用户也可以使用</span>\n </div>\n <div class=\"line\">\n $ npx taro build --\n <span class=\"built_in\">type</span> rn --watch\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>执行完命令之后,Taro 将会开始编译文件,最终获得 RN 端的编译指引,并且启动你的应用了</p>\n<p><img src=\"http://img14.360buyimg.com/uba/jfs/t27748/263/340701379/478755/c8b632f8/5b8e8539N35b82790.jpg\" alt=\"\"></p>\n<p>React Native 端完整开发流程请移步 <a href=\"https://nervjs.github.io/taro/docs/react-native.html\" target=\"_blank\" rel=\"external\">React Native 开发流程介绍</a>。</p>\n<h4 id=\"更加健全的-TypeScript-支持\" class=\"post-heading\"><a href=\"#更加健全的-TypeScript-支持\" class=\"headerlink\" title=\"更加健全的 TypeScript 支持\"></a>更加健全的 TypeScript 支持<a class=\"post-anchor\" href=\"#更加健全的-TypeScript-支持\" aria-hidden=\"true\"></a></h4>\n<p><img src=\"http://img14.360buyimg.com/img/jfs/t25927/210/1256109652/8732/ad4062ce/5b8f8ef1N50c24d7b.jpg\" alt=\"\"></p>\n<p>随着前端项目的复杂度和规模的增加,近年来越来越多的项目开始使用 TypeScript 进行开发。Taro 也注意到了这一趋势,在开源之初就提供了对 TypeScript 的支持。在社区的帮助下,Taro 对 TypeScript 的支持也更加健全和完善:</p>\n<ul>\n <li><code>@tarojs/cli</code> 可以直接创建 TypeScript 项目</li>\n <li><code>@tarojs/components</code> 包含了所有组件的类型定义</li>\n <li><code>@tarojs/taro</code> 的所有 API 也都包含了类型定义</li>\n <li><code>@tarojs/redux</code> 和 <code>taro-ui</code> 也都全部内置了类型定义</li>\n <li>构建系统的编译器从 Babel 切换到原生 TypeScript 编译器,支持所有 TypeScript 语法</li>\n</ul>\n<h4 id=\"H5-端转换优化\" class=\"post-heading\"><a href=\"#H5-端转换优化\" class=\"headerlink\" title=\"H5 端转换优化\"></a>H5 端转换优化<a class=\"post-anchor\" href=\"#H5-端转换优化\" aria-hidden=\"true\"></a></h4>\n<p>同时,Taro 在 H5 端的转换中,也进行诸多转换优化,修复了之前版本 H5 下诸多 Bug,让 H5 端路由系统更加健壮,同时开放了 Webpack 配置,可以让开发者自由地进行相关配置。</p>\n<p>最后,附上 Taro 完整的 <a href=\"https://github.com/NervJS/taro/blob/master/CHANGELOG.md\" target=\"_blank\" rel=\"external\">迭代历程</a>。</p>\n<h2 id=\"重生之路\" class=\"post-heading\"><a href=\"#重生之路\" class=\"headerlink\" title=\"重生之路\"></a>重生之路<a class=\"post-anchor\" href=\"#重生之路\" aria-hidden=\"true\"></a></h2>\n<p>正如上文提到,Taro 1.0.0 带来许多新的优秀特性,而同时,在社区内的 Bug 反馈已经愈来愈少,这也是开源期间不断努力打磨的结果。</p>\n<p><img src=\"http://img20.360buyimg.com/uba/jfs/t1/5087/4/5578/33760/5ba072d5E58ffa438/8c289d58d3f8527b.png\" alt=\"\"></p>\n<p>从 <code>6.7</code> 到行文截止,Taro 一共经历了 <strong>1800</strong> 余次提交,平均每天近 <strong>20</strong> 次,最多的一天达 <strong>30</strong> 次。每一次提交都是进步,每一次提交都让 Taro 更加强大。经过这么多次迭代之后,已经让 Taro 获得重生,尤其是小程序组件化重构完成之后,Taro 从旧版架构的泥潭中一跃而出,成为更加健壮的开发框架。</p>\n<p>在我们自己不断反思、优化的同时,也积极融入开源社区,依托社区的力量去建设 Taro。</p>\n<p>GitHub ISSUES 是检验一个开源项目靠谱程度的标准之一,到目前为止,一共收到了 <strong>500</strong> 余个 ISSUES,已关闭近 <strong>400</strong> 个,关闭率在 <strong>80%</strong> 左右,未关闭的也大部分是一些功能的迭代需求。在 Taro 开发团队内部更是要求每一个 ISSUE 的响应时间不能超过 <strong>24</strong> 小时。正是因为这些 ISSUES ,让我们不断意识到 Taro 的不足,让我们知道如何去进行迭代。</p>\n<p>同时,我们也一直鼓励社区的开发者积极提 PR,一个优秀的开源项目需要依靠整个社区的力量才能完善起来,到目前为止,一共收到了 <strong>120</strong> 余个 PR,已几近全部合入,这些 PR 为 Taro 注入了许多新鲜血液,让 Taro 更加健壮,我们也期望能有更多的开发者可以加入进来,一起来让 Taro 更加美好。</p>\n<p>在 GitHub 上交流之余,我们也为开发者们开通了<a href=\"https://github.com/NervJS/taro/issues/198\" target=\"_blank\" rel=\"external\">官方微信群</a>供大家一起讨论 Taro 与技术,目前已有超过 <strong>1700</strong> 位开发者在关注、使用 Taro ,期待更多开发者的加入。</p>\n<h2 id=\"开发者生态完善\" class=\"post-heading\"><a href=\"#开发者生态完善\" class=\"headerlink\" title=\"开发者生态完善\"></a>开发者生态完善<a class=\"post-anchor\" href=\"#开发者生态完善\" aria-hidden=\"true\"></a></h2>\n<p>在开源之初,Taro 一直处于封闭的状态,没有适配的 UI 库,也无法使用第三方组件库,而这些对开发效率的桎梏非常严重,社区内对此反馈较多。</p>\n<p>所以,在 <code>1.0.0-beta</code> 版本开发期间,Taro 团队开发了多端 UI 库打包功能,提供了 <code>taro build --ui</code> 命令来将按照一定规则组织的代码打包成可以在 Taro 中使用的多端 UI 库。</p>\n<p><img src=\"http://img12.360buyimg.com/img/jfs/t25957/217/1263982871/58035/52bedb06/5b8f8ef2Nfde1c993.jpg\" alt=\"\"></p>\n<p>并且,基于这一功能,我们推出了首个可以跨多端使用的多端 UI 库 <a href=\"https://taro-ui.aotu.io/\" target=\"_blank\" rel=\"external\">Taro UI</a>,目前已经支持了微信小程序与 H5 端,不久之后将完成 React Native 端的适配,可以同步提供给 React Native 端使用。</p>\n<p><img src=\"http://img10.360buyimg.com/img/jfs/t1/3616/25/135/84031/5b8f8ef2Ebd101c2f/0ec4bd1dae310555.jpg\" alt=\"\"></p>\n<p>目前,多端 UI 库打包功能还处于内测阶段,在 Taro 1.0.0 发布之后,这一功能将同步发布,这样更多开发者就可以为 Taro 开发更丰富的多端组件库和 UI 库了。</p>\n<h2 id=\"未来规划\" class=\"post-heading\"><a href=\"#未来规划\" class=\"headerlink\" title=\"未来规划\"></a>未来规划<a class=\"post-anchor\" href=\"#未来规划\" aria-hidden=\"true\"></a></h2>\n<p>Taro 将会继续保持迭代,目前已经规划了如下重要功能。</p>\n<h4 id=\"便捷测试\" class=\"post-heading\"><a href=\"#便捷测试\" class=\"headerlink\" title=\"便捷测试\"></a>便捷测试<a class=\"post-anchor\" href=\"#便捷测试\" aria-hidden=\"true\"></a></h4>\n<p>在编译时与运行时提供代码诊断的功能,分析代码优劣,判定代码写法是否规范,以便帮助开发者规避一些由于写法带来的问题。</p>\n<p>同时将提供一套测试方案,方便开发者书写并运行组件测试用例,提升代码质量。</p>\n<h4 id=\"多端同步调试\" class=\"post-heading\"><a href=\"#多端同步调试\" class=\"headerlink\" title=\"多端同步调试\"></a>多端同步调试<a class=\"post-anchor\" href=\"#多端同步调试\" aria-hidden=\"true\"></a></h4>\n<p>目前 Taro 只能一次调试一个端,这对于开发多端应用来说效率略低,所以,计划提供微信小程序/ H5 / React Native 端同时调试的功能,可以一键启动多端同时编译,从而获得多端同步预览。</p>\n<h4 id=\"微信小程序-H5-代码转-Taro-代码\" class=\"post-heading\"><a href=\"#微信小程序-H5-代码转-Taro-代码\" class=\"headerlink\" title=\"微信小程序/H5 代码转 Taro 代码\"></a>微信小程序/H5 代码转 Taro 代码<a class=\"post-anchor\" href=\"#微信小程序-H5-代码转-Taro-代码\" aria-hidden=\"true\"></a></h4>\n<p>目前已支持 Taro 代码到小程序代码、 H5 代码的转换,在未来,将提供逆向转换功能,帮助开发者将原本就存在的小程序/H5 项目直接转换成 Taro 项目,从而让原本只能运行在一端的项目获得多端运行的能力,降低开发者的重构成本。</p>\n<h4 id=\"与-React-新特性保持同步\" class=\"post-heading\"><a href=\"#与-React-新特性保持同步\" class=\"headerlink\" title=\"与 React 新特性保持同步\"></a>与 React 新特性保持同步<a class=\"post-anchor\" href=\"#与-React-新特性保持同步\" aria-hidden=\"true\"></a></h4>\n<p>Taro 是遵循 React 语法规范的,但是 React 一直在迭代在变化,Taro 作为 React 的追随者也将会保持与 React 新特性同步,让 Taro 最大程度接近 React 开发体验。</p>\n<h4 id=\"快应用端支持\" class=\"post-heading\"><a href=\"#快应用端支持\" class=\"headerlink\" title=\"快应用端支持\"></a>快应用端支持<a class=\"post-anchor\" href=\"#快应用端支持\" aria-hidden=\"true\"></a></h4>\n<p>目前 Taro 已经完成了快应用端组件库与 API 的适配,快应用端的文件转换与模板转换也正在开发中,不久的将来就会发布支持快应用端转换的版本。</p>\n<h4 id=\"支付宝小程序与百度智能小程序支持\" class=\"post-heading\"><a href=\"#支付宝小程序与百度智能小程序支持\" class=\"headerlink\" title=\"支付宝小程序与百度智能小程序支持\"></a>支付宝小程序与百度智能小程序支持<a class=\"post-anchor\" href=\"#支付宝小程序与百度智能小程序支持\" aria-hidden=\"true\"></a></h4>\n<p>已预研支付宝小程序与百度智能小程序转换的可行性,即将进入开发。</p>\n<h4 id=\"多端可视化拖拽搭建\" class=\"post-heading\"><a href=\"#多端可视化拖拽搭建\" class=\"headerlink\" title=\"多端可视化拖拽搭建\"></a>多端可视化拖拽搭建<a class=\"post-anchor\" href=\"#多端可视化拖拽搭建\" aria-hidden=\"true\"></a></h4>\n<p>目前 Taro 是依靠开发者手工编写代码来获得多端应用的,Taro 未来计划提供一个多端可视化拖拽搭建的功能,可以通过拖拽组件的方式来生成多端应用。</p>\n<p>同时,Taro 将联合各大公司小程序开发团队,推出丰富的行业模板,为各行业应用可视化搭建提供完整的解决方案。</p>\n<h2 id=\"使用案例\" class=\"post-heading\"><a href=\"#使用案例\" class=\"headerlink\" title=\"使用案例\"></a>使用案例<a class=\"post-anchor\" href=\"#使用案例\" aria-hidden=\"true\"></a></h2>\n<p>在开源期间,随着 Taro 的逐步完善,越来越多的开发者加入到 Taro 的使用、开发中,产生了更多更优秀的使用案例。</p>\n<p><img src=\"http://img13.360buyimg.com/uba/jfs/t1/4426/11/5716/492741/5ba072ccE63cc3586/8dd883d27d6a40d1.png\" alt=\"qrcode.png\"></p>\n<h2 id=\"特别鸣谢\" class=\"post-heading\"><a href=\"#特别鸣谢\" class=\"headerlink\" title=\"特别鸣谢\"></a>特别鸣谢<a class=\"post-anchor\" href=\"#特别鸣谢\" aria-hidden=\"true\"></a></h2>\n<p>Taro 的发展离不开广大开源爱好者的帮助,在此特别鸣谢广大 Taro 的使用者以及 Taro 主要贡献者(排名不分先后)。</p>\n<ul>\n <li><a href=\"https://github.com/aijiacy\" target=\"_blank\" rel=\"external\">aijiacy</a></li>\n <li><a href=\"https://github.com/AlexStacker\" target=\"_blank\" rel=\"external\">AlexStacker</a></li>\n <li><a href=\"https://github.com/AsukaSong\" target=\"_blank\" rel=\"external\">AsukaSong</a></li>\n <li><a href=\"https://github.com/atzcl\" target=\"_blank\" rel=\"external\">atzcl</a></li>\n <li><a href=\"https://github.com/Boshen\" target=\"_blank\" rel=\"external\">Boshen</a></li>\n <li><a href=\"https://github.com/Bless-L\" target=\"_blank\" rel=\"external\">Bless-L</a></li>\n <li><a href=\"https://github.com/beidan\" target=\"_blank\" rel=\"external\">beidan</a></li>\n <li><a href=\"https://github.com/Chen-jj\" target=\"_blank\" rel=\"external\">Chen-jj</a></li>\n <li><a href=\"https://github.com/cuitianze\" target=\"_blank\" rel=\"external\">cuitianze</a></li>\n <li><a href=\"https://github.com/dogbutcat\" target=\"_blank\" rel=\"external\">dogbutcat</a></li>\n <li><a href=\"https://github.com/finian\" target=\"_blank\" rel=\"external\">finian</a></li>\n <li><a href=\"https://github.com/frontlich\" target=\"_blank\" rel=\"external\">frontlich</a></li>\n <li><a href=\"https://github.com/guotie\" target=\"_blank\" rel=\"external\">guotie</a></li>\n <li><a href=\"https://github.com/icodytan\" target=\"_blank\" rel=\"external\">icodytan</a></li>\n <li><a href=\"https://github.com/JerrySir\" target=\"_blank\" rel=\"external\">JerrySir</a></li>\n <li><a href=\"https://github.com/js-newbee\" target=\"_blank\" rel=\"external\">js-newbee</a></li>\n <li><a href=\"https://github.com/jas0ncn\" target=\"_blank\" rel=\"external\">jas0ncn</a></li>\n <li><a href=\"https://github.com/jinjinjin0731\" target=\"_blank\" rel=\"external\">jinjinjin0731</a></li>\n <li><a href=\"https://github.com/kdong007\" target=\"_blank\" rel=\"external\">kdong007</a></li>\n <li><a href=\"https://github.com/kenberkeley\" target=\"_blank\" rel=\"external\">kenberkeley</a></li>\n <li><a href=\"https://github.com/Lizhooh\" target=\"_blank\" rel=\"external\">Lizhooh</a></li>\n <li><a href=\"https://github.com/Littly\" target=\"_blank\" rel=\"external\">Littly</a></li>\n <li><a href=\"https://github.com/lolipop99\" target=\"_blank\" rel=\"external\">lolipop99</a></li>\n <li><a href=\"https://github.com/lijinke666\" target=\"_blank\" rel=\"external\">lijinke666</a></li>\n <li><a href=\"https://github.com/looch\" target=\"_blank\" rel=\"external\">looch</a></li>\n <li><a href=\"https://github.com/ladjzero\" target=\"_blank\" rel=\"external\">ladjzero</a></li>\n <li><a href=\"https://github.com/limichange\" target=\"_blank\" rel=\"external\">limichange</a></li>\n <li><a href=\"https://github.com/leeenx\" target=\"_blank\" rel=\"external\">leeenx</a></li>\n <li><a href=\"https://github.com/luckyadam\" target=\"_blank\" rel=\"external\">luckyadam</a></li>\n <li><a href=\"https://github.com/ly525\" target=\"_blank\" rel=\"external\">ly525</a></li>\n <li><a href=\"https://github.com/Manjiz\" target=\"_blank\" rel=\"external\">Manjiz</a></li>\n <li><a href=\"https://github.com/mclockw\" target=\"_blank\" rel=\"external\">mclockw</a></li>\n <li><a href=\"https://github.com/Mr-Prune\" target=\"_blank\" rel=\"external\">Mr-Prune</a></li>\n <li><a href=\"https://github.com/missmimia\" target=\"_blank\" rel=\"external\">missmimia</a></li>\n <li><a href=\"https://github.com/mushan0x0\" target=\"_blank\" rel=\"external\">mushan0x0</a></li>\n <li><a href=\"https://github.com/Pines-Cheng\" target=\"_blank\" rel=\"external\">Pines-Cheng</a></li>\n <li><a href=\"https://github.com/rojer95\" target=\"_blank\" rel=\"external\">rojer95</a></li>\n <li><a href=\"https://github.com/ronffy\" target=\"_blank\" rel=\"external\">ronffy</a></li>\n <li><a href=\"https://github.com/Songkeys\" target=\"_blank\" rel=\"external\">Songkeys</a></li>\n <li><a href=\"https://github.com/Simbachen\" target=\"_blank\" rel=\"external\">Simbachen</a></li>\n <li><a href=\"https://github.com/smoothdvd\" target=\"_blank\" rel=\"external\">smoothdvd</a></li>\n <li><a href=\"https://github.com/soulhat\" target=\"_blank\" rel=\"external\">soulhat</a></li>\n <li><a href=\"https://github.com/thewei\" target=\"_blank\" rel=\"external\">thewei</a></li>\n <li><a href=\"https://github.com/wowlusitong\" target=\"_blank\" rel=\"external\">wowlusitong</a></li>\n <li><a href=\"https://github.com/xunge0613\" target=\"_blank\" rel=\"external\">xunge0613</a></li>\n <li><a href=\"https://github.com/YikaJ\" target=\"_blank\" rel=\"external\">YikaJ</a></li>\n <li><a href=\"https://github.com/yuche\" target=\"_blank\" rel=\"external\">yuche</a></li>\n <li><a href=\"https://github.com/zaaack\" target=\"_blank\" rel=\"external\">zaaack</a></li>\n <li><a href=\"https://github.com/zacksleo\" target=\"_blank\" rel=\"external\">zacksleo</a></li>\n <li><a href=\"https://github.com/ZodiacSyndicate\" target=\"_blank\" rel=\"external\">ZodiacSyndicate</a></li>\n <li><a href=\"https://github.com/zuoge85\" target=\"_blank\" rel=\"external\">zuoge85</a></li>\n <li><a href=\"https://github.com/zuorichongxian\" target=\"_blank\" rel=\"external\">zuorichongxian</a></li>\n</ul>\n<div class=\"post-tags\" style=\"display:none\">\n <a href=\"/tags/React/\">React</a> \n <a href=\"/tags/小程序/\">小程序</a> \n <a href=\"/tags/Nerv/\">Nerv</a> \n <a href=\"/tags/Taro/\">Taro</a>\n</div>\n<div class=\"post-categories\" style=\"display:none\">\n <a href=\"/cates/小程序/\">小程序</a>\n</div>\n<div class=\"post-announce\">\n 感谢您的阅读,本文由 \n <a href=\"//aotu.io\">凹凸实验室</a> 版权所有。如若转载,请注明出处:凹凸实验室(\n <a href=\"https://aotu.io/notes/2018/09/18/taro-1-0-0/\">https://aotu.io/notes/2018/09/18/taro-1-0-0/</a>)\n</div>\n<div class=\"post-revision\">\n <i class=\"fa fa-clock-o\"></i> \n <time class=\"post-updated\" datetime=\"2018-11-07T01:01:15.657Z\">上次更新:2018-11-07 09:01:15</time>\n</div>');
INSERT INTO `rumo_blog` VALUES ('31', 'Nerv - 京东高性能前端框架', '1', 'Nerv - 京东高性能前端框架', '1', '2018-11-11 21:02:33', '2018-11-11 21:02:33', '1', '0', 'https://misc.aotu.io/712/2018-03-22-nerv/nerv_cover_logo.jpg', '', '0', '0', '<a id=\"more\"></a>\n<p>Nerv 是一款由京东凹凸实验室打造的类 React 前端框架。目前已广泛运用在<a href=\"https://www.jd.com/\" target=\"_blank\" rel=\"external\">京东商城</a>(JD.COM)核心业务及<a href=\"http://www.toplife.com\" target=\"_blank\" rel=\"external\">TOPLIFE</a>全站。Nerv 基于React标准,使用 Virtual Dom 技术,拥有和 React 一致的 API 与生命周期,如果你已经对 React 使用非常熟悉,那么使用 Nerv 开发对你来说绝对是零学习成本。</p>\n<p>与此同时,相比于 React 以及市面其他同类型框架,Nerv 更具体积轻量,性能高效的特点。并且,它符合当下国情,可以完美兼容 IE8 及以上浏览器。</p>\n<ul>\n <li>官网:<a href=\"https://nerv.aotu.io/\" target=\"_blank\" rel=\"external\">https://nerv.aotu.io/</a></li>\n <li>文档:<a href=\"https://nervjs.github.io/docs/\" target=\"_blank\" rel=\"external\">https://nervjs.github.io/docs/</a></li>\n <li>GitHub:<a href=\"https://github.com/NervJS/nerv\" target=\"_blank\" rel=\"external\">https://github.com/NervJS/nerv</a></li>\n</ul>\n<h2 id=\"特性\" class=\"post-heading\"><a href=\"#特性\" class=\"headerlink\" title=\"特性\"></a>特性<a class=\"post-anchor\" href=\"#特性\" aria-hidden=\"true\"></a></h2>\n<ul>\n <li><p>更小的体积:<br>Gzip 后仅有9k,不到 React 三分之一的体积,低性能设备也能高速地加载并解析执行。</p></li>\n <li><p>更高的性能:<br>高效、同步的 diff 算法和诸多优化策略使得 Nerv 成为性能最高的前端框架之一。</p></li>\n <li><p>完备的生态:<br>无需 nerv-compat,只需要在稍稍设置就能享受整个 React 生态的共同成果。</p></li>\n <li><p>更一致的渲染:<br>不仅在浏览器上能高效地渲染,在服务器上 Nerv 同样能高效地运行。</p></li>\n <li><p>更优的兼容:<br>和大多数现代框架不同,Nerv 将继续保持对 IE8 的兼容。</p></li>\n <li><p>更具说服力的案例:<br>不管是京东首页的高流量,还是 Toplife 的复杂业务,Nerv 都经受住了来自于真实业务的考验。</p></li>\n</ul>\n<h2 id=\"性能\" class=\"post-heading\"><a href=\"#性能\" class=\"headerlink\" title=\"性能\"></a>性能<a class=\"post-anchor\" href=\"#性能\" aria-hidden=\"true\"></a></h2>\n<p>在 Nerv 开发过程中,我们针对虚拟 Dom 算法做了一次升级,将并行的虚拟 Dom diff 过程替换成同步的,边 diff 边 patch ,这样大幅度提升了虚拟 Dom 更新的速度。同时我们还对diff算法进行了探索升级,参照目前市面上最快的虚拟 Dom 算法对我们的代码进行了改造。</p>\n<p>经过重构升级后,我们的框架性能大幅提升,如下可见。</p>\n<p><img src=\"https://misc.aotu.io/712/2018-03-22-nerv/benchmark.jpeg\" alt=\"image\"></p>\n<p>更多性能数据详见<a href=\"https://nerv.aotu.io/\" target=\"_blank\" rel=\"external\">官网</a>。</p>\n<h2 id=\"项目背景\" class=\"post-heading\"><a href=\"#项目背景\" class=\"headerlink\" title=\"项目背景\"></a>项目背景<a class=\"post-anchor\" href=\"#项目背景\" aria-hidden=\"true\"></a></h2>\n<p>是的,我们又造了一个轮子,也是一次抛离传统开发模式的技术革新。同行们或许有疑问,目前市面上已经有非常多的同类型技术框架,为什么我们还要不厌其烦地打造一个呢?这当然不是在做无用功。</p>\n<p>日常开发中,相对于 Vue ,我们更倾向于选择 React 模式作为我们的开发标准,因为 React 天生组件化且函数式编程的方式,更加灵活且便于维护。<br>然而,React 仍然有一些不能满足我们需求的地方:</p>\n<ul>\n <li>IE8 浏览器兼容性:当前环境所限,即便很不情愿,我们仍然要支持 IE8。</li>\n <li>体积:React 大概 130kb 的体积。在低网速 / 低版本浏览器 / 低配置设备的加载速度和解析速度都不能让我们满意。</li>\n <li>性能:React 的 Virtual Dom 算法(React 自己叫 Reconciler)并没有做太多的优化。</li>\n</ul>\n<p>而我们的新轮子 —— Nerv,它完全能提供上述 React 的所有优点,并且它也能完全满足我们自己的需求:更好的兼容性、更小的体积、更高的性能。</p>\n<h2 id=\"安装\" class=\"post-heading\"><a href=\"#安装\" class=\"headerlink\" title=\"安装\"></a>安装<a class=\"post-anchor\" href=\"#安装\" aria-hidden=\"true\"></a></h2>\n<p>推荐使用 npm 的方式进行开发,享受 Node 生态圈和 Webpack 工具链带来的便利。</p>\n<figure class=\"highlight sql\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n npm \n <span class=\"keyword\">install</span> nervjs \n <span class=\"comment\">--save</span>\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h2 id=\"简单示例\" class=\"post-heading\"><a href=\"#简单示例\" class=\"headerlink\" title=\"简单示例\"></a>简单示例<a class=\"post-anchor\" href=\"#简单示例\" aria-hidden=\"true\"></a></h2>\n<p>下面是一个计时器的例子。</p>\n<figure class=\"highlight processing\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div>\n <div class=\"line\">\n 16\n </div>\n <div class=\"line\">\n 17\n </div>\n <div class=\"line\">\n 18\n </div>\n <div class=\"line\">\n 19\n </div>\n <div class=\"line\">\n 20\n </div>\n <div class=\"line\">\n 21\n </div>\n <div class=\"line\">\n 22\n </div>\n <div class=\"line\">\n 23\n </div>\n <div class=\"line\">\n 24\n </div>\n <div class=\"line\">\n 25\n </div>\n <div class=\"line\">\n 26\n </div>\n <div class=\"line\">\n 27\n </div>\n <div class=\"line\">\n 28\n </div>\n <div class=\"line\">\n 29\n </div>\n <div class=\"line\">\n 30\n </div>\n <div class=\"line\">\n 31\n </div>\n <div class=\"line\">\n 32\n </div>\n <div class=\"line\">\n 33\n </div>\n <div class=\"line\">\n 34\n </div>\n <div class=\"line\">\n 35\n </div>\n <div class=\"line\">\n 36\n </div>\n <div class=\"line\">\n 37\n </div>\n <div class=\"line\">\n 38\n </div>\n <div class=\"line\">\n 39\n </div>\n <div class=\"line\">\n 40\n </div>\n <div class=\"line\">\n 41\n </div>\n <div class=\"line\">\n 42\n </div>\n <div class=\"line\">\n 43\n </div>\n <div class=\"line\">\n 44\n </div>\n <div class=\"line\">\n 45\n </div>\n <div class=\"line\">\n 46\n </div>\n <div class=\"line\">\n 47\n </div>\n <div class=\"line\">\n 48\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">import</span> Nerv from \n <span class=\"string\">\'nervjs\'</span>\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n class Counter extends Nerv.Component {\n </div>\n <div class=\"line\">\n setTime = () =&gt; {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">const</span> date = \n <span class=\"keyword\">new</span> Date()\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">const</span> \n <span class=\"built_in\">year</span> = date.getFullYear()\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">const</span> \n <span class=\"built_in\">month</span> = date.getMonth() + \n <span class=\"number\">1</span>\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">const</span> \n <span class=\"built_in\">day</span> = date.getDay()\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">const</span> \n <span class=\"built_in\">hour</span> = date.getHours()\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">const</span> \n <span class=\"built_in\">minute</span> = date.getMinutes()\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">const</span> sec = date.getSeconds()\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">this</span>.setState({\n </div>\n <div class=\"line\"> \n <span class=\"built_in\">year</span>,\n </div>\n <div class=\"line\"> \n <span class=\"built_in\">month</span>,\n </div>\n <div class=\"line\"> \n <span class=\"built_in\">day</span>,\n </div>\n <div class=\"line\"> \n <span class=\"built_in\">hour</span>,\n </div>\n <div class=\"line\"> \n <span class=\"built_in\">minute</span>,\n </div>\n <div class=\"line\">\n sec\n </div>\n <div class=\"line\">\n })\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n componentWillMount () {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">this</span>.setTime()\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n componentWillUnmount () {\n </div>\n <div class=\"line\">\n clearInterval(\n <span class=\"keyword\">this</span>.interval)\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n componentDidMount () {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">this</span>.interval = setInterval(\n <span class=\"keyword\">this</span>.setTime, \n <span class=\"number\">1000</span>)\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n componentWillReceiveProps () {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">this</span>.setTime()\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n render () {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">const</span> { \n <span class=\"built_in\">year</span>, \n <span class=\"built_in\">month</span>, \n <span class=\"built_in\">day</span>, \n <span class=\"built_in\">hour</span>, \n <span class=\"built_in\">minute</span>, sec } = \n <span class=\"keyword\">this</span>.state\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">return</span> (\n </div>\n <div class=\"line\">\n &lt;div className=\n <span class=\"string\">\'counter\'</span>&gt;\n </div>\n <div class=\"line\">\n &lt;span&gt;The time is&lt;/span&gt;{\n <span class=\"built_in\">year</span>}-{\n <span class=\"built_in\">month</span>}-{\n <span class=\"built_in\">day</span>} {\n <span class=\"built_in\">hour</span>}:{\n <span class=\"built_in\">minute</span>}:{sec}\n </div>\n <div class=\"line\">\n &lt;/div&gt;\n </div>\n <div class=\"line\">\n )\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n export \n <span class=\"keyword\">default</span> Counter\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>更多详细例子可阅读文档<a href=\"https://nervjs.github.io/docs/\" target=\"_blank\" rel=\"external\">NervJs</a>了解。</p>\n<h2 id=\"写在最后\" class=\"post-heading\"><a href=\"#写在最后\" class=\"headerlink\" title=\"写在最后\"></a>写在最后<a class=\"post-anchor\" href=\"#写在最后\" aria-hidden=\"true\"></a></h2>\n<p>秉承万维网的开放精神,以及开源世界的信条,我们接受各种评价和讨论,最终目标是为开源世界贡献我们的一份力,感恩!<br>欢迎各位同行使用 <a href=\"https://github.com/NervJS/nerv\" target=\"_blank\" rel=\"external\">Nerv</a>,如果你在使用过程中遇到问题,或者有好的建议,欢迎给我们提<a href=\"https://github.com/NervJS/nerv/issues\" target=\"_blank\" rel=\"external\">Issue</a> 或者 <a href=\"https://github.com/NervJS/nerv/pulls\" target=\"_blank\" rel=\"external\">Pull Request</a>。</p>\n<blockquote>\n <p>结尾小彩蛋&gt;&gt; <a href=\"http://static.360buyimg.com/mtd/pc/components/nerv/nerv_540p_0306.mp4\" target=\"_blank\" rel=\"external\">一个视频带你看懂Nerv</a></p>\n</blockquote>\n<div class=\"post-tags\" style=\"display:none\">\n <a href=\"/tags/javascript/\">javascript</a> \n <a href=\"/tags/library/\">library</a> \n <a href=\"/tags/react/\">react</a> \n <a href=\"/tags/nerv/\">nerv</a>\n</div>\n<div class=\"post-categories\" style=\"display:none\">\n <a href=\"/cates/Web开发/\">Web开发</a>\n</div>\n<div class=\"post-announce\">\n 感谢您的阅读,本文由 \n <a href=\"//aotu.io\">凹凸实验室</a> 版权所有。如若转载,请注明出处:凹凸实验室(\n <a href=\"https://aotu.io/notes/2018/03/22/nerv/\">https://aotu.io/notes/2018/03/22/nerv/</a>)\n</div>\n<div class=\"post-revision\">\n <i class=\"fa fa-clock-o\"></i> \n <time class=\"post-updated\" datetime=\"2018-11-07T01:01:15.657Z\">上次更新:2018-11-07 09:01:15</time>\n</div>');
INSERT INTO `rumo_blog` VALUES ('32', 'H5动画:轨迹移动', '1', 'H5动画:轨迹移动', '1', '2018-11-11 21:02:33', '2018-11-11 21:02:33', '1', '0', '//misc.aotu.io/booxood/path-animation/cover_900.png', '', '0', '0', '<a id=\"more\"></a>\n<blockquote>\n <p><a href=\"https://zh.wikipedia.org/wiki/%E5%8A%A8%E7%94%BB\" target=\"_blank\" rel=\"external\">动画</a>,是指由许多帧静止的画面,以一定的速度(如每秒16张)连续播放时,肉眼因视觉残象产生错觉,而误以为画面活动的作品。</p>\n</blockquote>\n<p>在 Web 开发中,经常需要实现各种动画效果,例如:移动、变形、透明度变化等,今天我们主要来讨论各种移动的实现。</p>\n<h4 id=\"直线移动\" class=\"post-heading\"><a href=\"#直线移动\" class=\"headerlink\" title=\"直线移动\"></a>直线移动<a class=\"post-anchor\" href=\"#直线移动\" aria-hidden=\"true\"></a></h4>\n<p><img src=\"//misc.aotu.io/booxood/path-animation/straight-line.png\" alt=\"straight-line\"></p>\n<p>通常可以直接由各个点的位置,以及到点的时间与整个动画持续时间的比值,写出类似下面的代码并可实现动画。</p>\n<figure class=\"highlight css\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"selector-class\">.cray</span> {\n </div>\n <div class=\"line\"> \n <span class=\"attribute\">animation</span>: move \n <span class=\"number\">2s</span> alternate infinite;\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n @\n <span class=\"keyword\">keyframes</span> move {\n </div>\n <div class=\"line\">\n 0% { \n <span class=\"attribute\">transform</span>: \n <span class=\"built_in\">translate</span>(0, 0); }\n </div>\n <div class=\"line\">\n 30% { \n <span class=\"attribute\">transform</span>: \n <span class=\"built_in\">translate</span>(100px, 0); }\n </div>\n <div class=\"line\">\n 60% { \n <span class=\"attribute\">transform</span>: \n <span class=\"built_in\">translate</span>(100px, 100px); }\n </div>\n <div class=\"line\">\n 100% { \n <span class=\"attribute\">transform</span>: \n <span class=\"built_in\">translate</span>(200px, 0); }\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h4 id=\"曲线移动\" class=\"post-heading\"><a href=\"#曲线移动\" class=\"headerlink\" title=\"曲线移动\"></a>曲线移动<a class=\"post-anchor\" href=\"#曲线移动\" aria-hidden=\"true\"></a></h4>\n<p><img src=\"//misc.aotu.io/booxood/path-animation/curve.png\" alt=\"curve\"></p>\n<p>在 CSS 中可以通过 <code>transform-origin</code> 配合 <code>rotate</code> 实现曲线移动,不过这种 <code>曲线</code> 都是圆的一部分且不太好控制。</p>\n<p>这种移动我们可以把它拆分成两个方向的运动叠加,如</p>\n<p><img src=\"//misc.aotu.io/booxood/path-animation/curve.gif\" alt=\"curve-gif\"></p>\n<p>更详细的说明可以参考这篇文章 <a href=\"http://tobiasahlin.com/blog/curved-path-animations-in-css/\" target=\"_blank\" rel=\"external\">《curved-path-animations-in-css》</a>。</p>\n<h4 id=\"路径移动\" class=\"post-heading\"><a href=\"#路径移动\" class=\"headerlink\" title=\"路径移动\"></a>路径移动<a class=\"post-anchor\" href=\"#路径移动\" aria-hidden=\"true\"></a></h4>\n<p><img src=\"//misc.aotu.io/booxood/path-animation/path.png\" alt=\"path\"></p>\n<p>这也是曲线移动,但是想像上面那样,这个很难拆分成几个方向的运动叠加。这样的移动路径可以尝试以下几个方法:</p>\n<ul>\n <li><strong>SVG Animation</strong></li>\n</ul>\n<p>这样的路径可以比较好的用 SVG path 来描述,然后使用 SVG Animation 做跟随动画,并可以达到预期的轨迹效果。</p>\n<p>主要代码(<a href=\"https://codepen.io/booxood/pen/MOjyVe\" target=\"_blank\" rel=\"external\">在线示例</a>):</p>\n<figure class=\"highlight html\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div>\n <div class=\"line\">\n 16\n </div>\n <div class=\"line\">\n 17\n </div>\n <div class=\"line\">\n 18\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"tag\">&lt;<span class=\"name\">svg</span> <span class=\"attr\">width</span>=<span class=\"string\">\"420px\"</span> <span class=\"attr\">height</span>=<span class=\"string\">\"260px\"</span> <span class=\"attr\">viewBox</span>=<span class=\"string\">\"0 0 420 260\"</span> <span class=\"attr\">version</span>=<span class=\"string\">\"1.1\"</span> <span class=\"attr\">xmlns</span>=<span class=\"string\">\"http://www.w3.org/2000/svg\"</span> <span class=\"attr\">xmlns:xlink</span>=<span class=\"string\">\"http://www.w3.org/1999/xlink\"</span>&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">g</span> <span class=\"attr\">stroke</span>=<span class=\"string\">\"#979797\"</span> <span class=\"attr\">stroke-width</span>=<span class=\"string\">\"1\"</span> <span class=\"attr\">fill</span>=<span class=\"string\">\"none\"</span>&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">path</span> <span class=\"attr\">id</span>=<span class=\"string\">\"motionPath\"</span> <span class=\"attr\">d</span>=<span class=\"string\">\"M370.378234,219.713623 C355.497359,218.517659 ...\"</span> &gt;</span>\n <span class=\"tag\">&lt;/<span class=\"name\">path</span>&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;/<span class=\"name\">g</span>&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">g</span> <span class=\"attr\">id</span>=<span class=\"string\">\"cray\"</span> <span class=\"attr\">transform</span>=<span class=\"string\">\"translate(0, -24)\"</span> <span class=\"attr\">stroke</span>=<span class=\"string\">\"#979797\"</span>&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">image</span> <span class=\"attr\">id</span>=<span class=\"string\">\"cray-img\"</span> <span class=\"attr\">xlink:href</span>=<span class=\"string\">\"http://7xt5iu.com1.z0.glb.clouddn.com/img/cray.png\"</span> <span class=\"attr\">x</span>=<span class=\"string\">\"0\"</span> <span class=\"attr\">y</span>=<span class=\"string\">\"0\"</span> <span class=\"attr\">width</span>=<span class=\"string\">\"100px\"</span>/&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;/<span class=\"name\">g</span>&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">animateMotion</span></span>\n </div>\n <div class=\"line\"> \n <span class=\"attr\">xlink:href</span>=\n <span class=\"string\">\"#cray\"</span>\n </div>\n <div class=\"line\"> \n <span class=\"attr\">dur</span>=\n <span class=\"string\">\"5s\"</span>\n </div>\n <div class=\"line\"> \n <span class=\"attr\">begin</span>=\n <span class=\"string\">\"0s\"</span>\n </div>\n <div class=\"line\"> \n <span class=\"attr\">fill</span>=\n <span class=\"string\">\"freeze\"</span>\n </div>\n <div class=\"line\"> \n <span class=\"attr\">repeatCount</span>=\n <span class=\"string\">\"indefinite\"</span>\n </div>\n <div class=\"line\"> \n <span class=\"attr\">rotate</span>=\n <span class=\"string\">\"auto-reverse\"</span>\n </div>\n <div class=\"line\">\n &gt;\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">mpath</span> <span class=\"attr\">xlink:href</span>=<span class=\"string\">\"#motionPath\"</span> /&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;/<span class=\"name\">animateMotion</span>&gt;</span>\n </div>\n <div class=\"line\">\n <span class=\"tag\">&lt;/<span class=\"name\">svg</span>&gt;</span>\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>效果:</p>\n<p><img src=\"//misc.aotu.io/booxood/path-animation/cray.gif\" alt=\"cray-gif\"></p>\n<ul>\n <li><strong>JavaScript</strong></li>\n</ul>\n<p>使用 JavaScript 可以直接操作元素进行运动,理论上可以实现任何动画,只是实现一些复杂的动画成本比较高,好在有各种已经开发好了的工具库可以供我们使用。例如,使用 Greensock 的 TweenMax 和 MorphSVGPlugin(收费),通过 MorphSVGPlugin 提供的 pathDataToBezier 方法将 SVG path 转成曲线数组,然后给 TweenMax 使用:</p>\n<figure class=\"highlight js\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"keyword\">var</span> hill = \n <span class=\"built_in\">document</span>.getElementById(\n <span class=\"string\">\'hill\'</span>)\n </div>\n <div class=\"line\">\n <span class=\"keyword\">var</span> path = MorphSVGPlugin.pathDataToBezier(\n <span class=\"string\">\"#motionPath\"</span>);\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n TweenMax.to(hill, \n <span class=\"number\">5</span>, {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">bezier</span>:{\n </div>\n <div class=\"line\"> \n <span class=\"attr\">values</span>:path,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">type</span>:\n <span class=\"string\">\"cubic\"</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">autoRotate</span>: \n <span class=\"number\">180</span>\n </div>\n <div class=\"line\">\n },\n </div>\n <div class=\"line\"> \n <span class=\"attr\">ease</span>:Linear.easeNone,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">repeat</span>: \n <span class=\"number\">-1</span>\n </div>\n <div class=\"line\">\n })\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p><a href=\"https://codepen.io/booxood/pen/BRJNyQ\" target=\"_blank\" rel=\"external\">在线示例</a></p>\n<ul>\n <li><strong>CSS</strong></li>\n</ul>\n<p>实现动画,其实就是在相应的时间点做相应的“变化”。再回头看直线移动的实现方式,其实如果能给出足够多点的位置和该点的时间与持续时间的比值,那其实曲线也可以直接用 CSS 来实现。</p>\n<p>很多时候设计师使用 AE 来设计动画,当我们拿到设计稿后,可以给动画增加关键帧,然后借助一些工具把关键帧的信息导出来,这里介绍一个 <a href=\"https://www.npmjs.com/package/keyframes-cli\" target=\"_blank\" rel=\"external\">keyframes-cli</a>,可以导出这样结构的数据</p>\n<p><img src=\"//misc.aotu.io/booxood/path-animation/ae.png\" alt=\"ae\"></p>\n<p>从属性名字可以判断出来 <code>X_POSITION</code> 和 <code>Y_POSITION</code> 是 <code>x</code> 和 <code>y</code> 的位置信息,而 <code>key_values</code> 里的 <code>data</code> 就是我们需要的<code>点位置</code>, <code>该点的时间与持续时间的比值</code> 可以根据 <code>start_frame</code> 得出,<br>写个脚本把这些数据处理下,可得到类似下面的 CSS 代码</p>\n<p><img src=\"//misc.aotu.io/booxood/path-animation/ae-css.jpeg\" alt=\"ae-css\"></p>\n<p>设置的关键帧越多,动画会越流畅,但 CSS 也会增多。</p>\n<blockquote>\n <p>注意:不是 AE 关键帧里所有的信息都可以导出来,还跟 AE 里使用的过渡属性有关,这里有<a href=\"https://github.com/facebookincubator/Keyframes/blob/master/docs/AfterEffectsGuideline.md\" target=\"_blank\" rel=\"external\">介绍</a>。</p>\n</blockquote>\n<p>最后,总结一下,移动动画就是用一种合适的方式把时间和位置的变化关系展示出来。除了上面方法,肯定还有很多其他的方法和帮助工具,欢迎留言交流讨论。</p>\n<p>如果对「H5游戏开发」感兴趣,欢迎关注我们的<a href=\"https://zhuanlan.zhihu.com/snsgame\" target=\"_blank\" rel=\"external\">专栏</a>。</p>\n<div class=\"post-tags\" style=\"display:none\">\n <a href=\"/tags/svg/\">svg</a> \n <a href=\"/tags/AE/\">AE</a> \n <a href=\"/tags/animation/\">animation</a> \n <a href=\"/tags/H5/\">H5</a> \n <a href=\"/tags/css/\">css</a> \n <a href=\"/tags/canvas/\">canvas</a>\n</div>\n<div class=\"post-categories\" style=\"display:none\">\n <a href=\"/cates/Web开发/\">Web开发</a>\n</div>\n<div class=\"post-announce\">\n 感谢您的阅读,本文由 \n <a href=\"//aotu.io\">凹凸实验室</a> 版权所有。如若转载,请注明出处:凹凸实验室(\n <a href=\"https://aotu.io/notes/2017/11/06/path-animation/\">https://aotu.io/notes/2017/11/06/path-animation/</a>)\n</div>\n<div class=\"post-revision\">\n <i class=\"fa fa-clock-o\"></i> \n <time class=\"post-updated\" datetime=\"2018-11-07T01:01:15.653Z\">上次更新:2018-11-07 09:01:15</time>\n</div>');
INSERT INTO `rumo_blog` VALUES ('33', '多端统一开发框架 - Taro', '1', '多端统一开发框架 - Taro', '1', '2018-11-11 21:02:33', '2018-11-11 21:02:33', '1', '0', '//img11.360buyimg.com/uba/jfs/t21205/91/853520716/145629/b03d7fa7/5b19f383N6a30536b.jpg', '', '0', '0', '<a id=\"more\"></a>\n<h1 id=\"Taro-多端开发框架\" class=\"post-heading\"><a href=\"#Taro-多端开发框架\" class=\"headerlink\" title=\"Taro - 多端开发框架\"></a>Taro - 多端开发框架<a class=\"post-anchor\" href=\"#Taro-多端开发框架\" aria-hidden=\"true\"></a></h1>\n<h2 id=\"Taro-是什么?\" class=\"post-heading\"><a href=\"#Taro-是什么?\" class=\"headerlink\" title=\"Taro 是什么?\"></a>Taro 是什么?<a class=\"post-anchor\" href=\"#Taro-是什么?\" aria-hidden=\"true\"></a></h2>\n<p>Taro 是由凹凸实验室打造的一套遵循 React 语法规范的多端统一开发框架。</p>\n<p>现如今市面上端的形态多种多样,Web、App 端(React Native)、微信小程序等各种端大行其道,当业务要求同时在不同的端都要求有所表现的时候,针对不同的端去编写多套代码的成本显然非常高,这时候只编写一套代码就能够适配到多端的能力就显得极为需要。</p>\n<p>使用 Taro,我们可以只书写一套代码,再通过 Taro 的编译工具,将源代码分别编译出可以在不同端(微信小程序、H5、App 端等)运行的代码。同时 Taro 还提供开箱即用的语法检测和自动补全等功能,有效地提升了开发体验和开发效率。</p>\n<h2 id=\"Taro-能提供什么?\" class=\"post-heading\"><a href=\"#Taro-能提供什么?\" class=\"headerlink\" title=\"Taro 能提供什么?\"></a>Taro 能提供什么?<a class=\"post-anchor\" href=\"#Taro-能提供什么?\" aria-hidden=\"true\"></a></h2>\n<h3 id=\"一次编写,多端运行\" class=\"post-heading\"><a href=\"#一次编写,多端运行\" class=\"headerlink\" title=\"一次编写,多端运行\"></a>一次编写,多端运行<a class=\"post-anchor\" href=\"#一次编写,多端运行\" aria-hidden=\"true\"></a></h3>\n<p>既然是一个多端解决方案,Taro 最重要的能力当然是写一套代码输出多端皆可运行的代码。目前 Taro 已经支持一套代码同时生成 H5 和小程序,App端(React Native)端也即将支持,同时诸如快应用等端也将得到支持。</p>\n<p>同时 Taro 也已经投入到了生产环境使用,目前已经支撑了一个 3 万行代码小程序 <a href=\"https://www.toplife.com/\" target=\"_blank\" rel=\"external\">TOPLIFE</a> 的开发并上线。京东购物 小程序和 一起有局 小程序也在使用 Taro 部分重构中,即将上线。未来也将接入更多业务。</p>\n<p><img src=\"http://img14.360buyimg.com/uba/jfs/t21817/73/625556299/346228/96240192/5b14a81eN8e6a43db.png\" alt=\"qrcode.png\"></p>\n<h3 id=\"现代前端开发流程\" class=\"post-heading\"><a href=\"#现代前端开发流程\" class=\"headerlink\" title=\"现代前端开发流程\"></a>现代前端开发流程<a class=\"post-anchor\" href=\"#现代前端开发流程\" aria-hidden=\"true\"></a></h3>\n<p>和微信自带的小程序框架不一样,Taro 积极拥抱社区现有的现代开发流程,包括但不限于:</p>\n<ul>\n <li>NPM 包管理系统</li>\n <li>ES6+ 语法</li>\n <li>自由的资源引用</li>\n <li>CSS 预处理器和后处理器(SCSS、Less、PostCSS)</li>\n</ul>\n<p>对于微信小程序的编译流程,我们从 <a href=\"https://parceljs.org/\" target=\"_blank\" rel=\"external\">Parcel</a> 得到灵感,自研了一套打包机制将 AST 不断传递,因此代码分析的速度得到了很大的提高。一台 2015 年 的 15寸 RMBP 在编译上百个组件时仅需要大约 15 秒左右。</p>\n<h3 id=\"和-React-完全一致的-API-和组件化系统\" class=\"post-heading\"><a href=\"#和-React-完全一致的-API-和组件化系统\" class=\"headerlink\" title=\"和 React 完全一致的 API 和组件化系统\"></a>和 React 完全一致的 API 和组件化系统<a class=\"post-anchor\" href=\"#和-React-完全一致的-API-和组件化系统\" aria-hidden=\"true\"></a></h3>\n<p>在 Taro 中,你不用像小程序一样区分什么是 <code>App</code> 组件,什么是 <code>Page</code> 组件,什么是 <code>Component</code> 组件,Taro 全都是 <code>Component</code> 组件,并且和 React 的生命周期完全一致。可以说,一旦你掌握了 React,那就几乎掌握了 Taro。而学习 React 的资源也几乎是汗牛充栋,完全不用担心学不会。</p>\n<p>Taro 和 React 一样,同样使用声明式的 JSX 语法。相比起字符串的模板语法,JSX 在处理精细复杂需求的时候会更得心应手。</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div>\n <div class=\"line\">\n 16\n </div>\n <div class=\"line\">\n 17\n </div>\n <div class=\"line\">\n 18\n </div>\n <div class=\"line\">\n 19\n </div>\n <div class=\"line\">\n 20\n </div>\n <div class=\"line\">\n 21\n </div>\n <div class=\"line\">\n 22\n </div>\n <div class=\"line\">\n 23\n </div>\n <div class=\"line\">\n 24\n </div>\n <div class=\"line\">\n 25\n </div>\n <div class=\"line\">\n 26\n </div>\n <div class=\"line\">\n 27\n </div>\n <div class=\"line\">\n 28\n </div>\n <div class=\"line\">\n 29\n </div>\n <div class=\"line\">\n 30\n </div>\n <div class=\"line\">\n 31\n </div>\n <div class=\"line\">\n 32\n </div>\n <div class=\"line\">\n 33\n </div>\n <div class=\"line\">\n 34\n </div>\n <div class=\"line\">\n 35\n </div>\n <div class=\"line\">\n 36\n </div>\n <div class=\"line\">\n 37\n </div>\n <div class=\"line\">\n 38\n </div>\n <div class=\"line\">\n 39\n </div>\n <div class=\"line\">\n 40\n </div>\n <div class=\"line\">\n 41\n </div>\n <div class=\"line\">\n 42\n </div>\n <div class=\"line\">\n 43\n </div>\n <div class=\"line\">\n 44\n </div>\n <div class=\"line\">\n 45\n </div>\n <div class=\"line\">\n 46\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">// 一个典型的 Taro 组件</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">import</span> Taro, { Component } \n <span class=\"keyword\">from</span> \n <span class=\"string\">\'@tarojs/taro\'</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">import</span> { View, Button } \n <span class=\"keyword\">from</span> \n <span class=\"string\">\'@tarojs/components\'</span>\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"keyword\">export</span> \n <span class=\"keyword\">default</span> \n <span class=\"class\"><span class=\"keyword\">class</span> <span class=\"title\">Home</span> <span class=\"keyword\">extends</span> <span class=\"title\">Component</span> </span>{\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">constructor</span> (props) {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">super</span>(props)\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">this</span>.state = {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">title</span>: \n <span class=\"string\">\'首页\'</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">list</span>: [\n <span class=\"number\">1</span>, \n <span class=\"number\">2</span>, \n <span class=\"number\">3</span>]\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"> \n </div>\n <div class=\"line\">\n componentWillMount () {}\n </div>\n <div class=\"line\"> \n </div>\n <div class=\"line\">\n componentDidMount () {}\n </div>\n <div class=\"line\"> \n </div>\n <div class=\"line\">\n componentWillUpdate (nextProps, nextState) {}\n </div>\n <div class=\"line\"> \n </div>\n <div class=\"line\">\n componentDidUpdate (prevProps, prevState) {}\n </div>\n <div class=\"line\"> \n </div>\n <div class=\"line\">\n shouldComponentUpdate (nextProps, nextState) {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">return</span> \n <span class=\"literal\">true</span>\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n add = \n <span class=\"function\">(<span class=\"params\">e</span>) =&gt;</span> {\n </div>\n <div class=\"line\"> \n <span class=\"comment\">// dosth</span>\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n render () {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">const</span> { list, title } = \n <span class=\"keyword\">this</span>.state\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">return</span> (\n </div>\n <div class=\"line\"> \n <span class=\"xml\"><span class=\"tag\">&lt;<span class=\"name\">View</span> <span class=\"attr\">className</span>=<span class=\"string\">\'index\'</span>&gt;</span></span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">View</span> <span class=\"attr\">className</span>=<span class=\"string\">\'title\'</span>&gt;</span>{title}\n <span class=\"tag\">&lt;/<span class=\"name\">View</span>&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">View</span> <span class=\"attr\">className</span>=<span class=\"string\">\'content\'</span>&gt;</span>\n </div>\n <div class=\"line\">\n {list.map(item =&gt; {\n </div>\n <div class=\"line\">\n return (\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">View</span> <span class=\"attr\">className</span>=<span class=\"string\">\'item\'</span>&gt;</span>{item}\n <span class=\"tag\">&lt;/<span class=\"name\">View</span>&gt;</span>\n </div>\n <div class=\"line\">\n )\n </div>\n <div class=\"line\">\n })}\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">Button</span> <span class=\"attr\">className</span>=<span class=\"string\">\'add\'</span> <span class=\"attr\">onClick</span>=<span class=\"string\">{this.add}</span>&gt;</span>添加\n <span class=\"tag\">&lt;/<span class=\"name\">Button</span>&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;/<span class=\"name\">View</span>&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;/<span class=\"name\">View</span>&gt;</span>\n </div>\n <div class=\"line\">\n )\n </div>\n <div class=\"line\">\n }\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<h3 id=\"良好的开发效率和体验\" class=\"post-heading\"><a href=\"#良好的开发效率和体验\" class=\"headerlink\" title=\"良好的开发效率和体验\"></a>良好的开发效率和体验<a class=\"post-anchor\" href=\"#良好的开发效率和体验\" aria-hidden=\"true\"></a></h3>\n<p>鉴于 Taro 的语法和 React 完全一样,因此编辑器/IDE 能够对 Taro 的支持和 React 是几乎一样的。现代的编辑器默认都对 JSX 进行了支持,如果没有,找一个插件也是非常容易的事情。但毕竟我们做 Taro 就是为了提升开发效率和开发体验,而真正使用 Taro 的人就是我们自己或正坐在我们旁边的同事。因此在此基础上,我们又对 Taro 开发体验进行了进一步加强。</p>\n<h4 id=\"自定义-ESLint-规则\" class=\"post-heading\"><a href=\"#自定义-ESLint-规则\" class=\"headerlink\" title=\"自定义 ESLint 规则\"></a>自定义 ESLint 规则<a class=\"post-anchor\" href=\"#自定义-ESLint-规则\" aria-hidden=\"true\"></a></h4>\n<p>我们之前提到过,当学会了 React,其实也差不多会 Taro 了。其中很重要的一个原因就是我们对 Taro 不支持的语法和特性单独写了 ESLint 规则:开发者只管写代码,写到不支持的语法/特性编辑器会报错,并给出报错信息和一个文档地址描述。</p>\n<p><img src=\"http://img11.360buyimg.com/uba/jfs/t22168/93/735088023/931233/8689f95e/5b176e0cN26618692.gif\" alt=\"Taro eslint\"></p>\n<h4 id=\"类型安全和运行时检测\" class=\"post-heading\"><a href=\"#类型安全和运行时检测\" class=\"headerlink\" title=\"类型安全和运行时检测\"></a>类型安全和运行时检测<a class=\"post-anchor\" href=\"#类型安全和运行时检测\" aria-hidden=\"true\"></a></h4>\n<p>JSX 的本质就是 JavaScript 的语法增强,所以例如没有 <code>import</code> 组件等语法错误在编译期就能发现。开发者也可以使用 <code>TypeScript</code> 或 <code>Flow</code> 来对代码的可靠性进一步增强,或使用 <code>PropsType</code> 在运行时进一步保障代码的鲁棒性。</p>\n<p><img src=\"http://img12.360buyimg.com/uba/jfs/t20401/15/759531786/1118417/1b827131/5b176e0cN7b17f4ec.gif\" alt=\"typings.gif\"></p>\n<h4 id=\"高效的自动补全和-ES6-语法\" class=\"post-heading\"><a href=\"#高效的自动补全和-ES6-语法\" class=\"headerlink\" title=\"高效的自动补全和 ES6+ 语法\"></a>高效的自动补全和 ES6+ 语法<a class=\"post-anchor\" href=\"#高效的自动补全和-ES6-语法\" aria-hidden=\"true\"></a></h4>\n<p>Taro 的所有 API(包括微信小程序等端能力接口)都有智能的提醒和自动补全,包括接口的参数和返回值。</p>\n<p><img src=\"http://img20.360buyimg.com/uba/jfs/t20455/36/730770188/1715215/573ff07e/5b176e15Nd65937f7.gif\" alt=\"Taro 自动补全\"></p>\n<h2 id=\"Taro-的设计思路\" class=\"post-heading\"><a href=\"#Taro-的设计思路\" class=\"headerlink\" title=\"Taro 的设计思路\"></a>Taro 的设计思路<a class=\"post-anchor\" href=\"#Taro-的设计思路\" aria-hidden=\"true\"></a></h2>\n<p>我们的初心就是做一款能够适配多端的解决方案,结合业务场景、技术选型和前端历史发展进程,我们的解决方案必须满足下述要求:</p>\n<ul>\n <li>代码多端复用,不仅能运行在时下最热门的 H5、微信小程序、React Native,对其他可能会流行的端也留有余地和可能性。</li>\n <li>完善和强大的组件化机制,这是开发复杂应用的基石。</li>\n <li>与目前团队技术栈有机结合,有效提高效率。</li>\n <li>学习成本足够低</li>\n <li>背后的生态强大</li>\n</ul>\n<p>同时满足这几个需求并不容易,在我们经过充分地调研和思考之后发现只有 React 体系能够满足我们的需求。而对于微信小程序而言,使用 React 完全没有办法进行开发——直到我们从 <a href=\"https://github.com/facebook/codemod\" target=\"_blank\" rel=\"external\">codemod</a> 得到灵感:</p>\n<blockquote>\n <p>在一个优秀且严格的规范限制下,从更高抽象的视角(语法树)来看,每个人写的代码都差不多。</p>\n</blockquote>\n<p>也就是说,对于微信小程序这样不开放不开源的端,我们可以先把 React 代码分析成一颗抽象语法树,根据这颗树生成小程序支持的模板代码,再做一个小程序运行时框架处理事件和生命周期与小程序框架兼容,然后把业务代码跑在运行时框架就完成了小程序端的适配。</p>\n<p>对于 React 已经支持的端,例如 Web、React Native 甚至未来的 React VR,我们只要包一层组件库再做些许样式支持即可。鉴于时下小程序的热度和我们团队本身的业务侧重程度,组件库的 API 是以小程序为标准,其他端的组件库的 API 都会和小程序端的组件保持一致。</p>\n<p><img src=\"http://img30.360buyimg.com/uba/jfs/t22360/120/839096197/151922/229ceba4/5b1a6fcdNed7d4039.jpg\" alt=\"Taro原理\"></p>\n<h2 id=\"技术选型与权衡\" class=\"post-heading\"><a href=\"#技术选型与权衡\" class=\"headerlink\" title=\"技术选型与权衡\"></a>技术选型与权衡<a class=\"post-anchor\" href=\"#技术选型与权衡\" aria-hidden=\"true\"></a></h2>\n<p>在我们前面社区已经有多个优秀的框架以小程序为核心对多端适配进行了探索,我们将各个开发框架的主要特点和特性进行了对比并制成图表。大家可以结合团队技术栈、技术需求以及框架特点、特性进行选型和权衡。</p>\n<p><img src=\"http://img13.360buyimg.com/uba/jfs/t22033/293/837111837/251330/3f89b58d/5b18f6edNa2311261.png\" alt=\"Taro 对比\"></p>\n<h2 id=\"结语\" class=\"post-heading\"><a href=\"#结语\" class=\"headerlink\" title=\"结语\"></a>结语<a class=\"post-anchor\" href=\"#结语\" aria-hidden=\"true\"></a></h2>\n<p>经过数个月的开发,Taro 从第一次 commit 到发展成包括 16 个包,十多位同学共同参与的大型项目。与此同时,Taro 也在生产环境支撑了数个复杂业务线上项目的开发,将来也会支撑更多业务。</p>\n<p>Taro 的技术方案和实现也根植于社区,我们也希望为技术社区的发展壮大贡献一份自己的力量。秉持着凹凸实验室长久以来开源、开放、共享的优良传统,我们今天将 Taro 全部代码开源,为广大开发者快速开发多端项目提供一整套技术解决方案。未来,我们也将继续拓展 Taro 现有能力,支持更多端能力,继续完善开发者体验,提高开发者效率,帮助更多开发者,同时也从社区中汲取养分,让 Taro 变得更加强大。</p>\n<p>官网:<a href=\"http://taro.aotu.io/\" target=\"_blank\" rel=\"external\">http://taro.aotu.io/</a></p>\n<p>GitHub: <a href=\"http://github.com/nervjs/taro\" target=\"_blank\" rel=\"external\">http://github.com/nervjs/taro</a></p>\n<p>如果你还没听过 Nerv,可以来这里看看:<a href=\"https://nerv.aotu.io/\" target=\"_blank\" rel=\"external\">https://nerv.aotu.io/</a></p>\n<div class=\"post-tags\" style=\"display:none\">\n <a href=\"/tags/React/\">React</a> \n <a href=\"/tags/小程序/\">小程序</a> \n <a href=\"/tags/Nerv/\">Nerv</a>\n</div>\n<div class=\"post-categories\" style=\"display:none\">\n <a href=\"/cates/小程序/\">小程序</a>\n</div>\n<div class=\"post-announce\">\n 感谢您的阅读,本文由 \n <a href=\"//aotu.io\">凹凸实验室</a> 版权所有。如若转载,请注明出处:凹凸实验室(\n <a href=\"https://aotu.io/notes/2018/06/07/Taro/\">https://aotu.io/notes/2018/06/07/Taro/</a>)\n</div>\n<div class=\"post-revision\">\n <i class=\"fa fa-clock-o\"></i> \n <time class=\"post-updated\" datetime=\"2018-11-07T01:01:15.657Z\">上次更新:2018-11-07 09:01:15</time>\n</div>');
INSERT INTO `rumo_blog` VALUES ('34', 'Taro 1.1 发布,全面支持微信/百度/支付宝 小程序', '1', 'Taro 1.1 发布,全面支持微信/百度/支付宝 小程序', '1', '2018-11-11 21:02:34', '2018-11-11 21:02:34', '1', '0', '//img30.360buyimg.com/uba/jfs/t1/4402/1/15681/113064/5be00611E6b534d26/41151b39b0ca48bc.jpg', '', '0', '0', '<a id=\"more\"></a>\n<p>在 9 月 16 日的掘金小程序大会上,Taro 正式发布了 1.0 版本。</p>\n<p>作为一个多端统一开发框架,Taro 1.0 版本为广大开发者带来了许多激动人心的特性,帮助开发者们更方便快捷地开发多端应用。</p>\n<p>前进的脚步没有停下,历时一个多月,Taro 1.1 版本正式来袭!</p>\n<p>从 1.0 到 1.1,期间产生了 <strong>500</strong> 余次 Commit,发布了 <strong>23</strong> 个<a href=\"https://github.com/NervJS/taro/blob/master/CHANGELOG.md\" target=\"_blank\" rel=\"external\">版本</a>,Taro 一直保持高速的迭代频率,只为不断打磨,为它注入更多优秀的特性,让更多开发者受益。</p>\n<h2 id=\"Taro-1-1-正式来袭\" class=\"post-heading\"><a href=\"#Taro-1-1-正式来袭\" class=\"headerlink\" title=\"Taro 1.1 正式来袭\"></a>Taro 1.1 正式来袭<a class=\"post-anchor\" href=\"#Taro-1-1-正式来袭\" aria-hidden=\"true\"></a></h2>\n<h3 id=\"更丰富的平台转换支持\" class=\"post-heading\"><a href=\"#更丰富的平台转换支持\" class=\"headerlink\" title=\"更丰富的平台转换支持\"></a>更丰富的平台转换支持<a class=\"post-anchor\" href=\"#更丰富的平台转换支持\" aria-hidden=\"true\"></a></h3>\n<p>自从<strong>微信小程序</strong>诞生以来,小程序的优势大家有目共睹,各大互联网厂商开始摩拳擦掌,纷纷布局小程序领域,其中势头最劲的当属<strong>百度智能小程序</strong>和<strong>支付宝小程序</strong>。对于前端而言,既是机遇,因为平台愈多,带来的机会愈多;也是挑战,因为不同的平台总会各有差异,为每个平台维护一份代码,成本显然太高。</p>\n<p>为了降低多端应用的开发成本,在已全面支持 <strong>微信小程序</strong> 的基础上,Taro 在 1.1 版本中加入了对 <strong>百度智能小程序</strong> 和 <strong>支付宝小程序</strong> 的支持。</p>\n<h4 id=\"支持百度智能小程序\" class=\"post-heading\"><a href=\"#支持百度智能小程序\" class=\"headerlink\" title=\"支持百度智能小程序\"></a>支持百度智能小程序<a class=\"post-anchor\" href=\"#支持百度智能小程序\" aria-hidden=\"true\"></a></h4>\n<p>升级到 1.1 版本后,即可获得将现有项目转换成百度智能小程序的能力。</p>\n<p>使用如下命令进行百度智能小程序端编译预览及打包:</p>\n<figure class=\"highlight bash\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\"># npm script</span>\n </div>\n <div class=\"line\">\n $ npm run dev:swan\n </div>\n <div class=\"line\">\n $ npm run build:swan\n </div>\n <div class=\"line\">\n <span class=\"comment\"># 仅限全局安装</span>\n </div>\n <div class=\"line\">\n $ taro build --\n <span class=\"built_in\">type</span> swan --watch\n </div>\n <div class=\"line\">\n $ taro build --\n <span class=\"built_in\">type</span> swan\n </div>\n <div class=\"line\">\n <span class=\"comment\"># npx 用户也可以使用</span>\n </div>\n <div class=\"line\">\n $ npx taro build --\n <span class=\"built_in\">type</span> swan --watch\n </div>\n <div class=\"line\">\n $ npx taro build --\n <span class=\"built_in\">type</span> swan\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>选择百度智能小程序模式,需要你下载并打开<a href=\"https://smartprogram.baidu.com/docs/develop/devtools/show_sur/\" target=\"_blank\" rel=\"external\">百度开发者工具</a>,然后在项目编译完后选择项目根目录下 <code>dist</code> 目录进行预览。</p>\n<p>目前已有使用 Taro 开发的首款百度智能小程序 <strong>“京东好物街”</strong> 正式上线,可以使用百度 APP,扫描以下二维码进行体验:</p>\n<p><img src=\"//img13.360buyimg.com/ling/jfs/t1/10931/36/2138/22454/5be02d3fE815a306d/0fc91ade32e59948.jpg\" alt=\"\"></p>\n<h4 id=\"支持支付宝小程序\" class=\"post-heading\"><a href=\"#支持支付宝小程序\" class=\"headerlink\" title=\"支持支付宝小程序\"></a>支持支付宝小程序<a class=\"post-anchor\" href=\"#支持支付宝小程序\" aria-hidden=\"true\"></a></h4>\n<p>支付宝小程序的转换方式与百度智能小程序一致。</p>\n<p>你可以使用如下命令进行百度智能小程序端编译预览及打包:</p>\n<figure class=\"highlight bash\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\"># npm script</span>\n </div>\n <div class=\"line\">\n $ npm run dev:alipay\n </div>\n <div class=\"line\">\n $ npm run build:alipay\n </div>\n <div class=\"line\">\n <span class=\"comment\"># 仅限全局安装</span>\n </div>\n <div class=\"line\">\n $ taro build --\n <span class=\"built_in\">type</span> alipay --watch\n </div>\n <div class=\"line\">\n $ taro build --\n <span class=\"built_in\">type</span> alipay\n </div>\n <div class=\"line\">\n <span class=\"comment\"># npx 用户也可以使用</span>\n </div>\n <div class=\"line\">\n $ npx taro build --\n <span class=\"built_in\">type</span> alipay --watch\n </div>\n <div class=\"line\">\n $ npx taro build --\n <span class=\"built_in\">type</span> alipay\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>选择支付宝小程序模式,需要你下载并打开<a href=\"https://docs.alipay.com/mini/developer/getting-started/\" target=\"_blank\" rel=\"external\">支付宝小程序开发者工具</a>,然后在项目编译完后选择项目根目录下 <code>dist</code> 目录进行预览。</p>\n<h4 id=\"新增平台标识\" class=\"post-heading\"><a href=\"#新增平台标识\" class=\"headerlink\" title=\"新增平台标识\"></a>新增平台标识<a class=\"post-anchor\" href=\"#新增平台标识\" aria-hidden=\"true\"></a></h4>\n<p>在加入 <strong>百度智能小程序</strong> 和 <strong>支付宝小程序</strong> 支持后,目前 Taro 已经提供了对 <strong>5</strong> 个平台的支持,分别是 微信/百度/支付宝/React Native/H5,为了方便开发者书写平台差异化代码,Taro 为每个平台提供了平台标识,并可以通过 <code>process.env.TARO_ENV</code> 来获得,其取值分别如下</p>\n<ul>\n <li><code>weapp</code>,代表微信小程序</li>\n <li><code>h5</code>,代表 H5</li>\n <li><code>rn</code>,代表 React Native</li>\n <li><code>swan</code>,代表百度智能小程序,<strong>1.1 版本新增</strong></li>\n <li><code>alipay</code>,代表支付宝小程序,<strong>1.1 版本新增</strong></li>\n</ul>\n<h3 id=\"开放多端-UI-库打包能力\" class=\"post-heading\"><a href=\"#开放多端-UI-库打包能力\" class=\"headerlink\" title=\"开放多端 UI 库打包能力\"></a>开放多端 UI 库打包能力<a class=\"post-anchor\" href=\"#开放多端-UI-库打包能力\" aria-hidden=\"true\"></a></h3>\n<p>在 1.0 版本中,Taro 的 <code>build</code> 命令新增了 <code>--ui</code> 参数,来将按照一定规则组织的代码打包成可以在 Taro 中使用的多端 UI 库。 并且,基于这一功能,Taro 推出了首个可以跨多端使用的多端 UI 库 <a href=\"https://taro-ui.aotu.io/#/\" target=\"_blank\" rel=\"external\">Taro UI</a>。</p>\n<p>为了完善 Taro 生态,扩展 Taro 社区,在 1.1 版本中开放了多端 UI 库的打包能力,并提供了详细的<a href=\"https://nervjs.github.io/taro/docs/ui-lib.html\" target=\"_blank\" rel=\"external\">开发指南</a>。</p>\n<p>同时 Taro 也提供了官方的多端 UI 库<a href=\"https://github.com/NervJS/taro-ui-sample\" target=\"_blank\" rel=\"external\">示例</a>,这是一个完整规范的 UI 库开发例子,包含必要的项目改造以及测试套件,基于这个示例,开发者可以快速地开发一个多端 UI 库项目。</p>\n<h3 id=\"还有更多\" class=\"post-heading\"><a href=\"#还有更多\" class=\"headerlink\" title=\"还有更多\"></a>还有更多<a class=\"post-anchor\" href=\"#还有更多\" aria-hidden=\"true\"></a></h3>\n<p>在 1.0 版本中,Taro 提供了对 <strong>微信小程序</strong>、<strong>H5</strong> 以及 <strong>React Native</strong> 的支持,而 1.1 版本,在原有支持的基础之上,对每一个平台进行不断完善,让其更加丰富完整。</p>\n<h4 id=\"微信小程序\" class=\"post-heading\"><a href=\"#微信小程序\" class=\"headerlink\" title=\"微信小程序\"></a>微信小程序<a class=\"post-anchor\" href=\"#微信小程序\" aria-hidden=\"true\"></a></h4>\n<ul>\n <li><code>F</code> 调用 <code>this.setState</code> 时,JSON Diff 算法 Bug 修复</li>\n <li><code>F</code> 修复 <code>this.setState</code> 回调里再调用 <code>this.setState</code> 的 Bug</li>\n <li><code>A</code> 新增 <code>componentWillPreload</code> 生命周期,用于数据预加载</li>\n <li><code>A</code> 支持在同一作用域对 JSX 赋值</li>\n <li><code>F</code> 多层 Map 循环嵌套包含条件判断时问题修复</li>\n <li><code>F</code> 中文字符被编译成 unicode 码问题修复</li>\n <li><code>F</code> 三元表达式解析问题修复</li>\n <li><code>A</code> 提升字符串模板的性能</li>\n</ul>\n<h4 id=\"H5\" class=\"post-heading\"><a href=\"#H5\" class=\"headerlink\" title=\"H5\"></a>H5<a class=\"post-anchor\" href=\"#H5\" aria-hidden=\"true\"></a></h4>\n<ul>\n <li><code>A</code> H5 支持分包配置</li>\n <li><code>F</code> 修复 H5 后台页面依然执行生命周期的问题</li>\n <li><code>F</code> 修复 PUT 、DELETE 等请求 body 为对象时无法发送 body 的问题</li>\n <li><code>A</code> 增加 <code>setTabBarStyle</code> 与 <code>setTabBarItem</code> API</li>\n <li><code>A</code> 增加 <code>arrayBufferToBase64</code> 与 <code>base64ToArrayBuffer</code> API</li>\n <li><code>A</code> 编译打包支持将公共 npm 包抽离成 lib 库</li>\n</ul>\n<h4 id=\"React-Native\" class=\"post-heading\"><a href=\"#React-Native\" class=\"headerlink\" title=\"React Native\"></a>React Native<a class=\"post-anchor\" href=\"#React-Native\" aria-hidden=\"true\"></a></h4>\n<ul>\n <li><code>A</code> 支持 TypeScript</li>\n <li><code>F</code> 修复项目初始化后 less 编译报错</li>\n <li><code>A</code> 添加 config.window.navigationStyle 配置</li>\n <li><code>A</code> 添加 <code>showNavigationBarLoading</code> 与 <code>hideNavigationBarLoading</code> API</li>\n <li><code>A</code> 增加 <code>arrayBufferToBase64</code> 与 <code>base64ToArrayBuffer</code> API</li>\n <li><code>F</code> 修复样式支持警告</li>\n <li><code>A</code> watch 模式时代码按需编译</li>\n <li><code>F</code> 修复同一文件夹下多个JS文件样式引用错误</li>\n <li><code>F</code> app.json 的 expo 配置改为可覆盖</li>\n <li><code>A</code> 支持 <code>Taro.pxTransform</code></li>\n <li><code>F</code> iconPath 和 selectedPath 的为同一个路径导致的重复引用的报错</li>\n <li><code>A</code> 在 config 中添加 rn 的 expo 配置</li>\n <li><code>A</code> 支持 deviceRatio 自定义</li>\n</ul>\n<p>更完整的功能迭代记录请参阅 <a href=\"https://github.com/NervJS/taro/blob/master/CHANGELOG.md\" target=\"_blank\" rel=\"external\">CHANGELOG</a></p>\n<h2 id=\"未来规划\" class=\"post-heading\"><a href=\"#未来规划\" class=\"headerlink\" title=\"未来规划\"></a>未来规划<a class=\"post-anchor\" href=\"#未来规划\" aria-hidden=\"true\"></a></h2>\n<p>Taro 将会继续保持高效迭代,以满足日益增长的多端开发需求</p>\n<p>目前已经确认的开发计划可以参见 <a href=\"https://github.com/NervJS/taro/blob/master/PLANS.md\" target=\"_blank\" rel=\"external\">Taro 版本开发计划</a>。</p>\n<h3 id=\"小程序转-Taro-代码开启内测\" class=\"post-heading\"><a href=\"#小程序转-Taro-代码开启内测\" class=\"headerlink\" title=\"小程序转 Taro 代码开启内测\"></a>小程序转 Taro 代码开启内测<a class=\"post-anchor\" href=\"#小程序转-Taro-代码开启内测\" aria-hidden=\"true\"></a></h3>\n<p>值得注意的是,激动人心的小程序转 Taro 代码功能已经开发完成,进入内测阶段,可以通过如下命令安装到 <code>canary</code> 版本,进行体验</p>\n<figure class=\"highlight bash\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n $ npm i -g @tarojs/cli@canary\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>欢迎针对这一功能,提供宝贵意见,意见入口<a href=\"https://github.com/NervJS/taro/issues/955\" target=\"_blank\" rel=\"external\">请戳</a> 👈👈👈</p>\n<p>同时,为了帮助 Taro 更好地成长,让 Taro 更好地回馈所有开发者,欢迎您给 Taro 提供<a href=\"https://github.com/NervJS/taro/issues\" target=\"_blank\" rel=\"external\">优秀想法</a>,或者直接贡献<a href=\"https://github.com/NervJS/taro/pulls\" target=\"_blank\" rel=\"external\">代码</a>。</p>\n<div class=\"post-tags\" style=\"display:none\">\n <a href=\"/tags/React/\">React</a> \n <a href=\"/tags/小程序/\">小程序</a> \n <a href=\"/tags/Nerv/\">Nerv</a> \n <a href=\"/tags/Taro/\">Taro</a>\n</div>\n<div class=\"post-categories\" style=\"display:none\">\n <a href=\"/cates/小程序/\">小程序</a>\n</div>\n<div class=\"post-announce\">\n 感谢您的阅读,本文由 \n <a href=\"//aotu.io\">凹凸实验室</a> 版权所有。如若转载,请注明出处:凹凸实验室(\n <a href=\"https://aotu.io/notes/2018/11/05/taro-1-1/\">https://aotu.io/notes/2018/11/05/taro-1-1/</a>)\n</div>\n<div class=\"post-revision\">\n <i class=\"fa fa-clock-o\"></i> \n <time class=\"post-updated\" datetime=\"2018-11-07T01:01:15.657Z\">上次更新:2018-11-07 09:01:15</time>\n</div>');
INSERT INTO `rumo_blog` VALUES ('35', '为何我们要用 React 来写小程序 - Taro 诞生记', '1', '为何我们要用 React 来写小程序 - Taro 诞生记', '1', '2018-11-11 21:02:34', '2018-11-11 21:02:34', '1', '0', '//img20.360buyimg.com/uba/jfs/t22954/193/251445690/357583/7232c10b/5b2a15e2N23cf1e55.jpg', '', '0', '0', '<a id=\"more\"></a>\n<blockquote>\n <p>在互联网不断发展的今天,前端程序员们也不断面临着新的挑战,在这个变化多端、不断革新自己的领域,每一年都有新的美好事物在发生。从去年微信小程序的诞生,到今年的逐渐火热,以及异军突起的轻应用、百度小程序等的出现,前端可以延伸的领域已经越来越广,当然也意味着业务在不断扩大。这时候,如何通过技术手段来提升开发效率,应对不断增长的业务,就是一个值得探索的话题。本文将对 Taro 诞生的故事,进行深入浅出地介绍,记录下这个忙碌的春夏之交发生的故事。</p>\n</blockquote>\n<h2 id=\"让人又爱又恨的微信小程序\" class=\"post-heading\"><a href=\"#让人又爱又恨的微信小程序\" class=\"headerlink\" title=\"让人又爱又恨的微信小程序\"></a>让人又爱又恨的微信小程序<a class=\"post-anchor\" href=\"#让人又爱又恨的微信小程序\" aria-hidden=\"true\"></a></h2>\n<p>自 <code>2017-1-9</code> 微信小程序(以下简称小程序)诞生以来,就伴随着赞誉与争议不断。从发布上线时的不被大多数人看好,到如今的逐渐火热,甚至说是如日中天也不为过,小程序用时间与实践证明了自己的价值。同时于开发者来说,小程序的生态不断在完善,许多的坑已被踩平,虽然还是存在一些令人诟病的问题,但已经足见微信的诚意了。这个时候要是还没有上手把玩过小程序,就显得非常OUT了。</p>\n<p>小程序对于前端程序员来说应该算得上是福音了,用前端相关的技术,获得丝般顺滑的 <code>Native</code> 体验,前端们又可以在产品小姐姐面前硬气一把了。可以说小程序给前端程序员打开了一扇新的大门,大家都应该感谢微信,但是从开发的角度来说,小程序的开发体验就非常值得商榷了,不仅语法上显得有些不伦不类,而且有些莫名其妙的坑也经常让人不经意间感叹一下和谐社会,从市面上层出不穷的小程序开发框架就可见一斑。以下就盘点部分小程序开发的痛点。</p>\n<h3 id=\"代码组织与语法\" class=\"post-heading\"><a href=\"#代码组织与语法\" class=\"headerlink\" title=\"代码组织与语法\"></a>代码组织与语法<a class=\"post-anchor\" href=\"#代码组织与语法\" aria-hidden=\"true\"></a></h3>\n<p>在小程序中,一个页面 <code>page</code> 可能拥有 <code>page.js</code>、<code>page.wxss</code>、<code>page.wxml</code> 、<code>page.json</code> 四个文件</p>\n<p><img src=\"http://img13.360buyimg.com/img/jfs/t24499/361/448655041/15273/6552407d/5b307974Nfaccdf2d.jpg\" alt=\"\"></p>\n<p>这样在开发的时候就需要来回进行文件切换,尤其是在同时开发模板和逻辑的时候,切来切去会显得尤其麻烦,影响开发效率,但小程序原生只支持这么写,就显得比较尴尬了。</p>\n<p>而在语法上,小程序的语法可以说既像 <code>React</code> ,又像 <code>Vue</code>,不能说显得有点不伦不类吧,但在使用上总是感觉有些别扭,对于开发者来说,等于又要学习一套新的语法,提升了学习成本。而且,小程序的模板由于没有编辑器插件的支持,书写的时候也没有智能提示与 lint 检查,书写起来显得有些麻烦。</p>\n<h3 id=\"命名规范\" class=\"post-heading\"><a href=\"#命名规范\" class=\"headerlink\" title=\"命名规范\"></a>命名规范<a class=\"post-anchor\" href=\"#命名规范\" aria-hidden=\"true\"></a></h3>\n<p>在小程序中到处可见规范不统一的情况</p>\n<p>例如组件的属性,以最简单的 <code>&lt;button /&gt;</code> 组件为例,在小程序官方文档中,该组件的属性部分截图如下,大家可以感受下</p>\n<p><img src=\"http://img10.360buyimg.com/img/jfs/t23614/365/454746445/128330/71fef006/5b307975Ne1ae5de9.jpg\" alt=\"\"></p>\n<p><code>&lt;button /&gt;</code> 组件属性名既有以中划线分割多个单词的情况 <code>session-form</code>,也有多个单词连写的情况 <code>bindgetphonenumber</code>。当然这也不是最严重的,你可以说事件绑定的规范就是 <code>bind + 事件名</code> ,而其他属性的规范就是中划线分割单词,我一度以为小程序就是这个作为标准,直到我看到了 <code>&lt;progress /&gt;</code> 组件</p>\n<p><img src=\"http://img12.360buyimg.com/img/jfs/t23056/19/454899133/104795/d15cd017/5b307975Nd43ac751.jpg\" alt=\"\"></p>\n<p>这和说好的不一样啊喂!</p>\n<p><img src=\"http://img11.360buyimg.com/img/jfs/t23071/12/457583038/19273/4ae32c30/5b307974N88af3974.jpg\" alt=\"\"></p>\n<p>同样的情况也出现在 <code>页面</code> 与 <code>组件</code> 的生命周期方法中,<code>页面</code> 的生命周期方法有 <code>onLoad</code>、<code>onReady</code>、<code>onUnload</code> 等,但到了 <code>组件</code> 中则是 <code>created</code>、<code>attached</code> 、<code>ready</code> 等,这样规范又不统一了,为啥 <code>页面</code> 的生命周期方法是 <code>on+Xxx</code> 的格式,但到了 <code>组件</code> 里缺不一样了呢,有点费解。</p>\n<h3 id=\"开发方式\" class=\"post-heading\"><a href=\"#开发方式\" class=\"headerlink\" title=\"开发方式\"></a>开发方式<a class=\"post-anchor\" href=\"#开发方式\" aria-hidden=\"true\"></a></h3>\n<p>小程序官方提供了 <code>微信开发工具</code> 作为开发编译工具,而对于代码本身没有提供一个类似 <code>webpack</code> 的工程化开发工具,来解决开发中的一些问题,所以小程序原生的开发方式显得不那么现代化,这也是很多小程序开发框架致力于解决的问题。例如,在小程序开发中</p>\n<ul>\n <li><strong>不能使用 <code>npm</code> 管理依赖</strong>,在小程序中需要手动把第三方代码文件下载到本地,然后再 <code>reuqire</code> 进行使用,显得不那么优雅</li>\n <li><strong>不能使用 Sass 等 CSS 预处理器</strong>,由于没有预编译的概念,小程序开发中无法使用市面上流行的 CSS 预处理器,这样会使得样式代码难以管理</li>\n <li><strong>不完整的 ES Next 语法支持</strong>,小程序默认只能支持极少一部分 ES6 规范的语法,而 ES 是不断往前发展的,一些非常优秀的新语法特性就不能使用了</li>\n <li><strong>手动的文件处理</strong>,像图片压缩、代码压缩等等的一些文件操作,必须手工来处理,显得有些繁琐</li>\n</ul>\n<p>以上就是从开发者的角度看到的一些小程序的开发问题,不过纵然有千般困难,我们总要面对,作为新时代的前端开发工程师,我们不能一味忍受问题,要保持技术的头脑,以技术作为武器,用技术手段去提升的我们开发体验。</p>\n<h2 id=\"突发奇想:我能不能用React来写小程序\" class=\"post-heading\"><a href=\"#突发奇想:我能不能用React来写小程序\" class=\"headerlink\" title=\"突发奇想:我能不能用React来写小程序\"></a>突发奇想:我能不能用React来写小程序<a class=\"post-anchor\" href=\"#突发奇想:我能不能用React来写小程序\" aria-hidden=\"true\"></a></h2>\n<p>目前前端界言及前端框架,必离不开依然保持着统治地位的 <code>React</code> 与 <code>Vue</code>,这两个都是非常优秀的前端 UI 框架,而且在网上也经常能看到两个框架的粉丝之间热情交流,碰撞出一些思想火花,显得社区异常活跃。</p>\n<p>而我们团队也在去年勇敢地抛弃了历史包袱,非常荣幸地引入了 <code>React</code> 开发方式,让我们团队丢掉了煤油灯,开始通上了电。而且也研发出了一款优秀的类 <code>React</code> 框架 <code>Nerv</code> ,让我们和 <code>React</code> 开发思想结合得更深。</p>\n<p>与小程序的开发方式相比,<code>React</code> 明显显得更加现代化、规范化,而且 <code>React</code> 天生组件化更适合我们的业务开发,<code>JSX</code> 也比字符串模板有更强的表现力。那么这时候我们就在思考,我们能不能用 <code>React</code> 来写小程序?</p>\n<h3 id=\"理性地探索\" class=\"post-heading\"><a href=\"#理性地探索\" class=\"headerlink\" title=\"理性地探索\"></a>理性地探索<a class=\"post-anchor\" href=\"#理性地探索\" aria-hidden=\"true\"></a></h3>\n<h4 id=\"类比\" class=\"post-heading\"><a href=\"#类比\" class=\"headerlink\" title=\"类比\"></a>类比<a class=\"post-anchor\" href=\"#类比\" aria-hidden=\"true\"></a></h4>\n<p>通过对比体验 小程序和 <code>React</code> ,我们还是能发现两者之间相似的地方</p>\n<h5 id=\"生命周期\" class=\"post-heading\"><a href=\"#生命周期\" class=\"headerlink\" title=\"生命周期\"></a>生命周期<a class=\"post-anchor\" href=\"#生命周期\" aria-hidden=\"true\"></a></h5>\n<p>小程序的生命周期和 <code>React</code> 的生命周期,在很大程度上是类似的,我们甚至能找到他们之间的对应关系</p>\n<p>app 及页面的生命周期</p>\n<table>\n <thead>\n <tr>\n <th>小程序</th>\n <th style=\"text-align:center\">React</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <td>onLaunch</td>\n <td style=\"text-align:center\">componentWillMount</td>\n </tr>\n <tr>\n <td>onLoad</td>\n <td style=\"text-align:center\">componentWillMount</td>\n </tr>\n <tr>\n <td>onReady</td>\n <td style=\"text-align:center\">componentDidMount</td>\n </tr>\n <tr>\n <td>onShow</td>\n <td style=\"text-align:center\">不支持,需要特殊处理</td>\n </tr>\n <tr>\n <td>onHide</td>\n <td style=\"text-align:center\">不支持,需要特殊处理</td>\n </tr>\n <tr>\n <td>onUnload</td>\n <td style=\"text-align:center\">componentWillUnmount</td>\n </tr>\n </tbody>\n</table>\n<p>可以看出,对于 <code>app</code> 及 <code>页面</code> 来说,除了 <code>onShow</code> 与 <code>onHide</code> 两个方法,其他方法都能在 <code>React</code> 中找到对应。</p>\n<h5 id=\"数据更新方式\" class=\"post-heading\"><a href=\"#数据更新方式\" class=\"headerlink\" title=\"数据更新方式\"></a>数据更新方式<a class=\"post-anchor\" href=\"#数据更新方式\" aria-hidden=\"true\"></a></h5>\n<p>在 <code>React</code> 中,组件的内部数据是用 <code>state</code> 来进行管理的,而在小程序中组件的内部数据都是用 <code>data</code> 来进行管理,两者具有一定相似性。而同时在 <code>React</code> 中,我们更新数据使用的是 <code>setState</code> 方法,传入新的数据或者生成新数据的函数,从而更新相应视图。在小程序中,则对应的有 <code>setData</code> 方法,传入新的数据,从而更新视图。</p>\n<p>两者都是以数据驱动视图的方式进行更新,而且 <code>api</code> 神似。</p>\n<h5 id=\"事件绑定\" class=\"post-heading\"><a href=\"#事件绑定\" class=\"headerlink\" title=\"事件绑定\"></a>事件绑定<a class=\"post-anchor\" href=\"#事件绑定\" aria-hidden=\"true\"></a></h5>\n<p>小程序中绑定事件使用的是 <code>bind + 事件名</code> 的方式,例如点击事件,小程序中是 <code>bindtap</code></p>\n<figure class=\"highlight handlebars\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"xml\"><span class=\"tag\">&lt;<span class=\"name\">view</span> <span class=\"attr\">bindtap</span>=<span class=\"string\">\"handlClick\"</span>&gt;</span>1<span class=\"tag\">&lt;/<span class=\"name\">view</span>&gt;</span></span>\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>而在 <code>React</code> 里,则是 <code>on + 事件名</code> 的方式,例如点击事件, <code>React</code> web 中是 <code>onClick</code></p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n &lt;View onClick={\n <span class=\"keyword\">this</span>.handlClick}&gt;\n <span class=\"number\">1</span>&lt;\n <span class=\"regexp\">/View&gt;</span>\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>虽然看上去不一样,但其实是可以类比的,我们只需要在编译时将 <code>on + 事件名</code> 的形式编译成 <code>bind + 事件名</code> 的形式就可以了。</p>\n<blockquote>\n <p>如此看来,两者之间有些相似,用 <code>React</code> 来写小程序貌似是可行的,但接下来我们就发现了巨大的差异。</p>\n</blockquote>\n<h3 id=\"巨大的差异\" class=\"post-heading\"><a href=\"#巨大的差异\" class=\"headerlink\" title=\"巨大的差异\"></a>巨大的差异<a class=\"post-anchor\" href=\"#巨大的差异\" aria-hidden=\"true\"></a></h3>\n<p><code>React</code> 与小程序之间最大的差异就是他们的模板了,在 <code>React</code> 中,是使用 <code>JSX</code> 来作为组件的模板的,而小程序则与 <code>Vue</code> 一样,是使用字符串模板的。这样两者之间就有着巨大的差异了。</p>\n<p>JSX</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n render () {\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">return</span> (\n </div>\n <div class=\"line\"> \n <span class=\"xml\"><span class=\"tag\">&lt;<span class=\"name\">View</span> <span class=\"attr\">className</span>=<span class=\"string\">\'index\'</span>&gt;</span></span>\n </div>\n <div class=\"line\">\n {this.state.list.map((item, idx) =&gt; (\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">View</span> <span class=\"attr\">key</span>=<span class=\"string\">{idx}</span>&gt;</span>{item}\n <span class=\"tag\">&lt;/<span class=\"name\">View</span>&gt;</span>\n </div>\n <div class=\"line\">\n ))}\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">Button</span> <span class=\"attr\">onClick</span>=<span class=\"string\">{this.goto}</span>&gt;</span>走你\n <span class=\"tag\">&lt;/<span class=\"name\">Button</span>&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;/<span class=\"name\">View</span>&gt;</span>\n </div>\n <div class=\"line\">\n )\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>小程序模板</p>\n<figure class=\"highlight htmlbars\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"xml\"><span class=\"tag\">&lt;<span class=\"name\">view</span> <span class=\"attr\">class</span>=<span class=\"string\">\"index\"</span>&gt;</span></span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">view</span> <span class=\"attr\">wx:key</span>=<span class=\"string\">{idx}</span> <span class=\"attr\">wx:for</span>=<span class=\"string\">\"</span></span>\n <span class=\"template-variable\">{{list}}</span>\n <span class=\"xml\"><span class=\"tag\"><span class=\"string\">\"</span> <span class=\"attr\">wx:for-item</span>=<span class=\"string\">\"item\"</span> <span class=\"attr\">wx:for-index</span>=<span class=\"string\">\"idx\"</span>&gt;</span></span>\n <span class=\"template-variable\">{{item}}</span>\n <span class=\"xml\"><span class=\"tag\">&lt;/<span class=\"name\">view</span>&gt;</span></span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">view</span> <span class=\"attr\">bindtap</span>=<span class=\"string\">\"goto\"</span>&gt;</span>走你\n <span class=\"tag\">&lt;/<span class=\"name\">view</span>&gt;</span>\n </div>\n <div class=\"line\">\n <span class=\"tag\">&lt;/<span class=\"name\">view</span>&gt;</span>\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>众所周知,<code>JSX</code> 其实本质上就是 <code>JS</code>,我们可以在里面写任意的逻辑代码,这样一来就比字符串模板的表现力与操作性要强多了,况且,小程序的字符串模板功能比较羸弱,只有一些比较基本的功能。那这样的话,要如何来实现用 <code>JSX</code> 来写小程序模板呢。</p>\n<h3 id=\"编译原理的力量\" class=\"post-heading\"><a href=\"#编译原理的力量\" class=\"headerlink\" title=\"编译原理的力量\"></a>编译原理的力量<a class=\"post-anchor\" href=\"#编译原理的力量\" aria-hidden=\"true\"></a></h3>\n<p>我们可以仔细来分析我们的需求,我们期望使用 <code>JSX</code> 来书写小程序模板,但小程序显然是不支持执行 <code>JSX</code> 代码的(要是支持的话,Taro 应该也就不存在了吧),我们也不能期望微信能给我们开个后门来跑 <code>JSX</code>。那么这个时候我们就想,我们要是能够将 <code>JSX</code> 编译成小程序模板就好了。</p>\n<p>事实上在我们平时的开发中,这种编译的操作到处可见,<code>babel</code> 就是我们最常用的 <code>JS 代码编译器</code>,一般浏览器是不能支持一些非常新的语法特性的,但我们又想使用它们,这个时候就可以借助 <code>babel</code> 来将我们的高版本的 ES 代码,编译成浏览器可以运行的 ES 代码。而我们像要将 <code>JSX</code>编译成小程序模板,也是同样的道理。我们首先来了解一下 <code>Babel</code> 的运行机制。</p>\n<p><code>Babel</code> 作为一个 <code>代码编译器</code> ,能够将 ES6/7/8 的代码编译成 ES5 的代码,其核心利用的就是计算中非常基础的编译原理知识,将输入语言代码,通过编译器执行,输出目标语言的代码。编译原理的一般过程就是,输入源程序,经过词法分析、语法分析,构造出语法树,再经过语义分析,理解程序正确与否,再对语法树做出需要的操作与优化,最终生成目标代码。</p>\n<p><img src=\"http://m.360buyimg.com/img/jfs/t23551/19/471755927/52473/e349bf6c/5b307975N60463097.jpg\" alt=\"\"></p>\n<p><code>Babel</code> 的编译过程亦是如此,主要包含三个阶段</p>\n<ul>\n <li>解析过程,在这个过程中进行词法、语法分析,以及语义分析,生成符合 <a href=\"https://github.com/estree/estree\" target=\"_blank\" rel=\"external\">ESTree 标准</a> 虚拟语法树(AST)</li>\n <li>转换过程,针对 AST 做出已定义好的操作,<code>babel</code> 的配置文件 <code>.babelrc</code> 中定义的 <code>preset</code> 、 <code>plugin</code> 就是在这一步中执行并改变 AST 的</li>\n <li>生成过程,将前一步转换好的 AST 生成目标代码的字符串</li>\n</ul>\n<p>为了更好地理解这些过程,大家可以利用 <a href=\"https://astexplorer.net/\" target=\"_blank\" rel=\"external\">Ast Explorer</a> 这个网站接一下自己的代码,感受一下每一部分代码所对应的 AST 结构。</p>\n<p><img src=\"http://img30.360buyimg.com/img/jfs/t22843/37/455454462/106864/1d96f394/5b307975Nf78e5829.jpg\" alt=\"\"></p>\n<p>可以看到,一份源码经过编译器解析后,会变成类似如下的结构</p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n {\n </div>\n <div class=\"line\"> \n <span class=\"attr\">type</span>: \n <span class=\"string\">\"Program\"</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">start</span>: \n <span class=\"number\">0</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">end</span>: \n <span class=\"number\">78</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">loc</span>: { start, end }\n </div>\n <div class=\"line\">\n sourceType: \n <span class=\"string\">\"module\"</span>,\n </div>\n <div class=\"line\"> \n <span class=\"attr\">body</span>: [\n </div>\n <div class=\"line\">\n { \n <span class=\"attr\">type</span>: \n <span class=\"string\">\"VariableDeclaration\"</span>, ... },\n </div>\n <div class=\"line\">\n { \n <span class=\"attr\">type</span>: \n <span class=\"string\">\"VariableDeclaration\"</span>, ... },\n </div>\n <div class=\"line\">\n { \n <span class=\"attr\">type</span>: \n <span class=\"string\">\"FunctionDeclaration\"</span>, ... },\n </div>\n <div class=\"line\">\n { \n <span class=\"attr\">type</span>: \n <span class=\"string\">\"ExpressionStatement\"</span>, ... }\n </div>\n <div class=\"line\">\n ]\n </div>\n <div class=\"line\">\n ...\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>其中,<code>body</code> 里包含的就是我们示例代码的语法树结构,第一个 <code>VariableDeclaration</code> 对应的是 <code>const a = 1</code>,第三个 <code>FunctionDeclaration</code> 对应的则是 <code>function sum (a, b) { }</code>,分别就是 JS 中的变量定义与函数定义,每一个树节点里都会包含许多子节点,这样就形成了一个树形结构,更多的节点类型,请参考 <a href=\"https://github.com/babel/babel/tree/master/packages/babel-types\" target=\"_blank\" rel=\"external\">babel types</a>。</p>\n<p>当然我们在这儿只是简单介绍下编译原理与 <code>babel</code>,编译原理是一门非常深奥的课程, <code>babel</code> 也是一个非常优秀的工具,希望在后续的文章中能和大家再详细探讨这一部分内容。</p>\n<p>再次回到我们的需求,将 <code>JSX</code> 编译成小程序模板,非常幸运的是 <code>babel</code> 的核心编译器 <code>babylon</code> 是支持对 <code>JSX</code> 语法的解析的,我们可以直接利用它来帮我们构造 AST,而我们需要专注的核心就是如何对 AST 进行转换操作,得出我们需要的新 AST,再将新 AST 进行递归遍历,生成小程序的模板。</p>\n<p><code>JSX</code> 代码</p>\n<figure class=\"highlight\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n &lt;View className=\'index\'&gt;\n </div>\n <div class=\"line\">\n &lt;Button className=\'add_btn\' onClick={this.props.add}&gt;+&lt;/Button&gt;\n </div>\n <div class=\"line\">\n &lt;Button className=\'dec_btn\' onClick={this.props.dec}&gt;-&lt;/Button&gt;\n </div>\n <div class=\"line\">\n &lt;Button className=\'dec_btn\' onClick={this.props.asyncAdd}&gt;async&lt;/Button&gt;\n </div>\n <div class=\"line\">\n &lt;View&gt;{this.props.counter.num}&lt;/View&gt;\n </div>\n <div class=\"line\">\n &lt;A /&gt;\n </div>\n <div class=\"line\">\n &lt;Button onClick={this.goto}&gt;走你&lt;/Button&gt;\n </div>\n <div class=\"line\">\n &lt;Image src={sd} /&gt;\n </div>\n <div class=\"line\">\n &lt;/View&gt;\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>编译生成小程序模板</p>\n<figure class=\"highlight handlebars\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"xml\"><span class=\"tag\">&lt;<span class=\"name\">import</span> <span class=\"attr\">src</span>=<span class=\"string\">\"../../components/A/A.wxml\"</span> /&gt;</span></span>\n </div>\n <div class=\"line\">\n <span class=\"tag\">&lt;<span class=\"name\">block</span>&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">view</span> <span class=\"attr\">class</span>=<span class=\"string\">\"index\"</span>&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">button</span> <span class=\"attr\">class</span>=<span class=\"string\">\"add_btn\"</span> <span class=\"attr\">bindtap</span>=<span class=\"string\">\"add\"</span>&gt;</span>+\n <span class=\"tag\">&lt;/<span class=\"name\">button</span>&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">button</span> <span class=\"attr\">class</span>=<span class=\"string\">\"dec_btn\"</span> <span class=\"attr\">bindtap</span>=<span class=\"string\">\"dec\"</span>&gt;</span>-\n <span class=\"tag\">&lt;/<span class=\"name\">button</span>&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">button</span> <span class=\"attr\">class</span>=<span class=\"string\">\"dec_btn\"</span> <span class=\"attr\">bindtap</span>=<span class=\"string\">\"asyncAdd\"</span>&gt;</span>async\n <span class=\"tag\">&lt;/<span class=\"name\">button</span>&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">view</span>&gt;</span>\n <span class=\"template-variable\">{{counter.num}}</span>\n <span class=\"xml\"><span class=\"tag\">&lt;/<span class=\"name\">view</span>&gt;</span></span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">template</span> <span class=\"attr\">is</span>=<span class=\"string\">\"A\"</span> <span class=\"attr\">data</span>=<span class=\"string\">\"</span></span>\n <span class=\"template-variable\">{{...$$A}}</span>\n <span class=\"xml\"><span class=\"tag\"><span class=\"string\">\"</span>&gt;</span><span class=\"tag\">&lt;/<span class=\"name\">template</span>&gt;</span></span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">button</span> <span class=\"attr\">bindtap</span>=<span class=\"string\">\"goto\"</span>&gt;</span>走你\n <span class=\"tag\">&lt;/<span class=\"name\">button</span>&gt;</span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;<span class=\"name\">image</span> <span class=\"attr\">src</span>=<span class=\"string\">\"</span></span>\n <span class=\"template-variable\">{{sd}}</span>\n <span class=\"xml\"><span class=\"tag\"><span class=\"string\">\"</span> /&gt;</span></span>\n </div>\n <div class=\"line\"> \n <span class=\"tag\">&lt;/<span class=\"name\">view</span>&gt;</span>\n </div>\n <div class=\"line\">\n <span class=\"tag\">&lt;/<span class=\"name\">block</span>&gt;</span>\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>这时候,聪明的你应该就能发现问题的难点所在了,要知道小程序的模板只是字符串,而 <code>JSX</code> 则是真正的 JS 代码扩展,其语法之丰富,显然不是字符串模板所能比,在这一步中,我们要做的操作,包括但不仅限于如下</p>\n<ul>\n <li>理解三目运算符与逻辑表达式,例如三目运算符 <code>abc ? : &lt;View&gt;1&lt;/View&gt; : &lt;View&gt;2&lt;/View&gt;</code> 需要编译成 <code>&lt;view wx:if=\"\"&gt;1&lt;/view&gt;&lt;view wx:else&gt;2&lt;/view&gt;</code></li>\n <li>理解数组 <code>map</code> 语法,例如 <code>map</code> 的使用 <code>abc.map(item =&gt; &lt;View&gt;item&lt;/View&gt;)</code> 需要编译成 <code>&lt;view wx:for=\"\" wx:for-item=\"item\"&gt;item&lt;/view&gt;</code></li>\n <li>等等</li>\n</ul>\n<p>以上仅仅是我们转换规则的冰山一角,<code>JSX</code> 的写法极其灵活多变,我们只能通过穷举的方式,将常用的、React 官方推荐的写法作为转换规则加以支持,而一些比较生僻的,或者是不那么推荐的写的写法则不做支持,转而以 <code>eslint</code> 插件的方式,提示用户进行修改。目前我们支持的 <code>JSX</code> 转换规则,大致能覆盖到 <code>JSX</code> 80% 的写法操作。</p>\n<p>关于 JSX 转小程序模板这一部分,我们将在后续的技术原理分析系列文章中,详细为大家介绍。</p>\n<h2 id=\"还能不能干点别的\" class=\"post-heading\"><a href=\"#还能不能干点别的\" class=\"headerlink\" title=\"还能不能干点别的\"></a>还能不能干点别的<a class=\"post-anchor\" href=\"#还能不能干点别的\" aria-hidden=\"true\"></a></h2>\n<p>经过我们一次次的探索,以及一波波猛如虎的操作,我们已经可以将类 React 代码转成小程序可以跑的代码了,也就是说我们已经可以正式以 React 的方式来写小程序的代码了。喜大普奔!但是我们激动之余,冷静下来继续思考,我们还能不能干点别的有意思的事情呢。</p>\n<h3 id=\"分析一下需求\" class=\"post-heading\"><a href=\"#分析一下需求\" class=\"headerlink\" title=\"分析一下需求\"></a>分析一下需求<a class=\"post-anchor\" href=\"#分析一下需求\" aria-hidden=\"true\"></a></h3>\n<p>我们发现,在平常的工作中,我们业务通常有一些多端的需求,就是要求小程序要有,H5 要有,甚至 RN 也能有就最好了,我猜产品经理还看不上快应用,不然肯定要求我们快应用也上一套吧,反正你们不是经常号称代码优秀、高度可复用么。这个时候,你就会发现,差不多的界面和逻辑,你可能需要重复写上好几轮,这时候要是有个多端代码生成工具就好了,只写一份代码,可以多端运行。Write once, run anywhere,相信是所有工程师的梦想。</p>\n<h3 id=\"依然编译原理的力量\" class=\"post-heading\"><a href=\"#依然编译原理的力量\" class=\"headerlink\" title=\"依然编译原理的力量\"></a>依然编译原理的力量<a class=\"post-anchor\" href=\"#依然编译原理的力量\" aria-hidden=\"true\"></a></h3>\n<p>这时候我们回忆一下前文的内容,将一份代码编译成多端代码,这不正是编译原理干的事么,我们可以输入一份源代码,针对不同的端设定好对应的转换规则,再一键转换出对应端的代码。而且由于我们已经遵循 React 语法了,那我们再转成 H5 端(使用 Nerv)与 RN 端(使用 React)也就有了天然的优势。</p>\n<p><img src=\"http://img11.360buyimg.com/img/jfs/t23863/65/477773801/39493/d1292897/5b307974Na1febb30.jpg\" alt=\"\"></p>\n<h3 id=\"设计思路补完\" class=\"post-heading\"><a href=\"#设计思路补完\" class=\"headerlink\" title=\"设计思路补完\"></a>设计思路补完<a class=\"post-anchor\" href=\"#设计思路补完\" aria-hidden=\"true\"></a></h3>\n<p>但是仔细思考我们又会发现,仅仅将代码按照对应语法规则转换过去后,还远远不够,因为不同端会有自己的原生组件,端能力 API 等等,代码直接转换过去后,可能不能直接执行。例如,小程序中普通的容器组件用的是 <code>&lt;view /&gt;</code>,而在 H5 中则是 <code>&lt;div /&gt;</code>;小程序中提供了丰富的端能力 API,例如网络请求、文件下载、数据缓存等,而在 H5 中对应功能的 API 则不一致。</p>\n<p>所以,为了弥补不同端的差异,我们需要订制好一个统一的组件库标准,以及统一的 API 标准,在不同的端依靠它们的语法与能力去实现这个组件库与 API,同时还要为不同的端编写相应的运行时框架,负责初始化等等操作。通过以上这些操作,我们就能实现一份一键生成多端的需求了。在 Taro 最初的设计中,我们组件库与 API 的标准就是源自小程序的,因为我们觉得既然已经有定义好的组件库与 API 标准,那为啥不直接拿来使用呢,这样不仅省去了定制标准的冥思苦想,同时也省去了为小程序开发组件库与 API 的麻烦,只需要让其他端来向小程序靠齐就好。</p>\n<p>可能有些人会有疑问,既然是为不同的端实现了对应的组件库与端能力 API (小程序除外,因为组件库和 API 的标准都是源自小程序),那么是怎么能够只写一份代码就够了呢?因为我们有编译的操作,在书写代码的时候,只需要引入标准组件库 <code>@tarojs/components</code> 与运行时框架 <code>@tarojs/taro</code> ,代码经过编译之后,会变成对应端所需要的库。</p>\n<p><img src=\"http://img14.360buyimg.com/img/jfs/t21535/241/1645070830/74027/775c8a15/5b307976Nce466138.jpg\" alt=\"\"></p>\n<p>既然组件库以及端能力都是依靠不同的端做不同实现来抹平差异,那么同样的,如果我们想为 Taro 引入更多的功能支持的话,有时候也需要按照这个套路来。例如,为了提升开发便利性,我们为 Taro 加入了 <code>Redux</code> 支持,我们的做法就是,在小程序端,我们实现了 <code>@tarojs/redux</code> 这个库来作为小程序的 <code>Redux</code> 辅助库,并且以他作为基准库,它具有和 <code>react-redux</code> 一致的 API,在书写代码的时候,引用的都是 <code>@tarojs/redux</code> ,经过编译后,在 H5 端会替换成 <code>nerv-redux</code>(<code>Nerv</code>的 <code>Redux</code> 辅助库),在 RN 端会替换成 <code>react-redux</code>。这样就实现了 <code>Redux</code> 在 Taro 中的多端支持。</p>\n<p><img src=\"http://img30.360buyimg.com/uba/jfs/t22360/120/839096197/151922/229ceba4/5b1a6fcdNed7d4039.jpg\" alt=\"\"></p>\n<p>以上就是 Taro 的整体设计思路,里面还有很多细节没有展开去阐述,可能大家会觉得有些意犹未尽,后续我们将会产出一系列的文章来阐述 Taro 的技术细节,例如 《Taro 开发工具原理分析》、《Taro 代码编译的背后》、《深入浅出 JSX 转小程序模板》等等。</p>\n<h2 id=\"最后的最后\" class=\"post-heading\"><a href=\"#最后的最后\" class=\"headerlink\" title=\"最后的最后\"></a>最后的最后<a class=\"post-anchor\" href=\"#最后的最后\" aria-hidden=\"true\"></a></h2>\n<p>Taro 从立项之初到现在已经差不多有了三个月左右的时间,从最初的激烈讨论方案,各种思想的碰撞,到方案逐渐成型,进入火热的开发迭代,再到现在的小程序端和 H5 端顺利支持,从而决定走向开源。这一路走来,收获颇丰,既有跟团队小伙伴一起创造的激动,也有无数个日夜加班的苦思。Taro 是凹凸实验室的诚意之作,我们也将会一直维护下去,希望 Taro 能越来越好,帮助更多人创造更多价值。</p>\n<p>项目官网:<a href=\"https://taro.aotu.io/\" target=\"_blank\" rel=\"external\">https://taro.aotu.io/</a></p>\n<p>项目 GitHub:<a href=\"https://github.com/NervJS/taro\" target=\"_blank\" rel=\"external\">https://github.com/NervJS/taro</a></p>\n<p><strong>同时,有任何关于 Taro 希望沟通交流的,欢迎~</strong></p>\n<p><img src=\"http://img20.360buyimg.com/uba/jfs/t20197/283/1687168874/136042/2b4d811f/5b30a65cN9d1f03f1.png\" alt=\"\"></p>\n<div class=\"post-tags\" style=\"display:none\">\n <a href=\"/tags/React/\">React</a> \n <a href=\"/tags/小程序/\">小程序</a> \n <a href=\"/tags/Nerv/\">Nerv</a> \n <a href=\"/tags/Taro/\">Taro</a>\n</div>\n<div class=\"post-categories\" style=\"display:none\">\n <a href=\"/cates/小程序/\">小程序</a>\n</div>\n<div class=\"post-announce\">\n 感谢您的阅读,本文由 \n <a href=\"//aotu.io\">凹凸实验室</a> 版权所有。如若转载,请注明出处:凹凸实验室(\n <a href=\"https://aotu.io/notes/2018/06/25/the-birth-of-taro/\">https://aotu.io/notes/2018/06/25/the-birth-of-taro/</a>)\n</div>\n<div class=\"post-revision\">\n <i class=\"fa fa-clock-o\"></i> \n <time class=\"post-updated\" datetime=\"2018-11-07T01:01:15.657Z\">上次更新:2018-11-07 09:01:15</time>\n</div>');
INSERT INTO `rumo_blog` VALUES ('36', 'H5游戏开发:指尖大冒险', '1', 'H5游戏开发:指尖大冒险', '1', '2018-11-11 21:02:34', '2018-11-11 21:02:34', '1', '0', 'http://misc.aotu.io/Tingglelaoo/h5game_jumping_900x500.png', '', '0', '0', '<a id=\"more\"></a>\n<p>在今年八月中旬,《指尖大冒险》SNS 游戏诞生,其具体的玩法是通过点击屏幕左右区域来控制机器人的前进方向进行跳跃,而阶梯是无穷尽的,若遇到障碍物或者是踩空、或者机器人脚下的阶砖陨落,那么游戏失败。</p>\n<blockquote>\n <p>笔者对游戏进行了简化改造,可通过扫下面二维码进行体验。</p>\n</blockquote>\n<p></p>\n<div style=\"margin:0 auto;width:fit-content\">\n <img src=\"//misc.aotu.io/Tingglelaoo/demo.png\" alt=\"demo.png\">\n</div>\n<br>\n<small style=\"display:block;text-align:center\">《指尖大冒险》SNS 游戏简化版</small>\n<p></p>\n<p>该游戏可以被划分为三个层次,分别为景物层、阶梯层、背景层,如下图所示。</p>\n<p></p>\n<div style=\"margin:0 auto;width:fit-content\">\n <img src=\"//misc.aotu.io/Tingglelaoo/layers.png\" alt=\"layers.png\">\n</div>\n<br>\n<small style=\"display:block;text-align:center\">《指尖大冒险》游戏的层次划分</small>\n<p></p>\n<p>整个游戏主要围绕着这三个层次进行开发:</p>\n<ul>\n <li>景物层:负责两侧树叶装饰的渲染,实现其无限循环滑动的动画效果。</li>\n <li>阶梯层:负责阶梯和机器人的渲染,实现阶梯的随机生成与自动掉落阶砖、机器人的操控。</li>\n <li>背景层:负责背景底色的渲染,对用户点击事件监听与响应,把景物层和阶梯层联动起来。</li>\n</ul>\n<p>而本文主要来讲讲以下几点核心的技术内容:</p>\n<ol>\n <li>无限循环滑动的实现</li>\n <li>随机生成阶梯的实现</li>\n <li>自动掉落阶砖的实现</li>\n</ol>\n<p>下面,本文逐一进行剖析其开发思路与难点。</p>\n<h2 id=\"一、无限循环滑动的实现\" class=\"post-heading\"><a href=\"#一、无限循环滑动的实现\" class=\"headerlink\" title=\"一、无限循环滑动的实现\"></a>一、无限循环滑动的实现<a class=\"post-anchor\" href=\"#一、无限循环滑动的实现\" aria-hidden=\"true\"></a></h2>\n<p>景物层负责两侧树叶装饰的渲染,树叶分为左右两部分,紧贴游戏容器的两侧。</p>\n<p>在用户点击屏幕操控机器人时,两侧树叶会随着机器人前进的动作反向滑动,来营造出游戏运动的效果。并且,由于该游戏是无穷尽的,因此,需要对两侧树叶实现循环向下滑动的动画效果。</p>\n<p></p>\n<div style=\"margin:0 auto;width:fit-content\">\n <img src=\"//misc.aotu.io/Tingglelaoo/Leafheight.png\" alt=\"Leafheight.png\">\n</div>\n<br>\n<small style=\"display:block;text-align:center\">循环场景图设计要求</small>\n<p></p>\n<p>对于循环滑动的实现,首先要求设计提供可前后无缝衔接的场景图,并且建议其场景图高度或宽度大于游戏容器的高度或宽度,以减少重复绘制的次数。</p>\n<p>然后按照以下步骤,我们就可以实现循环滑动:</p>\n<ul>\n <li>重复绘制两次场景图,分别在定位游戏容器底部与在相对偏移量为贴图高度的上方位置。</li>\n <li>在循环的过程中,两次贴图以相同的偏移量向下滑动。</li>\n <li>当贴图遇到刚滑出游戏容器的循环节点时,则对贴图位置进行重置。</li>\n</ul>\n<p></p>\n<div style=\"margin:0 auto;width:fit-content\">\n <img src=\"//misc.aotu.io/Tingglelaoo/leafmove.gif\" alt=\"leafmove.gif\">\n</div>\n<br>\n<small style=\"display:block;text-align:center\">无限循环滑动的实现</small>\n<p></p>\n<p>用伪代码描述如下:<br></p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div>\n <div class=\"line\">\n 16\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">// 设置循环节点</span>\n </div>\n <div class=\"line\">\n transThreshold = stageHeight;\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"comment\">// 获取滑动后的新位置,transY是滑动偏移量</span>\n </div>\n <div class=\"line\">\n lastPosY1 = leafCon1.y + transY; \n </div>\n <div class=\"line\">\n lastPosY2 = leafCon2.y + transY;\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"comment\">// 分别进行滑动</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">if</span> leafCon1.y &gt;= transThreshold \n <span class=\"comment\">// 若遇到其循环节点,leafCon1重置位置</span>\n </div>\n <div class=\"line\">\n then leafCon1.y = lastPosY2 - leafHeight;\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">else</span> leafCon1.y = lastPosY1;\n </div>\n <div class=\"line\"></div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"keyword\">if</span> leafCon2.y &gt;= transThreshold \n <span class=\"comment\">// 若遇到其循环节点,leafCon2重置位置</span>\n </div>\n <div class=\"line\">\n then leafCon2.y = lastPosY1 - leafHeight;\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">else</span> leafCon2.y = lastPosY2;\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<p>在实际实现的过程中,再对位置变化过程加入动画进行润色,无限循环滑动的动画效果就出来了。</p>\n<h2 id=\"二、随机生成阶梯的实现\" class=\"post-heading\"><a href=\"#二、随机生成阶梯的实现\" class=\"headerlink\" title=\"二、随机生成阶梯的实现\"></a>二、随机生成阶梯的实现<a class=\"post-anchor\" href=\"#二、随机生成阶梯的实现\" aria-hidden=\"true\"></a></h2>\n<p>随机生成阶梯是游戏的最核心部分。根据游戏的需求,阶梯由「无障碍物的阶砖」和「有障碍物的阶砖」的组成,并且阶梯的生成是随机性。</p>\n<h3 id=\"无障碍阶砖的规律\" class=\"post-heading\"><a href=\"#无障碍阶砖的规律\" class=\"headerlink\" title=\"无障碍阶砖的规律\"></a>无障碍阶砖的规律<a class=\"post-anchor\" href=\"#无障碍阶砖的规律\" aria-hidden=\"true\"></a></h3>\n<p>其中,无障碍阶砖组成一条畅通无阻的路径,虽然整个路径的走向是随机性的,但是每个阶砖之间是相对规律的。</p>\n<p>因为,在游戏设定里,用户只能通过点击屏幕的左侧或者右侧区域来操控机器人的走向,那么下一个无障碍阶砖必然在当前阶砖的左上方或者右上方。</p>\n<p></p>\n<div style=\"margin:0 auto;width:fit-content\">\n <img src=\"//misc.aotu.io/Tingglelaoo/stairsguilv.png\" alt=\"stairsguilv.png\">\n</div>\n<br>\n<small style=\"display:block;text-align:center\">无障碍路径的生成规律</small>\n<p></p>\n<p>用 0、1 分别代表左上方和右上方,那么我们就可以建立一个无障碍阶砖集合对应的数组(下面简称无障碍数组),用于记录无障碍阶砖的方向。</p>\n<p>而这个数组就是包含 0、1 的随机数数组。例如,如果生成如下阶梯中的无障碍路径,那么对应的随机数数组为 [0, 0, 1, 1, 0, 0, 0, 1, 1, 1]。</p>\n<p></p>\n<div style=\"margin:0 auto;width:fit-content\">\n <img src=\"//misc.aotu.io/Tingglelaoo/stairArr.png\" alt=\"stairArr.png\">\n</div>\n<br>\n<small style=\"display:block;text-align:center\">无障碍路径对应的 0、1 随机数</small>\n<p></p>\n<h3 id=\"障碍阶砖的规律\" class=\"post-heading\"><a href=\"#障碍阶砖的规律\" class=\"headerlink\" title=\"障碍阶砖的规律\"></a>障碍阶砖的规律<a class=\"post-anchor\" href=\"#障碍阶砖的规律\" aria-hidden=\"true\"></a></h3>\n<p>障碍物阶砖也是有规律而言的,如果存在障碍物阶砖,那么它只能出现在当前阶砖的下一个无障碍阶砖的反方向上。</p>\n<p>根据游戏需求,障碍物阶砖不一定在邻近的位置上,其相对当前阶砖的距离是一个阶砖的随机倍数,距离范围为 1~3。</p>\n<p></p>\n<div style=\"margin:0 auto;width:fit-content\">\n <img src=\"//misc.aotu.io/Tingglelaoo/barrguilv.png\" alt=\"barrguilv.png\">\n</div>\n<br>\n<small style=\"display:block;text-align:center\">障碍阶砖的生成规律</small>\n<p></p>\n<p>同样地,我们可以用 0、1、2、3 代表其相对距离倍数,0 代表不存在障碍物阶砖,1 代表相对一个阶砖的距离,以此类推。</p>\n<p>因此,障碍阶砖集合对应的数组就是包含 0、1、2、3 的随机数数组(下面简称障碍数组)。例如,如果生成如下图中的障碍阶砖,那么对应的随机数数组为 [0, 1, 1, 2, 0, 1, 3, 1, 0, 1]。</p>\n<p></p>\n<div style=\"margin:0 auto;width:fit-content\">\n <img src=\"//misc.aotu.io/Tingglelaoo/barrArr.png\" alt=\"barrArr.png\">\n</div>\n<br>\n<small style=\"display:block;text-align:center\">障碍阶砖对应的 0、1、2、3 随机数</small>\n<p></p>\n<p>除此之外,根据游戏需求,障碍物阶砖出现的概率是不均等的,不存在的概率为 50% ,其相对距离越远概率越小,分别为 20%、20%、10%。</p>\n<h3 id=\"利用随机算法生成随机数组\" class=\"post-heading\"><a href=\"#利用随机算法生成随机数组\" class=\"headerlink\" title=\"利用随机算法生成随机数组\"></a>利用随机算法生成随机数组<a class=\"post-anchor\" href=\"#利用随机算法生成随机数组\" aria-hidden=\"true\"></a></h3>\n<p>根据阶梯的生成规律,我们需要建立两个数组。</p>\n<p>对于无障碍数组来说,随机数 0、1 的出现概率是均等的,那么我们只需要利用 <code>Math.random()</code>来实现映射,用伪代码表示如下:<br></p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">// 生成随机数i,min &lt;= i &lt; max</span>\n </div>\n <div class=\"line\">\n <span class=\"function\"><span class=\"keyword\">function</span> <span class=\"title\">getRandomInt</span>(<span class=\"params\">min, max</span>) </span>{ \n </div>\n <div class=\"line\"> \n <span class=\"keyword\">return</span> \n <span class=\"built_in\">Math</span>.floor(\n <span class=\"built_in\">Math</span>.random() * (max - min) + min);\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">// 生成指定长度的0、1随机数数组</span>\n </div>\n <div class=\"line\">\n arr = [];\n </div>\n <div class=\"line\">\n <span class=\"keyword\">for</span> i = \n <span class=\"number\">0</span> to len\n </div>\n <div class=\"line\">\n arr.push(getRandomInt(\n <span class=\"number\">0</span>,\n <span class=\"number\">2</span>));\n </div>\n <div class=\"line\">\n <span class=\"keyword\">return</span> arr;\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p>而对于障碍数组来说,随机数 0、1、2、3 的出现概率分别为:P(0)=50%、P(1)=20%、P(2)=20%、P(3)=10%,是不均等概率的,那么生成无障碍数组的办法便是不适用的。</p>\n<p>那如何实现生成这种满足指定非均等概率分布的随机数数组呢?</p>\n<p>我们可以利用概率分布转化的理念,将非均等概率分布转化为均等概率分布来进行处理,做法如下:</p>\n<ol>\n <li>建立一个长度为 L 的数组 A ,L 的大小从计算非均等概率的分母的最小公倍数得来。</li>\n <li>根据非均等概率分布 P 的情况,对数组空间分配,分配空间长度为 L * Pi ,用来存储记号值 i 。</li>\n <li>利用满足均等概率分布的随机办法随机生成随机数 s。</li>\n <li>以随机数 s 作为数组 A 下标,可得到满足非均等概率分布 P 的随机数 A[s] ——记号值 i。</li>\n</ol>\n<p>我们只要反复执行步骤 4 ,就可得到满足上述非均等概率分布情况的随机数数组——障碍数组。</p>\n<p>结合障碍数组生成的需求,其实现步骤如下图所示。</p>\n<p></p>\n<div style=\"margin:0 auto;width:fit-content\">\n <img src=\"//misc.aotu.io/Tingglelaoo/alg1_demo.png\" alt=\"alg1_demo.png\">\n</div>\n<br>\n<small style=\"display:block;text-align:center\">障碍数组值随机生成过程</small>\n<p></p>\n<p>用伪代码表示如下:<br></p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div>\n <div class=\"line\">\n 14\n </div>\n <div class=\"line\">\n 15\n </div>\n <div class=\"line\">\n 16\n </div>\n <div class=\"line\">\n 17\n </div>\n <div class=\"line\">\n 18\n </div>\n <div class=\"line\">\n 19\n </div>\n <div class=\"line\">\n 20\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">// 非均等概率分布Pi</span>\n </div>\n <div class=\"line\">\n P = [\n <span class=\"number\">0.5</span>, \n <span class=\"number\">0.2</span>, \n <span class=\"number\">0.2</span>, \n <span class=\"number\">0.1</span>]; \n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"comment\">// 获取最小公倍数</span>\n </div>\n <div class=\"line\">\n L = getLCM(P); \n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"comment\">// 建立概率转化数组</span>\n </div>\n <div class=\"line\">\n A = [];\n </div>\n <div class=\"line\">\n l = \n <span class=\"number\">0</span>;\n </div>\n <div class=\"line\">\n <span class=\"keyword\">for</span> i = \n <span class=\"number\">0</span> to P.length\n </div>\n <div class=\"line\">\n k = L * P[i] + l\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">while</span> l &lt; k\n </div>\n <div class=\"line\">\n A[l] = i;\n </div>\n <div class=\"line\">\n l++;\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"comment\">// 获取均等概率分布的随机数</span>\n </div>\n <div class=\"line\">\n s = \n <span class=\"built_in\">Math</span>.floor(\n <span class=\"built_in\">Math</span>.random() * L);\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"comment\">// 返回满足非均等概率分布的随机数</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">return</span> A[s];\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<p>对这种做法进行性能分析,其生成随机数的时间复杂度为 O(1) ,但是在初始化数组 A 时可能会出现极端情况,因为其最小公倍数有可能为 100、1000 甚至是达到亿数量级,导致无论是时间上还是空间上占用都极大。</p>\n<p>有没有办法可以进行优化这种极端的情况呢?<br>经过研究,笔者了解到 <a href=\"https://en.wikipedia.org/wiki/Alias_method\" target=\"_blank\" rel=\"external\">Alias Method</a> 算法可以解决这种情况。</p>\n<p>Alias Method 算法有一种最优的实现方式,称为 Vose’s Alias Method ,其做法简化描述如下:</p>\n<ol>\n <li>根据概率分布,以概率作为高度构造出一个高度为 1(概率为1)的矩形。</li>\n <li>根据构造结果,推导出两个数组 Prob 数组和 Alias 数组。</li>\n <li>在 Prob 数组中随机取其中一值 Prob[i] ,与随机生成的随机小数 k,进行比较大小。</li>\n <li>若 k &lt;= Prob[i] ,那么输出符合期望概率分布的随机数为 i,否则输出的值是 Alias[i] 。</li>\n</ol>\n<p></p>\n<div style=\"margin:0 auto;width:fit-content\">\n <br>\n <img src=\"//misc.aotu.io/Tingglelaoo/alg3_demo.png\" alt=\"alg3_demo.png\">\n</div>\n<br>\n<small style=\"display:block;text-align:center\">对障碍阶砖分布概率应用 Vose’s Alias Method 算法的数组推导过程</small>\n<p></p>\n<blockquote>\n <p>如果有兴趣了解具体详细的算法过程与实现原理,可以阅读 Keith Schwarz 的文章<a href=\"http://www.keithschwarz.com/darts-dice-coins/\" target=\"_blank\" rel=\"external\">《Darts, Dice, and Coins》</a>。</p>\n</blockquote>\n<p>根据 Keith Schwarz 对 Vose’s Alias Method 算法的性能分析,该算法在初始化数组时的时间复杂度始终是 O(n) ,而且随机生成的时间复杂度在 O(1) ,空间复杂度也始终是 O(n) 。</p>\n<p></p>\n<div style=\"margin:0 auto;width:fit-content\">\n <img src=\"//misc.aotu.io/Tingglelaoo/suanfaxingneng.png\" alt=\"suanfaxingneng.png\">\n</div>\n<br>\n<small style=\"display:block;text-align:center\">两种做法的性能比较(引用 Keith Schwarz 的<a href=\"http://www.keithschwarz.com/darts-dice-coins/\" target=\"_blank\" rel=\"external\">分析结果</a>)</small>\n<p></p>\n<p>两种做法对比,明显 Vose’s Alias Method 算法性能更加稳定,更适合非均等概率分布情况复杂,游戏性能要求高的场景。</p>\n<blockquote>\n <p>在 Github 上,@jdiscar 已经对 Vose’s Alias Method 算法进行了很好的实现,你可以到<a href=\"https://github.com/jdiscar/vose-alias-method.js\" target=\"_blank\" rel=\"external\">这里</a>学习。</p>\n</blockquote>\n<p>最后,笔者仍选择一开始的做法,而不是 Vose’s Alias Method 算法。因为考虑到在生成障碍数组的游戏需求场景下,其概率是可控的,它并不需要特别考虑概率分布极端的可能性,并且其代码实现难度低、代码量更少。</p>\n<h3 id=\"根据相对定位确定阶砖位置\" class=\"post-heading\"><a href=\"#根据相对定位确定阶砖位置\" class=\"headerlink\" title=\"根据相对定位确定阶砖位置\"></a>根据相对定位确定阶砖位置<a class=\"post-anchor\" href=\"#根据相对定位确定阶砖位置\" aria-hidden=\"true\"></a></h3>\n<p>利用随机算法生成无障碍数组和障碍数组后,我们需要在游戏容器上进行绘制阶梯,因此我们需要确定每一块阶砖的位置。</p>\n<p>我们知道,每一块无障碍阶砖必然在上一块阶砖的左上方或者右上方,所以,我们对无障碍阶砖的位置计算时可以依据上一块阶砖的位置进行确定。</p>\n<p></p>\n<div style=\"margin:0 auto;width:fit-content\">\n <img src=\"//misc.aotu.io/Tingglelaoo/stairPos.gif\" alt=\"stairPos.gif\">\n</div>\n<br>\n<small style=\"display:block;text-align:center\">无障碍阶砖的位置计算推导</small>\n<p></p>\n<p>如上图推算,除去根据设计稿测量确定第一块阶砖的位置,第n块的无障碍阶砖的位置实际上只需要两个步骤确定:</p>\n<ol>\n <li>第 n 块无障碍阶砖的 x 轴位置为上一块阶砖的 x 轴位置偏移半个阶砖的宽度,若是在左上方则向左偏移,反之向右偏移。</li>\n <li>而其 y 位置则是上一块阶砖的 y 轴位置向上偏移一个阶砖高度减去 26 像素的高度。</li>\n</ol>\n<p>其用伪代码表示如下:<br></p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">// stairSerialNum代表的是在无障碍数组存储的随机方向值</span>\n </div>\n <div class=\"line\">\n direction = stairSerialNum ? \n <span class=\"number\">1</span> : \n <span class=\"number\">-1</span>;\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"comment\">// lastPosX、lastPosY代表上一个无障碍阶砖的x、y轴位置</span>\n </div>\n <div class=\"line\">\n tmpStair.x = lastPosX + direction * (stair.width / \n <span class=\"number\">2</span>);\n </div>\n <div class=\"line\">\n tmpStair.y = lastPosY - (stair.height - \n <span class=\"number\">26</span>);\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<p>接着,我们继续根据障碍阶砖的生成规律,进行如下图所示推算。</p>\n<p></p>\n<div style=\"margin:0 auto;width:fit-content\">\n <img src=\"//misc.aotu.io/Tingglelaoo/barrPos.gif\" alt=\"barrPos.gif\">\n</div>\n<br>\n<small style=\"display:block;text-align:center\">障碍阶砖的位置计算推导</small>\n<p></p>\n<p>可以知道,障碍阶砖必然在无障碍阶砖的反方向上,需要进行反方向偏移。同时,若障碍阶砖的位置相距当前阶砖为 n 个阶砖位置,那么 x 轴方向上和 y 轴方向上的偏移量也相应乘以 n 倍。</p>\n<p>其用伪代码表示如下:<br></p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">// 在无障碍阶砖的反方向</span>\n </div>\n <div class=\"line\">\n oppoDirection = stairSerialNum ? \n <span class=\"number\">-1</span> : \n <span class=\"number\">1</span>;\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"comment\">// barrSerialNum代表的是在障碍数组存储的随机相对距离</span>\n </div>\n <div class=\"line\">\n n = barrSerialNum;\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"comment\">// x轴方向上和y轴方向上的偏移量相应为n倍</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">if</span> barrSerialNum !== \n <span class=\"number\">0</span> \n <span class=\"comment\">// 0 代表没有</span>\n </div>\n <div class=\"line\">\n tmpBarr.x = firstPosX + oppoDirection * (stair.width / \n <span class=\"number\">2</span>) * n, \n </div>\n <div class=\"line\">\n tmpBarr.y = firstPosY - (stair.height - \n <span class=\"number\">26</span>) * n;\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<p>至此,阶梯层完成实现随机生成阶梯。</p>\n<h2 id=\"三、自动掉落阶砖的实现\" class=\"post-heading\"><a href=\"#三、自动掉落阶砖的实现\" class=\"headerlink\" title=\"三、自动掉落阶砖的实现\"></a>三、自动掉落阶砖的实现<a class=\"post-anchor\" href=\"#三、自动掉落阶砖的实现\" aria-hidden=\"true\"></a></h2>\n<p>当游戏开始时,需要启动一个自动掉落阶砖的定时器,定时执行掉落末端阶砖的处理,同时在任务中检查是否有存在屏幕以外的处理,若有则掉落这些阶砖。</p>\n<p>所以,除了机器人碰障碍物、走错方向踩空导致游戏失败外,若机器人脚下的阶砖陨落也将导致游戏失败。</p>\n<p>而其处理的难点在于:</p>\n<ol>\n <li>如何判断障碍阶砖是相邻的或者是在同一 y 轴方向上呢?</li>\n <li>如何判断阶砖在屏幕以外呢?</li>\n</ol>\n<h3 id=\"掉落相邻及同一y轴方向上的障碍阶砖\" class=\"post-heading\"><a href=\"#掉落相邻及同一y轴方向上的障碍阶砖\" class=\"headerlink\" title=\"掉落相邻及同一y轴方向上的障碍阶砖\"></a>掉落相邻及同一y轴方向上的障碍阶砖<a class=\"post-anchor\" href=\"#掉落相邻及同一y轴方向上的障碍阶砖\" aria-hidden=\"true\"></a></h3>\n<p>对于第一个问题,我们理所当然地想到从底层逻辑上的无障碍数组和障碍数组入手:判断障碍阶砖是否相邻,可以通过同一个下标位置上的障碍数组值是否为1,若为1那么该障碍阶砖与当前末端路径的阶砖相邻。</p>\n<p>但是,以此来判断远处的障碍阶砖是否是在同一 y 轴方向上则变得很麻烦,需要对数组进行多次遍历迭代来推算。</p>\n<p>而经过对渲染后的阶梯层观察,我们可以直接通过 y 轴位置是否相等来解决,如下图所示。</p>\n<p></p>\n<div style=\"margin:0 auto;width:fit-content\">\n <img src=\"//misc.aotu.io/Tingglelaoo/autodrop1.png\" alt=\"autodrop1.png\">\n</div>\n<br>\n<small style=\"display:block;text-align:center\">掉落相邻及同一 y 轴方向上的障碍阶砖</small>\n<p></p>\n<p>因为不管是来自相邻的,还是同一 y 轴方向上的无障碍阶砖,它们的 y 轴位置值与末端的阶砖是必然相等的,因为在生成的时候使用的是同一个计算公式。</p>\n<p>处理的实现用伪代码表示如下:<br></p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div>\n <div class=\"line\">\n 12\n </div>\n <div class=\"line\">\n 13\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">// 记录被掉落阶砖的y轴位置值</span>\n </div>\n <div class=\"line\">\n thisStairY = stair.y;\n </div>\n <div class=\"line\"> \n </div>\n <div class=\"line\">\n <span class=\"comment\">// 掉落该无障碍阶砖</span>\n </div>\n <div class=\"line\">\n stairCon.removeChild(stair);\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"comment\">// 掉落同一个y轴位置的障碍阶砖</span>\n </div>\n <div class=\"line\">\n barrArr = barrCon.children;\n </div>\n <div class=\"line\">\n <span class=\"keyword\">for</span> i \n <span class=\"keyword\">in</span> barrArr\n </div>\n <div class=\"line\">\n barr = barrArr[i],\n </div>\n <div class=\"line\">\n thisBarrY = barr.y;\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">if</span> barr.y &gt;= thisStairY \n <span class=\"comment\">// 在同一个y轴位置或者低于</span>\n </div>\n <div class=\"line\">\n barrCon.removeChild(barr);\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<h3 id=\"掉落屏幕以外的阶砖\" class=\"post-heading\"><a href=\"#掉落屏幕以外的阶砖\" class=\"headerlink\" title=\"掉落屏幕以外的阶砖\"></a>掉落屏幕以外的阶砖<a class=\"post-anchor\" href=\"#掉落屏幕以外的阶砖\" aria-hidden=\"true\"></a></h3>\n<p>那对于第二个问题——判断阶砖是否在屏幕以外,是不是也可以通过比较阶砖的 y 轴位置值与屏幕底部y轴位置值的大小来解决呢?</p>\n<p>不是的,通过 y 轴位置来判断反而变得更加复杂。</p>\n<p>因为在游戏中,阶梯会在机器人前进完成后会有回移的处理,以保证阶梯始终在屏幕中心呈现给用户。这会导致阶砖的 y 轴位置会发生动态变化,对判断造成影响。</p>\n<p>但是我们根据设计稿得出,一屏幕内最多能容纳的无障碍阶砖是 9 个,那么只要把第 10 个以外的无障碍阶砖及其相邻的、同一 y 轴方向上的障碍阶砖一并移除就可以了。</p>\n<p></p>\n<div style=\"margin:0 auto;width:fit-content\">\n <img src=\"//misc.aotu.io/Tingglelaoo/autodrop2.png\" alt=\"autodrop2.png\">\n</div>\n<br>\n<small style=\"display:block;text-align:center\">掉落屏幕以外的阶砖</small>\n<p></p>\n<p>所以,我们把思路从视觉渲染层面再转回底层逻辑层面,通过检测无障碍数组的长度是否大于 9 进行处理即可,用伪代码表示如下:<br></p>\n<figure class=\"highlight javascript\">\n <table>\n <tbody>\n <tr>\n <td class=\"gutter\"><pre>\n <div class=\"line\">\n 1\n </div>\n <div class=\"line\">\n 2\n </div>\n <div class=\"line\">\n 3\n </div>\n <div class=\"line\">\n 4\n </div>\n <div class=\"line\">\n 5\n </div>\n <div class=\"line\">\n 6\n </div>\n <div class=\"line\">\n 7\n </div>\n <div class=\"line\">\n 8\n </div>\n <div class=\"line\">\n 9\n </div>\n <div class=\"line\">\n 10\n </div>\n <div class=\"line\">\n 11\n </div></pre></td>\n <td class=\"code\"><pre>\n <div class=\"line\">\n <span class=\"comment\">// 掉落无障碍阶砖</span>\n </div>\n <div class=\"line\">\n stair = stairArr.shift();\n </div>\n <div class=\"line\">\n stair &amp;&amp; _dropStair(stair);\n </div>\n <div class=\"line\"></div>\n <div class=\"line\">\n <span class=\"comment\">// 阶梯存在数量超过9个以上的部分进行批量掉落</span>\n </div>\n <div class=\"line\">\n <span class=\"keyword\">if</span> stairArr.length &gt;= \n <span class=\"number\">9</span>\n </div>\n <div class=\"line\">\n num = stairArr.length - \n <span class=\"number\">9</span>,\n </div>\n <div class=\"line\">\n arr = stairArr.splice(\n <span class=\"number\">0</span>, num);\n </div>\n <div class=\"line\"> \n <span class=\"keyword\">for</span> i = \n <span class=\"number\">0</span> to arr.length\n </div>\n <div class=\"line\">\n _dropStair(arr[i]);\n </div>\n <div class=\"line\">\n }\n </div></pre></td>\n </tr>\n </tbody>\n </table>\n</figure>\n<p></p>\n<p>至此,两个难点都得以解决。</p>\n<h2 id=\"后言\" class=\"post-heading\"><a href=\"#后言\" class=\"headerlink\" title=\"后言\"></a>后言<a class=\"post-anchor\" href=\"#后言\" aria-hidden=\"true\"></a></h2>\n<p>为什么笔者要选择这几点核心内容来剖析呢?<br>因为这是我们经常在游戏开发中经常会遇到的问题:</p>\n<ul>\n <li>怎样处理游戏背景循环?</li>\n <li>有 N 类物件,设第 i 类物件的出现概率为 P(X=i) ,如何实现产生满足这样概率分布的随机变量 X ?</li>\n</ul>\n<p>而且,对于阶梯自动掉落的技术点开发解决,也能够让我们认识到,游戏开发问题的解决可以从视觉层面以及逻辑底层两方面考虑,学会转一个角度思考,从而将问题解决简单化。</p>\n<p>这是本文希望能够给大家在游戏开发方面带来一些启发与思考的所在。最后,还是老话,行文仓促,若错漏之处还望指正,若有更好的想法,欢迎留言交流讨论!</p>\n<p>另外,本文同时发布在<a href=\"https://zhuanlan.zhihu.com/snsgame\" target=\"_blank\" rel=\"external\">「H5游戏开发」专栏</a>,如果你对该方面的系列文章感兴趣,欢迎关注我们的专栏。</p>\n<h2 id=\"参考资料\" class=\"post-heading\"><a href=\"#参考资料\" class=\"headerlink\" title=\"参考资料\"></a>参考资料<a class=\"post-anchor\" href=\"#参考资料\" aria-hidden=\"true\"></a></h2>\n<ul>\n <li><a href=\"http://www.keithschwarz.com/darts-dice-coins/\" target=\"_blank\" rel=\"external\">《Darts, Dice, and Coins》</a></li>\n</ul>\n<div class=\"post-tags\" style=\"display:none\">\n <a href=\"/tags/游戏/\">游戏</a> \n <a href=\"/tags/createjs/\">createjs</a> \n <a href=\"/tags/H5/\">H5</a> \n <a href=\"/tags/随机算法/\">随机算法</a>\n</div>\n<div class=\"post-categories\" style=\"display:none\">\n <a href=\"/cates/H5游戏开发/\">H5游戏开发</a>\n</div>\n<div class=\"post-announce\">\n 感谢您的阅读,本文由 \n <a href=\"//aotu.io\">凹凸实验室</a> 版权所有。如若转载,请注明出处:凹凸实验室(\n <a href=\"https://aotu.io/notes/2017/11/28/h5_game_jumping/\">https://aotu.io/notes/2017/11/28/h5_game_jumping/</a>)\n</div>\n<div class=\"post-revision\">\n <i class=\"fa fa-clock-o\"></i> \n <time class=\"post-updated\" datetime=\"2018-11-07T01:01:15.653Z\">上次更新:2018-11-07 09:01:15</time>\n</div>');
-- ----------------------------
-- Table structure for rumo_category
-- ----------------------------
DROP TABLE IF EXISTS `rumo_category`;
CREATE TABLE `rumo_category` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL DEFAULT '',
`sort` int(11) DEFAULT NULL COMMENT '排序',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of rumo_category
-- ----------------------------
INSERT INTO `rumo_category` VALUES ('1', 'java', '1', '2018-11-07 20:11:56');
INSERT INTO `rumo_category` VALUES ('2', 'javascript', '2', '2018-11-07 20:11:59');
INSERT INTO `rumo_category` VALUES ('3', 'css', '3', '2018-11-07 20:12:04');
INSERT INTO `rumo_category` VALUES ('4', 'Nodejs', '4', '2018-11-07 20:12:08');
INSERT INTO `rumo_category` VALUES ('5', 'Redis', '5', '2018-11-07 20:12:12');
-- ----------------------------
-- Table structure for rumo_comment
-- ----------------------------
DROP TABLE IF EXISTS `rumo_comment`;
CREATE TABLE `rumo_comment` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`ip` varchar(20) NOT NULL DEFAULT '',
`content` varchar(600) NOT NULL DEFAULT '',
`parent_id` int(11) DEFAULT '0',
`replay_userid` int(11) DEFAULT '0',
`status` int(1) DEFAULT '0' COMMENT '0未发布1发布',
`blog_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of rumo_comment
-- ----------------------------
INSERT INTO `rumo_comment` VALUES ('1', '1', '2018-11-11 21:32:30', '127.0.0.1', '没毛病', '0', '0', '1', '28');
INSERT INTO `rumo_comment` VALUES ('2', '2', '2018-11-11 21:34:07', '127.0.0.1', '结局很圆满,很欢乐,和谐。', '0', '0', '1', '28');
INSERT INTO `rumo_comment` VALUES ('3', '2', '2018-11-11 21:34:20', '127.0.0.1', '好', '0', '0', '1', '28');
INSERT INTO `rumo_comment` VALUES ('4', '3', '2018-11-11 21:35:03', '127.0.0.1', '我认同', '1', '1', '1', '28');
INSERT INTO `rumo_comment` VALUES ('5', '3', '2018-11-11 21:35:03', '127.0.0.1', '我认同111', '2', '2', '1', '28');
INSERT INTO `rumo_comment` VALUES ('6', '3', '2018-11-11 21:35:03', '127.0.0.1', '我认同222', '2', '2', '1', '28');
-- ----------------------------
-- Table structure for rumo_user
-- ----------------------------
DROP TABLE IF EXISTS `rumo_user`;
CREATE TABLE `rumo_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(20) NOT NULL DEFAULT '',
`password` varchar(100) NOT NULL DEFAULT '',
`header_pic` varchar(200) NOT NULL DEFAULT '',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of rumo_user
-- ----------------------------
INSERT INTO `rumo_user` VALUES ('1', '秋风微醺', '123556', 'http://upload.jianshu.io/users/upload_avatars/5818439/a6be175c-a93f-44aa-bef5-89d48710030b.jpg?imageMogr2/auto-orient/strip|imageView2/1/w/114/h/114/format/webp', '2018-11-11 21:30:52');
INSERT INTO `rumo_user` VALUES ('2', '再抢救一下', '1234567', 'http://upload.jianshu.io/users/upload_avatars/13904007/d0b1b3a6-8289-420e-b1e0-111c9dd485ca.png?imageMogr2/auto-orient/strip|imageView2/1/w/114/h/114/format/webp', '2018-11-11 21:31:08');
INSERT INTO `rumo_user` VALUES ('3', 'keke', '1343245', 'http://cdn2.jianshu.io/assets/default_avatar/8-a356878e44b45ab268a3b0bbaaadeeb7.jpg', '2018-11-11 21:34:44');
Java
1
https://gitee.com/kekesam/rumo-blog.git
git@gitee.com:kekesam/rumo-blog.git
kekesam
rumo-blog
rumo-blog
master

搜索帮助