前言
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
- Next规定在
- 在浏览器中预览
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> </> ) - 注意点:
- Link标签默认不渲染任何东西,所以还要在标签内写上要进行渲染的标签
- Link标签通过
href属性进行跳转 - 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
在实际开发中,通常会在组件初始化后,请求远端数据。如在vue的mounted生命周期中,react的componentDidMount生命周期中发起请求。而在在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; - 注意:
getInitialProps默认传入的参数为一个对象,对象属性如下:
具体说明见githubpathname- URL 的 path 部分query- URL 的 query 部分asPath- 路由映射后,浏览器中显示的路径(包含查询部分)req- HTTP 请求对象 (只有服务器端有)res- HTTP 返回对象 (只有服务器端有)jsonPageRes- 获取数据响应对象 (只有客户端有)err- 渲染过程中的任何错误对象
- 在进行服务端和客户端渲染时都能执行该方法。
- 如果是服务端进行首屏渲染时,会执行该路由组件下的
getInitialProps,并返回相应的props参数,当组件拿到props并渲染好后,服务端才会返回,这时客户端不会再执行一遍该组件的getInitialProps - 当客户端用Link标签或API方法跳转路由组件时,客户端会去执行对应的路由组件的
getInitialProps,此时服务端则不会执行
- 如果是服务端进行首屏渲染时,会执行该路由组件下的
- 只有放在
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.jsconst withCss = require('@Zeit/next-css'); if(typeof require !== 'undefined'){ require.extensions['.css'] = file => {} } module.exports = withCss({}) - 在
publicorstatic目录下新建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- 示例
可以在控制台面板中观察,当点击按钮时,才会加载1.js,即momnet.js的内容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> </> ) }
异步加载组件
- 新建
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作为同时启动next和koa的入口文件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 - 新建