next是个基于react的同构框架,官网文档。默认SSR,基于文件系统的路由,默认code split,默认集成webpack和babel。
- 路由
- 数据
- CSS
- 动态加载
- 部署
- 自定义
路由
路由基于文件系统,自动根据pages目录里的文件夹/文件名生成路由,默认首页是pages/index.js。pages和static(存放图片)目录是next里限定名字的目录,其他目录都可以随意命名。所以不想让用户直接通过url访问的组件,不要直接放在pages目录下。
客户端路由通过next/link组件,本质上是个HOC,内部是location.history,有自己的props:
- href:不解释
- replace:避免新增一条history记录
- as:路由遮盖
- prefetch:预加载(仅生产环境有效)
- scroll:设为scroll={false}不跳转到页面顶部
但不要在上面自定义props,就算你自定义了,也不会被传递给next/link的子组件。
<Link href="/about"> <a title="About Page">About Page</a> </Link> <Link href={`/post?title=${props.title}`}> <a title="Post Page">Post Page</a> </Link> // href属性也支持url对象形式,例如:href={{ pathname: '/about', query: { name: 'Zeit' } }}
也可以通过next/router组件实现客户端路由,Router对象支持push,replace等方法,还提供了beforePopState等钩子函数,允许你在触发路由的时机搞些事情,这都是通用概念,各种版本的router都差不多,不赘述。
import Router from 'next/router' function ReadMore() { return ( <span onClick={() => Router.push('/about')}>here</span> ) } export default ReadMore
另外next/router还提供了个HOC,包在组件外部即可。这样组内的props上就会多了router属性,很方便取出query string。
import { withRouter } from 'next/router'; const Page = withRouter(props => ( <p>{props.router.query.title}</p> )); export default Page;
但只是传递参数query string,用于做url路由不太好看,想将 /post?title=Hello 这样的url改成 /p/hello,我们需要路由遮盖route masking,方法很简单给next/link组件设置as属性即可:
import Link from 'next/link' const PostLink = props => ( <li> <Link as={`/p/${props.id}`} href={`/post?title=${props.title}`}> // 设置 as 属性 <a>{props.title}</a> </Link> </li> ) export default function Blog() { return ( <PostLink id="hello" title="Hello" /> ) } // url上原本 /post?title=Hello 会显示成 /p/hello
但路由遮盖只是障眼法,实际服务器端并不存在 /p/hello 这样的目录结构,因此页面一刷新就404了,还需要服务端渲染:
server.get('/p/:id', (req, res) => { const actualPage = '/post' const queryParams = { title: req.params.id } app.render(req, res, actualPage, queryParams) })
数据
使用getInitialProps(context)来异步获取数据,参数是服务端上下文context,里面有这些属性:
- pathname:url的pathname
- query:url的query对象
- asPath:url的真实path(通过next/link的as属性设置)
- req:HTTP request object (server only)
- res:HTTP response object (server only)
- jsonPageRes:Fetch Response object (client only)
- err:异常对象
方法的返回值会被放入组件的props里。
import fetch from 'isomorphic-unfetch' const Post = props => ( <div> <h1>{props.show.name}</h1> <p>{props.show.summary.replace(/<[/]?p>/g, '')}</p> <img src={props.show.image.medium} /> </div> ) Post.getInitialProps = async function(context) { const { id } = context.query const res = await fetch(`https://xxx/shows/${id}`) const show = await res.json() console.log(`Fetched show: ${show.name}`) return { show } } export default Post
log是在浏览器端被打印出来,还是在服务器端被打印出来,取决于你是浏览器跳转还是服务端跳转。例如通过link只在浏览器端跳转,那console只在浏览器端被打印。如果直接访问url,或刷新页面,那console只在服务器端被打印。
CSS
因为独立CSS文件在SSR时的诸多问题,所以next推荐用CSS in JS的方式,<style jsx>{`…`}</style>
<style jsx>{` ul { padding: 0; } li { list-style: none; margin: 5px 0; } a { text-decoration: none; color: blue; } a:hover { opacity: 0.6; } `}</style>
需要注意的是,CSS in JS默认是自带scoped的,即只针对该组件生效,对子组件无效。子组件你要么自己写style,要么声明成global:<style jsx global>{`…`}</style>
想使用预处理器,例如可以安装插件@zeit/next-less来引入less文件。自己写完less,在组件中import后,next.config.js里配置:
const withLess = require('@zeit/next-less') module.exports = withLess()
预编译打包后的CSS会放在.next/static/css目录。
动态加载
next天生支持code split,如果一个module被至少一半页面都用到,就会自动被抽取到main bundle中。但要注意的是这些module需要用import,而非require来引用。
const xxx = require('xxx'); // 源码打包进每个页面的bundle文件中 import('xxx'); // 使用次数多了会被自动抽取到main bundle中
还有些超出首屏可视区域的组件,应该只在真正被用到时,或进入可视区域时,才加载。用next/dynamic很容易实现懒加载。
// 正常加载 import xxx from 'xxx'; // 懒加载 import dynamic from 'next/dynamic'; const xxx = dynamic(import('xxx'));
懒加载时,可以配合loading来使用,另外因为默认SSR,可以改成客户端渲染:
import dynamic from 'next/dynamic'; const DynamicComponentWithCustomLoading = dynamic( import('../components/hello2'), { ssr: false, loading: () => <p>...</p> } ); export default () => <div> <DynamicComponentWithCustomLoading /> <p>HOME PAGE is here!</p> </div>
支持一次性懒加载多个组件:
import dynamic from 'next/dynamic'; const HelloBundle = dynamic({ modules: () => { const components = { Hello1: () => import('../components/hello1'), Hello2: () => import('../components/hello2') }; return components; }, render: (props, { Hello1, Hello2 }) => ( <div> <h1>{props.title}</h1> <Hello1 /> <Hello2 /> </div> ) });
部署
package.json里:
"scripts": { "dev": "node server.js", "build": "next build", "export": "next export", "start": "next start" },
如果要部署在多个端口上(默认是3000端口),可以如下配置:
"scripts": { ... "start": "next start -p $PORT" },
然后执行 PORT=8000 yarn start,PORT=8000 yarn start 可以在两个端口上打开同一个应用。
服务端文件都在根目录下的.next目录里,静态html资源都在.out目录里。
生成的静态html需要通过next.config.js来配置:
module.exports = { exportPathMap: function() { return { '/': { page: '/' }, '/about': { page: '/about' }, '/p/hello': { page: '/post', query: { title: 'Hello' } }, '/p/learn': { page: '/post', query: { title: 'Learn' } }, '/p/deploy': { page: '/post', query: { title: 'Deploy' } }, '/p/exporting': { page: '/post', query: { title: 'Export HTML Pages' } } } } }
能在.out目录下看到配置后生成的静态html文件,cd out & serve -p 8080可以打开这些页面。
next.config.js里可以对next工程进行自定义配置。
自定义
./pages/_app.js:可以让你自定义如何初始化页面,修改的是next/app
./pages/_document.js:可以让你自定义html模板,修改的是next/document,该模板是SSR,无法在客户端渲染,因此模板里不要放入例如 onClick 等事件。