Skip to content

前端面试常考点

SPA页面的理解

单页应用(Single Page Application,简称 SPA)是一个只包含一个 HTML 页面的应用,所有页面元素都加载在这个页面内,通过 JavaScript 实现不同内容的切换。 优点:

  1. 无刷新加载,提升用户体验
  2. 减少服务器压力
  3. 前后端分离清晰 缺点:
  4. SEO不友好
  5. 首屏加载慢
  6. 不支持IE8及以下

前端页面优化?

代码分割 图片优化(webp、懒加载) 合并/减少请求数量 加快资源传输(CDN、http/2、http/3) 浏览器缓存 gzip压缩 首屏优化(路由懒加载) v-for必须绑定key,避免与v-if同时使用

v-for为什么不能和v-if一起用?

首先,官方是不推荐把两者放在同一个元素上的。

在Vue 2中,v-for 的优先级高于 v-if。这意味着哪怕你只想渲染列表中的3个元素,Vue也会先完整遍历整个数组(比如100条数据),生成100个虚拟节点,然后再对每个节点判断 v-if。这会造成大量的性能浪费,属于编译时和渲染时的双重开销。

在Vue 3中,优先级被调整了:v-if 高于 v-for。这样做虽然更符合逻辑直觉,但会导致一个新的问题:在 v-if 的判断条件里,往往要用到 v-for 作用域里的变量(比如 item.isActive),因为 v-if 先执行,这时 item 变量还没定义,程序会直接报错。

nextTick的作用?

Vue 更新 DOM 是异步执行的 数据变化后,不会立即更新 DOM,而是将更新任务推入队列 同一个事件循环中的数据变更会合并成一次更新

html
<template>
    <div>
        <p ref="message">{{ text }}</p>
        <button @click="updateText">更新文本</button>
    </div>
</template>

<script>
    export default {
        data() {
            return {
                text: '旧的文字'
            }
        },
        methods: {
            updateText() {
                // 修改数据
                this.text = '新的文字'

                // ❌ 立即获取 DOM - 还是旧内容
                console.log('立即获取:', this.$refs.message.textContent) // '旧的文字'

                // ✅ 使用 nextTick 获取 - 新内容
                this.$nextTick(() => {
                    console.log('nextTick 后:', this.$refs.message.textContent) // '新的文字'
                })
            }
        }
    }
</script>

Vue的响应式原理

Vue2 响应式基于 Object.defineProperty,通过 getter 收集依赖,setter 派发更新,核心由 Dep 和 Watcher 实现。但它无法监听属性新增、删除及数组索引变化。

Vue3 使用 Proxy 重构响应式系统,通过 targetMap + ReactiveEffect 精准依赖追踪,支持新增删除属性、数组、Map、Set,并采用懒代理和调度器机制,性能与扩展性显著提升。

vue2:Object.defineProperty()

劫持对象单个属性的getter/setter,不能监听对象新增属性(this.$set)、删除属性、数组下标监听不到

js
function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    get() {
      dep.depend()
      return val
    },
    set(newVal) {
      val = newVal
      dep.notify()
    }
  })
}

vue3:ES6 Proxy

劫持整个对象,监听新增、删除、数组下标、数组方法,Map / Set (数据劫持get/set->依赖收集track->派发更新trigger)(并通过effect栈实现嵌套响应式逻辑的正确追踪)

watch 和 computed 的区别?

能详细介绍一下如何多种平台小程序之间的跨端一致性、以及在性能或体验上做了哪些关键优化?

1.API差异封装(关键) 不同平台 API 差异很大 微信:wx.login 支付宝:my.getAuthCode H5:OAuth App:Native SDK

ts
// utils/platform/login.ts

export async function platformLogin() {
  // 按平台走不同实现
}

2.UI 一致性处理 不同平台:

字体渲染不同 safe-area 不同 navbar 高度不同 tabbar 表现不同 scroll-view 行为不同

全部使用 rpx 设计稿统一 750 宽 原生 navbar 在不同平台高度不一致。

所以:

很多页面改成 custom navbar 自己计算状态栏高度

例如:

ts
const { statusBarHeight } = uni.getSystemInfoSync()

我们会尽量:

“把条件编译控制在组件内部,而不是业务页面里。” 4.性能优化 首屏性能优化:分包加载,路由懒加载(H5 端可以通过 import() 实现真正代码分割,小程序端的核心优化方式其实是“分包加载”)、图片压缩(webp)、CDN 渲染性能优化:分页加载、debounce/throttle 用户体验优化:骨架屏、页面预加载

什么是 CI/CD?如何在前端项目中实施 CI/CD?

CI/CD 是一种自动化软件交付流程。

CI(Continuous Integration)是持续集成(自动执行按照依赖、单元测试、构建) CD(Continuous Delivery / Deployment)是持续交付或持续部署

核心目标是:

“让代码从提交到测试、构建、发布整个流程自动化,提高开发效率和上线稳定性。”

为什么要前端工程化?

前端工程化是为解决前端项目规模扩大后出现的协作低效、构建不一致、质量难保障、维护成本高等问题而产生的系统性解决方案。

其核心动因包括:支撑大型SPA/微前端架构的模块化与依赖管理; 提升多团队并行开发的代码可维护性与接口契约性; 通过自动化保障代码质量(ESLint/TSLint、Prettier、单元/集成测试); 统一构建与发布流程以确保环境一致性; 加速本地开发体验(HMR、按需编译); 以及实现CI/CD流水线驱动的可靠交付。 关键环节涵盖: 1)模块化与规范(ES Module、CommonJS、TS支持); 2)构建工具链(Webpack/Vite/Rspack,负责打包、压缩、tree-shaking、代码分割); 3)包管理(npm/pnpm/yarn,版本控制与依赖解析); 4)代码质量保障(静态检查、格式化、测试覆盖、覆盖率门禁); 5)本地开发体验优化(Mock服务、代理配置、DevServer); 6)持续集成与部署(GitHub Actions/Jenkins,自动化构建、测试、发布、回滚); 7)性能与监控集成(Lighthouse审计、Sentry接入、资源加载追踪); 8)文档与组件库建设(Storybook、Docusaurus)。工程化本质是将软件工程最佳实践系统性落地到前端研发全生命周期。

Vue 中 v-model 的原理是什么?在 Vue3 中如何实现自定义 v-model?

v-model 是 Vue 的双向绑定语法糖。 在vue2中:

html
<input v-model="message">
html
<input :value="message" @input="message = $event.target.value">

在vue3中:

html
<CustomInput v-model="searchText" />

等价于

html
<CustomInput
  :modelValue="searchText"
  @update:modelValue="newValue => searchText = newValue"
/>

vue3.4+

ts
// 子组件内部
const model = defineModel()

// 可以直接读取和修改这个值
console.log(model.value)
model.value = '新值' // 会触发父组件的更新

默认情况

js
<!-- 父组件 -->
<MyComponent v-model="bookTitle" />

<!-- 子组件 -->
const model = defineModel()
// 等价于 defineModel('modelValue')

带参数情况

js
<!-- 父组件 -->
<MyComponent v-model:title="bookTitle" />

<!-- 子组件 -->
const model = defineModel('title')  // 'title' 必须匹配

v-model修饰符:.lazy(默认情况下通过@input触发更新,加了lazy后通过@change实践触发更新) .number(自动将用户输入值转为数值) .trim(去掉首尾空白)

flex 实现左右固定,中间自适应

html
<div class="container">
  <div class="left">left</div>
  <div class="center">center</div>
  <div class="right">right</div>
</div>
<style>
.container {
  display: flex;
}
.left, .right {
  width: 100px;
}
.center {
  flex: 1;
}
</style>

多列卡片最后一行不能留空怎么实现?

html
<div class="container">
  <div class="item">1</div>
  <div class="item">2</div>
  <div class="item">3</div>
  <div class="item">4</div>
  <div class="item">5</div>
  <div class="item">6</div>
</div>
<style>
.container {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16px;
}
</style>
<script setup>
    import { computed, ref } from 'vue'

    const columns = ref(4) // 每行显示的列数
    const realCards = ref([
    { id: 1, title: '卡片1', content: '内容1' },
    { id: 2, title: '卡片2', content: '内容2' },
    { id: 3, title: '卡片3', content: '内容3' },
    { id: 4, title: '卡片4', content: '内容4' },
    { id: 5, title: '卡片5', content: '内容5' },
    { id: 6, title: '卡片6', content: '内容6' },
    // ... 更多卡片
    ])

    // 计算需要显示的卡片(包括占位符)
    const displayCards = computed(() => {
    const remainder = realCards.value.length % columns.value
    const needPlaceholders = remainder !== 0 ? columns.value - remainder : 0

    const cards = [...realCards.value]

    // 添加占位卡片
    for (let i = 0; i < needPlaceholders; i++) {
    cards.push({
    id: `placeholder-${i}`,
        isPlaceholder: true
        })
    }
        return cards
})
</script>

computed 和 watch 的区别?原理是什么?

区别

computedwatch
有返回值无返回值
有缓存无缓存
偏“计算”偏“监听”
惰性执行数据变化立即执行
用于模板计算用于副作用

computed 原理

核心机制:懒计算 + 缓存 + 依赖收集

所以性能高。

watch 原理:

核心机制:依赖追踪 + 副作用执行 + 异步队列

RESTful常用方法

方法用途幂等性安全性示例
GET 获取资源 ✅ 是 ✅ 是 GET /users/123
POST 创建资源 ❌ 否 ❌ 否 POST /users
PUT 完整更新/替换 ✅ 是 ❌ 否 PUT /users/123
PATCH 部分更新 ❌ 否 ❌ 否 PATCH /users/123
DELETE 删除资源 ✅ 是 ❌ 否 DELETE /users/123
HEAD 获取响应头 ✅ 是 ✅ 是 HEAD /users/123
OPTIONS 获取允许方法 ✅ 是 ✅ 是 OPTIONS /users
bash
# ❌ 错误(使用动词)
GET /getUser/123
POST /createUser
GET /getAllUsers
POST /user/create

# ✅ 正确(使用名词)
GET /users/123
POST /users
GET /users

后端代码实例

php
<?php
namespace app\controller;

use think\Request;
use app\BaseController;

class User extends BaseController
{
    // 模拟数据库
    private $users = [
        1 => ['id' => 1, 'name' => '张三', 'email' => 'zhang@example.com', 'age' => 25],
        2 => ['id' => 2, 'name' => '李四', 'email' => 'li@example.com', 'age' => 30],
    ];

    // ========== GET ==========
    public function index()
    {
        return json([
            'code' => 200,
            'data' => array_values($this->users)
        ]);
    }

    public function read($id)
    {
        if (!isset($this->users[$id])) {
            return json(['code' => 404, 'msg' => '用户不存在'], 404);
        }
        return json(['code' => 200, 'data' => $this->users[$id]]);
    }

    // ========== POST ==========
    public function create(Request $request)
    {
        $data = $request->post();
        $newId = max(array_keys($this->users)) + 1;
        $this->users[$newId] = [
            'id' => $newId,
            'name' => $data['name'],
            'email' => $data['email'],
            'age' => $data['age']
        ];
        
        // 201 Created + Location 头[citation:2]
        return json(['code' => 201, 'data' => $this->users[$newId]], 201)
            ->header('Location', "/api/users/{$newId}");
    }

    // ========== PUT:完整更新(必须传所有字段)==========
    public function update($id, Request $request)
    {
        if (!isset($this->users[$id])) {
            return json(['code' => 404, 'msg' => '用户不存在'], 404);
        }
        
        // 获取原始 JSON 输入
        $input = json_decode($request->getContent(), true);
        
        // PUT 要求完整替换:缺少的字段应置空或报错
        if (!isset($input['name']) || !isset($input['email']) || !isset($input['age'])) {
            return json(['code' => 400, 'msg' => 'PUT 请求需要提供完整字段'], 400);
        }
        
        $this->users[$id] = [
            'id' => $id,
            'name' => $input['name'],
            'email' => $input['email'],
            'age' => $input['age']
        ];
        
        return json(['code' => 200, 'data' => $this->users[$id]]);
    }

    // ========== PATCH:部分更新(只改传入的字段)==========
    public function patch($id, Request $request)
    {
        if (!isset($this->users[$id])) {
            return json(['code' => 404, 'msg' => '用户不存在'], 404);
        }
        
        $input = json_decode($request->getContent(), true);
        
        // 只更新传入的字段,未传入的保持不变
        foreach ($input as $key => $value) {
            if (isset($this->users[$id][$key])) {
                $this->users[$id][$key] = $value;
            }
        }
        
        return json(['code' => 200, 'data' => $this->users[$id]]);
    }

    // ========== DELETE:删除资源 ==========
    public function delete($id)
    {
        if (!isset($this->users[$id])) {
            return json(['code' => 404, 'msg' => '用户不存在'], 404);
        }
        
        unset($this->users[$id]);
        
        // 204 No Content 不返回 body[citation:2]
        return response('', 204);
    }

    // ========== HEAD:仅返回响应头 ==========
    // 注意:HEAD 请求会自动被框架处理,无需显式实现
    // 但如果你想自定义 HEAD 行为,可以这样写:
    public function head($id)
    {
        if (!isset($this->users[$id])) {
            return response('', 404);
        }
        // HEAD 请求不应返回 body
        return response('', 200)
            ->header('X-Resource-Exists', 'true')
            ->header('X-Last-Modified', date('Y-m-d H:i:s'));
    }

    // ========== OPTIONS:返回允许的请求方法 ==========
    // 跨域预检请求需要返回 204 并带上 Allow 头[citation:4][citation:9]
    public function options($id)
    {
        return response('', 204)
            ->header('Allow', 'GET, PUT, PATCH, DELETE, HEAD, OPTIONS')
            ->header('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS');
    }
}

HEAD 和 OPTIONS 的使用场景

HEAD 请求的典型用途 一、检查资源是否存在(不下载整个内容)

二、获取资源元信息(最后修改时间、大小等)

三、验证缓存有效性

js
// 检查用户头像是否存在
const response = await api.head('/users/1/avatar')
if (response.status === 200) {
// 头像存在,可以从 CDN 加载
}

OPTIONS 请求的典型用途 一.CORS 预检:浏览器自动发送,检查跨域请求是否允许 预检触发条件: 1.使用了非简单请求的方法(如 PUT、DELETE等,GET、POST、HEAD一般不会触发预检) 2.使用了非简单请求头

js
// ❌ 自定义头会触发预检
fetch('https://api.example.com/users', {
    headers: {
        'X-Custom-Header': 'custom-value',  // 自定义头
        'Authorization': 'Bearer token123'   // 虽然常见,但也会触发
    }
})

// ✅ 简单请求头不会触发预检
fetch('https://api.example.com/users', {
    headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/x-www-form-urlencoded'
    }
})

3.Content-Type 不是简单类型 简单请求只允许以下 Content-Type:

  • application/x-www-form-urlencoded
  • multipart/form-data
  • text/plain
js
// ❌ 使用 JSON 会触发预检(最常见的情况)
fetch('https://api.example.com/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'  // 不是简单类型
  },
  body: JSON.stringify({ name: '张三' })
})

// ✅ 改成表单格式就不会触发预检
fetch('https://api.example.com/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded'
  },
  body: 'name=张三'
})

二、API 发现:客户端探测某个端点支持哪些 HTTP 方法

js
// 探测 /users 端点支持的方法
const res = await api.options('/users')
console.log(res.headers.allow)  // "GET, POST, HEAD, OPTIONS"

Set、Map、WeakSet、WeakMap 区别

Set

特点

  • 值唯一
  • 类数组
js
const set = new Set([1,2,2])//结果:{1,2}

Map

特点

  • 键值对
  • key 可以是任意类型
js
map.set(obj, 'value')

WeakSet

特点

  • 只能存对象
  • 弱引用
  • 不可遍历

对象被释放后自动移除。

WeakMap

特点

  • key 必须是对象
  • 弱引用
  • 不可遍历

常用于:

  • 私有变量
  • DOM 缓存
  • 避免内存泄漏
js
// 强引用(Map)
let obj = { data: '重要数据' }
const map = new Map()
map.set(obj, '缓存数据')

obj = null  // 断开原始引用
// 但 map 仍然持有 obj 的引用,内存无法释放 ❌

// 弱引用(WeakMap)
let obj = { data: '重要数据' }
const weakMap = new WeakMap()
weakMap.set(obj, '缓存数据')

obj = null  // 断开所有引用
// weakMap 的引用是弱引用,对象可以被垃圾回收 ✅

节流和防抖

防抖(Debounce) 核心(类比电梯关门)

事件停止触发后才执行。

例如:

  • 搜索框
  • 输入联想
js
// 基础版防抖
function debounce(fn, delay) {
    let timer = null
    return function(...args) {
        // 每次触发都清除之前的定时器
        if (timer) clearTimeout(timer)
        // 重新设置定时器
        timer = setTimeout(() => {
            fn.apply(this, args)
        }, delay)
    }
}

节流(throttle) 核心(类比红绿灯) 固定时间内只执行一次。 例如:

  • scroll
  • resize
  • 鼠标移动
js
function throttle(fn, delay) {
    let lastTime = 0
    return function(...args) {
        const now = Date.now()
        if (now - lastTime >= delay) {
            fn.apply(this, args)
            lastTime = now
        }
    }
}
  1. websocket 乱码怎么解决?

核心答法:

前后端编码格式不一致。

常见原因

  1. 编码不统一

例如:

  • 前端 UTF-8
  • 后端 GBK
  1. 二进制数据没处理

如果传:

js
ArrayBuffer
Blob

需要:

js
socket.binaryType = 'arraybuffer'
  1. JSON 序列化问题

推荐统一:

js
JSON.stringify()
JSON.parse()

面试加分

实际项目里一般会统一:

  • UTF-8
  • JSON 协议
  • messageType

避免乱码。

事件循环的理解

JavaScript 是单线程的,事件循环就是它处理异步任务的方式:一边执行同步代码,一边把异步任务放到队列里,等同步代码执行完再按顺序执行队列里的任务。 每次事件循环的末尾会清空微任务队列,然后才会处理宏任务。

微任务队列考题

js
async function async1() {
    console.log('async1 start')//同步
    await async2()
    console.log('async1 end') //微任务
}
async function async2() {
    console.log('async2')//同步
}
console.log('script start') //同步任务
setTimeout(() => {
    console.log('setTimeout')//宏任务
}, 0);
async1()
new Promise(resolve => {
    console.log('promise1')//同步
    resolve()
}).then(() => {
    console.log('promise2')//微任务
}).then(()=>{
    console.log('promise3')//微任务
})
console.log('script end') //同步任务

await的原理

js
Promise.resolve(async2()).then(() => {
    console.log('async1 end')  // 放入微任务队列
})

输出结果:

text
【同步代码执行】
script start
async1 start
async2
promise1
script end
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

【微任务队列清空】
async1 end      ← await 后面的代码,不是await这一行
promise2        ← Promise 的 .then()
promise3        ← 链式调用的第二个 .then()
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

【宏任务队列执行】
setTimeout

宏任务和微任务

宏任务

  • setTimeout
  • setInterval
  • 整体 script 代码
  • I/O操作(ajax、文件等)
  • DOM 事件

微任务

  • Promise.then/ catch/ finally
  • MutationObserver(DOM 变化监听)
  • queueMicrotask(显式创建微任务的 API)
  • await 后续代码
  • nextTick

闭包

闭包是指一个函数可以访问它定义时所在的作用域中的变量,即使这个函数在别处被调用。换句话说,闭包让函数“记住”了它出生的环境。

js
function fn() {
  let count = 0

  return function() {
    count++
  }
}

解决的问题

作用:保存变量,实现私有变量,封装模块

v-for 为什么必须加 key?

key 是 Vue 识别节点的唯一标识,用于虚拟 DOM 的 diff 算法中判断节点是移动、新增还是删除。 没有 key 时,Vue 不知道节点之间的关系,只是简单地"就地更新"文本内容,复用了原来的 DOM 元素。

html
<template>
    <div>
        <button @click="addFirst">头部插入</button>
        <ul>
            <!-- ❌ 没有 key -->
            <li v-for="item in list">
                <input type="checkbox" /> {{ item.name }}
            </li>
        </ul>
    </div>
</template>

<script setup>
    import { ref } from 'vue'

    const list = ref([
        { id: 1, name: '任务A' },
        { id: 2, name: '任务B' },
        { id: 3, name: '任务C' }
    ])

    const addFirst = () => {
        list.value.unshift({ id: 4, name: '新任务' })
    }
</script>

template 怎么变成浏览器认识的 HTML?

template ↓ compiler 编译 ↓ render 函数 ↓ virtual dom ↓ 真实 DOM

Vue 扮演了“翻译官”的角色:把 template 编译成 render 函数,再把 render 函数生成的虚拟 DOM 转译成浏览器能懂的 DOM 操作。

为什么要有虚拟 DOM? 直接操作 DOM 非常慢,而操作 JavaScript 对象非常快。虚拟 DOM 让 Vue 可以先在内存中算出最优的更新方案,再批量、高效地去更新真实页面。

uniapp 如何实现热更新?

UniApp 实现代码的热更新,其实就是我们通常说的wgt资源包热更新。它的核心原理是只下发生成的 wgt 资源包,替换掉你App里的前端代码,这样用户不用重新下载整个安装包,就能完成更新。

一个完整的实现方案可以遵循下面这4个步骤。

1.搭建更新后台,管理资源

你需要搭建一个后台,用来上传和管理版本文件。这个后台可以根据版本号、平台(Android/iOS)等信息,提供对应更新包的下载地址。

你可以自己搭建服务器,或者使用DCloud官方推荐的 uni-upgrade-center 升级中心框架。

方案一:自己搭建服务器。你需要提供一个接口,用于返回应用的最新版本信息和文件下载地址。

方案二(推荐):使用 uni-upgrade-center。这是一个官方升级中心框架,可以配合uniCloud云开发快速部署。引入插件、部署云函数后,就能在后台直接管理版本的发布和更新了。

2.生成和发布资源包

当你的代码有更新时,不要重新打包生成 apk 或 ipa。

在 HBuilderX 中,选择菜单栏的「发行」->「原生App-制作应用资源包(wgt)」。

打包成功后会生成一个 .wgt 文件。

将这个 .wgt 文件上传到你后台指定的位置,并在后台数据库中记录好这次的版本号和文件地址。

3.客户端检查与下载更新

在App启动或某个合适的时机,请求你的后台接口,获取最新的版本信息,并与当前App版本做比较。

当发现有新版本时,就调用API去下载资源包。这里有一个关键的安全环节:

为了防止下载的包被篡改或中途出错,务必校验下载文件的MD5值,确保资源包的完整性。

4.安装更新包并重启

下载完成后,调用 plus.runtime.install 这个API来安装热更新包,并提示用户重启应用以使更新生效。

json
'app-plus': {
    "distribute": {
        "android": {
            "permissions": [
                "<uses-permission android:name=\"android.permission.INTERNET\"/>"
            ]
        }
    }
}
js
/**
 * UNIAPP WGT 热更新
 */

const SERVER_URL = 'https://xxx.com/api/version'

/**
 * 获取当前APP版本
 */
function getCurrentVersion() {
    return new Promise((resolve) => {
        plus.runtime.getProperty(plus.runtime.appid, (widgetInfo) => {
            resolve(widgetInfo.version)
        })
    })
}

/**
 * 比较版本号
 * return true 表示需要更新
 */
function needUpdate(oldVersion, newVersion) {
    const oldArr = oldVersion.split('.')
    const newArr = newVersion.split('.')

    const maxLen = Math.max(oldArr.length, newArr.length)

    for (let i = 0; i < maxLen; i++) {
        const oldNum = parseInt(oldArr[i] || 0)
        const newNum = parseInt(newArr[i] || 0)

        if (newNum > oldNum) {
            return true
        }

        if (newNum < oldNum) {
            return false
        }
    }

    return false
}

/**
 * 下载并安装WGT
 */
function downloadWgt(url, silent = false) {

    if (!silent) {
        uni.showLoading({
            title: '更新下载中...'
        })
    }

    uni.downloadFile({
        url,
        success: (res) => {

            if (!silent) {
                uni.hideLoading()
            }

            if (res.statusCode !== 200) {
                uni.showToast({
                    title: '下载失败',
                    icon: 'none'
                })
                return
            }

            installWgt(res.tempFilePath, silent)
        },
        fail: () => {

            if (!silent) {
                uni.hideLoading()
            }

            uni.showToast({
                title: '下载失败',
                icon: 'none'
            })
        }
    })
}

/**
 * 安装WGT
 */
function installWgt(path, silent = false) {

    plus.runtime.install(
        path,
        {
            force: false
        },
        () => {

            if (!silent) {
                uni.showModal({
                    title: '更新完成',
                    content: '应用将重启',
                    showCancel: false,
                    success: () => {
                        plus.runtime.restart()
                    }
                })
            } else {
                plus.runtime.restart()
            }
        },
        (e) => {

            console.error('安装失败', e)

            uni.showToast({
                title: '安装失败',
                icon: 'none'
            })
        }
    )
}

/**
 * 检查更新
 */
export async function checkUpdate() {

    // #ifdef APP-PLUS

    const currentVersion = await getCurrentVersion()
    const res = await uni.request({
        url: SERVER_URL,
        method: 'GET'
    })
    const {
        version,
        wgtUrl,
        description,
        force,
        silent
    } = res.data
    // 不需要更新
    if (!needUpdate(currentVersion, version)) {
        return
    }

    // 静默更新
    if (silent) {
        downloadWgt(wgtUrl, true)
        return
    }

    // 手动更新
    uni.showModal({
        title: '发现新版本',
        content: description || '是否立即更新?',
        confirmText: '更新',
        cancelText: '取消',
        showCancel: !force,

        success: (modalRes) => {

            if (modalRes.confirm) {
                downloadWgt(wgtUrl, false)
            } else {

                // 强制更新不允许取消
                if (force) {
                    plus.runtime.quit()
                }
            }
        }
    })
    // #endif
}

App.vue调用

html
<script>
import { checkUpdate } from '@/utils/hotUpdate.js'

export default {
    onLaunch() {

        // #ifdef APP-PLUS
        checkUpdate()
        // #endif

    }
}
</script>

小程序端:当新版本通过审核后,用户下次打开或冷启动小程序时,平台会自动、静默地下载最新代码包并替换旧版本。这个过程无需用户确认,也无需像App那样重新下载安装包

js
// 1. 获取全局唯一的版本更新管理器
const updateManager = uni.getUpdateManager();

// 2. 监听向微信后台请求检查更新结果
updateManager.onCheckForUpdate(function (res) {
    // 如果有新版本,res.hasUpdate 为 true
    console.log(res.hasUpdate);
});

