NextJS 是一个 React 应用框架,使用它可以快速上手开发 React 应用,而不是先花很多时间和精力去折腾各种开发工具。AntD 是蚂蚁金服开源的一个 React 组件库,提供了许多功能强大的 UI 组件,特别适合业务复杂的企业级后台产品。两者相结合,可以快速开发出功能复杂的 Web 应用。

NextJS 介绍

熟悉 React 的开发人员都知道,它只是一个用来构建 UI 的库,这对于开发一个完整的 React 应用是远远不够的。我们还需要构建、打包和运行等开发工具,比如使用 Babel 转换使用了新特性的 JavaScript 代码、处理 SASS 和 LESS 样式文件等。开发功能时,除了构建界面,还需要处理页面路由、获取服务端数据、管理应用状态等。为了让应用对搜索引擎友好,最好支持服务端渲染。如果自己从零去安装配置各种开发工具,需要花费许多时间和精力,因此 React 官方提供了 Create React App(简称 CRA)工具来降低上手开发 React 应用的门槛。NextJS 可以看作是 CRA 的升级版,它具有以下重要特性。

服务端渲染(SSR)

CRA 默认使用客户端渲染(CSR),首屏需要先加载应用基础代码然后在客户端生成 DOM 树并渲染,打开速度较慢。并且客户端渲染对搜索引擎不友好,它抓取到的是空白页面,除非搜索引擎加载并执行页面中的 JavaScript 代码。NextJS 支持服务端渲染,有效解决了这两个问题。首屏在服务端生成 HTML 并返回给浏览器,浏览器直接渲染就行,并且服务端性能一般比客户端强,因此首屏打开非常快。后续打开的页面在客户端渲染,不会造成页面整体刷新,体验上跟单页面应用一致。搜索引擎来抓取页面的时候,服务端会返回生成好的 HTML 内容,因此网站内容能够被收录。

基于文件系统的页面路由

一般编写 React 应用都需要使用第三方的路由组件,比如 react-router ,并且需要建立 URL 路径跟页面组件的对应关系,比较麻烦。而 NextJS 则采用了类似传统服务端页面生成网站(比如 PHP)的方式,页面 URL 路径跟磁盘文件路径一致,这样就不用去为每个页面创建路由了。并且 NextJS 还支持指定路径别名,也就是显示在浏览器地址栏里的路径可以跟磁盘文件路径不一样。

自动按页面拆分代码

NextJS 将每个页面单独打包,打开首页时会加载应用基础代码和首页代码,其它页面代码只会在打开时才去加载,这对于大型应用来说非常有用。并且 NextJS 还支持页面预取,在链接页面时可以选择是否在打开本页时就预先获取被链接页面的代码,这样打开链接时就不需要发送网络请求了,直接渲染!

静态页面导出

如果网站页面数据都可以预先确定,不需要在运行时再从其它地方获取,那么你甚至可以将整个 NextJS 应用导出为一个静态网站。这样就没有任何运行时的开销了,使用 Nginx 这样的 Web 服务器对外提供静态网页服务就可以,适合流量非常大的网站。

CSS-in-JS

目前已经有很多 CSS-in-JS 的方案,包括 React 使用的 JSX 就支持通过 style 属性指定组件样式。但 React 使用的 CSS 属性名称跟标准不一样,它用的是驼峰方式,而标准是小写+中划线方式,另外 React 不是百分百支持所有 CSS 属性。NextJS 内置了 styled-jsx 方案,样式写法遵循标准,并且样式作用域局限于组件内部而不是全局,避免了组件之间样式互相影响。

比如下面的代码,对 p 标签设置的样式只会作用于本组件内的 p 标签,并且不会作用于本组件内嵌入的其它组件里的 p 标签。

export default () => (
  <div>
    <p>only this paragraph will get the style :)</p>

    { /* you can include <Component />s here that include
         other <p>s that don't get unexpected styles! */ }

    <style jsx>{`
      p {
        color: red;
      }
    `}</style>
  </div>
)

AntD 介绍

AntD 是一套用于构建 Web 界面的 React 组件库,是蚂蚁金服前端团队的技术结晶。

设计语言

AntD 有自己的设计语言,定义了交互、视觉、模式、可视化、动效等界面体验相关的原则和方案。遵循这套设计语言,不懂设计的开发人员也可以编写出界面美观、用户体验良好的 Web 页面。

组件库

AntD 组件库里提供了大量功能强大、可定制性强的组件,涵盖布局、导航、数据录入、数据展示、反馈等各种前端交互场景。熟练使用这些组件,能够快速实现复杂的前端交互,大大提升开发效率。

动手实践

光说不练假把式,学习任何一门技术只有亲自动手才能够深入掌握。为了方便开发人员在应用中使用 AntD 组件库,其发布者还配套提供了应用框架 umi 和基于 Redux 的轻量数据流方案 dva 。我们这里不打算使用这两者,而是在业界使用更广泛的应用框架 NextJS 和原生 Redux。

创建 NextJS 应用

  1. 为了使用 NPM 来管理库包依赖,需要先创建一个 NPM 项目。
$ mkdir nextjs-demo && cd nextjs-demo
$ npm init
  1. 安装 React 和 NextJS 包。
$ npm install --save next react react-dom
  1. 编写一个测试用的首页。

./pages/index.js

function Home() {
  return <div>Welcome to next.js!</div>
}

export default Home
  1. 运行应用
$ npm run dev

在浏览器里打开网址 http://localhost:3000 即可访问到应用首页。

引入 AntD 组件库

安装 antd 包

$ npm install --save antd

使用 AntD 组件

AntD 样式文件采用的 LESS 语法,如果不考虑性能,并且不需要定制 AntD 组件的样式,那么可以直接在 NextJS App 入口文件中引入已经转换过后的完整 CSS 文件。

NextJS App 入口文件默认是隐藏的,需要显示创建一个,并在其头部引入 antd.css 文件。

./pages/_app.js

import React from 'react'
import App, { Container } from 'next/app'

@import '~antd/dist/antd.css'

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

修改前面创建的首页,在页面添加一个 AntD 的按钮。

./pages/index.js

import Button from 'antd/lib/button'

function Home() {
  return (
    <div>
      Welcome to next.js!
      <Button type="primary">Button</Button>
    </div>
  )
}

export default Home

NextJS 提供了热加载功能,只需要保存修改,打开的调试页就会自动更新。如果有错误,页面上会展示详细的堆栈,定位问题非常方便。

上面的方式虽然可以工作,但会引入全部组件的样式,即便我们只是使用了 Button 组件。并且为了避免引入其它未用到组件的 JS 代码,我们单独引入了 Button 组件所在的文件。如果页面中使用了许多 AntD 组件,需要编写很多 import 语句。更好的方式是按需加载,只有用到了某个组件,才加载其相关的 JS 和 CSS 代码。这可以通过 babel 的 import 插件来实现。

按需加载 AntD 组件

首先安装 babel-plugin-import

$ npm install --save babel-plugin-import

然后配置 babel。

./.babelrc

{
  "presets": [
    "next/babel"
  ],
  "plugins": [
    [
      "import",
      {
        "libraryName": "antd",
        "style": true
      },
      "antd"
    ]
  ]
}

最后修改 APP 文件,删除引入 antd.css 的语句,或者删除整个 APP 文件,使用 NextJS 默认的。修改首页,改成从 antd 包里引入 Button 组件,而不是从具体文件。因为已经配置了按需加载,不用担心这样会加载整个 antd 包的代码。

./pages/index.js

import { Button } from 'antd'

function Home() {
  return (
    <div>
      Welcome to next.js!
      <Button type="primary">Button</Button>
    </div>
  )
}

export default Home

自定义 AntD 主题

AntD 样式文件使用的 LESS 语法,并且提供了许多 LESS 变量来允许自定义主题,包括颜色、字号、背景、边框等。由于需要覆盖 LESS 变量,因此我们需要加载组件的 LESS 文件而不是 CSS 文件。

  1. NextJS 应用默认不支持 LESS,需要安装插件 @zeit/next-less
$ npm install --save @zeit/next-less less
  1. 修改 NextJS 配置文件,该配置文件默认没有提供,需要新建。

./next.config.js

const withLess = require('@zeit/next-less')

module.exports = withLess({
  webpack: (config, options) => {
    // Further custom configuration here
    return config
  }
})
  1. 创建一个 LESS 文件来保存需要覆盖的 AntD LESS 变量及其修改值,文件路径可任意。

./assets/antd.less

@primary-color: #d4380d;

  1. 配置 NextJS 加载自定义 LESS 变量并覆盖 AntD LESS 变量。

先安装几个要用到的 npm 包。

$ npm install --save less-vars-to-js fs path

修改 NextJS 配置文件为如下。

./next.config.js

const withLess = require('@zeit/next-less')
const lessToJS = require('less-vars-to-js')
const fs = require('fs')
const path = require('path')

// fix: prevents error when .less files are required by node
if (typeof require !== 'undefined') {
  require.extensions['.less'] = file => { }
}

module.exports = withLess({
  lessLoaderOptions: {
    javascriptEnabled: true,
    modifyVars: lessToJS(
      fs.readFileSync(path.resolve(__dirname, './assets/antd.less'), 'utf8')
    ),
  },
})

到这里就基本完成了开发环境和工具的配置,可以开始开发业务功能。

引入 Redux

由于 NextJS 使用了服务端渲染,因此引入 Redux 会略微麻烦一些,不过可以借助于 next-redux-wrapper 这个 NextJS 扩展来简化工作。

  1. 首先创建应用的 Store。

./store.js

import { createStore } from 'redux'

const initialState = { foo: '' }

