使用型別化引數和注入屬性進行路由
這個解決方案更加複雜,利用自定義 TypeScript 裝飾器將 match
,history
和/或 location
資料注入到 React.Component
類中,這樣就可以獲得全型別安全性而無需任何型別保護,如前面的示例所示。
// Routed.ts
- 定義裝飾器
import { RouteComponentProps, match } from 'react-router';
import { History, Location } from 'history';
// re-export for convenience, uppercase match to be in line with everything else
export { History, Location, match as Match };
// names for the three types we support injecting
type InjectionPropType = 'location' | 'history' | 'match';
// holder for a given property to be injected as a specific type
class InjectionProp {
prop: string;
type: InjectionPropType;
}
// a store, key = class name (constructor.name) and array of InjectionProp's for that class
// this will be filled in by the three property decorators @RoutedMatch, @RoutedLocation and @RoutedHistory
class InjectionStore {
[key: string]: InjectionProp[];
}
// instance of the store
const store: InjectionStore = {};
// type guard for RouteComponentProps
function instanceOfRouteProps<P>(object: any): object is RouteComponentProps<P> {
return 'match' in object && 'location' in object && 'history' in object;
}
// class level decorator, wraps the constructor with custom one which injects
// values into instances based on the InjectionStore instance
export function Routed<T extends { new (...args: any[]): {} }>(constructor: T) {
// get the class name from the constructor
const className = (constructor as any).name;
// return a new class with a new constructor which calls super(..)
return class extends constructor {
constructor(...args: any[]) {
super(args);
// if there is a React props passed as arg[0]
if (args.length >= 1) {
const routeProps = args[0];
// check type guard to see if the React props is enriched with RouteComponentProps by react-router
if (instanceOfRouteProps(routeProps)) {
// check if the current class has any registered properties to be injected
if (store[className]) {
const injectionProps = store[className];
// iterate over properties to inject
for (let i = 0; i < injectionProps.length; i++) {
const injectionProp = injectionProps[i];
// inject the specified property with the appropriate type
switch (injectionProp.type) {
case 'match':
(this as any)[injectionProp.prop] = routeProps.match;
break;
case 'history':
(this as any)[injectionProp.prop] = routeProps.history;
break;
case 'location':
(this as any)[injectionProp.prop] = routeProps.location;
break;
}
}
}
}
}
}
}
}
// generic property decorator, registers a classes property for inject in the store above
function RoutedInjector(proto: any, prop: string, type: InjectionPropType): any {
const className = proto.constructor.name;
if (!store.hasOwnProperty(className)) {
store[className] = [];
}
store[className].push({
prop: prop,
type: type
});
}
// property decorator for Match instances
export function RoutedMatch(proto: any, prop: string): any {
RoutedInjector(proto, prop, 'match');
}
// property decorator for Location instances
export function RoutedLocation(proto: any, prop: string): any {
RoutedInjector(proto, prop, 'location');
}
// property decorator for History instances
export function RoutedHistory(proto: any, prop: string): any {
RoutedInjector(proto, prop, 'history');
}
// index.ts
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Route, BrowserRouter as Router, Link, match } from 'react-router-dom';
import { History, Location, Match, Routed, RoutedHistory, RoutedLocation, RoutedMatch } from './Routed';
// define React components for multiple pages
class Home extends React.Component<any, any> {
render() {
return (
<div>
<div>HOME</div>
<div><Link to='/details/id123'>Goto Details</Link></div>
</div>);
}
}
interface DetailParams {
id: string;
}
interface DetailsProps {
required: string;
}
@Routed
class Details extends React.Component<DetailsProps, any> {
@RoutedMatch
match: Match<DetailParams>;
@RoutedLocation
location: Location;
@RoutedHistory
history: History;
render() {
return (
<div>
<div>Details for {this.match.params.id} on location {this.location.pathname}</div>
<span
onClick={(e) => this.history.push('/')}
style={{ textDecoration: 'underline', cursor: 'pointer' }}
>Goto Home</span>
</div>
);
}
}
ReactDOM.render(
<Router>
<div>
<Route exact path="/" component={Home} />
<Route exact path="/details/:id" component={(props) => <Details required="some string" {...props} />} />
</div>
</Router>
, document.getElementById('root')
);
這個例子使用自定義裝飾器在裝飾有 @Routed
的 React.Component
上使用屬性裝飾器 @RoutedMatched
,@RoutedLocation
和 @RoutedHistory
注入一些 react-router
特定資料例項。
最終結果是 react-router
特定資料型別現在完全與自定義 React.Component
屬性和狀態分離。另一個好處是它們不再是可選屬性,這意味著你不需要型別保護來安全地訪問它們的值。
history
引數顯示可以從 history
包訪問 History
物件(react-router
的依賴關係),如圖所示,可以使用它以程式設計方式操作瀏覽器歷史記錄。
location
引數攜帶有關當前位置的資訊