From 97ced02827c0172c03d18298bc5c77feb8052abb Mon Sep 17 00:00:00 2001 From: Freya Yu Date: Tue, 13 Dec 2016 15:25:11 +0800 Subject: [PATCH 01/38] Translate Version 1.0 --- ...uild-a-spritekit-game-in-swift-3-part-2.md | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md b/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md index 0ea6de42d4a..2672659752e 100644 --- a/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md +++ b/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md @@ -2,31 +2,31 @@ > * 原文地址:[ How To Build A SpriteKit Game In Swift 3 (Part 2) ](https://www.smashingmagazine.com/2016/12/how-to-build-a-spritekit-game-in-swift-3-part-2/ ) * 原文作者:[ Marc Vandehey ]( https://www.smashingmagazine.com/author/marcvandehey/) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) -* 译者: +* 译者:[ZiXYu](https://github.com/ZiXYu) * 校对者: -## [How To Build A SpriteKit Game In Swift 3 (Part 2)](https://www.smashingmagazine.com/2016/12/how-to-build-a-spritekit-game-in-swift-3-part-2/) ## +## [ 如何在 Swift 3 中用 SpriteKit 框架编写游戏 (Part 2)](https://www.smashingmagazine.com/2016/12/how-to-build-a-spritekit-game-in-swift-3-part-2/) ## -Have you ever wondered what it takes to create a [SpriteKit](https://developer.apple.com/spritekit/)[1](#1) game? Does collision detection seem like a daunting task? Do you want to know how to properly handle sound effects and background music? Game-making has never been easier on iOS since the introduction of SpriteKit. In part two of this three-part series, we will explore the basics of SpriteKit. +你是否曾经想过要创建一个  [SpriteKit](https://developer.apple.com/spritekit/)[1](#1) 游戏?碰撞检测看起来像是个令人生畏的任务吗?你想知道如何正确的处理音效和背景音乐吗?自从 SpriteKit 推出以来,在 iOS 上的游戏制作从未看起来如此简单过。本文中,我们将探索 SpriteKit 的基础使用。 -If you missed out on the [previous lesson](https://www.smashingmagazine.com/2016/11/how-to-build-a-spritekit-game-in-swift-3-part-1/)[2](#2), you can catch up by getting the [code on GitHub](https://github.com/thirteen23/RainCat/releases/tag/smashing-magazine-lesson-one)[3](#3). Remember that this tutorial requires Xcode 8 and Swift 3. +如果你错过了[之前的课程](https://www.smashingmagazine.com/2016/11/how-to-build-a-spritekit-game-in-swift-3-part-1/)[2](#2),你可以通过获取[ Github 上的代码](https://github.com/thirteen23/RainCat/releases/tag/smashing-magazine-lesson-one)[3](#3)来赶上进度。请记住,本教程需要使用 Xcode 8 和 Swift 3。 [![Raincat: Lesson 2](https://www.smashingmagazine.com/wp-content/uploads/2016/10/raincat_header_sm-preview-opt.png) ](https://www.smashingmagazine.com/wp-content/uploads/2016/10/raincat_header_sm-preview-opt.png)[4](#4) -RainCat, lesson 2 +RainCat, 第二课 -In the [last lesson](https://www.smashingmagazine.com/2016/11/how-to-build-a-spritekit-game-in-swift-3-part-1/)[5](#5), we created a floor and a background, randomly generated raindrops and added the umbrella. The umbrella sprite has a custom `SKPhysicsBody`, generated with a `CGPath`, and we enabled touch detection so that we could move it around the screen. We hooked up collision detection by leveraging `categoryBitMask` and `contactTestBitMask`. We removed collision on raindrops when they hit anything, so that they don’t pile up, but rather fall through the floor after one bounce. Finally, we set up a world frame to remove any `SKNode` that comes into contact with it. +在 [上一课](https://www.smashingmagazine.com/2016/11/how-to-build-a-spritekit-game-in-swift-3-part-1/)[5](#5), 我们创建了地板和背景,随机生成了雨滴并添加了雨伞。这把雨伞的精灵(译者注:sprite,中文译名精灵,在游戏开发中,精灵指的是以图像方式呈现在屏幕上的一个图像)中存在一个自定义的 `SKPhysicsBody`,是通过 `CGPath` 来生成的,同时我们启用了触摸检测,因此我们可以在屏幕范围内移动它。我们使用了 `categoryBitMask` 和 `contactTestBitMask` 来链接了碰撞检测。我们移除了当雨滴落到任何物体上时的碰撞,因此它们不会堆积起来,而是在一次弹跳后穿过地板。最后,我们设置了一个世界的边框来移除所有和它接触的 `SKNode`。 -Today, we will focus on the following: +本文中,我们将重点实现以下几点: -- Spawn the cat. -- Implement cat collision. -- Spawn food. -- Implement food collision. -- Move the cat toward the food. -- Animate the cat. -- “Damage” the cat when it comes into contact with the rain. -- Add sound effects and music. +- 生成猫 +- 实现猫的碰撞 +- 生成食物 +- 实现食物的碰撞 +- 将猫移向食物 +- 创建猫的动画 +- 当猫接触雨滴时,使猫受到伤害 +- 添加音效和背景音乐 ### Get The Assets Again From 041c5246a17c293edcf17f2f64b7eb276ef04901 Mon Sep 17 00:00:00 2001 From: Freya Yu Date: Tue, 13 Dec 2016 17:31:25 +0800 Subject: [PATCH 02/38] Translate Version 1.1 --- ...uild-a-spritekit-game-in-swift-3-part-2.md | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md b/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md index 2672659752e..33a5c854a62 100644 --- a/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md +++ b/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md @@ -29,18 +29,21 @@ RainCat, 第二课 - 添加音效和背景音乐 ### Get The Assets Again +### 重新获取资源 You can get the assets needed for this lesson [on GitHub](https://github.com/thirteen23/RainCat/blob/smashing-day-2/dayTwoAssets.zip)[6](#6) (ZIP). Download and add the images to your `Assets.xcassets` file by dragging them all in at once. You should now have an asset for the cat animation and the food dish. We will add in the sound effects and music later on. +你可以从 [GitHub](https://github.com/thirteen23/RainCat/blob/smashing-day-2/dayTwoAssets.zip)[6](#6) (ZIP) 上获取本课所需要的资源。下载图片后,通过一次性拖拽所有图片将它们添加到你的 `Assets.xcassets` 文件中。你现在应该有了包含猫动画和宠物碗的资源文件。我们之后将会添加音效和背景音乐文件。 -[![App assets](https://www.smashingmagazine.com/wp-content/uploads/2016/10/App-assets-preview-opt-1.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/App-assets-large-opt.png)[7](#7) +[![App 资源](https://www.smashingmagazine.com/wp-content/uploads/2016/10/App-assets-preview-opt-1.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/App-assets-large-opt.png)[7](#7) -A lot of assets! ([View large version](https://www.smashingmagazine.com/wp-content/uploads/2016/10/App-assets-large-opt.png)[8](#8)) -### Cat Time! +一大堆资源! ([查看源版本](https://www.smashingmagazine.com/wp-content/uploads/2016/10/App-assets-large-opt.png)[8](#8)) -We begin the lesson by adding the protagonist of the game. Start out by creating a new file under the “Sprites” group, named `CatSprite`. +### 猫猫时间! -Update the code in `CatSprite.swift` to this: +我们从添加游戏主角开始本期课程。我们首先在 “Sprites” 组下创建一个新文件,命名为 `CatSprite`。 + +更新 `CatSprite.swift` 中的代码如下: ``` import SpriteKit @@ -61,24 +64,24 @@ public class CatSprite : SKSpriteNode { } ``` -We’ve stubbed out the file with a static initializer that returns a generated cat sprite. We’ve also stubbed out another `update` function. If we need to update more sprites, we should look into making this function a part of a [protocol](https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Protocols.html)[9](#9) for our sprites to conform to. One thing to note here: For the cat sprite, we are using a circular `SKPhysicsBody`. We could use the texture to create the physics body, as we did with the raindrops, but this is an “artistic” decision. When the cat is hit by the raindrops or the umbrella, it would be much more amusing for the cat to roll around in a fit of despair, instead of just sitting still. +我们已经使用了一个会返回猫精灵的静态初始化器来处理这些文件。我们也同样处理了另一个 `update` 函数。如果我们需要更新更多的精灵,我们应该尝试把这个函数变成一个[协议](https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Protocols.html)[9](#9)的一部分来让我们的精灵能够符合要求。这里需要注意一点,对于猫精灵,我们使用了一个圆形的 `SKPhysicsBody`。我们可以使用纹理来创建猫的物理身体,就像我们创建雨滴一样,但是这是一个“艺术”的决定。当猫被雨滴打中时, 与其让猫始终坐着,让猫痛苦地打滚显然更有趣一些。 -We will need callbacks for when the cat comes into contact with the rain or falls off the world. Moving to the `Constants.swift` file, we can add the following line to it, to act as a `CatCategory`: +当猫接触雨滴或猫掉出该世界时,我们将需要回调函数来处理这些事件。我们可以打开 `Constants.swift` 文件,将下列代码加入该文件,使它作为一个 `CatCategory`: ``` let CatCategory : UInt32 = 0x1 << 4 ``` -The code above will be the constant used to determine which `SKPhysicsBody` is the cat. Let’s move back to `CatSprite.swift` and update the sprite to include `categoryBitMask` and `contactTestBitMask`. Add the following lines before we return the `catSprite` in `newInstance()`: +上面代码中定义的变量将决定猫的身体是哪个`SKPhysicsBody`。让我们重新打开 `CatSprite.swift` 来更新猫精灵,使它包含 `categoryBitMask` 和 `contactTestBitMask`。 在 `newInstance()` 返回 `catSprite` 之前,我们需要添加如下代码: ``` catSprite.physicsBody?.categoryBitMask = CatCategory catSprite.physicsBody?.contactTestBitMask = RainDropCategory | WorldCategory ``` -Now, we will get a callback when the cat is hit by rain or when it comes into contact with the edge of the world. After adding this, we need to add the cat to the scene. +现在,当猫被雨滴击中或者当猫跌出世界时,我们将会得到一个回调。在添加了如上代码后,我们需要将猫添加到场景中。 -At the top of `GameScene.swift`, below where we initialized `umbrellaSprite`, add the following line: +在 `GameScene.swift` 文件的顶部, 在初始化了 `umbrellaSprite` 之后, 我们需要添加如下代码: ``` private var catNode : CatSprite! From b562221543f82a8a1df9536e69e5135026171d6f Mon Sep 17 00:00:00 2001 From: Freya Yu Date: Wed, 14 Dec 2016 17:28:48 +0800 Subject: [PATCH 03/38] Translate Version 1.2 --- ...uild-a-spritekit-game-in-swift-3-part-2.md | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md b/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md index 33a5c854a62..9490d7577e4 100644 --- a/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md +++ b/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md @@ -72,7 +72,7 @@ public class CatSprite : SKSpriteNode { let CatCategory : UInt32 = 0x1 << 4 ``` -上面代码中定义的变量将决定猫的身体是哪个`SKPhysicsBody`。让我们重新打开 `CatSprite.swift` 来更新猫精灵,使它包含 `categoryBitMask` 和 `contactTestBitMask`。 在 `newInstance()` 返回 `catSprite` 之前,我们需要添加如下代码: +上面代码中定义的变量将决定猫的身体是哪个 `SKPhysicsBody` 。让我们重新打开 `CatSprite.swift` 来更新猫精灵,使它包含 `categoryBitMask` 和 `contactTestBitMask`。 在 `newInstance()` 返回 `catSprite` 之前,我们需要添加如下代码: ``` catSprite.physicsBody?.categoryBitMask = CatCategory @@ -81,17 +81,17 @@ catSprite.physicsBody?.contactTestBitMask = RainDropCategory | WorldCategory 现在,当猫被雨滴击中或者当猫跌出世界时,我们将会得到一个回调。在添加了如上代码后,我们需要将猫添加到场景中。 -在 `GameScene.swift` 文件的顶部, 在初始化了 `umbrellaSprite` 之后, 我们需要添加如下代码: +在 `GameScene.swift` 文件的顶部, 初始化了 `umbrellaSprite` 之后, 我们需要添加如下代码: ``` private var catNode : CatSprite! ``` -We can create a cat right away in `sceneDidLoad()`, but we want to spawn the cat from a separate function in order to reuse the code. The exclamation point (`!`) tells the compiler that it does not need to be initialized in an `init` statement, and that it probably will not be `nil`. We do this for two reasons. First, we don’t want to include an `init()` statement for only one variable. Secondly, we don’t want to initialize it right away, only to reinitialize and position the cat when we hit `spawnCat()` the first time. We could have it as an optional (`?`), but after we hit the `spawnCat()` function the first time, our cat sprite will never be `nil` again. To get around this and the headache of unwrapping it every time we want to use it, we’ll say that it is safe to automatically unwrap using the exclamation pont. If we touch our cat object before initializing it, our app would crash, because we told it that it was safe to unwrap when it wasn’t. We need to initialize it before using it, but in the proper function. +我们可以立刻在 `sceneDidLoad()` 里创建一只猫,但是我们更想要从一个单独的函数中来创建猫对象,以便于代码重用。感叹号(`!`)告诉编译器,它并不需要在 `init` 语句中立即初始化,而且它应该不会是 `nil`。我们这么做有两个理由。首先,我们不想单独为了一个变量创建 `init()` 语句。其次,我们并不想立刻初始化它,只要在我们第一次运行 `spawnCat()` 时重新初始化和定位猫对象就可以了。我们也可以用 optional(`?`) 来定义该变量,但是当我们第一次运行了 `spawnCat()` 函数后,我们的猫精灵就再也不会变成 `nil` 了。为了解决初始化问题和让我们头疼的拆包,我们会说使用感叹号来进行自动拆包是安全的操作。如果我们在初始化我们的猫对象前就使用了它,我们的应用就会闪退,因为我们告诉我们的应用对我们的猫对象进行拆包是安全的,然而并不是。我们需要在使用它之前初始化它,但是是在合适的函数中。 -Next, we will create a `spawnCat()` function in `GameScene.swift`, so that we can initialize our cat sprite. We’ll break this out into its own function to be able to reuse the code and to make sure that only one cat is in the scene at a time. +接下来,我们将要在 `GameScene.swift` 文件中新建一个 `spawnCat()` 函数来初始化我们的猫精灵。我们会把这个初始化的部分拆分到一个单独的函数中,使这部分代码具有重用性,同时保证在一个场景中一次只可能存在一只猫。 -Add the following code near the bottom of the file, just under our `spawnRaindrop()` function: +在这个文件中接近底部的地方,我们的 `spawnRaindrop()` 函数后面添加如下代码: ``` func spawnCat() { @@ -108,9 +108,9 @@ func spawnCat() { } ``` -Walking through this function, we’re first checking whether the cat is not `nil`. Then, we’re checking whether the scene contains a cat. If the scene does contain a cat, we remove it from the parent, remove any actions that the cat is currently performing, and clear out the `SKPhysicsBody` of the cat. This will only trigger if the cat comes into contact with the edge of the game world. After this, we initialized a new cat and set the position to 30 pixels below the umbrella. We could spawn the cat anywhere, but I thought that this would be a clever place, instead of dropping the cat from the sky. +纵观这段函数,我们首先检查了猫对象是否为空。然后,我们确定了这个场景中是否存在一个猫对象。如果这个场景内存在一只猫,我们就要从父类中移除它,移除它现在正在进行的所有操作,并清除这个猫对象的 `SKPhysicsBody` 。这些操作仅仅会在猫掉出该世界时被触发。在这之后,我们会重新初始化一个新的猫对象,同时设定它的初始位置为伞下 30 像素的地方。其实我们可以在任何位置初始化我们的猫对象,但是我想这个位置总比直接从天空中把猫丢下来更好。 -Finally, in `sceneDidLoad()`, after we position and add the umbrella, call the `spawnCat()` function: +最后,在 `sceneDidLoad()` 函数中,在我们定位并添加了雨伞之后,调用 `spawnCat()` 函数: ``` umbrellaNode.zPosition = 4 @@ -119,15 +119,15 @@ addChild(umbrellaNode) spawnCat() ``` -Now we can run the app, and voilà! +现在我们可以运行我们的应用并且欢呼啦! -[![App assets](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Cat-preview-opt.png) ](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Cat-large-opt.png)[10](#10) +[![应用资源](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Cat-preview-opt.png) ](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Cat-large-opt.png)[10](#10) -Cat ([View large version](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Cat-large-opt.png)[11](#11)) +猫 ([查看源文件](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Cat-large-opt.png)[11](#11)) -If the cat now comes into contact with raindrops or the umbrella, it will roll around. At this point, the cat can roll off screen and would be deleted as soon as it hits the world edge; so, we will need to respawn the cat. Because a callback is currently in place for when the cat comes into contact with raindrops and the game world, we can handle the collision in the `didBegin(_ contact:)` function. +如果现在猫走出了雨伞并且碰到了雨滴,它将会在地上打滚。这时候,猫可能会滚出屏幕然后在接触世界边框的一瞬间被删除掉,那么,我们就需要重新生成猫对象了。因为现在回调函数会在当猫接触到雨滴时或猫掉出世界时被触发,所以我们可以在 `didBegin(_ contact:)` 函数中来处理这个碰撞事件。 -We want to differentiate between when a raindrop hits the cat and when the cat hits the world frame, so let’s break out the logic into a new function. At the bottom of `GameScene.swift`, below `didBegin(_ contact:)`, enter the code below: +我们想要在猫触碰到雨滴后和触碰世界边框后触发不同的事件,所以我们把这些逻辑拆分到了一个新的函数中。在 `GameScene.swift` 文件的底部, `didBegin(_ contact:)` 函数的后面,加上如下代码: ``` func handleCatCollision(contact: SKPhysicsContact) { @@ -150,9 +150,9 @@ func handleCatCollision(contact: SKPhysicsContact) { } ``` -In this block of code, we are looking for the physics body that is not the cat. Once we have the other body, we need to figure out what hit the cat. For now, if a raindrop hits the cat, we just print out to the console that the collision occurred. If the cat hits the edge of the game world, we will respawn the cat. +在这段代码中,我们在寻找除了猫以外的物理对象。一旦我们获取到了另外的物理对象,我们需要判断是什么触碰了猫。现在,如果是雨滴击中了猫,我们只在控制台中输出这个碰撞发生了,而如果是猫触碰了这个游戏世界的边缘,我们就会重新生成一个猫对象。 -We need to call this function if contact occurs with the cat object. So, let’s update `didBegin(_ contact:)` to the following code: +我们需要在有触碰发生猫对象上时调用这个函数。那么,让我们用如下代码来更新 `didBegin(_ contact:)` 函数: ``` func didBegin(_ contact: SKPhysicsContact) { @@ -180,19 +180,19 @@ func didBegin(_ contact: SKPhysicsContact) { } ``` -We snuck in a condition between where we remove collision from raindrops and where we cull off-screen nodes. This `if` statement checks whether either body is the cat, and then we handle the cat behaviors in the `handleCatCollision(contact:)` function. +我们在移除雨滴碰撞和移除离屏指针中间插入了一个条件判断。这个 `if` 语句判断了碰撞物体是不是猫,然后我们在 `handleCatCollision(contact:)` 函数中处理猫的行为。 -We can now test our cat’s respawning function by pushing the cat off screen with the umbrella. The cat will now respawn below the umbrella! Note that if the umbrella’s bottom is beneath the floor, the cat will fall off screen for as long as the umbrella stays beneath the floor. This isn’t a huge issue for now, but we should figure out a way around this later on. +我们现在可以用雨伞把猫推出屏幕来测试猫的重生函数了。猫现在将会在伞下重新被定义出来。请注意,如果雨伞的底部低于地板,那么猫就会一直从屏幕中掉出去。到现在为止这并不是什么大问题,但是我们之后会提供一个方法来解决它。 -### Spawning Food +### 生成食物 -Now seems like a good time to spawn some food for our cat to eat. Sure, the cat can’t move by itself, but we can fix that later. Before creating the food sprite, we can add a new category for food in our `Constants.swift` file. Add the following line below the `CatCategory`. +现在看来,是时候生成一些食物来喂我们的小猫了。当然了,现在猫并不能自己移动,不过我们一会可以修复这个问题。在创建食物精灵之前,我们可以先在 `Constants.swift` 文件中为食物新建一个类。让我们在 `CatCategory` 中添加如下代码: ``` let FoodCategory : UInt32 = 0x1 << 5 ``` -The code above will be the constant used to determine which `SKPhysicsBody` is the food. Create a file exactly how we created the `CatSprite.swift` file, but this time named `FoodSprite.swift`, under the “Sprites” group. Add the following code to the new file: +上面代码中定义的变量将决定食物的物理对象是哪个 `SKPhysicsBody` 。在 “Sprites” 组中,我们用创建 `CatSprite.swift` 文件图片同样的方法新建一个名为 `FoodSprite.swift` 的文件,并在该文件中添加如下代码: ``` import SpriteKit @@ -211,14 +211,15 @@ public class FoodSprite : SKSpriteNode { } ``` -This is a static function that, when called, will initialize a `FoodSprite` and return it. We set the physics body to a rectangle of the sprite’s size. This is fine because the sprite itself is a rectangle. Then, we set the category to the `FoodCategory` that we just created, and added it to the object that we want collisions with (the world frame, the raindrops and the cat). The `zPosition`s of the food and the cat are the same, and they’ll never overlap, because as soon as they come into contact, the food will be deleted and the player will earn a point. +这是一个静态的类,当它被调用时,将会初始化一个 `FoodSprite` 并且返回它。我们把食物的物理对象设置为一个和食物精灵同样大小的矩形。这种处理很好,因为食物精灵本身就是一个矩形。接下来,我们把物理对象的种类设置为我们刚刚创建的 `FoodCategory` ,然后把它添加到它可能会碰撞的对象(世界边框,雨滴和猫)中。我们把食物和猫的 `zPosition` 设置成相同的,它们将永远不会重叠,因为当它们相遇时,食物就会被删除然后用户将会得到一分。 -Heading back to `GameScene.swift`, we will need to add functionality to spawn and remove the food. At the top of the file, beneath the `rainDropSpawnRate` variable, we can add this line: +重新打开 `GameScene.swift` 文件,我们需要添加一些功能来生成和移除食物。在这个文件的顶部,`rainDropSpawnRate` 变量的下面,我们添加如下代码: ``` private let foodEdgeMargin : CGFloat = 75.0 ``` +This will act as a margin when spawning food. We don’t want to spawn it too close to either the left or right side of the screen. We place it at the top of the file, so that we won’t have to search all over to change this value if we need to later on. Next, below our `spawnCat()` function, we can add our `spawnFood` function: This will act as a margin when spawning food. We don’t want to spawn it too close to either the left or right side of the screen. We place it at the top of the file, so that we won’t have to search all over to change this value if we need to later on. Next, below our `spawnCat()` function, we can add our `spawnFood` function: ``` From 5397a809d5dcd2c16e3d0a9e64518d5d00b4e15c Mon Sep 17 00:00:00 2001 From: Freya Yu Date: Thu, 15 Dec 2016 17:29:37 +0800 Subject: [PATCH 04/38] Translate Version 1.3 --- ...uild-a-spritekit-game-in-swift-3-part-2.md | 46 +++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md b/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md index 9490d7577e4..984bfefad63 100644 --- a/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md +++ b/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md @@ -28,10 +28,8 @@ RainCat, 第二课 - 当猫接触雨滴时,使猫受到伤害 - 添加音效和背景音乐 -### Get The Assets Again ### 重新获取资源 -You can get the assets needed for this lesson [on GitHub](https://github.com/thirteen23/RainCat/blob/smashing-day-2/dayTwoAssets.zip)[6](#6) (ZIP). Download and add the images to your `Assets.xcassets` file by dragging them all in at once. You should now have an asset for the cat animation and the food dish. We will add in the sound effects and music later on. 你可以从 [GitHub](https://github.com/thirteen23/RainCat/blob/smashing-day-2/dayTwoAssets.zip)[6](#6) (ZIP) 上获取本课所需要的资源。下载图片后,通过一次性拖拽所有图片将它们添加到你的 `Assets.xcassets` 文件中。你现在应该有了包含猫动画和宠物碗的资源文件。我们之后将会添加音效和背景音乐文件。 [![App 资源](https://www.smashingmagazine.com/wp-content/uploads/2016/10/App-assets-preview-opt-1.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/App-assets-large-opt.png)[7](#7) @@ -64,7 +62,7 @@ public class CatSprite : SKSpriteNode { } ``` -我们已经使用了一个会返回猫精灵的静态初始化器来处理这些文件。我们也同样处理了另一个 `update` 函数。如果我们需要更新更多的精灵,我们应该尝试把这个函数变成一个[协议](https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Protocols.html)[9](#9)的一部分来让我们的精灵能够符合要求。这里需要注意一点,对于猫精灵,我们使用了一个圆形的 `SKPhysicsBody`。我们可以使用纹理来创建猫的物理身体,就像我们创建雨滴一样,但是这是一个“艺术”的决定。当猫被雨滴打中时, 与其让猫始终坐着,让猫痛苦地打滚显然更有趣一些。 +我们已经使用了一个会返回猫精灵的静态初始化器来处理这些文件。我们也同样处理了另一个 `update` 函数。如果我们需要更新更多的精灵,我们应该尝试把这个函数变成一个[协议](https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Protocols.html)[9](#9)的一部分来让我们的精灵能够符合要求。这里需要注意一点,对于猫精灵,我们使用了一个圆形的 `SKPhysicsBody`。我们可以使用纹理来创建猫的物理实体,就像我们创建雨滴一样,但是这是一个“艺术”的决定。当猫被雨滴打中时, 与其让猫始终坐着,让猫痛苦地打滚显然更有趣一些。 当猫接触雨滴或猫掉出该世界时,我们将需要回调函数来处理这些事件。我们可以打开 `Constants.swift` 文件,将下列代码加入该文件,使它作为一个 `CatCategory`: @@ -150,7 +148,7 @@ func handleCatCollision(contact: SKPhysicsContact) { } ``` -在这段代码中,我们在寻找除了猫以外的物理对象。一旦我们获取到了另外的物理对象,我们需要判断是什么触碰了猫。现在,如果是雨滴击中了猫,我们只在控制台中输出这个碰撞发生了,而如果是猫触碰了这个游戏世界的边缘,我们就会重新生成一个猫对象。 +在这段代码中,我们在寻找除了猫以外的物理实体。一旦我们获取到了另外的实体对象,我们需要判断是什么触碰了猫。现在,如果是雨滴击中了猫,我们只在控制台中输出这个碰撞发生了,而如果是猫触碰了这个游戏世界的边缘,我们就会重新生成一个猫对象。 我们需要在有触碰发生猫对象上时调用这个函数。那么,让我们用如下代码来更新 `didBegin(_ contact:)` 函数: @@ -211,7 +209,7 @@ public class FoodSprite : SKSpriteNode { } ``` -这是一个静态的类,当它被调用时,将会初始化一个 `FoodSprite` 并且返回它。我们把食物的物理对象设置为一个和食物精灵同样大小的矩形。这种处理很好,因为食物精灵本身就是一个矩形。接下来,我们把物理对象的种类设置为我们刚刚创建的 `FoodCategory` ,然后把它添加到它可能会碰撞的对象(世界边框,雨滴和猫)中。我们把食物和猫的 `zPosition` 设置成相同的,它们将永远不会重叠,因为当它们相遇时,食物就会被删除然后用户将会得到一分。 +这是一个静态的类,当它被调用时,将会初始化一个 `FoodSprite` 并且返回它。我们把食物的物理实体设置为一个和食物精灵同样大小的矩形。这种处理很好,因为食物精灵本身就是一个矩形。接下来,我们把物理对象的种类设置为我们刚刚创建的 `FoodCategory` ,然后把它添加到它可能会碰撞的对象(世界边框,雨滴和猫)中。我们把食物和猫的 `zPosition` 设置成相同的,它们将永远不会重叠,因为当它们相遇时,食物就会被删除然后用户将会得到一分。 重新打开 `GameScene.swift` 文件,我们需要添加一些功能来生成和移除食物。在这个文件的顶部,`rainDropSpawnRate` 变量的下面,我们添加如下代码: @@ -219,8 +217,7 @@ public class FoodSprite : SKSpriteNode { private let foodEdgeMargin : CGFloat = 75.0 ``` -This will act as a margin when spawning food. We don’t want to spawn it too close to either the left or right side of the screen. We place it at the top of the file, so that we won’t have to search all over to change this value if we need to later on. Next, below our `spawnCat()` function, we can add our `spawnFood` function: -This will act as a margin when spawning food. We don’t want to spawn it too close to either the left or right side of the screen. We place it at the top of the file, so that we won’t have to search all over to change this value if we need to later on. Next, below our `spawnCat()` function, we can add our `spawnFood` function: +这个变量将会作为生成食物时的外边距。我们不想将食物生成在离屏幕两侧特别近的位置。我们把这个值定义在文件的顶部,这样如果我们之后可能要改变这个值的时候就不用搜索整个文档了。接下来,在我们的 `spawnCat()` 函数下面,我们可以新增我们的 `spawnFood` 函数了。 ``` func spawnFood() { @@ -235,18 +232,18 @@ func spawnFood() { } ``` -This function acts almost exactly like our `spawnRaindrop()` function. We create a new `FoodSprite`, then place it in a random `x` location on the screen. We use the margin variable that we set earlier to shrink the amount of screen space where the food sprite can spawn. First, we get a random location from 0 to the width of the screen, minus the margin, multiplied by 2. Then, we offset the start by the margin. This prevents food from spawning anywhere between 0 and 75 from each side of the screen. +这个函数和我们的 `spawnRaindrop()` 函数几乎一模一样。我们新建了一个 `FoodSprite`,然后把它放在了屏幕上一个随机的位置 `x`。我们用了之前设定的外边距变量来限制了能够生成食物精灵的屏幕范围。首先,我们设置了随机的位置的范围为屏幕的宽度减去 2 乘以外边距。然后,我们用外边距来偏移起始位置。这使得食物不会生成在距屏幕边界 0 到 75 的任意位置。 -Near the top of the file in `sceneDidLoad()`, let’s add the following code beneath our initial call of `spawnCat()`: +在 `sceneDidLoad()` 文件接近顶部的位置,让我们在 `spawnCat()` 函数的初始化调用下面加上如下代码: ``` spawnCat() spawnFood() ``` -Now when the scene loads, we will spawn an umbrella, a cat from the umbrella, some raindrops and food falling from the sky. Currently, the rain can interact with the food, possibly moving it around. The rain will act in the same way relative to the food as the umbrella and the floor, bouncing once and then losing all of its collision until it interacts with the edge of the game world and is deleted. We also need some interaction between the food and the cat. +现在当场景加载时,我们会生成一把雨伞,雨伞下一只猫,还有一些从天上掉下来的雨滴和食物。现在雨滴可以和猫互动,还可以让它来回滚动。对于雨伞和地板,食物将和雨滴有一样的碰撞效果,反弹一次然后失去所有的碰撞属性,直到触碰到世界边界后被删除。我们也同样需要添加一些食物和猫的互动。 -At the bottom of the file in `GameScene.swift`, we will add our code for the food collision. Add the following below `handleCatCollision()`: +在 `GameScene.swift` 文件的底部,我们将添加所有有关于食物碰撞的代码。让我们在 `handleCatCollision()` 函数后添加如下代码: ``` func handleFoodHit(contact: SKPhysicsContact) { @@ -277,9 +274,9 @@ func handleFoodHit(contact: SKPhysicsContact) { } ``` -With this function, we will handle the food’s collision in the same way we handle the cat’s collision. First, we identify which physics body is the food, and then we run a `switch` statement on the other body. Then, we add cases for `CatCategory` — this is a stub so that we can later update our score. Then we `fallthrough` to hit the `WorldFrameCategory`, where we remove the food sprite from the scene, and also the `physicsBody` associated with it. Finally, we spawn the food again. When it hits the world boundary, we just remove the sprite and `physicsBody`. If anything else is detected, our default case will trigger and send a generic statement to the console. Currently, the only thing that can hit this statement is `RainDropCategory`. As of now, we don’t care what happens when the rain hits the food. We just want the rain to act the same as it does when it hits the floor or the umbrella. +在这个函数中,我们将用和处理猫的碰撞同样的方式来处理食物的碰撞。首先,我们定义了食物的物理实体,然后我们运行了一个 `switch` 语句来判断除食物之外的物理实体。接下来,我们为 `CatCategory` 创建一个新的分支语句 - 这是个预留的接口,我们之后可以来更新我们的代码。接下来我们 `fallthrough` 到 `WorldFrameCategory` 分支语句,这里我们需要从场景里移除食物精灵和它的物理实体。最后,我们需要重新生成食物。当食物触碰到了世界边界,我们只需要移除食物精灵和它的物理实体。如果食物触碰到了其它物理实体,那么 default 分支语句就会被触发然后在控制台打印一个通用语句。现在,唯一能触发这个语句的物理实体就是 `RainDropCategory`。而到现在为止,我们并不关心当雨击中食物时会发生什么。我们只希望雨滴和食物在击中地板或雨伞时有同样的表现。 -To get everything wired up, we will add in a few lines to `didBegin(_ contact)`. Add the following code above the check for `CatCategory`: +为了让所有部分连接起来,我们将在 `didBegin(_ contact)` 函数中添加几行代码。在判断 `CatCategory` 之前添加如下代码: ``` if contact.bodyA.categoryBitMask == FoodCategory || contact.bodyB.categoryBitMask == FoodCategory { @@ -288,7 +285,7 @@ if contact.bodyA.categoryBitMask == FoodCategory || contact.bodyB.categoryBitMas } ``` -The final code for `didBegin(_ contact)` should look like the following code snippet: +`didBegin(_ contact)` 最后应该看起来像下面这个代码片段: ``` func didBegin(_ contact: SKPhysicsContact) { @@ -322,21 +319,21 @@ func didBegin(_ contact: SKPhysicsContact) { } ``` -Let’s run the app again. The cat won’t be running anywhere right now, but we can test our method by pushing the cat food off screen or by rolling the cat onto the cat food. Either case should now delete the food node, and one should respawn from off screen. +我们再次运行我们的应用。猫现在还不会自己跑来跑去,但是我们可以通过把食物推出屏幕边界或把猫移动到食物上来测试我们的函数。两个情况都会删除食物节点,而其中一个情况则会从屏幕外重新生成食物。 -### Let’s Get Moving +### 让物理实体动起来吧 -Now it’s time to get this kitty moving. What motivates RainCat to move? Food, of course! We just spawned food, and now we need to get the cat to move towards it. Currently, our food sprite is added to the scene and forgotten about. We need to fix this. If we keep a reference to the food, we can know its location at any given time, and thus we can tell the cat where the food is in the scene. The cat can know where it is in the scene by checking its position. With all of this information in place, we can now move the cat towards the food. +现在是时候让我们的小猫动起来了。是什么驱使了小猫移动呢?当然是食物啦!我们刚刚生成了食物,那么现在我们就需要让小猫向着食物移动啦。 现在我们的食物精灵被添加到了场景中,然后就被遗忘了。我们需要修正这个问题。如果我们能够保留食物的引用,我们就可以知道它在任何时候的位置,这样我们就可以告诉小猫食物在场景的哪个位置了。小猫可以通过检查自己的坐标来了解自己在场景中的哪个位置。有了这些位置信息,我们就可以让小猫向着食物移动了。 -Going back to `GameScene.swift`, let’s add a variable to the top of the file, beneath our local cat variable: +重新打开 `GameScene.swift` 文件,让我们在文件的顶部,猫变量的下面添加一个变量: ``` private var foodNode : FoodSprite! ``` -Now, we can update the `spawnFood()` function to set this variable every time the food is spawned. +现在我们可以更新 `spawnFood()` 函数,使每次食物生成时都会设置这个变量的值: -Update `spawnFood()` to the following: +用如下代码更新 `spawnFood()` 函数: ``` func spawnFood() { @@ -357,17 +354,17 @@ func spawnFood() { } ``` -This will change the scope of the food variable from the `spawnFood()` function to that of the `GameScene.swift` file. The way we’ve coded things, we have only one `FoodSprite` spawning at a time, and we’re keeping a reference to it. Because we have this reference, we can detect where the food is at any given time. We also have a single cat on screen at any time, and we also keep a reference to it. +这个函数将把食物变量的作用域从 `spawnFood()` 函数变为整个 `GameScene.swift` 文件。在我们的代码中,同一时间我们只会生成一个 `FoodSprite`,同时我们需要保持对它的引用。因为有这个引用,我们就可以检测到在任何给定时间里食物的位置了。同样的,在任何时间场景内也只会有一只猫,同样我们也需要保持对它的引用。 -We know the cat wants the food; we just need to provide a means for the cat to move. We need to edit the `CatSprite.swift` file in order to know in which direction it needs to travel toward the food. For it to get to the food, we need to know the rate of speed it can move towards it. At the top of the `CatSprite.swift` file, we can add the following line above the `newInstance()` function. +我们知道小猫想要获得食物,我们只需要提供一个方法让小猫能够移动。我们需要编辑 `CatSprite.swift` 文件以便我们知道小猫需要往哪个方向前进来获取食物。为了让小猫获得食物,我们还需要知道小猫的移动速度。在 `CatSprite.swift` 文件的顶部,我们可以在 `newInstance()` 函数前添加如下代码: ``` private let movementSpeed : CGFloat = 100 ``` -This line is our movement speed, which is a simple solution to a complex problem. It’s a simple linear equation, ignoring any complexities that friction or acceleration brings into the mix. +这一行代码定义了猫的移动速度,这是对一个复杂问题的简单解法。这是个简单的线性方程,忽略了所有摩擦和加速带来的复杂性。 -Now we need to do something with our `update(deltaTime:)` method. Because we will know where the food is, we need to move toward that location. Replace the update function in `CatSprite.swift` with the following code: +现在我们需要在我们的 `update(deltaTime:)` 方法中做点什么了。因为我们已经知道了食物的位置,我们需要让小猫朝着这个位置移动了。用如下代码更新 `CatSprite.swift` 文件中的 update 函数: ``` public func update(deltaTime : TimeInterval, foodLocation: CGPoint) { @@ -384,6 +381,7 @@ public func update(deltaTime : TimeInterval, foodLocation: CGPoint) { ``` We’ve updated the signature of the function! We needed to give the cat the location of the food, so instead of only sending in the delta time, we’ve added the food location as well. Because many things can affect the location of the food, we need to update it constantly so that we are always moving in the right direction. Let’s also look at the function here. In the new update function, we take in the delta time, which will be pretty short, about 0.166 seconds or so. We also take in the food’s location, which is a `CGPoint`. If the food location’s `x` position is less than the cat’s `x` position, then we’ll know that the food is to the left. If not, then the food will be either on top or to the right. If we move left, then we take the cat’s `x` position and subtract the movement speed, multiplied by the delta time. We need to cast it from a `TimeInterval` to a `CGFloat`, since our position and movement speed use those units, and Swift is a strongly typed language. +我们更新了整个函数。我们需要告诉小猫食物的位置, updated the signature of the function! We needed to give the cat the location of the food, so instead of only sending in the delta time, we’ve added the food location as well. Because many things can affect the location of the food, we need to update it constantly so that we are always moving in the right direction. Let’s also look at the function here. In the new update function, we take in the delta time, which will be pretty short, about 0.166 seconds or so. We also take in the food’s location, which is a `CGPoint`. If the food location’s `x` position is less than the cat’s `x` position, then we’ll know that the food is to the left. If not, then the food will be either on top or to the right. If we move left, then we take the cat’s `x` position and subtract the movement speed, multiplied by the delta time. We need to cast it from a `TimeInterval` to a `CGFloat`, since our position and movement speed use those units, and Swift is a strongly typed language. What this does exactly is nudge the cat left at a pretty constant rate to make it appear as if it is moving. In case the delta time is 0.166 seconds, we position the cat sprite 16.6 units to the left of its previous location. This is because our `movementSpeed` variable is 100, and 0.166 × 100 = 16.6. The same will happen when going right, except that we position the cat 16.6 units to the right of its previous location. Next, we edit the [xScale](https://developer.apple.com/reference/spritekit/sknode/1483087-xscale)[12](#12) property of our cat. This governs the width of our sprite. The default value is 1.0; so, if we set the `xScale` to 0.5, the cat will be half its original width. If we double it to 2.0, then the cat will be double its original width, and so on. Because the original sprite is looking to the right, when moving right, the scale will be set to its default value of 1. If we want to “flip” the cat, we set the scale to -1, which sets its frame to a negative value and renders it backwards. We keep it at -1 to maintain the proportions of the cat. Now, when moving left, the cat will face left, and when moving right, will face right! From 349987ebf3c3e1f58f109c80c4bc7d4bcbcd3065 Mon Sep 17 00:00:00 2001 From: Freya Yu Date: Fri, 16 Dec 2016 11:03:01 +0800 Subject: [PATCH 05/38] Translate Version 1.5 --- TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md b/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md index 984bfefad63..0ea3b77675f 100644 --- a/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md +++ b/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md @@ -380,14 +380,14 @@ public func update(deltaTime : TimeInterval, foodLocation: CGPoint) { } ``` -We’ve updated the signature of the function! We needed to give the cat the location of the food, so instead of only sending in the delta time, we’ve added the food location as well. Because many things can affect the location of the food, we need to update it constantly so that we are always moving in the right direction. Let’s also look at the function here. In the new update function, we take in the delta time, which will be pretty short, about 0.166 seconds or so. We also take in the food’s location, which is a `CGPoint`. If the food location’s `x` position is less than the cat’s `x` position, then we’ll know that the food is to the left. If not, then the food will be either on top or to the right. If we move left, then we take the cat’s `x` position and subtract the movement speed, multiplied by the delta time. We need to cast it from a `TimeInterval` to a `CGFloat`, since our position and movement speed use those units, and Swift is a strongly typed language. -我们更新了整个函数。我们需要告诉小猫食物的位置, updated the signature of the function! We needed to give the cat the location of the food, so instead of only sending in the delta time, we’ve added the food location as well. Because many things can affect the location of the food, we need to update it constantly so that we are always moving in the right direction. Let’s also look at the function here. In the new update function, we take in the delta time, which will be pretty short, about 0.166 seconds or so. We also take in the food’s location, which is a `CGPoint`. If the food location’s `x` position is less than the cat’s `x` position, then we’ll know that the food is to the left. If not, then the food will be either on top or to the right. If we move left, then we take the cat’s `x` position and subtract the movement speed, multiplied by the delta time. We need to cast it from a `TimeInterval` to a `CGFloat`, since our position and movement speed use those units, and Swift is a strongly typed language. +我们更新了这个函数的函数签名。因为我们需要告诉小猫食物的位置,所以在传参时,我们不仅传递了 delta 时间,也传递了食物的位置信息。因为很多事情可以影响食物的位置,所以我们需要不停地更新食物的位置信息,以保证小猫一直在正确的方向上前进。接下来,让我们来看一下函数的功能。在这个更新过的函数中,我们取的 delta 时间是一个非常短的时间,大约只有 0.166 秒左右。我们也取了食物的位置,是 `CGPoint` 类型的参数。如果食物的 `x` 位置比小猫的 `x` 位置更小,那么我们就知道食物在小猫的左边,反之,食物就在小猫的上边或右边。如果小猫朝左边移动,那么我们取小猫的 `x` 位置减去小猫的移动速度乘以 delta 时间。我们需要把 delta 时间的类型从 `TimeInterval` 转换到 `CGFloat`,因为我们的位置和速度变量用的是这个单位,而 Swift 恰恰是一种强类型语言。 +What this does exactly is nudge the cat left at a pretty constant rate to make it appear as if it is moving. In case the delta time is 0.166 seconds, we position the cat sprite 16.6 units to the left of its previous location. This is because our `movementSpeed` variable is 100, and 0.166 × 100 = 16.6. The same will happen when going right, except that we position the cat 16.6 units to the right of its previous location. Next, we edit the [xScale](https://developer.apple.com/reference/spritekit/sknode/1483087-xscale)[12](#12) property of our cat. This governs the width of our sprite. The default value is 1.0; so, if we set the `xScale` to 0.5, the cat will be half its original width. If we double it to 2.0, then the cat will be double its original width, and so on. Because the original sprite is looking to the right, when moving right, the scale will be set to its default value of 1. If we want to “flip” the cat, we set the scale to -1, which sets its frame to a negative value and renders it backwards. We keep it at -1 to maintain the proportions of the cat. Now, when moving left, the cat will face left, and when moving right, will face right! What this does exactly is nudge the cat left at a pretty constant rate to make it appear as if it is moving. In case the delta time is 0.166 seconds, we position the cat sprite 16.6 units to the left of its previous location. This is because our `movementSpeed` variable is 100, and 0.166 × 100 = 16.6. The same will happen when going right, except that we position the cat 16.6 units to the right of its previous location. Next, we edit the [xScale](https://developer.apple.com/reference/spritekit/sknode/1483087-xscale)[12](#12) property of our cat. This governs the width of our sprite. The default value is 1.0; so, if we set the `xScale` to 0.5, the cat will be half its original width. If we double it to 2.0, then the cat will be double its original width, and so on. Because the original sprite is looking to the right, when moving right, the scale will be set to its default value of 1. If we want to “flip” the cat, we set the scale to -1, which sets its frame to a negative value and renders it backwards. We keep it at -1 to maintain the proportions of the cat. Now, when moving left, the cat will face left, and when moving right, will face right! Now we will move toward the food dish’s location at a constant rate of speed. First, we check which direction to move in, then we move in that direction on the x-axis. We should also update the `xScale`, because we want to face the correct direction while moving… unless, of course, we want our cat to do the moonwalk! Finally, we need to tell the cat to update in our game scene. -Moving to the `GameScene.swift` file, navigate to our `update(_ currentTime:)` function, and beneath the update umbrella call, enter the following line: +打开 `GameScene.swift` 文件,找到我们的 `update(_ currentTime:)` 函数,在更新雨伞的调用下面,新增如下代码: ``` catNode.update(deltaTime: dt, foodLocation: foodNode.position) From 05d18e8e07dab5c20e579164984d6d76705a6371 Mon Sep 17 00:00:00 2001 From: Freya Yu Date: Fri, 16 Dec 2016 17:34:00 +0800 Subject: [PATCH 06/38] Translate Version 1.6 --- ...uild-a-spritekit-game-in-swift-3-part-2.md | 102 +++++++++--------- 1 file changed, 54 insertions(+), 48 deletions(-) diff --git a/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md b/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md index 0ea3b77675f..48380ddaf55 100644 --- a/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md +++ b/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md @@ -382,10 +382,9 @@ public func update(deltaTime : TimeInterval, foodLocation: CGPoint) { 我们更新了这个函数的函数签名。因为我们需要告诉小猫食物的位置,所以在传参时,我们不仅传递了 delta 时间,也传递了食物的位置信息。因为很多事情可以影响食物的位置,所以我们需要不停地更新食物的位置信息,以保证小猫一直在正确的方向上前进。接下来,让我们来看一下函数的功能。在这个更新过的函数中,我们取的 delta 时间是一个非常短的时间,大约只有 0.166 秒左右。我们也取了食物的位置,是 `CGPoint` 类型的参数。如果食物的 `x` 位置比小猫的 `x` 位置更小,那么我们就知道食物在小猫的左边,反之,食物就在小猫的上边或右边。如果小猫朝左边移动,那么我们取小猫的 `x` 位置减去小猫的移动速度乘以 delta 时间。我们需要把 delta 时间的类型从 `TimeInterval` 转换到 `CGFloat`,因为我们的位置和速度变量用的是这个单位,而 Swift 恰恰是一种强类型语言。 -What this does exactly is nudge the cat left at a pretty constant rate to make it appear as if it is moving. In case the delta time is 0.166 seconds, we position the cat sprite 16.6 units to the left of its previous location. This is because our `movementSpeed` variable is 100, and 0.166 × 100 = 16.6. The same will happen when going right, except that we position the cat 16.6 units to the right of its previous location. Next, we edit the [xScale](https://developer.apple.com/reference/spritekit/sknode/1483087-xscale)[12](#12) property of our cat. This governs the width of our sprite. The default value is 1.0; so, if we set the `xScale` to 0.5, the cat will be half its original width. If we double it to 2.0, then the cat will be double its original width, and so on. Because the original sprite is looking to the right, when moving right, the scale will be set to its default value of 1. If we want to “flip” the cat, we set the scale to -1, which sets its frame to a negative value and renders it backwards. We keep it at -1 to maintain the proportions of the cat. Now, when moving left, the cat will face left, and when moving right, will face right! -What this does exactly is nudge the cat left at a pretty constant rate to make it appear as if it is moving. In case the delta time is 0.166 seconds, we position the cat sprite 16.6 units to the left of its previous location. This is because our `movementSpeed` variable is 100, and 0.166 × 100 = 16.6. The same will happen when going right, except that we position the cat 16.6 units to the right of its previous location. Next, we edit the [xScale](https://developer.apple.com/reference/spritekit/sknode/1483087-xscale)[12](#12) property of our cat. This governs the width of our sprite. The default value is 1.0; so, if we set the `xScale` to 0.5, the cat will be half its original width. If we double it to 2.0, then the cat will be double its original width, and so on. Because the original sprite is looking to the right, when moving right, the scale will be set to its default value of 1. If we want to “flip” the cat, we set the scale to -1, which sets its frame to a negative value and renders it backwards. We keep it at -1 to maintain the proportions of the cat. Now, when moving left, the cat will face left, and when moving right, will face right! +这个效果实际上是以一个恒定的速率将小猫往左边推,让它看起来像是在移动。在这里,每隔 0.166 秒,我们将猫精灵放在上一位置左边 16.6 单位的位置上。这是因为我们的 `movementSpeed` 变量是 100,而 0.166 × 100 = 16.6。小猫往右边移动时进行一样的处理,除了我们是将猫精灵放在上一位置右边 16.6 单位的位置上。接下来,我们设定了我们猫的 [xScale](https://developer.apple.com/reference/spritekit/sknode/1483087-xscale)[12](#12) 属性。这个值决定了猫精灵的宽度。默认值是 1.0,如果我们把 `xScale` 设置成 0.5,猫的宽度就会变成之前的一半。如果我们把这个值翻倍到 2.0,那么猫的宽度就会变成之前的一倍,以此类推。因为原始的猫精灵是面朝右边的,当猫朝着右边移动时,xScale 值会被设定为默认的 1。如果我们想要“翻转”猫精灵,我们就把 xScale 设置成 -1,这会把猫的 frame 值置为负数并且反向渲染。我们把这个值保持在 -1 来保证猫精灵的比例一致。现在,当猫朝左边移动时,它会面朝左边,当猫朝右边移动时,它会面朝右边。 -Now we will move toward the food dish’s location at a constant rate of speed. First, we check which direction to move in, then we move in that direction on the x-axis. We should also update the `xScale`, because we want to face the correct direction while moving… unless, of course, we want our cat to do the moonwalk! Finally, we need to tell the cat to update in our game scene. +现在小猫会以一个恒定的速率朝着食物的位置移动了。首先,我们确定了小猫需要移动的方向,之后小猫在 x 轴上朝着那个方向移动。我们同样也需要更新猫的  `xScale` 参数,因为我们希望小猫可以在移动时面朝正确的方向。当然了,除非我们希望小猫在用太空步移动!最后,我们需要告诉小猫来更新我们的游戏场景。 打开 `GameScene.swift` 文件,找到我们的 `update(_ currentTime:)` 函数,在更新雨伞的调用下面,新增如下代码: @@ -393,23 +392,24 @@ Now we will move toward the food dish’s location at a constant rate of speed. catNode.update(deltaTime: dt, foodLocation: foodNode.position) ``` -Run the app, and we should have success! Mostly, at least. Currently, the cat does indeed move toward the food, but it can get into some interesting situations. +运行我们的应用,然后我们成功啦!最起码是在绝大多数情况下。到现在为止,小猫会朝着食物移动了,但是却可能会陷入一些有意思的情况里。 -Just a normal cat doing normal cat things +只是一只小猫做着小猫该做的事 -Next, we can add the walking animation! After that, we’ll circle back to fix the cat’s rotation after it gets hit. You may have noticed an unused asset named `cat_two`. We need to pull this texture in and alternate it to make it appear as if the cat is walking. To do this, we will add our first `SKAction`! +接下来,我们就要来添加移动动画啦!在这之后,我们会绕回来解决猫被打中后的滚动效果。你可能已经注意到了一个名为 `cat_two` 的未使用资源。我们需要添加这个纹理,并且穿插使用它,使小猫看起来像在行走。为了实现这个,我们需要添加我们第一个 `SKAction`! -### Walking With Style +### 行走样式 At the top of `CatSprite.swift`, we will add in a string constant so that we can add a walking action associated with this key. This way, we can stop the walking action without removing all of the actions that we may have on the cat later on. Add the following line above the `movementSpeed` variable: +在 `CatSprite.swift` 文件的顶部,我们将要添加一个字符串常量,以便我们添加一个与该键值相关联的步行动作。这样做使得我们可以单独停止猫的步行动作,而不是移除之后我们可能会添加的所有动作。在 `movementSpeed` 变量前添加如下代码: ``` private let walkingActionKey = "action_walking" ``` -The string itself is not really important, but it is unique to this walking animation. I also like adding something meaningful to the key for debugging purposes down the line. For example, if I see the key, I will know that it is a `SKAction` and, specifically, that it is the walking action. +这个字符串本身并不是那么重要,但是它是步行动画的标志位。我也很喜欢在给键值命名时添加一些有意义的字段,以方便调试。例如,当我看到这个键值时,我会知道这是个 `SKAction`,具体来说,是个步行动作。 -Beneath the `walkingActionKey`, we will add in the frames. Because we will be using only two frames, we can do this at the top of the file without it looking messy: +在 `walkingActionKey` 的下面,我们将会添加图像帧。因为我们只会使用两个不同的图象帧,我们可以把它放在文件的顶部也不会让它看起来乱糟糟的: ``` private let walkFrames = [ @@ -418,7 +418,7 @@ private let walkFrames = [ ] ``` -This is just an array of the two textures that we will switch between while walking. To finish this off, we will update our `update(deltaTime: foodLocation:)` function to the following code: +这只是个包含了两个纹理的数组,而这两个纹理是在猫行走时需要交替使用的。为了完成这个功能,我们需要用如下代码更新我们的 `update(deltaTime: foodLocation:)` 函数: ``` public func update(deltaTime : TimeInterval, foodLocation: CGPoint) { @@ -444,24 +444,25 @@ public func update(deltaTime : TimeInterval, foodLocation: CGPoint) { } ``` -With this update, we’ve checked whether our cat sprite is already running the walking animation sequence. If it is not, we will add an action to the sprite. This is a nested `SKAction`. First, we create an action that will repeat forever. Then, in *that* action, we create the animation sequence for walking. The `SKAction.animate(with: …)` takes in the array of animation frames, along with the time per frame. The next variable in the function checks whether one of the textures is of a different size and whether it should resize the `SKSpriteNode` when it gets to that frame. `Restore` checks whether the sprite should return to its initial state after the action is completed. We set both of these to `false` so that we don’t get any unintended side effects. After we set up the walking action, we tell the sprite to start running it with the `run()` function. +通过此更新,我们检查了我们的猫精灵是否已经在运行步行动画序列了。如果没有,那么我们就会将步行动画添加到猫精灵上。这是个嵌套的 `SKAction`。首先,我们新建了一个会一直重复的动作。然后,在*那个*动作里,我们新建了步行的动画序列。`SKAction.animate(with: …)` 函数会接收动画帧数组,以及每帧持续的时间。 函数中接收的下一个变量确定了其中的纹理是否具有不一样的大小,同时当该纹理在动画帧上生效时是否需要调整 `SKSpriteNode` 的大小。 `Restore` 确定了当动画结束时,精灵是否需要重置到它的初始状态。我们把这两个值都设置成了 `false`,这样就不会有什么出人意料的事情发生了。在我们设定好了步行动画之后,我们就可以通过运行 `run()` 函数来让猫精灵开始行走了。 -Run the app again, and we will see our cat walking intently toward the food! +再次运行我们的应用,我们将看到我们的小猫专心致志地朝着食物移动啦! -Yeah, on the catwalk, on the catwalk, yeah I do my little turn on the catwalk. +Yeah, on the catwalk, on the catwalk, yeah I do my little turn on the catwalk(译者注:这是 “I am Too Sexy” 的歌词). -If the cat gets hit, it will rotate but still move toward the food. We need to show a damaged state for the cat, so that the user knows they did something bad. Also, we need to correct the cat’s rotation while moving, so that it is not walking on its side or upside down. +如果在这个过程中,小猫被击中,它会打滚,但是仍旧朝着食物移动。我们需要显示小猫的受损状态,以便用户知道他们做了什么不好的事。同样的,我们需要修正小猫在移动过程中的打滚动作,以保证小猫不会在乱七八糟的方向上移动。 Let’s go over the plan. We want to show the user that the cat has been hit, other than by just updating the score later on. Some games will make the unit invulnerable while flashing. We could also do a damage animation if we get the textures for this. For this game, I want to keep things simple, so I will add in some functionality for “flailing.” This cat, when hit by rain, will become stunned and just sort of roll onto its back in disbelief; the cat will be *shocked* that you would let this happen. To accomplish this, we will set up a few variables. We need to know for how long the cat will be stunned and for how long it has been stunned. Add the following lines to the top of the file, below the `movementSpeed` variable: +让我们来看一下我们的计划。我们希望能够显示小猫被击中了,而不是仅仅更新游戏得分。有些游戏会使该受损单位闪烁并且进入无敌状态。如果我们有纹理的话,我们也可以做一个受损动画。对这个游戏而言,我想保持它的简单性,所以我只添加了一些“摇动”功能。当小猫被雨滴击中时,它会被晕眩然后不可置信地翻倒;它会被*震惊*,因为玩家居然让这种事发生了。为了实现这个功能,我们会定义一些变量。我们需要知道小猫会被晕眩多长时间和它已经被晕眩了多长时间。在这个文件的顶部, `movementSpeed` 变量的下面添加如下代码: ``` private var timeSinceLastHit : TimeInterval = 2 private let maxFlailTime : TimeInterval = 2 ``` -The first variable, `timeSinceLastHit` holds how long it has been since the cat was hit last. We set it to `2` because of the next variable, `maxFlailTime`. This is a constant, saying that the cat will be stunned for only 2 seconds. Both are set to 2 so that the cat does not start out stunned when spawned. You can play with these variables later to get the perfect stun time. +第一个变量, `timeSinceLastHit` 保存了自小猫上次被打中后过了多长时间。因为下一个变量 `maxFlailTime`,我们把这个值设置成 2。`maxFlailTime` 变量是个常数,表示小猫每次会被晕眩 2 秒钟。我们把这两个值都被设置成 2,这样小猫就不会在生成的一瞬间就被晕眩了。你可以尝试着重新设定这两个值,来确定最好的晕眩时间。 -Now we need to add in a function to let the cat know it’s been hit, and that it needs to react, by stopping moving. Add the following function below our `update(deltaTime: foodLocation:)` function: +现在,我们需要添加一个函数,让小猫知道它被打中了,它需要通过停止移动来对此做出反应。在我们的 `update(deltaTime: foodLocation:)` 函数下添加如下代码: ``` public func hitByRain() { @@ -470,7 +471,7 @@ public func hitByRain() { } ``` -This just updates the `timeSinceLastHit` to `0`, and removes the walking animation that we set up earlier. Now we need to overhaul `update(deltaTime: foodLocation:)`, so that the cat doesn’t move while it is stunned. Update the function to the following: +这段代码只是把 `timeSinceLastHit` 变量设置成了 `0`,同时移除了小猫的步行动画。现在我们需要重写 `update(deltaTime: foodLocation:)` 函数,以保证小猫就不会在它被晕眩的时候移动。让我们用如下代码更新该函数: ``` public func update(deltaTime : TimeInterval, foodLocation: CGPoint) { @@ -500,41 +501,42 @@ public func update(deltaTime : TimeInterval, foodLocation: CGPoint) { } ``` -Now, our `timeSinceLastHit` will constantly be updated, and if the cat hasn’t been hit in the past 2 seconds, it will walk toward the food. If our walking animation isn’t set, then we’ll set it correctly. This is a frame-based animation that just swaps out the texture every 0.1 seconds to make it appear as though the cat is walking. It looks exactly like how real cats walk, doesn’t it? +现在,我们的 `timeSinceLastHit` 变量会不停更新,而且如果小猫在过去的 2 秒钟没有被打中,那么它就会继续朝着食物移动。如果我们并没有设置步行动画,那么必须要正确地设置它。步行动画是个基于帧的动画,而它只是每 0.1 秒交换两个纹理使得小猫看起来像在行走。不过它看起来的确很像小猫真的在行走,对吧? We need to move over to `GameScene.swift` to tell the cat that it has been hit. In `handleCatCollision(contact:)`, we will call the `hitByRain` function. In the `switch` statement, look for the `RainDropCategory` and replace this… +我们需要重新打开 `GameScene.swift` 文件来告诉小猫它被击中了。在 `handleCatCollision(contact:)` 函数中,我们需要调用 `hitByRain` 函数。在 `switch` 语句里,找到 `RainDropCategory` 然后把其中的这个语句: ``` print("rain hit the cat") ``` -… with this: +换成这个: ``` catNode.hitByRain() ``` -If we run the app now, the cat will be stunned for 2 seconds once rain touches it! +如果我们现在运行我们的应用,当小猫被雨滴击中时,它就会被晕眩 2 秒啦! -It works, but the cat seems to get into a rotated state and looks funny. Also, it looks like the rain really hurts — maybe we need to do something about that. +这个功能成功实现了,只是现在小猫会进入一个颠倒的状态,看起来很滑稽。同样的,这也会让雨滴看起来真的很痛——可能我们需要做点什么了。 -For the raindrop problem, we can make a slight tweak to its `physicsBody`. Under `spawnRaindrop` and below where we initialize `physicsBody`, we can add the following line: +对于雨滴的问题,我们可以对它的 `physicsBody` 做点细微的调整。在 `spawnRaindrop` 函数中,初始化 `physicsBody` 语句的下面,我们可以添加如下代码: ``` raindrop.physicsBody?.density = 0.5 ``` -This will halve the density of the raindrop from its normal value of `1.0`. This will launch the cat a little less. +这会使雨滴的密度从它的初始值 `1.0` 减半。这会使得小猫没这么容易被击中了。 -Moving to `CatSprite.swift`, we can correct the rotation of the cat with an `SKAction`. Add the following to the `update(deltaTime: foodLocation:)` function. Make sure that it is inside the `if` statement that checks whether the cat is flailing. +打开 `CatSprite.swift` 文件,我们可以修改 `SKAction` 来修正小猫的旋转。在 `update(deltaTime: foodLocation:)` 函数中添加如下代码。确保它在 `if` 语句的里面判断猫是否在抖动。 -Find this line: +找到这一行: ``` if timeSinceLastHit >= maxFlailTime { ``` -And add the following code to correct the angular rotation: +并且添加如下代码来修正小猫的旋转角度: ``` if zRotation != 0 && action(forKey: "action_rotate") == nil { @@ -542,9 +544,9 @@ if zRotation != 0 && action(forKey: "action_rotate") == nil { } ``` -This block of code checks whether the cat is rotated, even in the slightest. Then, we check currently running `SKAction`s to see whether we are already animating the cat to its standing position. If the cat is rotated and not animating, we run an action to rotate it back to 0 radians. Note that we are hardcoding the key here, because we currently don’t need to use this key outside of this spot. In the future, if we need to check the animation of our rotation in another function or class, we would make a constant at the top of the file, exactly like the `walkingActionKey`. +这个代码块会判断是否小猫已经被旋转了,哪怕只是一点点。然后,我们要判断当前正在运行的这些 `SKAction` 来确定我们是否已经运行猫的重置动画。如果小猫被旋转了,而又没有运行其他动画,那么我们就需要运行一个动画来让小猫回归到初始状态。需要注意的是,我们这里直接设置了代码的默认值,因为我们暂时不需要在任何别的部分使用这个值。以后如果我们需要在别的函数或类中判断旋转动画,我们就需要在文件的顶部设置一个常量了,就像 `walkingActionKey` 一样。 -Run the app, and you will see the magic happen: Cat gets hit, cat probably rotates, cat fixes itself, cat is then happy to eat more. There are still two problems, though. Because we are using a circle for the cat’s `physicsBody`, after the cat corrects itself the first time, you might notice that the cat gets jittery. It is constantly rotating and correcting itself. To get around this, we need to reset the `angularVelocity`. Basically, the cat is rotating from getting hit, and we’ve never corrected the velocity that was added. The cat also does not update its velocity after being hit. If the cat is hit and tries to move in the opposite direction, you might notice that it goes slower than normal. The other problem is when the food is directly above the cat. The cat will quickly turn around endlessly while the food is above it. We can fix these issues by updating our `update(deltaTime :, foodLocation:)` function to the following: +运行我们的应用,现在你能看到奇迹发生了:小猫被击中了,小猫旋转了,小猫又转回来了,它很开心可以继续去吃掉更多的食物了。可是这里仍旧有两个小问题。因为我们把猫的 `physicsBody` 设置成了一个圆,在小猫第一次修正自己时,你可能会发现小猫的状态变得不太稳定了。它会不停的旋转然后修正自己。为了解决这个问题,我们需要重设 `angularVelocity`。本质上,小猫在被击中时会旋转,然而我们并没有修正我们为小猫添加的移动速度。而小猫也在被击中后没有更新自己的速度。如果小猫被击中了然后尝试着向相反方向移动,你可能会发现它比正常的速度慢了。另外一个问题是,食物可能会在小猫的正上方。当食物在小猫正上方时,小猫会迅速地转身。我们可以通过用如下代码更新我们的 `update(deltaTime :, foodLocation:)` 函数来解决这个问题: ``` public func update(deltaTime : TimeInterval, foodLocation: CGPoint) { @@ -585,25 +587,26 @@ public func update(deltaTime : TimeInterval, foodLocation: CGPoint) { } ``` -Run the app yet again, and much of the herky-jerky action will be corrected. Not only that, but the cat will now stand still when the food is directly above it. +现在让我们再来重新运行应用,大部分的不稳定动作已经被修正了。不仅仅是这样,当食物在小猫正上方时,小猫也会稳稳地站着了。 -### Now Add Sound +### 现在来添加音乐吧 -Before we start the programming, we should look into finding sound effects. Generally, when looking for sound effects, I just search for a phrase like “cat meow royalty free.” The first hit is usually [SoundBible.com](http://soundbible.com/tags-cat-meow.html)[13](#13), which generally has a good selection of royalty-free sound effects. Make sure to read the licenses. If you plan to never release the app, then pay no concern to licensing, since the app is for personal use. However, if you wish to sell this in the App Store, distribute it or the like, then make sure to attach a Creative Commons Attribution 3.0 licence or something similar. A lot of licenses are out there, so find out what the license is for a sound or image before using someone else’s work. +在我们开始写代码前,我们应该先要找点音效。一般来说,在寻找音效时,我只会搜索一些类似于 “cat meow royalty free” 的关键词。第一个匹配的通常是 [SoundBible.com](http://soundbible.com/tags-cat-meow.html)[13](#13),它通常会提供一些免费的音效。请务必阅读许可证。如果你不打算发布你的应用,那么就不需要关心许可证,因为这只是个个人应用。可是,如果你想要在 App store 中发售它,或者通过别的方式发布它,那么就请确保附上了 Creative Commons Attribution 3.0 或者是类似的许可证。这里有许多种许可证,所以当你使用别人的作品前,请确定你找到了相对应的许可证。 All of these RainCat sound effects are Creative Commons-licensed and are free to use. For the next step, move the `SFX` folder that we downloaded earlier into the `RainCat` folder. +在该应用中使用的音效都是通过 Creative Commons-licensed 授权并且免费使用的。为了之后的操作,我们需要将之前下载的 `SFX` 文件夹移动到 `RainCat` 文件夹中。 -[![Finder mode activated](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Finder-Mode-Activated-preview-opt.png) ](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Finder-Mode-Activated-large-opt.png)[14](#14) +[![Finder 模式已激活](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Finder-Mode-Activated-preview-opt.png) ](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Finder-Mode-Activated-large-opt.png)[14](#14) -Add in your sound effects to the file system. ([View large version](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Finder-Mode-Activated-large-opt.png)[15](#15)) +把音效添加到文件系统中。 ([查看源文件](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Finder-Mode-Activated-large-opt.png)[15](#15)) -After you add the files to the project, add them to your project in Xcode. Create a group under “Support” named “SFX.” Right-click on the group and click “Add Files to RainCat…” +在你把这些文件拷贝到项目中之后,你需要用 Xcode 来把它们添加到你的项目中。在 “Support” 文件夹下新建一个名为 “SFX” 的 group。右键点击这个group 然后点击 “Add Files to RainCat…” 选项 -[![Adding in sound effects](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Adding-in-SFX-preview-opt.png) ](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Adding-in-SFX-preview-opt.png)[16](#16) +[![添加音效](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Adding-in-SFX-preview-opt.png) ](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Adding-in-SFX-preview-opt.png)[16](#16) -Adding in your sound effects +添加音效 -Find your “SFX” folder, select all of your sound-effect files, and click the “Add” button. Now you have sound effects to play with. Moving to `CatSprite.swift`, we can add in an array of the sound-effect file names so that we can play them on the hit of a raindrop. Add the following array below the `walkFrames` variable up at the top of the file: +找到你的 “SFX” 文件夹,选中你的所有音效文件,然后点击 “Add” 按钮。现在项目中就有了你所有需要使用的音效文件了。打开 `CatSprite.swift` 文件,我们可以添加一个包含了所有音效文件名的数组,这样我们就可以在雨滴击中物体时播放它们了。在该文件的顶部, `walkFrames` 变量下,添加如下数组: ``` private let meowSFX = [ @@ -616,7 +619,7 @@ private let meowSFX = [ ] ``` -We can have the cat make sounds by adding two lines to the `hitByRain` function: +我们在 `hitByRain` 函数中添加两行代码,来让小猫发出声音了: ``` let selectedSFX = Int(arc4random_uniform(UInt32(meowSFX.count))) @@ -624,19 +627,22 @@ run(SKAction.playSoundFileNamed(meowSFX[selectedSFX], waitForCompletion: true)) ``` The code above selects a random number, with the minimum being `0` and maximum being the size of the `meowSFX` array. Then, we pick the sound effect’s name from the string array and play the sound file. We will get to the `waitForCompletion` variable in a bit. Also, we’ll use `SKAction.playSoundFileNamed` for our short-and-sweet sound effects. +上面的代码会在 0 到 `meowSFX` 数组大小的范围内随机选择一个值。然后,我们从字符串数组中选择相对应的音效名并且播放它。我们将得到一个 1 bit 的 `waitForCompletion` 变量. 同样的,我们将使用 `SKAction.playSoundFileNamed` 来播放我们可爱的音效。 -And we have sound! Too much sound! We have sounds playing over other sounds. Right now, we’re playing one of the sound effects every time the cat gets hit by rain. This gets annoying fast. We need to add more logic around when to play the sound, and we also shouldn’t play two clips at the same time. +那么现在我们的应用就有声音啦!那么多声音!可是有些声音会重叠起来。现在,每当小猫被雨滴击中时,我们就会播放一个音效。很快我们就会觉得烦了。我们需要在播放音效时添加更多的逻辑判断,而且我们也不应该同时播放两个音效。 + +在 `CatSprite.swift` 文件的顶部,`maxFlailTime` 变量的下面,添加如下两个变量: -Add these variables to the top of the `CatSprite.swift` file, below the `maxFlailTime` variable: ``` private var currentRainHits = 4 private let maxRainHits = 4 ``` -The first variable, `currentRainHits`, is a counter for how many times the cat has been hit, and `maxRainHits` is the number of hits it will take before meowing. +第一个变量,`currentRainHits`,是一个计数器,会统计小猫总共被雨滴打中了多少次,而 `maxRainHits` 表示了在小猫喵喵叫前能被击中几次。 Now we will update the `hitByRain` function. We need to apply the rules for `currentRainHits` and `maxRainHits`. Replace the `hitByRain` function with the following: +现在我们将要更新 `hitByRain` 函数了。我们需要应用 `currentRainHits` 和 `maxRainHits` 两个变量来制定规则了。让我们用如下代码来更新 `hitByRain` 函数: ``` public func hitByRain() { @@ -661,15 +667,15 @@ public func hitByRain() { } ``` -Now, if the `currentRainHits` is less than the maximum, we just increment the `currentRainHits` and return before we play the sound effect. Then, we check whether we are currently playing the sound effect by the key we provided: `action_sound_effect`. If we are not running the action, then we select a random sound effect to play. We set `waitForCompletion` to `true` because the action will not complete until the sound effect is completed. If we set it to `false`, then it would count the sound-effect action as completed as soon as it begins to play. - -### Adding Music +现在,如果 `currentRainHits` 的值比设定的最大值小,那么我们只增加 `currentRainHits` 的值而不播放音效。然后,我们需要通过我们提供的键值: `action_sound_effect` 来判断我们现在是否已经在播放音效了。如果我们没在播放音效,那么我们可以随机播放一个音效。我们把 `waitForCompletion` 参数设置成 `true`, 因为这个操作在音效结束前并不会完成。如果我们把该参数设置成 `false`,那么它会在音效刚开始时就把它当做播放结束来计数了。 + +### 添加音乐 -Before we create a way to play music in our app, we need something to play. Similar to our search for sound effects, we can search Google for “royalty free music,” and we will generally find something. Additionally, you can go to SoundCloud and talk to artists there. See if you can reach an agreement, either by using the music for free with attribution or by paying for a license to use it in your game. For this app, I happened across [Bensound](http://www.bensound.com/royalty-free-music)[28](#28)[17](#17), which had some music I could use, under the Creative Commons license. To use it, you must follow its [licensing agreement](http://www.bensound.com/licensing)[18](#18). Pretty straightforward: Either credit Bensound or pay for a license. +在我们新建一个方法在我们的应用中播放音乐之前,我们需要找到能播放的东西。类似于搜索音效的过程,我们可以在 Google 中搜索 “royalty free music” 来找到需要播放的音乐。此外,你可以去 SoundCloud 网站,并与里面的艺术家交谈。你需要查看你是否可以找到音乐相对应的许可证以保证你可以在你的游戏中使用它。 对这个应用而言,我碰巧发现了 [Bensound](http://www.bensound.com/royalty-free-music)[28](#28)[17](#17),根据 Creative Commons license,有一些我们可以使用的音乐。你必须遵从 [licensing agreement](http://www.bensound.com/licensing)[18](#18) 来使用它。操作其实很简单:credit Bensound 或者付费购买许可。 -Download all four tracks ([1](http://www.bensound.com/royalty-free-music/track/little-idea)[19](#19), [2](http://www.bensound.com/royalty-free-music/track/clear-day)[20](#20), [3](http://www.bensound.com/royalty-free-music/track/jazzy-frenchy)[21](#21), [4](http://www.bensound.com/royalty-free-music/track/jazz-comedy)[22](#22)), or move them over from the “Music” folder that we downloaded earlier. We will use them and cycle between each track to keep things fresh. Another thing to consider is that these tracks don’t loop correctly, so you will know when each starts and ends. Good background music will loop or morph one track into another really well. +下载我们的四个音轨 ([1](http://www.bensound.com/royalty-free-music/track/little-idea)[19](#19), [2](http://www.bensound.com/royalty-free-music/track/clear-day)[20](#20), [3](http://www.bensound.com/royalty-free-music/track/jazzy-frenchy)[21](#21), [4](http://www.bensound.com/royalty-free-music/track/jazz-comedy)[22](#22)),或者把它们从之前下载的 “Music” 文件夹里拖出来。我们将在四个音轨循环播放,来保证玩家不会感到厌烦。另外一件需要考虑的事是,这些音轨并不能正确循环,这样你就会知道每个音轨的开始和结束时间。 好的背景音乐可以很好的在不同的音轨间循环或切换。 -Once you download the tracks, create a folder named “Music” in the “RainCat” folder, the same way you created the “SFX” folder earlier. Move the tracks to that folder. +在你下载了这些音轨之后,你需要在 “RainCat” 文件夹下新建一个名叫 “Music” 的文件,和你之前创建 “SFX” 文件夹的操作一样。然后把下载的音轨移动到这个文件夹中。 [![Adding in some music tracks](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Adding-in-some-music-tracks-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Adding-in-some-music-tracks-large-opt.png)[23](#23) @@ -743,7 +749,7 @@ trackPosition = (trackPosition + 1) % SoundManager.tracks.count This sets the next position of the track by incrementing it and then performing a [modulo](https://en.wikipedia.org/wiki/Modulo_operation)[30](#30) on it to keep it within the bounds of the tracks’ array. Finally, in `audioPlayerDidFinishPlaying(_ player:successfully flag:)`, we implement the `delegate` method, which lets us know when the track finishes. Currently, we don’t care whether it succeeds or not — we just play the next track when this is called. -### Just Press Play +### 按下 Play 键 Now that we are done explaining the `SoundManager`, we just need to tell it to start, and we’ll have music playing on a loop forever. Quickly run over to `GameViewController.swift` and place the following line of code below where we set up the scene the first time: From a6e89c03cf1e79182c92c37b107a173e323f8971 Mon Sep 17 00:00:00 2001 From: Freya Yu Date: Fri, 16 Dec 2016 18:43:33 +0800 Subject: [PATCH 07/38] translate Version 1.7 --- ...uild-a-spritekit-game-in-swift-3-part-2.md | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md b/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md index 48380ddaf55..2c191887ac6 100644 --- a/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md +++ b/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md @@ -677,14 +677,13 @@ public func hitByRain() { 在你下载了这些音轨之后,你需要在 “RainCat” 文件夹下新建一个名叫 “Music” 的文件,和你之前创建 “SFX” 文件夹的操作一样。然后把下载的音轨移动到这个文件夹中。 -[![Adding in some music tracks](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Adding-in-some-music-tracks-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Adding-in-some-music-tracks-large-opt.png)[23](#23) +[![添加音乐](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Adding-in-some-music-tracks-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Adding-in-some-music-tracks-large-opt.png)[23](#23) -Adding in some music tracks ([View large version](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Adding-in-some-music-tracks-large-opt.png)[24](#24)) +添加音乐 ([清晰版](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Adding-in-some-music-tracks-large-opt.png)[24](#24)) -Then, create a group in the “Support” group of our project’s structure, named “Music.” Add our tracks to the project by right-clicking on the “Music” group and clicking “Add Files to RainCat”. This is the same procedure that we used when adding our sound effects. - -Next, we will create a new file named `SoundManager.swift`, as you may have seen in the picture above. This will act as the single source of music tracks to be played. For sound effects, we don’t care if one plays over another, but it would sound terrible if two music tracks played at the same time. Finally, we can implement the `SoundManager`: +然后,在我们的项目结构里的“Support”中创建一个组,命名为“Music.” 在 “Music”组右击,点击 “Add Files to RainCat”,把我们的音乐添加到项目里。这和我们添加音效是同样的过程 +然后,我们创建一个新的文件,命名为`SoundManager.swift`,正如你在上面图片中看到的那样,这将用来作为播放音乐的单例,对声音来说,我们不需要关心在哪里播放,但是如果同时播放那将是一件很恐怖的事。所以我们可以实现`SoundManager`: ``` import AVFoundation @@ -739,15 +738,20 @@ class SoundManager : NSObject, AVAudioPlayerDelegate { Going through the new `SoundManager` class, we are making a [singleton](https://www.codefellows.org/blog/singletons-and-swift/)[25](#25) class that handles playback of the large track files and continuously plays them in order. For longer-format audio files, we need to use `AVFoundation`. It is built for this, and `SKAction` cannot load the file in quickly enough to play it in the same way it could load in a small SFX file. Because this library has been around forever, the `delegate` still depends on [`NSObjects`](https://developer.apple.com/reference/objectivec/nsobject)[26](#26). We need to be the [`AVAudioPlayerDelegate`](https://developer.apple.com/reference/avfoundation/avaudioplayerdelegate)[27](#27) to detect when the audio player completes playback. We’ll hold class variables for the current `audioPlayer` for now; we will need them later to mute playback. +我们需要使用[单例](https://www.codefellows.org/blog/singletons-and-swift/)[25](#25)来创建`SoundManager`,处理巨大的音轨文件并且按顺序连续播放它们。为了处理更长时间的音频文件,我们需要使用`AVFoundation`。它是专门为此构建的,并且`SKAction`不能快速加载文件,并且与一个小的 SFX 文件加载方式相同。因为这个库一直都存在,`delegate`也是依赖于 [`NSObjects`](https://developer.apple.com/reference/objectivec/nsobject)[26](#26)。我们需要使用[`AVAudioPlayerDelegate`](https://developer.apple.com/reference/avfoundation/avaudioplayerdelegate)[27](#27) 来检测音频何时播放完毕。 +现在我们将持有`audioPlayer`变量,以用来静音后的播放。 + + + Now we have the current track’s location, so we know the next track to play, followed by an array of the names of the music tracks in our project. We should attribute it to [Bensound](http://www.bensound.com/royalty-free-music)[28](#28)[17](#17) to honor our licensing agreement. -We need to implement the default `init` function. Here, we choose a random track to start with, so that we don’t always hear the same track first. From then on, we wait for the program to tell us to start playing. In `startPlaying`, we check to see whether the current audio player is playing. If it is not, then we attempt to start playing the selected track. We’ll attempt to start the audio player, which can fail, so we need to surround it in a [try/catch block](https://www.bignerdranch.com/blog/error-handling-in-swift-2/)[29](#29). After that, we prepare playback, play the audio clip, and then set the index for the next track. This line is pretty important: +现在我们有当前音轨的位置,我们可以按照文件名数组来播放下一个音轨。当然我们也应该遵守 [Bensound](http://www.bensound.com/royalty-free-music)[28](#28)[17](#17)协议。 +我们需要实现默认的`init`方法,在这里,我们选择随机播放起始音乐,这样我们不用在一开始总是听同样的音乐。在这之后,我们需要等待程序告诉我们去开始播放,在`startPlaying`,我们需要检查是否有别的播放器正在播放,如果没有,我们开始尝试播放被选中的音乐,我们需要去启动音乐播放器,因为有可能会失败,所以我们需要[try/catch block](https://www.bignerdranch.com/blog/error-handling-in-swift-2/)[29](#29)。然后,我们准备好了播放,我们可以播放音频剪辑,我们需要设置索引给下一个音乐,下面这行非常重要: ``` trackPosition = (trackPosition + 1) % SoundManager.tracks.count ``` - -This sets the next position of the track by incrementing it and then performing a [modulo](https://en.wikipedia.org/wiki/Modulo_operation)[30](#30) on it to keep it within the bounds of the tracks’ array. Finally, in `audioPlayerDidFinishPlaying(_ player:successfully flag:)`, we implement the `delegate` method, which lets us know when the track finishes. Currently, we don’t care whether it succeeds or not — we just play the next track when this is called. +这行会通过增加这个值来设置音轨这的下一个位置,然后会执行[modulo](https://en.wikipedia.org/wiki/Modulo_operation)[30](#30),以保持音轨不会数组越界。最后,在`audioPlayerDidFinishPlaying(_ player:successfully flag:)`, 我们实现`delegate`方法,可以让我们知道音乐播放完毕。现在,我们不需要关心是否成功,我们就是当这个方法被调用时播放下一个音乐。 ### 按下 Play 键 From 15591bc7bb40ad1f24869f7830f011a3e6de626d Mon Sep 17 00:00:00 2001 From: Freya Yu Date: Fri, 16 Dec 2016 19:01:40 +0800 Subject: [PATCH 08/38] translate Version 1.8 --- ...-build-a-spritekit-game-in-swift-3-part-2.md | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md b/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md index 2c191887ac6..8d296ddeb3b 100644 --- a/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md +++ b/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md @@ -754,24 +754,21 @@ trackPosition = (trackPosition + 1) % SoundManager.tracks.count 这行会通过增加这个值来设置音轨这的下一个位置,然后会执行[modulo](https://en.wikipedia.org/wiki/Modulo_operation)[30](#30),以保持音轨不会数组越界。最后,在`audioPlayerDidFinishPlaying(_ player:successfully flag:)`, 我们实现`delegate`方法,可以让我们知道音乐播放完毕。现在,我们不需要关心是否成功,我们就是当这个方法被调用时播放下一个音乐。 ### 按下 Play 键 - -Now that we are done explaining the `SoundManager`, we just need to tell it to start, and we’ll have music playing on a loop forever. Quickly run over to `GameViewController.swift` and place the following line of code below where we set up the scene the first time: +现在我们已经完成了`SoundManager`,我们就需要跟它说开始,我们就有音乐一直在无限循环播放了。快速进入`GameViewController.swift`,然后将下面这行代码放到第一次设置场景的地方: ``` SoundManager.sharedInstance.startPlaying() ``` -We do this in `GameViewController` because we want the music to be independent of the scene. If we run the app at this point, and everything has been added to the project correctly, we will have background music for our game! - -In this lesson, we’ve touched on two major topics: sprite animation and sound. We used a frame-based animation to animate our sprite, used `SKAction`s to animate, and used methods to correct our cat after it is hit by rain. We added sound effects using `SKAction`s and assigned them to play when the cat gets hit by rain. Finally, we added initial background music for our game. - -For those who have made it this far, congratulations! Our game is nearing completion! If you missed a step or got confused along the way, please check out the completed code for this lesson [on GitHub](https://github.com/thirteen23/RainCat/releases/tag/smashing-magazine-lesson-two)[31](#31). -How did you do? Does your code look almost exactly like mine? What has changed? Did you update the code for the better? Let me know in the comments below. +我们在 `GameViewController`里做这个是因为我们需要音乐独立于场景,如果我们在这个时候运行app,所有的东西已经被添加到项目中,我们就有背景音乐了! -Lesson 3 is coming up next! +在本课中,我们讨论了两个主要议题:sprite动画和声音。 我们使用一个基于框架的动画来使sprite有动画,使用SKAction来进行动画,并使用方法来纠正我们的猫被雨滴到。 我们添加了声音效果使用`SKAction`,并指定猫被雨滴到时播放音乐。 最后,我们为我们的游戏添加了初始背景音乐。 +到这里,恭喜!我们的游戏即将完成!如果有什么地方疏忽,请仔细检查代码[在Github](https://github.com/thirteen23/RainCat/releases/tag/smashing-magazine-lesson-two)[31](#31). +你是怎么完成的?你的代码和我的差不多吗?如果有一些修改,或者有更好的更新,可以通过评论让我知道。 +第三节课即将到来! -#### Footnotes +#### 附录 1. [ https://developer.apple.com/spritekit/](#note-1) 2. [ https://www.smashingmagazine.com/2016/11/how-to-build-a-spritekit-game-in-swift-3-part-1/](#note-2) From 31726d1785719b895cc7711634bcf6e89fdebbb7 Mon Sep 17 00:00:00 2001 From: Freya Yu Date: Mon, 19 Dec 2016 16:49:08 +0800 Subject: [PATCH 09/38] Update Version 2.0 --- ...uild-a-spritekit-game-in-swift-3-part-2.md | 132 +++++++++--------- 1 file changed, 63 insertions(+), 69 deletions(-) diff --git a/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md b/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md index 8d296ddeb3b..6611e5d1f47 100644 --- a/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md +++ b/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md @@ -7,15 +7,15 @@ ## [ 如何在 Swift 3 中用 SpriteKit 框架编写游戏 (Part 2)](https://www.smashingmagazine.com/2016/12/how-to-build-a-spritekit-game-in-swift-3-part-2/) ## -你是否曾经想过要创建一个  [SpriteKit](https://developer.apple.com/spritekit/)[1](#1) 游戏?碰撞检测看起来像是个令人生畏的任务吗?你想知道如何正确的处理音效和背景音乐吗?自从 SpriteKit 推出以来,在 iOS 上的游戏制作从未看起来如此简单过。本文中,我们将探索 SpriteKit 的基础使用。 +你是否想过如何来开发一款 [SpriteKit](https://developer.apple.com/spritekit/) 游戏?实现碰撞检测会是个令人生畏的任务吗?你想知道如何正确的处理音效和背景音乐吗?随着 SpriteKit 的发布,在 iOS 上的游戏开发已经变得空前简单了。本文中,我们将继续探索 SpriteKit 的基础知识。 -如果你错过了[之前的课程](https://www.smashingmagazine.com/2016/11/how-to-build-a-spritekit-game-in-swift-3-part-1/)[2](#2),你可以通过获取[ Github 上的代码](https://github.com/thirteen23/RainCat/releases/tag/smashing-magazine-lesson-one)[3](#3)来赶上进度。请记住,本教程需要使用 Xcode 8 和 Swift 3。 +如果你错过了[之前的课程](https://www.smashingmagazine.com/2016/11/how-to-build-a-spritekit-game-in-swift-3-part-1/),你可以通过获取[ Github 上的代码](https://github.com/thirteen23/RainCat/releases/tag/smashing-magazine-lesson-one)来赶上进度。请记住,本教程需要使用 Xcode 8 和 Swift 3。 -[![Raincat: Lesson 2](https://www.smashingmagazine.com/wp-content/uploads/2016/10/raincat_header_sm-preview-opt.png) ](https://www.smashingmagazine.com/wp-content/uploads/2016/10/raincat_header_sm-preview-opt.png)[4](#4) +[![Raincat: 第二课](https://www.smashingmagazine.com/wp-content/uploads/2016/10/raincat_header_sm-preview-opt.png) ](https://www.smashingmagazine.com/wp-content/uploads/2016/10/raincat_header_sm-preview-opt.png) RainCat, 第二课 -在 [上一课](https://www.smashingmagazine.com/2016/11/how-to-build-a-spritekit-game-in-swift-3-part-1/)[5](#5), 我们创建了地板和背景,随机生成了雨滴并添加了雨伞。这把雨伞的精灵(译者注:sprite,中文译名精灵,在游戏开发中,精灵指的是以图像方式呈现在屏幕上的一个图像)中存在一个自定义的 `SKPhysicsBody`,是通过 `CGPath` 来生成的,同时我们启用了触摸检测,因此我们可以在屏幕范围内移动它。我们使用了 `categoryBitMask` 和 `contactTestBitMask` 来链接了碰撞检测。我们移除了当雨滴落到任何物体上时的碰撞,因此它们不会堆积起来,而是在一次弹跳后穿过地板。最后,我们设置了一个世界的边框来移除所有和它接触的 `SKNode`。 +在 [上一课](https://www.smashingmagazine.com/2016/11/how-to-build-a-spritekit-game-in-swift-3-part-1/) 中,我们创建了地板和背景,随机生成了雨滴并添加了雨伞。这把雨伞的精灵(译者注:sprite,中文译名精灵,在游戏开发中,精灵指的是以图像方式呈现在屏幕上的一个图像)中存在一个自定义的 `SKPhysicsBody`,是通过 `CGPath` 来生成的,同时我们启用了触摸检测,因此我们可以在屏幕范围内移动它。同时,我们使用了 `categoryBitMask` 和 `contactTestBitMask` 来链接了碰撞检测。我们移除了当雨滴落到任何物体上时的碰撞,因此它们不会堆积起来,而是会在一次弹跳后穿过地板。最后,我们设置了一个世界边框来移除所有和它接触的 `SKNode`。 本文中,我们将重点实现以下几点: @@ -23,25 +23,24 @@ RainCat, 第二课 - 实现猫的碰撞 - 生成食物 - 实现食物的碰撞 -- 将猫移向食物 +- 使猫向食物移动 - 创建猫的动画 - 当猫接触雨滴时,使猫受到伤害 - 添加音效和背景音乐 -### 重新获取资源 +### 获取资源 -你可以从 [GitHub](https://github.com/thirteen23/RainCat/blob/smashing-day-2/dayTwoAssets.zip)[6](#6) (ZIP) 上获取本课所需要的资源。下载图片后,通过一次性拖拽所有图片将它们添加到你的 `Assets.xcassets` 文件中。你现在应该有了包含猫动画和宠物碗的资源文件。我们之后将会添加音效和背景音乐文件。 +你可以从 [GitHub](https://github.com/thirteen23/RainCat/blob/smashing-day-2/dayTwoAssets.zip)(ZIP) 上获取本课所需要的资源。下载图片后,通过一次性拖拽所有图片将它们添加到你的 `Assets.xcassets` 文件中。你现在应该有了包含猫动画和宠物碗的资源文件。我们之后将会添加音效和背景音乐文件。 -[![App 资源](https://www.smashingmagazine.com/wp-content/uploads/2016/10/App-assets-preview-opt-1.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/App-assets-large-opt.png)[7](#7) +[![App 资源](https://www.smashingmagazine.com/wp-content/uploads/2016/10/App-assets-preview-opt-1.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/App-assets-large-opt.png) - -一大堆资源! ([查看源版本](https://www.smashingmagazine.com/wp-content/uploads/2016/10/App-assets-large-opt.png)[8](#8)) +一大堆资源! ([查看源文件](https://www.smashingmagazine.com/wp-content/uploads/2016/10/App-assets-large-opt.png)) ### 猫猫时间! 我们从添加游戏主角开始本期课程。我们首先在 “Sprites” 组下创建一个新文件,命名为 `CatSprite`。 -更新 `CatSprite.swift` 中的代码如下: +将如下代码添加到 `CatSprite.swift` 文件中: ``` import SpriteKit @@ -62,15 +61,15 @@ public class CatSprite : SKSpriteNode { } ``` -我们已经使用了一个会返回猫精灵的静态初始化器来处理这些文件。我们也同样处理了另一个 `update` 函数。如果我们需要更新更多的精灵,我们应该尝试把这个函数变成一个[协议](https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Protocols.html)[9](#9)的一部分来让我们的精灵能够符合要求。这里需要注意一点,对于猫精灵,我们使用了一个圆形的 `SKPhysicsBody`。我们可以使用纹理来创建猫的物理实体,就像我们创建雨滴一样,但是这是一个“艺术”的决定。当猫被雨滴打中时, 与其让猫始终坐着,让猫痛苦地打滚显然更有趣一些。 +在这个文件中,我们用了一个会返回猫精灵的静态初始化器。在另一个 `update` 函数中,我们也使用了同样的方法。如果我们需要生成更多的精灵,我们应该尝试把这个函数变成一个[协议](https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Protocols.html)的一部分来生成合适的精灵。这里需要注意一点,对于猫精灵,我们使用的是一个圆形的 `SKPhysicsBody`。就像我们创建雨滴一样,我们当然可以使用纹理来创建猫的物理实体,但是这是一个“艺术”的决定。当猫被雨滴或雨伞打中时, 与其让猫始终坐着,让猫痛苦地打滚显然更有趣一些。 当猫接触雨滴或猫掉出该世界时,我们将需要回调函数来处理这些事件。我们可以打开 `Constants.swift` 文件,将下列代码加入该文件,使它作为一个 `CatCategory`: ``` -let CatCategory : UInt32 = 0x1 << 4 +let CatCategory : UInt32 = 0x1 << 4 ``` -上面代码中定义的变量将决定猫的身体是哪个 `SKPhysicsBody` 。让我们重新打开 `CatSprite.swift` 来更新猫精灵,使它包含 `categoryBitMask` 和 `contactTestBitMask`。 在 `newInstance()` 返回 `catSprite` 之前,我们需要添加如下代码: +上面代码中定义的变量将决定猫的身体是哪个 `SKPhysicsBody`。让我们重新打开 `CatSprite.swift` 来更新猫精灵的状态,使它包含 `categoryBitMask` 和 `contactTestBitMask` 这两个属性。 在 `newInstance()` 返回 `catSprite` 之前,我们需要添加如下代码: ``` catSprite.physicsBody?.categoryBitMask = CatCategory @@ -85,11 +84,11 @@ catSprite.physicsBody?.contactTestBitMask = RainDropCategory | WorldCategory private var catNode : CatSprite! ``` -我们可以立刻在 `sceneDidLoad()` 里创建一只猫,但是我们更想要从一个单独的函数中来创建猫对象,以便于代码重用。感叹号(`!`)告诉编译器,它并不需要在 `init` 语句中立即初始化,而且它应该不会是 `nil`。我们这么做有两个理由。首先,我们不想单独为了一个变量创建 `init()` 语句。其次,我们并不想立刻初始化它,只要在我们第一次运行 `spawnCat()` 时重新初始化和定位猫对象就可以了。我们也可以用 optional(`?`) 来定义该变量,但是当我们第一次运行了 `spawnCat()` 函数后,我们的猫精灵就再也不会变成 `nil` 了。为了解决初始化问题和让我们头疼的拆包,我们会说使用感叹号来进行自动拆包是安全的操作。如果我们在初始化我们的猫对象前就使用了它,我们的应用就会闪退,因为我们告诉我们的应用对我们的猫对象进行拆包是安全的,然而并不是。我们需要在使用它之前初始化它,但是是在合适的函数中。 +我们可以立刻在 `sceneDidLoad()` 里创建一只猫,但是我们更想要从一个单独的函数中来创建猫对象,以便于代码重用。感叹号(`!`)告诉编译器,它并不需要在 `init` 语句中立即初始化,而且它应该不会是 `nil`。我们这么做有两个理由。首先,我们不想单独为了一个变量创建 `init()` 语句。其次,我们并不想立刻初始化猫精灵,只要在我们第一次运行 `spawnCat()` 时重新初始化和定位它就可以了。我们也可以用 optional(`?`) 来定义该变量,但是当我们第一次运行了 `spawnCat()` 函数后,我们的猫精灵就再也不会变成 `nil` 了。为了解决初始化问题和让我们头疼的拆包,我们会说使用感叹号来进行自动拆包是安全的操作。如果我们在初始化我们的猫对象前就使用了它,我们的应用就会闪退,因为我们告诉应用对猫对象进行拆包是安全的,然而并不是。在合适的函数中,我们需要在使用猫对象之前初始化它。 接下来,我们将要在 `GameScene.swift` 文件中新建一个 `spawnCat()` 函数来初始化我们的猫精灵。我们会把这个初始化的部分拆分到一个单独的函数中,使这部分代码具有重用性,同时保证在一个场景中一次只可能存在一只猫。 -在这个文件中接近底部的地方,我们的 `spawnRaindrop()` 函数后面添加如下代码: +在这个文件中接近底部的地方,我们在 `spawnRaindrop()` 函数后面添加如下代码: ``` func spawnCat() { @@ -106,7 +105,7 @@ func spawnCat() { } ``` -纵观这段函数,我们首先检查了猫对象是否为空。然后,我们确定了这个场景中是否存在一个猫对象。如果这个场景内存在一只猫,我们就要从父类中移除它,移除它现在正在进行的所有操作,并清除这个猫对象的 `SKPhysicsBody` 。这些操作仅仅会在猫掉出该世界时被触发。在这之后,我们会重新初始化一个新的猫对象,同时设定它的初始位置为伞下 30 像素的地方。其实我们可以在任何位置初始化我们的猫对象,但是我想这个位置总比直接从天空中把猫丢下来更好。 +纵观这段函数,我们首先检查了猫对象是否为空。然后,我们确定了这个场景中是否已经存在了一个猫对象。如果这个场景内已经存在了一只小猫,我们就要从父类中移除它,移除它现在正在进行的所有操作,并清除这个猫对象的 `SKPhysicsBody`。而这些操作仅仅会在猫掉出该世界时被触发。在这之后,我们会重新初始化一个新的猫对象,同时设定它的初始位置为伞下 30 像素的地方。其实我们可以在任何位置初始化我们的猫对象,但是我想这个位置总比直接从天空中把猫丢下来好一些。 最后,在 `sceneDidLoad()` 函数中,在我们定位并添加了雨伞之后,调用 `spawnCat()` 函数: @@ -117,13 +116,13 @@ addChild(umbrellaNode) spawnCat() ``` -现在我们可以运行我们的应用并且欢呼啦! +现在我们可以运行我们的应用啦! -[![应用资源](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Cat-preview-opt.png) ](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Cat-large-opt.png)[10](#10) +[![应用资源](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Cat-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Cat-large-opt.png) -猫 ([查看源文件](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Cat-large-opt.png)[11](#11)) +猫 ([查看源文件](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Cat-large-opt.png)) -如果现在猫走出了雨伞并且碰到了雨滴,它将会在地上打滚。这时候,猫可能会滚出屏幕然后在接触世界边框的一瞬间被删除掉,那么,我们就需要重新生成猫对象了。因为现在回调函数会在当猫接触到雨滴时或猫掉出世界时被触发,所以我们可以在 `didBegin(_ contact:)` 函数中来处理这个碰撞事件。 +如果现在猫碰到雨滴或是雨伞 ,它将会在地上打滚。这时候,猫可能会滚出屏幕然后在接触世界边框的一瞬间被删除掉,那么,我们就需要重新生成猫对象了。因为现在回调函数会在当猫接触到雨滴时或猫掉出世界时被触发,所以我们可以在 `didBegin(_ contact:)` 函数中来处理这个碰撞事件。 我们想要在猫触碰到雨滴后和触碰世界边框后触发不同的事件,所以我们把这些逻辑拆分到了一个新的函数中。在 `GameScene.swift` 文件的底部, `didBegin(_ contact:)` 函数的后面,加上如下代码: @@ -148,7 +147,7 @@ func handleCatCollision(contact: SKPhysicsContact) { } ``` -在这段代码中,我们在寻找除了猫以外的物理实体。一旦我们获取到了另外的实体对象,我们需要判断是什么触碰了猫。现在,如果是雨滴击中了猫,我们只在控制台中输出这个碰撞发生了,而如果是猫触碰了这个游戏世界的边缘,我们就会重新生成一个猫对象。 +在这段代码中,我们在寻找除了猫以外的物理实体(physics body)。一旦我们获取到了另外的实体对象,我们需要判断是什么触碰了猫。现在,如果是雨滴击中了猫,我们只在控制台中输出这个碰撞发生了,而如果是猫触碰了这个游戏世界的边缘,我们就会重新生成一个猫对象。 我们需要在有触碰发生猫对象上时调用这个函数。那么,让我们用如下代码来更新 `didBegin(_ contact:)` 函数: @@ -180,17 +179,17 @@ func didBegin(_ contact: SKPhysicsContact) { 我们在移除雨滴碰撞和移除离屏指针中间插入了一个条件判断。这个 `if` 语句判断了碰撞物体是不是猫,然后我们在 `handleCatCollision(contact:)` 函数中处理猫的行为。 -我们现在可以用雨伞把猫推出屏幕来测试猫的重生函数了。猫现在将会在伞下重新被定义出来。请注意,如果雨伞的底部低于地板,那么猫就会一直从屏幕中掉出去。到现在为止这并不是什么大问题,但是我们之后会提供一个方法来解决它。 +我们现在可以用雨伞把猫推出屏幕来测试猫的重生函数了。我们会看到,猫将在伞下重新被定义出来。请注意,如果雨伞的底部低于地板,那么猫就会一直从屏幕中掉出去。到现在为止这并不是什么大问题,但是我们之后会提供一个方法来解决它。 ### 生成食物 现在看来,是时候生成一些食物来喂我们的小猫了。当然了,现在猫并不能自己移动,不过我们一会可以修复这个问题。在创建食物精灵之前,我们可以先在 `Constants.swift` 文件中为食物新建一个类。让我们在 `CatCategory` 中添加如下代码: ``` -let FoodCategory : UInt32 = 0x1 << 5 +let FoodCategory : UInt32 = 0x1 << 5 ``` -上面代码中定义的变量将决定食物的物理对象是哪个 `SKPhysicsBody` 。在 “Sprites” 组中,我们用创建 `CatSprite.swift` 文件图片同样的方法新建一个名为 `FoodSprite.swift` 的文件,并在该文件中添加如下代码: +上面代码中定义的变量将决定食物的物理对象是哪个 `SKPhysicsBody` 。在“Sprites”组中,我们用创建 `CatSprite.swift` 文件同样的方法新建一个名为 `FoodSprite.swift` 的文件,并在该文件中添加如下代码: ``` import SpriteKit @@ -209,7 +208,7 @@ public class FoodSprite : SKSpriteNode { } ``` -这是一个静态的类,当它被调用时,将会初始化一个 `FoodSprite` 并且返回它。我们把食物的物理实体设置为一个和食物精灵同样大小的矩形。这种处理很好,因为食物精灵本身就是一个矩形。接下来,我们把物理对象的种类设置为我们刚刚创建的 `FoodCategory` ,然后把它添加到它可能会碰撞的对象(世界边框,雨滴和猫)中。我们把食物和猫的 `zPosition` 设置成相同的,它们将永远不会重叠,因为当它们相遇时,食物就会被删除然后用户将会得到一分。 +这是一个静态的类,当它被调用时,将会初始化一个 `FoodSprite` 并且返回它。我们把食物的物理实体设置为一个和食物精灵同样大小的矩形。因为食物精灵本身就是一个矩形。接下来,我们把物理对象的种类设置为我们刚刚创建的 `FoodCategory` ,然后把它添加到它可能会碰撞的对象(世界边框,雨滴和猫)中。我们把食物和猫的 `zPosition` 设置成相同的,这样它们将永远不会重叠,因为当它们相遇时,食物就会被删除然后玩家将会得到一分。 重新打开 `GameScene.swift` 文件,我们需要添加一些功能来生成和移除食物。在这个文件的顶部,`rainDropSpawnRate` 变量的下面,我们添加如下代码: @@ -217,7 +216,7 @@ public class FoodSprite : SKSpriteNode { private let foodEdgeMargin : CGFloat = 75.0 ``` -这个变量将会作为生成食物时的外边距。我们不想将食物生成在离屏幕两侧特别近的位置。我们把这个值定义在文件的顶部,这样如果我们之后可能要改变这个值的时候就不用搜索整个文档了。接下来,在我们的 `spawnCat()` 函数下面,我们可以新增我们的 `spawnFood` 函数了。 +这个变量将会作为生成食物时的外边距。我们不想将食物生成在离屏幕两侧特别近的位置。我们把这个值定义在文件的顶部,这样如果我们之后要改变这个值的时候就不用搜索整个文档了。接下来,在我们的 `spawnCat()` 函数下面,我们可以新增我们的 `spawnFood` 函数了。 ``` func spawnFood() { @@ -232,7 +231,7 @@ func spawnFood() { } ``` -这个函数和我们的 `spawnRaindrop()` 函数几乎一模一样。我们新建了一个 `FoodSprite`,然后把它放在了屏幕上一个随机的位置 `x`。我们用了之前设定的外边距变量来限制了能够生成食物精灵的屏幕范围。首先,我们设置了随机的位置的范围为屏幕的宽度减去 2 乘以外边距。然后,我们用外边距来偏移起始位置。这使得食物不会生成在距屏幕边界 0 到 75 的任意位置。 +这个函数和我们的 `spawnRaindrop()` 函数几乎一模一样。我们新建了一个 `FoodSprite`,然后把它放在了屏幕上一个随机的位置 `x`。这里我们用了之前设定的外边距(margin)变量来限制了能够生成食物精灵的屏幕范围。首先,我们设置了随机位置的范围为屏幕的宽度减去 2 乘以外边距。然后,我们用外边距来偏移起始位置。这使得食物不会生成在任意距屏幕边界 0 到 75 的位置里。 在 `sceneDidLoad()` 文件接近顶部的位置,让我们在 `spawnCat()` 函数的初始化调用下面加上如下代码: @@ -274,7 +273,7 @@ func handleFoodHit(contact: SKPhysicsContact) { } ``` -在这个函数中,我们将用和处理猫的碰撞同样的方式来处理食物的碰撞。首先,我们定义了食物的物理实体,然后我们运行了一个 `switch` 语句来判断除食物之外的物理实体。接下来,我们为 `CatCategory` 创建一个新的分支语句 - 这是个预留的接口,我们之后可以来更新我们的代码。接下来我们 `fallthrough` 到 `WorldFrameCategory` 分支语句,这里我们需要从场景里移除食物精灵和它的物理实体。最后,我们需要重新生成食物。当食物触碰到了世界边界,我们只需要移除食物精灵和它的物理实体。如果食物触碰到了其它物理实体,那么 default 分支语句就会被触发然后在控制台打印一个通用语句。现在,唯一能触发这个语句的物理实体就是 `RainDropCategory`。而到现在为止,我们并不关心当雨击中食物时会发生什么。我们只希望雨滴和食物在击中地板或雨伞时有同样的表现。 +在这个函数中,我们将用和处理猫碰撞同样的方式来处理食物碰撞。首先,我们定义了食物的物理实体,然后我们运行了一个 `switch` 语句来判断除食物之外的物理实体。接下来,我们为 `CatCategory` 创建一个新的分支语句 - 这是个预留的接口,我们之后可以来更新我们的代码。接下来我们 `fallthrough` 到 `WorldFrameCategory` 分支语句,这里我们需要从场景里移除食物精灵和它的物理实体。最后,我们需要重新生成食物。总而言之,当食物触碰到了世界边界,我们只需要移除食物精灵和它的物理实体。如果食物触碰到了其它物理实体,那么 default 分支语句就会被触发然后在控制台打印一个通用语句。现在,唯一能触发这个语句的物理实体就是 `RainDropCategory`。而到现在为止,我们并不关心当雨击中食物时会发生什么。我们只希望雨滴和食物在击中地板或雨伞时有同样的表现。 为了让所有部分连接起来,我们将在 `didBegin(_ contact)` 函数中添加几行代码。在判断 `CatCategory` 之前添加如下代码: @@ -285,7 +284,7 @@ if contact.bodyA.categoryBitMask == FoodCategory || contact.bodyB.categoryBitMas } ``` -`didBegin(_ contact)` 最后应该看起来像下面这个代码片段: +`didBegin(_ contact)` 最后应该看起来像这样: ``` func didBegin(_ contact: SKPhysicsContact) { @@ -323,7 +322,7 @@ func didBegin(_ contact: SKPhysicsContact) { ### 让物理实体动起来吧 -现在是时候让我们的小猫动起来了。是什么驱使了小猫移动呢?当然是食物啦!我们刚刚生成了食物,那么现在我们就需要让小猫向着食物移动啦。 现在我们的食物精灵被添加到了场景中,然后就被遗忘了。我们需要修正这个问题。如果我们能够保留食物的引用,我们就可以知道它在任何时候的位置,这样我们就可以告诉小猫食物在场景的哪个位置了。小猫可以通过检查自己的坐标来了解自己在场景中的哪个位置。有了这些位置信息,我们就可以让小猫向着食物移动了。 +现在是时候让我们的小猫动起来了。是什么驱使了小猫移动呢?当然是食物啦!我们刚刚生成了食物,那么现在我们就需要让小猫向着食物移动啦。现在我们的食物精灵被添加到了场景中,然后就被遗忘了。我们需要修正这个问题。如果我们能够保留食物的引用(reference),我们就可以知道它在任何时候的位置,这样我们就可以告诉小猫食物在场景的哪个位置了。小猫可以通过检查自己的坐标来了解自己在场景中的哪个位置。有了这些位置信息,我们就可以让小猫向着食物移动了。 重新打开 `GameScene.swift` 文件,让我们在文件的顶部,猫变量的下面添加一个变量: @@ -331,7 +330,7 @@ func didBegin(_ contact: SKPhysicsContact) { private var foodNode : FoodSprite! ``` -现在我们可以更新 `spawnFood()` 函数,使每次食物生成时都会设置这个变量的值: +现在我们可以更新 `spawnFood()` 函数,使每次食物生成时都会刷新这个变量的值。 用如下代码更新 `spawnFood()` 函数: @@ -354,7 +353,7 @@ func spawnFood() { } ``` -这个函数将把食物变量的作用域从 `spawnFood()` 函数变为整个 `GameScene.swift` 文件。在我们的代码中,同一时间我们只会生成一个 `FoodSprite`,同时我们需要保持对它的引用。因为有这个引用,我们就可以检测到在任何给定时间里食物的位置了。同样的,在任何时间场景内也只会有一只猫,同样我们也需要保持对它的引用。 +这个函数将把食物变量的作用域从 `spawnFood()` 函数变为整个 `GameScene.swift` 文件。在我们的代码中,同一时间我们只会生成一个 `FoodSprite`,同时我们需要保持对它的引用。因为有这个引用,我们就可以检测到在任何时间食物的位置了。同样的,在任何时间场景内也只会有一只猫,同样我们也需要保持对它的引用。 我们知道小猫想要获得食物,我们只需要提供一个方法让小猫能够移动。我们需要编辑 `CatSprite.swift` 文件以便我们知道小猫需要往哪个方向前进来获取食物。为了让小猫获得食物,我们还需要知道小猫的移动速度。在 `CatSprite.swift` 文件的顶部,我们可以在 `newInstance()` 函数前添加如下代码: @@ -362,7 +361,7 @@ func spawnFood() { private let movementSpeed : CGFloat = 100 ``` -这一行代码定义了猫的移动速度,这是对一个复杂问题的简单解法。这是个简单的线性方程,忽略了所有摩擦和加速带来的复杂性。 +这一行代码定义了猫的移动速度,这是对一个复杂问题的简单解法。我们用了一个简单的线性方程,不考虑任何摩擦和加速。 现在我们需要在我们的 `update(deltaTime:)` 方法中做点什么了。因为我们已经知道了食物的位置,我们需要让小猫朝着这个位置移动了。用如下代码更新 `CatSprite.swift` 文件中的 update 函数: @@ -380,11 +379,11 @@ public func update(deltaTime : TimeInterval, foodLocation: CGPoint) { } ``` -我们更新了这个函数的函数签名。因为我们需要告诉小猫食物的位置,所以在传参时,我们不仅传递了 delta 时间,也传递了食物的位置信息。因为很多事情可以影响食物的位置,所以我们需要不停地更新食物的位置信息,以保证小猫一直在正确的方向上前进。接下来,让我们来看一下函数的功能。在这个更新过的函数中,我们取的 delta 时间是一个非常短的时间,大约只有 0.166 秒左右。我们也取了食物的位置,是 `CGPoint` 类型的参数。如果食物的 `x` 位置比小猫的 `x` 位置更小,那么我们就知道食物在小猫的左边,反之,食物就在小猫的上边或右边。如果小猫朝左边移动,那么我们取小猫的 `x` 位置减去小猫的移动速度乘以 delta 时间。我们需要把 delta 时间的类型从 `TimeInterval` 转换到 `CGFloat`,因为我们的位置和速度变量用的是这个单位,而 Swift 恰恰是一种强类型语言。 +我们更新了这个函数的函数签名(signature)。因为我们需要告诉小猫食物的位置,所以在传参时,我们不仅传递了 delta 时间,也传递了食物的位置信息。因为很多事情可以影响食物的位置,所以我们需要不停地更新食物的位置信息,以保证小猫一直在正确的方向上前进。接下来,让我们来看一下函数的功能。在这个更新过的函数中,我们取的 delta 时间是一个非常短的时间,大约只有 0.166 秒左右。我们也取了食物的位置,是 `CGPoint` 类型的参数。如果食物的 `x` 位置比小猫的 `x` 位置更小,那么我们就知道食物在小猫的左边,反之,食物就在小猫的上边或右边。如果小猫朝左边移动,那么我们取小猫的 `x` 位置减去小猫的移动速度乘以 delta 时间。我们需要把 delta 时间的类型从 `TimeInterval` 转换到 `CGFloat`,因为我们的位置和速度变量用的是这个单位,而 Swift 恰恰是一种强类型语言。 -这个效果实际上是以一个恒定的速率将小猫往左边推,让它看起来像是在移动。在这里,每隔 0.166 秒,我们将猫精灵放在上一位置左边 16.6 单位的位置上。这是因为我们的 `movementSpeed` 变量是 100,而 0.166 × 100 = 16.6。小猫往右边移动时进行一样的处理,除了我们是将猫精灵放在上一位置右边 16.6 单位的位置上。接下来,我们设定了我们猫的 [xScale](https://developer.apple.com/reference/spritekit/sknode/1483087-xscale)[12](#12) 属性。这个值决定了猫精灵的宽度。默认值是 1.0,如果我们把 `xScale` 设置成 0.5,猫的宽度就会变成之前的一半。如果我们把这个值翻倍到 2.0,那么猫的宽度就会变成之前的一倍,以此类推。因为原始的猫精灵是面朝右边的,当猫朝着右边移动时,xScale 值会被设定为默认的 1。如果我们想要“翻转”猫精灵,我们就把 xScale 设置成 -1,这会把猫的 frame 值置为负数并且反向渲染。我们把这个值保持在 -1 来保证猫精灵的比例一致。现在,当猫朝左边移动时,它会面朝左边,当猫朝右边移动时,它会面朝右边。 +这个效果实际上是以一个恒定的速率将小猫往左边推,让它看起来像是在移动。在这里,每隔 0.166 秒,我们就将猫精灵放在上一位置左边 16.6 单位的位置上。这是因为我们的 `movementSpeed` 变量是 100,而 0.166 × 100 = 16.6。小猫往右边移动时进行一样的处理,除了我们是将猫精灵放在上一位置右边 16.6 单位的位置上。接下来,我们设定了我们猫的 [xScale](https://developer.apple.com/reference/spritekit/sknode/1483087-xscale) 属性。这个值决定了猫精灵的宽度。默认值是 1.0,如果我们把 `xScale` 设置成 0.5,猫的宽度就会变成之前的一半。如果我们把这个值翻倍到 2.0,那么猫的宽度就会变成之前的一倍,以此类推。因为原始的猫精灵是面朝右边的,当猫朝着右边移动时,xScale 值会被设定为默认的 1。如果我们想要“翻转”猫精灵,我们就把 xScale 设置成 -1,这会把猫的 frame 值置为负数并且反向渲染。我们把这个值保持在 -1 来保证猫精灵的比例一致。现在,当猫朝左边移动时,它会面朝左边,当猫朝右边移动时,它会面朝右边。 -现在小猫会以一个恒定的速率朝着食物的位置移动了。首先,我们确定了小猫需要移动的方向,之后小猫在 x 轴上朝着那个方向移动。我们同样也需要更新猫的  `xScale` 参数,因为我们希望小猫可以在移动时面朝正确的方向。当然了,除非我们希望小猫在用太空步移动!最后,我们需要告诉小猫来更新我们的游戏场景。 +现在小猫会以一个恒定的速率朝着食物的位置移动了。首先,我们确定了小猫需要移动的方向,之后让小猫在 x 轴上朝着那个方向移动。我们同样也需要更新猫的  `xScale` 参数,因为我们希望小猫可以在移动时面朝正确的方向。除非我们希望小猫在用太空步移动!最后,我们需要告诉小猫来更新我们的游戏场景。 打开 `GameScene.swift` 文件,找到我们的 `update(_ currentTime:)` 函数,在更新雨伞的调用下面,新增如下代码: @@ -392,7 +391,7 @@ public func update(deltaTime : TimeInterval, foodLocation: CGPoint) { catNode.update(deltaTime: dt, foodLocation: foodNode.position) ``` -运行我们的应用,然后我们成功啦!最起码是在绝大多数情况下。到现在为止,小猫会朝着食物移动了,但是却可能会陷入一些有意思的情况里。 +运行我们的应用,然后成功!最起码是在绝大多数情况下。到现在为止,小猫会朝着食物移动了,但是却可能会陷入一些有意思的情况里。 只是一只小猫做着小猫该做的事 @@ -400,8 +399,7 @@ catNode.update(deltaTime: dt, foodLocation: foodNode.position) ### 行走样式 -At the top of `CatSprite.swift`, we will add in a string constant so that we can add a walking action associated with this key. This way, we can stop the walking action without removing all of the actions that we may have on the cat later on. Add the following line above the `movementSpeed` variable: -在 `CatSprite.swift` 文件的顶部,我们将要添加一个字符串常量,以便我们添加一个与该键值相关联的步行动作。这样做使得我们可以单独停止猫的步行动作,而不是移除之后我们可能会添加的所有动作。在 `movementSpeed` 变量前添加如下代码: +在 `CatSprite.swift` 文件的顶部,我们将要添加一个字符串常量,以便我们添加一个与该键值相关联的步行动作。这样做使得我们可以单独停止猫的步行动作,而不是移除之后可能会添加的所有动作。在 `movementSpeed` 变量前添加如下代码: ``` private let walkingActionKey = "action_walking" @@ -409,7 +407,7 @@ private let walkingActionKey = "action_walking" 这个字符串本身并不是那么重要,但是它是步行动画的标志位。我也很喜欢在给键值命名时添加一些有意义的字段,以方便调试。例如,当我看到这个键值时,我会知道这是个 `SKAction`,具体来说,是个步行动作。 -在 `walkingActionKey` 的下面,我们将会添加图像帧。因为我们只会使用两个不同的图象帧,我们可以把它放在文件的顶部也不会让它看起来乱糟糟的: +在 `walkingActionKey` 的下面,我们将会添加图像帧。因为我们只会使用两个不同的图象帧,我们可以把它放在文件的顶部: ``` private let walkFrames = [ @@ -444,7 +442,7 @@ public func update(deltaTime : TimeInterval, foodLocation: CGPoint) { } ``` -通过此更新,我们检查了我们的猫精灵是否已经在运行步行动画序列了。如果没有,那么我们就会将步行动画添加到猫精灵上。这是个嵌套的 `SKAction`。首先,我们新建了一个会一直重复的动作。然后,在*那个*动作里,我们新建了步行的动画序列。`SKAction.animate(with: …)` 函数会接收动画帧数组,以及每帧持续的时间。 函数中接收的下一个变量确定了其中的纹理是否具有不一样的大小,同时当该纹理在动画帧上生效时是否需要调整 `SKSpriteNode` 的大小。 `Restore` 确定了当动画结束时,精灵是否需要重置到它的初始状态。我们把这两个值都设置成了 `false`,这样就不会有什么出人意料的事情发生了。在我们设定好了步行动画之后,我们就可以通过运行 `run()` 函数来让猫精灵开始行走了。 +通过此更新,我们检查了我们的猫精灵是否已经在运行步行动画序列了。如果没有,那么我们就会将步行动画添加到猫精灵上。这是个嵌套的 `SKAction`。首先,我们新建了一个会一直重复的动作。然后,在*那个*动作里,我们新建了步行的动画序列。 `SKAction.animate(with: …)` 函数会接收动画帧数组,以及每帧持续的时间。 函数中接收的下一个变量确定了其中的纹理是否具有不一样的大小,同时当该纹理在动画帧上生效时是否需要调整 `SKSpriteNode` 的大小。 `Restore` 确定了当动画结束时,精灵是否需要重置到它的初始状态。我们把这两个值都设置成了 `false`,这样就不会有什么出人意料的事情发生了。在我们设定好了步行动画之后,我们就可以通过运行 `run()` 函数来让猫精灵开始行走了。 再次运行我们的应用,我们将看到我们的小猫专心致志地朝着食物移动啦! @@ -452,7 +450,6 @@ Yeah, on the catwalk, on the catwalk, yeah I do my little turn on the catwalk( 如果在这个过程中,小猫被击中,它会打滚,但是仍旧朝着食物移动。我们需要显示小猫的受损状态,以便用户知道他们做了什么不好的事。同样的,我们需要修正小猫在移动过程中的打滚动作,以保证小猫不会在乱七八糟的方向上移动。 -Let’s go over the plan. We want to show the user that the cat has been hit, other than by just updating the score later on. Some games will make the unit invulnerable while flashing. We could also do a damage animation if we get the textures for this. For this game, I want to keep things simple, so I will add in some functionality for “flailing.” This cat, when hit by rain, will become stunned and just sort of roll onto its back in disbelief; the cat will be *shocked* that you would let this happen. To accomplish this, we will set up a few variables. We need to know for how long the cat will be stunned and for how long it has been stunned. Add the following lines to the top of the file, below the `movementSpeed` variable: 让我们来看一下我们的计划。我们希望能够显示小猫被击中了,而不是仅仅更新游戏得分。有些游戏会使该受损单位闪烁并且进入无敌状态。如果我们有纹理的话,我们也可以做一个受损动画。对这个游戏而言,我想保持它的简单性,所以我只添加了一些“摇动”功能。当小猫被雨滴击中时,它会被晕眩然后不可置信地翻倒;它会被*震惊*,因为玩家居然让这种事发生了。为了实现这个功能,我们会定义一些变量。我们需要知道小猫会被晕眩多长时间和它已经被晕眩了多长时间。在这个文件的顶部, `movementSpeed` 变量的下面添加如下代码: ``` @@ -503,7 +500,6 @@ public func update(deltaTime : TimeInterval, foodLocation: CGPoint) { 现在,我们的 `timeSinceLastHit` 变量会不停更新,而且如果小猫在过去的 2 秒钟没有被打中,那么它就会继续朝着食物移动。如果我们并没有设置步行动画,那么必须要正确地设置它。步行动画是个基于帧的动画,而它只是每 0.1 秒交换两个纹理使得小猫看起来像在行走。不过它看起来的确很像小猫真的在行走,对吧? -We need to move over to `GameScene.swift` to tell the cat that it has been hit. In `handleCatCollision(contact:)`, we will call the `hitByRain` function. In the `switch` statement, look for the `RainDropCategory` and replace this… 我们需要重新打开 `GameScene.swift` 文件来告诉小猫它被击中了。在 `handleCatCollision(contact:)` 函数中,我们需要调用 `hitByRain` 函数。在 `switch` 语句里,找到 `RainDropCategory` 然后把其中的这个语句: ``` @@ -593,7 +589,6 @@ public func update(deltaTime : TimeInterval, foodLocation: CGPoint) { 在我们开始写代码前,我们应该先要找点音效。一般来说,在寻找音效时,我只会搜索一些类似于 “cat meow royalty free” 的关键词。第一个匹配的通常是 [SoundBible.com](http://soundbible.com/tags-cat-meow.html)[13](#13),它通常会提供一些免费的音效。请务必阅读许可证。如果你不打算发布你的应用,那么就不需要关心许可证,因为这只是个个人应用。可是,如果你想要在 App store 中发售它,或者通过别的方式发布它,那么就请确保附上了 Creative Commons Attribution 3.0 或者是类似的许可证。这里有许多种许可证,所以当你使用别人的作品前,请确定你找到了相对应的许可证。 -All of these RainCat sound effects are Creative Commons-licensed and are free to use. For the next step, move the `SFX` folder that we downloaded earlier into the `RainCat` folder. 在该应用中使用的音效都是通过 Creative Commons-licensed 授权并且免费使用的。为了之后的操作,我们需要将之前下载的 `SFX` 文件夹移动到 `RainCat` 文件夹中。 [![Finder 模式已激活](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Finder-Mode-Activated-preview-opt.png) ](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Finder-Mode-Activated-large-opt.png)[14](#14) @@ -626,7 +621,6 @@ let selectedSFX = Int(arc4random_uniform(UInt32(meowSFX.count))) run(SKAction.playSoundFileNamed(meowSFX[selectedSFX], waitForCompletion: true)) ``` -The code above selects a random number, with the minimum being `0` and maximum being the size of the `meowSFX` array. Then, we pick the sound effect’s name from the string array and play the sound file. We will get to the `waitForCompletion` variable in a bit. Also, we’ll use `SKAction.playSoundFileNamed` for our short-and-sweet sound effects. 上面的代码会在 0 到 `meowSFX` 数组大小的范围内随机选择一个值。然后,我们从字符串数组中选择相对应的音效名并且播放它。我们将得到一个 1 bit 的 `waitForCompletion` 变量. 同样的,我们将使用 `SKAction.playSoundFileNamed` 来播放我们可爱的音效。 那么现在我们的应用就有声音啦!那么多声音!可是有些声音会重叠起来。现在,每当小猫被雨滴击中时,我们就会播放一个音效。很快我们就会觉得烦了。我们需要在播放音效时添加更多的逻辑判断,而且我们也不应该同时播放两个音效。 @@ -641,7 +635,6 @@ private let maxRainHits = 4 第一个变量,`currentRainHits`,是一个计数器,会统计小猫总共被雨滴打中了多少次,而 `maxRainHits` 表示了在小猫喵喵叫前能被击中几次。 -Now we will update the `hitByRain` function. We need to apply the rules for `currentRainHits` and `maxRainHits`. Replace the `hitByRain` function with the following: 现在我们将要更新 `hitByRain` 函数了。我们需要应用 `currentRainHits` 和 `maxRainHits` 两个变量来制定规则了。让我们用如下代码来更新 `hitByRain` 函数: ``` @@ -671,19 +664,20 @@ public func hitByRain() { ### 添加音乐 -在我们新建一个方法在我们的应用中播放音乐之前,我们需要找到能播放的东西。类似于搜索音效的过程,我们可以在 Google 中搜索 “royalty free music” 来找到需要播放的音乐。此外,你可以去 SoundCloud 网站,并与里面的艺术家交谈。你需要查看你是否可以找到音乐相对应的许可证以保证你可以在你的游戏中使用它。 对这个应用而言,我碰巧发现了 [Bensound](http://www.bensound.com/royalty-free-music)[28](#28)[17](#17),根据 Creative Commons license,有一些我们可以使用的音乐。你必须遵从 [licensing agreement](http://www.bensound.com/licensing)[18](#18) 来使用它。操作其实很简单:credit Bensound 或者付费购买许可。 +在我们新建一个方法在我们的应用中播放音乐之前,我们需要找到能播放的东西。类似于搜索音效的过程,我们可以在 Google 中搜索 “royalty free music” 来找到需要播放的音乐。此外,你可以去 SoundCloud 网站,并与里面的艺术家交谈。你需要查看你是否可以找到音乐相对应的许可证以保证你可以在你的游戏中使用它。 对这个应用而言,我碰巧发现了 [Bensound](http://www.bensound.com/royalty-free-music),根据 Creative Commons license,有一些我们可以使用的音乐。你必须遵从 [licensing agreement](http://www.bensound.com/licensing) 来使用它。操作其实很简单:credit Bensound 或者付费购买许可。 + +下载我们的四个音轨 ([1](http://www.bensound.com/royalty-free-music/track/little-idea), [2](http://www.bensound.com/royalty-free-music/track/clear-day)), [3](http://www.bensound.com/royalty-free-music/track/jazzy-frenchy), [4](http://www.bensound.com/royalty-free-music/track/jazz-comedy)),或者把它们从之前下载的 “Music” 文件夹里拖出来。我们将在四个音轨循环播放,来保证玩家不会感到厌烦。另外一件需要考虑的事是,这些音轨可能并不能正确循环,这样你就需要知道每个音轨的开始和结束时间。好的背景音乐可以很好的在不同的音轨间循环或切换。 -下载我们的四个音轨 ([1](http://www.bensound.com/royalty-free-music/track/little-idea)[19](#19), [2](http://www.bensound.com/royalty-free-music/track/clear-day)[20](#20), [3](http://www.bensound.com/royalty-free-music/track/jazzy-frenchy)[21](#21), [4](http://www.bensound.com/royalty-free-music/track/jazz-comedy)[22](#22)),或者把它们从之前下载的 “Music” 文件夹里拖出来。我们将在四个音轨循环播放,来保证玩家不会感到厌烦。另外一件需要考虑的事是,这些音轨并不能正确循环,这样你就会知道每个音轨的开始和结束时间。 好的背景音乐可以很好的在不同的音轨间循环或切换。 +在你下载了这些音轨之后,你需要在 “RainCat” 文件夹下新建一个名叫 “Music” 的文件夹,和你之前创建 “SFX” 文件夹的操作一样。然后把下载的音轨移动到这个文件夹中。 -在你下载了这些音轨之后,你需要在 “RainCat” 文件夹下新建一个名叫 “Music” 的文件,和你之前创建 “SFX” 文件夹的操作一样。然后把下载的音轨移动到这个文件夹中。 +[![添加音乐](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Adding-in-some-music-tracks-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Adding-in-some-music-tracks-large-opt.png) -[![添加音乐](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Adding-in-some-music-tracks-preview-opt.png)](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Adding-in-some-music-tracks-large-opt.png)[23](#23) +添加音乐 ([查看源文件](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Adding-in-some-music-tracks-large-opt.png)) -添加音乐 ([清晰版](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Adding-in-some-music-tracks-large-opt.png)[24](#24)) +然后,在我们的项目结构里的 “Support” 中创建一个组,命名为 “Music”。 右键点击 “Music” 组,点击 “Add Files to RainCat”,把我们的音乐添加到项目里。这和我们添加音效的操作一样。 -然后,在我们的项目结构里的“Support”中创建一个组,命名为“Music.” 在 “Music”组右击,点击 “Add Files to RainCat”,把我们的音乐添加到项目里。这和我们添加音效是同样的过程 +然后,我们需要创建一个名为 `SoundManager.swift` 新文件,正如你在上面图片中看到的那样。这将用来作为播放音乐的单例,对音效而言,我们并不介意两个音效重叠,但是如果有两个背景音乐同时播放那将是一件很恐怖的事。所以我们需要实现 `SoundManager`: -然后,我们创建一个新的文件,命名为`SoundManager.swift`,正如你在上面图片中看到的那样,这将用来作为播放音乐的单例,对声音来说,我们不需要关心在哪里播放,但是如果同时播放那将是一件很恐怖的事。所以我们可以实现`SoundManager`: ``` import AVFoundation @@ -736,36 +730,36 @@ class SoundManager : NSObject, AVAudioPlayerDelegate { } ``` -Going through the new `SoundManager` class, we are making a [singleton](https://www.codefellows.org/blog/singletons-and-swift/)[25](#25) class that handles playback of the large track files and continuously plays them in order. For longer-format audio files, we need to use `AVFoundation`. It is built for this, and `SKAction` cannot load the file in quickly enough to play it in the same way it could load in a small SFX file. Because this library has been around forever, the `delegate` still depends on [`NSObjects`](https://developer.apple.com/reference/objectivec/nsobject)[26](#26). We need to be the [`AVAudioPlayerDelegate`](https://developer.apple.com/reference/avfoundation/avaudioplayerdelegate)[27](#27) to detect when the audio player completes playback. We’ll hold class variables for the current `audioPlayer` for now; we will need them later to mute playback. - -我们需要使用[单例](https://www.codefellows.org/blog/singletons-and-swift/)[25](#25)来创建`SoundManager`,处理巨大的音轨文件并且按顺序连续播放它们。为了处理更长时间的音频文件,我们需要使用`AVFoundation`。它是专门为此构建的,并且`SKAction`不能快速加载文件,并且与一个小的 SFX 文件加载方式相同。因为这个库一直都存在,`delegate`也是依赖于 [`NSObjects`](https://developer.apple.com/reference/objectivec/nsobject)[26](#26)。我们需要使用[`AVAudioPlayerDelegate`](https://developer.apple.com/reference/avfoundation/avaudioplayerdelegate)[27](#27) 来检测音频何时播放完毕。 -现在我们将持有`audioPlayer`变量,以用来静音后的播放。 - - +在 `SoundManager` 类中,我们需要使用[单例](https://www.codefellows.org/blog/singletons-and-swift/)来创建 `SoundManager`,来处理巨大的音轨文件并且按顺序连续播放它们。为了处理更长时间的音频文件,我们需要使用 `AVFoundation`。它是专门为此构建的,而 `SKAction` 并不能边加载边播放一个大音频文件,这和它在加载小的 SFX 文件时不一样。因为这个库一直都存在, `delegate` 是依赖于 [`NSObjects`](https://developer.apple.com/reference/objectivec/nsobject)。我们需要使用 [`AVAudioPlayerDelegate`](https://developer.apple.com/reference/avfoundation/avaudioplayerdelegate) 来检测音频何时播放完毕。 +我们需要持有现在正在播放的 `audioPlayer` 变量,以用来实现静音操作。 -Now we have the current track’s location, so we know the next track to play, followed by an array of the names of the music tracks in our project. We should attribute it to [Bensound](http://www.bensound.com/royalty-free-music)[28](#28)[17](#17) to honor our licensing agreement. +现在我们有当前音轨的位置,我们可以按照文件名数组来播放下一个音轨。当然我们也应该遵守 [Bensound](http://www.bensound.com/royalty-free-music) 协议许可。 -现在我们有当前音轨的位置,我们可以按照文件名数组来播放下一个音轨。当然我们也应该遵守 [Bensound](http://www.bensound.com/royalty-free-music)[28](#28)[17](#17)协议。 -我们需要实现默认的`init`方法,在这里,我们选择随机播放起始音乐,这样我们不用在一开始总是听同样的音乐。在这之后,我们需要等待程序告诉我们去开始播放,在`startPlaying`,我们需要检查是否有别的播放器正在播放,如果没有,我们开始尝试播放被选中的音乐,我们需要去启动音乐播放器,因为有可能会失败,所以我们需要[try/catch block](https://www.bignerdranch.com/blog/error-handling-in-swift-2/)[29](#29)。然后,我们准备好了播放,我们可以播放音频剪辑,我们需要设置索引给下一个音乐,下面这行非常重要: +我们需要实现默认的 `init` 函数,在这里,我们将随机选择起始音乐,这样我们不用总是在游戏开始时听同样的音乐。在这之后,我们需要等待程序告诉我们开始播放操作。在 `startPlaying` 函数中,我们需要检查当前播放器是否正在播放,如果没有,我们开始尝试播放被选中的音乐。我们需要启动音乐播放器,因为该操作有可能失败,所以我们需要将该操作放到 [try/catch block](https://www.bignerdranch.com/blog/error-handling-in-swift-2/) 中。然后,我们准备开始播放选中的音轨,同时设置索引给下一个需要播放的音乐。因此,下面这行代码非常重要: ``` trackPosition = (trackPosition + 1) % SoundManager.tracks.count ``` -这行会通过增加这个值来设置音轨这的下一个位置,然后会执行[modulo](https://en.wikipedia.org/wiki/Modulo_operation)[30](#30),以保持音轨不会数组越界。最后,在`audioPlayerDidFinishPlaying(_ player:successfully flag:)`, 我们实现`delegate`方法,可以让我们知道音乐播放完毕。现在,我们不需要关心是否成功,我们就是当这个方法被调用时播放下一个音乐。 + +这行代码会通过增加索引值来设置音轨的下个位置,然后会执行 [modulo](https://en.wikipedia.org/wiki/Modulo_operation) 操作,以保持索引值不会越界。最后,在 `audioPlayerDidFinishPlaying(_ player:successfully flag:)` 函数中,我们实现了 `delegate` 方法,这可以让我们知道音乐播放完毕。现在,我们并不需要关心这个方法是否成功——只要在这个方法被调用时播放下一个音乐就好了。 ### 按下 Play 键 -现在我们已经完成了`SoundManager`,我们就需要跟它说开始,我们就有音乐一直在无限循环播放了。快速进入`GameViewController.swift`,然后将下面这行代码放到第一次设置场景的地方: + +现在我们已经实现了 `SoundManager`,我们就需要告诉它什么时候开始运行,这样我们就有无限循环播放的背景音乐了。让我们重新打开 `GameViewController.swift` 文件,然后将下面这行代码放到初始化场景的地方: ``` SoundManager.sharedInstance.startPlaying() ``` -我们在 `GameViewController`里做这个是因为我们需要音乐独立于场景,如果我们在这个时候运行app,所有的东西已经被添加到项目中,我们就有背景音乐了! +我们在 `GameViewController` 里执行这个操作,是因为我们需要音乐独立于场景。如果我们在这个时候运行 app,而且所有的东西都已经被正确地添加到了项目中,我们就可以听到背景音乐了! + +在本课中,我们主要实现了两个部分:精灵动画和声音。我们使用了一个基于帧的动画来使精灵可以动起来,用了 SKAction 来实现,并使用了一些方法来重设我们被雨滴击中的小猫。我们使用了 `SKAction` 来添加了音效,并指定了当小猫被雨击中时来播放音效。 最后,我们为我们的游戏添加了初始背景音乐。 + +到这里,恭喜!我们的游戏即将完成!如果你有什么不明白的地方,请仔细检查我们在 [在Github](https://github.com/thirteen23/RainCat/releases/tag/smashing-magazine-lesson-two) 上的代码。 + +你做的怎么样了?你的代码和我的差不多吗?如果你做了一些修改,或者有更好的更新,可以通过评论让我知道。 -在本课中,我们讨论了两个主要议题:sprite动画和声音。 我们使用一个基于框架的动画来使sprite有动画,使用SKAction来进行动画,并使用方法来纠正我们的猫被雨滴到。 我们添加了声音效果使用`SKAction`,并指定猫被雨滴到时播放音乐。 最后,我们为我们的游戏添加了初始背景音乐。 -到这里,恭喜!我们的游戏即将完成!如果有什么地方疏忽,请仔细检查代码[在Github](https://github.com/thirteen23/RainCat/releases/tag/smashing-magazine-lesson-two)[31](#31). -你是怎么完成的?你的代码和我的差不多吗?如果有一些修改,或者有更好的更新,可以通过评论让我知道。 第三节课即将到来! #### 附录 From 8768dd524444547381eacba8ff415d3761e0fcd4 Mon Sep 17 00:00:00 2001 From: Freya Yu Date: Mon, 19 Dec 2016 16:53:05 +0800 Subject: [PATCH 10/38] Update Version 2.1 --- .../how-to-build-a-spritekit-game-in-swift-3-part-2.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md b/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md index 6611e5d1f47..974603c7c26 100644 --- a/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md +++ b/TODO/how-to-build-a-spritekit-game-in-swift-3-part-2.md @@ -587,17 +587,17 @@ public func update(deltaTime : TimeInterval, foodLocation: CGPoint) { ### 现在来添加音乐吧 -在我们开始写代码前,我们应该先要找点音效。一般来说,在寻找音效时,我只会搜索一些类似于 “cat meow royalty free” 的关键词。第一个匹配的通常是 [SoundBible.com](http://soundbible.com/tags-cat-meow.html)[13](#13),它通常会提供一些免费的音效。请务必阅读许可证。如果你不打算发布你的应用,那么就不需要关心许可证,因为这只是个个人应用。可是,如果你想要在 App store 中发售它,或者通过别的方式发布它,那么就请确保附上了 Creative Commons Attribution 3.0 或者是类似的许可证。这里有许多种许可证,所以当你使用别人的作品前,请确定你找到了相对应的许可证。 +在我们开始写代码前,我们应该先要找点音效。一般来说,在寻找音效时,我只会搜索一些类似于 “cat meow royalty free” 的关键词。第一个匹配的通常是 [SoundBible.com](http://soundbible.com/tags-cat-meow.html),它会提供一些免费的音效。请务必阅读使用许可证。如果你不打算发布你的应用,那么就不需要关心许可证,因为这只是个个人应用。可是,如果你想要在 App store 中发售它,或者通过别的方式发布它,那么就请确保附上了 Creative Commons Attribution 3.0 或者是类似的许可证。这里有许多种许可证,所以当你使用别人的作品前,请确定你找到了相对应的许可证。 在该应用中使用的音效都是通过 Creative Commons-licensed 授权并且免费使用的。为了之后的操作,我们需要将之前下载的 `SFX` 文件夹移动到 `RainCat` 文件夹中。 -[![Finder 模式已激活](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Finder-Mode-Activated-preview-opt.png) ](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Finder-Mode-Activated-large-opt.png)[14](#14) +[![Finder 模式已激活](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Finder-Mode-Activated-preview-opt.png) ](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Finder-Mode-Activated-large-opt.png) -把音效添加到文件系统中。 ([查看源文件](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Finder-Mode-Activated-large-opt.png)[15](#15)) +把音效添加到文件系统中。 ([查看源文件](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Finder-Mode-Activated-large-opt.png))) -在你把这些文件拷贝到项目中之后,你需要用 Xcode 来把它们添加到你的项目中。在 “Support” 文件夹下新建一个名为 “SFX” 的 group。右键点击这个group 然后点击 “Add Files to RainCat…” 选项 +在你把这些文件拷贝到项目中之后,你需要用 Xcode 来把它们添加到你的项目中。在 “Support” 文件夹下新建一个名为 “SFX” 的 group。右键点击这个group 然后点击 “Add Files to RainCat…” 选项。 -[![添加音效](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Adding-in-SFX-preview-opt.png) ](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Adding-in-SFX-preview-opt.png)[16](#16) +[![添加音效](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Adding-in-SFX-preview-opt.png) ](https://www.smashingmagazine.com/wp-content/uploads/2016/10/Adding-in-SFX-preview-opt.png) 添加音效 From 68e77fe7213dfef3eae3280bb965fca5d0466567 Mon Sep 17 00:00:00 2001 From: AidenLiudm Date: Tue, 20 Dec 2016 19:01:00 +0800 Subject: [PATCH 11/38] translate progressive-web-amps --- TODO/progressive-web-amps.md | 190 ++++++++++++++++------------------- 1 file changed, 86 insertions(+), 104 deletions(-) diff --git a/TODO/progressive-web-amps.md b/TODO/progressive-web-amps.md index 70f719b81cf..dc6b7b8429b 100644 --- a/TODO/progressive-web-amps.md +++ b/TODO/progressive-web-amps.md @@ -1,92 +1,84 @@ * 原文地址:[ Progressive Web AMPs ](https://www.smashingmagazine.com/2016/12/progressive-web-amps/) * 原文作者:[ Paul Bakaus ]( https://www.smashingmagazine.com/author/paulbakaus/) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) -* 译者: +* 译者:[L9m](https://github.com/L9m) * 校对者: -# Progressive Web AMPs +# 渐进增强的 Web 体验(Progressive Web Apps) -If you’ve been following the web development community these last few months, chances are you’ve read about [progressive web apps](https://www.smashingmagazine.com/2016/08/a-beginners-guide-to-progressive-web-apps/)(PWAs). It’s an umbrella term used to describe web experiences so advanced that they compete with ever-so-rich and immersive native apps: [full offline support](https://www.smashingmagazine.com/2016/02/making-a-service-worker/),[installability](https://developers.google.com/web/fundamentals/engage-and-retain/app-install-banners/?hl=en), “Retina,” full-bleed imagery, sign-in support for personalization, fast, smooth in-app browsing, push notifications and a great UI. +如果你最近几个月一直关注着 Web 开发社区,可能你对[渐进增强的 Web 应用](https://www.smashingmagazine.com/2016/08/a-beginners-guide-to-progressive-web-apps/)(简称 PWAs)已有所了解。它是应用体验能与原生应用媲美的 Web 应用的统称:[不依赖网络连接](https://www.smashingmagazine.com/2016/02/making-a-service-worker/),[易安装](https://developers.google.com/web/fundamentals/engage-and-retain/app-install-banners/?hl=en),支持视网膜屏幕,支持无边距图像,支持登录和个性化,快速且流畅的应用体验,支持推送通知和一个好看的界面。 ![From Google’s Advanced Mobile Pages (AMP) to progressive web apps](https://www.smashingmagazine.com/wp-content/uploads/2016/12/progressive-web-amp-2.png) -A few of Google’s sample progressive web apps. -But even though the new [Service Worker API](https://developers.google.com/web/fundamentals/primers/service-worker/)allows you to cache away all of your website’s assets for an almost instant *subsequent* load, like when meeting someone new, the first impression is what counts. If the first load takes more than 3 seconds, the latest [DoubleClick study](https://www.doubleclickbygoogle.com/articles/mobile-speed-matters/) shows that more than 53% of all users will drop off. +一些 Google 的渐进式 Web 应用示例。 -And 3 seconds, let’s be real, is already a quite *brutal* target. On mobile connections, which often average around a 300-millisecond latency and come with other constraints such as limited bandwidth and a sometimes poor signal connection, you might be left with a total load performance budget of less than 1 second to actually do the things you need to do to initialize your app. +虽然新的 [Service Worker API](https://developers.google.com/web/fundamentals/primers/service-worker/) 允许离线缓存所有的网站资源以便在*后续*加载中瞬时加载。就像陌生人的第一印象很关键。最新的 [DoubleClick 研究](https://www.doubleclickbygoogle.com/articles/mobile-speed-matters/) (译者注:DoubleClick 是谷歌旗下一家公司)表明,如果首次加载超过 3秒,超过 53% 的用户将放弃访问。 + +老实说,3秒已是一个相当*严峻*的目标。移动端连接通常有平均 300ms 延迟,而且附带有带宽限制和时不时信号弱等不利情况,你可能只剩下不到 1 秒时间留给应用初始化等事情。 ![From Google’s Advanced Mobile Pages (AMP) to progressive web apps](https://www.smashingmagazine.com/wp-content/uploads/2016/12/progressive-web-amp-7.png) -The delays that sit between your user and your content. +用户和内容之间的延迟。 -Sure, there are ways to [mitigate](https://codelabs.developers.google.com/codelabs/your-first-pwapp/#4) this problem of a slow first load — prerendering a basic layout on the server, lazy-loading chunks of functionality and so on — but you can only get so far with this strategy, and you have to employ, or be, a front-end performance wizard. +当然,有一些方法能[缓解](https://codelabs.developers.google.com/codelabs/your-first-pwapp/#4)首次加载缓慢的问题 — 在服务器上预先渲染好一个基础结构,再懒加载各个功能模块等等 — 但是使用此种策略达到的优化程度也有限,而且不得不雇佣一个前端优化专家。 -So, if an almost instant first load is fundamentally at odds with a native-like app experience, what can we do? +那么,我们有什么方法来做一个(根本)和原生应用不同的瞬时加载呢? -### AMP, For Accelerated Mobile Pages +### AMP ,为移动页面加速 -One of the most significant advantages of a website is almost frictionless entry — that is, no installation step and almost instant loading. A user is always just a click away. +网站的最重要的优势之一是跨平台——无需安装和即刻加载。用户用完即走。 -To really benefit from this opportunity for effortless, ephemeral browsing, all you need is a crazy-fast-loading website. And all you need to make your website crazy fast? A proper diet: no megabyte-sized images, no render-blocking ads, no 100,000 lines of JavaScript, just the content, please. +要想轻松地从短浏览(ephemeral browsing)的机会中收益,所需的就是一个瞬时加载(crazy-fast-loading)的网站。让网站瞬时加载,你需要做些什么呢?你所需做的只是一个适当的节制:没有兆字节大小图片,阻塞渲染的广告,没有十万行 JavaScript,就只有这些要求。 -[AMPs](https://www.ampproject.org/), short for Accelerated Mobile Pages, are [very good at this](https://www.ampproject.org/docs/get_started/technical_overview.html)[10](#10). In fact, it is their *raison d’être*. It’s like a car-assist feature that forces you to stay in the fast lane by enforcing a set of sensible rules to always prioritize your page’s main content. And by creating this strict, [statically](https://www.ampproject.org/docs/get_started/technical_overview.html#size-all-resources-statically)[11](#11) laid out environment, it enables platforms such as Google Search to get one step closer to “instant” by [preloading just the first viewport](https://www.ampproject.org/docs/get_started/technical_overview.html#load-pages-in-an-instant)[12](#12). +[AMPs](https://www.ampproject.org/),是加速移动网页(Accelerated Mobile Pages)的简称,[擅长于此](https://www.ampproject.org/docs/get_started/technical_overview.html),实际上,这是它们*存在的原因*(raison d’être)。它就像一个驾驶辅助功能,通过实行一套合理的规则,优化你的网页主体内容,让它们处于快车道。并通过创建这种严格的,[静态的](https://www.ampproject.org/docs/get_started/technical_overview.html#size-all-resources-statically) 布局环境,使譬如像谷歌搜索等平台[仅预渲染首屏](https://www.ampproject.org/docs/get_started/technical_overview.html#load-pages-in-an-instant),得以进一步接近”瞬时“。 ![From Google’s Advanced Mobile Pages (AMP) to progressive web apps](https://www.smashingmagazine.com/wp-content/uploads/2016/12/progressive-web-amp-0.png) -The (first) hero image and the headline of this AMP will be prerendered, so that the visitor can see its (above-the-fold) content instantly. +此 AMP 的首屏横幅图片(hero image)和标题将被提前渲染,以便访问者瞬时看到首屏内容。 -### AMP Or PWA? +### AMP 还是 PWA? -To make the experience reliably fast, you need to live with some constraints when implementing AMP pages. AMP isn’t useful when you need highly dynamic functionality, such as Push Notifications or Web Payments, or really anything requiring additional JavaScript. In addition, since AMP pages are usually served from an AMP Cache, you won’t get the biggest Progressive Web App benefits on that first click, since your own Service Worker can’t run. On the other hand, a Progressive Web App can never be as fast as AMP on that first click, as platforms can safely and cheaply prerender AMP pages – a feature that also makes embedding simpler (e.g. into an inline viewer). +AMP 可靠快速的体验,在实现时也伴随着一些限制。当你需要高度动态的功能时,AMP 是不适用的,譬如推送通知,网络支付和依靠额外 JavaScript 的功能。此外,因为 AMP 页面通常从 AMP 缓存中提供,你的 Service Worker 不能运行,首次访问享受不到渐进式 Web 应用的重要好处。另一方面,在首次访问的速度上,渐进式 Web 应用永远不及 AWP,因为平台能顺利且毫不费力地预渲染 AMP 页面—内嵌更简单(比如,在内嵌浏览器中)。 ![From Google’s Advanced Mobile Pages (AMP) to progressive web apps](https://www.smashingmagazine.com/wp-content/uploads/2016/12/progressive-web-amp-8.png) -Once the user leaves the AMP Cache as they click an internal link, you can enhance the website by installing service workers to make the website available offline (and more). - -So, AMP or progressive web app? Instant delivery and optimized delivery, or the latest advanced platform features and flexible application code? What if there was a way to combine the two, to reap the benefits of both? - -### The Perfect User Journeyr - -In the end, what matters is the ideal user experience you’re aiming for — the **user journey**. It goes like this: - -1. A user discovers a link to your content and clicks it. -2. The content loads almost instantly and is a pleasure to consume. -3. The user is invited and automatically upgraded to an even richer experience, with smoother in-app navigation, push notifications and offline support. -4. The user exclaims, “Why, hello. Heck yeah!” and is instantly redirected to an app-like experience and they can install the site onto their home screen. - -The first hop to your website should feel almost instant, and the browsing experience should get more and more engaging afterwards. - -Sound too good to be true? Well, what if we **combine the two technologies** — although at the first glance, they are seemingly unrelated and solve different needs? - -### PWAMP Combination Patterns - -To get to instant-loading, auto-upgrading experience, all you need to do is combine the laser-sharp leanness of AMPs with the richness of progressive web apps in one (or multiple) of the following ways: - -- **AMP as PWA** +一旦用户点击内部链接,离开 AMP 缓存,你就能通过安装 service worker 来增强网站,让网站支持离线和更多的功能。 -When you can live with AMP’s limitations. +那么,是 AMP 还是渐进式 Web 应用?瞬时交付还是优化交付,或是最先进的平台功能和灵活的应用代码?有没有一种结合两者的好处的方式呢? -- **AMP to PWA** +### 完美的用户旅程(User Journey) +终究,重要的是针对**用户旅程**的理想体验。它大概是这样的: +1. 用户点击你的内容链接。 +2. 内容快速加载是一种愉快的体验。 +3. 用户被通知并进阶到有推送通知和支持离线的更流畅的体验。 +4. 立即重定向到一个类原生的体验,并且可将网站放在你的主屏幕上。用户惊呼:“怎么回事?好神奇!”。 +访问网站的第一步应该让人感觉快速,其后的浏览体验应该越来越引人入胜。 -When you want to smoothly transition between the two. +听起来是不是好的难以置信?好吧,尽管乍看,它们解决不同的需求且不相关,要是我们**结合两种技术**会怎么样呢? -- **AMP in PWA** +### PWAMP 结合模式 +要获得瞬时加载,渐进增强的体验,你所需做的是将 AMPs 和渐进式 Web 应用的丰富功能用下列之一(或多)的方式相结合: -When you want to reuse AMPs as a data source for your PWA. +- **AMP 作为 PWA** +当你可以接受 AMP 的限制时。 -Let’s walk through each of them individually. +- **AMP 转作 PWA** +当你想要在两者之间平滑过渡时。 -#### AMP as PWA +- **AMP 在 PWA 中** +当你重用 AMP 为 PWA 的数据源时。 -Many websites won’t ever need things beyond the boundaries of AMPs. [Amp by Example](https://ampbyexample.com/)[16](#16)[15](#15), for instance, is both an AMP and a progressive web app: +让我们每个都过一遍吧。 -- It has a service worker and, therefore, allows offline access, among other things. -- It has a manifest, prompting the “Add to Homescreen” banner. +#### AMP 作为 PWA +许多网站其实用不到超出 AMPs(功能)范围。[Amp by Example](https://ampbyexample.com/) 就是一个例子,它既是 AMP 也是一个渐进式 Web 应用。 +- 它有 service worker,因此允许包括离线访问等在内的其他功能。 +- 它有清单(manifest),在网页横幅上会提醒“添加到主屏幕“。 -When a user visits [Amp by Example](https://ampbyexample.com/)[16](#16)[15](#15) from a Google search and then clicks on another link on that website, they navigate away from the AMP Cache to the origin. The website still uses the AMP library, of course, but because it now lives on the origin, it can use a service worker, can prompt to install and so on. +当用户从谷歌搜索访问 [Amp by Example](https://ampbyexample.com/),然后点击网站上链接,它们将从 AMP 缓存页面转到源页面上。当然,网站仍在使用 AMP 库,但是现在由于它处于源页面上,它能使用 service worker 或提示安装等等。 -You can use this technique to enable offline access to your AMP website, as well as extend your pages as soon as they’re served from the origin, because you can modify the response via the service worker’s `fetch` event, and return as a response whatever you want: +你可以使用此项技术让你的 AMP 网站支持离线访问,一旦他们访问源页面就进行扩展,因为你可以通过 service worker 的 `fetch` 事件来修改响应(response),并返回你想要的响应 (response)。 ``` function createCompleteResponse (header, body) { @@ -106,18 +98,16 @@ function createCompleteResponse (header, body) { } ``` -This technique allows you to insert scripts and more advanced functionality outside of the scope of AMPs on subsequent clicks. +这一技术也允许你在 AMP 后续访问中插入脚本,提供超出 AMP 范围外的更进阶的功能。 -#### AMP to PWA +#### AMP 转作 PWA -When the above isn’t enough, and you want a dramatically different PWA experience around your content, it’s time for a slightly more advanced pattern: - -- All content “leaf” pages (those that have specific content, not overview pages) are published as AMPs for that nearly instant loading experience. -- These AMPs use AMP’s special element [``](https://www.ampproject.org/docs/reference/extended/amp-install-serviceworker.html)[17](#17) to warm up a cache and the PWA shell **while the user is enjoying** your content. -- When the user clicks another link on your website (for example, the call to action at the bottom for a more app-like experience), the service worker intercepts the request, takes over the page and loads the PWA shell instead. - -You can implement the experience above in three easy steps, provided you are familiar with how service workers work. (If you aren’t, then I greatly recommend my colleague [Jake’s Udacity course](https://www.udacity.com/course/offline-web-applications--ud899)[18](#18)). First, install the service worker on all of your AMPs: +当上述不能满足时,并且你想让内容有一个完全不同的 PWA 体验时,是时候用一种更高级一点的模式了: +- 为了接近瞬时加载的体验,所有内容“叶”页(指有特定内容,不是概述的页面)被发布成 AMP。 +- 这些 AMPs 使用 AMP 的特殊元素 [``](https://www.ampproject.org/docs/reference/extended/amp-install-serviceworker.html) 来预备缓存,并且**当用户喜欢**你的内容时用 PWA 的外壳。 +- 当用户点击你网站上的另一个链接(比如,在底部的行为召唤(按钮),为了就更像原生) service worker 拦截请求并接管页面,然后加载 PWA 外壳替代之。 +你可以通过以上三个简单步骤来实现这种体验,假如你熟悉 service worker 的运作(如果你不清楚的话,强烈推荐我的同事[杰克在优达学城(Udacity)上的课程](https://www.udacity.com/course/offline-web-applications--ud899))。第一步,在你所有的 AMP 上放置 service worker。 ``` ``` -Secondly, in the service worker’s installation step, cache any resources that the PWA will need: - +第二步,在 service worker 安装过程中,缓存 PWA 所需的所有资源。 ``` var CACHE_NAME = 'my-site-cache-v1'; var urlsToCache = [ @@ -147,8 +136,7 @@ self.addEventListener('install', function(event) { }); ``` -Finally, again in the service worker, respond with the PWA instead of the requested AMP on navigation requests. (The code below, while functional, is overly simplified. A more advanced example follows in the demo at the end.) - +最后,又回到 service worker,拦截 AMP 的导航请求,用 PWA 替代响应。(下面的代码是功能简化版本,后面还有一个更进阶的示例。) ``` self.addEventListener('fetch', event => { if (event.request.mode === 'navigate') { @@ -160,16 +148,15 @@ self.addEventListener('fetch', event => { }); ``` -Now, whenever a user clicks on a link on your page served from the AMP Cache, the service worker registers the `navigate` request mode and takes over, then responds with the full-blown, already-cached PWA instead. +现在,每当用户点击从 AMP 缓存页面上的链接,service worker 注册 `navigate` 请求模式(request mode)并接管,然后用已缓存的成熟(full-brown)的 PWA 代替。 ![From Google’s Advanced Mobile Pages (AMP) to progressive web apps](https://www.smashingmagazine.com/wp-content/uploads/2016/12/progressive-web-amp-6.png) -You can progressively enhance the website by installing service workers. For browsers that don’t support service workers, they will just move to another page within the AMP Cache. - -What’s especially interesting about this technique is that you are now using progressive enhancement to go from AMP to PWA. However, this also means that, as is, browsers that don’t yet support service workers will jump from AMP to AMP and will never actually navigate to the PWA. +你可以通过在网站上安装一些 service worker 来实行渐进增强。对于不支持 service worker 的浏览器,它们仅会转移到 AMP 缓存页面。 -AMP solves this with something called [shell URL rewriting](https://www.ampproject.org/docs/reference/components/amp-install-serviceworker#shell-url-rewrite)[20](#20). By adding a fallback URL pattern to the `` tag, you are instructing AMP to rewrite all matching links on a given page to go to another legacy shell URL instead, if no service worker support has been detected: +此项技术很有意思之处在于从 AMP 渐进增强到 PWA。然而,这也意味着,暂时不支持 service worker 的浏览器将从 AMP 跳到 AMP 并且不会导航到 PWA。 +AMP 通过 [外壳 URL 重写](https://www.ampproject.org/docs/reference/components/amp-install-serviceworker#shell-url-rewrite) 来跳转。通过在 `` 标签中添加一个备用 URL模式(URL pattern),如果检测到不支持 service worker,就指示 AMP 重写特定页面上所有匹配的链接,用另一个传统的 shell URL 替代(Shell 是应用的用户界面所需的最基本的 HTML、CSS 和 JavaScript,也是一个用来确保应用有好多性能的组件。它的首次加载将会非常快,加载后立刻被缓存下来。这意味着应用的外壳不需要每次使用时都被下载,而是只加载需要的数据。 ): ``` ``` -With these attributes in place, all subsequent clicks on an AMP will go to your PWA, regardless of any service worker. Pretty neat, huh? +在有 service worker 的情况下具有了这些属性,AMP 上所有后续点击都将转到 PWA。挺巧妙的,是吧? -#### AMP in PWA +#### AMP 在 PWA 中 +那么,现在用户处于渐进式 Web 应用中,你可能会使用一些 AJAX 驱动(AJAX-driven)的导航,通过 JSON 来获取内容。你当然可以这么做,但是现在有两个完全不同的内容后端和基础架构需求——一个生成 AMP 页面,另外一个为你的渐进式 Web 应用提供基于 JSON 格式的接口。 -So, now the user is in the progressive web app, and chances are you’re using some AJAX-driven navigation that fetches content via JSON. You can certainly do that, but now you have these crazy infrastructure needs for two totally different content back ends — one generating AMP pages, and the other offering a JSON-based API for your progressive web app. - -But think for a second about what an AMP really is. It’s not just a website. It’s designed as a ultra-portable content unit. An AMP is self-contained by design and can be safely embedded into another website. What if we could dramatically simplify the back-end complexity by ditching the additional JSON API and instead reuse AMP as the data format for our progressive web app? +但请想一想 AMP 的本质是什么。它不只是一个网站,它被设计成一个超轻便的内容单元。AMP 是独立的且可以顺利地嵌入到其他网站。我们是否可以抛弃 JSON 接口,使用 AMP 作为我们渐进式 Web 应用的数据格式,从而大大降低后端复杂性呢? ![From Google’s Advanced Mobile Pages (AMP) to progressive web apps](https://www.smashingmagazine.com/wp-content/uploads/2016/12/progressive-web-amp-3.png) -AMP pages can be safely embedded into another website — the AMP library is compiled and loaded only once for the entire PWA. - -Of course, one easy way to do this would be simply to load AMP pages in frames. But iframes are slow, and then you’d need to recompile and re-initialize the AMP library over and over. Today’s cutting-edge web technology offers a better way: the shadow DOM. - -The process looks like this: +AMP 页面能顺利地嵌入其他网站中—PWA 的 AMP库只会编译并加载一次。 -1. The PWA hijacks any navigation clicks. -2. Then, it does an XMLHttpRequest to fetch the requested AMP page. -3. It puts the content into a new shadow root. -4. And it tells the main AMP library, “Hey, I have a new document for you. Check it out!” (calling `attachShadowDoc` on runtime). +当然,一个简单的方法是在 frames 中加载 AMP 页面。但是使用 iframes 比较慢,并且需要你一遍又一遍地重新编译和初始化 AMP 库。现在前沿的 Web技术提供了一种更好的方式:Shadow DOM。 -Using this technique, the AMP library is compiled and loaded only once for the entire PWA, and then is responsible for every shadow document you attach to it. And, because you’re fetching pages via XMLHttpRequests, you can modify the AMP source before inserting it into a new shadow document. You could do this, for instance, to: +处理过程看起来是这样的: +1. PWA 操纵所有的导航点击事件。 +2. 然后,用 XMLHttpRequest 获取请求的 AMP 页面。 +3. 将内容放入一个新的 shadow root 中。 +4. 然后返回给主 AMP 库,”嘿,我有一个新文档给你。请查收!“(在运行时调用 `attachShadowDoc` )。 -- strip out unnecessary things, such as headers and footers; -- insert additional content, such as more obnoxious ads or fancy tooltips; -- replace certain content with more dynamic content. +使用此种技术,整个 PWA 只会编译和加载一次 AMP 库,并且然后,因为你是通过 XMLHttpRequests 获取的页面,你能在 AMP 源插入新的 shadow document 之前进行一些修改,你可以像这样做: +- 去掉不必要的内容,比如页眉(headers)和页脚(footerds); +- 插入额外的内容,比如令人反感的广告或信息提示; +- 用更动态的内容替换特定内容。 -Now, you’ve made your progressive web app much less complex, and you’ve dramatically reduced the complexity of your back-end infrastructure. +现在,你使你的渐进式 Web 应用更简单了,而且大大简化了后端结构。 -### Ready, Set, Action! - -To demonstrate the shadow DOM approach (i.e. an AMP within a PWA), the AMP team has created a [React-based demo called The Scenic,](https://choumx.github.io/amp-pwa/)[22](#22) a fake travel magazine: +### 准备,配置,实行! +AMP 团队做了一个[名为 The Scenic 的 React 示例](https://choumx.github.io/amp-pwa/) 来演示 shadom DOM 方法(也就是:PWA 中的 AMP),它是一本假的旅行杂志: ![From Google’s Advanced Mobile Pages (AMP) to progressive web apps](https://www.smashingmagazine.com/wp-content/uploads/2016/12/progressive-web-amp-4.png) -The [whole demo](https://github.com/ampproject/amp-publisher-sample/blob/master/amp-pwa)is on GitHub, but the magic happens in [`amp-document.js`’ React component](https://github.com/ampproject/amp-publisher-sample/blob/master/amp-pwa/src/components/amp-document/amp-document.js#L92)[25](#25). - -#### Show me something real + [整个示例](https://github.com/ampproject/amp-publisher-sample/blob/master/amp-pwa)的代码在 Github 上,但关键代码在 [React 组件 `amp-document.js`’ ](https://github.com/ampproject/amp-publisher-sample/blob/master/amp-pwa/src/components/amp-document/amp-document.js#L92)中。 -For a real production example, take a look at [Mic’s new PWA)](https://beta.mic.com) (in beta): If you shift-reload a [random article](https://beta.mic.com/articles/161568/arrow-season-5-episode-9-a-major-character-returns-in-midseason-finale-maybe)[27](#27) (which will ignore the Service Worker temporarily) and look at the source code, you’ll notice it’s an AMP page. Now try clicking on the hamburger menu: It reloads the current page, but since `` has *already installed* the PWA app shell, the reload is almost *instant*, and the menu is open after the refresh, making it look like no reload has happened. But now you’re in the PWA (that embeds AMP pages), bells and whistles and all. Sneaky, but magnificent. +#### 看点真东西 -### (Not So) Final Thoughts +一个真实产品的例子是 [Mic 新的 PWA](https://beta.mic.com)(beta 阶段), 研究一下 :如果你按住 shift 重新刷新(shift-reload)[任意文章](https://beta.mic.com/articles/161568/arrow-season-5-episode-9-a-major-character-returns-in-midseason-finale-maybe)(暂时忽略了 service worker)查看源代码, 你会注意到这是一个 AMP 页面。现在尝试点击一下菜单: 它会重新加载当前页面, 但由于 `` *已存在*于 PWA 应用外壳中,重载几乎是*瞬间*完成的,并且菜单在刷新后打开,使其看起来不像是重新加载过一样。但现在你处于拥有其他丰富功能的(内嵌 AMP 页面)PWA 中。狡猾,但很了不起。 -Needless to say, I’m extremely excited about the potential of this new combination. It’s a combination that brings out the best of both. +### 结语 -Recapping the highlights: +无需多说,我非常激动地憧憬着新结合的潜力。这个结合集两者之所长。 -- always fast, no matter what; -- great distribution built-in (through AMP’s platform partners); -- progressively enhanced; -- one back end to rule them all; -- less client complexity; -- less overall investment; +优点概括: +- 不论什么情况,都很快; +- 良好的内置支持(通过 AMP 的平台伙伴); +- 渐进增强; +- 只需一种后端接口; +- 降低客户端复杂性; +- 成本低; -But we’re just starting to discover variations of the pattern, as well as completely new ones. Build the best web experiences that 2016 and beyond have to offer. Onward, to a new chapter of the web! +但是我们才开始发掘这种模式的变型,也是全新的一种。除提供构建 2016 最好的 Web 体验之外,继续向前到达 Web 的新篇章。 From 13eb9d5f419763fba022dfb8c1815868d4220a94 Mon Sep 17 00:00:00 2001 From: AidenLiudm Date: Tue, 20 Dec 2016 19:22:43 +0800 Subject: [PATCH 12/38] translate progressive-web-amps --- TODO/progressive-web-amps.md | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/TODO/progressive-web-amps.md b/TODO/progressive-web-amps.md index dc6b7b8429b..821f60cf9f9 100644 --- a/TODO/progressive-web-amps.md +++ b/TODO/progressive-web-amps.md @@ -13,9 +13,9 @@ 一些 Google 的渐进式 Web 应用示例。 -虽然新的 [Service Worker API](https://developers.google.com/web/fundamentals/primers/service-worker/) 允许离线缓存所有的网站资源以便在*后续*加载中瞬时加载。就像陌生人的第一印象很关键。最新的 [DoubleClick 研究](https://www.doubleclickbygoogle.com/articles/mobile-speed-matters/) (译者注:DoubleClick 是谷歌旗下一家公司)表明,如果首次加载超过 3秒,超过 53% 的用户将放弃访问。 +虽然新的 [Service Worker API](https://developers.google.com/web/fundamentals/primers/service-worker/) 允许离线缓存所有的网站资源以便在*后续*加载中瞬时加载。就像陌生人的第一印象很关键。最新的 [DoubleClick 研究](https://www.doubleclickbygoogle.com/articles/mobile-speed-matters/) (译者注:DoubleClick 是谷歌旗下一家公司)表明,如果首次加载超过 3 秒,超过 53% 的用户将放弃访问。 -老实说,3秒已是一个相当*严峻*的目标。移动端连接通常有平均 300ms 延迟,而且附带有带宽限制和时不时信号弱等不利情况,你可能只剩下不到 1 秒时间留给应用初始化等事情。 +老实说,3 秒已是一个相当*严峻*的目标。移动端连接通常有平均 300ms 延迟,而且附带有带宽限制和时不时信号弱等不利情况,你可能只剩下不到 1 秒时间留给应用初始化等事情。 ![From Google’s Advanced Mobile Pages (AMP) to progressive web apps](https://www.smashingmagazine.com/wp-content/uploads/2016/12/progressive-web-amp-7.png) @@ -31,7 +31,7 @@ 要想轻松地从短浏览(ephemeral browsing)的机会中收益,所需的就是一个瞬时加载(crazy-fast-loading)的网站。让网站瞬时加载,你需要做些什么呢?你所需做的只是一个适当的节制:没有兆字节大小图片,阻塞渲染的广告,没有十万行 JavaScript,就只有这些要求。 -[AMPs](https://www.ampproject.org/),是加速移动网页(Accelerated Mobile Pages)的简称,[擅长于此](https://www.ampproject.org/docs/get_started/technical_overview.html),实际上,这是它们*存在的原因*(raison d’être)。它就像一个驾驶辅助功能,通过实行一套合理的规则,优化你的网页主体内容,让它们处于快车道。并通过创建这种严格的,[静态的](https://www.ampproject.org/docs/get_started/technical_overview.html#size-all-resources-statically) 布局环境,使譬如像谷歌搜索等平台[仅预渲染首屏](https://www.ampproject.org/docs/get_started/technical_overview.html#load-pages-in-an-instant),得以进一步接近”瞬时“。 +[AMPs](https://www.ampproject.org/),是加速移动网页(Accelerated Mobile Pages)的简称,它[擅长于此](https://www.ampproject.org/docs/get_started/technical_overview.html),实际上,这是它们*存在的原因*(raison d’être)。它就像一个驾驶辅助功能,通过实行一套合理的规则,优化你的网页主体内容,让它们处于快车道。并通过创建这种严格的,[静态的](https://www.ampproject.org/docs/get_started/technical_overview.html#size-all-resources-statically)布局环境,使譬如像谷歌搜索等平台[仅预渲染首屏](https://www.ampproject.org/docs/get_started/technical_overview.html#load-pages-in-an-instant),得以进一步接近”瞬时“。 ![From Google’s Advanced Mobile Pages (AMP) to progressive web apps](https://www.smashingmagazine.com/wp-content/uploads/2016/12/progressive-web-amp-0.png) @@ -39,7 +39,7 @@ ### AMP 还是 PWA? -AMP 可靠快速的体验,在实现时也伴随着一些限制。当你需要高度动态的功能时,AMP 是不适用的,譬如推送通知,网络支付和依靠额外 JavaScript 的功能。此外,因为 AMP 页面通常从 AMP 缓存中提供,你的 Service Worker 不能运行,首次访问享受不到渐进式 Web 应用的重要好处。另一方面,在首次访问的速度上,渐进式 Web 应用永远不及 AWP,因为平台能顺利且毫不费力地预渲染 AMP 页面—内嵌更简单(比如,在内嵌浏览器中)。 +AMP 可靠快速的体验,在实现时也伴随着一些限制。当你需要高度动态的功能时,AMP 是不适用的,譬如推送通知,网络支付和依靠额外 JavaScript 的功能。此外,因为 AMP 页面通常从 AMP 缓存中提供,你的 Service Worker 不能运行,首次访问享受不到渐进式 Web 应用的最重要的好处。另一方面,在首次访问的速度上,渐进式 Web 应用永远不及 AWP,因为平台能顺利且毫不费力地预渲染 AMP 页面—内嵌更简单(比如,在内嵌浏览器中)。 ![From Google’s Advanced Mobile Pages (AMP) to progressive web apps](https://www.smashingmagazine.com/wp-content/uploads/2016/12/progressive-web-amp-8.png) @@ -49,13 +49,15 @@ AMP 可靠快速的体验,在实现时也伴随着一些限制。当你需要 ### 完美的用户旅程(User Journey) 终究,重要的是针对**用户旅程**的理想体验。它大概是这样的: -1. 用户点击你的内容链接。 -2. 内容快速加载是一种愉快的体验。 -3. 用户被通知并进阶到有推送通知和支持离线的更流畅的体验。 -4. 立即重定向到一个类原生的体验,并且可将网站放在你的主屏幕上。用户惊呼:“怎么回事?好神奇!”。 + +1. 用户点击你的内容链接。 +2. 内容快速加载是一种愉快的体验。 +3. 用户被通知并进阶到有推送通知和支持离线的更流畅的体验。 +4. 立即重定向到一个类原生的体验,并且可将网站放在你的主屏幕上。用户惊呼:“怎么回事?好神奇!”。 + 访问网站的第一步应该让人感觉快速,其后的浏览体验应该越来越引人入胜。 -听起来是不是好的难以置信?好吧,尽管乍看,它们解决不同的需求且不相关,要是我们**结合两种技术**会怎么样呢? +听起来是不是好的难以置信?好吧,尽管乍看,它们解决不同的问题且不相关,要是我们**结合两种技术**会怎么样呢? ### PWAMP 结合模式 要获得瞬时加载,渐进增强的体验,你所需做的是将 AMPs 和渐进式 Web 应用的丰富功能用下列之一(或多)的方式相结合: @@ -105,7 +107,7 @@ function createCompleteResponse (header, body) { 当上述不能满足时,并且你想让内容有一个完全不同的 PWA 体验时,是时候用一种更高级一点的模式了: - 为了接近瞬时加载的体验,所有内容“叶”页(指有特定内容,不是概述的页面)被发布成 AMP。 - 这些 AMPs 使用 AMP 的特殊元素 [``](https://www.ampproject.org/docs/reference/extended/amp-install-serviceworker.html) 来预备缓存,并且**当用户喜欢**你的内容时用 PWA 的外壳。 -- 当用户点击你网站上的另一个链接(比如,在底部的行为召唤(按钮),为了就更像原生) service worker 拦截请求并接管页面,然后加载 PWA 外壳替代之。 +- 当用户点击你网站上的另一个链接(比如,在底部的行为召唤(按钮),使其更像原生)service worker 拦截请求并接管页面,然后加载 PWA 外壳替代之。 你可以通过以上三个简单步骤来实现这种体验,假如你熟悉 service worker 的运作(如果你不清楚的话,强烈推荐我的同事[杰克在优达学城(Udacity)上的课程](https://www.udacity.com/course/offline-web-applications--ud899))。第一步,在你所有的 AMP 上放置 service worker。 ``` @@ -156,7 +158,8 @@ self.addEventListener('fetch', event => { 此项技术很有意思之处在于从 AMP 渐进增强到 PWA。然而,这也意味着,暂时不支持 service worker 的浏览器将从 AMP 跳到 AMP 并且不会导航到 PWA。 -AMP 通过 [外壳 URL 重写](https://www.ampproject.org/docs/reference/components/amp-install-serviceworker#shell-url-rewrite) 来跳转。通过在 `` 标签中添加一个备用 URL模式(URL pattern),如果检测到不支持 service worker,就指示 AMP 重写特定页面上所有匹配的链接,用另一个传统的 shell URL 替代(Shell 是应用的用户界面所需的最基本的 HTML、CSS 和 JavaScript,也是一个用来确保应用有好多性能的组件。它的首次加载将会非常快,加载后立刻被缓存下来。这意味着应用的外壳不需要每次使用时都被下载,而是只加载需要的数据。 ): +AMP 通过 [Shell URL 重写](https://www.ampproject.org/docs/reference/components/amp-install-serviceworker#shell-url-rewrite) 来跳转。通过在 `` 标签中添加一个备用 URL 模式(URL pattern),如果检测到不支持 service worker,就指示 AMP 重写特定页面上所有匹配的链接,用另一个传统的 shell URL 替代(外壳(Shell)是应用的用户界面所需的最基本的 HTML、CSS 和 JavaScript,也是一个用来确保应用有好多性能的组件。它的首次加载将会非常快,加载后立刻被缓存下来。这意味着应用的外壳不需要每次使用时都被下载,而是只加载需要的数据。 ): + ``` ` *已存在*于 PWA 应用外壳中,重载几乎是*瞬间*完成的,并且菜单在刷新后打开,使其看起来不像是重新加载过一样。但现在你处于拥有其他丰富功能的(内嵌 AMP 页面)PWA 中。狡猾,但很了不起。 +一个真实产品的例子是 [Mic 新的 PWA](https://beta.mic.com)(beta 阶段),研究一下 :如果你按住 shift 重新刷新(shift-reload)[任意文章](https://beta.mic.com/articles/161568/arrow-season-5-episode-9-a-major-character-returns-in-midseason-finale-maybe)(这样暂时忽略 service worker)查看源代码, 你会注意到这是一个 AMP 页面。现在尝试点击一下菜单:它会重新加载当前页面, 但由于 `` *已存在*于 PWA 应用外壳中,重载几乎是*瞬间*完成的,并且菜单在刷新后打开,使其看起来不像是重新加载过一样。但现在你处于拥有其他丰富功能的(内嵌 AMP 页面)PWA 中。狡猾,但很了不起。 ### 结语 From d83875d481fdb0eae4a8422731f1616320f52977 Mon Sep 17 00:00:00 2001 From: cdpath Date: Thu, 22 Dec 2016 00:24:09 +0800 Subject: [PATCH 13/38] Update check-in-frequency-and-codebase-impact-the-surprising-correlation.md --- ...ebase-impact-the-surprising-correlation.md | 76 +++++++++---------- 1 file changed, 37 insertions(+), 39 deletions(-) diff --git a/TODO/check-in-frequency-and-codebase-impact-the-surprising-correlation.md b/TODO/check-in-frequency-and-codebase-impact-the-surprising-correlation.md index 36705b61a32..79da935ef4d 100644 --- a/TODO/check-in-frequency-and-codebase-impact-the-surprising-correlation.md +++ b/TODO/check-in-frequency-and-codebase-impact-the-surprising-correlation.md @@ -1,82 +1,80 @@ * 原文地址:[ Prolific Engineers Take Small Bites — Patterns in Developer Impact ](https://blog.gitprime.com/check-in-frequency-and-codebase-impact-the-surprising-correlation/ ) * 原文作者:[ Ben Thompson ]( https://blog.gitprime.com/author/ben-thompson) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) -* 译者: +* 译者:[cdpath](https://github.com/cdpath) * 校对者: -# Prolific Engineers Take Small Bites — Patterns in Developer Impact +# 高效的工程师一步一步来 —— 开发者影响力中的模式 -The work patterns of engineers reveal interesting things about their character, but it’s not what you might imagine. +工程师的工作模式可以揭示他们性格中一些有趣的东西,不过和你想象的不太一样。 -### Our Hypothesis: Speedy vs. Bulky - -To explore this, we started with a hypothesis that some engineers on a team are better at turning work around fast, while others are better at burning through lots of work. If correct, this would lead to an interesting distribution of engineers across two axes: Frequency and Volume, yielding a chart that looked something like this: +### 前提假设:要速度还是要数量 +要探讨这个,我们得先假设团队里有的工程师更容易上手新工作,其他人则更喜欢一次搞定大量工作。如果假设成立,我们可以用频率和工作量这两个轴画出工程师的分布,就像这样: ![](https://blog.gitprime.com/hubfs/GitPrime/Blog/eng-character-1assumption-4.png?t=1481225729545) -But what does “volume” even mean? +但是「工作量」如何定义呢? -### Finding ‘Big’ +### 什么是「大」工作量? -We tried all versions of 'big' we could think of. Since Lines of Code (LoC) is simple (though much reviled!), we started there to get a baseline. LoC doesn't actually work as an operational metric, for reasons we explore [in more depth here](//blog.gitprime.com/lines-of-code-is-a-worthless-metric-except-when-it-isnt/), but it did provide a starting point for something better. +我们试了能想到的所有衡量「大」工作量的手段。因为代码行数 (LoC) 比较简单(也很能说明问题!),所以用它来做基准线。代码行数并不是切实可行的度量,详见这篇[更深入的文章](http://blog.gitprime.com/lines-of-code-is-a-worthless-metric-except-when-it-isnt/)。 -The challenge is to find some metric that account for the fact deleting 50 lines of code and replacing them with 5 lines of more efficient code is arguably 'bigger' (e.g. more work) than contributing 100 new lines of un-edited prototype code: purely green-field work is often much easier than hunting down a pesky bug that results in a 4-line change. +真正的挑战是找到一种度量,可以判断出用五行更高效的代码替换五十行旧代码的工作量要高于写一百行未编辑过的原型代码。毕竟从头开始写新代码可比追查一个可恶的 bug 结果只修改了四行代码要来得容易。 -We took a run at measuring the “bigness” of work from just about every other conceivable angle: overall code footprint, live lines in the codebase, lines that ‘stick,’ number of files hit, distinct edit locations — pretty much everything we could conceivably measure. +我们穷尽了能想到的所有办法尝试衡量工作量的大小:总体代码面积,源码库中有效代码行数,被持续使用的代码,用到的文件数,不同的编辑位置数。差不多能想到的手段都用上了。 -What ended up helping us crack this nut was the impressively large body of scholarly research around engineering work, particularly around commit normality: there is, in fact, such a thing as a ‘normal’ commit, and this ended up being the seed of a very reasonable proxy for engineering work, something we now call [‘Impact’](https://blog.gitprime.com/impact-a-better-way-to-measure-codebase-change). +最后帮我们搞定这个难题的是大量关于工程工作的学术研究,尤其是关于提交常态(commit normality)的,正是这个概念最终促使我们找到了可以公平衡量工程工作量的指标,也叫做[「影响力(Impact)」](https://blog.gitprime.com/impact-a-better-way-to-measure-codebase-change)。 -With impact, we cross-weighted several data points in an attempt to approximate the cognitive load of engineering work and, after a bit of tuning, found something that maps to developer intuitions much better than LoC alone ever could. +我们用影响力交叉衡量几个数据点,试着让数值接近我们认知上的工程工作量,再做一些调整,最后找到了比代码行数更符合开发者直觉的度量。 -### Learning that quick is big +### 快速学习很重要 -In analyzing millions of commits across thousands of teams, our initial theory about how engineers work turned out to be completely false. +分析了数千个团队的数百万个提交(commit)之后,我们最初提出的关于工程师如何工作的理论被彻底推翻了。 -Checking in frequently has such a tight correlation to an individual’s overall impact on the codebase that they are functional equivalents, resulting in banding that looks like this: +更新代码的频率和个人对源码库的综合影响力的关联如此紧密以至于两者在作用上完全相等了,两者的关系如下图所示: ![](https://blog.gitprime.com/hubfs/GitPrime/Blog/eng-character-2actual-1.png?t=1481225729545) -Commit frequency moves in lock-step with overall impact to the codebase, by any other measure we came up up with. +当调整我们能想到的任何度量时,提交频率和对源码库的综合影响力都以同样的速率变化。 -This correlation is so strong, in fact, in analyzing over 20 million commits across thousands of teams, we did not find any strong counter-examples to this. +这种相关性如此之强,实际上在分析了数千个团队超过两千万个提交之后,我们没有找到一个有力的反例。 -### High impact engineers take small, rapid bites +### 高影响力的工程师细分工作,提交频繁 -This is sort of fascinating in itself, because saying “we need to do more work” is kind of a crappy way to approach improving throughput of an engineering team. Most engineers are already doing their best, so if the team needs to “do more work,” it’s not immediately obvious how that’s going to happen and just results in bad feelings. +这个观点本身就比较有意思,因为提出「我们得干更多活」就是种蹩脚的,试图提高工程团队产出的方法。大多数工程师已经尽力了,所以如果团队要「干更多活」,不能马上看出来什么东西,结果就是伤害了大家的感情。 -What’s fascinating about this particular data set is that it suggests that there may be structural things to change the way we work that can net out to efficiency gains. +这个特殊的数据集有意思的地方在于,它暗示了有可能通过某种结构性的工作方式的调整来提升工作效率。 -Breaking work into granular bits is already considered a best practice, but this data suggests it may be more than that: encouraging this (and developing feedback loops that help make sure sure it’s happening) looks to be a more actionable path to encouraging higher individual impact. +将工作分成小部分已是公认的最佳实践,不过这个数据表明它比我们想象的还重要:鼓励细分工作(并且实行反馈机制帮助更好地实现)看起来是更可行的激励更高的个人影响力(impact)的手段。 -“Take small bites and check in frequently” is actionable and visible; saying “do more work” just acts as a stressor. +「细分工作,频繁提交」是可行且可见的;而说「干更多活」实际上只能增加压力。 -And this is a holistic gain: besides being generally good for an engineer’s ability to make a personal impact, smaller and more frequent commits yield other positive externalities for the rest of the team: +而且这是整体上的提升:除了通常有助于提高工程师的能力并提高个人影响力以外,更小更频繁的提交还可以为团队的其他成员带来额外的好处: -- More granular commits make it easier to find and track down bugs when something breaks -- It’s easier to roll back specific changes instead of a giant change set. -- Smaller work is focused and easier for team mates to review and integrate -- Lower process overhead for everyone with less merge commits +- 更细粒度的提交便于追踪 bug +- 撤回具体的改动比撤回一大块改动更容易。 +- 细分的工作重点明确,团队成员做代码审查和集成也更容易。 +- 更少的合并提交可以减少流程上的开支。 -### A successful picture of engineering activity +### 成功的工程活动图 -We did end up creating something that says a lot about engineering style, but it worked out a bit differently than we thought. We took everything we learned about impact and rolled this up into a single axis called *throughput*. Because of all our exploration, we were able to come up with a way of measuring code-impact that leaves behind the overly reductionist LoC view of software development (you can read more about how we’re calculating impact [here](http://help.gitprime.com/537-calculations/1606-what-is-impact)). +我们的确创造了能说明很多工程风格的东西,但是得到它的过程却出乎我们的意料。我们用了从影响力概念了解到的所有东西一股脑塞进了一个叫做**生产量**的轴。经过了所有的探索,我们终于得到了能够度量软件开发中代码影响力的方法,可以替换掉过度简化的代码行数了。我们具体计算影响力的方法参见[这篇文章](http://help.gitprime.com/537-calculations/1606-what-is-impact)。 -On the other axis, we plotted the *churn* rate — how much of an engineer’s focus is spent re-working recent code. +另一个轴是**改动率**,用来衡量工程师花了多少精力重写最近的代码。 -Plotting the result into a scatterplot can reveal quite a bit about an engineer’s work pattern over a specific timeframe in the context of the rest of the team. +把结果画成散点图可以充分说明一个工程师在特定时间段内(相对于团队其他成员)工作模式。 ![a successful framework for visualizing how software engineers work](https://blog.gitprime.com/hubfs/GitPrime/Blog/eng-character-3rev2.png?t=1481225729545) -The resulting visual offers helpful clues about a team: +我们可以从这张可视化图看出一些启示: -- In the **bottom left** quadrant, we retained the ‘stuck detector’ that we wanted from our original design. -- The **top left** highlights engineers who are exploring an implementation — we usually see engineers who are rapidly prototyping a new feature in this quadrant. -- The **bottom right** houses the “perfectionists” who have the lowest code churn on the team, but move a bit slower overall. -- Finally, in the **top right** are the engineers who are checking in lots of work, with little need for re-works — it’s generally a good idea to avoid interrupting these people, because they’re on fire! +- 左下角还是最初设计中的「难题发现者」。 +- 左上角表示那些探索新实现方式的工程师,我们经常可以在这一区域看到提出新特性的原型的工程师。 +- 右下角则是那些「完美主义者」,他们的代码搅拌率在团队中较低,但是总体进展速度也较慢。 +- 最后**右上角**代表提交频繁,不太需要返工的工程师,最好别打扰他们,他们正干得起劲呢! --- **Notes:** -1. Thanks to [Bobby Wallace](https://twitter.com/bikeath1337) for help renaming the old “Exploring” quadrant, now called “Discovery”. [↩](#fnref:1) ) - +1. 感谢 [Bobby Wallace](https://twitter.com/bikeath1337) 帮忙将可视化图中的「探索(Exploring)」象限改成了「发现(Discovery)」。 From ecc4ac683d49df3cded60f3098d47f35a7052b78 Mon Sep 17 00:00:00 2001 From: Zhiw Date: Thu, 22 Dec 2016 15:13:14 +0800 Subject: [PATCH 14/38] =?UTF-8?q?=E7=BD=91=E7=BB=9C=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E6=A1=86=E6=9E=B6=20Retrofit=202=20=E4=BD=BF=E7=94=A8=E8=AF=A6?= =?UTF-8?q?=E8=A7=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODO/getting-started-with-retrofit.md | 403 ++++++++++++++++++-------- 1 file changed, 276 insertions(+), 127 deletions(-) diff --git a/TODO/getting-started-with-retrofit.md b/TODO/getting-started-with-retrofit.md index a567211c96b..dd5c116d8e2 100644 --- a/TODO/getting-started-with-retrofit.md +++ b/TODO/getting-started-with-retrofit.md @@ -1,23 +1,34 @@ > * 原文地址:[Get Started With Retrofit 2 HTTP Client](https://code.tutsplus.com/tutorials/getting-started-with-retrofit-2--cms-27792) * 原文作者:[Chike Mgbemena](https://tutsplus.com/authors/chike-mgbemena) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) -* 译者: +* 译者:[Zhiw](https://github.com/Zhiw) * 校对者: # Get Started With Retrofit 2 HTTP Client +# 网络请求框架 Retrofit 2 使用详解 ![Final product image](https://cms-assets.tutsplus.com/uploads/users/1499/posts/27792/final_image/gt5.JPG) What You'll Be Creating +你将要创造什么 + ## What Is Retrofit? +## Retrofit 是什么? + +[Retrofit](https://square.github.io/retrofit/) is a type-safe HTTP client for Android and Java. Retrofit makes it easy to connect to a REST web service by translating the API into Java interfaces. In this tutorial, I'll show you how to use one of most popular and often-recommended HTTP libraries available for Android. + +[Retrofit](https://square.github.io/retrofit/) 是一个用于 Android 和 Java 平台的类型安全的网络请求框架。Retrofit 通过将 API 抽象成 Java 接口而让我们连接到 REST web 服务变得很轻松。在这个教程里,我会向你介绍如何使用这个 Android 上最受欢迎和经常推荐的网络请求库之一。 -[Retrofit](https://square.github.io/retrofit/) is a type-safe HTTP client for Android and Java. Retrofit makes it easy to connect to a REST web service by translating the API into Java interfaces. In this tutorial, I'll show you how to use one of most popular and often-recommended HTTP libraries available for Android. -This powerful library makes it easy to consume JSON or XML data which is then parsed into Plain Old Java Objects (POJOs). `GET`, `POST`, `PUT`, `PATCH`, and `DELETE` requests can all be executed. +This powerful library makes it easy to consume JSON or XML data which is then parsed into Plain Old Java Objects (POJOs). `GET`, `POST`, `PUT`, `PATCH`, and `DELETE` requests can all be executed. -Like most open-source software, Retrofit was built on top of some other powerful libraries and tools. Behind the scenes, Retrofit makes use of [OkHttp](http://square.github.io/okhttp/) (from the same developer) to handle network requests. Also, Retrofit does not have a built-in any JSON converter to parse from JSON to Java objects. Instead it ships support for the following JSON converter libraries to handle that: +这个强大的库可以很简单的把返回的 JSON 或者 XML 数据解析成简单 Java 对象(POJOs)。`GET`, `POST`, `PUT`, `PATCH`, 和 `DELETE` 这些请求都可以执行。 + +Like most open-source software, Retrofit was built on top of some other powerful libraries and tools. Behind the scenes, Retrofit makes use of [OkHttp](http://square.github.io/okhttp/) (from the same developer) to handle network requests. Also, Retrofit does not have a built-in any JSON converter to parse from JSON to Java objects. Instead it ships support for the following JSON converter libraries to handle that: + +和大多数开源软件一样,Retrofit 也是建立在一些强大的库和工具基础上的。Retrofit 背后用了同一个开发者的 [OkHttp](http://square.github.io/okhttp/) 来处理网络请求。而且 Retrofit 不再内置 JSON converter 来将 JSON 装换为 Java 对象。取而代之的是提供以下 JSON converter 来处理: - Gson: `com.squareup.retrofit:converter-gson` - Jackson: `com.squareup.retrofit:converter-jackson` @@ -25,51 +36,75 @@ Like most open-source software, Retrofit was built on top of some other powerful For [Protocol Buffers](https://developers.google.com/protocol-buffers/), Retrofit supports: +对于 [Protocol Buffers](https://developers.google.com/protocol-buffers/), Retrofit 提供了: + - Protobuf: `com.squareup.retrofit2:converter-protobuf` - Wire: `com.squareup.retrofit2:converter-wire` And for XML, Retrofit supports: +对于 XML 解析, Retrofit 提供了: + - Simple Framework: `com.squareup.retrofit2:converter-simpleframework` ## So Why Use Retrofit? +## 那么我们为什么要用 Retrofit 呢? + Developing your own type-safe HTTP library to interface with a REST API can be a real pain: you have to handle many functionalities such as making connections, caching, retrying failed requests, threading, response parsing, error handling, and more. Retrofit, on the other hand, is very well planned, documented, and tested—a battle-tested library that will save you a lot of precious time and headaches. +开发一个自己的用于请求 REST API 的类型安全的网络请求库是一件很痛苦的事情:你需要处理很多功能,比如建立连接,处理缓存,重连接失败请求,线程,响应数据的解析,错误处理等等。从另一方面来说,Retrofit 是一个有优秀的计划,文档和测试并且经过考验的库,它会帮你节省你的宝贵时间以及不让你那么头痛。 + In this tutorial, I will explain how to use Retrofit 2 to handle network requests by building a simple app to query recent answers from the [Stack Exchange](https://api.stackexchange.com/docs) API. We'll perform `GET` requests by specifying an endpoint—`/answers`, appended to the base URL [https://api.stackexchange.com/2.2](https://api.stackexchange.com/2.2)/—then get the results and display them in a recycler view. I will also show you how to do this with RxJava for easy management of the flow of state and data. +在这个教程里,我会构建一个简单的应用,根据 [Stack Exchange](https://api.stackexchange.com/docs) API 查询上面最近的回答,从而来教你如何使用 Retrofit 2 来处理网络请求。我们会指明 `/answers` 这样一个路径,然后拼接到 base URL [https://api.stackexchange.com/2.2](https://api.stackexchange.com/2.2)/ 上执行一个 `GET` 请求——然后我们会得到响应结果并且显示到 recycler view 上。我还会向你展示如何利用 RxJava 来轻松地管理状态和数据流。 + ## 1. Create an Android Studio Project +## 1.创建一个 Android Studio 工程 + Fire up Android Studio and create a new project with an empty activity called `MainActivity`. + +打开 Android Studio,创建一个新工程,然后创建一个命名为 `MainActivity` 的空白 Activity。 ![Create a new empty activity](https://cms-assets.tutsplus.com/uploads/users/1499/posts/27792/image/a2.png) ## 2. Declaring Dependencies -After creating a new project, declare the following dependencies in your `build.gradle`. The dependencies include a recycler view, the Retrofit library, and also Google's Gson library to convert JSON to POJO (Plain Old Java Objects) as well as Retrofit's Gson integration. +## 2. 添加依赖 + +After creating a new project, declare the following dependencies in your `build.gradle`. The dependencies include a recycler view, the Retrofit library, and also Google's Gson library to convert JSON to POJO (Plain Old Java Objects) as well as Retrofit's Gson integration. + +创建一个新的工程后,在你的 `build.gradle` 文件里面添加以下依赖。这些依赖包括 recycler view,Retrofit 库,还有 Google 出品的将 JSON 装换为 POJO(简单 Java 对象)的 Gson 库,以及 Retrofit 的 Gson。 // Retrofit compile 'com.squareup.retrofit2:retrofit:2.1.0' - + // JSON Parsing compile 'com.google.code.gson:gson:2.6.1' compile 'com.squareup.retrofit2:converter-gson:2.1.0' - - // recyclerview + + // recyclerview compile 'com.android.support:recyclerview-v7:25.0.1' - -Don't forget to sync the project to download these libraries. + +Don't forget to sync the project to download these libraries. + +不要忘记同步工程来下载这些库。 ## 3. Adding Internet Permission +## 3. 添加网络权限 + To perform network operations, we need to include the `INTERNET` permission in the application manifest: **AndroidManifest.xml**. +要执行网络操作,我们需要在应用的清单文件 **AndroidManifest.xml** 里面声明网络权限。 + - + - + - + - + ## 4. Generating Models Automatically -We are going to create our models automatically from our JSON response data by leveraging a very useful tool: [jsonschema2pojo](http://www.jsonschema2pojo.org/). +## 4.自动生成 Java 对象 + +We are going to create our models automatically from our JSON response data by leveraging a very useful tool: [jsonschema2pojo](http://www.jsonschema2pojo.org/). + +我们利用一个非常有用的工具来帮我们将返回的 JSON 数据自动生成 Java 对象:[jsonschema2pojo](http://www.jsonschema2pojo.org/)。 ### Get the Sample JSON Data +### 取得示例的 JSON 数据 + + Copy and paste [https://api.stackexchange.com/2.2/answers? order=desc&sort=activity&site=stackoverflow](https://api.stackexchange.com/2.2/answers?order=desc&sort=activity&site=stackoverflow) in your browser's address bar (or you could use [Postman](https://chrome.google.com/webstore/detail/postman/fhbjgbiflinjbdggehcddcbncdddomop?hl=en) if you are familiar with that tool). Then press **Enter**—this will execute a GET request on the given endpoint. What you will see in response is an array of JSON objects. The screenshot below is the JSON response using Postman. +复制粘贴 [https://api.stackexchange.com/2.2/answers?order=desc&sort=activity&site=stackoverflow](https://api.stackexchange.com/2.2/answers?order=desc&sort=activity&site=stackoverflow) 到你的浏览器地址栏,或者如果你熟悉的话,你可以使用 [Postman](https://chrome.google.com/webstore/detail/postman/fhbjgbiflinjbdggehcddcbncdddomop?hl=en) 这个工具。然后点击 **Enter** —— 它将会根据那个地址执行一个 GET 请求,你会看到返回的是一个 JSON 对象数组,下面的截图是使用了 Postman 的 JSON 响应结果。 + ![API response to GET request](https://cms-assets.tutsplus.com/uploads/users/769/posts/27792/image/1.jpg) - -``` + +``` { "items": [ { @@ -141,42 +185,65 @@ order=desc&sort=activity&site=stackoverflow](https://api.stackexchange.com/2.2/a "quota_remaining": 241 } ``` - -Copy this JSON response either from your browser or Postman. + +Copy this JSON response either from your browser or Postman. + +从你的浏览器或者 Postman 复制 JSON 响应结果。 ### Map the JSON Data to Java +### 将 JSON 数据映射到 Java 对象 + Now visit [jsonschema2pojo](http://www.jsonschema2pojo.org/) and paste the JSON response into the input box. -Select a source type of **JSON**, annotation style of **Gson**, and uncheck **Allow additional properties**. +现在访问 [jsonschema2pojo](http://www.jsonschema2pojo.org/),然后粘贴 JSON 响应结果到输入框。 + +Select a source type of **JSON**, annotation style of **Gson**, and uncheck **Allow additional properties**. + +选择 Source Type 为 **JSON**,Annotation Style 为 **Gson**,然后取消勾选 **Allow additional properties**。 ![](https://cms-assets.tutsplus.com/uploads/users/1499/posts/27792/image/u99.jpg) -Then click the **Preview** button to generate the Java objects. +Then click the **Preview** button to generate the Java objects. + +然后点击 **Preview** 按钮来生成 Java 对象。 ![](https://cms-assets.tutsplus.com/uploads/users/769/posts/27792/image/kpo09.jpg) You might be wondering what the `@SerializedName` and `@Expose` annotations do in this generated code. Don't worry, I'll explain it all! +你可能想知道在生成的代码里面, `@SerializedName` 和 `@Expose` 是干什么的。别着急,我会一一解释的。 + The `@SerializedName` annotation is needed for Gson to map the JSON keys with our fields. In keeping with Java's camelCase naming convention for class member properties, it is not recommended to use underscores to separate words in a variable. `@SerializedName` helps translate between the two. +Gson 使用 `@SerializedName` 注解来将 JSON 的 key 映射到我们类的变量。为了与 Java 对类成员属性的驼峰命名方法保持一致,不建议在变量中使用下划线将单词分开。`@SerializeName` 就是作为两者的翻译官。 + @SerializedName("quota_remaining") @Expose private Integer quotaRemaining; In the example above, we are telling Gson that our JSON key `quota_remaining` should be mapped to the Java field `quotaRemaining`. If both of these values were the same, i.e. if our JSON key was `quotaRemaining` just like the Java field, then there would be no need for the `@SerializedName` annotation on the field because Gson would map them automatically. -The `@Expose` annotation indicates that this member should be exposed for JSON serialization or deserialization. +在上面的示例中,我们告诉 Gson 我们的 JSON 的 key `quota_remaining` 应该被映射到 Java 变量 `quotaRemaining`上。如果两个值是一样的,即如果我们的 JSON 的 key 和 Java 变量一样是 `quotaRemaining`,那么就没有必要为变量设置 `@SerializedName` 注解,Gson 会自己搞定。 + +The `@Expose` annotation indicates that this member should be exposed for JSON serialization or deserialization. + +`@Expose` 注解表明在 JSON 序列化或反序列化的时候,该成员是否暴露给 Gson。 ### Import Data Models to Android Studio -Now let's go back to Android Studio. Create a new sub-package inside the main package and name it **data**. Inside the newly created data package, create another package and name it **model**. Inside the model package, create a new Java class and name it `Owner`. Now copy the `Owner` class that was generated by jsonschema2pojo and paste it inside the `Owner` class you created. +### 将数据模型导入 Android Studio + +Now let's go back to Android Studio. Create a new sub-package inside the main package and name it **data**. Inside the newly created data package, create another package and name it **model**. Inside the model package, create a new Java class and name it `Owner`. Now copy the `Owner` class that was generated by jsonschema2pojo and paste it inside the `Owner` class you created. + +现在让我们回到 Android Studio。新建一个 **data** 的子包,在 data 里面再新建一个 **model** 的包。在 model 包里面,新建一个 Owner 的 Java 类。 +然后将 jsonschema2pojo 生成的 `Owner` 类复制粘贴到刚才新建的 `Owner` 类文件里面。 import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; - + public class Owner { - + @SerializedName("reputation") @Expose private Integer reputation; @@ -198,72 +265,74 @@ Now let's go back to Android Studio. Create a new sub-package inside the main pa @SerializedName("accept_rate") @Expose private Integer acceptRate; - - + + public Integer getReputation() { return reputation; } - + public void setReputation(Integer reputation) { this.reputation = reputation; } - + public Integer getUserId() { return userId; } - + public void setUserId(Integer userId) { this.userId = userId; } - + public String getUserType() { return userType; } - + public void setUserType(String userType) { this.userType = userType; } - + public String getProfileImage() { return profileImage; } - + public void setProfileImage(String profileImage) { this.profileImage = profileImage; } - + public String getDisplayName() { return displayName; } - + public void setDisplayName(String displayName) { this.displayName = displayName; } - + public String getLink() { return link; } - + public void setLink(String link) { this.link = link; } - + public Integer getAcceptRate() { return acceptRate; } - + public void setAcceptRate(Integer acceptRate) { this.acceptRate = acceptRate; } } -Do the same thing for a new `Item` class, copied from jsonschema2pojo. +Do the same thing for a new `Item` class, copied from jsonschema2pojo. + +利用同样的方法从 jsonschema2pojo 复制过来,新建一个 `Item` 类。 import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; - + public class Item { - + @SerializedName("owner") @Expose private Owner owner; @@ -288,81 +357,83 @@ Do the same thing for a new `Item` class, copied from jsonschema2pojo. @SerializedName("last_edit_date") @Expose private Integer lastEditDate; - + public Owner getOwner() { return owner; } - + public void setOwner(Owner owner) { this.owner = owner; } - + public Boolean getIsAccepted() { return isAccepted; } - + public void setIsAccepted(Boolean isAccepted) { this.isAccepted = isAccepted; } - + public Integer getScore() { return score; } - + public void setScore(Integer score) { this.score = score; } - + public Integer getLastActivityDate() { return lastActivityDate; } - + public void setLastActivityDate(Integer lastActivityDate) { this.lastActivityDate = lastActivityDate; } - + public Integer getCreationDate() { return creationDate; } - + public void setCreationDate(Integer creationDate) { this.creationDate = creationDate; } - + public Integer getAnswerId() { return answerId; } - + public void setAnswerId(Integer answerId) { this.answerId = answerId; } - + public Integer getQuestionId() { return questionId; } - + public void setQuestionId(Integer questionId) { this.questionId = questionId; } - + public Integer getLastEditDate() { return lastEditDate; } - + public void setLastEditDate(Integer lastEditDate) { this.lastEditDate = lastEditDate; } } -Finally, create a class named `SOAnswersResponse` for the returned StackOverflow answers. You'll find the code for this class in jsonschema2pojo as `Example`. Make sure you update the class name to `SOAnswersResponse` wherever it occurs. +Finally, create a class named `SOAnswersResponse` for the returned StackOverflow answers. You'll find the code for this class in jsonschema2pojo as `Example`. Make sure you update the class name to `SOAnswersResponse` wherever it occurs. + +最后,为返回的 StackOverflow 回答新建一个 `SOAnswersResponse` 类。注意在 jsonschema2pojo 里面类名是 `Example`,别忘记把类名改成 `SOAnswersResponse`。 import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; - + import java.util.List; - + public class SOAnswersResponse { - + @SerializedName("items") @Expose private List items = null; @@ -378,43 +449,43 @@ Finally, create a class named `SOAnswersResponse` for the returned StackOverflow @SerializedName("quota_remaining") @Expose private Integer quotaRemaining; - + public List getItems() { return items; } - + public void setItems(List items) { this.items = items; } - + public Boolean getHasMore() { return hasMore; } - + public void setHasMore(Boolean hasMore) { this.hasMore = hasMore; } - + public Integer getBackoff() { return backoff; } - + public void setBackoff(Integer backoff) { this.backoff = backoff; } - + public Integer getQuotaMax() { return quotaMax; } - + public void setQuotaMax(Integer quotaMax) { this.quotaMax = quotaMax; } - + public Integer getQuotaRemaining() { return quotaRemaining; } - + public void setQuotaRemaining(Integer quotaRemaining) { this.quotaRemaining = quotaRemaining; } @@ -422,17 +493,23 @@ Finally, create a class named `SOAnswersResponse` for the returned StackOverflow ## 5. Creating the Retrofit Instance -To issue network requests to a REST API with Retrofit, we need to create an instance using the [`Retrofit.Builder`](http://square.github.io/retrofit/2.x/retrofit/retrofit2/Retrofit.Builder.html) class and configure it with a base URL. +## 5. 创建 Retrofit 实例 -Create a new sub-package package inside the `data` package and name it `remote`. Now inside `remote`, create a Java class and name it `RetrofitClient`. This class will create a singleton of Retrofit. Retrofit needs a base URL to build its instance, so we will pass a URL when calling `RetrofitClient.getClient(String baseUrl)`. This URL will then be used to build the instance in line 13. We are also specifying the JSON converter we need (Gson) in line 14. +To issue network requests to a REST API with Retrofit, we need to create an instance using the [`Retrofit.Builder`](http://square.github.io/retrofit/2.x/retrofit/retrofit2/Retrofit.Builder.html) class and configure it with a base URL. + +为了使用 Retrofit 向 REST API 发送一个网络请求,我们需要用 [`Retrofit.Builder`](http://square.github.io/retrofit/2.x/retrofit/retrofit2/Retrofit.Builder.html) 类来创建一个实例,并且配置一个 base URL。 + +Create a new sub-package package inside the `data` package and name it `remote`. Now inside `remote`, create a Java class and name it `RetrofitClient`. This class will create a singleton of Retrofit. Retrofit needs a base URL to build its instance, so we will pass a URL when calling `RetrofitClient.getClient(String baseUrl)`. This URL will then be used to build the instance in line 13. We are also specifying the JSON converter we need (Gson) in line 14. + +在 `data` 包里面新建一个 `remote` 的包,然后在 `remote` 包里面新建一个 `RetrofitClient` 类。这个类会创建一个 Retrofit 的单例。Retrofit 需要一个 base URL 来创建实例。所以我们在调用 `RetrofitClient.getClient(String baseUrl)` 时会传入一个 URL 参数。参见 13 行,这个 URL 用于构建 Retrofit 的实例。参见14行,我们也需要指明一个我们需要的 JSON converter(Gson)。 import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; - + public class RetrofitClient { - + private static Retrofit retrofit = null; - + public static Retrofit getClient(String baseUrl) { if (retrofit==null) { retrofit = new Retrofit.Builder() @@ -443,37 +520,48 @@ Create a new sub-package package inside the `data` package and name it `remote`. return retrofit; } } - + ## 6. Creating the API Interface -Inside the remote package, create an interface and call it `SOService`. This interface contains methods we are going to use to execute HTTP requests such as `GET`, `POST`, `PUT`, `PATCH`, and `DELETE`. For this tutorial, we are going to execute a `GET` request. +## 6.创建 API 接口 + +Inside the remote package, create an interface and call it `SOService`. This interface contains methods we are going to use to execute HTTP requests such as `GET`, `POST`, `PUT`, `PATCH`, and `DELETE`. For this tutorial, we are going to execute a `GET` request. + +在 remote 包里面,创建一个 `SOService` 接口,这个接口包含了我们将会用到用于执行网络请求的方法,比如 `GET`, `POST`, `PUT`, `PATCH`, 以及 `DELETE`。在该教程里面,我们将执行一个 `GET` 请求。 import com.chikeandroid.retrofittutorial.data.model.SOAnswersResponse; - + import java.util.List; - + import retrofit2.Call; import retrofit2.http.GET; - + public interface SOService { - + @GET("/answers?order=desc&sort=activity&site=stackoverflow") - Call> getAnswers(); - + Call> getAnswers(); + @GET("/answers?order=desc&sort=activity&site=stackoverflow") Call> getAnswers(@Query("tagged") String tags); } - + The `@GET` annotation explicitly defines that `GET` request which will be executed once the method gets called. Every method in this interface must have an HTTP annotation that provides the request method and relative URL. There are five built-in annotations available: `@GET`, `@POST`, `@PUT`, `@DELETE`, and `@HEAD`. +`GET` 注解明确的定义了当该方法调用的时候会执行一个 `GET` 请求。接口里每一个方法都必须有一个 HTTP 注解,用于提供请求方法和相对的 `URL`。Retrofit 内置了5种注解:`@GET`, `@POST`, `@PUT`, `@DELETE`, 和 `@HEAD`。 + In the second method definition, we added a query parameter for us to filter the data from the server. Retrofit has the `@Query("key")` annotation to use instead of hard-coding it in the endpoint. The key value represents the parameter name in the URL. It will be added to the URL by Retrofit. For example, if we pass the value `"android"` as an argument to the `getAnswers(String tags)` method, the full URL will be: +在第二个方法定义中,我们添加一个 query 参数用于从服务端过滤数据。Retrofit 提供了 `@Query("key")` 注解,这样就不用在地址里面直接写了。key 的值代表了 URL 里参数的名字。Retrofit 会把他们添加到 URL 里面。比如说,如果我们把 `android` 作为参数传递给 `getAnswers(String tags)` 方法,完整的 URL 将会是: + + https://api.stackexchange.com/2.2/answers?order=desc&sort=activity&site=stackoverflow&tagged=android Parameters of the interface methods can have the following annotations: +接口方法的参数支持以下注解: + |||| |---|---|---| |@Path|variable substitution for the API endpoint| @@ -481,92 +569,107 @@ Parameters of the interface methods can have the following annotations: |@Body|payload for the POST call| |@Header|specifies the header with the value of the annotated parameter| +|||| +|---|---|---| +|@Path|替换 API 地址中的变量| +|@Query|通过注解的名字指明 query 参数的名字| +|@Body|POST 请求的请求体| +|@Header|通过注解的参数值指明 header| + ## 7. Creating the API Utils +## 7.创建 API 工具类 + Now are going to create a utility class. We'll name it `ApiUtils`. This class will have the base URL as a static variable and also provide the `SOService` interface to our application through the `getSOService()` static method. +现在我们要新建一个工具类。我们命名为 `ApiUtils`。该类设置了一个 base URL 常量,并且通过静态方法 `getSOService()` 为应用提供 `SOService` 接口。 + public class ApiUtils { - + public static final String BASE_URL = "https://api.stackexchange.com/2.2/"; - + public static SOService getSOService() { return RetrofitClient.getClient(BASE_URL).create(SOService.class); } } - + ## 8. Display to a RecyclerView +## 8.显示到 RecyclerView + Since the results will be displayed in a [recycler view](https://code.tutsplus.com/tutorials/getting-started-with-recyclerview-and-cardview-on-android--cms-23465), we need an adapter. The following code snippet shows the `AnswersAdapter` class. +我们需要一个 adapter 来将结果将显示到 [recycler view](https://code.tutsplus.com/tutorials/getting-started-with-recyclerview-and-cardview-on-android--cms-23465) 上面。以下是 `AnswersAdapter` 类的代码片段。 + public class AnswersAdapter extends RecyclerView.Adapter { - + private List mItems; private Context mContext; private PostItemListener mItemListener; - + public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener{ - + public TextView titleTv; PostItemListener mItemListener; - + public ViewHolder(View itemView, PostItemListener postItemListener) { super(itemView); titleTv = (TextView) itemView.findViewById(android.R.id.text1); - + this.mItemListener = postItemListener; itemView.setOnClickListener(this); } - + @Override public void onClick(View view) { Item item = getItem(getAdapterPosition()); this.mItemListener.onPostClick(item.getAnswerId()); - + notifyDataSetChanged(); } } - + public AnswersAdapter(Context context, List posts, PostItemListener itemListener) { mItems = posts; mContext = context; mItemListener = itemListener; } - + @Override public AnswersAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - + Context context = parent.getContext(); LayoutInflater inflater = LayoutInflater.from(context); - + View postView = inflater.inflate(android.R.layout.simple_list_item_1, parent, false); - + ViewHolder viewHolder = new ViewHolder(postView, this.mItemListener); return viewHolder; } - + @Override public void onBindViewHolder(AnswersAdapter.ViewHolder holder, int position) { - + Item item = mItems.get(position); TextView textView = holder.titleTv; textView.setText(item.getOwner().getDisplayName()); } - + @Override public int getItemCount() { return mItems.size(); } - + public void updateAnswers(List items) { mItems = items; notifyDataSetChanged(); } - + private Item getItem(int adapterPosition) { return mItems.get(adapterPosition); } - + public interface PostItemListener { void onPostClick(long id); } @@ -574,12 +677,16 @@ Since the results will be displayed in a [recycler view](https://code.tutsplus.c ## 9. Executing the Request -Inside the `onCreate()` method of the `MainActivity`, we initialize an instance of the `SOService` interface (line 9), the recycler view, and also the adapter. Finally, we call the `loadAnswers()` method. +## 9.执行请求 + +Inside the `onCreate()` method of the `MainActivity`, we initialize an instance of the `SOService` interface (line 9), the recycler view, and also the adapter. Finally, we call the `loadAnswers()` method. + +在 `MainActivity` 的 `onCreate()` 方法内部,我们初始化 `SOService` 的实例(参见第 9 行),recycler view 以及 adapter。最后我们调用 `loadAnswers()` 方法。 private AnswersAdapter mAdapter; private RecyclerView mRecyclerView; private SOService mService; - + @Override protected void onCreate (Bundle savedInstanceState) { super.onCreate( savedInstanceState ); @@ -587,30 +694,32 @@ Inside the `onCreate()` method of the `MainActivity`, we initialize an instance mService = ApiUtils.getSOService(); mRecyclerView = (RecyclerView) findViewById(R.id.rv_answers); mAdapter = new AnswersAdapter(this, new ArrayList(0), new AnswersAdapter.PostItemListener() { - + @Override public void onPostClick(long id) { Toast.makeText(MainActivity.this, "Post id is" + id, Toast.LENGTH_SHORT).show(); } }); - + RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(this); mRecyclerView.setLayoutManager(layoutManager); mRecyclerView.setAdapter(mAdapter); mRecyclerView.setHasFixedSize(true); RecyclerView.ItemDecoration itemDecoration = new DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST); mRecyclerView.addItemDecoration(itemDecoration); - + loadAnswers(); } The `loadAnswers()` method makes a network request by calling `enqueue()`. When the response comes back, Retrofit helps us to parse the JSON response to a list of Java objects. (This is made possible by using `GsonConverter`.) +`loadAnswers()` 方法通过调用 `enqueue()` 方法来进行网络请求。当响应结果返回的时候,Retrofit 会帮我们把 JSON 数据解析成一个包含 Java 对象的 list(这是通过 `GsonConverter` 实现的)。 + public void loadAnswers() { mService.getAnswers().enqueue(new Callback() { @Override public void onResponse(Call call, Response response) { - + if(response.isSuccessful()) { mAdapter.updateAnswers(response.body().getItems()); Log.d("MainActivity", "posts loaded from API"); @@ -619,54 +728,83 @@ The `loadAnswers()` method makes a network request by calling `enqueue()`. When // handle request errors depending on status code } } - + @Override public void onFailure(Call call, Throwable t) { showErrorMessage(); Log.d("MainActivity", "error loading from API"); - + } }); } ## 10. Understanding `enqueue()` +## 10. 理解 `enqueue()` + `enqueue()` asynchronously sends the request and notifies your app with a callback when a response comes back. Since this request is asynchronous, Retrofit handles it on a background thread so that the main UI thread isn't blocked or interfered with. +`enqueue()` 会发送一个异步请求,当响应结果返回的时候通过回调通知应用。因为是异步请求,所以 Retrofit 将在后台线程处理,这样就不会让 UI 主线程堵塞或者受到影响。 + To use `enqueue()`, you have to implement two callback methods: +要使用 `enqueue()`,你必须实现这两个回调方法: + - `onResponse()` - `onFailure()` -Only one of these methods will be called in response to a given request. +Only one of these methods will be called in response to a given request. + +只有在请求有响应结果的时候才会调用其中一个方法。 - `onResponse()`: invoked for a received HTTP response. This method is called for a response that can be correctly handled even if the server returns an error message. So if you get a status code of 404 or 500, this method will still be called. To get the status code in order for you to handle situations based on them, you can use the method `response.code()`. You can also use the `isSuccessful()` method to find out if the status code is in the range 200-300, indicating success. -- `onFailure()`: invoked when a network exception occurred communicating to the server or when an unexpected exception occurred handling the request or processing the response. +- `onFailure()`: invoked when a network exception occurred communicating to the server or when an unexpected exception occurred handling the request or processing the response. + +- `onResponse()`:接收到 HTTP 响应时调用。该方法会在响应结果能够被正确地处理的时候调用,即使服务器返回了一个错误信息。所以如果你收到了一个 404 或者 500 的状态码,这个方法还是会调用。为了拿到状态码以便后续的处理,你可以使用 `response.code()` 方法。你也可以使用 `isSuccessful()` 来确定返回的状态码是否在 200-300 范围内,该范围的状态码也表示响应成功。 +- `onFailure()`:在与服务器通信的时候发生网络异常或者在处理请求或响应的时候发生异常的时候调用。 To perform a synchronous request, you can use the `execute()` method. Be aware that synchronous methods on the main/UI thread will block any user action. So don't execute synchronous methods on Android's main/UI thread! Instead, run them on a background thread. +要执行同步请求,你可以使用 `execute()` 方法。要注意同步请求在主线程会阻塞用户的任何操作。所以不要在主线程执行同步请求,要在后台线程执行。 + ## 11. Testing the App -You can now run the app. +## 11.测试应用 + +You can now run the app. + +现在你可以运行应用了。 ![Sample results from StackOverflow](https://cms-assets.tutsplus.com/uploads/users/1499/posts/27792/image/gt5.JPG) ## 12. RxJava Integration -If you are a fan of RxJava, you can easily implement Retrofit with RxJava. In Retrofit 1 it was integrated by default, but in Retrofit 2 you need to include some extra dependencies. Retrofit ships with a default adapter for executing `Call` instances. So you can change Retrofit's execution mechanism to include RxJava by including the RxJava `CallAdapter`. +## 12. 结合 RxJava + +If you are a fan of RxJava, you can easily implement Retrofit with RxJava. In Retrofit 1 it was integrated by default, but in Retrofit 2 you need to include some extra dependencies. Retrofit ships with a default adapter for executing `Call` instances. So you can change Retrofit's execution mechanism to include RxJava by including the RxJava `CallAdapter`. + +如果你是 RxJava 的粉丝,你可以通过 RxJava 很简单的实现 Retrofit。RxJava 在 Retrofit 1 中是默认整合的,但是在 Retrofit 2 中需要额外添加依赖。Retrofit 附带了一个默认的 adapter 用于执行 `Call()` 实例,所以你可以通过 RxJava 的 `CallAdapter` 来改变 Retrofit 的执行流程。 ### **Step 1** +### **第一步** + Add the dependencies. +添加依赖。 + compile 'io.reactivex:rxjava:1.1.6' compile 'io.reactivex:rxandroid:1.2.1' compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0' ### **Step 2** +### **第二步** + Add the new CallAdapter `RxJavaCallAdapterFactory.create()` when building a Retrofit instance. +在创建新的 Retrofit 实例的时候添加一个新的 CallAdapter `RxJavaCallAdapterFactory.create()`。 + public static Retrofit getClient(String baseUrl) { if (retrofit==null) { retrofit = new Retrofit.Builder() @@ -680,7 +818,11 @@ Add the new CallAdapter `RxJavaCallAdapterFactory.create()` when building a Retr ### **Step 3** -When making the requests, our anonymous subscriber responds to the observable's stream which emits events, in our case `SOAnswersResponse`. The `onNext` method is then called when our subscriber receives any event emitted which is then passed to our adapter. +### **第三步** + +When making the requests, our anonymous subscriber responds to the observable's stream which emits events, in our case `SOAnswersResponse`. The `onNext` method is then called when our subscriber receives any event emitted which is then passed to our adapter. + +当我们执行请求时,我们的匿名 subscriber 会响应 observable 发射的事件流,在本例中,就是 `SOAnswersResponse`。当 subscriber 收到任何发射事件的时候,就会调用 `onNext()` 方法,然后传递到我们的 adapter。 @Override public void loadAnswers() { @@ -688,14 +830,14 @@ When making the requests, our anonymous subscriber responds to the observable's .subscribe(new Subscriber() { @Override public void onCompleted() { - + } - + @Override public void onError(Throwable e) { - + } - + @Override public void onNext(SOAnswersResponse soAnswersResponse) { mAdapter.updateAnswers(soAnswersResponse.getItems()); @@ -703,11 +845,18 @@ When making the requests, our anonymous subscriber responds to the observable's }); } -Check out [Getting Started With ReactiveX on Android](https://code.tutsplus.com/tutorials/getting-started-with-reactivex-on-android--cms-24387) by Ashraff Hathibelagal to learn more about RxJava and RxAndroid. +Check out [Getting Started With ReactiveX on Android](https://code.tutsplus.com/tutorials/getting-started-with-reactivex-on-android--cms-24387) by Ashraff Hathibelagal to learn more about RxJava and RxAndroid. + +查看 Ashraff Hathibelagal 的 [Getting Started With ReactiveX on Android](https://code.tutsplus.com/tutorials/getting-started-with-reactivex-on-android--cms-24387) 以了解更多关于 RxJava 和 RxAndroid 的内容。 ## Conclusion -In this tutorial, you learned about Retrofit: why you should use it and how. I also explained how to add RxJava integration with Retrofit. In my next post, I'll show you how to perform `POST`, `PUT`, and `DELETE`, how to send `Form-Urlencoded` data, and how to cancel requests. +## 总结 + +In this tutorial, you learned about Retrofit: why you should use it and how. I also explained how to add RxJava integration with Retrofit. In my next post, I'll show you how to perform `POST`, `PUT`, and `DELETE`, how to send `Form-Urlencoded` data, and how to cancel requests. + +在该教程里,你已经了解了使用 Retrofit 的原因以及方法。我也解释了如何将 RxJava 结合 Retrofit 使用。在我的下一篇文章中,我将为你展示如何执行 `POST`, `PUT`, 和 `DELETE` 请求,如何发送 `Form-Urlencoded` 数据,以及如何取消请求。 To learn more about Retrofit, do refer to the [official documentation](https://square.github.io/retrofit/2.x/retrofit/). And in the meantime, check out some of our other courses and tutorials on Android app development. +要了解更多关于 Retrofit 的内容,请参考 [official documentation](https://square.github.io/retrofit/2.x/retrofit/)。同时,请查看我们其他一些关于 Android 应用开发的课程和教程。 From 0c6aa31893dda556204abd5c37e2d556efb9e9af Mon Sep 17 00:00:00 2001 From: AidenLiudm Date: Fri, 23 Dec 2016 17:40:20 +0800 Subject: [PATCH 15/38] translate lint --- TODO/progressive-web-amps.md | 42 ++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/TODO/progressive-web-amps.md b/TODO/progressive-web-amps.md index 821f60cf9f9..3d559fadea5 100644 --- a/TODO/progressive-web-amps.md +++ b/TODO/progressive-web-amps.md @@ -2,36 +2,36 @@ * 原文作者:[ Paul Bakaus ]( https://www.smashingmagazine.com/author/paulbakaus/) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) * 译者:[L9m](https://github.com/L9m) -* 校对者: +* 校对者:[marcmoore](https://github.com/marcmoore),[qrthree](https://github.com/sqrthree) -# 渐进增强的 Web 体验(Progressive Web Apps) +# 渐进增强的 Web 体验(Progressive Web AMP) -如果你最近几个月一直关注着 Web 开发社区,可能你对[渐进增强的 Web 应用](https://www.smashingmagazine.com/2016/08/a-beginners-guide-to-progressive-web-apps/)(简称 PWAs)已有所了解。它是应用体验能与原生应用媲美的 Web 应用的统称:[不依赖网络连接](https://www.smashingmagazine.com/2016/02/making-a-service-worker/),[易安装](https://developers.google.com/web/fundamentals/engage-and-retain/app-install-banners/?hl=en),支持视网膜屏幕,支持无边距图像,支持登录和个性化,快速且流畅的应用体验,支持推送通知和一个好看的界面。 +如果你最近几个月一直关注着 Web 开发社区,可能你对[渐进增强的 Web 应用](https://www.smashingmagazine.com/2016/08/a-beginners-guide-to-progressive-web-apps/)(简称 PWA)已有所了解。它是应用体验能与原生应用媲美的 Web 应用的统称:[不依赖网络连接](https://www.smashingmagazine.com/2016/02/making-a-service-worker/),[易安装](https://developers.google.com/web/fundamentals/engage-and-retain/app-install-banners/?hl=en),支持视网膜屏幕,支持无边距图像,支持登录和个性化,快速且流畅的应用体验,支持推送通并且有一个好看的界面。 -![From Google’s Advanced Mobile Pages (AMP) to progressive web apps](https://www.smashingmagazine.com/wp-content/uploads/2016/12/progressive-web-amp-2.png) +![从谷歌的 Advanced Mobile Page(AMP)到渐进式 Web 应用](https://www.smashingmagazine.com/wp-content/uploads/2016/12/progressive-web-amp-2.png) 一些 Google 的渐进式 Web 应用示例。 -虽然新的 [Service Worker API](https://developers.google.com/web/fundamentals/primers/service-worker/) 允许离线缓存所有的网站资源以便在*后续*加载中瞬时加载。就像陌生人的第一印象很关键。最新的 [DoubleClick 研究](https://www.doubleclickbygoogle.com/articles/mobile-speed-matters/) (译者注:DoubleClick 是谷歌旗下一家公司)表明,如果首次加载超过 3 秒,超过 53% 的用户将放弃访问。 +虽然新的 [Service Worker API](https://developers.google.com/web/fundamentals/primers/service-worker/) 允许离线缓存所有的网站资源以便在**后续**加载中瞬时加载,就像陌生人的第一印象很关键一样。最新的 [DoubleClick 研究](https://www.doubleclickbygoogle.com/articles/mobile-speed-matters/) (译者注:DoubleClick 是谷歌旗下一家公司)表明,如果首次加载超过 3 秒,超过 53% 的用户将放弃访问。 -老实说,3 秒已是一个相当*严峻*的目标。移动端连接通常有平均 300ms 延迟,而且附带有带宽限制和时不时信号弱等不利情况,你可能只剩下不到 1 秒时间留给应用初始化等事情。 +老实说,3 秒已是一个相当**严峻**的目标。移动端连接通常有平均 300ms 延迟,而且附带有带宽限制和时不时信号弱等不利情况,你可能只剩下不到 1 秒时间留给应用初始化等事情。 ![From Google’s Advanced Mobile Pages (AMP) to progressive web apps](https://www.smashingmagazine.com/wp-content/uploads/2016/12/progressive-web-amp-7.png) 用户和内容之间的延迟。 -当然,有一些方法能[缓解](https://codelabs.developers.google.com/codelabs/your-first-pwapp/#4)首次加载缓慢的问题 — 在服务器上预先渲染好一个基础结构,再懒加载各个功能模块等等 — 但是使用此种策略达到的优化程度也有限,而且不得不雇佣一个前端优化专家。 +当然,有一些方法能[缓解](https://codelabs.developers.google.com/codelabs/your-first-pwapp/#4)首次加载缓慢的问题 — 在服务器上预先渲染好一个基础结构,再懒加载各个功能模块等等 — 但是使用此种策略达到的优化程度也有限,而且不得不雇佣一个前端优化专家,或者自己成长为一个专家。 -那么,我们有什么方法来做一个(根本)和原生应用不同的瞬时加载呢? +那么,我们有什么方法来做一个从根本上和原生应用不同的首次瞬时加载呢? -### AMP ,为移动页面加速 +### AMP,为移动页面加速 -网站的最重要的优势之一是跨平台——无需安装和即刻加载。用户用完即走。 +网站的最重要的优势之一是跨平台 — 无需安装和即刻加载,用户通常只需轻击一下鼠标即可。 要想轻松地从短浏览(ephemeral browsing)的机会中收益,所需的就是一个瞬时加载(crazy-fast-loading)的网站。让网站瞬时加载,你需要做些什么呢?你所需做的只是一个适当的节制:没有兆字节大小图片,阻塞渲染的广告,没有十万行 JavaScript,就只有这些要求。 -[AMPs](https://www.ampproject.org/),是加速移动网页(Accelerated Mobile Pages)的简称,它[擅长于此](https://www.ampproject.org/docs/get_started/technical_overview.html),实际上,这是它们*存在的原因*(raison d’être)。它就像一个驾驶辅助功能,通过实行一套合理的规则,优化你的网页主体内容,让它们处于快车道。并通过创建这种严格的,[静态的](https://www.ampproject.org/docs/get_started/technical_overview.html#size-all-resources-statically)布局环境,使譬如像谷歌搜索等平台[仅预渲染首屏](https://www.ampproject.org/docs/get_started/technical_overview.html#load-pages-in-an-instant),得以进一步接近”瞬时“。 +[AMPs](https://www.ampproject.org/),是加速移动网页(Accelerated Mobile Pages)的简称,它[擅长于此](https://www.ampproject.org/docs/get_started/technical_overview.html),实际上,这是它们**存在的原因**(raison d’être)。它就像一个驾驶辅助功能,通过实行一套合理的规则,优化你的网页主体内容,让它们处于快车道。并通过创建这种严格的,[静态的](https://www.ampproject.org/docs/get_started/technical_overview.html#size-all-resources-statically)布局环境,使譬如谷歌搜索等平台[仅预渲染首屏](https://www.ampproject.org/docs/get_started/technical_overview.html#load-pages-in-an-instant),得以进一步接近“瞬时”。 ![From Google’s Advanced Mobile Pages (AMP) to progressive web apps](https://www.smashingmagazine.com/wp-content/uploads/2016/12/progressive-web-amp-0.png) @@ -39,7 +39,7 @@ ### AMP 还是 PWA? -AMP 可靠快速的体验,在实现时也伴随着一些限制。当你需要高度动态的功能时,AMP 是不适用的,譬如推送通知,网络支付和依靠额外 JavaScript 的功能。此外,因为 AMP 页面通常从 AMP 缓存中提供,你的 Service Worker 不能运行,首次访问享受不到渐进式 Web 应用的最重要的好处。另一方面,在首次访问的速度上,渐进式 Web 应用永远不及 AWP,因为平台能顺利且毫不费力地预渲染 AMP 页面—内嵌更简单(比如,在内嵌浏览器中)。 +AMP 可靠快速的体验,在实现时也伴随着一些限制。当你需要高度动态的功能时,AMP 是不适用的,譬如推送通知、网络支付和依靠额外 JavaScript 的功能。此外,因为 AMP 页面通常从 AMP 缓存中提供,你的 Service Worker 不能运行,首次访问享受不到渐进式 Web 应用的最重要的好处。另一方面,在首次访问的速度上,渐进式 Web 应用永远不及 AWP,因为平台能顺利且毫不费力地预渲染 AMP 页面 — 内嵌更简单(比如在内嵌浏览器中)。 ![From Google’s Advanced Mobile Pages (AMP) to progressive web apps](https://www.smashingmagazine.com/wp-content/uploads/2016/12/progressive-web-amp-8.png) @@ -50,7 +50,7 @@ AMP 可靠快速的体验,在实现时也伴随着一些限制。当你需要 ### 完美的用户旅程(User Journey) 终究,重要的是针对**用户旅程**的理想体验。它大概是这样的: -1. 用户点击你的内容链接。 +1. 用户发现了一个指向你的内容的链接,并且点击了它。 2. 内容快速加载是一种愉快的体验。 3. 用户被通知并进阶到有推送通知和支持离线的更流畅的体验。 4. 立即重定向到一个类原生的体验,并且可将网站放在你的主屏幕上。用户惊呼:“怎么回事?好神奇!”。 @@ -74,7 +74,7 @@ AMP 可靠快速的体验,在实现时也伴随着一些限制。当你需要 让我们每个都过一遍吧。 #### AMP 作为 PWA -许多网站其实用不到超出 AMPs(功能)范围。[Amp by Example](https://ampbyexample.com/) 就是一个例子,它既是 AMP 也是一个渐进式 Web 应用。 +许多网站其实用不到超出 AMP(功能)范围。[Amp by Example](https://ampbyexample.com/) 就是一个例子,它既是 AMP 也是一个渐进式 Web 应用。 - 它有 service worker,因此允许包括离线访问等在内的其他功能。 - 它有清单(manifest),在网页横幅上会提醒“添加到主屏幕“。 @@ -106,7 +106,7 @@ function createCompleteResponse (header, body) { 当上述不能满足时,并且你想让内容有一个完全不同的 PWA 体验时,是时候用一种更高级一点的模式了: - 为了接近瞬时加载的体验,所有内容“叶”页(指有特定内容,不是概述的页面)被发布成 AMP。 -- 这些 AMPs 使用 AMP 的特殊元素 [``](https://www.ampproject.org/docs/reference/extended/amp-install-serviceworker.html) 来预备缓存,并且**当用户喜欢**你的内容时用 PWA 的外壳。 +- 这些 AMP 使用 AMP 的特殊元素 [``](https://www.ampproject.org/docs/reference/extended/amp-install-serviceworker.html) 来预备缓存,并且**当用户喜欢**你的内容时用 PWA 的外壳。 - 当用户点击你网站上的另一个链接(比如,在底部的行为召唤(按钮),使其更像原生)service worker 拦截请求并接管页面,然后加载 PWA 外壳替代之。 你可以通过以上三个简单步骤来实现这种体验,假如你熟悉 service worker 的运作(如果你不清楚的话,强烈推荐我的同事[杰克在优达学城(Udacity)上的课程](https://www.udacity.com/course/offline-web-applications--ud899))。第一步,在你所有的 AMP 上放置 service worker。 @@ -154,7 +154,7 @@ self.addEventListener('fetch', event => { ![From Google’s Advanced Mobile Pages (AMP) to progressive web apps](https://www.smashingmagazine.com/wp-content/uploads/2016/12/progressive-web-amp-6.png) -你可以通过在网站上安装一些 service worker 来实行渐进增强。对于不支持 service worker 的浏览器,它们仅会转移到 AMP 缓存页面。 +你可以通过在网站上安装一些 service worker 来实现渐进增强。对于不支持 service worker 的浏览器,它们仅会转移到 AMP 缓存页面。 此项技术很有意思之处在于从 AMP 渐进增强到 PWA。然而,这也意味着,暂时不支持 service worker 的浏览器将从 AMP 跳到 AMP 并且不会导航到 PWA。 @@ -172,24 +172,24 @@ AMP 通过 [Shell URL 重写](https://www.ampproject.org/docs/reference/componen 在有 service worker 的情况下具有了这些属性,AMP 上所有后续点击都将转到 PWA。挺巧妙的,是吧? #### AMP 在 PWA 中 -那么,现在用户处于渐进式 Web 应用中,你可能会使用一些 AJAX 驱动(AJAX-driven)的导航,通过 JSON 来获取内容。你当然可以这么做,但是现在有两个完全不同的内容后端和基础架构需求——一个生成 AMP 页面,另外一个为你的渐进式 Web 应用提供基于 JSON 格式的接口。 +那么,现在用户处于渐进式 Web 应用中,你可能会使用一些 AJAX 驱动(AJAX-driven)的导航,通过 JSON 来获取内容。你当然可以这么做,但是现在有两个完全不同的内容后端和基础架构需求 — 一个生成 AMP 页面,另外一个为你的渐进式 Web 应用提供基于 JSON 格式的接口。 但请想一想 AMP 的本质是什么。它不只是一个网站,它被设计成一个超轻便的内容单元。AMP 是独立的且可以顺利地嵌入到其他网站。我们是否可以抛弃 JSON 接口,使用 AMP 作为我们渐进式 Web 应用的数据格式,从而大大降低后端复杂性呢? ![From Google’s Advanced Mobile Pages (AMP) to progressive web apps](https://www.smashingmagazine.com/wp-content/uploads/2016/12/progressive-web-amp-3.png) -AMP 页面能顺利地嵌入其他网站中—PWA 的 AMP库只会编译并加载一次。 +AMP 页面能顺利地嵌入其他网站中 — PWA 的 AMP 库只会编译并加载一次。 当然,一个简单的方法是在 frames 中加载 AMP 页面。但是使用 iframes 比较慢,并且需要你一遍又一遍地重新编译和初始化 AMP 库。现在前沿的 Web技术提供了一种更好的方式:Shadow DOM。 处理过程看起来是这样的: 1. PWA 操纵所有的导航点击事件。 -2. 然后,用 XMLHttpRequest 获取请求的 AMP 页面。 +2. 然后,用 XMLHttpRequest 获取请求的 AMP 页面。 3. 将内容放入一个新的 shadow root 中。 4. 然后返回给主 AMP 库,”嘿,我有一个新文档给你。请查收!“(在运行时调用 `attachShadowDoc`)。 使用此种技术,整个 PWA 只会编译和加载一次 AMP 库,并且然后,因为你是通过 XMLHttpRequests 获取的页面,你能在 AMP 源插入新的 shadow document 之前进行一些修改,你可以像这样做: -- 去掉不必要的内容,比如页眉(headers)和页脚(footerds); +- 去掉不必要的内容,比如页眉(headers)和页脚(footers); - 插入额外的内容,比如令人反感的广告或信息提示; - 用更动态的内容替换特定内容。 @@ -204,7 +204,7 @@ AMP 团队做了一个[名为 The Scenic 的 React 示例](https://choumx.github #### 看点真东西 -一个真实产品的例子是 [Mic 新的 PWA](https://beta.mic.com)(beta 阶段),研究一下 :如果你按住 shift 重新刷新(shift-reload)[任意文章](https://beta.mic.com/articles/161568/arrow-season-5-episode-9-a-major-character-returns-in-midseason-finale-maybe)(这样暂时忽略 service worker)查看源代码, 你会注意到这是一个 AMP 页面。现在尝试点击一下菜单:它会重新加载当前页面, 但由于 `` *已存在*于 PWA 应用外壳中,重载几乎是*瞬间*完成的,并且菜单在刷新后打开,使其看起来不像是重新加载过一样。但现在你处于拥有其他丰富功能的(内嵌 AMP 页面)PWA 中。狡猾,但很了不起。 +一个真实产品的例子是 [Mic 新的 PWA](https://beta.mic.com)(beta 阶段),研究一下 :如果你按住 shift 重新刷新(shift-reload)[任意文章](https://beta.mic.com/articles/161568/arrow-season-5-episode-9-a-major-character-returns-in-midseason-finale-maybe)(这样暂时忽略 service worker)查看源代码, 你会注意到这是一个 AMP 页面。现在尝试点击一下菜单:它会重新加载当前页面, 但由于 `` **已存在**于 PWA 应用外壳中,重载几乎是*瞬间*完成的,并且菜单在刷新后打开,使其看起来不像是重新加载过一样。但现在你处于拥有其他丰富功能的(内嵌 AMP 页面)PWA 中。狡猾,但很了不起。 ### 结语 From e7da1dffd56e442b41ff0d29aa9642e74da42eba Mon Sep 17 00:00:00 2001 From: AidenLiudm Date: Fri, 23 Dec 2016 17:59:18 +0800 Subject: [PATCH 16/38] translate lint --- TODO/progressive-web-amps.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/TODO/progressive-web-amps.md b/TODO/progressive-web-amps.md index 3d559fadea5..ab4cfc5300b 100644 --- a/TODO/progressive-web-amps.md +++ b/TODO/progressive-web-amps.md @@ -6,7 +6,7 @@ # 渐进增强的 Web 体验(Progressive Web AMP) -如果你最近几个月一直关注着 Web 开发社区,可能你对[渐进增强的 Web 应用](https://www.smashingmagazine.com/2016/08/a-beginners-guide-to-progressive-web-apps/)(简称 PWA)已有所了解。它是应用体验能与原生应用媲美的 Web 应用的统称:[不依赖网络连接](https://www.smashingmagazine.com/2016/02/making-a-service-worker/),[易安装](https://developers.google.com/web/fundamentals/engage-and-retain/app-install-banners/?hl=en),支持视网膜屏幕,支持无边距图像,支持登录和个性化,快速且流畅的应用体验,支持推送通并且有一个好看的界面。 +如果你最近几个月一直关注着 Web 开发社区,可能你对[渐进增强的 Web 应用](https://www.smashingmagazine.com/2016/08/a-beginners-guide-to-progressive-web-apps/)(Progressive Web App 简称 PWA)已有所了解。它是应用体验能与原生应用媲美的 Web 应用的统称:[不依赖网络连接](https://www.smashingmagazine.com/2016/02/making-a-service-worker/),[易安装](https://developers.google.com/web/fundamentals/engage-and-retain/app-install-banners/?hl=en),支持视网膜屏幕,支持无边距图像,支持登录和个性化,快速且流畅的应用体验,支持推送通并且有一个好看的界面。 ![从谷歌的 Advanced Mobile Page(AMP)到渐进式 Web 应用](https://www.smashingmagazine.com/wp-content/uploads/2016/12/progressive-web-amp-2.png) @@ -76,7 +76,7 @@ AMP 可靠快速的体验,在实现时也伴随着一些限制。当你需要 #### AMP 作为 PWA 许多网站其实用不到超出 AMP(功能)范围。[Amp by Example](https://ampbyexample.com/) 就是一个例子,它既是 AMP 也是一个渐进式 Web 应用。 - 它有 service worker,因此允许包括离线访问等在内的其他功能。 -- 它有清单(manifest),在网页横幅上会提醒“添加到主屏幕“。 +- 它有清单(manifest),在横幅(banner)上会提醒“添加到主屏幕“。 当用户从谷歌搜索访问 [Amp by Example](https://ampbyexample.com/),然后点击网站上链接,它们将从 AMP 缓存页面转到源页面上。当然,网站仍在使用 AMP 库,但是现在由于它处于源页面上,它能使用 service worker 或提示安装等等。 @@ -104,12 +104,12 @@ function createCompleteResponse (header, body) { #### AMP 转作 PWA -当上述不能满足时,并且你想让内容有一个完全不同的 PWA 体验时,是时候用一种更高级一点的模式了: +当上述不能满足,并且你想让内容有一个完全不同的 PWA 体验时,是时候用一种更高级一点的模式了: - 为了接近瞬时加载的体验,所有内容“叶”页(指有特定内容,不是概述的页面)被发布成 AMP。 - 这些 AMP 使用 AMP 的特殊元素 [``](https://www.ampproject.org/docs/reference/extended/amp-install-serviceworker.html) 来预备缓存,并且**当用户喜欢**你的内容时用 PWA 的外壳。 - 当用户点击你网站上的另一个链接(比如,在底部的行为召唤(按钮),使其更像原生)service worker 拦截请求并接管页面,然后加载 PWA 外壳替代之。 -你可以通过以上三个简单步骤来实现这种体验,假如你熟悉 service worker 的运作(如果你不清楚的话,强烈推荐我的同事[杰克在优达学城(Udacity)上的课程](https://www.udacity.com/course/offline-web-applications--ud899))。第一步,在你所有的 AMP 上放置 service worker。 +假如你熟悉 service worker 的运作,你可以通过以上三个简单步骤来实现这种体验。(如果你不清楚的话,强烈推荐我的同事[杰克在优达学城(Udacity)上的课程](https://www.udacity.com/course/offline-web-applications--ud899))。第一步,在你所有的 AMP 上放置 service worker。 ``` ` **已存在**于 PWA 应用外壳中,重载几乎是*瞬间*完成的,并且菜单在刷新后打开,使其看起来不像是重新加载过一样。但现在你处于拥有其他丰富功能的(内嵌 AMP 页面)PWA 中。狡猾,但很了不起。 +一个真实产品的例子是 [Mic 新式 PWA](https://beta.mic.com)(beta 阶段),研究一下 :如果你按住 shift 重新刷新(shift-reload)[任意文章](https://beta.mic.com/articles/161568/arrow-season-5-episode-9-a-major-character-returns-in-midseason-finale-maybe)(这样暂时忽略 service worker)查看源代码, 你会注意到这是一个 AMP 页面。现在尝试点击一下菜单:它会重新加载当前页面, 但由于 `` **已存在**于 PWA 应用外壳中,重载几乎是**瞬间**完成的,并且菜单在刷新后打开,使其看起来不像是重新加载过一样。但现在你处于拥有其他丰富功能的(内嵌 AMP 页面)PWA 中。狡猾,但很了不起。 ### 结语 From 23bc6a5cfd2348070271cdd9f361af14f3d2877e Mon Sep 17 00:00:00 2001 From: ace Date: Sat, 24 Dec 2016 11:58:58 +0800 Subject: [PATCH 17/38] =?UTF-8?q?add:=20=E5=9C=A8=E7=AE=97=E6=B3=95?= =?UTF-8?q?=E6=A8=AA=E8=A1=8C=E7=9A=84=E6=97=B6=E4=BB=A3=EF=BC=8C=E4=BB=8D?= =?UTF-8?q?=E9=9C=80=E8=A6=81=E4=BA=BA=E7=B1=BB=E6=8A=8A=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODO/age-of-algorithm-human-gatekeeper.md | 36 ++++++++++++----------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/TODO/age-of-algorithm-human-gatekeeper.md b/TODO/age-of-algorithm-human-gatekeeper.md index 058f39a7f5c..3f303ab88ce 100644 --- a/TODO/age-of-algorithm-human-gatekeeper.md +++ b/TODO/age-of-algorithm-human-gatekeeper.md @@ -1,40 +1,42 @@ * 原文地址:[ In the age of the algorithm, the human gatekeeper is back ](https://www.theguardian.com/technology/2016/sep/30/age-of-algorithm-human-gatekeeper) * 原文作者:[Michael Bhaskar](https://www.theguardian.com/profile/michael-bhaskar) * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) -* 译者: +* 译者:[Jiang Haichao](https://github.com/AceLeeWinnie) * 校对者: -# In the age of the algorithm, the human gatekeeper is back +# 在算法横行的时代,仍需要人类把关 -Greg Linden may not be a household name, but he changed the way we interact with culture and transformed retail forever. [An engineer at Amazon in the late 1990s](https://www.theguardian.com/technology/2006/mar/09/newmedia.guardianweeklytechnologysection), Linden worked on a curious problem: how to recommend books without human intervention. Until then Amazon relied on editors who wrote hundreds of reviews every year. It was a costly and time-consuming process. +Greg Linden 或许不是一个家喻户晓的名字,但他改变了我们与文化的相互影响并且永久变革了零售业。作为[九十年代晚期 Amazon 的一名工程师](https://www.theguardian.com/technology/2006/mar/09/newmedia.guardianweeklytechnologysection),Linden 要解决一个奇怪的问题:如何在没有人工干预的情况下向顾客推荐书籍。那时 Amazon 还要依靠编辑们每年写上上百篇评论。这不仅费钱还费时。 -Automating recommendations proved trickier than anyone expected. Linden cracked it. He hit on “personalisation”, which paradoxically meant looking not at an individual’s purchasing history, but only at correlations among products. Regardless of what you had bought in the past, Amazon realised that if product A was often bought alongside product B, it meant almost anyone buying product A would also want product B. Amazon tested the results to see which method sold more books. No surprises: the editors were soon looking for new jobs. Humans out; machines in. Some estimates suggest a third of Amazon sales arise from these recommendations. Ever since, the rise of algorithms has been relentless. Now books, articles, music, films, not to mention holidays and clothes, are all suggested by machines. +自动化推荐在当时是难以想象的棘手。 Linden 成功破解。他把目光放在 "个性化" 上,矛盾的是,这关注的是产品之间的相关性而不是个人购买历史。忽略过去的购买记录后, Amazon 发现如果 A 产品一般和 B 产品一起售出,也就是说几乎所有人买 A 时也会想买 B。 Amazon 通过不同的售书方法的销量测试了这一发现。不出意外:编辑们要卷铺盖走人了。人类退出,机器当道。一些估算显示因为这些推荐算法, Amazon 的营业额上升了 1/3。从此,算法被大量使用。现在书籍、文章、音乐、电影,还有不消说的度假和服装,都是通过机器推荐的。 -Last year 1m new books were published in English. Since at least the ancient Greeks, people have believed there is too much to read; now they may be right. That, of course, doesn’t even count all the self-published works, the reams of news or the Borgesian vastness of the internet. By any measure, we have an astounding surplus of reading matter. +去年,英文书籍新出版了一百万册。至少自从古老的希腊人开始,人们一直赞成大量阅读;现在看来也许是正确的。当然,自出版的作品算入在内,大量的新闻与互联网快餐阅读也囊括其中。不管怎么说,我们都处在令人惊讶的阅读过剩之中。 -The more we have, the more we rely on algorithms and automated recommendation systems. Hence the unstoppable march of algorithmic recommendations, machine learning, artificial intelligence and big data into the cultural sphere. +我们拥有的越多,我们就越依赖算法和自动化推荐系统。因此,推荐算法、机器学习、人工智能和大数据不可抵挡地侵入了文化领域。 -Yet this isn’t the end of the story. Search, for example, tells us what we want to know, but can’t help if we don’t already know what we want. Far from disappearing, human curation and sensibilities have a new value in the age of algorithms. Yes, the more we have the more we need automation. But we also increasingly want informed and idiosyncratic selections. Humans are back. +然而故事并没有结束。例如搜索,能告诉我们想知道的,但是不能帮助我们知道我们还不知道我们想知道的。人类的挑选和识别力在算法时代有了新的意义, 远不能消失。是的,随着拥有的越多,我们越来越需要算法了。但是我们也更加希望见多识广和特殊选择。人类又回来了。 -This is why, despite having the world’s most powerful book recommendation engine, Amazon bought [Goodreads](https://www.theguardian.com/books/2013/apr/02/amazon-purchase-goodreads-stuns-book-industry), a website based around personal book reviews. It is why sites such as Canopy.co thrive atop Amazon. Canopy knows many of Amazon’s best items are hidden in the mediocre morass. Canopy’s founders, all designers, trawl through thousands of entries a day to highlight exceptional products. +这是为什么尽管有全世界最强大的图书推荐引擎,Amazon 还是买下了 [Goodreads](https://www.theguardian.com/books/2013/apr/02/amazon-purchase-goodreads-stuns-book-industry),一个主营个人书籍评论的网站。这也是为什么像 Canopy.co 这类的网站活跃程度在 Amazon 之上。Canopy 知道 Amazon 最棒的商品隐藏在一堆乱七八糟的东西里。Canopy 的创始人全都是设计师,每天筛选数千条记录以重点标注高质量商品。 -It’s why publishers keep producing new imprints, to allow for more diverse and personal lists, and why bookshops are once again flourishing, even though we can find any book we want online. We go to browse their tables. In Japan they talk about *tsundoku*, or the uneasy feeling of having too many books to read. They also have its solution: a [bookshop in Tokyo’s Ginza ](https://www.theguardian.com/books/2015/dec/23/japanese-bookshop-stocks-only-one-book-at-a-time)that sells only one book at a time. +这是为什么尽管在网上能找到任何想要的书,但出版商仍印刷新的书籍满足多样化与个人书单,书店也再次兴起的原因。我们能够漫不经心的查看桌子上的书籍。在日本,人们谈论 ***tsundoku***,即太多书可读的不安感受。他们自有解决办法:[东京银座书店](https://www.theguardian.com/books/2015/dec/23/japanese-bookshop-stocks-only-one-book-at-a-time)一次就只售卖一本书。 -This rejuvenated interest in curation isn’t just happening in publishing. On Spotify you can listen to 30m songs, 20% of which have never been streamed once. To help manage this huge catalogue, Spotify spent a reported $100m (£77m) acquiring a company called [the Echo Nest](https://www.theguardian.com/technology/2014/mar/06/spotify-echo-nest-streaming-music-deal), which pioneered a technique known as audio-fingerprinting, which automatically categorises songs. At the same time, however, [Spotify](https://www.theguardian.com/business/2016/may/24/spotify-revenues-surge-80-to-more-than-13bn) has massively expanded its range of playlist makers, musical experts who are rapidly becoming the new DJs. +这个在内容挑选上重焕生机的趣味不仅出现在出版业。在 Spotify(某在线音乐播放器)上,你可以听 30m 的音乐,20% 只播放一次。为了帮助管理庞大的音乐目录,Spotify 花了 1 个亿收购了 [the Echo Nest](https://www.theguardian.com/technology/2014/mar/06/spotify-echo-nest-streaming-music-deal) 公司,后者拥有一项先进技术,用于识别音乐,自动分类曲目。同时,[Spotify](https://www.theguardian.com/business/2016/may/24/spotify-revenues-surge-80-to-more-than-13bn) 扩充了自己的歌单推荐人和快速成为新 DJ 的音乐专家。 -![Spotify’s office](https://i.guim.co.uk/img/media/b1817d17c3857559c8c5bb3ebdd852627eefa181/0_192_5760_3456/master/5760.jpg?w=620&q=55&auto=format&usm=12&fit=max&s=aa4b8651de389bfc8cff727a2bb8c24d) +![Spotify 办公室](https://i.guim.co.uk/img/media/b1817d17c3857559c8c5bb3ebdd852627eefa181/0_192_5760_3456/master/5760.jpg?w=620&q=55&auto=format&usm=12&fit=max&s=aa4b8651de389bfc8cff727a2bb8c24d) -[Netflix](https://www.theguardian.com/tv-and-radio/tvandradioblog/2013/aug/15/netflix-subscribe-breaking-bad-justified) has more TV and film than we could ever want. An early pioneer of using data science to manage culture, it even launched a $1m competition for teams of researchers to improve its algorithms – and then, despite the prize money, didn’t implement any changes as they weren’t seen as good enough. Yet Netflix also trained viewers to tag its content exhaustively. They make judgments machines cannot: is the ending wistful? Are moustaches important in the film or not? +[Netflix](https://www.theguardian.com/tv-and-radio/tvandradioblog/2013/aug/15/netflix-subscribe-breaking-bad-justified) 有远超观众需要的影视剧集。它是一个用数据科学管理文化的先驱者,它甚至发起一个奖金 100 万美元的比赛为了研究团队们来升级它的算法,最后钱花出去了,却没有实现他们想要的效果。然而 Netflix 还训练观众为它内容全部打标签。他们做到了评论系统做不到的是:结局是想要的吗?胡子在电影里重要吗? -Facebook is mired in a series of controversies about the curation of its news feed, from its broadcasting live killings, to editing out an iconic photo of the Vietnam war, to accusations of political bias. It recently tried to smooth the process out by firing its human editors … only to find the news feed degenerated into a mass of fake and controversial news stories. +Facebook 陷入一系列信息流内容管理的争议中,从直播杀戮,到删除越战的象征图片,再到政治偏见的指控。它最近试图通过开除人工编辑消除审核流程... 仅仅为了发现信息流退化成大量虚假和有争议的新闻故事。 -[Apple’s news and music ](https://www.theguardian.com/technology/2016/may/04/apple-music-wwdc-taylor-swift) apps make much of their human curation, even hiring famous names from newsrooms and radio. Twitter invested heavily in its [Moments](https://www.theguardian.com/technology/2015/oct/06/twitter-launches-news-moments-curation) product. While not universally liked, it shows Twitter wants to curate better. Samsung’s news app divides into what you want to know and need to know; the former chosen by algorithm, the latter by editors. Big tech is on a hiring spree for old-fashioned experts. +[苹果新闻和音乐](https://www.theguardian.com/technology/2016/may/04/apple-music-wwdc-taylor-swift) 应用有大量人工内容管理,甚至找了新闻编辑部和广播的名人们。Twitter 在它的 [Moments](https://www.theguardian.com/technology/2015/oct/06/twitter-launches-news-moments-curation) 产品中下了重金。虽然普遍不看好,但 Twitter 确实希望在内容上做的更好。Samsung 的新闻应用分成你想知道的和你需要知道的;前者通过算法挑选,后者通过编辑。大型科技公司对老牌专家求贤若渴。 We’ve also got excess stuff. The average western European household owns 10,000 items, more in the US. But in order to cope we turn not to an app but the [Kondo](https://www.theguardian.com/lifeandstyle/2016/jan/21/tidying-up-marie-kondo-spark-joy-new-york-book-singing) method, the wildly popular home organisation technique relying on our personal histories. In retail’s upper tier, a renewed emphasis on expert selection is behind the success of shops as diverse as fashion boutique [Opening Ceremony](https://www.theguardian.com/fashion/fashion-blog/2012/jul/23/opening-ceremony-london) and “supermarket of the future” Eataly. As with our media, we’ve passed from an era of bulk industrial selection to finely honed choice. -Curation can be a clumsy, sometimes maligned word, but with its Latin root *curare* (to take care of), it captures this irreplaceable human touch. We want to be surprised. We want expertise, distinctive aesthetic judgments, clear expenditure of time and effort. We relish the messy reality of another’s taste and a trusted personal connection. We don’t just want correlations – we want a why, a narrative, which machines can’t provide. Even if we define curation as selecting and arranging, this won’t be left solely to algorithms. Unlike so many sectors experiencing technological disruption, from self-driving cars to automated accountancy, the cultural sphere will always value human choice, the unique perspective. +我们也有多余的东西。西欧家庭平均拥有 1 万件东西,美国家庭更多。但是处理这个情况不需要应用,只需要 [Kondo](https://www.theguardian.com/lifeandstyle/2016/jan/21/tidying-up-marie-kondo-spark-joy-new-york-book-singing) 方法,受到广泛欢迎的家庭整理术依靠我们的个人经验。在零售业的上游,成功商店的背后有一个更新的专家精选,和时尚精品 [Opening Ceremony](https://www.theguardian.com/fashion/fashion-blog/2012/jul/23/opening-ceremony-london) 和 ”未来超市“ Eataly 一样多样化。随着媒体发展,我们从大量工业选择时代过渡到精选时代。 -This is where the arts and humanities strike back in a world of machine learning. Here is a new generation of jobs. Information overload and its technology-driven response are one of the great transformations of our time. But amid today’s saturation (and those teetering piles of new books), knowledge and subjective judgment are more valuable than ever. In the words of one Silicon Valley investor, “software eats the world”. Well, software can’t eat human curation. Contrary to myth, traditional gatekeeping roles are here to stay. +精选可以是不得当的,有时还是贬义的词语,然而加上拉丁语词根 ***curare***,它捕捉人类独一无二的触摸。我们想要惊喜,我们想要专业知识,独特的审美评论,取消的时间和精力的花费。我们享受另一种味道的混乱的现实世界,也相信人与人之间的联系。我们不仅想要相关性,我们还想知道为什么,想要故事,这是机器无法提供的。即使我们将精选定义为选择和排列,这也不完全是算法的工作。与许多行业经历技术破坏不同,从自动驾驶汽车到自动化会计,文化领域将一直重视人类选择和独特的感受。 -What we will see are hybrids: rich blends of human and machine curation that handle huge datasets while going far beyond narrow confines. We now have so much – whether it’s books, songs, films or artworks (let alone data) – that we can’t manage it all alone. We need an “algorithmic culture”. Yet we also need something more than ever: human taste. +这是艺术和人文对机器学习世界的反击。这会创造新的就业。信息过载和技术驱动响应是我们的时代最好的转变。但是在当今的饱和(和那些成堆的摇摇欲坠的书)中,知识和主观判断比以往更有价值。用一名硅谷投资人的话说,“软件吃掉了世界“。当然,软件吃不掉人类选择。与神话相反,传统守门人角色仍健在。 + +我们下一步要看的是混合动力车:充分混合了人类和机器选择来处理庞大数据集,在狭窄范围之上越走越远。我们现在有许多我们不能独自处理的东西,例如书籍、音乐、电影和艺术作品。我们需要一个 ”算法文化“。但是我们比以往更需要:人类品味。 From 46ae3a9f63d3e4e8bfab7e4b80c12f819d98c666 Mon Sep 17 00:00:00 2001 From: sqrthree Date: Sat, 24 Dec 2016 16:43:17 +0800 Subject: [PATCH 18/38] creat react-or-vue-which-javascript-ui-library-should-you-be-using.md --- ...vascript-ui-library-should-you-be-using.md | 218 ++++++++++++++++++ front-end.md | 2 +- 2 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 TODO/react-or-vue-which-javascript-ui-library-should-you-be-using.md diff --git a/TODO/react-or-vue-which-javascript-ui-library-should-you-be-using.md b/TODO/react-or-vue-which-javascript-ui-library-should-you-be-using.md new file mode 100644 index 00000000000..3c67483a76d --- /dev/null +++ b/TODO/react-or-vue-which-javascript-ui-library-should-you-be-using.md @@ -0,0 +1,218 @@ +> * 原文地址:[React or Vue: Which Javascript UI Library Should You Be Using? +](https://medium.com/js-dojo/react-or-vue-which-javascript-ui-library-should-you-be-using-543a383608d#.ahhl9apir) +* 原文作者:[JS Dojo](https://medium.com/@jsdojo) +* 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) +* 译者:[]() +* 校对者:[]() + +# React or Vue: Which Javascript UI Library Should You Be Using? + +![](https://cdn-images-1.medium.com/max/2000/1*jmH6Eu_WPkTJ0ajzW_pZqA.png) + +In 2016 React cemented its position as king of the Javascript web frameworks. This year saw rapid growth of both its web and native mobile libraries, and a comfortable lead over main rival Angular. + +But 2016 has been an equally impressive year for Vue. The release of its version 2 made a huge impression on the Javascript community, attested to by the 25,000 extra Github stars it gained this year. + +The scope of both React and Vue is undeniably similar: both are lightweight component-based libraries for building user interfaces that focus on the view layer only. Both can be used in a simple project, or be scaled up to a sophisticated app using cutting edge tooling. + +As a result, a lot of web developers are wondering which one they should be using. Is one clearly superior over the other? Do they have specific pros and cons to be aware of? Or are they basically the same? + +#### **Two frameworks, two advocates.** + +In this article I want to answer those questions with a thorough and fair comparison. The only problem is: I’m an unashamed Vue fan-boy and totally biased. I’ve used Vue heavily in my projects this year, sung its praises here on Medium and even released a [Udemy course](https://www.udemy.com/vuejs-2-essentials). + +To even out my biased position I’ve bought in my friend Alexis Mangin who is both a great Javascript developer and a big React fan. He’s similarly immersed in React, using it frequently in both web and mobile projects. + +Alexis asked me one day: “why are you so into Vue, and not React?” Since I didn’t know React that well, I couldn’t give a good answer. So I put the idea to him that we sit down one day with our laptops and show each other what our chosen library had to offer. + +![](https://cdn-images-1.medium.com/max/1600/1*0_PABYfDH4JN8GDzg2rgvA.jpeg) + +After a lot of discussion and learning from both sides, the following six points are our key findings. + +#### If you like building apps with templates (or want the option to), go with Vue. + +Putting your markup in an HTML file is the default option for a Vue app. Similar to Angular, moustache braces are used for data-binding expressions, while directives (special HTML attributes) are used for adding functionality to the template. + +The follow demonstrates a simple Vue app. It prints a message and has a button that dynamically reverses the message: + + // HTML + +
+

{{ message }}

+ +
+ + // JS + + new Vue({ + el: '#app', + data: { + message: 'Hello Vue.js! + }, + methods: { + reverseMessage: function () { + this.message = this.message.split('').reverse().join(''); + } + } + }); + +In contrast, React apps shun templates and require the developer to create their DOM in Javascript, typically aided with JSX. Below is the same simple app implemented with React: + + // HTML + +
+ + // JS (pre-transpilation) + + class App extends React.Component { + constructor(props) { + super(props); + this.state = { + message: 'Hello React.js!' + }; + } + reverseMessage() { + this.setState({ + message: this.state.message.split('').reverse().join('') + }); + } + render() { + return ( +
+

{this.state.message}

+ +
+ ) + } + } + + ReactDOM.render(App, document.getElementById('app')); + +Templates are easier to understand for newer developers who’ve come from the standard web development paradigm. But even some experienced developers prefer them as templates can better seperate layout from functionality and give the option of using pre-processors like Pug. + +But templates come at the cost of having to learn all the extended HTML syntax, while render functions only require knowledge of standard HTML and Javascript. Render functions also benefit from easier debugging and testing. + +On this point, though, you can’t go wrong with Vue, as it’s introduced the option of using either templates *or* render functions in version 2. + +#### If you like simplicity and things that “just work”, go with Vue. + +A simple Vue project can be run directly from a browser with no need of transpilation. This allows Vue to be easily dropped into a project the way jQuery is. + +While this is also technically possible with React, typical React code leans more heavily on JSX and on ES6 features like classes and non-mutating array methods. + +But Vue’s simplicity runs more deeply in its design. Let’s compare how the two libraries handle application data (i.e. “state”). + +State in React is immutable so you can’t directly change it. You need to use the *setState *API method: + + this.setState({ + message: this.state.message.split('').reverse().join('') + }); + +Diff’ing the current and previous state is how React knows when and what to re-render in the DOM, hence the need for immutable state. + +In contrast, data is just mutated in Vue. The same data property can be altered far less verbosely in Vue: + + // Note that data properties are available as properties of + // the Vue instance + + this.message = this.message.split('').reverse().join(''); + +Before you conclude that Vue’s rendering system must lack the efficiency of React’s, let’s examine how state in Vue is managed under the hood: when you add a new object to the state, Vue will walk through all of its properties and convert them to getter and setters. Vue’s reactivity system now keeps track of the state and will automatically re-render the DOM when it is mutated. + +Impressively, altering state in Vue is not only more succinct, but its re-rendering system is actually faster and more efficient than React’s. + +Vue’s reactivity system does have caveats, though. For example, it cannot detect property addition or deletion and certain array changes. These cases can be worked around with a React-like *set* method from the Vue API. + +#### If you need your application to be as small and fast as possible, go with Vue. + +Both React and Vue will build a virtual DOM and synchronise the real DOM when the app’s state changes. Both have their own means of optimising this process. + +Vue core developers have offered a benchmark test that shows Vue’s rendering system to be faster than React’s. In this test a list of 10,000 items are rendered 100 times. The comparison is tabled below. + +![](https://cdn-images-1.medium.com/max/1600/1*xy2KzzZrqP0mdLUbIdC-xA.png) + +From a pragmatic standpoint, this kind of benchmark is only relevant in edge cases. Most apps will not need to do this kind of operation routinely so it should generally not be considered an important point of comparison. + +Page size, though, is relevant to all projects, and again Vue has the upper hand. Minified, the current release of the Vue library is only 25.6KB. + +To get a similar set of functionality in React you need React DOM (37.4KB) and the React with Addons library (11.4KB), which totals 48.8KB, almost double the size of Vue. To be fair you will get a larger API with React, but you don’t get double as much functionality. + +#### If you plan to build a large scale app, go with React. + +A comparison of a simple app implemented in both Vue and React, like the one at the beginning of this article, may initially bias a developer to favour Vue. This is because template-based apps are easier to understand at first look, and quicker to get up and running with. + +But these initial benefits introduce technical debt that can slow development of apps reaching a larger scale. Templates are prone to unnoticed runtime errors, are hard to test, and are not easy to restructure or decompose. + +In contrast, Javascript-made templates can be organised into components with nicely decomposed and DRY code that is more reusable and testable. + +Vue also has a component system and render functions. But React’s rendering system is more configurable and has features like shallow rendering that, combined with React’s testing utilities, allow for far more testable and maintainable code. + +Meanwhile, React’s immutable application data may not be as succinct, but it shines in larger application when transparency and testability become critical. + +#### If you want a library that is adaptable for both web and native apps, go with React. + +React Native is a library for building native mobile applications with Javascript. It’s the same as React.js, only instead of using web components, it uses native components. If you’ve learnt React.js, you’ll very easily be able to pick up React Native, and vice versa. + + // JS + + import React, { Component } from 'react'; + import { AppRegistry, Text, View } from 'react-native'; + + class HelloWorld extends Component { + render() { + return ( + + Hello, React Native! + + ); + } + } + + AppRegistry.registerComponent('HelloWorld', () => HelloWorld); + +The significance is that a developer can build an app on either the web or native mobile without requiring a different set of knowledge and tools. Learning React gives you a huge bang for you buck if you intend to develop for both web and mobile. + +Alibaba’s Weex is another cross-platform UI project. Currently it considers Vue an “inspiration” and uses a lot of the same syntax, with plans to fully integrate Vue. However, the timeline and specifics of this integration are still unclear. + +Since Vue has HTML templates as a core part of its design and does not have custom rendering as a current feature, it’s hard to see that a native counterpart for Vue.js in its current form will be as tight as what React.js and React Native are. + +#### If you want the biggest ecosystem, go with React. + +There’s no question that React is currently the more popular library with ~2.5M NPM downloads a month as opposed to Vue’s ~225K per month. + +![](https://cdn-images-1.medium.com/max/1600/1*YTD_ZbHE77GWKzYJVGh_Fw.png) + +Popularity is not merely a shallow benefit. It means there are more articles, tutorials and Stack Overflow answers for help. It means there are more tools and add-ons to leverage in a project and save developers from building everything themselves. + +Both libraries are open source, but React was born from Facebook and benefits from that patronage. Developers and companies committing to React can be assured of continued maintenance. + +In contrast, Vue was created by a single developer, Evan You, and You is currently the only full time maintainer of Vue. Vue has some corporate sponsorship but not on the scale of Facebook or Google. + +To the credit of the Vue team, its small size and independence has not materialised as a disadvantage. Vue has a regular release cycle and even more impressively, Vue has only 54 open issues on Github compared to 3456 closed issues, while React has a far larger 530 open issues compared to 3447 closed. + +#### If you’re already happy with one or the other, there’s no need to switch. + +To recap, our findings, Vue’s strengths are: + +- Flexible options for template or render functions +- Simplicity in syntax and project setup +- Faster rendering and smaller size + +React’s strengths: + +- Better at scale, studier and more testable +- Web and native apps +- Bigger ecosystem with more support and tools available + +However, both React and Vue are exceptional UI libraries and have more similarities than differences. Most of their best features are shared: + +- Fast rendering with virtual DOM +- Lightweight +- Reactive components +- Server-side rendering +- Easy integration with router, bundler and state management +- Great support and community + +If you think we’ve missed something we’d love to hear in the comments. Happy developing! diff --git a/front-end.md b/front-end.md index 01c4ffdea20..318cea36d32 100644 --- a/front-end.md +++ b/front-end.md @@ -95,7 +95,7 @@ * [在 chrome 的开发者工具里 debug node.js 代码](http://gold.xitu.io/entry/56d3cfea6be3ff005c5f9b89?utm_source=gold-miner&utm_medium=readme&utm_campaign=github) ([sqrthree](https://github.com/sqrthree) 翻译) * [350 个特性看透 ES6](https://github.com/xitu/gold-miner/blob/master/TODO/es6.md) ([Go7hic](https://github.com/dyygtfx) 翻译) * [2015 年底 JS 必备工具集](http://gold.xitu.io/entry/56cee8afc24aa800545f73bb?utm_source=gold-miner&utm_medium=readme&utm_campaign=github) ([赵鑫晖](https://github.com/zxc0328) 翻译) -* [[译] Promise 是如何工作的?](http://gold.xitu.io/entry/56cc0bcf8ac2470053b7c5ab?utm_source=gold-miner&utm_medium=readme&utm_campaign=github) ([Zhangjd](https://github.com/Zhangjd) 翻译) +* [Promise 是如何工作的?](http://gold.xitu.io/entry/56cc0bcf8ac2470053b7c5ab?utm_source=gold-miner&utm_medium=readme&utm_campaign=github) ([Zhangjd](https://github.com/Zhangjd) 翻译) * [JavaScript 开发者年度调查报告](http://gold.xitu.io/entry/56ca985071cfe40054d98994?utm_source=gold-miner&utm_medium=readme&utm_campaign=github) ([sqrthree](https://github.com/sqrthree) 翻译) * [Functional JavaScript 教程(一)](http://gold.xitu.io/entry/56d4e8b57db2a200556c64cb?utm_source=gold-miner&utm_medium=readme&utm_campaign=github) ([Zhangjd](https://github.com/zhangjd) 翻译) * [人人须知的 jQuery 技巧](http://gold.xitu.io/entry/56e1a95b731956005da35c24?utm_source=gold-miner&utm_medium=readme&utm_campaign=github) ([Yves-X](https://github.com/Yves-X) 翻译) From 6c5d6a387ba9ad4afbb01696a4b891d135be80b1 Mon Sep 17 00:00:00 2001 From: sqrthree Date: Sat, 24 Dec 2016 18:24:40 +0800 Subject: [PATCH 19/38] 10-things-i-learned-making-the-fastest-site-in-the-world.md --- ...ed-making-the-fastest-site-in-the-world.md | 422 ++++++++++++++++++ 1 file changed, 422 insertions(+) create mode 100644 TODO/10-things-i-learned-making-the-fastest-site-in-the-world.md diff --git a/TODO/10-things-i-learned-making-the-fastest-site-in-the-world.md b/TODO/10-things-i-learned-making-the-fastest-site-in-the-world.md new file mode 100644 index 00000000000..3839493c667 --- /dev/null +++ b/TODO/10-things-i-learned-making-the-fastest-site-in-the-world.md @@ -0,0 +1,422 @@ +# 10 things I learned making the fastest site in the world + +This post is about performance techniques, so I hope you won’t mind that the site in question is not *quite* finished. + +But you need something to click on so you can decide whether you value my opinion or not, so here you go: [https://knowitall-9a92e.firebaseapp.com/](https://knowitall-9a92e.firebaseapp.com/) + +Hopefully that opened quickly and I have established credibility. If that sounds immodest it’s because I’m awesome. + +Let me practice my pitch for the site: “have you ever wondered what you don’t know about the web? What hidden property or method or attribute has managed to evade your attention? Wouldn’t you like to go through a long list and tick off the stuff you know, to be left with a glorious summary of things to go and learn?” + +If you’re wondering why I’m writing about this site when it’s not finished… because feedback. Performance is a series of tricks and ways of thinking about things. I certainly hope I don’t know everything there is to know, and I would be most pleased if you had something to offer me. The [source is here](https://github.com/davidgilbertson/know-it-all) if you would like to get your feet wet. + +### Let’s talk speed + +What’s that, you want a chart? + +What’s that, you want it to be *unnecessarily stylised*? + +![](https://cdn-images-1.medium.com/max/800/1*FGWpR_rcxj7IhhozihOgdw.png) + +### Is it a fair fight? + +Oh no, not at all. These sites do completely different things, it’s not sensible to think they should all take the same time to load. But these are all sites that have put effort into performance and aiming to make a site load faster than the Google home page keeps the pressure on. + +### Why it isn’t very impressive + +If by chance you are impressed, I suggest you cool your jets (for the moment). I don’t have to read from a database to produce content, or sign you in, or load seven 3rd party ads that each do 40 redirects before they load a flash file. I don’t even have any pictures. Oh and page count = 1. + +### Why it isn’t completely unimpressive + +In the time before the site is loaded and ready to go, I’m downloading and parsing a 75,000 line JSON file. The result is a tree that, if expanded, would be 9,986 rows (yep, I read all the specs and typed all that out). + +And, I’m using a library. Libraries are slow, no matter how fast they are. + +--- + +I learned a few things along the way and changed my attitude about as much as I’m capable of changing my attitude, and have rounded it all up into ten little learnings. Seven of these I would have found quite useful two months ago, three are just rubbish. + +I’m not telling you which three. + +### #1 Try not to make a slow site + +I heard this recently from a non-web-developer: “I was on mercedes.com the other day and it was *so slow*, how can they even make a site that slow?” + +I was getting a tattoo at the time so my response was mostly in the upper register and not what one would call comprehensible. But I was *thinking* that it’s really easy to make a slow site, all you have to do is not try to make a fast one. + +(It’s a bear with a machine gun.) + +This is good news, because if all you do is *try* to make a fast site, you will automatically get a faster site. You have to try non stop, but it’s like being a jerk, it’s actually pretty easy with a bit of practice. + +For this site, at every step of the way, I took a few moments to think of the performance impact of what I was doing. For literally every library that I’ve used in this app, I’ve timed three metrics before and after: + +- first meaningful paint +- time to interactive +- expanding one DOM node + +If a library had a negative impact, out it goes. For example, because I’m an idiot head, I was at one point doing a lodash `deepClone` on my 75,000 prop JS object. Switching that out for `Immutable.js` made a big difference. + +I’m using React, and at one point brought in the `classnames` library. Then I measured my metrics again and … no difference. `classnames`, you may stay. + +The annoying five minutes you have to spend every time you import a new library or make a big change is a pretty cheap way to get performance gains. It’s exactly the same as buying a dolphin, right? If you take the time upfront to get matching bandanas oh I’m not sure where I was going with that. On to … + +### #2 Do mobile first. Like, really do it. + +There are two “mobile first” strategies. + +In the first one (which I have done up until this project), I sit at my 27" monitor with a website imax style in front of me with a monster fan-cooled CPU and oceans of RAM doing whatever I ask of them. + +Then I write my CSS media queries with `min-width` and tell my friends I’m doing mobile first. + +![](https://cdn-images-1.medium.com/max/1000/1*CG8BUSNsCzk7tC4h28_IZg.png) + +For this project I actually did *real *mobile first. That is, developed the site with it running on a **mobile** device. I did this **first**, and when I was satisfied with the UI and the performance, I went about getting it to work on a big computer. + +You’d be surprised how easy it is to get a fast site to run on a fast machine! + +(The story isn’t quite that simple, I started the project with my old bad habits, then halfway through got all grown up and started developing on mobile by default. This post is genuinely a list of ten things I learned, except the three rubbish ones.) + +Now, running your site only on a phone is the dream, but for comparing performance metrics over many days and weeks, you want a consistent benchmark. If you [watch this video](https://www.youtube.com/watch?v=4bZvq3nodf4&index=17&list=PLNYkxOF6rcIBTs2KPy1E6tIYaWoFcG3uj) you will learn that testing on a mobile device for-reals is not at all consistent. You will also learn why I said “fan-cooled CPU” like that was a big deal. + +When you do your benchmarking, you should use the Chrome DevTools and throttle your CPU and network. I use a 10x CPU slowdown and set the network to “Good 3G”. I know that’s maybe not quite as slow as the average phone, but I don’t want to get so frustrated with slow speeds that I get out of the habit of really doing this. + +Because the key is to not just nod and agree it’s a good idea, but to actually, really, do it. + +![](https://cdn-images-1.medium.com/max/1000/1*YIpE12eX50W6tPUhVd94TQ.png) + +Here’s something I find surprising: really loud noises. + +Here’s something else I find surprising: I have a big fat i7 CPU that I do most of my work on, and a brand new Pixel XL: the fastest phone in the world. What do you think the phone performance will be? 80% of the desktop? 60%? Any other guess above 10%? + +Wrong! It’s only 10%. If I slow my i7 down *by ten times*, it is the same speed as the $1,400 phone in front of me. + +That’s the difference between a 20ms click and a 200ms click. Or a frame taking 16ms to render vs 160ms. + +(Let me know in the comments if you would like more examples of numbers multiplied by 10). + +### #3 Be a benchmark hussy + +My ego came into play here (it was outside). Once I got one good score on a benchmarking site I wanted to go to all the benchmarking sites to bask in their automated praise. + +When I ran lighthouse over the site and only got a 97 the basking ceased quick smart. Where the hell are my last three points! + +![](https://cdn-images-1.medium.com/max/800/1*368wXO3XzjX4KubqA41olw.png) + +Oh I see, I’m being told I have an input latency of 285ms. That would be quite appalling, *if it were true.* But I know it’s only 20ms. + +Clearly the lighthouse people are wrong idiots. + +Then I reluctantly admitted to myself that perhaps I should look into this, despite the fact that I was obviously right and an algorithm written by Google was clearly in the wrong. + +This prompted me to start the whole CPU slowdown thing, and sure enough, the response times that I *thought* were instant became 200+ ms laggards. + +So I did some good ‘ol profiling and it looked like a lot of the time was being spent in React land. I had already done all the React performance best practices, I had no ‘wasted updates’ (something that’s easy to just do as you go along). + +I even did some silly stuff memoizing low-cardinality components. (I don’t *really* know what cardinality means, but I think I used it right.) + +I should point out here that I am a huge React fan. I have named three of my pets React (dog, possum, possum), I only stopped because they kept dying and I wondered if it was something in the name. + +Such is my love for React that I felt like I was on Ashley Madison when I was looking up other front-end frameworks. But performance beats fidelity and into the loving embrace of Preact I flopped. + +At first I tried `preact-compat`. It took about 15 minutes to convert my codebase. Immediate improvement. Sweeeet. + +I told the dude who wrote Preact and he said I should try full Preact and *wowsers* it was even faster. + +Y’all want a chart, don’t y’all? + +![](https://cdn-images-1.medium.com/max/800/1*sOt-hI-gzfnGSanKocQ2rg.png) + +Another tweeter chimed in that I should try Inferno (wait, can *everyone* see everyone’s tweets? We must warn the others!). So I converted my app to `inferno` to squeeze a few extra drops of that sweet sweet performance juice. + +What? Chart? Green? + +![](https://cdn-images-1.medium.com/max/800/1*NN4ipFryJOtaTzh7Yyps4Q.png) + +OK, I tried, and Inferno is fast, but not quite as fast as Preact. So I rolled back that change. + +There’s a lesson here, don’t be shy about throwing work out. But you should be shy in general. No one likes an extrovert. With their “fun” and their “talking”. + +Whenever I feel reluctant to throw out some work, I recall that life is pointless, and nothing we do even exists if the power goes out. There’s a handy hint for ya. + +And now … + +![](https://cdn-images-1.medium.com/max/800/1*gnMQQam81ZgIXjmSgJeW_A.png) + +Next up I tested on yslow. I almost got top marks, except they gave me a D (!) for having too many DOM nodes. Which is ridiculous because I know how many DOM nodes I need and no one else can tell me what to do because I’m the boss of me. + +Clearly the yslow people are wrong and stupid. + +Then I reluctantly thought that I should *maybe* have a look at reducing the number of DOM nodes I rendered. So I changed which branches of my tree are expanded by default. + +Chart? + +White on blue this time? + +![](https://cdn-images-1.medium.com/max/800/1*AQ15w9wzbpx5LmDuEXAkmw.png) + +Thanks, yslow people, turns out that was a pretty good suggestion. + +--- + +They were the rubbish three, the rest are pure gold. + +### #4 Client Side Rendering is expensive + +Client Side Rendering (CSR), or as I call it “setting money on fire and throwing it in a river” has its uses, but for this site would have been madness. + +I don’t have anything user-specific on the page so I can send out the same HTML to everyone. Also I have a relatively hefty processing job to do on the client, making CSR even worse. CSR was clearly not the fastest way for me to get my pixels into your eye holes. + +Here’s my advice if you are building a site for a company that has — and desires to maintain — a revenue stream: + +- Go into your analytics and discover what percentage of traffic comes from Bing. (For my employer it’s 1.6%). +- Yes that’s right, Bing. Because they do not execute JS when indexing and will thus not index a CSR site. +- Multiply your employer’s yearly revenue by 1.6%. +- Ask your employer if they’re happy for that number of dollars to go to the competition because you won’t be in the Bing search results any more. + +Yes that logic has more holes than a, um, net. But you get the idea. + +I have digressed. + +Just send rendered HTML from your server. + +### #5 Don’t server-render HTML + +Well that certainly seems contradictory, what sort of trickery is this? How do you serve HTML without server-rendering HTML? You do what people probably did in the 90s… + +React (and its faster little siblings) will take a few dozen milliseconds to render a modest page of HTML. (Does anyone have stats on how long PHP or JSP takes? I’d be interested to see a comparison.) That means a single core can only service maybe 50 people per second; additional requests would be neatly queued, this is no good. + +For my little site I am sending the same HTML in every response. If your site happens to be the same, then you don’t need a web server sitting there rendering HTML and sending out CSS and JS files on demand. You can **generate your HTML at build time** and ship the whole lot from static hosting (or cache it on a good CDN). Github and Firebase and probably other people will give you static hosting just because they’re nice. + +This idea doesn’t apply to many so I won’t be upset if you skip this one. But if you have any pages that can be rendered at build time (e.g. the home page of LinkedIn, PayPal, GitHub) then take advantage of that. + +I actually came about this backwards. I was looking at some blog and I realised that it was only 96 milliseconds ago that I had clicked on the link to said blog. (Yet somehow I maintain that *mine* is the fastest site in the world — reality doesn’t really come into it.) + +I did some digging and discovered the blog was hosted as a static site with Firebase. I have already fathered a child with Firebase and quite enjoyed myself so I thought I’d try out their static hosting. + +But that would mean I have to generate my HTML at build time. + +[scratches chin with squinty eyes] + +If only React was capable of outputting its generated DOM as a string. Then I could just save that string to an HTML file as part of my build script. + +For those not in the know, the above is very funny because React *does* have a method, called `renderToString`. + +(As it happens, it’s a good opportunity to run the HTML through a minifier before saving to disk.) + +### #6 Inline stuff, probably + +Every time I’ve sat down to work out if inlining CSS is worth it or not, I have come the conclusion that … it depends. + +If you’re facebook.com and 99.9% of page views are return visitors, then have a separate CSS file and cache it. If you’re a funeral home website that doesn’t get many repeat visits, you might want to inline your CSS and save a network request. + +If your life is too easy and you want to create a never ending stream of tears and frustration you can try and inline the CSS that applies to elements above the fold and have the rest load in a separate CSS file. + +My personal rule now (so I don’t have to think about it): if you can get all your CSS into your HTML and keep the lot under 14 KB, do it. (Don’t know why 14? [Read this](https://hpbn.co/building-blocks-of-tcp/#slow-start).) + +My CSS + HTML (minified) is 3.5 KB, so it’s a no brainer — I’ve inlined my CSS. + +### #7 Preload, then load + +Our ``, much like our feet, should be at the bottom of our ``. Oh I just realised that’s why they call it a footer! OMG and header too! Oh holy crap, body as well! I’ve just blown my own mind. + +[five minutes later…] + +![](https://cdn-images-1.medium.com/max/800/1*N0FXMcRze-eZU0bauRx-1g.png) + +Sometimes I think I’m too easily distracted. I was once told that my mind doesn’t just wander, it runs around screaming in its underpants. + +What are you doing here? + +Ah yes, blog post about site speed. + +--- + +A common pattern in React with SSR (Server Side Rendering, if you’d forgotten) is this: + +1. On the server, generate the HTML by passing in some data for the components to render. +2. Write that data into the HTML document like `window.APP_DATA = data;` +3. Send both the rendered DOM (based on the data) *and the data* back in the HTML payload. +4. In the browser, read `window.APP_DATA` and ‘rehydrate’ the app with it. + +I always thought that was a bit wasteful size-wise, but that it doesn’t really matter because the HTML will be rendered in the same time, it’s just the JavaScript will be delayed a bit. This doesn’t matter if your site works just fine before JavaScript is loaded. But my site does sweet fudge all if there isn’t any JavaScript. + +So, this is what I wanted to happen: + +- Start downloading the data as soon as possible (without blocking the HTML). +- Start downloading the app’s JavaScript as soon as possible (without blocking the HTML). +- When both the script and the data are downloaded, and the HTML has been parsed, run the script which will breath life into the page. + +I could have done all sorts of fancy footwork in JavaScript to get this working, but I have a better solution. Well, it works, but I have a nagging feeling I’m doing something dumb. + +Browser experts get your magnifying glasses out… + +- In the `` I have a `` for both the JSON and and the JS (I have `prefetch` as well for browsers that don’t support preload yet) +- At the end of the body I load the application JS in a regular `