当前位置: 首页 > 后端技术 > Node.js

[1]游达说Vite香,我一步步分析Vite的原理

时间:2023-04-03 12:51:01 Node.js

1.Vite是什么?法语Vite(轻量级、轻量级)vite是一个基于Vue3单文件组件的非封装开发服务器。实现了本地快速开发启动,按需编译,不再需要等待整个应用编译完成的功能。Vite说明:一款非打包的Vue单页组件开发服务器,可以直接在浏览器中运行请求的Vue文件。对于现代浏览器,Vite基于原生的模块系统ESModule实现了按需编译,但是在webpack开发环境下速度非常慢,因为在开发时需要将编译放入内存并打包所有文件。Vite有这么多优点,那么它是如何实现的呢?2、Vite的实现原理首先总结一下Vite的实现原理:Vite在浏览器端使用exportimport导入导出的模块;Vite也实现了按需加载;Vite高度依赖模块脚本特性。实现过程如下:在koa中间件中获取请求体;通过es-module-lexer解析资源ast,得到导入内容;判断导入的资源是否为npm模块;返回处理后的资源路径:"vue"=>"/@modules/vue"以http请求和查询参数的形式区分模板、脚本、样式等需要处理的依赖,加载每个模块的内容SFC(vue单文件)文件。接下来我自己写一个Vite来实现同样的功能:3.手工实现Vite1。安装依赖环境实现Vite需要es-module-lexer、koa、koa-static、magic-string模块构建:npminstalles-module-lexerkoakoa-staticmagic-string这几个模块的功能是:koa和koa-static是vite内部使用的服务框架;es-module-lexer用于解析ES6import语法;magic-string用于重写字符串内容。2.基本结构搭建Vite,需要搭建一个koa服务:constKoa=require('koa');functioncreateServer(){constapp=newKoa();constroot=process.cwd();//构建上下文对象constcontext={app,root}app.use((ctx,next)=>{//扩展ctx属性Object.assign(ctx,context);returnnext();});constresolvedPlugins=[];//命令注册所有插件resolvedPlugins.forEach(plugin=>plugin(context));返回应用程序;}createServer().listen(4000);3、koa静态服务配置用于处理项目中的静态资源:const{serveStaticPlugin}=require('./serverPluginServeStatic');constresolvedPlugins=[serveStaticPlugin];constpath=require('path');functionserveStaticPlugin({app,root}){//使用当前根目录作为静态目录app.use(require('koa-static')(root));//使用public目录作为根目录app.use(require('koa-static')(path.join(root,'public')))}exports.serveStaticPlugin=serveStaticPlugin;目的是让文件在当前目录和公共目录下的文件可直接访问4.重写模块路径const{moduleRewritePlugin}=require('./serverPluginModuleRewrite');constresolvedPlugins=[moduleRewritePlugin,serveStaticPlugin];const{readBody}=require("./utils");const{parse}=require('es-module-lexer');constMagicString=require('magic-string');functionrewriteImports(source){letimports=parse(source)[0];constmagicString=newMagicString(来源);if(imports.length){for(leti=0;i{awaitnext();//拦截js类型的文件if(ctx.body&&ctx.response.is('js')){//读取文件内容constcontent=awaitreadBody(ctx.b欧迪);//重写import中无法识别的路径constr=rewriteImports(content);ctx.body=r;}});}exports.moduleRewritePlugin=moduleRewritePlugin;改写js文件中import语法的路径,改写后的路径会再次拦截请求服务端读取文件内容:const{Readable}=require('stream')asyncfunctionreadBody(stream){if(streaminstanceofReadable){//returnnewPromise((resolve,reject)=>{letres='';stream.on('data',(chunk)=>res+=chunk).on('end',()=>resolve(res));})}else{returnstream.toString()}}exports.readBody=readBody5.Resolve/@modulesfileconst{moduleResolvePlugin}=require('./serverPluginModuleResolve');constresolvedPlugins=[moduleRewritePlugin,moduleResolvePlugin,serveStaticPlugin];constfs=require('fs').promises;constpath=require('path');const{resolve}=require('path');constmoduleRE=/^\/@modules\//;const{resolveVue}=require('./utils')functionmoduleResolvePlugin({app,root}){constvueResolved=resolveVue(root)app.use(async(ctx,next)=>{//映射以/@modules开头的路径if(!moduleRE.test(ctx.path)){returnnext();}//移除/@modules/路径constid=ctx.path.replace(moduleRE,'');ctx.type='js';constcontent=awaitfs.readFile(vueResolved[id],'utf8');ctx.body=content});}exports.moduleResolvePlugin=moduleResolvePlugin;将/@modules开头的路径解析成对应的真实文件,返回给浏览器constpath=require('path');functionresolveVue(root){constcompilerPkgPath=path.resolve(root,'node_modules','@vue/compiler-sfc/package.json');constcompilerPkg=require(compilerPkgPath);//编译模块路径节点constcompilerPath=path.join(path.dirname(compilerPkgPath),compilerPkg.main);constresolvePath=(name)=>path.resolve(root,'node_modules',`@vue/${name}/dist/${name}.esm-bundler.js`);//dom操作lineconstruntimeDomPath=resolvePath('runtime-dom')//核心操作construntimeCorePath=resolvePath('runtime-core')//响应模块constreactivityPath=resolvePath('reactivity')//共享模块constsharedPath=resolvePath('shared')return{vue:runtimeDomPath,'@vue/runtime-dom':runtimeDomPath,'@vue/runtime-core':runtimeCorePath,'@vue/reactivity':reactivityPath,'@vue/shared':sharedPath,编译器:compilerPath,}}编译模块使用commonjs规范,其他文件使用es6模块。6、处理进程问题浏览器中没有进程变量,所以我们需要在html中注入进程变量const{htmlRewritePlugin}=require('./serverPluginHtml');constresolvedPlugins=[htmlRewritePlugin,moduleRewritePlugin,moduleResolvePlugin,serveStaticPlugin];const{readBody}=require("./utils");functionhtmlRewritePlugin({root,app}){constdevInjection=``应用程序使用(异步(ctx,next)=>{awaitnext();if(ctx.response.is('html')){consthtml=awaitreadBody(ctx.body);ctx.body=html.replace(//,`const{readBody}=require("./utils");functionhtmlRewritePlugin({root,app}){constdevInjection=``app.use(async(ctx,next)=>{awaitnext();if(ctx.response.is('html')){consthtml=awaitreadBody(ctx.body);ctx.body=html.replace(//,`$&${devInjection}`)}})}exports.htmlRewritePlugin=htmlRewritePluginamp;${devInjection}`)}})}exports.htmlRewritePlugin=htmlRewritePlugin在html的头标签中注脚本7.处理.vue后缩文件const{vuePlugin}=require('./serverPluginVue')constresolvedPlugins=[htmlRewritePlugin,moduleRewritePlugin,moduleResolvePlugin,vueStaticPlugin,serveStaticPlugin]常量路径=要求ire('path');constfs=require('fs').promises;const{resolveVue}=require('./utils');constdefaultExportRE=/((?:^|\n|;)\s*)exportdefault/functionvuePlugin({app,root}){app.use(async(ctx,next)=>{if(!ctx.path.endsWith('.vue')){returnnext();}//vue文件处理constfilePath=path.join(root,ctx.path);constcontent=awaitfs.readFile(filePath,'utf8');//获取文件内容let{parse,compileTemplate}=require(resolveVue(root).compiler);let{descriptor}=parse(content);//解析文件内容if(!ctx.query.type){letcode=``;if(descriptor.script){letcontent=descriptor.script.content;letreplaced=content.replace(defaultExportRE,'$1const__script=');code+=replaced;}if(descriptor.template){consttemplateRequest=ctx.path+`?type=template`code+=`\nimport{renderas__render}from${JSON.stringify(templateRequest)}`;code+=`\n__script.render=__render`}ctx.type='js'code+=`\nexportdefault__script`;ctx.body=代码;}if(ctx.query.type=='template'){ctx.type='js';让content=descriptor.template.content;const{code}=compileTemplate({source:content});ctx.body=代码;}})}exports.vuePlugin=vuePlugin;在后端.vue文件进行解析形成如下结果import{reactive}from'/@modules/vue';const__script={setup(){letstate=reactive({count:0});functionclick(){state.count+=1}return{state,click}}}import{renderas__render}from"/src/App.vue?type=template"__script.render=__renderexportdefault__scriptimport{toDisplayStringas_toDisplayString,createVNode为_creatVNode,Fragmentas_Fragment,openBlockas_openBlock,createBlockas_createBlock}from"/@modules/vue"exportfunctionrender(_ctx,_cache){return(_openBlock(),_createBlock(_Fragment,null,[_createVNode("div",null,"Counter:"+_toDisplayString(_ctx.state.count),1/*TEXT*/),_createVNode("button",{onClick:_cache[1]||(_cache[1]=$event=>(_ctx.click($event)))},"+")],64/*STABLE_FRAGMENT*/))}解析后的结果可以直接在createApp方法中使用8.总结至此,实现了一个基本的Vite总结itup:通过koa服务实现按需读取文件,省去打包步骤,提高项目启动速度。这包括解析代码内容、读取静态文件、浏览器新特性实践等一系列处理。其实Vite的内容远不止于此。这里我们实现了一个非打包的开发服务器,那么它是如何实现热更新的呢?下回实现Vite热更新的原理~本文使用mdnice排版