webpack4 打包优化
1 参考文章
详解webpack4之splitchunksPlugin代码包分拆
开发工具心得:如何 10 倍提高你的 Webpack 构建效率
2: webpack4中的 optimization.runtimeChunk的作用是什么?
首先看参考文章:
优化持久化缓存的, runtime 指的是 webpack 的运行环境(具体作用就是模块解析, 加载) 和 模块信息清单,
模块信息清单在每次有模块变更(hash 变更)时都会变更, 所以我们想把这部分代码单独打包出来,
配合后端缓存策略, 这样就不会因为某个模块的变更导致包含模块信息的模块(通常会被包含在最后一个 bundle 中)缓存失效.
optimization.runtimeChunk 就是告诉 webpack 是否要把这部分单独打包出来.
假设一个使用动态导入的情况(使用import()),在app.js动态导入component.js
const app = () =>import('./component').then();
build之后,产生3个包。
0.01e47fe5.js
main.xxx.js
runtime.xxx.js
其中runtime,用于管理被分出来的包。下面就是一个runtimeChunk的截图,可以看到chunkId这些东西。
...
function jsonpScriptSrc(chunkId) { /******/ return __webpack_require__.p + "" + ({}[chunkId]||chunkId) + "." + {"0":"01e47fe5"}[chunkId] + ".bundle.js" /******/ }
...
如果采用这种分包策略
当更改app的时候runtime与(被分出的动态加载的代码)0.01e47fe5.js的名称(hash)不会改变,main的名称(hash)会改变。
当更改component.js,main的名称(hash)不会改变,runtime与 (动态加载的代码) 0.01e47fe5.js的名称(hash)会改变。
总结一下:
runtime.js文件相当于动态文件的索引文件,相当于一个文件夹中的index索引文件,告诉main.js要引用的文件的名字
这样app.js 变化的时候 由于不影响 componment.js 所以生成的0.01.js 和runtime.js 不会发生变化;
当 componment.js 发生变化的时候,生成的0.01.js要发生变化,同时索引文件 runntime.js 也会发生变化。但是main.js引用的是runntime.js 则不会发生变化
---
3 使用 splitchunksPlugin 提取公共代码
optimization:{ splitChunks: { chunks: 'all',//同步异步全都打包 minSize: 30000,//打包的库或者文件必须大于这个字节才会进行拆分 minChunks: 1,//规定当模块在生成的js文件(trunk)中被调用过多少次的时候再进行拆分 maxAsyncRequests: 5, maxInitialRequests: 3, automaticNameDelimiter: '~',//如果不写filename 默认名字 组名~[name] name: true, cacheGroups: {//缓存组,因为需要打包完成之后,在把所有要拆分的代码合并拆分,所以先要缓存 vendors: { test: /[\\/]node_modules[\\/]/, //如果上面chunks定为all,就是找到所有的import文件,看他是不是调用于 node_modules 文件夹 是的话就拆分 priority: -10,//优先级 比如同时符合vender 和 default 这个优先级高 所以存在这里 filename: 'vendors.js', //拆分后打包的文件名字 }, default: {//像文件中 import进来的文件 如果不在 node_modules文件夹中 则走默认组,打包出的文件名字是 common.js priority: -20, minChunks: 2, reuseExistingChunk: true,//比如a.js 引用了 b.js;如果b.js在之前已经被拆分过,则这里不再对其进行拆分 filename: 'common.js' } } } }
为了支持异步js,
npm install --save-dev @babel/plugin-syntax-dynamic-import
记得修改 .babelrc
{ "presets":[ ["@babel/preset-env",{ "useBuiltIns":"usage", "corejs":2, "targets":{ "browsers":[">1%","last 2 version","not ie <= 8"] } }] ], "plugins": ["@babel/plugin-syntax-dynamic-import"] }
注意:
4: 使用 DLLPlugin 提取第三方不变化的代码库:
DLLPlugin 它能把第三方库代码分离开,并且每次文件更改的时候,它只会打包该项目自身的代码。所以打包速度会更快。
const path = require('path'); const DllPlugin = require('webpack/lib/DllPlugin'); const {CleanWebpackPlugin} = require('clean-webpack-plugin'); module.exports = { mode:'production', // 入口文件 entry: { // 项目中用到该两个依赖库文件 vue: ['vue'] }, // 输出文件 output: { // 文件名称 filename: '[name].dll.js', // 将输出的文件放到dist目录下 path: path.resolve(__dirname, '../static'), /* 存放相关的dll文件的全局变量名称,比如对于jquery来说的话就是 _dll_jquery, 在前面加 _dll 是为了防止全局变量冲突。 */ library: '[name]_library' }, plugins: [ new CleanWebpackPlugin(), //注意在webpack4 中 不需要定于删除哪个文件夹 默认会根据output中生成的path路径删掉 // 使用插件 DllPlugin new DllPlugin({ /* 该插件的name属性值需要和 output.library保存一致,该字段值,也就是输出的 manifest.json文件中name字段的值。 比如在jquery.manifest文件中有 name: '_dll_jquery' */ name: '[name]_library', context:__dirname, //context (可选): manifest文件中请求的上下文,默认为该webpack文件上下文
/* 生成manifest文件输出的位置和文件名称 */ path: path.join(__dirname, '../static/', '[name].manifest.json') }) ] };
const DllReferencePlugin = require('webpack/lib/DllReferencePlugin'); const proConfig = { plugins:[ new DllReferencePlugin({ context:__dirname, manifest:require('../static/vue.manifest.json') }) ] }
然后增加 package.json 文件:
"dll": "webpack --config ./build/webpack.dll.js --hide-modules --progress"
最后在模版文件中增加引用:
<script type="text/javascript" src="../static/vue.dll.js"></script>
所以先执行 npm run dll,在执行npm run build
但是这样修该模版文件不太好,所以引入插件:
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin'); plugins:[ new AddAssetHtmlPlugin({ filepath: require.resolve('../static/vue.dll.js'),//相当于path.join(__dirname, '../static/vendordev.dll.js') includeSourcemap: false }) ]
注意:
1: html-webpack-include-assets-plugin 和 add-asset-html-webpack-plugin 的区别
两个插件都是把规定的文件插入到html中:
主要的不同是 html-webpack-include-assets-plugin 不会copy文件,而 add-asset-html-webpack-plugin 会copy文件,什么意思呢?
使用 add-asset-html-webpack-plugin 后,会把引入的 filepath: require.resolve('../static/vue.dll.js') 自动引入到 dist 文件夹中,而 html-webpack-include-assets-plugin 则不会;
所以在dev环境下,直接把vue.dll.js 引入dist目录下即可,所以使用 add-asset-html-webpack-plugin;而在production环境下,要把 vue.dll.js 放在dist/lib 文件夹下,所以使用CopyWebpackPlugin 复制vue.dll.js文件,配合 html-webpack-include-assets-plugin 插入html中;
所以分为dev和product环境:
const htmlWebpackIncludeAssetsPlugin = require('html-webpack-include-assets-plugin'); const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin'); const CopyWebpackPlugin = require('copy-webpack-plugin'); //dev环境中,该插件会把 vue.dll.js 文件复制到当前路径下 new AddAssetHtmlPlugin({ filepath: require.resolve('../static/vue.dll.js'),//相当于path.join(__dirname, '../static/vendordev.dll.js') includeSourcemap: false, }), //production 环境中 // 1 需要把 dll 文件复制到打包的 dist/lib 文件夹下 -- CopyWebpackPlugin // 2 html中引入 dist/lib 下的dll 文件 -- htmlWebpackIncludeAssetsPlugin new htmlWebpackIncludeAssetsPlugin({ //这个插件是把vue.dll.js 插入到 html 中 assets:['./lib/vue.dll.js'], append:false }), new CopyWebpackPlugin([ //文件复制到打包的 dist/lib 文件夹下 { from: path.join(__dirname, "../static/vue.dll.js"), to: path.join(__dirname, "../dist/lib/vue.dll.js") } ]),
注意2:
这里引入 AddAssetHtmlPlugin 你会发现即使不使用:
new DllReferencePlugin({ context:__dirname, manifest:require('../static/vue.manifest.json') })
页面也可以使用vue。
那么 DllReferencePlugin 的作用是什么呢?
我们看打包生成的文件:
情况1: 不使用 DllReferencePlugin:
可以看出,index.js 文件中使用的vue居然还是来自 node_modules 说明并没有把vue第三方库剔除;
对比使用DllReferencePlugin :
发现index中已经没有 vue 第三方库,但是页面仍旧能打开,说明使用的是 dll 文件。
再来看 DllReferencePlugin 的作用:
有了映射文件,在webpack打包的时候就可以结合映射文件以及生成的全局变量,来对需要打包的源代码进行分析。
一旦发现你打包的源代码里面用到了映射文件中已有的文件,则直接使用vendors.dll.js 中的内容,而不会去node_modules里引入该模块
5. 引入的loader一定要加上 exclude和include 可以大幅降低打包的代码文件大小
6. 使用别名优化:
resolve:{ extensions:['.js','.vue','.json'], alias:{ "@":path.resolve('src') } }
使用extensions省略后缀名字;使用alias给src加上别名,这样可以简化html中的路径,比如:
pages/index/index.vue 中要使用 assest/imgs/logo.png 的图片:
<img src="../../assest/imgs/logo.png" alt="" class="img-box">
可以简化成:
<img src="@/assest/imgs/logo.png" alt="" class="img-box">
类似的:
//简化前 import Chinese from '../../component/chinese.vue'; //简化后 import Chinese from '@/component/chinese.vue';
但是对于css注意,在引用路径的字符串前面加上 ~ 的符号,如:@import “~Css/…”。webpack 会以~符号作为前缀的路径视为依赖模块去解析
background: url('~@/assest/imgs/logo.png');
代码:
import VueRouter from 'vue-router' import routers from './routers' Vue.use(VueRouter) const router = new VueRouter({ mode: 'history', routers })
原因:
routes:不是routers
解决办法:
routers改为routes即可。
8.解决webpack中css独立成文件后图片路径错误的问题
说明:此配置针对webpack4+
配置img图片路径问题:
output:{ filename:'js/[name].[chunkhash].js', path:path.resolve(__dirname,'../dist'), }, //module { test:/\.(png|gif|jpeg|jpg)$/, use:[ { loader:'url-loader?cacheDirectory=true', options:{ name:'img/[name].[ext]', limit:1024 } } ] }, //plugins new MiniCssExtractPlugin({ filename: devMode ? '[name].css' : 'css/[name].[contenthash].css', chunkFilename: devMode ? '[name].css' : 'css/[name].[contenthash].css', })
生成的图片会在css文件中: css/img/logo.png 这样的话 找不到该图片,因为生成的图片路径应该是 img/logo.png
如果修改output的配置中加入一个 publicPath: '../' 配置,则所有的css文件和js的路径都会变化,所以应该只处理css文件,设置css文件中的公共路径:
output:{ filename:'js/[name].[chunkhash].js', path:path.resolve(__dirname,'../dist'), }, //module { test:/\.scss$/, use:[ { loader:MiniCssExtractPlugin.loader, options:{ publicPath:'../' } }, { loader:'css-loader', options:{ importLoaders:2, } }, 'postcss-loader', 'sass-loader' ] }, { test:/\.(png|gif|jpeg|jpg)$/, use:[ { loader:'url-loader?cacheDirectory=true', options:{ name:'img/[name].[ext]', limit:1024 } } ] }, //plugins new MiniCssExtractPlugin({ filename: devMode ? '[name].css' : 'css/[name].[contenthash].css', chunkFilename: devMode ? '[name].css' : 'css/[name].[contenthash].css', })
9 为了避免不同路由页面间样式冲突,css样式要加 scoped
import Home from "./view/home"; const ProInsight = () => import(/* webpackPrefetch: true */ /* webpackChunkName: 'proInsight' */ "./view/proInsight"); const ProTarget = () => import(/* webpackPrefetch: true */ /* webpackChunkName: 'proTarget' */ "./view/proTarget"); Vue.use(VueRouter); const routes = [ { path: "/", name: "home", component: Home }, { path: "/proInsight", name: "proInsight", component: ProInsight }, { path: "/proTarget", name: "proTarget", component: ProTarget } ];
中,首页入口 home文件没有使用异步加载,则该组件所有引用的公共代码,都会被打包到home中,而不会提取出来,
所以,home这个组件也要改成异步加载的形式。这样所有的公共代码才能被提取出来。
stats: { warningsFilter: (warning) => /Conflicting order between/gm.test(warning), children: false }