webpack

381 阅读6分钟

基本配置

webpack的配置是在项目根路径下的webpack.config.js这个文件中进行配置的。由于这个文件将会运行在nodejs环境中,所以需要使用CommonJS规范。这个文件需要导出一个对象,通过对该对象属性的配置来完成对webpack的配置。

module.exports = {
    enrty: './src/main.js',  // 将webpack的打包入口指定为src下面的main.js
}

entry这个属性中的路径如果是相对路径的话,./是不能被省略的

还可以通过output这个属性去设置打包后的文件的输出路径:

const path = require('path')
module.exports = {
    enrty: './src/main.js',  // 将webpack的打包入口指定为src下面的main.js
    output: {
        filename: 'bundle.js',  // 打包结果的文件名
        path: path.join(__dirname, 'dist'),  // 打包结果的路径
        publicPath: 'dist/'  // 配置静态资源路径
    }
}

path属性必须是绝对路径

webpack工作模式配置

在使用webpack进行打包的时候,可以通过mode参数来指定工作模式:

yarn webpack --mode development  #开发环境
#或
yarn webpack --mode production  #生产环境(默认)
#或
yarn webpack --mode none  #仅打包,不做任何处理

也可以在配置文件中进行配置:

module.exports = {
  mode: 'development',  //	'production' or 'none'
}

下面是官方对于这三个模式的描述:

选项描述
development会将 DefinePluginprocess.env.NODE_ENV 的值设置为 development. 为模块和 chunk 启用有效的名。
production会将 DefinePluginprocess.env.NODE_ENV 的值设置为 production。为模块和 chunk 启用确定性的混淆名称,FlagDependencyUsagePluginFlagIncludedChunksPluginModuleConcatenationPluginNoEmitOnErrorsPluginTerserPlugin
none不使用任何默认优化选项

webpack加载资源的方式

webpack打包的过程中,以下几种方式会触发模块的加载:

  • 遵循ES Modules标准的import声明

  • 遵循CommonJSrequire函数

    通过require去载入ESM的默认导出结果的话需要的是require结果的default属性,如

    const Header = require('Header').default
    
  • 遵循AMD标准的define函数和require函数

  • 一些loader中也会触发模块加载,如:

    • css-loader中的@import指令和url函数
    • html-loader中如img标签的src属性

webpack导入资源模块

webpack打包非js文件的时候需要安装对应的loader,例如css文件就需要css-loader以及style-loader,同时需要在配置文件中加入以下配置:

module.exports = {
    // ...other config
    module: {
        // rules是一个数组,它是对各种文件的加载规则的配置
        rules: [
            {
                test: /.css$/,
                use: ['style-loader', 'css-loader']
            }
        ]
    }
}

rules的每一配置对象都需要testuse属性。test属性的值是一个正则表达式,它用来匹配对应文件的路径;use属性是用来规定加载当前test所对应的文件的loaderuse属性可以是字符串,也可以是数组,当use属性为数组时,对应的loader的执行顺序是从后往前执行的。

webpack引入文件资源

对于一些例如图片等的文件资源,需要配合文件资源加载器来对其进行打包操作:

可以使用file-loader来将文件拷贝至dist文件夹下,以便打包后的结果能使用文件:

module.exports = {
    // ...other config
    module: {
        rules: [
            {
                // 引入png文件
                test: /.png$/,
                use: 'file-loader'
            }
        ]
    }
}

也可以使用url加载器:

module.exports = {
    // ...other config
    module: {
        rules: [
            {
                // 引入png文件
                test: /.png$/,
                use: 'url-loader'
            }
        ]
    }
}

url-loader是将文件转换成base64字符串的形式打包文件的,生成的字符串一般都会比较长。

综上:

  • 对于小文件,例如icon等可以使用url-loader来将其转成base64字符串以减少HTTP请求次数;
  • 对于大文件,还是使用file-loader来进行打包以提高加载速度;

这样的话配置文件可以如下配置:

module.exports = {
    // ...other config
    module: {
        rules: [
            {
                test: /.png$/,
                use: {
                    loader: 'url-loader',
                    options: {
                        limit: 10 * 1024  // 在文件大小不超过10k时使用url-loader
                    }
                }
            }
        ]
    }
}

使用url-loader的时候一定要同时安装file-loader

webpack常用加载器分类

webpackloader大致可分为三类:

  • 编译转换类

    css-loader,它是将css文件转换为打包后js文件中的一个模块,通过js来运行css

  • 文件操作类

    将文件拷贝至输出目录,并将访问路径导出,如file-loader

  • 代码检查类

    检查代码格式、语法等问题,提高代码质量

webpack与ES6

webpack自己并不能处理js中的新语法,因此需要babel-loader来将其进行转译。

使用前需要安装以下模块:babel-loader@babel/core以及@babel/preset-env

使用时需要在module.rules中配置:

module.exports = {
    //  ...other config
    module: {
        rules: [
            {
                test: /.js$/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        preset: ['@babel/preset-env']
                    }
                }
            }
        ]
    }
}

webpack核心工作原理

1621862452585.png

一般项目中会按照文件夹的划分有各种的文件,webpack会根据配置找到对应的入口文件为打包入口(一般情况下都会是js文件),然后根据入口文件中的importrequire等导入语句来找到其所依赖的资源模块,再分别去解析每个资源模块所对应的依赖,并生成整个项目各文件的“依赖关系树”:

1621862831921.png

webpack会递归这个依赖树找到每个节点所对应的资源文件,然后通过webpack.config.js文件的rules配置的属性去找到对应的加载器(loader),再用该加载器去加载当前资源模块。

最后会将加载到的结果加载到bundle.js——即打包结果中,从而去实现整个项目的打包。

整个过程中loader机制是webpack的核心,

开发一个简易的markdown-loader

**需求:**将md文件中的内容转换成html内容

loader运行机制:loader运行机制类似node中的管道pipe,运行时中会有一个或多个loader按顺序处理输入并将结果转交给下一个,最后输出一段js代码

实现:

  1. 先创建个文件,取名markdown-loader.js

    loader中需要先导出一个函数,这个函数里面就是要对该种文件处理的过程。函数接收(输入)的是加载到资源文件的内容,输出是此次加工后的结果:

    module.exports = source => {
        console.log(source)
        return 'console.log('hello')'
    }
    

    同时在配置文件中配置一下:

    module.exports = {
        // ...other config
        module: {
            rules: [
                {
                    test: /.md$/,
                    use: './markdown-loader'  // use属性也可以设置为路径
                }
            ]
        }
    }
    

    执行打包时会打印出md文档中的内容。

  2. 安装并引入marked模块:

    npm i marked --dev
    
    const marked = required('marked')
    
    module.exports = source => {
        const html = marked(source)
        return `module.exports = ${JSON.stringify(html)}`
        // 或使用ESM的导出
        // return `export default ${JSON.stringify(html)}`
    }
    

    同时还可以使用html-loader来处理这个html字符串:

    module.exports = {
        // ...other config
        module: {
            rules: [
                {
                    test: /.md$/,
                    use: [
                        'html-loader',
                        './markdown-loader'
                    ]
                }
            ]
        }
    }
    

webpack插件机制

除了loader,插件(plugin)机制是webpack另一个核心特性。插件是为了增强webpack的自动化能力。

webpack常见插件

  • 自动清除上次打包结果:clean-webpack-plugin

    安装:

    npm i clean-webpack-plugin --dev
    

    使用:

    // webpack.config.js
    // 需要引入插件导出的CleanWebpackPlugin构造函数
    const { CleanWebpackPlugin } = require('clean-webpack-plugin')
    
    module.exports = {
        // ...other config
        plugin: [  // plugin属性的值是一个数组
            new CleanWebpackPlugin()
        ]
    }
    
  • 自动生成使用bundle.jsHTMLhtml-webpack-plugin

    安装:

    npm i html-webpack-plugin --dev
    

    使用:

    const HtmlWebpackPlugin = require('html-webpack-plugin')
    
    module.exports = {
        // ...other config
        plugin: [
            new HtmlWebpackPlugin({  // 可以传入一个对象对其进行配置
                title: 'Webpack',  // 设置title标签内容
                meta: {  // 设置meta标签属性
                    viewport: 'width=device-width'
                }
            })
        ]
    }
    

    如果需要大量的设置dom元素等,可以使用模板。

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <title>Webpack</title>
    </head>
    <body>
      <div class="container">
        <!-- 可以通过模板字符串的形式给对应的位置设上变量 -->  
        <h1><%= htmlWebpackPlugin.options.title %></h1>
      </div>
    </body>
    </html>
    

    同时在配置中加入template属性:

    new HtmlWebpackPlugin({
        template: './src/index.html'
    })
    

    如果需要输出多个页面文件,可以再添加一个插件实例:

    const HtmlWebpackPlugin = require('html-webpack-plugin')
    
    module.exports = {
        // ...other config
        plugin: [
            new HtmlWebpackPlugin({  // 可以传入一个对象对其进行配置
                title: 'Webpack',  // 设置title标签内容
                meta: {  // 设置meta标签属性
                    viewport: 'width=device-width'
                },
                template: './src/index.html'
            }),
            new HtmlWebpackPlugin({
                filename: 'about.html'  // 指定输出文件的名称
            })
        ]
    }
    
  • 处理不需要处理的静态文件:copy-webpack-plugin

    安装:

    npm i copy-webpack-plugin --dev
    

    使用:

    const CopyWebpackPlugin = require('copy-webpack-plugin')
    
    module.exports = {
        plugin: [
            new CopyWebpackPlugin([  // 传入一个数组,来指定需要拷贝的文件的路径,例如网站的ico
                './public/favicon.ico',  // 文件的相对路径
                './public',  // 或者整个文件夹
            ])
        ]
    }
    

webpack插件机制原理

plugin是由钩子机制(hooks)实现的。钩子机制类似“事件”,在webpack工作过程中会有很多的环节,为了webpack的扩展,每个环节都被埋下了钩子,然后在这些钩子下添加不同的任务就可以扩展webpack的能力。

webpack要求插件必须是一个函数或者是一个包含apply方法的对象。一般都会把它定义成一个类型,然后在类型里面添加一个apply方法,使用时通过这个类型去构建一个实例去使用。

class MyPlugin {
    // apply方法接收一个compiler对象
    // compiler对象是webpack工作时最核心的一个对象,它包含了此次构建的所有信息
    // 可以通过这个对象去注册钩子函数
    apply(compiler) {
        console.log('plugin working')
        
        // tap方法第一个参数是插件的名称
        // 第二个参数是个回调函数,里面传入的compilation可以理解成此次打包的上下文
        compiler.hooks.emit.tap('MyPlugin', compilation => {
            // compilation下的assets属性包含了此次打包过程中的各个文件
            for(const name in compilation.assets){
                if(name.endWith('.js')) {
                    // compilation.assets下每一个键的值都有source方法,该方法会返回当前文件的内容
                    const contents = compilation.assets[name].source()
                    // 替换js文件中的注释
                    const withoutComments = contents.replace(/\/\*\*+\//g, '')
                    // 然后再将当前name下的source方法和size给重写一遍
                    compilation.assets[name] = {
                        source: () => withoutComments,
                        // size方法会返回当前文件的大小,这个是必需的
                        size: () => withoutComments.length
                    }
                }
            }
        })
    }
}

这是以上示例中emit钩子的官方解释:

emit

AsyncSeriesHook

生成资源到 output 目录之前。

参数:compilation

相关钩子的官方文档: compiler

更深层的webpack的插件机制需要靠阅读源码来进行了解,后面有机会再写一篇关于webpack源码的笔记。

webpack自动编译

使用webpack-cli中的watch工作模式可以实现自动编译的功能。在watch模式下,会监视文件变化,然后再自动重新打包。

npx webpack --watch
#or
yarn webpack --watch

webpack dev server

使用webpack-dev-server可以实现“修改源代码,页面就能同步”的效果。而且通过传入open参数可以实现自动打开浏览器。

yarn webpack-dev-server --open

对于一些静态文件,可以通过配置devServer属性来让其在打包过程中不参与打包,这样就能提高源代码修改后重新打包的效率。

module.exports = {
    // ...other config
    devServer: {
        contentBase: './public'  // contentBase可以是一个路径的字符串也可以是一个包含多个路径字符串的数组
    }
}

当使用webpack-dev-server时,项目自然就是在本地localhost服务上启动的,此时发起网络请求就不可避免的产生跨域问题,此时可以通过配置devServer下的proxy属性来让带有指定请求路径前缀的请求被代理以此解决跨域问题。

module.exports = {
    // ...other config
    devServer: {
        contentBase: './public',
        proxy: {
            '/api': {  // 这个是请求路径前缀,所有以/api开头的请求都会被代理到接口中
                target: 'https://www.exanple.com',  // 代理目标,原本要访问的地址
                // 当请求https://localhost:8080/api/xxx时,会被代理到https://www.example.com/api/xxx
                // 若代理目标不需要这个请求前缀,可以通过pathRewrite属性实现路径重写
                pathRewrite: {
                    '^/api': ''
                },
                changeOrigin: true,  // 改变发送请求的host,
            }
        }
    }
}

Source Map

在开发过程中,打包后的结果报错需要通过source map来定位该错误在源代码中的位置,从而提高开发体验。

module.exports = {
    // ... otehr config
    devtool: 'source-map', // 开启source map
}

webpack支持大概12种不同的source map方式,每种方式的效率和效果都是不一样的,效果好的生成效率就会降低。以下是官方给出的不同模式下的效果和效率的对比:

devtoolperformanceproductionqualitycomment
(none)build: fastest rebuild: fastestyesbundleRecommended choice for production builds with maximum performance.
evalbuild: fast rebuild: fastestnogeneratedRecommended choice for development builds with maximum performance.
eval-cheap-source-mapbuild: ok rebuild: fastnotransformedTradeoff choice for development builds.
eval-cheap-module-source-mapbuild: slow rebuild: fastnooriginal linesTradeoff choice for development builds.
eval-source-mapbuild: slowest rebuild: oknooriginalRecommended choice for development builds with high quality SourceMaps.
cheap-source-mapbuild: ok rebuild: slownotransformed
cheap-module-source-mapbuild: slow rebuild: slownooriginal lines
source-mapbuild: slowest rebuild: slowestyesoriginalRecommended choice for production builds with high quality SourceMaps.
inline-cheap-source-mapbuild: ok rebuild: slownotransformed
inline-cheap-module-source-mapbuild: slow rebuild: slownooriginal lines
inline-source-mapbuild: slowest rebuild: slowestnooriginalPossible choice when publishing a single file
eval-nosources-cheap-source-mapbuild: ok rebuild: fastnotransformedsource code not included
eval-nosources-cheap-module-source-mapbuild: slow rebuild: fastnooriginal linessource code not included
eval-nosources-source-mapbuild: slowest rebuild: oknooriginalsource code not included
inline-nosources-cheap-source-mapbuild: ok rebuild: slownotransformedsource code not included
inline-nosources-cheap-module-source-mapbuild: slow rebuild: slownooriginal linessource code not included
inline-nosources-source-mapbuild: slowest rebuild: slowestnooriginalsource code not included
nosources-cheap-source-mapbuild: ok rebuild: slownotransformedsource code not included
nosources-cheap-module-source-mapbuild: slow rebuild: slownooriginal linessource code not included
nosources-source-mapbuild: slowest rebuild: slowestyesoriginalsource code not included
hidden-nosources-cheap-source-mapbuild: ok rebuild: slownotransformedno reference, source code not included
hidden-nosources-cheap-module-source-mapbuild: slow rebuild: slownooriginal linesno reference, source code not included
hidden-nosources-source-mapbuild: slowest rebuild: slowestyesoriginalno reference, source code not included
hidden-cheap-source-mapbuild: ok rebuild: slownotransformedno reference
hidden-cheap-module-source-mapbuild: slow rebuild: slownooriginal linesno reference
hidden-source-mapbuild: slowest rebuild: slowestyesoriginalno reference. Possible choice when using SourceMap only for error reporting purposes.

webpack HMR(模块热替换)

当开启webpack dev server后,会在开发过程中提供很多便利。但是还有些问题,比如当输入框内输入文本后再去修改源代码,此时dev server会重新渲染界面,之前输入框内的文本就会消失了。若想解决这样的问题需要使用HMR这个功能。HMR可以只将修改的模块实时替换到应用中而不是重新渲染整个页面。

HMR集成在webpack dev server之中,使用时需要在命令中加入hot参数来开启HMR:

yarn webpack-dev-serve --hot

也可以在配置文件中开启:

// 由于需要在插件中使用HMR,所以先导入webpack
const webpack = require('webpack')

module.exports = {
    // ...other config
    devServer: {
        hot: true
    },
    plugins: [
        // 使用这个插件
        new webpack.HotModuleReplacementPlugin()
    ]
}

这样会在样式文件修改时自动替换样式,对于js文件还需要额外的配置。

在打包的入口文件可以使用HMRAPI来实现js文件的热替换:

import header from './header.js'  // 引入的文件
// 引入的这个js文件是一个函数,其调用结果是在body上创建一个元素


// 通过module.hot.accept方法来处理需要热替换的文件
// 第一个参数是要处理的文件的路径,
// 第二个参数是文件发生改变后的回调函数
let head = header()  // 将结果保存下,便于后面更新时替换
module.hot.accept('./header.js', () => {
    // 在文件发生变化时将页面上的元素移除
    document.body.removeChild(head)
    const newHead = header()  // 这是新的
    document.body.appenChild(newHead)
    head = newHead // 将这次的结果保存,便于后面更新时替换
})

如果想要保存里面可输入这样的内容不变,也可以先将这样的内容保存下来,然后在更新后再给它重新赋值上去,这样就能保持内容不变。

对于每个不同的js文件都需要不同的处理函数来应对热替换。

DefinePlugin

在使用vue开发过程中,经常会看到process.NODE.ENV这样的全局属性。如果想要自定义这些属性就可以通过DefinePlugin来做到。

DefinePluginwebpack内置的一个插件,所以使用的时候直接引用就可以了:

const webpack = require('webpack')

module.exports = {
    // ...other config
    plugins: [
        new webpack.DefinePlugin({
            // 属性值的引号里面是需要一个js片段,这里我们需要的是字符串,所以需要在引号里面再加个引号
            API_BASE_URL: '"www.example.com"', // 为全局注入API_BASE_URL这个属性
        })
    ]
}

这样在项目中使用这个地址的话就可以直接使用API_BASE_URL这个地址,当需要修改这个地址时,只要在配置文件中修改一次即可,不需要全局去找地址然后修改。

Tree-Shaking(摇树)

在开发过程中,有时会有一些导出但未被引用的代码(Dead-code),这些代码对于整个项目来说是无用,所以需要不需要将其打包到生产环境中。将这些未引用代码给剔除的过程就叫摇树tree-shaking

webpack打包的过程中会有自动的tree-shaking。如果想要手动配置,也可以通过optimization这个属性来配置:

module.exports = {
    // ...other config
    optimization: {
        usedExports: true,  // 只导出外部使用了的成员
        minimize: true,  // 清除未被引用的成员
    }
}

usedExports相当于负责标记dead-codeminimize相当于负责清除dead-code

还可以通过concatenateModules这个属性来将所有的模块合并到同一个函数中以此减少体积。

注意:

Tree-Shaking的前提是使用ES Modules,也就是说交给webpack去打包的代码需要使用ES Modules去实现了模块化。

当使用了babel去转译代码时,它会先将ES Modules转换成CommonJS,导致tree-shaking失效。

但是结果却是tree-shaking并不会失效,因为在最新版本的babel-loader中已经关闭了ES Modules转换的插件。

SideEffects

通过配置的方式去标识代码是否有副作用,从而为tree-shaking提供更大的压缩空间。

sideEffects一般用于npm包标记是否有副作用。

module.exports = {
    // ...other config
    optimization: {
        sideEffects: true,  // 开启sideEffects
    }
}

开启后打包文件webpack会先检查package.json中是否有sideEffects标识,以此来判断这个模块是否有副作用。如果没有副作用,那么这些没有用到的模块就不会去打包。

{
    "sideEffects": false
}

设为false是表示整个项目没有副作用。

注意:

使用sideEffects时要确定整个代码没有副作用,否则打包时会删除有副作用的代码。

比如当有的js文件只进行一些操作而未导出成员时,这类代码就有副作用:

import Vue from 'vue'

Vue.prototype.$http = xxx

还有打包入口导入的样式文件,也会有副作用。

解决办法就是关闭sideEffects或者在package.json中标识这些文件:

{
    "sideEffects": [
        "./utils/http.js",
        "./assets/styles/main.less"
    ]
}

也可以使用通配符的方式来配置:*.less

Code Splitting

在打包过程中,所有模块都会被打包进入同一个模块,这就导致生产环境下在项目启动时会一次性加载所有模块,从而产生性能问题。

这时候就需要根据不同的模块需要来分包按需加载。

webpack实现分包的方式主要有两种:

  • 多入口打包
  • 动态导入

Multi Entry(多入口打包)

一般适用于多页面应用程序,常见的规则就是一个页面对应一个打包入口,公共代码可以单独提取。

const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
    // ...other config
    // entry属性的值改成一个对象,一个路径对应一个页面
    entry: {
        index: './src/index.js',
        detail: './src/detail.js'
    },
    output: {
        // 输出的文件名也需要修改
        filename: '[name].bundle.js',  // 这样是动态输出文件名,name对应entry里面的属性名
    },
    plugins: [
        // 设置多个页面
        new HtmlWebpackPlugin({
            title: 'index',
            template: './src/index.html',
            filename: 'index.html',
            // 设置chunks来让每个页面只引入自己的chunk,而不是一下引入所有的chunk
            chunks: ['index']
        }),
        new HtmlWebpackPlugin({
            title: 'detail',
            template: './src/detail.html',
            filename: 'detail.html',
            chunks: ['detail']
        })
    ]
}

当不同的页面有相同的模块时,还需要额外的配置:

module.exports = {
    // ...other config
    optimization: {
        splitChunks: {
            chunks: 'all'  // 设置所有的公共模块都提取到单独的bundle中
        }
    }
}

动态导入

需要某个模块时才加载这些模块。所有动态导入的模块都会被自动分包。

