使用Go写一个简易的MVC的Web框架

Bingo

首发于Golang中文网php

这东西是我最近开始写的一个玩意儿...html

刚从PHP转过来,对Go的特性还不是很了解,适用了一下gin,以为虽然挺好的,可是一些语法没有Laravel那么方便git

因此想再造个轮子看看... en .... 就酱github

bingo是一个基于go语言的轻量级API框架,专一构建restfulAPIajax

GitHub地址:silsuer/bingo数据库

最近我作了不少修改,gayhub上的跟这篇文章很不符,因此在这再把commit的连接贴出来点这里这里~json

目录结构

  • app 放置与网站相关代码
  • core 放置框架核心代码a
  • vendor 放置第三方库,使用glide管理第三方库
  • public 放置html代码

开发过程

go的net包极其好用,用它开发框架也是极其快速(竟然比写php框架还要快...)数组

首先肯定main函数,在main函数中实例化一个结构体,而后调用其中的Run函数便可浏览器

动手操做:缓存

func main() {
  // new 一个bingo对象,而后bingo.Run()便可
  // 指定静态目录,默认指向index.html ,加载路由文件
  // 加载env文件
    bingo := new(core.Bingo)
    bingo.Run(":12345")
 }
复制代码

接下来去写bingo文件:

func (b *Bingo) Run(port string) {
     // 传入一个端口号,没有返回值,根据端口号开启http监听
 
     // 此处要进行资源初始化,加载全部路由、配置文件等等
     // 实例化router类,这个类去获取全部router目录下的json文件,而后根据json中的配置,加载数据
     // 实例化env文件和config文件夹下的全部数据,根据配置
     // 根据路由列表,开始定义路由,而且根据端口号,开启http服务器
     http.ListenAndServe(port, bin)
     // TODO 监听平滑升级和重启
 }
复制代码

Run函数很是简单,只有一行代码,就是开启一个Http服务器并监听传入的端口,

因为咱们要本身控制各类路由,因此咱们不能用net包中自带的http服务,网上有不少原理说的很清楚了

咱们须要本身实现 ServeHTTP方法,以实现Mux这种路由器接口,因此再写一个ServeHttp方法

func (b *Bingo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
        flag := false   // 这个变量用来标记是否找到了动态路由
        // 每个http请求都会走到这里,而后在这里,根据请求的URL,为其分配所须要调用的方法
        params := []reflect.Value{reflect.ValueOf(w), reflect.ValueOf(r)}
        for _, v := range RoutesList {
            // 检测中间件,根据中间件首先开启中间件,而后再注册其余路由
            // 检测路由,根据路由指向须要的数据
            if r.URL.Path == v.path && r.Method == v.method {
                  flag = true   // 寻找到了对应路由,无需使用静态服务器
                  
                //TODO 调用一个公共中间件,在这个中间件中寻找路由以及调用中间件收尾等功能
    
                // 检测该路由中是否存在中间件,若是存在,顺序调用
                for _, m := range v.middleware {
                    if mid, ok := MiddlewareMap[m]; ok { // 判断是否注册了这个中间件
                        rmid := reflect.ValueOf(mid)
                        params = rmid.MethodByName("Handle").Call(params) // 执行中间件,返回values数组
                        // 判断中间件执行结果,是否还要继续往下走
                        str := rmid.Elem().FieldByName("ResString").String()
                        if str != "" {
                            status := rmid.Elem().FieldByName("Status").Int()
                            // 字符串不空,查看状态码,默认返回500错误
                            if status == 0 {
                                status = 500
                            }
                            w.WriteHeader(int(status))
                            fmt.Fprint(w,str)
                            return
                        }
                    }
                }
                // 检测成功,开始调用方法
                // 获取一个控制器包下的结构体
                if d, ok := ControllerMap[v.controller]; ok { // 存在 c为结构体,调用c上挂载的方法
                    reflect.ValueOf(d).MethodByName(v.function).Call(params)
                }
                // 中止向后执行
                return
            }
        }
    
        // 若是路由列表中仍是没有的话,去静态服务器中寻找
        if !flag {
             // 去静态目录中寻找
             http.ServeFile(w,r,GetPublicPath()+ r.URL.Path)
        }
        return
    }
复制代码

能够看到,咱们使用从新定义了ServeHttp方法,在这个方法中,咱们根据浏览器访问的不一样URL,经过反射获得不一样的控制器或者中间件的结构体,而且

