从零构建Vue3全栈工程化体系:脚手架、CI/CD与Docker部署实战
1. 项目缘起为什么我们需要一个“猴子代码”在团队里待久了你肯定遇到过这种场景新项目启动大家摩拳擦掌准备大干一场结果光是搭建开发环境、配置构建工具、搞CI/CD流水线就花了两三天。每个人电脑上的Node版本、包管理器、IDE插件都不一样张三能跑起来的项目李四一拉下来就报错。好不容易本地跑通了要部署上线又是一堆麻烦手动构建、手动上传、手动重启服务一个不小心就出线上事故。这就是典型的前端工程化问题。它不是一个技术难题而是一个效率和组织难题。我们需要的不是某个高深莫测的框架而是一套能快速复制、稳定运行、并且团队里每个人都能无脑上手的“标准答案”。这就是我折腾“MonkeyCode”的初衷。你可以把它理解为一个高度定制化的前端项目脚手架但它又不止于脚手架。它更像一个“工程化解决方案包”里面打包了从项目初始化、开发规范、构建优化到自动化测试、容器化部署、CI/CD流水线的一整套东西。目标很简单用一套命令生成一个“开箱即用”的现代化Vue 3项目并且连上云端的生产线实现代码提交即部署。为什么叫“MonkeyCode”一方面它想做到像猴子一样“简单复制”就能用降低心智负担另一方面也是提醒自己工具是为人服务的别把工程化搞成束缚创造力的枷锁。接下来我就把这套从零到一的完整过程拆开给你看。2. 核心蓝图MonkeyCode工程化体系包含什么在动手写第一行代码之前我们必须想清楚这个“解决方案包”到底要解决哪些具体问题。一个完整的前端工程化体系远不止vue create那么简单。我把它分成了四个层次像搭积木一样层层递进。2.1 第一层项目脚手架与开发规范这是地基。我们需要一个能快速生成项目骨架的命令行工具。这个骨架必须包含技术栈锁定Vue 3 TypeScript Vite 作为基础。这是当前性能和开发体验的黄金组合。Vite的闪电般的热更新和构建速度是开发效率的保障。目录结构规范不是随意的src下面放所有东西。我会预设好components,views,utils,apis,stores,types等目录并且约定它们的职责。比如components下再分common全局通用和business业务相关避免后期组件混乱。代码规范与质量门禁集成ESLint语法检查、Prettier代码格式化、Stylelint样式检查。关键一步是在package.json的scripts里加入lint: eslint . --ext .vue,.js,.ts,.jsx,.tsx --fix和format: prettier --write .。更狠的是配置Git Hooks通过husky和lint-staged在git commit时自动检查暂存区的文件不合格的代码根本提交不上去。基础工具封装封装好常用的工具函数如请求拦截、日期格式化、Vue组合式函数如usePagination分页逻辑和全局组件如Loading、Message。这些是避免重复造轮子的关键。2.2 第二层构建优化与产物管理本地开发爽了还得保证线上用户访问也快。这里涉及构建过程的深度定制。环境变量管理区分development、test、production等多环境。使用dotenv和Vite的环境变量模式.env.development,.env.production将API地址、密钥等敏感信息与代码分离。构建策略优化依赖分包ManualChunks将vue、vue-router、pinia、element-plus等几乎不变的第三方库单独打包成一个vendor块利用浏览器缓存。按需引入对于Element Plus这类大型UI库必须配置自动按需引入避免全量导入导致包体积膨胀。资源压缩与Hash配置vite的build选项启用terser代码压缩为静态资源JS、CSS、图片添加内容Hash便于长期缓存和增量更新。产物分析集成rollup-plugin-visualizer构建后会生成一个可视化的依赖分析图一个HTML文件让你一眼看出是哪个依赖让你的包变“胖”了从而有针对性地优化。2.3 第三层自动化测试与质量保障没有测试的代码就像没有刹车的汽车。对于MonkeyCode我要求生成的模板必须内置测试能力。单元测试Unit Test使用Vitest与Vite生态完美契合速度快替代Jest。为工具函数、组合式函数、纯逻辑组件编写单元测试。配置好测试覆盖率报告--coverage。组件测试Component Test使用vue/test-utils来测试Vue组件。模拟用户交互断言组件渲染输出和状态变化。端到端测试E2E Test使用Cypress或Playwright。虽然重但对于核心用户流程如登录、下单是必要的保障。在CI/CD流水线中可以在部署前自动运行E2E测试。测试脚本集成在package.json中配置test:unit,test:e2e等脚本并确保它们能在CI环境中无头headless运行。2.4 第四层CI/CD与自动化部署这是将代码变动自动、安全、可靠地交付到用户手中的最后一步也是工程化的终极体现。我选择的是GitLab CI/CD Docker 云服务器的组合这也是目前中小团队最实用、成本可控的方案。GitLab CI/CD利用GitLab自带的CI/CD功能通过在项目根目录创建.gitlab-ci.yml文件来定义流水线。它比Jenkins更轻量与代码仓库集成度更高。Docker容器化将应用和其运行环境Node版本、Nginx配置等一起打包成Docker镜像。这解决了“在我机器上能跑”的经典问题保证了环境一致性。部署流程代码推送到特定分支如main - 触发CI流水线 - 运行测试 - 构建Docker镜像 - 将镜像推送到私有仓库如阿里云容器镜像服务 - 在服务器上拉取新镜像并重启容器。这四层构成了MonkeyCode的完整闭环。下面我们就从第一层开始一步步实现它。3. 从零手搓MonkeyCode CLI与Vue3项目模板我们不依赖vue-cli而是自己创建一个更灵活的CLI工具来生成项目。这能让我们对模板有百分百的控制权。3.1 创建MonkeyCode CLI工具首先我们新建一个目录monkeycode-cli。mkdir monkeycode-cli cd monkeycode-cli npm init -y编辑package.json设置入口文件并添加必要的依赖。我们不需要复杂的命令行框架用Node.js原生fs、path模块和inquirer用于交互式提问就够了。{ name: monkeycode-cli, version: 1.0.0, description: A CLI to generate Vue3 project with full engineering stack., main: index.js, bin: { monkeycode: ./bin/monkeycode.js }, scripts: {}, dependencies: { inquirer: ^9.0.0, chalk: ^4.1.2, // 用于终端彩色输出 fs-extra: ^11.1.0 // 增强的文件操作比原生fs更好用 } }创建bin/monkeycode.js文件并在开头加上Shebang让它成为可执行脚本。#!/usr/bin/env node console.log(Welcome to MonkeyCode!); // ... 后续逻辑通过npm link命令将这个包链接到全局这样我们就可以在任意地方使用monkeycode命令了。npm link3.2 设计交互式模板生成逻辑CLI的核心逻辑是询问用户几个关键选项 - 根据选项选择模板 - 将模板文件复制到目标目录 - 动态替换模板中的变量如项目名。 我们在index.js中实现const inquirer require(inquirer); const fs require(fs-extra); const path require(path); const chalk require(chalk); const templates { vue3-standard: { url: https://github.com/your-org/vue3-standard-template.git, // 你的模板仓库地址 description: Vue 3 TypeScript Vite Pinia Element Plus }, // 未来可以扩展更多模板如 vue3-mobile, react-standard }; async function createProject() { const answers await inquirer.prompt([ { type: input, name: projectName, message: 请输入项目名称, default: my-vue3-app }, { type: list, name: template, message: 请选择项目模板, choices: Object.keys(templates).map(key ({ name: ${key} (${templates[key].description}), value: key })) }, { type: confirm, name: useE2E, message: 是否需要集成E2E测试 (Cypress)?, default: false } ]); const targetDir path.join(process.cwd(), answers.projectName); if (fs.existsSync(targetDir)) { console.log(chalk.red(目录 ${targetDir} 已存在)); process.exit(1); } console.log(chalk.blue(正在创建项目 ${answers.projectName}...)); // 这里简化处理实际应该去拉取远程模板或复制本地模板 // 我们假设有一个本地的模板目录 templates/vue3-standard const templateDir path.join(__dirname, templates, answers.template); await fs.copy(templateDir, targetDir); // 动态更新 package.json 中的项目名 const pkgPath path.join(targetDir, package.json); const pkg require(pkgPath); pkg.name answers.projectName; await fs.writeJson(pkgPath, pkg, { spaces: 2 }); console.log(chalk.green(项目创建成功)); console.log(chalk.yellow(cd ${answers.projectName})); console.log(chalk.yellow(npm install)); console.log(chalk.yellow(npm run dev)); } createProject().catch(console.error);这个CLI工具虽然简单但已经具备了核心的交互和模板复制功能。真正的精髓在于我们准备好的那个templates/vue3-standard模板。3.3 构建“开箱即用”的Vue3项目模板现在我们来精心打造这个模板。在monkeycode-cli/templates/vue3-standard目录下创建一个标准的Vue3项目。# 在模板目录下 npm create vuelatest . -- --typescript --router --pinia --eslint --prettier根据提示选择Yes或No我们这里已经预设了要TS、Router、Pinia、ESLint和Prettier。接下来是深度定制1. 规范目录结构src/ ├── apis/ # 所有API请求封装按模块划分 ├── assets/ # 静态资源 ├── components/ # 组件 │ ├── common/ # 全局通用组件 (如Button, Modal) │ └── business/ # 业务相关组件 ├── composables/ # Vue组合式函数 ├── layouts/ # 布局组件 ├── router/ # 路由配置 ├── stores/ # Pinia状态管理按模块划分 ├── styles/ # 全局样式、变量 ├── types/ # TypeScript类型定义 ├── utils/ # 工具函数 ├── views/ # 页面视图组件 ├── App.vue └── main.ts2. 配置ESLint Prettier Husky在生成的.eslintrc.cjs和.prettierrc基础上我们可以根据团队习惯调整规则。比如强制使用变量命名必须使用camelCase等。 安装husky和lint-stagednpm install husky lint-staged -D npx husky init编辑package.json添加lint-staged配置lint-staged: { *.{js,ts,vue}: [ eslint --fix, prettier --write ] }在.husky/pre-commit钩子文件中写入#!/usr/bin/env sh . $(dirname -- $0)/_/husky.sh npx lint-staged这样每次提交前都会自动检查和修复代码格式问题。3. 集成Element Plus并配置按需导入npm install element-plus element-plus/icons-vue npm install -D unplugin-vue-components unplugin-auto-import修改vite.config.tsimport AutoImport from unplugin-auto-import/vite import Components from unplugin-vue-components/vite import { ElementPlusResolver } from unplugin-vue-components/resolvers export default defineConfig({ plugins: [ // ... AutoImport({ resolvers: [ElementPlusResolver()], }), Components({ resolvers: [ElementPlusResolver()], }), ], })现在你可以在组件中直接使用ElButton而无需手动import插件会自动处理。4. 封装Axios请求实例在src/utils/request.ts中创建一个配置了拦截器、基础URL、超时时间的Axios实例并导出。在src/apis/user.ts等文件中引入这个实例定义具体的API函数。这样做的好处是统一错误处理、请求/响应拦截如添加token、处理通用错误码。至此一个具备基础工程化能力的Vue3项目模板就准备好了。CLI工具会将它复制到用户指定的目录。接下来我们要为这个项目注入灵魂——自动化部署流水线。4. 构建CI/CD流水线GitLab CI Docker实战本地开发环境再完美代码上不了线也是白搭。我们采用GitLab CI/CD因为它与Git仓库无缝集成配置简单直观。4.1 编写.gitlab-ci.yml文件在项目模板的根目录我们预先创建好.gitlab-ci.yml文件。这个文件定义了整个CI/CD的流程Pipeline它由多个按顺序或并行执行的“作业Job”组成。# .gitlab-ci.yml stages: - test # 测试阶段 - build # 构建阶段 - deploy # 部署阶段 variables: DOCKER_IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA # 定义镜像标签使用提交哈希 # 缓存 node_modules加速后续作业 cache: key: ${CI_COMMIT_REF_SLUG} paths: - node_modules/ # 1. 单元测试作业 unit-test: stage: test image: node:18-alpine # 使用Node官方镜像作为运行环境 script: - npm ci --cache .npm --prefer-offline # 使用ci命令它比install更严格适合CI环境 - npm run test:unit -- --coverage # 运行单元测试并生成覆盖率报告 artifacts: when: always paths: - coverage/ # 将测试覆盖率报告保存为产物可在GitLab界面查看 reports: junit: - junit.xml # 如果有JUnit格式的测试报告可以在这里配置 only: - merge_requests # 仅在合并请求时运行加快主分支推送速度 - main # 2. 构建作业 build: stage: build image: node:18-alpine script: - npm ci --cache .npm --prefer-offline - npm run build # 执行Vite构建生成dist目录 artifacts: paths: - dist/ # 将构建产物保存后续部署作业可以使用 only: - main # 仅当代码推送到main分支时触发构建 # 3. 构建并推送Docker镜像 docker-build-push: stage: build image: docker:20.10.16 # 使用Docker in Docker (dind)环境 services: - docker:20.10.16-dind # 启动一个Docker守护进程服务 variables: DOCKER_HOST: tcp://docker:2375 DOCKER_TLS_CERTDIR: script: - echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY # 登录到容器镜像仓库 - docker build -t $DOCKER_IMAGE_TAG . # 根据项目根目录的Dockerfile构建镜像 - docker push $DOCKER_IMAGE_TAG # 推送镜像到仓库 dependencies: - build # 声明依赖build作业确保先有构建产物 only: - main # 4. 部署到服务器 deploy-to-server: stage: deploy image: alpine:latest before_script: - apk add --no-cache openssh-client # 安装ssh客户端 - eval $(ssh-agent -s) - echo $SSH_PRIVATE_KEY | ssh-add - # 添加存储在GitLab CI变量中的SSH私钥 - mkdir -p ~/.ssh - chmod 700 ~/.ssh script: # 通过SSH连接到部署服务器执行部署脚本 - ssh -o StrictHostKeyCheckingno $SERVER_USER$SERVER_IP cd /path/to/deploy/script ./deploy.sh $DOCKER_IMAGE_TAG only: - main when: manual # 设置为手动触发点击按钮才部署增加安全性这个配置文件定义了一个清晰的流水线代码推送到main分支后先运行单元测试如果是合并请求也会触发然后并行进行项目构建和Docker镜像的构建与推送最后等待手动点击触发部署到服务器。注意这里有几个关键的安全和配置点$CI_REGISTRY_USER,$CI_REGISTRY_PASSWORD,$CI_REGISTRY,$SSH_PRIVATE_KEY,$SERVER_USER,$SERVER_IP这些都是GitLab CI/CD Variables。你需要在GitLab项目的Settings - CI/CD - Variables中设置。绝对不要将这些敏感信息硬编码在YAML文件或代码里。docker-build-push作业使用了services: - dind这需要在GitLab Runner的配置中启用Docker in Docker特权模式。对于共享RunnerGitLab.com通常已配置好对于自托管Runner需要在config.toml中配置privileged true。deploy-to-server设置为when: manual手动触发这是一个重要的安全实践防止每一次提交都自动上线。你可以在GitLab Pipeline界面看到一个“播放”按钮点击它才会执行部署。4.2 编写项目Dockerfile要让Docker镜像构建成功我们需要在项目根目录提供一个Dockerfile。这里采用多阶段构建以减小最终镜像体积。# Dockerfile # 第一阶段构建阶段 FROM node:18-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci --onlyproduction --cache .npm --prefer-offline COPY . . RUN npm run build # 第二阶段运行阶段 FROM nginx:alpine # 将构建产物从builder阶段复制到Nginx的默认静态文件目录 COPY --frombuilder /app/dist /usr/share/nginx/html # 如果需要自定义Nginx配置可以复制进去 # COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD [nginx, -g, daemon off;]这个Dockerfile非常高效。第一阶段用Node环境安装依赖并构建生成dist目录。第二阶段使用轻量级的nginx:alpine镜像只把第一阶段的构建产物复制过来抛弃了Node环境的所有中间文件和源码使得最终镜像非常小巧通常只有几十MB。4.3 编写服务器端部署脚本最后我们需要在部署服务器上准备一个简单的Shell脚本deploy.shGitLab CI会通过SSH远程执行它。#!/bin/bash # deploy.sh set -e # 遇到错误立即退出 IMAGE_TAG$1 # 从CI传入的镜像标签如 registry.example.com/group/project:abc1234 echo 开始部署镜像标签: $IMAGE_TAG # 1. 拉取最新的镜像 docker pull $IMAGE_TAG # 2. 停止并删除旧容器如果存在 CONTAINER_NAMEmy-vue3-app if [ $(docker ps -aq -f name$CONTAINER_NAME) ]; then echo 停止并移除旧容器... docker stop $CONTAINER_NAME docker rm $CONTAINER_NAME fi # 3. 运行新容器 echo 启动新容器... docker run -d \ --name $CONTAINER_NAME \ --restart always \ # 设置容器总是重启应对服务器重启等情况 -p 8080:80 \ # 将宿主机的8080端口映射到容器的80端口 $IMAGE_TAG # 4. 清理旧的、无用的Docker镜像释放磁盘空间 echo 清理无用镜像... docker image prune -f echo 部署完成将这个脚本放在服务器的某个路径下如/path/to/deploy/script/并赋予执行权限chmod x deploy.sh。当GitLab CI的deploy-to-server作业触发时就会通过SSH执行这个脚本完成服务的更新。5. 避坑实录那些我踩过的“猴子坑”工程化的道路从来不是一帆风顺的。下面分享几个我在搭建这套体系时遇到的典型问题和解决方案希望能帮你省下几个小时甚至几天的调试时间。5.1 GitLab Runner 注册与配置的“网络迷踪”问题在自托管服务器上安装GitLab Runner后执行作业总是卡在Preparing environment或直接失败。 排查过程检查Runner状态sudo gitlab-runner status确保服务是running的。查看Runner日志sudo gitlab-runner run在前台运行观察实时日志。发现错误信息是dial tcp x.x.x.x:443: i/o timeout。网络分析Runner需要连接GitLab服务器通常是gitlab.com或你的私有地址来获取作业。服务器可能因为防火墙或网络策略无法访问外网。解决方案方案A如果有网络权限为服务器配置正确的HTTP/HTTPS代理。在Runner的配置文件通常是/etc/gitlab-runner/config.toml中找到对应的Runner添加environment [http_proxyhttp://your-proxy:port, https_proxyhttp://your-proxy:port]。方案B内网环境搭建一个内网GitLab实例并将Runner注册到内网地址。这是最稳定可靠的方案。方案C使用Shell Executor如果只是简单项目可以将Runner的执行器executor从docker改为shell。这样作业会在Runner机器本身的Shell中运行避免了Docker网络问题但牺牲了环境隔离性。心得CI/CD工具链的网络连通性是第一步也是最容易出问题的一步。务必先确保Runner能稳定“回连”到GitLab。5.2 Docker镜像构建缓慢与体积膨胀问题每次构建Docker镜像都要好几分钟而且镜像越来越大。 根因分析没有合理利用Docker缓存Dockerfile的每一条指令都会生成一个镜像层。如果COPY . .放在了npm install之前那么任何源码文件的改动都会导致缓存失效需要重新安装所有node_modules。构建上下文Build Context过大docker build .命令中的.会把当前目录所有文件包括node_modules,.git, 日志文件等发送给Docker守护进程如果文件很多很大传输和准备就会很慢。最终镜像包含了构建工具如果你用FROM node:18作为最终运行镜像它会包含完整的Node和npm而运行一个Vue编译后的静态站点根本不需要这些。优化方案即我们上面采用的方案调整Dockerfile指令顺序把变化频率低的指令放前面如COPY package*.json ./和RUN npm ci变化频率高的指令放后面如COPY . .。使用.dockerignore文件在项目根目录创建.dockerignore忽略不需要的文件。node_modules .git .DS_Store *.log .env.local dist采用多阶段构建正如我们之前的Dockerfile所示第一阶段用完整Node环境构建第二阶段只复制构建产物到轻量级Nginx镜像中最终镜像体积可能只有原来的1/10。5.3 CI/CD流水线中的环境变量“捉迷藏”问题在GitLab CI作业中npm run build能成功但构建出的页面访问API时却指向了localhost。 排查过程检查代码发现API地址是通过import.meta.env.VITE_API_BASE_URL获取的。在本地.env.production文件中明明配置了VITE_API_BASE_URLhttps://api.myapp.com。查看GitLab CI构建日志发现构建命令是直接npm run build没有指定模式。根本原因Vite或类似的构建工具在构建时会从当前Shell进程的环境变量和.env文件中读取VITE_开头的变量。在CI环境中.env.production文件可能不存在或者Shell中没有设置这些变量。解决方案 在.gitlab-ci.yml的build作业中通过variables关键字注入环境变量。build: stage: build variables: VITE_API_BASE_URL: https://api.myapp.com # 在这里定义 # 或者更安全地从GitLab CI变量中读取 # VITE_API_BASE_URL: $PRODUCTION_API_URL script: - npm run build这样在CI执行npm run build时VITE_API_BASE_URL就被正确设置了。重要提醒永远不要在代码仓库中提交包含真实密码、密钥的.env文件。.env.example可以提交用于说明需要哪些变量。真实的生产环境变量只应通过CI/CD平台的安全变量功能或服务器环境来设置。6. 进阶与扩展让猴子代码更“智能”基础流程跑通后我们可以考虑一些进阶优化让整个体系更健壮、更高效。6.1 基于Git Tag或分支的差异化部署目前的流水线只监听main分支。在实际开发中我们可能有develop开发环境、staging预发布环境、main生产环境等多个分支。 可以在.gitlab-ci.yml中利用rules关键字进行更精细的控制docker-build-push: # ... 其他配置 rules: - if: $CI_COMMIT_BRANCH main variables: ENVIRONMENT: production DOCKER_IMAGE_TAG: $CI_REGISTRY_IMAGE:prod-$CI_COMMIT_SHORT_SHA - if: $CI_COMMIT_BRANCH staging variables: ENVIRONMENT: staging DOCKER_IMAGE_TAG: $CI_REGISTRY_IMAGE:staging-$CI_COMMIT_SHORT_SHA when: manual # 预发布环境可以手动触发构建然后在构建脚本或Dockerfile中根据ENVIRONMENT变量使用不同的环境配置文件如.env.staging。6.2 集成代码扫描与安全检测除了单元测试可以在CI中集成静态代码安全扫描SAST和依赖漏洞扫描。使用npm audit在package.json的脚本中添加audit: npm audit --audit-levelhigh并在CI的test阶段执行它。如果发现高危漏洞可以让流水线失败。集成SonarQube对于更全面的代码质量分析可以配置一个sonar作业在构建后运行SonarScanner将分析结果上传到SonarQube服务器从复杂度、重复率、代码坏味道、安全漏洞等多个维度评估代码质量。6.3 部署回滚与监控自动化部署了也要能快速回滚。一个简单的回滚策略是在服务器上每次部署新镜像前给当前正在运行的容器打一个标签例如my-app:previous。如果新版本出现问题只需执行一个回滚脚本将服务重新指向my-app:previous镜像即可。更成熟的做法是使用Kubernetes的滚动更新和版本管理功能可以做到无缝升级和秒级回滚。监控方面可以在Docker容器中集成应用性能监控APM工具的Agent或者在服务器层面使用PrometheusGrafana来监控容器资源使用情况、应用接口健康状态等。折腾完这一整套从输入monkeycode create my-project到代码提交后自动部署上线整个过程已经高度自动化。它可能不是最完美的方案但绝对是一个能立刻用起来、并且能随着团队成长而不断演进的坚实起点。工程化的最终目的不是炫技而是让开发者能更专注于创造业务价值而不是在环境配置和部署琐事上浪费时间。希望这套“猴子代码”的思路能给你带来一些切实的启发。