简体   繁体   English

在不影响日志行的情况下扩展 console.log

[英]Extending console.log without affecting log line

I would like to extend the 'console.log' function to add additional information to its output - but I dont want to affect the script name/line number information generated by the browser in the console window.我想扩展 'console.log' 函数以向其输出添加附加信息 - 但我不想影响控制台窗口中浏览器生成的脚本名称/行号信息。 See how if I create my own implementation, I get useless trace information, should I want to locate that region of code... (they all link to the log implementation, not the actual script that caused the log message)看看如果我创建自己的实现,我得到了无用的跟踪信息,我是否应该找到该代码区域...(它们都链接到日志实现,而不是导致日志消息的实际脚本)

在此处输入图片说明

Basically, my application is a very pluggable infrastructure, were any log output may occur within any number of frames.基本上,我的应用程序是一个非常可插拔的基础设施,任何日志输出都可能发生在任意数量的帧内。 As such, I want every log message to include a special unique identifier at the beginning of the log message.因此,我希望每条日志消息在日志消息的开头都包含一个特殊的唯一标识符。

I have tried replacing the console.log method with my own, but chrome complains with Uncaught TypeError: Illegal invocation我尝试用我自己的方法替换 console.log 方法,但 chrome 抱怨Uncaught TypeError: Illegal invocation

this is how I override it这就是我覆盖它的方式

var orig = console.log;
console.log = function( message )
{
    orig( (window == top ? '[root]' : '[' + window.name + ']') + ': ' + message );
}

Any ideas?有任何想法吗?

[EDIT] Note: After fixing the 'illegal invocation' problem, it seems the filename/linenumber is still 'polluted' by the override... [编辑] 注意:修复“非法调用”问题后,似乎文件名/行号仍然被覆盖“污染”...

[EDIT] It looks like the general answer is - NO - despite some confusing goose chases, the desired functionality is NOT achievable in the current versions of browsers. [编辑] 看起来一般的答案是 - 不 - 尽管有一些令人困惑的鹅追逐,但在当前版本的浏览器中无法实现所需的功能。

Yes, it is possible to add information without messing up the originating line numbers of the log invocation.是的,可以在不弄乱日志调用的原始行号的情况下添加信息。 Some of the other answers here came close, but the trick is to have your custom logging method return the modified logger.这里的其他一些答案很接近,但诀窍是让您的自定义日志记录方法返回修改后的记录器。 Below is a simple example that was only moderately tested that uses the context variant.下面是一个简单的示例,该示例仅经过适度测试,使用上下文变体。

log = function() {
    var context = "My Descriptive Logger Prefix:";
    return Function.prototype.bind.call(console.log, console, context);
}();

This can be used with:这可以用于:

log("A log message..."); 

Here is a jsfiddle: http://jsfiddle.net/qprro98v/这是一个 jsfiddle: http : //jsfiddle.net/qprro98v/

One could get easily get creative and pass the context variable in, and remove the auto-executing parens from the function definition.可以轻松获得创意并传入上下文变量,并从函数定义中删除自动执行括号。 ie log("DEBUG:")("A debug message"), log("INFO:")("Here is some info"), etc.即 log("DEBUG:")("A debug message"), log("INFO:")("Here is some info") 等。

The only really import part about the function (in regards to line numbers) is that it returns the logger.该函数唯一真正重要的部分(关于行号)是它返回记录器。

If your use case can deal with a few restrictions, there is a way that this can be made to work.如果您的用例可以处理一些限制,则一种方法可以使其发挥作用。 The restrictions are:限制是:

  • The extra log content has to be calculated at bind time;额外的日志内容必须在绑定时计算; it cannot be time sensitive or depend on the incoming log message in any way.它不能对时间敏感或以任何方式依赖传入的日志消息。

  • The extra log content can only be place at the beginning of the log message.额外的日志内容只能放在日志消息的开头。

With these restrictions, the following may work for you:有了这些限制,以下内容可能对您有用:

var context = "ALIASED LOG:"
var logalias;

if (console.log.bind === 'undefined') { // IE < 10
    logalias = Function.prototype.bind.call(console.log, console, context);
}
else {
    logalias = console.log.bind(console, context);
}

logalias('Hello, world!');

http://jsfiddle.net/Wk2mf/ http://jsfiddle.net/Wk2mf/

