阅读视图

发现新文章,点击刷新页面。

若依前端vue 项目 build 文件夹内容详解

1 build

B站有完整的前端后端源码解析视频课程

image.png

你完全可以不使用这个 build/index.js 脚本,直接用 Vue CLI 自带的 npm run build 命令进行打包,两者的核心打包逻辑完全一致,最终生成的 dist 目录内容也完全相同。

为什么可以不使用这个脚本?

因为这个 build/index.js 本质上是对 Vue CLI 原生打包命令的「封装和扩展」,而非“必须依赖的打包工具”:

  • 脚本中最核心的打包逻辑是 run('vue-cli-service build'),这和 npm run build 底层执行的命令完全一样(Vue CLI 项目中,package.jsonbuild 脚本默认就是 "vue-cli-service build");
  • 它的额外功能(如 --preview 预览、--report 生成报告)只是“锦上添花”,而非打包的必要步骤。

两种打包方式的对比(选哪种都可以)

打包方式 命令 效果 适用场景
原生方式 npm run build 仅执行打包,生成 dist 目录 只需要打包结果,不需要预览
脚本方式 node build/index.jsnode build/index.js --preview 执行打包 + 可选预览/生成报告 需要打包后快速预览,或分析打包体积

总结

  • 完全可以不用:直接用 npm run build 就能完成打包,这是 Vue CLI 推荐的标准方式,简单直接;
  • 脚本是可选工具:它的存在只是为了提供“打包+预览”的一站式体验,如果你不需要这个功能,忽略它即可,对项目没有任何影响。

选择哪种方式,完全取决于你的开发习惯——想用原生命令就用 npm run build,想方便预览就用脚本,两者最终的打包结果完全一致。


// 导入runjs工具的run函数,用于执行终端命令
const { run } = require('runjs')
// 导入chalk工具,用于在终端输出带颜色的文字
const chalk = require('chalk')
// 导入vue项目的配置文件,用于获取项目的基础路径配置
const config = require('../vue.config.js')
// 获取命令行参数(去掉前两个默认参数:node和当前文件路径)
const rawArgv = process.argv.slice(2)
// 将命令行参数拼接成字符串,方便后续传递给打包命令
const args = rawArgv.join(' ')

// 判断是否需要执行"打包并预览"功能:
// 1. 检查是否有npm_config_preview环境变量(通过npm run命令传递)
// 2. 检查命令行参数中是否包含--preview
if (process.env.npm_config_preview || rawArgv.includes('--preview')) {
  // 检查是否需要生成构建分析报告(命令行参数包含--report时)
  const report = rawArgv.includes('--report')

  // 执行Vue CLI的打包命令,同时传递所有命令行参数
  // 等价于在终端执行vue-cli-service build [参数]
  run(`vue-cli-service build ${args}`)

  // 预览服务器的端口号(固定为9526)
  const port = 9526
  // 从vue配置中获取项目的公共路径(publicPath),用于正确拼接预览地址
  const publicPath = config.publicPath

  // 导入connect框架,用于创建本地服务器
  var connect = require('connect')
  // 导入serve-static中间件,用于托管静态文件
  var serveStatic = require('serve-static')
  // 创建connect服务器实例
  const app = connect()

  // 配置服务器:
  // 1. 以vue配置的publicPath为基础路径
  // 2. 托管当前目录下的dist文件夹(打包后的静态文件)
  // 3. 设置默认首页为index.html
  app.use(
    publicPath,
    serveStatic('./dist', {
      index: ['index.html', '/']
    })
  )

  // 启动服务器,监听指定端口
  app.listen(port, function () {
    // 在终端输出绿色的预览地址,方便开发者直接访问
    console.log(chalk.green(`> Preview at  http://localhost:${port}${publicPath}`))
    // 如果需要生成报告,额外输出报告的访问地址
    if (report) {
      console.log(chalk.green(`> Report at  http://localhost:${port}${publicPath}report.html`))
    }
  })
} else {
  // 如果不需要预览,仅执行打包命令(与npm run build效果相同)
  run(`vue-cli-service build ${args}`)
}

若依前端vue 项目 里面dragWidth.js 文件详解

以下是基于该指令核心逻辑扩展的3个实用案例,覆盖不同场景,附带完整代码和使用说明:

完整笔记直接主页联系,或者B站有完整视频

image.png

案例1:左右分栏联动调整(适用于编辑器、后台布局)

场景:在左右分栏布局中(如左侧菜单+右侧内容、编辑区+预览区),拖拽中间手柄同时调整两侧宽度,保持总宽度不变。

image.png

指令代码(directive/columnResize.js

image.png


export default {
  bind(el) {
    // 查找左右分栏元素(需在模板中定义对应类名)
    const leftDom = el.querySelector('.left-column');
    const rightDom = el.querySelector('.right-column');
    if (!leftDom || !rightDom) {
      console.error('未找到左右分栏元素,请添加 .left-column 和 .right-column 类名');
      return;
    }

    // 获取父容器总宽度(左右分栏总宽 = 父容器宽)
    const parentWidth = el.offsetWidth;

    // 创建中间拖拽手柄
    const lineEl = document.createElement('div');
    lineEl.style = `
      width: 5px; 
      height: 100%; 
      background: #e0e0e0; 
      position: absolute; 
      left: ${leftDom.offsetWidth}px; /* 初始位置在左栏右侧 */
      top: 0; 
      z-index: 10; 
      cursor: col-resize; /* 水平调整光标 */
      transition: background 0.2s;
    `;
    // 鼠标悬停时高亮手柄
    lineEl.onmouseover = () => lineEl.style.background = '#ccc';
    lineEl.onmouseout = () => lineEl.style.background = '#e0e0e0';

    // 绑定鼠标按下事件
    lineEl.addEventListener('mousedown', (e) => {
      e.preventDefault();
      // 记录鼠标按下时的X坐标和左栏宽度
      const startX = e.clientX;
      const startLeftWidth = leftDom.offsetWidth;

      // 鼠标移动时调整宽度
      const handleMouseMove = (e) => {
        e.preventDefault();
        // 计算左栏宽度变化量(限制最小宽度为200px,最大为父容器的80%)
        const deltaX = e.clientX - startX;
        const newLeftWidth = Math.max(200, Math.min(parentWidth * 0.8, startLeftWidth + deltaX));
        const newRightWidth = parentWidth - newLeftWidth;

        // 更新左右分栏宽度和手柄位置
        leftDom.style.width = `${newLeftWidth}px`;
        rightDom.style.width = `${newRightWidth}px`;
        lineEl.style.left = `${newLeftWidth}px`;
      };

      // 鼠标松开时移除事件
      const handleMouseUp = () => {
        document.removeEventListener('mousemove', handleMouseMove);
        document.removeEventListener('mouseup', handleMouseUp);
      };

      // 绑定全局事件(确保拖拽不中断)
      document.addEventListener('mousemove', handleMouseMove);
      document.addEventListener('mouseup', handleMouseUp);
    }, false);

    // 将手柄添加到父容器
    el.appendChild(lineEl);
  }
};

使用示例(ColumnDemo.vue


<template>
  <!-- 父容器:相对定位,包含左右分栏 -->
  <div class="column-container" v-columnResize>
    <!-- 左分栏 -->
    <div class="left-column">
      <h3>左侧菜单</h3>
      <p>拖拽中间手柄可调整宽度</p>
    </div>
    <!-- 右分栏 -->
    <div class="right-column">
      <h3>右侧内容区</h3>
      <p>总宽度固定,左侧变宽时右侧自动变窄</p>
    </div>
  </div>
</template>

<style scoped>
.column-container {
  position: relative;
  width: 1000px;
  height: 500px;
  margin: 20px auto;
  border: 1px solid #ccc;
}
.left-column {
  float: left;
  width: 300px; /* 初始宽度 */
  height: 100%;
  background: #f5f5f5;
  box-sizing: border-box;
  padding: 20px;
}
.right-column {
  float: left;
  width: 700px; /* 初始宽度 = 总宽 - 左栏宽 */
  height: 100%;
  background: #fff;
  box-sizing: border-box;
  padding: 20px;
}
</style>

<script>
import columnResize from '@/directive/columnResize.js';
export default {
  directives: {
    columnResize // 局部注册指令
  }
};
</script>

效果:拖拽中间灰色手柄,左侧宽度增加时右侧自动减少,且两侧宽度不会小于200px或超过总宽的80%。

案例2:表格容器高度调整(解决表格内容过多问题)

场景:表格数据过多时,用户可拖拽表格底部手柄调整容器高度,避免频繁滚动。

指令代码(directive/tableHeightResize.js


export default {
  bind(el) {
    // 查找表格容器(需在模板中定义 .table-container 类名)
    const tableContainer = el.querySelector('.table-container');
    if (!tableContainer) {
      console.error('未找到表格容器,请添加 .table-container 类名');
      return;
    }

    // 创建底部拖拽手柄
    const lineEl = document.createElement('div');
    lineEl.style = `
      height: 5px; 
      width: 100%; 
      background: #f0f0f0; 
      position: absolute; 
      bottom: 0; 
      left: 0; 
      cursor: s-resize; /* 垂直调整光标 */
    `;
    // 鼠标悬停时显示深色提示
    lineEl.onmouseover = () => lineEl.style.background = '#e0e0e0';
    lineEl.onmouseout = () => lineEl.style.background = '#f0f0f0';

    // 绑定鼠标按下事件
    lineEl.addEventListener('mousedown', (e) => {
      e.preventDefault();
      // 记录鼠标按下时的Y坐标和容器当前高度
      const startY = e.clientY;
      const startHeight = tableContainer.offsetHeight;

      // 鼠标移动时调整高度
      const handleMouseMove = (e) => {
        e.preventDefault();
        // 计算高度变化量(限制最小高度300px,最大800px)
        const deltaY = e.clientY - startY;
        const newHeight = Math.max(300, Math.min(800, startHeight + deltaY));
        tableContainer.style.height = `${newHeight}px`;
      };

      // 鼠标松开时移除事件
      const handleMouseUp = () => {
        document.removeEventListener('mousemove', handleMouseMove);
        document.removeEventListener('mouseup', handleMouseUp);
      };

      // 绑定全局事件
      document.addEventListener('mousemove', handleMouseMove);
      document.addEventListener('mouseup', handleMouseUp);
    }, false);

    // 将手柄添加到表格容器(容器需设为相对定位)
    tableContainer.appendChild(lineEl);
  }
};

使用示例(TableDemo.vue


<template>
  <div class="table-page">
    <h3>可调整高度的表格</h3>
    <!-- 表格容器:相对定位,溢出时显示滚动条 -->
    <div class="table-container" v-tableHeightResize>
      <table border="1" width="100%" cellpadding="10">
        <thead>
          <tr><th>ID</th><th>名称</th><th>状态</th></tr>
        </thead>
        <tbody>
          <!-- 模拟100行数据 -->
          <tr v-for="i in 100" :key="i">
            <td>{{ i }}</td>
            <td>数据项 {{ i }}</td>
            <td>正常</td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</template>

<style scoped>
.table-page {
  width: 800px;
  margin: 20px auto;
}
.table-container {
  position: relative; /* 确保手柄绝对定位生效 */
  height: 400px; /* 初始高度 */
  overflow: auto; /* 内容超出时显示滚动条 */
  border: 1px solid #ddd;
}
</style>

<script>
import tableHeightResize from '@/directive/tableHeightResize.js';
export default {
  directives: {
    tableHeightResize
  }
};
</script>

效果:拖拽表格底部灰色手柄,可上下调整容器高度(300px~800px),内容超出时自动显示滚动条。

案例3:图片透明度调整(可视化交互)

场景:图片预览页面中,用户可通过拖拽滑块直观调整图片透明度。

指令代码(directive/imgOpacityResize.js


export default {
  bind(el) {
    // 查找图片和控制区(需在模板中定义对应类名)
    const imgDom = el.querySelector('.target-img');
    const controlDom = el.querySelector('.opacity-controls');
    if (!imgDom || !controlDom) {
      console.error('未找到图片或控制区,请添加 .target-img 和 .opacity-controls 类名');
      return;
    }

    // 创建透明度滑块容器
    const sliderContainer = document.createElement('div');
    sliderContainer.style = `
      width: 200px; 
      height: 6px; 
      background: linear-gradient(to right, rgba(0,0,0,0), rgba(0,0,0,1)); 
      margin: 10px 0; 
      position: relative;
      border-radius: 3px;
    `;

    // 创建滑块按钮
    const sliderBtn = document.createElement('div');
    sliderBtn.style = `
      width: 16px; 
      height: 16px; 
      background: #409eff; 
      border-radius: 50%; 
      position: absolute; 
      top: 50%; 
      left: 50%; /* 初始在中间(透明度0.5) */
      transform: translate(-50%, -50%); 
      box-shadow: 0 0 3px rgba(0,0,0,0.3);
      cursor: pointer;
    `;
    sliderContainer.appendChild(sliderBtn);

    // 显示当前透明度值
    const valueText = document.createElement('span');
    valueText.style = 'margin-left: 10px; color: #666;';
    valueText.textContent = '透明度:50%';

    // 组合控制区
    controlDom.appendChild(sliderContainer);
    controlDom.appendChild(valueText);

    // 绑定鼠标按下事件
    sliderBtn.addEventListener('mousedown', (e) => {
      e.preventDefault();
      const sliderWidth = sliderContainer.offsetWidth;
      const btnWidth = sliderBtn.offsetWidth;
      // 计算滑块可移动的最大范围(容器宽 - 按钮宽)
      const maxLeft = sliderWidth - btnWidth;

      // 鼠标移动时调整滑块位置和透明度
      const handleMouseMove = (e) => {
        e.preventDefault();
        // 计算滑块相对容器的left值(限制在0~maxLeft之间)
        const sliderRect = sliderContainer.getBoundingClientRect();
        let left = e.clientX - sliderRect.left - btnWidth / 2;
        left = Math.max(0, Math.min(maxLeft, left));

        // 更新滑块位置
        sliderBtn.style.left = `${left}px`;

        // 计算透明度(0~1)并应用到图片
        const opacity = left / maxLeft;
        imgDom.style.opacity = opacity;

        // 更新显示文本
        valueText.textContent = `透明度:${Math.round(opacity * 100)}%`;
      };

      // 鼠标松开时移除事件
      const handleMouseUp = () => {
        document.removeEventListener('mousemove', handleMouseMove);
        document.removeEventListener('mouseup', handleMouseUp);
      };

      // 绑定全局事件
      document.addEventListener('mousemove', handleMouseMove);
      document.addEventListener('mouseup', handleMouseUp);
    }, false);
  }
};

使用示例(ImageDemo.vue


<template>
  <div class="image-container" v-imgOpacityResize>
    <h3>图片透明度调整</h3>
    <!-- 图片元素 -->
    <img 
      class="target-img" 
      src="https://picsum.photos/800/400" 
      alt="示例图片"
    >
    <!-- 控制区(滑块会被插入到这里) -->
    <div class="opacity-controls">
      <p>拖动滑块调整透明度:</p>
    </div>
  </div>
</template>

<style scoped>
.image-container {
  width: 800px;
  margin: 20px auto;
  text-align: center;
}
.target-img {
  max-width: 100%;
  border: 1px solid #eee;
  opacity: 0.5; /* 初始透明度 */
}
.opacity-controls {
  margin-top: 20px;
}
</style>

<script>
import imgOpacityResize from '@/directive/imgOpacityResize.js';
export default {
  directives: {
    imgOpacityResize
  }
};
</script>

效果:拖拽滑块时,图片透明度随滑块位置变化(左→右:透明→不透明),同时显示当前透明度百分比。

扩展逻辑总结

这三个案例均基于原指令的核心逻辑(手柄+鼠标事件+样式修改),通过以下调整实现不同功能:

  1. 手柄位置/样式:从弹窗右侧→分栏中间→表格底部→图片控制区,样式适配场景(垂直条→水平条→滑块)。
  2. 目标元素:从弹窗→分栏容器→表格容器→图片,只要能通过类名查找即可扩展。
  3. 修改的样式属性:从 widthwidth(联动)→heightopacity,覆盖尺寸和非尺寸调整。

根据实际需求,还可进一步扩展(如调整字体大小、边框粗细等),核心是保持“用户交互→事件监听→样式更新”的逻辑链。

若依前端vue项目里面的drag.js 详解

作用说明

这个自定义指令的核心作用是 为 Element UI 的 el-dialog 弹窗添加拖拽功能,允许用户通过拖拽弹窗头部(.el-dialog__header)自由移动弹窗位置,提升交互灵活性(默认 el-dialog 无法拖拽,只能固定在页面中)。

使用案例

步骤1:注册指令

main.js 中全局注册指令(假设文件路径为 @/directive/drag.js):


import dialogDrag from '@/directive/drag.js';
Vue.directive('dialogDrag', dialogDrag); // 注册指令,命名为 v-dialogDrag

步骤2:在组件中使用(结合 Element UI 的 el-dialog)


<template>
  <div>
    <!-- 按钮触发弹窗 -->
    <el-button @click="dialogVisible = true">打开可拖拽弹窗</el-button>

    <!-- 
      v-dialogDrag:启用拖拽功能(默认true,可传false禁用)
      .el-dialog:Element UI 弹窗组件,指令会自动查找其内部的头部和容器
    -->
    <el-dialog
      title="可拖拽弹窗"
      v-model="dialogVisible"
      width="50%"
      v-dialogDrag  <!-- 应用拖拽指令 -->
    >
      <div>这是一个可以通过头部拖拽的弹窗</div>
      <template #footer>
        <el-button @click="dialogVisible = false">关闭</el-button>
      </template>
    </el-dialog>
  </div>
</template>

<script>
export default {
  data() {
    return {
      dialogVisible: false // 控制弹窗显示/隐藏
    };
  }
};
</script>

步骤3:可选配置(动态启用/禁用拖拽)

通过指令值控制是否启用拖拽(true 启用,false 禁用):

<!-- 仅在 isDraggable 为 true 时允许拖拽 -->
<el-dialog
  title="条件拖拽弹窗"
  v-model="dialogVisible"
  v-dialogDrag="isDraggable"  <!-- 动态控制 -->
>
  <div>是否可拖拽:{{ isDraggable ? '是' : '否' }}</div>
  <el-button @click="isDraggable = !isDraggable">切换拖拽状态</el-button>
</el-dialog>

<script>
export default {
  data() {
    return {
      dialogVisible: false,
      isDraggable: true // 初始允许拖拽
    };
  }
};
</script>

关键说明

  1. 依赖 Element UI:指令内部通过类名 .el-dialog__header.el-dialog 查找元素,因此仅适用于 Element UI 的 el-dialog 组件。
  2. 拖拽区域:只能通过弹窗头部(包含标题的区域)拖拽,其他区域(如内容区、底部按钮)无法触发拖拽。
  3. 定位逻辑:弹窗会被设置为 position: absolute,初始位置水平居中,拖拽时通过更新 lefttop 样式实现移动。

通过该指令,可解决默认弹窗位置固定的问题,尤其在多弹窗叠加或弹窗遮挡内容时,用户可自由调整位置,提升操作体验。

若依vue前端后端对应完整笔记,直接后台联系或者B站联系

image.png

若依项目前端vue里面的dicts: [] 属性详解

1 界面位置

B站 gzh 有对应的视频讲解,源码解析,一行一行代码详解

2   dicts: []

结合之前的全局混入逻辑和组件配置 dicts: ['sys_normal_disable', 'sys_user_sex'],我会帮你把「字典加载→数据存储→最终使用」的完整逻辑讲透,包括这个类里每个核心方法、属性的作用,以及数据最终存在哪里、怎么用。

一、先看懂 Dict 类的核心设计

这个类是字典管理的“核心容器”,先明确它的核心属性和方法的作用:

核心成员 类型 作用
owner Object 绑定当前字典实例所属的 Vue 组件(来自全局混入的 dict.owner = this
label Object 字典标签映射表,格式 { 字典类型: { 值: 标签 } }(比如 sys_user_sex: {1: '男', 2: '女'}
type Object 字典完整数组表,格式 { 字典类型: [DictData实例数组] }(存储完整的字典项)
_dictMetas Array 内部属性,存储所有字典的元数据(DictMeta实例),记录字典的类型、请求方式等
constructor() 构造函数 初始化 ownerlabeltype 三个核心属性
init() 方法 入口方法:解析字典配置、初始化响应式数据、加载字典数据
reloadDict() 方法 重新加载指定类型的字典数据
loadDict() 内部函数 实际发起请求加载字典、处理数据并更新到 label/type

二、组件渲染时,Dict 类的完整执行流程

结合组件配置 dicts: ['sys_normal_disable', 'sys_user_sex'],一步一步看 Dict 类是怎么工作的:

步骤1:创建 Dict 实例(全局混入的 data 钩子)

// 全局混入的data钩子中执行
const dict = new Dict(); 
// 此时dict的初始状态:
dict = {
  owner: 当前组件实例,
  label: {}, // 空对象
  type: {},  // 空对象
  _dictMetas: undefined
}
步骤2:调用 init() 初始化字典(全局混入的 created 钩子)

this.dict.init(['sys_normal_disable', 'sys_user_sex']);

init() 内部执行逻辑:

  1. 格式化配置:因为传入的是数组,包装成配置对象 { types: ['sys_normal_disable', 'sys_user_sex'] }

  2. 合并默认配置:用 mergeRecursive 合并 DEFAULT_DICT_OPTIONS 和用户配置,最终 opts = { types: ['sys_normal_disable', 'sys_user_sex'] }

  3. 解析元数据:把每个字典类型转成 DictMeta 实例,存储到 _dictMetas

    
    this._dictMetas = [
      DictMeta.parse('sys_normal_disable'), // 解析出字典类型、请求地址等
      DictMeta.parse('sys_user_sex')
    ];
    
  4. 初始化响应式数据:遍历 _dictMetas,为每个字典类型初始化响应式的 labeltype

    
    // 以sys_user_sex为例
    Vue.set(this.label, 'sys_user_sex', {}); // 响应式创建 label.sys_user_sex
    Vue.set(this.type, 'sys_user_sex', []);  // 响应式创建 type.sys_user_sex
    

    ✨ 关键:用 Vue.set 是为了让 label/type 的子属性也具备响应式(Vue 无法检测对象新增属性,必须用 Vue.set);

  5. 加载非延迟字典:两个字典都不是延迟加载(lazy: false),调用 loadDict() 加载数据,并把 Promise 存入数组 ps

  6. 等待所有加载完成:返回 Promise.all(ps),确保两个字典都加载完再执行后续回调。

步骤3:loadDict() 实际加载字典数据

这是核心步骤,负责从后端请求数据并处理:

  1. 发起请求:调用 dictMeta.request(dictMeta)DictMeta 类的 request 方法,实际是调用后端接口);

  2. 处理响应数据:用 dictMeta.responseConverter 把后端返回的原始数据转换成 DictData 实例数组(比如:

    
    // 后端返回 → 转换后
    [{value: 1, label: '男'}, {value: 2, label: '女'}] → [new DictData(1, '男'), new DictData(2, '女')]
    
  3. 验证数据格式:确保转换后是 DictData 数组,否则清空数据避免报错;

  4. 更新 type 数组:用 splice 替换数组内容(保持数组引用不变,响应式不丢失):

    
    // 以sys_user_sex为例
    dict.type.sys_user_sex.splice(0, Number.MAX_SAFE_INTEGER, ...dicts);
    // 此时 dict.type.sys_user_sex = [DictData{value:1, label:'男'}, DictData{value:2, label:'女'}]
    
  5. 更新 label 映射:遍历 DictData 数组,建立「值→标签」的映射:

    
    // 以sys_user_sex为例
    Vue.set(dict.label.sys_user_sex, 1, '男');
    Vue.set(dict.label.sys_user_sex, 2, '女');
    // 此时 dict.label.sys_user_sex = {1: '男', 2: '女'}
    
步骤4:加载完成后,Dict 实例的最终状态

dict = {
  owner: 当前组件实例,
  label: {
    sys_normal_disable: {0: '正常', 1: '禁用'},
    sys_user_sex: {1: '男', 2: '女'}
  },
  type: {
    sys_normal_disable: [DictData{value:0, label:'正常'}, DictData{value:1, label:'禁用'}],
    sys_user_sex: [DictData{value:1, label:'男'}, DictData{value:2, label:'女'}]
  },
  _dictMetas: [DictMeta{type: 'sys_normal_disable'}, DictMeta{type: 'sys_user_sex'}]
}

三、字典数据最终存在哪里?怎么用?

1. 数据存储位置(核心!)
  • 完整字典数组:存在 this.dict.type[字典类型] 中(比如 this.dict.type.sys_user_sex);
  • 标签映射表:存在 this.dict.label[字典类型] 中(比如 this.dict.label.sys_user_sex);
  • 两者都是响应式数据(因为用 Vue.set 创建),数据更新后模板会自动刷新。
2. 实际开发中怎么使用这些数据?

<template>
  <!-- 1. 下拉框:用type取完整数组 -->
  <el-select v-model="form.sex">
    <el-option
      v-for="item in dict.type.sys_user_sex"
      :key="item.value"
      :label="item.label"
      :value="item.value"
    />
  </el-select>

  <!-- 2. 显示标签:用label快速取值 -->
  <div>用户状态:{{ dict.label.sys_normal_disable[form.status] }}</div>
</template>

<script>
export default {
  dicts: ['sys_normal_disable', 'sys_user_sex'],
  data() {
    return {
      form: { sex: 1, status: 0 }
    }
  },
  methods: {
    // 3. 方法中重新加载字典
    refreshDict() {
      this.dict.reloadDict('sys_user_sex').then(() => {
        console.log('性别字典重新加载完成');
      });
    }
  }
}
</script>

四、关键设计亮点(新手需要理解)

  1. 响应式保障:用 Vue.set 初始化 label/type 的子属性,确保数据更新后模板能响应;
  2. 数组引用不变:更新 type 数组时用 splice 替换内容,而不是直接赋值(dict.type[type] = dicts),避免组件中绑定的数组丢失响应式;
  3. 数据标准化:用 DictData 类包装字典项,确保所有字典数据结构一致;
  4. 元数据解耦:用 DictMeta 处理字典的请求、转换逻辑,让 Dict 类只负责管理数据,职责更清晰。

总结

  1. 数据存储核心:字典数据最终存在 this.dict.type(完整数组)和 this.dict.label(标签映射)中,都是响应式属性;
  2. 执行流程:创建 Dict 实例 → init 解析配置 → loadDict 发起请求 → 处理数据并更新到 type/label → 组件可直接使用;
  3. 使用方式:模板中用 dict.type[类型] 渲染下拉框,用 dict.label[类型][值] 快速显示标签。

简单来说,这个 Dict 类就是把后端返回的字典数据,转换成了组件能直接用的、响应式的“数组+映射表”结构,方便开发中快速使用。

3 this.$refs.tree.filter(val)

this.$refs.tree.filter(val) 这行代码里每个部分的含义

一、逐部分解析核心概念

1. this 指的是谁?

和之前讲的 Vue 组件中 this 的含义完全一致:this 指向当前执行这段代码的 Vue 组件实例

  • 比如这段代码写在 UserList 组件的 methods 里,点击按钮触发该方法时,this 就是 UserList 组件实例;
  • 写在组件的 created/mounted 钩子中,this 也是当前组件实例;
  • 核心:this 永远是“当前代码所属的那个组件”。
2. $refs 是什么?

$refs 是 Vue 组件实例的内置属性,作用是:获取组件模板中通过 ref 属性标记的 DOM 元素或子组件实例

  • 你可以把 $refs 理解成一个“引用字典”:键是模板中 ref 的值,值是对应的 DOM/组件实例;
  • 注意:$refs 只有在组件挂载完成(mounted 钩子之后) 才会有值(DOM 渲染完成),在 created 中访问 $refs 会是 undefined
3. tree 是什么?

tree 是你在模板中给某个元素/组件设置的 ref 属性值,是自定义的名称(你也可以命名为 myTreedeptTree 等)。比如模板中会有这样的代码(以 Element UI 的树形组件为例):


<template>
  <!-- 给树形组件设置 ref="tree" -->
  <el-tree
    ref="tree"  <!-- 关键ref的值是tree -->
    :data="treeData"
    :filter-node-method="filterNode"
  />
</template>

此时 this.$refs.tree 就指向这个 <el-tree> 组件的实例(不是 DOM 元素,是组件实例)。

4. filter 是什么?

filter<el-tree>(Element UI 树形组件)内置的方法,作用是:触发树形组件的节点过滤功能,根据传入的 val 筛选出符合条件的节点并展示。

  • 这个 filter 方法是 Element UI 封装好的,不是 Vue 原生方法;
  • 传入的 val 是“过滤关键词”,树形组件会根据你配置的 filter-node-method 方法,用这个关键词筛选节点。

二、完整示例:结合代码理解

模板部分(Element UI 树形组件)

<template>
  <div>
    <!-- 输入框:输入过滤关键词 -->
    <el-input
      v-model="filterText"
      placeholder="请输入筛选关键词"
      @input="handleFilter"  <!-- 输入时触发过滤 -->
    />
    <!-- 树形组件:设置ref="tree",配置过滤方法 -->
    <el-tree
      ref="tree"
      :data="treeData"
      :filter-node-method="filterNode"  <!-- 定义过滤规则 -->
      default-expand-all
    />
  </div>
</template>
脚本部分

<script>
export default {
  data() {
    return {
      filterText: '', // 过滤关键词
      treeData: [ // 树形数据
        { label: '部门1', children: [{ label: '员工1' }, { label: '员工2' }] },
        { label: '部门2', children: [{ label: '员工3' }, { label: '员工4' }] }
      ]
    }
  },
  methods: {
    // 触发过滤的方法
    handleFilter() {
      // 核心代码:调用树形组件的filter方法,传入过滤关键词
      this.$refs.tree.filter(this.filterText);
    },
    // 过滤规则:判断节点是否匹配关键词
    filterNode(value, data) {
      if (!value) return true; // 无关键词时显示所有节点
      // 节点label包含关键词则显示
      return data.label.indexOf(value) !== -1;
    }
  }
}
</script>

三、这行代码的整体作用

this.$refs.tree.filter(val) 翻译成大白话:

“让当前组件中,模板里标记为 ref="tree" 的树形组件(el-tree),执行它的 filter 方法,传入 val 作为过滤关键词,按照预设的规则筛选节点。”

四、关键注意事项(新手易踩坑)

  1. $refs 不能响应式$refs 是普通对象,不是响应式的,修改 ref 对应的 DOM/组件不会触发视图更新;
  2. 必须挂载后访问:在 created 中调用 this.$refs.tree 会是 undefined,要写在 mounted 或点击/输入等事件中;
  3. filter 是组件内置方法:只有 Element UI 的 el-tree 有这个方法,原生 DOM 或其他组件(如 el-table)没有,不要乱用;
  4. tree 是自定义的:如果模板中 ref="myTree",代码就要写成 this.$refs.myTree.filter(val)

总结

  1. this → 当前 Vue 组件实例;
  2. $refs → Vue 内置属性,存储模板中 ref 标记的元素/组件引用;
  3. tree → 模板中 ref="tree" 的自定义名称,指向 el-tree 组件实例;
  4. filter → el-tree 组件的内置方法,用于触发节点过滤;
  5. 整行代码作用:给树形组件传入关键词,执行节点筛选。

产品催: 1 天优化 Vue 官网 SEO?我用这个插件半天搞定(不重构 Nuxt) | 掘金一周 1.22

本文字数1800+ ,阅读时间大约需要 4分钟。

【掘金一周】本期亮点:

「上榜规则」:文章发布时间在本期「掘金一周」发布时间的前一周内;且符合各个栏目的内容定位和要求。 如发现文章有抄袭、洗稿等违反社区规则的行为,将取消当期及后续上榜资格。

一周“金”选

掘金一周 文章头图 1303x734.jpg

内容评审们会在过去的一周内对社区深度技术好文进行挖掘和筛选,优质的技术文章有机会出现在下方榜单中,排名不分先后。

前端

Tailwind 因为 AI 的裁员“闹剧”结束,而 AI 对开源项目的影响才刚刚开始@恋猫de小郭

Tailwind CSS因在前端AI中被大量使用,虽使用量增大但切断其原本“流量to转化”的赚钱路径,出现“越火越穷”情况,裁掉部分工程师。赞助仅解燃眉之急,AI对开源项目影响才刚开始,盈利更取决于在AI链路中的角色。

鸿蒙应用的“任意门”:Deep Linking 与 App Linking 的相爱相杀@SameX

本文基于HarmonyOS Next,介绍了鸿蒙系统用于解决应用跳转问题的Deep Linking和App Linking。前者用自定义协议,简单但不安全;后者用标准HTTPS,正规安全。还给出实战代码,最后建议推广用App Linking,内部互通选Deep Linking。

告别手写礼簿!一款开源免费的电子红白喜事礼簿系统!@Java陈序员

文章推荐了开源免费的电子红白喜事礼簿系统 gift - book,它纯本地运行,无需联网、数据加密。具备秒级记账、双色主题等特色功能。介绍了快速上手、本地开发步骤,称其操作简单、无数据泄露风险,值得一试。

产品催: 1 天优化 Vue 官网 SEO?我用这个插件半天搞定(不重构 Nuxt)@不一样的少年_

文章围绕 1 天内优化 Vue 官网 SEO 展开。因时间紧、迁移成本及运维问题未选 Nuxt,采用 vite - ssg 实现 SPA 到 SSG 转变,解决环境兼容、数据填充等难题,最终提升 SEO 评分,成果显著。

视频播放弱网提示实现@古茗前端团队

业务反馈视频播放卡顿多因弱网,为优化体验、减少客诉,提出两种方案:一是用 NetworkInformation 判断网络情况,二是监听 Video 元素的 waiting 和 canplay 事件。还拓展了网络速度检测功能,最终成功实现弱网提示。

现代 CSS 颜色使用指南@冴羽

本文介绍现代 CSS 颜色使用技巧,包括现代写法如 rgb 可直接加透明度、用空格分隔;相对颜色能基于现有颜色生成新颜色;浅暗主题用 light - dark 切换;渐变可指定颜色空间;还能使用超宽色域满足特殊需求。

一个纯前端的网站集合管理工具@Younglina

本文介绍一个纯前端网站集合管理工具,支持本地数据存储、完整 CRUD 操作和 JSON 数据导入导出,可作 Chrome 扩展。它具备网站管理、图片上传等功能,使用 Vue 3、TypeScript 等技术栈,还给出使用、安装及注意事项。

后端

为什么Java里面,Service 层不直接返回 Result 对象?@一只叫煤球的猫

文章围绕Java里Service层不直接返回Result对象展开。从职责分离、可复用性、异常处理、测试便利性等多方面阐述原因,强调Service应专注业务逻辑,避免与表现逻辑耦合,以提升代码质量和可维护性。

物品超领取损失1万事故复盘(一)@提前退休的java猿

本文围绕物品超领取损失1万事故展开复盘。先分析核心代码,探讨事务失效、主从读写不一致问题;再剖析异常日志与数据库日志;虽发现事务报错、阻塞等问题,但仍无法解释超领原因,后续将跟进。

Spring 的西西弗斯之石:理解 BeanFactory、FactoryBean 与 ObjectFactory@一旅人

文章深入剖析了 Spring 中易混淆的三个概念。BeanFactory 是容器,管理 Bean 生命周期;FactoryBean 是特殊 Bean,用于复杂 Bean;ObjectFactory 是接口,提供延迟获取对象能力。清晰对比三者,助开发者理解 Spring 底层逻辑。

工作中最常用的5种本地缓存@苏三说技术

文章介绍了5种常用本地缓存方案。ConcurrentHashMap简单轻量,LRU缓存可自动淘汰,Ehcache功能全面适合企业级,Caffeine性能卓越,Guava Cache设计优雅。还给出选型建议,强调选合适方案提升系统性能。

WebSocket 在 Spring Boot 中的实战解析:实时通信的技术利器@苏渡苇

文章围绕 Spring Boot 中 WebSocket 展开,先介绍其解决传统通信高延迟、高开销问题的优势,接着阐述技术栈、核心要点,如生命周期管理、心跳机制等,还给出监控系统实战和典型场景,指出适用与不适用场景。

Android

Compose Multiplatform 1.10 Interop views 新特性:Overlay 和 Autosizing@恋猫de小郭

Compose Multiplatform 1.10 在 Interop views 有两大新特性。Overlay 让 UIKit interop 可置于 Compose 之上,支持透明背景等效果;Autosizing 使 interop view 能自动调整尺寸,简化混合开发布局,而 Android 因生态融合无此割裂问题。

HarmonyOS下饭菜时间 -- @Monitor@猫猫头啊

文章聚焦 HarmonyOS 的 @Monitor,介绍其创建使用方式与调用链。解释多个属性同时修改时回调仅执行一次的去重机制,是通过 Set 集合收集监控器。还指出设计要点与限制,助开发者合理运用。

人工智能

OpenCode 上手初体验:从安装到使用@wangruofeng

OpenCode 是开源 AI 编码助手,支持多形式使用,内置免费模型,可接入多种服务商模型。可用于代码分析、生成等工作。介绍了安装、使用方法及命令选项,还提及增强插件 oh - my - opencode 的功能、安装与使用。

活动日历

活动名称 活动时间
🏆2025 AI/Vibe Coding 对我的影响 年终征文 2025年12月26日-2026年1月25日

📖 投稿专区

大家可以在评论区推荐认为不错的文章,并附上链接和推荐理由,有机会呈现在下一期。文章创建日期必须在下期掘金一周发布前一周以内;可以推荐自己的文章、也可以推荐他人的文章。

学习Three.js--曲线(Curve)

学习Three.js--曲线(Curve)

前置核心说明

Curve 是 Three.js 中所有曲线/直线的基类,定义了曲线的核心行为(如采样顶点、计算长度等)。所有2D/3D曲线均继承自 Curve,核心作用是「通过数学公式生成连续的顶点序列」,再基于这些顶点创建线条模型,实现任意自定义曲线的绘制。

核心规则

  1. 维度分类
    • 2D曲线:基于XY平面(Z=0),继承 THREE.Curve,如 LineCurve/ArcCurve
    • 3D曲线:基于XYZ三维空间,继承 THREE.Curve(部分别名/子类),如 LineCurve3/CatmullRomCurve3
  2. 核心流程
    创建曲线实例 → 采样顶点(getPoints) → 几何体绑定顶点 → 创建线模型 → 添加到场景
  3. 线模型类型(决定曲线渲染方式):
    模型类型 核心特点 适用场景
    THREE.Line 按顶点顺序绘制连续线条 开放曲线(如直线、贝塞尔曲线)
    THREE.LineLoop 闭合线条(最后一个顶点连接第一个) 封闭曲线(如椭圆、圆)
    THREE.LineSegments 每两个顶点为一组绘制分段线 离散线条(如网格线)
  4. 采样精度getPoints(n)n 是采样点数(非顶点数),n 越大曲线越平滑(推荐50~100,复杂曲线可设200)。

一、2D曲线(XY平面,Z=0)

所有2D曲线均基于XY平面,Z坐标默认0,核心用于绘制平面曲线。

1. LineCurve(2D直线)

核心说明

两点确定的2D直线,是最简单的2D曲线,无曲率。

构造函数参数
// 语法:new THREE.LineCurve(起点向量, 终点向量)
const lineCurve = new THREE.LineCurve(
  new THREE.Vector2(x1, y1), // 必传:起点(Vector2对象)
  new THREE.Vector2(x2, y2)  // 必传:终点(Vector2对象)
);
参数 类型 说明
v1 THREE.Vector2 直线起点(XY坐标)
v2 THREE.Vector2 直线终点(XY坐标)
使用示例
// 1. 创建2D直线(从(0,0)到(100, 50))
const lineCurve = new THREE.LineCurve(
  new THREE.Vector2(0, 0),
  new THREE.Vector2(100, 50)
);

// 2. 采样顶点(50个点,直线足够平滑)
const points = lineCurve.getPoints(50);

// 3. 创建几何体并绑定顶点
const geometry = new THREE.BufferGeometry();
geometry.setFromPoints(points);

// 4. 创建线材质
const material = new THREE.LineBasicMaterial({ color: 0xff0000 });

// 5. 创建线模型(开放直线用Line)
const line = new THREE.Line(geometry, material);
scene.add(line);

2. ArcCurve(2D圆弧)

核心说明

基于圆心、半径、起始角/终止角的2D圆弧,可绘制圆、半圆、任意弧度的圆弧。

构造函数参数
// 语法:new THREE.ArcCurve(圆心X, 圆心Y, 半径, 起始角, 终止角, 是否逆时针)
const arcCurve = new THREE.ArcCurve(
  0,        // 必传:圆心X坐标
  0,        // 必传:圆心Y坐标
  50,       // 必传:圆弧半径
  0,        // 必传:起始角(弧度,0=右向X轴)
  Math.PI,  // 必传:终止角(弧度,Math.PI=180°)
  false     // 可选:是否逆时针绘制,默认false(顺时针)
);
参数 类型 默认值 说明
aX Number 圆心X坐标
aY Number 圆心Y坐标
aRadius Number 圆弧半径
aStartAngle Number 起始角度(弧度,0=X轴正方向,Math.PI/2=Y轴正方向)
aEndAngle Number 终止角度(弧度)
aClockwise Boolean false 是否顺时针绘制,true=顺时针,false=逆时针
使用示例(绘制半圆)
// 1. 创建半圆(圆心(0,0),半径50,0~π弧度)
const arcCurve = new THREE.ArcCurve(0, 0, 50, 0, Math.PI);

// 2. 采样顶点
const points = arcCurve.getPoints(50);

// 3. 几何体+材质+模型
const geometry = new THREE.BufferGeometry();
geometry.setFromPoints(points);
const material = new THREE.LineBasicMaterial({ color: 0x00ff00 });
const line = new THREE.Line(geometry, material);
scene.add(line);

3. EllipseCurve(2D椭圆/圆)

核心说明

基于圆心、长半轴、短半轴的2D椭圆,当长半轴=短半轴时即为圆(替代ArcCurve绘制完整圆)。

构造函数参数
// 语法:new THREE.EllipseCurve(圆心X, 圆心Y, 长半轴, 短半轴, 起始角, 终止角, 是否逆时针, 旋转角)
const ellipseCurve = new THREE.EllipseCurve(
  0,          // 必传:圆心X坐标
  0,          // 必传:圆心Y坐标
  100,        // 必传:X轴方向长半轴
  50,         // 必传:Y轴方向短半轴
  0,          // 可选:起始角,默认0
  2 * Math.PI,// 可选:终止角,默认2π(完整椭圆)
  false,      // 可选:是否逆时针,默认false
  0           // 可选:椭圆旋转角(弧度),默认0
);
参数 类型 默认值 说明
aX Number 圆心X坐标
aY Number 圆心Y坐标
xRadius Number X轴方向半轴长度(长半轴)
yRadius Number Y轴方向半轴长度(短半轴)
aStartAngle Number 0 起始角度(弧度)
aEndAngle Number 终止角度(弧度,2π=完整椭圆)
aClockwise Boolean false 是否顺时针绘制
aRotation Number 0 椭圆整体旋转角度(弧度)
使用示例(优化版,用户示例升级)
// 1. 创建椭圆(圆心(0,0),长半轴100,短半轴50,完整椭圆)
const ellipseCurve = new THREE.EllipseCurve(0, 0, 100, 50);

// 2. 采样顶点(50个点,椭圆足够平滑)
const points = ellipseCurve.getPoints(50);

// 3. 创建几何体并绑定顶点
const geometry = new THREE.BufferGeometry();
geometry.setFromPoints(points);

// 4. 线材质(红色,线宽1)
const material = new THREE.LineBasicMaterial({ 
  color: 0xff0000,
  linewidth: 1 // 注意:WebGL中线宽仅部分浏览器支持>1
});

// 5. 创建闭合线模型(椭圆用LineLoop)
const line = new THREE.LineLoop(geometry, material);
scene.add(line);

4. SplineCurve(2D样条曲线)

核心说明

通过多个控制点生成的平滑2D曲线(插值曲线),曲线会穿过所有控制点,比贝塞尔曲线更易控制。

构造函数参数
// 语法:new THREE.SplineCurve(控制点数组)
const splineCurve = new THREE.SplineCurve([
  new THREE.Vector2(x1, y1),
  new THREE.Vector2(x2, y2),
  // ... 更多控制点
]);
参数 类型 说明
points Array<THREE.Vector2> 必传:2D控制点数组(至少2个,越多曲线越复杂)
使用示例
// 1. 创建2D样条曲线(4个控制点)
const splineCurve = new THREE.SplineCurve([
  new THREE.Vector2(-100, 0),  // 控制点1
  new THREE.Vector2(-50, 80),  // 控制点2
  new THREE.Vector2(50, -80),  // 控制点3
  new THREE.Vector2(100, 0)    // 控制点4
]);

// 2. 采样顶点(100个点,保证平滑)
const points = splineCurve.getPoints(100);

// 3. 几何体+材质+模型
const geometry = new THREE.BufferGeometry();
geometry.setFromPoints(points);
const material = new THREE.LineBasicMaterial({ color: 0x0000ff });
const line = new THREE.Line(geometry, material);
scene.add(line);

5. QuadraticBezierCurve(2D二次贝塞尔曲线)

核心说明

由「起点+控制点+终点」3个点定义的2D贝塞尔曲线,单曲率,适合简单弯曲。

构造函数参数
// 语法:new THREE.QuadraticBezierCurve(起点, 控制点, 终点)
const quadBezier = new THREE.QuadraticBezierCurve(
  new THREE.Vector2(x1, y1), // 必传:起点
  new THREE.Vector2(x2, y2), // 必传:控制点(决定弯曲方向)
  new THREE.Vector2(x3, y3)  // 必传:终点
);
参数 类型 说明
v0 THREE.Vector2 起点
v1 THREE.Vector2 控制点(核心,决定曲线形状)
v2 THREE.Vector2 终点
使用示例
// 1. 创建二次贝塞尔曲线
const quadBezier = new THREE.QuadraticBezierCurve(
  new THREE.Vector2(-80, 0),  // 起点
  new THREE.Vector2(0, 80),   // 控制点(向上弯曲)
  new THREE.Vector2(80, 0)    // 终点
);

// 2. 采样顶点
const points = quadBezier.getPoints(80);

// 3. 渲染
const geometry = new THREE.BufferGeometry();
geometry.setFromPoints(points);
const material = new THREE.LineBasicMaterial({ color: 0xffff00 });
const line = new THREE.Line(geometry, material);
scene.add(line);

6. CubicBezierCurve(2D三次贝塞尔曲线)

核心说明

由「起点+控制点1+控制点2+终点」4个点定义的2D贝塞尔曲线,双曲率,适合复杂弯曲(如字体轮廓、路径动画)。

构造函数参数
// 语法:new THREE.CubicBezierCurve(起点, 控制点1, 控制点2, 终点)
const cubicBezier = new THREE.CubicBezierCurve(
  new THREE.Vector2(x1, y1), // 必传:起点
  new THREE.Vector2(x2, y2), // 必传:控制点1
  new THREE.Vector2(x3, y3), // 必传:控制点2
  new THREE.Vector2(x4, y4)  // 必传:终点
);
参数 类型 说明
v0 THREE.Vector2 起点
v1 THREE.Vector2 控制点1(左侧弯曲)
v2 THREE.Vector2 控制点2(右侧弯曲)
v3 THREE.Vector2 终点
使用示例
// 1. 创建三次贝塞尔曲线
const cubicBezier = new THREE.CubicBezierCurve(
  new THREE.Vector2(-100, 0), // 起点
  new THREE.Vector2(-50, 100),// 控制点1(向上)
  new THREE.Vector2(50, -100),// 控制点2(向下)
  new THREE.Vector2(100, 0)   // 终点
);

// 2. 采样顶点
const points = cubicBezier.getPoints(100);

// 3. 渲染
const geometry = new THREE.BufferGeometry();
geometry.setFromPoints(points);
const material = new THREE.LineBasicMaterial({ color: 0xff00ff });
const line = new THREE.Line(geometry, material);
scene.add(line);

二、3D曲线(XYZ三维空间)

3D曲线突破XY平面限制,支持XYZ三维坐标,核心用于3D路径(如飞行轨迹、管道模型)。

1. LineCurve3(3D直线)

核心说明

两点确定的3D直线,Z坐标可自定义,是3D最基础的曲线。

构造函数参数
// 语法:new THREE.LineCurve3(起点向量, 终点向量)
const lineCurve3 = new THREE.LineCurve3(
  new THREE.Vector3(x1, y1, z1), // 必传:3D起点
  new THREE.Vector3(x2, y2, z2)  // 必传:3D终点
);
参数 类型 说明
v1 THREE.Vector3 3D起点(XYZ坐标)
v2 THREE.Vector3 3D终点(XYZ坐标)
使用示例
// 1. 创建3D直线(从(0,0,0)到(100, 50, 80))
const lineCurve3 = new THREE.LineCurve3(
  new THREE.Vector3(0, 0, 0),
  new THREE.Vector3(100, 50, 80)
);

// 2. 采样顶点
const points = lineCurve3.getPoints(50);

// 3. 渲染
const geometry = new THREE.BufferGeometry();
geometry.setFromPoints(points);
const material = new THREE.LineBasicMaterial({ color: 0x00ff00 });
const line = new THREE.Line(geometry, material);
scene.add(line);

2. CatmullRomCurve3(3D样条曲线)

核心说明

Three.js官方推荐的3D样条曲线(替代SplineCurve3),通过多个3D控制点生成平滑曲线,曲线穿过所有控制点,适合3D路径规划。

构造函数参数
// 语法:new THREE.CatmullRomCurve3(控制点数组, 是否闭合, 曲线类型, 张力)
const catmullCurve = new THREE.CatmullRomCurve3(
  [
    new THREE.Vector3(x1, y1, z1),
    new THREE.Vector3(x2, y2, z2),
    // ... 更多控制点
  ],
  false,       // 可选:是否闭合,默认false
  'centripetal',// 可选:曲线类型,默认'centripetal'
  0.5          // 可选:张力(0~1),默认0.5,值越大曲线越平缓
);
参数 类型 默认值 说明
points Array<THREE.Vector3> 必传:3D控制点数组(至少2个)
closed Boolean false 是否闭合曲线
type String 'centripetal' 曲线类型:'centripetal'(默认,自然)、'chordal'(更紧绷)、'catmullrom'(更平滑)
tension Number 0.5 张力(0=无张力,1=最大张力)
使用示例
// 1. 创建3D样条曲线(4个控制点,空间弯曲)
const catmullCurve = new THREE.CatmullRomCurve3([
  new THREE.Vector3(-100, 0, 0),   // 控制点1
  new THREE.Vector3(-50, 80, 50),  // 控制点2
  new THREE.Vector3(50, -80, 100), // 控制点3
  new THREE.Vector3(100, 0, 50)    // 控制点4
]);

// 2. 采样顶点(100个点,保证3D平滑)
const points = catmullCurve.getPoints(100);

// 3. 渲染
const geometry = new THREE.BufferGeometry();
geometry.setFromPoints(points);
const material = new THREE.LineBasicMaterial({ color: 0x0000ff });
const line = new THREE.Line(geometry, material);
scene.add(line);

3. QuadraticBezierCurve3(3D二次贝塞尔曲线)

核心说明

3D版本的二次贝塞尔曲线,由「3D起点+3D控制点+3D终点」定义,支持空间弯曲。

构造函数参数
// 语法:new THREE.QuadraticBezierCurve3(起点, 控制点, 终点)
const quadBezier3 = new THREE.QuadraticBezierCurve3(
  new THREE.Vector3(x1, y1, z1), // 必传:3D起点
  new THREE.Vector3(x2, y2, z2), // 必传:3D控制点
  new THREE.Vector3(x3, y3, z3)  // 必传:3D终点
);
参数 类型 说明
v0 THREE.Vector3 3D起点
v1 THREE.Vector3 3D控制点(决定空间弯曲方向)
v2 THREE.Vector3 3D终点
使用示例
// 1. 创建3D二次贝塞尔曲线
const quadBezier3 = new THREE.QuadraticBezierCurve3(
  new THREE.Vector3(-80, 0, 0),   // 起点
  new THREE.Vector3(0, 80, 50),   // 控制点(Z轴偏移)
  new THREE.Vector3(80, 0, 100)   // 终点
);

// 2. 采样顶点
const points = quadBezier3.getPoints(80);

// 3. 渲染
const geometry = new THREE.BufferGeometry();
geometry.setFromPoints(points);
const material = new THREE.LineBasicMaterial({ color: 0xffff00 });
const line = new THREE.Line(geometry, material);
scene.add(line);

4. CubicBezierCurve3(3D三次贝塞尔曲线)

核心说明

3D版本的三次贝塞尔曲线,由4个3D点定义,支持复杂空间弯曲,是3D路径动画的核心曲线。

构造函数参数
// 语法:new THREE.CubicBezierCurve3(起点, 控制点1, 控制点2, 终点)
const cubicBezier3 = new THREE.CubicBezierCurve3(
  new THREE.Vector3(x1, y1, z1), // 必传:3D起点
  new THREE.Vector3(x2, y2, z2), // 必传:3D控制点1
  new THREE.Vector3(x3, y3, z3), // 必传:3D控制点2
  new THREE.Vector3(x4, y4, z4)  // 必传:3D终点
);
参数 类型 说明
v0 THREE.Vector3 3D起点
v1 THREE.Vector3 3D控制点1
v2 THREE.Vector3 3D控制点2
v3 THREE.Vector3 3D终点
使用示例
// 1. 创建3D三次贝塞尔曲线
const cubicBezier3 = new THREE.CubicBezierCurve3(
  new THREE.Vector3(-100, 0, 0),  // 起点
  new THREE.Vector3(-50, 100, 50),// 控制点1
  new THREE.Vector3(50, -100, 80),// 控制点2
  new THREE.Vector3(100, 0, 100)  // 终点
);

// 2. 采样顶点
const points = cubicBezier3.getPoints(100);

// 3. 渲染
const geometry = new THREE.BufferGeometry();
geometry.setFromPoints(points);
const material = new THREE.LineBasicMaterial({ color: 0xff00ff });
const line = new THREE.Line(geometry, material);
scene.add(line);

三、CurvePath(组合曲线)

核心说明

CurvePath 是「曲线容器」,可将多个2D/3D曲线组合成一个复合曲线,支持统一采样、平移、旋转等操作,适合绘制复杂路径(如迷宫、文字轮廓)。

核心方法
方法名 说明 示例
add(curve) 添加单个曲线到容器 curvePath.add(lineCurve)
getPoints(n) 统一采样所有曲线的顶点 curvePath.getPoints(100)
closePath() 闭合组合曲线(最后一个曲线终点连接第一个曲线起点) curvePath.closePath()
使用示例(组合2D直线+圆弧)
// 1. 创建CurvePath容器
const curvePath = new THREE.CurvePath();

// 2. 添加子曲线(直线+圆弧)
// 子曲线1:2D直线(从(0,0)到(100,0))
const lineCurve = new THREE.LineCurve(
  new THREE.Vector2(0, 0),
  new THREE.Vector2(100, 0)
);
curvePath.add(lineCurve);

// 子曲线2:圆弧(从(100,0)到(100,100),90°圆弧)
const arcCurve = new THREE.ArcCurve(
  100, 0, 100, 0, Math.PI/2, true
);
curvePath.add(arcCurve);

// 3. 闭合曲线(可选)
// curvePath.closePath();

// 4. 统一采样顶点(100个点)
const points = curvePath.getPoints(100);

// 5. 渲染组合曲线
const geometry = new THREE.BufferGeometry();
geometry.setFromPoints(points);
const material = new THREE.LineBasicMaterial({ color: 0xff6600 });
const line = new THREE.Line(geometry, material);
scene.add(line);

四、完整实战示例(多曲线组合)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>Three.js 曲线完整示例</title>
  <style>body { margin: 0; overflow: hidden; }</style>
</head>
<body>
 <script type="module">
    // 地址,升级为174版本
    import * as THREE from 'https://esm.sh/three@0.174.0';
    import { OrbitControls } from 'https://esm.sh/three@0.174.0/examples/jsm/controls/OrbitControls.js';
    import { GLTFLoader } from 'https://esm.sh/three@0.174.0/examples/jsm/loaders/GLTFLoader.js';
    // 1. 创建三大核心
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000);
    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);
    camera.position.set(0, 0, 300); // 相机后退,看清所有曲线

    // 2. 轨道控制器(3D视角交互)
    const controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;
    controls.dampingFactor = 0.05;

    // 3. 绘制2D椭圆
    const ellipseCurve = new THREE.EllipseCurve(0, 0, 80, 40);
    const ellipsePoints = ellipseCurve.getPoints(50);
    const ellipseGeo = new THREE.BufferGeometry();
    ellipseGeo.setFromPoints(ellipsePoints);
    const ellipseMat = new THREE.LineBasicMaterial({ color: 0xff0000 });
    const ellipseLine = new THREE.LineLoop(ellipseGeo, ellipseMat);
    scene.add(ellipseLine);

    // 4. 绘制3D样条曲线
    const catmullCurve = new THREE.CatmullRomCurve3([
      new THREE.Vector3(-100, 0, 0),
      new THREE.Vector3(-50, 80, 50),
      new THREE.Vector3(50, -80, 100),
      new THREE.Vector3(100, 0, 50)
    ]);
    const catmullPoints = catmullCurve.getPoints(100);
    const catmullGeo = new THREE.BufferGeometry();
    catmullGeo.setFromPoints(catmullPoints);
    const catmullMat = new THREE.LineBasicMaterial({ color: 0x0000ff });
    const catmullLine = new THREE.Line(catmullGeo, catmullMat);
    scene.add(catmullLine);

    // 5. 动画循环
    function animate() {
      requestAnimationFrame(animate);
      controls.update();
      renderer.render(scene, camera);
    }
    animate();

    // 6. 窗口适配
    window.addEventListener('resize', () => {
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(window.innerWidth, window.innerHeight);
    });
  </script>
</body>
</html>

示例效果

c7c96963-4107-4138-a72c-08caaa36d7f3.png

  1. 场景中显示红色2D椭圆(XY平面)和蓝色3D样条曲线(空间弯曲);
  2. 支持鼠标旋转/缩放视角,查看3D曲线的空间形态;
  3. 曲线平滑无锯齿,色彩无偏色。

五、注意事项与优化

1. 常见坑点

  • 线宽限制:WebGL标准中线宽(linewidth)仅支持1px,部分浏览器支持>1但兼容性差,如需粗线条建议用 Mesh 模拟(如挤压曲线成面);
  • 3D曲线视角:3D曲线需调整相机位置(Z轴后退),否则可能看不到;
  • 采样点数:复杂曲线(如三次贝塞尔)需增加采样点数(100~200),否则会出现锯齿;
  • CurvePath闭合closePath() 仅对2D曲线有效,3D曲线闭合需手动调整控制点。

2. 性能优化

  • 复用几何体:多个曲线复用同一个 BufferGeometry(清空顶点后重新绑定);
  • 减少采样点数:简单曲线(直线、圆弧)采样点数设50即可,无需过高;
  • 批量渲染:多个曲线合并为一个 Line 模型,减少渲染调用。

核心总结

  1. 核心流程创建曲线 → getPoints采样顶点 → 几何体绑定顶点 → Line/LineLoop渲染
  2. 曲线选型
    • 2D简单曲线:LineCurve/ArcCurve/EllipseCurve;
    • 2D复杂曲线:SplineCurve/CubicBezierCurve;
    • 3D路径:CatmullRomCurve3(推荐)/CubicBezierCurve3;
    • 复合曲线:CurvePath(组合多个子曲线);
  3. 关键参数
    • getPoints(n)n 决定平滑度,推荐50~100;
    • 3D曲线需调整相机Z轴位置,确保可见;
    • 贝塞尔曲线的「控制点」是决定曲线形状的核心。

Vue 中后台表格选型(Element/VXE/AntD):我在真实项目里踩过的坑,比 Demo 多得多

时间:2026/01/22

如果你的项目出现过这些情况:

  • 表格一加固定列就开始样式错位
  • demo 跑得完美,上线后不断改 bug
  • 合并单元格 + 虚拟滚动总会存在样式问题或者性能问题

别怀疑自己代码水平,90% 是选型问题。

在 Vue 生态里,表格组件的真正难点从来不是"有没有某个功能",而是:

当固定列、多级表头、单元格合并、虚拟滚动这些能力叠加时,它还能不能稳定工作。

这篇文章基于真实业务测试,对比 Element Plus / VXE / Ant Design Vue / TanStack 四大方案,只讲工程实践,不堆功能清单

一、结论前置:不同场景怎么选

场景 推荐方案 原因
小中型项目 + 追求稳定交付 Element Plus Table / Ant Design Vue 默认观感稳定,开箱即用
大数据量(1k 行以上) VXE Grid / Table V2 内建虚拟滚动,性能无压力
高度定制 + 团队能力强 TanStack Table+TanStack Virtual headless 架构,完全自主可控
表格是核心业务 + 复杂交互 VXE Grid 企业级能力最完整

一个底线必须明确:

一旦业务出现「合并 + 固定 + 虚拟滚动」组合,选型阶段不谨慎,后期一定持续返工。

二、真正拉开差距的 6 个关键点

1️⃣ 默认观感:不是"好看",而是"稳定"

很多人评价表格只看 UI 美不美,但工程上更重要的是:默认状态能不能直接上线

  • Element Plus / Ant Design Vue:边框、hover、斑马纹开箱即用,不需要额外调样式
  • VXE Grid:默认不启用 hover 高亮,新手容易误判为"交互不完整"
  • TanStack:完全 headless,所有样式自己写

个人测试中: 同样的需求,用 Element Plus 1 天交付,用 TanStack 可能要 3 天才能调好样式。

如果团队 UI 能力有限,默认观感稳定比可定制性更重要。

2️⃣ 滚动条体验:最容易被低估的致命伤

在这些场景组合下:

  • 固定表头 + 固定右列 + 纵向滚动

滚动条的同步性、对齐度、视觉一致性会直接影响可用性。

个人测试结论:

  • Element Plus 的 Scrollbar 在复杂固定列场景下UI最好
  • Ant / VXE 的滚动条看起来怪怪的,特别是表头

表格组件最容易被低估的不是功能,而是滚动条体验。

3️⃣ 虚拟滚动:1k 行数据的生存线

当数据量达到 1000 行以上

  • Element Plus Table / Ant Design Vue Table 已明显卡顿
  • 是否支持虚拟滚动,直接决定组件还能不能继续用
方案 行虚拟滚动 列虚拟滚动
Element Plus Table
Element Plus Table V2
Ant Design Vue Table
VXE Grid
TanStack Table 需接入 @tanstack/virtual 需接入 @tanstack/virtual

⚠️ Table V2 的坑:虚拟滚动开启后,单元格合并、固定列的组合行为会出现意外 bug。

虚拟滚动不是加分项,而是复杂中后台表格的生存线。

4️⃣ 单元格合并:真正难的是"组合行为"

合并本身不难实现,难的是它要和这些能力同时存在:

  • hover 高亮
  • 行选中 / 多选
  • 固定列边框

典型失败表现:

  • hover 背景不协调(合并区域的子单元格没有高亮)
  • 合并区域选中时复选框异常

个人测试中:

  • Element Plus Table:用 span-method,小规模场景稳定
  • Ant Design Vue也很nice
  • Table V2:简单 demo 没问题,复杂组合会集中暴露 bug
  • VXE Grid:内建合并能力最完善,边界情况处理最好
  • 行选中同单元格一起合并都不支持

如果你的表格需要「合并 + 固定列 + 虚拟滚动」同时存在,务必先做完整测试再选型。

5️⃣ 树形表格 + 懒加载:

树形表格看起来简单,但要支持懒加载 + 展开状态管理

  • Ant Design Vue:官方不支持树表懒加载(这是硬伤)
  • 其他方案:element-plus和vex内建支持

6️⃣ 列筛选:复杂时应该"脱离表头"

大部分表格组件的列筛选都只支持:

  • 简单选项列表
  • 单个输入框

当筛选条件开始出现:

  • 日期范围选择
  • 多条件联动(如:省市区联动)
  • 复杂的数值区间

更合理的做法是:独立查询区,而不是死磕表头。

方案 内建筛选能力
Element Plus Table 仅选项列表
Ant Design Vue Table 内建筛选 + 自定义
VXE Grid 筛选能力强,但复杂时需额外 UI 库
TanStack 完全自研

三、完整对比表(供选型参考)

维度 Element Plus Table Element Plus Table V2 Ant Design Vue Table VXE Grid TanStack Table
默认观感 ✅ 稳定 ✅ 稳定 ✅ 稳定(Ant 风格) ⚠️ hover 默认未开 ❌ 完全自研
滚动条体验 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐(自研)
行虚拟滚动 需接入 virtual
列虚拟滚动 需接入 virtual
单元格合并 span-method ⚠️ 坑多 rowSpan / colSpan ✅ 内建 自研
树形 + 懒加载 需自研 ❌ 不支持 自研
列筛选 仅选项 自研 内建 内建 自研
个性化列 自研 自研 自研 内建 toolbar 状态内建

四、工具链落地:从选型到上线

第一步:验证核心组合场景

不要只跑 demo,必须测试这些组合:

  1. 固定列 + 多级表头 + 单元格合并
  2. 虚拟滚动 + 树形结构 + 懒加载
  3. 1000 行数据 + 筛选 + 排序

测试清单:

// 1. 固定列对齐
- 横向滚动时左右固定列是否有阴影/边框
- 滚动条是否同步
- hover 高亮是否完整

// 2. 单元格合并
- 合并区域 hover 是否错位
- 固定列边框是否断裂
- 选中逻辑是否正常

// 3. 虚拟滚动
- 快速滚动时是否白屏
- 固定列是否错位
- 合并单元格是否异常

阶段总结

验证完这些组合场景,至少能排除 50% 的不适配方案。

第二步:评估团队能力与时间成本

团队情况 推荐方案
前端 2-3 人,追求快速交付 Element Plus / Ant Design Vue
有专职 UI 开发,追求定制化 TanStack + 自研
表格是核心业务,需要企业级能力 VXE Grid
大数据量 + 性能要求高 Table V2 / VXE Grid

真实项目经验:

  • 用 Element Plus Table和Ant Design Vue 做普通后台,1 周交付
  • 用 TanStack 做同样需求,3 周才稳定(样式 + 交互全自研)
  • 用 VXE Grid 做复杂报表,2 周交付(但学习成本稍高)

阶段总结

选型不仅是技术问题,更是时间成本 + 团队能力的权衡。

第三步:建立组件封装规范

无论选哪个方案,都要做二次封装:

<!-- 错误示范:直接用原始组件 -->
<el-table :data="tableData" ...>
  <el-table-column prop="name" .../>
</el-table>

<!-- 正确示范:封装业务组件 -->
<business-table
  :columns="columns"
  :data-source="dataSource"
  :row-key="rowKey"
/>

封装收益:

  • 统一默认配置(如 hover / 边框 / 斑马纹)
  • 统一 loading / error 处理
  • 统一分页逻辑
  • 后续替换方案成本低

阶段总结

二次封装不是过度设计,而是降低后期返工成本的必要手段。

五、如果让我重新选型

基于真实项目经验,我会这样选:

场景 1:普通中后台(CRUD 为主)

  • 首选:Element Plus TableAnt Desing Vue
  • 理由:稳定、文档全、生态好、招人容易

场景 2:数据量大(1k 行以上)

  • 首选:VXE Grid
  • 理由:虚拟滚动稳定、企业级能力完整

场景 3:高度定制(如数据可视化平台)

  • 首选:TanStack Table
  • 理由:headless 架构,完全可控

场景 4:预算充足 + 复杂交互

  • 可考虑:AG Grid(付费版)
  • 理由:企业级方案最成熟(但本文不展开)

场景5:在已有Element Plus或者Ant Design Vue的情况下,需要处理大数据

  • 可考虑再接入VXE Gid,甚至可能还要接入完整的Vxe UI,此外还要评估带来的css的副作用

但有一个底线:

一旦出现「合并 + 固定 + 虚拟滚动」组合,务必先做完整测试。 选型阶段省的时间,后期会加倍还回来。

六、常见问题速查

Q1:Table V2 和 VXE Grid 怎么选?

  • Table V2:Element 生态统一,学习成本低,但复杂组合有坑
  • VXE Grid:企业级能力最完整,但学习曲线陡、文档不如 Element 友好

Q2:一定要用虚拟滚动吗?

  • 数据量 < 500 行:不需要
  • 数据量 500-1000 行:建议用
  • 数据量 > 1000 行:必须用

Q3:TanStack 适合新手吗?

不适合。它是"表格引擎",不是"表格组件",所有 UI 要自己写。

Q4:已经用了不合适的方案,怎么办?

  • 如果只是样式问题:二次封装兜底
  • 如果是能力缺失:评估迁移成本,必要时重构
  • 如果是性能问题:优先上虚拟滚动或分页

参考链接

在线示例: astonishing-peony-a9d523.netlify.app 源码仓库: github.com/parade0393/…

最后提醒:

表格选型没有"最好",只有"最合适"。

但如果你不想后期持续返工,选型阶段多花 2 天做完整测试,绝对值得。

欢迎在评论区分享你踩过的坑 👇

在AI时代下,技术人要拥有内耗识别系统

内耗识别系统(当场可用)

内耗最危险的地方在于:它看起来像合理的思考。

所以你最需要的不是反省,而是一个即时识别装置。

内耗自检三问

每天只问以下三个问题。

不分析,不解释,只看答案。

1️⃣ 我现在在做的事,今天能 Ship 吗?

不能 -> 高风险内耗

能,但我在回避 -> 已发生内耗

2️⃣ 如果不完美,我还会继续吗?

不会 -> 自尊接入

会 -> 构建行为

3️⃣ 这个动作是在构建,还是在保护自尊?

这是最重要的问题,如果你犹豫了,答案通常已经很明显了。


“保护自尊”信号清单

以下信号一旦出现,说明你已经不在构建系统里。

典型表现包括:

  • 我再学一个相关技术再开始

  • 等别人给点反馈再说

  • 这个结构不太对,先优化一下

  • 先把文档写完整一点

  • 这个版本有点拿不出手

💡 必须提醒:一旦你在保护自尊,系统已经失效了。


内耗止损机制(关键执行)

这一节是反直觉的。

内耗出现时,你不能“想明白”

你可能会本能地想:

  • 再想清楚一点
  • 再理一理逻辑
  • 再等等状态

这是内耗最擅长的诱导。

必须记住:

内耗,无法靠思考解决,只能靠行动中断。


三步止损法(可执行)

不需要工具,不需要环境,不需要状态。

Step 1:命名(30秒)

直接说出来: “我现在是在 ___ 型内耗中”。

认知型、社交型、情绪型。

命名的作用只有一个:把你从“我就是这样”中拉出来。

Step 2:缩小(1分钟)

立刻把任务缩到:10分钟内必须能完成的版本。

不是推进项目,而是推进一个动作。

例如:写一句描述、改一个字段、截一张图。

Step 3:强制 Ship(不讲道理)

哪怕是一句话、一个截图、一个帖子。

原则只有一个:必须被看见。


为什么“强制 Ship”有效?

因为 Ship 做了三件事请:

1️⃣ 结束犹豫

2️⃣ 完成闭环

3️⃣ 把注意力拉回现实

📌 核心(请记住):情绪跟随行动,不是反过来,你不需要好状态,你只需要完成一个动作。


深入浅出 TinyEditor 富文本编辑器系列5:开发环境配置

你好,我是 Kagol,个人公众号:前端开源星球

TinyEditor 是一个基于 Quill 2.0 的富文本编辑器,在 Quill 基础上扩展了丰富的模块和格式,功能强大、开箱即用。

本文是《深入浅出 TinyEditor 富文本编辑器系列》文章的第5篇,主要介绍 TinyEditor 的开发环境配置。

搭建适当的开发环境对于为 TinyEditor 做贡献至关重要。本文将引导你完成整个流程,从克隆代码仓库到运行开发服务器。

前置条件

开始之前,请确保已安装以下工具:

  • Node.js:版本 18 或更高
  • pnpm:版本 9.13.0(项目的包管理器)
  • Git:用于版本控制

必须使用 pnpm,因为本项目使用 pnpm workspaces 进行 monorepo 管理。其他包管理器可能无法正常工作。

项目架构概述

TinyEditor 采用包含多个包的 monorepo 结构:

快速设置流程

设置过程可以可视化为一个简化的工作流:

分步设置

1. 克隆代码仓库

git clone git@github.com:opentiny/tiny-editor.git
cd tiny-editor

2. 安装依赖

项目使用 pnpm workspaces 管理多个包。使用以下命令安装所有依赖:

pnpm i

这将为 monorepo 中的所有包安装依赖,包括:

  • 核心编辑器库
  • 文档站点
  • 示例项目
  • 协作编辑后端

3. 启动开发服务器

对于一般开发,使用主要的开发命令:

pnpm dev

这将在 http://localhost:5173/tiny-editor/ 启动文档开发服务器。

开发工作流

核心库开发

要开发核心 fluent-editor 库:

pnpm watch

此命令以监听模式构建库,当源文件更改时自动重新构建。

文档开发

要开发文档:

# 文档开发服务器
pnpm -F docs dev

文档站点使用 VitePress,包含交互式演示和示例。

示例项目开发

要开发示例项目:

# 启动示例开发服务器
pnpm dev:projects

这将运行展示 TinyEditor 各种功能的示例项目。

协作后端开发

对于协作编辑功能:

# 启动协作后端
pnpm -F collaborative-editing-backend dev

这将为实时协作编辑启动 WebSocket 服务器。

包结构

Package 用途 开发命令
fluent-editor 核心编辑器库 pnpm watch
docs 文档站点 pnpm -F docs dev
projects 示例项目 pnpm dev:projects
collaborative-editing-backend 实时协作服务器 pnpm -F collaborative-editing-backend dev

构建命令

库构建

# 构建库用于发布
pnpm build:lib

这将构建库,同时输出 ES modules 和 CommonJS 格式。

文档构建

# 构建文档用于生产环境
pnpm build

项目构建

# 构建示例项目
pnpm build:projects

测试

项目使用 Jest 进行单元测试,使用 Playwright 进行端到端测试:

# 运行单元测试
pnpm test 
# 运行 E2E 测试
pnpm -F docs test 
# 安装 E2E 测试的浏览器依赖
pnpm install:browser

代码质量

项目使用 ESLint 配合 Antfu 配置来保证代码质量:

# 检查所有文件
pnpm lint 
# 修复检查问题
pnpm lint:fix

ESLint 配置支持 TypeScript、Vue,并强制执行一致的代码风格。

Git 钩子

项目使用 pre-commit 钩子来确保代码质量:

  • Pre-commit:运行 lint-staged 修复检查问题
  • Commit-msg:使用 verifyCommit.js 验证提交信息

开发技巧

开发核心库时,使用 pnpm watch 自动重新构建更改。库会以 ES modules 和 CommonJS 两种格式输出到 dist/ 目录。

Workspace 依赖

包使用 workspace 依赖(workspace:^)相互引用,确保在开发期间始终使用最新的本地版本。

补丁管理

项目使用 pnpm patches 修改 Quill 依赖。补丁文件位于 patches/quill@2.0.3.patch

开发环境为你提供了为 TinyEditor 做贡献所需的一切,无论是修复错误、添加功能还是改进文档。

联系我们

GitHub:github.com/opentiny/ti…(欢迎 Star ⭐)

官网:opentiny.github.io/tiny-editor

个人博客:kagol.github.io/blogs/

小助手微信:opentiny-official

公众号:OpenTiny

Vue2 后台管理系统整体缩放方案:基于 pxtorem 的最佳实践

使用 pxtorem 实现 Vue2 后台管理系统整体布局缩放的完整方案

适用场景:Vue2 + Element UI / Ruoyi 等传统后台管理系统
核心目标:在不破坏组件定位、不影响弹窗/浮层体验的前提下,实现“系统级整体缩放”


一、背景说明

笔者正在开发一款公司后台管理系统,由于该系统中的表单、表格数据字段十分庞大,默认的布局UI放不下那么多字段,用户在使用时经常需要缩放页面。所以这里我想能不能实现系统级的整体项目缩放,从而让用户使用更舒服

在后台管理系统中,表单字段多、表格列密集是非常常见的场景。随着业务复杂度提升,常规的布局方式逐渐暴露出几个明显问题:

  • 页面横向空间不足,需要频繁左右滚动
  • 浏览器缩放会影响字体清晰度与组件交互体验
  • UI 组件在缩放后容易出现错位(尤其是弹窗、下拉框)

用户的真实诉求并不是“放大字体”,而是“在同一屏内看到更多信息”

二、效果对比

默认布局 UI

字段密集,信息承载能力有限

默认布局


缩放后的布局 UI

在不改变业务结构的前提下,同屏可展示更多字段,尤其在多列表格场景中效果明显

缩放布局


三、方案选型与思路分析

在实现过程中,我一共尝试过 两种思路

❌ 方案一:CSS transform: scale() 整体缩放

实现方式

  • html / body 外包裹一层 wrapper
  • 使用 transform: scale() 对整个页面进行缩放

存在的问题

  • position: fixed / absolute 定位全部失真
  • ❗ Element UI 的 Dialog / Popover / Select 等组件定位错误
  • ❗ 实际缩放的是“视觉”,而非“布局”

结论:该方案不适合复杂后台系统,仅适用于展示型页面。


⭐ 方案二:postcss-pxtorem + 动态 rem(最终方案)

核心思想

  • 将整个项目中的 px 统一转为 rem,再通过动态修改根节点 font-size,实现真正意义上的“布局缩放”。

四、原理解析

4.1 为什么 pxtorem 在 Vue2 中可行?

在 Vue2 项目中:

  • UI 框架(如 Element UI)样式大多来源于 静态 CSS 文件
  • 构建阶段通过 Webpack + PostCSS 统一处理样式

这使得我们可以在 编译阶段

px  →  rem

再在 运行阶段

动态调整 html { font-size }

从而达到:一次转换,全局缩放


五、具体实现步骤

⚠️ 本方案 仅适用于 Vue2 项目
Vue3(如 Element Plus / Naive UI)因大量使用 JS 动态注入样式,不完全适用


5.1 安装依赖

npm install postcss-pxtorem@5.1.5 --save-dev

✅ 推荐使用 5.1.5,与 Vue2 + Webpack 兼容性最佳


5.2 配置 vue.config.js

module.exports = {
  css: {
    loaderOptions: {
      postcss: {
        plugins: [
          require('postcss-pxtorem')({
            rootValue: 16,              // 1rem = 16px(基准值)
            propList: ['*'],            // 转换所有属性
            selectorBlackList: [],      // 不参与转换的选择器
            minPixelValue: 0,           // 0px 以上全部转换
            exclude: /node_modules/i,   // 忽略 node_modules(可按需调整)
          })
        ]
      }
    }
  }
}

实践建议

  • 如果希望 UI 库也参与缩放,可移除 exclude
  • 或使用白名单方式精细控制
  • 关于参数的更多信息您可以直接访问官方文档:npm:postcss-pxtorem

5.3 在 main.js 中动态控制缩放比例

const html = document.documentElement
html.style.fontSize = '8px'  // 1rem = 8px(整体缩小 50%)

🔷 另外,在5.3的main.js中配置时,你也可以直接设置rootValue32,表示将每32px按照1rem的格式进行转换,和第3步的实现效果是一样的,可以按照需求选择

六、Ruoyi 动态主题不生效问题

6.1 问题原因

Ruoyi 的主题切换逻辑:

  • 使用 JS 动态生成 CSS
  • 直接插入到 <style> 标签中
  • 绕过了 PostCSS 编译阶段

导致:

部分样式仍然是 px,缩放失效


6.2 解决方案(核心代码)

src/components/ThemePicker/index.vue 中:

updateStyle(style, oldCluster, newCluster) {
  let newStyle = style

  oldCluster.forEach((color, index) => {
    newStyle = newStyle.replace(new RegExp(color, 'ig'), newCluster[index])
  })

  // 新增代码补丁:将 px 转为 rem , 这里的32就是同设置rootValue为32一样的道理,缩放1/2
  newStyle = newStyle.replace(/(\d+(.\d+)?)px/g, (match, p1) => {
    return (parseFloat(p1) / 32).toFixed(5) + 'rem'
  })

  return newStyle
}

七、总结建议

✅ 本方案的优势

  • 真正的“系统级布局缩放”
  • 不影响 Element UI 弹窗、浮层定位
  • 对业务代码零侵入

❌ 局限性

  • 不适合 Vue3 动态样式体系
  • 对第三方动态注入样式需额外处理

📌 适用场景

  • Vue2 后台管理系统
  • 表格字段密集型系统
  • 对信息密度有较高要求的中后台项目

深入浅出 JavaScript 柯里化:从原理到高级实践

在函数式编程的世界里,有一个优雅而强大的概念被称为“柯里化”(Currying)。很多开发者在面试中遇到过它,或者在 Redux、Ramda 等库中见到过它的身影,但往往只停留在“面试八股文”的层面。

今天,我们将结合实际代码案例,深入剖析柯里化的本质、通用实现方案以及它在现代前端工程中的实际价值。

1. 什么是柯里化?

简单来说,柯里化(Currying)是指将一个多参数函数转换为一系列单参数函数的技术

在传统的 JavaScript 开发中,我们习惯了一次性传递所有参数。例如,一个简单的加法函数通常是这样写的:

// 普通函数:一次性接受所有参数
function add(a, b) {
    return a + b;
}
console.log(add(1, 2)); // 输出 3

而在柯里化的世界里,我们通过嵌套函数的方式,让参数“一个一个地传递”。

// 手动柯里化:参数一个一个传递
function add(a) {
    return function(b) {
        return a + b;
    }
}
// 调用方式变为 add(1)(2)
console.log(add(1)(2)); 

这种形式虽然看起来只是语法的改变,但其背后的数学模型发生了变化:我们将一个 f(a,b)f(a, b) 的函数转换为了 f(a)(b)f(a)(b)

2. 核心原理:闭包与参数收集

柯里化的本质不仅仅是参数传递方式的改变,其核心在于闭包(Closure) 。闭包的作用是记住函数的参数,形成一个“闭包链”。每一层函数都可以接收自己的参数,并借助闭包长久地保存这些变量。

柯里化的两种形态

在实际应用中,柯里化主要分为两种形态,理解它们的区别对于编写灵活的代码至关重要:

  1. 严格柯里化(Strict Currying):

    函数必须接受单参数,必须一步步调用。例如 log('error')('message')。如果试图一次性调用 log('error', 'message'),不仅无效,甚至可能报错或返回错误的函数句柄。

  2. 非严格柯里化(Loose/Dynamic Currying):

    这是工程中更通用的形式。它既允许你一个一个传参,也允许一次传多个。只要收集到的参数数量不够,它就返回新函数;一旦够了,就执行原函数。

3. 进阶:手写一个通用的 Curry 函数

理解了原理,我们不仅要会写简单的 add(a)(b),更要能够实现一个通用的工具函数,将任意普通函数转化为柯里化函数。

以下是一个经典的非严格柯里化通用实现:

// 辅助函数:只负责转换,不负责具体逻辑
function curry(fn) {
    // 1. 获取原函数的参数个数 (fn.length)
    
    // 2. 返回一个递归函数 curried,用于收集参数
    return function curried(...args) {
        // 3. 退出条件:如果收集的参数个数 >= 原函数需要的个数
        if(args.length >= fn.length){
            return fn(...args); // 执行原函数并返回结果
        }
        
        // 4. 参数不够时,返回一个新的匿名函数,继续接收剩余参数 (...rest)
        // 这里利用闭包,将当前的 args 和新传入的 rest 合并,递归调用 curried
        return (...rest) => curried(...args, ...rest);
    } 
}

// 原始的多参数函数
function add(a, b, c, d) {
    return a + b + c + d;
}

// 转化为柯里化函数
const curriedAdd = curry(add);

// 灵活的调用方式
console.log(curriedAdd(1, 2)(3, 4)); // 等价于 add(1, 2, 3, 4)
// 也可以 curriedAdd(1)(2)(3)(4)

代码解析

这个实现展示了柯里化的精髓:

  • 闭包的持久性: fn 是自由变量,args 在递归过程中不断累积,不会被销毁。
  • 递归扫描: 只要参数不足,就递归返回新的函数,直到满足 args.length >= fn.length
  • 灵活性: 支持 add(1, 2)(3) 这种混合调用,比严格柯里化更实用。

4. 为什么要用柯里化?工程实战场景

许多开发者会问:“直接调 add(1, 2, 3, 4) 不是更省事吗?”

柯里化真正的威力在于参数预设(Partial Application)和提升代码语义。当某些参数在特定场景下是固定的,或者是异步分批次到达(如大模型流式返回)时,柯里化能极大地简化代码逻辑。

场景一:日志系统的语义化

假设我们有一个通用的日志工具:

const log = type => message => {
    console.log(`${type}: ${message}`);
}

在实际开发中,我们可能需要频繁打印 Error 类型的日志。如果不使用柯里化,每次都要写 log('error', '数据库连接失败')

利用柯里化,我们可以“预设”第一个参数,生成具有特定语义的新函数:

// 利用柯里化“固定”第一个参数
// 这个过程叫做 参数预设(partial application)
const errorLog = log("error");
const infoLog = log("info");

// 现在,调用者只需要关注具体的业务信息
errorLog("接口异常");      // 输出:error: 接口异常
infoLog("页面加载完成");    // 输出:info: 页面加载完成

这样做的好处显而易见:

  1. 代码可读性更高: errorLoglog('error', ...) 更直观。
  2. 专注度提升: 新函数通过“固化”一部分参数,变得更加专注。
  3. 复用性增强: 基础函数 log 可以被复用生成各种特定场景的日志工具。

场景二:处理异步数据流

在现代大模型应用或复杂的异步交互中,函数需要的参数往往不是一次性拿到的。

  • 可能参数 A 来自用户的初始配置。
  • 可能参数 B 来自几秒后的服务器响应。

柯里化允许我们“参数一个一个地传递”,在参数没齐之前,函数处于“等待”状态(返回新函数),一旦数据流结束参数凑齐,自动执行逻辑。这完美契合了异步数据流的处理需求。

5. 总结

柯里化不仅仅是一个函数式编程的技巧,它更是一种代码组织思想。

  • 定义上,它是将多参转单参的技术。
  • 实现上,它依赖闭包来保持状态,依赖递归来收集参数。
  • 应用上,它帮助我们实现参数预设(Partial Application),让代码更加语义化、模块化,并且能够优雅地处理参数分批到达的场景。

当你发现代码中存在大量重复的参数传递,或者需要将一个通用函数“特化”为专用函数时,不妨试试柯里化。

一行代码解决文本溢出提示:Vue 3 + Element Plus 打造智能 v-ellipsis-tooltip 指令

前言

在 B 端业务开发中,表格和列表是出现频率极高的场景。我们经常遇到这样的需求: “当文本内容过长导致显示省略号时,鼠标悬停显示完整内容的 Tooltip;如果文本未溢出,则不显示 Tooltip。”

通常的做法是:

  1. 给元素设置 CSS 省略样式。
  2. 套一层 el-tooltip
  3. 通过 disabled 属性控制是否显示。

但是,手动计算 disabled 状态非常繁琐,需要获取 DOM 元素判断 scrollWidth > clientWidth,如果在表格中使用,每个单元格都要写一套逻辑,代码重复率极高且难以维护。

今天,我们来封装一个 Vue 3 自定义指令 v-ellipsis-tooltip,彻底解决这个问题。

核心思路

我们的目标是实现一个指令,挂载到元素上即可自动检测溢出并挂载 Tooltip。

核心步骤如下:

  1. 检测溢出:比较元素的 scrollWidthclientWidth
  2. 动态渲染:如果溢出,使用 Vue 的 h 函数和 render 函数动态创建一个 ElTooltip 组件。
  3. 状态管理:使用 WeakMap 存储每个 DOM 元素对应的 Tooltip 实例和状态,防止内存泄漏。
  4. 响应式更新:利用 ResizeObserver 监听元素尺寸变化,实时更新 Tooltip 状态。

代码实现

以下是完整的指令实现代码。注意项目中使用了 unplugin-auto-import,所以 hDirectiveBinding 等 API 是自动导入的。如果你没有配置自动导入,请手动补充 import。

import type { ElTooltipProps } from 'element-plus'
import type { Directive, DirectiveBinding } from 'vue'
import { ElTooltip } from 'element-plus'
import { render, h } from 'vue' // 如果没有自动导入,需要手动引入 h

type TooltipValue = string | (Partial<ElTooltipProps> & { content?: string, observe?: boolean })

interface TooltipContext {
  container: HTMLElement
  binding: DirectiveBinding<TooltipValue>
  observer?: ResizeObserver
}

// 使用 WeakMap 存储上下文,避免直接修改 DOM 对象类型和使用 any
const contextMap = new WeakMap<HTMLElement, TooltipContext>()

/**
 * 核心渲染逻辑:根据溢出状态和配置渲染 Tooltip
 */
const renderTooltip = (el: HTMLElement, binding: DirectiveBinding<TooltipValue>) => {
  const { value, instance } = binding
  // 1. 检测溢出
  // scrollWidth > clientWidth 说明水平方向溢出
  // scrollHeight > clientHeight 说明垂直方向溢出(针对多行省略场景)
  const isOverflow = el.scrollWidth > el.clientWidth || el.scrollHeight > el.clientHeight

  // 2. 解析配置
  let content = ''
  let props: Partial<ElTooltipProps> = {}

  if (typeof value === 'string') {
    content = value
  }
  else if (value && typeof value === 'object') {
    content = value.content ?? ''
    props = value
  }

  // 如果没有提供 content,回退到元素文本
  if (!content) {
    content = el.textContent || ''
  }

  // 3. 创建 Virtual Tooltip
  // 利用 Element Plus 的 virtualTriggering 能力,将 Tooltip 绑定到当前元素
  const vnode = h(ElTooltip, {
    virtualTriggering: true,
    virtualRef: el,
    placement: 'top',
    ...props,
    content,
    disabled: props.disabled ?? !isOverflow, // 优先使用用户配置,否则根据溢出状态自动控制
  })

  // 注入上下文以继承全局配置(如 Element Plus 的 ConfigProvider)
  if (instance && instance.$) {
    vnode.appContext = instance.$.appContext
  }

  // 4. 渲染到内存中的 container
  const ctx = contextMap.get(el)
  if (ctx) {
    render(vnode, ctx.container)
  }
}

/**
 * 管理 ResizeObserver 的启用/禁用
 */
const manageObserver = (el: HTMLElement, binding: DirectiveBinding<TooltipValue>, ctx: TooltipContext) => {
  // 支持通过指令值或修饰符开启监听
  const shouldObserve = (typeof binding.value === 'object' && binding.value?.observe) || binding.modifiers.observe

  if (shouldObserve) {
    if (ctx.observer) {
      return
    }
    // 当元素尺寸变化时,重新检测溢出状态
    ctx.observer = new ResizeObserver(() => renderTooltip(el, ctx.binding))
    ctx.observer.observe(el)
  }
  else {
    if (!ctx.observer) {
      return
    }
    ctx.observer.disconnect()
    ctx.observer = undefined
  }
}

export const vEllipsisTooltip: Directive<HTMLElement, TooltipValue, 'observe'> = {
  mounted(el: HTMLElement, binding: DirectiveBinding<TooltipValue>) {
    const ctx: TooltipContext = {
      container: document.createElement('div'), // 创建一个游离的 div 作为渲染容器
      binding,
      observer: undefined,
    }
    contextMap.set(el, ctx)

    manageObserver(el, binding, ctx)
    renderTooltip(el, binding)
  },

  updated(el: HTMLElement, binding: DirectiveBinding<TooltipValue>) {
    const ctx = contextMap.get(el)
    if (!ctx) {
      return
    }
    ctx.binding = binding
    manageObserver(el, binding, ctx)
    renderTooltip(el, binding)
  },

  beforeUnmount(el: HTMLElement) {
    const ctx = contextMap.get(el)
    if (!ctx) {
      return
    }
    ctx.observer?.disconnect()
    render(null, ctx.container) // 卸载组件,触发 unmounted 生命周期
    contextMap.delete(el)
  },
}

使用方法

1. 基础用法

最简单的场景,直接加上 v-ellipsis-tooltip。注意元素本身需要有 CSS 省略样式(overflow: hidden; text-overflow: ellipsis; white-space: nowrap;)。

<div 
  class="truncate w-200px" 
  v-ellipsis-tooltip
>
  这段文字很长很长,如果超出会显示省略号,并且鼠标悬停会有 Tooltip。
</div>

2. 自定义内容

如果你希望 Tooltip 显示的内容与元素文本不同,可以传入字符串。

<div 
  class="truncate w-200px" 
  v-ellipsis-tooltip="'这是自定义的 Tooltip 内容'"
>
  显示的文本...
</div>

3. 传递 Element Plus Props

需要配置 placementeffect 等属性时,传入对象即可。

<div 
  class="truncate w-200px" 
  v-ellipsis-tooltip="{ 
    content: '深色主题提示', 
    effect: 'dark', 
    placement: 'bottom' 
  }"
>
  显示的文本...
</div>

4. 响应式监听 (ResizeObserver)

如果容器宽度是动态变化的(例如拖拽改变列宽),普通的检测可能只在 mounted 时生效。加上 .observe 修饰符,让指令监听元素尺寸变化,实时更新 Tooltip 状态。

<div 
  class="truncate" 
  style="width: 50%"
  v-ellipsis-tooltip.observe
>
  宽度变化时会自动重新计算是否溢出
</div>

遇到的坑与细节

  1. Context 丢失问题:在使用 render 函数手动渲染组件时,新组件会丢失当前的 appContext,导致无法获取全局配置(如 Element Plus 的 localez-index 配置)。解决方案是将 vnode.appContext 指向 instance.$.appContext
  2. Virtual Triggering:Element Plus 的 ElTooltip 支持 virtual-triggering 模式,这使得我们可以不改变 DOM 结构,直接将 Tooltip 逻辑附加到现有元素上,非常适合指令封装。
  3. 内存泄漏:一定要在 beforeUnmount 中销毁 ResizeObserverrender(null, container),并清理 WeakMap

总结

通过这个指令,我们成功将“溢出检测”与“Tooltip 显示”逻辑解耦,保持了模板的整洁。在表格、卡片列表等密集展示数据的场景下,极大地提升了开发效率和用户体验。


希望这篇文章对你有帮助!如果觉得有用,请点赞收藏支持一下~

彻底搞懂 CSS 定位:relative 与 absolute 的奥秘

在前端开发中,CSS 定位 (position) 是布局的基石之一。relativeabsolute 是最常用也最容易混淆的两种定位方式。掌握它们的区别和用法,对于构建复杂、精确的页面布局至关重要。今天,我们就来深入剖析这两个定位属性的工作原理和应用场景。

1. 一切的起点:static (默认定位)

在讨论 relativeabsolute 之前,我们先明确一个概念:static

  • 定义: static 是元素的默认定位方式

  • 特点:

    • 元素按照正常的文档流(Normal Flow)从上到下、从左到右依次排列。
    • top, right, bottom, left 这些偏移属性对 static 定位的元素无效
    • 元素会出现在它在 HTML 源码中的自然位置。

理解 static 是理解其他定位方式的基础,因为它代表了“无特殊定位”的状态。

2. 相对定位:position: relative

  • 作用: 元素仍然占据着它在正常文档流中的原始位置

    • 你可以使用 top, right, bottom, left 属性来相对于它原本的位置进行偏移
    • 偏移之后,原来的空间仍然保留,不会被其他元素占据。这可能导致元素重叠。
  • 关键点:

    • 参照物: 元素相对于它原本在文档流中的位置进行移动。

    • 脱离文档流? : 没有。它依然占据着原始空间,周围的元素会认为它还在那里。

    • 用途:

      • 微调元素位置:当需要对某个元素进行轻微的位置调整,但又不想影响页面其他元素的整体布局时,relative 非常有用。
      • 最重要的用途:为绝对定位的子元素提供定位基准。当一个元素设置了 position: relative,它就成为了其内部设置了 position: absolute 的子元素的定位参考点 (Containing Block)
  • 示例代码:

<!DOCTYPE html>
<html>
<head>
<style>
.relative-box {
  position: relative;
  top: 20px; /* 相对于原始位置向下移动 20px */
  left: 30px; /* 相对于原始位置向右移动 30px */
  background-color: lightblue;
  width: 200px;
  height: 100px;
}
.text-after {
  background-color: lightgreen;
}
</style>
</head>
<body>
  <div class="relative-box">我是一个相对定位的元素</div>
  <p class="text-after">这段文字会出现在 .relative-box 原本应该在的地方,即使它已经移动了。</p>
</body>
</html>

在这个例子中,蓝色的 div 移动了,但它原来的空间(灰色虚线框示意)依然存在,绿色文字紧随其后。

3. 绝对定位:position: absolute

  • 作用:

    • 元素完全脱离正常文档流
    • 它不再占据文档流中的空间,周围的元素会忽略它的存在,好像它不存在一样。
    • 你可以使用 top, right, bottom, left 属性来相对于其最近的已定位祖先元素(即 position 属性为 relative, absolute, fixed, 或 sticky 的祖先元素)进行定位。
    • 如果其所有祖先元素都没有设置定位(即都是 static),那么它将**相对于初始包含块(通常是视口 viewport)**进行定位。
  • 关键点:

    • 参照物: 相对于最近的已定位祖先元素position 不是 static 的祖先)。如果没有这样的祖先,则相对于视口

    • 脱离文档流? : 是的。它不再占用文档流中的空间。

    • 用途:

      • 实现精确的位置控制:常用于创建弹窗、提示框、工具提示、覆盖层、图标定位、侧边栏等需要精确定位在某个位置的元素。
      • 创建不依赖于文档流顺序的布局
  • 示例代码:

<!DOCTYPE html>
<html>
<head>
<style>
.parent-container {
  position: relative; /* 关键:为子元素提供定位基准 */
  width: 300px;
  height: 200px;
  background-color: lightblue;
  margin-top: 50px; /* 方便观察 */
}
.absolute-child {
  position: absolute;
  top: 10px; /* 相对于 .parent-container 的顶部 */
  left: 10px; /* 相对于 .parent-container 的左侧 */
  background-color: red;
  width: 50px;
  height: 50px;
}
</style>
</head>
<body>
  <div class="parent-container">
    父容器
    <div class="absolute-child">绝对定位子元素</div>
  </div>
</body>
</html>

在这个例子中,红色的 div 是绝对定位的,并且它的父元素(蓝色的 div)是相对定位的。因此,红色 divtop: 10pxleft: 10px 是相对于蓝色 div 的左上角计算的。同时,红色 div 不会占用蓝色 div 内部文档流的空间。

4. relativeabsolute 的关系

relativeabsolute 经常配合使用。relative 最重要的作用之一就是absolute 子元素创建一个定位上下文

  • 当父元素设置了 position: relative(或 absolute, fixed),它就成为了子元素 position: absolute 的参考坐标系。
  • 子元素的 top, right, bottom, left 将不再是相对于浏览器窗口,而是相对于这个设置了定位的父元素。

5. 对比总结

特性 position: static (默认) position: relative (相对定位) position: absolute (绝对定位)
是否脱离文档流
是否占据空间 是 (移动后原位置仍保留)
top/right/bottom/left 是否有效 是 (相对于原始位置偏移) 是 (相对于定位祖先元素或视口)
定位参考点 无 (遵循文档流) 元素自身的原始位置 最近的已定位祖先元素 (或视口)
主要用途 正常布局 微调位置、为绝对定位子元素提供参考 精确控制位置、创建悬浮元素

结语

理解 relativeabsolute 的核心区别在于是否脱离文档流以及定位的参考点。relative 是“我动了,但我原来的地方还在”,常用于为 absolute 子元素划定一个活动范围。absolute 是“我自由了,我不占地方了,我想在哪就在哪(相对于我的定位祖先)”。掌握好这对组合,你就能更灵活地掌控页面元素的布局了。

Vue 列表渲染设计决策表(v-for source)

使用场景:

  • v-for
  • computed → v-for

一、v-for 数据源设计(最核心)

问题 必须满足 正确做法 错误做法 后果
v-for 的 source 初始值是什么? 必须可遍历 [] / {} / Map() undefined / null / false 列表语义无法建立
source 的类型是否稳定? 类型不可跃迁 [] → [...] undefined → [] diff 通道缺失
是否依赖 ?. 比如 v-for = listItem in object?.list object?.list不能是undefined 初始值:object=reactive({list:[]}) 初始值:object=ref(undefined) 列表语义无法建立

二、computed + v-for 决策

场景 推荐 不推荐 原因
computed 作为 v-for source 返回 [] 返回 undefined undefind会导致未建立列表语义结构
computed 内部判空 ?. ?? [] if (!x) return 避免短路,短路会导致v-for source为undefined
computed 首次执行 访问完整结构 return {list:[]}这样的预定义稳定结构 v-for source只会在首次建立列表语义结构,即使source值变化了,也不会再重新建立语义结构
v-for 绑定 computedList computed?.list v-for source不能依赖?.,因为可能返回undefined,会导致未定义列表结构语义

三、看到v-for检查设计列表

  • v-for 的 source 第一次 render 是不是数组 / 对象?

  • 是否存在 undefined → array 的路径?

  • 是否用了 ?. 直接喂给 v-for?

  • computed 是否可能 return 非遍历值?

四、可以反复使用的代码模版

const state = reactive({
  list: [],
  loading: true
})

onMounted(async () => {
  state.list = await fetchList()
  state.loading = false
})
<template>
  <div v-if="state.loading">loading...</div>
  <div v-else>
    <div v-for="item in state.list" :key="item.id" />
  </div>
</template>

五、关于v-for的统一心智模型

你可以把 Vue 渲染分成三层:

① 编译期(决定结构)
② 首次 render(建立语义)
③ diff 更新(只做比较)

v-for 的“可遍历语义”只在第 ② 步建立一次

如果你在第 ② 步给了:

  • undefined
  • null
  • false

👉 后面改不回来了,即使v-for source变了也不会重新建立

结果:列表一定始终渲染不出来

编译期报错的后果

如果在编译期v-for source = souceObject.property,而且source初始值为null,那么必然报错Can not read property of null (reading property),编译期报错会导致白屏

echarts的亿级渲染性能优化

由于业务每秒极限能生成数十万的数据,echarts的单条曲线就可能就有千万到十亿级的数据量。即使echarts本身具有采样能力,但这个量级的数据js内存本身就承载不了。

为了提升整体性能,我们进行了以下技术改进:

一、流式传输或websocket

全量查询即便后端也会因构建的json过大而出现内存溢出,而且大数据量一次性下载也需要等待很久。因此我们前后端采用了流式传输,基于SSE的协议进行数据解析,边传输边渲染,可以很好地减少页面loading时间。

image.png

不过这里更推荐使用websocket,直接传输二进制数据,避免构建json,也方便前端后续处理。

二、采样

最关键的部分就是采样。比如按最常见的分辨率1920,dpr为2来计算,曲线图即便最大,其像素点数量也远远小于需要渲染的数据,因此在拿到数据后可以进行二次采样。采样算法根据不同场景,可以选择固定步长的系统采样或尽可能保留形状的lttb(Largest-Triangle-Three-Bucket)算法。

结合前面的流式传输,在接收到数据片段时就进行采样并渲染,并在数据全部接收完成后,针对已有全量数据再次采样,并渲染完整图形。

每根曲线采用后的数据量,建议不超过200万——后文会详细解释200万数据的内存大小。

引申:lttb算法

lttb(Largest Triangle Three Buckets)算法是一种高保真的折线数据降采样算法,它通过在数据分桶后选择能最大化三角形面积的点,在大幅减少数据量的同时,最大程度保留原始数据的趋势特征(峰值、谷值、拐点)。

lttb算法大致原理如下图,首先根据采样率分桶,比如10采1则可以每10个点分一个桶,每个桶内取一个点。根据上一个保留点和当前桶内的候选点、下一个桶的中点计算三角形面积,取能形成最大面积的点作为这个桶的保留点。(此处面积计算可以使用向量叉积)

image.png

三、内容裁剪

内容裁剪主要分为两个部分,冗余字段删除和区间查询。

曲线需要的数据只有time和value,因此在后端传输时就将额外字段全部剔除。

同时前端查询时,根据当前曲线图的缩放范围进行查询,比如当前窗口的x轴范围是100 ~ 200,则查询80 ~ 220区间。这里为了避免echarts缩放失效,要额外增加一定的冗余区间。

image.png

四、IDB存储

全量数据的传输和采样成本很高,因此每当曲线全量采样完成后,可以针对采样数据存储到本地IndexedDB中,当二次查询该曲线时可以直接使用本地的。

这里有两个难点:存储时机和清理时机的选择。

IDB的存储和清理时机

存储时机的最大问题在于,如何判断数据已经传输完毕。

一方面流式传输过程中用户可能关闭曲线而取消后续传输,另一方面如果数据还在持续入库就查看曲线,也会导致数据传输不完整。前者比较简单,通过AbortController取消传输时就可以判断;而后者最简单粗暴的方法是,如果最后一条数据的时间小于查询区间,就认为数据尚未入库完成。当然,如果能拿到当前数据消费状态,或者能够根据业务规则判断数据完整性就更好。

清理时机的可以根据业务需求,可以按天清理或按项目维度清理或者按使用量清理(navigator.storage.estimate()可以获取IndexedDB占用存储大小)。

image.png

虽然IndexedDB可以持久化存储,但曲线查看操作往往是临时性的,因此我的决策更激进:每次页面打开(或刷新)就清理。

五、采用TypedArray而非Array

前文有提到1亿条数据建议采样到200万条,那么200万条数据实际占用内存多少呢?

    const array = [];

      const step = 0.01;
      let x = 0;
      while (x < 20000) {
        const y = Math.sin(x);
        array.push([x, y]);
        x = Number((x + step).toFixed(2));
      }

image.png如果是单一的200万元素二维数组,占用91738kB,也就是100M左右。 然而如果将其传给echarts,数组会被拷贝和包装——下图中三个数组都有200万个元素。

image.png

整个曲线图仅一根曲线就占用内存达到了624M! 但如果改用Float64Array,内存降到了142MB,明显更能接受(js内存限制为4G)。同样200万条数据,Float64Array结构占用内存约为30M,只有普通Array的30%!(如果只看浅层大小,两者差距更明显,引申中有解释浅层大小差距)

image.png

引申1: js内存大小限制

js内存可以通过window.performance.memory查看 image.png

关键指标说明

  • usedJSHeapSize:当前 JS 堆已使用的内存;
  • totalJSHeapSize:浏览器为 JS 堆分配的总内存;
  • jsHeapSizeLimit:浏览器允许 JS 堆使用的最大内存(我的是4G)。

引申2: 堆快照中的名词解释

  • 距离:对象到垃圾回收根(GC Roots)的最短路径长度,数字越大嵌套越深。如果是-,要么已经被gc回收了,要么将在下一次gc会被回收。
  • 浅层大小:对象本身占用的内存
  • 保留的大小:如果删除该对象,能释放的总内存量,包括对象本身以及它唯一引用的其他对象

引申3: 从backing_store到 Native Heap 与 JS Heap

TypedArray的内存机制和普通Array不一样,简单来说TypedArray仅仅是一个视图对象,具体内容存在ArrayBuffer中,而ArrayBuffer的内容存在Native Heap中,并不占用JS Heap。相关具体解释可见juejin.cn/post/759705…

六、增量渲染

echarts的文档也指出在百万级以上数据量时,最好使用增量渲染 image.png

参考

大数据的渲染其实有很成熟的方案了,有需要的可以直接使用perspective: perspective-dev.github.io/examples/

【节点】[Billboard节点]原理解析与实际应用

【Unity Shader Graph 使用与特效实现】专栏-直达

Billboard节点是UnityShaderGraph中一个功能强大的顶点变换工具,专门用于实现面向相机的渲染效果。在实时渲染中,Billboard技术被广泛应用于粒子系统、植被渲染、UI元素和特效制作等领域,能够确保特定物体始终面向摄像机,从而提供最佳的视觉效果。

Billboard技术概述

Billboard技术源于计算机图形学中的精灵渲染概念,其核心思想是通过动态调整物体的朝向,使其始终面对观察者。这种技术在游戏开发中具有重要价值:

  • 在粒子系统中用于渲染烟雾、火焰、魔法效果等动态元素
  • 在开放世界游戏中用于优化树木和植被的渲染性能
  • 在UI系统中确保界面元素始终以正确角度显示
  • 在特效制作中创建各种视觉欺骗效果

UnityShaderGraph中的Billboard节点封装了这一复杂技术,让开发者能够通过可视化方式轻松实现面向相机的渲染效果,无需编写复杂的着色器代码。

节点端口详解

Billboard节点包含多个输入和输出端口,每个端口都有特定的功能和用途。

输入端口

Position OS端口接收物体空间的顶点位置数据。这个端口是Billboard变换的基础,提供了需要进行旋转的原始顶点坐标信息。在实际应用中,这个端口通常直接连接到顶点着色器的位置输出,或者与其他位置变换节点相连。

Normal OS端口处理物体空间的法线向量。法线数据对于光照计算至关重要,Billboard节点会对法线进行相应的旋转,确保光照效果在物体旋转后仍然正确。如果忽略法线变换,可能会导致光照异常或材质表现不正确。

Tangent OS端口管理物体空间的切线向量。切线主要用于法线贴图和某些高级着色效果,Billboard节点会同步旋转切线数据,保持与顶点和法线的一致性。在需要复杂材质表现的场景中,正确的切线变换尤为重要。

输出端口

Position输出端口提供旋转后的物体空间顶点位置。这是Billboard节点的核心输出,包含了经过相机对齐变换后的顶点坐标。这个输出通常直接连接到主节点的顶点位置输入,完成最终的顶点变换。

Normal输出端口返回旋转后的物体空间法线向量。变换后的法线确保了光照计算与物体新朝向的一致性,对于保持材质视觉真实性至关重要。

Tangent输出端口提供旋转后的物体空间切线向量。这个输出确保了法线贴图和其他依赖切线空间的着色效果能够正确工作。

控件参数解析

Billboard Mode是Billboard节点最重要的控制参数,决定了物体的对齐方式和旋转行为。

All Axis模式

All Axis模式实现完全相机对齐,物体的所有坐标轴都会与相机坐标系对齐。在这种模式下,物体会完全面向相机,类似于始终正对观察者的广告牌。

这种模式的特点包括:

  • 物体完全面向相机,保持正面朝向观察者
  • 所有轴向都会根据相机方向进行旋转
  • 适用于需要完全正面展示的效果,如粒子特效、公告板文字等
  • 在VR和AR应用中特别有用,确保UI元素始终面向用户

All Axis模式的一个典型应用场景是粒子系统中的精灵渲染。当相机移动时,每个粒子都会自动调整方向,始终以最佳角度面向观察者,从而保证视觉效果的一致性。

Around Y Axis模式

Around Y Axis模式提供受限的对齐方式,物体仅围绕Y轴旋转,保持Y轴方向不变。这种模式在保持物体部分方向稳定的同时,实现基本的面向相机效果。

这种模式的特点包括:

  • 物体围绕世界空间或物体空间的Y轴旋转
  • X轴和Z轴与相机对齐,但Y轴保持原有方向
  • 适用于树木、路灯等需要保持垂直方向的物体
  • 在开放世界游戏中广泛用于植被渲染优化

Around Y Axis模式在大型场景的性能优化中特别有用。通过将3D树木替换为Billboard四边形,可以大幅减少渲染负载,同时通过限制Y轴旋转保持视觉上的自然感。

技术实现原理

理解Billboard节点的内部工作原理有助于更好地使用和调试相关效果。

顶点变换矩阵

Billboard节点的核心是基于视图矩阵的逆向变换。本质上,它计算相机的旋转矩阵,然后将这个旋转应用于输入的顶点数据。在All Axis模式下,节点会提取相机的完整旋转矩阵;而在Around Y Axis模式下,则会提取并修改旋转矩阵,将Y轴分量重置为单位矩阵的Y轴。

数学上,这个过程可以表示为:

旋转矩阵 = 提取相机旋转矩阵
如果模式为Around Y Axis:
    旋转矩阵[1] = [0, 1, 0] // 重置Y轴
变换后位置 = 旋转矩阵 × 原始位置

法线和切线变换

法线和切线的变换遵循与位置数据相同的旋转逻辑,但由于它们是方向向量而非位置点,变换时不考虑平移分量。正确的法线和切线变换确保了光照和材质效果在Billboard变换后仍然保持视觉一致性。

法线变换需要特别注意,由于法线是协变向量,其变换矩阵通常为顶点变换矩阵的逆转置矩阵。但在Billboard这种纯旋转的情况下,由于旋转矩阵是正交矩阵,逆转置矩阵等于原矩阵,因此可以直接使用相同的旋转矩阵。

实际应用案例

Billboard节点在游戏开发中有多种实际应用,以下是一些典型场景。

粒子系统效果

在粒子系统中,Billboard技术是创建各种视觉特效的基础。

火焰和烟雾效果可以通过Billboard四边形配合透明度渐变纹理实现。每个粒子都是一个面向相机的四边形,使用噪声纹理和颜色渐变创建动态的火焰和烟雾外观。通过All Axis模式确保无论相机如何移动,效果都能正确显示。

魔法和能量场效果利用Billboard节点创建环绕角色的魔法光环或能量屏障。结合扭曲效果和发光着色器,可以制作出视觉上吸引人的魔法特效。Billboard确保这些效果始终面向玩家,提供最佳的视觉体验。

环境装饰优化

在大型开放世界游戏中,Billboard技术是性能优化的重要手段。

树木和植被渲染使用Around Y Axis模式的Billboard技术,将复杂的3D树木模型替换为简单的四边形,大幅减少三角形数量。当玩家距离较远时,使用Billboard树木;当玩家靠近时,逐渐淡入完整的3D模型。这种LOD(层次细节)策略在保持视觉质量的同时显著提升性能。

远处山脉和云层可以通过Billboard技术创建。使用多层Billboard平面配合透明度混合,可以模拟出具有深度感的远景效果。这种方法比使用完整3D模型更加高效,特别适合移动平台或性能受限的场景。

UI和交互元素

在用户界面和交互设计中,Billboard技术确保重要信息始终可见。

世界空间UI元素使用Billboard技术创建始终面向玩家的对话框、任务提示或交互图标。这在3D游戏中特别有用,玩家可以从任何角度都能清晰看到UI内容。

AR和VR应用中的界面元素通过Billboard技术确保虚拟界面始终面向用户,提供自然的交互体验。无论是信息面板、控制菜单还是虚拟标签,Billboard都能保证最佳的可读性和可用性。

性能优化考虑

使用Billboard节点时需要考虑性能影响,特别是在大量使用的情况下。

渲染性能

Billboard技术通过减少几何复杂度来提升性能,但顶点着色器的计算负载会增加。在移动设备或低端硬件上,需要平衡视觉质量和性能消耗。

优化策略包括:

  • 控制Billboard物体的数量,避免在同一帧中渲染过多Billboard
  • 使用LOD系统,根据距离动态切换Billboard和完整模型
  • 合并多个Billboard物体,减少绘制调用
  • 在性能敏感的区域使用更简单的Billboard效果

内存和带宽

Billboard通常使用简单的四边形几何体,这有助于减少内存占用和顶点数据传输带宽。但在使用高质量纹理时,需要注意纹理内存的消耗。

优化建议:

  • 使用纹理图集将多个Billboard纹理合并为一张大图
  • 根据距离使用不同分辨率的纹理
  • 压缩纹理格式以减少内存占用
  • 合理管理纹理的加载和卸载,避免内存峰值

常见问题与解决方案

在使用Billboard节点时可能会遇到一些常见问题,以下是相应的解决方案。

光照异常

问题描述:Billboard物体上的光照显示不正确,高光或阴影位置异常。

解决方案:

  • 确保正确连接Normal OS端口,并提供准确的法线数据
  • 检查Billboard模式是否适合场景需求
  • 在复杂光照环境下,考虑使用自定义光照模型或简化光照计算
  • 验证法线贴图是否正确应用,确保切线数据正确变换

深度排序问题

问题描述:Billboard物体与其他物体的深度排序错误,出现穿透或遮挡异常。

解决方案:

  • 调整渲染队列顺序,确保Billboard物体在正确的渲染阶段绘制
  • 使用Alpha混合时,注意透明物体的渲染顺序问题
  • 在粒子系统中使用软粒子技术缓解深度冲突
  • 考虑使用自定义深度偏移解决特定的排序问题

运动模糊和抗锯齿

问题描述:快速移动的Billboard物体可能出现运动模糊异常或抗锯齿效果不佳。

解决方案:

  • 在运动剧烈的Billboard物体上禁用运动模糊,或使用自定义运动向量
  • 调整抗锯齿设置,确保Billboard边缘平滑
  • 对于特别敏感的视觉效果,考虑使用更高分辨率的纹理
  • 在后期处理中应用特定的抗锯齿技术,如TAA(时间性抗锯齿)

高级应用技巧

掌握了Billboard节点的基本用法后,可以探索一些高级应用技巧。

自定义Billboard效果

通过组合Billboard节点与其他ShaderGraph节点,可以创建独特的视觉效果。

倾斜Billboard效果通过修改旋转矩阵,使Billboard物体以特定角度倾斜,而不是完全面向相机。这种效果可以用于创建更有动态感的粒子特效或风格化的视觉元素。

动态朝向Billboard根据游戏逻辑或玩家输入动态调整Billboard的朝向,而不是始终面向主相机。这种技术可以用于创建始终面向特定目标的效果,如追踪导弹的尾焰或指向任务目标的导航标记。

与其他系统的集成

Billboard节点可以与Unity的其他系统集成,创建更复杂的效果。

与VFX Graph集成,在视觉特效图中使用Billboard技术创建高性能的粒子效果。VFX Graph提供了更强大的粒子系统功能,结合Billboard可以实现电影级的视觉效果。

与Shader Graph高级特性结合,如曲面细分、几何着色器或光线追踪,创建更复杂的Billboard效果。这些高级技术可以增强Billboard的视觉质量,提供更逼真或更风格化的外观。


【Unity Shader Graph 使用与特效实现】专栏-直达 (欢迎点赞留言探讨,更多人加入进来能更加完善这个探索的过程,🙏)

JS和PHP操作cookie对比

一、核心操作对比(基础用法)

先通过代码示例直观展示两者操作 Cookie 的核心方式:

1. JavaScript 操作 Cookie

(基于之前封装的工具函数,原生 JS 无内置 API,需手动拼接字符串)

运行

// 设置 Cookie:7天有效期,全站生效
setCookie('user_id', '1001', {
  expires: 60 * 60 * 24 * 7, // max-age 单位秒
  path: '/',
  secure: true // 仅HTTPS
});

// 获取 Cookie
const userId = getCookie('user_id'); // 输出:1001

// 删除 Cookie
removeCookie('user_id', { path: '/' });

2. PHP 操作 Cookie

(PHP 提供内置函数 setcookie()/$_COOKIE,更简洁)

运行

<?php
// 1. 设置 Cookie:7天有效期,全站生效
// 参数:name, value, expire(时间戳), path, domain, secure, httponly
setcookie(
  'user_id',
  '1001',
  time() + 7 * 24 * 60 * 60, // 过期时间戳(当前时间+7天)
  '/', // 全站生效
  '',  // 域名(默认当前域名)
  true, // 仅HTTPS传输
  true  // 禁止JS访问(防XSS)
);

// 2. 获取 Cookie(超全局变量 $_COOKIE)
$userId = $_COOKIE['user_id'] ?? null; // 输出:1001

// 3. 删除 Cookie(设置过期时间为过去)
setcookie('user_id', '', time() - 3600, '/');
?>

二、关键差异对比表

维度 JavaScript 操作 Cookie PHP 操作 Cookie
操作时机 运行在客户端浏览器,随时可读写(页面加载后) 运行在服务端,仅在HTTP 响应头中设置,读取依赖请求头
API 特性 无内置封装,需手动拼接 Cookie 字符串、处理编码 提供 setcookie() 内置函数,参数化配置更规范
HttpOnly 控制 无法设置!HttpOnly 仅能由服务端(PHP)设置 可直接通过 setcookie() 第 7 个参数设置
生效时机 设置后立即生效(可马上读取) 设置的 Cookie 需下一次请求才生效(当前脚本无法读取刚设置的 Cookie)
跨域 / 路径限制 仅能操作当前域名、当前路径下的 Cookie 可设置跨子域名(如 .example.com)、任意生效路径
编码处理 需手动用 encodeURIComponent/decodeURIComponent 建议手动编码(如 urlencode()),PHP 不会自动处理
错误处理 无内置错误提示,需自行判断 可通过 headers_sent() 检查是否已发送响应头(发送后无法设置 Cookie)

三、核心差异详解

1. 生效时机(最易踩坑点)

  • JS:设置 document.cookie = "name=value" 后,立刻能通过 document.cookie 读取到该值,因为操作的是浏览器本地存储。

  • PHPsetcookie() 是往 HTTP 响应头中添加 Set-Cookie 字段,当前脚本的 $_COOKIE 是从本次请求头中读取的,所以刚设置的 Cookie 要等客户端下次请求时才会被带入请求头,当前脚本无法读取。

    示例(PHP 坑点):

    运行

    <?php
    setcookie('test', '123', 0, '/');
    echo $_COOKIE['test'] ?? '未获取到'; // 输出:未获取到(当前请求无该Cookie)
    // 刷新页面后再次执行,才会输出:123
    ?>
    

2. HttpOnly 权限

HttpOnly 是 Cookie 的核心安全属性,作用是禁止 JS 访问该 Cookie,防止 XSS 攻击:

  • JS 完全无法设置 / 修改这个属性,只能由服务端(PHP)控制;
  • PHP 可通过 setcookie() 最后一个参数直接设置,这也是生产环境中登录 token 等关键 Cookie 必须设置的属性。

3. 操作环境限制

  • JS 只能在浏览器环境运行,无法操作非当前域名的 Cookie(浏览器同源策略限制);
  • PHP 运行在服务端,可根据业务需求设置任意生效路径 / 域名(如让 Cookie 在 a.example.comb.example.com 共享)。

四、最佳实践与使用场景

场景 推荐使用 原因
登录 token / 敏感数据 PHP 可设置 HttpOnly 防 XSS,服务端控制更安全
页面临时偏好(如主题) JavaScript 无需请求服务端,本地操作更高效
跨子域名共享数据 PHP 可设置 domain: '.example.com',JS 无法跨子域名操作
立即生效的本地存储 JavaScript 设置后立即读取,PHP 需等待下一次请求

总结

  1. 核心差异:JS 操作客户端本地 Cookie,即时生效但无 HttpOnly 权限;PHP 操作响应头 Cookie,需下次请求生效但可控制全部安全属性;
  2. 安全优先:敏感 Cookie(如 token)必须用 PHP 设置并开启 HttpOnly,避免 JS 窃取;
  3. 效率优先:页面级临时存储(如主题、语言)用 JS 操作,减少服务端请求。

系统日志分析:排查 Linux 系统异常重启原因

在Linux服务器运维工作中,系统突发关机或异常重启是高频高发的故障场景,不仅可能导致业务中断、数据丢失,还可能隐藏着安全风险。导致这类问题的原因复杂多样,常见诱因包括以下几类:

•供电故障
•软件/硬件错误
•内存故障
•未授权用户操作
系统重启与关机均属于核心系统事件,直接关联业务稳定性与数据安全,因此管理员必须将这类事件纳入日常监控重点。而系统日志(Syslog)作为Linux系统的“运行日记”,正是获取重启、关机事件详细信息的核心载体,定期监控分析Syslog是排查此类故障的关键前提。

从操作场景来看,局域网内的Linux用户可直接通过命令执行关机操作,其中Linux系统关机命令的基础语法为:shutdown [OPTIONS] [TIME] [MESSAGE]。掌握这一基础命令,能帮助管理员快速区分操作是人为执行还是系统异常触发。

若故障排查过程中怀疑是人为操作导致的系统关机,管理员可通过检查认证日志文件精准定位操作记录,包括操作人、操作时间等关键信息。需要注意的是,个别用户还可能通过远程登录的方式执行关机指令,这类操作往往更具隐蔽性,需重点排查。

重启事件的日志记录形式

Dec 24 21:03:41 ip-172-31-34-37 sudo: joker : TTY=pts/0 ; PWD=/home/joker ; USER=root ; COMMAND=/sbin/shutdown -r now

除了人为操作,系统硬件故障、软件崩溃等也会导致重启,这类信息可通过检索内核日志定位。但实际运维场景中,系统日志数据量大、类型繁杂,人工逐条筛选不仅耗时费力,还容易遗漏关键信息。

因此,借助专业的日志管理解决方案成为高效运维的必然选择——这类工具可自动完成日志数据的采集、解析,将杂乱的原始日志转换为直观的价值信息,并生成开箱即用的统计报表,大幅提升故障排查效率。

高效挖掘Linux重启事件价值:日志管理工具的核心作用

在众多日志管理工具中,EventLog Analyzer凭借全面的功能成为运维人员的常用选择。

作为运维人员常用的日志管理工具,EventLog Analyzer具备全网络日志整合监控能力。可跨平台覆盖Linux等各类系统,实现日志数据的集中采集、梳理与实时监控,打破分散日志管理壁垒,让管理员全面掌握全网日志动态,为运维决策提供完整数据支撑,提升运维效率。

image.png

针对系统关机、重启等关键事件,EventLog Analyzer可精准触发实时告警,支持短信、邮件等多渠道通知,助力管理员即时响应异常,避免故障扩大,保障系统稳定运行。EventLog Analyzer具备智能报表生成功能,自动汇总日志数据生成详尽报表,清晰呈现关键信息,助力管理员快速追溯Linux故障根源、定位责任,缩短排查周期。

依托全面日志管理分析能力,EventLog Analyzer实时监控异常日志、识别风险,提供合规报表,构建全方位Linux运维安全体系,规避安全风险,保障业务系统稳定。

nuxt配置之head动态配置

讲一下nuxt中的配置,那么首先的配置文件是nuxt.config.js,那么也就是说在我们的项目的根目录的一个nuxt.config.js文件。然后大家需要明确的一点就是这个文件的所有的配置,是全局配置。

image.png

head来说,那我们知道nuxt它是可以解决seo的部分问题的。比如说它可以解决每个网页都有他自己所独有的title描述以及关键字。那如果说我这里有很多页面,到那时呢我都没有进行配置,那这个时候就会走全局的配置。

当然,如果某个页面配置了,就会走独有的页面的head。

pages里边新建一个页面。比如list.vue

<template>
  <div>列表页</div>
</template>

然后一个about页面

<template>
  <div>关于页</div>
</template>

所有页面的title全是页面的项目名。因为这些页面都没有独有的head配置。所以如果想要做针对单独页面的配置。那应该怎么去配置呢?

可以注意一下写法,参考文档这个api有一个head的单篇介绍。只不过文档写得不是特别的完整。

在全局是head的一个对象,然后在单独页面就是一个head的一个函数。

image.png

image.png

动态title

做动态title标题。包括描述或者关键词可能都是动态的。

然后呢现在我们做的是点击每一个进入到这个文章的详情页。

/pages/news/index.vue

<template>
  <div>
    <h1>新闻列表</h1>
    <ul>
      <li 
        v-for='item in newList' 
        :key='item.id'
        @click='goDetail(item.id)'
      >
        {{item.title}}
      </li>
    </ul>
  </div>
</template>

<script type="text/javascript">
export default {
  data() {
    return {
      newList: [
        { id: 1, title: '111' },
        { id: 2, title: '222' },
        { id: 3, title: '333' },
        { id: 4, title: '444' },
        { id: 5, title: '555' },
      ]
    }
  },
  methods: {
    goDetail(id) {
      this.$router.push({
        path: `/news/${id}`
      })
    }
  }
}
</script>
// _id.vue
<template>
  <div>
    {{ id }} 新闻的详情页
  </div>
</template>

<script type="text/javascript">
export default{
  head() {
    return {
      title: this.id,
      meta: [
        { hid: 'description', name: 'description', content: '此处是网站描述' },
        { hid: '', name: 'keywords', content: '此处是网站关键词' }
      ]
    }
  },
  data () {
    return {
      id: 0
    }
  },
  created() {
    this.id = this.$route.params.id
  }
}
</script>

总结

head 可以全局配置,也可以局部配置,也可以动态配置。

❌