1,luci在openwrt上使用的是uhttpd服务器,了解不多,uhttpd会将lua的默认io修改连接到uhttpd,当调用io.write时会传到uhttpd中!所以想写到web上,直接把html代码调用io.write就可以显示!
2,入口在/www文件下,lua的入口程序在sgi/cgi.lua中的run函数,在这里会建立httpdispatch的协程,当协程没dead就会不停地调用!所以实际的函数运作在httpdispatch中!以下是run中的程序,在httpdispatch中调用yeild协程,就会把数据返回到这里,分为6种!id=4,传递HTML代码到uhttpd;id=5,刷新缓冲区;id=1,响应消息比如200OK;id=3,4写消息头字段。
if active then
if id == 1 then
io.write("Status: " .. tostring(data1) .. " " .. data2 .. "\r\n")
elseif id == 2 then
hcache = hcache .. data1 .. ": " .. data2 .. "\r\n"
elseif id == 3 then
io.write(hcache)
io.write("\r\n")
elseif id == 4 then
io.write(tostring(data1 or ""))
elseif id == 5 then
io.flush()
io.close()
active = false
elseif id == 6 then
data1:copyz(nixio.stdout, data2)
data1:close()
end
end
3,接下在进入httpdispatch中,在这里调用dispatch中,这是真正实际运作的程序!
这里要介绍一下util.coxpcall程序,在此之后还会继续出现,当调用这个函数时,会为传递的参数(一个函数)再建立一个协程,当协程中的协程yield时,会递归的把上一级直到dispatch这个协程yield,作用待会在说。
local stat, err = util.coxpcall(function()
dispatch(context.request)
end, error500)
4,接下来进入dispatch函数,
第一步,调用createtree函数建立node-tree(结点是非常重要的之后再说)。
c = createtree()
我们进入createtree函数中,在这里先调用createindex函数!
createindex()
这是非常重要的函数,进入createindex函数,这里和之后的一些循环获取到controller下的所有lua文件名。
local controllers = { }
local base = "%s/controller/" % util.libpath()
在这个循环中,会将所有文件require进来并且得到这些文件中的index函数,并且存储到index表中。
for _, path in ipairs(controllers) do
local idx = mod.index
index[modname] = idx
接下来再回到createtree函数中,遍历index表,将这里的环境设置给表中的函数,运行所有程序index函数!所以你在controller下按照格式添加文件就能,显示在页面上。
for k, v in pairs(index) do
scope._NAME = k
setfenv(v, scope)
v()
end
接下来我们进入controller下的lua文件,controller中的环境设置为dispatch的,所以函数在dispatch文件中寻找。
第二行就是index函数,下面是简略的函数,其中重要的就是entry函数,建立"结点"。
function index()
entry({"admin", "services"}, firstchild(), _("Services"), 40).index = true
entry({"admin", "logout"}, call("action_logout"), _("Logout"), 90)
end
说明一下node-tree,这其实就是luci上面的菜单栏,最高一级就是admin;第二级,就是system、status;然后这些还有更低一级的结点,类似一个从上而下的树结构。访问时,就是一级一级的写,比如admin/system/status。
介绍一下entry函数的参数,第一个就是是node-tree的顺序;第二个是这个结点的函数,不过在entry函数中并不是返回这个函数,而是函数运行后的结果,c.target实际是call("action_logout")运行完的return;第三个是名称,前面加_似乎是为了国际化;第四个是顺序,越小越前。targe主要是call、template、cbi之后介绍。
进入entry函数,建立node,node-tree是一大串的node联系在一起,将参数赋值给node。
local c = node(unpack(path))
c.target = target
进入node函数,调用createnode,进入createnode中。path参数是{"admin", "services"},这里是使用了递归的写法,不算复杂,都会一直递归到admin结点,逐个生成,并且在nodes中记录子节点。当controller下的所有lua文件被加载,就会生成一个node-tree,父节点的nodes会记录它的子节点。
if not c then
local last = table.remove(path)
local parent = _create_node(path)
parent.nodes[last] = c
5,返回到dispatch文件中,这时已经建立了node-tree,我们现在顺着程序往下走,先绕过鉴权等一些程序代码。
request是输入url,根据这个找到相应的nodes。最后将nodes对应的target使用copcall运行。
for i, s in ipairs(request) do
if type(c.target) == "table" then
ok, err = util.copcall(target, c.target, unpack(args))
else
ok, err = util.copcall(target, unpack(args))
end
6,接下来看一下cbi是怎么运作的,以及页面的生成。target主要是cbi、template、call这些返回的都是table,firstchildred、alias返回的是函数,都是修改request然后再次调用dispatch,可以直接看源码。
调用cbi最后返回的是这样一个table,实际调用的是_cbi(有一个下划线)函数。
return {
type = "cbi",
post = { ["cbi.submit"] = true },
config = config,
model = model,
target = _cbi
}
接下来进入_cbi函数,调用cbi.load函数。参数是entry中cbi中的值,实际上是model下的文件路径。
local maps = cbi.load(self.model, ...)
cbi("admin_system/fstab/mount")
接下来进入load函数,将lua文件loadfile进来,并且运行。
if fs.access(cbidir..cbimap..".lua") then
func, err = loadfile(cbidir..cbimap..".lua")
elseif fs.access(cbimap) then
func, err = loadfile(cbimap)
else
func, err = nil, "Model '" .. cbimap .. "' not found!"
end
在进入model下的lua文件,大致就是调用Map函数,然后这个m在调用section、value等,代表着页面上的一些模块,比如文本框等。这是面对对象的方式实现的,map对应页面、section、value等对应着页面上的框。这样就生成了一整个页面,同时所有的结点继承Node,section是map的子类,value是section的子类(可能不太合适)。这里每一种,都会有其对应的htm文件。
m = Map("system", translate("Router Password"),
translate("Changes the administrator password for accessing the device"))
返回到_cbi中,会调用一个parse函数,这个函数在cbi中。parse会递归map上的每一个section、value。
似乎是会检查当点击save或其他操作,对应点是否有输入(比如文本框,没仔细看)。
local cstate = res:parse()
最后生成页面,调用的是render函数,实际上最后调用的是template下的render函数。说明一下Template以及其他一些对象都是
用util.class()生成的,当你Template(name)时会调用其对应的__init__函数。
res:render({
firstmap = (i == 1),
redirect = redirect,
messages = messages,
pageaction = pageaction,
parsechain = parsechain
})
Template(name):render(scope or getfenv(2))
看一下template.__init__函数,这里有一个非常重要的一段代码。
在找一下name的参数,可以再map调用__init__看到,self.template = "cbi/map",这就是name参数,实际上是view下的文件。
这里就是会解析对应view文件。parse函数实际上是用C语言写的代码,被编译成了so文件。
parse函数会将view下的整个文件解析成一个lua函数,并存储起来,当需要是可以调用这个函数。
render函数实际就是调用相应文件的函数,使用copcall运行这个函数。
if name then
sourcefile = viewdir .. "/" .. name .. ".htm"
self.template, _, err = tparser.parse(sourcefile)
else
sourcefile = "[string]"
self.template, _, err = tparser.parse_string(template)
end
最后进入这个parse解析文件,可以直接看源码。
简单说一下具体思想,在view下都是htm文件,这些文件有lua代码,html代码,使用<%、%>以及其他一些:、+分隔开来。
当解析文件时,会根据上述的一些分隔符做响应处理,对于lua代码,直接作为代码;对于html代码,作为字符串变为write("htmlstring")这样的代码;其他也类似,源码不复杂。最后整个文件作为一个函数使用lua_load传回到lua中。
在dispatch中,会设置这个数组,将一些函数作为env设置到这些htm的lua函数去,write函数就是这个。
tpl.context.viewns = setmetatable({
write = http.write;
我们用write做一个简单说明,这里会将要传到uhttpd的html代码,返回到协程,然后在copcall函数逐层的yield最后返回到httpdispatch中去。然后再次resume这个函数,接着运行lua代码,这样就可以在html代码中使用lua控制。
coroutine.yield(4, content)
7,这里可以进入map.htm文件可以看到,有一行self:render_children(),递归显示map下的所有结点。
到这里已经明白cbi是怎么把html代码传回到uhttpd,记录一下学习。
鉴权什么的就不说了!