Next.js详细教程(下)

简介: 本文适合对Next感兴趣的小伙伴阅读。

六、路由系统


   Nextjs默认匹配pages目录的index.js作为根路径/,其他的路径也是这样按文件名匹配的。


路由跳转

  Nextjs官方推荐了两种跳转方式,一种是Link组件包裹,一种使用Router。Link的原理也是用Router实现的,Link用起来总感觉很冗余,个人推荐使用Router。


  Nextjs提供了一个'next/router'的包,专门用来处理路由。Router便是其中一个对象,Router.push('url')进行跳转。


简单Demo:

import React from 'react'     
import Router from 'next/router'
export default () => {
 return(
    <>
      <button onClick={()=>Router.push('/demo')} >前往demo页</button>
      <div>这里是首页</div>
    </>
  )
}


路由传参

 Nextjs使用query传参数!


官方例子:

import { useRouter } from 'next/router'
export default function ReadMore({ post }) {
  const router = useRouter()
  return (
    <button
      type="button"
      onClick={() => {
        router.push({
          pathname: '/post/[pid]',
          query: { pid: post.id },
        })
      }}
    >
      Click here to read more
    </button>
  )
}


接收参数的时候使用props.router.query.pid


6个路由钩子

// routeChangeStart     history模式路由改变刚开始  
// routeChangeComplete  history模式路由改变结束
// routeChangeError     路由改变失败
// hashChangeStart      hash模式路由改变刚开始
// beforeHistoryChange  在routerChangeComplete之前执行
// hashChangeComplete   hash模式路由改变结束


来个Demo看看:

import React from 'react'
import Link from 'next/link'
import Router from 'next/router'  
const Home = () => {
    /**6个钩子事件
    routeChangeStart
    routerChangeComplete
    beforeHistoryChange
    routeChangeError
    hashChangeStart
    hashChangeComplete*/
    //路由开始变化
    Router.events.on('routeChangeStart',(...args)=>{
        console.log('1.routeChangeStart->路由开始变化,参数为:',...args)
    })
    //路由变化结束
    Router.events.on('routeChangeComplete',(...args)=>{
        console.log('2.routeChangeComplete->路由变化结束,参数为:',...args)
    })
    //Next.js全部都用History模式
    Router.events.on('beforeHistoryChange',(...args)=>{
        console.log('3.beforeHistoryChange,参数为:',...args)
    })
    //路由发生错误时,404不算
    Router.events.on('routeChangeError',(...args)=>{
        console.log('4.routeChangeError->路由发生错误,参数为:',...args)
    })
    //Hash路由切换之前
    Router.events.on('hashChangeStart',(...args)=>{
        console.log('5.hashChangeStart,参数为:',...args)
    })
    //Hash路由切换完成
    Router.events.on('hashChangeComplete',(...args)=>{
        console.log('6.hashChangeComplete,参数为:',...args)
    })
    function gotoSport(){
        Router.push({
            pathname:'/sport',
            query:{name:'前端早茶'}
        })
        // 同以下:
        // Router.push('/sport?前端早茶')
    }
    return (
        <>
            <div>调试下6个钩子</div>
            <div>
                <Link href={{pathname:'/sport',query:{name:'前端早茶'}}}><a>选择前端早茶</a></Link>
                <br/>
                <Link href="/sport?name=广东靓仔"><a>选择广东靓仔</a></Link>
            </div>
            <div>
                <button onClick={gotoSport}>选前端早茶</button>
            </div>
            <!-- 这里没有设置锚点,因此不会有跳转效果 -->
            <div>
                <Link href='/#juan'><a>选Juan</a></Link>
            </div>
        </>
    )
}


七、状态管理


Token存储

SSR之间只能通过cookie才能在Client和Server之间通信,以往我们在SPA项目中是使用localStorage或者sessionStorage来存储,但是在SSR项目中Server端是拿不到的,因为它是浏览器的属性,要想客户端和服务端同时都能拿到我们可以使用Cookie,所以token信息只能存储到Cookie中。


集成状态管理器

大型项目推荐使用Redux,方便我们维护以及二次开发。


四个步骤

  • 创建store/axios.js文件
  • 修改pages/_app.js文件
  • 创建store/index.js文件
  • 创建store/slice/auth.js文件

核心梳理:pages/_app.js文件使用next-redux-wrapper插件将redux store数据注入到next.js。


import {Provider} from 'react-redux'
import {store, wrapper} from '@/store'
const MyApp = ({Component, pageProps}) => {
  return <Component {...pageProps} />
}
export default wrapper.withRedux(MyApp)


store/index.js文件

使用@reduxjs/toolkit集成reducer并创建store,

使用next-redux-wrapper连接next.js和redux,

使用next-redux-cookie-wrapper注册要共享到cookie的slice信息。


