Webpack——【入门篇-下篇】

542 阅读7分钟

已同步语雀:www.yuque.com/go/doc/5319…

github:www.yuque.com/go/doc/5319…

本系列为Webpack——入门篇,包括:

1、Webpack——【入门篇-上篇】juejin.cn/post/695439…

2、Webpack——【入门篇-中篇】juejin.cn/post/695438…

3、Webpack——【入门篇-下篇】【本篇】juejin.cn/post/695438…

17 性能优化配置

分为两种

  • 开发环境性能优化

  • 优化打包构建速度

  • HMR

  • 优化调试功能

  • s****ource-map(devtool

  • 生产环境性能优化

  • 优化打包构建速度

  • one-of

  • babel缓存

  • 多进程打包

  • externals

  • dll

  • 优化代码运行性能(体积小)

  • 缓存(hash - chunkhash - contenthash)

  • tree-shaking

  • code-split

  • 懒加载/预加载

  • PWA

具体:

HMR:hot

解释:hot module replacement 热模块替换 / 模块热替换

作用:一个模块发生变化,只会重新打包这一个模块,而不是重新打包所有模块,极大的提升构建速度

  • 样式文件:可以使用HMR功能,因为style-loader内部实现了

  • js文件:默认没有HMR功能,重新打包所有模块

  • 注意:HMR功能对js处理,只能处理非入口文件,因为入口文件引入了全部内容,入口文件变化,其他文件变化

  • html文件:默认没有HMR功能,同时会导致问题:html文件不能热更新(不需要做HMR功能)

  • 解决:修改entry入口,将html文件引入

使用:

webpack.config.js

module.exports = {
  ...
  target: 'web', //自动更新
  devServer: {
    hot: true //热模块替换
  }
}

main.js

// import './print.js'
if(module.hot){
  // 一旦module.hot为true,说明开启了hotHMR功能 --> 让HMR功能代码生效
  module.hot.accept('./print.js', function(){
    // 方法会监听print.js文件,一旦发生变化,其他模块不会重新打包构建
    // 会执行后面的回调函数
    print()
  })
}

source-map(devtool)

解释:一种提供源代码到构建后代码映射技术

作用:如果构建代码错了,通过映射关系可以追踪到原代码错误

使用:开发环境用devtool: 'eval-source-map';生产环境用devtool: 'source-map',

webpack.config.js

module.exports = {
  ...
  devtool: 'source-map',
}

执行webpack,可以看到打包的dist文件夹下面有main.js.map文件,如下图

image.png

具体配置:

[inline-|hidden-|eval-][nosources][cheap-[module-]]source-map

配置字段

解释

错误定位

source-map

外部,生成单独文件

错误代码准确信息,和源代码的错误位置,精确到行、列

inline-source-map

内联,不生成单独文件,整体在main.js文件里面下方扩展,只生成一个source-map,构建速度快

同source-map

hidden-source-map

外部,生成单独文件,main.js.map

错误代码准确信息,和构建后代码的错误位置,不能追踪到源代码错误;只隐藏源代码

eval-source-map

内联,不生成单独文件,在main.js文件每一个文件后面追加sourceMappgingURL字段,每个文件都生成一个source-map,都在eval中,构建速度快

同source-map

nosources-source-map

外部,生成单独文件

错误代码准确信息,不能追踪到源代码和构建后代码错误;为了隐藏源代码,全部隐藏

cheap-source-map

外部,生成单独文件

同source-map,但是仅能精确到行,整行,不能精确到列

cheap-module-source-map

外部,生成单独文件

同cheap-source-map,但module会将loader的source-map加入

其他组合...

怎么用?

  • 开发环境

  • 速度快

  • eval > inline > cheap > ... 即 eval-cheap-source-map > eval-source-map

  • 调试友好

  • source-map > cheap-module-source-map > cheap-source-map

  • 综上:eval-source-map / eval-cheap-module-source-map

用vue、react脚手架开发,默认使用的是eval-source-map

  • 生产环境

  • 隐藏源代码

  • nosoures-source-map,hidden-source-map

  • 体积小

  • 内联会让体积变得很大,所以生产环境不能用内联

  • 调试友好

  • 同上

  • 综上:source-map

one-of

解释:只匹配一个loader

作用:一个文件只会匹配一个loader,当匹配到了就不走下面的;提升构建速度

注意:不能有两个配置处理同一类型文件,比如js文件,eslint-loader,babel-loader;需要提取一个到oneof外面

webpack.config.js

{
  rules: [
     {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'eslint-loader',
        enforce: 'pre', // 指定先后顺序,先执行
        options: {
          fix: true,
        },
     },
    // 注意:不能有两个配置处理同一类型文件,比如js文件,eslint-loader,babel-loader;需要提取一个到oneOf外面
    oneOf: [
        {
        test: /\.css$/,
        use: [...commonCssLoader],
      },
      {
        test: /\.less$/,
        use: [...commonCssLoader, 'less-lodaer'],
      },
      // 正常来讲,一个文件只能被一个loader处理,那么一定要指定loader执行的先后顺序
      // 先执行eslint,在执行babel
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
        options: {
          presets: [
            [
              '@babel/preset-env',
              {
                useBulitIns: 'usage',
                corejs: {
                  version: 3,
                },
                targets: {
                  chrome: '60',
                  firefox: '60',
                  ie: '9',
                  safari: '10',
                  edge: '17',
                },
              },
            ],
          ],
        },
      },
      {
        test: /\.(jpg|png|gif)/,
        loader: 'url-loader',
        options: {
          limit: 8 * 1024,
          name: '[hash:10].[ext]',
          outputPath: 'imgs',
          esModule: false, // html中用的是common, url-loader中用的是es6,需要关掉
        },
      },
      {
        test: /\.html/,
        loader: 'html-loader', // 处理html中的img
      },
      {
        exclude: /\.(js|css|less|html|jpg|png|gif)/,
        loader: 'file-lodaer',
        options: {
          outputPath: 'media',
        },
      },
    ],
  ]
}

缓存 hash

  • babel缓存:cacheDirectory --> 让第二次打包构建速度更快

生产环境下,文件变化时读取缓存

{
  test: /\.js$/,
  exclude: /node_modules/,
  loader: 'babel-loader',
  options: {
    presets: [
        [
            '@babel/preset-env',
            {
            useBulitIns: 'usage',
            corejs: {
              version: 3,
            },
            targets: {
              chrome: '60',
              firefox: '60',
              ie: '9',
              safari: '10',
              edge: '17',
            },
          },
      ],
    ],
    cacheDirectory: true, //开启babel缓存,第二次构建时,读取缓存,加快速度
  },
},
  • **文件资源缓存:**hash --> 让代码上线运行缓存更好使用

  • hash:每次webpack构建时会生成一个唯一的hash值

  • 问题:因为js和css同时使用一个hash值,如果重新打包就会导致所有的缓存失效(仅改变一个文件)

  • chunkhash:根据chunk生成hash值,如果打包来源于同一个chunk,那么hash值一样

  • 问题:css和js的hash值还是一样

  • 因为css是在js中引入的,所以同属一个chunk

  • contenthash:根据文件的内容生成hash值,不同文件hash值一定不一样,只有改变的文件会重新生成hash值,没变的不会

    output: { filename: 'main.[contenthash:10].js', //文件资源缓存处理 'main.[chunkhash:10].js' || 'main.[hash:10].js' path: resolve(__dirname, 'dist'), publicPath: '/', },

    ...

    new MiniCssExtractPlugin({ filename: 'css/index.[contenthash:10].css', // 文件资源缓存处理 'css/index.[hash:10].css' || 'css/index.[hash]:10].css' }),

tree-shaking

作用:去除无用代码,减少代码体积

前提:必须使用es6模块,mode = production环境默认开启tree-shaking,webpack默认的UglifyJsPlugin实现

最好在package.json配置"sideEffects": ["*.css","*.less"],防止文件被摇掉

"sideEffects": false,
// 所有代码都没有副作用(都可以进行tree shaking)
// 问题:可能会把css文件摇掉
"sideEffects": ["*.css","*.less"], //.css,*.less不进行tree shaking

code-split 代码分割

**作用:**将打包生成的一个chunk分割成多个代码块

  • 并行加载,加快速度
  • 按需加载,(spa,单文件需要按照路由分割)

方法:

  • 多入口分割

问题:很难配置多入口,不灵活

//webpack.config.js
{ // 单入口
  // entry: './src/main.js',
  // 多入口,有一个入口,最终输出就有一个bundle
  entry: {
    main: "./src/main.js",
    print: "./src/print.js"
  },
  output: {
    filename: '[name].[contenthash:10].js',
  }
}
  • optimization配置

    //webpack.config.js { optimization: { /* 作用 1. 可以将node_modules中代码单独打包成一个chunk最终输出 2. 多入口还可以:自动分析多入口文件中有没有公共的依赖文件,如果有,回单独打包成一个chunk,而不是每个文件都去打包一次 */ splitChunks: { chunks: 'all' } } }

  • import动态导入

    //main.js // import {mul, count} from './print'

    // console.log(mul(5,2)) // console.log(count(5,2))

    /* 通过js代码,让某个文件被单独打包成一个chunk import动态导入语法:能将某个文件单独打包 常用作路由 / import(/ webpackChunkName: 'print' */'./print') .then(( {mul, count}) => { console.log(mul(5,2)) console.log(count(5,2)) }) .catch( () => { console.log('文件加载失败') })

执行webpack,可以看到结果如下图,print.js分割成单独文件

image.png

懒加载和预加载

利用代码分割-动态加载实现

//index.html
 <button id="btn">按钮</button>

//main.js
document.getElementById('btn').onclick = function(){
  /*
    懒加载:当文件需要使用时才加载
      问题:但当文件较大时,不合适,加载时间太长,导致用户等待,这时候考虑预加载
    预加载:配置webpackPrefetch: true;prefetch:会在使用之前,提前加载js文件
      问题:兼容性不好
    正常加载 && 预加载
      正常加载:并行加载,同一时间加载多个文件,浏览器一个域名下统一时间只能并行加载最多6个文件,超过需往后排队
      预加载:等其他资源加载完毕后,浏览器空闲后再加载
  */
  
  import(/* webpackChunkName: 'test', webpackPrefetch: true */'./test')
  .then(() => {
    console.log('记载test.js成功')
  })
  .catch(() => {
    console.log('文件加载失败')
  })
}

PWA

解释:渐进式网络开发应用程序(离线也可以访问)

使用:workbox --> workbox-webpack-plugin

// webpack.config.js
const workboxWebpackPlugin = require('workbox-webpack-plugin')

{
  ...
  plugins: [
     // PWA
    // npm i workbox-webpack-plugin -D
    new workboxWebpackPlugin.GenerateSW({
      /*
        1. 帮助serviceworker快速启动
        2. 删除旧的serviceworker

        生成一个 serviceworker 配置文件,需要通过配置文件注册serviceworker
       */
      clientsClaim: true,
      skipWaiting: true
    })
  ]
}

// main.js
// PWA 注册serviceworker
// 注意 sw代码必须运行在服务器上 npm i serve -g;serve -s dist 启动服务器,将dist目录暴露出去
// 处理兼容性问题
if('serviceWorker' in navigator){
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/service-worker.js')
    .then( () => {
      console.log('sw注册成功')
    })
    .catch( () => {
      console.log('sw注册失败')
    })
  })
}

执行webpack,可以看到

image.png

image.png

多进程打包

解释:js是单线程,排队构建速度慢

使用:npm i thread-loader -D

双刃剑:

  • 缺点:进程启动大概为600ms,进程间通信也有开销,只有工作消耗时间比较长,才需要多进程打包,比如js babel-loader

    // webpack.config.js { ... { test: /.js$/, exclude: /node_modules/, use: { 'thread-loader', 'babel-loader' } }
    }

extranals

解释:外部的,拒绝某些包被打包进来,在index.html cdn引入

// webpack.config.js
{ 
  ...
  externals: {
    // 拒绝jquery被打包进来;需要在index.html cdn引入             
    jquery: 'jQuery' // 包名
  }
}

// main.js
// externals
import $ from 'jquery'

console.log($)

// index.html
 <script crossorigin="anonymous" integrity="sha512-6ORWJX/LrnSjBzwefdNUyLCMTIsGoNP6NftMy2UAm1JBm6PRZCO1d7OHBStWpVFZLO+RerTvqX/Z9mBFfCJZ4A==" src="https://lib.baomitu.com/jquery/3.6.0/jquery.slim.min.js"></script>

dll

解释:动态连接库

新建webpack.dll.js,配置如下

// webpack.dll.js
/*
    使用dll技术,对某些库(第三方库:jQuery、react、vue...)进行单独打包
    webpack命令默认执行webpack.config.js文件
    运行:webpack --config webpack.dll.js
*/
const path = require("path")
const webpack = require("webpack")

module.exports = {
    entry: {
        // 最终打包生成的[name] --> jquery
        // ['jquery'] --> 要打包的库是jquery
        jquery: ['jQuery']
    },
    output: {
        filename: '[name].js',
        path: path.join(__dirname, 'dll'),
        library: '[name]_[hash]', // 打包的库里面向外暴露出去的内容叫什么名字
    },
    plugins: [
        // 打包生成一个manifest.json --> 文件提供和jquery映射
        new webpack.DllPlugin({
            name: '[name]_[hash]', // 映射库的暴露的内容名称
            path: path.join(__dirname, 'dll/manifest.json'), //输出文件路径
        })
    ],
    mode: "production"
}

执行webpack --config webpack.dll.js,可以看到打包后的结果

image.png

webpack.config.js配置

// webpack.config.js
// npm i add-asset-html-webpack-plugin -D
{
  const webpack = require("webpack")
    const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin')
  ...
  pluigns: [
     // dll
    // 告诉webpack哪些库不参与打包。同时使用时的名称也得变
    new webpack.DllReferencePlugin({
      manifest: path.join(__dirname, 'dll/manifest.json')
    }),
    // npm i add-asset-html-webpack-plugin -D
    // 将某个文件打包输出出去,并在html中自动引入该资源
    new AddAssetHtmlWebpackPlugin({
      filepath: path.join(__dirname, 'dll/jquery.js')
    })
  ]
}

执行webpack,可以看到单独生成了一个jquery,并且在index.html中引入了

image.png

externals && dll

externals:彻底不打包,需要手动引入cdn

dll:只需要打包一次,后面就只需执行webpack即可;第二次之后就不需要考虑第三方库了,提高速度

18 回顾:性能优化总结

分为两种

  • 开发环境性能优化

  • 优化打包构建速度

  • HMR

  • 优化调试功能

  • source-map

  • 生产环境性能优化

  • 优化打包构建速度

  • one-of

  • babel缓存

  • 多进程打包

  • externals

  • dll

  • 优化代码运行性能(体积小)

  • 缓存(hash - chunkhash - contenthash)

  • tree-shaking

  • code-split

  • 懒加载/预加载

  • PWA

19 entry详细配置

entry:入口起点

  • string --> './src/index.js' 默认值

  • 单入口

  • 打包成一个chunk,输出一个bundle文件

  • 此时chunk的名称默认是main

  • array --> ['./src/index.js', './src/add.js']

  • 多入口

  • 所有入口文件最终只会形成一个chunk,输出出去只有 一个bundle文件,add.js会打包到index.js中

  • 此时chunk的名称默认是main

  • object --> 如下

  • 多入口

  • 有几个入口文件就形成几个chunk,输出几个bundle文件

  • 此时chunk的名称是key

    // string entry: './src/index.js'

    // array entry: ['./src/index.js', './src/add.js']

    // object entry: { index: './src/index.js', add: './src/add.js }

    // 特殊用法 如dll配置 entry: { index: ['./src/index.js', './src/count.js'], add: './src/add.js }

20 output详细配置

output: {
  // 文件名称(指定名称+目录)
  filename: 'js/[name].js',
  // 输出文件目录(将来所有资源输出的公共目录)
  path: resolve(__dirname, 'build'),
  // 所有资源引入公共路径前缀 --> imgs/a,jpg --> /imgs/a.jpg
  publichPath: '/',
  // 非入口chunk名称重命名,不定义的话会走filename,都叫main,webpack会用id命名 0.js 1.js 2.js ...
  chunkFilename: 'js/[name]_chunk.js',
  // 打包的js默认是一个function
  // 整个库向外暴露的变量名
  library: '[name].js', // var main = (function(modules){})
  // 变脸名添加哪个上
  libraryTarget: 'window' // browser window['main'] = (function(modules){})
  // libraryTarget: 'global' // node
  // libraryTarget: 'commonjs' // browser exports['main'] = (function(modules){})
  
}

21 module详细配置

module: {
    rules: {
    // loader的配置
    {
      test: /\.css$/,
      // 多loader用use
      use: ['style-loader', 'css-loader']
    },
    {
      test: /\.js$/,
      exclude: /node_modules/,
      include: resolve(__dirname, 'src'),
      enforce: 'pre', //优先执行
      // enforce: 'post', //延后执行
      // 单个loader用loader
      loader: 'eslint-loader',
      options: {
        ...
      }
    },
    {
      // 以下配置只会生效一个
      oneOf: []
    }
  }
}

22 resolve详细配置

// 解析模块规则
resolve: {
  // 配置解析模块路径别名:简写路径
    alias: {
    $css: resolve(__dirname, 'src/css')
  },
  // 配置省略文件路径的后缀名
  extensions: ['.js', '.json', '.css'],
  // 告诉webpack解析模块是去找哪个目录,默认去当前路径层级找,找不到找上一层级的
  modules: [resolve(__dirname, '../../node_modules'), 'node_modules'] //先找第一个,再找第二个,防止第一个找不到
}

23 devServer详细配置

// 开发环境
devServer: {
  // 运行代码的目录
  contentBase: resolve(__dirname, 'build'),
  // 监视contentBase目录下的所有文件,一旦文件变化就会reload
  watchContentBase: true,
  watchOptions: {
    // 忽略监视文件
    ignored: /node_modules/
  },
  // 启动gzip压缩
  compress: true,
  // 端口号
  port: 50000,
  // 域名
  host: 'localhost',
  // 自动打开浏览器
  open: true,
  // 开启HMR功能
  hot: true,
  // 不要显示启动服务器日志信息
  clientLogLevel: 'none',
  // 除了一些基本的信息以外,其他内容都不要显示
  quite: true,
  // 如果出错了,不要全屏提示
  overlay: false,
  // 服务器代理 --> 解决开发环境跨域问题
  proxy: {
    '/api': {
            target: 'http://localhost:3000',
        pathRewrite: {
            '^/api': ''
        } 
    }
  }
}

24 optimization详细配置

const TerserWebpackPlugin = require('terser-webpack-plugin')
// 生产环境
optimization: {
  splitChunks: {
    chunks: "all",
    // 以下是默认值,可以不写
    miniSize: 30 * 1024, //分割的chunk最小为30kb,大于30kb才会被分割
    maxSize: 0, // 最大没有限制
    minChunks: 1, // 要提取的chunk最少被引用一次
    maxAsyncRequest: 5, // 按需加载时并行加载的文件最大数量
    maxInitialRequests: 3, // 入口js文件最大并行请求数量
    automaticNameDelimiter: '~', // 命名连接符
    name: true, // 可以使用命名规则
    cacheGroups: {
      // 分割chunk的组
      // node_modules文件会被打包到vendors组chunk中 => vendors~xxx.js
      // 满足上面的公共规则,如:大小超过30kb,至少被引用一次
      vendors: {
        test: /[\\/]node_modules[\\/]/,
        // 优先级
        priority: -10
      },
      defalut: {
        // 要提取的chunk至少被引用两次
        minChunks: 2,
        // 优先级
        priority: -20,
        // 如果当前要打包的模块,和之前已经被提取的模块是同一个,就会复用,而不是重新打包模块
        reuseExistingChunk: true,
      }
    }
  },
  // 将当前模块的记录其他模块的hash单独打包为一个文件runtime
  // 解决:修改a文件导致b文件的contenthash变化
  runtimeChunk: {
    name: entrypoint => `runtime-${entrypoint.name}`
  },
  minimizer: {
    // 配置生产环境的压缩方案:js和css
    new TerserWebpackPlugin({
      // 开启缓存
        cache: true,
      // 开启多线程打包
      parallel: true,
      // 启用source-map
      sourceMap: true
    }
  }
}

terser-webpack-plugin(webpack.docschina.org/plugins/ter…)

如果你使用的是 webpack v5 或以上版本,你不需要安装这个插件。webpack v5 自带最新的 terser-webpack-plugin。如果使用 webpack v4,则必须安装 terser-webpack-plugin v4 的版本,不再用UglifyJsPlugin

已同步语雀:www.yuque.com/go/doc/5319…

github:www.yuque.com/go/doc/5319…

本系列为Webpack——入门篇,包括:

1、Webpack——【入门篇-上篇】juejin.cn/post/695439…

2、Webpack——【入门篇-中篇】juejin.cn/post/695438…

3、Webpack——【入门篇-下篇】【本篇】juejin.cn/post/695438…