1.使用 unplugin-vue-components 按需引入组件(内附实现原理)
2.elementUI 按需引入的插插件babel-plugin-component
3.了解babel:polyfill、loader、插插件 preset-env及 core之间的插插件关系
4.一口气解决项目中JS 所有精度丢失问题!
5.import方式随意互转,插插件感受babel插件的插插件威力
6.想弄懂Babel?你必须得先弄清楚这几个包
使用 unplugin-vue-components 按需引入组件(内附实现原理)
在开发Vue项目时,通常会利用组件库进行开发,插插件python requests源码分析组件库的插插件加载方式主要有两种:全局引入和按需引入。全局引入组件库虽方便,插插件但往往会导致产物体积过大,插插件对性能要求较高的插插件项目不太友好。
为了解决这个问题,插插件出现了使用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+,wxg源码所以改成@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)`,ims源码从而消除精度丢失。
开发插件需要准备 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-11-28 10:39
2024-11-28 10:27
2024-11-28 10:19
2024-11-28 10:02
2024-11-28 09:35
2024-11-28 08:57