文件读取
- 导入
fs
模块
const fs = require('fs')
- 调用
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);
})
文件写入
-
导入
fs
模块 -
调用
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 服务器
- 导入
http
fs
path
模块,创建服务器实例 server
const http = require('http');
const fs = require('fs');
const path = require('path');
const server = http.createServer();
- 监听 web 服务器的 request 事件
一共有三个资源会被请求:
/clock/index.html
/clock/index.css
/clock/index.js
server.on('request', (req, res)=>{
})
- 获取请求的
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)
}
- 根据获取的文件路径读取对应的内容,并响应给客户端
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')
})
完成代码:
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()方法用于加载模块)
模块的加载机制
- 首先从缓存中加载
- 模块在第一次加载后会被缓存。所以多次调用 require() 不会导致模块的代码被指向多次
- 内置模块的加载优先级最高
- 用户的自定义模块
- 必须以
./
或../
开头的路径标识符。如果没有加node会把它当作内置或第三方模块加载
- 必须以
- 第三方模块
- 尝试从/node_modules 文件夹中加载第三方模块,并一级一级的沿着目录向上寻找
- 目录作为模块
- 在被加载目录下寻找 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() 可以将路由模块话管理
-
创建路由模块的js文件,在js文件中调用
express.Router()
函数创建路由对象const express = require('express'); const router = express.Router();
-
往路由对象上挂载路由
router.get('/user/list', (req, res) => { res.send('Get user list'); }) router.post('/user/add', (req, res) => { res.send('Add new user'); })
-
使用
module.exports
向外暴露对象module.exports = router
-
使用
app.use()
注册路由模块// 导入路由模块 const userRouter = require('./router/user/js'); // 使用 app.use 注册路由模块 app.use(userRouter);
-
也可以为路由模块添加前缀
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")
})
注意事项
- 一定要在路由之前注册中间件
- 执行完中间件后,不要忘记调用 next() 函数
- 连续调用多个中间件时,多个中间件共享 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,解析请求体数据
自定义中间件解析表单数据
- 新建 my-body-parser
js
文件, 导入 querystring 模块用于解析字符串,定义中间件函数
const qs = require('querystring');
const bodyParser = (req, res, next) => {
}
- 通过 监听 req 对象的data 事件获取客户端发送给服务器的数据
- 当数据量大的时候,无法一次发完,客户端会把数据切割,分批发送到服务器
- data 事件会被多次触发,每触发一次就对收到的数据进行拼接
let str = ''
req.on('data', (datapart) => {
str += datapart;
})
- 当请求体数据接受完毕,会触发 req 的end事件,在end事件中将req解析为对象格式,并挂载到 req的body上,便于下游中间件使用
req.on('end', ()=>{
console.log(str);
req.body = qs.parse(str);
next();
})
- 将中间函数暴露出去供其他模块使用
module.exports = bodyParse
- 在其他模块导入中间件,并注册中间件
const myBodyParse = require('./my-body-parse');
app.use(myBodyParse);
// 在其他路由上获取
app.post('/user', (req, res) =>{
res.send(req.body)
})
Express 写接口
- 创建基本的服务器
const express = require('express');
const app = express();
app.listen(80, ()=>{
console.log('run') ;
});
- 新建一个文件 创建 api 路由模块
const express = require('express');
const apiRouter = express.Router();
// 绑定路由
...
module.exports = apiRouter;
- 导入路由并注册路由模块
const apiRouter = require('./apiRouter.js')
app.use('api', apiRouter);
- 编写 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 等攻击
- 接口调用:
XMLHttpRequest
和Fetch
都遵循同源策略
浏览器限制跨域请求一般有两种方式
- 浏览器限制发送跨域请求
- 跨域请求可以正常发起,但是返回的结果被浏览器拦截了
一般浏览器都是第二种方式进行跨域请求的限制,那么请求就已经到达了服务器,可能对数据库的数据进行了操作,但是返回的结果被浏览器拦截了,那么我们就获取不到返回的结果。这是一个失败的请求,但是对数据库里的数据产生了影响。
为了防止对数据操作了,但返回的结果被浏览器拦截,导致无法获取的问题。规范要求,这种可能对服务器数据产生操作的 HTTP
方法,浏览器必须使用 OPTIONS
方法发起一个预检请求,从而获知服务器是否支持跨域请求;如果允许,再发送携带数据的真实请求;如果不允许,则阻止发送带数据的请求;
预检请求
前端 | 浅谈preflight request - 简书 (jianshu.com)
HTTP
请求包括:简单请求和 需要预检的请求
- 简单请求
使用下列方法之一:
- 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)
- 需要预检的请求
- 使用了下面任意
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
原理:
利用了 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})`);
});