环境记录
windows10
node v14.17.3
yarn 1.22.4
由于make new执行的target是node build/bin/new.js,所以关注点直接切到new.js文件(其实是公司的windows电脑不支持make命令)
在源码解读过程中,约定执行node new.js的入参为:node new.js new-comp 新组件
new.js 1 2 3 4 if (!process.argv[2 ]) { console .error('[组件名]必填 - Please enter new component name' ); process.exit(1 ); }
process.argv 根据nodejs官方文档,process.argv为一个字符串数组,其中:
第一个元素为process.execPath
第二个元素是正在执行的Javascript文件的路径
其余元素是任何其他命令行参数
如:
1 2 console .log(process.argv)
执行
1 node test.js one two=true three:123
会输出:
1 2 3 4 5 6 7 [ 'NODE_HOME/node.exe', '__dirname/test.js', 'one', 'two=true', 'three:123' ]
process.argv的第一个元素是process.execPath 所以根据文档描述,execPath属性返回启动node.js进程的可执行文件的绝对路径。
结论 1 2 3 4 5 6 7 if (!process.argv[2 ]) { console .error('[组件名]必填 - Please enter new component name' ); process.exit(1 ); }
依赖及变量 1 2 3 4 5 6 7 8 9 10 11 12 13 14 const fileSave = require ('file-save' ); const uppercamelcase = require ('uppercamelcase' ); const componentname = process.argv[2 ]; const chineseName = process.argv[3 ] || componentname; const ComponentName = uppercamelcase(componentname); const PackagePath = path.resolve(__dirname, '../../packages' , componentname); const Files = [...]
上边依赖的的file-save已经超过7年没有维护了,而且原作者已经删除了代码仓库.. uppercamelcase依赖camelcase。而且自己的代码也只有短短3行,通过camelcase转为小驼峰后再将第一个字符转为大写然后拼接到原字符串的第二位前
Files内容 Files生成的文件目录较长,拆分开单独看会比较清晰:
生成packages/new-comp/index.js 1 2 3 4 5 6 7 8 9 10 11 { filename : 'index.js' , content : `import ${ComponentName} from './src/main'; /* istanbul ignore next */ ${ComponentName} .install = function(Vue) { Vue.component(${ComponentName} .name, ${ComponentName} ); }; export default ${ComponentName} ;` },
istanbul ignore next 作用是istanbul统计覆盖率的时候忽略install方法。 istanbul是土耳其最大城市伊斯坦布尔的命名,因为土耳其地毯世界文明,而地毯是用来覆盖的
生成packages/new-comp/src/main.vue 1 2 3 4 5 6 7 8 9 10 11 12 13 14 { filename : 'src/main.vue' , content : `<template> <div class="el-${componentname} "></div> </template> <script> export default { name: 'El${ComponentName} ' }; </script>` },
生成不同语言的文档 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 { filename : path.join('../../examples/docs/zh-CN' , `${componentname} .md` ), content : `## ${ComponentName} ${chineseName} ` }, { filename : path.join('../../examples/docs/en-US' , `${componentname} .md` ), content : `## ${ComponentName} ` }, { filename : path.join('../../examples/docs/es' , `${componentname} .md` ), content : `## ${ComponentName} ` }, { filename : path.join('../../examples/docs/fr-FR' , `${componentname} .md` ), content : `## ${ComponentName} ` },
文档通过markdown-it转成html后直接展示在页面上,开启本地调试后,可以访问本地文档查看修改后的内容
生成单元测试文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 { filename : path.join('../../test/unit/specs' , `${componentname} .spec.js` ), content : `import { createTest, destroyVM } from '../util'; import ${ComponentName} from 'packages/${componentname} '; describe('${ComponentName} ', () => { let vm; afterEach(() => { destroyVM(vm); }); it('create', () => { vm = createTest(${ComponentName} , true); expect(vm.$el).to.exist; }); }); ` }
生成样式 1 2 3 4 5 6 7 8 9 { filename : path.join('../../packages/theme-chalk/src' , `${componentname} .scss` ), content : `@import "mixins/mixins"; @import "common/var"; @include b(${componentname} ) { }` },
**@include ** ** b() **是sass中mixins的引入方式:查看theme-chalk/src/mixins.scss可以找到b的定义:
1 2 3 4 5 6 7 @mixin b($block ) { $B : $namespace +'-' +$block !global; .#{$B } { @content ; } }
其中$namespace在config.scss中被定义为el- ,**#{}**是一个插值语句,用#{}可以在属性名或选择其中使用变量。
**@content ** ** **可以的效果有点类似slot,@include b(new-comp){…attrs},attrs中的内容将被替换到@content的位置(不是非常确定)
可以理解为生成的CSS文件如下:
关于mixins中定义的BEM 在看mixins的时候发现其中对BEM的处理很不错,让我学到了之前一直没有想到的用法,在此记录:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 @mixin b($block ) { $B : $namespace +'-' +$block !global; .#{$B } { @content ; } } @mixin e($element ) { $E : $element !global; $selector : &; $currentSelector : "" ; @each $unit in $element { $currentSelector : #{$currentSelector + "." + $B + $element-separator + $unit + "," }; } @if hitAllSpecialNestRule($selector ) { @at-root { #{$selector } { #{$currentSelector } { @content ; } } } } @else { @at-root { #{$currentSelector } { @content ; } } } } @mixin m($modifier ) { $selector : &; $currentSelector : "" ; @each $unit in $modifier { $currentSelector : #{$currentSelector + & + $modifier-separator + $unit + "," }; } @at-root { #{$currentSelector } { @content ; } } }
如果我们在new-comp组件中遵守BEM的规范,定义一个paragraph元素:
1 2 3 4 5 6 <template > <div class ="el-new-comp" > <p class ="el-new-comp__paragraph" > This is new component</p > <p class ="el-new-comp__paragraph--dark" > This is new component,but my color is dark</p > </div > </template >
在new-comp.scss中定义样式:
1 2 3 4 5 6 7 8 9 10 11 12 @import 'mixins/mixins' ;@import 'common/var' ;@include b(new-comp) { @include e(paragraph) { font-size : 12px ; color : #ff0000 ; @include m(dark){ color :#000 ; } } }
new-comp.scss中的内容会被编译成:
1 2 3 4 5 6 7 .el-new-comp .el-new-comp__paragraph { font-size : 12px ; color : #ff0000 ; } .el-new-comp .el-new-comp__paragraph--dark { color : #000 ; }
在页面查看:
生成d.ts文件 1 2 3 4 5 6 7 8 9 { filename : path.join('../../types' , `${componentname} .d.ts` ), content : `import { ElementUIComponent } from './component' /** ${ComponentName} Component */ export declare class El${ComponentName} extends ElementUIComponent { }` }
将新组件添加到components.json 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const componentsFile = require ('../../components.json' ); if (componentsFile[componentname]) { console .error(`${componentname} 已存在.` ); process.exit(1 ); } componentsFile[componentname] = `./packages/${componentname} /index.js` ; fileSave(path.join(__dirname, '../../components.json' )) .write(JSON .stringify(componentsFile, null , ' ' ), 'utf8' ) .end('\n' );
将新组件的样式文件添加到index.scss 1 2 3 4 5 6 7 8 const sassPath = path.join(__dirname, '../../packages/theme-chalk/src/index.scss' ); const sassImportText = `${fs.readFileSync(sassPath)} @import "./${componentname} .scss";` ;fileSave(sassPath) .write(sassImportText, 'utf8' ) .end('\n' );
追加新组件的d.ts到element-ui.d.ts 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const elementTsPath = path.join(__dirname, '../../types/element-ui.d.ts' );let elementTsText = `${fs.readFileSync(elementTsPath)} /** ${ComponentName} Component */ export class ${ComponentName} extends El${ComponentName} {}` ;const index = elementTsText.indexOf('export' ) - 1 ;const importString = `import { El${ComponentName} } from './${componentname} '` ;elementTsText = elementTsText.slice(0 , index) + importString + '\n' + elementTsText.slice(index); fileSave(elementTsPath) .write(elementTsText, 'utf8' ) .end('\n' );
创建组件在package的内容 1 2 3 4 5 6 7 8 9 10 Files.forEach(file => { fileSave(path.join(PackagePath, file.filename)) .write(file.content, 'utf8' ) .end('\n' ); });
将新组件添加到路由中 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const navConfigFile = require ('../../examples/nav.config.json' );Object .keys(navConfigFile).forEach(lang => { let groups = navConfigFile[lang][4 ].groups; groups[groups.length - 1 ].list.push({ path : `/${componentname} ` , title : lang === 'zh-CN' && componentname !== chineseName ? `${ComponentName} ${chineseName} ` : ComponentName }); }); fileSave(path.join(__dirname, '../../examples/nav.config.json' )) .write(JSON .stringify(navConfigFile, null , ' ' ), 'utf8' ) .end('\n' );
花了部分时间阅读从nav.config到route.config然后生成路由的逻辑,搭配生成文件和生成路由,可以在自己的项目里进一步减少工作量
总结
自动生成文件的内容和阅读前想象的基本差不多,都是通过预定义模板动态插入内容 及固定位置插入文件的形式实现自动生成模板。之前在项目中研究过基于plop生成新的页面及组件,调整逻辑后更适用于业务层面的项目,但增加了额外依赖和hbs的学习成本。element-ui的new.js感觉更加适合组件库这种同级别组件的开发。
疑问 关于new.js中一开始的:
1 2 3 4 5 console .log();process.on('exit' , () => { console .log(); });
感觉没什么用,不是很明白保留这段代码的理由
参考文献 代码覆盖率工具 Istanbul 入门教程 - 阮一峰
Sass教程 Sass中文文档 | Sass中文网
NodeJs 14.x documentation