webpack

发布于:2024-05-12 ⋅ 阅读:(262) ⋅ 点赞:(0)

核心

webpack是用来搭建前端工程的
它运行在node环境中,它所做的事情,简单来说,就是打包
在这里插入图片描述
具体来说,就是以某个模块作为入口,根据入口分析出所有模块的依赖关系,然后对各种模块进行合并、压缩,形成最终的打包结果

在webpack的世界中,一切皆是模块

体验

老师提供的工程,以src/main.js作为入口文件

按照习惯,所有的模块均放置在src目录中

  1. 安装依赖
  2. 编写多个模块
    随意编写一些模块,可以是js、图片、音视频,以入口模块为起点,形成依赖关系
  3. 运行npm run build命令,进行打包
  4. 查看打包结果
    打包结果放置在dist目录中

通过上面的体验,可以发现,webpack给我们带来了至少以下好处:

  • 可以大胆的使用任意模块化标准
    无须担心兼容性问题,因为webpack完成打包后,已经没有了任何模块化语句
  • 可以将一些非JS代码也视为模块
    这样可以对css、图片等资源进行更加细粒度的划分
  • 在前端开发中,也可以使用npm
    webpack不会运行你的源代码,无论是你自己写的模块,还是通过npm安装的模块,webpack一视同仁,统统视为依赖,最终合并到打包结果中
  • 非常适合开发单页应用
    单页应用是前端用户体验最好的web应用
    所谓单页应用,是指只有一个html页面,页面中没有任何内容,所有的内容均靠js生成
    要优雅的实现单页应用,最好依托于前端框架,比如vue、react

webpack给我们开发带来的变化远不止于此,接下来一一体验

页面模板

对于单页应用而言,只有一个空白的页面,所有内容都靠JS代码创建
webpack会自动生成一个页面,并且在页面中会自动加入对js和css的引用
它生成页面时,参考的是public/index.html,其称之为页面模板

public目录

webpack会非常暴力的将public目录中的所有文件(除页面模板外),复制到打包结果中

开发服务器

如果每次修改完代码,都要经过打包->运行,未免太过麻烦
在开发阶段,我们可以运行npm run serve命令获得更好的打包体验
该命令会让webpack启动一个开发服务器。
在这个阶段,webpack并不会形成打包结果文件,而是把打包的内容放到内存中,当我们请求服务器时,服务器从内存中给予我们打包结果
与此同时,当源码发生变动时,webpack会自动重新打包,同时刷新页面以访问到最新的打包结果
在这里插入图片描述

文件缓存

可以看到,除了页面外,其他的资源在打包完成后,文件名多了一些奇奇怪怪的字符
例如:js/app-9ea93.js
其中,9ea93这样的字符称之为hash,它会随着模块内容的变化而变化
源码内容不变,hash不变;源码内容变化,hash变化
之所以这样做,是因为生产环境中,浏览器会对除页面外的静态资源进行缓存
如果不设置hash值,一旦代码更新,浏览器还会使用之前缓存的结果,无法使用最新的代码
在这里插入图片描述
有了hash值之后,即可解决此问题
在这里插入图片描述
webpack会在打包时自动处理hash值,并不会对我们写代码造成任何影响,但作为一个前端开发者,有必要了解这一点

资源路径

除代码和样式模块外,其他模块被视为资源模块

值得特别注意的是,资源模块在源代码中的路径和打包后的路径是不一样的,这就导致我们在编写代码的时候,根本无法知晓最终的路径

最常见的例子,就是在css中使用背景图片

.container{
  /* 背景图使用了源码中的路径 */
  backgroud: url('../assets/1.png'); 
}

它能正常工作吗?
它能!

因为webpack非常智能的发现了这一点,对于css中的路径,webpack在打包时,会将其自动转换为打包结果的路径,比如,上面的代码在打包完成后,可能被转换为下面的格式

.container{
  /* css中的资源路径会被自动替换,我们无须关心 */
  background: url(/img/1492ea.png);
}

但如果我们要通过js动态的使用路径,webpack是无法识别的

// 打包前
const url = './assets/1.png'; // 该路径无法被转换
img.src = url;

// 打包后
const url = './assets/1.png'; // ❌
img.src = url;

正确的做法是,通过模块化的方式导入资源,并获取资源路径

// 打包前
import url from './assets/1.png'; // 打包后,url得到的将是真实的路径
img.src = url;

// 打包后
const url = '/img/1492ea.png'; // ✅
img.src = url;

缺省的文件和后缀名

导入模块时,所有js模块均可省略.js,若导入的模块文件名为index.js,可省略文件名

import './home'; // 若存在home.js,可省略js
import './movie'; // 若movie是一个目录,此次导入的是 ./movie/index.js

路径别名

随着体量的增长,不可避免的,会形成层级极深的目录

root
  |- src
    |- a
        |- a1
            |- a2
               |- index.js
    |- b
        |- b1
            |- index.js

如果需要在./src/a/a1/a2/index.js中导入./src/b/b1/index.js,则可能产生下面特别恶心的代码

import '../../../b/b1/index.js';

webpack提供了别名供我们快速定位到./src目录,通常,该别名为@

上面的导入代码可简化为

import '@/b/b1'; // @表示src目录,同时省略了index.js

js兼容性

当webpack读取到js代码时,会自动对其进行兼容性处理

具体的处理方案涉及到两个配置文件:

babel.config.js:通过配置该文件,可以设置对哪些js代码进行降级处理
.browserslistrc:通过配置该文件,可以设置在降级时,要兼容哪些浏览器,兼容的范围越光,降级产生的代码就越多,自然,打包后的体积就越大
你无须知晓具体的配置方式

打包压缩

webpack在打包时,会对所有js和css代码进行压缩

对于js,除了压缩之外,还会对其中的各种名称进行混淆

(self.webpackChunkmovie_list=self.webpackChunkmovie_list||[]).push([[587],{3587:(r,t,n)=>{"use strict";n.r(t),n(5666),n(1539),n(8674),n(9600),n(1249),n(2222);var e=n(9755),a=n.n(e);var o;function i(r){o.html(r.map((function(r){return'<li>\n  <a href="'.concat(r.url,'" target="_blank">\n    <img src="').concat(r.cover,'" title="').concat(r.title,'">\n  </a>\n  <a href="').concat(r.url,'" target="_blank" class="').concat("qmUYQv1xlJhGMQKz-kfAp",'">').concat(r.title,'</a>\n  <p class="').concat("_3yV5wC-URYTUP0sPvaE0ZR",'">').concat(r.rate,"</p>\n  </li>")})).join(""))}o=a()("<ul>").addClass("_1fsrc5VinfYHBXCF1s58qS").appendTo("#app");var c=n(8138);const u=

混淆的作用一方面是为了进一步压缩包体积,另一方面是为了让我们的代码更难被其他人理解利用

源码地图 source map

我们运行的是webpack打包后的结果,而打包后的结果是很难阅读的

但这样一来会带来新的问题,如果代码报错,我们就难以知道到底是那一行代码写的有问题

此时源码地图就发挥了作用

可以发现,js代码打包后都会跟上一个同名的、后缀为.map的文件,该文件就保存了原始代码的内容

请放心,这个内容人类是看不懂的,但浏览器可以看懂

当代码报错时,浏览器会定位到源码地图中的对应代码,而不是把真实报错的代码展示给我们

你无须关心这一点,但可以自然的从其中获得巨大的便利

css工程化

webpack能够识别所有的样式代码,包括css、less、sass、stylus

在打包时,会将它们转换成纯正的css

除此之外,它还具备以下的神奇能力

自动厂商前缀

css有很多兼容性问题,解决这些兼容性问题的最常见办法,就是加上厂商前缀。

比如:

/* 兼容性不好的代码 */
.container{
  display: flex;
  transition: 1s;
}

/* 兼容性好的代码 */
.container {
  display: -webkit-box;
  display: -webkit-flex;
  display: flex;
  -webkit-transition: 1s;
  transition: 1s;
}

webpack会根据.browserlistrc中指定的浏览器范围,按需、自动加上厂商前缀

我们开发无须关心

css module

css文件多了后,你怎么保证它们里面没有冲突的类样式?

靠层级选择器?就不担心效率?

靠命名规范?就不担心脑袋爆炸?

要靠就靠css module

当样式文件以xxx.mdoule.xxx的方式命名时,webpack会将该文件当成一个开启了css module的文件

比如:index.module.less、movie.module.css,都是开启了css module的文件

文件中的所有类名都会被hash化

// 源码

.container{}
.list{}
.item{}

// 打包结果,绝无可能重名
._2GFVidHvoHtfgtrdifua24{}
._1fsrc5VinfYHBXCF1s58qS{}
.urPUKUukdS_UTSuWRI5-5{}

现在就一个问题,我们在使用类名时,如何知道它打包结果的类名呢?

import './index.module.less';
dom.classList.add('container'); // ❌ 最终的类名可不是这个

正确的方式如下:

// styles 是一个对象,里面映射了源码类名和打包类名的关系
import styles from './index.module.less';
dom.classList.add(styles.container); // ✅ 属性container中记录的就是container转换后的类名

真正的webpack没有那么神奇
在这里插入图片描述
webpack通过插件(plugin)和加载器(loader)将这些技术整合在一起

最后,说明一下工程中看不懂的文件:

  • .browserslistrc,表达适配的浏览器范围,会被工程化中的其他技术所使用
  • .babel.config.js,babel的配置文件,做js降级处理
  • .postcss.config.js,postcss的配置文件,做css代码转换
  • .webpack.config.js,webpack的配置文件,整合其他工程化技术,以及配置打包细节、开发服务器、路径别名等等

面试题

1、webpack的过程

整个过程大致分为三个步骤:初始化、编译(最重要)、输出

1、初始化

在初始化这个阶段webpack会将CLI参数、配置文件、默认配置进行融合,形成一个最终的配置对象。

CLI参数:使用命令行工具,可能会加一些参数进去,比如:

npx webpack --mode=development --config xxx

配置文件:webpack.config.js文件里面的配置
默认配置:比如入口entry,默认为 ./src/index.js

对配置的处理过程是依托一个第三方库yargs完成的,yargs库就是融合配置的。初始化阶段相对比较简单,主要是为接下来的编译阶段做必要的准备。目前,可以简单的理解为:初始化阶段主要用于产生一个最终的配置。
为什么编译期间用node编写?因为要读取文件

2、编译

  1. 创建chunk

chunk是webpack在内部构建过程中的一个概念,译为块,它表示通过某个入口找到的所有依赖的统称,比方说:入口模块(./src/index.js)依赖a模块(./src/a.js),a模块又依赖b模块(./src/b.js),通过一个入口模块分析依赖关系,可以找到三个模块,那么index.js、a.js、b.js这三个统称为一个chunk

根据入口模块(默认为./src/index.js)创建一个chunk,每一个chunk是有名字的,意味着chunk有可能也会有多个,入口文件是可以有多个的。

默认情况下只有一个chunk,每个chunk都有至少两个属性:

  • name:默认为main
  • id:唯一编号,如果是开发环境,那么id和name相同,如果是生产环境,则是一个数字,从0开始。
  1. 构建所有依赖模块
    我们先通过下面代码来简单过一遍这个图:
    在这里插入图片描述
//模块名:
./src/index.js (未加载状态)

//模块内容:
console.log("index");
require("./a");
require("./b");

第1步:根据入口模块文件(./src/index.js)进行构建,模块文件它是有一个路径的,入口模块文件路径就是./src/index.js,它会通过这个路径检查当前这个模块是否已经加载过,注意哦:它不是运行模块,而是瞅一眼,看看模块记录表(上图右边蓝色表格)中该模块是不是被加载过,首次检查表格是没有内容的,空的。
第2步:如果说模块记录表中有记录,说明模块已经加载过了。如果没有记录,那么会继续走下一步,说明该模块需要加载
第3步:读取该模块中的内容,内容其实是个字符串

//读取内容(字符串)
console.log("index");
require("./a");
require("./b");

第4步:对模块的内容进行语法分析,树形结构遍历,找到所有依赖,最后生成AST抽象语法树

require("./a");
require("./b");

AST在线测试工具:https://astexplorer.net/

第5步:将分析出来的依赖记录到dependencies数组中,记录完整相对路径

//记录依赖
["./src/a.js","./src/b.js"]

第6步:替换依赖函数,什么意思呢?就是说把有依赖的地方变一种代码格式,将require改为_webpack_require,将依赖的模块改为模块id

console.log("index");
_webpack_require("./src/a.js");
_webpack_require("./src/b.js");//模块id

第7步:我们将替换后的代码称为转换后的模块代码,并且把它保存到模块记录表中
第8步:index.js模块处理完成,由于index.js依赖其它模块,所以递归循环保存在dependencies数组中的依赖,开始分析./src/a.js模块,从头再走一遍这个流程就可以了,假设a模块依赖./src/b.js模块,那么它会等a模块处理完成后,再处理a模块所依赖的b模块,再最后处理index模块所依赖的b模块,此时它会发现b模块在处理a模块所依赖的b模块已经加载过了,那么index模块所依赖的b模块是不会进行下一步处理,直接结束。
以上就是webpack编译过程,做这一切最终的目的就是形成一个模块记录表。

3、产生 chunk assets

在第二步完成后,chunk中会产生一个模块列表,列表中包含了模块id和模块转换后的代码。接下来,webpack会根据配置为chunk生成一个资源列表,即chunk assets,资源列表可以理解为是生成到最终文件的文件名和文件内容。
在这里插入图片描述

chunk hash是根据所有chunk assets的内容生成的一个hash字符串
hash:一种算法,具体有很多分类,特点是将一个任意长度的字符串转换为一个固定长度的字符串,而且可以保证原始内容不变,产生的hash字符串就不变。

4、合并 chunk assets

将多个chunk的assets合并到一起,并产生一个总的hash
在这里插入图片描述

3、输出

「输出 emit」

webpack将利用node中的fs模块(文件处理模块),根据编译产生的总的assets,生成相应的文件。
在这里插入图片描述

「总过程」
在这里插入图片描述

当敲下webpack打包命令之后,文件开始初始化,各个参数进行融合,形成一个最终的配置对象,然后把配置对象交给编译器进行编译, 通过入口模块找到互相依赖模块形成模块列表,接下来webpack会根据配置,为chunk生成一个资源列表,然后将每一个chunk生成的资源合并成一个完整的资源,并且生成一个完整的hash值,最终根据完整的资源列表输出到文件。

涉及术语

module:模块,分割的代码单元,webpack中的模块可以是任何内容的文件,不仅限于JS
chunk:webpack内部构建模块的块,一个chunk中包含多个模块,这些模块是从入口模块通过依赖分析得来的
bundle:chunk构建好模块后会生成chunk的资源清单,清单中的每一项就是一个bundle,可以认为一个bundle就是最终生成的一个文件
hash:最终的资源清单所有内容联合生成的hash值
chunkhash:chunk生成的资源清单内容联合生成的hash值
chunkname:chunk的名称,如果没有配置则使用main
id:通常指chunk的唯一编号,如果在开发环境下构建,和chunkname相同;如果是生产环境下构建,则使用一个从0开始的数字进行编号


网站公告

今日签到

点亮在社区的每一天
去签到