我写了十多年 Go 后端。CCS(内容创作系统)这个项目,前端是用 AI 生成的,我能看懂大概,但始终有种”隔了一层”的感觉。相信很多后端同行都有类似体会:会写 API,会配 Nginx,但前端从写代码到浏览器里跑起来,中间到底发生了什么,总有点模糊。
这篇文章以 CCS 的真实代码为例,把前端这件事一次讲透。不讲 Vue 语法,只讲流程和架构——后端工程师最关心的那些事。
一、整体流程:从浏览器输入密码到进入系统
以 CCS 的登录为例,用一张图概括整个过程:
1 | 浏览器 Nginx Go后端 |
逐步拆解。
1.1 浏览器请求页面
你在浏览器输入 ccs.kentonly.com,浏览器发出 GET / 请求。这个请求先到达 Nginx。
Nginx 配置里有这么一行:
1 | location / { |
try_files 的意思是:先找文件 $uri,找不到就找目录 $uri/,都找不到就返回 index.html。对于 GET /,直接命中 index.html。
这个 index.html 就是前端打包后的入口文件,长这样:
1 |
|
注意那个 <div id="app"></div>——空的。Vue 应用会挂载到这个 div 上,把整个页面”塞”进去。这跟后端模板渲染(Go template、JSP)完全不同:HTML 只是个空壳,真正的页面内容由 JavaScript 生成。
1.2 浏览器加载 JS
浏览器看到 <script src="/assets/index-DYmUXCPZ.js">,再发一个请求拿这个 JS 文件。这个文件就是前端所有代码打包后的产物——Vue 框架、业务代码、Element Plus 组件库,全在里面。
JS 下载完毕后,浏览器执行它。入口是 main.js(已被打包进那个大 JS 文件):
1 | import { createApp } from 'vue' |
app.mount('#app') 执行后,Vue 接管了那个空 div,开始根据当前 URL 渲染页面。
1.3 路由守卫:判断是否已登录
Vue Router 配置了全局守卫:
1 | // router/index.js |
你第一次访问,localStorage 里没有 token,所以被重定向到 /login,渲染登录组件。
1.4 登录页:用户输入并提交
登录页 Login.vue 的核心逻辑:
1 | <template> |
v-model="form.username" 是 Vue 的双向绑定:输入框里打字,form.username 自动更新。点登录时调用 auth.login()。
1.5 API 调用链:从组件到 HTTP
auth.login() 的调用链,是理解前端架构的关键。一层一层看:
第一层:Pinia Store(状态管理)——stores/auth.js
1 | async function login(username, password) { |
Store 负责”状态”——登录后 token 存哪、怎么取、怎么清。它不直接发 HTTP 请求,而是调用 API 层。
第二层:API 封装——api/auth.js
1 | import http from './index' |
API 层负责定义”调哪个接口”。注意 URL 是 /auth/login,不是完整路径——因为 http 实例配了 baseURL: '/api/v1',最终请求路径是 /api/v1/auth/login。
第三层:Axios 实例——api/index.js
1 | const http = axios.create({ baseURL: '/api/v1', timeout: 30000 }) |
Axios 实例是所有 HTTP 请求的统一出口。拦截器做了两件事:
- 请求拦截:自动从 localStorage 取 token 塞进 Header(后端工程师可以理解为中间件)
- 响应拦截:统一拆包,业务代码拿到的直接是
data字段,不用每次写res.data.data
1.6 后端处理:Go Gin
请求到达后端,走的是熟悉的 Go 流程:
1 | // handler/auth.go |
后端返回的 JSON 格式是统一的:
1 | { |
前端响应拦截器会检查 code:0 表示成功,非 0 弹错误提示。成功的 data 字段会被剥离出来,返回给 Store。
1.7 登录完成:token 存储 + 页面跳转
Store 拿到 token 后:
1 | function setAuth(accessToken, userInfo) { |
localStorage 是浏览器的本地存储,关页面也不会丢。之后每次 API 请求,Axios 拦截器都会从 localStorage 取 token 放到 Header 里。
然后 router.push('/') 触发路由跳转。这次有 token 了,路由守卫放行,渲染 Layout 组件——侧边栏 + 内容区,CCS 系统主界面出来了。
二、Nginx 的作用
后端工程师对 Nginx 不陌生,但前后端分离场景下,Nginx 的角色有点变化。
2.1 静态文件服务器
传统后端渲染,Nginx 主要做反向代理,把请求转给后端。前后端分离后,Nginx 还多了一个角色:静态文件服务器。
前端打包后就是一堆静态文件(HTML、JS、CSS、图片),Nginx 直接把这些文件返回给浏览器,不需要经过后端。
2.2 反向代理
API 请求需要转发给后端。看 CCS 的 Nginx 配置:
1 | server { |
两个 location,职责分明:
| 请求路径 | 匹配规则 | 去向 |
|---|---|---|
/、/assets/xxx.js |
location / |
Nginx 直接返回静态文件 |
/api/v1/auth/login |
location /api |
转发到 Go 后端 :8082 |
2.3 为什么不能前端直接连后端
前端代码里 Axios 的 baseURL 是 /api/v1——相对路径,没有域名和端口。
如果浏览器从 ccs.kentonly.com 加载页面,Ajax 请求也发到 ccs.kentonly.com/api/v1/...。如果没有 Nginx,这个请求没人处理。
有了 Nginx,请求到 80 端口,Nginx 根据 /api 前缀转发到后端。浏览器始终只跟一个域名通信,不存在跨域问题。
开发模式下,Vite 的
proxy配置做同样的事:把/api转发到localhost:8080。
2.4 SPA 路由的兜底
1 | location / { |
最后那个 /index.html 很关键。Vue Router 用的是 History 模式,URL 长这样:ccs.kentonly.com/contents/create。
如果用户刷新页面,浏览器向 Nginx 请求 /contents/create,但服务器上没有这个文件——这是一个前端路由,不是物理文件。try_files 找不到文件就返回 index.html,让 Vue Router 自己在前端处理路由。
三、前端代码如何到达浏览器、如何跟后端通信
3.1 代码到达浏览器的过程
1 | 源码 (.vue/.js) → Vite打包 → dist/ → 复制到Nginx → 浏览器下载 |
开发时和上线后,代码到达浏览器的方式不同:
开发模式(npm run dev):
Vite 启动一个开发服务器(端口 80),你访问 localhost,Vite 实时编译 .vue 文件,按需把模块转成浏览器能执行的 JS。改代码自动刷新(HMR)。这个阶段代码不落盘,全在内存里。
生产模式(npm run build):
Vite 把所有 .vue、.js、.css 编译、压缩、打包成静态文件,输出到 dist/ 目录。然后把这些文件复制到 Nginx 的网站目录,Nginx 直接当静态文件返回。
3.2 前后端通信:HTTP + JSON
前端和后端之间唯一的通信方式就是 HTTP 请求 + JSON 响应。没有魔法。
以获取内容列表为例:
1 | 前端 后端 |
跟 Postman 调接口一模一样,只是这个过程被封装在 Axios 里,对前端开发者透明。
3.3 开发时的跨域问题
开发时前端在 localhost:80,后端在 localhost:8080,端口不同,浏览器会拦截请求(同源策略)。
Vite 的 proxy 解决这个问题:
1 | // vite.config.js |
前端请求 localhost/api/v1/...,Vite 开发服务器拦截 /api 开头的请求,转发到 localhost:8080。跟 Nginx 的 proxy_pass 一个道理。
上线后不存在这个问题——Nginx 同时服务静态文件和代理 API,浏览器只跟 ccs.kentonly.com:80 通信。
四、前端编译打包流程:到底发布的是什么
这是后端工程师最困惑的部分。后端发布很简单:go build 产出一个二进制,扔到服务器跑起来。前端呢?
4.1 前端源码长什么样
CCS 前端的源码目录:
1 | frontend/ |
.vue 文件浏览器不认识,必须经过编译。
4.2 编译打包过程
1 | cd frontend |
npm run build 实际执行的是 vite build,它做了这些事:
- 编译 .vue 文件:把
<template>编译成 JS 渲染函数,把<style>提取成 CSS - 处理 ES模块:把
import/export转成浏览器能执行的代码 - Tree-shaking:去掉没用到的代码
- Code-splitting:按路由拆分,每个页面一个 JS 文件(懒加载)
- 压缩混淆:变量名缩短、去掉注释空白
- Hash命名:文件名加内容哈希,如
index-DYmUXCPZ.js
4.3 打包产物
打包后生成 dist/ 目录:
1 | dist/ |
对比一下打包前后的 index.html:
打包前(源码):
1 | <div id="app"></div> |
打包后(dist):
1 | <div id="app"></div> |
Vite 把 /src/main.js 替换成了打包后的 /assets/index-DYmUXCPZ.js,并自动加上了 CSS 链接。
4.4 后端发布 vs 前端发布
| 后端(Go) | 前端(Vue) | |
|---|---|---|
| 源码 | .go 文件 |
.vue / .js 文件 |
| 编译 | go build |
npm run build (vite build) |
| 产物 | 一个二进制文件 | dist/ 目录(HTML + JS + CSS) |
| 运行环境 | 服务器进程 | 浏览器 |
| 部署方式 | 二进制 + 配置文件 → 启动进程 | 静态文件 → Nginx 托管 |
| 更新方式 | 替换二进制,重启进程 | 替换 dist 目录,Nginx reload |
| 配置文件 | config.yaml |
编译时写死(环境变量通过构建参数注入) |
核心区别:后端发布是”部署一个运行的程序”,前端发布是”部署一堆静态文件”。前端没有进程,没有运行时,就是一堆被 Nginx 直接返回给浏览器的文件。
4.5 CCS 的完整发布流程
1 | # 后端:编译 + 部署 |
后端三步:编译 → 复制二进制 → 重启容器。
前端四步:安装依赖 → 编译打包 → 替换静态文件 → Nginx reload。
总结
用一张表把四个问题串起来:
| 问题 | 答案 |
|---|---|
| 整体流程 | 浏览器拿 HTML → 加载 JS → Vue 渲染页面 → Axios 调 API → Nginx 转发后端 → 后端返回 JSON → Vue 更新页面 |
| Nginx 的作用 | 静态文件服务器 + API 反向代理 + SPA 路由兜底 |
| 代码如何到达浏览器 | 源码经 Vite 编译打包成静态文件,Nginx 直接返回给浏览器 |
| 前端发布什么 | 发布 dist/ 目录——一堆被 Nginx 托管的 HTML/JS/CSS 静态文件 |
如果你也是后端工程师,理解了这些,再去看前端代码就不会觉得陌生了。Vue 组件的 <template> 就是 HTML,<script> 就是 JavaScript,Axios 就是你用 Postman 调接口的代码版,Nginx 配置你已经会了。
剩下的 Vue 语法、组件化、响应式——那些是术,不是道。道就是这一张请求流转图,这篇文章讲的这些。