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

简介: 【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");
});
相关文章
|
前端开发
react hooks 使用小技巧—useState传值函数
当使用useState时,传入一个函数作为初始状态值的参数和传入一个值的参数的效果是一样的,都会在组件渲染时被调用,但它们的使用场景略有不同。
835 1
|
Linux 调度 数据安全/隐私保护
Qt之QFtp
简述 QFtp 类提供了一个 FTP 协议的客户端实现。 该类提供了一个到 FTP 的直接接口,允许对请求有更多的控制。但是,对于新的应用程序,建议使用 QNetworkAccessManager 和 QNetworkReply,因为这些类拥有一个更简单、还更强大的 API。 简述 QFtp 工作流程 基本使用 连接并登录 FTP 服务器 切换工作目录 列出目
7500 1
|
JavaScript
vite的快的原因居然如此简单!探秘其依赖预加载机制
【8月更文挑战第1天】探秘vite预加载机制
229 4
vite的快的原因居然如此简单!探秘其依赖预加载机制
|
JavaScript Java 开发工具
Electron V8排查问题之接近堆内存限制的处理如何解决
Electron V8排查问题之接近堆内存限制的处理如何解决
694 1
|
缓存 网络协议 Serverless
函数计算操作报错合集之遇到AxiosError: Network Error错误,该如何排查
在使用函数计算服务(如阿里云函数计算)时,用户可能会遇到多种错误场景。以下是一些常见的操作报错及其可能的原因和解决方法,包括但不限于:1. 函数部署失败、2. 函数执行超时、3. 资源不足错误、4. 权限与访问错误、5. 依赖问题、6. 网络配置错误、7. 触发器配置错误、8. 日志与监控问题。
604 1
处理Vite项目首屏加载响应迟缓和二次刷新的问题
处理Vite项目首屏加载响应迟缓和二次刷新的问题
1195 0
|
JSON JavaScript 定位技术
Echarts 绘制地图(中国、省市、区县),保姆级教程!
Echarts 绘制地图(中国、省市、区县),保姆级教程!
|
算法 Java 数据安全/隐私保护
使用 Spring Boot 进行加密和解密:SecretKeySpec 和 Cipher
【6月更文挑战第7天】在现代软件开发中,数据加密和解密是保护敏感信息的重要手段。本文将介绍如何在 Spring Boot 项目中使用 Java 的 SecretKeySpec 和 Cipher 类来实现对称加密和解密。
1312 1
一招轻松解决node内存溢出问题
一招轻松解决node内存溢出问题
|
JavaScript Linux iOS开发
node版本管理工具n
Node版本管理工具n是一个用于管理Node.js版本的命令行工具。它可以让您在同一台机器上同时安装和切换不同版本的Node.js。 使用n工具,您可以执行以下操作: 1. 安装Node.js版本:通过运行`n install <version>`命令,您可以安装指定版本的Node.js。例如,`n install 12.18.3`将安装Node.js 12.18.3版本。 2. 切换Node.js版本:通过运行`n use <version>`命令,您可以切换当前正在使用的Node.js版本。例如,`n use 14.15.4`将切换到Node.js 14.15.4版本。 3. 查看
1475 1