开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情
背景介绍
先简单介绍下项目的背景,由于某些不可抗的神秘力量(ling dao),在工作中遇到了代码迁移的工作,就是把A项目的XXXX代码迁移到B项目中去。
需求探索
作为CV工程师,这种事情处理起来当然是洒洒水啦,直接就是CV大法好!但是有没有更优雅的办法呢?首先我们对页面进行分析。
我们可以看到,我们要迁移的HTML文件引入了一些css样式、js文件、还有html中img标签引用的图片,如果要手动CV,几十个html文件工作量简直逆!天!而且分析可以看出来,需要迁移的文件种类十分明确,只是具体的文件不同,所以是一些比较机械化、重复化的工作,完全可以通过脚本去实现。
demo实现
说干就干,我们先创建一个node项目,然后初始化一下配置
mkdir html-migration
cd html-migration
npm init -y
然后我们在package.json中增加如下配置:
"bin": {
"migration": "./index.js"
},
"type": "module"
bin属性会在install对应包之后添加可执行文件,对应的可执行文件就是./index.js,对应的命令就是migration,"type": "module"可以让我们使用esmodule 引用包文件。
然后创建我们的入口文件 index.js,并输入如下内容:
#!/usr/bin/env node
console.log('migration Run!')
然后在项目根目录运行指令npm install -g,然后运行migration查看是否成功,如果不成功的话,可以检查下npm-global是否在你的path变量中,运行结果如下:
代码实现
1.解析html文件
我们解析HTML文件使用的是node-html-parser包,安装好之后,开始写我们的解析代码。
function parseHtml (html) {
const document = nodeHtmlParser.parse(html)
// 迁移js文件
const jsFileList = []
document.querySelectorAll('script[src]').forEach(script => {
if (script.attrs.src) {
jsFileList.push({
rawPath: script.attrs.src,
type: "js",
absolutePath: resolve(script.attrs.src)
})
}
})
// 迁移css文件
const styleFileList = []
document.querySelectorAll('link[href]').forEach(link => {
if (link.attrs.href) {
styleFileList.push({
rawPath: link.attrs.href,
type: "css",
absolutePath: resolve(link.attrs.href)
})
}
})
// 迁移图片
const imageList = []
document.querySelectorAll('img').forEach(image => {
if (image.attrs.src) {
imageList.push({
rawPath: image.attrs.src,
type: "image",
absolutePath: resolve(image.attrs.src)
})
}
})
}
解析HTML文件过程,就是获取HTML的所有依赖的过程。我们之前分析确定了,需要迁移的文件类型有 js、css、image,我们通过querySelectorAll方法,可以查询到引入的js、css、image文件并保存起来,方便后续的迁移工作。
2.代码迁移
代码迁移工作的最重要的就是要保证迁移后的资源的地址要与原来的地址保持一致,从而才能保证资源的有效性。
const successOperationList = []
const errorOperationList = []
const migrationFileList = (fileList) => {
fileList.forEach((file) => {
try {
// 目标文件
const target = path.resolve(targetPath, file.rawPath)
// 目标文件目录
const targetDir = path.join(...target.split(path.sep).slice(0, -1))
// 目标文件是否存在
const exists = fs.existsSync(target)
console.log(file.type, '文件:', file.absolutePath, '》》》》》》》》》》', target, `(${ exists ? '存在': '新增'})`)
// 如果已经存在就不管了
if (exists) {
successOperationList.push({
file,
exists,
success: true
})
} else {
// 读取文件
const fileBuffer = fs.readFileSync(file.absolutePath);
const dirExists = fs.existsSync(targetDir)
if (!dirExists) {
fs.mkdirSync(targetDir, {
recursive: true
})
}
fs.writeFileSync(target, fileBuffer, {flag: 'wx+'})
successOperationList.push({
file,
exists,
success: true
})
}
} catch (error) {
console.log(error)
successOperationList.push({
file,
error,
success: false
})
}
})
}
我们维护了两个操作列表,用于记录成功的操作和失败的操作,用于回滚和重试(虽然我暂时没有做)。首先我们判断迁移文件对应迁移后的文件是否存在(因为有可能公用某些资源文件),如果存在则不需要任何操作,把操作记录储存到数组中即可。如果不存在则需要copy文件过去(其实可以把copy文件的方法抽象出来的,但是毕竟是个临时脚本,就没有抽取),先判断对应的目录是否存在,如果不存在需要先创建目录(递归创建),然后再创建文件即可。这样子迁移部分的代码就完成了。
3.用户操作
最后就是完善脚本的使用方法啦,这部分代码比较简单,用到的npm包是inquirer。
#!/usr/bin/env node
import path from "path";
import fs from "fs";
import nodeHtmlParser from 'node-html-parser';
import inquirer from "inquirer";
const currentPath = process.cwd()
function resolve(...args) {
return path.resolve(currentPath, ...args)
}
const successOperationList = []
const errorOperationList = []
const migrationFileList = (fileList) => {
/** 省略 */
}
function parseHtml (html) {
/** 省略 */
migrationFileList([...jsFileList, ...styleFileList, ...imageList])
}
function dealFile(file) {
parseHtml(fs.readFileSync(resolve(file)))
migrationFileList([{
rawPath: file,
absolutePath: resolve(file),
type: 'html'
}])
}
const targetPath = process.argv[2];
const sourceFile = process.argv[3];
if (sourceFile) {
dealFile(sourceFile)
} else {
const fileOrDirList = fs.readdirSync('.');
inquirer.prompt([
{
name: '选择要迁移的文件',
type: 'list',
choices: fileOrDirList.filter(file => file.endsWith('.html'))
}
]).then(res => {
dealFile(res['选择要迁移的文件'])
})
}
我们接收两个参数,第一个参数是迁移到的目录文件,第二个参数是需要迁移的文件(可选),如果用户不输入可选文件就提供当前的目录的html文件名称进行选择。我们来测试一下。
可以看到 非常成功!
总结
因为场景比较简单,所以量不多,大家在日常的工作中如果遇到重复的工作,也可以用脚本去解决。当然这个项目还有很多可以优化的地方,欢迎大家指导和讨论!