Nodejs 学习

文件读取

  1. 导入 fs模块
const fs = require('fs')
  1. 调用 fs.readFile 方法读取文件

参数:文件名, 编码格式,回调函数(err, data)

  • 如果读取成功,err 为 null
  • 如果读取失败, err 为 错误对象, data 为 undefined
fs.readFile('./files/test.txt', 'utf8', function(err, data){
    if(err) throw err;
    console.log(data);
})

Promise 封装文件读取 fs 模块

const fs = require('fs');
let p = new Promise((resolve, reject) => {
    fs.readFile('./files/test.txt', 'utf8', (err, data) => {
        if(err) reject(err);
        resolve(data);
    });
});

p.then(value => {
    console.log(value.toString());
}, reason =>{
    console.log(reason);
})

文件写入

  1. 导入 fs 模块

  2. 调用 fs.writeFile() 方法

参数:写入文件名,写入的内容,回调函数

  • 如果写入成功,err 为 null
  • 写入失败,err 为错误对象
fa.writeFile('./files/test.txt', 'write data test', function(err){
    if(err) return err.message;
    console.log('write success!')
})

路径问题和文件名与后缀

__dirname 表示当前文件所处的目录

引入 path 模块

const path = require('path')

path.join() 可以拼接路径

结合使用:../ 会抵消前面的路径

path.join(__dirname, './files/1.txt', ...,...)


获取文件名:

const fullName = path.basename('/a/b/c/d/index.html')
// fullName = 'index.html'

去掉后缀:

const nameWithoutExt = path.basename('/a/b/c/d/index.html', '.html')
// nameWithoutExt = 'index'

获取文件后缀:

const fileExt = path.extname('/a/b/c/d/index.html');
// fileExt = '.html'

http 模块创建服务器

使用 http 模块的 createServer() 函数创建 web 服务器实例

// 1.导入http模块
const http = require('http');
// 2. 创建 web 服务器实例
const server = http.createServer()
// 3. 为服务器绑定 request 事件,监听客户端请求
server.on('request', function(req, res){
          console.log('Someone visit my web server!')
          });
// 4. 启动服务器
server.listen(8080, function(){
    console.log('server running at http://127.0.0.1:8080');
})

req 请求对象

req 是客户端的请求对象,包含了与客户端相关的数据和属性

req.url 是客户端请求的地址

req.method 是客户端请求的 method 类型

res.end(数据) 方法,向客户端响应数据

const http = require('http');
const server = http.createServer();
server.on('request', function(req, res) => {
		const url = req.url;
         const method = req.method;
         const str = `你的请求 url 是${url}, 请求的类型是:${method}`;
		res.end(str);
});
server.listen(8080, funtion(){
	console.log('server running at http://127.0.0.1:8080');
})

在这里插入图片描述
中文乱码

设置响应头:

server.on('request', (req, res) => {
  // 定义一个字符串,包含中文的内容
  const str = `您请求的 URL 地址是 ${req.url},请求的 method 类型为 ${req.method}`
  //调用 res.setHeader() 方法,设置 Content-Type 响应头,解决中文乱码的问题
  res.setHeader('Content-Type', 'text/html; charset=utf-8')
  // res.end() 将内容响应给客户端
  res.end(str)
})

在这里插入图片描述

根据不同的 url 响应不同的 html 内容

req 中获取请求地址的 url,判断 url 的内容,给 content 赋值不同的内容,最后由 res.end 响应给客户端

const http = require('http');
const server = http.createServer()

server.on('request', (req, res) => {
    // 1. 获取请求地址的 url
    const url = req.url;
    // 2. 设置默认响应内容 
    let content = '<h1>404 Not Found</h1>';
    // 3.判断用户请求是否为 / 或 /index.html页面
    if(url === '/' || url === '/index.html'){
        content = '<h1> 首页 </h1>';
    }else if(url === '/about.html'){
        // 4. 判断是否为 about 页面
        content = '<h1>关于页面</h1>';
    }
    // 5 设置响应头,防止中文乱码
    res.setHeader('Content-Type', 'text/html; charset=utf-8');
    // 6. 响应内容
    res.end(content)
})
// 启动服务器
server.listen(8080, ()=>{
    console.log('server running at http://127.0.0.1')
})

clock时钟的 web 服务器

  1. 导入 http fs path 模块,创建服务器实例 server
const http = require('http');
const fs = require('fs');
const path = require('path');
const server = http.createServer();
  1. 监听 web 服务器的 request 事件

一共有三个资源会被请求:

/clock/index.html

/clock/index.css

/clock/index.js

server.on('request', (req, res)=>{
    
})
  1. 获取请求的 url 地址,根据地址判断请求的资源,并拼接具体的资源存放路径
const url = req.url;
let filePath = '';
if(url === '/'){	// 如果只有 /,需要具体到 index.html
    filePath = path.join(__dirname, '/clock/index.html');
}else{
    filePath = path.join(_dirname, '/clock', url)
}
  1. 根据获取的文件路径读取对应的内容,并响应给客户端
fs.readFile(filePath, 'utf8', (err, data)=>{
    if(err) return res.end('404 not found');
    res.end(data);
})
  1. 启动服务器
server.listen(8080, () => {
    console.log('server running at http://127.0.0.1')
})

完成代码:

const http = require('http');
const fs = require('fs');
const path = require('path');
const server = http.createServer();

server.on('request', (req, res)=>{
    const url = req.url;
    let filePath = '';
    if(url === '/'){	// 如果只有 /,需要具体到 index.html
        filePath = path.join(__dirname, '/clock/index.html');
    }else{
        filePath = path.join(_dirname, '/clock', url)
    }
    fs.readFile(filePath, 'utf8', (err, data)=>{
        if(err) return res.end('404 not found');
        res.end(data);
    })
});

server.listen(8080, () => {
    console.log('server running at http://127.0.0.1')
})

模块化

  • 内置模块(fs, http, path...)
  • 自定义模块
  • 第三方模块(需要下载)

模块作用域

自定义模块中的方法和成员只能在当前模块被访问

优点:防止了全局变量污染的问题

向外共享模块作用域的成员

module :每个自定义模块都有一个 moudle 对象,存储了和当前模块有关的信息

Module{
	id: '.',
    path: '文件存放的绝对路径',
    exports:{} // 向外暴露对象
    parent: null,
    filename: "文件路径+文件名"
	loaded: false,
	children: [],        
	paths: [
        'C:\\User\\RAINSUN\\.....\node_modules',
        ...,
        'C:\\node_modules'
    ]
}

module.exports :用于将模块内的成员共享出去,使用 require()方法导入自定义模块时,得到的就是 module.exports所指向的对象

exports 对象和 module.exports 对象指向同一个对象,如果和 module.exports 一起使用,以 module.exports 为准

module.exports.username = 'xyq';
exports = {
    sex: 'woman',
    age: 22
}

// {username: 'xyq'}
exports.username = 'xyq'
module.exports.sex = 'woman'
// {username: 'xyq', sex: 'woman'}

模块化规范

Nodejs遵循CommonJS的模块化规范,

  • 每个模块内部,module变量代表当前模块
  • module变量是一个变量,他的 exports 属性是对外的解构
  • 加载某个模块就是加载该模块的 module.exports属性。(require()方法用于加载模块)

模块的加载机制

  1. 首先从缓存中加载
    • 模块在第一次加载后会被缓存。所以多次调用 require() 不会导致模块的代码被指向多次
  2. 内置模块的加载优先级最高
  3. 用户的自定义模块
    • 必须以 ./../ 开头的路径标识符。如果没有加node会把它当作内置或第三方模块加载
  4. 第三方模块
    • 尝试从/node_modules 文件夹中加载第三方模块,并一级一级的沿着目录向上寻找
  5. 目录作为模块
    • 在被加载目录下寻找 package.json 文件,寻找 main属性,作为 require的入口
    • 没有 package.json文件或者main入口不存在,则尝试在目录下加载index.js文件
    • 上面都失败了,打印错误消息:模块缺失,Error: Cannot find module ‘xxx’

Express 创建服务器

Express 是基于 http 模块封装出来的,效率更高,用来创建 web 服务器

创建基本的 web 服务器

  • 导入 express 模块
  • 创建 web 服务器 app
  • 启动服务器 app.listen
const express = require('express');
const app = express();
app.listen(8080, () => {
    console.log('express server running at http://127.0.0.1')
})

监听get请求

req 请求对象

res 响应对象

app.get('请求的url', function(req, res){})

监听 post 请求

app.post('请求的url', function(req, res){})

响应内容

res.send(数据)

获取查询参数

客户端使用 ?name=xyq&age=22 这种字符串的形式发送到服务器的参数

可以使用 req.query 对象获取,默认是空对象

app.get('/', (req, res) => {
    console.log(req.query);
})

获取 url 的动态参数

在 url 地址中,通过 :参数名的形式,匹配动态参数值

通过req.params 对象获取,默认是空对象

app.get('/user/:id', (req, res) => {
    console.log(req.params);
})

托管静态资源 express.static

  • 将 public 目录下的资源对外访问,托管多个静态资源就调用多个express.static函数
app.use(express.statice('public'));
  • 挂载路径前缀
app.use('public', express.statice('public'))

路由

在Express 中,路由指客户端的请求和服务器处理函数之间的映射关系

路由分为三部分:请求类型method、请求URL地path,处理函数handler

app.METHOD(PATH, HANDLER)

路由匹配

  • 按照定义的先后顺序进行匹配
  • 请求的类型和请求的URL同时匹配成功,才会调用对应的处理函数

模块化路由

使用 express.Router() 可以将路由模块话管理

  1. 创建路由模块的js文件,在js文件中调用 express.Router() 函数创建路由对象

    const express = require('express');
    const router = express.Router();
    
  2. 往路由对象上挂载路由

    router.get('/user/list', (req, res) => {
        res.send('Get user list');
    })
    router.post('/user/add', (req, res) => {
        res.send('Add new user');
    })
    
  3. 使用 module.exports 向外暴露对象

    module.exports = router
    
  4. 使用app.use() 注册路由模块

    // 导入路由模块
    const userRouter = require('./router/user/js');
    // 使用 app.use  注册路由模块
    app.use(userRouter);
    
  5. 也可以为路由模块添加前缀

    app.use('/api', userRouter);
    

中间件

使用中间件可以对请求进行预处理

next 函数是多个中间件连续调用的关键,表示把流转关系交给下一个中间件

中间件函数的定义

const mw = function(req, res, next){
    console.log('这是一个中间件函数');
    next()
}

中间件的作用

多个中间共享同一个 req, res。

所以我们可以在上游定义的中间件,为req,res添加属性和方法,供下游中间件使用

全局生效的中间件

客户端发起的任何请求,都会触发的中间件,叫做全局生效的中间件

通过 app.use(中间件函数) 定义一个全局生效的中间件

app.use(mw);
app.use(function(req, res, next){
    console.log('这是一个中间件函数');
    next()
})

局部生效的中间件

app.get('/', mw1, (req, res) =>{
    ....
    res.send('Home page')
})

多个局部中间件:参数一个一个,参数数组的形式

app.get('/', mw1, mw2, (req, res) => {
    ....
    res.send("Home page")
})
app.get('/', pmw1, mw2], (req, res) => {
    ....
    res.send("Home page")
})

注意事项

  1. 一定要在路由之前注册中间件
  2. 执行完中间件后,不要忘记调用 next() 函数
  3. 连续调用多个中间件时,多个中间件共享 req, res对象

中间件的分类

  • 应用级别中间件

    • app.use((req, res, next)=>{..,next()})
    • app.get('/', mw1, (req, res)=>{})
    • app.post('/', mw1, (req, res)=>{})
  • 路由级别中间件

    • 绑定到 express.Router()实例上的中间件,叫做路由级别中间件

    • var app = express();
      var router = express.Router();
      
      router.use((req, res, next) => {
          ....
          next()
      });
      app.use('/', router)
      
  • 错误级别中间件

    • 用来捕获项目异常,防止项目崩溃的问题

    • 错误级别中间件必须注册到所有路由之后

    • app.use((err, req, res, next) =>{
          ....
          res.send('Error!')
      });
      
    • 可以发现没有调用next函数,没有必要,这已经是最后的步骤了

  • 内置中间件

    • express.static 用来快速托管静态资源
    • express.json 解析JSON格式的请求体数据
      • 解析 application/json 格式数据
    • express.urlencoded 解析 URL-encoded 格式请求体数据
      • 解析 application.x-www-form-urlencoded
  • 第三方中间件:需要下载配置

    • body-parser,解析请求体数据

自定义中间件解析表单数据

  1. 新建 my-body-parser js文件, 导入 querystring 模块用于解析字符串,定义中间件函数
const qs = require('querystring');

const bodyParser = (req, res, next) => {
    
}
  1. 通过 监听 req 对象的data 事件获取客户端发送给服务器的数据
  • 当数据量大的时候,无法一次发完,客户端会把数据切割,分批发送到服务器
  • data 事件会被多次触发,每触发一次就对收到的数据进行拼接
let str = ''
req.on('data', (datapart) => {
    str += datapart;
})
  1. 当请求体数据接受完毕,会触发 req 的end事件,在end事件中将req解析为对象格式,并挂载到 req的body上,便于下游中间件使用
req.on('end', ()=>{
    console.log(str);
    req.body = qs.parse(str);
    next();
})
  1. 将中间函数暴露出去供其他模块使用
module.exports = bodyParse
  1. 在其他模块导入中间件,并注册中间件
const myBodyParse = require('./my-body-parse');
app.use(myBodyParse);

// 在其他路由上获取
app.post('/user', (req, res) =>{
    res.send(req.body)
})

Express 写接口

  1. 创建基本的服务器
const express = require('express');
const app = express();

app.listen(80, ()=>{
   console.log('run') ;
});
  1. 新建一个文件 创建 api 路由模块
const express = require('express');
const apiRouter = express.Router();
// 绑定路由
...
module.exports = apiRouter;
  1. 导入路由并注册路由模块
const apiRouter = require('./apiRouter.js')
app.use('api', apiRouter);
  1. 编写 get post 接口
apiRouter.get('/get', (req, res) => {
    const query = req.query; // 获取url中的数据
    res.send({	// res.send(data) 响应数据
        status:0,
        msg: 'success',
        data: query
    })
})
apiRouter.post('/post', (req, res) => {
    const body = req.body;	// 获取请求体数据,数据格式为url-encoded,需要配置 express.urlencoded中间件解析
    res.send({
        ....
    })
})

接口的跨域问题

  • 浏览器的同源策略引起的接口调用问题
  • 同源策略:同源是指协议 域名 端口 都相同,它是浏览器遵循的一种约定,是浏览器最核心也是最基本的安全功能。缺少同源策略,浏览器容易受到 XSS CSFR 等攻击
  • 接口调用:XMLHttpRequestFetch 都遵循同源策略

浏览器限制跨域请求一般有两种方式

  • 浏览器限制发送跨域请求
  • 跨域请求可以正常发起,但是返回的结果被浏览器拦截了

一般浏览器都是第二种方式进行跨域请求的限制,那么请求就已经到达了服务器,可能对数据库的数据进行了操作,但是返回的结果被浏览器拦截了,那么我们就获取不到返回的结果。这是一个失败的请求,但是对数据库里的数据产生了影响。

为了防止对数据操作了,但返回的结果被浏览器拦截,导致无法获取的问题。规范要求,这种可能对服务器数据产生操作的 HTTP 方法,浏览器必须使用 OPTIONS 方法发起一个预检请求,从而获知服务器是否支持跨域请求;如果允许,再发送携带数据的真实请求;如果不允许,则阻止发送带数据的请求;