import {configureStore, combineReducers} from '@reduxjs/toolkit';
import {createWrapper} from 'next-redux-wrapper';
import {nextReduxCookieMiddleware, wrapMakeStore} from "next-redux-cookie-wrapper";
import {authSlice} from './slices/auth';
import logger from "redux-logger";
const combinedReducers = combineReducers({
  [authSlice.name]: authSlice.reducer
});
export const store = wrapMakeStore(() => configureStore({
  reducer: combinedReducers,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().prepend(
      nextReduxCookieMiddleware({
        // 在这里设置在客户端和服务器端共享的cookie数据
        subtrees: ["auth.accessToken", "auth.isLogin", "auth.me"], 
        }) 
    ).concat(logger) 
}));
const makeStore = () => store; 
export const wrapper = createWrapper(store, {storeKey: 'key', debug: true});


store/slice/auth.js

import {createAsyncThunk, createSlice} from '@reduxjs/toolkit';
import axios from '../axios';
import qs from "qs";
import {HYDRATE} from 'next-redux-wrapper';
// 获取用户信息
export const fetchUser = createAsyncThunk('auth/me', async (_, thunkAPI) => {
  try {
    const response = await axios.get('/account/me');
    return response.data.name;
  } catch (error) {
    return thunkAPI.rejectWithValue({errorMsg: error.message});
  }
});
// 登录
export const login = createAsyncThunk('auth/login', async (credentials, thunkAPI) => {
  try {
    // 获取token信息
    const response = await axios.post('/auth/oauth/token', qs.stringify(credentials));
    const resdata = response.data;
    if (resdata.access_token) {
      // 获取用户信息
      const refetch = await axios.get('/account/me', {
        headers: {Authorization: `Bearer ${resdata.access_token}`},
      });
      return {
        accessToken: resdata.access_token,
        isLogin: true,
        me: {name: refetch.data.name}
      };
    } else {
      return thunkAPI.rejectWithValue({errorMsg: response.data.message});
    }
  } catch (error) {
    return thunkAPI.rejectWithValue({errorMsg: error.message});
  }
});
// 初始化数据
const internalInitialState = {
  accessToken: null,
  me: null,
  errorMsg: null,
  isLogin: false
};
// reducer
export const authSlice = createSlice({
  name: 'auth',
  initialState: internalInitialState,
  reducers: {
    updateAuth(state, action) {
      state.accessToken = action.payload.accessToken;
      state.me = action.payload.me;
    },
    reset: () => internalInitialState,
  },
  extraReducers: {
    // 水合,拿到服务器端的reducer注入到客户端的reducer,达到数据统一的目的
    [HYDRATE]: (state, action) => {
      console.log('HYDRATE', state, action.payload);
      return Object.assign({}, state, {...action.payload.auth});
    },
    [login.fulfilled]: (state, action) => {
      state.accessToken = action.payload.accessToken;
      state.isLogin = action.payload.isLogin;
      state.me = action.payload.me;
    },
    [login.rejected]: (state, action) => {
      console.log('action=>', action)
      state = Object.assign(Object.assign({}, internalInitialState), {errorMsg: action.payload.errorMsg});
      console.log('state=>', state)
      // throw new Error(action.error.message);
    },
    [fetchUser.rejected]: (state, action) => {
      state = Object.assign(Object.assign({}, internalInitialState), {errorMsg: action.errorMsg});
    },
    [fetchUser.fulfilled]: (state, action) => {
      state.me = action.payload;
    }
  }
});
export const {updateAuth, reset} = authSlice.actions;


Tips:

1、使用了next-redux-wrapper一定要加HYDRATE,目的是同步服务端和客户端reducer数据,否则两个端数据不一致造成冲突

2、注意next-redux-wrapper和next-redux-cookie-wrapper版本


八、旧项目升级Next12


温馨提示:看Nextjs的文档我们最好选择英文版本,中文文档好像很久不更新了


React Server Components

允许我们在服务器上渲染所有内容,包括组件本身。


开启配置:

// next.config.js
module.exports = {
  experimental: {
    concurrentFeatures: true,
    serverComponents: true
  }
}


现在我们可以组件级别进行数据获取,通过使用 React Server 组件,我们可以简化事情。不再需要getServerSidePropsgetStaticProps


我们可以将任何 Next.js 页面重命名为.server.js以创建服务器组件并直接在我们的服务器组件中导入客户端组件。


【温馨提示】广东靓仔从官网截了个图

image.png


我们需要安装React18才能使用哦~

React 18添加了新功能,包括 Suspense、自动批处理更新、API 等startTransition,以及支持React.lazy.


【广东靓仔试用了下,确实方便,不建议在生产项目上使用】


详细内容

官方出了一个 demo :https://github1s.com/vercel/next-rsc-demo/blob/HEAD/pages/ssr.js

demo在线预览地址:https://next-news-rsc.vercel.sh/


目录如下所示:

image.png


以往的SSR

