likes
comments
collection
share

【源码阅读】Element-UI 新增组件功能

作者站长头像
站长
· 阅读数 40

功能介绍

作用

在 Element-UI 源码中可以通过命令 make new 新增组件,减少重复工作。通过该命令创建组件目录结构,包含测试代码、入口文件、文档。

使用

make new testCom 阿豪阿卡阿甘 // make new 组件名称 中文名称

源码整体流程

1. 了解 make new 命令

既然是通过 make new 进行的创建,那么就先了解一下 make 是什么,通过 这篇文章 可以知道它其实是一个构建命令,也有对应的配置文件,对应到项目中就是根目录下的 Makefile,从配置文件中可以看到对应的 new 的命令

new:
  node build/bin/new.js $(filter-out $@,$(MAKECMDGOALS))

这里就可以看到 make new 命令对应的是: build/bin/new.js,接下来就进入到该文件中看看都做了什么。

2. 命令参数校验

'use strict';
​
console.log();
process.on('exit', () => {
  console.log();
});
​
if (!process.argv[2]) {
  console.error('[组件名]必填 - Please enter new component name');
  process.exit(1);
}

文件开始就先对程序退出进行了监听,退出之后打印一个空行,这里的空行和上边的空行都是在美化 UI。

接下来就判断了该命令传入的参数,如果没有组件名则退出并提示。

这里产生的疑问有

  1. process.argv 都包含了什么?
  2. process.exit(1) 为什么要传入 1 不传可以吗?

这里的疑问不影响主流程,知道它的意思就可以继续往下看,走完主流程之后来解决这些问题。

3. 引入依赖并初始化变量

3.1 引入依赖

const path = require('path');
const fs = require('fs');
// file-save: 将数据流存入文件并使用 Stream 保存(如果目录不存在,模块将自己创建目录)。
const fileSave = require('file-save');
// uppercamelcase: 根据指定格式进行驼峰转换
const uppercamelcase = require('uppercamelcase');

3.2 初始化变量

// 组件名称
const componentname = process.argv[2];
// 组件对应的中文名称
const chineseName = process.argv[3] || componentname;
// 格式转换后的组件名称,这里是转换为了大驼峰
const ComponentName = uppercamelcase(componentname);
// packages 目录下放了所有的组件源码,这里拼出新的组件源码目录
const PackagePath = path.resolve(__dirname, '../../packages', componentname);
// 组件需要用到的文件模板
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>`
  }
  ...
];
// 拿到根目录下的 components.json 中的内容,这里包含了组件名称及对应源码入口。
// 这里拿到的是文件内容,并不是该文件的目录
const componentsFile = require('../../components.json');

path.resolve(__dirname, '../../packages', componentname) 这里产生了疑问

  1. path.resolve() 的作用是什么?为什么不用 path.join()
  2. __dirname 的作用?

这些不影响主流程的问题后面统一解决。

4. 将组件入口文件路径定义到 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');

componentsFile 是在前一步拿到的 components.json 文件内容,这里先判断了组件是否已经存在该文件中,存在则退出。

然后将该组件名称和组件入口文件路径以 key: value 的形式定义到 componentsFile 中,然后将 componentsFile 重新写入到 components.json

这里在写入的时候通过 JSON.stringify() 的第三个参数对待写入文件进行了格式化。

在写入完成之后在最后写入了一个换行,这里也是为了优化文件内容的可读性。

5. 将组件的样式文件在 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');

index.scss 文件是所有组件样式的入口文件,这里将新的组件的样式文件进行引入。

这里的操作和上一步类似,也是先获取到了统一的样式入口文件,然后定义该组件的样式文件路径,并将新的文件内容重新写入。

6. 组件的 TS 类型定义

// 添加到 element-ui.d.ts
const elementTsPath = path.join(__dirname, '../../types/element-ui.d.ts');
​
// 文件中是先将组件的 TS 类型定义 import,然后再 export,这里是拼接了 export 的内容
let elementTsText = `${fs.readFileSync(elementTsPath)}
/** ${ComponentName} Component */
export class ${ComponentName} extends El${ComponentName} {}`;
​
// 根据文件中第一个 export 的位置获取到文件中 import 的结束位置
const index = elementTsText.indexOf('export') - 1;
// 定义 import 内容
const importString = `import { El${ComponentName} } from './${componentname}'`;
​
// 将 import 内容和 export 内容进行拼接组成新的文件内容
elementTsText = elementTsText.slice(0, index) + importString + '\n' + elementTsText.slice(index);
​
// 覆写原有文件
fileSave(elementTsPath)
  .write(elementTsText, 'utf8')
  .end('\n');

element-ui.d.ts 文件是所有组件 TS 类型的汇总,在这里将统一进行导出。

这里的操作和上一步类似,拿到原有内容,将新内容进行拼接,然后覆写原有文件。

6. 将 Files 中定义的组件文件模板进行创建

// 创建 package
Files.forEach(file => {
  fileSave(path.join(PackagePath, file.filename))
    .write(file.content, 'utf8')
    .end('\n');
});

Files 重定义了文件名称组件需要用的文件模板,PackagePath 是在前边定义的存放组件源码的目录,然后遍历 Files 进行创建文件。

7. 将组件名称定义到菜单栏中

// 添加到 nav.config.json
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');
​
console.log('DONE!');

examples/nav.config.json 文件中定义的是 Element-UI 组件页面 左侧菜单。这里也是拿到原始内容将新的组件名称拼接进去然后覆写。在写入的过程中对文件格式通过 JSON.stringify() 进行美化。

到这里新建组件的流程就全部走完了。

总结

下面这些就是执行 make new 命令之后会生成及修改的文件,有了这个命令就可以省略这么多的文件操作,提高了开发效率。

【源码阅读】Element-UI 新增组件功能

疑问

process.argv 都包含了什么

  1. 第 0 个元素是启动 Node.js 进程的可执行文件所在的绝对路径
  2. 第 1 个元素是当前所执行的 js 文件绝对路径
  3. 第 2 个及后面所有的元素是参数

process.exit(1) 为什么要传入 1 不传可以吗

可以传入 0 或 1,0 表示正常结束进程,1 表示因为有故障结束进程(Process exited with code 1)

path.resolve() 的作用是什么?为什么不用 path.join() ,两者有什么区别

  1. path.resolve() 的作用: 返回被执行文件的绝对目录

  2. 为什么不用 path.join() ,两者有什么区别:

    • path.join() 用于连接路径,接受多个参数,同时对路径进行规范化。

      const path = require('path'); 
      //合法的字符串连接 
      path.join('/foo', 'bar', 'baz/asdf', 'quux', '..') 
      // 连接后
      '/foo/bar/baz/asdf' 
      
    • path.resolve() 可以将多个路径解析为一个规范化的绝对路径。

      其处理方式类似于对这些路径逐一进行 cd 操作,与 cd 操作不同的是这些路径可以是文件,并且可以是不存在的路径。

      例如:

      path.resolve('foo/bar', '/tmp/file/', '..', 'a/../subfile')
      

      相当于

      cd foo/bar
      cd /tmp/file/
      cd ..
      cd a/../subfile
      pwd
      

      例子

      path.resolve('/foo/bar', './baz') 
      // 输出结果为 
      '/foo/bar/baz' 
      path.resolve('/foo/bar', '/tmp/file/') 
      // 输出结果为 
      '/tmp/file' 
      ​
      path.resolve('wwwroot', 'static_files/png/', '../gif/image.gif') 
      // 当前的工作路径是 /home/itbilu/node,则输出结果为 
      '/home/itbilu/node/wwwroot/static_files/gif/image.gif'
      

__dirname 的作用

返回被执行文件的绝对目录

参考资料