使用Next.js构建React服务端渲染应用

next.js简介

最近在学React.js,React官方推荐使用next.js框架做为构建服务端渲染的网站,因此今天来研究一下next.js的使用。css

next.js做为一款轻量级的应用框架,主要用于构建静态网站和后端渲染网站。前端

框架特色

  • 使用后端渲染
  • 自动进行代码分割(code splitting),以得到更快的网页加载速度
  • 简洁的前端路由实现
  • 使用webpack进行构建,支持模块热更新(Hot Module Replacement)
  • 可与主流Node服务器进行对接(如express)
  • 可自定义babel和webpack的配置

使用方法

建立项目并初始化

mkdir server-rendered-website
cd server-rendered-website
npm init -y

安装next.js

使用npm或者yarn安装,由于是建立React应用,因此同时安装react和react-domnode

npm:
npm install --save react react-dom next
yarn:
yarn add react react-dom next

在项目根目录下添加文件夹pages(必定要命名为pages,这是next的强制约定,否则会致使找不到页面),而后在package.json文件里面添加script用于启动项目:react

"scripts": {
    "dev": "next"
}

以下图imagewebpack

建立视图

在pages文件夹下建立index.js文件,文件内容:git

const Index = () => (
  <div>
    <p>Hello next.js</p>
  </div>
)

export default Index

运行

npm run next

在浏览器中打开http://localhost:3000/,网页显示以下:
imagegithub

这样就完成了一个最简单的next网站。web

前端路由

next.js前端路由的使用方式很是简单,咱们先增长一个page,叫about,内容以下:express

const About = () => (
    <div>
        <p>This is About page</p>
    </div>
)

export default About;

当咱们在浏览器中请求https://localhost:3000/about时,能够看到页面展现对应内容。(==这里须要注意:请求url的path必须和page的文件名大小写一致才能访问,若是访问localhost:3000/About的话是找不到about页面的。==)npm

咱们可使用传统的a标签在页面之间进行跳转,但每跳转一次,都须要去服务端请求一次。为了增长页面的访问速度,推荐使用next.js的前端路由机制进行跳转。

next.js使用next/link实现页面之间的跳转,用法以下:

import Link from 'next/link'

const Index = () => (
  <div>
    <Link href="/about">
      <a>About Page</a>
    </Link>
    <p>Hello next.js</p>
  </div>
)

export default Index

这样点击index页面的AboutPage连接就能跳转到about页面,而点击浏览器的返回按钮也是经过前端路由进行跳转的。 官方文档说用前端路由跳转是不会有网络请求的,实际会有一个对about.js文件的请求,而这个请求来自于页面内动态插入的script标签。可是about.js只会请求一次,以后再访问是不会请求的,毕竟相同的script标签是不会重复插入的。 可是想比于后端路由仍是大大节省了请求次数和网络流量。前端路由和后端路由的请求对好比下:

前端路由:

image

后端路由:

image

Link标签支持任意react组件做为其子元素,不必定要用a标签,只要该子元素能响应onClick事件,就像下面这样:

<Link href="/about">
    <div>Go about page</div>
</Link>

Link标签不支持添加style和className等属性,若是要给连接增长样式,须要在子元素上添加

<Link href="/about">
    <a className="about-link" style={{color:'#ff0000'}}>Go about page</a>
</Link>

Layout

所谓的layout就是就是给不一样的页面添加相同的header,footer,navbar等通用的部分,同时又不须要写重复的代码。在next.js中能够经过共享某些组件实现layout。

咱们先增长一个公共的header组件,放在根目录的components文件夹下面(页面级的组件放pages中,公共组件放components中):

import Link from 'next/link';

const linkStyle = {
    marginRight: 15
}

const Header = () => (
    <div>
        <Link href="/">
            <a style={linkStyle}>Home</a>
        </Link>
        <Link href="/about">
            <a style={linkStyle}>About</a>
        </Link>
    </div>
)

export default Header;

而后在index和about页面中引入header组件,这样就实现了公共的layout的header:

import Header from '../components/Header';

const Index = () => (
    <div>
        <Header />
        <p>Hello next.js</p>
    </div>
)

export default Index;

若是要增长footer也能够按照header的方法实现。
除了引入多个header、footer组件,咱们能够实现一个总体的Layout组件,避免引入多个组件的麻烦,一样在components中添加一个Layout.js文件,内容以下:

import Header from './Header';

const layoutStyle = {
    margin: 20,
    padding: 20,
    border: '1px solid #DDD'
}

const Layout = (props) => (
    <div style={layoutStyle}>
        <Header />
        {props.children}
    </div>
)

export default Layout

这样咱们只须要在页面中引入Layout组件就能够达到布局的目的:

import Layout from '../components/Layout';

const Index = () => (
    <Layout>
        <p>Hello next.js</p>
    </Layout>
)

export default Index;

页面间传值

经过url参数(query string)

next中的页面间传值方式和传统网页同样也能够用url参数实现,咱们来作一个简单的博客应用:

首先将index.js的内容替换成以下来展现博客列表:

import Link from 'next/link';
import Layout from '../components/Layout';

const PostLink = (props) => (
    <li>
        <Link href={`/post?title=${props.title}`}>
            <a>{props.title}</a>
        </Link>
    </li>
);

export default () => (
    <Layout>
        <h1>My Blog</h1>
        <ul>
            <PostLink title="Hello next.js" />
            <PostLink title="next.js is awesome" />
            <PostLink title="Deploy apps with Zeit" />
        </ul>
    </Layout>
);

经过在Link的href中添加title参数就能够实现传值。

如今咱们再添加博客的详情页post.js

import { withRouter } from 'next/router';
import Layout from '../components/Layout';

const Post = withRouter((props) => (
    <Layout>
        <h1>{props.router.query.title}</h1>
        <p>This is the blog post content.</p>
    </Layout>
));

export default Post;

上面代码经过withRouter将next的router做为一个prop注入到component中,实现对url参数的访问。

运行后显示如图:

列表页

image

点击进入详情页:

image

使用query string能够实现页面间的传值,可是会致使页面的url不太简洁美观,尤为当要传输的值多了以后。因此next.js提供了Route Masking这个特性用于路由的美化。

路由假装(Route Masking)

这项特性的官方名字叫Route Masking,没有找到官方的中文名,因此就根据字面意思暂且翻译成路由假装。所谓的路由假装即让浏览器地址栏显示的url和页面实际访问的url不同。实现路由假装的方法也很简单,经过Link组件的as属性告诉浏览器href对应显示为何url就能够了,index.js代码修改以下:

import Link from 'next/link';
import Layout from '../components/Layout';

const PostLink = (props) => (
    <li>
        <Link as={`/p/${props.id}`} href={`/post?title=${props.title}`}>
            <a>{props.title}</a>
        </Link>
    </li>
);

export default () => (
    <Layout>
        <h1>My Blog</h1>
        <ul>
            <PostLink id="hello-nextjs" title="Hello next.js" />
            <PostLink id="learn-nextjs" title="next.js is awesome" />
            <PostLink id="deploy-nextjs" title="Deploy apps with Zeit" />
        </ul>
    </Layout>
);

运行结果:

image

浏览器的url已经被如期修改了,这样看起来舒服多了。并且路由假装对history也很友好,点击返回再前进仍是可以正常打开详情页面。可是若是你刷新详情页,确报404的错误,如图:

image

这是由于刷新页面会直接向服务器请求这个url,而服务端并无该url对应的页面,因此报错。为了解决这个问题,须要用到next.js提供的自定义服务接口(custom server API)。

自定义服务接口

自定义服务接口前咱们须要建立服务器,安装Express:

npm install --save express

在项目根目录下建立server.js 文件,内容以下:

const express = require('express');
const next = require('next');

const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();

app.prepare()
    .then(() => {
        const server = express();
        server.get('*', (req, res) => {
            return handle(req, res);
        });
        server.listen(3000, (err) => {
            if (err) throw err;
            console.log('> Ready on http://localhost:3000');
        });
    })
    .catch((ex) => {
        console.error(ex.stack);
        process.exit(1);
    });

而后将package.json里面的dev script改成:

"scripts": {
    "dev": "node server.js"
}

运行npm run dev后项目和以前同样能够运行,接下来咱们须要添加路由将被假装过的url和真实的url匹配起来,在server.js中添加:

......
const server = express();
server.get('/p/:id', (req, res) => {
    const actualPage = '/post';
    const queryParams = { title: req.params.id };
    app.render(req, res, actualPage, queryParams);
});
......

这样咱们就把被假装过的url和真实的url映射起来,而且query参数也进行了映射。重启项目以后就能够刷新详情页而不会报错了。可是有一个小问题,前端路由打开的页面和后端路由打开的页面title不同,这是由于后端路由传过去的是id,而前端路由页面显示的是title。这个问题在实际项目中能够避免,由于在实际项目中咱们通常会经过id获取到title,而后再展现。做为Demo咱们偷个小懒,直接将id做为后端路由页面的title。

以前咱们的展现数据都是静态的,接下来咱们实现从远程服务获取数据并展现。

远程数据获取

next.js提供了一个标准的获取远程数据的接口:getInitialProps,经过getInitialProps咱们能够获取到远程数据并赋值给页面的props。getInitialProps便可以用在服务端也能够用在前端。接下来咱们写个小Demo展现它的用法。咱们打算从TVMaze API 获取到一些电视节目的信息并展现到个人网站上。首先,咱们安装isomorphic-unfetch,它是基于fetch实现的一个网络请求库:

npm install --save isomorphic-unfetch

而后咱们修改index.js以下:

import Link from 'next/link';
import Layout from '../components/Layout';
import fetch from 'isomorphic-unfetch';

const Index = (props) => (
    <Layout>
        <h1>Marvel TV Shows</h1>
        <ul>
            {props.shows.map(({ show }) => {
                return (
                    <li key={show.id}>
                        <Link as={`/p/${show.id}`} href={`/post?id=${show.id}`}>
                            <a>{show.name}</a>
                        </Link>
                    </li>
                );
            })}
        </ul>
    </Layout>
);

Index.getInitialProps = async function () {
    const res = await fetch('https://api.tvmaze.com/search/shows?q=marvel');
    const data = await res.json();
    return {
        shows: data
    }
}

export default Index;

以上代码的逻辑应该很清晰了,咱们在getInitialProps中获取到电视节目的数据并返回,这样在Index的props就能够获取到节目数据,再遍历渲染成节目列表。

运行项目以后,页面完美展现:

image

接下来咱们来实现详情页,首先咱们把/p/:id的路由修改成:

...
server.get('/p/:id', (req, res) => {
    const actualPage = '/post';
    const queryParams = { id: req.params.id };
    app.render(req, res, actualPage, queryParams);
});
...

咱们经过将id做为参数去获取电视节目的详细内容,接下来修改post.js的内容为:

import fetch from 'isomorphic-unfetch';
import Layout from '../components/Layout';

const Post = (props) => (
    <Layout>
        <h1>{props.show.name}</h1>
        <p>{props.show.summary.replace(/<[/]?p>/g, '')}</p>
        <img src={props.show.image.medium} />
    </Layout>
);

Post.getInitialProps = async function (context) {
    const { id } = context.query;
    const res = await fetch(`https://api.tvmaze.com/shows/${id}`);
    const show = await res.json();
    return { show };
}

export default Post;

重启项目(修改了server.js的内容须要重启),从列表页进入详情页,已经成功的获取到电视节目的详情并展现出来:

image

增长样式

到目前为止,我们作的网页都太平淡了,因此接下来我们给网站增长一些样式,让它变得漂亮。

对于React应用,有多种方式能够增长样式。主要分为两种:

  1. 使用传统CSS文件(包括SASS,PostCSS等)
  2. 在JS文件中插入CSS

使用传统CSS文件在实际使用中会用到挺多的问题,因此next.js推荐使用第二种方式。next.js内部默认使用styled-jsx框架向js文件中插入CSS。这种方式引入的样式在不一样组件之间不会相互影响,甚至父子组件之间都不会相互影响。

styled-jsx

接下来,咱们看一下如何使用styled-jsx。将index.js的内容替换以下:

import Link from 'next/link';
import Layout from '../components/Layout';
import fetch from 'isomorphic-unfetch';

