2023年4月

一、使用git rebase命令

如果您想彻底删除 Git 中的某次提交的内容,可以使用
git rebase
命令并将该提交删除。

以下是删除 Git 提交内容的步骤:

  1. 找到要删除的提交的哈希值。可以使用
    git log
    命令查看提交历史记录,然后找到要删除的提交的哈希值。

  2. 在终端中使用
    git rebase -i
    命令并指定要删除的提交的哈希值。例如,要删除哈希值为
    abc123
    的提交,您可以运行以下命令:

git rebase -i abc123~1
git rebase -i HEAD~1

在这里,
abc123~1
表示要删除的提交的前一个提交。

  1. 然后 Git 会打开一个交互式编辑器,并显示一个类似于下面的文本:
pick abc123 commit message
pick def456 another commit message
pick ghi789 yet another commit message
  1. 将要删除的提交所在的行的单词
    pick
    替换为单词
    drop
    。例如,如果要删除
    abc123
    ,则应将第一行更改为:
drop abc123 commit message
  1. 保存更改并关闭编辑器。

  2. 然后 Git 会自动重播您的提交,并在重播时跳过要删除的提交。如果需要,您可能需要解决任何冲突,并提交最终更改。

注意,使用
git rebase
命令来删除提交可以永久删除提交并更改 Git 历史记录。如果您不确定如何使用
git rebase
命令,建议在使用前备份您的 Git 存储库。

二、使用git revert命令

使用
git revert
命令可以撤销指定提交的更改,并创建一个新的提交来保留已撤销的更改的历史记录。

以下是使用
git revert
撤销提交的步骤:

  1. 找到要撤销的提交的哈希值。可以使用
    git log
    命令查看提交历史记录,然后找到要撤销的提交的哈希值。

  2. 在终端中使用
    git revert
    命令并指定要撤销的提交的哈希值。例如,要撤销哈希值为
    abc123
    的提交,您可以运行以下命令:

git revert abc123
  1. 然后 Git 会打开一个编辑器,让您输入此次撤销提交的注释。如果您希望使用默认注释,请直接关闭编辑器。

  2. 最后,Git 将创建一个新的提交,用于撤销您要撤销的提交所做的更改。这个新的提交将保留已撤销的更改的历史记录。您可以使用
    git log
    命令检查您的提交历史记录以确认撤销操作已成功完成。

需要注意的是,使用
git revert
命令撤销提交会创建一个新的提交,这意味着您的 Git 历史记录将包含一个新的提交来保留已撤销的更改的历史记录。这与使用
git reset
命令删除提交不同,后者会直接删除提交及其历史记录。

三、使用git reset命令

使用
git reset
命令可以将当前分支的 HEAD 指针移到任意提交,从而使您能够删除 Git 存储库中的提交。

以下是使用
git reset
删除提交的步骤:

  1. 找到要删除的提交的哈希值。可以使用
    git log
    命令查看提交历史记录,然后找到要删除的提交的哈希值。

  2. 使用以下命令来删除该提交:

git reset --hard <commit-hash>

其中
<commit-hash>
是您要删除的提交的哈希值。

  1. 运行此命令后,Git 将删除所有在该提交之后进行的更改,并将您的当前分支 HEAD 指针移动到要删除的提交上。

需要注意的是,使用
git reset
命令删除提交会更改 Git 存储库的历史记录,这意味着您的提交历史记录将会被修改。因此,您应该谨慎使用此命令,并确保您已经了解了删除提交的后果。

另外,请注意,使用
git reset --hard
命令删除提交时,将会删除该提交及其之后的所有更改。如果您只想删除该提交本身而不影响其他更改,请使用
git reset --soft
命令。这将将 HEAD 指针移到要删除的提交上,但不会更改 Git 存储库中的文件或文件夹。这样,您就可以重新提交您想要保留的更改

4.如果您已经将更改推送到 Git 存储库的远程分支,并且想要删除提交,可以使用
git push --force
命令将更改强制推送到远程分支。但是,请注意,强制推送会更改远程分支的历史记录,并且可能会影响其他人正在使用的分支。因此,在进行强制推送之前,应该确保其他人知道并同意这样做。

使用以下命令将更改强制推送到远程分支:

git push --force <remote-name> <branch-name>  

其中
<remote-name>
是远程仓库的名称,
<branch-name>
是要推送更改的分支名称。此命令将强制将更改推送到远程分支,并覆盖远程分支的历史记录。如果其他人正在使用该分支,则可能需要通知他们进行相应的更改。

请注意,强制推送更改可能会对其他人造成影响,因此应该谨慎使用。如果您想要删除提交,但不想对其他人造成影响,则可以使用
git revert
命令来撤消该提交所做的更改,而不会修改 Git 存储库的历史记录。

相比于之前写的ResNet18,下面的ResNet50写得更加工程化一点,这还适用与其他分类,就是换一个分类训练只需要修改图片数据的路径即可。

我的代码文件结构

1. 数据处理

首先已经对数据做好了分类

文件夹结构是这样

开始划分数据集

split_data.py

importosimportrandomimportshutildef move_file(target_path, save_train_path, save_val_pathm, scale=0.1):

file_list
=os.listdir(target_path)
random.shuffle(file_list)

number
= int(len(file_list) *scale)
train_list
=file_list[number:]
val_list
=file_list[:number]for file intrain_list:
target_file_path
=os.path.join(target_path, file)
save_file_path
=os.path.join(save_train_path, file)
shutil.copyfile(target_file_path, save_file_path)
for file inval_list:
target_file_path
=os.path.join(target_path, file)
save_file_path
=os.path.join(save_val_pathm, file)
shutil.copyfile(target_file_path, save_file_path)
def split_classify_data(base_path, save_path, scale=0.1):
folder_list
=os.listdir(base_path)for folder infolder_list:
target_path
=os.path.join(base_path, folder)
save_train_path
= os.path.join(save_path, 'train', folder)
save_val_path
= os.path.join(save_path, 'val', folder)if notos.path.exists(save_train_path):
os.makedirs(save_train_path)
if notos.path.exists(save_val_path):
os.makedirs(save_val_path)
move_file(target_path, save_train_path, save_val_path, scale)
print(folder, 'finish!')if __name__ == '__main__':
base_path
= r'C:\Users\Administrator.DESKTOP-161KJQD\Desktop\save_dir'save_path= r'C:\Users\Administrator.DESKTOP-161KJQD\Desktop\dog_cat' #验证集比例 scale = 0.1split_classify_data(base_path, save_path, scale)

运行完以上代码的到的文件夹结构

一个训练集数据,一个验证集数据

2.数据集的导入

我这个文件写了一个数据集的导入和一个学习率更新的函数。数据导入是通用的

tools.py

importosimporttimeimportcv2importnumpy as npimporttorchimporttorch.nn as nnimporttorch.nn.functional as Fimporttorch.optim as optimimporttorchvisionfrom torch.autograd.variable importVariablefrom torch.utils.tensorboard importSummaryWriterfrom torchvision importdatasets, transformsfrom torch.utils.data importDataset, DataLoaderfrom torch.optim.lr_scheduler importExponentialLR, LambdaLRfrom torchvision.models importResNet50_Weightsfrom tqdm importtqdmfrom classify_cfg import *mean=MEAN
std
=STDdef get_dataset(base_dir='', input_size=160):
dateset
=dict()
transform_train
=transforms.Compose([#分辨率重置为input_size transforms.Resize(input_size),
transforms.RandomRotation(
15),#对加载的图像作归一化处理, 并裁剪为[input_sizexinput_sizex3]大小的图像(因为这图片像素不一致直接统一) transforms.CenterCrop(input_size),
transforms.ToTensor(),
transforms.Normalize(mean
=mean, std=std)
])

transform_val
=transforms.Compose([
transforms.Resize(input_size),
transforms.RandomRotation(
15),
transforms.CenterCrop(input_size),
transforms.ToTensor(),
transforms.Normalize(mean
=mean, std=std)
])
base_dir_train
= os.path.join(base_dir, 'train')
train_dataset
= datasets.ImageFolder(root=base_dir_train, transform=transform_train)#print("train_dataset=" + repr(train_dataset[1][0].size())) #print("train_dataset.class_to_idx=" + repr(train_dataset.class_to_idx)) #print(train_dataset.classes) classes =train_dataset.classes#classes = train_dataset.class_to_idx classes_num =len(train_dataset.classes)

base_dir_val
= os.path.join(base_dir, 'val')
val_dataset
= datasets.ImageFolder(root=base_dir_val, transform=transform_val)

dateset[
'train'] =train_dataset
dateset[
'val'] =val_datasetreturndateset, classes, classes_numdefupdate_lr(epoch, epochs):"""假设开始的学习率lr是0.001,训练次数epochs是100
当epoch<33时是lr * 1
当33<=epoch<=66 时是lr * 0.5
当66<epoch时是lr * 0.1
""" if epoch == 0 or epochs // 3 >epoch:return 1 elif (epochs // 3 * 2 >= epoch) and (epochs // 3 <=epoch):return 0.5 else:return 0.1

3.训练模型

数据集导入好了以后,选择模型,选择优化器等等,然后开始训练。

mytrain.py

importosimporttimeimportcv2importnumpy as npimporttorchimporttorch.nn as nnimporttorch.optim as optimimporttorchvisionfrom torch.autograd.variable importVariablefrom torch.utils.tensorboard importSummaryWriterfrom torch.utils.data importDataset, DataLoaderfrom torch.optim.lr_scheduler importExponentialLR, LambdaLRfrom torchvision.models importResNet50_Weights#from tqdm import tqdm
from classify_cfg import *
from tools importget_dataset, update_lrdeftrain(model, dateset, epochs, batch_size, device, optimizer, scheduler, criterion, save_path):
train_loader
= DataLoader(dateset.get('train'), batch_size=batch_size, shuffle=True)
val_loader
= DataLoader(dateset.get('val'), batch_size=batch_size, shuffle=True)#保存为tensorboard文件 write =SummaryWriter(save_path)#训练过程写入txt f = open(os.path.join(save_path, 'log.txt'), 'w', encoding='utf-8')

best_acc
=0for epoch inrange(epochs):
train_correct
= 0.0model.train()
sum_loss
= 0.0accuracy= -1total_num=len(train_loader.dataset)#print(total_num, len(train_loader)) #loop = tqdm(enumerate(train_loader), total=len(train_loader)) batch_count =0for batch_idx, (data, target) inenumerate(train_loader):
start_time
=time.time()
data, target
=Variable(data).to(device), Variable(target).to(device)
output
=model(data)
loss
=criterion(output, target)
optimizer.zero_grad()
loss.backward()
optimizer.step()

print_loss
=loss.data.item()
sum_loss
+=print_loss
train_predict
= torch.max(output.data, 1)[1]iftorch.cuda.is_available():
train_correct
+= (train_predict.cuda() ==target.cuda()).sum()else:
train_correct
+= (train_predict ==target).sum()
accuracy
= (train_correct / total_num) * 100 #loop.set_description(f'Epoch [{epoch+1}/{epochs}]') #loop.set_postfix(loss=loss.item(), acc='{:.3f}'.format(accuracy)) batch_count +=len(data)
end_time
=time.time()
s
= f'Epoch:[{epoch+1}/{epochs}] Batch:[{batch_count}/{total_num}] train_acc: {"{:.2f}".format(accuracy)}'\
f
'train_loss: {"{:.3f}".format(loss.item())} time: {int((end_time-start_time)*1000)} ms' #print(f'Epoch:[{epoch+1}/{epochs}]', f'Batch:[{batch_count}/{total_num}]', #'train_acc:', '{:.2f}'.format(accuracy), 'train_loss:', '{:.3f}'.format(loss.item()), #'time:', f'{int((end_time-start_time)*1000)} ms') print(s)
f.write(s
+'\n')

write.add_scalar(
'train_acc', accuracy, epoch)
write.add_scalar(
'train_loss', loss.item(), epoch)#print(optimizer.param_groups[0]['lr']) scheduler.step()if best_acc <accuracy:
best_acc
=accuracy
torch.save(model, os.path.join(save_path,
'best.pt'))if epoch+1 ==epochs:
torch.save(model, os.path.join(save_path,
'last.pt'))#预测验证集 #if (epoch+1) % 5 == 0 or epoch+1 == epochs: model.eval()
test_loss
= 0.0correct= 0.0total_num=len(val_loader.dataset)#print(total_num, len(val_loader)) with torch.no_grad():for data, target inval_loader:
data, target
=Variable(data).to(device), Variable(target).to(device)
output
=model(data)
loss
=criterion(output, target)
_, pred
= torch.max(output.data, 1)iftorch.cuda.is_available():
correct
+= torch.sum(pred.cuda() ==target.cuda())else:
correct
+= torch.sum(pred ==target)
print_loss
=loss.data.item()
test_loss
+=print_loss
acc
= correct / total_num * 100avg_loss= test_loss /len(val_loader)
s
= f"val acc: {'{:.2f}'.format(acc)} val loss: {'{:.3f}'.format(avg_loss)}" #print('val acc: ', '{:.2f}'.format(acc), 'val loss: ', '{:.3f}'.format(avg_loss)) print(s)
f.write(s
+'\n')
write.add_scalar(
'val_acc', acc, epoch)
write.add_scalar(
'val_loss', avg_loss, epoch)#loop.set_postfix(val_loss='{:.3f}'.format(avg_loss), val_acc='{:.3f}'.format(acc)) f.close()if __name__ == '__main__':
device
=DEVICE
epochs
=EPOCHS
batch_size
=BATCH_SIZE
input_size
=INPUT_SIZE
lr
=LR#---------------------------训练------------------------------------- #图片的路径 base_dir = r'C:\Users\Administrator.DESKTOP-161KJQD\Desktop\dog_cat' #保存的路径 save_path = r'C:\Users\Administrator.DESKTOP-161KJQD\Desktop\dog_cat_save'dateset, classes, classes_num= get_dataset(base_dir, input_size=input_size)#model = torchvision.models.resnet50(pretrained=True) model = torchvision.models.resnet50(weights=ResNet50_Weights.IMAGENET1K_V1)
num_ftrs
=model.fc.in_features
model.fc
=nn.Linear(num_ftrs, classes_num)
model.to(DEVICE)
## 损失函数,交叉熵损失函数 criteon =nn.CrossEntropyLoss()#选择优化器 optimizer = optim.SGD(model.parameters(), lr=lr)#学习率更新 #scheduler = ExponentialLR(optimizer, gamma=0.9) scheduler = LambdaLR(optimizer, lr_lambda=lambdaepoch: update_lr(epoch, epochs))#开始训练 train(model, dateset, epochs, batch_size, device, optimizer, scheduler, criteon, save_path)#将label保存起来 with open(os.path.join(save_path, 'labels.txt'), 'w', encoding='utf-8') as f:
f.write(f
'{classes_num} {classes}')

训练结束以后,在保存路径下会得到下面的文件

最好的模型,最后一次的模型,标签的列表,训练的记录和tensorboard记录

在该路径下执行 tensorboard --logdir=.

然后在浏览器打开给出的地址,即可看到数据训练过程的绘图

4.对图片进行预测

考虑对于用户来说,用户是在网页或者手机上上传一张图片进行预测,所以这边是采用二进制数据。

mypredict.py

importcv2importnumpy as npimporttorchfrom classify_cfg import *



defimg_process(img_betys, img_size, device):

img_arry
= np.asarray(bytearray(img_betys), dtype='uint8')#im0 = cv2.imread(img_betys) im0 =cv2.imdecode(img_arry, cv2.IMREAD_COLOR)
image
=cv2.resize(im0, (img_size, img_size))
image
= np.float32(image) / 255.0image[:, :, ]-=np.float32(mean)
image[:, :, ]
/=np.float32(std)
image
= image.transpose((2, 0, 1))
im
=torch.from_numpy(image).unsqueeze(0)
im
=im.to(device)returnimdefpredict(model_path, img, device):
model
=torch.load(model_path)
model.to(device)
model.eval()
predicts
=model(img)#print(predicts) _, preds = torch.max(predicts, 1)
pred
=torch.squeeze(preds)#print(pred) returnpredif __name__ == '__main__':
mean
=MEAN
std
=STD
device
=DEVICE
classes
= ['', '']## 预测 model_path = r'C:\Users\Administrator.DESKTOP-161KJQD\Desktop\dog_cat_save\best.pt'img_path= r'C:\Users\Administrator.DESKTOP-161KJQD\Desktop\save_dir\狗\000000.jpg'with open(img_path,'rb') as f:
img_betys
=f.read()
img
=img_process(img_betys, 160, device)#print(img.shape) #print(img) pred =predict(model_path, img, device)print(classes[int(pred)])

还有我的配置文件classify_cfg.py

importtorch

BATCH_SIZE
= 2 #每批处理的数据 DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu') #放在cuda或者cpu上训练 EPOCHS = 30 #训练数据集的轮次 LR = 1e-3 #学习率 INPUT_SIZE = 160 #输入图片大小 MEAN = [0.485, 0.456, 0.406] #均值 STD = [0.229, 0.224, 0.225] #方差

作者:京东零售 周明亮

写在前面

这里我们初步提到了一些基础概念和应用:

  • 分析器
  • 抽象语法树 AST
  • AST 在 JS 中的用途
  • AST 的应用实践

有了初步的认识,还有常规的代码改造应用实践,现在我们来详细说说使用 AST, 如何进行代码改造?

Babel AST 四件套的使用方法

其实在解析 AST 这个工具上,有很多可以使用,上文我们已经提到过了。对于 JS 的 AST 大家已经形成了统一的规范命名,唯一不同的可能是,不同工具提供的详细程度不一样,有的可能会额外提供额外方法或者属性。

所以,在选择工具上,大家按照各自喜欢选择即可,这里我们选择了babel这个老朋友。

初识 Babel

我相信在这个前端框架频出的时代,应该都知道babel的存在。 如果你还没听说过babel,那么我们通过它的相关文档,继续深入学习一下。

因为,它在任何框架里面,我们都能看到它的影子。

  • Babel JS 官网
  • Babel JS Github

作为使用最广泛的 JS 编译器,他可以用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。

而它能够做到向下兼容或者代码转换,就是基于代码解析和改造。接下来,我们来说说:如何使用@babel/core里面的核心四件套:@babel/parser、@babel/traverse、@babel/types及@babel/generator。

1. @babel/parser

@babel/parser 核心代码解析器,通过它进行词法分析及语法分析过程,最终转换为我们提到的 AST 形式。

假设我们需要读取React中index.tsx文件中代码内容,我们可以使用如下代码:

const { parse } = require("@babel/parser")

// 读取文件内容
const fileBuffer = fs.readFileSync('./code/app/index.tsx', 'utf8');
// 转换字节 Buffer
const fileCode = fileBuffer.toString();
// 解析内容转换为 AST 对象
const codeAST = parse(fileCode, {
  // parse in strict mode and allow module declarations
  sourceType: "module",
  plugins: [
    // enable jsx and typescript syntax
    "jsx",
    "typescript",
  ],
});

当然我不仅仅只读取React代码,我们甚至可以读取Vue语法。它也有对应的语法分析器,比如:@vue/compiler-dom。

此外,通过不同的参数传入 options,我们可以解析各种各样的代码。如果,我们只是读取普通的.js文件,我们可以不使用任何插件属性即可。

const codeAST = parse(fileCode, {
  // parse in strict mode and allow module declarations
  sourceType: "module"
});

通过上述的代码转换,我们就可以得到一个标准的 AST 对象。在上一篇文章中,已做详细分析,在这里不在展开。比如:

// 原代码
const me = "我"
function write() {
  console.log("文章")
}

// 转换后的 AST 对象
const codeAST = {
  "type": "File",
  "errors": [],
  "program": {
    "type": "Program",
    "sourceType": "module",
    "interpreter": null,
    "body": [
      {
        "type": "VariableDeclaration",
        "declarations": [
          {
            "type": "VariableDeclarator",
            "id": {
              "type": "Identifier",
              "name": "me"
            },
            "init": {
              "type": "StringLiteral",
              "extra": {
                "rawValue": "我",
                "raw": "\"我\""
              },
              "value": "我"
            }
          }
        ],
        "kind": "const"
      },
      {
        "type": "FunctionDeclaration",
        "id": {
          "type": "Identifier",
          "name": "write"
        },
        "generator": false,
        "async": false,
        "params": [],
        "body": {
          "type": "BlockStatement",
          "body": [
            {
              "type": "ExpressionStatement",
              "expression": {
                "type": "CallExpression",
                "callee": {
                  "type": "MemberExpression",
                  "object": {
                    "type": "Identifier",
                    "computed": false,
                    "property": {
                      "type": "Identifier",
                      "name": "log"
                    }
                  },
                  "arguments": [
                    {
                      "type": "StringLiteral",
                      "extra": {
                        "rawValue": "文章",
                        "raw": "\"文章\""
                      },
                      "value": "文章"
                    }
                  ]
                }
              }
            }
          ]
        }
      }
    ]
  }
}

2. @babel/traverse

当我们拿到一个标准的 AST 对象后,我们要操作它,那肯定是需要进行树结构遍历。这时候,我们就会用到 @babel/traverse 。

比如我们得到 AST 后,我们可以进行遍历操作:

const { default: traverse } = require('@babel/traverse');

// 进入结点
const onEnter = pt => {
   // 进入当前结点操作
   console.log(pt)
}
// 退出结点
const onExit = pe => {
  // 退出当前结点操作
}
traverse(codeAST, { enter: onEnter, exit: onExit })

那么我们访问的第一个结点,打印出pt的值,是怎样的呢?

// 已省略部分无效值
<ref *1> NodePath {
  contexts: [
    TraversalContext {
      queue: [Array],
      priorityQueue: [],
      ...
    }
  ],
  state: undefined,
  opts: {
    enter: [ [Function: onStartVist] ],
    exit: [ [Function: onEndVist] ],
    _exploded: true,
    _verified: true
  },
  _traverseFlags: 0,
  skipKeys: null,
  parentPath: null,
  container: Node {
    type: 'File',
    errors: [],
    program: Node {
      type: 'Program',
      sourceType: 'module',
      interpreter: null,
      body: [Array],
      directives: []
    },
    comments: []
  },
  listKey: undefined,
  key: 'program',
  node: Node {
    type: 'Program',
    sourceType: 'module',
    interpreter: null,
    body: [ [Node], [Node] ],
    directives: []
  },
  type: 'Program',
  parent: Node {
    type: 'File',
    errors: [],
    program: Node {
      type: 'Program',
      sourceType: 'module',
      interpreter: null,
      body: [Array],
      directives: []
    },
    comments: []
  },
  hub: undefined,
  data: null,
  context: TraversalContext {
    queue: [ [Circular *1] ],
    priorityQueue: [],
    ...
  },
  scope: Scope {
    uid: 0,
    path: [Circular *1],
    block: Node {
      type: 'Program',
      sourceType: 'module',
      interpreter: null,
      body: [Array],
      directives: []
    },
    ...
  }
}

是不是发现,这一个遍历怎么这么多东西?太长了,那么我们进行省略,只看关键部分:

// 第1次
<ref *1> NodePath {
  listKey: undefined,
  key: 'program',
  node: Node {
    type: 'Program',
    sourceType: 'module',
    interpreter: null,
    body: [ [Node], [Node] ],
    directives: []
  },
  type: 'Program',
}

我们可以看出是直接进入到了程序program结点。 对应的 AST 结点信息:

  program: {
    type: 'Program',
    sourceType: 'module',
    interpreter: null,
    body: [
      [Node]
      [Node]
    ],
  },

接下来,我们继续打印输出的结点信息,我们可以看出它访问的是program.body结点。

// 第2次
<ref *2> NodePath {
  listKey: 'body',
  key: 0,
  node: Node {
    type: 'VariableDeclaration',
    declarations: [ [Node] ],
    kind: 'const'
  },
  type: 'VariableDeclaration',
}

// 第3次
<ref *1> NodePath {
  listKey: 'declarations',
  key: 0,
  node: Node {
    type: 'VariableDeclarator',
    id: Node {
      type: 'Identifier',
      name: 'me'
    },
    init: Node {
      type: 'StringLiteral',
      extra: [Object],
      value: '我'
    }
  },
  type: 'VariableDeclarator',
}

// 第4次
<ref *1> NodePath {
  listKey: undefined,
  key: 'id',
  node: Node {
    type: 'Identifier',
    name: 'me'
  },
  type: 'Identifier',
}

// 第5次
<ref *1> NodePath {
  listKey: undefined,
  key: 'init',
  node: Node {
    type: 'StringLiteral',
    extra: { rawValue: '我', raw: "'我'" },
    value: '我'
  },
  type: 'StringLiteral',
}

  • node当前结点
  • parentPath父结点路径
  • scope作用域
  • parent父结点
  • type当前结点类型

现在我们可以看出这个访问的规律了,他会一直找当前结点node属性,然后进行层层访问其内容,直到将 AST 的所有结点遍历完成。

这里一定要区分NodePath和Node两种类型,比如上面:pt是属于NodePath类型,pt.node才是Node类型。

其次,我们看到提供的方法除了进入 [enter]还有退出 [exit]方法,这也就意味着,每次遍历一次结点信息,也会退出当前结点。这样,我们就有两次机会获得所有的结点信息。

当我们遍历结束,如果找不到对应的结点信息,我们还可以进行额外的操作,进行代码结点补充操作。结点完整访问流程如下:

  • 进入>Program
    • 进入>node.body[0]
      • 进入>node.declarations[0]
        • 进入>node.id
        • 退出<node.id
        • 进入>node.init
        • 退出<node.init
      • 退出<node.declarations[0]
    • 退出<node.body[0]
    • 进入>node.body[1]
      • ...
      • ...
    • 退出<node.body[1]
  • 退出<Program

3. @babel/types

有了前面的铺垫,我们通过解析,获得了相关的 AST 对象。通过不断遍历,我们拿到了相关的结点,这时候我们就可以开始改造了。@babel/types 就提供了一系列的判断方法,以及将普通对象转换为 AST 结点的方法。

比如,我们想把代码转换为:

// 改造前代码
const me = "我"
function write() {
  console.log("文章")
}

// 改造后的代码
let you = "你"
function write() {
  console.log("文章")
}

首先,我们要分析下,这个代码改了哪些内容?

  1. 变量声明从const改为let
  2. 变量名从me改为you
  3. 变量值从"我"改为"你"

那么我们有两种替换方式:

  • 方案一:整体替换,相当于把program.body[0]整个结点进行替换为新的结点。
  • 方案二:局部替换,相当于逐个结点替换结点内容,即:program.body.kind,program.body[0].declarations[0].id,program.body[0].declarations[0].init。

借助@babel/types我们可以这么操作,一起看看区别:

const bbt = require('@babel/types');
const { default: traverse } = require('@babel/traverse');

// 进入结点
const onEnter = p => {
  // 方案一,全结点替换
  if (bbt.isVariableDeclaration(p.node) && p.listKey == 'body') {
    // 直接替换为新的结点
    p.replaceWith(
      bbt.variableDeclaration('let', [
        bbt.variableDeclarator(bbt.identifier('you'),           
        bbt.stringLiteral('你')),
      ]),
    );
  }
  // 方案二,单结点逐一替换
  if (bbt.isVariableDeclaration(p.node) && p.listKey == 'body') {
    // 替换声明变量方式
    p.node.kind = 'let';
  }
  if (bbt.isIdentifier(p.node) && p.node.name == 'me') {
    // 替换变量名
    p.node.name = 'you';
  }
  if (bbt.isStringLiteral(p.node) && p.node.value == '我') {
    // 替换字符串内容
    p.node.value = '你';
  }  
};
traverse(codeAST, { enter: onEnter });

我们发现,不仅可以进行整体结点替换,也可以替换属性的值,都能达到预期效果。

当然 我们不仅仅可以全部遍历,我们也可以只遍历某些属性,比如VariableDeclaration,我们就可以这样进行定义:

traverse(codeAST, { 
  VariableDeclaration: function(p) {
    // 只操作类型为 VariableDeclaration 的结点
    p.node.kind = 'let';
  }
});

@babel/types提供大量的方法供使用,可以通过官网查看。对于@babel/traverse返回的可用方法,可以查看 ts 定义:
babel__traverse/index.d.ts 文件。

常用的方法:p.stop()可以提前终止内容遍历, 还有其他的增删改查方法,可以自己慢慢摸索使用!它就是一个树结构,我们可以操作它的兄弟结点,父节点,子结点。

4. @babel/generator

完成改造以后,我们需要把 AST 再转换回去,这时候我们就需要用到 @babel/generator 工具。只拆不组装,那是二哈【狗头】。能装能组,才是一个完整工程师该干的事情。

废话不多说,上代码:

const fs = require('fs-extra');
const { default: generate } = require('@babel/generator');

// 生成代码实例
const codeIns = generate(codeAST, { retainLines: true, jsescOption: { minimal: true } });

// 写入文件内容
fs.writeFileSync('./code/app/index.js', codeIns.code);

配置项比较多,大家可以参考具体的说明,按照实际需求进行配置。

这里特别提一下:jsescOption: { minimal: true }这个属性,主要是用来保留中文内容,防止被转为unicode形式。

Babel AST 实践

嘿嘿~ 都到这里了,大家应该已经能够上手操作了吧!

什么?还不会,那再把 1 ~ 4 的步骤再看一遍。慢慢尝试,慢慢修改,当你发现其中的乐趣时,这个 AST 的改造也就简单了,并不是什么难事。

留个课后练习:

// 改造前代码
const me = "我"
function write() {
  console.log("文章")
}

// 改造后的代码
const you = "你"
function write() {
  console.log("文章")
}
console.log(you, write())

大家可以去尝试下,怎么操作简单的 AST 实现代码改造!写文章不易,大家记得一键三连哈~

AST 应用是非常广泛,再来回忆下,这个 AST 可以干嘛?

  1. 代码转换领域,如:ES6 转 ES5, typescript 转 js,Taro 转多端编译,CSS预处理器等等。
  2. 模版编译领域,如:React JSX 语法,Vue 模版语法 等等。
  3. 代码预处理领域,如:代码语法检查(ESLint),代码格式化(Prettier),代码混淆/压缩(uglifyjs) 等等
  4. 低代码搭建平台,拖拽组件,直接通过 AST 改造生成后的代码进行运行。

下一期预告

《带你揭开神秘的Javascript AST面纱之手写一个简单的 Javascript 编译器》

作者:京东零售 姜海

灵动岛是苹果在iPhone 14 Pro和iPhone 14 Pro Max上首次提出的全新UI交互形式,创新性的让虚拟软件和硬件的交互变得更为流畅。当有来电、短信等通知时,灵动岛会变化形态,以便让用户能够更直观地接收到这些信息。

而在用户使用一些应用App,比如音乐,并将其切换到后台时,灵动岛也能以另一种形态来显示这些软件,还可以通过轻点,重按等来实现的操作,比如切换歌曲。

苹果在iOS16.1系统对第三方开放了灵动岛的API,并允许开发者基于灵动岛开发相应软件,越来越多的APP开始基于灵动岛的交互进行设计和开发,本文将简单介绍灵动岛开发的流程和将其与业务场景相结合的思考。

接入灵动岛

如果项目之前开发过widget小组件,已经添加过Widget Extension,并有WidgetBundle文件,那么可以直接基于其进行扩展开发。但要注意的是,灵动岛开发用到的是Live Activity,主要包括锁屏通知,顶部通知等样式:

而并不是widget开发用到的Time Line形式,两者在UI形态上基本毫无关系,只是需要在WidgetBundle中实例化。如果之前没有开发过widget,可以参见另一篇文章:《
Widget开发以及动态配置

首先给工程添加Widget Extension:

勾选Live Activity:

建立Extension以后,系统会自动生成三个文件,除了widget开发用到的TimeLine相关内容的文件和WidgetBundle文件外,还会生成一个专门用来开发灵动岛Live Activity的文件:

文件中已经自动生成了部分代码大纲,可以直接查看效果并基于其上进行开发:

struct DJDynamicIslandAdvanceLiveActivity: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: DJDynamicIslandAdvanceAttributes.self) { context in
            // Lock screen/banner UI goes here
            VStack {
                Text("Hello")
            }
            .activityBackgroundTint(Color.cyan)
            .activitySystemActionForegroundColor(Color.red)

        } dynamicIsland: { context in
            DynamicIsland {
                // 点击灵动岛后展开的样式
                // various regions, like leading/trailing/center/bottom
                DynamicIslandExpandedRegion(.leading) {
                    Text("Leading")
                }
                DynamicIslandExpandedRegion(.trailing) {
                    Text("Trailing")
                }
                DynamicIslandExpandedRegion(.center) {
                    Text("Center")
                }
                DynamicIslandExpandedRegion(.bottom) {
                    Text("Bottom")
                    // more content
                }
            } compactLeading: {
                // compact模式(长条样式)左侧内容,一般放icon
                Text("compactLeading")
            } compactTrailing: {
                // compact模式(长条样式)右侧内容,一般放描述文案
                Text("compactTrailing")
            } minimal: {
                // minimal模式(其他APP挤占后的圆圈样式)
                Text("minimal")
            }
            .widgetURL(URL(string: "http://www.apple.com"))
            .keylineTint(Color.red)
        }
    }
}

同时需要在info.plist中添加对Live Activity的支持,在TARGETS - Info - Custom iOS Target Properties中添加NSSupportsLiveActivities并设置为YES:

不同展示样式

灵动岛主要包括三种展示样式:

灵动岛被重按后,展开的完整模式(expanded)

此模式分为Leading、Trailing、Center和Bottom四个部分,在系统自动为我们生成的代码中,ActivityConfiguration的dynamicIsland中可以分别找到对应控制的代码段:

dynamicIsland: { context in
            DynamicIsland {
                // 点击灵动岛后展开的样式
                // various regions, like leading/trailing/center/bottom
                DynamicIslandExpandedRegion(.leading) {
                    Text("Leading")
                }
                DynamicIslandExpandedRegion(.trailing) {
                    Text("Trailing")
                }
                DynamicIslandExpandedRegion(.center) {
                    Text("Center")
                }
                DynamicIslandExpandedRegion(.bottom) {
                    Text("Bottom")
                    // more content
                }
            }

APP切后台后的长条形展示样式(compact)

此形式分为两个部分:左边的Leading,一般用于放图片icon等;右边的Trailing,一般用与放置文案描述

在系统自动为我们生成的代码中,ActivityConfiguration的dynamicIsland部分分别对应compactLeading和compactTrailing,可以在其中编写我们想要的UI展示:

compactLeading: {
                // compact模式(长条样式)左侧内容,一般放icon
                Text("compactLeading")
            } compactTrailing: {
                // compact模式(长条样式)右侧内容,一般放描述文案
                Text("compactTrailing")
            } 

其他APP切后台时,变化为灵动岛将本app挤占后展示的圆点模式(minimal)

此形势一般用于放置图标icon或动态图等

在系ActivityConfiguration的dynamicIsland部分对应minimal,可以在其中编写我们想要的UI展示

minimal: {
                // minimal模式(其他APP挤占后的圆圈样式)
                Text("minimal")
            }

动态数据更新

上文说灵动岛视图布局是在ActivityConfiguration中编写的,而其上的数据更新依靠的是ActivityAttributes对象。需要注意的是,ActivityAttributes不需要跟ActivityConfiguration写在一起,就像view不需要跟model写在一起一样。

苹果官方建议ActivityAttributes分为两部分:固定不变的属性(比如总数,订单号等等)和会动态变化的属性(比如配送员名称,配送时间等等)。官方给出的demo是披萨配送的app,我们可以参考它的Attributes声明规则:

struct PizzaDeliveryAttributes: ActivityAttributes {
    public typealias PizzaDeliveryStatus = ContentState

    public struct ContentState: Codable, Hashable {
        var driverName: String
        var deliveryTimer: ClosedRange<Date>
    }

    var numberOfPizzas: Int
    var totalAmount: String
    var orderNumber: String
}

其中ContentState是会动态改变的部分。在完成布局编写后,实际的工程应用当中可以调用Activity对象的各种方法对灵动岛进行操作,包括开启,更新和关闭:

调用Activity
.request成功开启灵动岛后,将APP切到后台,就可以看到效果了,调用request以及Activity 里每一个activity的update方法,都可以触发ActivityConfiguration的闭包调用,从它回掉的context可以获取到Attributes的数据内容,比如context.state.deliveryManName和context.attributes.totalAmount:

与到家业务结合的思考

灵动岛提供了一种全新的“通知交互”形式,不再是单调的一个横幅或者提示框,而是一个实时显示,动态更新的UI,就像他的名字“Live Activity”一样,是一场“直播”。

对灵动岛的适配被形象地称为“登岛”,针对到家的业务场景,我们也做了一系列思考,最适用的业务场景也是下单后订单状态的实时更新“直播”,并且编写了Demo展示:

灵动岛挂件

灵动岛挂件是我们提出的另一种非常有意思的灵动岛应用。

首先,灵动岛的各项UI数据如下:

经过精确布局,可以在灵动岛上动态的展示一个会动的挂件,就像在灵动岛上养了一只可爱的宠物:

我们会持续跟进最新的灵动岛技术动态,并且探索其他实用灵动岛的业务场景,利用这项技术带来更多的流量和利益点

1. 效果:

image
image
image

2. 实现:

创建外层菜单
AsideMenu.vue
组件和子菜单项
AsideSubMenu.vue
组件,在
AsideSubMenu
中进行递归操作。

AsideMenu.vue文件内容如下:

<template>
  <aside class="wrap">
    <el-menu
      :default-active="activeMenu"
      router
      :class="'menu-left'"
      :default-openeds="openedsArr"
      text-color="#fff"
    >
      <AsideSubMenu :menuData="menuData"></AsideSubMenu>
    </el-menu>
  </aside>
</template>

<script>
import AsideSubMenu from "./AsideSubMenu.vue";
export default {
  name: "AsideMenu",
  components: {
    AsideSubMenu,
  },
  props: {
    menuData: {
      type: Array,
    },
  },
  computed: {
    activeMenu() {
      const route = this.$route;
      const { meta, path } = route;
      // 此处添加判断的原因见说明
      if (meta.matchPath) {
        return meta.matchPath;
      } else {
        return path;
      }
    },
    // 设置默认展开菜单项
    openedsArr() {
      // const arr = this.menuData.map((item) => {
      //   return item.title;
      // });
      // return arr;
      return [];
    },
  },
};
</script>



判断高亮状态的activeMenu方法中的判断matchPath属性可以让多个路由不同的页面匹配同一个菜单高亮状态,因为菜单高亮状态是根据路由地址匹配的。如果两个不同的路由页面想公用同一个菜单高亮状态(如详情页面和列表页)就可以使用该方法实现。在router文件里设置meta对象,添加matchPath属性设置为想要共用的高亮状态的页面的路由地址。(有点绕