next.js从0到1

1,787 阅读4分钟

前言

目前spa项目已经很火了,基于vue、react。但是随之而来的也有两个问题比较困扰使用者:
1、CSR 项目的 TTFP(Time To First Page)首屏时间比较长
2、CSR 项目的 SEO 能力极弱,在搜索引擎中基本上不可能有好的排名。因为目前大多数搜索引擎主要识别的内容还是 HTML,对 JavaScript 文件内容的识别都还比较弱。
所以SSR随之诞生,为了解决spa应用的seo和首屏渲染问题,目前成熟的方案有vue的nuxt.js,react的next.js。
需要熟悉的技术栈有react,node,webpack,对开发者的要求比较高,尤其node层优化不好的话会给服务器造成很大的压力,所以小白可以选择成熟的方案来做ssr。接下来带你一步一步搞next.js。

起步

按照官网的步骤安装

npm install --save next react react-dom

将下面代码添加到package.json中

{
 "scripts": {
   "dev": "next",
   "build": "next build",
   "start": "next start"
 }
}

因为next.js是以pages下的文件名为路由的,所以新建文件夹pages,里面新建文件index.js,老规矩先来句hello world,在你新建的index.js里添加:

export default () => <div>hello world!this is next.js</div>

可以启动了,控制台运行:

npm run dev

打开浏览器localhost:3000就可以看到你写的内容了。

配置typescript

现在很多公司都在使用ts了,基本上已经是react的标配,现在next.js也可以无缝支持了,别的文档上介绍需要用'@zeit/next-typescript',但是在next.js9.0以上版本不需要了,直接将你的.js/.jsx文件改成.ts/.tsx,当然里面的语法也要改成ts语法。再次运行npm run dev, next.js会自动帮你创建tsconfig.js,再搭配vscode提供ts语法支持,写起来爽到飞。

写css

官网上推荐写style-jsx,可以内联样式,当然如果想写scss,less也是可以的,这里以less为例,根目录下新建next.config.js,这个文件里可以自行配置webpack,还有更多的配置可以自行查看文档。

npm install @zeit/next-less
const withLess = require('@zeit/next-less');
module.exports = withLess({
    lessLoaderOptions: {
        javascriptEnabled: true,
        localIdentName: "[local]___[hash:base64:5]"
    }
});

接下来根目录下新建static文件夹,注意:next只认static做为静态资源的文件夹,叫别的名都不行,在static里新建css,img文件夹,css中新建index.less文件,里面就可以用less写样式了,最后在我们的pages/index.tsx下导入:

import '../static/css/index.less'

当然也可以使用cssmodule,

const withLess = require('@zeit/next-less')
module.exports = withLess({
  cssModules: true,
  cssLoaderOptions: {
    importLoaders: 1,
    localIdentName: "[local]___[hash:base64:5]",
  }
})

在pages/index.tsx中

import style from '../static/css/index.less'

生成head

这可能是使用ssr最关心的问题了,在next.js这里都不是事,配置很简单,在你的index.tsx文件下:

import React from 'react'
import Head from 'next/head';
import '../../static/css/index.less';
export default class extends React.Component<any, any> {
    constructor (props) {
        super(props)
    }
    render() {
        return (
            <div className='app'>
                <Head>
                    <title>自定义title</title>
                    <meta name="viewport" content="initial-scale=1.0, width=device-width" />
                    <link rel="shortcut icon" href="https://vv.bdstatic.com/static/videoui/img/favicon-4_f4b9465.ico" type="image/x-icon"></link>
                </Head>
            </div>
        )
    }
}

获取数据及组件生命周期

getInitialProps,它能异步获取 JS 普通对象,并绑定在props上。

当服务渲染时,getInitialProps将会把数据序列化,就像JSON.stringify。所以确保getInitialProps返回的是一个普通 JS 对象,而不是Date, Map 或 Set类型。

当页面初始化加载时,getInitialProps只会加载在服务端。只有当路由跳转(Link组件跳转或 API 方法跳转)时,客户端才会执行getInitialProps。

注意:getInitialProps将不能使用在子组件中。只能使用在pages页面中。只有服务端用到的模块放在getInitialProps里, 否则会拖慢你的应用速度。
新建component文件夹,这里存放组件。直接贴代码了components/list/index.tsx:


import './index.less';
import Link from 'next/link'
export default (props: any) => {
    return (
        <ul className='infinite hkrecommend'>
            {
                props.list.map((v, i) =>
                    <li key={v.id}>
                        <Link href={/v?vid=${v.id}}>
                            <a>你好我是渣渣辉</a>
                        </Link>
                    </li>
                )
            }
        </ul>
    )
}

