eggjs / egg

🥚 Born to build better enterprise frameworks and apps with Node.js & Koa

Home Page:https://eggjs.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

配置中的属性能否支持上symbol

simpul opened this issue · comments

commented

请详细告知你的新点子(Nice Ideas):

现状:
在配置文件中以symbol作为属性自定义配置值后,通过app.config访问会丢失这些配置值。

期望实现:
在配置文件中,能够以symbol作为属性,做一些自定义的配置。(看了一圈issue好像没有人提到过

理由:
目前我使用的业务框架继承自egg,内部集成了很多插件和相关的配置。在写业务的时候担心写自定义配置时,因使用了重复的属性名导致配置冲突所以考虑使用symbol作为属性。

能够做些什么:
配置的合并使用了extend2模块进行深度拷贝,看了下源码没有对symbol值做拷贝处理所以导致symbol丢失。我自己参考lodash的深拷贝方法在extend2模块上添加了symbol值的拷贝处理后实现了这个功能。想问下大佬这里是否可以给extend2模块提个pr然后让egg更新依赖版本支持上这个功能。

可以贴一下示例代码。

commented

可以贴一下示例代码。

@fengmk2 感谢大佬回复,下面是我的一个现状还原和实现的过程:


在项目配置文件中添加一个symbol指向的自定义配置(SYMBOL_PERMISSION):

// config/config.local.ts
import { EggAppConfig, PowerPartial } from 'egg';
import { SYMBOL_PERMISSION } from '../utils/constants'; // export const SYMBOL_PERMISSION = Symbol('Middleware#permission');

export default (appInfo) => {
    const config: PowerPartial<EggAppConfig> = {};

    config.security = {
        csrf: {
            enable: false,
            ignoreJSON: true,
        },
        domainWhiteList: ['*']
    };

    const bizConfig = {
        permission: {
            enable: true,
            whitelist: ['simpul']
        },
        // 自定义配置
        [SYMBOL_PERMISSION]: {
            permission: {
                msg: 'local',
                author: 'simpul'
            }
        }
    };

    return {
        ...config,
        ...bizConfig,
    };
};

期望在我自己实现的中间件permission中访问到对应配置项,但是结果返回undefined:

// app/middleware/permission.ts
import { Context } from 'egg';
import { SYMBOL_PERMISSION } from '../../utils/constants';

export default function permission(): any {
    return async (ctx: Context, next: () => Promise<any>) => {
        const config = ctx.app.config.permission;
      	console.log((ctx.app.config as any)[SYMBOL_PERMISSION]);// 这里ctx.app.config丢失SYMBOL_PERMISSION属性, 返回undefined
        if (config?.enable) {
            if (!config.whitelist.includes(ctx.user.nick)) {
                // 没有权限
                ctx.status = 403;
                ctx.body = {
                    code: 403,
                    message: 'You do not have permission',
                };
                return;
            }
        }
        return await next();
    };
}

发现需要补充extend2模块拷贝symbol的能力,这里我参考了lodash模块深拷贝的实现改了下源码:

// node_modules/extend2/index.js

'use strict';

var hasOwn = Object.prototype.hasOwnProperty;
var toStr = Object.prototype.toString;
var propertyIsEnumerable = Object.prototype.propertyIsEnumerable;
var nativeGetSymbols = Object.getOwnPropertySymbols;

var isPlainObject = function isPlainObject(obj) {
  if (!obj || toStr.call(obj) !== '[object Object]') {
    return false;
  }

  var hasOwnConstructor = hasOwn.call(obj, 'constructor');
  var hasIsPrototypeOf = obj.constructor && obj.constructor.prototype && hasOwn.call(obj.constructor.prototype, 'isPrototypeOf');
  // Not own constructor property must be Object
  if (obj.constructor && !hasOwnConstructor && !hasIsPrototypeOf) {
    return false;
  }

  // Own properties are enumerated firstly, so to speed up,
  // if last one is own, then all properties are own.
  var key;
  for (key in obj) { /**/ }

  return typeof key === 'undefined' || hasOwn.call(obj, key);
};

var getSymbols = function getSymbols(object) {
  if (object == null) {
    return [];
  }
  object = Object(object);
  return nativeGetSymbols(object).filter((symbol) => propertyIsEnumerable.call(object, symbol))
}

var handleCopy = function handleCopy(target, options, name, deep) {
  var src, copy, clone;
  src = target[name];
  copy = options[name];

  // Prevent never-ending loop
  if (target === copy) return;

  // Recurse if we're merging plain objects
  if (deep && copy && isPlainObject(copy)) {
    clone = src && isPlainObject(src) ? src : {};
    // Never move original objects, clone them
    target[name] = extend(deep, clone, copy);

  // Don't bring in undefined values
  } else if (typeof copy !== 'undefined') {
    target[name] = copy;
  }
};

var extend = function extend() {
  var options, name;
  var target = arguments[0];
  var i = 1;
  var length = arguments.length;
  var deep = false;

  // Handle a deep copy situation
  if (typeof target === 'boolean') {
    deep = target;
    target = arguments[1] || {};
    // skip the boolean and the target
    i = 2;
  } else if ((typeof target !== 'object' && typeof target !== 'function') || target == null) {
    target = {};
  }

  for (; i < length; ++i) {
    options = arguments[i];
    // Only deal with non-null/undefined values
    if (options == null) continue;

    // Extend the base object
    for (name in options) {
      if (name === '__proto__') continue;
      handleCopy(target, options, name, deep);
    }
    
    // handle symbol
    var symbols = getSymbols(options);
    for (const symbol of symbols) {
      handleCopy(target, options, symbol, deep);
    }
  }

  // Return the modified object
  return target;
};

module.exports = extend;

然后再次访问就发现可以拿到配置项了:

console.log((ctx.app.config as any)[SYMBOL_PERMISSION]); // { permission: { msg: 'local', author: 'simpul' } }

真 Symbol 没法被 copy 的,除非你拿到了 Symbol 的引用才能获取属性值。

commented

真 Symbol 没法被 copy 的,除非你拿到了 Symbol 的引用才能获取属性值。

@fengmk2 可能我表述有问题,就是期望拿引用去获取属性值,现状是不支持的。

@simpul 你这些配置值给谁来消费的,只是你自己么?谁可能会覆盖掉你的?

commented

@simpul 你这些配置值给谁来消费的,只是你自己么?谁可能会覆盖掉你的?

@atian25 这些配置值是给我自己消费的。实际场景是我写业务用的是公司框架(框架是在继承egg的基础上,里面集成了一些公司业务相关的插件,这些插件里面也有对应的配置项),然后在我在写自己业务的配置的时候,考虑到可能会覆盖掉内置插件的同名配置,所以才会想到用symbol

如果你是框架维护者,属于集成方,那你集成的插件应该都心里有数,按理说不会跟他们的配置 key 冲突吧?譬如你自定义一个特殊的 xx_framrwork_config 的 key,相关配置都在底下,是不是就能解决这个问题了?

另外,在 app/extend/application.js 里面是可以用 symbol 做属性 key 然后合并的, config 那边之前没实现,我持中立。
/cc @fengmk2 @popomore 看看有什么建议不?

commented

我这边是不是框架的维护者,只是用框架来写业务。从业务开发者角度来说,我可能不会过多地去关心这个框架集成了哪些插件以及对应的配置内容,所以会有担心配置key冲突的思考。
“ctx.app上挂载的属性是可以用symbol做属性key做合并的”——当时我也考虑既然app上的属性可以以symbol作为key,那么配置应该也可以支持一下。当然具体还是看大佬们的建议(抱拳

喔,我看错了,是你自己的业务。

其实你用 Symbol 和用一个比较长的 key 的意义差不多的,譬如 {app_name}_xx 这样,应该不会冲突,跟你预期的用法差不多。

另一种方式是可以考虑这样: app.xxConfig 搞一个特殊的,然后提供个 BaseController 和 BaseService 啥的,把 this.config 指向它。

commented

了解,我还是采用长key的方法吧,第二种方式看起来不太优雅。
感谢天🐷大大的解答

嗯,从你的代码来看,用法是 import { SYMBOL_PERMISSION } from '../utils/constants';

那这个 SYMBOL_PERMISSION 的取值,到底是一个 Symbol('xxx') 还是 特定前缀的字符串,其实就只在 constants 这个文件里面了,对外面的使用是没差别的。

commented

emm,还是有点区别的。如果是字符串的话还是会有因重名而被误覆盖的可能性,只是加特定前缀会大大降低这个可能性。而用symbol的话除非拿到引用才能访问到配置里面的内容。我是这样理解的。

反正你是在 constants 文件里面定义的,里面到底是 Symbol('xx') 还是一个特定前缀 + 随机数,区别不大。
没必要追求绝对的误覆盖,譬如插件的 config 名总不可能叫 xx_biz_{app_name}.xxx 吧,是的话可以去找下插件作者打一顿。