ejs源码

与artTemplate大不同的是

artTemplate使用split分割模板字符串;ejs通过基于正则的match方法分割模板字符串。

artTemplate通过传入辅助函数的引用拼接编译函数字符串;ejs有赖于js原生语句实现each、if语法,普通内容直接push到__output中,构建编译函数字符串。

 

1.ejs.js

/*
 * EJS Embedded JavaScript templates
 * Copyright 2112 Matthew Eernisse (mde@fleegix.org)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
*/

'use strict';

/**
 * @file Embedded JavaScript templating engine.
 * @author Matthew Eernisse <mde@fleegix.org>
 * @author Tiancheng "Timothy" Gu <timothygu99@gmail.com>
 * @project EJS
 * @license {@link http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0}
 */

/**
 * EJS internal functions.
 *
 * Technically this "module" lies in the same file as {@link module:ejs}, for
 * the sake of organization all the private functions re grouped into this
 * module.
 *
 * @module ejs-internal
 * @private
 */

/**
 * Embedded JavaScript templating engine.
 *
 * @module ejs
 * @public
 */

var fs = require('fs');
var path = require('path');
var utils = require('./utils');

var scopeOptionWarned = false;
var _VERSION_STRING = require('../package.json').version;
var _DEFAULT_DELIMITER = '%';
var _DEFAULT_LOCALS_NAME = 'locals';
var _NAME = 'ejs';
var _REGEX_STRING = '(<%%|%%>|<%=|<%-|<%_|<%#|<%|%>|-%>|_%>)';// 开启闭合标签的默认匹配正则
var _OPTS = ['delimiter', 'scope', 'context', 'debug', 'compileDebug',
  'client', '_with', 'rmWhitespace', 'strict', 'filename'];
var _BOM = /^\uFEFF/;

// 缓存编译函数
exports.cache = utils.cache;

exports.localsName = _DEFAULT_LOCALS_NAME;

// 获取子模板的绝对路径
// 当前模板设置filename文件或目录路径,name传参为相对filename目录的文件路径
exports.resolveInclude = function(name, filename, isDir) {
    var dirname = path.dirname;
    var extname = path.extname;
    var resolve = path.resolve;
    var includePath = resolve(isDir ? filename : dirname(filename), name);
    var ext = extname(name);
    if (!ext) {
        includePath += '.ejs';
    }
    return includePath;
};

// 获取子模板的绝对路径
function getIncludePath(path, options){
    var includePath;
    if (path.charAt(0) == '/') {
        includePath = exports.resolveInclude(path.replace(/^\/*/,''), options.root || '/', true);
    } else {
      if (!options.filename) {
          throw new Error('`include` use relative path requires the \'filename\' option.');
      }
      includePath = exports.resolveInclude(path, options.filename);
    }
    return includePath;
}

// 缓存中有编译函数,获取编译函数;若无,通过文件名读取文件后,获得编译函数,并存入缓存
function handleCache(options, template) {
    var func;
    var filename = options.filename;
    var hasTemplate = arguments.length > 1;

    if (options.cache) {
        if (!filename) {
            throw new Error('cache option requires a filename');
        }
        func = exports.cache.get(filename);
        if (func) {
            return func;
        }
        if (!hasTemplate) {
            template = fs.readFileSync(filename).toString().replace(_BOM, '');
        }
    } else if (!hasTemplate) {
        if (!filename) {
          throw new Error('Internal EJS error: no file name or template '
                        + 'provided');
        }
        template = fs.readFileSync(filename).toString().replace(_BOM, '');
    }
    func = exports.compile(template, options);
    if (options.cache) {
        exports.cache.set(filename, func);
    }
    return func;
}

// 通过文件路径获取编译函数
function includeFile(path, options) {
    var opts = utils.shallowCopy({}, options);
    opts.filename = getIncludePath(path, opts);
    // handleCache 缓存中有编译函数,获取编译函数;若无,通过文件名读取文件后,获得编译函数,并存入缓存
    return handleCache(opts);
}

// 获取嵌套模板的编译函数核心字符串、模板字符串内容
function includeSource(path, options) {
    var opts = utils.shallowCopy({}, options);
    var includePath;
    var template;
    includePath = getIncludePath(path,opts);
    template = fs.readFileSync(includePath).toString().replace(_BOM, '');
    opts.filename = includePath;
    var templ = new Template(template, opts);
    templ.generateSource();
    return {
        source: templ.source,
        filename: includePath,
        template: template
    };
}

/**
 * 报错
 *
 * @param {Error}  err      Error object
 * @param {String} str      编译核心核心内容,字符串形式
 * @param {String} filename 文件名
 * @param {String} lineno   错误的行号
 */
function rethrow(err, str, flnm, lineno){
    var lines = str.split('\n');
    var start = Math.max(lineno - 3, 0);
    var end = Math.min(lines.length, lineno + 3);
    var filename = utils.escapeXML(flnm);

    var context = lines.slice(start, end).map(function (line, i){
        var curr = i + start + 1;
        return (curr == lineno ? ' >> ' : '    ')
          + curr
          + '| '
          + line;
    }).join('\n');

    err.path = filename;
    err.message = (filename || 'ejs') + ':'
        + lineno + '\n'
        + context + '\n\n'
        + err.message;

    throw err;
}

// 去除";"及其空格
function stripSemi(str) {
    return str.replace(/;(\s*$)/, '$1');
}

// 编译模板字符串template,返回编译函数,编译配置为opts
exports.compile = function compile(template, opts) {
    var templ;

    // 旧版本的scope替换为context
    if (opts && opts.scope) {
        if (!scopeOptionWarned){
            console.warn('`scope` option is deprecated and will be removed in EJS 3');
            scopeOptionWarned = true;
        }
        if (!opts.context) {
            opts.context = opts.scope;
        }
        delete opts.scope;
    }
    templ = new Template(template, opts);
    return templ.compile();
};

// 通过模板路径和模板数据、编译配置获取编译结果
exports.render = function (template, d, o) {
    var data = d || {};
    var opts = o || {};

    // No options object -- if there are optiony names
    // in the data, copy them to options
    if (arguments.length == 2) {
      utils.shallowCopyFromList(opts, data, _OPTS);
    }

    // handleCache 缓存中有编译函数,获取编译函数;若无,通过文件名读取文件后,获得编译函数,并存入缓存
    return handleCache(opts, template)(data);
};

/**
 * 通过模板路径和模板数据获取编译结果,并执行回调
 * @param {String}            filename  模板路径
 * @param {Object}            [data={}] 模板数据
 * @param {Options}           [opts={}] 编译配置
 * @param {RenderFileCallback} cb 回调函数
 */
exports.renderFile = function () {
    var args = Array.prototype.slice.call(arguments);
    var filename = args.shift();
    var cb = args.pop();
    var data = args.shift() || {};
    var opts = args.pop() || {};
    var optsKeys =_OPTS.slice();
    var result;

    opts = utils.shallowCopy({}, opts);

    // `renderFile`方法添加"cache"为可配置内容
    optsKeys.push('cache');

    if (arguments.length == 3) {
        // Express 4
        if (data.settings && data.settings['view options']) {
            utils.shallowCopyFromList(opts, data.settings['view options'], optsKeys);
        }
        // Express 3 and lower
        else {
            utils.shallowCopyFromList(opts, data, optsKeys);
        }
    }
    opts.filename = filename;

    try {
        // handleCache 缓存中有编译函数,获取编译函数;若无,通过文件名读取文件后,获得编译函数,并存入缓存
        result = handleCache(opts)(data);
    }catch(err) {
        return cb(err);
    }
    return cb(null, result);
};

// 清空模板编译函数
exports.clearCache = function () {
    exports.cache.reset();
};

function Template(text, opts) {
    opts = opts || {};
    var options = {};
    this.templateText = text;// 模板字符串
    this.mode = null;// 模板字符串各line处理模式
    this.truncate = false;
    this.currentLine = 1;// 错误提示时输出行号
    this.source = '';// 编译函数体核心内容,字符串形式
    this.dependencies = [];// 记录当前模板包含的模板
    options.client = opts.client || false;// 客户端或服务端不同模块将escape、rethow函数注入到编译函数体内
    options.escapeFunction = opts.escape || utils.escapeXML;// html转义
    options.compileDebug = opts.compileDebug !== false;// 是否开启调试模板编译模板
    options.debug = !!opts.debug;// 是否以字符串形式输出编译函数
    options.filename = opts.filename;// 模板名,作为模板的标识符,缓存编译函数的键值
    options.delimiter = opts.delimiter || exports.delimiter || _DEFAULT_DELIMITER;// 开启或者闭合尖括号所用的字符
    options.strict = opts.strict || false;// 编译函数采用严格模式 "use strict",严格模式下不能使用with语句
    options.context = opts.context;// 编译函数上下文
    options.cache = opts.cache || false;// 是否开启编译函数缓存
    options.rmWhitespace = opts.rmWhitespace;// 是否清除起始的空格
    options.root = opts.root;
    options.localsName = opts.localsName || exports.localsName || _DEFAULT_LOCALS_NAME;

    if (options.strict) { // var obj={a:1};with(obj){console.log(a)} with语句快速引用对象的属性与方法
        options._with = false;
    }else {
        options._with = typeof opts._with != 'undefined' ? opts._with : true;
    }

    this.opts = options;

    this.regex = this.createRegex();// 开启闭合标签的匹配正则
}

Template.modes = {
    EVAL: 'eval',// "<%  %>"、"<%_  %>"内语句原样拼接到编译函数体内,可承接js语句,后跟模板字符串通过push方法添加到__output
    ESCAPED: 'escaped',// "<%=  %>"内语句通过html转义后,可承接变量,由push方法添加到__output
    RAW: 'raw',// "<%-  %>"不转义,可承接变量,由push方法添加到__output
    COMMENT: 'comment',// "<%#  %>"注释,不添加编译函数体内
    LITERAL: 'literal'// "<%%  %%>"通过push方法将"<%  %>"由push方法添加到__output
};

Template.prototype = {
    // 根据delimiter配置,重新生成开启闭合标签的匹配正则
    createRegex: function () {
        var str = _REGEX_STRING;
        var delim = utils.escapeRegExpChars(this.opts.delimiter);
        str = str.replace(/%/g, delim);
        return new RegExp(str);
    },

    // 获取编译函数,客户端或者服务器端不同模块间将escape、include函数以字符串形式写入
    // 同artTemplate不同的是,artTemplate主要面向客户端,$utils、$helpers都通过引用的方式构建传入
    //    ejs面向服务端,each语法借用js的原生语句,escape、include函数以参数或字符串形式传入
    //    两者都同样以拼接字符串的方式生成编译函数体,再通过new Function构建函数
    compile: function () {
        var src;
        var fn;
        var opts = this.opts;
        var prepended = '';
        var appended = '';
        var escape = opts.escapeFunction;

        if (!this.source) {
            // 将模板字符串通过match方法拆分为开闭标签前后各段,按条件拼接编译函数体,或者作为js语句,或者作为普通字符串
            this.generateSource();
            prepended += '  var __output = [], __append = __output.push.bind(__output);' + '\n';
            if (opts._with !== false) {
                prepended +=  '  with (' + opts.localsName + ' || {}) {' + '\n';
                appended += '  }' + '\n';
            }
            appended += '  return __output.join("");' + '\n';
            this.source = prepended + this.source + appended;
        }

        if (opts.compileDebug) {
            src = 'var __line = 1' + '\n'
                + '  , __lines = ' + JSON.stringify(this.templateText) + '\n'
                + '  , __filename = ' + (opts.filename ?
                      JSON.stringify(opts.filename) : 'undefined') + ';' + '\n'
                + 'try {' + '\n'
                + this.source
                + '} catch (e) {' + '\n'
                + '  rethrow(e, __lines, __filename, __line);' + '\n'
                + '}' + '\n';
        }else {
            src = this.source;
        }

        if (opts.debug) {
            console.log(src);
        }

        // 客户端将escape、rethow函数注入到编译函数体内
        if (opts.client) {
            src = 'escape = escape || ' + escape.toString() + ';' + '\n' + src;
            if (opts.compileDebug) {
                src = 'rethrow = rethrow || ' + rethrow.toString() + ';' + '\n' + src;
            }
        }

        if (opts.strict) {
            src = '"use strict";\n' + src;
        }

        try {
            fn = new Function(opts.localsName + ', escape, include, rethrow', src);
        }catch(e) {
            if (e instanceof SyntaxError) {
                if (opts.filename) {
                    e.message += ' in ' + opts.filename;
                }
                e.message += ' while compiling ejs\n\n';
                e.message += 'If the above error is not helpful, you may want to try EJS-Lint:\n';
                e.message += 'https://github.com/RyanZim/EJS-Lint';
            }
            throw e;
        }

        if (opts.client) {
            fn.dependencies = this.dependencies;
            return fn;
        }

        var returnedFn = function (data) {
            var include = function (path, includeData) {
                var d = utils.shallowCopy({}, data);
                if (includeData) {
                    d = utils.shallowCopy(d, includeData);
                }
                // includeFile 通过文件路径获取编译函数
                return includeFile(path, opts)(d);
            };
            return fn.apply(opts.context, [data || {}, escape, include, rethrow]);
        };
        returnedFn.dependencies = this.dependencies;
        return returnedFn;
    },

    // 将模板字符串通过match方法拆分为开闭标签前后各段,按条件拼接编译函数体,或者作为js语句,或者作为普通字符串
    generateSource: function () {
        var opts = this.opts;

        if (opts.rmWhitespace) {
            this.templateText = this.templateText.replace(/\r/g, '').replace(/^\s+|\s+$/gm, '');
        }

        // 清除<%_前、_%>后的制表符\t及空格
        this.templateText = this.templateText.replace(/[ \t]*<%_/gm, '<%_').replace(/_%>[ \t]*/gm, '_%>');

        var self = this;

        // 模板字符串清除开闭标签,以数组形式记录开闭标签及其前后字符串,并返回
        var matches = this.parseTemplateText();
        var d = this.opts.delimiter;

        if (matches && matches.length) {
            matches.forEach(function (line, index) {
                var opening;
                var closing;
                var include;
                var includeOpts;
                var includeObj;
                var includeSrc;
                
                // 开始标签存在时,校验闭合标签是否存在
                if ( line.indexOf('<' + d) === 0 && line.indexOf('<' + d + d) !== 0 ) {
                    closing = matches[index + 2];// 闭合标签
                    if (!(closing == d + '>' || closing == '-' + d + '>' || closing == '_' + d + '>')) {
                        throw new Error('Could not find matching close tag for "' + line + '".');
                    }
                }

                // 拼接include子模板字符串
                if ((include = line.match(/^\s*include\s+(\S+)/))) {
                    opening = matches[index - 1];
                    if (opening && (opening == '<' + d || opening == '<' + d + '-' || opening == '<' + d + '_')) {
                        // utils.shallowCopy 对象浅拷贝
                        includeOpts = utils.shallowCopy({}, self.opts);
                        // 获取嵌套模板的编译函数核心字符串、模板字符串内容
                        includeObj = includeSource(include[1], includeOpts);
                        if (self.opts.compileDebug) {
                            includeSrc =
                                '    ; (function(){' + '\n'
                                + '      var __line = 1' + '\n'
                                + '      , __lines = ' + JSON.stringify(includeObj.template) + '\n'
                                + '      , __filename = ' + JSON.stringify(includeObj.filename) + ';' + '\n'
                                + '      try {' + '\n'
                                + includeObj.source
                                + '      } catch (e) {' + '\n'
                                + '        rethrow(e, __lines, __filename, __line);' + '\n'
                                + '      }' + '\n'
                                + '    ; }).call(this)' + '\n';
                        }else{
                            includeSrc = '    ; (function(){' + '\n' + includeObj.source +
                                '    ; }).call(this)' + '\n';
                        }
                        self.source += includeSrc;
                        self.dependencies.push(exports.resolveInclude(include[1],
                            includeOpts.filename));
                        return;
                    }
                }

                // line为开闭标签,及其内外的各项字符串,根据开闭标签拼接编译函数字符串
                self.scanLine(line);
            });
        }
    },

    // 模板字符串清除开闭标签,以数组形式记录开闭标签及其前后字符串,并返回
    parseTemplateText: function () {
        var str = this.templateText;
        var pat = this.regex;// 开启闭合标签的匹配正则
        var result = pat.exec(str);// 获取str所有开启闭合标签,index属性为其在str中的序号
        var arr = [];
        var firstPos;

        while (result) {
            firstPos = result.index;

            if (firstPos !== 0) {
              arr.push(str.substring(0, firstPos));
              str = str.slice(firstPos);
            }

            arr.push(result[0]);
            str = str.slice(result[0].length);
            result = pat.exec(str);
        }

        if (str) {
            arr.push(str);
        }

        return arr;
    },

    // line为开闭标签,及其内外的各项字符串,根据开闭标签拼接编译函数字符串
    scanLine: function (line) {
        var self = this;
        var d = this.opts.delimiter;// 开启或者闭合尖括号所用的字符
        var newLineCount = 0;

        // 普通字符串处理
        function _addOutput() {
            if (self.truncate) {
                line = line.replace(/^(?:\r\n|\r|\n)/, '');
                self.truncate = false;
            } else if (self.opts.rmWhitespace) {
                line = line.replace(/^\n/, '');
            }

            if (!line) {
                return;
            }

            // 转义
            line = line.replace(/\\/g, '\\\\');
            line = line.replace(/\n/g, '\\n');
            line = line.replace(/\r/g, '\\r');
            line = line.replace(/"/g, '\\"');

            self.source += '    ; __append("' + line + '")' + '\n';
        }

        newLineCount = (line.split('\n').length - 1);

        switch (line) {
            // "<%  %>"、"<%_  %>"内语句原样拼接到编译函数体内,后跟模板字符串通过push方法添加到__output
            case '<' + d:
            case '<' + d + '_':
                this.mode = Template.modes.EVAL;
                break;
            // "<%=  %>"内语句通过html转义后,可承接变量,由push方法添加到__output
            case '<' + d + '=':
                this.mode = Template.modes.ESCAPED;
                break;
            // "<%-  %>"不转义,可承接变量
            case '<' + d + '-':
                this.mode = Template.modes.RAW;
                break;
            // "<%#  %>"评论,不添加编译函数体内
            case '<' + d + '#':
                this.mode = Template.modes.COMMENT;
                break;
            // "<%%  %%>"通过push方法将"<%  %>"由push方法添加到__output
            case '<' + d + d:
                this.mode = Template.modes.LITERAL;
                this.source += '    ; __append("' + line.replace('<' + d + d, '<' + d) + '")' + '\n';
                break;
            case d + d + '>':
                this.mode = Template.modes.LITERAL;
                this.source += '    ; __append("' + line.replace(d + d + '>', d + '>') + '")' + '\n';
                break;
            case d + '>':
            case '-' + d + '>':
            case '_' + d + '>':
                if (this.mode == Template.modes.LITERAL) {
                    _addOutput();
                }

                this.mode = null;
                this.truncate = line.indexOf('-') === 0 || line.indexOf('_') === 0;
                break;
            default:
                // 开闭标签包裹的内容
                if (this.mode) {
                    // 单行注释后没有"\n",添加"\n"
                    switch (this.mode) {
                        case Template.modes.EVAL:
                        case Template.modes.ESCAPED:
                        case Template.modes.RAW:
                            if (line.lastIndexOf('//') > line.lastIndexOf('\n')) {
                                line += '\n';
                            }
                    }

                    switch (this.mode) {
                        case Template.modes.EVAL:
                            this.source += '    ; ' + line + '\n';
                            break;
                        case Template.modes.ESCAPED:
                            this.source += '    ; __append(escape(' + stripSemi(line) + '))' + '\n';
                            break;
                        case Template.modes.RAW:
                            this.source += '    ; __append(' + stripSemi(line) + ')' + '\n';
                            break;
                        case Template.modes.COMMENT:
                            break;
                        case Template.modes.LITERAL:
                            _addOutput();
                            break;
                    }
                // 开闭标签之外的内容
                } else {
                    _addOutput();
                }
        }

        // 调试模式下,添加编译的所在行数,输出错误提示时所用
        if (self.opts.compileDebug && newLineCount) {
            this.currentLine += newLineCount;
            this.source += '    ; __line = ' + this.currentLine + '\n';
        }
    }
};

// html转义
exports.escapeXML = utils.escapeXML;

// 通过模板路径和模板数据获取编译结果,并执行回调
exports.__express = exports.renderFile;

// node环境下,添加require方法加载扩展名为ejs的模块,值为编译函数
if (require.extensions) {
    require.extensions['.ejs'] = function (module, flnm) {
        var filename = flnm || /* istanbul ignore next */ module.filename;
        var options = {
            filename: filename,
            client: true// 同ejs.js不在同一个模块,escape、include以字符串形式赋给编译函数体
        };
        var template = fs.readFileSync(filename).toString();
        var fn = exports.compile(template, options);
        module._compile('module.exports = ' + fn.toString() + ';', filename);
    };
}

exports.VERSION = _VERSION_STRING;

exports.name = _NAME;

if (typeof window != 'undefined') {
    window.ejs = exports;
}

 

2.utils.js

/*
 * EJS Embedded JavaScript templates
 * Copyright 2112 Matthew Eernisse (mde@fleegix.org)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
*/

/**
 * Private utility functions
 * @module utils
 * @private
 */

'use strict';

// 用户设置开启或者闭合尖括号所用的字符delimiter时,以下字符串有效
var regExpChars = /[|\\{}()[\]^$+*?.]/g;

// 用户设置开启或者闭合尖括号所用的字符delimiter时,仅|\{}()[]^$+*?.有效
exports.escapeRegExpChars = function (string) {
    if (!string) {
        return '';
    }
    return String(string).replace(regExpChars, '\\$&');// 输出匹配部分
};

var _ENCODE_HTML_RULES = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&#34;',
    "'": '&#39;'
};
var _MATCH_HTML = /[&<>\'"]/g;

function encode_char(c) {
    return _ENCODE_HTML_RULES[c] || c;
}

var escapeFuncStr =
  'var _ENCODE_HTML_RULES = {\n'
+ '      "&": "&amp;"\n'
+ '    , "<": "&lt;"\n'
+ '    , ">": "&gt;"\n'
+ '    , \'"\': "&#34;"\n'
+ '    , "\'": "&#39;"\n'
+ '    }\n'
+ '  , _MATCH_HTML = /[&<>\'"]/g;\n'
+ 'function encode_char(c) {\n'
+ '  return _ENCODE_HTML_RULES[c] || c;\n'
+ '};\n';

// html转义
exports.escapeXML = function (markup) {
    return markup == undefined ? '' : String(markup).replace(_MATCH_HTML, encode_char);
};

// 函数转化成字符串时,同时将参数变量全部转化为字符串输入
exports.escapeXML.toString = function () {
    return Function.prototype.toString.call(this) + ';\n' + escapeFuncStr;
};

// 将from的属性或方法浅拷贝给to
exports.shallowCopy = function (to, from) {
    from = from || {};
    for (var p in from) {
        to[p] = from[p];
    }
    return to;
};

// list以数组形式约定从from浅拷贝给to的属性名或方法名
exports.shallowCopyFromList = function (to, from, list) {
    list.forEach(function (p) {
        if (typeof from[p] != 'undefined') {
            to[p] = from[p];
        }
    });
    return to;
};

// 缓存编译函数
exports.cache = {
    _data: {},
    set: function (key, val) {
        this._data[key] = val;
    },
    get: function (key) {
        return this._data[key];
    },
    reset: function () {
        this._data = {};
    }
};

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值