webpack 加载动态路径处理方式

背景

项目开发中使用了 webpack 打包,其中有一段逻辑是:

  • 运行时,将用户上传的压缩包解压放在服务器目录
  • 从解压后的目录,动态获取文件内容并返回

本地运行这段逻辑并没有什么问题,会在本机电脑创建目录,解压文件放在对应目录,也能正确读取文件并返回。但是到了线上发现一直报错,无法读取文件。

手动查看服务器对应目录,文件明明是存在的呀,到底是为什么呢,经过一系列排查,发现是 webpack 打包的问题。

这是我的源代码:

csharp 复制代码
 const componentSchema = require(join(tempDIR, 'componentSchema.js'))

打包后这一行变成了:

csharp 复制代码
 const componentSchema = __webpack_require__(2868)(path_1.join(tempDIR, 'componentSchema.js'))

重点就是 __webpack_require__(2868) 到底是什么东西呢。发现打包后的产物里,这个 2868 对应的是以下方法:

ini 复制代码
 /* 2868 */
 /***/ ((module) => {
 ​
 function webpackEmptyContext(req) {
   var e = new Error("Cannot find module '" + req + "'");
   e.code = 'MODULE_NOT_FOUND';
   throw e;
 }
 webpackEmptyContext.keys = () => ([]);
 webpackEmptyContext.resolve = webpackEmptyContext;
 webpackEmptyContext.id = 2868;

问题原因找到了,原来 webpack 在编译时,无法解析完全动态的路径,即需要在运行时才指定的路径,所以就会使用以上的 webpackEmptyContext 函数替换了原始的 require,导致打包后的产物部署后,根本不可能去动态获取文件,直接就报错了,气!!!!!!

基于以上问题,所以完整梳理一下 webpack 动态参数的处理办法

部分动态 require

部分动态路径,例如以一段路径开头进行加载,例如: require('./animals/' + dynamicFile + '.js')。webpack 遇到这种情况会自动推断资源路径,打包所有关联的文件

这种动态 require 需要文件已经在源代码中存在,只是由于性能或其他考虑需要动态获取。

这里需要首先介绍一下 webpackMagic Comments

ruby 复制代码
 // 单个目标
 import(
   /* webpackChunkName: "my-chunk-name" */
   /* webpackMode: "lazy" */
   /* webpackExports: ["default", "named"] */
   'module'
 );
 ​
 // 多个可能的目标
 import(
   /* webpackInclude: /.json$/ */
   /* webpackExclude: /.noimport.json$/ */
   /* webpackChunkName: "my-chunk-name" */
   /* webpackMode: "lazy" */
   /* webpackPrefetch: true */
   /* webpackPreload: true */
   `./locale/${language}`
 );

本文暂时先只介绍 webpackChunkNamewebpackMode 的用法。以下是 webpakc 官网介绍:

webpackChunkName: 新 chunk 的名称。 从 webpack 2.6.0 开始,占位符 [index][request] 分别支持递增的数字或实际的解析文件名。 添加此注释后,将单独的给我们的 chunk 命名为 [my-chunk-name].js 而不是 [id].js。

webpackMode:从 webpack 2.6.0 开始,可以指定以不同的模式解析动态导入。支持以下选项:

  • 'lazy' (默认值):为每个 import() 导入的模块生成一个可延迟加载(lazy-loadable)的 chunk。
  • 'lazy-once':生成一个可以满足所有 import() 调用的单个可延迟加载(lazy-loadable)的 chunk。此 chunk 将在第一次 import() 时调用时获取,随后的 import() 则使用相同的网络响应。注意,这种模式仅在部分动态语句中有意义,例如 import(./locales/${language}.json),其中可能含有多个被请求的模块路径。
  • 'eager':不会生成额外的 chunk。所有的模块都被当前的 chunk 引入,并且没有额外的网络请求。但是仍会返回一个 resolved 状态的 Promise。与静态导入相比,在调用 import() 完成之前,该模块不会被执行。
  • 'weak':尝试加载模块,如果该模块函数已经以其他方式加载,(即另一个 chunk 导入过此模块,或包含模块的脚本被加载)。仍会返回 Promise, 但是只有在客户端上已经有该 chunk 时才会成功解析。如果该模块不可用,则返回 rejected 状态的 Promise,且网络请求永远都不会执行。当需要的 chunks 始终在(嵌入在页面中的)初始请求中手动提供,而不是在应用程序导航在最初没有提供的模块导入的情况下触发,这对于通用渲染(SSR)是非常有用的。

例如有一个名为 mainFolder 的目录,其中有各种文件

复制代码
 ├── mainFolder
 │   ├── file1.js
 │   ├── file2.js
 │   ├── file3.js
 ├── index.js

动态加载时使用 require(./mainFolder/${fileName}.js)

使用 lazy 模式,打包后产物为:

复制代码
 ├── dist
 │   ├── mainFolder0.js
 │   ├── mainFolder1.js
 │   ├── mainFolder2.js
 │   ├── index.js

使用 eager 模式时,不会创建任何额外的块,所有匹配 import 模式的模块都将成为同一个主块的一部分

bash 复制代码
 import(/* webpackChunkName: 'mainFolder',webpackMode: 'eager'  */ `./mainFolder/${fileName}.js`)
复制代码
 ├── dist
 │   ├── index.js

完全动态 require

完全动态的路径,例如在运行时(如环境变量或 cwd 指定)决定加载路径。也就是文章开头我遇到的问题。

webpack 在编译时,无法解析完全动态的路径,即需要在运行时才指定的路径,所以就会使用以上的 webpackEmptyContext 函数替换了原始的 require,导致打包后的产物部署后,根本不可能去动态获取文件。

这种情况无法直接绕过。完全动态的 require 不会经过 IgnorePlugin 或 externals 的过滤(因为它没有任何的 context)。

查阅资料,发现有3种解决方案

1、使用 evalnew Function 等方式让 webpack 无法识别 require ,从而绕过将它替换为webpackEmptyContext

bash 复制代码
 const componentSchema = eval('require')(join(tempDIR, 'componentSchema.js'))

2、使用在 @zeit/ncc 中发现的解决方案(ncc 用以将任意 npm 包打包可运行的单文件),改变 webpack 的行为仍生成一个原始的 require 语句 (大佬写的,我还没尝试过呢)

javascript 复制代码
 // webpack config
 module.exports = {
   // ...
   plugins: [fixModuleNotFound],
 }
 ​
 // ncc master,webpack#next(未发布的 webpack 5.0-alpha)
 // https://github.com/zeit/ncc/blob/c2fb87e0c0/src/index.js#L147-L182
 // 如果是 webpack 4,使用:
 // https://github.com/zeit/ncc/blob/c289b28ff8/src/index.js#L145-L173
 const fixRequireNotFound = {
   apply() {
     // override "not found" context to try built require first
     compiler.hooks.compilation.tap('ncc', compilation => {
       compilation.moduleTemplates.javascript.hooks.render.tap(
         'ncc',
         (moduleSourcePostModule, module, options, dependencyTemplates) => {
           // hack to ensure __webpack_require__ is added to empty context wrapper
           const getModuleRuntimeRequirements =
             compilation.chunkGraph.getModuleRuntimeRequirements
           compilation.chunkGraph.getModuleRuntimeRequirements = function(
             module
           ) {
             const runtimeRequirements = getModuleRuntimeRequirements.apply(
               this,
               arguments
             )
             if (module._contextDependencies)
               runtimeRequirements.add('__webpack_require__')
             return runtimeRequirements
           }
           if (
             module._contextDependencies &&
             moduleSourcePostModule._value.match(
               /webpackEmptyAsyncContext|webpackEmptyContext/
             )
           ) {
             return moduleSourcePostModule._value.replace(
               'var e = new Error',
               `if (typeof req === 'number')\n` +
                 `  return __webpack_require__(req);\n` +
                 `try { return require(req) }\n` +
                 `catch (e) { if (e.code !== 'MODULE_NOT_FOUND') throw e }\n` +
                 `var e = new Error`
             )
           }
         }
       )
     })
   },
 }

3、使用 __non_webpack_require__,不知道为什么,在我的项目里,一直报这个方法未定义,没找到正确的使用方法,哭!!!!等我后面有空再研究研究

refer

  1. zhuanlan.zhihu.com/p/52990313
  1. juejin.cn/post/708367...
相关推荐
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60617 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了7 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅7 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅8 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment8 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅9 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊9 小时前
jwt介绍
前端
爱敲代码的小鱼9 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax