谈谈你对webpack的理解
首先我们需要知道,我们为什么需要webpack这个打包工具。我们前端开发的时候是需要模块化的,不使用模块化开发的项目,依赖管理困难,项目难以维护,代码复用性不高,还可能出现变量冲突的问题。
但是浏览器是不支持模块化的,所以我们需要将模块化的代码打包成能在浏览器上直接运行的代码。
另一方面,我们会在项目开发过程中,用一些新语法和框架的特殊写法(es,ts,vue),浏览器只认识js,而且有些浏览器对es6的支持不全面,所以我们需要打包工具来进行代码转换,语法降级。
模块化打包是webpack最基本的功能,其他功能比如语法降级(将es6转化成es5),将ts,vue的代码编译成浏览器上能执行的代码,都是通过loader和插件实现的。还有其他功能比如文件合并,拆分,压缩,资源处理,也是通过其他loader和插件实现的,
快速开始
下载
1 | npm i webpack webpack-cli -D //下载webpack和调用webpack的命令 |
不下载到全局的原因:
不同项目可能需要使用不同版本的webpack
下载到全局无法被项目中的
package.json
文件记录,分享项目给其他人使用的时候需要额外下载webpack,对于没有前端基础的人可能甚至不知道需要下载webpack。
webpack打包命令
如果全局安装:
1 | webpack |
如果安装到本地
1 | npx webpack #npx会自动在node_modules/.bins目录下查找可执行文件,效果和运行package.json中的脚本一样 |
或者
1 | npx webpack --watch #实时监测文件的变化,变化后保存文件自动打包 |
webpack.config.js
webpack配置文件,在此自定义webpack配置,就不用每次都在命令行中指定配置参数。
这个文件的执行环境是node.js,必须使用cjs语法,使用module.exports
导出配置对象。
常见配置属性
entry
是webpack中的必备配置项,用来指定入口文件,入口文件是依赖分析的起点,一个入口文件对应一个打包后的文件
指定一个入口文件:
1 | entry:'./src/main.js' |
使用对象指定多个入口文件:
1 | entry:{ |
更详细的配置:
1 | entry:{ |
output
是webpack中的必备配置项,用来指定打包后的产物(图片,css,js,html文件)的存储位置和名称(只能指定js文件的名称)
1 | { |
- filename:打包后输出的js文件名
- path:打包后**所有文件(包括js文件,css文件和图片文件)**的存放位置,必须是
绝对路径
,所以使用path.resolve()
来拼接路径,如果最终拼接的不是绝对路径,还会和当前工作目录拼接
,确保结果是一个绝对路径。 - clean:值为布尔值,为true表示每次打包清除之前的打包文件
- publicPath:指定所有文件的公共路径
哈希值
在使用 Vue CLI 构建项目时,可以通过在文件名中配置哈希值来优化缓存。具体的来说,如果没添加哈希值,即便文件内容被修改了,打包后的文件名也不会改变,然后在浏览器中重新加载的时候,就会因为文件名相同使用先前的缓存,导致更新失效。在文件名中使用哈希值,能确保当文件内容发生变化时,浏览器能够识别并下载最新版本的资源,而不是使用旧的缓存。
Vue CLI 基于 Webpack 实现了这一功能,因此可以利用 Webpack 的相关配置,来控制哈希值的生成方式。
以下是几种常见的哈希计算方式:
[hash]:基于某次构建过程中的编译结果,生成唯一一个哈希值,一次打包后的所有文件共用同一个hash值。这意味着,如果构建过程中,有任何文件发生了变化,所有输出文件的哈希值都会改变,这就导致某些可用的缓存失效。从这个意义来说,这个类型的hash值可以看作版本号。
1 | output: { |
**[chunkhash]**:基于整个 chunk 的内容生成哈希,一个 chunk 可能包含多个文件(如 JS + CSS),只要 chunk 内任一文件内容变化,整个 chunk 的 chunkhash 就变
1 | output: { |
当某个入口点下的文件发生更改时,仅该入口点相关的文件哈希值会更新,其他入口点的文件哈希值保持不变。
**[contenthash]**:这是 Vue CLI 推荐的方式,它根据文件内容生成哈希值。对于 CSS 文件,Vue CLI 使用 extract-text-webpack-plugin
或 mini-css-extract-plugin
插件提取样式到单独的文件,并为这些文件生成基于内容的哈希值。
1 | output: { |
这种方法更加精细,只会在文件的实际内容发生变化时才更新其哈希值,从而最大限度地利用浏览器缓存。
mode
定义打包模式(必填)
1 | mode:developmemt||production |
开发模式和打包模式的区别
开发环境:
- 不需要使用文件缓存,所以不需要给文件名额外添加[contenthash]
- 保留devServer
- 删除压缩css,js文件配置
生产环境:
- 需要使用缓存,保留文件额外名[contenthash]
- 删除devServer
- 保留压缩css,js文件配置
- 使用tree-shaking
module
非必须,配置loader的地方
plugins
非必须,配置插件的地方
optimization
非必须,优化相关
resolve
非必须,提供一些简化功能,比如:
路径别名:允许你为常用路径设置别名,避免写长相对路径
1 | // webpack.config.js |
1 | // 以前 |
自动补全扩展名:
1 | resolve: { |
1 | import api from '@/utils/api.js'; |
处理JS
babel-loader
webpack本身只能对js代码打包, 压缩,不能进行ES6到ES5的转换,需要借助babel-loader将新的语法转换成低版本的语法,实现语法降级,提高代码的兼容性,比如对esm语法的转化。
安装:
1 | npm i babel-loader @babel/core @babel/preset-env --save-dev |
babel-loader
是一个 Webpack 加载器,用于在 Webpack 构建过程中使用 Babel 转译 JavaScript 代码。
@babel/core
是 Babel 的核心库,负责执行实际的代码转译工作
@babel/preset-env
是一个智能预设,可以根据目标环境自动选择需要的 Babel 插件,以生成兼容的代码。
配置:
1 | module: { |
balel-loader的详细配置,也就是presets
部分,有时候内容会很多,我们还可以将这部分内容移动到.babelrc
文件中,以json的格式书写
1 | { |
eslint插件
eslint在webpack中是作为一个插件存在的,使用前需要先安装:
1 | # ESLint 核心 |
然后在配置文件中配置:
1 | // webpack.config.js |
eslint的代码规范是人为配置的,这部分的代码通常比较多,我们可以放在.eslintrc.js
文件中,
1 | // .eslintrc.js |
其中最关键的配置项就是rules
,是直接指定代码规范的地方,如果我们不想自己编写规范,也可以使用标准的规范,这就需要我们下载eslint-config-standard
,然后配置在extend中。
处理CSS
如果我们在wepack中直接引入css文件,然后打包,一定会报错,因为webpack本身无法处理css文件。我们需要下载css-loader,以及style-loader或者mini-css-extract-plugin
style-loader
: 把css写入js,代码执行后,把解析后的css样式,放到打包后的html文件的style标签中。
mini-css-extract-plugin
:把css提取为单独的文件,多个css文件会合并为一个单独的css文件,在html-webpack-plugin
插件的作用下,还会自动在html文件中引入(在head标签中)这个css文件。
1 | module:{ |
配置mini-css-extract-plugin
1 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') |
loader是支持链式调用的,顺序从右往左,就拿上面的例子来说,先对css文件使用css-loader,再使用style-loader。
注意:单纯打包css文件不会修改原来的css代码,也不会压缩代码,只是把原来的css代码放到一个文件中。压缩,修改css代码需要借助其他插件。也就是说,单纯借助上述的3个工具,只能做到提取css代码,无法对css代码进行优化。
想要压缩css代码,还需要下载并配置css-minimizer-webpack-plugin
1 | npm i css-minimizer-webpack-plugin -D |
1 | const CssMinimizeWebpackPlugin = require('css-minimizer-webpack-plugin') |
处理其他资源文件
在webpack5中,不需要再使用loader来处理各类资源文件(比如图片,字体),因为自带了对各类资源文件的处理功能(存在资源模块)。当然也可以使用原来的File-loader或者url-loader。
asset/resource
无论资源的大小如何,返回资源打包后的路径,打包后的文件中会包含源文件
1 | module:{ |
用[contenthash]
代替文件名,这意味着会根据文件的内容来确定文件名,如果文件内容改变,这个值也会改变
用[ext]
来代替后缀,表示源文件是什么类型的后缀,打包后的文件也是什么类型的后缀。
图片可以通过import
或者src
的方式被引入
1 | //通常会得到该图片打包后的路径 或 Base64 编码字符串(取决于你的构建配置) |
asset/inline
无论资源的大小如何,返回源文件的data:url
,也就是base64格式字符串,打包后的结果就不会包括源文件了
设你有一张 PNG 格式的图片,通过 data URL
和 Base64 编码的方式内联到 HTML 文件中,它可能看起来像这样:
1 | <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA..." alt="Embedded Image"> |
data:image/png
表示数据的MIME类型,base64
表示数据是否经过了 Base64 编码,如果数据未进行 Base64 编码,则应省略此部分。
这里的 "iVBORw0KGgoAAAANSUhEUgAAAAUA..."
就是该图片经过 Base64 编码后的字符串。
asset
webpack将按照默认条件,自动地在resource
和 inline
之间进行选择,小于4kb
的文件,将会视为inline
模块类型,否则会被视为resource
模块类型。也可以修改这个配置文件。
1 | module:{ |
asset/source
导出资源的源码,这种方式非常适合需要将小文件的内容,直接包含到代码中的场景,比如模板、小型脚本或样式表等。
1 | module.exports = { |
假设有一个名为 example.txt
的文本文件,其内容为 "Hello, Webpack!"
,你可以通过以下方式将其导入到 JavaScript 文件中:
1 | import content from './example.txt'; |
在这种情况下,content
变量将会包含 example.txt
文件的所有文本内容。
注意事项
- 仅限文本文件:
asset/source
最适合用于文本文件。如果你尝试用它来处理二进制文件(如图片、字体),可能会导致不可预料的结果,因为这些文件的二进制数据会被当作字符串处理。 - 文件大小:虽然可以将任何大小的文件作为源代码导入,但通常建议只对较小的文件这样做,以避免增加最终打包文件的体积。
loader
loader
本质是一个函数,用于对文件的源代码进行转换,使之变为webpack可用的模块,在 import
或加载文件时,使用loader预处理文件。
比如我们可以在我们的项目根目录下,编写一个mycss-loader
,大致项目结构如下:
1 | ----- |
在mycss-loader/index.js
中编写如下代码:
1 | // 导出一个函数,source为webpack传递给loader的文件源内容 |
然后在webpack.config.js
中配置:
1 | module:{ |
然后打包后的css文件中,0都会变成10px。
webpack
做的事情,仅仅是分析出各种模块的依赖关系,然后形成资源列表,最终打包生成到指定的文件中。如下图所示:

默认情况下,在遇到import
或者require
加载模块的时候,webpack
只支持对js
和 json
文件打包,像css
、sass
、png
等这些类型的文件的时候,webpack
则无能为力,这时候就需要配置对应的loader
进行文件内容的解析
得益于loader,在webpack
内部中,不仅仅只是js
文件,任何文件都是模块。
loader在webpack.config.js文件中使用的时候通常不需要导入,直接使用即可, Webpack 会根据配置中的名称自动查找并使用相应的加载器,而对于插件,需要导入然后手动创建实例。
处理html
我们需要html做些什么?
- 提供一个html模板复用固定的内容,返回一个html
- 打包的时候自动引入css,js
要实现上述功能,我们就要借助html-webpack-plugin
这个插件,它会生成一个自动引用打包后的文件的html文件,包括mini-css-extract-plugin
生成的css文件,会自动注入打包后的html文件中。
安装:
1 | npm i html-webpack-plugin -D |
在webpack.config.js
中配置:
1 | const HtmlWebpackPlugin = require(' html-webpack-plugin') //引入 |
当我们指定多个入口文件的时候,意在构建多页面应用程序,希望不同的入口文件打包后,能嵌入到不同的html文件中,但是如果我们在配置中只创建了一个HtmlWebpackPlugin实例,那么这些入口文件打包后都会被插入同一个html文件中,为了避免这个效果,**我们可以在配置中创建多个HtmlWebpackPlugin实例,并在chunks属性中指定:”这个html文件要引入哪些入口文件”**。
自定义打包后的html的标题:
- 在配置对象中添加titile属性并赋值
- 在html模板中的title标签内容替换为
<%= htmlWebpackPlugin.options.title %>
代码分割
由于我们是通常是单入口的项目,所有js文件通常会打包成一个js文件,css文件也是如此,这就导致我们页面初次加载的时候,需要花很长时间来加载js和css文件,尽管其中的许多代码并不是立马就会用到(非首屏需要)。
动态导入
1 | import('./math.js')//返回一个promise对象 |
1 | export const add = (x,y)=>{ |
动态导入的文件打包的时候会被自动抽离为一个单独的模块,最终输出为一个单独的文件,使用的时候再被导入(会引发一个额外的请求),即便没有被多次使用,因为是动态导入的,所以不参与模块的静态依赖分析。
魔法注释
1 | import(/* webpackChunkName:'math' */'./lazy.js') //指定动态导入的文件,打包后的模块名(也许不是最后文件名) |
模块抽离
在多入口的项目中,不同的入口文件中可能会引入相同的模块,如果我们不做任何处理,打包后的2个入口文件将包含相同的模块代码,然后浏览器就会重复请求这部分相同的代码。更好的做法是,在得知哪个模块被重复引用的前提下,抽离出该模块,将其单独打包为一个文件。
1 | entry:{ |
除了用上述配置方法,更好的方式是使用插件split-chunks-plugin
,自动抽离重复引用的模块,无需下载,webpack内置。
1 | module.exports = { |
要注意但是,所有重复引用的模块,默认情况下都会打包到同一个文件中去。有些时候,我们需要把某个单独的文件分割出来,更多情况下,无论是单入口还是多入口,我们会把第三方库单独打包成vendor,以及单独打包webpack用来组织模块运行的runtime代码,对此我们需要使用cacheGroup
选项。
1 | module.exports = { |
单独打包webpack用来组织模块运行的runtime代码,则需要使用optimization
中的另一个配置项:runtimeChunks。
1 | { |
开发模式devServer
webpack-dev-server是一个由webpack团队维护的,webpack高度支持的独立的工具,用于在开发过程中提供一个开发服务器, 使用不需要导入,但是需要额外下载。
1 | npm i webpack-dev-server -D |
1 | module.exports = { |
工作原理
使用webpack-dev-server开启一个wepack服务器,底层会使用Express开启一个node服务器,然后调用webpack方法进行打包,再将打包后的结果返回给wepack服务器。当项目中的文件变更了,又会通知wepack服务器重新调用webpack方法进行打包。简单实现如下:
1 | const express =require("express"); |
开启服务器
1 | npx webpack-dev-server |
运行这个命令不仅会启动Webpack的打包过程(打包到内存,不输出实际文件),还会启动一个开发服务器,部署的是打包到内存中的文件。这个服务器会监听源文件(src目录下的js文件)的变化,源文件修改并保存会自动重新编译和刷新浏览器。
值得注意的是,如果修改了wepack配置文件,就需要重启wepack服务器。
热更新
配置hot:true
,即开启热更新,我们要将热更新和强制更新区分开。热更新指的是在不刷新浏览器的前提下更新页面,而强制更新是通过自动刷新页面来更新页面,二者都不需要我们手动刷新页面。区别在于前者能保持当前页面的状态,后者不能。
通常来说更改了除js之外的代码(比如css代码)使用的是热更新,修改js代码使用的是强制更新(无论是否开启热更新)。
proxy
在webpack中开启proxy,就是我们的webpack服务器帮我们发送请求,然后返回响应,简单的来说就是本地的webpack服务器帮助我们代理请求,目的是解决跨域问题。
sourceMap
1 | // webpack.config.js |
配置了sourceMap,开发过程中遇到了错误就能定位到源码的位置,而不是定位到打包后的代码的位置。
static
指定开发环境中的一个静态资源(比如图片,字体,视频)目录的路径,在vue和react中,这个静态资源目录都是public目录,存放不需要经过构建处理的静态资源,构建时直接复制到输出目录(如 dist
)。
plugin
是什么
webpack构建过程中会广播很多事件,plugin可以监听自己感兴趣的事件,在合适的时机通过Webpack
提供的 API
改变最后的打包结果。
插件本质是一个类,插件实例其本质是一个具有apply
方法javascript
对象,apply方法被调用的时候会传入compiler
对象
1 | const pluginName = 'ConsoleLogOnBuildWebpackPlugin'; |
配置插件,就是在webpack构建过程中的某个事件上注册回调函数。
和loader的区别:loader主要负责文件转换,提高webpack的模块化能力,而插件主要负责解决loader功能以外的问题;loader 运行在打包文件之前(因为webpack只能处理js文件,所以需要在打包前处理其他类型的文件),plugins 在整个打包周期都起作用。
webpack-bundle-analyzer
可以帮助开发者可视化和分析 Webpack 打包后的文件大小和内容,它生成一个交互式的报告,显示每个模块的大小及其在最终打包文件中的占比,从而帮助识别和优化代码。
安装:
1 | npm i webpack-bundle-analyzer -D |
配置:
1 | const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer') |
terse-webpack-plugin
用来压缩js代码,但是Webpack 5 内置了对 Terser 的支持,在生产模式下会自动使用 Terser 进行js代码压缩,这一功能已经是webpack核心功能的一部分。但是如果需要自定义 Terser 的选项,仍然需要安装 terser-webpack-plugin
并进行相应的配置。这时,terser-webpack-plugin
是作为一个第三方插件来使用的。
webpack的构建流程
初始化阶段
合并指令中的配置参数和配置文件中的配置参数,得到最终的配置对象options
根据options
对象初始化Compiler
对象:该对象掌控者webpack
生命周期(所有生命周期钩子都在这个对象上)。
初始化插件,注册事件监听:步骤就是执行 new MyPlugin()
并调用插件的 apply
方法,传入 compiler对象。
1 | class Compiler extends Tapable { |
1 | class MyPlugin { |
emit
钩子:是一个特殊的钩子,它在 Webpack 准备好要输出所有资源文件到磁盘之前触发,允许插件作者在这个关键时刻介入处理或修改即将输出的内容。
tap
方法:用来注册一个回调函数到特定的 Webpack 钩子上,使得当这个钩子被触发时,能够执行你的自定义逻辑。
这个方法有2个参数,第一个参数是字符串,用来标识哪些插件,在这个事件上注册了事件监听,第二个参数则是一个回调函数,用来自定义事件触发后执行的逻辑。
编译阶段
创建并初始化好compiler对象后,就会调用compiler.run
方法,创建一个Compilation
对象
compilation
是编译阶段的主要执行者,主要会依次执行下述流程:执行模块创建、依赖收集、分块、打包等主要任务。
当创建了上述的compilation
对象后,就开始从Entry
入口文件开始读取,调用配置的loader
构建模块,构建好模块后,进行依赖分析,发现其他模块,然后才对其他模块递归使用loader构建模块,再进行依赖分析,以此类推,就能构建完所有模块,并构建好模块依赖图,然后进行打包输出。
1 | _addModuleChain(context, dependency, onModule, callback) { |
打包并输出
打包指的就是seal,打包打包的是所有模块,打包的结果是一个或者多个chunk,每个chunk最终都会被输出为单独的文件,模块数目无论如何都是大于chunk数目的。
chunk
webpack
中的chunk
,可以理解为配置在entry
(入口文件) 中的模块,或者是动态引入的模块。每个 chunk 可以包含一个或多个模块,并且可以每个chunk可以被
单独加载
。每个入口点都会创建一个初始 chunk。
import()
动态加载模块时,会创建异步 chunks,它们是按需加载的。开发者可以通过配置,让 Webpack 根据某些规则自动分割代码到不同的 chunks 中。
输出指的就是emit
,在确定好输出内容后,根据配置的输出的路径和文件名,把chunk输出为真实的文件。
使用webpack如何前端性能
通过webpack
优化前端的手段有:
- JS,CSS,Html代码压缩
- 文件大小压缩(区别于代码压缩,会改变文件的后缀)
- 图片压缩
- Tree Shaking
- 代码分离
TreeShaking
Tree Shaking
是一个术语,在计算机中表示消除死代码(一般指的是js代码),基于ES Module
的静态语法分析(不执行任何的代码,可以明确知道模块的依赖关系)
在 Webpack5 中,Tree Shaking 在生产环境下默认启动,这就意味着不需要配置usedExports
,同时还会自动启用代码压缩。
usedExports
用于在 Webpack 编译过程中启动标记功能,使用之后,没被用上的变量/函数(包括未导入的函数/变量和导入后未使用的函数/变量),在webpack
打包中,会被加上unused harmony export
注释,当生成产物时,被标记的变量/函数对应的导出语句会被删除。当然,仅仅删除未被使用的变量/函数
的导出语句
显然是不够看的,若 Webpack 配置启用了代码压缩工具,如 Terser
插件,那么在打包的最后它还会删除所有引用被标记内容
的代码语句,这些语句一般称作 Dead Code
。可以说,真正执行 Tree Shaking 操作的是 Terser 插件。
如下面sum
函数没被用到,webpack
打包会添加注释,terser
在优化时,则将该函数连同引用该函数的代码删除掉。
要注意的是,上述注释只有在开发打包下,开启usedExports,不开启代码压缩,才能看到。

但是,并不是所有 Dead Code 都会被 Terser 删除。
1 | // src/math.js |
在上述代码中,由于square
函数没有被导入,自然也就不会在index.js
中被使用,所以这个函数会被标记,然后由于console.log(square(10))
引用了这个函数,所以它会被标记为Dead Code
,最终在压缩js代码的时候会被删除。
然而实际上,并非如此,打包后的math.js
模块中:square
函数的痕迹被完全清除,但是打印语句仍然被保留,这是因为,这条语句存在副作用。
副作用(side effect) 的定义是,在导入时会执行特殊行为的代码(不是export,而是比如调用函数之类的代码)。
显然,以上示例的 console.log()
语句存在副作用。Terser 在执行 Tree Shaking 时,会保留它认为存在副作用
的代码,而不是将其删除,即便这个代码是Dead code。
PURE注释
作为开发者,如果你非常清楚某条语句会被判别为有副作用,但其实是无害的(删除后无影响),应该被删除,可以使用 /*#__PURE__*/
注释,来向 terser 传递信息,表明这条语句是pure的,没有副作用,terser 可以放心将它删除:
1 | // src/math.js |
然后在打包结果中就不会有console.log
语句
sideEffects
sideEffects
用于告知webpack compiler
哪些模块是有副作用,区别于pure注释
的代码层面,
"sideEffects"
是 package.json
的一个字段,默认值为 true
,即认为所有模块都可能是有副作用的。如果你非常清楚你的 package 是纯粹的,不包含副作用,那么可以简单地将该属性标记为 false
,来告知 webpack 整个包都是没有副作用的,可以安全地删除所有未被使用的代码
(Dead Code),执行比较激进的tree-shaking
;如果你的 package 中有些模块确实有一些副作用,可以改为提供一个数组:
1 | "sideEffects":[ |
更多内容参考:Webpack 5 实践:你不知道的 Tree Shaking本篇文章从 什么是 Tree Shaking、如何使用 T - 掘金
总结一下关键点:
tree-shaking的作用是用来删除未使用的js代码,减小最终代码的体积
tree-shaking基于esm的静态导入,即在编译的时候,就能确定哪些模块的具体哪些功能被使用
生产模式打包,会默认开启tree-shaking,开发模式打包,需要手动开启tree-shaking,即设置usedExports属性为true。
默认情况,tree-shaking会标记模块中未被导入的变量和函数,以及导入了但是没有被使用的变量和函数,然后使用terser压缩js代码的时候,不仅仅会删除这些被标记的的变量和函数,还会删除引用了这些被标记的变量和函数的代码,这些代码被叫做dead code
但是如果某条dead code被认为是有副作用的,则不会被tree-shaking删除,比如console.log
语句
使用webpack搭建vue脚手架
npm包下载
webpack开发必备:
1 | npm i webpack webpack-cli html-webpack-plugin webpack-dev-server -D |
vue相关:
1 | npm i vue-template-compiler vue-loader -D |
vue-template-compiler
用来解析vue模板
vue-loader
用来解析.vue文件
配置vue-loader:
1 | const {VueLoaderPlugin} = require('vue-loader') //vue-loader还包含一个plugin |
css相关:
1 | npm i css-loader mini-css-extract-plugin -D |
sass相关:
1 | npm i sass sass-loader -D |
babel相关:
1 | npm i babel-loader @babel/core @babel/preset-env -D |
生产环境相关:
1 | npm i vue axios vue-router |
注意vue下载的是vue2
模拟vue-cli打包
1 | { |
完整配置文件
1 | const path = require('path') |
RollUp
打包后的代码更简洁,就像源代码一样,不像webpack
那样存在大量引导代码和模块函数,打包效率更高。但是rollup的缺点也很明显,本身只支持打包js,实现其他功能都需要借助插件,而且插件生态也不好。
Vite
特点
特点是开箱即用,0配置:天生支持CSS和各种预处理语言,支持TS,能处理各种资源(比如图片),不需要做任何配置,直接引入即可。
常见配置
1 | // vite.config.js |
其中,server
控制 Vite 开发服务器 的行为,build
控制 Vite 构建打包 的行为, rollupOptions
是 Vite 提供给用户的入口,让你可以在 Vite 的构建流程中,直接控制底层 Rollup 的行为。
与webpack的区别
loader
在vite中没有loader,对于vite无法处理的文件类型,使用插件来解决。具体的来说,使用插件来拦截对模块的请求,然后动态转换为浏览器可识别的格式。
1 | // 插件机制示意 |
启动速度
vite
我们先介绍一下浏览器的原生模块系统。
目前,绝大多数现代浏览器都已经支持ES module
了, 我们只需要在<script>
标签中添加type="module"
,内部就能书写ES module
代码了。
1 | <script type="module"> |
浏览器遇到 import
导入语句时,会自动发起 HTTP 请求,去加载对应的模块文件(如 foo.js
),每个模块可以继续导入其他模块,形成依赖树,所有模块都是 按需异步加载 的,不会阻塞主页面渲染。这就是浏览器原生支持的“模块系统”,不需要打包工具也可以运行。
Vite 利用的是 原生 ES Module 的加载机制 + 请求拦截 + 即时编译,启动开发服务器时不需要打包,请求哪个模块就编译哪个模块,编译成标准的esm。
请求路径 | 服务器如何响应 |
---|---|
/src/main.js | 直接返回文件内容 |
/src/App.vue | 拦截,编译 .vue 文件 → 返回 JS |
/src/style.css | 拦截,编译 CSS → 返回 CSS |
举个例子说明这个过程,假设项目结构如下:
1 | my-vue-app/ |
index.html
文件内容如下
1 |
|
main.js
文件内容如下
1 | import { sayHello } from './utils.js'; |
.utils.js
文件内容如下
1 | export function sayHello() { |
HelloWorld.vue
内容如下
1 | <template> |
当运行 npm run dev 启动 Vite 时发生了什么?
第一步:启动开发服务器:Vite 启动了一个本地开发服务器,默认监听 localhost:5173。它并没有像 Webpack 那样一次性打包所有文件。
第二步:浏览器访问 index.html
,浏览器开始解析 HTML,遇到:
1 | <script type="module" src="/main.js"></script> |
于是发起 HTTP 请求获取 /main.js
,请求的是本地开发服务器,请求的内容是一个模块
第三步:Vite搭建的服务器拦截请求并处理,发现main.js
是一个标准的esm,所以直接返回 main.js
的内容
第四步:浏览器执行 main.js
,由于main.js
中导入了其他模块,所以触发更多 import
,浏览器执行到:
1 | import { sayHello } from './utils.js'; |
这时浏览器会自动发出新的 HTTP 请求:GET /utils.js
,Vite 拦截这个请求,找到 utils.js
文件,直接返回即可(因为它已经是标准 JS,无需编译)。
第五步:加载 .vue
文件:当浏览器执行到:
1 | import HelloWorld from './components/HelloWorld.vue'; |
浏览器会发起请求:GET /components/HelloWorld.vue
,这时问题来了,.vue
文件不是原生 ESM 模块,浏览器无法识别,于是Vite使用插件(比如 @vitejs/plugin-vue
)将 .vue 文件编译成标准的 ESM 模块,然后把编译后的 JS 返回给浏览器。
思考
那如果所有模块都被静态 import 了呢?当模块多的时候会有很多请求,但现代浏览器和 HTTP/2 能很好地处理大量小请求,所以这个缺点可接受。
缓存机制:vite利用浏览器的缓存策略,针对源码模块(我们自己写的代码)做了协商缓存
处理,针对依赖模块(第三方库)做了强缓存
处理,这样我们项目的访问的速度也就更快了。
webpack
而webpack首次启动开发服务器,在分析好模块的依赖关系后,进行一次全量的打包,存储到内存中等待服务器请求,所以webpack开发服务器的启动速度慢。
热更新速度
vite
- 在Vite中某个模块被修改,会被Vite 监听到,然后通过 WebSocket主动通知浏览器:“这个文件变了”
- 浏览器重新发起对该模块的请求(例如
/utils.ts
),Vite 编译该模块后返回新内容 - 浏览器执行新的模块代码并更新页面
整个过程不需要重新打包整个应用,所以vite的热更新极快。
webpack
在webpack中,后续某个模块被修改,重新编译被修改的模块和依赖于它的模块(增量编译,其他未修改的模块使用缓存),然后再进行一次打包。因为需要打包,所以webpack的热更新速度较慢。
打包速度
vite生产阶段使用rollup来打包。rollup打包后的代码非常简洁,完成不像webpack
那样存在大量引导代码和模块函数,所以使用rollup的打包速度更快。
引导代码 = Webpack 自带的“运行时系统”,用于管理模块加载、缓存、依赖解析等。比如__webpack_require__
表示核心模块加载函数,__webpack_modules__
用来存放所有模块的代码,这些代码是 Webpack 能运行的根本,但它们不是你写的业务代码。
模块函数 = 每个模块被包装成的一个函数,由__webpack_require__
调用。
1 | __webpack_modules__["./src/utils.js"] = function(module, exports, __webpack_require__) { |
其他
vite的入口文件是index.html而大部分的打包工具比如webpack的入口文件是一个js文件。