Vito's blog
  • js基础
  • es6+基础
  • js进阶
  • js手写系列
  • typescript
  • js读书笔记

    • js异步编程
    • 你不知道的js系列
  • vue2基础
  • vue2进阶
  • vue3基础
  • vuex
  • vue router
  • pinia
html
  • 图解css3
  • css进阶
  • scss
浏览器
网络通信
  • git使用笔记
  • linux使用记录
  • npm
  • webpack基础
  • webpack5
  • qiankun
  • 排序算法
  • 剑指offer
  • 常见算法
  • 排序算法python
  • 剑指offer-python
  • labuladong算法
github主页
  • js基础
  • es6+基础
  • js进阶
  • js手写系列
  • typescript
  • js读书笔记

    • js异步编程
    • 你不知道的js系列
  • vue2基础
  • vue2进阶
  • vue3基础
  • vuex
  • vue router
  • pinia
html
  • 图解css3
  • css进阶
  • scss
浏览器
网络通信
  • git使用笔记
  • linux使用记录
  • npm
  • webpack基础
  • webpack5
  • qiankun
  • 排序算法
  • 剑指offer
  • 常见算法
  • 排序算法python
  • 剑指offer-python
  • labuladong算法
github主页
  • webpack基础用法

webpack基础用法

概述

webpack是基于nodejs平台的,前端资源构建工具,同类工具还有vite、rollup、parcel等

5个核心概念

  • entry 打包入口起点
  • output 打包后资源输出配置
  • loader(module) 处理非js、json文件
  • plugins 扩展webpack功能,如处理环境变量打包优化、压缩等
  • mode 指示webpack使用相应模式的配置,取值为development、production

基本工作流程:

  1. 加载配置文件

  2. 根据文件中entry解析所依赖的所有module

  3. 对每个module使用loader中的规则,将module转换为webpack能处理的资源

  4. 并生成chunk文件输出,整个过程中plugin会利用hook函数在适当的时机运行

基本配置

使用webpack打包的项目,通常有webpack.config.js配置文件,以下以代码及注释的方式展示基本配置:

/*
  webpack.config.js  webpack的配置文件,语法为nodejs的commonjs风格
*/

const { resolve } = require('path'); // resolve用来拼接绝对路径的方法
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');

process.env.NODE_ENV = 'production'; // 定义nodejs环境变量:决定使用browserslist的哪个环境

const commonCssLoader = [ // 以常量的形式提取公共loader配置进行复用
  'style-loader', // 创建style标签,将js中的样式资源插入进行,添加到head中生效
  // MiniCssExtractPlugin.loader, // 该插件的loader取代style-loader,提取css文件单独打包
  'css-loader', // 将css文件变成commonjs模块加载js中,里面内容是样式字符串
  { // css兼容性处理,根据package.json中browserslist配置进行兼容
    loader: 'postcss-loader',
    options: {
      ident: 'postcss',
      plugins: () => [
        require('postcss-preset-env')() // postcss的预设环境插件
      ]
    }
  },
];
module.exports = {
  // 多入口配置,支持String、Array、Object,其中只有Object会输出多个chunk,Object的value也可以取String、Array
  entry: './src/index.js',
  output: {  // 输出
    filename: 'built.js', // 输出文件名
    // 输出路径; __dirname是nodejs的环境变量,代表当前文件的目录绝对路径
    path: resolve(__dirname, 'build'),
    publicPath: '/', // html中src的公共路径前缀
    chunkFilename: 'js/[name]_chunk.js', // 非入口chunk的名称
    // library: '[name]', // 整个库向外暴露的变量名
    // libraryTarget: 'window/global/commonjs' // 变量挂载目标 可通过[libraryTarget].[library]访问
  },
  module: { // loader的配置
    rules: [
      // 详细loader配置,不同文件必须配置不同loader处理
      {
        test: /\.css$/,  // 匹配哪些文件
        use: [ // 数组中指定用于处理test匹配到文件的loader,多个loader从后向前执行
          ...commonCssLoader
        ]
      },
      {
        test: /\.less$/,
        use: [
          ...commonCssLoader, 'less-loader' // 将less文件编译成css文件,需要下载 less-loader和less
        ]
      },
      {
        // 处理图片资源
        test: /\.(jpg|png|gif)$/,
        // 使用一个loader
        // 下载 url-loader file-loader
        loader: 'url-loader',
        options: {
          limit: 8 * 1024, // 图片小于该值将会被base64编码为dataurl
          esModule: false, // 关闭url-loader默认的es6模块化,使用commonjs与其他loader保持一致
          name: '[hash:10].[ext]', // 输出图片资源名,[hash:10]取图片的hash的前10位,[ext]取文件原来扩展名
          outputPath: 'imgs' // 以build下的子目录
        }
      },
      {
        test: /\.html$/,
        // 处理html文件的img图片(负责引入img,从而能被url-loader进行处理)
        loader: 'html-loader'
      },
      { // 其他资源打包
        exclude: /\.(css|js|html|less)$/, // 排除css/js/html资源
        loader: 'file-loader',
      },
      { // js语法检查loader,在package.json中eslintConfig中设置检查规则
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'eslint-loader',
        enforce: 'pre', // 当js被多个loader执行时,此loader优先执行;取post则延后执行
        options: {
          fix: true // 自动修复eslint的错误
        }
      },
      { // js兼容性处理,对不支持es6的浏览器兼容
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
        options: {
          // 预设:指示babel做怎么样的兼容性处理
          presets: [
            [
              '@babel/preset-env', // 预设环境不能转换es6的api,如Promise,
              // 还可取@babel/polyfill,引入全部兼容性,core-js按需处理兼容性
              {
                useBuiltIns: 'usage', // 按需加载
                corejs: {
                  version: 3 // 指定core-js版本
                },
                targets: { // 指定兼容性做到哪个版本浏览器
                  chrome: '60',
                  firefox: '60',
                  safari: '10',
                  edge: '17'
                }
              }
            ]
          ],
          cacheDirectory: true // 开启babel缓存,第二次构建时会读取之前的缓存
        }
      }
    ]
  },
  plugins: [  // plugins的配置
    // html-webpack-plugin
    // 功能:默认会创建一个空的HTML,自动引入打包输出的所有资源(JS/CSS)
    // 需求:需要有结构的HTML文件
    new HtmlWebpackPlugin({
      template: './src/index.html', // 指定html模板
      minify: { // 压缩html代码
        collapseWhitespace: true, // 移除空格
        removeComments: true // 移除注释
      }
    }),
    new MiniCssExtractPlugin({ // 用于将css与js代码分离单独打包,配合其loader使用
      filename: 'css/built.css' // 对输出的css文件进行重命名
    }),
    new OptimizeCssAssetsWebpackPlugin() // css压缩插件
  ],
  // 模式
  mode: 'development', // 开发模式
  // mode: 'production',  // 生产环境,会自动启用js压缩

  // 开发服务器 devServer:用来自动化(自动编译,自动打开浏览器,自动刷新浏览器~~)
  // 特点:只会在内存中编译打包,不会有任何输出
  // 启动devServer指令为:npx webpack-dev-server
  devServer: {
    contentBase: resolve(__dirname, 'build'), // 项目构建后路径
    compress: true,  // 启动gzip压缩
    port: 3000,  // 端口号
    open: true, // 自动打开浏览器
  }
}

优化配置

HMR

HMR 热模块替换,只打包发生变化的模块,提升开发时的构建速度

module.exports = {
  entry:['./src/js/index.js', './src/index.html'], // 多入口配置
  // 省略其他基本配置
  devServer:{ // 省略devServer其他配置
    // style-loader内部实现了对HMR的支持,js文件需要修改代码才能对HMR支持,html默认不支持HMR
    hot: true // 开启HMR功能
  }
}


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

source-map

source-map: 源代码到构建后代码映射(如果构建后代码出错了,通过映射可以追踪源代码错误)

module.exports = { // 省略其他配置
  devtool: 'eval-source-map'
}

取值可以为[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map

  • source-map:外部; 错误代码准确信息和源代码的错误位置
  • inline-source-map:内联; 只生成一个内联source-map; 错误代码准确信息和源代码的错误位置
  • hidden-source-map:外部; 错误代码错误原因,但是没有错误位置; 不能追踪源代码错误,只能提示到构建后代码的错误位置
  • eval-source-map:内联; 每一个文件都生成对应的source-map,都在eval; 错误代码准确信息和源代码的错误位置
  • nosources-source-map:外部; 错误代码准确信息, 但是没有任何源代码信息;
  • cheap-source-map:外部; 错误代码准确信息和源代码的错误位置; 只能精确的行
  • cheap-module-source-map:外部; 错误代码准确信息和源代码的错误位置; module会将loader的source map加入

内联 和 外部的区别:1. 外部生成了文件,内联没有 2. 内联构建速度更快

开发环境:需要速度快,调试更友好

  • 速度快(eval>inline>cheap>...) eval-cheap-souce-map;eval-source-map
  • 调试更友好
    souce-map;cheap-module-souce-map;cheap-souce-map
  • 推荐使用 eval-source-map / eval-cheap-module-souce-map

生产环境:需要考虑源代码隐藏 调试友好

  • 内联会让代码体积变大,所以在生产环境不用内联
  • nosources-source-map 全部隐藏
  • hidden-source-map 只隐藏源代码,会提示构建后代码错误信息
  • 推荐使用source-map / cheap-module-souce-map

oneof

module.exports = {
  module:{
    rules:[
      {test:/\.js$/, loader: 'eslint-loader'},
      { oneof:[ // oneof修饰的rules对每个文件仅引用一个规则,
      // 即css文件匹配到css规则后不会取判断是否为js文件
          {test:/\.css$/,use:[...commonCssLoader]},
          {test:/\.js$/}
        ]
      }
    ]
  }
}

缓存

  • babel缓存,二次构建时读取缓存可提高构建速度
  • 文件资源缓存,通过文件资源hash命名进行缓存
    • [hash:位数]: webpack每次构建时都会生成一个hash,重新打包会导致所有的缓存失效
    • [chunkhash:位数]: 根据chunk生成hash值,打包来源于同一个chunk时hash值就一样
    • [contenthash:位数](推荐使用):根据文件内容生成hash,每个文件的hash值不同,可以更好的利用缓存机制

tree shaking

tree shaking 用于去除不会被使用的代码,减小代码体积;在配置mode:production并且js代码使用es6模块化即可启用tree shaking

tree shaking可能会把css,@babel/polyfill文件删掉,可在package.json中配置"sideEffects": false或"sideEffects": ["*.css", "*.less"]

tree shaking无法应用于多层嵌套中三级及以上的模块

code split

module.exports = { // 省略了其他配置
  entry: {
    // 多入口:有n个入口,对应输出n个bundle
    index: './src/js/index.js',
    test: './src/js/test.js'
  },
  output: {
    filename: 'js/[name].[contenthash:10].js', // [name]:取文件名
    path: resolve(__dirname, 'build')
  },
    /* optimization
    1. 可以将node_modules中代码单独打包一个chunk最终输出
    2. 自动分析多入口chunk中公共的文件,打包成单独一个chunk
  */
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  },
}
  • 代码拆分还可在代码中使用es6的import(/* webpackChunkName: '单独打包的包名',webpackPrefetch: true */).then()方式懒加载进行拆分,被这种方式引入的模块会被单独打包
  • 预加载 webpackPrefetch: 等其他资源加载完毕,浏览器空闲时加载资源

