天天看點

luci架構-LUA的一個web架構使用

一:luci 的啟動

  在web server 中的 cgi-bin 目錄下,運作 luci 檔案(權限一般是 755 ), luci 的代碼如下:

  1. #!/usr/bin/lua      --cgi的執行指令的路徑  
  2. require"luci.cacheloader"    --導入cacheloader包  
  3. require"luci.sgi.cgi"         --導入sgi.cgi包   
  4. luci.dispatcher.indexcache = "/tmp/luci-indexcache"   --cache緩存路徑位址  
  5. luci.sgi.cgi.run()  --執行run方法,此方法位于*/luci/sgi/cgi.lua中  

 run方法的主要任務就是在安全的環境中打開開始頁面(登入頁面),在 run 中,最主要的功能還是在dispatch.lua 中完成。

 運作luci 之後,就會出現登入界面:

  1.  -bash-4.0# pwd    
  2. /var/www/cgi-bin    
  3. -bash-4.0# ./luci    
  4.   Status: 200 OK        
  5.   Content-Type: text/html;     
  6.   charset=utf-8         
  7.   Cache-Control: no-cache       
  8.   Expires: 0    
  9. <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"     
  10. "http://www.w3.org/TR/html4/strict.dtd">        
  11.  <html class=" ext-strict">    
  12.  </html>    

二:LUCI 的 MVC

1:使用者管理:

      在luci 的官方網站說明了 luci 是一個 MVC 架構的架構,這個 MVC 做的可擴充性很好,可以完全的統一的寫自己的 html 網頁,而且他對 shell 的支援相當的到位,(因為 luci 是 lua 寫的, lua 是 C 的兒子嘛,       與 shell 是兄弟)。在登入界面使用者名的選擇很重要,      luci 是一個單使用者架構,公用的子產品放置在 */luci/controller/ 下面,各個使用者的子產品放置在 */luci/controller/ 下面對應的檔案夾裡面,比如              admin 登入,最終的頁面隻顯示 /luci/controller/admin 下面的菜單。這樣既有效的管理了不同管理者的權限。

2: controller 檔案夾下的 lua 檔案說明:(以 mini 使用者為例)

     在mini 目錄下面,會有一個 index.lua 檔案,簡略的代碼如下:

  1.  module("luci.controller.mini.index", package.seeall)    
  2. 17      
  3. 18  function index()    
  4. 19      luci.i18n.loadc("admin-core")    
  5. 20      local i18n = luci.i18n.translate    
  6. 21      
  7. 22      local root = node()    
  8. 23      if not root.lock then    
  9. 24          root.target = alias("mini")    
  10. 25          root.index = true    
  11. 26      end    
  12. 27         
  13. 28      entry({"about"}, template("about")).i18n = "admin-core"    
  14. 29         
  15. 30      local page   = entry({"mini"}, alias("mini", "index"), i18n("essentials", "Essentials"), 10)    
  16. 31      page.i18n    = "admin-core"    
  17. 32      page.sysauth = "root"    
  18. 33      page.sysauth_authenticator = "htmlauth"    
  19. 34      page.index = true    
  20. 35         
  21. 36      entry({"mini", "index"}, alias("mini", "index", "index"), i18n("overview"), 10).index = true    
  22. 37      entry({"mini", "index", "index"}, form("mini/index"), i18n("general"), 1).ignoreindex = true    
  23. 38      entry({"mini", "index", "luci"}, cbi("mini/luci", {autoapply=true}), i18n("settings"), 10)    
  24. 39      entry({"mini", "index", "logout"}, call("action_logout"), i18n("logout"))    
  25. 40  end    
  26. 41      
  27. 42  function action_logout()    
  28. 43      luci.http.header("Set-Cookie", "sysauth=; path=/")    
  29. 44      luci.http.redirect(luci.dispatcher.build_url())    
  30. 45  end    

這個檔案定義了node ,最外面的節點,最上層菜單的顯示等等。在其他的 lua 檔案裡,定義了其他菜單的顯示和html 以及業務處理路徑。每個檔案對應一個菜單相。

例如 system.lua 檔案

  1.  function index()    
  2. 19      luci.i18n.loadc("admin-core")    
  3. 20      local i18n = luci.i18n.translate    
  4. 21      
  5. 22      entry({"mini", "system"}, alias("mini", "system", "index"), i18n("system"), 40).index = true    
  6. 23      entry({"mini", "system", "index"}, cbi("mini/system", {autoapply=true}), i18n("general"), 1)    
  7. 24      entry({"mini", "system", "passwd"}, form("mini/passwd"), i18n("a_s_changepw"), 10)    
  8. 25      entry({"mini", "system", "backup"}, call("action_backup"), i18n("a_s_backup"), 80)    
  9. 26      entry({"mini", "system", "upgrade"}, call("action_upgrade"), i18n("a_s_flash"), 90)    
  10. 27      entry({"mini", "system", "reboot"}, call("action_reboot"), i18n("reboot"), 100)    
  11. 28  end    

mudel是對應檔案的, function index 定義了菜單,比如這一句entry({"mini", "system", "reboot"}, call("action_reboot"), i18n("reboot"), 100)

第1 項為菜單入口:

{"mini", "system", "reboot"}, mini 是最上層的菜單,即為使用者項, system 為一個具體的菜單, reboot 為這個菜單的子菜單,如果 reboot 還需要加上子菜單的話,可以這樣寫:

entry({"mini", "system", "reboot", "chreboot"}, call("action_chreboot"), i18n("chreboot"), 1), 這樣就會在reboot 上産生一個新的子菜單,以此類推,可以産生 N 層菜單。

第二項為菜單對應的頁面,可以是lua 的源代碼檔案,也可以是 html 頁面。

alias cgi form call 等定義了此菜單相應的處理方式, form 和 cgi 對應到 model/cbi 相應的目錄下面,那裡面是對應的定制好的 html 和 lua 業務處理。

alias是等同于别的連結, call 調用了相應的 action_function 。還有一種調用,是 template ,是直接連結到view 相應目錄下面的 htm 頁面。(說明: luci 架構下面的 htm 都是可以嵌入 lua 語句的,做業務處理,相當于 jsp 頁面的内部的 Java 語句)。

問價查找對應簡介:

entry({"mini", "system", "reboot"}, call("action_reboot"), i18n("reboot"), 100)  :對應此檔案的action_reboot function

entry({"mini", "system", "index"}, cbi("mini/system", {autoapply=true}), i18n("general"), 1):對應*/model/cbi/mini/system.lua  {autoapply=true}   這個失傳的參數。

。。。。。

第三項為i18n 顯示,比如entry({"mini", "system", "reboot"}, call("action_reboot"), i18n("reboot"), 100),菜單的名字為admin-core 檔案内的對應顯示。此處也可以這樣寫,  i18n("reboot"," 重新開機 ") ,即直接做了國際化。菜單上顯示的就是“重新開機”。

第四項為現實的順序,這個數字越小,顯示越靠前,靠上。

現在說一下這些檔案的解析是怎麼解析的呢?你當然是說dispatch.lua中,你說對了,但是真正解析成菜單的遞歸算法确實在header.htm中 位置:*/view/themes/openwrt/ 代碼如下:

  1.  <%    
  2. require("luci.sys")    
  3. local load1, load5, load15 = luci.sys.loadavg()    
  4. local request  = require("luci.dispatcher").context.path    
  5. local category = request[1]    
  6. local tree     = luci.dispatcher.node()    
  7. local cattree  = category and luci.dispatcher.node(category)    
  8. local node     = luci.dispatcher.context.dispatched    
  9. local hostname = luci.sys.hostname()    
  10. local c = tree    
  11. for i,r in ipairs(request) do    
  12.      if c.nodes and c.nodes[r] then    
  13.           c = c.nodes[r]    
  14.           c._menu_selected = true    
  15.      end    
  16. end    
  17. require("luci.i18n").loadc("default")    
  18. require("luci.http").prepare_content("application/xhtml+xml")    
  19. -%>    
  20. <?xml version="1.0" encoding="utf-8"?>    
  21. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">    
  22. <html xmlns="http://www.w3.org/1999/xhtml" xml:attribute-value">"<%=luci.i18n.context.lang%>" attribute-value">"<%=luci.i18n.context.lang%>">    
  23. <head>    
  24. <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />    
  25. <meta http-equiv="Content-Script-Type" content="text/javascript" />    
  26. <link rel="stylesheet" type="text/css" media="screen" href="<%=media%>/cascade.css" />    
  27. <!--[if lt IE 7]><link rel="stylesheet" type="text/css" media="screen" href="<%=media%>/ie6.css" target="_blank" rel="external nofollow"  /><![endif]-->    
  28. <!--[if IE 7]><link rel="stylesheet" type="text/css" media="screen" href="<%=media%>/ie7.css" target="_blank" rel="external nofollow"  /><![endif]-->    
  29. <% if node and node.css then %><link rel="stylesheet" type="text/css" media="screen" href="<%=resource%>/<%=node.css%>" />    
  30. <% end -%>    
  31. <mce:script type="text/javascript" src="<%=resource%><!--    
  32. /VarType.js">    
  33. // --></mce:script>    
  34. <mce:script type="text/javascript" src="<%=resource%><!--    
  35. /XHTML1.js">    
  36. // --></mce:script>    
  37. <mce:script type="text/javascript" src="<%=resource%><!--    
  38. /Dropdowns.js">    
  39. // --></mce:script>    
  40. <title><%=striptags( hostname .. ( (node and node.title) and ' - ' .. node.title or '')) %> - LuCI</title>    
  41. </head>    
  42. <body class="lang_<%=luci.i18n.context.lang%>">    
  43. <p class="skiplink">    
  44. <span id="skiplink1"><a href="#navigation" mce_href="#navigation"><%:skiplink1 Skip to navigation%></a></span>    
  45. <span id="skiplink2"><a href="#content" mce_href="#content"><%:skiplink2 Skip to content%></a></span>    
  46. </p>    
  47. <div id="header">    
  48. <h1><%=luci.version.distname%></h1>    
  49. <p>    
  50. <%=luci.version.distversion%><br />    
  51. <%:load%>: <%="%.2f" % load1%> <%="%.2f" % load5%> <%="%.2f" % load15%><br />    
  52. <%:hostname%>: <%=hostname%>    
  53. </p>    
  54. </div>    
  55. <div id="menubar">    
  56. <h2 class="navigation"><a id="navigation" name="navigation"><%:navigation Navigation%></a></h2>    
  57. <ul id="mainmenu" class="dropdowns">    
  58. <%-    
  59. local function submenu(prefix, node)    
  60.      if not node.nodes or node.hidden then    
  61.           return false    
  62.      end    
  63.      local index = {}    
  64.      local count = 0    
  65.      for k, n in pairs(node.nodes) do    
  66.           if n.title and n.target then    
  67.                table.insert(index, {name=k, order=n.order or 100})    
  68.                count = count + 1    
  69.           end    
  70.      end    
  71.      table.sort(index, function(a, b) return a.order < b.order end)    
  72.      if count > 0 then    
  73. %>    
  74. <ul id="submenu_<%=string.gsub(string.gsub(prefix, "/", "_"), "^_(.-)_$", "%1")%>">    
  75. <%-    
  76.           for j, v in pairs(index) do    
  77.                if #v.name > 0 then    
  78.                     local nnode = node.nodes[v.name]    
  79.                     local href = controller .. prefix .. v.name .. "/"    
  80.                     href = (nnode.query) and href .. luci.http.build_querystring(nnode.query) or href    
  81.                     if nnode.nodes then    
  82.                          for k1, n1 in pairs(nnode.nodes) do    
  83.                               href = "#"    
  84.                          end    
  85.                     end         
  86. %>    
  87. <li><a<% if nnode._menu_selected then %> class="active"<%end%> href="<%=luci.util.pcdata(href)%>"><%=nnode.title%></a><%-    
  88. submenu(prefix .. v.name .. "/", nnode)    
  89. %></li>    
  90. <%-    
  91.                end    
  92.           end    
  93. %>    
  94. </ul>    
  95. <%    
  96.      end    
  97. end    

3: model 業務處理和頁面生成簡介

    我認為model 的業務處理和 html 生成,是 luci 架構的精華,但是想用好它,最終擴充定義自己的頁面也是最難的,但是一旦定義好了,後面的工作就會輕松高效簡介統一,不失為一種好的解決方案。但是它又有缺點,就是寫頁面雖然統一,但是不夠靈活。

下面以 SimpleForm為例,講解一下。

具體檔案 */luci/model/cbi/passwd.lua

f = SimpleForm("password", translate("a_s_changepw"), translate("a_s_changepw1"))  --調用SimpleForm 頁面  當然還是 I18N 從中搗亂,看上去沒那麼直覺,不理他 

pw1=f:field(Value,"pw1",translate("password")) --  

SimpleForm 裡面加一個 field   至于 SimpleForm  和 fiemd 是什麼,一會去看 SimpleForm 頁面去 

pw1.rmempty=false -- 把 SimpleForm的 rmempty 為不顯示  後面就不做注釋了 應該看得懂了 

  1. pw2 = f:field(Value, "pw2", translate("confirmation"))   
  2. pw2.rmempty = false   
  3. function pw2.validate(self, value, section)   
  4.      return pw1:formvalue(section) == value and value   
  5. end   
  6. function f.handle(self, state, data)   
  7.      if   state == FORM_VALID   then     --這個就是業務處理了  你懂得  呵呵   
  8.           local stat = luci.sys.user.setpasswd("admin", data.pw1) == 0  -- root --> admin       
  9.           if stat then   
  10.                f.message = translate("a_s_changepw_changed")   
  11.           else   
  12.                f.errmessage = translate("unknownerror")   
  13.           end   
  14.           data.pw1 = nil   
  15.           data.pw2 = nil   
  16.      end   
  17.      return true   
  18. end   
  19. return f  

說明:( simpleForm  位于 view/cbi   下面,可以研究一下,各個元素是如何定義的 )

現在在給一個小例子:

以.*/luci/model /cbi/admin_system/version_manage.lua 為例,介紹一下 luci 中 web 頁面 lua 代碼 

  1.   6 local h = loadfile("/usr/local/luci/help.lua")   
  2.   7 if h then   
  3.   8     h()   
  4.   9 end   
  5. 10 local help_txt = help_info and  help_info.version   

加載幫助幫助檔案help.lua, 關于 loadfile() 的用法可以檢視 lua 的手冊 ( 我還沒完全弄明白,先用了 ) 

help_txt 是一個全局變量 

12 appadmin_path = "/usr/local/appadmin/bin/" 

定義一個全局變量,其實跟功能跟宏一樣,定義appadmin 的絕對路徑 

  1. 14 versionlist = {}   
  2. 15   
  3. 16 function getline (s)   
  4. .........   
  5. 32 end   
  6. 33   
  7. 34 function get_versionlist()   
  8. .........   
  9. 68 end   
  10. 69   
  11. 70 versionlist = get_versionlist()   

定義一個全局變量和兩個函數,并初始化此變量 

接下來就是和最終展現的Web 頁面直接相關的代碼了,大部分都是對 luci 封裝好的一些 html 控件(代碼)的使用和擴充。 luci  封裝好的 html 控件 

類可以在以下檔案檢視:./host/usr/lib/lua/luci/cbi.lua 

  1. 71 m = SimpleForm("version", translate("版本管理 "))   
  2. 72 m.submit = false   
  3. 73 m.reset = false   
  4. 74 m.help = help_txt and true or false   
  5. 75 m.helptxt = help_txt or ""   

使用了一個SimpleForm 的控件, SimpleForm 實際上對應一個 html 表單,是整個頁面最大的 " 容器 " ,本頁面内的絕大部分控件都處于 SimpleForm 内 

,是它的子控件 。我知道的可以做> 頁面最大 " 容器 " 的控件還有一個 Map, 但它需要 ./host/etc/config/ 目錄下的一個配置檔案,我沒有使用。 

submit reset是 luci 預設就封裝好的一個屬性,分别控制 html 表單的 " 送出 "" 重置 " 按鈕 ;help helptxt 是我擴充的表單屬性,分别控制 web 頁面的 

"幫助 " 功能和幫助内容。關于控件屬 

性的意義、實作和擴充可以按以下步驟進行: 

    在檔案./host/usr/lib/lua/luci/cbi.lua 中查找控件名 SimpleForm,  然後可以找到以下行 664     self.template = "cbi/simpleform" 這 

表明SimpleForm 的 html 模版檔案為 ./host/usr/lib/lua/luci/view/cbi /simpleform.htm ,通過研究 simpleform.htm 檔案内容可以知道各屬性的 

功能以及模版中使用lua 代碼的方法,然後可以按類似的方法添加自定義的 

屬性。 

77 s = m:section(Table, versionlist) 

建立了一個section,section 内定義了一個表格類, versionlist 是與其相關的變量( lua 的所有變量都可歸類于 table 類型 ) 

與Table 關聯的 table 變量應該是這種結構的: 

  1. t = {   
  2.     row1 = {column1 = "xxx", column2 = "xxx", .... },   
  3.     row2 = {column1 = "xxx", column2 = "xxx", .... },   
  4.     row3 = {column1 = "xxx", column2 = "xxx", .... },   
  5.     row4 = {column1 = "xxx", column2 = "xxx", .... },   
  6. }   

然後定義Table 的列控件 

  1. 79 enable = s:option(DummyValue, "_enabled", translate("軟體狀态 "))   
  2. 83 appid  = s:option(DummyValue, "_appid", translate("軟體版本 "))   
  3. 84 appname = s:option(DummyValue, "_appname", translate("軟體名稱 "))   

DummyValue是隻讀的文本框,隻輸出不輸入。 Value 是單行文本框,可輸出也可輸入。 Flag 是一個 checkbox,值為 "1" 時被選中,為 "0" 時未選中。 

ListValue是清單框 ... 具體的用法可 

以看./host/usr/lib/lua/luci /model/cbi/ 下的檔案( find ./host/usr/lib/lua/luci/model/cbi/ -name "*.lua" |xargs grep 

"ListValue") 

對于table 内普通的字元串類的值,隻需要把列控件的 id (括号内第二個值,如 "_appid" )定義為 table 内對應的變量名(比如 column1 ) 

對于非變通字元串類的值,或者為字元串但需要進行一定的處理然後再顯示的值,可以按以下方法顯示:定義該控件的cfgvalue 函數屬性 

  1. 127     newinfo = up_s:option(TextValue, "_newifo", translate("新版本資訊 "))   
  2. 128     newinfo.readonly = true   
  3. 129     newinfo.rows = 11   
  4. 130     newinfo.cfgvalue = function(self, section)   
  5. 131         local t = string.gsub(info, "Archive:[^/n]*", "")   
  6. 132         return t   
  7. 133     end   

定義cfgvalue 後, luci 的處理函數會調用此函數為此控件指派,(傳入的 section 參數值為 row1/row2/row3等,當處理到 row 幾時值就為 row 幾 ) 

對于DummyValue 等隻輸出不輸入的類,還有一種指派方法: 控件執行個體名(如 enable).value = xxx 

對于有輸入的控件Value 等,  .value 方法指派在處理輸入裡會有一些問題,有什麼問題以及如何解決可以做實驗試試  , 也許是我使用方法不對造 

成的 

對有輸入控件的處理有兩種方法: 

1 定義控件的 .write 屬性 

    這種方法對處理比較獨立的輸入(與其它控件輸入關系不大)比較适用 

  1. 88 up_s = m:section(SimpleSection)   
  2. 89 up_version = up_s:option(Button, "_up_version", translate("上傳新版本 "))   
  3. 90 up_version.onlybutton = true   
  4. 91 up_version.align = "right"   
  5. 92 up_version.inputstyle = "save"   
  6. 93 up_version.write = function(self, section)   
  7. 94     luci.http.redirect(luci.dispatcher.build_url("admin", "system", "version_manage", "upload"))  
  8. 95 end   

ps:隻有當 Value 的 rmempty == false 時, Value 輸入為空也會觸發 write 函數 ,  需要對 rmemtpy 顯示指派為false ( xx.rmempty = false) 

4: view 下面的 html 簡介

這個是最好了解的  例:passwd.htm

    1.  <%+header%>  
    2. <h2><a id="content" name="content"><%:system%></a></h2>  
    3. <h3><%:reboot%></h3>  
    4. <p><%:a_s_reboot1%></p>  
    5. <%-  
    6. local c = require("luci.model.uci").cursor():changes()  
    7. if c and next(c) then  
    8. -%>  
    9.        <p class="warning"><%:a_s_reboot_u%></p>  
    10. <%-  
    11. end  
    12. if not reboot then  
    13. -%>  
    14. <p><a href="<%=controller%>/admin/system/reboot?reboot=1"><%:a_s_reboot_do%></a></p>  
    15. <%- else -%>  
    16. <p><%:a_s_reboot_running%></p>  
    17. <script type="text/javascript">setTimeout("location='<%=controller%>/admin'", 60000)</script>  
    18. <%- end -%>  
    19. <%+footer%>  
    20. <%+header%> <%+footer%>  加載公用的頭部和尾部  
    21. <% lua code%>  
    22. <%:i18n%>  
    23. <%lua code%>  
    24. <%=lua 變量 %>