加载器(Microloader)

“加载器(Microloader)” 是 Sencha 的数据驱动(data-driven), 用来动态加载 JavaScript 和 CSS. 加载器(Microloader) 是 Sencha Cmd 生成的应用程序的一部分. 本指南会让你对 Microloader 的作用有个深刻的理解,以及如何对它进行调整优化以便适应你特定的需求.

需要澄清的一点是,Ext 6 用的 Microloader 不同于 Ext JS 5 或 Sencha Touch 的 Microloader. Ext JS 6 的 Microloader 不仅提供了和 Sencha Touch 的 microloader 一样的全部功能, 而且还有一些改进,以及在"app.json"文件中增加了新的配置, 我们稍后讨论. 所有的 microloader 实现由 Sencha Cmd 提供, microloader 的升级则通过“sencha app upgrade” 这个过程.

清单文件(Manifest)

app.json

这个文件是为你的应用程序指定详细配置的地方. 这个配置文件是 Sencha Cmd 在构建过程要用到的. Sencha Cmd 解析 "app.json" 里的内容,并把清单结果传递给 Microloader 使用. 最后, Ext JS 自身也在运行时参照这个清单文件的配置.

Ext.manifest

当你启动你的应用程序, 你会发现 "app.json" 处理过的内容被加载到了“Ext.manifest”. Ext JS 6 使用 Ext.manifest 中你指定的属性,去做一些比如启用兼容层的事情. Microloader 支持各种选项,这个我们稍后讨论.

script 标签

要使用 Microloader, 你的页面需要包含下面的 script 标签:

<script id="microloader" data-app="12345" type="text/javascript" src="bootstrap.js"></script>

默认情况下, 这个 script 标签只在开发时使用,然后会在 build 过程中被替换掉. data-app 属性已经帮你自动生成. 它是一个唯一 ID编号,用来在本地数据存储的时候防止冲突.

默认设置和自定义设置

Sencha Cmd 生成的"app.json" 文件包含了很多属性,你可能需要调整它们. 这些属性上面有注释文档,解释了它们各自的作用.

假如你要升级一个项目, 你的 "app.json" 也许不包含所有的属性配置. 升级之后, 如果丢失了某些属性配置,可以在“.sencha/app/app.defaults.json”找到它们的默认值.

以下属性用得比较多.

indexHtmlPath

这是应用程序的 HTML 文档的路径 (相对于 "app.json" 的路径). 默认值是 “index.html”. 如果你的服务器用的是 PHP, ASP, JSP或其它技术, 你可以修改它指向合适的文件,像这样:

"indexHtmlPath": "../page.jsp"

如果你修改了这个设置, 你可能还需要修改“output” 选项 (往下看).

framework

框架的名字; 可能的值是 “ext”或 “touch”. 例如:

"framework": "ext"

theme

对于 Ext JS 应用程序, 这是主题包的名称. 例如:

"theme": "ext-theme-crisp"

toolkit

对于 Ext JS 6 应用程序, 这是工具包的名称(toolkit package). 例如:

"toolkit": "classic"

js

一个数组,描述了需要加载的 JavaScript 文件. 默认地, Ext JS 6 应用程序中内容如下:

"js": [{
    "path": "app.js",
    "bundle": true
}]

这些项目指定了你应用程序的入口. “bundle” 标识表示的是,当你执行“sencha app build”之后, 这一项会被最后合并输出的 js 文件所替换. 你可以在这个数组中增加其他文件:

"js": [{
    "path": "library.js",
    "includeInBundle": true
},{
    "path": "app.js",
    "bundle": true
}]

当你增加了其它文件, 有一些可选属性表示构建(build)过程中,如何处理这些文件. 例如, 上面的例子, “library.js” 会被包含到最后合并的文件中,并从运行时的 清单(Ext.manifest) 中移除.

如果你删除了 “includeInBundle”, 那么 “library.js” 会留在应用程序文件夹内,并拷贝到 build 后的输出目录下. 这一项会一直留在清单(manifest) 中,单独被 Microloader 加载

