借助Code Splitting 提升单页面应用性能

Monique8936 8年前

来自: http://www.cnblogs.com/E-WALKER/p/5166623.html

近日的工作集中于一个单页面应用(Single-page application),在项目中尝试了闻名已久的Code splitting,收获极大,特此分享。

Why we need code splitting

SPA的客户端路由极大的减少了Server 与 Client端之间的Round trip,在此基础上,我们还可以借助Server Side Rendering 砍掉客户端的初次页面渲染时间(这里是SSR实现的参考链接: ReactAngular2 ).

仍然有一个问题普遍存在着:随着应用复杂度/规模的增加,应用初始所加载的文件大小也随之增加。我们可以通过将文件分割成按需加载的chunks来解决这一问题,对于初始页面,只请求他所用到的模块的相关文件,等我们进入新的路由,或者使用到一些复杂的功能模块时,才加载与之相关的chunk。

借助于 webpackreact-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图:

</div>