next.js基础

本文为学习next.js而做的笔记,主要参照官网 ,也会借鉴Next.js踩坑

起因与目的

最早接触next.js是在看webpack-isomorphic-tools的官网时候,其推荐新项目使用universal-tools,或者直接用next.js这样的框架。那时候基于create-react-app而搭建的同构已经走了大半,没有继续研究next.js。而搭建的系统遇到了stylus问题,一时间没有找到解决方案,就试着接触一下next.js,一方面想看一下next.js是如何解决这个问题的,另一方也看一下是否可以直接使用next.js。
看过之后,觉得next.js是不错的框架,值得去整理一下。

next.js是一个基于react的SSR框架,它有很多特性值得去应用,下边对这些特性进行整理

对于SSR,首先它需要根据浏览器的路径,选择合适的页面、组件进行渲染;
其次,<head>中对静态文件的引用,页面中也会包含一些静态的文件,包括react、material的js,还有一些图片等;
接着,页面、组件加载时为了提升性能,就需要一些技术:代码切割、预取页面、动态导入等
然后,对于有些请求,需要从api sever中加载数据,然后再渲染到页面上;
再然后,对于登录,需要判断是否登录(头部判断),再进行路由的跳转;
最后,在同构的时候react-router有对history的使用,对于这一点有点疑问,浏览器显然是有自己的一块存储区域来存储历史,为什么在服务器也会看到history?
这期间还夹杂一些基础,如对css的支持、路由的参数等

路由

文件系统路由

next.js是一个文件系统路由,意思是,需要一个pages目录,这个目录对应’/‘路由;若存在’/home’路由,则需要page目录下有一个home目录。路由与目录结构对应。

这种方式跟最早接触的tomcat有些类似,也与http的静态文件形态时候形同。
webship中维护一个路由与文件的映射;
react-router常见的用法是SPA,只有一个页面,也就不太需要这种映射。

Next.js没有一个记录所有路由的清单,当前页面对其他页面一无所知。这样,浏览器中页面的组织、跳转是通过Link来完成的.与html的<a>标签很相似。

它有2个主要属性:
href:a标签的href,包括路由+请求参数。
as:在浏览器中展示的URL。这个听起来有点怪,需要多做一个说明。next.js本身只支持文件系统路由 与 query形式的参数传递,但为了支持制定方式,可以讲文件系统路由命名成其他的路由,展示在浏览器上。这个就是定制路由

url参数:
/about?name=Zeit,从根上,next只支持query形式的传参,对于param形式的传参,需要用as

1
2
3
<Link href={{ pathname: '/about', query: { name: 'Zeit' } }}>
<a>here</a>
</Link>

定制路由

  • 对于/post/:slug路由
  • 需要在pages/post.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Post extends React.Component {
    static async getInitialProps({ query }) {
    console.log('SLUG', query.slug);
    return {};
    }
    render() {
    return <h1>My blog post</h1>;
    }
    }
    export default Post;
  • 在服务增加一个对/post/:slug路由的响应

    1
    2
    3
    server.get('/post/:slug', (req, res) => {
    return app.render(req, res, '/post', { slug: req.params.slug });
    });

    这里需要将这个/post/:slug路由转换成内部的/post路由,参数通过query的方式传递。

  • 在前端使用next/link

    1
    <Link href="/post?slug=something" as="/post/something">

静态文件

  • static
    next.js除了pages目录用于放页面,还有一个static目录用于存放静态文件,这个目录是设定死的,不能改变。路由上使用/ static/与之对应。

    1
    2
    3
    4
    function MyImage() {
    return <img src="/static/my-image.png" alt="my image" />;
    }
    export default MyImage;

加载性能

代码切割

代码切割不应该属于静态文件,指的是对于页面,仅会加载import到的组件,并不会加载其他组件。

预取页面

prefetch预取页面是Link的第三个属性,加上它,next.js会在后台自动加载这些页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import Link from 'next/link';
function Header() {
return (
<nav>
<Link prefetch href="/">
<a>Home</a>
</Link>
<Link prefetch href="/about">
<a>About</a>
</Link>
<Link prefetch href="/contact">
<a>Contact</a>
</Link>
</nav>
);
}
export default Header;

动态引用组件

动态引用可以认为是另一种控制代码切割的方式。

1
2
3
4
// components/hello.js
export function Hello() {
return <p>Hello!</p>;
}
1
2
3
4
5
6
7
8
9
10
11
12
import dynamic from 'next/dynamic';
const DynamicComponent = dynamic(() => import('../components/hello'));
function Home() {
return (
<div>
<Header />
<DynamicComponent />
<p>HOME PAGE is here!</p>
</div>
);
}
export default Home;

