Webpack学习之路二(概念、Loader、Plugin)

216 阅读8分钟

一、前言

上一章节主要介绍工程化的意义、模块、浏览器模块化局限性及它的解决方案(npm+webpack)。本章主要来进行介绍Webpack的基本概念以及快速上手(当然里面也包括一些深层次的解析)。

二、🛩️webpack核心概念

  • entry:入口模版文件路径
  • output:输出bundle文件路径
  • module:module模块,webpack构建对象。
  • bundle:输出文件,构建产物。
  • chunk:中间文件,webpack构建的中间产物
  • loader:文件转换器。
  • plugin:插件,执行特定任务。

三、🚟快速上手

  1. 初始化项目
yarn init -y 
  1. yarn安装webpack、webpack-cli
yarn add webpack webpack-cli -D
  1. 搭建项目结构目录

图片.png

  1. 编写index.js文件内容
console.log("hello world");
  1. 编写webpack打包配置文件(webpack.config.js)
const path = require('path')
module.exports = {
    mode:"development",
    //入口文件
    entry:'./src/index.js',
    //打包后文件输出
    output:{
        path:path.resolve(__dirname,"./dist"),
        filename:"bundle.js"
    }
}
  1. 编写package.json中的脚本。
"scripts": {
    "build":"webpack"
  },

7.最后可直接通过npm run build实现打包

图片.png

根据上述步骤就可以快速对代码进行打包。下面来详细分析下及打包后的结果解析

🌍步骤分析及结果解析

1. 为什么要去安装webpack-cli这个包?

webpack下载完成后,会在node_modules中的.bin目录下生成webpack.cmd(webpack.sh)可执行文件。npm run build脚本运行时,会执行webpack命令。此命令是去执行node_modules中的.bin目录下的webpack.cmd(webpack.sh)文件。在npm run命令执行时,他会将node_modules中的.bin目录下的所有命令以软连接形式放置在环境变量中,等到命令执行结束之后就会恢复原样。

如果使用未在环境变量里申明的命令行指令则会提示无法将“....”项识别为 cmdlet、函数、脚本文件或可运行程序的名称

在webpack.cmd中,他会去利用node执行../webpack/bin/webpack.js文件。

1713860941997.png

这个webpack.js文件一开始它就会去判断有无安装webpack-cli

1713861153592.png

通过npm run build指令,生成bundle.js文件

2. webpack打包做了什么?

首先先看一下打包后的代码

(() => {
var __webpack_modules__ = ({
"./src/index.js":(() => {
eval("console.log(\"hello world\");\n\n//# sourceURL=webpack://quickly-start/./src/index.js?");
})
});
var __webpack_exports__ = {};
__webpack_modules__["./src/index.js"]();

})();

从打包后的程序来看,打包后的文件其实就是一个闭包,它是以入口文件开始,依次去遍历他的依赖模块,将模块放置在__webpack_modules__对象中。该对象以一个模块为键值对,键为导入模块或者入口文件配置指定的路径,值为以eval函数为函数体的一个箭头函数,eval函数主要执行的是文件内具体的内容以及sourceMap的内容。关于sourceMap他其实是提供了打包后内容与我们编写的源码进行映射,方便我们通过控制台去调试。感兴趣可以自行搜查哦。最后则是通过__webpack_modules__["./src/index.js"]();入口文件模块调用开始运行代码。

✏️Loader

在实际开发中,webpack只可打包以.js为后缀的文件,如果遇到非.js为后缀的文件则webpack会进行报错,因为它对其他后缀是是不进行处理的。这个时候就需要我们的loader了。

先上例子:

<!-- index.html -->
<script src="../dist/bundle.js"></script>
<body>
    <div class="box"></div>
</body>
/*index.css*/
.box{
    width: 100px;
    height: 100px;
    background-color: red;
}

我们通过import在index.js中引入index.css。

/*index.js*/
import './index.css'
console.log("hello world");

这个时候去进行npm run build会进行报错。

图片.png

报错信息提示需要我们去进行安装相对应文件类型解析的loader来解决不识别的问题.我们可安装配置css-loader来进行css文件解析。

yarn add css-loader -D
//webpack.config.js
const path = require('path')

module.exports = {
    mode:"development",
    entry:'./src/index.js',
    output:{
        path:path.resolve(__dirname,"./dist"),
        filename:"bundle.js"
    },
    module:{
        rules:[
            {
                //test根据正则匹配文件
                test:/\.css$/,
                //指定loader来对匹配文件进行解析
                use:['css-loader']
            }
        ]
    }
}

当通过webpack进行打包之后,运行结果如下。

1713937542900.png

可见,后缀为.css文件能够正常打包,打包后只生成了一个bundle.js文件.这也就意味着loader是只负责将指定后缀文件打包成js文件(CSS in Js)。我们将打包后的bundle.js引用至html中并且浏览器打开。发现样式并未生效

css为什么不会生效?

打包后的代码太长,我将代码分割成部分进行一一分析。

  1. __webpack_modules___:如上述第一个打包文件分析,它是根据入口文件出发,遍历依赖模块,将其全部放置在__webpack_modules___中。
var __webpack_modules__ = ({
"./src/index.css":((module, __webpack_exports__, __webpack_require__) =>{....}),
"./node_modules/css-loader/dist/runtime/api.js":((module) => {...}),
"./node_modules/css-loader/dist/runtime/noSourceMaps.js":((module) => {....}),
"./src/index.js":((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {...})

  1. __webpack_module_cache__:见名知意,它是已加载模块的缓存对象。
  2. __webpack_require__:模块导入函数
function __webpack_require__(moduleId) {
    //缓存模块中是否有当前模块,如有直接将缓存模块的exports导出
    var cachedModule = __webpack_module_cache__[moduleId];
    if (cachedModule !== undefined) {
      return cachedModule.exports;
    }
    //创建模块并且将模块放入缓存中
    var module = __webpack_module_cache__[moduleId] = {
      id: moduleId,
      exports: {}
    };
    //传入当前模块ID、模块导出对象、__webpack_require__方法
    __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
    return module.exports;

  }

__webpack_require__上又挂载了四个方法:

  • __webpack_require__.o__webpack_require__.o 被赋值为一个箭头函数,该函数接受两个参数:objprop。这个函数使用 Object.prototype.hasOwnProperty.call(obj, prop) 来检查 obj 对象是否直接拥有 prop 这个属性(而不是从原型链上继承的)。
__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))

- __webpack_require__.r:这个函数做的事情就是去做一个模块约定,方便规范之间的相互转换(实际就是在exports上挂载__esModule属性以及根据条件挂载Symbol.toStringTag属性)

__webpack_require__.r = (exports) => {
      //当`Symbol` 类型存在且`Symbol.toStringTag` 存在,则exports上添加一个名为 `Symbol.toStringTag` 的属性,其值为字符串 `'Module'`
      if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
        Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
      }
      //使用 `Object.defineProperty` 方法为 `exports` 对象添加一个名为 `__esModule` 的属性,其值为 `true`
      Object.defineProperty(exports, '__esModule', { value: true });

    };
  • __webpack_require__.d:将definition对象中的key与值全部挂载至传入的exports中。
__webpack_require__.d = (exports, definition) => {
      for(var key in definition) {
        if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
           // 挂载到exports上,对象访问属性可枚举为true
          Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
        }
      }
    };
  • __webpack_require__.n:处理模块的导出,确保无论是ES模块还是CommonJS模块,都能以统一的方式获取其导出内容。
__webpack_require__.n = (module) => {  
    // 定义一个getter函数,根据module是否是ES模块(即是否有__esModule属性)来决定返回什么  
    var getter = module && module.__esModule ?  
        // 如果是ES模块,getter函数返回模块的默认导出(module['default'])  
        () => (module['default']) :  
        // 如果不是ES模块(可能是CommonJS模块),getter函数返回整个module对象  
        () => (module);  
    // 使用__webpack_require__.d来定义getter函数的属性描述符   
    __webpack_require__.d(getter, { a: getter });  
    // 返回getter函数  
    return getter;  
};

默认导出与整个模块的区别:在ES模块中,你可以使用export default来导出一个默认值,也可以使用多个export语句来导出多个值。而CommonJS模块(如Node.js中的模块)通常导出整个模块对象。

好了铺垫了这么多,下面开始进入正题:

  1. 闭包函数从入口文件./src/index.js开始执行.
var __webpack_exports__ = __webpack_require__("./src/index.js");
  1. 由上方可知,__webpack_require__实际就是开始执行./src/index.js属性对应的箭头函数。箭头函数即执行下列的eval函数。
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _index_css__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./index.css */ \"./src/index.css\");\n\r\nconsole.log(\"hello world\");\n\n//# sourceURL=webpack://quickly-start/./src/index.js?");

看起来其实比较复杂,但是实际分析就是做了三个事情:

  • __webpack_require__.r(__webpack_exports__);在exports中利用__webpack_require__.r函数加了ESModule属性标识.

  • __webpack_require__("./src/index.css");去加载./src/index.css模块。并将导出内容赋值_index_css__WEBPACK_IMPORTED_MODULE_0__属性。

    关于这里加载index.css根据代码而言主要看他module.exports导出的是什么?

    1. __webpack_require__.d(__webpack_exports__, {"default": () => (__WEBPACK_DEFAULT_EXPORT__)});这里在一开始就利用__webpack_require__.d将后者挂载到前者上。

    2. 在文件执行的最后将CSS代码保存到___CSS_LOADER_EXPORT___变量中

      ___CSS_LOADER_EXPORT___.push([module.id, `.box{\r\n    width: 100px;\r\n    height: 100px;\r\n    background-color: red;\r\n}`, \"\"])
      
    3. ___CSS_LOADER_EXPORT___赋值给__WEBPACK_DEFAULT_EXPORT__。通过第一点合并exports导出

      const __WEBPACK_DEFAULT_EXPORT__ = (___CSS_LOADER_EXPORT___);
      
  • console.log("hello world");执行模块内其他的内容。

由上面我们可以看出,css-loader就是将我们在index.css写的样式内容导出,并没有做其他事情,所以样式无法显示在页面上。 至此我们引出另一个loader来进行解决这个问题(style-loader)。

  1. 添加style-loader模块
 yarn add style-loader -D
  1. 将style-loader配置在webpack.config.js中(配置在use数组中即可),但是必须放置在css-loader前面,因为loader是从右往左来进行的
{
    test:/\.css$/,
    use:['style-loader','css-loader']
}

在执行npm run build重新打包

我们就可看到正常运行的css代码啦!!

图片.png

那么,我们通过检查元素可以看,他是直接把css代码通过<style>标签将css加到了head上。 我们也可以通过编译后的代码来进行验证。

  1. 他先是以css-loader去加载index.css文件,并且按照上面一样将值保存至_node_modules_css_loader_dist_cjs_js_index_css__WEBPACK_IMPORTED_MODULE_6__变量中.
 var _node_modules_css_loader_dist_cjs_js_index_css__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__("./node_modules/css-loader/dist/cjs.js!./src/index.css\");
  1. style-loader加载了多个模块到__webpack_modules__模块中,它们其实大概流程就是创建style标签,并且根据条件添加一些属性,最后就将css加入到这个标签里面去。我们可以根据以下部分代码来看。
//1、在insertStyleElement模块中主要创建style标签及添加他的属性。
function insertStyleElement(options) {
 var style = document.createElement("style");
  options.setAttributes(style, options.attributes);
  options.insert(style);
  return style;
}
//在styleTagTransform模块中,将css加载进入style标签内。
function styleTagTransform(css, style) {
    ........
    style.appendChild(document.createTextNode(css));
}

当然中间还做了很多事情,这里就不细讲了。

通过以上例子,我们可以知道,其实loader就是去做一个其他后缀文件到js文件的一个转换(转换器)。

😿Plugin

我们知道loader实际上是一个转换器,对于loader而言,他只是负责将其他文件转换成js文件。但是针对于实际开发来说,不可能将代码全部打包到一个文件中,这样子会造成文件过大从而加载性能问题😂。所以这个时候就引出了我们的Plugin

webpack就是一条生产线,经过一系列处理流程将源文件转换成输出结果。webpack 通过 Tapable(类似于事件的注册监听一个库) 来组织这条复杂的生产线。webpack在运行过程中,流程节点会进行广播事件从而进行相对应时间的触发。这其实就是消息的订阅与发布。对于我们开发者而言,根据自身需求,找到对应的钩子函数,往上面挂载自己的任务。在webpack构建的时候,可进行触发执行。

🌄钩子函数是什么?

钩子函数从本质上而言,它就是事件。在webpack中,它将各个流程节点进行相对应事件触发,进而方便我们可直接介入编译过程。而钩子函数就是webpack构建时触发对应事件之后执行的函数

🌍webpack构建流程

  1. 初始化流程:Webpack首先会从配置文件(如webpack.config.js)和Shell语句中读取并合并参数,初始化所需的插件和配置插件等执行环境所需要的参数。此阶段,Webpack会根据提供的参数初始化compiler对象,并注册所有配置的插件。这些插件会监听Webpack构建生命周期的事件节点,并做出相应的反应。

  2. 编译构建流程

    • 确定入口:Webpack从配置文件中指定的entry入口开始,解析文件并构建AST(抽象语法树),找出依赖,递归地进行下去。
    • 编译模块:在递归过程中,Webpack会根据文件类型和loader配置,调用所有配置的loader对文件进行转换。然后,找出该模块依赖的其他模块,再递归进行本步骤,直到所有入口依赖的文件都经过了处理。
  3. 输出流程

    • 完成模块编译并输出:递归完成后,Webpack会得到每个文件的结果,包含每个模块以及他们之间的依赖关系。根据entry配置,Webpack会生成代码块(chunk)。
    • 输出完成:最后,Webpack会将所有的chunk转换成文件并输出到文件系统。

有两个概念需要注意:CompilerCompilationCompilercompiler 在 webpack 构建之初就已经创建,并且贯穿webpack整个生命周期,只要是做webpack编译,就会去创建一个compiler。它是全局有且仅有一个。 CompilationCompilation 在 webpack 准备编译模块时创建,它代表的是当前此次编译对象。为什么需要Compilation?我们在使用watch模式的时候,当源代码发生改变时,需要重新编译模块,但是compiler可以继续使用。如果使用compiler初始化则需要注册所有的plugin。这显然是一种浪费。

在plugin而言,webpack在CompilerCompilation提供了许多钩子,方便我们注入我们的自定义的钩子函数进而可以介入编译过程。关于钩子,点击此处查看。

✨自定义plugin

我们在以打包后文件bundle.js中,添加一个页脚作为练习。

  1. 创建plugin文件夹及FooterPlugin.js文件
  2. 下载webpack-sources包
npm install webpack-sources -D
  1. FooterPlugin.js编写
//顾名思义:拼接功能
const {ConcatSource} = require('webpack-sources')
class FooterPlugin{
    constructor(options){
        //外部传入options保存
        console.log('FooterPlugin',options);
        this.options = options;
    }
    apply(compiler){
        compiler.hooks.compilation.tap('FooterPlugin',compilation=>{
            //processAssets钩子时机:生成资源文件后对其进行处理
            compilation.hooks.processAssets.tap('FooterPlugin',()=>{
                //获取此次编译全部的chunks
                const chunks = compilation.chunks;
                for(const chunk of chunks){
                    for(const file of chunk.files){
                        //将页脚写入文件内
                        const comment = `/*${this.options.banner}*/`;
                        compilation.updateAsset(file,old=>new ConcatSource(old,'\n',comment))
                    }
                }
            })
        })
    }
}
module.exports = FooterPlugin;
  1. webpack配置文件进行注册。
const FooterPlugin = require('./plugin/FooterPlugin')
module.exports = {
     ...webpack其他配置
    plugins:[
        new FooterPlugin({
            banner:"fightingBoy 出品"
        })
    ]
}

最后就可以执行npm run build来进行构建啦。

图片.png

如结果所示,成功显示!!!

本章主要是针对于基本概念、loader、plugin的讲解,如果对你有帮助,给我点个赞吧🤩!!!!