eyasliu / blog

博客文章
179 stars 13 forks source link

webpack 按需打包加载 #8

Open eyasliu opened 8 years ago

eyasliu commented 8 years ago

为什么需要按需加载

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

使用 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 的第三个参数

配置文件中

// 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 是一个路由解决方案的第一选择,它本身就有一套动态加载的方案

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

根路由

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

示例代码

官方有个很简易明了的示例应用, 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函数在这里只能接受字符串,不能接受变量 。所以还是忍忍算了

cedcn commented 8 years ago

屌屌屌

wefiy commented 8 years ago

要是三级路由怎么办呢?

eyasliu commented 8 years ago

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

<Route path="......" getComponent={handler}></Route>
xiaoji201509 commented 8 years ago

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

eyasliu commented 8 years ago

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

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

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

xiaoji201509 commented 8 years ago
代码结构大概是这样的。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>
eyasliu commented 8 years ago

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

xiaoji201509 commented 8 years ago

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

xiaoji201509 commented 8 years ago

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

cobish commented 8 years ago

你好,我在 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

eyasliu commented 8 years ago

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

cobish commented 8 years ago

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

HugoPresents commented 8 years ago

很强!谢谢!

wikieswan commented 8 years ago

谢谢 有用

kainy commented 8 years ago

呵呵不错。

bailicangdu commented 8 years ago

谢谢,很有用🙏

CommanderXL commented 8 years ago

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

如果有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() {}) 异步加载模块里面的共同模块,然后去打包。

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

cobish commented 8 years ago

@CommanderXL 试一下这个 https://github.com/cobish/demo/blob/master/webpack-common-vendor/webpack.config.js#L16

eyasliu commented 8 years ago

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

CommanderXL commented 8 years ago

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

eyasliu commented 8 years ago

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

xuyongtao commented 7 years ago

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

image

react-router的配置如下:

image image

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

image

FAOfao931013 commented 7 years ago

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

FAOfao931013 commented 7 years ago

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

FAOfao931013 commented 7 years ago

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

FengHaiSheng commented 7 years ago

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

eyasliu commented 7 years ago

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

FengHaiSheng commented 7 years ago

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

mqliutie commented 7 years ago
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 组件的内容,发现在输出的那个文件中. 这是怎么回事呢,写法完全相同啊

eyasliu commented 7 years ago

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

mqliutie commented 7 years ago

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

GZWZC commented 7 years ago

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

这样是正常吗?

FAOfao931013 commented 7 years ago

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

Pines-Cheng commented 7 years ago

这个很详细。

lixingyangok commented 7 years ago

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

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

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

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

lixingyangok commented 7 years ago

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

xunv commented 7 years ago

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

1215904405a commented 7 years ago

css重复问题 怎么处理

xiangwenhu commented 7 years ago

厉害了

LLLQQQ commented 7 years ago

666

yinguangyao commented 7 years ago

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

niwei531769914 commented 7 years ago

请问一下,我用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(

//首页 , Indexs ); 求大神帮忙看下是什么原因
ayfickle commented 6 years ago

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

ghost commented 6 years ago

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

humorHan commented 4 years ago

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

n1203 commented 4 years ago

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

eyasliu commented 4 years ago

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

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

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

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

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

liuliuboy commented 2 years ago

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