Angular 中的路由重构💿
Remix团队正在构建一个基于 React Router 原理并使用 React 进行渲染的 Web 框架。他们还有更大的计划,将相同的路由功能移植到其他框架中。最近,该团队发布了一篇博文,宣布他们正在对React Router 进行 Remixing,即将 React Router 的底层组件提取出来,使其与框架无关,可供任何框架使用。本文将介绍我如何让 Remix Routing 与 Angular 协同工作。
太长不看
您可以在这里查看演示应用:https://remix-router-angular.netlify.app/
GitHub 仓库:https://github.com/brandonroberts/remix-router-angular
一切都始于一条推文……
一段时间过去了,我们得到了更多面包屑。
那这意味着什么呢?我深入研究了一下,果然,@remix-run/router它作为一个独立的软件包存在,用于处理管理路由器状态、浏览器历史记录等底层逻辑。
构建路由器服务
import { createBrowserRouter } from '@remix-run/router';
const router = createBrowserRouter({
routes,
});
router.initialize();
几行代码就能在 Angular 中集成路由解决方案?🤔 别急。Remix Router 的确可以处理路由设置、路由状态管理、命令式导航等等,但它并不负责组件渲染。不过,Angular v14 大大简化了动态组件的渲染,所以我们最多只需要一个路由服务和一个出口组件就能开始使用了。
export const ROUTES = new InjectionToken<RouteObject[]>('ROUTES');
export const REMIX_ROUTER = new InjectionToken('Remix Router', {
providedIn: 'root',
factory() {
const routes = inject(ROUTES);
const router = createBrowserRouter({
routes,
});
router.initialize();
return router;
},
});
Remix Router 需要预先定义所有路由。上面的代码定义了一些注入令牌,以便我们可以注入提供的路由,并为路由服务创建浏览器路由器。
@Injectable({
providedIn: 'root',
})
export class Router {
private _remixRouter = inject(REMIX_ROUTER);
routerState$ = new BehaviorSubject<RouterState>(this._remixRouter.state);
constructor() {
this._remixRouter.subscribe((rs) => this.routerState$.next(rs));
}
get state() {
return this._remixRouter.state;
}
navigate(path: string, opts?: NavigateOptions) {
this._remixRouter.navigate(path, opts);
}
}
export function provideRoutes(routes: RouteObject[]) {
return [{ provide: ROUTES, useValue: routes }];
}
路由服务非常精简。它没有使用 Angular 的PlatformLocation任何Location服务,因为这些都由 Remix Router 处理。Remix Router 有一个subscribe监听路由状态变化的方法,所以我们将其封装成一个易于观察的对象,方便所有组件监听。
接下来是渲染组件的输出接口。
建造路由器插座
@Directive({
selector: 'outlet',
standalone: true,
})
export class Outlet {
private destroy$ = new Subject();
private cmp!: Type<any>;
private context? = getRouteContext();
private router = getRouter();
private vcr = inject(ViewContainerRef);
ngOnInit() {
this.setUpListener();
}
setUpListener() {
this.router.routerState$
.pipe(
tap((rs) => {
const matchesToRender = this.getMatch(rs);
const currentCmp = matchesToRender.route.element;
if (this.cmp !== currentCmp) {
this.vcr.clear();
this.vcr.createComponent(currentCmp, {
injector: this.getInjector(matchesToRender),
});
this.cmp = currentCmp;
}
}),
takeUntil(this.destroy$)
)
.subscribe();
}
getInjector(matchesToRender: DataRouteMatch) {
const injector = Injector.create({
providers: [
{
provide: ROUTE_CONTEXT,
useValue: {
id: matchesToRender.route.id,
index: matchesToRender.route.index === true,
params: matchesToRender.params,
},
},
],
parent: this.vcr.injector,
});
return injector;
}
getMatch(routerState: RouterState) {
const { matches } = routerState;
const idx = matches.findIndex(
(match) => match.route.id === this.context?.id
);
const matchesToRender = matches[idx + 1];
return matchesToRender;
}
ngOnDestroy() {
this.destroy$.next(true);
}
}
出口是一个占位符指令,它监听路由状态的变化,并渲染与路由 ID 匹配的组件。Remix Router 知道所有路径,因此它会提供一个匹配项数组进行渲染。这使我们能够轻松处理嵌套路由。
可以使用指令定义父组件outlet以渲染子路由
@Component({
selector: 'home',
standalone: true,
imports: [Outlet],
template: `
Parent -
<a (click)="child('child')">Child</a>
<outlet></outlet>
`
})
export class ParentComponent {
router = getRouter();
}
定义路由
现在我有了路由器和插座,可以注册一些路由了。
import { RouteObject } from 'remix-router-angular';
import {
AboutComponent,
loader as aboutLoader,
action as aboutAction,
} from './about.component';
import { HomeComponent } from './home.component';
import { ParentComponent } from './parent.component';
import { ChildComponent } from './child.component';
export const routes: RouteObject[] = [
{ path: '/', element: HomeComponent },
{
path: '/parent',
element: ParentComponent,
children: [
{
path: ':child',
element: ChildComponent,
},
],
},
{
path: '/about',
element: AboutComponent,
action: aboutAction,
loader: aboutLoader,
},
];
该about路由使用一个loader用于加载数据的组件和一个action用于处理表单数据的组件。它们的工作方式与 Remix 中现有的组件完全相同。
export const action: ActionFunction = async ({ request }) => {
const formData = await request.formData();
const name = formData.get('name');
if (!name) {
return {
name: 'Name is required',
};
}
return redirect(`/?name=${name}`);
};
export const loader: LoaderFunction = async () => {
const res = await fetch('https://jsonplaceholder.typicode.com/todos/1');
const todos = await res.json();
return json({ todos });
};
操作和加载器允许您为路由预取数据、处理表单验证、重定向等。
提供路线
因为我使用的是带有独立功能的 Angular v14,所以我使用了该bootstrapApplication函数,并通过该函数传递了一些提供程序provideRoutes。
import { enableProdMode } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRoutes } from 'remix-router-angular';
import { AppComponent } from './app/app.component';
import { routes } from './app/routes';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
bootstrapApplication(AppComponent, {
providers: [provideRoutes(routes)],
});
我为使用新inject()功能从 Remix Router 获取操作/加载器数据以及其他部分进行了一些改进,但对于早期开发阶段来说,一切都运行得相当不错。
我很期待看到这个项目取得更多进展!Remix团队为此付出了辛勤努力,值得称赞。
您可以在这里查看演示应用:https://remix-router-angular.netlify.app/
GitHub 仓库:https://github.com/brandonroberts/remix-router-angular
了解更多
Remix
Angular
Angular v14 发布帖
remix-router-vue
如果你喜欢这篇文章,请点击❤️,让更多人看到。关注我的推特并订阅我的YouTube频道,获取更多关于Angular、NgRx等内容!
文章来源:https://dev.to/brandontroberts/remix-ing-routing-in-angular-4g90

