You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用twig-loader时如何处理资源链接?

问题:Twig-loader无法处理资源链接,嵌套模板导致html-loader异常

我使用twig-loader处理Twig模板文件,但它无法像html-loader那样处理资源链接。Stack Overflow上有类似问题,但现有解决方案对我无效。当同时使用html-loader、extract-loader和twig-loader时,由于模板内部可能存在嵌套,html-loader会接收到尚未处理的Twig文件,从而引发问题。


Webpack配置

type WebpackBuildMode = "development" | "production";

interface WebpackBuildPaths {
    readonly pages: string;
    readonly src: string;
    readonly output: string;
    readonly svgo: string;
    readonly public: string;
}

interface WebpackBuildEnv {
    readonly mode: WebpackBuildMode,
    readonly port: number;
    readonly svg: boolean;
}

interface WebpackBuildOptions {
    readonly mode: WebpackBuildMode;
    readonly paths: WebpackBuildPaths;
    readonly isDev: boolean;
    readonly port: number;
    readonly isSvg: boolean;
}

const buildEntry = (options: WebpackBuildOptions): webpack.Configuration["entry"] => {
    const {paths} = options;
    
    const entry = fs.readdirSync(paths.pages).reduce((acc, dir) => {
        acc.push([
            dir,
            path.resolve(paths.pages, dir, 'script.ts')
        ]);
        
        return acc;
    }, []);
    
    return Object.fromEntries(entry);
};

const buildPlugins = (options: WebpackBuildOptions): webpack.Configuration["plugins"] => {
    const {paths, isSvg, isDev} = options;
    
    const html = fs.readdirSync(paths.pages).filter(dir => dir !== 'base').map(dir => (
        new HtmlWebpackPlugin({
            inject: false,
            template: path.resolve(paths.pages, dir, "template.twig"),
            templateParameters: (compilation, assets, assetTags, options) => {
                const compilationAssets = compilation.getAssets();
                
                const sprites = compilationAssets.filter((asset: any) => asset.name.includes('sprites/'));
                
                return {
                    compilation,
                    webpackConfig: compilation.options,
                    htmlWebpackPlugin: {
                        tags: assetTags,
                        files: {
                            ...assets,
                            svgSprites: sprites.map((sprite: any) => ([
                                sprite.name.split('/')[1],
                                sprite.source._valueAsString
                            ])),
                        },
                        options,
                    },
                };
            },
            filename: `${dir}.html`,
            minify: false,
        })
    ));
    
    const plugins: webpack.Configuration["plugins"] = [
        new MiniCssExtractPlugin({
            filename: "[name].css",
        }),
        new SvgChunkWebpackPlugin({
            filename: "sprites/[name].svg",
            generateSpritesPreview: true,
            svgstoreConfig: {
                svgAttrs: {
                    'aria-hidden': true,
                    style: 'position: absolute; width: 0; height: 0; overflow: hidden;'
                }
            }
        }),
    ];
    
    if (!isSvg) {
        plugins.push(...html);
    }
    
    return plugins;
};

const buildLoaders = (options: WebpackBuildOptions): webpack.Configuration["module"]["rules"] => {
    const {isDev, isSvg, paths} = options;
    
    const tsLoader = {
        test: /\.ts$/,
        use: "ts-loader",
        exclude: /node_modules/,
    };
    
    const twigLoader:  webpack.RuleSetRule = {
        test: /\.twig$/,
        use: [
            {
                loader: "html-loader",
                options: {
                    sources: {
                        list: [
                            {
                                tag: "img",
                                attribute: "src",
                                type: "src",
                            },
                        ],
                    },
                    minimize: false,
                },
            },
            {
                loader: "twig-loader",
            },
        ],
    };
    
    const styleLoader = {
        test: /\.s[ac]ss$/i,
        use: [
            MiniCssExtractPlugin.loader,
            "css-loader",
            "sass-loader",
        ],
    };
    
    const fontLoader = {
        test: /\.(woff|woff2|eot|ttf|otf)$/i,
        type: "asset/resource",
        generator: {
            filename: 'assets/fonts/[name][ext]'
        }
    };
    
    const svgLoader = {
        test: /\.svg$/i,
        include: [
            path.resolve(paths.src, "shared", "assets", "icons")
        ],
        use: [
            {
                loader: (SvgChunkWebpackPlugin as any).loader,
                options: {
                    configFile: paths.svgo,
                },
            },
        ],
    };
    
    const assetsLoader = {
        test: /\.(png|jpg|jpeg|gif|svg)$/i,
        type: "asset/resource",
        exclude: [
            path.resolve(paths.src, "shared", "assets", "icons")
        ],
        generator: {
            outputPath: (pathData: any) => {
                const dirname = path.dirname(pathData.filename);
                let subDir = '';
                
                if (dirname.includes('src/shared/assets/images')) {
                    subDir = "assets/images/" + dirname.replace('src/shared/assets/images', '') + "/";
                }
                
                return subDir;
            },
            publicPath: (pathData: any) => {
                const dirname = path.dirname(pathData.filename);
                let subDir = '';
                
                if (dirname.includes('src/shared/assets/images')) {
                    subDir = "assets/images/" + dirname.replace(/src\/shared\/assets\/images(\/?)/ig, '') + "/";
                }
                
                return subDir;
            },
            filename: '[name][ext]'
        }
    };
    
    const loaders: webpack.Configuration["module"]["rules"] = [
        styleLoader,
        fontLoader,
        assetsLoader,
        svgLoader,
        tsLoader,
    ];
    
    if (!isSvg) {
        loaders.push(twigLoader);
    }
    
    return loaders;
};

