学习Webpack | 青训营笔记

83 阅读8分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 13 天

一、本堂课重点内容:

本堂课的知识要点有哪些?

  • 认识Webpack
  • 使用Webpack
  • 理解Loader
  • 理解插件
  • 学习方法建议

二、详细知识点介绍:

认识Webpack

什么是Webpack

前端由CSS 样式文件、图片文件、JS 文件、Vue 文件、TS 文件、JSX 文件等各种资源构成。

  • 如果资源文件过多,手工操作流程繁琐。
  • 当文件之间有依赖关系时,必须严格按依赖顺序书写。
  • 开发与生产环境需要一致,难以接入 JS 和 TS 的新特性。
  • 比较难接入 Less、Sass 等。
  • JS、图片、CSS 资源管理模型不一致。

为了管理这些资源,出现了许多工程化工具(例如vite、webpack)

2009年诞生的 Node.js 和2010年诞生的 npm 将前端项目带入了工程化,而 Node.js 的 CommonJS 模块化规范不兼容浏览器。所以相继出现了一些打包工具,比如 Browserify、Gulp、RequireJS、Rollup、Webpack 等。

webpack本质上是一种前端资源编译打包工具。

webpack本身默认只对JavaScript文件进行处理,其他类型的文件需要配置loader或者plugins进行处理; 

webpack将多种静态资源js、css、less转换成一个静态文件,减少了页面的请求

  • 多份资源文件打包成一个 Bundle

  • 支持 Babel、Eslint、TS、CoffeScript、Less、Sass

  • 支持模块化处理 css、图片等资源

  • 支持 HMR + 开发服务器

  • 支持持续监听、持续构建

  • 支持代码分离

  • 支持 Tree-shaking

  • 支持 Sourcemap 等

使用Webpack

Webpack打包核心流程

image.png

1.入口处理:从'entry' 文件开始,启动编译流程;

2.依赖解析:从'entry'文件开始,根据'require' or import' 等语句找到依赖资源;

3.资源解析:根据module' 配置,调用资源转移器,将png、css等非标准JS资源转译为JS内容;

4.资源合并打包:将转译后的资源内容0合并,打包为可直接在浏览器运行的JS文件;

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

  • 示例

安装

npm i -D webpack webpack-cli

编辑配置文件webpack.config.js

module.exports = {
    entry: 'main.js',   // 定义当前项目的入口文件
    output: {   // 定义当前项目的输出文件
        filename: "[name].js",
        path: path.join(__dirname, "./dist"),
    },
    module: {// 定义一些loader相关的内容,可在下文看到
        rules: [{
            test: /.less$/i,
            use: ['style-loader', 'css-loader', 'less-loader']
        }]
    }
}

执行编译命令

npx webpack

会在根目录的 dist 文件夹下得到编译出的打包文件 main.js

Webpack 本质上完成的事情:模块化 + 一致性

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

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

流程类:作用于流程中某个or若干个环节,直接影响打包效果的配置项

  • 输入: entry、context
  • 模块解析: resolve、externals
  • 模块转译: module
  • 后处理: optimization、mode、target
  • 输出:output

工具类:主流程之外,提供更多工程化能力的配置项

  • 开发效率类:watch、devtool、devServer
  • 性能优化类:cache、performance
  • 日志类:stats、infrastructureLogging

流程类配置

image.png

使用示例

文件结构:

image.png

1、声明入口entry

module.exports = {
entry: "./src/index"
};

2、声明产物出口output

const path = require("path");
module.exports = {
  entry: "./src/index",
  output: {
    filename : "[name].js",
    path: path.join(__dirname, "./dist"),
  }
};

3、运行

npx webpack

安装 loader 处理 CSS,npm add -D css-loader style-loader

  • webpack.config.js
const path = require("path");
module.exports = {
  entry:"./src/index",
  output: {
    filename: "[name].js",
    path: path.join(__dirname, "./dist"),
  },
  module: {
    // css 处理器
    rules: [{
      test: /.css/i,
      use: [
        "style-loader",
        "css-loader",
      ]
    }],
  },
};
  • index.js
const styles = require('./index.css');
// or
import styles from './index.css';

安装 loader 接入 Babel,npm i -D @babel/core ababel/preset-env babel-loader

  • webpack.config.js
const path = require("path");
module.exports = {
  entry:"./src/index",
  output: {
    filename: "[name].js",
    path: path.join(__dirname, "./dist"),
  },
  module: {
    // Babel 处理器
    rules: [{
      test: /.js?$/,
      use: [{
        loader: 'babel-loader',
        options: {
          presets: [
            ['@babel/preset-env']
          ]
        }
      }, ]
    }],
  },
};
  • index.js
class Person {
  constructor() {
    this.name = 'Tecvan';
  }
}
console.log((new Person()).name);
const say = () => {};

使用Webpack生成html

  1. 安装依赖
  2. 配置
  3. 执行npx webpack

需要使用的是插件,npm i -D html-webpack-plugin

  • webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
  entry:"./src/index",
  output: {
    filename: "[name].js",
    path: path.join(__dirname, "./dist"),
  },
  plugins: [new HtmlWebpackplugin()]
};
  • index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<script defer src="main.js"></script>
</head>
<body>
</body>
</html>

使用Webpack——HMR

Hot Module Replacement——模块热替换(写的代码会被立刻更新到浏览器上~)

image.png

image.png

  1. 开启HMR
  2. 启动webpack
  • webpack.config.js
const path = require("path");
module.exports = {
  // ...
  watch: true,
  devServer: {
    hot: true,
    open: true
  }
};

使用Webpack——Tree-Shaking

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

无用代码消除在广泛存在于传统的编程语言编译器中,编译器可以判断出某些代码根本不影响输出,然后消除这些代码,这个称之为DCE(dead code elimination)

在 webpack 项目中,有一个入口文件,相当于一棵树的主干,入口文件有很多依赖的模块,相当于树枝。实际情况中,虽然依赖了某个模块,但其实只使用其中的某些功能。通过 Tree-Shaking,将没有使用的模块摇掉,这样来达到删除无用代码的目的。

image.png

Dead Code

  • 代码没有被用到,不可到达
  • 代码的执行结果不会被用到
  • 代码只读不写

Tree-shaking

模块导出来了,但未被其他模块使用

开启Tree-shaking:

module.exports = { 
  entry:"./src/ index",
  mode: "production",
  devtool: false,
  optimization: {
  usedExports: true,
 },
}

理解Loader

Loader核心功能:将非JS资源转换为JS资源

  1. 安装Loader
  2. 添加module处理less文件

认识Loader:

less-loader: 实现less => css的转换

css-loader: 实现css => js的转换,将CSS包装成类似module.exports = "${css}"的内容,包装后的内容符合JavaScript 语法

style-loader:将css模块包进require语句,并在运行时调用injectStyle等函数将内容注入到页面的style标签

html-loader:将 html 转换为 __WEBPACK_DEFAULT_EXPORT__ = "<!DOCTYPE html" 格式。

vue-loader:更复杂一些,会将 .vue 文件转化为多个 JavaScript 函数,分别对应 template、js、css、custom block。

image.png

  • webpack.config.js
const path = require("path");
module.exports = {
  entry:"./src/index",
  output: {
    filename: "[name].js",
    path: path.join(__dirname, "./dist"),
  },
  module: {
    // css 处理器
    rules: [{
      test: /.less/i,
      use: [
        "style-loader",
        "css-loader",
        "less-loader",
      ]
    }],
  },
};
  • index.js
import styles from './a.less';

特性:

  • 链式执行
  • 支持异步执行
  • 分normal、pitch两种模式

编写Loader

Loader 通常是一个函数,结构如下:

module.exports = function(source, sourceMap?, data?) {
  // source 为 loader 的输入,可能是文件内容,也可能是上一个 loader 处理结果
  return source;
};

Loader 函数接收三个参数,分别为:

  • source:资源输入,对于第一个执行的 loader 为资源文件的内容;后续执行的 loader 则为前一个 loader 的执行结果。
  • sourceMap: 可选参数,代码的 sourcemap 结构。
  • data: 可选参数,其它需要在 Loader 链中传递的信息,比如 posthtml/posthtml-loader 就会通过这个参数传递参数的 AST 对象。

其中 source 是最重要的参数,大多数 Loader 要做的事情就是将 source 转译为另一种形式的 output ,比如 webpack-contrib/raw-loader 的核心源码:

//... 
export default function rawLoader(source) {
  // ...
  const json = JSON.stringify(source)
    .replace(/\u2028/g'\u2028')
    .replace(/\u2029/g'\u2029');
  const esModule =
    typeof options.esModule !== 'undefined' ? options.esModule : true;
  return `${esModule ? 'export default' : 'module.exports ='} ${json};`;
}

这段代码的作用是将文本内容包裹成 JavaScript 模块,例如:

// source
I am Amy
// output
module.exports = "I am Amy"

经过模块化包装之后,这段文本内容转身变成 Webpack 可以处理的资源模块,其它 module 也就能引用、使用它了。

上例通过 return 语句返回处理结果,除此之外 Loader 还可以以 callback 方式返回更多信息,供下游 Loader 或者 Webpack 本身使用,例如在 webpack-contrib/eslint-loader 中:

export default function loader(content, map) {
  // ...
  linter.printOutput(linter.lint(content));
  this.callback(null, content, map);
}

通过 this.callback(null, content, map) 语句同时返回转译后的内容与 sourcemap 内容。callback 的完整签名如下:

this.callback(
  // 异常信息,Loader 正常运行时传递 null 值即可
  err: Error | null,
  // 转译结果
  content: string | Buffer,
  // 源码的 sourcemap 信息
  sourceMap ? : SourceMap,
  // 任意需要在 Loader 间传递的值
  // 经常用来传递 ast 对象,避免重复解析
  data ? : any
);

理解插件

前端社区里很多有名的框架都各自有一套插件架构,例如 axios、quill、vscode、webpack、vue、rollup 等等。插件架构灵活性高,扩展性强,但是通常需要非常强的架构能力,需要至少解决三个方面的问题:

  • 接口:需要提供一套逻辑接入方法,让开发者能够将逻辑在特定时机插入特定位置
  • 输入:如何将上下文信息高效传导给插件
  • 输出:插件内部通过何种方式影响整套运行体系

image.png

针对这些问题,webpack 为开发者提供了基于 tapable 钩子的插件方案:

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

区别Loader

Loader 和 插件 都是 Webpack 的扩展机制。

  • Loader 是一个函数,负责代码的转换、编译。在 webpack 读取模块内容之后,生成 AST 语法树之前进行。操作的是文件,比如将 A.scss 转换为 A.css,是单纯的文件转换过程。
  • 插件是一个类,利用 webpack 提供的 hooks,当什么时,执行什么。可以在 webpack 整个打包过程中进行。功能更强,能够在各个对象的钩子中插入特化处理逻辑,它可以覆盖 Webpack 全生命流程,能力、灵活性、复杂度都会比 Loader 强很多。甚至,Webpack 本身的很多功能也是基于插件实现的。不直接操作文件,而是基于事件机制工作,会监听 webpack 打包过程中的某些事件钩子,执行任务。通过 plugin 可以访问 compliler 和 compilation 过程,通过钩子拦截 webpack 的执行。

插件架构精髓:对扩展开放,对修改封闭,其实就是开闭原则

  • 使用 html-webpack-plugin + DefinePlugin
const path = require("path");
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
  entry:"./src/index",
  output: {
    filename: "[name].js",
    path: path.join(__dirname, "./dist"),
  },
  plugins: [
    new HtmlWebpackplugin(),
    new webpack.DefinePlugin({
      PRODUCTION: JSON.stringify(true),
      VERSION: JSON.stringify('5fa3b9')
    }
  ]
};

编写插件

  1. Webpack 的插件体系是一种基于 Tapable 实现的强耦合架构。
  2. 它在特定时机触发钩子时会附带上足够的上下文信息,插件定义的钩子回调中,能也只能与这些上下文背后的数据结构、接口交互产生 side effect(副作用),进而影响到编译状态和后续流程。

从形态上看,插件通常是一个带有 apply 函数的类:

class SomePlugin {
  apply(compiler) {}
}

Webpack 会在启动后按照注册的顺序逐次调用插件对象的 apply 函数,同时传入编译器对象 compiler ,插件开发者可以以此为起点触达到 webpack 内部定义的任意钩子,例如:

class SomePlugin {
  apply(compiler) {
    compiler.hooks.thisCompilation.tap('SomePlugin', (compilation, params) => {
    })
  }
}
  • thisCompilation 为 tapable 仓库提供的钩子对象。

  • tap 为订阅函数,用于注册回调。

  • compilationparams 参数是 webpack 传递给插件的上下文信息,也是插件能拿到的输入。不同钩子会传递不同的上下文对象,这一点在钩子被创建的时候就定下来了。

钩子的核心信息:

image.png

  • 时机: 编译过程的特定节点,Webpack 会以钩子形式通知插件此刻正在发生什么事情。
  • 上下文: 通过 tapable 提供的回调机制,以参数方式传递上下文信息。
  • 交互: 在上下文参数对象中附带了很多存在副作用的交互接口,插件可以通过这些接口改变。
class EntryPlugin {
  apply(compiler) {
    compiler.hooks.compilation.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);
      });
    });
  }
}
  • 时机:compier.hooks.compilation
  • 参数:compilationcallback 等。
  • 交互:dependencyFactories.set

学习方法建议

Webpack知识体系图

入门应用

理解打包流程 熟练掌握常用配置项、Loader、插件的使用方法,灵活搭建集成Vue、React、Babel、Eslint、Less、Sass、图片处理工具等webapck环境 掌握常见脚手架工具的用法

进阶

理解Loader、Plugin机制,自行开发Webpack组件 理解常见性能优化手段,可解决实际问题 理解前端工程化概念与生态现状

Master

阅读源码,理解Webpack编译、打包原理,参与共建

image.png

三、引用参考: