Next.js框架学习

2,162 阅读12分钟

前言

Next.js是一个轻量级的 React 服务端渲染应用框架。使用Next.js,可以优化SEO、加快首屏加载速度...

一、初始化项目

手动创建Next.js项目

  • 新建文件夹

    mkdir nextDemo
    cd nextDemo
    npm init
    
  • 安装依赖包

    yarn add react react-dom next
    # OR
    npm i -S next react react-dom
    
  • 修改package.json

    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "dev" : "next" ,
        "build" : " next build",
        "start" : "next start"
    },
    
  • 创建pages文件夹,尝试开发首页代码

    • Next规定在pages下写入的文件,会自动创建对应的路由

    • 新建index.js文件,并写入以下代码,Next将自动创建/路由

      function Index(){
          return (
              <div>Hello World!</div>
          )
      }
      export default Index
      
  • 在浏览器中预览
    yarn dev
    成功后即可在浏览器对应端口下访问

使用create-next-app脚手架

  • 准备步骤

    npm i -g create-next-app
    npm i -g npx
    # OR
    yarn global add create-next-app
    yarn global add npx
    
  • 创建Next.js项目

    npx create-next-app next-demo    # next-demo为项目名
    cd next-demo
    yarn dev                         # 成功后即可在3000端口下访问
    

二、Next.js基础应用

Pages新建路由

前面已经介绍过如何在next中新建一级路由:只需在pages目录下新建frontend.js文件,那么next将自动注册/frontend路由
那么如果想新建一个二级路由,同理只需调整pages下的目录结构

├── pages                    
│   ├── blog                # 一级路由
│   |   ├── firstblog.js    # 二级路由 /blog/firstblog
│   |   ├──secondblog.js    # 二级路由 /blog/secondblog

路由基础

Link标签进行路由跳转

  • 新建pages/pageA.js示范

    import Link from 'next/link'  // 重点
    
    export default ()=>(
        <>
            <div> This is PageA </div>
            <Link href="/"><a>返回首页</a></Link>
        </>
    )
    
  • 注意点:

    1. Link标签默认不渲染任何东西,所以还要在标签内写上要进行渲染的标签
    2. Link标签通过href属性进行跳转
    3. Link内部代码不允许兄弟标签并列,必须嵌套一层父标签

利用Router模块,编程式路由跳转

  • pages/index.js示范

    import Router from 'next/router'  // 重点
    
    export default ()=>(
        <>
            <div> This is Index </div>
            <button onClick={()=> Router.push('/pageA')}>前往pageA页面</button>
        </>
    )
    

路由传参

  • 注意:在Next中只能用query形式传递参数,即只能用?id=1形式,而不能用/:id形式

    import React from 'react'
    import Link from 'next/link'
    import Router from 'next/router'
    
    const Index = () => {
    
      function gotoPageA(){
        Router.push({
          pathname: '/pageA',
          query: { id: 4 }
        })
      }
      
      return(
        <>
          <div> This is Index </div>
          <div>
            <Link href="/pageA?id=1"><a>go to pageA, id is 1</a></Link>
            <Link href={{ pathname: '/pageA', query: {id: 2} }}><a>go to pageA, id is 2</a></Link>
            <button onClick={()=> Router.push('/pageA?id=3')}>go to pageA, id is 3</button>
            <button onClick={gotoPageA}>go to pageA, id is 4</button>
          </div>
        </>
      )
    }
    export default Index
    
  • 在组件中接收路由参数

    import { withRouter } from 'next/router'
    
    // 只有使用 withRouter 包裹才能使用 router 接收路由参数
    const pageA = ({ router })=>{
        return (
            <>
                <div>This is pageA.</div>
                <div>id is {router.query.id}</div>
            </>
        )
    }
    
    export default withRouter(pageA)
    

路由映射

