Vite 是什么
官方给出含义:下一代前端开发与构建工具
Vite 与 其它打包工具有何不同
- 在冷启动中,Vite 的启动速度比其它的快很多,因为它不需要将一些 ES6+ 的语法进行转义打包,直接引用依赖
- 在开发过程中,因为现代化浏览器都支持 ES 模块,所以Vite在开发阶段只做包的引入
- 使用 Vite 构建 Vue、React 等项目很简单,只需要 npm init @vitejs/app 构建一个 Vite 项目,在命令行中可以选择 Vue、React 等模板即可
- 相对于 webpack 的优势:在开发环境中,Vite 是不做编译也不做打包,只对一些非 JS 文件进行 Loaded 转换成 JS 文件 所以加载速度快
原理
- 在开发环境中 Vite 只开启一个静态服务器监听代码的变化,不需要将代码打包,由于在 Vite 项目中不需要打包,所以每一次修改更新都很快
- 使用 Rellup 进行构建
- 项目运行时可以看到很多个文件(.js、.vue、.jsx等等),虽然浏览器默认不会解析这些文件,但是服务器在返回的头部信息中把 Content-Type: application/javascript,所以浏览器在解析这些文件时是以 JavaScript 文件进行解析的
- 语法使用 AMD 语法,不需要转成 CommonJS,因为现在主流的浏览器都支持 AMD 语法,只需要把项目中使用 Import 导入的 node_module 文件改成绝对路径即可
- webpack 是将所有文件打包到一个 js 文件中,而 Vite 是直接加载这些文件(文件引入时修改路径)
手写 Vite
- 创建文件 kvite 夹,进到文件夹并 npm init -y 初始化项目
- 项目前的准备
- 在 Vite 中是需要一个 node 服务器监听,所以引入一个 node 框架 koa(这里你可以使用 node 原生进行开启服务器,引入 http 模块,还可以使用其它 node 框架,例如 express、egg 等),然后运行服务器。
- 为了监听代码的变化,还需要引入一个 nodemon 热更新
- 当前项目以构建 vue3 为例,所以还需要引入 vue3 模块
mkdir kvite && cd kvite
npm init - y
// 下载文件由于使用 npm install 有些依赖会下载失败(如果有梯子的话就使用 npm install 吧),推荐用 yarn 进行安装
// yarn add 依赖 相当于 npm install 依赖 --save
yarn add koa
yarn add nodemon
yarn add vue @next // 这里说明一下,如果使用 yarn add vue 下载的是 vue2 版本
// 下载完后可以在 package.json 中查看下载后的依赖
{
"dependencies": {
"global": "^4.4.0",
"koa": "^2.13.4",
"nodemon": "^2.0.15",
"vue": "^3.2.23"
}
}
- 前期准备好之后就开始进入正题
- 在 kvite 文件夹中分别创建 index.js、index.html、src 文件夹、src文件夹/main.js
- 在 index.html 中写一个常规 html 结构,在 body 中 创建一个 id 为 div 的节点
// kvite/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>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
- 在 kvite 中把 koa 引入,创建一个 node 服务器,并且运行文件,node index.js
// kvite/index.js
/**
* @description 手写 Vite
* @author oyzx
* node 服务器,处理浏览器加载各种资源的请求
*/
const fs = require('fs')
const path = require('path')
const Koa = require('koa')
// 创建实例
const app = new Koa()
// 处理中间配置
// 处理路由
app.use(async ctx => {
ctx.type = 'text/html' // 浏览器根据类型解析文件
ctx.body = fs.readFileSync(path.join(__dirname, './index.html'), 'utf-8')
})
app.listen(3000, () => {
console.log('kvite start');
})
- 打开浏览器,输入 http://localhost:3000 点击回车,我们就可以看到 /kvite/index.html 文件了,你可以在 /kvite/index.html 中添加一些元素再刷新一下就可以看到内容了 - 为了项目工程化,我在 package.json 中加入执行脚本,我们可以使用 yarn dev 或者 npm run dev 执行文件
// kvite/package.json
{
"scripts": {
"dev": "nodemon ./index.js"
},
"dependencies": {
"global": "^4.4.0",
"koa": "^2.13.4",
"nodemon": "^2.0.15",
"vue": "^3.2.23"
}
}
- 在浏览器中我们打开控制台,可以看到一个错误: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. ,因为在 index.html 中引入一个 main.js,但是我们返回给浏览器解析的 type 类型却是 text/html,所以浏览器解析不了 js 文件,这时候我们就要根据路由来指定解析类型了
/**
* @description 手写 Vite
* @author oyzx
*
* node 服务器,处理浏览器加载各种资源的请求
*/
const fs = require('fs')
const path = require('path')
const Koa = require('koa')
// 创建实例
const app = new Koa()
// 处理中间配置
// 处理路由
app.use(async ctx => {
const { url } = ctx
if (url === '/') {
ctx.type = 'text/html'
ctx.body = fs.readFileSync(path.join(__dirname, './index.html'), 'utf-8')
} else if (url.endsWith('.js')) {
ctx.type = 'application/javascript'
ctx.body = fs.readFileSync(path.join(__dirname, url), 'utf-8')
}
})
app.listen(3000, () => {
console.log('kvite start');
})
- 在 main.js 可以写一段代码,刷新页面后,在浏览器控制台中可以看到输出
// kvite/src/main.js
consol.log('hello world')
- 接下来对引入文件的转换,将 import 文件的非相对路径转成相对路径进行一个文件导入,如果文件中还有 import 中非相对路径的引入文件继续转换导入
// kvite/index
/**
* @description 手写 Vite
* @author oyzx
*
* node 服务器,处理浏览器加载各种资源的请求
*/
const fs = require('fs')
const path = require('path')
const Koa = require('koa')
// 创建实例
const app = new Koa()
/**
* @description 将原来的裸地址改成相对地址
*/
const GRewriteImport = content => {
return content.replace(/ from ['"](.*)["']/g, (s1, s2) => {
if (s2.startsWith('.') || s2.startsWith('/') || s2.startsWith('../')) {
return s1
} else {
// 替换
return ` from '/@modules/${s2}'`
}
})
}
// 处理中间配置
// 处理路由
app.use(async ctx => {
const { url } = ctx
if (url === '/') {
ctx.type = 'text/html'
ctx.body = fs.readFileSync(path.join(__dirname, './index.html'), 'utf-8')
} else if (url.endsWith('.js')) {
ctx.type = 'application/javascript'
ctx.body = GRewriteImport(fs.readFileSync(path.join(__dirname, url), 'utf-8'))
} else if (url.startsWith('/@modules/')) {
// 裸模块
const vModuleName = url.replace('/@modules/', '')
// 接下来去 node_modules 文件夹下面去找
const vPrefix = path.join(__dirname, '/node_modules/', vModuleName)
// 接下来查找文件中的 package.json,根据 package.json 中 module 读取文件
const vModule = require(path.join(vPrefix, 'package.json')).module
const vFilepath = path.join(vPrefix, vModule)
// 获取具体文件
const vResult = GRewriteImport(fs.readFileSync(vFilepath, 'utf-8'))
ctx.type = 'application/javascript'
ctx.body = vResult
}
})
app.listen(3000, () => {
console.log('kvite start');
})
// kvite/src/main.js
import { createApp, h } from 'vue'
createApp({
render() {
return h('div', 'hello world')
}
}).mount('#app')
- 接下来对 vue 文件进行转义引入,引入 @vue/compiler-sfc、@vue/compiler-dom 两个转义文件,
- @vue/compiler-sfc 将 vue 文件中 script 标签包含的 vue 语法代码进行转义,
- @vue/compiler-dom 将 vue 文件中 tempalte 标签包含的 vue 语法代码进行转义,
// 在 src 下创建一个 App.vue
// kvite/src/App.vue
<template>
<div>
<button @click="pReduceState">减一</button>
hello {{ title }} world
<button @click="pAddState">加一</button>
</div >
</template >
<script>
import {reactive, toRefs} from "vue";
export default {
setup() {
const state = reactive({
title: 0,
});
const pAddState = () => {
state.title += 1;
};
const pReduceState = () => {
state.title -= 1;
};
return {
...toRefs(state),
pAddState,
pReduceState,
};
},
};
</script>
/**
* @description 手写 Vite
* @author oyzx
*
* node 服务器,处理浏览器加载各种资源的请求
*/
const fs = require('fs')
const path = require('path')
const Koa = require('koa')
const compilerSFC = require('@vue/compiler-sfc')
const compilerDOM = require('@vue/compiler-dom')
// 创建实例
const app = new Koa()
/**
* @description 将原来的裸地址改成相对地址
*/
const GRewriteImport = content => {
return content.replace(/ from ['"](.*)["']/g, (s1, s2) => {
if (s2.startsWith('.') || s2.startsWith('/') || s2.startsWith('../')) {
return s1
} else {
// 替换
return ` from '/@modules/${s2}'`
}
})
}
// 处理中间配置
// 处理路由
app.use(async ctx => {
const { url, query } = ctx
if (url === '/') {
ctx.type = 'text/html'
ctx.body = fs.readFileSync(path.join(__dirname, './index.html'), 'utf-8')
} else if (url.endsWith('.js')) {
ctx.type = 'application/javascript'
ctx.body = GRewriteImport(fs.readFileSync(path.join(__dirname, url), 'utf-8'))
} else if (url.startsWith('/@modules/')) {
// 裸模块
const vModuleName = url.replace('/@modules/', '')
// 接下来去 node_modules 文件夹下面去找
const vPrefix = path.join(__dirname, '/node_modules/', vModuleName)
// 接下来查找文件中的 package.json,根据 package.json 中 module 读取文件
const vModule = require(path.join(vPrefix, 'package.json')).module
const vFilepath = path.join(vPrefix, vModule)
// 获取具体文件
const vResult = GRewriteImport(fs.readFileSync(vFilepath, 'utf-8'))
ctx.type = 'application/javascript'
ctx.body = vResult
} else if (url.indexOf('.vue') > -1) {
// 获取加载文件路径
const vPath = path.join(__dirname, url.split('?')[0])
// 编译解析vue文件
const vResult = compilerSFC.parse(fs.readFileSync(vPath, 'utf-8'))
if (!query.type) {
// 将 .vue 文件中的 script信息 解析成 js 文件
// 获取脚本部分的内容
const vScriptContent = vResult.descriptor.script.content
// 替换文件中的 export default 导出文件,将其改成一个变量
const vScript = vScriptContent.replace('export default ', 'const __script = ')
ctx.type = 'application/javascript'
ctx.body = `
${GRewriteImport(vScript)}
import {render as __render} from '${url}?type=template'
__script.render = __render
export default __script
`
} else if (query.type === 'template') {
// 将 vue 中的 template 模板信息进行编译转义
const vTpl = vResult.descriptor.template.content
// 编译 render
const vRender = compilerDOM.compile(vTpl, { mode: 'module' }).code
ctx.type = 'application/javascript'
ctx.body = GRewriteImport(vRender)
}
}
})
app.listen(3000, () => {
console.log('kvite start');
})
- 保存,刷新浏览器即可看到信息