资讯动态

Node.js前端项目搭建与登录接口开发实战指南

发布时间:2026/10/22 23:38:44 来源:尧图企业网站定制
1. 从零到一为什么我还在用最朴素的方式搭前端项目每次带新人总会被问到同一个问题现在脚手架这么多为什么还要手写一遍项目搭建我的回答很直接——脚手架帮你省掉的是重复劳动但省不掉你对项目结构的理解。一个前端项目从空文件夹到能跑起来中间涉及包管理、构建工具、目录约定、开发服务器、接口联调这几件事任何一环出问题你都得知道去哪儿找。所以这篇内容我打算把nodejs的前端项目搭建以及登陆接口开发这件事拆开揉碎讲一遍既适合刚接触全栈开发的朋友照着做也适合已经能跑通但说不清原理的人回头补课。这里说的nodejs的前端项目指的是用Node.js作为运行环境和构建工具链的前端工程而不是把Node当后端框架用。前端项目本身跑在浏览器里但它的依赖安装、代码打包、开发服务器、接口代理全都依赖Node生态。登录接口开发则是在同一个工程里加一个轻量的服务端用来处理账号密码校验、会话维持这些逻辑。两者放在一起讲是因为实际开发中前端和后端的边界往往就在一个项目目录里分开讲反而割裂。我见过太多人卡在第一步npm install跑完不知道装了什么npm run dev起来了不知道背后发生了什么接口调不通就到处改配置。这篇内容会把这些黑盒一个个打开让你不仅跑得起来还说得清楚每一步在干什么。下面我会按真实开发顺序推进从环境准备到项目骨架再到登录接口的完整实现最后聊几个我踩过的坑。2. 环境准备Node版本、包管理器与目录规划2.1 Node版本的选择逻辑Node的版本迭代很快偶数版本是长期支持版奇数版本是尝鲜版。做前端项目我建议锁定偶数版本比如18或20系列。原因很简单前端构建工具链对Node版本有硬性要求很多工具在奇数版本上会出现原生模块编译失败的问题。你可以用node -v查看当前版本如果低于16建议升级因为Vite 5和Webpack 5的较新版本已经不再支持16以下。版本管理我推荐用nvm这类工具它允许你在同一台机器上装多个Node版本按项目切换。比如老项目锁在16新项目用20一条命令就能切。安装nvm之后nvm install 20装好nvm use 20切换nvm alias default 20设为默认。这样你就不用为了一个新项目把全局环境折腾一遍。提示切换Node版本后之前装的全局包不会跟着走需要在新版本下重新安装。这是很多人换版本后命令找不到的原因。2.2 包管理器npm、yarn还是pnpm包管理器这件事没有绝对答案但有几个判断维度。npm是Node自带的零配置但安装速度慢、磁盘占用大因为每个项目的依赖都是独立复制一份。yarn早期解决了npm的速度和锁文件问题但现在npm的差距已经很小。pnpm用硬链接和内容寻址存储多个项目共享同一份依赖磁盘占用最小安装也最快但它在某些老旧的构建工具上会有兼容性问题。我的实际选择是新项目一律pnpm老项目维护继续用原来的。pnpm的node_modules结构是符号链接这会让一些直接读取node_modules路径的工具出问题但主流构建工具都已经适配。如果你团队里有人不熟悉pnpm用npm也不会错只是慢一点。安装pnpm的方式是npm install -g pnpm然后pnpm -v验证。之后所有npm install都可以换成pnpm install命令基本兼容。2.3 目录结构的前置规划很多人拿到项目就开始写代码写到一半发现文件到处放改个接口地址要翻五个文件。我的习惯是在动手前先把目录定下来。一个典型的前端项目加登录接口目录大概长这样project-root/ src/ 前端源码 api/ 接口请求封装 components/ 通用组件 pages/ 页面 utils/ 工具函数 App.jsx main.jsx server/ 服务端代码 routes/ 路由 controllers/ 业务逻辑 middleware/ 中间件 app.js 服务入口 public/ 静态资源 package.json vite.config.js这个结构的好处是前后端代码在同一个仓库但物理隔离前端构建产物不会混进服务端代码服务端也不会被前端构建工具误处理。api目录单独抽出来是为了让接口地址、请求拦截、错误处理集中管理后面改baseURL只动一个文件。3. 项目骨架搭建从空文件夹到能跑起来的开发服务器3.1 初始化与依赖安装在空目录下执行pnpm init会生成一个package.json。然后安装核心依赖。前端框架我选React构建工具选Vite理由是Vite的开发服务器启动快热更新几乎无感配置也简单。安装命令是pnpm add react react-dom pnpm add -D vite vitejs/plugin-react-D表示开发依赖这些包只在开发和构建时用不会打进最终产物。装完之后在package.json的scripts里加两条{ scripts: { dev: vite, build: vite build } }pnpm dev启动开发服务器默认端口5173。pnpm build打包生产版本产物在dist目录。3.2 Vite配置里最容易被忽略的两项Vite的配置文件vite.config.js默认几乎不用改就能跑但有两个配置我建议一开始就加上。第一个是server.proxy用来解决开发时的跨域问题。前端跑在5173后端接口跑在3000浏览器直接请求会跨域。代理的写法是export default { server: { proxy: { /api: { target: http://localhost:3000, changeOrigin: true } } } }这样前端请求/api/loginVite开发服务器会转发到http://localhost:3000/api/login浏览器看到的是同源请求跨域问题消失。changeOrigin的作用是把请求头里的host改成目标地址某些后端框架会校验这个。第二个是resolve.alias给常用路径起别名。比如把src映射成这样不管文件嵌套多深导入都用/components/Button不用写一堆../../。配置是import { resolve } from path export default { resolve: { alias: { : resolve(__dirname, src) } } }3.3 入口文件与最小可运行页面创建index.html放在项目根目录Vite默认以它为入口。内容里有一个div idroot/div和一个指向/src/main.jsx的script标签。然后写main.jsximport React from react import ReactDOM from react-dom/client import App from ./App ReactDOM.createRoot(document.getElementById(root)).render( React.StrictMode App / /React.StrictMode )App.jsx先放一个最简单的组件确认页面能渲染出来。这一步的意义是建立一个可验证的基线后面加任何功能出问题都能回退到这个能跑的状态。我见过有人一上来就装一堆库、写一堆配置结果页面白屏排查半天发现是某个配置写错了。先跑通最小闭环再逐步加东西是更稳的做法。4. 登录接口开发在同一个工程里写一个够用的服务端4.1 为什么用Express而不是更重的框架登录接口的逻辑其实很薄接收账号密码、查库比对、返回结果、维持会话。用Express就够了它轻、中间件生态成熟、上手快。NestJS这类框架适合大型项目但对一个登录接口来说配置成本太高。安装Express和几个必要的中间件pnpm add express cors body-parser jsonwebtoken bcryptjscors处理跨域虽然开发时用Vite代理绕过了但生产环境如果前后端分域部署还是需要。body-parser解析请求体Express 4.16之后内置了express.json()其实可以不装但显式写出来更清楚。jsonwebtoken生成和校验令牌bcryptjs做密码哈希。4.2 服务端入口与中间件顺序创建server/app.jsconst express require(express) const cors require(cors) const app express() app.use(cors()) app.use(express.json()) app.use(/api, require(./routes/auth)) app.listen(3000, () { console.log(server running on 3000) })中间件的顺序很重要。cors必须在路由之前否则预检请求会被路由拦截。express.json()也必须在处理请求体的路由之前不然req.body是undefined。这个顺序问题我踩过当时接口一直返回400排查半天发现是json解析中间件写在了路由后面。4.3 密码哈希为什么不能明文存密码存储的第一原则是永远不要明文存。原因是一旦数据库泄露所有用户的密码直接暴露而且很多人跨平台用同一个密码影响面会扩大。哈希的作用是把密码变成不可逆的字符串即使拿到哈希值也推不出原密码。bcryptjs是bcrypt算法的纯JS实现不需要编译原生模块安装即用。它的特点是自带盐值每次哈希同一个密码结果都不同但校验时能正确匹配。用法const bcrypt require(bcryptjs) const hash await bcrypt.hash(password, 10) const match await bcrypt.compare(inputPassword, hash)第二个参数10是盐的轮数数值越大越安全但越慢。10到12是常用范围再高会影响登录响应速度。我一般用10对普通应用足够。4.4 JWT令牌的生成与校验登录成功后服务端需要给客户端一个凭证后续请求带着它证明身份。JWT是常用方案它把用户信息编码进令牌服务端不用存session适合前后端分离。生成令牌const jwt require(jsonwebtoken) const token jwt.sign( { userId: user.id }, process.env.JWT_SECRET, { expiresIn: 7d } )JWT_SECRET是签名密钥必须放在环境变量里不能写死在代码中。写死的话代码一旦泄露任何人都能伪造令牌。expiresIn设过期时间7天是常见值太长不安全太短用户频繁登录。校验令牌用中间件function auth(req, res, next) { const token req.headers.authorization?.split( )[1] if (!token) return res.status(401).json({ message: 未登录 }) try { req.user jwt.verify(token, process.env.JWT_SECRET) next() } catch { res.status(401).json({ message: 令牌无效 }) } }authorization头的格式是Bearer token所以用空格分割取第二部分。校验失败统一返回401前端收到401就跳登录页。4.5 登录路由的完整实现把上面的东西串起来server/routes/auth.jsconst express require(express) const bcrypt require(bcryptjs) const jwt require(jsonwebtoken) const router express.Router() const users [] router.post(/register, async (req, res) { const { username, password } req.body if (users.find(u u.username username)) { return res.status(409).json({ message: 用户已存在 }) } const hash await bcrypt.hash(password, 10) users.push({ id: users.length 1, username, password: hash }) res.json({ message: 注册成功 }) }) router.post(/login, async (req, res) { const { username, password } req.body const user users.find(u u.username username) if (!user) return res.status(401).json({ message: 账号或密码错误 }) const match await bcrypt.compare(password, user.password) if (!match) return res.status(401).json({ message: 账号或密码错误 }) const token jwt.sign({ userId: user.id }, process.env.JWT_SECRET, { expiresIn: 7d }) res.json({ token, username: user.username }) }) module.exports router这里用内存数组模拟数据库实际项目换成真实数据库即可逻辑不变。注意登录失败时账号不存在和密码错误返回同样的提示这是为了避免攻击者通过错误信息判断账号是否存在。5. 前端对接登录接口请求封装与状态管理5.1 请求封装的必要性直接在组件里写fetch或axios不是不行但接口地址散落各处改一个baseURL要全局搜索替换加统一错误处理也无处下手。封装一层的好处是集中管理。我用axios先装pnpm add axios然后建src/api/request.jsimport axios from axios const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) config.headers.Authorization Bearer ${token} return config }) request.interceptors.response.use( res res.data, err { if (err.response?.status 401) { localStorage.removeItem(token) window.location.href /login } return Promise.reject(err) } ) export default request请求拦截器自动带令牌响应拦截器统一处理401。这样业务代码里不用关心令牌怎么带、401怎么跳只关注数据本身。5.2 登录页面的交互细节登录表单看起来简单但有几个细节容易忽略。第一是提交时禁用按钮防止重复点击造成多次请求。第二是错误提示要具体但不要泄露账号是否存在。第三是登录成功后令牌存哪里。localStorage简单但有XSS风险httpOnly cookie更安全但需要服务端配合设置。普通项目用localStorage够用安全要求高的场景建议用cookie。一个简化的登录组件逻辑const [loading, setLoading] useState(false) const [error, setError] useState() async function handleSubmit(e) { e.preventDefault() setLoading(true) setError() try { const data await request.post(/login, { username, password }) localStorage.setItem(token, data.token) navigate(/dashboard) } catch (err) { setError(err.response?.data?.message || 登录失败) } finally { setLoading(false) } }finally里重置loading保证无论成功失败按钮都能恢复。这个细节很多人漏掉导致请求失败后按钮一直转圈。5.3 路由守卫与登录态判断未登录用户不应该能访问需要权限的页面。路由守卫的思路是在进入路由前检查令牌是否存在不存在就重定向到登录页。React Router里可以用一个包装组件function RequireAuth({ children }) { const token localStorage.getItem(token) if (!token) return Navigate to/login replace / return children }replace的作用是替换历史记录这样用户点后退不会回到需要权限的页面。令牌是否有效最终由服务端校验前端只做初步拦截不能作为安全依据。6. 联调阶段最容易踩的五个坑6.1 跨域问题的两种表现跨域有两种情况简单请求和预检请求。简单请求浏览器直接发服务端返回的响应头里没有Access-Control-Allow-Origin就报错。预检请求是浏览器先发一个OPTIONS请求问服务端允不允许允许了才发真实请求。很多人只配了Access-Control-Allow-Origin没处理OPTIONS导致带自定义头的请求失败。用cors中间件的话它会自动处理OPTIONS手写的话要记得加。开发阶段用Vite代理是最省事的方案生产环境如果前后端同域部署代理配置直接搬到Nginx即可。6.2 令牌过期后的用户体验令牌设了7天过期用户第8天打开页面请求全部401如果直接跳登录页用户正在填的表单就丢了。更好的做法是拦截401后先尝试刷新令牌刷新失败再跳登录。刷新令牌需要服务端提供一个/refresh接口用长期有效的refresh token换新的access token。这套机制稍复杂但用户体验好很多。简单项目可以先不做但要知道这个方向。6.3 密码传输的安全边界前端到服务端的密码传输如果是HTTP明文中间任何节点都能截获。生产环境必须上HTTPS。前端可以对密码做一次哈希再传但这只是增加一点成本不能替代HTTPS因为哈希值本身也能被重放。真正的安全靠HTTPS加服务端哈希存储加登录频率限制。6.4 环境变量的管理JWT_SECRET、数据库连接串这些不能写进代码。用.env文件管理配合dotenv加载。.env要加进.gitignore不能提交到仓库。团队协作时提供一个.env.example列出需要哪些变量但不含真实值。我见过有人把密钥提交到公开仓库结果被人扫到滥用这个教训很深刻。6.5 构建产物与接口地址的分离开发时用代理生产时前端静态文件和后端接口可能在不同域名。这时候baseURL不能写死/api要根据环境切换。Vite支持用import.meta.env.MODE判断或者用.env.production文件配不同的值。构建时Vite会把对应的值注入代码。这个机制要在项目初期就配好后期改会牵动很多文件。7. 我在这类项目里积累的几条实操心得第一条是关于依赖版本的。前端生态更新快但不要盲目追新。大版本升级往往有破坏性变更比如Vite从4到5、React从17到18都有需要改代码的地方。我的做法是锁版本package.json里不用^用精确版本升级时手动改并跑一遍测试。这样避免某天pnpm install之后项目突然跑不起来。第二条是关于接口设计的。登录接口的返回结构要统一成功返回{ token, username }失败返回{ message }前端拦截器按这个约定处理。不要一会儿返回字符串一会儿返回对象前端处理起来很痛苦。错误码也要统一401表示未登录或令牌无效403表示无权限409表示资源冲突这些约定俗成的东西遵守就好不要自创。第三条是关于调试的。接口调不通时先看Network面板的请求有没有发出去发出去了看请求头和请求体对不对再看响应状态码和响应体。这三步能定位大部分问题。如果请求根本没发出去检查前端代码发出去了但服务端没收到检查代理配置服务端收到了但返回错误看服务端日志。按这个顺序排查比到处改配置高效得多。第四条是关于代码组织的。登录相关的逻辑服务端集中在routes/auth.js前端集中在api/auth.js和登录组件里不要散落。这样以后要加第三方登录、短信登录改动范围可控。我见过登录逻辑散在五六个文件里的项目加一个验证码功能要改一圈维护成本很高。最后说一个心态上的事。搭建项目和写登录接口技术上都不复杂但它是理解前后端协作的最好切入点。把这一套跑通再去学数据库、缓存、消息队列都是在同样的框架上加东西。所以不要觉得简单就跳过认真走一遍把每个环节的原理搞清楚后面学什么都会快很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