Dojo构建系统

Dojo构建系统为构建Dojo以及你自己的js、css资源提供了一种途径,这样你的程序在生产环境下就会更有效率。

什么是构建

如果你用过其他编程语言的话,应该知道构建指的是将源代码编译、链接成二进制机器码文件。但是,这里的构建指的是代码压缩、优化、连接以及移除死代码。
从服务器下载html、js、css等文件,会占用带宽和时间。文件越大,数量越多,花费的时间就越多。Dojo Builder将会帮助我们更有效率地将代码发送到用户的浏览器,包括处理非Dojo自定义的代码、模块和CSS。如果做的正确,管理维护构建系统将很轻松。

基础知识

Dojo构建系统很复杂,可以高度自定义,还可以扩展(这里不做介绍)。新的构建系统是从Dojo 1.7引入的,被完全重写了。
开始之前,我们需要了解一下几个概念和术语。

模块和包

模块是Dojo 1.10的基础概念。包是由模块组成的。包是模块的逻辑集合。Dojo 1.10中,每个包都应该有package.json文件。文件提供包的描述信息。许多包还有package.js文件。这个文件提供Dojo特有的构建信息。

Dojo配置

Dojo内部有很多程序中有可能会用到的配置项。这些配置不仅对程序的正确执行很重要,而且在程序的构建过程中也很重要。你可以通过这些配置构建出你想要的程序。如果你对如何配置Dojo还不熟悉,那么请先回顾Configuring Dojo with dojoConfig这一篇。

层是一个独立的js文件,包含一些模块和其他资源。
层是构建过程的主要输出。
层可以是一个启动层。什么意思呢?就是它包含Dojo的启动代码,用于加载其他模块。
层的内容取决于你的代码和设计,所以没有一种正确答案。

Build Profile

Build profiles是一些小的js文件,为builder处理代码提供信息。在老的构建系统中,profile只有一个,位于util/buildScripts/profiles目录。Dojo 1.7之后,profile变成好多个,每个包里都有一个。你通过一个主profile来指导builder构建优化代码。

压缩

压缩代码会使js文件体积变小,功能不变,加载更有效率,还能混淆代码,这是优点。但是也有缺点,那就是代码变得难以调试。
Dojo 1.7之前,Dojo只能用ShrinkSafe来构建代码。Dojo 1.7之后(包括1.7),还可以使用Google的Closure Compiler。

移除死代码

Google Closure Compiler强大优势之一是它能够检测代码不可达,并从文件中移除。许多年前,我们也启动过一个叫Dojo Linker的工程来解决相似问题,但是一直没有时间来完成它。所以,很高兴能有一个替代方案。Dojo Builder在设计之初就考虑到了移除死代码的特性。
构建中的许多配置会导致Dojo Builder输出代码中含有硬编码路径。Google Closure Compiler会检测不可达代码并删除。

构建控制、转换器和解析器

构建控制、转换器和解析器是构建系统的基本组成部分。大多数情况,你不需要了解它们。但是如果你对高级构建感兴趣,就可以通过修改这些部分来做更有趣的事情。
构建控制实际上就是一个包含一大堆指令的js文件,包括读profile、使用哪种转换器、使用哪种解析器等等。
转换器,顾名思义,就是这种东西变成那种东西的东西。
解析器在构建时用来解析AMD插件。比如,代码中经常用到dojo/text来加载控件的模板。在构建时,解析器会把模板直接插入压缩文件。

构建前,你需要准备什么

为了使用Dojo构建系统,你必须拥有一份未编译压缩过的Dojo源代码。
Java是必须的。
NodeJS是可选的。

程序目录结构

我们注意到许多人的程序是Dojo 1.6甚至更早的时候迁移过来的,结构怪异,与我们的期望不符。这对于程序构建非常具有挑战性。所以,在你开始之前,最好能重新整理一下你的程序目录结构。
标准的结构应该是这样的,如下图所示。

标准程序目录结构