PWA

PWA: 渐进式网络开发应用程序(离线可访问),依赖于workbox,和服务端(未做深入了解,后续补充)

const WorkboxWebpackPlugin = require('workbox-webpack-plugin');
module.exports = {
  new WorkboxWebpackPlugin.GenerateSW({
    /*
      1. 帮助serviceworker快速启动
      2. 删除旧的 serviceworker
      生成一个 serviceworker 配置文件~
    */
    clientsClaim: true,
    skipWaiting: true
  })
}

/********************入口js文件中**********************/
/*
  1. eslint不认识 window、navigator全局变量
    解决:需要修改package.json中eslintConfig配置
      "env": {
        "browser": true // 支持浏览器端全局变量
      }
   2. sw代码必须运行在服务器上
      --> nodejs
      -->
        npm i serve -g
        serve -s build 启动服务器,将build目录下所有资源作为静态资源暴露出去
*/
// 注册serviceWorker
// 处理兼容性问题
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker
      .register('/service-worker.js')
      .then(() => {
        console.log('sw注册成功了~');
      })
      .catch(() => {
        console.log('sw注册失败了~');
      });
  });
}

externals

externals配置用于排除要打包的库

module.exports = {
  externals: {
    /*
      拒绝jQuery被打包进来,开发者需自己引入该库
      目标环境为浏览器情况下,代码中对jQuery的引用将被替换为window.jQuery
    */
    jquery: 'root jQuery'
  }
}

dll

顾名思义动态链接库Dynamic-link library,用于单独打包某些库,而第二次构建时就不用再次对这部分进行打包,提升构建速度

提示

此技术趋于过时

const webpack = require('webpack');
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');
module.exports = {
  plugins: [
    // 告诉webpack哪些库不参与打包,使用时的名称保存在manifest.json文件中
    new webpack.DllReferencePlugin({
      manifest: resolve(__dirname, 'dll/manifest.json')
    }),
    // 将某个文件打包输出去,并在html中自动引入该资源
    new AddAssetHtmlWebpackPlugin({
      filepath: resolve(__dirname, 'dll/jquery.js')
    })
  ]
}

/********另外该项目还需要webpack.dll.js配置文件********/
/*
  使用dll技术,对某些库(第三方库:jquery、react、vue...)进行单独打包
    当你运行 webpack 时,默认查找 webpack.config.js 配置文件
    先运行 webpack --config webpack.dll.js
*/

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

resolve解析配置

module.exports = {
 // 解析模块的规则
  resolve: {
    alias: { // 配置解析模块路径别名: 优点简写路径 缺点路径没有提示
      $css: resolve(__dirname, 'src/css')
    },
    // 配置省略文件路径的后缀名,从前到后查找文件名对应的扩展名
    extensions: ['.js', '.json', '.jsx', '.css'],
    // 配置node_modules目录路径,若不配置,默认从当前层,层层向外查找
    modules: [resolve(__dirname, '../../node_modules'), 'node_modules']
  }
}

devServer

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

optimization配置

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

webpack5

参看

Last Updated:
Contributors: vito