React同构,从零学习Next.js,服务端渲染SSR

1,157 阅读10分钟

一、Next.js的介绍

1.1、使用React同构构建完整的web项目应用

1.1.1、 React同构设计的技术点

  • css方案、路由方案、数据获取方案。。。
  • 使用打包工具(如webpack,rollup等等)将代码进行打包
  • 使用诸如Babel之类的编译器进行代码装换
  • 生产环境代码优化
  • 静态预先渲染、服务端渲染、客服端渲染

2.2 Next.s 与 Create React App Cli有什么异同

Next.js vs Create React App

  • 有了Create React App, 为什么有出现了Next.js ?
  • Create React App 与 next.js有什么不同

2.2.1 相同点

  • 勇于快速构建React应用程序
  • 体积轻薄
  • 自动的代码拆分
  • 开箱即用(零配置),同事支持自定义
  • 支持所有现代浏览器环境
  • 良好的开发体验,广泛应用于生产

2.2.2 不同点

Create React App

  • 构建单页面应用(SPA)
  • 需要自行引入路由方案
  • 无法处理任何后端逻辑

Next.js

  • 基于Node构建静态/服务端渲染应用
  • 开箱即用的样式&路由方案
  • 新加入的API Routes特性,提供了一种构建PI的解决方案

2、Next.js功能与特性

2.2.1、next.js功能

  • 路由: 基于“pages”的静态/动态路由
  • 数据获取: 正对不同场景的数据获取API
  • css支持: 全局样式、组件级样式、CSS-in-JS
  • 静态文件服务: 提供图片、文本、甚至html等静态文件支持
  • TypeScript: 集成类似IDE的TS体验,开箱即用
  • 环境变量: 内置对于环境变量的支持,并能导出至浏览器环境

2.2.2、next.js路由

  • 静态路由: ‘pages/index.js’ -> '/'
  • 嵌套静态路由: ‘pages/a/b.js’ -> '/a/b'
  • 动态路由: ‘pages/books/[id].js’ -> '/books/1', '/books/2'...
  • 动态全匹配路由: ‘pages/books/[...all].js’ -> '/books/1/1','/books/1/2'...

路由匹配的优先级是: 静态路由 > 嵌套静态路由 > 动态路由 > 动态全匹配路由

2.2.3 数据获取

预渲染

  • 场景: 静态生成(请求前就知道的页面) &服务端渲染(请求时才知道的页面) 对应使用以下不同API
  • API: getStaticProps、getServerSideProps、getStaticPaths、getInitailProps

2.2.4 css支持

  • 全局样式
  • 组件级样式
  • Sass
  • Less & Stylus: @zeit/next-less & @zeit/next-stylus
  • CSS-in-JS: styled-jsx

2.2.5 静态文件服务

  • 基于/public, 此路径下的静态文件,通过‘/',路径访问

2.2.6 TypeScript

  • Next.js提供开箱即用的TS支持
  • 创建tsconfig.json, Next为你提供默认的配置
  • 此外,可扩展自定义配置

2.2.7 环境变量

  • ‘.env.local’ -> 'process.env'
  • 浏览器端获取: 变量前追加‘NEXT_PUBLIC_’

3、Next.js与周边生态

  • 状态管理: 与redux、Mobx等高校接入
  • 数据请求: 与GraphQL等工具配合使用
  • 多端开发:与Elextron等框架的完美配合
  • 静态站点: 与Agility CMS、 Wordpress等静态站框架协同助力

二、Next.js项目搭建

2.1 、如何初始化Next.js项目

2.1.1 常见方式总览

  • 手动: 从零开始引入依赖进行构建
  • 脚手架: 使用create-next-app(甚至可以借助模板)

2.1.2 手动

# 创建项目目录
mkdir nextjs_demo
# 进入项目目录
cd nextjs_demo
# 根路径下创建package.json文件
npm init -y
# 安装依赖
npm install --save next react react-dom

项目的结构

image.png

接下来在package.json中手动添加3个脚本


  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "next",
    "build": "next build",
    "start": "next start"
  },

然后创建pages的目录, 接着创建index.js, 创建第一个react组价

import React from 'react'

const Home = () => (
  <main>
    <h1>Hello Next.js</h1>
  </main>
)

export default Home

yarn dev 启动项目,可以看见,已经是3000端口启动成功了

image.png

image.png

2.1.3 自动, create-next-app 脚手架创建项目

next.js 官网: nextjs.org/learn/basic…

脚手架 + 模板创建

npx create-next-app nextjs-demo2 --use-npm --example "https://github.com/vercel/next-learn/tree/master/basics/learn-starter"

image.png

image.png

2.1.4 自动, create-next-app 脚手架

# 这个的项目目录结构最完整
 npx create-next-app
 
 pages下:
 页面/API接口
 publick下:
 静态文件
 next.config.js
 自定义配置

image.png

三、Next.js路由

  • Next.js 提供的两种预渲染形式
  • 基于文件系统的静态/动态路由
  • next/link和next/router的使用
  • Shallow Routing

3.1 Next.js 预渲染

nextjs提供: 静态生产 & 服务端渲染

3.1.1 静态生成(Staic Generation)

  • 页面生产时机: 构建时(build time )
  • 优势: CDN缓存
  • 数据: 不包含数据

包含数据(getStaicPaths + getStaticProps)

  • 场景
  • 营销页面
  • 博客和文章
  • 帮助和文档

3.1.2 服务端渲染(Server-side Rendering)

  • 页面生成时机: 请求时(request time)
  • 优势: 驳斥数据更新(每次访问都是最新的数据)
  • 数据:getServerSideProps
  • 场景: 因请求不同而数据不同的页面

3.1.3 静态路由与动态路由

  • 静态路由: 我们在pages/book/index.js, 创建一个新页面
pages/inex.js => '/'
pages/book/index.js => '/book'

image.png

新页面创建好了,路由会根据目录的名称,自动屁屁额

image.png

嵌套路由 我们写个深点的路径,也是可以识别到

image.png

image.png

  • 动态路由: 我们需要通过url传一些参数(params)的时候,就需要动态路由了
pages/book/[id].js -> '/book/id' (/book/123)
pages/[book]/price.js -> /:book/price (/helloWorld/price)
pages/book/[id]/[detail].js -> '/book/:id/:detail' (/book/123/a-detail)

最后的参数

pages/book/[id].js -> '/book/id' (/book/123)

截屏2022-02-27 下午4.00.50.png

/book/321,任意:id都会匹配book/[id].js, 而不是去找index.js image.png

目录是动态的

pages/[book]/price.js -> /:book/price (/helloWorld/price) 截屏2022-02-27 下午4.08.23.png 任意的目录都能够匹配动态路由,除了book,因为有个固定的book目录,动态的【book】优先级会低于动态的book

动态目录 和 动态文件

截屏2022-02-27 下午4.23.24.png

image.png

总结: 带【】的目录和js都理解为模糊匹配就好了

全匹配路由

pages/course/[...params].js-》 ‘/course/"params’
(匹配: /course/123 /course/detail/123 /course/123/chapter/1 )

[...params]不管后面多少级,都可以匹配到 截屏2022-02-27 下午4.31.42.png

image.png

3.1.4 路由匹配优先级?

  • 静态路由 > 动态路由
  • 动态路由 > 全匹配路由

总结: 越模糊的优先级越低

3.1.5 Link 路由跳转

import Link from 'next/link'

export default function Home() {
  return (
    <main>
      <h1>首页</h1>
      <ul>
        <li>
          <Link href={'/book/newBook/goodBook.js'}>
            <a>/book/newBook/goodBook.js</a>
          </Link>
        </li>
        <li>
          <Link href={'/book'}>
            <a>bookjs</a>
          </Link>
        </li>
      </ul>
    </main>
  )
}

z6bn2-7wj6v.gif

3.1.6 动态路由跳转

[if] 占位表示需要模糊匹配的占位符, as是真正跳的属性

<li>
          <Link href={'/book/[if]'} as={'/book/123'}>
            <a>/book/[id].js</a>
          </Link>
        </li>

3.1.7 自定义路由跳转组件

在componetns下新建一个CustomeLink组件

import React from 'react'

const CustomeLink = React.forwardRef(({ onClick, href }, ref) => {
  return (
    <a href={href} ref={ref} style={{ color: 'red' }}>
      自定义函数子组件: /book/one
    </a>
  )
})

export default CustomeLink

通过forwardRef构建,href和ref向下传递,在调用组件时,在组件上写passHref传递点击的事件和属性

<li>
          <Link href={'/book/[if]'} as={'/book/123'} passHref>
            <CustomeLink></CustomeLink>
          </Link>
</li>

以上两种动态路由跳转的方式效果如下:

2022-02-27 下午7.07.00.gif

3.1.8 query带参数跳转

 <li>
          <Link href={{ pathname: '/book', query: { year: '2022' } }}>
            <a>URL带参数跳转 /book/newBook/goodBook.js?year=2022</a>
          </Link>
        </li>

image.png

3.1.9 replace不记录路由跳转

<li>
          <Link href={'/book/newBook/goodBook.js'} replace>
            <a>/book/newBook/goodBook.js</a>
          </Link>
        </li>

3.1.9 Link除了a标签,button标签等带Onclick的都可以跳

底层不是像a标签一样通过href去跳,而是,传递onClick

 <Link href={'/book/newBook/goodBook.js'}>
            <button>/book/newBook/goodBook.js</button>
          </Link>

