java开发web网站的路由设计_go web开发之url路由设计

概述

最近在搞自己的go web开发框架, 反正也没打算私藏, 所以现在先拿出url路由设计这块来写一篇博客. 做过web开发的都知道, 一个好的url路由可以让用户浏览器的地址栏总有规律可循, 可以让我们开发的网站更容易让搜索引擎收录, 可以让我们开发者更加方便的MVC. 我们在使用其他web开发框架的时候, url路由肯定也会作为框架的一个重点功能或者说是一个宣传”卖点”. 所以说, 一个web框架中url路由的地位还是非常重要的.

回到go web开发中, 那如何用go来实现一个url路由功能呢? 实现后代码如何书写呢? 下面我们就来一步步的去实现一个简单的url路由功能.

如何使用

在我们学习如何实现之前, 肯定是要先看看如何使用的. 其实使用起来很简单, 因为我之前写过一个PHP的web开发框架, 所以我们的路由部分的使用像极了PHP(ThinkPHP). 来看看代码吧.

package main

import (

"./app"

"./controller"

)

func main() {

app.Static["/static"] = "./js"

app.AutoRouter(&controller.IndexController{})

app.RunOn(":8080")

}

三行代码, 第一行的作用大家都应该清楚, 就是去serve一些静态文件(例如js, css等文件), 第二行代码是去注册一个Controller, 这行代码在PHP是没有的, 毕竟PHP是动态语言, 一个__autoload就可以完成类的加载, 而go作为静态语言没有这项特性, 所以我们还是需要手工注册的(思考一下, 这里是不是可以想java一样放到配置文件中呢? 这个功能留到以后优化的时候添加吧.) 还有最后一行代码没说, 其实就是启动server了, 这里我们监听了8080端口.

上面的代码很简单, 我们来看看那个IndexController怎么写的.

package controller

import (

"../app"

"../funcs"

"html/template"

)

type IndexController struct {

app.App

}

func (i *IndexController) Index() {

i.Data["name"] = "qibin"

i.Data["email"] = "qibin0506@gmail.com"

//i.Display("./view/info.tpl", "./view/header.tpl", "./view/footer.tpl")

i.DisplayWithFuncs(template.FuncMap{"look": funcs.Lookup}, "./view/info.tpl", "./view/header.tpl", "./view/footer.tpl")

}

首先我们定义一个结构体, 这个结构体匿名组合了App这个结构体(用面向对象的话说就是继承了), 然我们给他定义了一个Index方法, 这里面具体干了啥我们先不用去关心. 那怎么访问到呢? 现在运行代码, 在浏览器输入http://localhost:8080或者输入http://localhost:8080/index/index就可以看到我们在Index方法里输出的内容了, 具体怎么做到的, 其实这完全是url路由的功劳, 下面我们就开始着手准备设计这么一个url路由功能.

url路由的设计

上面的AutoRouter看起来很神奇,具体干了啥呢? 我们先来看看这个注册路由的功能是如何实现的吧.

package app

import (

"reflect"

"strings"

)

var mapping map[string]reflect.Type = make(map[string]reflect.Type)

func router(pattern string, t reflect.Type) {

mapping[strings.ToLower(pattern)] = t

}

func Router(pattern string, app IApp) {

refV := reflect.ValueOf(app)

refT := reflect.Indirect(refV).Type()

router(pattern, refT)

}

func AutoRouter(app IApp) {

refV := reflect.ValueOf(app)

refT := reflect.Indirect(refV).Type()

refName := strings.TrimSuffix(strings.ToLower(refT.Name()), "controller")

router(refName, refT)

}

首先我们定义了一个map变量, 他的key是一个string类型, 我们猜想肯定是我们在浏览器中输入的那个url的某一部分, 然后我们通过它来获取到具体要执行拿个结构体. 那他的value呢? 一个reflect.Type是干嘛的? 先别着急, 我们来看看AutoRouter的实现代码就明白了. 在AutoRouter里, 首先我们用reflect.ValueOf来获取到我们注册的那个结构体的Value, 紧接着我们又获取了它的Type, 最后我们将这一对string,Type放到了map了. 可是这里的代码仅仅是解释了怎么注册进去的, 而没有解释为什么要保存Type啊, 这里偷偷告诉你, 其实对于每次访问, 我们找到对应的Controller后并不是也一定不可能是直接调用这个结构体上的方法, 而是通过反射新建一个实例去调用. 具体的代码我们稍后会说到.

到现在为止, 我们的路由就算注册成功了, 虽然我们对于保存Type还寸有一定的疑虑. 下面我们就开始从RunOn函数开始慢慢的来看它是如何根据这个路由注册表来找到对应的Controller及其方法的.

首先来看看RunOn的代码.

func RunOn(port string) {

server := &http.Server{

Handler: newHandler(),

Addr: port,

}

log.Fatal(server.ListenAndServe())

}

这里面的代码也很简单, 对于熟悉go web开发的同学来说应该非常熟悉了, Server的Handler我们是通过一个newHandler函数来返回的, 这个newHandler做了啥呢?

func newHandler() *handler {

h := &handler{}

h.p.New = func() interface{} {

return &Context{}

}

return h

}

首先构造了一个handler, 然后又给handler里的一个sync.Pool做了赋值, 这个东西是干嘛的, 我们稍后会详细说到, 下面我们就来安心的看这个handler结构体如何设计的.

type handler struct {

p sync.Pool

}

很简单, 对于p上面说了, 在下面我们会详细说到, 对于handler我们相信它肯定会有一个方法名叫ServeHTTP, 来看看吧.

func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {

if serveStatic(w, r) {

return

}

ctx := h.p.Get().(*Context)

defer h.p.Put(ctx)

ctx.Config(w, r)

controllerName, methodName := h.findControllerInfo(r)

controllerT, ok := mapping[controllerName]

if !ok {

http.NotFound(w, r)

return

}

refV := reflect.New(controllerT)

method := refV.MethodByName(methodName)

if !method.IsValid() {

http.NotFound(w, r)

return

}

controller := refV.Interface().(IApp)

controller.Init(ctx)

method.Call(nil)

}

这里面的代码其实就是我们路由设计的核心代码了, 下面我们详细来看一下这里面的代码如何实现的. 前三行代码是我们对于静态文件的支持.

接下来我们就用到了sync.Pool, 首先我们从里面拿出一个Context, 并在这个方法执行完毕后将这个Context放进去, 这样做是什么目的呢? 其实我们的网站并不是单行的, 所以这里的ServeHTTP并不是只为一个用户使用, 而在咱们的Controller中还必须要保存ResponseWriter和Request等信息, 所以为了防止一次请求的信息会被其他请求给重写掉, 我们这里选择使用对象池, 在用的时候拿出来, 用完了之后进去, 每次使用前先将信息刷新, 这样就避免了不用请求信息会被重写的错误.对于sync.Pool这里简单解释一下, 还及得上面我们曾经给他的一个New字段赋值吗? 这里面的逻辑就是, 当我们从这个pool中取的时候如果没有就会到用New来新建一个, 因此这里在可以保证Context唯一的前提下, 还能保证我们每次从pool中获取总能拿到.

继续看代码, 接下来我们就是通过findControllerInfo从url中解析出我们要执行的controller和method的名字, 往下走, 我们通过反射来新建了一个controller的对象, 并通过MethodByName来获取到要执行的方法.具体代码:

refV := reflect.New(controllerT)

method := refV.MethodByName(methodName)

这里就解释了, 上面为什么要保存reflect.Type. 最后我们将Context设置给这个Controller,并且调用我们找到的那个方法. 大体的url路由就这样,主要是通过go的反射机制来找到要执行的结构体和具体要执行到的那个方法, 然后调用就可以了. 不过,这其中我们还有一个findControllerInfo还没有说到, 它的实现就相对简单, 就是通过url来找到controller和我们要执行的方法的名称. 来看一下代码:

func (h *handler) findControllerInfo(r *http.Request) (string, string) {

path := r.URL.Path

if strings.HasSuffix(path, "/") {

path = strings.TrimSuffix(path, "/")

}

pathInfo := strings.Split(path, "/")

controllerName := defController

if len(pathInfo) >1 {

controllerName = pathInfo[1]

}

methodName := defMethod

if len(pathInfo) >2 {

methodName = strings.Title(strings.ToLower(pathInfo[2]))

}

return controllerName, methodName

}

const (

defController = "index"

defMethod = "Index"

)

到现在位置, 我们的url路由基本已经成型了, 不过还有几个点我们还没有射击到, 例如上面经常看到的App和Context. 首先我们来看看这个Context吧,这个Context是啥? 其实就是我们对请求信息的简单封装.

package app

import (

"net/http"

)

type IContext interface {

Config(w http.ResponseWriter, r *http.Request)

}

type Context struct {

w http.ResponseWriter

r *http.Request

}

func (c *Context) Config(w http.ResponseWriter, r *http.Request) {

c.w = w

c.r = r

}

这里我们先简单封装一下, 仅仅保存了ResponseWriter和Request, 每次请求的时候我们都会调用Config方法将新的ResponseWriter和Request保存进去.

而App呢? 设计起来就更加灵活了, 除了几个在handler里用到的方法, 基本都是”临场发挥的”.

type IApp interface {

Init(ctx *Context)

W() http.ResponseWriter

R() *http.Request

Display(tpls ...string)

DisplayWithFuncs(funcs template.FuncMap, tpls ...string)

}

这个接口里的方法大家应该都猜到了, Init方法我们在上面的ServeHTTP已经使用过了, 而W和R方法纯粹是为了方便获取ResponseWriter和Request的, 下面的两个Display方法这里也不多说了, 就是封装了go原生的模板加载机制. 来看看App是如何实现这个接口的吧.

type App struct {

ctx *Context

Data map[string]interface{}

}

func (a *App) Init(ctx *Context) {

a.ctx = ctx

a.Data = make(map[string]interface{})

}

func (a *App) W() http.ResponseWriter {

return a.ctx.w

}

func (a *App) R() *http.Request {

return a.ctx.r

}

func (a *App) Display(tpls ...string) {

if len(tpls) ==0 {

return

}

name := filepath.Base(tpls[0])

t := template.Must(template.ParseFiles(tpls...))

t.ExecuteTemplate(a.W(), name, a.Data)

}

func (a *App) DisplayWithFuncs(funcs template.FuncMap, tpls ...string) {

if len(tpls) ==0 {

return

}

name := filepath.Base(tpls[0])

t := template.Must(template.New(name).Funcs(funcs).ParseFiles(tpls...))

t.ExecuteTemplate(a.W(), name, a.Data)

}

ok, 该说的上面都说了, 最后我们还有一点没看到的就是静态文件的支持, 这里也很简单.

var Static map[string]string = make(map[string]string)

func serveStatic(w http.ResponseWriter, r *http.Request) bool {

for prefix, static := range Static {

if strings.HasPrefix(r.URL.Path, prefix) {

file := static + r.URL.Path[len(prefix):]

http.ServeFile(w, r, file)

return true

}

}

return false

}

到现在为止, 我们的一个简单的url路由就实现了, 但是我们的这个实现还不完善, 例如自定义路由规则还不支持, 对于PathInfo里的参数我们还没有获取, 这些可以在完善阶段完成. 在设计该路由的过程中充分的参考了beego的一些实现方法. 在遇到问题时阅读并理解别人的代码才是读源码的正确方式.

最后我们通过一张运行截图来结束这篇文章吧.

97d15516168625ee6091bdf99d586827.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值