Webpack的style-loader/css-loader:url()路径解析不起作用

25

有一些关于style-loadercss-loader的SO帖子,但是尽管如此,我仍然无法找到解决我的问题的方法。

简而言之,当我在其他css文件中@importcss文件,并且导入的css包含相对路径的url()时,路径不会被正确解析。

基本上,错误消息显示Webpack最终认为导入的CSS中的url()路径相对于src(主入口点),而不是相对于导入它的css文件:

// css-one.scss
@import "./assets/open-iconic-master/font/css/open-iconic-bootstrap.css";

// open-iconic-bootstrap.css
@font-face {
    src: url('../fonts/open-iconic.eot');
}

错误:

ERROR in ./src/main.scss (./node_modules/css-loader??ref--5-1!./node_modules/postcss-loader/src??ref--5-2!./node_modules/sass-loader/lib/loader.js??ref--5-3!./src/main.scss)

无法找到模块:Error: Can't resolve '../fonts/open-iconic.eot',位于'C:\Users\...\src' @ ./src/main.scss (./node_modules/css-loader??ref--5-1!./node_modules/postcss-loader/src??ref--5-2!./node_modules/sass-loader/lib/loader.js??ref--5-3!./src/main.scss) 7:106-141 7:172-207 @ ./src/main.scss @ ./src/index.js

我尝试过的方法:

我的Webpack配置文件(加载器在底部):

const path = require('path');
const webpack = require('webpack'); // for webpack built-in plugins
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
// const WriteFilePlugin = require('write-file-webpack-plugin');
// const ManifestPlugin = require('webpack-manifest-plugin');
// const InlineManifestWebpackPlugin = require('inline-manifest-webpack-plugin');

// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

const PATHS = {
  // when using __dirname, resolve and join gives same result,
  // because __dirname is absolute path to directory of this file.
  // OK to use no slashes,
  // both resolve and join adds platform-specific separators by default
  src: path.resolve(__dirname, 'src'),
  dist: path.resolve(__dirname, 'dist'),
  build: path.resolve(__dirname, 'build'),
  test: path.resolve(__dirname, 'test')
};

const NAMES = {
  // JS FILES
  index: 'index',
  print: 'print',
  // Chrome Extension Development
  popup: 'popup',
  options: 'options',
  background: 'background',
  contentScript: 'contentScript',

  // FOLDERS
  assets: 'assets',
  utilities: 'utilities',
  images: 'images',
  fonts: 'fonts',
  include: 'include'
};

const FILE_PATHS = {
  // JS
  indexJs: `${path.join(PATHS.src, NAMES.index)}.js`,
  printJs: `${path.join(PATHS.src, NAMES.print)}.js`,
  // Chrome Extension Development
  popupJs: `${path.join(PATHS.src, NAMES.popup)}.js`,
  optionsJs: `${path.join(PATHS.src, NAMES.options)}.js`,
  backgroundJs: `${path.join(PATHS.src, NAMES.background)}.js`,
  contentScriptJs: `${path.join(
    PATHS.src,
    NAMES.include,
    NAMES.contentScript
  )}.js`,

  // HTML
  indexHtml: `${path.join(PATHS.src, NAMES.index)}.html`,
  printHtml: `${path.join(PATHS.src, NAMES.print)}.html`,
  // Chrome Extension Development
  popupHtml: `${path.join(PATHS.src, NAMES.popup)}.html`,
  optionsHtml: `${path.join(PATHS.src, NAMES.options)}.html`,
  backgroundHtml: `${path.join(PATHS.src, NAMES.background)}.html`
};

// Third-party (vendor) libraries to include
// const VENDORS = ['react', 'bootstrap', 'lodash', 'jQuery']; // Relative paths to node_modules

// Note: These are relative
const ASSETS = {
  images: path.join(NAMES.assets, NAMES.images),
  fonts: path.join(NAMES.assets, NAMES.fonts)
};

// CleanWebpackPlugin config
const pathsToClean = [PATHS.dist, PATHS.build];
const cleanOptions = {
  root: __dirname,
  exclude: ['shared.js'],
  verbose: true,
  dry: false
};

