webpack解惑:多⼊⼝⽂件打包策略
本⽂是我⽤webpack进⾏项⽬构建的实践⼼得,场景是这样的,项⽬是⼤型类cms型,技术选型是vue,只⽀持chrome,有诸多⼦功能模块,全部打包在⼀起的话会有好⼏MB,所以最佳⽅式是进⾏多⼊⼝打包。⽂章包含我探索的过程以及webpack在使⽤中的⼀些技巧,希望能给⼤家带来参考价值。
⾸先,项⽬打包策略遵循以下⼏点原则:
1. 选择合适的打包粒度,⽣成的单⽂件⼤⼩不要超过500KB
2. 充分利⽤浏览器的并发请求,同时保证并发数不超过6
3. 尽可能让浏览器命中304,频繁改动的业务代码不要与公共代码打包
4. 避免加载太多⽤不到的代码,层级较深的页⾯进⾏异步加载
基于以上原则,我选择的打包策略如下:
1. 第三⽅库如vue、jquery、bootstrap打包为⼀个⽂件
2. 公共组件如弹窗、菜单等打包为⼀个⽂件
3. ⼯具类、项⽬通⽤基类打包为⼀个⽂件
4. 各个功能模块打包出⾃⼰的⼊⼝⽂件
5. 各功能模块作⽤⼀个SPA,⼦页⾯进⾏异步加载
各⼊⼝⽂件的打包
由于项⽬不适宜整体作为⼀个SPA,所以各⼦功能都有⼀个⾃⼰的⼊⼝⽂件,我的源码⽬录结构如下:
apps⽬录下放置各个⼦功能,如question和paper,下⾯是各⾃的⼦页⾯。components⽬录放置公共组件,这个后⾯再说。
由于功能模块是随时会增加的,我不能在webpack的entry中写死这些⼊⼝⽂件,所以⽤了⼀个叫做glob的模块,它能够⽤通配符来取到所有的⽂件,就像我们⽤gulp那样。动态获取⼦功能⼊⼝⽂件的代码如下:
/**
* 动态查所有⼊⼝⽂件
*/
var files = glob.sync('./public/src/apps/*/index.js');
var newEntries = {};
files.forEach(function(f){
var name = /.*\/(apps\/.*?\/index)\.js/.exec(f)[1];//得到apps/question/index这样的⽂件名
newEntries[name] = f;
});
< = Object.assign({}, , newEntries);
webpack打包后的⽬录是很乱的,如果你⼊⼝⽂件的名字取为question,那么会在dist⽬录下直接⽣成⼀个js的⽂件。但是如果把名字取为apps/question/index这样的,则会⽣成对应的⽬录结构。我是⽐较喜欢构建后的⽬录也有清晰的结构的,可能是习惯gulp的后遗症吧。这样也便于我们在前端路由中进⾏统⼀操作。也是⼀个⼩技巧吧,我⽣成的各⼊⼝⽂件的⽬录如下:
第三⽅库的打包
项⽬中⽤到了⼀些第三⽅库,如vue、vue-router、jquery、boostrap等。这些库我们基本上是不会改动源代码的,并且项⽬初期就基本确定了,不会再添加。所以把它们打包在⼀起。当然这个也是要考虑⼤⼩不超过500KB的,如果是⽤到了像ueditor这样的⼤型⼯具库,还是要单独打包的。
配置⽂件的写法是很简单的,在entry中配⼀个名为vendor的就好,⽐如:
entry: {
vendor: ['vue', 'vue-router', './public/vendor/jquery/jquery']
},
不管是⽤npm安装的还是⾃⼰放在项⽬⽬录中的库都是可以的,只要路径写对就⾏。
为了把第三⽅库拆分出来(⽤<script>标签单独加载),我们还需要⽤webpack的CommonsChunkPlugin插件来把它提取⼀下,这样他就不会与业务代码打包到⼀起了。代码:
new webpack.optimize.CommonsChunkPlugin('vendor');
公共组件的打包
这部分代码的处理我是纠结了好久的,因为webpack的打包思想是以模块的依赖树为标准来进⾏分析的,如果a模块使⽤了loading组件,那么loading组件就会被打包进a模块,除⾮我们在代码中⽤sure或者AMD式的require加回调,显式声明该组件异步加载,这样loading组件会被单独打包成⼀个chunk⽂件。
以上两者都不是我想要的,理由参见⽂章开头的打包原则,把所有公共组件打包在⼀起是⼀个⾃然合理的选择,但这⼜与webpack的精神相悖。
⼀开始我想到了⼀招曲线救国,就是在components⽬录下建⼀个main.js⽂件,该⽂件引⽤所有的组件,这样打包main.js的时候所有组件都会被打包进来,main.js的代码如下:
import loading from './loading.vue';
import topnav from './topnav.vue';
import centernav from './centernav.vue';
export {loading, topnav, centernav}
有点像sass的main⽂件的感觉。使⽤的时候这样写:
let components = require('./components/main');
export default {
components: {
loading: (resolve) =>{
require(['./components/main'],function(components){
resolve(components.loading);
})
}
}
}
缺点是也得写成异步加载的,否则main.js还是会被打包进业务代码。
不过后来我⼜⼀想,既然vendor可以,为什么组件不可以⽤同样的⽅式处理呢?于是乎到了最佳⽅法。同样先⽤glob动态到所有的components,然后写进entry,最后再⽤CommonsChunkPlugin插件剥离出来。代码如下:
/*动态查所有components*/
var comps = glob.sync('./public/src/components/*.vue');
var compsEntry = {components: comps};
< = Object.assign({}, , compsEntry);
要注意CommonsChunkPlugin是不可以new多个的,要剥离多个需要传数组进去,写法如下:
new webpack.optimize.CommonsChunkPlugin({
names: ['vendor', 'components']
})
如此⼀来,components就和vendor⼀样可以⽤<script>标签引⼊页⾯了,使⽤的时候就可以随便引⼊了,不会再被重复打包进业务代码。如:
import loading from './components/loading';
import topnav from './components/topnav';
把这些⽂件塞进⼊⼝页⾯
之前说过我们的⼦功能模块有各⾃的页⾯,所以我们需要把这些⽂件都给引⼊进这些页⾯,webpack的HtmlWebpackPlugin可以做这件事情,我们在动态查⼊⼝⽂件的时候顺便把它做了就⾏了,代码如下:
/**
* 动态查所有⼊⼝⽂件
*/
var files = glob.sync('./public/src/apps/*/index.js');
var newEntries = {};
files.forEach(function(f){
var name = /.*\/(apps\/.*?\/index)\.js/.exec(f)[1]; //得到apps/question/index 这样的⽂件名
newEntries[name] = f;
var plug = new HtmlWebpackPlugin({
filename: solve(__dirname, '../public/dist/'+ name +'.html'),
chunks: ['vendor', name, 'components'],
jquery是什么功能组件template: solve(__dirname, '../public/src/index.html'),
inject: true
});
config.plugins.push(plug);
});
⼦页⾯的异步载⼊
每个功能模块是作为⼀个SPA应⽤来处理的,这就意味着我们会根据前端路由来动态加载相应⼦页⾯,使⽤官⽅的vue-router是很容易实现的,⽐如我们在question/index.js中可以如下写:
router.map({
'/list': {
component: (resolve) => {
require(['./list.vue'], resolve);
}
},
'/edit': {
component: (resolve) => {
require(['./edit.vue'], resolve);
}
}
});
在webpack的配置⽂件中就⽆需再写什么了,它会⾃动打包出对应的chunk⽂件,此时我的dist⽬录就长这样了:
有⼀点让我疑惑的是,异步加载的chunk⽂件貌似⽆法输出⽂件名称,尽管我在output参数中这么配置:chunkFilename: '[name]. [chunkhash].js',[name]那⾥输出的还是id,可能和webpack处理异步chunk的机制有关吧,猜测的。不过也⽆所谓的,反正能够正确加载,就是名字难看点。
--------更新于2016.10.11-------
为异步chunk命名的⽅法我到了,需要两步。⾸先output中还是应该这么配置:chunkFilename: '[na
me].[chunkhash].js'。然后,利⽤
router.map({
'/list': {
component: (resolve) => {
// require(['./list.vue'], resolve);
resolve(require('./list.vue'));
}, 'list');
}
},
'/edit': {
component: (resolve) => {
//require(['./edit.vue'], resolve);
resolve(require('./edit.vue'));
}, 'edit');
}
}
});
这样list和edit这两个组件⽣成的chunk就有名字了,如下:
我个⼈还是偏好⽣成的chunk能带上名字,这样可读性好⼀些,便于调试和尽快发现错误。
以上就是⼀个⼤概的架⼦了,由于我也是刚刚开始探索webpack(之前gulp党),⼀边实践⼀边分享吧,还有很多细节的东西没法细讲,我在本系列⽂章中慢慢道来吧。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论