🔥Vite6.0 都发布了,还没了解过原理?敲一个本地服务,模拟下 Vite 加载资源的过程

1,210 阅读7分钟

前言

哈喽大家好!我是 嘟老板Vite 我也用了有一段时间了,但是说实话并没有很深入的研究过它,为了加深个人对于 Vite 基本原理的理解,特地敲一个本地服务,模拟下 Vite 加载和处理资源的过程。

介绍

Vite 是一种新型的构建工具,能够显著提升前端开发体验。内部预置了一系列默认配置,可以让我们很轻松的用它来构建项目。

快速搭建

最快速的方式还是使用脚手架 create-vite 提供的命令行,比如创建 vue 项目,可使用以下命令:

pnpm create vite vite-vue-app --template vue

然后按照提示操作即可。

若没有全局安装过 create-vite,会提示先安装。

介绍下手动使用 Vite 搭建项目的过程。

初始化 npm 项目

终端输入以下命令,回车执行:

mkdir vite-test
cd vite-test
npm init -y

执行成功后将在当前目录下生成一个 package.json 文件。

安装 vite

终端输入以下命令,安装 vite:

pnpm add -D vite

创建 index.html

在项目根目录下创建 index.html 文件,作为项目的入口文件。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vite Test</title>
</head>
<body>
  <div>hello vite</div>
</body>
</html>

添加 npm script

package.jsonscripts 配置中添加以下脚本:

{
  "scripts": {
    "dev": "vite",
    "build": "vite build"
  }
}

启动项目

终端输入以下命令,启动项目:

pnpm dev

执行成功后,Vite 会在本地启动一个开发服务器,默认端口是 5173

image.png

浏览器访问 http://localhost:5173/

image.png

页面正常展示。

vite.config.js

完成上面的步骤,已经可以使用 Vite 启动服务并正常访问了,但是,我们还没有进行任何配置,若要项目支持支持更多自定义的特性,就需要配置文件 - vite.config.js 了。

在项目根目录下创建 vite.config.js 文件。

touch vite.config.js

现在我想把本地服务启动的端口改为 9000,在 vite.config.js 中添加以下代码:

export default {
  server: {
    port: 9000
  }
}

保存后重新启动服务,

image.png

OK,已启动 9000 端口。

Vite 提供了许多可定义的配置,如共享配置(root、base、plugins....),服务器配置(server.*),构建配置(build.*) 等等...,感兴趣的同学可以自行查阅,可玩度相当高。

bundleless

bundleless 是啥玩意,不太了解的小伙伴,肯定感觉莫名其妙吧。莫急,我们往下看。

传统的构建工具,比如 webpack,会从项目入口文件开始,解析模块依赖,构建依赖图,然后打包成一个或多个文件,这个过程叫做 bundle

相对 webpack 而言,Vite 则提倡少打包甚至不打包,将代码直接交给浏览器处理,这种方式则被称为 bundleless

那为什么 Vite 可以实现 bundleless,而 webpack 却没有呢?

一句话总结,时势造英雄。可以说 Vite 是时代发展的产物。正是由于浏览器对 esm 的支持,才使得 Vite 能够实现 bundleless。

一个完整的前端项目,需要处理的文件类型很多,比如:

  • js/jsx/ts/tsx
  • css
  • 图片 webp、png、svg...
  • 字体文件
  • 等等...

除了 js,其他类型的资源,浏览器并不能处理。

因此 Vite 也不是完全不打包,对于浏览器无法直接处理的资源,Vite 会对其进行构建处理。

这些工作基本都是由插件完成,在官网可以看到很多可用的插件,如 @vitejs/plugin-vue,用来支持 Vue3 单文件组件;@vitejs/plugin-vue-jsx 用来支持 Vue3 JSX;@vitejs/plugin-react 用来支持 React,等等等等...

开发环境和构建环境

目前 Vite 对于开发环境和构建环境的处理方式不同。主要区别在于开发环境使用 esbuild 对代码进行构建、打包,而构建环境使用 rollup 进行打包。

为什么这样区别处理呢?

  1. esbuild 构建速度够快,可以显著提升开发体验。
  2. esbuild 需要自行处理 dts 文件。
  3. esbuild 不支持 es5 以下版本转换。
  4. 等等...

目前 Vue 团队使用 Rust 开发了一个整合工具 - rolldown,后续将代替 esbuildrollup,统一开发环境和构建环境的处理方式。

动手模拟 Vite 加载资源的过程

上面有提到,一个完整的前端项目,会包含多种多样的资源,比如 js、jsx、ts、tsx、css、图片以及字体(woff2)等等...很多很多,而这其中,浏览器仅能直接处理 js,那如何才能让其他文件被正常加载、执行呢?这就需要对其他类型的资源转换成浏览器可以处理的类型,比如 ts 转为 js,css 添加到 html 中...然而这一切在使用 Vite 时,对我们来说都是无感的,它会自动帮我们完成这些工作。

但是你对这中间的处理过程就不好奇吗,Vite 到底用了什么魔法,能帮我们做这么多事?莫急,在本节中,我们将搭建一个本地服务,模拟 Vite 对于资源文件的加载和处理。

目标

  1. 搭建一个本地服务,实现资源加载
  2. 使用 esbuild 处理 TypeScript 资源。

初始化

终端输入以下命令,创建基础项目结构:

mkdir min-Vite
cd min-Vite
npm init -y
mkdir src
cd src
touch index.html
touch index.js

执行成功后,目录结构如下:

image.png

在 index.html 和 index.js 中分别写入如下代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Min Vite</title>
</head>
<body>
  <script src="./index.js" ></script>
</body>
</html>
console.log('index.js')

index.js 中直接打印日志,然后在 index.html 中引入 index.js。

创建 server

现在开始编写本地服务,我们使用 express 来搭建。

终端输入以下命令,回车安装 express:

pnpm add express -S

安装完成后,在根目录下创建 server.js,写入以下代码:

const express = require('express')

const app = express()

app.get('/', (req, res) => {
  res.send('Hello Vite!')
})

const port = 3000
app.listen(port, () => {
  console.log("🚀~~ 服务已启动,访问:", `http://localhost:${port}`)
})

基础的服务就写好了,接下来在 package.json 添加一条启动命令:

{
  "scripts": {
    "start": "nodemon ./server.js"
  },
}

nodemon 可以监听文件变更,当我们修改了代码后,服务会自动重启。 执行 pnpm add nodemon -D 安装。

终端输入 pnpm start 启动服务:

image.png

OK,启动完成。

浏览器打开 http://localhost:3000/

image.png

加载 index.html

细心的小伙伴可能发现,server 启动的服务比较独立,并没有加载 src/index.html,我们来处理一下。

get 请求调整如下:

const fs = require('fs')
const path = require('path')

app.get('/', (req, res) => {
  res.sendFile(path.resolve(__dirname, './src/index.html'))
})

表示当前请求路径为 / 时,响应 src/index.html

刷新浏览器,看下效果:

image.png

咦!报错了,没有找到 index.js,这是因为 index.html 中引入了 index.js,<script src="./index.js" ></script>,但是 server 服务还没有处理 js 文件的加载。

我们继续完善...

加载 index.js

思考一下,应该如何加载 js 文件呢?

浏览器完全可以处理 js 文件,所以我们直接将 js 文件的内容返回即可。

说干就干,在 server.js 中新增一个加载 js 文件的路由。

app.get('/*.js', (req, res) => {
  const file = fs.readFileSync(path.resolve(__dirname, `./src${req.path}`), 'utf-8')

  res.type('js')
  res.end(file)
})

通过 fs 模块的 readFileSync 同步读取 js 文件,并响应文件内容。

刷新浏览器,看下效果:

image.png

OK,index.js 正常加载。

看下控制台是否打印消息:

image.png

OK,正常打印,符合预期。

处理 TypeScript 资源

目前已经可以正常加载 js 资源了,但是我不想用 js 开发,我想用 ts,于是我把 index.js 改为 index.ts,并写入如下代码:

function add(a: number, b: number): number {
  return a + b;
}

console.log("🚀 ~ 'index.ts':", add(1, 2));

然后在 index.html 中引入 index.ts

<script src="./index.ts" ></script>

这样浏览器可以正常加载吗,我们刷新下看看:

image.png

哦!server 服务里还没有处理 ts 类型的请求,我们参考 js 类型请求加一个:

app.get('/*.ts', (req, res) => {
  const file = fs.readFileSync(path.resolve(__dirname, `./src${req.path}`), 'utf-8')

  res.type('js')
  res.end(file)
})

现在可以请求 ts 资源了,应该没问题了吧,继续刷新浏览器验证:

image.png

不出意外的话,出意外了!!为什么呢?

因为浏览器无法识别 ts 语法,代码中定义的类型,浏览器无法执行,只能报错了,那怎么办呢?这就需要 esbuild 帮忙了,帮我们把 ts 代码编译成 js

esbuild 入场

终端输入以下命令,回车安装:

pnpm add esbuild -S

ts 请求调整如下:

app.get('/*.ts', async (req, res) => {
  const file = fs.readFileSync(path.resolve(__dirname, `./src${req.path}`), 'utf-8')

  let transformedResult = await esbuild.transform(file, {
    loader: 'ts',
    format: 'esm',
    target: 'es6'
  })
  res.type('js')
  res.end(transformedResult.code)
})

读取文件后,借助 esbuildtransform api 将源代码转换为 esm 格式,并响应转换后的代码。

现在能否正常加载了呢?浏览器验证下:

image.png

OK,控制台正常打印,符合预期。

提示

实际上 Vite 源码中,基本上以插件的形式处理资源,比如使用 esbuildPlugin 处理需要 esbuild 编译的资源,如 ts、jsx、tsx 等;使用 cssPlugin 处理类 css 资源,如 css、scss、less 等;

结语

本文重点介绍了 Vite 的基本使用及资源加载方式,通过编写一个本地服务,模拟 Vite 加载和处理资源的过程,旨在加深对于 Vite 基本原理的理解,希望对您有所帮助!

如您对文章内容有任何疑问或想深入讨论,欢迎评论区留下您的问题和见解。

技术简而不凡,创新生生不息。我是 嘟老板,咱们下期再会。


往期干货