import 方式隨意互轉,感受 babel 插件的威力

zxg_神說要有光 2021-10-13 23:10:27
import 方式 感受 babel 插件


當我們 import 一個模塊的時候,可以這樣默認引入:

import path from 'path';
path.join('a', 'b');
function func() {
const sep = 'aaa';
console.log(path.sep);
}
複制代碼

也可以這樣解構引入:

import { join, sep as _sep } from 'path';
join('a', 'b');
function func() {
const sep = 'aaa';
console.log(_sep);
}
複制代碼

第一種默認引入叫 default import,第二種解構引入叫 named import。

不知道大家習慣用哪一種。

如果有個需求,讓你把所有的 default import 轉成 named import,你會怎麼做呢?

可能你會說這個不就是找到所有用到引入變量的地方,修改成直接調用方法,然後那些方法名以解構的方式寫在 import 語句裏麼。

但如果說要改的項目有 100 多個這種文件呢?(觸發 treeshking 就需要這麼改)

這時候就可以考慮 babel 插件了,它很適合做這種有規律且數量龐大的代碼的自動修改。

讓我們通過這個例子感受下 babel 插件的威力吧。

因為代碼比較多,大家可能沒耐心看,要不我們先看效果吧:

測試效果

輸入代碼是這樣:

import path from 'path';
path.join('a', 'b');
function func() {
const sep = 'aaa';
console.log(path.sep);
}
複制代碼

我們引入該 babel 插件,讀取輸入代碼並做轉換:

const { transformFileSync } = require('@babel/core');
const importTransformPlugin = require('./plugin/importTransform');
const path = require('path');
const { code } = transformFileSync(path.join(__dirname, './sourceCode.js'), {
plugins: [[importTransformPlugin]]
});
console.log(code);
複制代碼

打印如下:

我們完成了 default import 到 named import 的自動轉換。

可能有的同學擔心重名問題,我們測試一下:

可以看到,插件已經處理了重名問題。

思路分析

import 語句中間的部分叫做 specifier,我們可以通過 astexplorer.net 來可視化的查看它的 AST。

比如這樣一條 import 語句:

import React, {useState as test, useEffect} from 'react';
複制代碼

它對應的 AST 是這樣的:

也就是說默認 import 是 ImportDefaultSpecifier,而解構 import 是 ImportSpecifier

ImportSpecifier 語句有 local 和 imported 屬性,分別代錶引入的名字和重命名後的名字:

那我們的目的明確了,就是把 ImportDefaultSpecifier 轉成 ImportSpecifier,並且使用到的屬性方法來設置 imported 屬性,需要重命名的還要設置下 local 屬性。

怎麼知道使用到哪些屬性方法呢?也就是如何分析變量的引用呢?

babel 提供了 scope 的 api,用於作用域分析,可以拿到作用域中的聲明,和所有引用這個聲明的地方。

比如這裏就可以用 scope.getBinding 方法拿到該變量的聲明:

const binding = scope.getBinding('path');
複制代碼

然後用 binding.references 就可以拿到所有引用這個聲明的地方,也就是 path.join 和 path.sep。

之後就可以把這兩處引用改為直接的方法調用,然後修改下 import 語句為解構就可以了。

我們總結一下步驟:

  • 找到 import 語句中的 ImportDefaultSpecifier
  • 拿到 ImportDefaultSpecifier 在作用域的聲明(binding)
  • 找到所有引用該聲明的地方(reference)
  • 修改各處引用為直接調用函數的形式,收集函數名
  • 如果作用域中有重名的變量,則生成一個唯一的函數名
  • 根據收集的函數名來修改 ImportDefaultSpecifierImportSpecifier

原理大概過了一遍,我們來寫下代碼

代碼實現

babel 插件是函數返回對象的形式,返回的對象中主要是通過 visitor 屬性來指定對什麼 AST 做什麼處理。

我們搭一個 babel 插件的骨架:

const { declare } = require('@babel/helper-plugin-utils');
const importTransformPlugin = 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 語句中的 default import
const importDefaultSpecifiers = path.node.specifiers.filter(item => api.types.isImportDefaultSpecifier(item));
// 對每個 default import 做轉換
importDefaultSpecifiers.forEach(defaultSpecifier => {
});
}
複制代碼

然後對每一個 default import 都要根據在作用域中的聲明找到所有引用的地方:

 // import 變量的名字
const importId = defaultSpecifier.local.name;
// 該變量的聲明
const binding = path.scope.getBinding(importId);
binding.referencePaths.forEach(referencePath=> {
});
複制代碼

然後對每個引用到該 import 的地方都做修改,改為直接調用函數,並且把函數名收集起來。這裏要注意的是,如果作用域中有同名變量還要生成一個新的唯一 id。

// 該變量的聲明
const binding = path.scope.getBinding(importId);
const referedIds = [];
const transformedIds = [];
// 收集所有引用該聲明的地方的方法名
binding.referencePaths.forEach(referencePath=> {
const currentPath = referencePath.parentPath;
const methodName = currentPath.node.property.name;
// 之前方法名
referedIds.push(currentPath.node.property);
if (!currentPath.scope.getBinding(methodName)) {// 如果作用域沒有重名變量
const methodNameNode = currentPath.node.property;
currentPath.replaceWith(methodNameNode);
transformedIds.push(methodNameNode); // 轉換後的方法名
} else {// 如果作用域有重名變量
const newMethodName = 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 語句為 named import
const newSpecifiers = referedIds.map((id, index) => api.types.ImportSpecifier(transformedIds[index], id));
path.node.specifiers = newSpecifiers;
複制代碼

沒有 babel 插件基礎可能看的有點暈,沒關系,知道他是做啥的就行。我們接下來試下效果。

思考和代碼

我們做了 default import 到 named import 的自動轉換,其實反過來也一樣,不也是分析 scope 的 binding 和 reference,然後去修改 AST 麼?感興趣的同學可以試下反過來轉換怎麼寫。

插件全部代碼如下:


const { declare } = require('@babel/helper-plugin-utils');
const importTransformPlugin = declare((api, options, dirname) => {
api.assertVersion(7);
return {
visitor: {
ImportDeclaration(path) {
// 找到 import 語句中的 default import
const importDefaultSpecifiers = path.node.specifiers.filter(item => api.types.isImportDefaultSpecifier(item));
// 對每個 default import 做轉換
importDefaultSpecifiers.forEach(defaultSpecifier => {
// import 變量的名字
const importId = defaultSpecifier.local.name;
// 該變量的聲明
const binding = path.scope.getBinding(importId);
const referedIds = [];
const transformedIds = [];
// 收集所有引用該聲明的地方的方法名
binding.referencePaths.forEach(referencePath=> {
const currentPath = referencePath.parentPath;
const methodName = currentPath.node.property.name;
// 之前方法名
referedIds.push(currentPath.node.property);
if (!currentPath.scope.getBinding(methodName)) {// 如果作用域沒有重名變量
const methodNameNode = currentPath.node.property;
currentPath.replaceWith(methodNameNode);
transformedIds.push(methodNameNode); // 轉換後的方法名
} else {// 如果作用域有重名變量
const newMethodName = referencePath.scope.generateUidIdentifier(methodName);
currentPath.replaceWith(newMethodName);
transformedIds.push(newMethodName); // 轉換後的方法名
}
});
// 轉換 import 語句為 named import
const newSpecifiers = referedIds.map((id, index) => api.types.ImportSpecifier(transformedIds[index], id));
path.node.specifiers = newSpecifiers;
});
}
}
}
});
module.exports = importTransformPlugin;
複制代碼

總結

我們要做 default import 轉 named import,也就是 ImportDefaultSpecifierImportSpecifier,要通過 scope 的 api 分析 binding 和 reference,找到所有引用的地方,替換成直接調用函數的形式,然後再去修改 import 語句的 AST 就可以了。

babel 插件特別適合做這種有規律且轉換量比較大的需求,在一些場景下是有很大的威力的。

版权声明
本文为[zxg_神說要有光]所创,转载请带上原文链接,感谢
https://qdmana.com/2021/10/20211013231026664r.html

  1. 为什么说 Node.js 是实时应用程序开发的绝佳选择
  2. PaddlePaddle:在 Serverless 架构上十几行代码实现 OCR 能力
  3. 使用elementui在完成项目中遇到的未知知识点2
  4. On the mechanism of webpack loader
  5. 云原生体系下 Serverless 弹性探索与实践
  6. vue开发技巧
  7. Une fleur merveilleuse de l'histoire de l'industrie des nouveaux véhicules énergétiques, Zhongtai Jiangnan T11, une voiture vintage que vous n'avez jamais vue
  8. 致敬!再见了!LayUI !
  9. Vue安装和卸载
  10. Implement a flipped character with the transform attribute of CSS
  11. 你的第一个 Docker + React + Express 全栈应用
  12. [apprentissage de l'algorithme] 1486. Fonctionnement exclusif du tableau (Java / C / C + + / python / go / Rust)
  13. Zhang Daxian sends a blessing video on xYG relay, showing positive energy in details
  14. 前端技巧-JS元编程ES6 symbol公开符号
  15. Article de 37 ans seul à l'hôpital!Il boitait, soupçonnait d'être blessé, souriait avec douleur
  16. 前端推荐!10分钟带你了解Konva运行原理
  17. npm ERR! iview-project@3.0.0 init: `webpack --progress --config webpack.dev.config.js
  18. 零基础学习Web前端需要注意什么呢?
  19. The Youth League promotes Yiyang Qianxi new film, and the relationship between the two generation and the generation is good. Li Fei is blessed.
  20. PaddlePaddle:在 Serverless 架构上十几行代码实现 OCR 能力
  21. JavaScript数组 几个常用方法
  22. Qu'est - ce qu'il faut remarquer à l'avant - plan Web de l'apprentissage de base zéro?
  23. 暢談this的四種綁定方式
  24. 2021最新Vue面试必胜宝典,大厂面试题解析!
  25. Quatre façons de lier ceci
  26. Préparation au développement de l'extension tagdown
  27. Intervieweur: Parlez - moi des flotteurs CSS
  28. Packaging the View Component Library with rollup
  29. Comment un composant enfant modifie les valeurs passées par le composant parent
  30. Résumé de l'API Express
  31. Optimisation de la structure du Code if else dans le projet
  32. Fonction magique pour résoudre le problème de la fonction maybe - - fonction either
  33. 新手学前端的方法是什么 自学前端该怎么规划
  34. 云原生体系下 Serverless 弹性探索与实践
  35. 如何全方位打造安全高效的HTTPS站点(一)
  36. "Liu Jing dit che 丨 point de vue" est - ce que Custom Road est un MpV digne de la terre?
  37. 从理念到LRU算法实现,起底未来React异步开发方式
  38. Compared with Volvo XC60, Lingke 09 goes out of the spa platform. What would you choose, regardless of the brand?
  39. PaddlePaddle:在 Serverless 架构上十几行代码实现 OCR 能力
  40. 云原生体系下 Serverless 弹性探索与实践
  41. 初学者怎么学Web前端?
  42. react
  43. PaddlePaddle:在 Serverless 架构上十几行代码实现 OCR 能力
  44. JavaScript数组 几个常用方法
  45. Angular 依赖注入 - 全面解析
  46. html_day02
  47. 那些年我们前端面试中经常被问到的题!
  48. The starting price of Ducati multistada V2 in North America is less than 100000 yuan
  49. Hls.js 使用文檔
  50. Hls.js travailler avec des documents
  51. Problèmes liés à la précision JS
  52. Copie une partie des propriétés d'un objet à un autre objet
  53. Multiplexage de modules en vuex
  54. Développement multilingue Android, questions d'entrevue pour le développement de logiciels Android
  55. Chen lushai and her best friend Wang Meng play video, fearless of the pressure of public opinion, and in a good mood to dance in a bare back
  56. # Sass速通(四):继承、混合与函数
  57. Vidéo de développement de combat Android, questions d'entrevue rxjava
  58. Bugatti Chiron maintenance cost exposure! One piece for one car, burn money endlessly
  59. android应用开发基础答案,深入理解Nginx
  60. 做了三年前端,你才知道10分钟就能实现一个PC版魔方游戏