前端工程化

工程化

定义

前端工程化就是指遵循一定的标准和规范,通过工具去提高效率,降低成本的一种手段。

主要解决的问题
  • 传统语言或语法的弊端
  • 无法使用模块化/组件化
  • 减少重复的机械式工作
  • 统一代码风格、保证代码质量
  • 解决依赖后端服务接口支持、整体依赖后端项目的问题

脚手架

脚手架的本质作用:

创建项目基础结构、提供项目规范和约定

  • 相同的组织结构
  • 相同的开发范式
  • 相同的模块依赖
  • 相同的工具配置
  • 相同的基础代码

以上问题将会导致,创建项目时有许多重复的工作要做,脚手架可以快速搭建项目骨架,解决上面的问题。

常用的脚手架工具

根据提供的信息创建对应的项目基础结构

特定项目脚手架
  1. React ===> create-react-app
  2. Vue ===> vue-cli
  3. Angular ===> angular-cli
通用项目脚手架

Yeoman(根据一套模板,生成一套对应的项目结构)

Plop(项目开发过程中,用于创建特定类型的文件,例如创建一个组件/模块所需要的文件)

Yeoman

yeoman 由 yo + 特定的generator 构成

安装配置
安装yo
yarn global add yo
配置环境变量

当使用yo -v时,提示不是内部或外部命令时,需要配置环境变量

//查看yarn全局安装的bin目录
yarn global bin
//查看yarn全局安装目录
yarn global dir
//将yo全局安装的bin目录配置到环境变量Path中
安装generator

以generator-node为例

yarn global add generator-node
基本使用
创建对应generator项目
yo node
Sub Generator
yo node:cli
Yeoman使用步骤总结
  1. 明确你的需求
  2. 找到合适的Generator
  3. 全局范围安装找到的generator
  4. 通过Yo运行对应的Generator
  5. 通过命令行交互填写选项
  6. 生成所需的项目结构
自定义Generator

Generator本质上就是一个NPM的模块

Generator基本结构
|- generators/ # 生成器目录
 |  |- app/ # 默认生成器目录
 |  |  |- index.js # 默认生成器实现
+|  |- component/ # 其他生成器目录
+|  |  |- index.js # 其他生成器实现
 |- package.json # 模块包配置文件

生成器模块名称必须是 generator-<name>的形式

自定义Generator步骤
  1. 创建一个generator-<name>文件夹

    mkdir generator-sample
    
  2. yarn init 初始化

  3. 安装依赖 yeoman-generator,提供工具函数,让generator创建更加便捷

    yarn add yeoman-generator
    
  4. 创建Generator基本结构

    generators/app/index.js 核心入口

    generators/app/templates 模板文件夹

    // index.js
    // 此文件作为Generator的核心入口
    // 需要导出一个继承自 Yeoman Generator 的类型
    // Yeoman Generator在工作时会自动调用我们在此类型中定义的一些生命周期方法
    // 我们在这些方法中可以通过调用父类提供的一些工具方法实现一些功能
    
    const Generator = require('yeoman-generator')
    
    module.exports = class extends Generator {
        prompting() {
            // Yeoman 在询问用户环节会自动调用此方法
            // 在此方法中可以调用父类的 prompt() 方法发出对用户的命令行询问
            // prompt 方法返回一个promise
            return this.prompt([{
                    type: 'input',
                    name: 'name',
                    message: 'Your project name',
                    default: this.appname //appname 为项目生成目录名称
                }])
                .then(answers => {
                    // answers=>{name:'users input value'}
                    // 挂载到this上,方便writing时调用
                    this.answers = answers
                })
        }
        writing() {
            // Yeoman 自动在生成文件阶段调用此方法
            // 我们这里尝试往项目目录中写入文件
            // this.fs.write(
            //     this.destinationPath('temp.txt'),
            //     '随机生成的文件内容' + Math.random().toString()
            // )
    
            // 通过模板方式写入文件到目标目录
    
            // // 模板文件路径
            // const tmpl = this.templatePath('foo.txt')
            // // 输出路径
            // const output = this.destinationPath('foo.txt')
            // // 模板数据上下文
            // const context = {
            //     title: '我是一个标题',
            //     success: true
            // }
    
            // this.fs.copyTpl(tmpl, output, context)
    
            
            // 模板文件路径
            const tmpl = this.templatePath('bar.html')
            // 输出路径
            const output = this.destinationPath('bar.html')
            // 获取用户输入
            const context = this.answers
    
            this.fs.copyTpl(tmpl, output, context)
        }
    }
    
发布Generator
  1. 创建远程仓库,并将本地代码推送到远程仓库
  2. 使用yarn publish --registry=https://registry.yarnpkg.comnpm publish --registry=https://registry.npmjs.org/命令发布

Plop

一个小而美的脚手架工具

Plop具体使用
安装plop开发依赖到项目中
yarn add plop --dev
项目根目录下新建plopfile.js文件
// Plop 入口文件,需要导出一个函数
// 此函数接收一个 plop 对象,用于创建生成器任务

module.exports = plop => {
    // plop.setGenerator(生成器名称,配置对象)
    plop.setGenerator('component', {
        description: 'create a component',
        // 用户交互
        prompts: [{
            type: 'input',
            name: 'name',
            message: 'component name',
            default: 'MyComponent'
        }],
        // 动作
        actions: [{
            type: 'add', //代表添加文件
            path: 'src/components/{{name}}/{{name}}.js', //输出路径
            templateFile: 'plop-templates/component.hbs' //模板文件路径
        }]
    })
}
在根目录下创建模板文件夹并定义模板文件

plop-templates/components.hbs

import React from 'react';

export default ()=>(
    <div calssName="{{name}}">
        <h1>{{name}} Component</h1>
    </div>
)
使用Plop
yarn plop component
Plop使用总结
  1. 将plop模块作为项目开发依赖安装
  2. 在项目根目录下创建一个plopfile.js文件
  3. 在plopfile.js文件中定义脚手架任务
  4. 编写用于生成特定类型文件的模板
  5. 通过Plop提供的CLI运行脚手架任务

自动化构建

在开发过程中,会使用一些不被浏览器支持的特性(例如:ECMAScript Next、sass、模板引擎),自动化构建可以转换这些不被支持的特性,这样便能在开发中使用这些特性,提高编码效率。

NPM Script构建

通过在项目中添加所需开发依赖(如:sass),再通过在NPM Script中配置脚本命令,实现自动执行依赖命令,完成构建工作(将sass编译为css)。

//安装依赖
yarn add sass --dev
//在package.json文件中配置脚本命令
"scripts": {
    "build":"sass scss/main.scss css/style.css"
}
//执行脚本命令
yarn build
Grunt
安装grunt
yarn add grunt --dev
使用grunt

创建 gruntfile.js 文件,该文件为grunt的入口文件,可以在该文件中添加任务、配置等

注册任务-registerTask
// Grunt 的入口文件
// 用于定义一些需要 Grunt 自动执行的任务
// 需要导出一个函数
// 此函数接收一个 grunt 的形参, grunt为一个对象, 内部提供一些创建任务时可以用到的 API

module.exports = grunt => {
    // foo 任务名称 + 执行任务时自动调用的回调函数
    grunt.registerTask('foo', () => {
        console.log('hello grunt~');
    })

    grunt.registerTask('bar', '任务描述', () => {
        console.log('other task~');
    })

    // grunt.registerTask('default', '默认任务', () => {
    //     console.log('default task~');
    // })

    // 默认任务,第二个参数传递需要依次执行的任务名称数组
    grunt.registerTask('default', ['foo', 'bad', 'bar'])

    // 执行异步任务,需要调用this.async()得到一个done函数,在异步任务完成后,执行这个函数,标识异步任务已结束
    // 需要获取this,不能使用箭头函数
    grunt.registerTask('async-task', function () {
        let done = this.async()
        setTimeout(() => {
            console.log('async task working!');
            done()
        }, 1000)
    })

    // return false可以标记任务失败 多个任务执行时,一个任务失败会中断后续任务的执行,在执行命令时加上--force可以强制后续任务执行
    grunt.registerTask('bad', () => {
        console.log('bad working!');
        return false
    })
    // 为done函数传递false参数,可以标记异步任务失败
    grunt.registerTask('bad-async', function () {
        let done = this.async()
        setTimeout(() => {
            console.log('bad-async working!');
            done(false)
        }, 1000)
    })
}
标记任务失败
module.exports = grunt => {
    // return false可以标记任务失败 多个任务执行时,一个任务失败会中断后续任务的执行,在执行命令时加上--force可以强制后续任务执行
    grunt.registerTask('bad', () => {
        console.log('bad working!');
        return false
    })
    // 为done函数传递false参数,可以标记异步任务失败
    grunt.registerTask('bad-async', function () {
        let done = this.async()
        setTimeout(() => {
            console.log('bad-async working!');
            done(false)
        }, 1000)
    })
}
Grunt的配置方法-initConfig

