React-router从0到1

2,574 阅读17分钟

Tags: JavaScript, React

引子

本文会讨论react生态下的常用路由库,React-router的版本迭代与源码架构,并尝试探讨路由思维的变化与未来。

什么是路由?

路由是一种向用户显示不同页面的能力。 这意味着用户可以通过输入URL或单击页面元素在WEB应用的不同部分之间切换。

版本

为了探究react-router设计思维,从v3开始有这几个版本:

  • react-router 3「静态路由」
  • react-router 4「动态路由」
  • react-router 5「意外发布」
  • @reach/router「简化轻量」
  • react-router 6「完全方案」

让我们逐个参与讨论。

react-router3:静态路由

静态路由的设计如下图所示:

React.render((
  <Router>
    <Route path="/" component={Wrap}>
      <Route path="a" component={App} />
      <Route path="b" component={Button} />
    </Route>
  </Router>
), document.body)

特点:

  • 路由集中在外层
  • 页面路由配置通过Route组件的嵌套而来
  • 布局和页面组件是完全纯粹的,它们是路由的一部分

v3静态路由的设计对前端工程师来说,相对更易接受,因为前端工程师很多都接触过类似的路由配置设计,比如express、rails等框架。

虽然细节各有不同,但是思路大致相同——将path静态映射为渲染模块。

react-router4:动态路由

虽然v3以一种质朴无华的方式完成了基本的路由工作,但react-router的几个核心成员感觉现有的实现严重受ReactAPI的制约,并且实现方式也不够优雅。

于是,经过了激烈的思考与讨论,他们大胆地在v4中做出了比较激进的更迭。

React-router4不再提倡静态路由的集中化架构,取之的是路由存在于布局和 UI 之间:

const App = () => (
    <BrowserRouter>
      <div>
      <Route path="/a" component={A}/>
      </div>
    </BrowserRouter>
);

const A  = ({ match }) => (
    <div>
     <span>A</span>
      <Route
     path={match.url + '/b'}
     component={B}
      />
    </div>
);

const B = () => <div>B</div>;

我们来看以上代码的逻辑

  1. 一开始在App组件里,只有一个路由/a
  2. 用户跳转访问/a时,渲染A组件,浏览器上出现字母A,然后子路由/b被定义
  3. 用户跳转访问/a/b时,渲染B组件,浏览器上出现字母B

我们可以看到,在v4中:

  • 路由不再集中在一处
  • 布局和页面的层叠不再由层叠的<Route>组件控制,<Route>与组件为替换的关系
  • 布局和页面组件也不在是路由的一部分

这被称之为「动态路由」。

动态路由

传统静态路是在程序渲染前就定义好。

而动态意味着路由功能在应用渲染时才动态生成,这需要把路由看成普通的 React 组件,传递 props 来正常使用,借助它来控制组件的展现。这样,没有了静态配置的路由规则,取而代之的是程序在运行渲染过程中动态控制的展现。

动态路由将带来很大的好处。比如代码分割,也就是react常说的code splitting,由于不需要在渲染前决定结果,动态路由可以满足代码块的按需加载,这对于大型在线应用非常有帮助。

但是,毕竟路由对一个应用的架构来说非常重要,这么大的改变显得过于激进,这会改变以前开发者比较习惯的一些模式,由于这次的更新过于激进,遭到了开发者们的一些负面反馈:

这就要讨论到动态路由的缺点了:

  • 不够直观,你无法从顶层知道程序中所有的路由,应用一层一层下来,搞不清最后显示出来什么,可读性很差
  • 测试困难。组件中掺杂了路由逻辑,原本对针对组件的单元测试(功能层面)完全不需要知道路由的存在,而现在就要考虑了

由于React-router团队保证v3会持续维护,所以当时很多开发者没有选择升级。

react-router5:沿用

