你应该考虑放弃 react-router 的数据路由模式,改而使用更加适合国内版本的封装版本(包含完整可 CV 的模版)

你应该考虑放弃 react-router 的数据路由模式,改而使用更加适合国内版本的封装版本(包含完整可 CV 的模版)

React 的路由库

React 只有核心是官方的,Nextjs 可以算半个官方的。这会造成什么问题呢?

那就是使用上的撕裂感,和各种相关库的野蛮生长。好处是有了更多的选择,坏处是实现上可能存在冗余,以及会对很多编码能力不强的同学造成很大的困扰

能叫上名的 React 路由是有好几个的,但我们最熟悉,国内用的最普遍的是 react-router,它现在整合了 remixjs 的路由包,对外的名称叫 react-router-dom,相当于三个库的结合体,也是本篇文章讲解的对象

数据路由

数据路由被叫做 data-router,核心功能都是从 remix-router 里引进来,它带来了许多的新特性,例如懒加载,最快的数据加载 loader 机制form-action 的交互模式等等

这里我们比较喜欢的是,如下开箱即用的功能

  1. 懒加载
  2. 返回路由匹配项 ------ 通过 hooks 函数直接获取到进入的是哪些路由,对于鉴权之类的功能,很有帮助
  3. Loader 机制 ------ 可以让进入的路由项并发的执行一个函数,通用用于数据加载的提速
  4. Promise 的组件化使用
  5. 围绕路由项周边的辅助功能,比如路由进入异常可以给个友好提示

这还只是个别功能,其他强大的功能也不少。看似美好的表面藏着的,却是国内项目很难用到有甩不掉的包袱,即它提供的新功能我们可能只能用到一小部分,加上也许会用到的撑破天应该都不足一半,如果只是盲目的按照官方推荐方式使用,那就得把另一半用不到的内容也带上

为了那一点点 easy 的功能,却会使得你的应用体积暴增

显然这是不划算的,我们只需要小小的封装也可以做到类似事情,本篇文章都会给你讲到

至于为什么我们用不上官方还推呢?也许国外的大佬们更喜欢吧

打包数据对比

图片太多我这里只把我测试的,数据、方式、结果,简单的贴出来,感兴趣的可以自己尝试

测试步骤为

  1. 使用 vite 创建一个 ts-react 应用
  2. 安装 react-router-dom@^6.22.2,这是测试时能安装到的最新版本
  3. 创建 3 个子路由,代码在下边
  4. 同样的功能分别用,数据路由和非数据路由的方式实现
  5. Dev 测试切换无报错,然后打包对比体积

子路由代码

tsx 复制代码
//类似于这样的代码,写三个,作为三个页面
export const Home = () => {
  return <div>
    <h1>home</h1>
  </div>
}

数据路由

tsx 复制代码
// 文件路径 src/DataRouter.tsx
import { createBrowserRouter } from "react-router-dom"
import { Home } from "./pages/Home"
import { Page1 } from "./pages/Page1"
import { Page2 } from "./pages/Page2"
import { App } from "./App"

// App 组件是全局的根组件
export const DataRouter = createBrowserRouter([
  {
    path: "/",
    element: (
      <App>
        <Home />
      </App>
    )
  },
  {
    path: "page1",
    element: (
      <App>
        <Page1 />
      </App>
    )
  },
  {
    path: "page2",
    loader: () => 1,
    element: (
      <App>
        <Page2 />
      </App>
    )
  }
])

// App.tsx
import { FC, ReactNode } from "react"
import { Link } from "react-router-dom"

export const App: FC<{ children: ReactNode }> = ({ children }) => {
  return (
    <div>
      <div className="nav" style={{ display: "flex", gap: "1rem" }}>
        <Link to="/">to home</Link>
        <Link to="/page1">to page1</Link>
        <Link to="/page2">to page2</Link>
      </div>
      <hr />
      {children}
    </div>
  )
}


// 入口文件 main.ts
import React from "react"
import ReactDOM from "react-dom/client"
import { DataRouter } from "./DataRouter.tsx"
import { RouterProvider } from "react-router-dom"

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <RouterProvider router={DataRouter}  />
  </React.StrictMode>
)

一般写法,非数据路由

tsx 复制代码
//App.tsx 和上边一样
//main.tsx 
import ReactDOM from "react-dom/client"
import { App } from "./App.tsx"
import { BrowserRouter, useRoutes } from "react-router-dom"
import { Home } from "./pages/Home.tsx"
import { Page1 } from "./pages/Page1.tsx"
import { Page2 } from "./pages/Page2.tsx"
import React from "react"

const AppRoutes = () => {
  return useRoutes([
    {
      path: "/",
      element: <Home />
    },
    {
      path: "/page1",
      element: <Page1 />
    },
    {
      path: "/page2",
      element: <Page2 />
    }
  ])
}

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <BrowserRouter>
      <App>
        <AppRoutes />
      </App>
    </BrowserRouter>
  </React.StrictMode>
)

打包结对对比

会发现只是单纯写了个路由,什么都没干,体积膨胀了 36.13kb

如果继续使用相关的 hooks 体积还会继续膨胀

演示中 App 组件是用来做全局包裹的组件,这里可以给一些公共的样式,依赖注入一些数据等,几乎是必备的,会发现数据路由下用起来很麻烦

如果你可以接受那就可以关闭这篇文章啦~

ts 复制代码
//数据路由
dist/index.html                  0.40 kB │ gzip:  0.27 kB
dist/assets/index-98d03929.js  162.97 kB │ gzip: 53.17 kB
✓ built in 961ms

//一般写法
dist/index.html                  0.40 kB │ gzip:  0.27 kB
dist/assets/index-2708b45c.js  199.10 kB │ gzip: 64.82 kB
✓ built in 991ms

整理下路由需要哪些功能

这里拿 vue-router 作对比,它足够的成熟,适用范围足够的广,这是全球开发者共同印证的事实

直接说结论,为了满足我们日常所需以下功能几乎是必备的

  1. 懒加载
  2. 生命周期/路由守卫
  3. 提供对外做动画的出口
  4. 组件内得能获取到路由的信息,越多越好

对于不适用数据路由的 react-router 来说

1,2 不提供,3,4 支持,但 4 刻意做的很有限(和 vue-router 比起来提供的信息少很多很多)

我们的目标也很明确,把 1,2 自己手动实现即可

至于其他的都是些锦上添花的功能,比如把 Promise 组件化之类的

懒加载

惰性加载分为两步

  1. 惰性路由的代码单独打包,这是打包工具都具备的基本能力,只要用 import() 引入就会生效
  2. 返回的结果是 Promise,框架得能解析,或者能根据开发者自定义的返回,正常运行

React. Lazy 是官方提供的,用它做个简单就够了

tsx 复制代码
const LazyLoad = (loader: () => Promise<any>) => {
  Const Element = lazy (loader)
  Return () => (
    <Suspense>
      <Element />
    </Suspense>
  )
}

使用也很简单

tsx 复制代码
LazyLoad (() => import ("./pages/Home"))

生命周期 / 路由守卫

生命周期想要实现的完整会非常的复杂,结合我们经常使用的场景来说,只有全局进入 是刚需,即当进入页面时,只有当符合要求了才能正常的加载显示页面,否则就给重定向到别的地方

路由也是由一个个 React 组件组成的,所以符合一般组件的加载顺序,即从上到下,一层一层顺序创建,我们想做个卡点其实相当于我们需要给每个路由创建一个父组件,动态的判断是否要显示真正的路由组件

这里有个技巧,就是我们没有必要给每个路由都创建个父组件,只需要给每条路由的那个祖先组件创建一个就行了,例如

tsx 复制代码
<Route name="祖先1" element={<Comp />}>
  <Route name="爷爷">...</Route>
</Route>
<Route name="祖先2" element={<Comp />}>
  <Route name="爷爷">...</Route>
</Route>

这是伪代码,里边每个最外层的就是组件组件,然后它可以嵌套,感觉会像链子一样

给祖先组件创建组件,是可以使用 useLocation 钩子的,有了它就能知道进来的 patname 路由路径,即便是深层次的子路由路径变化,也是能触发的。所以给祖先组件套一层,就能监控到所有的路由变化

我们可以简单的封装出以下结构

tsx 复制代码
Export interface GuardRouteProps {
  Element: FC<any>
}
export const GuardRoute: FC<GuardRouteProps> = ({ Element }) => {
  Const { pathname } = useLocation ()
  const [RouteComp, setRouteComp] = useState<FC>(() => null) 
  Const nav = useNavigate ()
  UseEffect (() => {
    If (RouteComp === Element) return
    // todo... 路由守卫的校验逻辑
    //通过 setRouteComp (Element)
    //不通过 nav ("/login")
  }, [])
  return <Element />
}

获取路由信息

这样做有个问题

我们不知道哪个路由,是否应该用什么策略来干些不同的事,比如页面 A 和页面 B 的校验逻辑不同,只知道一个路由路径我们无法做差异化配置

这里就可以做个 map 备用,对于路由的定义我们写出以下代码

tsx 复制代码
Type GRouteObject = RouteObject & {
  meta?: Record<string, any>
}
Export const routes: GRouteObject[] = [
  {
    Path: "/",
    element: <GuardRoute Element={LazyLoad(() => import ("./pages/Home"))} />
  },
  {
    Path: "/page 1",
    element: <GuardRoute Element={LazyLoad(() => import ("./pages/Page 1"))} />
  },
  {
    Path: "/page 2",
    element: <GuardRoute Element={LazyLoad(() => import ("./pages/Page 2"))} />,
  }
]

Const mapRoutesToMap = (
  Routes: GRouteObject[],
  map: Map<string, GRouteObject>
) => {
  Routes.ForEach (route => {
    Map.Set (route. Path!, route)
    If (route. Children) {
      MapRoutesToMap (route. Children, map)
    }
  })
  Return map
}
Export const routesMap = mapRoutesToMap (routes, new Map ())

吐槽:react-router-domts 类型声明连个扩展都不给,还得自己定义,人家 Vue 直接给了个 meta 给用户使用

之所以吐槽是因为,我们开发者是不知道将来的某个版本中,我们使用的某个字段是否会被库作者使用,反之 vue-router 明确给了个 meta 字段就代表它只会被用于用户使用,就不会产生可能的意外冲突

这里我们扩展出一个 meta 字段用户写一些差异化的配置

制作这个 routesMap 并不会拉胯性能,它会递归的把嵌套路由拉成一级

它能帮助封装的路由守卫获取差异化配置,对于后台管理系统的左侧菜单也会有帮助

tsx 复制代码
Export interface GuardRouteProps {
  Element: FC<any>
}
export const GuardRoute: FC<GuardRouteProps> = ({ Element }) => {
  Const { pathname } = useLocation ()
  const [RouteComp, setRouteComp] = useState<FC>(() => null) 
  Const nav = useNavigate ()
  //react-router-dom 会帮我们把 query/hash/多余的符号去掉,只要不是动态参数是一定能够匹配到的
  Const { meta = {} } = routesMap.Get (pathname)
  UseEffect (() => {
    If (RouteComp === Element) return
    // todo... 路由守卫的校验逻辑
    //通过 setRouteComp (Element)
    //不通过 nav ("/login")
  })
  return <Element />
}

完整代码

到此一个很普通的模版就做好了,我们规整下就可以 cv 直接用了

tsx 复制代码
//router. Tsx
Import { FC, Suspense, lazy, useEffect, useState } from "react"
Import {
  RouteObject,
  UseLocation,
  UseNavigate,
  UseRoutes
} from "react-router-dom"

Export interface GuardRouteProps {
  Element: FC<any>
}
export const GuardRoute: FC<GuardRouteProps> = ({ Element }) => {
  Const { pathname } = useLocation ()
  const [RouteComp, setRouteComp] = useState<FC>(() => null)
  Const nav = useNavigate ()
  //react-router-dom 会帮我们把 query/hash/多余的符号去掉,只要不是动态参数是一定能够匹配到的
  Const { meta = {} } = routesMap.Get (pathname)
  UseEffect (() => {
    If (RouteComp === Element) return
    // todo... 路由守卫的校验逻辑
    //通过 setRouteComp (Element)
    //不通过 nav ("/login")
  })
  return <Element />
}

const LazyLoad = (loader: () => Promise<any>) => {
  Const Element = lazy (loader)
  Return () => (
    <Suspense>
      <Element />
    </Suspense>
  )
}

Type GRouteObject = RouteObject & {
  meta?: Record<string, any>
}
Export const routes: GRouteObject[] = [
  {
    Path: "/",
    element: <GuardRoute Element={LazyLoad(() => import ("./pages/Home"))} />
  },
  {
    Path: "/page 1",
    element: <GuardRoute Element={LazyLoad(() => import ("./pages/Page 1"))} />
  },
  {
    Path: "/page 2",
    element: <GuardRoute Element={LazyLoad(() => import ("./pages/Page 2"))} />
  }
]
Export const useRouterRoutes = () => routes

Const mapRoutesToMap = (
  Routes: GRouteObject[],
  map: Map<string, GRouteObject>
) => {
  Routes.ForEach (route => {
    Map.Set (route. Path!, route)
    If (route. Children) {
      MapRoutesToMap (route. Children, map)
    }
  })
  Return map
}
Export const routesMap = mapRoutesToMap (routes, new Map ())
Export const useRoutesMap = () => routesMap

Export const AppRoutes = () => {
  Return useRoutes (routes)
}

// main. Tsx
Import ReactDOM from "react-dom/client"
Import { App } from "./App. Tsx"
Import { BrowserRouter } from "react-router-dom"
Import { AppRoutes } from "./router. Tsx"

ReactDOM.CreateRoot (document.GetElementById ("root")!). Render (
  <BrowserRouter>
    <App>
      <AppRoutes />
    </App>
  </BrowserRouter>
)

其他功能 ------ 动态参数,SSR,Loader

动态参数目前实现不支持,尽量用 query 参数代替即可,如果需要可以做以下扩展

tsx 复制代码
export const GuardRoute: FC<GuardRouteProps> = ({ Element }) => {
  Const { pathname } = useLocation ()
  const [RouteComp, setRouteComp] = useState<FC>(() => null)
  Const nav = useNavigate ()
  //react-router-dom 会帮我们把 query/hash/多余的符号去掉,只要不是动态参数是一定能够匹配到的
  Let route: GRouteObject | undefined = routesMap.Get (pathname)
  If (! Route) {
    Const path = Array.From (routesMap.Keys ()). Find (path => !!UseMatch (path))
    If (! Path) {
      // 说明没有一个兜底的,自己看着办
      Throw "说明没有一个兜底的,自己看着办"
    }
    Route = routesMap.Get (path)
  }

  UseEffect (() => {
    If (RouteComp === Element) return
    // todo... 路由守卫的校验逻辑
    //通过 setRouteComp (Element)
    //不通过 nav ("/login")
  })
  return <Element />
}

useMatch 会拿我们自己的路径,经过编译再和实际的路由路径对比,它可以匹配动态路径参数

如果你能理解并且能靠自己手写出来这个结构,那么你可以尝试看看我的另一篇文章,尝试自己实现个 loader 机制

这里只是个半成品代码,因为存在匹配顺序的问题,觉得复杂的话就用 query 代替所有动态路径即可,这是百分百可以轻松做到的

标题是:如何设计并实现一个所谓"全网最快的路由数据加载 loader" ------ vue/react

地址 juejin.cn/post/729709...

总结

本文给出的模版直接 CV 就足够日常所需了,扩展起来也容易

因为自己的封装中没有用到数据路由相关的 api,经过打包工具 treeshaking 后就不会携带这些死代码,最后给出完整的打包对比

bash 复制代码
/* 
Dist/index. Html                  0.40 kB │ gzip:  0.27 kB
Dist/assets/index-98 d 03929. Js  162.97 kB │ gzip: 53.17 kB
✓ built in 961 ms

Dist/index. Html                  0.40 kB │ gzip:  0.27 kB
Dist/assets/index-2708 b 45 c. Js  199.10 kB │ gzip: 64.82 kB
✓ built in 991 ms



Dist/index. Html                  0.40 kB │ gzip:  0.27 kB
Dist/assets/Error-0 cc 7 b 8 e 1. Js    0.11 kB │ gzip:  0.12 kB
Dist/assets/Page 1-3 e 256 cac. Js    0.12 kB │ gzip:  0.12 kB
Dist/assets/Page 2-319 ed 62 d. Js    0.14 kB │ gzip:  0.14 kB
Dist/assets/Home-e 8 de 57 d 5. Js     0.16 kB │ gzip:  0.15 kB
Dist/assets/index-8 de 41 b 2 e. Js  164.70 kB │ gzip: 53.93 kB
✓ built in 1.00 s
*/

最下边是经过自己封装后打包的体积,体积碰撞的范围非常有限,和使用了数据路由后的代码对比还是相当明显的

如果细化的话,我们封装的代码其实连 1kb 都没,ts 代码不会打包进去,膨胀的代码主要来源于项目代码+ react router 相关钩子的代码

相关推荐
黄毛火烧雪下9 小时前
React Native (RN)项目在web、Android和IOS上运行
android·前端·react native
fruge9 小时前
前端正则表达式实战合集:表单验证与字符串处理高频场景
前端·正则表达式
baozj9 小时前
🚀 手动改 500 个文件?不存在的!我用 AST 撸了个 Vue 国际化神器
前端·javascript·vue.js
用户4099322502129 小时前
为什么Vue 3的计算属性能解决模板臃肿、性能优化和双向同步三大痛点?
前端·ai编程·trae
海云前端19 小时前
Vue首屏加速秘籍 组件按需加载真能省一半时间
前端
蛋仔聊测试9 小时前
Playwright 中route 方法模拟测试数据(Mocking)详解
前端·python·测试
零号机9 小时前
使用TRAE 30分钟极速开发一款划词中英互译浏览器插件
前端·人工智能
molly cheung10 小时前
FetchAPI 请求流式数据 基本用法
javascript·fetch·请求取消·流式·流式数据·流式请求取消
疯狂踩坑人10 小时前
结合400行mini-react代码,图文解说React原理
前端·react.js·面试
Mintopia10 小时前
🚀 共绩算力:3分钟拥有自己的文生图AI服务-容器化部署 StableDiffusion1.5-WebUI 应用
前端·人工智能·aigc