在上面路由传参实例中,使用了query形式传递路由参数id?id=1,但在实际开发中,对于参数是id,更习惯用path:id形式传递,这样url看上去也更贴近真实开发,所以我们可以使用路由映射来解决这个问题

  • as属性的使用

    import Link from 'next/link'
    import Router from 'next/router'
    
    export default () => {
        gotoPageA = () => {
            // 在Router.push方法中传递第二个参数,实现路由映射
            Router.push({
                    pathname: '/pageA',
                    query: { id: 2 }
                }, '/pageA/2') 
        }
        
        return (
        	<>
        	    {/* 利用as属性,实现路由映射 */}
                <Link href="/pageA?id=1" as="/pageA/1"><a>go to pageA, id is 1</a></Link>
                <button onClick={gotoPageA}>go to pageA, id is 2</button>
            </>
        )
    }
    
  • 注意:使用as属性处理路由映射后,页面url将会对应改变。此时可以试着刷新页面,会发现竟然出现404访问失败。

原因是:这时的路由不是真实的路由,真实的路由是通过query形式传递参数(/pageA?id=1),而使用路由映射后,地址栏的url变成/pageA/1,刷新页面后,next会进行服务端渲染,服务端会按照映射后的路由去渲染对应的文件,但这个文件是不存在的。比如服务端这时会去找pages/pageA目录下的1.js文件,但该文件并不存在,因此会出现404的错误。
解决方法:既然是在服务端渲染过程中出错,当然是在服务端代码中解决。这点将在后续讲述。

路由钩子事件

import Link from 'next/link'
import Router from 'next/router'

// 路由的六个钩子事件,见名知意
const events = [
    'routeChangeStart',
    'routeChangeComplete',
    'routeChangeError',
    'beforeHistoryChange',
    'hashChangeStart',
    'hashChangeComplete'
];

function makeEvent(type){
    return (...args) => {
        console.log(type, ...args)
    }
}

events.forEach(event => {
    Router.events.on(event, makeEvent(event));
});

export default () => {
    return (
    	<>
            <Link href="/pageA"><a>go to pageA</a></Link>
            <Link href="#one"><a>go to part one</a></Link>
            <Link href="/404"><a>go to 404</a></Link>
        </>
    )
}

getInitialProps

在实际开发中,通常会在组件初始化后,请求远端数据。如在vuemounted生命周期中,reactcomponentDidMount生命周期中发起请求。而在在Next.js中提供了getInitialProps方法用来获取远端数据,而不是放在组件的生命周期里。
使用示例:

  • yarn add axios

  •     import axios from 'axios'
      
        const pageA = ({ data })=>{
            return (
                <>
                    <div>initial props: {data}</div>
                </>
            )
        }
        
        pageA.getInitialProps = async () => {
            const {status, data} = await axios(url);
            if(status === 200){
                return { data }  // 作为props.data传出
            }
        }
        
        export default pageA 
    
  • 类组件中使用

     import React from "react"
    
    class PageA extends React.Component {
      static getInitialProps = async () => {
       const {status, data} = await axios(url);
        if(status === 200){
            return { data }  // 作为props.data传出
        }
      };
    
      render() {
        const { data } = this.props
        return (
            <>
                <div>initial props: {data}</div>
            </>
        )
      }
    }
    
    export default PageA;
    
  • 注意:

    1. getInitialProps默认传入的参数为一个对象,对象属性如下:

    具体说明见github + pathname - URL 的 path 部分 + query - URL 的 query 部分 + asPath - 路由映射后,浏览器中显示的路径(包含查询部分) + req - HTTP 请求对象 (只有服务器端有) + res - HTTP 返回对象 (只有服务器端有) + jsonPageRes - 获取数据响应对象 (只有客户端有) + err - 渲染过程中的任何错误对象
    2. 在进行服务端和客户端渲染时都能执行该方法。 + 如果是服务端进行首屏渲染时,会执行该路由组件下的getInitialProps,并返回相应的props参数,当组件拿到props并渲染好后,服务端才会返回,这时客户端不会再执行一遍该组件的getInitialProps + 当客户端用Link标签或API方法跳转路由组件时,客户端会去执行对应的路由组件的getInitialProps,此时服务端则不会执行 3. 只有放在 pages目录下的路由组件,它的getInitialProps才会被调用,子组件使用该方法无效

页面CSS

