Webpack 简书入门教程 (汇总)

Webpack 简书入门教程 (汇总)

总结:

  • 简介+五大核心

  • 一般流程

    • 使用 npm init 初始化一个项目,并安装webpack
    • 将之前新建的三个文件拷贝到项目中
    • 在命令行中输入webpack index.js
    • 将index.html中引入的index.js文件改为./dist/main.js文件,在浏览器中运行
      • 如果不希望代码被压缩,可以在命令后面加上 --mode development,表示以开发模式进行打包(开发模式不会压缩代码
    • webpack 打包时会默认将项目根目录下的 webpack.config.js 文件当做配置文件,因此可以通过新建 webpack.config.js 文件来更改webpack的默认配置
  • npm安装时-S -D作用及区别

    • -S

      • 即--save(保存)
        包名会被注册在package.json的dependencies里面,在生产环境下这个包的依赖依然存在
        
    • -D

      • 即--dev(生产)
        包名会被注册在package.json的devDependencies里面,仅在开发环境下存在的包用-D,如babel,sass-loader这些解析器
        

        它们不同于 dependencies,因为它们只需安装在开发机器上,而无需在生产环境中运行代码

  • 插件Plugin

    • CleanWebpackPlugin:每次打包时自动清除旧的文件,默认清除output.path目录
    • HtmlWebpackPlugin:生成一个html文件,自动引入打包后的js文件
    • MiniCssExtractPlugin :将 css 抽离成一个单独的文件,然后通过 link 的方式引入
    • OptimizeCssAssetsWebpackPlugin:为了减少文件体积,一般我们会对 css 文件进行压缩
  • loader处理项目中的样式文件css

    • webpack 默认是无法处理 css 文件的,因此需要使用 css-loader 来处理项目中的 css 文件新建 css 文件,在 js 中引入,一定要在 js 中引入(无需在 html 中引入)
    • 使用loader需要在webpack配置文件的module.rules中配置
    • 步骤
      • 安装 css-loader,并在配置文件中添加配置
      • 安装 style-loader,继续修改配置
        • style-loader 的作用是将 css 插入到 head 里的 style 标签中
      • 执行打包命令,在浏览器中打开 index.html 文件
  • loader处理项目中的照片

    • file-loader
      • 只有 css 引入的 bg.jpeg 被打包输出,并且引入的图片路径也被替换成了打包后的路径,js 及 html 直接引入的图片皆存在问题
      • 对于通过 js 动态添加的图片,我们可以使用 require() 方法来引入
      • 修改 file-loader 参数(解决js引入图片)
    • html-loader
      • 对于 html 中的图片,我们可以使用 html-loader 或 html-withimg-loader 来处理.
      • 所以当 html 中存在 ejs 语法时,我们不能使用 html-loader 来处理图片,而是直接使用 ejs 语法来引入
    • 部署
      • 目前为止,我们所有的文件都是输出到 dist 目录下的,不便于之后的发布上线
      • 例如 css 文件输出到 style 目录,图片输出到 images 目录,js 输出到 js 目录…
      • 修改 webpack 配置重新打包
    • url-loader
      • 使用 url-loader 代替 file-loader,url-loader 会将体积较小的图片进行 base64 编码打包进文件中,减少网络请求次数
    • image-webpack-loader
      • 对图片进行压缩
  • 术语介绍

    • chunk name
      • 多入口 chunkname 为其key值,单入口不指定key则默认为main
    • chunkhash
      • 根据 chunk 的内容生成的 hash
      • 同一个 chunk中,只要其中一个文件内容改变,其 chunkhash 就会改变
    • contenthash
      • 单个输出文件内容的 hash,或者说 bundle 的 hash
  • webpack-dev-server(localhost

    • 可以在每次修改后,自动打包并刷新页面

    • webpack-dev-server 是 webpack 官方提供的一个小型 Express 服务器。使用它可以为 webpack 打包生成的资源文件提供web服务。

    • 步骤

      • 首先需要安装 webpack-dev-server,接着在 package.json 中添加一个脚本
      • 在命令行执行 npm start 即可(start 是一个特殊的命令,不同于其他命令,直接 npm start 就行,中间不需要 run)
    • devServer 是可以自动打开浏览器的

      • 简单配置

        // webpack.config.js
        module.exports = {
          // ...
          devServer: {
            open: true, // 自动打开浏览器,下面三项不是必须的
            host: "0.0.0.0", // 如果希望被局域网访问,设为 0.0.0.0,默认 localhost
            port: "8888",  // 端口,默认 8080
            useLocalIp: true // 使用本地ip,如果 host 设置 0.0.0.0,请将此参数设为 true,否则结果就会像下面这样
          }
        }
        
    • HMR (Hot Module Replacement,热模块替换)技术

      • 每次修改完内容保存自动刷新页面,看起来没有什么问题,但是当我们的项目很庞大的时候,就会显得特别的慢

      • 因此我们需要使用 devServer 提供的 HMR (Hot Module Replacement,热模块替换)技术

      • 在 devServer 中添加一个参数即可开启热更新

        hot: true // webpack 会自动引入 HotModuleReplacementPlugin 插件
        
      • 修改完配置需要重新启动

      • js文件会自动更新,css不会自动更新, MiniCssExtractPlugin.loader 默认是不支持热更新的,修改为之前的 style-loader(默认支持热更新)

    • devServer的其他常用配置

      devServer: {
        contentBase: path.resolve(__dirname, "public"), // 告诉服务器从哪里提供内容,默认为当前工作目录
        wacthContentBase: true, // 监视 contentBase 里面的内容,一旦变化就 reload,
        watchOptions: {
          ignore: "", // 忽略哪些文件的变化
        },
        historyApiFallback: true, // 请求的资源不存在时返回 index.html,比如 vue-router 的 history 模式
        clientLogLevel: "none", // 不要显示启动日志信息
        overlay: false, // 如果出错了,不要全屏提示 
        progress: true, // 控制台输出运行进度
        compress: true, // 启用 gzip 压缩
        proxy: { // 设置代理
          "/api": { // 当 url 中含有 /api 时就会使用这里设置的代理
            target: "http://xxxx.com", // 目标服务器地址
            changeOrigin: true, // 跨域
            ws: true, // 代理websocket
            pathRewrite: {
              "^/api": "" // url 重写,将 url 里面的 /api 去掉
            }
          }
        }
      }
      
  • IE浏览器兼容

    • babel
      • 需要安装
        • babel-loader
        • @babel/core(babel核心库,核心 api 都在这里)
        • @babel/preset-env(babel 预设,babel 是插件化的,转换不同的语法,需要不同的插件,预设的作用就是按需引入插件)只能转换高阶的语法,并不能转换高阶的 API
        • @babel/polyfill 来处理这些高阶的 API
    • thread-loader
      • 当项目里的 js 文件越来越多时,babel 转换耗时会越来越长,可以使用 babel 缓存及多进程打包来提高速度
  • tree shaking

    • 移除文件中未被引用使用的代码,它依赖于 ES6 模块语法的静态结构特性,例如 import 和 export
    • 两个条件
      • 使用 es6 模块化语法(使用 babel 时记得设置参数 modules 为 false)
      • 生产模式打包(默认开启 tree shaking)
  • resolve

    • 设置模块如何被解析
    • alias:为路径设置别名,让引入变得更简单
    • extensions:引入哪些类型的文件时可以省略后缀名
    • modules:解析模块时应该搜索的目录
  • externals

    • 设置某些库不被打包,而是运行时去外部获取
  • watch

    • 初始构建之后,继续监听任何已解析文件的更改
  • devtool

    • 控制如何生成 source map
    • source map 是一个信息文件,保存源代码与打包后代码的映射关系,帮助我们快速定位报错的代码

一. 基本概念

1. 什么是webpack

Webpack是一个模块化的打包工具,它根据模块的依赖关系进行静态分析,然后将这些模块按照指定的规则生成对应的静态资源

img

2. 理解module、chunk、bundle

在 webpack 中,一切皆module,任何一个文件都可以看成是module。js、css、图片等都是module
webpack 会将入口文件以及它的依赖引入到一个 chunk (大块)中,然后进过一系列处理打包成bundle(捆绑)。

img

3. webpack的五大核心

(1) entry
webpack打包的入口,其取值可以是字符串,数组或者一个对象

// 单入口单文件
entry: "./src/index.js"

// 单入口多文件
entry: ["./ src/index.js", "./src/common.js"] // 若index.js与common.js没有依赖关系,可以通过此方式将它们打包在一起

// 多入口
entry: {
  page1: "./src/page1.js",  //page1和page2即为key值
  page2: "./src/page2.js"
}

(2) output
webpack打包的输出,常用配置如下

output: {
  path: path.resolve(__dirname, "./dist"),
  // 单入口时(默认)
  // filename: "main.js",
  // filename: "js/main.js", // filename也可以写路径,表示输出到 dist/js 目录下
  // 多入口时,由于会有多个输出,因此文件名不能写死
  filename: "[name].js", // name表示chunk的名称,此处为entry中的key值
  chunkFilename: "[name].js", // 按需加载的模块打包后的名称
  publicPath: "/" // 项目部署在服务器上的路径,如果在根路径则为 /
}

(3) mode
webpack打包分为两种模式,开发模式(development)与生产模式(production),默认为生产模式

选项描述
development会将 process.env.NODE_ENV 的值设为 development。启用 NamedChunksPlugin 和 NamedModulesPlugin。
production会将 process.env.NODE_ENV 的值设为 production。启用 FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPlugin 和 UglifyJsPlugin.

(4) loader
webpack默认只能处理js、json格式的文件,而loader的作用则是将其他格式的文件,转换成webpack能够处理的文件

使用loader需要在webpack配置文件的module.rules中配置

module.exports = {
 entry: ...,
 output: ...,
 module: {
   noParse:  /node_modules/, //忽略解析 node_modules 中的文件
   rules: [
     {
       test: /\.xxx$/, // 匹配后缀名为xxx的文件;正则匹配规则:^ 表示字符串的开始位置
//$ 表示字符串的结束位置,/\.xxx$/中的/ /就表示正则表达式自带的
       // 单个loader
       // loader: "xxx-loader",
       // options: {},
       // 多个loader,loader的处理顺序为从后往前,因此需要优先处理的loader放在数组最后面
       // use: ["xxxx-loader", "xxx-loader"],
       //  如果某个loader需要配置,写成下面的格式
       use: [
         {
           loader: "xxxx-loader",
           options: {}
         },
         "xxx-loader"
       ],
       include: [path.resolve(__dirname, "./src")], // 只解析src中的文件,可以是正则
       exclude: [path.resolve(__dirname, "./library")], // 忽略library中的文件,可以是正则
       // 当多个规则同时匹配某类文件时,可以使用enforce参数指定优先级
       enforce: "pre" // 优先执行该规则里的loader,post 最后执行该规则里的loader
     },
     {
       // 当规则匹配时,不再匹配后面的规则。例如某个文件匹配到了第一个规则,不再匹配后面规则
       oneOf: [
         {
           test: /\.xxx$/,
           use: "xxx-loader"
         }, 
         {
           test: /\.xxx$/,
           use: "xxx-loader"
         }
       ] 
     }
   ]
 }
}

(5) plugin
webpack插件,每一个插件都有一个特定的功能,它能处理loader无法处理的事情
插件的使用非常简单,在plugins数组中添加插件的实例化对象即可

const xxxWebpackPlugin = require("xxx-webpack-plugin");
module.exports = {
  entry: ...,
  output: ...,
  plugins: [
    new xxxWebpackPlugin(),
    new xxxxWebpackPlugin({
      // 插件的配置项
    })
  ]
}

二. webpack的简单使用

  1. 新建util.js,index.js,index.html,写入如下内容
// util.js
export const print = (str) => {
  console.log(str);
};

// index.js
import {print} from "./util";
print("hello webpack");

// index.html
...
<!-- 在body中引入index.js -->
<body>
<script src="./index.js"></script>
</body>
...
  1. 浏览器中打开index.html,发现控制台报错

img

因为浏览器是不支持es6的模块化语法的,这个时候webpack就可以发挥作用了

  1. 使用 npm init 初始化一个项目,并安装webpack

    npm i webpack webpack-cli -g // 全局安装
    npm i webpack webpack-cli -D // 项目中安装

4.将之前新建的三个文件拷贝到项目中,结构如下

img

  1. 在命令行中输入webpack index.js

img

index.js表示入口文件,由于webpack的默认输出为dist/main.js,所以不用指定出口,也可以使用 webpack entry -o output 来指定出口

执行命令后会发现左侧生成了一个dist文件夹以及main.js文件

  1. 将index.html中引入的index.js文件改为./dist/main.js文件,在浏览器中运行

img

成功运行,没有报错

打开main.js会发现,里面的代码是被压缩过的,如果不希望代码被压缩,可以在命令后面加上 --mode development,表示以开发模式进行打包(开发模式不会压缩代码)

使用配置文件

每次打包时都输入一长串命令非常繁琐,可以在package.json中添加脚本,如下图

img

然后通过npm run build:dev 或 npm run build:pro 打包(build:dev 与 build:pro 为自定义名称)

webpack 打包时会默认将项目根目录下的 webpack.config.js 文件当做配置文件,因此可以通过新建 webpack.config.js 文件来更改webpack的默认配置

// webpack.config.js
module.exports = {
  //mode: "development", // 因为在package.json脚本指定了打包模式,所以无需设置
  entry: "./src/index.js",
  output: {
    path: path.resolve(__dirname, "./dist"),
    filename: "main.js",
  }
}

三. HtmlWebpackPlugin 与 CleanWebpackPlugin

将 index.html 复制一份到 dist 目录中去,并修改其引入的 main.js 路径为正确的相对路径
新建 server.js 文件,添加以下代码(使用的是NodeJS中的express框架

const express = require("express"); // 先安装 npm i express -D
const path = require("path");
const app = express();
app.use(express.static(path.resolve(__dirname, "dist"), {maxAge: 3600000}))
app.listen(3000);

命令行输入 node server.js,然后在浏览器打开 127.0.0.1:3000,结果如下

img

修改 index.js 中 print 函数的参数为 “hello node”,重新打包刷新页面

img

打印结果并没有更新,因为浏览器强缓存生效,没有去请求新的文件。而缓存是基于 url 的,只要 url 变了,缓存就会失效,我们只需要修改 js 的文件名便可以修改对应的 url,因此我们希望打包后的文件名根据文件内容动态生成,此时我们可以修改配置为

module.exports = {
  entry: "./src/index.js",
  output: {
    path: path.resolve(__dirname, "./dist"),
    filename: "[chunkhash:10].js", // chunk内容的hash值,后面会详细介绍
  }
}

打包刷新页面(记得修改 ./dist/index.html 中引入的 js 文件名)

img

可以发现,文件名发生了变化,缓存的问题也顺利解决了。但是文件名的变化却引发了两个新的问题

  1. 每次打包都要手动修改 html 中引入的 js 文件名
  2. 新的文件不会覆盖旧的文件,输出目录中的文件越来越多

HtmlWebpackPlugin 与 CleanWebpackPlugin 便可以为我们解决这两个问题

npm i html-webpack-plugin clean-webpack-plugin -D

CleanWebpackPlugin:每次打包时自动清除旧的文件,默认清除output.path目录
HtmlWebpackPlugin:生成一个html文件,自动引入打包后的js文件

在config中添加plugins配置

const  HtmlWebpackPlugin  = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
module.export = {
  ...
  plugins: [
    new HtmlWebpackPlugin(),
    new CleanWebpackPlugin()
  ]
}

最终打包生成的html如下图,自动帮我们引入了js文件

img

HtmlWebpackPlugin 自动生成的html没有任何内容,如果需要按照已有的 html 来生成,可以给该插件指定模板,常用参数如下

module.exports = {
  // 这里三个入口是为了解释 HtmlWebpackPlugin 的 chunks 及 excludeChunks
  entry: {
    page1: "./src/page1.js",
    page2: "./src/page2.js",
    common: "./src/common.js"
  },
  plugins: [
    new HtmlWebpackPlugin({
      filename: "index.html", // 生成的 html 的文件名
      template: "index.template.html", // 指定模板
      title: "hello webpack", // 设置 html 的 title,可以在 html 中通过 ejs 语法引入
      inject: true, // 默认值,script标签位于 body 底部,可选值 body、header、false(表示不自动引入js) 
      hash: false, // true 表示引入的js文件后面添加 hash 值作为参数,src="main.js?78ccc964740f25e35fca"
      chunks: [page1, common], // 多入口打包会有多个文件,默认引入全部,此配置表示只引入 page1, common
      minify: {
        collapseWhitespace: true,   // 去除空格
        minifyCSS: true, // 压缩 html 内联的css
        minifyJS: true,  // 压缩 html 内联的js
        removeComments: true,  // 移除注释
      }
    }),
    // 多页面需要 new 多个对象
    new HtmlWebpackPlugin({
      ...
      excludeChunk: [page1], // 不需要引入page1,即只引入 page2 与
    }) 
  ]
}

// index.html
...
<head>
  <!-- 打包后会被替换成插件参数中的title — hello webpack -->
  <title><%= htmlWebpackPlugin.options.title %></title>
</head>
...

img

四.处理项目中的样式文件

webpack 默认是无法处理 css 文件的,因此需要使用 css-loader 来处理项目中的 css 文件
新建 css 文件,在 js 中引入,一定要在 js 中引入(无需在 html 中引入

// index.css
body {
  background-color: plum;
}

// index.js
...
import "./index.css"

安装 css-loader,并在配置文件中添加配置

npm i css-loader -D

module.exports = {
  ...
  module: {
    rules: [
      {
        test: /\.css$/,
        use: "css-loader"
      }
    ]
  }
}

执行打包命令,在浏览器中打开 index.html 文件

img

如上图所示,页面背景色没有改变,html 中也没有任何样式

css 虽然被打包到了 js 文件中,但并没有作用到对应的元素上,此时,就需要 style-loader 来处理了
安装 style-loader,继续修改配置为

npm i style-loader -D

module: {
  rules: [
    {
      test: /\.css$/,
      use: ["style-loader", "css-loader"] // 从后往前,先执行css-loader,后执行style-loader
    }
  ]
}

重新打包,刷新页面,最终效果如下图

img

由此可知,style-loader 的作用是将 css 插入到 head 里的 style 标签中

在实际的项目开发中,我们可能需要将 css 抽离成一个单独的文件,然后通过 link 的方式引入,因此我们可以使用 MiniCssExtractPlugin 插件来完成此事

安装 MiniCssExtractPlugin,并使用 MiniCssExtractPlugin.loader 替换 style-loader,修改配置如下

npm i mini-css-extract-plugin -D

output: {
  path: path.resolve(__dirname, "./dist"),
  filename: "[name].js" // 为了方便理解,我们把 [chunkhash:10].js 改为 [name].js,表示chunk 的名称
},
module: {
  rules: [
    {
      test: /\.css$/,
      use: [MiniCssExtractPlugin.loader, "css-loader"]
    }
  ]
},
plugins: [
  new MiniCssExtractPlugin(),
  new CleanWebpackPlugin(),
  new HtmlWebpackPlugin({
    template: "index.html",
    title: "hello webpack"
  })
]

打包后生成的文件及页面效果如下图

img

css 被单独打包到了 main.css(因为 index.css 与 index.js 属于同一个 chunk ,因此它们的chunkname 都是 main)中,并且被生成的 html 文件使用 link 引入(HtmlWebpackPlugin 插件帮我们做了此事)

为了减少文件体积,一般我们会对 css 文件进行压缩,使用 OptimizeCssAssetsWebpackPlugin 插件即可
安装 OptimizeCssAssetsWebpackPlugin ,在配置中添加

npm i optimize-css-assets-webpack-plugin -D

plugins: [
  ...
  new OptimizeCssAssetsWebpackPlugin()
]

为了演示效果,在 index.css 中随便多加点样式,然后打包,发现输出的 css 文件内容被压缩到了一行

img

对于 .less、.scss 等样式文件,只需要安装对应的 loader 并添加配置即可,以 less 为例

{
  test: /\.less$/,
  use: [MiniCssExtractPlugin.loader, "css-loader", "less-loader"]
}

五.处理项目中的图片

与样式文件一样,webpack 默认也无法处理图片,因此需要对应的 loader 来处理
安装 file-loader,添加对应配置

npm i file-loader -D

// 在module.rules中新增
{
  test: /\.(png|jpe?g|gif|svg)$/,
  use: {
    loader: "file-loader",
    options: {
      name: "[name]-[contenthash:10].[ext]" // ext表示文件的后缀名
    }
  }
}

修改 index.html、index.css、index.js 文件并打包,内容如下图

img

预览结果

img

从上面两张图可以看出,只有 css 引入的 bg.jpeg 被打包输出,并且引入的图片路径也被替换成了打包后的路径,js 及 html 直接引入的图片皆存在问题

对于通过 js 动态添加的图片,我们可以使用 require() 方法来引入

photo.src = require("./images/vue.jpeg");

打包预览,结果如下

img

图片被打包输出了,但是 src 的路径却不正确,变成了一个 Module 对象,那是因为 file-loader 处理后的图片默认是使用 ESModule 导出的,而我们却是使用 commonJS 来引入的。有两种方式解决这个问题
方式一: 修改 file-loader 参数(推荐,因为 html 中的处理也是使用 commonJS 引入)

img

方式二: 修改js中的代码如下

photo.src =  require("./images/vue.jpeg").default;
// 或者
import img from "./images/vue.jpeg"
photo.src = img;

对于 html 中的图片,我们可以使用 html-loader 或 html-withimg-loader 来处理,以 html-loader 为例
安装html-loader,并添加配置重新打包

npm i html-loader -D

{
    test: /\.html$/,
    use: "html-loader"
}

img

至此,css、js、html 中的图片都能被正确打包处理,但是注意看 head 中的 title,并没有被 htmlWebpackPlugin 解析

因为使用 html-loader 处理之后,htmlWebpackPlugin 无法解析 html 中的 ejs 语法,而是当做 string 来输出。所以当 html 中存在 ejs 语法时,我们不能使用 html-loader 来处理图片,而是直接使用 ejs 语法来引入

<img src="<%= require('./src/images/webpack.jpg') %>">

目前为止,我们所有的文件都是输出到 dist 目录下的,不便于之后的发布上线,因此,我们希望不同类型的文件输出到不同的目录中,例如 css 文件输出到 style 目录,图片输出到 images 目录,js 输出到 js 目录…

修改 webpack 配置重新打包

// 修改 output
filename: "js/[name].js" // 修改前为 "[name].js"
// file-loader options 添加参数
outputPath: "images" // 输出到 dist 目录下的 images 中
//  MiniCssExtractPlugin 添加参数
new MiniCssExtractPlugin({
  filename: "style/[name].css"
})

img

可以看到,文件确实被打包到了对应的目录中,但是 css 引入的图片却没有显示,分析一下原因

图片放在了 images 目录,css 引入的图片路径前面也自动拼上了 images/,但是由于 css 放在了 style 目录,导致 css 与 图片的相对路径发生了变化,因此 css 引入的图片路径错误无法显示

给 MiniCssExtractPlugin.loader 添加一个参数便可解决此问题

{
  test: /\.css$/,
  use: [{
    loader: MiniCssExtractPlugin.loader,
    options: {
      publicPath: "../" // css 引入的资源路径前面拼上../这就是添加的参数
    }
  }, "css-loader"]
}

再次打包看效果

img

ok!图片展示的问题已经完美解决了,接下来进行些小优化

  1. 使用 url-loader 代替 file-loader,url-loader 会将体积较小的图片进行 base64 编码打包进文件中,减少网络请求次数
  2. 使用 image-webpack-loader 对图片进行压缩

npm i url-loader image-webpack-loader -D

{
  test: /\.(jpe?g|png|gif|svg)$/,
  use: [{
    loader: "url-loader",
    options: {
      name: "[name]-[contenthash:10].[ext]",
      outputPath: "images",
      limit: 10 * 1024, // 表示 小于10k 的图片会被 base64 编码
      fallback: "file-loader", // 大于 10k 的图片由 file-loader 处理,默认值,可不设置
      esModule: false
    }
  }, {
    loader: "image-webpack-loader",
    options: {
      disabled: true // 在开发或使用webpack-dev-server时,使用它可以加快初始编译速度,并在较小程度上加快后续编译速度(来自官方文档)
    }
  }]
}

img

可以看到,只有 bg.jpeg(21k 被压缩到了 19k) 被打包输出,而 webpack.jpg(9.9k) 与 vue.jpeg(7k) 都被 base64 编码直接打包进了文件中,关于图片的处理就到此为止

六.理解name、hash、chunkhash、contenthash

  • name:chunk name,chunk的名称
    多入口 chunkname 为其key值,单入口不指定key则默认为main,异步加载的模块默认为数字

img

  • hash:每次打包生成的hash,见上图
    项目中任意与打包相关的文件内容改变(包括配置文件),此hash就会改变
  • chunkhash:根据 chunk 的内容生成的 hash

修改前面例子中的 js 及 css 输出名称为 [chunkhash:10],然后打包

output: {
  filename: "js/[chunkhash:10].js",  // :10 表示取前面10 位
  path: path.resolve(__dirname, "./dist")
},
plugins: [
  // ...
  new MiniCssExtractPlugin({
      filename: "style/[chunkhash:10].css"
  })
]

img

index.js 中引入了 util.js 及 index.css,它们属于同一个 chunk,所以它们的 chunkhash 是一样的,最终打包出来的文件名称相同 ,只要其中一个文件内容改变,其 chunkhash 就会改变。随意修改 util.js 中的内容重新打包,css 文件内容并没有变化,其文件名依然发生了改变

// util.js
export const print = str => {
  console.log(str);
}
// 修改后
export const print = str => {
  console.log(str);
  console.log(123);
}

img

  • contenthash:单个输出文件内容的 hash

我们将 js 及 css 的文件名从 chunkhash 改为 contenthash,打包结果如下

img

image.png

可以发现,js 的文件名 与 css 的文件名并不一致,接下来我们还原 util.js 中的内容再打包一次

img

最终,js 文件的名称发生了变化,而 css 文件名并没有改变,继续修改 index.css 中的内容

body {
  /* background-color: plum;  修改前 */
  background-color: peru; /* 修改后 */
}

img

较之上次,css 文件名发生了改变,而 js 文件名却没有变化
所以,contenthash 可以认为是单个输出文件内容的 hash,或者说 bundle 的 hash

七. devServer 的使用(localhost、HMR)

就目前来说,每次我们修改完代码,都需要重新打包,然后刷新页面才能看到最新的效果,这极大的影响了我们的开发效率。那有没有办法可以在每次修改后,自动打包并刷新页面呢?答案肯定是有的,就是接下来我们要用到的devServer

webpack-dev-server 是 webpack 官方提供的一个小型 Express 服务器。使用它可以为 webpack 打包生成的资源文件提供web服务。

首先需要安装 webpack-dev-server,接着在 package.json 中添加一个脚本

npm i webpack-dev-server -D

"scripts": {
  ...
  "start": "webpack-dev-server"
}

在命令行执行 npm start 即可(start 是一个特殊的命令,不同于其他命令,直接 npm start 就行,中间不需要 run),如下图所示,启动成功,打开 localhost:8081 便可访问

img

并且我们发现dist目录中没有任何文件,那是因为 webpack-dev-server 将我们的代码打包到了内存中,并没有输出到指定目录

修改项目中的 js、 css 或 html 文件然后保存,可以看到页面会自动刷新展示最新效果(无法贴图演示,可自行尝试)

每次启动项目时都需要手动打开浏览器,很不方便,其实 devServer 是可以自动打开浏览器的,只需要一个简单的配置

// webpack.config.js
module.exports = {
  // ...
  devServer: {
    open: true, // 自动打开浏览器,下面三项不是必须的
    host: "0.0.0.0", // 如果希望被局域网访问,设为 0.0.0.0,默认 localhost
    port: "8888",  // 端口,默认 8080
    useLocalIp: true // 使用本地ip,如果 host 设置 0.0.0.0,请将此参数设为 true,否则结果就会像下面这样
  }
}

img

每次修改完内容保存自动刷新页面,看起来没有什么问题,但是当我们的项目很庞大的时候,就会显得特别的慢,因此我们需要使用 devServer 提供的 HMR (Hot Module Replacement,热模块替换)技术,在 devServer 中添加一个参数即可开启热更新

hot: true // webpack 会自动引入 HotModuleReplacementPlugin 插件

修改完配置需要重新启动

img

热更新已经开启,修改 js 文件,内容会自动更新。但是修改 css,比如 body 的背景色

img

页面并没有热更新,因为 MiniCssExtractPlugin.loader 默认是不支持热更新的,在开发中,我们可以将其修改为之前的 style-loader(默认支持热更新),或者修改其参数

{
  loader: MiniCssExtractPlugin.loader,
  options: {
    publicPath: "../",
    hmr: true // 开启热更新
  }
}

重启并修改 css 中 body 的背景色,会发现页面自动更新了,并且只请求了与 css 相关的文件

img

html 一般不做热更新,因为 html 变了,肯定整个页面都需要刷新

devServer的其他常用配置

devServer: {
  contentBase: path.resolve(__dirname, "public"), // 告诉服务器从哪里提供内容,默认为当前工作目录
  wacthContentBase: true, // 监视 contentBase 里面的内容,一旦变化就 reload,
  watchOptions: {
    ignore: "", // 忽略哪些文件的变化
  },
  historyApiFallback: true, // 请求的资源不存在时返回 index.html,比如 vue-router 的 history 模式
  clientLogLevel: "none", // 不要显示启动日志信息
  overlay: false, // 如果出错了,不要全屏提示 
  progress: true, // 控制台输出运行进度
  compress: true, // 启用 gzip 压缩
  proxy: { // 设置代理
    "/api": { // 当 url 中含有 /api 时就会使用这里设置的代理
      target: "http://xxxx.com", // 目标服务器地址
      changeOrigin: true, // 跨域
      ws: true, // 代理websocket
      pathRewrite: {
        "^/api": "" // url 重写,将 url 里面的 /api 去掉
      }
    }
  }
}

为了更好的演示,后面的例子都不使用 devServer,继续使用 webpack 手动打包

八. js兼容性处理

之前的案例都是在 chrome 浏览器上运行的,接下来我们在某浏览器上试试

img

意料之中的报错,根据错误信息,知道是 util.js 文件中报的错,因为我们在 util.js 中使用了 es6 的 const 及箭头函数,而某浏览器(IE:看我作甚?)并不认识这些高阶语法

所以呢,我们就需要将这些高阶语法,转换成某浏览器能够识别的语法,也就是大名鼎鼎的 babel
需要安装 babel-loader、
@babel/core(babel核心库,核心 api 都在这里)、
@babel/preset-env(babel 预设,babel 是插件化的,转换不同的语法,需要不同的插件,预设的作用就是按需引入插件)

npm i babel-loader @babel/core @babel/preset-env -D

在配置文件中添加loader,重新打包

{
  test: /\.js$/,
  exclude: /node_modules/, // 不处理 node_modules 中的文件
  use: {
    loader: "babel-loader",
    // 也可以在项目根目录新建 .babelrc 文件,将配置写入文件中
    options: {
      presets: ["@babel/preset-env"]
    }
  }
}

img

成功运行,并且 const 转化成了 var, 箭头函数也被转成了普通函数
接下来我们在 index.js 中添加一段代码,打包后再次在ie中预览

new Promise(resolve => {
  setTimeout(() => {
    resolve("promise resolve")
  }, 1000);
}).then(res => {
  console.log(res);
});

img

Promise未定义,promise 是 ES6 中新出的 API ,@babel/preset-env 只能转换高阶的语法,并不能转换高阶的 API ,因此我们就需要使用 @babel/polyfill 来处理这些高阶的 API

@babel/polyfill 其实是 core-js2 与 regenerator-runtime 组成的一个集成包,使用 core-js2,则安装 @babel/polyfill,使用 core-js3 则安装 core-js 与 regenerator-runtime ,以 core-js3 为例

npm i core-js regenerator-runtime -S

修改 babel-loader 配置

{
  test: /\.js$/,
  exclude: /node_modules/,
  use: {
    loader: "babel-loader",
    options: {
      presets: [
        ["@babel/preset-env", {
          modules: false, // 对ES6的模块文件不做转化,以便使用 tree shaking
          useBuiltIns: "usage", // 取值可以是 false,"entry","usage"
          corejs: 3, // corejs 版本号
          targets: {} // 需要兼容的浏览器,若未配置,取 browserslist 中的值
        }]
      ]
    }
  }
}
关于useBuiltIns取值的说明

**false:**需要在 js 文件顶部引入,不需要指定 corejs 版本号,会将整个内容全部打包

// <--- core-js2 --->
// import "@babel/polyfill";

// <--- core-js3 --->
import "core-js/stable";
import "regenerator-runtime/runtime";

**entry:**需要在 js 文件顶部引入,需要指定 corejs 版本号,根据配置的浏览器,打包浏览器不兼容的内容
**usage:**不需要在 js 文件顶部引入,需要指定corejs 版本号,根据配置的浏览器兼容性,以及代码中用到的 API 来按需打包

本案例使用 usage,因此不需要在 js 文件顶部引入包,配置完成后直接打包刷新页面,结果如下,没有报错,成功打印

img

当项目里的 js 文件越来越多时,babel 转换耗时会越来越长,可以使用 babel 缓存及多进程打包来提高速度,安装 thread-loader,修改 babel 配置

{
  test: /\.js$/,
  exclude: /node_modules/,
  use: [
    {
      loader: "thread-loader", // 耗时比较长的 loader 才需要多进程,否则只会更慢
      options: {
        workers: 2 // 进程数 2
      }
    },
    {
      loader: "babel-loader",
      options: {
        cacheDirectory: true, // 开启babel 缓存,未修改的 js 文件直接取缓存
        presets: [...]
      }
    }
  ]
}

九. tree shaking

移除文件中未被引用使用的代码,它依赖于 ES6 模块语法的静态结构特性,例如 import 和 export
满足以下两个条件即可 tree shaking

  1. 使用 es6 模块化语法(使用 babel 时记得设置参数 modules 为 false)
  2. 生产模式打包(默认开启 tree shaking)

为了演示效果,我们对代码做如下修改

// util.js 由原本的 ESModule 导出改为 commonJS 导出,并随便添加一个函数
exports.print = str => {
  console.log(str);
}
exports.test = () => {
  console.log("tree shaking");
}

// index.js 修改 print 函数的引入方式
const print = require("./util").print;
print("hello webpack")

util.js 中新增的 test 函数并未引入使用,然后使用生产模式打包,在打包生成的 main.js 中搜索 “tree shaking”

img

可以看到,虽然 test 函数并未引入使用,但是依然被打包进来了。将模块化语法改为 ESModule,重新打包并搜索 “tree shaking”

// util.js
export const print = str => {...}
export const test = () => {...}

// index.js
import  {print} from "./util";  // 只引入 print
print("hello webpack")

结果如下图,main.js 中搜索不到 “tree shaking”,说明 test 函数并未被打包进来

img

如果只是引入,并未使用,依然会被移除,例如下面的代码,test 未被打包进文件

import {print, test} from "./util"; // 两个都引入,但是不使用 test

**sideEffects:**哪些文件具有副作用,配合 tree shaking 使用
所谓副作用是指在导入时会执行特殊行为的代码,就比如 css 文件,只要导入,势必影响样式
tree shaking 只会对没有副作用的文件生效,例如我们将所有文件都设置为无副作用

// package.json
{
  "sideEffects": false // 表示所有文件都没有副作用
}

生产模式打包然后刷新页面,会发现所有的样式都没有了,即 css 被 tree shaking 了

img

项目开发时,如果代码确实有一些副作用,将其文件路径放入 sideEffects 数组中,以防被 tree shaking,可以是绝对路径、相对路径或通配符,例如:

"sideEffects": [
  "*.css",
  "./src/someSideEffectfulFile.js"
]

网络资料显示 sideEffects 默认值为 true,即所有文件都有副作用,但是上面的例子中,明显 util.js 被认为没有副作用,所以默认值是啥暂不清楚,而且就算手动设置为 true,util.js 还是会被 tree shaking(不知道是否与 webpack 版本有关)

十. 其他配置

1.resolve:设置模块如何被解析

**alias:**为路径设置别名,让引入变得更简单

// webpack.config.js
module.exports = {
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "src"),
      // $ 匹配结尾,不会影响 vue/xxx 的路径,表示项目中引入的 vue 为运行时版本
      "vue$": "vue/dist/vue.runtime.js" 
    }
  }
}
// 例如某 .vue 文件中引入的一个组件为
import xxx from "../../components/xxx";
// 则可写为
import xxx from "@/components/xxx";

**extensions:**引入哪些类型的文件时可以省略后缀名

resolve: {
  // 默认值为 [".js", ".json"]
  extensions: [".js", ".json", ".vue"] // 引入 js、json、vue 文件时不需要写后缀名
}

**modules:**解析模块时应该搜索的目录

resolve: {
  // 默认值:["node_modules"],表示从当前目录的 node_modules 中查找,不存在时则去上级目录的 node_modules 中查找,一直到根目录为止。
  // 若设置为绝对路径,则只在指定的目录中查找
  modules: [path.resolve(__dirname, "src"), "node_modules"]
}
2. externals:设置某些库不被打包,而是运行时去外部获取
// index.js 添加代码
import $ from "jquery";
$("p").css("color", "red");

在 index.js 中引入 jquery,最终会被打包进 main.js 中,导致整个项目体积过大,使用 externals 排除 jquery 打包,并通过 cdn 引入(index.js 中的引入要保留,不能删除)

// index.html 中 body 底部添加
<body>
  <% for (let src of htmlWebpackPlugin.options.cdnList) { %>
    <script src="<%= src %>"></script>
  <% } %>
  <!-- 直接写死,与上面二选一,推荐上面的方式 -->
  <script src="https://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
</body>

// webpack.config.js 添加下面内容
module.exports = {
  externals: {
    jquery: "jQuery" // key 为引入的包名,value 为全局变量名
  },
  plugins: [
    new HtmlWebpackPlugin({
      // 用来在 html 中通过 script 引入,cdnList 为自定义变量。若直接写死则不需要此配置
      cdnList: ["https://libs.baidu.com/jquery/2.0.0/jquery.min.js"]
    })
  ]
}
3. watch:初始构建之后,继续监听任何已解析文件的更改

将此参数设置为 true,使用 webpack 打包之后,每当有文件内容发生变化,就会自动重新打包(devServer默认开启)

module.exports = {
  watch: true,
  watchOptions: {
    aggregateTimeout: 300, // 延时 300ms 打包,防抖
    ignored: /node_modules/, // 忽略监听,也可以是 anymatch 模式,例: "files/**/*.js"
    poll: true // 开启轮询模式,如果值为数字,表示轮询的间隔,单位毫秒
  }
}
4. devtool:控制如何生成 source map

source map 是一个信息文件,保存源代码与打包后代码的映射关系,帮助我们快速定位报错的代码
在 index.js 中添加一行报错的代码,比如打印一个未声明的变量 a,生产模式打包,报错信息如下图,左边未使用 source map,很明显,无法准确定位到报错的代码,而右边直接映射到了打包前的代码

img

关于不同 source map 之间的区别,请自行百度,不做阐述

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ChrisP3616

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值