@copyright 楊同峰 保留所有權(quán)利
本文可以轉(zhuǎn)載,但請保留版權(quán)信息。
SSH框架配置復(fù)雜、難用。個人認(rèn)為這不是一個框架應(yīng)該有的樣子。框架應(yīng)該使用簡單、配置簡單、代碼簡潔。于是參照Django的一些特性,編寫了這個MVC+ORM框架。
<filter> <filter-name>yangmvc</filter-name> <filter-class>org.docshare.mvc.MVCFilter</filter-class> <init-param> <param-name>controller</param-name> <param-value>org.demo</param-value> </init-param> <init-param> <param-name>template</param-name> <param-value>/view</param-value> </init-param> </filter> <filter-mapping> <filter-name>yangmvc</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <context-param> <param-name>dbhost</param-name> <param-value>localhost</param-value> </context-param> <context-param> <param-name>dbusr</param-name> <param-value>root</param-value> </context-param> <context-param> <param-name>dbpwd</param-name> <param-value>123456</param-value> </context-param> <context-param> <param-name>dbname</param-name> <param-value>mvc_demo</param-value> </context-param> <context-param> <param-name>dbport</param-name> <param-value>3306</param-value> </context-param>
所有需要配置的都在這里了。這里做個簡要說明
MVCFilter是我們MVC框架的入口。(不管是啥MVC框架都免不了這個)
它有controller和template兩個參數(shù)。
controller 是你控制器存放位置的包名。 比如這里是org.demo 你建立的控制器都必須寫在這個包中
template是你存放模板(視圖)的地方。這個路徑是相對于WebRoot即網(wǎng)站根目錄的。
比如這里的配置(/view)是WebRoot下的view目錄。
dbhost dbname dbusr dbpwd 是數(shù)據(jù)庫的 地址、數(shù)據(jù)庫名、用戶名和密碼。目前這個MVC框架只支持MySQL,后續(xù)會添加其他數(shù)據(jù)庫的支持。
注意,模板目錄(template參數(shù)所配置的值)以/開頭,如/view。
public class IndexController extends Controller { public void index(){ output("Hello YangMVC"); } }
他的作用就是顯示一句話。如圖
第零個例子的顯示
IndexController來處理應(yīng)用的根目錄下的請求。 index方法來處理這個目錄下的默認(rèn)請求。
在org.demo包下建立此類:
public class BookController extends Controller { public void index(){ DBTool tool = Model.tool("book"); LasyList list = tool.all().limit(0, 30); put("books", list); render(); } }
在WebRoot/view/book/下建立一個index.jsp
其中核心的代碼為
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> (此處省略一堆無關(guān)的HTML代碼) <table class="table table-bordered"> <c:forEach var="b" items="${books }"> <tr> <td>${b.id }</td> <td>${b.name }</td> <td>${b.author }</td> <td>${b.chaodai }</td> <td>${b.tm_year }</td> <td> <a href='book/edit?id=${b.id}'>編輯</a> <a href='book/del?id=${b.id}'>刪除</a> </td> </tr> </c:forEach> </table>
一個顯示列表的網(wǎng)頁就此搞定。訪問應(yīng)用目錄下的book/目錄即可顯示出結(jié)果
這里寫圖片描述
你作出的結(jié)果可能沒那么好看,這完全取決于css。
在YangMVCDemo / WebRoot / view / book / mvc.css 中有一個漂亮的表格定義。
你可以通過類似下面的語句來加入到網(wǎng)頁中
<link href="view/book/mvc.css" rel="stylesheet">
注意路徑要對。
說明:
這個BookController是一個控制器,它的每一個公共方法都對應(yīng)一個網(wǎng)頁(如果不想對應(yīng),你需要將其設(shè)為私有的)
Model和DBTool是整個ORM框架的核心。Model表示模型,它用來與數(shù)據(jù)庫表相對應(yīng)。在創(chuàng)建一個Model時,會指定對應(yīng)的表名。
這里和Hibernate不同,Hibernate需要預(yù)先生成所有數(shù)據(jù)庫表的對應(yīng)類, 而這個Model可以與任何表格關(guān)聯(lián),而不需要預(yù)先生成任何一個類。 這正是YangMVC中的ORM的優(yōu)勢所在。
DBTool tool = Model.tool("book");
程序中使用Model的靜態(tài)方法tool獲取一個DBTool對象,tool傳入的參數(shù)book是數(shù)據(jù)庫的表名。
這樣DBTool就和book表建立了關(guān)聯(lián)。
LasyList list = tool.all().limit(0, 30);
伙計們快看,這是個LasyList,一個支持懶惰加載機(jī)制的列表。它是List類的子類,這也就是它為什么能在JSTL中使用foreach變量的原因。
首先我們調(diào)用了tool的all()方法,天哪,難道要加載book表的所有數(shù)據(jù),兄弟不用害怕,在這個時候,它并沒有進(jìn)行任何數(shù)據(jù)的讀寫,指示記錄了現(xiàn)在要訪問book表的所有數(shù)據(jù)這一信息。 all()方法會返回一個LasyList對象。這么設(shè)計的原因是我們后面可以跟一連串的過濾方法。方便我們編程。我們可以寫出這樣的東西:
list = tool.all().gt("id", 12).lt("id", 33).eq("name","haha").like("author","王");
這個例子相當(dāng)于執(zhí)行了如下SQL語句:
select * from book where id>12 and id<33 and name='haha' and author like '%王%'
在上面的例子中, all()返回的LasyList又調(diào)用了它的limit方法,這一步仍然沒有真正訪問數(shù)據(jù)庫。
那么訪問數(shù)據(jù)庫從哪里開始呢? 從你獲取這個列表的一項時。
一個List,可以使用枚舉的方法來訪問
for(Model m : list){ }
也可以使用get方法來訪問。如
Model m = list.get(12)
在你訪問具體它的一個元素(Model)時,數(shù)據(jù)庫查詢才會啟動。而且也不是將所有數(shù)據(jù)放到內(nèi)存中。比如你通過上面for的方法枚舉時,其實它是通過ResultSet的next游標(biāo)在移動,所以它很高效!也避免了無用的數(shù)據(jù)庫操作。
put("book",list)
該方法將查詢得到的book塞入request中,在jsp網(wǎng)頁中就可以使用JSTL來使用它。因為它是一個List,所以用forEach去訪問他。
Model 的一個對象對應(yīng)于數(shù)據(jù)庫表的一行(一條記錄),Model是一個Map的子類!!!,所以在JSTL中,你可以使用
${ b.name } 的方式來訪問名為b的Model 的name項。 它相當(dāng)于
Model m = .... m.get("name")
是不是很方便??? 真的是非常方便的。。
添加書籍頁面
public void add(){ DBTool tool = Model.tool("book"); //處理提交數(shù)據(jù) if(isPost()){ //isPost Model m = tool.create(); //創(chuàng)建新的 Log.d(m); paramToModel(m); tool.save(m); put("msg","添加成功"); } //顯示數(shù)據(jù) renderForm(tool.create()); }
對應(yīng)的/view/book/add.jsp (這是默認(rèn)對應(yīng)的模板地址)的核心內(nèi)容
<div style="margin-left:100px"> <h1>添加書籍 ${msg }</h1> ${book_form } </div>
這里寫圖片描述
上面的例子控制器其實是對應(yīng)兩個頁面。 在收到Get請求的時候顯示表單,在用戶提交數(shù)據(jù)時,做插入操作,并顯示表單。(我們當(dāng)然可以把這兩個頁面寫到兩個不同的方法中)
我們還是使用Model.tool獲取一個DBTool。
先來看顯示表單,就一句話
renderForm(tool.create());
tool的create方法會返回一個Model對象,這個對象和book表相關(guān)聯(lián)(因為tool和book表關(guān)聯(lián))。
并將這個Model傳遞給renderForm方法。這個方法會根據(jù)book表格的元數(shù)據(jù)自動創(chuàng)建一個表格。
哇偶!
那么這個Form插入到網(wǎng)頁的什么位置呢? 將 ${book_form } 放入網(wǎng)頁中 即可。
如果來的是POST請求(使用isPost()方法來判斷)
使用tool的create方法創(chuàng)建一個新的Model, 盡快還有其他創(chuàng)建Model對象的方式,但如果你希望插入,請盡量使用這種方式。
paramToModel(m) ,這個方法會自動查找表單中,名字與數(shù)據(jù)庫字段名匹配的項,并自動賦值給Model的相應(yīng)項。是不是很方便。。。
想起了Struts那悲催的功能定義。 淚奔。。。。
隨后直接調(diào)用tool的save方法將其保存到數(shù)據(jù)庫中!OK了!萬事大吉!
細(xì)心的小朋友會問: 數(shù)據(jù)庫中的字段名都是英文的如name,為什么在網(wǎng)頁上顯示的是中文???
看看我的數(shù)據(jù)庫表格定義
CREATE TABLE `book` ( `id` int(11) NOT NULL auto_increment COMMENT '編號', `file_name` varchar(50) default NULL, `name` varchar(50) default NULL COMMENT '名稱', `author` varchar(50) default NULL COMMENT '作者', `chaodai` varchar(50) default NULL COMMENT '朝代', `tm_year` varchar(50) default NULL COMMENT '年代', `about` longtext COMMENT '簡介', `type` varchar(50) default NULL COMMENT '類型', `catalog_id` int(11) default NULL COMMENT '分類', PRIMARY KEY (`id`), KEY `catalog` USING BTREE (`catalog_id`) ) ENGINE=InnoDB AUTO_INCREMENT=912 DEFAULT CHARSET=utf8;
真相大白與天下,我是通過給字段加注釋實現(xiàn)的這一點(diǎn)。只要你將數(shù)據(jù)庫表格加上注釋,它就會自動獲取注釋并顯示,對于沒有注釋的字段,則會顯示字段名。如那個扎眼的file_name
好了,這幾行代碼就搞定了輸入表單和表單的處理。
細(xì)心的朋友發(fā)現(xiàn),我們是按照CRUD的邏輯來將的。下面是編輯網(wǎng)頁。
public void edit() throws NullParamException{ DBTool tool = Model.tool("book"); //處理提交數(shù)據(jù) if(isPost()){ //isPost Model m = tool.get(paramInt("id")); Log.d(m); paramToModel(m); tool.save(m); put("msg","修改成功"); } //顯示數(shù)據(jù) Integer id = paramInt("id"); checkNull("id", id); renderForm(tool.get(id)); }
HTML頁面放在/view/book/edit.jsp中,核心代碼只是將add.jsp中的添加二字改為了"編輯“二字。
<div style="margin-left:100px"> <h1>編輯書籍 ${msg }</h1> ${book_form } </div>
這個代碼長了一點(diǎn), 有17行。對于用YangMVC的,已經(jīng)算夠長的了。它仍然是兩個網(wǎng)頁!!!
你可以吧顯示表單的代碼和處理表單的分到兩個方法中寫。
先看顯示數(shù)據(jù)。 首先使用paramInt方法獲取URL參數(shù)id,我們就是要編輯id指定的書籍。
調(diào)用checkNull來檢查一下。 在我的開發(fā)生涯中,遇到各種參數(shù)檢查,所以這個功能是必須有的,如果checkNull不過,就會拋出一個異常。 這樣做的目的是不要讓這種參數(shù)檢查干擾我們正常的邏輯。這不就是異常之所以存在的意義么?
如果缺少這個參數(shù),頁面會提示說缺少這個參數(shù)。
下面使用tool.get(id)方法來獲取一個Model(一條記錄)。這個方法是根據(jù)表格的主鍵進(jìn)行查詢,返回的不是列表而是一個具體的Model對象。在這里我建議主鍵應(yīng)當(dāng)是整數(shù)、且是數(shù)據(jù)庫自增的。
renderForm傳入一個model,這個model中有數(shù)據(jù),就會被顯示出來。
就這樣。編輯功能寫好了。
有的朋友問,如果不想用默認(rèn)的表單怎么辦? 那你自己寫一個表單在你的模板里就是了。只不過,你可以先用這個方法吧表單生成出來,然后按你的意圖修改就成了。這也節(jié)省大量時間啊。做過Form的請舉手。
public void del(){ Integer id = paramInt("id"); Model.tool("book").del(id); jump("index"); }
瞧瞧就這點(diǎn)代碼了, 獲取參數(shù)id,并調(diào)用tool的del方法刪除。最后一句我們第一次見,就是跳轉(zhuǎn)。跳轉(zhuǎn)到同目錄下的index這個默認(rèn)頁(顯示的是書籍列表)
控制器是一個Java類,類有若干方法。在YangMVC的設(shè)計中,控制器的每一個公共的方法都映射對應(yīng)一個網(wǎng)頁。這樣一個Java類可以寫很多的網(wǎng)頁。 方便管理。(當(dāng)然,你也可以在一個控制器中只寫一個方法來支持網(wǎng)頁,這沒問題(⊙﹏⊙)b)
所有的控制器都要繼承 org.docshare.mvc.Controller 這個類。充當(dāng)控制器方法的方法應(yīng)當(dāng)是沒有參數(shù)沒有返回值的。如上面demo所示。
public class IndexController extends Controller { public void index(){ output("Hello YangMVC"); } }
這些控制器都要寫在配置所制定的package中,或者子package中。如在上面的配置中
<init-param> <param-name>controller</param-name> <param-value>org.demo</param-value> </init-param>
這個包為org.demo所有的控制器都要卸載這個包內(nèi)。(你可以寫到外面,但它不會管用O(∩_∩)O~)
所謂路徑映射就是要將 一個控制器(一個Java類)和一個網(wǎng)址建立關(guān)聯(lián)。 用戶訪問某網(wǎng)址時,框架自動調(diào)用控制器的某個函數(shù)。
因為本框架設(shè)計思想希望配置盡可能少,所以這里的路徑映射是通過命名關(guān)系的。
假設(shè)應(yīng)用的根目錄為
http://localhost:8080/YangMVC/
如在org.demo下(這個目錄可以在web.xml中配置,可見上一節(jié))有一個BookController。
那么這個類的路徑是 http://localhost:8080/YangMVC/book/
用戶訪問這個路徑時,框架會調(diào)用BookController 的index方法。如果沒有這個方法則會報錯。
index方法用以處理某個路徑下的默認(rèn)網(wǎng)頁(網(wǎng)站以斜杠結(jié)尾的都會調(diào)用某個類的index方法來處理)。
book這個地址,將第一個字母大寫,后面追加Controller。于是
book (路徑名)-> Book -> BookController(類名)
這就是路徑和類名的默認(rèn)關(guān)聯(lián)。
在這個網(wǎng)站后加入方法名可以訪問BookController的 任何一個公共方法。
如 http://localhost:8080/YangMVC/book/edit 與BookController的edit方法關(guān)聯(lián)。
需要注意的是,如果你寫的是 http://localhost:8080/YangMVC/book/edit/ (比上一個網(wǎng)站多了一個斜杠), 則它對應(yīng)的是 book.EditController下的index方法 而不是BookController下的edit方法。
獲取request中的參數(shù)
String s = param("name"); Integer id = paramInt("id");
輸出方法
output方法
output("Hello YangMVC");
這個方法輸出一個文本到網(wǎng)頁上(輸出流中),并關(guān)閉輸出流。因為它會關(guān)閉流,所以你不要調(diào)用它兩次。你如果需要輸出多次,以將內(nèi)容放到StringBuffer中,然后統(tǒng)一輸出。
render方法
public void paramDemo(){ put("a", "sss"); render("/testrd.jsp"); }
這里的testrd.jsp是模板目錄(/view)目錄下的。 /view/testrd.jsp
這里的參數(shù)應(yīng)該是相對于模板目錄的相對路徑。
render方法使用參數(shù)制定的網(wǎng)頁(一個包含JSTL的jsp文件),將其輸出。可以通過put來制定參數(shù)。下面會詳細(xì)講。
render()方法
這個render方法是沒有參數(shù)的,它會使用默認(rèn)模板,如果這個模板不存在,就會提示錯誤。 public void renderDemo(){ request.setAttribute("a", "sss"); render(); }
在配置 controller 為org.demo , template為/view 這種情況下。
org.demo.IndexController的renderDemo方法會對應(yīng)/view/renderDemo.jsp
之所以模板存在于模板根目錄下,是因為這個IndexController是處理應(yīng)用根目錄的。他們有對應(yīng)關(guān)系。
如果是org.demo.BookController,它對應(yīng) app根目錄下的 /book/ 目錄。
它的add方法對應(yīng)路徑 /book/add
如果應(yīng)用名為hello,那么完成路徑應(yīng)該是 /hello/book/add
outputJSON 方法
該方法將參數(shù)轉(zhuǎn)化為JSON,并向網(wǎng)頁輸出。
public void jsonDemo(){ Map<String, Object> map = new HashMap<String, Object>(); map.put("id", 12); map.put("name", "Yang MVC"); map.put("addtm",new Date()); outputJSON(map); } 這個代碼稍長,其實上面的所有都是生成一個Map,最后一句輸出。outputJSON可以輸出List,Map和任何Java對象。內(nèi)部轉(zhuǎn)換是使用fastjson實現(xiàn)的。
自動生成并輸出一個表單
public void renderForm(Model m,String template,String postTo)
該函數(shù)會根據(jù)模型對應(yīng)的表結(jié)構(gòu),自動生成一個表單,并將其內(nèi)容放入 表格名_form 中,如book表會輸出到 book_form 中。
在網(wǎng)頁中,直接寫 ${book_form}就可以將表單放下去。
template制定對應(yīng)的模板文件,可以省略,省略后按照默認(rèn)規(guī)則查找模板文件。
postTo設(shè)定 表單提交的網(wǎng)頁,可以省略,默認(rèn)是"",即當(dāng)前網(wǎng)頁(Controller)。
獲取參數(shù)的方法
檢查方法
public void checkNull(String name,Object obj)
檢查obj是否為null,如果是拋出NullParamException異常。
Model與DBTool
Model 對象對應(yīng)數(shù)據(jù)庫的表格,它會與一個表格進(jìn)行綁定。DBTool相當(dāng)于是它的DAO類。
YangMVC的ORM組件可以單獨(dú)使用。使用前需要先配置數(shù)據(jù)庫:
Config.dbhost = "localhost"; Config.dbname = "dc2"; Config.dbpwd = "123456"; Config.dbusr ="root"; Config.dbport="3306";
也可以和MVC框架一起使用。配置時在web.xml中配置
創(chuàng)建一個DBTool對象
DBTool tool = Model.tool("book");
其中book是數(shù)據(jù)庫表的名字。
創(chuàng)建一個空的Model
DBTool tool = Model.tool("book");
Model m = tool.create(); //創(chuàng)建新的
根據(jù)主鍵讀取一個Model
Model m = tool.get(12);
查詢表中所有的行
LasyList list = tool.all(); all返回一個LasyList對象。這個對象在此事并沒有真正進(jìn)行數(shù)據(jù)庫查詢,只有在頁面真正讀取時才會讀取數(shù)據(jù)庫。這是它叫做Lasy的原因。此處借鑒了Django的實現(xiàn)機(jī)制。
查詢的limit語句
LasyList list = tool.all().limit(30); list = tool.all().limit(10,30);
查詢的等式約束
tool.all().eq("name","本草綱目")
查詢的不等式約束
tool.all().gt("id",12) //id < 12 tool.all().lt("id",33) //id <33 tool.all().gte("id",12) //id>=12 tool.all().lte("id",33) //id<=33 tool.all().ne("id",33) //不相等
模糊查詢
tool.all().like("name","本草") 查找所有名字中包含本草的書。返回一個LasyList
排序
tool.all().orderby("id",true); 按照id的增序排列。 如果是false,則是降序。
級聯(lián)查詢
因為這些上面的過濾器函數(shù)全部都會返回一個LasyList對象, 所以可以采用級聯(lián)的方式進(jìn)行復(fù)雜查詢。如:
list = tool.all().gt("id", 12).lt("id", 33).eq("name","haha").like("author","王");
這個例子相當(dāng)于執(zhí)行了如下SQL語句:
select * from book where id>12 and id<33 and name='haha' and author like '%王%'
根據(jù)原始sql獲取(version >=1.5.4)
LasyList list = LasyList.fromRawSql("select name from book");
使用原始的sql獲取的List中的模型將和數(shù)據(jù)庫表沒有關(guān)聯(lián)。
Model的相關(guān)功能
model 是一個繼承自Map<String,Object> 的類,所以對于
Model m;
你可以在網(wǎng)頁中使用${m.name}的方式來訪問它的name鍵對應(yīng)的值。相當(dāng)于m.get("name")
這種寫法在JSTL中非常有用。讓Model繼承Map的初衷就在于此:方便在JSTL中使用。
大家也許注意到了LasyList是一個繼承自List<Model> 的類.
這就使得不管是LasyList還是Model在JSTL中訪問都極為的便利。
訪問所有的鍵值(即DAO對象的所有屬性)
model.keySet();
訪問某一個屬性的值
model.get(key)
設(shè)置某一個屬性的值
多 ASP.NET 開發(fā)人員開始接觸 MVC,都認(rèn)為 MVC 與 ASP.NET 完全沒有關(guān)系,是一個全新的 Web 開發(fā)。
事實上 ASP.NET 是創(chuàng)建 WEB 應(yīng)用的框架,而 MVC 是一種能夠用更好的方法來組織并管理代碼的體系,所以可以稱之為 ASP.NET MVC。
因此,我們可以將原來的 ASP.NET 稱為 ASP.NET Webforms,新的 MVC 稱為 ASP.NET MVC。
ASP.NET Webforms
ASP.NET 在過去的十幾年里,已經(jīng)服務(wù)并成功實現(xiàn)Web 應(yīng)用的開發(fā)。那么,我們先了解一下為什么ASP.NET能夠如此流行,并成功應(yīng)用?
微軟編程語言從 VB 開始就能夠成為流行并廣泛應(yīng)用,都源于其提供的強(qiáng)大的 Visual studio 能夠進(jìn)行可視化的編程,實現(xiàn)快速開發(fā)。
使用 VS 時,開發(fā)人員能夠通過拖拽 UI 元素,并在后臺自動生成這些界面的代碼,稱為后臺代碼。在后臺代碼中,開發(fā)人員可以添加操作這些UI元素的邏輯代碼。
因此微軟的可視化 RAD 架構(gòu)體系有兩方面組成,一方面是 UI,一方面是后臺代碼。
ASP.NET WebForms 存在的問題
響應(yīng)時間
如圖所示,每一次 WebForms 請求都有轉(zhuǎn)換邏輯,運(yùn)行并轉(zhuǎn)換服務(wù)器控件為 HTML 輸出。如果頁面使用表格,樹形控件等復(fù)雜控件,轉(zhuǎn)換就會變得很糟糕,HTML 輸出也是非常復(fù)雜的。由于這些不必要的轉(zhuǎn)換從而增加了響應(yīng)時間。上圖是 ASP.Net MVC 和 Webforms 的響應(yīng)時間對比,我們會發(fā)現(xiàn) Webforms 的響應(yīng)時間是 MVC 的兩倍左右。
帶寬消耗
ASP.NET 開發(fā)人員都非常熟悉 Viewstates,因為它能夠自動保存 post 返回的狀態(tài),減少開發(fā)時間。但是這種開發(fā)時間的減少會帶來巨大的消耗,Viewstate增加了頁面的大小。
從上圖中,我們可以看到與 MVC 對比,Viewstate 增加了兩倍的頁面存儲。
MVC是怎么彌補(bǔ)這些問題的?
Asp.Net MVC 由 Model,View,Controller 三部分組成。Controller 中包含后臺代碼邏輯,View 是ASPX,如純 HTML 代碼,Model 是中間層。
不使用服務(wù)器控件,直接編寫 HTML 代碼,并且將后臺代碼遷移到獨(dú)立的類庫中,是 MVC 解決 Webforms 問題的方法。
直接編寫HTML代碼的好處在于,web設(shè)計者可以與開發(fā)人員緊密合作及時溝通,設(shè)計人員也可以使用他們喜愛的設(shè)計工具來設(shè)計HTML代碼。
將后臺代碼遷移到獨(dú)立的簡單的類庫,執(zhí)行效率也會大大提高。
ASP.NET Webform 和 MVC 比較,如上圖所示。
深入理解 ASP.NET MVC 今天就講到這里,后續(xù)還會更新 “七天學(xué)會 ASP.NET MVC” 的其它篇章。
敬請期待!
相關(guān)開發(fā)工具
要進(jìn)行 ASP.ET MVC 的開發(fā),不但需要具備 MVC 的知識,還需要高效的工具來幫助開發(fā)。
使用 ComponentOne Studio Enterprise 中提供的 ComponentOne Studio ASP.NET MVC,您能獲取快速的輕量級控件來滿足用戶所有需求,大大減輕工作量。
快人一步,免費(fèi)試用
如果您想試用 ComponentOne Studio ASP.NET MVC,請聯(lián)系我們:
微信:GrapeCityDT
郵件:marketing.xa@grapecity.com
官網(wǎng):www.gcpowertools.com.cn
關(guān)于葡萄城控件
葡萄城是一家跨國軟件研發(fā)集團(tuán),專注控件領(lǐng)域近30年,是全球最大的控件提供商,也是微軟認(rèn)證的金牌合作伙伴
下文章來源于非正式解決方案 ,作者winlion
非正式解決方案
思考鏈接價值,非正式解決方案,既扯高大上如人工智能、大數(shù)據(jù),也關(guān)注碼農(nóng)日常如分布式、java和golang,每天分享瞎想的東西。
目錄結(jié)構(gòu)說明如下
名稱內(nèi)容model模型層目錄,類比Java 中的entityview視圖層,存放所有templete模板ctrl控制器層, 存放全部控制器service服務(wù)層,類比Java里面的servicehtml一些靜態(tài)資源頁面util核心工具包,Md5加密,返回數(shù)據(jù)封裝等asset靜態(tài)資源目錄,存放js/css/image等args封裝全部請求參數(shù)對象mnt上傳文件的存放目錄app.dev.conf開發(fā)環(huán)境配置文件app.prod.conf生產(chǎn)環(huán)境配置文件start.sh/start.bat啟動腳本build.sh/build.bat打包腳本main.go主應(yīng)用程序文件
主程序主要做各種初始化工作
func main() {
//解析配置文件
fpath := flag.String("c","app.dev.conf","config file path")
flag.Parse()
_,err:=util.Parse(*fpath)
if err!=nil{
fmt.Sprintf("error when %s",err.Error())
return
}
//配置日志
logmap := util.GetSec("log")
service.InitLog(logmap)
//初始化數(shù)據(jù)庫
dbmap := util.GetSec("database")
service.InitDb(dbmap)
//注冊funcmap
ctrl.RegisterFuncMap()
//控制器
ctrl.RegisterCtrl()
//靜態(tài)資源文件
fileservermap := util.GetSec("fileserver")
ctrl.InitFileServer(fileservermap)
//初始化session
sessioncfg:=util.GetSec("session")
util.StartSession(sessioncfg)
appcfg := util.GetSec("app")
//視圖控制器
ctrl.RegisterView(appcfg)
fmt.Println("http ListenAndServe " + appcfg["addr"])
//打開服務(wù)器監(jiān)聽http
err = http.ListenAndServe(appcfg["addr"], nil)
if err!=nil{
fmt.Println(err.Error())
log.Println(err.Error())
}
}
使用配置文件開發(fā)包,如github.com/Unknwon/goconfig 包。
//util/config.go
var cfg *goconfig.ConfigFile
var cfgmap map[string]map[string]string = make(map[string]map[string]string)
var filepath string
//解析peiz
func Parse(fpath string)(c map[string]map[string]string ,err error){
cfg, err := goconfig.LoadConfigFile(fpath)
filepath = fpath
sec :=cfg.GetSectionList()
for _,v :=range sec{
cfgmap[v]=make(map[string]string,0)
keys := cfg.GetKeyList(v)
for _,b:= range keys{
cfgmap[v][b],_ = cfg.GetValue(v,b)
}
}
return cfgmap,err
}
//全部都存放在存放
func GetAllCfg()(c map[string]map[string]string){
return cfgmap
}
//重新刷新配置文件
func ReloadAllCfg()(c map[string]map[string]string){
return return Parse(filepath)
}
調(diào)用案列
util.GetAllCfg()["app"]["port"]
使用github.com/fsnotify/fsnotify包,裝時候注意,一個函數(shù)里面如果有參數(shù)共享,應(yīng)該放到一個攜程里。
//監(jiān)聽文件
func WatchConfig(filepath ...string) {
//創(chuàng)建一個監(jiān)控對象
go func() {
watch, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
defer watch.Close()
//添加要監(jiān)控的對象,文件或文件夾
for _, fpath := range filepath {
err = watch.Add(fpath)
if err != nil {
log.Fatal(err)
}
fmt.Println("WatchConfig " + fpath)
}
for {
select {
case ev := <-watch.Events:
{
if ev.Op&fsnotify.Write == fsnotify.Write {
//監(jiān)聽到文件系統(tǒng)使用加載新東西
ReloadAllCfg()
}
fmt.Println(ev.Op, ev.Name)
}
case err := <-watch.Errors:
{
log.Println("error : ", err)
return
}
}
}
}()
}
fsnotify 支持很多種事件監(jiān)聽,一般在 Write 事件刷新配置文件
//判斷事件發(fā)生的類型,如下5種
// Create 創(chuàng)建
// Write 寫入
// Remove 刪除
// Rename 重命名
// Chmod 修改權(quán)限
if ev.Op&fsnotify.Create == fsnotify.Create {
log.Println("創(chuàng)建文件 : ", ev.Name);
}
if ev.Op&fsnotify.Write == fsnotify.Write {
log.Println("寫入文件 : ", ev.Name);
}
if ev.Op&fsnotify.Remove == fsnotify.Remove {
log.Println("刪除文件 : ", ev.Name);
}
if ev.Op&fsnotify.Rename == fsnotify.Rename {
log.Println("重命名文件 : ", ev.Name);
}
if ev.Op&fsnotify.Chmod == fsnotify.Chmod {
log.Println("修改權(quán)限 : ", ev.Name);
}
系統(tǒng)級配置參數(shù) 假設(shè)修改了會影響整個應(yīng)用,需要另起服務(wù)的我們稱之為系統(tǒng)級配置,修改了參數(shù),往需要進(jìn)行相應(yīng)的操作。如修改了數(shù)據(jù)庫連接地址,需要重置數(shù)據(jù)庫連接操作。修改了應(yīng)用服務(wù)器端口,則需要重啟應(yīng)用服務(wù)。
用戶級配置參數(shù) 如微信公眾號 appsecret,每次調(diào)用的時候會從配置中獲取,因此只需要重新加載數(shù)據(jù)即可。
需要將配置文件內(nèi)容緩存到 map 中。 需要考慮到 map 的并發(fā)操作。
//model/user.go
//用戶性別和角色
const (
WOMEN=2
MAN=1
Unknow=0
ROLE_ADMIN =1
ROLE_USER=0
)
type User struct {
Id int64 `xorm:"pk autoincr BIGINT(20)" form:"id" json:"id"`
NickName string `xorm:"VARCHAR(40)" form:"nickName" json:"nickName"`
Openid string `xorm:"VARCHAR(40)" form:"openid" json:"openid"`
Mobile string `xorm:"VARCHAR(15)" form:"mobile" json:"mobile"`
Passwd string `xorm:"VARCHAR(40)" form:"passwd" json:"-"`
Role int `xorm:"int(11)" form:"role" json:"role"`
Enable int `xorm:"int(11)" form:"enable" json:"enable"`
Gender int `xorm:"int(11)" form:"gender" json:"gender"`
}
在如上代碼中,常用角色變量 ROLE_USER和ROLE_ADMIN 定義在同一個文件中,便于閱讀。
實體和表結(jié)構(gòu)對應(yīng),一定要定義 Form 和 Json tag。這樣可以提高系統(tǒng)適用性,為什么呢?因為可以適配前端以各種 Content-Type 提交數(shù)據(jù)如。后端統(tǒng)一用該實體接收數(shù)據(jù)即可。
//urlencode類
application/x-www-form-urlencoded格式
mobile=18273252300&passwd=123456
//json類
application/x-www-form-urlencoded格式
{"mobile":"18273252315","passwd":"123456"}
約定統(tǒng)一使用駝峰式或者下劃線標(biāo)記。如下,建議使用駝峰式。
#駝峰
NickName string `xorm:"VARCHAR(40)" form:"nickName" json:"nickName"`
#下劃線
NickName string `xorm:"VARCHAR(40)" form:"nick_name" json:"nick_name"`
如下幾點(diǎn)需要注意
關(guān)聯(lián)便于代碼管理和閱讀。模板位置 /view/demo/index.html,模板內(nèi)容如下。
{{define "demo/index"}}
<div>
Hello,Modal
</div>
{{end}}
外部調(diào)用方法如下,大家能很自然知道模板文件位置。
http://localhost/demo/index
主要是為了程序員生活更美好(早點(diǎn)下班+偷懶)。
//ctrl/base.go
func RegisterPage(isDev bool) {
//初始化一個全局的模板變量
GlobTemplete := template.New("root")
//把一些函數(shù)添加進(jìn)去,這樣頁面里面就可以使用函數(shù)啦
GlobTemplete.Funcs(GetFuncMap())
//解析模板 ,demo/index => 模板
GlobTemplete, err := GlobTemplete.ParseGlob("view/**/*")
for _, templete := range GlobTemplete.Templates() {
tplname := templete.Name()
patern := "/" + tplname
fmt.Printf("register templete %s ==> %s\n", patern, tplname)
//這里就是 /demo/index 這個url 和對應(yīng)的處理函數(shù)之間的關(guān)系
http.HandleFunc(patern, func(w http.ResponseWriter, req *http.Request) {
fmt.Println(patern + "=>" + tplname)
if isDev {
GlobTemplete := template.New("root")
GlobTemplete.Funcs(GetFuncMap())
GlobTemplete, err = GlobTemplete.ParseGlob("view/**/*")
for _, v := range GlobTemplete.Templates() {
if v.Name() == tplname {
templete = v
}
}
}
err = templete.ExecuteTemplate(w, tplname, nil)
if err != nil {
fmt.Println(err.Error())
}
})
}
}
//在main.go中初始化
func main(){
///
ctrl.RegisterPage(true)
//
}
外部調(diào)用方法如下,大家能很自然知道模板文件位置。
http://localhost/demo/index
為什么要添加調(diào)試模式支持?因為調(diào)試模式狀態(tài)下,我們修改來了頁面模板,需要立即看到頁面內(nèi)容,而不需要重啟應(yīng)用。核心代碼如下,即在調(diào)試模式狀態(tài)下,每次請求都重新解析模板。
if isDev {
GlobTemplete := template.New("root")
GlobTemplete.Funcs(GetFuncMap())
GlobTemplete, err = GlobTemplete.ParseGlob("view/**/*")
for _, v := range GlobTemplete.Templates() {
if v.Name() == tplname {
templete = v
}
}
}
由上可見,調(diào)試模式效率是非常低的,我們不應(yīng)該在生產(chǎn)環(huán)境采用調(diào)試模式。
應(yīng)用場景是在每個頁面中都需要使用 session 中的用戶 ID 數(shù)據(jù)。方法是在 RegisterPage 函數(shù)內(nèi)部模板templete.ExecuteTemplate(w, tplname, nil)處秀修改成如下代碼
//從session中獲取用戶信息
user := loadDataFromSession(req)
err = templete.ExecuteTemplate(w, tplname, user)
前端模板調(diào)用代碼如下
{{define "demo/index"}}
<div>
Hello,Modal ,User id is {{.Id}}
</div>
{{end}}
返回結(jié)果
Hello,Modal ,User id is xxx
在 RegisterPage 方法內(nèi)定義一個 funMap
//ctrl/funcmap.go
var resFuncMap template.FuncMap = make(template.FuncMap)
func hello (){
return "hello"
}
func hello2 (test string){
return "hello" + test
}
//初始化方法
func RegisterFuncMap(){
resFuncMap ["hello"]=hello
}
main.go 中初始化
//在main.go中初始化
func main(){
///
ctrl.RegisterFuncMap()
//
}
前端模板調(diào)用代碼如下
{{define "demo/index"}}
<div>
Hello,Modal ,hello func retutn {{hello}}
Hello,Modal ,hello2 func retutn {{hello2 "參數(shù)2"}}
</div>
{{end}}
返回結(jié)果
Hello,Modal ,hello func retutn hello
Hello,Modal ,hello func retutn hello2參數(shù)2
主要使用場景是分角色菜單,用戶
{{define "demo/memo"}}
{{if eq .Role 1}}
菜單內(nèi)容1
{{else if eq .Role 2}}
菜單內(nèi)容2
{{end}}
<script>
GLOB={"ROLE":.Role}
</script>
{{end}}
其他頁面統(tǒng)一調(diào)用,進(jìn)行角色菜單等控制。
{{define "demo/index"}}
<div>
{{templete "demo/menu"}}
Hello,Modal ,hello func retutn {{hello}}
Hello,Modal ,hello2 func retutn {{hello2 "參數(shù)2"}}
</div>
{{end}}
控制器層主要處理對外接口
import (
///很多包
)
//定義個一個控制器對象
type UserCtrl struct {
}
//將url和處理函數(shù)綁定
func ( ctrl *UserCtrl)Router(){
Router("/user/login",ctrl.authwithcode)
}
//定義用戶處理函數(shù)
var userService service.UserService
//用戶通過小程序登錄處理函數(shù),輸入code
//通過util.RespOk 或者util.RespFail輸出
func( ctrl * UserCtrl)authwithcode(w http.ResponseWriter, req *http.Request) {
var requestdata args.AuthArg
util.Bind(req,&requestdata)
cfgminapp := util.GetSec("miniapp")
resp,err := util.Code2Session(cfgminapp["appid"],cfgminapp["secret"],requestdata.Code)
if err!=nil{
util.RespFail(w,err.Error())
return
}
requestdata.User.Openid = resp.Openid
requestdata.User.SessionKey = resp.SessionKey
u,err:= userService.LoginWithOpenId(requestdata.User)
if err!=nil{
util.RespFail(w,err.Error())
}else{
util.RespOk(w,model.User{
Ticket:u.Ticket,
ClientId:u.ClientId,
Role:u.Role,
})
}
}
所有參數(shù)都需要可預(yù)期在一個結(jié)構(gòu)體里面。這樣整個系統(tǒng)編程將變得非常簡單。在上 面函數(shù)中,通過如下代碼實現(xiàn)參數(shù)綁定
var requestdata args.AuthArg
util.Bind(req,&requestdata)
其中 args.AuthArg 對象定義如下
package args
import "../model"
type AuthArg struct {
PageArg
model.User
Code string `json:"code" form:"code"`
Kword string `json:"kword" form:"kword"`
Passwd string `json:"passwd" form:"passwd"`
}
args 作用是存放一切請求參數(shù)。每個業(yè)務(wù)都建議定義一個 arg。每個 arg 都有一個公共屬性 PageArg。PageArg 定義如下
import (
"fmt"
"time"
)
//常用的搜索,大家可以自行添加
type PageArg struct {
Pagefrom int `json:"pagefrom" form:"pagefrom"`
Pagesize int `json:"pagesize" form:"pagesize"`
Kword string `json:"kword" form:"kword"`
Asc string `json:"asc" form:"asc"`
Desc string `json:"desc" form:"desc"`
Stat int `json:"stat" form:"stat"`
Datefrom time.Time `json:"datafrom" form:"datafrom"`
Dateto time.Time `json:"dateto" form:"dateto"`
Total int64 `json:"total" form:"total"`
}
//獲得分頁大小
func (p*PageArg) GetPageSize() int{
if p.Pagesize==0{
return 100
}else{
return p.Pagesize
}
}
//獲得分頁當(dāng)前第幾頁
func (p*PageArg) GetPageFrom() int{
if p.Pagefrom<0{
return 0
}else{
return p.Pagefrom
}
}
//獲得排序 ID DESC ,前端傳遞參數(shù) desc=排序字段 或者asc=排序字段
func (p*PageArg) GetOrderBy() string{
if len(p.Asc)>0{
return fmt.Sprintf(" %s asc",p.Asc)
} else if len(p.Desc)>0{
return fmt.Sprintf(" %s desc",p.Desc)
}else{
return ""
}
}
大體結(jié)構(gòu)如下
func Bind(req *http.Request,obj interface{}) error{
contentType := req.Header.Get("Content-Type")
//如果是簡單的json,那么直接用JSON解碼
if strings.Contains(strings.ToLower(contentType),"application/json"){
return BindJson(req,obj)
}
//如果是其他的urlencode那么就用BindForm去處理
if strings.Contains(strings.ToLower(contentType),"application/x-www-form-urlencoded"){
return BindForm(req,obj)
}
//可以自行擴(kuò)展xml
if strings.Contains(strings.ToLower(contentType),"text/xml"){
return BindXml(req,obj)
}
return errors.New("當(dāng)前方法暫不支持")
}
以 BindJson 為例子
func BindJson(req *http.Request,obj interface{}) error{
s, err := ioutil.ReadAll(req.Body) //把 body 內(nèi)容讀入字符串
if err!=nil{
return err
}
err = json.Unmarshal(s,obj)
return err
}
可能大家更關(guān)心 BindForm,篇幅太長,大家可以移步
https://www.github/winlion/restgo-admin
一般封裝一個底層 JSON,然后根據(jù)返回成功或失敗響應(yīng)對應(yīng)的 code
/util/resp.go
package util
import (
"net/http"
"encoding/json"
"fmt")
//定義個通用的結(jié)構(gòu)體用于裝載返回的數(shù)據(jù)
type H struct {
Code int `json:"code"`
Rows interface{} `json:"rows,omitempty"`
Data interface{} `json:"data,omitempty"`
Msg string `json:"msg,omitempty"`
Total interface{} `json:"total,omitempty"`
}
//返回Json的底層方法
func RespJson(w http.ResponseWriter,data interface{}){
header :=w.Header()
header.Set("Content-Type","application/json;charset=utf-8")
w.WriteHeader(http.StatusOK)
ret,err :=json.Marshal(data)
if err!=nil{
fmt.Println(err.Error())
}
w.Write(ret)
}
//當(dāng)操作成功返回Ok,
func RespOk(w http.ResponseWriter,data interface{}){
RespJson(w,H{
Code:http.StatusOK,
Data:data,
})
}
//當(dāng)操作失敗返回Error,
func RespFail(w http.ResponseWriter,msg string){
RespJson(w,H{
Code:http.StatusNotFound,
Msg :msg,
})
}
以訂單管理為例
package service
import (
"../model"
"../args"
"github.com/go-xorm/xorm"
"log"
"github.com/pkg/errors"
"encoding/json"
"time"
)
type OrderService struct {
}
//構(gòu)造條件
func (service *OrderService)buildCond(arg args.PageArg)(*xorm.Session){
orm := DBengin.Where("id > ?",0)
if(!arg.Datefrom.IsZero()){
orm = orm.And("createat >= ?",arg.Datefrom.String())
}
if(!arg.Dateto.IsZero()){
orm = orm.And("createat <to ?",arg.Datefrom.String())
}
if (arg.Seller>0){
orm = orm.And("seller = ?",arg.Seller)
}
if (arg.Buyer>0){
orm = orm.And("buyer = ?",arg.Buyer)
}
if (arg.Stat>0){
orm = orm.And("stat = ?",arg.Stat)
}
return orm
}
//增加
func (service *OrderService) Create(order model.Order) (model.Order,err){
_,err = DBengin.InsertOne(&order)
if err!=nil{
log.Println(err.Error())
}
return order
}
//刪除
func (service *OrderService) Delete(order model.Order) (error){
return nil
}
//修改
func (service *OrderService) Create(order model.Order) (model.Order,err){
_,err = DBengin.InsertOne(&order)
if err!=nil{
log.Println(err.Error())
}
return order
}
//搜索
func (service *OrderService) Search(orderArg args.OrderArg) ([]model.Order, error){
var ret []model.Order = make([]model.Order,0)
return ret,nil
}
//查詢某一個
func (service *OrderService) Create(order model.Order) (model.Order){
_,err := DBengin.InsertOne(&order)
if err!=nil{
log.Println(err.Error())
}
return order
}
一般需要構(gòu)建如下幾類函數(shù),具體隨業(yè)務(wù)而定
名稱內(nèi)容Create添加Update修改Search搜索,返回列表Find返回某一個對象Delete刪除buildCond構(gòu)建條件函數(shù)Count符合某一條件的記錄數(shù)目
我們可以用類似于如下函數(shù)來統(tǒng)一管理查詢條件,該函數(shù)輸出參數(shù),輸出一個 session。
func (service *OrderService)buildCond(arg args.PageArg)(*xorm.Session)
條件規(guī)范化可以讓應(yīng)用更靈活,讓業(yè)務(wù)更清晰。如果不規(guī)范,樓主曾經(jīng)經(jīng)歷的教訓(xùn)可能也會撞上你。
數(shù)據(jù)庫建議使用 xorm。 在 server 包目錄下新建 init.go 在其中實現(xiàn)數(shù)據(jù)庫的初始化
//定義全局變量DBengin
var DBengin *xorm.Engine
//定義初始化函數(shù)InitDb,dbmap是數(shù)據(jù)庫配置參數(shù),=來自于外部參數(shù)
func InitDb(dbmap map[string]string){
driverName := dbmap["driveName"]
dataSourceName := dbmap["dataSourceName"]
showsql := dbmap["showSql"]!="false"
maxIdle,_ := strconv.Atoi(dbmap["maxIdle"])
maxOpen,_ := strconv.Atoi(dbmap["maxOpen"])
sync := dbmap["sync"]=="true"
dbengin , err := xorm.NewEngine(driverName, dataSourceName)
if err != nil {
panic("data source init error ==>"+err.Error())
}
if sync{
dbengin.Sync2(new(model.User),
new(model.Item),
new(model.Order),
new(model.User),
)
}
dbengin.ShowSQL(showsql)
dbengin.SetMaxIdleConns(maxIdle)
dbengin.SetMaxOpenConns(maxOpen)
dbengin.SetConnMaxLifetime(5*time.Second)
DBengin = dbengin
}
main.go 中初始化數(shù)據(jù)庫
func main(){
//
dbmap = util.GetSec("database")
server.InitDb(dbmap)
//
}
具體使用可以參考 Xorm
func (service *OrderService) Create(order model.Order) (model.Order){
//就是這么用的
_,err := DBengin.InsertOne(&order)
if err!=nil{
log.Println(err.Error())
}
return order
}
在每一個 ctrl 中都定義一個 Router 函數(shù)
func ( ctrl *UserCtrl)Router(){
Router("/open/register",ctrl.Register)
Router("/open/authwithpwd",ctrl.authwithpwd)
Router("/user/find",ctrl.Find)
Router("/user/quit",ctrl.quit)
Router("/open/authwithcode",ctrl.authwithcode)
}
這些函數(shù)調(diào)用了 Router 方法,該方法本質(zhì)上是對 http.HanderFunc 的封裝
//ctrl/base.go
func Router(pantern string, fun func(w http.ResponseWriter, req *http.Request)) {
http.HandleFunc(pantern, func(w http.ResponseWriter, req *http.Request) {
fun(w, req)
})
}
定義路由注冊函數(shù)
//注冊控制器
func RegisterCtrl() {
new(UserCtrl).Router()
new(OpenCtrl).Router()
new(AttachCtrl).Router()
}
注冊路由 在 main.go 中完成路由注冊
func main(){
//
ctrl.RegisterCtrl()
//
}
解決思路如下 首先在 ctrl/base.go 里面定義一個 map
PostRouterMap := make(map[string]HandFunc)
GetRouterMap := make(map[string]HandFunc)
接著定義路由綁定函數(shù)
type Handlefunc func(w http.ResponseWriter,req *http.Request)
func Post(formate string,handlefunc func(w http.ResponseWriter,req *http.Request)){
http.HandleFunc(formate,func(w http.ResponseWriter,req *http.Request){
if req.Method==http.MethodPost {
handlefunc(w,req)
}else{
//not sourport 處理
}
})
}
func Get(formate string,
handlefunc func(w http.ResponseWriter,req *http.Request)){
http.HandleFunc(formate,
func(w http.ResponseWriter,req *http.Request){
if req.Method==http.MethodGet {
handlefunc(w,req)
}else{
//not sourport 處理
}
})
}
//支持任意方式
func Any(formate string,
handlefunc func(w http.ResponseWriter,req *http.Request)){
http.HandleFunc(formate,
func(w http.ResponseWriter,req *http.Request){
handlefunc(w,req)
})
}
首先需要定義默認(rèn)路由。RegisterRegExRouter() 中定義了默認(rèn)路由 http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request)。任何找不到的路由都會進(jìn)入這個。
//這一個專門存uri和處理函數(shù)之間關(guān)系的字典
var RegExRouterMap map[string]func(w http.ResponseWriter, req *http.Request) = make(map[string]func(w http.ResponseWriter, req *http.Request), 0)
//這是一個存儲Uri和對應(yīng)正則表達(dá)式的字典以后就不要編譯啦。
var RegexpMatchMap map[string]*regexp.Regexp = make(map[string]*regexp.Regexp, 0)
func RegExRouter(pantern string, fun func(w http.ResponseWriter, req *http.Request)) {
RegExRouterMap[pantern] = fun
//形成映射關(guān)系
RegexpMatchMap[pantern],_ = regexp.Compile(pantern)
}
//沒有找到需要一個默認(rèn)404
func notfound(w http.ResponseWriter, req *http.Request){
w.Write([]byte("404 NOT FOUNT"))
}
func RegisterRegExRouter(){
http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
uris := strings.Split(req.RequestURI,"?")
uri := uris[0]
handlefunc := notfound
for p,regm := range RegexpMatchMap{
if regm.MatchString(uri){
handlefunc = RegExRouterMap[p]
break
}
}
handlefunc(w,req)
})
}
在路由注冊中初始化
//注冊控制器
func RegisterCtrl() {
//new(AttachCtrl).Router()
RegisterRegExRouter()
}
現(xiàn)在我們可以在控制器頁面通過 RegExRouter 添加正則路由啦
//ctrl/user.go
func (ctrl *UserCtrl) Router() {
Router("/open/authwithcode", ctrl.authwithcode)
RegExRouter("/d/.*", ctrl.regtext)
}
func (ctrl *UserCtrl) regtext(w http.ResponseWriter, req *http.Request) {
util.RespOk(w, req.RequestURI)
}
客戶端請求
http://localhost/d/12345678977
響應(yīng)數(shù)據(jù)
{"code":200,"data":"/d/12345678977"}
在如上所示中定義了 notfound 函數(shù),當(dāng)沒有任何一個匹配對象時候,進(jìn)入這個函數(shù)。
//沒有找到需要一個默認(rèn)404
func notfound(w http.ResponseWriter, req *http.Request){
w.Write([]byte("404 NOT FOUNT"))
}
我們可以在 Router 方法里面實現(xiàn)攔截器功能,主要用來做鑒權(quán),日志記錄等
func Router(pantern string, fun func(w http.ResponseWriter, req *http.Request)) {
http.HandleFunc(pantern, func(w http.ResponseWriter, req *http.Request) {
//包含某些關(guān)鍵字的不需要鑒權(quán)啦
if strings.Contains(req.RequestURI, "/test/") {
fun(w, req)
} else {
//否則判斷一下,如果從xxxplatform平臺來的不需要鑒權(quán),直接往下走
ticket := req.Header.Get("request-ticket")
clientid := req.Header.Get("request-clientid")
platform := req.Header.Get("request-platform")
if platform != "xxxplatform" {
fun(w, req)
return
}
//否則這要鑒權(quán),通過就直接往下走
if userService.Authorization(ticket, clientid) {
fun(w, req)
} else {
//沒通過返回木有權(quán)限。
util.RespFail(w, "沒有權(quán)限")
}
}
})
fmt.Printf("register patern %s ==> %s\n", pantern, pantern)
}
我主要在 Router 函數(shù)上下功夫,一種可用的設(shè)計是利用攜程,如下
func Router(pantern string, fun func(w http.ResponseWriter, req *http.Request)) {
http.HandleFunc(pantern, func(w http.ResponseWriter, req *http.Request) {
//先copy出來
var bodyBytes []byte
if c.Request.Body != nil {
bodyBytes, _ = ioutil.ReadAll(req.Body)
}
// 把剛剛讀出來的再寫進(jìn)去
req.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
//這樣就可以利用攜程干事情啦
go fun(w, req)
})
}
需要注意的是要先把數(shù)據(jù) copy 出來,然后才能利用攜程,否則 fun 函數(shù)里面取出的數(shù)據(jù)是空的。
session、日志,可以引用第三方包。 鑒權(quán)可以參考攔截器。 安全,防 xss 攻擊可以參考攔截器。 代碼獲取在公眾號回復(fù):golang框架
*請認(rèn)真填寫需求信息,我們會在24小時內(nèi)與您取得聯(lián)系。