Sucha's Blog ~ Welcome

21年11月25日 周四 22:51

基于 luarocks/luastatic 的二进制打包工具

luastatic 可以打无依赖的二进制包,但是官方的版本有一些限制,比如在命令行里面,lua 源文件之只能是基于当前打包目录的相对路径。

而实际上,由于 lua 的 LUA_PATH、LUA_CPATH 以及 package.path 和 package.cpath 太自由,可以定制很多种搜索优先级,以及目录相对关系,打包目录下的相对路径,就不能反映源码上面的 require 路径逻辑了。

于是我基于上面的 luastatic,做了一点改进,可以输入相对路径,代码在 lalawue/luastatic

举个例子,一方面,我重度使用 luarocks 安装的库,比如 cincau,默认目录可以是 /usr/local/share/lua/5.1/cincau,而另外自己写的源码,都在 app 目录下,然后设置了如下的 LUA_PATH:

export LUA_PATH="/usr/local/share/lua/5.1/cincau/?.lua;./app/?.lua"

这样在 require 的时候,省却了很多相对路径。如果使用官方的 luastatic 打包,就得建立相对目录,然后作为命令行参数输入给 luastatic,用我修改后的 luastatic,只需要先设置 shell 变量:

export LUASTATIC_SOURCE_PATH="/usr/local/share/lua/5.1/cincau/;app/;"

注意目录最后的 '/' 和 ';',如果输入的 lua 源文件检测到相应的路径前缀,会将这个前缀去掉,这样就实现了 LUA_PATH 和 package.path 的功能,运行时的路径搜索和打包时候的路径一致了,不需要构建相对目录。

另外需要注意的是,虽然 luastatic 支持生成无依赖的 binary,但那只是对应官方的 lua,luajit 依赖的 .so 库还是需要动态加载的,比如很多的 ffi.load 依赖的是系统的 dlopen。这个我看了一些文章,大佬们根据安全原则,系统的接口都不支持从内存中加载一个动态库,虽然某些平台上通过 mmap 是可以做到的,但是这个总感觉不大好。

那如果是基于 luajit,lua 源文件是打到 binary 里面了,但是依赖的 so 库,是在运行前,通过 export LD_LIBRARY_PATH 或者 DYLD_LIBRARY_PATH 来设置 so 搜索路径,做成外部库加载的。

通过这样的方式做成的 binary 发布包,可以在对应平台上不依赖 luarocks 就可以运行,安全性也好一些,发布更新都很方便。


话说昨天看了一下 Programming Language and compiler Benchmarks 里面 go vs java,go vs javascript 以及 go vs luajit,不得不说 go 性能确实好,速度跟 java 一样,但是内存消耗少了不知多少。

luajit 平均速度是 go 的一半,内存消耗多了一些,javascript 速度跟 luajit 类似,但内存消耗多很多。

如果是从头学起的话,我觉得 go 是要比 java 简单很多的,工程属性方面,go 更是标杆,如果算上多线程 goroutine 的话,毫无疑问,go 是首选。多线程多难搞呀,所以小公司很可能都慢慢转向 go 了,反正我是觉得真的香呀,现在都有点后悔搞 luajit 了,因为速度是赶不上 go 的,基础工具又差这么多。

上面的所谓打二进制包,就是补充了一部分基础工具的缺失,即便不说标杆 go,人家 deno 也是可以打成二进制包的。

luajit 不多的优点,一方面是不需要编译,虽然 go 编译很快,但是不需要编译连这个部分都省略了;另外就是 luajit 语法几乎是固定了,不大担心后续会有比较大的变动,唯一担心的额是 Mike Pall 是 luajit 几乎唯一的维护者,社区太小了,这个软件太高深,往后一些平台的支持以及升级估计够呛,更新也慢。

lua 语言的的优点在 web 开发上面不大体现得出来,相比之下,比如虚拟机很小,这样轻量的虚拟环境、沙箱,可以造就诸如国内有名的 skynet

lua 语法更简单,类型都可以少关心,但并不意味说这个语言抽象能力不够,或者扩展不行,如果 metatable 抽象得好,可能性太多,OO 小菜一碟,比如我之前基于 lua 构建的 mooncake;所以,也没有泛型渴望症;但是因为缺乏类型系统,那么测试系统得自己保证才放心。话说,也是因为语言自由,lua 很多库的源码,相对 go 源码来说,可太难懂了。

最近还看了 WASM,感觉也很香,go 自从 1.11 后,支持输出 WASM,但 WASM 这个技术还比较早期,W3C 上面到了提案 2 的阶段,还没有多线程这样的标准。从上面效率对比,跟 go 是一个级别。

这次 post 先到这里吧。

21年11月17日 周三 21:52

诛仙读后感

以前没读过仙侠系列,算是第一次读。

前段意境真的很美,特别是上了青云山拜师之后,掌管大厨的这一段,跟师姐砍竹子,带领一猴一狗每天在这仙山里面慢慢悠悠升级,就觉得这感觉很好。

而且内线是有内功见长,又少不了武侠里面的捡到了绝世大宝贝,比如土到不能更土的烧火棍,以及后期那些救人于水火,法力无边的宝贝,更是让人羡慕神往。

起伏线的大起大落也很经典,以及一直不受师傅待见,但最后唯一一个给师傅长脸的徒弟,又比如被掌门下杀心的一段而成为鬼厉,从此杀人无数。

支线里面厉害的有万剑一,以及仙人指路,野狗跟仙人指路能走到一块,这个组合在人心里面又重了,这条线里面的仙人指路水平太高,应该是作者的用来补充剧情的旁白。

但是也有遗憾的支线,比较不能理解的是陆雪琪,一直走不到明线,但是暗线又这么强烈,难受。

前面写得太好了,觉得跟兽人大战之后,跟魔教对决的这一段,马马虎虎,没啥感觉。

总体可以打 7.5 ~ 8 分了。

21年11月11日 周四 00:33

Mithril 使用心得(1)

这个网站已经诞生 10 多年,但其是静态页面,基于 XHTML 结构,而不是最新的 HTML5,站长本人是专业 APP 开发, H5/Web 开发比较业余,对 Mithril 的使用感觉还不够深入,但也算趟了一些坑, 可以作为心得说一下的。

说一下为何不使用 React、Vue 或者 Preact,以及 Angular 等等其他框架,没什么原因,因为其实入手之前看了非常多的入门对比文章, 在对比的过程中总是要上手的嘛,因为看到 Mithril 吹自己简单,其网站入门教程的介绍也是非常到位的(React 没能一下学会, 但是 Mithril 可以),加上不需要 nodejs 这样的依赖,不需要 build,学习的曲线感觉很平缓。

话说我之前是一个连 SPA(Single Page Application)都不懂的人,通过 Mithril 的教程,学会了基础的 component, route、以及 XHR,基本上一个单页面就可以搞定了。

再说还有 stackblitz 这样的平台,入门是很方便的。因为是个无需 build 的方案, 简单的 HTML5 + rel 标签带入 Mithril 就可以开始工作了。

js 我也只懂皮毛,很多东西都得百度,没关系,出来的程序能用就行。最后做了 2 个 SPA 页面,一个是 wiki,一个是 note 便签。

wiki 的相对简单一些,大概 2 - 3 天才搞定,学习了 route,但是内部的很多状态我是通过一个全局状态变量来控制的。 便签这个页面没有用到路由,纯内部状态跳转刷新,两个都没有用到 browser 的 history,浏览器的返回按钮会退回上个 URL。

Mithirl 的 bind 或者 route,都可以仅指定页面的一个部分,一个 element 来刷新,内部是根据 vnode 的变化来判断是否更新的, 这个 vnode 变化感觉有点悬乎,比如我在创建一些子节点的时候,attribute 是有变化的,但是 Mithril 感觉不到这个变化, 所以我不得不在外层做更明显的 vnode 变化,比如增加一个用不到的 span 标签这样,来确保重新输出 HTML 节点。

Mithril 限定 60hz 刷新确实也不错,但是遇到一些极端情况,比如自己本地测试的时候,网络返回就挺快的,当我在用内部变量控制 vnode 输出的不同时,因为间隔过短,虽然变量有切换状态,但 Mithril 间隔读取到的其实都是同一个状态下的 vnode, 而忽略了中间这个变量状态下 vnode 的输出,最后导致认为没有变化,不需要刷新,出了问题,如下:

// 如下的 A、B 表示变量值,后面数字表示 vnode 结构为 A 但内容不同,这里需要保证 A1 -> B 间隔 1/60 秒
A1 -> B -> A2

所以有时候,需要将一些操作延时到 60hz 单次刷新之外才行,让 Mithril 检测到 vnode 变化。

Mithril 教程用的是古老的创建 vnode 的函数调用方案,但其实可以配合 preact 的 htm, 我觉得使用方式上要比教程里面的简单很多,比如教程是下面这样创建 vnode 的:

// 下面 title_str 是变量
// <h1 class="title">My first app</h1>
const title_str = 'title';
m("h1", {class: title_str}, "My first app")

如果用了 htm,可以像下面这样:

// 先绑定,最后也是通过 m 函数来输出 vnode 的
const html = htm.bind(m);
const title_str = 'title';
html`<h1 class="${title_str}">My first app</h1>`

明显是使用了 htm 的代码更容易定位问题。

因为追求短平快,单个 js 文件就完成了 wiki 或者 note 便签这样的功能,所以 Mithril 结构化方面我是没有发言权了,教程上面 呼吁结构化组件,分解功能模块我是一点都没用上。

先这样吧。

21年11月07日 周日 11:46

stunnel 的 SNI 配置

因为是很小的网站,资源也有限,感觉没必要用上 openresty 这个大件。

先贴配置:

debug=info
output=/path/to/access.log
pid=/path/to/process.pid
client=no

[https]
accept = 443
connect = 127.0.0.1:port1
cert = /path/to/default.cer
key = /path/to/default.key

[site1]
connect = 127.0.0.1:port1
sni = https:site1.domain
cert = /path/to/site1.cer
key = /path/to/site1.key

[site2]
connect = 127.0.0.1:port2
sni = https:*.site2.domain
cert = /path/to/site2.cer
key = /path/to/site2.key

说一下上面的配置。

这是类似 INI 方式的配置文件,首先定义是 server 端的配置,设置 access.log 和 process.pid 文件的位置。

之后设置默认协议,比如叫做 https,监听 443 端口,并设置连接地址,使用默认的 cert 和 key 做验证,这里其实是可以跟下面的 site1 或者 site2 用同样的 cert 和 key 配置。

之后分别设置不同网站对应的处理地址,比如 site1,连接的是 port1,注意 sni 字段配置,第一个是协议,就是上面描述过的 https,而不是 HTTPS,之后是需要处理的 domain,这里只处理二级域名,没有通配字,最后加入网站申请到的 cert 和 key。

site2 的 sni 用了通配字,所有 site2.domain 的二级、三级域名都可通过验证处理。

21年10月02日 周一 22:48

独立 SideBar

将 site 和 blog 的边栏 sidebar 独立到各自不同的 js 文件上去了,比如

修改后的好处,是需要更新 archive links 时,也只需要更新对应的 sidebar js,而不需要更新每个月份输出的 HTML 了。而之前作为 static site generator,是独立输出到每个月份 HTML 上面去的。

内容跟 sidebar 分离后,完全可以没有什么顾忌的更新 sidebar 了,当然其实我也几乎不会更新 sidebar,感觉没啥必要,🤗

21年9月30日 周四 23:59

9 月份干嘛了

其实 9 月份做了蛮多的事情,公司那边慢慢忙起来了,在公司里面做了一个数据库抽象模块,其实就是一个抽象的 DAO(Data Access Object),使用的是 GRDB,起因是使用的 Realm 版本太旧了,启动后未知原因的 crash 飙升,堆栈上看是启动时候 Realm 在初始化 ObjC 这一层某个 sub class 时候就挂了,都还没有任何 DB 的操作逻辑呢,其实就是 DidFinishLaunch 都没走完。

因为工程早就都是 Swift 了,不想呆在 ObjC 里面,太多的 swizzle 黑魔法,对工程代码质量要求太高,小公司很难保证。因为 DB 操作大部分都仅仅是序列化逻辑,上没有复杂的 join、foreign key 操作,所以只是简单封装了一下 GRDB,抽象独立为 Pod 库,不污染工程代码就好。基于这个前提,抽象了 fetch、push、watch 接口,Pod 内部建立内存模型跟 DB 模型的对应,其实就是表操作。可以通过上述的几个接口,接入不同的带 param 的 enum 参数,就可以完成功能。有了这一层抽象,后续即便 GRDB 因为各种原因不能使用,我们不需要修改业务代码,只需要在 Pod 这一层做内存到外存的转换就好。

