webpack构建工具

127 阅读16分钟

webpack是一个模块打包工具,可以把各种资源如JavaScript、CSS、图片等都视为模块,使得工程中的各种资源能够被打包成一个整体的bundle.js文件。Webpack具有很高的可配置性和灵活性,可以通过插件和loader来扩展webpack的功能,适用于大型、复杂的项目。

webpack通过一种叫做loader的机制来处理非JavaScript类型的文件,并且可以把这些文件打包成合适的格式供浏览器使用(它可以处理多种不同类型的文件(如js、css、图片等),并根据需求进行转换、压缩和打包。)。除此之外,webpack还具有代码拆分、优化、模块热替换等强大功能。

安装webpack&webpack-cli(建议安装本地开发依赖):npm i webpack webpack-cli -D

webpack原理

当webpack处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个bundle。webpack的主要原理可以概括为以下几个步骤:

  1. 入口(Entry):webpack根据配置文件指定的入口点开始分析,找出所有依赖的模块。
  2. 依赖分析(Dependency Analysis):webpack通过代码中的require、import等语句分析出模块间的依赖关系。
  3. 加载器(Loader):webpack使用加载器处理非JavaScript文件(如CSS、图片等),并将它们转换为有效的模块即浏览器可以理解的格式,以便webpack能够处理它们。
  4. 插件(Plugins):webpack允许使用范围广泛的插件来扩展其功能,插件可以执行范围广泛的任务,如打包优化、资源管理和环境变量注入等。
  5. 输出(Output):webpack将处理后的模块合并成少量的bundle,通常是一个或多个js文件,能够在浏览器中加载这些文件。

打包流程

读取配置文件 —》 找到入口文件 —》 递归解析项目中的所有依赖模块(包括js文件、css、图片等)—》 使用相应的loader编译这些模块,例如转译ES6、压缩代码、提起公共模块等 —》 合并模块(webpack将所有模块合并成一个或多个包bundle,根据配置规则将模块分组打包)—》 输出最终的包到指定目录下,以便在浏览器中运行。

webpack配置

五个核心概念:

  • Entry:入口起点(entry point)指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。

  • Output:output 属性告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件,默认值为 ./dist。

  • Loader:webpack通过loaders去支持其他文件类型并把他们转成有效模块,并且添加到依赖图中。loader本身是一个函数,接收源文件作为参数,返回转换的结果。(webpack 自身只能解析 JavaScript和json两种文件类型)。

    常见loader:

    • babel-loader:转换ES6、ES7等js新语法;
    • css-loader:支持.css文件的加载和解析,把它当成一个模块;
    • style-loader:将css加到style标签内,否则打包后的样式不生效;
    • less-loader:把less文件转换成css;
    • ts-loader:将TS转换成JS;
    • file-loader:进行图片、字体的打包;
    • raw-loader:将文件以字符串的形式导入;
    • thread-loader:多进程打包js和css。

    注:file-loader、raw-loader、url-loader等在webpck5中已经被资源模块类型(asset module type)替换掉了,

  • Plugins:插件则可以用于执行范围更广的任务。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量等。

    常见的插件:

    • HtmlWebpackPlugin:创建html标签,去承载输出的bundle;
    • CleanWebpackPlugin:清理构建目录(dist目录);
    • CommonsChunkPlugin:将chunks相同的模块代码提取成公共的js;
    • ExtractTextWebpackPlugin:将css从bundle文件里提取成一个独立的css文件
    • uglifyjsWebpackPlugin:压缩JS。
  • Mode:模式,有生产模式production和开发模式development。设置模式可以让webpack自动调起相应的内置优化,设置了 mode 之后,webpack4 会同步配置 process.env.NODE_ENV 为 development 或 production。不同模式,优化内容也有不同:

    production 模式下有更好的用户体验:

    • 较小的输出包体积
    • 浏览器中更快的代码执行速度
    • 忽略开发中的代码
    • 不公开源代码或文件路径
    • 易于使用的输出资产

    development 模式会提供最好的开发体验:

    • 浏览器调试工具
    • 快速增量编译可加快开发周期
    • 运行时提供有用的错误消息

    none:不使用任何默认优化选项。

