Lua 脚本重启 机制

不管是 现在开发中的游戏服务端, 还是近期love2D 开发的前端, 都使用 Lua 做脚本引擎, 需要涉及到 脚本的修改和重启. 第一种方法是 写个封装函数, 里面进行对所有 lua 脚本文件的 require() 操作, 这就要求 :

1.对每个支持重新加载的文件进行

package.loaded[ filename]  = nil
require( filename)

2.文件加载要保持一定的顺序, 以免造成资源的错乱.

就当前使用 love2D 前端来看, 其实只有一个 "启动"文件: main.lua, 并在其内进行 各个子功能脚本的 require 加载.如果在 重新加载时, 自动按照 main.lua 提供的

require(...) 顺序进行自动加载就好了, 并且无需像上面的针对每个文件编写:

function reload_files()
    require( f1)
    require( f2)
    ...

end

整体目标有几个:
1.无需静态维护一个重新加载的文件, 或函数, 进行编写 各个脚本文件的 require() 进行重新加载;

2.能够按照当前文件中各个文件的 顺序进行加载, 即如果

--main.lua

require( "config")
require( "function")
require( "globals")
require( "gameplayer")
require( "scene") 

NeedReset = true
...

这种顺序编写main.lua( 或其他文件), 都尽量保持 config > function > globals > gameplayer > scene 的顺序进行重新加载;
3.能够避免 "已被重新记载的文件" 再次被重新加载;

4.能够避免 嵌套递归加载;

5.能够对 外部库进行识别, 即 

require( "bit")

是在加载 "位操作"的 库 bit.dll , 而不是 bit.lua, 不应该进行 嵌套加载;
6.能够识别某些 "禁止重新加载"的文件, 例如:

-- global.lua

require( "skill_cfg")
require( "effect_cfg")

g_object_list = {}

global.lua 文件本身不能被 多次require(), 不然 g_object_list 全局变量会被重置, 但又能够不会影响 skill_cfg 和 effect_cfg 的重新加载;

7.应该要支持 "后序" 方式进行加载, 记载加载 main.lua 过程中, 应该现在递归加载完 子脚本文件:

require( "config")
require( "function")
require( "globals")
require( "gameplayer")
require( "scene")

然后在进行 加载 main.lua 的后序内容:

NeedReset = true
...

8.能够 识别 文件中的 require(...) 行.

大概这 8 点目标 和要求, 但对于第7点, 有个问题:

假设 重新加载 的 递归函数为

function recursive_reload( filename)

   package.loaded[ filename] = nil  
   require( filename ) end

并且main.lua 的内容简单有如:

--main.lua

require( "config") 
require( "function") 
require( "globals") 
require( "gameplayer") 
require( "scene")

NeedReset = true

在 触发重新加载的 入口中:

function main_reloader()  
    recursive_reload( "mian" ) 
end

调用 main_reloader() 进行重新加载的过程 展开将会如:

--先递归地使用 recursive_reload() 重新加载子文件 

package.loaded[ 'config'] = nil 
require( 'config')

package.loaded[ 'function'] = nil 
require( 'function')

package.loaded[ 'globals'] = nil 
require( 'globals')

package.loaded[ 'gameplayer'] = nil 
require( 'gameplayer')

package.loaded[ 'scene'] = nil 
require( 'scene')

--再最后加载 main.lua 
package.loaded[ 'main'] = nil 
require( 'main') --但就在这个操作中, 还会涉及到嵌套的:  
require( "config")  
require( "function")  
require( "globals")  
require( "gameplayer")  
require( "scene")  

NeedReset = true



这 5 个 文件不就会被 多次 require() 了吗? 虽然 完整的 recursive_reload() 能够防止 "显示的" 重复require(),  但是不能禁止 "隐式的" require() 其实, 就算第二次的 "隐式" requre() 确实会调用, 但不会重新加载 实际的物理文件, 见于 lua 开发手册上:

require (modname) Loads the given module. 

The function starts by looking into the package.loaded table to determine whether modname is already loaded. 
If it is, then require returns the value stored at package.loaded[modname].
Otherwise, it tries to find a loader for the module.

即是说, 只要曾经加载了 文件, 并在 package.loaded 内有记录, 后序的 requre() 将会直接返回.

这 5 个 文件不就会被 多次 require() 了吗? 虽然 完整的 recursive_reload() 能够防止 "显示的" 重复require(),  但是不能禁止 "隐式的" require() 其实, 就算第二次的 "隐式" requre() 确实会调用, 但不会重新加载 实际的物理文件, 见于 lua 开发手册上:

require (modname) Loads the given module. The function starts by looking into the package.loaded table to determine whether modname is already loaded. If it is, then require returns the value stored at package.loaded[modname]. Otherwise, it tries to find a loader for the module.

即是说, 只要曾经加载了 文件, 并在 package.loaded 内有记录, 后序的 requre() 将会直接返回.

具体运行效果:

只是具体的实现代码:

-- 外部库 登记
local package_list = {
    bit = true 
}

-- 全局性质类/或禁止重新加载的文件记录
local ignored_file_list = {
    global = true ,
}

--已重新加载的文件记录
local loaded_file_list = {}

--视图排版控制
function leading_tag( indent )
    -- body
    if indent < 1 then
        return ''
    else
        return string.rep( '    |',  indent - 1  ) .. '    '
    end
end

--关键递归重新加载函数
--filename 文件名
--indent   递归深度, 用于控制排版显示
function recursive_reload( filename, indent )
    -- body
    if package_list[ filename] then 
        --对于 外部库, 只进行重新加载, 不做递归子文件
        --卸载旧文件
        package.loaded[ filename] = nil

        --装载信文件
        require( filename )

        --标记"已被重新加载"
        loaded_file_list[ filename] = true

        print( leading_tag(indent) .. filename .. "... done" )
        return true
    end

    --普通文件
    --进行 "已被重新加载" 检测
    if loaded_file_list[ filename] then 
        print( leading_tag(indent) .. filename .. "...already been reloaded IGNORED" )
        return true
    end

    --读取当前文件内容, 以进行子文件递归重新加载
    local file, err = io.open( filename..".lua" )
    if file == nil then 
        print( string.format( "failed to reaload file(%s), with error:%s", filename, err or "unknown" ) )
        return false
    end

    print( leading_tag(indent) .. filename .. "..." )

    --读取每一行
    for line in file:lines() do 
        
        --识别 require(...)行, 正则表达? 模式匹配? 并拾取文件名 到 subFileName
        line = string.gsub( line, '%s', '' )
        local subFileName = nil 
        local ret = string.gsub( line, '^require%("(.+)"%)', function ( s ) subFileName = s end )

        if subFileName then
            --进行递归 
            local success = recursive_reload( subFileName, indent + 1 )
            if not success then 
                print( string.format( "failed to reload sub file of (%s)", filename ) )
                return false 
            end

        end
        
    end    


    -- "后序" 处理当前文件...

    
    if ignored_file_list[ filename] then
        --忽略 "禁止被重新加载"的文件
        print( leading_tag(indent) .. filename .. "... IGNORED" )
        return true
    else

        --卸载旧文件
        package.loaded[ filename] = nil

        --装载新文件
        require( filename )

        --设置"已被重新加载" 标记
        loaded_file_list[ filename] = true
        print( leading_tag(indent) .. filename .. "... done" )
        return true
    end
end

--主入口函数
function reload_script_files()
    
    print( "[reload_script_files...]")

    loaded_file_list = {}

    --本项目是以 main.lua 为主文件
    recursive_reload( "main", 0 )
    
    print( "[reload_script_files...done]")

    return "reload ok"
end

备注: 该机制只支持简单文件目录

原文地址:https://www.cnblogs.com/Wilson-Loo/p/3301989.html