// 3. 监听新版本已下载完成
updateManager.onUpdateReady(function () {
    uni.showModal({
        title: '更新提示',
        content: '新版本已准备好,是否重启应用?',
        success(res) {
            if (res.confirm) {
                // 用户点击确定,强制小程序重启并使用新版本
                updateManager.applyUpdate();
            }
        }
    });
});

// 4. 监听新版本下载失败
updateManager.onUpdateFailed(function () {
    // 新的版本下载失败
});

vue3相比vue2的优势

以下是核心差异的详细对比:

特性Vue2Vue3带来的好处
核心变化:代码写法选项式 API (Options API)组合式 API (Composition API)告别逻辑分散问题,相关功能的代码可以写在一起,便于管理和复用。
关键改进:响应式Object.definePropertyProxy突破原有限制,可以监听属性的动态添加/删除和数组索引修改,无需再用 $set。
性能提升标准性能更快、更轻启动和更新速度提升 1.3~2 倍,打包体积减小(Tree-shaking 更佳),内存占用更低。
语言支持TypeScript 支持较弱TypeScript 原生支持类型推导更准确,代码提示更智能,大型项目开发体验大幅提升。
新增内置组件单一根节点支持多根节点(Fragment)不再需要多余的父标签包裹,减少无用 DOM 层级。
新增内置组件 需手动处理 提供 Teleport 组件 可以轻松将组件内容(如弹窗)渲染到 DOM 的任意位置,摆脱样式层级困扰。
新增内置组件需手动处理提供 Teleport 组件可以轻松将组件内容(如弹窗)渲染到 DOM 的任意位置,摆脱样式层级困扰。

副作用

  1. 什么是Vue 3中的副作用?

副作用是指响应式函数执行过程中,除了计算结果以外,对外部环境产生影响的操作。 这些操作可能会对组件的状态产生影响,但不会由Vue框架自动追踪和响应。副作用通常是在组件的生命周期钩子函数之外进行的异步操作、事件监听、定时器设置或DOM操作等。

  1. Vue 3中的副作用有哪些?

① 发送网络请求

js
watch(
    () => userId.value,
    async (id) => {
        const res = await getUser(id)
        user.value = res.data
    }
)

这里:

调用了接口 与服务器通信

属于副作用。

② 修改DOM

js
watch(
    () => darkMode.value,
    (val) => {
        document.body.classList.toggle('dark', val)
    }
)

这里:

操作浏览器DOM

属于副作用。

③ 定时器

js
const timer = setInterval(() => {
    console.log('tick')
}, 1000)

属于副作用。

因为:

创建了浏览器资源 会持续执行 ④ localStorage

js
watch(
    () => token.value,
    (val) => {
        localStorage.setItem('token', val)
    }
)

属于副作用。

⑤ WebSocket

js
const ws = new WebSocket(url)

属于副作用。

⑥ 事件监听

js
window.addEventListener('resize', handleResize)

属于副作用。

  1. 如何处理Vue 3中的副作用?

为了处理Vue 3中的副作用,可以使用Vue提供的watch API或者watchEffect API来监视状态的变化,并在状态变化时执行相应的副作用操作。

使用watch API:通过watch API可以监视指定的状态或表达式,当状态发生变化时,可以执行相应的副作用操作。例如,可以监视数据的变化,并在数据变化时发送网络请求来更新页面。

使用watchEffect API:watchEffect API可以监听组件内部使用的任何响应式数据,并在这些数据发生变化时自动执行副作用操作。这个API更加简洁和灵活,适用于监听多个响应式数据。

除了Vue提供的API,还可以使用第三方库来处理副作用,例如使用axios库发送异步请求,使用lodash库进行防抖或节流等操作。

js
watch(source, (newVal, oldVal, onCleanup) => {})

onCleanup 用于: “副作用重新执行前清理旧副作用”

js
watchEffect((onCleanup) => {
  const timer = setInterval(() => {
    console.log(count.value)
  }, 1000)

  onCleanup(() => {
    clearInterval(timer)
  })
})

中断请求(防止旧数据覆盖新数据)

js
watch(keyword, async (val, _, onCleanup) => {
  const controller = new AbortController()

  onCleanup(() => {
    controller.abort()
  })

  const res = await fetch('/api', {
    signal: controller.signal
  })
})

中断请求

js
let abortController = null;

function uploadFile(file) {
    const formData = new FormData();
    formData.append('file', file);

    // 创建 AbortController
    abortController = new AbortController();

    axios.post('/upload', formData, {
        signal: abortController.signal,  // 使用 signal 而非 cancelToken
        onUploadProgress: (progressEvent) => {
            const percent = (progressEvent.loaded / progressEvent.total) * 100;
            console.log(`上传进度: ${percent}%`);
        }
    }).then(response => {
        console.log('上传成功', response.data);
        abortController = null;
    }).catch(error => {
        if (error.name === 'CanceledError' || error.code === 'ERR_CANCELED') {
            console.log('上传已暂停');
        } else {
            console.error('上传失败:', error);
        }
    });
}

