使用 Grails 构建 TVML 应用
使用 Grails 为 Apple TV 构建一款应用。利用标记视图、资产管道和 Grails 的国际化功能简化 TVML 的开发
作者:Sergio del Amo
Grails 版本 3.3.1
1 Grails 培训
Grails 培训 - 由 Grails 框架的创建者和积极维护人员开发并提供!.
2 入门
在此指南中,你将编写 TVML 应用程序。
TVML 应用让你可以使用 TVMLKit JavaScript (TVMLKit JS)、Apple TV 标记语言 (TVML) 和 TVMLKit 框架为 Apple TV 创建应用。每个模板都有符合 Apple 样式指南的特定设计,能让你快速创建美观的应用。
在此指南中构建的 TVML 应用显示 Apple TVML Stack Template Grails Quickcast。Grails Quickcast 是 pure Grails 编码的短视频。当用户选择一项时,Apple TVML Product Template 显示 Quickcast 详情。用户可以通过选择播放按钮播放视频。
2.1 架构
本指南采用客户端-服务器架构。客户端是 tvOS 应用程序。服务器是 Grails 应用程序。客户端连接到服务器,服务器会响应 TVMLKit JS 和 TVML 文档。
我们将 Grails 应用程序与数据(mp4 文件和 Png 文件)以及视频和缩略图分开了。我们使用 MAMP 来在本地 Apache 服务器上运行端口 8888,并指向数据文件夹。在阅读本指南时,您会看到对 mp4 文件(如下所示)的引用:
heroImg: 'http://localhost:8888/quickcast_interceptor.png',
在产品环境中,您可能希望将这些文件托管在诸如 Amazon Simple Store Service - AWS S3 等服务中
2.2 您需要什么
要完成本指南,您需要以下内容
-
一些时间
-
一个像样的文本编辑器或 IDE
-
Java Development Kit (JDK) 1.8 或更高版本,并妥善配置了
JAVA_HOME
-
XCode 的最新稳定版本。我们使用 Xcode 8.2.1 编写了本指南。
2.3 如何完成本指南
要在本指南中开发 Grails 应用程序,您需要从 Github 检出源代码,并完成指南提供的步骤。
要开始,请执行以下操作
-
下载并解压缩源代码,或者如果您已安装 Git:
git clone https://github.com/grails-guides/grails-tvmlapp.git
-
cd
至grails-guides/grails-tvmlapp/initial
-
转至下一部分
如果您cd 至 grails-guides/grails-tvmlapp/complete ,可以直接转至已完成的示例 |
3 编写 TVML 客户端
要创建 TVML 应用程序并将其连接到 Grails 服务器,我们需要修改变量 tvBaseURL
和 tvBootURL
。
使用 TVML 应用程序模板创建一个 TvOS 项目。
选择 Swift 作为语言。
添加一个新值(区分大小写)App Transport Security Settings,并将其作为它的子级,添加Allow Arbitrary Loads,并将其值设置为 YES。
向 Info.plist 中添加此键是必要的,因为从 iOS 9 开始,为了鼓励良好的实践,您的应用程序会阻止链接到非 HTTPS 服务器。在本教程中,您将针对未启用 HTTPS 的本地服务器进行测试,因此您将禁用默认行为。 |
修改 AppDelegate.swift
,并设置静态变量 tvBaseURL
和 tvBootURL
。
static let tvBaseURL = "http://localhost:8080/"
static let tvBootURL = "\(AppDelegate.tvBaseURL)/assets/application.js"
4 编写 TVML Grails 应用程序
初始 Grails 应用程序(您可以在 initial 文件夹中找到)通过该命令创建
grails create-app --profile rest-api -features=hibernate5,markup-views,asset-pipeline
您知道吗?您可以在不安装任何其他工具的情况下下载一个完整的 Grails 项目。前往 start.grails.org,并使用 Grails 应用程序生成器 来生成您的 Grails 项目。您可以选择您的项目类型(应用程序或插件),挑选 Grails 版本并选择个人资料 - 然后点击“生成项目”以下载一个 ZIP 文件。无需安装 Grails! 您甚至可以使用类似于
|
4.1 TVMLKit JS Utils
TVMLKit JS 是一个 JavaScript API 框架,专门设计用于 Apple TV 和 TVML。TVMLKit JS 包含了 JavaScript 中的基本功能以及为 Apple TV 设计的特殊 API。使用 TVMLKit JS,您可以加载和显示 TVML 模板、播放媒体流、加载自定义内容以及控制应用流程。有关更多信息,请参阅 TVJS Framework Reference。
将 JavaScript 文件添加到我们的项目。它定义了一系列方法,几乎与 TVML Programming Guide 的“播放媒体”部分中描述的方法相同。
这些方法允许将页面推送到导航堆栈、获取文档和播放媒体。
function loadingTemplate() {
var template = '<document><loadingTemplate><activityIndicator><text>Loading</text></activityIndicator></loadingTemplate></document>';
var templateParser = new DOMParser();
var parsedTemplate = templateParser.parseFromString(template, "application/xml");
navigationDocument.pushDocument(parsedTemplate);
}
function getDocument(extension) {
var templateXHR = new XMLHttpRequest();
var url = baseURL + extension;
loadingTemplate();
templateXHR.responseType = "document";
templateXHR.addEventListener("load", function() {pushPage(templateXHR.responseXML);}, false);
templateXHR.open("GET", url, true);
templateXHR.send();
}
function pushPage(document) {
var currentDoc = getActiveDocument();
if (currentDoc.getElementsByTagName("loadingTemplate").item(0) == null) {
navigationDocument.pushDocument(document);
} else {
navigationDocument.replaceDocument(document, currentDoc);
}
}
function playMedia(videourl, mediaType) {
var singleVideo = new MediaItem(mediaType, videourl);
var videoList = new Playlist();
videoList.push(singleVideo);
var myPlayer = new Player();
myPlayer.playlist = videoList;
myPlayer.play();
}
4.2 TV 启动 URL
本指南使用 Asset Pipeline 插件
Asset-Pipeline 是一个插件,用于主要通过 Gradle (但不是强制性的)在 JVM 应用程序中管理和处理静态资产。Asset-Pipeline 功能包括处理和压缩 CSS 和 JavaScript 文件
创建一个 javascript 文件作为我们 TVML Grails 应用的入口
// This is a manifest file that'll be compiled into application.js.
//
// Any JavaScript file within this directory can be referenced here using a relative path.
//
// You're free to add application-wide JavaScript to this file, but it's generally better
// to create separate JavaScript files as needed.
//
//= require tvmlkit.js
//= require_self
var baseURL;
App.onLaunch = function(options) {
baseURL = options.BASEURL;
var extension = "quickcast";
getDocument(extension);
}
请注意,下一条列表中显示的行如何包括上一部分中讨论的 TVMLKit JS 文件。
//= require tvmlkit.js
初始 TVML 文档是 http://localhost:8080/quickcast
呈现的 XML。
下一行指定初始文档
getDocument(extension);
4.3 域名类
创建一个持久实体来存储 Quickcasts。在 Grails 中处理持久性的最常见方法是使用 Grails 域名类
域名类实现了模型视图控制器 (MVC) 模式中的 M,并表示映射到底层数据库表的持久实体。在 Grails 中,域是位于 grails-app/domain 目录中的类。
./grailsw create-domain-class Quickcast
Quickcast
域名类是我们的数据模型。我们定义不同的属性来存储 Quickcast
特征。
package com.ociweb.quickcasts
class Quickcast {
String title
String subtitle
String description
int durationMinutes
int durationSeconds
int releaseYear
String heroImg
String videoUrl
static hasMany = [authors: String]
static constraints = {
description nullable: true
}
static mapping = {
description type: 'text'
}
}
通过 单元测试,我们测试了属性正文是否可选项。
package com.ociweb.quickcasts
import spock.lang.Specification
import grails.testing.gorm.DomainUnitTest
/**
* See the API for {@link grails.test.mixin.domain.DomainClassUnitTestMixin} for usage instructions
*/
class QuickcastSpec extends Specification implements DomainUnitTest<Quickcast> {
void "test description is optional"() {
expect:
new Quickcast(description: null).validate(['description'])
}
}
然后我们在 BootStrap.groovy
中加载一些测试数据。
package com.ociweb.quickcasts
class BootStrap {
def init = { servletContext ->
new Quickcast(title: 'Interceptors - Grails 3',
subtitle: '#1 - Grails Quickcast from OCI',
durationMinutes: 17,
durationSeconds: 01,
releaseYear: 2016,
// tag::dataServerLink[]
heroImg: 'http://localhost:8888/quickcast_interceptor.png',
// end::dataServerLink[]
videoUrl: 'http://localhost:8888/grails_quickcast_1_interceptors.mp4',
authors: ['Jeff Scott Brown'],
description: 'This Quickcast assumes only basic familiarity with Groovy (which is pretty darn expressive anyway) and the MVC concept (which you already know). Also serves as an excellent introduction to the interceptor pattern in any language, because Grails\' behind-the-scenes legwork lets you focus on the logic of the pattern.'
).save()
new Quickcast(title: 'JSON Views - Grails 3 ',
subtitle: '#2 - Grails Quickcast from OCI',
videoUrl: 'http://localhost:8888/grails_quickcast_2_jsonviews.mp4',
heroImg: 'http://localhost:8888/quickcast_jsonviews.png',
durationMinutes: 15,
durationSeconds: 40,
releaseYear: 2016,
description: '''
n a delightful and informative 15 minutes, Brown probes JSON views. Beginning with a Grails 3.1.1 application, created with a standard web profile, Brown added a few custom domain classes. The artist class has albums associated with it, and is annotated with grails.rest.Resource.
The ultimate goal is publishing a REST API under /artists for managing instances of the artist class, and to support the JSON and XML formats.
Brown uses music examples, including Space Oddity by David Bowie (RIP), and Close to the Edge by Yes. Sending a request to /artists gives a list of artists all of whom have albums associated with them. While the app is running in development mode, the JSON files can be altered and the effects of those changes can be seen real-time in the application. For example, switching "ArtistName": "Riverside" to "theArtistName": "Riverside".
This Quickcast assumes basic knowledge of Grails, JSON, and REST APIs. Check out Brown’s neat intro to JSON views!
''',
authors: ['Jeff Scott Brown']).save()
new Quickcast(title: 'Multi-Project Builds - Grails 3',
subtitle: '#3 - Grails Quickcast from OCI',
heroImg: 'http://localhost:8888/quickcast_multiprojectbuilds.png',
videoUrl: 'http://localhost:8888/grails_quickcast_2_jsonviews.mp4',
durationMinutes: 14,
durationSeconds: 28,
releaseYear: 2016,
description: '''
In this Quickcast, Graeme Rocher, Head of Grails Development at OCI, walks you through multi-project builds in Grails. Grails does a few handy things with multi-project builds and plugins, not the least of which being that Grails compiles your plugins first and puts the class and resources of those plugins directly in the classpath. This lets you make changes to your plugins and instantly see those changes in your build.
''',
authors: ['Graeme Rocher']).save()
new Quickcast(title: 'Angular Scaffolding - Grails 3',
subtitle: '#4 - Grails Quickcast from OCI',
heroImg: 'http://localhost:8888/quickcast_angularscaffolding.png',
videoUrl: 'http://localhost:8888/grails_quickcast_2_jsonviews.mp4',
durationMinutes: 9,
durationSeconds: 27,
releaseYear: 2016,
description: '''
In this Quickcast, OCI Engineer James Kleeh walks you through using the Angular Scaffolding for Grails to build a fully functional web app, using a simple blog format for demonstration. The tutorial explains how to have Grails set up a REST endpoint and all the Angular modules needed to get the web app running.
''',
authors: ['James Kleeh']).save()
new Quickcast(title: 'Retrieving Runtime Config Values - Grails 3',
subtitle: '#5 - Grails Quickcast from OCI',
heroImg: 'http://localhost:8888/quickcast_retrievingruntimeconfigvalues.png',
videoUrl: 'http://localhost:8888/grails_quickcast_2_jsonviews.mp4',
durationMinutes: 17,
durationSeconds: 51,
releaseYear: 2016,
description: '''
In the fifth Grails QuickCast, Grails co-founder, Jeff Scott Brown, highlights some of the great features of the Grails framework. In less than 18 minutes, Jeff describes several techniques for retrieving configuration values at runtime, and discusses the pros and cons of these different techniques. For this Quickcast, you’ll need no more than a basic understanding of Grails. The Grails Quickcast series is brought to you from OCI and DZone.
Grails leverages the “Convention Over Configuration” design paradigm, which functions to decrease the number of decisions that a developer using the framework is required to make without losing flexibility. This is one of the main reasons why Grails significantly increases developer productivity!
While Grails applications often involve considerably less configuration than similar applications built with other frameworks, some configuration may still be necessary. In this short video, Jeff shares a number of mechanisms that make it easy for Grails developers to define configuration values, and to gain access to those configuration values at runtime.
''',
authors: ['Jeff Scott Brown']).save()
new Quickcast(title: 'Developing Grails Application with IntelliJ IDEA - Grails 3',
subtitle: '#6 - Grails Quickcast from OCI',
heroImg: 'http://localhost:8888/quickcast_developinggrailsappswithintellij.png',
videoUrl: 'http://localhost:8888/grails_quickcast_2_jsonviews.mp4',
durationMinutes: 22,
durationSeconds: 42,
releaseYear: 2016,
description: '''
In the sixth Grails QuickCast, Grails co-founder, Jeff Scott Brown, introduces several tips and tricks related to building Grails 3 applications in IDEA. The Grails Quickcast series is brought to you from OCI and DZone.
Grails 3 is a high productivity framework for building web applications for the JVM. IntelliJ IDEA is a high productivity Integrated Development Environment (IDE) for building a variety of types of applications. IDEA has always had really great support for building Grails applications and, in particular, has the very best support of any IDE for doing development with Grails 3.
''',
authors: ['Jeff Scott Brown']).save()
}
def destroy = {
}
}
4.4 堆栈模板
首先展示一个 Apple TVML 堆栈模板 和 Grails Quickcasts
创建一个 Grails 控制器 来处理请求
控制器实现了模型视图控制器 (MVC) 模式中的 C,负责处理 Web 请求。在 Grails 中,控制器是一个以“Controller”约定的名称结尾并且位于 grails-app/controllers 目录中的类。
./grailsw create-controller com.ociweb.quickcasts.Quickcast
对 http://localhost:8080/quickcast 的请求对 QuickcastController
执行 index
操作
package com.ociweb.quickcasts
class QuickcastController {
static responseFormats = ['xml']
def index() {
[quickcasts: Quickcast.findAll()]
}
}
QuickcastController
索引方法使用下一个 标记视图 渲染每个 Quickcast
标记视图允许使用 Groovy’s MarkupTemplateEngine 渲染 XML 响应
import com.ociweb.quickcasts.Quickcast
model {
Iterable<Quickcast> quickcasts
}
document {
stackTemplate {
banner {
title this.g.message(code: 'quickcasts.title')
}
collectionList {
shelf {
section {
quickcasts.each { quickcast ->
lockup(onselect: "getDocument('quickcast/${quickcast.id}')") {
img(src: quickcast.heroImg, width: 150, height: 226)
title quickcast.title
}
}
}
}
}
}
}
标记视图以 Groovy 编写,以 gml
文件扩展名结尾并且驻留在 grails-app/views
目录中。
编写一个 功能测试 来检查生成的 XML 是否符合预期。
package com.ociweb.quickcasts
import grails.plugins.rest.client.RestBuilder
import grails.testing.mixin.integration.Integration
import grails.gorm.transactions.*
import groovy.xml.XmlUtil
import org.custommonkey.xmlunit.XMLUnit
import spock.lang.Specification
import org.springframework.beans.factory.annotation.Value
@Rollback
@Integration
class QuickcastControllerIndexSpec extends Specification {
def "test stack template is rendered correctly"() {
given:
def expected = '''<document>
<stackTemplate>
<banner>
<title>Grails Quickcasts</title>
</banner>
<collectionList>
<shelf>
<section>
<lockup onselect="getDocument('quickcast/1')">
<img src="http://localhost:8888/quickcast_interceptor.png" width="150" height="226"/>
<title>Interceptors - Grails 3</title>
</lockup>
<lockup onselect="getDocument('quickcast/2')">
<img src="http://localhost:8888/quickcast_jsonviews.png" width="150" height="226"/>
<title>JSON Views - Grails 3</title>
</lockup>
<lockup onselect="getDocument('quickcast/3')">
<img src="http://localhost:8888/quickcast_multiprojectbuilds.png" width="150" height="226"/>
<title>Multi-Project Builds - Grails 3</title>
</lockup>
<lockup onselect="getDocument('quickcast/4')">
<img src="http://localhost:8888/quickcast_angularscaffolding.png" width="150" height="226"/>
<title>Angular Scaffolding - Grails 3</title>
</lockup>
<lockup onselect="getDocument('quickcast/5')">
<img src="http://localhost:8888/quickcast_retrievingruntimeconfigvalues.png" width="150" height="226"/>
<title>Retrieving Runtime Config Values - Grails 3</title>
</lockup>
<lockup onselect="getDocument('quickcast/6')">
<img src="http://localhost:8888/quickcast_developinggrailsappswithintellij.png" width="150" height="226"/>
<title>Developing Grails Application with IntelliJ IDEA - Grails 3</title>
</lockup>
</section>
</shelf>
</collectionList>
</stackTemplate>
</document>
'''
when:
RestBuilder rest = new RestBuilder()
def resp = rest.get("http://localhost:${serverPort}/quickcast")
XMLUnit.setIgnoreWhitespace(true)
XMLUnit.setNormalizeWhitespace(true)
then:
resp.status == 200
XMLUnit.compareXML(XmlUtil.serialize(resp.xml), expected).identical()
}
}
为了在测试中便捷地比较 XML,我们包含 XMLUnit
作为依赖。
testCompile "org.grails:grails-web-testing-support"
4.5 产品模板
当我们选择第一个 Quickcast 时,驻留在 http://localhost:8080/quickast/1
中的文档会被推送到导航堆栈中
为了渲染 Quickcast,我们使用 Apple TVML 产品模板。
对 http://localhost:8080/quickcast/1
的请求对 QuickcastController
执行 show
操作。
package com.ociweb.quickcasts
class QuickcastController {
static responseFormats = ['xml']
def relatedQuickcastsService
def show() {
def quickcast = Quickcast.get(params.id)
def relatedQuickcasts = relatedQuickcastsService.findAllRelatedQuickcasts(quickcast)
[quickcast: quickcast, relatedQuickcasts: relatedQuickcasts]
}
传递给我们的视图的模型是请求的 Quickcast 和一系列相关的视频。
在 application.yml
中,我们配置要显示的相关 Quickcast 数量。
---
ociweb:
quickcasts:
numberOfRelatedQuickcasts: 3
---
提取相关 quickcast 是在服务中封装的。
package com.ociweb.quickcasts
import grails.config.Config
import grails.core.support.GrailsConfigurationAware
import grails.transaction.Transactional
@Transactional
class RelatedQuickcastsService implements GrailsConfigurationAware {
int numberOfRelatedQuickcasts
@Override
void setConfiguration(Config co) {
numberOfRelatedQuickcasts = co.getRequiredProperty('ociweb.quickcasts.numberOfRelatedQuickcasts', Integer)
}
List<Quickcast> findAllRelatedQuickcasts(Quickcast quickcast) {
def criteria = Quickcast.createCriteria()
criteria.list {
ne('id', quickcast?.id)
maxResults(numberOfRelatedQuickcasts)
} as List<Quickcast>
}
}
QuickcastController
索引方法使用下一个 标记视图 渲染 Quickcast
import com.ociweb.quickcasts.Quickcast
model {
Quickcast quickcast
Iterable<Quickcast> relatedQuickcasts
}
document {
productTemplate {
banner {
infoList {
info {
header {
title this.g.message(code: 'quickcast.authors.header')
}
quickcast.authors.each { author ->
text "$author"
}
}
}
stack {
title quickcast.title
subtitle quickcast.subtitle
row {
text "${quickcast.durationMinutes}min ${quickcast.durationSeconds}sec"
text quickcast.releaseYear
}
description(allowsZooming: "true", moreLabel: "more", "${quickcast.description}")
row {
buttonLockup(onselect: "playMedia('${quickcast.videoUrl}', 'video')") {
badge(src: "resource://button-play")
title "Play"
}
}
}
heroImg(src: quickcast.heroImg)
}
shelf {
header {
title this.g.message(code: 'quickcast.cross.sell.header')
}
section {
relatedQuickcasts.each { relatedQuickcast ->
lockup(onselect: "getDocument('quickcast/${quickcast.id}')") {
img(src: relatedQuickcast.heroImg, width: 150, height: 226)
title relatedQuickcast.title
}
}
}
}
}
}
通过一个功能测试,我们对渲染的 XML 进行检查以确定它是否符合预期。
package com.ociweb.quickcasts
import grails.plugins.rest.client.RestBuilder
import grails.testing.mixin.integration.Integration
import grails.gorm.transactions.*
import groovy.xml.XmlUtil
import org.custommonkey.xmlunit.XMLUnit
import spock.lang.Specification
import org.springframework.beans.factory.annotation.Value
@Rollback
@Integration
class QuickcastControllerShowSpec extends Specification {
def "test product template is rendered correctly"() {
given:
def expected = '''<document>
<productTemplate>
<banner>
<infoList>
<info>
<header>
<title>Authors</title>
</header>
<text>Jeff Scott Brown</text>
</info>
</infoList>
<stack>
<title>Interceptors - Grails 3</title>
<subtitle>#1 - Grails Quickcast from OCI</subtitle>
<row>
<text>17min 1sec</text>
<text>2016</text>
</row>
<description allowsZooming="true" moreLabel="more">This Quickcast assumes only basic familiarity with Groovy (which is pretty darn expressive anyway) and the MVC concept (which you already know). Also serves as an excellent introduction to the interceptor pattern in any language, because Grails' behind-the-scenes legwork lets you focus on the logic of the pattern.</description>
<row>
<buttonLockup onselect="playMedia('http://localhost:8888/grails_quickcast_1_interceptors.mp4', 'video')">
<badge src="resource://button-play"/>
<title>Play</title>
</buttonLockup>
</row>
</stack>
<heroImg src="http://localhost:8888/quickcast_interceptor.png"/>
</banner>
<shelf>
<header>
<title>Viewers also Watched</title>
</header>
<section>
<lockup onselect="getDocument('quickcast/1')">
<img src="http://localhost:8888/quickcast_jsonviews.png" width="150" height="226"/>
<title>JSON Views - Grails 3</title>
</lockup>
<lockup onselect="getDocument('quickcast/1')">
<img src="http://localhost:8888/quickcast_multiprojectbuilds.png" width="150" height="226"/>
<title>Multi-Project Builds - Grails 3</title>
</lockup>
<lockup onselect="getDocument('quickcast/1')">
<img src="http://localhost:8888/quickcast_angularscaffolding.png" width="150" height="226"/>
<title>Angular Scaffolding - Grails 3</title>
</lockup>
</section>
</shelf>
</productTemplate>
</document>
'''
when:
RestBuilder rest = new RestBuilder()
def resp = rest.get("http://localhost:${serverPort}/quickcast/1")
XMLUnit.setIgnoreWhitespace(true)
XMLUnit.setNormalizeWhitespace(true)
then:
resp.status == 200
XMLUnit.compareXML(XmlUtil.serialize(resp.xml), expected).identical()
}
}
4.6 国际化
国际化 是 Grails 中的一等公民。
Grails 通过利用底层的 Spring MVC 国际化支持来开箱即用地支持国际化 (i18n)。使用 Grails,你能够 根据用户的语言环境自定义显示在视图中的文本。
为了渲染这样的代码段
<title>Authors</title>
</header>
我们使用以下代码
header {
title this.g.message(code: 'quickcast.authors.header')
}
在 messages.properties
中定义消息代码和默认值
resouce.button.play=Play
quickcast.authors.header=Authors
quickcast.cross.sell.header=Viewers also Watched
quickcasts.title=Grails Quickcasts
添加西班牙语本地化十分容易。在 messages_es.properties
中添加本地化消息代码。
resouce.button.play=Reproducir
quickcast.authors.header=Autores
quickcast.cross.sell.header=Otros usuarios también vieron
quickcasts.title=Grails Quickcasts
5 运行应用程序
要运行 Grails 应用程序,可以使用 ./gradlew bootRun
命令,该命令将在端口 8080 上启动应用程序。
运行 Xcode 项目,你的 Apple TvOS 应用将连接到你的 Grails 后端。