首页
/ Storybook @storybook/nextjs 框架下通过 webpackFinal 接入 SVGR 渲染 SVG 组件的完整配置指南

Storybook @storybook/nextjs 框架下通过 webpackFinal 接入 SVGR 渲染 SVG 组件的完整配置指南

2026-09-07 15:54:56作者:韦蓉瑛

本篇指南基于 Storybook 官方文档中的 SVGR 配置片段,讲解在使用 @storybook/nextjs 框架时,如何通过 .storybook/main.js|ts 中的 webpackFinal 钩子把 @svgr/webpack 接入构建流程,让 import Logo from './logo.svg' 直接得到可渲染的 React 组件。读完本文,你能理解 Next.js 框架为何会改写图片规则、这段配置每一行的作用,以及它在框架源码中的对应实现,从而可以在自己的 Next.js 项目中复制并验证这套方案。

背景:为什么 Next.js 框架下的 SVG 导入不能直接当组件用

在常规 Vite/webpack 项目中,配合 @svgr/webpack 后 SVG 可以像组件一样被导入使用。但在 @storybook/nextjs 框架中,为了还原 Next.js 对静态资源导入的行为(import img from './x.png' 得到 { src, width, height, blurDataURL } 这样的对象),框架会主动改写 webpack 的图片处理规则。

从源码结构看,这一改写发生在框架的 webpackFinal 预设链中。preset.ts 里的 webpackFinal 会依次调用字体、CSS、图片、styled-jsx 等一系列 configure* 函数,其中 第 184 行的 configureImages(baseConfig, nextConfig) 就是图片规则的来源。

images/webpack.ts 中的 configureStaticImageImport 做了两件事:

  1. 找到现有能匹配 test.jpg 的 asset 规则,把它的 test 改写为 /\.(apng|eot|otf|ttf|woff|woff2|cur|ani|pdf)(\?.*)?$/,即只保留字体、PDF 等格式(见 第 48 行);
  2. 新增一条覆盖 png|jpg|jpeg|gif|webp|avif|ico|bmp|svg 的规则,交给 @storybook/nextjs/next-image-loader-stub 处理(见 第 50-62 行)。

而这个 stub loader 的行为,正是 SVG 无法直接作为组件使用的根因。next-image-loader-stub.ts 会把文件原样 emit 出去,并返回形如 export default { src, height, width, blurDataURL } 的代码——也就是说,在默认配置下导入 .svg 得到的是一个字符串/对象,而不是 React 组件。框架模板中的 typings.d.ts 也印证了这一点:它把 *.svg 模块声明为 const content: string

因此,若希望 SVG 图标在 Story 中像组件一样使用,就必须:让框架的 stub 规则排除 .svg 文件,再追加一条把 .svg 交给 @svgr/webpack 的新规则。由于框架预设的 webpackFinal 先于用户配置执行,你在 main.js|ts 里编写的 webpackFinal 拿到的 config.module.rules 中已经包含了上述框架规则,这正是该方案可行、且文档示例中能用 rules.find 直接找到“现有 image rule”的原因。

完整配置示例

前置条件:项目中已安装 @svgr/webpack(作为开发依赖,例如 yarn add -D @svgr/webpack)。以下配置来源于官方文档 nextjs-configure-svgr.md,并被 Next.js 框架指南 的 “Custom Webpack config” 一节收录。

.storybook/main.js(JavaScript 配置)

export default {
  // ...
  webpackFinal: async (config) => {
    config.module = config.module || {};
    config.module.rules = config.module.rules || [];

    // This modifies the existing image rule to exclude .svg files
    // since you want to handle those files with @svgr/webpack
    const imageRule = config.module.rules.find((rule) => rule?.['test']?.test('.svg'));
    if (imageRule) {
      imageRule['exclude'] = /\.svg$/;
    }

    // Configure .svg files to be loaded with @svgr/webpack
    config.module.rules.push({
      test: /\.svg$/,
      use: ['@svgr/webpack'],
    });

    return config;
  },
};

.storybook/main.ts(TypeScript 配置)

import type { StorybookConfig } from '@storybook/nextjs';

const config: StorybookConfig = {
  // ...
  webpackFinal: async (config) => {
    config.module = config.module || {};
    config.module.rules = config.module.rules || [];

    // This modifies the existing image rule to exclude .svg files
    // since you want to handle those files with @svgr/webpack
    const imageRule = config.module.rules.find((rule) => rule?.['test']?.test('.svg'));
    if (imageRule) {
      imageRule['exclude'] = /\.svg$/;
    }

    // Configure .svg files to be loaded with @svgr/webpack
    config.module.rules.push({
      test: /\.svg$/,
      use: ['@svgr/webpack'],
    });

    return config;
  },
};

export default config;

使用 defineMain 的配置写法(TS / JS 两个版本)

新版框架配置建议通过 defineMain 获得类型推断。注意片段中的注释:把 your-framework 替换为实际使用的 nextjsnextjs-vite

// Replace your-framework with nextjs or nextjs-vite
import { defineMain } from '@storybook/your-framework/node';

export default defineMain({
  // ...
  webpackFinal: async (config) => {
    config.module = config.module || {};
    config.module.rules = config.module.rules || [];

    // This modifies the existing image rule to exclude .svg files
    // since you want to handle those files with @svgr/webpack
    const imageRule = config.module.rules.find((rule) => rule?.['test']?.test('.svg'));
    if (imageRule) {
      imageRule['exclude'] = /\.svg$/;
    }

    // Configure .svg files to be loaded with @svgr/webpack
    config.module.rules.push({
      test: /\.svg$/,
      use: ['@svgr/webpack'],
    });

    return config;
  },
});
// Replace your-framework with nextjs or nextjs-vite
import { defineMain } from '@storybook/your-framework/node';

export default defineMain({
  // ...
  webpackFinal: async (config) => {
    config.module = config.module || {};
    config.module.rules = config.module.rules || [];

    // This modifies the existing image rule to exclude .svg files
    // since you want to handle those files with @svgr/webpack
    const imageRule = config.module.rules.find((rule) => rule?.['test']?.test('.svg'));
    if (imageRule) {
      imageRule['exclude'] = /\.svg$/;
    }

    // Configure .svg files to be loaded with @svgr/webpack
    config.module.rules.push({
      test: /\.svg$/,
      use: ['@svgr/webpack'],
    });

    return config;
  },
});

逐行解析这段配置

结合 框架源码 逐行看,这段 webpackFinal 的每一步都在针对框架已注入的规则做“外科手术式”修改:

  1. config.module = config.module || {};config.module.rules = config.module.rules || [];:防御性兜底,确保 module.rules 一定存在,避免在异常配置下抛错。
  2. config.module.rules.find((rule) => rule?.['test']?.test('.svg')):在所有规则中找到第一条能匹配 .svg 的规则。从源码结构看,此时框架已把原始 asset 规则的 test 改写为字体/PDF 格式,因此第一条能命中 .svg 的规则,正是框架追加的、使用 next-image-loader-stub 的规则(images/webpack.ts 第 50-62 行test: /\.(png|jpg|jpeg|gif|webp|avif|ico|bmp|svg)$/i)。
  3. imageRule['exclude'] = /\.svg$/;:给该规则追加 exclude,使 .svg 文件不再走 stub loader,从而不会被构造成 { src, width, height, blurDataURL } 模块。注意框架还追加了另一条 issuer: /\.(css|scss|sass)$/asset/resource 规则(第 63-70 行),它只在 CSS 等文件中引用图片时生效;在 .ts/.tsximport SVG 时 issuer 不满足,因此不受影响,示例只需排除第一条规则。
  4. config.module.rules.push({ test: /\.svg$/, use: ['@svgr/webpack'] }):追加新规则,把 .svg 交给 SVGR,最终 import Logo from './logo.svg' 得到默认导出的 React 组件,可直接 <Logo className="icon" /> 使用,并可传入 SVGR 支持的 props(如 title)。
  5. return config;webpackFinal 必须返回(或返回 Promise)修改后的完整 webpack 配置,遗漏 return 是此类钩子最常见的错误。

适用范围与验证方式

  • 仅适用于 webpack 构建链路webpackFinal@storybook/builder-webpack5 的钩子。@storybook/nextjs 框架内部固定使用该 builder(见 preset.ts 中 core 预设返回的 builder 指向 @storybook/builder-webpack5),所以本方案针对 @storybook/nextjs 直接可用;对 @storybook/nextjs-vite 这类 Vite 链路框架,webpackFinal 不参与生效,不应套用本配置。
  • 修改位置:所有面向 Storybook 的 webpack 调整都应写在 .storybook/main.js|ts 中,而不是 next.config.js——这也是 框架指南 “Custom Webpack config” 一节的明确提示:并非所有 Next.js 的 webpack 修改都能原样复制到 Storybook 侧,需理解 webpack 规则匹配(test / exclude / issuer)后再改动。
  • 验证:配置完成后,在任一 Story 中导入一个项目内的 .svg 文件并作为组件渲染;若控制台仍出现 { src, width, height, blurDataURL } 对象形态的模块,说明排除规则未命中,可检查 find 是否定位到了预期规则(例如打印 config.module.rules 排查)。

小结与延伸阅读

本方案的核心是“两条规则各司其职”:让框架的 next-image-loader-stub 规则通过 exclude 让出 .svg,再由新增规则把 .svg 交给 @svgr/webpack。关键参考文件:

登录后查看全文
热门项目推荐
相关项目推荐