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;
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) => {
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>
</>
);
};
静态文件
cssModul
- 如果想使用cssModul,那么在声明css文件的时候,文件名应为xxx.module.css
- 默认支持Sass,也支持xxx.module.sass,不过用之前要自己手动npm下,它内置编译但没有内置node包
- next.js会自动识别
.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__,里边就是服务器返回来的数据
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;
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
- 直接访问页面的话,该方法在服务端执行
- 由其他页面切换过来的话,会在加载页面前自动调接口
...
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
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 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进行事件的注册
class LayoutApp extends App {
constructor(props) {
....
this.state = { loading: false };
}
.....
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>}
</>
)
}
}
headers丢失问题
- 使用next可能会出现一些问题,如headers中的数据明明已经传递过去,但感觉没生效
- 最常见的场景就是以cookie作为登录态,传递过去,但判断为没有登录
- 原因:
- 我们的请求从前端发出的时候,携带了cookie,它是请求的
next服务器
- 然后next服务器会去请求
接口服务器,但是它并没有透传,所以接口服务器判断为没有登录
- 为什么我们在前端直接请求会生效?
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 {
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();
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);
}
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;