asynchronously load a group of modules - javascript

In Vue.js (2.X) I can make a component load asynchronously - which means the code for that component will be stored in a separate bundle - by changing the corresponding route's definition from
// routes.js
import Messages from '#/views/messages'
export default [
{
path: '/messages',
name: 'messages',
component: Messages
}
]
to
// routes.js
export default [
{
path: '/messages',
name: 'messages',
component: () => import('#/views/messages')
}
]
Is there a way that I can bundle 2 (or more) route components together, such that the code for them is stored in the same bundle and loaded (asynchronously) simultaneously?
The reason I want to do this is because the components in this group are only accessible to a user with a certain role, so either all of them are accessible to a user, or none of them are.

Documentation here.
#vue/cli
If you develop using #vue/cli, you're using webpack under the hood and you need to tell webpack how to group its chunks, using comments inside import():
const Foo = () =>
import(/* webpackChunkName: "foo-bar" */ './Foo.vue')
const Bar = () =>
import(/* webpackChunkName: "foo-bar" */ './Bar.vue')
const Baz = () =>
import(/* webpackChunkName: "baz" */ './Baz.vue')
vite
In you develop using vite, you're using rollup under the hood and you need to tell rollup how to group chunks, in vite.config.js:
export default defineConfig({
build: {
rollupOptions: {
// https://rollupjs.org/guide/en/#outputmanualchunks
output: {
manualChunks: {
'foo-bar': [
'./src/Foo',
'./src/Bar',
],
baz: [
'./src/Baz'
]
},
},
},
})

Related

React lazy import doesn't work when using inside for loop

I want to create folder-based router management and splitting the whole pages into chunk files by using webpackChunkName. if I declared my whole routes into a static array, chunk splitting and lazy loading work fine. However, when I use React.lazy import inside a for loop, it doesn't work at all. It wont split any chunk file based on each route.
So here is my declarations:
This works like a charm:
links: [
{
id: "about",
path: "/about",
component: lazy(() =>
import(/* webpackChunkName: "About" */ "#/views/About")
),
icon: faExclamationCircle,
},
{
id: "guide",
path: "/guide",
component: lazy(() =>
import(/* webpackChunkName: "Guide" */ "#/views/Guide")
),
icon: faBookReader,
},
],
When I try to dynamic import inside a for loop, it doesn't work at all...
sections: result[0].pages.map((i) => {
// It throws an error if doesn't have a config file
const config = require(`#/views/sections/${i.section}/config.js`).default;
return {
id: i.section,
icon: config.icon,
groups: config.group,
pages: i.pages.map((x, k) => {
if (x.section === "index.js")
return {
id: i.section,
path: "/" + i.section,
component: lazy(() =>
/* webpackChunkName: "someModule", */
import(`#/views/sections/${i.section}/index.js`)
),
};
return {
id: getCleanPath(x.section),
path:
k === 0
? `/${i.section}`
: getCleanFullPath(i.section, x.section),
component: lazy(() =>
import(
/* webpackChunkName: "someModule" */
`#/views/sections/${i.section}/${x.section}`
)
),
};
}),
};
}),
Ups, I used require.context for getting all folders inside the src folder, so I have to load it 'lazy'. That's why webpack won't split chunk files.
// Before
require.context("../views/sections", true, /^(?!.*config\.js$).*\.(js)$/)
// After
require.context("../views/sections", true, /^(?!.*config\.js$).*\.(js)$/, "lazy")

Vue js Prefetch components

I recently learnt about lazy loading components and started using it. Now I am trying to prefetch the lazy loaded components as well as vue-router routes. But using the chrome devtools I found that lazy loaded chunks are only loaded when we actually navigate to the lazy loaded route (in case of a vue-router route) or when the v-if evaluates to true and the component is rendered (in case of a lazy loaded component).
I have also tried using the webpackPrefetch: true magic string in the router as well as component import statement but doing that does not seem to make any difference.
Project structure:
Master-Detail layout
router config:
import Vue from "vue";
import Router from "vue-router";
Vue.use(Router);
var routes = [
{
path: "/DetailPage",
component: () => import(/* webpackChunkName: "Detail-chunk" */ "AppModules/views/MyModuleName/DetailPage.vue")
},
{
path: "/MasterPage",
component: () => import("AppModules/views/MyModuleName/MasterPage.vue")
}
]
export const router = new Router({
routes: routes,
stringifyQuery(query) {
// encrypt query string here
}
});
export default router;
Master view:
<template>
<div #click="navigate">
Some text
</div>
</template>
<script>
export default {
name: "MasterPage",
methods: {
navigate() {
this.$router.push({
path: "/DetailPage",
query: {},
});
},
},
};
</script>
Details page:
<template>
<div>
<my-component v-if="showComponent" />
<div #click="showComponent = true">Show Component</div>
</div>
</template>
<script>
const MyComponent = () => import(/* webpackChunkName: "MyComponent-chunk" */ "AppCore/components/AppElements/Helpers/MyComponent");
export default {
name: "DetailPage",
components: {
MyComponent,
},
data() {
return {
showComponent: false
}
}
};
</script>
vue.js.config file:
const path = require("path");
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
.BundleAnalyzerPlugin;
module.exports = {
publicPath: "some-url",
outputDir: "./some/path",
chainWebpack: webapckConfig => {
webapckConfig.plugin("html").tap(() => {
return [
{
inject: true,
filename: "index.html",
template: "./public/index.html"
}
];
});
},
productionSourceMap: true,
configureWebpack: {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: "server",
generateStatsFile: false,
statsOptions: {
excludeModules: "node_modules"
}
})
],
output: {
filename: "some file name",
libraryTarget: "window"
},
module: {
rules: [
{
test: /\.(woff|woff2)(\?v=\d+\.\d+\.\d+)?$/,
use: [
{
loader: "url-loader",
options: {
limit: 50000,
fallback: "file-loader",
outputPath: "/assets/fonts",
name: "[name].[ext]?hash=[hash]"
}
}
]
}
]
},
resolve: {
alias: {
vue$: process.env.NODE_ENV == 'production' ? 'vue/dist/vue.min.js' : 'vue/dist/vue.js',
AppCore: path.resolve(__dirname, "..", "..", "AppCoreLite"),
AppModules: path.resolve(__dirname, "..", "..", "AppModulesLite")
}
}
}
};
Both the async route and component do get split into separate chunks but these chunks are not prefetched.
When I navigate to the master view, I dont see Detail-chunk.[hash].js in the network tab. It gets requested only when the navigate method in the master page is executed (this the correct lazy load behaviour without prefetch).
Now when I am on the details page, MyComponent-chunk.[hash].js is only requested when the showComponent becomes true (on click of a button)
I've also read at a few places that vue-cli v3 does has prefetch functionality enabled by default and webpack magic string is not needed. I also tried that by removing the webpackPrefetch comment but it made no difference.
I did vue-cli-service inspect and found that prefetch plugin is indeed present in the webpack config:
/* config.plugin('preload') */
new PreloadPlugin(
{
rel: 'preload',
include: 'initial',
fileBlacklist: [
/\.map$/,
/hot-update\.js$/
]
}
),
/* config.plugin('prefetch') */
new PreloadPlugin(
{
rel: 'prefetch',
include: 'asyncChunks'
}
),
UPDATE: I tried removing the prefetch webpack plugin using config.plugins.delete('prefetch'); and then using the webpack magic comment: /* webpackPrefetch: true */ but it made no difference.
How do I implement prefetch functionality?
I solved this by creating a simple prefetch component that loads after a custom amount of time.
Prefetch.vue
<script>
import LazyComp1 from "./LazyComp1.vue";
import LazyComp2 from "./LazyComp2.vue";
export default {
components:{
LazyComp1,
LazyComp2,
}
}
</script>
App.vue
<template>
<Prefech v-if="loadPrefetch"></Prefech>
</template>
<script>
export default {
components: {
Prefech: () => import("./Prefetch");
},
data() {
return {
loadPrefetch: false
}
},
mounted() {
setTimeout(() => {
this.loadPrefetch = true;
}, 1000);
}
}
</script>
Lazy loaded components are meant to be loaded only when user clicks the route. If you want to load component before it, just don't use lazy loading.
vue-router will load components to memory and swap the content of the tag dynamically even if you will use normally loaded component.
You need to implement vue-router-prefetch package for your need. Here is a working demo.
Note: From the working demo, you can notice from console.log that only page 2 is prefetched by the QuickLink component imported from vue-router-prefetch
Code :
import Vue from "vue";
import Router from "vue-router";
import RoutePrefetch from "vue-router-prefetch";
Vue.use(Router);
Vue.use(RoutePrefetch, {
componentName: "QuickLink"
});
const SiteNav = {
template: `<div>
<ul>
<li>
<router-link to="/page/1">page 1</router-link>
</li>
<li>
<quick-link to="/page/2">page 2</quick-link>
</li>
<li>
<router-link to="/page/3">page 3</router-link>
</li>
</ul>
</div>`
};
const createPage = (id) => async() => {
console.log(`fetching page ${id}`);
return {
template: `<div>
<h1>page {id}</h1>
<SiteNav />
</div>`,
components: {
SiteNav
}
};
};
const routers = new Router({
mode: "history",
routes: [{
path: "/",
component: {
template: `<div>
<h1>hi</h1>
<SiteNav />
</div>`,
components: {
SiteNav
}
}
}]
});
for (let i = 1; i <= 3; i++) {
routers.addRoutes([{
path: `/page/${i + 1}`,
component: createPage(i + 1)
}]);
}
export default routers;
I'm working on a mobile app. and wanted to load some components dynamically while showing the splash screen.
#Thomas's answer is a good solution (a Prefetch component), but it doesn't load the component in the shadow dom, and Doesn't pass Vetur validation (each component must have its template)
Here's my code:
main.vue
<template>
<loader />
</template>
<script>
import Loader from './Loader'
const Prefetch = () => import('./Prefetch')
export default {
name: 'Main',
components: {
Loader,
Prefetch
}
}
</script>
Prevetch.vue
<template>
<div id="prefetch">
<lazy-comp-a />
<lazy-comp-b />
</div>
</template>
<script>
import Vue from 'vue'
import LazyCompA from './LazyCompA'
import LazyCompB from './LazyCompB'
Vue.use(LazyCompA)
Vue.use(LazyCompB)
export default {
components: {
LazyCompA,
LazyCompB
}
}
</script>
<style lang="scss" scoped>
#prefetch {
display: none !important;
}
</style>
The loader component is loaded & rendered, then the Prefetch component can load anything dynamically.
since vue-router-prefetch didn't work for me i ended up doing it manually.
Vue 3 Example - all routes are iterated on page load and async components are loaded
const router = createRouter({
history: createWebHistory(),
routes: [{
path: '/',
component: HomeView
}, {
path: '/about',
component: () => import('./views/AboutView.vue')
}]
});
async function preloadAsyncRoutes() {
// iterate all routes and if the component is async - prefetch it!
for (const route of router.getRoutes()) {
if (!route.components) continue;
// most routes have just a "default" component unless named views are used - iterate all entries just in case
for (const componentOrImporter of Object.values(route.components)) {
if (typeof componentOrImporter === 'function') {
try {
// prefetch the component and wait until it finishes before moving to the next one
await componentOrImporter();
} catch (err) {
// ignore failing requests
}
}
}
}
}
window.addEventListener('load', preloadAsyncRoutes);