const reducer = (state = initialState, action = {}) => {
  switch (action.type) {
    case 'FOO':
      return { ...state, foo: action.payload };
    default:
      return state
  }
}

export function makeStore(initialState, { isServer }) {
  initialState = initialState || reducer()
  
  if (isServer) {
    return createStore(reducer, initialState)
  } else {
    const { persistReducer, persistStore } = require('redux-persist')
    const storage = require('redux-persist/lib/storage').default

    const persistedReducer = persistReducer({
      key: 'nextjs',
      storage
    }, reducer)
    const store = createStore(persistedReducer, initialState)

    store.__persistor = persistStore(store)

    return store
  }
}

上面的代码里通过 isServer 变量来判断当前环境是服务端还是客户端,该变量在 next-redux-wrapper 调用 makeStore 时会传递进来。如果是在客户端环境,我们还使用了 redux-persist 来持久化存放在 Store 里的应用状态。

值得注意的是,由于服务端渲染的存在,也就没有必要再使用 PersistGate 来监听是否已经完成持久化状态加载,因为服务端渲染肯定会先于持久化状态加载。在服务端渲染时也可以初始化 Redux store 的状态,不过这里需要防止后续客户端加载的持久化状态覆盖掉服务端初始状态。

  1. 自定义 NextJS 的入口 APP。

因为要在应用组件树的最上层引入 Redux 的 Store,因此需要自定义 NextJS 的入口 APP,这可以通过自定义的 _app 文件来完成。

./pages/_app.js

import React from 'react'
import App from 'next/app'
import { Provider } from 'react-redux'
import withRedux from 'next-redux-wrapper'

import { makeStore } from '../store'

class JWPApp extends App {
  static async getInitialProps({ Component, ctx }) {
    const { store } = ctx

    const pageProps = Component.getInitialProps ? await Component.getInitialProps(ctx) : {};

    return { store, pageProps };
  }

  render() {
    const { store, Component, pageProps } = this.props

    return (
      <Provider store={store}>
        <Component {...pageProps} />
      </Provider>
    )
  }
}

export default withRedux(makeStore)(JWPApp)

这样就完成了 Redux 的引入,在下层组件里可以使用 react-redux 提供的 connect 方法来连接创建的 store,以便获取存放在 store 里的应用状态,或者调用 dispatch 来发送 action 。

使用 REST 风格的 URL

NextJS 的 Router 除了支持传统的带 Query 参数的 URL 格式,也支持 REST 那样的把参数放在路径里的 URL 格式。这是通过地址别名来实现的,只需在使用 Link 组件的时候同时指定 hrefas 属性就可以。href 是真实的页面地址,as 为显示在浏览器地址栏里的地址,如果不指定则跟 href 一致。

下面的例子里任务页的真实地址为 /task/detail?id=1 ,但显示在浏览器的地址为 /tasks/1

<Link
  href={{ pathname: '/task/detail', query: { id: task.id } }}
  as={`/tasks/${task.id}`}
>
  <a>{task.title}</a>
</Link>

上面的实现,如果只是在客户端里点击链接没有什么问题,但如果在任务页刷新页面的话则会报 404 错误。这是因为刷新时页面在服务端渲染,NextJS 默认会去磁盘里寻找 ./pages/tasks/1.js 这个文件,但这个文件并不存在。为了解决这个问题,需要自定义 NextJS 的 Server 来处理虚拟路径。

./server.js

const { parse } = require('url')
const express = require('express')
const next = require('next')

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

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

    server.all('*', (req, res) => {
      const parsedUrl = parse(req.url, true)
      const { pathname } = parsedUrl

      let m = pathname.match(/^\/tasks\/(\d+)$/)
      if (m) {
        parsedUrl.pathname = '/task/detail'
        parsedUrl.query.id = m[1]
      }

      handle(req, res, parsedUrl)
    })

    server.listen(port, (err) => {
      if (err) throw err
      console.log(`> Ready on http://localhost:${port}`)
    })
  })
  .catch(err => {
    console.log('An error occurred, unable to start the server')
    console.log(err)
  })

上面的代码里,使用正则表达式去匹配路径是否为美化后的任务页路径,并且提取出路径里的任务 ID 参数。如果是则替换 pathname 为真实的任务页文件路径,并且把提取出来的 ID 添加到 query 参数集里。这样就解决了虚拟路径页面在服务端的渲染问题。

打包和部署

各个业务功能开发完成和测试通过后,可以执行下面的命令来打包和以生产模式运行应用。

$ npm run build
$ npm run start

也可以编写 Dockerfile 来打包为一个 Docker 镜像,部署起来更加方便。

FROM node:11

WORKDIR /app
COPY . .

RUN npm install
RUN npm run build

EXPOSE 3000

CMD ["npm", "run", "start"]

实例分享

笔者使用 NextJS 和 AntD 开发了一个支付小工具 及未支付 ,解决个人小商户的虚拟商品交易,感兴趣的可以去体验一下。