node学习之旅

node学习之旅

1. 创建服务并进行简单的前端页面配置

// 引入http模块
const http = require('http');
const url = require('url')
// http.createServer创建一个web服务
// request, 获取浏览器url传递过来的信息
// response,给浏览器的响应信息
http.createServer(function (request, response) {
  // 设置相应头,
      // 状态码200,
      // 编码为:text/html
  // console.log(request.url)
  response.writeHead(200, {'Content-Type': 'text/html;charset="utf-8"'});
  // 设置html页面的编码格式,解决乱码问题
  response.write('<head><meta charset="UTF-8"></head>')
  response.write("hello");
  response.write('<h2>hello你好</h2>');
// 表示给页面上输出一句话,并结束响应
// response.end("hello")
  response.end();//结束响应
}).listen(8081);
// 监听8081端口
console.log('Server running at http://127.0.0.1:8081/');

2.获取页面的get传值

const http = require('http');
const url = require('url')
http.createServer(function (request, response) {
  response.writeHead(200, {'Content-Type': 'text/html;charset="utf-8"'});
  // 设置html页面的编码格式,解决乱码问题
  response.write('<head><meta charset="UTF-8"></head>')
  response.write("hello");
// 获取页面的get传值
    // 参数一:要解析的地址
    // 参数二:是否将值转换成object
    // request.url = http://127.0.0.1:8081/?name=zhangsan&age=12
// console.log(request)
if (request.url != "/favicon.ico"){
  const temp = url.parse(request.url, true).query
  console.log(url.parse(request.url, true))
  console.log(temp)
  console.log(`姓名:${temp.name}--年龄:${temp.age}`)
}
  response.end();//结束响应
}).listen(8081);
// 监听8081端口
console.log('Server running at http://127.0.0.1:8081/');

3.自启动工具supervisor

supervisor会不停的监听应用下的所有文件,发现文件被修改就重新载入程序文件,这样就实现了修改了程序文件后马上就能看到变更后的结果

  1. 安装supervisor
cnpm install -g supervisor
  1. 使用supervisor代替node启动应用
supervisor 要启动的文件
   // supervisor HttpUrl.js
  1. 修改代码并保存,刷新页面即可查看修改后的页面结果

4.模块、自定义模块

在nodejs中模块分为核心模块(http、url模块)以及文件模块(自定义模块)

自定义模块:将公共的功能抽离成单独的js文件作为一个模块,并使用exports或者module.exports暴露相关属性或方法即可。

  1. 新建含有公用功能的文件,文件结构与代码如下
    在这里插入图片描述
  2. 使用抽离的js功能
 // 引用
const public = require("./4_moudle/public.js")
console.log('public', public)
 // 功能调用 (以下三种方法分别为公用js中的三种暴露方法的调用方法)
 
 // 方法一:调用exports向外暴露属性或方法
 	//输出结果 public { UrlApi: [Function: UrlApi] }
	var api = public.UrlApi('/api');
	response.write(api)
 // 方法二:调用module.exports向外暴露属性或方法
 	//输出结果 public [Function: UrlApi]
 	var api = public('/api');
    response.write(api)
 // 方法三:调用exports向外暴露属性或方法
 	// 输出结果:public { UrlApi: [Function: UrlApi], get: [Function], post: [Function] }
   	public.get()
   	public.post()
  1. 注意事项
1.使用exports向外暴露属性或方法:
	返回的是模块函数
	使用时需要用模块名称去调用(public.UrlApi)
2.使用module.exports向外暴露属性或方法:
	返回的是模块对象本身,返回的是一个类
	输出的直接是需要的对象或方法,可直接使用公用js中的方法或属性(public3.参考地址:
	1.https://www.ucloud.cn/yun/95597.html

通过package.json文件配置项目文件路径(当路径不全时node在默认情况在只找文件夹下的index文件)

1. npm init 或 npm init -y 生成项目的配置文件(package.json文件)
2. package.js中查看入口文件配置main 

5.node中的fs模块

  1. fs.stat 检测是文件还是目录
  2. fs.mkdir 创建目录(无则创建, 有则返回提示信息)
  3. fs.writeFile 创建并写入文件(无则创建, 有则替换)
  4. fs.appendFile 追加文件(无则创建, 有则追加)
  5. fs.readFile 读取文件
  6. fs.readdir 读取目录或文件夹
  7. fs.rename 重命名或移动文件
  8. fs.rmdir 删除空目录
  9. fs.unlink 删除文件
var fs = require('fs');

// stat(要检测的路径,回调函数)
fs.stat('./html',(err,data)=>{
    if(err){
        console.log(err);
        return;
    }
    console.log(`是文件:${data.isFile()}`)
    console.log(`是目录:${data.isDirectory()}`)
})

// mkdir(要创建的路径,目录的读写权限、默认777、选填,回调函数传递异常参数err)
fs.mkdir('./html/css',(err)=>{
    if (err) {
        console.log(err);
        return;
    }
    console.log("mkdir success")
})

// writeFile(要创建文件的路径与文件名称,要写入的内容,设置文件的编码encoding、文件的读写权限默认438、选填,回调函数传递异常参数err)
fs.writeFile('./html/index.html','你好nodejs',(err)=>{
    if (err) {
        console.log(err);
        return;
    }
    console.log("writeFile success")
})

// appendFile()
fs.appendFile('./html/css/base.css', 'body{color:red}', (err) => {
    if (err) {
        console.log(err);
        return;
    }
    console.log("appendFile success")
})

// readFile(要检测的路径,回调函数返回的数据为buffer类型),执行需将上述的所有方法注释掉
fs.readFile('./html/index.html',(err,data)=>{
    if (err) {
        console.log(err.EEXIST);
        return;
    }
    console.log("readFile success:", data)
    console.log("readFile success & toString:", data.toString())//将buffer转换成string类型
})

// readdir()
fs.readdir('./html',(err,data)=>{
    if (err) {
        console.log(err);
        return;
    }
    console.log("readdir success:",data)
})

// rename(旧路径,新路径,回掉函数)
fs.rename('./html/css/aaa.css', './html/css/content.css', (err) => {
    if (err) {
        console.log(err);
        return;
    }
    console.log("重命名 success")
})
fs.rename('./html/js/index.css', './html/css/index.css', (err) => {
    if (err) {
        console.log(err);
        return;
    }
    console.log("移动文件 success")
})

// unlink()
fs.unlink('./html/aa/a.js', (err) => {
    if (err) {
        console.log(err);
        return;
    }
    console.log("unlink success")
})

// rmdir(),执行需将上述的所有方法注释掉
fs.rmdir('./html/aa', (err) => {
    if (err) {
        console.log(err);
        return;
    }
    console.log("rmdir success")
})

最终目录结构如下
在这里插入图片描述

6.node中的fs模块功能练习

判断服务器上是否有upload目录,无则创建upload,有则不做操作(图片上传)

  1. 方法一:自己编码
var fs = require('fs');
var path = './upload';
fs.stat(path, (err, data) => {
    if (err) {
        // 无则创建upload
        mkdir(path);
    }else{
        // 有则判断是文件夹还是文件,是文件则创建upload文件夹
        if(data.isDirectory()){
            // 是文件夹
            console.log(`${path}目录存在`)
        }else{
            // 创建upload:如果存在同名的文件则先删除在创建
            if(data.isFile()){
                fs.unlink(path, (err) => {
                    if (err) {
                        console.log(err);
                        return;
                    } else {
                        mkdir(path)
                    }
                })
            }
        }
    }
})
function mkdir(dir){
    fs.mkdir(dir, (err) => {
        if (err) {
            console.log(err);
            return;
        }
        console.log(`${dir}目录创建成功`)
    })
}
  1. 方法二:使用已有的mkdirp依赖模块
  • 安装:cnpm install -S mkdirp
  • 使用:
var mkdirp = require('mkdirp');
// 创建单个目录下的文件夹
mkdirp('./uploads', function (err) {
     if (err) {
         console.error(err)
     }
});
//同时创建目录下的多个文件夹
mkdirp('./uploads/img', function (err) {
    if (err) {
        console.error(err)
    }
});

找出指定文件夹wwwroot下面的所有目录,并以数组的形式输出

  1. 方法一:自己编码
var fs = require('fs');
var dirArr = [];
var path1 = './wwwroot';
fs.readdir(path1, (err, data) => {
    if (err) {
        console.log(err);
        return;
    }
    // 输出的数组为空(循环先执行,在执行fs.stat)

    // for(var i=0;i<data.length;i++){
    //     fs.stat(path1 + '/' + data[i], (err, stats) => {
    //         if(stats.isDirectory()){
    //             dirArr.push(data[i])
    //         }
    //     })
    // }

    // 递归自执行函数
    (function getdir(i){
        if (i == data.length){
            // 执行完成
            console.log(dirArr);
            return;
        }
        fs.stat(path1 + '/' + data[i], (err, stats) => {
            if(stats.isDirectory()){
                dirArr.push(data[i])
            }
            getdir(i+1)
        })
    })(0) // 从0开始执行

})

/*
循环先执行,在执行定时器
    for(var i=0;i<3;i++){
        setTimeout(function(){
            console.log(i)
        },100)
    }
    // 3,3,3
*/

fs里面的所有方法全部为异步方法
2. 方法二:使用node的新特性(8+版本)async和await

  • promise简介
// promise处理异步
   // resolve成功的回调函数
   // reject失败的回调函数
function getData(resolve, reject) {
    setTimeout(function () {
        var name = "zhangsan";
        resolve(name);
    }, 1000)
}
var p = new Promise(getData);
// 获取异步方法中的数据
p.then((data) => {
    console.log(data)
})
  • Async、Await简介
1.async是异步的简写,async用于申明一个异步的function,即async是让方法变成异步。(在方法前边加上async即表示该方法为一个异步方法,在外部调用该异步方法时会默认返回一个promise。)
2.await则是async wait的简写,await用于等待一个异步方法执行完成。(await必须用在async的方法里面)
3.异步方法返回一个字符串
async function test(){
    return 'hello nodejs'
}
console.log(test()); //Promise { 'hello nodejs' }
// async 方法可以返回一个promise也可以返回一个字符串(会将字符串包装成一个promise)
async function main(){
    var data =  await test(); // 获取异步方法里面的数据
    console.log(data)// 此时data不是promise,而是异步方法test里面的返回值
}
main()
4.异步方法返回一个promise
async function test() {
    return new Promise((resolve, reject) => {
        setTimeout(function () {
            var name = "zhangsan";
            resolve(name);
        }, 1000)
    })
}
async function main() {
    var data = await test(); // 获取异步方法里面的数据
    console.log(data) // 此时data是异步方法test里面的返回值
}
main()
  • 功能实现
// 1.判断一个资源是目录还是文件
var fs = require('fs');
async function isDir(path){
    // fs.stat为异步方法,因此需要使用return new Promise解决
    return new Promise((resolve, reject)=>{
        fs.stat(path, (err, stats) => {
            if (err) {
                console.log(err);
                reject(err);
                return;
            }
            if (stats.isDirectory()) {
                resolve(true);
            } else {
                resolve(false);
            }
        })
    })
}
// 2. 获取目录中的所有资源,循环遍历
function main() {
    var path = './wwwroot';
    var dirArr = [];
    fs.readdir(path, async (err, data) => {
        if (err) {
            console.log(err);
            return;
        }
        for(var i=0;i<data.length;i++){
            if (await isDir(path + '/' + data[i])) {
                dirArr.push(data[i])
            }
        }
        console.log(dirArr)
    })
}
main()

7.node中的流

以流的方式读取文件和写入文件

  • 以流的方式读取文件:fs.createReadStream
var fs = require('fs');
//  以流的方式读取文件:fs.createReadStream
// 1. 创建读取流
// fs.createReadStream(要读取数据的文件路径)
var ReadStream = fs.createReadStream('./test.txt');
// 2. 监听读取的状态ReadStream.on,若监听到data则表示读取到了数据,
    // 以流的形式读取时是分段读取的,可记录其读取的次数
var count = 0;
var str='';
ReadStream.on('data',(data)=>{
    count ++
    str+=data 
})
    // 读取结束
ReadStream.on('end',()=>{
    console.log(str) 
    console.log(count)
})
    // 读取出错
ReadStream.on('error',(err)=>{
    console.log(err);
})
  • 以流的方式写入文件:fs.createWriteStream
var fs = require('fs');
//  以流的方式写入文件:fs.createWriteStream
var str="";
for(var i=0;i<500;i++){
    str += '我是用来测试写入流的数据。\n'
}
// 1. 创建写入流
// fs.createWriteStream(要写入内容的文件路径)
var WriteStream = fs.createWriteStream('./output.txt')
// 2. 写入数据(写入时是分段写入的)
WriteStream.write(str);
// 3. 标记写入完成(标记文件末尾)
WriteStream.end();
// 4. 监听写入流完成
WriteStream.on('finish',()=>{
    console.log("写入完成")
})

管道流:从一个流中读取数据并写入到另一个流中(复制大文件且可以进行重命名)

var fs = require('fs');
// 管道流:从一个流中读取数据并写入到另一个流中(复制大文件且可以进行重命名)
// 1. 创建读取流
var ReadStream = fs.createReadStream("./test.txt")
// 2. 创建写入流
var WriteStream = fs.createWriteStream("./test1.txt")
// 3. 通过管道pipe写入数据
ReadStream.pipe(WriteStream)

8. 利用http、url、path、fs模块创建一个web服务器

web服务器一般指网站服务器,是指驻留于因特网上某种类型计算机的程序,可以向浏览器等web客户端提供文档,也可以放置网站文件让全世界浏览,可以放置数据文件让全世界下载,目前最主流的三个web服务器是Apache、nginx、iis

// 1. 引入http模块,创建服务
var http = require('http');
var fs = require('fs');
var path = require('path');
var url = require('url');
    // 方法一:根据后缀名手动设置响应头
// var mime = require("./model/extName")
    // 方法二:使用readFileSync同步读取文件数据,再根据文件后缀设置响应头
// var mime = require("./model/getExtNameFromFile")
    // 方法三:使用回调函数读取文件数据,再根据文件后缀设置响应头
// var mime2 = require("./model/getExtNameFromFileCallBack")
    // 方法四:
var events = require("events");
var EventEmitter = new events.EventEmitter();
var mime3 = require("./model/getExtNameFromFileEvents")
http.createServer(function (request, response) {

  // 2. 获取页面的地址并过滤get的请求值
  var pathname = url.parse(request.url).pathname;

  // 3. 重置空请求、过滤无效的请求
  if(pathname == "/"){
    //默认加载首页
      pathname = "/index.html"
  }
  if (pathname != "/favicon.ico"){

    // 4. 引入fs模块,获取文件操作
    fs.readFile('static/' + pathname,(err,data)=>{
        if(err){
            console.log("404")
            fs.readFile("./static/404.html", (err, data404) => {
                 if (err) {
                     console.log(err)
                     return;
                 }
                response.writeHead(404, {
                    'Content-Type': 'text/html;charset="utf-8"'
                });
                response.write(data404);
                response.end();
            })
        }else{
            // 5. 引入path模块,根据文件后缀名设置响应头
              // 获取文件的后缀名
            var extName = path.extname(pathname)
              // 根据文件后缀名设置响应头
// 方法一、方法二
            // var ExtNameResult = mime.getExtName(extName);
            // response.writeHead(200, {
            //     'Content-Type': `${ExtNameResult};charset="utf-8"`
            // });
            // response.write(data);
            // response.end();
// 方法三
            // mime2.getExtName(extName, function (result) {
            //   response.writeHead(200, {
            //     'Content-Type': `${result};charset="utf-8"`
            //   });
            //   response.write(data);
            //   response.end();
            // })
// 方法四
            mime3.getExtName(extName);
            // 监听广播
            EventEmitter.on("on_data", function (result) {
              response.writeHead(200, {
                'Content-Type': `${result};charset="utf-8"`
              });
            })
            response.end(data);
        }
    })
  }
}).listen(8081);
console.log('Server running at http://127.0.0.1:8081/');

各类方法的js文件如下
方法一:

// extName.js
exports.getExtName = function (extName) {
    switch (extName){
        case ".html":
            return "text/html";
        case ".css":
            return "text/css";
        case ".js":
            return "text/javascript";
        default:
            return "text/html";
    }
}

方法二:

// getExtNameFromFile.js
var fs = require("fs");
exports.getExtName = function (extName) {
    var data = fs.readFileSync("./index.json");
    var Mimes=JSON.parse(data.toString())
    return Mimes[extName] || "text/html";
}
// index.json文件内容见:https://blog.csdn.net/weixin_39893889/article/details/103912469

方法三:

// getExtNameFromFileCallBack.js
// 根据后缀名设置相应头,并根据回调函数读取数据
var fs = require("fs");
exports.getExtName = function (extName ,callback) {
    // 根据回调函数读取数据
    fs.readFile("./index.json", (err, data) => {
         if (err) {
             console.log(err)
             return;
         }
         var Mime = JSON.parse(data.toString())
         var result = Mime[extName] || "text/html";
         callback(result);
     })
}

方法四:

// getExtNameFromFileEvents.js
var fs = require("fs");
var events = require("events");
var EventEmitter = new events.EventEmitter();
exports.getExtName = function (extName) {
    // 根据回调函数读取数据
    fs.readFile("./index.json", (err, data) => {
        if (err) {
            console.log(err)
            return;
        }
        var Mime = JSON.parse(data.toString())
        var result = Mime[extName] || "text/html";
        // 触发广播
        EventEmitter.emit("on_data", result);
    })
}

目录结构如下
在这里插入图片描述

9. 非阻塞i/o、异步,事件驱动

在java、 PHP或者.net等服务器端语言中, 会为每一个客户端连接创建一个新的线程,而每个线程需要耗费大约2MB内存。也就是说理论上, 一个8GB内存的服务器可以同时连接的最大用户数为4000个左右, 要让Web应用程序支持更多的用户, 就需要增加服务器的数量, 而Web应用程序的硬件成本当然就上升了。
node.js不为每个客户连接创建一个新的线程,而仅仅使用一个线程。当有用户连接了,就触发一个内部事件, 通过非阻塞I/o、事件驱动机制, 让Node程序宏观上也是并行的。
使用node.js,一个8GB内存的服务器, 可以同时处理超过4万户的连接。

非阻塞i/o、异步

//  对异步方法进行封装,并在封装的方法内部获取数值
function getMime() {
    fs.readFile("../static/index.json", (err, data) => {
        if (err) {
            console.log(err)
            return;
        }
        console.log(data.toString())
    })
}
getMime()


//  对异步方法进行封装,并在外部调用时获取数值
function getMime() {
    console.log("第一步执行")
    fs.readFile("../static/index.json", (err, data) => {
        console.log("第三步执行")
        if (err) {
            console.log(err)
            return;
        }
        return data.toString() // 得到一个undefined
    })
    console.log("第二步执行")
}
console.log(getMime())
/**
 * 得到undefined的原因
 * fs.readFile是异步执行的,执行顺序为一、二、三,当程序第二步执行完成后,在执行第三步之前,遇见{}便会返回undefined
 * undefined的解决方法
 * 1. 用回调函数的方法获取数据
 * 2. 用事件驱动的方法获取数据
 */


//  方法一:用回调函数的方法解决undefined
function getMime(callback) {
    fs.readFile("../static/index.json", (err, data) => {
        if (err) {
            console.log(err)
            return;
        }
        callback(data.toString())
    })
    console.log("第二步执行")
}
getMime(function (data) {
    console.log(data)
})


//  方法二:用事件驱动的方法获取数据
var events = require("events")
var EventEmitter = new events.EventEmitter();

function getMime() {
    fs.readFile("../static/index.json", (err, data) => {
        if (err) {
            console.log(err)
            return;
        }
        // 触发广播
        EventEmitter.emit("on_data", data.toString());
    })
    console.log("第二步执行")
}
// 执行方法
getMime()
// 监听广播
EventEmitter.on("on_data",function(mime){
    console.log("EventEmitter-on_data:", mime)
})

事件驱动

nodejs有多个内置的事件,可通过引入events模块,并通过实例化EventEmitter类来绑定和监听事件.
nodejs是单进程单线程的应用程序,但是通过事件和回调支持并发,所有性能非常高.
nodejs的每一个API都是异步的,并作为一个独立线程运行,使用异步函数调用,并处理并发。
// 第一步:引入events模块
var events = require("events")
console.log(events)
// 第二步:实例化EventEmitter
var EventEmitter = new events.EventEmitter();
// 第三步:通过EventEmitter发送与接受广播
// 监听to_parent事件(广播)
EventEmitter.on("to_parent", function (data) {
    console.log("接受到了这个广播:", data)
    EventEmitter.emit("to_mime", "mime发送的数据")
})
EventEmitter.on("to_mime", function (data) {
    console.log("接受到了这个广播:", data)
})
// 触发to_parent广播
setTimeout(() => {
    EventEmitter.emit("to_parent", "发送的数据")
}, 1000)

10. 路由、ejs模板引擎

针对不同请求的URL,处理不同的业务逻辑

http.createServer(function (request, response) {
    console.log(request.url)
    var pathName = url.parse(request.url).pathname;
    if (pathName == "/login") {
        response.end("login");
    } else if (pathName == "/register") {
        response.end("register");
    } else if (pathName == "/order") {
        response.end("order");
    } else {
        response.end("index");
    }
}).listen(8081);
console.log('Server running at http://127.0.0.1:8081/');

ejs模板引擎:后台模板,可以将数据库和文件读取的数据显示到html页面上,是一个第三方模块

  1. 安装ejs
cnpm install -S ejs
  1. 使用ejs
http.createServer(function (request, response) {
    response.writeHead(200, {
        'Content-Type': 'text/html;charset="utf-8"'
    });
    var pathName = url.parse(request.url).pathname;
    if (pathName == "/login") {
        // 需要将数据库中的数据渲染到模板上
        ejs.renderFile("./view/login.ejs", [], function(err, str){          
            response.end(str);
        });
    } else {
        var data = "我是后台数据"
        ejs.renderFile("./view/other.ejs", {msg:data}, function(err, str){          
            response.end(str);
        });
    }
}).listen(8081);
console.log('Server running at http://127.0.0.1:8081/');


// ejs模板other.ejs
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    other ejs 模板
    <h2><%= msg %></h2>
</body>
</html>

11. get,post

get:从指定的资源请求数据(一般用于获取数据)

  1. 通过request.method可以获得请求方式(get、post)
  2. 通过fs.appendFile将数据写入文件。

post:向指定的资源提交要被处理的数据(一般用于提交数据)

// 获取get或post的请求数据,并将post的数据写入文件
http.createServer(function (request, response) {
    response.writeHead(200, {
        'Content-Type': 'text/html;charset="utf-8"'
    });
    var pathName = url.parse(request.url).pathname;
    var method = request.method.toLowerCase();
    if (pathName == "/login") {
        // 显示登录页面
        ejs.renderFile("./view/login.ejs", [], function(err, str){          
            response.end(str);
        });
    } else if (pathName == "/dologin") {
        // 执行登录操作
        // request.method获取请求方式
        if(method == "get"){
            // get获取数据
            console.log(url.parse(request.url,true).query)
            response.end("dologin get");
        }else if(method == "post"){
            // post获取数据
            var postStr = '';
            request.on("data",function(chunk){
                postStr+=chunk;
            })
            request.on("end",function(chunk){
                // response.end(postStr);
                fs.appendFile("login.txt", postStr, (err) => {
                    if (err) {
                        console.log(err);
                        return;
                    }
                    console.log("写入数据 success")
                })
                response.end("<script>alert('登录成功');history.back()</script>")
            })
        }
    } else {
        ejs.renderFile("./view/index.ejs", [], function(err, str){          
            response.end(str);
        });
    }
}).listen(8081);
console.log('Server running at http://127.0.0.1:8081/');



// login.ejs
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <div>
        <h2>登录</h2>
        <form action="/dologin" method="post">
            <input type="text" name="user" /><br/>
            <input type="password" name="pwd" /><br />
            <input type="submit" value="登录" /><br />
        </form>
    </div>
</body>
</html>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值