通过grunt.initConfig(配置对象),进行配置。在任务中通过grunt.config(配置对象属性名),获取配置。

module.exports = grunt => {
    grunt.initConfig({
        // foo: 'bar'
        foo: {
            bar:123
        }
    })

    grunt.registerTask('foo', () => {
        const foo=grunt.config('foo')
        console.log('grunt config:',foo);
    })
}
Grunt多目标任务
module.exports = grunt => {
    // 配置多目标任务时,需要为其配置不同的目标
    grunt.initConfig({
        build: {
            // options中的信息,会作为当前任务的配置选项,
            options: {
                foo: 'bar'
            },
            // 其他属性,均为任务的目标
            css: {
                // 目标中也可以设置配置选项,将会覆盖任务的配置选项
                options:{
                    foo:'baz'
                }
            },
            js: '2'
        }
    })
    // 多目标模式,可以让任务根据配置形成多个子任务
    grunt.registerMultiTask('build', function () {
        console.log('build task');
        // 可以通过 this.target 获取当前执行的目标名称,this.data获取当前target的配置数据
        console.log(`target: ${this.target}, data: ${this.data}`);
        // 可以通过 this.options 获取当前任务的所有配置选项
        console.log('build options:',this.options());
    })
}

yarn grunt build时,多目标中的回调将会执行两次,即两个目标。若需指定执行哪个目标,可使用yarn grunt build:css的形式,指定需要执行的目标。

Grunt插件使用

安装插件依赖(以grunt-contrib-clean为例)

yarn add grunt-contrib-clean

配置任务目标

module.exports = grunt => {
    grunt.initConfig({
        // 添加插件配置
        clean: {
            temp: 'temp/app.css',
            js:'temp/*.js',
            any:'temp/**'
        }
    })
    // 引入插件任务
    grunt.loadNpmTasks('grunt-contrib-clean')
}

运行

yarn grunt clean
Gulp
安装gulp
yarn add gulp --dev
使用gulp

创建 gulpfile.js 文件,该文件为gulp的入口文件,可以在该文件中定义任务。gulp中的任务为异步任务,需调用回调函数或其他方式标识任务结束,否则会报错。

定义任务
// gulp 的入口文件

// 通过导出函数的方式定义任务
exports.foo = done => {
    console.log('foo task working!');
    done() // 标识任务完成
}

// default为默认任务
exports.default = done => {
    console.log('default task working!');
    done()
}

// gulp 4.0以前的任务定义方式 不再推荐使用
const gulp=require('gulp')

gulp.task('bar',done=>{
    console.log('bar task working!');
    done()
})
组合任务

series串行、parallel并行

const {
    series,
    parallel
} = require('gulp')

const task1 = done => {
    setTimeout(() => {
        console.log('task1 working!');
        done()
    }, 1000)
}

const task2 = done => {
    setTimeout(() => {
        console.log('task2 working!');
        done()
    }, 1000)
}

const task3 = done => {
    setTimeout(() => {
        console.log('task3 working!');
        done()
    }, 1000)
}

// series将会按照顺序执行传入的任务
exports.foo = series(task1, task2, task3)
// parallel将会并行执行传入的任务
exports.bar = parallel(task1, task2, task3)
异步任务
const fs = require('fs');
// 回调函数形式
exports.callback = done => {
    console.log('callback task!');
    done()
}
exports.callback_error = done => {
    console.log('callback_error task!');
    // done中传入一个错误,错误优先原则,后续任务将不会执行
    done(new Error('task failed!'))
}

// promise形式
exports.promise = () => {
    console.log('promise task!');
    return Promise.resolve()
}
exports.promise_error = () => {
    console.log('promise_error task!');
    // 抛出异常,后续任务将不会执行
    return Promise.reject(new Error('task failed!'))
}

// async、await形式
const timeout = time => {
    return new Promise(resolve => {
        setTimeout(resolve, time);
    })
}
exports.async = async () => {
    await timeout(500)
    console.log('async task!');
}

// return 一个stream形式
exports.stream = () => {
    // 读取Stream
    const readStream = fs.createReadStream('package.json')
    // 写入Stream
    const writeStream = fs.createWriteStream('temp.txt')
    readStream.pipe(writeStream)
    return readStream
}
// 模拟gulp中stream结束
exports._stream = done => {
    // 读取Stream
    const readStream = fs.createReadStream('package.json')
    // 写入Stream
    const writeStream = fs.createWriteStream('temp.txt')
    readStream.pipe(writeStream)
    readStream.on('end', () => {
        done()
    })
}
Gulp构建过程核心工作原理
const fs = require('fs');
const {
    Transform
} = require('stream')

exports.default = () => {
    // 文件读取流
    const read = fs.createReadStream('css/app.css')
    // 文件写入流
    const write = fs.createWriteStream('dist/app.min.css')
    // 文件转换流
    const transform = new Transform({
        transform: (chunk, encoding, callback) => {
            // 核心转换过程实现
            // chunk=>读取流中读取到的内容(Buffer)
            const input = chunk.toString()
            const output = input.replace(/\s+/g, '').replace(/\/\*.+?\*\//g, '')
            // 第一个参数为错误对象,无错误时传入null即可
            callback(null, output)
        }
    })

    // 把读取出来的文件流导入写入文件流
    read.pipe(transform) //转换
        .pipe(write) //写入
    return read
}
Gulp文件操作API
const {
    src, //读取
    dest //写入
} = require('gulp');
const cleanCss = require('gulp-clean-css'); //压缩整理Css
const rename = require('gulp-rename'); //重命名

exports.default = () => {
    return src('css/*.css')
        .pipe(cleanCss())
        .pipe(rename({
            extname: '.min.css'
        }))
        .pipe(dest('dist'))
}
压缩转换CSS
// 安装压缩Css插件
yarn add gulp-clean-css --dev
// 安装重命名插件
yarn add gulp-rename --dev

// gulpfile.js 入口文件
const {
    src,
    dest,
} = require('gulp');
const cleanCss = require('gulp-clean-css')
exports.default = () => {
    return src('src/*.css')
        .pipe(cleanCss())	//压缩CSS
        .pipe(rename({extname:'.min.css'}))	//更改文件拓展名
        .pipe(dest('dist'))
}
项目构建完整流程
样式编译
// 安装sass转换插件
yarn add gulp-sass --dev
// 安装sass编译器
yarn add sass --dev
===

const {
  src,
  dest
} = require('gulp')
// 引入sass转换插件,并指定默认编译器为sass
const sass = require('gulp-sass')(require('sass'));

// 样式编译
const style = () => {
  return src('src/assets/styles/*.scss', {
      base: 'src'
    }) //通过base指定基准路径,文件输出时将会保留基准路径
    .pipe(sass()) //转换sass
    .pipe(dest('dist')) //dist/src/assets/styles/*.scss'
}

module.exports = {
  style
}
脚本编译
// 安装脚本转换插件
yarn add gulp-babel --dev
// 安装转换模块
yarn add @babel/core @babel/preset-env --dev
===

const {
  src,
  dest
} = require('gulp')
// 引入babel插件
const babel = require('gulp-babel')

// 脚本编译
const script = () => {
  return src('src/assets/scripts/*.js', {
      base: 'src'
    })
    .pipe(babel({
      presets: ['@babel/preset-env']    //指定转换模块
    })) //转换js
    .pipe(dest('dist'))
}

module.exports = {
  script
}
页面文件编译
// 安装swig模板转换插件
yarn add gulp-swig --dev

// 引入swig插件
const swig = require('gulp-swig')

// 模板页面所需参数
const data = {
  menus: [{
      name: 'Home',
      icon: 'aperture',
      link: 'index.html'
    },
    {
      name: 'Features',
      link: 'features.html'
    },
    {
      name: 'About',
      link: 'about.html'
    },
    {
      name: 'Contact',
      link: '#',
      children: [{
          name: 'Twitter',
          link: 'https://twitter.com/w_zce'
        },
        {
          name: 'About',
          link: 'https://weibo.com/zceme'
        },
        {
          name: 'divider'
        },
        {
          name: 'About',
          link: 'https://github.com/zce'
        }
      ]
    }
  ],
  pkg: require('./package.json'),
  date: new Date()
}
// 页面编译
const page = () => {
  return src('src/**/*.html', {
      base: 'src'
    })
    .pipe(swig({
      data, //传入模板参数
      defaults: { cache: false } //关闭缓存,未关闭会导致页面刷新时显示不变
    })) //转换模板页面
    .pipe(dest('dist'))
}

module.exports = {
  page,
}
组合任务

为样式、脚本、页面文件创建组合任务

const {
  src,
  dest,
  series,
  parallel
} = require('gulp')

// 组合任务
const compile = parallel(style, script, page)

module.exports = {
  compile,
}
图片字体压缩
// 安装图片压缩插件
yarn add gulp-imagemin --dev

// 引入imagemin插件
const imagemin = require('gulp-imagemin')

// 压缩图片
const image = () => {
  return src('src/assets/images/**', {
      base: 'src'
    })
    .pipe(imagemin())
    .pipe(dest('dist'))
}
// 压缩字体
const font = () => {
  return src('src/assets/fonts/**', {
      base: 'src'
    })
    .pipe(imagemin()) //压缩svg字体
    .pipe(dest('dist'))
}

module.exports = {
  compile,
  image,
  font
}
其他文件及文件清除
// 安装清除插件
yarn add del --dev

// 引入del文件清除插件
const del = require('del');

// 其他文件拷贝
const extra = () => {
  return src('public/**')
    .pipe(dest('dist/public'))
}
// 文件清除
const clean = () => {
  return del(['dist'])
}
// build任务
const build = series(clean, parallel(compile, extra, image, font))

module.exports = {
  compile,
  build
}
自动加载插件

为避免频繁手动引入插件,安装一个能够自动加载gulp依赖的插件

// 安装自动加载插件
yarn add gulp-load-plugins --dev

// 引入自动加载插件
const loadPlugins=require('gulp-load-plugins')
// 自动加载glup插件,使用plugins.<name>的方式调用,gulp-name-subnam会被转为驼峰命名法的形式,使用=>plugins.nameSubname
const plugins=loadPlugins()
热更新

为了方便调试,让本地代码修改无需刷新就能够实时反馈到浏览器,需要安装browser-sync插件实现热更新的效果。

// 安装热更新模块
yarn add browser-sync --dev

// 引入热更新插件
const browserSync = require('browser-sync')
// 创建开发服务器
const bs = browserSync.create()

// 热更新
const serve = () => {
   bs.init({
    notify:false, //关闭提示
    port:2080, //启动端口
    // open: false, //是否自动开启浏览器
    files:'dist/**', //指定目录下的文件变化时,自动更新浏览器
    server: {
      baseDir: 'dist', //指定服务器根目录,为打包发布的目录dist
      //   配置路由
      routes: {
        '/node_modules': 'node_modules' //将文件中对'/node_modules'的请求,映射到当前gulpfile相对路径下的node_modules文件夹
      }
    }
  })
}

此时,dist文件夹下文件发生变化,浏览器将会立即更新

监听文件变化

为了让src目录下的文件修改时,能够实现热更新的效果,需要在src目录文件变更时,重新编译生成dist目录下的文件。此时,需要用到gulp中的watchAPI对文件进行监听。由于热更新是应用于开发阶段调试,所以无需重新编译图片、字体以及其他文件,但需要监听其变化,重新刷新浏览器。

注意:swig模板引擎缓存机制可能会导致页面不会变化,需要将page任务中swig配置选项的cache设置为false

const {
  src,
  dest,
  series,
  parallel,
  watch, //自动监视一个文件路径通配符,根据文件是否变化,决定是否执行某个任务
} = require('gulp')

// 热更新
const serve = () => {
  // 监听文件是否变化
  watch('src/assets/styles/*.scss', style)
  watch('src/assets/scripts/*.js', script)
  watch('src/**/*.html', page)
  // 开发过程中无需监听以下文件变化并对其重新构建,减少性能消耗
  //   watch('src/assets/images/**', image)
  //   watch('src/assets/fonts/**', font)
  //   watch('public/**', extra)

  // 文件变化后,刷新浏览器
  watch([
      'src/assets/images/**',
      'src/assets/fonts/**',
      'public/**',
    ],
    bs.reload
  )

  bs.init({
    notify: false, //关闭提示
    port: 2080, //启动端口
    // open: false, //是否自动开启浏览器
    // files: 'dist/**', //指定目录下的文件变化时,自动更新浏览器-方法1
    // 自动更新浏览器-方法2,在文件编译完成后使用.pipe(bs.reload({stream:true}))刷新浏览器
    server: {
      //   baseDir: 'dist', //指定服务器请求目录,为打包发布的目录dist
      baseDir: ['dist', 'src', 'public'],  //将请求目录设为数组,当在dist目录中找不到资源时,继续查找其他目录(开发阶段不对图片、字体及其他文件打包,dist目录中不生成对应文件,需要在src、public中查找)
      //   配置路由
      routes: {
        '/node_modules': 'node_modules' //将文件中对'/node_modules'的请求,映射到当前gulpfile相对路径下的node_modules文件夹
      }
    }
  })
}
useref文件引用处理

由于页面中存在对node_modules文件夹的引用,但未对/node_modules中的文件进行打包处理,上线时会找不到引用文件。此时需要引入useref插件进行处理,useref能将构建注释中的文件引用,打包构建到指定文件中

 // 注释中的文件,将被打包构建到assets/styles/vendor.css中
 <!-- build:css assets/styles/vendor.css -->
 <link rel="stylesheet" href="/node_modules/bootstrap/dist/css/bootstrap.css">
 <!-- endbuild -->

useref将会把构建注释中的引用,在指定路径中查找所需文件,并将其重新编译到新的目录中。在输出到新的目录前,需要对文件进行打包压缩处理,此时需要引入gulp-if插件,判断文件类型,进行不同的处理。

// 安装useref插件
yarn add gulp-useref --dev
// 安装压缩文件插件
yarn add gulp-htmlmin gulp-uglify gulp-clean-css --dev
// 安装判断插件
yarn add gulp-if --dev

// 以上gulp插件,gulp-load-plugins将会自动引入

// useref文件引用处理
const useref = () => {
  // 查看temp目录下所有的html文件中的引用
  return src('temp/**/*.html', {
      base: 'temp'
    })
    // 在以下路径中搜索需要的引用文件
    .pipe(plugins.useref({
      searchPath: ['temp', '.']
    }))
    // 对重新生成的html、css、js进行压缩
    .pipe(plugins.if(/\.html$/, plugins.htmlmin({
      collapseWhitespace: true, //折叠空白字符
      minifyCSS: true, //压缩行内样式
      minifyJS: true, //压缩JS
    })))
    .pipe(plugins.if(/\.css$/, plugins.cleanCss()))
    .pipe(plugins.if(/\.js$/, plugins.uglify()))
    // 输出到如下目录中
    .pipe(dest('dist'))
}
注意:
  1. 由于需要对初步构建后的代码进行二次转换,所以需要修改原先的输出路径
  2. 在对html、css、js初次编译时,先将结果放在temp目录中。
  3. 本地serve启动时,首先使用temp中的文件(编译完的样式、脚本、模板页面),找不到时,将会继续到src、public目录中寻找(图片、字体、其他文件),再通过路由把对/node_modules的引用映射到node_modules中,即可满足开发阶段的文件引用需求。
  4. 使用useref能将temp中的构建标记包裹的引用进行转换,然后输出到最终目录dist中
  5. 上线前使用build进行打包,将样式、脚本、模板页面(从temp中useref),图片、字体以及其他文件全部构建到dist,此时dist目录中的文件已经完整。
  6. clean任务中del选项改为[‘dist’,‘temp’]
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值