记一次开发

记一次全栈开发

目录

概述

企业官网是公司的数字门面,不仅展示公司形象,还承载着品牌传播、产品展示、客户沟通等多种功能,我身为一名全栈开发又是公司的董事长必须自己开发
现在让我们开始吧,程序代码全部手敲耗时20小时,希望大家可以给各位开发者带来帮助

核心功能需求

  • 公司简介与品牌展示
  • 产品与服务介绍
  • 成功案例展示
  • 新闻动态发布
  • 团队介绍
  • 联系方式
  • 后台内容管理系统

技术栈选择

前端技术栈

技术 版本 用途
Vue.js 3.x 前端框架
Vite 4.x 构建工具
Vue Router 4.x 路由管理
Pinia 2.x 状态管理
Element Plus 2.x UI组件库
TypeScript 4.x 类型系统
SCSS - CSS预处理器

后端技术栈

技术 版本 用途
Node.js 16.x 运行环境
Express 4.x Web框架
MongoDB 4.4 数据库
Mongoose 6.x ODM工具
JWT - 身份认证
Multer - 文件上传

前端开发

项目结构

jishun-website/ ├── public/                 # 静态资源 ├── src/ │   ├── assets/             # 资源文件 │   ├── components/         # 组件 │   │   ├── common/         # 通用组件 │   │   └── business/       # 业务组件 │   ├── directives/         # 自定义指令 │   ├── router/             # 路由配置 │   ├── services/           # API服务 │   ├── stores/             # 状态管理 │   ├── views/              # 页面视图 │   ├── App.vue             # 根组件 │   └── main.ts             # 入口文件 ├── .env                    # 环境变量 ├── index.html              # HTML模板 ├── package.json            # 项目依赖 ├── tsconfig.json           # TypeScript配置 └── vite.config.ts          # Vite配置 

关键实现

响应式设计

使用媒体查询和弹性布局实现全设备适配:

// 断点变量 $breakpoints: (   'sm': 576px,   'md': 768px,   'lg': 992px,   'xl': 1200px,   'xxl': 1400px );  // 响应式混合宏 @mixin respond-to($breakpoint) {   $value: map-get($breakpoints, $breakpoint);      @if $value {     @media (min-width: $value) {       @content;     }   } @else {     @error "Unknown breakpoint: #{$breakpoint}";   } }  .container {   width: 100%;   padding: 0 15px;      @include respond-to('md') {     max-width: 720px;     margin: 0 auto;   }      @include respond-to('lg') {     max-width: 960px;   }      @include respond-to('xl') {     max-width: 1140px;   } } 

组件化开发

基于Vue 3组合式API实现高复用性组件:

// src/components/common/ImageCarousel.vue <script setup lang="ts"> import { ref, onMounted, watch } from 'vue';  interface Props {   images: string[];   autoplay?: boolean;   interval?: number; }  const props = withDefaults(defineProps<Props>(), {   autoplay: true,   interval: 3000 });  const currentIndex = ref(0); const timer = ref<number | null>(null);  const next = () => {   currentIndex.value = (currentIndex.value + 1) % props.images.length; };  const prev = () => {   currentIndex.value = (currentIndex.value - 1 + props.images.length) % props.images.length; };  const startAutoplay = () => {   if (props.autoplay && props.images.length > 1) {     timer.value = window.setInterval(() => {       next();     }, props.interval);   } };  const stopAutoplay = () => {   if (timer.value) {     clearInterval(timer.value);     timer.value = null;   } };  watch(() => props.images, () => {   stopAutoplay();   startAutoplay(); });  onMounted(() => {   startAutoplay(); });  defineExpose({   next,   prev }); </script>  <template>   <div      class="carousel"      @mouseenter="stopAutoplay"      @mouseleave="startAutoplay"   >     <div class="carousel-inner">       <transition-group name="fade">         <div            v-for="(image, index) in images"            :key="image"           class="carousel-item"           :class="{ active: index === currentIndex }"         >           <img :src="image" alt="Carousel image" />         </div>       </transition-group>     </div>          <button class="carousel-control prev" @click="prev">       <span class="icon">&#10094;</span>     </button>          <button class="carousel-control next" @click="next">       <span class="icon">&#10095;</span>     </button>          <div class="carousel-indicators">       <button          v-for="(_, index) in images"          :key="index"         class="indicator"         :class="{ active: index === currentIndex }"         @click="currentIndex = index"       ></button>     </div>   </div> </template>  <style scoped lang="scss"> .carousel {   position: relative;   width: 100%;   overflow: hidden;      &-inner {     position: relative;     width: 100%;     height: 100%;   }      &-item {     position: absolute;     top: 0;     left: 0;     width: 100%;     height: 100%;     opacity: 0;     transition: opacity 0.5s ease;          &.active {       opacity: 1;       z-index: 1;     }          img {       width: 100%;       height: 100%;       object-fit: cover;     } }  .fade-enter-active, .fade-leave-active {   transition: opacity 0.5s ease; }  .fade-enter-from, .fade-leave-to {   opacity: 0; } </style> 

状态管理

我使用Pinia进行状态管理:

// src/stores/news.ts import { defineStore } from 'pinia'; import { ref, computed } from 'vue'; import type { NewsItem } from '@/types'; import { fetchNewsList, fetchNewsDetail } from '@/services/api';  export const useNewsStore = defineStore('news', () => {   const newsList = ref<NewsItem[]>([]);   const currentNews = ref<NewsItem | null>(null);   const loading = ref(false);   const error = ref<string | null>(null);      const latestNews = computed(() => {     return [...newsList.value].sort((a, b) =>        new Date(b.publishDate).getTime() - new Date(a.publishDate).getTime()     ).slice(0, 5);   });      async function getNewsList() {     loading.value = true;     error.value = null;          try {       newsList.value = await fetchNewsList();     } catch (err) {       error.value = err instanceof Error ? err.message : '获取新闻列表失败';     } finally {       loading.value = false;     }   }      async function getNewsDetail(id: string) {     loading.value = true;     error.value = null;          try {       currentNews.value = await fetchNewsDetail(id);     } catch (err) {       error.value = err instanceof Error ? err.message : '获取新闻详情失败';     } finally {       loading.value = false;     }   }      return {     newsList,     currentNews,     loading,     error,     latestNews,     getNewsList,     getNewsDetail   }; }); 

后端开发

项目结构

jishun-website-backend/ ├── src/ │   ├── config/             # 配置文件 │   ├── controllers/        # 控制器 │   ├── middlewares/        # 中间件 │   ├── models/             # 数据模型 │   ├── routes/             # 路由定义 │   ├── services/           # 业务逻辑 │   ├── utils/              # 工具函数 │   ├── app.ts              # 应用配置 │   └── index.ts            # 入口文件 ├── uploads/                # 上传文件目录 ├── .env                    # 环境变量 ├── package.json            # 项目依赖 └── tsconfig.json           # TypeScript配置 

关键实现

数据模型设计

使用Mongoose定义数据模型:

// src/models/News.ts import mongoose, { Schema, Document } from 'mongoose';  export interface INews extends Document {   title: string;   content: string;   summary: string;   coverImage: string;   publishDate: Date;   author: string;   tags: string[];   isPublished: boolean;   viewCount: number;   createdAt: Date;   updatedAt: Date; }  const NewsSchema: Schema = new Schema({   title: { type: String, required: true, trim: true },   content: { type: String, required: true },   summary: { type: String, required: true, trim: true },   coverImage: { type: String, required: true },   publishDate: { type: Date, default: Date.now },   author: { type: String, required: true },   tags: [{ type: String, trim: true }],   isPublished: { type: Boolean, default: false },   viewCount: { type: Number, default: 0 } }, {   timestamps: true });  // 添加全文搜索索引 NewsSchema.index({   title: 'text',   content: 'text',   summary: 'text',   tags: 'text' });  export default mongoose.model<INews>('News', NewsSchema); 

API路由设计

RESTful API设计:

// src/routes/news.ts import express from 'express'; import {    getAllNews,   getNewsById,   createNews,   updateNews,   deleteNews,   searchNews } from '../controllers/newsController'; import { authenticate, authorize } from '../middlewares/auth'; import { validateNewsInput } from '../middlewares/validation';  const router = express.Router();  // 公开路由 router.get('/', getAllNews); router.get('/search', searchNews); router.get('/:id', getNewsById);  // 需要认证的路由 router.post('/', authenticate, authorize(['admin', 'editor']), validateNewsInput, createNews); router.put('/:id', authenticate, authorize(['admin', 'editor']), validateNewsInput, updateNews); router.delete('/:id', authenticate, authorize(['admin']), deleteNews);  export default router; 

身份验证中间件

JWT认证实现:

// src/middlewares/auth.ts import { Request, Response, NextFunction } from 'express'; import jwt from 'jsonwebtoken'; import User from '../models/User';  interface DecodedToken {   id: string;   role: string; }  declare global {   namespace Express {     interface Request {       user?: any;     }   } }  export const authenticate = async (req: Request, res: Response, next: NextFunction) => {   try {     const authHeader = req.headers.authorization;          if (!authHeader || !authHeader.startsWith('Bearer ')) {       return res.status(401).json({ message: '未提供认证令牌' });     }          const token = authHeader.split(' ')[1];     const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as DecodedToken;          const user = await User.findById(decoded.id).select('-password');          if (!user) {       return res.status(401).json({ message: '用户不存在' });     }          req.user = user;     next();   } catch (error) {     return res.status(401).json({ message: '无效的认证令牌' });   } };  export const authorize = (roles: string[]) => {   return (req: Request, res: Response, next: NextFunction) => {     if (!req.user) {       return res.status(401).json({ message: '未认证的用户' });     }          if (!roles.includes(req.user.role)) {       return res.status(403).json({ message: '没有权限执行此操作' });     }          next();   }; }; 

性能优化

前端性能优化

  1. 代码分割与懒加载
// src/router/index.ts import { createRouter, createWebHistory } from 'vue-router';  const routes = [   {     path: '/',     component: () => import('../views/Home.vue')   },   {     path: '/about',     component: () => import('../views/About.vue')   },   {     path: '/news',     component: () => import('../views/News.vue')   },   {     path: '/news/:id',     component: () => import('../views/NewsDetail.vue')   }, ];  const router = createRouter({   history: createWebHistory(),   routes,   scrollBehavior(to, from, savedPosition) {     if (savedPosition) {       return savedPosition;     } else {       return { top: 0 };     }   } });  export default router; 
  1. 图片优化
// src/directives/lazyload.ts import { DirectiveBinding } from 'vue';  export default {   mounted(el: HTMLImageElement, binding: DirectiveBinding) {     const observer = new IntersectionObserver((entries) => {       entries.forEach(entry => {         if (entry.isIntersecting) {           el.src = binding.value;           observer.unobserve(el);         }       });     });          observer.observe(el);   } };  // 使用方式 // <img v-lazy="'/path/to/image.jpg'" alt="Lazy loaded image"> 
  1. 资源预加载
<!-- index.html --> <head>   <!-- 预加载关键资源 -->   <link rel="preload" href="/fonts/custom-font.woff2" as="font" type="font/woff2" crossorigin>   <link rel="preload" href="/assets/hero-image.webp" as="image">      <!-- 预连接第三方域名 -->   <link rel="preconnect" href="https://api.example.com">   <link rel="dns-prefetch" href="https://api.example.com"> </head> 

后端性能优化

  1. 数据库查询优化
// 添加适当的索引 NewsSchema.index({ publishDate: -1 }); NewsSchema.index({ tags: 1 });  // 使用投影只返回需要的字段 const newsList = await News.find({ isPublished: true })   .select('title summary coverImage publishDate author')   .sort({ publishDate: -1 })   .limit(10); 
  1. API响应缓存