图中,所有的包都位于src目录下。Dojo的包(dojo、dijit、dojox、util)和自定义包(app)并列。
如果你不想从零开始,Dojo Boilerplate为你准备好了一切。

为了使用Dojo构建系统,包根目录中必须有两个文件。
第一个是package.json。它是CommonJS/1.0包描述符。
第二个是build profile。它指导构建工具处理包的内容。它有两种命名方式<package_name>.profile.js或者package.js

包描述符

包描述符中有很多属性,包括包名、包的依赖、许可证信息、贡献者信息、bug追踪信息等等。
其中,dojoBuild指示build profile文件所在位置。

{
    "name": "app",
    "description": "My Application.",
    "version": "1.0",
    "keywords": ["JavaScript", "Dojo", "Toolkit", "DojoX"],
    "maintainers": [{
        "name": "Kitson Kelly"
    }],
    "contributors": [{
        "name": "Kitson Kelly"
    },{
        "name": "Colin Snover"
    }],
    "licenses": [{
        "type": "AFLv2.1",
        "url": "http://bugs.dojotoolkit.org/browser/dojox/trunk/LICENSE#L43"
    },{
        "type": "BSD",
        "url": "http://bugs.dojotoolkit.org/browser/dojox/trunk/LICENSE#L13"
    }],
    "bugs": "https://github.com/example/issues",
    "repositories": [{
        "type": "git",
        "url": "http://github.com/example.git",
        "path": "packages/app"
    }],
    "dependencies": {
        "dojo": "~1.10.4",
        "dijit": "~1.10.4",
        "dojox": "~1.10.4"
    },
    "main": "src",
    "homepage": "http://example.com/",
    "dojoBuild": "app.profile.js"
}

The Package Build Profile

build profile是构建系统中的主要的配置文件。这个js文件中包含一个叫profile的对象。profile中存储了所有的指令,用于创建全功能构建。
最基本的build profile如下图所示。

var profile = (function(){
    return {
        resourceTags: {
            amd: function(filename, mid) {
                return /\.js$/.test(filename);
            }
        }
    };
})();

注意这里是一个立即调用的函数表达式IIFE(Imdiately Invoked Function Expression)。这有助于你的profile与环境中其他代码隔离。这样,你就可以随意发挥,写出更复杂的profile。
如果profile不是你程序中的主profile,那么该profile中仅需要包含resourceTags指令。
resourceTags指令是一个对象。对象的key是标签,值是函数,用来判断文件是否匹配该标签。
当构建工具读取包的内容时,会把每个文件都传递给resourceTags函数集,为文件打标签,分类。

标签如下所示:

标签 含义
amd 资源是AMD模块
declarative 资源使用了声明式语法,你想扫描它们的依赖
test 资源是测试代码
copyOnly 该属性为真时,资源应该被直接复制到目的位置;否则不要动
miniExclude 当mini属性为真时,资源不应该被复制到目的位置

如果你没有为AMD模块的代码打上amd标签,构建工具会发出抱怨,但还是会处理。你最好能够为你的代码精准地打上标签。
declarative标签超出了本篇的范围,如果你想了解更多的信息,请查看depsDeclarative

正确的标签是构建工具正确工作的前提。这里我们假设src/app/tests目录是你的测试代码(所有好的开发者都写单元测试,是这样的吧?)。另外,package.json以及其他一些文件你只想原封不动地复制到目的位置。所以,一份更加复杂的build profile如下图所示。

var profile = (function(){
    var testResourceRe = /^app\/tests\//,
        // checks if mid is in app/tests directory

        copyOnly = function(filename, mid){
            var list = {
                "app/app.profile": true,
                // we shouldn't touch our profile
                "app/package.json": true
                // we shouldn't touch our package.json
            };
            return (mid in list) ||
                (/^app\/resources\//.test(mid)
                    && !/\.css$/.test(filename)) ||
                /(png|jpg|jpeg|gif|tiff)$/.test(filename);
            // Check if it is one of the special files, if it is in
            // app/resource (but not CSS) or is an image
        };

    return {
        resourceTags: {
            test: function(filename, mid){
                return testResourceRe.test(mid) || mid=="app/tests";
                // Tag our test files
            },

            copyOnly: function(filename, mid){
                return copyOnly(filename, mid);
                // Tag our copy only files
            },

            amd: function(filename, mid){
                return !testResourceRe.test(mid)
                    && !copyOnly(filename, mid)
                    && /\.js$/.test(filename);
                // If it isn't a test resource, copy only,
                // but is a .js file, tag it as AMD
            }
        }
    };
})();

