紧接着Tutorial-第一部分回顾,继续往super-rental应用添加更有趣的功能。当用户点击列表时可以进入一个详情页面,详情页面可以展示一张更大的地图。
效果图
本篇学习要点:
- 在路由中使用动态参数
- 组件的路由上使用动态参数
- 可以访问路由器的组件测试
- 通过动态参数段访问路由参数
- 在不同的测试用例中共享公共代码
在路由中使用动态参数
目前Ember应用的首页列表还是无法点击的,我们可以想象一下,平常在网上冲浪的时候,如果从一个列表点击进入到一个详情页面通常的URL路径是这样的:/列表页面URL/某个数据的id。那么同理rental列表也可以做到,路径是这样的:/rentals/grand-old-mansion。grand-old-mansion就是数据的id。
之所以能接收参数需要修改的路由配置。
ember g route rental
修改路由的默认路径。
import EmberRouter from '@ember/routing/router';import config from './config/environment';export default class Router extends EmberRouter { location = config.locationType; rootURL = config.rootURL;}Router.map(function() { this.route('about'); this.route('contact', { path: '/getting-in-touch' }); this.route('rental', { path: '/rentals/:rental_id' });});
注意路由设置路径里面。:rental_id 是一个动态参数,前面是有一个冒号的。这个就是一个动态参数,你可以理解为一个占位符,在访问请求的时候会自动替换成数据的id。
组件的路由上使用动态参数
修改rental.hbs,在中传递动态参数。
{{!-- app/components/rental.hbs --}} {{!-- 调用Image组件,组件上有两个HTML属性,这两个HTML属性会传递到组件内部的...attributes上 --}} <:image src="%7B%7B@rental.image%7D%7D">
{{!-- 添加一个连接,跳转到路由rental上 --}} {{@rental.title}}
Owner: {{@rental.owner}}
Type: {{@rental.type}}
Location: {{@rental.city}}
Number of bedrooms: {{@rental.bedrooms}}
主要看这一行
在调用LinkTo跳转组件的时候传递了一个@model参数,页面刷新后,鼠标移动到首页图片上,可以看到浏览器底部的状态栏上显示的路径有一个undefine。这是怎么回事呢??
如果还有印象,前面文章中在路由index.js里面的传递过来的数据是没有包含id属性值的。由于没有id,整个模型就没有id,获取不到自然就是一个空。
修改index.js的取值,增加一个id属性。
// app/routes/index.jsimport Route from '@ember/routing/route';export default class IndexRoute extends Route { // 使用异步方式返回,也就是说,路由一进来之后调用到model方法, // 不会等到model方法执行完毕,它就会先转到模板上。 // 进入本路由的时候就会自动调用这个方法。 async model() { // 通过fetch方法调用后端数据 // 更多有关fetch相关的信息请看:https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API let response = await fetch('/api/rentals.json'); // 解析数据 // let parsed = await response.json(); // return parsed; // 解析JSON数据,获取attributes这个参数里面的数据 let { data } = await response.json(); // 通过map函数解析数据。 return data.map(model => { // 拿到attributes参数 let { id, attributes } = model; // 把数据做一个分类,'Condo','Townhouse','Apartment'归为一类,其他又做了微一类 let type; let community = ['Condo','Townhouse','Apartment']; if (community.includes(attributes.category)) { type = 'Community'; } else { type = 'Standalone'; } return { id, type, ...attributes }; }); }}
注意这两行的改动。
let { id, attributes } = model;return { id, type, ...attributes };
修改之后,再看地址状态栏,可以看到是这样的一个地址:http://localhost:4200/rentals/urban-living
点击之后跳转一个空白页面,因为我们还没对这个子路由做任何处理,所以还是一个空白页面。先不急着添加详情页面,先通过测试用例验证新增的链接是否成功了
import { module, test } from 'qunit';import { setupRenderingTest } from 'ember-qunit';import { render } from '@ember/test-helpers';import { hbs } from 'ember-cli-htmlbars';module('Integration | Component | rental', function(hooks) { setupRenderingTest(hooks); test('测试Rental组件,验证组件模板上的HTML属性或者标签是否达到预期', async function(assert) { // Set any properties with this.set('myProperty', 'value'); // Handle any actions with this.set('myAction', function(val) { ... }); // 渲染组件,模拟组件的调用等同于在hbs模板中调用 // await render(hbs``); // 使用动态数据之后,需要调整测试用例,此前的测试用例测试的是静态数据, // 先构造一个JSON数据,然后再在调用组件的时候传递过去。 this.setProperties({ rental: { // 添加一个id属性 id: "grand-old-mansion", title: 'Grand Old Mansion', owner: 'Veruca Salt', city: 'San Francisco', location: { lat: 37.7749, lng: -122.4194, }, category: 'Estate', type: 'Standalone', bedrooms: 15, image: 'https://upload.wikimedia.org/wikipedia/commons/c/cb/Crane_estate_(5).jpg', description: 'This grand old mansion sits on over 100 acres of rolling hills and dense redwood forests.' } }); // 调用组件时把构造的数据传递过去。 await render(hbs``); // 断言组件模板的HTML标签 assert.dom('article').hasClass('rental'); assert.dom('article h3').hasText('Grand Old Mansion'); // 断言在rental.hbs添加的详情页面跳转链接 assert.dom('article h3 a').hasAttribute('href', '/rental/grand-old-mansion'); // 断言这个标签下是否包含文字内容 assert.dom('article .detail.owner').includesText('Veruca Salt'); assert.dom('article .detail.type').includesText('Standalone'); assert.dom('article .detail.location').includesText('San Francisco'); assert.dom('article .detail.bedrooms').includesText('15'); assert.dom('article .image').exists(); // 验证组件调用成功 assert.dom("article .map").exists(); });});
注意21行和43行位置,添加了一个id属性,然后断言模板页面上的href属性值。
保存之后,看用例测试结果,but。。。。具体没测试通过。这又是怎么肥四!!!!
对于这种有跳转的路由需要通过代码先关联路由。
在setupRenderingTest(hooks);方法之后增加如下3行代码。
hooks.beforeEach(function () { this.owner.setupRouter();});
如果其他测试也发现报错,也需要添加这段代码。
通过动态参数段访问路由参数
增加详情页面处理,现在点击图片上面的链接跳转到的是一个空白页面。首先在路由rental.js增加查询代码,根据动态参数查询某个数据。
// app/routes/rental.jsimport Route from '@ember/routing/route';export default class RentalRoute extends Route { async model(params) { // 获取上面传递参数,参数的名字就是在router.js里面的定义的动态参数rental_id let response = await fetch(`/api/rentals/${params.rental_id}.json`); let { data } = await response.json(); // 拿到attributes参数 let { id, attributes } = data; // 把数据做一个分类,'Condo','Townhouse','Apartment'归为一类,其他又做了微一类 let type; let community = ['Condo','Townhouse','Apartment']; if (community.includes(attributes.category)) { type = 'Community'; } else { type = 'Standalone'; } return { id, type, ...attributes }; }}
代码和index路由的很相似,不同的是index获取的是所有数据,rental路由只根据id获取指定的数据。返回的是一条数据。
params.rental_id 这个参数是非常关键的,params是model的入参,这个入参会自动把动态段参数rental_id封装进去。直接获取即可。
详细信息页面
为了展示点击之后的详情页面信息,新增一个子组件用于展示,这个页面会直接展示大图片,和标题。
ember g component rental/detailed
{{!-- app/components/rental/details.hbs 内容和rental模板的基本一致 --}}
{{@rental.title}}
Nice find! This looks like a nice place to stay near {{@rental.city}}.
Share on Twitter <:image src="%7B%7B@rental.image%7D%7D">
About {{@rental.title}}
Owner: {{@rental.owner}}
Type: {{@rental.type}} – {{@rental.category}}
Location: {{@rental.city}}
Number of bedrooms: {{@rental.bedrooms}}
{{@rental.description}}
修改app/templates/rental.hbs,在模板中调用新增的子组件detailed,页面刷新后,点击首页列表的标题。
效果完美,跳转到一个详情页面,页面上展示一张大图地图,再通过测试用例验证我们的代码。
// tests/acceptance/super-renlats.jsimport { module, test } from 'qunit';import { click, visit, currentURL } from '@ember/test-helpers';import { setupApplicationTest } from 'ember-qunit';module('Acceptance | super rentals', function(hooks) { setupApplicationTest(hooks); hooks.beforeEach(function() { this.owner.setupRouter(); }); test('visiting /', async function(assert) { // 模拟访问项目,等效于在浏览器输入http://localhost:4200/然后按enter。 await visit('/'); // 相当于进入首页之后,断言当前的URL是/ assert.equal(currentURL(), '/'); // 断言首页是否有h2标签,并且标签里面的内容是Welcome to Super Rentals! assert.dom('h2').hasText('Welcome to Super Rentals!'); // 通过class属性层级找到按钮 断言按钮的内容是About Us assert.dom('.jumbo a.button').hasText("About Us"); // 测试导航条 assert.dom('nav').exists(); assert.dom('h1').hasText('SuperRentals'); assert.dom('p').hasText("We hope you find exactly what you're looking for in a place to stay."); // 触发事件,等同于你页面上点击了按钮,然后跳转到about路由下 await click('.jumbo a.button'); // 断言是否正确跳转到about路由 assert.equal(currentURL(), '/about'); }); // test('测试首页列表的点击链接', async function(assert) { await visit('/'); assert.dom('.rental').exists({ count: 3 }); await click('.rental :first-of-type a'); assert.equal(currentURL(), '/rentals/grand-old-mansion'); }); // 测试详情页, test('测试<:detailed>页面,点击跳转到rentals/grand-old-mansion', async function(assert) { await visit('/rentals/grand-old-mansion'); assert.equal(currentURL(), '/rentals/grand-old-mansion'); assert.dom('nav').exists(); assert.dom('h1').containsText('SuperRentals'); assert.dom('h2').containsText('Grand Old Mansion'); assert.dom('.rental.detailed').exists(); }); // 测试about页面,访问/about路由 test('visiting /about', async function(assert) { // 测试进入路由about成功 await visit('/about'); // 断言当前的路由是/about assert.equal(currentURL(), '/about'); // 断言h2标签的内容是About Super Rentals assert.dom('h2').hasText('About Super Rentals'); // 测试导航条 assert.dom('nav').exists(); assert.dom('h1').hasText('SuperRentals'); assert.dom('p').hasText("The Super Rentals website is a delightful project created to explore Ember. By building a property rental site, we can simultaneously imagine traveling AND building Ember applications."); // 断言有Contact Us按钮 assert.dom('.jumbo a.button').hasText('Contact Us'); // 触发点击事件,进入contact路由 await click('.jumbo a.button'); // 断言进入contact成功,因为自定义了路由URL,所以要判断当期路径和getting-in-touch是否一致 assert.equal(currentURL(), '/getting-in-touch'); }); // 测试contact页面,访问/getting-in-touch test('visiting /getting-in-touch', async function(assert) { await visit('/getting-in-touch'); assert.equal(currentURL(), '/getting-in-touch'); assert.dom('h2').hasText('Contact Us'); assert.dom('a.button').hasText('About'); // 测试导航条 assert.dom('nav').exists(); assert.dom('h1').hasText('SuperRentals'); await click('.jumbo a.button'); assert.equal(currentURL(), '/about'); });});
如果发现还有其他测试无法通过需要修改一些路由文件的的请求URL。
一个是app/routes/index.js,一个是app/routes/rental.js
把里面的URL地址前缀http://location:4200删除。
比如:
let response = await fetch(`http://location:4200/api/rentals/${params.rental_id}.json`);
改为
let response = await fetch(`/api/rentals/${params.rental_id}.json`);