projecbt-eslint

eslint是一个代码检查工具,用来检查你的代码是否符合指定的规范

1.编码规范

在一些项目里我们常会遇到以下问题:

在这里插入图片描述

相信编码规范对于大家来说不算是陌生,如果在 9102 年再次聊起这个话题,恐怕耳朵会起茧,但编码规范从制定到落地是一个艰难的旅程,特别是对于不同成员的编码习惯,还有棘手的祖传代码。无论你是老司机还是新手不妨了解一下。

1.1 编码规范的制定

如何制定编码规范?这是一个永恒的话题,甚至出现过开发者按照自己的习惯和想法不停的去修改 eslint rules,没错,主观性非常强的开发者就会这么干,最后发现 eslint rules 成了一锅粥。

如果客观些也许会在这三个方面去思考问题:
  • 兼顾习惯:尽可能兼顾团队各成员习惯,人是有个性的,要兼顾似乎不大可能。
  • 规则从严:规则越严格越好,也绝不让他松散无约束。
  • 投个票:投票似乎是最民主的决定,但往往最具争议的 rules 会出现票数差异不大。

如果你能从以上三个方法中取得成果,那说明你是老板,这一切就会变得太简单了。但无论如何,每一个被扩展的 rule 都能找到具支持点和反驳点,在制定规范时同学们通常往往会在几个点去发表他的意见:

  • 习惯:“我一直都这样干,没问题”
  • 业界标准:“你去看某某大公司的开源代码吧”
  • 必要性:“行尾不加分号我从没见过跑不起来的JS”
  • 耗时:“按一次 tab 比按两次 tab 省时省力;行尾加分号简直浪费生命”

2.ESLint-自定义规则

在项目根目录下面,有一个.eslintrc.js文件,它是对eslint进行配置的,其中有一个属性是专门用来设置自定义代码规则的:rules 参考:[官网(https://eslint.vuejs.org/rules/)]

在配置eslint时,针对rules的配置很是头疼,主要是针对如下三个点:

参考:掘金简单版

2.1 plugins

ESLint提供的默认规则涵盖了基本规则,但JavaScript可以使用的范围非常广泛。因此,您可能希望规则不在默认规则中。在这种情况下,可以在ESLint中开发自己的独立规则。为了让第三方开发自己的规则,ESLint允许使用插件。如果你在npm中搜索eslint-plugin- *,你可以找到第三方提供的大量自定义插件。

2.2 extends

plugins的配置仅仅代表在项目中引入了哪些规则,并没有指明该规则是警告、报错、忽略,extends要做的就是引入eslint推荐的规则设置。按照eslint插件的开发规范,每个插件的入口文件都会导出一个对象,其中就有一个configs字段,该字段是个对象,他可以把该插件已有的规则分成不同的风格

2.3 rules

当eslint插件没有指定风格时,我们可以基于rules完全自定义一套风格,当某个风格的单个规则不满足需求,在rules中重新设置便可覆盖。

小技巧:

如何让prettier的规则当做eslint规则使用
eslint配置文件中增加红框中的两个内容,重启项目生效
在这里插入图片描述

// ESlint 检查配置
module.exports = {
  root: true, // 当前项目使用这个配置文件, 不会往父级目录找.eslintrc.js文件
  parser: 'vue-eslint-parser',
  parserOptions: { 
  //此项是用来指定javaScript语言类型和风格,sourceType用来指定js导入的方式,默认是script,此处设置为	module,指某块导入方式
    parser: '@typescript-eslint/parser', //使用typescript-eslint 来解析新语法
    ecmaVersion: 2020,
    sourceType: 'module',
    jsxPragma: 'React',
    ecmaFeatures: {
      jsx: true,
    },
    project: './tsconfig.*?.json',
    createDefaultProgram: false,
    extraFileExtensions: ['.vue'],
  },
  env: { //此项指定环境的全局变量,扩展配置
    browser: true,
    node: true,
    es2022: true,
  },
  plugins: ['vue', '@typescript-eslint', 'import', 'prettier'],
  extends: [
    'plugin:vue/recommended',
    'plugin:@typescript-eslint/recommended',
    'eslint:recommended',
    'plugin:prettier/recommended',
  ],
  // add your custom rules here
  // it is base on https://github.com/vuejs/eslint-config-vue
  rules: { // 自定义规则
  // 这里可以进行自定义规则配置
  // key:规则代号
  // value:具体的限定方式
  // "off" or 0 - 关闭规则
  // "warn" or 1 - 将规则视为一个警告(不会影响退出码),只警告,不会退出程序
  // "error" or 2 - 将规则视为一个错误 (退出码为1),报错并退出程序
  rules: { // 自定义规则  这里可以进行规则的一些修改
    'prettier/prettier': 'error',
    'vue/require-default-prop': 'off',
    'arrow-body-style': 'off',
    'prefer-arrow-callback': 'off',
    'vue/max-attributes-per-line': [
      2,
      {
        singleline: 10,
        multiline: {
          max: 1,
        },
      },
    ],
    'vue/multi-word-component-names': 'off',
    'vue/singleline-html-element-content-newline': 'off',
    'vue/multiline-html-element-content-newline': 'off',
    'vue/name-property-casing': ['error', 'PascalCase'],
    'vue/no-v-html': 'off',
    'accessor-pairs': 2, // 定义对象的set存取器属性时,强制定义get 
    'arrow-spacing': [ 
      2,{ // 规则在箭头函数的箭头(=>)之前/之后标准化间距样式。
      'before': true,
      {
        before: true,
        after: true,
      },
    ],
    'block-spacing': [2, 'always'], //规则在打开的块令牌内和同一行上的下一个令牌内强制执行一致的间距。
    'brace-style': [
      2,
      '1tbs',// 规则为块执行一致的括号样式。
      {
        allowSingleLine: true,
      },
    ],
    camelcase: [
      0,// 变量命名规范 
      {
        properties: 'always',
      },
    ],
    'comma-dangle': [2, 'only-multiline'], // 强制在对象和数组文字中一致地使用尾随逗号。
    'comma-spacing': [
      2,	// 规则在变量声明,数组文字,对象文字,函数参数和序列中的逗号前后加上一致的间距。
      {
        before: false,
        after: true,
      },
    ],
    'comma-style': [2, 'last'],
    'constructor-super': 2, // 该规则检查是否存在有效的super()调用 [构造函数]。
    curly: [2, 'multi-line'], // 通过确保将块语句包装在花括号中来防止错误并提高代码清晰度。当它遇到忽略大括号的块时它会发出警告。 [if else]
    'dot-location': [2, 'property'],
    'eol-last': 2,// 文件末尾强制换行 
    eqeqeq: ['error', 'always', { null: 'ignore' }],// 随便想用啥 用啥
    'generator-star-spacing': [
      2,//生成器函数*的前后空格
      {
        before: true,
        after: true,
      },
    ],
    'handle-callback-err': [2, '^(err|error)$'],//nodejs 处理错误
    'key-spacing': [
      2, // 强制在对象字面量属性中的键和值之间保持一致的间距。在长行的情况下,允许在允许空格的地方添加新行
      {
        beforeColon: false,
        afterColon: true,
      },
    ],
    'keyword-spacing': [
      2, // 围绕关键字和关键字标记的一致空格
      {
        before: true,
        after: true,
      },
    ],
    'new-cap': [
      2,// 要求构造函数名以大写字母开头。某些内置标识符可免除此规则。
      {
        newIsCap: true,
        capIsNew: false,
      },
    ],
    'new-parens': 2,// 此规则在使用new关键字调用不带参数的构造函数时需要括号,以便提高代码清晰度
    'no-array-constructor': 2,// 此规则不允许使用Array构造函数
    'no-caller': 2,
    'no-console': 'off',
    'no-class-assign': 2,//禁止给类赋值
    'no-cond-assign': 2,//禁止在条件表达式中使用赋值语句
    'no-const-assign': 2, //禁止修改const声明的变
    'no-control-regex': 0,
    'no-delete-var': 2, //不能对var声明的变量使用delete操作符
    'no-dupe-args': 2,//函数参数不能重复
    'no-dupe-class-members': 2,// 此规则旨在标记在级别成员中使用重复名称。
    'no-dupe-keys': 2,//在创建对象字面量时不允许键重复 {a:1,a:1}
    'no-duplicate-case': 2,//switch中的case标签不能重复
    'no-empty-character-class': 2, //正则表达式中的[]内容不能为空
    'no-empty-pattern': 2,// 不能空解构,可以默认赋值
    'no-eval': 2,
    'no-ex-assign': 2, //禁止给catch语句中的异常参数赋值
    'no-extend-native': 2,//禁止扩展native对象
    'no-extra-bind': 2,//禁止不必要的函数绑定
    'no-extra-boolean-cast': 2,//禁止不必要的bool转换
    'no-extra-parens': [2, 'functions'], //禁止非必要的括号
    'no-fallthrough': 2,
    'no-floating-decimal': 2,//禁止省略浮点数中的0 .5 3
    'no-func-assign': 2,//禁止重复的函数声明
    'no-implied-eval': 2,//禁止使用隐式eval
    'no-inner-declarations': [2, 'functions'],//禁止在块语句中使用声明(变量或函数)
    'no-invalid-regexp': 2, //禁止无效的正则表达式
    'no-invalid-regexp': 2,//禁止无效的正则表达式
    'no-irregular-whitespace': 2,//不能有不规则的空格
    'no-iterator': 2,//禁止使用__iterator__ 属性
    'no-label-var': 2,, //label名不能与var声明的变量名相同
    'no-labels': [
      2, //禁止标签声明
      {
        allowLoop: false,
        allowSwitch: false,
      },
    ],
    'no-lone-blocks': 2,//禁止不必要的嵌套块
    'no-mixed-spaces-and-tabs': 2,//禁止混用tab和空格
    'no-multi-spaces': 2,// 没有多余的空格
    'no-multi-str': 2,//字符串不能用\换行
    'no-multiple-empty-lines': [
      2,//空行最多不能超过2行
      {
        max: 1,
      },
    ],
      'no-native-reassign': 2, //不能重写native对象
    'no-negated-in-lhs': 2, //in 操作符的左边不能有!
    'no-new-object': 2,   // /禁止使用new Object()
    'no-new-require': 2,  //禁止使用new require
    'no-new-wrappers': 2, //禁止使用new创建包装实例,new String new Boolean new Number
    'no-obj-calls': 2, //不能调用内置的全局对象,比如Math() JSON()
    'no-octal': 2, //禁止使用八进制数字
    'no-octal-escape': 2, //禁止使用八进制转义序列
    'no-path-concat': 2, //禁止给参数重新赋值
    'no-proto': 2, //禁止使用__proto__属性
    'no-redeclare': 2, //禁止重复声明变量
    'no-regex-spaces': 2, //禁止在正则表达式字面量中使用多个空格 /foo bar/
    'no-return-assign': [2, 'except-parens'], // return 赋值除非用圆括号括起来,否则不允许赋值。
    'no-self-assign': 2,
    'no-self-compare': 2,//不能比较自身
    'no-sequences': 2,
    'no-shadow-restricted-names': 2,//严格模式中规定的限制标识符不能作为声明时的变量名使用
    'no-sparse-arrays': 2,//禁止稀疏数组, [1,,2]
    'no-this-before-super': 2,
    'no-throw-literal': 2,
    'no-trailing-spaces': 2,
    'no-undef': 2,
    'no-undef-init': 2,
    'no-unexpected-multiline': 2,
    'no-unmodified-loop-condition': 2,
    'no-unneeded-ternary': [
      2,
      {
        defaultAssignment: false,
      },
    ],
    'no-unreachable': 2,
    'no-unsafe-finally': 2,
    'no-unused-vars': [
      2,
      {
        vars: 'all',
        args: 'none',
      },
    ],
    'no-useless-call': 2,//禁止不必要的call和apply
    'no-useless-computed-key': 2,// 没有必要使用带文字的计算属性
    'no-useless-constructor': 2,
    'no-useless-escape': 0,// 转义字符串
    'no-whitespace-before-property': 2,// 如果对象的属性位于同一行上,则该规则不允许围绕点或在开头括号之前留出空白。当对象和属性位于不同的行上时,此规则允许使用空格,
    'no-with': 2,// //禁用with
    'one-var': [
      2,//连续声明
      {
        initialized: 'never',
      },
    ],
    'operator-linebreak': [
      2,
      'after',//换行时运算符在行尾还是行首
      {
        overrides: {
          '?': 'before',
          ':': 'before',
        },
      },
    ],
    'padded-blocks': [2, 'never'], //块语句内行首行尾是否要空行
    quotes: [
      2,
      'single', //引号类型 `` "" ''
      {
        avoidEscape: true,
        allowTemplateLiterals: true,
      },
    ],
    semi: [2, 'never'],// 不使用分号
    'semi-spacing': [
      2,// 禁止或强制使用分号周围的空格可以提高程序的可读性
      {
        before: false,
        after: true,
      },
    ],
    'space-before-blocks': [2, 'always'],//此规则将强化块之前的间距一致性
    'space-in-parens': [2, 'never'], //小括号里面要不要有空格
    'space-infix-ops': 2,//中缀操作符周围要不要有空格
    'space-unary-ops': [
      2,//一元运算符的前/后要不要加空格
      {
        words: true,
        nonwords: false,
      },
    ],
    'spaced-comment': [
      2,
      'always',//注释风格要不要有空格什么的
      {
        markers: ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ','],
      },
    ],
    'template-curly-spacing': [2, 'never'], //此规则旨在保持模板文字内部空间的一致性
    'use-isnan': 2,//禁止比较时使用NaN,只能用isNaN()
    'valid-typeof': 2, //必须使用合法的typeof的值
    'wrap-iife': [2, 'any'],//立即执行函数表达式的小括号风格
    'yield-star-spacing': [2, 'both'],// 规则强制执行*周围 yield*表达式的间距
    yoda: [2, 'never'],// 强制执行一种将变量与文字值进行比较的一致条件样式
    'prefer-const': 2,// 标记使用let关键字声明的变量,但在初始分配后从未重新分配变量。
    'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,// debugger可以终止代码执行
    'array-bracket-spacing': [2, 'never'],// 规则在数组括号内强制实现一致的间距。
  },
}

详情参考:CSDN

2.1ESLint-在vscode中使用插件(vue中使用)

安装eslint插件, 让vscode实时告诉咱们哪里错了
在这里插入图片描述

用vscode打开项目时,将脚手架工程作为vscode根目录, 因为eslint要使用配置文件.eslintrc

2.2 eslint自动格式化修正代码:

按如下五个步骤:

在这里插入图片描述

下面是补充内容:

如果想ctrl+s自动格式化 则可以在设置的配置json文件 写入一下代码:

{
  "eslint.enable": true,
  "eslint.run": "onType",
  "eslint.options": {
      "extensions": [
          ".js",
          ".vue",
          ".jsx",
          ".tsx"
      ]
  },
  "editor.codeActionsOnSave": {
      "source.fixAll.eslint": true
  }
}

3、在代码中 eslint并不能处理jsx中的代码 我们可以在vscode中下载插件 prettier-now

在这里插入图片描述

4、Eslint 的实现原理,简单版

在这里插入图片描述

Eslint 是我们每天都在用的工具,我们会用它的 cli 或 api 来做代码错误检查和格式检查,有时候也会写一些 rule 来做自定义的检查和修复。

虽然每天都用,但我们却很少去了解它是怎么实现的。而了解 Eslint 的实现原理能帮助我们更好的使用它,更好的写一些插件。

所以,这篇文章我们就通过源码来探究下 Eslint 的实现原理吧。

Linter

Linter 是 eslint 最核心的类了,它提供了这几个 api:

verify // 检查
verifyAndFix // 检查并修复

getSourceCode // 获取 AST
defineParser // 定义 Parser
defineRule // 定义 Rule
getRules // 获取所有的 Rule

SourceCode 就是指的 AST(抽象语法树),Parser 是把源码字符串解析成 AST 的,而 Rule 则是我们配置的那些对 AST 进行检查的规则。这几个 api 比较容易理解。

Linter 主要的功能是在 verify 和 verifyAndFix 里实现的,当命令行指定 --fix 或者配置文件指定 fix: true 就会调用 verifyAndFix 对代码进行检查并修复,否则会调用 verify 来进行检查。

那 verify 和 fix 是怎么实现的呢?这就是 eslint 最核心的部分了:

确定 parser

我们知道 Eslint 的 rule 是基于 AST 进行检查的,那就要先把源码 parse 成 AST。而 eslint 的 parser 也是可以切换的,需要先找到用啥 parser:

默认是 Eslint 自带的 espree,也可以通过配置来切换成别的 parser,比如 @eslint/babel-parser、@typescript/eslint-parser 等。

下面是 resolve parser 的逻辑:

确定了 parser 之后,就是调用 parse 方法了。

parse 成 SourceCode

parser 的 parse 方法会把源码解析为 AST,在 eslint 里是通过 SourceCode 来封装 AST 的。后面看到 SourceCode 就是指 AST.
在这里插入图片描述

有了 AST,就可以调用 rules 对 AST 进行检查了

调用 rule 对 SourceCode 进行检查,获得 lintingProblems

parse 之后,会调用 runRules 方法对 AST 进行检查,返回结果就是 problems,也就是有什么错误和怎么修复的信息。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Bh6OpmZh-1683725597559)(C:\Users\admin\AppData\Roaming\Typora\typora-user-images\1683681925939.png)]

那 runRules 是怎么运行的 rule 呢?

rule 的实现如下,就是注册了对什么 AST 做什么检查,这点和 babel 插件很类似。

在这里插入图片描述

runRules 会遍历 AST,然后遇到不同的 AST 会 emit 不同的事件。rule 里处理什么 AST 就会监听什么事件,这样通过事件监听的方式,就可以在遍历 AST 的过程中,执行不同的 rule 了。

注册 listener:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jniq0RUi-1683725597560)(C:\Users\admin\AppData\Roaming\Typora\typora-user-images\1683682371770.png)]

遍历 AST,emit 不同的事件,触发 listener:

在这里插入图片描述

这样,遍历完一遍 AST,也就调用了所有的 rules,这就是 rule 的运行机制

还有,遍历的过程中会传入 context,rule 里可以拿到,比如 scope、settings 等。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2YJjWGcs-1683725597560)(C:\Users\admin\AppData\Roaming\Typora\typora-user-images\1683682449381.png)]

还有 ruleContext,调用 AST 的 listener 的时候可以拿到:

在这里插入图片描述

而 rule 里面就是通过这个 report 的 api 进行报错的,那这样就可以把所有的错误收集起来,然后进行打印。

这个 problem 是什么呢?

linting problem

lint problem 是检查的结果,也就是从哪一行(line)哪一列(column)到哪一行(endLine)哪一列(endColumn),有什么错误(message)。

还有就是怎么修复(fix),修复其实就是 从那个下标到哪个下标(range),替换成什么文本(text)

在这里插入图片描述

为什么 fix 是 range 返回和 text 这样的结构呢?因为它的实现就是简单的字符串替换。

通过字符串替换实现自动 fix

遍历完 AST,调用了所有的 rules,收集到了 linting problems 之后,就可以进行 fix 了。

fix 部分的相关源码是这样的:

在这里插入图片描述

