10 分钟构建一个 next.js 博客

yufei       6 年, 4 月 前       4559

会编程语言最伟大的地方,就在于可以随心所欲的使用各种技术重新构建我们想要尝试的东西。比如今天发现了一个非常有趣的框架 next.js

next.js 融合了好多技术栈,比如 Node.jsReact,而且关注的人极多,在 Github 上将近又 3w 粉丝

next.js

随着 React 揭开它的神秘面纱,前端组件化已经是当前的趋势,而趋势的中心,就是 React

于是,很多人就开始尝试服务器端能否也 React 组件化,这样前后端都可以共用一套模板。服务器端 React 化的好处很多,如共享组件,更快的渲染,更好的工具

不过,想归想,要让它们跑起来却不件容易的事情

Next.js 的出现解决了这个问题。Next.js 是一个强大的,用于构建通用 React 应用程序的新框架。简单来说,它使用 React 在服务器端渲染模板,可以用上组件这种我们最常用的前端开发方式

随着 6.1.1 版本的发布,我想,是时候体验下这个框架了,本章节,我们就用它快速开发一个小博客吧

主要是太久没关注 Node.js 圈了,这么好的框架竟然没发现

技术

本章节接下来的内容,会涉及到以下技术

  1. next ( 6.1.1 )
  2. styled-components ( 疯狂的 css-in-js 解决方案,我们称之为样式组件化 )
  3. next-routes ( next 路由中间件 )
  4. express ( 页面服务器,其实可以直接使用 Node.js 原生静态文件服务 )

1. 开始

我们首先要做的就是设置环境,这个相对来说很简单,毕竟 Node.js 有一套完整的工具,你可以使用你最称手的包管理器安装三个组件 next, reactreact-dom,比如使用 npm

  1. 首先使用 npm init 开始一个新项目

    $ npm init
    This utility will walk you through creating a package.json file.
    It only covers the most common items, and tries to guess sensible defaults.
    
    See `npm help json` for definitive documentation on these fields
    and exactly what they do.
    
    Use `npm install <pkg>` afterwards to install a package and
    save it as a dependency in the package.json file.
    
    Press ^C at any time to quit.
    package name: (next) blog
    version: (1.0.0) 
    description: a simple next.js blog
    entry point: (index.js) app.js
    test command: 
    git repository: 
    keywords: 
    author: 
    license: (ISC) 
    About to write to /Users/yufei/nodejs/next/package.json:
    
    {
      "name": "blog",
      "version": "1.0.0",
      "description": "a simple next.js blog",
      "main": "app.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "author": "",
      "license": "ISC"
    }
    
    Is this OK? (yes) yes
    
  2. 可以使用下面的命令安装所需要的包

    npm i --save next react react-dom
    
  3. 偷懒起见,我们添加一些命令到 scripts 中,修改 package.json 为如下内容

    {
        "name": "blog",
        "version": "1.0.0",
        "description": "a simple next.js blog",
        "main": "app.js",
        "scripts": {
            "dev": "next",
            "build": "next build",
            "start": "next start"
        },
        "author": "",
        "license": "ISC",
        "dependencies": {
            "next": "^6.1.1",
            "react": "^16.4.2",
            "react-dom": "^16.4.2"
        }
    }
    
  4. 创建 pages 目录并在 pages 目录下新建文件 index.js,然后将以下内容添加到 index.js

    // ./pages/index.js
    
    export default () => (
      <div>欢迎来到 next.js !</div>
    )
    
  5. 使用 npm run dev 运行我们的博客,然后打开浏览器访问 http://localhost:3000/ 就能看到如下页面

    npm run dev 会自动检查文件更新然后自动重启

2. 添加一些样式

接下来,我们将配置 styled-components 来设置我们的博客样式

首先使用 npm i --save styled-components 安装,然后重启服务

最后在 page 目录下新建一个名为 _document.js 的文件,并添加以下内容

import Document, { Head, Main, NextScript } from 'next/document'
import { ServerStyleSheet } from 'styled-components'
import '../styles/global-styles';