原本只是计划发布 React Router 4.4 版本,但由于不小心误用了^字符,将依赖错误地写成 "react-router": "^4.3.1",导致报错。于是最后团队决定撤销 4.4 版本,直接改为发布 React Router v5。

react-router5延续了动态路由的模式,但是提供了更加直观的写法:

export default function App() {
  return (
    <Router>
      <Switch>
        <Route path="/about">
          <About />
        </Route>
        <Route path="/topics">
          <Topics />
        </Route>
        <Route path="/">
          <Home />
        </Route>
      </Switch>
    </Router>
  );
}

以上的写法,/about显示<About>组件,/topics显示<Topic>组件,根路由显示<Home>组件。

同时,v5还允许你将路由配置作为一个config的json数据,写在组件外引入。

<Route>将作为父组件用于匹配路由,同时还有一系列辅助组件,比如<Switch>可以限制子元素进行单一的路由匹配。当然,这也会带来一定的

@reach/router:简洁

Reach-Router 是 ReactRouter 成员 Ryan Florence 开发的一套基于 react 的路由控件。

那么已经有比较成熟的 ReactRouter 了, 为什么要”再”做一套 Router 呢?

  • Accessibility「易用」
  • 相对链接的跳转方式
  • 嵌套的路由配置
  • 合适的路径优先(顺序不会造成影响)等等

优点:小而简

  • 4kb,压缩后比react-router小40kb左右,同时有更少的配置
  • 比起react-router需要3个包(historyreact-router-domreact-router-redux),reach-router只需要一个
  • 不需要在store配置router相关信息
  • 不需要显示的使用history
  • 基本一样的api,学习成本非常低
  • 源码非常简洁,总共就3个文件,900行

react-router6:终极方案

2021年11月,react-router 6.0.0 正式版发布:

  • 全部用 ts 重写
  • 不以 '/' 开头,都是「相对路径」
  • 路由按照最佳匹配选择,可以嵌套或者分散

v6的设计可以说很大程度参照了@reach/router,API和@reach/router v1.3非常相似。因此,官方也宣称v6可以被看做@reach/router的v2。

总体来说,v6更像是一个以前版本的完善和整合,相对路径与嵌套分散的选择方式,让大家能够按个人喜好去构建路由。

源码

探讨完设计哲学与版本更迭,我们正式进入从0到1的源码学习。

本文对源码的探讨,就是以v6为基础(中间存在各种简化)。

我们先从V6的简易的实例开始:

import { render } from "react-dom";
import {
  BrowserRouter,
  Routes,
  Route,
  Link,
} from "react-router-dom";
import App from "./App";
import Expenses from "./routes/expenses";
import Invoices from "./routes/invoices";

const rootElement = document.getElementById("root");
render(
  <BrowserRouter>
    <Routes>
      <Route path="/" element={<App />}>
        <Route path="expenses" element={<Expenses />} />
        <Route path="invoices" element={<Invoices />} />
      </Route>
    </Routes>
    <Link to="/invoices">Check Invoices</Link>
  </BrowserRouter>,
  rootElement
);

React-router的结构主要分为四个模块:

  • History:

    • history
    • 「状态机」
    • 负责路由的状态的管理和记录
  • Router:

    • <Router>
    • 「路由管理者」
    • 负责自上到下传递路由数据
  • Route:

    • <Route>
    • 「路由端口」
    • 路由对应组件配置
  • Link:

    • <Link /><Navigate />
    • 「导航」
    • 负责导航的跳转链接

让我们分别对各部分的源码进行拆分与讨论。

history

每个<Router>都会创建一个history对象,它记录了当前以及历史的路由位置。

react-router使用了history库作为路由历史状态的管理模块:

history这个库可以让你在 JavaScript 运行的任何地方都能轻松地管理回话历史,history对象抽象化了各个环境中的差异,并提供了最简单易用的的 API 来给你管理历史堆栈、导航,并保持会话之间的持久化状态。——React Training 文档

这部分值得关注的源码:

  1. 工厂函数createBrowserHistory

    它们代码差别很小,不同的router只有parsePath的入参不同。还有其它的差别,比如hashHistory增加了hashchange事件的监听等

    由于篇幅所限,这里我们只讨论createBrowserHistory

  2. history.push,用于基本的切换路由

    go/replace/forward/back也类似,不过pushhistory栈变化的基础

  3. history.listen

    添加路由监听器,每当路由切换可以收到最新的actionlocation,从而做出不同的判断,BrowserRouter中就是通过history.listen(setState)来监听路由的变化,从而管理所有的路由

  4. history.block

    添加阻塞器,会阻塞push等行为和浏览器的前进后退,阻止离开当前页面。且只要判断有 blockers,那么同时会阻止浏览器刷新、关闭等默认行为。且只要有blocker,会阻止上面listener的监听

createBrowserHistory

我们先看工厂函数:

工厂函数的用途是创建一个history对象,后面的listenunlisten都是挂载在这个API的返回对象上面的。

  • history.listen:这个是用在Router组件里面的,用来监听路由变化
  • history.unlisten:这个也是在Router组件里面用的,是listen方法的返回值,用来在清理的时候取消监听的
export function createBrowserHistory(
  options: BrowserHistoryOptions = {}
): BrowserHistory {
 // -----------------------------第一部分--------------------------------
 const [index, location] = getIndexAndLocation();

 function getIndexAndLocation(): [number, Location] {
   const { pathname, search, hash } = window.location;
   const state = window.history.state || {};
   return [
     state.idx,
     readOnly<Location>({
       pathname,
       search,
       hash,
       state: state.usr || null,
       key: state.key || 'default'
     })
   ];
 }

 if (index == null) {
   index = 0;
   window.history.replaceState({ ...window.history.stateidx: index }, '');
 }

 function handlePop() {
   const [nextIndex, nextLocation] = getIndexAndLocation();
   const delta = index - nextIndex;
   go(delta)
 }

 window.addEventListener('popstate', handlePop);

  // ----------------------------第二部分-------------------------------

  const listeners = createEvents<Listener>();
 const blockers = createEvents<Blocker>();
 
 function createEvents<F extends Function>(): Events<F> {
   let handlers: F[] = [];
 
   return {
     get length() {
       return handlers.length;
     },
     push(fn: F) {
       handlers.push(fn);
       return function() {
         handlers = handlers.filter(handler => handler !== fn);
       };
     },
     call(arg) {
       handlers.forEach(fn => fn && fn(arg));
     }
   };
 }
 
 listeners.call({ action, location });
 blockers.call({ action, location, retry });
  
  // ----------------------------第三部分------------------------------—

 const historyBrowserHistory = {
   get action() {
     return action;
   },
   get location() {
     return location;
   },
   createHref,
   push, // 重点
   replace,
   go(delta: number) {
     window.history.go(delta);
   },
   back() {
     go(-1);
   },
   forward() {
     go(1);
   },
   listen(listener) { // 重点
     return listeners.push(listener);
   },
   block(blocker) {  // 重点
     const unblock = blockers.push(blocker);
     if (blockers.length === 1) {
       window.addEventListener('beforeunload', promptBeforeUnload);
     }
     return function() {
       unblock();
       if (!blockers.length) {
         window.removeEventListener('beforeunload', promptBeforeUnload);
       }
     };
   }
 };
  return history
}

我们可以将源码分为三部分:

  • 第一部分「初始化和绑定」

    通过getIndexAndLocation获取初始当前路径的indexlocation,初始index为空,对应history.state.idx为0。

    同时,handlePopwindow监听url的变化,在handleState里面进行触发。

  • 第二部分「发布订阅」

    我们看到这部分是个标准的发布订阅模式:

    createEvents是创建listenersblockers的工厂函数,其返回了一个对象,通过push添加每个listener,通过call通知每个 listener,代码中叫做handler

    listeners通过call传入actionlocation,这样每个listener在路由变化时就能接收到,从而做出对应的判断

    blockers,比listeners多了传入了一个retry,从而判断是否要阻塞路由,不阻塞的话需要调用函数retry

  • 第三部分「构建history」

    我们可以看看得到的history对象

    • action代表上一个修改当前locationactionPOP/PUSH/REPLACE
    • actionlocation这两个属性都通过修饰符get获取,那么我们每次要获取就可以通过history.actionhistory.location。避免了只能拿到第一次创建的值,可以每次调用函数才能拿到。
    • createHref作用是通过location返回新的href, to为字符串则返回to,否则返回pathname+search+hash
    • backforward都通过go实现

    这里我们重点关注:pushlistenblock

history.push

replacepush非常相似,区别在于replace将历史堆栈中当前location替换为新的,被替换的将不再存在,所以我们着重关注push

function push(toTo, state?: State) {
  const nextAction = Action.Push;
  const nextLocation = getNextLocation(to, state);

 function getNextLocation(toTo, state: State = null): Location {
   return readOnly<Location>({
     ...location,
     ...(typeof to === 'string' ? parsePath(to) : to),
     state,
     key: createKey()
   });
 }

  function retry() {
    push(to, state);
  }

  if (allowTx(nextAction, nextLocation, retry)) { // blockers的限制

    const [historyState, url] = getHistoryStateAndUrl(nextLocation, index + 1);

  function getHistoryStateAndUrl(
    nextLocation: Location,
    index: number
  ): [HistoryState, string] {
    return [
      {
        usr: nextLocation.state,
        key: nextLocation.key,
        idx: index
      },
      createHref(nextLocation)
    ];
  }
  
  window.history.pushState(historyState, '', url);

    try {
      globalHistory.pushState(historyState, '', url);
    } catch (error) {
      window.location.assign(url);
    } // 用try-catch的原因是因为ios限制了100次pushState的调用,catch后只能选择刷新页面

    applyTx(nextAction); // 调用listeners
  }
}
  • allowTx下面blockers会讲到,用于阻塞路由

  • applyTx下面listeners会讲到,用于调用监听器

  • getNextLocation

    路由还没切换的时候,根据history.pushtostate(新的path和状态)获取到新的 location

    to是字符串的话,会通过parsePath解析对应的pathnamesearchhash(三者都是可选的,不一定会出现在返回的对象中)

  • getHistoryStateAndUrl

    根据新的location获取新的stateurl

    因为是push,这里的index自然是加一

    再调用createHref,根据location生成url

  • 最后调用history.pushState成功跳转页面,这个时候路由也就切换了

history.listener

const history: HashHistory = {
  // ...
  listen(listener) {
    return listeners.push(listener);
  },
  // ...
}

function applyTx(nextAction: Action) {
  const [index, location] = getIndexAndLocation();
  listeners.call({ action: nextAction, location });
}

function push(to: To, state?: State) { // replace
  // ...
  if (allowTx(nextAction, nextLocation, retry)) {
    // ...
    applyTx(nextAction);
  }
}

function handlePop() {
  if (blockedPopTx) {
    // ...
  } else {
    // ...
    if (blockers.length) {
    // ...
    } else {
      applyTx(nextAction);
    }
  }
}

function allowTx(action: Action, location: Location, retry: () => void): boolean {
  return (
    !blockers.length || (blockers.call({ action, location, retry }), false)
  );
}

history.listen是一个标准的发布订阅模式,可以往history中添加listener,返回一个取消监听的可调用方法

  • listenerpushreplacehandlePop三个函数中成功切换路由后调用
  • 每当成功切换路由,就会调用applyTx(nextAction)来通知每个listener
  • allowTx的作用是判断是否允许路由切换,有blockers就不允许,也即是说,listener能否监听到路由变化,取决于当前页面是否被blockers阻塞了

history.block