正如你所见,build profile很快变得复杂起来。但是,基本上讲了一件事,那就是profile对象需要包含resourceTags函数集。你可以利用强大的JavaScript理清资源与标签的对应关系。
这里有一个profile示例:dgrid Profile

应用程序级别 Build Profile

如果你只想将包合并到整个build中,打好标签就行了。但是,真正做出在生产环境下有用的build,还需要其他配置。

  • 如果你的程序比较简单,你只有一个自定义包,那么你可能只想创建一个比较全的配置文件就行了。
  • 如果你的程序比较复杂,有好几个自定义包; 或者你想为为不同的构建创建不同的配置文件,那么你应该创建应用程序级别的build profile。

在这篇教程的余下部分中,我们假设你将创建应用程序级别的build profile,命名为myapp.profile.js,放在你的应用程序的根目录下。

下表列出了一些关键配置项。

选项 类型 描述
basePath Path build的根路径,相对于build profile所在目录
releaseDir Path build的释出目录,builder会尝试创建该目录,会覆盖目录下的所有东西,该路径相对于basePath
releaseName String build的名字,如果你想释出release/prd,那么releaseDir设置为release,releaseName设置为prd
action String 应该设置为release
package Array 这是builder在映射模块时会用到的包信息数组。它提供了灵活性,可以将不同位置的包拉到一起构建
layers Object 这允许你创建不同的层模块,每个层都会形成一个单独的文件

所以假设我们将要构建一个application profile,我们想构建出用于在单页面应用中加载的两个文件。其中一个文件包含我们将要作为依赖使用的大多数代码。另一个文件,我们在确定的情景下有条件地加载。

var profile = (function(){
    return {
        basePath: "./src",
        releaseDir: "../../app",
        releaseName: "lib",
        action: "release",

        packages:[{
            name: "dojo",
            location: "dojo"
        },{
            name: "dijit",
            location: "dijit"
        },{
            name: "dojox",
            location: "dojox"
        },{
            name: "app",
            location: "app"
        }],

        layers: {
            "dojo/dojo": {
                include: [ "dojo/dojo", "dojo/i18n", "dojo/domReady",
                    "app/main", "app/run" ],
                customBase: true,
                boot: true
            },
            "app/Dialog": {
                include: [ "app/Dialog" ]
            }
        }
    };
})();

如果现在按照上面的profile构建,最终会在app/lib目录下得到两个文件app/lib/dojo/dojo.js和app/lib/app/Dialog.js。这两个文件包含了上面四个包的所有模块和资源。

后面,我们会更加深入讲解层的复杂性。

构建优化

Just building a build won't necessarily give you everything you might want out of a build. The builder does default to a situation where any layers built will be minified, but the rest of your build will be essentially "left alone". There are several other build profile knobs/options you should consider:
仅仅完成一个构建并不能给你所有你想要的功能。下表是你应该考虑的其他一些build profile选项:

选项 类型 描述
layerOptimize String/Boolean 为层设置压缩. 默认为 shrinksafefalse意味着不压缩。其他有效值分别有shrinksafe.keeplinesclosureclosure.keeplinescommentcomment.keeplines
optimize String/Boolean 为不是层的一部分的模块设置压缩。默认为false。有效值与layerOptimize相同。
cssOptimize String/Boolean 处理已经优化过的输出CSS。默认为false。值为comments时会剥离注释、额外行以及内嵌@import命令引用的代码。值为A value of <code>"comments.keepLines" strips the comments and inlines the @imports, but preserves any line breaks.
mini Boolean This determines if the build is a "mini" build or not. If true it will exclude files that are tagged as miniExclude which is typically things like tests, demos and other items not required for the build to work. This defaults to false.
stripConsole String This determines how console handling is dealt with in the output code. This defaults to "normal" which strips all console messages except console.error and console.warn. It is important to note though, this feature only applies when there is a level of optimization going on, otherwise it is ignored. Other possible values are "none", "warn" and "all".
selectorEngine String This identifies the default selector engine for the build and builds it into the code. While this does not directly make the code smaller, it ensure that a selector engine won't require another call to be loaded. It defaults to nothing and the two engines included with Dojo are lite and acme.
staticHasFeatures Object This is a hash of features that you are "forcing" to be on or off for the build. When coupled with the Closure Compiler, this allows dead code path removal. We will talk in detail later about the specific values that can be set.

死代码移除

如上所述,staticHasFeature与Closure Compiler合作,可以极大地优化代码。Dojo通过has API进行特征检测。与某个特征相关的代码被需要时,if(has("some-feature")){...}被加进代码。你可以通过staticHasFeature将特征相关的代码硬编码,加入最终的构建输出中。如果某个特征被禁用,Closure Compiler会检测出相关的代码不可达,并从构建输出文件中移除这些代码。

但是,需要注意的是,这样创建出的构建版本与未构建版本不能一样地工作。因此,你需要仔细考虑目标环境,并对构建的代码进行一定程度的测试,以确保它按预期工作。

特征 设置 描述
config-deferredInstrumentation 0 禁用自动加载用来报告未处理的被拒绝promise的代码
config-dojo-loader-catches 0 在加载模块时禁用一些错误处理
config-tlmSiblingOfDojo 0 禁用非标准模块解析代码
dojo-amd-factory-scan 0 假设所有模块都是AMD模块
dojo-combo-api 0 禁用一些遗留加载器API
dojo-config-api 1 确保构建是可配置的
dojo-config-require 0 通过require()禁用配置
dojo-debug-messages 0 禁用一些诊断信息
dojo-dom-ready-api 0 确保DOM ready API可用
dojo-firebug 0 禁用Firebug Lite。Firebug Lite专门为没有开发者控制台而准备,比如IE6
dojo-guarantee-console 1 确保在没有控制台的浏览器中控制台是可用的,比如IE6
dojo-has-api 1 确保has 特征检测API可用
dojo-inject-api 1 确保支持跨域加载模块
dojo-loader 1 确保加载器是可用的
dojo-log-api 0 禁用加载器记录日志代码
dojo-modulePaths 0 Removes some legacy API related to loading modules
dojo-moduleUrl 0 Removes some legacy API related to loading modules
dojo-publish-privates 0 禁止暴露加载器内部信息
dojo-requirejs-api 0 不支持RequireJS
dojo-sniff 1 使能在dojo.js script标签内扫描data-dojo-config and djConfig
dojo-sync-loader 0 禁用遗留的加载器
dojo-test-sniff 0 禁用一些测试用的特征
dojo-timeout-api 0 禁用关于处理没有加载模块的代码
dojo-trace-api 0 禁用模块加载跟踪
dojo-undef-api 0 不支持模块卸载
dojo-v1x-i18n-Api 1 Enables support for v1.x i18n loading (required for Dijit)
dom 1 确保DOM代码可用
host-browser 1 Ensures the code is built to run on a browser platform
extend-dojo 1 Ensures pre-Dojo 2.0 behavior is maintained

为了让它生效,我们需要将下面的配置放到profile中。