预检请求

前端 | 浅谈preflight request - 简书 (jianshu.com)

HTTP请求包括:简单请求和 需要预检的请求

  1. 简单请求

使用下列方法之一:

  • GET
  • HEAD
  • POST
    • 其中仅当 POST 方法的 Content-Type 值等于下列之一才是简单请求
    • text/plain 文本资源
    • multipart/form-data 表单文件上传
    • application/x-www-form-urlencoded key=value&key 2=value 2,放入 http 请求体的 form-data 字段中保存

简单请求不会触发 CORS 预检请求(OPTIONS)

  1. 需要预检的请求
  • 使用了下面任意 HTTP 方法
    • PUT
    • DELETE
    • CONNECT
    • OPTIONS
    • TRACE
    • PATCH
  • 人为设置了下面集合(对CORS安全的首部字段)之外的首部字段(自己设置了首部字段会发送预检请求)
    • Accept
    • Accept-Language
    • Content-Language
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width
    • Content-Type 不为下面的值
      • text/plain
      • multipart/form-data
      • application/x-www-form-urlencoded

需要预检的请求必须首先用一个 OPTIONS 方法发起一个预检请求到服务器,用来获知服务器是否允许使用该实际请求。

解决跨域问题

CORS

CORS (Cross-Origin Resource Sharing,跨域资源共享),是一种基于 HTTP 头的机制,该机制允许服务器标示除了它自己之外的其他源,使得浏览器可以访问加载资源。

  • 简单请求

当浏览器发现发送的 ajax 请求是简单请求时,会在请求头中携带一个 Origin 字段,指出当前请求属于哪个域(协议+域名+端口)

当服务器允许跨域时,需要在返回的响应头携带对应的信息

Access-Control-Allow-Origin: <origin> |*>

origin 指定了允许访问改资源的外域 URL

在 express 框架中的使用:

const express = require('express');
const app = express();
app.all('/cors-server', (req, res) => {
    res.setHeader("Access-Control-Allow-Origin", "*")
    res.send('hello CORS');
})
app.listen(8000, () => {
    console.log("服务已经启动, 8000 端口监听中....");
})

在服务端5500端口想服务器的8000端口发送 GET 请求

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CORS</title>
    <style>
        #result {
            width: 200px;
            height: 100px;
            border: solid 1px #90b;
        }
    </style>
</head>
<body>
    <button>发送请求</button>
    <div id="result"></div>
    <script>
        const btn = document.querySelector('button');
        btn.onclick = function () {
            //1. 创建对象
            const x = new XMLHttpRequest();
            //2. 初始化设置
            x.open("GET", "http://127.0.0.1:8000/cors-server");
            //3. 发送
            x.send();
            //4. 绑定事件
            x.onreadystatechange = function () {
                if (x.readyState === 4) {
                    if (x.status >= 200 && x.status < 300) {
                        //输出响应体
                        console.log(x.response);
                        document.getElementById('result').innerHTML = x.response;
                    }
                }
            }
        }
    </script>
</body>
</html>
  • 特殊请求

不是简单请求的时候,浏览器会预先增加一次 HTTP 查询,称为 预检请求。用来获知当前网页所在的域名是否在服务器的许可名单中,以及可以使用的 HTTP 请求类型和头信息字段

一个预检请求 OPTIONS 的例子:

OPTIONS /doc HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: https://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type

预检请求中除了 Origin 字段外,还有用来表示请求方式和头信息的两个头

Access-Control-Request-Method: PUT/DELETE...

Access-Control-Request-Headers: 额外的头信息

当服务器收到预检请求后,如果允许跨域,对预检请求进行响应:

HTTP/1.1 204 No Content
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive

Access-Control-Allow-Methods 当客户端希望通过 PUT DELETE 等方式请求服务器资源,则需要在服务器指明实际请求所允许使用的 HTTP 方法

res.setHeader('Access-Control-Allow-Methods', 'POST, GET, DELETE,HEAD');
// res.setHeader('Access-Control-Allow-Methods', '*') // * 号允许所有方法