const buildDevServer = (options: WebpackBuildOptions): DevServerConfiguration => {
    const {port} = options;
    
    return {
        port,
        open: true,
        hot: 'only',
    }
}

const buildResolve = (options: WebpackBuildOptions): webpack.Configuration["resolve"] => {
    const {paths} = options;
    
    return {
        extensions: [".ts", ".js"]
    };
};

const buildWebpackConfig = (options: WebpackBuildOptions): webpack.Configuration => {
    const {mode, isDev, isSvg, paths} = options;
    
    return {
        mode: mode,
        entry: buildEntry(options),
        plugins: buildPlugins(options),
        module: {
            rules: buildLoaders(options),
        },
        devtool: isDev ? 'inline-source-map' : undefined,
        devServer: isDev && !isSvg ? buildDevServer(options) : undefined,
        resolve: buildResolve(options),
        output: {
            path: paths.output,
            filename: '[name].js',
            clean: true,
        },
        optimization: {
            runtimeChunk: "single",
        }
    };
};

const config = (env: WebpackBuildEnv): webpack.Configuration => {
    const mode = env.mode;
    const isSvg = env.svg;
    const port = env.port || 3000;
    const isDev = mode === "development";
    
    const paths: WebpackBuildPaths = {
        src: path.resolve(__dirname, "src"),
        output: path.resolve(__dirname, "build"),
        pages: path.resolve(__dirname, "src", "pages"),
        svgo: path.resolve(__dirname, "config", "svgo", "svgo.config.ts"),
        public: path.resolve(__dirname, "public"),
    };
    
    return buildWebpackConfig({
        isDev,
        isSvg,
        mode,
        port,
        paths,
    });
};

export default config;

Package.json

{
  "name": "krep-comp",
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
    "build": "webpack --env mode=production --env svg && webpack --env mode=production"
  },
  "keywords": [],
  "license": "ISC",
  "devDependencies": {
    "@types/node": "^20.8.4",
    "@types/webpack": "^5.28.3",
    "css-loader": "^6.8.1",
    "html-loader": "^4.2.0",
    "html-webpack-plugin": "^5.5.3",
    "mini-css-extract-plugin": "^2.7.6",
    "sass": "^1.69.2",
    "sass-loader": "^13.3.2",
    "style-loader": "^3.3.3",
    "svg-chunk-webpack-plugin": "^4.0.2",
    "ts-loader": "^9.5.0",
    "ts-node": "^10.9.1",
    "typescript": "^5.2.2",
    "webpack": "^5.88.2",
    "webpack-cli": "^5.1.4",
    "webpack-dev-server": "^4.15.1",
    "twig-loader": "^0.5.5"
  }
}

项目结构

├── webpack.config.ts
├── package.json
├── src
|   ├── app
|   |   ├── main
|   |   |   ├── template.twig
|   |   |   ├── script.ts
|   ├── shared
|   |   ├── assets
|   |   |   ├── images
|   |   |   |   ├── test.jpg

Twig模板

<!doctype html>
<html lang="{% block lang %}ru{% endblock %}">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>{% block title %}{% endblock %}</title>

    {% for js in htmlWebpackPlugin.files.js %}
        {% if js|split('.')[0] == htmlWebpackPlugin.options.filename|split('.')[0] %}
            <script defer="defer" src="{{ js }}"></script>
        {% endif %}
    {% endfor %}

    {% for css in htmlWebpackPlugin.files.css %}
        {% if css|split('.')[0] == htmlWebpackPlugin.options.filename|split('.')[0] %}
            <link href="{{ css }}" rel="stylesheet">
        {% endif %}
    {% endfor %}
</head>
<body>
    {% block content %}{% endblock %}

    <img src="/src/shared/assets/images/test.jpg">

    {% for sprite in htmlWebpackPlugin.files.svgSprites %}
        {% if sprite[0]|split('.')[0] == htmlWebpackPlugin.options.filename|split('.')[0] %}
            {{ sprite[1] }}
        {% endif %}
    {% endfor %}
</body>
</html>

内容的提问来源于stack exchange,提问作者kxhcfk

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 10:02:35