export default class SiteDocument extends Document {
  render () {
    const sheet = new ServerStyleSheet()
    const main = sheet.collectStyles(<Main />)
    const styleTags = sheet.getStyleElement()
    return (
      <html>
        <Head>
          <meta charset="utf-8" />
          <meta name="viewport" content="initial-scale=1.0, width=device-width" />
          {styleTags}
        </Head>
        <body>
          <div className="root">
            {main}
          </div>
          <NextScript />
          <div>copyright &copy; 简单教程简单编程</div>
        </body>
      </html>
    )
  }
}

自定义 _document.js 允许我们覆盖默认页面布局并注入我们自己的样式和 HTML 代码

毫无意外的,服务挂了

ERROR  Failed to compile with 1 errors

This dependency was not found:

* styles/global-styles in ./pages/_document.js

To install it, you can run: npm install --save styles/global-styles

虽然错误提示我们安装 styles/global-styles,但实际上不是这样的,是因为我们缺少了几个文件

在根目录下 ( 也就是 pages 的父目录 ) 创建 styles 目录,并添加一个 javascript 文件 global-styles.js,内容如下

import { injectGlobal } from 'styled-components'

injectGlobal `
  ul {
    margin: 0;
    padding: 0;
  }

添加完之后就会发现我们的页面又能访问了,页面上多出了版权说明

布局文件

刚刚我们创建的 _document.js 是所有页面的基础布局文件,接下来我们要为我们的博客页面创建一个布局文件

创建一个与 pages 平级的目录 layouts,然后在 layouts 中添加一个文件 Main.jsMain.js 文件内容如下

import Head from 'next/head'
import Wrapper from './Wrapper'
import Nav from '../components/Nav'
import Footer from '../components/Footer'

export default ({ children, title = 'This is the default title' }) => (
  <Wrapper>
    <Head>
      <title>{ title }简单教程简单编程</title>
    </Head>
    <header>
      <Nav />
    </header>

    <main>
      { children }
    </main>

    <Footer>
      简单教程编程入门第一站
    </Footer>
  </Wrapper>
)

这次我们的服务没挂掉,因为我们还没用到这个布局文件。我们将使用此布局来包装博客,这些页面可以覆盖 <Head> 标记并将内容渲染到 {children} 块中

渲染博文

添加完布局页面后,我们就可以修改 index.js 来使用它了,将 index.js 的内容修改如下

// ./pages/index.js

import React from 'react'
import Layout from '../layouts/Main';
import { getPosts } from '../api/posts'
import { Link } from '../routes'


import Post from '../components/Post'

const IndexPage = ({ posts }) => (
  <Layout>
    <ul>
      {posts.map(p => (
        <Post key={p.title} post={p} />
      ))}
    </ul>
  </Layout>
)

IndexPage.getInitialProps = async ({ req }) => {
  const res = await getPosts()
  const json = await res.json()
  return { posts: json }
}

export default IndexPage

这里的关键是我们的 IndexPage 组件上的 getInitialProps ,它获取渲染此页面所需的所有必需数据。当直接在 http://localhost:3000 上访问此页面时, Next 将负责在渲染页面之前获取数据。

因为 <Link /> 组件的存在,如果我们从另一个页面导航到此页面,则不会重新任何其它其它页面,Next 的客户端路由将在渲染组件之前接管并获取数据

同时,你还可以 prefetch 属性来告诉 Next 预先获取该页面以获得快速的页面加载

毫无任何意外的,这次服务又挂了,原因是缺少了 api/posts 模块,也就是缺少了博文数据

ModuleNotFoundError: Module not found: Error: Can't resolve 'api/posts' in '/Users/yufei/nodejs/next/pages'

添加博文

对于博文,我并不想添加自己的文章,而是直接利用现有的网络资源,比如 https://jsonplaceholder.typicode.com ,它有很多 API 接口,其中就包括了一些文章,所以,我们可以直接使用 fetch 函数访问相关的接口资源

创建一个与 pages 同级的 api 目录,并在 api 目录下创建子目录 posts,然后在 api/posts 目录中添加文件 index.js ,内容如下

import fetch from 'isomorphic-fetch'

export function getPosts () {
    return fetch('https://jsonplaceholder.typicode.com/posts')
}

export function getPost (slug) {
    return fetch(`https://jsonplaceholder.typicode.com/posts?title=${slug}`)
}

