Taobao FED

使用 JavaScript 开发原生 tvOS 应用

使用 JavaScript 开发原生 tvOS 应用

前言

Apple 于今年秋季发布了新版的 Apple TV,也带来了 iOS 开发者一直期盼的全新电视操作系统 — tvOS,正如 iPhone 的成功,Apple 从根本上就坚信基于应用的电视体验才是未来。tvOS 脱胎于 iOS,但又是一个完全独立的操作系统,拥有独立的 App Store。

官方提供了两种解决方案开发 tvOS 应用:

  • Traditional Apps: 使用原有的 iOS Framework 开发,开发出的 App 可以同时兼容 iOS 设备和 Apple TV
  • Client-Server Apps: 面向 Web 开发者的新解决方案,使用 JavaScript 和 TVML 编写 Native 应用

本文将介绍如何使用 TVML 和 TVJS 开发 一个 Client/Server App。

环境准备

  • 开发 tvOS 应用需要 Xcode 7.1 及以上版本,下载页面
  • Midway,本教程使用淘宝的 Node.js 框架 Midway 生成动态的 TVML 模板。(如果你对 Midway 或者 Node.js 不熟悉,也可以使用其他 Server 技术,只要能够在特定路由生成对应的 XML 模板和 main.js 就可以了)

SDK 介绍

先介绍 SDK 的组成:

  • TVML: Apple’s Television Markup Language,基本上是一些 XML 语句,用于布局界面,布局界面时,我们会用到一些 Apple 提供的 TVML 模板创建我们的 UI,然后用 TVJS 写交互脚本
  • TVJS: 一系列 JavaScript API,通过它你可以展示 TVML,控制应用流程
  • TVMLKit: C/S 应用的容器,原生 SDK,实现 JavaScript 和 Native 的 Bridge

下图是 C/S App 的应用架构:

 C/S App 结构图

  • 所有界面和逻辑代码都可以放在 Web Server 上,客户端只需要提供容器
  • 每一个界面只需要提供一个 TVML 文件,App 中的 TVMLKit 框架负责解析并生成 Native 界面

让我们开始吧~

准备 Web Server

进入工作目录,先初始化一个 Midway 项目:

midway init //选择经典的 `Midway(koa) + BDO + Render + Security` 即可
// ... 等待依赖安装完成
midway start // 启动应用

打开 http://localhost:6001/,如果显示 Midway 欢迎页面就是说明 Server 环境 ok 啦。

我们需要做一些定制,主要用来请求远程数据和创建 TVML 模板。

  • 安装 npm 依赖包

    1
    tnpm i koa-jade koa-static --save
  • 打开 app.js,替换为如下代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    'use strict';
    var midway = require('midway'),
    koa = require('koa'),
    serve = require('koa-static'),
    Jade = require('koa-jade');

    var app = midway(
    koa()
    );

    // 使用 jade 模板引擎生成 xml 内容
    var jade = new Jade({
    viewPath: __dirname + '/app/views/',
    debug: false,
    noCache: true,
    debug:true
    })
    app.use(jade.middleware);

    // static,存储 app 需要的启动 JS
    app.use(serve(__dirname + '/static'));

    module.exports = app;
  • 删除 app/views 下的所有文件和文件夹,新建一个名为 hello.jade 的文件,输入以下内容

    1
    2
    3
    4
    document
    alertTemplate
    title hello world
    description first tvOS App with TVML and Midway
  • app 目录下新建 static 文件夹,新建一个 main.js 文件,输入以下内容

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    /* tvjs 启动文件 */

    // app 启动回调
    App.onLaunch = function() {
    getDocument('http://localhost:6001/', function(error, doc) {
    navigationDocument.pushDocument(doc);
    });
    }

    // 获取 xml doc
    function getDocument(url, callback) {
    callback = callback || function() {};

    var templateXHR = new XMLHttpRequest();
    templateXHR.responseType = "document";
    templateXHR.addEventListener("load", function() {
    callback(null, templateXHR.responseXML);
    }, false);
    templateXHR.addEventListener("error", function(err) {
    callback(err);
    }, false);
    templateXHR.open("GET", url, true);
    templateXHR.send();
    return templateXHR;
    }
  • 打开 app/controllers/home-controller,替换为如下内容

    1
    2
    3
    4
    5
    6
    'use strict';

    exports.index = function* () {
    this.render('hello');
    this.type = 'text/plain';
    };
  • 重启 Midway

  • 打开 http://localhost:6001/main.js,你会看到刚刚创建的 main.js
  • 打开 http://localhost:6001/,你应该看到 jade 生成的 xml 模板

    1
    2
    3
    4
    5
    6
    <document>
    <alertTemplate>
    <title>hello world</title>
    <description>first tvOS App with TVML and Midway </description>
    </alertTemplate>
    </document>

好了,我们的服务端就搭建完成啦~

