大神眼中的webpack构建工具:对编译原理的分析

首先原谅我是个标题党,我虽然不是大神,但这是我自己对webpack构建工具编译过程和编译结果的分析的理解。

webpack的安装和使用

webpack概念:本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器。它通过一个开发时态的入口模块为起点,分析出所有的依赖关系,然后经过一系列的过程(压缩、合并),最终生成运行时态的文件。

webpack官网:https://www.webpackjs.com/

webpack的安装

webpack通过npm安装,它提供了两个包:

  • webpack:核心包,包含了webpack构建过程中要用到的所有api
  • webpack-cli:提供一个简单的cli命令,它调用了webpack核心包的api,来完成构建过程

webpack也是有两种安装方式,全局安装本地安装,全局安装可以全局使用webpack命令,但是如果你有多个项目,对应不同的webpack版本的话,全局安装就显得捉襟见肘。所以我们通常使用本地安装,每个项目都是用自己的webpack版本进行构建。我们可以通过以下命令安装webpack和webpack-cli。仅开发环境使用(-D

npm install -D webpack webpack-cli

webpack的使用

  • 打包
npx webpack

在这里插入图片描述

  • Hash:总chunk的哈希值
  • Version:webpack的版本号
  • Time:构建消耗时间
  • Built at:打包的时间和模块信息
  • EntryPoint:入口文件

注意

  1. 默认生产环境下进行打包
  2. 默认情况下,webpack会以./src/index.js作为入口文件分析依赖关系,打包到./dist/main.js文件中
  3. 因为本地安装,所以需要加npx 执行。
  • 开发环境下打包
npx webpack --mode=development

注意: 通过--mode选项可以控制webpack的打包结果的运行环境

  • 生产环境下打包
npx webpack --mode=production
  • 在脚本中配置 package.json
"scripts": {
    "build":"webpack --mode=production",
    "dev":"webpack --mode=development"
  }

到这里,基本就是就是wepack所有的指令了,下面要介绍的就是webpack的各种配置和打包原理了。


在正式学习webpack之前,我认为弄清楚以下几个概念是非常重要的

  • 入口(entry):指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。

  • 出口(output):告诉 webpack 在哪里输出它所创建的 、bundles,以及如何命名这些文件。等等

  • loader:loader本质上是一个函数,它的作用是将某个源码字符串转换成另一个源码字符串返回。当然在转换过程做了一系列的处理操作

  • 插件(plugins):loader是用来转换代码的,而插件用来执行范围更广的任务。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。

我会在下面依次具体介绍这四个重要的概念,为了更方便的理解,我先要介绍一下sourece map源码地图、webpack的编译结果编译过程

source map 源码地图

在前端工程化中,我们大部分时候不回直接运行源码,而是运行对多个源码文件进行合并、压缩等操作后转换的代码。这样就会出现一个问题,当代码出现错误时,我们不清楚具体是哪个代码发生了错误。为了方便调试错误代码,就出现了source map

当然这种source map只是为了调试方便,所以我们仅仅是在开发模式上使用。

下面是浏览器处理source map的原理。
在这里插入图片描述

在webpack中使用source map

具体配置看官方文档:https://www.webpackjs.com/configuration/devtool/

通过devtool配置源码地图,在开发环境下,默认是eval。这也是下面的编译结果中,为什么放在eval中执行

module.exports = {
    mode:"producetion",
    devtool:"eval"
}

webpack编译结果

我是将index.js文件作为入口,在index.js中导入a.js文件

/*-----------index.js----------------*/
console.log("moudle index")
const a =require("./a.js")
console.log(a)
/*--------------a.js-----------------*/
console.log("module a")
module.exports = {
    a:1
}

当我们运行npx webpack --mode=development,我们看看结果是什么吧。以下是输出目录dist/main.js内容,我将其他一些特殊处理的结果,和一些注释去掉,最后生成的代码。

 (function(modules) { // webpackBootstrap
 	// The module cache
 	var installedModules = {};
     
 	function __webpack_require__(moduleId) {
 		if(installedModules[moduleId]) {
 			return installedModules[moduleId].exports;
 		}
 		var module = installedModules[moduleId] = {
 			i: moduleId,
 			l: false,
 			exports: {}
 		};
 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
        
 		return module.exports;
 	}
 	// Load entry module and return exports
 	return __webpack_require__(__webpack_require__.s = "./src/index.js");
 })
 ({
  "./src/a.js":(function(module, exports) {
      eval("console.log(\"module a\")\r\nmodule.exports = {\r\n    a:1\r\n}\n\n//# sourceURL=webpack:///./src/a.js?");
  }),

  "./src/index.js":(function(module, exports, __webpack_require__) {
      eval("console.log(\"moudle index\")\r\nconst a =__webpack_require__(/*! ./a.js */ \"./src/a.js\")\r\nconsole.log(a)\n\n//# sourceURL=webpack:///./src/index.js?");
  })
 });

如果你能看的懂,在下佩服,可以直接略过。下面我来仿写一下输出的结果,并对其中进行解释。先附代码

(function(modules){
    let moudleExports = {} //用于缓存模块的导出结果
	//require函数相当于是运行一个模块,得到模块导出结果
    function require(moudleId){
        if(moudleExports[moudleId]){//moduleId就是模块的路径
            //检查是否有缓存,有缓存直接返回导出结果
            return moudleExports[moudleId]
        }
        let func = modules[moudleId]//得到该模块对应的函数
        let moudule = { //创建导出对象,与commonjs模块化require函数中的导出规范一致
            exports:{}
        }
        func(moudule,moudule.exports,require); //运行模块
        let result = moudule.exports //得到模块导出的结果
        moudleExports[moudleId] = result //加入缓存
        return result //返回模块的导出结果
    }
    return require("./src/index.js") //执行入口模块
})({
    //该对象保存了所有的模块,以及模块对应的代码
    "./src/a.js":function(module,exports){
        console.log("module a")
        module.exports = {
            a:1
        }
    },
    "./src/index.js":function(module,exports,require){
        console.log("moudle index")
        const a =require("./src/a.js")
        console.log(a)
    }
})

首先我们可以清楚的看到,这是一个立即执行函数,传入的参数是一个对象。

(function(modules){
	...
})(传入的参数)

首先我们看一下传入的参数,参数对象其实就是一个个模块。对象的是导入模块的路径名,并且是基于该工程下的相对路径,而对象的是一个函数,传入了3个参数,与模块化中的CMD几乎相同(不多解释了,不懂的去看模块化知识)。函数的内容是项目应模块中的代码。在这里有一点需要注意,webpack处理的函数中的代码放在了eval中运行,就是因为默认情况下,开发环境启用eval模式的源码地图。

参数解释完了,我们再来看立即执行函数中的内容,moudleExports这个变量是用来缓存模块的导出结果,如果有多个模块同时依赖一个相同的模块时,我们不需要再去读取内容,直接使用缓存中已经有的导出结果即可。接下来就是执行require函数了(webpack中进行了特殊处理__webpack_require__),这个函数是运行一个模块,并且得到这个模块的导出结果(具体的代码解释每一行都有注释)。

这就是webpack打包后的文件的结果分析。

webpack编译过程

在介绍编译过程中可能会涉及到一些概念,在这里先解释一下

涉及概念

  • module:模块,分割的代码单元,webpack中的模块可以是任何内容的文件,不仅限于JS

  • chunk:webpack内部构建模块的块,一个chunk中包含多个模块,这些模块是从入口模块通过依赖分析得来的

  • bundle:chunk构建好模块后会生成chunk的资源清单,清单中的每一项就是一个bundle,可以认为bundle就是最终生成的文件

  • hash:最终的资源清单所有内容联合生成的hash值

  • chunkhash:chunk生成的资源清单内容联合生成的hash值

  • chunkname:chunk的名称,如果没有配置则使用main

  • id:通常指chunk的唯一编号,如果在开发环境下构建,和chunkname相同;如果是生产环境下构建,则使用一个从0开始的数字进行编号

webpack 的作用是将源代码编译(构建、打包)成最终代码
在这里插入图片描述

整个过程大致分为三个步骤:初始化编译输出
在这里插入图片描述

初始化

在这个阶段,webpack会将CLI参数配置文件默认配置进行融合,形成一个最终的配置对象。对配置的处理过程是依托一个第三方库yargs完成的。

编译

编译这一阶段有四个步骤:创建chunk构建所有依赖模块产生chunk assets合并chunk assets

  1. 创建chunk

chunk是webpack在内部构建过程中的一个概念,译为,它表示通过某个入口找到的所有依赖的统称。

根据入口模块(默认为./src/index.js)创建一个chunk。

每个chunk都有至少两个属性:

  • name:默认为main
  • id:唯一编号,开发环境和name相同,生产环境是一个数字,从0开始
  1. 构建所有依赖模块
    在这里插入图片描述

很重要的解释:webpack在找到入口文件后,首先会查看模块文件,检查chunk模块记录中是否已经记录过该模块,如果记录过则直接结束。如果未记录过,则读取文件的内容,对内容进行语法分析,形成抽象语法树(AST),记录抽象树中的依赖(require函数),保存到dependencies中,接下来替换依赖函数,最后保存转换后的模块代码,(替换依赖函数和保存转换后的模块化代码就是我在编译结果分析的步骤),记录在chunk的模块记录中。再根据dependencies中保存的依赖依次递归加载模块,循环这个过程,直到所有模块都读取完成。

  1. 产生chunk assets

在第二步完成后,webpack会根据chunk中的模块记录和配置生成一个资源列表,即chunk assets,列表中包含了模块id模块转换后的代码,资源列表可以理解为是生成到最终文件的文件名和文件内容
在这里插入图片描述

解释:chunk hash是根据所有chunk assets的内容生成的一个hash字符串

  1. 合并chunk assets

因为一个项目中可能有多个文件入口,也可能有多个chunk,这个步骤是将多个chunk assets合并到一起,并产生一个总的hash
在这里插入图片描述

输出

此步骤非常简单,webpack将利用node中的fs模块(文件处理模块),根据编译产生的总的assets,生成相应的文件。

总过程示意图
在这里插入图片描述


webpack.config.js配置文件

默认情况下,webpack会读取webpack.config.js文件作为配置文件,但也可以通过CLI参数--config来指定某个配置文件。我们的入口、出口loader和plugins都是在配置文件中配置的。因为读取配置文件是在node中运行,所以配置文件中的代码,必须是有效的node代码,否则会报错。

基本配置:

/*--------------webpack.config.js---------*/
module.exports = {
    mode:"producetion",   	 //mode:打包模式,生产环境(producetion)还是开发环境(development)
    entry:"./src/index.js",  //entry:入口文件
    output:{    			 //output:出口文件
        filename:"main.js"
    }
}   

在node中有一个path模块,通常是用来组装路径,会根据不同的操作系统修改/\

const path = require("path")
const result = path.resolve(__dirname,"src")
//__dirname:所有情况下,都表示当前运行的js文件所在的目录,他是一个绝对路径

具体看node内置模块path的官网: https://nodejs.org/dist/latest-v12.x/docs/api/path.html

1、入口

入口文件通过entry来配置。一个工程可能有一个入口文件,也可能有多个配置文件。

入口配置的是chunk,属性名是chunk的名称。可以写相对位置

/*--------------webpack.config.js---------*/
module.exports = {
    entry:"./src/index.js",  //entry:入口文件
}

多个入口文件

/*--------------webpack.config.js---------*/
module.exports = {
    entry:{
        main: './main.js'
        app:'./app.js'
    }
}

2、出口

出口文件通过output来配置,同样的可能有多个出口文件。出口文件的写法比入口文件就多得多了。

  • 静态名称
/*--------------webpack.config.js---------*/
const path = require("path")
module.exports = {
    output:{//出口配置
        path:path.resolve(__dirname,"target"),//输出资源放置的文件夹,默认是dist
        //直接写静态写法,如main.js等等
        filename:"main.js"//配置资源的文件名,合并模块后的的js代码的文件的规则
    }
}  
  • 带哈希的名称
/*--------------webpack.config.js---------*/
const path = require("path")
module.exports = {
    entry:{
        index:"./src/index.js"
    },
   	output:{//出口配置
        path:path.resolve(__dirname,"target"),//输出资源放置的文件夹,默认是dist
        /**
         * name:入口文件的属性名,占位符
         * hash:总资源的哈希值 
         * chunkhash:chunk的哈希值
         */
        filename:"[name].[hash:5].js"  // eg: index.ea656.js
    }
}

3、loader

webpack做的事情,仅仅是分析出各种模块的依赖关系,然后形成资源列表,最终打包生成到指定的文件中。更多的功能需要借助webpack loaderswebpack plugins完成。

loader本质上是一个函数,它的作用是将某个源码字符串转换成另一个源码字符串返回。
在这里插入图片描述
在webpack编译过程中,loader是在读取文件内容之后,对文件内容进行语法树分析之前,对文件内容进行操作的。如图所示:
在这里插入图片描述

处理loaders流程:
在这里插入图片描述

首先会判断当前模块是否符合loaders规定的规则,如果符合则读取规则中对应的loaders,并把它加入loaders数组中。如果不符合,则loaders是一个空数组。接下来代码会依次安装loaders数组中的规则被转换。类似于一个栈操作,第一个匹配的规则被最后执行,最后匹配的规则被先执行。

loader配置:

module.exports = {
    module: { //针对模块的配置,目前版本只有两个配置,rules、noParse
        rules: [ //模块匹配规则,可以存在多个规则
            { //每个规则是一个对象
                test: /\.js$/, //匹配的模块正则
                use: 
                [ //匹配到了之后,使用哪些加载器
                    {  //每个加载器都是一个对象
                        loader: "模块路径", //loader模块的路径,该字符串会被放置到require中
                        options: 
                        { 
                            //向对应loader传递的额外参数
                            name:"lkx"
                        }
                    }
                ]
            }
        ]
    }
}
//--------是不是看起来很复杂,去掉注释和格式化以后-------------
module.exports = {
    module: {
        rules: [{
            test: /\.js$/,
            use: [{
                loader: "模块路径",
                options: {...}
            }]
        }]
    }
}

简化后的配置

module.exports = {
    module: { //针对模块的配置,目前版本只有两个配置,rules、noParse
        rules: [ //模块匹配规则,可以存在多个规则
            { //每个规则是一个对象
                test: /\.js$/, //匹配的模块正则
                use: ["模块路径1", "模块路径2"]//loader模块的路径,该字符串会被放置到require中
            }
        ]
    }
}

一般来说,loader是不需要自己写的。为了装逼我们自己写一个loader。

这个loader的作用就是把文件中的变量 a = 1中的变量两个字转换成var

//----------------webpack.config.js-------------------
module.exports = {
    mode:"development",
    module:{
        //模块的匹配规则
        rules:[//从后往前看,先看规则2再看规则1
            //规则1
            {
                test:/index\.js$/,//匹配的正则表达式
                use:[//匹配到了之后,使用哪些加载器
                    {
                        //每个加载器都是一个对象
                        loader:"./loaders/test-loader.js",//加载器路径
                        options:{
                            // 参数,在loader函数中上下文this中
                            //可以通过loader-utils第三方库完成
                            changeVar:"变量"
                        }
                    }
                ]
            }
            //规则2
        ],
        // noParse:[],//是否不要解析某个模块
    }
}
//----------------test-loader.js-------------------
const loaderUtils = require("loader-utils")//专门处理loader参数的插件库
module.exports = function(sourceCode){
    console.log("执行了test-loader")
    const options = loaderUtils.getOptions(this)
    const reg = new RegExp(options.changeVar,"g")
    return sourceCode.replace(reg,"var")
}
//----------------index.js-------------------
变量 a = 1

4、plugins

loader的只能用来转换代码,一些在初始化或者输出时需要进行特殊处理的功能就无法实现。这时候就有了plugin

在这里插入图片描述

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

class MyPlugin{
    apply(compiler){
		...
    }
}

要将插件应用到webpack,需要把插件对象配置到webpack的plugins数组中,如下:

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

apply函数会在初始化阶段,创建好Compiler对象后运行。整个webpack打包期间只有一个compiler对象,后续完成打包工作的是compiler对象内部创建的compilation

compiler对象提供了大量的钩子函数(hooks),plugin的开发者可以注册这些钩子函数,参与webpack编译和生成。

举例说明钩子函数的使用:

class MyPlugin{
    apply(compiler){
        compiler.hooks.事件名称.事件类型(name, function(compilation){
            //事件处理函数
        })
    }
}
//-----------偷一个官方的例子给大家看看-----------------------
class ConsoleLogOnBuildWebpackPlugin {
    apply(compiler) {
        compiler.hooks.run.tap(pluginName, compilation => {
            console.log("webpack 构建过程开始!");
        });
    }
}

事件名称:即要监听的事件名,即钩子名

详细的钩子看官方文档:https://www.webpackjs.com/api/compiler-hooks

事件类型:这一部分使用的是 Tapable API,这个小型的库是一个专门用于钩子函数监听的库。

它提供了一些事件类型:

  • tap:注册一个同步的钩子函数,函数运行完毕则表示事件处理结束
  • tapAsync:注册一个基于回调的异步的钩子函数,函数通过调用一个回调表示事件处理结束
  • tapPromise:注册一个基于Promise的异步的钩子函数,函数通过返回的Promise进入已决状态表示事件处理结束

webpack的大部分知识点到这里就结束了。下面时整理的一些零散的配置


区分环境

有些时候,我们需要针对生产环境和开发环境分别书写webpack配置。webpack允许配置不仅可以是一个对象,还可以是一个函数。在开始构建时,webpack如果发现配置是一个函数,会调用该函数,将函数返回的对象作为配置内容,因此,开发者可以根据不同的环境返回不同的对象。

module.exports = env => {
    if(env.prod){ //生产环境
       return {
        	//配置内容
    	}
    }else{//开发环境
        return {
        	//配置内容
    	}
    }
}

在执行webpack命令时传入参数

npx webpack --env.prod #  env: {prod:true}
npx webpack --env.prod=true #  env: {prod:true}

resolve解析中配置

resolve的相关配置主要用于控制模块解析过程,这里主要说extensionsalias

extensions

当解析模块时,遇到无具体后缀的导入语句,例如require("test"),会依次测试它的后缀名。可以简写后缀名

extensions: [".js", ".json"]  //默认值

alias

在配置中添加别名,比如文件路径。在大型系统中,源码结构往往比较深和复杂,别名配置可以让我们更加方便的导入依赖。

alias: {
  "@": path.resolve(__dirname, 'src'),
  "_": __dirname
}
//require("@/abc.js"),webpack会将其看作是:require(src的绝对路径+"/abc.js")

externals

防止将某些 import 的包打包到 bundle 中,而是在运行时再去从外部获取这些扩展依赖。

这比较适用于一些第三方库来自于外部CDN的情况,这样一来,即可以在页面中使用CDN,又让bundle的体积变得更小,还不影响源码的编写。

举例

externals: {
    jquery: "$",
    lodash: "_"
}

如果外部已经用cdn引入了jqueryloadsh这两个模块,不需要再用webpack进行打包,则通过externals配置,直接将jquery代码改为导出一个$,将lodash导出为_

//-----------index.js-----------
require("jquery")
require("lodash")
//-----------bundle.js----------
(function(){
    ...
})({
    "./src/index.js": function(module, exports, __webpack_require__){
        __webpack_require__("jquery")
        __webpack_require__("lodash")
    },
    "jquery": function(module, exports){
        module.exports = $;
        //如果不用externals,这里是大量的jquery的源码
    },
    "lodash": function(module, exports){
        module.exports = _;
        //如果不用externals,这里是大量的loadsh的源码
    },
})

stats

stats控制的是构建过程中控制台的输出内容

具体看官方文档:https://www.webpackjs.com/configuration/stats/

stats: {
    colors: true, //输出控制台颜色
    modules: false, //构建模块信息
    hash: false, //compilation 的哈希值
    builtAt: false //添加构建日期和构建时间信息
}

备注:解释图片非本人制作,侵删

发布了21 篇原创文章 · 获赞 82 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/Newbie___/article/details/104782796