likes
comments
collection
share

Next13支持less & 自定义less-module

作者站长头像
站长
· 阅读数 14

支持less

首先安装 next-with-less

npm install next-with-less -D

然后编辑 next.config.js

// next.config.js
const withLess = require("next-with-less");
const path = require('node:path');

const nextConfig = {
  webpack(config, options) {
    config.module.rules.forEach((rule) => {
      const { oneOf } = rule;
      if (oneOf) {
        oneOf.forEach((one) => {
          if (!`${one.issuer?.and}`.includes('_app')) return;
          one.issuer.and = [path.resolve(__dirname)];
        });
      }
    })
    return config;
  }
}

module.exports = withLess(nextConfig);

然后再运行项目,就可以了

自定义less-module

启用全局 less-module 或修改 less-module 匹配规则

默认情况下 less-module 和 css-module 的启用方式一致,都是将样式表文件的命名方式改为 *.module.less,这样样式就是组件级的样式,而不会污染全局。

不过有时候会嫌弃这种命名方式麻烦,如果想直接默认全局启用 less-module 要怎样配置呢?

刚开始我想到的是在 next.config.js 中匹配到对应的 loader 后设置 module: true,但是并没有生效,因为我们的配置被 next-with-less 导出的 withLess 加工过,我就想到了是不是 withLess 的一些处理导致配置不生效,因此我们需要查看 withLess 源码

很快,我们能找到下面的几段内容

// node_modules\next-with-less\dist\index.js
···	--line:93
else if (((_rule$test = rule.test) === null || _rule$test === void 0 ? void 0 : _rule$test.source) === "\\.module\\.(scss|sass)$") {
  sassModuleRule = rule;
} else if (((_rule$test2 = rule.test) === null || _rule$test2 === void 0 ? void 0 : _rule$test2.source) === "(?<!\\.module)\\.(scss|sass)$") {
  sassGlobalRule = rule;
}

···	--line:107
var lessModuleRule = cloneDeep(sassModuleRule);

var configureLessRule = function configureLessRule(rule) {
  rule.test = new RegExp(rule.test.source.replace("(scss|sass)", "less")); // replace sass-loader (last entry) with less-loader

  rule.use.splice(-1, 1, lessLoader);
};

configureLessRule(lessModuleRule);
cssRule.oneOf.splice(cssRule.oneOf.indexOf(sassModuleRule) + 1, 0, lessModuleRule);

可以看到,next-with-less 拷贝了 sass 的配置,然后进行了简单的规则替换,而这个替换是写死的,因此我们没办法通过外部配置进行调整

没办法,要在使用 next-with-less 的基础上默认全局启用 less-module 的话我们就需要修改源码,我们将源码全部拷出来,在我们的主目录下再创建一个 next-with-less.js,然后引用这个文件,即 next.config.js 第一行的导入做调整

//next.config.js
const withLess = require("./next-with-less.js");

我这边还是希望具体的配置写在 next.config.js 里面,而不是写死在 next-with-less 里面,所以改成了下面这样

// next-with-less.js
···--line:24
function withLess(_ref, additionalConfig) {
  var _ref$lessLoaderOption = _ref.lessLoaderOptions,
      lessLoaderOptions = _ref$lessLoaderOption === void 0 ? {} : _ref$lessLoaderOption,
      nextConfig = _objectWithoutProperties(_ref, _excluded);
  
  const { lessConfig } = additionalConfig;

···--line:111

  var configureLessRule = function configureLessRule(rule, isGlobal) {
  if (lessConfig?.moduleTest && !isGlobal) rule.test = lessConfig.moduleTest;
  else {
    if (lessConfig?.globalTest) rule.test = lessConfig.globalTest;
    else rule.test = new RegExp(rule.test.source.replace("(scss|sass)", "less"));
  }

    rule.use.splice(-1, 1, lessLoader);
  };
  
  configureLessRule(lessModuleRule, false);
  cssRule.oneOf.splice(cssRule.oneOf.indexOf(sassModuleRule) + 1, 0, lessModuleRule);
    
  if (sassGlobalRule) {
    var lessGlobalRule = cloneDeep(sassGlobalRule);
    configureLessRule(lessGlobalRule, true);
    cssRule.oneOf.splice(cssRule.oneOf.indexOf(sassGlobalRule) + 1, 0, lessGlobalRule);
  }

···

然后,根据修改的代码,再次对 next.config.js 做调整

// next.config.js
/** @type {import('next').NextConfig} */
const withLess = require("./next-with-less.js");
const path = require('node:path');

const nextConfig = {
  webpack(config) {
    config.module.rules.forEach((rule) => {
      const { oneOf } = rule;
      if (oneOf) {
        oneOf.forEach((one) => {
          if (!`${one.issuer?.and}`.includes('_app')) return;
          one.issuer.and = [path.resolve(__dirname)];
        });
      }
    })
    return config;
  }
}

const additionalConfig = {
  lessConfig: {
    moduleTest: /.less$/,
  }
}

module.exports = withLess(nextConfig, additionalConfig);

这样就全局启用了 less-module 了

我本人并不希望全局启用 less-module,只是希望改一下匹配规则而已,所以我最后的代码是下面这样,仅供参考

// next.config.js
const additionalConfig = {
  lessConfig: {
    moduleTest: /\.m\.less$/,
    globalTest: /(?<!\.m)\.less$/,
  }
}

上面代码就表示,我要使用组件级样式,需要将 less 格式文件改为 *.m.less,仅仅是以 less 结尾的文件依旧是对全局生效

自定义 less-module 命名规则

这部分内容代码我放在了最后,不想看过程的直接跳到最后即可

我们都知道,在 react 中的组件级样式都是在样式类名后面加了一段 hash,从而实现的隔离,比如下面这样的

index_m_cardList__b0R6V

那么要怎么做呢

因为我们的 less 样式最终都是会转换成 css 的,所以一开始我直接去看了 css-loader 的源码,发现了下面的内容

// node_modules\next\dist\build\webpack\loaders\css-loader\src\index.js
···--line:33
let modulesOptions = {
    compileType: rawOptions.icss ? "icss" : "module",
    auto: true,
    mode: "local",
    exportGlobals: false,
    localIdentName: "[hash:base64]",
    localIdentContext: loaderContext.rootContext,
    localIdentHashPrefix: "",
    // eslint-disable-next-line no-undefined
    localIdentRegExp: undefined,
    namedExport: false,
    exportLocalsConvention: "asIs",
    exportOnlyLocals: false
};
···

我注意到了这个 localIndentName,我就先入为主的认为后面那个 hash 值就是因为这个生成的,于是我做了很多尝试配上了这个 localIndentName,最后发现并没有生效,无论怎么改都不会引起丝毫的变化,但是其他东西看起来又不像跟 hash 能扯上关系的,这就激起了我的好奇心,那这东西是拿来干嘛的?

于是我就找,到底哪里用到了这个,最好找到了在 utils 里面用到了

// node_modules\next\dist\build\webpack\loaders\css-loader\src\utils.js
function getModulesPlugins(options, loaderContext, meta) {
    const { mode , getLocalIdent , localIdentName , localIdentContext , localIdentHashPrefix , localIdentRegExp  } = options.modules;
    let plugins = [];
    try {
        plugins = [
            _postcssmodulesvalues.default,
            (0, _postcssmoduleslocalbydefault.default)({
                mode
            }),
            (0, _postcssmodulesextractimports.default)(),
            (0, _postcssmodulesscope.default)({
                generateScopedName (exportName) {
                    return getLocalIdent(loaderContext, localIdentName, exportName, {
                        context: localIdentContext,
                        hashPrefix: localIdentHashPrefix,
                        regExp: localIdentRegExp
                    }, meta);
                },
                exportGlobals: options.modules.exportGlobals
            })
        ];
    } catch (error) {
        loaderContext.emitError(error);
    }
    return plugins;
}

这边看是 getLocalIdent 这个函数用到了,而 getLocalIdent 就是用来生成类名的,这不是对上了吗?为什么不生效?

只能再去看源码,而这个函数是中间处理带过去的,因此我没办法定位到位置,只能到网上查找,找到了真实使用的函数其实是 getCSSModuleLocalIdent,是 react-dev-utils 下面的内容,那就好找了,直接去搜这个包的代码

www.npmjs.com/package/rea…

Next13支持less & 自定义less-module

/**
 * Copyright (c) 2015-present, Facebook, Inc.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

'use strict';

const loaderUtils = require('loader-utils');
const path = require('path');

module.exports = function getLocalIdent(
  context,
  localIdentName,
  localName,
  options
) {
  // Use the filename or folder name, based on some uses the index.js / index.module.(css|scss|sass) project style
  const fileNameOrFolder = context.resourcePath.match(
    /index\.module\.(css|scss|sass)$/
  )
    ? '[folder]'
    : '[name]';
  // Create a hash based on a the file location and class name. Will be unique across a project, and close to globally unique.
  const hash = loaderUtils.getHashDigest(
    path.posix.relative(context.rootContext, context.resourcePath) + localName,
    'md5',
    'base64',
    5
  );
  // Use loaderUtils to find the file or folder name
  const className = loaderUtils.interpolateName(
    context,
    fileNameOrFolder + '_' + localName + '__' + hash,
    options
  );
  // Remove the .module that appears in every classname when based on the file and replace all "." with "_".
  return className.replace('.module_', '_').replace(/\./g, '_');
};

然后发现了原因,人家根本就没用到 localIdentName ,虽然有传参,好嘛,难怪我怎么改这玩意都不生效

Next13支持less & 自定义less-module

然后看了,这个 getLocalIdent 是可以通过配置传过去的,也就是你不需要使用人家默认的,那这样就好解决了,我再次更改 next-with-less,加个函数

我是直接 copy 的这个源码,测试阶段只是做了一个很小的调整,并把这个函数写进配置传了过去,至于怎么传的我会放在最后

const className = loaderUtils.interpolateName(
  context,
  'mino' + '-' + fileNameOrFolder + '_' + localName + '__' + hash,
  // fileNameOrFolder + '_' + localName + '__' + hash,
  options
);

没错,我这边只是在最前面加了个 mino,测试嘛,然后运行代码,诡异的事情发生了,样式类名虽然成功生成了,但是样式根本没生效。为什么?

中间排查了好久,最后想了想,难道这个 getCSSModuleLocalIdent 其实和我找到的不一样?

于是我在 css-loader 下 utils 的代码里面加了一句 console

Next13支持less & 自定义less-module

这是我在控制台看到的源代码

function getCssModuleLocalIdent(context, _, exportName, options) {
    const relativePath = _path.default.relative(context.rootContext, context.resourcePath).replace(/\\+/g, "/");
    // Generate a more meaningful name (parent folder) when the user names the
    // file `index.module.css`.
    const fileNameOrFolder = regexLikeIndexModule.test(relativePath) ? "[folder]" : "[name]";
    // Generate a hash to make the class name unique. 
    const hash = _loaderutils3.default.getHashDigest(Buffer.from(`filePath:${relativePath}#className:${exportName}`), "sha1", "base64", 5);
    // Have webpack interpolate the `[folder]` or `[name]` to its real value.
    return _loaderutils3.default.interpolateName(context, fileNameOrFolder + "_" + exportName + "__" + hash, options).replace(// Webpack name interpolation returns `about.module_root__2oFM9` for
    // `.root {}` inside a file named `about.module.css`. Let's simplify
    // this.
    /\.module_/, "_")// Replace invalid symbols with underscores instead of escaping
    // https://mathiasbynens.be/notes/css-escapes#identifiers-strings
    .replace(/[^a-zA-Z0-9-_]/g, "_")// "they cannot start with a digit, two hyphens, or a hyphen followed by a digit [sic]"
    // https://www.w3.org/TR/CSS21/syndata.html#characters
    .replace(/^(\d|--|-\d)/, "__$1");
}

乍一看好像完全不一样,但是仔细看的话就能发现,实际上差别只体现在最后两行上

Next13支持less & 自定义less-module

Next13支持less & 自定义less-module

啥意思,必须加上这两行吗,然后我加在了我代码里面,最终,我的 getLocalIdent 函数如下

function getLocalIdent(
  context,
  localIdentName,
  localName,
  options,
) {
  const fileNameOrFolder = context.resourcePath.match(
    /index\.module\.(css|scss|sass)$/
  )
    ? '[folder]'
    : '[name]';
  const hash = loaderUtils.getHashDigest(
    path.posix.relative(context.rootContext, context.resourcePath) + localName,
    'md5',
    'base64',
    5
  );
  const className = loaderUtils.interpolateName(
    context,
    'mino' + '-' + fileNameOrFolder + '_' + localName + '__' + hash,
    // fileNameOrFolder + '_' + localName + '__' + hash,
    options
  );
  return className
    .replace(/\.module_/, "_")
    .replace(/[^a-zA-Z0-9-_]/g, "_")
    .replace(/^(\d|--|-\d)/, "__$1");
}

最后我的页面

Next13支持less & 自定义less-module

结果居然真的可以,再检查下页面元素

Next13支持less & 自定义less-module

和我定义的命名规则完全一样,这块确实疑惑,等我以后有时间再去深究为什么要加这两行吧


总结:如何自定义 less-module 命名规则

自定义一个命名规则的函数 getLocalIdent 传进配置里面即可,getLocalIdent 的代码我上面已经提供了,怎么传的可以参考我下面的代码

修改我们上面提供的 next-with-less 源码,找到 configureLessRule ,改成下面的内容

// next-with-less.js
···--line:114
var configureLessRule = function configureLessRule(rule, isGlobal) {
  if (lessConfig?.moduleTest && !isGlobal) {
    rule.test = lessConfig.moduleTest;
    if (lessConfig?.getLocalIdent) {
      for (let item of rule.use) {
        if (typeof item.options.modules === 'object' && item.options.modules) {
          item.options.modules = {
            ...item.options.modules,
            getLocalIdent: lessConfig.getLocalIdent,
          }
        }
      }
    }
  } else {
    if (lessConfig?.globalTest) rule.test = lessConfig.globalTest;
    else rule.test = new RegExp(rule.test.source.replace("(scss|sass)", "less"));
  }

  rule.use.splice(-1, 1, lessLoader);
};

其中 getLocalIdent 我其实也是写在这里面的,在最后一起导出的。

module.exports = {
  withLess,
  getLocalIdent,
};

大家可以依据自己的习惯来,看要把这个函数放哪

因为我是这样导出的,所以我 next.config.js 中也做了调整,最终代码如下

// next.config.js
/** @type {import('next').NextConfig} */
const { withLess, getLocalIdent } = require("./next-with-less.js");
const path = require('node:path');

const nextConfig = {
  webpack(config) {
    config.module.rules.forEach((rule) => {
      const { oneOf } = rule;
      if (oneOf) {
        oneOf.forEach((one) => {
          if (!`${one.issuer?.and}`.includes('_app')) return;
          one.issuer.and = [path.resolve(__dirname)];
        });
      }
    })
    return config;
  }
}

const additionalConfig = {
  lessConfig: {
    moduleTest: /\.m\.less$/,
    globalTest: /(?<!\.m)\.less$/,
    getLocalIdent,
  }
}

module.exports = withLess(nextConfig, additionalConfig);