mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-11-10 00:27:41 +08:00
fix(router): redirects now account for query string (#23337)
resolves #23136
This commit is contained in:
@ -6,7 +6,7 @@ import { debounce } from '../../utils/helpers';
|
|||||||
import { ROUTER_INTENT_BACK, ROUTER_INTENT_FORWARD, ROUTER_INTENT_NONE } from './utils/constants';
|
import { ROUTER_INTENT_BACK, ROUTER_INTENT_FORWARD, ROUTER_INTENT_NONE } from './utils/constants';
|
||||||
import { printRedirects, printRoutes } from './utils/debug';
|
import { printRedirects, printRoutes } from './utils/debug';
|
||||||
import { readNavState, waitUntilNavNode, writeNavState } from './utils/dom';
|
import { readNavState, waitUntilNavNode, writeNavState } from './utils/dom';
|
||||||
import { routeRedirect, routerIDsToChain, routerPathToChain } from './utils/matching';
|
import { findRouteRedirect, routerIDsToChain, routerPathToChain } from './utils/matching';
|
||||||
import { readRedirects, readRoutes } from './utils/parser';
|
import { readRedirects, readRoutes } from './utils/parser';
|
||||||
import { chainToPath, generatePath, parsePath, readPath, writePath } from './utils/path';
|
import { chainToPath, generatePath, parsePath, readPath, writePath } from './utils/path';
|
||||||
|
|
||||||
@ -188,7 +188,7 @@ export class Router implements ComponentInterface {
|
|||||||
|
|
||||||
private onRedirectChanged() {
|
private onRedirectChanged() {
|
||||||
const path = this.getPath();
|
const path = this.getPath();
|
||||||
if (path && routeRedirect(path, readRedirects(this.el))) {
|
if (path && findRouteRedirect(path, readRedirects(this.el))) {
|
||||||
this.writeNavStateRoot(path, ROUTER_INTENT_NONE);
|
this.writeNavStateRoot(path, ROUTER_INTENT_NONE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -226,13 +226,15 @@ export class Router implements ComponentInterface {
|
|||||||
|
|
||||||
// lookup redirect rule
|
// lookup redirect rule
|
||||||
const redirects = readRedirects(this.el);
|
const redirects = readRedirects(this.el);
|
||||||
const redirect = routeRedirect(path, redirects);
|
const redirect = findRouteRedirect(path, redirects);
|
||||||
|
|
||||||
let redirectFrom: string[] | null = null;
|
let redirectFrom: string[] | null = null;
|
||||||
|
|
||||||
if (redirect) {
|
if (redirect) {
|
||||||
this.setPath(redirect.to!, direction);
|
const { segments, queryString } = redirect.to!;
|
||||||
|
this.setPath(segments, direction, queryString);
|
||||||
redirectFrom = redirect.from;
|
redirectFrom = redirect.from;
|
||||||
path = redirect.to!;
|
path = segments;
|
||||||
}
|
}
|
||||||
|
|
||||||
// lookup route chain
|
// lookup route chain
|
||||||
|
|||||||
@ -70,7 +70,7 @@
|
|||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
<ion-content>
|
<ion-content>
|
||||||
this is the first pahe
|
this is the first page
|
||||||
</ion-content>`;
|
</ion-content>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -211,6 +211,7 @@
|
|||||||
<ion-route url="/four" component="tab-four"> </ion-route>
|
<ion-route url="/four" component="tab-four"> </ion-route>
|
||||||
</ion-route>
|
</ion-route>
|
||||||
|
|
||||||
|
<ion-route-redirect from="/redirect-to-three" to="/three?has_query_string=true"></ion-route-redirect>
|
||||||
</ion-router>
|
</ion-router>
|
||||||
|
|
||||||
<ion-nav></ion-nav>
|
<ion-nav></ion-nav>
|
||||||
|
|||||||
12
core/src/components/router/test/basic/redirect.e2e.ts
Normal file
12
core/src/components/router/test/basic/redirect.e2e.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { newE2EPage } from '@stencil/core/testing';
|
||||||
|
|
||||||
|
test('redirect should support query string', async () => {
|
||||||
|
const page = await newE2EPage({
|
||||||
|
url: '/src/components/router/test/basic#/redirect-to-three?ionic:_testing=true'
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.waitForChanges();
|
||||||
|
|
||||||
|
const url = await page.url();
|
||||||
|
expect(url).toContain('#/three?has_query_string=true');
|
||||||
|
});
|
||||||
@ -47,20 +47,22 @@ describe('parser', () => {
|
|||||||
const r3 = mockRedirectElement(win, '*', null);
|
const r3 = mockRedirectElement(win, '*', null);
|
||||||
const r4 = mockRedirectElement(win, '/workout/*', '');
|
const r4 = mockRedirectElement(win, '/workout/*', '');
|
||||||
const r5 = mockRedirectElement(win, 'path/hey', '/path/to//login');
|
const r5 = mockRedirectElement(win, 'path/hey', '/path/to//login');
|
||||||
|
const r6 = mockRedirectElement(win, 'path/qs', '/path?qs=true');
|
||||||
|
|
||||||
root.appendChild(r1);
|
root.appendChild(r1);
|
||||||
root.appendChild(r2);
|
root.appendChild(r2);
|
||||||
root.appendChild(r3);
|
root.appendChild(r3);
|
||||||
root.appendChild(r4);
|
root.appendChild(r4);
|
||||||
root.appendChild(r5);
|
root.appendChild(r5);
|
||||||
|
root.appendChild(r6);
|
||||||
|
|
||||||
const expected: RouteRedirect[] = [
|
const expected: RouteRedirect[] = [
|
||||||
{ from: [''], to: undefined },
|
{ from: [''], to: undefined },
|
||||||
{ from: [''], to: ['workout'] },
|
{ from: [''], to: { segments: ['workout'] }},
|
||||||
{ from: ['*'], to: undefined },
|
{ from: ['*'], to: undefined },
|
||||||
{ from: ['workout', '*'], to: [''] },
|
{ from: ['workout', '*'], to: { segments: [''] } },
|
||||||
{ from: ['path', 'hey'], to: ['path', 'to', 'login'] }
|
{ from: ['path', 'hey'], to: { segments: ['path', 'to', 'login'] } },
|
||||||
|
{ from: ['path', 'qs'], to: { segments: ['path'], queryString: 'qs=true' } },
|
||||||
];
|
];
|
||||||
expect(readRedirects(root)).toEqual(expected);
|
expect(readRedirects(root)).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -16,7 +16,7 @@ export const printRedirects = (redirects: RouteRedirect[]) => {
|
|||||||
console.group(`[ion-core] REDIRECTS[${redirects.length}]`);
|
console.group(`[ion-core] REDIRECTS[${redirects.length}]`);
|
||||||
for (const redirect of redirects) {
|
for (const redirect of redirects) {
|
||||||
if (redirect.to) {
|
if (redirect.to) {
|
||||||
console.debug('FROM: ', `$c ${generatePath(redirect.from)}`, 'font-weight: bold', ' TO: ', `$c ${generatePath(redirect.to)}`, 'font-weight: bold');
|
console.debug('FROM: ', `$c ${generatePath(redirect.from)}`, 'font-weight: bold', ' TO: ', `$c ${generatePath(redirect.to.segments)}`, 'font-weight: bold');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.groupEnd();
|
console.groupEnd();
|
||||||
|
|||||||
@ -18,7 +18,7 @@ export interface RouterEventDetail {
|
|||||||
|
|
||||||
export interface RouteRedirect {
|
export interface RouteRedirect {
|
||||||
from: string[];
|
from: string[];
|
||||||
to?: string[];
|
to?: ParsedRoute;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RouteWrite {
|
export interface RouteWrite {
|
||||||
@ -45,6 +45,13 @@ export interface RouteNode extends RouteEntry {
|
|||||||
children: RouteTree;
|
children: RouteTree;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ParsedRoute {
|
||||||
|
// Parts of the route (non empty "/" separated parts of an URL).
|
||||||
|
segments: string[];
|
||||||
|
// Unparsed query string.
|
||||||
|
queryString?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type RouterDirection = 'forward' | 'back' | 'root';
|
export type RouterDirection = 'forward' | 'back' | 'root';
|
||||||
export type NavOutletElement = NavOutlet & HTMLStencilElement;
|
export type NavOutletElement = NavOutlet & HTMLStencilElement;
|
||||||
export type RouteChain = RouteEntry[];
|
export type RouteChain = RouteEntry[];
|
||||||
|
|||||||
@ -1,12 +1,17 @@
|
|||||||
import { RouteChain, RouteID, RouteRedirect } from './interface';
|
import { RouteChain, RouteID, RouteRedirect } from './interface';
|
||||||
|
|
||||||
export const matchesRedirect = (input: string[], route: RouteRedirect): route is RouteRedirect => {
|
// Returns whether the given redirect matches the given path segments.
|
||||||
const { from, to } = route;
|
//
|
||||||
|
// A redirect matches when the segments of the path and redirect.from are equal.
|
||||||
|
// Note that segments are only checked until redirect.from contains a '*' which matches any path segment.
|
||||||
|
// The path ['some', 'path', 'to', 'page'] matches both ['some', 'path', 'to', 'page'] and ['some', 'path', '*'].
|
||||||
|
export const matchesRedirect = (path: string[], redirect: RouteRedirect): boolean => {
|
||||||
|
const { from, to } = redirect;
|
||||||
if (to === undefined) {
|
if (to === undefined) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (from.length > input.length) {
|
if (from.length > path.length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -15,15 +20,16 @@ export const matchesRedirect = (input: string[], route: RouteRedirect): route is
|
|||||||
if (expected === '*') {
|
if (expected === '*') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (expected !== input[i]) {
|
if (expected !== path[i]) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return from.length === input.length;
|
return from.length === path.length;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const routeRedirect = (path: string[], routes: RouteRedirect[]) => {
|
// Returns the first redirect matching the path segments or undefined when no match found.
|
||||||
return routes.find(route => matchesRedirect(path, route)) as RouteRedirect | undefined;
|
export const findRouteRedirect = (path: string[], redirects: RouteRedirect[]) => {
|
||||||
|
return redirects.find(redirect => matchesRedirect(path, redirect));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const matchesIDs = (ids: string[], chain: RouteChain): number => {
|
export const matchesIDs = (ids: string[], chain: RouteChain): number => {
|
||||||
|
|||||||
@ -8,7 +8,7 @@ export const readRedirects = (root: Element): RouteRedirect[] => {
|
|||||||
const to = readProp(el, 'to');
|
const to = readProp(el, 'to');
|
||||||
return {
|
return {
|
||||||
from: parsePath(readProp(el, 'from')).segments,
|
from: parsePath(readProp(el, 'from')).segments,
|
||||||
to: to == null ? undefined : parsePath(to).segments,
|
to: to == null ? undefined : parsePath(to),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { RouteChain, RouterDirection } from '../../../interface';
|
|
||||||
|
|
||||||
import { ROUTER_INTENT_FORWARD } from './constants';
|
import { ROUTER_INTENT_FORWARD } from './constants';
|
||||||
|
import { ParsedRoute, RouteChain, RouterDirection } from './interface';
|
||||||
|
|
||||||
export const generatePath = (segments: string[]): string => {
|
export const generatePath = (segments: string[]): string => {
|
||||||
const path = segments
|
const path = segments
|
||||||
@ -81,7 +80,7 @@ export const readPath = (loc: Location, root: string, useHash: boolean): string[
|
|||||||
// Parses the path to:
|
// Parses the path to:
|
||||||
// - segments an array of '/' separated parts,
|
// - segments an array of '/' separated parts,
|
||||||
// - queryString (undefined when no query string).
|
// - queryString (undefined when no query string).
|
||||||
export const parsePath = (path: string | undefined | null): {segments: string[], queryString?: string} => {
|
export const parsePath = (path: string | undefined | null): ParsedRoute => {
|
||||||
let segments = [''];
|
let segments = [''];
|
||||||
let queryString;
|
let queryString;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user