// CopyWebpackPlugin config
const copyPattern = [
  // {
  // from: NAMES.assets,
  // to: NAMES.assets
  // },
  // {
  // from: path.join(NAMES.include, 'contentScript.css')
  // },
  // {
  // from: 'manifest.json',
  // transform(content, copyPath) {
  // // generates the manifest file using the package.json informations
  // return Buffer.from(
  // JSON.stringify({
  // ...JSON.parse(content.toString())
  // // description: env.npm_package_description,
  // // version: env.npm_package_version
  // })
  // );
  // }
  // }
];
const copyOptions = {
  // ignore: ['*.js'],
  context: PATHS.src
};

module.exports = (env = {}) => {
  // webpack injects env variable, into webpack config.
  // perfect to check for production.
  // remember to specify --env.production in command
  // (if in production mode).
  const isProduction = env.production === true;

  return {
    entry: {
      index: FILE_PATHS.indexJs

      // Chrome Extension Development
      // popup: FILE_PATHS.popupJs,
      // contentScript: FILE_PATHS.contentScriptJs
      // options: FILE_PATHS.optionsJs,
      // background: FILE_PATHS.backgroundJs,

      // vendor: VENDORS
    },
    mode: isProduction ? 'production' : 'development',
    devtool: isProduction ? 'source-map' : 'inline-source-map',
    optimization: {
      splitChunks: {
        chunks: 'all'
      }
    },
    output: {
      filename: isProduction ? '[name].[chunkhash:8].js' : '[name].js',
      // chunkFilename determine name of non-entry chunk files,
      // for example dynamic imports in the app
      chunkFilename: isProduction ? '[name].[chunkhash:8].js' : '[name].js',
      path: PATHS.dist
    },
    plugins: [
      // new webpack.SourceMapDevToolPlugin({
      // filename: '[file].map',
      // exclude: ['vendor', 'runtime']
      // }),
      new webpack.DefinePlugin({
        // specifies environment variable for dependencies.
        // does not apply to browser runtime environment
        // (process.env is provisioned by Node)
        'process.env.NODE_ENV': isProduction ?
          JSON.stringify('production') :
          JSON.stringify('development')
      }),
      // new BundleAnalyzerPlugin(),
      new CleanWebpackPlugin(pathsToClean, cleanOptions),
      new MiniCssExtractPlugin({
        // Options similar to the same options in webpackOptions.output
        // both options are optional
        // does not work with Hot Module Replacement (HMR)
        // allows HMR in development (will only use this plugin in production)
        filename: isProduction ? '[name].[contenthash].css' : '[name].css',
        chunkFilename: isProduction ? '[id].[contenthash].css' : '[id].css'
      }),
      new webpack.HashedModuleIdsPlugin(),
      isProduction ?
      new UglifyJSPlugin({
        cache: true,
        parallel: true,
        sourceMap: true // set to true if you want JS source maps
      }) :
      () => {},
      new CopyWebpackPlugin(copyPattern, copyOptions),
      // new WriteFilePlugin(),
      new HtmlWebpackPlugin({
        template: FILE_PATHS.indexHtml,
        filename: `${NAMES.index}.html`
      })
      // new HtmlWebpackPlugin({
      // template: FILE_PATHS.popupHtml,
      // filename: `${NAMES.popup}.html`,
      // excludeChunks: [NAMES.contentScript]
      // In dev mode, chunks excluded vendor chunk (which holds CSS).
      // Above check fixes it.
      // }),
      // new HtmlWebpackPlugin({
      // filename: `${NAMES.contentScript}.html`,
      // excludeChunks: [NAMES.popup, 'runtime'] // Runtime only needed in one HTML
      // }),
      // new HtmlWebpackPlugin({
      // template: FILE_PATHS.optionsHtml,
      // filename: `${NAMES.options}.html`,
      // chunks: isProduction ? [NAMES.options] : ''
      // }),
      // new HtmlWebpackPlugin({
      // template: FILE_PATHS.backgroundHtml,
      // filename: `${NAMES.background}.html`,
      // chunks: isProduction ? [NAMES.background] : ''
      // }),
      // no need for CSS minimization here <-- Done by PostCSS (cssnano)
      // new InlineManifestWebpackPlugin(),
      // new ManifestPlugin({fileName: 'webpack-manifest.json'}),
    ],
    module: {
      rules: [{
          test: /\.js$/,
          exclude: /(node_modules|bower_components)/,
          use: {
            loader: 'babel-loader',
            options: {
              presets: ['@babel/preset-env']
            }
          }
        },
        {
          test: /\.s?[ac]ss$/,
          exclude: /node_modules/,
          use: [
            isProduction ?
            MiniCssExtractPlugin.loader :
            {
              // creates style nodes from JS strings
              loader: 'style-loader',
              options: {
                sourceMap: true,
                convertToAbsoluteUrls: true
              }
            },
            {
              // CSS to CommonJS (resolves CSS imports into exported CSS strings)
              loader: 'css-loader',
              options: {
                sourceMap: true,
                importLoaders: 2
              }
            },
            {
              loader: 'postcss-loader',
              options: {
                config: {
                  ctx: {
                    cssnext: {},
                    cssnano: {},
                    autoprefixer: {}
                  }
                },
                sourceMap: true
              }
            },
            {
              // compiles Sass to CSS
              loader: 'sass-loader',
              options: {
                sourceMap: true
              }
            }
          ]
        },
        {
          test: /\.(png|svg|jpg|gif)$/,
          use: [{
            loader: 'file-loader',
            options: {
              name: '[name].[hash:4].[ext]',
              outputPath: ASSETS.images
            }
          }]
        },
        {
          test: /\.(woff|woff2|eot|ttf|otf)$/,
          use: [{
            loader: 'file-loader',
            options: {
              name: '[name].[hash:4].[ext]',
              outputPath: ASSETS.fonts
            }
          }]
        },
        {
          test: /\.(csv|tsv)$/,
          use: ['csv-loader']
        },
        {
          test: /\.xml$/,
          use: ['xml-loader']
        },
        {
          test: /\.(html)$/,
          use: {
            loader: 'html-loader',
            options: {
              interpolate: 'require',
              minimize: true
            }
          }
        }
        // {
        // test: /\.tsx?$/,
        // exclude: /(node_modules|bower_components)/,
        // use: 'ts-loader'
        // }
      ]
    },
    devServer: {
      // contentBase: path.join(__dirname, 'dist'),
      contentBase: PATHS.dist,
      compress: false,
      port: 8080,
      open: false
    }
  };
};

7个回答

25

我花了大约5天的时间来理解这个webpack混乱的工具是如何工作的。说实话,我真的不明白为什么它们是当下"事实上"的工具之一。我无法理解为什么让配置文件正常工作会有多困难,在gulp中做同样的事情只用了1小时。

我的问题是所有url()规则(包括字体和图像)都被css-loader加载为[object Module],并且它们由file-loader导出但从未被加载,所以如果我在css-loader中添加?url=false,它就不会复制文件并将其导出。我必须说这是一个非常麻烦的事情,但我已经解决了,并希望对世界上的其他人有所帮助,这是使用webpack 4完成的。

const webpack = require("webpack");
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const ImageminPlugin = require('imagemin-webpack-plugin').default;
const CopyPlugin = require('copy-webpack-plugin');
module.exports = {
    entry: "./src/index.js",
    mode: "development",
    module: {
        rules: [
        {
            test: /\.(js|jsx)$/,
            exclude: /(node_modules|bower_components)/,
            loader: "babel-loader",
            options: { presets: ["@babel/env"] }
        },
        {
            test: /\.(gif|png|jpe?g|svg)$/i,
            use: [
            {
                loader: 'image-webpack-loader',
                options: {
                    mozjpeg: {
                        progressive: true,
                        quality: 65
                    },

                    optipng: {
                        enabled: false,
                    },
                    pngquant: {
                        quality: [0.65, 0.90],
                        speed: 4
                    },
                    gifsicle: {
                        interlaced: false,
                    },

                    webp: {
                        quality: 75
                    },
                }
            },
            {
                loader: 'file-loader',
                options:{
                    name: '[name].[ext]',
                    outputPath: 'images/',
                    publicPath: 'images/'
                }
            },
            'url-loader?limit=100000'
            ],
        },
        {
            test: /\.(woff(2)?|ttf|eot)(\?v=\d+\.\d+\.\d+)?$/,
            use: [
            {
                loader: 'file-loader',
                options: {
                    name: '[name].[ext]',
                    outputPath: 'fonts/'
                }
            }
            ]
        },
        {
            test: /\.s[ac]ss$/i,
            use: [
            MiniCssExtractPlugin.loader,
            { loader: 'css-loader?url=false'},
            { loader: 'sass-loader', options: { sourceMap: true } }
            ],
        },
        ]
    },
    resolve: { extensions: ["*", ".js", ".jsx"] },
    output: {
        path: path.resolve(__dirname, "dist/"),
        publicPath: "",
        filename: "bundle.js"
    },
    devServer: {
        contentBase: path.join(__dirname, "dist/"),
        port: 3000,
        publicPath: "http://localhost:3000/dist/",
        hotOnly: true
    },
    plugins: [ new MiniCssExtractPlugin(),
    new CopyPlugin([{ from: 'src/images/', to: 'images/' }]),
    new CopyPlugin([{ from: 'src/fonts/', to: 'fonts/' }]),
    new ImageminPlugin({ test: /\.(jpe?g|png|gif|svg)$/i }),
    new HtmlWebpackPlugin({
        hash: true,
        template: './src/index.html',
            filename: './index.html' //relative to root of the application
        }),
    ]
};

