第一章 Webpack核心功能

511 阅读34分钟

Webpack的安装和使用

webpack官网:www.webpackjs.com/

介绍

webpack是用来构建工程的

具体来说,webpack会以某个js模块文件作为入口,然后根据该入口分析出模块的依赖关系,并对各种模块进行合并、压缩,形成最终的打包结果

分析模块的依赖关系需要读取文件内容,因此webpack需要运行在nodejs环境中

webpack会将一切资源都视为模块,包括图片、视频、css样式文件等

image.png

使用webpack工具构建工程时,开发者编写的代码并不是最终被执行的代码,真正执行的代码是webpack对开发者编写的代码进行编译后的结果

开发者在编写代码时,可以不用考虑浏览器对代码的兼容性,而是可以安心书写任何版本的JS代码,因为兼容性问题webpack会帮你解决(准确地讲是webpack的插件会帮助处理)

在webpack中,还可以实现模块化标准混用,例如使用CommonJS导入其他模块中使用ES Module导出的内容,因为在最终的打包结果中不会出现任何模块化语句

webpack不仅仅支持CommonJS和ES Module,AMD和CMD等也支持

使用webpack构建工程,并采用ES Module导入其他js模块时,可以省略.js后缀,并且允许路径不以./..//开头,若不以这些开头,则webpack会从当前目录的node_modules目录中寻找导入的模块内容,这和nodejs查找模块的特点是一样的

若路径的终点是目录,则webpack会读取目录中的index.js

由于webpack会将一切资源视为模块,因此在导入资源时,可以导入非JS文件,例如可以导入图片,视频,css文件等,若导入的不是一个JS模块,则导入的路径中必须加上文件的后缀名

例如:require("./style.css"),import "./style.css"

在开发阶段,开发者可以使用npm下载第三方包到工程中,只要开发者编写的代码中使用到了下载的第三方包,打包结果中就会出现第三方包中的代码,这解决了浏览器环境中无法使用npm下载的包的问题

浏览器环境下无法使用npm下载的包的原因如下:

  1. npm下载的包有许多是使用CommonJS的方式导出的,而浏览器不支持CommonJS规范
  2. 即使包是通过ES Module的方式导出的,但由于包与包之间存在依赖关系,导入的包往往会依赖非常多其他的包,浏览器在加载一个包时,就需要同步加载其他数量庞大的依赖包,这会导致页面加载的时间变长

安装

要使用webpack,就需要安装下面两个包:

  1. webpack

    核心包,其中包含了webpack构建过程中会使用到的所有API

  2. webpack-cli

    提供了一个简单的cli命令,通过运行这些命令就可以调用webpack核心包的API来完成构建

建议使用本地安装安装webpack,因为不同的项目可能会使用到不同版本的webpack

npm i -D webpack webpack-cli

使用

默认情况下,在工程根目录下运行下面的命令,webpack就会以"./src/index.js"作为入口进行依赖分析,最终形成一个打包结果,并将打包结果存放在"./dist/main.js"文件中

npx webpack [--mode=xxx]

--mode后面指定打包结果的运行环境,默认为production

如果mode为production,表明打包后的结果是要在生产环境中运行的,webpack就会在打包期间对代码进行压缩(减少打包结果的体积)、丑化(以防止他人阅读)

如果mode为development,表明打包后的结果是要在开发环境中运行的,webpack就不会在打包期间对代码进行压缩丑化,让开发者能够更方便地对打包结果中的代码进行调试

可以在该命令中加入--watch,这样只要开发者编写的模块发生改变,webpack就会自动重新生成打包结果,而无需开发者再次输入打包命令进行打包

npx webpack --mode=xxx --watch

模块化兼容性

在webpack中,可以使用ES Module的语法来导入CommonJS导出的内容,也可以使用require导入ES Module导出的内容

模块化兼容性就是要探讨在webpack中这两者是如何相互配合的

同模块化标准

如果导出和导入使用的是同一种模块化标准,则这部分所打包后的结果和正常情况下没有区别

image.png

image.png

不同的模块化标准

对于导入和导出使用的是不同的模块化,webpack的处理如下图所示

image.png

image.png

在webpack中,使用ES Module中的导入所有内容和默认导入都是导入CommonJS所导出的所有内容

也可以使用ES Module的具名导入语法导入CommonJS导出的内容

// a.js
module.exports = {
    a: "aaa",
       b: "bbb",
       c: "ccc"
   }
// index.js
import { a, b } from "./a"

// a = "aaa", b = "bbb"

webpack打包结果分析

假设工程中有如下两个模块:

入口模块./src/index.js的内容如下:

console.log("module index");
var a = require("./a");
console.log(a);

同目录下的a模块的内容如下:

console.log("module a");
module.exports = "a";

则使用webpack --mode=development进行打包后,会形成下面的结果:

