I'm transitioning a legacy app to Webpack. I'm using Webpack 5.56 (latest at time of writing).
My app is localised and I have a folder with a handful of locale files,
locales
- locale.en.ts
- locale.de.ts
- etc
Each of these locale files is an ES module and they all export (different implementations of) the same functions — getText
, printNumber
, etc. I have a wrapper module which dynamically import
s the correct locale for the current user:
// localization.ts
interface LocaleModule {
getText(text: string): string;
// etc
}
let module: LocaleModule;
import(`locales/locale.${currentUser.language}`).then(m => {
module = m;
});
export function getText(text: string): string {
return module.getText(text);
}
I know the current user's language when the page is being rendered. I want to include the correct locale.*.js
script as an initial chunk, so that:
localization.ts
can be synchronous. This seemed like it'd be a good fit forwebpackMode: "weak"
, since I'd like to get an error in the console if the locale
file is missing for whatever reason (rather than silently degrade performance). The docs seem to explicitly call out my use case:
This is useful for universal rendering when required chunks are always manually served in initial requests (embedded within the page).
Here's my code:
let module: LocaleModule;
import(
/* webpackMode: "weak" */
/* webpackChunkName: "locales/[request]" */
`./locales/locale.${currentUser.language}`
).then(m => {
module = m;
});
However, it seems webpackMode: "weak"
causes Webpack to emit no chunks for the referenced modules at all . There aren't any locale
files in Webpack's output folder. I can't very well include a chunk in the HTML if it was never emitted!
What's the reason for this behaviour? Is there a clean way to get Webpack to emit chunks for dynamically import
ed modules but not download them asynchronously? (I know that I could use webpackMode: "lazy"
and just include the chunk upfront in a script tag, but I'd like to get an error if the locale file is missing.) Or do I have an XY problem , and there's some better way to do this which I'm unaware of?
I have similar issues and resolve this.
My local file looks like:
const ru = LOCALE_DICTIONARY_RU
window.__default_dictionary__ = ru
module.exports = ru
My locales structure looks like: enter image description here
LOCALE_DICTIONARY_RU it's var defined with DefinePlugin
const localePath = path.resolve(__dirname, '../src/i18n/locales')
const localeKeys = fs.readdirSync(localePath)
const locales = localeKeys.reduce((acc, key) => {
const file = require(path.resolve(__dirname, '../src/i18n/locales/', key))
acc[`LOCALE_DICTIONARY_${path.basename(key, '.json').toUpperCase()}`] = JSON.stringify(file)
return acc
}, {})
// in webpack config
new webpack.DefinePlugin(locales),
You must add new cacheGroup
for splitChunks.cacheGroups
in webpack config
locales: {
enforce: true,
reuseExistingChunk: true,
priority: 50,
chunks: 'all',
test(module) {
if (/src\/i18n\/(.+).js$/.test(module.resource)) return true
return false
},
name(module) {
// Локали лежат в src/i18n
// где *.js файл - файл-обёртка над переменной через DefinePlugin
const moduleFileName = module
.identifier()
.split('/')
.reduceRight((item) => item)
.replace('.js', '')
return `locales~${moduleFileName}`
},
},
Now all of your locales files will be extracted to another chunk files.
You can use any handler for load locales, for example:
loadLocaleHandler: async (locale: Locale) => {
let localeModule: { default: Dictionary } = await import(`i18n/${locale}.js`)
return localeModule.default
},
And for everything to work correctly you must
<script src="/assets/webpack/js/runtime.js" defer="defer"></script>
<script src="/assets/webpack/js/vendors.js" defer="defer"></script>
<!-- For example it maybe value from cookie or context of app -->
<script src="/assets/webpack/js/locales~(ru|en|it|es).chunk.js" defer="defer"></script>
<script src="/assets/webpack/js/entry.js" defer="defer"></script>
const defaultLocale: Locale = cookies.getItem('locale') || process.env.DEFAULT_LOCALE
if (__webpack_modules__[`./src/i18n/${defaultLocale}.js`]) {
__webpack_require__(`./src/i18n/${defaultLocale}.js`)
}
Totally:
In order to use weak
you have to already manually served the chunks as stated in the docs. This means that adding it in a dynamic import as comment does not create any chunks (in contradiction with lazy
and lazy-once
).
Is there a clean way to get Webpack to emit chunks for dynamically imported modules but not download them asynchronously?
For synchronous loading:
You can either:
webpackMode: "lazy"
and include the chunk upfront in a script tag as you stated (the Promise
returned is rejected in case of missing chunk).For your example, creating an entrypoint for each locale could be something like:
const glob = require('glob')
module.exports = {
devtool: false,
entry: {
...glob.sync('./src/locales/*').reduce((acc, module) => {
const name = module.replace('./src/locales/', '').replace('.js', '')
acc[name] = module
return acc
}, {})
}
};
This would emit locale.de.js
and locale.en.js
bundles and then you should somehow manually load a <script defer src="locale.<locale>.js"></script>
, but that depends on how you serve your app.
For asynchronous loading:
You can use webpackMode: "lazy"
along with webpackPreload: true
in order to decouple main
and locale
chunk requests. As stated in the docs
A preloaded chunk starts loading in parallel to the parent chunk.
I can't post such long comment, so it has to be an answer...
So it looks like there isn't a real link between the modules and the bundler can't resolve them compile time so they aren't emitted. The only think I changed in your code is how modules are imported and it worked out of the box:
const langCode = getLangCode();
let mod;
import("./locales/locale.en")
switch (langCode) {
case "en":
import(`./locales/locale.en.js`).then(m => {
mod = m;
console.log("loaded locale");
})
break;
case "de":
import(`./locales/locale.de.js`).then(m => {
mod = m;
console.log("loaded locale");
})
break;
default:
}
export function getText(text) {
return mod.getText(text);
}
function getLangCode() {
return "de";
}
I know the switch case is not ideal, but the bundler can't automatically guess that pattern: ./locales/locale.${langCode}.js
and add all files in the directory that match .js
.
The doc says the following:
'weak': Tries to load the module if the module function has already been loaded in some other way (eg another chunk imported it or a script containing the module was loaded). A Promise is still returned, but only successfully resolves if the chunks are already on the client. If the module is not available, the Promise is rejected. A network request will never be performed. This is useful for universal rendering when required chunks are always manually served in initial requests (embedded within the page), but not in cases where app navigation will trigger an import not initially served.
From what I understand this means the chunks are expected to be already on the page and generated through some other means.
I hope that helps you resolve your issue.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.