通过tdesign-vue-next 学学人家开发UI组件库新建组件的方法

653 阅读4分钟

我们平时新建页面的时候都是一个一个的新建,但是在开发大型UI组件库的时候新建一个组件除了新建这个组件的源文件外,还要新建对应的demo文件,单元测试文件等,手动可不是好办法,今天要学习的tdesign-vue-next 初始化组件使用一个命令就自动创建相关文件,一起学习吧。本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。这是源码共读的第34期

1.准备工作

(1)克隆代码

(2)安装依赖(npm i失败了 删掉node_modules 用cnpm i 成功了)

到这儿还没完事儿,看下图:

要问怎么办?快把资料看:【第三十四期】- 骁 - tdesign-vue-next 初始化组件 ,这位大佬关于调试环节介绍的很清楚。我是切到了src_common目录下然后执行 git clone git@github.com:Tencent/tdesign-common.git

顺便说一下,关于连不上github解决办法好多,我用的是dev-sidecar。

另外说一下.gitmodules文件的作用:用于记录一个项目的子项目,格式如下:

[submodule "abc"]
	path = abc
	url = http://github.xxx.xxx/xxxx
	branch = release

tdesign-vue-next中是这样的:

[submodule "src/_common"]
	path = src/_common
	url = git@github.com:Tencent/tdesign-common.git

(3)改package.json文件

修改init对应的脚本:

"init": "node script/init/index.js testCom1",

(4) 打断点

在script/init/index.js的init,如下图所示在156行打上断点:

2.入口文件init

2.1 init方法概览

执行npm run init时,会执行tdesign-vue-next/script/init/index.js 中的init方法,init方法中的核心逻辑是参数检查、获取目标文件、添加或者删除文件。通过一张导图概览其核心内容:

2.2 init方法分析

下面结合断点调试过程分析:

const [component, isDeleted] = process.argv.slice(2);

获取要创建或者删除的组件名以及是否删除,process.argv是一个数组,第一元素为nodejs的路径,第二个元素为当前js文件的路径,第三个元素为参数,如下图所示:

如下图所示,笔者传入的参数为 'testCom2' :

如下图所示indexPath的值为src目录下index.ts的路径:

接下来是通过调用getToBeCreatedFiles获取要创建的文件。

2.3 getToBeCreatedFiles

getToBeCreatedFiles方法代码如下:

function getToBeCreatedFiles(component) {
  // keys are directories, values are files.
  // desc - directory description
  // files - will be created
  // dirDeletable - if this directory can be deleted.
  return {
    [`src/${component}`]: {
      desc: 'component source code',
      files: [
        {
          file: 'index.ts',
          template: 'index.ts.tpl',
        },
        {
          file: `${component}.tsx`,
          template: 'component.tsx.tpl',
        },
      ],
    },
    [`examples/${component}`]: {
      desc: 'component API',
      files: [
        {
          file: `${component}.md`,
          template: 'component.md.tpl',
        },
      ],
    },
    [`examples/${component}/demos`]: {
      desc: 'component demo code',
      files: [
        {
          file: 'base.vue',
          template: 'base.demo.tpl',
        },
      ],
    },
    [`test/unit/${component}`]: {
      desc: 'unit test',
      files: [
        {
          file: 'index.test.js',
          template: 'index.test.tpl',
        },
        {
          file: 'demo.test.js',
          template: 'demo.test.tpl',
        },
      ],
    },
    [`test/e2e/${component}`]: {
      desc: 'e2e test',
      files: [`${component}.spec.js`],
    },
  };
}

module.exports = {
  getToBeCreatedFiles,
};

通过如上代码我们知道,创建一个组件首先要在src下面创建名为由参数component指定的目录,然后在此目录下创建名为由参数component指定的tsx文件和index.tsx文件。除了创建源文件外还需要创建demo示例文件,单元测试文件。创建每一种文件都有对应的模板文件,模板文件的存放路径是script/init/tpl。

下图是展示了getToBeCreatedFiles对象:

3.新增组件

和新增组件有关的方法有两个:addComponent和insertComponentToIndex。addComponent是用于创建组件的方法,insertComponentToIndex是要让src/index.ts 文件引入我们新创建的组件。

3.1 addComponent

addComponent代码如下:

function addComponent(toBeCreatedFiles, component) {
  // At first, we need to create directories for components.
  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!`);
      // Then, we create files for components.
      const contents = toBeCreatedFiles[dir];
      contents.files.forEach((item) => {
        if (typeof item === 'object') {
          if (item.template) {
            outputFileWithTemplate(item, component, contents.desc, _d);
          }
        } else {
          const _f = path.resolve(_d, item);
          createFile(_f, '', contents.desc);
        }
      });
    });
  });
}

addComponent整体结构为双循环,第一个forEach用于创建目录,第二个forEach用于创建文件。创建目录调用的是fs.mkdir API;创建文件调用outputFileWithTemplate或者createFile, 两者最终都是通过调用fs.writeFile API实现的,下文会分析这两个函数。

如下图所示,要创建'src/testCom2'目录:

如下图所示是要根据index.ts.tpl模板文件创建index.ts文件:

如下图所示,要创建的文件存在模板文件则根据模板文件创建文件:

3.2 outputFileWithTemplate

根据模板文件创建文件使用的是outputFileWithTemplate方法:

function outputFileWithTemplate(item, component, desc, _d) {
  const tplPath = path.resolve(__dirname, `./tpl/${item.template}`);
  let data = fs.readFileSync(tplPath).toString();
  const compiled = _.template(data);
  data = compiled({
    component,
    upperComponent: getFirstLetterUpper(component),
  });
  const _f = path.resolve(_d, item.file);
  createFile(_f, data, desc);
}

tplPath是获取模板文件目录,通过fs.readFileSync获取模板文件内容,这里还使用到了lodash的template方法创建模板编译函数compiled,compiled可以把模板中的内同替换为对应的参数。最后调用createFile方法创建文件。outputFileWithTemplate方法的调试过程如下图所示:

3.3 createFile

function createFile(path, data = '', desc) {
  fs.writeFile(path, data, (err) => {
    if (err) {
      utils.log(err, 'error');
    } else {
      utils.log(`> ${desc}\n${path} file has been created successfully!`, 'success');
    }
  });
}

写入文件主要是调用fs.writeFile, 如果文件写入成功则提示文件创建成功。

3.4 insertComponentToIndex

insertComponentToIndex是要在src/index.ts中引入我们新创建的组件,代码如下:

function insertComponentToIndex(component, indexPath) {
  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');
    }
  });
}

getFirstLetterUpper是将组件名的首字母转成大写,getImportStr是获取引入组件的import语句,调用fs.readFileSync方法读取index.ts文件的内容。然后使用字符串的match方法检查是否已经导入过新创建的组件了,如果没有导入过则使用replace方法对原来的内容进行替换。最后使用 fs.writeFile方法将内容写入文件。

下图为具体调试过程:

4.删除组件

4.1 deleteComponent方法

删除组件使用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');
}

删除文件的时候要把执行单元测试生成的文件也删除,所以调用了getSnapshotFiles方法;fs.existsSync(f)用于判断路径是否存在,fs.unlinkSync(f)用于删除文件。在调试的可以发现由于item上不存在deleteFiles属性则会调用deleteFolderRecursive方法, 此方法用于递归删除目录下的文件。

4.2 deleteFolderRecursive

function deleteFolderRecursive(path) {
  if (fs.existsSync(path)) {
    fs.readdirSync(path).forEach((file) => {
      const current = `${path}/${file}`;
      if (fs.statSync(current).isDirectory()) {
        deleteFolderRecursive(current);
      } else {
        fs.unlinkSync(current);
      }
    });
    fs.rmdirSync(path);
  }
}

deleteFolderRecursive是一个递归方法,如果当前目录下的某个路径对应的是目录而不是文件则继续在这个目录下进行递归删除文件的操作。这里使用了fs.statSync方法获取文件信息,然后调用fs.Stats 类的isDirectory()

方法判断是否为目录。fs.rmdirSync用于删除目录,注意目录下的文件删除完了之后,要把目录本身删除。

下图展示了删除组件的具体调试过程:

4.3 deleteComponentFromIndex

由于新增组件时,在src/index.ts文件中引入了新增的组件,所以删除组件的时候要把对应的import语句删除,代码如下:

function deleteComponentFromIndex(component, indexPath) {
  const upper = getFirstLetterUpper(component);
  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');
    }
  });
}

代码执行流程和insertComponentToIndex类似,不再赘述,下图为具体调试过程:

5.收获总结

1.了解.gitmodules文件的作用和格式;

2.了解tdesign-vue-next初始化(或删除)组件的流程;

3.了解.tpl模板文件的使用以及lodash的template方法的作用;

4.了解nodejs常用API的使用,例如:fs.readFileSync,fs.readdirSync,fs.writeFile,fs.rmdirSync,fs.unlinkSync,fs.statSync,fs.existsSync,fs.mkdir。