本博客主要为黑马程序员网课中关于 node.js 模块(网课在下面有介绍)所整理记录的学习笔记,仅作学习用途,如有侵权,烦请联系本人侵删。同时感谢黑马老师们的辛苦教导付出,如有不足之处,欢迎各位小伙伴、官老爷们多多提意见或建议,感谢!
一、node.js
概念
Node.js 是一个跨平台 JavaScript 运行环境,使开发者可以搭建服务器端的 JavaScript 应用程序。
作用:使用 Node.js 编写服务器端程序
-
编写后端数据接口,提供网页资源等
-
前端工程化:集成各种开发中使用的工具和技术,为后续学习 Vue 和 React 等框架做铺垫
什么是前端工程化?
前端工程化:开发项目直到上线,过程中集成的所有工具和技术Node.is 是前端工程化的基础 (因为 Node.is 可以主动读取前端代码内容
Node.js 为何能执行JS?
首先:浏览器能执行JS 代码,依靠的是内核中的 V8 引擎 (C++ 程序)
其次:Node.js 是基于 Chrome V8 引擎的 JavaScript 运行环境
区别:都支持 ECMAScript 标准语法,Node.js 有独立的 API
注意:
-
浏览器 是 JS 的 前端运行环境
-
Node.js 是 JS 的后端运行环境
-
Node.js 环境没有 DOM 和 BOM 等,无法调用浏览器的内置API
安装和使用
安装
下载地址:Node.js (nodejs.org)
推荐版本:16.14.0 长期维护版 或 node-v16.19.0.msi (为了兼容 vue-admin-template 模板)
安装过程:默认下一步即可
注释事项:安装在非中文路径下、无需勾选自动安装其他配套软件
成功验证:打开 cmd 终端,输入 node -v 命令查看版本号,如果有显示,则代表安装成功
使用
需求:新建 JS 文件,并编写代码后,在 node 环境下执行
命令:在 VSCode 集成终端中(或者点击文件夹路径输入cmd打开终端),输入 node xxx.js,回车即可执行
终端快捷键:tab补全路径,esc清空命令,cls清空终端,向上箭头 快速定位上次执行的命令
fs模块 - 读写文件
模块:类似插件,封装了方法/属性
fs 模块:封装了与本机文件系统进行交互的,方法/属性
语法:1. 加载 fs 模块对象 2. 读取/写入文件内容
读取:fs.readFile()
// 加载模块
const fs = require('fs') // fs 是模块标识符,模块的名字
// 读取
fs.readFile('文件路径',(err, data) => {
// 读取后的回调函数
console.log(data) // 成功:返回文件内容,并且err为null
console.log(err) // 失败:返回错误对象
// data 是文件内容的 Buffer 数据流
})
例:
写入:fs.writeFile()
注意
-
fs.writeFile() 方法只能用来创建文件,不能用来创建路径
-
重复调用 fs.write() 写入同一个文件,新写入的内容会覆盖之前的旧内容
// 加载模块
const fs = require('fs')
// 写入
fs.writeFile('文件路径', '写入内容', 'utf-8', err => {
// 写入后的回调函数
console.log(err) // 成功:返回null,失败:返回一个错误对象
})
例:
练习
使用fs文件系统模块,将素材中的 成绩.txt 文件中的考试数据,整理到 成绩-ok.txt 文件中
整理前,成绩.txt 文件中的数据格式如下:
小红=99 小白=100 小黄=70 小黑=66 小绿=78
整理后,成绩-ok.txt 文件中的数据格式如下:
小红:99
小白:100
小黄:70
小黑:66
小绿:78
实现步骤
-
导入需要的 fs 文件系统模块
-
使用 fs.readFile() 方法,读取素材目录下的 成绩.txt
-
判断文件是否读取失败
-
文件读取成功后,处理成绩数据
-
将处理完的成绩数据,调用 fs.writeFile() 方法,写入新文件 成绩-ok.txt 中
const fs = require('fs' )
fs.readFile('./成绩.txt','utf8',function (err,dataStr) {
// 判断是否读取成功
if (err) {
console.Log( 文件读取失败! + err.message)
} else {
// console.log( 文件读取成功’ + datastr)
// 把数据按照空格进行分割
const arrold = dataStr.split(' ')
// 循环分割后的数组,把每项数据进行字符串的替换操作
const arrNew = []
arrOld.forEach(item => {arrNew.push(item.replace( '=',':'))
})
// 把新数组中的每一项进行合并,得到一个新的字符串
const newStr = arrNew.join('\r\n')
// 调用fs.writeFile( )方法,把处理完毕的成绩,写到新文件中
fs.writeFile('./成绩-ok.txt',newStr, function(err) {
if(err){
console.log('文件写入错误!' + err.message)
}
else {
console.log('文件写入成功!')
}
})
}
})
动态拼接路径
在使用fs模块操作文件时,如果提供相对路径(./或../)的话,很容易出现路径动态拼接错误的问题
原因:代码在运行时,会以执行node命令所处的目录,动态拼接出被操作文件的完整路径,如果使用绝对路径的话移植性又会非常差,不利于维护
解决方案:__dirname (表示当前文件所处目录) + ' 相对路径 '(不用再在前面加点,这样动态拼接后读取的也是绝对路径)
fs.readFile(__dirname + '/.txt','utf8', function(err, dataStr) {
if(err) {
console.log( 读取文件失败’ + err.message)
}else {
console.log("读取文件成功')
}
})
path 路径模块
path路径模块是Node.js官方提供的、用来处理路径的模块,它提供了一系列方法和属性,用来满足用户对路径的处理需求
如果要在 JavaScript 中使用path路径模块,则需要先导入它:
const path = require('path')
path.join()
将多个路径片段拼接成一个完整的路径字符串
注意:以后凡是涉及到路径拼接的操作,都要使用path.join()方法进行处理,不要使用 + 进行字符串的拼接
const path = require('path')
const fs = require('fs')
// path.join(__dirname, '/1.txt')
fs.readFile(path.join(__dirname, '/1.txt'), 'utf8', (err, data) => {
if(err) console.log(err.message)
else console.log(data)
})
path.basename()
定义:获取路径中的最后一部分,经常通过这个方法获取路径中的文件名
语法:path.basename('路径字符串', '文件扩展名'), 扩展名参数可选
let p = 'a/b/c/index.html'
console.log(path.basename(p)) // index.html
console.log(path.basename(p, '.html')) // index
path.txtname()
获取路径中的文件扩展名
const path = require('path')
console.log(path.txtname('a/b/c/index.js')) // .js
练习
压缩html
/**
* 目标1:压缩 html 代码
* 需求:把回车符 \r,换行符 \n 去掉,写入到新 html 文件中
* 1.1 读取源 html 文件内容
* 1.2 正则替换字符串
* 1.3 写入到新的 html 文件中
*/
// 1.1 读取源 html 文件内容
const fs = require('fs')
const path = require('path')
fs.readFile(path.join(__dirname, 'public/index.html'), (err, data) => {
if (err) console.log(err)
else {
const htmlStr = data.toString()
// 1.2 正则替换字符串
const resultStr = htmlStr.replace(/[\r\n]/g, '')
console.log(resultStr)
// 1.3 写入到新的 html 文件中
fs.writeFile(path.join(__dirname, 'dist/index.html'), resultStr, err => {
if (err) console.log(err)
else console.log('写入成功')
})
}
})
拆分html/js/css
将index.html拆分成index.html、index.js、index.css,并放到 clock 目录中
/**
* 1.导入模块,创建匹配正则
* 2.使用fs,读取被处理的html文件
* 3.读取成功后,分别拆解和写入html/js/css文件
*/
// 1.导入模块,创建匹配正则
// 导入fs文件系统模块
const fs = require('fs')
// 导入path路径处理模块
const path = require('path')
// 匹配标签正则:\s空白字符,\S非空白字符,*任意多次
const regStyle = /<style>[\s\S]*<\/style>/
const regScript = /<script>[\s\S]*<\/script>/
// 2.使用fs,读取被处理的html文件
fs.readFile(path.join(__dirname, 'index.html'), 'utf8', (err,data) => {
if(err) return console.log('读取html文件失败' + err.message)
// 3.读取文件成功后,分别拆解html/js/css
// 使用正则表达式提取style标签
const r1 = regStyle.exec(data)
//将提取出来的样式字符串,利用 replace 把开始跟结束标签替换成空字符串
const newCSS = r1[0].replace('<style>', '').replace('</style>', '')
// 将提取出来的 css 样式,写入到 index.css 文件中
fs.writeFile(path.join(__dirname,'/index.css'), newCSS, err => {
if(err) return console.log('写入 CSS 样式失败! + err.message)
console.log('写入 CSS 样式成功!)
})
// 使用正则表达式提取js标签
const r1 = regScript.exec(data)
//将提取出来的样式字符串,利用 replace 把开始跟结束标签替换成空字符串
const newJS = r1[0].replace('<script>', '').replace('</script>', '')
// 将提取出来的 css 样式,写入到 index.css 文件中
fs.writeFile(path.join(__dirname,'/index.js'), newCSS, err => {
if(err) return console.log('写入 CSS 样式失败! + err.message)
console.log('写入 CSS 样式成功!)
})
// 使用字符串的 replace 方法,把内嵌的 <style> 和 <script> 标签,替换为外联的<link> 和 <script> 标签
const newHTML = htmlstr
.replace(regStyle,'<link rel="stylesheet" href="./index.css"/>')
.replace(regScript,'<script src="./index.js"></script>')
// 将替换完成之后的 html 代码,写入到 index.html 文件中
fs.writeFile(path.join(__dirname, './index.html'),newHTML, err => {
if(err) return console.log('写入 HTML 文件失败!' + err.message)
console.log('写入 HTML 文件成功!')
})
})
二、http
Node.js官方提供的,用来创建web服务器的模块;通过http服务模块提供的http.createServer()方法,把一台普通电脑变成Web服务器,从而对外提供Web服务资源
概念
IP地址:互联网上每台计算机的唯一地址,因此IP具有唯一性,常用“点分十进制”(a.b.c.d)形式,都是0~255之间的十进制整数。可以在终端中运行 ping 查看IP地址。
客户端:在网络节点中,负责消费资源的电脑
服务器:负责对外提供网络资源的电脑
区别:服务器上安装了例如 IIS、Apache等web服务器软件,就能将一台普通电脑变成服务器
在Node.js中,我们不需要通过IIS、Apache等这些第三方web服务器软件。因为可以基于Node.js提供的http模块,通过几行代码手写一个服务器软件,从而对外提供web服务
域名(DN):字符型的地址方案,即域名地址。跟IP地址一一对应,比IP地址而言方便记忆。
域名服务器(DNS):提供IP地址和域名之间的转换服务。
端口号:类似于门牌号,方便外卖员送快递;而一台电脑也有n个web服务,每个web服务对应一个端口号,客户端通过端口号发送网络请求,来准确地交给web服务器处理。
注意:
-
单纯使用IP地址,在互联网中也能正常工作,但有域名加持,能变得更方便
-
127.0.0.1对应域名是localhost,代表本机电脑,在使用效果上没区别
-
每个端口号不能同时被多个 web 服务占用
-
在实际应用中,URL中的 80 端口可以被省略
创建web服务器
需求: 创建 Web 服务并响应内容给浏览器
步骤:
-
加载 http 模块,创建 Web 服务对象
-
监听 request 请求事件,设置响应头和响应体(req:发送请求,res:返回响应)
-
配置端口号并启动 Web 服务
-
浏览器请求 http://localhost:3000 测试(localhost: 固定代表本机的域名)
// 1. 加载 http 模块,创建 Web 服务对象
const http = require('http')
const server = http.createserver()
// 2. 监听 request 请求事件,设置响应头和响应体
server.on('request', (req, res) => {
// 设置响应头:内容类型,普通文本;编码格式为 utf-8(防止传递中文乱码)
res.setHeader('content-Type','text/plain;charset=utf-8')
res.end("您好,欢迎使用 node.js 创建的 Web 服务" )
console.log(`客户端地址:${req.url},请求类型:${req.method}`) // 客户端地址:page.html,请求类型:GET
})
// 3. 配置端口号并启动 Web 服务
server.listen(3000,() => {
console.1og('web 服务已经启动')
})
例:
基于 web 服务,开发提供网页资源的功能,将前面压缩html的网页展示在页面上
/**
* 目标:基于 Web 服务,开发提供网页资源的功能
* 步骤:
* 1. 基于 http 模块,创建 Web 服务
* 2. 使用 req.url 获取请求资源路径,并读取 index.html 里字符串内容返回给请求方(可用if elseif 来响应不同url的内容)
* 3. 其他路径,暂时返回不存在提示
* 4. 运行 Web 服务,用浏览器发起请求
*/
const fs = require('fs')
const path = require('path')
// 1. 基于 http 模块,创建 Web 服务
const http = require('http')
const server = http.createServer()
server.on('request', (req, res) => {
// 2. 使用 req.url 获取请求资源路径,并读取 index.html 里字符串内容返回给请求方
if (req.url === '/index.html') {
fs.readFile(path.join(__dirname, 'dist/index.html'), (err, data) => {
res.setHeader('Content-Type', 'text/html;charset=utf-8')
res.end(data.toString())
})
} else {
// 3. 其他路径,暂时返回不存在提示
res.setHeader('Content-Type', 'text/html;charset=utf-8')
res.end('你要访问的资源路径不存在')
}
})
server.listen(8080, () => {
console.log('Web 服务启动成功了')
})
优化资源请求路径
上面的访问路径需要写全了127.0.0.1/dist/index.html才能访问,不利于用户访问,所以要优化下路径,只需输入127.0.0.1/ 或 127.0.0.1/index.html就能访问
const fs = require('fs')
const path = require('path')
const http = require('http')
const server = http.createServer()
server.on('request', (req, res) => {
// 预定义空白的文件存放路径
let fpath = ''
if(url ==='/') {
// 如果用户请求的路径为 /,则手动指定文件的存放路径
fpath = path.join(__dirname,'/dist/index.html' )
}else {
// 如果用户请求的路径不为 / ,则动态地拼接文件的存放路径
fpath = path.join(__dirname,'/dist', url)
}
// 根据映射过来的文件路径读取文件
fs.readFile( fpath, 'utf8',(err, dataStr) => {
// 读取文件失败后,向客户端响应固定地错误消息
if(err) return res.end('404 Not found.' )
//读取文件成功后,将“读取成功的内容”响应给客户端
res .end( dataStr )
})
})
server.listen(8080, () => {
console.log('Web 服务启动成功了')
})
三、模块化
commonJS 标准
什么是模块化?
commonJs 模块是为 Nodeis 打包, JavaScript 代码的原始方式。CommonJS 规定了模块的特性和各模块之间如何互相依赖。Node.js 还支持浏览器和其他 Javascript 运行时使用的 ECMAScript 模快标准。在 Nodejs 中,每个文件都被视为一个单独的模块。Node.js 同时遵循 CommonJS 模块化规范
概念:项目是由很多个模块文件组成的
好处:提高代码复用性、可维护性,按需加载,独立作用域
使用:需要标准语法导出和导入进行使用,作为模块之间的联系桥梁
语法
-
导出:module.exports = {}
-
导入:require("模块名 / 模块文件路径")
const baseURL = 'http://hmajax.itheima .net'
const getArraySum = arr => arr,reduce((sum, va) => sum += val, 0)
// 导出
module.exports = {
对外属性名1: baseURL,
getSum: getArraySum
}
// 导入并使用module.exports中的对象
const obj = require('getSum')
模块的分类加载
分类
Node.js 中根据模块来源的不同,将模块分为 3 大类,分别是:
内置模块(由Node.js官方提供、eg:fs、path、http等)
自定义模块(用户创建的每个js文件,都是自定义模块,包括自定义方法,比如上面的getArraySum方法)
第三方模块(有第三方开发出来的模块,非官方提供的,也不是用户创建的,使用前需下载)
加载
跟分类一致,也可以加载需要的内置、自定义、第三方模块进行使用(比如上面加载的自定义模块 getSum)
模块作用域
和函数作用域类似,在自定义模块中定义的变量、方法等成员,只能在当前模块中被访问,这种模块级别的访问限制,叫做模块作用域
好处:防止全局变量污染
模块作用域的成员
module 对象
每个 .js 自定义模块都有,里面存储了和当前模块有关的信息
module.exports 对象
将模块内的成员共享出去,供外界使用
外界用 require() 方法导入自定义模块时,得到的就是 module.exports 所指向的对象
// index.js 在一个自定义模块中,默认情况下
module.exports = {。。。}
// 在外界使用 require 导入一个自定义模块的时候,得到的成员就是通过 module.exports 指向的那个对象
console.log(require('./index.js')) // {。。。}
exports 对象
由于 module.exports 单词写起来比较复杂,为了简化向外共享成员的代码,Node 提供了exports 对象,默认情况下,exports 和 module.exports 指向同一个对象,最终共享的结果,还是以 module.exports 指向的对象为准
console.log(exports) // {}
console.log(module.exports) // {}
console.log(exports === module.exports) // true
使用误区
exports 和 module.exports 同时使用时,得到的永远都是 module.exports 指向的对象
为了防止混乱,还是不要在同一个模块中共同使用 exports 和 module.exports
ECMAScript 标准
默认导出和导入
语法
-
导出:export default {}
-
导入:import 变量名 from '模块名或路径'
注意!如需使用 ECMAScript 标准语法,需在运行模块所在文件夹新建 package.json 文件,并设置 { "type" : "module" }
// index.js
// 导出
export default {
const baseURL = 'http://hmajax.itheima.net'
const getArraySum = arr => arr,reduce((sum, va) => sum += val, 0)
...
}
// 导入(全部加载)
import base from './index.js'
// 按需加载:使用对象解构来获取 export 里具体的方法
import { baseURL, getArraySum } from './index.js'
命名导出和导入
语法
-
导出:export 修饰定义语句
-
导入:import { 同名变量 } from '模块名或路径‘
// index.js
// 导出
export const baseURL = 'http://hmajax.itheima.net'
export const getArraySum = arr => arr,reduce((sum, va) => sum += val, 0)
// 导入
import baseURL from './index.js'
import { baseURL, getArraySum } from './index.js'
总结
node.js 模块化
-
概念:每个文件当做一个模块,独立作用域,按需加载
-
使用:采用特定的标准语法导出和导入进行使用
使用环境
-
CommonJS 标准:一般应用在 Node.js 项目环境中
-
ECMAScript 标准:一般应用在前端工程化项目中
四、Express
概念
Express是基于 Node.js 平台,快速、开放、极简的 Web 开发框架
通俗地说就是:Express 和 Node.js内置的 http 模块类似,是专门用来创建 Web 服务器的
Express 的本质:就是一个 npm 上的第三方包,提供了快速创建 Web 服务器的便捷方法
进一步理解
不使用 Express 能否创建 Web 服务器?
答:能,使用 Node.js 提供的原生 http 模块即可
既有了 http 模块,为什么还要用 express ?
答:http 模块用起来很复杂,开发效率低;Express 是基于内置的 http 模块进一步封装出来的,能够极大地提高开发效率
http 内置模块与 Express 是什么关系?
答:类似于浏览器中 Web API 和 jQuery 的关系。后者是基于前者进一步封装出来的
Express 的作用
对前端程序员来说,常见的两种服务器,分别是:
-
Web 网站服务器:专门对外提供 Web 网页资源的服务器
-
API 接口服务器:专门对外提供 API 接口的服务器
使用 Express,我们可以方便、快速地创建 Web 网站的服务器或 API 接口的服务器
安装和使用
基本步骤
-
安装express:npm i express@4.17.1,建议版本号跟老师保持一致,方便后续开发
-
创建基本的 web 服务器:导入express、创建 web 服务器、启动服务器
-
监听 get、post 请求
-
把内容响应(发送)给客户端
-
运行服务器:输入命令行 node 基本服务器.js,跑起来的服务器要依靠 postman / apifox 等第三方接口工具来测试是否跑通
postman 是一个用于构建和使用 API 的 API 平台。Postman 简化了 API 生命周期的每个步骤并简化了协作,因此您可以更快地创建更好的 API。它可以自定义请求URL、请求的类型【GET,POST等】,可以加入Head头信息以及HTTP body信息等,让我们简单直观的进行HTTP请求测试,简单来说就是测试接口的工具。另外,因为是全英文的,所以还得下载 汉化包 进行汉化,并关闭自动更新,防止更新完后汉化失败
/* 基本服务器.js */
// 导入express
const express = require('express')
// 创建 web 服务器
const app = express()
// 通过 app.get() ,可以监听客户端的 GET 请求
app.get('/user',(req, res) => {
// 通过 res.send() ,向客户端发送JSON对象
res.send({name: 'zs', age: 20, gender: '男'})
})
// 通过 app.post() ,可以监听客户端的 POST 请求
app.post('/user',(req, res) => {
// 通过 res.send() ,向客户端响应一个文本字符串
res.send('请求成功')
})
// 调用 app.listen(端口号,成功启动后的回调函数),启动服务器
app.listen(80,() => {
// console.log('express server running at http://127.0.0.1')
})
获取 URL 中携带的查询参数
通过 req.query 对象,可以获取到客户端发过来的 查询参数
-
req.query 默认是一个空对象
-
客户端使用 ?name=zs&age=20 这种查询字符串形式,发送到服务器的参数,可以通过 req.query 对象访问到,例如:req.query.name、req.query.age
app.get('/user',(req, res) => {
// 通过 res.send() ,向客户端发送JSON对象
res.send({name: 'zs', age: 20, gender: '男'})
console.log(req.query.name) // zs
})
例:
/* 基本服务器.js */
const express = require('express')
const app = express()
app.get('/user',(req, res) => {
res.send({name: 'zs', age: 20, gender: '男'})
})
app.post('/user',(req, res) => {
res.send('请求成功')
})
app.get('/',(req, res) => {
// 通过req.query可以获取到客户端发送过来的 查询参数
console.log(req.query) // {name: 'zs', age: 20, gender: '男'}
// 把req.query响应到客户端
res.send(req.query)
})
app.listen(80,() => {
console.log('express server running at http://127.0.0.1')
})
// 运行服务器:输入命令行 node 基本服务器.js,效果如下图所示:
并且这种查询字符串的方式也能同步到下面的请求体键值里
获取 URL 中的动态参数
通过 req.params 对象,可以访问到 URL 中,通过 “:” 匹配到的 动态参数:
// 注意:这里的 :id 是一个动态值
app.get('/user/:id',(req,res) => {
// req.params 是动态匹配到的url参数,默认也是一个空对象
console.log(req.params)
res.send(req.params)
})
// 可以传递多个动态值
app.get('/user/:id/:name',(req,res) => {
console.log(req.params)
res.send(req.params)
})
// 运行服务器:输入命令行 node 基本服务器.js,效果如下图所示:
// (后面就不再重复写这句启动服务器的命令提示,启动命令都是 “node 文件名”)
托管静态资源
托管单个
express.static(),创建一个静态资源服务器,快速对外提供静态资源
注意:在指定的静态目录中查找文件,并对外提供资源的访问路径。因此,存放静态文件的目录名(eg: public)不会出现在 URL 中
/* eg:将 public 目录下的图片、html文件对外开放访问 */
const express = require('express')
const app = express()
// 调用express.static(),访问 public 目录下的所有文件(比如001.jpg图片和网址index.html)
app.use(express.static('./public'))
app.listen(80, () => {
console.log('express server running at http://127.0.0.1')
})
托管多个
多次调用 express.static() 方法即可
// 如果 public 跟 files 文件夹下都有相同名字的文件,则按代码添加顺序查找所需文件
app.use(express.static('public'))
app.use(express.static('files'))
挂载路径前缀
要想存放静态文件的目录名(eg:public)出现在 URL 中,则可以在前面挂载路径前缀
// 这样就可以通过带有 /public 前缀地址来访问 public 文件夹中的文件了
app.use('public', express.static('public'))
// eg:127.0.0.1/public/001.jpg
nodemon(热重启)
在编写调试 Node.js 项目时,如果修改了项目的代码,则需要频繁地手动 close 掉,再重启,非常地繁琐,所以可以使用 nodemon 来监听项目文件的变动、当代码被修改后,nodemon 会帮我们自动重启项目,极大地方便开发和调试。
步骤:
-
安装插件:npm i nodemon -g(全局安装)
-
使用插件:nodemon app.js
在基于 Node.js 编写了一个网站应用后,传统的方式,是运行 node app.js 命令来启动项目,这样做的话,每次修改都需要手动重启项目;而现在,我们可以将 node 命令替换为 nodemon 命令,使用 来启动项目,这样做的话,代码每次修改都会被 nodemon 监听到,从而实现自动重启项目的效果
express 路由
概念
广义上来讲,路由就是映射关系,比如现实中人工客服电话按键对应具体功能(1->业务查询...)的映射,而 express路由则指的是:
客户端的请求与服务器处理函数之间的映射关系
Express 中的路由分 3 部分组成,分别是请求的类型、请求的URL地址、处理函数,格式为
app.method(path,handler)
// eg:
app.get('/', (req,res) => {
res.send('get a GET request!')
})
路由的匹配过程
每当一个请求到达服务器之后,需要先经过路由的匹配,只有匹配成功之后,才会调用对应的处理函数
在匹配时,会按照路由的顺序进行匹配,如果请求类型和请求 URL 同时匹配成功,则 Express 会将这次请求,转交给对应的 function 函数进行处理。
路由的基本使用
采用原始的基本办法:把路由挂载到 app 上,运行效果看之前的 “创建基本的 web 服务器” 即可,这里不重复演示
/* 基本服务器.js */
// 导入express
const express = require('express')
// 创建 web 服务器
const app = express()
app.get('/user',(req, res) => {
res.send({name: 'zs', age: 20, gender: '男'})
})
app.post('/user',(req, res) => {
res.send('请求成功')
})
app.listen(80,() => {
console.log('express server running at http://127.0.0.1')
})
路由的模块化
为了方便对路由进行模块化的管理,Express 不建议直接将路由挂载到 app 上,而是推荐将路由抽离为单独的模块。步骤如下:
-
创建路由模块对应的 .js 文件
-
调用 express.Router() 函数创造路由对象
-
向路由对象上挂载具体的路由
-
使用 module.exports 向外共享路由对象
-
使用 app.use() 函数注册路由模块
注意:app.use() 函数的作用:就是用来注册全局中间件
/* router.js */
const express = require('express') // 导入express
const router = express.Router() // 创建路由对象
// 挂载具体的路由
router.get('/user/list', (req,res) => {
res.send('Get user list')
})
router.post('/user/add', (req,res) => {
res.send('Add new user')
})
module.exports = router // 导出路由
/* 模块化路由.js */
const express = require('express')
const app = express()
const router = require('./router') // 导入上面定义的路由模块
app.use(router) // 注册并使用路由模块
// app.use('/api', router) // 挂载路由前缀(后面演示需要用到的例子)
app.listen(80,() => {
console.log('express server running at http://127.0.0.1')
})
挂载路由前缀
类似于托管静态资源时,为静态资源统一挂载访问前缀一样,路由模块添加前缀的方式也非常简单,例子就不重复写,上面演示了
// 导入路由模块
const userRouter = require(' ./router/user.js')
// 使用 app.use() 注册路由模块,并添加统一的访问前缀 /api
app.use('/api',userRouter)
express 中间件
概念
中间件(Middleware),特指业务流程的中间处理环节,比如现实中,在处理污水的时候,一般要经过三个处理环节,从而保证处理过后的废水,达到排放标准,处理污水的这三个中间处理环节,就可以叫做中间件。
中间件的调用流程
当一个请求达到 Express 服务器之后,可以连续调用多个中间件,从而对这次请求进行预处理
中间件的格式
Express 中间件,本质上来说就是 function 处理函数,其格式如下
注意:中间件函数的形参列表中,必须包含 next 参数,而路由处理函数中只包含 req 和 res
next函数的作用
next 函数是实现多个中间件连续调用的关键,它表示把流转关系转交给下一个中间件或路由
基本使用
const express = require('express')
const app = express()
// 定义一个最简单的中间件函数
const mw = function(req, res, next) {
console.log("这是一个最简单的中间件函数')
//注意: 在当前中间件的业务处理完毕之后,必须调用 next() 函数
// 表示把流转关系转交给下一个中间件或路由
next()
}
app.listen(80,() => {
console.log('http://127.0.0.1')
})
全局中间件
客户端发起的任何请求,到达服务器之后,都会触发的中间件,叫做全局生效的中间件
通过调用 app.use( 中间件函数 ),即可定义一个全局生效的中间件
const express = require('express')
const app = express()
// 常量mw所指向的,就是一个中间件函数
const mw = function(req, res, next) {
console.log("这是一个最简单的中间件函数')
//注意: 在当前中间件的业务处理完毕之后,必须调用 next() 函数
// 表示把流转关系转交给下一个中间件或路由
next()
}
app.use(mw) // 全局中间件
app.get('/', (req,res) => {
console.log('调用了/这个路由')
res.send('home page')
})
app.get('/user', (req,res) => {
console.log('调用了/user这个路由')
res.send('user page')
})
app.listen(80,() => {
console.log('http://127.0.0.1')
})
简化、作用
中间件的简化:将中间件函数直接写在 app.use() 里即可。
中间件的作用:多个中间件之间,共享同一份 req 和 res 。基于这样的特性,我们可以在上游的中间件里,统一为 req 或 res 对象添加自定义的属性或方法,以供下游的中间件或路由进行使用
const express = require('express')
const app = express()
// 常量mw所指向的,就是一个中间件函数
// const mw = function(req, res, next) {
// console.log("这是一个最简单的中间件函数')
// //注意: 在当前中间件的业务处理完毕之后,必须调用 next() 函数
// // 表示把流转关系转交给下一个中间件或路由
// next()
//}
//app.use(mw) // 全局中间件
// 简化形式
app.use((req,res,next) => {
// 获取请求到达服务器的时间
const time = Date.now()
req.startTime = time
next()
})
app.get('/', (req,res) => {
res.send('home page' + req.startTime)
})
app.get('/user', (req,res) => {
res.send('user page' + req.startTime)
})
app.listen(80,() => {
console.log('http://127.0.0.1')
})
多个全局中间件
可以使用 app.use() 连续定义多个全局中间件,客户端请求到达服务器后,会按照中间件定义的先后顺序依次调用
const express = require('express')
const app = express()
app.use((req,res,next) => {
console.log('调用了第一个全局中间件')
next()
})
app.use((req,res,next) => {
console.log('调用了第二个全局中间件')
next()
})
app.get('/user', (req,res) => {
res.send('home page')
})
app.listen(80,() => {
console.log('http://127.0.0.1')
})
局部生效中间件
不使用 app.use() 定义的中间件,叫做局部生效的中间件
const express = require('express')
const app = express()
const jb = function(req,res,next){
console.log('调用了局部生效的中间件')
next()
}
// jb这个中间件只在“当前路由中生效”,这种用法属于“局部生效的中间件”
app.get('/', jb, (req,res) => {
res.send('Home page')
})
// jb中间件不会影响下面的路由
app.get('/user', (req,res) => {
res.send('User page')
})
app.listen(80,() => {
console.log('http://127.0.0.1')
})
多个局部中间件
// 在路由中,通过如下两种等价方式,使用多个局部中间件(根据个人喜好选一种用即可,上面写了很多例子,这里就不再演示)
app.get('/', jb1, jb2, (req,res) => { res.send('Home page')})
app.get('/', [jb1, jb2], (req,res) => { res.send('Home page')})
注意事项
-
一定要在路由之前注册中间件
-
客户端发送过来的请求,可以连续调用多个中间件进行处理
-
执行完中间件的业务代码后,不要忘记调用 next() 函数
-
为了防止代码逻辑混乱,调用 next() 函数之后不要再写额外代码
-
连续调用多个中间件时,多个中间件之间,共享 req 和 res 对象
中间件的分类
为了方便记忆和使用, Express 官方把常见的中间件用法,分成 5 大类,分别是:
应用级别、路由级别、错误级别、Express 内置、第三方
应用级别
通过 app.use() 或 app.get() 或 app.post(),绑定到 app 实例上的中间件
// 应用级别的中间件(全局中间件)
app.use((req,res,next)) => {
next()
}
// 应用级别的中间件(局部中间件)
app.get('/', jb, (req,res,next) => {
res.send('Home page')
})
路由级别
绑定到 express .Router() 实例上的中间件,叫做路由级别的中间件。它的用法和应用级别中间件有任何区别,只不过,应用级别中间件是绑定到 app 实例上,路由级别中间件绑定到 router 实例上
let app = express()
let router = express.Router()
// 路由级别的中间件
router.use(function(req,res,next) => {
console.log('time',Date.now())
next()
})
app.use('/', router)
错误级别
专门用来捕获整个项目中发生的异常错误,从而防止项目异常崩溃的问题
注意:错误级别的中间件,必须注册在所有路由之后,跟之前的路由之前注册中间件相反
let express = require('express')
let app = express()
app.get('/', (req,res) => { // 路由
throw new Error('服务器内部发生错误!') // 抛出一个自定义错误
res.send( ' Home page ' )
})
// 错误级别的中间件 err
app.use((err, req, res, next) => {
console.log('发生了错误' + err.message) // 在服务器打印错误消息
res.send('Error!' + err.message ) // 向客户端发送错误消息
})
app.listen(80,() => {
console.log('http://127.0.0.1')
})
内置级别
自 Express 4.16.0 版本开始,Express 内置了 3 个常用的中间件,极大地提高了 Express 项目的开发效率和体验:
-
express.static 快速托管静态资源,eg:HTML文件、图片、CSS样式等(无兼容性),这个例子在上面的托管静态资源里有
-
express.json 解析JSON 格式的请求体数据(有兼容性,仅在 4.16.0+ 版本中可用)
-
express.urlencoded 解析 URL-encoded 格式的请求体数据(有兼容性,同2)
// 配置解析 application/json 格式数据的内置中间件
app.use(express.json())
// 配置解析 application/x-wwww-form-urlencoded 格式数据的内置中间件
app.use(express.urlencoded({extended: false}))
简单来说:
需要 req.body 属性接收客户端发送过来的 json 数据,并配置 express.json() 中间件解析 json 数据,才能在服务器端显示接收的数据
请求体数据:从客户端发送过来的请求数据,这里使用的 postman 接口测试软件也相当于从客户端发送请求数据。
例:express.json 解析JSON 格式的请求体数据
let express = require('express')
let app = express()
app.use(express.json) // 通过 express.json() 这个中间件,解析表单中的JSON格式的数据
app.post('/', (req, res) => {
// 在服务器,可用req.body来接收客户端发送过来的请求体数据
// 默认情况下,如果不配置解析表单数据的中间件(上面的注释掉),则打印undefind(注意:请求体数据都要在 body 中发送)
console.log(req.body)
res.send('ok')
})
app.listen(80, () => {
console.log('http://127.0.0.1')
})
例:express.urlencoded 解析 URL-encoded 格式的请求体数据
let express = require('express')
let app = express()
app.use(express.urlencoded({ extend: false })) // 解析表单中 url-encoded 格式的数据
app.post('/', (req,res) => {
console.log(req.body)
res.send('ok')
})
app.listen(80, ()=> {
console.log('http://127.0.0.1')
})
第三方的
非 Express 官方内置的,而是由第三方开发出来的中间件,叫做第三方中间件
在项目中,可以按需下载并配置第三方中间件,从而提高项目的开发效率。
在 express@4.16.0之前的版本中,经常使用 body-parser 这个第三方中间件,来解析请求体数据,使用步骤如下:
-
运行 npm install body-parser 安装中间件
-
使用 require() 导入中间件
-
调用 app.use() 注册并使用中间件
注意: Express 内置的 express.urlencoded 中间件,就是基于 body-parser 这个第三方中间件进一步封装出来的,所以这俩中间件的注册才非常像
let express = require('express')
let app = express()
// 导入body-parser
let parser = require('body-parser')
// 注册使用中间件
app.use(parser.urlencoded({ extended: false}))
app.post('/',(req,res) => {
console.log(req.body)
res.send('ok')
})
app.listen(80, () => {
console.log('http://127.0.0.1')
})
自定义的
自己手动模拟一个类似于 express.urlencoded 这样的中间件,来解析 POST 提交到服务器的表单数据,步骤如下:
-
自定义中间件:app.use()
-
监听 req 的 data 事件,获取客户端发送到服务器的数据
如果数据比较大,无法一次性发送完毕,则客户端会把数据切割后,分批发送到服务器。所以 data 事件可能会触发多次,每一次触发 data 事件时,获取到数据只是完整数据的一部分,需要手动对接收到的数据进行拼接
-
监听 req 的 end 事件,拿到并处理完整的请求体数据
-
使用 querystring 模块解析请求体数据,把查询的字符串,解析成对象的格式
如果不解析的话,拿到的中文会被转码成一些特殊符号
-
将解析出来的数据对象挂载为 req.body
上游的中间件和下游的中间件及路由之间,共享同一份 req 和 res。因此,我们可以将解析出来的数据,挂载为 req 的自定义属性,命名为 req.body,以供下游使用
-
将自定义中间件封装为模块
/* custom-body-parser.js */
// 4.导入 Node.js 内置的 querystring 模块,用来处理查询字符串的解析
const qs = require('querystring')
const bodyParser = (req, res, next) => {
// 定义中间件具体业务逻辑
// 定义一个 str 字符串,用来存储客户端发送过来的请求体数据
let str = ''
// 2.监听 req 对象的 data 事件
req.on('data', (chunk) => {
// 拼接请求体数据,隐式转换为字符串
str += chunk
})
// 3.监听 req 对象的 end 事件
req.on('end', () => {
// 在 str 中存放的是完整的请求体数据
// console.log(str)
// 4. 使用 querystring 模块解析请求体数据,把字符串解析成对象格式
const body = qs.parse(str)
// 5. 将解析出来的数据对象挂载为 req.body
req.body = body
next()
})
)
// 6.将自定义中间件封装为模块并导出
module.exports = bodyParser
const express = require('express')
const app = express()
// 1.前面自定义好中间件后,这里导入进来使用
const customBodyParse = require('./custom-body-parser')
app.use(customBodyParse)
app.post('/user', (req, res) => {
res.send(req.body)
})
app.listen(80, () => {
console.log('http://127:0.0.1')
})
使用 express 写接口
创建步骤
-
创建 API 路由模块
-
创建基本的服务器,导入路由模块
-
编写 GET、POST 接口,挂载对应路由
-
CORS 跨域资源共享(get、post接口不支持跨域请求,需要借助CORS实现跨域资源共享,记得安装中间件:npm i cors)
/* apiRouter.js */
// 1.创建 API 路由模块
const express = require('express')
const router = express.Router
// 3.编写GET接口
router.get( '/get', (req, res) => {
// 获取到客户端通过查询字符串,发送到服务器的数据
const query = req.query
// 调用 res.send() 方法,把数据响应给客户端
res.send({
status: 0, // 状态,0表示成功,1表示失败
msg: 'GET请求成功!' //状态描述
data: query // 需要响应给客户端的具体数据
})
}
// 3.编写POST接口
router.post('/post', (req, res) => {
// 获取客户端通过请求体,发送到服务器的 URL-encoded 数据
const body = req.body
// 调用res.send()方法,把数据响应给客户端
res.send({
status: 0, // 状态,0表示成功,1表示失败
msg: 'POST请求成功!', // 状态描述消息
data: body // 需要响应给客户端的具体数据
})
module.exports = router
// 2.创建基本的服务器
const express = require('express')
const app = express()
// 注意!如果要获取 URL-encoded 格式的请求体数据,必须要配置解析表单数据的中间件
app.use(express.urlencoded({ extend: false} ))
// 4.CORS 跨域资源共享
const cors = require('cors')
app.use(cors())
// 导入并使用路由模块
const router = require('./apiRouter')
app.use('/api', router)
app.listen(80, () => {
console.log('express server running at http://127.0.0.1')
})
接口的跨域问题
在上面express接口里没有写上CORS 跨域资源共享的有关代码时是会报跨域请求错误的,错误原因如下:
解决跨域问题通常有两种方法,一种是上面的 CORS(主流方案),一种是 JSONP(有缺陷,只支持 GET 请求)后面将详细讲解下
CORS
概念
CORS(Cross-Origin Resource Sharing,跨域资源共享)由一系列 HTTP 响应头组成,这些 HTTP 响应头决定浏览器是否阻止前端 JS 代码跨域获取资源。
浏览器的同源安全策略默认会阻止网页 “跨域” 获取资源。但如果接口服务器配置了 CORS 相关的 HTTP 响应头,就可以解除浏览器端的跨域访问限制。
CORS 主要在服务器端进行配置,客户浏览器无须做任何额外的配置,即可请求开启了 CORS 的接口
CORS 在浏览器中有兼容性,只有支持 XMLHttpRequest Level2 的浏览器,才能正常访问开启 CORS 的服务端接口(eg:IE10+、Chrome4+、FireFox3.5+)
CORS响应头
Origin
响应头部中可以携带一个 Access-Control-Allow-Origin 字段,后面跟随的参数指定了允许访问该资源的外域 URL
这三个前面都一样是Access-Control-Allow,所以标题就简写了
// 规定只允许来自 “http://itcast.cn” 的请求
res.setHeader('Access-Control-Allow-Origin', 'http://itcast.cn')
// 允许任何域的请求
res.setHeader('Access-Control-Allow-Origin', '*')
Headers
默认情况下,CORS 仅支持客户端向服务器发送如下的 9 个请求头:
Accept、Accept-Language、Content-Language、DPR、Downlink、Save-Data、Width、Viewport-Width、Content-Type(值仅限于 text/plain、multipart/form-data、application/x-www-form-urlencoded 三者之一)
如果客户端向服务器发送了额外的请求头信息,则需要在服务器端,通过 Access-Control-Allow-Headers 对额外的请求头进行声明,否则这次请求会失败!
// 允许客户端额外向服务器发送 Content-Type 请求头和 X-Custom-Header 请求头
// 注意:多个请求头之间使用英文的逗号进行分割
res.setHeader('Access-Control-Allow-Headers','Content-Type, X-Custom-Header')
Methods
默认情况下,CORS 仅支持客户端发起 GET、POST、HEAD 请求。如果客户端希望通过 PUT、DELETE 等方式请求服务器的资源,则需要在服务器端,通过 Access-Control-Allow-Methods 来指明实际请求所允许使用的 HTTP 方法。
// 只允许 POST,GET,DELETE,HEAD 请求方法
res,setHeader('Access-Control-Allow-Methods','POST,GET,DELETE,HEAD')
// 允许所有的 HTTP 请求方法
res.setHeader('Access-Control-Alow-Methods','*'
CORS请求分类
简单请求
同时满足以下两个条件的请求,就属于简单请求:
-
请求方式:GET、POST、HEAD 三者之一
-
HTTP 头部信息不超过一下几种字段:无自定义头部字段、Accept、Accept-Language、Content-Language、DPR、Downlink、Save-Data、Viewport-Width、Width、Content-Type(只有三个值 application/x-www-form-urlencoded、multipart/form-data、text/plain)
预检请求
只要符合以下任何一个条件的请求,都需要进行预检请求:
-
请求方式为 GET、POST、HEAD 之外的请求 Method 类型
-
请求头中包含自定义头部字段
-
向服务器发送了 application/json 格式的数据
在浏览器与服务器正式通信之前,浏览器会先发送 OPTION 请求进行预检,以获知服务器是否允许改实际请求,所以这一次的 OPTION 请求被称为 “预检请求”。
服务器成功响应预检请求后,才会发送真正的请求,并携带真实数据
二者的区别
简单请求:客户端与服务器之前只会发生一次请求
预检请求:客户端与服务器之间会发生两次请求,OPTION 预检请求成功后,才会发起真正的请求
<!DOCTYPE htm1>
<htm1>
<head>
<meta charset="utf-8">
<title></title>
<script src="https://cdn.staticfile.org/jquery/1.10.@/jquery.min.js"></script></head>
<body>
<button id="btnDelete">DELETE</button>
<script>
$(function() {
//为删除按钮绑定点击事件处理函数
$('#btnDelete').on('click',function() {
$.ajax({
type: .'DELETE',
url:.'http://127.0.0.1/api/delete',
success:function(res) {
console.log(res)
}
})
}
</script>
</body>
</html>
/* apiRouter.js */
const express = require('express')
const router = express.Router()
router.delete('/delete', (req, res) => {
res.send({
status: 0,
msg: 'DELETE请求成功!'
})
})
module.exports = router
JSONP
概念
浏览器端通过 <script> 标签的 src 属性,请求服务器上的数据,同时,服务器返回一个函数的调用。这种请求数据的方式叫做 JSONP
特点:
-
JSONP 不属于真正的 Ajax 请求,因为它没有使用 XMLHttpRequest 这个对象
-
JSONP 仅支持 GET 请求,不支持 POST、PUT、DELETE 等请求
注意事项
如果项目中已经配置了 CORS 跨域资源共享,为了防止冲突,必须在配置 CORS 中间件之前声明 JSONP 的接口,否则 JSONP 接口会被处理成开启了 CORS 的接口
const express = require('express')
const app = express()
app.use(express.urlencoded({ extended: false }))
// 优先创建JSONP接口,这个接口不会被处理成cors接口
app.get('/api/jsonp', (req, res) => {
// TODO:定义了JSONP接口的具体实现过程
})
// 再创建cors中间件,后续的接口都会被处理成cors接口
const cors = require('cors')
app.use(cors())
const router = require('./apiRouter')
app.use('/api', router)
app.listen(80, () => {
console.log('http://127.0.0.1')
})
创建步骤
-
获取客户端发送过来的回调函数的名字
-
定义要发送给客户端的数据对象
-
拼接出一个函数调用的字符串
-
把拼接的字符串,响应给客户端的 <script> 标签进行解析执行
const express = require('express')
const app = express()
app.use(express.urlencoded({ extended: false }))
app.get('/api/jsonp', (req, res) => {
// 1.获取客户端发送过来的回调函数的名字
const funcName = req,query.callback
// 2.定义要发送给客户端的数据对象
const data = { name: wjh, age: 25 }
// 3.拼接出一个函数调用的字符串
const scriptStr = `${funcName}(${JSON.stringify(data)})`
// 4.把拼接的字符串,响应给客户端的 <script> 标签进行解析执行
res.send(scriptStr)
})
const cors = require('cors')
app.use(cors())
const router = require('./apiRouter')
app.use('/api', router)
app.listen(80, () => {
console.log('http://127.0.0.1')
})
在网页中使用 jQuery,调用 $.ajax() 函数,提供 JSONP 的配置选项,从而发起 JSONP 请求
<!DOCTYPE htm1>
<htm1>
<head>
<meta charset="utf-8">
<title></title>
<script src="https://cdn.staticfile.org/jquery/1.10.@/jquery.min.js"></script></head>
<body>
<button id="btnDelete">DELETE</button>
<script>
$(function() {
//为删除按钮绑定点击事件处理函数
$('#btnDelete').on('click',function() {
$.ajax({
type: .'DELETE',
url:.'http://127.0.0.1/api/delete',
success:function(res) {
console.log(res)
}
})
}
</script>
</body>
</html>
五、关联数据库
步骤
-
安装操作 MySQL 数据库的第三方模块:npm i mysql
-
配置 mysql 模块,连接到 MySQL 数据库,并测试是否连接成功
-
通过 mysql 模块执行 SQL 语句,操作数据库
用到的软件:phpstudy_pro(开启数据库服务)、Navicat Premium 15(需提前做好建库建表工作,不然下面操作时会报错)
// 2.导入 mysql 模块
const mysql = require('mysql')
// 2.建立与 MySQL 据库的连接
const db = mysq1.createPool({
host: '127.0.0.1' // 据库的 IP 地址
user: ' root' // 登录数据库的账号
password:admin123 ,// 登录数据库的密码
database: my_db_01 // 指定要操作哪个数据库
})
// 测试mysql模块是否正常工作,是否连接成功
db.query('select 1', (err, results) => {
// mysql 模块工作期间报错
if(err) return console.log(err.message)
// 能够成功执行sql语句
console.log(results)
})
SQL语句:增删改查
const mysql = require('mysql')
const db = mysq1.createPool({
host: '127.0.0.1' // 据库的 IP 地址
user: ' root' // 登录数据库的账号
password:admin123 ,// 登录数据库的密码
database: my_db_01 // 指定要操作哪个数据库
})
// 查询
db.query('select * from users', (err, results) => {
if(err) return console.log(err.message)
console.log(results)
})
// 增加数据
// 要插入到 users 表中的数据对象
const user = { username: 'Spider-Man', password: 'pcc321' }
// 待执行的 SQL 语句,其中英文的 ? 表示占位符
const sqlstr = 'INSERT INTO users (username, password) VALUES (?, ?)'
// 使用数组的形式,依次为 ? 占位符指定具体的值
db.query(sqlStr,[user.username, user.password], (err, results) => {
if (err) return console.log(err.message) // 失败
if(results.affectedRows === 1) { console.log('插入数据成功') } // 成功
})
// 修改数据
const user = { id: 2, username: 'Spider', password: 'pcc123' }
const sqlstr = 'UPDATE users SET username=?,password=? WHERE id=?'
db.query(sqlStr,[user.id, user.username, user.password], (err, results) => {
if (err) return console.log(err.message) // 失败
if(results.affectedRows === 1) { console.log('插入数据成功') } // 成功
})
// 删除数据
const sqlStr = 'DELETE FROM users WHERE id=?'
// 在删除数据时,推荐根据 id 这样的唯一标识,来删除对应的数据
db.query(sqlStr, 2, (err, results) => {
if (err) return console.log(err.message) // 失败
if(results.affectedRows === 1) { console.log('插入数据成功') } // 成功
})
快速增改数据
添加表数据时,如果数据对象的每个属性和数据表的字段一一对应,可以通过 set ? 方式快速添加数据
更新表数据时,如果数据对象的每个属性和数据表的字段一一对应,则可以通过 set ? where id=? 方式快速修改数据
const mysql = require('mysql')
const db = mysq1.createPool({
host: '127.0.0.1' // 据库的 IP 地址
user: ' root' // 登录数据库的账号
password:admin123 ,// 登录数据库的密码
database: my_db_01 // 指定要操作哪个数据库
})
// 增加数据
// 向表中新增数据时,如果数据对象的每个属性和数据表的字段一一对应,也可以直接将数据对象当占位符,效果同上
const user = { username: 'Spider-Man', password: 'pcc321' }
// 待执行的 SQL 语句,其中英文的 ? 表示占位符
const sqlstr = 'INSERT INTO users SET ?'
// 使用数组的形式,依次为 ? 占位符指定具体的值
db.query(sqlStr, user, (err, results) => {
if (err) return console.log(err.message) // 失败
if(results.affectedRows === 1) { console.log('插入数据成功') } // 成功
})
// 修改数据
// 修改表中数据时,如果数据对象的每个属性和数据表的字段一一对应,也可以直接将数据对象当占位符,效果同上
const user = { id: 2, username: 'Spider-Man', password: 'pcc321' }
// 待执行的 SQL 语句,其中英文的 ? 表示占位符
const sqlstr = 'UPDATE users SET ? WHERE id=?'
// 调用 db.query() 执行时,使用数组依次为占位符指定具体值
db.query(sqlStr, [user, user.id], (err, results) => {
if (err) return console.log(err.message) // 失败
if(results.affectedRows === 1) { console.log('插入数据成功') } // 成功
})
标记删除数据
使用 DELETE 语句,会把真正的把数据从表中删除掉。为了保险起见,推荐使用标记删除的形式,来模拟删除的动作。
所谓的标记删除,就是在表中设置类似于 status这样的状态字段,来标记当前这条数据是否被删除。
当用户执行了删除的动作时,我们并没有执行 DELETE 语句把数据删除掉,而是执行了 UPDATE 语句,将这条数据对应的 status 字段修改为删除即可。
const mysql = require('mysql')
const db = mysq1.createPool({
host: '127.0.0.1' // 据库的 IP 地址
user: ' root' // 登录数据库的账号
password:admin123 ,// 登录数据库的密码
database: my_db_01 // 指定要操作哪个数据库
})
// 标记删除
const sqlDeleteX = 'update users set status=? where id=?'
db.query(sqlDeletex, [1, 2], (err, results) => {
if(err) return console.log(err.message)
if(results.affectedRows === 1) {console.log("标记删除成功")}
})
六、前后端的身份认证
目前主流的 Web 开发模式有两种,分别是:基于服务端渲染的传统 Web 开发模式、基于前后端分离的新型 Web 开发模式
web 开发模式
服务端渲染
概念
服务器发送给客户端的 HTML 页面,是在服务器通过字符串的拼接,动态生成的。
因此,客户端不需要使用 Ajax 这样的技术额外请求页面的数据
app.get('/index.html',(req,res) => {
// 1.要渲染的数据
const user = { name: 'zs' , age: 20 ]
// 2.服务器端通过字符串的拼接,动态生成 HTML 内容
const html =<h1>姓名: ${user.name},年龄: ${user.age}</h1>
// 3.把生成好的页面内容响应给客户端。因此,客户端拿到的是带有真实数据的 HTML 页面
res.send(html)
}
优缺点
优点:
-
前端耗时少。因为服务器端负责动态生成 HTML 内容,浏览器只需要直接渲染页面即可。尤其是移动端,更省电。
-
有利于SEO。因为服务器端响应的是完整的 HTML 页面内容,所以爬虫更容易爬取获得信息,更有利于 SEO。
缺点:
-
占用服务器端资源。即服务器端完成 HTML 页面内容的拼接,如果请求较多,会对服务器造成一定的访问压力。
-
不利于前后端分离,开发效率低。使用服务器端渲染,则无法进行分工合作,尤其对于前端复杂度高的项目,不利于项目高效开发。
前后端分离
概念
前后端分离的开发模式,依赖于 Ajax 技术的广泛应用。
简而言之,前后端分离的 Web 开发模式,就是后端只负责提供 API 接口,前端使用 Ajax 调用接口的开发模式
优点:
-
开发体验好。前端专注于 UI 页面的开发,后端专注于api的开发,且前端有更多的选择性。
-
用户体验好。Ajax 技术的广泛应用,极大的提高了用户的体验,可以轻松实现页面的局部刷新。
-
减轻了服务器端的渲染压力。因为页面最终是在每个用户的浏览器中生成的。
缺点:
不利于 SEO。因为完整的 HTML 页面需要在客户端动态拼接完成,所以爬虫对无法爬取页面的有效信息。
(解决方案:利用 Vue、React 等前端框架的 SSR(server side render)技术能够很好的解决 SEO 问题!)
二者的选择
不谈业务场景而盲目选择使用何种开发模式都是耍流氓。
-
比如企业级网站,主要功能是展示而没有复杂的交互,并且需要良好的 SEO,则这时我们就需要使用服务器端渲染;
-
而类似后台管理项目,交互性比较强,不需要考虑 SEO,那么就可以使用前后端分离的开发模式。
另外,具体使用何种开发模式并不是绝对的,为了同时兼顾了首页的渲染速度和前后端分离的开发效率,一些网站采用了首屏服务器端渲染 + 其他页面前后端分离的开发模式。
身份认证
身份认证(Authentication)又称“身份验证”、“鉴权”,是指通过一定的手段,完成对用户身份的确认。
-
日常生活中的身份认证随处可见,例如:高铁的验票乘车,手机的密码或指纹解锁,支付宝或微信的支付密码等。
-
在 Web 开发中,也涉及到用户身份的认证,例如:各大网站的手机验证码登录、邮箱密码登录、二维码登录等。
身份认证的目的,是为了确认当前所声称为某种身份的用户,确实是所声称的用户。例如,你去找快递员取快递,你要怎么证明这份快递是你的。在互联网项目开发中,如何对用户的身份进行认证,是一个值得深入探讨的问题。例如,如何才能保证网站不会错误的将“马云的存款数额”显示到“马化腾的账户”上。
对于服务端渲染和前后端分离这两种开发模式来说,分别有着不同的身份认证方案:
-
服务端渲染推荐使用 Session 认证机制
-
前后端分离推荐使用 JWT 认证机制
session认证机制
先了解 HTTP 协议的无状态性,是进一步学习 Session 认证机制的必要前提。
HTTP 协议的无状态性,指的是客户端的每次 HTTP 请求都是独立的,连续多个请求之间没有直接的关系,服务器不会主动保留每次 HTTP 请求的状态。而如何突破无状态性的限制?则是像超市识别会员加上会员卡一样,加上一个 Cookie,进行身份认证。
Cookie概念
Cookie 是存储在用户浏览器中的一段不超过 4 KB 的字符串。它由一个名称(Name)、一个值(Value)和其它几个用于控制 Cookie 有效期、安全性、使用范围的可选属性组成。
不同域名下的 Cookie 各自独立,每当客户端发起请求时,会自动把当前域名下所有未过期的 Cookie 一同发送到服务器。
Cookie的几大特性:自动发送、域名独立、过期时限、4KB 限制
Cookie作用
客户端第一次请求服务器的时候,服务器通过响应头的形式,向客户端发送一个身份认证的 Cookie,客户端会自动将 Cookie 保存在浏览器中。随后,当客户端浏览器每次请求服务器的时候,浏览器会自动将身份认证相关的 Cookie,通过请求头的形式发送给服务器,服务器即可验明客户端的身份。
Cookie安全性
由于 Cookie 是存储在浏览器中的,而且浏览器也提供了读写 Cookie 的 API,因此 Cookie 很容易被伪造,不具有安全性。因此不建议服务器将重要的隐私数据,比如用户的身份信息、密码等,通过 Cookie 的形式发送给浏览器。
为了防止客户伪造会员卡,收银员在拿到客户出示的会员卡之后,可以在收银机上进行刷卡认证。只有收银机确认存在的会员卡,才能被正常使用。这种“会员卡 + 刷卡认证”的设计理念,就是 Session 认证机制的精髓。
session工作原理
在express中使用session
配置session
先在 express 项目中安装 express-session 中间件:npm i express-session,然后在项目中使用 Session 认证:
const express = require('express')
const app = express()
// 导入session中间件
let session = require('express-session')
// 配置session中间件
app.use(
session({
secret: 'keyboard cat', // 属性值可以为任意字符串
resave: false, // 固定写法
saveUninitialized: true, // 固定写法
})
)
app.listen(80, () => {
console.log('express server running at http://127.0.0.1')
})
session存数据
当 express-session 中间件配置成功后,即可通过 req.session 来访问和使用 session 对象,从而存储用户的关键信息
app.post('/api/login', (req, res) => {
// 判断用户提交的登录信息是否正确
if (req.body.username !== 'admin' || req.body.password !== '000000' ) {
return res.send({ status: 1, msg: '登录失败' })
}
req.session.user = req.body // 将用户的信息,存储到 Session 中
req.session.islogin = true // 将用户的登录状态,存储到 Session 中
res.send({ status: 0,msg: '登录成功' })
})
session取数据
可以直接从 req.session 对象上获取之前存储的数据
// 获取用户姓名的接口
app.get('/api/username' ,(req,res) => {
// 判断用户是否登录
if (!req.session.islogin) {
return res.send({ status: 1, msg: 'fail' })
}
// 从session中获取用户的名称,响应给客户端
res.send({
status: 0,
msg: 'success',
username: req.session.user.username
})
})
清空session
调用 req.session.destroy() 函数,即可清空服务器保存的 session 信息
// 退出登录的接口
app.post('/api/logout' ,(req,res) => {
// 清空当前客户端对应的session信息
req.session.destory()
res.send({ status: 0, msg: '退出登录成功' })
})
全部代码
/* express服务器.js */
const express = require('express')
const app = express()
const session = require('express-session')
app.use(
session({
secret: 'keyboard cat',
resave: false,
saveUninitialized: true,
})
)
app.use(express.static('./pages'))
app.use(express.urlencoded({ extended: false }))
app.post('/api/login', (req, res) => {
if (req.body.username !== 'admin' || req.body.password !== '000000' ) {
return res.send({ status: 1, msg: '登录失败' })
}
req.session.user = req.body
req.session.islogin = true
res.send({ status: 0,msg: '登录成功' })
})
app.get('/api/username' ,(req,res) => {
if (!req.session.islogin) {
return res.send({ status: 1, msg: 'fail' })
}
res.send({
status: 0,
msg: 'success',
username: req.session.user.username
})
})
app.post('/api/logout' ,(req,res) => {
// 清空当前客户端对应的session信息
req.session.destory()
res.send({ status: 0, msg: '退出登录成功' })
})
app.listen(80, () => {
console.log('http:/127.0.0.1')
})
/* index.html */
<!DOCTYPE htm1>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>后台主页</title>
<script src="./jquery.js"></script>
</head>
<body>
<h1>首页</h1>
<button id="btnLogout">退出登录</button>
<script>
$(function () {
// 页面加载完成后,自动发起请求,获取用户姓名
$.get('/api/username', (res) => {
// status 为0表示获取用户名称成功,否则为失败
if (res.status !== 0) {
alert("您尚未登录,请登录后再执行此操作!")
location.href = './login.html'
}else {
alert('欢迎您': + res.username)
}
})
// 点击按钮退出登录
$('#btnLogout').on('click', () => {
// 发起 POST 请求,退出登录
$.post('/api/logout', (res) => {
if(res.status === 0) location.href = './login.html'
})
})
})
</script>
</body>
/* login.html */
<!DOCTYPE htm1>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录页面</title>
<script src="./jquery.js"></script>
</head>
<body>
<h1>登录表单</h1>
<form id="form1">
<div>账号:<input type="input" name="username" autocomplete="off" /></div>
<div>密码:<input type="password" name="password" /></div>
<button>登录</button>
</form>
<script>
$(function () {
$('.form1').on('submit', (e) => {
e.preventDefault()
$.post('/api/login', $(this).serialize(), (res) => {
if(res.status === 0) {
location.href = './index.html'
}else {
alert('登录失败')
}
})
})
})
</script>
</body>
JWT认证机制
JWT(英文全称:JSON Web Token)是目前最流行的跨域认证解决方案
session局限性
Session 认证机制需要配合 Cookie 才能实现。由于 Cookie 默认不支持跨域访问,所以,当涉及到前端跨域请求后端接口的时候,需要做很多额外的配置,才能实现跨域 Session 认证。
注意:
-
当前端请求后端接口不存在跨域问题的时候,推荐使用 Session 身份认证机制。
-
当前端需要跨域请求后端接口的时候,不推荐使用 Session 身份认证机制,推荐使用 JWT 认证机制。
JWT工作原理
JWT组成部分
JWT 通常由三部分组成,分别是 Header(头部)、Payload(有效荷载)、Signature(签名)。三者之间使用英文的“.”分隔
其中:
Payload 部分才是真正的用户信息,它是用户信息经过加密之后生成的字符串。
Header 和 Signature 是安全性相关的部分,只是为了保证 Token 的安全性。
客户端收到服务器返回的 JWT 之后,通常会将它储存在 localStorage或 sessionStorage中。
此后,客户端每次与服务器通信,都要带上这个 JWT 的字符串,从而进行身份认证。推荐的做法是把 JWT 放在 HTTP 请求头的 Authorization 字段中,格式为:Authorization:Bearer <token>
在express中使用JWT
安装JWT
安装两个有关的包:npm i jsonwebtoken express-jwt,可在 package.json 查看配置。其中:
jsonwebtoken:用于生成 JWT 字符串 (加密用)
express-jwt: 用于将 JWT 字符串解析还原成JSON对象 (解密用)
配置JWT
-
导入 JWT 相关的包
const jwt = require('jsonwebtoken') const expressJWT = require('express-jwt')
-
定义 secret 密钥
为了保证 JWT 字符串的安全性,防止 JWT 字符串在网络传输过程中被别人破解
我们需要专门定义一个用于加密和解密的 secret 密钥:cosnt secretKey = "itheima No1 ^_^"
-
当生成 JWT 字符串的时候,需要使用 secret 密钥对用户的信息进行加密,最终得到加密好的 JWT 字符串
-
当把 JWT 字符串解析还原成 JSON 对象的时候,需要使用 secret 密钥进行解密
-
-
在登录成功后生成 JWT 字符串
调用 jsonwebtoken包提供的 sign(用户名、密钥、加密有效期) 方法,将用户的信息加密成 JWT 字符串,响应给客户端
token:jwt.sign({ username: userinfo.username}, secretKey, { expiresIn: '30s' })
-
将 JWT 字符串还原成 JSON 对象
客户端每次在访问那些有权限接口的时候,都需要主动通过请求头中的 Authorization 字段,将 Token 字符串发送到服务器进行身份认证。因此,服务器可以通过 express-jwt这个中间件,自动将客户端发送过来的 Token 解析还原成 JSON 对象
app.use(expressJWT({ secret: secretKey }).unless({ path: [/^\/api\//] }))
-
使用 req.user 获取用户信息
当 express-jwt这个中间件配置成功之后,即可在那些有权限的接口中,使用 req.user 对象,来访问从 JWT 字符串中解析出来的用户信息了
-
捕获 JWT 解析失败后产生的错误
当使用 express-jwt解析 Token 字符串时,如果客户端发送过来的 Token 字符串过期或不合法,会产生一个解析失败的错误,影响项目的正常运行。我们可以通过 Express 的错误中间件,捕获这个错误并进行相关的处理
const express = require('express')
const app = express()
// 1.导入JWT相关的包
const jwt = require('jsonwebtoken')
const expressJWT = require('express-jwt')
const cors = require('cors')
app.use(cors())
const bodyParser = require('body-parser')
app.use(bodyParser.urlencoded({ extended: false }))
// 2.定义secret密钥
cosnt secretKey = "itheima No1 ^_^"
// 4.将 JWT 字符串还原成 JSON 对象
app.use(expressJWT({ secret: secretKey }).unless({ path: [/^\/api\//] }))
// 登录接口
app.post('/api/login', (req,res) => {
const userInfo = req.body
// 登录失败
if(userInfo.username !== 'admin' || userInfo.password !== '00000') {
return res.send({
status: 400,
message: '登录失败',
})
}
// 登录成功
res.send({
status: 200,
message: '登录成功!',
// 在登录成功后生成 JWT 字符串
token: jwt.sign({ username: userInfo.username, secretKey, { expiresIn: '30s' }})
})
})
// 有权限的接口
app.get('/admin/getinfo', (req, res) => {
res.send({
status: 200,
message: '获取用户信息成功!',
// 5.使用 req.user 获取用户信息,发送给客户端
data: req.user,
})
})
// 6.使用全局错误处理中间件,捕获解析JWT失败后产生的错误
app.use((err, req, res, next) => {
// 这次错误是由token解析失败导致的
if(err.name === 'UnauthorizedError') {
return res.send({
status: 401,
message: '无效的token',
})
}
res.send({
status: 500,
message: '未知的错误',
})
})
app.listen(80, () => {
console.log('express server running at http://127.0.0.1')
})
七、npm与包
概念
包:将模块,代码,其他资料聚合成一个文件夹
包分类
-
项目包:主要用于编写项目和业务逻辑
-
软件包:封装工具和方法进行使用
注意:导入软件包时,默认导入的是 index.js 模块文件,或者是 main 属性指定的模块文件
Node.js 中的第三方模块又叫做包,这两者是同一个概念;不同于 Node.js 中内置模块与自定义模块,包是由第三方个人或团队开发出来的,免费供所有人使用,所以 Node.js 的包都是开源免费可下载使用的
为什么需要包?
-
由于 Node.js 的内置模块仅提供了一些底层的 API,导致在基于内置模块进行项目开发时效率很低。
-
包是基于内置模块封装出来的,提供了更高级的、更方便的API,极大地提高了开发效率。
-
包和内置模块之间的关系,类似于 jQuery 和浏览器内置 API 之间的关系
规范的包结构
一个规范的包,它的组成结构,必须包含以下 3 点要求:
-
包必须以单独的目录存在
-
包的顶级目录下必须包含 package.json 包管理配置文件
-
package.json 中必须包含 name(包的名字) versoin(版本号) main(包的入口)
更多规范请参考规范包结构格式
下载
npm, Inc 国外 IT 公司提供了服务器 https://registry.npmjs.org/ 来共享包
包搜索地址:npm
包下载地址:https://registry.npmjs.org/
npm, Inc公司也提供了包管理工具 Node Package Manager (简称npm包管理工具)
注意!在使用 npm 下包时,默认从国外的 https://registry.npmjs.org/ 服务器进行下载,网络数据传输需要经过漫长的海底光缆,所以下载速度会很慢。而淘宝在国内搭建了一个服务器,专门把国外官方服务器上的包同步到国内的服务器,然后在国内提供下包的服务,从而极大地提高下载速度。所以可以用 npm 切换到淘宝的镜像源
// 下包镜像源:下载包时用到的服务器地址
// 查看当前的下包镜像源
npm config get registry
// 将下包的镜像源切换为淘宝镜像源
npm config set registry=https://registry.npm.taobao.org/
// 检查镜像源是否下载成功
npm config get registry
扩展:镜像是一种文件存储方式,一个磁盘上的数据在另一个磁盘上存在一个完全相同的副本即为镜像
npm - 软件包管理器
npm 是 Node.js 标准的软件包管理器,这个包管理工具随着Node.js的安装一起被安装到用户的电脑上,可终端上 npm -v 查看版本号
它起初是作为下载和管理 Node,js 包依赖的方式,但其现在也已成为前端JavaScript 中使用的工具。
初次安装后的文件:node_modules、package.json
-
node_modules 文件夹:存放所有已安装到项目中的包,require() 导入第三方包时,就是从这个目录中查找并加载包
-
package-lock.json 配置文件:用来记录node_modules 目录下的每一个包的下载信息,如包的名字,版本号,下载地址等
注意:
程序员不要手动修改这两个文件中的任何代码,npm 包管理工具会自动维护它们
package.json 包配置管理文件中是不允许有注释的,需要删掉前面的注释,否则报错
项目初始化
定义:当前项目内使用,封装属性和方法,下载的包存在于 node_modules,并记录在 package.json 中(package-lock.json的简略版,可用来查看项目中的一些配置信息、在开发和部署时用到了哪些包)
使用
-
初始化清单文件:npm init -y(得到 package.json 文件,有则略过此命令)
-
下载软件包(第三方):npm i 软件包名称(npm i 可一次性下载 package.json 中记录的所有包)
-
根据具体包文档来使用
项目包又分为2类:核心依赖包dependencies 和 开发依赖包devDependencies
dependencies (开发 + 上线):核心依赖包,开发和项目上线都会用到( npm i 包名)
devDependencies(开发):开发依赖包,只有开发时用到(npm i 包名 -D)(npm i 包名 --save-dev)
全局安装
定义:本机所有项目使用,封装命令和工具,存在于系统设置的位置
使用:npm i 包名 -g(-g 代表安装到全局环境中)
全局包会被安装到:C:\Users\用户目录\AppData\Roaming\npm\node_modules 目录下
查看版本号:npm webpack -v
注意
-
只有工具性质的包,才有全局安装的必要性,因为它们提供了好用的终端命令
-
因为 AppData 是隐藏的,不方便管理,所以需要自定义全局模块安装路径
-
判断某个包是否需要全局安装后才能使用,可参考官方提供说明即可
局部安装
定义:当前项目内使用,封装属性和方法,下载的包也存在于 node_modules,并记录在 package.json 中
使用:
-
npm install webpack@3.6.0 --save-dev(开发时依赖,后续不需要时才使用)
-
通过 node_modules/webpack 启动 webpack 打包
为什么全局安装完后还要局部安装?
-
在终端直接执行 webpack 命令,使用的全局安装 webpack
-
在 package.json 中定义 scripts 时,其中包含了 webpack 命令,那么使用的是局部webpack
-
一个项目往往依赖特定的 webpack 版本,全局的版本可能跟这个项目的 webpack版本不一致,导致打包出现问题,所以通常一个项目都有自己的局部 webpack,后面的【npm run build】讲的就是这种情况
-
安装成功后会在 package.json 中生成 “devDependencies”(开发时依赖,项目打包后不需要使用的),并生成 “node_modules” 的本地局部 webpack 文件夹,但 Vue CLI3 中已经升级到 webpack4,它将配置文件隐藏了起来,所以查看起来不是很方便
具体版本号
包的版本号是以 “点分十进制” 形式定义的,总共有三位数字,例如:2.24.0
-
第一位数字:大版本
-
第二位数字:功能版本
-
第三位数字:Bug修复版
版本号提升规则:只要前面的版本号增长了,则后面的版本号归零
默认情况下,使用 npm install 命令安装包的时候,会自动安装最新版本的包。
如果需要安装指定版本的包,可以通过 @ 手动安装具体版本
// 指定版本号
npm install moment@2.22.2
// 一次性安装多个包,卸载也是同理
npm i monent@2.22.2 axios@0.21.4
package.json 的作用
在项目进行多人协作时,遇到第三方软件包 node_modules 体积过大,不方便团队成员共享的问题。所以在共享时可以用.gitignore文件来忽略 node_modules 的git上传共享;这时候创建的 package.json 配置文件就派上用场了,它记录了项目中安装了哪些包,从而方便剔除 node_modules 后,在团队成员之间共享项目的源代码。
注意:
上述命令只能在英文的目录下成功运行!所以项目文件夹名称一定要用英文,不能出现空格 (不包括前面的路径)
运行 npm install 命令安装包时,npm 包管理工具会自动把包的名称和版本号记录到 package.json 中
当我们拿到一个剔除了 node_modules 的他人的项目之后,需要先把所有的包 npm i 下载到项目中,才能将项目运行起来,否则会报类似下面的错误
Error:Cannot find module 'moment'
总结
Node.js 包:
概念:把模块文件,代码文件,其他资料聚合成一个文件夹
项目包:编写项目需求和业务逻辑的文件夹
软件包:封装工具和方法进行使用的文件夹(一般使用 npm 管理)
✓ 本地软件包:作用在当前项目,一般封装的属性/方法,供项目调用编写业务需求
✓ 全局软件包:作用在所有项目,一般封装的命令/工具,支撑项目运行
开发属于自己的包(了解)
需要实现的功能
-
格式化日期
-
转义 HTML 中的特殊字符,防止用户在提交时填写一些 html 标签
-
还原 HTML 中的特殊字符
初始化包的基本结构
新建 itheima-tools 文件夹,作为包的根目录, 在 itheima-tools 文件夹中,新建并编写三个文件:
-
package.json(包管理配置文件)
-
index.js (包的入口文件,仅集成方法,具体的方法还是封装到新建的src文件夹下)
-
README.md (包的说明文档,没有强制写法要求,但基本要包含安装、使用方法,开源协议等)
将项目开发完共享到 npm Inc 公司的话,在搜索页面就能查到相关信息
注意:package.json 包配置管理文件中是不允许有注释的,需要删掉里面的注释,否则报错
编写有关代码
发布包
注册 npm 账号步骤:
-
访问 http://www.npmjs.com/ 网站,点击 sign up 按钮,进入登录注册界面
-
填写相关账号信息:Full Name、Public Email、Username、Password
-
点击 Create an Account 按钮,注册账号
-
登录邮箱,点击验证链接,进行账号的验证
登录 npm 账号:
npm注册完后,在终端执行 npm login 命令,依次输入用户名、密码、邮箱、邮箱验证码后,即可登录成功
把包发布到 npm:
将终端切换到包的根目录后,运行 npm publish 命令,即可将包发布到 npm 上
注意:包名不能雷同,否则会上传失败;可以先在官网上进行查重 ,然后在开发包里修改有关包名
删除包
运行 npm unpublish 包名 --force 命令,即可从 npm 官网上删除已发布的包
注意:
-
npm unpublish 命令只能删除 72 小时内发布的包
-
npm unpublish 删除的包,在 24小时内不允许重复发布
-
发布包时要慎重,尽量不要往 npm 上发布没有意义的包
模块的加载机制(了解)
优先从缓存加载
模块在第一次加载后会被缓存。这意味着多次调用 require() 不会导致模块的代码被执行多次
注意:不论是内置模块、用户自定义模块、还是第三方模块,它们都会优先从缓存中加载,从而提高模块的加载效率
内置模块的加载机制
内置模块是从 Node.js 官方提供的模块,内置模块的加载优先级最高
eg:
require('fs') 始终返回内置的 fs模块,即使在 node_modules 目录下有自定义模块包名也叫 fs
自定义模块的加载机制
使用 require() 加载自定义模块时,必须指定 ./ 或 ../ 开头的路径标识符,如果没有路径标识符的话,node 会把它当做是内置模块或第三方模块进行加载
加载如果省略了文件的扩展名,则 Node.js 会按顺序分别尝试加载以下文件:
-
按照确切文件名进行加载
-
补全 .js 扩展名进行加载
-
补全 .json 扩展名进行加载
-
补全 .node 扩展名进行加载
-
加载失败,终端报错
第三方模块的加载机制
如果传递给 require() 的模块标识符不是一个内置模块,也没有./ 或 ../ 开头,则 Node.js 会从当前模块的父目录开始,尝试从 /node_modules 文件夹中加载第三方模块。如果没有找到对应的第三方模块,则移动到再上一层父目录中,进行加载,直到文件系统的根目录
eg:
假如在 'C:\Users\itheima\project\foo.js' 文件里调用了 require('tools'),则 Node.js 会按以下顺序查找:
-
C:\Users\itheima\project\node_modules\tools'
-
C:\Users\itheima\node_modules\tools'
-
C:\Users\node_modules\tools'
-
C:\node_modules\tools'
目录作为模块
当把目录作为模块标识符,传递给 require() 进行加载时,有三种加载方式:
-
在被加载的目录下查找一个叫做 package.json 的文件,并寻找 main 属性,作为 require() 加载的入口
-
如果目录里没有 package.json 文件,或者 main 入口不存在或无法解析,则 Node.js 将会试图加载目录下的 index.js 文件
-
加载失败,并在终端打印消息,报告模块的缺失:Error: Cannot find module 'xxx'
八、webpack
概念
定义:本质上,webpack 是一个用于现代JavaScript应用程序的静态模块打打包工具。当webpack处理应用程序时,它会在内部从一个或多个入口点构建一个依赖图dependency graph),然后将你项目中所需的每一个模块组合成一个或多个bundles,它们均为静态资源,用于展示你的内容。简而言之:
webapck 的作用:压缩,转译,整合,打包我们的静态模块
静态模块:指编写代码过程中的 html,css,js,图片等固定内容的文件
打包:把静态模块内容,压缩,整合,转译等 (前端工程化),打包过程中注意只有和入口有直接/间接引入关系的模块,才会被打包
-
把 less / sass 转成 css 代码
-
把 ES6+ 降级成 ES5
-
支持多种模块标准语法
为何不学 vite ?
因为很多项目还是基于 Webpack 构建,并为 Vue React 脚手架使用做铺垫!
安装和使用
例:封装 utils 包,校验手机号和验证码长度,在 src/index.js 中使用,使用 Webpack 打包
步骤
-
新建并初始化项目:npm init -y(初始化 package.json),编写业务源代码
-
新建 src 源代码文件夹(书写代码)包括 utils/check.js 封装用户名和密码长度函数,引入到 src/index.js 进行使用
-
下载 webpack webpack-cli 到当前项目中(版本独立),并配置局部自定义命令(这是最终方案,下面有打包原理解析)
-
运行打包命令,自动产生 dist 分发文件夹(压缩和优化后,用于最终运行的代码)
/* /src/utils/check.js */
// 封装校验手机号长度和校验验证码长度的函数
export const checkPhone = phone => phone.length === 11
export const checkCode = code => code.length === 6
/* src/index.js */
import { checkPhone, checkCode } from '../utils/check.js'
console.log(checkPhone('13900002020'))
console.log(checkCode('123123123123'))
// cmd里运行
npm i webpack webpack-cli --save-dev
// package.json里配置打包别名
"scripts": {
"build": "webpack"
}
// npm run 自定义别名(实际上在终端运行的是 build 右侧的名字:webpack)
npm run build
注意:虽然 webpack 是全局软件包,封装的是命令工具,但是为了保证项目之间版本分别独立,所以这次比较特殊,下载到某个项目环境下,但是需要把 webpack 命令配置到 package.json 的 scripts 自定义命令,作为局部命令使用
总结:初始化环境,编写代码,安装 Webpack,配置自定义命令,打包体验查看结果
打包原理(了解)
打包原理,是对上述安装和使用的补充,能更好地了解最终 npm run build 打包命令的由来,如果对上述有不明白之处可移步此处查看
webpack原始打包命令:webpack ./src/main.js ./dist/bundle.js(后面会讲怎么过渡成 npm run build)
需在文件对应目录下输入cmd打开命令窗口操作,或者在编辑器的终端里操作命令。
打包前要通过“cd 目录名”跳转到相应的目录下才可以进行。(“cd ..”:返回上一目录)
原理:
将 src 文件夹下的 main.js 文件以及与 main.js 相关联的js文件(模块化思想)一起打包到 dist文件下并生成新的打包文件 bundle.js。因为 webpack 打包工具会自动处理模块之间的依赖关系,所以打包命令只需要写主程序入口 main.js,它会自动根据 module 模块化思想将其他相关联 js 文件一并进行打包操作。Index.html 引用时只需要引用 bundle.js 文件就等于引用main.js 和 mathUtil.js 等 js 文件。
使用模块化的方式(打包)进行开发后,js文件不可以再直接使用,如果直接在index.html中引入main.js、mathUtils.js这两个js文件,则浏览器并不识别其中的模块化代码,而在真实项目中有许多这样的js文件,一个个引用非常麻烦,而且后期不方便管理,因此给打包配置别名则方便点
【给打包配置别名】
如果每次使用 webpack 命令都写上入口和出口(webpack ./src/main.js ./dist/bundle.js)作为参数,就非常麻烦,所以可以创建一个 webpack.config.js 文件(名字固定的),将这两个参数写到一个配置中。
1. 利用 npm init 命令建立 package.json(一直回车)
2. 建立 webpack.config.js,写上 filename: 'bundle.js'
3. 输入 webpack,会自动根据 webpack.config.js 便捷打包生成 bundle.js
【npm run build】
如果电脑只全局安装webpack(4.11.5),但有时候接手别人的项目(需要本地webpack3.6.0)并进行打包时使用 webpack,因为使用到全局安装 webpack 导致报错。
每次输入 node_modules/webpack 启动 webpack 打包,这么一长串命令写着不方便,所以可以使用 npm run build 来映射 webpack 命令(即代替webpack命令)。配置如下:
-
在 package.json 文件中的 “scripts:”{} 里写上 "build": "webpack",定义自己的执行脚本
-
使用 npm run build 命令生成 bundle.js
原理:package.json 中的 scripts 脚本在执行时,会按照一定顺序寻找命令对应的位置
-
首先,会寻找本地的 node_modules/bin 路径中对应的命令
-
如果没有找到,会去全局的环境变量里找
Webpack 修改入口和出口
如果不想用默认的入口和出口来打包文件,可以修改 webpack 配置,来影响 webpack 打包的过程和结果
入口:需要压缩打包的js/css/html等文件
出口:打包后在 dist 文件夹里生成的文件
步骤:
-
项目根目录,新建 webpack.config.js 配置文件
-
导出配置对象,配置入口,出口文件的路径(别忘了同步修改磁盘文件夹和文件的名字)
-
重新打包观察
注意:只有和入口产生直接/间接的引入关系,才会被打包
更多修改配置请查看文档:概念 | webpack 中文文档 (docschina.org)
const path = require('path')
module.exports = {
// 入口
entry: path.resolve(__dirname, 'src/login/index.js'),
// 出口
output: {
path: path.resolve(__dirname, 'dist'),
filename: './login/index.js'
}
}
例:点击登录按钮,判断手机号和验证码长度是否符合要求
步骤:
-
新建 public/login.html 准备网页模板(方便查找标签和后期自动生成 html 文件做准备)
-
编写核心 JS 逻辑代码(代码写在 src/login/index.js 文件里)
-
运行自定义命令,让 Webpack 打包 JS 代码
-
手动复制 public/login.html 到 dist 下,手动引入打包后的 JS 代码文件,运行 dist/login.html 在浏览器查看效果
为什么要手动把 html 网页文件复制到 dist 下? 因为 webpack 不会自动将该类型的文件压缩打包,所以要手动操作
/**
* 目标3:用户登录-长度判断案例
* 3.1 准备用户登录页面
* 3.2 编写核心 JS 逻辑代码
3.3 打包并手动复制网页到 dist 下,引入打包后的 js,运行
*/
// 3.2 编写核心 JS 逻辑代码
document.querySelector('.btn').addEventListener('click', () => {
const phone = document.querySelector('.login-form [name=mobile]').value
const code = document.querySelector('.login-form [name=code]').value
if (!checkPhone(phone)) {
console.log('手机号长度必须是11位')
return
}
if (!checkCode(code)) {
console.log('验证码长度必须是6位')
return
}
console.log('提交到服务器登录...')
})
安装 plugin 插件
plugin插件:是用于扩展 Webpack 功能的工具,它可以在Webpack运行期间执行一些任务,比如生成HTML文件、压缩代码、提取公共代码等。 Plugin是通过 Webpack 的事件机制来实现的,可以在Webpack的不同阶段注册不同的事件来实现不同的功能。
添加版权
该插件属于 webpack 自带的,所以不用再用 npm 安装一遍,只需要配置即可
const path = require('path')
module.exports = {
// 入口
entry: path.resolve(__dirname, 'src/login/index.js'),
// 出口
output: {
path: path.resolve(__dirname, 'dist'),
filename: './login/index.js'
},
plugins: {
new webpack.BannerPlugin('最终版权归xxx所有!')
}
}
打包 html
在真实发布项目时(npm run build),发布的是 dist 文件夹中的内容,但 dist 文件夹中如果没有 index.html 文件,那么打包的 js 等文件也没有意义了,所以需要打包 html 的插件,在 Webpack 打包时生成 html。
配置文档:HtmlWebpackPlugin | webpack 中文文档 (docschina.org)
步骤:
-
安装插件:npm install html-webpack-plugin@3.2.0 –save-dev
需要具体指定一个较低的版本,比较高的版本可能不兼容会报错
-
配置 webpack.config.js,让 Webpack 拥有插件功能
-
运行打包命令:npm run build,重新打包观察效果
// ...
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
// ...
plugins: [
new HtmlWebpackPlugin({
// 指定以 public/login.html 为模板,打包复制到 dist/login/index.html,并自动引入其他打包后资源
template: './public/login.html', // 模板文件
filename: './login/index.html' // 输出文件
})
]
}
js 压缩
开发阶段是不需要使用的,是为了方便修改 js 代码,直到发布阶段要减少在线运行项目包体积才用这个
-
安装插件:npm install uglifyjs-webpack-plugin@1.1.1 --save-dev
-
配置 webpack.config.js
注意!这个插件会将js文件中空闲、注释部分都去掉,所以前面的BannerPlugin添加版权插件就会失效,因此这两个插件只能用一个
// ...
const UglifyJsPlugin = require('uglify-webpack-plugin')
module.exports = {
// ...
plugins: [
// ...
// 配置js压缩插件
new UglifyJsPlugin()
]
}
安装 loader 加载器
loader 加载器:是用于对模块的源代码进行转换的工具,它可以将一些非JavaScript文件(如CSS、图片、字体等)转换成JavaScript模块。 Loader会在 Webpack 打包的过程中被自动调用,每个Loader只负责一种文件类型的转换。
webpcak 虽然能自动处理 js 之间相关的依赖,但不能将:css、图片等 ES6 转成 ES5 代码,将TypeScript转成ES5代码,将scss、less转成css,将.jsx、.vue文件转成js文件等等,这时给 webpack 扩展对应的 loader 就支持转化了。通用步骤如下:
-
通过npm安装需要使用的 loader
-
在 webpack.config.js 中的 modules 关键字下进行配置
大部分 loader 都可以在 webpack 的官网中找到,并且学习对应的用法
总结:
loader的作用:让 Webpack 识别更多的代码内容类型
打包 CSS 代码
Webpack 默认只识别 JS 和 JSON 文件内容,所以想要让 Webpack 识别更多不同内容,需要使用2 个加载器来辅助 Webpack 打包工作
-
加载器 css-loader:加载 css 代码,但不进行解析,所以没有显示效果
-
加载器 style-loader:把解析后的 css 代码插入到 DOM(style 标签之间),显示出效果
步骤:
-
准备 css 代码,引入到 src/login/index.js 中(压缩转译处理等)
-
下载 css-loader 和 style-loader 本地软件包
-
配置 webpack.config.js 让 Webpack 拥有该加载器功能
-
运行打包命令:npm run build,重新打包观察效果
// 1 准备 css 代码(下面这两个路径里得有css文件),并引入到 js 中
// 注意:这里只是引入代码内容让 Webpack 处理,不需定义变量接收在 JS 代码中继续使用,所以没有定义变量接收
import 'bootstrap/dist/css/bootstrap.min.css'
import './index.css'
// 2. 下载 css-loader 和 style-loader 加载器
npm i css-loader style-loader --save-dev
// ...
module.exports = {
// ...
module: { // 加载器
rules: [ // 规则列表
{
test: /\.css$/i, // 匹配 .css 结尾的文件
use: ['style-loader', 'css-loader'], // 使用多个loader时,是从右向左读取,所以这两个顺序要反着写
}
]
}
};
优化 - 提取 CSS 代码
为了让 Webpack 能够提取 css 代码到独立的 css 文件中,可以改用 mini-css-extract-plugin 插件来实现。
好处:css 文件可以被浏览器缓存,减少 JS 文件体积,让浏览器并行下载 css 和 js 文件,但注意不能和 style-loader 一起使用
步骤:
-
安装插件:npm i --save-dev mini-css-extract-plugin(这里为了知识连贯性,就不把该方案放到plugin目录下了)
-
配置 webpack.config.js,让 Webpack 拥有该插件功能
-
运行打包命令:npm run build,重新打包观察效果
// ...
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
module.exports = {
// ...
module: {
rules: [
{
test: /\.css$/i,
// use: ['style-loader', 'css-loader']
use: [MiniCssExtractPlugin.loader, "css-loader"],
},
],
},
plugins: [
// ...
new MiniCssExtractPlugin() // 生成 CSS 文件
]
};
优化 - 压缩过程
因为上面 css 代码提取后没有压缩,因此需要用到 css-minimizer-webpack-plugin 插件来实现
步骤:
-
安装插件:npm i css-minimizer-webpack-plugin --save-dev
-
配置 webpack.config.js,让 Webpack 拥有该插件功能
-
运行打包命令:npm run build,重新打包观察效果
// ...
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = {
// ...
// 优化
optimization: {
// 最小化
minimizer: [
// 在 webpack@5 中,你可以使用 `...` 语法来扩展现有的 minimizer(即
// `terser-webpack-plugin`),将下一行取消注释(保证 JS 代码还能被压缩处理)
`...`,
new CssMinimizerPlugin(),
],
}
};
打包 less 代码
让 Webpack 拥有打包 less 代码功能,把 less 代码编译为 css 代码,还需要依赖 less 软件包
步骤:
-
新建 login/index.less 文件,并把 less 样式引入到 src/login/index.js 中
-
下载 less 和 less-loader 本地软件包:npm i less less-loader --save-dev
-
配置 webpack.config.js 让 Webpack 拥有该加载器功能
-
运行打包命令:npm run build,重新打包观察效果
// ...
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /\.less$/i,
use: [MiniCssExtractPlugin.loader, "css-loader", "less-loader"]
}
]
}
}
打包图片
让 webpack 支持图片等资源打包
在 webpack5 之前
需要安装 url-loader、file-loader 加载器,然后在 modules 添加一个 rules 选项,用于处理 .jpg/jpge/png/gif/ 文件,最后在出口中添加 publicPath:‘dist/’,并进行打包测试即可
npm i url-loader file-loader --save-dev
// ...
module.exports = {
// 出口
output: {
// 默认情况下,webpack 会将生成的路径直接返回给使用者
// 但我们整个程序是打包在dist文件夹下的,所以需要添加:publicPath:‘dist/’
publicPath: 'dist/'
},
module: {
rules: [
// ...
{
test: /\.(png|jpg|jpeg|gif)$/i,
use: [{
loader: 'url-loader',
options: { limit: 8192 }
}]
}
]
}
}
在Webpack5 之后
则内置了资源模块的打包,无需下载额外 loader,同时也改了一些步骤
官方文档:资源模块 | webpack 中文文档 (docschina.org)
步骤:
1. 配置 webpack.config.js 让 Webpack 拥有打包图片功能
占位符 【hash】对模块内容做算法计算,得到映射的数字字母组合的字符串
占位符 【ext】使用当前模块原本的占位符,例如:.png / .jpg 等字符串
占位符 【query】保留引入文件时代码中查询参数(只有 URL 下生效)
2. 在 src/login/index.js 中给 img 标签添加 logo 图片
// 2 创建 img 标签并动态添加到页面,配置 webpack.config.js
// 注意:js 中引入本地图片资源要用 import 方式(如果是网络图片http地址,字符串可以直接写)
import imgObj from './assets/logo.png'
const theImg = document.createElement('img')
theImg.src = imgObj
document.querySelector('.login-wrap').appendChild(theImg)
3. 配置 webpack.config.js 让 Webpack 拥有该加载器功能
// ...
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /\.(png|jpg|jpeg|gif)$/i,
type: 'asset',
generator: {
filename: 'assets/[hash][ext][query]'
}
}
]
}
}
4. 运行打包命令:npm run build,重新打包观察效果
注意:判断临界值默认为 8KB
大于 8KB 文件:发送一个单独的文件并导出 URL 地址
小于 8KB 文件:导出一个 data URI(base64字符串)
ES6 -> ES5
将webpack 打包的 js 文件中的ES6 语法转换为 ES5 语法
步骤:
-
安装插件:npm install --save-dev babel-loader@7 babel-core babel-preset-es2015
-
配置 webpack.config.js,让 Webpack 拥有该插件功能
-
运行打包命令:npm run build,重新打包观察效果
// ...
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/, // 排除掉一些文件
use: {
loader: 'babel-loader',
options: { presets: ['es2015'] }
}
}
]
}
}
注意:如果不是工程化地开发,遇到 template、script、style 这种特殊的文件格式时必须有相应的插件支持,需要安装 vue-loader 和 vue-template-compiler:npm install vue-loader vue-template-compiler --save-dev
配置分离
将 webpack.config.js 进行分离的目的是使得开发时依赖一些组件,到真正发布时依赖另外一些文件,不用像之前plugin又是添加版权插件、又是添加压缩插件、又是添加上传服务器插件等都弄在同一个配置文件里;这样分离的好处是开发跟发布所依赖的插件能各自发挥作用,不会互相干扰
创建三个配置文件
我们在根目录下创建 config 文件夹,并创建三个配置文件,分别是:
-
common.config.js 公共环境的配置文件
-
dev.config.js 开发环境下的配置文件
-
prod.config.js 生产环境下的配置文件
合并文件
在开发时配置文件就是 common.config.js + dev.config.js。
生产时配置文件就是 common.config.js+ prod.config.js。
·为了让两个文件合并在一起,必须装 合并文件的merge:npm install webpack-merge –save-dev
配置文件
做完这些后则webpack.config.js文件就没什么存在的必要,可以删除
接下来配置下面代码,然后 npm run build 即可
案例 - 用户登录功能
目的:体验下 npm 在前端项目中的作用
需求:完成登录功能的核心流程,以及 Alert 警告框使用
步骤:
-
使用 npm 下载 axios:npm i axios
-
准备并修改 utils 工具包源代码导出实现函数
import axios from 'axios'
// axios 公共配置
// 基地址
axios.defaults.baseURL = 'http://geek.itheima.net'
// 添加请求拦截器
axios.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
// 统一携带 token 令牌字符串在请求头上
const token = localStorage.getItem('token')
token && (config.headers.Authorization = `Bearer ${token}`)
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});
// 添加响应拦截器
axios.interceptors.response.use(function (response) {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么,例如:直接返回服务器的响应结果对象
const result = response.data
return result;
}, function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么,例如:统一对 401 身份验证失败情况做出处理
console.dir(error)
if (error?.response?.status === 401) {
alert('身份验证失败,请重新登录')
localStorage.clear()
location.href = '../login/index.html'
}
return Promise.reject(error);
});
export default axios
/**
* 目标10:完成登录功能
* 10.1 使用 npm 下载 axios(体验 npm 作用在前端项目中)
* 10.2 准备并修改 utils 工具包源代码导出实现函数
* 10.3 导入并编写逻辑代码,打包后运行观察效果
*/
// 10.3 导入并编写逻辑代码,打包后运行观察效果
import myAxios from '../utils/request.js'
import { myAlert } from '../utils/alert.js'
document.querySelector('.btn').addEventListener('click', () => {
const phone = document.querySelector('.login-form [name=mobile]').value
const code = document.querySelector('.login-form [name=code]').value
if (!checkPhone(phone)) {
myAlert(false, '手机号长度必须是11位')
console.log('手机号长度必须是11位')
return
}
if (!checkCode(code)) {
myAlert(false, '验证码长度必须是6位')
console.log('验证码长度必须是6位')
return
}
myAxios({
url: '/v1_0/authorizations',
method: 'POST',
data: {
mobile: phone,
code: code
}
}).then(res => {
myAlert(true, '登录成功')
localStorage.setItem('token', res.data.token)
location.href = '../content/index.html'
}).catch(error => {
myAlert(false, error.response.data.message)
})
})
导入并编写逻辑代码打包后运行观察效果
搭建开发环境
每次改动代码,都要重新打包,很麻烦,所以这里给项目集成 webpack-dev-server 开发服务器,用于快速开发应用程序
作用:启动 webpack 开发服务器,启动 Web 服务,自动检测代码变化,热更新到网页
注意:dist 目录和打包内容是在内存里(更新快)
后面用 cli 框架搭建的vue项目就会自动集成 npm run dev 功能了,无需再进行以上手动搭建配置
步骤
-
下载 webpack-dev-server 软件包到当前项目:npm i webpack-dev-server --save-dev
-
配置自定义命令,并设置打包的模式为开发模式
-
使用 npm run dev 来启动开发服务器,访问提示的域名+端口号,在浏览器访问打包后的项目网页,修改代码后试试热更新效果
/* package.json */
"scripts": {
// 配置自定义命令(后面再加个 --open 则运行完会自动打开浏览器)
"dev": "webpack serve --open"
},
/* webpack.config.js */
module.exports = {
// 设置打包模式为开发模式
mode: 'development'
}
注意:
-
webpack-dev-server 借助 http 模块创建 8080 默认 Web 服务
-
默认以 public 文件夹作为服务器根目录
-
webpack-dev-server 根据配置,打包相关代码在内存当中,以 output.path 的值作为服务器根目录(所以可以直接自己拼接访问 dist 目录下内容,或者直接在public文件夹下新建index.html,然后写 location.href = '/login/index.html',也可以实现运行即跳转)
Webpack 打包模式
定义:告知 Webpack 使用相应模式的内置优化
官网:模式(Mode) | webpack 中文文档 (docschina.org)
分类:
模式名称 | 模式名字 | 特点 | 场景 |
---|---|---|---|
开发模式 | development | 调试代码,实时加载,模块热替换更快 | 本地开发 |
生产模式 | production | 压缩代码、项目体积小,资源优化,更轻量,适配不同浏览器环境 | 打包上线 |
设置:
方式1:在 package.json 命令行设置 mode 参数
"scripts": {
"build": "webpack --mode=production",
"dev": "webpack serve --mode=development"
},
方式2:在 webpack.config.js 配置文件设置 mode 选项
module.exports = {
// ...
mode: 'production'
}
注意:命令行设置的优先级高于配置文件中的,如果同时使用,命令行会覆盖配置文件的设置
打包模式的应用
需求:在开发模式下用 style-loader 内嵌更快,在生产模式下提取 css 代码
方案1:webpack.config.js 配置导出函数,但是局限性大(只接受 2 种模式)
方案2:配置不同的 webpack.config.js (适用多种模式差异性较大情况)
方案3:借助 cross-env (跨平台通用)包命令,设置参数区分环境
方案3步骤:
- 下载 cross-env 软件包到当前项目:npm i cross-env --save-dev
- 配置自定义命令,传入参数名和值(会绑定到第三步的 process.env 对象下)
- 使用三元表达式,区分不同环境使用不同配置
- 重新打包观察两种配置区别
/* package.json */
"scripts": {
"test": "echo "Error: no test specified\" && exit 1",
"build": "cross-env NODE_ENV=production webpack --mode=production",
"dev": "cross-env NODE_ENV=development webpack serve --open --mode=development!"
},
/* webpack.config.js */
module: {
rules: [
{
test: /\.css$/i,
// use: ['style-loader', "css-loader"],
use: [process.env.NODE_ENV === 'development' ? 'style-loader' : MiniCssExtractPlugin.loader, "css-loader"]
},
{
test: /\.less$/i,
use: [
// compiles Less to CSS
process.env.NODE_ENV === 'development' ? 'style-loader' : MiniCssExtractPlugin.loader,
'css-loader',
'less-loader',
],
}
],
},
因为 dev 运行时是打包进内存,看不到区别,所以下面都用 build 来区分
区别一:
/* package.json */
"build": "cross-env NODE_ENV=production webpack --mode=production",
根据 webpack.config.js 里的三元表达式得出不等于 development,所以 MiniCssExtractPlugin.loader 生效,独立打包出来
区别二:
/* package.json */
"build": "cross-env NODE_ENV=development webpack --mode=production",
根据 webpack.config.js 里的三元表达式得出等于 development,所以 style-loader 生效,css 内嵌在html文件里,不会再独立出现
注入环境变量
需求:前端项目中,开发模式下打印语句生效,生产模式下打印语句失效
问题:cross-env 设置的只在 Node.js 环境生效,前端代码无法访问 process.env.NODE_ENV
解决:使用 Webpack 内置的 DefinePlugin 插件
作用:在编译时,将前端代码中匹配的变量名,替换为值或表达式
配置:在 webpack.config.js 中给前端注入环境变量
/* webpack.config.js */
// ...
const webpack = require('webpack')
module.exports = {
// ...
plugins: [
// ...
new webpack.DefinePlugin({
// key 是注入到打包后的前端 JS 代码中作为全局变量
// value 是变量对应的值(在 corss-env 注入在 node.js 中的环境变量字符串)
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
})
]
}
/* index.js */
if(process.env.NODE_ENV === 'production') {
console.log = function() {}
}
console.log('开发模式下好用,生产模式下失效并隐藏')
开发环境调错
目标:在开发环境如何精准定位到报错源码位置
-
source map:可以准确追踪 error 和 warning 在原始代码的位置
-
问题:代码被压缩和混淆,在控制台无法准确定位源代码位置(行数和列数)
-
设置:webpack.config.js 配置 devtool 选项
/* webpack.config.js */
module.exports = {
// ...
devtool: 'inline-source-map' // 把源码的位置信息一起打包在 JS 文件内,但这么配置的话,在生产环境也能定位报错
}
注意:source map 适用于开发环境,但最好不要在生产环境使用(防止被轻易查看源码位置),则配置项可改成下面这种:
/* webpack.config.js */
const config = {...}
module.exports = {
// 只在开发环境下使用 sourcemap 选项
if(process.env.NODE_ENV === 'development') {
config.devtool = 'inline-source-map'
}
}
module.exports = config
这样在开发环境运行时能看到一些报错信息(点击追查源码位置),但打包上线后则看不到报错信息了,也就无法精准定位
定义路径别名
解析别名:配置模块如何解析,创建 import 引入路径的别名,来确保模块引入变得更简单
例如:原来路径如下,比较长而且相对路径不安全
import { checkPhone, checkCode } from '../src/utils/check.js'
解决:在 webpack.config.js 中配置解析别名 @ 来代表 src 绝对路径
/* webpack.config.js */
const config = {
// ...
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
}
}
这样我们以后,引入目标模块写的路径就更简单了
import { checkPhone, checkCode } from '@/utils/check.js'
优化 - CDN 使用
CDN定义:内容分发网络,指的是一组分布在各个地区的服务器
作用:把静态资源文件/第三方库放在 CDN 网络中各个服务器中,供用户就近请求获取
好处:生产环境下打包的项目体积小了,也减轻自己服务器请求压力,就近请求物理延迟低,配套缓存策略
说人话就是:开发时使用第三方插件/库时需要下载并导入使用,造成项目体积较大,而项目上线时采用 CDN 加载引入可以缩小体积
实现需求的思路图:
需求:开发模式使用本地第三方库,生产模式下使用 CDN 加载引入
步骤:
1. 在 html 中引入第三方库的 CDN 地址 并用模板语法判断
<% if(htmlWebpackPlugin.options.useCdn){ %>
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.2.3/css/bootstrap.min.css" rel="stylesheet">
<% } %>
2. 配置 webpack.config.js 中 externals 外部扩展选项(防止某些 import 的包被打包)
// 生产环境下使用相关配置
if (process.env.NODE_ENV === 'production') {
// 外部扩展(让 webpack 防止 import 的包被打包进来)
config.externals = {
// key:import from 语句后面的字符串
// value:留在原地的全局变量(最好和 cdn 在全局暴露的变量一致)
'bootstrap/dist/css/bootstrap.min.css': 'bootstrap',
'axios': 'axios'
}
}
// ...
const config = {
// ...
plugins: [
new HtmlWebpackPlugin({
// ...
// 自定义属性,在 html 模板中 <%=htmlWebpackPlugin.options.useCdn%> 访问使用
useCdn: process.env.NODE_ENV === 'production'
})
]
}
3. 两种模式下打包观察效果
多页面打包
单页面:单个 html 文件,切换 DOM 的方式实现不同业务逻辑展示,后续 Vue/React 会学到
多页面:多个 html 文件,切换页面实现不同业务逻辑展示
需求:把黑马头条-数据管理平台-内容页面一起引入打包使用
步骤:
-
准备源码(html,css,js)放入相应位置,并改用模块化语法导出
-
下载 form-serialize 包并导入到核心代码中使用(略过)
-
配置 webpack.config.js 多入口和多页面的设置
-
重新打包观察效果
// ...
const config = {
entry: {
'login': path.resolve(__dirname, 'src/login/index.js'),
'content': path.resolve(__dirname, 'src/content/index.js'),
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: './[name]/index.js' // [name] 根据入口文件不同具体的key值生成不同的打包文件
clean: true // 生成打包后内容之前,清空输出目录
}
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(_dirname, 'public/login.html'), // 模板文件
filename: path.resolve(_dirname, 'dist/login/index.html'), // 输出文件
chunks: ['login'] // 引入哪些打包后的模块(和 entry 的 key 一致)
})
new HtmlWebpackPlugin({
template: path.resolve(_dirname, 'public/content.html'), // 模板文件
filename: path.resolve(_dirname, 'dist/content/index.html'), // 输出文件
chunks: ['content']
})
]
}
优化 - 抽离公共代码
需求:把 2 个以上页面引用的公共代码提取
步骤:
-
配置 webpack.config.js 的 splitChunks 分割功能
-
打包观察效果
// ...
const config = {
// ...
optimization: {
// ...
splitChunks: {
chunks: 'all', // 所有模块动态非动态移入的都分割分析
cacheGroups: { // 分隔组
commons: { // 抽取公共模块
minSize: 0, // 抽取的chunk最小大小字节
minChunks: 2, // 最小引用数
reuseExistingChunk: true, // 当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用
name(module, chunks, cacheGroupKey) { // 分离出模块文件名
const allChunksNames = chunks.map((item) => item.name).join('~') // 模块名1~模块名2
return `./js/${allChunksNames}` // 输出到 dist 目录下位置
}
}
}
}
}
}