staticHasFeatures: {
    "config-deferredInstrumentation": 0,
    "config-dojo-loader-catches": 0,
    "config-tlmSiblingOfDojo": 0,
    "dojo-amd-factory-scan": 0,
    "dojo-combo-api": 0,
    "dojo-config-api": 1,
    "dojo-config-require": 0,
    "dojo-debug-messages": 0,
    "dojo-dom-ready-api": 1,
    "dojo-firebug": 0,
    "dojo-guarantee-console": 1,
    "dojo-has-api": 1,
    "dojo-inject-api": 1,
    "dojo-loader": 1,
    "dojo-log-api": 0,
    "dojo-modulePaths": 0,
    "dojo-moduleUrl": 0,
    "dojo-publish-privates": 0,
    "dojo-requirejs-api": 0,
    "dojo-sniff": 1,
    "dojo-sync-loader": 0,
    "dojo-test-sniff": 0,
    "dojo-timeout-api": 0,
    "dojo-trace-api": 0,
    "dojo-undef-api": 0,
    "dojo-v1x-i18n-Api": 1,
    "dom": 1,
    "host-browser": 1,
    "extend-dojo": 1
},

var profile = {
    layers: {
        "app/main": {
            include: [ "app/main" ],
            exclude: [ "app/mail", "app/calendar" ]
        },
        "app/mail": {
            include: [ "app/mail" ],
            exclude: [ "app/main" ]
        },
        "app/calendar": {
            include: [ "app/calendar" ],
            exclude: [ "app/main" ]
        }
    }
};

默认配置

将上面所有的配置集中成一个build profile

var profile = (function(){
    return {
        basePath: "./src",
        releaseDir: "../../app",
        releaseName: "lib",
        action: "release",
        layerOptimize: "closure",
        optimize: "closure",
        cssOptimize: "comments",
        mini: true,
        stripConsole: "warn",
        selectorEngine: "lite",

        defaultConfig: {
            hasCache:{
                "dojo-built": 1,
                "dojo-loader": 1,
                "dom": 1,
                "host-browser": 1,
                "config-selectorEngine": "lite"
            },
            async: 1
        },

        staticHasFeatures: {
            "config-deferredInstrumentation": 0,
            "config-dojo-loader-catches": 0,
            "config-tlmSiblingOfDojo": 0,
            "dojo-amd-factory-scan": 0,
            "dojo-combo-api": 0,
            "dojo-config-api": 1,
            "dojo-config-require": 0,
            "dojo-debug-messages": 0,
            "dojo-dom-ready-api": 1,
            "dojo-firebug": 0,
            "dojo-guarantee-console": 1,
            "dojo-has-api": 1,
            "dojo-inject-api": 1,
            "dojo-loader": 1,
            "dojo-log-api": 0,
            "dojo-modulePaths": 0,
            "dojo-moduleUrl": 0,
            "dojo-publish-privates": 0,
            "dojo-requirejs-api": 0,
            "dojo-sniff": 1,
            "dojo-sync-loader": 0,
            "dojo-test-sniff": 0,
            "dojo-timeout-api": 0,
            "dojo-trace-api": 0,
            "dojo-undef-api": 0,
            "dojo-v1x-i18n-Api": 1,
            "dom": 1,
            "host-browser": 1,
            "extend-dojo": 1
        },

        packages:[{
            name: "dojo",
            location: "dojo"
        },{
            name: "dijit",
            location: "dijit"
        },{
            name: "dojox",
            location: "dojox"
        },{
            name: "app",
            location: "app"
        }],

        layers: {
            "dojo/dojo": {
                include: [ "dojo/dojo", "dojo/i18n", "dojo/domReady",
                    "app/main", "app/run" ],
                customBase: true,
                boot: true
            },
            "app/Dialog": {
                include: [ "app/Dialog" ]
            }
        }
    };
})();

开始构建

结论

构建系统对于部署Web应用很重要。通过Dojo 1.10的异步加载机制,构建过的应用比未构建过的应用加载更快。加载时间是影响用户体验的关键因素,因此不要释出未构建过的应用。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,494评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,047评论 25 707
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,650评论 2 59
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,678评论 6 342
  • 表者, 若皮毛覆于身也! 源于古者衣,多自为裘矣! 本意为皮衣, 申为外衣也! 亦可做人之外貌, 外表当为其例! ...
    灰常出色阅读 450评论 0 3