Webpack 相关【持续更新...】

三种hash策略

  • hash

    hash 和每次 build有关,没有任何改变的情况下,每次编译出来的 hash都是一样的,但当你改变了任何一点东西,它的hash就会发生改变。

    简单理解,你改了任何东西,hash 就会和上次不一样了。

  • chunkhash

    chunkhash根据不同的入口文件(Entry)进行依赖文件解析、构建对应的代码块(chunk),生成对应的哈希值,某文件变化时只有该文件对应代码块(chunk)的hash会变化

  • contenthash

    它的出现主要是为了解决,让css文件不受js文件的影响。比如foo.cssfoo.js引用了,所以它们共用相同的chunkhash值。但这样子是有问题的,如果foo.js修改了代码,css文件就算内容没有任何改变,由于是该模块的 hash 发生了改变,其css文件的hash也会随之改变。

    这个时候我们就可以使用contenthash了,保证即使css文件所处的模块里有任何内容的改变,只要 css 文件内容不变,那么它的hash就不会发生变化。

    contenthash 你可以简单理解为是 moduleId + content 所生成的 hash

优化策略

优化loader查找范围

loader中结合test include exclude三个配置项来缩⼩loader的处理范围,推荐include

// ...
rules: [
    { test: /\.js$/, use: 'babel-loader', exclude: '/node_modules/' },
]
// ...

exclude 优先级要优于 includetest ,所以当三者配置有冲突时, exclude 会优先于其他两个配置。

优化resolve.modules配置

resolve.modules⽤于配置webpack去哪些⽬录下寻找第三⽅模块,默认是[‘node_modules’]

寻找第三⽅模块,默认是在当前项⽬⽬录下的node_modules⾥⾯去找,如果没有找到,就会去上⼀级⽬录../node_modules找,再没有会去../../node_modules中找,以此类推,和Node.js的模块寻找机制很类似。

如果我们的第三⽅模块都安装在了项⽬根⽬录下,就可以直接指明这个路径。

module.exports={
 resolve:{
 modules: [path.resolve(__dirname, "./node_modules")]
 }
}

优化resolve.alias配置

resolve.alias配置通过别名来将原导⼊路径映射成⼀个新的导⼊路径,拿react为例,我们引⼊的react库,⼀般存在两套代码

  • cjs

    采⽤commonJS规范的模块化代码

  • umd

    已经打包好的完整代码,没有采⽤模块化,可以直接执⾏

默认情况下,webpack会从⼊⼝⽂件./node_modules/bin/react/index开始递归解析和处理依赖的⽂件。我们可以直接指定⽂件,避免这处的耗时。

resolve: {
  alias: {
    "@assets": path.resolve(__dirname, "../src/assets"),
    "@src": path.join(__dirname, "./src"),
    "react": path.resolve(__dirname, "./node_modules/react/umd/react.production.min.js"),
    "react-dom": path.resolve(__dirname, "./node_modules/react-dom/umd/react-dom.production.min.js")
  },
},

优化resolve.extensions配置

resolve.extensions在导⼊语句没带⽂件后缀时,webpack会⾃动带上后缀后,去尝试查找⽂件是否存在

默认值:

extensions:['.js','.json','.jsx','.ts']
  • 后缀尝试列表尽量的⼩
  • 导⼊语句尽量的带上后缀

利⽤多线程提升构建速度

由于运⾏在 Node.js 之上的 Webpack 是单线程模型的,所以 Webpack 需要处理的事情需要⼀件⼀件的做,不能多件事⼀起做。我们需要 Webpack 能同⼀时间处理多个任务,发挥多核 CPU 电脑的威⼒。

thread-loader是针对 loader 进⾏优化的,它会将 loader 放置在⼀个 worker 池⾥⾯运⾏,以达到多线程构建。thread-loader 在使⽤的时候,需要将其放置在其他 loader 之前,如下⾯实例:

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          'thread-loader'
          // 你的⾼开销的loader放置在这后面 (e.g babel-loader)
        ]
      }
    ]
  }
};

缓存cache相关

Webpack 中打包的核⼼是 JavaScript ⽂件的打包,JavaScript 使⽤的是 babel-loader,其实打包时间⻓很多时候是babel-loader 执⾏慢导致的。这时候我们不仅要使⽤ excludeinclude 来尽可能准确的指定要转换内容的范畴,还需要关注 babel-loader 在执⾏的时候,可能会产⽣⼀些运⾏期间重复的公共⽂件,造成代码体积⼤冗余,同时也会减慢编译的速度。

