【若川视野 x 源码共读】第34期 | tdesign-vue 初始化组件

1,413 阅读2分钟

本文源码学习主要是了解 TDesign 中组件初始化命令,包括以下几个方面:

  • npm run init [component] :生成初始化组件代码及文件
  • npm run init [component] del :移除组件代码及文件 从这里我们可以了解到:
  • 怎么实现通过模板 + 数据 的方式生成我们想要的基础代码
  • 怎么实现通过命令删除对应基础代码及文件
  • lodash.template 使用

流程

npm run init table 命令逻辑执行流程如下图:

image.png npm run init table del 命令逻辑执行流程如下图:

image.png

组件模板

在了解其实现前,先看看其组件模板配置。

目录信息

script/init目录:

image.png

导出配置

外部通过调用getToBeCreatedFiles来获取对应的配置目录信息

image.png

实现

npm run init入口

const config = require('./config');
function init() {
  // 从参数中获取组件名称、del
  const [component, isDeleted] = process.argv.slice(2);
  if (!component) {
    console.error('[组件名]必填 - Please enter new component name');
    process.exit(1);
  }
  // 获取组件入口路径
  const indexPath = path.resolve(cwdPath, 'src/index.ts');
  // 获取组件配置信息
  const toBeCreatedFiles = config.getToBeCreatedFiles(component);
  if (isDeleted === 'del') {
    // 移除组件目录
    deleteComponent(toBeCreatedFiles, component);
    // 从组件入口移除该组件引入
    deleteComponentFromIndex(component, indexPath);
  } else {
    // 添加组价目录
    addComponent(toBeCreatedFiles, component);
    // 在组件入口加入该组件引入
    insertComponentToIndex(component, indexPath);
  }
}

了解了入口逻辑,我们再来看看其内部实现。

新加组件

新加组件主要逻辑: addComponent->outputFileWithTemplate->insertComponentToIndex

addComponent

function addComponent(toBeCreatedFiles, component) {
  // 遍历配置信息
  Object.keys(toBeCreatedFiles).forEach((dir) => {
    const _d = path.resolve(cwdPath, dir);
    // 生成对应目录
    fs.mkdir(_d, { recursive: true }, (err) => {
      if (err) {
        utils.log(err, 'error');
        return;
      }
      console.log(`${_d} directory has been created successfully!`);
      const contents = toBeCreatedFiles[dir];
      // 获取配置信息中文件配置,即 files 字段
      /*
          [`src/${component}`]: {
              desc: 'component source code',
              files: [
                {
                  file: 'index.ts',
                  template: 'index.ts.tpl',
                },
                {
                  file: `${component}.tsx`,
                  template: 'component.tsx.tpl',
                },
              ],
            },
      */
      contents.files.forEach((item) => {
        if (typeof item === 'object') {
          if (item.template) {
            // 对象,内部通过 lodash 进行数据与模板结合,并生成相对应文件
            outputFileWithTemplate(item, component, contents.desc, _d);
          }
        } else {
          // 非对象,直接生成文件
          const _f = path.resolve(_d, item);
          createFile(_f, '', contents.desc);
        }
      });
    });
  });
}

outputFileWithTemplate

const _ = require('lodash');

function outputFileWithTemplate(item, component, desc, _d) {
  // 解析模板路径
  const tplPath = path.resolve(__dirname, `./tpl/${item.template}`);
  // 获取模板文件内容
  let data = fs.readFileSync(tplPath).toString();
  // 通过 _.template 返回模板编译函数
  const compiled = _.template(data);
  // 模板注入数据,返回注入数据后的模板内容
  data = compiled({
    component,
    upperComponent: getFirstLetterUpper(component),
  });
  const _f = path.resolve(_d, item.file);
  // 生成文件
  createFile(_f, data, desc);
}

lodash.template的一个使用例子~

// 使用 "interpolate" 分隔符创建编译模板\
var compiled = _.template('hello <%= user %>!');\
compiled({ 'user': 'fred' });\
// => 'hello fred!'

insertComponentToIndex

function insertComponentToIndex(component, indexPath) {
  // 组件名首字母大写:table -> Table
  const upper = getFirstLetterUpper(component);
  // last import line pattern
  const importPattern = /import.*?;(?=\n\n)/;
  // components pattern
  const cmpPattern = /(?<=const components = {\n)[.|\s|\S]*?(?=};\n)/g;
  const importPath = getImportStr(upper, component);
  const desc = '> insert component into index.ts';
  let data = fs.readFileSync(indexPath).toString();
  if (data.match(new RegExp(importPath))) {
    utils.log(`there is already ${component} in /src/index.ts`, 'notice');
    return;
  }
  // insert component at last import and component lines.
  data = data.replace(importPattern, (a) => `${a}\n${importPath}`).replace(cmpPattern, (a) => `${a}  ${upper},\n`);
  fs.writeFile(indexPath, data, (err) => {
    if (err) {
      utils.log(err, 'error');
    } else {
      utils.log(`${desc}\n${component} has been inserted into /src/index.ts`, 'success');
    }
  });
}

删除组件

删除组件主要逻辑为:deleteComponent -> deleteComponentFromIndex

deleteComponent

function deleteComponent(toBeCreatedFiles, component) {
   // 获取组件单元测试文件
  const snapShotFiles = getSnapshotFiles(component);
  // 合并到配置信息中
  const files = Object.assign(toBeCreatedFiles, snapShotFiles);
  // 遍历
  Object.keys(files).forEach((dir) => {
    const item = files[dir];
    if (item.deleteFiles && item.deleteFiles.length) {
      item.deleteFiles.forEach((f) => {
      // 移除文件
        fs.existsSync(f) && fs.unlinkSync(f);
      });
    } else {
      utils.deleteFolderRecursive(dir);
    }
  });
  utils.log('All radio files have been removed.', 'success');
}

// 单元测试文件
function getSnapshotFiles(component) {
  return {
    [`test/unit/${component}/__snapshots__/`]: {
      desc: 'snapshot test',
      files: ['index.test.js.snap', 'demo.test.js.snap'],
    },
  };
}

deleteComponentFromIndex

function deleteComponentFromIndex(component, indexPath) {
    // 获取组件名称
  const upper = getFirstLetterUpper(component);
  // 获取 import 字符串
  const importStr = `${getImportStr(upper, component)}\n`;
  // 获取入口文件内容
  let data = fs.readFileSync(indexPath).toString();
  // 匹配内容替换
  data = data.replace(new RegExp(importStr), () => '').replace(new RegExp(`  ${upper},\n`), '');
  // 重新写回到入口文件中
  fs.writeFile(indexPath, data, (err) => {
    if (err) {
      utils.log(err, 'error');
    } else {
      utils.log(`${component} has been removed from /src/index.ts`, 'success');
    }
  });
}

function getImportStr(upper, component) {
  return `import ${upper} from './${component}';`;
}

总结

在了解了npm run init [component]命令内部逻辑后,可以学习到一种模板思想,即通过输入命令,让模板与数据结合,生成组件代码目录,减少手动创建同类型的目录结构以及文件操作(类似于cli,即生成项目文件)~