深入浅出vite(核心原理 + 手撕mini-vite)

5,455 阅读15分钟

image.png

前言

之前作者学了一下Vite,如果您了解其速度之快,我相信你会爱上Vite,甚至怀疑大家会摒弃需要打包的构建工具。Vue3+Vite+Ts 绝对会成为新的技术时代潮流,引起新的一轮技术更新热潮。现在跟着我一起从浅入深,一起点亮自己的前端技能树,为技术赋能,在未来的蓝海里有属于自己的一片栖息地。

🎈 一. Vite是什么?

Vite, 一个基于浏览器原生ES MODULE的下一代前端开发和构建工具。利用浏览器解析import发出Http请求,服务端拦截返回给定资源,完全摒弃了打包操作,服务器即启即用。他不仅支持Vue文件,还搞定了热更新,并且热更新的速度并不会随着模块的增多而直线下降(其它构建工具的弊端),按需加载,仅使用需要的模块。在生产环境上使用Rollup进行构建。

🐬 二. Vite的特点

🎃 1. 真正的按需编译

你需要啥,就返回啥。

💡 2. 极速的服务启动

使用原生ESM模块,无需打包

⚡️ 3. 轻量快速的热重载HMR

无论应用程序大小如何,始终于一的模块热重载。

🛠️ 4. 丰富的功能

对 TypeScript、JSX、CSS 等支持开箱即用。

📦 5. 优化的构建

可选 “多页应用” 或 “库” 模式的预配置 Rollup 构建

🔩 6. 通用的插件

vite和Rollup的插件是可以共用的

🔑 7. 完全类型化的API

API完整和完全的TS类型声明

🦞 三. ESModule

ESModule是浏览器支持的模块化方案,允许在代码中实现模块化。这儿我们以vite项目的app.vue为例子

<script setup>
// This starter template is using Vue 3 <script setup> SFCs
// Check out https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup
import HelloWorld from './components/HelloWorld.vue'
</script>

<template>
  <img alt="Vue logo" src="./assets/logo.png" />
  <HelloWorld msg="Hello Vue 3 + Vite" />
</template>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

当我们请求App.vue时,浏览器会发起请求,如图所示

image.png

🦝 四. 浏览器兼容性

  1. 开发环境:Vite 需要在支持 原生 ES 模块动态导入 的浏览器中使用。

  2. 线上环境:默认支持的浏览器需要支持 通过脚本标签来引入原生 ES 模块 。 目前基于Web标准规范的ESModule以及覆盖市面上90%的浏览器。

image.png

image.png

在Vite中我们在入口index.html中可以看到这样一段代码:

image.png

判断兼容性的方法如下: 我们根据script的type="module"可以判断浏览器支不支持es modules,如果不支持,该script里面的内容就不会运行。 对于不支持的浏览器解决方案咱们可以使用systemjs来支持,它就是使浏览器支持的polyfill。也可以使用vite官方插件 @vitejs/plugin-legacy 支持旧浏览器。这儿不是重点咱们就不提及了。

🐧 五. 为什么使用Vite

1. 基于打包构建工具的痛点

image.png

在浏览器支持ES模块之前,开发者没有以模块式的方法开发的原生机制。因此出现了打包这个概念,使用工具将源代码以链接,转换,处理,抓取等方式到文件中,使其可以运行在浏览器上。 直到今日,我们见证了许多打包工具的诞生并掀起了一波新的技术热潮(webpack,Parcel,rollup等),这些工具极大程度上提升了前端开发者的开发体验。在大型企业应用中,随着模块的数量提升(上千个模块),这些基于“打包”的构建工具带来一个现实痛点,开发效率呈直线下降,开发服务器启动长达几分钟。即使使用HMR,有可能出现改动一行代码,热重载几秒钟才能呈现出来,如此循环往复,浪费了极大的时间。

(1)开发服务器启动缓慢

当冷启动开发服务器时,基于打包器的方式是在提供服务前经过一系列处理来构建你的应用。

