前端应用程序的构建过程





5.00/5 (4投票s)
自动化构建前端应用程序的任务
引言
我撰写此文的目的是概述当前端应用程序发生代码更改时如何设置构建过程。我不会深入探讨用于演示的每项技术,例如 Angular,但本文的目的是通过一个简单的例子来说明我们在构建代码时可以设置哪些类型的任务以及如何进行设置。
构建过程
多年来,应用程序开发过程已得到很大改进。如今,构建应用程序除了编译之外,还包括许多任务,这些任务是每个开发人员在代码准备好部署到不同环境之前都会遵循的。
- 编写代码(至少)
- 编写单元测试用例以覆盖代码(代码优先或单元测试优先,即 TDD 方法)
- 每当代码发生更改时,执行所有测试用例以防止破坏现有功能
- 检查代码质量(编码风格/代码分析工具)
- 生成可执行文件(以发布模式构建),以便部署到各种环境
如果您是服务器端开发人员,您可能已经看到过借助某些工具(例如 MS-Build)强制执行此类相似实践。
前端开发与服务器端编程在许多方面都不同。首先,它通常是浏览器渲染的工件(如 JavaScript、CSS 和 HTML 代码)的混合体,甚至不使用编译器,因此很难找到错误。其次,它有一套不同的工具来创建/执行测试用例。最后,一个重要因素是脚本和 CSS 的压缩,这可以提高 UI 性能。在代码提交到版本控制之前,必须有一个构建过程来强制每个开发人员验证编码风格、测试用例执行/覆盖率。一旦所有检查都完成,它还应该在压缩后准备好输出文件。
我将使用流行的工具 Gulp 来在我们的示例中启用这样的自动化过程,这将有助于在任何代码更改发生时执行以下任务。
- "步骤 1 - 检查编码风格" - 验证 JavaScript 代码中的任何错误,如果存在任何错误,构建过程将失败
- "步骤 2 - 执行测试用例" - 执行应用程序中的所有测试用例,以确保更改未影响任何现有功能,如果任何测试用例失败,构建过程将失败
- "步骤 3 - 压缩文件" - 为输出文件夹准备所有所需 JavaScript 文件的压缩版本
- "步骤 4 - 复制输出文件" - 将所有 HTML 文件、JavaScript 文件复制到输出文件夹
- "步骤 5 - 复制依赖项" - 将依赖文件的压缩版本复制到输出文件夹
- "步骤 6 - 替换引用" - 替换 HTML 页面中的引用以使用压缩的 JavaScript 文件
一旦以上所有步骤成功完成,Output 文件夹将准备好部署到任何环境。
Using the Code
在我们的示例中,我们将创建一个 HTML 页面来显示图书馆中书籍的总数。在创建此演示时,我使用了以下工具,但这并非严格的强制选择。
- 编辑器 - Visual Studio 2015
- JavaScript 框架 - Angular JS
- 测试 - Jasmine / Karma
- 构建过程 - Gulp JS。
首先,我们需要安装一些包:Angular、Karma、Jasmine、Gulp 及其插件。我创建了一个批处理文件,您可以从本文顶部的链接下载。
请确保您的机器上已安装 Node JS。
call npm init --save-dev
call npm install gulp --save-dev
call npm install gulp-jshint --save-dev
call npm install gulp-jscs --save-dev
call npm install gulp-uglify --save-dev
call npm install gulp-replace --save-dev
call npm install gulp-clean-dest --save-dev
call npm install gulp-rename --save-dev
call npm install jasmine --save-dev
call npm jasmine init --save-dev
call npm install angular --save-dev
call npm install angular-mocks --save-dev
call npm angular init --save-dev
call npm install karma --save-dev
call npm install karma-jasmine --save-dev
call npm install karma-chrome-launcher --save-dev
call npm install karma-Phantomjs-launcher --save-dev
call npm karma init --save-dev
我们可以独立运行命令来安装所有这些插件。但将这些预设放在批处理文件中可以简化维护,特别是当预设发生更改,或者团队中新开发人员首次设置他/她的本地环境时。
批处理文件完成后,您会在解决方案中找到一个新文件夹“node_modules”。我们将从“node_modules”中复制以下文件并将其放置在根文件夹中。
- package.json - 文件包含所有插件
- karma.conf.js - 文件包含 Karma(测试运行器)的设置。您需要更新有关 JavaScript 文件、浏览器等您想要使用的设置
- .jscsrc - JavaScript 风格的规则集。您可以根据需要更新一些规则
安装所有先决条件后,我们将在项目的 scripts 文件夹下添加一个 JavaScript 文件“BooksList.js”。此脚本将包含一个函数,用于返回“书籍总数”。我创建了一个 Angular 控制器来引入函数“GetTotalNumberOfBooks
”,该函数返回硬编码值 10
。如果您是 Angular 新手,Angular 中的模块作为容器,其中包含控制器、服务等其他部分。Angular 控制器是一个 JavaScript 类,用于运行视图的业务逻辑,而 $scope
是一个 Angular 对象,用于在控制器和视图之间共享数据。互联网上有大量的 Angular 教程可供参考,以获取更多详细信息。
var booksModule = angular.module("booksModule",[]);
booksModule.controller("booksCtrl",["$scope", function ($scope) {
$scope.GetTotalNumberOfBooks = function () {
return "10";
};
}]);
});
现在我们的函数已经准备好,我们再添加一个 JavaScript 文件来引入一个测试用例,它将测试函数的结果。我们将使用 JasmineJS 编写测试用例。Angular-mock-js 将为您提供在测试用例中使用的 Angular 对象。在 Jasmine 中,我们以“describe
”块开始测试用例,该块可以包含多个定义为“it
”块的测试用例。BeforeEach
类似于 TestSetup
,我们可以在其中编写一些代码,这些代码将由所有测试用例执行。目前,我们的 BooksList.js 在我们的示例中只有一个测试用例,但是由于我们在 BeforeEach
中注入了 Angular 对象,它将适用于同一 describe 块中的任何新测试。
var scope, ctrl;
describe("BookTests", function () {
beforeEach(angular.mock.module("booksModule"));
describe("BooksCount", function () {
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
ctrl = $controller("booksCtrl", { $scope: scope });
}));
it("GetTotalNumberOfBooks", function () {
expect(scope.GetTotalNumberOfBooks()).toBe(10);
})
})
})
让我们添加 App.HTML 文件,以便用户在浏览器上查看书籍总数的结果。Angular 指令用于在 UI 中访问 Angular 对象。我们正在使用两个指令——“ng-app
”用于加载我们的模块“booksModule
”,然后“ng-controller
”用于访问控制器。另外,请注意我们正在使用两个引用“angular.js”和“BooksList.js”。在本文后面,我们将在构建过程中创建一些自动化任务,将我们的 Angular 依赖项更改为“angular.min.js”,并将“BooksList.js”压缩为“BooksList.min.js”,并自动替换 HTML 中的引用。
<!DOCTYPE html>
<html>
<head>
<title></title>
<script src="../node_modules/angular/angular.js"></script>
<script src="/scripts/BooksList.js"></script>
<meta charset="utf-8" />
</head>
<body>
<div ng-app="booksModule" ng-controller="booksCtrl">
<p>Total number of books are </p> {{GetTotalNumberOfBooks()}}
</div>
</body>
</html>
现在我们的 UI 和 JavaScript 函数都已准备就绪,我们将开始创建构建过程。为此,我们需要创建另一个 JavaScript 文件“Gulpfile.js”。在这个文件中,我们将通过创建 Gulp 任务来启用构建过程中的所有步骤。但在创建任务之前,我们需要使用“require
”方法导入插件。我们可以使用“require(gulp-load-plugins)”导入所有插件,但对于这个示例,我们不打算这样做,这样我们就可以讨论我们将使用的每个插件。
var gulp = require("gulp");
var jscs = require("gulp-jscs");
var notify = require("gulp-notify")
var uglify = require("gulp-uglify");
var clean = require("gulp-clean-dest");
var util = require("gulp-util");
var replace = require("gulp-replace");
var Server = require('karma').Server;
现在我们将为“步骤 1 - 检查编码风格”创建任务,为此,我们将使用“gulp
”和“gulp-jscs
”。gulp
对象将用于创建任务,以调用函数对 JavaScript 文件执行风格检查。gulp.src 函数用于提供源文件名称,该函数在输入参数中接受单个值或数组(如果存在多个源文件)。
Gulp 中的 Pipe 函数用于串联我们希望在特定任务中使用的插件的执行。在我们的“验证风格”任务中,我们正在 pipe 中调用两个插件。JSCS 将根据 .jscsrc 文件中定义的规则(我们在前面的步骤中将其复制到根文件夹)执行所有风格检查。
gulp.task("ValidatingStyles", function () {
return gulp.src("scripts/BooksList.js")
.pipe(jscs())
.pipe(jscs.reporter())
})
});
如果与规则不匹配,Reporter 函数用于在屏幕上提供所有风格错误的详细信息。它将显示错误,如下图所示。
添加 reporter('fail')
将导致我们的构建过程在发现某些错误时失败
。
gulp.task("ValidatingStyles", function () {
return gulp.src("scripts/BooksList.js")
.pipe(jscs())
.pipe(jscs.reporter())
.pipe(jshint.reporter('fail'))
})
});
在成功验证 JavaScript 函数中的编码风格后,我们将创建另一个 gulp 任务来实施我们的“步骤 2 - 执行测试用例”。在这里,我们将使用 Karma 执行所有测试用例。Karma 是 JavaScript 测试用例的测试运行器。我们将使用 PhantomJS 启动 Karma。PhantomJS 是一种没有用户界面的浏览器,但它会启动测试用例的环境。我们将为此任务传递配置文件“karma.conf.js”,该文件已在之前的步骤中复制到根文件夹。一旦 gulp 任务运行,它将首先通过启动 PhantomJS 来启动 karma 服务器,并执行“karma.conf.js”设置中定义的文件中的所有测试用例。
gulp.task("RunTests", function (done) {
new Server({
configFile: __dirname + '\\karma.conf.js',
singleRun: true
}, done).start();
});
RunTests
的结果将如下所示
一旦我们通过样式验证和测试用例执行确保了代码质量,我们将继续创建下一步“步骤 3 - 压缩文件”。压缩我们的 JavaScript 文件。我们将创建任务“Uglfying”,它将以“BooksList.js”作为源文件。Gulp-Uglify 插件将被调用来压缩文件。
此外,由于我们希望这个压缩文件用于各种环境,我们将其放置到我们的目标文件夹“Output”中。Dest 函数用于指定目标位置。Gulp-rename 插件将用于在将“BooksList.js”放置到输出文件夹时将其重命名为“BooksList.min.js”。
这将通过将 JavaScript 文件复制到 Output 文件夹中,部分地实现我们的下一步“步骤 4 - 复制输出文件”。
gulp.task("Uglfying", function () {
return gulp.src(["scripts\/BooksList.js"])
.pipe(clean("./output"))
.pipe(uglify())
.pipe(rename({suffix: ".min"}))
.pipe(gulp.dest("./output"))
})
"步骤 5 - 复制依赖项" - 现在我们将确保所有必需的依赖项也放置在 output 文件夹中。在我们的例子中,HTML 文件需要 angular.min.js。因此,我们将 angular.min.js 复制到 output 文件夹。但由于这只是依赖项,我们不希望每次构建应用程序时都复制此文件。我们将使用“fs.existsSync
”函数来检查文件是否存在于输出文件夹中。
gulp.task("CheckDependencies", function () {
var sourceFile = "node_modules/angular/angular.min.js";
var destFile = "output/angular.min.js";
if (!(fs.existsSync(destFile))) {
return gulp.src(sourceFile)
.pipe(gulp.dest("./output"))
}
});
"步骤 6 - 替换引用" 是此过程的最后一步。现在我们已将 JavaScript 压缩文件放入输出文件夹。我们将创建另一个任务,它将复制“App.html”,同时将 JavaScript 引用替换为使用压缩文件。这里使用 Gulp-replace
插件来替换文件名。
gulp.task("ReplaceTask", function () {
return gulp.src("App.html")
.pipe(replace("scripts\/BooksList.js", "BooksList.min.js"))
.pipe(replace("node_modules\/angular\/angular.js", "angular.min.js"))
.pipe(gulp.dest("./output"));
})
所以,我们的构建过程已经准备就绪,现在我们将创建另一个 Gulp 任务来安排我们的任务序列。
"Build All" 任务将依赖于 "ValidateCode
"、"RunTests
"、"MinifyFiles
"、"CopyFiles
"、"RenameReferences
",这意味着,当我们执行 "BuildAll
" 时,它将首先检查所有编码风格,然后执行测试。测试用例成功执行后,它将压缩 JavaScript 文件并将其与 Angular JS 和 HTML 文件一起复制到输出文件夹。最后,它将最终替换 HTML 页面中所有 JavaScript 引用为压缩版本。这很简单。
gulp.task("BuildAll", ["ValidatingStyles", "RunTests", "Uglfying",
"CheckDependencies", "ReplaceTask"], function () {
console.log("Build completed succesfully");
})
大多数情况下,上述任务应该足以启用我们预期的构建任务,但 Gulp 会并行激活所有这些任务,这可能无法按照我们期望的正确顺序执行。
要同步运行所有任务,我们可以使用“gulp-sync
”插件
gulp.task("BuildAll", gulpsync.sync
(["ValidatingStyles", "RunTests", "Uglfying", "CheckDependencies", "ReplaceTask"]), function () {
console.log("Build completed succesfully");
})
现在打开命令提示符,使用 Gulp BuildAll 执行测试用例,它将按顺序执行所有任务以准备 Output 文件夹。
一旦 BuildAll
任务成功完成,您可能会发现在项目根文件夹下创建了一个 Output 文件夹。尝试浏览 App.html 并查看源代码。您会发现所有引用文件都映射到输出文件夹下的压缩文件。
我们就是这样管理代码更改的质量的,我已将此演示中使用的所有脚本文件放置在下载链接下。您可以将这些文件下载到任何空的 Web 应用程序中,并运行“build.bat”文件来安装所有插件。
关注点
在本文中,我们探讨了 Gulp 的几个插件,以设置一些可以确保构建过程质量的任务。有许多有用的插件可以帮助我们做更多的事情,例如,报告代码覆盖率、记录失败等。在后续文章中,我们将深入探讨更多最佳实践。