webpack 使用-进阶篇

194 阅读2分钟

前言

在上一篇我们讲到了webpack的初级使用,接下来我们讲一下进阶的使用方式。本篇的进阶方式主要是用在生产环境的打包上,开发环境由于只是在本地运行,webpack在热更新方面速度也还可以接受,所以开发环境一般优化需求不大。

多页面打包

有时候我们做一个web应用是多页面的,如果按照以前webpack的配置方式,我们可能需要写多个entry和HtmlWebpackPlugin。显然这种重复繁琐的功能不是我们程序员想要的,于是我们可以这么做:

const setMPA = () => {
    const entry = {};
    const htmlWebpackPlugins = [];
    const entryFiles = glob.sync(path.join(__dirname, './src/*/index.js'));

    Object.keys(entryFiles)
        .map((index) => {
            const entryFile = entryFiles[index];
            //我们默认多页面规范的入口是src里面每个文件夹里的index.js
            const match = entryFile.match(/src\/(.*)\/index\.js/);
            const pageName = match && match[1];
            entry[pageName] = entryFile;
            htmlWebpackPlugins.push(
                new HtmlWebpackPlugin({
                    template: path.join(__dirname, `src/${pageName}/index.html`),
                    filename: `${pageName}.html`,
                    chunks: [pageName],
                    inject: true,
                    minify: {
                        html5: true,
                        collapseWhitespace: true,
                        preserveLineBreaks: false,
                        minifyCSS: true,
                        minifyJS: true,
                        removeComments: false
                    }
                })
            );
        });

    return {
        entry,
        htmlWebpackPlugins
    }
}

const { entry, htmlWebpackPlugins } = setMPA();

module.exports = {
    entry: entry,
    output: {
        path: path.join(__dirname, 'dist'),
        filename: '[name].js'
    },
    plugins: [
     
    ].concat(htmlWebpackPlugins)
};

压缩js

TerserPlugin 是一个比较成熟的js压缩插件,可以帮助我们压缩js代码,清除注释、console等

npm install terser-webpack-plugin --save-dev

const TerserPlugin = require('terser-webpack-plugin');
optimization: {
		concatenateModules:true,
    minimizer: [
     new TerserPlugin({
     terserOptions: {
          output: {
          	comments:false
          },
          cache: true,
          extractComments:false
        }
     })
    ]
 }
压缩css,补充浏览器兼容前缀

optimize-css-assets-webpack-plugin 能够较好地帮助我们压缩css,结合cssnano可以自动补全代码

npm install --save-dev optimize-css-assets-webpack-plugin

const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
optimization: {
    minimizer: [
     new OptimizeCssAssetsPlugin({
      assetNameRegExp: /\.optimize\.css$/g,
      cssProcessor: require('cssnano'),
      cssProcessorPluginOptions: {
        preset: ['default', { discardComments: { removeAll: true } }],
      },
      canPrint: true
    })
    ]
 },
自定义分包

webpack提供了默认的分包机制,通常情况下可以直接使用。但是我们仍然可以根据项目需要来制定我们的最佳分包方式

optimization: {
    splitChunks:{
    	chunks:"all",
      minSize: 30000,  //表示在压缩前的最小模块大小,默认值是30kb
    	minChunks: 1,  // 表示被引用次数,默认为1;
      maxAsyncRequests: 5,  //所有异步请求不得超过5个
      maxInitialRequests: 3,  //初始话并行请求不得超过3个
     	automaticNameDelimiter:'~',//名称分隔符,默认是~
      name: true,  //打包后的名称,默认是chunk的名字通过分隔符(默认是~)分隔
      cacheGroups: { //设置缓存组用来抽取满足不同规则的chunk
      // 举例,echarts是登陆后才需要引入的,单独拿出来
      	echarts:{
      		minChunks:1,
          test:/[\\/]node_modules[\\/](echarts|echart-gl)/,
          priority:10,
          name:'echarts'
      	},
      	 // 举例,demoCommonModule是被很多其他模块调用的,单独拿出来,不然会被分散重复打到其他的包去
      	demoCommonModule:{
      		minChunks:1,
          test:/[\\/]views[\\/]demoCommonModule/,
          priority:10,
          name:'echarts'
      	}
      	
      	... 其他模块也采用类似的方式抽出来,具体要看BundleAnalyzerPlugin分析是否合理
      }
 }
标签式引入第三方库

有时候对于一些第三方类库,我们并不希望webpack将他打包进bundle里面,而是采用script标签的形式引入。这种情况下HtmlWebpackExternalsPlugin可以帮我们实现。(主要注意的是你仍需要在代码里面进行import)

 npm install --save-dev html-webpack-externals-plugin
 
 const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin')
 new HtmlWebpackExternalsPlugin({
            externals: [
              {
                module: 'vue',
                entry: 'https://cdn.bootcdn.net/ajax/libs/vue/2.6.11/vue.min.js',
                global: 'Vue',
              }
            ]
        }),
速度分析

有时候项目过大会导致构建起来非常耗时,处于对构建优化的考虑,我们可能需要知道哪个构建步骤运行的时间比较久,从而有针对性地进行优化。speed-measure-webpack-plugin就可以帮我们计算各构建步骤的耗时时间。但需要注意的是:只在优化的过程中使用,优化完之后应该禁用该插件,因为这个插件本身也会带来一些时间消耗。

npm install --save-dev speed-measure-webpack-plugin

const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
const webpackConfig = smp.wrap({
  plugins: [
    new MyPlugin(),
    new MyOtherPlugin()
  ]
});
体积分析

webpack-bundle-analyzer是个优化项目的神兵利器,他可以帮助我们查看打出来的bundle都包含了哪些文件,从而让我们识别构建是否合理,分包是否正确。

npm install --save-dev webpack-bundle-analyzer

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
 plugins: [
    new BundleAnalyzerPlugin()
  ]
gzip压缩

通常,用户访问web项目时加载资源文件最好使用gzip的格式,而这个格式转化是由服务器来完成的。但是毕竟服务器转化格式也需要耗时,所以我们可以将gzip压缩这个步骤在前端完成。compression-webpack-plugin就是做这个事情。

npm i -D compression-webpack-plugin

const CompressionPlugin = require("compression-webpack-plugin")

module.exports = {
  plugins: [
    new CompressionPlugin()
  ]
}
文件复制
npm install --save-dev copy-webpack-plugin

const CopyWebpackPlugin = require('copy-webpack-plugin');

module.exports = {
    context: path.join(__dirname, 'app'),
    devServer: {
        // This is required for older versions of webpack-dev-server
        // if you use absolute 'to' paths. The path should be an
        // absolute path to your build destination.
        outputPath: path.join(__dirname, 'build')
    },
    plugins: [
        new CopyWebpackPlugin([
            // {output}/to/directory/file.txt
            { from: 'from/file.txt', to: 'to/directory' },
            // Copy directory contents to {output}/to/directory/
            { from: 'from/directory', to: 'to/directory' },
        ])
    ]
};
webpack自定义bundle名称以及预加载
import(/* webpackChunkName:"global" */ /* webpackPreFetch:true*/ "@/utils/globalImport.js");
构建使用缓存

webpack每次构建的时候都会去遍历编译每一个依赖的文件,这种做法有时候在二次编译时往往低效,因为只有少量的文件被更改了。于是我们可以配置使用缓存的方式,将已经编译过的并且没有更改的文件缓存起来,提高二次编译速度。

npm install --save-dev cache-loader

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          'cache-loader',
          ...loaders
        ],
        include: path.resolve('src')
      }
    ]
  }
}

也可以使用bableLoader 本身支持的cache功能
 {
    test: /\.js$/,
    loader: 'babel-loader?cacheDirectory
  }
多线程打包

我们知道JavaScript是单线程的,但是在构建中我们依然可以使用多线程构建我们的应用,从而加快构建速度。

  npm install --save-dev thread-loader
  
  const threadLoader = require('thread-loader');
  //线程预热
  threadLoader.warmup({}, [
    'babel-loader',
    'babel-preset-es2015',
    'sass-loader',
  ]);

  rules: [
      {
        test: /\.js$/,
        include: path.resolve("src"),
        use: [
          "thread-loader",
          // your expensive loader (e.g babel-loader)
        ]
      }
    ]
    

结语

以上就是webpack的一些使用方式,后续仍会继续补充,也欢迎更多的同行参与讨论,批评指正。

最终webpack.config.prod.js

const path=require('path');
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
const CompressionPlugin = require("compression-webpack-plugin")
const threadLoader = require('thread-loader');
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin')
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
//线程预热
threadLoader.warmup({}, [
  'babel-loader',
  'sass-loader',
  'vue-loader'
]);

const webpackconfig = {
    entry:{
        index:'./src/main.js' 
    },
    output:{
        filename:'[name].js',
        path: path.resolve(__dirname, "dist"),
    },
    optimization: {
        minimizer: [
            new TerserPlugin({
                terserOptions: {
                        output: {
                            comments:false
                        },
                        extractComments:false
                    }
            }),
            new OptimizeCssAssetsPlugin({
                assetNameRegExp: /\.optimize\.css$/g,
                cssProcessor: require('cssnano'),
                cssProcessorPluginOptions: {
                    preset: ['default', { discardComments: { removeAll: true } }],
                },
                canPrint: true
            })
        ],
        splitChunks:{
            chunks:"all",
            minSize: 30000,  //表示在压缩前的最小模块大小,默认值是30kb
            minChunks: 1,  // 表示被引用次数,默认为1;
            maxAsyncRequests: 5,  //所有异步请求不得超过5个
            maxInitialRequests: 3,  //初始话并行请求不得超过3个
            automaticNameDelimiter:'~',//名称分隔符,默认是~
            name: true,  //打包后的名称,默认是chunk的名字通过分隔符(默认是~)分隔
            cacheGroups: { //设置缓存组用来抽取满足不同规则的chunk
                // 举例,echarts是登陆后才需要引入的,单独拿出来
                echarts:{
                minChunks:1,
                test:/[\\/]node_modules[\\/](echarts|echart-gl)/,
                priority:10,
                name:'echarts'
              },
               // 举例,demoCommonModule是被很多其他模块调用的,单独拿出来,不然会被分散重复打到其他的包去
              demoCommonModule:{
                minChunks:1,
                test:/[\\/]views[\\/]demoCommonModule/,
                priority:10,
                name:'echarts'
              }
            }
        }
     },
    mode:"production",
      module:{
        rules:[
            {
                test: /\.js$/,
                exclude: /node_modules/,
                loader: [
                    'cache-loader',
                    'babel-loader',
                ]
            },
            {
                test: /\.vue$/,
                loader: [
                    'cache-loader',
                    'vue-loader'
                ]
            },
            {
                test: /\.css$/,
                use: [
                     MiniCssExtractPlugin.loader,
                  'css-loader',
                ],
              },
              {
                test: /.less$/,
                use: [
                    MiniCssExtractPlugin.loader,
                    'css-loader',
                    'less-loader'
                    ]
              },
              {
              test: /\.scss$/,
              use: [
                    MiniCssExtractPlugin.loader,
                    'css-loader',
                    'sass-loader'
                ],
            },
            {
                test: /\.(png|jpg|gif)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                          esModule: false, // 不加的话会有这种情况 img属性src="[object Module]"
                          limit: 1024 * 1,
                          name: 'assets/images/[name]-[contenthash:8].[ext]'
                        }
                      }
                ]
            },
            {
                test: /\.(ttf|woff)$/,
                use: [
                  {
                    loader: 'file-loader',
                    options: {
                        name:'./assets/styles/font/[name]-[contenthash:8].[ext]'
                    }
                  },
                  {
                    loader: 'url-loader',
                    options: {
                      limit: 10240
                    }
                  }
                ]
            }
        ]
      },
      plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
            filename: "index.html",
            template: "./src/template/index.html",
            inlineSource: '.css$',
            inject: true,
            minify: {
                html5: true,
                collapseWhitespace: true,
                preserveLineBreaks: false,
                minifyCSS: true,
                minifyJS: true,
                removeComments: false
            }
        }),
        // new HtmlWebpackExternalsPlugin({
        //     externals: [
        //       {
        //         module: 'vue',
        //         entry: 'https://cdn.bootcdn.net/ajax/libs/vue/2.6.11/vue.min.js',
        //         global: 'Vue',
        //       }
        //     ]
        // }),
        new MiniCssExtractPlugin({
            chunkFilename: 'assets/styles/[id]-[contenthash:8].css',
            ignorOrder:true
        }),
        new VueLoaderPlugin(),
        // new BundleAnalyzerPlugin(),
        new CompressionPlugin()
      ]
}

module.exports= smp.wrap(webpackconfig)