【Node.js从 0 到 1:入门实战与项目驱动】2.4 开发工具推荐(VS Code 、IDEA及插件、终端工具、调试工具)

内容分享3天前发布
0 0 0

文章目录

第2章:Nodejs环境搭建 —— 准备你的开发工具(基于Windows 10)2.4 开发工具推荐2.4.1 Node.js开发环境搭建安装Node.js和npm环境配置多版本管理(可选)
2.4.2 代码编辑器:VS Code及其插件配置VS Code核心优势必备插件推荐VS Code配置建议
2.4.3 调试工具与技术内置调试器VS Code调试配置Chrome DevTools调试Iron-node调试工具whistle调试代理工具
2.4.5 实战案例:创建博客统计工具项目初始化项目结构设计核心代码实现运行与调试项目扩展建议
总结

第2章:Nodejs环境搭建 —— 准备你的开发工具(基于Windows 10)

2.4 开发工具推荐

Node.js开发效率很大程度上取决于开发工具的选择与配置。一个精心配置的开发环境可以让你专注于编码而不是环境问题,提供更顺畅的开发体验。本节将为你推荐Windows 10平台上Node.js开发所需的各类工具,包括代码编辑器、终端工具和调试工具,并通过实战案例展示如何应用这些工具。

2.4.1 Node.js开发环境搭建

在开始选择开发工具前,我们需要先搭建好Node.js基础开发环境。

Node.js是一个基于Chrome V8引擎的JavaScript运行时环境,使JavaScript能够脱离浏览器在服务器端运行。

安装Node.js和npm

下载Node.js安装包
访问Node.js官方网站,下载LTS(长期支持版)或Current(最新版)版本的安装包。LTS版更稳定适合生产环境,Current版包含最新特性。
运行安装程序
双击下载的.msi文件启动安装向导。在安装过程中,关键配置选项包括:
✅ 勾选Add to PATH ( 自动添加环境变量 )✅ 选择Install additional tools(可选,自动安装Chocolatey、Python等工具)
验证安装
安装完成后,打开CMD或PowerShell,输入以下命令验证安装:


node -v  # 查看Node.js版本
npm -v   # 查看npm版本

如果显示版本号(如v20.12.2和10.5.0),则表示安装成功。
【Node.js从 0 到 1:入门实战与项目驱动】2.4 开发工具推荐(VS Code 、IDEA及插件、终端工具、调试工具)

环境配置

解决权限问题和配置自定义路径:


# 创建全局模块存放目录
mkdir D:
ode_global
mkdir D:
ode_cache

# 配置npm
npm config set prefix "D:
ode_global"
npm config set cache "D:
ode_cache"

# 将路径添加到系统环境变量PATH中
多版本管理(可选)

使用nvm-windows(Windows版Node版本管理器)管理多版本Node.js:


nvm list available  # 查看可安装版本

nvm install 18.16.0 # 安装指定版本

nvm use 18.16.0     # 切换版本

【Node.js从 0 到 1:入门实战与项目驱动】2.4 开发工具推荐(VS Code 、IDEA及插件、终端工具、调试工具)

2.4.2 代码编辑器:VS Code及其插件配置


Visual Studio Code(简称VS Code)
是微软开发的轻量级但功能强大的源代码编辑器,已成为Node.js开发的首选编辑器。

VS Code核心优势

智能代码补全:基于语义分析提供智能提示集成调试功能:内置调试支持,可直接在编辑器中设置断点、检查变量Git集成:内置Git版本控制功能扩展性强:通过安装扩展支持多种编程语言和工作流程

必备插件推荐

以下是26个能够极大提升Node.js开发效率的VS Code插件,为了更清晰地展示这些插件,我整理了以下表格。

插件类别 插件名称 主要功能 适用场景
通用开发增强
Prettier - Code formatter
自动格式化代码,保持风格统一 JavaScript/TypeScript/HTML/CSS等前端语言
ESLint 检测并自动修复代码错误,确保编码标准 JavaScript/TypeScript开发
GitLens 增强Git集成,展示详细提交历史和代码注释 需要频繁进行版本控制的项目

Bracket Pair Colorizer
为配对括号着色,使代码结构更清晰 处理深层嵌套的代码逻辑
Auto Import 自动查找、分析并提供导入建议 TypeScript/TSX开发
Node.js开发专用
Node.js Extension Pack
Node.js开发扩展包,包含多种功能 Node.js项目开发
npm 提供npm包管理功能,运行脚本 所有Node.js项目
Thunder Client 轻量级REST API客户端,测试API功能 API开发和测试
Code Runner 快速运行代码片段 快速测试代码逻辑
前端开发专用
Live Server
提供HTML/CSS/JS文件的实时热加载和浏览器预览 前端网页开发
Auto Rename Tag 自动匹配并重命名配对的HTML/JSX/XML标签 HTML/JSX/XML开发
Import Cost 在行尾显示导入包的大小 前端性能优化
Tailwind CSS IntelliSense 提供Tailwind CSS的高级自动完成和工具提示 使用Tailwind CSS的项目
AI辅助编程 通义灵码(TONGYI Lingma) 智能代码续写、优化、注释生成和代码解释 提高编码效率和质量的场景
GitHub Copilot AI编程助手,提供代码建议 多种编程任务
主题与美化
Material Theme
美观大方的界面主题 所有开发场景

vscode-icons
用图标增强文件导航体验 所有开发场景
Community Material Theme 社区提供的Material主题变体 所有开发场景
VS Code配置建议

为了获得更好的Node.js开发体验,建议进行以下配置:

配置终端
将默认终端设置为PowerShell或Windows Terminal,提高命令行操作效率。
启用自动保存


"files.autoSave": "afterDelay",
"files.autoSaveDelay": 1000

配置npm集成


"npm.enableScriptExplorer": true,
"npm.packageManager": "npm",
"npm.scriptCodeLens.enabled": true

2.4.3 调试工具与技术

调试是Node.js开发过程中不可或缺的环节,好的调试工具和技术能帮助快速定位和解决问题。

内置调试器

Node.js自带内置调试器,可以使用以下命令启动:


node inspect app.js
VS Code调试配置

在VS Code中,可以通过配置launch.json文件来设置调试环境:


{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "Launch Program",
            "program": "${workspaceFolder}/app.js",
            "env": {
                "NODE_ENV": "development"
            },
            "console": "integratedTerminal"
        }
    ]
}

配置完成后,可以设置断点、查看变量值、单步调试等。

Chrome DevTools调试

Node.js还支持使用Chrome DevTools进行调试:


node --inspect-brk app.js

然后在Chrome浏览器中打开
chrome://inspect
,即可使用熟悉的DevTools界面进行调试。

Iron-node调试工具

Iron-node是一个基于Node.js的调试工具,它提供了一个交互式的调试环境。

特点和优势

实时调试:在代码运行时提供实时的调试功能,可以随时设置断点、查看变量值等命令行界面:方便地输入调试命令,如设置断点、查看变量值、执行表达式等多语言支持:支持JavaScript、TypeScript、CoffeeScript等轻量级:安装和配置简单,不会给系统带来额外的负担跨平台:可以在Windows、Mac和Linux等多个操作系统上运行

安装与使用


npm install -g iron-node
iron-node app.js
whistle调试代理工具

whistle是基于Node实现的跨平台web调试代理工具,支持所有安装了Node的操作系统。

主要功能

查看请求(响应)的基本信息、头部、内容及Timeline等重发请求、构造请求设置hosts修改请求的url参数、方法、头部、内容延迟请求、限制请求速度、设置请求超时时间替换本地文件或线上请求修改响应的状态码、头部、内容(可以指定注入js、css或html)

安装与使用


npm install -g whistle
wistle start

2.4.5 实战案例:创建博客统计工具

下面通过一个实际案例演示如何使用Node.js和推荐的工具开发一个博客统计工具,用于获取CSDN博客列表并展示统计信息。
【Node.js从 0 到 1:入门实战与项目驱动】2.4 开发工具推荐(VS Code 、IDEA及插件、终端工具、调试工具)

项目初始化

创建项目目录


mkdir blog-statistics
cd blog-statistics

初始化npm项目


npm init -y

安装依赖包


npm install express superagent cheerio async
npm install -g nodemon
项目结构设计

blog-statistics/
├── app.js                 # 主应用程序文件
├── routes/
│   └── blog.js           # 博客路由处理
├── public/
│   └── index.html        # 前端展示页面
├── package.json
└── node_modules/
核心代码实现

package.json


{
  "name": "blog-statistics",
  "version": "1.0.0",
  "description": "CSDN博客统计工具",
  "main": "app.js",
  "scripts": {
    "start": "node app.js",
    "dev": "nodemon app.js"
  },
  "dependencies": {
    "express": "^4.18.2",
    "axios": "^1.6.2",
    "cheerio": "^1.0.0-rc.12",
    "async": "^3.2.5"
  },
  "devDependencies": {
    "nodemon": "^3.0.1"
  },
  "keywords": ["blog", "statistics", "csdn"],
  "author": "Your Name",
  "license": "MIT"
}

后端API实现(app.js)


// 在 app.js 开头添加
console.log('程序开始启动...');

const express = require('express');
const path = require('path');
const blogRouter = require('./routes/blog');  // 修正路径
const app = express();
const port = process.env.PORT || 3001;

// 解决File未定义问题的兼容代码
if (typeof File === 'undefined') {
  global.File = class File {};
}

// 设置静态文件目录
app.use(express.static(path.join(__dirname, 'public')));

// 解析JSON请求体
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// 使用博客路由
app.use('/api/blog', blogRouter);

// 添加根路径路由,确保能访问index.html
app.get('/', (req, res) => {
  res.sendFile(path.join(__dirname, 'index.html'));
});

// 404处理
app.use((req, res) => {
  res.status(404).json({ error: '请求的资源不存在' });
});

// 错误处理中间件
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: '服务器内部错误' });
});

// 启动服务器
app.listen(port, () => {
  console.log(`博客统计工具运行在 http://localhost:${port}`);
  console.log(`请打开浏览器访问上述地址`);
}).on('error', (err) => {
  if (err.code === 'EADDRINUSE') {
    console.error(`端口 ${port} 已被占用,请更换端口后重试`);
  } else {
    console.error('服务器启动失败:', err);
  }
});

博客路由处理(routes/blog.js)


const express = require('express');
const router = express.Router();
const axios = require('axios');
const cheerio = require('cheerio');
const async = require('async');

// 获取博客列表接口
router.get('/list', async (req, res) => {
    try {
        const userBlogLink = req.query.url;
        if (!userBlogLink) {
            return res.status(400).json({ error: '请输入博客地址' });
        }

        // 首先获取总页数
        const totalPage = await getTotalPage(userBlogLink);
        console.log(`总共发现 ${totalPage} 页博客`);
        
        // 并发获取所有页面的博客文章
        const pagePromises = [];
        for (let i = 1; i <= totalPage; i++) {
            pagePromises.push(getOnePageBlogLink(i, userBlogLink));
        }
        
        const blogsArr = await Promise.all(pagePromises);
        
        // 合并所有博客文章
        const allBlogs = blogsArr.flat();
        
        // 计算统计数据
        const stats = {
            total: allBlogs.length,
            readCount: 0,
            commentCount: 0,
            likeCount: 0
        };
        
        // 更新统计数据
        allBlogs.forEach(blog => {
            stats.readCount += blog.readCount || 0;
            stats.commentCount += blog.commentCount || 0;
            stats.likeCount += blog.likeCount || 0;
        });
        
        console.log(`获取到数据了: ${allBlogs.length}篇博客`);
        console.log(`总阅读量: ${stats.readCount}, 总评论数: ${stats.commentCount}, 总点赞数: ${stats.likeCount}`);
        
        res.json({ 
            stats,
            blogs: allBlogs 
        });
    } catch (error) {
        console.error('获取博客列表失败:', error);
        res.status(500).json({ error: '获取博客列表失败,请检查博客地址是否正确' });
    }
});

