webpack4主流程源码阅读,以及动手实现一个简单的webpack(二)

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: webpack4主流程源码阅读,以及动手实现一个简单的webpack

Compiler 和 Compilation 的区别


webpack 打包离不开 CompilerCompilation,它们两个分工明确,理解它们是我们理清 webpack 构建流程重要的一步。

Compiler 负责监听文件和启动编译 它可以读取到 webpack 的 config 信息,整个 Webpack 从启动到关闭的生命周期,一般只有一个 Compiler 实例,整个生命周期里暴露了很多方法,常见的 run,make,compile,finish,seal,emit 等,我们写的插件就是作用在这些暴露方法的 hook 上

Compilation 负责构建编译。每一次编译(文件只要发生变化,)就会生成一个 Compilation 实例,Compilation 可以读取到当前的模块资源,编译生成资源,变化的文件,以及依赖跟踪等状态信息。同时也提供很多事件回调给插件进行拓展。


完成编译


  1. 输出资源:(seal 阶段)

在编译完成后,调用 compilation.seal 方法封闭,生成资源,这些资源保存在 compilation.assets, compilation.chunk,然后便会调用 emit 钩子,根据 webpack config 文件的 output 配置的 path 属性,将文件输出到指定的 path.

  1. 输出完成:done/failed 阶段

done 成功完成一次完成的编译和输出流程。failed 编译失败,可以在本事件中获取到具体的错误原因 在确定好输出内容后, 根据配置确定输出的路径和文件名, 把文件内容写入到文件系统。

emitAssets(compilation, callback) {
    const emitFiles = (err) => {
      //...省略一系列代码
      // afterEmit:文件已经写入磁盘完成
      this.hooks.afterEmit.callAsync(compilation, (err) => {
        if (err) return callback(err)
        return callback()
      })
    }
    // emit 事件发生时,可以读取到最终输出的资源、代码块、模块及其依赖,并进行修改(这是最后一次修改最终文件的机会)
    this.hooks.emit.callAsync(compilation, (err) => {
      if (err) return callback(err)
      outputPath = compilation.getPath(this.outputPath, {})
      mkdirp(this.outputFileSystem, outputPath, emitFiles)
    })
  }

然后,我们来看一下 webpack 打包好的代码是什么样子的。


webpack 输出文件代码分析


未压缩的 bundle.js 文件结构一般如下:

;(function (modules) {
  // webpackBootstrap
  // 缓存 __webpack_require__ 函数加载过的模块,提升性能,
  var installedModules = {}
  /**
   * Webpack 加载函数,用来加载 webpack 定义的模块
   * @param {String} moduleId 模块 ID,一般为模块的源码路径,如 "./src/index.js"
   * @returns {Object} exports 导出对象
   */
  function __webpack_require__(moduleId) {
    // 重复加载则利用缓存,有则直接从缓存中取得
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports
    }
    // 如果是第一次加载,则初始化模块对象,并缓存
    var module = (installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {},
    })
    // 把要加载的模块内容,挂载到module.exports上
    modules[moduleId].call(
      module.exports,
      module,
      module.exports,
      __webpack_require__
    )
    module.l = true // 标记为已加载
    // 返回加载的模块,直接调用即可
    return module.exports
  }
  // 在 __webpack_require__ 函数对象上挂载一些变量及函数 ...
  __webpack_require__.m = modules
  __webpack_require__.c = installedModules
  __webpack_require__.d = function (exports, name, getter) {
    if (!__webpack_require__.o(exports, name)) {
      Object.defineProperty(exports, name, {
        enumerable: true,
        get: getter,
      })
    }
  }
  __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
  }
  // __webpack_require__对象下的r函数
  // 在module.exports上定义__esModule为true,表明是一个模块对象
  __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/index.js'))
})({
  './src/index.js': function (
    module,
    __webpack_exports__,
    __webpack_require__
  ) {
    'use strict'
    eval(
      '__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _moduleA__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./moduleA */ "./src/moduleA.js");\n/* harmony import */ var _moduleA__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_moduleA__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var _moduleB__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./moduleB */ "./src/moduleB.js");\n/* harmony import */ var _moduleB__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_moduleB__WEBPACK_IMPORTED_MODULE_1__);\n\n\n\n//# sourceURL=webpack:///./src/index.js?'
    )
  },
  './src/moduleA.js': function (module, exports) {
    eval('console.log("moduleA")\n\n//# sourceURL=webpack:///./src/moduleA.js?')
  },
  './src/moduleB.js': function (module, exports) {
    // 代码字符串可以通过eval 函数运行
    eval('console.log("moduleB")\n\n//# sourceURL=webpack:///./src/moduleB.js?')
  },
})

上述代码的实现了⼀个 webpack_require 来实现⾃⼰的模块化把代码都缓存在 installedModules ⾥,代码⽂件以对象传递进来,key 是路径,value 是包裹的代码字符串,并且代码内部的 require,都被替换成了 webpack_require,代码字符串可以通过 eval 函数去执行。

bundle.js 能直接运行在浏览器中的原因在于输出的文件中通过 webpack_require 函数定义了一个可以在浏览器中执行的加载函数来模拟 Node.js 中的 require 语句。

总结一下,生成的 bundle.js只包含一个立即调用函数(IIFE),这个函数会接受一个对象为参数,它其实主要做了两件事:

  1. 定义一个模块加载函数 webpack_require。
  2. 使用加载函数加载入口模块 "./src/index.js",从入口文件开始递归解析依赖,在解析的过程中,分别对不同的模块进行处理,返回模块的 exports

所以我们只需要实现 2 个功能就可以实现一个简单的仿 webpack 打包 js 的编译工具

  1. 从入口开始递归分析依赖
  2. 借助依赖图谱来生成真正能在浏览器上运行的代码


实现一个简单的 webpack


接下来从 0 开始实践一个 Webpack 的雏形,能够让大家更加深入了解 Webpack

const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser') //解析成ast
const traverse = require('@babel/traverse').default //遍历ast
const { transformFromAst } = require('@babel/core') //ES6转换ES5
module.exports = class Webpack {
  constructor(options) {
    const { entry, output } = options
    this.entry = entry
    this.output = output
    this.modulesArr = []
  }
  run() {
    const info = this.build(this.entry)
    this.modulesArr.push(info)
    for (let i = 0; i < this.modulesArr.length; i++) {
      // 判断有依赖对象,递归解析所有依赖项
      const item = this.modulesArr[i]
      const { dependencies } = item
      if (dependencies) {
        for (let j in dependencies) {
          this.modulesArr.push(this.build(dependencies[j]))
        }
      }
    }
    //数组结构转换
    const obj = {}
    this.modulesArr.forEach((item) => {
      obj[item.entryFile] = {
        dependencies: item.dependencies,
        code: item.code,
      }
    })
    this.emitFile(obj)
  }
  build(entryFile) {
    const conts = fs.readFileSync(entryFile, 'utf-8')
    const ast = parser.parse(conts, {
      sourceType: 'module',
    })
    //  console.log(ast)
    // 遍历所有的 import 模块,存入dependecies
    const dependencies = {}
    traverse(ast, {
      //  类型为 ImportDeclaration 的 AST 节点,
      // 其实就是我们的 import xxx from xxxx
      ImportDeclaration({ node }) {
        const newPath =
          './' + path.join(path.dirname(entryFile), node.source.value)
        dependencies[node.source.value] = newPath
        // console.log(dependencies)
      },
    })
    // 将转化后 ast 的代码重新转化成代码
    // 并通过配置 @babel/preset-env 预置插件编译成 es5
    const { code } = transformFromAst(ast, null, {
      presets: ['@babel/preset-env'],
    })
    return {
      entryFile,
      dependencies,
      code,
    }
  }
  emitFile(code) {
    //生成bundle.js
    const filePath = path.join(this.output.path, this.output.filename)
    const newCode = JSON.stringify(code)
    const bundle = `(function(modules){
           // moduleId 为传入的 filename ,即模块的唯一标识符
            function require(moduleId){
                function localRequire(relativePath){
                   return require(modules[moduleId].dependencies[relativePath]) 
                }
                var exports = {};
                (function(require,exports,code){
                    eval(code)
                })(localRequire,exports,modules[moduleId].code)
                return exports;
            }
            require('${this.entry}')
        })(${newCode})`
    fs.writeFileSync(filePath, bundle, 'utf-8')
  }
}