function pauseUpload() {
    if (abortController) {
        abortController.abort();  // 取消请求
        abortController = null;
    }
}

webpack和vite核心区别

维度WebpackVite
开发启动速度慢(需要全量打包)极快(按需编译)
热更新速度慢(全量更新)极快(ESM + 精确失效)
配置复杂度高(loader/plugin 繁多)低(内置常用功能)
生产构建优化成熟(Tree-shaking、分包等)依赖 Rollup(能力接近)
底层机制Bundle-based(打包器)Native ESM(浏览器原生模块)
生态成熟度老牌、生态丰富较新、但增长迅速

安卓上架相关

获取公钥

keytool -list -rfc -keystore your_keystore_name.keystore -alias outerheaven

获取MD5指纹

keytool -exportcert -keystore your_keystore.keystore -alias outerheaven | openssl dgst -md5

查看详细信息

keytool -list -v -keystore outerheavenai.keystore -alias outerheaven

history路由和hash路由区别

Hash 模式是利用 URL 中 # 后面的「哈希值」实现路由跳转,# 是浏览器的锚点标识,哈希值不会被发送到服务器。

示例 URL:http://yourdomain.com/#/proOrder 其中 #/proOrder 就是哈希路由,# 后面的内容完全由前端控制。 核心 API:window.onhashchange 优点:

  1. 无需后端配置:哈希值不发服务器,直接访问 http://xxx.com/#/proOrder 不会 404;
  2. 兼容性极好:支持所有浏览器(包括 IE6/7/8);
  3. 开发便捷:本地 / 线上部署无需协调后端; 缺点:
  4. URL 不美观:带 # 符号,用户体验略差;
  5. SEO 不友好:部分搜索引擎爬虫会忽略 # 后的内容,影响页面收录;
  6. 锚点冲突:若页面内有原生锚点,会和路由哈希冲突;

History 模式基于 HTML5 新增的 History API 实现,URL 无 #,和传统后端路由的 URL 格式一致,哈希值会被完整发送到服务器。

示例 URL:http://yourdomain.com/proOrder(和后端路由格式完全一致) 该模式是现代前端项目的主流选择(追求 URL 美观、SEO 友好)。 核心 API:HTML5 History API(history.pushState(), history.replaceState()) 优点:

  1. URL 美观:无 #,符合用户对 URL 的认知习惯;
  2. SEO 友好:完整 URL 会被搜索引擎收录,利于网站优化;
  3. 无锚点冲突:和页面内原生锚点(如 #top)互不干扰; 缺点:
  4. 需要后端配置:直接访问子路由(如 /proOrder)会触发服务器请求,服务器未配置时返回 404;
  5. 兼容性一般:仅支持 IE10+ 及现代浏览器;
  6. 部署成本高:需协调后端修改 Nginx/Apache 配置
bash
location / {
  try_files $uri $uri/ /index.html;
}

let var const的区别

特性varletconst|
作用域函数作用域块级作用域块级作用域
变量提升是(初始 undefined)是(但暂存死区)是(但暂存死区)
重复声明✅ 允许❌ 不允许❌ 不允许
必须初始值❌ 不需要❌ 不需要✅ 必须
重新赋值✅ 允许✅ 允许❌ 不允许
全局属性是 (window.xx)

前端表单按钮重复点击提交,如何防止数据重复提交?

方案一:点击后立刻禁用按钮,直到请求完成或超时

js
const submit = async () => {
  if (loading.value) return; // 状态锁
  loading.value = true;
  try {
    await api.submit(data);
    // 提示成功
  } catch (err) {
    // 处理错误
  } finally {
    loading.value = true; // 注意:如果是跳转页面,可能不需要解锁
    // 或者使用 setTimeout 延迟解锁,比如 2 秒后
  }
};

方案二:请求层面(防抖 & 节流 & 取消请求)

js
// 使用 AbortController
let controller = null;

const submit = async () => {
  // 如果上一次请求还没回来,直接中断它
  if (controller) {
    controller.abort();
  }
  controller = new AbortController();
  try {
    await fetch('/api', { signal: controller.signal });
  } catch (e) {
    if (e.name === 'AbortError') console.log('请求已取消');
  } finally {
    controller = null;
  }
};

方案三:后端幂等性(终极保障)

bash
获取token

提交携带token

后端校验

立即失效