[源码共读] 第15期 element 初始化组件功能

141 阅读3分钟

第15期  element 初始化组件功能

每个大型开源项目都会有一个贡献指南,在Element-ui中的贡献指南中的组件开发规范中有一句:

  • 通过 make new 创建组件目录结构,包含测试代码、入口文件、文档

使用make new 创建组件目录结构。

make new

Element-ui的代码仓库中有 Makefile 这样一个文件。

Makefile 是一个适用于 C/C++ 的工具,较早作为工程化工具出现在 UNIX 系统中, 通过 make 命令来执行一系列的编译和连接操作。在拥有 make 环境的目录下, 如果存在一个 Makefile 文件。 那么输入 make 命令将会执行 Makefile 文件中的某个目标命令。

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

Filter-out反过滤函数

过滤掉 objs 中所有含有 pattert 的内容, 格式:

$(filter-out pattert, objs)

objs = aa bb cc dd ee 
pattert = bb cc ee 
out: 
    @echo $(filter-out $(pattert), $(objs))

执行 make out 输出 aa dd

执行make new , 就是用 node 命令 执行 build/bin/new.js 文件。

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

这条命令是使用 node 来运行 build/bin/new.js 这个文件, 并且使用 filter-out 函数在命令行的参数中把当前目标去掉,并作为参数传入。

new.js:

'use strict';

console.log();
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 chineseName = process.argv[3] || componentname;
const ComponentName = uppercamelcase(componentname);
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>`
  },
  {
    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}`
  },
  {
    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;
  });
});
`
  },
  {
    filename: path.join('../../packages/theme-chalk/src', `${componentname}.scss`),
    content: `@import "mixins/mixins";
@import "common/var";

@include b(${componentname}) {
}`
  },
  {
    filename: path.join('../../types', `${componentname}.d.ts`),
    content: `import { ElementUIComponent } from './component'

/** ${ComponentName} Component */
export declare class El${ComponentName} extends ElementUIComponent {
}`
  }
];

// 添加到 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');

// 添加到 element-ui.d.ts
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
Files.forEach(file => {
  fileSave(path.join(PackagePath, file.filename))
    .write(file.content, 'utf8')
    .end('\n');
});

// 添加到 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!');

解析:

process.argv 返回一个数组,数组的第一个参数是node.exe ,第二个参数是正在执行的js文件,其他参数是命令行输入的参数

 node test.js haha lala

执行上面的命令,则process.argv的结果是

[  'node.exe',  'test.js',       'haha',  'lala']

new.js 的作用就是通过 命令行指定组件名,自动化创建文件及其内容。

node build/bin/new.js  <英文组件名> <中文组件名>

通过fs模块加载原文件内容,然后通过字符串拼接得到新内容,然后fileSave方法将内容写入,达到自动化配置组件的效果,如下

// 添加到 element-ui.d.ts
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');

file-save

var fs = require('fs'),
  path = require('path'),
  mkdirp = require('mkdirp'),
  _savefs = savefs,
  savefs = {};

module.exports = function(file, opts) {

  dir_name= path.dirname(file)
  // origin file path
  ori_path = path.resolve(file);
  // folder path
  dir_path = path.resolve(dir_name);

  savefs._create_dir(dir_path, opts)
  //返回一个输入流对象
  return savefs.wstream(ori_path)
}

savefs._create_dir = function (fp, opts) {
    //创建多级文件夹
  mkdirp.sync(fp, opts);
}

savefs.wstream = function (file) {
  var ws = fs.createWriteStream(file);
  this.writer = ws;
    //支持链式调用,返回当前实例,并给实例添加writer属性,其内容是一个fs模块的输入流对象
  return this;
}

// chaining write method 
savefs.write = function(chunk, encoding, cb) {
  if(arguments.length === 3) {
    this.writer.write(chunk, encoding, cb);
  }else if(arguments.length === 2 && typeof arguments[1] === 'function') {
    cb = encoding;
    encoding = null;
    this.writer.write(chunk, cb);
  }else if(arguments.length === 2 && typeof arguments[1] !== 'function') {
    this.writer.write(chunk, encoding)
  }else {
    this.writer.write(chunk)
  }

  return this;
}

// chaining end method
savefs.end = function(chunk, encoding, cb) {
  if(arguments.length === 3) {
    this.writer.end(chunk, encoding, cb);
  }else if(arguments.length === 2 && typeof arguments[1] === 'function') {
    cb = encoding;
    encoding = null;
    this.writer.end(chunk, cb);
  }else if(arguments.length === 2 && typeof arguments[1] !== 'function') {
    this.writer.end(chunk, encoding)
  }else {
    this.writer.end(chunk)
  }

  return this;
}

savefs.finish = function(cb) {
  this.writer.on('finish', cb);
}

savefs.error = function(cb) {
  this.writer.on('error', cb);
}

总结:要活用node,node能做的事情特别多,如果在做一些重复的工作,可以考虑用node来写个自动化脚本,一键生成一些文件及其内容。