我有完全相同的问题,我的文件从未被复制... 你解决方案的关键部分是什么? 我正在尝试使用webpack 5使其正常工作。 - Merc
似乎我无法使用 options: { url: false } 使其正常工作。我不得不重写某些 url 链接... - Merc
@Merc,如果您能分享您的解决方案,那就太棒了,这样其他人就可以以此为基础。 - Alejandro Giraldo
这并没有解决我的问题。使用上面的选项,我会掩盖问题,因为这些文件不再被webpack处理了。 我仍在努力寻找一个合适的解决方案,但这个现有的项目正在折磨我 :S。url: false 解决方案只是此线程中提出的解决方案之一: https://dev59.com/Wuk5XIcBkEYKwwoY7OHv#63662916 已经有描述了。 - Merc
我简直不敢相信这个小东西为我解决了问题!谢谢!FYI:我的设置是 { Webpack: 1.15.0, css-loader: 0.23.1 },在 webpack.config.js 中我有 module:loaders:loader: ExtractTextPlugin.extract("style-loader", "css-loader")。问题是 node_modules 中自定义的 npm 包内的路径,例如 url(./assets/... 没有被解析。 - pkpapani
显示剩余2条评论

18
你可以关闭对url()规则的处理,顺便说一句,我不知道为什么这是默认行为。

你可以关闭对url()规则的处理,顺便说一句,我不知道为什么这是默认行为。

{
  loader: 'css-loader',
  options: {
    ...
    url: false,
  }
},

只有在我重新启动Webpack后,这才对我起作用。 - Kevin Johnson
在使用 Phoenix 框架时,发现它默认捆绑了 webpack 和 css-loader,这对于使用 Elixir 很有帮助。但是,在升级 css-loader 后遇到了文件路径问题,最简单的解决方法就是按照上述方法关闭处理。 - Optimae

12

我已经自己解决了这个问题。如果以后能帮助到其他人,请在下面找到解决方案。


  1. 首先,如果您同时使用postcss-loaderpostcss-import插件,并且使用了css-loader,请关闭/删除postcss-import插件。您不需要多个解析@import规则的工具。如果加载器的顺序正确,则这不是一个问题,但最好将其删除。
  2. sass-loader文档中,您可以阅读以下内容

由于Sass/libsass没有提供URL重写功能,所有链接的资源必须相对于输出路径。

  • 如果你只是生成CSS而没有传递给css-loader,那么它必须相对于你的web根目录。

  • 如果将生成的CSS传递给css-loader,则所有URL都必须相对于入口文件(例如main.scss)。

更可能会受到第二个问题的干扰。自然而然地期望相对引用与它们所在的.scss文件相对解析(就像常规的.css文件一样)。幸运的是,有两种解决方法:

  • 使用resolve-url-loader添加缺少的URL重写。将其放在loader链中的sass-loader之前。

  • 库作者通常会提供一个变量来修改资源路径。例如,bootstrap-sass有一个$icon-font-path。查看这个工作的bootstrap示例。

我决定采用第二个方案,在Webpack配置中在sass-loader上方添加了resolve-url-loader。现在它按预期工作。

我的最终Webpack配置(目前)如下:

    {
      test: /\.s?[ac]ss$/,
      exclude: /node_modules/,
      use: [
        isProduction
          ? MiniCssExtractPlugin.loader
          : {
              // creates style nodes from JS strings
              loader: 'style-loader',
              options: {
                sourceMap: true,
                // convertToAbsoluteUrls: true
              }
            },
        {
          // CSS to CommonJS (resolves CSS imports into exported CSS strings)
          loader: 'css-loader',
          options: {
            sourceMap: true,
            importLoaders: 2
            // url: false,
            // import: false
          }
        },
        {
          loader: 'postcss-loader',
          options: {
            config: {
              ctx: {
                cssnext: {},
                cssnano: {},
                autoprefixer: {}
              }
            },
            sourceMap: true
          }
        },
        {
          loader: 'resolve-url-loader',
          options: {
            attempts: 1,
            sourceMap: true
          }
        },
        {
          // compiles Sass to CSS
          loader: 'sass-loader',
          options: { sourceMap: true }
        }
      ]
    },

注意事项

  1. 我注意到在Chrome的调试器中,“无域名”下的源映射路径会重复。如果有人知道原因,请分享一下。
  2. 请记得在package.json中包含以下副作用,这样在生产模式下进行的树摇时不会删除提取的CSS:

    "sideEffects": [ ".css", ".scss" ],


1
在webpack 5中,您应该避免使用raw-loaderfile-loader,而是使用asset/sourceasset/resource
  {
    test: /\.txt$/,
    type: 'asset/source',
  },
  {
    test: /\.png$/,
    type: 'asset/resource',
  },

您可以在以下链接中了解有关资产的更多信息:https://webpack.js.org/guides/asset-modules/。其中介绍了对于Webpack 4及更早版本应使用file-loader,而对于Webpack5及更高版本应使用资产模块。


0

我的问题是webpack 5显然通过“资产模块”改变了一切,破坏了file-loader。

官方文档解释得很好:https://webpack.js.org/guides/asset-modules/

简而言之,您可以通过将type: 'javascript/auto'添加到规则/加载器来修复它。

            {
                test: /\.(eot|ttf|woff2?)($|\?)/i,
                loader: 'file-loader',
                options: {
                    name: '[name]-[sha1:hash:hex:10].[ext]',
                    esModule: false,
                },
                type: 'javascript/auto'
            },

0

如果有人正在使用Webpack 5并尝试从css-loader 5升级到css-loader 6时遇到问题,您可能需要检查this issue,其中发布者与OP有类似的问题:

使用css-loader 5.2.7时,输入样式表中的图像被嵌入为数据URL在输出CSS中。使用css-loader 6时,这些图像被移动到输出目录。

请参阅此处的注释here以了解如何升级到css-loader 6 - 要点如下:

当启用esModules选项时,使用~已被弃用...可以从您的代码中删除

file-loader和url-loader已弃用,请迁移到asset模块,因为v6 css-loader正在生成新的URL(...)语法,它默认启用内置资产模块,即所有url()的类型:'asset'


因此,我已经完成了以下操作:

  1. 从我的.scss文件中删除任何'~'

$font-path: "~/src/fonts" !default;

变成

$font-path: "/src/fonts" !default;
  1. 我已经完全移除了 'file-loader' 模块。

所有的 NPM 包现在都是最新的,CSS 中的 URL 已经可以正常工作了。


0

IE 不兼容新的 URL()


2
目前你的回答不够清晰,请编辑并添加更多细节,以帮助其他人理解它如何回答问题。你可以在帮助中心找到有关如何撰写好答案的更多信息。 - Community

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接