vite加载文件的基本原理
vite对非js文件的处理
我们通过vue的官方脚手架工具create-vue创建一个基础的vite+vue3项目。当我们启动服务打开浏览器的控制台时,一定会很好奇,我们的项目内只有一个App.vue文件,但是浏览器器为什么请求了很多类型的资源文件?
这正是vite为什么比webpack启动要快的缘故了:
- webapck在启动时,会将所有类型的文件处理成浏览器识别的js,所以打开慢。
- vite在启动时,通过ES6模块化的方式加载所有文件,所以非常快。
你一定会问,浏览器并不认识css、.vue这些文件,怎么能加载它们呢?答案很简单,vite在加载这些文件时,进行了拦截,将它们统一处理成了js文件。
不信你看,css文件里面的内容其实被处理成了Js:
.vue文件里面的内容也被处理成了js:
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的路径解析
我们都知道,浏览器是无法识别 import { createApp } from 'vue' 这样的代码的,那么vite中是如何处理的呢?我们在浏览器控制台可以找到答案。
打开main.js,我们可以看出vue的核心依赖已经被预打包成vue.js文件了,并且放在了/node_modules/.vite/deps缓存目录下。
现在,我们已经理解了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内容
因此,我们需要进行路由匹配,根据不同路由展示不同内容
//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("我执行了"),现在,我们打开浏览器
可以看出来,我们的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")
当然,这会报错
浏览器并不认识 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() 方法的第二个参数是要替换的字符串或函数。修改后:
此时报错的原因是无法在@modules下找到vue文件,这是我们接下来要做的事情。vue的源码是放在node_modules文件夹的下的,其源码位置可以在node_modules\vue\package.json文件中找到
所以,我们只需要将@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的相关资源已经被请求回来了
只不过,控制台依旧报错。
报错原因依旧能够推断出来,浏览器环境不认识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>
此时,浏览器就可以正常加载我们的文件了!
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");
});