38

一看就懂之webpack高级配置与优化
一看就懂之webpack原理解析与实现一个简单的webpack

一、webpack 简介

本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。

简单说,webpack可以看做是一个模块打包机,主要作用就是: 分析你的项目结构,找到JavaScript模块以及一些浏览器不能直接运行的拓展语言(sass、less、typescript等),然后将它们打包为合适的格式以供浏览器使用。

webpack主要实现的功能:

代码转换(如: ES6转换ES5、sass和less转换为css等)
文件优化(如: 将模块内容进行压缩)
代码分割(如: 多页面应用公共模块的抽离、路由懒加载)
模块合并(如: 按照不同的功能将多个模块合并为一个模块)
自动刷新(如: 启动本地服务,代码更新后进行自动刷新)
代码校验(如: 添加eslint进行代码规范检查)
自动发布(如: 应用打包完成后,自动发布)

二、webpack 安装

在webpack 3中,webpack本身和它的CLI以前都是在同一个包中的,但在第4版之后,已经将两者分开来更好地管理它们,所以安装webpack4之后的版本,要同时安装webpack和webpack-cli

注意,安装webpack的时候,必须进行本地安装才能生效,否则会报错,如:
图片描述
但是全局安装了也有一个好处,那就是可以在项目根目录下直接执行webpack即可完成项目的打包,如果没有进行全局安装,那么可以通过npx直接执行项目本地安装的模块,即 npx webpack也可以完成项目的打包。

三、webpack 基础配置

webpack4是支持零配置的,即不需要配置文件即可完成打包,其默认入口文件为项目根目录下的src目录下的index.js文件,其默认出口为项目根目录下的dist目录的main.js

如果没有给webpack添加配置文件,那么webpack的打包能力就会非常弱,webpack执行的时候默认会加载项目根目录下的webpack.config.js文件,注意,该配置文件是一个js文件,而不是json文件,并且其是通过node去执行的,所以其完全支持node语法,即node中能用的,在配置文件中都可以用
webpack配置文件必须要对外暴露一个对象,即通过module.exports进行对外暴露,其中的所有配置都必须写在这个对外暴露的对象中。

mode
mode用于配置当前webpack所使用的模式,其支持nonedevelopmentproduction三个值。如果不配置mode,那么可以正常打包出结果,但是打包后会提示一些警告信息,如:

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/

也就是说,当前配置文件中没有配置mode,webpack将会给以一个默认值production,由于production和development都赋予了一些默认行为,所以如果想关闭这些默认行为,可以将mode设置为none

none: 就是webpack打包后最初的样子,没有赋予任何默认行为,比如传递给webpack运行函数的modules是一个数组,如:

(function(modules){
    // 返回数组的第0项(入口)模块的值
    return __webpack_require__(__webpack_require__.s = 0);
})([
    (function(module, exports) {
        // 模块内容
    })
])

development: webpack将以开发模式进行打包,development模式下设置了一些默认行为,比如: 给每个模块命名给每个chunk命名设置NODE_ENV环境变量开启sourceMap
NamedModulesPlugin: 可以给模块设置一个名字,以该模块的相对路径为名称,使用该插件后,传入webpack运行函数的modules就变成一个对象形式了,因为以数组的索引命名的方式对于开发者而言难于阅读,新增模块的时候可能会导致模块索引发生变化

module.exports = {
    mode: "none",
    plugins: [
        new webpack.NamedModulesPlugin() // 给模块命名
    ]
}

NamedChunksPlugin: 可以给chunk设置一个名字,从webpack打包输出信息中可以看到,使用该插件前chunks名称仍然为索引号,使用该插件后,chunks名称也会用于一个自己的名字

Built at: 2020-06-18 19:01:42
  Asset     Size  Chunks             Chunk Names
main.js  3.7 KiB       0  [emitted]  main
Entrypoint main = main.js
module.exports = {
    mode: "none",
    plugins: [
        new webpack.NamedModulesPlugin(), // 给模块命名
        new webpack.NamedChunksPlugin() // 给chunk命名
    ]
}
Built at: 2020-06-18 19:05:44
  Asset     Size  Chunks             Chunk Names
main.js  3.7 KiB    main  [emitted]  main
Entrypoint main = main.js

DefinePlugin: 给webpack设置一个环境变量,development模式下会将NODE_ENV设置为development,如:

module.exports = {
    mode: "none",
    plugins: [
        new webpack.NamedModulesPlugin(), // 给模块命名
        new webpack.NamedChunksPlugin(), // 给chunk命名
        // 设置proces.env.NODE_ENV 为development
        new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("development") })
    ]
}

所以在development模式下,我们可以直接在模块中通过proces.env.NODE_ENV拿到对应的值development
devtool: 设置源码映射类型,development模式下会将devtool设置为eval,打包后会用eval()方法将模块代码包裹起来,并且整个模块代码变成了一行。未设置devtool的时候则是以模块打包前的样子显示。

production: webpack将以生产模式进行打包,production模式下设置了一些默认行为,比如: 压缩JS开启Scope Hoisting设置NODE_ENV环境变量忽略编译错误等。

module.exports = {
    mode: "none",
    plugins: [
        new UglifyJsPlugin(), // 压缩JS
        new webpack.optimize.ModuleConcatenationPlugin(), // 开启Scope Hoisting
        new webpack.DefinePlugin({ "process.env.NODE\_ENV": JSON.stringify("production") }), // 设置环境变量
        new webpack.NoEmitOnErrorsPlugin() // 忽略编译错误
    ]
}

context
context属性表示的是webpack的上下文目录必须是绝对路径,配置入口文件的时候,如果入口文件使用的是相对路径,那么就是相对于context所在的目录。

context默认值为执行webpack命令时所在的当前工作目录,通常是在项目根目录下执行webpack命令,所以可以认为其值默认为项目根目录,所以如果入口文件路径写成相对路径,最好将context配置成context: path.resolve(__dirname),以防止在非项目根目录下执行webpack命令时找不到入口文件路径而报错。

真实项目中,我们的webpack配置文件通常不会放到项目根目录下,而是放在build等文件夹下,为了让入口文件始终相对于项目根目录,我们需要配置context为项目根目录

entry

entry用于配置模块的入口文件,可以配置多个,webpack将从入口文件开始搜索以及递归解析所有入口文件依赖的模块,其是必填的,如果配置文件中没有entry则会报错。entry的属性值可以是表示路径的单个字符串也可以是数组,数组中的元素为入口文件的路径,还可以是对象,对象的属性名为入口文件的chunk名,即打包后输出文件的名字,属性值为入口文件的路径。注意,入口文件的路径可以是绝对路径也可以是相对路径,相对路径默认是以配置文件中的context属性值表示的路径
module.exports = {
    entry: "./foo.js" // 属性值为一个表示路径的字符串
    //entry: "foo.js" // 这种直接写文件名的方式是不支持的,必须带上./或者../
}
其输出结果文件取决于配置文件的output配置,如果output.filename没有配置,则默认输出为main.js,如果output.filename值为指定的名称,则输出结果为output.filename的属性值
module.exports = {
    /*
     * 引入最终打包后输出的文件后,模块的执行顺序为foo.js --> bar.js,即按数组中的顺序执行
     * 引入最终打包后输出的文件后,拿到的输出将会是bar.js的模块输出,即数组中最后一个元素的模块输出
     */
    entry: [ "./foo.js", "./bar.js"] // 属性值为一个数组
}
其输出结果文件也是取决于配置文件的output配置,只不过,其会将foo.js和bar.js一起打包输出为一个文件,如果output.filename没有配置,则默认输出为main.js,如果output.filename值为指定的名称,则输出结果为output.filename的属性值
module.exports = {
    entry: { // 属性值为一个对象
        a: "./src/bar.js",
        b: "./src/foo.js",
        c: "./src/index.js"
    }
}
其输出结果不再取决于output.filename的配置,因为entry已经指定好了模块输出的chunk名,即会分别输出a.js、b.js和c.js三个文件,并且此时output.filename属性值不能配置为一个固定名称的输出文件,因为入口文件有多个,必然输出文件也会有多个
chunk和module的区别,二者都是表示模块,但是module可以看做是具有独立功能的小模块,即小块,也就是打包之前,程序员编写的一个一个的文件,每个文件称为一个module;而chunk则可以看做是由多个小module打包成的大模块,即大块

output
output配置的是如何输出最终想要的代码,output是一个object。

path: 用于配置打包后输出文件的本地存放目录,必须是绝对路径,当然也可以不配置,因为如果没有配置path,那么会自动在执行webpack命令时所在的目录下自动创建dist目录并将打包结果输出到dist目录下,与context的配置路径无关

module.exports = {
    output: {
        path: path.resolve(__dirname, "./dist") // 必须是绝对路径
    }
}

filename: 用于配置输出文件的名称,如果只有一个输出文件,那么可以配置成静态不变的文件名,如:

module.exports = {
    output: {
        filename: "bundle.js"
    }
}

但是,如果有多个chunk要输出时,即入口文件配置了多个时,那么filename就不能配置成静态不变的了,就必须借助模板和变量了,常见的两个变量,如:
[name]: 可以自动获取到入口文件配置的chunk名称;
[hash]: 可以自动生成整个项目的文件对应的hash值,hash值的长度是可以指定的,默认为20位;
[chunkhash]: 生成输出的chunk文件所包含内容对应的hash值,hash值的长度是可以指定的,默认为20位
[contenthash]: 针对文件内容所产生的hash,只要文件内容没有变化就不会导致hash值变化,通常用于分离出来的css文件,因为我们通过js引入css后再将css分离出来后,如果和js一样用chunkhash,那么当我们仅仅修改js也会导致css文件的hash值变化,而实际上css文件并没有发生变化,导致缓存失效。

如果使用的是hash,那么打包输出目录下的所有chunk都会使用同一个hash值,项目中文件修改后,所有chunk的hash值也会跟着变化,不利于缓存
如果使用的是chunkhash,那么打包输出目录下的每一个chunk都有一个各自的hash值,项目中文件修改后,只会导致包含该文件的chunk的hash值变化利于缓存

module.exports = {
    output: {
        filename: "[name][hash:8].js" // 以入口文件设置的chunk作为输出名,并且指定hash值为8位
        // filename: "js/[name].js" // 可以带上目录,将打包后的js放到指定的目录下
    }
}

chunkFilename: 用于指定动态生成的模块的输出路径和文件名。比如,通过import()方法动态引入的模块打包后输出的路径和文件名,用法同filename一致。

library和libraryTarget: 用于指定将模块的输出结果挂载到哪个地方或者以什么样的方式导出库(模块输出结果)。二者通常要搭配一起使用。
libraryTarget通常用于指定以何种方式导出库,默认值为var, library通常用于指定接收库的名称无默认值
我们将入口的一个或多个js文件打包输出后的结果也是一个js文件,在没有配置library和libraryTarget的时候,这个输出的js文件中包含了一个匿名自执行函数, 即输出文件的执行结果没有被任何东西接收,我们引入输出文件执行后不会得到任何结果。 如:

(function(){
    console.log("foo");
    return "foo"; // 虽然有返回值,但是匿名自执行函数执行完毕后拿不到任何结果
})();
// 我们以var 变量 的方式来接收函数的返回值
var foo = (function(){ // 匿名自执行函数执行完毕后就会将函数返回值保存到foo变量上
    console.log("foo");
    return "foo";
})();
console.log(`foo is ${foo}`);
打包后的输出文件的输出结果(导出结果),就是入口文件的输出结果(导出结果),即入口文件通过export、exports、module.exports等输出(导出)的结果

var: 将libraryTarget设置为var, 同时指定一个自定义的变量名来接收模块的输出,这个自定义的变量名就是library的属性值

module.exports = {
    output: {
        filename: "bundle.js",
        path: path.resolve(__dirname, "./dist/"),
        libraryTarget: "var",
        library: "test"
    }
}

模块的输出结果将会赋值给test变量,其输出文件bundle.js内容大致如下:

var test = (function(modules){
    return result; // 返回值result将会被赋值给test变量 
})();

commonjs: 将libraryTarget设置为commonjs, 即通过commonjs规范导出,同时指定一个自定义的变量名来接收模块的输出,这个自定义的变量名就是library的属性值, 只不过这个自定义的变量名是exports的属性名,如:

module.exports = {
    output: {
        filename: "bundle.js",
        path: path.resolve(__dirname, "./dist/"),
        libraryTarget: "commonjs",
        library: "test"
    }
}

模块的输出结果将会赋值给exports["test"]上,其输出文件bundle.js内容大致如下:

exports["test"] = (function(modules){
    return result; // 返回值result将会被赋值给exports["test"]
})();

commonjs2: 将libraryTarget设置为commonjs2,即通过commonjs2规范导出,此时library的配置将无意义,因为commonjs2的输出是固定的module.exports,所以不需要指定library了,如:

module.exports = {
    output: {
        filename: "bundle.js",
        path: path.resolve(__dirname, "./dist/"),
        libraryTarget: "commonjs2"
    }
}

模块的输出结果将会被赋值到module.exports上,其输出文件bundle.js内容大致如下:

module.exports = (function(modules){
    return result; // 返回值result将会被赋值给module.exports
})();
commonjs和commonjs2的区别在于,commonjs只能使用exports进行导出,而commonjs2在commonjs的基础上增加了module.exports进行导出;

this: 将libraryTarget设置为this, 那么此时library配置的变量名将作为this的属性名来接收模块的导出结果,如:

module.exports = {
    output: {
        filename: "bundle.js",
        path: path.resolve(__dirname, "./dist/"),
        libraryTarget: "this",
        library: "test"
    }
}

模块的输出结果将会被赋值到this["test"] 上,其输出文件bundle.js内容大致如下:

this["test"] = (function(modules){
    return result; // 返回值result将会被赋值给this["test"]
})();

同理libraryTarget的属性值还可以是windowglobal,这里就不一一列举了。

publicPath
publicPath用于配置打包资源发布到线上时服务器的url地址,打包完成后,html文件中如果引入了js、image、css等资源,那么都会在前面加上publicPath所表示的路径

module.exports = {
    output: {
        filename: "bundle.js",
        path: path.resolve(__dirname, "./dist/"),
        publicPath: "http://www.lihb.com/"
    }
}

// index.html

<!DOCTYPE html>
<html lang="en">
<body>
    <script type="text/javascript" src="http://www.lihb.com/index.js"></script></body>
</html>

四、webpack 打包输出后的内容分析

webpack打包输出后的结果默认是一个匿名自执行函数,匿名自执行函数传递的参数为一个对象,对象的属性名为入口文件的路径名属性值为一个函数,函数体内部通过会执行eval(),eval()方法的参数为入口文件的内容字符串,而这个匿名自执行函数,内部有一个自定义的__webpack_require__方法,该方法需要传入入口文件的路径名作为参数,匿名自执行函数执行完成后会返回__webpack_require__的结果,而__webpack_require__()方法内部执行的时候,会首先创建一个module对象module对象里面有exports属性,属性值为一个空的对象,用于接收入口文件的模块输出,如:

(function(modules) {
    function __webpack_require__(moduleId) { // 传入入口文件的路径
        var module = installedModules[moduleId] = { // 创建一个module对象
             i: moduleId,
             l: false,
             exports: {} // exports对象用于保存入口文件的导出结果
         };
        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); // 执行入口文件
        return module.exports; // 返回模块输出结果
    }
    return __webpack_require__(__webpack_require__.s = "./src/bar.js"); // 返回入口文件
})({
     "./src/bar.js": (function(module, exports) {
         eval("module.exports = \"bar\";");
     })
  });
所以不管入口文件是以ES6模块的方式输出还是以commonjs模块的方式输出,最终入口文件的模块输出结果都会被绑定到__webpack_require__方法中定义的module对象的exports属性上,只不过,如果是以commonjs的方式输出,那么入口文件的输出结果将会直接替换掉__webpack_require__方法中定义的module对象的exports属性;如果是以ES6模块的方式输出,则是在__webpack_require__方法中定义的module对象的exports属性值中添加一个default属性或者具体变量名来保存入口文件的输出
// webpack中任何一个模块被require或者import的时候,都会被转换为__webpack_require__
import foo from "./foo.js"; // 等价于 __webpack_require__("./foo.js")
const foo = require("./foo.js"); // 等价于 __webpack_require__("./foo.js")

webpack打包后,会将整个模块的代码解析成字符串,并放到eval()中执行,然后用一个函数包裹起来,如:

function(module, module.exports, __webpack_require__) {
 eval("当前模块代码");
}

如果一个模块使用的是export default 导出,如:

export default "foo";

那么包裹函数如下:

function(module, __webpack_exports__, __webpack_require__) { // __webpack_exports__对象的值就是module.exports只是换了个名字
 eval("__webpack_require__.r(__webpack_exports__);__webpack_exports__[\"default\"] = (\"foo\");");
}

执行的时候,首先将module.exports对象交给__webpack_require__.r()方法进行处理,主要就是给module.exports对象添加一个__esModule属性,值为true。

__webpack_require__.r = function(exports) {
    if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
        Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
    }
    Object.defineProperty(exports, '__esModule', { value: true });
};

然后在module.exports对象上添加一个defaul属性,值为模块的输出,最终输出的module.exports对象为:

// 模块输出对象
{default: "foo", __esModule: true}

如果一个模块使用的是export 非default导出,如:

export const foo = "foo";

那么包裹函数如下:

function(module, __webpack_exports__, __webpack_require__) { // __webpack_exports__对象的值就是module.exports只是换了个名字
 eval("__webpack_require__.r(__webpack_exports__);__webpack_require__.d(__webpack_exports__, \"foo\", function() { return foo; });\nvar foo = \"foo\";");
}

也是先执行的时候,首先将module.exports对象交给__webpack_require__.r()方法进行处理,主要就是给module.exports对象添加一个__esModule属性,值为true
然后通过__webpack_require__.d()函数给module.exports对象定义export的属性

__webpack_require__.d = function(exports, name, getter) {
    if(!__webpack_require__.o(exports, name)) {
        Object.defineProperty(exports, name, { enumerable: true, get: getter });
    }
}

最终输出的module.exports对象为:

// 最终的模块输出对象
{foo: "foo", __esModule: true}

如果一个模块是通过module.exports 导出,如:

module.exports = "foo";

那么包裹函数如下:

function(module, exports) {
    eval("module.exports = \"foo\";");
}

直接给module.exports对象赋值为"foo"即可
模块最终的输出结果为module.exports对象,即"foo"

// 模块最终输出结果
"foo"

如果一个模块是通过exports导出,如:

exports.foo = "foo";

那么包裹函数如下:

function(module, exports) {
    eval("exports.foo = \"foo\";");
}

直接给module.exports对象添加foo属性
模块最终的输出结果为module.exports对象,即

// 模块最终输出结果
{foo: "foo"}

不管使用的是什么方式引入,拿到的结果都是module.exports这个对象,只不过不同的方式引入,使用的时候会有一定的差别

如果使用的是require的方式引入模块,如:

const foo = require("./foo");
console.log(foo);

那么require()拿到的结果就是module.exports的值,使用的时候也是直接用的module.exports这个对象
所以如果引入的模块使用的是ES模块标准导出,那么require方式拿到的将是这样一个对象

{default: "foo", __esModule: true}
{foo: "foo", __esModule: true}

如果使用的是import的方式引入模块,如:

import foo from "./foo.js";
console.log(foo);

那么拿到的结果是module.exports对象,但是使用的时候用的是module.exports对象的default属性的值

import {foo} from "./foo.js";
console.log(foo);

那么拿到的结果是module.exports对象,但是使用的时候用的是module.exports对象的foo属性的值

所以我们最好用import的方式来引入,模块的输出可以使用commonjs也可以使用ES模块标准,引入的时候能够正确匹配即可。

五、webpack 本地服务器配置

为了更方便调试,我们需要用到webpack的本地http服务器功能,要想使用webpack提供的Web服务器功能,我们需要安装webpack-dev-server模块,webpack-dev-server会启动一个web服务器用于实现网页请求,也可以监听文件的变化自动刷新网页。

webpack-dev-server模块安装完成后,我们可以在项目根目录下运行npx webpack-dev-server,其和webpack命令一样,如果没有配置文件,则使用内置默认配置进行打包输出,如果有则使用配置文件中的配置,只不过其不会将打包结果输出到指定的目录中,因为webpack-dev-server会忽略配置文件中的output.path配置,其会将打包输出结果保存到内存中。webpack-dev-server启动后会默认将启动devServer时所在的目录作为根目录,即执行npx webpack-dev-server命令时所在的目录

webpack提供了一个devServer属性用于配置启动的服务器的一些参数,当然webpack本身是无法识别devServer属性配置的,只有通过webpack-dev-server去启动webpack时,devServer的配置才会生效

module.exports = {
    devServer: {
        port: 3000, // 让devServer监听3000端口
        contentBase: "./dist", // 将当前项目的dist目录作为devServer的根目录
        progress: true, // 显示打包进度条
        compress: true // 是否启用Gzip压缩,默认为false
    }
}
webpackDevServer启动后,默认会自动监听打包源文件的变化,如果修改了打包源文件,那么会自动重新打包到内存,并且会自动刷新浏览器,但是自动刷新浏览器功能必须将target设置成web,否则自动刷新功能将会失效,比如target为node就无法起作用。

六、webpack 插件配置

在不使用插件的时候,webpack默认只能打包输出js文件,如果我们想要输出其他格式的文件到输出目录中,那么我们必须使用插件。webpack提供了一个plugins属性用于配置使用到的插件,其属性值为一个数组,数组中的元素为插件对象,通常插件都是一个类,我们需要通过插件类来创建一个插件对象
html-webpack-plugin
该插件可以指定一个html文件,webpack会将该html文件打包输出到输出目录中,同时会将打包输出后的文件自动插入到该html文件中,即让该html文件自动引入打包后的js文件

module.exports = {
    plugins: [
        new HtmlWebpackPlugin({
            template: "./src/index.html", // 要打包输出哪个文件,可以使用相对路径
            filename: "index.html", // 打包输出后该html文件的名称
            minify: {
                removeAttributeQuotes: true, // 去除html文件中的引号
                collapseWhitespace: true // 合并空格,即将html进行单行显示
            },
            hash: true // 向html文件中引入打包后的js时候带上hash值
        })
    ]
}
html插件中配置了hash为true, 是在引入打包后的js的时候带上hash值,如:
<script type="text/javascript" src="main.js?c7086a400fa368e84ad6"></script></body>

七、webpack 模块配置

webpack默认将所有格式的文件都当做模块进行处理,但是wepback默认只能处理js模块。如果在js中通过require引入了其他格式的模块(文件),那么webpack就必须通过安装合适的模块加载器,才能正确解析对应的模块内容,webpack提供了一个module属性,用于进行模块解析器的配置,其属性值为一个对象,对象中有一个rules属性,其属性值为一个数组,数组中的元素为一个对象,该对象主要完成两件事,匹配对应格式的文件,并且使用对应模块加载器进行加载,匹配使用的是test属性,属性值为一个正则表达式,【使用】使用的是use属性,属性值可以是字符串也可以是数组,如果只有一个模块加载器的时候,可以使用字符串的形式,如果有多个模块加载器的时候,那么就需要使用数组的形式,当然,如果模块加载器需要传递参数配置,那么可以将模块加载器写成对象的形式,通过loader属性指定模块加载器名称,通过options属性传递参数配置。

① 处理css样式,需要使用到css-loader和style-loader。
首先需要安装css-loader和style-loader。
css-loader必须同时和style-loader一起使用才能正确加载css文件,一个负责加载,一个负责插入css-loader负责加载css, 即在js文件中能够通过require的方式引入css,即加载和解析css,同时支持在css文件中使用@ import的方式引入其他css文件style-loader负责将加载并解析好的css文件插入到html文件中去,从名字可以看出其是在html文件中生成style标签来引入css文件,loader的执行顺序是从右向左,所以必须先加载然后再插入

比如,打包入口文件index.js中通过require的方式引入了一个index.js文件,即require("./index.css"),那么webpack需要进行如下配置:

module.exports = {
    module: {
        rules: [
            {
                test: /\.css$/, // 匹配以.css结尾的文件
                use: [ // 并交给css-loader和style-loader进行处理
                    {
                        loader: "style-loader", // 以对象的形式配置loader
                        options: { // 通过options给loader传递参数
                            insertAt: 'top' // 默认为bottom, 将加载的css文件插入到head标签的最上面,即优先级最低,会被覆盖
                        }
                    },
                    "css-loader" // 直接以字符串的形式配置loader
                ]
            }
        ]
    }
}
打包输出后,会将index.css中的内容放到<style>标签中,并且将这个<style>标签自动插入到index.html文件的<head>标签的最底部,如果配置了insertAt: "top", 那么就会插入到<head>标签的最上面。
需要注意的是,style-loader并不会在打包完成后直接将require的css以style标签的形式插入到html文件中,而是在引入js执行后,会动态的将require的css以style标签的形式插入到html文件中
同理,我们也可以出来sass,即scss文件,要处理scss文件,我们需要安装sass-loader,而sass-loader需要配合node-sass一起使用,安装好sass-loader之后我们只需要再添加一个rule,改成匹配.scss结尾的模块,处理sass和处理css都需要用到style-loader和css-loader,只不过处理sass还需要sass-loader,即需要用sass-loader将scss文件转换为css文件,在css-loader后加一个sass-loader即可。

八、webpack 样式的高级处理

抽离样式
我们通过css-loader和style-loader处理css样式后是直接通过<style>标签将解析后样式插入到了html中,如果需要将css样式抽离成一个单独的css文件, 并且自动link进html文件中,那么就需要mini-css-extract-plugin这个插件。

首先安装mini-css-extract-plugin插件,然后创建插件对象,并进行相应的配置,主要就是filename,即抽离出的css文件叫什么名字
module.exports = {
    plugins: [
        new MiniCssExtractPlugin({
            filename: "static/css/index.[contenthash:8].css", // 抽离出的css文件叫什么名字,前面可加路径
        })
    ]
}
filename属性值中可以添加上一个路径,实现css资源的分类输出,上面index.css文件就会输出到输出目录下的css目录下
MiniCssExtractPlugin的配置参数可以什么都不配置,因为filename的默认值为"[name].css",也就是说,其默认会以打包输出的js文件同名,也可以使用[name]-[hash:8].css
如果entry中配置了多个入口(index.js和foo.js),也就是说有多个输出结果,并且index.js和foo.js中都引入了同一个bar.css文件,那么将会输出index.cssfoo.css两个文件,但是这两个css文件的内容都和bar.css一样
插件安装配置好之后还不行,因为还要对loader进行相应的配置,之前css文件是通过了style-loader处理的,而style-loader会将样式通过<style>标签的方式插入到html文件中,所以必须先移除style-loader,然后使用mini-css-extract-plugin这个插件提供的loader
module.exports = {
    module: {
        rules: [
            {
                test: /\.css$/, // 匹配以.css结尾的文件
                use: [ // 并交给css-loader和MiniCssExtractPlugin的loader进行处理
                    MiniCssExtractPlugin.loader, // 将css进行抽离
                    "css-loader" // 直接以字符串的形式配置loader
                ]
            },
        ]
    }
}
css文件分离出来后,会自动在html文件中通过link标签引入只有html中引入的js中通过require或import引入的css才会被link进html文件中
// index.html
<link href="./static/css/index.css" rel="stylesheet">

压缩css
要想压缩css,那么需要用到optimize-css-assets-webpack-plugin插件,即优化CSS资源插件

module.exports = {
    plugins: [
        new OptimizeCSSAssetsPlugin({}) // 压缩css,可以不用传递任何配置
    ]
}

带配置参数配置如下:

new OptimizeCssAssetsPlugin({
    assetNameRegExp: /\.css$/g,
    cssProcessor: require('cssnano'),
    cssProcessorPluginOptions: {
        preset: ['default', { discardComments: { removeAll: true } }],
    },
    canPrint: true
})

对于用于压缩JS和CSS的插件,webpack4中给我们提供了另一种方式配置,我们可以将插件配置到optimization.minimizer数组中,放到optimization.minimizer数组中的好处是,我们可以通过optimization.minimize属性来控制是否开启压缩功能,也就是说只有将optimization.minimize属性设置为true,optimization.minimizer数组中配置的插件才会生效。如:

module.exports = {
    optimization: {
        minimize: true, // 开启minimizer数组中配置的插件
        minimizer: [
            new OptimizeCssAssetsPlugin() // 压缩css
        ]
    }
}

③ 给css自动加前缀
有的时候为了兼容性,我们需要给我们的css加上一些前缀,如-webkit--ms--moz-,我们需要用到postcss-loaderautoprefixer,即在css-loader之前,先用post-loader处理,然后使用autoprefixer插件进行处理,如:

// index.css
#app {
    display: flex;
}
module.exports = {
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [
                    "style-loader",
                    "css-loader",
                    {
                        loader: "postcss-loader", // 用于给css加前缀
                        options: {
                            plugins: [
                                require('autoprefixer')
                            ]
                        }
                    }
                ]
            }
        ]
    }
}

同时还需要修改package.json文件,需要设置浏览器版本信息,否则无法自动添加css前缀,如:

// package.json文件
{
    "browserslist": [
        "last 100 versions" // 向后兼容所有浏览器的后100个版本
      ]
}

此时打包可以看到最终css代码如下:

// 打包后的index.css
#app {
    display: -webkit-box;
    display: -webkit-flex;
    display: -moz-box;
    display: -ms-flexbox;
    display: flex;
}

postcss-loader中配置的options可以放到postcss.config.js配置文件中。

// postcss.config.js
module.exports = {
    plugins: [ // postcss-loader中的options部分
        require('autoprefixer')
    ]
}

④ 移除未使用到的css样式
样式文件中很可能会出现一些未被使用到的css样式,我们可以通过purgecss-webpack-plugin插件将其去除,如:

const PurgecssWebpackPlugin = require("purgecss-webpack-plugin");
module.exports = {
    plugins: [
        new PurgecssWebpackPlugin({
            paths:["public/index.html"] // 检测public目录下的index.html中未使用到的css样式
        })
    ]
}

如果有多个文件,那么我们可以通过glob模块通过正则表达式进行匹配搜索,如:

const PurgecssWebpackPlugin = require("purgecss-webpack-plugin");
const glob = require("glob");
module.exports = {
    plugins: [
        new PurgecssWebpackPlugin({
            paths: glob.sync(`public/**/*`, { // 即同步搜索,匹配public目录下所有的文件,返回一个包含所有匹配到文件的数组
                nodir: true // 不包含目录
            })
        })
    ]
}

九、webpack JS的高级处理

① 将ES6以上的高级语法特性转换ES5
Babel是一个JavaScript的编译器,能将ES6的代码转换为ES5的代码,我们需要安装babel-loader来处理我们js文件,而其需要配合@babel/core模块一起使用,还要告诉babel当前要转换的JS代码中使用了哪些新特性,即预设,我们使用包含当前所有最新ES语法特性的预设即可,@babel/preset-env

module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                use: [
                    {
                        loader: "babel-loader", // 使用babel-loader进行处理js文件
                        options: {
                            presets: ["@babel/preset-env"] // 用了最新的ES6语法预设
                        }
                    }
                ]
            }
        ]
    }
}
options部分可以写到.babelrc文件中,并且options可以去除也可以不去除,都能正确运行,但两边必须一致,否则可能会出错,比如.babelrc中配置成presets:["env"],而options中配置的是presets:["@babel/preset-env"],就会报错,正确的.babelrc文件如下:
// .babelrc
{
    "presets": ["@babel/preset-env"]
}

② 减少代码冗余
babel在转换ES6语法的时候,会使用一些由ES5编写的帮助函数来实现新语法的转换。比如转换class语法,就需要使用到classCallCheck()函数,如果多个文件中都使用到了Class语法,那么每个文件都会被注入classCallCheck()辅助函数,代码就会变得非常冗余,通过引入@babel/babel-plugin-transform-runtime插件就可以在输出文件中通过require的方式引入一个公共的classCallCheck()辅助函数,然后所有文件一起使用即可减少代码的冗余。@babel/babel-plugin-transform-runtime插件需要配合@babel/runtime一起使用,因为babel-plugin-transform-runtime插件引入的帮助函数,都存放在@babel/runtime中

module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                use: [
                    {
                        loader: "babel-loader", // 使用babel-loader进行处理js文件
                        options: {
                            presets: ["@babel/preset-env"] // 用了最新的ES6语法预设
                            plugins: ["@babel/plugin-transform-runtime"] // 减少代码冗余插件
                        }
                    }
                ]
            }
        ]
    }
}

③ 转换新的ES6API
babel默认只能转换ES6的新语法,如果想要转换一下ES6的新功能(Promise)、新接口(数组的includes方法)等,那么需要使用到@babel/polyfill, 其工作原理是在全局对象或者内置对象中添加一下属性或者方法,其使用方式为: 一种是直接在js文件中require, 如: require("@babel/polyfill"); 另一种是将"@babel/polyfill"作为入口文件一起打包成一个chunk,如:

module.exports = {
    entry: ['@babel/polyfill', "./src/index.js"] 
}
比如,使用了Promise,那么打包输出的文件中,可以看到全局对象global被添加了一个Promise属性

④ 压缩JS
webpack已经内置了压缩js功能,直接将mode设置为production,即可实现js的压缩。如果想在development模式下也能够压缩js,那么我们可以通过设置optimization.minimize为true实现,但是css不会被压缩

module.exports = {
    mode: "development", // 在开发模式下
    optimization: {
        minimize: true // 默认为false,压缩js
    }
}

我们也可以通过插件实现,如uglifyjs-webpack-plugin其压缩效果同optimization.minimize,可以实现多进程并行压缩,用法如下:

const UglifyjsWebpackPlugin = require("uglifyjs-webpack-plugin");
module.exports = {
    plugins: [
        new UglifyjsWebpackPlugin({ // 可以不配置参数,只给一个空对象
            cache: true,
            parallel: true,
            sourceMap: true
        })
    ]
}

从最终输出的代码上看,通过optimization.minimize和uglifyjs-webpack-plugin插件压缩的js,并不是很彻底,如果想压缩的更彻底些,可以使用terser-webpack-plugin,可实现多进程并行压缩,如:

const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
    plugins: [
        new TerserPlugin({ // 可以不配置参数,只给一个空对象
            cache: true,
            parallel: true,
            sourceMap: true, // Must be set to true if using source-maps in production
        })
    ]
}

同CSS压缩一样,我们也可以将JS压缩插件配置到optimization.minimizer数组中,如:

const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
    optimization: {
        minimize: true, // 开启minimizer数组中配置的插件
        minimizer: [
            new TerserPlugin() // 压缩js
        ]
    }
}

十、webpack 第三方模块的处理

① 如果我们的项目中使用到了第三方模块,比如jquery,我们直接在我们的js模块中引入jquery,那么这个jquery只能在当前模块中使用,而无法暴露到全局变量window中,如果想要实现,自动将jquery暴露给全局变量,那么需要引入expose-loader,其可以作为内联loader直接在js中使用,也可以作为普通loader在配置文件中使用。
// index.js

import $ from "expose-loader?$!jquery";
console.log($);
console.log(window.$); // 可以获取到jquery

或者在配置文件中引入

module.exports = {
    module: {
        rules: [
            {
                test: require.resolve("jquery"), // 如果require的时候引入了jquery
                use: {
                    loader: 'expose-loader',
                    options: {
                        exposes: ['$', 'jQuery'] // 将$和jQuery变量暴露到全局,即在window或者global上加上一个全局变量$和jQuery
                    }
                }
            }
        ]
    }
}
expose-loader是将一个模块的输出暴露给全局变量(不限于第三方模块,任何模块都可以),但是具体是暴露给window和global取决于配置文件中target的配置,如果是node则暴露给global,如果是web则暴露给window

打包后输出结果如下:

"./node_modules/jquery/dist/jquery-exposed.js":
(function(module, exports, __webpack_require__) {
// 引入jquery模块
var ___EXPOSE_LOADER_IMPORT___ = __webpack_require__("./node_modules/jquery/dist/jquery.js")
// 引入全局对象window或global
var ___EXPOSE_LOADER_GET_GLOBAL_THIS___ = __webpack_require__("./node_modules/expose-loader/dist/runtime/getGlobalThis.js");
var ___EXPOSE_LOADER_GLOBAL_THIS___ = ___EXPOSE_LOADER_GET_GLOBAL_THIS___;
// 在全局变量上添加一个$,并赋值为jquery
if (typeof ___EXPOSE_LOADER_GLOBAL_THIS___["$"] === 'undefined') ___EXPOSE_LOADER_GLOBAL_THIS___["$"] = ___EXPOSE_LOADER_IMPORT___;
module.exports = ___EXPOSE_LOADER_IMPORT___;
})

使用expose-loader后,会生成一个新的模块(带exposed),这个模块内部会引入需要暴露的模块,同时引入当前环境下的全局对象,然后将要暴露在全局的变量名放到全局对象上,并赋值为模块输出。

② 我们也可以不直接在我们的js文件中引入第三方模块,就可以使用他们,可以通过webpack提供的providePlugin内置插件来实现,将第三方模块注入到每个模块中,当然也可以是其他任何一个模块,如:

new webpack.ProvidePlugin({
            $: "jquery", // 等价于自动在每个模块中帮你加入import $ from "jquery"
            foo: path.resolve(__dirname,"./src/foo.js") // 自定义模块必须是模块的绝对路径否则无法解析
        })
我们就可以在任何一个模块中使用&dollar;和foo变量了,因为他们被注入到了所有模块中,但是全局变量中是无法访问到的。虽然providePlugin可以让我们不用直接在源文件中引入指定的模块,但是该模块还是会被打包进最终的打包输出文件中

打包后输出结果如下:

/* WEBPACK VAR INJECTION */(function($) {
    console.log($);
}.call(this, __webpack_require__("./node_modules/jquery/dist/jquery.js"))

webpack会自动帮你引入jquery模块,拿到结果后以参数的形式传入模块内部,从而实现注入功能。

以上两种方式第三方模块都会被打包输出到我们的最终输出文件中,我们通常是不希望将第三方模块打包到我们的输出文件中的,因为会我们可以通过cdn的方式直接引入第三方模块,这个时候我们就需要告诉webpack哪些模块不需要打包输出,并且用运行环境中的全局变量进行替换,需要用到webpack提供的externals配置,如:

module.exports = {
    externals: {
        "jquery": "$" // 排除jquery模块打包,并用浏览器中的全局的$替换掉jquery,即引入jquery模块的时候从全局的$变量中取值
    }
}
此时虽然js模块中引入了jquery,但是也不会被打包到输出文件中,并且html模板文件中直接通过cdn引入了jQuery,所以全局变量中也可以直接访问到$变量。
// webpack打包后的输出结果
(function (modules) {

})({
    "jquery": (function(module, exports) {
        module.exports = $;
    })
})

当我们的模块中引入jquery模块的时候,将会全局变量&dollar;中取值,从而避免将整个jquery模块打包进来。此时如果全局中没有window.&dollar;,那么import &dollar; from "jquery" 拿到的将会是undefined。

综上,ProvidePlugin的作用是自动注入模块引用语句,可以让我们不用直接引入指定的模块;而expose-loader的作用是将引入的模块输出暴露到全局的某个变量上,即将引入的模块输出挂载到全局的某个变量上;而externals是避免将某个模块打包到最终的输出文件中,而是通过cdn的方式引入该模块

十一、webpack 图片的处理

webpack是将所有文件都当做模块进行处理的,所以图片也被认为是一个模块,需要通过require或者import进行引入加载才能被正确打包输出。
如果想要加载并解析图片模块,那么必须使用合适的loader,解析图片可以使用file-loaer。如:

module.exports = {
    module: {
        rules: [
            {
                test: /\.(png|jpg|gif)$/, 
                use: [
                    {
                        loader: "file-loader", // 用file-loader解析图片模块
                        options: {
                            outputPath: "static/images//", // 将打包后的图片放到images目录下
                            name: "[hash:32].[ext]", // 打包后图片默认名称为32位hash值.图片扩展名
                            publicPath: "../images/",
                            // name: "[name]-[hash:32].[ext]", // 可以通过[name]拿到图片打包前的名称
                            // name: "static/images/[name].[hash:3].[ext]", // 也可以在name中指定图片目录
                            // publicPath: "../../" // name中是否指定图片路径,会导致publicPath的配置不同
                            
                        }
                    }
                ]
            }
        ]
    }
}

file-loader并不仅仅用于处理图片,可以是任何类型的文件,其作用本质就是将指定类型的文件拷贝输出到打包输出目录下

图片的使用方式:

  • 在css文件中作为背景图片使用
// index.css
body {
    background: url(./test.png);
}
图片会被单独打包出来放在输出目录中,但是输出的图片名称会发生变化,以hash值作为图片文件名,如果在css文件中使用图片,不需要通过require的方式,可以直接通过url("图片路径")的方式,因为css-loader对url()进行了转换,会转换为require的方式

如果我们在处理css的时候使用了mini-css-extract-plugin插件进行分离,那么打包后的css文件中引入的图片路径有可能是错误的,比如:

module.exports = {
    module: {
        rules: [
            {
                loader: MiniCssExtractPlugin.loader
            }
        ]
    }
    ,
    plugins: [
        new MiniCssExtractPlugin({
            filename: "css/index.css"
        })
    ]
}

此时引入的css文件会被打包到输出目录的css目录下,图片会被打包到输出目录的images目录下,其中index.css内容如下:

body {
    background: url(images/41c1d3e0a34d2cfed4a2133be356854a.png);
}

可以看到css/index.css中引入的图片是相对于当前目录,即css目录下的,而css目录下并没有images目录,所以无法引入图片。
这个时候,我们就需要用到MiniCssExtractPlugin.loader的配置参数了,其中有一个publicPath,当分离的css中引入其他资源的时候会与publicPath中配置的路径进行连接,如:

module.exports = {
    module: {
        rules: [
            {
                loader: MiniCssExtractPlugin.loader,
                options: {
                    publicPath: "../" // css中引入其他资源的时候会加上 ../
                }
            }
        ]
    }
}

此时打包后的css变为了

body {
    background: url(../images/41c1d3e0a34d2cfed4a2133be356854a.png);
}
  • 在js中使用
import bodyBg from "./test.png"; // 拿到打包后的图片url地址
const img = new Image(); // 相当于document.createElement("img")
img.src = bodyBg; // 不能直接赋值一个图片地址,必须引入进来再赋值
// img.src = "./test.png"; // 这是一个打包前的图片地址,因为图片打包后路径和文件名都会发生变化,所以必须通过import和require才能正确找到打包后图片的位置 
document.body.appendChild(img);

这里需要注意的是,引入图片的时候,可以使用import也可以使用require,但是默认情况下,二者拿到的返回值有些不同

import bodyBg from "./test.png";
console.log(bodyBg); // 输出结果为images/41c1d3e0a34d2cfed4a2133be356854a.png
const bodyBg = require("./test.png");
console.log(bodyBg); // 输出结果为Module {default: "images/41c1d3e0a34d2cfed4a2133be356854a.png", __esModule: true, Symbol(Symbol.toStringTag): "Module"}
console.log(bodyBg.default); // 输出结果为images/41c1d3e0a34d2cfed4a2133be356854a.png

由于file-loader默认情况下使用的是esModule
所以使用require的时候必须调用default属性才能拿到正确的url地址
我们可以通过给file-loader设置配置参数,将esModule设置为false即可,如:

module.exports = {
    module: {
        rules: [
            {
                loader: MiniCssExtractPlugin.loader,
                options: {
                    publicPath: "../", // css中引入其他资源的时候会加上 ../
                    esModule: false // 保证import和require返回的都是图片的url地址
                }
            }
        ]
    }
}
  • 在html文件中使用

如果想在html文件中直接使用<img/> 标签引入图片,那么需要使用到html-withimg-loader来处理我们的html文件,图片同样会被打包单独输出到输出目录中。

<body>
    <img src="./test.png"/>
</body>
module.exports = {
    module: {
        rules: [
            {
                test: /\.html$/, // 处理html文件
                use: {
                    loader: "html-withimg-loader",
                    options: {
                        min: false // 不去除html中的换行符
                    }
                }
            }
        ]
    }
}

如果想将图片打包成base64编码格式,那么需要使用到url-loader来处理我们的图片url-loader其实包含了file-loader的功能,因为其可以设置一个limit,限制图片的大小,只有图片的大小在limit范围内才会被打包为base64编码格式,超过limit则还是单独打包图片到输出目录中

module.exports = {
    module: {
        rules: [
            {
                test: /\.(png|jpg|gif)$/, 
                use: {
                    loader: "url-loader", // 用url-loader解析图片模块
                    options: {
                        limit: 200 * 1024, // 限制图片大小为200kb内才打包为base64编码格式
                        outputPath: "/img/", // 将图片打包输出到输出目录的img目录下
                        publicPath: "http://www.lihb.com/", // 仅仅给输出的图片资源添加资源服务器存放地址
                        esModule: false // 保证import和require返回的都是图片的url地址
                    }
                }
            }
        ]
    }
}
url-loader可以配置一个outputPath将图片输出到指定目录下面,实现资源的分类输出。还可以配置一个publicPath,在引入图片资源的时候添加上图片资源所在的服务器地址。

十二、.vue文件的处理

在vue项目中,我们需要对.vue文件进行打包,打包前需要通过对.vue文件进行转换,即需要对.vue文件进行预编译,这就需要使用到vue-loader了,由于.vue文件中包含了三部分,其中<template></template>部分,需要进行编译,所以需要使用到vue-template-compiler,而<style></style>部分是css部分,所以需要配置对.css文件的处理,可以使用vue-style-loader也可以使用style-loader。需要注意的时候,除了配置loader外,还需要配置一个插件,即VueLoaderPlugin。
// ① 安装处理.vue文件的loader

npm install vue-loader vue-template-compiler --save-dev
// ② 修改webpack配置文件
module.exports = {
    module: {
        rules: [
            {
                test: /\.vue$/,
                use: [
                    {
                        loader: "vue-loader"
                    }
                ]
            },
            {
                test: /\.css$/,
                use: [
                    'style-loader',
                    'css-loader'
                ]
            }
        ]
    }
}
// ③ 添加一个vue-loader中的插件

const VueLoaderPlugin = require('vue-loader/lib/plugin');

module.exports = {
    plugins: [
        new VueLoaderPlugin()
    ]
}

十三、optimization优化选项

optimization 主要用于配置一些优化项。webpack4中会根据mode的不同进行一些默认的优化。
① minimize: 配置是否压缩打包后的js代码,开发模式下为false,生产模式下为true。

module.exports = {
    mode: "development",
    optimization: {
        minimize: true, // 开启js代码压缩
    }
}

② splitChunks: 主要用于分割代码。其默认配置如下:

module.exports = {
    optimization: {
        splitChunks: {
          chunks: 'async', // 默认按需加载的时候才会优化
          minSize: 30000,
          minRemainingSize: 0,
          maxSize: 0,
          minChunks: 1,
          maxAsyncRequests: 6,
          maxInitialRequests: 4,
          automaticNameDelimiter: '~',
          cacheGroups: {
            defaultVendors: {
              test: /[\\/]node_modules[\\/]/,
              priority: -10
            },
            default: {
              minChunks: 2,
              priority: -20,
              reuseExistingChunk: true
            }
          }
        }
    }
}

chunks有三个可选值: initialasyncall
默认为async,initial表示同步加载即使用 import ... from ... 或者 require("...")的方式加载模块;async表示的是异步加载,即使用import("...")的方式加载;all表示的是同步和异步加载都进行优化
minSize表示的是模块的大小,单位为字节数,因为默认需要30KB,所以测试的时候使用的模块需要在30KB以上
minChunks表示的是模块被引用的最小次数
cacheGroups表示的是缓存组,这是splitChunks的核心配置,分割代码的时候会按照cacheGroups的配置去分割。cacheGroups会继承splitChunks下的配置选项,同时可以进行覆盖,此外还可以配置testpriority等进行更详细的配置。test可以使用正则表达式进行精确匹配,priority可以设置优先级

module.exports = {
    optimization: {
        splitChunks: {
            cacheGroups: {
                "vendors": {
                    test: /node_modules/,
                    minChunks: 2,
                    chunks: "initial", // 覆盖默认值async
                    priority: -10,
                    minSize: 0 // 覆盖默认值30KB,防止库文件太小而无法抽离
                },
                "common": {
                    minChunks: 2,
                    chunks: "initial", // 覆盖默认值async
                    priority: -20,
                    minSize: 0 // 覆盖默认值30KB,防止库文件太小而无法抽离
                }
            }
        }
    }
}

③ runtimeChunk 用于抽离出webpack的运行文件,因为每次打包webpack的运行文件都会发生变化,为了防止缓存失效,所以需要将webpack的运行文件抽离出来,如:

module.exports = {
    optimization: {
        runtimeChunk: {
            name: "manifest" // 生成文件名为manifest的webpack运行文件
        }
    }
}

十四、让webpack支持智能提示

目前我们的webpack配置文件都是以js的形式进行配置的,而js无法像ts那样可以通过类型声明进行相应的提示,但是我们仍然可以实现智能提示,其方法为:

  • 通过import的方式引入webpack中的Configuration
  • 以多行注释的形式添加@type {Configuration}
// 由于webpack是node环境执行,不支持ES模块规范,所以配置文件编写完成后需要注释掉该语句
import {Configuration} from "webpack";

/**
 * @type {Configuration}
 */
const config = {
    // 此时书写配置文件就会有提示了
}
module.exports = config;

当然也可以将这两步合在一起,这样就可以省去需要注释的麻烦,如:

/**
 * 一行搞定智能提示
 * @type {import('webpack').Configuration }
 */
const config = {
    // 此时书写配置文件就会有提示了
}
module.exports = config;

需要注意的就是,@type类型声明必须与config配置对象紧紧连接在一起,中间不能有其他语句,否则智能提示会失效。


JS_Even_JS
2.6k 声望3.7k 粉丝

前端工程师