和大多数的前端开发者相比,我并没有选择node.js做cli而投靠了golang。

14,309 阅读6分钟

业务原因

随着业务的不断增大,现在的系统已经堆积了23个业务大模块,每个模块的代码量都很多。如果放在3年前没有拆分的react的单体机构去构建那即将是一场噩梦,放在现在来构建的话。我估计没有50分钟你是无法回家的有个好梦的。

虽然现在已经实现了RcModule模块注入功能,构建的时候是解放了漫长的等待时间,但是迎面而来的是交付项目的时候需要在标品里面把多余的代码,还有base.config.json等一些核心代码去掉。还有客户没有给钱的代码也要自己去删除。

每次都在标品里面进行很多重复的工作,才能进行交付到客户的手中。比如paas服务是不交付给客户,我就要删审核代码去进行删减。但有一些客户只需要某些模块的一小部分功能,在进行交付的那段期间让我的工作变得无比复杂。

有时候需要c++工程师生成一个二进制文件,然后在build的时候通过c++的扩展在V8引擎里面进行解密把这个二进制内容进行打包的。这样就算客户拿到部分代码,也无法知道我们是怎么运行的。

在23个业务模块里面,如果有些客户只需要7个业务模块对我来说工作量还是勉强可以接受的。但是遇到某些大客户上来就要18个业务模块,还要SRMERP这些庞大的系统的时候,我要把多余的代码删单。为了保护标品的数据安全下交付到客户手中,这工作量就无疑是个巨坑。

为什么需要Cli?

这个时候,我需要的已经不是要类似vue-cliumijs这种单单创建项目(管理/构建)工具这么简单了。更多是需要类似@angular/cli这一类的强大cli工具提供我实现多模块项目管理

比如ng提供的

  1. add 为你的项目添加对外部库的支持
  2. build 把 Angular 应用编译到给定输出路径下名为 dist/ 的输出目录中。此命令必须在工作空间目录下执行。
  3. config 从本工作空间的 xxx.json 文件中获取或设置 项目 的配置值。
  4. doc 快速构建打开项目docs,可以进行文档查阅
  5. i18n 再源码中提取i18n的信息,进行配置
  6. new 快速添加一个模块
  7. remove 通过eny.yaml 的配置去除相关代码,不交付给客户。

可惜的是目前只有ng提供了一个比较强大的cli,还好他是开源。

可惜它不是React的生态。导致我有一段时间都在找React相关的cli,面临的结果是没有这种工具,几乎用来构建项目的脚手架居多,然后得配合他们的框架使用比如umi可以自己进行扩展。

但是阿里开源,kpi后必挂。

逼迫无奈,只能自己动手风衣足食制作公司内部的cli工具,来方便开发者/crud的简单低代码构建器。

题外话: 低代码并不是现在到处宣传的那种东西,低代码类似于代码构建器。生成出来的东西还得让程序员进行加工,低代码早就存在,这是前端发展得慢,想用可视化来制作,那是非常困难的,程序开发追求的是维护性,而不是这种虚头。你们可以想想20年前做dreamweaver和asp.net webFrom的两个巨头最后都怎么了?

零代码也没有说的那么神奇好吗,零代码能做的事情知识行业公用工作流的集合。你可以理解成后端事件开发的工作流框架。工作流能帮你解决70%的问题,但是还有30%未知的问题,无法解决。所以零代码是可行的,但依然无法完全代替剩下30%的事情。如果可以的话,发展20年的工作流,早就已经干掉程序员了,后端还需要这么多人去堆业务吗?早就开始进入系统设计系统优化这个时代。和外国人一样项目立项前都在考虑长久的维护成本了...

选择node还是golang

在制作cli的前期,我还在考虑使用node,golang,java呢?但是java的话配置环境,还有一大堆的oop编程规范要去搞,我有不是做什么大项目。只是为了方便项目做辅助工作而已,比如低代码的模板生成器这种。

最后只能在node和golang之间进行选择。

首先进行10w次,写入操作进行对比

由于我需要对项目做很多的读写,remove操,所以读写性能是我比较关心的。公司是使用yapi来做代码生成的。后端注解生成yapi文档,前端通过yapi-to-typescript生成前端的数据调用层。一开始接口数量不多的时候,生成的时间还是可以接受的,但是现在系统将近2w的接口量。node.js做写入操作,每次部署上线的时候为了拿到最新的DB层都需要等待8-10分钟的写入时间

由于发布与凌晨有些地方没看清楚,哈哈哈我好像误导了800多个人

image.png,不过经过修改下面的测试是修过来的。

package main
import "os"
func main() {
	file, _ := os.OpenFile("./go.txt", os.O_CREATE|os.O_RDWR|os.O_APPEND, 0777)
	defer file.Close()
	i := 0
        // 5个零了喔
	for i < 100000 {
		file.WriteString("a\n")
		i++
	}
}

在windows子系统里面跑真慢....所以我用10w进行测试了 go的结果

react-boot-cli $ time go run main.go 

real    0m9.950s
user    0m0.639s
sys     0m2.630s

由于node.js的api文件,我只知道这种写法。可能对比的方式不正确吧。但是api是这样,我也没办法。

const fs = require('fs')
let i = 0;
while(i<100000){
    fs.appendFileSync("node.txt","a\n")
    i++;
}

node.js写入10w次的结果

react-boot-cli $ time node main.js 

real    1m33.210s
user    0m3.633s
sys     0m12.617s

流写入

image.png

我也试试

func main() {
	file, _ := os.OpenFile("./go.txt", os.O_CREATE|os.O_RDWR|os.O_APPEND, 0777)
	defer file.Close()
	ws := bufio.NewWriter(file)
	i := 0
	for i < 100000 {
		ws.WriteString("a\n")
		i++
	}
	ws.Flush()
}

用流处理快得我想哭了。。。

real    0m0.167s
user    0m0.251s
sys     0m0.094s

js

const fs = require('fs')

let i = 0;
ws = fs.createWriteStream("node.txt")
//5个零
while (i < 100000) {
    ws.write("a\n")
    i++;
}
ws.end()
real    0m2.397s
user    0m0.156s
sys     0m0.398s

用流之后,发现。。其实node好像也不是不行...

😓速度一个天一个地,如果只是用来创建crud模版的构建器,这个速度还是能接受。但是如果是交付项目代替人力做大量读写、remove操作,这个会让我感动非常痛苦,加上node.js的多线程还的自己单独配置。想到这些原因我回头就直接选择了golang做这次项目的启动,并向公司申请了一名中级的golang开发协助我的工作。

在这次的编码中我选择了golang著名cli开源库cobra

简介Cobra

cobra是一个命令行程序库,可以用来编写命令行程序。同时,它也提供了一个脚手架, 用于生成基于 cobra 的应用程序框架。非常多知名的开源项目使用了 cobra 库构建命令行,如KubernetesHugoetcd等等等等。 本文介绍 cobra 库的基本使用和一些有趣的特性。

快速使用

go get -u github.com/spf13/cobra/cobra

-u 安装全局变量类似npm i -g cobra

cobra init appname

cd appname && go mod init appname

我们拥有的项目结构

appname/
    ▾ cmd/
        helper.go
        root.go
        version.go
    main.go

添加更多的命令

cobra add time

会在cmd文件夹添加一个命令文件。

/*
Copyright © 2022 NAME HERE <EMAIL ADDRESS>

*/
package cmd

import (
	"log"
	"os"
	"os/exec"

	"github.com/spf13/cobra"
)

// timeCmd represents the time command
var timeCmd = &cobra.Command{
	Use:   "time",
	Short: "计算时间用的",
	Long:  `其实是一个dome`,
	Run: func(cmd *cobra.Command, args []string) {
		//主要关注这里,执行的调用
		c := exec.Command("bash", "-c", "time ls")
		c.Stdout = os.Stdout
		c.Stderr = os.Stderr
		err := c.Run()
		if err != nil {
			log.Println(err)
		}
	},
}

func init() {
	rootCmd.AddCommand(timeCmd)
}

截屏2022-01-25 下午11.50.39.png

我们试试go run ./main.go time

截屏2022-01-26 上午12.01.31.png

也能实现我们的效果,好了今天简单的水了一文,出自什么原因要开始做自己的cli工具。