也就是 verify 进行检查,然后根据 fix 信息自动 fix。

fix 其实就是个字符串替换:
在这里插入图片描述

有的同学可能注意到了,字符串替换为什么要加个 while 循环呢?

因为多个 fix 之间的 range 也就是替换的范围可能是有重叠的,如果有重叠就放到下一次来修复,这样 while 循环最多修复 10 次,如果还有 fix 没修复就不修了。

这就是 fix 的实现原理,通过字符串替换来实现的,如果有重叠就循环来 fix。

preprocess 和 postprocess

其实核心的 verify 和 fix 的流程就是上面那些,但是 Eslint 还支持之前和之后做一些处理。也就是 pre 和 post 的 process,这些也是在插件里定义的。

module.exports = {
    processors: {
        ".txt": {
            preprocess: function(text, filename) {
                return [ // return an array of code blocks to lint
                    { text: code1, filename: "0.js" },
                    { text: code2, filename: "1.js" },
                ];
            },

            postprocess: function(messages, filename) {
              
                return [].concat(...messages);
            }
        }
    }
};

之前的处理是把非 js 文件解析出其中的一个个 js 文件来,这和 webpack 的 loader 很像,这使得 Eslint 可以处理非 JS 文件的 lint。

之后的处理呢?那肯定是处理 problems 啊,也就是 messages,可以过滤掉一些 messages,或者做一些修改之类的。

那 preprocess 和 postprocess 是怎么实现的呢?

这个就比较简单了,就是在 verify 之前和之后调用就行。

在这里插入图片描述

在这里插入图片描述

通过 comment directives 来过滤掉一些 problems

我们知道 eslint 还支持通过注释来配置,比如 /* eslint-disable */ /*eslint-enable*/ 这种。

那它是怎么实现的呢?

注释的配置是通过扫描 AST 来收集所有的配置的,这种配置叫做 commentDirective,也就是哪行那列 Eslint 是否生效。

然后在 verify 结束的时候,对收集到的 linting problems 做一次过滤即可。

在这里插入图片描述

上面讲的这些就是 Eslint 的实现原理:
在这里插入图片描述

Eslint 和 CLIEngine 类

Linter 是实现核心功能的,上面我们介绍过了,但是在命令行的场景下还需要处理一些命令行参数,也就需要再包装一层 CLIEngine,用来做文件的读写,命令行参数的解析。

它有 executeOnFiles 和 executeOnText 等 api,是基于 Linter 类的上层封装。

但是 CLIEngine 并没有直接暴露出去,而是又包装了一层 EsLint 类,它只是一层比较好用的门面,隐藏了一些无关信息。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SFbMkYC3-1683725597562)(C:\Users\admin\AppData\Roaming\Typora\typora-user-images\1683682764103.png)]

我们看下 eslint 最终暴露出来的这几个 api:

  • Linter 是核心的类,直接对文本进行 lint
  • ESLint 是处理配置、读写文件等,然后调用 Linter 进行 lint(中间的那层 CLIEngine 并没有暴露出来)
  • SourceCode 就是封装 AST 用的
  • RuleTester 是用于 rule 测试的一些 api。
    在这里插入图片描述

总结

我们通过源码理清了 eslint 的实现原理:

ESLint 的核心类是 Linter,它分为这样几步:

  • preprocess,把非 js 文本处理成 js
  • 确定 parser(默认是 espree)
  • 调用 parser,把源码 parse 成 SourceCode(ast)
  • 调用 rules,对 SourceCode 进行检查,返回 linting problems
  • 扫描出注释中的 directives,对 problems 进行过滤
  • postprocess,对 problems 做一次处理
  • 基于字符串替换实现自动 fix

除了核心的 Linter 类外,还有用于处理配置和读写文件的 CLIEngine 类,以及最终暴露出去的 Eslint 类。

这就是 Eslint 的实现原理,其实还是挺简单的:

基于 AST 做检查,基于字符串做 fix,之前之后还有 pre 与 post 的process,支持注释来配置过滤掉一些 problems。

  • SourceCode 就是封装 AST 用的
  • RuleTester 是用于 rule 测试的一些 api。

在这里插入图片描述

总结

我们通过源码理清了 eslint 的实现原理:

ESLint 的核心类是 Linter,它分为这样几步:

  • preprocess,把非 js 文本处理成 js
  • 确定 parser(默认是 espree)
  • 调用 parser,把源码 parse 成 SourceCode(ast)
  • 调用 rules,对 SourceCode 进行检查,返回 linting problems
  • 扫描出注释中的 directives,对 problems 进行过滤
  • postprocess,对 problems 做一次处理
  • 基于字符串替换实现自动 fix

除了核心的 Linter 类外,还有用于处理配置和读写文件的 CLIEngine 类,以及最终暴露出去的 Eslint 类。

这就是 Eslint 的实现原理,其实还是挺简单的:

基于 AST 做检查,基于字符串做 fix,之前之后还有 pre 与 post 的process,支持注释来配置过滤掉一些 problems。

把这些理清楚之后,就算是源码层面掌握了 Eslint 了。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值