加载页面中...
NodeJS–02 | lwstkhyl

NodeJS--02

b站课程尚硅谷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.jsonmain属性对应的文件,如果存在则导入这些文件,如果不存在其中某个文件则报错

  • 如果没有package.jsonpackage.json中没有main属性,则会尝试导入文件夹中index.jsindex.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.exportsexportsrequire这些都是CommonJS模块化规范中的内容,而Node.js是CommonJS模块化规范的具体实现。Nodejs与CommonJS的关系类似于JavaScript与ECMAScript

包管理工具

npm

npm是nodejs内置的包管理工具,安装nodejs时会默认安装npm

初始化

创建一个空文件夹,在其中打开命令行,输入npm init,其作用是将该文件夹变成我们自己创建的一个包,并引导我们创建package.json文件,它是包的配置文件,每个包都必须有

npm1

从上到下依次为包名称版本号入口点包的描述测试命令git仓库关键字作者名字开源证书,括号内的为默认值,按回车即可使用默认值,最后输入yes即可创建一个package.json文件

注意:

  • 包名不能用中文、大写字母,默认是文件夹名称,因此文件夹名称也不能是中文、大写字母

  • 版本号要用x.x.x的形式,x必须是数字

  • package.json也可手动创建并修改

  • 使用npm init -y可以快速创建package.json

  • 更多关于开源证书

搜索与下载包

搜索包

  • 命令行中输入npm s 关键字

    npm2

  • 通过npm官网搜索

    npm3

安装包

  • npm i 包名,如npm i uniq安装uniq包

  • npm i 包名@版本号,如npm i jquery@1.11.2安装1.11.2版本的jquery包

运行后文件夹下会增加两个资源:

  • node_modules文件夹:存放下载的包

  • package-lock.json包的锁文件:用来锁定包的版本,确保每次安装时包的版本相同

注意:安装包前需要初始化该文件夹

以安装uniq包为例

npm4

在文件夹下新建一个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.jsonmain属性对应的js文件,将这个文件引入

  • 在上级目录的node_modules文件夹中使用上面的方法找,直到磁盘根目录

例:对于如图所示的文件结构

npm5

在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程序

npm6

npm8

启动一个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再重启

npm7


如果执行nodemon命令时报错,可以修改Windows执行策略:

  • 以管理员身份打开命令行

  • 输入命令set-ExecutionPolicy remoteSigned

还可以将vscode的终端由powershell改成cmd

npm9

还可以在nodemon命令前加上npx:npx nodemon js文件

删除包
  • 局部删除:npm remove 包名

  • 全局删除:npm remove -g 包名

注:remove可简写为r

例:删除jquery包

npm10

删除后,可以看到package.json.package-lock.json发生了改变

生产环境与开发环境

开发环境:写代码的环境,其中的项目只能编写者自己访问

生产环境:项目代码正式运行的环境,一般指服务器,每个客户都可访问

开发依赖:只在开发阶段使用的依赖。例如将.less转为.cssless包,只需在开发阶段使用

生产依赖:既在开发阶段使用,也在最终运行阶段使用。例如提供jq功能的jquery包,在全程都需要

类型 命令 补充
生产依赖 npm i -S 包名 -S相当于--save,是默认值
包信息保存在package.jsondependencies属性
开发依赖 npm i -D 包名 -D相当于--save-dev,是默认值
包信息保存在package.jsondevDependencies属性

这两种依赖都会存入node_modules文件夹中,都是通过require引入

安装依赖

npm i:根据文件夹中的package.jsonpackage-lock.json安装项目依赖

一般情况下,代码仓库中都不会包含node_modules文件夹,因为它文件体积大、数量多,不方便git上传

当克隆一个nodejs仓库时,要首先执行npm i下载依赖,否则代码无法正常执行

配置命令别名

package.json"scripts"属性中新增键值对:命令别名: "命令",可以配置多个

在终端中使用命令别名执行命令:npm run 命令别名

例:为node ./index.js设置命令别名server

npm11

npm12

一个特殊的命令别名:start,它在终端使用时可省略run,直接npm start

npm13

注:

  • npm run自动向上寻找的特性,即如果当前文件夹没有package.json,它就会向上一级目录中找,类似于require

  • 对于陌生的项目,可以通过查看package.json"scripts"属性来获取项目的一些操作

cnpm及配置镜像

是一个淘宝构建的npm镜像,网址,其服务器部署在阿里云服务器上,可提供包下载速度。该网站也可搜索包,但只能搜索具体的包名称,无法给出搜索结果列表(相关包)

它还提供了一个全局工具包cnpm,安装方法:终端执行npm install -g cnpm --registry=https://registry.npmmirror.com

cnpm操作命令大致同npm,如cnpm initcnpm 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 unpublishnpm 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请求、访问路径为’/’时执行

  • 常用请求方法共有三种:getpostall(只要是请求就执行)

  • 路径可以是'/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("服务已经启动");
});

路由1

路由2

路由3->路由4

路由5

获取请求报文参数

含义 语法
请求方法(同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

获取请求报文参数1

获取路由参数

路由参数指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("服务已经启动");
});

获取路由参数1

获取路由参数2

响应设置

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("服务已经启动");
});

响应设置1

为什么中文不会乱码: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/

响应设置2

例3:下载响应,即打开该网址后下载指定文件

const express = require("express");
const app = express();
app.get('/', (req, res) => {
    res.download("../package.json");
});
app.listen(9000, () => {
    console.log("服务已经启动");
});

响应设置3

例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("服务已经启动");
});

响应设置4

例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>

响应设置5

中间件

中间件(Middleware) 本质是一个回调函数,它可以封装公共操作,简化代码,并可以像路由中的回调函数一样访问reqres

分为全局中间件和路由中间件:

  • 全局中间件:每个请求发送到服务端时,都会首先执行它,之后再继续执行路由操作

  • 路由中间件:只有满足某个路由规则才执行

可以理解为一个拦截器

function 中间件函数名(req, res, next){ //声明
    函数体
    next():
}
app.use(全局中间件函数名); //调用全局中间件
app.请求方法(路径, 路由中间件函数名, 回调函数); //调用路由中间件
  • reqres就是请求和响应报文对象

  • 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("服务已经启动");
});

中间件1

例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("服务已经启动");
});

中间件2

中间件3

中间件4


静态资源中间件:通过设定静态资源目录,express框架实现了根据请求的文件路径,自动响应相应文件的功能,同时自动根据文件类型设定content-type,无需自己再拼接路径

app.use(express.static(静态资源目录路径));

例:设定res文件夹为静态资源目录

中间件5

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("服务已经启动");
});

中间件6

中间件7

使用静态资源中间件的注意事项

  • 如果路径为/,则默认响应index.html

    中间件8

  • 如果静态资源与路由规则同时匹配,则谁先声明(代码中顺序谁在上面)就执行谁

    例如对于上面的例子,/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("服务已经启动");
});

获取请求体数据1

点击登录按钮后:

获取请求体数据2

防盗链

防盗链:一个网页下的资源(如图片等)不能在其它网页(域名)下访问

实现:请求头中的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("服务已经启动");
    });
    

    防盗链1

    注:静态资源中间件会自动响应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("服务已经启动");
    });
    

    防盗链2

    注:这里不能使用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_routeradmin_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("服务已经启动");
});

路由模块化1


补充:设置路由前缀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"不可省略,否则无法上传文件内容

文件上传的报文

文件上传1

可以看到整个表单被分隔成了多个部分,上图的乱码部分就是图片的内容,说明文件上传实际上也是发送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文件类型的内容

  • 它们都是对象的形式

文件上传2

例:上传头像和用户名后将其显示在页面中

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("服务已经启动");
});

文件上传3点击提交按钮,

文件上传4

上传的文件都保存在了upload文件夹中:

文件上传5

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);

EJS1

条件渲染
<% 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);
});

EJS2

与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("服务已经启动");
});

EJS3

express-generator

express-generator是一种express应用生成器,它可以快速创建一个应用的骨架

使用npm i -g express-generator安装

它提供了命令express用于创建框架:express -e 文件夹路径表示在指定文件夹下创建框架,-e是添加ejs支持

创建的目录

EJS4

使用

  • 在该文件夹下npm i安装依赖

  • npm start启动服务

    如果想要使用nodemon就更改package.json中的start值

    可以看到入口文件实际是/bin/www

  • 服务启动在http://127.0.0.1:3000/

EJS5

案例:记账本

共有2个页面:

  • 添加账本记录的页面

    案例:记账本1

  • 记账本的展示页面

    案例:记账本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的路径应为/xxxxxx为css和js文件在public文件夹中的路径;注意:这里不能写成./xxx,因为这样会与html和ejs文件的路径进行拼接,导致路径错误

此例中将网盘资源中的index.html文件内容放入/views/list.ejs中,作为记账本的展示页面;create.html文件内容放入/views/create.ejs中,作为添加账本记录的页面;jscss文件夹放入public

最后将create.ejs中引入路径的./改成/

文件结构:

案例:记账本3

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时删除账单

案例:记账本4

思路:给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" }); //删除成功提醒
});