12、打造单页应用:从测试到路由配置的全流程指南

打造单页应用:从测试到路由配置的全流程指南

在开发过程中,我们已经掌握了使用 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": {
    // ...
  }
}
  1. 配置 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'
      }
    ]
  }
};
  1. 移动 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"
  }
}
  1. 修改测试代码 :在测试文件中引入 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() {}
});
  1. 安装和配置 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() {}
});
  1. 创建顶级组件
    • 创建组件 :在 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
  1. 实现 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
  1. 复制视图文件 :将 app/views/customers/index.html.erb 复制到 app/views/customers/ng.html.erb
  2. 设置基础 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 路由器的协同工作后,接下来我们要为应用添加组件间的实际导航功能。这将让用户能够在搜索结果和详细视图之间自由切换。

  1. 在搜索结果中添加导航按钮
    首先,我们需要在 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;
  1. 在详细信息组件中获取路由参数
    当用户点击按钮导航到详细信息页面时,我们需要在 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;
数据获取与展示

现在,我们已经实现了导航功能,接下来要从后端获取客户的详细信息并在详细信息页面展示。

  1. 在后端添加获取客户信息的接口
    • 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
  1. 在前端获取并展示客户信息
    • 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,更新路由配置 |
| 添加组件间导航功能 | 在搜索结果中添加导航按钮,在详细信息组件中获取路由参数 |
| 数据获取与展示 | 在后端添加获取客户信息的接口,在前端获取并展示信息 |

通过遵循这些步骤,我们可以逐步构建一个功能强大、易于维护的单页应用。在实际开发中,还可以根据需求进一步优化和扩展应用的功能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值