调用

let path = require('path')
let { resolve } = path
let webpackConfig = require(path.resolve('webpack.config.js'))
let Webpack = require('./myWebpack.js')
const defaultConfig = {
  entry: 'src/index.js',
  output: {
    path: resolve(__dirname, '../dist'),
    filename: 'bundle.js',
  },
}
const config = {
  ...defaultConfig,
  ...webpackConfig,
}
const options = require('./webpack.config')
new Webpack(options).run()

输入到浏览器看一下执行结果8df5ecd6c094bf4f36ec61655355ad64_640_wx_fmt=jpeg&wxfrom=5&wx_lazy=1&wx_co=1.jpg

参考:

  1. 快狗打车:实现一个简单的 webpack
  2. tapable 详解+webpack 流程
  3. 本文调试以及打断点用到的源码

目录
相关文章
|
缓存 资源调度 编译器
原来是这样啊!浅谈webpack4和webpack5的区别
相对于webpack4,webpack5内置了很多plugin插件,比如、打包、压缩、缓存
772 1
|
1月前
|
缓存 前端开发 JavaScript
Webpack 4 和 Webpack 5 区别?
【10月更文挑战第23天】随着时间的推移,Webpack 可能会继续发展和演进,未来的版本可能会带来更多的新特性和改进。保持对技术发展的关注和学习,将有助于我们更好地应对不断变化的前端开发环境。
|
7月前
|
前端开发 JavaScript 安全
【网络安全】WebPack源码(前端源码)泄露 + jsmap文件还原
【网络安全】WebPack源码(前端源码)泄露 + jsmap文件还原
1134 0
|
Shell
webpack4主流程源码阅读,以及动手实现一个简单的webpack(一)
webpack4主流程源码阅读,以及动手实现一个简单的webpack
65 3
webpack4主流程源码阅读,以及动手实现一个简单的webpack(一)
|
JavaScript 前端开发 API
webpack的几个常见loader源码浅析,以及动手实现一个md2html-loader(二)
webpack的几个常见loader源码浅析,以及动手实现一个md2html-loader
124 0
webpack的几个常见loader源码浅析,以及动手实现一个md2html-loader(二)
|
缓存 前端开发 JavaScript
webpack的几个常见loader源码浅析,以及动手实现一个md2html-loader(一)
webpack的几个常见loader源码浅析,以及动手实现一个md2html-loader
122 0
|
缓存
webpack原理篇(五十二):webpack-cli源码阅读
webpack原理篇(五十二):webpack-cli源码阅读
151 0
webpack原理篇(五十二):webpack-cli源码阅读
|
开发工具 git Windows
webpack 4升级到 webpack 5 (node 14.6 升级到 node16 引发的问题)
第一次启动项目报错,报错内容是 Node Sass does not yet support your current environment: Windows 64-bit, 这个问题相信大家都清楚, node-sass 出问题了
webpack 4升级到 webpack 5 (node 14.6 升级到 node16 引发的问题)
|
3月前
|
JavaScript
webpack打包TS
webpack打包TS
138 60
|
2月前
|
缓存 前端开发 JavaScript
Webpack 打包的基本原理
【10月更文挑战第5天】