- 本文参加了由公众号@若川视野发起的每周源码共读活动, 点击了解详情一起参与。
- 这是源码共读的第 34 期,链接:/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.*?;(?=nn)/; // 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 异步写入文件内容
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。
评论(0)