自动注入 Less 全局变量

2025-04-25 前端

一、为什么要自动注入 Less 全局变量

在大型前端项目中,我们通常会在 Less 中定义一些全局变量——颜色、字体、间距、断点等。比如:

// variables.less
@primary-color: #1890ff;
@font-size-base: 14px;
@border-radius: 6px;
@spacing-unit: 8px;

但问题是:每个使用这些变量的 .less 文件都需要手动 @import:

// ❌ 每个组件都要写这一行
@import '../../styles/variables.less';

.my-component {
  color: @primary-color;
}

这就是重复劳动的源头。自动注入 Less 全局变量,就是在构建工具层面,自动在每个 Less 文件开头注入指定的变量文件,让所有组件文件都能直接使用这些变量,无需手动 import。

┌─────────────────────────────────────┐
│         构建工具(Webpack/Vite)       │
│                                     │
│  编译每个 .less 文件时,自动在顶部追加:  │
│  @import "variables.less";          │
│                                     │
│  组件 A.less   → 自动注入 → 正常编译 ✅ │
│  组件 B.less   → 自动注入 → 正常编译 ✅ │
│  组件 C.less   → 自动注入 → 正常编译 ✅ │
└─────────────────────────────────────┘

二、Webpack 配置

2.1 less-loader v6 及以下:prependData

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.less$/,
        use: [
          'style-loader',
          'css-loader',
          {
            loader: 'less-loader',
            options: {
              lessOptions: {
                javascriptEnabled: true,
              },
              // ⬇️ 自动注入全局变量
              prependData: `
                @import "@/styles/variables.less";
                @import "@/styles/mixins.less";
              `,
            },
          },
        ],
      },
    ],
  },
};

2.2 less-loader v7+: additionalData

less-loader v7 将 prependData 重命名为 additionalData,功能不变:

// webpack.config.js  (less-loader v7+)
{
  loader: 'less-loader',
  options: {
    additionalData: `
      @import "@/styles/variables.less";
      @import "@/styles/mixins.less";
    `,
  },
}

2.3 配合 chainWebpack(Vue CLI 3/4)

// vue.config.js
module.exports = {
  chainWebpack: config => {
    const oneOfsMap = config.module.rule('less').oneOfs.store
    oneOfsMap.forEach(item => {
      item
        .use('less-loader')
        .loader('less-loader')
        .tap(options => {
          options.additionalData = `
            @import "~@/styles/variables.less";
            @import "~@/styles/mixins.less";
          `
          return options
        })
    })
  },
}

注意:Vue CLI 内部有多条 Less 规则(普通 .less、<style lang="less"> 等),需要遍历所有 oneOf 逐一注入。


三、Vite 配置

Vite 天生支持 CSS 预处理器,配置更简洁:

// vite.config.ts
import { defineConfig } from 'vite'

export default defineConfig({
  css: {
    preprocessorOptions: {
      less: {
        additionalData: `
          @import "@/styles/variables.less";
          @import "@/styles/mixins.less";
        `,
        javascriptEnabled: true,
      },
    },
  },
})

如果使用了 @ 别名,需要确保 Vite 的 resolve 别名已配置:

import { fileURLToPath, URL } from 'node:url'

export default defineConfig({
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
  css: {
    preprocessorOptions: {
      less: {
        additionalData: `@import "@/styles/variables.less";`,
      },
    },
  },
})

Vite 中多文件注入

需要注入多个文件时,用分号或换行分隔:

additionalData: `
  @import "@/styles/variables.less";
  @import "@/styles/themes/default.less";
`

⚠️ Vite 与 Webpack 的区别:Vite 的 additionalData 只在编译 Less 文件时注入,不影响 JavaScript 中的 Less 变量引用。如果你在 JS 中动态使用 Less 变量(如 less.modifyVars()),仍需另做处理。


四、Demo:主题系统实战

假设我们要构建一个支持深色/浅色主题切换的组件库,自动注入全局变量就是最优雅的方案。

4.1 目录结构

src/
├── styles/
│   ├── variables.less        # 全局设计变量
│   ├── themes/
│   │   ├── light.less        # 浅色主题覆盖
│   │   └── dark.less         # 深色主题覆盖
│   └── mixins.less           # 混入工具
├── components/
│   ├── Button/
│   │   ├── index.tsx
│   │   └── style.less        # 直接使用变量,无需 import
│   └── Card/
│       ├── index.tsx
│       └── style.less

4.2 全局变量文件

4.3 组件直接使用(无需 import)

// components/Card/style.less
// ✅ 同样无需 import

.card {
  background: @bg-color;
  border: 1px solid @border-color;
  border-radius: @radius-md;
  padding: @spacing-lg;
  box-shadow: @shadow-sm;

  &__title {
    font-size: 16px;
    font-weight: 600;
    color: @text-color;
    margin-bottom: @spacing-md;
  }

  &__content {
    color: @text-color-secondary;
    line-height: 1.6;
  }
}

4.4 主题切换实现

通过 CSS 自定义属性(CSS Variables)实现运行时主题切换:

// styles/themes/light.less
// 浅色主题:将 Less 变量映射为 CSS 自定义属性

:root,
[data-theme='light'] {
  --primary-color: @primary-color;
  --text-color: @text-color;
  --bg-color: @bg-color;
  --border-color: @border-color;
  --shadow-sm: @shadow-sm;
}
// styles/themes/dark.less
// 深色主题:覆盖 CSS 自定义属性

[data-theme='dark'] {
  --primary-color: #177ddc;
  --text-color: rgba(255, 255, 255, 0.85);
  --bg-color: #141414;
  --border-color: #434343;
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
}

JS 端切换主题:

// theme.ts
function setTheme(theme: 'light' | 'dark') {
  document.documentElement.setAttribute('data-theme', theme)
  localStorage.setItem('theme', theme)
}

4.5 Vite 配置中的注入

// vite.config.ts
import { defineConfig } from 'vite'
import path from 'path'

export default defineConfig({
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src'),
    },
  },
  css: {
    preprocessorOptions: {
      less: {
        additionalData: `
          @import "@/styles/variables.less";
          @import "@/styles/mixins.less";
        `,
        javascriptEnabled: true,
        // 开启 math 严格模式,避免 Less 3.x/4.x 兼容性问题
        math: 'always',
      },
    },
  },
})

五、高级技巧

5.1 按环境注入不同变量

不同环境(开发/生产)可能需要不同的变量值:

// vite.config.ts
const isDev = process.env.NODE_ENV === 'development'

export default defineConfig({
  css: {
    preprocessorOptions: {
      less: {
        additionalData: `
          @import "@/styles/variables.less";
          ${isDev ? `@import "@/styles/dev-overrides.less";` : ''}
        `,
      },
    },
  },
})

5.2 排除第三方库

自动注入可能会影响 node_modules 中的第三方 Less 文件(如 Ant Design),需要排除:

Vite 中通过 include / exclude 参数控制:

// vite.config.ts
import { defineConfig } from 'vite'

export default defineConfig({
  css: {
    preprocessorOptions: {
      less: {
        additionalData: (content, filePath) => {
          // 不对 node_modules 中的文件注入
          if (filePath.includes('node_modules')) {
            return content
          }
          return `@import "@/styles/variables.less";\n${content}`
        },
        // ⬆️ additionalData 支持函数签名
      },
    },
  },
})

Vite 的 additionalData 支持函数签名:

additionalData: (content: string, filePath: string) => string

这让你可以按文件路径有选择地注入。

5.3 使用 modifyVars 运行时覆盖

部分场景需要在运行时修改变量(如换肤):

// webpack.config.js
{
  loader: 'less-loader',
  options: {
    lessOptions: {
      modifyVars: {
        'primary-color': '#722ed1',    // 覆盖主色
        'font-size-base': '16px',
      },
      javascriptEnabled: true,
    },
    additionalData: `@import "@/styles/variables.less";`,
  },
}

modifyVars 的优先级高于 additionalData 中 @import 的变量,适合做构建时的主题定制。


六、注意事项

6.1 变量重复定义问题

自动注入后,组件文件中不要重复 import 全局变量文件:

// ❌ 错误:重复导入导致编译警告或变量冲突
@import '../../styles/variables.less';

.my-class {
  color: @primary-color;
}

// ✅ 正确:直接使用,无需 import
.my-class {
  color: @primary-color;
}

6.2 副作用问题

自动注入的文件中不要包含实际的 CSS 输出:

// ❌ 错误:全局变量文件中不要有 CSS 输出
@primary-color: #1890ff;
body {                    // ← 这会在每个组件文件中重复输出
  font-family: sans-serif;
}

// ✅ 正确:只放变量定义和 mixin
@primary-color: #1890ff;
@font-family: sans-serif;

否则每个编译后的 CSS 文件中都会重复输出这些样式,导致包体积膨胀。

6.3 Less 版本兼容性

Less 版本 additionalData math 模式 注意
3.x ✅ 支持 默认 parens-division 除法用 () 包裹
4.x ✅ 支持 需设置 math: 'always' 不加 () 的除法也支持
// Less 4.x 推荐配置
css: {
  preprocessorOptions: {
    less: {
      math: 'always',
      additionalData: `@import "@/styles/variables.less";`,
    },
  },
},

6.4 性能影响

自动注入会对每个 Less 文件增加一次 @import 解析,但影响通常可忽略。如果项目极大(数千个 Less 文件),可以:

  • 只对 src/ 目录注入,排除 node_modules
  • 合并全局变量为一个文件(减少 @import 次数)
  • 使用 CSS 自定义属性替代部分 Less 变量

七、总结

配置方法 Webpack Vite Vue CLI
参数名 prependData / additionalData additionalData additionalData
支持函数 ❌ ✅(按文件路径选择注入) ❌
排除 node_modules 需拆分 rule 函数签名判断 需 chainWebpack
配置位置 less-loader options css.preprocessorOptions.less chainWebpack

核心要点:

  1. 自动注入让组件代码更干净,省去重复的 @import
  2. Webpack 用 prependData / additionalData,Vite 统一用 additionalData
  3. 变量文件不要包含实际 CSS 输出,避免样式重复
  4. 排除 node_modules,避免干扰第三方库
  5. 与 CSS 自定义属性配合,可以实现运行时主题切换
  6. additionalData 支持函数签名(Vite),可以实现按需注入

自动注入 Less 全局变量是小投入大回报的工程化改造——一次配置,永远省去重复 import,让代码更聚焦于业务本身。