babel-loader提供了 cacheDirectory 配置给 Babel 编译时给定的⽬录,并且将⽤于缓存加载器的结果,但是这个设置默认是 false 关闭的状态,我们需要设置为 true ,这样 babel-loader 将使⽤默认的缓存⽬录 。node_modules/.cache/babel-loader ,如果在任何根⽬录下都没有找到 node_modules ⽬录,将会降级回退到操作系统默认的临时⽂件⽬录。

rules: [
  {
    test: /\.js$/,
    loader: 'babel-loader',
    options: {
      cacheDirectory: true
    },
  }
];

压缩速度优化

相对于构建过程⽽⾔,压缩相对我们来说只有⽣产环境打包才会做,⽽且压缩我们除了添加 cache 和多线程⽀持之外,可以优化的空间较⼩。我们在使⽤ terser-webpack-plugin 的时候可以通过下⾯的配置开启多线程和缓存:

const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
  optimization: {
    minimizer: [
      new TerserPlugin({
        cache: true, // 开启缓存
        parallel: true // 多线程
      })
    ]
  }
};

使⽤externals优化cdn静态资源

公司有cdn,静态资源有部署到cdn有链接了,我们使⽤cdn,我们的bundle⽂件⾥,就不⽤打包进去这个依赖了,体积会⼩很多,我们可以将⼀些JS⽂件存储在 CDN 上(减少 Webpack 打包出来的 js 体积),在index.html中通过标签引⼊,如:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
  </head>
  <body>
    <div id="root">root</div>
    <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
  </body>
</html>

我们希望在使⽤时,仍然可以通过 import 的⽅式去引⽤(如 import $ from 'jquery' ),并且希望webpack 不会对其进⾏打包,此时就可以配置 externals

module.exports = {
 //...
  externals: {
    //jquery通过script引⼊之后,全局中即有了 jQuery 变量
    'jquery': 'jQuery'
  }
}

使⽤静态资源路径publicPath(CDN)

CDN通过将资源部署到世界各地,使得⽤户可以就近访问资源,加快访问速度。要接⼊CDN,需要把⽹⻚的静态资源上传到CDN服务上,在访问这些资源时,使⽤CDN服务提供的URL

// webpack.config.js
output:{
  publicPath: '//cdnURL.com', //指定存放JS⽂件的CDN地址
}

代码压缩

借助postcsscssnano,实现css代码的压缩,借助terser-webpack-plugin,实现js的压缩,借助html-webpack-plugin,实现html的压缩

  • 压缩html

    plugin: [
      new htmlWebpackPlugin({
        title: "京东商城",
        template: "./index.html",
        filename: "index.html",
        minify: {
          // 压缩HTML⽂件
          removeComments: true, // 移除HTML中的注释
          collapseWhitespace: true, // 删除空⽩符与换⾏符
          minifyCSS: true // 压缩内联css
        }
      }),
    ]
    
  • 压缩css

    webpack.config.js

    {
      test: /\.css$/,
      use: [
        'style-loader',
        'css-loader',
        'postcss-loader'
      ]
    },
    

    postcss.config.js

    module.exports = {
      plugins: [
        require('autoprefixer'),
        require('cssnano')
      ]
    };
    
  • 压缩js

    mode=production 下,Webpack 会⾃动压缩代码,我们可以⾃定义⾃⼰的压缩⼯具,这⾥推荐terser-webpack-plugin,它是 Webpack 官⽅维护的插件,使⽤terser来压缩 JavaScript 代码。UglifyJS在压缩 ES5 ⽅⾯做的很优秀,但是随着 ES6 语法的普及,UglifyJSES6 代码压缩上做的不够好,所以有了 uglify-es 项⽬,但是之后 uglify-es 项⽬不在维护了,terser 是从 uglify-es 项⽬拉的⼀个分⽀,来继续维护。

    const TerserPlugin = require('terser-webpack-plugin');
    module.exports = {
      optimization: {
        minimizer: [
          new TerserPlugin()
        ]
      }
    };
    

    在实际开发中,我们可以通过移除⼀些不⽤的代码从⽽达到优化代码体积的作⽤,Tree-Shaking 也是依赖这个插件的

    new TerserPlugin({
      // 使⽤ cache,加快⼆次构建速度
      cache: true,
      terserOptions: {
        comments: false,
        compress: {
          // 删除⽆⽤的代码
          unused: true,
          // 删掉 debugger
          drop_debugger: true, // eslint-disable-line
          // 移除 console
          drop_console: true, // eslint-disable-line
          // 移除⽆⽤的代码
          dead_code: true // eslint-disable-line
        }
      }
    });
    

    压缩是发布前处理最耗时间的⼀个步骤,在 Webpack 配置中可以通过开启 terser-webpack-plugin 的多线程压缩来加速我们的构建压缩速度:

    const TerserPlugin = require('terser-webpack-plugin');
    module.exports = {
      optimization: {
        minimizer: [new TerserPlugin(
          parallel: true // 多线程
        )],
      },
    };
    

tree Shaking:擦除⽆⽤的JS,CSS

webpack4.x开始⽀持 tree shaking概念,顾名思义,”摇树”,清除⽆⽤ css,js(Dead Code)
Dead Code ⼀般具有以下⼏个特征

  • 代码不会被执⾏,不可到达

  • 代码执⾏的结果不会被⽤到

  • 代码只会影响死变量(只写不读)

  • Js tree shaking只⽀持ES module的引⼊⽅式!!!!

  • Css tree shaking

    npm install glob-all purify-css purifycss-webpack -D
    
    const PurifyCSS = require('purifycss-webpack')
    const glob = require('glob-all')
    
    // ...
    plugins: [
    // 清除⽆⽤ css
      new PurifyCSS({
        paths: glob.sync([
          // 要做 CSS Tree Shaking 的路径⽂件
          path.resolve(__dirname, './src/*.html'), // 请注意,我们同样需要对 html ⽂件进⾏ tree shaking
          path.resolve(__dirname, './src/*.js')
        ])
      })
    ]
    // ...
    
  • JS tree shaking

    只⽀持import⽅式引⼊,不⽀持commonjs的⽅式引⼊

    ⽣产模式不需要配置,默认开启

    // ...
    optimization: {
      usedExports: true // 哪些导出的模块被使⽤了,再做打包
    }
    // ...
    

    只要modeproduction就会⽣效,develpomenttree shaking是不⽣效的,因为webpack为了⽅便你的调试

    sideEffects 处理副作⽤
    //package.json

    "sideEffects":false
    

    正常对所有模块进⾏tree shaking , 仅⽣产模式有效,需要配合usedExports使用

    或者 在数组⾥⾯排除不需要tree shaking的模块

    "sideEffects": ['*.css','@babel/polyfill']
    

代码分割 code Splitting

单⻚⾯应⽤spa

打包完后,所有⻚⾯只⽣成了⼀个bundle.js

  • 代码体积变⼤,不利于下载
  • 没有合理利⽤浏览器资源

多⻚⾯应⽤mpa:

如果多个⻚⾯引⼊了⼀些公共模块,那么可以把这些公共的模块抽离出来,单独打包。公共代码只需要
下载⼀次就缓存起来了,避免了重复下载。

其实code Splitting概念 与 webpack并没有直接的关系,只不过webpack中提供了⼀种更加⽅便的⽅法供我们实现代码分割

webpack的配置基于split-chunks-plugin

optimization: {
  splitChunks: {
    chunks: "all", // 所有的 chunks 代码公共的部分分离出来成为⼀个单独的⽂件
  },
},
optimization: {
  splitChunks: {
    chunks: 'async',  //对同步 initial,异步 async,所有的模块有效 all
    minSize: 30000, //最⼩尺⼨,当模块⼤于30kb
    maxSize: 0, //对模块进⾏⼆次分割时使⽤,不推荐使⽤
    minChunks: 1, //打包⽣成的chunk⽂件最少有⼏个chunk引⽤了这个模块
    maxAsyncRequests: 5,  //最⼤异步请求数,默认5
    maxInitialRequests: 3,  //最⼤初始化请求书,⼊⼝⽂件同步请求,默认3
    automaticNameDelimiter: '-',  //打包分割符号
    name: true, //打包后的名称,除了布尔值,还可以接收⼀个函数function
    cacheGroups: {  //缓存组
      vendors: {
        test: /[\\/]node_modules[\\/]/, // 判断引入库是否是node_modules里的
        name: "vendor",  // 要缓存的 分隔出来的 chunk 名称
        priority: -10, //缓存组优先级 数字越⼤,优先级越⾼
        // filename: 'vendor.min.js'  // 设置代码分割后的文件名,仅在chunks为initial时能用,否则报错
      },
      other:{
        chunks: "initial",  // 必须三选⼀: "initial" | "all" | "async" (默认就是async)
        test: /react|lodash/,   // 正则规则验证,如果符合就提取 chunk,
        name:"other",
        minSize: 30000,
        minChunks: 1,
      },
      default: {
        minChunks: 2, // 在拆分之前共享模块的最小块数, 默认为1,表示只要有一个地方引入,就抽离到公共模块,设置为2表示 有两个chunk 引入同一个人文件才进行抽离,设置2更为合理一点
        priority: -20,
        reuseExistingChunk: true  // 允许在模块完全匹配时重用现有的块,而不是创建新的块
      }
    }
  }
}

webpack-bundle-analyzer: 分析webpack打包后的模块依赖关系:

// npm install webpack-bundle-analyzer -D
const BundleAnalyzerPlugin = require('webpack-bundleanalyzer').BundleAnalyzerPlugin;
module.exports = merge(baseWebpackConfig, {
 //....
 plugins: [
 //...
  new BundleAnalyzerPlugin(),
 ]
})

development vs Production模式区分打包

区分环境打包可以创建多个不同环境的config文件,执行打包命令的时候指定对应的config文件,我们可以定义一个基础文件webpack.common.config.js存放公用的配置,然后再创建其他环境的配置文件,例如webpack.prod.config.jswebpack.dev.config.jswebpack.test.config.js等,在里面进行公共配置的合并

例如:

const merge = require("webpack-merge")
const commonConfig = require("./webpack.common.js")
const devConfig = {
 ...
}
module.exports = merge(commonConfig,devConfig)

package.js

"scripts":{
 "dev": "webpack --config ./webpack.dev.config.js",
 "build": "webpack --config ./webpack.prod.config.js"
}

还可以借助cross-env基于环境变量区分,由于不同平台之间环境变量稍有不同,因此需要借助这个工具进行统一,这个工具可以在我们执行打包命令的时候,自动修改nodeprocess.env环境变量的配置,比如

"scripts":{
 "dev": "cross-env NODE_ENV='development' webpack --config ./webpack.dev.config.js",
 "build": "cross-env NODE_ENV='production' webpack --config ./webpack.prod.config.js"
}

我们执行npm run dev的时候,process.env.NODE_ENV 被设置为development,而执行npm run build的时候process.env.NODE_ENV 被设置为production,然后我们可以根据这个环境变量,只写一份webpack.config.js配置,实现不同环境环境的打包;

例如:

const webpackEnv = process.env.NODE_ENV;
const isEnvDevelopment = webpackEnv === 'development';
const isEnvProduction = webpackEnv === 'production';

module.exports = {
  mode: isEnvProduction ? 'production' : isEnvDevelopment && 'development',
  devtool: isEnvProduction
    ? shouldUseSourceMap
      ? 'source-map'
      : false
    : isEnvDevelopment && 'cheap-module-source-map',
  // ...
}

webpack.config.js还可以导出一个函数,函数接收两个入参:

  • env:当前运行 webpack 的环境变量, 设置它时需要在启动 webpack 时候带上参数, 例如: webpack --env.production;
  • args:代表在 webpack 启动时候通过命令行传入的所有参数; 例如: --config --env --devtool 等;

例如:

"scripts": {
    "dev": "webpack --env development --config ./webpack.dev.config.js",
  },
module.exports = function(env, args) {
  console.log(env, args)
  const isEnvDevelopment = env === 'development';
  const isEnvProduction = env === 'production';
  return {
    mode: isEnvProduction ? 'production' : isEnvDevelopment && 'development',
    devtool: isEnvProduction
      ? shouldUseSourceMap
        ? 'source-map'
        : false
      : isEnvDevelopment && 'cheap-module-source-map',
    // ...
  }
}

envargs打印结果

development // env
{ // args
  _: [],
  cache: null,
  bail: null,
  profile: null,
  color: { level: 3, hasBasic: true, has256: true, has16m: true },
  colors: { level: 3, hasBasic: true, has256: true, has16m: true },
  env: 'development',
  config: './webpack.dev.config.js',
  'info-verbosity': 'info',
  infoVerbosity: 'info',
  '$0': 'node_modules\\webpack\\bin\\webpack.js'
}

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 lyucan_1@163.com