# Next筆記 ## Automatic Setup 與CRA類似只要把create-react-app改成create-next-app ``` npx create-next-app@latest [projectName] # or yarn create next-app [projectName] ``` 如需支援typescript只需新增--typescript flag ``` npx create-next-app@latest [projectName] --typescript # or yarn create next-app [projectName] --typescript ``` 在pacage.json中你會看到一些scripts ```next "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint" } ``` 這些都是開發者可以調適的環境 * **dev** : run Next in development * **build** : build Next to production * **start** : start Next in production mode after run npm run build * **lint** : set up Next.js' built-in ESLint configuration ## Pages Pages 對於components來說是放置再pages資料夾裡,每一個components檔名就會對應到相對的pase e.g. 我創建一個pages/about.js,這個components就會出現在/about上 ```next function About() { return <div>About</div> } export default About ``` ## Pages with Dynamic Routes Next 提供動態的路徑,只需要創建**pages/posts/[id].js**,這樣在**posts/1, posts/2**都會看到檔案 當你進到 **pages/post/[pid].js** 這個頁面時 : ```next import { useRouter } from 'next/router' const Post = () => { const router = useRouter() const { pid } = router.query return <p>Post: {pid}</p> } export default Post ``` **useRouter** 是Next管理router的hooks,router裡面涵蓋相當多內容,比較於react-router-dom對Next來說router就涵蓋全部,可以透過router.push去對應的頁面,router.query就有對應的pidID,例如我導入pages/post/1,這時我的pid就是1。 ### 以下以pages/post/[pid].js為例子 當我route到pages/post/abc,我對應到的query object : ```next { "pid": "abc" } ``` 當我route到pages/post/abc?foo=bar,我對應到的query object: ```next { "foo": "bar", "pid": "abc" } ``` 但如果我們query的string與page parameters 命名依樣時例如我的route是/post/abc?pid=123,query object就會複寫 : ```next { "pid": "abc" } ``` ### 以下以pages/post/[pid]/[comment].js為例子 如果我route到/post/abc/a-comment,我的query object就會是 ```next { "pid": "abc", "comment": "a-comment" } ``` 考慮到我們clientSide的動態導入,next會用以下的方式 : ```next import Link from 'next/link' function Home() { return ( <ul> <li> <Link href="/post/abc"> <a>Go to pages/post/[pid].js</a> </Link> </li> <li> <Link href="/post/abc?foo=bar"> <a>Also goes to pages/post/[pid].js</a> </Link> </li> <li> <Link href="/post/abc/a-comment"> <a>Go to pages/post/[pid]/[comment].js</a> </Link> </li> </ul> ) } export default Home ``` 是不是跟react-router-donm很像,唯一不同的點原本的to改成href就可以導入了。 > 註 : 把link與a tag比較雖然二者都可以導入到想要的頁面,但兩者確有不同的差別在於,a tag導到新頁面會將畫面refresh,link則是漸進式導入,簡單來說a tag會重整頁面,link跳轉頁面時畫面不會閃爍 ### Catch all routes 但如果今天的router需要比較多階層的話,總不可能一直創建[id].js的檔案,容易造成巢狀地域。 next很貼心的只要新增pages/post/[...slug].js就可以捕捉所有的分頁 例如pages/post/[...slug].js 就會符合 /post/a, but also /post/a/b, /post/a/b/c等等. 而query object就會呈現以下樣子 : ```next { "slug": ["a", "b"] } ``` ### Optional catch all routes 上面的Catch all routes有個缺陷就是無法使用在/post頁面,只要改成pages/post/[[...slug]].js就可以了對應的query object如下 ```next { } // GET `/post` (empty object) { "slug": ["a"] } // `GET /post/a` (single-element array) { "slug": ["a", "b"] } // `GET /post/a/b` (multi-element array) ``` ### API Routes 用法語routes一樣只需要在url前面加api For example, the API route pages/api/post/[pid].js has the following code: ```next export default function handler(req, res) { const { pid } = req.query res.end(`Post: ${pid}`) } ``` ## Pre-rendering Pre-rendering是next非重要的一環,除了可以提高使用者體驗,同時別於CRA更可以做到SEO指標,next有以下兩種Pre-rendering: - Static Generation (Recommended): The HTML is generated at build time and will be reused on each request. - Server-side Rendering: The HTML is generated on each request. 上敘是官方說法Static Generation會再使用者訪談頁面時regeneration對應的html檔案與一個JSON檔,如果你把next專案build起來,你會發現next是根據不同html去router你的page,雖然ssg看起來很美好,但還是有不足的地方,SSG缺點就是會把所有頁面全部generation起來,所以你在build的時候會比較耗時,對user有善但對server是負擔,其二問題是api資料的更新並不會即時update而是使用者造訪頁面時才會rerender,所以才會有SSR出現。 Server-side與Static Generation最大的差別是根據使用者request,直接跟server溝通,使用者進到頁面會丟一個request出來,當有資料更新頁面也會自動render不需要使用者重整頁面。 ## getStaticProps ```next function Blog({ posts }) { // Render posts... } // This function gets called at build time export async function getStaticProps() { // Call an external API endpoint to get posts const res = await fetch('https://.../posts') const posts = await res.json() // By returning { props: { posts } }, the Blog component // will receive `posts` as a prop at build time return { props: { posts, }, } } export default Blog ``` getStaticProps就是典型的ssg,getStaticProps會在server上跑然後傳到props ## getStaticPaths ```next // This function gets called at build time export async function getStaticPaths() { // Call an external API endpoint to get posts const res = await fetch('https://.../posts') const posts = await res.json() // Get the paths we want to pre-render based on posts const paths = posts.map((post) => ({ params: { id: post.id }, })) // We'll pre-render only these paths at build time. // { fallback: false } means other routes should 404. return { paths, fallback: false } } ``` 簡單來說getStaticPaths裡面的paths就是告訴getStaticProps你要render哪些頁面 ```next function Post({ post }) { // Render post... } export async function getStaticPaths() { // ... } // This also gets called at build time export async function getStaticProps({ params }) { // params contains the post `id`. // If the route is like /posts/1, then params.id is 1 const res = await fetch(`https://.../posts/${params.id}`) const post = await res.json() // Pass post data to the page via props return { props: { post } } } export default Post ``` getStaticProps裡的{params}就會去比對getStaticPaths裡的paths是否有match到 ### fallback: false 告訴getStaticProps當進入的router沒有paths指定入徑就會導入404頁面 ### fallback: true 使用 fallback: true 的使用比較複雜一點,因為與 fallback: false 不同的點在於,當使用者瀏覽沒有在 getStaticPaths 中定義的頁面時,Next.js 並不會回應 404 的頁面,而是回應 fallback 的頁面給使用者。 這個流程會呼叫 getStaticProps ,在伺服器產生資料前,使用者瀏覽的是 fallback 的頁面,在 getStaticProps 執行完後,同樣由 props 注入資料到網頁中,使用者這時就能看到完整的頁面。 而經過這個流程的頁面,該頁面會被加入到 pre-rendering 頁面中,下次如果再有同樣頁面的請求時,伺服器並不會再次的重新呼叫 getServerSideProps ,產生新的頁面,而是回應已經產生的頁面給使用者。 使用前幾個章節用到的產品詳細介紹頁面,由於在 getStaticPaths 中的 id 只有 '1' ,所以在 next build 階段只會生成 /products/1 這個頁面的 HTML,但是在設定 fallback: true 的情況下,當一位使用者瀏覽 /products/2 時, Next.js 會做以下幾件事情: 即使該頁面不存在 Next.js 不會回傳 404 頁面,Next.js 會開始動態地生成新的頁面。 在生成頁面時, router.isFallback 會一直為 true ,因次可以用條件式渲染 loading 情況下的頁面,而這時從 props 中拿到的 product 是 undefined。 而在 HTML 生成完畢後,使用者就會看到完整的商品介紹頁面 ``` next import { GetStaticPaths, GetStaticProps } from "next"; import { useRouter } from "next/router"; import { getProductById, Product as ProductType } from "../../fake-data"; import ProductCard from "../../components/ProductCard"; import { PageTitle, ProductContainer } from "./[id].style"; interface ProductProps { product: ProductType; } const Product = ({ product }: ProductProps) => { const router = useRouter(); if (router.isFallback) { return <div>Loading...</div>; } return ( <> <PageTitle>商品詳細頁面</PageTitle> <ProductContainer> <ProductCard product={product} all /> </ProductContainer> </> ); }; export const getStaticProps: GetStaticProps = async ({ params }) => { const product = getProductById(params?.id as string); return { props: { product, }, }; }; export const getStaticPaths: GetStaticPaths = async () => { return { paths: [{ params: { id: "1" } }], fallback: true, }; }; export default Product; ``` ### fallback: block 在 getStaticPaths 使用這個設定時,跟 fallback: true 一樣,在使用者瀏覽不存的頁面時,伺服器不會回傳 404 的頁面,而是執行 getStaticProps ,走 pre-rendering 的流程。 但是與 fallback: true 不一樣的點在於沒有 router.isFallback 的狀態可以使用,而是讓頁面卡在 getStaticProps 的階段,等待執行完後回傳結果給使用者。 所以使用者體驗會非常像似 getServerSideProps ,但優點是下次使用者再次瀏覽同一個頁面時,伺服器可以直接回傳已經生成的 HTML 檔案,往後甚至可以藉由 CDN 的 cache 提升頁面的載入速度。 ## getServerSideProps ```next function Page({ data }) { // Render data... } // This gets called on every request export async function getServerSideProps() { // Fetch data from external API const res = await fetch(`https://.../data`) const data = await res.json() // Pass data to the page via props return { props: { data } } } export default Page ``` getServerSideProps會在request產生頁面,而不是在buildTime ## Client-side data fetching with SWR #### API const { data, error, isValidating, mutate } = useSWR(key, fetcher, options) ```next import useSWR from 'swr' const fetcher = (...args) => fetch(...args).then((res) => res.json()) function Profile() { const { data, error } = useSWR('/api/profile-data', fetcher) if (error) return <div>Failed to load</div> if (!data) return <div>Loading...</div> return ( <div> <h1>{data.name}</h1> <p>{data.bio}</p> </div> ) } ``` SWR優先返回緩存數據(stable),然後再遠端發送請求(revalidate),最後再更新數據, useSWR接收key、feacher做為參數,然後返回data和error。 ### 有条件的请求(Conditional Fetching) ```next // 条件请求 const { data } = useSWR(shouldFetch ? '/api/data' : null, fetcher) // 返回false const { data } = useSWR(() => shouldFetch ? '/api/data' : null, fethcer) // 抛出错误 const { data } = useSWR(() => '/api/data?uid=' + user.id, fetcher) ``` ## Single Shared Layout with Custom App ```next // pages/_app.js import Layout from '../components/layout' export default function MyApp({ Component, pageProps }) { return ( <Layout> <Component {...pageProps} /> </Layout> ) } ``` _app為所有router的共用page只要新增layout所有頁面都可以共用 ### 客製化共用組件 ```next function About() { return ( <div>about</div> ) } About.getPage = (page)=>{ return page } export default About ``` 註 :在About新增getPage方法 ```next function MyApp({ Component, pageProps }) { if (Component.getPage){ return Component.getPage(<Component {...pageProps} />) } return ( <Layout> <Component {...pageProps} /> </Layout> ) } export default MyApp ``` 這裡可以把Component想成每一個頁面,當我的頁面有getPage方法時我就render特定畫面已達到客製化組建方法 ### Using the Image Component ```next import Image from 'next/image' import profilePic from '../public/me.png' function Home() { return ( <> <h1>My Homepage</h1> <Image src={profilePic} alt="Picture of the author" // width={500} automatically provided // height={500} automatically provided // blurDataURL="data:..." automatically provided // placeholder="blur" // Optional blur-up while loading /> <p>Welcome to my homepage!</p> </> ) } ``` 值得一提的是next的Image component很貼心,會自動偵測倒入的圖片畫面大小 ## Image props ### priority 預先加載img的參數 ### ObjectFit 等同使用css的objectfit ### blurDataURL next的Image會有Lazying loading,blurDataURL就是當img在loading時呈現的模糊畫面,通常會搭配 placeholder='blur'一起使用 註 :next很貼心的提供畫面模糊效果製作 [Demo](https://png-pixel.com/) ## next.config.js ### basePath ```jsonld module.exports = { basePath: '/docs', } ``` ### Environment Variables 以往我們使用環境變數會用.env檔案去process,現在只要在next.config.js裡面新增就好 ```NEXT const nextConfig = { reactStrictMode: true, env:{ customkey:'my-value' } } ``` 這樣當我們需要環境變數時直接使用就好 ```next const devEnvironment = process.env.NODE_ENV === 'development' const pro = process.env.NODE_ENV === 'production' const nextConfig = { env: { title: (() => { if (devEnvironment) return 'development'; if (pro) return 'production'; })() } } ``` 我們也可以透過判斷改變env變數結果 ### Redirects Next強大的ROUTER系統 ```next async redirects() { return [ { source: '/home\\(default\\)', destination: 'https://www.youtube.com/watch?v=ndTh5KOIRVQ&ab_channel=PragmaticReviews', permanent: true, has: [ { type: 'query', key: 'page', // the page value will not be available in the // destination since value is provided and doesn't // use a named capture group e.g. (?<page>home) value: 'home', }, { type: 'cookie', key: 'authorized', value: 'true', }, ], } ] } ``` redirects功能是設定特定URL重新導向新頁面 **-source** : 當前的URL這裡範例是以/home(default)為例子 **-destination** : 需要導入的路徑 **-permanent** : 改變瀏覽器狀態定義 **-has** :檢查當前頁面是否有query與cookie內容 ### Rewrites 告訴next當我輸入特定URL時,其實代表另一個page ```next const pro = process.env.NODE_ENV === 'production' const nextConfig = { async rewrites() { return { beforeFiles: [ // These rewrites are checked after pages/public files // are checked but before dynamic routes { source: `${pro ? '/posts' : '/process'}`, destination: '/About', }, ], } }, } ``` rewrites有很多複寫時機點如beforeFiles、afterFiles、fallback這些都是告訴next,我的rewrites到底要在pages/public files比對前後取代page,因為有時我們的source可能已經有寫頁面了,如果沒有用beforeFiles,這樣next就不會去複寫,上述例子我刻意用pro測試環境當我在production mode的情況下要如何做writes。 | beforeFiles| afterFiles |fallback | | -------- | -------- | -------- | | public前 | public後| 調適完再導入 | ### Custom Webpack Config ```javascript // Example config for adding a loader that depends on babel-loader // This source was taken from the @next/mdx plugin source: // https://github.com/vercel/next.js/tree/canary/packages/next-mdx module.exports = { webpack: (config, options) => { config.module.rules.push({ test: /\.mdx/, use: [ options.defaultLoaders.babel, { loader: '@mdx-js/loader', options: pluginOptions.options, }, ], }) config.resolve: { alias: { '@img': path.resolve(__dirname, 'src/img'), }, }, return config }, } ``` next js 可以客製化webpack,把需要的套件載入與設置就好