说三道四技术文摘-感悟人生的经典句子
说三道四 > 文档快照

Apple TV指南:从零开始用Swift开发个tvOS应用

HTML文档下载 WORD文档下载 PDF文档下载
这篇文章主要介绍了如何在Apple TV上开发一个名为tvOS的App,以及关于tvOS的一些相关的定义,最终呈现给用户一个非常有趣的界面。

本文出自:Jameson Quave's Blog,作者:Jameson Quave,译文出自:SwiftGG,译者:ray16897188

 

教程结束时,我们会做出这样一个应用:


你更喜欢哪个艺术家?

第一部分

开始

在我们开始之前你需要安装 Xcode 7.1 beta 版,可以从这里下载:下载 Xcode 7.1 Beta。

注意:下载 Xcode 7.1 Beta需要有一个苹果开发者账号,由于目前 Xcode 是预发行版,以后正式发布的时候可能会有变化。
安装的时候要注意,如果你重命名Xcode7.1应用,会遇到一个已知Bug。一定有人会这么做,所以提前说明……别这么做,否则你的tvOS模拟器会崩溃。

同时要注意,虽然支持优胜美地 (Yosemite),但是在该操作系统上,功能会受限。推荐用 OSX 10.11 El Capitan 或更新的系统。ElCapitan beta可以在这里下载。

下面我们来介绍一些 tvOS 相关的定义。

TVMLKit

TVMLKit 是 Apple 设计的一个新框架,能在使用 Swift 或 Objective-C 实现应用逻辑的同时使用 Javascript 和 XML 开发更炫酷的用户界面。

TVML

TVML 是“TV Markup Language”(TV 标记语言)的缩写,基本上是一些 XML 语句,用于实现基于C/S(client-server,客户端-服务端)架构的 tvOS 应用布局。布局界面时,我们会用到一些 Apple 提供的 TVML 模板创建我们的 UI,然后用 TVJS 写交互脚本。

TVJS

我能告诉你的是,TVJS 就是你(可能已经)熟悉的 JavaScript。

Hello World

我们从一个基本的 hello world 程序开始。就 Apple TV 而言,我们可以只把"Hello World"输出到控制台上。这也许是个不错的开始,但更好的选择是使用 Apple TV 的一些 TVMLKit 元素在屏幕上创建一个模板。

首先,打开 Xcode 7.1 并创建一个新项目。你可以看到一个模板列表,我们在左侧选择CHANGE tvOS,然后再选Single View Application模板。

这样就会根据 tvOS 模板创建一些默认文件和一个简单的 Swift 入口点,对一会儿创建 UI 很有帮助。

建立 TVJS 主文件

在 C/S 架构的 tvOS 应用中,服务端本质上就是 TVML 和 JavaScript 文件以及和它们相关的所有数据。JavaScript 文件会装载 TVML 并把页面(page)放入视图栈中。可以从另一个角度理解:JavaScript 文件就像 TVML 文件的路由器或是控制器(controller),而 TVML 文件本质上是若干视图(views)。

拉开序幕

首先我们要修改应用的AppDelegate.swift文件。第一步是让我们的应用遵循TVApplicationControllerDelegate协议。该协议定义在 TVMLKit 框架中,所以需要导入它。更新AppDelegate.swift文件,如下所示:

