webpack 使用指南

859 阅读8分钟

参考文献

一、概述

webpack-des.png

本质上,webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具。当 webpack 处理应用程序时,它会在内部从一个或多个入口点构建一个 依赖图(dependency graph),然后将你项目中所需的每一个模块组合成一个或多个 bundles,它们均为静态资源,用于展示你的内容。

从图中我们可以看出,webpack 可以将多种静态资源 js、css、sass 转换成一个静态文件,减少了页面的请求。

1. 什么是webpack?

webpack 可以看做是 模块打包机:它做的事情是,分析你的项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言(Sass,TypeScript等),并将其转换和打包为合适的格式供浏览器使用。在3.0出现后,Webpack还肩负起了优化项目的责任。

这段话有三个重点:

  • 打包:可以把多个 JavaScript 文件打包成一个文件,减少服务器压力和下载带宽。
  • 转换:把拓展语言转换成为普通的 JavaScript,让浏览器顺利运行。
  • 优化:前端变的越来越复杂后,性能也会遇到问题,而 webpack 也开始肩负起了优化和提升性能的责任。

2. 为什么需要webpack?

webpack 是现代前端技术的基石,常规的开发方式,比如 jQuery、HTML、CSS 静态网页开发已经落后了。现在是 MVVM 的时代,数据驱动视图,webpack 将现代js开发中的各种新型有用的技术,集合打包。通过下图理解webpack生态圈:

webpack-ecosphere.jpg

3. 模块化

模块化是一种将复杂系统分解为更好的可管理模块的方式,简单来说就是解耦。通常一个文件就是一个模块,有自己的作用域,只向外暴露特定的变量和函数。

其优势为:简化开发、按需加载、便于管理、可复用。

目前流行的js模块化规范有CommonJS、AMD、CMD以及ES6的模块系统。webpack是一个模块打包机,它对模块有一个更广泛的定义,对于webpack来说,模块是:

  • Common JS modules
  • AMD modules
  • ES modules
  • CSS import
  • Images url

webpack 还可以从这些模块中获取 依赖关系

更多内容,可参考 这里 >>

4. 构建过程

初始化编译输出

  • 初始化:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler
  • 编译:从 Entry 发出,针对每个 Module 串行调用对应的 Loader 去翻译文件内容,再找到该 Module 依赖的 Module,递归地进行编译处理。
  • 输出:对编译后的 Module 组合成 Chunk,把 Chunk 转换成文件,输出到本地。

二、基础知识

1. Entry

Entry >>:入口起点,指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。

入口文件常用的配置形式如下:

module.exports = {
    entry: {
        "main": "./src/js/main.js",
        "news": "./src/js/news.js"  
    }
}

提示:在多页面项目中设置出口时,通过 [name] 即可获取文件名,其中文件名就是入口设置中的 key 项。

2. Output

Output >>:该属性告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件。你可以通过在配置中指定一个 output 字段,来配置这些处理过程。

出口文件常用的配置形式如下:

output: {
  // 输出目录/绝对路径
  path: path.resolve(__dirname, "./dist/"),
  // 输出文件名
  filename: "js/[name]-bundle-[hash].js",
}
  • [name]:模块名称,也就是在指定入口时的 key 值。
  • [hash]:打包后文件的 hash 值,md5,保证文件唯一性。
  • [chunkhash]:模块自身的hash值。

3. Loader

Loader >>:webpack 只能理解 JavaScript 和 JSON 文件,这是 webpack 开箱可用的自带能力。loader 让 webpack 能够去处理其他类型的文件,并将它们转换为有效 模块,以供应用程序使用,以及被添加到依赖图中。

module.exports = {
    module: {
        rules: [...loaders]
    }
}

在 webpack 的配置中,loader 有以下个属性:

  • test:required - 处理文件
  • use:required - 加载器
  • include/exclude:包含/不包含文件(可选);

4. Plugins

Plugins >>:插件是用来拓展Webpack功能的,它们会在整个构建过程中生效,执行相关的任务。

LoadersPlugins 对于新手常常被弄混,但是他们其实是完全不同的东西,可以这么来说,loaders 是在打包构建过程中用来处理源文件的,一次处理一个,插件并不直接操作单个文件,它直接对整个构建过程起作用。

Webpack有很多内置插件,同时也有很多第三方插件,可以让我们完成更加丰富的功能。

使用插件步骤:安装插件导入插件配置插件:在 plugins 数组中创建插件实例

5. Mode

Mode >>:用于配置打包环境,它主要有以下两个值:

  • development:开发环境
  • production:生成环境(会自动压缩打包后的文件)

6. Context

Context >>:上下文,基础目录,绝对路径,用于从配置中解析入口起点和 loader,入口起点会相对于此目录查找。默认使用 Node.js 进程的当前工作目录,即配置文件所在的目录。webpack 推荐在配置中传入一个值,这使得你的配置独立于 CWD(current working directory, 当前工作目录)。

一般当自定义配置文件之后,我们需要设置该属性,比如配置文件放在在 build/ 目录中,则上下文配置如下:

context: path.resolve(__dirname, "../");

三、实战

了解了webpack的基础知识以后,接下来我们通过实战的形式帮助大家去理解webpack的配置。

本案例主要以单页配置为主,多页配置其实也就是在配置入口时根据实际需要配置多个入口即可。

1. 准备工作

① 创建项目 - 安装依赖

$ mkdir webpack-demo & cd webpack-demo & npm init -y 
$ npm install webpack webpack-cli webpack-dev-server clean-webpack-plugin webpack-bundle-analyzer --save-dev
$ ./node_modules/.bin/webpack --version    
webpack: 5.65.0
webpack-cli: 4.9.1
webpack-dev-server 4.7.1

提示:windows 系统提示 “'.' 不是内部或外部命令,也不是可运行的程序或批处理文件。”,需将上述指令中路径部分中的 / 变为 \ 即可。

依赖解读:

  • webpack-dev-server:开发服务,配置此插件可以实现热替换和自动刷新。
  • clean-webpack-plugin:文件清除,每次webpack打包之前需调用此插件清除上一次打包的文件。
  • webpack-bundle-analyzer:依赖分析,可以据此分析项目哪些模块体积较大,然后进行优化。

② 目录结构

webpack-test
.
├── node_modules         # 安装依赖时自动生成
├── public 
│   └── UHgu8uXGys.txt   # 校验文件,可随意创建,比如README.MD,主要用于测试打包时拷贝
├── src                  # 源码文件
│   ├── fonts	         # 字体文件,可自行到字体网站下载
│   │   └── din-regular.otf 
│   ├── images           # 图片资源 
│   │   └── logo.png
│   ├── styles           # 样式,本案例主要讲解 less 编译
│   │   └── index.less
│   ├── utils            # 工具函数
│   │   └── index.js
│   ├── app.js         	 # 入口文件
│   └── index.html	 # 模板文件
├── package.json								# 
└── webpack.config.js    # webpack 配置文件

③ 文件内容

npm script

"scripts": {
  "build": "webpack --mode=production",
  "dev": "webpack --mode=development",
  "serve": "webpack serve --open --hot --host=local-ip --port=8090 --mode=development"
},

webpack.config.js

// 1. 引入模块
const path = require('path');
const webpack = require('webpack');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

// 2. 导出配置
module.exports = {
  context: path.resolve(__dirname, './'),
  entry: {
    main: './src/app.js',
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name]-bundle-[hash].js',
  },
  plugins: [
    new CleanWebpackPlugin(),
    new webpack.BannerPlugin('版权Li-HONGYAO所有,翻版必究!'),
  ],
  devServer: {
    liveReload: true,
    watchFiles: ['src/**'],
    static: {
      directory: path.join(__dirname, 'dist'),
    },
  },
};

src/styles/index.less

@keyframes ani {
  to {
    transform: translateY(100px);
  }
}
@font-face {
  font-family: 'din-regular';
  src: url('../fonts/din-regular.otf');
}

img {
  width: 500px;
}
.logo {
  width: 200px;
  height: 78px;
  background: url('../images/logo.png') no-repeat center center / cover;
  margin: 50px 0;
}

#title {
  color: blue;
  letter-spacing: 2px;
  font-size: 36px;
  font-family: "din-regular";
  animation: ani 2s linear infinite alternate;
}

.wrapper {
  display: flex;
  justify-content: center;
  align-items: center;
}

src/utils/index.js

/**
 * 修改元素标题
 * @param {*} id
 * @param {*} title
 */
export function setTitle(id, title) {
  const dom = document.getElementById(id);
  dom.textContent = title;
}

src/app.js

import * as Utils from './utils/index.js';
import "./styles/index.less";

Utils.setTitle("title", "Hello, webpack!!!");

src/index.html

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>webpack - test</title>
  </head>
  <body>
    <h1 id="title"></h1>
    <div class="logo"></div>
    <img src="./images/logo.png"/>
  </body>
</html>

2. 打包HTML

安装依赖:

$ npm install html-webpack-plugin --save-dev

配置文件:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      // -- 模板文件
      template: 'src/index.html',
      // -- 文件名,相对于output.path,
      // -- 可通过文件名设置目录,如 static/pages/detail.htm
      filename: 'index.html',
      // -- 指定输出文件所依赖的入口文件(*.js)的[name]
      chunks: ['main'],
    }),
  ]
};

3. 打包脚本

安装依赖:

$ npm install babel-loader @babel/core @babel/preset-env --save-dev

配置文件(module.rules):

{
  test: /\.js$/,
  exclude: /node_modules/,
  use: {
    loader: 'babel-loader',
    options: {
      presets: ['@babel/preset-env', { targets: 'defaults' }],
    },
  },
},

4. 打包样式

安装依赖:

$ npm install style-loader css-loader less less-loader postcss-loader postcss-preset-env --save-dev

依赖解读:

配置文件:

{
  test: /\.less$/,
  exclude: /node_modules/,
  use: [
    'style-loader',
    'css-loader',
    {
      loader: 'postcss-loader',
      options: {
        postcssOptions: {
          plugins: ['postcss-preset-env'],
        },
      },
    },
    'less-loader',
  ],
},

注意:引用顺序为从右到左。

→ 分离样式

如果需要分离CSS文件,可使用插件 mini-css-extract-plugin,webpack v4.0之前使用 extract-text-webpack-plugin

首先安装依赖:

$ npm install --save-dev mini-css-extract-plugin

然后修改配置文件:

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'css/[name]-[hash].css',
    }),
  ],
  module: {
    rules: [
      {
        test: /\.less$/,
        exclude: /node_modules/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          {
            loader: 'postcss-loader',
            options: {
              postcssOptions: {
                plugins: ['postcss-preset-env'],
              },
            },
          },
          'less-loader',
        ],
      },
    ],
  },
};

提示:如果使用 MiniCssExtractPlugin,就不需要引入 style-loader 了。

→ 去除无效样式

安装依赖:

$ npm install  purgecss-webpack-plugin --save-dev

配置代码:

const glob = require('glob');
const PurgeCSSPlugin = require('purgecss-webpack-plugin');

module.exports = {
    plugins: [
        new PurgeCSSPlugin({
        	paths: glob.sync('./src/**/*', { nodir: true }),
        }),
    ]
}

5. 打包图片

webpack5 新增了资源模块 >>asset module),它允许使用资源文件(字体,图标等)而无需配置额外 loader。在 webpack5之前,通常使用 url-loaderfile-loader 处理图片、字体等静态资源。

安装依赖:

$ npm install html-loader --save-dev

提示:安装 html-loader 的目的是为了能够在 html 文件中通过 src 属性引入的图片资源,需将 esModule: false

配置文件:

module.exports = {
  module: {
    rules: [
      // -- 打包图片
      {
        test: /\.(png|svg|jpg|jpeg|gif)$/i,
        exclude: /node_modules/,
        type: 'asset/resource',
        generator: {
			filename: 'images/[hash][ext][query]',
        },
      },
      // - 处理html文件中的img图片(负责引入img)
      {
        test: /\.html$/,
        exclude: /node_modules/,
        loader: 'html-loader',
        options: {
          esModule: false,
        },
      },
    ],
  },
};

6. 打包字体

同样的,打包字体我们直接使用 webpack5中的 Asset Modules >>,无需安装 loader,直接配置即可:

// -- 打包字体
{
  test: /\.(woff|woff2|eot|ttf|otf)$/i,
  exclude: /node_modules/,
  type: 'asset/resource',
  generator: {
    filename: 'fonts/[hash][ext][query]',
  },
},

7. 拷贝资源

开发中,有时我们需要将一些资源在打包时直接拷贝至根目录,比如微信公众号配置业务域名时,需将校验文件放置在域名根目录,我们可以将其放置在 public 目录下,然后通过 copy-webpack-plugin >> 将其拷贝至输出根目录下。

安装依赖:

$ npm install copy-webpack-plugin --save-dev

配置文件:

const CopyPlugin = require('copy-webpack-plugin');

module.exports = {
    plugins: [
        new new CopyPlugin({
            patterns: [{ from: 'public' }]
        })      
    ]
}

8. 完整配置文件


// 1. 引入模块
const path = require('path');
const webpack = require('webpack');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CopyPlugin = require('copy-webpack-plugin');
const glob = require('glob');
const PurgeCSSPlugin = require('purgecss-webpack-plugin');

