完美融合 nextjs 和 antd

相信你们在使用nextjs的时候,不免遇到一些坑。其实可能大部分缘由在于 nextjs 作了不少封装,咱们可能不能第一时间搞清楚包括它相关的全部配置,好比其中的webpack配置。我前面也写过 SSR 实现的文章和简单的轮子《实现ssr服务端渲染》,也知道 SSR 要实现为 nextjs 这样的三方框架,仍是会须要经历很复杂编码的。css

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

 

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

 

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

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

 

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

而后我在 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 下须要的组件正则,发现并无效果。然后我经历了各类痛苦,尝试过各类方面的办法,网上也查不出解决方案。好,跳过心酸的部分。github

 

 

 

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

 

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

    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 删除才没有报错的。sass

最后在我彻底能正常运行的时候,仍是尝试删除了它,发现并无报错,由于从理论上来讲,这个重复的loader自己也没有用,所以我给做者提了一个建议,建议去掉这个新loader, 对方说再认真看看。这里:https://github.com/martpie/next-transpile-modules/issues/32。 (事实证实我理解错了,请看文章后文详情)babel

     // 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 和 postcss.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 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
        ); 
      }
    }
  }
});
 
//postcss.config.js
const pxtorem = require("postcss-pxtorem");
module.exports = {
  plugins: [
    pxtorem({
      rootValue: 50,
      unitPrecision: 5,
      propList: ["*"],
      selectorBlackList: [/^\.nop2r/, /^\.am/],//排除antd样式
      replace: true,
      mediaQuery: false,
      minPixelValue: 0
    })
  ]
}

 

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

注意:

若是还会存在 antd 的报错,在 next.config.js 中添加 webpack 配置方法去掉 next-transpile-modules 额外添加的 loader,清空其 include。 这个多余的 loader 确实会致使 bug,或许你在使用的时候此包的代码已经更新。
//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 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){
    config.module.rules.forEach(item=>{
      if(item.loader&&item.loader.loader){
        item.include = []
      }
    })
    return config
  }
});

 

终解:

后来我终于想清楚了,首先 next-transpile-modules 的目的就是让 node_modules 中的包可使用 next-babel-loader ,它的文档第一句就是这个意思,我当时理解错误了。

其次咱们再来讲说 webpack.externals 这个配置,好比 nextjs 默认就是以下这样配置的,它把 node_modules 下的 js 做为一个公共的js来处理,当这样配置之后,webpack 就不会去分析 node_modules 下的 js 的依赖了。

 

好比我本身在 node_modules 里写一个文件夹 @test,里面是一个 index.js,index.js require了同级的 b.js,而后咱们在 nextjs 的项目代码里引入 @test/index.js ,编译时就会报错,报错的行就在 require('b.js') 这里。

再来讲说 next-transpile-modules, 它作了两个事情,第一是从 nextjs 默认的 externals 中,排除掉咱们定义的  transpileModules: ["antd-mobile"],这样 antd-mobile 中的 js 就会被 webpack 正常解析依赖了。然后新建了一个 next-babel-loader ,include 的值是 transpileModules 配置的 ["antd-mobile"]。 因为咱们的 antd-mobile 中的代码不须要被 next-babel-loader 解析,甚至若是使用 next-babel-loader 解析就会报错,所以我前面的配置把它添加的 loader 的 include 给清空了,这样全部的配置就 ok 了。所以咱们只须要它其中的 externals 功能,ok, next.config.js 最终代码以下(  .babelrc 和 postcss.config.js 参照上面不变)

const withLess = require("@zeit/next-less");
const withCss = require("@zeit/next-css");
const withPlugins = require("next-compose-plugins");
const cssLoaderGetLocalIdent = require("css-loader/lib/getLocalIdent.js");
const path = require('path');

module.exports = withPlugins([withLess,withCss], {
  cssModules: true,
  cssLoaderOptions: {
    camelCase: true,
    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){
    if(config.externals){
      const includes = [/antd-mobile/];
      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);
        };
      });
    }
    return config;
  }
});

 

若是没有实现按需加载,最终打包出来的文件会很大,由于包含了整个antd库。 

 

发现的一些问题记录

1.页面切换样式问题

开发环境页面 A 切换到 B 后,B 没有样式。这个状况是在开发模式下才有。
好比我初次启动应用以后,访问 A,A 发现没登陆访 B,这个时候 B 样式加载不出来,页面没样式。若是我在 B 页面刷新一次,让服务端渲染一次,而后 A 再跳到 B 就有样式了。我发如今第一次从 A 跳到 B 的时候,有一个相似这样的一个请求:/_next/static/chunks/styles.js?ts=1557217006063,就是 B 样式的热更新文件。可是实际 _next/static/css/styles.chunk.css 这个文件里没有成功载入 B 的样式。而当咱们用服务端渲染一次 B 页面,也就是在 B 的路由下刷新一次。然后的 chunk.css 就有样式了。

咱们再看看生产环境,生产环境,nextjs 会把全部依赖的 css 打包到一个 chunk.css 文件中,在首次渲染的时候,整个应用的全部样式都已经被载入了,好比 A 和 B 的样式都有了。因此在切换页面的时候,样式都没问题。

依照这个状况看来,开发环境下,样式是被加载到运行时的内存中的,一旦有用服务端渲染 A 页面,A 的样式就会被添加进服务端内存中,再用服务端渲染一次 B 页面,然后请求 chunk.css 就才会有两个页面的样式。问题在于开发环境下的热更新没有起到做用,应该是一个官方的bug。

此 issue 说不是 next 核心的 bug,是三方插件的问题,那么问题应该在next-css, https://github.com/zeit/next.js/issues/4732 。

 
 


        
相关文章
相关标签/搜索