带你进入webpack侏罗纪大冒险(解决方案)

334 阅读4分钟

持续更新:2022.05.02

生产|开发环境构建

不同环境config文件

开发环境和生产环境构建存在巨大差异,所以通常编写独立的webpack配置。但为了遵循不重复原则,我们又会存在common webpack配置
为了将这些配置合并在一起,可以使用 webpack-merge 插件,也可以采用其他方法
👇

目录

demo
  |- build
    |- webpack.common.js  
    |- webpack.dev.js   
    |- webpack.prod.js  
  |- package.json
  |-postcss.config.js
  |-.babelrc
  |- /dist
  |- /src
    |- index.js
  |- /node_modules

webpack.common.js

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const {CleanWebpackPlugin} = require('clean-webpack-plugin');
const commonConfig = {
    entry: {
        main: './src/index.js',
    },
    module: {
        rules: [{
            test: /\.js$/,
            exclude: /node_modules/,
            loader: "babel-loader"
        }, {
            test: /\.(jpg|gif|png)$/,
            use: {
                loader: 'url-loader',
                options: {
                    name: '[name]_[hash].[ext]',
                    outputPath: 'images/',
                    limit: 1024 //100KB
                }
            }
        }, {
            test: /\.css$/,
            use: ['style-loader', 'css-loader', 'postcss-loader']
        }, {
            test: /\.scss$/,
            use: ['style-loader',
                {
                    loader: 'css-loader',
                    options: {
                        importLoaders: 2,
                        modules: true
                    }
                },
                'sass-loader',
                'postcss-loader'
            ]
        }, {
            test: /\.(woff|woff2|eot|ttf|otf)$/,
            use: [
                'file-loader'
            ]
        }]
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: 'src/index.html' // 以src/目录下的index.html为模板打包
        }),
        new CleanWebpackPlugin({}),
    ],
    output: {
        filename: '[name].js',
        // publicPath: "https://cdn.example.com/assets/",
        path: path.join(__dirname, '../dist')
    }
}

module.exports = commonConfig

webpack.dev.js

const path = require('path')
const webpack = require('webpack')
const { merge } = require('webpack-merge')
const commonConfig = require('./webpack.common')

const devConfig = {
    mode: 'development',
    devtool: 'cheap-module-eval-source-map',
    devServer: {
        contentBase: path.join(__dirname, "dist"),
        compress: true,
        port: 9000,
        open: true,
        hot: true,
    },
    plugins: [
        new webpack.NamedModulesPlugin(),
        new webpack.HotModuleReplacementPlugin(),
    ],
    optimization:{
        usedExports: true
    }
}

module.exports = merge(commonConfig, devConfig)

webpack.prod.js

const { merge } = require('webpack-merge')
const commomConfig = require('./webpack.common')
const prodConfig = {
    mode: 'production',
    devtool: 'cheap-module-source-map',
}

module.exports = merge(commomConfig, prodConfig)

打包脚本

我们将 npm run dev 定义为开发环境脚本,并在其中使用 webpack-dev-server,将 npm run build 定义为生产环境脚本

  {
    "name": "demo",
    "scripts": {
    "dev": "webpack-dev-server --config ./build/webpack.dev.js",
    "build": "webpack --config ./build/webpack.prod.js",
    "start": "npx webpack --config ./build/webpack.dev.js"
    },
  }

打包一个library(库)

  1. 安装工具
npm install --save-dev webpack webpack-cli
  1. 增加 output.library 选项,让构建产物以 library 形式输出,纯字符串:
output: {
  library: "myLib",  // 这样打包只能以 script 方式引入,无法适配 CommonJS/AMD/Node等环境
}

更改为对象:

output: {
   library: {
      name: 'myLib',
      type: 'umd',  // 以umd的格式打包lib,通常使用这个 type
   },
}

library 详解请看 output篇

  1. 用到的第三方库,安装到 devDependencies中,避免库体积很大,如
npm install --save-dev lodash
  1. 将 lodash 从library里剔除,以 peerDependency 形式出现(让使用该library的应用自行安装lodash),避免包过大
  externals: {
     lodash: {
       commonjs: 'lodash',
       commonjs2: 'lodash',
       amd: 'lodash',
       root: '_',
     },
   },

这意味着你的 library 需要一个名为 lodash 的依赖,这个依赖在 consumer 环境中必须存在且可用。 5. 将生成 bundle 的文件路径,添加到 package.json 中的 main 字段中

// package.json
{
  ...
  "main": "dist/webpack-numbers.js",
  ...
}
  1. 发布成npm包

如何提高 webpack 构建速度?

这篇文章针对第一点有更详细介绍,如有需要可点击

1. 缩小编译范围,减少不必要的编译工作

  • exclude/include 指定
  • resolve.modules 指明第三方模块的绝对路径 (减少不必要的查找)
  • resolve.mainFields 只采用 main 字段作为入口文件描述字段 (减少搜索步骤,需要考虑到所有运行时依赖的第三方模块的入口文件描述字段)
  • resolve.extensions 尽可能减少后缀尝试的可能性
  • noParse 对完全不需要解析的库进行忽略 (不去解析但仍会打包到 bundle 中,注意被忽略掉的文件里不应该包含 import、require、define 等模块化语句)
  • IgnorePlugin, webpack 的内置插件,作用是忽略第三方包指定目录。有些包我们只需要核心功能,可以排除掉不需要的部分(并不是都可以这样)
  • 合理使用alias
  • externals 将一些 JS文件存储在 CDN 上(减少 Webpack 打包出来的 js 体积)
const resolve = dir => path.join(__dirname, '..', dir);
resolve: {
    modules: [ // 指定以下目录寻找第三方模块,避免webpack往父级目录递归搜索
        resolve('src'),
        resolve('node_modules'),
        resolve(config.common.layoutPath)
    ],
    mainFields: ['main'], // 只采用main字段作为入口文件描述字段,减少搜索步骤
    alias: {
        vue$: "vue/dist/vue.common",
        "@": resolve("src") // 缓存src目录为@符号,避免重复寻址
    }
},
module: {
    noParse: /jquery|lodash/, // 忽略未采用模块化的文件,因此jquery或lodash将不会被下面的loaders解析
    rules: [
        {
            test: /\.js$/,
            include: [ // 表示只解析以下目录,减少loader处理范围
                resolve("src"),
                resolve(config.common.layoutPath)
            ],
            exclude: file => /test/.test(file), // 排除test目录文件
            loader: "happypack/loader?id=happy-babel" // 后面会介绍
        },
    ]
}

2. 压缩代码

多进程并行压缩

可以使用 webpack-parallel-uglify-plugin,如何使用参考 三石的webpack(Plugins篇)

css 单独压缩

通过 mini-css-extract-plugin 提取 Chunk 中的 CSS 代码到单独文件,通过 optimize-css-assets-webpack-plugin 压缩 CSS,如何使用参考 三石的webpack(Plugins篇)

图片压缩

  • 使用基于 Node 库的 imagemin
  • 配置 image-webpack-loader

3. 并行处理 Loader

  1. 使用HappyPack,如何使用参考 三石的webpack(Plugins篇)
    缺点是 HappyPack 已经不太维护了
  2. 使用 thread-loader,如何使用参考 三石的webpack(Loader篇)

4. DllPlugin

第三方库打包成动态链接库,如何使用参考 三石的webpack(Plugins篇)

5. 利用缓存提升二次构建速度

  • babel-loader 开启缓存
  • terser-webpack-plugin 开启缓存
  • 使用 cache-loader 或者 hard-source-webpack-plugin
  • splitChunks 抽取公共模块,公共代码下载一次就缓存起来,避免重复下载

参考官网-构建性能

webpack 如何优化前端性能

  1. 压缩代码

  2. 删除冗余代码 「Tree-Shaking」

webpack4 以后,mode设置成 "production"即可

  1. 第三方库按需加载、路由懒加载
// 按需加载,避免全部引入,加大项目体积
import { connect } from 'react-redux';

//路由懒加载
const showImage = () => import('@/components/common/showImage');
  1. 代码分割 进⾏公共模块抽取,利⽤浏览器缓存可以⻓期缓存这些⽆需频繁变动的公共代码
  • 提取第三方库 「vendor」
module.exports = {
    entry: {
        main: './src/index.js',
        vendor: ['react', 'react-dom'],
    },
}
  • 依赖库分离 「splitChunks」
optimization: {
  splitChunks: {
     chunks: "async", // 必须三选一: "initial" | "all"(推荐) | "async" (默认就是async)
     minSize: 30000, // 最小尺寸,30000
     minChunks: 1, // 最小 chunk ,默认1
     maxAsyncRequests: 5, // 最大异步请求数, 默认5
     maxInitialRequests : 3, // 最大初始化请求书,默认3
     automaticNameDelimiter: '~',// 打包分隔符
     name: function(){}, // 打包后的名称,此选项可接收 function
     cacheGroups:{ // 这里开始设置缓存的 chunks
         priority: 0, // 缓存组优先级
         vendor: { // key 为entry中定义的 入口名称
             chunks: "initial", // 必须三选一: "initial" | "all" | "async"(默认就是async)
             test: /react|lodash/, // 正则规则验证,如果符合就提取 chunk
             name: "vendor", // 要缓存的 分隔出来的 chunk 名称
             minSize: 30000,
             minChunks: 1,
             enforce: true,
             maxAsyncRequests: 5, // 最大异步请求数, 默认1
             maxInitialRequests : 3, // 最大初始化请求书,默认1
             reuseExistingChunk: true // 可设置是否重用该chunk
         }
     }
  }
 },
  1. 利用CDN加速 在构建过程中,将引⽤的静态资源路径修改为CDN上对应的路径。可以利⽤webpack对于 output 参数和各loader的 publicPath 参数来修改资源路径

通用缓存方案

  1. output.filename 采用 可替换模板字符串 命名,这样内容hash变化文件名就会变化,避免更新不及时 filename: '[name].[contenthash].js'

  2. 采用代码分离 runtimeChunk,这样,chunk1 引入变化的chunk2,最终 chunk1 能够成功缓存(详情见 optimization篇

   optimization: {
     runtimeChunk: 'single',
   }
  1. 将第三方库提到单独的 vendor chunk 中,因为它们在开发中不会频繁修改
  optimization: {
     splitChunks: {
       cacheGroups: {
         vendor: {
           test: /[\/]node_modules[\/]/,
           name: 'vendors',
           chunks: 'all',
         },
       },
     },
    },
  1. moduleIds 设置 'deterministic',避免新增文件影响到自己的 moduleId
   optimization: {
     moduleIds: 'deterministic'
   }

综合如下:

  const path = require('path');
  const HtmlWebpackPlugin = require('html-webpack-plugin');

  module.exports = {
    entry: './src/index.js',
    plugins: [
      new HtmlWebpackPlugin({
      title: 'Caching',
      }),
    ],
    output: {
      filename: '[name].[contenthash].js',
      path: path.resolve(__dirname, 'dist'),
      clean: true,
    },
    optimization: {
      moduleIds: 'deterministic',
      runtimeChunk: 'single',
      splitChunks: {
        cacheGroups: {
          vendor: {
            test: /[\/]node_modules[\/]/,
            name: 'vendors',
            chunks: 'all',
          },
        },
      },
    },
  };

多页面应用打包

使用 html-webpack-plugin插件生成多个实例,点击 查看详情