原生node知识总结

17 篇文章 0 订阅
9 篇文章 0 订阅

一、nodejs安装

1、普通方式安装

访问官网 ,下载,安装。

运行 node -v 和 npm -v 测试

2、使用nvm安装

nvm:nodejs 版本管理工具,可以切换多个nodejs版本

mac os :使用 brew install nvm

windows: github 中搜索 nvm-windows

nvm使用:

配置淘宝镜像:

nvm root  找到 nvm 安装目录

找到 settings.txt 文件

添加:

node_mirror: https://npm.taobao.org/mirrors/node/
npm_mirror: https://npm.taobao.org/mirrors/npm/
nvm list 查看当前所有的已安装的node版本
nvm install v10.13.0 64 安装置顶版本的nodejs
nvm use 10.13.0 64 切换到指定版本

二、nodejs 和 javascript区别

ECMAScript是语法规范

nodejs = ES + nodejs API

1、ECMAScript

定义了语法,写JavaScript 和 nodejs都必须遵守

变量定义 ,循环,判断,函数

原型和原型链,作用域和闭包、异步

文档:http://es6.ruanyifeng.com/

2、javascript

  • 使用ECMAScript语法规范,外加Web API ,缺一不可

  • DOM操作,BOM操作,事件绑定,AJAX等

  • 两者结合,即可完成浏览器端的任何操作

3、nodejs

文档:http://nodejs.cn/api/

  • 使用ECMAScript语法规范,外加nodejs API ,缺一不可

  • 处理http、处理文件等

  • 两者结合,即可完成server端的任何操作

三、npm

1、npm引入依赖的版本

"5.0.3",
"~5.0.3",
"^5.0.3"

“5.0.3”表示安装指定的5.0.3版本,“~5.0.3”表示安装5.0.X中最新的版本,“^5.0.3”表示安装5.X.X中最新的版本

四、yarn

npm i yarn -g 全局安装yarn
npm install -g yrm   yarn镜像源控制
yrm ls
yrm use taobao
yrm test taobao 
yrm current
yarn config get registry
yarn config set registry https://registry.npm.taobao.org -g
yarn config set electron_mirror https://npm.taobao.org/mirrors/electron/ -g
yarn cache clean
yarn
  • 并行安装:无论 npm 还是 Yarn 在执行包的安装时,都会执行一系列任务。npm 是按照队列执行每个 package,也就是说必须要等到当前 package 安装完成之后,才能继续后面的安装。而 Yarn 是同步执行所有任务,提高了性能。

  • 离线模式:如果之前已经安装过一个软件包,用Yarn再次安装时之间从缓存中获取,就不用像npm那样再从网络下载了。

  • 安装版本统一:为了防止拉取到不同的版本,Yarn 有一个锁定文件 (lock file) 记录了被确切安装上的模块的版本号。每次只要新增了一个模块,Yarn 就会创建(或更新)yarn.lock 这个文件。这么做就保证了,每一次拉取同一个项目依赖时,使用的都是一样的模块版本。npm 其实也有办法实现处处使用相同版本的 packages,但需要开发者执行 npm shrinkwrap 命令。这个命令将会生成一个锁定文件,在执行 npm install 的时候,该锁定文件会先被读取,和 Yarn 读取 yarn.lock 文件一个道理。npm 和 Yarn 两者的不同之处在于,Yarn 默认会生成这样的锁定文件,而 npm 要通过 shrinkwrap 命令生成 npm-shrinkwrap.json 文件,只有当这个文件存在的时候,packages 版本信息才会被记录和更新。
  • 更简洁的输出:npm 的输出信息比较冗长。在执行 npm install 的时候,命令行里会不断地打印出所有被安装上的依赖。相比之下,Yarn 简洁太多:默认情况下,结合了 emoji直观且直接地打印出必要的信息,也提供了一些命令供开发者查询额外的安装信息。
  • **多注册来源处理:**所有的依赖包,不管他被不同的库间接关联引用多少次,安装这个包时,只会从一个注册来源去装,要么是 npm 要么是 bower, 防止出现混乱不一致。
  • 更好的语义化: yarn改变了一些npm命令的名称,比如 yarn add/remove,感觉上比 npm 原本的 install/uninstall 要更清晰。

Yarn和npm命令对比:

npmyarn
npm installyarn
npm install react --saveyarn add react
npm uninstall react --saveyarn remove react
npm install react --save-devyarn add react --dev
npm update --saveyarn upgrade

五、commonjs

nodejs里默认包含commonjs

1、单个导入导出:

a.js

function add(a, b) {
    return a + b
}

module.exports = add

b.js

const add = require('./a')

const sum = add(10, 20)

console.log(sum)

运行:

node b

2、多个导入导出:

a.js

function add(a, b) {
    return a + b
}

function mul(a, b) {
    return a * b
}

module.exports = {add, mul}

b.js

//es6 解构
const {add, mul} = require('./a')

const a = add(10, 20)
const b = mul(100, 200)

console.log(a)
console.log(b);

运行:

node b

但是:如果导出多个的时候是这样导出的:

module.exports = add
module.exports = mul

第二个会覆盖第一个导出

3、另外一种导出

function add(a, b) {
    return a + b
}

function mul(a, b) {
    return a * b
}


exports.add=add
exports.mul=mul

console.log(module.exports)

打印

{ add: [Function: add], mul: [Function: mul] }

说明 exports 和 module.exports是一块内存空间,但是

exports = {'add': add, 'mul': mul}

或者

exports = { add,  mul}

都不可以

4、引入的测试

a.js

function add(a, b) {
    return a + b
}

function mul(a, b) {
    return a * b
}


exports.add = add
exports.mul = mul

b.js 直接引入对象

const a = require('./a')

console.log(a.add(5, 6))
console.log(a.mul(5, 6))

这样是可以的

5、es6语法ecport和default

export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。

// 写法一
export var m = 1;

// 写法二
var m = 1;
export {m};

// 写法三
var n = 1;
export {n as m};

export default 命令

使用export default命令,为模块指定默认输出。

// export-default.js
export default function () {
  console.log('foo');
}

6、引入npm包:

npm i lodash --save --registry=https://registry.npm.taobao.org
const _ = require('lodash')
let arr = _.concat([1, 2], 3)
console.log(arr)
const {concat} = require('lodash')
let arr = concat([1, 2], 3)
console.log(arr)

六、知识节点

1、开发流程

定目标>定需求>定ui设计>定技术方案>开发>联条>测试>上线>评估

2、pv和uv

pv访问量(Page View),即页面访问量,每打开一次页面PV计数+1,刷新页面也是。

UV访问数(Unique Visitor)指独立访客访问数,一台电脑终端为一个访客。

PV(访问量):PV反映的是浏览某网站的页面数,所以每刷新一次也算一次。就是说PV与来访者的数量成正比,但PV并不是页面的来访者数量,而是网站被访问的页面数量。

UV(独立访客):可以理解成访问某网站的电脑的数量。网站判断来访电脑的身份是通过来访电脑的cookies实现的。如果更换了IP后但不清除cookies,再访问相同网站,该网站的统计中UV数是不变的。

3、访问url的过程

  • dns解析,找到ip地址
  • 三次握手,建立tcp连接
  • 发送http请求,数据传输
  • server接收到http请求,处理,并返回
  • 客户端收到返回数据,处理数据(渲染页面,执行js)
  • TCP四次挥手,断开连接

三次握手即可建立TCP连接

1、第一次握手:客户端发送syn包(seq=x)到服务器,并进入SYN_SEND状态,等待服务器确认;

2、第二次握手:服务器收到syn包,必须确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(seq=y),即SYN+ACK包,此时服务器进入SYN_RECV状态;

3、第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。

握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。理想状态下,TCP连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP 连接都将被一直保持下去。

为什么需要三次握手呢?

相互确认!
为了保证服务端能收接受到客户端的信息 并能做出正确的应答而进行前两次(第一次和第二次)握手,为了保证客户端能够接收到服务端的信息 并能做出正确的应答而进行后两次(第二次和第三次)握手。

买手机的时候试通话功能的时候:

1老机(客户端)打给新机(服务器) : 喂 , 听到了吗 ?

2新机回复老机 : 听到了 , 你听到了吗 ?

3老机 : 听到了听到了 …

四次挥手即可断开TCP连接

1、第一次挥手:主动关闭方发送一个FIN,用来关闭主动方到被动关闭方的数据传送,也就是主动关闭方告诉被动关闭方:我已经不会再给你发数据了(当然,在fin包之前发送出去的数据,如果没有收到对应的ack确认报文,主动关闭方依然会重发这些数据),但此时主动关闭方还可以接受数据。

2、第二次挥手:被动关闭方收到FIN包后,发送一个ACK给对方,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号)。

3、第三次挥手:被动关闭方发送一个FIN,用来关闭被动关闭方到主动关闭方的数据传送,也就是告诉主动关闭方,我的数据也发送完了,不会再给你发数据了。

4、第四次挥手:主动关闭方收到FIN后,发送一个ACK给被动关闭方,确认序号为收到序号+1,至此,完成四次挥手。

白话文:

1、第一次挥手,浏览器对服务器说:“煞笔,我不再给你发数据啦,但可以接受数据。”

2、第二次挥手,服务器对浏览器说:“骚货,我知道啦!”

3、第三次挥手,服务器对浏览器说:“骚货,我也不再给你发数据啦!”

4、第四次挥手,浏览器对服务器说:“煞笔,我知道啦!”

七、node-http搭建后台服务

const http = require('http')
const querystring = require('querystring')

module.exports = function () {
    const server = http.createServer((req, res) => {
        const method = req.method
        const url = req.url
        const path = url.split("?")[0]
        const query = querystring.parse(url.split("?")[1])

        //设置返回格式为json
        res.setHeader('Content-Type', 'application/json')
        //返回的数据
        const resData = {method, url, path, query}
        if (method === "GET") {
            res.end(JSON.stringify(resData))
        }

        if (method === "POST") {
            let postData = ''
            req.on('data', chunk => postData += chunk.toString())
            req.on('end', () => {
                resData.postData = postData
                //返回
                res.end(JSON.stringify(resData))
            })
        }

    })

    server.listen(8000)
    console.log('OK')
}

八、node搭建博客后台服务

1、cross-env

  • 讲解

    它是运行跨平台设置和使用环境变量(Node中的环境变量)的脚本。

    我们在自定义配置环境变量的时候,由于在不同的环境下,配置方式也是不同的。

    • 例如在window和linux下配置环境变量。
    #node中常用的到的环境变量是NODE_ENV,首先查看是否存在 
    set NODE_ENV 
    
    #如果不存在则添加环境变量 
    set NODE_ENV=production 
    
    #环境变量追加值 set 变量名=%变量名%;变量内容 
    set path=%path%;C:\web;C:\Tools 
    
    #某些时候需要删除环境变量 
    set NODE_ENV=
    
    • 在linux下配置
    #node中常用的到的环境变量是NODE_ENV,首先查看是否存在
    echo $NODE_ENV
    
    #如果不存在则添加环境变量
    export NODE_ENV=production
    
    #环境变量追加值
    export path=$path:/home/download:/usr/local/
    
    #某些时候需要删除环境变量
    unset NODE_ENV
    
    #某些时候需要显示所有的环境变量
    env
    
    • cross-env的作用是什么?
      当我们使用 NODE_ENV = production 来设置环境变量的时候,大多数windows命令会提示将会阻塞或者异常,或者,windows不支持NODE_ENV=development的这样的设置方式,会报错。因此 cross-env 出现了。我们就可以使用 cross-env命令,这样我们就不必担心平台设置或使用环境变量了。也就是说 cross-env 能够提供一个设置环境变量的scripts,这样我们就能够以unix方式设置环境变量,然而在windows上也能够兼容的。
  • 安装

    npm install --save-dev cross-env
    
  • package.json中使用

    "scripts": {
        "dev": "cross-env NODE_ENV=dev node ./bin/www",
        "prod": "cross-env NODE_ENV=prod node ./bin/www"
    }
    
  • 获取环境变量

    cosnt env = process.env.NODE_ENV
    

2、nodemon

nodemon用来监视node.js应用程序中的任何更改并自动重启服务,非常适合用在开发环境中。

nodemon将监视启动目录中的文件,如果有任何文件更改,nodemon将自动重新启动node应用程序。

nodemon不需要对代码或开发方式进行任何更改。nodemon只是简单的包装你的node应用程序,并监控任何已经改变的文件。nodemon只是node的替换包,只是在运行脚本时将其替换命令行上的node。

安装在全局。

cnpm install -g  nodemon

九、nodejs操作 mysql

1、安装mysql插件

yarn add mysql

2、demo

const mysql = require('mysql')

//创建连接对象
const con = mysql.createConnection({
    host: '192.168.100.230',
    user: 'root',
    password: '123456',
    port: '3306',
    database: 'blog'
})

//开始执行
con.connect()

//执行sql语句
const sql = 'select * from user'
con.query(sql, (err, result) => {
    if (err) {
        console.error(err)
        return
    } else {
        console.log(result)
    }
})

const sql2 = "update user set name = 'c' where name = 'b'  "
con.query(sql2, (err, result) => {
    if (err) {
        console.error(err)
        return
    } else {
        console.log(result)
    }
})

//关闭连接
con.end()

3、封装

配置:

'use strict';
const env = process.env.NODE_ENV; //环境参数
//配置
let MYSQL_CONF

if (env === 'dev') {
    MYSQL_CONF = {
        host: '192.168.100.230',
        user: 'root',
        password: '123456',
        port: '3306',
        database: 'blog'
    }
}

if (env === 'production') {
    MYSQL_CONF = {
        host: 'online' +
            '',
        user: 'root',
        password: '123456',
        port: '3306',
        database: 'blog'
    }
}

module.exports = {
    MYSQL_CONF
}

工具

const mysql = require('mysql')
const {MYSQL_CONF} = require('../conf/db')

//创建连接对象
const con = mysql.createConnection(MYSQL_CONF);

//开始连接
con.connect();

//统一执行sql的函数
function exec(sql) {
    return new Promise((resolve, reject) => {
        con.query(sql, (err, result) => {
            if (err) {
                console.error(err);
                reject(err)
            } else {
                console.log(result);
                resolve(result)
            }
        });
    })
}

module.exports = {
    exec,
    escape: mysql.escape// 预防sql注入
}

十、cookie

1、存储在浏览器中的一段字符串(最大5kb)

2、跨域不共享

3、格式如 k1=v1;k2=v2;因此可以存储格式化数据

4、每次发送http请求,会将请求与的cookie一起发送给server

5、server可以修改cookie并返回给浏览器

6、浏览器中也可以通过js修改cookie(有限制)

//js查看cookie
document.cookie
//js累加cookie
document.cookie='k1=v1;'

1、server端nodejs操作cookie

//解析cookie
req.cookie = {}
let cookieStr = req.headers.cookie || "";
cookieStr.split(';').forEach(item => {
    if (!item) {
        return
    }
    let arr = item.split('=');
    let key = arr[0].trim();
    let value = arr[1].trim();
    req.cookie[key] = value;
})

//设置cookie
const getCookieExpires = () => {
    console.log(new Date(Date.now() + 24 * 60 * 60 * 1000).toGMTString())
    return new Date(Date.now() + 24 * 60 * 60 * 1000).toGMTString()
}

res.setHeader('Set-Cookie', `username=${result.username};path=/; httpOnly;expires=${getCookieExpires()}`)

httpOnly:只能通过http修改cookie,用 js修改cookie(document.cookie='username=cc') 只会添加一条新cookie而不是修改原来的cookie,然后带到后端之后只会接收到原来的cookie;但是可以在浏览器的application中直接手动修改原来的cookie


十一、nodejs操作redis

1、安装

yarn add redis

2、demo

const redis = require('redis')

//创建客户端
let redisClient = redis.createClient(6379, '192.168.100.142');
redisClient.on('error', err => {
    console.log(err)
})

//测试
redisClient.set('my', 'ha', redis.print)
redisClient.get('my', (err, val) => {
    if (err) {
        console.log(err)
        return
    }
    console.log(val);

    //退出
    redisClient.quit();
})

3、封装

配置:

'use strict';
const env = process.env.NODE_ENV; //环境参数
//配置
let MYSQL_CONF;
let REDIS_CONF;

if (env === 'dev') {
    MYSQL_CONF = {
        host: '192.168.100.230',
        user: 'root',
        password: '123456',
        port: '3306',
        database: 'blog'
    }

    REDIS_CONF = {
        port: 6379,
        host: '192.168.100.230'
    }
}

if (env === 'production') {
    MYSQL_CONF = {
        host: 'online' +
            '',
        user: 'root',
        password: '123456',
        port: '3306',
        database: 'blog'
    }
    REDIS_CONF = {
        port: 6379,
        host: '192.168.100.230'
    }
}

module.exports = {
    MYSQL_CONF
}

工具redis.js

const redis = require('redis')
const {REDIS_CONF} = require('../conf/db')

//创建客户端
let redisClient = redis.createClient(REDIS_CONF.port, REDIS_CONF.host);
redisClient.on('error', err => {
    console.log(err)
})

function set(key, value) {
    if (typeof value === 'object') {
        value = JSON.stringify(value)
    }
    redisClient.set(key, value, redis.print)
}

function get(key) {
    return new Promise((resolve, reject) => {
        redisClient.get(key, (err, val) => {
            if (err) {
                reject(err)
                return
            }
            console.log(val);
            if (val == null) {
                resolve(null)
                return;
            }
            try {
                resolve(JSON.parse(val))
            } catch (e) {
                resolve(val)
            }
        })
    })
}

module.exports = {get, set}

十二、nodejs操作文件

1、path.join和path.resolve的区别

path.join() 方法使用平台特定的分隔符把全部给定的 path 片段连接到一起,并规范化生成的路径。
长度为零的 path 片段会被忽略。 如果连接后的路径字符串是一个长度为零的字符串,则返回 '.',表示当前工作目录。

“平台特定的分隔符”:

  windows下文件路径分隔符使用的是"\"

  Linux下文件路径分隔符使用的是"/"

“path片段”:即是说,该方法接收的是多个路径的部分或全部,然后简单将其拼接。

“规范化”:顾名思义,如果你给出的路径片段中任一路径片段不是一个字符串,则抛出TypeError。

我们来举个例子:
path.join('/foo', 'bar', 'baz/asdf', 'quux', '..');
// 返回: '/foo/bar/baz/asdf'
这里需要注意:如果路径中出现"..",那么它前面的路径片段将被丢失。
path.resolve() 方法会把一个路径或路径片段的序列解析为一个绝对路径。

给定的路径的序列是从右往左被处理的,后面每个 path 被依次解析,直到构造完成一个绝对路径。 例如,给定的路径片段的序列为:/foo、/bar、baz,则调用 path.resolve('/foo', '/bar', 'baz') 会返回 /bar/baz。

如果处理完全部给定的 path 片段后还未生成一个绝对路径,则当前工作目录会被用上。

生成的路径是规范化后的,且末尾的斜杠会被删除,除非路径被解析为根目录。

长度为零的 path 片段会被忽略。

如果没有传入 path 片段,则 path.resolve() 会返回当前工作目录的绝对路径。

path.resolve('/foo/bar', './baz');
// 返回: '/foo/bar/baz'

path.resolve('/foo/bar', '/tmp/file/');
// 返回: '/tmp/file'

path.resolve('wwwroot', 'static_files/png/', '../gif/image.gif');
// 如果当前工作目录为 /home/myself/node,
// 则返回 '/home/myself/node/wwwroot/static_files/gif/image.gif'

总结一下:path.join只是简单的将路径片段进行拼接,并规范化生成一个路径,而path.resolve则一定会生成一个绝对路径,相当于执行cd操作。

2、file 操作 demo

const fs = require('fs')
const path = require('path')

const fileName = path.resolve(__dirname, './data.txt')

//1、一次性读取文件内容
function test_read() {
    fs.readFile(fileName, (err, data) => {
        if (err) {
            console.log(err)
        } else {
            console.log(data.toString())
        }
    });
}

//2、一次性写入文件
function test_write() {
    const content = '这是写入的内容\n';
    const opt = {
        flag: 'a' //追加写入 覆盖用'w'
    };
    fs.writeFile(fileName, content, opt, err => {
        if (err) console.log(err)
    })
}

//3、判断文件是否存在
function test_exist() {
    fs.exists(fileName, exists => console.log(exists))
}

test_read();
test_exist();

3、stream 操作 demo

const path = require('path');
const fs = require('fs');

//1、测试标准输入输出
function test_std() {
    process.stdin.pipe(process.stdout)
    const http = require('http');
    const server = http.createServer((req, res) => {
        req.pipe(res)
    })
    server.listen(8000)
}

//2、测试stream复制文件
function test_copy() {
    let aPath = path.resolve(__dirname, 'a.txt');
    let bPath = path.resolve(__dirname, 'b.txt');
    let cc = path.join(__dirname, 'b.txt');

    console.log(aPath)
    console.log(bPath)
    console.log(cc)


    let readStream = fs.createReadStream(aPath);
    let writeStream = fs.createWriteStream(bPath);
    readStream.pipe(writeStream);

    readStream.on('data', chunk => {
        console.log('copy :', chunk.toString())//一点点的读取
    })

    readStream.on('end', () => {
        console.log('copy done')
    })
}

//3、测试stream读取file
function test_read() {
    const http = require('http');
    const server = http.createServer((req, res) => {
        const fileName = path.resolve(__dirname, './data.txt');
        let readStream = fs.createReadStream(fileName);
        readStream.pipe(res)
    })
    server.listen(8000)
}

//4、测试stream创建并写入文件
function test_write() {
    let c = path.resolve(__dirname, 'c.txt');
    let writeStream = fs.createWriteStream(c);
    writeStream.write('还是返回肯定是减肥', error => {
        console.log(error)
    })
}

//5、测试 readline
function test_readline() {
    let filePath = path.resolve(__dirname, 'c.txt');
    const readline = require('readline')
    //创建readStream
    let readStream = fs.createReadStream(filePath);

    //创建readline 对象
    let rl = readline.createInterface({input: readStream});

    //逐行读取
    let count = 0;
    rl.on('line', data => {
        console.log(data)
        count++;
    })

    //监听读取完成
    rl.on('close', args => {
        console.log(`总共有${count}行`)
    })
}

// test_std();
// test_copy();
// test_read();
// test_write();
test_readline();

4、封装

const path = require('path');
const fs = require('fs');


function createWriteStream(fileName) {
    let logPath = path.resolve(__dirname, '../../logs', fileName);
    return fs.createWriteStream(logPath, {flags: 'a'});
}

const accessWs = createWriteStream('access.log');
const errorWs = createWriteStream('error.log');
const eventWs = createWriteStream('event.log');


//写日志
function log(writeStream, logContent) {
    writeStream.write(logContent + '\n')
}

function accessLog(logContent) {
    log(accessWs, logContent);
}

function errorLog(logContent) {
    log(errorWs, logContent);
}

function eventLog(logContent) {
    log(eventWs, logContent);
}

//测试
accessLog(`-- ${Date.now()}`)

module.exports = {accessLog, errorLog, eventLog}

十三、crontab 拆分日志

1、脚本

copy.sh

#!/bin/sh
cd [路径]/myblog-simple/logs
cp access.log $(date +%Y-%m-%d).access.log
echo "" > access.log #清空

2、创建定时任务

crontab -e #创建任务 
0 0 0 * * * sh copy.sh
crontab -l #查看任务

十四、xss攻击防护

1、 安装

yarn add xss 

2、使用

const xss = require('xss');

let content = `<script>window.alert('sd')</script>`;

console.log(xss(content));

结果:

&lt;script&gt;window.alert('sd')&lt;/script&gt;

十五、nodejs 加密数据

const crypto = require('crypto');

//密匙
const SECRET_KEY = 'sadasd_dsads2';

//md5加密
function md5(content) {
    let md5 = crypto.createHash('md5');
    return md5.update(content).digest('hex');
}


//加密函数
function genPassword(password) {
    const str = `password=${password}&key=${SECRET_KEY}`; //就是md5加盐
    return md5(str);
}


//测试
console.log(genPassword('12324'));

十六、总结

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值