如果需要 Microloader 跳过加载这一项(并且不拷贝文件到 build 后的输出目录), 可以像下面这样添加“remote” 属性:

"js": [{
    "path": "library.js",
    "remote": true
},{
    "path": "app.js",
    "bundle": true
}]

虽然你可以在这个数组中添加, 但是大部分的 依赖文件都应该在代码中或者"app.json"中 (引入包的时候)用 “requires” 关键字来引入.

注意: 对于 Sencha Touch, 一边要在“js”数组中写“sencha-touch-debug.js”. 对于 Ext JS 6 则不需要这一项,因为“framework” 这个设置就足够了.

css

你的 CSS 资源文件 和 JavaScript 文件的处理稍有不同. 因为在 Sencha Cmd 应用程序中, CSS 由 .scss 代码编译而来. “css” 属性的初始值可能如下:

"css": [{
    "path": "boostrap.css",
    "bootstrap": true
}],

这个 CSS 文件只是一个简单的占位,用来引入"sass" 文件夹最后编译出来的内容. “bootstrap” 标识表示的是,当你执行“sencha app build”之后, 这一项会被最后合并输出的 CSS 文件所替换. For a build, 编译过后的 CSS 文件会追加至清单(manifest) 的“css”数组中.

起初, “bootstrap.css”引入框架中的主题,这个文件内容如下:

@import "..../ext-theme-neptune/build/ext-theme-neptune-all.css";

当你 build 了你的应用程序之后, 此文件会被改为指向最新变异的 CSS 文件. 例如,你运行了“sencha app build”, 产生的 CSS 文件会被“bootstrap.css”引入,像这样:

@import "..../build/..../app-all.css";

“css” 里的这些项目也支持“remote” 属性. 如果不设置 remote,那么和上面的“js” 那些项目一样,文件会拷贝到 build 后的输出目录.

requires

这个数组存放了 引入的包(package) 的名称. 当 Sencha Cmd 处理到这些列表项的时候, 如果遇到 workspace 中不存在的包,它会自动下载包并解压 . 在包(Packages) 的 "package.json"文件中同样可以有 requires块 . 也一样会自动按需下载并解压.

这些包的名字也可以包含一个版本号. 更多关于指定包版本的内容, 请看 Sencha Cmd 包(Packages).

output

“output”对象让你可以控制输出文件生成到哪里以及如何生成. 这个对象可以控制 build 输出的很多方面. 上面的 indexHtmlPath, 我们告诉 Sencha Cmd 页面路径是 "../page.jsp". 为了继续下去, 我们通过 output 来告诉 Sencha Cmd 构建后的页面存放在什么位置(相对于编译后的 JavaScript 和 CSS 文件). 为了保持和原来一样的相对位置, 我们应当在"app.json"中添加下面的代码:

"output": {
    "page": {
        "path": "../page.jsp",
        "enable": false
    },
    appCache: {
        "enable": false
    },
    "manifest": {
        "name": "bootstrap.js"
    }
}

这里, 我们添加了一个“enable”属性并设置为了 false. 这是为了告诉 Sencha Cmd 这个文件就是最终页面,而且不会通过拷贝代码的形式生成它 (也就是 “indexHtmlPage”中指定的文件).

因为我们不生成这个页面文件, Microloader script 标签的 src 仍然写的是“bootstrap.js”. 上面的“manifest” 选项指示 Sencha Cmd 也以此名字生成编译后的 Microloader 文件. 这种用法在服务端模板语言环境中是很常见的,比如 JSP 或 ASP, 等等.

应用程序缓存(Application Cache)

应用程序缓存 是一个清单,表示那些文件需要被浏览器缓存,以供离线使用. 要启用这个功能,只要把 “output”块中appCacheenable 标识设为 true. 例如:

"output": {
    "page": "index.html",
    "appCache": {
        "enable": true
    }
}

appCache 属性是用于生成 HTML5 应用程序缓存清单文件的. 如果设为 true,那么则会根据 app.json 中的顶层appCache 配置项指定的内容来生成清单文件. 这个配置项的内容如下:

"appCache": {
    "cache": [
        "index.html"
    ],
    "network": [
        "*"
    ],
    "fallback": []
}

