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 | 为层设置压缩. 默认为 shrinksafe。false意味着不压缩。其他有效值分别有shrinksafe.keeplines,closure,closure.keeplines, comment和comment.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的异步加载机制,构建过的应用比未构建过的应用加载更快。加载时间是影响用户体验的关键因素,因此不要释出未构建过的应用。