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...
相关推荐
hackeroink1 小时前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者3 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-3 小时前
验证码机制
前端·后端
燃先生._.4 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖5 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235245 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240256 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar6 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人7 小时前
前端知识补充—CSS
前端·css