简化版angular-cli文件生成器

1,617 阅读9分钟
原文链接: github.com

环境搭建之文件生成器

前言

03 环境搭建之服务端 里我介绍过关于我的Nodejs目录结构说明, 最后选择的是an功能划分文件夹的约定:

src 
    app.js        服务器入口
    app.routes.js 服务器路由配置
    config        配置文件
    utils         工具库
    core          核心模块
    shared        共享模块
    users         用户模块
       users.controller.ts 用户控制器
       users.model.ts 用户数据库
       users.route.ts 用户路由
       users.filter.ts 过滤器
       users.services.ts 用户服务
       users.interface.ts 用户TypeScript接口申明

是这个样子,就有人问我了,每次新建一个业务模块就要,新建6个对于的类型文件,就算ctrl+cctrl+v,在F2重命名,需要6下,那6个文件里面的内容,又需要多久时间改好了,能不能做一个模板生成器,类似强大angular-cli一样,ng g component new-component就自动在当前目录生成一个new-component的component文件。

工善其事 必利其器

分析需求

  1. 需要一个可以传参的命令行 这个process.argv内置API,可以满足我们的需求。

  2. 生成文件需要有个约定,路径,文件名,类型

  • 路径:需要在那个文件夹里面生成 开发都在src/ 至少生成的文件路径要带上它
  • 文件名:文件名决定显示你是否一眼能看明白它是做什么的
  • 类型:类型决定我们生成什么样的模板内容,想要自定义定制高这个约定很重要
  1. 需要生成文件夹 我们需要mkdirp,这个包来解决问题,它可以无限级创建目录的库

  2. 需要一套模板替换 我发现了一个包,可以解决这个问题,tpl_apply

该有的我们都有了,那接下来开始约定规则。

我生成了3种类型模板

理想的模板生成器

模板类型 说明 用法 简写用法
Module 业务类型模板(自动深生成6个类型文件) node generate module users node generate m users
Service 单个服务生成 node generate service redis node generate s redis
Filter 单个过滤器生成 node generate filter passport node generate f passport

process.argv 属性返回一个数组,这个数组包含了启动Node.js进程时的命令行参数。第一个元素为process.execPath。如果需要获取argv[0]的值请参见 process.argv0。第二个元素为当前执行的JavaScript文件路径。剩余的元素为其他命令行参数。

node generate module users这个命令,如果我们通过process.argv获取结果就是['node.exe', 'generate.js', 'module', 'users']这样的一个数组。我们实际需要的是module users,那么简单js语法就是process.argv.slice(2) => ['module', 'users'];

这样我们返回一个2个参数的数组,如果第一个参数不是module|m|service|s|filter|f,那么就认为是一个非法的操作,用正则来判断。 第一个参数,默认是从src起,我们需要有2种写法,一种是直接在src文件夹下创建一个指定类型的文件,例如:node generate m users,这样返回就是:

src
       users         用户模块
       users.controller.ts 用户控制器
       users.model.ts 用户数据库
       users.route.ts 用户路由
       users.filter.ts 过滤器
       users.services.ts 用户服务
       users.interface.ts 用户TypeScript接口申明

一种是在某个文件夹下创建指定类型的文件,例如:node generate s core/redis,这样返回就是:

src
     core          核心模块
         redis.services.ts redis服务

开始干活,写代码

写代码之前一定要把我们想要做的和准备做的,预期要做的设想都大致想一下,为接下来写代码打基础。

注意:这不是cli,不能定制化太多,功能不是很强大,需要借助node命令行来实现。

选择一款你喜欢拉风的编辑器开始干活吧,骚年。这里用vscode。

在根目录先建一个generate.js文件。

等等,你肯定要说为什么,都是ts编写的应用,你要新建一个js文件,因为ts需要编译成js,才能使用node运行,虽然有node-ts,可以解决这个问题,我目前没有使用这个。这里generate.js文件,是命令行需要的,node generate。任何程序都是Hello world开始。

generate.js

console.log('Hello world');

cmd

 node generate
Hello world

完美。

先完成一个小目标

我们先完成node generate service redis这个生成。

generate.js

const argv = process.argv.slice(2);

console.log(argv);

cmd

 node generate service redis
[ 'service', 'redis' ]

来验证第一个参数

generate.js


 process.argv 返回一个数组
 [0] node.exe
 [1] 当前运行的文件
 [2...] 传递参数

const process_argv = process.argv.slice(2);


 类型文件
 业务模块 module|m
 服务 service|s
 过滤器 filter|f

const TYPE_REGULAR = /^(module|m|service|s|filter|f)$/;
console.log(TYPE_REGULAR.test(process_argv[0]));

cmd

node generate service redis
true

ok,通过测试,来个controller,我们没有打算自定义controller,这个肯定是假的。test一下。 cmd

node generate controller user
false

generate.js


 如果不是预期就直接抛出错误,强制逗比结束

if (!TYPE_REGULAR.test(process_argv[0])) {
    throw Error(`${process_argv[0]} is not module or service or filter`);
}

用判断来解决逗比的事情发现吧。

不走错误,我们就要思考一个问了, 因为有3种类型,那么我们需要些if else来判断或者switch。no,我不想用这个,我想有个高达上一点的解决方案,策略模式。它能完美解决这个问题。

为什么要用它,它的定义:我们可以根据环境或者条件的不同选择不同的算法或者策略来完成该功能。

因为我们定义了3种类型文件,他们实现方式有差别,通过类型来获取对于的结果。

这么高大上的设计模式怎么使用了,js很好实现。

这里也一样需要判断输入 generate.js


 支持几种写法
 xxx
 xxx/xxx
 xxx/xxx/xxx

const FILE_REGULAR = /^[\w]+[\w\/]+[\w]$/g;

 如果不是预期就直接抛出错误,强制逗比结束

if (!FILE_REGULAR.test(process_argv[1])) {
    throw Error(`${process_argv[1]} The file name does not match Or the file name length is less than 3`);
}

接下了就要定义一波常量备用

// 当前项目目录
const ROOT_PATH = __dirname;
// 默认开发目录
const DEV_PATH = 'src';

获取预期的参数备用

// 类型
const file_type = process_argv[0];
// 文件路径和文件名
const file_path = process_argv[1];

定义算法策略:


 策略算法

const typeStrategy = {
    'm': generateModule,
    'module': generateModule,
    's': generateService,
    'service': generateService,
    'f': generateFilter,
    'filter': generateFilter
};

定义各种对于的算法


 业务模块 module|m 生成策略
 @param {any} filePath 文件路径
 @param {any} fileName 文件名

function generateModule(filePath, fileName) {

}

 服务 service|s 生成策略
 @param {any} filePath 文件路径
 @param {any} fileName 文件名

function generateService(filePath, fileName) {

}

 过滤器 filter|f 生成策略
 @param {any} filePath 文件路径
 @param {any} fileName 文件名

function generateFilter(filePath, fileName) {

}

定义策略条件


 策略条件

function generateStrategy() {
   
     file_path 有2种策略
     1. user        => filePath = src fileName = user
     2. core/redis  => filePath = src/core fileName = redis
    
    typeStrategy[file_type](...getFilePathAndName());  // es6解构 [filepath, filename]
}
// 调用策略模式
generateStrategy();

这就js策略模式其中一种最简单的写法,不要怀疑,这是真的。我下次想在写一种controller算法需要,去添加对于的代码,typeStrategy增加一个隐射关系,创建一个generateController(filePath, fileName)算法处理函数。

我们还需要通过file_path生成我们想要的filePath, fileName功能算法处理器使用。

 获取文件路径和文件名
 @returns {array} [0] filePath [1] fileName;

function getFilePathAndName() {
    let _filename, _filepath;
    if (!/\//g.test(file_path)) {   // 如果里面没有/ 就表示是在src/ 新建一个文件 否者就是src/xxx 文件里新建文件
        _filename = file_path;
        _filepath = DEV_PATH;
    } else {
        const _paths = file_path.split('/');
        _filename = _paths.pop();
        _filepath = ['src'].concat(_paths).join('/');
    }
    _filepath = process.platform.startsWith('win') ?    // 系统不同,文件夹分割/或者\\
        _filepath.replace(/\//g, '\\') :
        _filepath;
    return [_filepath, _filename];   // 最后返回一个数组, 在调用时候就可以...getFilePathAndName()这样使用了,es6解构;
}

接下来就该创建文件了。 nodejs提供了fs的API来与本地文件交互。但是好像写创建写文件只能在已经有文件夹里面进行,如果没有这个文件夹就会报错。

我们平常用命令行创建文件夹,

mkdir user

mkdir user/core
命令语法不正确。(win不能使用)

mkdir user\core (win正确的姿势)

要我们去手动建文件夹,然后再去生成对应的文件那不是要我们去屎吗。刚刚一大堆代码不是白写了吗。

node有个模块,node-mkdirp就能解决我们这些问题。

我们需要安装 npm install mkdirp -D 引入包。

// mkdirp是无限级创建目录的库
const mkdirp = require('mkdirp');

用法:

mkdirp('/tmp/foo/bar/baz', function (err) {
    if (err) console.error(err)
    else console.log('pow!')
});

第一个参数是生成的路径,第二个参数是回调 失败就跑错。不失败就可以继续下一步了。

function generateService(filePath, fileName) {

    mkdirp(filePath, function(err) {
        if (err) console.error(err);

        console.log('generateService', filePath, fileName)
    });
}

文件夹里面已经生成src文件夹,因为wiki不能直接上传图片,就不一一截图了,用心感受。

既然有了文件夹,就可以写文件了。

const fs = require('fs');
// 写入数据, 文件不存在会自动创建
fs.writeFile(__dirname + '/test.js', JSON.stringify({
    status: 0,
    data: data
}), function (err) {
    if (err) throw err;
    console.log('写入完成');
});

fs.writeFile(filename,data,[options],callback); 创建并写入文件

 filename, 必选参数,文件名
 data, 写入的数据,可以字符或一个Buffer对象
 [options],flag 默认‘2’,mode(权限) 默认‘0o666’,encoding 默认‘utf8’
 callback  回调函数,回调函数只包含错误信息参数(err),在写入失败时返回。

还有用到一个内置包:

// 路径对象
const path = require('path');

它可以做很多事情,最好的磨平了系统的差异。

function generateService(filePath, fileName) {
    const createFile = `${fileName}.service.ts`;
    mkdirp(filePath, function(err) {
        if (err) console.error(err);
        fs.writeFile(path.join(ROOT_PATH, filePath, createFile), `${createFile} generate complete!`, function(err) {
            if (err) throw err;
            console.log(`${createFile} generate complete!`);
        });

    });
}

cmd

 node generate service redis
[ 'src', 'redis' ]
redis.service.ts generate complete!

文件夹里面有对应的文件了。可以创建我们想要的文件了,其实到这里就可以完结了。但是,单纯的创建还是不爽,因为我们代码很多都是类似模板,我们需要更进一步,完成需要。

使用模板完成一个大目标

在根目录创建一些模板留着备用

generateTemplate    
       Module    存放业务模块模板
          tpl.controller.ts 控制器
          tpl.model.ts 数据库
          tpl.route.ts 路由
          tpl.filter.ts 过滤器
          tpl.services.ts 服务
          tpl.interface.ts TypeScript接口申明
       Service   存放服务模板
          tpl.services.ts 服务
       Filter    存放过滤器模板
          tpl.filter.ts 过滤器

这里这里需要一个包来处理模板替换,tpl_apply 这是cnodejs 一位大牛写的。 安装:npm install -D tpl_apply;

// 使用tpl_apply根据模板文件和数据
tpl_apply.tpl_apply(source, data, dest);
source 模板文件
data 传入的数据
dest 生成的文件  fs.writeFile 就是它的干的活

我们改造一下

function generateService(filePath, fileName) {
    // 生成目标文件
    const createFile = `${fileName}.service.ts`;
    // 生成目标文件夹 
    const destDir = path.join(ROOT_PATH, filePath);
    // 获取对应的模板文件列表
    const tpls = fs.readdirSync(path.join(ROOT_PATH, "/generateTemplate/Service"));
    tpls.forEach(function(element, index) {
        // 文件名遵循 模板命名空间.type类型.文件后缀
        let type = element.split('.')[1];
        if (type !== 'service') {
            throw Error(`/generateTemplate/Service/${element} is not service type`);
        }
        // 传递参数
        const data = {
                name: fileName
            }
            // 获取对应的模板
        const source = path.join(ROOT_PATH, "/generateTemplate/Service", `tpl.${type}.ts`);
        // 生成的文件
        const dest = path.join(destDir, createFile);
        mkdirp(destDir, function(err) {
            if (err) console.error(err);
            // 使用tpl_apply根据模板文件和数据
            tpl_apply.tpl_apply(source, data, dest);
            console.log(`${createFile} generate complete!`);
        });
    });
}

这样改造就已经完成了。

我们改造一下模板 tpl.service.ts

  {{fileName}} 服务
  Created by {{author}} on {{createAt}}.

 定义{{fileName}}服务类接口

interface Interface{{fileName}} {
    test();
};


 {{fileName}}服务

class {{fileName}}Service implements Interface{{fileName}} {
    constructor() {
    }

    test() {
    }
}


 导出{{fileName}}服务模块

export default new {{fileName}}Service();

我们只需要替换对应的模板数据即可

// 获取package.json的author字段,如果没有就设一个默认的
const author = require("./package.json").author || 'jiayi';
// 创建时间 yyyy-MM-dd hh-mm-ss
const createAt = new Date().toLocaleString();
// 文件里名称显示大驼峰
fileName = bigCamelCaseFormat(filePath);
data = {
   fileName,
   filePath,
   author,
   createAt
}

处理文件里的命名方式


  大驼峰式命名法格式化
  @param  {string} str 
  @return {string} 

function bigCamelCaseFormat(str){
    return str.replace(/-(\w)/g, function(k, r) {
        return r.toUpperCase();
    }).replace(/^\S/,function(s){return s.toUpperCase();});
}

已经完美生成出文件

  Redis 服务
  Created by jiayi on 2017-9-18 14:21:59.
 


  定义Redis服务类接口

interface InterfaceRedis {
    test();
};


 Redis服务

class RedisService implements InterfaceRedis {
    constructor() {
    }

    test() {
    }
}


 导出Redis服务模块

export default new RedisService();

后面的代码就一样,粘贴复制的工作。

源代码: generate.js generateTemplate

这里实现angular-cli生成文件的第一步,比如后面还有参数,那是后话,这里的工具,已经基本够我们使用了,以后有需求在扩展。