blog icon indicating copy to clipboard operation
blog copied to clipboard

webpack 按需打包加载

Open eyasliu opened this issue 8 years ago • 49 comments

为什么需要按需加载

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

使用 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演示,方法很简单,就是把 <Route /> 组件的 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函数在这里只能接受字符串,不能接受变量 。所以还是忍忍算了

eyasliu avatar May 29 '16 15:05 eyasliu

屌屌屌

cedcn avatar Jul 09 '16 15:07 cedcn

要是三级路由怎么办呢?

wefiy avatar Jul 14 '16 06:07 wefiy

@wefiy 不管是第几级,一直嵌套下去就是了,写法是一样的

<Route path="......" getComponent={handler}></Route>

eyasliu avatar Jul 14 '16 07:07 eyasliu

但是三级路由写了,this.props.children为undefined,导致页面无法渲染,大神求解答

xiaoji201509 avatar Jul 19 '16 09:07 xiaoji201509

@xiaoji201509 你确定是这样子写的吗

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

注意Route的props是getComponent而不是component,值是一个函数,在函数里面使用 require.ensure 去指定组件

eyasliu avatar Jul 19 '16 09:07 eyasliu

代码结构大概是这样的。TaskRoute 在这个里面取不到 this.props.children,但是在AllRoute 取得到。
class AllRoute extends React.Component{
  render() {
    return (
        <div >
              {this.props.children}
        </div>
    );
  }
}
class TaskRoute extends React.Component{
  constructor(props){
    super(props);
  }
  render() {
    return (
        <div >
             { this.props.children}
        </div>
    );
  }
}
class ListRoute extends React.Component{
  constructor(props){
    super(props);
  }
  render() {
    return (
        <div >
             { this.props.children}
        </div>
    );
  }
}
const Success = (location, callback) => {
  require.ensure([], require => {
    callback(null, require('./components/Success'))
  }, 'Success')  
}

<Provider store={store}> 
    <Router history={history}>
      <Route path="/" component={AllRoute}>
        <Route path="test1" component={TaskRoute}>
            <Route path="success" getComponent={Success}/>
        </Route>
        <Route path="test2" component={ListRoute}>
            .........
        </Route>
      </Route>
    </Router>
  </Provider>

xiaoji201509 avatar Jul 19 '16 10:07 xiaoji201509

@xiaoji201509 这么看来代码是没什么问题的,确定做到下面这几点没有

  • 访问的路由是 /test1/success
  • webpack 配置是否正确,特别是 output.chunkFilename 配置
  • webpack 打包的时候有没有出现 Success 这个 chunk

eyasliu avatar Jul 19 '16 15:07 eyasliu

本地热加载是出现了这个chunk的,但是就是没执行。也没渲染

xiaoji201509 avatar Jul 20 '16 08:07 xiaoji201509

问题解决了,require('./components/Success')改成这样就可以了require('./components/Success').default,

xiaoji201509 avatar Jul 22 '16 05:07 xiaoji201509

你好,我在 webpack.config.js 中定义了 chunkFilename 的命名方式,可是实际生成的 chunkfile 中还是有 id,请问你有遇到过这个问题吗?

我是想生成 [name].[chunkhash:8].chunk.js 这样格式的文件,可是实际生成的是 [id].[name].js 这样的文件。

我的 react-router 的代码如下:

var Movies = function(location, callback) {
  require.ensure([], function(require) {
    callback(null, require('./movies.jsx'));
  }, 'movies');
};

var Movie = function(location, callback) {
  require.ensure([], function(require) {
    callback(null, require('./movie.jsx'));
  }, 'movie');
};

var Books = function(location, callback) {
  require.ensure([], function(require) {
    callback(null, require('./books.jsx'));
  }, 'books');
};

var Book = function(location, callback) {
  require.ensure([], function(require) {
    callback(null, require('./book.jsx'));
  }, 'book');
};

ReactDOM.render((
    <Router history={hashHistory}>
      <Route path="/" component={App}>
        <Route path="movies" getComponent={Movies} />
        <Route path="/movie/:id" getComponent={Movie} />
        <Route path="books" getComponent={Books} />
        <Route path="/book/:id" getComponent={Book} />
      </Route>
    </Router>
  ),
  document.getElementById('main')
);

webpack.config.js 的 output 代码:

output: {
  path: './dist',
  filename: '[name].js',
  chuckFilename: '[name].[chunkhash:8].chunk.js',
  publicPath: './dist/'
},

命令行生成的 chunkfile如下:

bede2596-4f22-4edf-bc65-74ee4947cff3

cobish avatar Aug 31 '16 01:08 cobish

@cobish 你写错单词了,正确是 chunkFilename 你写成了 chuckFilename

eyasliu avatar Aug 31 '16 05:08 eyasliu

@eyasliu 噢原来如此,真是太感谢你了!

cobish avatar Aug 31 '16 05:08 cobish

很强!谢谢!

HugoPresents avatar Sep 16 '16 10:09 HugoPresents

谢谢 有用

wikieswan avatar Oct 23 '16 12:10 wikieswan

呵呵不错。

kainy avatar Oct 27 '16 09:10 kainy

谢谢,很有用🙏

bailicangdu avatar Oct 29 '16 08:10 bailicangdu

在开发过程当中还遇到一个问题:

如果有2个异步加载的页面:

require.ensure([], function() {
    require('modules/A');
})

require.ensure([], function() {
    require('modules/B');
})

其中A,B模块都共同引用了模块C,那么在打包过程中,webpack会将A,C打包在一起,同时还会在B,C打包在一起。

虽然webpack提供了CommonChunkPlugin插件,但是这个插件是将entry里面的共同的模块抽离出来打包。它没法去分析require.ensure([], funciton() {}) 异步加载模块里面的共同模块,然后去打包。

这样就造成了重复打包的情况。请问遇到这种问题,有什么比较好的方法去解决呢?

CommanderXL avatar Oct 30 '16 10:10 CommanderXL

@CommanderXL 你可以在基础包中引用一下 C 包,这样就会将 C 包打进基础包中,在 A 和 B 模块,就不会在将 C 打包进去了。或者像 @cobish 给的 demo 那样也行,专门用一个 entry 来打包需要重复用到的模块,不过这样会多出一个需要手动引入的包,但是这样对于以后的增量升级也是有好处的

eyasliu avatar Oct 31 '16 01:10 eyasliu

@cobish @eyasliu 对应到具体的业务上来看的话,我的理解是将一写工具模块可以单独打一个包,可以放到entry里面引入,具体到不同页面的业务逻辑的话,可以通过require.ensure([], function(){})这种方式进行按需加载。这种方式是否合理呢?

CommanderXL avatar Oct 31 '16 01:10 CommanderXL

@CommanderXL 这种方式是可以的,将一些完全跟业务逻辑无关的工具模块打一个包,可以跨项目使用。将有业务逻辑的模块打包成各个小模块按需加载

eyasliu avatar Oct 31 '16 02:10 eyasliu

请教一下各位,我有个404页面component,import了一个less文件(index.less)代码如下:

image

react-router的配置如下:

image image

执行webpack确没有生成404.css的chunk文件,我想请问是哪里有问题?谢谢!我已经用extract-text-webpack-plugin来处理import进来的less文件。

image

xuyongtao avatar Nov 25 '16 12:11 xuyongtao

想问下 我按这样写了之后 一切是正常运行的 但是 怎么样 看出项目是按需加载的?

FAOfao931013 avatar Dec 26 '16 15:12 FAOfao931013

我发现 我这样写了之后 并没有 按需加载啊。。。是什么情况? 我的项目

FAOfao931013 avatar Dec 27 '16 06:12 FAOfao931013

解决了,自己代码的问题,在路由这里异步加载过,就不需要在其他地方 同步加载了,否则会自动去掉异步的方法

FAOfao931013 avatar Dec 27 '16 07:12 FAOfao931013

webpack.config.js 的 entry 该如何配置呢?

FengHaiSheng avatar Jan 04 '17 07:01 FengHaiSheng

@FengHaiSheng entry不需要其他特殊配置

eyasliu avatar Jan 04 '17 07:01 eyasliu

@eyasliu 非常感谢回答。我照着教程试了,发现确实达到了按需加载的功能。只有一点比较不懂,虽然做了按需加载,但是以前那个文件(所有代码都打包到了这个文件)仍然被加载了(我在network中看到的)

FengHaiSheng avatar Jan 04 '17 10:01 FengHaiSheng

childRoutes: [{
        path: '/welcome',
        getComponents: (location, callback) => require.ensure([], require => {callback(null, require("./components/welcome/Welcome.react.jsx").default)},'welcome')
    },{
        path: '/menu',
        getComponents: (location, callback) => require.ensure([], require => {callback(null, require("./components/menu/MenuMain.react").default)},'menu')
    },{
        path: '/combo/:menuId/:isInclude/:combo_carts/:menu_item',
        getComponents: (location, callback) => require.ensure([], require => {callback(null, require("./components/menu/MenuComboMain.react").default)},'combo')
    }]

这样配置, 除了'/menu' 剩下的进入对应的路由都有单独的文件生成,唯独 menu 没有生成单独的文件,在 打包好的文件里搜 menu 组件的内容,发现在输出的那个文件中. 这是怎么回事呢,写法完全相同啊

mqliutie avatar Jan 05 '17 09:01 mqliutie

@mqliutie 可能是menu里面所有引用的包都在基础包中引用过,所以menu就不需要了

eyasliu avatar Jan 06 '17 05:01 eyasliu

@eyasliu 不会的,menu这个组建里面有我自定义的组建,其他文件中没有引入的

mqliutie avatar Jan 08 '17 02:01 mqliutie

你好,我有一个项目,entry入口,有8个路由页面,我没有用按需加载的时候直接在index.html里引入bundle.js,bundle.js大小为2.8M。 如果我用了按需加载后,我还需要在index.html引用bundle.js吗?如果不引用的话,页面没有加载任何js,如果引用了,network里的先加载bundle.js(2.2M),再引入对应页面的chunk.js。

这样是正常吗?

GZWZC avatar Mar 29 '17 06:03 GZWZC

@GZWZC 我认为是正常的 我的页面也是这样的

FAOfao931013 avatar Mar 29 '17 06:03 FAOfao931013

这个很详细。

Pines-Cheng avatar Apr 17 '17 17:04 Pines-Cheng

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

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

———————————————— 我看到上面代码后还挺高兴的:“这多方便”,后来才看到:

答案是:不能。这样看起来代码没有任何问题,好像更优雅的样子,但是经过亲自实践后,不行!!

lixingyangok avatar May 07 '17 08:05 lixingyangok

非常感谢老师的分享,这个帖子给我解开了积压许久的困惑。 通过这个文章,经过几番尝试,终于把路由拆分了。 这个帖子开头的部分的一些理论铺垫起到了由其关键的作用。 这个帖子是真的从“头”讲起,由“浅”入深。真希望所有的教程都像这篇一样有基础理论铺垫。 再次感谢您的分享。3Q 好人一生平安,老司机永远顺风……

lixingyangok avatar May 07 '17 09:05 lixingyangok

@CommanderXL 你好,我也遇到了你上述问到的代码分割后多次引入模块(比如echart)重复打包的问题,请问,你这边最后用什么方法处理的?

xunv avatar May 19 '17 09:05 xunv

css重复问题 怎么处理

1215904405a avatar Jun 30 '17 02:06 1215904405a

厉害了

xiangwenhu avatar Jul 25 '17 05:07 xiangwenhu

666

LLLQQQ avatar Jul 27 '17 07:07 LLLQQQ

弱弱问句,就是每次webpack打包后会生成对应的文件,但是再执行一次webpack命令后又重新打包了,打包出来的文件就是命名的hash值不一样,这个怎么解决。。 image 就是类似于这种,我执行了一次webpack命令后,退出后又执行了两次,就打了三次包

yinguangyao avatar Aug 31 '17 15:08 yinguangyao

请问一下,我用webpack配置了一个多页面的开发环境,但是我又在一个页面中用路由的形式来配一个路由页面,如果不用懒加载js的话是正常的,但是如果用了懒加载的,发现js没有打包出来。

//首页 const index = (location, callback) => { require.ensure([], require => { callback(null, require('./containers/lehu.h5.container.index').default) }, 'index') }; //分类 const classify = (location, callback) => { require.ensure([], require => { callback(null, require('./containers/lehu.h5.container.classify').default) }, 'classify') }; let Indexs = document.getElementById('index'); render( <Router history={hashHistory} > <Route path="/" component={Roots}> <IndexRoute component={ Classify }/>//首页 <Route path="index" getComponent={Index}/> <Route path="classify" getComponent={Classify}/> </Route> </Router>, Indexs ); 求大神帮忙看下是什么原因

niwei531769914 avatar Sep 20 '17 13:09 niwei531769914

@yinguangyao 可以用 clean-webpack-plugin 在打包前清楚之前打包的文件重新生成。 output: { filename: '[name]-[chunkhash].js', publicPath: BUILD_PATH }, 然后如果打包多个,配置output的filename 是 [name]-[chunkhash].js 格式。 然后用webpack的HashedModuleIdsPlugin插件,嗯,可以检测不变动未经修改的文件的hash名。

ayfickle avatar Nov 09 '17 04:11 ayfickle

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

现在函数里包裹 require 会报下面的错误,有解决的办法吗?

Critical dependency: require function is used in a way in which dependencies cannot be statically extracted

ghost avatar Mar 05 '18 16:03 ghost

异步组件a中再异步加载b会怎样,还是打出一个包a还是两个包a和b

humorHan avatar Jun 17 '20 10:06 humorHan

组件库很庞大,但是用到了某些独立的依赖,并且这些依赖随时可以用到,但是又不会经常使用,而且体积也比较大。这种情况没办法使用路由的方式按需加载,必须判断对应的部分是否使用再进行加载。有招吗

n1203 avatar Aug 14 '20 08:08 n1203

@SouWinds 新版本的webpack有 import 函数可以做按需加载,你可以这样

// 在需要使用那个组件的时候,才执行import
import('./your/mod').then(() => {
    // 组件模块加载完成
})

eyasliu avatar Aug 14 '20 08:08 eyasliu

@SouWinds 新版本的webpack有 import 函数可以做按需加载,你可以这样

// 在需要使用那个组件的时候,才执行import
import('./your/mod').then(() => {
    // 组件模块加载完成
})

我试试看,我查了下文档,要最小化搜索范围、缩小变量控制区域

n1203 avatar Aug 18 '20 03:08 n1203

webpack 懒加载使用 import 引入 js 文件么有生成 chunk 文件

liuliuboy avatar Mar 17 '22 13:03 liuliuboy