Skip to content

Commit

Permalink
feat: improve SSR support
Browse files Browse the repository at this point in the history
BREAKING CHANGE:

- SSR has been rewritten from scratch, if you use it, please follow the
new guide.
- Prefetch component and prefetch functions have been removed, please
use `webpackPrefetch` instead.
  • Loading branch information
gregberge committed Nov 10, 2018
1 parent 766bd66 commit eb1cfe8
Show file tree
Hide file tree
Showing 42 changed files with 5,450 additions and 462 deletions.
39 changes: 6 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,46 +230,19 @@ export const OtherComponent = loadable(() =>
### Prefetching
To enhance user experience, you can prefetch components, it loads component in background. This way you will avoid loading at first component display.
Loadable Components is fully compatible with [webpack hints `webpackPrefetch` and `webpackPreload`](https://webpack.js.org/guides/code-splitting/#prefetching-preloading-modules).
Each `loadable` component exposes a `Prefetch` component. It renders nothing but prefetch the component.
Most of the time, you want to "prefetch" a component, it means it will be loaded when the browser is idle. You can do it by adding `/* webpackPrefetch: true */` inside your import statement.
```js
import loadable from '@loadable/component'

const OtherComponent = loadable(() => import('./OtherComponent'))

function MyComponent() {
return (
<div>
{/* Nothing will be rendered, but the component will be loaded in background */}
<OtherComponent.Prefetch />
</div>
)
}
```
A method `prefetch` is also exposed, you can call it to trigger `prefetch` on user action.
```js
import loadable from '@loadable/component'

const OtherComponent = loadable(() => import('./OtherComponent'))

function MyComponent() {
return (
<div>
<button onMouseOver={() => OtherComponent.prefetch()}>
Prefetch on hover
</button>
</div>
)
}
const OtherComponent = loadable(() =>
import(/* webpackPrefetch: true */ './OtherComponent'),
)
```
> `prefetch` and `Prefetch` are also available for components created with `lazy`, `loadable.lib` and `lazy.lib`.
> Only component based prefetching (`<Prefetch>`) is compatible with Server Side Rendering.
> You can extract prefetched resources server-side to add `<link rel="prefetch">` in your head.
## API
Expand Down
9 changes: 9 additions & 0 deletions examples/server-side-rendering/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"env": {
"browser": true
},
"rules": {
"import/no-unresolved": "off",
"import/no-extraneous-dependencies": "off"
}
}
1 change: 1 addition & 0 deletions examples/server-side-rendering/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
public/dist
29 changes: 29 additions & 0 deletions examples/server-side-rendering/babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const loadableBabelPlugin = require('../../packages/babel-plugin')

function isWebTarget(caller) {
return Boolean(caller && caller.target === 'web')
}

function isWebpack(caller) {
return Boolean(caller && caller.name === 'babel-loader')
}

module.exports = api => {
const web = api.caller(isWebTarget)
const webpack = api.caller(isWebpack)

return {
presets: [
'@babel/preset-react',
[
'@babel/preset-env',
{
useBuiltIns: web ? 'entry' : undefined,
targets: !web ? { node: 'current' } : undefined,
modules: webpack ? false : 'commonjs',
},
],
],
plugins: ['@babel/plugin-syntax-dynamic-import', loadableBabelPlugin],
}
}
6 changes: 6 additions & 0 deletions examples/server-side-rendering/nodemon.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"ignore": ["client", "public"],
"execMap": {
"js": "babel-node"
}
}
32 changes: 32 additions & 0 deletions examples/server-side-rendering/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "client-side",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"dev": "nodemon src/server/main.js",
"build": "NODE_ENV=production yarn build:webpack && yarn build:lib",
"build:webpack": "webpack",
"build:lib": "babel -d lib src",
"start": "NODE_ENV=production node lib/server/main.js"
},
"devDependencies": {
"@babel/cli": "^7.1.2",
"@babel/node": "^7.0.0",
"babel-loader": "^8.0.4",
"css-loader": "^1.0.1",
"mini-css-extract-plugin": "^0.4.4",
"nodemon": "^1.18.5",
"webpack": "^4.24.0",
"webpack-cli": "^3.1.2",
"webpack-dev-middleware": "^3.4.0",
"webpack-node-externals": "^1.7.2"
},
"dependencies": {
"express": "^4.16.4",
"express-async-handler": "^1.1.4",
"moment": "^2.22.2",
"react": "^16.6.0",
"react-dom": "^16.6.0"
}
}
25 changes: 25 additions & 0 deletions examples/server-side-rendering/src/client/App.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react'
// eslint-disable-next-line import/no-extraneous-dependencies
import loadable from '@loadable/component'
import './main.css'

const A = loadable(() => import('./letters/A'))
const B = loadable(() => import('./letters/B'))
const C = loadable(() => import(/* webpackPreload: true */ './letters/C'))
const D = loadable(() => import(/* webpackPrefetch: true */ './letters/D'))

// We keep some references to prevent uglify remove
A.C = C
A.D = D

const Moment = loadable.lib(() => import('moment'))

const App = () => (
<div>
<A />
<B />
<Moment>{moment => moment().format('HH:mm')}</Moment>
</div>
)

export default App
1 change: 1 addition & 0 deletions examples/server-side-rendering/src/client/letters/A.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/* A CSS */
10 changes: 10 additions & 0 deletions examples/server-side-rendering/src/client/letters/A.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// We simulate that "moment" is called in "A" and "B"
import moment from 'moment'
import './A.css'

const A = () => 'A'

// We keep a reference to prevent uglify remove
A.moment = moment

export default A
9 changes: 9 additions & 0 deletions examples/server-side-rendering/src/client/letters/B.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// We simulate that "moment" is called in "A" and "B"
import moment from 'moment'

const B = () => 'B'

// We keep a reference to prevent uglify remove
B.moment = moment

export default B
1 change: 1 addition & 0 deletions examples/server-side-rendering/src/client/letters/C.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default () => 'C'
1 change: 1 addition & 0 deletions examples/server-side-rendering/src/client/letters/D.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default () => 'D'
1 change: 1 addition & 0 deletions examples/server-side-rendering/src/client/main-node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './App'
10 changes: 10 additions & 0 deletions examples/server-side-rendering/src/client/main-web.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react'
import { hydrate } from 'react-dom'
// eslint-disable-next-line import/no-extraneous-dependencies
import { loadableReady } from '@loadable/component'
import App from './App'

loadableReady(() => {
const root = document.getElementById('main')
hydrate(<App />, root)
})
1 change: 1 addition & 0 deletions examples/server-side-rendering/src/client/main.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/* Main CSS */
68 changes: 68 additions & 0 deletions examples/server-side-rendering/src/server/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import path from 'path'
import express from 'express'
import React from 'react'
import { renderToString } from 'react-dom/server'
import asyncHandler from 'express-async-handler'
import { ChunkExtractor } from '@loadable/server'

const app = express()

app.use(express.static(path.join(__dirname, '../../public')))

if (process.env.NODE_ENV !== 'production') {
/* eslint-disable global-require, import/no-extraneous-dependencies */
const { default: webpackConfig } = require('../../webpack.config.babel')
const webpackDevMiddleware = require('webpack-dev-middleware')
const webpack = require('webpack')
/* eslint-enable global-require, import/no-extraneous-dependencies */

const compiler = webpack(webpackConfig)

app.use(
webpackDevMiddleware(compiler, {
logLevel: 'silent',
publicPath: '/dist/web',
writeToDisk(filePath) {
return /dist\/node\//.test(filePath) || /loadable-stats/.test(filePath)
},
}),
)
}

const nodeStats = path.resolve(
__dirname,
'../../public/dist/node/loadable-stats.json',
)

const webStats = path.resolve(
__dirname,
'../../public/dist/web/loadable-stats.json',
)

app.get(
'*',
asyncHandler(async (req, res) => {
const nodeExtractor = new ChunkExtractor({ statsFile: nodeStats })
const { default: App } = nodeExtractor.requireEntrypoint()

const webExtractor = new ChunkExtractor({ statsFile: webStats })
const jsx = webExtractor.collectChunks(<App />)

const html = renderToString(jsx)

res.set('content-type', 'text/html')
res.send(`<!DOCTYPE html>
<html>
<head>
${webExtractor.getLinkTags()}
${webExtractor.getStyleTags()}
</head>
<body>
<div id="main">${html}</div>
${webExtractor.getScriptTags()}
</body>
</html>`)
}),
)

app.listen(8000, () => console.log('Server started http://localhost:8000'))
50 changes: 50 additions & 0 deletions examples/server-side-rendering/webpack.config.babel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import path from 'path'
import nodeExternals from 'webpack-node-externals'
import LoadablePlugin from '@loadable/webpack-plugin'
import MiniCssExtractPlugin from 'mini-css-extract-plugin'

const DIST_PATH = path.resolve(__dirname, 'public/dist')
const production = process.env.NODE_ENV === 'production'
const development =
!process.env.NODE_ENV || process.env.NODE_ENV === 'development'

const getConfig = target => ({
name: target,
mode: development ? 'development' : 'production',
target,
entry: `./src/client/main-${target}.js`,
module: {
rules: [
{
test: /\.js?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
caller: { target },
},
},
},
{
test: /\.css$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
},
'css-loader',
],
},
],
},
externals:
target === 'node' ? ['@loadable/component', nodeExternals()] : undefined,
output: {
path: path.join(DIST_PATH, target),
filename: production ? '[name]-bundle-[chunkhash:8].js' : '[name].js',
publicPath: `/dist/${target}`,
libraryTarget: target === 'node' ? 'commonjs2' : undefined,
},
plugins: [new LoadablePlugin(), new MiniCssExtractPlugin()],
})

export default [getConfig('web'), getConfig('node')]
Loading

0 comments on commit eb1cfe8

Please sign in to comment.