手写vite让你深刻了解Vite的文件加载原理

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 【8月更文挑战第3天】 手写vite让你深刻了解Vite的文件加载原理

vite加载文件的基本原理

vite对非js文件的处理

我们通过vue的官方脚手架工具create-vue创建一个基础的vite+vue3项目。当我们启动服务打开浏览器的控制台时,一定会很好奇,我们的项目内只有一个App.vue文件,但是浏览器器为什么请求了很多类型的资源文件?
GIF 2022-12-19 17-10-55.gif
这正是vite为什么比webpack启动要快的缘故了:

  • webapck在启动时,会将所有类型的文件处理成浏览器识别的js,所以打开慢。
  • vite在启动时,通过ES6模块化的方式加载所有文件,所以非常快。

你一定会问,浏览器并不认识css.vue这些文件,怎么能加载它们呢?答案很简单,vite在加载这些文件时,进行了拦截,将它们统一处理成了js文件。
不信你看,css文件里面的内容其实被处理成了Js:
image.png
.vue文件里面的内容也被处理成了js:
image.png

vite的模块化文件加载方式

vite速度快,是因为它读取到文件时,把所有非js类型的的文件内容转译为js,然后通过ES6模块化的方式进行加载。那么,vite是怎么使用ES6的模块化语法呢?非常简单,vite在index.html宿主页的script标签内写了type="module":

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + Vue</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

基于这种形式,我们在main.js内就可以直接使用js的模块语法而不需要打包了。基于 type="module" 的形式加载js文件,具有两个明显优点:

  • 不用打包
  • 按需加载

注意:vite加载非js文件时,已经将其内容处理成了js,并且也采用esmodule模块语法>

我们看看main.js内的内容

import {
   
    createApp } from 'vue'
import './style.css'
import App from './App.vue'
createApp(App).mount('#app')

现在,我们也就能看明白浏览器的一些资源加载顺序了。
image.png

vite的路径解析

我们都知道,浏览器是无法识别 import { createApp } from 'vue' 这样的代码的,那么vite中是如何处理的呢?我们在浏览器控制台可以找到答案。
打开main.js,我们可以看出vue的核心依赖已经被预打包成vue.js文件了,并且放在了/node_modules/.vite/deps缓存目录下。

image.png现在,我们已经理解了vite加载文件的基本原理,接下来,我们通过手写一个简单的vite来加深我们对这个过程的理解。

手写一个简单的vite

仿照vite的使用,我们先进行项目的目录搭建

项目创建

├─ myVite
│  ├─ myVite.js
│  ├─ package.json
│  └─ src
│     └─ main.js
│  ├─ index.html

我们在myVite.js中编写vite的核心文件加载逻辑

要实现一个简单的vite,我们肯定要开启一个node服务器,用来处理浏览器加载各种资源的请求

  • index.html
  • js
  • vue

因此,我们采用koa作为node服务器,安装依赖:

npm i koa

为了方便每次修改myVite.js代码后,node代码实时做出响应,我们安装nodemon

npm i nodemon

首页资源文件请求

我们实现vite的第一步,应该是让它具备展示index.html文件的能力

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + Vue</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

然后使用koa服务器进行index.html内容请求

// myVite.js
const koa = require("koa");
//创建实例
const app = new koa();

const fs = require("fs");
//中间件配置
//处理路由
app.use(async (ctx) => {
   
   
  //加载index.html
  ctx.type = "text/html";
  ctx.body = fs.readFileSync("./index.html", "utf-8");
});

app.listen(3000, () => {
   
   
  console.log("服务运行在3000端口:http://localhost:3000");
});

现在,我们在浏览器输入http://localhost:3000,理论上就可以看见index.html的内容了。

路由处理

很可惜,启动项目后,我们会发现浏览器出现报错
:::danger
Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of "text/html". Strict MIME type checking is enforced for module scripts per HTML spec.
加载模块脚本失败:需要一个 JavaScript 模块脚本,但服务器以“text/html”的 MIME 类型响应。 根据 HTML 规范对模块脚本强制执行严格的 MIME 类型检查。
:::
报错原因很简单,index.html里会加载"/src/main.js" ,我们没有对路由进行处理,请求到“/src/main.js”时,依旧会返回index.html内容
image.png
因此,我们需要进行路由匹配,根据不同路由展示不同内容

//koa
const koa = require("koa");
//创建实例
const app = new koa();

const fs = require("fs");
//中间件配置
//处理路由
app.use(async (ctx) => {
   
   
  const {
   
    url } = ctx.request;
  if (url == "/") {
   
   
    //加载index.html
    ctx.type = "text/html";
    ctx.body = fs.readFileSync("./index.html", "utf-8");
  }else if( url.endsWith('.js')){
   
   
    // js文件加载处理
    const p = path.join(__dirname,url)
    ctx.type = "application/javascript"
    ctx.body = fs.readFileSync(p,"utf8")
  }
});

app.listen(3000, () => {
   
   
  console.log("服务运行在3000端口:http://localhost:3000");
});

在main.js简单写一句代码:alert("我执行了"),现在,我们打开浏览器
image.png
可以看出来,我们的main.js文件也执行了。现在,我们需要完善main.js的内容了

JS加载与裸模块重载

要完善main.js的内容,我们首先需要安装一下vue

npm i vue

然后,在main.js中,我们简单的写点东西

import {
   
    createApp ,h} from "vue"
createApp({
   
   
    render() {
   
   
        return h("div","myVite")
    }
}).mount("#app")

当然,这会报错image.png
浏览器并不认识 import { createApp } from "vue" 这样的文件引入方式,所以我们需要对“vue”这样的路径进行处理。
我们可以在模块加载时,将“/”,“./”,"../"这样的路径进行重写,使其以特定字符/@modules开头,并将特定开头的路径进行重新定向至nodemodules目录下。
myVite\myVite.js

//koa
const koa = require("koa");
//创建实例
const app = new koa();

const fs = require("fs");
const path = require("path");
//中间件配置
//处理路由
app.use(async (ctx) => {
   
   
  const {
   
    url } = ctx.request;
  if (url == "/") {
   
   
    //加载index.html
    ctx.type = "text/html";
    ctx.body = fs.readFileSync("./index.html", "utf-8");

    // 判断字符串是否以指定的子字符串结尾(区分大小写):
  }else if( url.endsWith('.js')){
   
   
    // js文件加载处理,p是js实际请求的绝对路径
    const p = path.join(__dirname,url)
    ctx.type = "application/javascript"
    ctx.body = fs.readFileSync(p,"utf8")
    ctx.body = rewriteImport(fs.readFileSync(p,"utf8"))
  }
});

//裸模块地址重写:将 import xx from "vue" 的路径改写为 import xx from "@"
function rewriteImport(content){
   
   
  return content.replace(/ from ['"](.*)['"]/g,function(s1,s2) {
   
   
    if(s2.startsWith("./") || s2.startsWith("/") || s2.startsWith("../")){
   
   
      return s1
    }else{
   
   
      //裸模块,替换
      return `from '/@modules/${
     
     s2}'`
    }
  })
}

app.listen(3000, () => {
   
   
  console.log("服务运行在3000端口:http://localhost:3000");
});

replace() 方法的第二个参数是要替换的字符串或函数。修改后:
image.png
此时报错的原因是无法在@modules下找到vue文件,这是我们接下来要做的事情。vue的源码是放在node_modules文件夹的下的,其源码位置可以在node_modules\vue\package.json文件中找到image.png
所以,我们只需要将@modules指向packjson的module的字段即可。我们在代码中增加相应逻辑

app.use(async (ctx) => {
   
   
  const {
   
    url } = ctx.request;
  if (url == "/") {
   
   
     ......
  }else if( url.endsWith('.js')){
   
   
    ......
  }else if(url.startsWith("/@modules/")){
   
   
    //裸模块名称 示例:vue
    const moduleName = url.replace("/@modules/","")
    // 去node_modules目录中找  示例:D:\Code\【源码及仿写系列】\vite\myVite\node_modules\vue
    const prefix = path.join(__dirname,"./node_modules",moduleName)
    console.log('prefix: ', prefix);
    // 去package.json中获取module字段
    const module = require(prefix + "/package.json").module
    // 获取js文件的完整路径
    const filePath = path.join(prefix,module)
    //读取文件内容
    const ret = fs.readFileSync(filePath,"utf8")
    // 设置返回的文件格式
    ctx.type = "application/javascript"
    // 将js内容中的文件引入进行路径替换
    ctx.body = rewriteImport(ret)
  }
});

打开控制台,可以发现,vue的相关资源已经被请求回来了
image.png
只不过,控制台依旧报错。
image.png
报错原因依旧能够推断出来,浏览器环境不认识node环境下的process环境变量。
我们在index.html里面创建这个变量即可。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + Vue</title>
  </head>
  <body>
    <div id="app"></div>
    <script>
      window.process = {
   
   
        env:{
   
   
          NODE_ENV:'dev'
        }
      }
    </script>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

此时,浏览器就可以正常加载我们的文件了!
image.png

vue文件解析

现在,我们试着加载一下vue文件。创建vue文件

├─ myVite
│  ├─ index.html
│  ├─ myVite.js
│  ├─ package-lock.json
│  ├─ package.json
│  └─ src
│     ├─ App.vue
│     └─ main.js

在App.vue中写入内容

<template>
    <div>
        {
   
   {
   
    title }}
    </div>
</template>
<script lang="ts">
import{
   
    defineComponent }from 'vue';
export default defineComponent({
   
   
    name: "",
    setup: () => {
   
   
        const title = 1
        return {
   
   
            title
        }
    }
})
</script>
<style lang="less" scoped>

</style>

在main.js中进行引入

import {
   
    createApp } from "vue"
import App from "./App.vue"
createApp(App).mount("#app")

接下来,我们需要解析vue文件。我们可以使用官方的解析器

npm i @vue/compiler-sfc -S

打印下解析后的.vue文件


......
const complilerSFC = require('@vue/compiler-sfc')

app.use(async (ctx) => {
   
   
  const {
   
    url } = ctx.request;
  if (url == "/") {
   
   
    ......
  }else if( url.endsWith('.js')){
   
   

  }else if(url.startsWith("/@modules/")){
   
   

  } else if(url.indexOf(".vue") > -1){
   
   
    //sfc请求
    //读取vue文件,解析为js
    const p = path.join(__dirname,url)
    const ret = complilerSFC.parse(fs.readFileSync(p,"utf8"))
    console.log('ret: ', ret);
  }
});

app.listen(3000, () => {
   
   
  console.log("服务运行在3000端口:http://localhost:3000");
});
相关文章
|
7月前
|
数据采集 前端开发 JavaScript
我是如何使用 Next.js14 + Tailwindcss 重构个人项目的
这篇文章介绍了作者在学习React和Nest时受到大佬imsyy项目DailyHot的启发,基于React开发了一个项目,并因为这个项目获得了少量流量而进行了优化。作者随后因为想要优化SEO和深入学习Next.js14,决定重构这个项目。文章详细介绍了项目的信息、特性、演示图、运行环境、Vercel本地部署步骤以及责任声明。作者还感谢了为本项目提供支持与灵感的项目,并承诺会记录下开发过程中遇到的问题及解决方法以帮助他人。
我是如何使用 Next.js14 + Tailwindcss 重构个人项目的
|
7月前
|
JavaScript 前端开发 编译器
js开发: 请解释什么是Babel,以及它在项目中的作用。
**Babel是JavaScript编译器,将ES6+代码转为旧版JS以保证兼容性。它用于前端项目,功能包括语法转换、插件扩展、灵活配置和丰富的生态系统。Babel确保新特性的使用而不牺牲浏览器支持。** ```markdown - Babel: JavaScript编译器,转化ES6+到兼容旧环境的JS - 保障新语法在不同浏览器的运行 - 支持插件,扩展编译功能 - 灵活配置,适应项目需求 - 富强的生态系统,多样化开发需求 ```
61 4
|
3月前
|
JavaScript
Nest.js 实战 (十一):配置热重载 HMR 给服务提提速
这篇文章介绍了Nest.js服务在应用程序引导过程中,TypeScript编译对效率的影响,以及如何通过使用webpackHMR来降低应用实例化的时间。文章包含具体教程,指导读者如何在项目中安装依赖包,并在根目录下新增webpack配置文件webpack-hmr.config.js来调整HMR相关的配置。最后,文章总结了如何通过自定义webpack配置来更好地控制HMR行为。
|
5月前
|
JavaScript 前端开发
前端 JS 经典:Vite 打包优化
前端 JS 经典:Vite 打包优化
277 0
|
7月前
|
缓存 JavaScript 前端开发
前端工程化:优化JS加载速度
在现代Web应用中,JavaScript已成为必不可少的一部分,但是随着业务复杂度的增加,JS文件的体积也越来越大,导致网页加载速度变慢,影响用户体验。本文将介绍前端工程化的优化策略,以提高JS文件的加载速度。
92 2
|
7月前
|
自然语言处理 JavaScript 前端开发
Vue.js 深度解析:模板编译原理与过程
Vue.js 深度解析:模板编译原理与过程
|
7月前
|
JavaScript 前端开发 算法
js手写题:第二章
js手写题:第二章
47 0
|
缓存 前端开发 JavaScript
Vite的原理
Vite的原理
94 0
|
JavaScript 前端开发 Go
|
前端开发
前端学习案例16-js中的HMR
前端学习案例16-js中的HMR
86 0
前端学习案例16-js中的HMR