import TVMLKitclass AppDelegate: UIResponder,UIApplicationDelegate,TVApplicationControllerDelegate {....
此协议包含四个 tvOS 实现AppDelegate后会调用的函数,用于给我们的应用发送 tvOS 生命周期通知。现在我们无需操心这些,但在后面的教程中我们会对它们进行深入研究。目前只要像上面的代码那样把协议加进去就够了。

下一步,我们要添加一些代码,让 JS 文件起作用。由于是 beta 版,我们还需要自己完成这些工作。我相信在 Xcode 的后续版本中这一步会变成一个模板。

在程序里didFinishLaunchingWithOptions这个函数中我们要完成一些步骤。它们对所有应用来说都是一样的,所以你可以直接复制这段代码:


// 在一个可选属性中保存对 appController 的引用var appController: TVApplicationController?func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {  self.window = UIWindow(frame:UIScreen.mainScreen().bounds)  let appControllerContext = TVApplicationControllerContext()  let jsFilePath = NSURL(string: "http://localhost:8000/main.js")  let javascriptURL = jsFilePath!  appControllerContext.javaScriptApplicationURL = javascriptURL  if let options = launchOptions  {    for (kind, value) in options    {      if let kindStr = kind as? String      {        appControllerContext.launchOptions[kindStr] = value      }    }  }  self.appController = TVApplicationController(context: appControllerContext, window: self.window, delegate: self)  return true}

简单说说这段代码干了什么:它拿到了一个TVApplicationControllerContext引用,这个Context只是为我们的AppDelegate类提供了一些启动数据,然后给了我们一个能调整和修改启动过程的接口。接着把URL 传给待会儿要运行的main.js文件,并将appController的路径设置成这个 URL。

现在就要添加我们的 JavaScript 文件了,点击 File > New,然后在 iOS tab 下面选择 Other >Empty file。将这个文件命名为main.js。

用同样方法创建一个hello.tvml文件。

在main.js文件中添加一些简单的 JavaScript 代码,用来装载hello.tvml文件:

function getDocument(url) {  var templateXHR = new XMLHttpRequest();  templateXHR.responseType = "document";  templateXHR.addEventListener("load", function() {pushDoc(templateXHR.responseXML);}, false);  templateXHR.open("GET", url, true);  templateXHR.send();  return templateXHR;}function pushDoc(document) {  navigationDocument.pushDocument(document);}App.onLaunch = function(options) {  var templateURL = 'http://localhost:8000/hello.tvml';  getDocument(templateURL);}App.onExit = function() {  console.log('App finished');}
现在在hello.tvml文件中添加:

<document>  <alertTemplate>      <title>Hello tvOS!</title>  </alertTemplate></document>

TVML 文件是 UI 的实际内容。文档(document)必须用模板编写,否则现在的代码运行时会崩溃。这个 TVML 文件只是包含了一个简单的模板和一个单元素的标题。

在编写这些代码时我发现一个问题:本地无法引用这些文件,文件必须放在一个 web 服务器上。所以最简单的解决方案是找到你刚创建 TVML 和JS 文件的位置,并在命令行中敲进如下指令:

启动服务端

python -m SimpleHTTPServer 8000

这条指令用 Mac OS 内建的 python 解释器开启了一个端口号为 8000 的 web 服务器,可以用它来托管本地文件。如果在命令行中,执行了上面给出的代码,那么现在按一下Xcode 的 play 按钮就能在 tvOS 模拟器中启动了。还有一个要注意的事情:这是一个不够安全的 HTTP 请求,在 iOS 9 中会被默认的应用传输安全机制拦截。为了能够按之前的方法来使用本地主机,我们需要在Info.plist文件中添加一个key。

允许直接加载(Allows Arbitrary Loads)

选择Info.plist文件然后按加号(+)来创建一条新记录。在列表中选择”App Transport Security Settings”并按 return 建。这将创建一个新的字典条目,展开它,在这行上按加号(+)添加一个子行。接着选中”Allows Arbitrary Loads”并将其设为true。都设好了之后我们就能用模拟器运行应用了。

添加按钮

在本例中你看到的实际上是一个被 Apple 称作alertTemplate的模板。你还能嵌入一些基本控件,比如在模板中添加文字和按钮。试着添加一些按钮吧:

<document>    <alertTemplate>        <title>Hello tvOS!</title>        <button>            <text>A Button</text>        </button>        <button>            <text>A Second Button</text>        </button>    </alertTemplate></document>

这里我们只加了子按钮(child button)元素,每个子按钮都有它自己的子文本(child text)元素。这段代码在 tvOS模拟器上全屏显示alert和两个按钮。如果你自学能力很强,苹果的官方文档中列出了你能使用的所有模板和控件。

第二部分

增加交互事件

在第一部分中我们创建了一个简单的 TVML document,里面有几个按钮。这个document看起来是这样的:

<document>	<alertTemplate>		<title>Hello tvOS!</title>		<button>			<text>A Button</text>		</button>		<button>			<text>A Second Button</text>		</button>	</alertTemplate></document>
这是一个带按钮的警告(alert)界面,目前这些按钮没有任何作用。这段代码直接硬编码了具体内容,更好的方式是使用代码生成XML,在 JS 中很容易实现。我们在main.js文件中添加一个新函数,把上面的代码封装成一个更简单的警告界面,它只包含一个 OK 按钮。

function alert(str) {	var alertXMLString = `<?xml version="1.0" encoding="UTF-8" ?>	<document>		<alertTemplate>			<title>Hey Listen!</title>			<description>${str}</description>			<button>				<text>OK</text>		</button>	</alertTemplate></document>`var parser = new DOMParser();var alertDOMElement = parser.parseFromString(alertXMLString, "application/xml");navigationDocument.presentModal(alertDOMElement);}
这里创建了一个alertXMLString字符串,它表示的是包含一个按钮的简单警告界面所对应的 TVML。description节点比较特殊,我们使用TVJS 的内嵌字符串语法${variable}来插入str的值。

接下来,创建一个新的DOMParser对象,把这个字符串转换成一个实际的 XML DOM 元素。

最后,我们用navigationDocument的presentModal方法展示一个模态框,内容就是上一步的 DOM 元素。navigationDocument是一个全局变量,它永远指向XML 文档的根节点。

现在,删除onLaunch函数中之前的代码,直接调用刚才创建的函数……

App.onLaunch = function(options) {	alert("Hello!");}


Hello警告

运行应用,你会看到一个炫酷的”Hello!” tvOS 警告。但是点击 OK 没有任何反应。我们该怎么处理像触摸之类的事件呢?

通常来说,在 JavaScript 和 TVML 的世界中,你需要给 DOM 元素添加一个事件监听器(event listener)。举个例子,我们可以给alert函数添加第二个参数,把 OK 按钮触发select事件时需要调用的函数作为参数传入。下面我们就加入这个名为doneCallback的参数:

alertDOMElement.addEventListener("select", function() { doneCallback }, false);
更新后的完整函数如下:

function alert(str, doneCallback) {	var alertXMLString = `<?xml version="1.0" encoding="UTF-8" ?>	<document>		<alertTemplate>			<title>Hey Listen!</title>			<description>${str}</description>			<button>				<text>OK</text>			</button>		</alertTemplate></document>`var parser = new DOMParser();var alertDOMElement = parser.parseFromString(alertXMLString, "application/xml");alertDOMElement.addEventListener("select", doneCallback, false);navigationDocument.presentModal(alertDOMElement);}

现在我们可以修改之前的onLaunch函数,添加一个回调函数来显示一个 TVML 页面。在此之前,我们需要再添加一个getDocumentContents函数,它会在页面加载完毕之后调用回调函数。这个回调函数只有一个参数,用来接收 XMLHttpRequest 对象的响应内容。这样我们就可以轻松地加载多种 TVML 文件。 

function getDocumentContents(url, loadCallback) {	var templateXHR = new XMLHttpRequest();	templateXHR.responseType = "document";	templateXHR.addEventListener("load", function() { loadCallback(templateXHR) }, false);	templateXHR.open("GET", url, true);	templateXHR.send();	return templateXHR;}
代码和之前定义的getDocument方法几乎一样,区别是这里是异步操作,而且不会在界面上显示任何内容。 

有个这个函数,我们就能执行下面的调用,当 OK 按钮被点击时替换屏幕上的警告内容。 

App.onLaunch = function(options) {    alert("Hello!", function() {      var helloDocument = getDocumentContents("http://localhost:8000/hello.tvml", function(xhr) {        navigationDocument.dismissModal();        navigationDocument.pushDocument(xhr.responseXML);      });    });}
我们使用stackTemplate模板来改写hello.tvml文件,这样界面会更有趣。stackTemplate非常适合用来展示一组包含标题和图片的列表内容。下面是本例用到的内容:

<document>    <stackTemplate>        <banner>            <title>Which Artist Do You Prefer?</title>        </banner>        <collectionList>            <shelf>                <section>                    <lockup>                        <img src="http://localhost:8000/nina.png" width="256" height="256" />                        <title>Nina Simone</title>                    </lockup>                    <lockup>                        <img src="http://localhost:8000/coltrane.png" width="256" height="256" />                        <title>John Coltrane</title>                    </lockup>                </section>            </shelf>        </collectionList>    </stackTemplate></document>

这基本上就是stackTemplate的布局方式,banner是顶部的横幅内容,collectionList包含许多shelf对象,而shelf对象则包含许多section对象,section对象又包含许多lockup对象,最后这个才真正包含我们的图片和标题。在本例中我向目录中添加了一些图片,它们是nina.png和coltrane.png。


预告:  2015中国移动开发者大会(MDCC 2015)将于10月14日-16日在北京新云南皇冠假日酒店召开。大会特设九大技术专场:平台与技术(iOS)、平台与技术(Android)、平台与技术(跨平台)、产品与设计、游戏开发、企业移动化、虚拟现实专场、硬件开发与技术、嵌入式开发。大会将聚集国内最具实力的产品技术团队,与开发者一道进行最前沿的探讨与交流。 

第一时间掌握最新移动开发相关信息和技术,请关注mobilehub公众微信号(ID: mobilehub)。

欲与Android展开价格战,微软宣布在移动设备上免费开放Windows 2014深圳英特尔信息技术峰会次日要点汇总 4月4日:微软39周岁,生日快乐! 正式进军硬件市场 Atlassian发布可穿戴设备Bitband 云计算时代IT专业人员需具备的10项技能 【走近院士】李德毅:大数据是连接虚拟和现实世界的桥梁 微软Build2014大会第二天干货总结 Microsoft Azure拥抱Puppet、Chef、MongoDB... 唏嘘不已!Brendan Eich上任Mozilla CEO仅十天即因歧视同性恋被迫辞职 最火HTML5 JavaScript游戏引擎:国外篇(一) Facebook发布PlanOut 开源部分A/B测试源码 一周消息树:6款Java转C#的最佳工具、Web开发者必备的9个软技能 对话“宙斯杯”获奖代表:依托京东云平台,打造精品电商CRM 关于微软开源WinJS,开发者必须知道的5件事 告别丑陋banner条,InMobi联合开发者尝试原生广告 免费游戏开发经验谈:第一印象很重要 《近匠》小旭音乐:游戏音乐金牌匠人的练级之路 日600亿消息,月4.65亿用户——WhatsApp的Erlang世界 一周热点:Cloudera新一轮融资总额已达9亿美元 【走近院士】李未:实现云服务的有效方法是群体软件工程 大神云集 大型创客聚会2014深圳Maker Faire盛大开幕 直击Maker Faire,将精彩“一网打尽”! 电池革命:超级充电器来袭,30秒为手机充满电 风雨13载,Windows XP今起正式退役 向Facebook看齐,Twitter收购Android屏幕待机应用开发商Cover 绝对的超现实!Jaunt打造360&#176;全景VR电影 Unite China 2014课程解析:行业解决方案专场免费开放 Manhattan,Twitter规模的实时、多租户分布式数据库 Ceph浅析(中):结构、工作原理及流程 孙元浩:基于Spark引擎的高速内存分析和挖掘工具应用 腾讯私有云背后的团队,品高要做企业云计算的产品经理 撼动企业应用架构的十大技术趋势 vc我自建了一个子窗体类,点击主窗体时显示,怎么做啊? 难道我的word文档全都废了吗?? 帮帮忙!一个小问题!呵呵! "自助建站系统"到底能不能实现真正的二级域名、独立域名? 请看看我的代码有什么问题 如何将一个OLE对象转换为位图(注:不要截屏的方式) 在olecontainer里面调出的excel文档,如何使之不可编辑。 急救啊!这个字体怎么设置? 请问在jcreator下能不能编译成exe啊? small_wei进来领分! 初学这样的,觉得是很简单的思路,但找不出错误,有经验的帮忙看看 if a sql syntan can succeed in sql but asp... GDI+的问题 谁有FLASH的注册码,给个,谢谢 怎样用js写一个终止程序,类似asp的response.end 求教:学习MFC文件系统-针对一段代码的3个初学者问题 如何使98一开机就运行屏保!!!急!!!来者有分!帮忙了!!!!! 怪问题 <了解Struts1.1 > 我刚看到的一篇好文章,贴出来,大家共享 dayday up (: 如何驱动8139网卡(笔记本) interbase能与VC结合吗?另外想问有些软件与interbase一起打包安装,如何实现的呢? Lotus——你在哪里? 网络编程中遇到的几个问题(求助) 在java中调用javac.exe 紧急在线等待,求助!!!!! 我是新来的,很多都不懂!也没有朋友,有谁能和我做朋友么? 分手为什么只要一个人说了就算,为什么不能像离婚一样要双方同意,法院判决??? 关于从TImage继承创建自定义控件的问题? JRUN4怎么运行Servlet? 关于远程创建表,删除表的问题?请帮忙。 启动数据库时报 监听程序无法启动专用服务器进程???ORACLE 9.2.0.1.0 如何將指針作為參數傳遞。 请教vb自带的报表编程 在Sql语言中的取整函数是什么? 怎样将添加的记录到所有的现有记录后面? 如何装双系统?(在线等候) CSS一般要学多久才算掌握? 从事共享软件开发的进来谈谈做共享的辛酸好吗? 请教使用notifyicon遇到的问题 ActiveState.Visual.XSLT.for.VS.2003.v1.7.9.2475.WinNT2K无法安装。 如何合并两个dataset win98系统下ISAM问题 如何减小生成的EXE文件大小等? asp.net有没有模板 帮帮忙,呵呵!!!一个小问题!!!!! 用VB的兄弟姐妹们,来整个MSN如何? 关于dllhost占用系统资源的问题! 谁是雍亲王,出来说话!!! 在WSAD5.0里怎么添加类型为MYSQL的数据源 高手们,帮帮我噢(解决立刻给分) 全中国程序员联和起来。 关于GenBank提交我按格式写的“>Seq1 [organism=Bjerkandera adusta]B.adusta H10001internal transcribed spacer 1 ,5.8S ribosomal RNA,internal transcribed spacer 2,complete cds”却被提示:“Error:Nucleotide sequence contains invalid char 上海话发音“称哪”是什么意思 he"s not very tall but quite fat. accession 偶然发现这个词.但一直不理解这个词的意思.朋友告诉我跟纹身有关.谁能给我个准确而详细点的答案不?Thanks. 英语好的人进来吖···.···[问问题]>.那个那个.->.各位大哥大姐.->.小女子问个问题哈.->.请问:crowd group team party band gang 的不同之处是什麽?////>.我96年的.->.现在在学新概念2.->.在英冠.->.我Qq}469 4月到5月有什么重大节日? 发音 行 .动词吧 谢你们吖!请问爽朗活泼乐观的英文分别是怎么样的? in an effort to的同义词是什么?英英翻译 上海话里发音‘’中牟‘’什么意思 quite什么意思 李将军列传有关于 “太史公曰:传曰“其身正.”找的 很累. 请问:上海话“八叶包”怎么发音?我完全不懂上海话. quite 李将军列传 简介越短越好 但要完整 地青 什么意思 quite a catch in an effort to . 羁绊在伙伴之间是表示什么意思? quite有几个意思 李将军列传每段段意急求每一段的段意义! 现实生活中的精神枷锁有哪些 文言文愚公移山中的“出入之迂也、跳往助之、隐土之北”中的“之”什么意思? 《李将军列传》的相关问题1、本文记叙了李广哪几件事?李广领兵打仗有什么特点?司马迁对李广的主要评价是什么?2、“其身正,不今而行;其身不正,虽令不从”,请结合生活实际谈谈这句话 如何打破对情感执着这把精神枷锁? 圆柱、长方体、三棱柱哪个承重更稳定?如题,最好能附上分析, 哪位大大给我解释下“富奸”是什么意思 有没有类似“浮生乱了流年”的词语像泪似伊人妆,浮生乱了流年啊等的词啊?用来做网名的 承重最好的物体是什么?(三棱柱,圆柱,三棱锥,长方体.)用20张a4纸做承重最大的纸桥,求设计 谁死的重于泰山 轻于鸿毛 50个字左右不要战争时的人物! 为什么希腊的哲学发达,成为“西方哲学的故乡”,而古罗马的法学成为近代西方各国法律体系的基础?还有!古希腊与古罗马建筑成就的最杰出代表分别是什么? 《春》里的排比 比喻 拟人各写一句 举一个例子说说还有谁的死是重于泰山的,概括写出他的英雄事迹,不少于50字.最好不要写错别字 方便抄嘛,呵呵 日本青奸什么意思 DNF束缚机率50%,束缚等级61是什么意思.比如打61级以上的怪,是束缚机率到50%以下,还是束缚时间变短?还是什么? 项羽本纪 简略的概括我需要的是青少年版的,不要太多,字数少点哦!我非常急用的! 人的体内为什么会长蛔虫? 要写荷花的400字 第1段写整体感受.2写花.3写叶和茎.4写作用.5写对比手法结尾.要用上比喻和拟人写其他的花也可以.但写的方法要一样 谁的死重于泰山要简单的事例20字 蛔虫生活在人体内属于什么关系 行人多得()嘴厥得()填成语石间溪流脉脉,如线如缕,水塘中闪闪的碧波,就像锦缎一样.把水塘中闪闪的碧波,就像锦缎一样.使它与没有画线的部分行成对偶. 诚信是排比 蛔虫在人体内的活动?一群一群的蛔虫在人体内扭成一堆~究竟它们是怎样生活怎样活动的? 君恩深似海——,臣忠重如山——.在每句话的后面加同一个字,要使得意思完全相反.请高人指点,急盼! 关于诚信的排比 怎样才能查出所需基因在GENBANK的ID号只知道它的中文名字,不知道英文名字.希望可以给出萝卜过氧化氢酶的ID.急用.谢谢. 请问这句话正确吗?是不是我们盲目地限制的意思?we are blind to that limitation还有就是这个limitation是不是宾补? you tell me什么意思? 怎么从genbank中找序列我现在在做一个生物的基因序列分析,需要从GENBANK中找一些别的相似基因做对比. “君使臣以礼,臣事君以忠”这句话是什么意思? We pass.中文中文的意思 怎样使用GenBank登录号?就是知道某个基因的登录号,怎么到数据库内查到该基因? 怎样用Rhino做五棱柱 每个人体内都有蛔虫? 英语翻译地震是一种破坏性极强的自然灾害,由于受当前技术水平的限制,我们还无法在地震发生前,准备的预测出地震发生的时间、方位以及震度强弱,因此,每当大地震发生时,都给人们的生命 黄河三角洲高效生态经济区的高效指什么 正常人体内有多少蛔虫? 英语翻译20世纪70年代在货币、票据、股票、债券等基本金融工具的基础上派生出的衍生金融工具蓬勃兴起,不断创新.由于它具有规避风险和以小博大的杠杆作用,已经成为金融领域规避金融风 Quite tall ...and quite striking 新概念课文主要内容急! 肯尼亚法官判轮奸犯割草赎罪 超百万人斯诺登请求美方宽恕 白宫说“不”(图穆尔西受审 拒穿囚服大闹法庭(图)“透明化”监听时代可能会来俄新反恐法:恐怖分子亲友将“连坐”印度今日追梦火星 否认与中国竞争白宫微博有“水军”专为奥巴马“解围”英国政府禁止内阁会议及其它敏感部门使斯诺登求美“宽大处理”被拒 在俄与德朝鲜证实海军官兵殉职美机场枪案嫌犯为“独狼”中国小伙MBA毕业 波士顿街头卖烤肉全国反“四风”开整7大问题 叫停公款武康女店主发现儿子走丢忘报警16岁少年带着理发用剪刀抢劫不成去自90后女子看老公送很多红心给别人醋意嘉兴男子家因排污管堵塞被污水淹 9户女子给骗子汇钱逼停出租车 的哥及时中杭州国际人才交流与合作大会明天开幕 多项违纪 孙杨被处停赛、停训、暂停一浙江杭州等地连续雾霾 “最美马拉松”淘宝推“假货快速退款”“龙江颂歌”蔚然成景重庆市民穿棉大衣坐“冰窖”里打麻将高温黄色预警:7省份部分地区最高气温编后>>>农药瓶子兑换肥皂宗教局局长:有党员丢掉灵魂 不问苍生上海气温直逼40度 外滩地面可煎鸡蛋关于漳州市辉昌工贸有限公司等债权资产油价迎来“四连跌”外资演出机构及娱乐场所可设在自贸试验福建跆拳道大奖赛开赛平潭公铁跨海大桥主塔全面开建东山投建生态低碳示范公路“泡面人生”无法为替考者开罪生态林中白鹭栖虽没撞人也犯罪清算公告铿锵玫瑰?她们才刚上路呢!(上)图片新闻排管中心主任潘志琛
备案号:鲁ICP备13029499号-2 说三道四 www.s3d4.cn 说三道四技术文摘