Package.js Specification

大纲

  1. 基本概念
  2. JS Package 扩展概念
  3. Package API
  4. Package Examples
  5. Package Build概念
  6. Package Build工具及配置
  7. 开发模式调试及部署

基本概念

一个Package声明的元信息:
id(必须) Package的全局标识,它必须是全局唯一的,通过此名称指定加载此包。 一个Package只有一个id。
id由namespace与name组成
命名空间的概念类似于文件路径,由空间名和分隔符组成。
常见的命名空间形式以"."作为分隔符。如:
Root.sub.inner.Foo
表示Foo标识在Root.sub.inner命名空间下
body(必须) Package声明的内容体
dependences(可选) 依赖的其它Package。 在Package中需要导入使用。
Package声明基本原则:
  1. Package命名不能冲突
  2. 依据惯例,Package存放物理路径根据其命名空间,以目录树的形式表示命名空间树
  3. 在同一个运行周期内,Package只会被声明一次,即body只会被执行一次
  4. Package的body的执行是同步的,执行完成即完成Package的创建
  5. Package不能存在循环依赖关系。如:A依赖B,同时B也依赖A

Library 概念

Library是一系列Package的集合。在文件组织上,一个Library是一个包含一系列Package文件的目录。 Library下的Package使用相同的根命名空间。Library根目录即是根命名空间目录。

JS Package 扩展概念

Package Id/Namespace
Package/Package Body
Package对象理论上可以为任何类型的非null/undefined对象。
在JavaScript中使用function块来声明Package body块。 即,body是一个Package factory函数. 见[Package API]
Package Dependences
JS Package除了依赖于其它包之外,还会依赖于CSS、HTML等资源文件。
Package文件的物理存放
对CoffeeScript的支持
对于使用CoffeeScript编写的Library,默认按照規約,对应源文件以coffee作为扩展名。 即对于Package:NS.ui.Button,对应源文件为NS/ui/Button.coffee, 但要求_nsconf_文件必须是JS。

Package API

Type Definitions

type Package=Object Package对象可以为任何非null/undefined对象,它将被注册到它对应的命名空间下。 Package对象的_pkgMeta_属性为此Package对应的PacakgeMeta对象, _pkgMeta_属性在PackageBody函数执行完成之后才具有。
type PackageId=String 格式为"NS.path.Pkg"的命名空间字符串。
type AssetDeps=Object 依赖资源的定义对象。 属性名为资源名称,以下划线开头的属性表示此资源只加载,但在JS中不会直接访问资源内容。
值为相对文件路径(通常,将该Package依赖的资源文件放在同一目录,一个JS Package不应该依赖它目录之外的资源文件,即,这里的相对文件路径不应包含".."),一般根据扩展名判断文件类型及加载方式。
可在文件路径前加":"注明文件类型。如:
{
    _style:"css:style.css",//加载style.css,"_"开头的属性表示JS中不会直接读取style.css内容
    tpl:"str:tpl.html",//加载tpl.html,并且JS中使用
    messages:"messages.txt",
    db:"json:db.json"
}
下表为常见支持的文件类型,及其文件扩展名,映射到JS中的对象类型:
文件类型 扩展名 对应JavaScript对象类型
str(纯文本字符串) .txt .html String
css(CSS样式表) .css null

(JS中不需要访问CSS内容,可将资源名称设为下划线开头)