使用Style JSX编写页面CSS

  • Next默认不能引用CSS文件,需要另外配置,但是可以使用style jsx编写

  • 示例

    export default () => {
        const fontColor = 'red'
        
        return (
            <>
                <div className="test">This is pageA.</div>
                <div className="test2">This is pageA.</div>
                
                注意使用格式:<style jsx>{`...`}</style>
                <style jsx>
                {`
                    .test { color: blue; }
                    .test2 { color: ${fontColor}; }
                `}
                </style>
            </>
        )
    }
    
  • 说明:加入Style jsx代码后,Next会自动生成随机类名jsx-xxxx,防止CSS全局污染

引入CSS文件

开发中对于全局样式,通常会引入css文件,而不是<style global jsx>标签形式。此时,需要让Next支持css文件引入

  • yarn add @zeit/next-css

  • 新建next.config.js

    const withCss = require('@Zeit/next-css');
    
    if(typeof require !== 'undefined'){
    	require.extensions['.css'] = file => {}
    }
    module.exports = withCss({})
    
  • public or static目录下新建css文件,在组件中引入即可
    import '../public/test.css'

拓展:集成styled-components

styled-components 是基于 Css In Js 方式实现的一个库。可以使用标签模板来对组件进行样式化,是一种广受关注的React的样式方案。

  •     yarn add style-components babel-plugin-styled-components
      复制代码
    
  • 编辑.babelrc

    {
      "presets": ["next/babel"],
      "plugins": [
        ["styled-components", { "ssr": true }]
      ]
    }
    
  • 在组件中使用

    import styled from 'styled-components'
    
    const Title = styled.h1`
            color: yellow;
            font-size: 40px;
        `
    
    export default () => (
        <>
        	<Title>This is a page</Title>
        </>
    )
    

动态加载模块&组件(懒加载)

异步加载模块

  • yarn add moment

  • 示例

    import React, {useState} from 'react'
    
    export default () => {
        const [time,setTime] = useState(Date.now())
    
        // 注意使用 async await 来等待模块加载完成
        const handleChangeTime = async ()=>{ 
            const moment = await import('moment') // 动态加载moment模块
            setTime(moment.default(Date.now()).format()) //注意使用default
        }
        return (
            <>
                <div>目前时间:{time}</div>
                <button onClick={handleChangeTime}>改变时间格式</button>
            </>
        )
    }
    

    可以在控制台面板中观察,当点击按钮时,才会加载1.js,即momnet.js的内容

异步加载组件

  • 新建components/one.js子组件

    export default ()=><div>Lazy Loading Component</div>
    复制代码
    
  • 在父组件中使用

    import React, {useState} from 'react'
    import dynamic from 'next/dynamic'     // 重点
    
    const One = dynamic(import('../components/one')) // 重点
    
    export default () => {
        const [time,setTime] = useState(Date.now())
    
        // 注意使用 async await 来等待模块加载完成
        const handleChangeTime = async ()=>{ 
            const moment = await import('moment') // 动态加载moment模块
            setTime(moment.default(Date.now()).format('YYYY-MM-DD')) //注意使用default
        }
        return (
            <>
                <div>目前时间:{time}</div>
                <button onClick={handleChangeTime}>改变时间格式</button>
                <One />
            </>
        )
    }
    

自定义Head

使用Next的目的其中就是为了优化SEO。对于不同的页面,有时候需要自定义Head,以便更好地支持SEO

  • 示例

    import Head from 'next/head'
    
    export default () => (
        <>
            <Head>
                <title>PageA</title>
                <meta charSet='utf-8' />
            </Head>
            <div>This is pageA</div>
        </> 
    )
    

自定义App

在开发中,有时我们需要自定义Layout组件,并全局使用;或者保持一些公用的状态,给页面传入一些自定义数据...此时,可以在pages/_app.js文件中实现

import App, { Container } from 'next/app'

class MyApp extends App {
    // 在App.getInitialProps中获取全局数据,每一次页面切换都会调用该方法
    // Component--页面组件
    static async getInitialProps({ Component, ctx }){
        // getInitialProps可以跳过不实现,这样会执行默认的方法
        // 一旦自定义App.getInitialProps后,必须判断页面组件是否有getInitialProps
        // 若有,必须执行它,获取返回的参数,再去传入<Component>
        // 这样页面组件才能拿到对应的props
        let pageProps
        if(Component.getInitialProps){
          pageProps  = await Component.getInitialProps(ctx)   
        }
        return {
            pageProps
        }
    }
    