// webpack.config.js
// 插件需要手动引入,loader会自动加载
const HtmlWebpackPlugin = require('html-webpack-plugin')

moudule.exports = {
  // 单入口 entry:'./index.js'
  // 多入口  key:构建包名称,即 [name] ,在这里为index;  value:入口路径
  // 入口决定 webapck 从哪个模块开始生成依赖关系图(构建包),每一个入口文件都对应着一个依赖关系图。
  entry: {
    "index": `./index.js`,
  },

  output: {
    // 打包输出文件路径 path 必须为绝对路径
    path: path.resolve(__dirname, '../../dist/build'),
    publicPath: "www.xxx.xx.com", // 如果打包出来的资源要放到cdn上,可以配置pbulicPath加上域名
    filename: "[name].bundle.js", // 包名称
    // 或使用函数返回名(不常用)
    // filename: (chunkData) => {
    //   return chunkData.chunk.name === 'main' ? '[name].js': '[name]/[name].js';
    // },    

    chunkFilename: '[name].[chunkhash].bundle.js', // 块名,公共块名(非入口)

    // 打包生成的 index.html 文件里面引用资源的前缀
    // 也为发布到线上资源的 URL 前缀 使用的是相对路径,默认为 ''
    publicPath: '/',

    // 打包成库 使用 webapck 构建一个可以被其它模块引用的库
    // 一旦设置后,该 bundle 将被处理为 library。
    library: 'webpackNumbers',
    // export 的 library 的规范,有支持 var, this, commonjs,commonjs2,amd,umd
    libraryTarget: 'umd',
  },

  // 环境 可以是 none、development、production;默认为 production
  mode: 'production',
  // mode 也可以在script命令行里配置
  // "build:prod": "webpack --config config/webpack.prod.config.js --mode production"

  // source map 通过source map 定位到源代码
  // 开发模式设置为eval-source-map可以直接定位到具体的错误行;
  // 生产环境下关闭,将devtool 设置为 nosources-source-map,防止源码泄漏,提高网站的安全性
  devtool: 'eval-source-map',

  module: {
    rules: [
      { // js 语法检查 npm install eslint-loader eslint --save-dev
        test: /.js$/,  //只检测js文件
        exclude: /node_modules/,  // 排除node_modules文件夹
        enforce: "pre",  //提前加载使用
        use: { //使用eslint-loader解析
          loader: "eslint-loader"
        }
      },
      { // 打包less资源 npm install css-loader style-loader less-loader less --save-dev
        test: /.less$/, // 检查文件是否以.less结尾(检查是否是less文件)
        use: [  // 数组中loader执行是从下到上,从右到左顺序执行
          'style-loader', // 创建style标签,添加上js中的css代码
          'css-loader', // 将css以commonjs方式整合到js文件中
          'less-loader'  // 将less文件解析成css文件
        ]
      },
      { // js 语法转换  将浏览器不能识别的新语法转换成原来识别的旧语法,做浏览器兼容性处理
        // npm install babel-loader @babel/core @babel/preset-env --save-dev
        test: /.js$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ['@babel/preset-env']
            // @babel/preset-env是一个智能预设,允许使用最新的JavaScript,而无需微观管理目标环境需要哪些语法转换(以及可选的浏览器polyfill)。
          }
        }
      },
      { // webpack4 打包样式文件中的图片资源 npm install file-loader url-loader --save-dev  
        test: /.(png|jpg|gif)$/,
        use: {
          // url-loader是对象file-loader的上层封装,使用时需配合file-loader使用。
          loader: 'url-loader',
          options: {
            limit: 8192, // 8kb --> 8kb以下的图片会base64处理
            outputPath: 'images', // 决定文件本地输出路径
            publicPath: '../build/images',  // 决定图片的url路径
            name: '[hash:8].[ext]' // 修改文件名称 [hash:8] hash值取8位  [ext] 文件扩展名
          }
        }
      },
      {
        //  webpack5 配置方式
        test: /.(png | svg | jpg | gif)$ /,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 10 * 1024 // 10kb 如果小于10kb直接转成base64 加到页面里面 
          }
        }
      },
      {
        test: /.(woff|woff2|eot|ttf|otf)$/, //字体
        type: 'asset/resource'
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      // 以当前文件为模板创建新的HtML(1. 结构和原来一样 2. 会自动引入打包的资源)
      template: './src/index.html',
    }),
  ],
  devServer: {
    contentBase: resolve(__dirname, 'build'), // 运行项目的目录
    hot: true, // 开启热更新
    open: true, // 自动打开浏览器
    compress: true, // 启动gzip压缩
    port: 3000, // 端口号
  },
  // 配置webpack的优化参数
  optimization: {

  },
  resolve: { // 通常配置一些简便开发的内容
    // 配置别名 提供路径的简写
    alias: {
      "@css": "/css"
    },
    // 扩展名省略,定义可以省略的扩展名
    extensions: [".js", ".css", ".json"]
  }
}

资源内联

页面框架的的初始化脚本、上报相关打点、css内联避免页面闪动、小图片或者字体内联(可以减少http请求次数)等都需要把代码放在打包后的代码前面先加载。在Webpack中内联html和javaScript代码可以通过raw-loader这个插件来完成,在引入JS插件时,需要先利用babel-loader进行转换,所以babel-loader也需要一起引入

<!-- 
    html与js内联的方式
    首先拆分需要内联的HTML片段 放在meta.html中
    利用插件内联HTML片段与JS插件
-->
<head>
    <!-- 引入meta.html片段-->
    ${ require('raw-loader!./meta.html')}
    <title>Webpack内联文件</title>
    <!-- 将外部JS插件进行内联 -->
    <script>
        ${ require('raw-loader!babel-loader!../../node_modules/lib-flexible/flexible.js')}
    </script>
</head>
​
// css内联在webpack.config.js中配置style-loader即可
// 也可以使用html-inline-css-webpack-plugin插件

热更新

当我们对代码修改并保存后,webpack会对修改的代码块进行重新打包,并将新的模块发送至浏览器端,浏览器用新的模块代替旧的模块,从而实现了在不刷新浏览器的前提下更新页面。webpack4及之前需要安装热更新插件:npm i webpack-dev-server;webapck5之后只需要在配置中新增参数devServer配置即可。

热更新工作原理

  1. 启动Webpack Dev Server:在Webpack配置中,设置devServer.hottrue,以启用热模块替换功能。
  2. 建立WebSocket连接:启动Webpack Dev Server时,它会创建一个Socket服务器,用于与浏览器建立WebSocket连接。在浏览器中访问应用程序时,Webpack Dev Server会将一个运行时脚本(runtime script)注入到页面中。这个脚本会建立与Webpack Dev Server的 WebSocket连接,以便实时接收来自服务器的更新通知。
  3. 文件监控与编译:Webpack会监控所有入口文件及其依赖的文件。当修改了源代码并保存时,Webpack会监听文件系统的变化,并调用webpack-complier编译修改后的模块。
  4. 推送更新:当编译完成后,Webpack会将更新的模块信息发送给Webpack Dev Server。Webpack Dev Server会通过WebSocket连接将更新的模块信息推送给浏览器,并带上构建时的 hash,让浏览器与上一次的资源进行对比。
  5. 客户端处理更新:浏览器接收到更新的模块信息后,会使用Webpack的HMR Runtime(热模块替换运行时)来处理这些更新。HMR Runtime会根据模块的更新信息,将新的模块代码插入到应用程序中,而无需重新加载整个页面。

webpack优化

  • 多页面打包:优化SEO,加快首屏渲染。

  • 基础库分离:将一些基础包分离,不打入bundle,单独打包,也可以引入CDN。对于echarts等大型包,也可以通过test检测单出打包,做更细致的优化。参考下面代码分割。

  • tree shaking:production时默认开启。

  • 动态import:即按需加载模块,减少文件体积,适用于单入口文件打包。通过import函数导入的模块都被打包成独立的js文件,在实际使用中,会通过触发某些事件,再去执行import函数,这时就会在html文档上插入一个script标签,加载我们需要的模块。需要安装插件@babel/plugin-syntax-dynamic-import。

    • 单文件入口:容易出现打包文件体积过大问题,可以使用动态引入来分割代码,且如果这个模块又大又靠后使用,则使用动态引入。
    //代码引入模块
    // js
    const loadimport = ()=>{
        // webpackChunkName可以指定打包后的文件名称
        import(/*webpackChunkName: a*/'./utilsA.js').then( res =>{
            console.log('utilsA导出的内容为res.default', res.default)
            // 其他操作
        })
        /* 另一种写法
        require.ensure(['导入文件需要的额依赖如jquery'], (jquery)=>{
            let res = require("./utilsA.js")
            console.log(res.default)
        }, '打包文件的别名')
        */
    }
    
    // html
    <button onClick={loadimport}>hello</button>
    
    // webpack配置
    { 
      "plugins": ["@babel/plugin-syntax-dynamic-import"]
    }
    // 很少会使用import函数来动态导入一个模块。因为在所有导入该模块的地方,如果有一处忘记使用import函数,它就会被打包到最终的bundle里面,这样优化的目的就达不到了。此外,还需要判断这个模块是否真的需要分割出来,因为建立一个http请求也需要一定的开销。import函数使用不当不仅达不到性能优化的目的,可能还会降低页面的性能。
    
  • 代码分割:配置多入口文件时问题主要是重复加载同一段逻辑代码,可以使用代码分割,将重复的部分单独打包,并利用浏览器缓存功能,第一个入口文件加载时,就会缓存此重复代码,下次再需要的时候就可以直接取缓存。

// 配置多文件入口
module.exports = {
  // 如果两个入口文件都引入了app3.js,那么两个出口文件中都会把app3.js打包进去
  entry: {
    app: './app.js',
    app2: './app2.js'
  },
  output: {
    path: __dirname + "/dist",
    filename: "[name].[hash:4].bundle.js"chunkFileName: "[name].js"
  },
  // 优化相关配置 一般设置好模式,就会又默认的优化配置,但是代码分割还是需要单独配置
  optimization: {
    splitChunks: {
      // all:不管同步或异步都会拆分; async:至拆分异步即动态import部分; initial:只拆分同步;
      chunks: "all",
      minChunks: 2, // 一个模块被重复引用几次才会被拆分成独立的文件
      // 1000bety 大于minSize的chunk才会进行拆分,避免拆分过多, 增加http请求次数
      minSize: 1000,
      name: "a",// 指定文件名 所有重复的内容,都会打包进同一个文件包括第三方库

      // 针对上面的基础分割规则,如果有特殊分割需求的时候可以配置 cacheGroups           
      cacheGroups: {
        // 例如:如果要把第三方库单独打包,需要再单独配置
        vendor: {
          test: /[\/]node_modules[\/]/// 第三方库从node_modules中找
          filename: "vendor.[hash:4].js",
          chunks: 'all',
          minChunks: 1,
        },
        common: { // 把业务公用代码单独打包再一起为common.js 上面基础配置可以去掉了
          filename: "common.[hash:4].js",
          chunks: 'all',
          minChunks: 2,
          minSize: 1000,
        }
      }
    },
    // 大多项目通用的、单入口或多入口都会把以下两种模块单独打包:
    // 第三方库单独打包 ...vendor.js
    // runtime运行代码(webpack用于组织模块运行的代码)...runtime.js 
    // 单独配置 runtimeChunk
    runtimeChunk: {
      name: 'runtime'
    }
  }
}
  • 多线程打包:thread-loader。

  • dll优化打包速度:提前打包不变的包(vendor),通知到正式打包,然后Dll处理过的就不再处理。 新建一个webpack.dll.config.js配置文件:

const webpack = require("webpack")
module.exports = {
  mode: "production",
  entry: {
    vendor: [
      "axios",
      "lod
    ]
  },
  output: {
    path: __dirname + "/dist/dll",
    filename: "[name].[chunkhash:4].dll.js",
    library: '[name_library]', // 通过library去定位哪些东西是已经打包的
  },
  plugins: [
    new webpack.DllPlugin({
      // DllPlugin插件会输出一个json,里面的东西是通知正式打包已经打包了内容,path为json的路径
      path: __dirname + "/[name]-manifest.json",
      name: '[name_library]', // 必须与library同名,标识
      context: __dirname, // 当前文件夹
    })
  ]
}

/*
// 新增script脚本 并执行 生成打包文件vendor.dll.js,以及vendor-manifest.json文件
"dll": "webpack --config ./webpack.dll.config.js"


// 通知正式打包 在webpack.proconfig.js 生产环境的配置文件中添加配置,把dll输出的json文件给它
plugins: [
  new webpack.DllReferencePlugin({
    manifest: require(__dirname + "/vendor-manifest.json")
  })
]

// 注:但是,这样有个问题:生成的vendor.dll.js文件不会自动引入到html里面,还需要手动引入才能生效。
// 解决:需要在入口文件index.html文件中手动加入:
<head>
    //...
    <script src="vendor.dll.js"></script>
</head>
*/
  • 优化打包速度:webpack内置的stats,分析plugin打包速度插件:speed-measure-webpack-plugin;

  • bundle分析:使用插件webpack-bundle-analyzer

    • 方法一,在打包脚本后面加上--json,把生成的json文件传到官网上解析

      'getJson' : "webpack --config ./webpack.proconfig.js--json>stats.json"

      网站:webpack.github.io/analyse/

    • 方法二、引入webpack-bundle-analyzer插件

  // 在webpack.baseconfig.js中引入插件
  const bundleanalyzer = require("webpack-bundle-analyzer").BundleAnalyzerPlugin
 
  module.exports= {
      // ...
      plugins:[
          new bundleanalyzer(),
      ]
  }

webpack打包后文件带的hash值的意义

浏览器加载了一个资源后会缓存资源,但是如果名字改了就会重新请求。所以内容改变后,hash值就会改变,浏览器就会重新请求最新的资源。配置文件名时通过[hash:4]添加hash值,也可以取8位,但是这样配置存在一个问题,一旦有一个模块改动,所有打包的文件都会重新生成hash值,没有修改的模块缓存就失效了。因此,想要精准控制各个模块的hash不相互影响,可以使用[chunkhash:4],可以最大程度利用缓存。

require.context("文件目录",boolean是否包含内层子文件,正则匹配规则):批量引入某个文件下的符合条件的所有文件。避免单独一个一个引入。

/*
>mode
  num1.js
  num2.js
  num3.js
*/ 
// 同时引入./mode文件夹下的所有js文件,不包含内层子文件。 返回引入的文件内容
const res = require.context("./mode", false, /.js/)
// res.keys()返回数组["num1.js","num2.js", "num3.js"]
let _all;
res.keys().forEach((item)=>{
    console.log(res(item).default)
    _all += res(item).default
})

webpack实战

区分环境,指导vite在不同环境下做不同的事情:

