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

Webpack 介绍

一个前端项目是由 CSS 样式文件、图片文件、JS 文件、Vue 文件、TS 文件、JSX 文件等部分组成。咱们能够手动管理这些资源:

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

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

Webpack 本质上是一种前端资源译、打包东西。

  • 多份资源文件打包成一个 Bundle,减少 http 请求数
  • 支撑 Babel、Eslint、TS、CoffeScript、Less、Sass
  • 支撑模块化处理 CSS、图片等资源文件
  • 一致图片、CSS、字体等其它资源的处理模型
  • 支撑 HMR + 开发服务器
  • 支撑继续监听、继续构建
  • 支撑代码分离支撑 Tree-shaking
  • 支撑 SourceMap

Webpack 知识体系简单总结 | 青训营笔记

中心流程:

  1. 进口处理:编译进口,webpack 编译的起点,从 entry 文件开端,启动编译流程。
  2. 依靠解析:从 entry 文件开端,根据 requireimport 等语句找到依靠资源。
  3. 资源解析:根据 module 装备项,调用资源转移器,将图片、CSS 等非标准 JS 资源转译为 JS 内容。webpack 内部一切资源都会以 module 目标方式存在,一切关于资源的操作、转译、兼并都是以 module 为基本单位进行的。
  4. 资源兼并打包:将转译后的资源内容兼并打包为可直接在浏览器运转的 JS 文件。

其间,2、3 步骤会递归调用,直到一切资源处理完毕。

运用

关于 Webpack 的运用办法,基本都环绕装备打开,而这些装备大致可划分为两类:

  • 流程类: 作用于流程中某个 or 若干个环节直接影响打包效果的装备项。
    • 输入: entry、context
    • 模块解析: resolve、externals
    • 模块转译: module
    • 后处理: optimization、mode、target
    • 输出:output
  • 东西类: 主流程之外,供给更多工程化才能的装备项。
    • 开发功率类:watch、devtool、devServer
    • 功用优化类:cache、performance
    • 日志类:stats、infrastructureLogging
  1. 首先,npm i -D webpack webpack-cli 装置。
  2. 定义进口和产品出口。
const path = require("path");
module.exports = {
  entry: "./src/index",
  output: {
    filename:"[name].js",
    path: path.join(__dirname,"./dist"),
  },
}
  1. 装置 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';
  1. 装置 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 = () => {};
  1. 生成 HTML 需求运用的是插件,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>
  1. Hot Module Replacement(HMR) – 模块热替换。

Webpack 知识体系简单总结 | 青训营笔记

Webpack 知识体系简单总结 | 青训营笔记

  • webpack.config.js
const path = require("path");
module.exports = {
  // ...
  watch: true,
  devServer: {
    hot: true,
    open: true
  }
};
  • 指令需求带 serve:npx webpack serve。
  1. Tree-Shaking -树摇,用于删去 Dead Code:
  • 代码没有被用到,不行抵达
  • 代码的履行成果不会被用到
  • 代码只读不写

Webpack 知识体系简单总结 | 青训营笔记

  • webpack.config.js
const path = require("path");
module.exports = {
  // ...
  mode: "production",
  optimization: {
    usedExports: true,
  }
};

对东西类库如 Lodash 有奇效。

Loader

为了处理非标准 JS 资源,设计出资源翻译模块 Loader,最中心的只能是完成内容转化器 —— 将各式各样的资源转化为标准 JavaScript 内容格局,例如:

  • less-loader: 完成 less => css 的转化,输出 css 内容,无法被直接应用在 Webpack 系统下。
  • css-loader:将 css 转化为__WEBPACK_DEFAULT_EXPORT__ = ".a { xxx }"格局。
  • style-loader:将 css 模块包进 require 语句,并在运转时调用 iniectStyle 等函数将内容注入到页面的 linkstyle 标签,并挂载到 html 中,让 css 代码能够正确运转在浏览器上。
  • html-loader:将 html 转化为__WEBPACK_DEFAULT_EXPORT__ = "<!DOCTYPE html"格局。
  • vue-loader:更复杂一些,会将.vue文件转化为多个 JavaScript 函数,别离对应 template、js、css、custom block。

Webpack 知识体系简单总结 | 青训营笔记

  • 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';

如何编写 loader

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

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

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

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

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

//...
exportdefaultfunctionrawLoader(source){
//...
constjson=JSON.stringify(source)
.replace(/\u2028/g,'\u2028')
.replace(/\u2029/g,'\u2029');
constesModule=
typeofoptions.esModule!=='undefined'?options.esModule:true;
return`${esModule?'exportdefault':'module.exports='}${json};`;
}

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

//source
IamTecvan
//output
module.exports="IamTecvan"

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

上例经过return语句回来处理成果,除此之外 Loader 还能够以callback方法回来更多信息,供下流 Loader 或者 Webpack 自身运用,例如在 webpack-contrib/eslint-loader 中:

exportdefaultfunctionloader(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 等等。插件架构灵活性高,扩展性强,可是通常需求非常强的架构才能,需求至少解决三个方面的问题:

  • 接口:需求供给一套逻辑接入办法,让开发者能够将逻辑在特定机遇刺进特定位置
  • 输入:如何将上下文信息高效传导给插件
  • 输出:插件内部经过何种方法影响整套运转系统

针对这些问题,webpack 为开发者供给了根据 tapable 钩子的插件计划:

  1. 编译进程的特定节点以钩子方式,告诉插件此刻正在产生什么事情;
  2. 经过 tapable 供给的回调机制,以参数方法传递上下文信息;
  3. 在上下文参数目标中附带了许多存在 side effect 的交互接口,插件能够经过这些接口改动

与 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 传递给插件的上下文信息,也是插件能拿到的输入。不同钩子会传递不同的上下文目标,这一点在钩子被创立的时候就定下来了。

Webpack 知识体系简单总结 | 青训营笔记

钩子的中心信息:

  • 机遇: 编译进程的特定节点,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