ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从课程作业到企业级Web应用:Vue3+Node.js全栈开发实战指南

从课程作业到企业级Web应用:Vue3+Node.js全栈开发实战指南 简介本资源是面向高校网页设计课程学习者的动态网页开发实践作业包聚焦前端与后端协同开发能力培养适用于掌握HTML/CSS/JavaScript基础后进阶学习服务器交互、数据库集成与响应式交互的学生。压缩包共76个文件涵盖28张界面截图png、7个配置与元数据文件xml/yaml/json等、5个Dart核心逻辑文件含main.dart与widgets模块以及Android/iOS双平台工程配置gradle、plist、storyboard、xcsettings等整体仅106KB轻量但结构完整体现Flutter跨端动态网页技术栈的课程实践路径。已有212人下载学习资源以‘010’项目为载体包含可运行的HTML入口、数据库操作示意、AJAX风格数据交互逻辑及完整构建脚本build.sh目录组织清晰便于理解动态网页从页面渲染、状态管理到平台适配的全流程实现。1. 从“课程作业.zip”到真实项目一个Web开发者的实战复盘最近整理硬盘翻出来一个尘封已久的压缩包名字就叫“Web 动态网页设计课程作业.zip”。点开一看是当年大学里为了应付课程匆匆赶出来的一个“学生信息管理系统”界面简陋功能单一典型的“玩具项目”。但正是这个不起眼的压缩包让我想起了自己从只会写静态页面的“小白”到能独立负责企业级Web应用开发的整个成长路径。今天我想以这个“课程作业”为引子结合这些年踩过的坑和积累的经验和大家聊聊一个真正的、可交付的Web动态网页项目到底应该包含哪些东西以及如何从零开始搭建一个扎实的、可维护的现代Web应用骨架。无论你是正在完成课程作业的学生还是刚入行的前端或全栈开发者希望这篇复盘能给你一些超越课堂的实战视角。很多人对“动态网页设计”的理解可能还停留在“用JavaScript让页面动起来”或者“能连接数据库增删改查”的层面。这没错但远远不够。一个合格的动态Web应用其核心在于数据驱动视图、状态可预测、前后端职责清晰、以及具备良好的工程化支撑。我们当年的作业往往只做到了“功能实现”而忽略了安全性、性能、可维护性和部署运维这些在生产环境中至关重要的问题。接下来我将抛开那个简陋的作业从头构建一个更贴近实战的“微型博客系统”涵盖从技术选型、环境搭建、核心功能实现到安全加固和部署上线的完整闭环。2. 技术栈选型为什么是它们而不是别的面对琳琅满目的框架和工具新手最容易犯的错就是“哪个火用哪个”或者“作业要求用什么就用什么”而不去思考背后的原因。这里我给出一个2024年依然稳健且学习曲线平缓的全栈方案并解释每个选择的理由。前端Vue 3 TypeScript Vite PiniaVue 3 vs React vs 原生JS/ jQuery课程作业可能只要求jQuery甚至原生JS操作DOM。但对于动态性强的应用手动管理DOM更新是灾难性的。Vue的响应式系统和组合式API让状态到视图的映射变得声明式和高效。相比ReactVue的模板语法对初学者更友好更贴近传统的HTML/CSS/JS分离思维易于从静态页面过渡。选择Vue 3而非Vue 2是为了直接拥抱更优的性能Composition API、更好的Tree-shaking和未来。TypeScript这是从“玩具”迈向“工程”的关键一步。它为JavaScript提供了静态类型检查能在编码阶段就发现潜在的类型错误极大提升了代码的可靠性和开发体验。对于稍复杂的项目没有TypeScript就像在黑暗中摸索重构时更是胆战心惊。Vite取代Webpack作为构建工具。它的开发服务器基于原生ESM冷启动和热更新速度极快能带来丝滑的开发体验。配置也比Webpack简单得多更符合现代前端开发节奏。PiniaVue官方推荐的状态管理库替代Vuex。它的API更简洁对TypeScript的支持天生友好并且去除了冗长的mutations概念直接用actions处理同步和异步逻辑心智负担更小。后端Node.js Express Prisma SQLite (开发) / PostgreSQL (生产)Node.js Express对于全栈JavaScript开发者这是最自然的选择。Express是一个极简的Web框架灵活、中间件生态丰富能让你专注于业务逻辑而非框架本身。相比更“重”的NestJSExpress的学习成本更低更适合快速原型和中小项目。Prisma下一代ORM对象关系映射工具。它用Prisma Schema定义数据模型然后生成类型安全的客户端代码。这意味着你在写后端业务逻辑时数据库查询会有完美的TypeScript类型提示和自动补全彻底告别手写SQL字符串和SELECT *的恐惧。这是提升开发效率和代码安全性的利器。数据库开发阶段使用SQLite零配置一个文件搞定非常适合本地开发和测试。生产环境切换为PostgreSQL它是功能强大、稳定可靠的开源关系型数据库。Prisma使得切换数据库连接几乎无需修改业务代码。为什么不用更“时髦”的比如Next.js/Nuxt.js等元框架或Go/Python后端对于学习核心原理和完成一个完整项目而言上述组合能让你更清晰地理解前后端分离的架构、RESTful API设计、数据库交互、以及类型安全的全栈开发流程。掌握了这些再切换到其他技术栈会容易得多。元框架虽然能简化部分工作如服务端渲染但也会隐藏一些细节不利于初学者理解底层HTTP请求/响应循环。3. 项目初始化与核心架构搭建光说不练假把式我们开始动手。首先确保你的系统已安装Node.js建议LTS版本和npm/yarn/pnpm包管理器。3.1 创建项目骨架我们将采用前后端分离的模式在同一个项目根目录下创建client前端和server后端两个文件夹。# 创建项目根目录 mkdir my-web-blog cd my-web-blog # 使用Vite快速创建VueTS前端项目 npm create vuelatest client # 根据提示选择TypeScript, JSX? No, Vue Router? Yes, Pinia? Yes, ESLint? Yes cd client npm install # 安装UI库这里选择轻量的Element Plus npm install element-plus element-plus/icons-vue npm install axios # HTTP客户端 # 回到根目录创建后端项目 cd .. mkdir server cd server npm init -y # 安装后端依赖 npm install express npm install -D typescript ts-node types/node types/express nodemon npm install prisma npm install prisma/client npm install bcryptjs jsonwebtoken # 用于密码加密和JWT认证 npm install cors dotenv # 处理跨域和环境变量3.2 配置后端环境与Prisma在server目录下初始化TypeScript和Prisma。npx tsc --init # 修改生成的tsconfig.json确保target是ES2020或更高module是commonjs npx prisma init这会创建prisma/schema.prisma文件。我们来定义博客系统最核心的User用户和Post文章模型。// prisma/schema.prisma generator client { provider prisma-client-js } datasource db { provider sqlite // 开发用sqlite url env(DATABASE_URL) } model User { id Int id default(autoincrement()) email String unique username String unique password String // 存储的是bcrypt加密后的哈希值 posts Post[] createdAt DateTime default(now()) updatedAt DateTime updatedAt } model Post { id Int id default(autoincrement()) title String content String db.Text // 长文本内容 published Boolean default(false) author User relation(fields: [authorId], references: [id]) authorId Int createdAt DateTime default(now()) updatedAt DateTime updatedAt }然后在.env文件中设置数据库连接DATABASE_URLfile:./dev.db运行以下命令生成数据库表和Prisma Clientnpx prisma migrate dev --name init npx prisma generate现在你可以在代码中导入PrismaClient并进行类型安全的数据库操作了。3.3 设计RESTful API路由与控制器在server/src目录下创建应用入口、路由和控制器。这是后端逻辑的核心。// server/src/index.ts import express from express; import cors from cors; import dotenv from dotenv; import authRoutes from ./routes/auth; import postRoutes from ./routes/posts; dotenv.config(); const app express(); const PORT process.env.PORT || 3000; app.use(cors()); // 允许前端跨域请求 app.use(express.json()); // 解析JSON请求体 // 路由挂载 app.use(/api/auth, authRoutes); app.use(/api/posts, postRoutes); app.listen(PORT, () { console.log(Server is running on http://localhost:${PORT}); });以文章Post的API为例我们设计一组标准的CRUD路由// server/src/routes/posts.ts import { Router } from express; import { createPost, getPosts, getPost, updatePost, deletePost } from ../controllers/postController; import { authenticateToken } from ../middleware/auth; // 认证中间件 const router Router(); // 获取所有已发布的文章公开 router.get(/, getPosts); // 获取单篇文章公开 router.get(/:id, getPost); // 创建文章需要登录 router.post(/, authenticateToken, createPost); // 更新文章需要登录且是作者本人 router.put(/:id, authenticateToken, updatePost); // 删除文章需要登录且是作者本人 router.delete(/:id, authenticateToken, deletePost); export default router;控制器Controller负责处理具体的业务逻辑。这里展示创建文章的控制器// server/src/controllers/postController.ts import { Request, Response } from express; import { PrismaClient } from prisma/client; const prisma new PrismaClient(); export const createPost async (req: Request, res: Response) { // 通过认证中间件req.user中已包含用户ID const userId (req as any).user.id; const { title, content, published } req.body; // 输入验证实际项目中应使用Joi、Zod等库 if (!title || !content) { return res.status(400).json({ error: 标题和内容为必填项 }); } try { const post await prisma.post.create({ data: { title, content, published: published || false, authorId: userId, }, include: { author: { select: { id: true, username: true } // 关联查询作者信息但不返回密码 } } }); res.status(201).json(post); } catch (error) { console.error(创建文章失败:, error); res.status(500).json({ error: 服务器内部错误 }); } };注意这里省略了详细的错误处理、输入验证和权限检查如updatePost和deletePost中需要验证当前用户是否为文章作者。在实际项目中这些是必须的否则会引发严重的安全漏洞如越权操作。4. 前端工程化与状态管理实战后端API准备好了现在我们来构建前端。前端不再是简单的几个HTML文件而是一个工程化的单页应用SPA。4.1 配置Vite与路由Vite的配置非常简洁。我们主要需要配置代理以便在开发时解决跨域问题。// client/vite.config.ts import { defineConfig } from vite import vue from vitejs/plugin-vue import { fileURLToPath, URL } from node:url export default defineConfig({ plugins: [vue()], resolve: { alias: { : fileURLToPath(new URL(./src, import.meta.url)) } }, server: { proxy: { // 将所有以 /api 开头的请求代理到后端服务器 /api: { target: http://localhost:3000, // 后端服务地址 changeOrigin: true, } } } })路由配置Vue Router用于管理页面切换// client/src/router/index.ts import { createRouter, createWebHistory } from vue-router import HomeView from ../views/HomeView.vue const router createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: /, name: home, component: HomeView }, { path: /login, name: login, component: () import(../views/LoginView.vue) }, { path: /post/:id, name: post, component: () import(../views/PostDetailView.vue), props: true // 将路由参数 id 作为 prop 传入组件 }, { path: /editor, name: editor, component: () import(../views/PostEditorView.vue), meta: { requiresAuth: true } // 需要认证的页面 } ] }) // 路由守卫检查页面是否需要登录 router.beforeEach((to, from, next) { const isAuthenticated !!localStorage.getItem(auth_token); // 简单判断实际应更复杂 if (to.meta.requiresAuth !isAuthenticated) { next({ name: login }); } else { next(); } }) export default router4.2 使用Pinia进行全局状态管理用户登录状态、用户信息等需要跨组件共享的数据我们使用Pinia来管理。// client/src/stores/auth.ts import { defineStore } from pinia import { ref, computed } from vue import axios from axios import type { User } from /types/user // 定义好的TS类型 export const useAuthStore defineStore(auth, () { const token refstring | null(localStorage.getItem(auth_token)) const user refUser | null(null) const isAuthenticated computed(() !!token.value) // 登录动作 async function login(credentials: { username: string; password: string }) { try { const response await axios.post(/api/auth/login, credentials) const { accessToken, user: userData } response.data token.value accessToken user.value userData localStorage.setItem(auth_token, accessToken) // 将token设置到axios请求头后续请求自动携带 axios.defaults.headers.common[Authorization] Bearer ${accessToken} return { success: true } } catch (error: any) { console.error(登录失败:, error) return { success: false, message: error.response?.data?.error || 登录失败 } } } // 登出动作 function logout() { token.value null user.value null localStorage.removeItem(auth_token) delete axios.defaults.headers.common[Authorization] } // 初始化时尝试获取用户信息 async function fetchUser() { if (!token.value) return try { const response await axios.get(/api/auth/me) user.value response.data } catch (error) { console.error(获取用户信息失败:, error) logout() // token可能失效直接登出 } } return { token, user, isAuthenticated, login, logout, fetchUser } })在main.ts中安装Pinia和Element Plus// client/src/main.ts import { createApp } from vue import { createPinia } from pinia import ElementPlus from element-plus import element-plus/dist/index.css import * as ElementPlusIconsVue from element-plus/icons-vue import App from ./App.vue import router from ./router const app createApp(App) const pinia createPinia() app.use(pinia) app.use(router) app.use(ElementPlus) // 注册所有图标 for (const [key, component] of Object.entries(ElementPlusIconsVue)) { app.component(key, component) } app.mount(#app)4.3 构建页面组件以文章列表和编辑器为例现在我们可以用组合式API来编写页面组件了。首先是首页的文章列表。!-- client/src/views/HomeView.vue -- template div classhome el-button v-ifauthStore.isAuthenticated typeprimary clickgoToEditor写文章/el-button div v-loadingloading el-empty v-ifposts.length 0 !loading description暂无文章 / div v-else classpost-list el-card v-forpost in posts :keypost.id classpost-card clickviewPost(post.id) template #header div classcard-header span{{ post.title }}/span el-tag v-if!post.published sizesmall typeinfo草稿/el-tag /div /template div classpost-content-preview{{ truncateContent(post.content) }}/div div classpost-meta spanel-iconUser //el-icon {{ post.author.username }}/span spanel-iconClock //el-icon {{ formatDate(post.createdAt) }}/span /div /el-card /div !-- 分页组件 -- el-pagination v-model:current-pagecurrentPage v-model:page-sizepageSize :page-sizes[5, 10, 20] layouttotal, sizes, prev, pager, next, jumper :totaltotal size-changehandleSizeChange current-changehandleCurrentChange / /div /div /template script setup langts import { ref, onMounted, computed } from vue import { useRouter } from vue-router import { useAuthStore } from /stores/auth import { Clock, User } from element-plus/icons-vue import axios from axios import type { Post } from /types/post const router useRouter() const authStore useAuthStore() const posts refPost[]([]) const loading ref(false) const currentPage ref(1) const pageSize ref(10) const total ref(0) // 计算属性用于构建API查询参数 const queryParams computed(() ({ page: currentPage.value, limit: pageSize.value, published: true // 首页只显示已发布的文章 })) // 获取文章列表 const fetchPosts async () { loading.value true try { const response await axios.get(/api/posts, { params: queryParams.value }) posts.value response.data.data // 假设API返回格式为 { data: [], total: 100 } total.value response.data.total } catch (error) { console.error(获取文章列表失败:, error) ElMessage.error(加载文章失败) } finally { loading.value false } } // 工具函数截断长文本 const truncateContent (content: string, length 150) { return content.length length ? content.substring(0, length) ... : content } // 工具函数格式化日期 const formatDate (dateString: string) { return new Date(dateString).toLocaleDateString(zh-CN) } const goToEditor () { router.push({ name: editor }) } const viewPost (id: number) { router.push({ name: post, params: { id } }) } const handleSizeChange (val: number) { pageSize.value val currentPage.value 1 fetchPosts() } const handleCurrentChange (val: number) { currentPage.value val fetchPosts() } onMounted(() { fetchPosts() }) /script style scoped .post-list { display: grid; gap: 20px; margin-top: 20px; } .post-card { cursor: pointer; transition: box-shadow 0.3s; } .post-card:hover { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); } .card-header { display: flex; justify-content: space-between; align-items: center; } .post-content-preview { color: #666; line-height: 1.6; margin-bottom: 12px; } .post-meta { display: flex; gap: 20px; font-size: 0.85rem; color: #999; } /style接下来是文章编辑页它集成了富文本编辑器这里以wangeditor/editor为例和表单提交。!-- client/src/views/PostEditorView.vue -- template div classeditor-container el-page-header backgoBack template #content span classtext-large font-600 mr-3 {{ isEditMode ? 编辑文章 : 创作新文章 }} /span /template /el-page-header el-form :modelform :rulesrules refformRef label-width80px classeditor-form el-form-item label文章标题 proptitle el-input v-modelform.title placeholder请输入文章标题 / /el-form-item el-form-item label文章内容 propcontent div styleborder: 1px solid #ccc; margin-top: 10px; Toolbar styleborder-bottom: 1px solid #ccc :editoreditorRef :defaultConfigtoolbarConfig modedefault / Editor styleheight: 500px; overflow-y: hidden; v-modelform.content :defaultConfigeditorConfig modedefault onCreatedhandleEditorCreated / /div /el-form-item el-form-item label发布状态 el-switch v-modelform.published active-text立即发布 inactive-text存为草稿 / /el-form-item el-form-item el-button typeprimary clicksubmitForm :loadingsubmitting提交/el-button el-button clickresetForm重置/el-button /el-form-item /el-form /div /template script setup langts import { onBeforeUnmount, reactive, ref, shallowRef } from vue import { useRouter, useRoute } from vue-router import { ElMessage, type FormInstance, type FormRules } from element-plus import { Editor, Toolbar } from wangeditor/editor-for-vue import wangeditor/editor/dist/css/style.css import type { IDomEditor, IEditorConfig } from wangeditor/editor import axios from axios const router useRouter() const route useRoute() const formRef refFormInstance() const submitting ref(false) // 编辑器实例 const editorRef shallowRefIDomEditor() // 表单数据 const form reactive({ id: undefined as number | undefined, title: , content: , published: false }) // 判断是编辑模式还是创建模式 const isEditMode ref(false) const postId route.query.id as string if (postId) { isEditMode.value true // 加载现有文章数据 loadPost(parseInt(postId)) } // 编辑器配置 const editorConfig: PartialIEditorConfig { placeholder: 请输入内容..., MENU_CONF: { uploadImage: { server: /api/upload, // 你需要实现一个图片上传接口 fieldName: image } } } const toolbarConfig { } // 工具栏配置可按需调整 const handleEditorCreated (editor: IDomEditor) { editorRef.value editor } // 表单验证规则 const rules: FormRules { title: [ { required: true, message: 请输入文章标题, trigger: blur }, { min: 2, max: 100, message: 长度在 2 到 100 个字符, trigger: blur } ], content: [ { required: true, message: 请输入文章内容, trigger: blur }, { min: 10, message: 内容至少10个字符, trigger: blur } ] } // 加载文章 const loadPost async (id: number) { try { const response await axios.get(/api/posts/${id}) Object.assign(form, response.data) } catch (error) { ElMessage.error(加载文章失败) router.back() } } // 提交表单 const submitForm async () { if (!formRef.value) return await formRef.value.validate(async (valid) { if (!valid) return submitting.value true try { const url isEditMode.value ? /api/posts/${form.id} : /api/posts const method isEditMode.value ? put : post await axios({ method, url, data: form }) ElMessage.success(isEditMode.value ? 更新成功 : 创建成功) router.push({ name: home }) } catch (error: any) { console.error(提交失败:, error) ElMessage.error(error.response?.data?.error || 操作失败) } finally { submitting.value false } }) } const resetForm () { if (!formRef.value) return formRef.value.resetFields() if (editorRef.value) { editorRef.value.clear() } } const goBack () { router.back() } // 组件销毁时及时销毁编辑器 onBeforeUnmount(() { if (editorRef.value null) return editorRef.value.destroy() }) /script style scoped .editor-container { max-width: 1200px; margin: 0 auto; padding: 20px; } .editor-form { margin-top: 30px; } /style5. 安全、性能与部署从“能跑”到“能用”课程作业往往止步于本地运行。但一个真正的项目必须考虑安全、性能和如何让用户访问到。5.1 后端安全加固要点输入验证与净化永远不要信任客户端传来的数据。除了前端验证后端必须对所有输入进行严格的验证和净化。可以使用Joi或Zod库定义验证模式。// 使用Zod示例 import { z } from zod; const createPostSchema z.object({ title: z.string().min(2).max(100), content: z.string().min(10), published: z.boolean().optional(), }); // 在控制器中使用 const validatedData createPostSchema.parse(req.body);身份认证与授权我们使用了JWTJSON Web Token。关键点密码存储必须使用bcrypt等加盐哈希算法绝对不要明文存储。JWT Secret使用强随机字符串并通过环境变量process.env.JWT_SECRET管理切勿硬编码。Token过期设置合理的过期时间如expiresIn: 24h并考虑使用Refresh Token机制。中间件校验像前面authenticateToken中间件那样在需要保护的路由上验证Token的有效性和用户身份。SQL注入防护使用Prisma等ORM的最大好处之一就是它使用参数化查询从根本上避免了拼接SQL字符串导致的注入风险。CORS配置在生产环境中不要使用app.use(cors())这样允许所有来源。应该明确指定前端域名。app.use(cors({ origin: process.env.FRONTEND_URL || http://localhost:5173, // 你的前端地址 credentials: true // 如果需要传递cookie }));Helmet中间件安装helmet包它通过设置一系列HTTP头来增强应用的安全性。npm install helmetimport helmet from helmet; app.use(helmet());5.2 前端性能与体验优化组件懒加载在Vue Router中我们已经在路由配置中使用了() import(...)语法来实现路由级别的懒加载这能有效减少初始包体积。图片等静态资源优化使用WebP等现代格式。实现懒加载对于长页面中的图片使用Intersection Observer API或vue-lazyload库当图片进入视口再加载。使用CDN加速。API请求优化防抖与节流对于搜索框输入等频繁触发的事件使用防抖如lodash.debounce避免短时间内发送大量请求。请求取消在组件卸载或路由跳转时取消未完成的请求。Axios提供了CancelToken机制。数据缓存对于不常变的数据如用户信息、网站配置可以在Pinia store或使用vueuse的useStorage进行本地缓存减少不必要的请求。5.3 部署上线让世界看到你的作品本地运行没问题后就该部署了。这里提供一个基于Docker和Nginx的部署方案这是目前非常主流且可靠的方式。1. 编写Dockerfile为前后端分别编写Dockerfile实现容器化。后端Dockerfile(server/Dockerfile)# 使用Node.js官方镜像 FROM node:18-alpine AS builder WORKDIR /app # 复制依赖文件并安装 COPY package*.json ./ RUN npm ci --onlyproduction # 复制源码 COPY . . # 生成Prisma Client RUN npx prisma generate # 编译TypeScript RUN npx tsc # 生产阶段 FROM node:18-alpine WORKDIR /app # 复制生产依赖和编译后的代码 COPY --frombuilder /app/node_modules ./node_modules COPY --frombuilder /app/dist ./dist COPY --frombuilder /app/package.json ./ COPY --frombuilder /app/prisma ./prisma # 设置环境变量生产环境数据库 ENV DATABASE_URLpostgresql://user:passworddb:5432/mydb ENV NODE_ENVproduction ENV PORT3000 # 暴露端口 EXPOSE 3000 # 启动命令 CMD [node, dist/index.js]前端Dockerfile(client/Dockerfile)# 构建阶段 FROM node:18-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . # 构建生产环境产物 RUN npm run build # 生产阶段使用Nginx提供静态文件 FROM nginx:alpine # 将构建产物复制到Nginx的默认静态文件目录 COPY --frombuilder /app/dist /usr/share/nginx/html # 可以复制自定义的nginx配置 # COPY nginx.conf /etc/nginx/nginx.conf EXPOSE 80 CMD [nginx, -g, daemon off;]2. 编写docker-compose.yml在项目根目录创建docker-compose.yml一键启动所有服务前端、后端、数据库。version: 3.8 services: postgres: image: postgres:15-alpine environment: POSTGRES_DB: mydb POSTGRES_USER: myuser POSTGRES_PASSWORD: mypassword volumes: - postgres_data:/var/lib/postgresql/data healthcheck: test: [CMD-SHELL, pg_isready -U myuser] interval: 10s timeout: 5s retries: 5 networks: - app-network backend: build: ./server depends_on: postgres: condition: service_healthy environment: DATABASE_URL: postgresql://myuser:mypasswordpostgres:5432/mydb JWT_SECRET: ${JWT_SECRET} # 从.env文件读取 NODE_ENV: production ports: - 3000:3000 networks: - app-network frontend: build: ./client depends_on: - backend ports: - 80:80 networks: - app-network nginx-proxy: # 可选一个统一的Nginx反向代理处理域名和SSL image: nginx:alpine volumes: - ./nginx/conf.d:/etc/nginx/conf.d:ro - ./nginx/ssl:/etc/nginx/ssl:ro ports: - 443:443 - 80:80 depends_on: - frontend - backend networks: - app-network volumes: postgres_data: networks: app-network: driver: bridge3. 部署到云服务器购买一台云服务器如阿里云ECS、腾讯云CVM。在服务器上安装Docker和Docker Compose。将项目代码包括docker-compose.yml和.env生产环境配置文件上传到服务器。在服务器项目目录下运行docker-compose up -d。配置域名解析将你的域名指向服务器IP。高级使用certbot为Nginx配置SSL证书实现HTTPS访问。通过以上步骤你的“课程作业”就升级成了一个拥有完整前后端分离架构、类型安全、具备基础安全防护、并可容器化部署的现代Web应用。这个过程远比单纯实现功能复杂但每一步都对应着真实开发中的核心关切点。希望这个详细的复盘能帮你打开一扇从“完成作业”到“构建产品”的大门。本文还有配套的精品资源点击获取
返回列表