欢送关注我的公众号睿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插件的实现细节。