背景
项目开发中使用了 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
需要文件已经在源代码中存在,只是由于性能或其他考虑需要动态获取。
这里需要首先介绍一下 webpack
的 Magic 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}`
);
本文暂时先只介绍 webpackChunkName
和 webpackMode
的用法。以下是 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、使用 eval
或 new 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__
,不知道为什么,在我的项目里,一直报这个方法未定义,没找到正确的使用方法,哭!!!!等我后面有空再研究研究