2-4 前端服务

 

概述

前端服务(大前端):

小的网站:

弊端:


前端服务 (不是前台):

→ 前台(Front in 的 UI)

→ WebServer(简单的处理,如:

→ 后端的服务(做真正的业务逻辑处理)

重要的知识点

Go的模板引擎

模板引擎:将HTML解析和元素预置替换,生成最终页面的工具

类似PHP中带tag的部分

比较古老的做法

现代的做法:在HTML里嵌入它的元素,让模板引擎把元素和简单的语言进行替换,最后展现在外面给用户看的前台的部分

Go的模板(相同的interface):

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

解决方案:可以做一些处理,比如:

两种转发模式

proxy模式

把域名做转换,其他请求参数透传

api模式

约定好web端请求参数的格式,按照格式做请求转换 & 转发 可以参考我团iOS直连实践

举例:

  1. 有一个api:http://xxx:8000/api这个api会把body中的格式按照约定规范起来,如
    1. {url:"", method: "", message: ""}
  2. 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)
  1. 把模板和需要渲染到模板中的变量一起执行
  2. 把执行结果放到response writer中
  3. 通过response writer返回给前端,在客户端能展现出来

实现自动化部署脚本

build.sh

作用:

#! /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模式 - 已登录

使用session/cookie判断用户是登录用户

思路:每次进到这个页面,读一下cookie,有木有sessionID和userName

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

几个重要的东东:


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() 文档
  1. 高效的做域名替换,后面的路径不会改变
  2. 没有修改头里面的内容
  3. 我们在这里做了一个转发,自己写代理的时候,也可以有这个函数做代理,不会对原生的request做侵入式的改变。


如上,web server端逻辑结束。


项目中剩下的内容讲解:前端逻辑和后端逻辑串联