本地缓存(Local Storage Cache)

本地缓存系统是浏览器内置的一个独立的离线缓存系统. 资源通过键值对存放在本地存储中,启动过程中,这些资源文件会先被请求,然后再请求其它远程资源. 这样,应用程序可以快速加载,而不需要网络连接. 这也可以实现增量更新,意思就是只有发生过改动的资源文件、css 或 js 才会远程重新加载. 然后这些文件会重新进入本地缓存,以展现给用户.

可以通过设置各个资源文件的update 属性来配置是否缓存. 它的值可以是 "full""delta". 下面的例子同时启用了 JS 和 CSS 资源文件的本地缓存.

// app.js 增量更新
"js": [
    {
        "path": "app.js",
        "bundle": true,
        "update": "delta"
    }
],

// app.css 会重新下载
"css": [
    {
        "path": "app.css",
        "update": "full"
    }
]

一旦某个文件启用了本地缓存,就必须启用 "app.json"中的全局cache配置. 通常,构建开发(development)版本的应用程序的时候设为 false,构建 production 版本的时候则设为 true.

"cache": {
    "enable": false
}

也可以设置增量更新的文件的生成和输出路径. 如果 deltas 属性设为 true,所有的启用了本地缓存的资源都会在"deltas"目录下生成增量更新文件 . 如果 deltas 设置为了一个字符串,则会被当成是增量文件输出目录的名字. 如果enable是 false,那么 delta 开关不起作用.

"cache": {
    "enable": true,
    "deltas": true
}

应用程序缓存(Application Cache) 和 本地缓存(Local Storage Cache) 都会立即加载文件以便离线访问. 因为这些更新的文件并不会影响用户使用当前的应用程序. 一旦 Microloader 检测到并加载了新的缓存文件,应用程序会触发一个全局事件,让你可以提示用户,是否重新加载已更新的应用程序. 你可以如下监听这个事件:

Ext.application({
name: 'MyApp',
mainView: 'MyMainView',
onAppUpdate: function () {
    Ext.Msg.confirm('应用程序更新', '应用程序已经更新, 是否重新加载?',
        function (choice) {
            if (choice === 'yes') {
                window.location.reload();
            }
        }
    );
}

更多

我们看到的很多属性,都是作为 build 过程中的指令, 另外一些则被 Microloader 使用. 如上所述, Ext JS 也可利用清单,来配置自己一些功能. 更多关于这些属性的细节, 请查看 "app.json"中的注释.

用于构建的配置文件(Build Profiles)

当应用程序有多个变种, 我们可以在"app.json"增加一个“builds” 对象,来描述不同的构建方案,像这样(取自 Kitchen Sink):

"builds": {
    "classic": {
        "theme": "ext-theme-classic"
    },
    "gray": {
        "theme": "ext-theme-gray"
    },
    "access": {
        "theme": "ext-theme-access"
    },
    "crisp": {
        "theme": "ext-theme-crisp"
    },
    "neptune": {
        "theme": "ext-theme-neptune"
    },
    "neptune-touch": {
        "theme": "ext-theme-neptune-touch"
    }
}

“builds”中的每一个键(key)叫做“用于构建的配置文件(build profile)”. 它的值是一些属性的集合,用来覆盖"app.json"的基本内容,以产生不同的清单文件(下面会说到). 这个例子中, “theme” 属性会运用到每个 build profile 中.

另外, 还有两个可选属性可以用于构建: “locales” 和 “themes”. 它们用来自动化生成最终的 build profile. 例如, Kitchen Sink 这个例子用了 “locales”:

"locales": [
    "en",
    "he"
],

当指定了 “locales” 或者 “themes”, 它们的值会合并到“builds”中,用来生成最终的清单. 此处“neptune-en”, “neptune-he”, “crisp-en”等就是最终 build profile 的名字.

生成清单(Manifest)

像以前提到的, "app.json"在 build 过程中经历了一次转换,以作为运行时的“Ext.manifest”. 这个过程有点像Ext.merge ,不过有点不同: 两个数组会以 连接(concat) 的方式合并.

这个转变的过程的第一步,是合并 build “环境”设置 (例如, “production” 或 “testing”). 然后, 如果用到了 build profile, 它们的内容会合并. 不管顶层还是 build profile 指定了 “toolkit” (“classic” 或 “modern”), 它们的属性也会合并. 最后, 如果配置了 packager (“cordova” 或 “phonegap”), 它们的属性也会合并.

用伪代码来表达,应该像这样:

var manifest = load('app.json');

// 下面的值来自 "sencha app build" 命令:
var environment = 'production';
var buildProfile = 'native';

mergeConcat(manifest, manifest[environment]);
if (buildProfile) {
    mergeConcat(manifest, manifest.builds[buildProfile]);
}
if (manifest.toolkit) {
    mergeConcat(manifest, manifest[manifest.toolkit]);
}    
if (manifest.packager) {
    mergeConcat(manifest, manifest[manifest.packager]);
}

包(Packages)

生成 清单(manifest) 的最后一步是加入 所有用到的 包(packages).

如果一个包的 package.json 文件指定了“js” 或 “css”项目, 它们会以 连接(concat) 的方式合并到之前的数组中. 这样包就可以单独管理自己的依赖.

此外, “js” 和 “css” 项目, 每个包的 package.json 文件的内容都会被清理掉,并把每个包名加到 清单(manifest) 中的“packages”对象里. 假如 "app.json" 已经有了 “packages” 对象, 则会和 package.json 文件的相应内容合并. "app.json"优先级比较高,以允许其属性以作为配置选项传递到包 (见下文).

用伪代码来表达, "app.json" 和 package.json 内容的合并过程如下:

var manifest;  // from above

manifest.packages = manifest.packages || {};

var js = [], css = [];

// 展开已 required 的包,并按依赖顺序排序
expandAndSort(manifest.requires).forEach(function (name) {
    var pkg = load('packages/' + name + '/package.json');

    js = js.concat(pkg.js);
    css = css.concat(pkg.css);
    manifest.packages[name] = merge(pkg, manifest.packages[name]);
});

manifest.css.splice(0, 0, css);

var k = isExtJS ? 0 : manifest.js.indexOf('sencha-touch??.js');
manifest.js.splice(k, 0, js);

结果生成的 Ext.manifest 类似这样:

{
    "name": "MyApp",
    "packages": {
        "ext": {
            "type": "framework",
            "version": "5.0.1.1255"
        },
        "ext-theme-neptune": {
            "type": "theme",
            "version": "5.0.1.1255"
        },
        ...
    },
    "theme": "ext-theme-neptune",
    "js": [{
        "path": "app.js"
    }],
    "css": [{
        "path": "app.css"
    }],
}

合并的结果意味着,如果包的 package.json 中的 “foo”选项提供了某些全局配置 (例如, “bar”) 并设有默认值. 任何使用了这个包的应用程序都可以在"app.json"配置这个选项 :

"packages": {
    "foo": {
        "bar": 42
    }
}

包 像这样获取值:

console.log('bar: ' + Ext.manifest.packages.foo.bar);

加载顺序

加载一个开发状态的应用程序,和加载一个 build 过的,它们有个显著的不同就是 文件在浏览器中的加载顺序. 在以前的版本中, app.js 文件几乎是第一个被浏览器加载的文件. 只有它先加载完了, 才可以加载更多其它必须的文件.

然而,在 build 过程中, 这个顺序是反的. app.js 基本是最后一个输出的. 这很容易导致一种情况 - 有的代码在开发下是正常的,发布之后就不好了 - 这显然不是我们希望的.

在 Ext JS 5 中, build 过程要用到的加载顺序 会加到清单(manifest)中, 加载文件也是这个顺序. 虽然这会导致清单文件非常大, 不过只在开发状态下用到.

加载清单(Manifest)

加载器(Microloader) 会根据"app.json"中的描述来加载应用程序,并把其内容传到 Ext.manifest. 为了做到这一点, Microloader 必须先加载 manifest. 有三种基本方法.

嵌入式清单(Embedded Manifest)

通常一个应用程序只有一个主题,一种本地化语言,一个框架,结果就是只有一个清单. 此清单可以放到输出的 HTML 文件中以优化下载时间.

要启用这个选项, 在"app.json"中加入下面代码:

"output": {
    "manifest": {
        "embed": true
    }
}

这些设置会把 清单 和 Microloader 嵌入到 HTML 文件中.

命名的清单(Named Manifest)

如果你用了 build profiles, 嵌入式清单并不合适. Microloader 应该可以在加载的时候根据名字请求清单. 默认情况下, 生成的"app.json"文件和 HTML文件在同一个文件夹下,也是清单默认的名字. 你也可以指定其它的名字:

<script type="text/javascript">
    var Ext = Ext || {};
    Ext.manifest = 'foo';  // loads "./foo.json" relative to your page
</script>

这种方法很有用,可以在服务器端动态生成一个名字来管理 build profiles,而不是在代码里写死.

动态的清单(Dynamic Manifest)

有时候你可能会想在客户端选择一个 build profile. 为了简化这种情况, Microloader 定义了一个钩子函数 “Ext.beforeLoad”. 如果你像下面这样定义这个函数, 你就可以在 Microloader 处理之前,控制“Ext.manifest”的名字和内容,同时可以检测运行平台.

<script type="text/javascript">
    var Ext = Ext || {};
    Ext.beforeLoad = function (tags) {
        var theme = location.href.match(/theme=([\w-]+)/),
            locale = location.href.match(/locale=([\w-]+)/);

        theme  = (theme && theme[1]) || 'crisp';
        locale = (locale && locale[1]) || 'en';

        Ext.manifest = theme + "-" + locale;
    };
</script>

上面的代码取自 Ext JS 5 的 Kitchen Sink 例子. 这个例子构建了 几个主题 和 2种语言 (英语 “en” 和 希伯来语 “he”). 通过这个函数,build profile 的名称就成了类似“crisp-en”这种样子,通知 Microloader 去加载名为“crisp-en.json” 的清单,而不是"app.json".

根据你应用程序的需要,这种选择 build profile 的过程可以是任何形式. 可以在页面渲染的时候,让服务端做这个工作. 可能基于 cookies, 或其它用户数据. 上面这个例子则是基于 URL.

平台检测(Platform Detection) / 响应式(Responsiveness)

通常会把设备或浏览器用来作为选择清单的依据, 所以 Microloader 会传递一个名为“tags”的对象到 beforeLoad 函数中. 这个对象里面包含了 Microloader 检测到的各种平台标记(tags). 标记(tags)的可能有以下取值:

  • phone
  • tablet
  • desktop
  • touch
  • ios
  • android
  • blackberry
  • safari
  • chrome
  • ie10
  • windows
  • tizen
  • firefox

beforeLoad 函数里面可以用到这些标记(tags),甚至可以修改它们. 这个对象随后用来控制资源文件的过滤器(清单里面描述的 js 或 css 项目),也可以在运行时使用platformConfig来控制某些配置(config)的值. 这个钩子代码(hook)允许你控制这些过滤器可以匹配什么. 而修改这些标记(tags)的值,主要用于添加新的标记(对你的应用程序有意义的标记). 自定义标记应当以“ux-”为前缀. Sencha 提供的标记没有这个前缀.

清单内指定的标记(Tags)

清单(manifest)也提供了一个“tags” 属性,用来设置可用标记(tags). 这个属性的值可以是一个字符串数组

"tags": ["ios", "phone", "fashion"]

也可以是一个对象, 以标记(tag)的名字为键, 以 true 或 false 为值.

"tags": {
    "ios": true,
    "phone": true,
    "desktop": false,
    "fashion": true
}

当提供了这个属性, 这些 tags 优先级别高于自动检测到的 tags. 因为这些 tags 的值是清单提供的, 所以在“beforeLoad” 函数内不可用这些值, 如果在这个函数里修改了 tags, 这些修改也是会被清单提供的 tags 给覆盖掉的(当 tag 的名字有冲突的时候).

Last updated