资讯专栏INFORMATION COLUMN

深入理解webpack的require.context

lncwwn / 1642人阅读

摘要:可以做做不到的事情。看完这段代码就不难理解文档中所说的会返回一个带有个的函数了。进阶深入探究源码我们知道在版本后,在加载模块时,可以指定模块加载模式,我们能使用几种方式来控制我们要加载的模块。

前言

require.context 其实是一个非常实用的 api。但是 3-4 年过去了,却依旧还有很多人不知道如何使用。

而这个 api 主要为我们做什么样的事情?它可以帮助我们动态加载我们想要的文件,非常灵活和强大(可递归目录)。可以做 import 做不到的事情。今天就带大家一起来分析一下,webpack 的 require.context是如何实现的。

准备工作

在分析这个 api 之前呢,我们需要先了解一下一个最简单的文件,webpack 会编译成啥样。

-- src
    -- index.ts
// index.ts
console.log(123)

编译之后,我们可以看见 webpack 会编译成如下代码

// 源码 https://github.com/MeCKodo/require-context-sourece/blob/master/simple-dist/bundle-only-index.js
 (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, {
                 configurable: false,
                 enumerable: true,
                 get: getter
             });
         }
     };
     // define __esModule on exports
     __webpack_require__.r = function(exports) {
         Object.defineProperty(exports, "__esModule", { value: true });
     };
     // 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/index.ts");
 })
 ({
 "./src/index.ts": (function(module, exports) {
      console.log("123");
    })
 });

初次一看是很乱的,所以为了梳理结构,我帮大家去除一些跟本文无关紧要的。其实主要结构就是这样而已,代码不多为了之后的理解,一定要仔细看下每一行

// 源码地址 https://github.com/MeCKodo/require-context-sourece/blob/master/simple-dist/webpack-main.js

(function(modules) {
  // 缓存所有被加载过的模块(文件)
  var installedModules = {};
  // 模块(文件)加载器 moduleId 一般就是文件路径
  function __webpack_require__(moduleId) {
    // 走 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: {}
    });
    // 执行我们的模块(文件) 目前就是 ./src/index.ts 并且传入 3 个参数
    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;
  }
  // 开始加载入口文件
  return __webpack_require__((__webpack_require__.s = "./src/index.ts"));
})({
  "./src/index.ts": function(module, exports, __webpack_require__) {
    console.log("123");
  }
});

__webpack_require__ 就是一个模块加载器,而我们所有的模块都会以对象的形式被读取加载

modules = {
    "./src/index.ts": function(module, exports, __webpack_require__) {
       console.log("123");
    }
}

我们把这样的结构先暂时称之为 模块结构对象

正片

了解了主体结构之后我们就可以写一段require.context来看看效果。我们先新增 2 个 ts 文件并且修改一下我们的 index.ts,以便于测试我们的动态加载。

--- src
    --- demos
        --- demo1.ts
        --- demo2.ts
    index.ts
// index.ts
// 稍后我们通过源码分析为什么这样写
function importAll(contextLoader: __WebpackModuleApi.RequireContext) {
  contextLoader.keys().forEach(id => console.log(contextLoader(id)));
}

const contextLoader = require.context("./demos", true, /.ts/);
importAll(contextLoader);

查看我们编译后的源码,发现多了这样一块的 模块结构对象

