Skip to content

webpack打包出的文件解析

打包出的结果

我们看下上一节打包出的结果,build\bundle.js,把没用的代码和注释删一删,然后开始捋一捋代码

看代码注释的时候,从001开始,一直到038结束,顺序是代码执行的顺序

js
// 001 其实就是一个匿名自执行函数
// 002 折叠一下就看出来了
(function (modules) { // 007 webpack的启动函数 webpackBootstrap
  // 008 先定义了一个缓存 The module cache
  // 009 缓存主要放什么呢?就是这个模块加载完了,我不需要再次加载模块,可以直接从缓存中拿
  var installedModules = {};


  // 010 自己实现了一个require方法 The require function
  // 因为require是没办法在浏览器中运行的,所以自己实现了一个
  function __webpack_require__(moduleId) { // 014 这个入口文件的名字 就会跑到这里./src/index.js

    // 015 检查这个模块是否在缓存中,刚加载肯定不在缓存中 Check if module is in cache
    if (installedModules[moduleId]) { // 016 不在缓存中
      return installedModules[moduleId].exports;
    }

    // 017 因为不在缓存中,所以会安装这个模块 Create a new module (and put it into the cache)
    // 018 是怎么安装呢,就是用了那个缓存对象
    // 019 在缓存中的对象 使用文件名为key ./src/index.js

    var module = installedModules[moduleId] = {
      i: moduleId, // 020 i属性是 id,现在用不到
      l: false, // 021 l属性是false,现在也用不到
      exports: {} // 022 主要是这个 exports 对象
    };


    // Execute the module function
    // 023 modules是在 006 中传入的对象,所以这就相当于拿到了参数中对应的函数
    // 024 使用call 执行这个函数,
    // 025 传入的参数 module.exports:当前模块的 exports 空对象(在 022 步骤中的)
    // 026 传入的参数 module,当前模块
    // 027 传入的参数  module.exports 当前模块的 exports 空对象(在 022 步骤中的)
    // 028 传入的参数 __webpack_require__ (010 中 自己实现的require方法)
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    // 031 新需要加载的模块 ./src/a.js,对应的函数被执行了


    // 033 当前模块的方法执行完毕了,把表示为改为true,表示当前模块已经被加载过了 Flag the module as loaded
    // 037 当前模块的方法执行完毕了,把表示为改为true,表示当前模块已经被加载过了
    module.l = true;
    // 034 把当前模块的 exports 结果返回 Return the exports of the module
    // 038 把当前模块的 exports 结果返回 程序执行完毕
    return module.exports;
  }
  // 013 入口模块 Load entry module and return exports
  // 011 在此处调用了 内部自己实现的require方法
  // 012 这个方法回去找 我们的入口文件 ./src/index.js
  return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
  ({ // 003 这就是匿名函数的参数,就是一个对象
    // 006 把这个对象传递给了 modules

    "./src/a.js": // 004 key,key是当前模块的路径
      (function (module, exports) { // 005 value,就是一个函数
        // 032 module.exports = 'xzz' 就把当前模块的 exports 空对象,变成了字符串 'xzz'
        eval("module.exports = 'xzz'\n\n//# sourceURL=webpack:///./src/a.js?");
      }),
    "./src/index.js":
      (function (module, exports, __webpack_require__) {
        // 029 这个函数被执行了
        // 030 let str = __webpack_require__( \"./src/a.js\") 发现代码里有需要加载的模块,然后就会跳转到 010 去加载这个模块
        eval("let str = __webpack_require__( \"./src/a.js\")\r\n\r\nconsole.log('hello cheny');\r\n\r\nconsole.log(str);\n\n//# sourceURL=webpack:///./src/index.js?");
        // 035 所以此时的 str 就是 034 中返回的字符串 'xzz'
        // 036 最后执行完毕,打印出结果 cheny xzz
      })
  });

小结一下

  1. 因为浏览器不会解析commonjs的模块,所以所有require的模块代码,浏览器都是看不懂的
  2. 所以webpack就自己实现了一个require方法,__webpack_require__
  3. webpack打包后的文件,是一个自执行函数,自执行函数的参数是一个对象,这个对象的key是当前模块的路径(相对路径),value是一个函数,待会加载这个模块的时候会执行这个函数。函数体是使用eval方法包裹的代码片段,原先我们使用require的写法,都被改写为了let xxx = __webpack_require__('模块路径')的形式,所以再次执行的执行,都会调用webpack自己写的require方法。
  4. __webpack_require__,在匿名自执行函数里声明的,在自执行函数的最开始,有一对象installedModules,用来放已经加载过的模块,如果没有加载的模块,第一次加载的时候都会塞到这个对象上,塞的时候每个模块也是一个对象,对象的key就是模块的路径,value也是一个对象,里面主要操作export空对象,所有的返回值都会挂载到这个对象上。
  5. 所以第一次执行__webpack_require__方法时,会从入口文件开始,如果入口文件里需要加载别的模块,就递归执行__webpack_require__方法,去找对应的模块,执行完成后,对应的返回值都会挂载到exports对象上,然后回溯到上一级,如此循环往复,直到代码执行完毕。
  6. 所以webpack的核心原理就是这个自己实现的require方法__webpack_require__

如何在打包的时候使用指定名称的配置文件

在使用npx webpack打包时,会默认找webpack.config.js文件(上一节的知识点)

假如我们想换个别的名称,怎么办?

在根目录下建一个新的配置文件,webpack.config.my.js

js
// webpack-dev-1\webpack.config.my.js
// webpack是node写出来的,所以使用node的语法

let path = require('path')

module.exports = {
    mode: 'development', // 模式,默认两种模式,production 和 development

    entry: './src/index.js', // 入口

    output: {
        filename: 'bundle.js', // 打包后的文件名
        path: path.resolve(__dirname, 'build2'), // 打包后的路径,必须是一个绝对路径
    }
}

我们可以使用带参数的npx webpack来使用指定配置文件打包

shell
npx webpack --config webpack.config.my.js

或者你嫌指令太长记不住,可以在package.json,自定义一个脚本来执行,这样也是可以的(npx就不用带了)

json
{
  "name": "webpack-dev-1",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "build": "webpack --config webpack.config.my.js"
  },
  "devDependencies": {
    "webpack": "4.32.2",
    "webpack-cli": "3.3.2"
  }
}

然后使用npm指令来打包

shell
npm run build

image-20220106230726138

另外,假如你在package.json里不知道这个文件的名字,你在打包的时候才知道,所以package.json就变成了这样

json
{
  "name": "webpack-dev-1",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "webpack": "4.32.2",
    "webpack-cli": "3.3.2"
  }
}

