Skip to content

Commit 01d3a17

Browse files
author
Oğuzhan Aslan
committed
feat: loadable components with partial js loading
1 parent ea56003 commit 01d3a17

File tree

10 files changed

+142
-89
lines changed

10 files changed

+142
-89
lines changed

babel.config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ module.exports = api => {
88
'@babel/plugin-transform-runtime',
99
'@babel/plugin-proposal-optional-chaining',
1010
'@babel/plugin-proposal-numeric-separator',
11-
'@babel/plugin-proposal-throw-expressions'
11+
'@babel/plugin-proposal-throw-expressions',
12+
'@loadable/babel-plugin',
1213
];
1314

1415
const basePresets = [];

lib/cli.js

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import arg from 'arg';
22
import fs from 'fs';
33
import path from 'path';
4-
import clc from "cli-color";
4+
import clc from 'cli-color';
55
import { spawn } from 'child_process';
66

77
import normalizeUrl from './os';
@@ -19,10 +19,10 @@ function parseArgumentsIntoOptions(rawArgs) {
1919
'--no-bundle': Boolean,
2020
'--analyze': Boolean,
2121
'--port': Number,
22-
'--ssr': Boolean,
22+
'--ssr': Boolean
2323
},
2424
{
25-
argv: rawArgs.slice(2),
25+
argv: rawArgs.slice(2)
2626
}
2727
);
2828
const argsList = removeUnneccesaryValueInObject({
@@ -32,17 +32,18 @@ function parseArgumentsIntoOptions(rawArgs) {
3232
noBundle: args['--no-bundle'],
3333
analyze: args['--analyze'],
3434
configFile: args['--config'],
35-
ssr: args['--ssr'],
35+
ssr: args['--ssr']
3636
});
3737

3838
return argsList;
3939
}
4040

4141
function getVoltranConfigs(configFile) {
42-
const normalizePath = normalizeUrl(path.resolve(__dirname));
43-
const dirName = normalizePath.indexOf('node_modules') > -1 ?
44-
normalizePath.split('/node_modules')[0] :
45-
normalizePath.split('voltran/lib')[0] + 'voltran';
42+
const normalizePath = normalizeUrl(path.resolve(__dirname));
43+
const dirName =
44+
normalizePath.indexOf('node_modules') > -1
45+
? normalizePath.split('/node_modules')[0]
46+
: `${normalizePath.split('voltran/lib')[0]}voltran`;
4647
const voltranConfigs = require(path.resolve(dirName, configFile));
4748

4849
return voltranConfigs;
@@ -68,35 +69,40 @@ function runDevelopmentMode() {
6869
function runProductionMode(voltranConfigs, onlyBundle) {
6970
const bundle = require('../src/tools/bundle');
7071

71-
bundle()
72-
.then((res) => {
73-
console.log(clc.green('Bundle is completed.\n',`File: ${voltranConfigs.distFolder}/server/server.js`));
72+
bundle().then(() => {
73+
console.log(
74+
clc.green('Bundle is completed.\n', `File: ${voltranConfigs.distFolder}/server/server.js`)
75+
);
7476

75-
if (!onlyBundle) {
76-
serve(voltranConfigs);
77-
}
78-
});
77+
if (!onlyBundle) {
78+
serve(voltranConfigs);
79+
}
80+
});
7981
}
8082

8183
function serve(voltranConfigs) {
8284
console.log(clc.green('Project Serve is starting...'));
8385

84-
const out = spawn('node', [
85-
'-r',
86-
'source-map-support/register',
87-
'--max-http-header-size=20480',
88-
`${voltranConfigs.distFolder}/server/server.js`
89-
], {env: {'NODE_ENV': 'production', ...process.env}});
86+
const out = spawn(
87+
'node',
88+
[
89+
'-r',
90+
'source-map-support/register',
91+
'--max-http-header-size=20480',
92+
`${voltranConfigs.distFolder}/server/server.js`
93+
],
94+
{ env: { NODE_ENV: 'production', ...process.env } }
95+
);
9096

91-
out.stdout.on('data', (data) => {
97+
out.stdout.on('data', data => {
9298
console.log(data.toString());
9399
});
94100

95-
out.stderr.on('data', (data) => {
101+
out.stderr.on('data', data => {
96102
console.error(data.toString());
97103
});
98104

99-
out.on('close', (code) => {
105+
out.on('close', code => {
100106
console.log(`child process exited with code ${code}`);
101107
});
102108
}
@@ -123,17 +129,17 @@ export function cli(args) {
123129
if (isValid) {
124130
const createdConfig = `module.exports = ${JSON.stringify(mergeAllConfigs)}`;
125131

126-
fs.writeFile(path.resolve(__dirname, '../voltran.config.js'), createdConfig, function (err) {
132+
fs.writeFile(path.resolve(__dirname, '../voltran.config.js'), createdConfig, function(err) {
127133
if (err) throw err;
128134

129135
console.log('File is created successfully.', mergeAllConfigs.dev);
130136

131137
if (mergeAllConfigs.dev) {
132138
runDevelopmentMode();
133139
} else {
134-
argumentList.noBundle ?
135-
serve(voltranConfigs) :
136-
runProductionMode(mergeAllConfigs, argumentList.bundle);
140+
argumentList.noBundle
141+
? serve(voltranConfigs)
142+
: runProductionMode(mergeAllConfigs, argumentList.bundle);
137143
}
138144
});
139145
} else {

package.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
},
2020
"dependencies": {
2121
"@babel/core": "7.10.4",
22+
"@babel/eslint-parser": "^7.13.14",
2223
"@babel/plugin-proposal-class-properties": "7.10.4",
2324
"@babel/plugin-proposal-numeric-separator": "7.10.4",
2425
"@babel/plugin-proposal-optional-chaining": "7.10.4",
@@ -29,14 +30,18 @@
2930
"@babel/preset-env": "7.10.4",
3031
"@babel/preset-react": "7.10.4",
3132
"@babel/runtime": "^7.11.2",
33+
"@loadable/babel-plugin": "^5.13.2",
34+
"@loadable/component": "^5.12.0",
35+
"@loadable/server": "^5.12.0",
36+
"@loadable/webpack-plugin": "^5.12.0",
3237
"@researchgate/react-intersection-observer": "1.0.3",
3338
"arg": "^4.1.3",
3439
"assets-webpack-plugin": "3.8.4",
3540
"async": "^3.2.0",
3641
"autoprefixer": "9.3.1",
3742
"axios": "0.19.0",
3843
"babel-eslint": "10.0.1",
39-
"@babel/eslint-parser": "^7.12.1",
44+
"babel-loader": "8.0.4",
4045
"classnames": "2.2.6",
4146
"clean-webpack-plugin": "1.0.0",
4247
"cli-color": "^2.0.0",
@@ -46,14 +51,16 @@
4651
"copy-webpack-plugin": "4.5.2",
4752
"css-loader": "1.0.1",
4853
"eev": "0.1.5",
49-
"eslint": "6.1.0",
5054
"esbuild-loader": "^2.11.0",
55+
"eslint": "6.1.0",
5156
"eslint-config-airbnb": "18.0.1",
5257
"eslint-config-prettier": "6.3.0",
58+
"eslint-plugin-babel": "^5.3.1",
5359
"eslint-plugin-import": "^2.19.1",
5460
"eslint-plugin-jsx-a11y": "6.2.3",
5561
"eslint-plugin-prettier": "3.1.1",
5662
"eslint-plugin-react": "7.14.3",
63+
"eslint-plugin-react-hooks": "^4.2.0",
5764
"esm": "^3.2.25",
5865
"file-loader": "1.1.11",
5966
"helmet": "3.21.3",

src/main.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,31 @@ import cluster from 'cluster';
44
import logger from './universal/utils/logger';
55
import Hiddie from 'hiddie';
66
import http from 'http';
7-
import voltranConfig from '../voltran.config';
87
import prom from 'prom-client';
9-
import {HTTP_STATUS_CODES} from './universal/utils/constants';
8+
9+
import voltranConfig from '../voltran.config';
10+
import { HTTP_STATUS_CODES } from './universal/utils/constants';
1011

1112
const enablePrometheus = voltranConfig.monitoring.prometheus;
1213

1314
function triggerMessageListener(worker) {
14-
worker.on('message', function (message) {
15+
worker.on('message', function(message) {
1516
if (message?.options?.forwardAllWorkers) {
1617
sendMessageToAllWorkers(message);
1718
}
1819
});
1920
}
2021

2122
function sendMessageToAllWorkers(message) {
22-
Object.keys(cluster.workers).forEach(function (key) {
23+
Object.keys(cluster.workers).forEach(function(key) {
2324
const worker = cluster.workers[key];
2425
worker.send({
25-
msg: message.msg,
26+
msg: message.msg
2627
});
2728
}, this);
2829
}
2930

30-
cluster.on('fork', (worker) => {
31+
cluster.on('fork', worker => {
3132
triggerMessageListener(worker);
3233
});
3334

@@ -52,7 +53,7 @@ if (cluster.isMaster) {
5253
return res.end(await aggregatorRegistry.clusterMetrics());
5354
}
5455
res.statusCode = HTTP_STATUS_CODES.NOT_FOUND;
55-
res.end(JSON.stringify({message: 'not found'}));
56+
res.end(JSON.stringify({ message: 'not found' }));
5657
});
5758

5859
http.createServer(hiddie.run).listen(metricsPort, () => {

src/universal/components/Html.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ function componentClassName(componentName, context) {
2424

2525
function Html({
2626
componentName,
27-
children,
27+
bodyHtml,
2828
styleTags,
2929
initialState,
3030
fullWidth,
@@ -41,7 +41,7 @@ function Html({
4141
class="${voltranConfig.prefix}-voltran-body voltran-body ${
4242
isMobileFragment ? 'mobile' : ''
4343
}${fullWidth ? 'full' : ''} ${componentClassName(componentName, context)}">
44-
${children}
44+
${bodyHtml}
4545
</div>
4646
<div>REPLACE_WITH_LINKS</div>
4747
<div>REPLACE_WITH_SCRIPTS</div>

src/universal/partials/withBaseComponent.js

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from 'react';
22
import ReactDOM from 'react-dom';
3+
import { loadableReady } from '@loadable/component';
34

45
import ClientApp from '../components/ClientApp';
56
import { WINDOW_GLOBAL_PARAMS } from '../utils/constants';
@@ -35,16 +36,18 @@ const withBaseComponent = (PageComponent, pathName) => {
3536

3637
const initialState = fragments[id].STATE;
3738

38-
ReactDOM.hydrate(
39-
<ClientApp>
40-
<PageComponent {...staticProps} initialState={initialState} history={history} />
41-
</ClientApp>,
42-
componentEl,
43-
() => {
44-
componentEl.style.pointerEvents = 'auto';
45-
componentEl.setAttribute('voltran-hydrated', 'true');
46-
}
47-
);
39+
loadableReady(() => {
40+
ReactDOM.hydrate(
41+
<ClientApp>
42+
<PageComponent {...staticProps} initialState={initialState} history={history} />
43+
</ClientApp>,
44+
componentEl,
45+
() => {
46+
componentEl.style.pointerEvents = 'auto';
47+
componentEl.setAttribute('voltran-hydrated', 'true');
48+
}
49+
);
50+
});
4851
});
4952
}
5053

src/universal/service/RenderService.js

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,27 @@
11
import React from 'react';
22
import { ServerStyleSheet } from 'styled-components';
3-
import PureHtml, { generateLinks, generateScripts } from '../components/PureHtml';
43
import ReactDOMServer from 'react-dom/server';
54
import { StaticRouter } from 'react-router';
5+
import { ChunkExtractor } from '@loadable/server';
6+
7+
/* Components */
8+
import PureHtml, { generateLinks, generateScripts } from '../components/PureHtml';
69
import ConnectedApp from '../components/App';
710
import Html from '../components/Html';
11+
12+
/* Utils */
813
import ServerApiManagerCache from '../core/api/ServerApiManagerCache';
914
import createBaseRenderHtmlProps from '../utils/baseRenderHtml';
1015
import { guid } from '../utils/helper';
16+
import path from 'path';
17+
18+
/* Config */
19+
const voltranConfig = require('../../../voltran.config');
20+
21+
const loadableStats = path.resolve(
22+
process.cwd(),
23+
`${voltranConfig.inputFolder}/universal/loadable-stats.json`
24+
);
1125

1226
const getStates = async (component, context, predefinedInitialState) => {
1327
const initialState = predefinedInitialState || { data: {} };
@@ -41,6 +55,10 @@ const renderLinksAndScripts = (html, links, scripts) => {
4155
.replace('<div>REPLACE_WITH_SCRIPTS</div>', scripts);
4256
};
4357

58+
const getExtractor = (entrypoints, statsFile = loadableStats) => {
59+
return new ChunkExtractor({ statsFile, entrypoints });
60+
};
61+
4462
const renderHtml = (component, initialState, context) => {
4563
// eslint-disable-next-line no-param-reassign
4664
component.id = guid();
@@ -51,21 +69,26 @@ const renderHtml = (component, initialState, context) => {
5169
return PureHtml(component.path, component.name, initialStateWithLocation);
5270
}
5371

54-
const children = ReactDOMServer.renderToString(
72+
const Fragment = () =>
5573
sheet.collectStyles(
5674
<StaticRouter location={component.path} context={context}>
57-
<ConnectedApp initialState={initialStateWithLocation} location={context} />
75+
<ConnectedApp initialState={initialStateWithLocation} location={context}/>
5876
</StaticRouter>
59-
)
60-
);
77+
);
78+
79+
const extractor = getExtractor([component.name]);
80+
const jsx = extractor.collectChunks(<Fragment />);
81+
const styleSheet = new ServerStyleSheet();
82+
83+
const bodyHtml = ReactDOMServer.renderToString(jsx);
6184

6285
const styleTags = sheet.getStyleTags();
6386
const resultPath = `'${component.path}'`;
6487

6588
return Html({
6689
resultPath,
6790
componentName: component.name,
68-
children,
91+
bodyHtml,
6992
styleTags,
7093
initialState: initialStateWithLocation,
7194
fullWidth: component.fullWidth,
@@ -108,7 +131,7 @@ const renderComponent = async (component, context, predefinedInitialState = null
108131
fullWidth: component.fullWidth,
109132
isMobileComponent: component.isMobileComponent,
110133
isPreviewQuery: component.isPreviewQuery,
111-
responseOptions,
134+
responseOptions
112135
};
113136
};
114137

0 commit comments

Comments
 (0)