likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 37

vite加载文件的基本原理

vite对非js文件的处理

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

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

这正是vite为什么比webpack启动要快的缘故了:

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

你一定会问,浏览器并不认识css.vue这些文件,怎么能加载它们呢?答案很简单,vite在加载这些文件时,进行了拦截,将它们统一处理成了js文件。

不信你看,css文件里面的内容其实被处理成了Js:

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

.vue文件里面的内容也被处理成了js:

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

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')

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

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

vite的路径解析

我们都知道,浏览器是无法识别 import { createApp } from 'vue' 这样的代码的,那么vite中是如何处理的呢?我们在浏览器控制台可以找到答案。

打开main.js,我们可以看出vue的核心依赖已经被预打包成vue.js文件了,并且放在了 /node_modules/.vite/deps缓存目录下。

手写vite让你深刻了解Vite的文件加载原理现在,我们已经理解了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的内容了。

路由处理

很可惜,启动项目后,我们会发现浏览器出现报错

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内容

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

因此,我们需要进行路由匹配,根据不同路由展示不同内容

//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("我执行了"),现在,我们打开浏览器

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

可以看出来,我们的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")

当然,这会报错手写vite让你深刻了解Vite的文件加载原理

浏览器并不认识 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() 方法的第二个参数是要替换的字符串或函数。修改后:

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

此时报错的原因是无法在@modules下找到vue文件,这是我们接下来要做的事情。vue的源码是放在node_modules文件夹的下的,其源码位置可以在node_modules\vue\package.json文件中找到手写vite让你深刻了解Vite的文件加载原理

所以,我们只需要将@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的相关资源已经被请求回来了

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

只不过,控制台依旧报错。

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

报错原因依旧能够推断出来,浏览器环境不认识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>

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

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

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");
});
转载自:https://juejin.cn/post/7178803290820804667
评论
请登录