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

游玉玺几年前开发的“玩具vite”,只有100多行代码,但是对理解vite的原理帮助很大

时间:2023-03-31 23:51:13 vue.js

1。前言大家好,我叫若川。最近,我们组织了一次源码分享活动。有兴趣的可以加我微信ruochuan12参与。已经持续两个多月了。大家可以一起交流学习,共同进步。如果想学习源码,强烈推荐我之前写的《学习源码整体架构系列》,包括jQuery、underscore、lodash、vuex、sentry、axios、redux、koa、vue-devtools、vuex4、koa-compose、vue-next-release、vue-this、create-vue等十余篇源码文章。最近组织了一次源码阅读活动,大家一起学习源码。所以各种搜索都值得学习,代码行数不多的源码。在vuejs组织下,找到了尤雨熙几年前写的“玩具vite”vue-dev-server,发现了100行代码,值得学习。因此这篇文章。阅读本文后,您将学习:1.学习vite的简单原理2.学习如何使用VSCode调试源码3.学习如何编译Vue单文件组件4.学习如何使用recast生成ast转换files5.如何加载包文件6.etc.2.vue-dev-server的原理是什么vue-dev-server#how-it-worksREADME文档中有四个英文句子。我发现谷歌翻译比较准确,就原封不动地拿过来了。浏览器请求导入作为原生ES模块导入——没有捆绑。服务器拦截对*.vue文件的请求,即时编译它们,然后将它们作为JavaScript发回。对于提供可在浏览器中运行的ES模块构建的库,只需直接从CDN导入即可。导入到.js文件中的npm包(只是包名)被即时重写以指向本地安装的文件。目前,仅支持vue作为特例。其他包可能需要转换才能公开为本地浏览器目标ES模块。也可以看看vitejs的文档了解一下原理,文档中的图片非常好。看完这篇文章,相信你会有更深的理解。3、准备工作3.1克隆项目仓库vue-dev-server-analysis,求一个star^_^#推荐克隆我的仓库gitclonehttps://github.com/lxchuan12/vue-dev-server-analysis。gitcdvue-dev-server-analysis/vue-dev-server#npmi-gyarn#安装依赖yarn#或者克隆官方仓库gitclonehttps://github.com/vuejs/vue-dev-server.gitcdvue-dev-server#npmi-gyarn#安装依赖yarn一般来说,我们看源码的时候都是从package.json文件开始的://vue-dev-server/package.json{"name":"@vue/dev-server","version":"0.1.1","description":"Vue单文件组件即时开发服务器","main":"middleware.js",//指定可执行命令"bin":{"vue-dev-server":"./bin/vue-dev-server.js"},"scripts":{//先跳转到test文件夹,然后使用Node执行vue-dev-serverfile"test":"cdtest&&node../bin/vue-dev-server.js"}}根据脚本测试命令。让我们看看测试文件夹。3.2test文件夹vue-dev-server/test文件夹下有三个文件,代码不长。index.htmlmain.jstext.vue如下图所示。然后我们找到vue-dev-server/bin/vue-dev-server.js文件,代码不长。3.3vue-dev-server.js//vue-dev-server/bin/vue-dev-server.js#!/usr/bin/envnodeconstexpress=require('express')const{vueMiddleware}=require('../middleware')constapp=express()constroot=process.cwd();app.use(vueMiddleware())app.use(express.static(root))app.listen(3000,()=>{console.log('serverrunningathttp://localhost:3000')})原来是express启动了3000端口服务。重点是vueMiddleware中间件。接下来,让我们调试这个中间件。由于估计很多朋友都没有用过VSCode调试,这里就详细介绍如何调试源码。学会调试源码后,源码并没有想象中那么难。3.4使用VSCode调试项目vue-dev-server/bin/vue-dev-server.js文件,在app.use(vueMiddleware())这一行打断点。找到vue-dev-server/package.json的脚本,将鼠标移动到test命令,会出现运行脚本和调试脚本的命令。选择调试脚本,如下图所示。点击进入功能(F11)按钮进入vueMiddleware功能。如果你发现断点在一个不在这个项目中的文件中,你不想读它,你可以退出或重新开始。可以用浏览器隐身(隐私)模式(快捷键Ctrl+Shift+N,防止插件干扰)打开http://localhost:3000,可以继续调试vueMiddleware函数返回的函数。如果你的VSCode不是中文的(不习惯英文),可以安装简体中文插件。如果VSCode没有这个调试功能。建议更新到最新版本的VSCode(目前最新版本v1.61.2)。那我们就跟着调试学习一下vueMiddleware的源码吧。可以先看主线,在你认为重要的地方打断点继续调试。4.VueMiddleware源码4.1有无vueMiddleware中间件对比在调试状态下,我们可以在vue-dev-server/bin/vue-dev-server.js文件中注释掉app.use(vueMiddleware()),执行npm运行测试打开http://localhost:3000。启用中间件后,如下图。我们可以通过查看图片来了解差异。4.2vueMiddleware中间件概述我们可以找到vue-dev-server/middleware.js来查看这个中间件功能的概述。//vue-dev-server/middleware.jsconstvueMiddleware=(options=defaultOptions)=>{//省略returnasync(req,res,next)=>{//省略//处理以.vue结尾的文件if(req.path.endsWith('.vue')){//处理以.js结尾的文件}elseif(req.path.endsWith('.js')){//处理以/__modules/process开头的文件}elseif(req.path.startsWith('/__modules/')){}else{next()}}}exports.vueMiddleware=vueMiddlewarevueMiddleware最后返回一个函数。这个函数主要做了四件事:处理.vue结尾的文件,处理.js结尾的文件,处理/__modules/开头的文件,如果不是以上三种情况,执行next方法,将控制权转移给出下一个中间件,让我们看看如何处理它。我们也可以在这些重要的地方断点查看实现。例如:4.3处理以.vue结尾的文件if(req.path.endsWith('.vue')){constkey=parseUrl(req).pathnameletout=awaittryCache(key)if(!out){//BundleSingle-FileComponentconstresult=awaitbundleSFC(req)out=resultcacheData(key,out,result.updateTime)}send(res,out.code,'application/javascript')}4.3.1bundleSFC编译单文件组件的函数将单文件组件根据@vue/component-compiler进行转换,最终返回浏览器可以识别的文件。constvueCompiler=require('@vue/component-compiler')asyncfunctionbundleSFC(req){const{filepath,source,updateTime}=awaitreadSource(req)constdescriptorResult=compiler.compileToDescriptor(filepath,source)constassembledResult=vueCompiler.assemble(compiler,filepath,{...descriptorResult,script:injectSourceMapToScript(descriptorResult.script),styles:injectSourceMapsToStyles(descriptorResult.styles)})返回{...assembledResult,updateTime}}接下来我们看一下readSource函数的实现。4.3.2readSource读取文件资源该函数的主要作用是根据请求获取文件资源。返回文件路径filepath、资源来源和更新时间updateTime。constpath=require('path')constfs=require('fs')constreadFile=require('util').promisify(fs.readFile)conststat=require('util').promisify(fs.stat)constparseUrl=require('parseurl')constroot=process.cwd()asyncfunctionreadSource(req){const{pathname}=parseUrl(req)constfilepath=path.resolve(root,pathname.replace(/^\//,''))return{filepath,source:awaitreadFile(filepath,'utf-8'),updateTime:(awaitstat(filepath)).mtime.getTime()}}exports.readSource=readSource然后我们看处理.js文件4.4处理以.js结尾的文件//转换导入语句//转换导入语句//importVuefrom'vue'//=>importVuefrom"/__modules/vue"constresult=awaitreadSource(req)out=transformModuleImports(result.source)cacheData(key,out,result.updateTime)}发送(res,out,'application/javascript')}forvue-dev-server/test/main.js转换importVuefrom'vue'importAppfrom'./test.vue'newVue({render:h=>h(App)}).$mount('#app')//公众号:若川视界//添加微信ruochuan12//参与源码阅读,一起学习源码importVuefrom"/__modules/vue"importAppfrom'./test.vue'newVue({render:h=>h(App)}).$mount('#app')//公众号:若川视界//添加微信ruochuan12//参与源码阅读,一起学习源码4.4.1transformModuleImports转换导入导入recastvalidate-npm-package-nameconstrecast=require('recast')constisPkg=require('validate-npm-package-name')functiontransformModuleImports(code){constast=recast.parse(code)recast.types.visit(ast,{visitImportDeclaration(path){constsource=path.node.source.valueif(!/^\.\/?/.test(source)&&isPkg(source)){path.node.source=recast.types.builders.literal(`/__modules/${source}`)}this.traverse(path)}})returnrecast.print(ast).code}exportts.transformModuleImports=transformModuleImports也是npm包转换这里是"/__modules/vue"importVuefrom'vue'=>importVuefrom"/__modules/vue"4.5处理以/__modules/开头的文件importVuefrom"/__modules/vue"这段代码最后返回的是读取vue-dev-server/node_modules/vue/dist/vue.esm.browser.js路径下的文件。if(req.path.startsWith('/__modules/')){//constkey=parseUrl(req).pathnameconstpkg=req.path.replace(/^\/__modules\//,'')放出=awaittryCache(key,false)//不要让模块过时if(!out){out=(awaitloadPkg(pkg)).toString()cacheData(key,out,false)//不要让模块过时}send(res,out,'application/javascript')}4.5.1loadPkg加载包(这里只支持Vue文件)目前只支持Vue文件,即读取路径vue-dev-server/node_modules/vue/dist/vue。返回.browser.js下的esm文件。//vue-dev-server/loadPkg.jsconstfs=require('fs')constpath=require('path')constreadFile=require('util').promisify(fs.readFile)异步函数loadPkg(pkg){if(pkg==='vue'){//路径//vue-dev-server/node_modules/vue/distconstdir=path.dirname(require.resolve('vue'))constfilepath=path.join(dir,'vue.esm.browser.js')returnreadFile(filepath)}else{//TODO//检查包是否有可以使用的浏览器es模块//否则将它与rollup捆绑在一起?thrownewError('npmimportssupportarenotreadyyet.')}}exports.loadPkg=loadPkg至此,我们基本分析完了主文件和一些导入文件。了解主要流程。5.总结最后我们看上面两张有无vue中间件中间件的图片来总结一下:启用中间件后,图片如下。浏览器支持nativetype=module模块请求加载。vue-dev-server拦截它并返回浏览器支持的内容。因为不需要打包构建,所以速度非常快。5.1importVuefrom'vue'转换//vue-dev-server/test/main.jsimportVuefrom'vue'importAppfrom'./test.vue'newVue({render:h=>h(App)}).$mount('#app')main.js中的import语句importVuefrom'vue'通过recast生成ast并转换intoimportVuefrom"/__modules/vue"最后返回给浏览器的是vue-dev-server/node_modules/vue/dist/vue.esm.browser.js5.2importAppfrom'./test.vue'convertmain.js导入.vue文件,importAppfrom'./test.vue'使用@vue/component-compiler将其转换为浏览器支持的文件。5.3未来还能做什么?鉴于文章篇幅有限,暂不分析缓存tryCache部分。简单的说node-lru-cache就是用来做缓存的(经常测试这个算法)。后续应该会分析这个仓库的源码,欢迎继续关注我@河川。强烈推荐读者朋友按照文章中的方法使用VSCode调试vue-dev-server源码。源码里面还有很多细节。由于文章篇幅有限,就不一一详述了。值得一提的是,这个仓库的master分支是尤雨熙两年前写的。与本文相比,会更加复杂,有余力的读者可以学习。也可以直接上vite源码。看完这篇文章,你可能会发现前端能做的事情越来越多,不禁感慨:前端深不可测,唯有继续学习。最后,欢迎加我微信ruochuan12一起交流,参与源码阅读活动。大家可以一起学习源码,共同进步。