共计 4146 个字符,预计需要花费 11 分钟才能阅读完成。
欢送关注我的公众号 睿 Talk
,获取我最新的文章:
一、前言
在上一篇文章中,我简略的介绍了基于区块开发的总体思路和配套工具。接下来我会用 2 篇文章来别离介绍 命令行工具
和VSCode 插件
的具体实现细节。
二、根底性能
命令行工具的根底性能蕴含以下几点:
- 列出可选的区块列表
- 通过链接预览成果
- 将选中的区块装置到我的项目中
列出可选的区块列表并预览
因为区块信息是会动态变化的,所以区块列表必须通过接口获取。数据源方面所有从简,只是在 ngnix
前面挂了一个 json
文件,区块信息有什么变动就间接批改文件。
获取到区块信息后,就要思考如何在命令行展示了。因为操作过程中波及到一系列交互,一番调研后决定应用 Inquirer.js。单单有列表还不够,用户还须要去预览区块的成果,这里用到了 terminal-link 这个工具。
为了使展示成果更活泼,应用 chalk 来增加字体色彩,
最终的展示成果如下:
要害代码如下:
const blockArray = blocks.map((block) => {const { name, value, preview} = block; | |
const link = terminalLink("预览", preview); | |
return {name: `???? ${chalk.cyan(name)} ${link}`, | |
value: value, | |
}; | |
}); | |
const choice = await inquirer.prompt([ | |
{ | |
type: "list", | |
name: "block", | |
message: `⛰ 请抉择区块(共 ${blockArray.length} 个 )`, | |
choices: blockArray, | |
pageSize: 10, | |
}, | |
{ | |
type: "confirm", | |
name: "cwd", | |
message: ` 在当前目录装置区块吗?[${cwd}]`, | |
default: "y", | |
}, | |
]); | |
// 选中的区块和装置门路 | |
const {block, cwd: cur} = choice; |
将选中的区块装置到我的项目中
取得用户抉择的区块和区块的装置门路后,下一步就是获取区块的源码,并增加到我的项目中。所有区块的源码都集中寄存在 git
仓库,只须要把仓库克隆下来,而后把文件夹拷贝到指标门路就能够了。过程中应用了 ora 来显示 loading 状态,rimraf 来删除目录。
// child_process 模块 | |
const exec = cp.exec; | |
const spinner = ora(); | |
// 目标目录 | |
const dir = path.resolve(".") | |
// 寄存区块源码仓库的长期目录 | |
const tmpdir = path.resolve(dir, ".block"); | |
spinner.start(`???? 获取区块 `); | |
exec(`git clone --depth=1 http://git.xxx.com/xxx/block.git ${tmpdir}` | |
); | |
spinner.succeed(); | |
spinner.start(`???? 拷贝区块 `); | |
// 这里都 block 变量是用户抉择都区块名称 | |
const sourceDir = path.resolve(tmpdir, `src/block/${block}`); | |
exec(`cp -r ${sourceDir}/. ${dir}/${block}`); | |
spinner.succeed(); | |
spinner.start(`????️ 清理源区块文件 `); | |
rimraf.sync(tmpdir); | |
spinner.succeed(); | |
spinner.start(`???? 区块装置实现 `); | |
spinner.succeed(); |
到此为止区块就能胜利的装置到我的项目中去了。
三、进阶性能
区块大体上分为 3 类:
- 单页面
- 页面 + 弹窗
- 多页面
其中多页面区块会波及到路由定义,为了更好的用户体验,会依据团队内约定的代码组织形式批改现有的文件。比方在区块装置前是这个样子:
区块装置后,不须要改任何代码,会主动增加对应的菜单项和路由:
再来看看代码的批改记录:
要实现这个性能,就要用到 AST
了。AST
的应用办法能够看我之前写的 AST 实战。
对应的,在装置区块的过程中,要加上解决批改现有文件的代码。
const containRoute = fs.existsSync(`${sourceDir}/routes`); | |
// 如果存在 routes 目录,则为多页面区块,须要解决路由逻辑 | |
if (containRoute) {const re = new RegExp("^(.*/src).*", "gi"); | |
const result = re.exec(dir); | |
let destDir; | |
if (!result || !result[1]) { | |
// 操作目录不蕴含 src 目录 | |
const inProjectRoot = fs.existsSync(`${cwd}/src`); | |
if (!inProjectRoot) { | |
throw new Error("找不到 src 目录。区块蕴含路由配置,请在我的项目内运行此命令。"); | |
} | |
destDir = `${cwd}/src`; | |
} else {destDir = result[1]; | |
} | |
// 拷贝新的文件 | |
await runCmd(`cp -r ${sourceDir}/modules ${destDir}/`); | |
await runCmd(`cp -r ${sourceDir}/routes ${destDir}/`); | |
const files = fs.readdirSync(`${sourceDir}/routes/route`); | |
if (!files || files.length === 0) {throw new Error("区块不蕴含路由文件"); | |
} | |
// 获取路由变量名【AST】const routeName = getNamedExport(`${sourceDir}/routes/route/${files[0]}`); | |
const i = files[0].indexOf(".tsx"); | |
const routeFileName = files[0].substring(0, i); | |
// 插入 import 和 export(routes/index)【AST】insertImportAndExport(`${destDir}/routes/index.tsx`, | |
routeName, | |
routeFileName | |
); | |
// 获取 route-path 一级路由名【AST】const moduleName = getModuleName(`${destDir}/constants/route-path.ts`); | |
if (!moduleName) { | |
console.error( | |
"❗️" + | |
chalk.red(` 没有找到文件 ${destDir}/constants/route-path.ts, 须要手动将新增路由增加到 app.ts` | |
) | |
); | |
} else { | |
// 更改 app.ts【AST】updateEntry(`${destDir}/app.ts`, moduleName, routeFileName, routeName); | |
const selectedItem = blocks.find((item) => item.value === block); | |
// 插入新菜单【AST】updateMenu(`${destDir}/configs/menu.js`, | |
moduleName, | |
routeFileName, | |
selectedItem.name | |
); | |
} | |
} |
正文中有 【AST】
标识的就波及到形象语法树的使用,上面是其中一个例子:
// 插入 import 和 export(routes/index)function insertImportAndExport(indexRoutePath, routeName, routeFileName) { | |
let importInserted = false; | |
const result = fs.existsSync(indexRoutePath); | |
if (!result) {throw new Error(` 没有找到文件:${indexRoutePath}`); | |
} | |
const content = fs.readFileSync(indexRoutePath, "utf8"); | |
const ast = parser.parse(content, { | |
sourceType: "module", | |
plugins: ["jsx", "typescript"], | |
}); | |
traverse(ast, {ImportDeclaration(astPath) { | |
// 插入 import(routes/index)if (!importInserted) {const id = t.identifier(routeName); | |
const sp = t.importSpecifier(id, id); | |
const literal = t.stringLiteral(`./route/${routeFileName}`); | |
const declare = t.importDeclaration([sp], literal); | |
astPath.insertBefore(declare); | |
importInserted = true; | |
} | |
}, | |
ExportDefaultDeclaration(astPath) { | |
// 导出路由(routes/index)const properties = astPath.node.declaration.properties; | |
const id = t.identifier(routeName); | |
const property = t.objectProperty(id, id, false, true); | |
properties.push(property); | |
}, | |
}); | |
const output = generate(ast, { jsescOption: { minimal: true} }); | |
fs.writeFileSync(indexRoutePath, output.code); | |
} |
应用 AST
批改文件并不难,就是有点繁琐,对文件的约定内容也有要求,在此就不开展了。
四、总结
本文介绍了服务于区块开发的命令行工具的实现细节,分享实现思路和当中用到的一些工具库。在下一篇文章中,我将会介绍配套 VSCode 插件
的实现细节。