Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(router): add simplified route generation option #67

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

Xy2002
Copy link
Contributor

@Xy2002 Xy2002 commented Nov 23, 2024

更加简单的路由

要使一个组件有路由功能,需要导出一个 createFileRoute(),因为目前选择的是 Tanstack Router 的 File-Based Routing. 虽然这一部分可以通过generate生成,但是如果碰到嵌套路由,使用这个命令就会有点反直觉,因此我的预期目标为:尽量不让这部分代码出现在组件文件内,示例代码如下:

// src/pages/foo/bar.tsx

export default function Bar() {
  const { users } = useLoaderData({
    from: '/foo/bar',
  });
  return (
    <div>
      <h3>Welcome Foo!</h3>
      <p>Users</p>
      <ul>
        {users.map((user: any) => (
          <li key={user.id}>{user.firstName}</li>
        ))}
      </ul>
    </div>
  );
}

// src/.tnf/routeTree.gen.ts

import { createRootRoute, createRoute } from '@tanstack/react-router'
import rootRouteImport from '../src/pages/__root.tsx'
import FooBarImport from '../src/pages/foor/bar.tsx'

const rootRouteComponent = createRootRoute({
  component: rootRouteImport
})

const FooBarComponent = createRoute({
  getParentRoute: () => rootRouteComponent,
  component: FooBarImport,
  path: '/foo/bar',
})

要达到这一目标,可以使用 Code-Based Routing ,在 TanStack Router 的官网里有写到:

Code-based routing is no different from file-based routing in that it uses the same route tree concept to organize, match and compose matching routes into a component tree. The only difference is that instead of using the filesystem to organize your routes, you use code.

Believe it or not, file-based routing is really a superset of code-based routing and uses the filesystem and a bit of code-generation abstraction on top of it to generate this structure you see above automatically.

因此,完全可以使用 Code-Based Routing 来替代 File-Based Routing 实现 TNF 中的路由功能,并简化路由。

只需按照 TanStack Router 的 File-Based Routing 路由规范( Code-Based Routing 和 File-Based Routing 的路由规范有差别,这里以 File-Based Routing 为准),实现路由路线的解析,生成一个符合 File-Based Routing 规范的路由路线,就可以平替原来的 generator 以自动生成 routeTree.gen.ts

本PR实现了路由生成的功能(如果这个方案ok的话,后面补充example和测试用例),但是缺少路由配置的功能,原因有两点:

  1. 不确定实现简化路由的 feature 这样做是否 ok
  2. 对于如何确定组件是否有导出 RouteConfig ,暂时没有确定的方案,可能要用 babel 或者 typescript 解析文件,不知道该用哪个方案比较好

路由配置的 feature 预期如下:

// src/pages/foo/bar.tsx

export const RouteConfig = {
  loader: () => {}
}

export default function Bar() {
  const { users } = useLoaderData({
    from: '/foo/bar',
  });
  return (
    <div>
      <h3>Welcome Foo!</h3>
      <p>Users</p>
      <ul>
        {users.map((user: any) => (
          <li key={user.id}>{user.firstName}</li>
        ))}
      </ul>
    </div>
  );
}

下面举一个例子,例子里尽可能覆盖了可能情况:

routes/
├── __root.tsx
├── index.tsx
├── about.tsx
├── posts.tsx
├── posts/
│   ├── index.tsx
│   ├── $postId.tsx
├── posts_/
│   ├── $postId
│   ├── ├── edit.tsx
├── posts.$postId.detail.tsx
├── settings/
│   ├── $type.tsx
│   ├── info.tsx
├── settings_.modify.tsx
├── _layout.tsx
├── _layout/
│   ├── layout-a.tsx
├── ├── layout-b.tsx   
├── files/
│   ├── $.tsx
├── splat/
│   ├── $.tsx
│   ├── $id.tsx
├── splat2/
│   ├── $.tsx
│   ├── ├── test.tsx
│   ├── $
│   ├── $id.tsx
├── splat3/
│   ├── $.tsx
│   ├── ├── test2.tsx
│   ├── $
│   ├── $id
│   ├── ├── test3.tsx
│   ├── $id.tsx
├── (auth)/
│   ├── login.tsx
│   ├── register.tsx
├── (auth).tsx (throw error, invaild route)

生成出来的代码:

import { createRootRoute, createRoute } from '@umijs/tnf/router';

import { Route as RootImport } from './../src/pages/__root'
import { Route as AboutImport } from './../src/pages/index'
import { Route as AboutImport } from './../src/pages/about'
import { Route as PostsImport } from './../src/pages/posts'
// ... 省略导入

// path: / , output: <Root>
const rootRoute = createRootRoute({
	component: RootImport
})

// path: / , output: <Root><RootIndex>
const indexRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: '/',
  component: IndexComponet
})

// path: /about , output: <Root><About>
const aboutRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: 'about',
  component: AboutImport
})

// 从这里开始,接下来的createRoute省略了component,除了pathless 路由,component是必须要配置的
// path: /posts, output: <Root><Posts>
const postsRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: 'posts',
})

// path: /posts, output: <Root><Posts><PostsIndex>
const postsIndexRoute = createRoute({
  getParentRoute: () => postsRoute,
  path: '/',
})

//path: /posts/$, output: <Root><Posts><Post>
const postRoute = createRoute({
  getParentRoute: () => postsRoute,
  path: '$postId',
})

//path: /posts/$/edit, output: <Root><PostEditor>
const postEditorRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: 'posts/$postId/edit',
})

//path: /posts/$/detail, output: <Root><Posts><Post><PostDetail>
const postDetailRoute = createRoute({
  getParentRoute: () => postRoute,
  path: 'detail',
})

// 最终需要生成的结果(忽略除了posts以外的路由)
const routeTree = rootRoute.addChildren([
  // The post editor route is nested under the root route
  postEditorRoute,
  postsRoute.addChildren([postRoute.addChildren(postDetailRoute)]),
])

// 在本例子中,Settings.tsx 不存在,因此跳过了渲染 <Settings>
// path: /settings/$, output: <Root><Settings>
const settingsRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: '/settings/$type',
})

// path: /settings/info, output: <Root><SettingsInfo>
const notificationsRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: '/settings/info',
})

// 以下划线(_) 为前缀的路由被视为 "无路径 "路由,用于用附加组件和逻辑封装子路由,而无需在 URL 中提供匹配路径
const layoutRoute = createRoute({
  getParentRoute: () => rootRoute,
  id: 'layout',
})

// path: /layout-a, output: <Root><Layout><LayoutA>
const layoutARoute = createRoute({
  getParentRoute: () => layoutRoute,
  path: 'layout-a',
})

// Layout -> LayoutIndenpendent -> Normal
parentRoute: Layout
path: 

// path: /layout-b, output: <Root><Layout><LayoutB>
const layoutBRoute = createRoute({
  getParentRoute: () => layoutRoute,
  path: 'layout-b',
})

// 这是 splat 路由
// 存在向后兼容的问题,详见 https://tanstack.com/router/latest/docs/framework/react/guide/routing-concepts#splat--catch-all-routes
// url path 为 files/* 时,获取到的param为{ _splat: $ }(*指代有效字符串)
// path: /files/*, output: <Root><Files>
const filesRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: 'files/$',
})

// 当存在 $ 和 $param 的情况时 (*指代有效字符串)
// url path 为 splat/* 时,会渲染splatIdRoute
// url path 为 splat/ 时,会渲染splatRoute
// path: /splat, output: <Root><Splat>
const splatRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: 'splat/$',
})

// path: /splat/*, output: <Root><SplatId> (*指代有效字符串)
const splatIdRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: 'splat/$id',
})

// 当存在 $ 和 $param 的情况时,且 $.tsx 和 $param.tsx 都实现的情况下
// $param 的优先级更高,此时就要渲染结果可以看下面两个例子

// 1. $param 目录下没有实现路由 请看代码块里的目录 splat2
// url path 为 splat2/$/test 时,会渲染 <Root><NotFound> 
// url path 为 splat/$ 时,会渲染 <Root><$Param>
// 解释:此时 $param 覆盖了 $,即使想访问已存在的 $/test ,是不存在的,因为路径匹配的是 $param/test
// 可是此时 $param 目录下没有 test.tsx

const splat2Route = createRoute({
  getParentRoute: () => rootRoute,
  path: 'splat2/$',
})

