webpack 按需打包加载

@eyasliu 2016-05-29 15:59:05发表于 eyasliu/blog react前端

为什么需要按需加载

在一个前端应用中,将所有的代码都打包进一个或几个文件中,加载的时候,把所有文件都加载进来,然后执行我们的前端代码。只要我们的应用稍微的复杂一点点,包括依赖后,打包后的文件都是挺大的。而我们加载的时候,不管那些代码有没有执行到,都会下载下来。如果说,我们 只下载我们需要执行的代码的 话,那么可以节省相当大的流量。也就是我们所说的 按需加载

使用 webpack 的按需加载

webpack 官方文档 其实是有介绍的,不过我还是啰嗦的在总结一下

首先我们要看一看一个加载函数

require.ensure(dependencies, callback, chunkName)

这个方法可以实现js的按需加载,分开打包,webpack 管包叫 chunk,为了打包能正常输出,我们先给webpack配置文件配置一下chunk文件输出路径

// webpack.config.js
module.exports = {
  ...
  output: {
    ...
    chunkFilename: '[name].[chunkhash:5].chunk.js',
    publicPath: '/dist/'
  }
  ...
}

这里顺带一提,打包后的js文件基础路径跟普通的资源(图片或字体文件之类)是一样的,就是publicPath, publicPath可以在运行时再去赋值,方法就是在应用入口文件对变量 __webpack_public_path__ 进行赋值就行,文档在这

每个chunk 都会有一个ID,会在webpack内部生成,当然我们也可以给chunk指定一个名字,就是 require.ensure 的第三个参数

配置文件中

  • [name] 默认是 ID,如果指定了chunkName则为指定的名字。
  • [chunkhash] 是对当前chunk 经过hash后得到的值,可以保证在chunk没有变化的时候hash不变,文件不需要更新,chunk变了后,可保证hash唯一,由于hash太长,这里我截取了hash的5个字符足矣

最简单的例子

// a.js
console.log('a');

// b.js
console.log('b');

// c.js
console.log('c');

// entry.js
require.ensure([], () => {
  require('./a');
  require('./b');
}, 'chunk1');
if(false){
  require.ensure([], () => {
    require('./c');
  }, 'chunk2');
}

将会打包出 3 个文件,基础包、chunk1 和 chunk2,但是chunk2在if判断中,而且永远为false,所以 chunk2 虽然打包了但永远不会被加载

结合 react-router 按需加载

如果需要做按需加载,那么这个 应该怎样定义呢?我们可以按照前端路由来定义这个 ,在react 应用中,react-router 是一个路由解决方案的第一选择,它本身就有一套动态加载的方案

  • getChildRoutes
  • getIndexRoute
  • getComponents

看他们的方法名字就知道他们是干什么的,我也不废话。他们的作用呢,就是在访问到了对应的路由的时候,才会去执行这个函数,如果没有访问到,那么就不会执行。那么我们把加载的函数放在里面就正好合适了,等到访问了该路由的时候,再去执行函数去加载脚本。

根路由

跟路由有点特殊,它一定要先加载一个组件才能渲染,也就是说,在跟路由不能使用按需加载方式,不过这个没关系,根路由用于基础路径,在所有模块都必须加载,所以他的 "需" 其实作用不大。

示例代码

官方有个很简易明了的示例应用, react-router 默认是推荐使用对象去定义路由而不是 jsx,所以这个例子演示了怎么使用 对象的形式定义按需加载模块。

jsx 定义按需加载路由

虽然官方推荐使用对象去定义,但是jsx语法看上去更清晰点,所以还是使用jsx演示,方法很简单,就是把 组件的 props.component 换成 props.getComponent ,函数还是上述例子的函数(记得根路由不要使用getComponent)。

<Router history={history}>
  <Route path="/" component={App}>
    <Route path="home" getComponent={(location, callback) => {
      require.ensure([], require => {
        callback(null, require('modules/home'))
      }, 'home')  
    }}></Route>
    <Route path="blog" getComponent={(location, callback) => {
      require.ensure([], require => {
        callback(null, require('modules/blog'))
      }, 'blog')  
    }}></Route>
  </Route>
</Router>

看上去很乱有木有,在jsx中写那么多 js 感觉真难看,把 js 独立出来就是:

const home = (location, callback) => {
  require.ensure([], require => {
    callback(null, require('modules/home'))
  }, 'home')  
}

const blog = (location, callback) => {
  require.ensure([], require => {
    callback(null, require('modules/blog'))
  }, 'blog')  
}

<Router history={history}>
  <Route path="/" component={App}>
    <Route path="home" getComponent={home}></Route>
    <Route path="blog" getComponent={blog}></Route>
  </Route>
</Router>

这样整理一下,就好看多了


注意: 或许有人会想,上面重复代码超级多,能不能用一个函数生成器去生成这些重复的函数呢?代码更进一步优化,比如:

const ensureModule = (name, entry) => (location, callback) => {
  require.ensure([], require => {
    callback(null, require(entry))
  }, name)
}

<Router history={history}>
  <Route path="/" component={App}>
    <Route path="home" getComponent={ensureModule('home', 'modules/home')}></Route>
    <Route path="blog" getComponent={ensureModule('blog', 'modules/blog')}></Route>
  </Route>
</Router>

答案是:不能。这样看起来代码没有任何问题,好像更优雅的样子,但是经过亲自实践后,不行!!因为 require函数太特别了,他是webpack底层用于加载模块,所以必须明确的声明模块名,require函数在这里只能接受字符串,不能接受变量 。所以还是忍忍算了