(function(modules){
    // 用于缓存模块
    var moduleCache = {};
    
    function webpack_require(moduleId){
        // 如果模块已缓存,则直接返回缓存结果
        if(moduleCache[moduleId]){
			return moduleCache[moduleId];
        }
        
        var module = {
            exports: {}
        };
        
        var fn = modules[moduleId];
        fn(module, module.exports, webpack_require);
        
        // 将模块的导出内容保存到缓存中
        moduleCache[moduleId] = module.exports;
        
        return module.exports;
    }
    
    return webpack_require("./src/index.js");
})({
    "./src/index.js": function(module, exports, webpack_require){
        // 函数中的代码就是index模块中的代码
		console.log("module index");
        // webpack会将require编译为webpack_require,为了防止与node中的require重名
        // webpack会对require函数中传入的模块路径进行处理,处理成一个相对于工程根目录的完整路径
        var a = webpack_require("./src/a.js");
        console.log(a);
    },
    "./src/a.js": function(module, exports, webpack_require){
        // 函数中的代码就是a模块中的代码
        console.log("module a");
        module.exports = "a";
	}
});

上面的代码就是webpack对入口模块进行分析,将入口模块和其所依赖其他模块进行编译,合并后形成的结果

使用ES Module进行导入导出得到的打包结果也类似,这里为了简单和描述方便统一使用了CommonJS规范

在默认情况下,开发环境下的打包结果中的模块代码其实是会被作为eval的参数字符串存在的

{
 "./src/index.js": function(module, exports, webpack_require){
     eval('console.log("module index");\nvar a = webpack_require("./src/a.js");\n console.log(a);//# sourceURL=./src/index.js');
 },
 "./src/a.js": function(module, exports, webpack_require){
     eval('console.log("module a");\nmodule.exports = "a";//# sourceURL=./src/a.js');
	}
}

因为webpack考虑到将模块代码直接作为打包结果中的js代码时,一旦代码抛出错误,之后通过调试控制台定位错误位置时定位到的就会是打包结果文件,而开发者很难知道打包结果中的代码在原始工程的什么位置

而eval环境是一个特殊的运行环境,eval的字符串代码在执行出错时,在浏览器调试控制台中定位到错误位置就只会显示eval中的字符串代码

