使用 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 的应用架构:

  • 所有界面和逻辑代码都可以放在 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 依赖包
    tnpm i koa-jade koa-static --save
  • 打开 app.js,替换为如下代码
    '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 的文件,输入以下内容
    document
    	alertTemplate
    		title hello world
    		description first tvOS App with TVML and Midway
  • app 目录下新建 static 文件夹,新建一个 main.js 文件,输入以下内容
    /* 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,替换为如下内容
      'use strict';
    
      exports.index = function* () {
      	this.render('hello');
      	this.type = 'text/plain';
      };
  • 重启 Midway
  • 打开 http://localhost:6001/,你应该看到 jade 生成的 xml 模板
      <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 中新增:
      <key>NSAppTransportSecurity</key>
      <dict>
      	<key>NSAllowsArbitraryLoads</key>
      	<true/>
      </dict>


  • 打开 AppDelegate.swift,替换为如下内容
      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,你会看到如下界面
  • 哇哈哈,配置这么长时间,成功走到这一步,必须要:

应用流

  • 从代码可以看到,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 的关系
  • 下图是官方提供的 catalogTemplate,用于展示二维数据
  • 官方一共提供了近 20 种模板和上百个标签元素(地址),已经能够满足绝大部分需求,你还可以用 Swift 和 TVMLKit 实现自定义的标签。

WWDC 视频合集

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

列表

  • 首先需要实现一个视频列表界面,这里可以使用前面介绍的 catalogTemplate
  • 打开 app/controllers/home-controller.js,替换成如下代码,添加一些假数据(你也可以直接从官方页面抓取数据)
      '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 模板,内容如下
      <?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,哇咔咔,列表这就出来了

播放视频

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

在 main.js 中添加

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 的官方文档: