博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

前言:

 

实话实说,之前我是有些小瞧了Sencha Touch中的Device Profile的作用,所以在翻译顺序上才把它放在了比较靠后的位置。细读此文之后才发现自己实在是大错特错,Device Profile简直堪称Sencha Touch MVC中的最大亮点之一。除非你甘愿放弃Sencha Touch那不可思议的跨设备能力,否则你都必须学透Device Profile这一功能。

 

这篇文章的英文原址是 http://docs.sencha.com/touch/2-0/#!/guide/profiles

原文标题是:Using Device Profiles应用程序中使用设备配置)

Sencha Touch 交流QQ213119459欢迎您的加入。


 

Using Device Profiles

在应用程序中使用设备配置

注:为方便起见,文中所有出现 Sencha Touch的地方均以 ST简写替代。


Today's mobile web applications are expected to work on a wide variety of devices, spanning from the smallest mobile phones to the largest tablets. These devices have a wide range of screen resolutions and are used in different ways. People tend to use phone apps while out of the house to rapidly gather some information or perform some action - often in under a minute. Tablet apps are more likely to be used for longer periods of time, usually within the home or somewhere else they can sit for a long time.

今日之移动web应用程序开发要求我们必须考虑尽可能广泛的设备适用性,从最小的智能手机,到最大的平板电脑,不一而足。这些设备分辨率跨度很大而且使用方式千奇百怪。通常人们不在家的时候会使用手机应用来快速获取信息或者做些别的什么,不过使用时间基本上都控制在一分钟以内(译者注:应该是每次一分钟以内吧)。平板电脑的使用时间显然就更长一些,而且一般使用场合是在家里或者在其他能够长久安坐的什么地方。


All of this means that people expect different app experiences on different devices. However, much of your application logic and assets can be shared between these different experiences. Writing separate apps for each platform is time consuming, error-prone and just plain boring. Thankfully, Device Profiles give us a simple way to share just as much code as we need to between device types while making it easy to customize behavior, appearance and workflows for each device.

凡此总总意味着人们在不同的设备上期望的是不同的体验,但事实上,尽管体验不同,可应用程序的绝大部分逻辑和资源还是可以共享的。为每个平台编写一个独立的app不仅浪费时间、容易出错,而且还超级无聊。谢天谢地,设备配置(这一功能)给我们提供了一种简单的方式,使得我们能够共享尽可能多的代码,同时又能轻松地为每种设备定制行为、外观和工作流。


Setting up Profiles

配置内容


Device Profiles exist within the context of an Application, for example if we want to create an email app with phone and tablet profiles we can define our app.js like this (see the Intro to Apps guide if this is not familiar):

