webpack 加载动态路径处理方式

背景

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

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

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

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

这是我的源代码:

const componentSchema = require(join(tempDIR, 'componentSchema.js'))

打包后这一行变成了:

const componentSchema = __webpack_require__(2868)(path_1.join(tempDIR, 'componentSchema.js'))

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

/* 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

// 单个目标
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 模式的模块都将成为同一个主块的一部分

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

const componentSchema = eval('require')(join(tempDIR, 'componentSchema.js'))

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

// 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. https://zhuanlan.zhihu.com/p/52990313

  2. https://juejin.cn/post/7083676342872440845在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值