读书/技术/webpack实战:入门、进阶与调优

52 阅读3分钟

webpack介绍

Webpack是一个模块打包工具,其最核心的功能是解决模块之间的依赖,把各个模块按照特定的规则和顺序组织在一起,最终合并为一个JS文件(有时会有多个,这里讨论的只是最基本的情况)

为何需要webpack

原因

当网页内容越来越丰富,网页中则需要引入多个js文件,但是如果没有整合会有如下问题。

  • 不清楚多个scirpt之间的依赖关系,需要手动维护JavaScript的加载顺序。
  • 在每个script标签中,顶层作用域即全局作用域,没有任何处理而直接在代码中进行变量或函数声明会污染全局作用域
  • 每一个script标签都意味着需要向服务器请求一次静态资源,在HTTP 2还没有出现的时期,建立连接的成本是很高的,过多的请求会严重拖慢网页的渲染速度。

到了2015年,现代浏览器终于开始支持了模块化,也就是es6。但在实际应用方面还有问题,如下:

  • 无法使用代码分片与删除死代码
  • 社区里的npm包好多都是commonjs的模式,浏览器不支持,也就无法原生导入使用了。
  • 现代浏览器才支持,要考虑兼容性问题。

解决

使用模块打包工具,解决不同类型模块间的依赖关系,并将它们整合成一个能够在浏览器正常运行的js文件。

webpack的优势

  • Webpack默认支持多种模块标准,包括AMD、CommonJS以及最新的ES6模块,而其他工具大多只能支持一到两种。
  • Webpack有完备的代码分片解决方案。
  • Webpack可以处理各种类型的资源。Webpack还可以处理样式、模板,甚至图片等
  • Webpack拥有庞大的社区支持。

模块打包

模块的作用

每个文件就是一个模块,每个模块拥有各自的作用域的。外界想要访问模块,只能访问模块的导出对象。

commonJs

// 导出方式
exports.a = 'a'
module exports = {
 a:'a'
}
// 导入方式
let module =  reuqire('commonJs.js')
module.a
// 注意
当我们使用require导入一个模块时会有两种情况:
该模块未曾被加载过。这时会首先执行该模块,然后获取到该模块最终导出的内容。
该模块已经被加载过。这时该模块的代码不会再次执行,而是直接获取该模块上一次导出的内容

es6

// 导出方式
export const a = 'a'
export default {
    a:'a'
}
// 导入方式
import module,{ a } from "es6.js"
module.a

commonJs与es6的区别

  1. commonJs的模块依赖是动态的,也就是说可以通过表达式引入模块,es6的模块依赖是静态的,也就是说模块的导入是写死的,不能用if来条件导入。
  2. commonJs是对导出的模块的值copy,es6是对导出的模块的值引用。

amd

// 导出
define('getSum', ['calculator'], function(math) 
{ 
    return function(a, b) { 
        console.log('sum: ' + calculator.add(a, b)); 
    } 
});
// 导入
require(['getSum'], function(getSum) { getSum(2, 3); });
require的第1个参数指定了加载的模块,第2个参数是当加载完成后执行的回调函数。

umd

使一个模块能运行在各种环境下,不论是CommonJS、AMD,还是非模块化的环境(当时ES6Module还未被提出)。

// calculator.js
(function (global, main) {
    // 根据当前环境采取不同的导出方式
    if (typeof define === 'function' && define.amd) {
        // AMD
        define(...);
    } else if (typeof exports === 'object') {
        // CommonJS
        module.exports = ...;
    } else {
        // 非模块化环境
        global.add = ...;
    }
}(this, function () {
    // 定义模块主体
    return {...}
}));

UMD其实就是根据当前全局对象中的值判断目前处于哪种模块环境。当前环境是AMD,就以AMD的形式导出;当前环境是CommonJS,就以CommonJS的形式导出。

在浏览器上,你可以通过

资源的输入和输出

entry配置

存在依赖关系的模块会在打包时被封装为一个chunk。

context可以理解为资源入口的路径前缀,配置context的主要目的是让entry的编写更加简洁,尤其是在多入口的情况下。context可以省略,默认值为当前工程的根目录

第三方模块单独打包,例如将react和react-dom打包进了vendor,之后再配置optimization.splitChunks,将它们从各个页面中提取出来,生成单独的bundle即可。

webpack配置文档

output配置

filename中的模版变量 image.png

path用来指定资源的输出位置,publicPath则用来指定资源的请求位置

webpack配置文档

loader

基础知识

每个loader本质上都是一个函数,可以理解为一个代码转换的工具,将其转换成js文件。

loader可以是链式的。我们可以对一种资源设置多个loader,第一个loader的输入是文件源码,之后所有loader的输入都为上一个loader的输出。

举例:Style标签 = style-loader(css-loader(sass-loader(SCSS)))

配置loader中,当exclude和include同时存在时,exclude的优先级更高

webpack配置文档

自定义loader

// force-strict-loader/index.js
var loaderUtils = require("loader-utils");
module.exports = function(content) {
    // 启用缓存
    if (this.cacheable) {
        this.cacheable();
    }
    // 获取和打印 options
    var options = loaderUtils.getOptions(this) || {};
    console.log('options', options);
    // 处理 content
    var useStrictPrefix = '\'use strict\';\n\n';
    return useStrictPrefix + content;
}

本地开发devServer

作用

内置一个服务器,方便程序员在本地进行开发。

代码分片

指的是将Web应用中那些不常变动的库和工具单独拆成文件。

意义:

  • 开发过程中减少了重复模块打包,可以提升开发速度;
  • 减小整体资源体积,合理分片后的代码可以更有效地利用客户端缓存

webpack4的分片方法

CommonsChunkPlugin(不行了,被替换掉了。这也暴露了一个问题,这种api只需要知道作用,详细的配置通过文档查询即可。)

optimization.SplitChunks替代了CommonsChunkPlugin

// 默认配置信息
splitChunks:
    {
    chunks: "async",    // 分别为async(默认)、initial和all。async即只提取异步chunk,initial只对入口chunk生效(如果配置了initial则上面异步的例子将失效),all则同时开启两种模式。
    minSize: 20000,     // 设置提取js的体积大于20kb
    minRemainingSize: 0,
    minChunks: 1,       // 提取的chunks数量大于等于1
    maxAsyncRequests: 30, // 按需加载,并行请求的资源数最大值小于等于30
    maxInitialRequests: 30,   // 首次加载,并行请求的资源数最大值小于等于30
    enforceSizeThreshold:50000, // 设置提取的css的体积大于50kb
    cacheGroups:{ 
        vendors:{   // 提取node_modules
            test: /[\\/]node_modules[\\/]/,
            priority: -10,
        },
        default: {  // 提取多次被引用的模块
            minChunks: 2,
            priority: -20,
            reuseExistingChunk: true,
        },
    },
},

webpack打包机制

image.png Resolver

Resolver找到这个文件后,将会返回一个对象,里面包含了
源代码中的引用路径、
最后实际找到的文件路径
以及其上下文等

Module Factory

它接收的是Resolver提供的对象,返回的则是一个模块对象,
这个模块对象中将会包含源代码,

Paresr

让不同类型的源代码经过loader的处理后变成js,再将js变成ast,并进一步寻找文件依赖。

模板渲染

把这些模块的ast变成jsStr。

plugin

自己的笔记

更多打包工具简单介绍

RollUp

如果当前的项目需求仅仅是打包JavaScript,比如一个JavaScript库,那么Rollup很多时候会是我们的第一选择。

esBuild

号称可以比之前的打包工具如Rollup、Webpack要快上10到100倍。

这么快的原因:

  • esbuild是使用Go编写的,具有多线程+共享内存

缺陷:

  • 社区生态不够完善
  • 支持文件类型有限

vite

一个更贴近实际Web应用的解决方案。

在开发模式下,Vite使用esbuild来进行最快速的本地构建。

生产模式下,Vite借助Rollup来打包和输出最后的资源。

vite本地构建那么快的原因: 使用了浏览器支持es6的特性,它js代码的导入导出由浏览器自己去找相关依赖,不再需要进行打包整合成chunk。

举例:

// 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>
</head>
<body>
</body>
<script type="module" src="./a.js" ></script>
</html>

// a.js
import { getB } from "./b.js"
getB()
export let a = 100

// b.js
export function getB(){
    console.log('bbb')
}

运行代码后,发现是可以正常打印出bbb的,可见这个依赖过程全部由现代浏览器自己去完成。

资料来源

webpack实战:入门、进阶与调优