重启服务,一点也不吃惊,又挂了,这次的错误是缺少 components/Post 组件

Error in ./pages/index.js
Module not found: Error: Can't resolve 'components/Post' in '/Users/yufei/nodejs/next/pages'

创建一个与 pages 平级的目录 components,并在 components 目录中创建子目录 Post,然后在目录 components/Post 目录下新建文件 index.js 并添加以下内容

import React from 'react'
import { Link } from '../../routes'
import Wrapper from './Wrapper'

const PostItem = ({ post }) => (
  <Wrapper>
    <Link route='post' params={{ slug: post.title }}>
      <a>
        <h3>{post.title}</h3>
        <p>{post.body}</p>
      </a>
    </Link>
  </Wrapper>
)

export default PostItem

内容很简单,想必会 React 的人都看得懂

浏览器会自动刷新,这次错误是因为在 components/Post 下缺少 Wrap

Error in ../components/Post
Module not found: Error: Can't resolve './Wrapper' in '/Users/yufei/nodejs/next/components/Post'

components/Post 目录下新建文件 Wrapper.js 并添加以下内容

import styled from 'styled-components'

const Wrapper = styled.div`
  border-bottom: 1px solid #ddd;
  a {
    padding: 15px;
    text-decoration: none;
    display: block;
    &:hover {
      background: #F5F5F5;
      h3 { color: #387EF5 }
    }
  }
  h3 {
    color: #222;
    font-weight: bold;
    font-size: 1.75rem;
    line-height: 35px;
    font-family: "PT Sans", sans-serif;
    text-transform: capitalize;
    margin: 0;
  }
  p {
    font-size: 1.2rem;
    line-height: 35px;
    color: #444;
    font-family: "PT Serif", sans-serif;
    margin: 0;
  }
`

export default Wrapper

一个非常简单的文件,就是添加了一些样式而已

保存之后,还是继续报错,缺少 layouts/Wrapper 模块

Error in ../layouts/Main
Module not found: Error: Can't resolve './Wrapper' in '/Users/yufei/nodejs/next/layouts'

layouts 目录下新建文件 Main.js 并添加以下内容

import styled from 'styled-components'

const Wrapper = styled.footer`
  display: flex;
  min-height: 100vh;
  flex-direction: column;
  main {
    flex: 1;
  }
`

export default Wrapper

也是一个很简单的文件,就是一些样式而已

浏览器自动刷新,继续报错,提示缺少 components/Footer 模块

Error in ../layouts/Main
Module not found: Error: Can't resolve '../components/Footer' in '/Users/luojianguo/Downloads/curl_mail/nodejs/next/layouts'

components 目录下新建目录 Footer ,然后在 components/Footer 目录下新建文件 index.js 并添加以下内容

import styled from 'styled-components'

const Footer = styled.footer`
  padding: 15px;
  text-align:center;
  background: #F5F5F5;
`

export default Footer

继续报错,提示缺少 components/Nav 模块

Error in ../layouts/Main
Module not found: Error: Can't resolve '../components/Nav' in '/Users/yufei/nodejs/next/layouts'

components 目录下新建目录 Nav ,然后在 components/Nav 目录下新建文件 index.js 并添加以下内容

import Link from 'next/link'
import styled from 'styled-components'

const Wrapper = styled.nav`
  padding: 15px;
  border-bottom: 1px solid #ddd;
  display: flex;
  background: #387EF5;
  a {
    padding: 0 15px;
    color: #FFF;
  }
`

const Nav = () => (
  <Wrapper>
    <Link href='/'><a>首页</a></Link> |
    <Link href='/about' prefetch><a>关于我们</a></Link> |
    <Link href='/contact' prefetch><a>联系我们</a></Link>
  </Wrapper>
)

export default Nav

添加了三个链接 首页关于我们联系我们 和它们的样式

然后,终于可以访问了,显示效果如下

文章详情页面

但是,大家有没有发现,首页文章不能点击啊...可能是因为我们缺少文章详情页面逻辑

pages 目录下新建一个文件 post.js 文件并添加以下内容

import React from 'react'
import styled from 'styled-components'
import Layout from '../layouts/Main'
import { getPost } from '../api/posts'

const Wrapper = styled.div`
  padding: 3rem;
  max-width: 750px;
  margin: 0 auto;

  @media (max-width: 750px) {
    width: 100%;
    padding: 1rem;
  }

  h1 {
    color: #222;
    font-weight: bold;
    font-size: 1.75rem;
    line-height: 35px;
    font-family: "PT Sans", sans-serif;
    text-transform: capitalize;
    margin: 0;
  }

  p {
    line-height: 28px;
    color: #666;
    font-family: "PT Sans", sans-serif;
  }
`

const PostPage = ({ post }) =>
  <Layout>
    <Wrapper>
      <h1>
        {post.title}
      </h1>
      <p>
        {post.body}
      </p>
    </Wrapper>
  </Layout>

PostPage.getInitialProps = async ({ query }) => {
  const res = await getPost(query.slug)
  const json = await res.json()
  return { post: json[0] }
}

export default PostPage

很悲剧,即使添加了这些我们仍然不能访问,因为,没法跳转到指定页面,提示 URL 错误

Uncaught TypeError: Parameter 'url' must be a string, not undefined
    at Url.module.exports.webpackJsonp../node_modules/url/url.js.Url.parse (url.js:112)

next-routes

真正的原因是缺少路由器 ( route ) ,为了定义路由器,我们需要使用 next-routes 包,可以使用下面的命令来安装

npm i --save next-routes

然后在 pages 目录下新建一个文件 routes.js 并添加以下内容

const nextRoutes = require('next-routes')
const routes = module.exports = nextRoutes()

routes.add('index', '/')
routes.add('about', '/about')
routes.add('contact','/contact')
routes.add('post', '/blog/:slug')

很好理解,对吧,所有的 Node.js 路由器都是这种格式

express

为了使用路由器,我们还需要安装其它的 Web 框架,再这里,我们就选用 express 吧,使用下面的命令安装 express

npm i --save express

然后在 routes.js 文件的同级目录下新建文件 app.js 并添加以下内容

const express = require('express')
const next = require('next')
const routes = require('./routes')

const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()
const handler = routes.getRequestHandler(app)

app.prepare()
.then(() => {
  const server = express()
  server.use(handler)

  server.get('*', (req, res) => {
    return handle(req, res)
  })

  server.listen(3000, (err) => {
    if (err) throw err
    console.log('> Ready on http://localhost:3000')
  })
})

这个文件有很多新内容

  1. app = next({ dev }) 是根据运行环境创建应用程序 app

  2. handlehandler 两个常量用于创建请求处理中间件

    const handle = app.getRequestHandler()
    const handler = routes.getRequestHandler(app)
    
  3. 剩余的代码就是创建 express 框架实例,并使用 next 中间件来处理页面逻辑

关闭我们的服务,然后使用 node app.js 重新启动博客,然后就可以正常访问了

对于正式环境,可以使用下面的脚本来启动

NODE_PATH=. NODE_ENV=production node app.js

其它页面

但这时候点击 联系我们关于我们 会提示 404

强迫症犯了的我,只能把这两个页面也加上

pages 目录新建两个文件 contact.jsabout.js ,内容分别如下

contact.js

import Layout from '../layouts/Main'

export default () => <Layout>联系我们那就访问: 简单教程</Layout>

about.js

import Layout from '../layouts/Main'

export default () => <Layout>我们为教程简化而努力</Layout>

结束语

我为什么要介绍这个框架呢 ?

因为我想写一些 API 开放给个人,这样它们就有了一个存储数据的地方

目前尚无回复
简单教程 = 简单教程,简单编程
简单教程 是一个关于技术和学习的地方
现在注册
已注册用户请 登入
关于   |   FAQ   |   我们的愿景   |   广告投放   |  博客

  简单教程,简单编程 - IT 入门首选站

Copyright © 2013-2022 简单教程 twle.cn All Rights Reserved.