设备配置存在于应用程序的上下文环境之中,例如要创建一个带有phonetablet配置的emal应用,我们可以像下面这样来定义app.js(如果对此不熟悉的话请参考 Intro to Apps guide 


Ext.application({

    name: 'Mail',

 

    profiles: ['Phone', 'Tablet']

});


We didn't give our Application a launch function, so at the moment all it is going to do is load those two Profiles. By convention, it expects to find these in app/profile/Phone.js and app/profile/Tablet.js. Here's what our Phone profile might look like:

我们没有给应用程序写上launch方法,所以现在它唯一能做的就是加载这两个Profiles。按照惯例,他们的存放路径是app/profile/Phone.jsapp/profile/Tablet.js,看看Phone配置文件长得什么样子吧:


Ext.define('Mail.profile.Phone', {

    extend: 'Ext.app.Profile',

 

    config: {

        name: 'Phone',

        views: ['Main']

    },

 

    isActive: function() {

        return Ext.os.is.Phone;

    }

});


The Tablet profile follows the same pattern. In our Phone profile we only really supplied three pieces of information - the Profile name, the optional set of additional views to load if this Profile is activated, and an isActive function.

Tablet的配置也是一样的格式。这个Phone配置里面我们只提供了3条信息:Profile名称,一组需要额外加载的视图(该项属于可选的),还有一个isActive函数。


The isActive function is what determines if a given profile should be active on the device your app happens to be booting up on. By far the most common split is to create profiles for Phone and Tablet, using the built-in Ext.os.is.Phone and Ext.os.is.Tablet properties. You can write any code you like in the isActive function so long as it always returns true or false for the device it is running on.

isActive这个函数将决定该profile在当前设备下是否被启用。到目前为止,最常用的区分就是使用内置的Ext.os.is.PhoneExt.os.is.Tablet属性,分别创建PhoneTabletprofile。你可以在isActive函数里面写任何代码,只要他最终能够为当前设备返回一个true或者false即可。


Determining the Active Profile

当前活动配置


Once the Profiles have been loaded, their isActive functions are called in turn. The first one to return true is the Profile that the Application will boot with. This Profile is then set to the Application's currentProfile, and the Application prepares to load all of its dependencies - the models, views, controllers and other classes that constitute the app. It does this by combining its own dependencies with those specified in the active profile.

一旦Profile文件加载成功,它们的isActive函数会一次被调用。而应用程序将适配第一个返回trueprofile来启动,该Profile随即被设置成为应用程序的currentProfile,然后应用程序开始加载它所有的依赖项,包含数据模型、视图、控制器和其他组成应用程序的一些类文件。


For example, let's amend our Application so that it loads a few Models and Views of its own:

看例子,我们改进一下刚才的应用程序,让他加载一些数据模型和试图进来:


Ext.application({

    name: 'Mail',

 

    profiles: ['Phone', 'Tablet'],

 

    models: ['User'],

    views: ['Navigation', 'Login']

});


Now when we load the app on a phone, the Phone profile is activated and the application will load the following files:

现在我们在手机上运行一下,Phone配置被激活,应用程序将加载如下文件:

app/model/User.js
app/view/Navigation.js
app/view/Login.js
app/view/phone/Main.js


The first three items are specified in the Application itself - the User model plus the Navigation and Login views. The fourth item is specified by the Phone Profile and follows a special form. By convention, classes that are specific to a Profile are expected to be defined in a subdirectory with the same name as the Profile. For example, the 'Main' view specified in the Phone Profile will be loaded from app/view/phone/Main.js, whereas if we had defined 'Main' in the Application it would be loaded from app/view/Main.js.

前三项(User数据模型以及NavigationLogin视图)是应用程序自己定义的(也就是公共资源),第四项是Phone配置定义的。按照惯例,Profile定义的类将被放置在Profile同名的目录下。比如,Phone配置定义的Main这个视图就应该放在app/view/phone/Main.js文件中,如果Main是在应用程序中定义的话,就该放在app/view/Main.js中。

The same applies to all of the models, views, controllers and stores loaded in a Profile. This is important as it enables us to easily share behavior, view logic and more between profiles (see the specializing views and controllers sections below). If you need to load classes that don't fit with this convention, you can specify the full class name instead:


以上原则对Profile中加载的所有数据模型、视图、控制器、数据存储都适用。这样做非常重要,因为这样就能使得我们轻易地在不同设备之间共享行为、视图、逻辑以及更多资源了,后面的章节你可以看到定制视图和控制器的例子。如果想加载没按照常规位置存放的类,你就得指定完整路径了。


Ext.define('Mail.profile.Phone', {

    extend: 'Ext.app.Profile',

 

    config: {

        name: 'Phone',

        views: ['Main', 'Mail.view.SpecialView'],

        models: ['Mail.model.Message']

    },

 

    isActive: function() {

        return Ext.os.is.Phone;

    }

});


As we see above, you can mix and match fully-qualified class names (e.g. 'Mail.view.SomeView') and relatively specified class names (e.g. 'Main', which becomes 'Mail.view.phone.Main'). Be aware that all models, views, controllers and stores specified for a Profile are treated this way. This means if there are Models or Stores that you want to load for Tablets only but do not want to make classes like Mail.model.tablet.User, you should specify the fully-qualified class names instead (e.g. Mail.model.User in this case).

如上面代码所示,你可以混合使用完整路径调用方式(比如Mail.view.SomeView)和常规路径调用方式(比如Main会被解析为Mail.view.phone.Main)。请注意所有在Profile中定义的数据模型、视图、控制器、数据存储都是这样处理的。这也意味着如果你想加载一些数据模型和数据存储却不想让它们的类名如Mail.model.tablet.User这样,那就得指定完整的类名了(比如Mail.model.User)。


The Launch Process

Launch进程


The launch process using Profiles is almost exactly the same as it is without Profiles. Profile-based apps have a 3-stage launch process; after all of the dependencies have been loaded, the following happens:

Profile和没有Profile的情况下,launch进程几乎是一样的。基于Profile构建的应用程序有3launch进程,当所有依赖项加载完成后,将会先后发生如下事件:


Controllers are instantiated; each Controller's init function is called

控制器被实例化,每个控制器的init方法被调用

The Profile's launch function is called

Profile中的launch方法被调用

The Application's launch function is called.

Application中的launch方法被调用


When using Profiles it's common to use the Profile launch functions to create the app's initial UI. In many cases this means the Application's launch function is completely removed as the initial UI is usually different in each Profile (you can still specify an Application-wide launch function for setting up things like analytics or other profile-agnostic setup).

当使用Profile的时候,通常会使用Profilelaunch方法去创建应用的初始界面,很多情况下意味着Applicationlaunch方法被彻底丢弃了,因为每一个Profile中的初始化UI通常是不同的。你依然可以指定一个跨profileapplication launch方法来初始化一些东西,比如分析数据或者其他。


A typical Profile launch function might look like this:

一个典型的Profile launch方法通常是这样的:

Ext.define('Mail.profile.Phone', {

    extend: 'Ext.app.Profile',

 

    config: {

        name: 'Phone',

        views: ['Main']

    },

 

    isActive: function() {

        return Ext.os.is.Phone;

    },

 

    launch: function() {

        Ext.create('Mail.view.phone.Main');

    }

});


Note that both Profile and Application launch functions are optional - if you don't define them they won't be called.

注意ProfileApplicationlaunch方法都是可选的,如果你没定义它们,就不会被调用。


Specializing Views

专用视图


Most of the specialization in a Profile occurs in the views and the controllers. Let's look at the views first. Say we have a Tablet Profile that looks like this:

Profile大部分的专用设置都发生在视图和控制器上。先看视图,假定我们有一个如下的Tablet配置:


Ext.define('Mail.profile.Tablet', {

    extend: 'Ext.app.Profile',

 

    config: {

        views: ['Main']

    },

 

    launch: function() {

        Ext.create('Mail.view.tablet.Main');

    }

});


When we boot this app up on a tablet device, the file app/views/tablet/Main.js will be loaded as usual. Here's what we have in our app/views/tablet/Main.js file:

当我们在平板电脑上启动这个应用程序时,app/views/tablet/Main.js这个文件将被加载,其内容如下:


Ext.define('Mail.view.tablet.Main', {

    extend: 'Mail.view.Main',

 

    config: {

        title: 'Tablet-specific version'

    }

});


Usually when we define a view class we extend one of Sencha Touch's built in views but in this case we're extending Mail.view.Main - one of our own views. Here's how that on looks:

通常我们定义一个视图的时候都会扩展某一个ST内置的视图,但是这个例子当众,我们扩展的是Mail.view.Main这个我们自己创建的视图,代码如下:


Ext.define('Mail.view.Main', {

    extend: 'Ext.Panel',

 

    config: {

        title: 'Generic version',

        html: 'This is the main screen'

    }

});


So we have a superclass (Mail.view.Main) and a Profile-specific subclass (Main.view.tablet.Main) which can customize any aspect of the superclass. In this case we're changing the title of the Main view from "Generic version" to "Tablet-specific version" in our subclass, so when our app launches that's what we will see.

这样我们就有了一个父类(Mail.view.Main)和一个Profile指定的子类(Main.view.tablet.Main),在子类中我们可以定制任何内容。这个例子里我们在子类中把它的标题从Generic version改成Tablet-specific version,当应用运行起来的时候你就会看到了。


Because these are just normal classes it's easy to customize almost any part of the superclass using the flexible config system. For example, let's say we also have a phone version of the app - we could customize its version of the Main view like this (app/view/phone/Main.js):

由于这都是些常规的类,所以我们通过伸缩性极强的config系统可以很容易定制子类中的任何部分。假设我们这个应用还有一个phone版本,我们将会在其中(app/view/phone/Main.js)像这样来定制他的版本。


Ext.define('Mail.view.phone.Main', {

    extend: 'Mail.view.Main',

 

    config: {

        title: 'Phone-specific version',

 

        items: [

            {

                xtype: 'button',

                text: 'This is a phone...'

            }

        ]

    }

});

 

Sharing sub views

共享子视图


While the above is useful, it's more common to share certain pieces of views and stitch them together in different ways for different profiles. For example, imagine an email app where the tablet UI is a split screen with a message list on the left and the current message loaded on the right. The Phone version is the exact same message list and a similar message view, but this time in a card layout as there is not enough screen space to see them both simultaneously.

上面的例子很有用,不过更常见的是共享视图中的特定部分并在不同的profile中以不同的形式把它们拼合在一起。例如一个email应用,在平板电脑上的界面应该是左右分屏左侧放置邮件列表右侧显示当前加载邮件的内容,而手机版本同样是一个邮件列表和类似的内容视图,但这次他们只能通过card布局来摆放(这就意味着显示一个,隐藏另一个),因为显然手机上没有足够的屏幕空间来同时显示两者。


To achieve this we'd start off creating the two shared sub views - the message list and the message viewer. In each case we've left the class config out for brevity:

要实现这一设想我们得先创建两个共享子视图:一个邮件列表和一个内容浏览器,下面例子里我们将省略类的config


Ext.define('Main.view.MessageList', {

    extend: 'Ext.List',

    xtype: 'messagelist',

 

    //config goes here...

});


And the Message Viewer:

下面是内容浏览器:


Ext.define('Main.view.MessageViewer', {

    extend: 'Ext.Panel',

    xtype: 'messageviewer',

 

    //config goes here...

});


Now, to achieve our target layout the tablet Main view might do something like this:

然后要达到目的我们得在tabletmain视图里我们要这样布局:


Ext.define('Main.view.tablet.Main', {

    extend: 'Ext.Container',

 

    config: {

        layout: 'fit',

        items: [

            {

                xtype: 'messagelist',

                width: 200,

                docked: 'left'

            },

            {

                xtype: 'messageviewer'

            }

        ]

    }

});


This will create a 200px wide messagelist on the left, and use the rest of the device's screen space to show the message viewer. Now let's say we want to achieve our Phone layout:

以上代码将在屏幕左侧创建一个200像素宽的邮件列表,然后把内容浏览器放在屏幕剩余的区域,下面我们要实现手机布局了:


Ext.define('Main.view.phone.Main', {

    extend: 'Ext.Container',

 

    config: {

        layout: 'card',

        items: [

            {

                xtype: 'messagelist'

            },

            {

                xtype: 'messageviewer'

            }

        ]

    }

});


In this case we're just using a Container with a card layout (a layout that only shows one item at a time), and putting both the list and the viewer into it. We'd need to plug in some logic that tells the Container to show the messageviewer when a message in the list is tapped on, but we've very easily reused our two sub views in different configurations based on which Profile is loaded.

这种情况下我们只需要使用一个card布局(每次只显示其子项中的一个),然后把列表视图和内容浏览视图放置其中。我们还需要加入一些逻辑代码来告诉容器当列表中的某一项被点击时应该如何去显示内容浏览视图,但是我们已经非常轻易地在不同Profile的不同配置中复用了两个子视图。


As before, we also have the option to customize the two shared views for each Profile - for example we could create Mail.view.phone.MessageViewer and Mail.view.tablet.MessageViewer subclasses, both of which extend the Mail.view.MessageViewer superclass. This enables us to again share a lot of view code between those classes while presenting customizations appropriate for the device the user happens to be using.

像前面所做的那样,我们同样可以选择分别为每个Profile定制共享视图,比如创建Mail.view.phone.MessageViewerMail.view.tablet.MessageViewer两个子类,他们都继承自Mail.view.MessageViewer这个父类。这样使得我们可以再次共享很多视图代码。


Specializing Controllers

定制控制器


Just like with Views, many applications have a lot of Controller logic that can be shared across multiple Profiles. The biggest differences here between profiles are usually workflow-related. For example, an app's tablet profile may allow you to complete a workflow on a single page whereas the phone profile presents a multi-page wizard.

跟视图一样,很多应用程序同样有很多控制器逻辑可以跨配置共享。他们最大的区别通常是操作流不一致,例如一个应用程序的平板profile可以允许你在一个页面上完成工作,而相同的内容在手机profile中得通过多页式的的向导来进行展现。


Here we have a simple Phone profile that loads a view called Main and a controller called Messages. As before, this will load app/view/phone/Main.js and app/controller/phone/Messages.js:

这里有一个简单的手机profile,它加载了一个叫做Main的视图和一个叫做Messages的控制器。显然,这将加载pp/view/phone/Main.jsapp/controller/phone/Messages.js两个文件:


Ext.define('Mail.profile.Phone', {

    extend: 'Ext.app.Profile',

 

    config: {

        views: ['Main'],

        controllers: ['Messages']

    },

 

    launch: function() {

        Ext.create('Mail.view.phone.Main');

    }

});


Now, we know that our phone and tablet-specific controllers share most of their functionality so we'll start by creating a controller superclass in app/controller/Messages.js:

现在我们知道手机和平板电脑上的控制器大部分功能都是一样的,所以我们在app/controller/Messages.js文件中先创建一个控制器的父类:


Ext.define('Mail.controller.Messages', {

    extend: 'Ext.app.Controller',

 

    config: {

        refs: {

            viewer: 'messageviewer',

            messageList: 'messagelist'

        },

        control: {

            messageList: {

                itemtap: 'loadMessage'

            }

        }

    },

 

    loadMessage: function(item) {

        this.getViewer().load(item);

    }

});


This Controller does 3 things:

这个控制器做了三件事:


Sets up refs to views that we care about

设置了视图上我们需要使用到的refs

Listens for the itemtap event on the message list and calls the loadMessage function when itemtap is fired

侦听列表上的itemtap事件,当事件被触发时调用loadMessage方法

Loads the selected message item into the Viewer when loadMessage is called

loadMessage被调用时,加载被选中的消息到内容浏览器


Now that we have this, it's easy to create our phone-specific controller:

现在我们很容易创建手机的专用控制器:


Ext.define('Mail.controller.phone.Messages', {

    extend: 'Mail.controller.Messages',

 

    config: {

        refs: {

            main: '#mainPanel'

        }

    },

 

    loadMessage: function(item) {

        this.callParent(arguments);

        this.getMain().setActiveItem(1);

    }

});


Here we're extending the Messages superclass controller and providing 2 pieces of functionality:

这里扩展了Messages父类控制器并增加了两个小功能:


We add another ref for the phone UI's main panel

为手机界面的主面板增加了一个ref

We extend the loadMessage function to perform the original logic and then set the main panel's active item to the message viewer

我们扩展了loadMessage功能,首先执行父类既有的逻辑然后把内容浏览器视图设为active


All of the configuration that was in the superclass is inherited by the subclass. In the case of duplicated configs like refs, the config is merged, so the phone Messages controller class has 3 refs - main, viewer and messageList. Just as with any other class that extends another, we can use callParent to extend an existing function in the superclass.

所有父类中的配置都会被子类继承。所以像refs这种在config中重复出现的配置属性,它们会被合并,也就是说手机的Messages控制器就会有3refsmainviewermessageList)。就像其它的继承一样,我们通过callParent来扩展一个父类中已经存在的函数。


Bear in mind that our Mail.controller.Messages superclass is not declared as a dependency by either the Application or the Profile. It it automatically loaded because our Mail.controller.phone.Messages controller extends it.

记住Mail.controller.Messages这个父类既不需要在Application中进行依赖声明,也无需在Profile中声明,它会自动被加载,因为Mail.controller.phone.Messages控制器继承了它。


What to Share

共享什么


In the example above we were able to share some (but not all) of our refs. We were also able to share the single event that we listen for with the Controller's control config. Generally speaking, the more the app diverges between profiles, the fewer refs and control configs you will be able to share.

前面例子当众我们能够共享一部分refs(但不是全部),我们也能够共享一个在控制器的control中设置了侦听的事件。总的来说,应用程序不同Profile之间的区别越大,能共享的refscontrolconfig也就越少。


The one Controller config that should be shared across profiles is routes. These map urls to controller actions and allow for back button support and deep linking. It's important to keep the routes in the superclass because the same url should map to the same content regardless of the device.

有一个应该被不同profile共享的控制器configroute,这些url被映射到控制器动作并被允许支持后退按钮和深度链接。在父类中定义route是很重要的,因为不论在哪个设备上,同样的url总应该被映射到同样的内容。


For example, if your friend is using the phone version of your app and sends you a link to the page she is currently on within your app, you should be able to tap that link on your tablet device and see the tablet-specific view for that url. Keeping all routes in the superclass enables you to keep a consistent url structure that will work regardless of device.

例如,你的朋友使用了应用程序的phone版本,然后发送给你一个他正在看的页面链接,当你在平板电脑上点击了这个链接之后,你看到的内容也应该是一样的,只不过是tablet专用的视图展示形式而已。确保所有的route都在父类里使得你可以保证url架构的一致性并无需关注是在什么设备上。


Specializing Models

专用的数据模型


Models are customized per Profile less frequently than Controllers and Views, so don't usually require a subclass. In this case we just specify the fully qualified class names for models:

每个Profile定制不同的数据模型这种情况比定制控制器和视图出现的要少,所以一般不需要用子类,在这里我们仅仅指定完整的类名称就可以了。


Ext.define('Mail.profile.Tablet', {

    extend: 'Ext.app.Profile',

 

    config: {

        models: ['Mail.model.Group']

    }

});