next

11 4月

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 等事件。

发表评论

电子邮件地址不会被公开。 必填项已用*标注