Node.js:Node核心模块
一、Buffer
什么是Buffer?
- 缓冲区Buffer是暂时存放输入/输出数据的一段内存
- JS语言没有二进制数据类型,而在处理TCP和文件流的时候,必须要处理二进制数据
- NodeJS提供了一个Buffer对象来提供对二进制数据的操作
- Buffer是一个表示固定内存分配的全局对象,也就是说要放到缓存区中的字节数需要提前确定
- Buffer就好比由一个8位字节元素组成的数组,可以有效的在JavasScript中表示二进制数据
什么是字节?
- 字节(Byte)是计算机存储时的一种计量单位,一个字节等于8位二进制数
- 一个位就代表一个0或1,每8个位(bit)组成一个字节(Byte)
- 字节是通过网络传输信息的单位
- 一个字节最大值十进制数是255
2^8-1
- 图示
- 单位
8位 = 1字节
1024字节 = 1K
1024K = 1M
1024M = 1G
1024G = 1T
定义Buffer的3种方式
- 通过长度定义buffer
// 创建一个长度为 10、且用 0 填充的 Buffer const buf1 = Buffer.alloc(10); // 创建一个长度为 10、且用 1 填充的 Buffer const buf2 = Buffer.alloc(10, 1); // 创建一个长度为 10、且未初始化的 Buffer const buf3 = Buffer.allocUnsafe(10);
- 通过数组定义buffer
// 创建一个包含 [0x1, 0x2, 0x3] 的 Buffer const buf4 = Buffer.from([1, 2, 3]);
正常情况下为0-255之间
- 字符串创建
const buf5 = Buffer.from('撩课教育');
buffer常用方法
fill方法
buf.fill(value[, offset[, end]][, encoding])
手动初始化,一般用于将buffer内容清0
buffer.fill(0);write方法
buf.write(string[, offset[, length]][, encoding])
buffer.write(‘撩’,0,3,‘utf8’);
buffer.write(‘课’,3,3,‘utf8’);
writeInt8()、readInt8()以八进制读写
let buf = new Buffer(4); buf.writeInt8(0,0); buf.writeInt8(16,1); buf.writeInt8(32,2); buf.writeInt8(48,3);//16*3*/ console.log(buf); console.log(buf.readInt8(0)); console.log(buf.readInt8(1)); console.log(buf.readInt8(2)); console.log(buf.readInt8(3));
isBuffer:判断是否是buffer
Buffer.isBuffer()
length :获取字节长度(显示是字符串所代表buffer的长度)
Buffer.byteLength("撩课"); buffer.length;
Base64
- Base64是网络上最常见的用于传输8Bit字节码的编码方式之一
- Base64就是一种基于64个可打印字符来表示二进制数据的方法
- Base64要求把每三个8Bit的字节转换为四个6Bit的字节(38 = 46 = 24)
- 然后把6Bit再添两位高位0,组成四个8Bit的字节,也就是说,转换后的字符串理论上将要比原来的长1/3
二、FS模块
什么是fs模块?
- 在Node.js中,使用fs模块来实现所有有关文件及目录的创建、写入及删除操作
- 在fs模块中,所有的方法都分为同步和异步两种实现
- 具有sync后缀的方法为同步方法,不具有sync后缀的方法为异步方法
- 在进行文件操作的时候,首先需要引入文件系统模块。
let fs = require('fs ');
- 通过
__dirname
获取当前文件夹目录
读取文件
同步读取
fs.readFileSync(path[, options])
举例:
let fd = fs.readFileSync(__dirname + '/source/a.txt');
异步读取
fs.readFile(path[, options], callback)
举例:
fs.readFile(__dirname + '/source/a.txt', (err, data)=>{if(!err){console.log(data);console.log(data.toString());} });
写入文件
同步写入
fs.writeFileSync(file, data[, options])
举例:
let fs = require('fs');// 1. 打开文件 let fd = fs.openSync(__dirname + '/source/b.txt', 'w'); console.log(fd);// 2. 同步写入内容 fs.writeFileSync(fd, '哈哈哈!');// 3. 保存并退出 fs.closeSync(fd);// 4. 后续操作 console.log('后续的操作');
异步写入
fs.writeFile(file, data[, options], callback)
举例:
let fs = require('fs');// 1. 打开文件 fs.open(__dirname + '/source/c.txt', 'a', (err, fd) => {if (!err) {// 2. 写入文件fs.writeFile(fd, '小撩漂亮!哈哈哈哈' + Date.now() + '\n', (err)=>{if(!err){console.log('写入文件成功');}else {throw err;}});// 3. 关闭文件fs.close(fd, (err)=>{if(!err){console.log('文件已经保存并关闭!');}else {throw err;}})} });// 后续操作 console.log('后续操作');
options
encoding
flag flag 默认 = ‘w’
图示
助记
mode 读写权限,默认为0666追加文件
fs.appendFile(file, data[, options], callback)
操作
fs.appendFile('./01.txt',Date.now()+'\n',function(){console.log('ok'); })
拷贝文件
function copy(src,target){fs.readFile(src,function(err,data){fs.writeFile(target,data);}) }
读取、写入图片
fs.readFile('C:/Users/yejianhua/Desktop/girl.jpg', (err, data)=>{if(!err){// 2. 写入图片文件fs.writeFile(__dirname + '/source/girl.jpg', data, (err)=>{if(err) throw err;console.log('写入成功');});}
});
读取、写入音频
fs.readFile('C:/Users/yejianhua/Desktop/lk.mp4', (err, data)=>{console.log(data);if(!err){// 2. 写入 文件fs.writeFile(__dirname + '/source/lk.mp4', data, (err)=>{if(err) throw err;console.log('写入成功');});}
});
三、Stream
什么是流?
- 流是一组有序的,有起点和终点的字节数据传输手段
- 它不关心文件的整体内容,只关注是否从文件中读到了数据,以及读到数据之后的处理
- 流是一个抽象接口,被 Node 中的很多对象所实现;比如HTTP 服务器request和response对象都是流。
可读流createReadStream
概念
实现了stream.Readable接口的对象,将对象数据读取为流数据,当监听data事件后,开始发射数据
fs.createReadStream = function(path, options) {return new ReadStream(path, options);
};
util.inherits(ReadStream, Readable);
创建可读流
let rs = fs.createReadStream(path,[options]);
参数
path读取文件的路径
options
flags打开文件要做的操作,默认为’r’
encoding默认为null
start开始读取的索引位置
end结束读取的索引位置(包括结束位置)
监听data事件
流切换到流动模式,数据会被尽可能快的读出
rs.on('data', function (data) {console.log(data);
});
监听end事件
该事件会在读完数据后被触发
rs.on('end', function () {console.log('读取完成');
});
监听error事件
rs.on('error', function (err) {console.log(err);
});
监听open事件
rs.on('open', function () {console.log(err);
});
监听close事件
rs.on('close', function () {console.log(err);
});
暂停和恢复触发data
通过pause()方法和resume()方法
rs.on('data', function (data) {rs.pause();console.log(data);
});
setTimeout(function () {rs.resume();
}, 2000);
可写流createWriteStream
概念
实现了stream.Writable接口的对象来将流数据写入到对象中
fs.createWriteStream = function(path, options) {return new WriteStream(path, options);
};
util.inherits(WriteStream, Writable);
创建可写流
let ws = fs.createWriteStream(path,[options]);
参数
path写入的文件路径
options
- flags打开文件要做的操作,默认为’w’
- encoding默认为utf8
- highWaterMark写入缓存区的默认大小16kb
write方法
ws.write(chunk,[encoding],[callback]);
- chunk写入的数据buffer/string
- encoding编码格式chunk为字符串时有用,可选
- callback 写入成功后的回调
监听end事件
ws.end(chunk,[encoding],[callback]);
表明接下来没有数据要被写入 Writable 通过传入可选的 chunk 和 encoding 参数,可以在关闭流之前再写入一段数据 如果传入了可选的 callback 函数,它将作为 ‘finish’ 事件的回调函数
finish方法
在调用了 stream.end() 方法,且缓冲区数据都已经传给底层系统之后, ‘finish’ 事件将被触发。
var writer = fs.createWriteStream('./2.txt');
for (let i = 0; i < 100; i++) {writer.write(`hello, ${i}!\n`);
}
writer.end('结束\n');
writer.on('finish', () => {console.error('所有的写入已经完成!');
});
drain方法
- 当一个流不处在 drain 的状态, 对 write() 的调用会缓存数据块, 并且返回 false
- 一旦所有当前所有缓存的数据块都排空了(被操作系统接受来进行输出), 那么 ‘drain’ 事件就会被触发
- 建议, 一旦 write() 返回 false, 在 ‘drain’ 事件触发前, 不能写入任何数据块
读入、写入流案例
let fs = require('fs');// 1. 创建读写流
let rs = fs.createReadStream('C:/Users/yejianhua/Desktop/lk1.mp4');
let ws = fs.createWriteStream(__dirname + '/source/lk888.mp4');// 2. 监听流的打开
rs.once('open', ()=>{console.log('写入流已经打开了~');
});ws.once('open', ()=>{console.log('读入流已经打开了~');
});// 3. 监听data
rs.on('data', (data)=>{console.log(data);ws.write(data);
});// 4. 监听流的关闭
rs.once('end', ()=>{console.log('读入数据已经完成~');
});rs.once('close', ()=>{console.log('读入数据通道关闭~');
});
pipe方法的使用
pipe用法
readStream.pipe(writeStream);let from = fs.createReadStream('./a.txt');
let to = fs.createWriteStream('./b.txt');
from.pipe(to);
案例:
let fs = require('fs');// 1. 创建读写流
let rs = fs.createReadStream('C:/Users/yejianhua/Desktop/lk1.mp4');
let ws = fs.createWriteStream(__dirname + '/source/lk999.mp4');// 2. 创建管道
rs.pipe(ws);
pipe方法的原理
let fs = require('fs');
let rs = fs.createReadStream("C:/Users/yejianhua/Desktop/lk.mp4");
let ws = fs.createWriteStream(__dirname + "/source/new.mp4");
rs.on('data', (data)=> {let flag = ws.write(data);console.log(flag);if(!flag)rs.pause();
});ws.on('drain', ()=> {rs.resume();
});rs.on('end', ()=> {ws.end();
});
四、 HTTP模块
HTTP协议和TCP协议?
长链接
管道化
URI和URL
URI
URI(Uniform Resource Identifier)是统一资源标识符,在某个规则下能把这个资源独一无二标示出来,比如人的身份证号
使用特点
- Uniform 不用根据上下文来识别资源指定的访问方式
- Resource 可以标识的任何东西
- Identifier 表示可标识的对象
URL
统一资源定位符,表示资源的地点,URL时使用浏览器访问WEB页面时需要输入的网页地址
使用特点
- Uniform 不用根据上下文来识别资源指定的访问方式
- Resource 可以标识的任何东西
- Location 定位
格式
具体组成:协议类型、登录信息、服务器地址、服务器端口号、带层次的文件路径、查询字符串、片段标识符
HTTP
概念
- 请求的一方叫客户端,响应的一方叫服务器端
- 通过请求和响应达成通信
- HTTP是一种不保存状态的协议
请求报文
- 请求行
主要方法:GET 获取资源、POST 向服务器端发送数据,传输实体、TRACE 追踪路径
协议/版本号
URL - 请求头
① 通用首部(General Header)
② 请求首部(Request Header)
③ 响应首部(Response Header)
④ 实体首部(Entity Header Fields) - 请求体
响应报文
- 响应行
- 响应头
- 响应体
状态码
- 概念
状态码负责表示客户端请求的返回结果、标记服务器端是否正常、通知出现的错误 - 状态码类别
- 2XX 成功
200(OK 客户端发过来的数据被正常处理
204(Not Content 正常响应,没有实体
206(Partial Content 范围请求,返回部分数据,响应报文中由Content-Range指定实体内容 - 3XX 重定向
301(Moved Permanently) 永久重定向
302(Found) 临时重定向,规范要求方法名不变,但是都会改变
303(See Other) 和302类似,但必须用GET方法
304(Not Modified) 状态未改变 配合(If-Match、If-Modified-Since、If-None_Match、If-Range、If-Unmodified-Since)
307(Temporary Redirect) 临时重定向,不该改变请求方法 - 4XX 客户端错误
400(Bad Request) 请求报文语法错误
401 (unauthorized) 需要认证
403(Forbidden) 服务器拒绝访问对应的资源
404(Not Found) 服务器上无法找到资源 - 5XX 服务器端错误
500(Internal Server Error)服务器故障
503(Service Unavailable) 服务器处于超负载或正在停机维护
HTTP服务器
概念
HTTP全称是超文本传输协议,构建于TCP之上,属于应用层协议
创建HTTP服务器
let server = http.createServer([requestListener]);
server.on('request',requestListener);
requestListener 当服务器收到客户端的连接后执行的处理
启动HTTP服务器
server.listen(port,[host],[backlog],[callback]);
server.on('listening',callback);
1)参数
port 监听的端口号
host 监听的地址
backlog 指定位于等待队列中的客户端连接数
2)实例
let http = require('http');
let server = http.createServer(function(req,res){}).listen(3000,'127.0.0.1',function(){console.log('服务器端开始监听!')});
关闭HTTP服务器
server.close();
server.on('close',function(){});
1)实例
let http = require('http');
let server = http.createServer(function(req,res){});
server.on('close',function(){console.log('服务器关闭');
});
server.listen(3000,'127.0.0.1',function(){console.log('服务器端开始监听!')server.close();
});
监听服务器错误
实例
server.on('error',function(){if(e.code == 'EADDRINUSE'){console.log('端口号已经被占用!); }
});
setTimeout
概念:设置超时时间,超时后不可再复用已经建立的连接,需要发请求需要重新建立连接。默认超时时间时2分钟
案例
server.setTimeout(msecs,callback);server.setTimeout(1000, ()=>{console.log('设置超时时间为1s'); }); server.on('timeout',function(){console.log('连接已经超时'); });
获取客户端请求信息
request
method 请求的方法
url 请求的路径
headers 请求头对象
httpVersion 客户端的http版本
socket 监听客户端请求的socket对象实操
let server = http.createServer((req, res) => {if (req.url !== '/favicon.ico') {let out = fs.createWriteStream(path.join(__dirname, 'request.log'));out.write('1) method: ' + req.method + '\n');out.write('2) url: ' + req.url + '\n');out.write('3) headers: ' + JSON.stringify(req.headers) + '\n');out.write('4) httpVersion: ' + req.httpVersion + '\n');} }).listen(8080, '127.0.0.1');
url模块
格式
url.parse(urlStr,[parseQueryString]);
模块属性
- href 被转换的原URL字符串
- protocal 客户端发出请求时使用的协议
- slashes 在协议与路径之间是否使用了//分隔符
- host URL字符串中的完整地址和端口号
- auth URL字符串中的认证部分
- hostname URL字符串中的完整地址
- port URL字符串中的端口号
- pathname URL字符串的路径,不包含查询字符串
- search 查询字符串,包含?
- path 路径,包含查询字符串
- query 查询字符串,不包含起始字符串?
- hash 散列字符串,包含#
发送服务器响应流
概念
http.ServerResponse对象表示响应对象
writeHead
response.writeHead(statusCode,[reasonPhrase],[headers]);
常用属性
- content-type 内容类型
- location 将客户端重定向到另外一个URL地址
- content-disposition 指定一个被下载的文件名
- content-length 服务器响应内容的字节数
- set-cookie 在客户端创建Cookie
- content-encoding 指定服务器响应内容的编码方式
- cache-cache 开启缓存机制
- expires 用于制定缓存过期时间
- etag 指定当服务器响应内容没有变化不重新下载数据
Header
设置、获取和删除Header
常用属性
- response.setHeader(‘Content-Type’,‘text/html;charset=utf-8’);
- response.getHeader(‘Content-Type’);
- response.removeHeader(‘Content-Type’);
- response.headersSent
判断响应头是否已经发送
const http = require('http');http.createServer((req, res)=>{console.log(res.headersSent ? '响应头已经发送' : '响应头未发送');// 设置隐式响应头res.setHeader('Content-Type', 'text/html;charset=utf-8');res.writeHead(200, 'ok');res.write('<h1>Hello, ItLike</h1>');res.write('<h1>Hello, ItLike</h1>');res.write('<h1>Hello, ItLike</h1>');res.write('<h1>Hello, ItLike</h1>');// 本次响应结束res.end('<h1>撩课学院</h1>');console.log(res.headersSent ? '响应头已经发送' : '响应头未发送');
}).listen(3000, '127.0.0.1', ()=>{console.log('服务器已经启动~')
});
write
可以使用write方法发送响应内容
常用操作
- response.write(chunk,[encoding]);
- response.end([chunk],[encoding]);
querystring
querystring模块用来转换URL字符串和URL中的查询字符串
- parse方法用来把字符串转换成对象
querystring.parse(str,[sep],[eq],[options]); - stringify方法用来把对象转换成字符串
querystring.stringify(obj,[sep],[eq]);
post请求
服务器端
const http = require('http');
const queryString = require('querystring');
const util = require('util');http.createServer((req, res)=>{let postData;// post请求, 得做事件监听req.on('data', (data)=>{postData += data;});// 监听数据接收完毕req.on('end', ()=>{postData = queryString.parse(postData);console.log(util.inspect(postData));res.end('数据接收成功!');});}).listen(3000, '127.0.0.1');
客户端
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title></title>
</head>
<body>
<form action="http://localhost:3000/" method="post"><label>用户名: <input type="text" name="user"></label><br><label>密 码: <input type="password" name="pwd"></label><br><input type="submit" value="提交">
</form>
</body>
</html>
补充
Node.js没有web容器
没有根目录, 不能像PHP, JavaWeb通过切换目录结构切换页面; 所有的页面资源都得通过路径配置
在Node中, 采用fs模块读入文件, 并手动配置路由
演示
- 演示1:加载静态界面
- 演示2:加载图片,css等静态资源
ejs的使用
https://github.com/mde/ejs
app.js
const http = require('http');
const fs = require('fs');
const ejs = require('ejs');
const path = require('path');// 1. 创建服务器
http.createServer((req, res)=>{// 1.1 读取文件readDataJson((jsonData)=>{// 1.2 读取模板信息fs.readFile(path.join(__dirname, 'view/list.ejs'), (err, data)=>{if(err) throw err;// 实例化模板引擎let ejsList = data.toString();let tempList = ejs.render(ejsList, jsonData);// 1.3 响应给客户端res.writeHead(200, {'Content-Type': 'text/html;charset=utf-8'});res.end(tempList);});});
}).listen(3000);let readDataJson = (callback)=>{fs.readFile(path.join(__dirname, 'model/data.json'), (err, data)=>{if(err) throw err;let jsonData = JSON.parse(data);callback && callback(jsonData);});
};
list.ejs
<!doctype html>
<html>
<head><meta charset="UTF-8"><meta name="viewport"content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>百度风云排行版</title><style>* {margin: 0;padding: 0;list-style: none;}#panel {width: 500px;border: 1px solid #c6c8ca;margin: 100px auto;}#panel_header {display: flex;justify-content: space-around;border-bottom: 1px solid #ccc;line-height: 44px;color: #777;}#panel_body > li {display: flex;flex-direction: row;justify-content: space-between;line-height: 44px;border-bottom: 1px solid #e8e8e8;}.c-icon {background: url(https://ss1.bdstatic.com/5eN1bjq8AAUYm2zgoY3K/r/www/cache/static/protocol/https/global/img/icons_5859e57.png) no-repeat 0 0;display: inline-block;width: 14px;height: 14px;vertical-align: text-bottom;font-style: normal;overflow: hidden;}.opr-toplist-st {margin-bottom: 2px;}.c-icon-up {background-position: -720px -168px;}.c-icon-down {background-position: -744px -168px;}.left {margin-left: 10px;display: flex;flex-direction: row;align-items: center;}.left .no {display: flex;justify-content: center;align-items: center;width: 18px;height: 18px;background-color: red;color: #fff;margin: 5px;}.right {margin-right: 10px;}#panel_footer {display: flex;justify-content: flex-end;margin: 10px;color: #777;font-size: 15px;}</style>
</head>
<body>
<section id="panel"><div id="panel_header"><span>排名</span><span>搜索指数</span></div><ul id="panel_body"><% for(var i = 0; i < lists.length; i++) { %><li><div class="left"><span class="no" style="background: <%= i > 2 ? 'gray' : 'red' %>;"><%= i + 1 %></span><span><%= lists[i].title %></span></div><div class="right"><span><%= lists[i].count %></span><% if(lists[i].up === 1){ %><i class="opr-toplist-st c-icon c-icon-up"></i><% }else { %><i class="opr-toplist-st c-icon c-icon-down"></i><% } %></div></li><% } %></ul><div id="panel_footer"><span style="margin-right: 5px">来源:</span><span><%= source %></span></div>
</section>
</body>
</html>
Node.js:Node核心模块相关推荐
- node.js中模块_在Node.js中需要模块:您需要知道的一切
node.js中模块 by Samer Buna 通过Samer Buna 在Node.js中需要模块:您需要知道的一切 (Requiring modules in Node.js: Everythi ...
- 浅析 Node.js 的 vm 模块以及运行不信任代码
为什么80%的码农都做不了架构师?>>> 在一些系统中,我们希望给用户提供插入自定义逻辑的能力,除了 RPC 和 REST 之外,运行客户提供的代码也是比较常用的方法,好处是可 ...
- Node.js的核心与红利
唯有明晰历史,才能了然当下,预知未来.作者从历史角度解读Node.js,帮助读者透过猜忌和谣言,看清真实的Node.js,了解Node.js的核心与红利. 令人惴惴不安的Node.js 我们越来越频繁 ...
- Node.js学习笔记——模块加载机制及npm指令详解
文章目录 二.模块化 1.模块化的基本概念 2.Node.js 中的模块化 Node.js 中模块的分类 加载模块 Node.js 中的模块作用域 向外共享模块作用域中的成员 Node.js 中的模块 ...
- Node.js(一、Node.js基础、模块加载机制、包等)
Node.js(一.Node.js基础.模块加载机制.包等) 1.Node.js基础 1.1.Node是什么 1.2.Node环境安装失败解决方法 1.2.1.Node环境搭建 1.2.2.错误代码2 ...
- Node.js 初识 fs 模块
fs 模块是文件操作的封装,它提供了文件的读取.写入.更名.删除.遍历目录.链接等 Unix 文件系统操作.与其他模块不同的是,fs 模块中所有的操作都提供了 同步 和 异步 两个版本,比如读取文件内 ...
- 开场 Live,分享点干货——「深入了解 Node.js 包与模块机制」
先放上 Live 地址: www.zhihu.com/lives/84274- 本次 Live 将深入剖析 Node.js 包与模块机制,包括且不限于解析 Node.js 源码.社区规范等.本人认为这 ...
- php node 目录,node.js基于fs模块对系统文件及目录进行读写操作的方法详解
本文主要介绍了node.js基于fs模块对系统文件及目录进行读写操作的方法,结合实例形式分析了nodejs使用fs模块针对文件与目录的读写.创建.删除等相关操作技巧,需要的朋友可以参考下. 如果要用这 ...
- Node.js web应用模块之forever
javascript 一统江湖的势头越来越猛,越来越重的前端,不得不实施前后端分离,angular.js 成功把前端javascript抽象成了一个复杂的MVC框架,注意,它是一个框架,可不是普普通通 ...
- Node.js web应用模块之Supervisor
在开发或调试Node.js应用程序的时候,当你修改js文件后,总是要按下CTRL+C终止程序,然后再重新启动,即使是修改一点小小的参数,也 总是要不断地重复这几个很烦人的操作.这是因为Node.js ...
最新文章
- Good Bye 2014 B. New Year Permutation(floyd )
- python推荐书-Python 阅读书目推荐
- 用 Ubuntu 重置 Windows 密码
- ldap导入mysql_openLDAP 部署(亲测可用)
- shell脚本采用crontab定时备份数据库日志
- php开启curl和openssl
- Asp.net 请求中变量的保存方式
- 全量更新和增量更新_增量BIOS更新或直接更新到最新版本哪个更好?
- Python深入06 Python的内存管理
- 网页文字无法免费复制的几种解决方法
- python3.7.4安装pip_python3.7下pip的安装教程
- 1006. 换个格式输出整数 (15)-PAT乙级真题
- 现代操作系统笔记2(操作系统概念和系统调用)
- 妙不可言的JASTVIN云域网,用过的都说好!你怎么看
- 【File类、递归】
- easyui datagrid mysql分页_Easyui 自定义分页_EasyUI 教程
- Flink OLAP 助力 ByteHTAP 亮相数据库顶会 VLDB
- 网友晒异性合租趣事:坐马桶都要签协议
- html java实训心得,学习中关于HTML的总结与一些心得
- Web网站模板-横向滚动个人简历响应式网站模板(HTML+CSS+JavaScript)