// 获取总页数
async function getTotalPage(userBlogLink) {
    try {
        const response = await axios.get(`${userBlogLink}/article/list/1`, {
            timeout: 10000,
            headers: {
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
                'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
                'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
                'Accept-Encoding': 'gzip, deflate, br',
                'Connection': 'keep-alive',
                'Upgrade-Insecure-Requests': '1'
            }
        });
        
        let $ = cheerio.load(response.data);
        
        // 多种方式尝试获取总页数
        let totalPage = 1;
        
        // 方式1: 从分页组件获取
        const pageElements = $('.pagination-box .paginationjs-page, .page-nav .page-item, .pagination a');
        if (pageElements.length > 0) {
            const pageNumbers = [];
            pageElements.each((i, elem) => {
                const pageNum = parseInt($(elem).text().trim());
                if (!isNaN(pageNum)) pageNumbers.push(pageNum);
            });
            if (pageNumbers.length > 0) {
                totalPage = Math.max(...pageNumbers);
            }
        }
        
        // 方式2: 从文本中提取
        if (totalPage === 1) {
            const pageText = $('.pagination-box, .page-nav').text();
            const pageMatch = pageText.match(/共(d+)页/);
            if (pageMatch) {
                totalPage = parseInt(pageMatch[1]);
            }
        }
        
        console.log(`解析到总页数: ${totalPage}`);
        return totalPage;
    } catch (error) {
        console.error('获取总页数失败:', error.message);
        // 如果获取页数失败,默认只获取第一页
        return 1;
    }
}

// 获取单页博客列表
async function getOnePageBlogLink(pageIndex, userBlogLink) {
    try {
        const response = await axios.get(`${userBlogLink}/article/list/${pageIndex}`, {
            timeout: 15000,
            headers: {
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
                'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
                'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
                'Accept-Encoding': 'gzip, deflate, br',
                'Connection': 'keep-alive',
                'Upgrade-Insecure-Requests': '1',
                'Referer': userBlogLink
            }
        });
        
        let blogHrefArr = [];
        let $ = cheerio.load(response.data);
        
        // 多种选择器尝试获取博客列表
        let articleSelectors = [
            '.article-item-box',
            '.blog-list-box .blog-list-box',
            '.article-list .article-item',
            '.mainContent .article-item',
            '#articleList .article-item',
            '.article-list .list-item',
            '.article-item'
        ];
        
        let allEle = null;
        for (let selector of articleSelectors) {
            allEle = $(selector);
            if (allEle.length > 0) {
                console.log(`使用选择器: ${selector}`);
                break;
            }
        }
        
        if (!allEle || allEle.length === 0) {
            console.log('未找到博客列表,尝试备用选择器');
            // 备用选择器:直接查找所有包含博客链接的元素
            allEle = $('a').filter((i, el) => {
                const href = $(el).attr('href');
                return href && href.includes('/article/details/');
            }).parent();
        }
        
        let len = allEle.length;
        
        if (len > 0) {
            console.log(`第${pageIndex}页获取到${len}条博客记录`);
            
            for (let i = 0; i < len; i++) {
                const element = allEle.eq(i);
                
                // 获取标题和链接
                let titleElem = element.find('h4 a, .content a, a.title, .title-link, .article-title');
                if (titleElem.length === 0) {
                    // 如果没找到标题元素,尝试直接获取链接
                    titleElem = element.find('a').first();
                }
                
                let title = titleElem.text().trim();
                let href = titleElem.attr('href');
                
                // 如果href是相对路径,转换为绝对路径
                if (href && !href.startsWith('http')) {
                    href = 'https://blog.csdn.net' + (href.startsWith('/') ? href : '/' + href);
                }
                
                if (!title || !href) continue;
                
                // 清理标题
                title = title.replace(/s+/g, ' ')
                            .replace('原创', '')
                            .replace('转载', '')
                            .replace('【', '').replace('】', '')
                            .trim();
                
                // 从列表项中提取阅读数、评论数和点赞数
                let readCount = 0, commentCount = 0, likeCount = 0;
                
                // 尝试多种选择器获取阅读数
                const readSelectors = [
                    '.read-num', '.read-count', '.num', 
                    '.info-box div:contains("阅读")', 
                    '.item-blog-bottom div:contains("阅读")',
                    '.blog-list-footer div:contains("阅读")'
                ];
                
                for (let selector of readSelectors) {
                    const readElem = element.find(selector);
                    if (readElem.length) {
                        const text = readElem.text().replace(/[^0-9]/g, '');
                        readCount = parseInt(text) || 0;
                        if (readCount > 0) break;
                    }
                }
                
                // 尝试多种选择器获取评论数
                const commentSelectors = [
                    '.comment-num', '.comment-count', 
                    '.info-box div:contains("评论")', 
                    '.item-blog-bottom div:contains("评论")',
                    '.blog-list-footer div:contains("评论")'
                ];
                
                for (let selector of commentSelectors) {
                    const commentElem = element.find(selector);
                    if (commentElem.length) {
                        const text = commentElem.text().replace(/[^0-9]/g, '');
                        commentCount = parseInt(text) || 0;
                        if (commentCount > 0) break;
                    }
                }
                
                // 尝试多种选择器获取点赞数
                const likeSelectors = [
                    '.like-num', '.like-count', 
                    '.info-box div:contains("点赞")', 
                    '.item-blog-bottom div:contains("点赞")',
                    '.blog-list-footer div:contains("点赞")'
                ];
                
                for (let selector of likeSelectors) {
                    const likeElem = element.find(selector);
                    if (likeElem.length) {
                        const text = likeElem.text().replace(/[^0-9]/g, '');
                        likeCount = parseInt(text) || 0;
                        if (likeCount > 0) break;
                    }
                }
                
                // 如果从列表页面无法获取数据,尝试访问详情页面
                if (readCount === 0 && commentCount === 0 && likeCount === 0) {
                    try {
                        const detail = await getBlogDetail(href);
                        readCount = detail.readCount;
                        commentCount = detail.commentCount;
                        likeCount = detail.likeCount;
                    } catch (error) {
                        console.log(`获取博客详情失败: ${href}`);
                    }
                }
                
                let blogItem = {
                    title,
                    href,
                    readCount,
                    commentCount,
                    likeCount
                };
                
                blogHrefArr.push(blogItem);
            }
        } else {
            console.log(`第${pageIndex}页未找到博客记录`);
        }
        
        return blogHrefArr;
    } catch (error) {
        console.error(`获取第${pageIndex}页博客列表失败:`, error.message);
        return [];
    }
}

// 获取博客文章详细数据
async function getBlogDetail(blogUrl) {
    try {
        const response = await axios.get(blogUrl, {
            timeout: 10000,
            headers: {
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
                'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
                'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
                'Accept-Encoding': 'gzip, deflate, br',
                'Connection': 'keep-alive'
            }
        });
        
        let $ = cheerio.load(response.data);
        let readCount = 0, commentCount = 0, likeCount = 0;
        
        // 尝试多种选择器获取阅读数
        const readSelectors = [
            '#viewCount', '.read-count', '.article-view-count',
            'span.read-count', 'div.read-count', 'li.read-count',
            'span:contains("阅读")', 'div:contains("阅读")', 'li:contains("阅读")'
        ];
        
        for (let selector of readSelectors) {
            const elem = $(selector);
            if (elem.length) {
                const text = elem.text().replace(/[^0-9]/g, '');
                readCount = parseInt(text) || 0;
                if (readCount > 0) break;
            }
        }
        
        // 尝试多种选择器获取评论数
        const commentSelectors = [
            '#commentCount', '.comment-count', '.article-comment-count',
            'span.comment-count', 'div.comment-count', 'li.comment-count',
            'span:contains("评论")', 'div:contains("评论")', 'li:contains("评论")'
        ];
        
        for (let selector of commentSelectors) {
            const elem = $(selector);
            if (elem.length) {
                const text = elem.text().replace(/[^0-9]/g, '');
                commentCount = parseInt(text) || 0;
                if (commentCount > 0) break;
            }
        }
        
        // 尝试多种选择器获取点赞数
        const likeSelectors = [
            '#likeCount', '.like-count', '.article-like-count',
            'span.like-count', 'div.like-count', 'li.like-count',
            'span:contains("点赞")', 'div:contains("点赞")', 'li:contains("点赞")'
        ];
        
        for (let selector of likeSelectors) {
            const elem = $(selector);
            if (elem.length) {
                const text = elem.text().replace(/[^0-9]/g, '');
                likeCount = parseInt(text) || 0;
                if (likeCount > 0) break;
            }
        }
        
        return {
            readCount,
            commentCount,
            likeCount
        };
    } catch (error) {
        console.error(`获取博客详情失败 (${blogUrl}):`, error.message);
        return {
            readCount: 0,
            commentCount: 0,
            likeCount: 0
        };
    }
}

module.exports = router;

前端页面(public/index.html)


<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CSDN博客统计工具</title>
    <!-- 替换为 Bootstrap 5.3.0 稳定版 -->
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
    <!-- 替换为 Bootstrap Icons 最新稳定版 -->
    <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/bootstrap-icons/1.11.0/font/bootstrap-icons.css">
	<!-- 替换原有的 Chart.js 引用 -->
	<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.8/dist/chart.umd.min.js"></script>
    <!-- 提前引入 jQuery 并使用稳定CDN -->
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
    <style>
        /* 保持原有样式不变 */
        body {
            background-color: #f8f9fa;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        }
        .header {
            background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
            color: white;
            padding: 2rem 0;
            margin-bottom: 2rem;
            border-radius: 0 0 1rem 1rem;
        }
        .card {
            border-radius: 1rem;
            box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
            margin-bottom: 1.5rem;
            border: none;
        }
        .stat-card {
            text-align: center;
            padding: 1.5rem;
            transition: transform 0.3s;
        }
        .stat-card:hover {
            transform: translateY(-5px);
        }
        .stat-number {
            font-size: 2.5rem;
            font-weight: bold;
            margin-bottom: 0.5rem;
        }
        .blog-item {
            border-left: 4px solid #6a11cb;
            padding: 1rem;
            margin-bottom: 1rem;
            border-radius: 0.5rem;
            background-color: white;
            transition: all 0.3s;
        }
        .blog-item:hover {
            box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.1);
            transform: translateX(5px);
        }
        .blog-stats {
            display: flex;
            gap: 1rem;
            margin-top: 0.5rem;
            font-size: 0.9rem;
        }
        .blog-stat {
            display: flex;
            align-items: center;
            gap: 0.3rem;
        }
        .chart-container {
            position: relative;
            height: 300px;
            margin-bottom: 2rem;
        }
        .btn-primary {
            background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
            border: none;
            border-radius: 2rem;
            padding: 0.5rem 2rem;
        }
        .form-control {
            border-radius: 2rem;
            padding: 0.5rem 1.5rem;
        }
        #loading {
            display: none;
            text-align: center;
            padding: 2rem;
        }
        .spinner-border {
            width: 3rem;
            height: 3rem;
        }
        #result {
            display: none;
        }
        .toast {
            position: fixed;
            top: 20px;
            right: 20px;
            z-index: 9999;
        }
    </style>
</head>
<!-- 底部的 Bootstrap JS 也替换为配套版本 -->
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
<body>
<!-- 保持原有HTML结构不变 -->
<div class="header">
    <div class="container">
        <div class="row align-items-center">
            <div class="col-md-8">
                <h1><i class="bi bi-journal-text"></i> CSDN博客统计工具</h1>
                <p class="lead">分析你的CSDN博客数据,获取详细统计信息</p>
            </div>
            <div class="col-md-4 text-md-end">
                <span class="badge bg-light text-dark">v1.0</span>
            </div>
        </div>
    </div>
</div>

<div class="container">
    <div class="row">
        <div class="col-lg-8 mx-auto">
            <div class="card">
                <div class="card-body">
                    <h5 class="card-title"><i class="bi bi-input-cursor-text"></i> 输入你的CSDN博客地址</h5>
                    <p class="card-text">例如: <code>https://blog.csdn.net/your_username</code></p>

                    <div class="input-group mb-3">
                        <span class="input-group-text"><i class="bi bi-link-45deg"></i></span>
                        <input type="text" class="form-control" id="userBlogLink"
                               placeholder="https://blog.csdn.net/your_username">
                        <button class="btn btn-primary" type="button" id="fetchBtn">
                            <i class="bi bi-graph-up"></i> 获取统计信息
                        </button>
                    </div>
                </div>
            </div>

            <div id="loading">
                <div class="spinner-border text-primary" role="status">
                    <span class="visually-hidden">加载中...</span>
                </div>
                <p class="mt-3">正在获取博客数据,请稍候...</p>
            </div>

            <div id="result">
                <div class="card">
                    <div class="card-body">
                        <h5 class="card-title"><i class="bi bi-bar-chart-fill"></i> 统计概览</h5>
                        <div class="row" id="statsOverview">
                            <!-- 统计卡片将通过JS动态生成 -->
                        </div>
                    </div>
                </div>

                <div class="card">
                    <div class="card-body">
                        <h5 class="card-title"><i class="bi bi-pie-chart-fill"></i> 数据分布</h5>
                        <div class="chart-container">
                            <canvas id="statsChart"></canvas>
                        </div>
                    </div>
                </div>

                <div class="card">
                    <div class="card-body">
                        <div class="d-flex justify-content-between align-items-center">
                            <h5 class="card-title"><i class="bi bi-list-ul"></i> 博客列表</h5>
                            <span class="badge bg-primary" id="blogCount">共 0 篇</span>
                        </div>
                        <div id="blogList">
                            <!-- 博客列表将通过JS动态生成 -->
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

<div class="toast" id="errorToast" role="alert" aria-live="assertive" aria-atomic="true">
    <div class="toast-header">
        <strong class="me-auto">错误</strong>
        <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
    </div>
    <div class="toast-body" id="errorMessage">
        <!-- 错误消息 -->
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
    // 保持原有JavaScript代码不变
    $(document).ready(function() {
        const errorToast = new bootstrap.Toast(document.getElementById('errorToast'));

        $('#fetchBtn').click(function() {
            const blogUrl = $('#userBlogLink').val().trim();
            if (!blogUrl) {
                showError('请输入博客地址');
                return;
            }

            // 验证URL格式
            if (!blogUrl.startsWith('https://blog.csdn.net/')) {
                showError('请输入正确的CSDN博客地址,格式为: https://blog.csdn.net/用户名');
                return;
            }

            // 显示加载中,隐藏结果
            $('#loading').show();
            $('#result').hide();

            // 发送请求获取博客数据
            $.ajax({
                url: `/api/blog/list?url=${encodeURIComponent(blogUrl)}`,
                type: 'GET',
                success: function(data) {
                    $('#loading').hide();
                    $('#result').show();
                    renderStats(data.stats);
                    renderBlogList(data.blogs);
                },
                error: function(xhr) {
                    $('#loading').hide();
                    let errorMsg = '获取博客数据失败';
                    if (xhr.responseJSON && xhr.responseJSON.error) {
                        errorMsg = xhr.responseJSON.error;
                    }
                    showError(errorMsg);
                }
            });
        });

        function showError(message) {
            $('#errorMessage').text(message);
            errorToast.show();
        }

        function renderStats(stats) {
            // 更新统计概览卡片
            const statsHtml = `
                <div class="col-md-3">
                    <div class="card stat-card bg-primary text-white">
                        <div class="stat-number">${stats.total}</div>
                        <div>总文章数</div>
                    </div>
                </div>
                <div class="col-md-3">
                    <div class="card stat-card bg-info text-white">
                        <div class="stat-number">${stats.readCount}</div>
                        <div>总阅读量</div>
                    </div>
                </div>
                <div class="col-md-3">
                    <div class="card stat-card bg-success text-white">
                        <div class="stat-number">${stats.commentCount}</div>
                        <div>总评论数</div>
                    </div>
                </div>
                <div class="col-md-3">
                    <div class="card stat-card bg-warning text-dark">
                        <div class="stat-number">${stats.likeCount}</div>
                        <div>总点赞数</div>
                    </div>
                </div>
            `;

            $('#statsOverview').html(statsHtml);

            // 渲染图表
            renderChart(stats);
        }

        function renderChart(stats) {
            const ctx = document.getElementById('statsChart').getContext('2d');

            // 如果已有图表实例,先销毁
            if (window.statsChartInstance) {
                window.statsChartInstance.destroy();
            }

            window.statsChartInstance = new Chart(ctx, {
                type: 'doughnut',
                data: {
                    labels: ['阅读量', '评论数', '点赞数'],
                    datasets: [{
                        data: [stats.readCount, stats.commentCount, stats.likeCount],
                        backgroundColor: [
                            'rgba(54, 162, 235, 0.8)',
                            'rgba(75, 192, 192, 0.8)',
                            'rgba(255, 206, 86, 0.8)'
                        ],
                        borderColor: [
                            'rgba(54, 162, 235, 1)',
                            'rgba(75, 192, 192, 1)',
                            'rgba(255, 206, 86, 1)'
                        ],
                        borderWidth: 1
                    }]
                },
                options: {
                    responsive: true,
                    maintainAspectRatio: false,
                    plugins: {
                        legend: {
                            position: 'bottom'
                        },
                        tooltip: {
                            callbacks: {
                                label: function(context) {
                                    return `${context.label}: ${context.raw.toLocaleString()}`;
                                }
                            }
                        }
                    }
                }
            });
        }

        function renderBlogList(blogs) {
            $('#blogCount').text(`共 ${blogs.length} 篇`);

            let blogListHtml = '';

            blogs.forEach(blog => {
                blogListHtml += `
                    <div class="blog-item">
                        <h6><a href="${blog.href}" target="_blank">${blog.title || '无标题'}</a></h6>
                        <div class="blog-stats">
                            <span class="blog-stat text-primary">
                                <i class="bi bi-eye-fill"></i> ${blog.readCount || 0}
                            </span>
                            <span class="blog-stat text-success">
                                <i class="bi bi-chat-dots-fill"></i> ${blog.commentCount || 0}
                            </span>
                            <span class="blog-stat text-warning">
                                <i class="bi bi-hand-thumbs-up-fill"></i> ${blog.likeCount || 0}
                            </span>
                        </div>
                    </div>
                `;
            });

            $('#blogList').html(blogListHtml);
        }
    });
</script>
</body>
</html>

【Node.js从 0 到 1:入门实战与项目驱动】2.4 开发工具推荐(VS Code 、IDEA及插件、终端工具、调试工具)
【Node.js从 0 到 1:入门实战与项目驱动】2.4 开发工具推荐(VS Code 、IDEA及插件、终端工具、调试工具)

运行与调试

启动开发服务器


nodemon app.js

使用nodemon可以实现代码修改后自动重启服务器。

【Node.js从 0 到 1:入门实战与项目驱动】2.4 开发工具推荐(VS Code 、IDEA及插件、终端工具、调试工具)
【Node.js从 0 到 1:入门实战与项目驱动】2.4 开发工具推荐(VS Code 、IDEA及插件、终端工具、调试工具)

访问应用
打开浏览器访问
http://localhost:3000
,输入CSDN博客地址,点击”获取博客列表”按钮。
调试技巧
在VS Code中设置断点调试使用console.log输出中间值使用Chrome DevTools进行性能分析使用whistle监控网络请求

项目扩展建议

这个博客统计工具可以进一步扩展功能:

添加博客详细数据获取(阅读数、评论数、点赞数)
实现数据可视化图表展示
添加数据导出功能(Excel、PDF)
实现定时自动更新数据
添加用户认证和数据库存储

通过这个实战案例,我们展示了如何使用Node.js和相关的开发工具完成一个完整的项目开发流程,从项目初始化、依赖安装、代码编写到调试和运行。

这个案例涵盖了Web开发的前后端技术,包括Express框架使用、HTTP请求处理、HTML解析、异步编程和前端交互等关键知识点。

总结

本章详细介绍了Windows 10平台上Node.js开发环境的搭建和工具配置。

我们从Node.js安装开始,介绍了VS Code编辑器及其插件的配置,终端工具的选择,以及各种调试工具的使用方法。最后通过一个实战案例展示了如何应用这些工具进行实际项目开发。

一个良好的开发环境可以显著提高开发效率和质量。
建议根据个人习惯和项目需求,选择合适的工具并熟练掌握它们。
随着技术的不断发展,新的工具和插件不断涌现,保持学习和探索的心态,定期更新工具链,是每个开发者应该养成的习惯。

希望本章内容能帮助你搭建起高效的Node.js开发环境,为后续的学习和项目开发打下坚实基础。

© 版权声明

相关文章

暂无评论

none
暂无评论...