2-4 前端服务
Wed, Jul 4, 2018
概述
前端服务(大前端):
- 前台的HTML+CSS+JS
- 后端的Go的WebServer
小的网站:
- 直接一个webServer把结果返回给前端完事儿
弊端:
- 没法解耦
- 效率低
- 能承载的业务量有限
- 扩容非常复杂
- 没办法进行分布式拆分和服务化解耦
前端服务 (不是前台):
→ 前台(Front in 的 UI)
→ WebServer(简单的处理,如:
- 归一化
- 数据过滤
- 普通处理
- 做成真正能够让后台的服务认识的Request)
→ 后端的服务(做真正的业务逻辑处理)
重要的知识点
Go的模板引擎
模板引擎:将HTML解析和元素预置替换,生成最终页面的工具
类似PHP中带tag的部分
比较古老的做法
现代的做法:在HTML里嵌入它的元素,让模板引擎把元素和简单的语言进行替换,最后展现在外面给用户看的前台的部分
Go的模板(相同的interface):
- text/template
- html/template
Go的模板采用动态生成的模式
典型的模板引擎的文件夹结构
home.html就是将要被渲染的模板
web文件夹下,处理web Request的时候,会读home.html
构建大前端
大前端两大任务
1 转发请求(解决跨域问题)
跨域资源共享问题
跨域资源:cross origin resource sharing - 当我们从浏览器端发起一个请求,而这个请求的域名实际上和浏览器所处的请求不同的时候,就是跨域资源访问
问题描述:前台用一些流行的库调用ajax去后端获取请求。使用前端请求api的时候,不能在js里面请求「无关域名」的内容,否则会产生跨域,被浏览器拦截下来。
问题举例:本来的web端render的配置,域名比如为127.0.0.1:8000,但是实际的api服务是127.0.0.1:9000
问题原因:「跨域」是危险操作,可能为用户带来严重损失。「浏览器」为了避免用户损失,而做的相应限制(客户端就没这种鸟问题)
为何必解:当每个服务都服务化之后,跨域访问必不可少(我们的两个端口,被当做两个独立domain来对待)不得不发起跨域请求。此时需要proxy
解决方案:可以做一些处理,比如:
- header里添加一些东西(不能真正解决问题,依然有危险)
- 服务端做请求转发(遵守跨域规则)
两种转发模式
proxy模式
把域名做转换,其他请求参数透传
api模式
约定好web端请求参数的格式,按照格式做请求转换 & 转发 可以参考我团iOS直连实践
举例:
- 有一个api:http://xxx:8000/api这个api会把body中的格式按照约定规范起来,如
- {url:"", method: "", message: ""}
- WebServer会把这些东西取出来,做转化,然后通过http的client发给后端,后端处理之后把结果返还给api,api把结果返还给客户端
2 渲染模板
代码架构 & 实现
web server部分
/web
/../main.go
/../defs.go
/../handlers.go
/../ client.go (不同的,起到代理转发的作用。用户进到前端的时候,会做一些操作,前端会做一些代理的方式,把用户的request转成真正的api)
web UI部分
/template (普通的前端)
/../img
/../scripts
/../../home.js
/../home.html - 登录界面
/../userhome.html - 登录之后的界面
WEB服务器渲染页面
关键点:
POST在提交表单时的用法
处理api转发类请求
挂载静态资源
package main import ( "net/http" "html/template" "xxx/httprouter" // 为啥用它?因为虽然是渲染页面,但本质上还是一个request到后台,后台接受之后处理再返回给前台。 // 只不过,在web UI部分,我们大部分返回的都是整个的html页面,而非json的消息或文件流。所以整个的原理是一样的。 ) func RegisterHandler() *httprouter.Router { router := httprouter.New() router.GET("/", homeHandler) // 主页 router.POST("/", homeHandler) // POST是提交表单过程中跳转的下一个页面 router.GET("/userhome", userHomeHandler) // 用户页 router.POST("/", userHomeHandler) router.POST("/api", apiHandler) // api转发类请求 // file server: 放静态文件的server // golang提供原生方法将url绑到文件夹作为file server。可以直接通过url引它 // 如下,我们的template文件夹,会挂到/statics/这个url下 router.ServeFiles("/statics/*filepath", http.Dir("./template")) return router } func main() { r := RegisterHandler() http.ListenAndServe(":8080", r) } |
homeHandler
html 模板解析
<a href="#news">{{.Name}}</a> // .Name就是模板里需要替换掉的东西 |
handler里面,会把楼上的东东替换为我们想要的东西
package main // 替换到模板上的传入结构体 type HomePage struct { Name string // 需要和模板中一一对应 } func homeHandler(w http.ResponseWriter, r*http.Request, ps httprouter.Router) { p := &HomePage{Name: "azen"} t, e := template.ParseFile("./template/home.html") // 工程中的相对/绝对路径问题 if e != nil { log.Printf("Parsing 模板出错:%s", e) return } t.Execute(w, p) return } |
工程中的相对/绝对路径问题
文件的相对路径或绝对路径,是以编译成的二进制可执行文件为主体视角,进行描述的。
我们编译成的二进制文件,会放到/bin文件夹下,所以我们的template文件也需要放到那个文件夹下。否则无法成功读取。
template.ParseFile()
把html文件,parse成模板引擎能理解的模板文件,而非初始的html文件
t.Execute(w, p)
- 把模板和需要渲染到模板中的变量一起执行
- 把执行结果放到response writer中
- 通过response writer返回给前端,在客户端能展现出来
实现自动化部署脚本
build.sh
作用:
- 用go编译web server
- 挪到web UI的文件夹下
- 把resource文件全部挪过去(包括template文件)
#! /bin/bash #Build web UI cd $GOPATH/src/xxxxxx/video_server/web go install cp $GOPATH/bin/web $GOPATH/bin/video_server_web_ui/web cp -R $GOPATH/src/xxxxx/video_server/templates $GOPATH/bin/video_server_web_ui |
提示:需要提前把文件夹创建好,否则cp指令会报错
#起服务 cd $GOPATH/bin/video_server_web_ui ./web |
以上,直接访问:8080端口,就可以看到模板了。
好处:修改了web的静态资源之后,不需要重启服务,只需要执行脚本重新部署下就好。
homehandler之身份验证
进到这个页面的时候,有两个情况
visitor模式 - 访客
- 展示注册页面
user模式 - 已登录
- 重定向到user-home页面
使用session/cookie判断用户是登录用户
思路:每次进到这个页面,读一下cookie,有木有sessionID和userName
- 有:302跳转
- 无:停留在注册页面
func homeHandler(w http.ResponseWriter, r *http.Request, ...) { ... cname, err1 := r.Cookie("username") sid, err1 := r.Cookie("session") if err1 != nil || err2 != nil { // 返回登录/注册页面 } if len(cname.Value) != 0 && len(sid.Value) != 0 { // 非生产环境的,减少了判断。生产:判断存在、判断用户匹配 http.Redirect(w, r, "/userhome", http.StatusFound) // 302 // 楼上的url,是相对url,不带host的 } ... } |
userHomeHandler
和之前的比较像,但是session校验流程和之前的相反。
userHomePage,大部分情况下是通过表单提交过来的 - 因为是homePage的下一个页面,一般是点击「登录」,发个POST请求过来的
func UserPage struct { Name string } func userHomeHandler(w http.ResponseWriter, r*http.Request, ps httprouter.Router) { // 判断cookie里有木有session和username // 和上面一样就不写了 // 如果xxx重定向到"/" fname := r.FormValue("username") // 这里直接读表单里的username,此处不判断用户是否合法 // 检查是否合法会放到客户端提交表单的时候,通过调api来判断是否合法,提交过来的都是合法的 // 原因:减少web server的逻辑处理能力,在处理能力上做到解耦,利于服务化解耦的理念和工程实践 // 写「前后端分离」的时候,尽量把和「业务」相关的内容放到后端 - api service //「业务相关」不大的逻辑判断和填充,放到大前端的web service里面 var p *UserPage if len(cname.Value) != 0 { p = &UserPage{Name: cname.Value} // 已登录,就直接拿cookie里的cname } else if len(fname) != 0 { p = &UserPage{Name: fname} // 没登录,从表单提交的里面读 } t, e := template.ParseFiles("./templates/userhome.html") if e != nil { log.Printf("Parsing userhome.html error: &s", e) return } e.Excute(w, p) } |
提示:
知识点在于:从表单里读数据、web重定向
表单提交、用户验证部分内容,后面详说
以上:模板引擎使用知识点完成。
代理与转发模块
api转发模块
前置条件:构造api转发的统一规范
所有三种API(POST、GET、DELETE):归并为一种,进行统一处理
type ApiBody struct { Url string `json:"url"` // 必有 Method string `json: "method"` // 必有 ReqBody string `json: "req_body"` // POST请求必有 } ... |
func apiHandler(w http.ResponseWriter, r*http.Request, ps httprouter.Router) { if r.Method != http.MethodPost { // 一定要是POST请求才符合我们的规范,否则抛错 re, _ := json.Marshal(ErrorRequestNotRecognized) // 返回一个异常 io.WriteString(w, string(re)) return } res, _ := ioutil.ReadAll(r.Body) apibody := &ApiBody{} if err := json.Unmarshal(res, apibody); err != nil { re, _ := json.Marshal(ErrorRequestBodyParseFailed) // 返回一个异常 io.WriteString(w, string(re)) return } // 真正处理Request request(apibody, w, r) derfer r.Body.Close() // 为了安全而Close - Request在退完之前可能会留在栈里 } |
client.go - 用来代理client发送真正的request
var httpClient *http.httpClient func init() { httpClient = &http.Client{} } func request(b *ApiBody, w http.ResponseWriter, r*http.Request) { var resp *http.Response var err error // 分别处理GET、POST、DELETE switch b.Method { case http.MethodGet: req, _ := http.NewRequest("GET", b.Url, nil) // method, url, body-get为nil req.Header = r.Header resp, err = httpClient.Do(req) if err != nil { log.Printf(err) return } normalResponse(w, resp) case http.MethodPost: req, _ := http.NewRequest("POST", b.Url, bytes.NewBuffer([]byte(b.ReqBody))) // method, url, body-get为nil req.Header = r.Header resp, err = httpClient.Do(req) if err != nil { log.Printf(err) return } normalResponse(w, resp) case http.MethodDelete: req, _ := http.NewRequest("Delete", b.Url, nil) // method, url, body-get为nil req.Header = r.Header resp, err = httpClient.Do(req) if err != nil { log.Printf(err) return } normalResponse(w, resp) default: w.WriteHeader(http.StatusBadRequest) io.WriteString(w, "Bad api request") } } func normalResponse(w http.ResponseWriter, r*http.Response) { // r用来写入真的response res, err := ioutil.ReadAll(r.Body) if err != nil { re, _ := json.Marshal(ErrorInternalFaluts) w.WriteHeader(500) io.WriteString(w, string(re)) return } w.WriterHeader(r.StatusCode) io.WriteString(w, string(res)) } |
以上,api转发结束
Proxy代理
api转发问题:处理不了一些原生的http请求(如upload传文件,如果透传的话,文件放不到body里面)
直接调用问题:跨域资源共享
proxy作用:直接把8080调用的api,转成9000调用的api
如:http://xxxxxx:8080/upload/:vid-id web的server端会调用xxx:9000/upload/:vid-id
在功能上完成了跨域访问
// web-server端 router.POST("upload/:vid-id", proxyHandler) |
proxyHandler
几个重要的东东:
- net/url
- net/http/httputil(提供了很有用的http的api,但又不是http的标准api)
func proxyHandler(w http.ResponseWriter, r*http.Request, ps httprouter.Router) { // url parse出来 u, _ := url.Parse("http://127.0.0.1:9000/") // 这个生产环境应该写到可下发的配置里面,而不是hard code. proxy := httputil.NewSingleHostReverseProxy(u) proxy.ServeHTTP(w, r) } |
NewSingleHostReverseProxy() 文档
- 高效的做域名替换,后面的路径不会改变
- 没有修改头里面的内容
- 我们在这里做了一个转发,自己写代理的时候,也可以有这个函数做代理,不会对原生的request做侵入式的改变。
如上,web server端逻辑结束。
项目中剩下的内容讲解:前端逻辑和后端逻辑串联