peges/index.tsx中使用

import React from 'react'
import Head from 'next/head';
import List from '../../components/list';
import fetch from 'isomorphic-unfetch';
import '../../static/css/index.less';
export default class extends React.Component<any, any> {
    constructor (props) {
        super(props)
    }
    static async getInitialProps({req}) {
        let hlist = [];
        if(req) { // 服务端执行
            const result = await fetch('xxxxxxx.json');
            const json = await result.json();
            hlist = json.data.response.videos;
        } else { // 客户端执行
            const list = window['__NEXT_DATA__'].props.pageProps.hlist;
            hlist = list;
        }
        return {hlist}
    }
    render() {
        return (
            <div className='app'>
                <Head>
                    <title>自定义title</title>
                    <meta name="viewport" content="initial-scale=1.0, width=device-width" />
                    <link rel="shortcut icon" href="https://vv.bdstatic.com/static/videoui/img/favicon-4_f4b9465.ico" type="image/x-icon"></link>
                </Head>
                {this.props.hlist.length > 0 ?
                    <List list={this.props.hlist}></List>
                    : <div>暂无数据哦</div>
                }
            </div>
        )
    }
}

这里用到了Link,是next官方推荐的跳转路由的方法,可以使用

<link prefetch>

使链接和预加载在后台同时进行,来达到页面的最佳性能。(只有生产环境有此功能)。还有其他更多参数详见官网,请求数据用isomorphic-unfetch,既可以在服务端也可以在客户端执行,数据地址要替换成自己的,这里都是假的,小伙伴们不要照抄。

自定义路由

next路由是pages下的每一个文件名,这样工程庞大的时候维护起来不方便,还不能够使用类似/types/id: xxxxxx的动态路由,这时可以使用自定义路由。根目录下新建server.js,可以选择koa2或express,既然写node服务就少不了缓存,好的缓存能让你的页面访问更快。这里使用express做后端服务,lru-cache做缓存。

npm install express lru-cache

在server.js中

const express = require('express');
const next = require('next');
const LRUCache = require('lru-cache');

const port = parseInt(process.env.PORT, 10) || 3000;
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();

// This is where we cache our rendered HTML pages
const ssrCache = new LRUCache({
  max: 100,
  maxAge: 1000 * 60 * 60 // 1hour
});

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

    // Use the `renderAndCache` utility defined below to serve pages
    server.get('/', (req, res) => {
      renderAndCache(req, res, '/homepage')
    })

    server.get('/v', (req, res) => {
      renderAndCache(req, res, '/videoland', req.query)
    })

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

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

/*
 * NB: make sure to modify this to take into account anything that should trigger
 * an immediate page change (e.g a locale stored in req.session)
 */
function getCacheKey (req) {
  return `${req.url}`
}

async function renderAndCache (req, res, pagePath, queryParams) {
  const key = getCacheKey(req)

  // If we have a page in the cache, let's serve it
  if (ssrCache.has(key)) {
    res.setHeader('x-cache', 'HIT')
    res.send(ssrCache.get(key))
    return
  }

  try {
    // If not let's render the page into HTML
    const html = await app.renderToHTML(req, res, pagePath, queryParams)

    // Something is wrong with the request, let's skip the cache
    if (res.statusCode !== 200) {
      res.send(html)
      return
    }

    // Let's cache this page
    ssrCache.set(key, html)

    res.setHeader('x-cache', 'MISS')
    res.send(html)
  } catch (err) {
    app.renderError(err, req, res, pagePath, queryParams)
  }
}

然后改写一下我们的启动命令,在package.json中:

  "scripts": {
    "dev": "node ./server.js",
    "build": "next build",
    "start": "NODE_ENV=production node server.js"
  },

项目部署

在你的服务器上安装node,部署中,你可以先构建打包生成环境代码,再启动服务。因此,构建和启动分为下面两条命令:

npm run build
npm start

接下来官方推荐使用now去部署next.js项目。

结语

个人认为前后端协作开发的趋势是后端专注于接口,提升后端服务性能,node做中间层聚合数据,渲染模版,前端负责展示页面,用户体验。next.js是实践道路上的一次优秀项目,比较适用于小型公司静态展示型项目,或者用户操作不多。如果是企业级应用,还需要去自己定制,可以参考阿里基于egg.js开发的北斗同构框架,可以预料的是未来的前端将会承担更多的职责。最后附GitHub地址,求star😄😄🙏🙏
github.com/wangyingjie…