[构建 Webpack 知识体系 | 青训营笔记13]

67 阅读8分钟

前言

这是我参与「第五届青训营」伴学笔记创作活动的第 13天,前几篇我们介绍了React用的jsx语法,而浏览器是识别不了jsx语法,得通过webpack等编译打包工具。那么我们就来看一下webpack具体是什么?

为什么要学习Webpack?

  • 理解前端“工程化”概念、工具、目标
  • 一个团队总要有那么几个人熟悉 Webpack某种程度上可以成为个人的核心竞争力
  • 高阶前端必经之路

主要内容

  • 什么是 Webpack
  • Webpack 打包核心流程
    • 示例
    • Entry => Dependencies Lookup => Transform => Bundle => Output
    • 关键配置项介绍
  • Loader 组件
  • Plugin 组件
  • 如何学习 Webpack
    • 入门级: 学会灵活应用
    • 进阶: 学会扩展 Webpack
    • 大师: 源码级理解 Webpack 打包编译原理

课程目标

  • 理解 Webpack 的基本用法
  • 通过介绍 Webpack 功能、LoaderPlugin 组件设计,建立一个知识体系
  • X 不会事无巨细,介绍 Webpack 所有
  • X 也不是深入源码,讲解底层实现原理

webpack认知,关于webpack的用法,webpack的扩展性

什么是 Webpack

  • 资源前端项目由什么构成?
    • PNG
    • css
    • JPG
    • Less
    • GIF
    • Vue
    • WEBP
    • JSX
    • js
    • Sass
    • TS
    • ...

资源管理方式

以前是通过以下方式去管理这些资源:

<!DOCTYPE html>
<html lang="en">
<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>Document</title>
  <link rel="stylesheet" href="./style.css">
</head>
<body>
  <div>
    <img src="./img.jpg" alt="" >
  </div>
  <script src="./main.js"></script>
  <script src="./foo.js"></script>
  <script src="./bar.js"></script>
</body>
</html>
  • 依赖手工,比如有50个JS文件...操作,过程繁琐
  • 当代码文件之间有依赖的时候,就得严格按依赖顺序书写
  • 开发与生产环境一致,难以接入 TSJS 新特性
  • 比较难接入 LessSass 等工具
  • JS、图片、CSS 资源管理模型不一致

这些都是旧时代非常突出的问题,对开发效率影响非常大,直到webpack 等构建打包工具的出现

工程化工具

出现了很多工程化工具,某种程度上正是这些工具的出现,才有了“前端工程”这一概念 628034ef-8954-4956-817a-5b9efcd1350a.jfif 3c40773e-a0f1-438e-ac05-4af865c6fbac.png

9b2017fd-89bb-44e1-be92-8867c70a75ee.png

a3ec07b3-8f0d-4893-8509-b4609c0ff8e5.jfif

a435f866-8f27-490b-a1bb-d2a5d2bc0945.png

a591fba0-4864-49b6-9e4f-2e88948710cd.png

e59f8ace-9e9b-49f0-87b9-e057695492e3.png

本质上是一种前端资源编译、打包工具 fe524fb4-fc46-4c23-a926-a7fd3cec535b.png

  • 多份资源文件打包成一个 Bundle
  • 支持BabelEslintTSCoffeScriptLessSass
  • 支持模块化处理 css、图片 等资源文件
  • 支持 HMR + 开发服务器
  • 支持持续监听、持续构建
  • 支持代码分离
  • 支持 Tree-shaking
  • 支持 Sourcemap
  • ...

使用 Webpack

使用 Webpack -- 示例

  1. 安装
npm i -D webpack webpack-cli 
  1. 编辑配置文件
module.exports = {
  entry: 'main.js',
  output: {
     filename:"[name].js"
     path: path.join(__dirname,"./dist"),
  },
  module: {
      rules: [{
          test: /\.less$/i,
          use: ['style-loader','css-loader','less-loader']
      }]
  }
}

  1. 执行编译命令
npx webpack

654413c9-751e-481a-bf23-c7198887a6d8.png

