Node.js学习笔记

提示:小白学前端:Node笔记


一、Node.js是什么

  1. 有哪些技术可以开发Web后台服务器?
    1.Java;2.PHP;3.Python;4.Ruby;5…Net(Dot Net);6.Node.js
  2. node.js官网是:nodejs.org npm的官网是:www.npmjs.com
  3. Node.js特点
    1. Node.js中的javascript
      • 没有BOM、DOM
      • EcmaScript
      • 为JavaScript提供了一些服务器级别的操作API,例如:文件读写、网络服务构建、网络通信、http服务器
    2. Node.js uses an event-driven,non-blocking I/O model that makes is lightweight and efficient
      • 事件驱动
      • 非阻塞IO模型(异步)
      • 轻量和高效
  4. Google Chrome 的v8引擎是公认解析执行JavaScript代码最快的引擎
  5. node.js 最大的特点是 单线程

二、 基本操作

1. 读写文件

1.读文件

代码如下(示例):

//1.使用require方法加载fs模块
var fs = require('fs')  //fs读取文件的方法模块
//2. 读取文件
//   fs.readFile 用来读取文件的方法之一
//   第一个参数是读取文件的路径;第二个参数是回调函数
//		成功:	data:数据;error:null
//		失败:	data:undefined;error:错误对象
fs.readFile('./data/haomi.txt',function(err,data){
	//读取出来的数据是Buffer类型,16进制的数据、使用toString()方法
	if(!err){console.log(data.toString())}
	else{console.log(err)}
})

2.写文件

代码如下(示例):

//1.使用require方法加载fs模块
var fs = require('fs')  //fs读取文件的方法模块
//2. 读取文件
//   fs.writeFile用来写入文件的方法之一
//   第一个参数是写入文件的路径;第二个参数是写入内容;第三个参数是回调函数
//		成功:	error:null
//		失败:	error:错误对象
fs.writeFile('./data/haomi.txt','我是写入内容',function(err){
	//读取出来的数据是Buffer类型,16进制的数据、使用toString()方法
	if(!err){console.log('写入成功')}
	else{console.log(err)}
})

2.简单的http服务

创建简单的服务器,代码如下(示例):

//在Node中专门提供了一个核心模块:http,用来编写服务器
//1. 引入http模块
var http = require('http')

//2. 使用http.createServer()方法创建一个web服务器
// 返回一个server实例
var server = http.createServer()
//3. 处理/响应
// 当客户端请求过来时,会自动触发服务器的request请求事件,然后执行第二个参数:回调处理
server.on('request',function(){console.log('收到请求了')})
//4. 绑定端口号,启动服务器
server.listen(3000,function(){console.log('3000端口已启动')})

运行结果如下:1.cmd窗口打印输出,浏览器一直在等待服务器端的响应
在这里插入图片描述

3.设置服务器端的响应

var http = require('http')
var server = http.createServer()
server.on('request',function(req,res){
	req.url //显示请求的路径
	res.write('hello')  //写入hello
	res.write('haomi')  //写入haomi
	//必须要加end,否则无效。
	res.end()
})
server.listen(3000,function(){console.log('3000端口已启动')})

此时任何该端口的请求,都会触发相同的这个事件
在这里插入图片描述

不同url

针对不同的请求url设置不同的响应,利用red.end()发送的同时结束

var http = require('http')
var server = http.createServer()
server.on('request',function(req,res){
  var items = [
    {name:'xxxx',id:1},
    {name:'yyyy',id:2}
  ]
  if (req.url ==='/') {res.end('index')}
  else if (req.url ==='/hello'){res.end('hello 你好')}
  else if (req.url === '/haha'){res.end(JSON.stringify(items) )}
  //必须要加end,否则无效。
  res.end()
})

server.listen(3000,function () {
  console.log('3000端口已启动...')
})

中文乱码

服务器端返回中文乱码问题:
 服务器端默认发送的数据,是utf-8编码的内容
 但是浏览器不知道是哪种编码
 这种情况下,会按照当前操作系统的默认编码去解析
 中文操作系统的默认编码是gbk
解决方式:
 Content-Type描述发送的数据内容是什么类型
  text/plain:普通文本;text/html:HTML格式
 charset:编码方式

server.on('request',function(req,res){
	res.setHeader('Content-Type','text/plain;charset = utf-8')
	res.end('hello 你好')
})

Content-type

参考手册:https://tool.oschina.net => 常用对照表

HTTP结合FS发送文件

var http = require('http')
var fs = require('fs')
// 1.创建服务器
var server = http.createServer()
// 2.响应客户端
server.on('request',function (req,res) {
  var url = req.url
  if (url ==='/'){
    fs.readFile('./resource/index.html',function (err,data) {
      if(!err){
        res.setHeader('Content-Type','text/html;charset=utf-8')
        res.end(data)
      }else{
        res.setHeader('Content-Type','text/plain;charset=utf-8')
        res.end('未找到相关网页')
      }
    })
  }else if(url === '/doubao'){
    fs.readFile('./resource/doubao.png',function (err,data) {
      if(!err){
        res.setHeader('Content-Type','image/png;')
        res.end(data)
      }else{
        res.setHeader('Content-Type','text/html;charset = utf-8')
        res.end('未找到相关网页')
      }
    })
  }else{
    res.setHeader('Content-Type','text/plain;charset = utf-8')
    res.end('未找到相关网页')
  }
})
server.listen(3000,function () {
  console.log('3000端口已启动')
})

三、模板引擎

实现类似apache功能:未导入相关网页,只实现了基本的模板引擎替换

var http = require('http')
var template = require('art-template')
var fs = require('fs')

var baseURL = 'D:\\【2】study pro\\1-Node.JS笔记\\day2\\resource'
// 1. 创建服务器
var server = http.createServer()
// 2. 设置响应
server.on('request',function (req,res) {
  var url = req.url
  if(url === '/') {url='/index.html'}
  fs.readFile(baseURL + url,function (err,data) {

    // 读取 index.html
    if (err){
      return res.end('404 Not Found')
    }


    // 处理 读取到的html
    fs.readdir(baseURL,function (err,path) {
      if (err){
        return res.end('Can not find www dir')
      }
      //模板引擎  解析替换
      var htmlStr = template.render(data.toString(),{
        path:path
      })


    // 返回 处理结果
      res.end(htmlStr)
    })

  })
})
//3.启动
server.listen(3000,function () {
  console.log('3000端口已启动')
})

客户端渲染和服务器端渲染

定义

  1. 客户端渲染:至少需要向服务器发送两次请求,第一次请求拿到页面基本结构,第二次针对页面中的{{}}发送ajax请求,得到数据,重新渲染页面。
  2. 服务器端渲染:只需要发送一次请求,直接在服务器端进行处理,把处理好的 页面数据 一起发送给客户端。

判断

服务器端渲染:在客户端的源代码中搜索,可以看到相应内容;页面刷新(例如点击下一页,页面刷新)
客户端渲染:异步渲染,即只改变了页面中的一小部分。

各自优势

  • 客户端渲染不利于SEO搜索引擎优化
  • 服务器端渲染可以被爬虫抓取到,客户端异步渲染很难被爬虫抓取到
  • 一般网站都是两者结合使用
  • 例如京东的商品列表采用服务器端渲染,可以被百度、谷歌等搜索引擎搜索到。
      京东的商品评论列表采用客户端渲染,提升用户体验且不需要SEO优化。

四、留言本Demo

关键问题

1.静态资源请求

浏览器收到HTML响应内容后,就要开始从上到下依次解析,
当在解析的过程中,如果发现:link、script、img、iframe、video、audio等带有 src 或者 href(link) 属性标签(具有外链的资源)的时候,浏览器会自动对这些资源发起新的请求。
解决方式:暴露一个public文件夹,将所有静态资源放在该文件夹下。
第一步:文件目录
在这里插入图片描述
第二步:设置服务端处理

else if (url.indexOf('/public') === 0) {
 fs.readFile('.' + url, function (err, data) {
    //如果找不到相应文件
    if (err) {
      return res.end('404 Not Found')
    }
    res.end(data)
  })
}

第三步:设置前端请求路径,改为绝对路径

  <link rel="stylesheet" href="/public/lib/bootstrap/dist/css/bootstrap.css">
  <link rel="stylesheet" href="/public/css/main.css">
  <script src="/public/js/main.js"></script>

2.在服务器端设置重定向

实现:

  1. 状态码设置为302:临时重定向
    301:永久重定向,浏览器会记住本次跳转,以后再访问a.com时,就不发送请求了,直接跳转到b.com。
    302:临时重定向,浏览器不记忆。每次访问a.com。都会向服务器发送求,服务器告诉跳转地址。
  2. 在响应头中通过 Location 描述重定向地址
res.statusCode = 302
res.setHeader('Location','/')

五、require加载规则

1.优先从缓存加载

a.js:
require('b.js')
var ExportC = require('c.js')

b.js:
console.log('b被加载了')
require('c,js')

c.js:
console.log('c被加载了')

打印输出:
 b被加载了
 c被加载了
分析:
 运行a.js中的第二句require时,并没有执行console.log,这样运行的目的只是获取c.js暴露的接口

2. 标识符分析

非路径形式的模块标识:核心模块和第三方模块

核心模块:本质上也是文件,核心模块文件已经被编译到了二进制文件中了,只需要按照名字来加载就可以了。
require(‘fs’) ; require(‘http’)
第三方模块:通过npm下来,通过require(包名)进行加载使用
导入过程:

  • node_modules/xxx包名/package.json文件的main属性:入口文件
  • main属性记录了xxx包的入口文件
  • 最后加载使用这个第三方文件
  • 注意:如果package.json文件不存在,或者main指定的入口文件不存在,则node自动找该目录下的index.js。

六、Express

web开发框架:原生的http在某些方面表现不足以应对我们的开发需求,所以需要用框架来加快我们的开发效率。

6.1 使用方法

6.1.1安装

	npm insatll express --save

6.1.2 hello world

	const express = require('express')
	const app = new express()
	app.get('/',(req,res)=>{res.send('Hello World')})
	app.listen(3000,()=> {console.log('3000端口已启动...')}) 

6.1.3 静态资源

app.use('/public',express.static('./public/'))
//请求路径必须以public开头,比如public里有一个a.js,那么访问方式是:127.0.0.1:3000/public/a.js

//不加第一个参数 和 就相当于第一个参数为/
app.use(express.static('./static/'))
//请求方式:127.0.0.1:3000/a.js

//第一个参数和第二个参数不同,那么可以认为第一个参数是第一个参数的别名
app.use('/a',express.static('./public/'))
//请求路径必须以a开头,比如public里有一个a.js,那么访问方式是:127.0.0.1:3000/a/a.js
//可以认为是起了个名字,既能够好记一点,又能保护原始目录中文件名
//前边是访问的url,后边是访问的资源

6.1.4 在Express中配置使用art-template模板引擎

  • 安装
	npm install -S art-template
	npm install -S express-art-template
  • 配置
//默认第一个参数是art,但是手动改为html
	app.engine('html',require('express-art-template'))
  • 使用 :默认寻找views目录中的文件,app.set(‘views’,预 修改路径)
  • app.set(‘views’,path.join(__dirname,’./views/’))
app.get('/',(req,res)=>{res.render('index.html',{
		title:'hello'
	})})

6.1.5 获取表单get请求体参数

req.query

6.1.6 获取表单post请求体参数

express中没有对应的api,需要引第三方包body-parser

//安装
npm i -S body-parser
//配置
const bodyParser = require('body-parser')
app.use(bodyParser.urlencoded({ extended:false}))
app.use(bodyParser.json())
//使用:通过req.body

七、Express结合fs搭建crud

  1. 设置项目目录
    在这里插入图片描述
  2. 搭建后端服务器,包括三个js文件
    2.1 app.js 重要配置项
const express = require('express')
const app = express()

//配置单独的路由文件
const router = require('./router')

//解析post参数
const bodyParser = require('body-parser')
//开放静态资源
//在html文件中引用,包括bootstrap
app.use('/node_modules',express.static('./node_modules/'))
//开放静态资源,本项目中只有基本的css样式
app.use('/public',express.static('./public/'))
//配置模板引擎
app.engine('html', require('express-art-template'));

//配置body-parser,解析post参数
app.use(bodyParser.urlencoded({ extended:false}))
app.use(bodyParser.json())
//配置模板引擎和body-parser一定要在挂载路由之前

//挂载router
app.use(router)

//启动服务3000...
app.listen(3000,function () {
  console.log('3000端口已启动...')
})

2.2 router.js 路由配置项,处理各个路由请求。

var fs = require('fs')

//对数据处理方法的封装
var Students = require('./students')