const history: BrowserHistory = {
  // ...
  block(blocker) {
    const unblock = blockers.push(blocker);

    if (blockers.length === 1) {
      window.addEventListener('beforeunload', promptBeforeUnload);
    }

    return function() {
      unblock();
      if (!blockers.length) {
        window.removeEventListener('beforeunload', promptBeforeUnload);
      }
    };
  }
};

blockers与listeners类似,区别在于:

  • 添加第一个blocker时会添加beforeunload事件

    只要block了,那么我们刷新、关闭页面,通过修改地址栏输入urlenter都会触发

  • 移除的时候发现blockers空了,那么就移除beforeunload事件

Router

应用顶层使用,为后代的Route提供Context的数据传递。

Router有很多种,区别在于路由在url上面存在的方式:

  • BrowserRouter「完整路由」,路由路径在url上完整对应,需要服务端支持
  • HashRouter「哈希路由」,路径为url里#后面的部分
  • StaticRouter「静态路由」,无状态:不改变路径地址、不记录历史栈

还有MemoryRouter(在内存中保存)、NativeRouter(在ReactNative中使用)等,他们使用的history状态机也不一样。

BrowserRouter

篇幅所限,这里我们主要讨论最通用的BrowserRouter

  • 使用browserHistory

  • 需要服务端支持

    • 原因:如果只给用户提供cdn静态html文件,强制刷新或通过“复杂路径”访问时,无法找到路径下匹配的资源
    • 对于BrowserRouter的应用,服务端渲染完成后,之后的路由由BrowserRouter独立完成解析
    • 将相关的路径都转发到静态文件上,静态文件执行后,会读取当前的浏览器路径并正确渲染对应的组件

作为应用的最外层的容器组件,BrowserRouter源码如下:

export function BrowserRouter({
  basename,
  children,
  window
}) {
  const history = useRef<BrowserHistory>();
  if (historyRef.current == null) {
    historyRef.current = createBrowserHistory({ window });
  }

  const history = historyRef.current;
  const [state, setState] = useState({
    action: history.action,
    location: history.location
  });

  useLayoutEffect(() => {
    history.listen(setState)
  }, [history]);

  return (
    <Router
      basename={basename}
      children={children}
      action={state.action}
      location={state.location}
      navigator={history}
    />
  );
}

可以看到是,是一个构建了history<Router>组件的封装

  • Router初始化会生成history实例,history一般变化的就是actionlocation,并把setState放入对应的listeners,那么路由切换就会setState了。
  • Router其接收的属性的变化的,就是路由相关的变化(actionlocation),这部分路由被存到Context。子组件作为消费者,就可以对页面进行修改,跳转,获取这些数值。

我们来看Router

export function Router({
  action = Action.Pop,
  basename: basenameProp = "/",
  children = null,
  location: locationProp,
  navigator,
  static: staticProp = false
}: RouterProps): React.ReactElement | null {

  // ...

  return (
    <NavigationContext.Provider value={navigationContext}>
      <LocationContext.Provider
        children={children}
        value={{ actionlocation }}
      />
    </NavigationContext.Provider>
  );
}

const { basename, navigator } = React.useContext(NavigationContext);
const { location } = React.useContext(LocationContext);

export function useLocation(): Location {
  return React.useContext(LocationContext).location;
}

Router最后返回了两个Context.Provider,中间就是针对于location的处理

Route「路由端口」

我们直接看RoutesRoute的源码:

export function Routes({
  children,
  location
}: RoutesProps): React.ReactElement | null {
  return useRoutes(createRoutesFromChildren(children), location);
}

export function Route(
  _props: PathRouteProps | LayoutRouteProps | IndexRouteProps
): React.ReactElement | null {

  invariant(
    false,
    `A <Route> is only ever to be used as the child of <Routes> element, ` +
      `never rendered directly. Please wrap your <Route> in a <Routes>.`
  );
}

可以发现

  • Routes实际上就是useRoutes的包装
  • Route实际上没有render,只是作为Routes的子组件存在