准备 Native 容器

  • 新建一个 tvOS Single View Application 项目:

  • 点击 next,输入项目名称为 demo,语言选择 Swift

  • 删除 Main.storyboardViewController.swift,选择 Move to Trash

  • 打开 Info.plist,删除 Main storyboard file base name 这一配置

  • iOS 9 默认不允许非 HTTPS 链接,需要在 Info.plist 里面添加配置。右击 Info.plist,选择 open as => SourceCode,在 dict child 中新增:

    1
    2
    3
    4
    5
    <key>NSAppTransportSecurity</key>
    <dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
    </dict>

配置
配置

  • 打开 AppDelegate.swift,替换为如下内容

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    import UIKit
    import TVMLKit

    @UIApplicationMain
    class AppDelegate: UIResponder, UIApplicationDelegate ,TVApplicationControllerDelegate{

    var window: UIWindow?

    var appController: TVApplicationController?;

    // 服务器地址
    static let TVBootURL = "http://localhost:6001/main.js";

    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    // 创建 tvmlkit 环境
    self.window = UIWindow(frame: UIScreen.mainScreen().bounds);

    let appControllerContext = TVApplicationControllerContext();

    if let javaScriptURL = NSURL(string: AppDelegate.TVBootURL) {
    appControllerContext.javaScriptApplicationURL = javaScriptURL;
    }

    appController = TVApplicationController(context: appControllerContext, window: window, delegate: self);

    return true
    }
    }
  • CTRL + R 启动 App,你会看到如下界面

    hello tvos

  • 哇哈哈,配置这么长时间,成功走到这一步,必须要:

    哇咔咔

应用流

  • 从代码可以看到,App 容器只需要指定一个 JS 文件地址(我们新建的 main.js ),而在 JS 文件中请求了一个 XML 模板,并添加到 navigationStack 中,界面就生成了。先介绍代码中用到的 TVJS 对象。

    • App: TVJS 提供的全局对象,用来管理应用生命周期,当应用启动的时候,会触发 App 的 onLaunch 事件
    • navigationDocument: NavigationDocument 实例,NavigationDocument 用来控制应用中的页面栈。应用生命周期中只有一个全局的 navigationDocument 实例
  • 下图展示了一个 C/S App 的生命周期流程
    生命周期

TVML

  • TVML 用来绘制每一个页面,App 页面栈中的每个页面都是一个 TVML 文件生成的 Docuemnt DOM
  • TVML 本质上就是 XML,Apple 官方定义了一些用于绘制界面的 XML Element 和 Template,你必须使用这些 Template 和元素搭建页面
  • 每个 Templete 代表了一种布局,如表单、列表、多维数据等,每个 TVML 文件只能使用一个 Templete
  • 下图表示了 TVML,TVML Templete,TVML Element 的关系
    tvml
  • 下图是官方提供的 catalogTemplate,用于展示二维数据
    tvml template

  • 官方一共提供了近 20 种模板和上百个标签元素(地址),已经能够满足绝大部分需求,你还可以用 Swift 和 TVMLKit 实现自定义的标签。

WWDC 视频合集

我们了解了 C/S App 的大致工作原理后,就可以开发更复杂的应用啦。这里我们实现一个历届 WWDC 视频播放的 App。

列表

  • 首先需要实现一个视频列表界面,这里可以使用前面介绍的 catalogTemplate
  • 打开 app/controllers/home-controller.js,替换成如下代码,添加一些假数据(你也可以直接从官方页面抓取数据)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    'use strict';

    // 模拟的 wwdc 数据
    var videoData = [{
    title: 'wwdc 2015',
    desc:'wwdc 2015 年的学习视频',
    data: [{
    img: 'http://devstreaming.apple.com/videos/wwdc/2015/1026npwuy2crj2xyuq11/102/images/102_734x413.jpg',
    title: 'Platforms State of the Union',
    video: 'http://devstreaming.apple.com/videos/wwdc/2015/1026npwuy2crj2xyuq11/102/hls_vod_mvp.m3u8'
    }, {
    img: 'http://devstreaming.apple.com/videos/wwdc/2015/105ncyldc6ofunvsgtan/105/images/105_734x413.jpg',
    title: 'Platforms State of the Union',
    video: 'http://devstreaming.apple.com/videos/wwdc/2015/1026npwuy2crj2xyuq11/102/hls_vod_mvp.m3u8'
    }, {
    img: 'http://devstreaming.apple.com/videos/wwdc/2015/106z3yjwpfymnauri96m/106/images/106_734x413.jpg',
    title: 'Platforms State of the Union',
    video: 'http://devstreaming.apple.com/videos/wwdc/2015/1026npwuy2crj2xyuq11/102/hls_vod_mvp.m3u8'
    }, {
    img: 'http://devstreaming.apple.com/videos/wwdc/2015/104usewvb5m0qbwafx8p/104/images/104_734x413.jpg',
    title: 'Platforms State of the Union',
    video: 'http://devstreaming.apple.com/videos/wwdc/2015/1026npwuy2crj2xyuq11/102/hls_vod_mvp.m3u8'
    }, {
    img: 'http://devstreaming.apple.com/videos/wwdc/2015/709jcaer6su/709/images/709_734x413.jpg',
    title: 'Platforms State of the Union',
    video: 'http://devstreaming.apple.com/videos/wwdc/2015/1026npwuy2crj2xyuq11/102/hls_vod_mvp.m3u8'
    }, {
    img: 'http://devstreaming.apple.com/videos/wwdc/2015/1026npwuy2crj2xyuq11/102/images/102_734x413.jpg',
    title: 'Platforms State of the Union',
    video: 'http://devstreaming.apple.com/videos/wwdc/2015/1026npwuy2crj2xyuq11/102/hls_vod_mvp.m3u8'
    }, {
    img: 'http://devstreaming.apple.com/videos/wwdc/2015/212mm5ra3oau66/212/images/212_734x413.jpg',
    title: 'Platforms State of the Union',
    video: 'http://devstreaming.apple.com/videos/wwdc/2015/1026npwuy2crj2xyuq11/102/hls_vod_mvp.m3u8'
    }, {
    img: 'http://devstreaming.apple.com/videos/wwdc/2015/106z3yjwpfymnauri96m/106/images/106_734x413.jpg',
    title: 'Platforms State of the Union',
    video: 'http://devstreaming.apple.com/videos/wwdc/2015/1026npwuy2crj2xyuq11/102/hls_vod_mvp.m3u8'
    }]
    },{
    title: 'wwdc 2014',
    desc:'wwdc 2014 年的学习视频',
    data: [{
    img: 'http://devstreaming.apple.com/videos/wwdc/2015/1026npwuy2crj2xyuq11/102/images/102_734x413.jpg',
    title: 'Platforms State of the Union',
    video: 'http://devstreaming.apple.com/videos/wwdc/2015/1026npwuy2crj2xyuq11/102/hls_vod_mvp.m3u8'
    }]
    },{
    title: 'wwdc 2013',
    desc:'wwdc 2013 年的学习视频',
    data: [{
    img: 'http://devstreaming.apple.com/videos/wwdc/2015/1026npwuy2crj2xyuq11/102/images/102_734x413.jpg',
    title: 'Platforms State of the Union',
    video: 'http://devstreaming.apple.com/videos/wwdc/2015/1026npwuy2crj2xyuq11/102/hls_vod_mvp.m3u8'
    }]
    }];

    exports.index = function*() {
    this.render('list', {
    data:videoData
    });
    this.type = 'text/plain';
    };
  • 在 views 目录中新增 list.jade 模板,内容如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <?xml version="1.0" encoding="UTF-8" ?>
    document
    catalogTemplate
    banner
    title 历届 wwdc 视频
    list
    section
    each item in data
    listItemLockup
    title #{item.title}
    decorationLabel
    relatedContent
    grid
    section
    each video in item.data
    lockup(data-video-url="#{video.video}")
    img(src="#{video.img}", width="350" , height="250")
    title #{video.title}
  • 重启 Midway
  • 重新运行 App,哇咔咔,列表这就出来了
    wwdc

播放视频

接下来就可以继续实现播放视频功能了,首先让我们实现 cell 的点击事件。

在 main.js 中添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
App.onLaunch = function() {
getDocument('http://localhost:6001/', function(error, doc) {
navigationDocument.pushDocument(doc);

// 添加下面的内容
// 点击事件
doc.addEventListener('select', function(event) {
var ele = event.target;
// 获取视频地址
var videoURL = ele.getAttribute('data-video-url');
if (videoURL) {
var player = new Player();
var playlist = new Playlist();
var mediaItem = new MediaItem("video", videoURL);

player.playlist = playlist;
player.playlist.push(mediaItem);
player.present();
};
})
});
}

再次重启 Midway,重启应用,点击视频,哇咔咔,done

  • TVML 的事件处理就是 标准的 DOM 事件 ,我们可以使用 DOM 的 API 添加元素的点击事件,获取元素的属性等(视频地址在元素的 data-video-url 属性上)
  • TVJS 提供了完善的 API 播放视频,可以参考官方 PlayerPlayerList 的 API
  • 我们的 App 就完成了,效果如下

结尾

C/S App 技术 是 Apple 第一次在自己的类 iOS 系统中推出的 使用 web 技术开发 native 应用 的解决方案。我们也期待 Apple 早日将这一技术带入 iPhone 和 iPad,正如 React Native 一样,这样的技术将会给我们的业务开发带来巨大的变革。本文只是做了一些基础的讲解和开发框架探索,更深入的学习可以参考 Apple 的官方文档: