你应该考虑放弃 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 相关钩子的代码

相关推荐
酷酷的阿云3 分钟前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js
微信:137971205875 分钟前
web端手机录音
前端
齐 飞11 分钟前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
神仙别闹28 分钟前
基于tensorflow和flask的本地图片库web图片搜索引擎
前端·flask·tensorflow
aPurpleBerry1 小时前
JS常用数组方法 reduce filter find forEach
javascript
GIS程序媛—椰子1 小时前
【Vue 全家桶】7、Vue UI组件库(更新中)
前端·vue.js
DogEgg_0011 小时前
前端八股文(一)HTML 持续更新中。。。
前端·html
ZL不懂前端1 小时前
Content Security Policy (CSP)
前端·javascript·面试
乐闻x2 小时前
ESLint 使用教程(一):从零配置 ESLint
javascript·eslint
木舟10092 小时前
ffmpeg重复回听音频流,时长叠加问题
前端