Skip to content

中恒智造官网 — 面试问答

项目时间:2022年7月 - 2022年9月 | 已上线运营 技术栈:Nuxt2 + Vue2 + ElementUI + JSEncrypt + AOS + ECharts + vue-baidu-map + v-viewer + cheerio + nuxt-precompress 线上地址:http://www.goomaker.com

Q1: Nuxt2 SSR 静态站点生成

面试官可能问:"为什么选择 Nuxt2?说说静态站点生成是怎么实现的?"

回答

中恒智造官网是企业级 B2B 官网,展示产品、案例、公司信息。选 Nuxt2 有三个核心原因:

  1. SEO 需求:B2B 企业官网需要被搜索引擎收录,纯 SPA 的 JS 渲染对搜索引擎不友好。Nuxt2 的 SSR 模式在服务端渲染完整 HTML,搜索引擎爬虫能直接拿到带内容的页面。
  2. 静态站点部署:官网内容更新频率低,最终用了 target: 'static' + npm run generate 生成纯静态 HTML 文件,部署到 Nginx 的 html 目录下,不需要 Node.js 进程,运维成本极低。
  3. Vue2 生态兼容:项目时间是 2022 年,Element UI 等依赖仍以 Vue2 为主,Nuxt2 是当时最成熟的 Vue2 SSR 方案。

静态生成流程nuxt.config.js 设置 target: 'static'npm run generate → Nuxt 遍历所有路由(/, /product, /news, /aboutus 等)→ 每个路由生成一个 index.html → 输出到 dist/ 目录 → 部署到 Nginx。

路由别名处理:做了一个 extendRoutes 的配置,给每个路由加了 /index.html 别名:

javascript
router: {
  extendRoutes(routes) {
    routes.forEach((route) => {
      const alias = route.path.length > 1
        ? `${route.path}/index.html`
        : "/index.html";
      route.alias = alias;
    });
  },
}

这样 /product/index.html/product 都能访问,兼容一些老旧的 Nginx 配置对目录路径的处理。

追问:"为什么不用 Nuxt3?"

项目是 2022 年 7 月启动的,当时 Nuxt3 还在 RC 阶段,生态不成熟——Element Plus 的 Nuxt3 适配还没出、很多 Nuxt2 的 module 和 plugin 不能直接用。对生产项目来说,稳定性优先于追新。

Q2: 首屏性能优化(5.87s→1.16s)

面试官可能问:"首屏加载时间从 5.87s 降到 1.16s,怎么做到的?"

回答

性能优化是项目的核心亮点之一,做了四个层面的优化:

第一层:资源压缩——Gzip + Brotli 双格式预压缩。

nuxt-precompress 模块在 npm run generate 时自动为每个静态文件(JS/CSS/HTML/SVG)同时生成 .gz.br 两种压缩版本:

javascript
nuxtPrecompress: {
  gzip: { enabled: true, compressionOptions: { level: 9 } },
  brotli: { enabled: true, compressionOptions: { level: 11 } },
  test: /\.(js|css|html|txt|xml|svg)$/,
}

Nginx 端配置 gzip_static on,请求到达时如果存在预压缩文件就直接返回,不用实时压缩。浏览器支持 Brotli 就返回 .br(压缩率更高),不支持就降级 .gz

第二层:Webpack 代码分割 + Tree Shaking。

Nuxt2 基于 Webpack 4,在生产构建时自动做 code splitting——把 node_modules 的第三方库拆成单独的 chunk(如 vendors.app.js),业务代码按路由懒加载。通过 splitChunksmaxSize: 244000 控制 chunk 大小,避免单个文件过大阻塞加载。

第三层:图片资源优化。

产品图片从 PNG 格式替换为 WebP 格式,体积平均减少 60-70%。Banner 背景图做了响应式适配——大屏用高清图,小屏用压缩图。

第四层:生产环境 Console 清理。