const splat2TestRoute = createRoute({
  getParentRoute: () => $Route,
  path: 'splat2/$/test',
})

// 这个path优先级更高
const splat2IdRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: 'splat/$',
})

// 2. $param 目录下有实现路由 请看代码块里的目录 splat3 (*指代有效字符串)
// url path 为 splat3/*/test2 时,会渲染 <Root><$Param><NotFound>
// url path 为 splat3/*/test3 时,会渲染 <Root><$Param><Test3>
// url path 为 splat3/* 时,会渲染 <Root><$Param>
// 解释:此时 $param 覆盖了 $,即使想访问已存在的 $/test2 ,也是不存在的,因为路径匹配的是 $param/test
// 可是此时 $param 目录下没有 test2.tsx ,但是,此时相比于上一个例子会多渲染一个 <$Param>

const splat3Route = createRoute({
  getParentRoute: () => rootRoute,
  path: 'splat3/$',
})

const splat3Test2Route = createRoute({
  getParentRoute: () => $Route,
  path: 'splat3/$/test2',
})

const splat3IdRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: 'splat/$id',
})

const splat3Test3Route = createRoute({
  getParentRoute: () => splat3IdRoute,
  path: 'splat3/$id/test3',
})

// 无路径路由组
// path: /login, output: <Root><Login>
const loginRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: 'login',
})

// path: /login, output: <Root><Register>
const registerRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: 'register',
})

Please don't delete this checklist! Before submitting the PR, please make sure you do the following:

  • It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC.
  • This message body should clearly illustrate what problems it solves.
  • Ideally, include a test that fails without this PR but passes with it.

Tests

  • Run the tests and other checks with pnpm ci

Changesets

  • If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running pnpm changeset and following the prompts. Changesets that add features and fix bugs should all be patch before we release 0.1.0. Please prefix changeset messages with feat:, fix:, or chore:.

Edits

  • Please ensure that 'Allow edits from maintainers' is checked. PRs without this option may be closed.

Copy link

changeset-bot bot commented Nov 23, 2024

🦋 Changeset detected

Latest commit: c84d716

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@Xy2002
Copy link
Contributor Author

Xy2002 commented Nov 23, 2024

当时想到还有一个方案其实也不错,可以通过复制pages的文件结构到临时文件夹下面,然后给相应的文件写入 import component 和 createFileRoute,但是这样会生成大量的临时文件,于是就放弃了这个方案

@sorrycc
Copy link
Member

sorrycc commented Nov 25, 2024

当时想到还有一个方案其实也不错,可以通过复制pages的文件结构到临时文件夹下面,然后给相应的文件写入 import component 和 createFileRoute,但是这样会生成大量的临时文件,于是就放弃了这个方案

@Xy2002

1、tanstack router 的 generator 支持虚拟文件吗?支持的话这种方式会更好。
2、不太想手写 generator,1)感觉还是缺了一些能力,比如 lazy component,2)跟官方功能会比较累。

@Xy2002
Copy link
Contributor Author

Xy2002 commented Nov 25, 2024

@Xy2002

1、tanstack router 的 generator 支持虚拟文件吗?支持的话这种方式会更好。 2、不太想手写 generator,1)感觉还是缺了一些能力,比如 lazy component,2)跟官方功能会比较累。

1、根据文档来说,虚拟文件路由是和 file-based route-tree generation 相结合的,所以应该是支持的
2、lazy component 也可以支持,只是问题关键确实是在于跟官方功能会比较累,仔细想了想,如果用这种方案的话,createFileRoute 的path参数的也需要进行额外的实现才能自动生成。

要达到简化路由的目的,我觉得最核心的点就是自动生成path,因为 File-Based Routing 的最好体验就是自动生成 path ,不然会产生额外的心智负担。

刚刚看了下 tsr 的 generator 和 vite plugin 的源码,我觉得可以移植成 tnf 的 plugin ,这应该是目前来看比较好的办法,我再试试。

@sorrycc
Copy link
Member

sorrycc commented Nov 25, 2024

刚刚看了下 tsr 的 generator 和 vite plugin 的源码,我觉得可以移植成 tnf 的 plugin ,这应该是目前来看比较好的办法,我再试试。

👍🏻

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants