ElementUI源码系列四 - 学习new.js文件之自动创建组件目录结构与生成components.json文件内容

「这是我参与2022首次更文挑战的第4天,活动详情查看:2022首次更文挑战」。

写在开头

上一篇文章 ElementUI源码系列三 - 学习gen-cssfile.js文件之自动创建组件的.scss文件与生成index.scss文件内容 我们讲过添加一个新组件要经历三个步骤:

  • 第一步 - 创建组件目录结构
  • 第二步 - 创建组件样式文件
  • 第三步 - 总入口文件引入组件

并且我们实现了第二步的自动创建操作,也就是仅通过命令即可完成创建文件,再也不需要手动创建操作。当然,这第二步还不够高级,这一章将带你来实现第一步骤的自动化并且吸收第二步骤成果,让它整体变得更高级一些,嘿嘿(✪ω✪)。

011275FF.jpg

前置小知识

process模块

process 模块是 Node 环境的全局模块,属于全局对象,它好比浏览器环境上的 window 对象,不同的是它存在于 Node 环境中而已,它身上有很多方法与属性。

下面我们在项目根目录下随便创建一个 test.js 文件,内容如下:

// test.js
console.log(process.argv);

process.on('exit', () => {
  console.log('命令行退出了');
});
复制代码

执行这个脚本文件:

image.png

由上面截图我们可以知道 process.argv 能获取到命令行输入的一些参数,知道这个知识就够了。对 process 模块感兴趣小伙伴可以再自行去学习学习。

file-save模块

file-save 模块,这是我们后面会用到的模块,它与我们上一篇文章讲的 fs.writeFileSync('文件路径', '文件内容', '文件编码/错误回调'); 很相似,也是用于创建文件与写入文件内容的。

下载该包:npm install [email protected] -D

基本使用:

// test.js
const fileSave = require('file-save');

fileSave('./1.js')
  .write(`var a = 1;`, 'utf8')
  .write(`var b = 2;`, () => {
    console.log('写入回调')
  })
  .end('\n')
  .finish(() => {
    console.log('写入完成');
  });
复制代码

如果目录不存在,模块将自行创建目录,它其实是对写入进行了一层封装,实现了分段的链式调用,使用起来更加方便快捷。

uppercamelcase模块

uppercamelcase 模块是将破折号/点/下划线/空格分隔的字符串转换为驼峰形式。

下载该包:npm install [email protected] -D

基本使用:

// test.js
const uppercamelcase = require('uppercamelcase');
const ComponentName = uppercamelcase('el-button'); // ElButton
const ComponentName = uppercamelcase('button'); // Button
复制代码

JSON.stringify()第三个参数

JSON.stringify(value[, replacer [, space]]) 方法相信大家都很熟了,但是可能平常在使用过程中比较少去注意到它的另外两个参数。

我们来看看下面三个例子:

var obj = {
  name: '橙某人',
  age: 18,
  hobby: ['coding', 'writing']
};
复制代码

只有一个参数时,对象转成字符串,这就不多说了。

console.log(JSON.stringify(obj)); // {"name":"橙某人","age":18,"hobby":["coding","writing"]}
复制代码

有第二个参数时,第二个参数可以是一个数组或者一个函数,它用于过滤某个键值对。

console.log(JSON.stringify(obj, ['name', 'age']));// {"name":"橙某人","age":18}
复制代码

有第三个参数时,第三个参数用于用来控制结果字符串里面的间距。

console.log(JSON.stringify(obj, null, '  '));
console.log(JSON.stringify(obj, null, '~'));
复制代码

image.png

自动创建组件目录结构

了解完上面的一些小知识后,我们来进入本章的主题,观察我们前面完成的 buttondivider 两个组件,他们的目录结构基本都是相同的,都是由一个 index.jssrc/main.vue 文件组成,而且 index.js 里面的内容也大体上是相似的。

image.png

而这小节我们的目标就是通过命令的形式来实现自动创建组件的目录结构。

首先,我们先来创建 build/src/new.js 文件。

image.png

其次,再给这个脚本文件配置一条命令:

"new": "node build/bin/new.js"
复制代码

然后我们来看看下面 new.js 文件的内容:

// 监听命令行退出
process.on('exit', () => {
  console.log('命令行退出了');
});
// 执行 npm run new xxx 必须填写组件名, 否则直接结束命令
if (!process.argv[2]) {
  console.error('[组件名]必填 - Please enter new component name');
  process.exit(1);
}

// 引入相关模块
const path = require('path');
const fileSave = require('file-save');

// 获取第一个参数
const componentname = process.argv[2];
// 绝对路径: 定位到你项目的packages目录下
const PackagePath = path.resolve(__dirname, '../../packages', componentname);

// 定义需要创建的文件 
const Files = [
  {
    filename: 'index.js',
    content: '',
  },
  {
    filename: 'src/main.vue',
    content: '',
  }
];

// 循环创建定义的文件
Files.forEach(file => {
  fileSave(path.join(PackagePath, file.filename))
    .write(file.content, 'utf8')
    .end('\n');
});
复制代码

脚本文件的内容应该不难吧,就是根据我们定义 Files 数组去循环创建所需的文件。执行 npm run new input 命令,可以看到组件的目录会自动创建出来。

image.png

不过现在这两个文件还都是空文件,但我们在 Files 里面给它们预留了一个 content 字段来填充,但是关键这内容要如何填呢?这个可以靠你自个发挥了,这有点像我们平常配置编辑器快速创建文件预填的模板一样。

...
// 导入uppercamelcase
const uppercamelcase = require('uppercamelcase');

...
// 转换成驼峰形式 el-button  :  ElButton
const ComponentName = uppercamelcase(componentname);

const Files = [
  {
    filename: 'index.js',
    content: 
    `
    import ${ComponentName} from './src/main.vue';

    /* istanbul ignore next */
    ${ComponentName}.install = function(Vue) {
      Vue.component(${ComponentName}.name, ${ComponentName});
    };
    export default ${ComponentName};
    `,
  },
  {
    filename: 'src/main.vue',
    content: 
    `
    <template>
      <div class="el-${componentname}">${componentname}</div>
    </template>

    <script>
      export default {
        name: 'El${ComponentName}'
      };
    </script>
    `,
  }
];
...
复制代码

我们直接把 ElementUI 源码的预留的模板搬过来,再次执行 npm run new input ,你会发生文件内容产生了。(文件内容格式可以自行在调整一下,这里为了浏览效果做了些换行处理)

001F93E6.jpg

自动生成components.json文件

上一篇文章 ElementUI源码分析系列 - 学习gen-cssfile.js文件之自动创建组件的.scss文件与生成index.scss文件 中我们学到了通过执行 npm run gen 命令可以帮助我们快速创建组件的 .scss 样式文件,但是前提是你在 components.json 文件中对组件进行了定义才行。

// components.json
{
  "button": "./packages/button/index.js",
  "divider": "./packages/divider/index.js",
  "loading": "./packages/loading/index.js",
  "alert": "./packages/alert/index.js",
}
复制代码

而上面小节我们学习了通过 npm run new xxx 命令去创建一个新组件,那么这个新组件的 .scss 文件我们一样先去 components.json 中定义,然后再去执行 npm run gen 命令自动创建出来?这显然是很麻烦、不靠谱的。。。

那能不能在我们创建完一个新组件,顺便在 components.json 文件中添加新组件的定义呢?答案当然是可以啦。

我们来看看如何改造 new.js 文件:

process.on('exit', () => {
  console.log('命令行退出了');
});
if (!process.argv[2]) {
  console.error('[组件名]必填 - Please enter new component name');
  process.exit(1);
}

const path = require('path');
const fileSave = require('file-save');
const uppercamelcase = require('uppercamelcase');
const componentname = process.argv[2];
const PackagePath = path.resolve(__dirname, '../../packages', componentname);
const ComponentName = uppercamelcase(componentname);

// 获取 components.json 的内容
const componentsFile = require('../../components.json');
if (componentsFile[componentname]) {
  console.error(`${componentname} 已存在.`);
  process.exit(1); // 如果 components.json 文件中已经有新组件的定义, 直接就退出命令行, 组件目录也不用生成了
}
// 添加新组件定义
componentsFile[componentname] = `./packages/${componentname}/index.js`;
// 把最终结果写回 components.json 文件中
fileSave(path.join(__dirname, '../../components.json'))
  .write(JSON.stringify(componentsFile, null, '  '), 'utf8') // JSON.stringify第三个参数上面预备知识有提及
  .end('\n');

const Files = [
  {
    filename: 'index.js',
    content: '',
  },
  {
    filename: 'src/main.vue',
    content: '',
  }
];

Files.forEach(file => {
  fileSave(path.join(PackagePath, file.filename))
    .write(file.content, 'utf8')
    .end('\n');
});
复制代码

Files 变量的 content 字段由于太占空间我就先省略了,实际就是增加了七八行代码。你可以再次执行 npm run new input 命令,查看 components.json 文件是否自动生成组件定义。

到此,我们创建一个新组件可以直接执行 npm run new xxxnpm run gen 命令来完成基础创建操作,但是,连续执行两个命令还不是我们最终想要的,我们追求一条命令搞定一切,下面我们接着来看。

合并第二步骤的自动化操作

我们的期望是:执行 npm run new xxx 命令即可自动创建组件目录结构并且创建组件的 .scss 文件,而且这个 .scss 文件会自动在 index.scss 总样式文件中引入。

这应该如何弄呢?创建组件的 .scss 文件?创建文件...?这还不简单?我们上面定义了 Files 数组变量还记得不?我们再往它里面添加一项不就搞定了。

// new.js
...

// 定义需要创建的文件 
const Files = [
  {
    filename: 'index.js',
    content: '',
  },
  {
    filename: 'src/main.vue',
    content: '',
  },
  {
    filename: path.join('../../packages/theme-chalk/src', `${componentname}.scss`),
    content: 
    `
      @import "mixins/mixins";
      @import "common/var";

      @include b(${componentname}) {
        
      }
    `
  },
];
...
复制代码

添加后,你再次执行 npm run new xxx 命令,看是否有生成对应组件的 .scss 文件,答案必然是有的吧?

.scss 文件的内容通过观察 ElementUI 的每个组件的 .scss 文件,它们都会有一个下图这样子的公共结构,直接加上就行啦,你也可以随便改咯,看你心情(^ω^) 。

image.png

创建的搞定了,剩下的就是在 index.scss 文件中引入的问题了,就不卖关子了,直接看:

// new.js
...
const fs = require('fs');

// 获取总样式文件 index.scss 路径
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');

const Files = [
  {
    filename: 'index.js',
    content: '',
  },
  {
    filename: 'src/main.vue',
    content: '',
  },
  {
    filename: path.join('../../packages/theme-chalk/src', `${componentname}.scss`),
    content: ''
  },
];
...
复制代码

再增加四行代码,最终你要执行 npm run new xxx 测试看看是否正确哦。

最终,我们就完成了一条命令 npm run new xxx 创建组件的一切相关事宜,大功告成,真是可喜可贺哇。

006169EA.gif

new.js 完整源码

process.on('exit', () => {
  console.log('命令行退出了');
});
if (!process.argv[2]) {
  console.error('[组件名]必填 - Please enter new component name');
  process.exit(1);
}

const path = require('path');
const fs = require('fs');
const fileSave = require('file-save');
const uppercamelcase = require('uppercamelcase');
const componentname = process.argv[2];
const PackagePath = path.resolve(__dirname, '../../packages', componentname);
const ComponentName = uppercamelcase(componentname);

// components.json
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
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');

const Files = [
  {
    filename: 'index.js',
    content: `import ${ComponentName} from './src/main';

/* istanbul ignore next */
${ComponentName}.install = function(Vue) {
  Vue.component(${ComponentName}.name, ${ComponentName});
};
    
export default ${ComponentName};`,
  },
  {
    filename: 'src/main.vue',
    content: `<template>
  <div class="el-${componentname}"></div>
</template>
  
<script>
export default {
  name: 'El${ComponentName}'
};
</script>`,
  },
  {
    filename: path.join('../../packages/theme-chalk/src', `${componentname}.scss`),
    content: `@import "mixins/mixins";
@import "common/var";

@include b(${componentname}) {
        
}`
  },
];

Files.forEach(file => {
  fileSave(path.join(PackagePath, file.filename))
    .write(file.content, 'utf8')
    .end('\n');
});
复制代码




往期内容




至此,本篇文章就写完啦,撒花撒花。

image.png

希望本文对你有所帮助,如有任何疑问,期待你的留言哦。
老样子,点赞+评论=你会了,收藏=你精通了。

Guess you like

Origin juejin.im/post/7061848394544709668