// src/middlewares/cache.ts import { Request, Response, NextFunction } from 'express'; import NodeCache from 'node-cache';  const cache = new NodeCache({ stdTTL: 60 }); // 默认缓存60秒  export const cacheMiddleware = (duration: number = 60) => {   return (req: Request, res: Response, next: NextFunction) => {     // 只缓存GET请求     if (req.method !== 'GET') {       return next();     }          const key = `__express__${req.originalUrl || req.url}`;     const cachedBody = cache.get(key);          if (cachedBody) {       res.send(cachedBody);       return;     }          const originalSend = res.send;          res.send = function(body): Response {       cache.set(key, body, duration);       return originalSend.call(this, body);     };          next();   }; };  // 使用方式 // app.use('/api/news', cacheMiddleware(300), newsRoutes); 

部署流程

宝塔面板部署

1. 安装宝塔面板

# CentOS系统 yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh && sh install.sh  # Ubuntu/Debian系统 wget -O install.sh http://download.bt.cn/install/install-ubuntu_6.0.sh && sudo bash install.sh 

2. 安装必要软件

通过宝塔面板安装:

  • Nginx 1.20
  • Node.js 16.x
  • MongoDB 4.4
  • PM2管理器

3. 前端部署

# 克隆代码库 git clone https://github.com/your-repo/jishun-website.git /www/wwwroot/jishunkeji.cn/jishun-website  # 安装依赖并构建 cd /www/wwwroot/jishunkeji.cn/jishun-website npm install npm run build 

4. 后端部署

# 克隆代码库 git clone https://github.com/your-repo/jishun-website-backend.git /www/wwwroot/jishunkeji.cn/jishun-website-backend  # 安装依赖并构建 cd /www/wwwroot/jishunkeji.cn/jishun-website-backend npm install npm run build  # 创建PM2配置文件 cat > ecosystem.config.js << 'EOL' module.exports = {   apps: [{     name: "jishun-backend",     script: "./dist/index.js",     instances: 2,     exec_mode: "cluster",     env: {       NODE_ENV: "production",       PORT: 5001     },     max_memory_restart: "300M"   }] } EOL  # 启动服务 pm2 start ecosystem.config.js 

5. Nginx配置

server {     listen 80;     server_name jishunkeji.cn www.jishunkeji.cn;          # 重定向到HTTPS     return 301 https://$host$request_uri; }  server {     listen 443 ssl http2;     server_name jishunkeji.cn www.jishunkeji.cn;          # SSL配置     ssl_certificate /path/to/cert.pem;     ssl_certificate_key /path/to/key.pem;          root /www/wwwroot/jishunkeji.cn/jishun-website/dist;     index index.html;          # 添加正确的MIME类型映射     include /etc/nginx/mime.types;     types {         application/javascript js mjs ts;         application/json json;         text/css css;     }          # JavaScript文件特殊处理     location ~* .(js|mjs|ts)$ {         add_header Content-Type application/javascript;         try_files $uri =404;     }          # 静态资源缓存     location ~* .(css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {         expires 1y;         add_header Cache-Control "public, max-age=31536000, immutable";     }          # 启用Gzip压缩     gzip on;     gzip_vary on;     gzip_proxied any;     gzip_comp_level 6;     gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;          # 前端路由支持     location / {         try_files $uri $uri/ /index.html;     }          # 后端API代理     location /api {         proxy_pass http://127.0.0.1:5001;         proxy_http_version 1.1;         proxy_set_header Upgrade $http_upgrade;         proxy_set_header Connection 'upgrade';         proxy_set_header Host $host;         proxy_set_header X-Real-IP $remote_addr;         proxy_cache_bypass $http_upgrade;     }          # 安全相关头部     add_header X-Content-Type-Options nosniff;     add_header X-XSS-Protection "1; mode=block";     add_header X-Frame-Options SAMEORIGIN;     add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; } 

常见问题与解决方案

1. MIME类型配置问题

问题:部署后前端页面出现白屏,控制台报错:

Failed to load module script: Expected a JavaScript-or-Wasm module script but the server responded with a MIME type of "video/mp2t". 

解决方案

在Nginx配置中添加正确的MIME类型映射:(这个最坑)

types {     application/javascript js mjs ts;     application/json json;     text/css css; }  location ~* .(js|mjs|ts)$ {     add_header Content-Type application/javascript;     try_files $uri =404; } 

上面是方法一
如果不行用方法二这个我搞了2个小时
然后发现是
记一次开发

2. 跨域资源共享(CORS)问题

问题:开发环境正常,生产环境API请求失败,控制台报CORS错误。

解决方案

在后端添加CORS中间件:

import cors from 'cors';  // 配置CORS app.use(cors({   origin: process.env.NODE_ENV === 'production'      ? 'https://jishunkeji.cn'      : 'http://localhost:3000',   methods: ['GET', 'POST', 'PUT', 'DELETE'],   allowedHeaders: ['Content-Type', 'Authorization'] })); 

3. 文件上传权限问题

问题:在生产环境中,文件上传失败,服务器日志显示权限错误。

解决方案

  1. 检查并修正上传目录的权限:
# 设置正确的目录权限 chown -R www:www /www/wwwroot/jishunkeji.cn/jishun-website-backend/uploads chmod -R 755 /www/wwwroot/jishunkeji.cn/jishun-website-backend/uploads 
  1. 在Node.js应用中确保上传目录存在:
import fs from 'fs'; import path from 'path';  const uploadDir = path.join(__dirname, '../uploads');  // 确保上传目录存在 if (!fs.existsSync(uploadDir)) {   fs.mkdirSync(uploadDir, { recursive: true }); } 

4. PM2启动问题

问题:PM2启动失败,提示"未检测到pm2,请先安装pm2或切换node版本后再试"。

解决方案

  1. 确保全局安装PM2:
npm install -g pm2 
  1. 如果使用NVM管理Node.js版本,确保在正确的Node.js版本下安装PM2:
    !!!:新版本没有pm2,pm2被默认到了建站的地方
nvm use 16 npm install -g pm2 
  1. 在宝塔面板中,确保选择了正确的Node.js版本。

维护与更新

自动化部署流程

创建简单的部署脚本:

#!/bin/bash # deploy.sh  # 前端部署 cd /www/wwwroot/jishunkeji.cn/jishun-website git pull npm install npm run build  # 后端部署 cd /www/wwwroot/jishunkeji.cn/jishun-website-backend git pull npm install npm run build pm2 reload jishun-backend 

监控与日志

  1. 使用PM2监控后端服务
# 查看应用状态 pm2 status  # 查看日志 pm2 logs jishun-backend  # 监控资源使用 pm2 monit 
  1. Nginx日志分析
# 安装GoAccess日志分析工具 yum install goaccess  # 分析Nginx访问日志 goaccess /var/log/nginx/access.log -o /www/wwwroot/report.html --log-format=COMBINED 

数据备份策略

创建MongoDB备份脚本:

#!/bin/bash # backup.sh  # 设置变量 BACKUP_DIR="/www/backup/mongodb" DATE=$(date +%Y%m%d_%H%M%S) DB_NAME="jishun_website"  # 创建备份目录 mkdir -p $BACKUP_DIR  # 执行备份 mongodump --db $DB_NAME --out $BACKUP_DIR/$DATE  # 压缩备份 cd $BACKUP_DIR tar -zcvf $DATE.tar.gz $DATE rm -rf $DATE  # 删除7天前的备份 find $BACKUP_DIR -name "*.tar.gz" -type f -mtime +7 -delete 

添加到crontab定时执行:

# 每天凌晨3点执行备份 0 3 * * * /path/to/backup.sh >> /var/log/mongodb_backup.log 2>&1 

本文档详细记录了企业网站的开发与部署流程,从技术选型到最终上线。通过合理的架构设计、性能优化和部署配置,我成功构建了一个高性能、易维护的企业官网系统,希望能给各位开发者帮助吧
那么下面请欣赏我的报错
记一次开发
记一次开发
记一次开发
记一次开发
记一次开发

感谢浏览,能否给我一个赞和评论呢让我们共同进步

发表评论

评论已关闭。

相关文章