如何在 webpack 和 Rollup 中忽略動態的 require
webpack
在 webpack 中有兩種常見的動態的 require:
- 部分動態,例如以一段路徑開頭進行載入
- 完全動態,例如在運行時(如環境變數或 cwd 指定)決定載入路徑
第一種情況可能會用來載入資源文件、語言文件或路由等等,如 require(./path-to-resource + name)
,webpack 遇到這種情況會自動推斷資源路徑,打包所有關聯的文件,如果編譯目標是 web 同時匹配了包含引用 Node 原生模塊的代碼,還可能需要屏蔽這些原生模塊的訪問(node: false)來防止出錯。在官網及 GoogleChromeLabs/webpack-libs-optimizations 中會特別提到使用 IgnorePlugin 或 ContextReplacementPlugin 來阻止或變更這個行為,防止使用 moment,date-fn 之類的庫時意外打包了所有的語言文件。
第二種情況無法推斷,當 webpack 編譯時會輸出一個「Critical dependency: the request of a dependency is an expression
」警告,它生成如下的 webpackEmptyContext
函數替換了原始的 require :
function webpackEmptyContext(req) {
var e = new Error("Cannot find module " + req + "");
e.code = MODULE_NOT_FOUND;
throw e;
}
無法繞過直接這個行為,完全動態的 require 不會經過 IgnorePlugin 或 externals 的過濾(因為它沒有任何的 context)。
通過 hack 繞過暫發現有兩種方式,其一讓 webpack 無法識別這個 require,如:
eval(require)(module)
另外可以使用在 @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`
)
}
}
)
})
},
}
Rollup
Rollup 不支持動態 require:
- 默認不處理,因為它本身僅做為 ESM 的 polyfill,無法直接 import CJS 文件(即使在 ESM 文件中寫
require
語句也會原樣保留) - 如果使用了 commonjs(使 import CJS 文件變成可能) plugin 且目標文件匹配了 include 選項,則生成一個會拋出異常的
commonjsRequire
函數,除非再額外配置ignore: [conditional-runtime-dependency]
參數來保留原始的動態 require 語句:
function commonjsRequire() {
throw new Error(
Dynamic requires are not currently supported by rollup-plugin-commonjs
)
}
Rollup 無法像 webpack 一樣推斷路徑,如果忽略了動態 require 又想在打包時保留資源文件,可以考慮使用 preval,在 build time 內聯到 bundle 中,例如:
import preval from preval.macro
const locales = preval`
const files = require(glob).sync(__dirname + /locales/*.js)
module.exports = files.map(require)
`
推薦閱讀: