前端面试宝典---webpack原理解析,并有简化版源码

前言

先看一下webpack打包后的bundle.js,前边的直接扫一眼就过,可以发现这个立即执行函数的形参就是一个,key为引入文件路径,value为该模块代码的函数。

所以比较重要的就是通过webpack的配置文件中的entry的入口文件,递归去生成这个modules,并把代码中require变成__webpack_require__。

javascript 复制代码
(function (modules) { // webpackBootstrap
  // The module cache
  var installedModules = {}

  // The require function
  function __webpack_require__ (moduleId) {

    // Check if module is in cache
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports

    }
    // Create a new module (and put it into the cache)
    var module = installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {}

    }

    // Execute the module function
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__)

    // Flag the module as loaded
    module.l = true

    // Return the exports of the module
    return module.exports

  }


  // 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 = ""


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

})
  ({

    "./src/app.js":
      (function (module, __webpack_exports__, __webpack_require__) {

        "use strict"
        __webpack_require__.r(__webpack_exports__)
        var _module__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./module */ "./src/module.js")
        var _module__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_module__WEBPACK_IMPORTED_MODULE_0__)

        console.log("Hello World")



      }),

    "./src/module.js":
      (function (module, exports) {

        module.exports = {
          name: 'module',
          description: 'module description',
          version: '1.0.0',
          dependencies: {
            'module-a': '1.0.0',
            'module-b': '1.0.0',
          },
          devDependencies: {
            'module-c': '1.0.0',
            'module-d': '1.0.0',
          },
        }


      })


  });
//# sourceMappingURL=bundle.js.map

实现思路

项目配置如下

  1. 读取webpack.config.js文件

  2. 对入口文件实现编译生成依赖对象modules

    2.1 根据入口文件递归去获取依赖及其代码,并通过ast抽象语法书,对require替换成__webpack_require__

    2.2 复制webpack打包生成打的bundles.js 将其改造成模板文件(bundlejsTemplate.ejs),通过ejs,把modules 插入模板中,生成代码

  3. 将替换后的模板代码生成到webpack.config.js配置的output路径下

具体实现

index.js

javascript 复制代码
#! /usr/bin/env node
/*
* 实现 webpack 打包功能
* 1. 配置文件的读取 webpack.config.js
*
* 2. 实现入口文件的编译,然后生成依赖对象 modules
*
*
* */
// console.log('jdpack打包功能');
// console.log(process.cwd()); // 打印当前命令所处的目录

/*
* 1. 配置文件的读取 webpack.config.js
* */
const Compiler = require('../lib/Compiler.js');

const path = require('path');
const configPath = path.resolve(process.cwd(), 'webpack.config.js');
const configObj = require(configPath);

// console.log(configObj); // 配置文件对象

/*
* 2. 实现入口文件的编译,然后生成依赖对象 modules
* */
const compile = new Compiler(configObj);
compile.run();
// console.log(compile.modules); // 模块依赖对象

Compiler.js (最重要的实现都在这个类里)

javascript 复制代码
/*
* 编译我们的代码,生成 打包后的内容
* 1. 根据配置文件 entry 入口文件,读取入口文件对象的代码
* 2. 生成依赖对象
* 3. 传递给 webpack的 自执行的匿名函数
*
* */
const fs = require('fs');
const ejs = require('ejs');
const path = require('path');

/*
* 导入ast相关的模块
* */
const {parse} = require('@babel/parser');
const generator = require('@babel/generator').default;
const traverse = require('@babel/traverse').default;
const t = require('@babel/types');


class Compiler {
    /*
    * 配置文件
    * */
    constructor(config) {
        this.config = config;
        // 模块依赖对象,保存了代码里面的所有的模块依赖关系 key 依赖模块的路径 value 依赖模块对应代码的函数
        this.modules = {};
    }

    run() {
        // 1. 根据配置文件的入口文件生成依赖对象
        this.buildModules(this.config.entry);

        // 编译后,生成 bundle.js
        this.generatorBundlejs();


    }

    /*
    * moduleId 依赖模块的路径
    *
    * 如果在 代码里面有 require其他的模块代码
    * 1. 先生成模块依赖对象
    * 2. 将代码里面的 require 替换为 __webpack_require__ ast 实现
    * 3. 将依赖模块的路径加载入口文件的目录
    *
    * */
    buildModules(moduleId) {
        let code = this.getCode(moduleId);
        let {deps, newCode} = this.parseModule(code);
        // console.log(deps, newCode);
        this.modules[moduleId] = newCode;

        // 针对 deps 里面再次做处理,引入依赖的文件里面还有可能 require
        deps.forEach(item => {
            this.buildModules(item);
        })
    }
    /*
    * path 依赖模块的路径
    * */
    getCode(modulePath) {
        return fs.readFileSync(modulePath, 'utf8');
    }

