作为一个纯前端的程序员,第一次开发后端服务,内心还是有点小激动的,但是为了一劳永逸
还是决定挑战一下。准备试试最近接触的Go语言,因为服务不是很复杂,不采用任何Go的框架
第一个服务器项目,先造势,这样显得专业一些,哈哈哈。。。
思维导图:
项目需求:
- 需要固定接口获取最新的项目地址
- 自动更新,配置文件
- 可视化后台界面
- 可以满足产品、测试人员操作/修改
立项:
预计开发一个后端服务,提供上传,更新配置,提供最新链接
选型:
使用Go语言,支持IO,压缩、编码,较完善的HTTP封装,新人友好
开发
- 先开发一个测试接口,实现最基础的HTTP行为
- 实现上传接口,
1、包含上传文件格式校验,
2、限制上传文件体积 - 更新配置文件 调用io功能模块,动态维护服务器配置文件
- 读取配置文件内容,拼接成合适的链接,提供给客户端
- 可视化后台界面(这里加入公司的统一后台界面了,本篇文章没有赘述,可以根据自己喜好到第三方网站上找自己喜欢的后台界面:下面推荐几个好用的模板)
饿了么组件模板
Layui
阿里中后台设计系统解决方案
部署
- 配置服务器nginx代理,监听需要的端口号;
- 配置域名(允许外部访问)
- 设置允许上传的包体大小 (这一步很容易忽略,表现是会报跨域的错,是个大坑!)
在nginx.conf文件的http{}标签中加入
client_max_body_size 1024m
可能会遇到的问题:
接触一个新的领域肯定会遇到很多的问题(踩各种坑):
这里分享一下我遇到的坑: (仅供借鉴)
- 校验上传格式 ——可能会遇到获取文件名不正确的清空,和对文件名的修改问题
- 配置文件格式(不同格式的文件读取和写入方式不同)
- 跨域警告(这个前端常遇到的问题,终于在写服务的时候完美提前考虑到了)
- nginx配置(主要是设置包体大小!这是个很容易忽略的坑。且没有直接报错,总是报跨域的错,一头雾水,后来终于找到原因,才得以解决。太坑了qaq)
- 服务器文件资源维护(涉及到服务器硬盘容量)
项目提取出的代码展示:
/*
* @Author: Hifun
* @Date: 2020/8/27 19:27
*/
package main
import (
"archive/zip"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"path/filepath"
"strings"
)
const (
configName = "config.json"
currentPath = "./path.txt"
)
var targetPath = ""
func sayHello(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello world!" + targetPath))
return
}
func initCurrentPath() {
targetPath = loadConfig(currentPath)
}
func main() {
// 先初始化当前的路径
initCurrentPath()
fmt.Printf("server start success!\n Listening...\nCurrentPath: "+targetPath)
// 测试接口
http.HandleFunc("/test",sayHello)
// 注册上传压缩包的接口
http.HandleFunc("/upload", uploadHandler)
// 注册更新文件内容的接口
http.HandleFunc("/updateContext",UpdateConfig)
// 注册读取文件内容的接口
http.HandleFunc("/getContext",getConfigContext)
// 创建一个监听 使用默认的handler监听 8090端口
err := http.ListenAndServe(":8090", nil)
if err != nil {
log.Fatal("ListenAndServe: ", err.Error())
}
}
func uploadHandler(w http.ResponseWriter, r *http.Request) {
fmt.Printf("\n enter func uploadHandler\n")
w.Header().Set("Access-Control-Allow-Origin", "*") //允许访问所有域
w.Header().Add("Access-Control-Allow-Headers", "Content-Type") //header的类型
w.Header().Set("content-type", "application/json") //返回数据格式是json
// 限制客户端上传文件的大小
r.Body = http.MaxBytesReader(w, r.Body, 20*1024*1024)
err := r.ParseMultipartForm(20 * 1024 * 1024)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// 获取上传的文件
file, fileHeader, err := r.FormFile("uploadFile")
// 检查文件类型
ret := strings.HasSuffix(fileHeader.Filename, ".zip")
if ret == false {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
//r.ParseForm()
appName := r.Form["appName"][0]
targetPath := "./" + appName
isExist,err := PathExists(targetPath)
if err != nil {
fmt.Printf("get dir error![%v]\n", err)
return
}
if !isExist {
// 创建文件夹
err := os.Mkdir(targetPath, os.ModePerm)
if err != nil {
fmt.Printf("mkdir failed![%v]\n", err)
} else {
fmt.Printf("mkdir success!\n")
}
}
// 写入文件
dst, err := os.Create(targetPath + "/" + fileHeader.Filename)
defer dst.Close()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer file.Close()
if _, err := io.Copy(dst, file); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
result := NewBaseJsonBean()
result.Code = 1000
result.Message = "hifun upload success!"
json, _ := json.Marshal(result)
w.Write(json)
return
}
func UpdateConfig(w http.ResponseWriter,r *http.Request) {
// 获取客户端POST方式传递的参数
r.ParseForm()
appName := r.Form["appName"]
version := r.Form["version"]
fmt.Println("hifun: getVersion: ",version)
updateConfig(appName[0],version[0])
w.Header().Set("Access-Control-Allow-Origin", "*")
result := NewBaseJsonBean()
result.Code = 1000
result.Message = "updateConfig success!"
//result.Data = data
w.Header().Set("Content-Type", "application/json")
json, _ := json.Marshal(result)
w.Write(json)
return
}
func updateConfig(appName,version string) {
targetPath := "./" + appName + "/"
// 解压缩文件
zipFil,tarDir := targetPath + version + ".zip",targetPath
Unzip(zipFil,tarDir)
strTest := appName + "/" + version
var strByte = []byte(strTest)
err := ioutil.WriteFile(targetPath + configName, strByte, 0666)
if err != nil {
fmt.Println("write fail")
}
fmt.Println("write success")
}
func getConfigContext(w http.ResponseWriter,r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
// 获取客户端POST方式传递的参数
r.ParseForm()
appName := r.Form["appName"][0]
targetConfig := "./" + appName + "/" + configName
config := loadConfig(targetConfig)
// 向客户端返回JSON数据
data := make(map[string]interface{})
data["url"] = targetPath + config+"/index.html"
data["appName"] = appName
temLen := len(config)
appVersion := config[temLen-5: temLen]
data["appVersion"] =appVersion
result := NewBaseJsonBean()
result.Code = 1000
result.Message = "获取成功"
result.Data = data
w.Header().Set("Content-Type", "application/json")
json, _ := json.Marshal(result)
w.Write(json)
return
}
//读取到file中,再利用ioutil将file直接读取到[]byte中, 这是最优
func loadConfig(targetConfig string) string {
f, err := os.Open(targetConfig)
if err != nil {
fmt.Println("read file fail", err)
return ""
}
defer f.Close()
fd, err := ioutil.ReadAll(f)
if err != nil {
fmt.Println("read to fd fail", err)
return ""
}
return string(fd)
}
// 判断文件夹是否存在
func PathExists(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
通用的返回代码模块:
/*NewBaseJsonBean用于创建一个struct对象:*/
type BaseJsonBean struct {
Code int `json:"code"`
Data interface{} `json:"data"`
Message string `json:"message"`
}
func NewBaseJsonBean() *BaseJsonBean {
return &BaseJsonBean{}
}
压缩文件通用方法:
// srcFile could be a single file or a directory
func Zip(srcFile string, destZip string) error {
zipfile, err := os.Create(destZip)
if err != nil {
return err
}
defer zipfile.Close()
archive := zip.NewWriter(zipfile)
defer archive.Close()
filepath.Walk(srcFile, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
header.Name = strings.TrimPrefix(path, filepath.Dir(srcFile) + "/")
// header.Name = path
if info.IsDir() {
header.Name += "/"
} else {
header.Method = zip.Deflate
}
writer, err := archive.CreateHeader(header)
if err != nil {
return err
}
if ! info.IsDir() {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(writer, file)
}
return err
})
return err
}
解压缩文件通用方法:
// unzip a zipFile to a directory
func Unzip(zipFile string, destDir string) error {
zipReader, err := zip.OpenReader(zipFile)
if err != nil {
return err
}
defer zipReader.Close()
for _, f := range zipReader.File {
fpath := filepath.Join(destDir, f.Name)
if f.FileInfo().IsDir() {
os.MkdirAll(fpath, os.ModePerm)
} else {
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
return err
}
inFile, err := f.Open()
if err != nil {
return err
}
defer inFile.Close()
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return err
}
defer outFile.Close()
_, err = io.Copy(outFile, inFile)
if err != nil {
return err
}
}
}
return nil
}
其他通用方法都在main模块中,可以选择性取舍。
总结:
做服务器项目(不论功能复杂与否),完全是和前端采用不一样的思维来开发的,这种经历值得仔细回味。看到自己的服务运行在服务器上,得到其他人的认可,这种感觉真的很棒。前端开发也可以上手后台服务,技术无界限,大家加油!
————————————————————————————————————————
如果本篇文章对你有启发和帮助,希望可以给我点个赞,嘻嘻嘻嘻。欢迎随时交流
笔者QQ:840658308
笔者微信:HifunAmos
(扫描他,带走我!)
github项目链接