<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          動手練一練,使用 React 和 Next.js 做一個簡單的博客網(wǎng)站(中)

          共 17049字,需瀏覽 35分鐘

           ·

          2021-04-11 23:32

          作者:Craig Bucklere
                 原文:Build a Blog with React and Next.js(sitepoint) 
                 字數(shù):4272 字 (非直譯,有添加部分)
                 閱讀: 10 分鐘

          大家好,在《動手練一練,使用 React 和 Next.js 做一個簡單的博客網(wǎng)站(上)》一篇文章里,我們一起了解了什么是 Next.js,并手工創(chuàng)建了一個簡單的 Next.js 項目,學會了如何基于模板創(chuàng)建簡單的頁面,本篇文章,我們繼續(xù)完善這個案例。

          一、基于MD文檔生成動態(tài)路由

          創(chuàng)建博客,自然少不了文章內(nèi)容,如果我們每寫一篇文章,就創(chuàng)建一個 JSX 單頁面,這樣太不現(xiàn)實,費事費力又不容易維護,我們開發(fā)人員更喜歡使用 Markdown 文檔寫文檔。

          慶幸的是,Next.js 允許我們使用 Markdown 作為文章的數(shù)據(jù)源,基于文件名生成動態(tài)路由,并且實現(xiàn)文件內(nèi)容的 HTML 靜態(tài)化。

          1、在編寫本功能時,最好停止 Next.js  服務(Ctrl | Cmd + C)。

          2、接下來,在項目的根目錄里創(chuàng)建 articles 文件夾,把你的 Markdown 文件放置在這里,例如:articles/article-01.md,MD 文檔格式如下所示:

          ---
          title: The first article
          description: This is the first article.
          date: 2020-10-01
          ---

          This is an article post.
          ## Subheading
          Lorem ipsum dolor sit amet, consectetur adipiscing elit.

          我們將文檔的標題名稱、文檔描述、創(chuàng)建日期放置在 — 之間,F(xiàn)ront-matter 這個 npm 插件基于這個格式可以讀取上述相關信息提取文檔的標題、描述、創(chuàng)建日期。要將 MD 文檔格式化成網(wǎng)頁的形式,我們還需要安裝 remark 和  remark-html 這兩個npm 插件,安裝命令如下:

          npm i front-matter remark remark-html

          3、安裝完成后,我們要實現(xiàn)讀取和格式化 MD 文檔的功能,接下來創(chuàng)建 lib/posts-md.js 工具函數(shù)文件。getFileIds(dir) 函數(shù)返回一個 MD 文件名的數(shù)組(不包含 .md 擴展名的文件名),示例代碼如下:

          import { promises as fsp } from 'fs';
          import path from 'path';
          import fm from 'front-matter';
          import remark from 'remark';
          import remarkhtml from 'remark-html';
          import * as dateformat from './dateformat';
          const fileExt = 'md';
          // return absolute path to folder
          function absPath(dir{
            return (
              path.isAbsolute(dir) ? dir : path.resolve(process.cwd(), dir)
            );
          }
          // return array of files by type in a directory and remove extensions
          export async function getFileIds(dir = './'{
            const loc = absPath(dir);
            const files = await fsp.readdir(loc);
            return files
              .filter((fn) => path.extname(fn) === `.${fileExt}`)
              .map((fn) => path.basename(fn, path.extname(fn)));
          }

          獲取到文件名數(shù)組后,我們需要解析 MD 的具體內(nèi)容,比如文件的標題、描述、創(chuàng)建日期、具體的內(nèi)容HTML格式化等,示例代碼如下:

          export async function getFileData(dir = './', id{
            const
              file = path.join(absPath(dir), `${id}.${fileExt}`),
              stat = await fsp.stat(file),
              data = await fsp.readFile(file, 'utf8'),
              matter = fm(data),
              html = (await remark().use(remarkhtml).process(matter.body)).toString();
            // date formatting
            const date = matter.attributes.date || stat.ctime;
            matter.attributes.date = date.toUTCString();
            matter.attributes.dateYMD = dateformat.ymd(date);
            matter.attributes.dateFriendly = dateformat.friendly(date);
            // word count
            const
              roundTo     = 10,
              readPerMin  = 200,
              numFormat   = new Intl.NumberFormat('en'),
              count       = matter.body.replace(/\W/g' ').replace(/\s+/g' ').split(' ').length,
              words       = Math.ceil(count / roundTo) * roundTo,
              mins        = Math.ceil(count / readPerMin);
            matter.attributes.wordcount = `${ numFormat.format(words) } words, ${ numFormat.format(mins) }-minute read`;
            return {
              id,
              html,
              ...matter.attributes
            };
          }

          你可能注意到我使用了日期格式化功能,其功能定義在 lib/dateformat.js 文件,示例代碼如下:

          // date formatting functions
          const toMonth = new Intl.DateTimeFormat('en', { month'long' });
          // format a date to YYYY-MM-DD
          export function ymd(date{
            return date instanceof Date
              ? `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2'0')}-${String(date.getUTCDate()).padStart(2'0')}` : '';
          }
          // format a date to DD MMMM, YYYY
          export function friendly(date{
            return date instanceof Date
              ? `${date.getUTCDate()} ${toMonth.format(date)}${date.getUTCFullYear()}` : '';
          }

          4、Next.js 使用帶 [ ] 符號的特殊的文件名生成動態(tài)路由。接下來我們在 Pages 目錄下創(chuàng)建這個特殊的文件 pages/articles/[id].js, Next.js 使用id作為路由的參數(shù),生成 /articles/article-01 的頁面路由。

          pages/articles/[id].js 這個文件里實現(xiàn)Next.js 特有的 getStaticPaths() 函數(shù)功能(Static Generation),在項目構建時生成指定的路由路徑,比如這個案例將 articles 目錄下的 MD 文檔返回如下的數(shù)組格式,id 將匹配 pages/articles/[id].js 對應的 [id] 參數(shù)生成動態(tài)路由:

          [
            { params: { id: "article-01" } },
            { params: { id: "article-02" } },
            { params: { id: "article-03" } },
            ...
          ]

          這個方法調(diào)用 lib/posts-md.js 文件里讀取 getFileIds 文件路徑列表的方法,示例代碼如下:

          import { getFileIds, getFileData } from '../../lib/posts-md';
          // post directory
          const postsDir = 'articles';
          // dynamic route IDs
          export async function getStaticPaths({
            const
              paths = (await getFileIds(postsDir))
                .map((id) => ({ params: { id } }));
            return {
              paths,
              fallbackfalse,
            };
          }

          5、動態(tài)路由生成后,我們需要實現(xiàn) MD 內(nèi)容格式化渲染,我們實現(xiàn)Next.js 特有的異步方法 getStaticProps({ params }),在項目構建時調(diào)用這個函數(shù)(Static Generation),通過 id 參數(shù)調(diào)用 lib/posts-md.js 文件中 getFileData() 定義的方法,將 MD 文檔內(nèi)容異步回傳至包含 postData 屬性的組件內(nèi)部(第六點的代碼部分),示例代碼如下:

          // dynamic route content
          export async function getStaticProps({ params }{
            return {
              props: {
                postDataawait getFileData(postsDir, params.id),
              },
            };
          }

          6、拿到數(shù)據(jù)后,我們需要填充到組件的模板里,以更友好的形式展現(xiàn),我們在 pages/articles/[id].js 里編寫JSX的相關代碼,將文章內(nèi)容嵌套在上節(jié)組件模板內(nèi),示例代碼如下:

          import Layout from '../../components/layout';
          import Head from 'next/head';
          ...
          export default function Article({ postData }{
            // generate HTML from markdown content
            const html = `
              <h1>${ postData.title }</h1>
              <p class="time"><time datetime="${ postData.dateYMD }">${ postData.dateFriendly }</time></p>
              <p class="words">${ postData.wordcount }</p>
              ${ postData.html }
            `
          ;
            return (
              <Layout hero="phone.jpg">
                <Head>
                  <title>{ postData.title }</title>
                  <meta name="description" content={ postData.description } />
                </Head>
                <article dangerouslySetInnerHTML={{ __html: html }} />
              </Layout>

            );
          }

          最后我們需要重啟 Next.js 服務,一切正常的話,你會發(fā)現(xiàn)所有的 MD 文檔可以同過 /articles/文件名的路徑在瀏覽器上查看, 例如 http://localhost:3000/articles/article-01  對應 /articles/article-01.md這個 MD 文檔,效果如下圖所示:


          二、創(chuàng)建博客列表頁

          有了博客相關的內(nèi)容頁,我們需要建一個按照文檔創(chuàng)建時間倒序排列的博客列表頁

          1、首先我們在 lib/posts-md.js 文件里,定義一個 getAllFiles() 方法獲取指定目錄下文件列表:

          • 將 MD 文檔的內(nèi)容加載到數(shù)組里
          • 移除沒有內(nèi)容的文件
          • 按照文章的日期倒序排列
          // return sorted array of all posts for indexes
          export async function getAllFiles(dir{
            const
              now = dateformat.ymd(new Date()),
              files = await getFileIds(dir),
              data = await Promise.allSettled( files.map(id => getFileData(dir, id)) );
            return data
              .filter(md => md.value && md.value.dateYMD <= now)
              .map(md => md.value)
              .sort((a, b) => (a.dateYMD < b.dateYMD ? 1 : -1));
          }

          2、接下來我們新建一個博客列表頁 pages/articles/index.js,創(chuàng)建一個異步方法 getStaticProps(),在項目構件時,調(diào)用剛才我們編寫的方法  getAllFiles(),將文件列表內(nèi)容返回組件的 postData 的屬性里(第三點的代碼部分),示例代碼如下:

          import { getAllFiles } from '../../lib/posts-md';
          const postsDir = 'articles';
          // fetch array of all article posts
          export async function getStaticProps({
            return {
              props: {
                postDataawait getAllFiles(postsDir),
              },
            };
          }

          3、接下來在我們需要將博客列表的內(nèi)容輸出到 pages/articles/index.js 頁面進行顯示,使用數(shù)組的 map() 方法迭代解析上述方法 postData 返回的內(nèi)容,示例代碼如下:

          import Layout from '../../components/layout';
          import Pagelink from '../../components/pagelink';
          import Head from 'next/head';
          export default function ArticleIndex({ postData }{
            return (
              <Layout hero="phone.jpg">
                <Head>
                  <title>Article index</title>
                  <meta name="description" content="A list of articles published on this site." />
                </Head>
                <h1>Article index</h1>
                <aside className="pagelist">
                  { postData.map(post => (
                    <Pagelink
                      key={ post.id }
                      postsdir={ postsDir }
                      id={ post.id }
                      title={ post.title }
                      description={ post.description }
                      dateymd={ post.dateYMD }
                      datefriendly={ post.dateFriendly }
                    />

                  ))}
                </aside>
              </Layout>

            );
          }

          4、你可能注意到,上述代碼我引用了一個<Pagelink>組件,其定義在 components/pagelink.js 文件里,此組件實現(xiàn)了顯示文章的標題、鏈接、描述、日期等,示例代碼如下:

          import Link from 'next/link';
          export default function Pagelink(props{
            const link = `/${ props.postsdir }/${ props.id }`;
            return (
              <article>
                <h2><Link href={ link }><a>{ props.title }</a></Link></h2>
                <p className="time"><time dateTime={ props.dateymd }>{ props.datefriendly }</time></p>
                <p>{ props.description }</p>
              </article>

            );
          }

          到這里博客列表頁的功能就全部完成了,在瀏覽器輸入 http://localhost:3000/articles  預覽效果如下圖所示:



          所有的 MD 的文件都會羅列在此頁面,隨著內(nèi)容的增加,你需要增加相關的邏輯進行分頁,這里你就需要用到 getStaticPaths() 這個方法,并且需要此頁面改成 pages/articles/[index].js(注:index可以換成你想要的參數(shù),但是需要和getStaticPaths 方法中的參數(shù)對應),在頁面構建時生成對應的頁面路由,你可以參照第一部分基于MD文檔生成動態(tài)路由這部分內(nèi)容,具體的邏輯你可以考慮下怎么實現(xiàn),這里就不再介紹了;

          三、創(chuàng)建網(wǎng)站導航

          為了讓用戶更方便瀏覽我們的博客網(wǎng)站,我們需要新建 components/navmenu.js 導航組件,用來實現(xiàn)網(wǎng)站導航的功能,由于功能簡單,這里就不再解釋,示例代碼如下:

          import Link from 'next/link';
          import Link from 'next/link';
          // menu name and link
          const menu = [
            { text'home'link'/' },
            { text'about'link'/about' },
            { text'articles'link'/articles' }
          ];
          // render menu
          export default function Navmenu({
            // get current page route
            const
              router = useRouter(),
              currentPage = router.pathname;
            return (
              <nav>
                <ul>
                  { menu.map(item => (
                    <Navlink
                      key={ item.link }
                      text={ item.text }
                      link={ item.link }
                      currentpage={ currentPage }
                    />

                  ))}
                </ul>
              </nav>

            )
          }
          // render individual menu link
          function Navlink({ text, link, currentpage }{
            if (link === currentpage) {
              return (
                <li className="active"><strong>{ text }</strong></li>
              );
            }
            else {
              return (
                <li><Link href={ link }><a>{ text }</a></Link></li>
              );
            }
          }

          導航組件完成后,我們將其引入 components/header.js 組件內(nèi),示例代碼如下:

          import Navmenu from './navmenu';

          更新后的 JSX 代碼如下:

          ...
          <Navmenu />
          <figure>
            <img src={ hero } width="400" height="300" alt="decoration" />
          </figure>

          ...

          完成后,博客導航的效果如下圖所示:


          四、使用Sass為博客添加全局樣式

          到這里,一個基于 MD 文檔的簡單博客網(wǎng)站到這里就完成了,最后我們要為網(wǎng)站添加樣式,要不網(wǎng)站丑的實在看不下去。

          Next.js 可以使用 Sass, Less, PostCSS, Styled JSX, CSS modules、plain old CSS等多種方式為站點添加樣式,這里我們使用 Sass 為站點添加樣式,這里我們手工為項目安裝Sass:

          npm i sass

          接下來我們可以為每個組件定義相關的樣式,然后將其合并在一個 styles/global.scss 文件里,由于本篇文章重點講述Next.JS 的用法,這里就不介紹如何編寫Sass,感興趣的同學可以點擊文末的閱讀原文下載本文的 Sass 樣式:

          // settings
          @import '01-settings/_variables';
          @import '01-settings/_mixins';
          // reset
          @import '02-generic/_reset';
          // elements
          @import '03-elements/_primary';
          // layout
          @import '04-layout/_site';
          // components
          @import '05-components/_header';
          @import '05-components/_footer';
          @import '05-components/_article';

          最后我們需要將 styles/global.scss 引入到 pages/_app.js 這個特殊的文件里,這樣網(wǎng)站所有的頁面都可以使用此樣式,示例代碼如下:

          import '../styles/global.scss';
          export default function App({ Component, pageProps }{
            return <Component {...pageProps} />
          };

          最后我們重新啟動 Next.js 服務,你將會看到一個還算漂亮的博客首頁,如下圖所示:


          未完待續(xù)

          由于篇幅原因,今天的文章就到這里,一個基于 MD 文檔的簡單博客網(wǎng)站就完成了,通過本篇文章我們學習了如何基于MD文檔生成動態(tài)路由,完成了文章內(nèi)容頁、列表頁、導航功能,并為網(wǎng)站添加了漂亮的樣式。在下篇文章里,我們?yōu)椴┛途W(wǎng)站添加暗黑模式,基于接口數(shù)據(jù)渲染內(nèi)容(服務端渲染),及如何編譯項目將博客網(wǎng)站部署到 Node.js 服務器上或純靜態(tài)化部署,最后會提供完整的項目源碼,敬請期待...

          瀏覽 29
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  有免费片视频吗 日日爱866 | 日韩人妻精品在线 | 欧美成人精品欧美一级 | 亚洲区色情区激情区小说纯熟调抖 | 爱爱免费看片 |