image.png 由图可知,在服务器就绪前,不管你否使用到某些模块,都打包到了bundle中。

image.png

(2)热重载缓慢

当基于打包器启动开发服务器时,我们更新一处代码,将会重构整个文件,显然我们不应该重新构建整个包,否则更新速度将随文件体积直线下降。 一些打包器将文件载入内存,当文件更改时,只要使模块的一部分失去活跃,即使如此,也是需要重新构建文件在重新载入,代价依然很高。打包器支持了动态模块热重载(HMR):允许一个模块 “热替换” 它自己,而对页面其余部分没有影响。这大大改进了开发体验,然而,在实践中发现,即使是 HMR 更新速度也会随着应用规模的增长而显著下降。

vite中这样完成,引用官方回答和截图:

在 Vite 中,HMR 是在原生 ESM 上执行的。当编辑一个文件时,Vite 只需要精确地使已编辑的模块与其最近的 HMR 边界之间的链失效(大多数时候只需要模块本身),使 HMR 更新始终快速,无论应用的大小。

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

Vite同时使用了http头加快了速度,vite中把模块视为两种,一种是依赖模块(我们使用到的依赖库,通常不会变化,一般是纯js),一种是源码模块(我们写的代码如.vue,.tsx,.less等)。在请求源码时,vite使用协商缓存(304 Not Modified);在请求依赖模块时,使用强缓存(Cache-Control: max-age=31536000,immutable) 来处理,这样一旦缓存即使刷新也不会进行在请求了。 知识点传送门🚀

有的同学会问,那如果要更新怎么办呢: 强烈看看大佬张云龙的回答

如此,vite来了,Vite的就是为了解决以上所述痛点而设计的,旨在利用生态系统中的新进展解决上述问题,利用浏览器开始原生支持ES模块化,真正意义上面的按需加载,热更新速度并不受文件体积大小影响。

🦂 六. 为什么生产环境还需要打包

即使ES MODULE得到广泛的支持,但是在嵌套导入时,会照成额外的网络请求,并且生产环境中发布为打包的esModule的不到最好的效率(即使使用HTTP2)。为了得到最好的性能,还是将code进行tree-shaking、懒加载和chunk分割来获得更好的效率

🧰 七. 核心原理

1. 模块声明

首先在把 type= "module"放到<script>标签中来声明这是一个模块

    <script type="module" src="main.js"></script>

这样通过srcimport导入的文件将会发起http请求;vite会拦截这些请求,并将请求文件进行特别处理。

2. 裸模块替换

当试图请求node_modules文件夹的文件时,会进行裸模块替换(路劲转换为相对路劲),浏览器中只识别相对路劲绝对路劲

import Vue from 'vue' 会转换成 import Vue from '/@modules/vue'

3. 解析/@modules

接下里将/@modules解析为真正的文件地址,并返回给浏览器。其它打包工具,如webpack打包时帮我们做了这件事情。

通过 import 导入的文件 webpack 会去 node_modules/包名/package.json 文件内找 moduel 属性,如下例子。

{ 
  "license": "MIT",
  "main": "index.js",
  "module": "dist/vue.runtime.esm-bundler.js",
  "name": "vue",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/vuejs/vue-next.git"
  },
  "types": "dist/vue.d.ts",
  "unpkg": "dist/vue.global.js",
  "version": "3.2.20"
}

只要将这个dist/vue.runtime.esm-bundler.js这个地址文件返回就好了。

4. 解析单文件(SFC)组件

我们知道.vue文件中包含了template,script,style,三个部分,vite分别对这三个部分做了处理

(1)解析template

vite中使用/@vue/compiler-dom来编译template, 编译后返回的结果类似如下图所示

image.png

(2)处理script

vite中使用/@vue/compiler-sfc来处理单文件组件,它的作用跟vue-loader一样。它的解析结果就好像如下所示。

image.png

(3)解析style

vite对 style 的处理比较特殊,可以看到vite项目请求type=style返回的内容中调用了 updateStyle 方法,在 Vite 中是把它放在了 热更新 的模块中,在mini-vite中,我们模拟实现的方式, 在client 实现该功能的简版。 image.png

🔥 八. mini-vite实现

1. 项目准备

创建文件夹mini-vite,然后进入文件夹执行npm init -y 初始化

image.png

编辑器打开,并修改package.json文件如下图:

{
  "name": "mini-vite",
  "version": "1.0.0",
  "description": "",
  "main": "mini-vite.js",
  "scripts": {
    "dev": "nodemon mini-vite",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "koa": "^2.13.4",
    "vue": "^3.2.20"
  }
}

npm i 安装依赖,完成后将index.js,重命名为mini-vite.js,作为项目的入口文件,修改后的项目结构如下所示:

image.png

向mini-vite.js中添加如下代码:

const Koa = require('koa')

const app = new Koa();

app.use(ctx => {
    ctx.body = "hello Vite"
})

app.listen(3000, function() {
  console.log('started vited')
})

在终端运行,npm run dev启动

image.png

浏览器打开项目,如下所示,到此项目准备完成

image.png

2. 返回html文件

在根目录下新建index.html文件,并写入如下代码

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>hello,mini-Vite!!!</title>
</head>
<body>
    <div id="app"></div>
    <script src="/main.js" type="module"></script>
</body>
</html>

项目根目录新建main.js,并写入以下代码

alert('hello mini-Vite!!!')

修改mini-vite代码,如下所示:

// mini-vite.js
const Koa = require("koa");
const path = require("path");
const fs = require("fs");

const app = new Koa();

app.use((ctx) => {
  const { url } = ctx.request;
  const home = fs.readFileSync('./index.html', 'utf-8')
  // 1. 返回html
  if (url === "/") {
    ctx.type = "text/html"
    ctx.body = home
  }
});

app.listen(3000, function () {
  console.log("started vited");
});

项目结构如下所示:

image.png

重新打开浏览器浏览

image.png

从图中咱们可以清楚的看到localhost时返回了咱们的index.html。有的小伙伴们就会问,为啥main.js请求报红呢???这时我们可以思考一下,我们只处理了"/"的请求,并没有处理其它请求,我们查看main.js的请求路劲

image.png

是不是恍然大悟,咱们是不是只要请求url匹配js就可以了,我们往下看

3. 返回js请求

我们思考一下,我们该如何对所有的js文件进行文件进行正确返回呢

有的童鞋已经发现了,所有js请求url都是以.js结尾的,我们只需取到对应url下的对应文件,在直接返回就ok了,我们看代码。

修改mini-vite代码, 添加如下代码

...
app.use((ctx) => {
  const { url } = ctx.request;
  const home = fs.readFileSync('./index.html', 'utf-8')
  // 1. 返回html
  if (url === "/") {
    ctx.type = "text/html"
    ctx.body = home
  } else if(url.endsWith('.js')) {
    // 2. 返回js
    const filePath = path.join(__dirname,url) // 获取绝对路劲
    const file = fs.readFileSync(filePath, 'utf-8')
    ctx.type = "application/javascript"
    ctx.body = file
  }
});
...

我们在浏览器刷新,结果如图所示

image.png

image.png 我们可以清晰的看到已经完成我们想要的结果了。

main.js是不是too young too simple!!! 我们对main.js代码作出以下修改:

// main.js
import { createApp, h } from "vue";

createApp({
  render() {
    return h("div", "", "hello mini-vite!!!");
  }
}).mount('#app');

我们刷新浏览器

image.png 报错,浏览无法识别模块vue,只能识别已/,./,../这种路劲开头(即相对路劲),那我们怎么解决呢,我们往下看

4. 裸模块替换

根据vite原理,首先我们应该将裸模块替换成/@modules/vite,我们修改mini-vite.js代码, 如下所示:

// main.js
app.use((ctx) => {
   // ...
   else if (url.endsWith(".js")) {
    // 2. 返回js
    const filePath = path.join(__dirname, url); // 获取绝对路劲
    const file = fs.readFileSync(filePath, "utf-8");
    ctx.type = "application/javascript";
    // ctx.body = file
    // 裸模快替换成/@modules/包名,浏览器就会发起请求
    ctx.body = rewirteImport(file);
  }
});

/**
 * @description: 裸模块替换, import xxx from "xxx" -----> import xxx from "/@modules/xxx"
 * @param {*} content
 * @return {*}
 */
function rewirteImport(content) {
  return content.replace(/ from ['"](.*)['"]/g, (s1, s2) => {
    // s1, 匹配部分, s2: 匹配分组内容
    if (s2.startsWith("./") || s2.startsWith("/") || s2.startsWith("../")) {
      // 相对路劲直接返回
      return s1;
    } else {
      return ` from "/@modules/${s2}"`
    }
  });
}
// ...

我们刷新浏览器,结果如下图所示

image.png

image.png

从结果上,我们完成了,初步的替换;也发起了Vue的请求,请求路劲也是咱们想要的,现在只需将请求路劲以/@modules/开头的请求返回 node_modules/包名/package.json里面的module属性值文件即可,我们在mini-vite代码下进行添加,如下所示:

app.use((ctx) => {
  // ...
   else if (url.startsWith("/@modules/")) {
   // 3. 返回裸模快引用的node_modules/包名/package.json.module引用的真实文件
    ctx.type = "application/javascript";
    /** 文件前缀 */
    const filePrefix = path.resolve(
      __dirname,
      "node_modules",
      url.replace("/@modules/", "")
    );
    /** 得到node_modules/包名/package.json 里面的moudule路劲 */
    console.log(filePrefix, "ttt");
    const module = require(filePrefix + "/package.json").module;
    const file = fs.readFileSync(filePrefix+'/'+module, "utf-8");
    // 如果里面还要import XXX 再继续替换
    ctx.body = rewirteImport(file);
  }
});
// ...

我们刷新浏览器,结果如图所示:

image.png 我们可以清楚的看到Vue已经成功返回了。

我们发现页面报了一个错误:

image.png

image.png

因为有的库会访问process,如这儿的Vue3会判断是否为生产环境,而我们这儿并没有process这个变量,那么问题来了,我们应该怎么做呢???

诶Σ(⊙▽⊙"a,是不是有的童鞋已经想到了,咱们模拟一下不就Ok了,咱们在window上面挂载一个process,是不是瞬间恍然大悟!!!我们往下看

5. 模拟process

针对这个问题我一开始使用的是方案一,引入cross-env,修改package.json的dev脚本为以下:

image.png 然后在mini-vite.js返回html时打印,代码如下:

image.png 大家觉得这个问题能够解决不?为什么?

答案是:NO!!!,原因就是,咱们的代码是在浏览器执行的,浏览器的根对象是window,并不存在属性process,我们应该在window上面添加这个属性

我们来看正确解决方法,在index.html中添加如下代码:

<body>
    <div id="app"></div>
    <script>
      // hash: 规避 通过process.env.NODE_ENV环境判断
      window.process = {
        env: {
          NODE_ENV: 'dev',
        }
      }
    </script>
    <script src="/main.js" type="module"></script>
</body>

刷新一下浏览器,咱们发现结果就不报错了。

6. 单文件(SFC)组件处理

vite中,单文件处理是使用的是@vue/compiler-sfc模块进行编译处理的,因此咱们也借助该模块完成Vue文件的处理。 它解析的结果大概如下:

image.png

(1)script处理

首先我们在src文件夹下新建App.vue

<!--
 * @Author: 水冰淼
 * @Date: 2021-11-01 15:06:48
 * @LastEditTime: 2021-11-01 15:43:51
 * @LastEditors: Please set LastEditors
 * @Description: App.vue代码
 * @FilePath: \mini-vite\src\App.vue
-->
<template>
  <div id="app">
    {{title}}
  </div>
</template>

<script>
export default {
  data () {
    return {
      title: "hello Vite"
    }
  }
}
</script>

<style>
  #app {
    color: red;
    font-weight: 400;
  }
</style>

然后修改main.js代码为以下:

/*
 * @Author: 水冰淼
 * @Date: 2021-10-22 20:58:14
 * @LastEditTime: 2021-11-01 15:18:34
 * @LastEditors: Please set LastEditors
 * @Description: main.js,模拟vue项目入口文件
 * @FilePath: \mini-vite\main.js
 */
import { createApp } from "vue";
import App from "./src/App.vue"
createApp(App).mount('#app')

然后在mini-vite里添加如下代码:

// ...
else if (url.includes(".vue")) {
    // 获得绝对路劲, url.slice(1)去掉第一个'/'
    const filePath = path.resolve(__dirname, url.slice(1));
    // console.log(filePath, url,'path')
    const { descriptor } = compilerSfc.parse(
      fs.readFileSync(filePath, "utf-8")
    );
    // 处理script
    if (!query.type) {
      // 获取script
      const scriptContent = descriptor.script.content
      // export default {...}  --------> const __script = {...}
      const script = scriptContent.replace('export default ', 'const __script = ')
      // 返回App.vue解析结果
      ctx.type = 'text/javascript'
      ctx.body = `
        ${rewirteImport(script)}
        // 如果有 style 就发送请求获取 style 的部分
        ${descriptor.styles.length ? `import "${url}?type=style"` : ''}
        // 发送请求获取template部分,这儿返回一个渲染函数
        import { render as __render } from '${url}?type=template'
        __script.render = __render
        export default __script
      `
    }
  }
  // ...

我们刷新浏览器,f12切到network查看结果:

image.png

我们可以看到请求style和template了,下面我们继续往下处理template和style

image.png

(2) 处理template

在vue中我们使用@vue/compiler-dom 来编译 template,由于我们返回的vueruntime版本的,是没有编译器的,我们应该将编译好的template返回回去,下面我们返回一个渲染(render)函数即可。 继续在mini-vite.js添加代码:

// ... 处理template
else if(query.type === 'template') {
      const templateContent = descriptor.template.content
      const render = compilerDom.compile(templateContent, {
        mode: 'module'
      }).code
      ctx.type = "application/javascript"
      ctx.body = rewirteImport(render);
    }
// ... template

我们刷新浏览器,发现结果如下:

image.png

下面我们来处理style

(3) 解析style

Vite对style的处理比较特殊,处于热更新模块中,由于我们没有实现热更新,咱们这儿就模拟实现一下,将style content返回,在客户端实现该方法。

mini-vite.js新增代码

// ...
// 处理style
else if (query.type === "style") {
      const styleBlock = descriptor.styles[0];
      ctx.type = "application/javascript";
      ctx.body = `
        const css = ${JSON.stringify(styleBlock.content)};
        updateStyle(css);
        export default css;
      `;
    }
// ...

index.html中添加updateStyle代码:

//...
<body>
    <div id="app"></div>
    <script>
      // hash: 规避 通过process.env.NODE_ENV环境判断
      window.process = {
        env: {
          NODE_ENV: 'dev',
        }
      };
     function updateStyle(content) {
       const isExist = typeof CSSStyleSheet !== undefined
       if(isExist) {
          // 方法1,使用可构造样式表
        let cssStyleSheet = new CSSStyleSheet()
        cssStyleSheet.replaceSync(content)
        document.adoptedStyleSheets = [
          ...document.adoptedStyleSheets,
          cssStyleSheet
        ]
       } else {
         // 方法2
        let style = document.createElement('style')
        style.setAttribute('type', 'text/css')
        style.innerHTML = content
        document.head.appendChild(style)
       }
      }
    </script>
    <script src="/main.js" type="module"></script>
</body>
//...

方法1:使用了可构造样式表,方法2咱们就不巴拉巴拉了

刷新浏览器,咱们查看结果:

image.png

image.png 从图咱们可以发现,SFC组件已成功解析完成。手撕mini-vite的核心原理到此结束。

7. 优化点

有的同学已经发现了咱们刷新的速度是非常慢的,咱们其实在上文说原理时已经说到过,vite将依赖使用强缓存,将源码使用协商缓存进行响应速度的加快,我们这边来模拟一下这个操作。核心思想:

如果是依赖我们使用强缓存,即(Cache-control:max-age=31536000,immutable),如果是源码咱们使用协商缓存(即Cache-control: no-cache)。这儿使用etag,和Last-Modified,首先判断是否存在etag,如果存在,会发送ifNoneMatch(值为etag)到服务器,咱们和源码的唯一hash值进行对比,如果相同返回304,如果不存在ifNoneMatch或者不相同,将会返回200和新的资源并在响应头设置新的etag;如果请求头不存在ifNoneMatch而存在ifModifiedSince(值为:Last-modified),将使用ifModifiedSince和源码的最后修改时间对比,如果相同,返回304;如果不相同或者不存在ifModifiedSince,将返回200和新的资源并在响应头设置新的Last-modified。返回304从缓存读取的条件就是前提有缓存,我们对源码都使用http头Expires缓存一段时间。

缓存流程可以参考下图所示:

image.png

image.png

跟图有点不同的是,源码文件第二次请求时,是直接走协商缓存了,是不会判断本地缓存的而是直接发起请求让服务器去决策了

image.png 代码如下所示:

// mini-vite.js
// ...

/** 获取文件的最后修改时间 */
const getFileUpdatedDate = (path) => {
  const stats = fs.statSync(path);
  return stats.mtime;
};

/** 协商缓存判断返回304还是200 */
const ifUseCache = (ctx, url, ifNoneMatch, ifModifiedSince) => {
  let flag = false
  // 使用协商缓存
  ctx.set('Cache-Control', 'no-cache')
  // 设置过期时间在30000毫秒,也就是30秒后
  ctx.set("Expires", new Date(Date.now() + 30000));
  let filePath = url.includes(".vue") ? url : path.join(__dirname, url);
  if (url === "/") {
    filePath = path.join(__dirname, "./index.html");
  }
  // 获取文件的最后修改时间
  let fileLastModifiedTime = getFileUpdatedDate(filePath);
  console.log(fileLastModifiedTime, "lastTime");
  const buffer = fs.readFileSync(filePath, "utf-8");
  // 计算请求文件的md5值
  const hash = crypto.createHash("md5");
  hash.update(buffer, "utf-8");
  // 得到etag
  const etag = `${hash.digest("hex")}`;
  if (ifNoneMatch === etag) {
    ctx.status = 304;
    ctx.body = "";
    flag = true
  } else {
    // etag不一致 更新tag值,返回新的资源
    ctx.set("etag", etag);
    flag = false
  }

  if (!ifNoneMatch && ifModifiedSince === fileLastModifiedTime) {
    ctx.status = 304;
    ctx.body = "";
    flag = true
  } else {
    // 最后修改时间不一致,更新最后修改时间,返回新的资源
    ctx.set("Last-Modified", fileLastModifiedTime);
    flag = false
  }
  return flag
};

app.use(async (ctx) => {
  const { url, query } = ctx.request;
  const { "if-none-match": ifNoneMatch, "if-modified-since": ifModifiedSince } =
    ctx.request.headers;
  const home = fs.readFileSync("./index.html", "utf-8");
  // 1. 返回html
  if (url === "/") {
    ctx.type = "text/html";
    ctx.body = home;
  } else if (url.endsWith(".js")) {
    ctx.set("cache-control", "no-cache");
    // 判断是否读取缓存
    const used = ifUseCache(ctx, url, ifNoneMatch, ifModifiedSince);
    if (used) {
      ctx.status = 304
      ctx.body = null
      return;
    }
    //...
  } else if (url.startsWith("/@modules/")) {
    // ...
    // 依赖使用强缓存
    ctx.set("cache-control", "max-age=31536000,immutable");
    // ...
  } else if (url.includes(".vue")) {
    // ...
    const usedCache = ifUseCache(
      ctx,
      url.slice(1).split("?")[0],
      ifNoneMatch,
      ifModifiedSince
    );
    if (usedCache) {
      ctx.status = 304
      ctx.body = null
      return;
    }
    // ...
    
  }
});

第一次请求结果如下,我们可以看到源码已经符合需求了,依赖也符合需求了:

image.png

image.png

第二次请求咱们可以发现,依赖已经使用强缓存从内存磁盘读取了, 源码走的协商是返回304还是200image.png

🐳 九. 第一个vite项目

1.安装

兼容性注意Vite需要 Node.js 版本 >= 12.0.0。

image.png 使用 NPM:

npm init @vitejs/app

使用YARN

yarn create @vitejs/app

也可以使用如下命令:

# npm 6.x
npm init @vitejs/app my-vue-app --template vue

# npm 7+, 需要额外的双横线:
npm init @vitejs/app my-vue-app -- --template vue

# yarn
yarn create @vitejs/app my-vue-app --template vue

支持的模板预设包括:

  • vanilla
  • vue
  • vue-ts
  • react
  • react-ts
  • preact
  • preact-ts
  • lit-element
  • lit-element-ts 查看 @vitejs/create-app 获取每个模板的更多细节。

image.png

image.png

image.png

我们观察页面请求: 页面入口返回了index.html,其中中通过script type="module"引入了入口文件main.js,当遇到src或者import导入时将会发起请求

image.png

浏览器在请求main.js文件,vite返回如下

image.png

这儿发生了裸模快替换,将import {createApp} from 'vue' 替换成了相对路径。因为浏览器只有相对路径或绝对路径。后面又发起了请求App.vue,vue.js,HelloWorld.vue

image.png

通过type=style中来处理css并返回, 对Css的处理比较特殊,可以看到返回之中调用了updateStyle(),vite是把它放在热模块更新之中,这个不是咱们的重点,本文写mini-vite时候会用模拟方法实现。

image.png

2.命令行

在安装了 Vite 的项目中,可以在 npm scripts 中使用 vite 可执行文件,或者直接使用 npx vite 运行它。下面是通过脚手架创建的 Vite 项目中默认的 npm scripts:

{
  "scripts": {
    "dev": "vite", // 启动开发服务器
    "build": "vite build", // 为生产环境构建产物
    "serve": "vite preview" // 本地预览生产构建产物
  }
}

你可以通过 --port 指定启动的端口,通过 --https使用https, 执行npx vite --help获取更多, 后续出一篇文章讲解这个带你从0撸一个脚手架工具

🐋十. 定制Vite

1. 配置文件

(1)配置文件解析

当以命令行运行vite时,默认读取的配置文件是根目录下的vite.config.js的文件, 脚手架生成的默认配置是这样的:

image.png

你也可以显示的通过 vite --config filePath,来指定配置文件,默认基于当前项目根目录。

(2)配置智能提示

由于Vite本身支持TS,因此你可以通过IDE和JSDOC配合来进行提示

/**
 ** @type {import('vite').UserConfig}
 */
const config = {
  // ...
}

export default config

或者使用助手函数defineConfig, 这样不适用jsdoc也能获得智能提示:

import { defineConfig } from 'vite'

export default defineConfig({
  // ...
})

image.png

根据环境来定义配置

我们可以基于环境(development, production)或者命令(server,build)来输出不同的配置, 如下所示:

export default ({ command, mode }) => {
  if (command === 'serve') {
    return {
      // serve 独有配置
    }
  } else {
    return {
      // build 独有配置
    }
  }
}

🦄 十一. 结语

代码比较粗糙,没有做太多的封装,仅做思想传递,其实还是有很多可以优化的地方。静态资源托管,解析less,sass,stylus等,可以集成esbuilder来做更多事情,这就不是mini-vite了,咱们只实现核心原理和思想,有兴趣的童鞋可以继续研究实现自己的ideal。

🦓 十二. 完整代码

mini-vite完整代码

🦝 十三. 参考