当前位置: 首页 > Web前端 > HTML

组件库按需导入的几种实现方式简析

时间:2023-03-28 19:53:34 HTML

按需加载是所有组件库都提供的基本能力。本文将对ElementUI、Vant、varlet组件库的实现进行分析,并进行相应的实践,帮助您深入了解其实现原理。先搭建一个简单的组件库。作者从ElementUI中复制了两个组件:Alert和Tag,并将我们的组件库命名为XUI。目前的目录结构是这样的:组件放在packages目录下,每个组件都是一个单独的文件夹,最基本的结构就是一个js文件和一个vue文件。组件支持Vue.component注册,也支持插件Vue.use注册。js文件用来支持插件的使用,比如Alert。js文件内容如下:importAlertfrom'./src/main';Alert.install=function(Vue){Vue.component(Alert.name,Alert);};导出默认警报;就是给组件添加一个install方法,这样就可以使用Vue.use(Alert)来注册了。组件的主题文件放在/theme-chalk目录下,每个组件还有一个样式文件。index.css包含所有组件的样式。ElementUI的源代码是一个scss文件。为简单起见,本文直接复制其npm包中编译好的css文件。最外层还有一个index.js文件,很明显是用来作为导出所有组件的入口文件:importAlertfrom'./packages/alert/index.js';importTagfrom'./packages/tag/index.js';constcomponents=[Alert,Tag]constinstall=function(Vue){components.forEach(component=>{Vue.component(component.name,component);});};if(typeofwindow!=='undefined'&&window.Vue){install(window.Vue);}exportdefault{install,Alert,Tag}首先依次导入组件库的所有组件,并然后提供一个install方法,遍历所有的组件,依次使用Vue的.component方法进行注册,然后判断是否有全局的Vue对象。如果是,说明是CDN方式使用,然后自动注册,最后导出install方法和所有组件。Vue插件是一个带有install方法的对象,所以我们可以直接导入所有组件:importXUIfrom'xui'import'xui/theme-chalk/index.css'Vue.use(XUI)也可以注册某个组件:importXUIfrom'xui'import'xui/theme-chalk/alert.css'Vue.use(XUI.Alert)为什么不直接通过import{Alert}form'xui'导入,显然会报错。因为我们的组件库没有发布到npm,所以通过npmlink将我们的组件库链接到全局。接下来笔者使用VueCLI搭建了一个测试项目,运行npmlinkxui链接组件库。然后使用前面的方法注册组件库或者某个组件,这里我们只使用Alert组件。通过测试可以发现,无论是注册所有组件还是只注册Alert组件,最终打包的js中都存在Tag组件的内容:下面打开本文正文,看看如何去除Tag。最简单的按需导入是因为每个组件都可以作为单独的插件使用,所以我们只能导入某个组件,比如:importAlertfrom'xui/packages/alert'import'xui/theme-chalk/alert.css'Vue.use(Alert)这样我们只引入了alert相关的文件,当然最后只会包含alert组件的内容。这种问题使用起来比较麻烦,成本也比较高。比较理想的方式是:import{Alert}from'xui'使用babel插件是目前大部分组件库实现按需导入的方式。ElementUI使用了babel-plugin-component:可以看到直接使用import{Alert}form'xui'方法导入Alert组件即可,不需要手动导入样式。那么这是如何实现的呢?接下来,我们一起来看看吧。极简主义者。原理很简单,我们要的是下面的方式:import{Alert}from'xui'但是实际的按需使用需要这样:importAlertfrom'xui/packages/alert'显然,我们只需要帮助用户把第一种方法转换成第二种方法就可以了,通过babel插件进行转换对用户来说是不敏感的。先添加一个和babel.config.js同级的babel-plugin-component.js文件作为我们的插件文件,然后修改babel.config.js文件:module.exports={//...plugins:['./babel-plugin-component.js']}使用相对路径引用我们的插件,然后就可以愉快的编码了。首先看import{Alert}from'xui'对应的AST树整体是一个ImportDeclaration,导入的来源可以通过source.value判断,导入的变量可以在specifiers数组中找到,每个变量是一个ImportSpecifier,可以看到里面有两个对象:ImportSpecifier.imported和ImportSpecifier.local,两者有什么区别,就是是否使用别名导入,例如:import{Alert}from'xui'中这种情况下,imported和local是一样的,但是如果使用别名:import{Alertasa}from'xui',那么就是这样:我们这里为了简单不考虑别名,只使用imported。下一个任务是转换。看一下importAlertfrom'xui/packages/alert'的AST结构:目标AST结构也很清楚。接下来的事情就简单了。遍历specifiers数组创建一个新的importDeclaration节点,然后替换它。原始节点就足够了://babel-plugin-component.jsmodule.exports=({types})=>{return{visitor:{ImportDeclaration(path){const{node}=pathconst{value}=node.sourceif(value==='xui'){//找出导入的组件名称列表letspecifiersList=[]node.specifiers.forEach(spec=>{if(types.isImportSpecifier(spec)){specifiersList.push(spec.imported.name)}})//为每个组件创建一个导入语句constimportDeclarationList=specifiersList.map((name)=>{//文件夹名首字母小写letlowerCaseName=name.toLowerCase()//构造importDeclaration节点returntypes.importDeclaration([types.importDefaultSpecifier(types.identifier(name))],types.stringLiteral('xui/packages/'+lowerCaseName))})//使用将单节点换成多节点path.replaceWithMultiple(importDeclarationList)}}},}}接下来打包测试结果如下:可以看到Tag组件的内容没有了。当然,上面的实现只是最简单的demo,还需要考虑样式引入、别名、去重、组件中引入、引入某个组件但没有实际使用等各种问题,那些有兴趣可以直接阅读babel-plugin-component的源码。Vant和antd也都采用这种方式,只是使用的插件不同。两者都使用babel-plugin-import,而babel-plugin-component实际上是babel-plugin-import的一个分支。TreeShaking方式Vant组件库不仅支持使用之前的Babel插件按需加载,还支持TreeShaking方式,实现起来也非常简单。Vant最终发布的代码提供了三种标准化的源码,分别是commonjs、umd、esmodule,如下图:commonjs规范是最常见的使用方式,umd一般用于直接引入通过CDN获取页面,并使用esmodule实现TreeShaking。为什么esmodule可以实现treeshaking而commonjs规范不能?原因是esmodule是Staticcompilation,即在编译阶段就可以确定某个模块导出什么,导入什么,代码执行阶段不会改变,所以打包工具可以分析出使用了哪些方法,包装时没有。您可以安全地删除不使用的那些。接下来修改我们的组件库来支持TreeShaking,因为我们的组件本身就是一个esmodule模块,所以不需要修改,但是需要修改导出的文件index.js,因为下面的方法还不支持export:import{Alert}from'xui'添加如下代码://index.js//...export{Alert,Tag}//...接下来我们需要修改package.json,我们都知道在package.json中,esmodule的main字段用来表示包的入口文件,所以将这个字段指向esmodule的入口文件就可以了,但是不可以,因为通常它指向的是入口commonjs模块,一个包可能同时支持nodejs和web环境。nodejs环境可能不支持esmodule模块。由于不能修改旧的字段,只能引入新的字段,即pkg.module,所以修改package.json文件如下://package.json{//..."mains":"index.js","module":"index.js",//添加这个字段//...}因为我们的组件库只有esmodulemodule,所以实际上这两个字段指向同一个。在实际开发中需要像Vant一样编译成不同类型的模块,而发布到npm的模块一般也需要编译成es5语法,因为这些不是本文的重点,所以这一步省略。添加了pkg.module字段。如果打包工具可以识别这个字段,那么会优先使用esmodule规范的代码,但是这里还没结束。这时候Tag组件的内容还是打包后找到的。为什么是这样?不妨看看下面的导入场景:import'core-js'import'style.css'这两个文件只是导入而已,显然没有用到。你能删除它们吗?显然不是,这就是所谓的“副作用”,所以我们需要告诉打包工具哪些文件没有副作用可以删除,哪些是我保留的。VueCLI使用了webpack,相应的我们需要在package.json文件中添加一个sideEffects字段://package.json{//..."sideEffects":["**/*.css"],//...}只有我们组件库中的样式文件有副作用。接下来我们打包测试,发现去掉了没有引入的Tag组件的内容:关于TreeShaking的更多信息可以阅读TreeShaking。使用unplugin-vue-components插件varlet组件库。官方文档的按需导入部分提到了unplugin-vue-components插件的使用:这种方式的好处是不需要自己导入组件,直接在模板中使用,这样由插件扫描、导入和注册。该插件内置了对市场上许多流行组件库的支持。对于内置支持的组件库,只需参考上图导入相应的分析函数并配置即可。但是我们的小破组件库不支持,所以需要自己写这个解析器。首先,这个插件的作用是帮助我们引入组件并进行注册。其实按需加载的功能还是要依赖前两种方法。对于TreeShaking,我们在上一节的基础上修改一下,保留package.json的module和sideEffects配置,然后从main.js中删除组件引入和注册的代码,然后修改vue.config.js文件.因为这个插件的官方文档比较简洁,没有理由,所以作者参考内置的vant解析器修改了它:返回的三个字段的意思应该比较清楚,importName表示导入的组件名称,如Alert,Path表示导入到哪里。对于我们的组件库来说,是xui,sideEffects是一个有副作用的文件,基本上是配置对应样式文件的路径,所以我们修改如下://vue.config.jsconstComponents=require('unplugin-vue-components/webpack')module.exports={configureWebpack:{plugins:[Components({resolvers:[{type:"component",resolve:(name)=>{if(name.startsWith("X")){constpartialName=name.slice(1);return{importName:partialName,path:"xui",sideEffects:'xui/theme-chalk/'+partialName.toLowerCase()+'.css'};}}}]}})]}}作者怕前缀和ElementUI重叠,所以将组件名前缀从El改为X,例如ElAlert改为XAlert,当然模板也需要改成x-alert,然后测试:可以看到运行正常,打包后成功去掉不用的Tag组件内容分离导入最后,我们来看一下分离导入的方法。先去掉pkg.module和pkg.sideEffects字段,然后修改各个组件的index.js文件支持导入如下:import{Alert}from'xui/packages/alert'Alert组件修改如下://index.jsimportAlertfrom'./src/main';Alert.install=function(Vue){Vue.component(Alert.name,Alert);};//添加下面两行export{Alert}exportdefault警报;然后修改我们的解析器:constComponents=require('unplugin-vue-components/webpack')module.exports={configureWebpack:{mode:'production',plugins:[Components({resolvers:[{type:"component",resolve:(name)=>{if(name.startsWith("X")){constpartialName=name.slice(1);return{importName:partialName,//修改path字段指向index.js的每个组件路径:"xui/packages/"+partialName.toLowerCase(),sideEffects:'xui/theme-chalk/'+partialName.toLowerCase()+'.css'};}}}]})]}}其实就是修改path字段指向各个组件的index.js文件,运行测试和打包测试后的结果也符合要求。本文简要分析几种实现组件库按需导入的方式。有组件库开发需求的朋友可以自行选择。示例代码请访问:https://github.com/wanglin2/ComponentLibraryImport。