使用 Grails 构建 TVML 应用程序
利用 Grails 的标记视图、资产流水线和国际化功能,使用 Grails 为 Apple TV 构建应用程序,简化 TVML 开发
作者:Sergio del Amo
Grails 版本 4.0.1
1 Grails 培训
Grails 培训 - 由创建和主动维护 Grails 框架的人员开发和提供!。
2 入门
在本指南中,您将编写一个 TVML 应用程序。
使用 TVML 应用程序可以利用 TVMLKit JavaScript (TVMLKit JS)、Apple TV 标记语言 (TVML) 和 TVMLKit 框架为 Apple TV 创建应用程序。每个模板都有一个遵循 Apple 样式指南的特定设计,使您可以快速创建美观的应用程序。
在此指南中构建的 TVML 应用程序会显示 Groovy Quickcast 的 Apple TVML 栈模板。Groovy Quickcast 是纯粹的 Groovy 编码的简短视频。当用户选择一项时,Apple TVML 产品模板 会显示 Quickcast 详细信息。用户可以通过选择播放按钮来播放视频。
2.1 架构
本指南包含一个客户端-服务器架构。客户端是 tvOS 应用程序。服务器是 Grails 应用程序。客户端连接到服务器,服务器使用 TVMLKit JS 和 TVML 文档进行响应。
我们将 Grails 应用程序与数据(mp4 文件和 Png 文件)分开了;视频和缩略图。我们使用 MAMP 在端口 8888 运行指向文件夹数据的本地 Apache 服务器。在本文档中,您会看到对 mp4 文件的引用,例如
heroImg: 'http://localhost:8888/quickcast_interceptor.png',
在生成环境中,您可能希望在 Amazon Simple Storage Service(AWS S3)等服务中托管这些文件 Amazon Simple Store Service – AWS S3
2.2 您需要准备的内容
若要完成本指南,您将需要以下内容
-
一些时间
-
一个合适的文本编辑器或 IDE
-
安装了 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 Application Forge生成您的 Grails 项目。您可以选择项目类型(应用程序或插件)、选择 Grails 版本并选择一个配置文件,然后单击“生成项目”以下载一个 ZIP 文件。无需安装 Grails! 您甚至可以用 HTTP 工具(例如
|
4.1 TVMLKit JS 辅助函数
TVMLKit JS 是专门设计为与 Apple TV 和 TVML 配合使用的 JavaScript API 框架。TVMLKit JS 融入了 JavaScript 中的基本功能以及专门针对 Apple TV 设计的专用 API。使用 TVMLKit JS,您能够加载和显示 TVML 模板、播放媒体流、加载自定义内容以及控制应用流程。如需了解更多信息,请参阅 TVJS 框架参考。
将一个 JavaScript 文件添加到我们的项目中。它定义一系列方法,这些方法与 TVML 编程指南 中“播放媒体”部分中描述的方法几乎相同。
这些方法允许将页面推入导航堆栈、提取文档并播放媒体。
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 是一个用于管理和处理 JVM 应用程序中静态资产的插件,主要通过 Gradle(但不是必需的)。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 域类
创建一个持久性实体以存储 Quickcast。在 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'])
}
}
创建一个 GORM 数据服务
package com.ociweb.quickcasts
import grails.gorm.services.Service
@Service(Quickcast)
interface QuickcastService {
List<Quickcast> findAll()
Quickcast findById(Long id)
Quickcast save(Quickcast quickcast)
}
然后我们在 BootStrap.groovy
中加载一些测试数据。
package com.ociweb.quickcasts
import groovy.transform.CompileStatic
@CompileStatic
class BootStrap {
QuickcastService quickcastService
def init = { servletContext ->
quickcastService.save(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'] as Set<String>,
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.'
))
quickcastService.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'] as Set<String>))
quickcastService.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'] as Set<String>))
quickcastService.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'] as Set<String>))
quickcastService.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'] as Set<String>))
quickcastService.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'] as Set<String>))
}
def destroy = {
}
}
4.4 堆栈模板
我们首先展示一个包含深入浅出的 Grails 的Apple TVML 堆栈模板
创建一个Grails 控制器来处理请求
控制器实现了 Model View Controller (MVC) 模式中的 C,负责处理网络请求。在 Grails 中,控制器是类,其名称以“Controller”结尾且位于 grails-app/controllers 目录中。
./grailsw create-controller com.ociweb.quickcasts.Quickcast
发送至http://localhost:8080/quickcast 的请求将执行 QuickcastController
的 index
操作
package com.ociweb.quickcasts
import groovy.transform.CompileStatic
@CompileStatic
class QuickcastController {
QuickcastService quickcastService
static responseFormats = ['xml']
RelatedQuickcastsService relatedQuickcastsService
def index() {
[quickcasts: quickcastService.findAll()]
}
def show(Long id) {
Quickcast quickcast = quickcastService.findById(id)
[
quickcast: quickcast,
relatedQuickcasts: relatedQuickcastsService.findAllRelatedQuickcasts(quickcast)
]
}
}
QuickcastController
索引方法使用下一个标记视图来呈现每个 Quickcast
标记视图允许使用Groovy 的 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 是否符合预期。
添加 Micronaut HTTP 客户端依赖
testCompile "io.micronaut:micronaut-http-client"
package com.ociweb.quickcasts
import grails.testing.mixin.integration.Integration
import grails.gorm.transactions.*
import grails.testing.spock.OnceBefore
import groovy.xml.XmlUtil
import io.micronaut.http.client.HttpClient
import org.custommonkey.xmlunit.XMLUnit
import spock.lang.Shared
import spock.lang.Specification
import org.springframework.beans.factory.annotation.Value
@Rollback
@Integration
class QuickcastControllerIndexSpec extends Specification {
@Shared HttpClient client
@OnceBefore
void init() {
String baseUrl = "http://localhost:$serverPort"
this.client = HttpClient.create(baseUrl.toURL())
}
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:
String xml = client.toBlocking().retrieve('/quickcast')
XMLUnit.setIgnoreWhitespace(true)
XMLUnit.setNormalizeWhitespace(true)
then:
XMLUnit.compareXML(XmlUtil.serialize(xml), expected).identical()
}
}
为了简化测试中的 XML 比较,我们包括 XMLUnit
作为依赖关系。
testCompile "xmlunit:xmlunit:1.6"
4.5 产品模板
当我们选择第一个速成视频时,位于 http://localhost:8080/quickast/1
中的文档将被推送到导航堆栈
为了呈现一个速成视频,我们使用Apple TVML 产品模板。
发送至 http://localhost:8080/quickcast/1
的请求将执行 QuickcastController
的 show
操作。
package com.ociweb.quickcasts
import groovy.transform.CompileStatic
@CompileStatic
class QuickcastController {
QuickcastService quickcastService
static responseFormats = ['xml']
RelatedQuickcastsService relatedQuickcastsService
def index() {
[quickcasts: quickcastService.findAll()]
}
def show(Long id) {
Quickcast quickcast = quickcastService.findById(id)
[
quickcast: quickcast,
relatedQuickcasts: relatedQuickcastsService.findAllRelatedQuickcasts(quickcast)
]
}
}
传递给我们的视图的模型是请求的速成视频和一系列相关视频。
在 application.yml
中,我们配置要显示的速成视频的数量。
ociweb:
quickcasts:
numberOfRelatedQuickcasts: 3
在服务中封装了相关速成视频的获取。
package com.ociweb.quickcasts
import grails.config.Config
import grails.core.support.GrailsConfigurationAware
import grails.gorm.transactions.ReadOnly
class RelatedQuickcastsService implements GrailsConfigurationAware {
int numberOfRelatedQuickcasts
@Override
void setConfiguration(Config co) {
numberOfRelatedQuickcasts = co.getRequiredProperty('ociweb.quickcasts.numberOfRelatedQuickcasts', Integer)
}
@ReadOnly
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.testing.mixin.integration.Integration
import grails.gorm.transactions.*
import grails.testing.spock.OnceBefore
import groovy.xml.XmlUtil
import io.micronaut.http.client.HttpClient
import org.custommonkey.xmlunit.XMLUnit
import spock.lang.Shared
import spock.lang.Specification
@Rollback
@Integration
class QuickcastControllerShowSpec extends Specification {
@Shared HttpClient client
@OnceBefore
void init() {
String baseUrl = "http://localhost:$serverPort"
this.client = HttpClient.create(baseUrl.toURL())
}
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:
String xml = client.toBlocking().retrieve('/quickcast/1')
XMLUnit.setIgnoreWhitespace(true)
XMLUnit.setNormalizeWhitespace(true)
then:
XMLUnit.compareXML(XmlUtil.serialize(xml), expected).identical()
}
}
4.6 国际化
国际化是 Grails 的一等公民。
Grails 通过利用底层的 Spring MVC 国际化支持,开箱即用地支持国际化 (i18n)。使用 Grails,你可以根据用户的语言环境自定义视图中显示的文本。
要呈现此类代码段
given:
def expected = '''<document>
我们使用此代码
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 后端。