完美融合 nextjs 和 antd

相信你們在使用nextjs的時候,不免遇到一些坑。其實可能大部分緣由在於 nextjs 作了不少封裝,咱們可能不能第一時間搞清楚包括它相關的全部配置,好比其中的 webpack 配置。我前面也寫過 SSR 實現的文章和簡單的輪子《實現ssr服務端渲染》,也知道 SSR 要實現爲 nextjs 這樣的三方框架,仍是會須要經歷很複雜編碼的。css

總歸有時候遇到問題,在網上也查不到一個正確的解決方案。好比,我爲此頭痛幾天的 antd-mobile 按需加載,最開始我沒法正常使用,就只能全局引入 antd-mobile的min.css,這致使我要在頁面加載 164k 的 css 文件,咱們使用 nextjs 就是爲了提高加載速度,這種狀況不能忍啊!node

言歸正傳,先說說我遇到的問題,我使用了 antd-mobile 而且須要對它進行按需加載,下面是官方的文檔,推薦咱們使用 babel-plugin-import。webpack

按照 nextjs 官方文檔咱們在 .babelrc 中添加插件git

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

可當我運行的時候報錯了,報錯以下。最開始感到奇怪,我並無引入這個包,後來發現實際上是 antd-mobile 中有引入它。可是爲何會報錯呢, 便想到應該是 webpack loader 的問題,我認爲是 loader 排除了node_modules。(這裏解釋一下,node_modules 中的包本應該都是打包好的,可是一些狀況下,咱們是直接引入源代碼中的模塊的,那這樣的話咱們就須要讓咱們的 loader 解析 node_modules 中的代碼,然而有些默認配置就是排除了 node_modules 的,這樣就會致使沒法解析)。github

而後我在 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 下須要的組件正則,發現並無效果。然後我經歷了各類痛苦,嘗試過各類方面的辦法,網上也查不出解決方案。好,跳過心酸的部分。web

再後來我開始仔細的一個個看官方的插件,我找到了它:next-transpile-modules,從名稱上來看彷佛和我想要的有點關係。github.com/martpie/nex…api

一看文檔果真,它就是我要找的,它就是解決 node_modules 中代碼不被 loader 解析的問題。我使用了它,這時報錯信息變了(其實後來我弄比較清楚之後就沒有報錯了,可能當時配置改的比較多,哪裏影響到了),我以爲彷佛起到做用了,可是仍是會報錯。因而我便看了一下它的代碼,我終於發現了 webpack.externals 這個配置,原來是這個地方排除了解析外部依賴。若是咱們使用插件 transpile 並配置好 transpileModules: ["antd-mobile"],transpile 內部會生成 includes 正則,在 externals 執行時,會排除掉咱們配置的 node_modules 模塊,所以 antd-mobile 就能被正常解析了,代碼以下sass

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 刪除纔沒有報錯的。bash

最後在我徹底能正常運行的時候,仍是嘗試刪除了它,發現並無報錯,由於從理論上來講,這個重複的loader自己也沒有用,所以我給做者提了一個建議,建議去掉這個新loader, 對方說再認真看看。這裏:github.com/martpie/nex…(事實證實我理解錯了,請看文章後文詳情)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;
  }
});複製代碼

發現的一些問題記錄

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, github.com/zeit/next.j…


關注大詩人公衆號,第一時間獲取最新文章。
相關文章
相關標籤/搜索