Skip to content
This repository has been archived by the owner on May 3, 2024. It is now read-only.

Commit

Permalink
Merge branch 'bugfix/use-localhost-rather-than-ip-for-dev-state-confi…
Browse files Browse the repository at this point in the history
…g' of https://github.com/americanexpress/one-app into bugfix/use-localhost-rather-than-ip-for-dev-state-config
  • Loading branch information
anescobar1991 committed Mar 13, 2020
2 parents 6c419d4 + b105030 commit 5bbaf1a
Show file tree
Hide file tree
Showing 14 changed files with 8,282 additions and 10 deletions.
28 changes: 28 additions & 0 deletions __tests__/integration/one-app.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -874,6 +874,34 @@ describe('Tests that can run against either local Docker setup or remote One App
});
});

describe('code-splitting', () => {
test(
'successfully loads a code-split module chunk with a language pack and then lazy loads `franks-burger` chunk',
async () => {
await browser.url(`${appInstanceUrls.browserUrl}/demo/franks-burgers`);

const openerMessage = await browser.$('#franks-opening-line');
await openerMessage.waitForExist();

expect(await openerMessage.getText()).toBe(
'Welcome to Franks Burgers! The best burgers in town.'
);

const btn = await browser.$('#order-burger-btn');
await btn.waitForExist();
// before clicking to lazy load our chunk, ensure `franks-burger` does not exist
const franksBurgerNonExistent = await browser.$('#franks-burger');
await franksBurgerNonExistent.waitForExist(undefined, true);
// once confirmed chunk does not exist, click to load it
await btn.click();
// grab the chunk and wait for it to load
const franksBurger = await browser.$('#franks-burger');
await franksBurger.waitForExist();

await expect(franksBurger.getText()).resolves.toEqual('Burger');
});
});

describe('HTML rendering', () => {
describe('partial only', () => {
test('responds with an incomplete HTML document', async () => {
Expand Down
284 changes: 282 additions & 2 deletions docs/recipes/Code-Splitting-Using-Holocron.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,287 @@
[holocron-readme]: https://github.com/americanexpress/holocron/tree/master/packages/holocron/README.md
[holocron-module-route-readme]: https://github.com/americanexpress/holocron/tree/master/packages/holocron-module-route/README.md
[one-app-bundler-readme]: https://github.com/americanexpress/one-app-cli/tree/master/packages/one-app-bundler/README.md
[one-app-router-readme]: https://github.com/americanexpress/one-app-router/README.md

[holocron-module-api]: https://github.com/americanexpress/holocron/blob/master/packages/holocron/API.md#holocronmodule
[bundler-webpack-config]: https://github.com/americanexpress/one-app-cli/tree/master/packages/one-app-bundler#webpackconfigpath-webpackclientconfigpath--webpackserverconfigpath

[frank-lloyd-root]: ../../prod-sample/sample-modules/frank-lloyd-root/0.0.0/README.md
[franks-burgers]: ../../prod-sample/sample-modules/franks-burgers/0.0.0/README.md

[webpack-dynamic-import-docs]: https://webpack.js.org/guides/code-splitting/#dynamic-imports
[react-code-splitting-docs]: https://reactjs.org/docs/code-splitting.html
[react-lazy-api]: https://reactjs.org/docs/react-api.html#reactlazy
[react-suspense-api]: https://reactjs.org/docs/react-api.html#reactsuspense

[👈 Return to Overview](./Recipes.md)

# Code Splitting Using Holocron

Recipe is forthcoming.
**Contents**
* [Route Based Code Splitting](#route-based-code-splitting)
* [Holocron Module Chunks _Via_ `import()`](#holocron-module-chunks-via-import())
* [Using `holocronModule`](#using-holocronModule)
* [Using `Suspense` and `lazy`](#using-suspense-and-lazy)
* [How It Works](#how-it-works)
* [Performance Considerations](#performance-considerations)
* [Include](#include-existing-tools-into-the-pipeline)

For One App, Holocron modules are a formal way of code-splitting for a modern,
micro front-end in the One App ecosystem. Utilizing the module map as a system
which promotes synergy and asynchrony across teams and collaborators on
a single application.

With Holocron each individual module is by definition, a code split bundled
component with language packs that can be combined and composed based on our
needs. Each module is versioned, configurable, routable and can be further
split into smaller chunks using dynamic importing.

The main advantage we get with code splitting our modules is controlling what parts of
a module is first delivered and larger chunks are loaded by user actions and are not
necessary to include at initial delivery. This can save end users kilobytes of data
transfer (until needed) and reduce the time for waiting on the Holocron module
bundle to load.

Like `@loadable/component` and `react-loadable`, `holocron` is a complete code-splitting
solution that works in both client and server side, however there is no need to scaffold
a build infrastructure to support code-splitting when we use `one-app-bundler`.

**📘 More Information**

* **[`holocron`][holocron-readme]**
* **[`one-app-bundler`][one-app-bundler-readme]**

## Route Based Code Splitting

A Holocron module can be route-driven using `ModuleRoute` and we can use this component
to shape our routing for One App. Since each Holocron module is an independent bundle,
our `childRoutes` is a map of code-split modules that is driven by the url.

`ModuleRoute` is an extension of `Route` from [`one-app-router`][one-app-router-readme]
and can render the module(s) by its route per `history`.
Our Holocron modules are now loaded based on routing through the application.

```jsx
import React from 'react';
import { RenderModule } from 'holocron';
import { ModuleRoute } from 'holocron-module-route';
import { IndexRedirect } from '@americanexpress/one-app-router';

export const childRoutes = () => (
<ModuleRoute moduleName="diner-finder">
<IndexRedirect to="map" />
<ModuleRoute path="map" moduleName="diner-finder-map" />
<ModuleRoute path="selection" moduleName="diner-finder-selection-container">
<ModuleRoute moduleName="diner-finder-selection" />
</ModuleRoute>
</ModuleRoute>
);
```

> To view a root Holocron module with a childRoutes config, [check out the sample module "Frank Lloyd Root"][frank-lloyd-root]
**📘 More Information**

* **[`holocron-module-route`][holocron-module-route-readme]**
* **[`one-app-router`][one-app-router-readme]**

## Holocron Module Chunks _Via_ `import()`

With a Holocron module, we use code-splitting to add granularity to our modules and create chunks
defined by user-driven actions or incorporate lazy loading for larger parts of our module.
Let's start with a sample Holocron module project:

```
root
|── locale
| └── en-US.json
|── src
| ├── Chunk.jsx
| ├── index.js
| └── Module.jsx
└── package.json
```

`src/index.js`
```jsx
import HolocronModule from './Module';

export default HolocronModule;
```

> It's important to mention, when we are code splitting with dynamic `import`s, we
> must rely on user actions to load module chunks dynamically.
`src/Module.jsx`
```jsx
import React from 'react';

export default function MyHolocronModule() {
const [loadModule, setLoadModule] = React.useState(false);
const [Component, setComponent] = React.useState(null);

React.useEffect(() => {
if (loadModule) {
import(/* webpackChunkName: "<chunkName>" */ './Chunk')
// the multi-line comment /* webpackChunkName: "..." */
// is used by webpack to name your chunk
.then((importedChunk) => importedChunk.default || importedChunk)
// when we convert ES modules to common/supported formats,
// we might need to interop the `default` property to get the export
.then((chunk) => {
// ... do things with the chunk once we have what we want
setComponent(chunk);
});
}
}, [loadModule]);

if (Component) return <Component />;

return (
<button type="button" onClick={() => setLoadModule(true)}>
Load Module
</button>
);
}
```

`src/Chunk.jsx`
```jsx
import React from 'react';

export default function ModuleChunk() {
return <p>My Holocron Module Chunk</p>;
}
```

In our basic example, if we wanted to use `Chunk.jsx` as a Holocron module chunk, we can use
the supported `import()` syntax to divide up our module bundle into logical chunks and name
our chunk using the [_magic comments_ `/* webpackChunkName: "..." */`][webpack-dynamic-import-docs]:

```js
// compared to traditional imports, which adds to the main bundle
import './Chunk';

import(/* webpackChunkName: "<chunkName>" */ './Chunk');
```

Our _chunk_ can be anything from a component to a `node_modules` package. Once we run
`bundle-module` from [`one-app-bundler`][one-app-bundler-readme], our output in `build/<version>`
will contain additional JavaScript files based on the number of chunks we have dynamically split
from our module.

```
root
└── build
└── 1.0.0
├── <chunkName>.<moduleName>.chunk.browser.js
├── vendors~<chunkName>.<moduleName>.chunk.browser.js
├── <chunkName>.<moduleName>.chunk.legacy.browser.js
├── <moduleName>.browser.js
├── <moduleName>.legacy.browser.js
├── <moduleName>.node.js
└── en-US
├── <moduleName>.json
├── integration.json
└── qa.json
```

> To view a module chunks example using dynamic importing, [check out the sample module "Franks Burgers"][franks-burgers]
### Using `holocronModule`

`src/Module.jsx`
```jsx
import React from 'react';
import { holocronModule } from 'holocron';

let Chunk = () => <p>Loading...</p>;

export function MyHolocronModule() {
return <Chunk />;
}

export default holocronModule({
name: 'holocron-module-name',
// the load property allows us to load our chunk with our module
load: () => () => Promise.all([
// the same dynamic import is used to create a promise with our chunk
import(/* webpackChunkName: '<chunkName>' */ './Chunk')
// we check if our chunk has a `default` export or not
.then((imported) => imported.default || imported)
// and assign the exported chunk to memory
.then((Component) => {
Chunk = Component;
})
// in the event of failure
.catch((error) => {
Chunk = () => <p>{error.message}</p>;
}),
]),
// we can wait for our holocron module to load the chunk during server-side rendering
// and render the module with the loaded chunk
options: { ssr: true },
})(MyHolocronModule);
```

### Using `Suspense` and `lazy`

React comes with an API made with code splitting in mind;
with [`React.Suspense`][react-suspense-api] and [`React.lazy`][react-lazy-api], we can
load a module chunk from our CDN and fallback to a given value while it asynchronously loads.

> server side rendering is not compatible with Suspense & lazy
`src/Module.jsx`
```jsx
import React from 'react';

const Chunk = React.lazy(() => import(/* webpackChunkName: "<chunkName>" */ './Chunk')
.then((importedChunk) => importedChunk.default || importedChunk));

export default function MyHolocronModule() {
return (
<React.Suspense fallback={<p>Loading...</p>}>
<Chunk />
</React.Suspense>
);
}
```

### Usage Considerations

#### Performance

When it comes to bundling a Holocron module and splitting it up into chunks for loading, it's
important to consider why we are code splitting our Holocron module. With each chunk
that is split from the module bundle, the browser has to open an extra connection to the server
to fetch every chunk. This can lead to performance degradation and can cause excessive traffic
to the web server if the code splitting is too granular.

#### UX

For an end user of One App, we want to create a fluid user experience while module chunks
are being asynchronously loaded. We want to focus on when the module is being loaded
and avoid "flashing" an indeterminate loading state (eg fast network, browser cache, service worker)
as well as handle delays in the module chunk delivery.

### How It Works

One App uses [`one-app-bundler`][one-app-bundler-readme] to support dynamic importing and code
splitting on the module level. Out of the box, [`one-app-bundler`][one-app-bundler-readme] uses
`webpack` and `babel` (with `@babel/plugin-syntax-dynamic-import`) under the hood to understand
the `import()` syntax and automatically split your module where ever the `import() ` statement
is used within your module.

We can expect the final bundled module output to contain everything it normally does,
with two additional JavaScript files per chunk (modern `browser` and `legacy.browser` versions).
For the `node` build output, all the dynamic imports have been imported, bundled and treated
as a `webpack_require` call.

### Include Other Tools Into The Pipeline

It can be helpful to test your current apps for migration and support `react-loadable` and
`@loadable/component`. [We can extend the webpack config if needed.][bundler-webpack-config]

[☝️ Return To Top](#code-splitting-using-holocron)
[☝️ Return To Top](#code-splitting-using-holocron)
Loading

0 comments on commit 5bbaf1a

Please sign in to comment.