并且通过在代码字符串的最后加入一个资源的虚拟位置信息(//# sourceURL=xxx),浏览器能识别出该信息,并在代码抛出错误时显示该位置信息

例如:

// eval之前的代码
const a = 10;
eval("const b = 20;\nb++;\n//# sourceURL=eval.js");		// eval中的代码存在问题
// eval之后的代码
const c = 30;

执行后:

image.png

定位错误:

image.png

如果是在生产环境下进行的打包,则打包结果会与上面有些许区别,这些区别主要是webpack为了减少打包结果的体积所导致的

配置文件

webpack提供的cli支持很多的参数,例如--watch、--mode,但每次打包都需要输入这些参数,不是很方便,因此webpack允许我们使用配置文件来控制webpack的打包过程

默认情况下,webpack会读取工程的根目录下的webpack.config.js配置文件,在该配置文件中就可以对webpack的打包过程进行配置

也可以通过cli参数--config来指定某个js文件为配置文件

例如:

webpack --config ./src/webpack.deve.js

webpack的打包过程需要运行配置文件,因此配置文件中的代码必须是有效的node代码

配置文件需要使用CommonJS标准来导出一个对象,对象中的各属性就是一个个webpack配置

当命令行参数与配置文件中的配置发生冲突时,以命令行参数为准

基本配置

  • mode

    编译模式,字符串,取值为development或production

    指定编译的运行环境,会影响webpack对代码的打包过程

  • entry

    入口模块的模块路径,字符串或对象或数组

    指定入口文件,默认为"./src/index.js"

  • output

    出口,对象

    编译结果的配置信息

    编译结果就是打包结果,一个意思

  • watch

    监控,布尔值

    当源代码发生变化时,是否需要webpack对代码进行重新打包

// webpack.config.js
module.exports = {
    mode: "development",
    entry: "./src/main.js",
    output: {
        filename: "bundle.js"
    },
    watch: true
}

devtool配置

source map源码地图

运行的打包后的代码,不是开发者编写的源代码,而是webpack对源代码进行合并、压缩、转换后的代码

image.png

这就会给调试带来困难,因为当代码运行出错时,开发者希望能看到的是错误在源代码中的文件,而不是在打包结果中的位置

为了解决这一问题,chrome浏览器率先支持了source map,后来其他浏览器纷纷效仿,目前,几乎所有新版浏览器都支持了source map

source map实际上是一个文件,文件中不仅记录了所有源码内容,还记录了源码和转换后的代码的映射关系

当浏览器请求到了转换后的代码后,若发现代码中有标注附带source map的注释,就会再次请求该文件对应的source map文件

image.png

之后,当浏览器运行代码时出现了错误,就可以根据source map中记录的对应关系,将错误所对应的源代码的位置显示到页面中,方便开发者进行调试

image.png

source map一般使用在开发环境中,作为一种调试手段,而一般不会放在生产环境中,因为这会带来额外的传输量,并且source map文件一般都比较大

webpack中的source map

在配置文件中添加如下的devtool配置,即可在打包过程中生成source map

// webpack.config.js

module.exports = {
    devtool: "source-map"
}

更多可配置的内容参见文档:www.webpackjs.com/configurati…

之前所遇见的,在开发环境下进行打包时,模块源代码会作为eval函数的字符串参数存在,是因为webpack默认在开发环境下的devtool配置值为"eval"

编译过程

编译,也称打包或构建,即对开发者编写的代码进行压缩、合并、转换等步骤形成最终的代码

整个过程大致可以分为三个阶段:

  1. 初始化
  2. 编译
  3. 输出

初始化

在此阶段,webpack会将命令中的cli参数、配置文件、默认配置进行融合,形成一个最终的配置对象

对配置的处理过程是依赖个第三方库yargs来完成的

编译

编译阶段又可以再细分为几个阶段:

  1. 创建chunk
  2. 构建所有依赖模块
  3. 产生chunk assets
  4. 合并chunk assets
创建chunk

在这一步中,webpack会根据配置文件中的entry配置创建出若干个chunk

chunk是webpack在编译过程中引出的一个概念,一个chunk就是入口文件以及入口文件所直接或间接依赖的所有模块的相关信息的统称

一个chunk中可能包含多个入口模块

每个chunk都会包含以下内容:

  • name

    chunk的名称

    默认情况下,工程打包后只会有一个chunk,该chunk的name为main

  • id

    唯一编号,在开发环境中和name相同,在生产环境中是一个数字,从0开始

  • 模块记录

    模块记录其实就是一张表格,表格中记录着已经加载完成了的模块的模块id和模块转换后的代码

    模块id就是模块的完整相对路径,例如:"./src/index.js"

    对于刚初始化的chunk,其模块记录就是一张空表

  • chunk assets

    后面小节详解

  • chunk hash

    后面小节详解

image.png

构建所有依赖模块

在这一步中,webpack根据入口模块,将它与它所依赖的其他所有模块都记录下来,记录到模块记录表格之中

具体步骤如下:

  1. webpack根据每个chunk的入口模块的完整相对路径(如./src/index.js),查看该路径是否在模块记录中有对应

    如果有,就说明该模块之前已经被加载过了,于是直接结束本轮处理,来到下一轮处理

    如果没有,就去加载模块内容并进入下面的步骤

    由于是当前处理的入口模块,所以肯定是没有被加载过的

  2. 模块内容下载完成后,webpack会对内容(其实就是字符串)进行语法分析,并生成抽象语法树AST

    抽象语法树中的节点是对原始代码进行分析后的结果,其中也包含了对require和import语句的分析,之后webpack就能够通过遍历抽象语法树,就能够得知该模块依赖了哪些模块

    需要注意的是,这里仅仅只是分析,并不会去运行模块代码

  3. 得到完整的AST后,webpack会对AST进行遍历,以找到入口模块的所有直接依赖,并将这些依赖的路径记录到dependencies数组中

    在模块源代码中,通常不会在导入语句中使用模块的完整相对路径,而webpack在记录依赖模块的路径时,若发现路径不是完整相对路径,则会将路径转换为完整相对路径,然后记录到数组之中

    每一轮处理都会初始化一个空的dependencies数组,数组中会记录着当前处理的模块的所有直接依赖模块的完整相对路径

  4. webpack会将模块中的代码进行转换,例如:将"require"替换为"webpack_require"、将require函数中的非完整相对路径参数都替换为完整的相对路径等

    经过这一步后,就得到了转换后的代码

    模块的完整相对路径是模块相对于工程根目录的完整路径,路径中包含文件后缀名

    例如:require("./b")经过转换后将得到webpack_require("./src/b.js")

  5. 在得到转换后的代码后,webpack将模块的完整相对路径和转换后的代码作为一个表项加入到chunk的模块记录中

    而完整相对路径就会作为表项的模块id存在

  6. 遍历dependencies数组,将数组中记录的当前模块的所有直接依赖递归执行上面的操作,直至将属于该chunk的所有模块都记录到模块记录中

image.png

对于有多个入口模块的chunk,会对每一个入口模块都执行上面的操作

这一步完成后,chunk中就包含了它所需的所有模块的代码

产生chunk assets

当模块记录生成完成后,webpack就会根据配置和chunk的模块记录为chunk生成一个资源列表,即chunk assets

chunk assets也是一个表格,其中记录该chunk在最终打包结果中所对应的文件的“文件存放路径 + 文件名”,以及文件中的内容

一个chunk可能会在最终打包结果中对应有多种文件,所以需要使用一个表格进行记录

一个chunk,它的chunk assets中会包括.js文件,.js.map文件,

但默认情况下(即没有plugin或loader手动添加新文件到chunk的情况下),一个chunk的chunk assets有且仅有一个js文件,如果是在开发环境下进行的打包,则该js文件中的内容就是之前介绍的【webpack打包结果分析】的内容,该内容就是对模块记录中的所有表项,将它们合并生成出来的

在默认情况下,chunk assets中每一个文件的文件名,使用的是chunk的name属性值

资源列表生成完成后,还会为chunk生成一个chunk hash,chunk hash是根据chunk assets中所有表项的文件内容计算出来的

hash是一个固定长度的字符串,它是通过对原始内容进行计算后得来的,可以保证在原始内容不变的情况下,产生的hash字符串就不变;原始内容稍微变化一点,hash就会发生很大变化

因此hash能够很好地反映文件内容是否变化

image.png

合并chunk assets

chunk可以有多个,合并chunk assets就是将这些不同的chunk中的chunk assets合并在一起,并根据合并后的assets中的所有文件内容生成一个总的hash

image.png

输出

在此阶段,webpack根据总的assets,利用nodejs的fs内置模块,生成相应的文件

生成的文件也称为一个个的bundle

image.png

若开启了watch,则一旦文件内容发生变化,webpack就会重新从编译阶段开始(不是从初始化阶段开始)重新生成编译结果

总过程

image.png

相关术语

  1. module:模块,分割的代码单元,webpack中的模块可以是任何内容的文件,不仅限于JS
  2. chunk:webpack内部构建模块的块,一个chunk中包含多个模块,这些模块是从入口模块通过依赖分析得来的
  3. bundle:chunk构建好模块后会生成chunk的资源清单,清单中的每一项就是一个bundle,你也可以认为一个bundle就是最终生成出来的一个文件
  4. hash:最终的资源清单所有内容联合生成的hash值
  5. chunkhash:chunk生成的资源清单内容联合生成的hash值
  6. chunkname:chunk的名称,如果没有配置则使用main
  7. id:通常指chunk的唯一编号,如果在开发环境下构建,和chunkname相同;如果是生产环境下构建,则使用一个从0开始的数字进行编号

entry和output配置

nodejs相关知识

  • 在nodejs中,./可能并不表示当前的JS文件所在的目录

    在模块化代码中,./表示当前JS文件所在的目录,比如require("./")

    而在路径(path模块)或文件(fs模块)处理中,./表示执行node命令的目录

  • __dirname

    在任何情况下,该全局变量都表示当前js文件所在的目录,它是一个绝对路径

  • path模块

    该模块中提供了大量关于路径处理的函数,是node的内置模块,可以直接通过require("path")来导入该模块

  • resolve([path1, path2, ...])

    该函数用于拼接路径,即将多个路径或路径片段拼接成为一个完整的绝对路径

    该函数在不同的操作系统中运行时,所使用的路径拼接符号不同

    该函数来自于path模块

proj
    |—— node_modules
    |—— src
        |—— index.js
    |—— package.json
    |—— webpack.config.js
// index.js

var path = require("path");

console.log(path.resolve());

对于在不同目录下执行的node命令,path模块中的函数所返回的路径也会不同:

D:\proj> node ./src/index.js
D:\proj

D:\proj\src> node ./index.js
D:\proj\src

entry

该配置用于设置chunk,它是一个对象,其中的每一个属性的属性名就是chunk的name,属性值是该chunk的入口模块的相对于当前工作路径的相对路径

当前工作路径CWD,即运行webpack命令时命令行中的路径(不包含用户输入的命令中的路径)

如:

module.exports = {
    ...,
    entry: {
        main: "./src/index.js"
    }
}

上面也是entry的默认值

该配置是一个对象,说明chunk可以有多个

module.exports = {
    ...,
    entry: {
        a: "./src/a.js",
    	b: "./src/b.js",
    	...
    }
}

一个chunk中也可以有多个入口模块

当chunk中有多个入口模块时,entry中的属性的值就需要为一个数组,数组的元素就是每一个入口模块的相对路径

module.exports = {
    ...,
    entry: {
        main: ["./src/index.js", "./src/a.js"]
    }
}

虽然一个chunk中包含了多个入口模块,但该chunk中的chunk assets中仍然只会有一个js资源文件

只不过在该js文件中,会按照配置数组中的元素顺序,使用webpack_require函数来执行相应模块中的代码,并且最后一个模块对应的函数的执行结果会被返回

例如:

(function(modules){
    ...
	function webpack_require(moduleId){
        ...
    }
    ...
    webpack_require("./src/index.js");
    return webpack_require("./src/a.js");
})({
    "./src/index.js": function(){
    	...
	},
    "./src/a.js": function(){
    	...
	}
});

output

该配置用于设置资源列表的文件路径和文件名

打包结果的位置,以及里面的文件名称就是受该配置的影响

该配置是一个对象,可以包含以下属性:

  • path

    该属性的值是一个绝对路径形式的字符串,表示输出目录所在的位置

  • filename

    该属性的值是一个字符串,表示js资源文件的文件名称(注意:该配置只设置js文件)

    该字符串中也可以加入目录,这样打包时也会生成相应目录,并会将js文件放到目录中,例如:"a/b/xxx.js"

    当entry配置中只设置了一个chunk时,该属性就可以是一个静态的普通字符串,之后打包形成的文件的名称就会直接参考该字符串

    当entry配置中设置了多个chunk,则该属性就必须是一个动态的规则字符串,规则字符串中包含"[规则]",这部分的字符串会在最终形成打包结果时被替换为相应的内容

    "[]"中可以包含以下规则:

    ① name:最终会被替换为chunk的name

    ② id:最终会被替换为chunk的id

    ③ hash:最终会被替换为总的hash,总的hash较长,因此webpack也允许只取hash的前面几位,例如:"[hash:5]"

    ④ chunkhash:最终会被替换为对应的chunk的hash,用法和总的hash一样

    ⑤ contenthash:最终会被替换为content hash,用法和总的hash一样

    content hash是根据当前文件的内容所生成的hash

    hash、chunk hash和content hash一般会在处理浏览器缓存的场景中出现,用于阻止浏览器使用缓存的js文件

  • publicPath

    对于资源模块,希望导入这些资源时得到的是模块在输出目录中的路径,而这些路径是由loader或plugin生成出来的,而在生成路径时,loader或plugin并不知道输出目录中有哪些资源会使用到该路径,更不知道这些使用路径的资源在输出目录中的什么位置,因此也就无法确定正确的最终路径

    一般情况下,输出目录中的资源都是以输出目录为基准来放置的,对于这种情况,可以设置publicPath"/",之后loader或plugin在生成路径时就会在路径最前面加上publicaPath的内容,这样生成出来的路径就是相对于输出目录的,这可以解决绝大多数情况下的路径问题

    大部分的loader和plugin也提供了仅自身使用的publicPath,这就需要将publicPath设置在具体的loader或plugin配置对象中

// webpack.config.js

var path = require("path");

module.exports = {
    ...,
    output: {
        path: path.resolve(__dirname, "dist"),
		filename: "[name].js",
		publicPath: ""
    }
}

上面也是output的默认值

loader

webpack能做的事情,仅仅只是分析出各个模块之间的依赖关系,然后形成资源列表,并生成指定文件

webpack的更多功能需要使用loader和plugin来完成

一个loader就是一个js文件,该文件需要使用CommonJS来导出一个函数,该函数可以将某个文件的原始内容转换成另一个新的内容并返回

文件的原始内容在默认情况下会被视为字符串传递给loader,而在任何情况下,loader也需要返回一个转换后的字符串

// loader.js
module.exports = function (originContentStr){
    ...
    return newContentStr;
}

之所以webpack能将一切资源视为模块,都是loader的功劳

详细流程

loader函数是在编译阶段执行的,文件中的原始内容经过loader的处理后,将会返回的新的内容,之后webpack会将经过处理的新内容视为文件内容进行抽象语法解析

由于抽象语法分析只能分析js代码,因此无论什么文件,经过若干个loader处理后都必须得到的是一个字符串形式的js代码

image.png

并不是所有的文件都会经过loader处理,只有满足匹配规则的文件才会被loader进行处理

一条匹配规则中可以同时设置多个loader,因此一个文件可以同时被多个规则所匹配

当webpack读取完文件内容后,若发现该文件满足某些匹配规则,webpack就会将这些匹配成功的规则中的loader函数按顺序加入到loaders数组中,之后webpack就会从后往前遍历loaders数组并执行相应操作:将文件的原始内容作为最后一个loader函数的参数传入,经过处理后会返回一个新的内容,之后该内容又会作为倒数第二个loader函数的参数传入...

当loaders中的所有loader函数运行完成时,loaders中的第一个loader函数的返回结果就会被webpack进行语法分析,从而形成AST

image.png

module配置

module配置是一个对象,其内部的rules属性就可以设置所匹配模块会使用到的loader

该对象可以有两个属性:

  1. rules

    模块的匹配规则以及匹配成功后需要使用到的loader

    每一个匹配规则就是一个对象,对象中包含test属性和use属性

    test属性是一个正则表达式,完整相对路径能够与正则表达式匹配的模块就会被use属性中的loader处理

    use属性是一个数组,数组中的元素可以为以下两种:

    ① 可以直接传入会使用到的loader模块的相对于当前工作路径的的路径,若loader在node_modules文件夹中,则直接书写loader文件的名称即可

    ​ 路径中也可以使用query的形式来为loader函数提供一些配置信息

    module.exports = {
        module: {
            rules: [
                {
                    test: /\.js$/,
                    use: ["./src/loader1.js", "./src/loader2.js?a=1&b=2"]
                }
            ]
        }
    }
    

    ② 可以传入一个对象,该对象可以有两个属性,分别是loader和options

    ​ loader属性为loader模块的相对于当前工作路径CWD的路径

    ​ options属性是一个对象,用于给loader函数传递一些配置信息,在loader函数内部可以通过this来获取到该options对象

    const loaderUtils = require("loader-utils");
    
    module.exports = function(){
        const options = loaderUtils.getOptions(this);		// 传递给loader的options
    }
    

    ​ options配置可以以query的形式存在于loader属性的路径字符串中

    module.exports = {
        module: {
            rules: [
                {
                    test: /\.js$/,
                    use: [
                        {
                            loader: "./src/loader1.js",
                            options: {
                                a:1,
                                b:2
                            }
                        },
                        "./src/loader2.js?a=1&b=2"
                    ]
                },
            ]
        }
    }
    

    当一个模块满足了多个rules中的匹配规则时,这些匹配规则中的loader会按顺序加入到loaders数组中

    module.exports = {
        module: {
            rules: [
                {
                    test: /index\.js$/,
                    use: ["./src/loader1.js", "./src/loader2.js"]
                },
                {
                    test: /\.js$/,
                    use: ["./src/loader3.js", "./src/loader4.js"]
                }
            ]
        }
    }
    

    以./src/index.js为例,最终该模块的loaders数组中的内容为["./src/loader1.js", "./src/loader2.js", "./src/loader3.js", "./src/loader4.js"]

    需要注意时,使用loader时,是从后往前遍历loaders数组的

  2. noParse

    值为一个正则表达式

    若模块的模块ID(即完整相对路径)能够匹配该正则表达式,则在编译过程中webpack会直接将该模块中的原始内容或经过loader转换后的内容保存到模块记录中,然后直接结束本轮编译,期间不会对该模块进行语法分析、生成AST、分析依赖关系、转换模块代码等工作

    通常用该配置来忽略那些大型的单模块库(即一个库中只有一个模块,且该模块中不会导入其他模块),以提高打包的速度

匹配内容为二进制的模块

loader能够处理图片文件,图片文件中保存的是二进制数据

默认情况下,loader会将匹配的模块的原始内容转换为字符串,再将字符串作为参数传入到函数中

如果不希望loader将模块内容视为字符串,而希望获得将模块内容的原始格式,则需要设置loader函数的raw属性为true

// image-loader.js

function loader(buffer){	// 保留内容原始形式后,传入的参数就变为了ArrayBuffer
    ...;
    return ...;
}

loader.raw = true;			// 阻止loader函数将模块内容转换为字符串,而是保留内容的原始二进制形式

module.exports = loader;

案例1:样式处理

// loader/style-loader.js
module.exports = function (sourceCode){
    return `
    	const style = document.createElement("style");
    	style.innerHTML = \`${sourceCode}\`;
    	document.head.appendChild(style);
    	module.exports = \`${sourceCode}\`;
    `;
    // 将sourceCode导出是为了方便后续loader处理
}
// webpack.config.js
module.exports = {
    module: {
        rules: [{
            test: /\.css$/,
            use: ["./loader/style-loader.js"]
        }]
    }
}

案例2:图片处理

// loader/img-loader.js
const loaderUtils = require("loader-utils");

function loader(buffer){
    // 获取传递给loader的options配置
    const { limit = 1000, filename = "[contenthash:5].[ext]" } = loaderUtils.getOptions(this);
    if(buffer.byteLength < limit){
        return `
        	module.exports = \`${toBase64(buffer)}\`;
        `;
    }else {
        const filePath = getFilePath.call(this, buffer, filename);
        return `
            module.exports = \`${filePath}\`;
        `;
    }
}

function toBase64(buffer){
    return "data:image/png;base64," + buffer.toString("base64");
}

function getFilePath(buffer, name){
    // 根据内容生成文件名
    const filename = loaderUtils.interpolateName(this, name, {
		content: buffer
    });
    // 往总的assets中添加一个项目
    this.emitFile(filename, buffer);
    return filename;
}

loader.raw = true;

module.exports = loader;
// webpack.config.js
module.exports = {
    module: {
        rules: [{
            test: /\.png$/,
            use: [{
                loader: "./loader/img-loader.js",
                options: {
                    limit: 3000,			// 图片尺寸在3000字节以上的,单独形成一个文件
                    filename: "img-[contenthash:5].[ext]"
                }
            }]
        }]
    }
}

plugin

loader只负责转换代码,不具备其他功能,而loader无法完成的功能,就需要由plugin来完成

plugin的本质是一个带有apply方法的对象

习惯上会在plugin对应的js文件中导出一个类,通过该类就可以构造出一个plugin

// MyPlugin.js
module.exports = class MyPlugin{
    apply(compiler){ }
}

要将plugin应用到webpack,需要把plugin对象配置到webpack的plugins数组中

// webpack.config.js
var MyPlugin = require("./MyPlugin.js");

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

plugin的apply方法可以接收一个compiler参数,compiler对象是在初始化阶段被创建的,整个webpack打包期间只会产生一个compiler对象

当compiler对象创建完成后,complier对象就会立即创建一个compilation对象,而后续的打包工作就是由该compilation对象来完成的,当打包工作完成时,compilation就会被销毁

若为webpack设置了watch,则一旦源代码发生变化,webpack就会重新从编译阶段开始,重新生成打包结果,此时compiler对象会重新生成一个compilation对象,让该compilation对象完成新一轮的打包

image.png

plugin的apply方法就是在compiler对象被创建后立即调用的,调用时compiler对象会作为apply函数的参数传入

可以为compiler或compilation对象注册一些钩子(hooks),这些钩子的处理函数会在webpack打包期间的某个时间点被执行

在这些钩子函数中,就可以进行一些对webpack打包过程进行干预的操作,以实现某种功能

image.png

为compiler对象注册钩子的方式如下:

module.exports = class MyPlugin{
    apply(compiler){
		compiler.hooks.钩子名称.处理函数类型(name, function(compilation){
            // 钩子的处理函数
        });
    }
}

为compilation对象注册钩子的方式如下:

module.exports = class MyPlugin{
    apply(compiler){
		compiler.hooks.钩子名称1.处理函数类型1(name, function(compilation){
            // compiler的钩子的处理函数
			compilation.hooks.钩子名称2.处理函数类型2(name, function(){
                // compilation的钩子的处理函数
            });
        });
    }
}

具体的钩子名称参见文档:www.webpackjs.com/api/compile…

name部分可以随便填,它是方便调试plugin而存在的

webpack需要知道这些钩子的处理函数在什么时候处理完成,因为这会对后续的构建过程造成影响

处理函数的处理完成并不是指函数运行结束,具体何时处理完成需要根据处理函数的类型来判断

处理函数的类型有以下三种:

  1. tap

    处理函数是一个同步的函数,处理函数运行完毕就表示处理完成

    module.exports = class MyPlugin{
        apply(compiler){
    		compiler.hooks.钩子名称.tap(name, function(compilation){
    			// 处理函数运行结束就表示处理完成
            });
        }
    }
    
  2. tapAsync

    处理函数是一个基于回调的异步的钩子函数,处理函数通过调用一个回调来表示处理完成

    module.exports = class MyPlugin{
        apply(compiler){
    		compiler.hooks.钩子名称.tapAsync(name, function(compilation, callback){
    			setTimeout(()=>{
                    callback();		// callback被调用就表示处理函数已经处理完成
                }, 1000);
            });
        }
    }
    
  3. tapPromise

    处理函数是一个基于Promise的异步函数,处理函数通过让返回的Promise进入完成状态来表示处理完成

    module.exports = class MyPlugin{
        apply(compiler){
    		compiler.hooks.钩子名称.tapPromise(name, async function(compilation){
                // 该处理函数返回的Promise进入完成状态时表示处理完成
            });
        }
    }
    

案例:添加文件列表

在output目录中加入fileList.txt,文件中记录着目录中的其它文件的文件名称以及文件的尺寸

【main.js】:30KB
【main.js.map】:50KB

实现:

// FileListPlugin.js
module.exports = class {
    constuctor(txtName = "fileList.txt"){
        this.txtName = txtName;
    }
    
    apply(compiler){
        // emit钩子: 在生成资源到output目录之前触发
        compiler.hooks.emit.tap("FileListPlugin", (complation)=>{
            let content = "";
            // complation.assets中记录着即将输出到output目录的文件的相关信息(就是总的assets)
            for(const key in complation.assets){
                const fileName = key;
                const fileSize = complation.assets[key].size();
                content += `【$key】:${fileSize/1024}KB\n`;
            }
            complation.assets[this.txtName] = {
                source(){		// 返回值就是文件的内容(字符串)
                    return content;
                },
                size(){			// 返回值就是文件的尺寸(字节)
                    return content.length;
                }
            }
        });
    }
}
// webpack.config.js
const FileListPlugin = require("./FileListPlugin.js");

module.exports = {
    plugins: [ new FileListPlugin("fileList.txt") ]
}

区分环境

有些时候,开发环境所使用到的webpack配置和生产环境所使用到的配置会有很大地差异

webpack为了方便区分开发环境和生产环境,支持在配置文件中导出一个函数,函数的返回值为webpack打包时所要参考的配置对象

// webpack.config.js
module.exports = function(env) {
    return {
        // 配置内容
    }
}

函数可以接收一个参数env,但该参数默认为undefined,需要开发者在运行webpack命令时通过加入--env指令参数来指定env参数的值

例如:

webpack --env abc					# env => "abc"
webpack --env.abc					# env => { abc: true }
webpack --env.abc=1					# env => { abc: 1 }
webpack --env.abc=1 --env.bcd=2		# env => { abc: 1, bcd: 2 }

在配置文件中就可以在函数中进行判断,以区分代码的运行环境,然后根据环境返回不同的配置对象

其他配置

context

该配置为一个绝对路径字符串

context: path.resolve(__dirname, "xxx")

默认情况下,entry和loaders配置中的相对路径是以CWD(当前工作路径)作为基准的,但如果设置了context配置,则会以context配置的绝对路径作为基准

output

output配置除了可以设置之前所介绍的path和filename属性外,还可以设置如下属性:

  • library

    该配置为一个标识符字符串,表示打包结果中的js文件在执行时应该将立即执行函数的返回值暴露哪个标识符

    例如:

    output: {
        library: "res"
    }
    

    则打包结果中的main.js的内容就变为了

    var res = (function(modules){
        ...;
    	function webpack_require(moduleId){
            ...;
        }
        ...;
        return webpack_require("./src/index.js");
    })({
        "./src/index.js": function(){
        	...;
    	}
    });
    

    当一个chunk中包含了多个入口文件(即entry中的chunk对应属性的值为一个数组),则取数组中最后一个入口文件的执行结果作为暴露的变量所接收的值

    var res = (function(modules){
        ...;
    	function webpack_require(moduleId){
            ...;
        }
        ...;
        webpack_require("./src/index1.js");
        return webpack_require("./src/index2.js");
    })({
        "./src/index1.js": function(){
        	...;
    	},
        "./src/index2.js": function(){
        	...;
    	}
    });
    
  • libraryTarget

    该配置为一个符字符串,用于指定打包结果中的js文件中的立即执行函数应该以什么样的方式暴露给library标识符

    默认值为"var",表示表示将立即执行函数的返回值暴露给一个使用var定义的变量

    output: {
        library: "res",
        libraryTarget: "var"
    }
    

    该属性允许的取值有:"window"、"this"、"global"、"commonjs"等

target

该配置为一个字符串,表示打包结果最终要在什么环境下运行

这里的环境不是指开发环境和生产环境,而是浏览器环境或node环境等

可以允许如下取值:

  • "web"

    默认值,表示打包结果最终是要在浏览器环境中运行的

    若设置target为"web",则源代码中导入的路径不以"./"、"../"和"/"开头的模块,webpack会直接从当前模块所在目录的node_modules目录中查找是否有相应模块,找不到就去找上级目录的node_modules目录

  • "node"

    表示打包结果最终是要在node环境中运行的

    若设置target为"node",则源代码中导入的路径不以"./"、"../"和"/"开头的模块,webpack会先找该模块是否是nodejs的内置模块,如果不是才会去找当前模块所在目录的node_modules目录中是否有相应模块,然后是上一级目录下的node_modules目录

  • 其他环境

    参见www.webpackjs.com/configurati…

resolve

该配置是一个对象,对象中包含三个属性:

  • modules

    该配置是一个数组,数组元素为默认的查找目录的名称

    resolve: {
        modules: ["node_modules"]		// 该配置的默认值
    }
    

    之所以webpack在查找导入路径不以"./"、"../"和"/"开头的模块时会从node_modules目录中进行查找,就是受该配置的影响

    数组元素可以有多个,当上一个元素对应的目录中没有对应模块时,就会查找下一个元素对应的目录

    若当前目录中所有数组元素对应的目录都没有查找成功,则返回上一级目录并重新按照此规则进行查找

  • extensions

    该配置是一个数组,数组元素为默认的模块后缀名

    resolve: {
        extensions: [".js", ".json"]				// 该配置的默认值
    }
    

    当导入的模块路径没有书写后缀名时,webpack会循环遍历extensions数组,将数组元素作为模块文件的后缀名再查找是否有模块存在

  • alias

    该配置是一个对象,用于给路径取别名

    对象中的属性的属性名为路径的别名,属性值为具体的路径(是一个绝对路径)

    var path = require("path");
    
    resolve: {
        alias: {
            "@": path.resolve(__dirname, "src")
        }
    }
    

    之后在导入模块时,就可以使用别名来代替某一段路径

externals

正常情况下,模块中如果引用了一个第三方库,在最终的打包结果中就需要把该库中的所有代码都装入到js文件中

例如:

// index.js

require("jquery");
require("lodash");

编译过后:

(function(){
    ...
})({
    "./src/index.js": function(module, exports, webpack_require){
        webpack_require("jquery");
        webpack_require("lodash");
    },
    "jquery": function(module, exports){
        // 大量的jquery代码
    },
    "lodash": function(module, exports){
        // 大量的lodash代码
    },
})

使用externals配置可以让webpack取消对某些第三方库代码的引用,以此加快打包速度,也减少了打包体积

externals的工作过程需要与开发者进行配合

开发者首先需要在html页面中使用script元素直接引用第三方库(通常会选择CDN),这些第三方库通常都会暴露一个全局变量到window对象中

在解析执行了script元素引用的第三方库代码后,页面中就可以直接使用第三方库暴露的变量了,因此在打包结果中就没有必要把node_modules中的第三方库代码加入进来

externals配置为一个对象,对象的属性的属性名为要取消引用的第三方库的模块名,属性值为第三方库暴露的变量的标识符字符串

当设置了externals配置后,配置中出现的第三方库的代码就不会被加入到打包结果中,而是在打包结果的第三方库的函数中会直接导出第三方库在全局暴露的变量

// webpack.config.js
module.exports = {
    externals: {
        jquery: "$",				// 因为jquery会暴露一个变量$到全局
        lodash: "_"					// 因为lodash会暴露一个变量_到全局
    }
}

例如:

(function(){
    ...
})({
    "./src/index.js": function(module, exports, webpack_require){
        const jq = webpack_require("jquery");
        const ld = webpack_require("lodash");
    },
    "jquery": function(module, exports){
        module.exports = $;			// 使用的是jquery在全局暴露出来的变量
    },
    "lodash": function(module, exports){
        module.exports = _;			// 使用的是lodash在全局暴露出来的变量
    },
})

最后,只需要把打包结果的js文件的script放到第三方库的script之后,js文件中的代码就可以使用到第三方库所提供的功能了

stats

stats用于控制打包过程中控制台的输出内容,其值可以是对象也可以是字符串

具体参见:www.webpackjs.com/configurati…