    /*
    * 将代码里面的依赖做替换
    * */
    parseModule(code) {

        let mainRootPath = path.dirname(this.config.entry);
        // 存储了代码里面所有的依赖路径
        let deps = [];

        const ast = parse(code);
        /*
        * 1. 对 require 节点做处理,替换 __webpack_require__
        * */

        traverse(ast, {
            CallExpression(NodePath) {
                let node = NodePath.node;

                if (node.callee.name === 'require') {

                    node.callee.name = '__webpack_require__';

                    // 2. 对依赖路径做替换
                    let depPath = node.arguments[0].value;
                    depPath = '.\\' + path.join(mainRootPath, depPath);
                    depPath = depPath.replace(/\\/g, '/');

                    // 利用语法树将 require 里面依赖路径做修改
                    node.arguments[0] = t.stringLiteral(depPath);

                    deps.push(depPath);

                }
            }
        });

        let newCode = generator(ast).code;

        // console.log(newCode);

        return {deps, newCode};


    }

    /*
    * 先根据生成的入口文件的依赖对象,生成打包文件。然后在 分析入口文件里面的内容,如果有其他的 require 进行再次生成依赖对象,在生成打包的文件
    * */
    generatorBundlejs() {

        /*
        * 使用 ejs 根据依赖对象,生成打包后的 bundle.js 文件
        * 1. 读取模板
        *
        *
        * */
        let bundlePath = path.resolve(__dirname, 'bundlejsTemplate.ejs');
        let bundleTemplate = fs.readFileSync(bundlePath, 'utf-8');

        /*
        * 2. 使用 ejs 做模板的替换
        * */
        let renderCode = ejs.render(bundleTemplate, {moduleId: this.config.entry, modules: this.modules});

        /*
        * 3. 将打包后的内容根据 webpack.config.js 里面的 output 进行保存
        * */
        let outputPath = this.config.output.path;
        // 判断打包后的输出目录是否存在,如果不存在,则先创建目录
        if (!fs.existsSync(outputPath)) {
            fs.mkdirSync(outputPath);
        }

        let outputFilePath = path.resolve(outputPath, this.config.output.filename);

        fs.writeFileSync(outputFilePath, renderCode);

    }

}

module.exports = Compiler;

bundlejsTemplate.ejs

javascript 复制代码
(function(modules) { // webpackBootstrap
// The module cache
var installedModules = {};

// The require function
function __webpack_require__(moduleId) {

// Check if module is in cache
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};

// Execute the module function
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

// Flag the module as loaded
module.l = true;

// Return the exports of the module
return module.exports;
}


// 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 = "";


// Load entry module and return exports
return __webpack_require__(__webpack_require__.s = "<%- moduleId %>");
})
/************************************************************************/
({
<% for(let key in modules) { %>
    "<%- key %>": (function(module, exports, __webpack_require__) {

    <%- modules[key] %>

    }),
<% } %>

});

package.json

javascript 复制代码
{
  "name": "jdpack",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "bin": {
    "mypack": "./bin/index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@babel/generator": "^7.18.13",
    "@babel/parser": "^7.18.13",
    "@babel/traverse": "^7.18.13",
    "@babel/types": "^7.18.13",
    "ejs": "^3.1.8"
  }
}
相关推荐
AI Echoes16 分钟前
大模型(LLMs)加速篇
人工智能·python·算法·机器学习·面试
Deepsleep.3 小时前
东田数码科技前端面经
前端·科技·面试
涵信4 小时前
第十八节:开放性问题-Vue生态未来趋势
前端·vue.js·devops
LeonNo114 小时前
面试的各种类型
面试·职场和发展
牧杉-惊蛰4 小时前
css 数字从0开始增加的动画效果
前端·javascript·css
孤灯淡茶4 小时前
Fiori学习专题十五:Nested Views
前端·javascript·学习
green_pine_5 小时前
CSS学习笔记14——移动端相关知识(rem,媒体查询,less)
前端·css·笔记·学习·less
Monly215 小时前
Vue:el-table-tree懒加载数据
前端·javascript·vue.js
进取星辰5 小时前
16、路由守卫:设置魔法结界——React 19 React Router
前端·javascript·react.js
清羽_ls6 小时前
cURL 入门:10 分钟学会用命令行发 HTTP 请求
前端·curl·命令行工具