背景是,基础库迁移到 npm
包,所有引用的地方也要进行迁移。
# 1. ImportDeclaration
对于 AST 而言,有三种引入方式:
import { postA } from 'src/common/post';
import postB from 'src/common/post';
import * as postC from 'src/common/post';
它们都属于 ImportDeclaration
,有一个共同的属性 specifiers
,其为数组,上面有3个例子分别对应3个 specifier
类型,分别为:
ImportSpecifier
,可视为“具名导入”ImportDefaultSpecifier
,可视为“默认导入”ImportNamespaceSpecifier
,可视为“命名空间导入”
下面这种也是 ImportSpecifier
import { postD as request } from 'src/common/post';
知道了它们有不同类型后,怎么拿到具体引入的内容是啥呢?
对于 ImportSpecifier
,有 imported
,local
两个属性,分别代表引入的名字和重命名的名字,比如下面的例子,分别是 postD
和 request
import { postD as request } from 'src/common/post';
如果没有 as
,那么 imported
和 local
就是相等的,比如:
import { postA } from 'src/common/post';
对于 ImportDefaultSpecifier
和 ImportNamespaceSpecifier
,只有 local
属性,对于下面的例子,local
值分别是 postB
和 postC
。
import postB from 'src/common/post';
import * as postC from 'src/common/post';
# 2. 替换的数据结构
构造了一个源和目标的替换关系,类型如下:
type IImportType = 'ImportSpecifier' | 'ImportDefaultSpecifier' | 'ImportNamespaceSpecifier';
// 替换关系,为字符串的时候,代表都为 ImportSpecifier 类型,且 sourceName 和 targetName 相同
// 为数组的时候,代表都为 ImportSpecifier 类型,数组的第1项和第2项分别为 sourceName 和 targetName
// 为对象的时候,就比较直白了
type IImportItem = string | Array<string> | {
sourceName: string;
sourceType: IImportType;
targetName: string;
targetType: IImportType;
};
type IReplaceConfig = {
importedList: Array<IImportItem>;
// 源位置
source: string;
// 目标位置
target: string;
}
# 3. 脚本到不了的地方
如果迁移前后的使用方式不一样,则需要手动改代码,因为不仅涉及 import
部分,还涉及使用部分。
下面列举几种脚本可以处理的情况。
# 3.1. 具名转具名
{
source: 'src/common/network',
importedList: [
'post',
],
target: 'npm-network',
}
可以转换的引入是
// 之前:
import { post } from 'src/common/network';
// 之后:
import { post } from 'npm-network';
// 使用不变
post();
# 3.2. 具名转命名
{
source: 'src/common/tools/dom2image',
importedList: [
{
sourceType: 'ImportSpecifier',
sourceName: 'dom2image',
targetType: 'ImportNamespaceSpecifier',
},
],
target: 'npm-tools/lib/dom2image'
}
可以转换的引入是
// 之前:
import { dom2image } from 'src/common/tools/dom2image';
// 之后:
import * as dom2image from 'npm-tools/lib/dom2image';
// 使用不变
dom2image.url2Base64('xxx')
# 3.3. 默认转默认
{
sourceType: 'ImportDefaultSpecifier',
targetType: 'ImportDefaultSpecifier',
}
可以转换的引入是
// 之前:
import Share from 'src/common/tools/share';
// 之后:
import Share from 'npm-tools/lib/share';
// 使用不变
Share.openShareUI();
# 3.4. 默认转具名
{
sourceType: 'ImportDefaultSpecifier',
targetName: 'MpExposure',
targetType: 'ImportSpecifier',
}
可以转换的引入是
// 之前:
import exposure from 'src/common/report/data-center/mp-exposure';
// 之后:
import { MpExposure as exposure } from "npm-report";
// 使用不变
exposure.init();
exposure.destroy();
# 3.5. 默认转命名
{
sourceType: 'ImportDefaultSpecifier',
targetType: 'ImportNamespaceSpecifier',
}
可以转换的引入是
// 之前:
import Pvpapp from 'src/common/tools/pvpapp';
// 之后:
import * as Pvpapp from 'npm-tools/lib/pvpapp';
// 使用不变
Pvpapp.invoke('xxx');
# 4. 正则 VS AST
用正则替换好处有:
- 简单,实现成本低
弊端在于:
- 无法处理复杂情况,比如夹杂注释
用 AST 替换好处有:
- 可以处理任意复杂情况
弊端在于
- 实现成本相对高
- 可能处理后的结果不符合
eslint
规范,比如一些换行、空格、括号可能会被去掉
综合来看,可以结合二者,具体做法是先用一个错略的正则捕获一个范围,然后对这个范围内的字符串进行 AST 的处理,即 parse
、transform
、generate
。然后拼接其他部分的字符串即可。大致代码如下:
const IMPORT_RE = /import [\s\S]+ from '.*';\n\n/;
function replaceOneJs(content, file) {
const match = content.match(IMPORT_RE);
if (!match) return;
let output = replaceDependencies(match[0]);
if (output === match[0]) {
return;
}
output = content.replace(IMPORT_RE, `${output}\n\n`);
writeFileSync(file, output);
}
# 5. 其他
path.insertAfter 可能会导致条件编译错位,相关issue: https://github.com/babel/babel/issues/7002
没有好的解决办法,目前是在替换过程中记录存在条件编译的文件,然后保存到日志中,二次检查下。