目录
-
next.js介绍
-
Hello World —— next.js
-
自定义共享组件
-
创建动态页面
-
动态路由及数据获取
-
路由的钩子函数
-
懒加载
-
CSS-in-JS:
style-jsx
-
外部CSS加载
-
项目部署
next.js介绍
next.js是一个React框架,可以用于构建服务端渲染APP,渐进式web APP(PWA),导出静态页面,SEO友好等特点;
- 服务端渲染
- 静态导出
- css-in-js:支持CSS-in-JS,同时支持使用
style jsx
语法; - 构建方便:
next.js
自动实现代码分割、基于文件系统的路由、热重载等; - 强大的扩展:支持
babel
、webpack
,自定义的路由、next插件等; - 打包、部署方便;
Hello World —— next.js
-
使用以下命令初始化项目,生成
package.json
文件npm init
-
使用以下命令安装依赖
npm install --save react react-dom next
-
添加一下脚本命令到
package.json
:"scripts": { "start": "next start", "dev": "next dev", "build": "next build", }
-
在项目根目录下新建目录
public
、pages
和components
;
public
:静态文件
pages
:页面文件
components
:自定义组件在
pages
目录下新建文件index.js
,编写以下代码:const Index = () => ( <div>Hello World! next.js.</div> ) export default Index;
-
打开命令行工具,输入以下命令运行项目:
npm run dev
项目会启动一个在
localhost:3000
启动一个服务
至此,我们的第一个项目就做好了
由于next.js
会根据文件系统自动构建路由,默认的的根目录是pages
,所以启动项目后会自动路由到根目录下的index.js
文件。直接访问localhost:3000/
即可访问到项目。
页面间导航
此节我们构建两个页面,并利用next.js
路由在两个页面之间相互跳转。
首先,需要引入next/link
用于页面间的导航,引入方式如下
import Link from 'next/link'
使用方式:link需要接收一个href属性,用于指定导航的目的页面,该地址是根据文件系统中子页面文件相对于index.js
文件定义的;Link
内部仅可以有一个子元素a
,用于包裹页面导航锚点;
<Link href="/about">
<a>Go to About</a>
</Link>
具体代码如下:
// pages/index.js
import Link from 'next/link'
export default () => (
<>
<h1>Hello, next.js</h1>
<Link href="/about">
<a>Go to About</a>
</Link>
</>
)
// pages/about.js
import Link from "next/link";
export default () => (
<>
<h1>This is About Page.</h1>
<Link href="/">
<a>Go Home</a>
</Link>
</>
);
tips
如果需要构建多级路径,可以通过在pages
文件夹下新建文件夹的方式实现。如我们可以在新建文件pages/blog/article.js
,具体内容如下:
export default () => <p>This is an article.</p>;
然后修改pages/about.js
:
import Link from "next/link";
export default () => (
<>
<h1>This is About Page.</h1>
<Link href="/">
<a>Go Home</a>
</Link>
<div>
<Link href="/blog/article">
<a>Go to Article</a>
</Link>
</div>
</>
);
此时通过localhost:3000/blog/article
可以访问到新页面
自定义共享组件
自定义共享组件并不是next.js
的新特性,而是一种通用的编程思维。通过自定义共享组件,我们可以通过组件化提高代码复用率,减少重复代码,提高编程效率;
我们对上一节的demo进行改编,定义一个公用的Header
组件,用于三个页面头部;
// components/Header.js
const headerStyle = {
paddingLeft: "10px",
color: "green"
};
const headerCss = {
color: "orange",
background: "#bfbfbf",
fontSize: "40px"
};
const Header = props => (
<div style={headerStyle}>
<header style={headerCss}>this is header</header>
{props.children}
</div>
);
export default Header;
同时在pages/index.js
中引入Header
组件
import Link from "next/link";
import Header from "../components/Header";
export default () => (
<>
<Header>
<h1>Hello, next.js</h1>
</Header>
<Link href="/about">
<a>Go to About</a>
</Link>
</>
);
这样我们就构建了自己的可复用的公共组件
demo参考codesandbox.io
创建动态页面
所谓的创建动态页面的通用思路就是:在URL中通过查询字符串的方式拼接参数,然后在页面中根据获取的不同参数展现不同的页面;
需要构建的页面结构如下
Blogs
- React基础
- React Router
- Redux
首先,我们构建一个组件,通过传入props传入title并在组件中渲染为列表项,列表项的跳转仍然通过Link
实现,href属性通过查询字符串的方式传入;
接收参数的子页面中,需要引入路由import {withRouter} from 'next/router
;
这时我们可以通过withRouter
获取其router
对象属性,通过router.query.title
可以获取传入的查询字符串;
此时组件导出格式为export default withRouter(Blog)
;
demo参考codesandbox.io
Talk is cheap, show me the code. 具体代码实现如下
// pages/index.js
import Link from "next/link";
const BlogLink = props => (
<li>
<Link href={`/blog?title=${props.title}`}>
<a>{props.title}</a>
</Link>
</li>
);
const Index = () => (
<>
<h1>Blogs</h1>
<ul>
<BlogLink title="React基础" />
<BlogLink title="React Router" />
<BlogLink title="Redux" />
</ul>
</>
);
export default Index;
// pages/blog.js
import { withRouter } from "next/router";
const Page = ({ router }) => {
return <div>{`blog page name: ${router.query.title}`}</div>;
};
export default withRouter(Page);
动态路由及数据获取
在上一节,我们介绍了如何通过动态传参进行动态页面渲染。如果我们访问上节的demo中的React Router子页面,此时页面的URL为https://06vfp.sse.codesandbox.io/blog?title=React%20Router
,这样的URL看起来又长又乱。
本节中,我们希望通过动态路由将上述URL改为类似以下的形式:
https://06vfp.sse.codesandbox.io/blog/React-Router
这里首先介绍一个用于查询TV show的API接口:
我们使用通过关键字查询可以获取节目列表的API:
获取到节目id后,我们在子页面通过节目id获取节目的更多详细信息,具体接口如下:
首先编写pages/index.js
文件
import Link from "next/link";
import fetch from "isomorphic-fetch";
const liStyle = {
marginBottom: "15px"
};
const Index = props => (
<>
<h1>TV Shows - Batman</h1>
{props.shows.map(show => (
<li key={show.id} style={liStyle}>
<Link href="/tv/[id]" as={`/tv/${show.id}`}>
<a>{show.name}</a>
</Link>
</li>
))}
</>
);
Index.getInitialProps = async () => {
const res = await fetch("https://api.tvmaze.com/search/shows?q=batman");
const data = await res.json();
return {
shows: data.map(item => item.show)
};
};
export default Index;
next.js
提供了一个钩子函数,用于动态获取页面的初始化数据;使用方法是
ComponentInstance.getInitialProps async () => {
...
return dataObj; // 返回格式为对象
}
然后在componentInstance
组件中可以通过props获取到动态获取的数据props.dataObj
而动态路由通过Link
标签的as
属性进行传值,href
属性则和之前保持一致;需要注意的是:href
和as
的属性值应该是相互兼容的,也就是符合一定的正则匹配规则。
另外,为了请求可以兼容客户端与服务端,推荐使用更加轻量级的isomorphic-unfetch
;请求都是异步的,推荐使用最新的async...await
处理;
然后新建文件pages/tv/[id].js
:
import fetch from "isomorphic-fetch";
const TVShow = ({ show }) => (
<>
<img src={show.image.medium} alt="" />
<h1>{show.name}</h1>
</>
);
TVShow.getInitialProps = async context => {
const { id } = context.query;
const res = await fetch(`http://api.tvmaze.com/shows/${id}`);
const show = await res.json();
return { show };
};
export default TVShow;
这里首先需要注意的是文件名[id].js
,需要next.js
以动态路由方式解析的文件命名必须像这样以[]
进行包裹;
另外,在动态路由页面的getInitialProps
函数中为了获取父级页面传入的动态参数,可以通过context对象获取,context.query
可以获取到查询字符串;
路由的钩子函数
next/router
具有几个钩子函数,可以在路由、history
、hash
等改变时触发某些动作;
常见的几个钩子函数:
- routeChangeStart
- routeChangeComplete
- beforeHistoryChange
- routeChangeError (404不属于路由错误)
- hashChangeStart
- hashChangeComplete
使用和其他事件监听函数基本一致,需要注意的是搞清楚每个钩子函数的触发时机,才能更好的应用;比如routeChangeStart
在离开页面时释放一些可能造成内存泄漏的定时器等;或者hash变化结束后将其写入到本地缓存,一定时间内打开页面直接跳到页面指定位置;或者在routeChangeStart
和routeChangeComplete
中分别开启和关闭加载动画,使页面平滑过渡,避免白屏
执行顺序:
routeChangeStart
> beforeHistoryChange
> routeChangeComplete
;
import Router from 'next/router';
Router.events.on('routeChangeStart', () => {});
Router.events.on('routeChangeComplete', () => {});
...
懒加载
- 外部库
外部库的加载通过异步方式,如下例import { useState } from "react"; const Index = () => { const [time, beatifyTime] = useState(Date.now()); const changeTime = async () => { const moment = await import("moment"); beatifyTime(moment.default(Date.now()).format("lll")); }; return ( <div> <p>{time}</p> <button onClick={changeTime}>时间格式</button> </div> ); }; export default Index;
- 自定义组件的加载
自动以组件库的懒加载可以通过next/dynamic
实现,具体参考codesandbox.io示例,demo如下:import dynamic from "next/dynamic"; const About = dynamic(import("./about")); const Index = () => ( <div> <h1>Lazy Loading</h1> <About /> </div> ); export default Index;
CSS-in-JS:style-jsx
next.js
推荐使用css-in-js的样式引入方式,而不是以模块的方式引入CSS,它提出了一个新的CSS-in-JS框架:style-jsx
;具体的样式是组件包裹元素的最顶级子元素,整个样式使用{``}
进行包裹,具有jsx语法的特点;具体参见官方demo
需要注意的是,默认的style-jsx
语法是局部作用域的,不会影响嵌套组件中的元素;如果需要全局生效,可以使用global
关键字;
<style jsx global>
{`
// component style
`}
</style>
外部CSS加载
-
外部CSS加载
前面提到,next.js
支持地是css-in-js
语法,如果需要以模块的形式引入外部css,需要借助@zeit/next-css
模块;首先安装模块
@zeit/next-css
npm install --save @zeit/next-css
然后需要进行相应配置,在项目根目录新建
next.config.js
,填入以下内容:const withCSS = require('@zeit/next-css'); module.exports = withCSS();
然后新建文件
public/index.css
:div { color: orange; border: 1px solid red; }
最后在·index.js`中引入css
import './public/index.css'
demo具体参考codesandbox.io
-
按需加载antd
为了避免使用antd时将所有的包都打包进来,影响页面性能,因此我们需要进行按需加载首先,安装模块
babel-plugin-import
;npm install --save babel-plugin-import
在根目录新建文件
.babelrc
:{ "presets": ["next/babel"], "plugins": [ [ "import", { "libraryName": "antd", "style": "css", } ] ] }
配置好后,重启服务运行即可;
如果在dev环境运行或最后打包时出现错误,需要单独解决;next.js
提供了一个可以全局使用的文件出口,在dev环境运行时可以看到network
中已经有_app.js
文件,我们根据需求可以定义自己的全局属性、引入全局样式,最终都会被打包到_app.js
中;
因此你阔以新建文件pages/_app.js
,然后在该文件中引入antd.css
,然后编写内容:
import App from 'next/app';
import 'antd/dist/antd.css';
你甚至可以在该文件中引入一些全局样式等;
项目部署
- 配置文件
package.json
:"scripts": { "start": "next start", "dev": "next dev", "build": "next build", }
- 运行以下命令进行打包
npm run build
- 一次构建,多次运行:
// 同时启动多个服务 npm start -- --port=8800 npm start -- --port=3000
参考文献
4. https://nextjs.org/#features
5. https://nextjs.org/learn/basics/create-dynamic-pages
6. 技术胖next.js博客
7. https://github.com/zeit/next.js/tree/canary/examples/with-ant-design