React-router 從 0 到 1
引子
本文會討論 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>;
我們來看以上代碼的邏輯
-
一開始在 App 組件裏,只有一個路由
/a
-
用戶跳轉訪問
/a
時,渲染A
組件,瀏覽器上出現字母 A,然後子路由/b
被定義 -
用戶跳轉訪問
/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 個包 (
history
,react-router-dom
,react-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 文檔
這部分值得關注的源碼:
-
工廠函數
createBrowserHistory
等它們代碼差別很小,不同的
router
只有parsePath
的入參不同。還有其它的差別,比如hashHistory
增加了hashchange
事件的監聽等由於篇幅所限,這裏我們只討論
createBrowserHistory
-
history.push
,用於基本的切換路由go
/replace
/forward
/back
也類似,不過push
是history
棧變化的基礎 -
history.listen
添加路由監聽器,每當路由切換可以收到最新的
action
和location
,從而做出不同的判斷,BrowserRouter
中就是通過history.listen(setState)
來監聽路由的變化,從而管理所有的路由 -
history.block
添加阻塞器,會阻塞
push
等行爲和瀏覽器的前進後退,阻止離開當前頁面。且只要判斷有blockers
,那麼同時會阻止瀏覽器刷新、關閉等默認行爲。且只要有blocker
,會阻止上面listener
的監聽
createBrowserHistory
我們先看工廠函數:
工廠函數的用途是創建一個history
對象,後面的listen
和unlisten
都是掛載在這個 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.state, idx: 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 history: BrowserHistory = {
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
獲取初始當前路徑的index
和location
,初始 index 爲空,對應 history.state.idx 爲 0。同時,
handlePop
在window
監聽url
的變化,在handleState
裏面進行觸發。 -
第二部分「發佈訂閱」
我們看到這部分是個標準的發佈訂閱模式:
createEvents
是創建listeners
與blockers
的工廠函數,其返回了一個對象,通過push
添加每個listener
,通過call
通知每個listener
,代碼中叫做handler
listeners
通過call
傳入action
和location
,這樣每個listener
在路由變化時就能接收到,從而做出對應的判斷blockers
,比listeners
多了傳入了一個retry
,從而判斷是否要阻塞路由,不阻塞的話需要調用函數retry
-
第三部分「構建 history」
我們可以看看得到的
history
對象這裏我們重點關注:
push
、listen
、block
-
action
代表上一個修改當前location
的action
,POP
/PUSH
/REPLACE
等 -
action
與location
這兩個屬性都通過修飾符get
獲取,那麼我們每次要獲取就可以通過history.action
或history.location
。避免了只能拿到第一次創建的值,可以每次調用函數才能拿到。 -
createHref
作用是通過location
返回新的href
,to
爲字符串則返回to
,否則返回pathname
+search
+hash
-
back
和forward
都通過go
實現
history.push
replace
和push
非常相似,區別在於replace
將歷史堆棧中當前location
替換爲新的,被替換的將不再存在,所以我們着重關注push
function push(to: To, state?: State) {
const nextAction = Action.Push;
const nextLocation = getNextLocation(to, state);
function getNextLocation(to: To, 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.push
的to
和state
(新的 path 和狀態)獲取到新的location
to
是字符串的話,會通過parsePath
解析對應的pathname
、search
、hash
(三者都是可選的,不一定會出現在返回的對象中) -
getHistoryStateAndUrl
根據新的
location
獲取新的state
和url
因爲是
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
,返回一個取消監聽的可調用方法
-
listener
在push
、replace
和handlePop
三個函數中成功切換路由後調用 -
每當成功切換路由,就會調用
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);
}
};
}
};
blocker
s 與listeners
類似,區別在於:
-
添加第一個
blocker
時會添加beforeunload
事件只要
block
了,那麼我們刷新、關閉頁面,通過修改地址欄輸入url
後enter
都會觸發 -
移除的時候發現
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
一般變化的就是action
和location
,並把setState
放入對應的listeners
,那麼路由切換就會setState
了。 -
Router
其接收的屬性的變化的,就是路由相關的變化(action
、location
),這部分路由被存到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={{ action, location }}
/>
</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「路由端口」
我們直接看Routes
和Route
的源碼:
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
的子組件存在
我們只需要着重研究createRoutesFromChildren
與useRoutes
:
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 了,獲取匹配的params
、pathname
等各種信息 -
第二段:通過當前 Routes 的相對路徑
remainingPathname
和routes
匹配到對應的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
,來看看是否能從頭到尾匹配到相應的路由,只要有一個不匹配,就返回 nullroutesMeta
最後一項是該次路由自己的路由信息,前面項都是parentMetas
-
第三段:通過
_renderMatches
渲染上面得到的匹配元素終於拿到「路由匹配元素」matches 了,那麼就要根據匹配項來渲染。
function _renderMatches( matches: RouteMatch[] | null, parentMatches: RouteMatch[] = [] ): React.ReactElement | null { if (matches == null) return null; return matches.reduceRight((_, match, index) => { return ( <RouteContext.Provider children={match.route.element} value={{ outlet, matches: parentMatches.concat(matches.slice(0, index + 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 技術通過路由,實現了真正的前後端的統一,走向了人人都可開發的大全棧未來。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Qd5aBRc72AV69nlUXONWng