    render(){
        // Component 相当于每个页面组件
        const { Component, pageProps } = this.props
        
        return (
            <Container>
            	<Component { ...pageProps } />
            </Container>
        )
    }
}
export default MyApp

三、在Next中集成Koa

集成Koa

  • yarn add koa

  • 根目录下新建server.js作为同时启动nextkoa的入口文件

    const Koa = require('koa');
    const next = require('next');
    
    // 判断处于开发环境或是生产环境
    const dev = process.env.NODE_ENV !== 'production';
    const app = next({ dev });
    const handle = app.getRequestHandler();  // 利用handle处理http请求
    
    // 待pages下的文件编译完成后执行
    app.prepare().then(() => {
        const server = new Koa();
        // 使用next中间件
        server.use(async (ctx, next) => {
            // ctx.req, ctx.res 为node原生的request/response对象
            await handle(ctx.req, ctx.res);  
            ctx.respond = false;
        });
        
        server.listen(3000, () => {
            console.log('koa server listening on 3000');
        });
    });
    
  • 修改package.json

    "scripts": {
        "dev": "nodemon server.js",
        "build": "next build",
        "start": "next start"
     }
    

解决服务端渲染404

之前提到在使用路由映射后,刷新页面,服务端渲染过程中可能会出现404情况。这里提出解决方案

const Koa = require('koa');
const Router = require('koa-router');
const next = require('next');

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

app.prepare().then(() => {
    const server = new Koa();
    const router = new Router();
    
    /* --- 重点start --- */
    router.get('/a/:id', async (ctx) => {
        const id = ctx.params.id;
        await handle(ctx.req, ctx.res, {
            pathname: '/a',
            query: { id }
        });
        ctx.respond = false;
    });
    /* ---- 重点end ---- */
    
    server.use(router.routes());
    
    server.use(async (ctx, next) => {
        await handle(ctx.req, ctx.res);
        ctx.respond = false;
    });
    
    server.listen(3000, () => {
        console.log('koa server listening on 3000');
    });
});

四、Next中集成Ant Design

  • yarn add @zeit/next-css

  • 新建next.config.js 支持css文件引入

    const withCss = require('@Zeit/next-css');
    
    if(typeof require !== 'undefined'){
        require.extensions['.css'] = file => {}
    }
    module.exports = withCss({})
    
  • yarn add babel-plugin-import antd 按需加载

  • 根目录下新建.babelrc

    {
      "presets": ["next/babel"],
      "plugins": [
        [
          "import",
          {
            "libraryName": "antd"
          }
        ]
      ]
    }
    
  • 引入antd全局CSS文件

    新建`pages/_app.js`
    
    import App from 'next/app'
    import 'antd/dist/antd.css'
    
    export default App
    

五、PM2 部署Next.js 应用

部署上线这一块实在没什么好说的,简单的话直接起一个node服务的就可以,复杂一点就要包括报警重启等等,都是看个人情况的。

开发环境

## 开发环境启动
yarn dev (启动一个热加载的Web服务器)

## 项目打包构建
yarn build  (利用webpack编译应用,压缩JS和CSS资源(发布用))

## 项目生产环境启动
yarn start  (以生产模式启动一个Web服务器)

生产环境

Tips: 由于生产环境需要程序持续运行,使用PM2守护进程持续部署前端应用

# 通过pm2命令启动应用  --name: 应用名称(起名) --interpreter: 以自定义命令执行(此处执行yarn start)
pm2 start yarn --name "client" --interpreter bash -- start 

# 启动脚本
pm2 startup

此时前端应用已使用PM2跑了起来,执行

pm2 list

查看pm2进程的状态,如果status 是online,就是正常状态,如果是其他状态则不正常,以后每次代码更新完,直接使用命令

pm2 restart xxx (xxx即最开始--name那里取的名字)

赋:PM2常用命令

1.全局安装

npm install pm2 -g 
or 
yarn add pm2 -g

2、pm2启动

pm2 start xxx 

3、pm2查看进程

pm2 list
pm2 show 1 或者 # pm2 info 1         #查看进程详细信息,1为PM2进程id 

4、pm2监控

pm2 monit

5、pm2停止

pm2 stop all                         #停止PM2列表中所有的进程
pm2 stop 0                           #停止PM2列表中进程为0的进程

6、pm2重启

pm2 restart all                      #重启PM2列表中所有的进程
pm2 restart 0                        #重启PM2列表中进程为0的进程

7、pm2删除某个进程

pm2 delete 0                         # 删除PM2列表中进程为0的进程
pm2 delete all                       # 删除PM2列表中所有的进程

六、 Docker部署

准备工作

  1. 打包生成.next目录
  2. BUILD_ID 的版本号和server static一致

docker化node应用

生成Dockerfile & .dockerignore文件

touch Dockerfile//生成docker文件

截屏2021-06-14 下午5.52.29.png Dockerfile配置

FROM node:14
# Create app directory
WORKDIR /usr/src/app
COPY package.json ./
COPY yarn.lock ./
RUN yarn install
COPY . .
EXPOSE 3000
CMD [ "yarn", "start" ]

.dockerignore配置

node_modules
*.log

参考

构建镜像

docker build . -t yourUsername/node-web-app docker run -p 49160:8080 -d yourUsername/node-web-app 浏览器输入3000查看是否成功,如果失败docker logs id 查看

host是一个坑,要注意

截屏2021-06-16 上午12.10.10.png

购买服务器

阿里云自行选择服务器

  1. 创建实例

  2. ssh root@公网ip //登陆服务器

  3. ssh-copy-id root@公网ip //上传私钥

  4. 修改host文件

    公网ip 变量名 //下次登陆就很简单啦,直接使用ssh root@变量名 就好啦 复制代码

  5. 创建单独的user

    ssh root@id//登陆
    adduser username//添加用户
    su - username//切换
    ssh username@id//使用用户登陆
    ssh-copy-id username@id

6.安装docker
docker官网 7.user添加分组 (解决非root用户无法运行docker)

cat /etc/group 查看分组
usermod -a -G docker username 

8. 安装node && yarn (用来打包)

    curl -fsSL https://deb.nodesource.com/setup_12.x | sudo -E bash -
    sudo apt-get install -y nodejs
    sudo apt-get install gcc g++ make
    curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | sudo tee /usr/share/keyrings/yarnkey.gpg >/dev/null
    
    echo "deb [signed-by=/usr/share/keyrings/yarnkey.gpg] https://dl.yarnpkg.com/debian stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
    
    sudo apt-get update && sudo apt-get install yarn
  1. 服务器内部docker化
    touch .env.local// 添加local文件,因为项目需要,不需要可不做
    //安装依赖性
    yarn
    //打包代码
    yarn build
    //创建文件夹
    mkdir blog-data
    //开启数据库
    docker run --network=host -v /home/xiong/blog-data:/var/lib/postgresql/data -p 5432:5432 -e POSTGRES_USER=blog -e POSTGRES_HOST_AUTH_METHOD=trust -d postgres:12.2
    yarn typeorm:build //打包typerom
    yarn migration:run//创建表
    
    //生成镜像
    docker build -t aslanxiong/node-web-app .
    //运行镜像
    docker run --network=host -p 3000:3000 -d aslanxiong/node-web-app
  1. 阿里云安全策略组添加要访问的端口

使用部署脚本

添加bin/deploy.sh 文件
chmod +x bin/deploy.sh //添加可执行权限
ssh xiong@nextjs "sh /home/。。。" //执行服务器的脚本
或者
ssh xiong@nextjs 'bash -s' < bin/deploy.sh//执行本地的脚本

11. 使用nginx

docker run --name nginxNextjs -d nginx:1.19.1

docker run --name nginxNextjs --network=host -v /home/xiong/app/nginx.conf:/etc/nginx/conf.d/default.conf -v /home/xiong/app/.next/static/:/home/xiong/nginx/html/_next/static/ -d nginx:1.19.1