动态引入还支持对组件的引入不使用SSR。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import dynamic from 'next/dynamic';
const DynamicComponentWithNoSSR = dynamic(
() => import('../components/hello3'),
{
ssr: false
}
);

function Home() {
return (
<div>
<Header />
<DynamicComponentWithNoSSR />
<p>HOME PAGE is here!</p>
</div>
);
}

export default Home;

动态数据加载

当页面启动需要加载数据时,使用getInitialProps函数来完成,它可以异步获取数据,并解析成对象,并发送给props。注意,getInitialProps 仅可以在pages中使用,不能在components中使用。其仅用于渲染在页面路由中有参数,需要根据这些参数来获取数据并进行渲染的情况。对于动态的交互,还是需要ajax。

1
2
3
4
5
6
7
8
9
10
11
import React from 'react';
class HelloUA extends React.Component {
static async getInitialProps({ req }) {
const userAgent = req ? req.headers['user-agent'] : navigator.userAgent;
return { userAgent };
}
render() {
return <div>Hello World {this.props.userAgent}</div>;
}
}
export default HelloUA;

定制路由

定制路由,next.js的文档写的有点没跟上,大体记录一下:

路由修改

其实与link的as类似,在express中,对外展示一层,对内依然用文件系统路由。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
app.prepare().then(() => {
createServer((req, res) => {
// Be sure to pass `true` as the second argument to `url.parse`.
// This tells it to parse the query portion of the URL.
const parsedUrl = parse(req.url, true);
const { pathname, query } = parsedUrl;
if (pathname === '/a') {
app.render(req, res, '/b', query);
} else if (pathname === '/b') {
app.render(req, res, '/a', query);
} else {
handle(req, res, parsedUrl);
}
}).listen(3000, err => {
if (err) throw err;
console.log('> Ready on http://localhost:3000');
});
);

后端禁用文件系统路由

如果使用定制路由,文件系统路由的方式可能会导致从多个路由里边访问的内容是一样的情况,可以禁用掉文件系统路由。

1
2
3
4
// next.config.js
module.exports = {
useFileSystemPublicRoutes: false
};

useFileSystemPublicRoutes属性会禁用掉SSR侧的文件名路由,CSR侧可能继续提供。对于CSR侧的需要popstate

以上是官方提供的文档,但并没有提供SSR侧禁用文件系统路由之后,服务端的渲染该如何处理的方案。

前端拦截popstate

监听popstate,在router响应之前进行拦截,这样可以操作request,或者强制SSR刷新。

1
2
3
4
5
6
7
8
9
10
import Router from 'next/router';
Router.beforePopState(({ url, as, options }) => {
// I only want to allow these two routes!
if (as !== '/' || as !== '/other') {
// Have SSR render bad routes as a 404.
window.location.href = as;
return false;
}
return true;
});

返回false时,router不再处理popstate

router对象的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import Router from 'next/router';
const handler = () => {
Router.push({
pathname: '/about',
query: { name: 'Zeit' }
});
};

function ReadMore() {
return (
<div>
Click <span onClick={handler}>here</span> to read more
</div>
);
}
export default ReadMore;

这里的功能与<link>相同,但在router上可以监听很多时间,包括routeChangeStart(url)routeChangeComplete(url)
通过:

1
Router.events.on('routeChangeStart', handleRouteChange);

页面跳转

页面跳转其实是express的功能,只需要将res.redirect(301, ‘/new/link’)即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const express = require('express')
const next = require('next')
const { join } = require('path')
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()

const redirects = [
{ from: '/old-link-1', to: '/new-link-1' },
{ from: '/old-link-2', to: 'https://externalsite.com/new-link-2' },
]

app.prepare().then(() => {
const server = express()

redirects.forEach(({ from, to, type = 301, method = 'get' }) => {
server[method](from, (req, res) => {
res.redirect(type, to)
})
})

server.get('*', (req, res) => {
return handle(req, res)
})

server.listen(3000, err => {
if (err) throw err
console.log('> Ready on http://localhost:3000')
})
})

package.json:

1
2
3
4
{
"dev": "node server.js",
"start": "NODE_ENV=production node server.js"
}

对于登录,可以在’*’中,检查req来进行重定向

其他

<App>

next利用App组件来初始化页面,我们可以重写它来控制页面的初始化,可以处理:

  • 在页面变换时,持久化布局
  • 在页面导航时,保存状态
  • 注入额外的数据
  • 使用componentDidCatch来做定制的错误处理

创建一个pages/_app.js,然后继承App来实现即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React from 'react';
import App, { Container } from 'next/app';

class MyApp 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 (
<Container>
<Component {...pageProps} />
</Container>
);
}
}

export default MyApp;

<Document>

用于改变初始化时候服务端渲染的document标记。处理与App类似
修改pages/_document.js,继承自Document