当前位置: 首页 > Web前端 > vue.js

VueConf在Vite上的分享启发了我

时间:2023-03-31 23:10:24 vue.js

Vite去年就出来了,但我真正了解它的是最近在VueConf上李奎在Vite上的分享:下一代网络工具。其中他提到的几点吸引了我。在分享的开始,他简单的解释了本次分享的重点:ESM和esbuild下面会详细讲解。然后他提到了Bundle-BasedDevServer。这就是我们一直使用webpack的方式:引用官网的一句话:当我们开始构建越来越大的应用程序时,需要处理的JavaScript代码量也呈指数级增长。包含数千个模块的大型项目并不少见。我们开始遇到性能瓶颈——用JavaScript开发的工具通常需要很长时间(甚至几分钟!)才能启动开发服务器,即使使用HMR,文件修改也需要几秒钟才能在浏览器中反映出来。如此循环往复,反馈慢会极大影响开发者的开发效率和幸福感。简单总结一下,如果应用比较复杂,使用Webpack的开发过程就不会那么顺利:WebpackDevServer的冷启动时间会比较长。WebpackHMR热更新的响应速度较慢。这就是Vite出现的原因。你可以使用它简单理解为:No-Bundler构建计划。其实就是使用浏览器原生ESM的能力。但是第一个使用浏览器原生ESM能力的工具并不是Vite,而是一个叫做Snowpack的工具。当然,本文不会展开比较Vite和它的区别。想知道Vite和X有什么区别?至此,我不禁开始思考一个问题:Vite这个工具为什么能够出现,它基于什么前提条件?带着这个疑问,结合分享和Vite的源码以及社区的一些文章,我发现了以下几个与Vite密不可分的模块:ESModulesHTTP2ESBuild这些我都听说过,但是具体的我没有深入了解了解。今天是深入分析它的好时机。ESModules在现代前端工程系统中,我们一直在使用ESModules:importafrom'xxx'importbfrom'xxx'importcfrom'xxx'可能是太司空见惯了,大家早就习以为常了。但是如果我们对ESModules没有深入的了解,那么我们对一些已有的轮子(比如本文中的Vite)的理解可能会有一些障碍。ESModules是浏览器原生支持的模块系统。过去,常用的是CommonJS和其他基于AMD的模块系统,例如RequireJS。看看目前浏览器对它的支持情况:主流浏览器(IE11除外)都已经支持了,它最大的特点就是在浏览器端使用export和import导入导出模块,在script标签中设置type=”模块”,然后使用模块内容。上面说了这么多,毕竟只是ESModules的自我介绍而已。一直以来,他就像一个黑盒子,我们不知道内部的执行机制。下面让我们一起来看看吧。先来看看模块系统的作用:传统脚本标签的代码加载容易造成全局作用域污染,需要维护一系列脚本的书写顺序。随着一个大项目,它变得越来越难以维护。模块系统通过声明式公开和引用模块,使模块之间的依赖关系显而易见。当您使用模块进行开发时,您正在构建一个依赖关系图。不同模块之间的线代表代码中的导入语句。正是这些import语句告诉浏览器或Node要加载什么代码。我们所做的是为依赖图指定一个入口文件。从这个入口文件开始,浏览器或者Node会按照import语句去寻找它所依赖的其他代码文件。对于ES模块,主要分为三个步骤:构建。查找、下载所有文件并将其解析为模块记录。实例化。在内存中找一块区域存放所有导出的变量(但还没有填充值)。然后使导出和导入都指向这些内存块。这个过程称为链接。评价。运行代码以用变量的实际值填充内存块。构建阶段(Construction)在构建阶段,每个模块都会经历三件事:查找:找到包含该模块的文件的下载位置(也称为模块解析)下载:获取文件(从URL下载或加载文件系统)解析:将文件解析成模块记录FindFind通常我们会有一个主文件main.js作为一切的开始:然后通过导入语句导入其他模块导出的内容:导入语句的一部分称为模块说明符。它告诉Loader在哪里可以找到导入的模块。关于模块标识符需要注意的一件事:它们有时需要在浏览器和Node.js之间以不同方式处理。每个主机都有自己的解释模块标识符字符串的方式。目前浏览器中只能使用URL作为ModuleSpecifier,即使用URL来加载模块。下载下载,出现问题。浏览器在解析文件之前并不知道文件依赖于哪些模块,当然也无法在获取文件之前解析文件。这会导致整个解析依赖的过程被阻塞。像这样阻塞主线程会使使用该模块的应用程序太慢而无法使用。这是ES模块规范将算法分为多个阶段的原因之一。单独分离构建过程允许浏览器在执行同步初始化过程之前下载文件并构建自己对框图的理解。对于ES模块,您需要在进行任何评估之前预先构建整个模块图。这意味着你的模块标识符中不能有变量,因为那些变量还没有值。但有时在模块路径中使用变量确实很有用。比如你可能需要根据代码的运行状态或者运行环境来切换加载某个模块。为了让ES模块支持这一点,有一个提案叫做动态导入。有了它,你就可以使用像import(${path}/foo.js这样的import语句了。它的原理是任何通过import()加载的文件都会作为一个独立的依赖图入口。动态导入的模块会打开一个新的依赖图Parse解析其实是帮助浏览器了解模块的组成,而我们解析出来的模块组成表叫做ModuleRecord模块记录,模块记录包含了当前模块的AST,哪些模块的变量都引用了,之前还有一些具体的属性和方法。一旦一个模块记录被创建,它就会被记录在模块映射ModuleMa中。被记录后,如果再次请求同一个URL,Loader会直接使用ModuleMap中该URL对应的ModuleRecord。解析中有一个细节看似微不足道,但实际上影响很大。所有模块都被解析为好像它们在顶部具有“usestrict”。还有一些其他的细微差别。例如,关键字await保留在模块的顶层代码中,而this的值是未定义的。这种不同的解析方式称为解析目标。如果你用不同的目标解析同一个文件,你会得到不同的结果。所以在你开始解析之前你需要知道你正在解析的文件类型:它是一个模块吗?在浏览器中很容易。您只需要在脚本标签中设置type="module"。这告诉浏览器这个文件应该被解析为一个模块。但是在Node中,没有HTML标签,所以需要其他方法来识别它们。目前社区主流的解决方案是修改文件的后缀为.mjs,告诉Node它会是一个模块。但是目前还没有标准化,还存在很多兼容性问题。至此,加载过程的最后,从普通的主入口文件变成了一堆模块记录ModuleRecord。下一步是实例化此模块并将所有实例链接在一起。在实例化阶段,为了实例化ModuleRecord,引擎会使用DepthFirstPost-orderTraversal(深度优先后序)进行遍历,JS引擎会为每个ModuleRecord创建一个ModuleEnvironmentRecord模块环境记录,它会管理ModuleRecord对应的变量,并为所有的exports分配内存空间。ESModules的这种连接方式称为LiveBindings(动态绑定)。ESModules之所以使用LiveBindings,是因为它会帮助静态分析,避免一些问题,比如循环依赖。CommonJS导出复制的导出对象,这意味着如果导出模块稍后更改值,导入模块将看不到更改。这是通常的结论:CommonJS模块导出是值的副本,而ES模块是对值的引用。评估阶段的最后一步是将值填充到内存中。记住我们通过内存连接了所有的导出和导入,但是内存还没有任何价值。JS引擎通过执行顶层代码(函数外的代码)来给这些内存区域加值。至此,我大致分析完了ESModules的黑盒。当然这部分我参考了es-modules-a-cartoon-deep-dive,然后结合自己的理解进行分析。如果你想了解更多背后的实现,可以点击上面的链接。ESModules在Vite中的体现我们可以打开一个正在运行的Vite项目:从上图可以看出:import{createApp}from"/node_modules/.vite/vue.js?v=2122042e";与之前的import{createApp}from"vue"相比,这里重写了导入的模块路径:Vite使用现代浏览器原生支持ESM特性,省略了模块的打包。(这也是开发环境中项目启动和热更新比较快的一个很重要的原因)HTTP2在看HTTP2之前,我们先来了解一下HTTP的发展历史。我们知道HTTP是浏览器中最重要也是使用最多的协议,是浏览器和服务器之间的通信语言。随着浏览器的发展,HTTP不断发展以适应新的形式。HTTP/0.9的初始实现比较简单:采用了基于请求-响应的模型,客户端发送请求,服务器返回数据。从图中可以看出只有一个请求行,服务器没有返回头信息。万维网的快速发展带来了很多新的需求,而HTTP/0.9已经不适合新兴网络的发展,因此需要一种新的协议来支持新兴网络,这就是HTTP/1.0诞生的原因。而在浏览器中显示的不仅仅是HTML文件,还有JavaScript、CSS、图片、音频、视频等不同类型的文件。因此,支持多种类型的文件下载是HTTP/1.0的一个核心需求。HTTP/1.0为了让客户端和服务端能够进行更深入的通信,引入了请求头和响应头,它们以Key-Value的形式存储。HTTP发送请求时,会带上请求头信息,服务器返回数据时,会先返回响应头信息。HTTP/1.0的具体请求过程可以参考下图:HTTP/1.0每次进行HTTP通信,需要经历三个阶段:建立TCP连接、传输HTTP数据、断开TCP连接。当时,由于通讯文件比较小,每个页面的引用也不多,这种传输方式问题不大。但是随着浏览器的普及,单个页面中的图片文件越来越多。有时一个页面可能包含数百个外部引用的资源文件。如果下载每个文件,都需要经过建立TCP连接、传输数据、断开连接等步骤,无疑会增加很多不必要的开销。为了解决这个问题,HTTP/1.1增加了一种持久连接的方法,其特点是可以在一个TCP连接上传输多个HTTP请求,只要浏览器或服务器没有明确断开连接,TCP连接就会一直保持保持。并且对于浏览器中的同一个域名,默认允许同时建立6个TCP长连接。通过这些方法,在一定程度上大大提高了页面的下载速度。之前我们使用Webpack将应用代码打包成bundle.js,有一个很重要的原因:分散的模块文件会产生大量的HTTP请求。大量HTTP请求会导致浏览器端并发请求资源:如上图,红圈圈出的请求为并发请求,但后续请求因为域名连接数超过限制而被挂起。等了一会儿。在HTTP1.1标准下,每个请求都需要建立一个单独的TCP连接。一个完整的沟通过程之后,是非常耗时的。出现这个问题的原因主要有以下三个原因:TCP慢启动TCP连接之间相互竞争带宽head-of-line阻塞前两个问题是TCP本身的机制导致的,而head-of-线路阻塞是由HTTP/1.1的机制造成的。为了解决这些已知问题,HTTP/2的思路是一个域名只使用一个TCP长期连接来传输数据,这样整个页面资源的下载过程只需要一次慢启动,并且也避免了多个TCP连接带来的带宽。问题来了。也就是多路复用,可以实现资源的并行传输。上面也提到了Vite使用ESM在浏览器中使用模块,也就是使用HTTP请求来获取模块。这样会产生大量的HTTP请求,但是由于HTTP/2多路复用机制的出现,很好的解决了传输时间长的问题。ESBuildesbuild官方介绍:是一个JavaScriptBundler打包压缩工具,可以打包分发JavaScript和TypeScript代码运行在网页上。它是用esbuild底层使用的golang编写的,与传统的建站工具相比,在打包速度上有明显的优势。编译Typescript的速度远比官方的tsc快。对于需要编译的文件,比如JSX或者TS,Vite使用esbuild进行编译。与Webpack的整体编译不同,Vite在浏览器请求文件时编译文件,然后提供给浏览器。因为esbuild编译速度够快,这种每次加载页面都要编译,不会影响加速速度。Vite实现原理结合上面的分析和源码,我们可以用一句话简单的描述一下Vite的原理:StaticServer+Compile+HMR:将当前项目目录作为静态文件服务器的根目录,拦截一些文件请求处理importnode_modules模块中的代码处理vue单文件组件(SFC)的编译,通过WebSocket实现HMR。当然,关于手写Vite的实现,社区中已经有很多文章,这里不再赘述,大体原理是一样的。总结写完这篇文章,带给我更多的思考。从一次分享中,发现背后庞大的生态系统,以及我们一直在使用但没有完全理解的技术黑匣子。更重要的是感叹大佬们的想法,站在技术的制高点,有很高的深度和广度,开发一些对提高生产力极其有用的轮子。于是,文章写完了,学习的步伐还在继续~参考https://hacks.mozilla.org/201...https://github.com/evanw/esbuild极客时间/罗剑锋/透视HTTP协议