Next.js是目前用于实现 React 服务端渲染框架中的比较流行的一个,得力于ZEIT的维护,使得相对于其它的一些框架,不论是在文档还是配套上面,它都比较齐全。

官方就提供了learnnextjs这样的交互式入门教程,这里就不多说了(结合文档效果更好)。

既然是实战,这里就说点自己遇到的问题及解决方案,下面将问题总的划分为几个分类:

JS 方面的坑

使用jsx作为文件后缀?

一开始笔者习惯性的使用jsx作为文件后缀,发现控制台中老是报错,说是模块没有找到,这种一看就是配置问题,需要修改next.config.js中 webpack 的配置,增加extensionsjsx还有对应的babel-loader的配置,结果发现完全不起作用,看来是 next.js 中对于相关文件做了特殊的处理,目前来看只能使用*.js作为文件后缀了。

官方目前的回复也是这样,暂时就支持*.js作为后缀,具体可以看一下这个issue

JS 中读取process.env.*返回undefined

如果直接用node执行*.js文件,那么cross-env就比较合适,这个包帮我们处理的跨平台时env定义的问题,但是next.js是依赖于构建工具webpack的,换句话说,如果直接在package.jsonscripts中使用cross-env是无法直接影响webpack的处理结果的,还需要用到webpack.DefinePlugin这个插件来做一次定义,next.js提供了一个简单的例子核心代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
webpack: (config) => {
  config.plugins.forEach((plugin) => {
    if (plugin.constructor.name === "DefinePlugin") {
      plugin.definitions["process.env.SECRET"] = JSON.stringify(
        process.env.SECRET
      );
    }
  });

  return config;
};

另外,这只是其中的一个解决方法,next.js还提供了一个基于Babeldotenv的例子,原理就是在 Babel 处理阶段就将代码中的process.env.*替换处理。

使用babel处理node_modules中部分 JS 的方法?

为了兼容 Android4.3,就必须要用 babel 结合相关的工具对 ES6 做兼容性处理,但是,默认的next.config.js中关于jsx的处理一般都会设置exclude过滤掉node_modules,就像这样的配置:

1
2
3
4
5
{
  test: /\.(js|jsx)$/,
  use: ['babel-loader'],
  exclude: /node_modules/,
}

next.js的 issues 中有不少关于这个的讨论,目前实践下来,最简单的就是直接修改exclude属性即可:

1
2
3
4
5
6
7
{
  test: /\.(js|jsx)$/,
  use: ['babel-loader'],
  // 这里对react及react-dom 2个做了过滤处理
  // 也就是说这2个会被babel做处理
  exclude: /node_modules\/(?!(react|react-dom)\/).*/
}

当然,babel 还需要结合.babelrc.browserslistrc这些配置项及babel-plugin-transform-runtime这样的工具,如果需要了解更多,可以参考 Babel 笔记 这篇文章。

Static 静态资源方面的坑

官网教程中并没有全面的介绍如何控制好静态资源的加载及维护,下面就说说一些常见的问题。

如何引入图片、字体等静态资源?

webpack 及配套的*-loader,使得我们可以很方便的在 JS 文件中通过import载入各种静态资源,不过 next.js 基础中并未对图片、字体等文件做特殊配置,所以需要我们手动在next.config.js中增加 webpack 的相关配置:

 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
31
32
33
34
35
{
  test: /\.(png|jpe?g|gif)(\?.*)?$/,
  use: [{
      loader: 'emit-file-loader',
      options: {
        name: 'dist/[path][name].[ext]',
      },
    },
    {
      loader: 'url-loader',
      options: {
        limit: 2000,
        outputPath: 'static/images/',
        publicPath: '/_next/',
      },
    },
  ],
}, {
  test: /\.(woff2?|eot|ttf|otf|svg)(\?.*)?$/,
  use: [{
      loader: 'emit-file-loader',
      options: {
        name: 'dist/[path][name].[ext]',
      },
    },
    {
      loader: 'url-loader',
      options: {
        limit: 2000,
        outputPath: 'static/fonts/',
        publicPath: '/_next/',
      },
    },
  ],
}

如何引用全局静态资源?

官网的推荐是在根目录的static文件夹中放入即可,后续不论是在 JS 还是 CSS 文件中,只需要写绝对路径/static/{path_to_file}即可,next.js 帮助做了一些类似于路由映射的处理,很遗憾,官方并未给出 cache 的方案,目前来看有 2 中思路

