使用Go寫一個簡易的MVC的Web框架

silsuer在掘金發表於2018-08-14

Bingo

首發於Golang中文網

這東西是我最近開始寫的一個玩意兒...

剛從PHP轉過來,對Go的特性還不是很瞭解,適用了一下gin,覺得雖然挺好的,但是一些語法沒有Laravel那麼方便

所以想再造個輪子看看... en .... 就醬

bingo是一個基於go語言的輕量級API框架,專注構建restfulAPI

GitHub地址:silsuer/bingo

最近我做了很多修改,gayhub上的跟這篇文章很不符,所以在這再把commit的連結貼出來點這裡這裡~

目錄結構

  • 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

這個東西很久之前寫的,現在框架做了很多更新,但是對於原生開發來說依舊有些參考價值吧,第一次玩掘金,當作第一篇釋出的內容吧,大家好,請多關照!

相關文章