社区文档构建进行中,欢迎编辑。社区答疑群(非官方):717421103

全站通知:

模块和Lua

阅读

    

2024-07-28更新

    

最新编辑:本森级7号舰拉菲

阅读:

  

更新日期:2024-07-28

  

最新编辑:本森级7号舰拉菲

来自WIKI实验室WIKI_BWIKI_哔哩哔哩
跳到导航 跳到搜索
页面贡献者 :
卡拉彼丘WIKI拉菲
Lu_23333

什么是模块

模块(Module)是一个MediaWiki的官方插件功能。该插件允许编写者在MediaWiki中,使用Lua语言来编写模块,完成一些批量工作的处理。同时模块也是Lua在MediaWiki执行的基础,Lua代码必须被写成若干个“模块”进行使用。所有模块,需要建立在命名空间Module:(模块:)下。

模块可以理解为是一个“高级形态”的模板。


什么是Lua

Lua是一种小巧的脚本语言。它是巴西里约热内卢天主教大学(Pontifical Catholic University of Rio de Janeiro)里的一个由Roberto Ierusalimschy、Waldemar Celes 和 Luiz Henrique de Figueiredo三人所组成的研究小组于1993年开发的。其设计目的是为了通过灵活嵌入应用程序中从而为应用程序提供灵活的扩展和定制功能。

  • Lua常见使用场景:
    • 游戏开发
    • 独立应用脚本
    • Web 应用脚本
    • 扩展和数据库插件如:MySQL Proxy 和 MySQL WorkBench
    • 安全系统,如入侵检测系统


什么时候可以用模块

在复杂的模板中,我们通常会使用各种解析函数来实现类似程序语言的逻辑判断和循环(比如{{#if}})。

当一个模板比较简单,只会使用到少量解析函数的时候,解析函数是一个非常方便的功能。

但是,当一个模板需要做大量逻辑判断、循环之类功能的时候,就会导致模板的代码臃肿不堪,陷入由{{}}组成的花括号地狱当中,这样的模板难以阅读理解和维护。

这种时候就更适合使用模块来进行编写。

模块使用了真正的程序语言Lua,使其可以处理更复杂的逻辑判断,相较于模板,模块有以下优势:

  • Lua代码的可读性更高,所以可以更轻松的制作出功能更丰富、也更利于维护的模板。
  • Lua的运行效率也比解析函数更高,根据页面解析函数的嵌套层次、复杂程度,改写成Lua可以显著提升页面的加载速度,解析函数嵌套越多、调用次数越多、使用的SMW和Loop类函数越多,改写成Lua的效果就越明显。

但由于模块使用Lua语言编写,其上手难度相较于模板有一定难度。


变量(variables)和值(value)

变量是数据的“命名存储”,即变量可以用来存储我们需要的数据,而且这些数据我们一般称之为“值”。

我们可以简单的将变量看作是一个装着数据的盒子,而盒子上有一个唯一的标注着盒子名字的贴纸。

在Lua中我们要为一个变量起名时,需要用到local,即声明变量,比如像下面这样:

local test

这样我们就声明了一个名字叫test的变量,接下来我们要给这个变量存入一些数据,即赋值:

local test
test = "world"

这样变量test的值即为"world",同时我们在声明变量的时候也可以一并赋值:

local test = "world"


Lua的变量共有三种类型:全局变量、局部变量、表(table)中的索引。

  • 全局变量:在整个模块中都有效的变量
  • 局部变量:只在声明的语句块(函数、for等)中生效的变量
  • 表中索引:是需要根据table和索引名,来确定到一个变量

Lua中的变量名称可以使用字母、下划线和数字组成变量名,但也存在一些限制,例如:

  • 数字不能作为变量名开头
  • 下划线后面不能接一个或多个大写单词
  • 不能使用保留的关键字作为变量名

同时Lua是大小写敏感类型的语言,and是关键字不能作为名称使用,但是And和AND就是两个不同的有效名称了。

关键字(keywords)和保留字(reserved words)

关键字是编程语言中一部分具有特殊含义和预定义功能的特殊字符,如上文提到的local就是Lua语言的关键字之一。 关键字通常用于语言本身提供的结构和功能,例如控制语句、声明赋值等。这些关键字通常是事先定好的,一般开发者无法更改或重新定义。


保留字是指在当前版本的编程语言中被保留但尚未使用的标识符,这些标识符一般不能被用作变量、函数的名称。 它们可能会在将来的版本中被引入并具有特定的功能。

可以说关键字是保留字的子集,所有的关键字都是保留字,但所有的保留字并非都是关键字。

Lua中的关键字
and break do else elseif end
false for function goto if in
local nil not or repeat return
then true until while

数据类型

在Lua中数据一共分为8种基本类型,分别为nilboolean(布尔值)string(字符串)number(数值)function(函数)table(表)threaduserdata

threaduserdata一般在模块中并不会用到,因此这里不展开讲解。

nil

Lua语言中的空值,该类型只包括nil一个值。所有未被赋值过的变量它的值都会是nil。当你为一个变量赋值nil时,相当于删除了这个变量。

boolean(布尔值)

布尔值类型只有两个值:truefalse,注意这2个值的所有字母均为小写。布尔值会作为关系运算的结果,或用于逻辑运算之中。在Lua中,只有falsenil会被视为“假”,其他值均为“真”(包括0)。 关系运算是指比较两个值之间的关系,例如5 > 10的结果就是false;逻辑运算是指对布尔值进行运算,Lua中的逻辑运算包括and(与)or(或)not(非),例如true and true,结果是true,具体后续会详细介绍。

string(字符串)

该类型用于保存文本类型的值,字符串通常有3种表示形式:

  • 两个英文半角单引号之间的一串字符,例如'字符串''string'
  • 两个英文半角双引号之间的一串字符,例如"字符串""string"
  • 一组成对的英文半角双方括号之间的一串字符,例如[[字符串]][[string]]

需要注意的是使用英文半角引号表示字符串时,前后引号必须一致,即前后同为单引号或前后同为双引号。

如果要在字符串内加入英文半角引号请使用与字符串不同的引号,例如:

  • 当字符串为英文半角单引号时,字符串内部需使用英文半角双引号,例'这是个"字符串"'
  • 反之也是可以的,例"这是个'字符串'"
  • 另外也可以使用转义符\,例"这是个\"字符串\""

number(数值)

该类型用于保存数字类型的值。

function(函数)

函数是Lua中的一种特殊数据类型。主要是为了处理一部分特定工作(尤其是需要重复进行的工作)时使用的功能。Lua提供了很多内建函数,也可以自行编写函数来完成某些操作。函数在Lua中的作用相当于WIKI中的模板。事实上模块的各种功能都是由模块中1个或者多个函数来完成的。

table(表)

表是Lua中唯一的组织数据结构的机制,它可以用于表示一般数组、列表、符号表、集合、记录、图、树……等等,表的值也可以是任意类型。


运算符

Lua中的运算符由算术运算符关系运算符逻辑运算符其他运算符组成。

算术运算符

用于进行基本的数学运算的符号。

+ 加法 10+5 结果为15
- 减法 10-5 结果为5
* 乘法 10*5 结果为50
/ 除法 10/5 结果为2
% 求余 10%3 结果为1
^ 乘幂 5^3 结果为125
- 负号 -10 结果为-10

关系运算符

关系运算符是比较两个数据之间关系,需要注意的是,>>=<<=主要用于数值类型的比较,字符串会优先转成数值(转换为nil时,关系运算会报错)。

== 等于 检测两个值是否相等,相等则返回true,否则返回false
~= 不等于 检测两个值是否不相等,不相等则返回true,否则返回false
> 大于 如果左侧的值大于右侧的值,则返回true,否则返回false
>= 大于等于 如果左侧的值大于或等于右侧的值,则返回true,否则返回false
< 小于 如果左侧的值小于右侧的值,则返回true,否则返回false
<= 小于等于 如果左侧的值小于或等于右侧的值,则返回true,否则返回false

逻辑运算符

可以用于比较和判断任意类型的数据,类似我们日常交流中的“并且”,“或者”,“除非”等思想。

and 逻辑与 当(A and B)时,如果A为false,则返回A(即返回false),否则返回B
or 逻辑或 当(A or B)时,如果A为true,则返回A,否则返回B
not 逻辑非 返回逻辑非运算的对象相反的逻辑值,如(not A),当A为true时,返回false;当A为false时,返回true

其他运算符

.. 字符串连接符 使用..连接两个字符串时,可以将两个字符串拼接成为一个新的字符串
# 返回字符串或表的长度 在字符串或是表前加#,将返回其长度数值。不过需要注意的是
  • 该运算符对中文支持不好
  • 该运算对表使用时,表只能是数组形式,其他形式会产生意料外的情况


函数(function)

当我们要编写一个函数需要用到function声明一个函数的名称和对应的参数列表,比如像下面这样:

    function 函数名(参数列表)
      执行代码
    end
     
    -- 与上面等价的另外一种基本声明方式
    函数名 = function (参数列表)
      执行代码
    end

函数默认会声明为全局函数,添加local可以将其声明为局部函数。

    local function 函数名(参数列表)
      执行代码
    end

函数可以预设0个或多个参数,当有预设参数位置时,调用函数时就可以传入对应数量的参数。一般调用函数时,参数需要用一组小括号(( ))表示。

不过,有一种特殊情况是,传入的参数有且仅有一个table的时候,可以不使用({...}),改用{...}(省略掉小括号)。

-- 预设了3个参数位置,分别命名为a, b和c
function test(a,b,c)
  mw.log(a+b+c)
end

--调用函数test并分别传入3个参数
test(1,2,4)
-- 结果
7

当传入参数和预设参数位置数量不一样时:

  • 如果传入参数较多,多余的参数将被舍弃
  • 如果预设参数位置较多,不足的参数将赋值nil

函数可以使用return设置返回值,传递运算结果给上级。可以同时返回多个值,返回多个值的时候,接收处也需要设置多个变量来接收多个返回值。

function test(...)
  local result = 0
    for i in ipairs(arg) do
      result = result + i
    end
  return 
end
     
local x = test(1,2,3,5,6,7,3,10)
  mw.log(x)
-- 结果
36

表(table)

Lua的table是一种关联型的数组,每个值都有一个与之对应的索引,索引一般使用字符串(string)或是数值(number)。

table并不固定大小,随着添加的索引越来越多,会自动扩容。

在Lua中,table除了用来当做其他变成语言中的列表(list),字典(dictionary)或是数组(array)等数据类型外,还用来解决模块、包(package)和对象(object)这些功能的实现。

需要注意的是,Lua的表从1开始索引,而不是0

基本使用

local test = {}

在Lua中,表用花括号{}表示,上述代码中我们声明了一个叫test的空表。

在Lua中,可以通过[]选中一个key索引进行赋值:

-- 为表test中[1]赋值为'Hello'
test[1] = 'Hello'

-- 为表test中['name']赋值为'Bwiki'
test['name'] = 'Bwiki'

同时我们也可以通过赋值nil来删除某个key值或者整个表:

-- 通过赋值 nil 删除一个索引对应的内容
test[1] = nil

-- 通过赋值 nil 删除table
test = nil

表的索引

    local table = {
      ['name'] = { ['first'] = 'hello', ['second'] = 'world' },
      ['age'] = 18,
      ['job'] = 'unkonwn'
    }

首先我们建立一个表,同时这个表是一个表套表的结构,那么:

  • table['job']的值就是'unkonwn'(key是‘job’的值)
  • table[1]的值是nil(因为表中没有以[1]为索引的内容)
  • table['name']['first']的值是'hello'

常见表中内容形式

数组(Array)

数组是一种线性数据结构,用于存储相同类型的元素。数组中的每个元素都有一个索引,用于访问和修改它们。在Lua中,数组形式的table索引是从1开始,依次递增的自然数索引。数组类的table会有许多额外的特性。

local table1 = { 'first' , 'hello', 'second' , 'world' }
local table2 = { 5 , 7, 10 , 15 }
数组形式的表的常用操作
table.insert(t, value) 在表t的结尾处,插入value
table.insert(t, pos, value) 在表t的第pos位,插入value,新插入值的索引为pos,原来及之后位置的值索引值顺次增加1
table.remove(t, pos) 从表t中,移除位于pos位置的元素。如果pos未指定,则移除最后一个元素
table.concat(t, sep, i, j) 将表t中,所有元素,依次以分隔符sep(默认为"")连接。ij分别可以指定起始索引和结束索引,默认为整个数组
table.sort(t, comp) 对表t进行排序。comp是排序函数,如果没有给出的话,sort将按照升序进行排列,如果给出的话需要给出一个函数

字典(Dictionary)

字典是一种键值对应(key-value)的数据结构,用于存储具有唯一键的元素,字典允许通过键快速查找对应的值。

local table1 = { ID = 10101, Profession = "守护", Camp = "欧泊", NameEn = "Michele Lee"  }
local table2 = { ["护甲值"] = 40, ["护甲回复值"] = 0, ["移动速度"] = 57, ["弦化移速"] = 66 }


分支结构

当需要程序根据条件判断接下来做什么的时候,就需要使用分支结构,Lua的分支结构,是使用if语句进行的。

该语句与解析器函数{{#if}}是一个原理,可以用替代该解析器函数。

if语句

    if 条件 then
      -- 执行满足条件时的操作代码
    end

注意:条件后必须跟then,语句结束必须有end。

条件可以是任何值,通常会使用关系运算符或逻辑运算符进行计算获取条件是否成立(true或false)

注意:在Lua中,只有false和nil为假,true和其他所有值均为真,所以在Lua中,数字0也是true。

    if emoterole == "米雪儿·李" then
        emoterole2 = "米雪儿"
    end

这是一个简单的if语句代码片段,大致的逻辑是当变量emoterole的值是"米雪儿·李",则变量emoterole2的值为"米雪儿"

if...else语句

如果判断条件后,对不满足条件的项目也要进行特定处理的话,可以使用if...else语句:

    if 条件 then
      -- 执行满足条件时的操作代码
    else
      -- 执行不满足条件时的操作代码
    end

elseif语句

当需要并列的数个条件,分别处理时,可以使用elseif语句,该语句功能类似解析函数{{#Switch}}

    if 1个条件 then
      -- 执行满足第1个条件时的操作代码
    elseif 2个条件 then
      -- 执行满足第2个条件时的操作代码
    elseif 3个条件 then
      -- 执行满足第3个条件时的操作代码
    else
      -- 执行上述三个条件都不满足时的操作代码
    end

if嵌套

同时if语句可以进行嵌套,当需要递进式的判断时,则可以使用嵌套式的if语句:

    if 第一个条件 then
      if 第二个条件 then
      	-- 执行同时满足第1个条件和第2个条件时的操作代码
      elseif 第三条件 then
      	-- 执行同时满足第1个条件和第3个条件时的操作代码
      else
      	-- 执行满足第1个条件,但不满足第2个或第3个条件时的操作代码
      end
    else
      -- 执行不满足第1个条件时的操作代码
    end


循环结构

很多时候,我们都需要让程序作出一系列有规律的重复操作,这个时候就需要循环结构。

一个循环结构需要包括循环条件和循环语句:

  • 循环条件:当满足循环条件时,会进行下一次循环的执行,当不满足循环条件时,将会结束循环,执行后面的代码
  • 循环语句:每次满足循环条件后,程序会执行一次循环语句

Lua提供了三种循环方式:for循环、while循环和repeat循环,并且提供了break来控制循环。

for循环

for循环有两类使用方法,第一种是数值循环,第二种是迭代器循环。

数值循环

数值循环,是指一组有规律的数组,依次作为变量进行循环。

    for var=exp1, exp2, exp3 do
      -- 需要执行的循环语句
    end

这个语句表示,var作为变量,以exp1作为第一个值,每次变化exp3,当大于exp2时,循环停止。

exp3可以省略,默认值为1,也可以设为负数(如果设为负数,exp1需要大于等于exp2

注意:循环条件后面需要跟do,循环结束处需要使用end

栗子:
    for i=1,10.5,2.3 do
      mw.log(i)
    end
     
    --结果
    1
    3.3
    5.6
    7.9
    10.2

迭代器循环

迭代器循环是使用迭代器生成用于循环的值的列表,按顺序将这些值进行循环,常用的迭代器有以下三种:

ipairs 该迭代器主要用于循环数组类的表,它从表里索引为1的内容开始,依次递增索引值查找值,当遇到返回值为nil时结束迭代。
pairs 该迭代器主要用于循环不是数组的表,它会取出表里所有的值,但因为机制的问题,pairs取值是乱序的,不适合输出,只能用于整理数据
mw.ustring.gmatch 不适合初学者,请参考匹配字符串迭代器
栗子:
    var name_list = {'张三', '李四', '王五'}
    for index,name in ipairs(name_list) do
      mw.log(name)
    end
     
    -- 结果
    张三
    李四
    王五

这段代码中,我们使用了ipairs将表name_list中的值依次取出。

while循环

while循环的使用方法如下:

    while 循环条件 do
      -- 需要循环执行的代码
    end

当循环条件为true时,将会继续进行循环,否则将会结束循环。

所以while循环的循环条件,大部分时候需要提前设定,并且要在循环体中进行调整,否则会进入无限循环(死循环)。

repeat循环

repeat循环的使用方法如下:

    repeat
      -- 需要循环执行的代码
    until 循环条件

当循环条件为true时,将会结束循环,否则将会继续循环

与while循环不同的是,repeat循环总是会执行至少一次。

循环控制语句

为了更容易控制循环,Lua中提供了break语句来退出当前循环。

注意使用break语句只能退出一层循环。

栗子:

    for i=1,10 do
      if i > 5 then
        break
      end
      mw.log(i)
    end
     
    -- 结果
    1
    2
    3
    4
    5


传入页面参数

我们在页面调用模块时通常会使用下面的代码:

{{#invoke:模块名|函数名|参数1|参数2...}}

而模块侧这里主要有两种接收参数的方法。

1.自模板取用参数

这种方法的传递逻辑是内容页面——模板——模块,内容页面调用模板,模板获得内容页面的模板参数,然后模板内使用{{#invoke:}}调用模块并将参数传递给模块中的函数。

栗子:

内容页面部分:

{{测试模板|24|tool|name=工具}}

模板部分:

{{#invoke:测试模块|test|{{{1|}}}|{{{2|}}}|{{{name|}}}}}

模块部分:

local p = {}
 
 
function p.test(frame)
  local args = frame.args
  local num = args[1]
  local type = args[2]
  local name = args['name']
  ...
  
end
 
return p

最终在模块侧这里变量num的数据为模板匿名参数1;变量type的数据为模板匿名参数2;变量name的数据为模板参数name。

2.自内容页面取用

这种方法不要在模板{{#invoke:}}指定要传递的参数,此方法需要搭配模块:Arguments使用。

栗子:

内容页面部分:

{{测试模板|24|tool|name=工具}}

模板部分:

{{#invoke:测试模块|test}}

模块部分:

local p = {}
local getArgs = require('Module:Arguments').getArgs
 
function p.test(frame)
  local args = getArgs(frame)
  local num = args[1]
  local type = args[2]
  local name = args['name']
  ...
end
 
return p

跨模块调用

在编写模块时,我们可以将通用性较高的函数单独存放在一个模块中,然后通过跨模块调用的方式来调用它们,从而缩减特定模块页面的代码篇幅,并实现模块化开发。与其他变成语言类似,在MediaWiki中,我们可以通过require( modulename )来实现代码块的引入。

本地化声明外部函数:

local _getImage =require('Module:GetData')._getImage
local _textTrim =require('Module:GetData')._textTrim
栗子:
--这是第一个模块Test的代码
local p = {}
local Test2 = require('Module:Test2')
 
function p.pinjie(frame)
  return Test2.do(frame.args[1])
end
 
return p
--这是第二个模块Test2的代码
local p = {}
 
function p.do(text)
  return '《'..text..'》'
end
 
return p

当我们在页面调用第一个模块Test中的pinjie函数并传入“这是个栗子”内容时,

{{#invoke:Test|pinjie|这是个栗子}}

模块Test的pinjie函数就会调用模块Test2的do函数并为传入内容前后添加,所以最终返回到页面的内容即为《这是个栗子》


挂载数据

当模块所需要的数据较多时,将数据新建页面存放和模块的逻辑代码分开是一种非常理想的方式。

挂载Lua表形式数据

我们假设在模块页面下新建一个子页面并命名为Data,数据格式如下:

return {
	["至纯物质-S1"] = {
		ID = 33000001,
		Quality = "精致",
		File = "勋章_33000001.png",
		Get = "第1赛季至纯物质段位奖励",
		Desc_string = "物质的尽头是以太,它一直延续到宇宙的终极。",
	}, 
	["升格分子-S1"] = {
		ID = 33000002,
		Quality = "精致",
		File = "勋章_33000002.png",
		Get = "第1赛季升格分子段位奖励",
		Desc_string = "初步开始变化的物质形态。",
	}, 
	["虚空原子-S1"] = {
		ID = 33000003,
		Quality = "精致",
		File = "勋章_33000003.png",
		Get = "第1赛季虚空原子段位奖励",
		Desc_string = "达到原子级别的物质形态。",
	}
   ...
}

在模块页可以使用mw.loadData('数据页名字')来挂载数据,例如:

local data = mw.loadData('module:Test/Data')

这样我们就将上面的数据挂载到了变量data中。

实际用例:

一些Wiki使用了相关特性,如下所示这个静态列表可能在下列页面更改后过时仅供批判性参考

挂载Json形式数据

挂载Json形式数据与Lua表类似,可以使用mw.text.jsonDecode(mw.title.new('数据页名字'):getContent())

local data = mw.text.jsonDecode(mw.title.new('module:Test/Data.json'):getContent())
实际用例:

一些Wiki使用了相关特性,如下所示这个静态列表可能在下列页面更改后过时仅供批判性参考

参考资料