启动 AngularJS 项目也意味着选择与 Angular 配套的整个工具链。在这篇博文中,我们将提出一套新的 Angular 项目可能需要的工具,并展示一个提供这些功能的 简单 Gulp 构建 。
让我们讨论以下主题:
- 为什么选择 Gulp?基线 Angular 构建的目标
- 为什么使用 CSS 预处理器,以及为什么使用 Sass
- 模块系统的优点,以及为什么使用 browserify
- 预填充 Angular 模板缓存
- 角度友好的 JavaScript 缩小
- 使用精灵减少 HTTP 请求的数量
- 设置开发 Web 服务器
- 使用 Karma 运行测试,使用 JSHint 运行代码质量,等等
为什么选择 Gulp?
尽管还有其他任务运行程序,但 Gulp 似乎越来越成为首选解决方案,因为它提供了以下功能:
- 它非常快,因为它从一开始就是为并发而构建的
- 它提供了一个易于学习的 API(基于管道和过滤器架构),可以生成一些非常易读和可维护的代码。
一个好的 Angular 构建的目标
理想情况下,Angular 项目的构建应该:
- 复杂性低,最好不要超过 100 行代码
- 快如闪电,大约几秒钟
- 暗示不依赖于非节点工具链
- 在开发和生产中完全相同
- 解决一些特定于 Angular 的问题:Angular 友好的 Javascript 缩小和模板缓存预填充
- 提供开发者生产力的开发模式
- 允许运行测试并进行代码质量检查
- 通过 CSS 和 Javascript 捆绑以及图像精灵,最大限度地减少生产中所需的 HTTP 请求数量
工作构建和示例应用程序
为了使其更具体,这里是 构建 并将其应用于示例应用程序:来自 TODO MVC 项目的 TODO 应用程序。您可以 在此处 试用示例应用程序。
该
Github 存储库
中还提供了构建和示例应用程序。
在接下来的部分中,我们将讨论一个为 Angular 项目提议的工具链,并浏览提供这些功能的 Gulp 构建。
为什么使用 CSS 预处理器,以及为什么使用 Sass
引用去年的年度 Thoughworks 技术雷达 :
我们相信手写 CSS 的时代已经结束,除了琐碎的工作。
纯 CSS 的局限性很大,很难编写可维护的样式表。尽管新的 CSS3 标准解决了许多限制,但广泛采用需要时间。
CSS 增强功能
为了生成可维护的 CSS,我们今天确实需要:
- 一种CSS打包机制
- 一种 partials 机制,允许将 CSS 拆分为逻辑块,而不生成额外的 http 请求
- 定义有限范围的 CSS 变量的可能性
- 在树结构中嵌入相关样式的可能性
- 定义 CSS“功能”的能力
所有这些功能都由当今可用的多种 CSS 预处理器提供,困难在于如何选择一个。两个主要的是 Sass 和 Less 。
许多库都建立在这些封装常用模式的预处理器之上,例如 Sass Compass 库。
Sass 还是更少?
Less 预处理器似乎是基于节点构建的更好选择,因为它也是基于节点的工具。
但是今天 Sass 不再依赖于 Ruby 工具链,因为有一个 Sass ( libsass ) 的 C 实现,它允许通过 gulp-sass 等插件在节点环境中快速编译 Sass。
此外,该行业似乎正在围绕 Sass 进行融合:例如,请参阅这篇关于 css-tricks 的帖子。最后,我决定选择 Sass 的三个主要原因是:
- 通过 libsass 与节点良好集成,无需安装 Ruby
- 知道最常用的 Sass 库 Compass 正在为 Compass 2 移植到 libsass,请参阅此 Github 问题
- Angular Material Design 基于 Sass 的 事实,所以我们很可能在未来再次找到它
构建 css 任务
Sass 通过下面的
build-css
任务集成到 gulp 构建中:
gulp.task('build-css', ['clean'], function() {
return gulp.src('./styles/*')
.pipe(sourcemaps.init())
.pipe(sass())
.pipe(cachebust.resources())
.pipe(sourcemaps.write('./maps'))
.pipe(gulp.dest('./dist'));
});
支持源映射,因此可以在浏览器中调试 CSS 并使其引用原始 Sass 源。稍后将详细介绍缓存清除功能。
为什么使用模块系统
模块系统对于任何重要的 Javascript 项目来说都是必不可少的,原因有以下几个:
- 随着代码库和团队的增加,在多个 html 页面底部的脚本标签中添加所有 Javascript 依赖项很快就会崩溃
- 那么连接和缩小呢?在构建级别的串联任务中重复脚本标签的内容很快变得难以管理
除了开箱即用地解决这些常见问题外,使用模块系统还鼓励:
- 开发具有明确边界和依赖关系的隔离良好的小模块
- 将代码组织成易于理解的小块
- 简化并更好地启用测试
但这个行业在模块系统方面非常分散:有 requireJS 、 CommonJS 、 browserify 、 ES6 模块 等等。那么选择哪一个呢?
为什么 CommonJS 如此吸引人
CommonJS 与节点通常使用的模块系统相同。它非常易于理解和使用。如果我们需要一个模块,例如 express ,我们只需说:
gulp.task('build-css', ['clean'], function() {
return gulp.src('./styles/*')
.pipe(sourcemaps.init())
.pipe(sass())
.pipe(cachebust.resources())
.pipe(sourcemaps.write('./maps'))
.pipe(gulp.dest('./dist'));
});
导入模块是同步的并且非常直观。这种语法的主要优点是不需要定义声明所有依赖项的配置文件:我们只需要指向入口文件,
require
调用本身将隐式记录 Javascript 文件的依赖树。
CommonJS 曾经是一个仅限后端的模块系统,直到 browserify 出现,允许我们在前端也使用同样熟悉的语法。
Angular 现在正式支持 Browserify
最近在 Angular 发布的 1.3.14 instantaneous-browserification 中,Angular 团队添加了对 browserify 的改进支持。
也就是说,它在 npm 中将所有 Angular 文件发布为 CommonJS 模块,为它们提供有意义的导出,从而简化 browserify 的使用。
尽管 ES6 模块将成为 Angular 2 的首选解决方案,但似乎 browserify 是 Angular 1 项目的推荐解决方案。
Build-js Gulp 任务
回到我们的构建,以下是构建我们应用程序的 Javascript 包的任务:
gulp.task('build-css', ['clean'], function() {
return gulp.src('./styles/*')
.pipe(sourcemaps.init())
.pipe(sass())
.pipe(cachebust.resources())
.pipe(sourcemaps.write('./maps'))
.pipe(gulp.dest('./dist'));
});
为什么不使用 Gulp-Browserify 插件
请注意,browserify 是直接使用的,而不是通过 gulp-browserify 等插件使用的。这是因为 gulp 团队已经将几个插件列入黑名单,例如 gulp-browserify,因为它们会产生难以与其他 gulp 插件很好集成的独立构建过程。
相反,gulp 团队提供了如何使用 browserify 的 方法 :我们只需直接调用它,然后通过管道将其输出(包括包装在模块中的串联 js 文件)传输到其他 gulp 任务。
Gulp 配方适用于修复一个特定于 Angular 的问题:缩小支持。
角度友好的 Javascript 缩小
Angular 以在缩小之前需要 额外的构建步骤 以支持其依赖注入机制而闻名。以此代码行为例:
gulp.task('build-css', ['clean'], function() {
return gulp.src('./styles/*')
.pipe(sourcemaps.init())
.pipe(sass())
.pipe(cachebust.resources())
.pipe(sourcemaps.write('./maps'))
.pipe(gulp.dest('./dist'));
});
如果我们不采取任何预防措施,这将缩小为类似以下内容:
gulp.task('build-css', ['clean'], function() {
return gulp.src('./styles/*')
.pipe(sourcemaps.init())
.pipe(sass())
.pipe(cachebust.resources())
.pipe(sourcemaps.write('./maps'))
.pipe(gulp.dest('./dist'));
});
问题是变量名
$http
丢失了,因此 Angular 无法再通过将变量名
$http
与
Provider
连接起来找到它标识的
$httpProvider
来注入它。如果缩小,这会导致应用程序中断。
在缩小之前,需要将上面的行重写为另一种数组语法,该语法保留字符串名称但有点冗长:
gulp.task('build-css', ['clean'], function() {
return gulp.src('./styles/*')
.pipe(sourcemaps.init())
.pipe(sass())
.pipe(cachebust.resources())
.pipe(sourcemaps.write('./maps'))
.pipe(gulp.dest('./dist'));
});
修复角度缩小问题
修复它的一种方法是在任何地方简单地使用数组语法,因为这仍然是可读的并且避免了额外的构建步骤。
或者,将 browserify 转换
browserify-annotate
包含在上面的
build-js
任务中以解决此问题:
gulp.task('build-css', ['clean'], function() {
return gulp.src('./styles/*')
.pipe(sourcemaps.init())
.pipe(sass())
.pipe(cachebust.resources())
.pipe(sourcemaps.write('./maps'))
.pipe(gulp.dest('./dist'));
});
请注意,此转换几乎在所有地方都应用了数组语法,但我仍然必须手动将其应用于路由定义
resolve
步骤,请参阅文件
app.js
。
预填充 Angular 模板缓存
构建需要解决的另一个特定于 Angular 的问题是预填充 Angular 模板缓存。
如果我们不这样做,那么每个指令的每个模板都会产生一个单独的 HTTP 请求,导致应用程序启动时间慢得令人无法接受,因为应用程序需要在呈现页面之前等待模板加载。
使用 Sprites 减少 HTTP 请求的数量
构建提供的许多优化都围绕减少引导应用程序所需的 HTTP 请求数量展开。那么图像,例如图标呢?
减少与图像相关的 HTTP 请求数量的一种方法是将它们组合在一个图像精灵中,并仅加载它。构建的以下部分使用
gulp.spritesmith
插件生成精灵:
gulp.task('build-css', ['clean'], function() {
return gulp.src('./styles/*')
.pipe(sourcemaps.init())
.pipe(sass())
.pipe(cachebust.resources())
.pipe(sourcemaps.write('./maps'))
.pipe(gulp.dest('./dist'));
});
images
文件夹中的所有
png
图像都被转换为一个单一的
todo-sprite.png
文件。为了能够使用精灵,在文件
_todo-sprite.scss
中生成了一个 Sass mixin。 mixin 可以通过以下方式使用:
gulp.task('build-css', ['clean'], function() {
return gulp.src('./styles/*')
.pipe(sourcemaps.init())
.pipe(sass())
.pipe(cachebust.resources())
.pipe(sourcemaps.write('./maps'))
.pipe(gulp.dest('./dist'));
});
这将创建一组与图像文件同名的 CSS 类,允许使用不同的图像。例如图像
cal-right-button.jpg
可以通过以下方式使用:
gulp.task('build-css', ['clean'], function() {
return gulp.src('./styles/*')
.pipe(sourcemaps.init())
.pipe(sass())
.pipe(cachebust.resources())
.pipe(sourcemaps.write('./maps'))
.pipe(gulp.dest('./dist'));
});
为什么使用缓存清除?
在开发和生产中都派上用场的一个方便的做法是防止浏览器保留过时的副本
应用程序的 CSS / Javascript。
这可以防止客户查看不再与 Html 匹配的应用程序的陈旧版本,并避免有时难以链接回陈旧资源存在的错误报告。
实现缓存清除的最有效方法是向每个 CSS/Javascript 文件附加其内容的哈希值。这样当文件改变时,文件名也会改变,浏览器将加载最新版本的资源。
使用 Gulp-Cachebust 实现缓存清除
插件
gulp-cachebust
用于实现缓存清除功能。我们在
build-css
和
build-js
任务中看到了这样的调用:
gulp.task('build-css', ['clean'], function() {
return gulp.src('./styles/*')
.pipe(sourcemaps.init())
.pipe(sass())
.pipe(cachebust.resources())
.pipe(sourcemaps.write('./maps'))
.pipe(gulp.dest('./dist'));
});
此调用跟踪需要将其名称与文件哈希连接的 CSS 和 Javascript 文件。 css 文件名的实际替换是在构建任务中完成的,方法是调用
cachebust.references()
:
gulp.task('build-css', ['clean'], function() {
return gulp.src('./styles/*')
.pipe(sourcemaps.init())
.pipe(sass())
.pipe(cachebust.resources())
.pipe(sourcemaps.write('./maps'))
.pipe(gulp.dest('./dist'));
});
使用 Karma 运行测试
事实上的 Angular 测试工具之一是 Karma 测试运行器,而最常用的测试框架之一是 Jasmine 。以下 Gulp 任务允许从命令行针对无头 PhantomJS 浏览器运行 Jasmine 测试:
gulp.task('build-css', ['clean'], function() {
return gulp.src('./styles/*')
.pipe(sourcemaps.init())
.pipe(sass())
.pipe(cachebust.resources())
.pipe(sourcemaps.write('./maps'))
.pipe(gulp.dest('./dist'));
});
以下命令将启动测试套件中的所有测试:
gulp.task('build-css', ['clean'], function() {
return gulp.src('./styles/*')
.pipe(sourcemaps.init())
.pipe(sass())
.pipe(cachebust.resources())
.pipe(sourcemaps.write('./maps'))
.pipe(gulp.dest('./dist'));
});
JSHint 的代码质量
JSHint 是最常用的 Javascript linters 之一。以下 gulp 任务允许将其集成到我们的构建周期中:
gulp.task('build-css', ['clean'], function() {
return gulp.src('./styles/*')
.pipe(sourcemaps.init())
.pipe(sass())
.pipe(cachebust.resources())
.pipe(sourcemaps.write('./maps'))
.pipe(gulp.dest('./dist'));
});
开发 Web 服务器
在完成主要构建并使用
gulp
默认任务运行所有测试后,启动本地开发服务器很有用。
gulp-webserver
插件配置为通过以下方式执行此操作:
gulp.task('build-css', ['clean'], function() {
return gulp.src('./styles/*')
.pipe(sourcemaps.init())
.pipe(sass())
.pipe(cachebust.resources())
.pipe(sourcemaps.write('./maps'))
.pipe(gulp.dest('./dist'));
});
结论
借助 Gulp 任务运行器及其丰富的插件生态系统,可以创建具有 Angular 项目可能需要的最常用功能的基线 Angular 构建,并且仍然保持构建的复杂性相对较低。
让我们知道您对以下评论的看法,您的 Angular 构建是什么样的?