babel-plugin-transform-remove-console 在生产构建时自动移除所有 console.log,减少不必要的 JS 执行和内存占用。

追问:"怎么验证首屏时间从 5.87s 降到 1.16s?"

用 Chrome DevTools 的 Lighthouse 和 Network 面板分别测试。优化前首页完整加载 5.87s,主要是大图未压缩、JS bundle 未分割、没有 Gzip 导致的。优化后 1.16s,核心指标提升明显。

Q3: SEO 优化与 Cheerio 冗余标签清理

面试官可能问:"Nuxt SSR 输出的 HTML 有什么问题?怎么用 Cheerio 清理的?"

回答

Nuxt2 的 SSR 渲染会在 HTML 标签上添加很多服务端渲染特有的属性,比如 data-n-head-ssrdata-n-headdata-hiddata-vue-ssr-iddata-server-rendered 等。这些属性对浏览器渲染没用,但会增加 HTML 体积、影响 SEO 评分、让人一看就知道是 Vue SSR。

nuxt.config.jshooks.generate.page 钩子里用 Cheerio 做了清理:

javascript
hooks: {
  generate: {
    page(page) {
      const cheerio = require("cheerio");
      const $ = cheerio.load(page.html, { decodeEntities: false });
      const attrs = [
        "data-n-head-ssr", "data-n-head", "data-hid",
        "data-vue-ssr-id", "data-server-rendered",
      ];
      attrs.forEach((value) => {
        $("*[" + value + "]").removeAttr(value);
      });
      page.html = $.html();
    },
  },
}

这个钩子在每个页面生成静态 HTML 后触发,Cheerio 用 jQuery 风格的 API 遍历 DOM 树,批量移除这些属性,然后写回 page.html。最终生成的 HTML 干净整洁,和手写静态页面一样。

追问:"Cheerio 在这里做了什么?和 Puppeteer 有什么区别?"

Cheerio 是 Node.js 端的 HTML 解析库,提供 jQuery 风格的 DOM 操作 API。它不执行 JS、不渲染 CSS,只是解析和修改 HTML 字符串——速度极快、内存占用低,非常适合批量处理静态 HTML。Puppeteer 会启动完整浏览器,在这个场景下太「重」了——几百个 HTML 页面用 Puppeteer 处理会非常慢。

SEO 其他方面:还配置了 Open Graph 标签(og:titleog:descriptionog:image),分享到微信/Facebook 时能正确显示标题、描述和缩略图。head() 里动态设置每个页面的 title,百度统计脚本注入,keywords meta 标签覆盖核心关键词。

Q4: RSA 非对称加密全链路

面试官可能问:"为什么在官网里用 RSA 加密?怎么用的?"

回答

官网有用户账户系统——注册、登录、密码重置等。为了保护用户的敏感信息(密码、手机号等)在传输过程中的安全,用 JSEncrypt 实现了 RSA 非对称加密,覆盖登录/注册/密码重置全链路。

核心技术选型

  • JSEncrypt:前端用 JSEncrypt 做常规加密,适合短文本
  • encryptlong:前端用 encryptlong 做分段加密,解决 RSA 单次加密长度限制问题(RSA-1024 最多加密 117 字节,超长文本需要分段加密后再拼接)

插件封装plugins/rsa.js):

javascript
Vue.prototype.$RSA = {
  rsaPublicData(data) {
    // 用 JSEncrypt + 公钥加密
  },
  encrypt(data) {
    // 用 encryptlong 分段加密(超长文本)
  },
  decrypt(data) {
    // 用 encryptlong + 私钥解密
  },
};

通过 Vue 插件挂载到原型上,所有组件通过 this.$RSA.encrypt(password) 直接调用。

使用场景

  • 登录:用户输入密码 → 前端 $RSA.encrypt(password) 加密 → POST 到后端 → 后端用私钥解密验证
  • 注册:密码、确认密码都经过 RSA 加密传输
  • 密码重置:新密码加密后提交

追问:"公钥写死在前端代码里安全吗?"

RSA 是非对称加密——公钥本来就是公开的,任何人都可以拿到。安全性在于只有后端的私钥才能解密。公钥暴露在前端不影响安全性。真正的风险是中间人攻击(MITM)——如果攻击者替换了公钥就能解密。生产环境最好配合 HTTPS + HSTS,确保传输层安全。

Q5: 用户账户体系与试用申请工作流

面试官可能问:"用户系统和试用申请怎么设计的?"

回答

官网有完整的用户账户体系,包括注册/登录/密码重置/实名认证,以及一个"申请试用"的业务工作流。

账户体系

  • 登录认证:JWT Token 机制——登录成功后后端返回 Token,前端存 localStorage,通过 Axios 拦截器自动在所有请求 Header 里带上 token 字段
  • Token 管理plugins/axios.js 的请求拦截器从 localStorage.getItem("loginToken") 读取 Token,自动注入到每个请求的 config.headers.token
  • 路由守卫middleware/account-authentication.jslogin-authentication.js 做页面级权限控制——未登录访问账户页自动跳转登录页

Axios 封装

javascript
const service = axios.create({ baseURL, timeout: 60 * 1000 });
// 请求拦截器:JSON 序列化 + Token 注入
service.interceptors.request.use((config) => {
  config.data = JSON.stringify(config.data);
  const token = localStorage.getItem("loginToken");
  if (token) config.headers.token = token;
  return config;
});
// 响应拦截器:统一错误处理(400/401/403/404/500 等)

试用申请工作流:用户在页面点击"申请试用"→ 弹出 applyForTrial 组件 → 填写企业名称、姓名、联系方式 → 表单校验(手机号正则 + 必填项)→ POST 到 /api/userInfo/commitUserInfo → 后端记录后销售人员跟进。支持提交成功/失败的差异化提示,提交成功后自动清空表单。

追问:"实名认证是怎么做的?"

components/account/realNameAuthentication.vue 里收集用户的真实姓名、身份证号等信息,提交到后端做验证。这是为 B2B 业务场景设计的——只有实名认证的企业用户才能正式申请试用,确保销售线索的真实性。

Q6: 五档响应式断点与自定义指令

面试官可能问:"响应式是怎么设计的?移动端有什么特殊处理?"

回答

官网做了五档响应式断点适配,覆盖从超宽屏到小手机的完整设备:

断点宽度范围适配策略
大屏>1920pxBanner 内容左侧留白 25%,视觉居中
桌面1280-1920px标准 5 列布局,导航栏完整展示
平板1000-1280px导航栏压缩间距,核心模块 flex 变 grid
大手机600-1000px导航栏隐藏→汉堡菜单,grid 缩为 2 列
小手机<600px单列布局,Banner 背景右对齐,Logo 简化

关键技术

  • CSS Media Query:用 SCSS 变量统一管理断点和间距,一个 _variables.scss 文件全局引入
  • JS 适配clientWidth 通过 resize 事件实时获取,用于计算 carouselHeight(响应式 Banner 高度)、组件切换(AOS 动画方向随屏幕宽度变化)
  • 移动端导航:<1000px 隐藏桌面导航栏,显示汉堡图标 → 点击滑入毛玻璃遮罩 Drawer,带动画延迟入场效果

自定义 v-ellipsis 指令plugins/ellipsisDirective.js):

javascript
Vue.directive("ellipsis", {
  inserted(el, bindings) {
    // 设置 CSS 文本溢出省略
    el.style.overflow = "hidden";
    el.style.textOverflow = "ellipsis";
    el.style.whiteSpace = "nowrap";
    // 检测是否溢出 → 绑定 hover 事件 → 显示 tooltip
    if (el.scrollWidth > el.offsetWidth) {
      el.addEventListener("mouseenter", handleMouseEnter);
      el.addEventListener("mouseleave", removeTooltip);
    }
  },
});

核心思路:先用 CSS 做文本截断 → 再判断是否真的溢出了(scrollWidth > offsetWidth)→ 溢出时才绑定 hover 事件显示 tooltip。没溢出就不绑事件,不创建 DOM,性能最优。

tooltip 的边界检测:根据鼠标位置判断显示在上方还是下方、左侧还是右侧,避免 tooltip 超出视口边界。

追问:"响应式为什么要拆五档而不是用 Bootstrap 的栅格系统?"

B2B 企业官网的设计稿是高度定制的——每个模块在不同屏幕下的布局差异很大,不是简单的 col-md-6 能解决的。比如首页的服务模块在桌面端是横排 4 列、平板是 2x2 网格、手机是 2 列,每种布局都需要独立设计。用原生 Media Query + CSS Grid 的组合更灵活。

Q7: 九大产品线动态路由与详情页

面试官可能问:"九个产品的详情页是怎么组织的?动态路由怎么做?"

回答

产品中心展示了 9 个产品,分三大类:智能装备(4 个)、智慧工厂(1 个)、ERP 定制产品(3 个)。产品列表页用 URL query 参数实现动态路由。

路由设计:产品详情页路由是 /product/details?type=1type 从 1 到 9 对应不同产品。

数据驱动details.vue 里用一个 product 对象映射所有产品信息:

javascript
product: {
  1: { title: "成衣业务管理系统", subTitle: "...", preTitle: "ERP定制产品" },
  2: { title: "面料生产管理系统", subTitle: "...", preTitle: "ERP定制产品" },
  // ... 共 9 个产品
}

computed 属性 currentTitle/currentSubTitle/currentPreTitle 根据 $route.query.type 动态取值,模板里用 v-if/v-else-if 加载对应的详情子组件。

子组件拆分:每个产品详情页是独立的 Vue 组件,放在 components/product/ 下——fineClothBussinessSystem.vuefabricProduceSystem.vuesteamSystem.vuebobbinMoistureMeter.vuesizingMoistureSystem.vue 等。每个组件内包含产品概述、技术参数、功能亮点等模块。底部统一带"预约现场演示"按钮,点击打开申请试用弹窗。

面包屑导航:ElementUI 的 el-breadcrumb 实现"首页 > 产品中心 > 当前产品名"的三级面包屑,支持点击跳转。

产品中心页导航:用滚动监听实现了吸顶快速导航栏——三个分类(智能装备/智慧工厂/ERP定制产品)锚点,滚动到对应区域时导航栏高亮当前分类,支持点击平滑滚动到对应区域。

追问:"为什么用 query 参数而不是动态路由 /product/:id?"

产品数量固定(9 个),用 query 参数更简单——不用在 nuxt.config.js 里配 generate.routes 动态生成。而且 B2B 官网的产品详情页本身就有被 SEO 收录的需求,用静态生成的方式每个产品独立一个页面更友好。

Q8: 第三方集成与交互细节

面试官可能问:"集成了哪些第三方服务?有什么交互细节可以讲?"

回答

1. 百度地图(vue-baidu-map)

用在"联系我们"页,展示北京总部和广州分公司两个办公地点。插件注册在 plugins/baiduMaps.js,配置 ssr: false 避免服务端渲染时报错(百度地图依赖 window 对象)。地图上加了自定义标注点、信息窗口,支持路况图层切换。

2. 图片预览(v-viewer)

用在资质证书展示、微信公众号二维码放大等场景。通过 plugins/picViewer.js 注册为全局方法 this.$viewerApi({ images: [...] }),调用时传入图片数组即可弹出全屏预览轮播。

3. 百度统计 + 51la 统计

nuxt.config.jshead.script 直接注入百度统计脚本。51la 通过插件 plugins/51la.js 在客户端加载。

4. AOS 滚动动画

全局注册 aos.js 插件,所有模块的入场动画通过 data-aos 属性声明——fade-upzoom-in-downzoom-out-right 等,配合 data-aos-delaydata-aos-offset 控制动画时序。移动端部分动画方向做了适配(比如概念模块的第三个卡片在大屏用 zoom-out-left,小屏用 fade-up)。

5. 交互细节——关于我们页资质证书无限轮播

requestAnimationFrame 实现了一个带焦点缩放的无限轮播动画。证书图片从左向右自动滚动,鼠标悬浮时暂停,悬停的证书放大(transform: scale(1.1)),离开后恢复。核心逻辑是不断更新 translateX 偏移量,超出边界后重置到起点,形成无缝循环。用 requestAnimationFrame 而非 setInterval 是因为 rAF 与浏览器刷新率同步,动画更流畅且页面不可见时自动暂停。

6. 背景粒子特效

plugins/stars.js(星空粒子背景)、plugins/waves.js(波浪背景)在特定页面使用,通过 Canvas 绘制实现。

追问:"AOS 初始化时机怎么控制?"

AOS 依赖 DOM 和 window,所以插件设了 ssr: false。初始化在 mounted 里调 AOS.init(),设置 duration: 700, easing: 'ease-out-cubic'。部分模块通过 v-if="isDone" 等待数据加载完成后再渲染,确保 AOS 能正确检测到元素的初始位置。

行为面试常见题快速答

"为什么做这个项目?"

公司的官网是业务的门面。之前用的是 WordPress 模板站,加载慢、不好维护、不支持定制化。老板提需求要做一套自主可控的企业官网,能展示 9 大产品线、支持用户注册和试用申请、在搜索引擎上有好的排名。我从零搭建,独立完成了整个前端项目。

"遇到的最大难点?"

Gzip + Brotli 预压缩的 Nginx 配置调试。nuxt-precompress 生成的 .gz.br 文件放在静态资源目录,但 Nginx 需要正确配置 gzip_static on 才能优先返回预压缩文件。如果 Nginx 版本太低(< 1.11.x),Brotli 模块需要额外编译安装。花了不少时间查文档、改 nginx.conf,最终用 encodingsPriority: ["br", "gzip"] 确保浏览器优先尝试 Brotli。

还有 IE 兼容性问题——部分工厂客户还在用 IE11,而 Nuxt2 默认不转译 node_modules 里的包。通过 build.transpile: [/^element-ui/] 强制转译 Element UI,解决了 IE 下的白屏问题。

"有什么遗憾?"

  1. 没有 TypeScript——项目是纯 JavaScript 写的,类型安全全靠记忆和注释。如果重来会用 Nuxt2 + TypeScript 或直接上 Nuxt3。
  2. 没有 CI/CD——部署流程是手动 npm run generate → 手动上传到服务器,应该配 GitHub Actions 自动构建部署。
  3. 样式变量管理不够统一——早期页面各自写了硬编码的颜色值,后来才抽取了 _variables.scss。还有一些老页面的样式没有完全迁移到变量体系。

"怎么跟设计协作的?"

设计师出 PSD 设计稿,我切图 + 写页面。因为没有 UI 组件库的约束,我封装了一些通用组件——common-banner(带背景图的页面标题横幅)、common-headercommon-footerapplyForTrial(申请试用表单)、rightSideBar(右侧浮动工具栏)——风格统一,20+ 页面共用同一套组件。

"项目上线后的维护情况?"

上线后持续迭代——加了新产品(筒子纱智能回潮仪、浆纱水分测量系统),优化了首页 Banner 和产品入口卡片的交互动效(3D 倾斜 + 聚光灯特效),升级了 Header 导航栏的毛玻璃效果。虽然不是高频迭代的项目,但公司有新需求时我会快速响应。

上一篇:08-EMI云数据平台 回到首页:README

前端开发工程师 · 面试准备