var express = require('express')

//1. 创建一个路由容器
var router = express.Router()

//2. 挂载路由到容器中
// 渲染表单页面(首页),通过地址栏输入url触发
// 1.读取数据:读取数据是异步操作,所以需要利用回调参数获取 读取结果,若返回error,则渲染类似404的报错页面,若返回正确数据,则进行第二步操作。
//   由于其他请求也需要读取数据,所以对读取文件数据的操作做一个封装。所有封装函数写在students.js文件中
// 2. 根据数据返回渲染页面
router.get('/students',function (req,res) {
  Students.find(function (err,data) {
    if(err){
      return res.status(500).send('Server error')
    }
    res.render('index.html',{
      fruits:['apple','orange','banana'],
      students:data
    })
  })
})


// 渲染新增页面(添加学生页面),通过点击a链接触发
// 只要一个静态页面,不要数据,所以简单渲染一下就好啦
router.get('/students/new',function (req,res) {
  res.render('new.html')
})

//渲染编辑页面(编辑学生页面),通过点击a链接触发,传入id参数
//1.  获取待编辑行的id。点击a链接,通过?的形式传入参数即可。例如:href=“/students/edit?id=1”,只需要利用模板字符串把id=1的部分替换成动态id即可
//   href = "/students/edit?id = {{ $value.id }}"
//2.  传入id,并根据id检索出对应的学生信息
//  利用req.query.id的方式 获取 get参数并传入,通过callback获取检索到的id对应的结果
//3.  渲染编辑页面
router.get('/students/edit',function (req,res) {
  Students.findById(req.query.id,function (err,student) {
    // 报错
    if(err){
      return res.status(500).send('Server error')
    }
    // 不报错
    res.render('edit.html',{
    // 对象
      student:student
    })
  })
})

//处理新增请求,通过提交表单触发
// 1. 通过get请求获取新增数据,是一个对象。并将参数传入save方法中
// 2. 将1中的对象保存到原始数组中
// 3. 拿到新数据,并渲染页面
router.post('/students/new',function (req,res) {
  //1.获取表单数据
  var student = JSON.stringify(req.body)
  //2. 处理:将数据保存到db.json文件中用以持久化
  //文件不是保存的不是对象,是字符串,所以不能直接操作,需要先转换成对象,处理完以后再返回字符串
  Students.save(student,function (err) {
    //3. 发送响应
    if (err){
      return res.status(500).send('Server error')
    }
    res.redirect('/students')
  })
})

//处理编辑数据请求,通过提交表单触发
router.post('/students/edit',function (req,res) {
  Students.updateById(req.body,function (err) {
    if(err){
      return res.status(500).send('Server error')
    }
    res.redirect('/students')
  })
})

//处理删除数据请求,通过点击a链接触发
router.get('/students/delete',function (req,res) {

  Students.deleteById(req.query.id,err =>{
    if (err) return res.status(500).send('Server error')
    res.redirect('/students')
  })
})

//总结:
// 1. 以上六个请求,get请求都可以通过a链接触发,post请求都可以通过提交form表单触发
// 2. 在router中不需要进行类似 toString   JSON.stringify   JSON.parser这些类型转转,所有的类型转换放在students.js中。
// 3. 在router中只做了两个内容:获取参数(包括get参数和post参数)和渲染页面
//  获取参数   ==》  调用相关数据处理方法  ==》 拿到返回的结果,并渲染页面
//3.导出router
module.exports = router

2.3 students.js 方法定义,定义了一系列数据处理方法,包括读取,替换,修改等等。
要注意数据类型的转换。二进制 JSON字符串 对象

// 操作文件中的数据   增删改查
const fs = require('fs')

//要读取很多次这个文件,如果该文件目录修改,那么所有路径都要改,所以先定义一个变量存储该路径
//星星星
const dbPath = './db.json'
// 获取所有学生列表
// 这里做两次数据转换,第一次二进制转字符串;第二次字符串转对象
function getStudents(callback) {
//读出来的是Buffer类型,所以要转成string类型,
//方式:可以用第二个参数utf8,也可以用toString()
  fs.readFile(dbPath,'utf8',function(err,data){
  //如果有错误,则第一个参数为err,第二个参数为undefined
  //如果无错误,则第一个参数为null,第二个参数为students
    if(err){
      return callback(err)
    }
    //从文件中读取的数据一定是字符串,所以要手动转成对象。
    
    callback(null,JSON.parse(data).students)
  })
}

//新增学生信息
function createStudents(student,callback){
  getStudents(function (err,data) {
    if(err){
      callback(err)
    }
    const students = data
    //这里传入的student是JSON的字符串类型,所以要转成对象才能操作
    student = JSON.parse(student)
    student.id = students[students.length-1].id +1
    student.id = parseInt(student.id)
    students.push(student)
    //定义对象
    //星星星星星   
    // 必须转乘string类型才可以进行文件写入,而且还要写成对象形式。。
    var res = JSON.stringify({
      students:students
    })
    fs.writeFile(dbPath,res,function (err) {
      if(err){ return callback(err)}
      callback(null)
    })
  })
}

//获取单个学生信息
function findById(id,callback){
  //1. 读取文件内容
  getStudents(function (err,students) {
    if(err){
      callback(err)
    }
    //2. 查找对应id
    var student = students.find(item => {
      return item.id = parseInt(id)
    })
    //3. 返回err或者对应id的相关信息
    callback(null,student)
  })


}
//更新学生信息
function updateStudents(student,callback){
  //1.读取json文件数据
  getStudents(function (err,students) {
    //1.1如果有错误,返回错误
    if(err){
      return callback(err)
    }
    //1.2如果没错误,对data进行处理
  //2. 处理文件数据
    //2.1 用新student对象替换原对象

    students.forEach( (item,index) => {
      students[index].id = parseInt(students[index].id)
      if(parseInt( item.id) === parseInt(student.id) ) {
        students[index] = student
      }
    })
    //2.2 写入文件
    var res = JSON.stringify({
      students:students
    })
    fs.writeFile(dbPath,res,function (err) {
      if(err){ return callback(err)}
      callback(null)
    })
  })
}

//删除学生
function deleteById(id,callback){
  //1.读取json文件数据
  getStudents(function (err,students) {
    //1.1如果有错误,返回错误
    if(err){
      return callback(err)
    }
    //1.2如果没错误,对data进行处理
    //2. 处理文件数据
    //2.1 用新student对象替换原对象
    students.forEach( (item,index) => {
      if(parseInt(item.id) === parseInt(id) ) {
        students.splice(index,1)
      }
    })
    //2.2 写入文件
    var res = JSON.stringify({
      students:students
    })
    fs.writeFile(dbPath,res,function (err) {
      if(err){ return callback(err)}
      callback(null)
    })
  })
}

//导出所有的方法,并重命名
let students = {
  find:getStudents,
  save:createStudents,
  updateById:updateStudents,
  findById:findById,
  deleteById:deleteById
}
module.exports = students

八、MongoDB

8.1 初始化

1.开启

mongod --dbpath E:\MongoDB\data\db

2.连接

// 默认连接本机的MongoDB服务
mongo

3.退出

exit

4.已配置手动启动mongodb

net start mongodb

8.2 基本命令

  1. show dbs:查看显示所有数据库
  2. db :查看当前操作的数据库
  3. use 数据库名称:切换到指定的数据库(如果没有,则会新建)
  4. show collections:查询当前数据库的集合
  5. db.集合.find():查询当前集合的具体数据

8.3 Node中操作MongoDB数据库

方法1:使用mongodb包
方法2:mongoose:基于mongodb进行的二次封装。
使用方法2,更简单。

基本概念

数据库:一个对象。可以有多个数据库
集合:一个数据库中可以有多个集合
文档:一个集合可以有多个文档

设计Schema发布model

var mongoose = require('mongoose')

//设置数据库的 集合结构
var Schema = mongoose.Schema

//连接数据库
//指定连接的数据库不需要存在,当插入第一条数据之后机会自动被创建出来
mongoose.connect('mongodb://localhost/itcast')

//设计文档结构(表结构)
//约束的目的是  保证数据的完整性,不要有脏数据
var userSchema = new Schema({
	username:{
		type:String,
		required:true  //必须有
	},
	password: {
		type:String,
		required:true  //必须有	
	}
})

//	将文档结构发布为模型
//	mongoose.model方法就是用来将一个架构发布为model
//	第一个参数:传入一个大写名词 单数字符串 用来表示数据库的名称
//				mongoose 自动将大写名词的字符串生成 小写复数 的集合名称
//				例如这里的User  最终会变成 users 的集合名称
//	第二个参数: Schema
//
//	返回值:	模型构造函数
var User = mongoose.model('User',userSchema)

//对模型进行操作。

操作model

  1. 新增数据
//1. 新建一个实例对象
var admin = new User({
	username:'admin',
	password:'123456'
})

//2. 利用save持久化存储
admin.save(function(err){
	if(err){console.log('err')}
	else{console.log('保存成功')}
})
  1. 查询数据
//查询所有,返回一个数组
User.find(function(err,res){
	if(err){ console.log('查询失败')}
	else { console.log(res)}
})

//按条件查询,返回一个数组,包含所有满足条件的对象
User.find({username:'admin'},function(err,res){
	if(err){ console.log('查询失败')}
	else { console.log(res)}
})

//按条件查询,返回第一个满足条件的对象
User.findOne({username:'admin'},function(err,res){
	if(err){ console.log('查询失败')}
	else { console.log(res)}
})
  1. 删除数据
User.remove({username:'user'},function(err){
  if(err){ console.log('删除失败')}
  else { console.log('删除成功')}
})
  1. 更新数据
//根据id来更新
User.findByIdAndUpdate('61a04f0043ef3f305ce3286b',{
  password:'admin'
},function (err) {
  if(err){
    console.log('err')
  }else{
    console.log('更新成功')
  }
})

8.4 Node中操作MySQL数据库

//1. 引入包
var mysql      = require('mysql');

//2. 创建连接
var connection = mysql.createConnection({
  host     : 'localhost',
  user     : 'root',
  password : 'root',
  database : 'users'
});

//3. 连接
connection.connect(function (err) {
  if(err){
    console.log('连接失败')
    return
  }
  console.log('连接成功')
});

//4. 执行数据数据操作

//添加
connection.query('INSERT INTO users VALUES(NULL,"admin","123456")', function (err, result) {
  if (err) {
    console.log('[INSERT ERROR] - ', err.message)
    return
  }
  console.log('--------------------------INSERT----------------------------');
  console.log('INSERT ID - ', result);
  console.log('------------------------------------------------------------\n\n');

});


//更新
let sqlstring = 'Update users Set password = "admin" where username = "admin"';
connection.query(sqlstring,function (err,result) {
  if(err){
    console.log('[UPDATE ERROR] - ', err.message);
    return;
  }
  console.log('--------------------------UPDATE----------------------------');
  console.log('UPDATE affectedRows - ', result.affectedRows);
  console.log('------------------------------------------------------------\n\n');
})


// 查询
let sqlstring = 'Select * From users';
connection.query(sqlstring,function (err,result) {
  if(err){
    console.log('[SELECT ERROR] - ', err.message);
    return;
  }
  console.log('--------------------------SELECT----------------------------');
  console.log('SELECT affectedRows - ', result);
  console.log('------------------------------------------------------------\n\n');
})

//删除
let sqlstring = 'Delete From users where username = "admin"';
connection.query(sqlstring,function (err,result) {
  if(err){
    console.log('[DELETE ERROR] - ', err.message);
    return;
  }
  console.log('--------------------------DELETE----------------------------');
  console.log('DELETE affectedRows - ', result.affectedRows);
  console.log('------------------------------------------------------------\n\n');
})

//5. 关闭连接
connection.end();

Tips

  1. 文件操作中的相对路径可以省略,模块加载中的相对路径不能省略
    在文件操作的路径中:
     ./data/a.txt 相对路径:当前目录
     data/a.txt 相对路径:当前目录
     /data/a.txt 绝对路径:当前文件模块所处磁盘根目录
     c://xx/xx/… 绝对路径
fs.readFile('data/a.txt',function(){})   //可以省略  ./
require('data/foo.js')	//报错,所以不可以省略./
  1. package-lock.json的作用:保存所有包的信息(下载地址、版本)
    有两个作用:1.重新npm i 的时候可以提升速度;2. 锁定版本,防止自动升级新版本
  2. Cookie不能保存敏感信息,因为用户可以自己手动修改,一般保存:用户名、购物车此类信息。
    Session在服务端保存敏感信息,给客户端一把钥匙,用户拿着钥匙获取对应的服务端存储的信息
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值