// 2. 导出配置
module.exports = {
  context: path.resolve(__dirname, './'),
  entry: {
    main: './src/app.js',
  },
  output: {
    path: path.resolve(__dirname, './dist/'),
    filename: '[name]-bundle-[hash].js',
  },
  module: {
    rules: [
      // -- 打包脚本
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
          },
        },
      },
      // -- 打包样式
      {
        test: /\.less$/,
        exclude: /node_modules/,
        use: [
          MiniCssExtractPlugin.loader,
          // 'style-loader',
          'css-loader',
          {
            loader: 'postcss-loader',
            options: {
              postcssOptions: {
                plugins: ['postcss-preset-env'],
              },
            },
          },
          'less-loader',
        ],
      },
      // -- 打包图片
      {
        test: /\.(png|svg|jpg|jpeg|gif)$/i,
        exclude: /node_modules/,
        type: 'asset/resource',
        generator: {
          filename: 'images/[hash][ext][query]',
        },
      },
      // -- 处理html文件中的img图片(负责引入img)
      {
        test: /\.html$/,
        exclude: /node_modules/,
        loader: 'html-loader',
        options: {
          esModule: false,
        },
      },
      // -- 打包字体
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/i,
        exclude: /node_modules/,
        type: 'asset/resource',
        generator: {
          filename: 'fonts/[hash][ext][query]',
        },
      },
    ],
  },
  plugins: [
    new CleanWebpackPlugin(),
    new webpack.BannerPlugin('版权Li-HONGYAO所有,翻版必究!'),
    new MiniCssExtractPlugin({
      filename: 'css/[name]-[hash].css',
    }),
    new CopyPlugin({
      patterns: [{ from: 'public' }],
    }),
    new PurgeCSSPlugin({
      paths: glob.sync('./src/**/*', { nodir: true }),
    }),
    new BundleAnalyzerPlugin(),
    new HtmlWebpackPlugin({
      // -- 模板文件
      template: 'src/index.html',
      // -- 文件名,相对于output.path,
      // -- 可通过文件名设置目录,如 static/pages/detail.htm
      filename: 'index.html',
      // -- 指定输出文件所依赖的入口文件(*.js)的[name]
      chunks: ['main'],
    }),
  ],
  devServer: {
    liveReload: true,
    watchFiles: ['src/**'],
    static: {
      directory: path.join(__dirname, 'dist'),
    },
  },
};

四、延伸

1. 指定配置文件编译

在实际开发过程中,你可能会分环境创建不同的配置文件来满足不同的开发需求,比如你在开发阶段,通常会创建一个 webpack.dev.config.js 文件,那么你在执行编译指令的时候需要指向该配置文件,如下所示:

$ ./node_modules/.bin/webpack --config ./build/webpack.dev.config.js

提示:假设配置文件的路径是:./build/webpack.dev.config.js,那你需要在配置文件中做如下修改:

module.exports = {
 context: path.resolve(__dirname, "../"),
 output: {   
     path: path.resolve(__dirname, "../dist/"),
 }
};

注意:

  • 一旦修改了webpack的配置文件,必须重启服务或重新build。否则失效。
  • 如果自定义配置文件,切记在执行打包时一定要指定配置文件路径

2. 编译参数配置

webpack 自身提供了一些参数来优化编译任务,以下简单列出了一些参数:

参数描述
--config指定配置文件
--watch, -w监听变动并自动打包
-p压缩混淆脚本
--progress显示进度条

提示:想了解webpack更多参数,可在终端输入 ./node_modules/.bin/webpack -h 查看

3. 引用三方库

→ 局部引入

import $ from 'jquery';

→ 全局引入

new webpack.ProvidePlugin({
  $: 'jquery',
  jQuery: 'jquery',
}),

5. alias

创建 importrequire 的别名,来确保模块引入变得更简单。例如,一些位于 src/ 文件夹下的常用模块:

moudle.exports = {
  resolve: {
    alias: {
      '@utils': path.resolve(__dirname, 'src/utils/'),
    },
  },
};

现在,替换“在导入时使用相对路径”这种方式,就像这样:

import * as Utils from './utils/index.js';

可以这样使用别名:

import * as Utils from '@utils/index.js';

6. 抽离公共文件

当一部分代码需要反复被用到,反复请求浪费资源,将公共代码 抽离,需要时读取缓存即可

output: {
  ...
  chunkFilename: "[name].chunk.js"
}
optimization: {
    splitChunks: {
        cacheGroups: {// 缓存组,缓存公共代码
            // 首先:打包node_modules中的文件
            vendor: {
                test: /node_modules/,
                name: "vendor",
                minSize: 0, 
                minChunks: 1,
                chunks: "all",
                priority: 1 
            },
            // 其次: 打包业务中公共代码
            common: {
                name: "common",
                chunks: "all",  
                minSize: 0,      
                minChunks: 2,   
                priority:0
            }
        }
    }
}