我的编程空间,编程开发者的网络收藏夹
学习永远不晚

学习Vite的原理

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

学习Vite的原理

1. 概述

Vite是一个更轻、更快的web应用开发工具,面向现代浏览器。底层基于ECMAScript标准原生模块系统ES Module实现。他的出现是为了解决webpack冷启动时间过长以及Webpack HMR热更新反应速度慢等问题。

默认情况下Vite创建的项目是一个普通的Vue3应用,相比基于Vue-cli创建的应用少了很多配置文件和依赖。

Vite创建的项目所需要的开发依赖非常少,只有Vite@vue/compiler-sfc。这里面Vite是一个运行工具,compiler-sfc则是为了编译.vue结尾的单文件组件。在创建项目的时候通过指定不同的模板也可以支持使用其他框架例如React。项目创建完成之后可以通过两个命令启动和打包。

# 开启服务器
vite serve
# 打包
vite build

正是因为Vite启动的web服务不需要编译打包,所以启动的速度特别快,调试阶段大部分运行的代码都是你在编辑器中书写的代码,这相比于webpack的编译后再呈现确实要快很多。当然生产环境还是需要打包的,毕竟很多时候我们使用的最新ES规范在浏览器中还没有被支持,Vite的打包过程和webpack类似会将所有文件进行编译打包到一起。对于代码切割的需求Vite采用的是原生的动态导入来实现的,所以打包结果只能支持现代浏览器,如果需要兼容老版本浏览器可以引入Polyfill

使用Webpack打包除了因为浏览器环境并不支持模块化和新语法外,还有就是模块文件会产生大量的http请求。如果你使用模块化的方式开发,一个页面就会有十几甚至几十个模块,而且很多时候会出现几kb的文件,打开一个页面要加载几十个js资源这显然是不合理的。

  • Vite创建的项目几乎不需要额外的配置默认已经支持TS、Less, Sass,Stylus,postcss了,但是需要单独安装对应的编译器,同时默认还支持jsx和Web Assembly。
  • Vite带来的好处是提升开发者在开发过程中的体验,web开发服务器不需要等待即可立即启动,模块热更新几乎是实时的,所需的文件会按需编译,避免编译用不到的文件。并且开箱即用避免loader及plugins的配置。
  • Vite的核心功能包括开启一个静态的web服务器,能够编译单文件组件并且提供HMR功能。当启动vite的时候首先会将当前项目目录作为静态服务器的根目录,静态服务器会拦截部分请求,当请求单文件的时候会实时编译,以及处理其他浏览器不能识别的模块,通过websocket实现hmr。

2. 实现静态测试服务器

首先实现一个能够开启静态web服务器的命令行工具。vite1.x内部使用的是Koa来实现静态服务器。(ps:node命令行工具可以查看我之前的文章,这里就不介绍了,直接贴代码)。

npm init
npm install koa koa-send -D

工具bin的入口文件设置为本地的index.js

#!/usr/bin/env node

const Koa = require('koa')
const send = require('koa-send')

const app = new Koa()

// 开启静态文件服务器
app.use(async (ctx, next) => {
    // 加载静态文件
    await send(ctx, ctx.path, { root: process.cwd(), index: 'index.html'})
    await next()
})

app.listen(5000)

console.log('服务器已经启动 http://localhost:5000')

这样就编写好了一个node静态服务器的工具。

3. 处理第三方模块

我们的做法是当代码中使用了第三方模块(node_modules中的文件),可以通过修改第三方模块的路径给他一个标识,然后在服务器中拿到这个标识来处理这个模块。

首先需要修改第三方模块的路径,这里需要一个新的中间件来实现。判断一下当前返回给浏览器的文件是否是javascript,只需要看响应头中的content-type。如果是javascript需要找到这个文件中引入的模块路径。ctx.body就是返回给浏览器的内容文件。这里的数据是一个stream,需要转换成字符串来处理。

const stream2string = (stream) => {
    return new Promise((resolve, reject) => {
        const chunks = [];
        stream.on('data', chunk => {chunks.push(chunk)})
        stream.on('end', () => { resolve(Buffer.concat(chunks).toString('utf-8'))})
        stream.on('error', reject)
    })
}

// 修改第三方模块路径
app.use(async (ctx, next) => {
    if (ctx.type === 'application/javascript') {
        const contents = await stream2string(ctx.body);
        // 将body中导入的路径修改一下,重新赋值给body返回给浏览器
        // import vue from 'vue', 匹配到from '修改为from '@modules/
        ctx.body = contents.replace(/(from\s+['"])(?![\.\/])/g, '$1/@modules/');
    }
})

接着开始加载第三方模块, 这里同样需要一个中间件,判断请求路径是否是修改过的@module开头,如果是的话就去node_modules里面加载对应的模块返回给浏览器。这个中间件要放在静态服务器之前。

// 加载第三方模块
app.use(async (ctx, next) => {
    if (ctx.path.startsWith('/@modules/')) {
        // 截取模块名称
        const moduleName = ctx.path.substr(10);
    }
})

拿到模块名称之后需要获取模块的入口文件,这里要获取的是ES Module模块的入口文件,需要先找到这个模块的package.json然后再获取这个package.json中的module字段的值也就是入口文件。

// 找到模块路径
const pkgPath = path.join(process.pwd(), 'node_modules', moduleName, 'package.json');
const pkg = require(pkgPath);
// 重新给ctx.path赋值,需要重新设置一个存在的路径,因为之前的路径是不存在的
ctx.path = path.join('/node_modules', moduleName, pkg.module);
// 执行下一个中间件
awiat next();

这样浏览器请求进来的时候虽然是@modules路径,但是在加载之前将path路径修改为了node_modules中的路径,这样在加载的时候就会去node_modules中获取文件,将加载的内容响应给浏览器。

加载第三方模块:


app.use(async (ctx, next) => {
    if (ctx.path.startsWith('/@modules/')) {
        // 截取模块名称
        const moduleName = ctx.path.substr(10);
        // 找到模块路径
        const pkgPath = path.join(process.pwd(), 'node_modules', moduleName, 'package.json');
        const pkg = require(pkgPath);
        // 重新给ctx.path赋值,需要重新设置一个存在的路径,因为之前的路径是不存在的
        ctx.path = path.join('/node_modules', moduleName, pkg.module);
        // 执行下一个中间件
        awiat next();
    }
})

4. 单文件组件处理

之前说过浏览器是没办法处理.vue资源的, 浏览器只能识别js、css等常用资源,所以其他类型的资源都需要在服务端处理。当请求单文件组件的时候需要在服务器将单文件组件编译成js模块返回给浏览器。

所以这里当浏览器第一次请求App.vue的时候,服务器会把单文件组件编译成一个对象,先加载这个组件,然后再创建一个对象。

import Hello from './class="lazy" data-src/components/Hello.vue'
const __script = {
    name: "App",
    components: {
        Hello
    }
}

接着再去加载入口文件,这次会告诉服务器编译一下这个单文件组件的模板,返回一个render函数。然后将render函数挂载到刚创建的组件选项对象上,最后导出选项对象。

import { render as __render } from '/class="lazy" data-src/App.vue?type=template'
__script.render = __render
__script.__hmrId = '/class="lazy" data-src/App.vue'
export default __script

也就是说vite会发送两次请求,第一次请求会编译单文件文件,第二次请求是编译单文件模板返回一个render函数。

编译单文件选项:

首先来实现一下第一次请求单文件的情况。需要把单文件组件编译成一个选项,这里同样用一个中间件来实现。这个功能要在处理静态服务器之后,处理第三方模块路径之前。

首先需要对单文件组件进行编译需要借助compiler-sfc

// 处理单文件组件
app.use(async (ctx, next) => {
    if (ctx.path.endsWith('.vue')) {
        // 获取响应文件内容,转换成字符串
        const contents = await streamToString(ctx.body);
        // 编译文件内容
        const { descriptor } = compilerSFC.parse(contents);
        // 定义状态码
        let code;
        // 不存在type就是第一次请求
        if (!ctx.query.type) {
            code = descriptor.script.content;
            // 这里的code格式是, 需要改造成我们前面贴出来的vite中的样子
            // import Hello from './components/Hello.vue'
            // export default {
            //      name: 'App',
            //      components: {
            //          Hello
            //      }
            //  }
            // 改造code的格式,将export default 替换为const __script =
            code = code.relace(/export\s+default\s+/g, 'const __script = ')
            code += `
                import { render as __render } from '${ctx.path}?type=template'
                __script.rener = __render
                export default __script
            `
        }
        // 设置浏览器响应头为js
        ctx.type = 'application/javascript'
        // 将字符串转换成数据流传给下一个中间件。
        ctx.body = stringToStream(code);
    }
    await next()
})

const stringToStream = text => {
    const stream = new Readable();
    stream.push(text);
    stream.push(null);
    return stream;
}
npm install @vue/compiler-sfc -D

接着我们再来处理单文件组件的第二次请求,第二次请求url会带上type=template参数,需要将单文件组件模板编译成render函数。

首先需要判断当前请求中有没有type=template

if (!ctx.query.type) {
    ...
} else if (ctx.query.type === 'template') {
    // 获取编译后的对象 code就是render函数
    const templateRender = compilerSFC.compileTemplate({ source: descriptor.template.content })
    // 将render函数赋值给code返回给浏览器
    code = templateRender.code
}

这里还要处理一下工具中的process.env,因为这些代码会返回到浏览器中运行,如果不处理会默认为node导致运行失败。可以在修改第三方模块路径的中间件中修改,修改完路径之后再添加一条修改process.env。

// 修改第三方模块路径
app.use(async (ctx, next) => {
    if (ctx.type === 'application/javascript') {
        const contents = await stream2string(ctx.body);
        // 将body中导入的路径修改一下,重新赋值给body返回给浏览器
        // import vue from 'vue', 匹配到from '修改为from '@modules/
        ctx.body = contents.replace(/(from\s+['"])(?![\.\/])/g, '$1/@modules/').replace(/process\.env\.NODE_ENV/g, '"development"');
    }
})

至此就实现了一个简版的vite,当然这里我们只演示了.vue文件,对于css,less等其他资源都没有处理,不过方法都是类似的,感兴趣的同学可以自行实现。

#!/usr/bin/env node

const path = require('path')
const { Readable } = require('stream)
const Koa = require('koa')
const send = require('koa-send')
const compilerSFC = require('@vue/compiler-sfc')

const app = new Koa()

const stream2string = (stream) => {
    return new Promise((resolve, reject) => {
        const chunks = [];
        stream.on('data', chunk => {chunks.push(chunk)})
        stream.on('end', () => { resolve(Buffer.concat(chunks).toString('utf-8'))})
        stream.on('error', reject)
    })
}

const stringToStream = text => {
    const stream = new Readable();
    stream.push(text);
    stream.push(null);
    return stream;
}

// 加载第三方模块
app.use(async (ctx, next) => {
    if (ctx.path.startsWith('/@modules/')) {
        // 截取模块名称
        const moduleName = ctx.path.substr(10);
        // 找到模块路径
        const pkgPath = path.join(process.pwd(), 'node_modules', moduleName, 'package.json');
        const pkg = require(pkgPath);
        // 重新给ctx.path赋值,需要重新设置一个存在的路径,因为之前的路径是不存在的
        ctx.path = path.join('/node_modules', moduleName, pkg.module);
        // 执行下一个中间件
        awiat next();
    }
})

// 开启静态文件服务器
app.use(async (ctx, next) => {
    // 加载静态文件
    await send(ctx, ctx.path, { root: process.cwd(), index: 'index.html'})
    await next()
})

// 处理单文件组件
app.use(async (ctx, next) => {
    if (ctx.path.endsWith('.vue')) {
        // 获取响应文件内容,转换成字符串
        const contents = await streamToString(ctx.body);
        // 编译文件内容
        const { descriptor } = compilerSFC.parse(contents);
        // 定义状态码
        let code;
        // 不存在type就是第一次请求
        if (!ctx.query.type) {
            code = descriptor.script.content;
            // 这里的code格式是, 需要改造成我们前面贴出来的vite中的样子
            // import Hello from './components/Hello.vue'
            // export default {
            //      name: 'App',
            //      components: {
            //          Hello
            //      }
            //  }
            // 改造code的格式,将export default 替换为const __script =
            code = code.relace(/export\s+default\s+/g, 'const __script = ')
            code += `
                import { render as __render } from '${ctx.path}?type=template'
                __script.rener = __render
                export default __script
            `
        } else if (ctx.query.type === 'template') {
            // 获取编译后的对象 code就是render函数
            const templateRender = compilerSFC.compileTemplate({ source: descriptor.template.content })
            // 将render函数赋值给code返回给浏览器
            code = templateRender.code
        }
        // 设置浏览器响应头为js
        ctx.type = 'application/javascript'
        // 将字符串转换成数据流传给下一个中间件。
        ctx.body = stringToStream(code);
    }
    await next()
})

// 修改第三方模块路径
app.use(async (ctx, next) => {
    if (ctx.type === 'application/javascript') {
        const contents = await stream2string(ctx.body);
        // 将body中导入的路径修改一下,重新赋值给body返回给浏览器
        // import vue from 'vue', 匹配到from '修改为from '@modules/
        ctx.body = contents.replace(/(from\s+['"])(?![\.\/])/g, '$1/@modules/').replace(/process\.env\.NODE_ENV/g, '"development"');
    }
})

app.listen(5000)

console.log('服务器已经启动 http://localhost:5000')

到此这篇关于学习Vite的原理的文章就介绍到这了,更多相关Vite原理内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

学习Vite的原理

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Vite的原理分析

这篇文章主要介绍了Vite的原理分析,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。1. 概述Vite是一个更轻、更快的web应用开发工具,面向现代浏览器。底层基于ECMASc
2023-06-29

scrapy入门学习(原理)

什么是网络爬虫?网络爬虫又称网页蜘蛛,网络机器人,是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本。如果有兴趣可查看百度百科网络爬虫网络蜘蛛(web spider)也叫网络爬虫,是一种自动化浏览网络的程序,或者说是一种网络机器人,它
2023-01-30

Java学习之缓冲流的原理详解

为了提高其数据的读写效率,Java中又定义了四种缓冲流,分别是:字节缓冲输入流、字节缓冲输出流、字符缓冲输入流和字符缓冲输出流。本文主要来和大家聊聊这些缓冲流的原理,希望对大家有所帮助
2023-01-28

学习Cisco路由器恢复密码的原理

  相信有不少的朋友在使用思科路由器的时候,都会碰见过忘记密码又或者是丢失的密码的情况出现吧。那么问题就来了?不见了密码我们应该怎样做呢?如果你也遇见过这样的情况,那就跟着小编一起来看看这一篇教程吧。学习思科(Cisco)路由器恢复密码的原理。  在这一篇教程里面,小编会从iOS引导选项,恢复原理以及类别这三个部分向大
学习Cisco路由器恢复密码的原理
2024-04-17

学习局域网(LAN)实现监听的原理

  局域网是在一个局部的地理范围内,将各种计算机、外部设备和数据库等互相联接起来组成的计算机通信网,简称LAN。它可以通过数据通信网或者是专用数据电路,与远方的局域网、数据库或者是处理里面心相连接,构成一个大范围的信息处理系统。现在就跟着小编共同来看一看:学习局域网(LAN)实现监听的原理。  一、工作方式  针对于现
学习局域网(LAN)实现监听的原理
2024-04-18

为什么要选择学习python?学习python的原因!

  python是一门非常不错的编程语言,广受初学者的喜欢,越来越多的人都想要学习python。不过对于不太了解的人来说,都会疑惑为什么要学习python呢?  很多人都知道python是一种计算机语言,具有简洁性、易读性以及可扩展性,相对
2023-06-01

HBase原理 | HBase Region 运行状态学习

HBase为每个Region维护一个状态,并将该状态保留在hbase:meta中。hbase:meta 本身的Region状态保留在ZooKeeper中。可以在Master Web UI中查看Region的过渡状态。以下是可能的Region状态列表。1. 状态
HBase原理 | HBase Region 运行状态学习
2021-02-06

学习web前端,必须要掌握的CSS原理

从事Web前端开发的人都与CSS打交道很多,有的人也许不知道CSS是怎么去工作的,写出来的CSS浏览器是怎么样去解析的呢?当这个成为我们提高CSS水平的一个瓶颈时,是否应该多了解一下呢?我还是要推荐下我自己创建的web前端资料分享群6067
2023-06-03

vue学习之聊聊模板编译原理

什么是模板编译?下面本篇文章带大家聊聊vue中的模板编译,探讨一下模板编译原理,希望对大家有所帮助!
2023-05-14

编程热搜

目录