在项目导入时,可以这样:

const hash = window.location.hash || '#index'

if(hash === '#index') {
    // 使用import函数导入
    import('./utils/greeting').then(({ default }) => {
        default()
    })
}

Magic Comments(魔法注释)

当需要给分包后的文件取名时,可以使用魔法注释来实现bundle的自定义名称:

// 在import函数的路径前面加上注释
import(/* webpackChunkName: 'greeting' */ './utils/greeting')

相同的chunk name会被打包到一起

MiniCssExtractPlugin

MiniCssExtractPlugin可以将css模块提取出来打包到单独的文件中实现css文件的懒加载。

const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = {
    module: {
        rules: [
            {
                test: /.css$/,
                use: [
                    // style-loader改成MiniCssExtractPlugin下的loader
                    MiniCssExtractPlugin.loader,
                    'css-loader'
                ]
            }
        ]
    },
    // ...other config
    plugins: [
        new MiniCssExtractPlugin()
    ]
}

不使用这个插件时,css模块将通过css-loaderstyle-loader的加工后在页面上生成一个style标签,而使用MiniCssExtractPlugin后,样式将会被存放在单独的文件中,然后通过link的方式去引入。

当样式文件不大时(size <= 150kb),建议使用style-loader

optimize-css-assets-webpack-plugin

上面生成的样式文件并不会被压缩,所以需要这个插件来将其进行压缩一下:

const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')

module.exports = {
    // ...other config
    optimization: {
        minimizer: [
            // 再次配置js压缩插件
            new TerserWebpackPlugin(),
            new OptimizeCssAssetsWebpackPlugin()
        ]
    }
}

OptimizeCssAssetsWebpackPlugin放在optimization下的minimizer属性中而不是在plugins中的原因是便于统一通过minimize属性来控制打包结果是否压缩。

但是这样会有一个问题,就是js文件不会自动压缩了。这时就需要手动配置js压缩插件。

substitutions(输出文件名hash)

在部署静态资源文件时,都会启用静态文件缓存。当静态资源缓存时间过短可能会导致没什么效果,过长则在更新后没有办法更新到客户端。

所以在生成模式下,可以给文件名使用hash,这样当文件发生改变时,文件名也可以随之改变。当文件名改变时,浏览器就会重新发起请求。这样就可以在缓存策略中把缓存时间设置的很长。

module.export = {
    // ...other config
    output: {
        // 这样是整个项目级别的hash
        filename: '[name]-[hash].bundle.js'
        // 这样是chunk级别的hash
        // filename: '[name]-[chunkhash].bundle.js'
        // 这样是文件级别的hash
        // filename: '[name]-[contenthash].bundle.js'
        // 还可以指定hash的长度
        // filename: '[name]-[contenthash:8].bundle.js'
    }
}