第3章:Luci框架入门:Luci架构解析、MVC模式在Luci中的实现、创建第一个Luci模块
好,咱们进入正题。这一章我要带你真正上手Luci。说实话,很多做CPE开发的朋友,一开始都被Luci吓住了——又是Lua又是JavaScript,还有一堆看不懂的目录结构。别急,我带你一步步拆解。
3.1 Luci架构解析:它到底是怎么跑的?
Luci本质上是一个轻量级的Web框架。它跑在uhttpd(或者nginx)之上,用Lua作为后端语言。你想想看,OpenWrt这种嵌入式系统,资源就那么点,C语言太重,PHP又太慢,Lua刚刚好。
Luci的整体架构分三层:
- Web服务器层:uhttpd处理HTTP请求,把请求转发给Luci的调度器
- 调度器层:根据URL路径,找到对应的模块和视图
- 应用层:你的业务逻辑、数据模型、界面模板都在这里
我记得第一次看Luci源码时,最让我困惑的是它的URL路由机制。比如你访问 /cgi-bin/luci/admin/network/wan,Luci会把它拆解成:
admin→ 模块名称network→ 控制器名称wan→ 动作名称
嗯,这里要注意:Luci的URL路径和文件目录结构是严格对应的。你如果路径写错了,页面就是404,没有任何提示。我当年踩过这个坑,排查了半天。
核心要点:Luci的调度器会根据URL自动加载 /usr/lib/lua/luci/controller/ 下的控制器文件。每个控制器文件里定义了一组动作(action),每个动作对应一个页面。
3.2 MVC模式在Luci中的实现
Luci是典型的MVC架构。但说实话,它和Spring MVC那种重型框架不一样,Luci的MVC更轻量,也更灵活。
| 层次 | 目录位置 | 职责 |
|---|---|---|
| Model(模型) | /usr/lib/lua/lua/model/ |
读写UCI配置、调用系统命令 |
| View(视图) | /usr/lib/lua/lua/view/ |
HTML模板,用Lua模板语法渲染 |
| Controller(控制器) | /usr/lib/lua/lua/controller/ |
处理请求、调用模型、选择视图 |
你可能会问:为什么模型层要单独放?我直接写在控制器里不行吗?
行是行,但你会后悔的。我在一个项目里见过一个控制器文件写了800行,里面混杂着数据库操作、HTML拼接、还有一堆if-else。后来要加一个新功能,没人敢动那个文件。说白了,MVC的核心价值就是分离关注点,让代码可维护。
来看一个实际的MVC交互流程:
-- 控制器:/usr/lib/lua/lua/controller/admin/mymodule.lua
module("luci.controller.admin.mymodule", package.seeall)
function index()
-- 注册一个页面,路径是 /admin/mymodule/settings
entry({"admin", "mymodule", "settings"}, call("action_settings"), "我的模块", 10)
end
function action_settings()
-- 调用模型层获取数据
local mymodel = require("luci.model.mymodel")
local data = mymodel.get_config()
-- 把数据传给视图
luci.template.render("admin_mymodule/settings", {config = data})
end
-- 模型:/usr/lib/lua/lua/model/mymodel.lua
module("luci.model.mymodel", package.seeall)
function get_config()
-- 读取UCI配置
local uci = require("luci.model.uci").cursor()
local value = uci:get("mymodule", "settings", "option1")
return {option1 = value}
end
<!-- 视图:/usr/lib/lua/lua/view/admin_mymodule/settings.htm -->
<%+header%>
<h2>我的模块设置</h2>
<form method="post" action="<%=REQUEST_URI%>">
<label>选项1:</label>
<input type="text" name="option1" value="<%=config.option1%>" />
<input type="submit" value="保存" />
</form>
<%+footer%>
个人经验:我建议你在模型层里统一处理错误。比如UCI读取失败时,不要直接返回nil,而是返回一个带错误码的表。这样视图层可以优雅地显示错误信息,而不是白屏。
3.3 创建第一个Luci模块
好,理论说完了,咱们动手。我会带你创建一个最简单的模块,名字就叫"helloworld"。
第一步:创建控制器文件
# 在OpenWrt设备上执行
mkdir -p /usr/lib/lua/luci/controller/admin
vi /usr/lib/lua/luci/controller/admin/helloworld.lua
写入以下内容:
module("luci.controller.admin.helloworld", package.seeall)
function index()
entry({"admin", "helloworld"}, call("action_index"), "Hello World", 20)
end
function action_index()
luci.template.render("admin_helloworld/index")
end
第二步:创建视图文件
mkdir -p /usr/lib/lua/luci/view/admin_helloworld
vi /usr/lib/lua/luci/view/admin_helloworld/index.htm
写入:
<%+header%>
<h2>我的第一个Luci模块</h2>
<p>恭喜!这个页面跑起来了。</p>
<p>当前时间:<%=os.date("%Y-%m-%d %H:%M:%S")%></p>
<%+footer%>
第三步:重启uhttpd并测试
/etc/init.d/uhttpd restart
然后浏览器访问:http://你的设备IP/cgi-bin/luci/admin/helloworld
如果一切顺利,你会看到那个页面。如果不顺利——嗯,最常见的问题是权限。Luci的控制器文件必须要有可读权限,而且Lua语法不能有错。我曾经因为少写了一个 end,排查了半小时。
避坑指南:我曾经在视图文件里用了中文注释,结果页面直接500错误。Luci的模板引擎对非ASCII字符处理不太好。建议所有注释都用英文,或者干脆不写注释。
3.4 模块注册与菜单集成
刚才的例子只是注册了一个页面,但没出现在左侧菜单里。要让模块出现在菜单中,需要调整 entry() 的参数。
function index()
-- 第一个参数是路径,第二个是处理函数,第三个是菜单名称,第四个是排序权重
entry({"admin", "helloworld"}, call("action_index"), "Hello World", 20)
-- 如果想加子菜单
entry({"admin", "helloworld", "settings"}, call("action_settings"), "设置", 10)
entry({"admin", "helloworld", "about"}, call("action_about"), "关于", 20)
end
排序权重越小,菜单越靠上。我一般习惯用10、20、30这样的间隔,方便以后插入新菜单项。
还有一个细节:如果你想让模块只对管理员可见,可以加一个 acl 参数:
entry({"admin", "helloworld"}, call("action_index"), "Hello World", 20).acl = "admin"
3.5 实战:带表单提交的模块
光显示信息不够,咱们做一个能保存配置的模块。这个例子会用到UCI模型。
-- 控制器
function action_save()
local uci = require("luci.model.uci").cursor()
local option1 = luci.http.formvalue("option1")
uci:set("helloworld", "settings", "option1", option1)
uci:commit("helloworld")
-- 保存成功后跳转回设置页面
luci.http.redirect(luci.dispatcher.build_url("admin", "helloworld", "settings"))
end
视图里加一个表单:
<form method="post" action="<%=luci.dispatcher.build_url("admin", "helloworld", "save")%>">
<input type="text" name="option1" value="<%=config.option1%>" />
<input type="submit" value="保存" class="cbi-button cbi-button-apply" />
</form>
小技巧:Luci的 cbi-button 类名是内置的样式,直接用就行。别自己写CSS,除非你想和Luci的默认主题打架。
好了,这一章的内容就到这里。你学会了Luci的架构、MVC的实现方式,还亲手创建了第一个模块。下一章我们会深入Luci的CBI模型,那是做复杂配置页面的核心工具。
公众号:蓝海资料掘金营,微信deep3321