NodeJS--02
JS-NodeJSb站课程尚硅谷Node.js零基础视频教程P67-P129笔记——包与模块化、express框架
写在前面:此笔记来自b站课程尚硅谷Node.js零基础视频教程 P67-P129 / 资料下载 提取码:s3wj / 课上案例
模块化
模块化:将一个复杂的程序文件依据一定规则拆分成多个文件。其中拆分出的每个文件就是一个模块,模块内部数据是私有的,也可以提供接口(暴露数据)让其它模块使用
好处:减少变量命名冲突、高复用性、高维护性
暴露数据
为方便说明,这里定义值名
为某个模块中的值名称,调用值名
为在其它文件中调用该模块中值时使用的值名称
-
module.exports
:-
module.exports = 值名
将指定值暴露除去,此时调用值名与值名相同。在需要调用该值的js文件中,使用const 调用值名=require('模块名')
即可获得该值注:这种方法只能暴露1个值
-
module.exports = {调用值名1:值名1, 调用值名2:值名2, ...}
将多个值暴露出去,调用值名可不写,默认与值名相同。此时调用该模块的方法同前面讲过的模块调用
例:创建
my_func.js
文件作为模块文件//my_func.js function func_a() { console.log("我是a函数"); } module.exports = func_a; //其它js文件调用该函数 const func_a = require('./my_func.js'); func_a(); //我是a函数
//my_func.js function func_a() { console.log("我是a函数"); } function func_b() { console.log("我是b函数"); } module.exports = { func_a, my_func_b: func_b }; //其它js文件调用该函数 const my_func = require('./my_func.js'); my_func.func_a(); //我是a函数 my_func.my_func_b(); //我是b函数
-
-
exports.调用值名 = 值名
这种方法可以暴露多个函数//my_func.js function func_a() { console.log("我是a函数"); } function func_b() { console.log("我是b函数"); } exports.func_a = func_a; exports.my_func_b = func_b; //其它js文件调用该函数 const my_func = require('./my_func.js'); my_func.func_a(); //我是a函数 my_func.my_func_b(); //我是b函数
注意:
-
暴露的数据可以是任意类型的
//my_func.js str = '我是字符串'; num = 100; exports.str = str; exports.num = num; //其它js文件调用该函数 const my_func = require('./my_func.js'); console.log(my_func.str, my_func.num); //我是字符串 100
-
不能使用
exports = 值名
的形式,因为exports
实际上是指向module.exports
的指针,而module.exports
是一个空对象,如果这样写就是将该指针覆盖了,而exports.调用值名 = 值名
则是向module.exports
中添加数据
导入模块
使用require导入模块的注意事项:
-
如果是nodejs内置模块,直接写模块名即可;而对于自己创建的模块,导入时建议写相对路径,且不能省略
./
和../
注:这里的相对路径不受工作目录的影响,都是以文件目录为准
-
js和json文件引入时可以省略后缀(如果同名,优先引入js),json文件引入时会自动转为对象形式。c/c++编写的
.node
扩展文件也可省略后缀(不常用) -
导入未知类型的文件或没有后缀的文件,会默认按js文件处理
导入文件夹:当导入的路径是一个文件夹时,
-
会首先检测文件夹中
package.json
中main
属性对应的文件,如果存在则导入这些文件,如果不存在其中某个文件则报错 -
如果没有
package.json
或package.json
中没有main
属性,则会尝试导入文件夹中index.js
和index.json
,如果不存在这两个文件则报错
//module文件夹下app.js
module.exports = "我是一个模块";
//module文件夹下package.json
{
"main": "./app.js"
}
//其它js文件调用该文件夹
const m = require('./module');
console.log(m); //我是一个模块
//module文件夹下index.js
module.exports = "我是一个index.js";
//其它js文件调用该文件夹
const m = require('./module');
console.log(m); //我是一个index.js
require导入自定义模块的基本流程:
-
将相对路径转为绝对路径,定位文件
-
缓存检测:检测之前有没有导入过这个文件,如果导入过,就直接利用缓存值,不用重复执行这个文件了
-
读取代码
-
将代码封装成一个函数并执行(自执行函数)
-
缓存模块的值
-
返回
module.exports
的值
//module文件夹下index.js
module.exports = "我是一个index.js";
console.log(arguments.callee.toString()); //输出自执行函数内容
//其它js文件调用该文件夹
const m = require('./module');
const m1 = require('./module');
function (exports, require, module, __filename, __dirname) {
module.exports = "我是一个index.js";
console.log(arguments.callee.toString()); //输出自执行函数内容
}
导入了两次模块,只输出了一次自执行函数,这是因为第二次导入模块时使用了第一次导入的缓存
补充:module.exports
、exports
、require
这些都是CommonJS模块化规范中的内容,而Node.js是CommonJS模块化规范的具体实现。Nodejs与CommonJS的关系类似于JavaScript与ECMAScript
包管理工具
npm
npm是nodejs内置的包管理工具,安装nodejs时会默认安装npm
初始化
创建一个空文件夹,在其中打开命令行,输入npm init
,其作用是将该文件夹变成我们自己创建的一个包,并引导我们创建package.json
文件,它是包的配置文件,每个包都必须有
从上到下依次为包名称
、版本号
、入口点
、包的描述
、测试命令
、git仓库
、关键字
、作者名字
、开源证书
,括号内的为默认值,按回车即可使用默认值,最后输入yes即可创建一个package.json
文件
注意:
-
包名不能用中文、大写字母,默认是文件夹名称,因此文件夹名称也不能是中文、大写字母
-
版本号要用
x.x.x
的形式,x必须是数字 -
package.json
也可手动创建并修改 -
使用
npm init -y
可以快速创建package.json
搜索与下载包
搜索包:
-
命令行中输入
npm s 关键字
-
通过npm官网搜索
安装包:
-
npm i 包名
,如npm i uniq
安装uniq包 -
npm i 包名@版本号
,如npm i jquery@1.11.2
安装1.11.2版本的jquery包
运行后文件夹下会增加两个资源:
-
node_modules
文件夹:存放下载的包 -
package-lock.json
包的锁文件:用来锁定包的版本,确保每次安装时包的版本相同
注意:安装包前需要初始化该文件夹
以安装uniq
包为例:
在文件夹下新建一个index.js
:
const uniq = require("uniq"); //导入uniq包
let arr = [1, 2, 1, 2, 3, 5, 5, 1, 4, 4]; //uniq包可以对数组去重
console.log(uniq(arr)); //[ 1, 2, 3, 4, 5 ]
安装uniq
包后,uniq就是我们创建的包的一个依赖包,简称依赖。比如我们创建了一个包A,A中安装了包B,则B是A的一个依赖包,A依赖B
导入包
require导入npm包的基本流程:require("包名")
-
在当前文件夹下的
node_modules
文件夹中找同名文件夹,找到后进入该文件夹,读取其中package.json
中main
属性对应的js文件,将这个文件引入 -
在上级目录的
node_modules
文件夹中使用上面的方法找,直到磁盘根目录
例:对于如图所示的文件结构
在index.js中导入uniq包:
//以下三种方式都可以
const uniq = require("uniq");
const uniq = require('./node_modules/uniq');
const uniq = require('./node_modules/uniq/uniq.js');
在实际操作中,一般都用require("uniq")
这种方式,因为它可以自动搜索js文件的位置
全局安装
npm i -g 包名
安装后,可在任意位置的命令行调用该包
注意:
-
全局安装命令不受工作目录的影响,可在任意位置执行安装命令
-
使用
npm root -g
查看全局安装包 -
不是所有的包都适合全局安装。只有全局类的工具才适合,可通过查看包的官方文档来确定安装方式
-
全局包都是提供命令行命令来执行,不能使用require引入
例:安装nodemon
包,该包提供nodemon
命令,可以自动重启node程序
启动一个HTTP服务:
const http = require('http');
const server = http.createServer((request, response) => { //创建服务对象
response.end('hello http server');
});
server.listen(9000, () => {
console.log("服务已经启动");
});
在终端中输入nodemon js文件路径
此时当这个文件夹下任一js/mjs/cjs/json文件改变时,就会自动重启服务,无需CTRL+c再重启
如果执行nodemon命令时报错,可以修改Windows执行策略:
-
以管理员身份打开命令行
-
输入命令
set-ExecutionPolicy remoteSigned
还可以将vscode的终端由powershell改成cmd
还可以在nodemon命令前加上npx:npx nodemon js文件
删除包
-
局部删除:
npm remove 包名
-
全局删除:
npm remove -g 包名
注:remove
可简写为r
例:删除jquery包
删除后,可以看到package.json
和.package-lock.json
发生了改变
生产环境与开发环境
开发环境:写代码的环境,其中的项目只能编写者自己访问
生产环境:项目代码正式运行的环境,一般指服务器,每个客户都可访问
开发依赖:只在开发阶段使用的依赖。例如将.less
转为.css
的less
包,只需在开发阶段使用
生产依赖:既在开发阶段使用,也在最终运行阶段使用。例如提供jq功能的jquery
包,在全程都需要
类型 | 命令 | 补充 |
---|---|---|
生产依赖 | npm i -S 包名 |
-S 相当于--save ,是默认值包信息保存在 package.json 中dependencies 属性 |
开发依赖 | npm i -D 包名 |
-D 相当于--save-dev ,是默认值包信息保存在 package.json 中devDependencies 属性 |
这两种依赖都会存入node_modules
文件夹中,都是通过require引入
安装依赖
npm i
:根据文件夹中的package.json
和package-lock.json
安装项目依赖
一般情况下,代码仓库中都不会包含node_modules文件夹,因为它文件体积大、数量多,不方便git上传
当克隆一个nodejs仓库时,要首先执行npm i
下载依赖,否则代码无法正常执行
配置命令别名
在package.json
的"scripts"
属性中新增键值对:命令别名: "命令"
,可以配置多个
在终端中使用命令别名执行命令:npm run 命令别名
例:为node ./index.js
设置命令别名server
一个特殊的命令别名:start
,它在终端使用时可省略run
,直接npm start
注:
-
npm run
有自动向上寻找的特性,即如果当前文件夹没有package.json
,它就会向上一级目录中找,类似于require
-
对于陌生的项目,可以通过查看
package.json
的"scripts"
属性来获取项目的一些操作
cnpm及配置镜像
是一个淘宝构建的npm镜像,网址,其服务器部署在阿里云服务器上,可提供包下载速度。该网站也可搜索包,但只能搜索具体的包名称,无法给出搜索结果列表(相关包)
它还提供了一个全局工具包cnpm
,安装方法:终端执行npm install -g cnpm --registry=https://registry.npmmirror.com
cnpm
操作命令大致同npm
,如cnpm init
、cnpm i 包名
等等
npm使用淘宝镜像,有两种方法:
-
直接配置:终端执行
npm config set registry https://registry.npmmirror.com/
-
使用
nrm
配置(推荐):终端执行以下代码-
安装nrm:
npm i -g nrm --registry=https://registry.npmmirror.com
-
设置镜像:
nrm use taobao
-
检查是否配置成功:
npm config list
,查看registry地址是否为https://registry.npmmirror.com/
补充:
-
nrm ls
:查看有哪些镜像地址可用 -
nrm use npm
:切换回原镜像npm,因为淘宝镜像只能下载,不能上传。如果想上传自己的包,就需要切换成npm镜像后上传
-
注:以上两种方法都是利用淘宝镜像下载包,但第二种方法,即配置npm的淘宝镜像更常用
发布包
将自己开发的包发布到npm上:
-
创建文件夹,并创建文件
index.js
,在文件中声明函数,并暴露 -
npm初始化文件夹,填写包信息
注:包名称不能带有
test
这类文字,官方有检测机制,会清除这类测试包package.json
中”main”属性对应着暴露函数的那个js文件 -
npm官网注册账号
-
将镜像修改为官方镜像
-
终端
npm login
登录npm账号 -
终端
npm publish
发布包如果发布失败,可能是包名称已被占用
更新包:
-
修改
package.json
中”version”版本号属性 -
终端
npm publish
更新包
删除包:终端npm unpublish
或npm unpublish --force
,需满足一定的条件
-
是包的作者
-
发布小于24小时
-
如果发布大于24小时:没有其它包依赖这个包,每周<300下载量,只有一个维护者
yarn
也是一个包管理工具
可以使用npm安装yarn:npm i -g yarn
常用命令:
-
初始化
yarn init
-
安装包
-
生产依赖
yarn add 包名
-
开发依赖
yarn add 包名 --dev
-
全局安装
yarn global add 包名
-
-
删除包
-
项目依赖
yarn remove 包名
-
全局删除
yarn global remove 包名
-
-
安装项目依赖
yarn
-
运行命令别名
yarn 命令别名
(不需添加run)
注:使用yarn
安装的全局包,需要为其安装路径配置环境变量
配置淘宝镜像:yarn config set registry https://registry.npmmirror.com/
可以通过yarn config list
查看配置项
如何选用npm和yarn:
-
个人项目:都可以
-
其它项目:根据锁文件判断使用的包管理工具
-
npm的锁文件为
package-lock.json
-
yarn的锁文件为
yarn.lock
-
注意:包管理工具不能混用
nvm
是管理nodejs版本的工具,方便切换不同版本的nodejs
常用命令:
-
nvm list available
显示所有可下载的nodejs版本 -
nvm list
显示已安装的版本 -
nvm install 版本号
安装指定版本的nodejs,如nvm install 18.12.1
-
nvm install latest
安装最新版的nodejs -
nvm uninstall 版本号
删除指定版本的nodejs -
nvm use 版本号
使用指定版本的nodejs
express框架
是一个web开发框架,官网(https://www.expressjs.com.cn/)
使用npm i express
安装
基本使用:
const express = require("express"); //导入模块
const app = express(); //创建应用对象
app.get('/home', (req, res) => { //get路由
res.end('hello express');
});
app.listen(9000, () => { //启动监听
console.log("服务已经启动");
});
注意:后续操作都是在应用对象app
上进行的
路由
路由确定了应用程序如何响应客户端对特定端点的请求。简单的说,当请求从浏览器发送到服务端后,路由决定这个请求怎样处理
一个路由由三部分组成:请求方法、路径、回调函数
方法:app.请求方法(路径, 回调函数)
,例如app.get('/', ()=>{})
当使用get请求、访问路径为’/’时执行
-
常用请求方法共有三种:
get
、post
、all
(只要是请求就执行) -
路径可以是
'/xxx'
类型的,也可以是'*'
,表示任意路径,一般放在最后,作为404页面
例:
const express = require("express");
const app = express();
app.get('/', (req, res) => {
res.end('home');
});
app.post('/login', (req, res) => {
res.end('login');
})
app.all('/test', (req, res) => {
res.end('test');
})
app.all('*', (req, res) => {
res.end('404 not found');
})
app.listen(9000, () => {
console.log("服务已经启动");
});
->
获取请求报文参数
含义 | 语法 |
---|---|
请求方法(同http模块) | req.method |
HTTP版本 (同http模块) | req.httpVersion |
请求路径(同http模块) | req.url |
请求头(同http模块) | req.headers |
查询字符串(express独有) | req.query |
路径(express独有) | req.path |
ip(express独有) | req.ip |
获取指定的请求头(express独有) | req.get(请求头名) |
例:
const express = require("express");
const app = express();
app.get('/', (req, res) => {
//原生操作
console.log("请求方法:" + req.method);
console.log("HTTP版本:" + req.httpVersion);
console.log("请求路径:" + req.url);
console.log("请求头:", req.headers);
//express独有操作
console.log("查询字符串:", req.query);
console.log("路径:" + req.path);
console.log("ip:" + req.ip);
console.log("host请求头:" + req.get("host"));
res.end('home');
});
app.listen(9000, () => {
console.log("服务已经启动");
});
url为http://127.0.0.1:9000/?a=100&b=200
获取路由参数
路由参数指url路径中的参数(每个/
后面的)
一个需求:有一个商品列表,每个商品的详情页的路径都是/数字.html
的格式(如/1234.html
),要响应所有这些路径,并获取这个数字(商品id)
方法:使用/:id.html
作为路径,id
是占位符,可以任取名,:
表示任意字符。在回调函数通过req.params.占位符
获取占位符代表的内容
const express = require("express");
const app = express();
app.get('/:id.html', (req, res) => {
console.log(req.params.id);
res.end('111');
});
app.listen(9000, () => {
console.log("服务已经启动");
});
http://127.0.0.1:9000/abc.html
->abc
http://127.0.0.1:9000/1234567.html
->1234567
练习:根据路由参数响应歌手的信息
路径为/singer/歌手id.html
,给定json文件,格式为
{
"singers": [
{
"singer_name": "周杰伦",
"singer_pic": "http://y.gtimg.cn/music/photo_new/T001R150x150M0000025NhlN2yWrP4.webp",
"other_name": "Jay Chou",
"singer_id": 4558,
"id": 1
}, ...]
}
并在页面中显示歌手的姓名和图片
const express = require("express");
const app = express();
const { singers } = require("./singers.json"); //导入json文件
app.get('/singer/:id.html', (req, res) => {
const { id } = req.params; //获取路径中的id
const singer = singers.find(item => { //在数组中根据id进行寻找
return item.id === Number(id);
});
if (!singer) {
res.statusCode = 404;
res.end("<h1>404 not found");
return;
}
res.end(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h2>${singer.singer_name}</h2>
<img src="${singer.singer_pic}">
</body>
</html>`);
});
app.listen(9000, () => {
console.log("服务已经启动");
});
响应设置
express中设置相应的方式完全兼容http模块:
作用 | 语法 |
---|---|
设置响应状态码 | res.statusCode = 状态码 |
设置响应状态描述 | res.statusMessage = 描述 |
设置响应头信息 | res.setHeader('头名', '头值') |
设置响应体 | res.write('xxx') res.end('xxx') |
express独有的一些常用相应方法:
作用 | 语法 |
---|---|
设置响应状态码 | res.status(状态码) |
设置响应头信息 | res.set('头名', '头值') |
设置响应体 | res.send('中文响应不乱码') |
重定向 | res.redirect(网址) |
下载响应 | res.download(文件路径) |
json响应 | res.json(对象) |
响应文件内容 | res.sendFile(文件绝对路径) |
注:express支持链式调用,例如res.status(500).set("xxx", "yyy").send("我是响应体")
例1:
const express = require("express");
const app = express();
app.get('/', (req, res) => {
res.status(500);
res.set("xxx", "yyy");
res.send("中文不会乱码");
//等效于:
//res.status(500).set("xxx", "yyy").send("中文不会乱码");
});
app.listen(9000, () => {
console.log("服务已经启动");
});
为什么中文不会乱码:send
函数自动设置了utf-8编码
例2:重定向
const express = require("express");
const app = express();
app.get('/', (req, res) => {
res.redirect("http://www.baidu.com");
});
app.listen(9000, () => {
console.log("服务已经启动");
});
输入http://127.0.0.1:9000/
后
例3:下载响应,即打开该网址后下载指定文件
const express = require("express");
const app = express();
app.get('/', (req, res) => {
res.download("../package.json");
});
app.listen(9000, () => {
console.log("服务已经启动");
});
例4:json响应,将json响应给网页,自动设定content-type
为json格式
const express = require("express");
const app = express();
app.get('/', (req, res) => {
res.json({
name: 'abc',
age: 19
});
});
app.listen(9000, () => {
console.log("服务已经启动");
});
例5:响应文件内容,即把一个文件中的内容响应给网页
const express = require("express");
const app = express();
app.get('/', (req, res) => {
res.sendFile(__dirname + "/test.html");
});
app.listen(9000, () => {
console.log("服务已经启动");
});
test.html:
<body>
<form action="http://127.0.0.1:9000/login" method="post">
<input type="text" name="xxx">
<input type="submit" value="submit">
</form>
</body>
中间件
中间件(Middleware) 本质是一个回调函数,它可以封装公共操作,简化代码,并可以像路由中的回调函数一样访问req
和res
分为全局中间件和路由中间件:
-
全局中间件:每个请求发送到服务端时,都会首先执行它,之后再继续执行路由操作
-
路由中间件:只有满足某个路由规则才执行
可以理解为一个拦截器
function 中间件函数名(req, res, next){ //声明
函数体
next():
}
app.use(全局中间件函数名); //调用全局中间件
app.请求方法(路径, 路由中间件函数名, 回调函数); //调用路由中间件
-
req
和res
就是请求和响应报文对象 -
next
代表后续的函数,包括路由操作等,这里需要调用它,从而让后续代码执行
例1:全局中间件——将每次访问的url和IP存入文件中
const express = require("express");
const fs = require("fs");
const path = require("path");
const app = express();
function recordMiddleware(req, res, next) {
const { url, ip } = req;
const file_path = path.resolve(__dirname, "./access.log");
fs.appendFileSync(file_path, `${url} ${ip}\r\n`); //写入文件
next(); //调用next
}
app.use(recordMiddleware); //调用中间件
app.get('/', (req, res) => {
res.send("首页");
});
app.all('*', (req, res) => {
res.send("<h1>404 not found</h1>");
});
app.listen(9000, () => {
console.log("服务已经启动");
});
例2:路由中间件——对于/admin
和/setting
的请求,要求url携带code=521
参数,否则提示“暗号错误”
const express = require("express");
const app = express();
function check_code_middleware(req, res, next) {
if (req.query.code === '521') {
next(); //执行每个路由内的函数
} else {
res.send("暗号错误"); //退出
}
}
app.get('/admin', check_code_middleware, (req, res) => {
res.send("后台首页");
});
app.get('/setting', check_code_middleware, (req, res) => {
res.send("设置页面");
});
app.all('*', (req, res) => {
res.send("<h1>404 not found</h1>");
});
app.listen(9000, () => {
console.log("服务已经启动");
});
静态资源中间件:通过设定静态资源目录,express框架实现了根据请求的文件路径,自动响应相应文件的功能,同时自动根据文件类型设定content-type
,无需自己再拼接路径
app.use(express.static(静态资源目录路径));
例:设定res文件夹为静态资源目录
const express = require("express");
const app = express();
app.use(express.static(__dirname + "/res"));
app.all('*', (req, res) => {
res.send("<h1>404 not found</h1>");
});
app.listen(9000, () => {
console.log("服务已经启动");
});
使用静态资源中间件的注意事项:
-
如果路径为
/
,则默认响应index.html
-
如果静态资源与路由规则同时匹配,则谁先声明(代码中顺序谁在上面)就执行谁
例如对于上面的例子,
/index.css
既满足静态资源路径,又符合'*'
路由规则,但因为先声明的静态资源,所以响应index.css文件 -
一般来讲,路由响应动态资源,而静态资源中间件响应静态资源
获取请求体数据
使用body-parser
包:npm i body-parser
const body_parser = require("body-parser"); //导入包
const json_parser = body_parser.json(); //解析json格式请求体的中间件
const urlencoded_parser = body_parser.urlencoded({extended:false}); //解析查询字符串格式请求体的中间件
//这两种中间件可以全局/路由中间件的形式调用,推荐使用路由中间件
app.请求方法(路径, urlencoded_parser/json_parser, (req, res) => { //根据请求体选择中间件调用
const body = req.body; //请求体(一个对象,键值对应查询字符串/json对象的键值)
});
例:搭建HTTP服务/login
-
如果是get请求,显示表单,可发送post请求
-
如果是post请求,获取表单中的用户名和密码
form.html:
<h2>登录</h2>
<form action="/login" method="post">
用户名:<input type="text" name="username"><br>
密码:<input type="password" name="password"><br>
<input type="submit" value="登录">
</form>
js文件:
const express = require("express");
const body_parser = require("body-parser");
const app = express();
const json_parser = body_parser.json();
const urlencoded_parser = body_parser.urlencoded({ extended: false });
app.get('/login', (req, res) => { //get--响应HTML文件
res.sendFile(__dirname + "/form.html");
});
app.post('/login', urlencoded_parser, (req, res) => { //post--输出用户名和密码
const { username, password } = req.body;
res.send(`<h3>用户名:${username}</h3><h3>密码:${password}</h3>`)
});
app.listen(9000, () => {
console.log("服务已经启动");
});
点击登录按钮后:
防盗链
防盗链:一个网页下的资源(如图片等)不能在其它网页(域名)下访问
实现:请求头中的referer
标识发送请求的网页网址
例:只允许127.0.0.1
域名访问图片,而localhost
不可以
注:localhost
也是一种域名,在访问网址时与127.0.0.1
效果相同,但它们是两种不同的域名
-
不作防盗链处理:
index.html:
<img src="http://127.0.0.1:9000/test.png" alt="" style="width: 100px;">
js文件:
const express = require("express"); const app = express(); app.use(express.static(__dirname)); //设置静态资源目录 app.listen(9000, () => { console.log("服务已经启动"); });
注:静态资源中间件会自动响应
index.html
-
使用中间件作防盗链处理:
js文件:
const express = require("express"); const app = express(); app.use((req, res, next) => { const referer = req.get("referer"); //获取referer,它是一个完整的网址 if (referer) { //第一次发送请求没有referer,如果没有referer就跳过判断 const url = new URL(referer); //实例化url对象 const hostname = url.hostname; //获取域名 if (hostname !== '127.0.0.1') { //判断是不是127.0.0.1 res.status(404).send(""); //不是就返回404 return; } } next(); }); app.use(express.static(__dirname)); //设置静态资源目录 app.listen(9000, () => { console.log("服务已经启动"); });
注:这里不能使用
app.get("/",...)
响应html文件,因为这样localhost
响应的网页的域名也是127.0.0.1
,无法通过防盗链拦截
路由模块化
即对路由函数进行拆分,存入多个js文件中
路由子文件:
const router = express.Router(); //创建router对象
router.请求方法(路径, 中间件, 回调函数); //添加路由
...
module.exports = router; //暴露
注:路由对象可以理解成是一个小型的app对象
服务端js文件:
const 路由子文件模块名 = require(路由子文件);
app.use(路由子文件模块名);
...
注:可以导入有多个路由子文件,每个路由子文件中可以有多个路由
例:创建两个路由子文件home_router
和admin_router
,并在01.js
中调用
home_router:
const express = require("express");
const router = express.Router();
router.get('/home', (req, res) => {
res.send("前台首页");
});
router.get('/search', (req, res) => {
res.send("内容搜索");
});
module.exports = router;
admin_router:
const express = require("express");
const router = express.Router();
router.get('/admin', (req, res) => {
res.send("后台首页");
});
router.get('/setting', (req, res) => {
res.send("设置页面");
});
module.exports = router;
01.js:
const express = require("express");
const home_router = require("./router/home_router");
const admin_router = require("./router/admin_router");
const app = express();
app.use(home_router);
app.use(admin_router);
app.all("*", (req, res) => {
res.send("<h3>404 not found</h3>");
});
app.listen(9000, () => {
console.log("服务已经启动");
});
补充:设置路由前缀app.use(路由前缀, 路由子文件)
作用是给路由子文件中所有路由函数的路径参数添加前缀
路由子文件:
router.get('/', (req, res) => {});
router.get('/setting', (req, res) => {});
添加前缀:
app.use('/admin', home_router);
此时实际的路由路径分别是
-
路由子文件的
'/'
->实际网址'/admin'
-
路由子文件的
'/setting'
->实际网址'/admin/setting'
文件上传
即用户可以选择自己电脑中的文件上传给服务器,比如更换头像、网盘的上传文件等等
HTML中上传文件的表单:
<form action="网址" method="post" enctype="multipart/form-data">
<input type="file" name="file_upload">
</form>
其中enctype="multipart/form-data"
不可省略,否则无法上传文件内容
文件上传的报文:
可以看到整个表单被分隔成了多个部分,上图的乱码部分就是图片的内容,说明文件上传实际上也是发送HTTP请求报文
处理上传文件:使用包formidable
(2.x的旧版本),使用npm install formidable@v2
进行安装
//在post请求内:
const form = formidable({
multiples: true,
uploadDir: __dirname + '/upload', //设置上传文件的保存路径
keepExtensions: true //保持文件后缀
});
form.parse(req, (err, fields, files) => {
if (err) {
next(err);
return;
}
console.log(fields);
console.log(files);
});
-
fields
存储一般字段,即不是文件的内容,如文本框、单选框、多选框等 -
files
文件类型的内容 -
它们都是对象的形式
例:上传头像和用户名后将其显示在页面中
index.html:
<form action="/" method="post" enctype="multipart/form-data">
用户名:<input type="text" name="username"><br><br>
头像:<input type="file" name="portrait"><br><br>
<input type="submit" value="提交">
</form>
js文件:
const express = require("express");
const formidable = require("formidable");
const app = express();
app.use(express.static(__dirname));
app.post("/", (req, res) => { //处理上传的文件
const form = formidable({
multiples: true,
uploadDir: __dirname + '/upload', //设置上传文件的保存路径
keepExtensions: true //保持文件后缀
});
form.parse(req, (err, fields, files) => {
if (err) {
res.send("上传失败").status(404);
return;
}
const url = '/upload/' + files.portrait.newFilename; //获取文件保存位置的完整url,保存在数据库中
res.send(`
<p>用户名:${fields.username}</p><br><br>
<img src="${url}" style="width:100px;">
`);
});
});
app.listen(9000, () => {
console.log("服务已经启动");
});
点击提交按钮,
上传的文件都保存在了upload文件夹中:
EJS
EJS是一个js的模板引擎,用于动态渲染HTML页面
模板引擎:一种分离用户界面(HTML)和业务数据(服务端JS)的技术
使用npm i ejs
安装
HTML语法及js调用:
<% js语句 %>
<%= 变量名 %>
const ejs = require("ejs");
const res = ejs.render(HTML字符串, {变量名:变量值, ...});
html中的<%= 变量名 %>
会被对应的变量值代替
列表渲染
<% list.forEach(item => { %>
<%= item %>
<% }) %>
例:给定字符串数组,要求把每个元素放入li标签内
原生js:
const names = ['abc', 'bcd', 'cde', 'def'];
let str = '<ul>';
names.forEach(name => {
str += `<li>${name}</li>`;
});
str += '</ul>';
console.log(str);
ejs:
<ul>
<% list.forEach(item => { %>
<li><%= item %></li>
<% }) %>
</ul>
const ejs = require("ejs");
const fs = require("fs");
const names = ['abc', 'bcd', 'cde', 'def']; //数据
const html = fs.readFileSync("./index.html").toString(); //获取HTML文件内容
const res = ejs.render(html, { names: names });
console.log(res);
条件渲染
<% if(条件){ %>
<% 语句 %>或<%= 变量 %>或HTML标签
<% }else{ %>
<% 语句 %>或<%= 变量 %>或HTML标签
<% } %>
例:根据num
变量值决定最终输出内容
const ejs = require("ejs");
const nums = [1, 2, 3]; //数据
const html = `
<% if(num===1){ %>
<span>欢迎回来</span>
<% }else if(num===2){ %>
<button>登录</button>
<% }else{ %>
<button>注册</button>
<% } %>
`;
nums.forEach(num => {
console.log('num:', num);
const res = ejs.render(html, { num: num });
console.log(res);
});
与express结合使用
app.set("view engine", "ejs"); //标明使用了哪种模板引擎
app.set("views", 文件路径); //标明模板文件存放位置
app.请求方法(路径, (req, res)=>{
res.render("模板文件名(不包含后缀)", {变量名:变量值, ...});
});
注:
-
模板文件就是具有模板语法内容的文件
-
模板文件名要以
.ejs
为后缀
例:
test.ejs:
<h2>
<%= title %>
</h2>
test.js:
const express = require("express");
const ejs = require("ejs");
const app = express();
app.set("view engine", "ejs");
app.set("views", __dirname);
app.get("/", (req, res) => {
const title = '我是标题';
res.render('test', { title: title });
});
app.listen(9000, () => {
console.log("服务已经启动");
});
express-generator
express-generator是一种express应用生成器,它可以快速创建一个应用的骨架
使用npm i -g express-generator
安装
它提供了命令express
用于创建框架:express -e 文件夹路径
表示在指定文件夹下创建框架,-e
是添加ejs支持
创建的目录:
使用:
-
在该文件夹下
npm i
安装依赖 -
npm start
启动服务如果想要使用nodemon就更改package.json中的start值
可以看到入口文件实际是
/bin/www
-
服务启动在
http://127.0.0.1:3000/
上
案例:记账本
共有2个页面:
-
添加账本记录的页面
-
记账本的展示页面
使用express-generator
创建框架结构:express -e 02-04记账本案例
;之后进入该文件夹内,npm i
安装依赖;更改package.json
中的start
属性为"nodemon ./bin/www"
(使用nodemon运行)
基本路由结构:
-
create.js
添加账本记录的页面 -
account.js
记账本的展示页面
app.js:
//更改相关路由名称设置
var accountRouter = require('./routes/account');
var createRouter = require('./routes/create');
app.use('/account', accountRouter);
app.use('/create', createRouter);
响应静态页面
在views
文件夹下存放html或ejs文件,在public
文件夹下存放css和js文件
html和ejs文件中引入css和js的路径应为/xxx
,xxx
为css和js文件在public
文件夹中的路径;注意:这里不能写成./xxx
,因为这样会与html和ejs文件的路径进行拼接,导致路径错误
此例中将网盘资源中的index.html
文件内容放入/views/list.ejs
中,作为记账本的展示页面;create.html
文件内容放入/views/create.ejs
中,作为添加账本记录的页面;js
和css
文件夹放入public
中
最后将create.ejs
中引入路径的./
改成/
文件结构:
create.js:
const express = require('express');
const router = express.Router();
router.get('/', function (req, res, next) {
res.render('create');
});
module.exports = router;
account.js:
const express = require('express');
const router = express.Router();
router.get('/', function (req, res, next) {
res.render('list');
});
module.exports = router;
获取表单数据
首先更改表单,为表单添加action跳转链接,并给每个输入框/选项框命名(添加name或value属性)
<form method="post" action="/account">
<input name="title" type="text" class="form-control" id="item" />
<input name="time" type="text" class="form-control" id="time" />
<select name="type" class="form-control" id="type">
<option value="-1">支出</option>
<option value="1">收入</option>
</select>
<input name="account" type="text" class="form-control" id="account" />
<textarea name="remarks" class="form-control" id="remarks"></textarea>
</form>
因为express-generator
框架已经添加了获取请求体数据的中间件,这里可以直接使用req.body
获取
router.post('/', function (req, res) {
console.log(req.body);
res.send('添加记录');
});
lowdb包保存信息
相当于一个简单的数据库,以json文件的格式存储数据
使用npm i lowdb@1.0.0
安装
//导入包
const low = require("lowdb");
const FileSync = require("lowdb/adapters/FileSync");
const adapter = new FileSync("db.json"); //存储的json文件名
//获取db对象
const db = low(adapter);
//初始化数据
db.defaults({
键名1: [], //数组形式,可以向里面添加多个对象
键名2: {} //对象形式,可以向里面添加键值对(单个对象)
, ...
}).write();
db.get("键名").push(对象).write(); //向键中添加数据(添加在最后面)
db.get("键名").unshift(对象).write(); //向键中添加数据(添加在最前面)
db.get("键名").remove(对象).write(); //删除指定条件的数据,返回删除掉的那个对象
console.log(db.get("键名").find(对象).value()); //获取指定条件的数据
console.log(db.get("键名").value()); //获取键值
db.get("键名").find(对象).assign(新对象).write(); //更改
注:删除和查询中使用的对象不一定要写全,比如在key
键中有{id:1, name:"abc"}
和{id:2, name:"bcd"}
两个对象,可以只写db.get("key").find({id:1}).value()
,就获取到id为1的那个对象{id:1, name:"abc"}
实际操作中,一般手动完成初始化,否则初始化代码会被重复执行:新建文件夹data
,其中新建文件db.json
用于存储数据
{
"accounts": []
}
在post请求路由中使用
db.get("accounts").unshift(req.body).write();
将请求体添加进去。为了方便后续读取时,先展示时间较近(后添加)的数据,使用unshift
添加在最前面
同时为了每条数据都有一个id方便操作,使用shortid
包,npm i shortid
account.js:
//导入lowdb包
const low = require("lowdb");
const FileSync = require("lowdb/adapters/FileSync");
const adapter = new FileSync(__dirname + "/../data/db.json");
const db = low(adapter);
//导入shortid包
const shortid = require("shortid");
//响应表单提交的post请求
router.post('/', function (req, res) {
const id = shortid.generate(); //创建id
db.get("accounts").unshift({ id: id, ...req.body }).write(); //添加数据
res.send('添加记录');
});
添加成功后提醒
即添加成功后响应一个网页,标识添加成功
在views
文件夹中创建success.ejs
,将资料中success.html
文件内容复制过去,更改提醒信息和跳转链接:
<h1>:) <%= msg %></h1>
<p><a href="<%= url %>">点击跳转</a></p>
将account.js
的post请求路由中res.send('添加记录')
改为:
res.render('success', { msg: "添加成功", url: "/account" }); //添加成功提醒
渲染记账列表
使用ejs的列表和条件渲染
list.ejs:
<div class="accounts">
<% accounts.forEach(account=>{ %>
<% if(account.type==='-1'){ %>
<div class="panel panel-danger">
<div class="panel-heading"><%= account.time %></div>
<div class="panel-body">
<div class="col-xs-6"><%= account.title %></div>
<div class="col-xs-2 text-center">
<span class="label label-warning">支出</span>
</div>
<div class="col-xs-2 text-right"><%= account.account %></div>
<div class="col-xs-2 text-right">
<span class="glyphicon glyphicon-remove" aria-hidden="true"></span>
</div>
</div>
</div>
<% }else{ %>
<div class="panel panel-success">
<div class="panel-heading"><%= account.time %></div>
<div class="panel-body">
<div class="col-xs-6"><%= account.title %></div>
<div class="col-xs-2 text-center">
<span class="label label-success">收入</span>
</div>
<div class="col-xs-2 text-right"><%= account.account %></div>
<div class="col-xs-2 text-right">
<span class="glyphicon glyphicon-remove" aria-hidden="true"></span>
</div>
</div>
</div>
<% } %>
<% }) %>
</div>
将account.js
的get请求路由中res.render('list')
改为:
res.render('list', { accounts: accounts }); //渲染账单
删除账单
点击账单中的x
时删除账单
思路:给x
添加链接,将id传入链接路径中,之后用get请求路由接收这个链接,并用params
获取这个id,进行删除。相当于静态网页中用自定义属性给标签设置id,js中获取自定义属性进行删除
更改list.ejs
中<span class="glyphicon glyphicon-remove" aria-hidden="true"></span>
标签为
<a href="/account/<%= account.id %>">
<span class="glyphicon glyphicon-remove" aria-hidden="true"></span>
</a>
account.js:
router.get('/:id', function (req, res) {
const id = req.params.id; //获取id
db.get("accounts").remove({ id: id }).write(); //删除数据
res.render('success', { msg: "删除成功", url: "/account" }); //删除成功提醒
});