Webpack5不完全指南-进阶篇

1,110 阅读4分钟

处理HTML中的静态资源

前面处理了css中的图片资源,webpack默认不会对HTML进行解析,当然也不会收集处理HTML中的图片等资源,html-withimg-loader可以解析HTML中图片资源收集到 dist/img

  • 安装
    npm i html-withimg-loader -D
  • 配置loader
    // webpack.config.js  
    module: {
      rules: [
        {
          test: /\.(htm|html)$/i,
          loader: "html-withimg-loader"
        }
      ]
     
    

多页面应用打包

基于MVVM的开发的web应用多为单页面应用,但是某些场景更需要多页面应用,例如OA办公系统等,webpack也可以实现多页面应用打包。

  • 在src下新建入口文件 main.jsother.js
    // src/main.js  
    console.log("这是main.js")
    // src/other.js  
    console.log("这是other.js")
    
  • 配置入口和出口
    // webpack.custom.config.js  
    module.exports = {
      // entry: "./src/index.js",
      // 1. 当为多组入口出口时 传入对象
      entry: {
        // 2. 注册多个入口文件
        index: "./src/main.js",
        other: "./src/other.js",
      },
      // 项目出口配置
      output: {
        path: path.resolve(__dirname, "./dist"),
        // filename: 'bundle.js'
        // 3. 当为多个出口文件时 文件名要用 name 变量 否则会报错
        filename: '[name].js'
      }
    },
    
  • 执行 npm run build 打包项目
    image.png
    image.png

配置多个html页面

前面只是配置了多个入口和出口,所以依赖最终还是在一个html这里我们还需要拆分多个html

  • 新建 src/other.html
  • 配置插件
      plugins: [
      // 新建一个HtmlPlugin实例
      new HtmlPlugin(
        {
          // 打包输出文件名
          filename: "index.html",
          // 被复制的目标html
          template: "./src/index.html",
          // 默认引入所有chunks(index.js和other.js),指定要引入的chunks
          chunks: ["index"]
        }
      ),
      new HtmlPlugin(
        {
          filename: "other.html",
          template: "./src/other.html",
          // 默认引入所有chunks(index.js和other.js),指定要引入的chunks
          chunks: ["other"]
        }
      ),
    ]
    
  • npm run build 重新打包

image.png

第三方库的引入方式

webpack的原理是将每个模块作为闭包来进行打包,局部模块导入的库只是局部的变量无法全局使用,可以借助 expose-loader 实现全局作用域变量注入,也可以通过内置插件 webpack.ProvidePlugin 对每个闭包作用域注入变量,自动加载模块

示例

以jquery为例

  • 安装
    npm i jquery

  • main.js 中使用jquery

    console.log("这是main.js");
    import $ from 'jquery'
    // 在当前作用域导入并访问JQ
    console.log("JQ", $);
    // 访问全局作用域下的JQ
    console.log("window.jquery", $);
    $("body").css("backgroundColor", "green")
    
  • npm run build重新构建后打开 index.html

    image.png 正如之前所讲,使用webpack,在当前模块导入的库只能作为局部变量使用。

expose-loader

  • 安装
    npm i expose-loader -D

  • 配置loader

    // webpack.custom.config.js
    module: {
      rules: [
        // 注入全局变量
        {
          // require.resolve 解析jquery库的绝对路径
          test: require.resolve('jquery'),
          loader: 'expose-loader',
          options: {
            // 使用变量 $ 挂载JQ
            exposes: '$',
          }
        }
      ]
    },
    
  • npm run build 重新打包,可以访问到全局变量 $

    image.png

webpack.ProvidePlugin

ProvidePlugin是webpack内置插件,可以为每个模块注入变量

  • 修改 main.jsother.js,在 other.js 不导入直接使用JQ
    // main.js
    console.log("这是main.js");
    // other.js
    console.log("这是other.js");
    $("body").css("backgroundColor", "pink")
    
  • 配置plugin
    plugins: [
      new HtmlPlugin(
        {
          filename: "index.html",
          template: "./src/index.html",
          // 1.index.html 引入所有js
          chunks: ["index", "other"]
        }
      ),
      // 2.使用 Webpack.ProvidePlugin 为每个模块注入局部变量
      new Webpack.ProvidePlugin({
        // 每个模块导入jquery模块,并挂载到$、jQuery变量上
        $: "jquery",
        jQuery: "jquery"
      })
    ]
    

区分环境配置文件

项目开发时,一般需要使用两套配合文件,开发阶段打包(不压缩代码、不优化代码、注重效率)和生产阶段打包(压缩代码、优化代码、打包后上线直接使用

我们抽离为三个配置文件

webpack.base.js:所有基础、公共的配置放在该配置文件中
webpack.prod.js:生产环境的单独配置放在该配置文件中放在该配置文件中
webpack.dev.js:开发环境的单独配置放在该配置文件中放在该配置文件中

操作流程

  • 将开发环境和生产环境的公共配置(入口、出口、loader等)放到webpack.base.js

    // webpack.base.js  
    const path = require("path")
    const HtmlPlugin = require("html-webpack-plugin")
    const CleanWebpackPlugin = require("clean-webpack-plugin").CleanWebpackPlugin
    const CopyWebpackPlugin = require("copy-webpack-plugin")
    const Webpack = require("webpack")
    module.exports = {
      entry: {
        index: "./src/main.js",
        other: "./src/other.js",
      },
      output: {
        path: path.resolve(__dirname, "./dist"),
        filename: '[name].js'
      },
      plugins: [
        new HtmlPlugin(
          {
            filename: "index.html",
            template: "./src/index.html",
            chunks: ["index", "other"]
          }
        ),
        new HtmlPlugin(
          {
            filename: "other.html",
            template: "./src/other.html",
            chunks: ["other"]
          }
        ),
        new CleanWebpackPlugin(),
        new CopyWebpackPlugin({
          patterns: [{
            from: path.join(__dirname, "assets"),
            to: 'assets'
          }]
        }),
        new Webpack.BannerPlugin("这是一段版权信息"),
        new Webpack.ProvidePlugin({
          $: "jquery",
          jQuery: "jquery"
        })
      ],
      module: {
        rules: [
          {
            test: /\.css$/i,
            use: ['style-loader', 'css-loader']
          },
          {
            test: /\.less$/i,
            use: ['style-loader', 'css-loader', 'less-loader']
          },
          {
            test: /\.jpg|png|gif|bmp|ttf|eot|svg|woff|wpff2$/i,
            type: "asset",
            parser: {
              dataUrlCondition: {
                maxSize: 100
              }
            },
            generator: {
              filename: 'img/[name].[hash:6][ext]',
              publicPath: './'
            }
          },
         {
            test: /\.js$/i,
            exclude: /(node_modules|bower_components)/,
            use: {
              loader: 'babel-loader',
              options: {
                presets: ['@babel/preset-env'],
                plugins: ["@babel/plugin-transform-runtime"]
              }
            }
          },
          {
            test: /\.(htm|html)$/i,
            loader: "html-withimg-loader"
          }
        ]
      }
    }
    
  • 安装 webpack-merge, 在proddev中,使用webpack.mergebase导出的配置项进行合并导出

    • npm i webpack-merge -D

    • 配置 devprod

      // webpack.dev.js
      // 1.导入公共webpack配置
      const baseConfig = require("./webpack.base.js")
      // 2.导入合并webpack配置项函数
      const merge = require("webpack-merge").merge
      console.log("merge", merge);
      // 3.合并配置项
      const devConfig = merge(baseConfig, {
       mode: "development",
       devServer: {
          open: true,
          port: 3000,
          compress: true,
         hot: true,
         // 服务的基准路径
         // contentBase: "./src"
       },
       devtool: "eval-cheap-module-source-map"
      })
      module.exports = devConfig
      
      // webpack.prod.js
      // 1.导入公共webpack配置
      const baseConfig = require("./webpack.base.js")
      // 2.导入合并webpack配置项函数
      const merge = require("webpack-merge").merge
      // 3.合并配置项
      const prodConfig = merge(baseConfig, {
        mode: "production",
        // 4.生产环境谨慎使用source-map
        // devtool: "cheap-module-source-map"
      })
      module.exports = prodConfig
      
  • 重新配置package.json中,开发和生产环境的脚本指令

    "scripts": {
      // 生产环境使用  webpack.prod.js 进行打包
      "build": "npx webpack --config webpack.prod.js",
      // 开发环境使用  webpack.dev.js 进行打包
      "dev": "npx webpack server --config webpack.dev.js",
    },
    

配置文件归类

前面根据环境配置了多个配置文件导致根目录文件过多,为了让项目结构清晰方便管理 我们要对配置文件归类处理

  • 将所有Webpack配置文件放到 config 文件夹

    image.png

  • 修改 package.json 中加载配置文件的路径

    "scripts": {
      "build": "npx webpack --config ./config/webpack.prod.js",
      "dev": "npx webpack server --config ./config/webpack.dev.js"
    }
    
  • 目录变更,修改配置文件的绝对路径
    注意:配置文件的相对路径都是相对于 项目根路径!!!

    // config/webpack.base.js  
    module.exports = {
      entry: {
        // 配置文件中的相对路径都是相对于  项目根目录而非当前文件
        index: "./src/main.js",
        other: "./src/other.js",
      },
      output: {
        // 1.配置文件中绝对路径是相对于当前文件 所以必须要修改
        path: path.resolve(__dirname, "..", "./dist"),
        filename: '[name].js'
      },
      plugins: [
       new CopyWebpackPlugin({
         patterns: [{
           // 2.配置文件中绝对路径是相对于当前文件 所以必须要修改
           from: path.join(__dirname, "..", "assets"),
           to: 'assets'
         }]
       }),
     ]
    } 
    

定义环境变量

在某些场景下需要定义环境变量,例如根据环境区分接口地址(开发阶段使用内网地址或本机地址)

  • 声明开发阶段、生产阶段的环境变量

    // webpack.dev.js
    const webpack = require("webpack")
    const devConfig = merge(baseConfig, {
      plugins: [
        /** webpack.DefinePlugin(options):声明变量
         *  options:配置对象  
         *    - key:变量名
         *    - value:值,表达式
         */
        new webpack.DefinePlugin({
          // 开发环境变量IS_DEV = true
          IS_DEV: 'true',
          // 变量test = 2
          test:'1+1',
          // 变量test1 = "1+1"
          test1:'"1+1"'
        })
      ],
    })
      
    // webpack.prod.js  
    const webpack = require("webpack")
    const prodConfig = merge(baseConfig, {
      plugins: [
        new webpack.DefinePlugin({
          // 生产环境变量IS_DEV = false
          IS_DEV: 'false',
          test: '1+1',
          test1: '"1+1"'
        })
      ],
    })
    
  • 根据环境变量动态设置 接口基准地址

    // src/main.js
    console.log("这是main.js");
    
    // 根据环境设置接口基准地址
    let BASEURL = IS_DEV ? "http://127.0.1:3000" : "http://www.baidu.com"
    
    console.log("环境变量", BASEURL, IS_DEV, test, test1);
    
  • 开发环境/生产环境打包结果

    image.png

    image.png

通过ajax代理解决跨域问题

由于客户端浏览器的同源策略问题,项目开发阶段访问源之外的接口会出现跨域问题,通过 devSever 配置代理可以解决跨域问题

// webpack.dev.js  
const devConfig = merge(baseConfig, {
  mode: "development",
  devServer: {
    open: true,
    port: 3000,
    compress: true,
    hot: true,
    proxy: {
      // 当发送的ajax请求地址以'/api'开头时,devServer会代理发送一个以'https://www.baidu.com'开头ajax请求
      //   /api/index.do  ==>  https://www.baidu.com/idnex.do
      '/api': {
        // 代理到哪个接口
        target: 'https://www.baidu.com',
        // 改变接口的源
        changeOrigin: true,
        ws: true,
        // 重写目标接口
        pathRewrite: {
          '^/api': ''
        }
      }
    }
    // 服务的基准路径
    // contentBase: "./src"
  },
})

模块热更新(HMR)

在开发阶段,devServer会监听代码如果代码更新会重新加载整个页面这会导致几个问题

  • 重新加载页面时丢失的应用程序状态

  • 其他未修改的地方也随着页面刷新重新加载 模块热更新(HMR)不会重新打包整个项目而是以补丁的形式进行局部的更新

  • 修改为单入口文件
    必须为单入口文件才能使用热更新

    // webpack.base.js
    module.exports = {
      entry: {
        index: "./src/main.js",
        // other: "./src/other.js"
      }
    }
    
  • 新建 src/hotModule.js

    export default "这是热更新模块"
    
  • 监听 hotModule.js

    import str from './hotModule.js'
    console.log(str);
    // console.log("这是main.js");
    if (module.hot) {
      module.hot.accept("./hotModule.js", function () {
        const hotModule = require("./hotModule.js")
        // 当hotModule 内容更新时触发  
        console.log("hotModule更新了+++++++++++++++++++++++++++++", hotModule);
      })
    } 
    
  • npm run dev执行脚本 并手动更新src/hotModule.js

    image.png