简体   繁体   中英

Why doesn't Webpack emit chunks when using webpackMode: "weak"?

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:

  • The browser doesn't have to wait for the main chunk to load before it can start downloading the locale file.
  • The functions in 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

  • Add locale chunk for result html
<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>
  • Add magic webpack code to entry point
const defaultLocale: Locale = cookies.getItem('locale') || process.env.DEFAULT_LOCALE
if (__webpack_modules__[`./src/i18n/${defaultLocale}.js`]) {
  __webpack_require__(`./src/i18n/${defaultLocale}.js`)
}

Totally:

  • you don't need wait loading locales by runtime import for first request
  • you can organize locales for multilocale and multidomain app
  • all of your locales remain dynamic modules and can be loaded at runtime

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:

  1. Use webpackMode: "lazy" and include the chunk upfront in a script tag as you stated (the Promise returned is rejected in case of missing chunk).
  2. You can define the locale js files as dynamic entry points and load them manually by yourself.

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM