Storybook @storybook/nextjs 框架下通过 webpackFinal 接入 SVGR 渲染 SVG 组件的完整配置指南
本篇指南基于 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 做了两件事:
- 找到现有能匹配
test.jpg的 asset 规则,把它的test改写为/\.(apng|eot|otf|ttf|woff|woff2|cur|ani|pdf)(\?.*)?$/,即只保留字体、PDF 等格式(见 第 48 行); - 新增一条覆盖
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 替换为实际使用的 nextjs 或 nextjs-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 的每一步都在针对框架已注入的规则做“外科手术式”修改:
config.module = config.module || {};与config.module.rules = config.module.rules || [];:防御性兜底,确保module.rules一定存在,避免在异常配置下抛错。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)。imageRule['exclude'] = /\.svg$/;:给该规则追加exclude,使.svg文件不再走 stub loader,从而不会被构造成{ src, width, height, blurDataURL }模块。注意框架还追加了另一条issuer: /\.(css|scss|sass)$/的asset/resource规则(第 63-70 行),它只在 CSS 等文件中引用图片时生效;在.ts/.tsx中importSVG 时 issuer 不满足,因此不受影响,示例只需排除第一条规则。config.module.rules.push({ test: /\.svg$/, use: ['@svgr/webpack'] }):追加新规则,把.svg交给 SVGR,最终import Logo from './logo.svg'得到默认导出的 React 组件,可直接<Logo className="icon" />使用,并可传入 SVGR 支持的 props(如title)。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。关键参考文件:
- 官方配置片段:docs/_snippets/nextjs-configure-svgr.md
- 文档上下文(Custom Webpack config 一节):docs/get-started/frameworks/nextjs.mdx
- 框架 webpackFinal 入口:code/frameworks/nextjs/src/preset.ts
- 图片规则改写实现:code/frameworks/nextjs/src/images/webpack.ts
- stub loader 行为:code/frameworks/nextjs/src/next-image-loader-stub.ts
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00