webpack 4.X - 构建流程

223 阅读5分钟

简单的事情重复做,困难的事情坚持做! ^_^

webpack

  • webpack 模块化管理工具和打包工具。通过 loader 的对任何形式的资源转换,任何形式的资源都可视为模块,比如:CommonJS 模块、AMD 模块、ES6 模块、CSS、图片等都可视为模块。它可将许多松散的模块安按照依赖和规则打包成符合生产环境部署的前端资源。
  • 可将按需加载的模块进行代码分割,实际需要使用的时候再异步加载。
  • 定位是模块打包器,对于 老牌的 Grunt/Gulp 是属于构建工具。而且 webpack 可以代替 Gulp/Grunt 的一些功能,还可以配合使用。

优点

  • 可以模块化打包多种资源
  • 适配多种模块的系统
  • 非常适合单页面(SPA)应用的开发,多页面配置也可以

缺点

  • 配置相对复杂,不过 webpack4.X 以后相对好一些
  • 通过 babel 编译后的 js 代码打包体积相对比较大

认识 Loader 和 Plugin

  • Loader - 加载器。webpack 将一切文件视为模块,由于 webpack 的原生只解析 js 文件,所以如果需要其他类型的文件(图片,字体图标等)资源文件,就需要 loader,这样就可以让 webpack 拥有了加载和解析非 javascript 文件的能力。
  • Plugin - 插件。Plugin 可以扩展 webpack 的功能,让 webpack 具有更多的灵活性。在 webpack 运行的生命周期中广播出许多事件,Plugin 可监听这些事件,在合适的时机通过 webpack 提供的 API 改变输出结果。

webpack 构建流程

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并设置的参数,得出最终的参数。
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译,确定入口:根据配置文件中 entry 指定的找出入口文件。
  3. 编译模块:从入口文件触发,调用所有的 Loader 对模块进行编译,再找出该模块中所依赖的模块,递归本次步骤直到所有的入口文件中所依赖的文件都经过本步骤处理。
  4. 完成模块编译:经过第 3 步骤中 loader 编译完所有模块后,得到每个模块编译后的最终内容以及他们之间的依赖关系输出资源,更具入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这里是可以修改输出内容的最后操作。
  5. 输出完成:确定好输出内容后,根据配置确定输出的路径和文件名,把打包后文件内容写入到文件系统。

模拟工作流程

/**
 * webpack 工作流程
 * 1. 初始化参数:从配置文件和 Shell 语句中读取与合并设置的参数,得出最终的参数。
 * 2. 开始编译:用上一步得到的参数初始化 Compiler 对象,
 *  2.1 加载所有配置的插件:执行对象的 run 方法开始执行编译;
 *  2.2 确定入口:根据配置文件中 entry 指定的找出入口文件。
 * 3. 编译模块:从入口文件触发,调用所有的 Loader 对模块进行编译,再找出该模块中所依赖的模块,递归本次步骤直到所有的入口文件中所依赖的文件都经过本步骤处理。
 * 4. 完成模块编译:经过第 3 步骤中 loader 编译完所有模块后,得到每个模块编译后的最终内容以及他们之间的依赖关系输出资源,更具入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这里是可以修改输出内容的最后操作。
 * 5. 输出完成:确定好输出内容后,根据配置确定输出的路径和文件名,把打包后文件内容写入到文件系统。
 */
const { SyncHook } = require('tapable');
class Compiler {
  constructor(options) {
    this.options = options;
    this.hooks = {
      run: new SyncHook(),
      done: new SyncHook()
    }
  }
  run() {
    const modules = []; // 存放所有的模块
    const chunks = []; // 存放所有的代码快
    const files = [];
    this.hooks.run.call(); // 触发 run 钩子函数执行
    // 2.2 根据配置文件中 entry 指定的找出入口文件
    const entry = path.join(this.options.context, this.options.entry);
​
    // 3. 编译模块
    // 3.1 递归本次步骤
    // 读取入口文件内容 const name = 'tom';
    const entryContent = fs.readFileSync(entry, 'utf8');
​
    // 通过 loader 转换文件内容
    // 如:js 通过 babel-loader 转换
    const babelLoadr = (source) => {
      // 对源文件内容的编译转换,
      /*
      源码:
      const { title } = require('./title.js')
      console.log('123456', title)
      */
​
      // 返回通过 loader 编译后的内容
      return ``;
    }
​
    // 这个部分需要不断地处理文件中的 import require 的依赖。不断提取
    const entrySource = babelLoadr(entryContent);
    const entryModule = {
      id: entry,
      source: entrySource
    }
    modules.push(entryModule);
​
    // 4. 完成模块编译
    // 4.1 根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk
    const chunk = {
      name: 'main', // 默认 main 名称
      modules
    };
    chunks.push(chunk);
​
    // 5. 输出完成
    // 5.1 根据配置确定输出的路径和文件名
    const file = {
      file: this.options.output,
      // 打包后的源码
      source: `(() => {
        var __webpack_modules__ = ({
          "./src/title.js":
            ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
              "use strict";
              __webpack_require__.r(__webpack_exports__);
              __webpack_require__.d(__webpack_exports__, {
                "title": () => (title)
              });
              var title = 'title';
            })
        });
        var __webpack_module_cache__ = {};
        function __webpack_require__(moduleId) {
          var cachedModule = __webpack_module_cache__[moduleId];
          if (cachedModule !== undefined) {
            return cachedModule.exports;
          }
          var module = __webpack_module_cache__[moduleId] = {
            exports: {}
          };
          __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
          return module.exports;
        }
        (() => {
          // ./src/main.js
          var _require = __webpack_require__("./src/title.js"),
            title = _require.title;
      
          console.log('123456', title);
        })();
      })();`
    }
    files.push(file);
    // 5.2 把打包后文件内容写入到文件系统
    const outputPath = path.join(this.options.output.path, this.options.output.filename);
    fs.writeFileSync(outputPath, file.source);
​
    // 完成
    this.hooks.done.call();
  }
}
// 1. 初始化参数
const options = require('./webpack.config.js');
// 2. 开始编译
const compiler = new Compiler(options);
// 2.1 加载所有的配置插件
if (options.plugins && Array.isArray(options.plugins)) {
  for (const plugin of options.plugins) {
    plugin.apply(); //执行插件中的 apply 方法
  }
}
// 2.2 确定入口
compiler.run();