2018-09-29

基本概念


  • Entry:入口,Webpack 执行构建的第一步将从 Entry 开始,可抽象成输入。
  • Module:模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。
  • Chunk:代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。
  • Loader:模块转换器,用于把模块原内容按照需求转换成新内容。
  • Plugin:扩展插件,在 Webpack 构建流程中的特定时机会广播出对应的事件,插件可以监听这些事件的发生,在特定时机做对应的事情。

2. 调试webpack [#](#t12. 调试webpack)

var webpackPath=require('path').resolve(__dirname,'node_modules', 'webpack-cli', 'bin', 'cli.js');
require(webpackPath);
const path=require('path');
module.exports={
    mode:"development",
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname,'dist'),
        filename:'bundle.js'
    }
}

3. 主要工作流程 [#](#t23. 主要工作流程)

Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的run方法开始执行编译;
  3. 确定入口:根据配置中的 entry 找出所有的入口文件
  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行编译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
  5. 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。
  8. 在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。

4. 流程详解 [#](#t34. 流程详解)

  • 初始化:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler。
  • 编译:从 Entry 发出,针对每个 Module 串行调用对应的 Loader 去编译文件内容,再找到该 Module 依赖的 Module,递归地进行编译处理。
  • 输出:对编译后的 Module 组合成 Chunk,把 Chunk 转换成文件,输出到文件系统。

4.1 初始化阶段 [#](#t44.1 初始化阶段)

事件名 解释 位置
初始化参数 从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。 这个过程中还会执行配置文件中的插件实例化语句 new Plugin()。 webpack-cli/bin/cli.js:58:require("./config-yargs")(yargs) cli.js:281:yargs.parse webpack-cli/bin/convert-argv.js:562:addPlugin(options, loadPlugin(value))
实例化 Compiler 用上一步得到的参数初始化 Compiler 实例 Compiler 负责文件监听和启动编译 Compiler 实例中包含了完整的 Webpack 配置,全局只有一个 Compiler 实例。 cli.js:440:compiler = webpack(options) cli.js:521:compiler.watch cli.js:524:compiler.run
加载插件 依次调用插件的 apply 方法,让插件可以监听后续的所有事件节点。 同时给插件传入 compiler 实例的引用,以方便插件通过 compiler 调用 Webpack 提供的 API。 lib/webpack.js:37:plugin.apply(compiler)
environment 开始应用 Node.js 风格的文件系统到 compiler 对象,以方便后续的文件寻找和读取。 lib/webpack.js:34:new NodeEnvironmentPlugin().apply(compiler)
entry-option 读取配置的 Entrys,为每个 Entry 实例化一个对应的 EntryPlugin,为后面该 Entry 的递归解析工作做准备。 webpack/lib/webpack.js::42:compiler.options = new WebpackOptionsApply().process(options, compiler); webpack/lib/WebpackOptionsApply.js:294:new EntryOptionPlugin().apply(compiler) webpack/lib/EntryOptionPlugin.js:23:new SingleEntryPlugin
after-plugins 调用完所有内置的和配置的插件的 apply 方法。 WebpackOptionsApply.js:476:compiler.hooks.afterPlugins.call(compiler);`
after-resolvers 根据配置初始化完 resolver,resolver 负责在文件系统中寻找指定路径的文件。 WebpackOptionsApply.js:514:compiler.hooks.afterResolvers.call(compiler);

4.2 编译阶段 [#](#t54.2 编译阶段)

事件名 解释 位置
run 启动一次新的编译。 cli.js:524compiler.run(compilerCallback)
watch-run 和 run 类似,区别在于它是在监听模式下启动的编译,在这个事件中可以获取到是哪些文件发生了变化导致重新启动一次新的编译。 webpack/lib/Watching.js:40:this.compiler.hooks.watchRun
compile 该事件是为了告诉插件一次新的编译将要启动,同时会给插件带上 compiler 对象。 Compiler.js:274:this.compile(onCompiled) Compiler.js:538:this.hooks.compile.call(params)
compilation 当 Webpack 以开发模式运行时,每当检测到文件变化,一次新的 Compilation 将被创建。一个 Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。Compilation 对象也提供了很多事件回调供插件做扩展。 Compiler.js:540:this.newCompilation(params);
make 一个新的 Compilation 创建完毕,即将从 Entry 开始读取文件,根据文件类型和配置的 Loader 对文件进行编译,编译完后再找出该文件依赖的文件,递归的编译和解析。 Compiler.js:542:this.hooks.make.callAsync
after-compile 一次 Compilation 执行完成。 Compiler.js:550:this.hooks.afterCompile.callAsync

4.3 compilation 事件 [#](#t64.3 compilation 事件)

事件名 解释 位置
build-module 使用对应的 Loader 去转换一个模块。 Compilation.js:1024:addEntry Compilation.js:1036:this._addModuleChain Compilation.js:993:this.buildModule Compilation.js:615:this.hooks.buildModule.call(module); webpack/lib/NormalModule.js:410:this.doBuild webpack/lib/NormalModule.js:263:runLoaders loader-runner/lib/LoaderRunner.js:362:iteratePitchingLoaders
normal-module-loader 在用 Loader 对一个模块转换完后,使用 acorn 解析转换后的内容,输出对应的抽象语法树(AST),以方便 Webpack 后面对代码的分析。 webpack/lib/NormalModule.js:204:compilation.hooks.normalModuleLoader.call
program 从配置的入口模块开始,分析其 AST,当遇到 require 等导入其它模块语句时,便将其加入到依赖的模块列表,同时对新找出的依赖模块递归分析,最终搞清所有模块的依赖关系。 Compilation.js:970:const afterBuild Compilation.js:977:this.processModuleDependencies Compilation.js:741:this.addModuleDependencies Compilation.js:1123:this.hooks.finishModules
seal 所有模块及其依赖的模块都通过 Loader 转换完成后,根据依赖关系开始生成 Chunk。 Compilation.js:1147:this.hooks.seal.call Compilation.js:1162:const chunk = this.addChunk(name) Compilation.js:1246:this.createHash() Compilation.js:1254:this.createModuleAssets() Compilation.js:1257:this.createChunkAssets()

4.4 输出阶段 [#](#t74.4 输出阶段)

事件名 解释 位置
should-emit 所有需要输出的文件已经生成好,询问插件哪些文件需要输出,哪些不需要。 Compiler.js:220:this.hooks.shouldEmit.call
emit 确定好要输出哪些文件后,执行文件输出,可以在这里获取和修改输出内容。 Compiler.js:231:this.emitAssets Compiler.js:364:this.hooks.emit.callAsync Compiler.js:309:emitFiles compilation.assets Compiler.js::339:this.outputFileSystem.writeFile(targetPath, content, callback)
after-emit 文件输出完毕。 Compiler.js:355:this.hooks.afterEmit.callAsync
done 成功完成一次完成的编译和输出流程。 Compiler.js::257:this.hooks.done.callAsync
failed 如果在编译和输出流程中遇到异常导致 Webpack 退出时,就会直接跳转到本步骤,插件可以在本事件中获取到具体的错误原因。

在输出阶段已经得到了各个模块经过转换后的结果和其依赖关系,并且把相关模块组合在一起形成一个个 Chunk。 在输出阶段会根据 Chunk 的类型,使用对应的模版生成最终要要输出的文件内容。

Compilation.js:1147

seal(callback) {
const connectChunkAndModule = (chunk, module) => {
    if (module.addChunk(chunk)) {
        chunk.addModule(module);
    }
};

addChunk(chunk) {
        if (this._chunks.has(chunk)) return false;
        this._chunks.add(chunk);
        return true;
    }
    addModule(module) {
        if (!this._modules.has(module)) {
            this._modules.add(module);
            return true;
        }
        return false;
    }
}
this.createModuleAssets();
this.createChunkAssets();
    this.assets[file] = source;

5. 文件解析 [#](#t85. 文件解析)

5.1 单入口 [#](#t95.1 单入口)

a.js
/**
let a=1;
let b=2;
export function log(val) {
    console.log(val);
}
export default {
    a,
    b
}
app.js
import tt from './a'
import {log} from './a'
console.log(tt.b)
log(tt.b)

 */

(function (modules) { // 启动入口
  //模块缓存
  var installedModules = {};

  //require函数
  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__);

    // 设置模块为已经加载
    module.l = true;

    // 返回模块的导出对象
    return module.exports;
  }

  //为了导出对象定义getter函数
  __webpack_require__.d = function(exports, name, getter) {
    if(!__webpack_require__.o(exports, name)) {
      Object.defineProperty(exports, name, { enumerable: true, get: getter });
    }
  };

  //在导出对象上定义__esModule
  __webpack_require__.r = function(exports) {
    if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
      Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
    }
    Object.defineProperty(exports, '__esModule', { value: true });
  };

  //判断对象身上是否有自己这个属性
  __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };

  //加载入口模块并且返回默认导出对象
  return __webpack_require__(__webpack_require__.s = "./src/app.js");
})
/************************************************************************/
({

"./src/a.js":
(function(module, __webpack_exports__, __webpack_require__) {
 eval(`
      __webpack_require__.r(__webpack_exports__);//表示这个一个es6 module
      //定义一个模块的getter
      __webpack_require__.d(__webpack_exports__, \"log\", function() { return log; });
      let a=1;//定义两个变量
      let b=2;
      function log(val) {
        console.log(val);
      }
      //默认导出对象{a,b}
      __webpack_exports__[\"default\"] = ({a,b});
     `);
  }),

  "./src/app.js":
  (function(module, __webpack_exports__, __webpack_require__) {
 eval(`
 __webpack_require__.r(__webpack_exports__);
 var _a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/a.js");
 console.log(_a__WEBPACK_IMPORTED_MODULE_0__["default"].b)
 _a__WEBPACK_IMPORTED_MODULE_0__["log"](_a__WEBPACK_IMPORTED_MODULE_0__[\"default\"].b)`);
  })
});

5.2 多入口 [#](#t105.2 多入口)

5.2.1 bundle.js [#](#t115.2.1 bundle.js)

/**
app.js
let play=document.getElementById('play');
play.addEventListener('click',() => {
    import('./video.js').then(video => {
        let name=video.getName();
        console.log(name);
    });
});
lazy.js
export function getName() {
    return 'zxmf';
}
 */
(function (modules) { // 启动函数
  //安装一个JSONP回调函数,为了懒加载chunk,发出JSONP请求,回来后调用此回调函数
  function webpackJsonpCallback(data) {
    debugger;
    var chunkIds = data[0];//获取的ids
    var moreModules = data[1];//额外的模块对象

   //把额外的模块对象添加到模块对象中,然后把所有的chunkIds设置为已经加载,并且触发回调
     var moduleId,chunkId,i=0,resolves=[];
     //循环所有的chunkIds
    for(;i < chunkIds.length; i++) {
       chunkId=chunkIds[i];//模块id
       //如果是在等待成功,则添加到解决数组中
      if(installedChunks[chunkId]) {
        resolves.push(installedChunks[chunkId][0]);
      }
      installedChunks[chunkId] = 0;//设置为加载完成
     }
     //合并新模块到老的模块对象
    for(moduleId in moreModules) {
      if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
        modules[moduleId] = moreModules[moduleId];
      }
     }
     //就是也缓存在数组里一份数据
    if(parentJsonpFunction) parentJsonpFunction(data);
   //让promise从前往后都成功
    while(resolves.length) {
      resolves.shift()();
    }

  };

  // 模块缓存
  var installedModules = {};

 //对象用来加载和存放chunk,一个chunk可以包含多个模块,一个模块可以被多个模块包含
 //chunk=undefined  表示未加载
 //chunk=null  表示预加载/预获取
 //chunk=promise  表示加载中
 //chunk=0  表示加载完成
  var installedChunks = {
    "main": 0
  };

  //返回脚本路径函数
  function jsonpScriptSrc(chunkId) {
   //公开路径+chunkId+bundle.js后缀
    return __webpack_require__.p + "" + chunkId + ".bundle.js"
  }

  // require函数
  function __webpack_require__(moduleId) {
    //检查模块ID是否在缓存中
    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__);

   //把模块标记为已经加载
    module.l = true;

   //返回模块导出对象
    return module.exports;
  }

 //入口文件里只包含入口代码块,其它的代码块需要靠这个函数来加载
  __webpack_require__.e=function requireEnsure(chunkId) {
     //promise数组
    var promises = [];

    // JSONP chunk loading for javascript
   //先获取这个代码块的加载状态
    var installedChunkData = installedChunks[chunkId];
    if(installedChunkData !== 0) { //0意味着已经安装
      //一个promise标示正在加载,有值就是promise
      if(installedChunkData) {
        promises.push(installedChunkData[2]);
      } else {
         //在chunk缓存中开启promise installedChunkData=[resolve,reject,promise]
        var promise = new Promise(function(resolve, reject) {
          installedChunkData = installedChunks[chunkId] = [resolve, reject];
         });
       //把promise放进去
        promises.push(installedChunkData[2] = promise);

        // 开始加载代码块
        var head = document.getElementsByTagName('head')[0];//获取head
        var script = document.createElement('script');//创建脚本
        var onScriptComplete;//脚本加载完毕

        script.charset = 'utf-8';//字符编码
        script.timeout = 120;//超时时间
        if (__webpack_require__.nc) {//加入随机数保证安全
          script.setAttribute("nonce", __webpack_require__.nc);
        }
        script.src = jsonpScriptSrc(chunkId);//指定加载的连接地址

        onScriptComplete = function (event) {//脚本加载完成
          // 在IE下避免内存泄漏
          script.onerror = script.onload = null;
          clearTimeout(timeout);//如果回来的就清除定时器
          var chunk = installedChunks[chunkId];//取的对象
          if(chunk !== 0) {//如果还未加载
            if(chunk) {//如果有值
              var errorType = event && (event.type === 'load' ? 'missing' : event.type);
              var realSrc = event && event.target && event.target.src;
              var error = new Error('Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')');
              error.type = errorType;
              error.request = realSrc;
              chunk[1](error);//用错误对象让promise失败
            }
            installedChunks[chunkId] = undefined;//表示未加载
          }
        };
        var timeout = setTimeout(function(){
          onScriptComplete({ type: 'timeout', target: script });
         },120000);
       //加载正确什么都不干,加载不正确就报错
        script.onerror = script.onload = onScriptComplete;
        head.appendChild(script);
      }
     }
    //返回了一个promise
    return Promise.all(promises);
  };

  // expose the modules object (__webpack_modules__)
  __webpack_require__.m = modules;

  // expose the module cache
  __webpack_require__.c = installedModules;

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

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

  // create a fake namespace object
  // mode & 1: value is a module id, require it
  // mode & 2: merge all properties of value into the ns
  // mode & 4: return value when already ns object
  // mode & 8|1: behave like require
  __webpack_require__.t = function(value, mode) {
    if(mode & 1) value = __webpack_require__(value);
    if(mode & 8) return value;
    if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
    var ns = Object.create(null);
    __webpack_require__.r(ns);
    Object.defineProperty(ns, 'default', { enumerable: true, value: value });
    if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
    return ns;
  };

  // getDefaultExport function for compatibility with non-harmony modules
  __webpack_require__.n = function(module) {
    var getter = module && module.__esModule ?
      function getDefault() { return module['default']; } :
      function getModuleExports() { return module; };
    __webpack_require__.d(getter, 'a', getter);
    return getter;
  };

  // Object.prototype.hasOwnProperty.call
  __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };

  // __webpack_public_path__
  __webpack_require__.p = "";

  // on error function for async loading
  __webpack_require__.oe = function(err) { console.error(err); throw err; };
  //获取老的数组 或声明一个空数组,等懒加载的chunk回来后就会调用这个方法
  //挂载到全局是为了方便在其它文件中调用
  var jsonpArray=window["webpackJsonp"]=window["webpackJsonp"]||[];
  //绑定this指针
  var oldJsonpFunction=jsonpArray.push.bind(jsonpArray);
  //让它的push方法等于json回调,回调函数里面也会调用老的数组的push方法
  jsonpArray.push=webpackJsonpCallback;
  //再浅克隆一个数组,这个一般是用来防止并发的,只取老数组里的元素
  jsonpArray=jsonpArray.slice();
  //循环jsonArray,都传给webpackJsonpCallback,让回调都执行了
  for (var i=0;i<jsonpArray.length;i++) webpackJsonpCallback(jsonpArray[i]);
  //把加载到的数据赋给parentJsonpFunction
  var parentJsonpFunction = oldJsonpFunction;

  // Load entry module and return exports
  return __webpack_require__(__webpack_require__.s = "./src/app.js");
})
({

"./src/app.js":
(function(module, exports, __webpack_require__) {
eval(`let play = document.getElementById('play');\nplay.addEventListener('click',() => {\n\t__webpack_require__.e(/*! import() */ 0).then(__webpack_require__.bind(null, /*! ./video.js */ \"./src/video.js\")).then(video => {\n\t\tlet name=video.getName();\n\t\tconsole.log(name);\n\t});\n});`);
})
});

5.2.2 0.bundle.js [#](#t125.2.2 0.bundle.js)

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{
 "./src/video.js":
 (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"getName\", function() { return getName; });\nfunction getName() {\n\treturn 'zxmf';\n}\n\n//# sourceURL=webpack:///./src/video.js?");
 })
}]);

Gitalking ...

Markdown is supported

Be the first guy leaving a comment!