CXYVIP官网源码交易平台_网站源码_商城源码_小程序源码平台-丞旭猿论坛
CXYVIP官网源码交易平台_网站源码_商城源码_小程序源码平台-丞旭猿论坛
CXYVIP官网源码交易平台_网站源码_商城源码_小程序源码平台-丞旭猿论坛

手写 Vite Server 系列(3)—— 更细粒度的复用

前言《手把手教你手写一个 Vite Server(一)》《手把手教你手写 Vite Server(二)—— 插件架构设计》在该系列的第一篇文章,我们实现了 Vite Server 的一些处理文件的功能(TS、TSX、CSS),但这个 Server 的功能是写死的,如果需要新增功能,就需要修改 Server 的代码,

没有任何的可扩展性。

而在系列的第二篇文章中,我们解决了这个问题,我们介绍了插件架构的概念,然后根据概念,对 Server 进行了架构插件化改造,通过插件往 Server 中添加新的中间件,来给 Vite Server 新增功能。

改造后的架构如下:

但是这套架构其实是不够好的,因为可扩展的颗粒度为中间件,中间件内的很多代码都没有复用(例如文件路径解析和文件加载)颗粒度较大,那么能复用的内容就小我们只实现了中间件级别(颗粒度)的插件化,没有对更底层的逻辑进行抽象。

因此,本篇文章,将继续对架构进行改造,实现更细粒度的代码复用本文的代码放在 GItHub 仓库,链接:https://github.com/candy-Tong/my-vite,目录为 packages/3. my-vite-transform-hook。

基础中间件扩展的问题及解决思路我们来看看之前的几个处理文件的中间件: Transform、CSS、Less,它们的共同点:

它们其实都经过这么三个阶段:解析模块(resolve),获取模块的真实路径加载模块(load),获取模块文件的代码字符串文本转换模块(transform),对代码进行转换处理,不同的中间件,处理的内容和结果都不相同。

其实,所有的文件处理,都可以分成这三个阶段在这三个中间件中,解析和加载这两个阶段的处理,其实是完全相同的那既然完全相同,那就证明可以抽离出来,而不同的内容,则可以新增一个 transform 钩子,在 transform 阶段一次调用,那么这样就可以。

通过插件实现 transform 钩子,来扩展新的文件转换能力.

整体思路如上图所示:中间件的扩展粒度太大,我们只用一个中间件,专门负责模块的处理插件通过提供 transform 钩子,实现不同的文件处理方式,实现更细粒度的扩展其实这个 transform 钩子设计,Rollup 插件也有。

Vite 生产环境用的是 Rollup 打包,因此这个思路也是从 Rollup 中借鉴过来的更多相关内容可以查看我之前写的文章:《Vite 是如何兼容 Rollup 插件生态的》如果多个插件都有 transform 钩子,会怎样处理?

我们在《Vite 是如何兼容 Rollup 插件生态的》详细描述过插件钩子的 4 种类型,其中 transform 钩子是 async 和 sequential 的:transform 钩子支持异步transform 钩子必须

串行执行较前的插件的 transform 钩子先执行,因此插件顺序会影响到最终的编译结果前一个的插件 transform 之后的 code 代码,会传递给下一个插件的 transform 钩子。

为什么要这么设计?必须要串行执行,因为并行执行钩子,transform 钩子的执行顺序就得不到保证,会导致每次的编译结果可能不一致而 transform 后的结果会传递给下一个插件,这是一个管道的设计,这样设计的目的是,让一个

模块能被多个插件处理,这种情况很常见,例如 Vue 插件分离出来的 ts 代码,还可以被 esbuild 插件处理成 js,还可以被代码压缩插件压缩transform 钩子我们新增 transform 钩子的定义。

exporttypeTransformResult=string|null|void;exporttypeTransformHook=(code: string,id: string)=>Promise

|TransformResult;exportinterfacePlugin{configureServer?: ServerHook;// 上篇文章用到的钩子 transform?

: TransformHook;// 这次新增的钩子 }钩子是一个函数,它的参数为 code 和 id:code:源代码或前一个钩子转换后的代码id:模块 id返回的是转换后的代码 / 空如果返回为空值

,则表示当前钩子不转换当前模块如果有返回值,则覆盖源码/上次转换接口,同时作为入参传给下一个 transform 钩子transform 钩子的处理流程,实现如下:code=// 读取的模块代码 url

=// 模块请求 vite server 时的 url // 遍历所有的插件 for(constpluginofserver.plugins){if(!plugin.transform)continue

;letresult: TransformResult;try{result=awaitplugin.transform(code,url);}catch(e){console.error(e);}// 如果返回为空,则表示当前钩子不转换当前模块

if(!result)continue;// 如果有返回值,用结果覆盖 code,作为入参传给下一个 transform 钩子 code=result;}// 最终的 code 就是转换后的代码 transform 钩子是在模块转换中间件中调用的,因此我们还需实现一个 transform 中间件(名字也叫 transform,但它跟前两篇文章写的 transform 中间件是不一样的)

transform 中间件的实现我们用一个中间件进行模块的处理,它有三个步骤:解析模块(resolve),获取模块的真实路径加载模块(load),获取模块文件的代码字符串文本转换模块(transform),对

代码进行转换处理,处理结果仍然是代码字符串文本中间件的实现如下:exportfunctiontransformMiddleware(server: ViteDevServer):NextHandleFunction。

{returnasyncfunctionviteTransformMiddleware(req,res,next){if(req.method!==GET){returnnext();}consturl

: string=req.url!;// JS 模块和 CSS 模块都是模块,都能用该中间件处理 if(isJSRequest(url)||isCSSRequest(url)){// 解析模块路径 const

file=url.startsWith(/)?.+url : url;// 加载文件,获取文件的内容 letcode: string=awaitreadFile(file,utf-8);// 遍历所有的插件

for(constpluginofserver.plugins){if(!plugin.transform)continue;letresult: TransformResult;try{result=

awaitplugin.transform(code,url);}catch(e){console.error(e);}// 如果返回为空,则表示当前钩子不转换当前模块 if(!result)continue

;// 如果有返回值,用结果覆盖 code,作为入参传给下一个 transform 钩子 code=result;}res.setHeader(Content-Type,application/javascript

);// 最终的 code 就是转换后的代码 returnres.end(code);}next();};}这里的 isJSRequest 和 isCSSRequest 的逻辑也跟之前有所不同:const

knownJsSrcRE=/\.((j|t)sx?)$/;exportconstisJSRequest=(url: string):boolean=>{url=cleanUrl(url);returnknownJsSrcRE

.test(url);};constcssLangs=\\.(css|less|sass|scss|styl|stylus|pcss|postcss)($|\\?);constcssLangRE=new

RegExp(cssLangs);exportconstisCSSRequest=(request: string):boolean=>cssLangRE.test(request);将类 JS 和 类 CSS 的语言,也加入到判断中,

transform 中间件,不对再具体的模块进行处理和判断,改为在插件的 transform 钩子中自行判断改造插件原有的 transform 插件,改为 esbuild 插件(处理类 JS 的模块):。

exportfunctionesbuildPlugin():Plugin{return{asynctransform(code,url){if(isJSRequest(url)){constextname

=path.extname(url).slice(1);const{code: resCode}=awaittransform(code,{target:esnext,format:esm,sourcemap

: true,loader: extnameasjs|ts|jsx|tsx,});returnresCode;}},};}直接在 transform 插件内,对 JS 的代码用 esbuild 进行编译。

less 和 css 合并成一个插件即可(实际上 Vite 也是这么做的):exportfunctioncssPlugin():Plugin{return{asynctransform(code,url

){if(isCSSRequest(url)){constfile=url.startsWith(/)?.+url : url;if(isLessRequest(url)){// 预处理器处理 less

constlessResult=awaitless.render(code,{// 用于 @import 查找路径 paths:[dirname(file)],});code=lessResult.css

;}const{css}=awaitpostcss([atImport()]).process(code,{from:file,to: file,});returncss;}}};}如果是 less 模块,先用 less 进行预处理,然后用 postcss 处理,最终返回 css 字符串。

这里还需要一个将 css 转换为 js 的插件:exportfunctioncssPostPlugin():Plugin{return{asynctransform(code,url){if(isCSSRequest

(url)){return` var style = document.createElement(style) style.setAttribute(type, text/css)

style.innerHTML = \`${code} \` document.head.appendChild(style) `;}}};}为什么要多拆分一个 cssPostPlugin 的插件,不能写到 CSS 插件中吗?

因为实际项目中,可能还有其他 CSS 相关的插件要等所有 CSS 组件处理完之后,才能将 CSS 转成 JS,否则 CSS 相关的工具就无法进行处理了因此这个插件应该放在所有 CSS 相关的插件后面这几个插件的顺序如下:

exportfunctionloadInternalPlugins():Plugin[]{return[esbuildPlugin(),cssPlugin(),cssPostPlugin(),staticPlugin

()];}只要保证 cssPostPlugin 在 cssPlugin() 之后即可实际上 Vite 插件,有个 enforce 属性用于控制插件的顺序,只是我们这里没有实现,详情可以查看插件顺序总结本文先回顾了上篇文章的插件化架构的缺点——有复用性,但

可扩展的粒度太大,复用性不高然后分析了模块处理的整个流程,分为解析模块加载模块、转换模块然后分析出之前的几个转换模块的中间件,其实只是在转换模块流程中不同,其他的流程都是相同的因此我们把转换流程,单独提取出来,。

插件通过提供 transform 钩子,来扩展 Vite 的转换模块能力用一个中间件负责模块的转换,在中间件中分别调用各个插件的 transform 钩子这样就实现了基于处理流程粒度的扩展机制拓展阅读《手把手教你手写 Vite Server(二)—— 插件架构设计》

手把手教你手写一个 Vite Server(一)Vite Server 是如何处理页面资源的?五千字剖析 vite 是如何对配置文件进行解析的前端进阶:跟着开源项目学习插件化架构最后如果这篇文章对您有所帮助,请帮忙点个赞 ,您的鼓励是我创作路上的最大的动力。

最近注册了一个公众号,刚刚起步,名字叫:Candy 的修仙秘籍,欢迎大家关注~

© 版权声明
THE END
喜欢就支持一下吧
点赞0赞赏 分享
相关推荐
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容