使用 unplugin-vue-components 按需引入组件(内附实现原理)
在开发Vue项目时,通常会利用组件库进行开发,插插件源码冰晨科技组件库的插插件加载方式主要有两种:全局引入和按需引入。全局引入组件库虽方便,插插件但往往会导致产物体积过大,插插件对性能要求较高的插插件项目不太友好。
为了解决这个问题,插插件出现了使用babel-plugin-import进行按需加载的插插件解决方案。它可以省去style的插插件引入,但还是插插件需要手动引入组件,且需要依赖babel插件。插插件而unplugin-vue-components的出现,使得开发者无需手动引入组件,能够像全局组件那样开发,但实际上是在进行按需引入,而且不限制打包工具,无需使用babel。
以Antd Vue和vite为例,unplugin-vue-components能够自动引入Antd Vue的组件,无需手动import组件以及组件样式,使用起来就像全局组件一样,但这是按需自动引入,可以减少产物大小。直接使用即可,unplugin-vue-components为主流的UI组件库提供了内置支持,通过使用对应UI组件库的解析器(resolvers),就能自动引入对应的组件库组件及样式。
解析器可以是一个函数或是一个对象。以对象为例,resolve是一个函数,当遇到特定组件时(如a-button),它会调用该函数,并返回一个对象。unplugin-vue-components会根据这个返回的对象修改编译后的代码,从而实现按需引入。
unplugin-vue-components的实现原理非常简单,它通过正则匹配Vue的全局组件(编译后使用_resolveComponent包裹),然后引入组件并替换_resolveComponent,从而实现将全局使用转换为按需引入的方式。
unplugin-vue-components也存在局限性,但总体上,其哥一线天选股公式源码它能够非常方便地实现按需引入组件的功能,从而减小项目体积、加快项目加载速度,提升用户体验。它能够自动引入项目components目录下的组件,也支持自定义指定的自动按需引入,更多内容请查看unplugin-vue-components文档。
使用unplugin-vue-components能够实现更加高效、便捷的组件引入方式,为项目开发提供便利,提升开发效率和项目性能。尝试使用unplugin-vue-components,让您的Vue项目开发更加轻松、高效。
elementUI 按需引入的babel-plugin-component
为什么要使用babel-plugin-component来实现按需引入?
babel-plugin-component是一个为element-ui项目单独开发的babel模块化构建插件。
最初我以为这是一个通用的babel插件,直到在GitHub上看到介绍,才了解到它实际上是element-ui针对自身项目开发的。
因此,只有使用这种静态路径转换方案,才能正确引入element-ui。因为它是专门针对element-ui的babel插件。
如果你不使用此插件,只是按照以下方式进行引入
项目也能正常运行,但build打包的体积将不会减少,和完全引入element-ui一样,体积相同。因此,即使使用import { Button, Select } from 'element-ui'这种引入方式,webpack的import依旧会引入整个element-ui包,因为webpack不知道element-ui包内部子级包的存放路径规则,所以必须完整引入。
使用此插件的细节:
vue的项目默认有一个presets预设规则,
而babel的presets数组是有先后顺序的,所以我暂时也不知道这两个应该谁先谁后,但是我测试都是可以的。
vue预设在前dist目录为1.7M,@babel/preset-env在前dist目录是1.8M,
不使用babel-plugin-component插件dist目录是5.6M。
细节2是,vuecli搭建的vue2项目,现在是使用babel7,而预设es的名字改成了@babel/preset-env,用于编译成ES+,源码资本第三期人民币基金所以改成@babel/preset-env就好了。
了解babel:polyfill、loader、 preset-env及 core之间的关系
在使用webpack配置babel解析es6语法的过程中,我们通常仅按照文档说明进行配置,而并未深入了解babel工具链的运作机制。以下是对相关工具链的回顾:
我们经常接触到的有babel、babel-loader、@babel/core、@babel/preset-env、@babel/polyfill以及@babel/plugin-transform-runtime,它们各自的作用是什么?
1、babel:根据babel官网的定义,babel是一个工具链,主要用于将ECMAScript +代码转换为向后兼容版本的JavaScript代码。它不仅包含语法转换等功能,还可以通过@babel/polyfill实现目标环境中缺少的功能。
需要注意的是,babel是一个可以安装的包,并且在webpack 1.x配置中使用它作为loader的简写。然而,这种方式在webpack 2.x以后不再支持,并会出现错误提示。此时,我们需要删除babel包,安装babel-loader,并指定loader: 'babel-loader'。
2、@babel/core:@babel/core是babel的核心库,包含了所有的核心Api,这些Api供babel-loader调用。
3、@babel/preset-env:这是一个预设的插件集合,包含了一组相关的插件,用于指导Babel如何进行代码转换。该插件包含所有es6转化为es5的翻译规则。
4、@babel/polyfill:@babel/preset-env提供了语法转换的规则,但无法弥补浏览器缺失的一些新功能。此时,我们需要polyfill来作为JavaScript的垫片,弥补低版本浏览器缺失的新功能。
需要注意的是,polyfill的谁有百度网盘共享资源码体积很大,如果我们不做特殊说明,它会将目标浏览器中缺失的所有es6的新功能都做垫片处理。为了解决这个问题,我们通常在presets的选项里配置"useBuiltIns": "usage",这样只对使用的新功能做垫片,同时也不需要单独引入import '@babel/polyfill'。
5、babel-loader:当使用webpack打包js时,webpack并不知道如何调用这些规则去编译js。此时,就需要babel-loader作为中间桥梁,通过调用babel/core中的api来告诉webpack如何处理js。
6、@babel/plugin-transform-runtime:polyfill的垫片是在全局变量上挂载目标浏览器缺失的功能,因此在开发类库、第三方模块或组件库时,不能使用babel-polyfill,否则可能会造成全局污染。此时,应使用transform-runtime,它的转换是非侵入性的,不会污染原有的方法。
一口气解决项目中JS 所有精度丢失问题!
在 JavaScript 中,精度丢失问题是一个常见的挑战。当你试图计算像 0.1 + 0.2 的结果时,实际输出的可能并非你期待的 0.3。这是因为 JavaScript 的浮点数存储机制导致的。要解决这个问题,可以借助 decimal.js 这个库,它提供了高精度的数学运算。
decimal.js 的使用相当简单,只需引入库并进行相应的操作,如 `new Decimal(0.1).add(0.2)`,就能得到正确的结果。然而,频繁手动引入和使用 decimal.js 可能会变得繁琐,尤其是对于大型项目。
为了解决这个问题,我决定创建一个 babel 插件。通过利用 babel 插件,我们可以自动将项目中的浮点数运算转换为 decimal.js 的形式。例如,表达式 `0.1 + 0.2` 将被转换为 `new Decimal(0.1).add(0.2)`,一个人的主页源码怎么找从而消除精度丢失。
开发插件需要准备 Rollup 打包环境,定义一个 babel 插件文件,并配置 Rollup。在插件代码中,我们利用抽象语法树(AST)进行操作,找出浮点数运算节点并替换为 decimal.js 的相应方法。关键点在于遍历 AST,并在适当位置插入 decimal.js 的导入和调用。
实现后,通过 `npm run build` 打包并发布到 NPM,然后在项目中安装并配置 babel-plugin-sx-accuracy。例如,只需在 .babelrc 或 babel.config.js 中添加该插件,代码如 `console.log(0.1 + 0.2)` 就会自动转换为 decimal.js 的精确运算,输出结果不会丢失精度。这样,你就可以在项目中轻松避免精度问题,而无需频繁手动处理。
import方式随意互转,感受babel插件的威力
当我们import一个模块的时候,可以这样默认引入:importpathfrom'path';path.join('a','b');functionfunc(){ constsep='aaa';console.log(path.sep);}也可以这样解构引入:
import{ join,sepas_sep}from'path';join('a','b');functionfunc(){ constsep='aaa';console.log(_sep);}第一种默认引入叫defaultimport,第二种解构引入叫namedimport。
不知道大家习惯用哪一种。
如果有个需求,让你把所有的defaultimport转成namedimport,你会怎么做呢?
可能你会说这个不就是找到所有用到引入变量的地方,修改成直接调用方法,然后那些方法名以解构的方式写在import语句里么。
但如果说要改的项目有多个这种文件呢?(触发treeshking就需要这么改)
这时候就可以考虑Babel插件了,它很适合做这种有规律且数量庞大的代码的自动修改。
让我们通过这个例子感受下babel插件的威力吧。
因为代码比较多,大家可能没耐心看,要不我们先看效果吧:
测试效果输入代码是这样:
importpathfrom'path';path.join('a','b');functionfunc(){ constsep='aaa';console.log(path.sep);}我们引入该babel插件,读取输入代码并做转换:
const{ transformFileSync}=require('@babel/core');constimportTransformPlugin=require('./plugin/importTransform');constpath=require('path');const{ code}=transformFileSync(path.join(__dirname,'./sourceCode.js'),{ plugins:[[importTransformPlugin]]});console.log(code);打印如下:
我们完成了defaultimport到namedimport的自动转换。
可能有的同学担心重名问题,我们测试一下:
可以看到,插件已经处理了重名问题。
思路分析import语句中间的部分叫做specifier,我们可以通过astexplorer.net来可视化的查看它的AST。
比如这样一条import语句:
importReact,{ useStateastest,useEffect}from'react';它对应的AST是这样的:
也就是说默认import是ImportDefaultSpecifier,而解构import是ImportSpecifier
ImportSpecifier语句有local和imported属性,分别代表引入的名字和重命名后的名字:
那我们的目的明确了,就是把ImportDefaultSpecifier转成ImportSpecifier,并且使用到的属性方法来设置imported属性,需要重命名的还要设置下local属性。
怎么知道使用到哪些属性方法呢?也就是如何分析变量的引用呢?
babel提供了scope的api,用于作用域分析,可以拿到作用域中的声明,和所有引用这个声明的地方。
比如这里就可以用scope.getBinding方法拿到该变量的声明:
constbinding=scope.getBinding('path');然后用binding.references就可以拿到所有引用这个声明的地方,也就是path.join和path.sep。
之后就可以把这两处引用改为直接的方法调用,然后修改下import语句为解构就可以了。
我们总结一下步骤:
找到import语句中的ImportDefaultSpecifier
拿到ImportDefaultSpecifier在作用域的声明(binding)
找到所有引用该声明的地方(reference)
修改各处引用为直接调用函数的形式,收集函数名
如果作用域中有重名的变量,则生成一个唯一的函数名
根据收集的函数名来修改ImportDefaultSpecifier为ImportSpecifier
原理大概过了一遍,我们来写下代码
代码实现babel插件是函数返回对象的形式,返回的对象中主要是通过visitor属性来指定对什么AST做什么处理。
我们搭一个babel插件的骨架:
const{ declare}=require('@babel/helper-plugin-utils');constimportTransformPlugin=declare((api,options,dirname)=>{ api.assertVersion(7);return{ visitor:{ ImportDeclaration(path){ }}}});module.exports=importTransformPlugin;这里我们要处理的是import语句ImportDeclaration。
@babel/helper-plugin-utils包的declare方法的作用是给api扩充一个assertVersion方法。而assertVersion的作用是如果这个插件工作在了babel6上就会报错说这个插件只能用在babel7,可以避免报的错看不懂。
path是用于操作AST的一些api,而且也保留了node之间的关联,比如parent、sibling等。
接下来进入正题:
我们要先取出specifiers的部分,然后找出ImportDefaultSpecifier:
ImportDeclaration(path){ //找到import语句中的defaultimportconstimportDefaultSpecifiers=path.node.specifiers.filter(item=>api.types.isImportDefaultSpecifier(item));//对每个defaultimport做转换importDefaultSpecifiers.forEach(defaultSpecifier=>{ });}然后对每一个defaultimport都要根据在作用域中的声明找到所有引用的地方:
//import变量的名字constimportId=defaultSpecifier.local.name;//该变量的声明constbinding=path.scope.getBinding(importId);binding.referencePaths.forEach(referencePath=>{ });然后对每个引用到该import的地方都做修改,改为直接调用函数,并且把函数名收集起来。这里要注意的是,如果作用域中有同名变量还要生成一个新的唯一id。
//该变量的声明constbinding=path.scope.getBinding(importId);constreferedIds=[];consttransformedIds=[];//收集所有引用该声明的地方的方法名binding.referencePaths.forEach(referencePath=>{ constcurrentPath=referencePath.parentPath;constmethodName=currentPath.node.property.name;//之前方法名referedIds.push(currentPath.node.property);if(!currentPath.scope.getBinding(methodName)){ //如果作用域没有重名变量constmethodNameNode=currentPath.node.property;currentPath.replaceWith(methodNameNode);transformedIds.push(methodNameNode);//转换后的方法名}else{ //如果作用域有重名变量constnewMethodName=referencePath.scope.generateUidIdentifier(methodName);currentPath.replaceWith(newMethodName);transformedIds.push(newMethodName);//转换后的方法名}});这部分逻辑比较多,着重讲一下。
我们对每个引用了该变量的地方都要记录下引用了哪个方法,比如path.join、path.sep就引用了join和sep方法。
然后就要把path.join替换成join,把path.sep替换成sep。
如果作用域中有了join或者sep的声明,需要生成一个新的id,并且记录下新的id是什么。
收集了所有的方法名,就可以修改import语句了:
import{ join,sepas_sep}from'path';join('a','b');functionfunc(){ constsep='aaa';console.log(_sep);}0没有babel插件基础可能看的有点晕,没关系,知道他是做啥的就行。我们接下来试下效果。
思考和代码我们做了defaultimport到namedimport的自动转换,其实反过来也一样,不也是分析scope的binding和reference,然后去修改AST么?感兴趣的同学可以试下反过来转换怎么写。
插件全部代码如下:
import{ join,sepas_sep}from'path';join('a','b');functionfunc(){ constsep='aaa';console.log(_sep);}1总结我们要做defaultimport转namedimport,也就是ImportDefaultSpecifier转ImportSpecifier,要通过scope的api分析binding和reference,找到所有引用的地方,替换成直接调用函数的形式,然后再去修改import语句的AST就可以了。
babel插件特别适合做这种有规律且转换量比较大的需求,在一些场景下是有很大的威力的。
想弄懂Babel?你必须得先弄清楚这几个包
Babel 是一个工具链,用于将采用 ECMAScript + 语法编写的代码转换为兼容旧版本的 JavaScript 语法,以在当前和旧版本的浏览器或其他环境中运行。
Babel 主要通过 plugins、presets 这两个配置来实现转换功能。plugins 负责具体语法转换,而 presets 是一个语法插件集合包,简化了新特性的配置过程。
@babel/core 是 Babel 实现编译的核心,是使用 Babel 的必要组件。版本如 Babel 6、Babel 7 实际指的就是 @babel/core 的版本。
@babel/cli 是 Babel 自带的 CLI 命令行工具,允许在终端中编译文件,便于调试并提供打印信息功能。安装时,最好将其安装到项目本地目录,避免全局安装。
@babel/preset-env 是一个智能预设,支持使用最新 JavaScript 特性,无需微观管理目标环境所需语法转换及浏览器补丁的导入。这简化了编码过程,使代码包更小。
@babel/preset-env 的 preset 是语法插件集合,env 指运行目标环境,控制补丁导入和语法编译,确保 ES6+ 特性在目标环境中正常运行。
@babel/preset-env 提供以下主要功能:不包括 stage-3 以下的语法提案,因为这些在 TC 过程中未被浏览器实现。用户需手动配置相应的 plugin。
@babel/polyfill 是依赖 core-js@2.x.x 实现的 polyfill 包,用于提供旧版本浏览器缺失的 API。它与 2 版本的 core-js 绑定,从 3 版本开始才有 stable 文件夹。
@babel/runtime 是一个包含 Babel 模块化运行时助手的库,辅助函数实现 ES6+ 语法糖的内联插入,节省代码大小。
@babel/plugin-transform-runtime 插件用于重用 Babel 注入的帮助程序代码,避免重复代码生成,减少最终输出包体积。
Babel 的使用结合 preset-env、polyfill、runtime 插件,能高效地编译 ES6+ 代码,适应多种目标环境,同时减小代码包大小,优化开发流程。
Babel教程7:Babel配置文件
在深入学习Babel配置文件之前,我们先了解其作用。无论是通过命令行工具babel-cli,还是构建工具如webpack进行编译,都需要配置文件来指定编译规则。Babel的配置文件是默认在当前目录寻找的文件,如.babelrc、.babelrc.js、babel.config.js和package.json。它们配置项相同,作用一致,选择其一即可。对于.babelrc文件,配置如下。babel.config.js和.babelrc.js通过module.exports输出配置项。在package.json中增加babel属性配置。观察这些配置文件,会发现配置项主要是plugins和presets。
配置文件的主要内容是配置plugins和presets数组,我们称其为插件数组和预设数组。除了plugins和presets,还有minified、ignore等配置项,但这些在日常使用中很少用到,因此建议专注于plugins和presets的学习。推荐使用后缀名js的配置文件,因为它可以使用JavaScript逻辑,更灵活。例如:
在配置文件中使用插件和预设时,要明确它们的作用。插件和预设分别位于plugins和presets数组中,是npm包。Babel提供了大量插件和预设,处理不同版本的ECMAScript标准。例如,处理ES、ES等。所有插件和预设在使用前需要先安装到node_modules中。
配置文件中的插件和预设数量众多,编写时可能会显得臃肿。为了解决这一问题,Babel提供了预设,预设是一组插件的集合,简化了配置。例如,babel-preset-es包含处理ES所需的所有插件。这样只需配置预设,而无需列出所有插件,减少了配置工作量。预设也可以是插件和其它预设的组合。
配置文件中的插件和预设可以使用短名称,如babel-plugin-前缀的插件,可以省略前缀。同样,预设名称前缀为babel-preset-或@xxx/babel-preset-xxx的可以省略部分前缀。但是,推荐使用全称以避免可能的混淆。
在配置文件的plugins插件数组和presets预设数组中,遵循一定的顺序规则。如果两个插件或预设需要处理同一段代码,那么将按照插件和预设的顺序执行。
每个插件或预设数组成员项默认为字符串形式,表示名称。若需设置参数,则将其转换为数组形式,包含插件或预设名称和参数对象。
综上所述,Babel配置文件的使用涵盖了插件、预设的配置以及参数设置等内容。在实际开发中,根据项目需求选择合适的插件和预设。后续教程将详细介绍如何在开发中选择适合的Babel插件和预设。
解剖Babel —— 向前端架构师迈出一小步
解剖Babel,前端架构师的探索之旅
Babel,作为前端工程基石,其功能远超API polyfill的简单定义。它是一个JavaScript编译器,负责接收并处理代码,转化为兼容目标环境的代码。这个过程涉及到Babel的核心组件,如preset、plugin和runtime等。
尽管preset和plugin概念初学者可能感到困惑,但它们是Babel实现编译和扩展功能的关键。preset是插件的集合,允许根据特定目标环境动态调整编译行为。而plugin则提供了插件化接口,开发者可以通过它们定制编译过程。
深入Babel的底层,核心模块如@babel/parser解析JavaScript源代码,生成抽象语法树(AST),再由@babel/traverse、@babel/types和@babel/generator等处理,最终输出修改后的代码。核心库@babel/core负责整合这些功能。
上层功能中,Babel通过polyfill和语法转换支持高级特性向低版本浏览器和环境兼容。@babel/polyfill和@babel/preset-env是实现这些功能的重要工具,前者是core-js和regenerator-runtime的组合,而后者则允许按需加载特性,优化打包体积。
学习Babel对前端架构师来说至关重要,它涉及的底层模块如@babel/plugin-*提供了API接入点,而preset-*如@babel/preset-env则是日常开发中的实用工具。掌握这些,对构建高效、兼容的前端工程至关重要。
参考资料:[1] Babel仓库: github.com/babel/babel/
[2] AST explorer: astexplorer.net/
[3] core-js仓库: github.com/zloirock/core-js/
[4] Browserslist: github.com/browserslist/
[5] Babel v7.4.0: babeljs.io/docs/en/babel/
[6] babel-plugin-syntax-decorators: github.com/babel/babel/
[7] Babel playground: babeljs.io/repl/
2024-12-28 23:18
2024-12-28 22:59
2024-12-28 22:52
2024-12-28 22:49
2024-12-28 21:26