生产模式:需要代码压缩、tree-shaking;不需要详细的source map,不需要开启开发模式;

开发模式:需要详细的额source map,开启开发模式,不需要压缩、代码混淆等。

根据不同环境进行不同打包,一般在process.env中设置,有的时候需要在js代码中获取环境,我们可以借助插件完成。

新建不同环境下的配置文件:

  • 基础配置:webpack.baseconfig.js
  • 开发环境配置:webpack.devconfig.js
  • 生产环境配置:webpack.proconfig.js
// webpack.baseconfig.js
const htmlWebpackPlugin = require("html-wepack-plugin");
const minicss = require("mini-css-extract-plugin");
let pluginArr = [
  new htmlWebpackPlugin({
    filename: "index.html",
    template: "./index.html"
  })
]
function hasMiniCss() {
  if (process.env.NODE_ENV === "production") {
    pluginArr.push(new minicss({
      filename: "test.bundle.css"
    }))
  }
}
hasMiniCss(); // 编程式配置
module.exports = {
  entry: {
    app: "./app.js"
  },
  output: {
    path: __dirname + "/dist",
    filename: "[name].[chunkhash:4].bundle.js"
  },
  module: {
    rules: [
      {
        test: /.css$/,
        use: [
          process.env.NODE_ENV === "production" ? minicss.loader : "style-loader",
          "css-loader"
        ]
      }
    ]
  },
  /*
  plugins: [
      new htmlWebpackPlugin({
          filename: "index.html",
          template: "./index.html"
      })
  ]
  */
}


// webpack.devconfig.js
const base = require("./webpack.baseconfig.js");
// webpack提供了合并配置对象的merge方法, 后面配置的会覆盖前面的。
const merge = require("webpack-merge").merge;
module.exports = merge(base, {
  mode: "development",
  devtool: "eval-cheap-source-map",
  devServer: {
    port: 1000,
    hot: true,
    proxy: {
      "/": {
        target: "http://loaclhost:3000",
        pathRewrite: {
          "^/num1": "/api/getNum1",
          "^/num2": "/api/getNum2",
        },
        headers: {},
      }
    }
  },
})

// webpack.proconfig.js
const base = require("./webpack.baseconfig.js");
const merge = require("webpack-merge").merge; // webpack提供了合并配置对象的merge方法
module.exports = merge(base, {
  mode: "production",
})
/*
// cross-env是一个库,帮助脚本可以跨平台运行 需要安装 npm i cross-env -save-dev
// 在package.json文件中配置打包脚本 运行脚本中指定当前环境cross-env,可用于配置文件中判断。
"dev": "cross-env NODE_ENV=development webpack-dev-server --config ./webpack.devconfig.js",

// 脚本中执行build是 利用本地安装的webpack打包。
"build": "cross-env NODE_ENV=production webpack --config ./webpack.proconfig.js"

// 指定环境方法二、 脚本中指定 --env
"dev": "webpack --config ./webpack.proconfig.js --env production",
*/

// 接收env参数需要改造以下配置文件,将配置对象改为函数方式接收env参数
const base = require("./webpack.baseconfig.js");
const merge = require("webpack-merge").merge;
module.exports = function (env) {
  return merge(base(env), {
    mode: "production",
  })
}

// webpack.baseconfig.js也需要改造成方法接收
module.exports = function (env) {
  console.log(env)
  return {
    // ...
  }
}

在业务代码中如何获取环境变量

// 在不同环境的配置文件中引入webpack,在插件中配置注册变量,会成为全局变量,在业务代码中可以全局引用
const webpack = require("webpack");
module.exports = merge(base, {
    mode: "production",
    plugins:[
        new webpack.DefinePlugin({
            baseURL: "www.xxx.com"  // baseURL就可以全局使用
        })
    ]
})
// 在development环境可以指定baseURL:"www.yyy.com"

// ./app.js
console.log(baseURL); // www.xxx.com