api同步工具

我们在项目开发的过程中,想要提升开发效率,要从各个方面入手,比如同步api接口。

目前服务端有很多比较流行的接口文档管理工具,比如swagger。本次api同步工具,以swagger为文档管理为基础。

首先以vswagger-cli为例,看下这个工具的代码结构。

一、分析依赖包的功能

1、axios:是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。

2、chalk:是一个颜色的插件。

3、commander:是一个轻巧的nodejs模块,提供了用户命令行输入和参数解析强大功能。commander源自一个同名的Ruby项目。

可进行对外命令扩展 即用其配置node命令

  • option(): 初始化自定义参数对象,设置“关键字”和“描述”。通过option设置的选项可以通过program.chdir或者program.noTests来访问。
  • command(): 初始化命令行参数对象,直接获得命令行输入。通过command设置的命令通常在action回调中处理逻辑。
  • Command.command(): 定义一个命令名字
  • Command.action(): 注册一个callback函数
  • Command.option(): 定义参数,需要设置“关键字”和“描述”,关键字包括“简写”和“全写”两部分,以”,”,” ”,”空格”做分隔。
  • Command.parse(): 解析命令行参数argv
  • Command.description(): 设置description值
  • Command.usage(): 设置usage值

4、download-git-repo: 下载并解压缩git存储库。

5、lodash: 是一个一致性、模块化、高性能的 JavaScript 实用工具库。

lodash的几个优点:

  • lodash 通过降低 array、number、objects、string 等等的使用难度从而让 JavaScript 变得更简单。
  • lodash 的模块化方法 非常适用于:

  • 遍历 array、object 和 string
  • 对值进行操作和检测
  • 创建符合功能的函数

6、metalsmith: 一个非常简单,可插拔的静态网站生成器,

7、ora: 主要用来实现node.js命令行环境的loading效果,和显示各种状态的图标等

8、rimraf :以包的形式包装rm -rf命令,就是用来删除文件和文件夹的,不管文件夹是否为空,都可以删除。

9、semver:它是 语义化版本(Semantic Versioning)规范 的一个实现,目前是由 npm 的团队维护的,实现了版本和版本范围的解析、计算、比较,在 NPM 的被依赖(Most depended-upon)榜单中排名 34.

10、tildify:把绝对路径转化成波浪路径, 如 /Users/sindresorhus/dev → ~/dev

11、user-home:获取用户主目录的路径

二、源码解析

1、vswagger

1
2
3
4
5
6
7
8
#!/usr/bin/env node
require('commander')
    .version(require('../package').version)
    .usage('<command> [options]')
    .command('init', 'generate a new API from a template')
    .command('clean', 'clean it!')
    .command('check', 'check code!')
    .parse(process.argv)

这里是扩展一些外部命令:vswagger init,vswagger clean,vswagger check,供项目开发者使用。

2、vswagger-init

思路是先找到.vswagger.js文件 如果有的话,获取里面的配置,遍历其projects数组,根据配置中的api-doc路径获取api接口数据,然后一一生成function到项目中。

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
    #!/usr/bin/env node
    var isLocalPath = localPath.isLocalPath;
    var getTemplatePath = localPath.getTemplatePath;
    
    program
      .usage('[project-path]')//定义usage值 添加要更新的src/api下的路径名和文件名
      .usage('[project-name]')
      .option('-c, --clone', '使用 git clone')//给vswagger init命令添加参数
      .option('--offline', '使用缓存模板')
      .parse(process.argv);
    
    run();
    var projectPath;
    function run() {
        var fileName = '.vswagger.js';//.vswagger.js为项目中添加用来生成api的配置文件
        //process.cwd() 方法返回 Node.js 进程的当前工作目录
        //path.resolve() 方法将路径或路径片段的序列解析为绝对路径
        projectPath = program.args[0] ? path.resolve(process.cwd(), program.args[0]) : process.cwd();
        //path.join() 方法使用平台特定的分隔符作为定界符将所有给定的 path 片段连接在一起,然后规范化生成的路径
        var configPath = path.join(projectPath, fileName);
        //判断.vswagger.js文件是否存在 如果路径存在,则返回 true,否则返回 false
        if (!fs.existsSync(configPath)) {
            //不存在则提示找不到配置文件
            logger.fatal('找不到 '+ fileName +' 配置文件');
        }
    
        var config = require(configPath);
        var projectName = program.args[1];
        //扩充vswagger里面的baseType字段
        config['baseType'] = _.assign({
            'Timestamp': '',
            'string': '',
            'boolean': false,
            'integer': '',
            'object': {},
            'JSONObject': {},
            'number': '',
            'array': []
        }, config.baseType);
    
        if (projectName) {//如果给commander设置了project-name
            //遍历.vswagger.js中的projects 筛选出符合条件的结果
            config.projects = _.filter(config.projects, item => {
                if (projectName.split(',').indexOf(item.modelName) !== -1) {
                    return {
                        token: item.token || '',
                        modelName: item.modelName || '',
                        docUrl: item.docUrl || []
                    };
                }
            });
        }
    
        var template = config.template || 'vue-swagger-template';//template默认是vue-swagger-template
        var hasSlash = template.indexOf('/') > -1;
    
        var tmp = path.join(home, '.vue-swagger', template.replace(/\//g, '-'));
    
        if (program.offline) {//vswagger init --offline 判断是否是离线下载 是的话应用缓存模版
            logger.log('使用缓存模板 %s', chalk.yellow(tildify(tmp)));
            template = tmp;
        }
    
        // 判断template是否是本地的
        if (isLocalPath(template)) {
            var templatePath = getTemplatePath(template, projectPath);
            //判断templatePath是否存在,存在的话执行generate方法
            if (fs.existsSync(templatePath)) {//判断templatePath路径是否存在
                //存在 则调用生成接口
                generate(projectPath, templatePath, config, generateDone);
            } else {
                logger.fatal('找不到本地模板 "%s".', template);
            }
        } else {
            //检测版本
            checkVersion(() => {
                if (!hasSlash) {
                    // 使用离线模版
                    var officialTemplate = 'Git-leng/' + template;
                    downloadAndGenerate(officialTemplate, tmp, projectPath, config);
                } else {
                    downloadAndGenerate(template, tmp, projectPath, config);
                }
            });
        }
    }
    
    function downloadAndGenerate (template, tmp, projectPath, config) {
        var spinner = ora('正在下载模板.');
        spinner.start();
        // 删除本地已有的api文件
        if (fs.existsSync(tmp)) rimraf.sync(tmp);
        download(template, tmp, {
            clone: program.clone || false
        }, function (err) {
            spinner.stop();
            if (err) logger.fatal('模板下载失败 ' + template + ': ' + err.message.trim());
            generate(projectPath, tmp, config, generateDone);
        });
    }
    //生成文档的回调
    function generateDone (error, files) {
        if (error) logger.fatal(error);
        _.forEach(files, (file) => {
            logger.success(chalk.green('%s'), '更新成功   ', `${path.relative(projectPath, tildify(file))}`);
        });
    }

3、generate.js

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
var config;
var projectPath;
var templatePath;

var docName = 'docUrl';
var projectName = 'modelName'

function init(data, project, cb) {
    var helperPath = path.resolve(templatePath, '../helper/index.js')
    var helper = fs.existsSync(helperPath) ? require(helperPath) : {}

    var dest = path.join(config.output, project[projectName]);
    // 使用vue环境变量的名称
    const moduleName = project[projectName].toUpperCase()

    var data = {
        data: {list: data, project: project, moduleName},
        config: config,
        _: _,
        $$: Object.assign({}, helper, {
            relative: function (targetFile) {
                var relative = path.relative(dest, config.output)
                return path.posix.join(relative, targetFile)
            }
        })
    }

    build(data, 'cover', dest, cb)
    if (fs.existsSync(path.join(templatePath, 'init'))) build(data, 'init', dest, cb, true)

    if (fs.existsSync(path.join(templatePath, 'common'))) {
        build({
            config: config,
            _: _,
            $$: helper
        }, 'common', config.output, cb, true)
    }
}

function build(data, source, dest, cb, ignore) {
    var metalsmith = Metalsmith(templatePath)
        .use(renderTemplateFiles(data))
        .clean(false)
        .source(source)
        .destination(dest)

    if (ignore) {
        metalsmith.ignore(filePath => {
            filePath = filePath.replace(path.join(templatePath, source), '')
            filePath = path.join(dest, filePath)
            return fs.existsSync(filePath)
        })
    }

    return metalsmith.build((error, files) => {
        if (error) logger.fatal(error)
        var f = Object.keys(files)
            .filter(o => fs.existsSync(path.join(dest, o)))
            .map(o => path.join(dest, o))

        // 搜车模板,临时可用
        if (config.generateType === 'souche' && source === 'cover') {
            // 将生成出来的文件,转译成对应的文件即可
            Utils.transform(dest);
        } else {
            cb(error, f)
        }

    })
}

function renderTemplateFiles(data) {
    return function (files) {
        Object.keys(files).forEach((fileName) => {
            var file = files[fileName]
            file.contents = _.unescape(_.template(file.contents, {
                interpolate: /\{\{(.+?)\}\}/g
            })(data))
        })
    }
}

//这一步 从config.projects中获取api数据
function getData(project, cb) {
    var arr = [];
    var parentUrlRequest = [];
    //push一些请求action到parentUrlRequest里
    _.uniq(project[docName]).map(item => {
        parentUrlRequest.push(makeRequest(`${item}?group=souche&_t=${new Date().getTime()}`, project));
    });
    //执行parentUrlRequest里的所有请求
    axios.all(parentUrlRequest).then(result => {
        let path = result[0].request.path;
        let code = result[0].data.code || '';
        if (path.indexOf('login') !== -1 || code == 10001) {//token失效
            logger.log('更新失败   ', `${project[projectName]}模块,token已失效!`);
        }
        var urlsRequest = [];
        result.map((itemRes, i) => {
            var itemRes = itemRes.data;

            if (itemRes.apis && itemRes.apis.length) {
                itemRes.apis.map(item => {
                    urlsRequest.push(makeRequest(project[docName][i] + item.path, project));
                });
            }
        });

        axios.all(urlsRequest).then(res => {
            res.map(item => {
                //apis 全部接口列表
                item.data.apis.map(o => {
                    let baseType = config.baseType;
                    // let type = o.operations[0].responseMessages[0].responseModel;
                    let type = o.operations[0].type;
                    let res = baseType[type] || '';
                    if (config.safe) {//如果要生成保护数据
                        if (item.data.models && type && item.data.models[type]) {
                            res = item.data.models[type] || {};
                            res = parseRes(res ? res.properties : {}, item.data);
                        }
                    }
                    //定义response数据结构 如{code:xx,success:xx,traceId:xx,msg:xx,data:xx}
                    o.operations[0].responseMessages[0].responseModel = JSON.stringify(res);
                    arr.push({
                        path: o.path,
                        ...o.operations[0]
                    });
                });
            });

            if (arr.length) {//若有接口更新
                //uniqBy去重结果
                init(_.uniqBy(arr, 'path'), project, cb);
            }
        });
    });
}

module.exports = function (_projectPath, _templatePath, _config, cb) {
    config = _config;
    projectPath = _projectPath;

    templatePath = path.join(_templatePath, 'template');
    config.output = path.resolve(projectPath, config.output || 'vswagger-api');

    if (_.isEmpty(config.projects)) return;
    if (!_.isArray(config.projects)) logger.fatal('请正确配置项目列表.');

    var projects = config.projects//遍历projects 分别执行getData方法获取api文档中的接口数据
        .filter(o => _.has(o, docName) && _.has(o, projectName) && !_.isEmpty(o[docName]))
        .map(project => getData(project, cb));

    if (projects.length !== config.projects.length) logger.fatal('projects,缺少字段,请正确配置项目列表.');
}

4、使用

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
    template: 'souche-xxxxx#v2', // 可为空使用默认接口生成模板
    safe: false, // 是否生成保护数据
    output: "src/api", // 输出到api目录
    projectDir: "src", // 代码存放目录(可不配置默认为src路径)
    projects: [{
        domain: 'xx',  // 环境变量
        modelName: "dforce", // 模块化名称
        docUrl: ['http://xx/api-docs'],  // swagger base-url
        token: 'xxxxxxx'
    }] // 项目配置
};

一、发布npm包

这一步我们要弄清楚如何发布npm包,

1、先执行npm adduser 输入账号 密码和邮箱(注意此步骤一定要验证邮箱)

这是我运行之后报的错:

1
2
3
4
5
    npm ERR! code E401
    npm ERR! Registry returned 401 for PUT on http://registry.npm.xxs.com/-/user/org.couchdb.user:ty0225: unauthorized
    
    npm ERR! A complete log of this run can be found in:
    npm ERR!     /Users/tahara/.npm/_logs/2019-07-01T10_17_22_157Z-debug.log

是因为之前设置了某个源 现在要把源设置为npm官方的源

1
2
npm config get registry 查看源
npm config set registry https://registry.npmjs.org 修改源

然后执行npm publish

成功后在别的项目中执行npm install npmname 即可使用

更新npm包也适用npm publish 不过要注意修改package.json的版本号,不然会报错

参考文献:

使用commander.js做一个Nodejs命令行程序

npm publish 发布