Next.js

766 阅读5分钟

next.js

  • 常用于:(服务端渲染)ssr
  • 操作:数据预取、首屏服务端渲染

如何查看是否为服务端渲染?

  • 谷歌浏览器 command+option+u 打开源代码,搜索react虚拟dom渲染的内容
  • react正常渲染是通过js插入的,也就是说源代码中只有一个空骨架和一段js,页面内容是执行js后插入到root中的

初始化

  • npm init -y
  • npm i axios body-parser connect-mongo cors express express-session koa koa-router mongoose next react react-dom react-redux redux--save
  • 修改package.json的scripts命令:"dev": "next dev"
  • 根目录下新建pages目录
  • 新建index.js
const Home = () => {
  return <div>Home</div>;
};
export default Home;

  • 新建 profile.js
const Profile = () => {
    return <div>Profile</div>
};
export default Profile;
  • 执行命令 npm run dev,看到Home页,进入/profile可看到profile页

特点

约定式路由

  • 根目录下的pages为路由根路径,每个js文件都是一个资源
  • next.js会按照pages下的js文件路径名和文件名自动生成对应路由
  • 会按照页面进行代码分割,执行npm run dev后可以看到生成了一个.next文件夹
  • 动态路由:[id].js,当前就是动态获取id
  • _app.js_document.js是特殊文件

_app.js

  • 默认的入口文件,可以在这里添加公共的内容
  • 当我们创建了_app.js文件后,会发现切换路由不展示对应组件了
    • 需要从props中拿到Component,这个Component就是我们的路由组件
const App = (props) => {
  //类组件一样,只不过是在this.props中
  const { Component: RouteComponent } = props;
  return (
    <div>
      <RouteComponent></RouteComponent>
    </div>
  );
};
export default App;

路由跳转

  • next/link是跳转组件,href为需要跳转地址,会在浏览器页面记录中push一条
  • next/router跳转的方法库,跟react-router一样
import Link from "next/link";
import router from 'next/router';
export default () => {
  return (
    <>
      <Link href="/">首页</Link>
      <button onClick={() => {router.back()}}>返回</button>
    </>
  );
};

静态文件

  • 静态文件会默认找public文件夹

cssModul

  • 如果想使用cssModul,那么在声明css文件的时候,文件名应为xxx.module.css
  • 默认支持Sass,也支持xxx.module.sass,不过用之前要自己手动npm下,它内置编译但没有内置node包
  • next.js会自动识别
/* _app.module.css */
.logo {
    width: 120px;
    height: 31px;
    float: left;
}
//例如:
import styles from './_app.module.css';

export default ()=>{
    return <img src="/images/logo.png" className={styles.logo}/>
}

数据预取

旧方法 getInitialProps

  • 给组件添加静态属性getInitialProps,是一个函数,函数内请求数据
  • 在_app.js的总入口组件中添加静态属性getInitialProps,是一个函数,这个方法会返回一个对象,里面是请求回来的数据
  • 在渲染路由组件时,通过props拿到pageProps,然后传递给路由组件即可
  • 我们打开网页源代码可以看到有一个script标签(返回来的数据是一坨,可以cv一下放html里格式化下),id为__NEXT_DATA__,里边就是服务器返回来的数据
// list.js
import Link from "next/link";
import Layout from "./index";

const UserList = (props) => {
  return (
    <Layout>
      <ul>
        {props.list.map((user) => (
          <li key={user.id}>
            <Link href={`/user/detail/${user.id}`}>{user.name}</Link>
          </li>
        ))}
      </ul>
    </Layout>
  );
};
UserList.getInitialProps = async () => {
  return {
    list: [
      { id: 1, name: "姓名1" },
      { id: 2, name: "姓名2" },
    ],
  };
};
export default UserList;


//_app.js
import App from "next/app";
class LayoutApp extends App {
  static async getInitialProps({ Component, ctx }) {
    let pageProps = {};
    if (Component.getInitialProps) {
      pageProps = await Component.getInitialProps(ctx);
    }
    return {
      pageProps,
    };
  }
  render() {
    const { Component, pageProps } = this.props;

    return (
      <>
        <Component {...pageProps}></Component>
      </>
    );
  }
}

export default LayoutApp;

推荐方法getServerSideProps

  • 直接访问页面的话,该方法在服务端执行
  • 由其他页面切换过来的话,会在加载页面前自动调接口
//将UserList.getInitialProps删掉,加上这个就可以了
...
export async function getServerSideProps() {
  const response = await request.get("http://localhost:5000/api/users");
  return {
    props: {
      list: response.data.data,
    },
  };
}
...

seo

  • 使用spa有个缺陷,如果只有一个入口的话,那么title与meta信息是一开始定好的,虽然可以js操作,但有些引擎爬不到,所以最好在页面返回时带上
  • next/head模块是一个组件,内部可传递title与meta
    • 多个title的话,后面的会覆盖前面的
import Head from "next/head";
const Home = () => {
  return (
    <>
      <Head>
        <title>我是首页</title>
      </Head>
      <div>Home</div>
    </>
  );
};
export default Home;

_document.js

  • 自定义模板,NextScript可以理解为是_app.js以及路由文件
  • 如果有_document.js的话,默认会以_document.js为入口
