Vite从0到1

vite 初识

创建一个 vite 项目,只需要:

yarn create vite

然后按照提示进行操作即可(这里选的是 react + js)。生成的项目目录如下:

├─ public
│ └─ vite.svg
├─ src
│ ├─ assets
│ │ └─ react.svg
│ ├─ App.css
│ ├─ App.jsx
│ ├─ index.css
│ └─ main.jsx
├─ index.html
├─ package.json
└─ vite.config.js

这个命令先是安装一个全局依赖 create-vite,然后运行 create-vite命令。等同于:

cnpm install create-vite -g && create-vite

create-vite 就是一个 vite 的脚手架,会根据你的需要选择不同模板来克隆项目。
需要注意的是, vite 对 node 版本有要求, 要求 node 版本是 ^14.18.0 || >=16.0.0

可以看到根目录有个 index.html。这个是 vite 项目的入口文件。
Vite 解析 <script type="module" src="..."> ,这个标签指向我们的 JavaScript 源码。

为什么选择 vite

Vite 是一种新型前端构建工具,能够 显著提升 前端开发体验
为什么能够显著提升开发体验呢?首先我们了解结构建工具做了哪些工作。

1. 传统构建工具所做的工作(自动化)

  • 模块化开发支持:支持直接从 node_modules 引入代码,支持多重模块化
  • 处理代码的兼容性:比如 ES6 的代码降级,jsx转换为js, less/sass 转换为 css(不是构建工具做的,构建工具将这些工具集成进来自动化处理)
  • 提高项目性能:压缩代码,代码分割
  • 提高开发体验:提供开发服务器,能够解决服务跨域的问题(本地代理)。监听文件的变化,文件变化后能够自动调用相应的工具重新处理、打包,在浏览器重新运行(热更新)

这样,我们就不用管理代码如何处理,如何在浏览器运行,只需要关注开发工作即可。

目前的构建工具,通常是这个流程:从入口构建依赖图 => 对所有模块打包 => 浏览器运行。如下图:


当我们开始构建越来越大型的应用时,需要处理的 JavaScript 代码量也呈指数级增长。包含数千个模块的大型项目相当普遍。基于 JavaScript 开发的工具就会开始遇到性能瓶颈:通常需要很长时间(甚至是几分钟!)才能启动开发服务器,即使使用模块热替换(HMR),文件修改后的效果也需要几秒钟才能在浏览器中反映出来。如此循环往复,迟钝的反馈会极大地影响开发者的开发效率和幸福感。
Vite 旨在利用生态系统中的新进展解决上述问题:浏览器开始原生支持 ES 模块,且越来越多 JavaScript 工具使用编译型语言编写。

2.vite 的设计理念

2.1 开发服务器

Vite 通过在一开始将应用中的模块区分为 依赖源码 两类,改进了开发服务器启动时间。

  • 依赖 大多为在开发时不会变动的纯 JavaScript。一些较大的依赖(例如有上百个模块的组件库)处理的代价也很高。依赖也通常会存在多种模块化格式(例如 ESM 或者 CommonJS)。Vite 将会使用 esbuild 预构建依赖。esbuild 使用 Go 编写,并且比以 JavaScript 编写的打包器预构建依赖快 10-100 倍。
  • 源码 通常包含一些并非直接是 JavaScript 的文件,需要转换(例如 JSX,CSS 或者 Vue/Svelte 组件),时常会被编辑。同时,并不是所有的源码都需要同时被加载(例如基于路由拆分的代码模块)。

Vite 以 原生 ESM 方式提供源码。这实际上是让浏览器接管了打包程序的部分工作:Vite 只需要在浏览器请求源码时进行转换并按需提供源码。根据情景动态导入代码,即只在当前屏幕上实际使用时才会被处理。如下图:


2.2 热更新(HMR)

传统的 HMR:当我们对代码做修改并保存后,webpack 会对修改的代码块以及该模块的依赖重新编译打包,并将新的模块发送至浏览器端,浏览器用新的模块代替旧的模块,从而实现了在不刷新浏览器的前提下更新页面。相比起直接刷新页面的方案,HMR 的优点是可以保存应用的状态。当然,随着项目体积的增长,热更新的速度也会随之下降。
在 Vite 中,HMR 是在原生 ESM 上执行的。当编辑一个文件时,Vite 只需要精确地使已编辑的模块与其最近的 HMR 边界之间的链失活(大多数时候只是模块本身),使得无论应用大小如何,HMR 始终能保持快速更新。
Vite 同时利用 HTTP 头来加速整个页面的重新加载(再次让浏览器为我们做更多事情):源码模块的请求会根据 304 Not Modified 进行协商缓存,而依赖模块请求则会通过 Cache-Control: max-age=31536000,immutable 进行强缓存,因此一旦被缓存它们将不需要再次请求。

2.3 为什么生产环境仍需打包

尽管原生 ESM 现在得到了广泛支持,但由于嵌套导入会导致额外的网络往返,在生产环境中发布未打包的 ESM 仍然效率低下(即使使用 HTTP/2)。为了在生产环境中获得最佳的加载性能,最好还是将代码进行 tree-shaking、懒加载和 chunk 分割(以获得更好的缓存)。

依赖预构建

1. 为什么需要依赖预构建

我们看这样一个例子。创建下面的目录,并在 index.html 中以 module 的形式引入 main.js

prebuild-demo    
├─ index.html    
├─ main.js       
└─ package.json  
// index.html
// ...
<script type="module" src="./main.js"></script>
// ...

接着,我们cnpm install lodash -S 安装 lodash,并在 main.js 中写入:

// main.js
import { throttle } from 'lodash';
console.log(throttle);

然后,在浏览器打开 index.html,会报错:


这是因为 ESModule 中,相对引用要采用/, ./, 或 ../开头。因此,不能够通过依赖的方式直接引入。
依赖预构建,能够重写这部分模块引入,从而解决问题:
我们安装 vite,并用 vite 启动项目:

cnpm install vite && npx vite

打开控制台,throttle 已经能打印出来了。


再看 main.js ,模块引入变成了具体的地址:

除了依赖补全,依赖预构建还做了这两个工作:

  • CommonJS 和 UMD 兼容性: 开发阶段中,Vite 的开发服务器将所有代码视为原生 ES 模块。因此,Vite 必须先将作为 CommonJS 或 UMD 发布的依赖项转换为 ESM。
  • 提高性能: Vite 将有许多内部模块的 ESM 依赖关系转换为单个模块,以提高后续页面加载性能。比如官网的例子,lodash-es。

2. 缓存

2.1 文件系统缓存

Vite 会将预构建的依赖缓存到 node_modules/.vite。它根据几个源来决定是否需要重新运行预构建步骤:

  • package.json中的 dependencies 列表
  • 包管理器的 lockfile,例如 package-lock.json, yarn.lock,或者 pnpm-lock.yaml
  • 可能在 vite.config.js 相关字段中配置过的
    只有在上述其中一项发生更改时,才需要重新运行预构建。
    如果出于某些原因,你想要强制 Vite 重新构建依赖,你可以用 --force 命令行选项启动开发服务器,或者手动删除 node_modules/.vite 目录。
2.2 浏览器缓存

解析后的依赖请求会以 HTTP 头 max-age=31536000,immutable 强缓存,以提高在开发时的页面重载性能。一旦被缓存,这些请求将永远不会再到达开发服务器。如果安装了不同的版本(这反映在包管理器的 lockfile 中),则附加的版本 query 会自动使它们失效。如果你想通过本地编辑来调试依赖项,你可以:

  1. 通过浏览器调试工具的 Network 选项卡暂时禁用缓存;
  2. 重启 Vite dev server,并添加--force 命令以重新构建依赖;
  3. 重新载入页面。

常用功能与配置

1. CSS Modules

任何以 .module.css 为后缀名的 CSS 文件都被认为是一个 CSS modules 文件。导入这样的文件会返回一个相应的模块对象。
也就是说我们直接可以模块引入:

import styles from "./index.module.less";```

CSS Modules 比较常用的配置:

// ...
css: {
    modules: {
      generateScopedName: "[path][name]__[local]__[hash:5]",
      localsConvention: "camelCaseOnly"
    }
  },
  // ...

其中,

  • generateScopedName: 生成的类名格式


  • localsConvention:修改生成对象的 key 的展示形式 (驼峰还是中划线)


2. CSS 预处理器

vite 提供了对 sass/less/stylus 的内置支持。
我们只需要安装相应的预处理器依赖即可直接使用。

cnpm install less -D

比较常用的配置:

css: {
    // ...
    preprocessorOptions: {
      less: {
        additionalData: `@import '@/assets/styles/common.less';`, // 全局注入样式文件
        modifyVars: {
          'primary-color': '#409eff' // 全局样式变量
        },
        javascriptEnabled: true
      }
    },
    
    devSourcemap: true // 默认false,设为true开发阶段启动用sourcemap
  },

3. PostCSS

vite 提供了对 PostCSS 的内置支持。
我们可以在 vite.config.js 中配置 PostCSS ,也可以直接新建 postcss.config.js 文件配置 PostCSS 。
这里我们使用 postcss-preset-env 试一下,postcss-preset-env 包含一系列 PostCSS 的插件。比如浏览器前缀自动添加:

cnpm install postcss-preset-env -D

根目录新增 postcss.config.js ,并配置如下:

import postcssPresetEnv from 'postcss-preset-env';

export default {
  plugins: [postcssPresetEnv()]
};

重启开发服务器,就能够看到浏览器前缀自动添加了:


4. 静态资源处理

4.1 资源引入

vite 中,引入一个静态资源会返回解析后的公共路径:

import exampleImg from "/src/assets/example.png";

exampleImg 在开发时会是 /src/assets/example.png,生产环境会是 /assets/example.2d8efhg.png。类似于 webpack4 中的 file-loader.

  • 常见的图像、媒体和字体文件类型被自动检测为资源。你可以使用 assetsInclude 选项 扩展内部列表。
export default defineConfig({
  assetsInclude: ['**/*.gltf'] // 会把.gltf文件当做资源文件处理
})
  • 较小的资源体积小于 assetsInlineLimit 选项值 则会被内联为 base64 data URL。
 // ...
 build: {
    assetsInlineLimit: 8 * 1024, // 小于 8 KB的资源会被内联成base64格式
    // ...
  },

也可以通过 ?raw 后缀声明作为字符串引入。类似于 webpack4 中的 raw-loader

import helloString from "./test.txt?raw";
console.log("exampleImg", helloString); // hello, vite
4.2 public 目录

如果你有下列这些资源:

  • 不会被源码引用(例如 robots.txt)
  • 必须保持原有文件名(没有经过 hash)
  • ...或者你压根不想引入该资源,只是想得到其 URL。
    那么你可以将该资源放在指定的 public 目录中,它应位于你的项目根目录。该目录中的资源在开发时能直接通过 / 根路径访问到,并且打包时会被完整复制到目标目录的根目录下。
    目录默认是 /public,但可以通过 publicDir 选项 来配置。
    请注意:
  • 引入 public 中的资源永远应该使用根绝对路径 —— 举个例子,public/icon.png 应该在源码中被引用为 /icon.png
  • public 中的资源不应该被 JavaScript 文件引用。

5. alias 与 extensions

通过下面的代码配置别名和扩展名缩写:

import { resolve } from "path";
// ...
resolve: {
    alias: {
      "@": resolve(__dirname, "./src")
    },
    extensions: [".jsx", ".js", ".tsx", ".ts", ".json"]
},

6. 本地开发服务器

server: {
    open: true, // 自动打开浏览器
    host: "0.0.0.0",
    port: 9999,
    strictPort: true, // 设置为false,端口被占用会直接退出
    proxy: {
      "/webapi": {
        target: "http://10.2.2.98:8090",
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/webapi/, "")
      }
    }
  }

环境变量

1. 内建环境变量

Vite 在一个特殊的 import.meta.env 对象上暴露环境变量。这里有一些在所有情况下都可以使用的内建变量:

  • import.meta.env.MODE: {string} 应用运行的模式。
  • import.meta.env.BASE_URL: {string} 部署应用时的基本 URL。他由 base 配置项决定。
  • import.meta.env.PROD: {boolean} 应用是否运行在生产环境。
  • import.meta.env.DEV: {boolean} 应用是否运行在开发环境 (永远与 import.meta.env.PROD 相反)。
  • import.meta.env.SSR: {boolean} 应用是否运行在 server 上。

2. .env 文件

我们可以在 .env 文件中编写自己需要的环境变量。

.env                # 所有情况下都会加载
.env.local          # 所有情况下都会加载,但会被 git 忽略
.env.[mode]         # 只在指定模式下加载
.env.[mode].local   # 只在指定模式下加载,但会被 git 忽略

自己编写的环境变量必须以 VITE_ 为前缀,比如:

VITE_SOME_KEY=123
DB_PASSWORD=foobar // 不合法
console.log(import.meta.env.VITE_SOME_KEY) // 123
console.log(import.meta.env.DB_PASSWORD) // undefined

3. 模式

默认情况下,开发服务器 (dev 命令) 运行在 development (开发) 模式,而 build 命令则运行在 production (生产) 模式。
如果我们需要额外的模式,则可以使用 --mode 覆盖默认的模式:

vite build --mode staging

同时我们还需要一个 .env.staging 文件来定义环境变量:

# .env.staging
NODE_ENV=production
VITE_HTTP=http://10.2.2.245:8890

生产构建优化

1. 分包策略

生产环境打包的时候,我们可能会需要分包。比如:把依赖单独打一个包,这样就可以避免依赖被重复打包。

    build: {
      assetsInlineLimit: 8 * 1024, // 小于8KB的资源base64内联
      rollupOptions: {
        output: {
          manualChunks(id) {
            if (id.includes("node_modules")) {
              return "vendor";
            }
          }
        }
      }
    },

rollupOptions 里还能够配置打包生成的目录,一个常用的配置:

 build: {
      rollupOptions: {
        output: {
          // ...
          assetFileNames: (assetInfo) => {
            var info = assetInfo.name.split(".");
            var extType = info[info.length - 1];
            if (/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/i.test(assetInfo.name)) {
              extType = "media";
            } else if (/\.(png|jpe?g|gif|svg)(\?.*)?$/.test(assetInfo.name)) {
              extType = "img";
            } else if (/\.(woff2?|eot|ttf|otf)(\?.*)?$/i.test(assetInfo.name)) {
              extType = "fonts";
            }
            return `static/${extType}/[name]-[hash][extname]`;
          },
          chunkFileNames: "static/js/[name]-[hash].js",
          entryFileNames: "static/js/[name]-[hash].js"
        }
      }
    },

打包后的目录:


2. 动态导入

动态导入(import 函数)是 ES6 的新特性。使用动态导入语法能够实现分包,进而实现懒加载。通常用于路由的懒加载。下面是一个例子。

// import { throttle } from 'lodash';
// console.log('object :>> throttle', throttle);

import('lodash').then(({ throttle }) => {
  console.log('object :>> throttle', throttle);
});

上面是直接导入,下面是动态导入。二者打包结果如下:


可以看出,下面的 lodash 已经自动分包了。

3. 图片压缩、gzip 压缩

通过 vite-plugin-imageminvite-plugin-compression 插件可以实现图片压缩与 gzip 压缩。用法也比较简单:

import compression from 'vite-plugin-compression';
import imagemin from 'vite-plugin-imagemin';

export default defineConfig({
  // ...
  plugins: [react(), compression(), imagemin()]
  // ...
});

4. CDN 优化(外网环境)

通过 vite-plugin-cdn-import 插件能够将一些依赖使用 cdn 加载,从而降低包的大小,加快依赖加载速度。用法如下:

import { Plugin as importToCDN } from 'vite-plugin-cdn-import';

export default defineConfig({
  plugins: [
    react(),
    importToCDN({
      modules: [
        {
          name: 'lodash',
          var: '_',
          path: 'https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.21/lodash.min.js'
        }
      ]
    })
  ]
});

参考文档

Vite 官方文档: https://cn.vitejs.dev/guide/
Vite 和 webpack、rollup 打包工具对比:https://blog.csdn.net/Ambibibition/article/details/127766551
Vite世界指南(带你从0到1深入学习 vite):
https://www.bilibili.com/video/BV1GN4y1M7P5/?spm_id_from=333.999.0.0&vd_source=41e6d1d28e504860272fd13300cb250c

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容