Node.js | Express+MongoDB 实现简易用户管理系统(一)(项目搭建 | RESTful API架构 | 前后端交互)

news2024/11/27 18:30:08

在这里插入图片描述


🖥️ NodeJS专栏:Node.js从入门到精通
🖥️ 博主的前端之路(源创征文一等奖作品):前端之行,任重道远(来自大三学长的万字自述)
🖥️ TypeScript知识总结:TypeScript 学习笔记(十万字超详细知识点总结)
🧑‍💼 个人简介:大三学生,一个不甘平庸的平凡人🍬
👉 你的一键三连是我更新的最大动力❤️!
🏆分享博主自用牛客网🏆:一个非常全面的面试刷题求职网站,点击跳转🍬


📑目录

  • 🔽 前言
  • 1️⃣ 效果演示
  • 2️⃣ 搭建项目
  • 3️⃣ 连接MongoDB
    • 🔹 配置MongoDB
    • 🔹 创建用户模型
  • 4️⃣ 创建API接口
    • 🔹 接口规范
    • 🔹 添加用户信息
    • 🔹 删除用户信息
    • 🔹 修改用户信息
    • 🔹 查询用户信息
  • 5️⃣ 搭建前端页面
    • 🔹 页面结构
    • 🔹 业务代码
  • 🔼 结语


🔽 前言

上一节我们介绍了MongoDBMongoose,这一节为了巩固Nodejs操作MongoDB数据库实现增删改查的功能,本文将带领大家使用Express+MongoDB初步制作一个简易的用户管理系统项目,并且在之后的文章中会对这个项目一点点完善,包括业务分层登录鉴权头像上传等,记的关注博主第一时间接收更新哦!

话不多说,开干!

1️⃣ 效果演示

在带领大家搭建项目之前,先向大家展示一下这一节搭建的项目最终实现的效果:

在这里插入图片描述

因为写这个项目的目的是巩固Nodejs操作数据库,重点在于功能的实现,所以我们并不会过多的去写CSS来美化页面。

本篇文章将带领大家去实现这个简易用户管理系统的以下功能:

  • 添加用户信息
  • 分页查询用户信息(不查询密码字段)
  • 修改用户信息
  • 删除用户信息

2️⃣ 搭建项目

我们将使用express 应用程序生成器来搭建项目骨架,控制台输入:

express 简易用户管理系统 --view=ejs

上面的命令将创建一个使用ejs模板的名为简易用户管理系统express项目。

express 应用程序生成器的介绍可以查看我的这篇文章:Node.js | 深入讲解 express 应用程序生成器

使用VS Code打开项目,找到package.json,修改下图所示的地方:

在这里插入图片描述

这里修改的目的是:在启动项目时使用nodemon指令来启动,这样当我们项目代码更改时它能够自动重新运行(前提是你安装了nodemon指令,没有安装的可以控制台输入npm i nodemon -g全局安装nodemon)。

之后安装mongoose,在项目根目录下打开终端执行下面这行代码进行安装:

npm i mongoose

再执行以下代码运行项目:

npm run start

浏览器打开http://localhost:3000/,出现以下页面表示运行成功:

在这里插入图片描述

3️⃣ 连接MongoDB

🔹 配置MongoDB

项目根目录下创建config文件夹(此文件夹用来存放配置文件),并在此文件夹内创建db.config.js文件(DB数据的配置文件):

// db.config.js
const mongoose = require("mongoose");

// 连接数据库
// 前缀mongodb:是固定的,后面是你的mongodb的运行端口
// user_test代表数据库名称
mongoose.connect("mongodb://127.0.0.1:27017/user_test");
// 当你插入集合和数据时,数据库user_test会自动创建

user_test是我定义的数据库的名称,大家根据需要可以自行更改。

之后在app.js文件中引入我们创建的这个db.config.js文件:

// app.js中添加以下代码
// 引入数据库模块
require("./config/db.config");

🔹 创建用户模型

项目根目录下创建model文件夹(此文件夹用来存放模型),并在此文件夹内创建UserModel.js文件(用户模型):

// UserModel.js
const mongoose = require("mongoose");

// 字段类型
const UserType = {
    username: String,
    password: String,
    age: Number,
};

// 创建一个模型(user),对应数据库中的集合(表)(users)
const UserModel = mongoose.model("user", new mongoose.Schema(UserType));
// 注意:创建的mongodb的集合名称是加s的
// mongoose.model第二个参数可以通过mongoose.Schema生成的实例来限制集合字段类型
// 因为mongodb过于自由,对类型没有限制,我们在开发中往往需要使用mongoose.Schema来手动限制数据库各种字段类型

// 导出模型
module.exports = UserModel;

根据这个用户模型,Mongoose能够自动帮我们在数据库中创建一个users集合,集合内的每条数据包含的字段有:usernamepasswordage、以及自动生成的_id

之后运行MongoDB数据库,再启动项目,MongoDB的运行窗口出现下图所示就代码我们连接成功了:

在这里插入图片描述

4️⃣ 创建API接口

app.js中的usersRouter的路由前缀修改为/api

// app.use("/users", usersRouter); 
app.use("/api", usersRouter); // 使用/api前缀

routes目录下的users.js中引入我们上面创建的用户模型:

// users.js
// 引入用户模型
const UserModel = require("../model/UserModel");

再将users.js中的下述代码删除:

/* GET users listing. */
router.get("/", function (req, res, next) {
    res.send("respond with a resource");
});

🔹 接口规范

我们常使用RES Tful架构来规范接口的定义,RES Tful特点包括:

  1. 每一个URI代表1种资源;
  2. 客户端使用GETPOSTPUTDELETE4个表示操作方式的动词对服务端资源进行操作:GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源;
  3. 通过操作资源的表现形式来操作资源;
  4. 资源的表现形式是XML或者HTML
  5. 客户端与服务端之间的交互在请求之间是无状态的,从客户端到服务端的每个请求都必须包含理解请求所必需的信息。

使用方式:

请求方式请求地址效果
GEThttp://localhost:3000/api/user获取用户列表(所有用户信息)
GEThttp://localhost:3000/api/user/{id}获取指定id的用户信息
POSThttp://localhost:3000/api/user添加用户信息
PUThttp://localhost:3000/api/user/{id}修改指定id的用户信息
DELETEhttp://localhost:3000/api/user/{id}删除指定id的用户信息

过滤信息:

过滤信息常用于GET请求,通过指定一些规则来获取对应的信息。

通用字段说明例子
limit返回记录的数量http://localhost:3000/api/user/?limit=10 获取10条数据
offset返回记录的开始位置http://localhost:3000/api/user/?offset=10 获取数据库中第10条之后的数据
page指定第几页常配合per_page使用
per_page每页的记录数http://localhost:3000/api/user/?page=2&per_page=10 按照每页10条数来算,获取第二页数据
sortby指定返回结果按照哪个属性排序常配合order使用
order指定排序顺序http://localhost:3000/api/user/?sortby=name&order=asc 获取按照name字段升序排序的数据
state指定筛选条件http://localhost:3000/api/user/?state=close 获取state为close的数据

多个过滤字段可以相互结合使用(通过&分割),并以URL参数的形式出现在请求地址之后。

🔹 添加用户信息

添加用户信息我们使用post接口,users.js中添加以下代码:

// 添加数据
router.post("/user", function (req, res, next) {
    // 获取请求体中的参数
    const { username, password, age } = req.body;
    // 插入数据库
    UserModel.create({ username, password, age })
        .then((data) => {
            res.send(data);
        })
        .catch((error) => {
            console.log(error);
            res.send({ mag: "添加信息出错!" });
        });
});

使用Api调试工具测试一下接口是否正常运行,这里使用Apifox进行测试:

Apifox的使用教程

先将测试环境的前置URL设置为我们项目运行的地址:

在这里插入图片描述

http://127.0.0.1:3000就是http://localhost:3000/

定义接口运行接口
在这里插入图片描述在这里插入图片描述

结果显示接口运行成功,在MongoDB可视化工具中能够查看到新添加的数据:

在这里插入图片描述

🔹 删除用户信息

删除用户信息我们使用delete接口,users.js中添加以下代码:

// 删除数据
router.delete("/user/:userId", function (req, res, next) {
	// 获取动态路由参数
    const { userId } = req.params;
    // 删除数据
    UserModel.deleteOne({ _id: userId })
        .then((data) => {
            res.send({ msg: "删除成功!", ...data });
        })
        .catch((error) => {
            console.log(error);
            res.send({ msg: "删除失败!" });
        });
});

使用Apifox测试:

定义接口运行接口效果
在这里插入图片描述在这里插入图片描述在这里插入图片描述

🔹 修改用户信息

修改用户信息我们使用put接口,users.js中添加以下代码:

// 修改数据
// 动态路由,获取前端传来的id
router.put("/user/:userId", function (req, res, next) {
	// 获取请求体中的参数
    const { username, password, age } = req.body;
    // 获取动态路由参数
    const { userId } = req.params;
    // 更新数据
    UserModel.updateOne(
        { _id: userId },
        {
            username,
            password,
            age,
        }
    )
        .then((data) => {
            res.send({ msg: "更新成功!", ...data });
        })
        .catch((error) => {
            console.log(error);
            res.send({ msg: "更新失败!" });
        });
});

使用Apifox测试,定义接口:

定义接口运行接口效果
在这里插入图片描述在这里插入图片描述在这里插入图片描述

🔹 查询用户信息

查询用户信息使用get接口,users.js中添加以下代码:

这里的查询先是获取到数据的总数量,然后再根据前端传来的pagelimit字段查询到指定数据,之后将查询的数据和数据总数量一并返回给前端,从而实现分页查询的功能。

// 查询数据:分页查询的接口
router.get("/user", function (req, res, next) {
	// 获取路由参数(过滤信息)
    const { page, per_page} = req.query;
    UserModel.find()
        .count() // count方法,获取数据的总数量
        .then((dataCount) => {
            // find查询,第二个参数数组指定获取的字段,这里是获取username和age以及id(默认具有),不获取password
            UserModel.find({}, ["username", "age"])
                // sort排序,按照age:1(正序)排序,age:-1为倒叙
                .sort({ age: 1 })
                // skip方法,代表跳过几条数据开始获取
                .skip((page - 1) * per_page)
                // limit方法,代表取多少条数据
                .limit(per_page)
                .then((data) => {
                    res.send({ data, dataCount });
                });
        });
});
定义接口运行接口
电脑在这里插入图片描述

至此,整个项目需要用到的Api接口就都定义好了,下面我们开始搭建前端页面。

5️⃣ 搭建前端页面

我们在views目录下的index.ejs文件中创建我们的前端页面:

注意:虽然我们这个项目是在ejs模板中搭建页面,但我们并不需要使用ejs语法,只需使用html语法即可。

🔹 页面结构

<body>
	<h1>NodeJS操作mongodb:简易用户管理系统</h1>
	
	<div>
	  用户名:<input type="text" id="username">
	</div>
	<div>
	  密码:<input type="text" id="password">
	</div>
	<div>
	  年龄:<input type="number" id="age">
	</div>
	<button id="addBtn">增加用户</button>
	
	<p>提示:在输入框中输入信息可选择点击增加用户,也可点击表格中更新按钮来更新指定数据</p>
	
	<hr>
	
	<table border="2">
	  <thead>
	    <tr>
	      <td>id</td>
	      <td>用户名</td>
	      <td>年龄</td>
	      <td>操作</td>
	    </tr>
	  </thead>
	  <tbody>
	  </tbody>
	</table>
	
	<!-- 存放分页按钮 -->
	<div id="pageBtn">
	</div>
	
	<!-- 存放业务代码 -->
	<script></script>
</body>

🔹 业务代码

获取DOM元素:

const username = document.getElementById('username')
const password = document.getElementById('password')
const age = document.getElementById('age')
const pageBtn = document.getElementById('pageBtn')
const addBtn = document.getElementById('addBtn')

定义变量:

// 数据总条数
let dataCount = 0
// 每页显示条目个数
let pageSize = 2
// 当前页数
let pageNum = 1
// 总页数
let pageCount = 1

渲染列表数据:

// 渲染列表数据
function renderTable(data) {
  const tbody = document.querySelector('tbody')
  tbody.innerHTML = data.map(item => `
    <tr>
      <td>${item._id}</td>
      <td>${item.username}</td>
      <td>${item.age}</td>
      <td>
        <button οnclick="deleteFn('${item._id}')">删除</button>
        <button οnclick="updateFn('${item._id}')">更新</button>
      </td>
  
    </tr>
  `).join('')
}

renderTable函数接收的是一个用户数据的数组集合,用来将此数据渲染到tbody中。

渲染分页按钮:

// 渲染分页按钮
function renderPageBtn(page) {
  let str = ''
  for (let i = 1; i <= page; i++) {
    if (i === pageNum) {
      // 添加红色背景
      str += `<button οnclick="getList(${i})" style="background-color: red;">第${i}页</button>`
    } else {
      str += `<button οnclick="getList(${i})">第${i}页</button>`
    }
  }
  pageBtn.innerHTML = str
}

renderPageBtn函数接收的参数表示总页数,有多少页就渲染多少个分页按钮,同时对当前所处的页对应的分页按钮做背景变红的处理,并且每个分页按钮都绑定了一个点击事件。

获取数据:

// 查询数据api
function getList(page) {
  fetch(`/api/user/?page=${page}&per_page=${pageSize}`).then(res => res.json()).then(res => {
    // 设置数据总条数
    dataCount = res.dataCount
    // 设置总页数
    pageCount = Math.ceil(dataCount / pageSize)
    // 设置当前页数
    pageNum = page
    // 渲染列表数据
    renderTable(res.data)
    // 渲染分页按钮
    renderPageBtn(pageCount)
  })
}

getList函数接收的参数表示页数,用来获取指定页的数据,并调用renderTable将数据渲染,同时调用renderPageBtn更新分页按钮。

添加数据:

// 添加数据
addBtn.onclick = () => {
  if (!(username.value && password.value && age.value)) {
    alert('请正确输入输入框信息!')
    return
  }
  fetch('/api/user', {
    method: 'post',
    body: JSON.stringify({
      username: username.value,
      password: password.value,
      age: age.value
    }),
    headers: {
      "Content-Type": "application/json"
    }
  }).then(res => res.json()).then(res => {
    // 添加完成后获取数据库中最新数据
    getList(pageNum);
    alert('添加成功!')
    // 清空表单
    username.value = ''
    password.value = ''
    age.value = ''
  })
}

更新数据:

// 更新数据api
function updateFn(id) {
  if (!(username.value && password.value && age.value)) {
    alert('请正确输入输入框信息!')
    return
  }
  fetch(`/api/user/${id}`, {
    method: 'put',
    body: JSON.stringify({
      username: username.value,
      password: password.value,
      age: age.value
    }),
    headers: {
      "Content-Type": "application/json"
    }
  }).then(res => res.json()).then(res => {
    // 添加完成后获取数据库中最新数据
    getList(pageNum);
    alert('更新成功!')
    // 清空表单
    username.value = ''
    password.value = ''
    age.value = ''
  })
}

删除数据:

// 删除数据api
function deleteFn(id) {
  fetch(`/api/user/${id}`, {
    method: 'delete',
  }).then(res => res.json()).then(res => {
    // 删除完成后获取数据库中最新数据
    // newPageCount删除完数据后最新的总页数
    let newPageCount = dataCount > 1 ? Math.ceil((dataCount - 1) / pageSize) : 1
    if (pageNum > newPageCount) {
      // 如果当前页数大于总页数则获取最后一页数据
      getList(newPageCount)
    } else {
      getList(pageNum)
    }
    alert('删除成功!')
  })
}

至此项目就全部完成啦!项目效果与文章开头展示的一样,项目最终的目录结构如下:

在这里插入图片描述

🔼 结语

经过上面一系列的操作,我们的简易用户管理系统总算是搭建好了,但这时你会发现我们的这个项目写的好乱,比如routes目录本该存放的是单纯的路由文件,但现在它还包含了操作数据库的代码:

在这里插入图片描述

并且这些操作数据库的代码也不是单纯的操作数据库,还掺杂了处理数据,返回数据的代码(如res.send),这就使整个项目的业务变得特别混乱。

各部分之间相互掺杂,导致耦合度太高,这势必为之后的维护种下了风险的种子。

我们可以使用MVC架构,实现业务分层来解决这些问题,下一篇我们就将深入去学习MVC架构,并使用它来重构这个简易用户管理系统,敬请期待!

博主的Node.js从入门到精通专栏正在持续更新中,关注博主订阅专栏学习Node不迷路!

如果本篇文章对你有所帮助,还请客官一件四连!❤️

在这里插入图片描述

基础不牢,地动山摇! 快来和博主一起来牛客网刷题巩固基础知识吧!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/3903.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【javaEE】多线程进阶(Part1 锁策略、CAS、synchronized )

目录前言/补充4. 描述一下线程池的执行流程和拒绝策略有哪些&#xff1f;【面试题&#xff01;】一、常见锁策略一&#xff09;乐观锁VS悲观锁二&#xff09;读写锁VS普通互斥锁三&#xff09;重量级锁VS轻量级锁四&#xff09;自旋锁VS挂起等待锁五&#xff09;公平锁VS非公平…

Vue框架背后的故事

文章目录前言Vue萌芽Vue名字的由来因着Vue免试进入MeteorVue逐步完善Taylor推荐VueVue因受质疑发布1.0LinusBorg加入萌生全职做Vue想法Vue在恰到好处的时机出现探索经济来源Serah Drasner加入全职投入Vue建设Vue引入国内Vue受拥国内Vue在决策背景方面的独有优势总结本期推荐前言…

JVM垃圾回收系列之垃圾收集器二

随笔 最近两个星期因为要忙公司项目上线的事情以至于发表的文章会显得碌碌庸流&#xff0c;在此以示歉意 引言 本文将介绍HotSpot中的G1GC 参考书籍&#xff1a;“深入理解Java虚拟机” 个人java知识分享项目——gitee地址 个人java知识分享项目——github地址 G1GC 介…

双向链表的操作

什么是双向链表&#xff1f; 指针域&#xff1a;用于指向当前节点的直接前驱节点&#xff1b; 数据域&#xff1a;用于存储数据元素。 指针域&#xff1a;用于指向当前节点的直接后继节点&#xff1b; typedef struct line{struct line * prior; //指向直接前趋&#xff0c;结…

超级简单的机器学习入门

超级简单的机器学习入门 文章目录超级简单的机器学习入门0.写在前面1.机器学习基本概念2.机器学习算法的类型2.1 监督学习2.2 无监督学习2.3 监督学习和无监督学习的对比2.4 强化学习3.机器学习的三个基本要素3.1 模型3.2 学习准则3.2.1 损失函数3.2.2 欠拟合和过拟合&#xff…

MySQL数据库 || 增删改查操作详解

目录 前言&#xff1a; 插入数据 查询数据 全列查询 指定列查询 带表达式查询 去重查询 查询结果排序 条件查询 比较运算符 逻辑运算符 示例 模糊查询 示例 空值比较 分页查询 修改数据 删除数据 注意&#xff1a; 前言&#xff1a; &#x1f388;增删改查…

Flutter——常用布局

Flutter—常用布局效果图widget 树形图左布局Text评分条提示内容右布局应用Stack布局效果图释示例效果图释电影封面电影信息电影演员电影简介应用效果图 widget 树形图 整个界面由一行组成&#xff0c;分为两列&#xff1b;左列包括电影介绍&#xff0c;由上到下垂直排列&…

java计算机毕业设计ssm+jsp线上授课系统

项目介绍 通篇文章的撰写基础是实际的应用需要&#xff0c;然后在架构系统之前全面复习线上授课的相关知识以及网络提供的技术应用教程&#xff0c;以线上授课的实际应用需要出发&#xff0c;架构系统来改善现线上授课工作流程繁琐等问题。不仅如此以操作者的角度来说&#xf…

【JavaSE】关于数组

文章目录数组的创建与初始化数组的初始化静态初始化动态初始化数组的存储null打印数组的三种方式循环遍历打印foreach打印Arrays.toString()打印数组的练习冒泡排序常用的API数组拷贝Arrays.copyOf()数组排序Arrays.sort()数组的快速初始化Arrays.fill()二维数组数组的创建与初…

mysql之MHA的高可用

一、MHA概述 1.什么是 MHA&#xff1a; MHA&#xff08;MasterHigh Availability&#xff09;是一套优秀的MySQL高可用环境下故障切换和主从复制的软件。 MHA 的出现就是解决MySQL 单点故障的问题。 MySQL故障切换过程中&#xff0c;MHA能做到0-30秒内自动完成故障切换操作…

1分钟完成在线测试部署便捷收集班级同学文件的web管理系统

最近CSDN推出了一个新功能【云IDE】&#xff0c;个人对这个新功能(比赛奖金 )挺感兴趣的&#x1f92d;&#xff0c;于是瞬速地拿之前自己搞的一个便捷收集班级同学文件的web管理系统&#xff08;下面简称该项目为cfile&#xff09;体验了一下&#xff0c;发现功能还是挺好用的&…

Node.js 实战 第1章 欢迎进入Node.js 的世界 1.5 三种主流的Node 程序 1.6 总结

Node.js 实战 文章目录Node.js 实战第1章 欢迎进入Node.js 的世界1.5 三种主流的Node 程序1.5.1 Web 应用程序1.5.2 命令行工具和后台程序1.5.3 桌面程序1.5.4 使用Node 的应用程序1.6 总结第1章 欢迎进入Node.js 的世界 1.5 三种主流的Node 程序 Node 程序主要可以分成三种类…

某网站视频播放花屏解密

某网站视频播放花屏解密样例网址&#xff1a;aHR0cHM6Ly90di5jY3R2LmNvbS8yMDIyLzA5LzMwL1ZJREVnZ0ZRYmZ6NmlMeXZjN0F4d0NlZjIyMDkzMC5zaHRtbA 站内之前也曾经发过相关的问题 1.CCTV视频m3u8视频下载&#xff0c;下载下来时长正确&#xff0c;有声音&#xff0c;但是画面是马…

聚沙成塔【45天玩转uni-app】初探uni-app

文章目录写在前面DCloud当下跨平台开发存在的问题为什么选择uni-app写在最后写在前面 聚沙成塔——每天进步一点点&#xff0c;大家好我是几何心凉&#xff0c;不难发现越来越多的前端招聘JD中都加入了uni-app 这一项&#xff0c;它也已经成为前端开发者不可或缺的一项技能了&…

ROS1可视化利器---Webviz

0. 简介 对于ROS1而言&#xff0c;rqt和plotjuggler是我们最常用的工具&#xff0c;这两个工具&#xff1a;rqt中嵌入了很多有用的小工具&#xff0c;但是它需要播放离线包&#xff0c;没有办法对离线包进行实时的分析。而plotjuggler支持对离线bag包进行分析&#xff0c;但是…

[C语言、C++]数据结构作业:用递归实现走迷宫(打印正确通路即可)

如果是非递归情况 如果当前点&#xff08;方格&#xff09;为出口&#xff0c;则成功退出 &#xff08;否则&#xff09; 如果可继续走(向相邻点试探)&#xff0c;存在某个可前进 的相邻点(分支)则&#xff1a; 1、将当前点保存&#xff0c;以便回退 2、将相邻点作为当前点…

【数据结构】排序3——交换排序(冒泡排序、快速排序)

文章目录交换排序冒泡排序冒泡排序算法算法分析快速排序改进后的快速排序算法算法分析交换排序 【基本思想】 两两比较&#xff0c;如果发生逆序则交换&#xff0c;直到所有记录都排好序为止。 常见的交换排序方法&#xff1a; 冒泡排序T(n)O(n2) 快速排序T(n)O( nlog2n) 冒…

SpringSecurity Oauth2实战 - 06 获取用户登录信息并存储到本地线程

文章目录1. 获取用户登录信息1. 用户信息共享的ThreadLocal类 UserInfoShareHolder2. 写一个拦截器 UserInfoInterceptor3. 配置拦截器 CommonWebMvcAutoConfiguration2. 源码分析1. 认证用户通过access_token访问受限资源2. 进入过滤器 OAuth2AuthenticationProcessingFilter#…

源码分析:Transport 开发

有关 transport 相关队列的调用过程: 【T ransportService 】 TransportService.java 的所有接口由 DefaultTransportService.java 实现,里面包含四种接口: (1)发送到 ruleEngine 发送 TbProtoQueueMsg<ToRuleEngineMsg> 消息。 由 DefaultTbRuleEngineConsumerServic…

【攻破css系列——附加篇】vscode自动格式化

文章目录1. 快速格式化1.1 格式化的定义1.2 vscode的格式化组合键2. 自动格式化2.1 定义2.2 设置自动格式化的步骤1. 快速格式化 1.1 格式化的定义 格式化会让我们的代码正确缩进&#xff0c;同级标签的缩进空格一致&#xff0c;最后使我们代码更好看且易懂。 没有格式化我们…