面向现代浏览器的一个更轻,更快的web应用开发工具,基于ECMAScript标准原生模块系统(ES Modules)实现。
Vite (法语意为 “快速的”,发音 /vit/
) 是一种新型前端构建工具,能够显著提升前端开发体验,它主要由两部分组成:
Vite 意在提供更开箱即用的配置,同时它的 插件 API 和 JavaScript API 带来了高度的可扩展性,并完全支持类型化
如果应用比较复杂,使用Webpack的开发过程相对没有那么丝滑
对于冷启动和vite启动的区别,vite官网是这么说的(以下摘于官网):
当冷启动开发服务器时,基于打包器的方式是在提供服务前,急切地抓取和构建你整个应用
vite通过在一开始将应用中的模块区分为依赖和源码两类,改进了开发服务器启动时间。
依赖:大多为纯JavaScript 并在开发时不会变动。一些较大的依赖(例如有上百个模块的组件库)处理的代价也很高。依赖也通常会以某些方式(例如 ESM 或者 CommonJS)被拆分到大量小模块中。
Vite 将会使用 esbuild 预构建依赖。Esbuild 使用 Go 编写,并且比以 JavaScript 编写的打包器预构建依赖快 10-100 倍。
源码:通常包含一些并非直接是 JavaScript 的文件,需要转换(例如 JSX,CSS 或者 Vue/Svelte 组件),时常会被编辑。同时,并不是所有的源码都需要同时被加载。(例如基于路由拆分的代码模块)。
Vite 以 原生 ESM 方式服务源码。这实际上是让浏览器接管了打包程序的部分工作:Vite 只需要在浏览器请求源码时进行转换并按需提供源码。根据情景动态导入的代码,即只在当前屏幕上实际使用时才会被处理。
当基于打包器启动时,编辑文件后将重新构建文件本身。显然我们不应该重新构建整个包,因为这样更新速度会随着应用体积增长而直线下降。
一些打包器的开发服务器将构建内容存入内存,这样它们只需要在文件更改时使模块图的一部分失活[1],但它也仍需要整个重新构建并重载页面。这样代价很高,并且重新加载页面会消除应用程序的当前状态,所以打包器支持了动态模块热重载(HMR):允许一个模块 “热替换” 它自己,而对页面其余部分没有影响。这大大改进了开发体验 - 然而,在实践中我们发现,即使是 HMR 更新速度也会随着应用程序规模的增长而显著下降。
在 Vite 中,HMR 是在原生 ESM 上执行的。当编辑一个文件时,Vite 只需要精确地使已编辑的模块与其最近的 HMR 边界之间的链失效(大多数时候只需要模块本身),使 HMR 更新始终快速,无论应用程序的大小。
Vite 同时利用 HTTP 头来加速整个页面的重新加载(再次让浏览器为我们做更多事情):源码模块的请求会根据 304 Not Modified
进行协商缓存,而依赖模块请求则会通过 Cache-Control: max-age=31536000,immutable
进行强缓存,因此一旦被缓存它们将不需要再次请求。
一旦你体验到 Vite 有多快,我们十分怀疑你是否愿意再忍受像曾经那样使用打包器开发。
Vite官方目前提供了一个比较简单的脚手架 create-vite-app
可以使用这个脚手架快速创建一个使用vite构建的vue.js应用
npm:
创建项目
npm init vite-app
启动项目
cd
npm run dev
yarn:
创建项目
yarn create vite-app
启动项目
cd
yarn dev
npm init 或者 yarn create 是这两个包管理工具提供的新功能,其内部就是自动去安装一个create-的模块(临时),然后自动执行这个模块中的bin
你还可以通过附加的命令行选项直接指定项目名称和你想要使用的模板。例如,要构建一个 Vite + Vue 项目,运行:
# npm 6.x
npm init @vitejs/app my-vue-app --template vue
# npm 7+, 需要额外的双横线:
npm init @vitejs/app my-vue-app -- --template vue
# yarn
yarn create @vitejs/app my-vue-app --template vue
支持的模板预设包括:
vanilla
vue
vue-ts
react
react-ts
preact
preact-ts
lit-element
lit-element-ts
打开生成的项目过后,你会发现是一个很普通的vue.js
应用,没有太多特殊的地方。
不过相比于之前的vue-cli
构建的项目或者是基于webpack
搭建的vue.js
项目,这里开发依赖非常简单,只有vite
和@vue/compiler-sfc
vite
就是我们今天要说的主角,而@vue/compiler-sfc
就是用来编译我们项目中.vue结尾的单文件组件(SFC
),它取代的就是vue.js 2.x
时使用的vue-template-compiler
再者就是Vite只支持Vue3.0版本 (实现原理了解以后,还可以改造vite支持vue2.0)
当我们执行vite serve
的时候,你会发现响应速度非常快,几乎就是秒开
可能单独体验你不会有太明显的感觉,你可以对比使用vue-cli-serive
(内部还是webpack)启动开发服务器
当我们对比使用vue-cli-service serve
的时候,你会有更明显的感觉。
因为Webpack Dev Server在启动的时候,需要先build一遍,而build的过程是需要耗费很多时间的。
这类工具的做法是将所有模块进行提前编译,打包进bundle里,换句话说,不管模块是否被执行,都要被编译和打包进bundle里,随着项目越来越大,打包后的bundle也越来越大,打包的速度自然也就越来越慢
而vite完全是不同的,当我们执行了vite serve
时,内部直接启动了Web server
并不会先编译所有的代码文件,仅仅是启动了web server
服务
对于vite来说,它利用现代浏览器原生支持ESM特性,省略了对模块的打包,对于需要编译的文件,vite采用的是另外一种模式:即时编译也就是说只有具体去请求某个文件时才会编译这个文件。
对于vite第一次启动的时候,vite也会进行一次简短的编译。这是由于我们的main.js
引入了vue
的模块,而vue的模块是存在于node_modules
中的,而node_modules
里面的模块会有个问题就是一个模块依赖另外一个模块,而我们一般不会去动里面的东西,vite会在node_modules\.vite_opt_cache
下生成一个依赖的缓存,而这个文件第一次被创建以后,只要配置没有发生变化是不会改变的,下次启动就会很迅速。
Vite 会将预构建的依赖缓存到
node_modules/.vite
。
它根据几个源来决定是否需要重新运行预构建步骤:
package.json
中的 dependencies
列表package-lock.json
, yarn.lock
,或者 pnpm-lock.yaml
vite.config.js
相关字段中配置过的只有当上面的一个步骤发生变化时,才需要重新运行预构建步骤。
如果出于某些原因,你想要强制 Vite 重新绑定依赖,你可以用 --force
命令行选项启动开发服务器,或者手动删除 node_modules/.vite
目录。
vite启动的时候,会将项目的根目录会作为静态文件服务器的根目录。
vite构建的项目目录:
index.html:
index.html文件请求
我们请求的html
文件就是根目录下的index.html
文件,我们可以看到请求过来的index.html
文件中的script
标签的type
属性为module
这个就是ESM
定义的一个标准,一旦这么写的时候,那么它加载的时候,它可以使用ESM
的标准,这样就可以使用import
去组织模块化,而这块不是vite
所实现的,这是浏览器自带的。这样main.js
就可以去加载一些模块文件。
原生 ES 引入不支持下面这样的裸模块导入:
import { someMethod } from 'my-dep'
上面的操作将在浏览器中抛出一个错误。Vite 将在服务的所有源文件中检测此类裸模块导入,并执行以下操作:
/node_modules/.vite/my-dep.js?v=f3sf2ebd
以便浏览器能够正确导入它们。同样也是模式的问题,热更新的时候,Vite只需要立即编译当前所修改的文件即可,然后传递给页面,所以响应速度非常快
而Webpack修改某个文件过后,会自动以这个文件为入口重写build一次,所有涉及到的依赖也会被重新加载一边,所以反应速度会非常慢。
vite的出现,引发了另外一个值得我们思考的问题:究竟还有没有必要打包应用
之前我们使用Webpack打包应用代码,使之称为一个bundle.js,主要有两个原因:
随着浏览器对ES标准支持的逐渐完善,第一个问题已经慢慢不存在了,大多数浏览器都是支持ES modules的
零散模块文件确实会产生大量的HTTP请求,而大量的HTTP请求在浏览器端就会并发请求资源的问题
并行请求就会产生因为域名连接数超限而被挂起等待一段时间。
在HTTP 1.1的标准下,每次请求都是单独建立TCP连接,经过完整的通讯过程,非常耗时。而且每次请求除了请求体中的内容,请求头也会包含很多数据,大量请求会耗费很多的资源。而在HTTP2.0也不复存在了,但是vite为了兼容性,还是选择了打包的方式。
vite的核心功能:Static Server + Compiler + HMR
创建js文件和package.json文件
npm init
创建cli.js文件
给webpack.json文件中增加bin配置指向cli.js
编写cli.js文件.(等会vite启动就使用cli.js文件里面编写实现vite的代码。)
安装koa依赖
npm install koa --save // 类似express
npm install koa-send --save //用于项目目录作为静态文件服务器的根目录
简单配置koa先让服务跑起来,并且能够访问到目录下的index.html
#!/usr/bin/env node
// 文件头,给linux系统用的,意思就是用node去执行当前这个文件脚本
// 1. 将当前项目目录作为静态文件服务器的根目录
// 用koa-send 去实现将当前目录作为静态文件服务器的根目录,因为它会有一些中间件。static封装的太彻底,没法简单修改
const send = require('koa-send');
// 这里web服务器用koa去做,vite也用的koa,类似express
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) =>{
ctx.body = 'my-vite';
// 将项目目录作为静态文件服务器的根目录,并自动请求index.html文件
// 因为有的路径为不完整的路径,所以通过send的第三个参数做一些处理
await send(ctx, ctx.path, {
root: process.cwd(), index: 'index.html'});// 有可能还要额外处理相应结果
await next();
})
app.listen(3080);
console.log('Server-running-@-http://localhost:3080');
在删除vite依赖的项目中将my-vite
(我们编写的实现vite)文件夹作为本地npm依赖进行安装,并更改启动命令配置
npm install …/my-vite
此时node_modules/.bin
文件夹下有了一个my-vite
的命令文件
更改package.json
配置
测试
这里页面请求成功了,但是报错:
Uncaught TypeError: Failed to resolve module specifier “vue”. Relative references must start with either “/”, “./”, or “…/”.
这就是因为请求的这个页面它里面调用了main.js
这个文件这个文件里面又
import
了vue
这个文件import { createApp } from 'vue'
,原生 ES 引入不支持下面这样的裸模块导入,所以它会报错,它会找不到这个模块。我们接下来可以使用Koa提供的中间件去处理这个问题。
HTML页面请求成功
分析这个报错的原因,因为我们一般开发当中,如果使用import { createApp } from 'vue'
, 我们是知道它会去项目目录里面的node_module/vue/package.json
文件里面去找对应的 module
配置,然后在dist
里面调相应的js文件。
所以如果人工去修改这个问题的话,只需要将dist下对应文件的路径配置在Import语句里即可,比如:import { createApp } from '../node_modules/vue/dist/vue.runtime.esm-bundler.js'
,当然这是不行的,我们更希望开发者能够更友好和简单的去引入模块
替换代码中的特殊位置
我们可以看到vite内部对于引入vue做的操作是将引入的目录改为@modules/vue.js
我们也来实现这样的操作,我们可以简单用正则表达式来实现。
因为send
的时候,它已经把那个文件读出来了,而且将结果放在了ctx.body
当中,我们可以输出一下ctx.body
看一下,它输出了一个ReadStream
文件读取流。我们就要把这个读取流里面的内容都拿出来。这里我们可以用一个处理函数去处理一下,然后转成字符串
app.use(async (ctx,next) => {
console.log(ctx.body)
})
// 输出结果
ReadStream {
_readableState: ReadableState {
objectMode: false,
highWaterMark: 65536,
buffer: BufferList {
head: null, tail: null, length: 0 },
length: 0,
pipes: [],
flowing: null,
ended: false,
endEmitted: false,
reading: false,
sync: true,
needReadable: false,
emittedReadable: false,
readableListening: false,
resumeScheduled: false,
errorEmitted: false,
emitClose: true,
autoDestroy: false,
destroyed: false,
errored: null,
closed: false,
closeEmitted: false,
defaultEncoding: 'utf8',
awaitDrainWriters: null,
multiAwaitDrain: false,
readingMore: false,
decoder: null,
encoding: null,
[Symbol(kPaused)]: null
},
_events: [Object: null prototype] {
end: [Function (anonymous)],
error: [Function: bound onceWrapper] {
listener: [Function (anonymous)] }
},
_eventsCount: 2,
_maxListeners: undefined,
path: 'D:\\Iprogram\\programs\\vite\\vite-myself\\index.html',
fd: null,
flags: 'r',
mode: 438,
start: undefined,
end: Infinity,
autoClose: true,
pos: undefined,
bytesRead: 0,
closed: false,
[Symbol(kFs)]: {
appendFile: [Function: appendFile],
appendFileSync: [Function: appendFileSync],
access: [Function: access],
accessSync: [Function: accessSync],
chown: [Function: chown],
chownSync: [Function: chownSync],
chmod: [Function: chmod],
chmodSync: [Function: chmodSync],
close: [Function: close],
closeSync: [Function: closeSync],
copyFile: [Function: copyFile],
copyFileSync: [Function: copyFileSync],
createReadStream: [Function: createReadStream],
createWriteStream: [Function: createWriteStream],
exists: [Function: exists],
existsSync: [Function: existsSync],
fchown: [Function: fchown],
fchownSync: [Function: fchownSync],
fchmod: [Function: fchmod],
fchmodSync: [Function: fchmodSync],
fdatasync: [Function: fdatasync],
fdatasyncSync: [Function: fdatasyncSync],
fstat: [Function: fstat],
fstatSync: [Function: fstatSync],
fsync: [Function: fsync],
fsyncSync: [Function: fsyncSync],
ftruncate: [Function: ftruncate],
ftruncateSync: [Function: ftruncateSync],
futimes: [Function: futimes],
futimesSync: [Function: futimesSync],
lchown: [Function: lchown],
lchownSync: [Function: lchownSync],
lchmod: undefined,
lchmodSync: undefined,
link: [Function: link],
linkSync: [Function: linkSync],
lstat: [Function: lstat],
lstatSync: [Function: lstatSync],
lutimes: [Function: lutimes],
lutimesSync: [Function: lutimesSync],
mkdir: [Function: mkdir],
mkdirSync: [Function: mkdirSync],
mkdtemp: [Function: mkdtemp],
mkdtempSync: [Function: mkdtempSync],
open: [Function: open],
openSync: [Function: openSync],
opendir: [Function: opendir],
opendirSync: [Function: opendirSync],
readdir: [Function: readdir],
readdirSync: [Function: readdirSync],
read: [Function: read],
readSync: [Function: readSync],
readv: [Function: readv],
readvSync: [Function: readvSync],
readFile: [Function: readFile],
readFileSync: [Function: readFileSync],
readlink: [Function: readlink],
readlinkSync: [Function: readlinkSync],
realpath: [Function: realpath] {
native: [Function (anonymous)] },
realpathSync: [Function: realpathSync] {
native: [Function (anonymous)] },
rename: [Function: rename],
renameSync: [Function: renameSync],
rm: [Function: rm],
rmSync: [Function: rmSync],
rmdir: [Function: rmdir],
rmdirSync: [Function: rmdirSync],
stat: [Function: stat],
statSync: [Function: statSync],
symlink: [Function: symlink],
symlinkSync: [Function: symlinkSync],
truncate: [Function: truncate],
truncateSync: [Function: truncateSync],
unwatchFile: [Function: unwatchFile],
unlink: [Function: unlink],
unlinkSync: [Function: unlinkSync],
utimes: [Function: utimes],
utimesSync: [Function: utimesSync],
watch: [Function: watch],
watchFile: [Function: watchFile],
writeFile: [Function: writeFile],
writeFileSync: [Function: writeFileSync],
write: [Function: write],
writeSync: [Function: writeSync],
writev: [Function: writev],
writevSync: [Function: writevSync],
Dir: [class Dir],
Dirent: [class Dirent],
Stats: [Function: Stats],
ReadStream: [Getter/Setter],
WriteStream: [Getter/Setter],
FileReadStream: [Getter/Setter],
FileWriteStream: [Getter/Setter],
_toUnixTimestamp: [Function: toUnixTimestamp],
F_OK: 0,
R_OK: 4,
W_OK: 2,
X_OK: 1,
constants: [Object: null prototype] {
UV_FS_SYMLINK_DIR: 1,
UV_FS_SYMLINK_JUNCTION: 2,
O_RDONLY: 0,
O_WRONLY: 1,
O_RDWR: 2,
UV_DIRENT_UNKNOWN: 0,
UV_DIRENT_FILE: 1,
UV_DIRENT_DIR: 2,
UV_DIRENT_LINK: 3,
UV_DIRENT_FIFO: 4,
UV_DIRENT_SOCKET: 5,
UV_DIRENT_CHAR: 6,
UV_DIRENT_BLOCK: 7,
S_IFMT: 61440,
S_IFREG: 32768,
S_IFDIR: 16384,
S_IFCHR: 8192,
S_IFLNK: 40960,
O_CREAT: 256,
O_EXCL: 1024,
UV_FS_O_FILEMAP: 536870912,
O_TRUNC: 512,
O_APPEND: 8,
F_OK: 0,
R_OK: 4,
W_OK: 2,
X_OK: 1,
UV_FS_COPYFILE_EXCL: 1,
COPYFILE_EXCL: 1,
UV_FS_COPYFILE_FICLONE: 2,
COPYFILE_FICLONE: 2,
UV_FS_COPYFILE_FICLONE_FORCE: 4,
COPYFILE_FICLONE_FORCE: 4
},
promises: [Getter]
},
[Symbol(kCapture)]: false,
[Symbol(kIsPerformingIO)]: false
}
ReadStream {
_readableState: ReadableState {
objectMode: false,
highWaterMark: 65536,
buffer: BufferList {
head: null, tail: null, length: 0 },
length: 0,
pipes: [],
flowing: null,
ended: false,
endEmitted: false,
reading: false,
sync: true,
needReadable: false,
emittedReadable: false,
readableListening: false,
resumeScheduled: false,
errorEmitted: false,
emitClose: true,
autoDestroy: false,
destroyed: false,
errored: null,
closed: false,
closeEmitted: false,
defaultEncoding: 'utf8',
awaitDrainWriters: null,
multiAwaitDrain: false,
readingMore: false,
decoder: null,
encoding: null,
[Symbol(kPaused)]: null
},
_events: [Object: null prototype] {
end: [Function (anonymous)],
error: [Function: bound onceWrapper] {
listener: [Function (anonymous)] }
},
_eventsCount: 2,
_maxListeners: undefined,
path: 'D:\\Iprogram\\programs\\vite\\vite-myself\\src\\main.js',
fd: null,
flags: 'r',
mode: 438,
start: undefined,
end: Infinity,
autoClose: true,
pos: undefined,
bytesRead: 0,
closed: false,
[Symbol(kFs)]: {
appendFile: [Function: appendFile],
appendFileSync: [Function: appendFileSync],
access: [Function: access],
accessSync: [Function: accessSync],
chown: [Function: chown],
chownSync: [Function: chownSync],
chmod: [Function: chmod],
chmodSync: [Function: chmodSync],
close: [Function: close],
closeSync: [Function: closeSync],
copyFile: [Function: copyFile],
copyFileSync: [Function: copyFileSync],
createReadStream: [Function: createReadStream],
createWriteStream: [Function: createWriteStream],
exists: [Function: exists],
existsSync: [Function: existsSync],
fchown: [Function: fchown],
fchownSync: [Function: fchownSync],
fchmod: [Function: fchmod],
fchmodSync: [Function: fchmodSync],
fdatasync: [Function: fdatasync],
fdatasyncSync: [Function: fdatasyncSync],
fstat: [Function: fstat],
fstatSync: [Function: fstatSync],
fsync: [Function: fsync],
fsyncSync: [Function: fsyncSync],
ftruncate: [Function: ftruncate],
ftruncateSync: [Function: ftruncateSync],
futimes: [Function: futimes],
futimesSync: [Function: futimesSync],
lchown: [Function: lchown],
lchownSync: [Function: lchownSync],
lchmod: undefined,
lchmodSync: undefined,
link: [Function: link],
linkSync: [Function: linkSync],
lstat: [Function: lstat],
lstatSync: [Function: lstatSync],
lutimes: [Function: lutimes],
lutimesSync: [Function: lutimesSync],
mkdir: [Function: mkdir],
mkdirSync: [Function: mkdirSync],
mkdtemp: [Function: mkdtemp],
mkdtempSync: [Function: mkdtempSync],
open: [Function: open],
openSync: [Function: openSync],
opendir: [Function: opendir],
opendirSync: [Function: opendirSync],
readdir: [Function: readdir],
readdirSync: [Function: readdirSync],
read: [Function: read],
readSync: [Function: readSync],
readv: [Function: readv],
readvSync: [Function: readvSync],
readFile: [Function: readFile],
readFileSync: [Function: readFileSync],
readlink: [Function: readlink],
readlinkSync: [Function: readlinkSync],
realpath: [Function: realpath] {
native: [Function (anonymous)] },
realpathSync: [Function: realpathSync] {
native: [Function (anonymous)] },
rename: [Function: rename],
renameSync: [Function: renameSync],
rm: [Function: rm],
rmSync: [Function: rmSync],
rmdir: [Function: rmdir],
rmdirSync: [Function: rmdirSync],
stat: [Function: stat],
statSync: [Function: statSync],
symlink: [Function: symlink],
symlinkSync: [Function: symlinkSync],
truncate: [Function: truncate],
truncateSync: [Function: truncateSync],
unwatchFile: [Function: unwatchFile],
unlink: [Function: unlink],
unlinkSync: [Function: unlinkSync],
utimes: [Function: utimes],
utimesSync: [Function: utimesSync],
watch: [Function: watch],
watchFile: [Function: watchFile],
writeFile: [Function: writeFile],
writeFileSync: [Function: writeFileSync],
write: [Function: write],
writeSync: [Function: writeSync],
writev: [Function: writev],
writevSync: [Function: writevSync],
Dir: [class Dir],
Dirent: [class Dirent],
Stats: [Function: Stats],
ReadStream: [Getter/Setter],
WriteStream: [Getter/Setter],
FileReadStream: [Getter/Setter],
FileWriteStream: [Getter/Setter],
_toUnixTimestamp: [Function: toUnixTimestamp],
F_OK: 0,
R_OK: 4,
W_OK: 2,
X_OK: 1,
constants: [Object: null prototype] {
UV_FS_SYMLINK_DIR: 1,
UV_FS_SYMLINK_JUNCTION: 2,
O_RDONLY: 0,
O_WRONLY: 1,
O_RDWR: 2,
UV_DIRENT_UNKNOWN: 0,
UV_DIRENT_FILE: 1,
UV_DIRENT_DIR: 2,
UV_DIRENT_LINK: 3,
UV_DIRENT_FIFO: 4,
UV_DIRENT_SOCKET: 5,
UV_DIRENT_CHAR: 6,
UV_DIRENT_BLOCK: 7,
S_IFMT: 61440,
S_IFREG: 32768,
S_IFDIR: 16384,
S_IFCHR: 8192,
S_IFLNK: 40960,
O_CREAT: 256,
O_EXCL: 1024,
UV_FS_O_FILEMAP: 536870912,
O_TRUNC: 512,
O_APPEND: 8,
F_OK: 0,
R_OK: 4,
W_OK: 2,
X_OK: 1,
UV_FS_COPYFILE_EXCL: 1,
COPYFILE_EXCL: 1,
UV_FS_COPYFILE_FICLONE: 2,
COPYFILE_FICLONE: 2,
UV_FS_COPYFILE_FICLONE_FORCE: 4,
COPYFILE_FICLONE_FORCE: 4
},
promises: [Getter]
},
[Symbol(kCapture)]: false,
[Symbol(kIsPerformingIO)]: false
}
文件读取流处理函数:
// 文件读取流处理函数
const streamToString = stream => {
// 读取操作涉及到异步,所以使用Promise
return new Promise((resolve, reject) => {
const chunks = [];
// 流读取因为是一个片段一个片段的,每次片段会触发一个data事件
// 每次事件里我们会拿到一个chunk,我们push到chunks里面
stream.on('data', chunk =>{
chunks.push(chunk)
});
// 读取结束触发end事件,将Buffer处理过后chunks的转成字符串,resolve返回出去
stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
// 如果出错直接传递reject
stream.on('error', reject);
})
}
我们可以输出一下使用函数处理过后的ctx.body
,我们就可以拿到正确的信息,我们如果要替换里面的内容,可以做一些操作。
但是每次请求可能是CSS,HTML,JS等等,我们不能每次都转成字符串来处理,实际上我们只需要将JS转一下再做处理即可。这里我们使用正则去处理一下
app.use(async (ctx, next) =>{
if(ctx.type === 'application/javascript') {
const contexts = await streamToString(ctx.body);
// 使用正则表达式处理成@module/xxx.js
// import { createApp } from 'vue'
// import { createApp } from '/@modules/vue'
ctx.body = contexts.replace(/(from\s+['"])(?![\.\/])/g, '$1/@modules/')
}
})
输出:
但是这样会导致更多的错误,因为这样去找@modules
下的vue
是找不到的,我们就要去做请求路径的重写
重写请求路径
// 重写请求路径 /@modules/xxx => /node_modules/
app.use(async (ctx, next) => {
// 只要开头是/@modules/的我们要重写
if (ctx.path.startsWith('/@modules/')) {
const moduleName = ctx.path.substr(10); // => 这步只是处理了vue
const modulePkg = require(path.join(cwd, 'node_modules', moduleName, 'package.json')); // 拿到package.json
// 拿到真实路径
ctx.path = path.join('/node_modules', moduleName, modulePkg.module);
}
})
这样vue就可以正常访问了
但是报了其他的错误,这是因为vue文件去掉了其他的模块。
.vue文件请求的处理,即时编译
实现即时编译肯定不能自己去实现,我们用到vue的@vue/compiler-sfc
给my-vite安装
npm i @vue/compiler-sfc
// .vue文件请求的处理,即时编译
app.use(async (ctx, next) =>{
if(ctx.path.endsWith('.vue')) {
// 编译我们肯定不能自己去写,我们要用到@vue/compiler-sfc这个模块
// parse方法接收单文件组件的内容--源代码
const contents = await streamToString(ctx.body);
// 它里面有个descriptor其实就是解析过后的代码
const {
descriptor } = compilerSfc.parse(contents);
// 输出一下descriptor
console.log(descriptor);
}
await next();
})
// 输出结果
{
filename: 'anonymous.vue',
source: '\n' +
' \n' +
' \n' +
'\n' +
'\n' +
'\n',
template: {
type: 'template',
content: '\n' +
' \n' +
' \n',
loc: {
source: '\n' +
' \n' +
' \n',
start: [Object],
end: [Object]
},
attrs: {
},
ast: {
type: 1,
ns: 0,
tag: 'template',
tagType: 0,
props: [],
isSelfClosing: false,
children: [Array],
loc: [Object],
codegenNode: undefined
},
map: {
version: 3,
sources: [Array],
names: [],
mappings: ';EACE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;EAC7C,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC',
file: 'anonymous.vue',
sourceRoot: '',
sourcesContent: [Array]
}
},
script: {
type: 'script',
content: '\n' +
"import HelloWorld from './components/HelloWorld.vue'\n" +
'\n' +
'export default {\n' +
" name: 'App',\n" +
' components: {\n' +
' HelloWorld\n' +
' }\n' +
'}\n',
loc: {
source: '\n' +
"import HelloWorld from './components/HelloWorld.vue'\n" +
'\n' +
'export default {\n' +
" name: 'App',\n" +
' components: {\n' +
' HelloWorld\n' +
' }\n' +
'}\n',
start: [Object],
end: [Object]
},
attrs: {
},
map: {
version: 3,
sources: [Array],
names: [],
mappings: ';AAMA,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;;AAEnD,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE;EACb,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;EACX,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE;IACV,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;EACX;AACF',
file: 'anonymous.vue',
sourceRoot: '',
sourcesContent: [Array]
}
},
scriptSetup: null,
styles: [],
customBlocks: [],
cssVars: [],
slotted: false
}
它打印了一个大对象,里面的template
其实就是我们在单文件组件的内容,还有个script
是一个纯js的函数,我们只需要将script
返回回去就好了
// .vue文件请求的处理,即时编译
app.use(async (ctx, next) =>{
if(ctx.path.endsWith('.vue')) {
// 编译我们肯定不能自己去写,我们要用到@vue/compiler-sfc这个模块
// parse方法接收单文件组件的内容--源代码
const contents = await streamToString(ctx.body);
// 它里面有个descriptor其实就是解析过后的代码
const {
descriptor } = compilerSfc.parse(contents);
// 这里把ctx.body设置为js脚本,你就要去设置一下contentType
// 这个contentType是服务端告诉客户端我给你返回的是什么类型的资源
ctx.type = 'application/javascript';
// 这样就和下面的中间件产生了冲突,现在的ctx.body是一个字符串而不是一个流,做一个处理
// 必须先转成Buffer,再转成读取流
ctx.body = Readable.from(Buffer.from(descriptor.script.content));
}
await next();
})
这样App.vue就是我们返回的script里面的内容
但是这样它缺少了很重要的组成部分template
来作为我们单文件组件的render函数
这里我们可以看一下vite的处理方法,他就是将export default
替换为了一个变量的定义,然后在底下拿了一下这个render
let code = descriptor.script.content;
code = code.replace(/export\s+default\s+/, 'const __script = ');
code += `
import { render as __render } from "${ctx.path}?type=template"
__script.render = __render
export default __script`
最后再使用一下code
ctx.body = Readable.from(Buffer.from(code));
这里我们要做一下判断,如果请求是template
的时候,我们要返回render
函数
if(ctx.path.endsWith('.vue')) {
// 编译我们肯定不能自己去写,我们要用到@vue/compiler-sfc这个模块
// parse方法接收单文件组件的内容--源代码
const contents = await streamToString(ctx.body);
// 它里面有个descriptor其实就是解析过后的代码
const { descriptor } = compilerSfc.parse(contents);
let code;
if (ctx.query.type === undefined) {
code = descriptor.script.content;
code = code.replace(/export\s+default\s+/, 'const __script = ');
code += `
import { render as __render } from "${ctx.path}?type=template"
__script.render = __render
export default __script`
} else if (ctx.query.type === 'template') {
// 如果请求是template的话
// source就是我们要传递的模板内容
const templateRender = compilerSfc.compileTemplate({ source: descriptor.template.content });
code = templateRender.code;
}
// 这里把ctx.body设置为js脚本,你就要去设置一下contentType
// 这个contentType是服务端告诉客户端我给你返回的是什么类型的资源
ctx.type = 'application/javascript';
// 这样就和下面的中间件产生了冲突,现在的ctx.body是一个字符串而不是一个流,做一个处理
// 必须先转成Buffer,再转成读取流
ctx.body = Readable.from(Buffer.from(code));
}
.
这样页面就可以请求到这个模板函数,拿到这个render
移除掉css调用和img图片的调用,程序已经能跑起来了
因为没做css的处理和img的处理,在vite示例项目中,它是调用了css和img
整体cli.js的展示
#!/usr/bin/env node
const send = require('koa-send');
const Koa = require('koa');
const app = new Koa();
const cwd = process.cwd();
const path = require('path');
const compilerSfc = require('@vue/compiler-sfc');
const {
Readable } = require('stream');
// 重写请求路径 /@modules/xxx => /node_modules/
app.use(async (ctx, next) => {
if (ctx.path.startsWith('/@modules/')) {
const moduleName = ctx.path.substr(10);
const modulePkg = require(path.join(cwd, 'node_modules', moduleName, 'package.json'));
ctx.path = path.join('/node_modules', moduleName, modulePkg.module);
}
await next();
})
// 根据请求路径得到相应文件
app.use(async (ctx, next) =>{
// ctx.body = 'my-vite';
await send(ctx, ctx.path, {
root: cwd, index: 'index.html'});
await next();
})
// .vue文件请求的处理,即时编译
app.use(async (ctx, next) =>{
if(ctx.path.endsWith('.vue')) {
const contents = await streamToString(ctx.body);
const {
descriptor } = compilerSfc.parse(contents);
let code;
if (ctx.query.type === undefined) {
code = descriptor.script.content;
code = code.replace(/export\s+default\s+/, 'const __script = ');
code += `
import { render as __render } from "${
ctx.path}?type=template"
__script.render = __render
export default __script`
} else if (ctx.query.type === 'template') {
const templateRender = compilerSfc.compileTemplate({
source: descriptor.template.content });
code = templateRender.code;
}
ctx.type = 'application/javascript';
ctx.body = Readable.from(Buffer.from(code));
}
await next();
})
// 替换代码中的特殊位置
app.use(async (ctx, next) =>{
if(ctx.type === 'application/javascript') {
const contents = await streamToString(ctx.body);
ctx.body = contents
.replace(/(from\s+['"])(?![\.\/])/g, '$1/@modules/')
.replace(/process.env.NODE_ENV/g, '"production"');
}
await next();
})
// 文件读取流处理函数
const streamToString = stream => {
return new Promise((resolve, reject) => {
const chunks = [];
stream.on('data', chunk =>{
chunks.push(chunk);
});
stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
stream.on('error', reject);
})
}
app.listen(3080);
console.log('Server-running-@-http://localhost:3080');
vite带来的优势主要体现在提升开发者在开发过程中的体验