- 本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
- 这是源码共读的第 34 期,链接:juejin.cn/post/710910…。
前言
在我们给我们的组件库添砖加瓦的时候,经常会有一些重复的工作,比如组件本身的文件、单元测试、使用示例、组件文档等的创建,那有没有更加优雅、方便快捷的方式去创建这些文件呢?答案是肯定的,我在 tdesing-vue 组件库中找到了答案,下面让我们一起来学习一下它是如何实现的。
源码地址
- github仓库地址 github.com/Tencent/tde…
一、使用
1.1 脚本文件结构
init
├── config.js # 配置文件
├── index.js # 入口文件
└── tpl # 模板文件夹
├── base.demo.tpl
├── component.md.tpl
├── component.tsx.tpl
├── demo.test.tpl
├── index.test.tpl
├── index.ts.tpl
└── ssr.demo.test.tpl
1.3 新增组件
// xiaolong 为新增的组件名
node init/index.js xiaolong
- 效果
1.4 删除组件
// xiaolong 为需要删除的组件名
node init/index.js xiaolong del
- 效果
二、源码解读
2.1 流程
2.2 init 方法解读
function init() {
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);
}
}
- 通过
process.argv
获取输入参数:组件名称component
、是否执行删除动作isDeleted
; component
必传参数是否传入,未传入则给出错误提示,已传入则继续向下执行;- 获取
indexPath
,指定组件的插入或移除的具体文件地址; - 通过
config.getToBeCreatedFiles
生成需要创建的文件信息对象toBeCreatedFiles
; isDeleted === 'del'
则执行删除组件方法deleteComponent
、insertComponentToIndex
,否则执行新增组件的方法addComponent
、insertComponentToIndex
。
2.3 新增组件
2.3.1 addComponent
添加新增组件所需文件夹及具体文件
function addComponent(toBeCreatedFiles, component) {
// At first, we need to create directories for components.
// 根据 toBeCreatedFiles 创建所需的文件夹
Object.keys(toBeCreatedFiles).forEach((dir) => {
// cwdPath 为当前工作目录
const _d = path.resolve(cwdPath, dir);
// recursive: 指示是否应创建父文件夹。 如果创建了文件夹,将返回第一个创建的文件夹的路径。
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];
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);
}
});
});
});
}
2.3.2 insertComponentToIndex
将新增组件插入到指定文件中(场景:组件的统一导出,使用时的导入)
function insertComponentToIndex(component, indexPath) {
// 通过 getFirstLetterUpper 方法,将组件名首字母大写
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');
}
});
}
2.4 删除组件
2.4.1 deleteComponent
删除组件相关的文件及文件夹
function deleteComponent(toBeCreatedFiles, component) {
// 通过 getSnapshotFiles 获取需要删除的组件快照相关文件信息
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 {
// 没有指定 deleteFiles 时,通过递归的形式去删除当前文件夹下的文件
utils.deleteFolderRecursive(dir);
}
});
utils.log('All radio files have been removed.', 'success');
}
2.4.2 deleteComponentFromIndex
从指定文件中移除组件的引用
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');
}
});
}
2.5 其它辅助方法
2.5.1 getToBeCreatedFiles
通过传入的组件名生成需要新增或删除的文件信息
function getToBeCreatedFiles(component) {
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`],
},
};
}
2.5.2 outputFileWithTemplate
通过模板来创建所需文件,item.template
指定了所需的具体模板,而所有相关的模板则放到了 tpl
文件夹中
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);
}
2.5.3 getFirstLetterUpper
将传入的组件名首字母转成大写
function getFirstLetterUpper(a) {
return a[0].toUpperCase() + a.slice(1);
}
2.5.4 getImportStr
拼接组件导入的文本信息
function getImportStr(upper, component) {
return `import ${upper} from './${component}';`;
}
2.5.5 getSnapshotFiles
获取组件快照文件信息
function getSnapshotFiles(component) {
return {
[`test/unit/${component}/__snapshots__/`]: {
desc: 'snapshot test',
files: ['index.test.js.snap', 'demo.test.js.snap'],
},
};
}
2.5.6 utils.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);
}
}
三、总结
通过阅读上述的源码,我们可以通过修改 getToBeCreatedFiles 方法返回的配置项以及 tpl 模版文件夹里的模版,获得符合我们自己业务场景的新增、删除组件的脚本。
另外,在技能知识点上,加深了对 fs 模块相关 API 的了解
- fs.mkdir 异步创建文件夹
- fs.existsSync 判断传入路径是否存在的同步版本
- fs.unlinkSync 同步删除文件或符号链接
- fs.rmdirSync 同步删除文件夹
- fs.statSync 同步获取文件状态
- fs.readFileSync 同步读取文件内容
- fs.writeFile 异步写入文件内容