// 编译后代码地址 https://github.com/MeCKodo/require-context-sourece/blob/master/simple-dist/contex-sync.js#L82-L113
{
"./src/demos sync recursive .ts": function(module, exports, __webpack_require__) {
  var map = {
    "./demo1.ts": "./src/demos/demo1.ts",
    "./demo2.ts": "./src/demos/demo2.ts"
  };

  // context 加载器,通过之前的模块加载器 加载模块(文件) 
  function webpackContext(req) {
    var id = webpackContextResolve(req);
    var module = __webpack_require__(id);
    return module;
  }
  
  // 通过 moduleId 查找模块(文件)真实路径
  // 个人在这不喜欢 webpack 内部的一些变量命名,moduleId 它都会编译为 request
  function webpackContextResolve(req) {
    // id 就是真实文件路径
    var id = map[req];
    // 说实话这波操作没看懂,目前猜测是 webpack 会编译成 0.js 1.js 这样的文件 如果找不到误加载就出个 error
    if (!(id + 1)) {
      // check for number or string
      var e = new Error("Cannot find module "" + req + "".");
      e.code = "MODULE_NOT_FOUND";
      throw e;
    }
    return id;
  }
  
  // 遍历得到所有 moduleId
  webpackContext.keys = function webpackContextKeys() {
    return Object.keys(map);
  };
  // 获取文件真实路径方法
  webpackContext.resolve = webpackContextResolve;
  // 该模块就是返回一个 context 加载器
  module.exports = webpackContext;
  // 该模块的 moduleId 用于 __webpack_require__ 模块加载器
  webpackContext.id = "./src/demos sync recursive .ts";
}

我在源码中写了很详细的注释。看完这段代码就不难理解文档中所说的require.context 会返回一个带有 3 个API的函数(webpackContext)了。

接着我们看看编译后 index.ts 的源码

"./src/index.ts": function(module, exports, __webpack_require__) {
  function importAll(contextLoader) {
    contextLoader.keys().forEach(function(id) {
      // 拿到所有 moduleId,在通过 context 加载器去加载每一个模块
      return console.log(contextLoader(id));
    });
  }
  var contextLoader = __webpack_require__(
    "./src/demos sync recursive .ts"
  );
  importAll(contextLoader);
}

很简单,可以发现 require.context 编译为了 __webpack_require__加载器并且加载了 id 为./src/demos sync recursive .ts 的模块,sync表明我们是同步加载这些模块(之后我们在介绍这个参数),recursive 表示需要递归目录查找。自此,我们就完全能明白 webpack 是如何构建所有模块并且动态加载的了。

进阶深入探究 webpack 源码

我们知道 webpack 在 2.6 版本后,在加载模块时,可以指定 webpackMode 模块加载模式,我们能使用几种方式来控制我们要加载的模块。常用的 mode一般为sync lazy lazy-once eager

所以在 require.context 是一样适用的,我们如果查看一下@types/webpack-env就不难发现它还有第四个参数。

简要来说

sync 直接打包到当前文件,同步加载并执行

lazy 延迟加载会分离出多带带的 chunk 文件

lazy-once 延迟加载会分离出多带带的 chunk 文件,加载过下次再加载直接读取内存里的代码。

eager 不会分离出多带带的 chunk 文件,但是会返回 promise,只有调用了 promise 才会执行代码,可以理解为先加载了代码,但是我们可以控制延迟执行这部分代码。

文档在这里 https://webpack.docschina.org...。

这部分文档很隐晦,也可能是文档组没有跟上,所以如果我们去看 webpack 的源码的话,可以发现真正其实是有 6 种 mode。

mode类型定义
https://github.com/webpack/we...

那 webpack 到底是如何做到可递归获取我们的文件呢?在刚刚上面的源码地址里我们能发现这样一行代码。

这一看就是去寻找我们所需要的模块。所以我们跟着这行查找具体的源码。

这就是 require.context 是如何加载到我们文件的具体逻辑了。其实就是fs.readdir而已。最后获取到文件之后在通过 context 加载器来生成我们的模块结构对象。比如这样的代码就是负责生成我们sync类型的context加载器。大家可以具体在看别的5种类型。

6种类型加载逻辑并且生成 context 加载器的模块结构对象 
https://github.com/webpack/we...
总结

1.学习了解 webpack 是如何组织加载一个模块的,webpack 的加载器如何运作,最后如何生成编译后的代码。

2.本来仅仅只是想了解 require.context 如何实现的,却发现了它第三个参数有 6 种mode,这部分却也是 webpack 文档上没有的。

3.从一个实用的 API 出发,探索了该 api 的实现原理,并且一起阅读了部分 webpack 源码。

4.探索本质远比你成为 API 的搬运工更重要。只有你不断地探寻本质,才可以发现世界的奥秘。

最后抛砖引玉,可以按照这样的思路再去学习另外 6 种 mode 编译后的代码。

文章里编译后的代码,都在这里 >>> https://github.com/MeCKodo/re...

个人网站 >>> http://www.meckodo.com

最后常年招人

厦门 RingCentral 外企,福利待遇厦门顶尖

5点半下班 5点半下班 5点半下班

有想法的加我微信 abcdefghijklmnoKodo

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/106037.html

相关文章

  • webpackrequire.context使用

    摘要:我们按照提供的思路,对路由按业务模块进行拆分。这时就可以使用到了打包入口文件自动引入子目录下所有文件参考分享使用的实现路由去中心化管理之用法的基础组件的自动化全局注册官方文档 概述 You can create your own context with the require.context() function. It allows you to pass in a directo...

    cyrils 评论0 收藏0
  • 【翻译向】webpack2 指南(上)

    摘要:原文发布与抹桥的博客翻译向指南上前置定义代码包代码块安装代码分割代码分割是中最引人注目的功能之一。回调函数一个回调函数会被执行一次当所有依赖都被加载以后。对象的实现作为一个参数传递给这个回调函数。 原文发布与 抹桥的博客 -【翻译向】webpack2 指南(上) 前置定义 Bundle 代码包Chunk 代码块 安装 npm install webpack --save-dev 代码分...

    史占广 评论0 收藏0
  • 基于webpack构建angular 1.x工程(angular篇)

    摘要:基于构建的工程篇上一篇基于构建的工程一篇里我们已经成功构建了整个项目的打包配置。不同的模块之间都会有标识来标志,所以不会说存在干扰和污染的问题。但是对于我这个重构的项目,就会有麻烦要改写的文件有太多了。然而还有一种情况。 基于webpack构建的angular 1.x工程(angular篇)   上一篇基于webpack构建的angular 1.x 工程(一)webpack篇里我们已经...

    jerry 评论0 收藏0
  • webpack2.x 中文文档 翻译 之 依赖管理 Dependency Management

    摘要:依赖管理该条已在中文网存在,点击这里表达式来调用当你的请求包含表达式,那个一个上下文环境将被创建。一个包含所有父文件夹和子及后代文件夹中以结尾的文件的上下文。一个函数,返回一个数组,包含上下文模块能够处理的所有的请求。 依赖管理 Dependency Management 该条已在webpack2.x中文网存在,点击这里 es6 modules commonjs amd 表达式...

    raledong 评论0 收藏0
  • Node.js 热更新(一)

    摘要:直到最近在使用微信机器人时,遇到了强烈的需求。增删文件后热更新上面的代码已经不小心实现了增加文件后热更新,因为表示检测的更新,如果增加一个,那么就变成,于是新模块不等于老模块不存在,从而使用注册事件监听器。 背景 刚思考这个话题的时候,首先想到的是 Vue 或 React 的组件热更新(基于 Webpack HMR),后来又想到了 Lua、Erlang 等语言的热更新,不过在实际开发 ...

    LancerComet 评论0 收藏0

发表评论

0条评论

lncwwn

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<