【AJAX项目】黑马头条——数据管理平台

目录

[一、 项目准备](#一、 项目准备)

[二、 验证码登录](#二、 验证码登录)

真正的验证码登录原理:

[三、 token的介绍](#三、 token的介绍)

概念:访问权限的令牌,本质上是一串字符串

创建:正确登录后,由后端签发并返回

作用:判断是否有登录状态等,控制访问权限

四、个人信息设置和axios请求拦截器

五、axios响应拦截器

优化-axios响应结果

[六、发布文章 - 富文本编辑器 (wangEditor插件)](#六、发布文章 - 富文本编辑器 (wangEditor插件))

七、发布文章-频道列表

[八、发布文章 - 封面设置](#八、发布文章 - 封面设置)

[九、发布文章 - 收集并保存](#九、发布文章 - 收集并保存)

[十、内容管理 - 文章列表展示](#十、内容管理 - 文章列表展示)

[十一、内容管理 - 筛选功能](#十一、内容管理 - 筛选功能)

[十二、内容管理 - 分页功能](#十二、内容管理 - 分页功能)

[十三、内容管理 - 删除功能](#十三、内容管理 - 删除功能)

十四、内容管理-编辑文章-回显

十五、编辑文字-保存

[总结不易~ 本章节对我有很大收获,希望对你也是!!!](#总结不易~ 本章节对我有很大收获,希望对你也是!!!)


本节素材已上传至Gitee:ajax_study: 这是ajax、Node.j学习的仓库 - Gitee.comhttps://gitee.com/liu-yihao-hhh/ajax_study/tree/master/%E9%A1%B9%E7%9B%AE-%E6%95%B0%E6%8D%AE%E7%AE%A1%E7%90%86

一、 项目准备

技术:

  • 基于BootStrap 搭建网站标签和样式
  • 集成wangEditor插件实现富文本编辑器
  • 使用原生JS完成增删改查等业务
  • 基于axios与黑马头条线上接口交互
  • 使用axios拦截器进行权限判断

包含html, css, js, 静态图片, 第三方插件

目录管理:

  • assets: 资源文件夹(图片、字体等)
  • lib: 资料文件夹(第三方插件,例如:form-serialize)
  • page: 页面文件夹
  • utils: 实用程序文件夹(工具插件)

二、 验证码登录

  • 目标:完成验证码登录,后端设置验证码默认为246810
  • 原因:因为短信接口不是免费的,防止攻击者恶意盗刷

步骤:

  • 再utils/request.js配置axios请求基地址
  • 作用:提取公共前缀地址,配置后axios请求时都会baseURL + url 防止项目太大,基地址发生变化会导致所有页面请求数据地址全要变动
  • 收集手机号和验证码数据
  • 基于axios调用验证码登录接口
  • 使用BootStrap的Alert警告框反馈结果给用户

收集表单数据,上传到服务器,然后通过try......catch......来判断是否请求成功,通过myAlert函数来进行登录效果弹窗

javascript 复制代码
// 1.2 收集手机号和验证码数据
document.querySelector('.btn').addEventListener('click', async () => {
  const form = document.querySelector('.login-form')
  const data = serialize(form, { hash: true, empty: true })
  console.log(data)

  try {
    // 1.3 基于 axios 调用验证码登录接口
    const response = await axios.post('/v1_0/authorizations', data)
    const result = response.data
    console.log('#', result)
    myAlert(true, '登录成功')
  } catch (error) {
    myAlert(false, error.response.data.message)
  }
})

alert检查验证码和手机号是否正确的弹窗

javascript 复制代码
// 弹窗插件
// 需要先准备 alert 样式相关的 DOM
/**
 * BS 的 Alert 警告框函数,2秒后自动消失
 * @param {*} isSuccess 成功 true,失败 false
 * @param {*} msg 提示消息
 */
function myAlert(isSuccess, msg) {
  const myAlert = document.querySelector('.alert')
  myAlert.classList.add(isSuccess ? 'alert-success' : 'alert-danger')
  myAlert.innerHTML = msg
  myAlert.classList.add('show')

  setTimeout(() => {
    myAlert.classList.remove(isSuccess ? 'alert-success' : 'alert-danger')
    myAlert.innerHTML = ''
    myAlert.classList.remove('show')
  }, 2000)
}

真正的验证码登录原理:

  1. 用户操作触发:用户在登录界面输入手机号码,主动点击 "发送验证码" 按钮,发起获取验证码流程,这是整个登录校验的起始交互 。
  2. 调用发送接口:系统携带用户输入的手机号码,调用专门用于发送短信验证码的服务器接口,传递手机号码参数,请求服务端执行发送验证码前置操作 。
  3. 服务端生成校验码:服务器接收到请求后,为该手机号码生成唯一的验证码,同时记录验证码生成时间,将这些信息存储在服务器,用于后续验证匹配 。
  4. 调用运营商通道:服务器携带手机号码,调用运营商(电信、移动、联通 )提供的短信发送接口,借助运营商网络能力传递验证码下发指令 。
  5. 基站下发短信:运营商通过基站,以无连接的短信方式,将包含验证码的信息发送到指定手机号码对应的终端设备 。
  6. 反馈发送状态:运营商将短信发送结果(成功 / 失败等 )返回给服务器,告知验证码是否已成功进入下发流程 。
  7. 返回前端结果:服务器把验证码发送成功的状态反馈给前端页面,让用户知晓验证码已尝试下发 。
  8. 用户填写验证码:用户查看手机收到的短信,获取验证码内容后,手动填写到登录页面的验证码输入框,完成信息回填 。
  9. 发起登录验证:用户点击 "登录",系统携带手机号码和填写的验证码,调用服务器的验证码登录验证接口,提交验证请求 。
  10. 服务端校验逻辑:服务器接收请求后,拿收到的手机号码、验证码,与第 3 步生成存储的验证码记录(含号码、验证码、生成时间 )比对,校验验证码是否正确、是否在有效期(基于生成时间判断 ),然后返回登录成功或失败结果 。

三、 token的介绍

概念:访问权限的令牌,本质上是一串字符串

创建:正确登录后,由后端签发并返回

复制代码
eyJzX0BAiOiJkV1QLChbc6IojT2UziIN19.eyJpc3MiOiJodHRwczovL3dL5dy5pdmNhc3RQVyJ2F1iOiJ3iCVjIof0wIY2THNnGEYJN5CO
UZDYLT110WOdGQYZ2ZIzUYJtxW11amR10j0wWZMMNJet2UY5DGYz3L7G4MKcAT2M2K62KtgNDcz21YtwFUj0xJ0nK
wJKxLcJleHAiOjE20DE20YOT9FJkvIGauMp1Z6w.co0eqX4SCR6v8VbouuPzYVw84

作用:判断是否有登录状态等,控制访问权限

注意:前端只能判断token有无,而后端才能判断token的有效性

token的使用

目标:只有登录状态,才可以访问内容页面

步骤:

  1. 在 utils/auth.js 中判断无 token 令牌字符串,则强制跳转到登录页 (手动修改地址栏测试)
  2. 在登录成功后,保存 token 令牌字符串到本地,再跳转到首页 (手动修改地址栏测试)

观察一下没有进行登录,也就没用本地存储的token,会被踢回登录页面

javascript 复制代码
// 1.1 判断无 token 令牌字符串,则强制跳转到登录页
const token = localStorage.getItem('token')
if(!token) {
  localStorage.href = '../login/index.html'
}

登录成功后,将当前服务器返回的token进行本地存储

javascript 复制代码
// 1.2 收集手机号和验证码数据
document.querySelector('.btn').addEventListener('click', async () => {
  const form = document.querySelector('.login-form')
  const data = serialize(form, { hash: true, empty: true })
  console.log(data)

  try {
    // 1.3 基于 axios 调用验证码登录接口
    const response = await axios.post('/v1_0/authorizations', data)
    const result = response.data
    console.log('#', result)
    myAlert(true, '登录成功')
    // 登录成功后 保存token令牌到本地 并跳转到列表表表面
    localStorage.setItem('token', result.data.token)
    console.log(result.data)
    setTimeout(() => {
      // 延迟跳转 让alert警告框停留一会
      location.href = '../content/index.html'
    }, 1500)

  } catch (error) {
    myAlert(false, error.response.data.message)
  }
})

四、个人信息设置和axios请求拦截器

axios请求拦截器:发起请求之前,触发的配置函数,对请求参数进行额外配置

javascript 复制代码
axios.interceptors.request.use(function (config) {
    const token = localStorage.getItem('token');
    token && (config.headers.Authorization = `Bearer ${token}`);
    // 在发送请求之前做些什么
    return config;
}, function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
});
  • 身份验证:如上述例子,在每次请求发送到服务器前,检查并添加用户的身份验证 token,确保只有已登录且拥有合法权限的用户请求才能被服务器处理 。
  • 请求参数统一处理:可以在拦截器中对所有请求的参数进行统一的格式化、添加公共参数(如时间戳、设备标识等) 等操作。
  • 请求头统一设置 :除了设置身份验证信息,还可以统一设置Content-TypeAccept等请求头字段,确保所有请求的格式符合服务器要求。

在request.js文件中设置axios请求拦截器

javascript 复制代码
// 目标2:设置个人信息
// 2.1 在utils/request.js 设置请求拦截器 同意携带token
// 2.2 请求个人信息并设置到页面
// utils/request.js
axios.interceptors.request.use(function (config) {
  // 统一携带 token
  const token = localStorage.getItem('token')
  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }
  return config
}, function (error) {
  return Promise.reject(error)
})

在每次 HTTP 请求发送前自动添加身份验证令牌(Token),确保后端能够识别请求者的身份。这样,所有需要身份验证的 API 请求都会自动携带 Token,无需在每个请求中手动设置。

五、axios响应拦截器

axios响应拦截器:作用是在请求返回结果(进入 then/catch 前)统一处理响应,让你不用在每个请求里重复写逻辑。

javascript 复制代码
axios.interceptors.response.use(
  function (response) { 
    // 1. 响应成功(状态码 2xx 范围)时触发
    return response; 
  },
  function (error) { 
    // 2. 响应失败(状态码非 2xx,或请求报错)时触发
    return Promise.reject(error); 
  }
);

Axios 响应拦截器会在所有请求完成后 自动触发,无论请求成功(状态码 2xx)还是失败(状态码非 2xx 或网络错误)。它的本质是一个中间层 ,在响应数据到达你的业务代码(.then().catch())之前执行。

无论哪一个页面发生响应401请求错误,都会在这段代码里进行拦截,并且跳转到登录页面。

javascript 复制代码
// 响应拦截器
axios.interceptors.response.use(
  function (response) {
    // 1. 响应成功(状态码 2xx 范围)时触发
    return response;
  },
  function (error) {
    // 2. 响应失败(状态码非 2xx,或请求报错)时触发
    console.dir(error)
    if (error?.response?.status === 401) {
      alert('身份验证失败,请重新登陆')
      localStorage.clear()
      location.href = '../login/index.html'
    }
    return Promise.reject(error);
  }
);

当我修改了token值后,后端服务器发现token值对不上了,就被axios响应拦截了。

打个比方:

  • 请求拦截器是 "快递员",负责把包裹(请求)送到仓库(后端)。
  • 后端是 "仓库安检员",检查包裹里的 token(身份)是否合法。
  • 响应拦截器是 "你",收到仓库(后端)退回的 "身份不符" 包裹(401 响应),然后决定怎么处理(提示重新登录)。

总结:

  • 响应回到then和catch之前,触发的拦截函数,对响应结果统一处理时axios响应拦截器
  • axios响应拦截器状态为2xx触发成功回调,其他则触发失败回调函数

优化-axios响应结果

axios直接接收服务器返回的响应结果 就是return response.data,返回后,其他文件得到服务器返回的数据都可以少一层.data操作

javascript 复制代码
// 响应拦截器
axios.interceptors.response.use(
  function (response) {
    // 1. 响应成功(状态码 2xx 范围)时触发
    const result = response.data
    return result
  },
  function (error) {
    // 2. 响应失败(状态码非 2xx,或请求报错)时触发
    console.dir(error)
    if (error?.response?.status === 401) {
      alert('身份验证失败,请重新登陆')
      localStorage.clear()
      location.href = '../login/index.html'
    }
    return Promise.reject(error);
  }
);

这是因为 Axios 响应拦截器的 "全局预处理" 特性 ,能在响应返回业务代码前,统一 "剥掉" response.data 这一层。

六、发布文章 - 富文本编辑器 (wangEditor插件)

富文本:带样式,多格式的文本,在前端一般使用标签配合内联样式实现

就是跟elementpuls组件库一样

javascript 复制代码
                <div id="editor---wrapper">
                  <div id="toolbar-container"><!-- 工具栏 --></div>
                  <div id="editor-container"><!-- 编辑器 --></div>
                </div>

editor.js文件

javascript 复制代码
// 富文本编辑器
// 创建编辑器函数,创建工具栏函数
const { createEditor, createToolbar } = window.wangEditor

// 编辑器配置对象
const editorConfig = {
  // 占位提示文字
  placeholder: '发布文章内容...',
  // 编辑器变化时回调函数
  onChange(editor) {
    // 获取富文本内容
    const html = editor.getHtml()
    // 也可以同步到 <textarea>
    // 为了后续快速收集整个表单内容做铺垫
    document.querySelector('.publish-content').value = html
  }
}

// 创建编辑器
const editor = createEditor({
  // 创建位置
  selector: '#editor-container',
  // 默认内容
  html: '<p><br></p>',
  // 配置项
  config: editorConfig,
  // 配置集成模式(default 全部)(simple 简洁)
  mode: 'default', // or 'simple'
})

// 工具栏配置对象
const toolbarConfig = {}

// 创建工具栏
const toolbar = createToolbar({
  // 为指定编辑器创建工具栏
  editor,
  // 工具栏创建的位置
  selector: '#toolbar-container',
  // 工具栏配置对象
  config: toolbarConfig,
  // 配置集成模式
  mode: 'default', // or 'simple'
})

七、发布文章-频道列表

很简单,就是通过axios来获取服务器的数据,然后进行数据渲染

javascript 复制代码
// 1.1 获取频道列表数据
const setChannleList = async () => {
  const res = await axios('/v1_0/channels')
  console.log(res.data)
  const htmlStr = '<option value="" selected="">请选择文章频道</option>' + res.data.channels.map(item => `<option value="${item.id}">${item.name}</option>`).join('')
  document.querySelector('.form-select').innerHTML = htmlStr
}
setChannleList()

八、发布文章 - 封面设置

axios获取图片后进行切换,对 + 进行显示和隐藏,然后为了再次能够点击进行触发,那么就要进行代码的优化,对图片进行绑定点击事件 ,当进行点击的时候,图片文件再次自动触发click()事件

javascript 复制代码
// 目标2:封面设置
// 选择文件并保存在FormData
document.querySelector('.img-file').addEventListener('change', async e => {
  const file = e.target.files[0]
  const fd = new FormData()
  fd.append('image', file)
  // 单独上传图片并得到图片 URL 网址
  const response = await axios.post('/v1_0/upload', fd)
  console.log(response)
  // 回显并切回 img 标签展示(隐藏 + 号上传标签)
  const imgUrl = response.data.url
  document.querySelector('.rounded').src = imgUrl
  document.querySelector('.rounded').classList.add('show')
  document.querySelector('.place').classList.add('hide')
})

// 优化: 点击 img 可以重新切换封面
// 思路: img 点击 => 用js方式触发文件选择元素click 事件方法
document.querySelector('.rounded').addEventListener('click', () => {
  document.querySelector('.img-file').click()
})

九、发布文章 - 收集并保存

  • 基于form-serialize插件收集表单数据对象
  • 基于axios提交到服务器保存
  • 调用Alert警告框反馈结果给用户
  • 重置表单并跳转到列表页

利用serialize插件进行表单数据获取,然后通过axios.post进行数据提交 提交成功后进行表单数据清空和页面跳转

javascript 复制代码
// 发布文章 - 收集并保存
document.querySelector('.send').addEventListener('click', async () => {
  const form = document.querySelector('.art-form')
  const data = serialize(form, { hash: true, empty: true })
  // 发布文章的时候 不需要id属性 可以删除id 为后续做编辑使用
  delete data.id
  // 自己收集封面图片地址并保存到data对象中
  data.cover = {
    type: 1, // 封面类型
    images: [document.querySelector('.rounded').src] // 封面图片 URL
  }

  try {
    // axios 提交到服务器
    const response = await axios.post('/v1_0/mp/articles', data)
    // 调用提示用户
    myAlert(true, '发布成功')
    // 重置表单
    form.reset()
    // 封面要手动重置
    document.querySelector('.rounded').src = ''
    document.querySelector('.rounded').classList.remove('show')
    document.querySelector('.place').classList.remove('hide')
    // 富文本编辑器重置
    editor.setHtml('')

    setTimeout(() => {
      location.href = '../content/index.html'
    }, 1500)
  } catch (error) {
    myAlert(false, '发布错误')
  }
})

十、内容管理 - 文章列表展示

这里第一步就是用到axios查询语法

javascript 复制代码
axios.get('/接口地址', {
  params: {
    键1: 值1,
    键2: 值2
  }
})

axios 会自动把 params 中的键值对拼接到 URL 上,形成查询字符串。
然后对html结构进行map编辑生成一个join()字符串,进行渲染

javascript 复制代码
// 1.1 准备查询参数对象
const queryObj = {
  status: '', // 文章状态 (1- 待审核 2-审核通过) 空字符串 - 全部
  channel_id: '', // 文章频道 id 空字符串 - 全部
  page: 1, // 当前页码
  per_page: 2 // 当前页面条数
}

const setArtileList = async () => {
  const response = await axios('/v1_0/mp/articles', { params: { queryObj } })
  console.log(response)
  // 展示到指定标签的结构中
  const htmlStr = response.data.results.map(item => `
      <tr>
        <td>
          <img src="${item.cover.type === 0 ? `https://img2.baidu.com/it/u=2640406343,1419332367&amp;fm=253&amp;fmt=auto&amp;app=138&amp;f=JPEG?w=708&amp;h=500 ` : item.cover.images[0]}" alt="">
        </td>
        <td>${item.title}</td>
        <td>
          ${item.status === 1 ? `<span class="badge text-bg-success">审核通过</span>` : `<span class="badge text-bg-primary">待审核</span>`}
        </td>
        <td>
          <span>${item.pubdate}</span>
        </td>
        <td>
          <span>${item.read_count}</span>
        </td>
        <td>
          <span>${item.comment_count}</span>
        </td>
        <td>
          <span>${item.like_count}</span>
        </td>
        <td>
          <i class="bi bi-pencil-square edit"></i>
          <i class="bi bi-trash3 del"></i>
        </td>
      </tr>
    `).join('')
  console.log(htmlStr)
  document.querySelector('.art-list').innerHTML = htmlStr
}

setArtileList()

十一、内容管理 - 筛选功能

  • 设置频道列表数据
  • 监听筛选条件改变,保存查询信息到查询参数对象
  • 点击筛选时, 传递查询参数对象到服务器
  • 获取匹配数据,覆盖到页面展示

通过将筛选内容进行赋值,传入服务器得到筛选后的结果进行返回,再次进行渲染

javascript 复制代码
// 1.1 准备查询参数对象
const queryObj = {
  status: '', // 文章状态 (1- 待审核 2-审核通过) 空字符串 - 全部
  channel_id: '', // 文章频道 id 空字符串 - 全部
  page: 1, // 当前页码
  per_page: 2 // 当前页面条数
}
javascript 复制代码
// 监听筛选条件改变, 保存查询信息到查询参数对象
// 筛选状态标记数字 -> change事件 -> 绑定到查询参数对象上
document.querySelectorAll('.form-check-input').forEach(radio => {
  radio.addEventListener('change', e => {
    queryObj.status = e.target.value
  })
})

// 筛选频道 id -> chenge事件 -> 绑定到查询参数对象上
document.querySelector('.form-select').addEventListener('change', e => {
  queryObj.channel_id = e.target.value
})

// 点击筛选时 传递查询参数对象到服务器
document.querySelector('.sel-btn').addEventListener('click', () => {
  setArtileList()
})

十二、内容管理 - 分页功能

  • 保存并设置文章总条数
  • 点击下一页,做临界值判断,并切换页码参数请求最新数据

首先就是要设置分页总共有多少条,才能判断上一页和下一页的临界条件

javascript 复制代码
// 目标3 分页功能
// 3.2 点击下一页 做临界判断
document.querySelector('.next').addEventListener('click', e => {
  // 当前页码小于最大页码数
  if (queryObj.page < Math.ceil(totalCount / queryObj.per_page)) {
    queryObj.page++
    setArtileList()
  }
})

// 3.3 点击上一页
document.querySelector('.last').addEventListener('click', e => {
  if (queryObj.page > 1) {
    queryObj.page--
    document.querySelector('.page-now').innerHTML = `第${queryObj.page}页`
    setArtileList()
  }
})

十三、内容管理 - 删除功能

  1. 关联文章id到删除图标
  2. 点击删除获取文章id
  3. 调用删除接口,传递文章id到服务器
  4. 重写获取文章列表,再次渲染
javascript 复制代码
// 目标四:删除文章
document.querySelector('.art-list').addEventListener('click', async e => {
  // 判断删除的元素
  if (e.target.classList.contains('del')) {
    const delId = e.target.parentNode.dataset.id
    // 4.3 调用删除接口 传递文章 id 到服务器
    console.log(delId)
    const response = await axios.delete(`/v1_0/mp/articles/${delId}`)

    // 4.5 删除最后一页的最后一条 需要自动向前翻页
    const children = document.querySelector('.art-list').children
    if (children.length === 1 && queryObj.page !== 1) {
      queryObj.page--
      document.querySelector('.page-now').innerHTML = `第${queryObj.page}页`
    }
    setArtileList()
  }
})

十四、内容管理-编辑文章-回显

  1. 页面跳转传参(URL查询参数方式)
  2. 发布文章页面接收参数判断(共用同一套表单)
  3. 修改标题和文章按钮
  4. 获取文章详情数据回显表单
javascript 复制代码
  // 进行文章的编辑
  // 自调用函数
  ; (function () {
    const paramsStr = location.search
    const parmas = new URLSearchParams(paramsStr)
    parmas.forEach(async (value, key) => {
      // 当前有要编辑的文章id 被传入过来
      console.log('#', value, key)
      if (key === 'id') {
        document.querySelector('.title span').innerHTML = '修改文章'
        document.querySelector('.send').innerHTML = '修改'
        const response = await axios(`/v1_0/mp/articles/${value}`)
        console.log(response)
        // 组织我需要的数据对象
        const dataObj = {
          channel_id: response.data.channel_id,
          title: response.data.title,
          rounded: response.data.cover.images[0],
          content: response.data.content,
          id: response.data.id
        }

        // 遍历数据对象属性 映射到页面
        Object.keys(dataObj).forEach(key => {
          if (key === 'rounded') {
            if (dataObj[key]) {
              document.querySelector('.rounded').src = dataObj[key]
              document.querySelector('.rounded').classList.add('show')
              document.querySelector('.place').classList.add('hide')
            }
            else if (key === 'content') {
              // 富文本内容
              editor.setHtml(dataObj[key])
            }
            else document.querySelector(`[name=${key}]`).value = dataObj[key]
          }
        })
      }
    })
  })()

十五、编辑文字-保存

当按钮内容为"修改"时,收集表单中的文章数据(包括标题、内容、频道等),并使用 serialize 函数将其转为对象格式。随后,通过 DOM 获取封面图片地址,并组装成符合接口要求的 cover 数据结构(包含封面类型和图片数组)。接着,调用 axios.put 向后端发送修改文章的请求,请求地址中携带文章的 id,请求体中包含完整的文章数据和封面信息。整个操作被包装在 try...catch 中,用于捕获可能出现的错误,并根据请求结果通过 myAlert 提示用户是否修改成功。

javascript 复制代码
document.querySelector('.send').addEventListener('click', async e => {
  if (e.target.innerHTML !== '修改') return
  // 修改文章
  const form = document.querySelector('.art-form')
  const data = serialize(form, { hash: true, empty: true })
  try {
    const response = await axios.put(`/v1_0/mp/articles/${data.id}`, { ...data, cover: { type: document.querySelector('.rounded').src ? 1 : 0, images: [document.querySelector('.rounded').src] } })
    console.log(response)
    myAlert(true, '修改文章成功')
  }
  catch (error) {
    myAlert(false, '修改错误')
  }

})

总结不易~ 本章节对我有很大收获,希望对你也是!!!

相关推荐
YGY Webgis糕手之路1 小时前
OpenLayers 综合案例-轨迹回放
前端·经验分享·笔记·vue·web
90后的晨仔2 小时前
🚨XSS 攻击全解:什么是跨站脚本攻击?前端如何防御?
前端·vue.js
Ares-Wang2 小时前
JavaScript》》JS》 Var、Let、Const 大总结
开发语言·前端·javascript
90后的晨仔2 小时前
Vue 模板语法完全指南:从插值表达式到动态指令,彻底搞懂 Vue 模板语言
前端·vue.js
德育处主任2 小时前
p5.js 正方形square的基础用法
前端·数据可视化·canvas
烛阴2 小时前
Mix - Bilinear Interpolation
前端·webgl
90后的晨仔2 小时前
Vue 3 应用实例详解:从 createApp 到 mount,你真正掌握了吗?
前端·vue.js
德育处主任2 小时前
p5.js 矩形rect绘制教程
前端·数据可视化·canvas
前端工作日常3 小时前
我学习到的babel插件移除Flow 类型注解效果
前端·babel·前端工程化
SY_FC3 小时前
uniapp input 聚焦时键盘弹起滚动到对应的部分
javascript·vue.js·elementui