Access-Control-Allow-Headers 默认情况下 CORS 仅仅支持上述提到的9个CORS 安全首部字段,如果要客户端向服务器发送了额外的请求头信息,则西药在服务端对额外的请求头进行声明

// 多个请求头之间用 , 分割
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-PINGOTHER')

Access-Control-Max-Age 本次许可的有效时长,单位为秒,过期前的 ajax 请求无需再次发送预检请求

JSONP

JSONP原理及实现 - 简书 (jianshu.com)

原理:

利用了 script 标签的 src 没有跨域限制完成的。通过 src 熟悉,请求服务器上的数据,然后服务器返回一个函数的调用,这种方式叫做 jsonp

特点:

  • jsonp 不属于 Ajax 请求,没有使用到 XMLHttpRequest 对象
  • jsonp 仅支持 GET 请求

实现步骤:

  • 创建一个 script 标签设置 src 属性中的回调函数名callback并插入到文档中
  • 服务器获取客户端发送过来的回调函数名
  • 得到需要发送给客户端的数据
  • 将数据和函数拼接成一个函数调用的字符串
  • 将函数调用的字符串响应给客户端的 script 标签解析执行

例子:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>案例</title>
</head>
<body>
    用户名: <input type="text" id="username">
    <p></p>
    <script>
        //获取 input 元素
        const input = document.querySelector('input');
        const p = document.querySelector('p');
        //声明 handle 函数
        function handle(data) {
            input.style.border = "solid 1px #f00";
            //修改 p 标签的提示文本
            p.innerHTML = data.msg;
        }

        //绑定事件
        input.onblur = function () {
            //获取用户的输入值
            let username = this.value;
            //向服务器端发送请求 检测用户名是否存在
            //1. 创建 script 标签
            const script = document.createElement('script');
            //2. 设置标签的 src 属性
            script.src = 'http://127.0.0.1:8000/check-username?callback=handle';
            //3. 将 script 插入到文档中
            document.body.appendChild(script);
        }
    </script>
</body>
</html>
//用户名检测是否存在
app.all('/check-username',(request, response) => {
    // response.send('console.log("hello jsonp")');
    const data = {
        exist: 1,
        msg: '用户名已经存在'
    };
    //将数据转化为字符串
    let str = JSON.stringify(data);
    //返回结果
    response.end(`${request.query.callback}(${str})`);
});

身份认证

session

jwt

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
对于想要学习 Node.js 的人来说,以下是一个常见的学习路线: 1. 入门基础知识:首先了解 JavaScript 的基础知识,包括变量、控制流、函数等等。可以通过在线教程、书籍或视频课程学习。 2. 了解后端开发概念:学习关于服务器端开发的基本概念,例如 HTTP、网络通信、数据库等。 3. 学习 Node.js 基础:通过官方文档或在线教程学习 Node.js 的基础知识,包括安装、模块系统、事件驱动编程等。 4. 掌握核心模块:了解和熟悉 Node.js 的核心模块,例如 fs、http、path 等,以便能够构建简单的服务器应用程序。 5. 学习 Express 框架:Express 是一个流行的 Node.js Web 框架,学习它可以帮助你构建更复杂的 Web 应用程序。可以阅读官方文档或参考教程来学习 Express。 6. 学习数据库集成:了解如何在 Node.js 中使用数据库,例如 MongoDB 或 MySQL。学习数据库连接、CRUD 操作等。 7. 异步编程:深入理解 Node.js 的异步编程模型,包括回调函数、Promise、async/await 等。 8. 学习 RESTful API 设计:了解如何设计和构建符合 RESTful 风格的 API。 9. 深入学习和探索:根据个人兴趣和需求,学习其他 Node.js 相关的技术、工具和框架,例如 WebSocket、GraphQL、Socket.io 等。 记住,这只是一个大致的学习路线,你可以根据自己的兴趣和需求进行调整和扩展。实践对于学习 Node.js 来说非常重要,所以尽量多做一些实际项目来巩固所学知识。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值