json(JS对象序列化JSON文件) .json Object
less(Less格式的 CSS 样式表) .less null
type Assets=Object 依赖资源的结果对象。 属性名与在AssetsDeps中定义的对应,但不包含以下划线开头的属性。 值为资源类型在JS对应类型的对象,如str类型对应String等。(见上表)。
type PackageMeta=Object 包含Package元信息的对象。 在PackageBody函数执行完成,即Package声明完成之后,可通过Pacakge对象的_pkgMeta_属性访问。在PackageBody函数执行时,PackageBody函数的this对象为Package对应的PackageMeta对象。
属性列表
  • pid:PackageId ,当前Pkg的命名空间ID
  • relUrl:String ,当前Pkg对应JS文件的相对Library根目录的相对URL(如:NS.ui.Button的relUrl为NS/ui/Button/init.js)
  • relPath:String ,当前Pkg对应JS文件所在目录的相对Library根目录的路径,以/结尾(即relUrl去掉文件名)
  • url:String ,当前Pkg对应JS文件的完整URL(当JS文件被合并之后,此URL仍然为合并前单个JS文件的加载URL,见[[build.js 配置]]
  • path:String ,当前Pkg对应JS文件所在目录的完整路径,以/结尾,即是将url去除了文件名
  • deps:[]PackageId ,当前Pkg依赖的PackageId数组
  • assetDeps:AssetDeps ,声明静态依赖的资源文件相对路径列表
  • [pkg:Package] ,创建完成后的Package对象
  • [depPkgs:[]Package] ,当前Pkg依赖的Package数组
  • [body:PackageBody] ,Pkg声明内容体
  • [assets:Assets] ,声明静态依赖的资源文件内容对象
type PackageBody=Function PackageBody为一个Factory函数,执行该函数返回生成的Package对象。
function (...deps:Package):Package {
    //....lots of code
    console.log(this.url,this.path,this.assets);
    return CreatedPackage;
}
可以从函数参数中获取到对依赖包的引用。
函数的this为当前PackageMeta对象, 函数的参数依次映射到define声明时列出的依赖包对象。

Package Define

define(id:PackageId,body:PackageBody):void
最简单的创建一个不依赖其它包,只依赖于当前根命名空间的Package。 任何Package都需要依赖它所在的根命名空间包。
define(id:PackageId,deps:[]PackageId,body:PackageBody):void
创建一个依赖于deps列表中列出包的Package。
define(id:PackageId,deps:[]PackageId,assets:AssetDeps,body:PackageBody):void
创建一个依赖于deps参数中列出包,并依赖于assets参数中列出的资源文件的Package。
PackageId始终为define第一个参数。 PackageBody声明函数始终为define的最后一个参数。

导入变量名

PackageBody Factory函数执行时的this指向当前的PackageMeta(包含当前Package的一些信息)对象, 在PackageBody的参数将依次映射到deps中列出的Package对象。如下:

Package.define("Root.ui.Button",["Root.ui.Pane","Root.util.Tpl","Root.util.Event"],
function (Pane,Tpl,Event) {
    //this为当前PackageMeta对象
    //Pane为Root.ui.Pane
    //Tpl对应Root.util.Tpl
    //依此类推
    //.....
});

当前Package的根命名空间可视为全局变量,所以当前根命名空间不需要显式写在依赖列表中导入,但其它根命名空间则需要导入,如下:

Package.define("Root.ui.Panel",["Root.ui.Border","dojo"],
function (Border,dojo) {
    var a=Root.getName();//Root不需要写在deps列表中
    var div=dojo.get("oDiv");//dojo为其它根命名空间,需要导入
    //.....
});

Library命名空间配置文件_nsconf_.js

默认JS Package文件名称都采用NS/path/Pkg.js形式,对于其它形式的都需要通过_nsconf_.js配置。

_nsconf_.js配置内容及格式
属性概览
属性(中括号表示可选) 说明
[rootPath:String] 当前Library的根路径,即根命名空间目录父级目录路径(如:_nsconf_.js加载URL为http://statics.abc.com/js/XLib/_nsconf_.js,则rootPath值为http://statics.abc.com/js/)。此属性一般是运行时获得,不需要直接配置(如果_nsconf_.js中配置了此属性,而使用配置的属性,而不动态获得)。此属性动态获取是通过取页面中script元素的src属性值以/NS/_nsconf_.js结尾的URL,存在URL结尾相同,即以NS/_nsconf_.js结尾,导致获取的值不正确的情形!
[libName:String] 当前Library的名称,即根命名空间名称,该属性是运行时自动创建的。
[useLoaders:Object] 指明当前Library下的Package使用的Asset Loaders。 当前Library下的Package的文件存放不是默认的NS/path/Package.js格式时,需要配置此选项。
[defineLoaders:Object] 定义一个非内置的loader。 自定义loader只支持自定义AssetDeps,并且asset类型也必须是内置支持的类型。 大部分情况不需要使用自定义的loader。
[drinkCoffee:Boolean] 当前Library使用CoffeeScript编写。即,源文件使用“.coffee”作为扩展名,而不是默认的“.js”.
注意,在开发模式下,如果使用CoffeeScript,必须还要手动加载对应的CoffeeScript解释器。参见下面[开发模式调试及部署]
只能配置整个Library使用或不使用CoffeeScript,同一个Library既使用JS又使用CoffeeScript太混乱的,谁要是这么混合写就罚他做10个Demo和部署更新10次服务器。
系统内置loader
loader名称 说明
base 默认的Loader,不需要配置,只包含Script,文件形式NS/path/pkg.js。
archive 基础的使用目录结构的Package loader,对于NS.ui.Button使用路径NS/ui/Button/init.js。注意根Package并不需要手动配置为archive。(即,NS/init.js是默认的,不需要配置。) archive是最基本的loader,仅表示使用Root/path/Pkg/init.js形式加载JS,没有依赖资源文件, 但init.js中定义时仍然可以通过define的assetDeps参数另外单独配置依赖的assets。
wui Web UI Package。带有CSS样式及HTML模板文件的archive形式的Package。定义为:
/*
NS.ui.Button对应为
NS/ui/Button/
            - init.js
            - style.css
            - tpl.html
*/
// 对应PackageMeta
{
    assetDeps:{
        _style:"style.css",
        tpl:"tpl.html"
    }
}
                
wui4ie6 特殊针对IE6的web ui package。带有HTML模板文件及针对IE6浏览器及其它浏览器的CSS样式。 因为IE6是非主流,所以需要特殊对待。
/*
NS.ui.Button对应为
NS/ui/Button/
            - init.js
            - style.css
            - ie6.css
            - tpl.html
*/
// 对应PackageMeta
{
    assetDeps:{
        _style: IE6?"ie6.css":"style.css",
        tpl:"tpl.html"
    }
}
                
lesswui 同wui,只是style使用less css
lesswui4ie6 同wui4ie6,只是style使用less css
//文件XLib/_nsconf_.js基本格式
Package.define("XLib._nsconf_",function () {
    return {};//返回空对象,最简单的情况,不做任何配置
    //表示该命名空间下所有JS Pkg都使用XLib/path/Pkg.js形式
});

//詳細配置
Package.define("XLib._nsconf_",function () {
    return {
        //声明当前Library下的Package使用的loader
        //只有以Dir/init.js形式的Package才需要使用loaders
        //Root/path/Pkg.js形式的Package不需要做任何配置(也不能做任何配置)
        //useLoaders键名为loader名称,值为不包括根命名的命名空间数组
        //不能定义一个Package使用多种loader
        useLoaders:{
            archive:["flash","net.PostMsg"],//表示XLib.flash命名空间下的所有包使用archive loader
            //(即JS文件路径为XLib/flash/Pkg/init.js). XLib.net.PostMsg包(这里单独配置一个Pkg)也使用这种加载方式
            wui:["ui.forms","ui.ctrls"],//表示XLib.ui.forms与XLib.ui.ctrls下的Pkg使用wui loader
            wui4ie6:["widgets"] //XLib.widgets下使用带判断ie6对应css的加载
            dbcom:["data"] //XLib.data.* 使用dbcom loader
        },

        //自定义loader
        //loaders键名为合法的属性名,不能以下划线开头
        //值为包含assetDeps(其依赖的资源文件列表)属性的对象(同Package.define中的assetDeps)
        //一般情形只需要使用内置标准loader,不需要在_nsconf_.js中自定义另外的loader
        defineLoaders:{
            //注意,凡是使用loader的Package,都是使用Dir/init.js形式存储的
            dbcom:{ //声明一个包含JSON数据的控件的加载器
                assetDeps:{
                    db:"db.json"
                }
            }
        }
    };
});

在开发模式下使用wui及wui4ie6之类的loader时,可能遇到跨域加载tpl.html的情形,在这种情况下,对于支持 HTTP Header: Access-Control-Allow-Origin "*";的浏览器,可以通过服务器端设置此header告诉浏览器允许跨域访问,如Nginx中可以添加配置:add_header Access-Control-Allow-Origin "*";(仅开发环境)。对于不支持此Header的浏览器,如IE6,则需要在Library根目录下放置一个_xproxy_.html文件,软链接指向src/Package/_xproxy_.html文件,通过此文件作中转代理进行跨域tpl.html文件加载.
中转代理文件 _xproxy_.html 支持两种跨域加载方式:postMessage(IE8及其它支持此特性的浏览器使用此方法)和iframe.contentWindow.name(仅IE6和IE7);

Package Examples

单一的Package NS.util
// NS/init.js 内容
Package.define("NS",function () {
    return {
        getName:function () {
            return XXX;
        }
    };
});

// NS/util.js 内容
Package.define("NS.util",function () {
    var util={
        //lots of code

    };
    //NS自动对子Package可访问
    util.ownerName=NS.getName();
    return util;
});
带有deps的Package NS.net.URL
//文件NS/net/URL.js内容
Package.define("NS.net.URL",["MT.string.Fields"],function (Fields) {
    Fields.split(s);
});
形式为Dir/init.js的Package NS.charts.FlashChart
//省略NS/init.js
//文件NS/_nsconf_.js内容
Package.define("NS._nsconf_",function () {
    return {
        useLoaders:{
            archive:["charts"]
        }
    };
});

//文件NS/charts/FlashChart/init.js内容
Package.define("NS.charts.FlashChart",["MT.url.Financial"],function (Financial) {
    var flashUrl=this.path+"flash/chart.swf";//通过PackageMeta#path获取flash文件的位置
    //lots of code
    return FlashChart;
})
使用wui loader的NS.ui.Button
//文件NS/_nsconf_.js内容
Package.define("NS._nsconf_",function () {
    return {
        useLoaders:{
            wui:["ui"]
        }
    };
});
/* NS/ui/Button目录结构为
NS/ui/Button/
    -init.js
    -style.css
    -tpl.html
*/
//NS/ui/Button/init.js文件内容
Package.define("NS.ui.Button",["MT.ui.Component"],function (Component) {
    //通过this.assets.tpl访问tpl.html内容
    var bgImgUrl=this.path+"img/bg.png",tpl=this.assets.tpl;
    function Button(opt) {
        //也可以通过当前Package对象的_pkgMeta_属性访问assets
        this.tpl=String2Dom(opt.tpl || Button._pkgMeta_.assets.tpl);
    }
    return Button;
})
自定义loader
// NS/_nsconf_.js 内容
Package.define("NS._nsconf_",function () {
    return {
        defineLoaders:{
            dataBind:{
                assetDeps:{
                    data:"data.json"
                }
            }
        },
        useLoaders:{
            dataBind:["data"],
            archive:["util.IpDetect"]
        }
    }
});

//NS/data/User/init.js内容
/*
NS/data/User/
    -init.js
    -data.json
*/
Package.define("NS.data.User",function () {
    var users=this.assets.data;
    function User() {}
    console.log(User._pkgMeta_); //Error:此时_pkgMeta_属性是不存在的
    return User;
});

//NS/util/IpDetect/init.js内容 (在Package.define时自定义assets)
/*
NS/util/IpDetect/
    -init.js
    -ip.txt
*/
Package.define("NS.util.IpDetect",{ //assets
    ipData:"ip.txt"
},function () {
    var ipArray=this.assets.ipData.split("|");
});

Package Build概念

通常从JS文件的加载性能上考虑,需要将使用到的JS包合并到一个文件中,并对其进行压缩。 合并/优化/压缩指定的JS Package涉及以下几点内容:

JS依赖资源文件合并方式:

Package Build工具及配置

配置文件使用JSON格式。但事实上config.json使用eval执行,可以并不严格按照JSON语法,如可以添加注释等。

//:config.json
//所有相对文件路径均相对于JSON文件的位置
{
    //列出用到的Library的_nsconf_.js文件路径列表

    "nsconfs":[ //#REQUIRED
        "src/statics/js/XLib/_nsconf_.js",
        "src/statics/js/Motu/_nsconf_.js"
    ],
    //static域名配置,表示Library的根路径,
    //如对于NS.ui.Button,对应URL为http://jex.im/js/NS/ui/Button/init.js,
    //则staticUrls则配置为http://jex.im/js/
    //此这路径将替换nsconf的rootPath
    "staticUrls":{ //#REQUIRED
        "defaults":"http://jex.im/js/",//默认的static路径
        //可以不同的Lib配置不同的根路径
        "NS":"http://jex.im/third-party-js/"
    },
    //列出需要打包的Package,但不需要列出它的依赖包
    //支持结尾的"*",获取该命名空间下的所有Package
    "includes":[//#REQUIRED
        "Package.fetchers.fetchText" //将fetchText也加载进去,以跨域加载tpl.html
        "Motu.apps.*"
    ],
    //是否将Package/init.js的内容也打包进去,默认exportMode为all及deps时打包
    //当exportMode为includes时,此选项不起作用
    "embedPackageCore":true,
    //列出运行时环境或preload.js中已经存在,不需要获取依赖的包(在打包里不会去找这个Package的代码)
    "imported":[
        "dojo.*"
    ],
    //需要在所有Package之前加载的JS文件(支持CoffeeScript)
    //按这里列出的顺序加载
    "preloadScripts":[
        "src/statics/js/preload.js",
    ],
    //在所有Package代码之后加载的JS文件(此时所有Package都完成了声明)(支持CoffeeScript)
    //按这里列出的顺序加载
    "postloadScripts":[
        "src/statics/js/lang-Zh_CN.js",
        "src/statics/js/postload.js"
    ],
    //在所有CSS之前按顺序加载的样式文件(支持LESS CSS)
    "preloadStyles":[
        "src/statics/css/reset.css"
    ],
    //在所有CSS文件之后按顺序加载的样式文件(Also LESS)
    "postloadStyles":[
        "src/statics/css/theme-blue.css"
    ],

    //对输出的JS和CSS文件进行压缩
    "compress":true,

    //导出模式
    /*
    支持模式:
    includes    只导出includes项列出的Package的JS和CSS
    deps        只导出includes项列出的Package依赖的Package的JS和CSS
    all         导入includes及其依赖Package的JS和CSS
    适用情形:
    因为apps.js和base-lib.js更新频率不一样,将依赖的base-lib.js合并到另一个文件中,
    使用较大的Expires头,对apps.js使用较小的Expires头
    */
    "exportMode":"all"
}
build.js工具的使用
./build.js config.json js all.min.js  #按配置合并所有JS到all.min.js
./build.js config.json css all.min.css   #合并所有CSS到all.min.css
./build.js config.json css4ie6 ie6-all.min.css #合并特殊的wui4ie6的ie6.css到ie6-all.min.css

开发模式调试及部署

在开发模式下,页面直接加载需要使用到的Package的代码,其依赖JS及CSS、HTML文件会自动加载。
作为一种设计模式,整个网页的主入口程序可作为MainApp,MainApp再声明依赖其它组件,依赖的组件将被自动加载。


部署到生产环境

  1. 将statics目录全部上传到静态文件服务器
  2. 假设statics目录的URL为 http://statics.example.com/ ; JS Library的根目录为statics/jslibs/ ;则build config.json中staticsUrl的default则可配置成 http://statics.example.com/jslibs/;
  3. 使用build.js生成all.min.js和all.min.css
  4. 修改HTML嵌入的代码为:
      
部署注意点
  1. 虽然JS和CSS都合并到了一个文件中,但CSS中引用的图片之类的资源仍然需要上传到静态服务器,并且路径和开发时的访问路径一样。但各个JS的源代码及CSS源代码可以不上传。
  2. 用于跨域访问的_xproxy_.html可以不上传到生产环境用的开放的静态服务器(除非你需要在生产环境上调试代码。),为了安全性,可以禁止访问_xproxy_.html
  3. 合并之后的JS中,每个Package的PackageMeta对象的url及path之类的属性仍然是源代码所在位置的URL,以此path仍能正常获得该Package目录下的其它资源文件的URL