3.2 next/router 非声明式路由

import Router from 'next/router'
import {useRouter} from 'next/router'
import {withRouter} from 'next/router'

3.2.1 Router API

push
replace
back
relaod
prefetch
beforePopState
events
 - routeChangeStart
 - routeChangeComplete
 - routeChangeError
  • push as 动态路由用 Router.push(url,as,options) 在pages下新建routerApi.js页面
import Router from 'next/router'

export default function RouterApi() {
  return (
    <main>
      <h1>RouterApi</h1>
      <ul>
        {/* push */}
        <li>
          <button
            onClick={() => {
              Router.push('/book')
            }}
          >
            /book
          </button>
        </li>
      </ul>
    </main>
  )
}

可以正常跳转

push动态路由

 <li>
          {/* 动态 */}
          <button
            onClick={() => {
              Router.push('/book/[id]', '/book/123')
            }}
          >
            /book/[id]
          </button>
        </li>
        {/* 动态 带参数 */}
        <li>
          <button
            onClick={() => {
              Router.push({ pathname: '/book/456', query: { userId: 666 } })
            }}
          >
            /bookid 带参数
          </button>
        </li>
// 不记录路由替换
<button onClick={() => Router.replace('/book')}replace /book</button>
// 返回
<button onClick={() => Router.back()}replace /book</button>
// 重新加载
<button onClick={() => Router.replace('/book')}replace /book</button>

preFetch

  • 此方法只有在没有Link组件做路由跳转时生效(Link组件默认完成prefetch)
  • 此方法值在生产环境有效 预加载链接,增强体验,也加重了服务器压力
import Router from 'next/router'
Router.prefetch(url,as)

3.2.2 beforePopState

路由popup事件前被调用

Router.beforePopState(cb: () => boolean)

// 返回不是’/book‘页面阻止逻辑

import { useEffect } from 'react'

