精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

Next.js 強(qiáng)勁對(duì)手來了! Remix 正式宣布開源

開源
近期,由 React Router 原班團(tuán)隊(duì)打造,基于 TypeScript 與 React,內(nèi)建 React Router V6 特性的全棧 Web 框架 Remix 正式開源。目前占據(jù) Github 趨勢(shì)總榜前 3.

[[437227]]

大家好,我是皮湯。周五翻 Github 趨勢(shì)榜看到了 Remix 這個(gè)內(nèi)容,覺得挺有發(fā)展前景的,初步了解了一下具體的特性,分享給大家。

近期,由 React Router 原班團(tuán)隊(duì)打造,基于 TypeScript 與 React,內(nèi)建 React Router V6 特性的全棧 Web 框架 Remix 正式開源。目前占據(jù) Github 趨勢(shì)總榜前 3,Github 標(biāo)星 5K+ Star:

Remix 開源之后可以說是在 React 全棧框架領(lǐng)域激起千層浪,絕對(duì)可以算是 Next.js 的強(qiáng)勁對(duì)手。Remix 的特性如下:

  • 追求速度,然后是用戶體驗(yàn)(UX),支持任何 SSR/SSG 等
  • 基于 Web 基礎(chǔ)技術(shù),如 HTML/CSS 與 HTTP 以及 Web Fecth API,在絕大部分情況可以不依賴于 JavaScript 運(yùn)行,所以可以運(yùn)行在任何環(huán)境下,如 Web Browser、Cloudflare Workers、Serverless 或者 Node.js 等
  • 客戶端與服務(wù)端一致的開發(fā)體驗(yàn),客戶端代碼與服務(wù)端代碼寫在一個(gè)文件里,無縫進(jìn)行數(shù)據(jù)交互,同時(shí)基于 TypeScript,類型定義可以跨客戶端與服務(wù)端共用
  • 內(nèi)建文件即路由、動(dòng)態(tài)路由、嵌套路由、資源路由等
  • 干掉 Loading、骨架屏等任何加載狀態(tài),頁面中所有資源都可以預(yù)加載(Prefetch),頁面幾乎可以立即加載
  • 告別以往瀑布式(Waterfall)的數(shù)據(jù)獲取方式,數(shù)據(jù)獲取在服務(wù)端并行(Parallel)獲取,生成完整 HTML 文檔,類似 React 的并發(fā)特性
  • 提供開發(fā)網(wǎng)頁需要所有狀態(tài),開箱即用;提供所有需要使用的組件,包括 <Links> 、<Link>、 <Meta> 、<Form> 、<Script/> ,用于處理元信息、腳本、CSS、路由和表單相關(guān)的內(nèi)容
  • 內(nèi)建錯(cuò)誤處理,針對(duì)非預(yù)期錯(cuò)誤處理的 <ErrorBoundary> 和開發(fā)者拋出錯(cuò)誤處理的 <CatchBoundary>

特性這么多?不明覺厲!接下來我們就嘗試一一來展示這些 Remix 的特性🚀。

一致的開發(fā)體驗(yàn)

Remix 提供基于文件的路由,將讀取數(shù)據(jù)、操作數(shù)據(jù)和渲染數(shù)據(jù)的邏輯都寫在同一個(gè)路由文件里,方便一致性處理,這樣可以跨客戶端和服務(wù)端邏輯共享同一套類型定義。