1. 基于 URL 中param的 cache 方案

1
2
3
4
5
<link
  rel="stylesheet"
  type="text/css"
  href={`/static/styles/app.css?${this.props.__NEXT_DATA__.buildStats["app.js"].hash}`}
/>

2. 基于 URL 中path结合脚本迁移文件路*(暂时是个思路,未具体实现)*

1
2
3
4
5
<link
  rel="stylesheet"
  type="text/css"
  href={`/_next/${this.props.__NEXT_DATA__.buildStats["app.js"].hash}/app.css`}
/>

文件迁移脚本

jq是一个轻量级的命令行 JSON 处理工具

1
2
3
#!/usr/bin/env bash
NEXT_BUILD_ID=`cat .next/build-stats.json | jq -r '.[][]'`
cp -Rf ./static/* ./_next/${NEXT_BUILD_ID}/

CSS 方面的坑

官方提供了很多 CSS 的示例,但是个人觉得这个是踩坑最多的地方

实现CSS Modules

得益于css-loader,如果单单是做客户端渲染,只需要在 webpack 的配置中启用modules配置即可,从而简简单单的实现BEM,但是 Next.js 官方并不支持该方案,具体可以参考这个issue,虽然大家也都集思广益,造出了各种方案,但是具体实施起来效果都不怎么理想,反倒是官方推荐的styled-jsx方案,结合postcss之后,成了不错的选择。

with-styled-jsx-plugins这个例子已经实现了基础的配置,你只需要增加postcss.config.js扩展一下即可:

 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
31
32
33
34
35
36
37
38
39
/**
 * postcss.config.js
 *
 * @see https://github.com/zeit/next.js/tree/master/examples/with-global-stylesheet/
 */
const postcssEasyImport = require("postcss-easy-import");
const postcssCssnext = require("postcss-cssnext");
const cssnano = require("cssnano");
const lost = require("lost");

const postCSSPluginCombinations = [
  // keep this first
  // https://github.com/TrySound/postcss-easy-import#options
  postcssEasyImport({
    // prefix: '_',
  }),
  lost(),
  postcssCssnext({
    // so imports are auto-prefixed too
    // @see http://robin-front.github.io/2016/04/09/postCSS-loader%E9%85%8D%E7%BD%AE/
    browsers: ["> 1% in CN", "last 2 versions"],
    features: {
      rem: false,
    },
  }),
];

module.exports = {
  plugins:
    process.env.NODE_ENV === "production"
      ? [
          ...postCSSPluginCombinations,
          cssnano({
            autoprefixer: false,
            discardUnused: { fontFace: false },
          }),
        ]
      : postCSSPluginCombinations,
};

styled-jsx与 babel 的冲突?

之前在babel的生产环境发布前,会利用一些plugin优化 react 的产出代码,其中最常用的就是transform-react-remove-prop-typestransform-react-constant-elementstransform-react-inline-elements,这个时候 styled-jsx对于<style jsx>{***}</style>中的处理就会与**transform-react-constant-elementstransform-react-inline-elements**冲突了,导致无法正常处理scopeoptimized的功能,这里建议直接将transform-react-constant-elementstransform-react-inline-elements去除即可。

cssnano处理后@font-face内容缺失?

一般现在都会在上生产环境前使用 cssnano 对 CSS 文件做一次压缩优化,不过现在碰到一个问题,在处理后,@font-face的内容缺失了,cssnano 的 issue 中有人提供了解决方式:禁用其对@font-face的优化即可。

1
2
3
4
5
6
7
cssnano({
  autoprefixer: false,
  // 禁用对`@font-face`的优化
  discardUnused: {
    fontFace: false,
  },
});

首次渲染pages/_document.js,全局未能生效?

next.js 推荐使用styled-jsx处理 css,所以在_document.js中想当然的使用了下面的写法:

1
2
3
<style jsx global>
  {mainStyles}
</style>

这样就直接导致了一个问题,在第一次访问服务的时候,发现mainStyles中的内容并未被加载并执行,next.js 的issues 中有人也已经反映过这个问题并且给出了解决方案,简单来说就是直接使用<style>加载即可,不要使用<style jsx global>