Dynamic Imports: Am I missing something?

I have a React project that uses Webpack as a bundler, and I'm splitting my bundle into two chunks -- the main codebase main.js, and the vendor bundle vendor.js.
After building these bundles, main.js ends up being 45kb and vendor.js is 651kb.
One specific vendor library is 225kb and seems to be the worst offendor in the vendor imports.
I am importing this library in a page component at the top of the file:
import React from 'react';
import { ModuleA, ModuleB } from 'heavyPackage'; // 225kb import
...
const Page = ({ setThing }) => {
...
};
To try and have this heavy import loaded in a separate bundle, I tried to instead import these modules using a dynamic import.
Inside the Page component, the modules weren't actually used until a particular function was called, so I tried to import the modules within that scope rather than at the top of the file:
import React from 'react';
...
const Page = ({ setThing }) => {
...
const handleSignIn = async () => {
const scopedPackage = await import('heavyPackage');
const { moduleA, moduleB } = scopedPackage;
// use moduleA & moduleB normally here
};
};
For some reason I figured Webpack would intelligently pick up on what I'm trying to do here and separate this heavy package into its own chunk that is downloaded only when needed, but the resulting bundles were the same -- a main.js that was 45kb and a vendor.js that was 651kb. Is my line of thinking here correct and possibly my Webpack configuration is off, or am I thinking of dynamic imports in the wrong way?
edit I have Webpack configured to split the bundle using splitChunks. Here is how I have this configured:
optimization: {
chunkIds: "named",
splitChunks: {
cacheGroups: {
commons: {
chunks: "initial",
maxInitialRequests: 5,
minChunks: 2,
minSize: 0,
},
vendor: {
chunks: "initial",
enforce: true,
name: "vendor",
priority: 10,
test: /node_modules/,
},
},
},
},
Update for React 18: The code below is no longer required to split chunks/dynamically load components. Instead, you can use React.lazy with Suspense, which achieves similar results (this only works for React components, therefore any node_module imports would need to be imported within this dynamically loaded component):
const ProfilePage = React.lazy(() => import('./ProfilePage')); // Lazy-loaded
<Suspense fallback={<Spinner />}>
<ProfilePage />
</Suspense>
#Ernesto's answer offers one way of code splitting by using react-loadable with the babel-dynamic-import plugin, however, if your Webpack version is v4+ (and has a custom Webpack config set to SplitChunks by all), then you'll only need to use magic comments and a custom React component.
From the docs:
By adding [magic] comments to the import, we can do things such as name our chunk or select different modes. For a full list of these magic comments see the code below followed by an explanation of what these comments do.
// Single target
import(
/* webpackChunkName: "my-chunk-name" */
/* webpackMode: "lazy" */
'module'
);
// Multiple possible targets
import(
/* webpackInclude: /\.json$/ */
/* webpackExclude: /\.noimport\.json$/ */
/* webpackChunkName: "my-chunk-name" */
/* webpackMode: "lazy" */
/* webpackPrefetch: true */
/* webpackPreload: true */
`./locale/${language}`
);
Therefore, you can create a reusable LazyLoad component like so:
import React, { Component } from "react";
import PropTypes from "prop-types";
class LazyLoad extends Component {
state = {
Component: null,
err: "",
};
componentDidMount = () => this.importFile();
componentWillUnmount = () => (this.cancelImport = true);
cancelImport = false;
importFile = async () => {
try {
const { default: file } = await import(
/* webpackChunkName: "[request]" */
/* webpackMode: "lazy" */
`pages/${this.props.file}/index.js`
);
if (!this.cancelImport) this.setState({ Component: file });
} catch (err) {
if (!this.cancelImport) this.setState({ err: err.toString() });
console.error(err.toString());
}
};
render = () => {
const { Component, err } = this.state;
return Component ? (
<Component {...this.props} />
) : err ? (
<p style={{ color: "red" }}>{err}</p>
) : null;
};
}
LazyLoad.propTypes = {
file: PropTypes.string.isRequired,
};
export default file => props => <LazyLoad {...props} file={file} />;
Then in your routes, use LazyLoad and pass it the name of a file in your pages directory (eg pages/"Home"/index.js):
import React from "react";
import { Route, Switch } from "react-router-dom";
import LazyLoad from "../components/LazyLoad";
const Routes = () => (
<Switch>
<Route exact path="/" component={LazyLoad("Home")} />
<Route component={LazyLoad("NotFound")} />
</Switch>
);
export default Routes;
On that note, React.Lazy and React-Loadable are alternatives to having a custom Webpack config or Webpack versions that don't support dynamic imports.
A working demo can be found here. Follow installation instructions, then you can run yarn build to see routes being split by their name.
Oki then, look! you have yow webpack config with the splitChunks property, also you need to add a chunkFilename property in side of the output object from webpack.
If we take for example the one generated by CRA
// The build folder.
path: isEnvProduction ? paths.appBuild : undefined,
// Add /* filename */ comments to generated require()s in the output.
pathinfo: isEnvDevelopment,
// There will be one main bundle, and one file per asynchronous chunk.
// In development, it does not produce real files.
filename: isEnvProduction
? 'static/js/[name].[contenthash:8].js'
: isEnvDevelopment && 'static/js/bundle.js',
// TODO: remove this when upgrading to webpack 5
futureEmitAssets: true,
// THIS IS THE ONE I TALK ABOUT
chunkFilename: isEnvProduction
? 'static/js/[name].[contenthash:8].chunk.js'
: isEnvDevelopment && 'static/js/[name].chunk.js',
// webpack uses `publicPath` to determine where the app is being served from.
// It requires a trailing slash, or the file assets will get an incorrect path.
// We inferred the "public path" (such as / or /my-project) from homepage.
publicPath: paths.publicUrlOrPath,
// Point sourcemap entries to original disk location (format as URL on Windows)
devtoolModuleFilenameTemplate: isEnvProduction
? info =>
path
.relative(paths.appSrc, info.absoluteResourcePath)
.replace(/\\/g, '/')
: isEnvDevelopment &&
(info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
// Prevents conflicts when multiple webpack runtimes (from different apps)
// are used on the same page.
jsonpFunction: `webpackJsonp${appPackageJson.name}`,
// this defaults to 'window', but by setting it to 'this' then
// module chunks which are built will work in web workers as well.
globalObject: 'this',
},
Once you have that on yow webpack. next thing is to install a npm i -D #babel/plugin-syntax-dynamic-import and add it to your babel.config.js
module.exports = api =>
...
return {
presets: [
.....
],
plugins: [
....
"#babel/plugin-syntax-dynamic-import",
....
]
}
then last thing npm install react-loadable
create a folder called: containers. in it place all the containers
inside index.js do some like:
The loadable object have two properties
export const List = Loadable({
loader: () => import(/* webpackChunkName: "lists" */ "./list-constainer"),
loading: Loading,
});
loader: component to dynamically import
loadinh: component to display until the dynamic component is loaded.
and for last on you Router set each loadable to a route.
...
import { Lists, List, User } from "../../containers";
...
export function App (): React.ReactElement {
return (
<Layout>
<BrowserRouter>
<SideNav>
<nav>SideNav</nav>
</SideNav>
<Main>
<Header>
<div>Header</div>
<div>son 2</div>
</Header>
<Switch>
<Route exact path={ROUTE_LISTS} component={Lists} />
<Route path={ROUTE_LISTS_ID_USERS} component={List} />
<Route path={ROUTE_LISTS_ID_USERS_ID} component={User} />
<Redirect from="*" to={ROUTE_LISTS} />
</Switch>
</Main>
</BrowserRouter>
</Layout>
);
}
so then when you bundle yow code we get some like:

Unexpected undefined while import with Webpack

I have a problem that has never happened to me before: I'm compiling a little basic starter browser web app (with React) using Webpack + Babel 7.
I've got three different file:
withAuth.js The Auth High Order Component
NavBar.js The NavBar Component
Login.js The Login Form
If I import the withAuth HOC in the NavBar is everything alright, but if I import the withAuth component in the Login.js file it return undefined
/** withAuth.js */
console.log('withAuth Loaded');
const withAuth = Child => ChildProps => (
<AuthContext.Consumer>
{ authClient => <Child {...ChildProps} authClient={authClient} }
</AuthContext.Consumer>
)
export { withAuth };
/** NavBar.js */
import { withAuth } from 'HOC/Auth';
console.log('NavBar Loaded', withAuth); // <- My HOC
const NavBarComponent = (authClient) => { /* ... My Code ... */ }
const NavBar = withAuth(NavBarComponent);
export default NavBar;
/** Login.js */
import { withAuth } from 'HOC/Auth';
console.log('Login Loaded', withAuth); // <- undefined ??
const LoginFormComponent = (authClient) => { /* ... My Code ... */ }
const LoginForm = withAuth(LoginFormComponent);
// /|\
// |
// Will produce an Error, withAuth is Undefined
This is my Webpack Configuration:
/** webpack.config.js */
module.exports = {
entry: { core: 'index.js' },
resolve: {
alias: {
HOC: './path/to/hoc/folder'
}
},
optimization: {
runtimeChunk: 'single',
splitChunks: {
chunks: 'all'
}
},
plugins: [ /* Various Plugin */ ],
module: {
rules: [ /* My Rules */ ]
}
}
Any one know why my HOC is undefined?
Edit:
I've placed Console Log in the tree file. The result are:
'Login Loaded' - undefined
'withAuth Loaded'
'NavBar Loaded' - function() { }
Edit 2:
This is the files structure:
app/
|-high-order-component/
| |-auth/
| |-withAuth.js
|
|-layout-component/
| |-navbar/
| |-index.js
|
|-pages/
|-auth/
|-login.js
Resolved
After much testing and research throughout the afternoon I came to the solution of the problem. As I said in the question, mine is a larger project and I only partially wrote its structure because I thought the problem was located in those three files.
In reality, the problem was a Circular Dependency problem and not a Webpack configuration problem.
In my project I have a module called 'Route' that store all Path and all Component for Path, so I can build the React Router using Array Map function. That module has a function that allow me to Route through path and that can return me a path string to a Component.
My problem was due to the fact that this module is often called in the project and this has created a Circular Dependency.
Webpack doesn't show the Circular Dependency during compiling, but I found useful adding a plugin, called CircualDependencyPlugin. This plugin will break Webpack compiling when a Circual Dependency will be found.
Splitting the Route module into two files solved my problem.

Alternative for "component: () => import()" in VueJS routing

I downloaded a template online to better understand VueJS and also create a web app. However I have a problem with routing. There is a function in my router's index.js that imports a path. The import syntax seems to be buggy due to some webpack issues. I tried a lot of different things but couldn't fix the bug so I want to find a workaround for that import syntax
This is my code for router's index.js
import Vue from 'vue'
import VueAnalytics from 'vue-analytics'
import Router from 'vue-router'
import Meta from 'vue-meta'
// Routes
import paths from './paths'
// import views from './views'
function route (path, view, name) {
return {
name: name || view,
path,
component: () => import(
`../views/${view}.vue`
)
}
}
Vue.use(Router)
// Create a new router
const router = new Router({
mode: 'history',
routes: paths.map(path => route(path.path, path.view, path.name)).concat([
{ path: '*', redirect: '/home' }
]),
scrollBehavior (to, from, savedPosition) {
if (savedPosition) {
return savedPosition
}
if (to.hash) {
return { selector: to.hash }
}
return { x: 0, y: 0 }
}
})
Vue.use(Meta)
// Bootstrap Analytics
// Set in .env
// https://github.com/MatteoGabriele/vue-analytics
if (process.env.GOOGLE_ANALYTICS) {
Vue.use(VueAnalytics, {
id: process.env.GOOGLE_ANALYTICS,
router,
autoTracking: {
page: process.env.NODE_ENV !== 'development'
}
})
}
export default router
When i try to build it, I get an error saying:
ERROR in ./src/router/index.js
Module build failed: SyntaxError: C:/djangoProjects/martin - Copy/martin/src/router/index.js: Unexpected token (15:21)
The syntax error is on line (15:21), in the route function on the component: () => import( line and exactly on import. Fixing this issue is a pain so I was wondering if there is a workaround for it without using the import syntax?
If I remember correctly you'll need a plugin for babel that can handle dynamic imports.
Check out:
https://babeljs.io/docs/en/babel-plugin-syntax-dynamic-import
Run npm install #babel/plugin-syntax-dynamic-import
Create or open .babelrc
{
"plugins": ["#babel/plugin-syntax-dynamic-import"]
}

Categories

Resources