看一段官網(wǎng)的代碼:

  1. import type { Post } from "~/post"
  2. import { Outlet, Link, useLoaderData, useTransition } from "remix"
  3.  
  4. let postsPath = path.join(__dirname, "..""posts"); 
  5.  
  6. async function getPosts() { 
  7.   let dir = await fs.readdir(postsPath); 
  8.   return Promise.all
  9.     dir.map(async (filename) => { 
  10.       let file = await fs.readFile(path.join(postsPath, filename)); 
  11.       let { attributes } = parseFrontMatter(file.toString()); 
  12.       invariant( 
  13.         isValidPostAttributes(attributes), 
  14.         `${filename} has bad meta data!` 
  15.       ); 
  16.       return { 
  17.         slug: filename.replace(/.md$/, ""), 
  18.         title: attributes.title, 
  19.       }; 
  20.     }) 
  21.   ); 
  22.  
  23. async function createPost(post: Post) { 
  24.   let md = `---\ntitle: ${post.title}\n---\n\n${post.markdown}`; 
  25.   await fs.writeFile(path.join(postsPath, post.slug + ".md"), md); 
  26.   return getPost(post.slug); 
  27.  
  28. export async function loader({ request }) { 
  29.   return getProjects(); 
  30.  
  31. export async function action({ request }) { 
  32.   let form = await request.formData(); 
  33.   const post = createPost({ title: form.get("title") }); 
  34.   return redirect(`/posts/${post.id}`); 
  35.  
  36. export default function Projects() { 
  37.   let posts = useLoaderData<Post[]>(); 
  38.   let { state } = useTransition(); 
  39.   let busy = state === "submitting"
  40.  
  41.   return ( 
  42.     <div> 
  43.       {posts.map((post) => ( 
  44.         <Link to={post.slug}>{post.title}</Link> 
  45.       ))} 
  46.  
  47.       <Form method="post"
  48.         <input name="title" /> 
  49.         <button type="submit" disabled={busy}> 
  50.           {busy ? "Creating..." : "Create New Post"
  51.         </button> 
  52.       </Form> 
  53.        
  54.       <Outlet /> 
  55.     </div> 
  56.   ); 

上述是一個(gè)路由文件,如果它是 src/routes/posts/index.tsx 文件,那么我們開啟服務(wù)器,通過 localhost:3000/posts 就可以訪問到這個(gè)文件,這就是文件即路由,而默認(rèn)導(dǎo)出的 Projects 函數(shù),即為一個(gè) React 函數(shù)式組件,此函數(shù)的返回模板則為訪問這個(gè)路由的 HTML 文檔。

  • 每個(gè)路由函數(shù),如 Projects 可以定義一個(gè) loader 函數(shù),類似處理 GET 請(qǐng)求的服務(wù)端函數(shù),可以獲取到路由信息,為初次服務(wù)端渲提供數(shù)據(jù),在這個(gè)函數(shù)中可以獲取文件系統(tǒng)、請(qǐng)求數(shù)據(jù)庫(kù)、進(jìn)行其他網(wǎng)絡(luò)請(qǐng)求,然后返回?cái)?shù)據(jù),在我們的 Projects 組件里,可以通過 Remix 提供的 useLoaderData 鉤子拿到 loader 函數(shù)獲取到的數(shù)據(jù)。
  • 每個(gè)路由函數(shù)也可以定義一個(gè) action 函數(shù),用于進(jìn)行實(shí)際的操作,類似處理非 GET 請(qǐng)求,如 POST/PUT/PATCH/DELETE 的操作的函數(shù),它可以操作修改數(shù)據(jù)庫(kù)、寫入文件系統(tǒng)等,同時(shí)其返回的結(jié)果可能是實(shí)際的數(shù)據(jù)或是重定向到某個(gè)新頁面,如 redirect("/admin")。當(dāng) action 函數(shù)返回?cái)?shù)據(jù)或錯(cuò)誤信息時(shí),我們可以通過 Remix 提供的 useActionData 鉤子拿到這個(gè)返回的錯(cuò)誤信息,進(jìn)行前端的展示等。

值得注意的是,action 函數(shù)是在 <Form method="post"> 表單里,用戶點(diǎn)擊提交按鈕之后自動(dòng)調(diào)用,Remix 通過 Fetch API 的形式去調(diào)用,然后在前端不斷的輪詢獲取調(diào)用結(jié)果,且自動(dòng)處理用戶多次點(diǎn)擊時(shí)的競(jìng)爭(zhēng)情況。

你的瀏覽器網(wǎng)絡(luò)面板將呈現(xiàn)如下情況,自動(dòng) Remix 發(fā)起 POST 請(qǐng)求,然后處理重定向到 /post/${post.id} ,同時(shí)加載對(duì)應(yīng)的 /posts 和 /posts/${post.id} 對(duì)應(yīng)的路由頁面內(nèi)容。

通過 Remix 提供的 useTransition 鉤子,我們可以拿到表單提交的狀態(tài),當(dāng)請(qǐng)求還未返回結(jié)果時(shí),我們可以通過這個(gè)狀態(tài) state 判斷是否要展示一個(gè)加載狀態(tài),提示用戶當(dāng)前的請(qǐng)求進(jìn)展。

同時(shí) Post 類型在 useLoaderData

有同學(xué)可能注意到了,上面我們整個(gè)頁面渲染、到發(fā)起創(chuàng)建 Post 請(qǐng)求、到后臺(tái)創(chuàng)建 Post,到重定向到 Post 詳情,這整個(gè)過程,我們無需在前端使用任何 JavaScript 相關(guān)的內(nèi)容,僅僅通過 HTML 與 HTTP 就完成了這個(gè)交互,所以 Remix 的網(wǎng)站在 Disbaled JavaScript 運(yùn)行環(huán)境下也可以正常工作。

通過上圖我們可以看到,即使 JavaScript 已經(jīng)關(guān)閉了,我們的網(wǎng)站依然可以正常運(yùn)行。

 強(qiáng)大的嵌套路由體系

基于文件即路由的理念,我們無需集中的維護(hù)一套路由定義,當(dāng)我們創(chuàng)建了對(duì)應(yīng)的文件之后,Remix 就為我們注冊(cè)了對(duì)應(yīng)的路由。

而 Remix 最具特色的功能之一就是嵌套路由。在 Remix 中,一個(gè)頁面通常包含多層級(jí)頁面,每個(gè)子頁面控制自身的 UI 展現(xiàn),而且獨(dú)立控制自身的數(shù)據(jù)加載和代碼分割。

拿官網(wǎng)的例子來看如下:

上述頁面的對(duì)應(yīng)關(guān)系如下:

  • 整個(gè)頁面模塊為 / 、而對(duì)應(yīng)到 /sales 則是右邊的整塊天藍(lán)色內(nèi)容、/sales/invoices 對(duì)應(yīng)到黃色的部分、/sales/invoices/102000 則對(duì)應(yīng)到右下角的紅色部分

整個(gè)路由分層,對(duì)應(yīng)到整個(gè)頁面的分層視圖,而每個(gè)分層下的代碼都是獨(dú)立編寫,視圖渲染獨(dú)立渲染,數(shù)據(jù)獨(dú)立獲取,錯(cuò)誤獨(dú)立展示。

來看一個(gè)實(shí)際例子:

  1. // src/root.tsx 
  2. import { 
  3.   Outlet, 
  4.    
  5. export default function App() { 
  6.   return ( 
  7.     <Document> 
  8.       <Layout> 
  9.         <Outlet /> 
  10.       </Layout> 
  11.     </Document> 
  12.   ); 
  13.  
  14. function Document() {} 
  15. function Layout() {} 
  1. // src/routes/admin.tsx 
  2. import { Outlet, Link, useLoaderData } from "remix"
  3. import { getPosts } from "~/post"
  4. import type { Post } from "~/post"
  5. import adminStyles from "~/styles/admin.css"
  6.  
  7. export let links = () => { 
  8.   return [{ rel: "stylesheet", href: adminStyles }]; 
  9. }; 
  10.  
  11. export let loader = () => { 
  12.   return getPosts(); 
  13. }; 
  14.  
  15. export default function Admin() { 
  16.   let posts = useLoaderData<Post[]>(); 
  17.   return ( 
  18.     <div className="admin"
  19.       <nav> 
  20.         <h1>Admin</h1> 
  21.         <ul> 
  22.           {posts.map((post) => ( 
  23.             <li key={post.slug}> 
  24.               <Link to={post.slug}>{post.title}</Link> 
  25.             </li> 
  26.           ))} 
  27.         </ul> 
  28.       </nav> 
  29.       <main> 
  30.         <Outlet /> 
  31.       </main> 
  32.     </div> 
  33.   ); 
  1. // src/routes/admin/index.tsx 
  2. import { Link } from "remix"
  3.  
  4. export default function AdminIndex() { 
  5.   return ( 
  6.     <p> 
  7.       <Link to="new">Create a New Post</Link> 
  8.     </p> 
  9.   ); 
  1. // src/routes/admin/new.tsx 
  2. import { useTransition, useActionData, redirect, Form } from "remix"
  3. import type { ActionFunction } from "remix"
  4. import { createPost } from "~/post"
  5. import invariant from "tiny-invariant"
  6.  
  7. export let action: ActionFunction = async ({ request }) => { 
  8.   await new Promise((res) => setTimeout(res, 1000)); 
  9.   let formData = await request.formData(); 
  10.  
  11.   let title = formData.get("title"); 
  12.   let slug = formData.get("slug"); 
  13.   let markdown = formData.get("markdown"); 
  14.  
  15.   let errors = {}; 
  16.   if (!title) errors.title = true
  17.   if (!slug) errors.slug = true
  18.   if (!markdown) errors.markdown = true
  19.  
  20.   if (Object.keys(errors).length) { 
  21.     return errors; 
  22.   } 
  23.  
  24.   await createPost({ title, slug, markdown }); 
  25.  
  26.   return redirect("/admin"); 
  27. }; 
  28.  
  29. export default function NewPost() { 
  30.   let errors = useActionData(); 
  31.   let transition = useTransition(); 
  32.  
  33.   return ( 
  34.     <Form method="post"
  35.       <p> 
  36.         <label> 
  37.           Post Title: {errors?.title && <em>Title is required</em>} 
  38.           <input type="text" name="title" /> 
  39.         </label> 
  40.       </p> 
  41.       <p> 
  42.         <label> 
  43.           Post Slug: {errors?.slug && <em>Slug is required</em>}{" "
  44.           <input type="text" name="slug" /> 
  45.         </label> 
  46.       </p> 
  47.       <p> 
  48.         <label htmlFor="markdown">Markdown:</label>{" "
  49.         {errors?.markdown && <em>Markdown is required</em>} 
  50.         <br /> 
  51.         <textarea rows={20} name="markdown" /> 
  52.       </p> 
  53.       <p> 
  54.         <button type="submit"
  55.           {transition.submission ? "Create..." : "Create Post"
  56.         </button> 
  57.       </p> 
  58.     </Form> 
  59.   ); 

上述代碼渲染的頁面如下:

整個(gè) App 網(wǎng)站是由 <Document> 嵌套 <Layout> 組成,其中 <Outlet> 是路由的填充處,即上圖中綠色的部分。當(dāng)我們?cè)L問 localhost:3000/ 時(shí),其中填充的內(nèi)容為 src/routes/index.tsx 路由文件對(duì)應(yīng)的渲染內(nèi)容,而當(dāng)我們?cè)L問 localhost:3000/admin 時(shí),對(duì)應(yīng)的是 src/routes/admin.tsx 路由文件對(duì)應(yīng)的渲染內(nèi)容。

而我們?cè)?的 src/routes/admin.tsx 繼續(xù)提供了 <Outlet> 路由顯然組件,意味著當(dāng)我們繼續(xù)添加分級(jí)(嵌套)路由時(shí),如訪問 http://localhost:3000/admin/new 那么這個(gè) <Outlet> 會(huì)渲染 src/routes/admin/new.tsx 對(duì)應(yīng)路由文件的渲染內(nèi)容,而訪問 http://localhost:3000/admin 時(shí),<Outlet> 部分會(huì)渲染 src/routes/admin/index.tsx 對(duì)應(yīng)路由文件的渲染內(nèi)容,見下圖:

而這種嵌套路由是自動(dòng)發(fā)生的,當(dāng)你創(chuàng)建了一個(gè) src/routes/admin.tsx 之后,又創(chuàng)建了一個(gè)同名的文件夾,并在文件夾下建立了其它文件,那么這些文件的文件名會(huì)被注冊(cè)為下一級(jí)的嵌套路由名:

  • localhost:3000/admin 同時(shí)注冊(cè) src/routes/admin.tsx 和 src/routes/admin/index.tsx
  • localhost:3000/admin/new 注冊(cè) src/routes/admin/new.tsx

通過這種文件即路由,同名文件夾下文件即嵌套路由的方式,然后通過在父頁面里面通過 的方式渲染根據(jù)子路由渲染子頁面內(nèi)容,極大的增加了靈活性,且每個(gè)子路由對(duì)應(yīng)獨(dú)立的路由文件,具有獨(dú)立的數(shù)據(jù)處理邏輯、內(nèi)容渲染邏輯、錯(cuò)誤處理邏輯。

上述嵌套路由一個(gè)顯而易見的優(yōu)點(diǎn)就是,某個(gè)部分如果報(bào)錯(cuò)了,結(jié)合后續(xù)會(huì)提到的 ErrorBoundary 和 CatchBoundary 這個(gè)部分可以顯示錯(cuò)誤的頁面,而用戶仍然可以操作其他部分,而不需要刷新整個(gè)頁面以重新加載使用,極大提高網(wǎng)站容錯(cuò)性。

 再見,加載狀態(tài)

通過嵌套路由,Remix 可以干掉幾乎所有的加載狀態(tài)、骨架屏,現(xiàn)在很多應(yīng)用都是在前端組件里進(jìn)行數(shù)據(jù)獲取,獲取前置數(shù)據(jù)之后,然后用前置數(shù)據(jù)去獲取后置的數(shù)據(jù),形成了一個(gè)瀑布式的獲取形式,當(dāng)數(shù)據(jù)量大的時(shí)候,頁面加載就需要很長(zhǎng)時(shí)間,所以絕大部分網(wǎng)站都會(huì)放一個(gè)加載的狀態(tài),如小菊花轉(zhuǎn)圈圈,或者體驗(yàn)更好一點(diǎn)的骨架屏,如下:

這是因?yàn)檫@些應(yīng)用缺乏類似 Remix 這樣的嵌套路由的概念,訪問某個(gè)路由時(shí),就是訪問這個(gè)路由對(duì)應(yīng)的頁面,只有這個(gè)頁面加載出來之后,里面的子組件渲染時(shí),再進(jìn)行數(shù)據(jù)的獲取,再加載子組件,如此往復(fù),就呈現(xiàn)瀑布流式的加載,帶來了很多中間的加載狀態(tài)。

而 Remix 提供了嵌套路由,當(dāng)訪問路由 localhost:3000/admin/new 時(shí),會(huì)加載三級(jí)路由,同時(shí)這三個(gè)路由對(duì)應(yīng)的頁面獨(dú)立、并行加載,獨(dú)立、并行獲取數(shù)據(jù),最后發(fā)送給客戶端的是一個(gè)完整的 HTML 文檔,如下過程:

可見雖然我們首屏拿到內(nèi)容可能會(huì)慢一點(diǎn),但是再也不需要加載狀態(tài),再見,菊花圖 👋🏻,再見,骨架屏👋🏻。

[[437231]]

同時(shí)借助嵌套路由,當(dāng)我們鼠標(biāo) Hover 到某個(gè)鏈接準(zhǔn)備點(diǎn)擊切換某個(gè)子路由時(shí),Remix 提供了預(yù)獲取(Prefetch)功能,可以提前并行獲取子路由文檔和各種資源,包括 CSS、圖片、相關(guān)數(shù)據(jù)等,這樣當(dāng)我們實(shí)際點(diǎn)擊這個(gè)鏈接切換子路由時(shí),頁面可以立即呈現(xiàn)出來:

 完善的錯(cuò)誤處理

我們的網(wǎng)站經(jīng)常會(huì)遇到問題,使用其他框架編寫時(shí),網(wǎng)站遇到問題可能用戶就需要重新刷新網(wǎng)站,而對(duì)于 Remix 來說,基于嵌套路由的理念,則無需重新刷新,只需要在對(duì)應(yīng)的錯(cuò)誤的子路由展示錯(cuò)誤信息,而頁面的其他部分仍然可以正常工作:

比如我們上圖的右下角子路由出現(xiàn)了問題,那么這塊會(huì)展示出問題時(shí)的錯(cuò)誤頁面,而其他頁面部分仍然展示正常的信息。

正因?yàn)殄e(cuò)誤經(jīng)常發(fā)生,且處理錯(cuò)誤異常困難,包含客戶端、服務(wù)端的各種錯(cuò)誤,包含預(yù)期的、非預(yù)期的錯(cuò)誤等,所以 Remix 內(nèi)建了完善的錯(cuò)誤處理機(jī)制,提供了類似 React 的 ErrorBoundary 的理念。

在 Remix 中,每個(gè)路由函數(shù)對(duì)應(yīng)一個(gè) ErrorBoundary 函數(shù):

  1. export default function RouteFunction() {} 
  2.  
  3. export function ErrorBoundary({ error }) { 
  4.   console.error(error); 
  5.   return ( 
  6.     <div> 
  7.       <h2>Oh snap!</h2> 
  8.       <p> 
  9.         There was a problem loading this invoice 
  10.       </p> 
  11.     </div> 
  12.   ); 

ErrorBoundary 函數(shù)代表處理那些來自 loader 和 action,客戶端或服務(wù)端的非預(yù)期的錯(cuò)誤,當(dāng)出現(xiàn)這些非預(yù)期的錯(cuò)誤時(shí),就會(huì)激活這個(gè)函數(shù),顯示對(duì)應(yīng)函數(shù)的表示錯(cuò)誤信息的 UI。

同時(shí)每個(gè)路由函數(shù)對(duì)應(yīng)著一個(gè) CatchBoundary 函數(shù):

  1. import { useCatch } from "remix"
  2.  
  3. export function CatchBoundary() { 
  4.   let caught = useCatch(); 
  5.  
  6.   return ( 
  7.     <div> 
  8.       <h1>Caught</h1> 
  9.       <p>Status: {caught.status}</p> 
  10.       <pre> 
  11.         <code>{JSON.stringify(caught.data, null, 2)}</code> 
  12.       </pre> 
  13.     </div> 
  14.   ); 

CatchBoundary 函數(shù)對(duì)應(yīng)著預(yù)期的錯(cuò)誤,即你在 loader、action 函數(shù)中,在客戶端或服務(wù)端,手動(dòng)拋出的 Response 錯(cuò)誤,這些錯(cuò)誤的路徑是可預(yù)期的,在 CatchBoundary 中,通過 useCatch 鉤子獲取這些拋出的 Response 錯(cuò)誤,然后展示對(duì)于的錯(cuò)誤信息的 UI。

當(dāng)我們沒有在子路由中添加 ErrorBoundary 或 CatchBoundary 函數(shù)時(shí),一旦遇到錯(cuò)誤,這些錯(cuò)誤就會(huì)向更上一級(jí)的路由冒泡,直至最頂層的路由頁面,所以你只最好在最頂層的路由文件里聲明一個(gè) ErrorBoundary 和 CatchBoundary 函數(shù),用于捕獲所有可能的錯(cuò)誤,然后在代碼審查( Code Review)時(shí)及時(shí)排查出來。

 基于 Web 基礎(chǔ)技術(shù)

Remix 專注于用 Web 基礎(chǔ)技術(shù),HTML/CSS + HTTP 等解決問題,同時(shí)提供了在 Web 全棧開發(fā)框架中所需要的所有狀態(tài)和所有基礎(chǔ)組件。

其中相關(guān)狀態(tài)包含:

  1. // 加載數(shù)據(jù)的狀態(tài) 
  2. useLoaderData() 
  3.  
  4. // 更新數(shù)據(jù)的狀態(tài) 
  5. useActionData() 
  6.  
  7. // 提交表單等相關(guān)狀態(tài) 
  8. useFormAction() 
  9. useSubmit() 
  10.  
  11. // 統(tǒng)一的加載狀態(tài) 
  12. useTransition() 
  13.  
  14. // 錯(cuò)誤抓取狀態(tài)等 
  15. useCatch() 

以及 Web 網(wǎng)站組成的基礎(chǔ)組件:

  • <Meta> 用于動(dòng)態(tài)的設(shè)置網(wǎng)頁的元信息,方便 SEO
  • <Script> 用于告知 Remix 是否需要在加載網(wǎng)頁時(shí)導(dǎo)入相關(guān) JS,因?yàn)榇蟛糠智闆r下 Remix 編寫的頁面無需 JS 也能正常工作
  • <Form> 用于替代原生的 <form> 方便在客戶端和服務(wù)端進(jìn)行表單操作,接管提交時(shí)的相應(yīng)功能,使用 Fetch API 發(fā)起請(qǐng)求等,以及處理多次重復(fù)提交的競(jìng)爭(zhēng)狀態(tài)等

同時(shí)在路由函數(shù)所在文件里,可以通過聲明 link 、meta 、links 、headers 等函數(shù)來聲明對(duì)應(yīng)的功能:

  • links 變量函數(shù):表示此頁面需要加載的資源,如 CSS、圖片等
  1. import type { LinksFunction } from "remix"
  2. import stylesHref from "../styles/something.css"
  3.  
  4. export let links: LinksFunction = () => { 
  5.   return [ 
  6.     // add a favicon 
  7.     { 
  8.       rel: "icon"
  9.       href: "/favicon.png"
  10.       type: "image/png" 
  11.     }, 
  12.  
  13.     // add an external stylesheet 
  14.     { 
  15.       rel: "stylesheet"
  16.       href: "https://example.com/some/styles.css"
  17.       crossOrigin: "true" 
  18.     }, 
  19.  
  20.     // add a local stylesheet, remix will fingerprint the file name for 
  21.     // production caching 
  22.     { rel: "stylesheet", href: stylesHref }, 
  23.  
  24.     // prefetch an image into the browser cache that the user is likely to see 
  25.     // as they interact with this page, perhaps they click a button to reveal in 
  26.     // a summary/details element 
  27.     { 
  28.       rel: "prefetch"
  29.       as"image"
  30.       href: "/img/bunny.jpg" 
  31.     }, 
  32.  
  33.     // only prefetch it if they're on a bigger screen 
  34.     { 
  35.       rel: "prefetch"
  36.       as"image"
  37.       href: "/img/bunny.jpg"
  38.       media: "(min-width: 1000px)" 
  39.     } 
  40.   ]; 
  41. }; 
  • links 函數(shù):聲明需要 Prefetch 的頁面,當(dāng)用戶點(diǎn)擊之前就加載好資源
  1. export function links() { 
  2.   return [{ page: "/posts/public" }]; 
  • meta 函數(shù):與 組件類似,聲明頁面需要的元信息
  1. import type { MetaFunction } from "remix"
  2.  
  3. export let meta: MetaFunction = () => { 
  4.   return { 
  5.     title: "Josie's Shake Shack", // <title>Josie's Shake Shack</title> 
  6.     description: "Delicious shakes", // <meta name="description" content="Delicious shakes"
  7.     "og:image""https://josiesshakeshack.com/logo.jpg" // <meta property="og:image" content="https://josiesshakeshack.com/logo.jpg"
  8.   }; 
  9. }; 
  • headers 函數(shù):定義此頁面發(fā)送 HTTP 請(qǐng)求時(shí),帶上的請(qǐng)求頭信息
  1. export function headers({ loaderHeaders, parentHeaders }) { 
  2.   return { 
  3.     "X-Stretchy-Pants""its for fun"
  4.     "Cache-Control""max-age=300, s-maxage=3600" 
  5.   }; 

由此可見,Remix 提供了整個(gè)全棧 Web 開發(fā)生命周期所需要的幾乎的一切內(nèi)容,且內(nèi)置最佳實(shí)踐,確保你付出很少的努力就能開發(fā)出性能卓越、體驗(yàn)優(yōu)秀的網(wǎng)站!

當(dāng)然這篇文章并不能包含所有 Remix 的特性,看到這里仍然對(duì) Remix 感興趣的同學(xué)可以訪問官網(wǎng)(https://remix.run/)詳細(xì)了解哦~ 官網(wǎng)提供了非常詳細(xì)的實(shí)戰(zhàn)教程幫助你使用 Remix 開發(fā)實(shí)際的應(yīng)用。

了解了 Remix 的特性之后,你對(duì) Remix 有什么看法呢?你覺得它能超過 Next.js 🐴?

 

責(zé)任編輯:姜華 來源: 程序員巴士
相關(guān)推薦

2021-11-26 10:29:24

jsRemix開源

2024-03-04 07:33:39

RemixReact框架

2024-09-18 00:00:01

ChatGPTOpenAI工具型

2024-03-05 19:17:37

2022-02-22 20:48:48

RemixNext.js框架

2023-09-17 12:21:21

RemixNext.js

2023-09-20 10:14:03

Next.js前端

2023-10-27 15:13:12

Next.jsRust

2023-11-07 11:47:59

2025-07-24 08:32:39

2025-02-03 00:00:35

2022-08-22 08:05:17

Fresh框架Remix

2023-10-28 09:41:12

Next.js函數(shù)配置選項(xiàng)

2024-09-04 10:27:53

2024-04-28 10:56:34

Next.jsWeb應(yīng)用搜索引擎優(yōu)化

2023-05-26 08:13:49

RSCNext.js開源

2024-12-13 08:37:32

2025-03-31 00:00:02

Next.jsReact漏洞

2024-12-20 07:30:00

重定向服務(wù)器端指令Next.js
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)

亚洲色婷婷久久精品av蜜桃| 国产精品男人的天堂| 韩国黄色一级片| 欧美裸体视频| 国产精品视频一二三区| 97超碰最新| 在线观看日本网站| 国产精品久久久久久| 日韩视频一区二区三区 | 免费超碰在线| 成人午夜视频免费看| 国产不卡在线观看| 国产va在线播放| 偷拍视屏一区| 欧美一区二区三区免费在线看| 国产男女免费视频| 免费观看在线午夜影视| 91麻豆精品一区二区三区| 国产日产欧美精品| 久久夜色精品国产噜噜亚洲av| 五月精品视频| 一本色道久久综合狠狠躁篇怎么玩| 香蕉视频色在线观看| 日韩成人影音| 国产ts丝袜人妖系列视频| 少妇一级淫免费观看| 新片速递亚洲合集欧美合集| 一区二区三区四区在线免费观看 | 午夜影院免费体验区| 蜜臀av一级做a爰片久久| 国模私拍视频一区| 成人在线观看小视频| 国产精品一区二区av日韩在线| 欧美不卡123| 潘金莲激情呻吟欲求不满视频| 亚洲校园激情春色| 五月天一区二区| 精品无码av无码免费专区| 大胆av不用播放器在线播放| 99国产精品久久久| 国产精品加勒比| 国内毛片毛片毛片毛片| 久久成人免费电影| 国产精品a久久久久久| 亚洲视频免费播放| 亚洲精品激情| 国内精品久久久久久久久| 九九热最新地址| 97久久夜色精品国产| 在线视频精品一| 国产综合精品在线| 亚洲高清极品| 亚洲美女喷白浆| 瑟瑟视频在线观看| 欧美猛男同性videos| 亚洲欧美激情一区| 成人免费毛片糖心| 欧州一区二区| 在线国产精品视频| 国产精品视频在| 欧美电影免费观看高清| 最近2019年好看中文字幕视频| av电影网站在线观看| 精品理论电影在线| 上原亚衣av一区二区三区| 午夜影院黄色片| 日产精品一区二区| 久久激情五月丁香伊人| 日韩一卡二卡在线观看| 天天综合网91| 九九热最新视频//这里只有精品 | 国产精品久久久久久在线观看| 一区二区三区在线资源| 亚洲第一福利网| 国产手机在线观看| 日韩av免费大片| 成人97在线观看视频| 欧美人妻一区二区| 一本色道久久| 国产福利精品av综合导导航| 中文字幕二区三区| 国产精品资源网站| 国产一区免费| se在线电影| 亚洲男人的天堂一区二区 | 91www在线| 一本大道久久a久久精品综合| 欧美精品成人网| 精品午夜视频| 亚洲国产精品小视频| 日韩精品无码一区二区三区久久久| 日韩国产一区二区三区| 欧美理论电影在线观看| 国产原创视频在线| 久久精品国产成人一区二区三区| 97中文在线| 国产在线视频网站| 怡红院av一区二区三区| 久久精品免费一区二区| 欧美视频在线视频精品| 欧美精品一区二区精品网| 国产jk精品白丝av在线观看| 亚洲综合婷婷| 国产99久久精品一区二区| 99热这里只有精品在线观看| 久久一区二区三区四区| 妞干网这里只有精品| 涩涩在线视频| 欧美一区二区视频在线观看2020 | 女人扒开双腿让男人捅| 中文字幕亚洲影视| 欧美黄色小视频| 亚洲专区在线播放| 26uuu另类欧美| 国产精品日韩三级| 激情小说亚洲| 亚洲欧美日韩久久久久久| 九九热视频精品| 日本不卡一区二区三区| 六十路精品视频| 欧美色图天堂| 欧美老人xxxx18| av中文字幕免费观看| 亚洲国产国产亚洲一二三| 成人av色在线观看| 国产一级网站视频在线| 婷婷国产v国产偷v亚洲高清| 99精品视频免费版的特色功能| 国产免费久久| 91精品国产91久久久久福利| a级片在线播放| 国产精品美女视频| 免费观看成人网| 一区二区导航| 欧美自拍视频在线| 香蕉视频免费在线看| 亚洲一区在线观看免费观看电影高清| 日本中文字幕影院| 欧洲三级视频| 国产精品激情av电影在线观看| 深夜影院在线观看| 精品成人av一区| 黄色污在线观看| 一本久道综合久久精品| 国语精品中文字幕| 2018av在线| 亚洲国产日韩欧美在线图片| 久久精品视频6| 成人午夜碰碰视频| 日本a在线免费观看| 香蕉大人久久国产成人av| 欧美成人精品在线| jlzzjlzz亚洲女人18| 亚洲免费av在线| 性生交大片免费看l| 欧美片第1页综合| 99九九视频| a级片免费在线观看| 精品国产伦理网| 国产无遮挡aaa片爽爽| 成人高清伦理免费影院在线观看| 日韩日韩日韩日韩日韩| 日韩精品免费一区二区三区竹菊| 国内精品一区二区三区| 亚洲欧美日本在线观看| 在线免费不卡电影| 999福利视频| 国产一区二区剧情av在线| 久久久国内精品| 老司机aⅴ在线精品导航| 欧美性一区二区三区| 国产黄在线观看| 欧美日韩激情一区二区三区| 成人免费视频网站入口::| 东方欧美亚洲色图在线| wwwxxx黄色片| 四季av一区二区三区免费观看| 亚洲综合中文字幕在线| 国产精品25p| 国产亚洲精品久久久久久| 伊人亚洲综合网| 亚洲综合免费观看高清完整版| www男人天堂| 噜噜噜91成人网| 秋霞在线一区二区| 国内精品麻豆美女在线播放视频 | 成人h动漫精品一区二区下载| 欧美激情在线看| 日韩精品国产一区| 日日骚欧美日韩| 久久综合亚洲精品| 自拍亚洲一区| 91中文字精品一区二区| 亚洲人免费短视频| 欧美成人小视频| 九色视频网站在线观看| 日韩一区二区免费高清| 精品国产一区二区三区四| 亚洲欧美日韩系列| 亚洲熟妇无码av| 国产一区视频在线看| 国产精品视频一区二区三区四区五区| 国产精品久久占久久| 国产一区二区免费在线观看| 国产原创一区| 欧美一性一乱一交一视频| 成人免费观看视频大全| 精品夜色国产国偷在线| 国产成人精品一区二三区四区五区| 一本大道久久a久久精品综合| 久久精品99久久久久久| 国产精品视频看| 国产精品久久久免费观看| 久久 天天综合| 99视频在线免费| 99在线热播精品免费99热| 热这里只有精品| 日韩精品欧美| 欧美亚州在线观看| 另类在线视频| 不卡一卡2卡3卡4卡精品在| 久久亚洲资源中文字| 日本精品视频在线观看| h片在线观看视频免费免费| 欧美成人sm免费视频| 久久bbxx| www.久久色.com| 最新国产在线观看| 亚洲一区二区精品| 清纯唯美亚洲色图| 亚洲国产精品一区二区久| 亚洲成人中文字幕在线| 欧美日韩激情一区| 综合久久中文字幕| 在线亚洲高清视频| 亚洲国产av一区二区三区| 高跟丝袜一区二区三区| 久久久久亚洲AV| 一区二区欧美精品| 欧美国产精品一二三| 亚洲美女区一区| 538任你躁在线精品视频网站| 中文字幕一区二区三区视频| 美国一级黄色录像| 国产精品国产三级国产aⅴ中文| 国内精品卡一卡二卡三| 国产亚洲一本大道中文在线| 国产成人精品无码免费看夜聊软件| www国产亚洲精品久久麻豆| 国产精品无码一区二区三区免费 | 丝袜诱惑一区二区| 97精品在线视频| 亚洲欧美韩国| 国产精品99蜜臀久久不卡二区| 国产一区二区三区影视| 国产精品网红直播| www一区二区三区| 1区1区3区4区产品乱码芒果精品| 欧美午夜网站| 国产精品一区二区三区观看| 欧美自拍视频| 日韩欧美第二区在线观看| 色999日韩| 亚洲啊啊啊啊啊| 亚洲一级电影| 欧在线一二三四区| 美腿丝袜亚洲一区| 在线观看日本www| 成人性生交大片免费| 在线观看日韩精品视频| 国产精品全国免费观看高清 | 91久久精品一区二区三| 在线观看毛片视频| 日韩午夜av电影| 亚洲aⅴ乱码精品成人区| 国产亚洲精品久久久久久777| 蜜桃视频在线观看www社区| 欧美第一淫aaasss性| 欧美激情20| 91精品久久久久久久久久| 136福利精品导航| 欧美下载看逼逼| 影音先锋日韩在线| 国产91在线免费| 久久国产剧场电影| 免费看91视频| 国产片一区二区| 免费三片在线播放| 91久久精品网| 亚洲成人黄色片| 中文字幕精品av| 97人人在线视频| 国产伦精品免费视频| 激情亚洲另类图片区小说区| 日韩区国产区| 亚洲福利国产| 欧美在线aaa| 99精品国产热久久91蜜凸| 小嫩苞一区二区三区| 婷婷综合五月天| 国产精品久久久久久久久毛片 | 久久久www| 三上悠亚 电影| 国产精品嫩草影院com| 日韩美女视频网站| 欧美猛男男办公室激情| 全部免费毛片在线播放网站| 欧美成人在线网站| 成人黄色在线| 欧美xxxx黑人又粗又长密月| 欧美成人一区二免费视频软件| 久草在在线视频| eeuss国产一区二区三区 | 国产女人18毛片水真多18| 国产精品毛片久久久久久久| 日韩黄色一级大片| 日韩一级片网站| 黄色成年人视频在线观看| 国产极品jizzhd欧美| 精品福利一区| 国产一线二线三线女| 国产综合成人久久大片91| 亚洲无人区码一码二码三码的含义 | 少妇一级淫片免费放中国 | 中文av一区特黄| 日本三级小视频| 精品国产露脸精彩对白| 国产在线观看av| 91精品国产自产在线| 国产一区二区三区电影在线观看| www.99热这里只有精品| 国产91丝袜在线18| 九九热最新地址| 7777精品伊人久久久大香线蕉 | 免费不卡av网站| 亚洲欧洲成人精品av97| 中国老头性行为xxxx| 亚洲人高潮女人毛茸茸| 黑人巨大亚洲一区二区久| 精品毛片久久久久久| 在线日韩中文| 女同性恋一区二区三区| 亚洲一二三四在线观看| 成人午夜免费福利| 欧美国产一区二区三区| 99久久人爽人人添人人澡| 一本色道久久88亚洲精品综合| 久久66热re国产| 欧美黄色aaa| 欧美刺激午夜性久久久久久久| 自拍亚洲图区| 成人在线观看网址| 好吊一区二区三区| 中文字幕一区二区人妻电影丶| 亚洲国产成人tv| 五十路在线观看| 国产成人精品a视频一区www| 国产一区二区三区探花| 日本三级黄色网址| 综合在线观看色| 精品国产乱码一区二区三| 欧美黄色片视频| 天堂日韩电影| 色乱码一区二区三区在线| 中文字幕日本不卡| www.国产欧美| 91精品国产91久久久久久久久| 亚洲ab电影| 亚洲77777| 一区二区三区欧美视频| 天堂在线视频免费| 国产成人在线一区| 这里只有精品在线| 欧亚乱熟女一区二区在线| 91国模大尺度私拍在线视频| 黄色在线免费网站| 国产精品福利视频| 久久亚洲欧美| 天天综合天天做| 日韩国产精品亚洲а∨天堂免| 亚洲精品粉嫩美女一区| wwwjizzjizzcom| 久久亚洲二区三区| 97国产成人无码精品久久久| 欧美激情精品久久久久久久变态| 亚洲系列另类av| 久久久久久久高清| 欧美日韩免费看| 麻豆网站在线免费观看| 好吊色欧美一区二区三区| 老司机精品视频导航| 国产精品二区一区二区aⅴ| 一个色综合导航| 国产成人一二| 一级做a免费视频| 欧美日韩国产在线看| 黄色网页在线免费看| 久久亚洲午夜电影| 国产东北露脸精品视频| 免费又黄又爽又猛大片午夜|