打造单页应用:从测试到路由配置的全流程指南
在开发过程中,我们已经掌握了使用 Jasmine 编写单元测试以及利用 Testdouble.js 模拟 Angular 提供的类的方法,这为测试 JavaScript 代码提供了有力的工具。接下来,我们将开启新的挑战,把简单的搜索界面转变为单页应用,这涉及到 Angular 路由器和导航服务的使用。
单元测试中的问题与改进思路
在测试代码中,我们使用了
td.verify
来验证
window.alert
是否被正确调用。例如:
window = td.object(["alert"]);
describe("CustomerSearchComponent", function() {
describe("A search that fails on the back-end", function() {
beforeEach(function() {
td.when(window.alert()).thenReturn();
component = new CustomerSearchComponent(mockHttp);
});
it("alerts the user with the response message", function() {
component.search("pat");
td.verify(window.alert("There was an error!"));
});
});
});
然而,这种方式存在明显的脆弱性,因为测试需要设置全局数据供生产代码读取。在 Angular 2 中,不像 Angular 1 有
$window
这样的抽象层。我们可以创建一个服务类来处理错误通知,并像注入
Http
一样注入它。同时,使用 Bootstrap 设计一个真正的警报组件可能会带来更好的用户体验。
存储视图模板到 HTML 文件
当前,视图模板以字符串形式存在于 JavaScript 中。当组件和视图增多时,管理这些字符串会变得困难。Webpack 的 raw loader 为我们提供了解决方案,具体操作步骤如下:
1.
添加依赖
:在
package.json
中添加
raw-loader
:
{
"name": "shine",
"version": "0.0.1",
"license": "MIT",
"dependencies": {
"stats-webpack-plugin": "^0.2.1",
"webpack": "^1.9.11",
"bootstrap": "3.3.7",
"raw-loader": "0.5.1"
},
"devDependencies": {
// ...
}
}
-
配置 Webpack
:修改
config/webpack.config.js,让 Webpack 在处理.html文件时使用 raw loader:
var config = {
entry: {
'application': './webpack/application.js'
},
module: {
loaders: [
// existing loader configuration ...
{
test: /\.html/,
loader: 'raw'
}
]
}
};
-
移动 HTML 代码
:将
webpack/CustomerSearchComponent.js中的 HTML 代码移到webpack/CustomerSearchComponent.html中,并使用require引入:
var CustomerSearchComponent = ng.core.Component({
selector: "shine-customer-search",
template: require("./CustomerSearchComponent.html")
}).Class({
// rest of class as previously defined ...
});
module.exports = CustomerSearchComponent;
但在运行 JavaScript 单元测试时,会出现
SyntaxError: Unexpected token <
错误。这是因为 Jasmine 使用了不同的
require
实现,不涉及 Webpack,将
.html
文件当作 JavaScript 文件处理导致的。我们可以使用
Proxyquire
来解决这个问题,步骤如下:
1.
添加依赖
:在
package.json
的
devDependencies
中添加
proxyquire
:
{
"name": "shine",
"version": "0.0.1",
"license": "MIT",
"dependencies": {
// existing production dependencies...
},
"devDependencies": {
"jasmine": "2.4.0",
"jasmine-node": "1.11.0",
"testdouble": "1.6.0",
"proxyquire": "1.7.10"
}
}
-
修改测试代码
:在测试文件中引入
proxyquire,并配置为忽略CustomerSearchComponent.html:
var proxyquire = require("proxyquire");
var CustomerSearchComponent = proxyquire(
"../../webpack/CustomerSearchComponent",
{
"./CustomerSearchComponent.html": {
"@noCallThru": "true"
}
}
);
配置 Angular 路由器实现用户导航
Angular 如同 Rails 一样,具备通过 URL 实现导航的功能。为了将现有的 Angular 应用转换为使用路由器的单页应用,我们需要完成以下步骤:
1.
创建第二个组件
:
-
创建静态视图
:在
webpack/CustomerDetailsComponent.html
中创建简单的静态视图:
<h1>Customer Details!</h1>
- **定义组件**:在 `webpack/CustomerDetailsComponent.js` 中定义组件:
var reflectMetadata = require("reflect-metadata");
var ng = {
core: require("@angular/core")
};
var CustomerDetailsComponent = ng.core.Component({
selector: "shine-customer-details",
template: require("./CustomerDetailsComponent.html")
}).Class({
constructor: [
function() {
}
]
});
module.exports = CustomerDetailsComponent;
- **引入组件**:在 `webpack/application.js` 中引入 `CustomerDetailsComponent`,并将其添加到 `NgModule` 的 `declarations` 中:
var CustomerDetailsComponent = require("./CustomerDetailsComponent");
var CustomerSearchAppModule = ng.core.NgModule({
imports: [
ng.platformBrowser.BrowserModule,
ng.forms.FormsModule,
ng.http.HttpModule
],
declarations: [
CustomerSearchComponent,
CustomerDetailsComponent
],
bootstrap: [ CustomerSearchComponent ]
}).Class({
constructor: function() {}
});
-
安装和配置 Angular 路由器
:
-
配置路由
:在
webpack/application.js中配置路由:
-
配置路由
:在
var routing = ng.router.RouterModule.forRoot(
[
{
path: "",
component: CustomerSearchComponent
},
{
path: ":id",
component: CustomerDetailsComponent
}
]
);
- **更新 `NgModule`**:将 `routing` 添加到 `NgModule` 的 `imports` 中:
var CustomerSearchAppModule = ng.core.NgModule({
imports: [
ng.platformBrowser.BrowserModule,
ng.forms.FormsModule,
ng.http.HttpModule,
routing
],
declarations: [
CustomerSearchComponent,
CustomerDetailsComponent
],
bootstrap: [ CustomerSearchComponent ]
}).Class({
constructor: function() {}
});
-
创建顶级组件
:
-
创建组件
:在
webpack/CustomerAppComponent.js中创建顶级组件:
-
创建组件
:在
var ng = {
core: require("@angular/core")
};
var AppComponent = ng.core.Component({
selector: "shine-customers-app",
template: "<router-outlet></router-outlet>"
}).Class({
constructor: [
function() { }
]
});
module.exports = AppComponent;
- **更新 Rails 视图**:在 `app/views/customers/index.html.erb` 中使用新的选择器:
<section id="shine-customer-search">
<shine-customers-app></shine-customers-app>
</section>
- **更新 `NgModule`**:在 `webpack/application.js` 中引入 `CustomerAppComponent`,并更新 `NgModule` 的 `declarations` 和 `bootstrap`:
var CustomerAppComponent = require("./CustomerAppComponent");
var CustomerSearchComponent = require("./CustomerSearchComponent");
var CustomerDetailsComponent = require("./CustomerDetailsComponent");
// router configuration ...
var CustomerSearchAppModule = ng.core.NgModule({
imports: [
ng.platformBrowser.BrowserModule,
ng.forms.FormsModule,
ng.http.HttpModule,
routing
],
declarations: [
CustomerSearchComponent,
CustomerDetailsComponent,
CustomerAppComponent
],
bootstrap: [ CustomerAppComponent ]
}).Class({
constructor: function() {}
});
配置 Rails 与 Angular 路由器协同工作
由于 Angular 对服务器端没有明确规定,我们需要解决 Rails 路由和 Angular 路由的交互问题。为了明确区分 URL 中由 Rails 和 Angular 处理的部分,我们将基础 URL 设置为
/customers/ng
,具体操作步骤如下:
1.
更新路由配置
:在
config/routes.rb
中添加新的路由:
Rails.application.routes.draw do
devise_for :users
root to: "dashboard#index"
# These supercede other /customers routes, so must
# come before resource :customers
get "customers/ng", to: "customers#ng"
get "customers/ng/*angular_route", to: "customers#ng"
resources :customers, only: [ :index ]
get "angular_test" => "angular_test#index"
end
-
实现
ng方法 :在CustomersController中实现ng方法,并设置基础 URL:
class CustomersController < ApplicationController
PAGE_SIZE = 10
def ng
@base_url = "/customers/ng"
end
def index
# method as it was before
respond_to do |format|
format.html {
redirect_to "/customers/ng"
}
format.json {
render json: { customers: @customers }
}
end
end
end
-
复制视图文件
:将
app/views/customers/index.html.erb复制到app/views/customers/ng.html.erb。 -
设置基础 URL
:在
app/views/layouts/application.html.erb中根据@base_url设置基础 URL:
<head>
<title>Shine</title>
<%= csrf_meta_tags %>
<%= stylesheet_link_tag
'application', media: 'all' %>
<%= javascript_include_tag 'application' %>
<%= javascript_include_tag *webpack_asset_paths("application") %>
<% if @base_url %>
<base href="<%= @base_url %>">
<% end %>
</head>
完成以上配置后,重启 Rails 服务器,访问
http://localhost:5000/customers
会重定向到
http://localhost:5000/customers/ng
,显示现有的客户搜索功能。访问
http://localhost:5000/customers/ng/42
可以看到简单的客户详情页面。同时,运行测试可以验证系统是否正常工作。
通过以上步骤,我们成功地将简单的搜索界面转换为单页应用,实现了用户在不同页面之间的导航。接下来,我们将添加组件之间的实际导航功能。
打造单页应用:从测试到路由配置的全流程指南
添加组件间的导航功能
在完成了 Angular 路由器的配置以及 Rails 与 Angular 路由器的协同工作后,接下来我们要为应用添加组件间的实际导航功能。这将让用户能够在搜索结果和详细视图之间自由切换。
-
在搜索结果中添加导航按钮
首先,我们需要在CustomerSearchComponent的模板中添加一个按钮,用于导航到客户详细信息页面。-
打开
webpack/CustomerSearchComponent.html文件,添加一个按钮:
-
打开
<button (click)="navigateToDetails(customer.id)">查看详情</button>
- 在 `webpack/CustomerSearchComponent.js` 中添加 `navigateToDetails` 方法:
var CustomerSearchComponent = ng.core.Component({
selector: "shine-customer-search",
template: require("./CustomerSearchComponent.html")
}).Class({
constructor: [ng.router.Router, function(router) {
this.router = router;
}],
navigateToDetails: function(id) {
this.router.navigate([id]);
}
});
module.exports = CustomerSearchComponent;
-
在详细信息组件中获取路由参数
当用户点击按钮导航到详细信息页面时,我们需要在CustomerDetailsComponent中获取路由参数,以便显示相应客户的详细信息。-
在
webpack/CustomerDetailsComponent.js中添加获取路由参数的逻辑:
-
在
var reflectMetadata = require("reflect-metadata");
var ng = {
core: require("@angular/core"),
router: require("@angular/router")
};
var CustomerDetailsComponent = ng.core.Component({
selector: "shine-customer-details",
template: require("./CustomerDetailsComponent.html")
}).Class({
constructor: [ng.router.ActivatedRoute, function(route) {
this.route = route;
}],
ngOnInit: function() {
this.route.params.subscribe(params => {
this.customerId = params['id'];
// 这里可以添加获取客户详细信息的逻辑
});
}
});
module.exports = CustomerDetailsComponent;
数据获取与展示
现在,我们已经实现了导航功能,接下来要从后端获取客户的详细信息并在详细信息页面展示。
-
在后端添加获取客户信息的接口
-
在
CustomersController中添加show方法:
-
在
class CustomersController < ApplicationController
PAGE_SIZE = 10
def ng
@base_url = "/customers/ng"
end
def index
# method as it was before
respond_to do |format|
format.html {
redirect_to "/customers/ng"
}
format.json {
render json: { customers: @customers }
}
end
end
def show
@customer = Customer.find(params[:id])
render json: @customer
end
end
-
在前端获取并展示客户信息
-
在
webpack/CustomerDetailsComponent.js中添加获取客户信息的逻辑:
-
在
var reflectMetadata = require("reflect-metadata");
var ng = {
core: require("@angular/core"),
router: require("@angular/router"),
http: require("@angular/http")
};
var CustomerDetailsComponent = ng.core.Component({
selector: "shine-customer-details",
template: require("./CustomerDetailsComponent.html")
}).Class({
constructor: [ng.router.ActivatedRoute, ng.http.Http, function(route, http) {
this.route = route;
this.http = http;
}],
ngOnInit: function() {
this.route.params.subscribe(params => {
this.customerId = params['id'];
this.http.get(`/customers/${this.customerId}`)
.map(res => res.json())
.subscribe(data => {
this.customer = data;
});
});
}
});
module.exports = CustomerDetailsComponent;
- 在 `webpack/CustomerDetailsComponent.html` 中展示客户信息:
<h1>客户详情</h1>
<p>姓名: {{ customer.name }}</p>
<p>邮箱: {{ customer.email }}</p>
<!-- 可以根据实际情况添加更多信息 -->
总结与流程图
通过以上步骤,我们成功地将一个简单的搜索界面转换为了一个功能完善的单页应用,实现了从搜索结果到详细信息页面的导航,并且能够从后端获取并展示客户的详细信息。
下面是整个流程的 mermaid 流程图:
graph LR
A[开始] --> B[单元测试与改进]
B --> C[存储视图模板到 HTML 文件]
C --> D[配置 Angular 路由器]
D --> E[配置 Rails 与 Angular 协同工作]
E --> F[添加组件间导航功能]
F --> G[数据获取与展示]
G --> H[结束]
整个开发过程可以总结为以下表格:
| 步骤 | 操作内容 |
| ---- | ---- |
| 单元测试与改进 | 使用
td.verify
验证
window.alert
调用,使用
Proxyquire
解决测试中 HTML 文件的问题 |
| 存储视图模板到 HTML 文件 | 使用 Webpack 的 raw loader,将 HTML 代码从 JavaScript 中分离 |
| 配置 Angular 路由器 | 创建组件,配置路由,创建顶级组件 |
| 配置 Rails 与 Angular 协同工作 | 设置基础 URL,更新路由配置 |
| 添加组件间导航功能 | 在搜索结果中添加导航按钮,在详细信息组件中获取路由参数 |
| 数据获取与展示 | 在后端添加获取客户信息的接口,在前端获取并展示信息 |
通过遵循这些步骤,我们可以逐步构建一个功能强大、易于维护的单页应用。在实际开发中,还可以根据需求进一步优化和扩展应用的功能。
超级会员免费看
1008

被折叠的 条评论
为什么被折叠?