export default function RouterApi() {
  // beforepopState

  useEffect(() => {
    Router.beforePopState((url) => {
      if (url !== 'book') {
        alert('url not allowed')
        return false
      }
      return true
    })
  }, [])

2022-02-27 下午7.53.47.gif

此外还有一些关于路由的事件钩子可以用

  • routeChangeStart(url): 路由开始改变时触发

  • routeCHnageComplete(url): 路由变化完成时触发

  • routeChangeError(err,url): 路由跳转发生错误或被取消时触发

  • beforeHistoryChange(url): 路由变化前触发

  • hashChangeStart(url): 页面不变,哈希变化时触发

  • hashChangeComplete(url): 页面不变,哈希辩护完成触发

  • 例如第一个时间

// router events
  useEffect(() => {
    const handleRouterChange = (url) => {
      alert(`App is change to : ${url}`)
    }
    Router.events.on('routeChangeStart', handleRouterChange)

    return () => {
      Router.events.off('routeChangeStart', handleRouterChange)
    }
  }, [])

2022-02-27 下午8.06.24.gif

注意需要在useEffect或didMounted中使用

3.2.3 useRouter & withRouter

用法一样,获取的方式写法不同

# useRouter 
import {useRouter} from 'next/router'
const router = useRouter()
# withRouter
import {withRouter} from 'next/router'

const Home = ({router}) = > {
     // todo
 }
 
 export default withRouter(Home)

3.2.4 Shallow Routing

就是改变url时,不触发数据获取方法 只适合同一页面的路由更改,reload这种

Router.push('book',undefined,{shallow: true})

3.3 API路由

3.3.1 API路由的基本概念

可以理解为提供node server 的接口服务

使用Next.js 构建自己的API提供的一种简单的解决方案

  • pages/api 目录下的任何文件都将作为API路由映射到/api/*
api路由
export default (req,res) => {
    ...
}

export default(req,res) => {
res.statusCode = 200
res.setHeader('Content-type','application/json')
res.end(JSON.stringify({course: 123}))
}
// 等价与
export default (req,res) => {
res.status(200).json({course: 123})
}

image.png

image.png

3.2.2 API请求方式

  • GET与POST
export default (req, res) => {
  if (req.method === 'POST') {
    res.status(200).json({ method: 'POST!!' })
  } else if (req.method === 'GET') {
    res.status(200).json({ method: 'GET!!' })
  } else {
    res.status(200).json({ method: 'others' })
  }
}

image.png

export default function getData() {
  const post = () => {
    fetch('http://localhost:3000/api/methods', { method: 'POST' })
      .then((res) => res.json())
      .then((json) => console.log(`post`, json))
  }
  return (
    <main>
      <h1>getData</h1>
      <button onClick={post}> postData</button>
    </main>
  )
}

2022-02-27 下午8.57.57.gif

3.3.3 动态API路由

与动态页面路由遵循相同的文件命名规则

pages/api/post/[xxx].js

创建 pages/api/course/[id].js

export default (req, res) => {
  const {
    query: { id },
  } = req
  res.status(200).json({ id })
}

image.png

  • 🌰:场景
GET api/courses/ 列表(静态)
GET api/courses/123 详情(动态)

GET api/courses/[:id] 仅有动态不会生效

全捕获路由

pages/api/course/[...slug].js

image.png

image.png

3.3.4 API路由优先级

静态API路由优先于动态API路由
动态API路由有限与捕获所有API的路由

3.3.5 API路由使用细节

 req: 
 cookies
 query
 body
 res: 
 status
 json
 send
 redirect([status],path)

四、Next.js 配置

4.1 next.config.js

- 位置 根路径 ./next.config.js (package.json旁边)
- 不编译 不会被Webpack,Babel or TypeScript编译
- js文件 不支持josn,只能用用你的Node版本支持的js特性
- 使用阶段 Next Server端, Build构建过程中,不会在Client端被使用

4.2 配置导出

module.exports = {
    / config.../
}

module.exports = (
    phase,
    {
        defaultConfig
    }) => {
    
        return {
             / config.../
        }
    }
) 

image.png

4.3 环境变量

module.exports = {
    env: {
    customKey: 'my-value'
    
   }

}

// pages/page.js
function Page(){
    return <h1>The value of customKey is: {process.env.customKey}</h1>
}

4.4 BasePath

module.exports = {
    basePath: '/docs'
}

注意会自动补充路由前缀/docs

4.5 assetPrefix

cdn加速用, 不支持static下的

const isProd = process.env.NODE_ENV === 'production'
module.exports = {
    assetPrefix: isPro ? 'https://cdn.mydomain.com' : ''
}

static下用的

image.png

4.6 自定义Webpack Config

image.png

image.png

4.7 distDir

module.exports = {
    distDir: 'build'
}

4.8 Next.js框架内置功能

4.8.1动态import

image.png

支持SSR
代码分割

4.8.2 动态import的loading

image.png

4.8.3 配置不开启SSR

image.png

4.9 浏览器兼容支持

1. >= IE11
2. 现代浏览器基本都兼容(chrome,火狐,苹果)

5.0 JavaScript语言支持

ES6基本都支持


![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b59ead39da0b4d5491d3238475de366d~tplv-k3u1fbpfcp-watermark.image?)

5.1 Polyfills

image.png

5.2 postcss

image.png

5.3 路劲别名alias

image.png

5.4 路径别名paths

image.png

5.5 环境变量

image.png

在client端也有效

image.png

5.6 其他配置

image.png

五、next.js的css支持

5.1 global CSS

  • 作用域 全局作用域,整个HTML文档
  • 引入位置 pages/_app.js
  • 文件后缀: *.css, eg.style.css
  • 库CSS 支持来自node_modules的css文件

image.png

5.2 组件级CSSModule

  • 作用域: 局部作用域
  • 引入位置 【name].module.css

image.png

5.3 预处理器Sass

  • 引入位置: 任何位置
  • 全局作用域 *.scss, *.sass
  • 局部作用域 *.module.scss, *.module.sass
npm installl sass
# next.config.js
const path = require('path')
module.exports = {
    sassOptions: {
        includePaths: [
            path.join(__dirname, 'styles')
        ]
    }
}

5.4 less & stylus

依赖插件
less: @zeit/next-less
stylus @zeit/next-stylus
后缀
.less
.stylus

5.5 styled-in-js

image.png

5.6 styled-jsx

image.png

5.7 postCss

image.png

image.png

六、 Next.js的根组件 & Document组件

项目目录

image.png

6.1 _app.js 是最高层级的组件

// 全局的css
import '../styles/globals.css'

// Component 当前路由组件
// pageProps 含有已请求数据的对象
function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />
}

export default MyApp

6.2 index.js 是根路由 / 页面

 <h1>hello next</h1>
  • 🌰:实现header - container -footer的经典布局
# components/Layout.js
import s from './Layout.module.css'

export default function Layout({ children }) {
  return (
    <main className={s.container}>
      <header>head</header>
      <section>{children}</section>
      <footer>footer</footer>
    </main>
  )
}
# components/Layout.module.css
.container {
  height: 100vh;
  width: 100vw;
  display: flex;
  flex-direction: column;
}

.container header {
  height: 100px;
  background: lightskyblue;
}

.container footer {
  height: 100px;
  background: lightpink;
}

.container section {
  flex: 1;
}

# 修改_app.js
// 全局的css
import '../styles/globals.css'
import Layout from '../components/Layout'

// Component 当前路由组件
// pageProps 含有已请求数据的对象
function MyApp({ Component, pageProps }) {
  return (
    <Layout>
      <Component {...pageProps} />
    </Layout>
  )
}

export default MyApp

  • 效果

image.png