完美融合 nextjs 和 antd 实现ssr服务端渲染

相信大家在使用nextjs的时候,难免遇到一些坑。其实可能大部分原因在于 nextjs 做了很多封装,我们可能不能第一时间搞清楚包括它相关的所有配置,比如其中的webpack配置。我前面也写过 SSR 实现的文章和简单的轮子《实现ssr服务端渲染》,也知道 SSR 要实现为 nextjs 这样的三方框架,还是会需要经历很复杂编码的。

总归有时候遇到问题,在网上也查不到一个正确的解决方案。比如,我为此头痛几天的 antd-mobile 按需加载,最开始我无法正常使用,就只能全局引入 antd-mobile的min.css,这导致我要在页面加载 164k 的 css 文件,我们使用 nextjs 就是为了提升加载速度,这种情况不能忍啊!

 

言归正传,先说说我遇到的问题,我使用了 antd-mobile 并且需要对它进行按需加载,下面是官方的文档,推荐我们使用 babel-plugin-import。

按照 nextjs 官方文档我们在 .babelrc 中添加插件

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

可当我运行的时候报错了,报错如下。最开始感到奇怪,我并没有引入这个包,后来发现其实是 antd-mobile 中有引入它。但是为什么会报错呢, 便想到应该是 webpack loader 的问题,我认为是 loader 排除了node_modules。(这里解释一下,node_modules 中的包本应该都是打包好的,但是一些情况下,我们是直接引入源代码中的模块的,那这样的话我们就需要让我们的 loader 解析 node_modules 中的代码,然而有些默认配置就是排除了 node_modules 的,这样就会导致无法解析)。

然后我在 next.config.js 中,定义 webpack 方法,打印出 webpack 配置。 nextjs 中的 webpack 配置大致是引入了一个 next-babel-loader 这样的 loader,而我们使用next-css、next-less或者next-sass等插件,相关的 loader 会被 push 到 rules 中。 核心的loader 就是 next-babel-loader。然而我在其参数中并没有发现 exclude, 到是有 include,而后我往 include 里添加 node_modules 下需要的组件正则,发现并没有效果。而后我经历了各种痛苦,尝试过各种方面的办法,网上也查不出解决方案。好,跳过心酸的部分。

再后来我开始仔细的一个个看官方的插件,我找到了它:next-transpile-modules,从名称上来看似乎和我想要的有点关系。https://github.com/martpie/next-transpile-modules

一看文档果然,它就是我要找的,它就是解决 node_modules 中代码不被 loader 解析的问题。我使用了它,这时报错信息变了(其实后来我弄比较清楚以后就没有报错了,可能当时配置改的比较多,哪里影响到了),我觉得似乎起到作用了,但是还是会报错。于是我便看了一下它的代码,我终于发现了 webpack.externals 这个配置,原来是这个地方排除了解析外部依赖。如果我们使用插件 transpile 并配置好 transpileModules: ["antd-mobile"],transpile 内部会生成 includes 正则,在 externals 执行时,会排除掉我们配置的 node_modules 模块,因此 antd-mobile 就能被正常解析了,代码如下

    if (config.externals) {
        config.externals = config.externals.map(external => {
          if (typeof external !== 'function') return external;
          return (ctx, req, cb) => {
            return includes.find(include =>
              req.startsWith('.')
                ? include.test(path.resolve(ctx, req))
                : include.test(req)
            )
              ? cb()
              : external(ctx, req, cb);
          };
        });
      }

而后它又添加了一个 next-babel-loader 到 rules 中,现在其实有两个 next-babel-loader 在 webpack 配置中。我认为这个配置是多余的,并且就是之前我可能哪里没配置对,这个多余的 loader 让我编译报错了,我把它生成的多余 loader 删除才没有报错的。

