什么是VueDemi?如果你想开发一个同时支持Vue2和Vue3的库,你可能会想到以下两种方式:1.创建两个分支分别支持Vue2和Vue32。只使用Vue2和Vue3支持的API。两种方法都有缺点。第一个很麻烦,第二个不能用Vue3新的组合API。其实Vue2.7+版本已经内置了支持组合API,Vue2.6及之前的版本也可以通过@vue/composition-api插件来支持,所以完全可以只写一套代码来支持同时使用Vue2和3。尽管如此,在实际开发中,同一个API在不同版本中可能会从不同的源导入,比如ref方法,在Vue2.7+中直接从Vue中导入,而在Vue2.6中只能从@中导入-从vue/composition-api导入,必然会涉及到版本判断。VueDemi就是用来解决这个问题的。使用非常简单,只需要从VueDemi中导出你需要的内容即可:import{ref,reactive,defineComponent}from'vue-demi'Vue-demi会根据你的项目决定使用哪个版本的Vue。具体来说,它的策略如下:<=2.6:Export2.7fromVueand@vue/composition-api:FromVueExportin(combinedAPIbuiltinVue2.7)>=3.0:ExportedfromVue,andalsopolyfill两个Vue2set和delAPI的版本接下来我们从源码的角度来看一下它是如何实现的。基本原理当我们在项目中使用npmivue-demi安装时,会自动执行一个脚本:{"scripts":{"postinstall":"node./scripts/postinstall.js"}}//postinstall.jsconst{switchVersion,loadModule}=require('./utils')constVue=loadModule('vue')if(!Vue||typeofVue.version!=='string'){console.warn('[vue-demi]未找到Vue。请运行“npminstallvue”进行安装。')}elseif(Vue.version.startsWith('2.7.')){switchVersion(2.7)}elseif(Vue.version.startsWith('2.')){switchVersion(2)}elseif(Vue.version.startsWith('3.')){switchVersion(3)}else{console.warn(`[vue-demi]Vue版本v${不支持vue.version}。`)}导入我们项目中安装的vue,然后根据不同的版本调用switchVersion方法。先看一下loadModule方法:functionloadModule(name){try{returnrequire(name)}catch(e){returnundefined}}很简单,就是把require包裹起来,防止错误阻塞代码。然后看switchVersion的方法:,vue)if(version===2)updateVue2API()}执行复制方法。从函数名我们可以大致知道是复制文件。三个文件的类型也很明确,分别是commonjs版本文件、ESM版本文件、TS类型定义文件。另外,updateVue2API方法是针对Vue2.6及以下版本实现的。后面再看updateVue2API方法,先看copy方法:|'vue'constsrc=path.join(dir,`v${version}`,name)constdest=path.join(dir,name)letcontent=fs.readFileSync(src,'utf-8')content=content.replace(/'vue'/g,`'${vue}'`)try{fs.unlinkSync(dest)}catch(error){}fs.writeFileSync(dest,content,'utf-8')}其实和版本目录下复制上面三个文件到outer目录下是不一样的,它也支持替换vue的名字,在给vue设置别名的时候需要用到。至此VueDemi安装完成后自动执行。实际上,根据用户项目安装的Vue版本,从对应的三个目录复制文件作为VueDemi包的入口文件。VueDemi支持三种Module语法:{"main":"lib/index.cjs","jsdelivr":"lib/index.iife.js","unpkg":"lib/index.iife.js","module":"lib/index.mjs","types":"lib/index.d.ts"}默认入口是commonjs模块的cjs文件,支持ESM可以使用mjs文件,还提供了iife类型可以直接在浏览器文件上使用。接下来我们就来看看Vue.js的三个版本都做了些什么。v2版本Vue2.6版本只有一个默认导出:我们只看mjs文件,对cjs感兴趣的可以自行阅读:importVuefrom'vue'importVueCompositionAPIfrom'@vue/composition-api/dist/vue-组合-api。mjs'函数安装(_vue){_vue=_vue||Vueif(_vue&&!_vue['__composition_api_installed__'])_vue.use(VueCompositionAPI)}install(Vue)//...导入Vue和VueCompositionAPI插件,自动调用Vue.use方法安装插件。继续://...varisVue2=truevarisVue3=falsevarVue2=Vuevarversion=Vue.versionexport{isVue2,isVue3,Vue,Vue2,version,install,}/**VCA-EXPORTS**/export*from'@vue/composition-api/dist/vue-composition-api.mjs'/**VCA-EXPORTS**/首先导出isVue2和isVue3两个变量,方便我们库代码判断环境。然后在导出Vue的同时,我也通过Vue2的名字再次导出了。为什么是这样?其实是因为Vue2的API是挂载在Vue对象上的。比如我要进行一些全局的配置,那么只能这样操作:import{Vue,isVue2}from'vue-demi'if(isVue2){Vue.config.xxx}在Vue2中是没有问题的环境,但是当我们的库在Vue3环境下的时候,其实并不是Vue对象需要导入,因为没有用到,但是构建工具不知道,所以会把Vue3的所有代码打包,但是很多Vue3中我们不用的内容是不需要的,但是因为我们导入的所有API包含的Vue对象是无法移除的,所以单独导出一个Vue2版本的Vue2对象,我们可以这样做:import{Vue2}from'vue-demi'if(Vue2){Vue2.config.xxx}然后跟着你会看到Vue3的export里面Vue2是undefined的,所以这个问题就可以解决了。然后导出了Vue版本和安装方法,也就是说可以手动安装VueCompositionAPI插件。然后导出VueCompositionAPI插件提供的API,也就是组合API,但是可以看到前后有两行注释。请记住,上面提到的switchVersion方法中还针对Vue2版本执行了updateVue2API方法。现在让我们看看它做了什么发生了什么:functionupdateVue2API(){constignoreList=['version','default']//检查是否安装了composition-apiconstVCA=loadModule('@vue/composition-api')if(!VCA){console.warn('[vue-demi]CompositionAPIpluginisnotfound.Pleaserun"npminstall@vue/composition-api"toinstall.')return}//获取所有导出的constexceptversionanddefaultexports=Object.keys(VCA).filter(i=>!ignoreList.includes(i))//读取ESM语法入口文件constesmPath=path.join(dir,'index.mjs')letcontent=fs.readFileSync(esmPath,'utf-8')//将export*替换为export{xxx}content=content.replace(/\/\*\*VCA-EXPORTS\*\*\/[\s\S]+\/\*\*VCA-EXPORTS\*\*\//m,`/**VCA-EXPORTS**/export{${exports.join(',')}}from'@vue/composition-api/dist/vue-composition-api.mjs'/**VCA-EXPORTS**/`)//重写文件fs.writeFileSync(esmPath,content,'utf-8')}主要做事情就是检查是否安装了@vue/composition-api,然后过滤掉@vue/composition-api除了version和default之外的所有导出内容,最后:export*from'@vue/composition-api/dist/vue-composition-api.mjs'的形式改写为:export{EffectScope,...}from'@vue/composition-api/dist/vue-composition-api.mjs'为什么要过滤掉version和default,version是因为vue的版本已经导出了,所以会冲突,一开始没必要。默认是默认导出。@vue/composition-api的默认导出实际上是一个包含其install方法的对象。我以前见过。可以默认导入@vue/composition-api,然后通过Vue.use安装。其实这个不需要从vue-demi中导出,不然看起来会很奇怪像下面这样:importVueCompositionAPIfrom'vue-demi'here,exportallcontent,然后我们就可以从vue中导入各种需要的内容-demi,例如:import{isVue2,Vue,ref,reactive,defineComponent}from'vue-demi'v2.7version接下来我们看看Vue2.7和之前的Vue2.6版本相比,内置了@直接vue/composition-api,所以除了默认导出Vue对象外,它还导出一个复合API:{}export{Vue,Vue2,isVue2,isVue3,install,warn}//...和v2相比,导出的内容差不多,因为不需要安装@vue/composition-api,所以install是一个空函数,不同的是还导出了一个warn方法,这个文档中没有提到,但是可以从过去的issues中找到原因。大致就是Vue3导出了一个warn方法,而Vue2的warn方法是在Vue.util对象上的,所以为了统一手动导出,为什么V2版本不手动导出一个呢,原因很简单,因为这个方法在@vue/composition-api的导出中continue://...export*from'vue'//...export上图中所有vue的导出,包括版本和组合API,但是注意这种写法不会导出默认的vue,所以如果你像这样使用默认导入不会得到Vue对象:importVuefrom'vue-demi'Continue://...//createApppolyfillexportfunctioncreateApp(rootComponent,rootProps){varvmvarprovide={}varapp={config:Vue.config,use:Vue.use.bind(Vue),mixin:Vue.mixin.bind(Vue),component:Vue.component.bind(Vue),provide:function(key,value){provide[key]=valuereturnthis},directive:function(name,dir){if(dir){Vue.指令(名称,目录)返回应用程序}else{返回Vue。directive(name)}},mount:function(el,hydrating){if(!vm){vm=newVue(Object.assign({propsData:rootProps},rootComponent,{provide:Object.assign(provide,rootComponent.提供)}))vm.$mount(el,hydrating)returnvm}else{returnvm}},unmount:function(){if(vm){vm.$destroy()vm=undefined}},}returnapp}不同于Vue2新的Vue创建Vue实例。Vue3使用createApp方法。@vue/composition-api插件polyfill这个方法,所以对于Vue2.7,VueDemi手动polyfill到这里,Vue2.7的工作就结束了。v3版本Vue3与之前版本最大的区别是不再提供单独的Vue导出:import*asVuefrom'vue'varisVue2=falsevarisVue3=truevarVue2=undefinedfunctioninstall(){}export{Vue,Vue2,isVue2,isVue3,install,}//...因为Vue对象默认是不导出的,所以通过整体导入import*asVue的方式将导出的全部加载到Vue对象中,然后也可以看到导出的Vue2是未定义的,安装也是一个空函数。Continue://...export*from'vue'//...没什么好说的,直接把vue导出的内容全部导出。继续://...exportfunctionset(target,key,val){if(Array.isArray(target)){target.length=Math.max(target.length,key)target.splice(key,1,val)returnval}target[key]=valreturnval}exportfunctiondel(target,key){if(Array.isArray(target)){target.splice(key,1)return}deletetarget[key]}最后polyfill有两个方法,这两个方法其实都是@vue/composition-api插件提供的,因为@vue/composition-api提供的响应式API没有使用Proxy代理,还是基于Vue2的响应式系统要实现,所以Vue2中响应系统的局限性还是存在的,所以需要提供Vue.set和Vue.delete两种方法来给响应数据添加或删除属性。