It is actually possible in chrome at least.至少在 chrome 中实际上是可能的。 Here is the most relevant.这是最相关的。 This may vary depending on setup, and how i got the splits was to just log the whole stack, and find the information I needed.这可能因设置而异,我如何获得拆分只是记录整个堆栈,并找到我需要的信息。

        var stack = new Error().stack;
        var file = stack.split("\n")[2].split("/")[4].split("?")[0]
        var line = stack.split("\n")[2].split(":")[5];

Here is the whole thing, preserving the native object logging.这是整个事情,保留本机对象日志记录。

var orig = console.log
console.log = function(input) {
    var isChrome = navigator.userAgent.indexOf("Chrome") !== -1;
    if(isChrome){
        var stack = new Error().stack;
        var file = stack.split("\n")[2].split("/")[4].split("?")[0]
        var line = stack.split("\n")[2].split(":")[5];
        var append = file + ":" + line;
    }
    orig.apply(console, [input, append])
}

An acceptable solution can be to make your own log-function that returns a console.log function bound with the log arguments.一个可接受的解决方案是创建自己的日志函数,该函数返回与日志参数绑定的console.log函数。

 log = function() { // Put your extension code here var args = Array.prototype.slice.call(arguments); args.unshift(console); return Function.prototype.bind.apply(console.log, args); } // Note the extra () to call the original console.log log("Foo", {bar: 1})();

This way the console.log call will be made from the correct line, and will be displayed nicely in the console, allowing you to click on it and everything.这样, console.log调用将从正确的行进行,并将在控制台中很好地显示,允许您单击它和所有内容。

You need to call the console.log with the correct context ( console ):您需要使用正确的上下文 ( console ) 调用console.log

orig.call(console, message);

To complete your function allowing multiple arguments:要完成允许多个参数的函数:

var orig = console.log;
console.log = function() {
    var msgs = [],
        prefix = (window== top ? '[root]' : '[' + window.name + ']');
    while(arguments.length) {
        msgs.push(prefix + ': ' + [].shift.call(arguments));
    }
    orig.apply(console, msgs);
};

Demo: http://jsfiddle.net/je2wR/演示: http : //jsfiddle.net/je2wR/

Remember that you loose the built-in object/array browser in the console when combining objects with strings using the + sign.请记住,当使用+号将对象与字符串组合时,您会丢失控制台中的内置对象/数组浏览器。

I just answered this on a post that helped me answer the original 'alias' question:我刚刚在一篇帮助我回答原始“别名”问题的帖子中回答了这个问题:

(http://stackoverflow.com/a/12942764/401735) (http://stackoverflow.com/a/12942764/401735)

my_log_alias = console.log.bind(console)

Apparently the capacity to do this has been designed in. Tested.显然已经设计了这种能力。经过测试。 Works.作品。

thereafter my_log_alias is the same as console.log and can be called in the same way;之后my_log_alias和console.log一样,可以用同样的方式调用; Calling this from inside the function will report the line number for that function call, including the line inside of an alias or advice function where applicable.从函数内部调用 this 将报告该函数调用的行号,包括适用的别名或建议函数内部的行。

Specifically, the line number Chrome provides will tell you the file the line is in, so what you are doing may be unneccesary;具体来说,Chrome 提供的行号会告诉您该行所在的文件,因此您所做的可能是不必要的; Consider reporting this as a bug/feature request in chrome that it provide this info in console.log.考虑将此报告为 chrome 中的错误/功能请求,它在 console.log 中提供此信息。

Christopher Currie provided an excellent solution. Christopher Currie 提供了一个极好的解决方案。 I've expanded it a bit for my needs.我已经根据我的需要对其进行了扩展。 Here's the AMD module:这是 AMD 模块:

define([], function () {

    var enableDebug = true;
    var separator = ">";    

    function bind(f, thisArg, ctx) {
        if (f.bind !== 'undefined') { // IE < 10
            return Function.prototype.bind.call(f, thisArg, ctx);
        }
        else {
            return f.bind(thisArg, ctx);
        }
    }

    function newConsole(context, parentConsole) {
        var log;
        var debug;
        var warn;
        var error;

        if (!parentConsole) {
            parentConsole = console;
        }

        context = context + separator;


        if (enableDebug) {
            debug = bind(console.log, console, context + "DEBUG" + separator);
        } else {
            debug = function () {
                // suppress all debug messages
            };
        }

        log = bind(console.log, console, context);

        warn = bind(console.warn, console, context);

        error = bind(console.error, console, context);

        return {
            debug: debug,
            info: log,
            log: log,
            warn: warn,
            error: error,
            /* access console context information */
            context: context,
            /* create a new console with nested context */
            nest: function (subContext) {
                return newConsole(context + subContext, this);
            },
            parent: parentConsole
        };
    }

    return newConsole("");
});

By default this will output > {message} .默认情况下,这将输出> {message} You can also add nested context to you logging, eg console.nest("my").log("test") will output >my> test .您还可以向日志添加嵌套上下文,例如console.nest("my").log("test")将输出>my> test

I've also added a debug function that will indent messages with >DEBUG>我还添加了一个debug功能,该功能将使用>DEBUG>缩进消息

Hope somebody will find it useful.希望有人会觉得它有用。

I have looked into this several times and always found it was not possible.我已经多次研究过这个问题,但总是发现这是不可能的。

My workaround if you are interested is to assign console to another variable and then wrap all my log messages in a function which lets me modify/style/whatever on the message.如果您有兴趣,我的解决方法是将控制台分配给另一个变量,然后将我所有的日志消息包装在一个函数中,该函数允许我修改/样式/任何消息。

It looks nice with CoffeeScript, not sure its practical with plain JS. CoffeeScript 看起来不错,但不确定它是否适用于普通的 JS。

I just get into the habit of prefixing everything with x .我只是养成了用x前缀所有内容的习惯。

logger.debug x 'Foo'

log x 'Bar'

log x('FooBar %o'), obj

Unfrotuantly it's currenlty not possible, In the future we might be able to do it with the Proxy object in ECMAScript 6. Unfrotuantly 目前是不可能的,将来我们可能可以使用 ECMAScript 6 中的Proxy对象来做到这一点。

My use case was to auto-prefix console messages with helpful information like the arguments passed and executing method.我的用例是使用有用的信息(例如传递的参数和执行方法)自动为控制台消息添加前缀。 at the moment the closest I got is using Function.prototype.apply .目前我得到的最接近的是使用Function.prototype.apply

A simple approach is to just write your debug statements as such:一种简单的方法是编写调试语句,如下所示:

console.info('=== LazyLoad.css(', arguments, '): css files are skipped, gives us a clean slate to style within theme\'s CSS.');

A complicated approach is to use helper function as per below, I personally now prefer the simple approach.一种复杂的方法是使用如下所示的辅助函数,我个人现在更喜欢简单的方法。

扩展“console.debug”功能方法

/* Debug prefixing function
 * ===========================
 * 
 * A helper used to provide useful prefixing information 
 * when calling `console.log`, `console.debug`, `console.error`.
 * But the catch is that to utilize one must leverage the 
 * `.apply` function as shown in the below examples.
 *
 * ```
 * console.debug.apply(console, _fDebugPrefix(arguments)
 *    .concat('your message'));
 *
 * // or if you need to pass non strings
 * console.debug.apply(console, _fDebugPrefix(arguments)
 *    .concat('json response was:', oJson));
 *
 *
 * // if you need to use strict mode ("use strict") one can't
 * // extract the function name but following approach works very
 * // well; updating the name is just a matter of search and replace
 * var aDebugPrefix = ['fYourFunctionName('
 *                     ,Array.prototype.slice.call(arguments, 0), 
 *                     ,')'];
 * console.debug.apply(console, 
 *                     aDebugPrefix.concat(['json response was:', oJson]));
 * ```
 */
function _fDebugPrefix(oArguments) {
    try {
        return [oArguments.callee.name + '('
                ,Array.prototype.slice.call(oArguments, 0)
                , ')'];
    }
    catch(err) { // are we in "use strict" mode ?
        return ['<callee.name unsupported in "use strict">('
                ,Array.prototype.slice.call(oArguments, 0)
                , ')'];
    }
}

Not long ago Chrome introduced a feature that can solve your problem without code hacks.不久前,Chrome 推出了一项功能,无需代码破解即可解决您的问题。 It is called "blackbox" which basically allows you to mark files which should be ignored with their tools.它被称为“黑匣子”,它基本上允许您标记应使用其工具忽略的文件。

https://gist.github.com/paulirish/c307a5a585ddbcc17242 https://gist.github.com/paulirish/c307a5a585ddbcc17242

Yes, this solution is browser specific, but if you are using Chrome you do want this solution.是的,此解决方案是特定于浏览器的,但如果您使用的是 Chrome,您确实需要此解决方案。

The solutions with a huge hack around throwing an Error for each log can show the right line, but it will not be a clickable link in your console.为每个日志抛出错误的解决方案可以显示正确的行,但它不会是控制台中的可点击链接。

The solutions based on binding/aliasing only enables you to modify the printed text.基于绑定/别名的解决方案仅允许您修改打印文本。 You will not be able to forward the arguments to a third function for further processing.您将无法将参数转发给第三个函数以进行进一步处理。

Reusable class in TS/JS TS/JS 中的可重用类

// File: LogLevel.ts
enum LogLevel {
   error = 0,
   warn,
   info,
   debug,
   verbose,
 }

 export default LogLevel;
// File: Logger.js
import LogLevel from "./LogLevel";

export default class Logger {
  static id = "App";
  static level = LogLevel.info;

  constructor(id) {
    this.id = id;

    const commonPrefix = `[${Logger.id}/${this.id}]`;

    const verboseContext = `[V]${commonPrefix}`;
    if (console.log.bind === "undefined") {
      // IE < 10
      this.verbose = Function.prototype.bind.call(console.log, console, verboseContext);
    } else {
      this.verbose = console.log.bind(console, verboseContext);
    }
    if (LogLevel.verbose > Logger.level) {
      this.verbose = function() {
        return // Suppress
      };
    }

    const debugContext = `[D]${commonPrefix}`;
    if (console.debug.bind === "undefined") {
      // IE < 10
      this.debug = Function.prototype.bind.call(console.debug, console, debugContext);
    } else {
      this.debug = console.debug.bind(console, debugContext);
    }
    if (LogLevel.debug > Logger.level) {
      this.debug = function() {
        return // Suppress
      };
    }

    const infoContext = `[I]${commonPrefix}`;
    if (console.info.bind === "undefined") {
      // IE < 10
      this.info = Function.prototype.bind.call(console.info, console, infoContext);
    } else {
      this.info = console.info.bind(console, infoContext);
    }
    if (LogLevel.info > Logger.level) {
      this.info = function() {
        return // Suppress
      };
    }

    const warnContext = `[W]${commonPrefix}`;
    if (console.warn.bind === "undefined") {
      // IE < 10
      this.warn = Function.prototype.bind.call(console.warn, console, warnContext);
    } else {
      this.warn = console.warn.bind(console, warnContext);
    }
    if (LogLevel.warn > Logger.level) {
      this.warn = function() {
        return // Suppress
      };
    }

    const errorContext = `[E]${commonPrefix}`;
    if (console.error.bind === "undefined") {
      // IE < 10
      this.error = Function.prototype.bind.call(console.error, console, errorContext);
    } else {
      this.error = console.error.bind(console, errorContext);
    }
    if (LogLevel.error > Logger.level) {
      this.error = function() {
        return // Suppress
      };
    }
  }
}

Usage (React):用法(反应):

// File: src/index.tsx

// ...

Logger.id = "MCA"
const env = new Env()
if (env.env == Environment.dev) {
  Logger.level = LogLevel.verbose
  const log = new Logger("Main")
  log.info("Environment is 'Development'")
}

///...
// File: src/App/CookieConsent/index.tsx
import React, { useEffect } from "react";
import { useCookies } from "react-cookie";
import "./index.scss";

import Logger from "@lib/Logger" // @lib is just alias configured in webpack.

const cookieName = "mca-cookie-consent";

// const log = new Logger(CookieConsent.name) // IMPORTANT! Don't put log instance here. It is too early! Put inside function.

export default function CookieConsent(): JSX.Element {
  const log = new Logger(CookieConsent.name) // IMPORTANT! Have to be inside function, not in global scope (after imports)

  useEffect(() => {
    log.verbose(`Consent is accepted: ${isAccepted()}`);
  }, []);

  const [cookie, setCookie] = useCookies([cookieName]);

  function isAccepted(): boolean {
    return cookie[cookieName] != undefined;
  }

  function containerStyle(): React.CSSProperties {
    return isAccepted() ? { display: "none" } : {};
  }

  function handleClick() {
    const expires = new Date();
    expires.setFullYear(expires.getFullYear() + 1);
    log.verbose(`Accepted cookie consent. Expiration: ${expires}`)
    setCookie(cookieName, true, { path: "/", expires: expires, sameSite: "lax" });
  }

  return (
    <div className="cookieContainer" style={containerStyle()}>
      <div className="cookieContent">
        <div>
          <p className="cookieText">This website uses cookies to enhance the user experience.</p>
        </div>
        <div>
          <button onClick={handleClick} className="cookieButton">
            I understand
          </button>
        </div>
      </div>
    </div>
  );
}

Output in browser console:浏览器控制台输出:

20:47:48.190 [I][MCA/Main] Environment is 'Development' index.tsx:19
20:47:48.286 [V][MCA/CookieConsent] Consent is accepted: false index.tsx:13
20:47:52.250 [V][MCA/CookieConsent] Accepted cookie consent. Expiration: Sun Jan 30 2022 20:47:52 GMT+0100 (Central European Standard Time) index.tsx:29

Hope this helps for some of your cases...希望这对您的某些情况有所帮助...

const log = console.log;
export default function middleWare(optionalStringExtension = '') {
    console.log = (...args) => {
        log(...args, optionalStringExtension);
    }
}

Either run as middleware, top of file, or first line of function.作为中间件、文件顶部或函数的第一行运行。

I ran into this issue as well about extending console.log() so that the application can extend, control and do fancy stuff with it in addition to logging stuff to the console.我也遇到了这个关于扩展 console.log() 的问题,以便应用程序除了将内容记录到控制台之外,还可以扩展、控制和使用它做一些有趣的事情。 Losing the line number information was tantamount to failure, however.然而,丢失行号信息无异于失败​​。 After wrestling with the issue, I came up with a long-winded workaround, but at least it's still a "1-liner" to use.在解决这个问题之后,我想出了一个冗长的解决方法,但至少它仍然是一个“1-liner”可以使用。

First, define a global class to use or add some methods to your main existing "app" class:首先,定义一个全局类以使用或向现有的主要“app”类添加一些方法:

/**
 * Log message to our in-app and possibly on-screen console, return args.
 * @param {!string} aMsgLevel - one of "log", "debug", "info", "warn", or "error"
 * @param {any} aArgs - the arguments to log (not used directly, just documentation helper)
 * @returns args so it can be nested within a console.log.apply(console,app.log()) statement.
 */
MyGlobalClassWithLogMethods.prototype.debugLog = function(aMsgLevel, aArgs) {
    var s = '';
    var args = [];
    for (var i=1; i<arguments.length; i++) {
        args.push(arguments[i]);
        if (arguments[i])
            s += arguments[i].toString()+' ';
    }
    if (typeof this.mLog === 'undefined')
        this.mLog = [];
    this.mLog.push({level: aMsgLevel, msg: s});
    return args;
};

MyGlobalClassWithLogMethods.prototype.log = function() {
    var args = ['log'].concat(Array.prototype.slice.call(arguments));
    return this.debugLog.apply(this,args);
};

MyGlobalClassWithLogMethods.prototype.debug = function() {
    var args = ['debug'].concat(Array.prototype.slice.call(arguments));
    return this.debugLog.apply(this,args);
};

MyGlobalClassWithLogMethods.prototype.info = function() {
    var args = ['info'].concat(Array.prototype.slice.call(arguments));
    return this.debugLog.apply(this,args);
};

MyGlobalClassWithLogMethods.prototype.warn = function() {
    var args = ['warn'].concat(Array.prototype.slice.call(arguments));
    return this.debugLog.apply(this,args);
};

MyGlobalClassWithLogMethods.prototype.error = function() {
    var args = ['error'].concat(Array.prototype.slice.call(arguments));
    return this.debugLog.apply(this,args);
};

//not necessary, but it is used in my example code, so defining it
MyGlobalClassWithLogMethods.prototype.toString = function() {
    return "app: " + JSON.stringify(this);
};

Next, we put those methods to use like so:接下来,我们像这样使用这些方法:

//JS line done as early as possible so rest of app can use logging mechanism
window.app = new MyGlobalClassWithLogMethods();

//only way to get "line info" reliably as well as log the msg for actual page display;
//  ugly, but works. Any number of params accepted, and any kind of var will get
//  converted to str using .toString() method.
console.log.apply(console,app.log('the log msg'));
console.debug.apply(console,app.debug('the log msg','(debug)', app));
console.info.apply(console,app.info('the log msg','(info)'));
console.warn.apply(console,app.warn('the log msg','(warn)'));
console.error.apply(console,app.error('the log msg','(error)'));

Now the console gets log messages with their appropriate line information as well as our app contains an array of log messages that can be put to use.现在,控制台获取带有相应行信息的日志消息,并且我们的应用程序包含一组可以使用的日志消息。 For example, to display your in-app log using HTML, JQuery and some CSS the following simplistic example can be used.例如,要使用 HTML、JQuery 和一些 CSS 显示您的应用内日志,可以使用以下简单示例。

First, the HTML:首先,HTML:

<div id="debug_area">
    <h4 class="text-center">Debug Log</h4>
    <ul id="log_list">
        <!-- console log/debug/info/warn/error ('msg') lines will go here -->
    </ul>
</div>

some CSS:一些CSS:

.log_level_log {
    color: black;
    background-color: white;
    font-size: x-small;
}
.log_level_debug {
    color: #060;
    background-color: #80FF80;
    font-size: x-small;
}
.log_level_info {
    color: #00F;
    background-color: #BEF;
    font-size: x-small;
}
.log_level_warn {
    color: #E65C00;
    background-color: #FB8;
    font-size: x-small;
}
.log_level_error {
    color: #F00;
    background-color: #FBB;
    font-size: x-small;
}

and some JQuery:和一些 JQuery:

var theLog = app.mLog || [];
if (theLog.length>0) {
    var theLogList = $('#log_list');
    theLogList.empty();
    for (var i=0; i<theLog.length; i++) {
        theLogList.prepend($('<li class="log_level_'+theLog[i].level+'"></li>').text(theLog[i].msg));
    }
}

This is a simplistic use, but once you have the mechanism in place, you can do whatever your imagination can come up with, including leaving the log lines in the code, but setting a threshold so that only warnings and errors get through.这是一个简单的用法,但是一旦你有了这个机制,你就可以做任何你能想到的事情,包括在代码中留下日志行,但设置一个阈值,以便只有警告和错误才能通过。 Hopefully this helps others with their projects.希望这可以帮助其他人完成他们的项目。

Today you have to use args with rest operator , because as the Mozilla docs says Function.arguments has been deprecated and is not accessible in arrow functions.今天,您必须将argsrest operator一起使用,因为正如 Mozilla 文档所说Function.arguments已被弃用并且无法在箭头函数中访问。 So simply you can extend it like below:所以简单地你可以像下面这样扩展它:

//#1
const myLog= (...args) =>
  console.log.bind(console, ...args);
//myLog("this is my new log")();
//#2
const myNewLog= (...args) =>{
 const prefix = "Prefixed: ";
 return console.log.bind(console, ...[prefix,...args]);
}
//myNewLog("test")()

And you can make a beautifulLog like this:你可以像这样制作一个beautifulLog

//#3
const colorizedLog = (text, color= "#40a7e3", ...args) =>
  console.log.bind(
    console,
    `%c ${text}`,
    `font-weight:bold; color:${color}`,
    ...args
  );
//colorizedLog("Title:", "#40a7e3", "This is a working example")();

This snippet apply a prefix to logs for all levels ( console.log console.debug console.info ...) :此代码段将前缀应用于所有级别的日志( console.log console.debug console.info ...):

export const makeConsole = (context: string, cons = console): Console =>
  Object.getOwnPropertyNames(cons).reduce((c, lev) => {
    if (typeof cons[lev] === "function") {
      c[lev] = Function.prototype.bind.call(cons[lev], cons, context);
    }
    return c;
  }, {});

console.debug("Hello world!")
// >> Hello world!

console = makeConsole("[logging is fun]")
// >> [logging is fun] Hello world!

Bonus, for React peeps:奖励,对于 React 窥视者:

export function useConsole(context: string): Console {
  return React.useMemo(() => makeConsole(context), [context]);
}

试试setTimeout(console.log.bind(console,'foo'));

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM