webpack学习笔记

98 阅读6分钟

基本概念

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

重要概念:

  • 入口(entry)
  • 输出(output)
  • Loader
  • 插件(plugin)
  • 模式(mode)
  • 浏览器兼容性(browser compatibility)
  • 环境(environment)

entry

入口起点(entry point)代表webpack将要使用哪个模块,来作为构建起内部依赖图的起点。在进入入口起点之后,webpack会自动找出那些直接或者间接依赖入口起点的模块。

默认的入口起点为:./src/index.js,可以在webpack configuration中自定义入口起点。

output

output属性会告诉webpack把打包后的bundle存储在哪个位置,以及重命名bundle的文件名。

主要输出文件的默认值是:./dist/main.js,其他生成文件默认放置在./dist文件夹中。

配置相关属性:

    const path = require('path');

    module.exports = {
      entry: './path/to/my/entry/file.js',
      output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'my-first-webpack.bundle.js',
      },
    };

loader

原始的webpack只能处理JavaScript和JSON文件,而loader让webpack可以去处理其它类型的文件,并将他们转换为有效的模块,并添加到依赖图中。

允许webpack去处理非JavaScript文件:css/img,类似于“翻译官”

在webpack的配置中,在modulerules下为loader设置相关属性,有两个属性:

  • test属性:用来识别出哪些文件需要被转换,值通常为判断文件类型的正则表达式
  • use属性:定义在进行文件转换时,webpack将使用哪个loader

通俗语义:webpack在对.xxx文件进行打包时,先使用use规定的loader转换一下,再去打包。

注意:在webpack配置中定义rules时,要在module.rules下进行配置,而不是在rules下进行配置

plugin

loader用于转换某些类型的模块,而plugin则可以用于执行范围更广的任务,包括:打包优化,资源管理,注入环境变量。

允许webpack执行范围更广的任务,允许webpack“开飞机开大炮”

webpack.config.js中,想要使用一个插件,只需要require()他,然后把它添加到plugins数组中即可。

注意:如果需要在一个配置文件中多次使用同一个插件,则需要使用new操作符来创建一个新的插件实例。

例子:

    const HtmlWebpackPlugin = require('html-webpack-plugin');
    const webpack = require('webpack'); // 用于访问内置插件
    
    module.exports = {
      module: {
        rules: [{ test: /\.txt$/, use: 'raw-loader' }],
      },
      plugins: [new HtmlWebpackPlugin({ template: './src/index.html' })],
    };

模式

通过选择developmentproductionnone中的一个,来设置mode参数。之后,webpack会启动不同环境下的内置优化

浏览器兼容性

webpack默认支持所有符合ES5标准的浏览器,不支持IE8及以下版本。

webpack的import()require.ensure()需要Promise

如果想要支持旧版本浏览器,在使用这些表达式之前,还需要提前加载polyfill

运行环境

Webpack5运行于Node.js v10.13.0+的版本

详细配置

entry points

用法: entry: string | string[] | {}

多种写法:

  • 简写:entry:'./path/proj/file.js'
  • 详细写法:
    entry:{
        main: './path/proj/file.js'
    }
  • 传递文件路径数组,创建所谓的"multi-main entry",在一次注入多个依赖文件时,并且将他们的依赖关系绘制在一个"chunk"中时,可以使用这种方法:
    entry:['./src/file_1.js', './src/file_2.js']
  • 对象语法:
    entry: {
        app: './src/app.js',
        adminApp: './src/adminApp.js'
    }

用于描述入口的对象,包含如下属性:

  1. dependOn:当前入口所依赖的入口,必须在当前入口被加载前加载完毕
  2. filename:指定要输出的文件名称
  3. import:启动时需加载的模块
  4. library:指定library选项,为当前entry构建一个library
  5. runtime:运行时chunk的名字。如果设置了,就会创建一个新的运行时 chunk。在 webpack 5.43.0 之后可将其设为false可以避免创建一个新的运行时 chunk
  6. publicPath:当该入口的输出文件在浏览器中被引用时,为他们制定一个公共URL地址

例:

    entry: {
        a2: 'dependingfile.js',
        b2: {
          dependOn: 'a2',
          import: './src/app.js',
        },
    },
  • 分离app(应用程序)和vendor(第三方库)入口:
  entry: {
    main: './src/app.js',
    vendor: './src/vendor.js',
  },

这样子可以让webpack配置两个单独的入口点,这样就可以在vendor.js中存入未做修改但是必要的library或其他文件,例如Bootstrap,jQuery,图片等,最后将他们打包在一起成为单独的chunk。这样内容哈希保持不变,可以使浏览器独立地缓存它们,减少加载时间。

  • 多页面程序:
    entry: {
        pageOne: './src/pageOne/index.js',
        pageTwo: './src/pageTwo/index.js',
        pageThree: './src/pageThree/index.js',
    },

这样告诉webpack需要三个独立分离的依赖图。增多入口起点数量,可以使得多页应用能够复用多个入口起点之间的大量代码/模块,从而提升效率。

output

  • 常见写法:
    module.exports = {
      output: {
        filename: 'bundle.js',
      },
    };
  • 当有多个entry文件时:需要使用占位符来确保每个文件具有唯一的名称
    module.exports = {
      entry: {
        app: './src/app.js',
        search: './src/search.js',
      },
      output: {
        filename: '[name].js',
        path: __dirname + '/dist',
      },
    };
    
    // 写入到硬盘:./dist/app.js, ./dist/search.js

loader

loader用于对模块的源代码进行转换,可以在importload(加载)模块时预处理文件。loader可以帮忙处理各种类型的文件,甚至允许项目在JavaScript模块中importCSS文件。

使用loader的两种方式:

  • webpack.config.js中直接配置(推荐)
    module.exports = {
      module: {
        rules: [
          { test: /\.css$/, use: 'css-loader' },
          { test: /\.ts$/, use: 'ts-loader' },
        ],
      },
    };
  • 在单个文件中的import语句中内联使用(不推荐)
  import Styles from 'style-loader!css-loader?modules!./styles.css';
  //使用 ! 来将不同的loader分开

loader常见特性:

  • loader是从右到左,或从下到上执行的
  • loader支持链式调用,当进行链式调用的时候,执行顺序是相反的,即:链中的第一个loader将其结果传递给下一个loader,以此类推,链中的最后一个loader来返回webpack所期望的JavaScript
  • loader可以同步也可以异步
  • loader运行在Node.js中
  • plugin(插件)可以为loader带来更多特性

plugin

plugin是webpack的支柱功能,webpack自身也是构建在webpack配置中用到的相同的插件系统上。

plugin存在的目的在于解决loader无法实现的其他的功能。

底层剖析

plugin实际上是一个具有apply方法的JavaScript对象。apply方法会被webpack compiler调用,并且在整个生命周期都可以访问compiler对象。

ConsoleLogOnBuildWebpackPlugin.js

    const pluginName = 'ConsoleLogOnBuildWebpackPlugin';
    
    class ConsoleLogOnBuildWebpackPlugin {
      apply(compiler) {
        compiler.hooks.run.tap(pluginName, (compilation) => {
          console.log('webpack 构建正在启动!');
        });
      }
    }
    
    module.exports = ConsoleLogOnBuildWebpackPlugin;

在这其中compiler hook的tap函数中的第一个参数为将要调用的plugin的名称,名称应该遵循驼峰命名。建议为此使用一个常量,以便它可以在所有hook中重复使用。

常见用法

  • webpack.config.js配置: (因为可以给plugin传参,所以需要传入一个new实例)
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    const webpack = require('webpack'); // 访问内置的插件
    
    module.exports = {
      ...,
      plugins: [
        new webpack.ProgressPlugin(),
        new HtmlWebpackPlugin({ template: './src/index.html' }),
      ],
    };
  • Node API方式:
    const webpack = require('webpack') // 访问 webpack 运行时(runtime)
    const configuration = require('./webpack.config.js');
    
    let compiler = webpack(configuration);
    
    new webpack.ProgressPlugin().apply(compiler);//为webpack中的插件调用apply方法
    
    compiler.run(function(err, stats){
        //...
    })

Configuration

webpack的配置文件是JavaScript文件,文件内导出了一个webpack配置对象

可以直接编写并导出一个配置模块:

    const path = require('path');
    
    module.exports = {
      mode: 'development',
      entry: './foo.js',
      output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'foo.bundle.js',
      },
    };

模块

模块化编程中,开发者将程序分解为功能离散的chunk,并称之为模块

每个模块都拥有小于完整程序的体积,使得验证、调试及测试都变得轻而易举。精心编写的模块提供了可靠的抽象和封装界限,使得应用程序中每个模块都具备了条理清晰的设计和明确的目的。

webpack天生支持如下模块类型:

  • ECMASript模块
  • CommonJS模块
  • AMD模块
  • Assets
  • WebAssembly模块

还为以下类型原生创建了对应的loader:

  • CoffeeScript
  • TypeScript
  • ESNext(Babel)
  • Sass
  • Less
  • Stylus
  • Elm

runtime & manifest

webpack的runtimemanifest,共同管理所有模块的交互行为。

runtime

runtime,以及伴随的manifest数据,主要是指:在浏览器运行过程中,webpack用来连接模块化应用程序所需的所有代码。它包含:在模块交互时,连接模块所需的加载和解析逻辑,以及:已经加载到浏览器中的连接模块逻辑,和尚未加载模块的延迟加载逻辑。

manifest

一旦应用程序被打包,并发送给浏览器,然后浏览器以index.html的形式运行应用程序,项目原先的/src等目录结构就已经不复存在。而manifest就是用来去管理新目录结构下的所需模块之间的交互活动。

当compiler开始执行、解析和映射应用程序时,它会保留所有模块的详细要点,这个数据集合即为manifest。当项目被打包并发送到浏览器的时候,runtime会通过manifest来解析和加载模块。因为原先的importrequire语句现在都已经被转换为__webpack_requie__方法,而此方法指向模块标识符(module identifier)。manifest中包含了各种所需模块的标识符,这样runtime就可以检索这些标识符,然后找到每个标识符背后对应的模块。

理解manifest的作用有助于正确利用浏览器缓存来提升项目性能。在通过内容散列(content hash)来作为bundle文件的名称,这样在文件内容修改时,会计算出新的hash值,浏览器会使用新的名称来加载文件,从而使得缓存无效。除此之外,即使内容没有变化,某些hash值仍然会改变,这是因为注入的runtime和manifest在每次构建之后都会发生变化。所以想要利用好浏览器缓存,需要小心利用manifest。