likes
comments
collection
share

命令行翻译工具

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

github地址

npm包地址

(温馨提示:注意保持package.json中版本一致,复制packeage.json,yarn install)

使用百度翻译工具API

使用Commander.js自带的功能

创建一个空目录,叫teanslate-reagen(webstorm快捷键:ALT+1打开关闭项目目录;Alt + 2打开或关闭命令行)

yarn init -y 把它初始化为一个npm包,package.json中版本号改为0.0.1

tsc --init 生成tsconfig.json配置文件,否则会报错

创建src目录放源代码,并在下面创建main.ts(console.log('hi'))

ts-node-dev src/main.ts 控制台输出hi,说明TS代码是可以执行的

如果不想每次都写ts-node-dev,可以在package.json中配置

{
  "name": "translate-reagen",
  "version": "0.0.1",
  "main": "index.js",
  "scripts": {
    "start": "ts-node-dev src/main.ts"
  },
  "license": "MIT"
}

直接运行yarn start即可

既然要做命令行就要使用commander.jsyarn add commander

既然做node的开发,就要安装node的类型声明文件yarn add --dev @types/node

命令行翻译工具

这样在webstorm中使用node写代码的时候就会有条提示,且不会划线报错了

当看到typings/index.ts 说明Commander自带了TS的声明文件,因此就不用安装额外的东西了

命令行翻译工具

创建 src/cli.ts 用来控制命令行(ts其实就是js,只不过多了类型等其他API)

Commander.js v3.0.2的文档

接下来就是CRM学习法,先拷贝Commander.js官方文档中的代码 const program = new commander.Command(); Webstorm 会自动引入响应模块

声明一下版本

import commander from "commander";

const program = new commander.Command();

program.version('0.1.0')


// parse就是对参数进行解析
program.parse(process.argv);

运行ts-node-dev src/cli.ts -h

命令行翻译工具

尝试修改下显示的cli.ts

program.version('0.1.0')
    .name('fy-bd')

命令行翻译工具

尝试修改下 [options],添加『用法』,"<>"表示必传的参数,“[]”表示可传的参数

program.version('0.1.0')
    .name('fy-bd')
    .usage('<a word>')

命令行翻译工具

我们可以获取到当前命令行工具的版本号

命令行翻译工具

因此,Commander.js是一个声明式写法,这样我们就写好了命令行工具的版本、名称及参数

添加我们自己的功能

获取用户传的参数(单词),然后去查这个单词的意思

// cli.ts
import commander from "commander";
import {translate} from "./main";

const program = new commander.Command();

program.version('0.1.0')
    .name('fy-bd')
    .usage('<a Word>')
    .arguments('<Word>')
    .action(function (word) {
        translate(word)
    });


// parse就是对参数进行解析
program.parse(process.argv);

命令行翻译工具

translate函数哪里来的?在main.ts(主要功能实现)中写的

// main.ts
export const translate = (word:any) => {
    console.log('word')
    console.log(word)
}

命令行翻译工具

这样我们就拿到了用户输入的单词

但是我们怎么把单词翻译出来?

接下来我们就要找一个免费翻译接口(推荐使用百度和有道的,国外的容易被墙)

命令行翻译工具

如何使用呢?

拼接完整请求:

api.fanyi.baidu.com/api/trans/v…

<======输出举例 =====>

正确情况

{
    "from": "en",
    "to": "zh",
    "trans_result": [
        {
            "src": "apple",
            "dst": "苹果"
        }
    ]
}

异常情况

{
    "error_code": "54001",
    "error_msg": "Invalid Sign"
}

备注

  1. 修改appid为自己的百度翻译开放平台appid
  2. sign就是要传自己的秘钥,当然不能直接传,需要appid+q+salt+密钥的MD5值,即要这四个值的MD5值

命令行翻译工具

  1. 搞一个MD5生成器,Google: md5 Hash Generator

将appid、q(这里为apple)、salt就是个随机数这个随便(1435660288)、密钥放到md5自动生成器中,将得到的那一串东西,放到sign中

命令行翻译工具

这样就在浏览器中实现了翻译效果了!!

但是我们怎么通过Node.js实现翻译效果呢?

我们需要借助Node.js中的https中的https.request()来实现在Node.js发请求,之前是用Node.js响应

使用Node.js调用百度翻译API

打开https.request文档

Node.js中构造查询参数

复制可运行的代码

import * as https from "https";

export const translate = (word:any) => {
    const options = {
        hostname: 'www.baidu,com',
        port: 443,
        path: '/',
        method: 'GET'
    };

    const req = https.request(options, (res) => {
        console.log('statusCode:', res.statusCode);
        console.log('headers:', res.headers);

        res.on('data', (d) => {
            process.stdout.write(d);
        });
    });

    req.on('error', (e) => {
        console.error(e);
    });
    req.end();
}

运行一下,我们就得到了百度首页的源代码

命令行翻译工具

那么接下来我们把访问地址改为百度翻译的地址(不要http),路径为/api/trans/vip/translate

那么查询参数(?...)我们怎么传?

我们需要借助Node.js中的querystring - 查询字符串

它有一个querystring.stringify方法

例子:

querystring.stringify({ foo: 'bar', baz: ['qux', 'quux'], corge: '' });
// 返回 'foo=bar&baz=qux&baz=quux&corge='

querystring.stringify({ foo: 'bar', baz: 'qux' }, ';', ':');
// 返回 'foo:bar;baz:qux'

我们不妨尝试console.log(querystring.stringify({name: 'LG', sign: '1234'}));

命令行翻译工具

这样我们就构造出了我们想要的查询参数了

Node.js中构MD5

const query: string = querystring.stringify({
    q: word,
    from: 'en',
    to: 'zh',
    appid: '???',
    salt: Math.random(),
    sign: '???'
})
  • 类似appid和密钥不能直接写,因为代码要上传到github
  • sign是MD5算出来的,安装下md5yarn add md5,因为我们的文件是ts,因此要看下md5包文件夹下是否自带了ts相关的文件,我们发现没有ts文件,说明这个库不支持ts,如果在ts中手动引入md5,发现webstorm根本没提示,不知道它有哪些方法属性。那怎么办呢,借助前端社区力量,有些人帮MD5写了TS的类型声明文件,我们安装吧yarm add --dev @types/md5,一般类型声明文件都使用--dev,因为只有开发者使用
  • 什么叫完美兼容TS,在webstorm中有相关API的用法,也有相关模块的ts的类型声明文件xxx.d.ts

命令行翻译工具

命令行翻译工具

这样我们的Node.js中也有MD5了

这时候Node.js中的sign只要像在浏览器中那样再实现一次就好了

import * as https from "https";
import * as querystring from "querystring";
import md5 from "md5";

export const translate = (word: any) => {
    const appid = '???'
    const appSecret = '???'
    const salt = Math.random()
    const sign = md5(appid + word + salt + appSecret)
    const query: string = querystring.stringify({
        q: word,
        from: 'en',
        to: 'zh',
        appid,
        salt,
        sign,
    })
    const options = {
        hostname: 'api.fanyi.baidu.com',
        port: 443,
        path: '/api/trans/vip/translate' + query,
        method: 'GET'
    };

    const req = https.request(options, (res) => {
        res.on('data', (d) => {
            process.stdout.write(d);
        });
    });

    req.on('error', (e) => {
        console.error(e);
    });
    req.end();
};

运行一下:

命令行翻译工具

由于我们的appid和秘钥还没上传,所以暂时会返回报错信息

处理appid和秘钥 & 使用TypeScript声明BaiduResult

如何不让appid和密钥不被泄露

为了使得appid和密钥不被泄露,我们在src目录下新建private.ts,然后再在根目录下新建一个.gitignore文件,在里面添加private.ts,这样在提交文件的时候就不会提交了

虽然在git中没有上传,但是打包之后appid和密钥还是在代码里,实在是没办法纯前端实现不暴露(除非将加密的过程放到服务器中)

tips: 如果发现yarn 安装的命令,运行命令的时候提示找不到命令,就运行yarn global bin的,把得到的路径配置到path中

接着运行代码,我们把单词apple传给百度翻译,返回给我们的不是error_code了,翻译的结果就放在dst中,它就是apple的中文的utf-8的形式

命令行翻译工具

命令行翻译工具

得到response的消息体

之前在做静态服务器的时候,获取post的消息体是通过监听data方法,只要有data将它放到数组中即可,最后监听它的end方法

同理,这里获取response的消息体也是一样

const request = https.request(options, (response) => {
    let chunks: any = []
    // data是下载的翻译结果数据
    response.on('data', (chunk) => {
        chunks.push(chunk);
    });
    // end表示下载完了
    response.on('end', () => {
    const string = Buffer.concat(chunks).toString()
    console.log(string)
    const object = JSON.parse(string);
    console.log(object)
    })
});

这样就得到了纯粹的JSON字符串,再把它变成JS对象,再对它进行JS的后续操作

命令行翻译工具

什么是Buffer(缓冲区)

缓冲区

缓冲区是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。 缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区。

为什么要引入缓冲区

比如我们从磁盘里取信息,我们先把读出的数据放在缓冲区,计算机再直接从缓冲区中取数据,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作大大快于对磁盘的操作,故应用缓冲区可大大提高计算机的运行速度。

那我们怎么知道得到的结果object有哪些属性呢?这是TS非常擅长的事情,通过下面的结果中的属性结合百度翻译文档中的说明,知道了这个对象拥有了哪些属性

命令行翻译工具

命令行翻译工具

注意:不要随便用简写,能用全写就全写,trans英文中有跨性别意思,即从一种性别转换到另一种性别

那么我们就用TS声明一个类型

const request = https.request(options, (response) => {
    let chunks: any = []
    // data是下载的翻译结果数据
    response.on('data', (chunk) => {
        chunks.push(chunk);
    });
    // end表示下载完了
    response.on('end', () => {
        const string = Buffer.concat(chunks).toString()
        type BaiduResult = {
            error_code?: string;
            error_msg?: string;
            from: string;
            to: string;
            trans_result:{
                src: string;
                dst: string;
            }[]
        }
        const object:BaiduResult = JSON.parse(string);
        console.log(object)
    })
});

备注:其中

  • error_code?表示这个属性在这个对象中可能有,也可能没有,这是TS的一个语法
  • trans_result:{ src: string; dst: string; }[] 表示trans_result它是一个数组,数组中的每一项是一个对象

为什么要声明类型呢?

如果不看文档,我们不知道对象object有这些属性,只有通过看文档才知道,现在呢?现在相当于把文档写在代码里!文档变了,我们只要改声明就行了

这样我们在写object的时候就有完全不一样的感觉了,如果不写类型声明,我们想用object下的属性object.,点什么呢,完全没提示,是不是又要去看文档?但是给object加上类型声明后object:BaiduResult

命令行翻译工具

这样写代码就爽多了,不用频繁的在文档和代码之间来回的切换!!!

response.on('end', () => {
    const string = Buffer.concat(chunks).toString()
    type BaiduResult = {
        error_code?: string;
        error_msg?: string;
        from: string;
        to: string;
        trans_result:{
            src: string;
            dst: string;
        }[]
    }
    const object:BaiduResult = JSON.parse(string);
    console.log(object.trans_result[0].dst);
})

命令行翻译工具

处理报错

命令行翻译工具

if(object.error_code){
    if(object.error_msg === '52003'){
        console.log('用户认证失败')
    }else{
        console.error(object.error_msg)
    }
    // 退出进程
    process.exit(2)
}else{
    // 获取到翻译的结果
    console.log(object.trans_result[0].dst);
    // 0表示没有错误
    process.exit(0)
}

备注

process.exit([code])

process.exit() 方法以结束状态码 code 指示Node.js同步终止进程。 如果 code 未提供,此exit方法要么使用'success' 状态码 0 ,要么使用 process.exitCode 属性值,前提是此属性已被设置。

表驱动编程(map)消除多余的if

当项目逻辑越来越多,可能会出现下面这样弱鸡的代码:

if (object.error_msg === '52003') {
    console.log('用户认证失败')
} else if (object.error_msg === '52004') {
    console.log('...')
} else if (object.error_msg === '52005') {
    console.log('...')
} else if (object.error_msg === '52006') {
    console.log('...')
} else {
    console.error(object.error_msg)
}

如何消除这样弱鸡的代码呢?

在最外层写一个map

const errorMap:any = {
    52003: '用户认证失败',
    52004: 'error2',
    52005: 'error3',
    52006: 'error4',
    other: '服务器繁忙'
}
//object.err_code为errorMap中的key值

if (object.error_code) {
    console.log(errorMap[object.error_code] || object.error_msg)
    // 退出进程
    process.exit(2)
} else {
    // 获取到翻译的结果
    console.log(object.trans_result[0].dst);
    // 0表示没有错误
    process.exit(0)
}

如果代码中有很多的if ... else ... ,那么一定是代码有问题,如果逻辑复杂,可以声明多个表,即多个map,表和表之间也可以关联

英译中 & 中译英

我们怎么知道用户是想英译中?还是想中译英?

把单词的第一个字母看一下呗~,借助一下正则中test

if (/[a-zA-Z]/.test(word[0])) {
    // 英译中
    from = 'en';
    to = 'zh';
} else {
    // 中译英
    from = 'zh';
    to = 'en';
}

命令行翻译工具

如何发布TypeScript包到npm

将ts文件编译成对一个的js文件

我们要在package.json中做一些配置,声明我们翻译的命令叫czd(查字典)

注意:czd对应的不是"src/cli.js"

"bin": {
  "czd": "src/cli.js"
},

因为这个文件是不能被Node执行的,它只能被ts-node-dev执行

那怎么办?

我们得要把代码进行编译,要把ts文件编译成js

怎么编译呢?

在全局安装了typescript之后,就会得到一个tsc的命令

如果没有初始化,需要使用tsc --init 初始化一下,就会得到一个tsconfig.json的文件

我们要把我们编译后的代码放到dist目录,因此要修改tsconfig.json中的outdir

  • 编译:把开发的程序源码编译成可执行文件。
  • 打包:将你开发后的可执行文件和必要的文档(如使用说明等)使用打包工具(如InstallShield)制作成软件包,就是我们通常用来安装软件的那个东西。

接着使用tsc -p . ,意思是把当前目录作为项目来编译

处理TS中的报错

如果命令行中报错,那有可能是因为ts并不知道一些变量的类型是什么,我们得告诉它

如: let chunks: Buffer[] = [] 意思是chunks是Buffer类型的数组,如果我们不知道chunks的类型就,console.log(chunks.constructor),如果此时报错,执行不了console.log()那么就删除掉tsconfig.json再运行一次,直到搞清楚chunks的类型为止,实在不行就标记为any(如果你用any,还用什么TS,干嘛不用JS)

TS的理念就是一开始你就要把什么都搞清楚(变量的类型)再写代码,什么都不清楚就不要写代码,还是老老实实用JS吧

TS和Node不是完美兼容的,为什么这么说呢?既然chunks的类型是Buffer类型的数组,那么chunk肯定是Buffer,怎么能是any呢?这点我们可以从Node 中 on的定义可以看到(ctrl+打击就可以跳转):

let chunks: Buffer[] = []
// data是下载的翻译结果数据
response.on('data', (chunk) => {
    chunks.push(chunk);
});

命令行翻译工具

这是因为Node偷懒了,它不想搞清楚chunk是什么类型,就写了any

一个TS的报错逻辑:

interface ErrorMap {
    [key: string]: string
}

const errorMap: ErrorMap = {
    52000: '成功',
    52001: '请求超时,请重试',
    52002: '系统错误,请重试',
    52003: '未授权用户,请检查appid是否正确或服务是否开通',
    54000: '必填参数为空,请检查是否少传参数 ',
    54001: '签名错误,请检查您的签名生成方法 ',
    54003: '访问频率受限,请降低您的调用频率,或进行身份认证后切换为高级版/尊享版 ',
    54004: '账户余额不足,请前往管理控制台为账户充值  ',
    54005: '长query请求频繁,请降低长query的发送频率,3s后再试 ',
    58000: '客户端IP非法,检查个人资料里填写的IP地址是否正确,可前往开发者信息-基本信息修改',
    58001: '译文语言方向不支持,检查译文语言是否在语言列表里',
    58002: '服务当前已关闭,请前往管理控制台开启服务 ',
    90107: '认证未通过或未生效,请前往我的认证查看认证进度 '
}

errorMap[object.error_code]

如果上面不声明errorMap的类型就会报错,报错的原因是,errorMap中的只有13个,你现在把object.error_code作为key,如果它不在13个key里面怎么办

因此我们需要告诉TS,它的key可以是任意的字符串,值也可以是任意的字符串

修改.gitginore && 添加shebang

tsc -p. 编译完成后我们发现,src下的ts文件都被编译成了对应的js文件,dist文件夹中的文件是不用上传到github中的,因为它是编译后的文件,因此要修改.gitignore文件

src/private.ts
/dist
/node_modules

为了指定cli.js只能使用Node执行,因此我们需要在cli.ts中加上shebang(cli.ts源代码是不需要shenbang的)

然后再使用tsc 编译.ts扩展名的文件

准备发布

1. 指定要上传的文件

修改package.json我们只需要上传dist目录下的所有js文件即可

"files": [
  "dist/**/*.js"
],

2. 登录npm

注意:要把npm中使用的taobao切换为npm官方源

命令行翻译工具

使用npm adduser登录npm,输入用户名、密码、邮箱和一次性密码

3. 发布

使用npm publish 发布

如果修改了代码,一定要修改版本号重新npm publish,同一个版本不能使用不同的代码,否则发布不出去

在根目录下添加README.md,告诉别人如何使用你这个工具

使用下面的方法来更新版本也是可以的:

npm version patch
npm publish

最终效果

命令行翻译工具

逐步完善功能

后续我们应该完善我们的翻译工具,对比优秀的翻译工具,发现不足和好的功能,逐步添加功能

比如npm中一个好用的翻译工具,额外还多了例句和发音

安装方法

yarn global add fanyi

给命令一个别名alias

如果不想分享出去,只想自己用,那么我们可以给命令添加一个alias

vi ~/.bashrc 或者打开这个文件进行编辑

alias fy='ts-node-dev /f/LG/node-snippet/translate-reagen/src/cli.ts'

注意绝对路径的格式

要使用绝对路径,如下图复制一下

命令行翻译工具

运行source ~/.bashrc

再使用czd hello 就相当于ts-node-dev src/cli.ts hello

命令行翻译工具

gitignore语法规范

package.json中的files的配置也是遵循这个规则

语法规范

  • 空行或是以#开头的行即注释行将被忽略。
  • 可以在前面添加正斜杠/来避免递归,下面的例子中可以很明白的看出来与下一条的区别。
  • 可以在后面添加正斜杠/来忽略文件夹,例如build/即忽略build文件夹。
  • 可以使用!来否定忽略,即比如在前面用了*.apk,然后使用!a.apk,则这个a.apk不会被忽略。
  • *用来匹配零个或多个字符,如*.[oa]忽略所有以".o"或".a"结尾,*~忽略所有以~结尾的文件(这种文件通常被许多编辑器标记为临时文件);[]用来匹配括号内的任一字符,如[abc],也可以在括号内加连接符,如[0-9]匹配0至9的数;?用来匹配单个字符。
# 忽略 .a 文件
*.a
# 但否定忽略 lib.a, 尽管已经在前面忽略了 .a 文件
!lib.a
# 仅在当前目录下忽略 TODO 文件, 但不包括子目录下的 subdir/TODO
/TODO
# 忽略 build/ 文件夹下的所有文件
build/
# 忽略 doc/notes.txt, 不包括 doc/server/arch.txt
doc/*.txt
# 忽略所有的 .pdf 文件 在 doc/ directory 下的
doc/**/*.pdf

备注

  1. 如果不使用还是最好将百度翻译api的服务功能停掉,否则如果由于账号的泄漏可能会导致付费

命令行翻译工具

  1. 为什么代码中用module.export.x = ...导出,使用require导入呢?

这是历史遗留的问题

✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘

  • 最早是Node.js那波人,搞了一个CommonJs,并没有被写入规范
module.export.x = ...
const api = require('...')

api.x()
  • 另外一波人不服,因为这种方式只能同步的引入,所以另外一波人又搞了一个require.js学名叫AMD的模块导入导出的方法

✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘✘

  • 不用管上面两种模块导入导出的方法,它们都是垃圾,因为标准里写了一个标准的方法
import 

export const x = ...   

默认都是使用标准的方法导入和导出,那为什么现在的代码中还有require呢?因为以前写的垃圾代码还没死,垃圾还在,比如:MD5的代码,还是用moudle.export来导出,并没有对代码进行升级,它没升级,那我们只能使用旧的方法去导入导出,总不能用新版的导入导出去导入导出旧版的代码吧

这点我们不用过于纠结,能用标准的导入导出就用标准的导入导出。或者我们直接使用模块就好了,其他的交给webstorm,它会帮我们使用不同的方法引入相应模块

  1. 项目中使用的TypeScript,因为我们是通过ts-node-dev来执行项目中的代码,只不过大部分和JS是相似的,如import * as xxx from yyy,TypeScript推荐这样写,由于Node.js使用的还是主流的CommonJS的导入导出方法,所以在使用TypeScript打包编译后的文件中,导入导出变成了exports.translate = function(){}

那Node.js能不能也支持import * as xxx from yyy这种写法呢?

搜索node esmodule 开启(Node9以上,兼容性差),乱七八糟的要求贼多,还不如直接使用TS