近日的工作集中於一個單頁面應用(Single-page application),在項目中嘗試了聞名已久的Code splitting,收獲極大,特此分享。
Why we need code splitting
SPA的客戶端路由極大的減少了Server 與 Client端之間的Round trip,在此基礎上,我們還可以借助Server Side Rendering 砍掉客戶端的初次頁面渲染時間(這里是SSR實現的參考鏈接:React,Angular2).
仍然有一個問題普遍存在着:隨着應用復雜度/規模的增加,應用初始所加載的文件大小也隨之增加。我們可以通過將文件分割成按需加載的chunks來解決這一問題,對於初始頁面,只請求他所用到的模塊的相關文件,等我們進入新的路由,或者使用到一些復雜的功能模塊時,才加載與之相關的chunk。
借助於webpack與react-router(目前我的應用是基於React開發的),我們可以快速實現這些按需加載的chunks。
webpack
Webpack是非常火的一個module bundler,這里是一個很好的入門參考鏈接。
我們可以借助代碼中定義split point以創建按需加載的chunk。
使用require.ensure(dependencies, callback)可以加載 CommonJs modules, 使用require(dependencies, callback)加載 AMD modules。webpack會在build過程中檢測到這些split points,創建chunks。
React router
React router 是一個基於React且非常流行的客戶端路由庫。
我們能以plain JavaScript object或者declaratively的形式定義客戶端路由。
Plain JavaScript way:
let myRoute = {
path: `${some path}`,
childRoutes: [
RouteA,
RouteB,
RouteC,
]
}
declaratively way:
const routes = (
<Route component={Component}>
<Route path="pathA" component={ComponentA}/>
<Route path="pathB" component={ComponentB}/>
</Route>
)
React router 可以實現代碼的lazy load, 而我們正好可以把split points 定義在這些lazy load code中(參考鏈接)。
Code Splitting implement
below is a demo of create two on demand loaded chunks, chunk A will load once when enter rootUrl/A, chunk B will load once when enter rootUrl/B.
接下來的代碼就是創建按需加載的chunks的例子,chunk A 只有當進入rootUrl/A才會加載,chunk B 只有當進入rootUrl/B才會加載。
routes
/* --- RootRoute --- */
...
import RouteA from './RouteA'
import RouteB from './RouteB'
export default {
path: '/',
component: App,
childRoutes: [
RouteA,
RouteB,
],
indexRoute: {
component: Index
}
}
/* --- RouteA --- */
...
export default {
path: 'A',
getComponent(location, cb) {
require.ensure([], (require) => {
cb(null, require(`${PathOfRelatedComponent}`))
}, 'chunkA')
}
}
/* --- RouteB --- */
...
export default {
path: 'B',
getComponent(location, cb) {
require.ensure([], (require) => {
cb(null, require(`${PathOfRelatedComponent}`))
}, 'chunkB')
}
}
client side code for client side render
...
import { match, Router } from 'react-router'
const { pathname, search, hash } = window.location
const location = `${pathname}${search}${hash}`
//use match to trigger the split code to load before rendering.
match({ routes, location }, () => {
render(
<Router routes={routes} history={createHistory()} />,
document.getElementById('app')
)
})
server code for server side rendering
...
app.createServer((req, res) => {
match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
if (error)
writeError('ERROR!', res)
else if (redirectLocation)
redirect(redirectLocation, res)
else if (renderProps)
renderApp(renderProps, res)
else
writeNotFound(res)
}).listen(PORT)
function renderApp(props, res) {
const markup = renderToString(<RoutingContext {...props}/>)
const html = createPage(markup)
write(html, 'text/html', res)
}
export function createPage(html) {
return `
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>My Universal App</title>
</head>
<body>
<div id="app">${html}</div>
<script src="/__build__/main.js"></script>
</body>
</html>
`
}
實現中可能會遇到的坑
取決於你是如何寫自己的模塊的,你可能會遇到這個錯誤:React.createElement: type should not be null, undefined, boolean, or number. It should be a string (for DOM elements) or a ReactClass (for composite components). Check the render method of RoutingContext.在require()之后加一個.default即可。
如果你收到了這樣的錯誤提示:require.ensure is not function, 增加一個polyfill即可: if (typeof require.ensure !== 'function') require.ensure = (d, c) => c(require),在Server端使用require來代替require.ensure.
謝謝,希望能指正我的錯誤!
最后附一張目前項目的chunks圖:

