
『包』英文单词是 package ,代表了一组特定功能的源码集合。
管理『包』的应用软件,可以对「包」进行 下载安装 , 更新 ,删除 , 上传 等操作。
借助包管理工具,可以快速开发项目,提升开发效率。
包管理工具是一个通用的概念,很多编程语言都有包管理工具,所以 掌握好包管理工具非常重要。
下面列举了前端常用的包管理工具
npm 全称 Node Package Manager ,翻译为中文意思是『Node 的包管理工具』
npm 是 node.js 官方内置的包管理工具,是 必须要掌握住的工具
node.js 在安装时会 自动安装 npm ,所以如果你已经安装了 node.js,可以直接使用 npm
可以通过 npm -v 查看版本号测试,如果显示版本号说明安装成功,反之安装失败
创建一个空目录,然后以此目录作为工作目录 启动命令行工具 ,执行 npm init
npm init 命令的作用是将文件夹初始化为一个『包』,交互式创建 package.json 文件
package.json 是包的配置文件,每个包都必须要有 package.json
package.json 内容示例:
xxxxxxxxxx111{2 "name": "01_npm",3 "version": "1.0.0",4 "description": "",5 "main": "index.js",6 "scripts": {7 "test": "echo \"Error: no test specified\" && exit 1"8 },9 "author": "",10 "license": "ISC"11}属性翻译:
xxxxxxxxxx111{2 "name": "1-npm", #包的名字3 "version": "1.0.0", #包的版本4 "description": "", #包的描述5 "main": "index.js", #包的入口文件6 "scripts": { #脚本配置7 "test": "echo \"Error: no test specified\" && exit 1"8 },9 "author": "", #作者10 "license": "ISC" #开源证书11}初始化的过程中还有一些注意事项:
- package name (
包名) 不能使用中文、大写,默认值是文件夹的名称,所以文件夹名称也不能使用中文和大写- version (
版本号)要求x.x.x的形式定义,x必须是数字,默认值是1.0.0- ISC 证书与 MIT 证书功能上是相同的,关于开源证书扩展阅读http://www.ruanyifeng.com/blog/2011/05/how_to_choose_free_software_licenses.html
package.json可以手动创建与修改- 使用
npm init -y或者npm init --yes极速创建package.json
搜索包的方式有两种
网站搜索 网址是 https://www.npmjs.com/。推荐使用这种方式。经常有同学问,『我怎样才能精准找到我需要的包?最精准、最高效的解决问题。』
这个事儿需要大家在实践中不断的积累,通过看文章,看项目去学习、去积累。
我们可以通过 npm install 或简写方式 npm i 命令安装包
xxxxxxxxxx71# 格式2npm install <包名>3npm i <包名>4 5# 示例6npm install uniq7npm i uniq运行之后文件夹下会增加两个资源
node_modules 文件夹,用于存放下载的包。package-lock.json 包的锁文件,用来锁定包的版本。比如说,安装 uniq 之后, uniq 就是当前这个包的一个 依赖包(什么叫“当前这个包”?指的就是我使用npm init初始化的这个文件夹,就称为“当前这个包”。也可以理解为我的当前的这个项目) ,有时会简称为 依赖。
举个例子,我们创建一个包名字为 A,A 中安装了包名字是 B,我们就说 B 是 A 的一个依赖包 ,也会说A 依赖 B
在nodejs中使用require导入npm包,直接写包名即可(不用写完整路径):
xxxxxxxxxx11const uniq = require("uniq")虽然Nodejs和第三方库的导入都直接写包名,但vscode的one dark主题对于导入的模块有不同的配色,这一点很好,方便我看一个模块到底是不是Nodejs的官方模块。当然,这个功能可有可无。
require导入npm包的基本流程如下:
开发环境是程序员 专门用来写代码 的环境,一般是指程序员的电脑,开发环境的项目一般 只能程序员自己访问
生产环境是项目 代码正式运行 的环境,一般是指正式的服务器电脑,生产环境的项目一般 每个客户都可以访问
我们可以在安装时设置选项来区分 依赖的类型 ,目前分为两类:
| 类型 | 命令 | 补充 | 备注 |
|---|---|---|---|
| 生产依赖 | npm i -S uniq npm i --save uniq | -S 等效于 --save,-S 是默认选项 包信息保存在 package.json 文件的dependencies 属性中 | 开发和生产阶段都需要 |
| 开发依赖 | npm i -D less npm i --save-dev less | -D 等效于 --save-dev 包信息保存在 package.json 文件的devDependencies 属性中 | 只在开发阶段需要 |
举个例子方便大家理解,比如说做蛋炒饭需要 大米 , 油 , 葱 , 鸡蛋 , 锅 , 煤气 , 铲子 等
其中 锅 , 煤气 , 铲子 属于开发依赖,只在制作阶段使用
而 大米 , 油 , 葱 , 鸡蛋 属于生产依赖,在制作与最终食用都会用到
所以
开发依赖是只在开发阶段使用的依赖包,而生产依赖是开发阶段和最终上线运行阶段都用到的依赖包。
怎么区分要安装的包是开发依赖还是生产依赖呢?其实我还是觉得有点难,上面的像less在打包后会转换为css,所以只需要在开发阶段使用,但别的包,特别是没有使用过的包,我就不知道了。可以在安装之前看一下别人的文档,推荐使用的安装命令是什么,照着文档做就行了,如果文档没有,就大胆使用-S,不要在这一点上纠结。
我们可以执行安装选项 -g 进行全局安装
xxxxxxxxxx11npm i -g nodemon全局安装完成之后就可以在命令行的任何位置运行 nodemon 命令。nodemon可以代替node在命令行中执行命令,可以帮助解决之前的http服务器调试的一个问题:代码更改了之后,需要关闭服务器,重新启动服务器,才能显示最新的效果。而nodemon会自动重启,比如说在命令行运行:nodemon server.js,更改server.js里面的代码并保存,可以看到自动重启了,可以获取最新的代码。

该命令的作用是 自动重启 node 应用程序。
说明:
- 全局安装的命令不受工作目录位置影响。意味着可以在任何工作目录下使用全局命令。
- 全局安装和局部安装的包,存放的位置不一样:局部安装的包,一般存放在所属文件夹下的node_modules中;全局安装的包,可以通过
npm root -g可以查看安装位置。
不是所有的包都适合全局安装,只有全局类的工具才适合,可以通过查看包的官方文档来确定安装方式,这里先不必太纠结。- 全局安装和局部安装的使用方式不一样:全局安装的包,是在命令行中使用,一般都是用它的包名当作全局命令名,比如说:nodemon、webpack、gulp、yarn等;而局部安装的包,是使用require导入,然后使用。
windows 默认不允许 npm 全局命令执行脚本文件,所以需要修改执行策略,这样像nodemon这样的全局命令就可以在任意工作目录的命令行里面执行了。

方法一:
1、以 管理员身份 打开 powershell 命令行
2、键入命令 set-ExecutionPolicy remoteSigned

3、键入 A 然后敲回车 👌
4、如果不生效,可以尝试重启 vscode
方法二:
在cmd中或者在vscode中选择command prompt命令行打开,就可以执行nodemon命令。
Path 是操作系统的一个环境变量,可以设置一些文件夹的路径,在当前工作目录下找不到可执行文件时,就会在环境变量 Path 的目录中挨个的查找,如果找到则执行,如果没有找到就会报错。

补充说明:
如果希望某个程序在任何工作目录下都能正常运行,就应该将该程序的所在目录配置到环境变量 Path 中。
如果希望一些应用程序可以使用cmd输入命令打开,可以找到此程序的安装位置,将安装位置(一般是安装位置的bin文件夹所在地址,bin文件夹里面存放着 .exe 或者 .cmd 后缀的文件,操作系统搜索的就是这些可执行文件。如果没有bin文件夹,一般就在程序的安装包的最外层有这些文件,多试一下就行了)配置到环境变量Path中。
如何才能找到程序的安装位置呢?如果有图标,可以右键→属性→快捷方式→打开文件所在的位置,一般都是将程序的bin文件夹所在位置给复制一份,比如说我安装的vscode位置:D:\Software\developEnvironment\vscode\Microsoft VS Code\bin,我把它粘贴到环境变量Path中。
如果没有图标,就找一下软件的安装位置,一般在C:/Program Files文件夹中可以找到。
在cmd中输入code,正常启动:
命令不是只有单独唤起cmd才能使用(此时的cmd所在目录是
C:\Users\Administrator),而是在任何目录的cmd或powershell下都可以使用,这就非常方便了,因为我可以将gifcam等常用的程序,都设置Path,在做笔记时我就可以在命令行中直接唤起了,省得我每次都要到处找gifcam。
windows 下查找命令的所在位置
- cmd 命令行 中执行
where nodemon- powershell命令行 执行
get-command nodemon
在项目协作中有一个常用的命令就是 npm i ,通过该命令可以依据 package.json 和 package-lock.json 的依赖声明安装项目依赖。
xxxxxxxxxx31npm i2# 或者3npm install
node_modules文件夹大多数情况都不会存入项目的git版本库,因为node_modules文件夹太大了,所以一般都会在.gitignore文件里面忽略掉,不让它上传到git仓库中,所以在协作开发的时候,使用了package.json和package-lock.json来记录使用的第三方库的情况,npm i可以很方便的安装这些记录好的依赖。
项目中可能会遇到版本不匹配的情况,有时就需要安装指定版本的包,可以使用下面的命令。
xxxxxxxxxx51## 格式,这里的 <> 只是表示这个命令是一个整体,在实际使用的时候,不要带上 <> 2npm i <包名@版本号>34## 示例5npm i jquery@1.11.2项目中可能需要删除某些不需要的包,可以使用下面的命令
xxxxxxxxxx61## 局部删除2npm remove uniq3npm r uniq45## 全局删除6npm remove -g nodemon通过配置命令别名可以更简单的执行命令。比如说在一个项目文件夹里面,我们之前学习的时候,是这样启动服务器的:node server.js,其中server.js是项目的主文件,那么通过配置命令别名,当命令非常长的时候,可以简化一些操作,在学习express时,你将会有所体会,因为启动express的命令,可以配置很多参数。
配置 package.json 中的 scripts 属性
xxxxxxxxxx111{2 .3 .4 .5 "scripts": {6 "server": "node server.js",7 "start": "node index.js",8 },9 .10 .11}配置完成之后,可以使用别名执行命令
xxxxxxxxxx21npm run server2npm run start不过 start 别名比较特别,使用时可以省略 run
xxxxxxxxxx11npm start补充说明:
npm start是项目中一个常用的命令,一般用来启动项目npm run有自动向上级目录查找的特性,跟require函数一样- 对于陌生的项目,我们可以通过查看
package.json文件里面的scripts属性来参考项目的一些操作
cnpm 是一个淘宝构建的 npmjs.com 的完整镜像,也称为『淘宝镜像』,网址https://npmmirror.com/
cnpm 服务部署在国内 阿里云服务器上 , 可以提高包的下载速度
官方也提供了一个全局工具包 cnpm ,操作命令与 npm 大体相同
我们可以通过 npm 来安装 cnpm 工具
xxxxxxxxxx11npm install -g cnpm --registry=https://registry.npmmirror.com| 功能 | 命令 |
|---|---|
| 初始化 | cnpm init / cnpm init -y |
| 安装包 | cnpm i uniq cnpm i -S uniq cnpm i -D uniq cnpm i -g nodemon |
| 安装项目依赖 | cnpm i |
| 删除 | cnpm r uniq |
用 npm 也可以使用淘宝镜像,配置的方式有两种
执行如下命令即可完成配置
xxxxxxxxxx11npm config set registry https://registry.npmmirror.com/使用 nrm 配置 npm 的镜像地址 npm registry manager
xxxxxxxxxx11npm i -g nrmxxxxxxxxxx11nrm use taobaoxxxxxxxxxx11npm config list检查 registry 地址是否为 https://registry.npmmirror.com/ , 如果 是 则表明设置成淘宝镜像成功。
xxxxxxxxxx11nrm ls
可以使用nrm use npm将镜像转为npm官方地址。
补充说明:
建议使用第二种方式进行镜像配置,因为后续修改起来会比较方便- 虽然 cnpm 可以提高速度,但是 npm 也可以通过淘宝镜像进行加速,所以
npm 的使用率还是高于 cnpm。建议使用npm配置淘宝镜像,一直使用npm命令。
yarn 是由 Facebook 在 2016 年推出的新的 Javascript 包管理工具,官方网址:https://yarnpkg.com/
yarn 官方宣称的一些特点
我们可以使用 npm 安装 yarn
xxxxxxxxxx11npm i -g yarn| 功能 | 命令 |
|---|---|
| 初始化 | yarn init / yarn init -y |
| 安装包 | yarn add uniq 生产依赖 yarn add less --dev 开发依赖 yarn global add nodemon 全局安装 |
| 删除包 | yarn remove uniq 删除项目依赖包 yarn global remove nodemon 全局删除包 |
| 安装项目所有依赖 | yarn |
| 运行命令别名 | yarn <别名> 不需要添加run |
思考题:
这里有个小问题就是 使用yarn
全局安装的包不可用,yarn 全局安装包的位置可以通过yarn global bin来查看,那你有没有办法使 yarn 全局安装的包能够正常运行?
可以通过如下命令配置淘宝镜像
xxxxxxxxxx11yarn config set registry https://registry.npmmirror.com/可以通过 yarn config list 查看 yarn 的配置项:

我在安装从codeSandbox下载的项目依赖时,使用
yarn,都会报错:
info There appears to be trouble with your network connection. Retrying...。项目使用的就是yarn,所以没有办法换到npm,怎么解决呢?其实我的yarn配置的就是淘宝源,下载应该很快才对啊?
查了一下:https://blog.csdn.net/weixin_43558117/article/details/130343910,由于yarn.lock文件是通过远程抓取的,而不是本地yarn生成的,所以即使设置了淘宝源,在执行
yarn install的时候,走的是别的仓库。可以通过
yarn install --verbose来验证,看是从哪里安装依赖的:
可以看到,安装依赖的时候,走的仍然是官方网址,没有走淘宝源。
怎么解决呢?可以通过忽略lock文件,同时install的时候加registry参数解决:
yarn install --no-lockfile --registry https://registry.npmmirror.com/。
大家可以根据不同的场景进行选择
如果是个人项目, 哪个工具都可以 ,可以根据自己的喜好来选择
如果是公司要根据项目代码来选择,可以 通过锁文件判断 项目的包管理工具
package-lock.jsonyarn.lock包管理工具npm和yarn
千万不要混着用,切记,切记,切记
我们可以将自己开发的工具包发布到 npm 服务上,方便自己和其他开发者使用,操作步骤如下:
nrm use npm,这就是为什么老师推荐使用nrm来配置镜像 )npm login 填写相关用户信息npm publish 提交包 👌后续可以对自己发布的包进行更新,操作步骤如下
package.json 中的版本号xxxxxxxxxx11npm publish执行如下命令删除包。不需要输入包的名称,也不需要登录操作,因为之前在发布的时候已经登录过了,反正先执行这个命令即可,后续操作按照提示来做就行。
xxxxxxxxxx11npm unpublish --force删除包需要满足一定的条件,https://docs.npmjs.com/policies/unpublish
- 你是包的作者
- 发布小于 24 小时
- 大于 24 小时后,没有其他包依赖,并且每周小于 300 下载量,并且只有一个维护者
在很多语言中都有包管理工具,比如:
| 语言 | 包管理工具 |
|---|---|
| PHP | composer |
| Python | pip |
| Java | maven |
| Go | go mod |
| JavaScript | npm/yarn/cnpm |
| Ruby | rubyGems |
除了编程语言领域有包管理工具之外,操作系统层面也存在包管理工具,不过这个包指的是『 软件包 』
| 操作系统 | 包管理工具 | 网址 |
|---|---|---|
| Centos | yum | https://packages.debian.org/stable/ |
| Ubuntu | apt | https://packages.ubuntu.com/ |
| MacOS | homebrew | https://brew.sh/ |
| Windows | chocolatey | https://chocolatey.org/ |

nvm 全称 Node Version Manager 顾名思义它是用来管理 node 版本的工具,方便切换不同版本的Node.js
nvm 的使用非常的简单,跟 npm 的使用方法类似
首先下载 nvm,下载地址 https://github.com/coreybutler/nvm-windows/releases
选择 nvm-setup.exe 下载,无脑安装即可。
nvm配置镜像:
在nvm所在的文件夹中,找到settings.txt,粘贴这两行代码。
xxxxxxxxxx21node_mirror: https://npmmirror.com/mirrors/node/2npm_mirror: https://npmmirror.com/mirrors/npm/
这样,nvm安装nodejs就会非常快了。
| 命令 | 说明 |
|---|---|
| nvm list available | 显示所有可以下载的 Node.js 版本 |
| nvm list | 显示本地已安装的版本 |
| nvm install 18.21.1 | 安装 18.12.1 版本的 Node.js |
| nvm install latest | 安装最新版的 Node.js |
| nvm uninstall 18.21.1 | 删除某个版本的 Node.js |
| nvm use 18.12.1 | 切换 18.21.1 的 Node.js |

express 是一个基于 Node.js 平台的极简、灵活的 WEB 应用开发框架,官方网址:https://www.expressjs.com.cn/
简单来说,express 是一个封装好的工具包,封装了很多功能,便于我们开发 WEB 应用(HTTP 服务)
express 本身是一个 npm 包,所以可以通过 npm 安装
xxxxxxxxxx31# 在项目文件夹中执行2npm init3npm i express大家可以按照这个步骤进行操作:
xxxxxxxxxx151//1. 导入 express2const express = require('express');34//2. 创建应用对象5const app = express();67//3. 创建路由规则8app.get('/home', (req, res) => { // req是请求报文的封装对象,res是响应报文的封装对象。这一点和http模块是类似的。9 res.end('hello express server');10});1112//4. 监听端口 启动服务13app.listen(3000, () => {14 console.log('服务已经启动, 端口监听为 3000...');15});结合之前学习的http模块来看express库,创建服务器的基本过程有什么区别?
区别在于express将路由规则单独提取出来了,而http服务器是所有路由都会走创建server的那段代码,先记住基本创建过程,不断复习,才能增强信心。
node app.jsxxxxxxxxxx41node <文件名>2# 或者3nodemon <文件名>4# 或者如果你在package.json里面指定了命令别名,同时将main属性指向正确的文件,那么可以使用 npm run 命令别名 来执行。小节:
这么简单的几行代码,就可以创建一个服务器,我是真的没有想到会这么简单。在我的印象中,服务端项目最起码要和前端项目一样复杂才对啊,这个想法没有错,但那应该是完整的项目,有很多复杂的功能。
应该说创建服务器本身是很简单的(就像我写一个html网页一样简单,里面可以加上一些基本的css和js),是我把它想的复杂了,面对我一直没有解决的问题,我总是习惯性地夸大难度、找不准方向。难就难在其功能的复杂性,要操作数据库、要保证安全、要验证用户信息、要写各种路由api、要错误兼容等等,把完整功能加上去之后就非常复杂了。不过再复杂,也只是一个服务器而已,记住这句话,抓住这个主线,才能不迷路。
官方定义: 路由确定了应用程序如何响应客户端对特定端点的请求
一个路由的组成由请求方法 , 路径 和 回调函数 组成。
express 中提供了一系列方法,可以很方便的使用路由,使用格式如下:
xxxxxxxxxx11app.<method>(path,callback)代码示例:
注意:
下面写响应报文的时候,用到了
res.send()方法,这是express的响应方法,express对http模块的一些操作做了兼容,可以使用res.end()方法,下面第四小节讲到了。
xxxxxxxxxx351//导入 express2const express = require('express');34//创建应用对象5const app = express();67//创建 get 路由8app.get('/home', (req, res) => {9 res.send('网站首页');10});1112//首页路由13app.get('/', (req,res) => {14 res.send('我才是真正的首页');15});1617//创建 post 路由18app.post('/login', (req, res) => {19 res.send('登录成功');20});2122// all 表示匹配所有的请求方法,不管请求方法是get还是post还是put还是delete还是options,只要path为 /search ,都匹配到。23app.all('/search', (req, res) => {24 res.send('1 秒钟为您找到相关结果约 100,000,000 个');25});2627//自定义 404 路由,* 表示通配符,如果上面的path都没有匹配到,那么就显示404。因此这段代码一定要放到最后来执行,否则就把正常的路由给匹配到了。28app.all("*", (req, res) => {29 res.send('<h1>404 Not Found</h1>')30});3132//监听端口 启动服务33app.listen(3000, () =>{34 console.log('服务已经启动, 端口监听为 3000');35});express 框架封装了一些 API 来方便获取请求报文中的数据,并且兼容原生 HTTP 模块的获取方式。
下面的示例中,直接在url上写一些query参数来测试即可,比如:http://127.0.0.1:9000/request?a=1&b=2
xxxxxxxxxx311//导入 express2const express = require('express');34//创建应用对象5const app = express();67//获取请求的路由规则8app.get('/request', (req, res) => {9 //1. 获取报文的方式与原生 HTTP 获取方式是兼容的10 console.log(req.method);11 console.log(req.url);12 console.log(req.httpVersion);13 console.log(req.headers);14 15 //2. express 独有的获取报文的方式16 // 获取路径17 console.log(req.path);18 // 获取ip19 console.log(req.ip);20 //获取查询字符串21 console.log(req.query); // 『相对重要』22 // 获取指定的请求头。这里举了一个获取host请求头参数的例子,不要以为只能获取host请求头参数,可以获取非常多的请求头参数,比如说:accept、accept-encoding、cache-control、user-agent、referer等等都可以获取。23 console.log(req.get('host'));24 25 res.send('请求报文的获取');26});2728//启动服务29app.listen(3000, () => {30 console.log('启动成功....')31})HTTP原生获取参数输出:

express独有获取参数方式输出:

路由参数指的是 URL 路径中的参数(数据)。


xxxxxxxxxx41// :id 表示占位符,可以先获取id,然后判断进行处理,符合要求的就返回数据。2app.get('/:id.html', (req, res) => {3 res.send('商品详情, 商品 id 为' + req.params.id);// 使用req.params获取路由参数时,里面的属性名和占位符是一致的。4});
需求:根据路由参数响应歌手的信息,路径结构:/singer/1.html,显示歌手的姓名和图片(数据在老师的代码中有)。
分析:这个练习还是蛮简单的,只需要在url栏里面更改url就当作发送请求了,使用req.params.id来获取歌手的id。不过导入json文件在这里不使用fs模块(fs读取文件返回的是buffer类型,处理起来很麻烦),而是使用require来导入,因为require导入json文件后,是一个js的对象,可以直接使用这个对象。响应报文的响应体是一个html,并且要对没有找到数据的情况做兼容。
xxxxxxxxxx111{2 "singers":[3 {4 "singer_name":"周杰伦",5 "singer_pic":"http://y.gtimg....",6 "other_name":"Jay Chou",7 "singer_id":4558,8 "id":19 },10 ]11}代码:
xxxxxxxxxx441// 导入express2const express = require("express");3// 导入json文件4const { singers } = require("./singers.json");56// 创建应用对象7const app = express();89// 创建路由规则10app.get("/singer/:id.html", (req, res) => {11 // 获取路由参数12 let { id } = req.params;13 // 查找歌手14 let result = singers.find((item) => Number(id) === item.id);15 if (!result) {16 res.statusCode(404).send("<h2>未找到此歌手</h2>");17 return;18 }19 res.send(`20 <!DOCTYPE html>21 <html lang="en">22 <head>23 <meta charset="UTF-8" />24 <meta name="viewport" content="width=device-width, initial-scale=1.0" />25 <title>我是歌手</title>26 </head>27 <body>28 <h2>歌手名称:${result.singer_name}</h2>29 <hr />30 <img src="${result.singer_pic}" />31 </body>32 </html> 33 `);34});3536// 设置404界面37app.all("*", (req, res) => {38 res.send("<h1>404 Not Found</h1>");39});4041// 启动服务,监听端口42app.listen(3000, () => {43 console.log("服务已启动,端口号3000监听中...");44});显示效果:

express 框架封装了一些 API 来方便给客户端响应数据,并且兼容原生 HTTP 模块的获取方式。
xxxxxxxxxx231//获取请求的路由规则2app.get("/response", (req, res) => {3 //1. express 中设置响应的方式兼容 HTTP 模块的方式4 res.statusCode = 404;5 res.statusMessage = 'xxx';6 res.setHeader('abc','xyz');7 res.setHeader("content-type","text/html;charset=utf8")8 res.write('响应体');// 能用write方法,但是必须在最后使用 end 方法做结束。这一点比http模块中要严格。9 res.end('xxx');10 11 //2. express 的响应方法12 res.status(500); //设置响应状态码13 res.set('xxx','yyy');//设置响应头14 res.send('中文响应不乱码');//设置响应体15 //连贯操作16 res.status(404).set('xxx','yyy').send('你好朋友')17 18 //3. 其他响应19 res.redirect('http://atguigu.com')//重定向20 res.download('./package.json');//下载响应21 res.json({"msg":"ok"});//响应 JSON22 res.sendFile(__dirname + '/home.html') //响应文件内容23});兼容HTTP响应方式:


express响应方法的输出:


express的其他响应很牛逼啊,把项目中主要用到的响应都涉及到了,特别是
res.json(),我以后应该是用的最多的,而且会对json的内容做一个封装,设置成这种形式:xxxxxxxxxx51{2"code":200,3"msg":"ok",4"data":""5}其中data部分是变化最多的,可以返回多种数据类型,其余部分会随着响应报文是否正常,来显示不同内容。
中间件(Middleware)本质是一个回调函数。
中间件函数 可以像路由回调一样访问 请求对象(request) , 响应对象(response)。
上面这句话是什么意思?
这句话的意思是在使用中间件的地方,中间件可以使用该地方的请求对象和响应对象,而且是在该地方使用请求对象和响应对象之前使用。如果是全局中间件,那么全局中间件可以使用每一个路由规则中的请求对象和响应对象。如果是路由中间件,那么在使用路由中间件的路由规则中,中间件就可以使用此路由中的请求对象和响应对象。把这一点认识清楚非常重要,否则永远搞不懂中间件是怎么运行的。
一般情况下(特殊情况我还不知道,按照普通使用方式都应该是这样),如果使用了中间件函数(不管是全局中间件还是路由中间件),那么中间件函数会在具体路由函数之前执行,比如说:
xxxxxxxxxx141// 声明路由中间件2let checkCodeMiddleware = (req,res,next)=>{3if(req.query.code === '521'){4next();// 注意这里的用法,我原本想在这个函数里面添加一个参数的,然后返回这个参数。但是很明显,我没有理解next()函数的作用,next()函数就是执行之后的代码的。5} else {6// 如果不符合条件,那么就直接返回响应结果。调用中间件的路由规则里面的函数就不会执行了。7res.send("暗号错误")8}9}1011// 调用中间件12app.get("/admin",checkCodeMiddleware,(req,res)=>{13res.send("后台admin界面")14})那么这里的
checkCodeMiddleware会在路由本身的(req,res)=>{}之前执行,而且是同步的执行,checkCodeMiddleware执行完成之后,(req,res)=>{}才会执行。什么地方可以看到是同步的执行?中间件函数都有一个next参数,这个参数是一个函数,只有显式地执行了
next()之后,才会执行路由本身的(req,res)=>{}。
中间件的作用 就是 使用函数封装公共操作,简化代码。
每一个请求到达服务端之后 都会执行全局中间件函数。
使用方式一:
1、声明中间件函数,函数名可以随便起,但一般都带上Middleware,用于提示这是个中间件。可以写成普通函数,也可以写成箭头函数。
xxxxxxxxxx61let recordMiddleware = function(request,response,next){2 //实现功能代码3 //.....4 //执行next函数(当如果希望执行完中间件函数之后,仍然继续执行路由中的回调函数,必须调用next)5 next();6}
2、应用中间件
xxxxxxxxxx11app.use(recordMiddleware)使用方式二:
声明时可以直接将匿名函数传递给 app.use()
xxxxxxxxxx41app.use(function (request, response, next) {2 console.log('定义第一个中间件');3 next();4})案例:
需求:记录每个请求的url和IP地址,存放到一个文件当中。
分析:因为要记录每个请求的URL和IP地址,所以就要使用全局中间件,解析里面的request参数,使用fs模块写入一个文件当中。
xxxxxxxxxx321const express = require("express")2const fs = require("fs")3const path = require("path")45const app = express();67// 定义全局中间件8const recordMiddleware = function(request,response,next){9 let {url,ip} = request;10 fs.appendFileSync(path.resolve(__dirname, "access.log"),`${url} ${ip}`);11 // 必须要写next()12 next();13}1415// 应用中间件16app.use(recordMiddleware);1718app.get("/home",(req,res) => {19 res.send("首页界面")20})2122app.get("/admin",(req,res)=>{23 res.send("后台admin界面")24})2526app.all("*",(req,res)=>{27 res.send("<h1>404 Not Found</h1>")28})2930app.listen(9000,()=>{31 console.log("服务已启动,端口号9000监听中...")32})效果:

查看access.log文件:

express 允许使用 app.use() 定义多个全局中间件。
方式一:
xxxxxxxxxx91app.use(function (request, response, next) {2 console.log('定义第一个中间件');3 next();4})56app.use(function (request, response, next) {7 console.log('定义第二个中间件');8 next();9})方式二:
xxxxxxxxxx111const checkMiddleware = function(req,res,next){2 console.log('定义第一个中间件');3 next();4}56const recordMiddleware = function(req,res,next){7 console.log('定义第二个中间件');8 next();9}1011app.use(checkMiddleware,recordMiddleware);如果使用多个全局中间件,执行顺序是什么样的?
是按照注册的顺序来执行的。如果是
app.use(middleware1,middleware2)这种方式,是按照参数的顺序来执行的。
如果 只需要对某一些路由进行功能封装 ,则就需要路由中间件。
还是先定义中间件,然后在路由中调用中间件函数。(就不是使用app.use()来调用了)
调用格式如下:
xxxxxxxxxx81// 注意:下面的中间件函数看似用模版字符串包裹起来了,其实不是的,这只是老师为了写中文加上去的。如果定义了中间件函数,直接写中间件函数的函数名即可。2app.get('/路径',`中间件函数`,(request,response)=>{3 4});56app.get('/路径',`中间件函数1`,`中间件函数2`,(request,response)=>{7 8});案例:
需求:针对 /admin 和 /setting 的请求,要求URL携带 code=521 的参数,如未携带提示“暗号错误”
分析:因为是多个路由中(不是全部路由)要处理一些类似的事情,所以用路由中间件。
xxxxxxxxxx291const express = require("express")23const app = express()45// 声明路由中间件6let checkCodeMiddleware = (req,res,next)=>{7 if(req.query.code === '521'){8 next();// 注意这里的用法,我原本想在这个函数里面添加一个参数的,然后返回这个参数。但是很明显,我没有理解next()函数的作用,next()函数就是执行之后的代码的。9 } else {10 res.send("暗号错误")11 }12}1314app.get("/home",(req,res)=>{15 res.send("首页界面")16})1718// 调用中间件19app.get("/admin",checkCodeMiddleware,(req,res)=>{20 res.send("后台admin界面")21})2223app.get("/setting",checkCodeMiddleware,(req,res)=>{24 res.send("设置界面")25})2627app.listen(9000,()=>{28 console.log("服务器已启动,端口号9000监听中...")29})显示效果:

express 内置处理静态资源的中间件,使用express.static()。
xxxxxxxxxx191//引入express框架2const express = require('express');34//创建服务对象5const app = express();67//静态资源中间件的设置,将当前文件夹下的public目录作为网站的根目录8app.use(express.static('./public')); //当然这个目录中都是一些静态资源9//如果访问的内容经常变化,还是需要设置路由10//但是,在这里有一个问题,如果public目录下有index.html文件,单独也有index.html的路由,11//则谁书写在前,优先执行谁12app.get('/index.html',(request,response)=>{13 respsonse.send('首页');14});1516//监听端口17app.listen(3000,()=>{18 console.log('3000 端口启动....');19});注意事项:
- index.html 文件为默认打开的资源
即使服务端代码里面没有配置
/index.html的路由,那么访问http://127.0.0.1:3000默认打开的就是index.html资源,而如果配置了静态资源中间件,那么系统就会自动到静态资源文件夹中去查找文件。
- 如果静态资源与路由规则同时匹配,谁先匹配就响应谁
- 路由响应动态资源,静态资源中间件响应静态资源
这个功能相当于我们之前在http模块里面做的,响应不同的静态资源的案例。只不过express用一行代码就全部搞定了,而且会自动设置MIME类型,可以说是非常方便了。但我还是有一个疑问:纯后端express项目,需要静态资源吗?这个问题等我做项目的时候,可以带着问题想一想。
这个还是很有作用的,当我没有tomcat时,可以使用express来做一个简易的服务器,让别人都能够访问我的网站。
有了express的这个功能,创建项目服务器更简单了。
xxxxxxxxxx91const express = require("express");23const app = express();45app.use(express.static("./info-collector"));67app.listen(9000, () => {8console.log("服务已启动,端口9000监听中...");9});项目可以直接使用
http://localhost:9000/来访问,更简单了。
需求:局域网内可以访问尚品汇的网页。尚品汇就是一个编写好的、打包好的前端项目。

分析:其实就是使用express的静态资源中间件,来展示index.html。
xxxxxxxxxx91const express = require("express")23const app = express()45app.use(express.static("./尚品汇"))67app.listen(3000,()=>{8 console.log("服务器已启动,端口号3000监听中...")9})express 可以使用 body-parser 包处理请求体。
使用案例来学习:
需求:按照要求搭建http服务,GET /login 显示表单网页,POST /login 获取表单中的用户名和密码。
分析:GET路由返回一个HTML页面,使用fs来读取。这个HTML页面里面,使用form表单来提交post请求。在POST路由里面,使用body-parser来获取请求体。
第一步:安装
xxxxxxxxxx11npm i body-parser第二步:导入 body-parser 包
xxxxxxxxxx11const bodyParser = require('body-parser')第三步:获取中间件函数
xxxxxxxxxx41//处理 querystring 格式的请求体的中间件2let urlParser = bodyParser.urlencoded({extended:false}));3//处理 JSON 格式的请求体的中间件4let jsonParser = bodyParser.json();第四步:设置路由中间件,然后使用 request.body 来获取请求体数据。
body-parser可以作为全局中间件使用,也可以作为路由中间件使用。但推荐作为路由中间件使用,因为不是所有路由都需要body-parser来解析请求体的,而且处理请求体的方式有两种,全局设置也不好判断怎么处理。
怎么看使用urlParser还是jsonParser呢?在前后端分离项目中,这其实是后端来定义前端的请求方式的。那么如果遇到实在不知道的情况,可以看前端提交参数的方式,在一个请求里面Payload,view parsed可以看到。
xxxxxxxxxx91app.post('/login', urlParser, (request,response)=>{2 //获取请求体数据3 //console.log(request.body);4 //用户名5 console.log(request.body.username);6 //密码7 console.log(request.body.password);8 response.send('获取请求体数据');9});获取到的请求体数据:
不用管前面的[Object: null prototype],这只是一种提示,值是后面的对象。
xxxxxxxxxx11[Object: null prototype] { username: 'admin', userpass: '123456' }完整代码:
xxxxxxxxxx211// index.html234<html lang="en">5 <head>6 <meta charset="UTF-8" />7 <meta http-equiv="X-UA-Compatible" content="IE=edge" />8 <meta name="viewport" content="width=device-width, initial-scale=1.0" />9 <title>登录</title>10 </head>11 <body>12 <h2>登录界面</h2>13 <hr />14 <!-- action里面可以简写为 /login ,为什么?复习一下前面讲绝对路径的知识就知道了。 -->15 <form action="http://127.0.0.1:3000/login" method="post">16 用户名:<input type="text" name="username" placeholder="请输入用户名" /> 17 密码:<input type="password" name="password" placeholder="请输入密码" />18 <button>登录</button>19 </form>20 </body>21</html>xxxxxxxxxx321// app.js23// 引入express框架4const express = require("express");5const path = require("path");6const bodyParser = require("body-parser");78// 创建服务对象9const app = express();1011// 定义body-parser解析中间件12let urlencodedParser = bodyParser.urlencoded({ extended: false });1314app.get("/login", (req, res) => {15 // 这里如果使用fs模块来读取html文件,并使用res.send()返回读取的数据,访问 /login 会自动下载这个文件。估计express的处理规则是这样的,所以这里要使用sendFile()方法,里面的参数是文件的绝对路径。16 res.sendFile(path.resolve(__dirname,"index.html"));17});1819app.post("/login", urlencodedParser, (req, res) => {20 console.log(req.body);21 // 请求体的参数保存在req.body中。这里的请求参数为什么是username和password?其实这是前端提交的,我不了解form的请求机制,所以这里有点疑问。请求体的参数一般都是后端规定好,前端再来使用的。22 res.send(`用户名是:${req.body.username},密码是:${req.body.password}`);23});2425app.all("*", (req, res) => {26 res.status(404).send("<h1>404 Not Found</h1>");27});2829// 监听端口30app.listen(3000, () => {31 console.log("服务已启动,端口号3000监听中...");32});效果:

定义:防止外部网站盗用本地的资源。原理:禁止其它域名的网站来访问本网站的资源。
比如说:我在百度搜图里面,找几张图,复制它们的链接,粘贴到我的html文件中的img上,打开html文件,可以看到有的图片可以显示,有的图片不能显示,这就说明了有的图片设置了防盗链。
实现防盗链:
通过判断referer请求头是否为网站域名来返回不同结果。referer是访问图片时自动带上的头信息,不需要额外设置。
(在此案例中,使用 127.0.0.1:3000 和 localhost:3000来访问,在index.html里面使用img来测试防盗链。如果是直接在url里面访问public里的文件,还是可以正常访问的:http://localhost:3000/images/logo.png。这种情况的访问在请求头里面没有referer参数,估计还要另外想办法才行,先不用管)

xxxxxxxxxx141<!-- index.html -->234<html lang="en">5 <head>6 <meta charset="UTF-8" />7 <meta name="viewport" content="width=device-width, initial-scale=1.0" />8 <title>Document</title>9 </head>10 <body>11 <h2>测试防盗链</h2>12 <img src="http://127.0.0.1:3000/images/logo.jpg" alt="" />13 </body>14</html>
xxxxxxxxxx271const express = require("express");23const app = express();45app.use(function (req, res, next) {6 const referer = req.get("referer");7 // console.log(referer);8 if (referer) {9 // referer此时是一个字符串,不好获取ip地址,所以将其构造成为url对象,这样获取hostname就很简单了10 let url = new URL(referer);11 // 获取hostname12 let hostname = url.hostname;13 // 根据hostname来判断,如果不是特定的域名,就返回40414 if (hostname !== "127.0.0.1") {15 res.send("<h2>404 Not Found</h2>");16 return;17 }18 }19 next();20});2122app.use(express.static("./public"));2324app.listen(3000, () => {25 console.log("服务已启动,端口号3000监听中...");26});27先输出req.get("referer")看一下是什么样子的:

测试,直接在url中访问,看不同的网站域名是什么效果。

下面显示使用localhost来访问,是访问不了的。


express 中的 Router 是一个完整的中间件和路由系统, Router可以看做是一个小型的 app 对象,你看独立router文件里面router的用法,和app.<method>(path,callback)的用法一样,也可以使用router.use()来使用中间件。
对路由进行模块化,更好的管理路由。
在public文件夹同层级创建routes文件夹,里面创建独立的 JS 文件(homeRouter.js),使用express.Router()来创建Router。
xxxxxxxxxx171//1. 导入 express2const express = require('express');34//2. 创建路由器对象5const router = express.Router();67//3. 在 router 对象身上添加路由8router.get('/', (req, res) => {9 res.send('首页');10})1112router.get('/cart', (req, res) => {13 res.send('购物车');14});1516//4. 暴露17module.exports = router;可以看到,创建router的过程和创建app基本流程是一致的,先记住基本流程的创建。
主文件
xxxxxxxxxx131const express = require('express');23const app = express();45//5.引入子路由文件6const homeRouter = require('./routes/homeRouter');78//6.设置和使用中间件9app.use(homeRouter);1011app.listen(3000,()=>{12 console.log('3000 端口启动....');13})注意:
虽然说router就是小型的app,但是如果一个router里面想要使用一个通用的中间件,可不可以这样呢:
xxxxxxxxxx11router.use(checkTokenMiddleware);这样就可以避免在每一个路由规则里面写同样的中间件了,省事了不少。但实际效果是什么样的呢?
实际效果是不同的routes文件中,所有使用router创建的路由规则,都挂载上了这个中间件,有一些路由规则是不能使用这个中间件的,比如说登录、注册的路由规则。
说明不同的routes文件中的router,是同一个router,只不过挂载上了不同的路由规则而已,暴露出去的也是挂载了不同路由规则的router。
模板引擎是分离 用户界面和业务数据 的一种技术。可以简单的认为是将html和js文件分离开来,分别进行处理(这里的将HTML和JS分离开来,指的是服务端的项目,在服务端写前端代码,像JSP,Django这种类型的)。
是的,我目前的感觉是,ejs不太重要,因为目前做的项目都是前后端分离,我在vue中写不是简单多了吗?这种模板语法在Django中看过,可以看一下这个文档:https://zhuanlan.zhihu.com/p/448064809。但是以后如果我找remote-developer的工作,不一定就是前后端分离的CRM这类全栈项目,也很有可能是工具类的全栈项目,这些全栈项目需要有一些页面在服务端进行处理,现在我就看到codeSandbox、prisma、vercel等等工具都是这样的,而这些工具类项目也很赚钱啊(提供基本功能的免费版,更多功能则收费,国外的很多公司都是愿意付费的),我如果能够参与进去,那肯定钱都不是问题了。那么这里的ejs还是要重视起来,最起码老师讲的内容要弄清楚,做好笔记。
搜索到的一段话:
EJS最方便的地方就是在于将项目给别人使用的时候,人家不用过多的去了解你的代码,直接修改配置文件就可以达到自己想要的效果。比如说工具类的软件Hexo,普通人想要自己创建博客网站,非常难,即使是程序员,也是很难的,但是使用Hexo只需要进行配置接口。Hexo中的配置都集中在_config.yml这个文件中,你根本不需要去一行一行的浏览源代码,就可以实现修改,达到你想要的效果。那么我如果做了hexo这个项目,让用户在_config.yml文件里面配置好了之后,执行build命令时我就可以从这个文件里面获取具体的值,填入到ejs文件中,速度会非常快。
EJS 是一个高效的 Javascript 的模板引擎。
官网: https://ejs.co/
下载安装EJS
xxxxxxxxxx11npm i ejs --save代码示例
xxxxxxxxxx91//1.引入ejs2const ejs = require('ejs');3//2.定义数据4let person = ['张三','李四','王二麻子'];5//3.ejs解析模板返回结构6//<%= %> 是ejs解析内容的标记,作用是输出当前表达式的执行结构7let html = ejs.render('<%= person.join(",") %>', {person:person});8//4.输出结果9console.log(html);示例:
老师用一步一步的示例来展示出ejs到底可以怎么写:
xxxxxxxxxx331// index.js23const ejs = require("ejs")4const fs = require("fs")56let china = "中国";78// 首先使用ejs的方法直接渲染9// let result = ejs.render(`我爱你 <%= china %>`,{china:china})10// console.log(result) // 输出:我爱你 中国1112// 优化:可以将ejs模板提取出来13// let str = `我爱你 <%= china %>`14// let result = ejs.render(str,{china:china})15// console.log(result) // 输出:我爱你 中国1617// 优化:将ejs模板放到html文件中,然后用fs模块读取出来。这里要使用toString()方法,因为ejs只能渲染字符串。18let str = fs.readFileSync('./1.html').toString();19let result = ejs.render(str,{china:china});20console.log(result)21/**输出结果:22 * <!DOCTYPE html>23<html lang="en">24<head>25 <meta charset="UTF-8">26 <meta name="viewport" content="width=device-width, initial-scale=1.0">27 <title>Document</title>28</head>29<body>30 <h2>我爱你 中国</h2>31</body>32</html>33 */xxxxxxxxxx1212<html lang="en">3<head>4 <meta charset="UTF-8">5 <meta name="viewport" content="width=device-width, initial-scale=1.0">6 <title>Document</title>7</head>8<body>9 <!-- 只需要在html中使用ejs语法,就可以将值传进来。 -->10 <h2>我爱你 <%= china %></h2>11</body>12</html>执行JS代码
xxxxxxxxxx11<% code %>输出转义的数据到模板上
xxxxxxxxxx11<%= code %>输出非转义的数据到模板上
xxxxxxxxxx11<%- code %>需求:有一个数组,将里面的每一个元素渲染为一个li标签。
xxxxxxxxxx141// 先用原生JS来实现23const xiyou = ['唐僧','孙悟空','猪八戒','沙僧']45let str = '<ul>'67xiyou.forEach(item => {8 str += `<li>${item}</li>`9})1011// 闭合ul12str += '</ul>'1314console.log(str)输出结果:

用ejs实现:
xxxxxxxxxx71const ejs = require("ejs");2const fs = require("fs");34const xiyou = ["唐僧", "孙悟空", "猪八戒", "沙僧"];5let html = fs.readFileSync("./2.html").toString();6let result = ejs.render(html, { xiyou: xiyou });7console.log(result);xxxxxxxxxx181<!-- 2.html -->234<html lang="en">5 <head>6 <meta charset="UTF-8" />7 <meta name="viewport" content="width=device-width, initial-scale=1.0" />8 <title>西游四人组</title>9 </head>10 <body>11 <h2>西游四人组</h2>12 <ul>13 <% xiyou.forEach(item => { %>14 <li><%= item %></li>15 <% }) %>16 </ul>17 </body>18</html>输出结果:

需求:
xxxxxxxxxx51/**2 * 需求:通过isLogin决定最终的输出内容3 * true 输出 <span>欢迎回来</span>4 * false 输出 <button>登录</button> <button>注册</button>5 */原生js实现:
xxxxxxxxxx71let isLogin = true;23if (isLogin) {4 console.log("<span>欢迎回来</span>");5} else {6 console.log("<button>登录</button> <button>注册</button>");7}ejs实现:
xxxxxxxxxx71const ejs = require("ejs");2const fs = require("fs");3let isLogin = true;45let html = fs.readFileSync("./3.html").toString();6let result = ejs.render(html, { isLogin: isLogin });7console.log(result)xxxxxxxxxx171<!-- 3.html -->234<html lang="en">5 <head>6 <meta charset="UTF-8" />7 <meta name="viewport" content="width=device-width, initial-scale=1.0" />8 <title>Document</title>9 </head>10 <body>11 <% if(isLogin) { %>12 <button>登录</button> <button>注册</button>13 <% } else { %>14 <span>欢迎回来</span>15 <% } %>16 </body>17</html>通过改变isLogin的值来看输出:

xxxxxxxxxx281// 导入express2const express = require("express")3const path = require("path")45// 创建应用对象6const app = express()78// express中使用ejs9// 1、设置模板引擎10app.set("view engine",'ejs')1112// 2、设置模板文件存放的位置。注意:这里的位置就是文件夹的位置,这里设置为views文件夹,层级与public同级,里面存放的是后缀名为 .ejs 的文件,具体的文件会在res.render()中用到,语法规则:res.render('模版的文件名','数据')13// 模板文件:具有模板语法的文件14app.set("views",path.resolve(__dirname,"./views"))1516// 创建路由17app.get("/home",(req,res)=>{18 // res.send("hello express")19 // 3、render响应。语法规则:res.render('模版的文件名','数据')20 let title = '书山有路勤为径,学海无涯苦作舟'21 res.render('home',{title:title})22 // 4、在views文件夹中创建home.ejs文件23})2425// 监听端口,启动服务26app.listen(9000,()=>{27 console.log('服务已启动,端口9000监听中...')28})views/home.ejs文件:
xxxxxxxxxx1112<html lang="en">3<head>4 <meta charset="UTF-8">5 <meta name="viewport" content="width=device-width, initial-scale=1.0">6 <title>Document</title>7</head>8<body>9 <h2><%= title %></h2>10</body>11</html>启动服务,查看效果:

express-generator可以快速创建一个express应用的骨架。相当于是一个脚手架工具。
安装:npm i -g express-generator
安装完成后,会有一个全局命令express,通过express -v来查看是否安装成功:

创建项目:
express [项目名],如果使用ejs模板,可以输入express [项目名] --view=ejs,简写方式:express -e [项目名]。

示例:express -e ejs-template

在项目中需要先安装依赖:npm install
项目结构:

查看package.json里面的命令,将"start": "node ./bin/www"改为"start": "nodemon ./bin/www",便于编写的时候调试。使用npm start将项目启动,浏览器输入127.0.0.1:3000来查看效果:

里面有一些用法可以说一下:
app.js:


下面的学习依赖上面express-generator创建的项目。
app.js里面不做任何更改,直接在indexRouter里面添加路由:

xxxxxxxxxx211// routes/index.js23var express = require("express");4var router = express.Router();56/* GET home page. */7router.get("/", function (req, res, next) {8 res.render("index", { title: "Express" });9});1011// 显示网页的表单12router.get("/portrait", (req, res) => {13 res.render("portrait");14});1516// 处理文件上传17router.post("/portrait", (req, res) => {18 res.send("提交成功");19});2021module.exports = router;访问/portrait页面时,默认执行的是get请求,所以需要结合ejs来展示页面,新建portrait.ejs文件:
xxxxxxxxxx1812<html lang="en">3 <head>4 <meta charset="UTF-8" />5 <meta name="viewport" content="width=device-width, initial-scale=1.0" />6 <title>文件上传</title>7 </head>8 <body>9 <h2>文件上传</h2>10 <hr />11 <!-- enctype="multipart/form-data" 是文件上传必需的属性 -->12 <form action="/portrait" method="post" enctype="multipart/form-data">13 用户名:<input type="text" name="username" /><br />14 头像:<input type="file" name="portrait" /><br />15 <button>点击提交</button>16 </form>17 </body>18</html>这个好像没有用到ejs的东西,但其实用到了,直接使用res.render了页面,等下一小节看会不会用到。浏览器输入127.0.0.1/3000/portrait进行访问:

因为这个页面在post请求后会跳转,所以需用用fiddle来监听http请求,结果如下:

可以看到,form表单的内容在请求体中。
需要用到formidable库,安装npm i formidable,这个包是用来解析form-data的。


但是代码示例有点问题,如果我直接粘贴的话,会报错:formidable is not a function,明明是官方的示例代码,却报错,搜索了一下没有找到答案,就打印formidable看一下是什么,发现里面有一个formidable的function,于是解构赋值,这才行了。
xxxxxxxxxx351// routes/index.js23var express = require("express");4var router = express.Router();5// 导入,官方代码没有解构赋值,但其实需要。6var { formidable } = require("formidable");78/* GET home page. */9router.get("/", function (req, res, next) {10 res.render("index", { title: "Express" });11});1213// 显示网页的表单14router.get("/portrait", (req, res) => {15 res.render("portrait");16});1718// 处理文件上传19router.post("/portrait", (req, res, next) => {20 // 创建form对象21 const form = formidable({ multiples: true });2223 // 解析请求报文24 form.parse(req, (err, fields, files) => {25 if (err) {26 next(err);27 return;28 }29 console.log(fields);30 console.log(files);31 res.json({ fields, files }); // fields属性返回的是“非文件”的属性值,比如说text、radio、checkbox、select等元素的值。而files属性返回的是file。32 });33});3435module.exports = router;输出效果看一下:

仔细看一下fields和files的值,这个处理结果我很满意啊,我不用特地的分开附件和其余的表单信息,formidable都帮我处理好了:

下一步,一般情况下,服务器在接收到上传的文件后,会把文件保存起来,供用户回显的时候用到。而且应该保存到用户能够很方便就能访问到的位置,所以保存在public/images文件夹里面。只需要对formidable进行配置即可:

上传文件试一试,看图片保存到public/images文件夹里面没有。

保存成功了。
下一步,在保存文件的同时,需要将这个文件的保存路径记录下来,放到数据库中,便于以后的查看。


查看输出:

这一小节真的是把请求最难的部分给解决了,以后不管是上传图片还是上传word、excel等,我都不用怕了,而且把我的一个长久的疑问给解决了:文件上传后,后端是怎么保存的?是需要转为base64保存吗?是把文件保存到数据库中吗?
都不是,是直接保存到public文件夹里面,不需要转换为base64,只需要保存文件的访问url地址即可。这真的是巧妙啊,一个插件就解决了这么难的问题。
而且这个文件夹我看着很眼熟啊:
而且我运行项目代码
npm start,实际上nodemon执行的是bin/www文件。
我在NGINX里面看到过,不过一直不知道这是干什么的,现在终于知道是干什么的了,这是部署服务端代码的。嘿嘿。
做一个记账本项目,有两个界面,一个记账,一个展示账单。


创建项目:express -e accounts,安装依赖:npm i,将package.json里面的命令别名代码改为"start": "nodemon ./bin/www",方便我们在写代码的时候实时看到效果,将项目运行起来:npm start。
只使用一个路由文件,将usersRouter注释掉。在routes/index.js里面创建路由规则。
xxxxxxxxxx161// routes/index.js23var express = require("express");4var router = express.Router();56// 记账本的列表7router.get("/account", function (req, res, next) {8 res.send("账本列表");9});1011// 记账本添加记录12router.get("/account/create", (req, res, next) => {13 res.send("添加记录");14});1516module.exports = router;输入路由地址,看一下有没有问题:


在views里面创建list.ejs、create.ejs文件,将老师提供的html页面粘贴进去,注意涉及到导入外部文件的时候,要使用绝对路径。
然后通过路由响应出去:
xxxxxxxxxx161// routes/index.js23var express = require("express");4var router = express.Router();56// 记账本列表界面7router.get("/account", function (req, res, next) {8 res.render("list");9});1011// 添加记录界面12router.get("/account/create", (req, res, next) => {13 res.render("create")14});1516module.exports = router;查看效果:


为create.ejs的表单元素添加name属性,这样接口才能获取到具体的数据。同时,为form表单添加method和action。
xxxxxxxxxx121<form method="post" action="/account">2 <div class="form-group">3 <label for="item">事项</label>4 <input5 name="title"6 type="text"7 class="form-control"8 id="item"9 />10 </div>11 ...12</form>添加新增记录的路由:
xxxxxxxxxx231// routes/index.js23var express = require("express");4var router = express.Router();56// 记账本的列表7router.get("/account", function (req, res, next) {8 res.render("list");9});1011// 新增记账记录12router.post("/account", (req, res, next) => {13 // 为什么可以直接使用req.body,不应该先引入body-parser吗?其实在app.js里面已经引入了。14 console.log(req.body)15 res.send("添加记录");16});1718// 记账本添加记录19router.get("/account/create", (req, res, next) => {20 res.render("create");21});2223module.exports = router;查看效果:

看一下请求体的数据:

添加的数据需要保存到数据库中,这样才能方便后续的使用。但目前没有讲到数据库,所以使用一个简单的数据库lowdb。

因为最新版本的lowdb使用了import来引入,而其余的模块是使用require引入,如果将package.json里面改为type:module的话,很多东西都要变,所以使用lowdb@1.0.0版本即可。lowdb的使用很简单,照着文档做就行了。

安装:npm i lowdb@1.0.0
在项目中新建data文件夹,创建db.json文件,里面手动初始化数据库,为什么手动初始化数据库呢?因为如果使用lowdb的方法来初始化的话,比较麻烦,重点不在这里。
xxxxxxxxxx31{2 "accounts": []3}
在routes/index.js中引入lowdb相关代码:
xxxxxxxxxx301// routes/index.js23var express = require("express");4var router = express.Router();56// 导入lowdb7const low = require("lowdb")8const FileSync = require("lowdb/adapters/FileSync")9const adapter = new FileSync(__dirname + "/../data/db.json")10// 获取db对象11const db = low(adapter)1213// 记账本的列表14router.get("/account", function (req, res, next) {15 res.render("list");16});1718// 新增记账记录19router.post("/account", (req, res, next) => {20 // 为什么可以直接使用req.body,不应该先引入body-parser吗?其实在app.js里面已经引入了。21 console.log(req.body)22 res.send("添加记录");23});2425// 记账本添加记录26router.get("/account/create", (req, res, next) => {27 res.render("create");28});2930module.exports = router;下一步,使用low的方法,将数据加入到数据库中,为了数据库更完善,需要给每条数据添加一个唯一的id,lowdb推荐使用shortid,安装npm i shortid,注意这个shortid库在npm网页里面已经找不到了,以后就不要使用这种库了,就使用nanoid。
xxxxxxxxxx371// routes/index.js23var express = require("express");4var router = express.Router();56// 导入lowdb7const low = require("lowdb");8const FileSync = require("lowdb/adapters/FileSync");9const shortid = require("shortid");10const adapter = new FileSync(__dirname + "/../data/db.json");11// 获取db对象12const db = low(adapter);1314// 记账本的列表15router.get("/account", function (req, res, next) {16 res.render("list");17});1819// 新增记账记录20router.post("/account", (req, res, next) => {21 // 为什么可以直接使用req.body,不应该先引入body-parser吗?其实在app.js里面已经引入了。22 // console.log(req.body)2324 let id = shortid();25 // 写入文件26 db.get("accounts")27 .unshift({ id, req.body })28 .write();29 res.send("添加记录");30});3132// 记账本添加记录33router.get("/account/create", (req, res, next) => {34 res.render("create");35});3637module.exports = router;查看一下数据库,非常OK。

在添加记录成功之后,跳转到一个成功信息的页面。老师提供了success.html,我们只需要创建success.ejs文件即可。
xxxxxxxxxx291<!-- views/success.ejs -->23<html lang="en">4<head>5 <meta charset="UTF-8">6 <meta http-equiv="X-UA-Compatible" content="IE=edge">7 <meta name="viewport" content="width=device-width, initial-scale=1.0">8 <title>提醒</title>9 <link10 href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.css"11 rel="stylesheet"12 />13 <style>14 .h-50{15 height: 50px;16 }17 </style>18</head>19<body>20 <div class="container">21 <div class="h-50"></div>22 <div class="alert alert-success" role="alert">23 <!-- 使用ejs语法,将动态数据展示出来 -->24 <h1>:) <%= msg %></h1>25 <p><a href="<%= url %>">点击跳转</a></p>26 </div>27 </div>28</body>29</html>xxxxxxxxxx11// routes/index.js
查看效果:

先从数据库中取出真实的数据,然后发送到list.ejs文件中。
xxxxxxxxxx11// routes/index.js
在list.ejs文件中,使用ejs语法,将数据列表渲染出来。
xxxxxxxxxx5612<html lang="en">3 <head>4 <meta charset="UTF-8" />5 <meta http-equiv="X-UA-Compatible" content="IE=edge" />6 <meta name="viewport" content="width=device-width, initial-scale=1.0" />7 <title>Document</title>8 <link9 href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.css"10 rel="stylesheet"11 />12 <style>13 label {14 font-weight: normal;15 }16 .panel-body .glyphicon-remove{17 display: none;18 }19 .panel-body:hover .glyphicon-remove{20 display: inline-block21 }22 </style>23 </head>24 <body>25 <div class="container">26 <div class="row">27 <div class="col-xs-12 col-lg-8 col-lg-offset-2">28 <h2>记账本</h2>29 <hr />30 <div class="accounts">3132 <% accounts.forEach(item => { %>33 <div class="panel <%= item.type === '-1' ? 'panel-warning' : 'panel-success' %>">34 <div class="panel-heading"><%= item.time %></div>35 <div class="panel-body">36 <div class="col-xs-6"><%= item.remark %></div>37 <div class="col-xs-2 text-center">38 <span class="label <%= item.type === '-1' ? 'label-warning' : 'label-success' %>"><%= item.type === '-1' ? '支出' : '收入' %></span>39 </div>40 <div class="col-xs-2 text-right"><%= item.account %> 元</div>41 <div class="col-xs-2 text-right">42 <span43 class="glyphicon glyphicon-remove"44 aria-hidden="true"45 ></span>46 </div>47 </div>48 </div>49 <% }) %> 5051 </div>52 </div>53 </div>54 </div>55 </body>56</html>显示效果:


首先为页面的元素绑定事件,点击可以发起请求。这里用一个a标签来模拟。

添加删除事件的路由:
xxxxxxxxxx111// routes/index.js23// 删除记录4router.get("/account/:id", (req, res, next) => {5 // 获取params的id参数6 let id = req.params.id;7 // 删除8 db.get("accounts").remove({ id }).write();9 // 提醒10 res.render("success", { msg: "删除成功", url: "/account" });11});查看效果:

提示:
一般删除都是用delete方法,以符合restful api的风格。但是这里只是演示,如果设置为router.delete方法,那么就不能使用a标签来发起请求了,后面讲到api时,会讲纯服务端怎么写,先不急。