我们只需要着重研究createRoutesFromChildrenuseRoutes:

export function createRoutesFromChildren(
  children: React.ReactNode
): RouteObject[] {
  const routes: RouteObject[] = [];
  React.Children.forEach(children, element => {
    if (!React.isValidElement(element)) return;

    if (element.type === React.Fragment) {
      routes.push.apply(
        routes,
        createRoutesFromChildren(element.props.children)
      );
      return;
    }

    const route: RouteObject = {
      caseSensitive: element.props.caseSensitive,
      element: element.props.element,
      index: element.props.index,
      path: element.props.path
    };

    if (element.props.children) {
      route.children = createRoutesFromChildren(element.props.children);
    }

    routes.push(route);
  });
  return routes;
}

我们看到,createRoutesFromChildren作用如下:

  • 递归收集子元素Route上的属性,最终返回一个嵌套数组
  • 支持React.Fragment
  • 创建一个routes路由配置
export function useRoutes(
  routes: RouteObject[],
): React.ReactElement | null {
 // --------------------------------第一段------------------------------------
  
  const { matches: parentMatches } = React.useContext(RouteContext);
  const routeMatch = parentMatches[parentMatches.length - 1];
  const parentParams = routeMatch ? routeMatch.params : {};
  const parentPathnameBase = routeMatch ? routeMatch.pathnameBase : "/";
  
  // --------------------------------第二段------------------------------------
 
  let location = useLocation();
  const pathname = location.pathname || "/";
  const remainingPathname =
    parentPathnameBase === "/"
      ? pathname
      : pathname.slice(parentPathnameBase.length) || "/";

  const matches = matchRoutes(routes, { pathname: remainingPathname });
  
  // --------------------------------第三段-------------------------------------
 
  return _renderMatches(
    matches &&
      matches.map(match =>
        Object.assign({}, match, {
          params: Object.assign({}, parentParams, match.params),
          pathname: joinPaths([parentPathnameBase, match.pathname]),
          pathnameBase: joinPaths([parentPathnameBase, match.pathnameBase])
        })
      ),
    parentMatches
  );
}

useRoutes参数的routes嵌套数组就是createRoutesFromChildren返回的路由配置,通过路由配置匹配到对应的route元素进行渲染:

  • 第一段:获取parentMatches最后一项「routeMatch」

    Routes中,上一次useRoutes匹配后得到的matches会作为下一层的parentMatches,如果match了,获取匹配的paramspathname等各种信息

  • 第二段:通过当前Routes的相对路径remainingPathnameroutes匹配到对应的matches

    这里最复杂的部分,也是react-router最精华的部分,就是匹配路由,而这部分的逻辑在matchRoutes上:

    export function matchRoutes(
      routes: RouteObject[],
      locationArg: Partial<Location> | string,
      basename = "/"
    ): RouteMatch[] | null {
    
      const location =
        typeof locationArg === "string" ? parsePath(locationArg) : locationArg;
      const pathname = stripBasename(location.pathname || "/", basename);
    
      if (pathname == null) {
        return null;
      }
    
      const branches = flattenRoutes(routes);
      rankRouteBranches(branches);
    
      let matches = null;
      for (let i = 0; matches == null && i < branches.length; ++i) {
        matches = matchRouteBranch(branches[i], pathname);
      }
    
      return matches;
    }
    

    matchRoutes的作用是通过当前相对路径和路由配置匹配到对应的matches

    routes有可能是多维路由配置,那么扁平化的过程中,会收集每个路由的属性作为 routeMeta,收集过程是一个深度优先遍历,routesMeta的长度等于路由嵌套自身所处层数

    对扁平后之后的路由进行排序,根据权重排序每个分支,如果权重相等才去比较 routesMeta的每个自权重

    直到matches有值(意味着匹配到,那么自然不用再找了)或遍历完才跳出循环

    matchRouteBranch会通过每个部分的routesMeta,来看看是否能从头到尾匹配到相应的路由,只要有一个不匹配,就返回 null

    routesMeta最后一项是该次路由自己的路由信息,前面项都是parentMetas

  • 第三段:通过_renderMatches渲染上面得到的匹配元素

    终于拿到「路由匹配元素」matches了,那么就要根据匹配项来渲染。

    function _renderMatches(
      matches: RouteMatch[] | null,
      parentMatches: RouteMatch[] = []
    ): React.ReactElement | null {
      if (matches == nullreturn null;
      return matches.reduceRight((_, match, index) => {
        return (
          <RouteContext.Provider
            children={match.route.element}
            value={{
              outlet,
              matches: parentMatches.concat(matches.slice(0index + 1))
            }}
          />
        );
      }, null as React.ReactElement | null);
    }
    
    • _renderMatches会根据匹配项和父级匹配元素parentMatches
    • 从右到左,从子元素向父元素,渲染RouteContext.Provider

Link、Switch等「导航」

  • Link组件功能就是实现一次跳转
  • 直接使用一般的a标签,会使页面刷新,所以需要借助history
  • history.pushState只会改变history状态,不会刷新页面
  • history.pushState的时候,不会触发popstate事件,所以history里面的回调不会自动调用,当用户使用history.push的时候,我们需要手动调用回调函数

我们来看看源码:

export default function Link({
  to,
  ...rest
}) {
  return (
    <RouterContext.Consumer>
      {context => {
        const { history } = context;
        const props = {
          ...rest,
          href: to,
          onClick: event => {
            event.preventDefault();
            history.push(to);
          }
        };

        return <a {...props} />;
      }}
    </RouterContext.Consumer>
  );
}

我们看到,<Link>只是渲染了一个没有默认行为的a标签,其跳转行为由context传入的history.push实现。

未来:Remix

remix是由react-router原班人马打造,并获得三百万美元融资的ts全栈明星开发框架,笔者认为remix作为一个全新的全栈的解决方案值得关注,其路由功能非常灵活高效。

“我们经常将 Remix 描述为"React Router 的编译器",因为有关 Remix 的所有内容都利用了嵌套路由。”

官网对remix的介绍如下:

  • 一个编译器
  • 一个有着HTTP处理器的服务端
  • 一个服务端框架
  • 一个浏览器端框架

remix可以干掉骨架屏等加载状态,所有资源都可预加载,而且管理后台,对于数据的加载、嵌套数据或者组件的路由、并发加载优化做得很好,并且异常的处理已经可以精确到局部级别:

remix告别瀑布式的方式来获取数据,数据获取在服务端并行获取,生成完整 HTML 文档,类似 React 并发特性:

相比之下,Next.js 更像是一个静态网站生成器。Gatsby相比下则门槛过高,需要一定的 GraphQL 基础。

同时,客户端与服务端能有一致的开发体验,客户端代码与服务端代码写在一个文件里,无缝进行数据交互,同时基于 TypeScript,类型定义可以跨客户端与服务端共用,路由也可以同步,实现一个组件化、路由为首的全栈模型。

结尾

我们看到,随着Web技术思维的变革,最早的渐进式应用正在走向越来越强的一体化,大前端、泛前端的思维性质越来越浓厚。

而服务端技术则通过云技术,走向了SaaS,容器化这样更灵活、成本更低的道路上,旨在为应用端提供更便捷的开发。

在未来的Web3浪潮下,由于公链的存在,「胖协议+瘦应用」会是大势所趋,越来越敏捷和低成本的开发会更为重要。

路由作为前后端,交互最紧密的桥梁,会是一个关键的变革区域,或许有天我们可以看到,Web技术通过路由,实现了真正的前后端的统一,走向了人人都可开发的大全栈未来。

- END -

关于奇舞团

奇舞团是 360 集团最大的大前端团队,代表集团参与 W3C 和 ECMA 会员(TC39)工作。奇舞团非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。