import Page from '../components/page.client'
import Story from '../components/story.client'
import Footer from '../components/footer.client'
// Utils
import fetchData from '../lib/fetch-data'
import { transform } from '../lib/get-item'
export async function getServerSideProps() {
  const storyIds = await fetchData('topstories', 500)
  const data = await Promise.all(
    storyIds
      .slice(0, 30)
      .map((id) => fetchData(`item/${id}`).then(transform))
  )
  return {
    props: {
      data,
    },
  }
}
export default function News({ data }) {
  return (
    <Page>
      {data.map((item, i) => {
        return <Story key={i} {...item} />
      })}
      <Footer />
    </Page>
  )
}

页面添加 getServerSideProps 函数用于 服务端获取数据,每个页面都需要这样编写。


更新后rsc.server.js :

import { Suspense } from 'react'
// Shared Components
import Spinner from '../components/spinner'
// Server Components
import SystemInfo from '../components/server-info.server'
// Client Components
import Page from '../components/page.client'
import Story from '../components/story.client'
import Footer from '../components/footer.client'
// Utils
import fetchData from '../lib/fetch-data'
import { transform } from '../lib/get-item'
import useData from '../lib/use-data'
function StoryWithData({ id }) {
  const data = useData(`s-${id}`, () => fetchData(`item/${id}`).then(transform))
  return <Story {...data} />
}
function NewsWithData() {
  const storyIds = useData('top', () => fetchData('topstories'))
  return (
    <>
      {storyIds.slice(0, 30).map((id) => {
        return (
          <Suspense fallback={<Spinner />} key={id}>
            <StoryWithData id={id} />
          </Suspense>
        )
      })}
    </>
  )
}
export default function News() {
  return (
    <Page>
      <Suspense fallback={<Spinner />}>
        <NewsWithData />
      </Suspense>
      <Footer />
      <SystemInfo />
    </Page>
  )
}

可以看到,我们还是按平时React项目来开发就可以实现SSR了。
最重要的一点,支持 HTTP Streaming,文档还没加载完,页面已经开始渲染了。

详情前往:https://nextjs.org/blog/next-12


九、总结


在我们阅读完官方文档后,我们一定会进行更深层次的学习,比如看下框架底层是如何运行的,以及源码的阅读。    


这里广东靓仔给下一些小建议:

  • 在看源码前,我们先去官方文档复习下框架设计理念、源码分层设计
  • 阅读下框架官方开发人员写的相关文章
  • 借助框架的调用栈来进行源码的阅读,通过这个执行流程,我们就完整的对源码进行了一个初步的了解
  • 接下来再对源码执行过程中涉及的所有函数逻辑梳理一遍
相关文章
|
4月前
|
JavaScript 前端开发 网络协议
​Node.js 教程(一) 基本概念与基本使用
​Node.js 教程(一) 基本概念与基本使用
|
2月前
|
JavaScript
vue中使用 HotKeys.js 教程(按键响应、快捷键开发)
vue中使用 HotKeys.js 教程(按键响应、快捷键开发)
131 0
|
4月前
|
Web App开发 JavaScript 前端开发
《手把手教你》系列技巧篇(三十九)-java+ selenium自动化测试-JavaScript的调用执行-上篇(详解教程)
【5月更文挑战第3天】本文介绍了如何在Web自动化测试中使用JavaScript执行器(JavascriptExecutor)来完成Selenium API无法处理的任务。首先,需要将WebDriver转换为JavascriptExecutor对象,然后通过executeScript方法执行JavaScript代码。示例用法包括设置JS代码字符串并调用executeScript。文章提供了两个实战场景:一是当时间插件限制输入时,用JS去除元素的readonly属性;二是处理需滚动才能显示的元素,利用JS滚动页面。还给出了一个滚动到底部的代码示例,并提供了详细步骤和解释。
65 10
|
4月前
|
JavaScript 前端开发 网络安全
【网络安全 | 信息收集】JS文件信息收集工具LinkFinder安装使用教程
【网络安全 | 信息收集】JS文件信息收集工具LinkFinder安装使用教程
224 4
|
24天前
|
JavaScript NoSQL 前端开发
|
2月前
|
JSON JavaScript 数据格式
vue 绘制波形图 wavesurfer.js (音频/视频) 【实用教程】
vue 绘制波形图 wavesurfer.js (音频/视频) 【实用教程】
278 3
|
2月前
|
JavaScript
vue 农历日期转公历日期(含插件 js-calendar-converter 使用教程)
vue 农历日期转公历日期(含插件 js-calendar-converter 使用教程)
163 0
|
2月前
|
开发框架 监控 JavaScript
企业级node.js开发框架 【egg.js】 实用教程
企业级node.js开发框架 【egg.js】 实用教程
33 0
|
2月前
命令行加载特效 【cli-spinner.js】 实用教程
命令行加载特效 【cli-spinner.js】 实用教程
24 0
|
2月前
|
JavaScript
文件查询匹配神器 【glob.js】 实用教程
文件查询匹配神器 【glob.js】 实用教程
31 0