# vite_demo **Repository Path**: mahoushaojo/vite_demo ## Basic Information - **Project Name**: vite_demo - **Description**: 基于Vue3+Typescript+Vite搭建,集成了Eslint+Prettier+Stylelint来实现自动格式化与修复, 使用Husky + lint-staged 的 Git 提交工作流集成,自定义commit-msg来规范Git 提交信息。 使用了ViteImagemin来对静态资源图片进行压缩。使用pnpm来进行包管理。 - **Primary Language**: JavaScript - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2022-06-24 - **Last Updated**: 2025-03-27 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Vite 实践 [TOC] ## 第一阶段 ### 1) 无模块化标准阶段 在还没有出现模块化标准以前,前端已经有了一些模块的手段,主要为**文件划分**`、`**命名空间**`和 `**IIFE 私有作用域** #### 1: 文件划分 ```js // module-a.js let data = "data"; // module-b.js function method() { console.log("execute method"); } // index.html Document ``` 从中可以看到`module-a`和`module-b`为两个不同的模块,通过两个 script 标签分别引入到 HTML 中,这么做看似是分散了不同模块的状态和运行逻辑,但实际上也隐藏着一些风险因素: 1. 模块变量相当于在全局声明和定义,会有变量名冲突的问题。比如 `module-b` 可能也存在`data`变量,这就会与 `module-a` 中的变量冲突。 2. 由于变量都在全局定义,我们很难知道某个变量到底属于哪些模块,因此也给调试带来了困难。 3. 无法清晰地管理模块之间的依赖关系和加载顺序。假如`module-a`依赖`module-b`,那么上述 HTML 的 script 执行顺序需要手动调整,不然可能会产生运行时错误。 #### 2: 命名空间 ​ `命名空间`是模块化的另一种实现手段,它可以解决上述文件划分方式中`全局变量定义`所带来的一系列问题。 ```js // module-a.js window.moduleA = { data: "moduleA", method: function () { console.log("execute A's method"); }, }; // module-b.js window.moduleB = { data: "moduleB", method: function () { console.log("execute B's method"); }, }; Document ``` #### 3: IIFE 私有作用域 不过,相比于`命名空间`的模块化手段,`IIFE`实现的模块化安全性要更高,对于模块作用域的区分更加彻底 ```js // module-a.js ;(function () { let data = 'moduleA' function method() { console.log(data + 'execute') } window.moduleA = { method: method } })() ``` ```js // module-b.js ;(function () { let data = 'moduleB' function method() { console.log(data + 'execute') } window.moduleB = { method: method } })() ``` ```html // index.html Document ``` 我们知道,每个`IIFE` 即`立即执行函数`都会创建一个私有的作用域,在私有作用域中的变量外界是无法访问的,只有模块内部的方法才能访问。对于其中的 `data`变量,我们只能在模块内部的 `method` 函数中通过闭包访问,而在其它模块中无法直接访问。这就是模块`私有成员`功能,避免模块私有成员被其他模块非法篡改,相比于`命名空间`的实现方式更加安全。 ### 2) ES6 Module 在现代浏览器中,如果在 HTML 中加入含有`type="module"`属性的 script 标签,那么浏览器会按照 ES Module 规范来进行依赖加载和模块解析,这也是 Vite 在开发阶段实现 no-bundle 的原因,由于模块加载的任务交给了浏览器,即使不打包也可以顺利运行模块代码。 简单来说就是使用 **import** 和 **export** 的使用 ### 3) Vite 的环境搭建 #! 建议使用 pnpm # #### 1:项目初始化 ``` pnpm create vite ``` 后续的交互流程梳理如下: - 输入项目名称 - 选择前端框架 - 选择开发语言 ``` // 进入项目目录 cd vite-project // 安装依赖 pnpm install // 启动项目 pnpm run dev ``` 可以发现 项目的运行速度和 webpack 相比 有质变。 下图是使用 vue3+ts+vite 搭建出的项目 ![image-20220621172549263](C:\Users\YZW\AppData\Roaming\Typora\typora-user-images\image-20220621172549263.png) image-20220621172742831 #### 2:项目入口解析 现在浏览器支持了`ES module`规范,也就是说可以在浏览器执行 ES 语法,如以下语法是可以使用的:可以直接使用`import`语法 ```html Vite App
相当于请求了http://localhost:3000/src/main.tsx这个资源 vite的dev Server会读取并解析该文件,然后把处理结果返回给浏览器 ``` 在读取到 `main.ts`文件的内容之后,Vite 会对文件的内容进行编译 ![image-20220621173627151](C:\Users\YZW\AppData\Roaming\Typora\typora-user-images\image-20220621173627151.png) 在 Vite 项目中,一个`import 语句即代表一个 HTTP 请求`。上述两个语句则分别代表了两个不同的请求,Vite Dev Server 会读取本地文件,返回浏览器可以解析的代码。当浏览器解析到新的 import 语句,又会发出新的请求,以此类推,直到所有的资源都加载完成。 **总结**:通过已上的内容,了解到了 vite 的`no bundle(不打包)`的真正含义,是通过利用浏览器对 ES 模块的支持,实现在开发环境下,对模块进行按需加载,而不是整体打包后再加载,相比 webpack 这种需要整体打包之后再加载的模式,vite 无疑更有优势。这也是为什么 vite 更快的原因之一。 #### 3:初识 Vite 的配置文件 这是生成的项目中 `vite`的配置文件 ```js // vite.config.ts import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' //引入vue export default defineConfig({ plugins: [vue()] //配置vue }) ``` ##### a:如何更改项目入口文件位置 如果需要更改`index.html`的位置 可以通过以下的方式进行调整 ```js // vite.config.ts import { defineConfig } from 'vite' // 引入 path 包注意两点: // 1. 为避免类型报错,你需要通过 `pnpm i @types/node -D` 安装类型 // 2. tsconfig.node.json 中设置 `allowSyntheticDefaultImports: true`,以允许下面的 default 导入方式 import path from 'path' import react from '@vitejs/plugin-react' export default defineConfig({ // 手动指定项目根目录位置 root: path.join(__dirname, 'src') plugins: [react()] }) ``` 当手动指定`root`参数之后,Vite 会自动从这个路径下寻找`index.html`文件 **思考**:是否可以通过这种方式来实现一个`单项目多工程`的框架,具体可以参考目前公司的 pth5 项目,该项目的问题是 一个项目下有若干个工程,每一个项目都是独立的,但所有项目都公用 main.js 以及一些公用的文件夹,弊病在于如果需要针对个别项目写一下独立的方法,文件的存放会显得杂乱无章。并且每次只需要某个项目的打包,但是通过 webpack 会把所有的项目都进行打包一遍。这些是否可以通过 vite 来进行解决。是一个后续的思考。 ##### b:项目打包 通过使用`pnpm run build`来进行打包,打包后会生成一个 dist 目录 ![image-20220621180050055](C:\Users\YZW\AppData\Roaming\Typora\typora-user-images\image-20220621180050055.png) 可以通过 `pnpm run preview`来预览打包后的文件 **思考**:打包后的 dist 目录位于和 index.html 同级,这个产生的位置可以结合上面的`单项目多工程`进行思考。 ### 4)代码规范 通过配置`vite.config.js` 并引入以下`代码格式化插件` - ### ESLint - ### Prettier - **Stylelint** 并引入`Husky + lint-staged` 的 `Git` 提交工作流集成 实现了`vite`整体的项目搭建工程化,做到`代码保存校验`、`代码提交校验`、`git commit信息规范`这一系列的流程。 **总结**:这一部分内容较多,但是内容比较重要,各项插件的配置项比较多 可以在使用时去对应的官网翻阅,这部分的内容主要是学习到了如何使用并集成这些插件创建一个完整的架构。对想成为一名架构师是很重要的一部分内容。 **实践**:使用 `Vite + Vue3 + Ts + Eslint + Prettier + Stylelint + Husky + lint-staged + commitlint` 搭建了一个完整的 vite 架构 实现从代码验证到提交的整个流程。 ## 第二阶段 ### 1)Vite_demo 优化 - 完善`vite_domo`的代码保存自动修复功能 - 引入了图片压缩插件,在项目`build`时对静态图片进行压缩处理 - 自定义了`git`的`commit-msg`,使提交代码时对`commit-message`的校验,不满足条件的进行拦截,并提示规则,如下图: ![image-20220629153305531](C:\Users\YZW\AppData\Roaming\Typora\typora-user-images\image-20220629153305531.png) ### 2)在 Vite 中处理静态资源 #### 1:配置路径别名 ```js // vite.config.ts import path from 'path'; { resolve: { // 别名配置 alias: { '@': path.join(__dirname, 'src') //至此可以适用 来代替src } } } ``` #### 2:单文件 or 内联 ​ 在 `Vite` 中,所有的静态资源都有两种构建方式,一种是打包成一个单文件,另一种是通过 base64 编码的格式内嵌到代码中。 ​ 对于比较小的资源,适合内联到代码中,一方面对`代码体积`的影响很小,另一方面可以减少不必要的网络请求,`优化网络性能`。而对 于比较大的资源,就推荐单独打包成一个文件,而不是内联了,否则可能导致上 MB 的 base64 字符串内嵌到代码中,导致代码体积瞬 间庞大,页面加载性能直线下降。 ​ `Vite `中内置的优化方案是下面这样的: - 如果静态资源体积 >= 4KB,则提取成单独的文件 - 如果静态资源体积 < 4KB,则作为 base64 格式的字符串内联 ​ 上述的`4 KB`即为提取成单文件的临界值,当然,这个临界值你可以通过`build.assetsInlineLimit`自行配置,如下代码所示: ```js // vite.config.ts { build: { // 8 KB assetsInlineLimit: 8 * 1024 } } ``` #### 3:图片压缩 ​ 可以通过`vite-plugin-imagemin`插件在`build`时对项目中的静态图片进行压缩,如果项目中静态图片比较多,那么效果会很明显。 ```js pnpm i vite-plugin-imagemin -D ``` ​ **注意**:安装时可能会报错 导致安装失败,通常国内网络导致,可以通过如下配置来解决 ```js // package.json // ['https://pnpm.io/zh/package_json#pnpmoverrides'] { "pnpm": { "overrides": { //参考上面的链接👆 此字段允许您指示 pnpm 覆盖依赖关系图中的任何依赖项 "bin-wrapper": "npm:bin-wrapper-china" //配置pnpm的路线 } } } ``` ​ **使用** ```js //vite.config.ts import viteImagemin from 'vite-plugin-imagemin' { plugins: [ // 忽略前面的插件 viteImagemin({ // 无损压缩配置,无损压缩下图片质量不会变差 optipng: { optimizationLevel: 7 }, // 有损压缩配置,有损压缩下图片质量可能会变差 pngquant: { quality: [0.8, 0.9] }, // svg 优化 svgo: { plugins: [ { name: 'removeViewBox' }, { name: 'removeEmptyAttrs', active: false } ] } }) ] } ``` ​ **效果** ​ 可以看的图片压缩的效果很明显 ![image-20220629160023138](C:\Users\YZW\AppData\Roaming\Typora\typora-user-images\image-20220629160023138.png) ### 3)Vite 的依赖预构建 依赖预构建主要做了两件事情: 一是将其他格式(如 UMD 和 CommonJS)的产物转换为 ESM 格式,使其在浏览器通过 ``; const createLink = (src) => ``; const generateHTML = (scripts, links) => ` Esbuild App ${links.join("\n")}
${scripts.join("\n")} `; module.exports = { createLink, createScript, generateHTML }; ``` ## 第三阶段 ### 1)HMR 简介 了解了 HMR 的全称叫做`Hot Module Replacement`,即`模块热替换`或者`模块热更新`。在计算机领域当中也有一个类似的概念叫`热插拔`,我们经常使用的 USB 设备就是一个典型的代表,当我们插入 U 盘的时候,系统驱动会加载在新增的 U 盘内容,不会重启系统,也不会修改系统其它模块的内容。HMR 的作用其实一样,就是在页面模块更新的时候,直接把**页面中发生变化的模块替换为新的模块**,同时不会影响其它模块的正常运作。而 vite 之所以能那么快,就是因为自身基于 Esbuild 实现了一套 HMR ### 2)Vite 拆包机制 为什么要拆包? 在生产环境下,为了提高页面加载性能,构建工具一般将项目的代码打包(bundle)到一起,这样上线之后只需要请求少量的 JS 文件,大大减少 HTTP 请求。某种意义上来说,对线上环境进行项目打包是一个必须的操作。但随着前端工程的日渐复杂,单份的打包产物体积越来越庞大,会出现一系列应用加载性能问题,而代码分割可以很好地解决它们。 #### vite 拆包可以使用该插件 ```js pnpm i vite-plugin-chunk-split -D //https://github.com/sanyuan0704/vite-plugin-chunk-split/blob/master/README-CN.md ``` ```js import { chunkSplitPlugin } from 'vite-plugin-chunk-split' { plugins: [ // ... chunkSplitPlugin({ strategy: 'default', customSplitting: { // `react` and `react-dom` 会被打包到一个名为`render-vendor`的 chunk 里面(包括它们的一些依赖,如 object-assign) 'react-vendor': ['react', 'react-dom'], // 源码中 utils 目录的代码都会打包进 `utils` 这个 chunk 中 utils: [/src\/utils/] } }) ] } ``` #### 2)Vite 语法降级与 Polyfill 注入 Vite 官方已经为我们封装好了一个开箱即用的方案: `@vitejs/plugin-legacy`,我们可以基于它来解决项目语法的浏览器兼容问题。 ``` pnpm i @vitejs/plugin-legacy -D ``` ```js // vite.config.ts import legacy from '@vitejs/plugin-legacy' import { defineConfig } from 'vite' export default defineConfig({ plugins: [ // 省略其它插件 legacy({ // 设置目标浏览器,browserslist 配置语法 targets: ['ie >= 11'] }) ] }) ``` 在引入插件后,我们可以尝试执行`npm run build`对项目进行打包,可以看到如下的产物信息: ![image-20220707173157648](C:\Users\YZW\AppData\Roaming\Typora\typora-user-images\image-20220707173157648.png) #### 3)Vite 项目性能优化 对于项目的加载性能优化而言,常见的优化手段可以分为下面三类: 1. **网络优化**。包括 `HTTP2`、`DNS 预解析`、`Preload`、`Prefetch`等手段。 2. **资源优化**。包括`构建产物分析`、`资源压缩`、`产物拆包`、`按需加载`等优化方式。 3. **预渲染优化**,包括`服务端渲染`(SSR)和`静态站点生成`(SSG)两种手段。 **重点:** ##### 1. HTTP2 传统的 `HTTP 1.1` 存在**队头阻塞**的问题,同一个 TCP 管道中同一时刻只能处理一个 HTTP 请求,也就是说如果当前请求没有处理完,其它的请求都处于阻塞状态,另外浏览器对于同一域名下的并发请求数量都有限制,比如 Chrome 中只允许 `6` 个请求并发(这个数量不允许用户配置),也就是说请求数量超过 6 个时,多出来的请求只能**排队**、等待发送。 因此,在 HTTP 1.1 协议中,**队头阻塞**和**请求排队**问题很容易成为网络层的性能瓶颈。而 HTTP 2 的诞生就是为了解决这些问题,它主要实现了如下的能力: - **多路复用**。将数据分为多个二进制帧,多个请求和响应的数据帧在同一个 TCP 通道进行传输,解决了之前的队头阻塞问题。而与此同时,在 HTTP2 协议下,浏览器不再有同域名的并发请求数量限制,因此请求排队问题也得到了解决。 - **Server Push**,即服务端推送能力。可以让某些资源能够提前到达浏览器,比如对于一个 html 的请求,通过 HTTP 2 我们可以同时将相应的 js 和 css 资源推送到浏览器,省去了后续请求的开销。 在 Vite 中,我们可以通过`vite-plugin-mkcert`在本地 Dev Server 上开启 HTTP2: ``` pnpm i vite-plugin-mkcert -D ``` ```js // vite.config.ts import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import mkcert from 'vite-plugin-mkcert' export default defineConfig({ plugins: [react(), mkcert()], server: { // https 选项需要开启 https: true } }) ``` ##### 2. 资源压缩 在 Vite 生产环境构建的过程中,JavaScript 产物代码会自动进行压缩,相关的配置参数如下: ```js // vite.config.ts export default { build: { // 类型: boolean | 'esbuild' | 'terser' // 默认为 `esbuild` minify: 'esbuild', // 产物目标环境 target: 'modules', // 如果 minify 为 terser,可以通过下面的参数配置具体行为 // https://terser.org/docs/api-reference#minify-options terserOptions: {} } } ``` 值得注意的是`target`参数,也就是压缩产物的目标环境。Vite 默认的参数是`modules`,即如下的 browserlist: ```js // https://vitejs.cn/config/#build-target 由于 Vite 默认的 target 无法覆盖所有支持原生 ESM 的浏览器,经过压缩器的语法转换后,在某些 iOS 机型(iOS 11.2)上出现白屏事故,最后通过指定 target 为 es2015 或者es6 解决了这个问题。 ```