学习Vite的原理

目录
  • 1. 概述
  • 2. 实现静态测试服务器
  • 3. 处理第三方模块
  • 4. 单文件组件处理

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 './src/components/Hello.vue'
const __script = {
    name: "App",
    components: {
        Hello
    }
}

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

import { render as __render } from '/src/App.vue?type=template'
__script.render = __render
__script.__hmrId = '/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原理内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • vite2.0 踩坑实录

    目录 vite项目构建优化 其它 最后 算是对上一篇的补充,记录了一些在配置项目中遇到的问题,希望对大家能有所帮助- vite项目构建优化 路由动态导入 经过rollup的构建,动态导入的文件将会生成异步的chunk文件,在我们访问项目的时候按需加载,极大的提升应用的加载速度 import Home from '@/views/home/index.vue' import Layout from '@/components/Layout.vue' const routes: Array<Rout

  • vite+vue3.0+ts+element-plus快速搭建项目的实现

    vite 出了 2.x 版本,抱着学一学的心态决定出个简单的项目,结合 element-plus,以及将会成为每位前端必会的 typescript,实现了如下内容. vite是一个由原生 ESM 驱动的 Web 开发构建工具.在开发环境下基于浏览器原生 ES imports 开发,在生产环境下基于 Rollup 打包. vite 作用 快速的冷启动:不需要等待打包操作: 即时的热模块更新:替换性能和模块数量的解耦让更新飞起: 真正的按需编译:不再等待整个应用编译完成,这是一个巨大的改变. 使用的

  • vue3.0+vite2实现动态异步组件懒加载

    创建一个vite项目 性能决定成败;vite确实快: cmd 命令行(默认你已经安装了node & npm),执行npm init @vitejs/app vue-study – --template vue: cd至vue-study,npm install(安装依赖); npm run dev(启动项目): 创建组件 新建一个目录为pages,pages下面再新建一个目录contents,contens下面可以新建具体的组件目录页面,此时目录结构为 App.vue <template&g

  • 如何为老vue项目添加vite支持详解

    1.前言 接手公司的某个项目已经两年了,现在每次启动项目都接近1分钟,hmr也要好几秒的时间,but vite2发布之后就看到了曙光,但是一直没有动手进行升级,昨天终于忍不住了,升级之后几秒钟就完成了. vite -- 一个由 vue 作者尤雨溪开发的 web 开发工具,它具有以下特点: 快速的冷启动 即时的模块热更新 真正的按需编译 2.开始升级 注:只是升级了开发环境,打包依旧是webpack(也试过打包也用vite,但是打包后发现iview的字体图标出现问题了,初步验证是静态资源的问题,v

  • vite构建项目并支持微前端

    目录 基础配置 支持微前端构建 其他说明 1. 老旧浏览器的支持 2. 关于 TypeScript 的说明 3. 对接 CDN 4. 构建出错 4.1 找不到包 4.2 请求超时 4.3 导入模块出错 小结 得益于 esbuild 的超高性能,vite 在诞生之初就备受关注,且一直保持着活跃的开发迭代.截至目前,vite 已经迭代到了 2.7.10 版本,各方面也基本具备了在生产使用的条件.这段时间,我在项目中尝试了使用 vite 进行打包构建,本文就是这次构建的过程记录. 基础配置 首先使用v

  • 一文带你了解vite对浏览器的请求做了什么

    目录 工作原理: 浏览器做的什么事啊 宿主文件index.html main.js 其他裸模块 了解一下预打包 服务器做的什么事啊 请求首页index.html 请求以.js结尾的文件 基础js文件 对main中的依赖进行处理 处理.vue文件 处理图片路径 总结 工作原理: type="module" 浏览器中ES Module原生native支持. 如果浏览器支持type="module" ,我i们可以使用es6模块化的方式编写.浏览器会把我们需要导入的文件再发

  • Vite+Electron快速构建VUE3桌面应用的实现

    目录 一. 简介 二. 创建一个Vite项目 1. 安装 vite 2. 创建项目 3. 进入且运行 三. 配置Electron 1. 官方文档 2. 安装 3. 配置文件 四. 运行 五. 最后 一. 简介 首先,介绍下vite和Electron. Vite是一种新型前端构建工具,能够显著提升前端开发体验.由尤大推出,其发动态表示"再也回不去webpack了..." Electron是一个使用 JavaScript.HTML 和 CSS 构建桌面应用程序的框架. 嵌入Chromium

  • vite+vue3+element-plus项目搭建的方法步骤

    使用vite搭建vue3项目 通过在终端中运行以下命令,可以使用 Vite 快速构建 Vue 项目. $ npm init vite-app <project-name> $ cd <project-name> $ npm install $ npm run dev 引入Element Plus 安装Element Plus: npm install element-plus --save main.js中完整引入 Element Plus: import { createApp

  • 学习Vite的原理

    目录 1. 概述 2. 实现静态测试服务器 3. 处理第三方模块 4. 单文件组件处理 1. 概述 Vite是一个更轻.更快的web应用开发工具,面向现代浏览器.底层基于ECMAScript标准原生模块系统ES Module实现.他的出现是为了解决webpack冷启动时间过长以及Webpack HMR热更新反应速度慢等问题. 默认情况下Vite创建的项目是一个普通的Vue3应用,相比基于Vue-cli创建的应用少了很多配置文件和依赖. Vite创建的项目所需要的开发依赖非常少,只有Vite和@v

  • Python学习之虚拟环境原理详解

    目录 认识虚拟环境 Python中的虚拟环境工具 Virtualenv Treminal 终端演示 该章节我们学习虚拟环境的相关知识,虚拟环境对于刚刚使用Python的初学者来说使用的概率可能会比较低.但是我们依然要对它有一定的了解. 认识虚拟环境 在我们平时的工作环境中,可能会存在一台电脑存在多个版本的 python 的情况 . 比如我们有一个 Python2.7的版本,还有一个 Python3.8的环境,它们两个都存在与我们当前的系统中.这就造成了一个问题,两个版本都在同一个环境下,造成 p

  • java并发学习-CountDownLatch实现原理全面讲解

    CountDownLatch在多线程并发编程中充当一个计时器的功能,并且维护一个count的变量,并且其操作都是原子操作. 如下图,内部有下static final的Sync类继承自AQS. 该类主要通过countDown()和await()两个方法实现功能的,首先通过建立CountDownLatch对象,并且传入参数即为count初始值. 如果一个线程调用了await()方法,那么这个线程便进入阻塞状态,并进入阻塞队列. 如果一个线程调用了countDown()方法,则会使count-1:当c

  • 尤雨溪开发vue dev server理解vite原理

    目录 1.引言 2. vue-dev-server 它的原理是什么 3. 准备工作 3.1 克隆项目 3.2 test 文件夹 3.3 vue-dev-server.js 3.4 用 VSCode 调试项目 4. vueMiddleware 源码 4.1 有无 vueMiddleware 中间件对比 4.2 vueMiddleware 中间件概览 4.3 对 .vue 结尾的文件进行处理 4.3.1 bundleSFC 编译单文件组件 4.3.2 readSource 读取文件资源 4.4 对

  • 正则表达式从原理到实战全面学习小结

    目录 正则是啥? 简单字符 转义字符 字符集和 量词 字符边界 选择表达式 分组与引用 预搜索 修饰符 图形化工具 JavaScript中的正则 RegExp#test RegExp#exec String#search String#match String#split String#replace RegExp 实例属性 实战实例 总结 正则表达式,名字听上去就没有吸引力,我发现很多前端对正则表达式了解不深,甚至有些惧怕,每次能够运行全凭运气,更有甚者完全靠复制粘贴. 正则表达式其实并不难,

  • node.js中RPC(远程过程调用)的实现原理介绍

    刚接触到RPC(远程过程调用),就是可以在本地调用远程机子上的程序的方法,看到一个简单的nodejs实现,用来学习RPC的原理很不错:nodejs light_rpc 使用示例: 复制代码 代码如下: //服务端 var light_rpc = require('./index.js'); var port = 5556; var rpc = new light_rpc({     combine: function(a, b, callback){         callback(a + b

  • 详解Vue中的MVVM原理和实现方法

    下面由我阿巴阿巴的详细走一遍Vue中MVVM原理的实现,这篇文章大家可以学习到: 1.Vue数据双向绑定核心代码模块以及实现原理 2.订阅者-发布者模式是如何做到让数据驱动视图.视图驱动数据再驱动视图 3.如何对元素节点上的指令进行解析并且关联订阅者实现视图更新 一.思路整理 实现的流程图: 我们要实现一个类MVVM简单版本的Vue框架,就需要实现一下几点: 1.实现一个数据监听Observer,对数据对象的所有属性进行监听,数据发生变化可以获取到最新值通知订阅者. 2.实现一个解析器Compi

  • 基于SpringBoot核心原理(自动配置、事件驱动、Condition)

    前言 SpringBoot是Spring的包装,通过自动配置使得SpringBoot可以做到开箱即用,上手成本非常低,但是学习其实现原理的成本大大增加,需要先了解熟悉Spring原理.如果还不清楚Spring原理的,可以先查看博主之前的文章,本篇主要分析SpringBoot的启动.自动配置.Condition.事件驱动原理. 正文 启动原理 SpringBoot启动非常简单,因其内置了Tomcat,所以只需要通过下面几种方式启动即可: @SpringBootApplication(scanBas

  • 解析Tomcat架构原理到架构设计

    目录 一.学习目的 1.1.掌握 Tomcat 架构设计与原理提高内功 1.2.宏观理解一个请求如何与 Spring 联系起来 1.3.提升自己的系统设计能力 二.整体架构设计 2.1.连接器 2.2.封装变与不变 2.3.容器 2.4.请求定位 Servlet 的过程 三.Tomcat 为何打破双亲委派机制 3.1.双亲委派 3.2.Tomcat 热加载 3.3.Tomcat 的类加载器 3.4.Tomcat 类加载器层次 四.整体架构设计解析收获总结 4.1.连接器 4.2.容器 4.3.类

  • Vue3和Vite不得不说的那些事

    目录 1.创建一个vite项目 2.vite简介 3.第一个疑问 3.1挖掘vite运行原理 为什么这里需要@modules? 3.2文件请求 4.hmr热更新 总结 1.创建一个vite项目 npm init vite-app <project-name> cd <project-name> npm install npm run dev 或者 yarn create vite-app <project-name> cd <project-name> ya

随机推荐