核心流程 - 极度简化版

  • Get Start
    • entry
      • 1.入口处理; 从 entry’文件开始,启动编译流程
  • Dependencies Lookup
    • require import
      • 2. 依赖解析; 从entry文件开始,根据requireor import等语句找到依赖资源
  • Transfrom
    • module
      • 3. 资源解析; 根据 module配置,调用资源转移器,将pngcss 等非标准JS资源转译为 JS 内容
  • Combine Assets
    • output
      • 4. 资源合并打包; 将转译后的资源内容合并打包为可直接在浏览器运行的 JS 文件

递归调用2、3,直到所有资源处理完毕

模块化 + 一致性

  • 多个文件资源合并成一个,减少 http 请求数
  • 支持模块化开发
  • 支持高级 JS 特性
  • 支持TypescriptCoffeeScript 方言
  • 统一图片、CSS、字体 等其它资源的处理模型
  • Etc..

那么,怎么使用 Webpack?


关于 Webpack 的使用方法,基本都围绕“配置展开,而这些配置大致可划分为两类:

  • 流程类: 作用于流程中某个 or 若干个环节直接影响打包效果的配置项可
  • 工具类: 主流程之外,提供更多工程化能力 的配置项
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { HotModuleReplacePlugin } = require('webpack')

module.exports = {
    entry: './src/index',
    mode: 'devlopment',
    devtool: false,
    output: {
        filename: '[name].js',
        path: path.join(__dirname,'./dist'),
        clean: true,
    },
    devServer:{
    hot: true,
    }
    plugins: [
    new HtmlWebpackPlugin({
        template: './public/index.html',
        title: 'Hot Module Replacement',
         }),
    ] 
};

使用 Webpack -- 流程类配置

  • Get Start
    • entry
        1. 输入 entry,context
  • Dependencies Lookup
    • require import
      • 2. 模块解析 resolve,externals
  • Transfrom
    • module
      • 3. 模块转译 module
  • Combine Assets
    • output
      • 4. 后处理: optimization,mode,target

使用 Webpack --配置总览

按使用频率:

  • entry/output
  • module/plugins
  • mode
  • watch/devServer/devtool

Webpack 配置官方文档

715b12c4-f8a6-4bd2-bab3-305bce05d6a1.png

文件结构

|-- src
|    - index.js
|- webpack.config.js
  1. 声明入口entry
module.exports = {
    entry: './src/index'
}
  1. 声明产物入口output
// webpack.config.js
const path = require('path')

module.exports = {
   entry:'src/index',
   output: {
       filename:'[name].js',
       path: paht.join(__dirname,'./dist')
   }
}
  1. 运行npx webpack

下载 webpckwebpack-cli,然后配置文件,文件内容至少配置 entryoutput 模块;通过 npx webpack 命令运行

使用 Webpack -- 处理 CSS

|-- src
|    - index.js
|    - index.css
|- webpack.config.js
const styles = require('./index.css');
// or
import styles from  './index.css'
  1. 安装 Loader
npm add  -D css-loader style-loader
  1. 添加module处理css 文件
// webpack.config.js
const path = require('path')

module.exports = {
   entry:'./src/index',
   output: {
       filename:'[name].js',
       path: paht.join(__dirname,'./dist')
   },
   module:{
       // css 处理器
       rules:[{
           test: /\.css/i,
           use:[
               'style-loader',
               'css-loader'
          ]
       }]
   }
}

微信截图_20230210104050.png

通过modulerules字段配置相应的规则,通过test字段,过滤文件名和文件后缀,通过use字段配置相应的loader序列;loader是在webpack用来处理不同的资源的组件


<!DOCTYPE html>
<html lang="en">
<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>Document</title>
  <link rel="stylesheet" href="./style.css">
</head>
<body>
  <div>
    <img src="./img.jpg" alt="" >
  </div>
  <script src="./main.js"></script>
  <script src="./foo.js"></script>
  <script src="./bar.js"></script>
</body>
</html>