如果是这样的话,在使用build命令时,可以直接把名字带上,但是记得多加个--

shell
npm run build -- --config webpack.config.my.js

image-20220106231008576

效果是一样的。多加的-- 后面的就会别识别为字符串了。

总结

webpack的原理

  1. 因为浏览器不会解析commonjs的模块,所以所有require的模块代码,浏览器都是看不懂的
  2. 所以webpack就自己实现了一个require方法,__webpack_require__
  3. webpack打包后的文件,是一个自执行函数,自执行函数的参数是一个对象,这个对象的key是当前模块的路径(相对路径),value是一个函数,待会加载这个模块的时候会执行这个函数。函数体是使用eval方法包裹的代码片段,原先我们使用require的写法,都被改写为了let xxx = __webpack_require__('模块路径')的形式,所以再次执行的执行,都会调用webpack自己写的require方法。
  4. __webpack_require__,在匿名自执行函数里声明的,在自执行函数的最开始,有一对象installedModules,用来放已经加载过的模块,如果没有加载的模块,第一次加载的时候都会塞到这个对象上,塞的时候每个模块也是一个对象,对象的key就是模块的路径,value也是一个对象,里面主要操作export空对象,所有的返回值都会挂载到这个对象上。
  5. 所以第一次执行__webpack_require__方法时,会从入口文件开始,如果入口文件里需要加载别的模块,就递归执行__webpack_require__方法,去找对应的模块,执行完成后,对应的返回值都会挂载到exports对象上,然后回溯到上一级,如此循环往复,直到代码执行完毕。
  6. 所以webpack的核心原理就是这个自己实现的require方法__webpack_require__

使用指定配置文件运行webpack

  1. npx webpack --config webpack.config.my.js
  2. 或者在package.json里配置脚本,"build": "webpack --config webpack.config.my.js"
  3. 如果使用脚本传参时,需要多加两个杠,npm run build -- --config webpack.config.my.js,多加的-- 后面的就会别识别为字符串了。

参考

https://www.bilibili.com/video/BV1a4411e7Bz?p=3