2018/2/25,webpack4正式发布,距离如今已通过去三个多月了,也逐渐趋于稳定,并且如今的最新版本都到了4.12.0(版本迭代快得真是让人惧怕)。javascript
不少人都说webpack复杂,难以理解,很大一部分缘由是webpack是基于配置的,可配置项不少,而且每一个参数传入的形式多种多样(能够是字符串、数组、对象、函数。。。),文档介绍也比较模糊,这么多的配置项各类排列组合,想一想都复杂。而gulp基于流的方式来处理文件,不管从理解上,仍是功能上都很容易上手。css
//gulp gulp.src('./src/js/**/*.js') .pipe('babel') .pipe('uglifyjs') .dest('./dist/js') //webpack module.exports = { entry: './src/main.js', output: __dirname + '/dist/app.js', module: { rules: [{ test: /\.js$/, loader: 'babel-loader' }] }, plugins: [ new require('uglifyjs-webpack-plugin')() ] }
上面简单对比了webpack与gulp配置的区别,固然这样比较是有问题的,gulp并不能进行模块化的处理。这里主要是想告诉你们使用gulp的时候,咱们能明确的知道js文件是先进行babel转译,而后进行压缩混淆,最后输出文件。而webpack对咱们来讲彻底是个黑盒,彻底不知道plugins的执行顺序。正是由于这些缘由,咱们经常在使用webpack时有一些不安,不知道这个配置到底有没有生效,我要按某种方式打包到底该如何配置?html
为了解决上面的问题,webpack4引入了零配置
的概念(Parcel ???),实际体验下来仍是要写很多配置。
可是这不是重点,重点是官方宣传webpack4可以提高构建速度60%-98%,真的让人心动。前端
首先安装最新版的webpack和webpack-dev-server,而后再安装webpack-cli。webpack4将命令行相关的操做抽离到了webpack-cli中,因此,要使用webpack4,必须安装webpack-cli。固然,若是你不想使用webpack-cli,社区也有替代方案webpack-command,虽然它与webpack-cli区别不大,可是仍是建议使用官方推荐的webpack-cli。vue
npm i webpack@4 webpack-dev-server@3 --save-dev npm i webpack-cli --save-dev
webpack-cli除了能在命令行接受参数运行webpack外,还具有migrate
和init
功能。java
$ webpack-cli migrate ./webpack.config.js ✔ Reading webpack config ✔ Migrating config from v1 to v2 - loaders: [ + rules: [ - loader: 'babel', - query: { + use: [{ + loader: 'babel-loader' + }], + options: { - loader: ExtractTextPlugin.extract('style', 'css!sass') + use: ExtractTextPlugin.extract({ + fallback: 'style', + use: 'css!sass' + }) ? Are you sure these changes are fine? Yes ✔︎ New webpack v2 config file is at /home/webpack-cli/build/webpack.config.js
webpack-cli init 1. Will your application have multiple bundles? No // 若是是多入口应用,能够传入一个object 2. Which module will be the first to enter the application? [example: './src/index'] ./src/index // 程序入口 3. What is the location of "app"? [example: "./src/app"] './src/app' 4. Which folder will your generated bundles be in? [default: dist] 5. Are you going to use this in production? No 6. Will you be using ES2015? Yes //是否使用ES6语法,自动添加babel-loader 7. Will you use one of the below CSS solutions? SASS // 根据选择的样式类型,自动生成 loader 配置 8. If you want to bundle your CSS files, what will you name the bundle? (press enter to skip) 9. Name your 'webpack.[name].js?' [default: 'config']: // webpack.config.js Congratulations! Your new webpack configuration file has been created!
更详细介绍请查看webpack-cli的文档node
零配置就意味着webpack4具备默认配置,webpack运行时,会根据mode
的值采起不一样的默认配置。若是你没有给webpack传入mode,会抛出错误,并提示咱们若是要使用webpack就须要设置一个mode。react
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/concep...
mode一共有以下三种配置:webpack
这个配置的意思就是不使用任何默认配置git
module.exports = { //开发环境下默认启用cache,在内存中对已经构建的部分进行缓存 //避免其余模块修改,可是该模块未修改时候,从新构建,可以更快的进行增量构建 //属于空间换时间的作法 cache: true, output: { pathinfo: true //输入代码添加额外的路径注释,提升代码可读性 }, devtools: "eval", //sourceMap为eval类型 plugins: [ //默认添加NODE_ENV为development new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("development") }), ], optimization: { namedModules: true, //取代插件中的 new webpack.NamedModulesPlugin() namedChunks: true } }
module.exports = { performance: { hints: 'warning', maxAssetSize: 250000, //单文件超过250k,命令行告警 maxEntrypointSize: 250000, //首次加载文件总和超过250k,命令行告警 } plugins: [ //默认添加NODE_ENV为production new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("production") }) ], optimization: { minimize: true, //取代 new UglifyJsPlugin(/* ... */) providedExports: true, usedExports: true, //识别package.json中的sideEffects以剔除无用的模块,用来作tree-shake //依赖于optimization.providedExports和optimization.usedExports sideEffects: true, //取代 new webpack.optimize.ModuleConcatenationPlugin() concatenateModules: true, //取代 new webpack.NoEmitOnErrorsPlugin(),编译错误时不打印输出资源。 noEmitOnErrors: true } }
其余的一些默认值:
module.exports = { context: process.cwd() entry: './src', output: { path: 'dist', filename: '[name].js' }, rules: [ { type: "javascript/auto", resolve: {} }, { test: /\.mjs$/i, type: "javascript/esm", resolve: { mainFields: options.target === "web" || options.target === "webworker" || options.target === "electron-renderer" ? ["browser", "main"] : ["main"] } }, { test: /\.json$/i, type: "json" }, { test: /\.wasm$/i, type: "webassembly/experimental" } ] }
若是想查看更多webpack4相关的默认配置,到这里来。能够看到webpack4把不少插件相关的配置都迁移到了optimization中,可是咱们看看官方文档对optimization的介绍简直寥寥无几,而在默认配置的代码中,webpack对optimization的配置有十几项,反正我是怕了。
虽然api发生了一些变化,好的一面就是有了这些默认值,咱们想经过webpack构建一个项目比之前要简单不少,若是你只是想简单的进行打包,在package.json中添加以下两个script,包你满意。
{ "scripts": { "dev": "webpack-dev-server --mode development", "build": "webpack --mode production" }, }
开发环境使用webpack-dev-server,边预览边打包不再用f5,简直爽歪歪;生产环境直接生成打包后的文件到dist目录
loader的升级就是一次大换血,以前适配webpack3的loader都须要升级才能适配webpack4。若是你使用了不兼容的loader,webpack会告诉你:
DeprecationWarning: Tapable.apply is deprecated. Call apply on the plugin directly insteadDeprecationWarning: Tapable.plugin is deprecated. Use new API on
.hooks
instead
若是在运行过程当中遇到这两个警告,就表示你有loader或者plugin没有升级。形成这两个错误的缘由是,webpack4使用的新的插件系统,而且破坏性的对api进行了更新,不过好在这只是警告,不会致使程序退出,不过建议最好是进行升级。对于loader最好所有进行一次升级,反正也不亏,百利而无一害。
关于plugin,有两个坑,一个是extract-text-webpack-plugin
,还一个是html-webpack-plugin
。
先说说extract-text-webpack-plugin
,这个插件主要用于将多个css合并成一个css,减小http请求,命名时支持contenthash(根据文本内容生成hash)。可是webpack4使用有些问题,因此官方推荐使用mini-css-extract-plugin
。
⚠️ Since webpack v4 the extract-text-webpack-plugin should not be used for css. Use mini-css-extract-plugin instead.
这里改动比较小,只要替换下插件,而后改动下css相关的loader就好了:
-const ExtractTextPlugin = require('extract-text-webpack-plugin') +const MiniCssExtractPlugin = require('mini-css-extract-plugin') module.exports = { module: { rules: [ { test: /\.css$/, - use: ExtractTextPlugin.extract({ - use: [{ - loader: 'css-loader', - options: { - minimize: process.env.NODE_ENV === 'production' - } - }], - fallback: 'vue-style-loader' - }) + use: [ + MiniCssExtractPlugin.loader, + { + loader: 'css-loader', + options: { + minimize: process.env.NODE_ENV === 'production' + } + ], } ] }, plugins:[ - new ExtractTextPlugin({ + new MiniCssExtractPlugin({ filename: 'css/[name].css', }), ... ] }
而后看看html-webpack-plugin
,将这个插件升级到最新版本,通常状况没啥问题,可是有个坑,最好是把chunksSortMode
这个选项设置为none。
const HtmlWebpackPlugin = require('html-webpack-plugin') module.exports = { plugins:[ new HtmlWebpackPlugin({ filename: 'index.html', template: 'index.html', inject: true, hash: true, chunksSortMode: 'none' //若是使用webpack4将该配置项设置为'none' }) ] }
官方有个issues讨论了这个问题,感兴趣能够去看看。目前做者还在寻找解决方案中。
另外,webpack-dev-server也有个升级版本,叫作webpack-serve,功能比webpack-dev-server强大,支持HTTP二、使用WebSockets作热更新,暂时还在观望中,后续采坑。
webpack3中,咱们常用CommonsChunkPlugin
进行模块的拆分,将代码中的公共部分,以及变更较少的框架或者库提取到一个单独的文件中,好比咱们引入的框架代码(vue、react)。只要页面加载过一次以后,抽离出来的代码就能够放入缓存中,而不是每次加载页面都从新加载所有资源。
CommonsChunkPlugin的常规用法以下:
module.exports = { plugins: [ new webpack.optimize.CommonsChunkPlugin({ //将node_modules中的代码放入vendor.js中 name: "vendor", minChunks: function(module){ return module.context && module.context.includes("node_modules"); } }), new webpack.optimize.CommonsChunkPlugin({ //将webpack中runtime相关的代码放入manifest.js中 name: "manifest", minChunks: Infinity }), ] }
以前CommonsChunkPlugin
虽然能用,可是配置不够灵活,难以理解,minChunks有时候为数字,有时候为函数,而且若是同步模块与异步模块都引入了相同的module并不能将公共部分提取出来,最后打包生成的js仍是存在相同的module。
如今webpack4使用optimization.splitChunks
来进行代码的拆分,使用optimization.runtimeChunk
来提取webpack的runtime代码,引入了新的cacheGroups
概念。而且webpack4中optimization提供以下默认值,官方称这种默认配置是保持web性能的最佳实践,不要手贱去修改,就算你要改也要多测试(官方就是这么自信)。
module.exports = { optimization: { minimize: env === 'production' ? true : false, //是否进行代码压缩 splitChunks: { chunks: "async", minSize: 30000, //模块大于30k会被抽离到公共模块 minChunks: 1, //模块出现1次就会被抽离到公共模块 maxAsyncRequests: 5, //异步模块,一次最多只能被加载5个 maxInitialRequests: 3, //入口模块最多只能加载3个 name: true, cacheGroups: { default: { minChunks: 2, priority: -20 reuseExistingChunk: true, }, vendors: { test: /[\\/]node_modules[\\/]/, priority: -10 } } }, runtimeChunk { name: "runtime" } } }
有了这些默认配置,咱们几乎不须要任何成功就能删除以前CommonChunkPlugin的代码,好神奇。
经过判断splitChunks.chunks
的值来肯定哪些模块会提取公共模块,该配置一共有三个选项,initial
、async
、 all
。
默认为async,表示只会提取异步加载模块的公共代码,initial表示只会提取初始入口模块的公共代码,all表示同时提取前二者的代码。
这里有个概念须要明确,webpack中什么是初始入口模块,什么是异步加载模块。e.g.
//webpack.config.js module.exports = { entry: { main: 'src/index.js' } } //index.js import Vue from 'vue' import(/* webpackChunkName: "asyncModule" */'./a.js') .then(mod => { console.log('loaded module a', mod) }) console.log('initial module') new Vue({}) //a.js import _ from 'lodash' const obj = { name: 'module a' } export default _.clone(obj)
上面的代码中,index.js
在webpack的entry配置中,这是打包的入口,因此这个模块是初始入口模块。再看看index.js
中使用了动态import语法,对a.js
(该异步模块被命名为asyncModule)进行异步加载,则a.js
就是一个异步加载模块。再看看index.js
和a.js
都有来自node_modules
的模块,按照以前的规则,splitChunks.chunks默认为async
,因此会被提取到vendors中的只有webpackChunkName中的模块。
若是咱们把splitChunks.chunks改为all,main中来自node_modules
的模块也会被进行提取了。
module.exports = { optimization: { splitChunks: { chunks: "all" } } }
如今咱们在index.js
中也引入lodash,看看入口模块和异步模块的公共模块还会不会像CommonsChunkPlugin同样被重复打包。
//index.js import Vue from 'vue' import _ from 'lodash' import(/* webpackChunkName: "asyncModule" */'./a.js') .then(mod => { console.log('loaded module a', mod) }) console.log('initial module') console.log(_.map([1,2,3], a => { return a * 10 })) new Vue({}) //a.js import _ from 'lodash' const obj = { name: 'module a' } export default _.clone(obj)
能够看到以前CommonsChunkPlugin的问题已经被解决了,main模块与asyncModule模块共同的lodash都被打包进了vendors~main.js
中。
splitChunks.cacheGroups
配置项就是用来表示,会提取到公共模块的一个集合,也就是一个提取规则。像前面的vendor
,就是webpack4默认提供的一个cacheGroup,表示来自node_modules的模块为一个集合。
除了cacheGroups配置项外,能够看下其余的几个默认规则。
对应到代码中就是这四个配置:
{ minSize: 30000, minChunks: 1, maxAsyncRequests: 5, maxInitialRequests: 3, }
webpack是一个基于node的前端打包工具,可是node基于v8运行时只能是单线程,可是node中可以fork子进程。因此咱们可使用多进程的方式运行loader,和压缩js,社区有两个插件就是专门干这两个事的:HappyPack、ParallelUglifyPlugin。
const path = require('path') module.exports = { module: { rules: [ { test: /\.js$/, // loader: 'babel-loader' loader: 'happypack/loader?id=babel' } ] }, plugins: [ new require('happypack')({ id: 'babel', loaders: ['babel-loader'] }), ], };
module.exports = { optimization: { minimizer: [ new require('webpack-parallel-uglify-plugin')({ // 配置项 }), ] } }
使windows的时候,咱们常常会看到一些.dll
文件,dll文件被称为动态连接库,里面包含了程序运行时的一些动态函数库,多个程序能够共用一个dll文件,能够减小程序运行时的物理内存。
webpack中咱们也能够引入dll的概念,使用DllPlugin插件,将不常常变化的框架代码打包到一个js中,好比叫作dll.js。在打包的过程当中,若是检测到某个块已经在dll.js中就不会再打包。以前DllPlugin与CommonsChunkPlugin并能相互兼容,本是同根生相煎何太急。可是升级到webpack4以后,问题就迎刃而解了。
使用DllPlugin的时候,要先写另一个webpack配置文件,用来生成dll文件。
//webpack.vue.dll.js const path = require('path') module.exports = { entry: { // 把 vue 相关模块的放到一个单独的动态连接库 vue: ['vue', 'vue-router', 'vuex', 'element-ui'] }, output: { filename: '[name].dll.js', //生成vue.dll.js path: path.resolve(__dirname, 'dist'), library: '_dll_[name]' }, plugins: [ new require('webpack/lib/DllPlugin')({ name: '_dll_[name]', // manifest.json 描述动态连接库包含了哪些内容 path: path.join(__dirname, 'dist', '[name].manifest.json') }), ], };
而后在以前的webpack配置中,引入dll。
const path = require('path') module.exports = { plugins: [ // 只要引入manifest.json就能知道哪些模块再dll文件中,在打包过程会忽略这些模块 new require('webpack/lib/DllReferencePlugin')({ manifest: require('./dist/vue.manifest.json'), }) ], devtool: 'source-map' };
最后生成html文件的时候,必定要先引入dll文件。
<html> <head> <meta charset="UTF-8"> </head> <body> <div id="app"></div> <script src="./dist/vue.dll.js"></script> <script src="./dist/main.js"></script> </body> </html>
前面的优化都是优化打包速度,或者减小重复模块的。这里有一种优化方式,可以减小代码量,而且减小客户端的运行时间。
使用Prepack,这是facebook开源的一款工具,可以运行你的代码中部分可以提早运行的代码,减小在线上真实运行的代码。
官方的demo以下:
//input (function () { function hello() { return 'hello'; } function world() { return 'world'; } global.s = hello() + ' ' + world(); })(); //output s = "hello world";
想在webpack中接入也比较简单,社区以及有了对应的插件prepack-webpack-plugin,目前正式环境运用较少,还有些坑,能够继续观望。
module.exports = { plugins: [ new require('prepack-webpack-plugin')() ] };
这里简单罗列了一些webpack的优化策略,可是有些优化策略仍是仍是要酌情考虑。好比多进程跑loader,若是你项目比较小,开了以后可能变慢了,由于原本打包时间就比较短,用来fork子进程的时间,说不定都已经跑完了。记住过早的优化就是万恶之源
。
webpack4带了不少新的特性,也大大加快的打包时间,而且减小了打包后的文件体积。期待webpack5的更多新特性,好比,以html或css为文件入口(鄙人认为html才是前端模块化的真正入口,浏览器的入口就是html,浏览器在真正的亲爹,不和爹亲和谁亲),默认开启多进程打包,加入文件的长期缓存,更多的拓展零配置。
同时也要感谢前端社区其它的优秀的打包工具,感谢rollup,感谢parcel。