巧设 Openresty 包含路径

路径的难题

  我在之前的一篇文章中提到,关于 OpenResty 中的文件包含路径,是个值得注意的问题。
  OpenResty 仅会将它自己的 lualib 目录加入 package.pathpackage.cpath,我们的项目目录需要自己处理。
  最初我曾经试过将 项目目录加入到 package.pathpackage.cpath 中,确实达到了目的。
  但在 nginx 上配置了第二个 server 并将它的目录也加入包含路径之后,由于 lua_code_cache 的存在,不同 server 间的相同相对路径的文件的缓存会互相冲突,导致 require 可能不能加载正确的文件。  

巧妙的方案

  其实解决方案来自 PHP 中 autoload 的启发,我使用了一些 nginx 配置和一个单独的函数用来加载项目中的文件。

  服务器上我的目录结构如下:

目录结构

  如图所示,所有的 server 都放在 /data/app/web 下,由于.在 lua 包含路径中的特殊含义,使用 OpenResty 的项目目录中的 . 都替换成了 _
  在 nginx 配置中,我将 /data/app/web 加入到了 package.pathpackage.cpath

lua_package_path '/data/app/web/?.lua;;';
lua_package_cpath '/data/app/web/?.so;;';

server
{
    listen 80;
    server_name dev.els.kunlun.com;
    
    set $SERVER_DIR dev_els_kunlun_com;
    set $BASE_PATH /data/app/web/$SERVER_DIR;    

    root $BASE_PATH/webroot;
    index index.html index.htm;
    
    location = / {
        lua_code_cache on;
        content_by_lua_file $BASE_PATH/lua/main.lua;
    }
}

  入口文件代码如下:

--- 定义NULL常量
_G.NULL = ngx.null

--- 置换系统 Require 函数
_G.loadMod = function(namespace)
    return require(ngx.var.SERVER_DIR .. ".lua." .. namespace)
end

--- 加载主模块
_G.loadMod("core.app"):run()

  通过利用 lua_package_path 配置对 require 进行封装,我构造了全局的 loadMod 函数用于加载项目中的文件。实际上,下面的代码是等同的:

local model1 = loadMod("core.app")
local model2 = require("dev_els_kunlun_com.lua.core.app")

  通过 lua_package_path 中追加的所有项目的主目录,require 能够加载到正确的文件。   

进阶的方案 

  这个方案看起来已经很好的达到了我们的目标,但是实际使用中,却不是那么方便。
  在加载 OpenResty 的自有扩展和库的时候,我们还是必须使用 require,否则将会因为路径不对找不到文件。比如:

local json = require("cjson")
local mysql = require("resty.mysql")
local util = loadMod("core.util")
local exception = loadMod("core.exception")
local counter = loadMod("core.counter")
local dbConf = loadMod("config.mysql")
local sysConf = loadMod("config.system")

  看起来好像没什么问题,但是实际编码过程中,不经意写错的时候还是很多的,确实不是很方便。于是,我开始构思一个简易通用的方案。

--- 定义NULL常量
_G.NULL = ngx.null

--- 已加载的包
local __loadedMods = {}

--- 加载模块
--
-- @param string namespace 模块名
-- @return table 模块
_G.loadMod = function(namespace)
    -- 查找系统模块
    local module = __loadedMods[namespace]

    if module then
        return module
    end

    -- 查找项目模块
    local pNamespace = ngx.var.SERVER_DIR .. ".lua." .. namespace
    local pModule = __loadedMods[pNamespace]

    if pModule then
        return pModule
    end

    -- 尝试加载系统模块
    local ok, module = pcall(require, namespace)

    if ok then
        __loadedMods[namespace] = module
        return module
    end

    -- 尝试加载项目模块
    local ok, module = pcall(require, pNamespace)

    if ok then
        __loadedMods[pNamespace] = module
        return module
    end

    -- 模块加载失败
    error(module, 2)
end

  现在不管是项目的文件还是自带的扩展和库,都可以使用 loadMod 加载,不需要再去分辨加载的是否是项目自己的文件。
  我在三个 server 的入口文件中,都加上了这段代码,但转念一想,使用重复的代码可不是一个好程序猿的习惯。于是,我将上面的代码保存为 init.lua,使用 init_by_lua_file 来加载。这样,我们将一个文件加载一次,所有项目就都能够使用了。

init_by_lua_file /usr/local/openresty/luainit/init.lua;

结语 

  这个最终方案我们的项目正在使用,配置简单,使用起来也很方便,而且经过了2个线上项目的检验,没有发现缺陷。

分享 评论
comments powered by Disqus