问题:

  • Loader 有什么作用? 为什么这里需要用到css-loaderstyle-loader
  • 与旧时代 -- 在 HTML 文件中维护 css 相比,这种方式会有什么优劣处?
  • 有没有接触过 LessSassStylus 这一类 CSS预编译框架? 如何在 Webpack 接入这些工具?
  1. loader用于处理不同资源文件的配置组件;
  2. 因为webpack 是用css-loader的css,后通过push到一个数组,将css 模块转化为 js 模块,然后将运用style-loader将这些css-loader转化为js的css,继续转换会css,再加上<style>标签上;
  3. 优点
    是减少很多繁琐重复的工作,提供了这样的模板写法,更清晰的看懂自己做了什么操作,只需关注单个文件,不需要在各个css文件进行穿梭和跳转;
    缺点
    是对新手不是那么的友好,需要记住许多的概念和配置,每深入一个概念就会有更多概念,让人难以琢磨
  4. 接触过less,也是需要配置相应的loader,只不过是在原先style-loadercss-loader上 多添加了less-loader

参考资料:
Css-loader
Webpack 原理系列七: 如何编写Loader
Style-loader

使用 Webpack -- 接入 Babel

文件结构

|-- src
|    - index.js
|- webpack.config.js
// index.js
class Person {
    constructor() {
        this.name = 'Tecvan'
    }
}

console.log((new Person()).name)

const say = () => {}
  1. 安装 依赖
npm i -D @babel/core @babel/preset-env babel-loader
  1. 声明产物出口output
// webpack.config.js
const path = require('path')

module.exports = {
   entry:'./src/index',
   output: {
       filename:'[name].js',
       path: paht.join(__dirname,'./dist')
   },
   module:{
       // css 处理器
       rules:[{
           test: /\.js?$/,
           use:[{
               loader: 'babel-loader',
               options: {
                   presets: [
                       [
                           '@babel/preset-env'
                       ]
                   ]
               }
           }
          ]
       }]
   }
}
  1. 执行npx webpack

下载.png

思考题:
Babel 具体有什么功能?
BabelWebpack 分别解决了什么问题? 为何两者能协作到一起了?

  1. babel 可以对js代码转译;
  2. 在那个浏览器不支持ES6的js新语法,所以那时候babel就 发挥出了它的作用;可以将高版本的js代码转译成低版本的js代 码,因为webpack集合各种loader对scassless等预处理 器进行编译和转译,还有一些plugin可以进一步提高开发效率; 通过webpack去管理这些loader,plugin,babel;可以理解为webpack负责管理这些工具函数和编译和打包,它们这些工具函数只需要负责自己的职责就好了

参考资料:
Babel-loader
Babel 官网
@babel/preset-env
@babel/preset-react
@babel/preset-typescript

使用 Webpack -- 生成 HTML

文件结构

.
|-- src
|    - index.js
|- webpack.config.js
  1. 安装依赖
npm i -D html-webpack-plugin
  1. 声明产物出口 output
// webpack.config.js
const path = require('path')

module.exports = {
   entry:'./src/index',
   output: {
       filename:'[name].js',
       path: paht.join(__dirname,'./dist')
   },
   plugins:[new HtmlWebpackPlugin()]
}
  1. 执行 npx webpack
<!- index.html  -!>
<!DOCTYPE html>
<html lang="en">
<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>Document</title>
  <script defer src="main.js"></script>
</head>
<body>
</body>
</html>

思考题:
相比于手工维护 HTML 内容,这种自动生成的方式有什么优缺点?

可以自动生成htmljs文件,假如要生成多个htmljs文件时会非常的方便,但是假如你只需要一两个文件时,就不需要去配置webpack.config.js文件了,直接去书写就好了;

参考资料:
html-webpack-plugin

使用 Webpack -- 工具线

adbbce35-9eea-455d-bf58-d3b0d4ab6929.png

使用 Webpack -- HMR

c482c3ef-8e27-4e03-8469-3eef323a98d0.gif

  1. 开启 HMR
// webpack.config.js
module.exports = {
    // ...
    devServer: {
        hot: true
    }
}
  1. 启动 Webpack
npx webpack serve

173b29a8-821e-4467-af2a-ac1cc596e2fa.png

可以利用webpack.config.js中的devServer:{hot:true}watch构建开发的服务器

参考: 《Webpack 原理系列十: HMR 原理全解析》

使用 Webpack- Tree-Shaking

Tree-Shaking - 树摇,用于删除 Dead Code

c37a0d3f-82f6-47a1-8f3a-b0222b039f0e.gif

d058dcab-d9f0-44aa-9506-14681ed09953.png

开启 tree-shaking:

  • mode:“production”
  • optimization.usedExports: true

PS: 对工具类库如 Lodash 有奇效

tree-shaking在生产环境、预编译时没有使用的全局变量进行清理


其它工具:

  • 缓存
  • Sourcemap
  • 性能监控
  • 日志
  • 代码压缩
  • 分包
  • ...

d468b305-fd30-482c-94ef-bc82bdbb7f1e.png

思考题

  • 除上面提到的内容,还有哪些配置可划分为“流程类”配置?
  • 工具类配置具体有什么作用? 包括 devtool/cache/stat
  1. resolve,performace
  2. devtool 就是可以用于开发工具,最常见是sourcemap可以开启打包前映射一份源文件,方便进行代码调试;cache可以启用缓存提升构建速度;stat可以进行 控制台输出日志控制

参考资料:
Awesome-webpack-4plus
《深入浅出 Webpack》
《Survivejs - Webpack Book》

进阶篇: 理解 Loader

// webpack.config.js
const path = require('path')

module.exports = {
   entry:'./src/index',
   output: {
       filename:'[name].js',
       path: paht.join(__dirname,'./dist')
   }
}
// index.js
import './index.css'

下载 (1).png

使用 Loader

.
|-- src
|    - a.less
|    - b.less
|    - index.js
|- webpack.config.js
// index.js
import styles from './a.less';
  1. 安装 Loader
npm add -D css-loader style-loader less-loader
  1. module处理 css 文件
// webpack.config.js
const path = require('path')

module.exports = {
  module:{
       rules:[{
           test: /\.less$/i,
           use:[
               'style-loader',
               'css-loader',
               'less-loader',
               }
           }
          ]
       }]
   }
}

认识 Loader: 链式调用

aee5057f-a3ae-4ebe-8605-5e7413b6a12c.png

  • less-loader: 实现 less => css 的转换
  • css-loader: 将 CSS 包装成类似 module.exports ="${css}"的内容,包装后的内容符合 JavaScript 语法
  • style-loader: 将 css 模块包进 require 语句,并在运行时调用 iniectStyle 等函数将内容注入到页面的 style 标签

认识 Loader: 其它特性

下载 (2).png

特点:

  • 链式执行
  • 支持异步执行
  • normalpitch 两种模式

参考:
Webpack 原理系列七: 如何编写loader

如何编写 Loader

// eslint-loader/index.js
import getOptions from './getOptions';
import Linter from './Linter';
importcacheLoader from './cacheLoader';
export default function loader(content, map) {
    const options = getOptions(this);
    const linter = new Linter(this, options);
    this.cacheable();

    // return early if cached
    if (options.cache) {
    cacheLoader(linter, content, map);
    return;
    }
    linter.printOutput(linter.lint(content));
    this.callback(null, content, map);
}
module.exports = function (source,sourceMap?,data?) {
    // source 为 loader 的输入
    // 可能是文件内容,也可能是上一个 loader 处理结果
    return source;
}

参考:
Webpack 原理系列七: 如何编写loader

常见 Loader

站在使用角度,建议掌握这些常见Loader 的功能、配置方法

24199aac-7610-4ab0-aee7-ddc70a60f7a1.png

理解插件

思考题:

进阶篇:理解插件

插件是什么? 为什么这么设计?

很多知名工具,如:

  • VS CodeWeb StormChromeFirefox
  • Babel、WebpackRollupEslint
  • VueReduxQuillAxios

等等,都设计了所谓插件架构,为什么?

因为需要整合插件的分类方便进行管理,各司其职

a20b7514-3244-4afa-810e-650e9c8809d8.png 这是一个特别复杂的过程,那么:

  • 新人需要了解整个流程细节,上手成本高
  • 功能迭代成本高,牵一发动全身
  • 功能僵化,作为开源项目而言缺乏成长性
  • Blabla

心智成本高 => 可维护性低 => 生命力弱

插件架构精髓:对扩展开放,对修改封闭

甚至,Webpack 本身的很多功能也是基于插件实现的

理解插件

1573348b-b97d-4449-9528-2236f7e10fe3.png

d335c7eb-2a1a-4c92-81a6-0e6ca15a68bb.png

// webpack.config.js

// npm i -D webpack-dashboard

// Import the plugin:
const DashboardPlugin = require( "webpack-dashboard/plugin");

// Add it ta your webpack configuration plugins.
module.exports = {
     // ... 
    plugins: [new DashboardPlugin( )];
    // ...
};

使用 html-webpack-plugin

/* webpack.config.js */
module.exports = {
    /* ... */ 
    plugins: [
        new HtmlWebpackPlugin()
    ];
    /* ... */ 
};

使用 html-webpack-plugin + DefinePlugin

// webpack.config.js
const path = require('webpack')

module.exports = {
   entry:'./src/index',
   output: {
       filename:'[name].js',
       path: paht.join(__dirname,'./dist')
   },
   plugins:{
       new HtmlWebpackPlugin(),
       new Webpack.DefinePlugin({
           PRODUCTION: JSON.stringify(true),
           VERSION: JSON.stringify('5fa3b9')
       })
   }
}

用起来很简单,但写起来...

首先:插件围绕 钩子 展开

// somePlugin.js
class somePlugin {
    apply(compiler) {
        compiler.hooks.thisCompilation.tap('SomePlugin', (compilation) => {
            
        }
    }
}

钩子的核心信息

  1. 时机:编译过程的特定节点,Webpack 会以钩子形式通知插件此刻正在发生什么事情
  2. 上下文: 通过 tapable 提供的回调机制,以参数方式传递上下文信息
  3. 交互:在上下文参数对象中附带了很多存在side effect 的交互接口,插件可以通过这些接口改变

d065b44d-0f91-47bb-9ab5-56145f2d856a.png

  • 时机:compier.hooks.compilation
  • 参数:compilation
  • 交互:dependencyFactories.set
class EntryPlugin {
  apply(compiler) {
    compiler.hooks.thisCompilation.tap("EntryPlugin", (compilation, { normalModuleFactory }) => {
      compilation.dependencyFactories.set(
        EntryDependency,
        normalModuleFactory
      )
    }
    )

    compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
      const { entry, options, context } = this;
      const dep = EntryPlugin.createDependency(entry, options);
      compilation.addEntry(context, dep, options, (err) => {
        callback(err);
      });
    })
  }
}

思考题:

  • Loader 与插件有什么区同点?
  • “钩子”有什么作用?如何监听钩子函数?
  1. loaderwebpack 拥有了加载和解析非JavaScript文件的能力,loader可以将文件从不同的语言(如TypeScript)转换为JavaScript,或者将内联图像转换为data URL。比如说:CSS-LoaderStyle-Loader等。 Plugin可以扩展webpack的功能,让webpack具有更多的灵活性。 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
  2. hook(钩子)是一种特殊的消息处理机制,它可以监视系统或者进程中的各种事件消息,截获发往目标窗口的消息并进行处理。它允许在某个特定时刻调用一些代码。用来监视系统中特定事件的发生,完成特定功能,如屏幕取词,监视日志,截获键盘、鼠标输入等等。
    为了监听钩子函数,你需要连接到钩子,并定义需要在钩子被触发时执行的代码。

参考:
《Webpack 插件架构深度讲解》
《[万字总结] 一文吃透 Webpack 核心原理》

小结

我们前面学了

  • Webpack 的作用
  • 理解 Webpack 配置结构,学习关键配置项
  • Loader 的作用与常用 Loader
  • 插件基本形态与作用

3bac9639-9844-4ae5-b9eb-a7207a25efa2.png 线上地址

学习方法

  1. 入门应用
  • 理解打包流程
  • 熟练掌握常用配置项、Loader、插件的使用方法,能够灵活搭建集成VueReactBabelEslintLessSass、图片处理等工具的 Webpack 环境
  • 掌握常见脚手架工具的用法,例如: Vue-clicreate-react-app@angular/cli
  1. 进阶
  • 理解 LoaderPlugin 机制,能够自行开发 Webpack组件
  • 理解常见性能优化手段,并能用于解决实际问题
  • 理解前端工程化概念与生态现状
  1. 大师级
  • 阅读源码,理解 Webpack 编译、打包原理,甚至能够参与共建

e18dbcc8-577f-4005-9772-b3b674c9ddad.png