|  | 
|  | 1 | +--- | 
|  | 2 | +title: 实现一个前端开发都使用过的工具 | 
|  | 3 | +date: 2024-12-15T14:55:36Z | 
|  | 4 | +slug: post-33 | 
|  | 5 | +author: chaseFunny:https://github.com/chaseFunny | 
|  | 6 | +tags: ["AI","前端开发","nodeJs"] | 
|  | 7 | +--- | 
|  | 8 | + | 
|  | 9 | +> 如何使用 Node.js 实现一个类似 VSCode Live Server 的本地静态资源服务器。通过 HTTP 模块搭建基础服务器,结合 livereload 和 connect-livereload 实现文件热更新功能。 | 
|  | 10 | +
 | 
|  | 11 | +最近在系统学习 nodejs,学到 http 模块的时候,想着写个东西熟悉一下相关的 API,那写个什么呢?也不知道怎么得鼠标点击了一下右键发现了它 ,相信每个前端都不会陌生,我们最开始学习前端都会安装的插件:https://open-vsx.org/extension/ritwickdey/LiveServer,它能帮我们启动具有**静态**和**动态页面实时重新加载**功能的**本地开发服务器**。 | 
|  | 12 | + | 
|  | 13 | + | 
|  | 14 | + | 
|  | 15 | +我们一般都会使用它来打开我们的 html 文件,使用它启动有很多好处: | 
|  | 16 | + | 
|  | 17 | +- 实时预览,文件更改自动刷新游览器,支持静态和动态页面的实时更新 | 
|  | 18 | +- 解决不能访问本地文件问题 | 
|  | 19 | +- 使用简单,点击一键启动服务 | 
|  | 20 | + | 
|  | 21 | +这么神奇的能力,其实实现也很简单,今天就借助 node 来实现一个阉割版本的 liveServer, | 
|  | 22 | + | 
|  | 23 | +## 需求分析 | 
|  | 24 | + | 
|  | 25 | +先不急写代码,让我们先分析一下我们要如何实现一个简易版的 liveServer,也就是需求分析,梳理清楚我们的实现思路,这样写代码也就水到渠成,很简单了,而且这样还有一个非常大的好处,我们后面说。实现思路: | 
|  | 26 | + | 
|  | 27 | +1. 创建一个 http 服务器 | 
|  | 28 | +2. 当启动服务器后,用户访问服务器不同的路由,也就是访问我们本地对应文件夹下的静态资源 | 
|  | 29 | +3. 首先我们需要把 url 的路径,转为我们本地资源的路径 | 
|  | 30 | +4. 然后我们需要判断是否存在这个文件,如果存在则读取返回 | 
|  | 31 | +5. 如果不存在,需要再次判断**是否是目录**,如果是目录,则判断下面是否有默认文件 `index.html` ,如果有则返回文件内容,没有则返回 404 | 
|  | 32 | +6. 如果不是目录,和文件不存在逻辑一致,返回 404 | 
|  | 33 | +7. 这样我们就完成了基本的一个静态资源服务器,但是还有几个细节需要处理 | 
|  | 34 | + | 
|  | 35 | +1)我们静态资源放在哪里才能被正确读取?  | 
|  | 36 | + | 
|  | 37 | +2)当读取的文件后缀不同,如何也做不同的展示形式 | 
|  | 38 | + | 
|  | 39 | +3)如何实现静态资源更新网页也实时进行热更新 | 
|  | 40 | + | 
|  | 41 | +## 功能实现 | 
|  | 42 | + | 
|  | 43 | +已经知道要干什么了,我们先完成初始功能代码: | 
|  | 44 | + | 
|  | 45 | +```js | 
|  | 46 | +// 搭建本地静态资源服务器 | 
|  | 47 | + | 
|  | 48 | +const http = require("http"); | 
|  | 49 | +const fs = require("fs"); | 
|  | 50 | +const path = require("path"); | 
|  | 51 | + | 
|  | 52 | +// 记录请求和错误 | 
|  | 53 | +/** | 
|  | 54 | + * 记录请求和错误 | 
|  | 55 | + * @param {string} message - 要记录的信息 | 
|  | 56 | + */ | 
|  | 57 | +const log = (message) => { | 
|  | 58 | +    console.log(new Date().toISOString() + ': ' + message); | 
|  | 59 | +}; | 
|  | 60 | + | 
|  | 61 | +/** | 
|  | 62 | + * 处理请求路径并返回文件路径 | 
|  | 63 | + * @param {string} p - 请求路径 | 
|  | 64 | + * @returns {string} - 解析后的文件路径 | 
|  | 65 | + */ | 
|  | 66 | +const resolvePath = (p) => { | 
|  | 67 | +    // 去除开头的 / | 
|  | 68 | +    const filePath = p.startsWith("/") ? p.slice(1) : p; | 
|  | 69 | +    //  assets 文件夹作为根目录 | 
|  | 70 | +    const fullPath = path.join(__dirname, "../assets", filePath); | 
|  | 71 | +    // 如果文件不存在 | 
|  | 72 | +    if (!fs.existsSync(fullPath)) { | 
|  | 73 | +        log(`File not found: ${fullPath}`); | 
|  | 74 | +        return null; | 
|  | 75 | +    } | 
|  | 76 | +    return fullPath; | 
|  | 77 | +}; | 
|  | 78 | + | 
|  | 79 | +/** | 
|  | 80 | + * 获取文件的内容类型 | 
|  | 81 | + * @param {string} filePath - 文件路径 | 
|  | 82 | + * @returns {string} - 内容类型 | 
|  | 83 | + */ | 
|  | 84 | +const getContentType = (filePath) => { | 
|  | 85 | +    const ext = path.extname(filePath); | 
|  | 86 | +    switch (ext) { | 
|  | 87 | +        case '.html': return 'text/html;charset=utf-8'; | 
|  | 88 | +        case '.css': return 'text/css;charset=utf-8'; | 
|  | 89 | +        case '.js': return 'application/javascript;charset=utf-8'; | 
|  | 90 | +        case '.json': return 'application/json;charset=utf-8'; | 
|  | 91 | +        default: return 'application/octet-stream'; | 
|  | 92 | +    } | 
|  | 93 | +}; | 
|  | 94 | + | 
|  | 95 | +/** | 
|  | 96 | + * 处理接受的请求和发送合适的响应 | 
|  | 97 | + * @param {*} req HTTP 请求的请求对象,包含标头、正文和查询参数等属性。 | 
|  | 98 | + * @param {*} res 响应对象用于将响应发送回客户端,允许您设置状态代码和响应数据。  | 
|  | 99 | + */ | 
|  | 100 | +const handler = (req, res) => { | 
|  | 101 | +    const filePath = resolvePath(req.url); | 
|  | 102 | +    if (filePath) { | 
|  | 103 | +        // 如果文件存在 | 
|  | 104 | +        if (fs.statSync(filePath).isFile()) { | 
|  | 105 | +            // 读取文件 | 
|  | 106 | +            fs.readFile(filePath, (err, data) => { | 
|  | 107 | +                if (err) { | 
|  | 108 | +                    log(`Error reading file: ${filePath} - ${err.message}`); | 
|  | 109 | +                    res.statusCode = 500; | 
|  | 110 | +                    res.end('500 Internal Server Error'); | 
|  | 111 | +                    return; | 
|  | 112 | +                } | 
|  | 113 | +                // 设置状态码和响应头 | 
|  | 114 | +                res.statusCode = 200; | 
|  | 115 | +                res.setHeader("Content-Type", getContentType(filePath)); | 
|  | 116 | +                // 发送响应 | 
|  | 117 | +                res.end(data); | 
|  | 118 | +            }); | 
|  | 119 | +            return; | 
|  | 120 | +        } else if (fs.statSync(filePath).isDirectory()) { | 
|  | 121 | +            // 如果是目录 | 
|  | 122 | +            //  index.html 作为目录的默认文件 | 
|  | 123 | +            const indexPath = path.join(filePath, "index.html"); | 
|  | 124 | +            if (fs.existsSync(indexPath)) { | 
|  | 125 | +                // 读取 index.html | 
|  | 126 | +                fs.readFile(indexPath, (err, data) => { | 
|  | 127 | +                    if (err) { | 
|  | 128 | +                        log(`Error reading index file: ${indexPath} - ${err.message}`); | 
|  | 129 | +                        res.statusCode = 500; | 
|  | 130 | +                        res.end('500 Internal Server Error'); | 
|  | 131 | +                        return; | 
|  | 132 | +                    } | 
|  | 133 | +                    // 设置状态码和响应头 | 
|  | 134 | +                    res.statusCode = 200; | 
|  | 135 | +                    res.setHeader("Content-Type", getContentType(indexPath)); | 
|  | 136 | +                    // 发送响应 | 
|  | 137 | +                    res.end(data); | 
|  | 138 | +                }); | 
|  | 139 | +                return; | 
|  | 140 | +            } | 
|  | 141 | +        } | 
|  | 142 | +    } | 
|  | 143 | +    // 如果文件不存在 | 
|  | 144 | +    res.statusCode = 404; | 
|  | 145 | +    res.setHeader("Content-Type", "text/html;charset=utf-8"); | 
|  | 146 | +    res.end("404 Not Found"); | 
|  | 147 | +}; | 
|  | 148 | + | 
|  | 149 | +const server = http.createServer(handler); | 
|  | 150 | + | 
|  | 151 | +server.listen(3003, () => { | 
|  | 152 | +    log('Server running at http://localhost:3003'); | 
|  | 153 | +}); | 
|  | 154 | +``` | 
|  | 155 | + | 
|  | 156 | +这里我们解决了 1 和 2 两个问题,我们约定了上一级的 assets 为静态资源存放地点,所以我们需要约定式的把要静态资源放在对应的位置;我们 getContentType 函数读取访问资源的后缀,来设置 `Content-Type` ,从而实现不同资源展示不同形式 | 
|  | 157 | + | 
|  | 158 | +那如何实现热更新呢? | 
|  | 159 | + | 
|  | 160 | +## 热更新 | 
|  | 161 | + | 
|  | 162 | +这里我们需要安装依赖来实现了,我们需要借助 `livereload` 和 `connect-livereload` ,使用 express 来搭建一个静态资源访问服务器,我们使用 `livereload` 创建一个服务器来监控文件变化,并使用 `connect-livereload` 中间件在 HTML 文件中注入 LiveReload 脚本,也就实现了实时静态服务器 | 
|  | 163 | + | 
|  | 164 | +```js | 
|  | 165 | +const fs = require("fs"); | 
|  | 166 | +const path = require("path"); | 
|  | 167 | +const livereload = require("livereload"); | 
|  | 168 | +const connectLivereload = require("connect-livereload"); | 
|  | 169 | +const express = require("express"); | 
|  | 170 | + | 
|  | 171 | +// 创建 Express 应用 | 
|  | 172 | +const app = express(); | 
|  | 173 | + | 
|  | 174 | +// 使用 connect-livereload 中间件 | 
|  | 175 | +app.use(connectLivereload()); | 
|  | 176 | + | 
|  | 177 | +// 记录请求和错误 | 
|  | 178 | +const log = (message) => { | 
|  | 179 | +  console.log(new Date().toISOString() + ": " + message); | 
|  | 180 | +}; | 
|  | 181 | + | 
|  | 182 | +// 处理请求路径并返回文件路径 | 
|  | 183 | +const resolvePath = (p) => { | 
|  | 184 | +  const filePath = p.startsWith("/") ? p.slice(1) : p; | 
|  | 185 | +  const fullPath = path.join(__dirname, "assets", filePath); | 
|  | 186 | +  if (!fs.existsSync(fullPath)) { | 
|  | 187 | +    log(`File not found: ${fullPath}`); | 
|  | 188 | +    return null; | 
|  | 189 | +  } | 
|  | 190 | +  return fullPath; | 
|  | 191 | +}; | 
|  | 192 | + | 
|  | 193 | +// 获取文件的内容类型 | 
|  | 194 | +const getContentType = (filePath) => { | 
|  | 195 | +  const ext = path.extname(filePath); | 
|  | 196 | +  switch (ext) { | 
|  | 197 | +    case ".html": | 
|  | 198 | +      return "text/html;charset=utf-8"; | 
|  | 199 | +    case ".css": | 
|  | 200 | +      return "text/css;charset=utf-8"; | 
|  | 201 | +    case ".js": | 
|  | 202 | +      return "application/javascript;charset=utf-8"; | 
|  | 203 | +    case ".json": | 
|  | 204 | +      return "application/json;charset=utf-8"; | 
|  | 205 | +    default: | 
|  | 206 | +      return "application/octet-stream"; | 
|  | 207 | +  } | 
|  | 208 | +}; | 
|  | 209 | + | 
|  | 210 | +// 处理接受的请求和发送合适的响应 | 
|  | 211 | +app.get("*", (req, res) => { | 
|  | 212 | +  const filePath = resolvePath(req.url); | 
|  | 213 | +  if (filePath) { | 
|  | 214 | +    if (fs.statSync(filePath).isFile()) { | 
|  | 215 | +      fs.readFile(filePath, (err, data) => { | 
|  | 216 | +        if (err) { | 
|  | 217 | +          log(`Error reading file: ${filePath} - ${err.message}`); | 
|  | 218 | +          res.status(500).send("500 Internal Server Error"); | 
|  | 219 | +          return; | 
|  | 220 | +        } | 
|  | 221 | +        res.status(200).type(getContentType(filePath)).send(data); | 
|  | 222 | +      }); | 
|  | 223 | +    } else if (fs.statSync(filePath).isDirectory()) { | 
|  | 224 | +      const indexPath = path.join(filePath, "index.html"); | 
|  | 225 | +      if (fs.existsSync(indexPath)) { | 
|  | 226 | +        fs.readFile(indexPath, (err, data) => { | 
|  | 227 | +          if (err) { | 
|  | 228 | +            log(`Error reading index file: ${indexPath} - ${err.message}`); | 
|  | 229 | +            res.status(500).send("500 Internal Server Error"); | 
|  | 230 | +            return; | 
|  | 231 | +          } | 
|  | 232 | +          res.status(200).type(getContentType(indexPath)).send(data); | 
|  | 233 | +        }); | 
|  | 234 | +      } | 
|  | 235 | +    } | 
|  | 236 | +  } else { | 
|  | 237 | +    res.status(404).type("text/html;charset=utf-8").send("404 Not Found"); | 
|  | 238 | +  } | 
|  | 239 | +}); | 
|  | 240 | + | 
|  | 241 | +// 创建 LiveReload 服务器并监控 assets 目录 | 
|  | 242 | +const liveReloadServer = livereload.createServer(); | 
|  | 243 | +liveReloadServer.watch(path.join(__dirname, "assets")); | 
|  | 244 | + | 
|  | 245 | +// 启动服务器 | 
|  | 246 | +const PORT = 3003; | 
|  | 247 | +app.listen(PORT, () => { | 
|  | 248 | +  log(`Server running at http://localhost:${PORT}`); | 
|  | 249 | +}); | 
|  | 250 | +``` | 
|  | 251 | + | 
|  | 252 | +如果你要尝试的话,记得 `npm init -y` ,然后安装依赖:`npm i express livereload connect-livereload` | 
|  | 253 | + | 
|  | 254 | +现在我们再启动,就是一个实时静态服务器了 | 
|  | 255 | + | 
|  | 256 | +这个工具虽然简单,但是里面的知识还是很多的: | 
|  | 257 | + | 
|  | 258 | +- 使用 http 模块搭建服务器 | 
|  | 259 | +- 使用 path 解析请求路径 | 
|  | 260 | +- 使用 fs 模块进行文件的校验,读取 | 
|  | 261 | +- 通过拆解需求,来模块化进行开发,让代码好阅读和理解 | 
|  | 262 | +-  最后的引入两个依赖基于 express 搭建一个静态资源服务器 | 
|  | 263 | + | 
|  | 264 | +最后,如果你把我们上面的需求分析喂给 AI ,它大概率也能给出正确的代码,AI 时代了解知识并且知道如何通过知识解决问题的能力变得尤为重要 | 
|  | 265 | + | 
|  | 266 | +--- | 
|  | 267 | +此文自动发布于:<a href="https://github.com/coderPerseus/blog/issues/33" target="_blank">github issues</a> | 
0 commit comments