前端面试手撕合集之纯HTML、Vue和React组件

 更新时间:2026年02月28日 08:26:28   作者:FE_Jinger  
Vue 和React都是当前非常流行的前端框架,它们都可以用来构建单页面应用(SPA)和响应式的用户界面,这篇文章主要介绍了前端面试手撕代码之纯HTML、Vue和React组件的相关资料,需要的朋友可以参考下

一、原生JS 核心组件

1. 轮播图(自动播放+点击切换+指示器)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>原生JS轮播图</title>
  <style>
    .carousel { width: 500px; height: 300px; margin: 50px auto; position: relative; overflow: hidden; }
    .carousel-list { display: flex; width: 100%; height: 100%; transition: transform 0.5s ease; }
    .carousel-item { flex: 0 0 100%; width: 100%; height: 100%; }
    .carousel-item img { width: 100%; height: 100%; object-fit: cover; }
    .carousel-btn { position: absolute; top: 50%; transform: translateY(-50%); width: 40px; height: 40px; background: rgba(0,0,0,0.5); color: white; border: none; cursor: pointer; z-index: 10; }
    .prev { left: 10px; }
    .next { right: 10px; }
    .carousel-indicator { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); display: flex; gap: 10px; }
    .indicator-item { width: 10px; height: 10px; border-radius: 50%; background: rgba(255,255,255,0.5); cursor: pointer; }
    .indicator-item.active { background: white; }
  </style>
</head>
<body>
  <div class="carousel">
    <div class="carousel-list">
      <div class="carousel-item"><img src="https://picsum.photos/500/300?1" alt="图1"></div>
      <div class="carousel-item"><img src="https://picsum.photos/500/300?2" alt="图2"></div>
      <div class="carousel-item"><img src="https://picsum.photos/500/300?3" alt="图3"></div>
    </div>
    <button class="carousel-btn prev">←</button>
    <button class="carousel-btn next">→</button>
    <div class="carousel-indicator">
      <div class="indicator-item active"></div>
      <div class="indicator-item"></div>
      <div class="indicator-item"></div>
    </div>
  </div>

  <script>
    const carousel = document.querySelector('.carousel');
    const list = document.querySelector('.carousel-list');
    const items = document.querySelectorAll('.carousel-item');
    const prevBtn = document.querySelector('.prev');
    const nextBtn = document.querySelector('.next');
    const indicators = document.querySelectorAll('.indicator-item');
    
    const itemWidth = items[0].offsetWidth;
    let currentIndex = 0;
    let timer = null;

    // 初始化指示器
    function updateIndicator() {
      indicators.forEach((item, index) => {
        item.classList.toggle('active', index === currentIndex);
      });
    }

    // 切换轮播
    function switchCarousel(index) {
      currentIndex = index;
      list.style.transform = `translateX(-${currentIndex * itemWidth}px)`;
      updateIndicator();
    }

    // 下一张
    function next() {
      currentIndex = (currentIndex + 1) % items.length;
      switchCarousel(currentIndex);
    }

    // 上一张
    function prev() {
      currentIndex = (currentIndex - 1 + items.length) % items.length;
      switchCarousel(currentIndex);
    }

    // 自动播放
    function autoPlay() {
      timer = setInterval(next, 3000);
    }

    // 事件绑定
    nextBtn.addEventListener('click', next);
    prevBtn.addEventListener('click', prev);
    indicators.forEach((item, index) => {
      item.addEventListener('click', () => switchCarousel(index));
    });

    // 鼠标悬停暂停
    carousel.addEventListener('mouseenter', () => clearInterval(timer));
    carousel.addEventListener('mouseleave', autoPlay);

    // 启动自动播放
    autoPlay();
  </script>
</body>
</html>

2. 下拉组件(点击展开+点击外部关闭)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>原生JS下拉组件</title>
  <style>
    .dropdown { width: 200px; margin: 50px auto; position: relative; }
    .dropdown-trigger { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 4px; cursor: pointer; }
    .dropdown-menu { width: 100%; padding: 0; margin: 0; list-style: none; border: 1px solid #ccc; border-top: none; border-radius: 0 0 4px 4px; position: absolute; top: 100%; left: 0; background: white; display: none; z-index: 999; }
    .dropdown-menu.show { display: block; }
    .dropdown-item { padding: 10px; cursor: pointer; }
    .dropdown-item:hover { background: #f5f5f5; }
  </style>
</head>
<body>
  <div class="dropdown" id="dropdown">
    <div class="dropdown-trigger">请选择</div>
    <ul class="dropdown-menu">
      <li class="dropdown-item">选项1</li>
      <li class="dropdown-item">选项2</li>
      <li class="dropdown-item">选项3</li>
    </ul>
  </div>

  <script>
    const dropdown = document.getElementById('dropdown');
    const trigger = dropdown.querySelector('.dropdown-trigger');
    const menu = dropdown.querySelector('.dropdown-menu');
    const items = dropdown.querySelectorAll('.dropdown-item');

    // 切换显示/隐藏
    function toggleMenu() {
      menu.classList.toggle('show');
    }

    // 点击外部关闭
    document.addEventListener('click', (e) => {
      if (!dropdown.contains(e.target)) {
        menu.classList.remove('show');
      }
    });

    // 选择选项
    items.forEach(item => {
      item.addEventListener('click', () => {
        trigger.textContent = item.textContent;
        menu.classList.remove('show');
      });
    });

    // 绑定触发事件
    trigger.addEventListener('click', (e) => {
      e.stopPropagation(); // 防止事件冒泡触发外部点击
      toggleMenu();
    });
  </script>
</body>
</html>

3. 表单验证(手机号+密码+提交验证)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>原生JS表单验证</title>
  <style>
    .form { width: 300px; margin: 50px auto; }
    .form-item { margin-bottom: 20px; }
    .form-label { display: block; margin-bottom: 5px; }
    .form-input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; }
    .form-input.error { border-color: red; }
    .error-tip { color: red; font-size: 12px; margin-top: 5px; display: none; }
    .error-tip.show { display: block; }
    .submit-btn { width: 100%; padding: 10px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; }
    .submit-btn:disabled { background: #ccc; cursor: not-allowed; }
  </style>
</head>
<body>
  <form class="form" id="loginForm">
    <div class="form-item">
      <label class="form-label">手机号</label>
      <input type="tel" class="form-input" id="phone" placeholder="请输入手机号">
      <div class="error-tip" id="phoneTip">请输入正确的11位手机号</div>
    </div>
    <div class="form-item">
      <label class="form-label">密码</label>
      <input type="password" class="form-input" id="pwd" placeholder="请输入6-16位密码">
      <div class="error-tip" id="pwdTip">密码长度需在6-16位之间</div>
    </div>
    <button type="submit" class="submit-btn" id="submitBtn" disabled>提交</button>
  </form>

  <script>
    const phoneInput = document.getElementById('phone');
    const pwdInput = document.getElementById('pwd');
    const phoneTip = document.getElementById('phoneTip');
    const pwdTip = document.getElementById('pwdTip');
    const submitBtn = document.getElementById('submitBtn');
    const loginForm = document.getElementById('loginForm');

    // 验证手机号
    function validatePhone() {
      const phone = phoneInput.value.trim();
      const reg = /^1[3-9]\d{9}$/;
      if (!reg.test(phone)) {
        phoneInput.classList.add('error');
        phoneTip.classList.add('show');
        return false;
      }
      phoneInput.classList.remove('error');
      phoneTip.classList.remove('show');
      return true;
    }

    // 验证密码
    function validatePwd() {
      const pwd = pwdInput.value.trim();
      if (pwd.length < 6 || pwd.length > 16) {
        pwdInput.classList.add('error');
        pwdTip.classList.add('show');
        return false;
      }
      pwdInput.classList.remove('error');
      pwdTip.classList.remove('show');
      return true;
    }

    // 检查所有验证
    function checkAll() {
      const isPhoneValid = validatePhone();
      const isPwdValid = validatePwd();
      submitBtn.disabled = !(isPhoneValid && isPwdValid);
    }

    // 实时验证
    phoneInput.addEventListener('input', checkAll);
    pwdInput.addEventListener('input', checkAll);

    // 提交验证
    loginForm.addEventListener('submit', (e) => {
      e.preventDefault();
      if (checkAll()) {
        alert('表单验证通过,提交成功!');
        loginForm.reset();
        submitBtn.disabled = true;
      }
    });
  </script>
</body>
</html>

4. 无限滚动(滚动到底部加载数据)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>原生JS无限滚动</title>
  <style>
    .container { width: 500px; margin: 0 auto; }
    .list { padding: 0; margin: 0; list-style: none; }
    .list-item { padding: 20px; border-bottom: 1px solid #eee; margin-bottom: 10px; }
    .loading { text-align: center; padding: 20px; color: #999; display: none; }
    .loading.show { display: block; }
  </style>
</head>
<body>
  <div class="container">
    <ul class="list" id="list"></ul>
    <div class="loading" id="loading">加载中...</div>
  </div>

  <script>
    const list = document.getElementById('list');
    const loading = document.getElementById('loading');
    let page = 1;
    const pageSize = 10;
    let isLoading = false; // 防止重复加载

    // 模拟请求数据
    function fetchData(page) {
      return new Promise((resolve) => {
        setTimeout(() => {
          const data = Array.from({ length: pageSize }, (_, i) => ({
            id: (page - 1) * pageSize + i + 1,
            content: `列表项 ${(page - 1) * pageSize + i + 1}`
          }));
          resolve(data);
        }, 1000);
      });
    }

    // 渲染列表
    function renderList(data) {
      data.forEach(item => {
        const li = document.createElement('li');
        li.className = 'list-item';
        li.textContent = item.content;
        list.appendChild(li);
      });
    }

    // 加载数据
    async function loadMore() {
      if (isLoading) return;
      isLoading = true;
      loading.classList.add('show');
      
      try {
        const data = await fetchData(page);
        renderList(data);
        page++;
      } catch (err) {
        alert('加载失败,请重试');
      } finally {
        isLoading = false;
        loading.classList.remove('show');
      }
    }

    // 监听滚动事件
    window.addEventListener('scroll', () => {
      // 滚动距离 + 视口高度 >= 文档总高度 - 100(提前加载)
      const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
      const clientHeight = document.documentElement.clientHeight;
      const scrollHeight = document.documentElement.scrollHeight;
      
      if (scrollTop + clientHeight >= scrollHeight - 100 && !isLoading) {
        loadMore();
      }
    });

    // 初始化加载第一页
    loadMore();
  </script>
</body>
</html>

二、Vue3 核心组件(组合式API)

1. 带插槽的卡片组件(Card.vue)

<template>
  <div class="card" :style="{ width: width + 'px' }">
    <!-- 头部插槽 -->
    <div class="card-header" v-if="$slots.header">
      <slot name="header"></slot>
    </div>
    <!-- 主体插槽 -->
    <div class="card-body">
      <slot></slot>
    </div>
    <!-- 底部插槽 -->
    <div class="card-footer" v-if="$slots.footer">
      <slot name="footer"></slot>
    </div>
  </div>
</template>

<script setup>
import { defineProps } from 'vue';

// 定义props
const props = defineProps({
  width: {
    type: Number,
    default: 300
  }
});
</script>

<style scoped>
.card {
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.card-header {
  padding: 16px;
  border-bottom: 1px solid #e5e7eb;
  font-weight: 600;
}
.card-body {
  padding: 16px;
}
.card-footer {
  padding: 16px;
  border-top: 1px solid #e5e7eb;
  color: #666;
}
</style>

<!-- 使用示例(App.vue) -->
<template>
  <div style="padding: 50px;">
    <Card :width="400">
      <template #header>卡片标题</template>
      <div>卡片主体内容,默认插槽</div>
      <template #footer>卡片底部 - 2025/12/27</template>
    </Card>
  </div>
</template>

<script setup>
import Card from './Card.vue';
</script>

2. 模态框组件(Modal.vue)

<template>
  <!-- 遮罩层 -->
  <div class="modal-mask" v-if="visible" @click="handleMaskClick"></div>
  <!-- 弹窗主体 -->
  <div class="modal-container" v-if="visible">
    <div class="modal-content" @click.stop>
      <!-- 头部 -->
      <div class="modal-header">
        <h3 class="modal-title">{{ title }}</h3>
        <button class="modal-close" @click="handleClose">×</button>
      </div>
      <!-- 主体 -->
      <div class="modal-body">
        <slot></slot>
      </div>
      <!-- 底部 -->
      <div class="modal-footer" v-if="$slots.footer">
        <slot name="footer"></slot>
      </div>
    </div>
  </div>
</template>

<script setup>
import { defineProps, defineEmits } from 'vue';

const props = defineProps({
  visible: {
    type: Boolean,
    default: false
  },
  title: {
    type: String,
    default: '提示'
  },
  closeOnMask: {
    type: Boolean,
    default: true
  }
});

const emit = defineEmits(['close']);

// 关闭弹窗
const handleClose = () => {
  emit('close');
};

// 点击遮罩关闭
const handleMaskClick = () => {
  if (props.closeOnMask) {
    handleClose();
  }
};
</script>

<style scoped>
.modal-mask {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0,0,0,0.5);
  z-index: 1000;
}
.modal-container {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 1001;
}
.modal-content {
  width: 400px;
  background: white;
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.modal-header {
  padding: 16px;
  border-bottom: 1px solid #e5e7eb;
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.modal-title {
  margin: 0;
  font-size: 16px;
}
.modal-close {
  background: transparent;
  border: none;
  font-size: 20px;
  cursor: pointer;
  color: #999;
}
.modal-close:hover {
  color: #333;
}
.modal-body {
  padding: 16px;
}
.modal-footer {
  padding: 16px;
  border-top: 1px solid #e5e7eb;
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}
</style>

<!-- 使用示例(App.vue) -->
<template>
  <div style="padding: 50px;">
    <button @click="modalVisible = true">打开弹窗</button>
    <Modal 
      :visible="modalVisible" 
      title="自定义标题"
      @close="modalVisible = false"
    >
      <div>弹窗内容</div>
      <template #footer>
        <button @click="modalVisible = false">取消</button>
        <button @click="handleConfirm">确认</button>
      </template>
    </Modal>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import Modal from './Modal.vue';

const modalVisible = ref(false);

const handleConfirm = () => {
  alert('确认操作');
  modalVisible.value = false;
};
</script>

3. 动态表单(可新增/删除表单项)

<template>
  <div class="dynamic-form" style="padding: 50px;">
    <div v-for="(item, index) in formList" :key="index" class="form-item">
      <input 
        v-model="item.value" 
        placeholder="请输入内容"
        style="padding: 8px; margin-right: 10px;"
      >
      <button @click="removeItem(index)" style="color: red;">删除</button>
    </div>
    <button @click="addItem" style="margin-top: 10px;">新增表单项</button>
    <button @click="submitForm" style="margin-left: 10px;">提交</button>
  </div>
</template>

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

// 初始化表单列表
const formList = ref([{ value: '' }]);

// 新增表单项
const addItem = () => {
  formList.value.push({ value: '' });
};

// 删除表单项
const removeItem = (index) => {
  if (formList.value.length <= 1) {
    alert('至少保留一个表单项');
    return;
  }
  formList.value.splice(index, 1);
};

// 提交表单
const submitForm = () => {
  const values = formList.value.map(item => item.value.trim()).filter(Boolean);
  if (values.length === 0) {
    alert('请填写至少一个表单项');
    return;
  }
  console.log('表单数据:', formList.value);
  alert(`提交成功:${JSON.stringify(formList.value)}`);
};
</script>

4. 父子组件通信(props + emit)

<!-- 子组件 Child.vue -->
<template>
  <div class="child" style="padding: 20px; border: 1px solid #ccc; margin-top: 20px;">
    <h3>子组件</h3>
    <p>父组件传递的值:{{ parentMsg }}</p>
    <button @click="handleSend">向父组件传值</button>
  </div>
</template>

<script setup>
import { defineProps, defineEmits } from 'vue';

// 接收父组件props
const props = defineProps({
  parentMsg: {
    type: String,
    default: ''
  }
});

// 定义emit事件
const emit = defineEmits(['child-send']);

// 向父组件传值
const handleSend = () => {
  emit('child-send', '我是子组件传递的消息');
};
</script>

<!-- 父组件 App.vue -->
<template>
  <div style="padding: 50px;">
    <h3>父组件</h3>
    <input 
      v-model="parentMsg" 
      placeholder="请输入要传递给子组件的内容"
      style="padding: 8px;"
    >
    <Child 
      :parent-msg="parentMsg"
      @child-send="handleChildMsg"
    />
    <p style="margin-top: 10px;">子组件传递的值:{{ childMsg }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import Child from './Child.vue';

const parentMsg = ref('初始消息');
const childMsg = ref('');

// 接收子组件消息
const handleChildMsg = (msg) => {
  childMsg.value = msg;
};
</script>

5. 自定义v-model

<!-- 子组件 CustomInput.vue -->
<template>
  <input 
    :value="modelValue" 
    @input="handleInput"
    placeholder="自定义v-model"
    style="padding: 8px;"
  >
</template>

<script setup>
import { defineProps, defineEmits } from 'vue';

// 接收v-model的值(默认是modelValue)
const props = defineProps({
  modelValue: {
    type: String,
    default: ''
  }
});

// 触发更新事件(默认是update:modelValue)
const emit = defineEmits(['update:modelValue']);

const handleInput = (e) => {
  emit('update:modelValue', e.target.value);
};
</script>

<!-- 使用示例(App.vue) -->
<template>
  <div style="padding: 50px;">
    <CustomInput v-model="inputValue" />
    <p>输入的值:{{ inputValue }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import CustomInput from './CustomInput.vue';

const inputValue = ref('');
</script>

6. 倒计时组件

<template>
  <div class="countdown" style="padding: 50px; font-size: 20px;">
    <button 
      @click="startCountdown" 
      :disabled="counting"
      style="padding: 8px 16px; margin-right: 10px;"
    >
      {{ counting ? `${count}秒后重新获取` : '开始倒计时' }}
    </button>
  </div>
</template>

<script setup>
import { ref, onUnmounted } from 'vue';

const count = ref(60);
const counting = ref(false);
let timer = null;

// 开始倒计时
const startCountdown = () => {
  if (counting.value) return;
  counting.value = true;
  timer = setInterval(() => {
    count.value--;
    if (count.value <= 0) {
      clearInterval(timer);
      count.value = 60;
      counting.value = false;
    }
  }, 1000);
};

// 组件卸载时清除定时器
onUnmounted(() => {
  if (timer) clearInterval(timer);
});
</script>

7. 购物车组件

<template>
  <div class="cart" style="width: 600px; margin: 50px auto;">
    <h3>购物车</h3>
    <table border="1" width="100%" cellpadding="10" cellspacing="0">
      <thead>
        <tr>
          <th>商品名称</th>
          <th>单价</th>
          <th>数量</th>
          <th>小计</th>
          <th>操作</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="(item, index) in cartList" :key="item.id">
          <td>{{ item.name }}</td>
          <td>¥{{ item.price }}</td>
          <td>
            <button @click="changeCount(item, -1)" :disabled="item.count <= 1">-</button>
            <span style="margin: 0 10px;">{{ item.count }}</span>
            <button @click="changeCount(item, 1)">+</button>
          </td>
          <td>¥{{ (item.price * item.count).toFixed(2) }}</td>
          <td>
            <button @click="removeItem(index)" style="color: red;">删除</button>
          </td>
        </tr>
      </tbody>
    </table>
    <div style="margin-top: 20px; text-align: right;">
      总价:¥{{ totalPrice.toFixed(2) }}
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue';

// 初始化购物车数据
const cartList = ref([
  { id: 1, name: '商品1', price: 99.9, count: 1 },
  { id: 2, name: '商品2', price: 199.9, count: 1 },
  { id: 3, name: '商品3', price: 299.9, count: 1 }
]);

// 改变数量
const changeCount = (item, num) => {
  item.count += num;
  if (item.count < 1) item.count = 1;
};

// 删除商品
const removeItem = (index) => {
  cartList.value.splice(index, 1);
};

// 计算总价
const totalPrice = computed(() => {
  return cartList.value.reduce((sum, item) => {
    return sum + item.price * item.count;
  }, 0);
});
</script>

8. TodoList组件

<template>
  <div class="todo-list" style="width: 400px; margin: 50px auto;">
    <h3>TodoList</h3>
    <!-- 新增 -->
    <div style="display: flex; gap: 10px; margin-bottom: 20px;">
      <input 
        v-model="inputValue" 
        placeholder="请输入待办事项"
        style="flex: 1; padding: 8px;"
        @keyup.enter="addTodo"
      >
      <button @click="addTodo">添加</button>
    </div>
    <!-- 列表 -->
    <div class="todo-items">
      <div 
        v-for="(todo, index) in todoList" 
        :key="todo.id"
        style="display: flex; align-items: center; padding: 8px; border-bottom: 1px solid #eee;"
      >
        <input 
          type="checkbox" 
          v-model="todo.done"
          style="margin-right: 10px;"
        >
        <span :style="{ textDecoration: todo.done ? 'line-through' : 'none', color: todo.done ? '#999' : '#333' }">
          {{ todo.content }}
        </span>
        <button 
          @click="deleteTodo(index)"
          style="margin-left: auto; color: red; border: none; background: transparent; cursor: pointer;"
        >
          删除
        </button>
      </div>
    </div>
    <!-- 统计 -->
    <div style="margin-top: 20px; display: flex; justify-content: space-between;">
      <span>已完成:{{ doneCount }} / 总数:{{ todoList.length }}</span>
      <button @click="clearDone" style="color: #666;">清空已完成</button>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue';

const inputValue = ref('');
const todoList = ref([
  { id: 1, content: '学习Vue', done: false },
  { id: 2, content: '手撕组件', done: true }
]);

// 添加待办
const addTodo = () => {
  const content = inputValue.value.trim();
  if (!content) return;
  todoList.value.push({
    id: Date.now(),
    content,
    done: false
  });
  inputValue.value = '';
};

// 删除待办
const deleteTodo = (index) => {
  todoList.value.splice(index, 1);
};

// 已完成数量
const doneCount = computed(() => {
  return todoList.value.filter(todo => todo.done).length;
});

// 清空已完成
const clearDone = () => {
  todoList.value = todoList.value.filter(todo => !todo.done);
};
</script>

三、React 核心组件(Hooks)

1. 带插槽的卡片组件(Card.jsx)

import React from 'react';
import './Card.css';

const Card = ({ width = 300, children, header, footer }) => {
  return (
    <div className="card" style={{ width: `${width}px` }}>
      {header && <div className="card-header">{header}</div>}
      <div className="card-body">{children}</div>
      {footer && <div className="card-footer">{footer}</div>}
    </div>
  );
};

export default Card;

// Card.css
.card {
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.card-header {
  padding: 16px;
  border-bottom: 1px solid #e5e7eb;
  font-weight: 600;
}
.card-body {
  padding: 16px;
}
.card-footer {
  padding: 16px;
  border-top: 1px solid #e5e7eb;
  color: #666;
}

// 使用示例(App.jsx)
import React from 'react';
import Card from './Card';

function App() {
  return (
    <div style={{ padding: '50px' }}>
      <Card width={400} header="卡片标题" footer="卡片底部 - 2025/12/27">
        <div>卡片主体内容</div>
      </Card>
    </div>
  );
}

export default App;

2. 模态框组件(Modal.jsx)

import React from 'react';
import './Modal.css';

const Modal = ({ visible, title = '提示', closeOnMask = true, onClose, children, footer }) => {
  if (!visible) return null;

  // 点击遮罩关闭
  const handleMaskClick = () => {
    if (closeOnMask) {
      onClose?.();
    }
  };

  return (
    <>
      <div className="modal-mask" onClick={handleMaskClick}></div>
      <div className="modal-container">
        <div className="modal-content" onClick={(e) => e.stopPropagation()}>
          <div className="modal-header">
            <h3 className="modal-title">{title}</h3>
            <button className="modal-close" onClick={onClose}>×</button>
          </div>
          <div className="modal-body">{children}</div>
          {footer && <div className="modal-footer">{footer}</div>}
        </div>
      </div>
    </>
  );
};

export default Modal;

// Modal.css
.modal-mask {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0,0,0,0.5);
  z-index: 1000;
}
.modal-container {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 1001;
}
.modal-content {
  width: 400px;
  background: white;
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.modal-header {
  padding: 16px;
  border-bottom: 1px solid #e5e7eb;
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.modal-title {
  margin: 0;
  font-size: 16px;
}
.modal-close {
  background: transparent;
  border: none;
  font-size: 20px;
  cursor: pointer;
  color: #999;
}
.modal-close:hover {
  color: #333;
}
.modal-body {
  padding: 16px;
}
.modal-footer {
  padding: 16px;
  border-top: 1px solid #e5e7eb;
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}

// 使用示例(App.jsx)
import React, { useState } from 'react';
import Modal from './Modal';

function App() {
  const [modalVisible, setModalVisible] = useState(false);

  const handleConfirm = () => {
    alert('确认操作');
    setModalVisible(false);
  };

  return (
    <div style={{ padding: '50px' }}>
      <button onClick={() => setModalVisible(true)}>打开弹窗</button>
      <Modal
        visible={modalVisible}
        title="自定义标题"
        onClose={() => setModalVisible(false)}
        footer={
          <>
            <button onClick={() => setModalVisible(false)}>取消</button>
            <button onClick={handleConfirm}>确认</button>
          </>
        }
      >
        <div>弹窗内容</div>
      </Modal>
    </div>
  );
}

export default App;

3. 动态表单(可新增/删除表单项)

import React, { useState } from 'react';

function DynamicForm() {
  const [formList, setFormList] = useState([{ value: '' }]);

  // 新增表单项
  const addItem = () => {
    setFormList([...formList, { value: '' }]);
  };

  // 删除表单项
  const removeItem = (index) => {
    if (formList.length <= 1) {
      alert('至少保留一个表单项');
      return;
    }
    const newList = [...formList];
    newList.splice(index, 1);
    setFormList(newList);
  };

  // 输入变更
  const handleInputChange = (index, value) => {
    const newList = [...formList];
    newList[index].value = value;
    setFormList(newList);
  };

  // 提交表单
  const submitForm = () => {
    const values = formList.map(item => item.value.trim()).filter(Boolean);
    if (values.length === 0) {
      alert('请填写至少一个表单项');
      return;
    }
    console.log('表单数据:', formList);
    alert(`提交成功:${JSON.stringify(formList)}`);
  };

  return (
    <div style={{ padding: '50px' }}>
      {formList.map((item, index) => (
        <div key={index} style={{ marginBottom: '10px' }}>
          <input
            value={item.value}
            onChange={(e) => handleInputChange(index, e.target.value)}
            placeholder="请输入内容"
            style={{ padding: '8px', marginRight: '10px' }}
          />
          <button onClick={() => removeItem(index)} style={{ color: 'red' }}>删除</button>
        </div>
      ))}
      <button onClick={addItem} style={{ marginRight: '10px' }}>新增表单项</button>
      <button onClick={submitForm}>提交</button>
    </div>
  );
}

export default DynamicForm;

4. 父子组件通信(props + 回调)

// 子组件 Child.jsx
import React from 'react';

const Child = ({ parentMsg, onChildSend }) => {
  const handleSend = () => {
    onChildSend('我是子组件传递的消息');
  };

  return (
    <div style={{ padding: '20px', border: '1px solid #ccc', marginTop: '20px' }}>
      <h3>子组件</h3>
      <p>父组件传递的值:{parentMsg}</p>
      <button onClick={handleSend}>向父组件传值</button>
    </div>
  );
};

export default Child;

// 父组件 App.jsx
import React, { useState } from 'react';
import Child from './Child';

function App() {
  const [parentMsg, setParentMsg] = useState('初始消息');
  const [childMsg, setChildMsg] = useState('');

  const handleChildMsg = (msg) => {
    setChildMsg(msg);
  };

  return (
    <div style={{ padding: '50px' }}>
      <h3>父组件</h3>
      <input
        value={parentMsg}
        onChange={(e) => setParentMsg(e.target.value)}
        placeholder="请输入要传递给子组件的内容"
        style={{ padding: '8px' }}
      />
      <Child parentMsg={parentMsg} onChildSend={handleChildMsg} />
      <p style={{ marginTop: '10px' }}>子组件传递的值:{childMsg}</p>
    </div>
  );
}

export default App;

5. 双向绑定(useState + onChange)

import React, { useState } from 'react';

function CustomInput() {
  const [inputValue, setInputValue] = useState('');

  return (
    <div style={{ padding: '50px' }}>
      <input
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        placeholder="双向绑定示例"
        style={{ padding: '8px' }}
      />
      <p>输入的值:{inputValue}</p>
    </div>
  );
}

export default CustomInput;

6. 倒计时组件

import React, { useState, useEffect } from 'react';

function Countdown() {
  const [count, setCount] = useState(60);
  const [counting, setCounting] = useState(false);

  // 清除定时器
  useEffect(() => {
    let timer = null;
    if (counting) {
      timer = setInterval(() => {
        setCount(prev => {
          if (prev <= 1) {
            setCounting(false);
            return 60;
          }
          return prev - 1;
        });
      }, 1000);
    }
    return () => clearInterval(timer);
  }, [counting]);

  const startCountdown = () => {
    if (!counting) {
      setCounting(true);
    }
  };

  return (
    <div style={{ padding: '50px', fontSize: '20px' }}>
      <button
        onClick={startCountdown}
        disabled={counting}
        style={{ padding: '8px 16px', marginRight: '10px' }}
      >
        {counting ? `${count}秒后重新获取` : '开始倒计时'}
      </button>
    </div>
  );
}

export default Countdown;

7. 购物车组件

import React, { useState, useMemo } from 'react';

function Cart() {
  const [cartList, setCartList] = useState([
    { id: 1, name: '商品1', price: 99.9, count: 1 },
    { id: 2, name: '商品2', price: 199.9, count: 1 },
    { id: 3, name: '商品3', price: 299.9, count: 1 }
  ]);

  // 改变数量
  const changeCount = (id, num) => {
    setCartList(
      cartList.map(item => {
        if (item.id === id) {
          const newCount = item.count + num;
          return { ...item, count: newCount < 1 ? 1 : newCount };
        }
        return item;
      })
    );
  };

  // 删除商品
  const removeItem = (id) => {
    setCartList(cartList.filter(item => item.id !== id));
  };

  // 计算总价
  const totalPrice = useMemo(() => {
    return cartList.reduce((sum, item) => sum + item.price * item.count, 0);
  }, [cartList]);

  return (
    <div style={{ width: '600px', margin: '50px auto' }}>
      <h3>购物车</h3>
      <table border="1" width="100%" cellpadding="10" cellspacing="0">
        <thead>
          <tr>
            <th>商品名称</th>
            <th>单价</th>
            <th>数量</th>
            <th>小计</th>
            <th>操作</th>
          </tr>
        </thead>
        <tbody>
          {cartList.map(item => (
            <tr key={item.id}>
              <td>{item.name}</td>
              <td>¥{item.price}</td>
              <td>
                <button onClick={() => changeCount(item.id, -1)} disabled={item.count <= 1}>-</button>
                <span style={{ margin: '0 10px' }}>{item.count}</span>
                <button onClick={() => changeCount(item.id, 1)}>+</button>
              </td>
              <td>¥{(item.price * item.count).toFixed(2)}</td>
              <td>
                <button onClick={() => removeItem(item.id)} style={{ color: 'red' }}>删除</button>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
      <div style={{ marginTop: '20px', textAlign: 'right' }}>
        总价:¥{totalPrice.toFixed(2)}
      </div>
    </div>
  );
}

export default Cart;

8. TodoList组件

import React, { useState, useMemo } from 'react';

function TodoList() {
  const [inputValue, setInputValue] = useState('');
  const [todoList, setTodoList] = useState([
    { id: 1, content: '学习React', done: false },
    { id: 2, content: '手撕组件', done: true }
  ]);

  // 添加待办
  const addTodo = () => {
    const content = inputValue.trim();
    if (!content) return;
    setTodoList([
      ...todoList,
      { id: Date.now(), content, done: false }
    ]);
    setInputValue('');
  };

  // 删除待办
  const deleteTodo = (id) => {
    setTodoList(todoList.filter(item => item.id !== id));
  };

  // 切换完成状态
  const toggleDone = (id) => {
    setTodoList(
      todoList.map(item => 
        item.id === id ? { ...item, done: !item.done } : item
      )
    );
  };

  // 清空已完成
  const clearDone = () => {
    setTodoList(todoList.filter(item => !item.done));
  };

  // 已完成数量
  const doneCount = useMemo(() => {
    return todoList.filter(item => item.done).length;
  }, [todoList]);

  return (
    <div style={{ width: '400px', margin: '50px auto' }}>
      <h3>TodoList</h3>
      {/* 新增 */}
      <div style={{ display: 'flex', gap: '10px', marginBottom: '20px' }}>
        <input
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          placeholder="请输入待办事项"
          style={{ flex: 1, padding: '8px' }}
          onKeyDown={(e) => e.key === 'Enter' && addTodo()}
        />
        <button onClick={addTodo}>添加</button>
      </div>
      {/* 列表 */}
      <div>
        {todoList.map(item => (
          <div
            key={item.id}
            style={{ display: 'flex', alignItems: 'center', padding: '8px', borderBottom: '1px solid #eee' }}
          >
            <input
              type="checkbox"
              checked={item.done}
              onChange={() => toggleDone(item.id)}
              style={{ marginRight: '10px' }}
            />
            <span
              style={{
                textDecoration: item.done ? 'line-through' : 'none',
                color: item.done ? '#999' : '#333'
              }}
            >
              {item.content}
            </span>
            <button
              onClick={() => deleteTodo(item.id)}
              style={{ marginLeft: 'auto', color: 'red', border: 'none', background: 'transparent', cursor: 'pointer' }}
            >
              删除
            </button>
          </div>
        ))}
      </div>
      {/* 统计 */}
      <div style={{ marginTop: '20px', display: 'flex', justifyContent: 'space-between' }}>
        <span>已完成:{doneCount} / 总数:{todoList.length}</span>
        <button onClick={clearDone} style={{ color: '#666' }}>清空已完成</button>
      </div>
    </div>
  );
}

export default TodoList;

核心说明

  1. 所有代码均保证可直接运行,无第三方依赖(仅Vue/React核心库)
  2. 原生JS部分:聚焦核心逻辑,样式精简但完整,适配常见场景
  3. Vue部分:基于Vue3组合式API(setup语法),符合最新开发规范
  4. React部分:基于Hooks(useState/useEffect/useMemo),函数式组件写法
  5. 每个组件都包含核心功能+使用示例,可直接手撕到面试场景中

面试手撕时,可根据时间精简样式,重点实现核心逻辑(如轮播图的切换、表单验证的规则、双向绑定的核心原理等)。

总结

到此这篇关于前端面试手撕合集之纯HTML、Vue和React组件的文章就介绍到这了,更多相关前端纯HTML、Vue和React组件内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 基于canvas实现手写签名(vue)

    基于canvas实现手写签名(vue)

    这篇文章主要为大家详细介绍了基于canvas实现简易的手写签名,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-05-05
  • 详解vue生命周期

    详解vue生命周期

    这篇文章主要为大家介绍了vue的生命周期,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能够给你带来帮助
    2021-11-11
  • Vue前端在线预览文件插件使用详解

    Vue前端在线预览文件插件使用详解

    文章介绍了两种用于在线预览文档的插件:view.xdocin和view.officeapps.live,view.xdocin插件支持在线预览并延长使用时间,但不支持下载,view.officeapps.live插件是微软插件,但预览速度较慢,且有时会出现翻页问题,两种插件都适用于不需要下载和安装软件的场景
    2026-01-01
  • Vue使用z-tree处理大数量的数据以及生成树状结构

    Vue使用z-tree处理大数量的数据以及生成树状结构

    这篇文章主要介绍了Vue使用z-tree处理大数量的数据以及生成树状结构方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-04-04
  • vue简单的store详解

    vue简单的store详解

    这篇文章主要介绍了详解vue简单的store,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-10-10
  • Vue3实现组件通信的14种方式详解与实战

    Vue3实现组件通信的14种方式详解与实战

    在Vue3中,组件通信仍然是一个非常重要的话题,因为在大多数应用程序中,不同的组件之间需要进行数据传递和交互,在Vue3中,组件通信有多种方式可供选择,本文给大家介绍了Vue3实现组件通信的14种方式,需要的朋友可以参考下
    2025-08-08
  • vue之input输入框防抖debounce函数的使用方式

    vue之input输入框防抖debounce函数的使用方式

    这篇文章主要介绍了vue之input输入框防抖debounce函数的使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-11-11
  • vue实现检测敏感词过滤组件的多种思路

    vue实现检测敏感词过滤组件的多种思路

    这篇文章主要介绍了vue编写检测敏感词汇组件的多种思路,帮助大家更好的理解和学习使用vue框架,感兴趣的朋友可以了解下
    2021-04-04
  • vue项目中使用rem替换px的实现示例

    vue项目中使用rem替换px的实现示例

    移动端页面适配,rem和vw适配方案,本文主要介绍了vue项目中使用rem替换px的实现示例,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-07-07
  • VUEJS实战之构建基础并渲染出列表(1)

    VUEJS实战之构建基础并渲染出列表(1)

    这篇文章主要为大家详细介绍了VUEJS实战之构建基础并渲染出列表,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-06-06

最新评论