const Index = (props) => (
    <Layout>
        <h1>Marvel TV Shows</h1>
        <ul>
            {props.shows.map(({ show }) => {
                return (
                    <li key={show.id}>
                        <Link as={`/p/${show.id}`} href={`/post?id=${show.id}`}>
                            <a className="show-link">{show.name}</a>
                        </Link>
                    </li>
                );
            })}
        </ul>
        <style jsx>
        {`
            *{
                margin:0;
                padding:0;
            }
            h1,a{
                font-family:'Arial';
            }
            h1{
                margin-top:20px;
                background-color:#EF141F;
                color:#fff;
                font-size:50px;
                line-height:66px;
                text-transform: uppercase;
                text-align:center;
            }    
            ul{
                margin-top:20px;
                padding:20px;
                background-color:#000;
            }
            li{
                list-style:none;
                margin:5px 0;
            }
            a{
                text-decoration:none;
                color:#B4B5B4;
                font-size:24px;
            }
            a:hover{
                opacity:0.6;
            }
        `}
        </style>
    </Layout>
);

Index.getInitialProps = async function () {
    const res = await fetch('https://api.tvmaze.com/search/shows?q=marvel');
    const data = await res.json();
    console.log(`Show data fetched. Count: ${data.length}`);
    return {
        shows: data
    }
}

export default Index;

运行项目,首页变成:
image

增长了一点样式以后比以前好看了一点点。咱们发现导航栏的样式并无变。由于Header是一个独立的的component,component之间的样式不会相互影响。若是须要为导航增长样式,须要修改Header.js:

import Link from 'next/link';

const Header = () => (
    <div>
        <Link href="/">
            <a>Home</a>
        </Link>
        <Link href="/about">
            <a>About</a>
        </Link>
        <style jsx>
            {`
                a{
                    color:#EF141F;
                    font-size:26px;
                    line-height:40px;
                    text-decoration:none;
                    padding:0 10px;
                    text-transform:uppercase;
                }
                a:hover{
                    opacity:0.8;
                }
            `}
        </style>
    </div>
)

export default Header;

效果以下:
image

全局样式

当咱们须要添加一些全局的样式,好比rest.css或者鼠标悬浮在a标签上时出现下划线,这时候咱们只须要在style-jsx标签上增长global关键词就好了,咱们修改Layout.js以下:

import Header from './Header';

const layoutStyle = {
    margin: 20,
    padding: 20,
    border: '1px solid #DDD'
}

const Layout = (props) => (
    <div style={layoutStyle}>
        <Header />
        {props.children}
        <style jsx global>
            {`
                a:hover{
                    text-decoration:underline;
                }
            `}
        </style>
    </div>
)

export default Layout

这样鼠标悬浮在全部的a标签上时会出现下划线。

部署next.js应用

Build

部署以前咱们首先须要能为生产环境build项目,在package.json中添加script:

"build": "next build"

接下来咱们须要能启动项目来serve咱们build的内容,在package.json中添加script:

"start": "next start"

而后依次执行:

npm run build
npm run start

build完成的内容会生成到.next文件夹内,npm run start以后,咱们访问的实际上就是.next文件夹的内容。

运行多个实例

若是咱们须要进行横向扩展( Horizontal Scale)以提升网站的访问速度,咱们须要运行多个网站的实例。首先,咱们修改package.json的start script:

"start": "next start -p $PORT"

若是是windows系统:

"start": "next start -p %PORT%"

而后运行build: npm run build,而后打开两个命令行并定位到项目根目录,分别运行:

PORT=8000 npm start
PORT=9000 npm start

运行完成后打开localhost:8000localhost:9000均可以正常访问:

image

经过以上方法虽然可以打包并部署,可是有个问题,咱们的自定义服务server.js并无运行,致使在详情页刷新的时候依然会出现404的错误,因此咱们须要把自定义服务加入app的逻辑中。

部署并使用自定义服务

咱们将start script修改成:

"start": "NODE_ENV=production node server.js"

这样咱们就解决了自定义服务的部署。重启项目后刷新详情页也可以正常访问了。

到此为止,咱们已经了解了next.js的大部分使用方法,若是有疑问能够查看next.js官方文档,也能够给我留言讨论。

感谢你们阅读,另外,在这边帮朋友推一个爱心众筹,但愿你们可以奉献点爱心,朋友母亲,身患直肠癌,目前在北京武警总医院接收治疗,可留言留下您的联系方式,往后感激你们!

clipboard.png

相关文章
相关标签/搜索