另外还抽象了一个指定 key 的 JSONObject 模型,里面可以放入各种深度、层次的 JSON 数据,insert、update、delete、watch 时候,都关联到 key 上就好,对于一些需要序列化的数据,又不想用 UserDefault 来做,就可以使用这个 JSONObject 来存入 GRDB,实际上是作为 blob 数据存储进去的,目前只支持一个 key,一般都是什么什么 id 之类的,这也是目前能够支持绝大多数业务的序列化功能。

另外学写了一下 SPA(Single Page Application),Web 发展迭代速度要比移动 App 快多了,可选的很多,React、Vue、Preact 都有,我稍微摸索了一下 React,觉得有点拎不动,然后 Vue、Preact 也一样,再说这些东西依赖很重,nodejs 几乎是必须的,build 过程少不了,最后我选了 Mithril。之前有听说过,然后在阮一峰那边又被推了一下。教程相对简单,而且是只支持 browser,不需要 build 过程,我学习了两三天,做了一个 Wiki 管理工具,用的 Markdown IDE 是 SimpleMDE Markdown Editor

选择 Mithril 的原因除了它自己吹嘘的之外,还有其他的考量,比如可以利用上之前的 HTTP Server Cincau。这个新作的 Wiki 管理工具,最后就作为了 Cincau 支持的一个 SPA Demo 页面。其实 SPA 对后端没什么依赖,后端主要是模版 SSR(Server Side Render)。当然有了这个 Wiki 工具,我就可以将之前另外一个小站点的 Wiki 移过来,直接在搭建在 Cincau 上面,话说之前的 Wiki 是 doku + php + nginx 来搭建的,改成 Luajit + Cincau 后,资源依赖少了很多,不过相对功能也少了很多,这些坑只好自己趟了,对于我这个职业 App 开发者来说,也是一个学习的过程。

上面说到 SSR,因为不大喜欢之前搭配的 etlua,于是自己做了 lua-html-tags,这个之前 8 月份的时候提过了。

话说之前 doku 除了基本的 Wiki 功能(用得比较早,当时还不知道 CommonMark 或者 GFM 的语法),还有 revision,以及 show diff 的功能。我觉得这个功能挺好的,对于刚用 Mithril 做的 Wiki 工具来说,可以有效避免某些错误操作导致的 clear 逻辑(当然这从来就没发生过),对心理造成的压力,毕竟有 revision,我可以随时恢复到任何一个版本。

增量保存用的工具是 bsdiff,其实用系统里面自带的 GNU 的 diffutils 或者 patch 也是可以的,只是在 server 这一层,需要调用太多次 shell 脚本完成相应的功能,觉得不大好。于是将这两个工具做成了 Lua 库 lua-bsdiff,并 apply 了两个觉得挺不错的 patch。

不过上面提到的 Wiki 支持 revision 功能,在 cincau 的 demo 上面是没有的,目前没打算开源,🙂

9 月份忘记写 blog 了,10 月初初回顾了一下,后面有时间再详细铺开,先这样吧,🤗

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"

21年7月17日 周六 21:10

定制 UINavigationBar 的转场动画

最近 app UI 改版,交互设计师给出了 UINavigationController 下 Push/Pop 的转场动画,Push 的时候,fromView 会被缓慢往左推,toView 往左慢慢全部盖住 fromView,盖住之前,fromView 大概往左侧移动了 1/3 个页面;Pop 的时候则相反。

单单这样的转场动画是容易实现的,网上的教程一大堆,但难点在 UINavigationBar 也需要实现同样的交互。问题是 UINavigationBar 的动画是系统提供的,UIViewController 仅仅提供左、右、中的 CustomView,而且就我们的 App 来说,首页是隐藏的,在二级页面才会显示出来。由于 UINavigationBar 是系统管理的,转场动画的框架是没有对 UINavigationBar 提供支持的,得自己做。

网上搜索了一下,美团的技术博客提供了思路,具体我用的方案,在 iOS 14 模拟器上验证了蛮久,加上一些调试,如下:

Push 时,对 fromView 的 UINavigationBar 截图;如果是 hidden 状态则不用截图。toView 的 NavigationBar 是假的,但却是用真的 UINavigationBarItem 来构建的,真的 UINavigationBar,系统还未开始绘制呢。假的 NavigationBar 使用了 UINavigationBarItem 的 leftBarItem、rightBarItem 和 titleView 或者 title string。

这里需要注意将 fromViewController 上面对真的 UINavigationBarItem 的链接进行切断,比如 titleView 给他设置一个空的 UIView,leftBarItems、rightBarItems 给设置空的数组来填充。

fromView 截图的 UINavigationBar 按照转场动画的时长 Push 到左侧,toView 带的假的 NavigationBar 则从右侧 Push 进来,等动画结束,将 UINavigationBarItem 上的数据再拿给 fromViewController 使用,将假的 NavigationBar removeFromSuperview。

Pop 的时候需要区分是按返回按钮的 Pop,还是全局右划可中断的 interactive Pop。

先说简单的按返回按钮的 Pop,fromView 在对 UINavigationBar 截图后往右,toView 带着截图的 NavigationBar 出来,等动画结束,截图 NavigationBar removeFromSuperview 就好,这里比较简单。

可中断的 interactive Pop 复杂一些,举例我们用的是 FDFullscreenPopGesture,它其实是截获系统提供的全局右划返回功能。但系统的全局右划返回,当 UINavigationBar 的 backItem 被设置为 nil 等时候,是不起作用的。这个库是拿到系统手势检测 view,添加手势,检测到后调用系统的全局右划返回接口,发起是可中断的右划返回动画。

这个库暴露出来的检测手势 gesture 实例,我们后面会用到。

可中断的全局右划返回 Pop 跟直接调用 UINavigationController 的 popViewController 很不一样,由于动画可以中断,因此需要在 fromView 上建立假的 NavigationBar,截图就好。

手势的开始,来自于监听上面说到的 gesture 的 target action,可以监听 begin、change、end 事件,并计算右滑的百分比,用于调整截图 NavigationBar 跟随 fromView。难点来了,手势结束的时候,实际上右划动画也许结束,也许会中断返回,具体会如何,其实是系统判定的,我们并不知道后面动画要怎么样的,包括时长多久也都不知道,因此这里,需要建立一个跟踪机制,我用的是 DisplayLink 来做的,模拟器上看,比较跟手,但是太极端的情况,也会偶尔拉跨。

另外上面说到的假的 NavigationBar 将 UINavigationBarItem 给真的 UINavigationBar 时,其实会闪一下;以及截图 NavigationBar 的时机,实际上比转场动画要早,也就是截图代码不能放在转场动画开始的地方,那个地方才截图的话,太晚了。

因此需要在 UINavigationController 的 push、pop 接口地方做截图,另外全局右划的可中断 pop,截图是在手势开始的 begin,因为流程是 begin 开始后,才会调用到 UINavigationController 的 pop。

太多细节细节,不现场调试根本说不清楚,也不晓得会不会在后面的系统更新中变化,这里大体说了思路,以及需要注意的地方。

当然上面也提到过最后的效果,其实蛮不错的,

说一下前提,如果 UINavigationBar 设置了 translant 且真的用上了半透的话,效果应该是不行的,因为系统真实的 UINavigationBar 动画在底下会动,得实际遮住才行,因此这套东西,我是觉得只能用在非 translant 的场景下,当然,如果只是设置了 translant,但实际上又带了 opaque 的背景,仅仅是为了带一丢丢高斯模糊背景的话,是没问题的。

21年7月11日 周日 12:08

PinStackView

目前工程主要使用 AutoLayout,但一些复杂页面上面,先不说各种约束优先级的问题,单单就因为约束不对造成的约束失败,在 debug console 里面打印一大堆,都不好定位在哪里,别说去修复了。

总觉得大部分的场景,其实使用固定 frame 的 layout 就足够了,不需要那么复杂,于是我在工程里面引进了 PinLayout

因为使用 PinLayout 构建复杂页面确实太辛苦,其只用于搭建不复杂的元素,之前使用的是 StackViewLayout,但测试反馈问题比较多,我自己也发现,不大稳定,偶尔还有 crash,毕竟这个库是未完成的状态,然后还自带内存泄漏。

于是就想着自己写一个,毕竟 StackViewLayout 也用了这么久,接口已经蛮熟悉的了,而在功能上面,需要的又不多。

于是就有了 PinStackView,这个库相比 PinStackLayout 来说,简单很多,其实只有一个文件,另外,其处理流程上也是很简单的。

配置

最后增加了一个 autoSizeChangedCallback 的回调函数,如果 style 设置为了 auto,PinStackView 在 axis 上的长度由其 item 动态长度计算得到,每次对 bounds 有更改,都会 callback 通知。

另外上面的设置其实隐含了一些前提,在 fixed style 时,PinStackView 的大小是外部设置的,此时才有 start、end、equal 这些 distribution;而在 auto style 下,只有 start distribution,其实很好理解。

管理接口

下面是管理 view 的接口,添加、插入都会返回 PinStackItemInfo

item 配置

对于每个管理 view,在添加、插入的时候,都绑定了一个 PinStackItemInfo 用来描述其 layout 信息,下面的接口都可以进行链式配置

上面第二点,如果设置了 alignment 或者 alignSelf 为 stretch,会覆盖 item 在 cross axis 的长度

计算流程

建议看代码吧,其实没啥好说的,fxied style 下面,equal 就是平均 axis 的长度,start、end 的计算是最复杂的,因为可以带 grow(),需要先计算固定长度的,后面再分配动态长度的。

auto style 其实相对好计算,计算完所有管理 view 沿着 axis 的长度后,对比 PinStackView 在 axis 的长度,有变化的话就修改 PinStackView 的长度,然后 callback。说一下 callback,这个通知,可以回调到外部依赖 PinStackView frame 的地方,重新设定相关的 layout。

这个当然有可能造成 layout 的循环,之前的 StackViewLayout 就有这样的场景,我在这里将这个依赖交给了用户,用户自己来控制。

PinStackView 里面对于管理 view 的宽高计算是一个焦点,代码在 calcViewSize() 里面

将 StackViewLayout 换成 PinStackView 后,循环 layout 的场景不再出现,layout 也挺正常的,但是公司人员变动太大,貌似这个需求测完了,但上线遥遥无期,再说吧。

21年6月15日 周一 21:15

MoonCake Programming Language

从 4 月底开始就很少玩游戏,因为迷上了另外一件事情,基于 Lua,创建一个语法上类似 Swift 的编程语言,一方面学习 LPeg, 另外一方面,按照自己的想法,对 Lua 的语法修修改改。

基本上,Lua 用了很多的词来分割语意单元,比如 C/C++ 里面的语法块,是放在 '{' 和 '}' 包裹中的,在 Lua 中是 do 和 end 中,或者 then 和 end 中, 当内容一多的时候,都是词;还有就是 function,连 Swift 都是 func 就可以了,为何要一个完整的单词呢。

还有少不了的 local,避免全局变量污染,就只好都是 local 了,我觉得默认就行,总是 local 有点多余。

其实最开始就是上面这 3 点,当然也参考了一下有名的 MoonScript,因为它是 MIT 协议开源的嘛,还想抄来着,结果实际上,从头到尾几乎都是重新构建的, 只有 global name,真的是抄的,加上了 LuaJIT 的 jit 等不多的几个而已。

LPeg 的 P、S、Cg、Ct、Cmt、Cp、Cb、Cf,刚开始的一两天,真的头大,后面就变成肌肉记忆了。

LPeg 的匹配规则有两种,一种是全文完全匹配,得到的是完整的 AST,另外一种是寻找匹配,拿到部分匹配的内容。对于一门编程语言,只能选择第一种。

语言中的 class / struct 当然是基于 metamethod 的,其中,class 被设计为跟普通的 table 一样,可以随机创建 key / value,当然少不了继承;而 struct,想学 Swift,没有继承, 且 key 不能为 nil,为了效率考虑,子类以及实例,都会读取自己的类或者父类,如果不为 nil,就 rawset 到本地,下次访问就快了,但是对于父类不断变动的信息,实际上是获取不到的, 这种情况只有通过类方法来解决。

实际上还加了 extension 的逻辑,使得 struct 不能继承这点其实是可以被突破的,而且 class 和 struct 两者就单单使用上来说,区分没有那么大。

我还加了 guard、switch 关键字,都是 if 的语法糖。

还有 defer 这个关键字,处理比较有意思,实际上我是能做到在任何 scope 都是起作用的,但是我嫌麻烦,而且还读到某 Swift 大 V 直觉认为 Swift 的 defer 只支持 function scope 而导致某开源软件的 bug,当然我之前也是这么认为的,所以最后在这个语言中,我将 defer 限定为了 function scope,毕竟能用就行了,直觉理解不好的我觉得也不大好。

说一下 continue,我在这个语言中也实现了,基于 goto,我的理解 continue 其实就是 goto 的语法糖,为何 Lua 不选择支持不理解。

语言鼓励偏向 Swift 一样基于 class / struct 的数据和方法来编程,也有相关 VSCode 的 extension 做简单的支持,比如高亮、outline,至于 LSP 嘛,那是没有的。

最后爆地址,在 MoonCake.

21年5月24日 周一 22:10

编写 VSCode Extension

如果只是搭一个框架,VSCode Extension 的编写没有想象中那么无从入手,照着文档 VSCode插件开发全攻略 一步一步往下学就是了。

其实我的要求有 2 个

至于 language server 的支持,自然是不敢想的。

语法高亮

先说语法高亮吧,用不到 typescript 编程,只需要会正则就好,但是这个正则的规则,是跟 TextMate 一样的,文档在这里

这个正则默认是行匹配的,不会跨行,如果需要跨行,就需要设置 begin、end capture,另外,这个正则匹配,在设置的时候,就需要指定高亮颜色,偏偏 VSCode 提供的几种高亮颜色其实不怎么够用,我是想抄 EmmyLua 的一些高亮,比如全局变量的高亮,没学会人家是怎么搞的。

一些实践来的经验:

"strings_long": {
	"name": "string.quoted.single.name",
	"begin": "\\[(=*)\\[",
	"end": "\\]\\1\\]",
	"patterns": [
		{
			"name": "constant.character.escape.name",
			"match": "\\\\."
		}
	]
}

侧边栏大纲

这个需要 typescript 编程了,我的 typescript 小学生水平都可以搞出来 outline,想必是不难的。

先吐槽一下,typescript 是要编译成 javascript 才能用的,另外有引入库的话,记得 npm install,否则会像我一样,浪费了大量的时间在这些基础上面。

outline 其实已经是 language server 的范畴了,只不过我们简单处理,将这个抽象借口部署在本地,就是简单的文本处理就好。

不贴代码了,反正可以在网上找到大量的的例子,比如一个行处理,split 空格拿到 token 关键字的 LS,其实不堪用,但是我是没办法,暂时先这么用着了,毕竟海没学会更好的处理方法。

然后呢,最外层的循环用的是变量 i,我是刚开始不小心,里面一层小循环也用变量 i 了,然后这个 typescript 停不下来了,也是无语,没有语法 scope 的吗,难过。

分析 token 语法的结构,是提供给 VSCode 一个带层次的 node 列表,首层是 outline 第一级,如果需要二级的画,比如 class node 里面再塞入 funciton、property 之类的,就是在这个一级 node 的 children 列表里面塞入 node,VSCode 就会帮你将这个 outline 画出来。

对了,这些 node 需要指定类型,比如是 class,还是 function,还是 field,其实还有很多,property,namescope 之类的,但是实话说,只有前面这 3 个是比较好看的,其他的应该是我比较少见,不明白这些个 icon 是想干嘛。

有了上面这些基础,我也终于理解了好些 Lua extension 为何只用上面这 3 个 outline 类型了,另外,我还知道了为何这些 Lua extension 列出了一大堆 local 变量,明明更重要的应该是全局变量呀。

有个坑必须要说一下。

不晓得是不是 outline 默认了一个 node 就是一行的原因,同一行塞入多个 node,会造成接下来的行拿不到内容,我猜测是我小学生水平的 typescript 哪里搞错了,但具体到底是哪里,目前无从知晓。

先这样吧。