最后在我完全能正常运行的时候,还是尝试删除了它,发现并没有报错,因为从理论上来说,这个重复的loader本身也没有用,因此我给作者提了一个建议,建议去掉这个新loader, 对方说再认真看看。这里:https://github.com/martpie/next-transpile-modules/issues/32

     // Add a rule to include and parse all modules
      config.module.rules.push({
        test: /\.+(js|jsx|ts|tsx)$/,
        loader: options.defaultLoaders.babel,
        include: includes
      });

我当前使用的 next 是8.x,在6.x里,我看了下它确实是用的 exclude 来排除的 node_modules,到 8 以后改为 externals 了,一定有它官方的道理吧。如果你用的是6.x,你可以尝试修改 exclude,不过建议大家都升级为 8 吧,很平滑的。

第二个问题,可能也是大家比较常见的,那就是 cssModules。官方代码是这样的

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

完全没有问题,可以正常使用。只是 antd-mobile 的 class 名称也被 cssModules 给改了,但是组件 dom 中的 class 名称并没有被修改,这样样式就不起作用了。ok,没有问题,这个简单,我们使用 css-loader api 中的 options.getLocalIdent,来控制修改 class 名称。代码大致如下

  const cssLoaderGetLocalIdent = require("css-loader/lib/getLocalIdent.js");
  /*.....*/
  cssLoaderOptions: {
    localIdentName: "[local]___[hash:base64:5]",
    getLocalIdent: (context, localIdentName, localName, options) => {
      let hz = context.resourcePath.replace(context.rootContext, "");
      if (/node_modules/.test(hz)) {
        return localName;
      } else {
        return cssLoaderGetLocalIdent(
          context,
          localIdentName,
          localName,
          options
        );
      }
    }
  },

通过阅读 css-loader 源码,发现其内部运行过程,它内部有一个 css-loader/lib/getLocalIdent.js 方法,如果用户自定义了 getLocalIdent 方法,它在编译 cssmodules 时,便会用用户定义的方法,否则使用自带的方法。我的想法就是通过自定义 getLocalIdent, 正则判断 node_modules,也就是当前样式如果是来自于 node_modules 中文件的话,我返回它本身的名称,就是不改动它,而它是我们的源码的话,我执行 css-loader 本身的 getLocalIdent 方法。这样就既使我们自己的代码能被 cssmodules,而三方库的代码不被 cssmodules 影响。

最后附上两个配置文件 .babelrc 和 next-config.js

//.babelrc 
{
  "presets": ["next/babel"],
  "plugins": [
    ["import", { "libraryName": "antd-mobile", "style": true }]
  ]
}
//next.config.js
const withLess = require("@zeit/next-less");
const withCss = require("@zeit/next-css");
const withPlugins = require("next-compose-plugins");
const withTM = require('next-transpile-modules');
const pxtorem = require("postcss-pxtorem");
const cssLoaderGetLocalIdent = require("css-loader/lib/getLocalIdent.js");

module.exports = withPlugins([withCss, withLess,withTM], {
  transpileModules: ["antd-mobile"], 
  cssModules: true,
  cssLoaderOptions: {
    localIdentName: "[local]___[hash:base64:5]",
    getLocalIdent: (context, localIdentName, localName, options) => {
      let hz = context.resourcePath.replace(context.rootContext, "");
      if (/node_modules/.test(hz)) {
        return localName;
      } else {
        return cssLoaderGetLocalIdent(
          context,
          localIdentName,
          localName,
          options
        ); 
      }
    }
  },
  webpack(config, options) {
    //less设置rem
    config.module.rules.push({
      test: /\.less$/,
      exclude: /node_modules/,
      loader: "postcss-loader",
      query: {
        plugins: [
          pxtorem({
            rootValue: 50,
            unitPrecision: 5,
            propList: ["*"],
            selectorBlackList: [/^\.nop2r/],
            replace: true,
            mediaQuery: false,
            minPixelValue: 0
          })
        ]
      }
    });
    return config;
  }
});

pxtorem是转换px为rem,有的需要的自取,如果此方案解决了你的问题,点个赞吧~



 

猜你喜欢

转载自www.cnblogs.com/1wen/p/10793868.html