Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

webpack 按需打包加载 #8

Open
eyasliu opened this issue May 29, 2016 · 49 comments
Open

webpack 按需打包加载 #8

eyasliu opened this issue May 29, 2016 · 49 comments

Comments

@eyasliu
Copy link
Owner

eyasliu commented May 29, 2016

为什么需要按需加载

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

使用 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函数在这里只能接受字符串,不能接受变量 。所以还是忍忍算了

@cedcn
Copy link

cedcn commented Jul 9, 2016

屌屌屌

@wefiy
Copy link

wefiy commented Jul 14, 2016

要是三级路由怎么办呢?

@eyasliu
Copy link
Owner Author

eyasliu commented Jul 14, 2016

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

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

@xiaoji201509
Copy link

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

@eyasliu
Copy link
Owner Author

eyasliu commented Jul 19, 2016

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

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

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

@xiaoji201509
Copy link

xiaoji201509 commented Jul 19, 2016

代码结构大概是这样的。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
Copy link
Owner Author

eyasliu commented Jul 19, 2016

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

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

@xiaoji201509
Copy link

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

@xiaoji201509
Copy link

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

@cobish
Copy link

cobish commented Aug 31, 2016

你好,我在 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
Copy link
Owner Author

eyasliu commented Aug 31, 2016

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

@cobish
Copy link

cobish commented Aug 31, 2016

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

@HugoPresents
Copy link

很强!谢谢!

@wikieswan
Copy link

谢谢 有用

@kainy
Copy link

kainy commented Oct 27, 2016

呵呵不错。

@bailicangdu
Copy link

谢谢,很有用🙏

@CommanderXL
Copy link

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

如果有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
Copy link

cobish commented Oct 31, 2016

@eyasliu
Copy link
Owner Author

eyasliu commented Oct 31, 2016

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

@CommanderXL
Copy link

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

@eyasliu
Copy link
Owner Author

eyasliu commented Oct 31, 2016

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

@xuyongtao
Copy link

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

image

react-router的配置如下:

image
image

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

image

@FAOfao931013
Copy link

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

@FAOfao931013
Copy link

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

@FAOfao931013
Copy link

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

@FengHaiSheng
Copy link

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

@eyasliu
Copy link
Owner Author

eyasliu commented Jan 4, 2017

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

@FengHaiSheng
Copy link

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

@mqliutie
Copy link

mqliutie commented Jan 5, 2017

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
Copy link
Owner Author

eyasliu commented Jan 6, 2017

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

@mqliutie
Copy link

mqliutie commented Jan 8, 2017

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

@GZWZC
Copy link

GZWZC commented Mar 29, 2017

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

这样是正常吗?

@FAOfao931013
Copy link

FAOfao931013 commented Mar 29, 2017

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

@Pines-Cheng
Copy link

这个很详细。

@lixingyangok
Copy link

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

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

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

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

@lixingyangok
Copy link

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

@xunv
Copy link

xunv commented May 19, 2017

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

@1215904405a
Copy link

css重复问题 怎么处理

@xiangwenhu
Copy link

厉害了

@LLLQQQ
Copy link

LLLQQQ commented Jul 27, 2017

666

@yinguangyao
Copy link

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

@niwei531769914
Copy link

niwei531769914 commented Sep 20, 2017

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


<IndexRoute component={ Classify }/>//首页



,
Indexs
);
求大神帮忙看下是什么原因

@ayfickle
Copy link

ayfickle commented Nov 9, 2017

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

@ghost
Copy link

ghost commented Mar 5, 2018

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
Copy link

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

@n1203
Copy link

n1203 commented Aug 14, 2020

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

@eyasliu
Copy link
Owner Author

eyasliu commented Aug 14, 2020

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

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

@n1203
Copy link

n1203 commented Aug 18, 2020

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

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

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

@liuliuboy
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests