如何使用Webpack将静态文件复制到构建目录?

432

我正在尝试从 Gulp 转移到 Webpack。在 Gulp 中,我有一个任务,它将 /static/ 文件夹中的所有文件和文件夹复制到 /build/ 文件夹中。如何使用 Webpack 实现相同的功能?我需要一些插件吗?


2
Gulp 很容易理解。如果你想要的话,只需从 gulpfile.js 中调用 webpack 即可。 - Baryon Lee
如果您正在使用Laravel Mix,可以访问https://laravel.com/docs/5.8/mix#copying-files-and-directories。 - Ryan
许多这些答案现在已经过时了。file-loader自webpack 5起已被弃用。推荐的方法是使用asset-modules,如此答案所述。 - Liam
13个回答

715

使用 file-loader 模块来要求资源是 webpack 的预期用法(来源)。但是,如果您需要更大的灵活性或更简洁的接口,也可以使用我的 copy-webpack-plugin 直接复制静态文件(npmGithub)。对于您的 staticbuild 示例:

const CopyWebpackPlugin = require('copy-webpack-plugin');
 
module.exports = {
    context: path.join(__dirname, 'your-app'),
    plugins: [
        new CopyWebpackPlugin({
            patterns: [
                { from: 'static' }
            ]
        })
    ]
};

兼容性提示:如果您使用的是旧版本的webpack,例如webpack@4.x.x,请使用copy-webpack-plugin@6.x.x。否则,请使用最新版本。


15
当你想要复制整个目录(例如静态HTML和其他的样板图片)时,这会变得简单得多! - Arjun Mehta
6
搞定了,谢谢 :) 在尝试了多次却不能让文件加载器执行一个非常简单的命令后,我放弃了。你的插件一次就成功了。 - arcseldon
4
插件会在文件更改时重新复制它们(通过dev-server或webpack --watch)。如果您发现没有复制,请提交一个问题报告。 - kevlened
4
我对webpack还很陌生,但是我很难理解为什么我们需要使用file-loader/url-loader/img-loader等工具,而不是直接复制它们?比如说,使用file-loader有什么好处呢? - BreakDS
2
由于您是插件的作者,没有比这更好的地方来问这个问题了。使用“copy-webpack-plugin”插件...我可以过滤源目录中的文件,以便仅复制具有特定文件扩展名的文件,例如仅复制“.html”吗?问候 - DevWL
显示剩余16条评论

197

不需要像使用gulp一样复制文件,webpack的工作方式不同。Webpack是一个模块打包器,你在文件中引用的所有内容都将被包含在内。你只需要为其指定一个加载器。

因此,如果你编写如下代码:

var myImage = require("./static/myImage.jpg");

Webpack首先会尝试将引用的文件解析为JavaScript(因为这是默认值)。当然,这会失败。这就是为什么您需要为该文件类型指定一个加载器。例如file-或url-loader会获取引用的文件,将其放入webpack的输出文件夹中(在您的情况下应该是build),并返回该文件的散列URL。

var myImage = require("./static/myImage.jpg");
console.log(myImage); // '/build/12as7f9asfasgasg.jpg'

通常情况下,通过webpack配置来应用加载器:

// webpack.config.js

module.exports = {
    ...
    module: {
        loaders: [
            { test: /\.(jpe?g|gif|png|svg|woff|ttf|wav|mp3)$/, loader: "file" }
        ]
    }
};

当然,您需要先安装file-loader才能使其正常工作。


48
当然,在使这个工作生效之前,您需要先安装file-loader。 这里是所提到的“file-loader”的链接在此处。这里是使用loader的文档,包括如何安装和使用它。 - Nate
25
您仍然面临HTML文件及其中所有引用未被加载的问题。 - kilianc
145
如果你想深入了解 webpack 插件,你可以使用 file-loader、css-loader、style-loader、url-loader 等等插件,然后愉快地进行配置,搜索和彻夜不眠 :) 或者你可以使用 copy-webpack-plugin,轻松完成你的工作... - Kamil Tomšík
12
@KamilTomšík,你的建议是我们应该使用一个webpack插件来避免使用webpack插件?(只是开玩笑,我懂你的意思了。) - Konrad Viltersten
14
大部分图片都在 CSS 和 HTML 中。那么我是否应该使用 require('img.png') 在我的 JS 文件中请求所有这些图片,以便与 file-loader 协同工作?这听起来非常疯狂。 - Rantiev
显示剩余9条评论

62
如果您想要复制静态文件,可以按照以下方式使用 file-loader:
对于 HTML 文件:
在 webpack.config.js 文件中:
module.exports = {
    ...
    module: {
        loaders: [
            { test: /\.(html)$/,
              loader: "file?name=[path][name].[ext]&context=./app/static"
            }
        ]
    }
};

在你的js文件中:

  require.context("./static/", true, /^\.\/.*\.html/);

./static/ 相对于您的 js 文件所在的位置。

您可以对图像或其他内容执行相同操作。上下文是探索的强大方法!!


4
相比于copy-webpack-plugin模块,我更喜欢使用这种方法。此外,在我的webpack配置中,我能够让它正常工作,而无需使用“&context=./app/static”。我只需要require.context这一行代码即可。 - Dave Landry
2
我正在尝试这个,它看起来很棒,但是我遇到了一个小问题,就是它把我的 index.html 放在一个名为 _(下划线)的子目录中,发生了什么? - kris
2
当你说“在你的JS文件中”时,你是什么意思?如果我没有JS文件怎么办? - evolutionxbox
1
这一行在入口脚本 main.js 中,导入了 static 文件夹中的所有内容:require.context("./static/", true, /^.*/); - Mario
2
这是一个不错的技巧,但如果你复制太多文件,你会耗尽内存。 - Tom
显示剩余2条评论

37

前文提到的copy-webpack-plugin插件具有一个优点,即与其他在此处提到的方法不同,它不会将资源打包进你的捆绑文件中(而且需要你在某个地方“require”或“import”它们)。如果我只想移动一些图像或模板局部,我不想用无用的引用占据我的JavaScript捆绑文件,我只想让这些文件被正确地放置出来。我还没有找到webpack中的其他方法来实现这一点。诚然,这不是webpack最初设计的目的,但它肯定是一个当前的使用案例。(@BreakDS,希望我的回答能解决你的问题 - 只有在你需要它时才是一个好处)


21
Webpack 5增加了资产模块,它们本质上是常见文件加载器的替代品。以下是文档的相关部分:
- `asset/resource`:发出一个单独的文件并导出URL。以前可以使用`file-loader`实现。 - `asset/inline`:导出资产的数据URI。以前可以使用`url-loader`实现。 - `asset/source`:导出资产的源代码。以前可以使用`raw-loader`实现。 - `asset`:自动选择导出数据URI或发出单独的文件。以前可以使用带有资产大小限制的`url-loader`实现。
要添加其中一个,您可以将配置设置如下:
// webpack.config.js

module.exports = {
    ...
    module: {
        rules: [
            {
                test: /\.(jpe?g|gif|png|svg|woff|ttf|wav|mp3)$/,
                type: "asset/resource"
            }
        ]
    }
};

为了控制文件的输出方式,您可以使用模板路径

在配置中,您可以在此设置全局模板:

// webpack.config.js
module.exports = {
    ...
    output: {
        ...
        assetModuleFilename: '[path][name].[hash][ext][query]'
    }
}

要针对特定一组资源进行覆盖,你可以这样做:

// webpack.config.js

module.exports = {
    ...
    module: {
        rules: [
            {
                test: /\.(jpe?g|gif|png|svg|woff|ttf|wav|mp3)$/,
                type: "asset/resource"
                generator: {
                    filename: '[path][name].[hash][ext][query]'
                }
            }
        ]
    }
};

提供的模板将会生成类似于build/images/img.151cfcfa1bd74779aadb.png这样的文件名。哈希值可以用于缓存清除等。您应该根据自己的需求进行修改。

3
虽然你的建议完全符合文件,但我的资产没有被复制:-\ - David Bullock
1
@DavidBullock 你可能是树摇晃的受害者。如果你没有在使用的文件中导入资源,例如 import myPath from "image.png"; 然后使用它,那么Webpack就不会复制它。或者你可能正在使用Webpack开发服务器选项,其中包括一个内存文件系统,不会写入文件系统。如果这些都不能回答你的问题,我建议你提出一个问题,因为我在这里可以列举的信息有限。 - David Archibald
1
啊哈!显式的“import”完成了任务。不过我不会为每个要复制的资源都放一个“import”!既然这不是构建中基本的依赖项/捆绑/缩小/转译步骤,我想我会在webpack之外处理它。 - David Bullock
2
但出于兴趣,是否可以从Tree Shaking中排除某些规则?这对于资产模块来说是有意义的,对吧? - David Bullock
如果你只是想复制资源,我建议使用 copy-webpack-plugin。但是,如果可能的话,我建议你完全集成Webpack,例如CSS和HTML都有各自的设置。你可能还需要使用imports with expressions。这样未使用的图像仍然可以被修剪,但你不必手动让Webpack认识到这些导入正在被使用。 - David Archibald
谢谢你的回答!我希望能在这里找到Webpack 5的方法。 - mowwwalker

8
上述建议很不错。但是为了直接回答您的问题,我建议在定义在package.json里的脚本中使用cpy-cli
以下示例假设您已经将node添加到了系统路径中。请将cpy-cli安装为开发依赖项: npm install --save-dev cpy-cli 然后创建一对Node.js文件:一个用于复制,另一个用于显示勾号和信息。
copy.js
#!/usr/bin/env node

var shelljs = require('shelljs');
var addCheckMark = require('./helpers/checkmark');
var path = require('path');

var cpy = path.join(__dirname, '../node_modules/cpy-cli/cli.js');

shelljs.exec(cpy + ' /static/* /build/', addCheckMark.bind(null, callback));

function callback() {
  process.stdout.write(' Copied /static/* to the /build/ directory\n\n');
}

checkmark.js

var chalk = require('chalk');

/**
 * Adds mark check symbol
 */
function addCheckMark(callback) {
  process.stdout.write(chalk.green(' ✓'));
  callback();
}

module.exports = addCheckMark;

package.json中添加脚本。假设脚本在<project-root>/scripts/目录下。
...
"scripts": {
  "copy": "node scripts/copy.js",
...

运行脚本:

npm run copy


3
OP想要在Webpack内完成文件移动,而不是使用npm脚本? - William S
1
即使 OP 想在 webpack 内部解决这个问题,他也可能是通过 npm 运行 webpack 的,因此他可以将其添加到构建脚本中,在运行 webpack 的地方进行处理。 - Piro
1
这其实更有意义。Webpack没有内置此功能,很可能是因为它并不是作为gulp/make等工具的替代品。 - jmathew

7

我加载静态图片字体的方式:

module: {
    rules: [
      ....

      {
        test: /\.(jpe?g|png|gif|svg)$/i,
        /* Exclude fonts while working with images, e.g. .svg can be both image or font. */
        exclude: path.resolve(__dirname, '../src/assets/fonts'),
        use: [{
          loader: 'file-loader',
          options: {
            name: '[name].[ext]',
            outputPath: 'images/'
          }
        }]
      },
      {
        test: /\.(woff(2)?|ttf|eot|svg|otf)(\?v=\d+\.\d+\.\d+)?$/,
        /* Exclude images while working with fonts, e.g. .svg can be both image or font. */
        exclude: path.resolve(__dirname, '../src/assets/images'),
        use: [{
          loader: 'file-loader',
          options: {
            name: '[name].[ext]',
            outputPath: 'fonts/'
          },
        }
    ]
}

不要忘记安装file-loader才能使其正常工作。

你如何处理重复的文件名?或者更好的方法是,你知道在新的输出目录中保留原始路径的任何方式吗? - cantuket
你的项目中不应该有相同扩展名的重复文件名。如果它们的内容相同,为什么要保留重复文件呢?如果不同,则根据其内容进行命名。但是,如果您想保持原始路径中的内容,为什么要使用webpack呢?如果只需要JS翻译,则Babel就足够了。 - RegarBoy
1
如果您正在实施基于组件的开发(其中的主要原则之一是封装,更具体地说是信息隐藏),那么您提到的所有内容都与此无关。即,当有人向程序添加新组件时,他们不应该检查是否存在另一个名为“logo.png”的图像,也不应该创建晦涩难懂且“希望”唯一的文件名以避免全局冲突。这也是我们使用CSS模块的原因。 - cantuket
1
关于为什么我希望图像保持原始路径和文件名;主要是为了调试,就像你使用sourcemaps的原因一样,但也是为了SEO。无论如何,对于我的问题,答案实际上非常简单... [path] [name] .[ext],并且提供了足够的灵活性来修改特定环境或用例的内容... file-loader - cantuket
1
话虽如此,我们确实实现了您示例的变体,因此感谢您提供! - cantuket
好的,那我误解了重点。所以你想要防止文件名重复。我以为你在问如何保留重复的文件名。 - RegarBoy

6

你可以在package.json文件中编写Bash脚本:

# package.json
{
  "name": ...,
  "version": ...,
  "scripts": {
    "build": "NODE_ENV=production npm run webpack && cp -v <this> <that> && echo ok",
    ...
  }
}

1
在Windows中,只需使用xcopy而不是cp:"build": "webpack && xcopy images dist\\images\\ /S /Y && xcopy css dist\\css\\ /S /Y" - SebaGra
9
你的解决方案是为每个操作系统编写不同的脚本? - Maciej Gurban
1
是的,对我来说每个操作系统都有一个脚本是可以接受的(实际上是Unix/非Unix,因为在Linux上的脚本也可以在Darwin或其他POSIX *nix上运行)。 - Victor Pudeyev
那个Windows的例子也不能在默认使用PowerShell作为shell的情况下运行。 - Julian Knight
根据@AlienTechnology的建议,使用cp.bat包装xcopy似乎是一个合理的解决方案,以实现跨操作系统的兼容性。 - Victor Pudeyev
显示剩余2条评论

5

很可能你应该使用CopyWebpackPlugin,这在kevlened的回答中提到了。另外,对于一些文件类型,比如.html.json,你也可以使用raw-loader或json-loader。通过npm install -D raw-loader安装它,然后你只需要将另一个加载器添加到我们的webpack.config.js文件中即可。

像这样:

{
    test: /\.html/,
    loader: 'raw'
}

注意:重新启动webpack-dev-server以使配置更改生效。
现在,您可以使用相对路径要求html文件,这使得移动文件夹变得更加容易。
template: require('./nav.html')  

2
webpack 2版本的配置文件允许你导出一个Promise链,只要最后一步返回webpack配置对象即可。请参阅promise配置文档。从那里开始:
webpack现在支持从配置文件中返回Promise。这样可以在配置文件中进行异步处理。
你可以创建一个简单的递归复制函数来复制文件,只有在复制完成后才触发webpack。例如:
module.exports = function(){
    return copyTheFiles( inpath, outpath).then( result => {
        return { entry: "..." } // Etc etc
    } )
}

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