天天看點

luci開發小插件_轉載和積累系列 - luci架構-LUA的一個web架構使用

LUCI 這個在百度上搜尋除了一篇我的百度文庫 luci 的介紹文章之外,前三頁都是些不知所雲的名詞(足見百度在專業領域的搜尋之爛),我卻在大學畢業的大半年的大部分時間裡與它糾結,由于開始的發懵到後來逐漸感覺到這家夥還很好玩的,現在就把我對 luci 的淺顯認識介紹給大家。

有關luci 的各個方面,你幾乎都可以從這裡獲得,當然,隻是淺顯的獲得, luci 的文檔寫的還算比較全,但是寫的稍顯簡略,開始看的時候會有一點不知所措。

UCI  熟悉 openwrt 的人都會有所了解,就是 Uni fi ed Con fi guration Interface 的簡稱,而 luci 這個 openwrt上的預設 web 系統,是一個獨立的由嚴謹的德國人開發的 web 架構,是 Lua  Con fi guration Interface 的簡稱,如果在您的應用裡, luci 是對 openwrt 的服務,我們就有必要做一下 uci 的簡介,我這裡就不說了,見連結:

有的時候,我們開發的luci 是在自己的 Linux PC 上開發,在普通的 linux 上,一般是沒有 uci 指令的,為了開發友善,可以手動編譯一下,方法見連結:

OK ,之前羅裡羅嗦的說了很多,現在就進入正題,進入正題的前提是你已經 make install 正确的安裝了 lua  ,luci ,以及編譯好連結了相關的 so (如果你需要,比如 uci.so nixio.so ),以及 make install 正确 web server,(我用的 web server 是 thttpd ,也編譯過 mongoose , lighttpd ,在這三個之中, lighttpd 是功能最完善的, mongoose 是最小巧的)。

進入正題:

一:luci 的啟動

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

#!/usr/bin/lua      --cgi的執行指令的路徑

require"luci.cacheloader"    --導入cacheloader包

require"luci.sgi.cgi"         --導入sgi.cgi包

luci.dispatcher.indexcache = "/tmp/luci-indexcache"   --cache緩存路徑位址

luci.sgi.cgi.run()  --執行run方法,此方法位于*/luci/sgi/cgi.lua中

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

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

-bash-4.0# pwd

/var/www/cgi-bin

-bash-4.0# ./luci

Status: 200 OK

Content-Type: text/html;

charset=utf-8

Cache-Control: no-cache

Expires: 0

HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"

"http://www.w3.org/TR/html4/strict.dtd">

二: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 檔案,簡略的代碼如下:

module("luci.controller.mini.index", package.seeall)

17

18  function index()

19      luci.i18n.loadc("admin-core")

20      local i18n = luci.i18n.translate

21

22      local root = node()

23      if not root.lock then

24          root.target = alias("mini")

25          root.index = true

26      end

27

28      entry({"about"}, template("about")).i18n = "admin-core"

29

30      local page   = entry({"mini"}, alias("mini", "index"), i18n("essentials", "Essentials"), 10)

31      page.i18n    = "admin-core"

32      page.sysauth = "root"

33      page.sysauth_authenticator = "htmlauth"

34      page.index = true

35

36      entry({"mini", "index"}, alias("mini", "index", "index"), i18n("overview"), 10).index = true

37      entry({"mini", "index", "index"}, form("mini/index"), i18n("general"), 1).ignoreindex = true

38      entry({"mini", "index", "luci"}, cbi("mini/luci", {autoapply=true}), i18n("settings"), 10)

39      entry({"mini", "index", "logout"}, call("action_logout"), i18n("logout"))

40  end

41

42  function action_logout()

43      luci.http.header("Set-Cookie", "sysauth=; path=/")

44      luci.http.redirect(luci.dispatcher.build_url())

45  end

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

例如 system.lua 檔案

function index()

19      luci.i18n.loadc("admin-core")

20      local i18n = luci.i18n.translate

21

22      entry({"mini", "system"}, alias("mini", "system", "index"), i18n("system"), 40).index = true

23      entry({"mini", "system", "index"}, cbi("mini/system", {autoapply=true}), i18n("general"), 1)

24      entry({"mini", "system", "passwd"}, form("mini/passwd"), i18n("a_s_changepw"), 10)

25      entry({"mini", "system", "backup"}, call("action_backup"), i18n("a_s_backup"), 80)

26      entry({"mini", "system", "upgrade"}, call("action_upgrade"), i18n("a_s_flash"), 90)

27      entry({"mini", "system", "reboot"}, call("action_reboot"), i18n("reboot"), 100)

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/

代碼如下:

require("luci.sys")

local load1, load5, load15 = luci.sys.loadavg()

local request  = require("luci.dispatcher").context.path

local category = request[1]

local tree     = luci.dispatcher.node()

local cattree  = category and luci.dispatcher.node(category)

local node     = luci.dispatcher.context.dispatched

local hostname = luci.sys.hostname()

local c = tree

for i,r in ipairs(request) do

if c.nodes and c.nodes[r] then

c = c.nodes[r]

c._menu_selected = true

end

end

require("luci.i18n").loadc("default")

require("luci.http").prepare_content("application/xhtml+xml")

-%>

html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

" >

/cascade.css" />

/" />

 - LuCI

:   

local function submenu(prefix, node)

if not node.nodes or node.hidden then

return false

end

local index = {}

local count = 0

for k, n in pairs(node.nodes) do

if n.title and n.target then

table.insert(index, {name=k, order=n.order or 100})

count = count + 1

end

end

table.sort(index, function(a, b) return a.order 

if count > 0 then

%>

  • ">

for j, v in pairs(index) do

if #v.name > 0 then

local nnode = node.nodes[v.name]

local href = controller .. prefix .. v.name .. "/"

href = (nnode.query) and href .. luci.http.build_querystring(nnode.query) or href

if nnode.nodes then

for k1, n1 in pairs(nnode.nodes) do

href = "#"

end

end

%>

 class="active" href="" target="_blank" rel="external nofollow" >

submenu(prefix .. v.name .. "/", nnode)

%>

end

end

%>

end

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 為不顯示  後面就不做注釋了 應該看得懂了

pw2 = f:field(Value, "pw2", translate("confirmation"))

pw2.rmempty = false

function pw2.validate(self, value, section)

return pw1:formvalue(section) == value and value

end

function f.handle(self, state, data)

if   state == FORM_VALID   then     --這個就是業務處理了  你懂得  呵呵

local stat = luci.sys.user.setpasswd("admin", data.pw1) == 0  -- root --> admin

if stat then

f.message = translate("a_s_changepw_changed")

else

f.errmessage = translate("unknownerror")

end

data.pw1 = nil

data.pw2 = nil

end

return true

end

return f

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

現在在給一個小例子:

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

6 local h = loadfile("/usr/local/luci/help.lua")

7 if h then

8     h()

9 end

10 local help_txt = help_info and  help_info.version

加載幫助幫助檔案help.lua, 關于 loadfile() 的用法可以檢視 lua 的手冊 ( 我還沒完全弄明白,先用了 ) help_txt 是一個全局變量 12 appadmin_path = "/usr/local/appadmin/bin/"

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

14 versionlist = {}

15

16 function getline (s)

.........

32 end

33

34 function get_versionlist()

.........

68 end

69

70 versionlist = get_versionlist()

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

接下來就是和最終展現的Web 頁面直接相關的代碼了,大部分都是對 luci 封裝好的一些 html 控件(代碼)的使用和擴充。 luci  封裝好的 html 控件 類可以在以下檔案檢視:./host/usr/lib/lua/luci/cbi.lua

71 m = SimpleForm("version", translate("版本管理 "))

72 m.submit = false

73 m.reset = false

74 m.help = help_txt and true or false

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 變量應該是這種結構的:

t = {

row1 = {column1 = "xxx", column2 = "xxx", .... },

row2 = {column1 = "xxx", column2 = "xxx", .... },

row3 = {column1 = "xxx", column2 = "xxx", .... },

row4 = {column1 = "xxx", column2 = "xxx", .... },

}

然後定義Table 的列控件

79 enable = s:option(DummyValue, "_enabled", translate("軟體狀态 "))

83 appid  = s:option(DummyValue, "_appid", translate("軟體版本 "))

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 函數屬性

127     newinfo = up_s:option(TextValue, "_newifo", translate("新版本資訊 "))

128     newinfo.readonly = true

129     newinfo.rows = 11

130     newinfo.cfgvalue = function(self, section)

131         local t = string.gsub(info, "Archive:[^/n]*", "")

132         return t

133     end

定義cfgvalue 後, luci 的處理函數會調用此函數為此控件指派,(傳入的 section 參數值為 row1/row2/row3等,當處理到 row 幾時值就為 row 幾 ) 對于DummyValue 等隻輸出不輸入的類,還有一種指派方法: 控件執行個體名(如 enable).value = xxx 對于有輸入的控件Value 等,  .value 方法指派在處理輸入裡會有一些問題,有什麼問題以及如何解決可以做實驗試試  , 也許是我使用方法不對造 成的

對有輸入控件的處理有兩種方法: 1 定義控件的 .write 屬性 這種方法對處理比較獨立的輸入(與其它控件輸入關系不大)比較适用

88 up_s = m:section(SimpleSection)

89 up_version = up_s:option(Button, "_up_version", translate("上傳新版本 "))

90 up_version.onlybutton = true

91 up_version.align = "right"

92 up_version.inputstyle = "save"

93 up_version.write = function(self, section)

94     luci.http.redirect(luci.dispatcher.build_url("admin", "system", "version_manage", "upload"))

95 end

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

4: view 下面的 html 簡介

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

local c = require("luci.model.uci").cursor():changes()

if c and next(c) then

-%>

end

if not reboot then

-%>

/admin/system/reboot?reboot=1">

   加載公用的頭部和尾部