import Document, { Html, Head, Main, NextScript } from "next/document";
class MyDocument extends Document {
  render() {
    return (
      <Html>
        <Head>
          <style>{`*{padding:0;margin:0;}`}</style>
        </Head>
        <body>
            <Main />
            <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument

懒加载

  • next/dynamic模块默认返回一个函数,函数接收一个callback,callback内部可导入模块,实现模块懒加载
  • 默认不加载,满足条件后,加载js渲染组件
import React from "react";
import Layout from "../index";
import request from "@/utils/request";
// import UserInfo from "@/components/UserInfo";
import dynamic from 'next/dynamic';
const DynamicUserInfo = dynamic(()=>import('@/components/UserInfo'));
const UserDetail = (props) => {
  const [show, setShow] = React.useState();
  return (
    <Layout>
      <p>Id:{props.user && props.user.name}</p>
      <button
        onClick={() => {
          setShow(!show);
        }}
      >
        {show ? "隐藏" : "显示"}更多信息
      </button>
      {show && props.user && <DynamicUserInfo user={props.user} />}
    </Layout>
  );
};
UserDetail.getInitialProps = async (ctx) => {
  let response = await request.get(`/api/users/${ctx.query.id}`);
  return {
    user: response.data.data,
  };
};
export default UserDetail;

  • 默认的懒加载
import React from "react";
const UserInfo = (props) => {
  const [createdAt, setCreatedAt] = React.useState(props.user.createdAt);
  const changeTime = async () => {
    const moment = await import("moment");
    setCreatedAt(moment.default(createdAt).fromNow());
  };
  return (
    <div>
      <p>name:{props.user.name}</p>
      <p>ID:{props.user.id}</p>
      <p>日期:{createdAt}</p>
      <button onClick={changeTime}>切换为相对时间</button>
    </div>
  );
};
export default UserInfo;


如果使用多次异步加载模块,是否会导致重复加载

  • 不会,第一次会调用懒加载,后续为直接调用

路由切换钩子(导航守卫)

  • 可以监听路由的切换,在跳转前做一些事情,跳转后做一些事情
  • 通过router.events.on进行事件的注册
//_app.js
class LayoutApp extends App {
  constructor(props) {
    ....
    this.state = { loading: false };//添加loading
  }
  .....
  routeChangeStart = () => {
    this.setState({ loading: true });
  };
  routeChangeComplete = () => {
    this.setState({ loading: false });
  };
  componentDidMount() {
    router.events.on("routeChangeStart", this.routeChangeStart);//跳转前
    router.events.on("routeChangeComplete", this.routeChangeComplete);//跳转后
  }
  componentWillUnmount() {
    router.events.off("routeChangeStart", this.routeChangeStart);//解绑
    router.events.off("routeChangeComplete", this.routeChangeComplete);//解绑
  }
  render() {
    return (
      <>
      {this.state.loading?<div>...loading</div>:<Component {...pageProps}></Component>}
      </>
      )
  }
}
  • 先走_app.js下的

headers丢失问题

  • 使用next可能会出现一些问题,如headers中的数据明明已经传递过去,但感觉没生效
  • 最常见的场景就是以cookie作为登录态,传递过去,但判断为没有登录
  • 原因:
    • 我们的请求从前端发出的时候,携带了cookie,它是请求的next服务器
    • 然后next服务器会去请求接口服务器,但是它并没有透传,所以接口服务器判断为没有登录
  • 为什么我们在前端直接请求会生效?
    • 因为是直接请求的接口服务器
  • 解决办法:在服务端请求的时候判断下
    • 这块会涉及到next的一些函数走向
//_app.js  当前引入了redux来存储公有数据
//   /api/validate 为验证是否登录接口,成功会返回{success:true,data:用户信息}
import App from "next/app";
import Link from "next/link";
import styles from "./_app.module.css";
import { Provider } from "react-redux";
import "../styles/global.css";
import createStore from "../store";
import request from "@/utils/request";
import { SET_USER_INFO } from "store/action-types";
function getStore(initialState) {
  //如果是服务器环境,每次都新建仓库
  if (typeof window === "undefined") {
    return createStore(initialState);
  } else {
    //客户端会多次执行getInitialProps,但不需要每次都创建仓库,所以判断下
    if (!window._REDUX_STORE) {
      window._REDUX_STORE = createStore(initialState);
    }
    return window._REDUX_STORE;
  }
}
class LayoutApp extends App {
  constructor(props) {
    super(props);
    //同步服务端的仓库到本地
    this.store = getStore(props.initialState);
  }
  static async getInitialProps({ Component, ctx }) {
    //页面更新会重新执行
    let pageProps = {};
    const store = getStore();
    //这块是应对header丢失问题的,如登录态cookie丢失
    //因为我们的请求是打到next服务器3000上,这个时候是带着cookie的,但next没有透传给api服务器,所以需要处理下
    if (typeof window === "undefined") {
      const options = { url: "/api/validate" };
      if (ctx.req && ctx.req.headers.cookie) {
        options.headers = options.headers || {};
        options.headers.cookie = ctx.req.headers.cookie;
      }
      const response = await request(options).then((res) => res.data);
      if (response.success) {
        store.dispatch({ type: SET_USER_INFO, payload: response.data });
      }
    }
    let props = {};
    if (Component.getInitialProps) {
      //数据
      props.pageProps = await Component.getInitialProps(ctx);
    }
    //将服务端的仓库同步给客户端,客户端从props中拿到值,当做客户端的初始值
    if (typeof window === "undefined") {
      props.initialState = store.getState();
    }
    return props;
  }
  render() {
    const { Component, pageProps } = this.props;
    const state = this.store.getState();
    return (
      <Provider store={this.store}>
        <style jsx>
          {`
            li {
              display: inline-block;
              margin-left: 10px;
              line-height: 31px;
            }
          `}
        </style>
        <header>
          <img src="/images/logo.png" className={styles.logo} />
          <ul>
            <li>
              <Link href="/">首页</Link>
            </li>
            <li>
              <Link href="/user">用户管理</Link>
            </li>
            <li>
              <Link href="/profile">个人中心</Link>
            </li>
            <li>
              {state.currentUser ? (
                <span>{state.currentUser.name}</span>
              ) : (
                <Link href="/login">登录</Link>
              )}
            </li>
          </ul>
        </header>
        <Component {...pageProps}></Component>
        <footer style={{ textAlign: "center" }}>底部栏</footer>
      </Provider>
    );
  }
}

export default LayoutApp;