Sucha's Blog ~ Archive for August, 2021

21年8月31日 周二 22:37

lua-html-tags

上周才知道 StackBlitz 的在线代码编辑工具,是一个 React JS 编辑器,包括了自动导入的包管理等功能,React 这边随便编辑修改页面添加变量,右侧的页面瞬间刷新,看着这样的前端开发实在太香了。相比之下,虽然 Swift 开发还有类型信息,但是为了上真机跑起来,还得编译链接,老费劲了。

其中 React 将页面和逻辑都放到一起的做法,感觉挺好的。

因为之前做了一个 Cincau web 框架,用了 etlua 做模版渲染引擎,发现其实还是要写很多的 HTML,而且之前的做法是将模版和实际的逻辑页面分开、model 分开,为了搭建一个页面,心智经常要顾及太多文件,再加上我搭建的多页面跳转的 demo,router 加上各个页面、model、template,真的让人头大。

但为此将模版文件放到业务逻辑里面,又感觉太啰嗦了。

就想找一个将 HTML tag 和 Lua 结合起来的描述语言,其实看过一些短小的,后来看到较大的是这个 lua-resty-tags,但是这个使用的时候需要建立 tags 描述的,非开箱即用感觉不够专业呀。

我描述一下自己的需求吧

最终是自己摸索着建立了一个 lua-html-tags,这些 tags 实际上都是 Lua function,因为 Lua 语法的关系,function 可以不加括号接受一个 string 和 table 作为参数,让之前相对简洁的 HTML 描述得以实现。

简单描述一下实现逻辑:

可以看到,从最外层的 table 开始遍历后,处理方式都是一致的递归描述,举个简单的例子

local Tags = require("html-tags")

local function pageSpec()
    return {
        html {
            head {
                meta { name="generator", content="MarkdownProjectCompositor.lua" },
                title "Example"
            },
            body {
                div {
                    { id="body" },
                    p {
                        "content 1, ",
                        "content 2"
                    }
                }
            }
        }
    }
end

print(Tags.render(pageSpec, {}))

最终会生成这样的 HTML(经过了部分换行编辑)

<html>
<head>
    <meta name="generator" content="MarkdownProjectCompositor.lua" />
    <title>Example</title>
</head>
<body>
    <div id="body">
        <p>content 1, content 2</p>
    </div>
</body>
</html>

由于这个页面描述就是 Lua,所以加入相关的变量、函数计算是很简单的事情,而且因为限定了 _ENV 和 fenv,所以 HTML tags 和自定义的 tags 对相关函数外的 Lua 的运行环境都没有影响。

感觉还可以说一下 _ENV 和 setfenv 等相关的事情。实践下来,是觉得 getfenv、setfenv 的灵活性很高,_ENV 比较受限,但也许从语言设计者的角度来说,_ENV 更安全一些吧。

先说一下这个 include tag 的作用,就是引入一个 Lua 文件描述的子页面,最终是输出一串字符串到这个 tag 的位置。使用场景时,比如我做了很多页面,但是想用同样的 HTML head 描述,当我改变 head 的描述是时,希望所有页面都能同样做更改,那么我将这个 head 文件拎出来单独描述就好。

比如将上面的例子命名为 head_tpl.lua,那么引入的时候可以是这样:

local function pageSpec()
    return {
        html {
            include "/path/to/head_tpl.lua",
            body {
                ...
            }
        }
    }
end

当这个文件被 loadfile 进入 Lua,就成了一个 function,之前我说过给 pageSpec 设定了 fenv,而 include 是早已经建立好的函数,有自己的 fenv,这时候 include 进来的 head_tpl 函数,如果我不设置 fenv,直接调用获取结果的话,用的是 include 的 fenv。

在 5.2 时,如果我不事先记录 pageSpec 的 fenv,从 pageSpec 调用了 include 函数,在 include 函数里,我无法获取到 pageSpec 函数的 fenv,即便你知道堆栈上的前一个函数有我需要的 fenv,但就是拿不到。

在 5.1 的时候就很简单了,getfenv(2) 可以取到堆栈上前一个函数的 fenv,然后 setfenv 就行,方便极了。

[特例]:HTML tags function 在实现时,如果紧接着的 table 参数里面的第一项还是 table,是特别作为属性 key / value 用 pairs 函数遍历的,比如之前例子里面的 div id="class"