调用对应的方法,若是访问的URL咱们没有定义的话,会到静态文件夹下去寻找,若是找到了,输出静态文件,不然输出404页面

(P.S. 由于咱们要实现的是一个无状态的API快速开发框架,因此不须要进行模版渲染,全部数据均经过ajax传输到页面中)

注意到在这个函数里我使用了MiddlewareMap[m]以及ControllerMap[m] ,这是中间件以及控制器的map,在程序初始化的时候就会存入内存中

具体定义以下:

// 这里记录全部的应该注册的结构体
    // 控制器map
    var ControllerMap map[string]interface{}
    // 中间件map
    var MiddlewareMap map[string]interface{}
    
    func init()  {
        ControllerMap = make(map[string]interface{})
        MiddlewareMap = make(map[string]interface{})
        // 给这两个map赋初始值 每次添加完一条路由或中间件,都要在此处把路由或者中间件注册到这里
        // 注册中间件
        MiddlewareMap["WebMiddleware"] =&middleware.WebMiddleware{}
    
        // 注册路由
        ControllerMap["Controller"] = &controller.Controller{}
    }

复制代码

在此处咱们用到了app/controller以及middleware包下的结构体,当路由解析完成后会把请求的路径和这里的map对应起来,如今咱们看看router中解析路由代码:

type route struct {
	path       string   // 路径
	target     string   // 对应的控制器路径 Controller@index 这样的方法
	method     string   // 访问类型 是get post 或者其余
	alias      string   // 路由的别名
	middleware []string // 中间件名称
	controller string   // 控制器名称
	function   string   // 挂载到控制器上的方法名称
}

type route_group struct {
	root_path   string   // 路径
	root_target string   // 对应的控制器路径 Controller@index 这样的方法
	alias       string   // 路由的别名
	middleware  []string // 中间件名称
	routes      []route  // 包含的路由
}

var Routes []route             // 单个的路由集合
var RoutesGroups []route_group // 路由组集合
var RoutesList []route         // 所有路由列表
var R interface{}

func init() {
	// 初始化方法,加载路由文件
	// 获取路由路径,根据路由路径获取全部路由文件,而后读取全部文件,赋值给当前成员变量
	routes_path := GetRoutesPath()
	dir_list, err := ioutil.ReadDir(routes_path)
	Check(err)
	// 根据dir list 遍历全部文件 获取全部json文件,拿到全部的路由 路由组
	for _, v := range dir_list {
		fmt.Println("正在加载路由文件........" + v.Name())
		// 读取文件内容,转换成json,而且加入数组中
		content, err := FileGetContents(routes_path + "/" + v.Name())
		Check(err)
		err = json.Unmarshal([]byte(content), &R)
		Check(err)
		// 开始解析R,将其分类放入全局变量中
		parse(R)
	}
}
复制代码

在准备编译的阶段便会执行init函数,获取到路由文件夹下的全部路由列表,咱们使用json格式来组织路由,解析出来的数据存入RoutesList列表中

下面是解析代码

func parse(r interface{}) {
	// 拿到了r 咱们要解析成实际的数据
	m := r.(map[string]interface{})
	//newRoute := route{}
	for k, v := range m {
		if k == "Routes" {
			// 解析单个路由
			parseRoutes(v)
		}
		if k == "RoutesGroups" {
			// 解析路由组
			parseRoutesGroups(v)
		}
	}

}

// 解析json文件中的单一路由的集合
func parseRoutes(r interface{}) {
	m := r.([]interface{})
	for _, v := range m {
		// v 就是单个的路由了
		simpleRoute := v.(map[string]interface{})
		// 定义一个路由结构体
		newRoute := route{}
		for kk, vv := range simpleRoute {
			switch kk {
			case "Route":
				newRoute.path = vv.(string)
				break
			case "Target":
				newRoute.target = vv.(string)
				break
			case "Method":
				newRoute.method = vv.(string)
				break
			case "Alias":
				newRoute.alias = vv.(string)
				break
			case "Middleware":
				//newRoute.middleware = vv.([])
				var mdw []string
				vvm := vv.([]interface{})
				for _, vvv := range vvm {
					mdw = append(mdw, vvv.(string))
				}
				newRoute.middleware = mdw
				break
			default:
				break
			}
		}

		// 把target拆分红控制器和方法
		cf := strings.Split(newRoute.target,"@")
		if len(cf)==2 {
			newRoute.controller = cf[0]
			newRoute.function = cf[1]
		}else{
			fmt.Println("Target格式错误!"+newRoute.target)
			return
		}

		// 把这个新的路由,放到单个路由切片中,也要放到路由列表中

		Routes = append(Routes, newRoute)
		RoutesList = append(RoutesList, newRoute)
	}
}

func parseRoutesGroups(r interface{}) {
	// 解析路由组
	m := r.([]interface{})
	for _, v := range m {
		group := v.(map[string]interface{})
		for kk, vv := range group {
			// 新建一个路由组结构体
			var newGroup route_group
			switch kk {
			case "RootRoute":
				newGroup.root_path = vv.(string)
				break
			case "RootTarget":
				newGroup.root_target = vv.(string)
				break
			case "Middleware":
				var mdw []string
				vvm := vv.([]interface{})
				for _, vvv := range vvm {
					mdw = append(mdw, vvv.(string))
				}
				newGroup.middleware = mdw
				break
			case "Routes":
				// 因为涉及到根路由之类的概念,因此不能使用上面的parseRoutes方法,须要再写一个方法用来解析真实路由
				rs := parseRootRoute(group)
				newGroup.routes = rs
				break
			default:
				break
			}
			// 把这个group放到路由组里
			RoutesGroups  = append(RoutesGroups,newGroup)
		}
	}
}

// 解析根路由 传入根路由路径 目标跟路径 而且传入路由inteface列表,返回一个完整的路由集合
// 只传入一个路由组,返回一个完整的路由集合
func parseRootRoute(group map[string]interface{}) []route {
	// 获取路由根路径和目标根路径,还有公共中间件
	var tmpRoutes []route  // 要返回的路由切片
	var route_root_path string
	var target_root_path string
	var public_middleware []string
	for k, v := range group {
		if k == "RootRoute" {
			route_root_path = v.(string)
		}
		if k == "RootTarget" {
			target_root_path = v.(string)
		}
		if k=="Middleware" {
			vvm := v.([]interface{})
			for _, vvv := range vvm {
				public_middleware = append(public_middleware, vvv.(string))
			}
		}
	}

	// 开始获取路由
	for k, s := range group {
		if k == "Routes" {
			m := s.([]interface{})
			for _, v := range m {
				// v 就是单个的路由了
				simpleRoute := v.(map[string]interface{})
				// 定义一个路由结构体
				newRoute := route{}
				for kk, vv := range simpleRoute {
					switch kk {
					case "Route":
						newRoute.path = route_root_path+ vv.(string)
						break
					case "Target":
						newRoute.target = target_root_path+ vv.(string)
						break
					case "Method":
						newRoute.method = vv.(string)
						break
					case "Alias":
						newRoute.alias = vv.(string)
						break
					case "Middleware":
						vvm := vv.([]interface{})
						for _, vvv := range vvm {
							newRoute.middleware = append(public_middleware,vvv.(string))// 公共的和新加入的放在一块儿就是总共的
						}

						break
					default:
						break
					}
				}
				// 把target拆分红控制器和方法
				cf := strings.Split(newRoute.target,"@")
				if len(cf)==2 {
					newRoute.controller = cf[0]
					newRoute.function = cf[1]
				}else{
					fmt.Println("Target格式错误!"+newRoute.target)
					os.Exit(2)
				}
				// 把这个新的路由,放到路由列表中,而且返回放到路由集合中,做为返回值返回
				RoutesList = append(RoutesList, newRoute)
				tmpRoutes = append(tmpRoutes,newRoute)
			}
		}
	}
   return tmpRoutes
}
复制代码

经过解析json文件,得到路由列表,而后在上面的ServeHttp文件中便可与路由列表进行对比了。

到此,咱们实现了一个简单的使用GO制做的Web框架

目前只能作到显示静态页面以及进行API响应

接下来咱们要实现的是:

  1. 一个便捷的ORM
  2. 制做快速添加控制器以及中间件的命令
  3. 实现数据库迁移
  4. 实现基于token的API认证
  5. 数据缓存
  6. 队列
  7. 钩子
  8. 便捷的文件上传/存储功能

求star,欢迎PR~哈哈哈(silsuer/bingo

这个东西好久以前写的,如今框架作了不少更新,可是对于原生开发来讲依旧有些参考价值吧,第一次玩掘金,看成第一篇发布的内容吧,你们好,请多关照!

相关文章
相关标签/搜索