阅读视图

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

基于微信小程序实现幸运大转盘页面

基于微信小程序实现幸运大转盘页面,以下是搭建教程:

1. 创建项目结构

首先,确保你已经安装了微信开发者工具,然后创建一个新的微信小程序项目。在项目目录下,会有 pages 文件夹,我们在其中创建一个新的页面文件夹,例如 lucky-wheel,并在该文件夹下创建四个文件:lucky-wheel.js、lucky-wheel.json、lucky-wheel.wxml 和 lucky-wheel.wxss。

2. 编写 lucky-wheel.json 文件

这个文件用于配置页面的相关信息,以下是示例代码:

{
  "navigationBarTitleText": "幸运大转盘"
}

3. 编写 lucky-wheel.wxml 文件

该文件用于构建页面的结构,包含一个大转盘和一个开始按钮,示例代码如下:

<view class="container">
  <view class="wheel" style="transform: rotate({{rotateDeg}}deg); transition: transform 3s ease-out;">
    <!-- 这里可以使用图片或者自定义样式绘制转盘的每一个扇形 -->
    <view class="sector" style="transform: rotate(0deg);">奖品1</view>
    <view class="sector" style="transform: rotate(60deg);">奖品2</view>
    <view class="sector" style="transform: rotate(120deg);">奖品3</view>
    <view class="sector" style="transform: rotate(180deg);">奖品4</view>
    <view class="sector" style="transform: rotate(240deg);">奖品5</view>
    <view class="sector" style="transform: rotate(300deg);">奖品6</view>
  </view>
  <button bindtap="startRotate">开始抽奖</button>
</view>

4. 编写 lucky-wheel.wxss 文件

此文件用于设置页面的样式,包括大转盘和扇形的样式,示例代码如下:

.container {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100vh;
}

.wheel {
  position: relative;
  width: 300px;
  height: 300px;
  border-radius: 50%;
  border: 1px solid #ccc;
  margin-bottom: 20px;
}

.sector {
  position: absolute;
  width: 0;
  height: 0;
  border-left: 50px solid transparent;
  border-right: 50px solid transparent;
  border-bottom: 150px solid #f0f0f0;
  transform-origin: 50px 150px;
  text-align: center;
  line-height: 200px;
}

5. 编写 lucky-wheel.js 文件

该文件用于实现页面的逻辑,包括开始抽奖和控制转盘旋转的功能,示例代码如下:

Page({
  data: {
    rotateDeg: 0,
    prizeList: [0, 60, 120, 180, 240, 300], // 每个奖品对应的角度
    isRotating: false
  },

  startRotate() {
    if (this.data.isRotating) return;
    this.setData({ isRotating: true });

    // 随机选择一个奖品
    const randomIndex = Math.floor(Math.random() * this.data.prizeList.length);
    const targetDeg = this.data.prizeList[randomIndex] + 360 * 5; // 旋转5圈后停在目标奖品处

    this.setData({
      rotateDeg: targetDeg
    });

    setTimeout(() => {
      wx.showToast({
        title: `恭喜你获得奖品${randomIndex + 1}`,
        icon: 'none'
      });
      this.setData({ isRotating: false });
    }, 3000); // 旋转动画时间为3秒
  }
});

运行项目

将上述代码保存后,在微信开发者工具中运行项目,即可看到幸运大转盘页面。点击“开始抽奖”按钮,大转盘会开始旋转,旋转停止后会弹出提示框显示中奖信息。添加更多奖品、美化界面等。

鸿蒙ArkUI框架中的状态管理

在ArkUI框架中,状态管理是构建动态应用的核心。以下是组件级别应用级别状态管理装饰器的分类、用途及区别的总结,结合了思考过程中的关键点:

一、组件级别状态管理

1. @State

  • 用途:组件内部私有状态,变化触发UI更新。
  • 示例:按钮的点击状态、计数器数值。
  • 特点:只能初始化一次,单向数据流(组件内修改)。

代码示例

@Component
struct CounterButton {
  @State count: number = 0; // 组件内部状态

  build() {
    Button(`点击次数:${this.count}`)
      .onClick(() => {
        this.count++; // 修改@State变量自动更新UI
      })
  }
}

2. @Prop

  • 用途:父组件向子组件传递数据(单向)。
  • 特点:需通过父组件回调更新数据。
  • 示例:显示父组件传递的文本,子组件不可直接修改。

代码示例

// 父组件
@Component
struct ParentComponent {
  @State parentCount: number = 0;

  build() {
    Column() {
      ChildComponent({ countProp: this.parentCount }) // 传递数据
      Button("父组件增加").onClick(() => this.parentCount++)
    }
  }
}

// 子组件
@Component
struct ChildComponent {
  @Prop countProp: number; // 单向接收父组件数据

  build() {
    Text(`来自父组件的值:${this.countProp}`)
  }
}

3. @Link

  • 用途:父子组件双向数据绑定。
  • 特点:双向同步,类似Vue的v-model
  • 示例:共享开关状态,子组件直接修改影响父组件。

代码示例

// 父组件
@Component
struct ParentComponent {
  @State sharedCount: number = 0;

  build() {
    Column() {
      ChildComponent({ countLink: $sharedCount }) // 双向绑定
      Text(`父组件值:${this.sharedCount}`)
    }
  }
}

// 子组件
@Component
struct ChildComponent {
  @Link countLink: number; // 双向绑定变量

  build() {
    Button("子组件修改").onClick(() => {
      this.countLink++; // 修改会同步到父组件
    })
  }
}

4. @Provide / @Consume

  • 用途:跨层级组件数据共享(祖先→后代)。
  • 示例:主题颜色全局设置。
  • 特点:避免逐层传递,类似React Context。

代码示例

// 祖先组件
@Component
struct AncestorComponent {
  @Provide themeColor: string = 'blue'; // 提供数据

  build() {
    Column() {
      ChildComponent()
    }
  }
}

// 后代组件
@Component
struct ChildComponent {
  @Consume themeColor: string; // 消费数据

  build() {
    Text(`当前主题色:${this.themeColor}`)
      .fontColor(this.themeColor)
  }
}

5. @Observed / @ObjectLink

  • 用途:观察嵌套对象属性变化。
  • 特点@Observed装饰类,@ObjectLink引用实例。
  • 示例:用户对象({name: string})属性更新。

代码示例

@Observed // 装饰类
class User {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

@Component
struct UserProfile {
  @ObjectLink user: User; // 引用被观察对象

  build() {
    Column() {
      Text(`姓名:${this.user.name}`)
      Button("修改年龄").onClick(() => {
        this.user.age++; // 修改会触发UI更新
      })
    }
  }
}

6. @Style

  • 用途:定义可复用的组件样式。
  • 示例:统一按钮样式(颜色、边距)。

代码示例

@Styles function customButtonStyle() {
  .width(120)
  .height(40)
  .backgroundColor(Color.Blue)
  .fontColor(Color.White)
}

@Component
struct StyledButton {
  build() {
    Button("样式按钮")
      .useStyle(customButtonStyle) // 应用样式
  }
}

7. @Builder / @BuilderParam

  • 用途:构建可复用的UI片段或动态插入布局。
  • 示例:自定义卡片布局,父组件传递头部Builder。
  • 区别@Builder定义结构,@BuilderParam接收结构作为参数。

代码示例

@Component
struct CustomCard {
  @BuilderParam header: () => void; // 接收Builder参数

  build() {
    Column() {
      this.header() // 插入自定义头部
      Text("卡片内容...")
    }
  }
}

// 使用组件时传递Builder
CustomCard({
  header: () => {
    Text("自定义标题")
      .fontSize(20)
      .fontColor(Color.Red)
  }
})

8. @Extend

  • 用途:扩展组件样式(如全局字体)。
  • 示例:统一所有文本的字体大小和颜色。

代码示例

@Extend(Text) function boldText() {
  .fontWeight(FontWeight.Bold)
  .fontColor('#333')
}

@Component
struct ExtendedText {
  build() {
    Column() {
      Text("普通文本")
      Text("加粗文本").useStyle(boldText) // 应用扩展样式
    }
  }
}

二、应用级别状态管理

1. @LocalStorage

  • 用途:页面级临时存储(页面关闭后可能保留)。
  • 示例:表单草稿保存。

补充知识点: @LocalStorageProp / @LocalStorageLink

区别:前者单向同步,后者双向绑定页面存储数据。

代码示例

// 页面A
@Entry
@Component
struct PageA {
  @LocalStorage('formData') formData: string = '';

  build() {
    TextInput(this.formData)
      .onChange((value) => {
        this.formData = value; // 数据保存到LocalStorage
      })
  }
}

// 页面B可读取同一LocalStorage
@Component
struct PageB {
  @LocalStorage('formData') formData: string;

  build() {
    Text(`保存的数据:${this.formData}`)
  }
}

2. @AppStorage

  • 用途:全局状态存储(应用生命周期内有效)。
  • 示例:用户登录Token。

补充知识点: @StorageProp / @StorageLink

用途:绑定到AppStorage中的具体键。

区别@StorageProp单向,@StorageLink双向。

代码示例

// 全局存储用户Token
@AppStorage.setOrCreate('userToken', '') // 初始化

@Component
struct LoginComponent {
  @StorageLink('userToken') token: string; // 双向绑定

  build() {
    Button("登录").onClick(() => {
      this.token = 'abc123'; // 修改全局状态
    })
  }
}

// 其他页面读取
@Component
struct ProfilePage {
  @StorageProp('userToken') token: string; // 单向读取

  build() {
    Text(`Token: ${this.token}`)
  }
}

3. @PersistentStorage

  • 用途:持久化存储(应用重启保留)。
  • 示例:用户偏好设置(语言、主题)。

代码示例

@PersistentStorage.setOrCreate('settings', { theme: 'light', fontSize: 16 })

@Component
struct SettingsPage {
  @StorageLink('theme') theme: string;
  @StorageLink('fontSize') fontSize: number;

  build() {
    Column() {
      Button("切换主题").onClick(() => {
        this.theme = this.theme === 'light' ? 'dark' : 'light';
      })
      Slider({ min: 12, max: 24 })
        .value(this.fontSize)
        .onChange((value) => {
          this.fontSize = value;
        })
    }
  }
}

4. @Environment

  • 用途:访问环境变量(如主题、语言)。
  • 示例:根据系统主题切换应用外观。

代码示例

@Component
struct ThemeAwareComponent {
  @Environment('currentTheme') theme: string;

  build() {
    Text("主题敏感文本")
      .fontColor(this.theme === 'dark' ? Color.White : Color.Black)
      .backgroundColor(this.theme === 'dark' ? Color.Black : Color.White)
  }
}

三、关键区别与选择

  • 作用域

    • 组件级:@State@Prop等用于组件或父子通信。
    • 应用级:@AppStorage@PersistentStorage等跨页面共享。
  • 数据流

    • 单向:@Prop@LocalStorageProp(父→子/存储→组件)。
    • 双向:@Link@LocalStorageLink(父子/组件与存储同步)。
  • 持久性

    • 临时:@State@LocalStorage(页面级)。
    • 持久:@PersistentStorage(设备存储)。
  • 使用场景

    • 简单组件状态 → @State
    • 跨组件共享 → @Provide/@Consume
    • 全局配置 → @AppStorage + @StorageLink
    • 复杂对象监听 → @Observed + @ObjectLink


四、最佳实践总结

  1. 简单交互优先使用@State:适合按钮状态、临时计数等
  2. 父子通信选择@Prop/@Link:单向传递用@Prop,双向同步用@Link
  3. 跨层级共享数据用@Provide/@Consume:避免多级传递的麻烦
  4. 全局状态使用@AppStorage:如用户登录状态、主题配置
  5. 复杂对象监听用@Observed+@ObjectLink:确保嵌套属性变化触发更新
  6. 持久化数据用@PersistentStorage:用户设置、历史记录等需要长期保存的数据

官方文档是更全面的参考:
HarmonyOS ArkUI 文档

面试官问我React组件和state的关系,我指了指路口的红绿灯…

🚥 马路边的面试奇遇

面试官:(突然指向路口的红绿灯)你看这个红灯变绿,像不像React组件的重新渲染?

:(战术挑眉)您这红绿灯要是用React实现,isGreen这个state一变,整个组件就得重绘。不过嘛...(突然掏出手电筒)要是只换灯泡不换灯罩,可能不用整个拆了重建?

面试官:(突然打开手电筒照我眼睛)说人话!


🔋 一、React组件的能量守恒定律

1. 基本法则:触发渲染的三原色

// 组件重渲染的三种触发方式
const 触发重渲染 = () => {
  1. setState(newValue) // 原生state变化
  2. props变更 // 父组件传值变化
  3. 父组件重渲染 // 上级组件更新
};

2. 红绿灯案例解析

function TrafficLight() {
  const [isGreen, setIsGreen] = useState(false);

  useEffect(() => {
    const timer = setInterval(() => {
      setIsGreen(prev => !prev); // ✅ 每次切换触发重渲染
    }, 3000);
    return () => clearInterval(timer);
  }, []);

  return (
    <div className={`light ${isGreen ? 'green' : 'red'}`}>
      {/* 每次isGreen变化都会重绘整个div */}
    </div>
  );
}

🚨 二、触发重渲染的四大天王

Case 1:useState值变化

const [count, setCount] = useState(0);

// 🚀 触发重渲染
setCount(1); 

// ❌ 不会触发(React使用Object.is比较)
setCount(0); 

Case 2:props对象引用变化

// 父组件
<Child items={[...items]} /> // 每次都是新数组,必触发

<Child items={items} /> // 数组引用不变不触发

Case 3:Context变更

const ThemeContext = createContext();

// 只要Provider的value变化
<ThemeContext.Provider value={newTheme}>
  <App />
</ThemeContext.Provider>

即使组件用memo包裹,只要消费了该Context的子组件都会重渲染

Case 4:祖传染色体攻击

// 祖父组件
const Grandfather = () => {
  const [state] = useState();
  return <Father />; // 👉 只要祖父重渲染,父亲不优化的话...
};

// 父亲组件
const Father = () => <Child />; // 👉 孩子也会被迫重渲染

🕶️ 三、金钟罩铁布衫:不触发重渲染的玄学时刻

1. 对象原地变性术

const [user, setUser] = useState({ name: '老王' });

// ❌ 不会触发
user.name = '隔壁老王'; 
setUser(user); // 引用地址没变!

// ✅ 正确做法
setUser({ ...user, name: '隔壁老王' });

2. 数组索引戏法

const [list, setList] = useState(['A', 'B', 'C']);

// ❌ 不会触发(React认为数组没变)
list.push('D');
setList(list);

// ✅ 正确做法
setList([...list, 'D']);

3. 函数式更新隐身术

const [count, setCount] = useState(0);

// ✅ 触发
setCount(1); 

// ❌ 不会触发(相同值)
setCount(prev => prev); 

👻 四、幽灵state现形记

实验:未使用的state会触发渲染吗?

function GhostComponent() {
  const [usedState] = useState('显形state'); 
  const [ghostState, setGhostState] = useState('幽灵state'); 

  return (
    <div>
      <button onClick={() => setGhostState(Math.random())}>
        触发幽灵state变化
      </button>
      <p>{usedState}</p> {/* 只展示usedState */}
    </div>
  );
}

现象
点击按钮时:

  • ✅ 组件重新渲染(控制台打印执行)
  • ❌ UI纹丝不动(ghostState从未被使用)

结论
所有state变化都会触发重渲染,哪怕它是个"幽灵"!


驱魔三式:让幽灵state安息

招式1:组件分家术

// 父组件(无state)
function Parent() {
  return <Child />; // 免疫幽灵攻击
}

// 子组件(独自承受)
const Child = React.memo(() => {
  const [ghostState, setGhostState] = useState();
  // ...处理state
});

招式2:useRef封印大法

function StealthComponent() {
  const ghostRef = useRef();
  
  const updateGhost = () => {
    ghostRef.current = Math.random(); // ✅ 无渲染触发
  };

  return <button onClick={updateGhost}>秘密行动</button>;
}

招式3:Context选择器狙击

const useGhostSelector = () => {
  const context = useContext(GhostContext);
  return useSelector(context, state => state.usedPart); // 精确打击
};

🧙 性能优化法典(新增条款)

闹鬼场景 驱魔方案 效果
未使用的UI state游荡 组件拆分 隔离在子组件内
纯逻辑state(如计时器ID) useRef镇压 完全隐形
全局state的幽灵扩散 Context选择器 精准狙杀
高频无用state波动 移出React生态 彻底驱散

🔍 五、组件渲染的量子纠缠实验

实验1:Memo的薛定谔防护

const ExpensiveComponent = memo(({ data }) => {
  // 只有data变化时才重渲染
});

// 父组件
const Parent = () => {
  const [state] = useState();
  return <ExpensiveComponent data={state} />; 
  // 👆父组件重渲染时,子组件不会跟着渲染
};

实验2:useMemo的时间结界

const heavyData = useMemo(() => {
  return computeHeavyData(); // 依赖项不变时缓存结果
}, [deps]);

return <Chart data={heavyData} />; 

实验3:useCallback的克隆人军团

const onClick = useCallback(() => {
  // 依赖项不变时保持函数引用
}, [deps]);

return <Button onClick={onClick} />;

💣 六、高频作死案例现场

作死案例1:在渲染中创建新对象

// 每次渲染都创建新style对象
<div style={{ color: 'red' }}> 
  <ChildComponent /> // 即使Child是memo也会重渲染
</div>

解法:将style提升到组件外或使用useMemo

作死案例2:匿名函数轰炸机

// 每次渲染都生成新函数
<Button onClick={() => handleClick()} />

// 正确做法
const handleClick = useCallback(() => {...}, []);
<Button onClick={handleClick} />

作死案例3:无脑Context

// 把整个state对象放入Context
<AppContext.Provider value={{ state, setState }}>
  {/* 任何state变化都会触发所有消费者重渲染 */}
</AppContext.Provider>

// 正确做法:拆分Context
<UserContext.Provider value={user}>
<CartContext.Provider value={cart}>

🚀 七、性能优化九阳神功

第一式:组件记忆术

// 用memo包裹组件
const UserCard = memo(({ user }) => {
  return <div>{user.name}</div>;
});

第二式:道具稳定符

// 用useMemo稳定props
const userData = useMemo(() => transformData(rawData), [rawData]);
return <Profile data={userData} />;

第三式:时间切片大法

// 用startTransition标记非紧急更新
const [tab, setTab] = useState('home');

function switchTab(nextTab) {
  startTransition(() => {
    setTab(nextTab); // 低优先级更新
  });
}

🎙️ 面试官の终极大招

面试官:(突然掏出三个iPhone)如果这三个手机同时运行React应用,分别出现:

  1. 疯狂重渲染
  2. 状态不同步
  3. 性能雪崩 要怎么快速定位问题?

:(战术擦汗)可能需要:

  1. 用React DevTools的Profiler抓重渲染元凶
  2. 检查state是否被意外篡改
  3. 上memo、useMemo、虚拟列表三连

不过...(突然抢过手机)您这三个都是模型机啊!


后记:后来发现,真正的"幽灵state"其实是产品经理半夜改需求时偷偷加的那些...(逃)

AntV X6 常用方法

AntV X6 常用方法

1. Graph 相关方法

1.1 创建 Graph

使用场景:初始化画布,设置基本参数。

import { Graph } from '@antv/x6';
const graph = new Graph({
  container: document.getElementById('container'),
  width: 800,
  height: 600,
  grid: true,
});

1.2 添加节点

使用场景:在画布上添加新的图形节点。

const node = graph.addNode({
  x: 40,
  y: 40,
  width: 100,
  height: 40,
  label: 'Hello',
});

1.3 添加边

使用场景:创建节点之间的连接关系。

const edge = graph.addEdge({
  source: node1,
  target: node2,
  label: 'Edge',
});

1.4 获取所有节点

使用场景:用于遍历所有节点,例如批量更新节点样式。

const nodes = graph.getNodes();

1.5 获取所有边

使用场景:获取当前所有连接关系,例如调整样式或删除特定边。

const edges = graph.getEdges();

1.6 删除元素

使用场景:当用户进行编辑操作时,删除选定的节点或边。

graph.removeNode(node);
graph.removeEdge(edge);

1.7 清空画布

使用场景:用于重置整个画布。

graph.clear();

2. Node 相关方法

2.1 设置/获取节点数据

使用场景:存储或获取节点的业务数据,例如状态信息。

node.setData({ key: 'value' });
const data = node.getData();
console.log(data.key);

2.2 更新节点数据

使用场景:在不覆盖原有数据的情况下,增量更新节点数据。

node.setData({ key: 'newValue' }, { overwrite: false });

2.3 设置/获取节点位置

使用场景:调整节点在画布中的位置。

node.position(100, 100);
const { x, y } = node.getPosition();

2.4 设置/获取节点大小

使用场景:改变节点尺寸,例如放大或缩小。

node.resize(120, 50);
const { width, height } = node.getSize();

2.5 设置/获取节点旋转角度

使用场景:适用于旋转特定类型的节点,如箭头或图标。

node.rotate(45);
const angle = node.getAngle();

2.6 设置/获取节点文本

使用场景:修改节点的显示文本。

node.setLabel('New Label');
const label = node.getLabel();

2.7 设置节点样式

使用场景:修改节点外观,例如颜色、字体大小。

node.attr('body/fill', 'blue');
node.attr('label/fontSize', 14);

3. Edge 相关方法

3.1 设置/获取边的文本

使用场景:给边添加描述信息。

edge.setLabel('New Edge Label');
const label = edge.getLabel();

3.2 设置/获取边的样式

使用场景:修改边的外观,例如颜色、宽度。

edge.attr('line/stroke', 'red');

3.3 设置/获取边的连接点

使用场景:动态调整边的起点和终点。

edge.setSource(node1);
edge.setTarget(node2);
const source = edge.getSource();
const target = edge.getTarget();

4. 事件监听

4.1 监听节点添加事件

使用场景:当用户添加新节点时触发特定操作。

graph.on('node:added', ({ node }) => {
  console.log('Node added:', node);
});

4.2 监听节点点击事件

使用场景:用户点击节点时,显示详细信息或执行操作。

graph.on('node:click', ({ node }) => {
  console.log('Node clicked:', node.id);
});

4.3 监听边点击事件

使用场景:点击边时高亮、删除或修改连接信息。

graph.on('edge:click', ({ edge }) => {
  console.log('Edge clicked:', edge.id);
});

4.4 监听画布点击事件

使用场景:用户点击空白区域时,取消选中所有元素。

graph.on('blank:click', () => {
  console.log('Canvas clicked');
});

5. 画布操作

5.1 放大/缩小

使用场景:调整画布的缩放级别。

graph.zoom(1.2); // 放大 1.2 倍
graph.zoom(0.8); // 缩小 0.8 倍

5.2 适应画布

使用场景:自动缩放图形以适应视图。

graph.zoomToFit();

5.3 平移画布

使用场景:调整视图中心点。

graph.translate(100, 50);

6. 自定义节点与边

6.1 自定义节点

使用场景:创建特定类型的节点,如流程图或组织结构图。

import { Shape } from '@antv/x6';

graph.addNode(
  new Shape.Rect({
    width: 100,
    height: 40,
    attrs: {
      body: { fill: 'blue' },
      label: { text: 'Custom Node', fill: 'white' },
    },
  })
);

6.2 自定义边

使用场景:创建具有特定样式的边,例如虚线或曲线连接。

import { Shape } from '@antv/x6';

graph.addEdge(
  new Shape.Edge({
    source: node1,
    target: node2,
    attrs: {
      line: { stroke: 'blue', strokeWidth: 2 },
    },
  })
);

以上是 AntV X6 的常用方法,包括对 GraphNodeEdge 以及 Data 的操作,并添加了使用场景,希望对你有所帮助!

轿车3D展示

本文将会以three.js 官网的一个轿车3D展示demo为例,进行讲解。示例具体查看地址:www.yanhuangxueyuan.com/threejs/exa…

车.gif

一、主要开发流程

  1. 搭建3D渲染场景
  2. 使用 GridHelper 对象,生成网格地板
  3. 使用 GLTFLoader 加载轿车模型,并自定义模型材质,可通过颜色选择器操控材质样式
  4. 将四个车轮模型保存在wheels对象中,通过改变车轮模型的 rotation.x 属性,让车轮旋转起来,模拟汽车奔跑。

二、查看3D模型

可以使用3D软件或者在线工具,预览轿车模型。这里推荐一个在线地址,用于浏览模型: gltf.nsdt.cloud/

image.png

三、绘制网格地板

GridHelper 是 Three.js 里的一个实用工具,用于创建网格辅助线,能在场景中直观地显示网格,辅助你理解和定位物体的位置。
该demo中使用 GridHelper 来模拟地板。

grid = new THREE.GridHelper( 20, 40, 0xffffff, 0xffffff );
grid.material.opacity = 0.2;
grid.material.depthWrite = false;
grid.material.transparent = true;
scene.add( grid );

代码解读:

1. 实例化GridHelper对象

grid = new THREE.GridHelper( 20, 40, 0xffffff, 0xffffff );

参数说明:

  • 第一个参数 20:表示网格的大小(边长),这里意味着创建的网格是一个边长为 20 个单位的正方形区域。
  • 第二个参数 40:表示网格的分割数量,即把整个网格区域在每个方向上平均分割成 40 份,这样会形成更密集的网格线。
  • 第三个参数 0xffffff:指定网格中轴线(穿过网格中心的线)的颜色,0xffffff 代表白色。
  • 第四个参数 0xffffff:指定网格线的颜色,同样是白色。

2. 设置材质透明度

grid.material.opacity = 0.2;

opacity 属性用于设置材质的透明度,取值范围是 0 到 1,其中 0 表示完全透明,1 表示完全不透明。这里将透明度设置为 0.2,意味着网格线会呈现出半透明的效果。

3. 禁用深度写入

grid.material.depthWrite = false;
  • depthWrite 是材质的一个属性,用于控制是否将该材质所渲染的物体的深度信息写入深度缓冲区。
  • 当设置为 false 时,意味着该材质渲染的物体不会影响深度缓冲区,这样可能会使得该物体在渲染时不会被其他物体遮挡,即使从深度上看它应该被遮挡。

4. 启用材质透明效果

grid.material.transparent = true;

transparent 属性用于启用材质的透明效果。当设置为 true 时,材质会根据 opacity 属性的值来呈现透明效果。

四、加载轿车模型,并自定义材质(核心)

下面将会介绍如何使用 Three.js 加载一个 GLTF 格式的汽车模型,并为模型的不同部分(车身、细节、玻璃等)设置不同的材质。同时,允许用户通过改变颜色值来动态改变这些部分的颜色。此外,还为汽车模型添加了底部阴影效果。

1. 定义材质

// 车身材质
const bodyMaterial = new THREE.MeshPhysicalMaterial( {
    color: 0xff0000, // 默认颜色
    metalness: 1.0, // 车外壳金属都
    roughness: 0.5, // 车外壳粗糙度
    clearcoat: 1.0, // 清漆层强度为 1.0,模拟清漆效果
    clearcoatRoughness: 0.03 //清漆层的粗糙度为 0.03
});

// 细节部分(如轮毂、装饰条等)的材质
const detailsMaterial = new THREE.MeshStandardMaterial( {
    color: 0xffffff, 
    metalness: 1.0, 
    roughness: 0.5
});

// 玻璃材质
const glassMaterial = new THREE.MeshPhysicalMaterial( {
    color: 0xffffff, 
    metalness: 0.25, 
    roughness: 0, 
    transmission: 1.0
});
(1) MeshPhysicalMaterial
  • MeshPhysicalMaterial 是具有有金属度metalness、粗糙度roughness属性的PBR材质。
  • MeshPhysicalMaterial是基于物理的材质,能够模拟真实世界中的光照和材质交互效果。对于车身材质,使用这种材质可以让车身在不同光照条件下表现出更加逼真的反射、折射、阴影等效果,使车身看起来更有质感和真实感。
(2) MeshStandardMaterial

MeshStandardMaterial也是一种常用的材质,它在计算光照时采用了标准的 PBR(基于物理的渲染)模型,能够提供较为真实的光照效果,同时性能相对较好。对于汽车的细节部分,如轮辋(rim)和装饰条(trim)等,使用MeshStandardMaterial可以在保证视觉效果的同时,减少计算量,提高渲染性能。

2. 模型加载

// 车底部阴影图
const shadow = new THREE.TextureLoader().load( 'models/gltf/ferrari_ao.png' );

// 车3D模型
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath( 'jsm/libs/draco/gltf/' );

const loader = new GLTFLoader();
loader.setDRACOLoader( dracoLoader );

loader.load( 'models/gltf/ferrari.glb', function ( gltf ) {
const carModel = gltf.scene.children[ 0 ];
})
  • 汽车底部阴影纹理图: 使用 THREE.TextureLoader 加载 ferrari_ao.png 图片
  • Draco 解码器设置:创建 DRACOLoader 对象并设置解码器路径,用于处理压缩的 GLTF 模型。
  • GLTF 模型加载:创建 GLTFLoader 对象并设置 Draco 解码器,然后使用 load 方法加载 ferrari.glb 模型。

3. 替换汽车材质,收集车轮

carModel.getObjectByName( 'body' ).material = bodyMaterial;

carModel.getObjectByName( 'rim_fl' ).material = detailsMaterial;
carModel.getObjectByName( 'rim_fr' ).material = detailsMaterial;
carModel.getObjectByName( 'rim_rr' ).material = detailsMaterial;
carModel.getObjectByName( 'rim_rl' ).material = detailsMaterial;
carModel.getObjectByName( 'trim' ).material = detailsMaterial;

carModel.getObjectByName( 'glass' ).material = glassMaterial;

wheels.push(
carModel.getObjectByName( 'wheel_fl' ),
carModel.getObjectByName( 'wheel_fr' ),
carModel.getObjectByName( 'wheel_rl' ),
carModel.getObjectByName( 'wheel_rr' )
);

4. 汽车底部阴影

ferrari_ao.png

const mesh = new THREE.Mesh(
    new THREE.PlaneGeometry( 0.655 * 4, 1.3 * 4 ),
    new THREE.MeshBasicMaterial( {
        map: shadow, 
        blending: THREE.MultiplyBlending, 
        toneMapped: false, 
        transparent: true 
    } )
);
mesh.rotation.x = - Math.PI / 2;
mesh.renderOrder = 2;
carModel.add( mesh );

scene.add( carModel );
  • 定义一个网格对象,并将之前加载好的阴影纹理应用到该材质上。
  • 对mesh 沿x轴旋转90度,使其平行于地面

5. 使车轮和地面动起来

function render() {
controls.update();
const time = - performance.now() / 1000;
for ( let i = 0; i < wheels.length; i ++ ) {
wheels[ i ].rotation.x = time * Math.PI * 2;
}
grid.position.z = - ( time ) % 1;
renderer.render( scene, camera );
stats.update();
}
  • 旋转车轮: for循环遍历四个车轮对象,wheels[i].rotation.x 表示第 i 个车轮绕 X 轴的旋转角度
  • 移动网格辅助线:( time ) % 1 计算出 time 的小数部分,取负号后将其赋值给 grid.position.z,使得网格辅助线在 Z 轴上以 1 个单位为周期循环移动,从而产生网格滚动的动画效果

6. 动态更改车模型材质颜色

const bodyColorInput = document.getElementById( 'body-color' );
bodyColorInput.addEventListener( 'input', function () {
bodyMaterial.color.set( this.value );
});

const detailsColorInput = document.getElementById( 'details-color' );
detailsColorInput.addEventListener( 'input', function () {
detailsMaterial.color.set( this.value );
});

const glassColorInput = document.getElementById( 'glass-color' );
glassColorInput.addEventListener( 'input', function () {
glassMaterial.color.set( this.value );
});

通过 color.set方法,修改材质颜色

四、完整代码

<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js webgl - materials - car</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<link type="text/css" rel="stylesheet" href="main.css">
<style>
body {
color: #bbbbbb;
background: #333333;
}
a {
color: #08f;
}
.colorPicker {
display: inline-block;
margin: 0 10px
}
</style>
</head>

<body>
<div id="info">
<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> car materials<br/>
Ferrari 458 Italia model by <a href="https://sketchfab.com/models/57bf6cc56931426e87494f554df1dab6" target="_blank" rel="noopener">vicent091036</a>
<br><br>
<span class="colorPicker"><input id="body-color" type="color" value="#ff0000"></input><br/>Body</span>
<span class="colorPicker"><input id="details-color" type="color" value="#ffffff"></input><br/>Details</span>
<span class="colorPicker"><input id="glass-color" type="color" value="#ffffff"></input><br/>Glass</span>
</div>

<div id="container"></div>

<script type="importmap">
{
"imports": {
"three": "../build/three.module.js",
"three/addons/": "./jsm/"
}
}
</script>

<script type="module">

import * as THREE from 'three';

import Stats from 'three/addons/libs/stats.module.js';

import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';

let camera, scene, renderer;
let stats;

let grid;
let controls;

const wheels = [];

function init() {

const container = document.getElementById( 'container' );

renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
// setAnimationLoop: 每个可用帧都会调用的函数。 如果传入“null",所有正在进行的动画都会停止。
renderer.setAnimationLoop( render );
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 0.85; // 色调映射的曝光级别。默认是1
container.appendChild( renderer.domElement );

window.addEventListener( 'resize', onWindowResize );

stats = new Stats();
container.appendChild( stats.dom );

//

camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 0.1, 100 );
camera.position.set( 4.25, 1.4, - 4.5 );

// OrbitControls: 轨道控制器
controls = new OrbitControls( camera, container );
controls.maxDistance = 9; // 能够将相机向外移动多少, 其默认值为Infinity
controls.maxPolarAngle = THREE.MathUtils.degToRad( 90 ); // 你能够垂直旋转的角度的上限,范围是0到Math.PI,其默认值为Math.PI。
controls.target.set( 0, 0.5, 0 );
controls.update();

scene = new THREE.Scene();
scene.background = new THREE.Color( 0x333333 );
// environment: 若该值不为null,则该纹理贴图将会被设为场景中所有物理材质的环境贴图。 然而,该属性不能够覆盖已存在的、已分配给 MeshStandardMaterial.envMap 的贴图。默认为null。
scene.environment = new RGBELoader().load( 'textures/equirectangular/venice_sunset_1k.hdr' );
scene.environment.mapping = THREE.EquirectangularReflectionMapping;
scene.fog = new THREE.Fog( 0x333333, 10, 15 );

// 网格地板
grid = new THREE.GridHelper( 20, 40, 0xffffff, 0xffffff );
grid.material.opacity = 0.2;
grid.material.depthWrite = false;
grid.material.transparent = true;
scene.add( grid );

// materials

const bodyMaterial = new THREE.MeshPhysicalMaterial( {
color: 0xff0000, 
metalness: 1.0, 
roughness: 0.5, 
clearcoat: 1.0, // 清漆层
clearcoatRoughness: 0.03
} );

const detailsMaterial = new THREE.MeshStandardMaterial( {
color: 0xffffff, metalness: 1.0, roughness: 0.5
} );

const glassMaterial = new THREE.MeshPhysicalMaterial( {
color: 0xffffff, metalness: 0.25, roughness: 0, transmission: 1.0
} );

const bodyColorInput = document.getElementById( 'body-color' );
bodyColorInput.addEventListener( 'input', function () {

bodyMaterial.color.set( this.value );

} );

const detailsColorInput = document.getElementById( 'details-color' );
detailsColorInput.addEventListener( 'input', function () {

detailsMaterial.color.set( this.value );

} );

const glassColorInput = document.getElementById( 'glass-color' );
glassColorInput.addEventListener( 'input', function () {

glassMaterial.color.set( this.value );

} );

// Car
// 车底部阴影图
const shadow = new THREE.TextureLoader().load( 'models/gltf/ferrari_ao.png' );

// 车3D模型
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath( 'jsm/libs/draco/gltf/' );

const loader = new GLTFLoader();
loader.setDRACOLoader( dracoLoader );

loader.load( 'models/gltf/ferrari.glb', function ( gltf ) {

const carModel = gltf.scene.children[ 0 ];

carModel.getObjectByName( 'body' ).material = bodyMaterial;

carModel.getObjectByName( 'rim_fl' ).material = detailsMaterial;
carModel.getObjectByName( 'rim_fr' ).material = detailsMaterial;
carModel.getObjectByName( 'rim_rr' ).material = detailsMaterial;
carModel.getObjectByName( 'rim_rl' ).material = detailsMaterial;
carModel.getObjectByName( 'trim' ).material = detailsMaterial;

carModel.getObjectByName( 'glass' ).material = glassMaterial;

wheels.push(
carModel.getObjectByName( 'wheel_fl' ),
carModel.getObjectByName( 'wheel_fr' ),
carModel.getObjectByName( 'wheel_rl' ),
carModel.getObjectByName( 'wheel_rr' )
);

// shadow  车底部阴影
const mesh = new THREE.Mesh(
new THREE.PlaneGeometry( 0.655 * 4, 1.3 * 4 ),
new THREE.MeshBasicMaterial( {
map: shadow, 
blending: THREE.MultiplyBlending, 
toneMapped: false, // 定义这个材质是否会被渲染器的toneMapping设置所影响,默认为 true 。
transparent: true // 定义此材质是否透明。这对渲染有影响,因为透明对象需要特殊处理,并在非透明对象之后渲染。设置为true时,通过设置材质的opacity属性来控制材质透明的程度。默认值为false。
} )
);
mesh.rotation.x = - Math.PI / 2;
// renderOrder: 这个值将使得scene graph(场景图)中默认的的渲染顺序被覆盖, 即使不透明对象和透明对象保持独立顺序。 渲染顺序是由低到高来排序的,默认值为0。
mesh.renderOrder = 2;
carModel.add( mesh );

scene.add( carModel );

// 坐标轴
const axesHelper = new THREE.AxesHelper(100);
scene.add(axesHelper);

} );

}

function onWindowResize() {

camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();

renderer.setSize( window.innerWidth, window.innerHeight );

}

function render() {

controls.update();

const time = - performance.now() / 1000;

for ( let i = 0; i < wheels.length; i ++ ) {

wheels[ i ].rotation.x = time * Math.PI * 2;

}

grid.position.z = - ( time ) % 1;

renderer.render( scene, camera );

stats.update();

}

init();

</script>

</body>
</html>

vite构建工具和webpack构建工具有什么共同点和不同处

Webpack 和 Vite 都是现代前端开发中常用的构建工具,但它们在设计理念、性能和使用方式上有显著的区别。以下是它们的对比以及如何使用它们的简要说明:

1. Webpack

特点

  • 成熟稳定:Webpack 是前端生态中历史最悠久、最成熟的构建工具之一,拥有庞大的社区和丰富的插件生态。
  • 模块化打包:Webpack 的核心功能是将项目中的所有资源(如 JavaScript、CSS、图片等)视为模块,并通过依赖关系将它们打包成一个或多个文件。
  • 高度可配置:Webpack 的配置非常灵活,支持通过配置文件(如 webpack.config.js)自定义打包行为。
  • 支持多种功能:Webpack 支持代码分割、懒加载、热更新(HMR)、Tree Shaking 等高级功能。

适用场景

  • 大型项目,尤其是需要复杂配置和自定义打包逻辑的项目。
  • 需要兼容旧版浏览器或处理复杂资源加载的项目。

如何使用

  1. 安装 Webpack
npm install webpack webpack-cli --save-dev

2. 创建配置文件webpack.config.js):

const path = require('path');
module.exports = {
  entry: './src/index.js', //入口文件
  output: {
    filename: 'bundle.js', //入口文件
    path: path.resolve(__dirname,'dist'), //输出目录
  },
  module: {
    rules: [
      { 
        test: /\.css$/, //处理css文件
        use: ['style-loader', 'css-loader'],
      },
      ],
  },
};



3. 运行 Webpack

npx webpack

2. Vite

特点

  • 极速开发体验:Vite 利用现代浏览器的原生 ES 模块支持,在开发环境下无需打包,直接按需加载模块,启动速度极快。
  • 基于 ES Modules:Vite 在开发模式下使用浏览器原生的 ES Modules,生产模式下使用 Rollup 进行打包。
  • 开箱即用:Vite 提供了默认配置,支持 TypeScript、CSS 预处理器、热更新等功能,无需复杂配置。
  • 面向现代浏览器:Vite 更适合现代浏览器,对旧版浏览器的支持需要通过插件实现。

适用场景

  • 中小型项目,尤其是需要快速启动和开发的场景。
  • 使用现代前端框架(如 Vue 3、React)的项目。

如何使用

  1. 安装 Vite

按照提示选择项目模板(如 Vue、React、Vanilla JS 等)。

2. 启动开发服务器

npm run dev

3. 构建生产环境代码

npm run build

3:Webpack和Vite的主要区别

1743643881186.png

4. 如何选择?

  • 选择 Webpack
    • 项目需要高度自定义的打包配置。
    • 需要兼容旧版浏览器。
    • 项目规模较大,依赖复杂。
  • 选择 Vite
    • 追求极速的开发体验。
    • 项目基于现代前端框架(如 Vue 3、React)。
    • 项目规模较小,配置简单。

总结

  • Webpack 是前端构建工具的“老大哥”,功能强大但配置复杂,适合大型项目。
  • Vite 是新一代构建工具,以极速开发体验著称,适合中小型项目和现代前端框架。

根据项目需求选择合适的工具,可以显著提升开发效率和体验!

需求:对表格操作列中的操作进行局部刷新

需求:对表格操作列中的操作进行局部刷新,如action列中有勾号,叉号,来回切换按钮能够改变数据状态,同时不希望使用整体调接口查询数据来刷新,这里用“局部刷新(缓存方式)”实现

如下图:

image.png

  1. 表格结构
<el-table-column
                  prop="address"
                  label="Action"
                  width="120"
                  align="center"
                >
                  <template #default="scope">
                    <div class="tableTools">
                      <template v-if="scope.row.show">
                              <el-button
                                @click="
                                  fileStatusBtn(
                                    scope.row.id,
                                    0,
                                    scope.row,
                                    scope.$index
                                  )
                                "
                                link
                                title="Set the file status (hide or show) for new reviewer and author"
                                type="primary"
                              >
                                <el-icon><Select /></el-icon>
                              </el-button>
                            </template>
                            <template v-else>
                              <el-button
                                @click="
                                  fileStatusBtn(
                                    scope.row.id,
                                    1,
                                    scope.row,
                                    scope.$index
                                  )
                                "
                                link
                                title="Set the file status (hide or show) for new reviewer and author"
                                type="primary"
                              >
                                <el-icon>
                                  <CloseBold />
                                </el-icon>
                              </el-button>
                            </template>

                  
                    </div>
                  </template>
                </el-table-column>
  1. 方法
const fileStatusBtn = (id, status, row, index) => {
    row.show = !row.show; // show是用来控制勾号,叉号的交替显示
    // 局部更新某一条数据的show状态,不要重新调用接口,造成服务器压力
    updateRowData(index, "show", row.show);
    
    // 原先的调整个表格接口查询新结果
    // emit("uploadDatas"); 
};
  1. updateRowData方法
// 修改数据并同步更新缓存的方法
const updateRowData = (rowIndex, key, value) => {
  // 修改当前数据
  const cacheKey = `tableSubmissionData_${pageId}`;
  const cachedData = JSON.parse(localStorage.getItem(cacheKey));

  if (cachedData) {
    // 修改缓存中的数据
    cachedData.data.content.preReview.articleFiles[rowIndex][key] = value;

    // 同步到组件的内存数据
    aeInfor.value.reviewFiles[rowIndex][key] = value;

    // // 更新缓存
    updateCache(cacheKey, cachedData);
  }
};

const updateCache = (key, data) => {
  localStorage.setItem(key, JSON.stringify(data));
};
  1. 联调接口的方法中添加缓存数据判断
const cacheKey = ref("");

const getSubmissionTabDatas = async () => {
  // 使用 localStorage 缓存数据,避免重复请求
  cacheKey.value = `tableSubmissionData_${pageId}`;
  const cachedData = localStorage.getItem(cacheKey.value);

  if (cachedData) {
    const res = JSON.parse(cachedData);

    allDatas.value = res.data.content;
    participent.value = res.data.content.summary.participantList;
    lastRoundId.value = res.data.content.summary.roundid;
    tabButtons.value = res.data.content.tabButtons;
    isachive.value = res.data.content.isachive ? true : false;
    aeInfor.value.reviewFiles = res.data.content.preReview.articleFiles;
    allDatas.value.showBlackList = res.data.content.showBlackList;

    console.log("====缓存===", aeInfor.value.reviewFiles);

    return; // 从缓存中获取数据,不需要再进行 API 请求
  }

  await getSubmissionTab({
    id: pageId,
    type: pageType ? "editorialAssistant" : undefined,
    accessArticle: route.query.isOnlyShowSE ? 1 : undefined,
  }).then((res) => {
    localStorage.setItem(cacheKey.value, JSON.stringify(res));

    allDatas.value = res.data.content;
    participent.value = res.data.content.summary.participantList;
    lastRoundId.value = res.data.content.summary.roundid;
    tabButtons.value = res.data.content.tabButtons;
    isachive.value = res.data.content.isachive ? true : false;
    aeInfor.value.reviewFiles = res.data.content.preReview.articleFiles;
    allDatas.value.showBlackList = res.data.content.showBlackList;

    console.log("====接口===", aeInfor.value.reviewFiles);
  });
};
  1. 缓存数据清除
const handleBeforeUnload = (event) => {
  localStorage.clear();
  localStorage.removeItem(cacheKey.value);
};

onMounted(async () => {
  window.addEventListener("beforeunload", handleBeforeUnload);
  localStorage.clear();
  localStorage.removeItem(cacheKey.value);
  await getSubmissionTabDatas();
  
  ----------------------------------------------------------------
  await initializeCollapseState(); // 初始化折叠状态
  nextTick(() => {
    const el = document.getElementById("submissionId");
    if (!route.query.roundIndex) return;
    el?.scrollIntoView({
      behavior: "smooth", //smooth:平滑,auto:直接定位
      block: "start",
      inline: "start",
    });
  });
});

onBeforeUnmount(() => {
  window.removeEventListener("beforeunload", handleBeforeUnload);
  localStorage.clear();
  localStorage.removeItem(cacheKey.value);
});

git subtree 最佳实践

目录

1背景

1.1 痛点

目前业务主要有A端和B端两个系统,这两个系统技术栈是完全相同的,许多功能也相同。所以在日常的开发过程中,产生了大量的重复工作,一个需求在A端完成后,还需要复制到B端,这样往往容易出现疏漏。

1.2 解决思路

实现代码复用目前,有下面两种方法:

  • 抽象成NPM包进行复用

  • 使用Git的子仓库对代码进行复用

由于本项目要实现业务代码复用,抽成 npm 包的方式就不太合适。

1.3 什么是git子仓库

通俗上的理解, 一个Git仓库下面放了多个其他的Git仓库,其他的Git仓库就是我们父级仓库的子仓库。

通过使用git子仓库将公共的组件抽离出来,实现在一端更改后,另一端通过git去合并代码,将我们从繁重的复制粘贴中解放出来。同时,可以在后续的需求中放入公共组件,通过增量的方式去应用这个技术,不会影响以前的代码。

1.4 git的两种子仓库方案

目前git实现子仓库有下面两种方案:

  1. git submodule。 tdesign 使用的就是这种方案。

  2. git subtree

两种方案的对比如下:

维度 subtree submodule 优劣对比
空间占用 subtree 在初始化 add 时,会将子仓库 copy 到父仓库中,并产生至少一次 merge 记录。所以会占用大量父仓库空间 submodule 在初始化 add 时,会在父仓库新建一个 .gitmodules 文件,用于保存子仓库的 commit hash 引用。所以不会占用父仓库空间 submodule 更优
clone subtree add 至父仓库之后,后续的 clone 操作与单一仓库操作相同 后续 clone 时 submodule 还需要 init/update 操作,且 submodule 子仓库有自己的分支。 流水线部署时需要更改配置。 subtree 更优
update 子仓库更新后,父仓库需要 subtree pull 操作,且命令行略长,需要指定 --prefix 参数。由于无法感知子仓库的存在,可能会产生 merge 冲突需要处理 子仓库更新后,父仓库需要 submodule update 操作。父仓库只需变动子仓库 hash 引用,不会出现冲突 submodule 更优
commit 父仓库直接提交父子仓库目录里的变动。若修改了子仓库的文件,则需要执行 subtree push 父子仓库的变动需要单独分别提交。且注意先提交子仓库再提交父仓库 subtree 更优

用一句话来描述 Git Subtree 的优势就是:

经由 Git Subtree 来维护的子项目代码,对于父项目来说是透明的,所有的开发人员看到的就是一个普通的目录,原来怎么做现在依旧那么做,只需要维护这个 Subtree 的人在合适的时候去做同步代码的操作。

1.5 git subtree 对现有项目的影响

使用git subtree 无需改变现有工程结构,可以只在新需求中使用它去复用代码,相当于它只是一个复制粘贴的工具。

2方案设计

2.1 创建子仓库

建立一个单独的git仓库命名为 common , 可以创建如下的目录结构:

-common
  -utils 公共的工具函数
  -services 接口
  -components 公共的组件
  -hooks 公共的hooks

2.2 关联子仓库

然后在A端和B端添加common的远程仓库:

 git remote add common [common仓库地址]

建立父仓库和子仓库的依赖关系:

git subtree add --prefix=src/common common master

将common远程仓库的master分支拷贝到父仓库的 src/common 目录下, 这时在两个项目的src目录多一个 common 的文件夹,我们可以像一个本地目录一样去使用里面的代码。

--prefix 可以用 -P 来代替,见下文。

2.3 拉取子仓库更新

git subtree pull -P src/common common master

2.4 推送更改到子仓库

方法一 直接提交

git subtree push -P src/common common master

subtree push实际上是遍历本工程每一次提交,把提交文件涉及到subtree目录的挑出来,同步到subtree工程,如果提交有很多,速度会非常慢。

方法二 拆分代码再push[推荐]

git subtree split --rejoin -P src/common
git subtree push -P src/common common master

如果想要split成功,一定要去除 commit msg 的校验。

方法三 拆分代码到单独分支

git subtree split --rejoin -P src/common -b split-common
git push common split-common

首先将 common 拆分到父仓库的 split-common 分支,可以通过 checkout 到这个分支查看内容。

2.5 删除子仓库

git rm -r src/common

2.5 细节

在开发一个需求的时候, A端更改了 common 后,其他人只需要向以前一样在父仓库拉取代码。而当想在B端使用 common 代码,则需要将A端的代码同步到common 仓库,B拉取一下就行。

问题

git subtree split 无效

我们项目是基于 umi 脚手架开发的项目,这个脚手架自带了一个 gitHooks 会对 commit 的msg进行校验,而git subtree split 的原理就是通过 msg 进行判断。 解决方法:去掉 package.json 中的 commit 校验

{
  "gitHooks": {
  }
}

修改后没有同步

问题描述

修改一个后,没有push代码,慢慢导致后面两端的子仓库出现差异, 出现代码冲突。

解决方法

每次修改公共的代码都要 push 和 pull, 手动保持一致。

git subtree pull 冲突

错误信息如下

fatal: refusing to merge unrelated histories

解决方法, 在 git subtree pull 时添加 --squash 参数, 类似于 git push 的 --allow-unrelated-historie参数。

git subtree push 不上去

git push using:  common feature/20221214
cache for f1156335aca1314ff75ba328a850cbdd13affb5a already exists!

stackoverflow.com/questions/6…

暂时无法解决

参考文章

# 为什么你的公司不应该使用git submodule # Git subtree用法与常见问题分析 # 用 Git Subtree 在多个 Git 项目间双向同步子项目 # Git subtree 要不要使用 –squash 参数 # 掌握Git的subtree[译]

🔥《爆肝整理》保姆级系列教程-玩转Charles抓包神器教程(15)-Charles如何配置反向代理

1.简介

在App开发的过程当中,抓包是一个很常见的需求,而有些app的请求不会在网络设置代理时被抓到数据包,这里若是需要抓包就需要搭建反向代理。

2.什么是代理?

什么是代理,来一张图了解一下。

代理又分为正向代理和反向代理。

3.什么是正向代理?

先来看张图~

【再举个栗子】

某同学喜欢面向搜索引擎编程,想通过 百度 搜索引擎查找一些学习资料,但是有些网站直接访问可能不太安全,会暴露自己的IP,同学比较苦恼,想着怎样才能使用百度 搜索自己想要的学习资料,又不会暴露自己的IP在网站上呢?

这时我告诉该同学,我呢手上刚好有一台代理服务器,这台代理服务器通过nginx配置了正向代理转发http和https请求,你呢,只需要在自己的Windows本地电脑的网关配置一下这台代理服务器的IP和端口号,就能正常通过代理服务器访问到百度 并搜索相关的学习资料了,还不会暴露自己真实的IP~

4.什么是反向代理?

先来一张图了解下~

和正向代理相应的,正向代理代理客户端,反向代理代理服务端。

反向代理(reverse proxy):是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。

我们在租房子的过程中,除了有些房源需要通过中介以外,还有一些是可以直接通过房东来租的。用户直接找到房东租房的这种情况就是我们不使用代理直接访问国内的网站的情况。

还有一种情况,就是我们以为我们接触的是房东,其实有时候也有可能并非房主本人,有可能是他的亲戚、朋友,甚至是二房东。但是我们并不知道和我们沟通的并不是真正的房东。这种帮助真正的房主租房的二房东其实就是反向代理服务器。这个过程就是反向代理。

对于常用的场景,就是我们在Web开发中用到的负载均衡服务器(二房东),客户端(租客)发送请求到负载均衡服务器(二房东)上,负载均衡服务器(二房东)再把请求转发给一台真正的服务器(房东)来执行,再把执行结果返回给客户端(租客)。
反向代理,其实是"代理服务器"代理了"目标服务器",去和"客户端"进行交互。

通过反向代理服务器访问目标服务器时,客户端是不知道真正的目标服务器是谁的,甚至不知道自己访问的是一个代理。

5.正向代理和反向代理的区别

虽然正向代理服务器和反向代理服务器所处的位置都是客户端和真实服务器之间,所做的事情也都是把客户端的请求转发给服务器,再把服务器的响应转发给客户端,但是二者之间还是有一定的差异的。

1、正向代理其实是客户端的代理,帮助客户端访问其无法访问的服务器资源。反向代理则是服务器的代理,帮助服务器做负载均衡,安全防护等。

2、正向代理一般是客户端假设的,比如在自己的机器上安装一个代理软件。而反向代理一般是服务器假设的,比如在自己的机器集群中部署一个反向代理服务器。

3、正向代理中,服务器不知道真正的客户端到底是谁,以为访问自己的就是真实的客户端。而在反向代理中,客户端不知道真正的服务器是谁,以为自己访问的就是真实的服务器。

4、正向代理和反向代理的作用和目的不同。正向代理主要是用来解决访问限制问题。而反向代理则是提供负载均衡、安全防护等作用。二者均能提高访问速度。

6.须要准备的工做

  • 在本身电脑上面搭建一个可用的Charles
  • 须要抓包的远端服务的端口号和Host地址
  • 在本身电脑上面搭建一个本地DNS解析服务

7.具体步骤 (Windows下的操作,Mac也同理)

1.确保手机可以连接上Charles,本身电脑上面可以看到正常请求出来的数据包(具体抓包可以看宏哥前边的教程)

2.打开Charles,勾选Proxy --> Reverse proxise...,进入反向代理设置界面。如下图所示:

3.进入Reverse Proxies Settings(反向代理设置)页面,勾选 Enable Reverse Proxies 。如下图所示:

4.在【Add】新增。如下图所示:

Edit Reverse Proxy 视图中的选项含义:

local port:本地端口
本地主机上的端口创建反向代理。该字段可能会自动填充一个可用的端口。如果有另一个应用程序使用该端口,则在反向代理启动时将收到一条警告消息

Remote host:远程主机
作为反向代理的目的地的远程主机的主机名或IP地址

Remote port:远程端口
远程端口默认为80,这是HTTP的默认端口。

Rewrite redirects:重写重定向
重定向远程服务器的响应将被重写与反向代理源地址相匹配,默认为开
远程服务器的重定向响应是完全限定的URL,即使它们在同一网站内
如果重定向到远程服务器地址,则需要将其重写为反向代理本地地址,否则客户端将使用重定向URL到远程主机,因此不再通过反向代理连接

Preserve host in header fields:保留主机头
Host HTTP标头从传入请求不变地传递,而不是正常重写主机头以匹配反向代理远程主机,默认为关闭
仅当您具有特定要求时,才需要保留主机头;普通使用的时候没有必要使用的

Listen on a specific address:监听特定地址
指定本地地址以侦听反向代理,可以启用此选项并在此处输入IP地址

8.Charles反向代理实战

Charles反向代理是提供一个端口转发的功能,用于除IE外发出的HTTP请求,例如需要跟踪Smartbi服务器与XMLA服务器之间的通信、Smartbi SDK与服务器之间通信等。

宏哥在Apache服务器安装在A计算机上,IP地址为:10.11.53.180,并开启服务,端口号为:80(默认)。然后宏哥简单部署一个HTML页面,在浏览器中访问服务。如下图所示:

现移动端访问A服务器部署的HTML页面出现错误,但是需要录制移动端的HTTP请求。这时候就需要Charles的反向代理帮助我们解决这个问题。具体操作步骤如下:

1.找一台其他计算机,如计算机B,其IP地址为10.11.53.193,宏哥这里演示的就是宏哥的本地计算机,如下图所示:

2.在计算机B上安装Charles,并启动,这里宏哥已经安装就不做演示了。

3.选中charles上的"Proxy"-》"Reverse Proxies",进入反向代理设置界面,如下图所示:

4.反向代理设置界面如下,点击"Add"按钮,新建反向代理设置,如下图所示:

5.设置反向代理的端口号,IP地址等信息。

其中 Local Port是指计算机B的一个空闲端口,如本例中使用8080;
Remote Host是指HTML页面服务的IP,即计算机A的IP: 10.11.55.182;
Remote Port是指HTML页面服务的端口号,在本例中访问HTML页面的端口号为80(Apache默认端口)
点击OK保存反向代理设置,如下图所示:

6.上一步点击OK之后会出现反向代理列表窗口,勾选我们上一步设置的反向代理,点击ok启用,如下图所示:

7.在任意一台计算机或者移动端上,通过**http://计算机B的IP:反向代理中设置的Loal Port端口/inde.html**,可以访问到HTML页面服务。本例中通过在浏览器或者移动端的服务器设置上输入**http://10.11.53.193:8080/index.html**访问,如下图所示:

*注:访问是需要写IP,不能写localhost。
*

8.在charles中会监测到反向代理访问,首次会弹出是否允许访问,选择'Allow'按钮,允许访问。没有设置代理之前是访问不到的,如下图所示:

9.在计算机B上的charles就可以录制到HTTP请求,如下图所示:

9.小结

反向代理位于用户和应用服务器之间,是连接用户和服务器的中介。

于是我们可以

1.缓存,将服务器的响应缓存在自己的内存中,减少服务器的压力。

2.负载均衡,将用户请求分配给多个服务器。

3.访问控制

4.加上一些特殊的东西做特殊的事情(如IPS—入侵防御系统、web应用防火墙等)

好了,今天时间也不早了,宏哥就讲解和分享到这里,感谢您耐心的阅读,希望对您有所帮助。

Flask学习笔记 - 视图函数

前言

继续自习...

Flask 视图函数

视图函数是Flask应用中的核心部分,它负责处理请求并生成响应

  1. 定义视图函数:视图函数是处理请求并返回响应的核心功能。
  2. 接收请求数据:使用 request 对象获取 URL 参数、表单数据、查询参数等。
  3. 返回响应:可以返回字符串、HTML、JSON 或自定义响应对象。
  4. 处理请求和响应:使用 request 对象和 make_response 来处理请求和生成自定义响应。
  5. 处理错误:视图函数内处理异常或使用 Flask 的错误处理机制。
  6. 视图函数的装饰器:使用 @app.before_request、@app.after_request 等装饰器处理请求前后逻辑。
  7. 视图函数返回的状态码:可以指定 HTTP 状态码来表示请求的处理结果。

定义视图函数/接收请求数据/返回响应/处理请求和响应

from flask import request

# 接收 - URL参数
@app.route('/greet/<name>')  
def greet(name):
    return f'Hello, {name}!'

# 接收 - 表单数据
@app.route('/submit', methods=['POST'])  
def submit():
    username = request.form.get('username')  
    return f'Form submitted by {username}!'

# 接收 - GET请求中query
@app.route('/search')
def search():
    query = request.args.get('query')
    return f'Search results for: {query}'
    
# 返回 - 字符串
@app.route('/message')
def message():
    return 'This is a simple message.'

# 返回 - HTML模板
@app.route('/hello/<name>')
def html_hello(name):
    return render_template('hello.html', name=name)

# 返回 - JSON
@app.route('/api/data')
def api_data():
    data = {'key': 'value'}
    return jsonify(data)

# 返回 - 自定义响应对象
@app.route('/custom')
def custom_response():
    response = Response('Custom response with headers', status=200)
    response.headers['X-Custom-Header'] = 'Value'
    return response

# 处理请求和响应
@app.route('/info')
def info():
    user_agent = request.headers.get('User-Agent')
    return f'Your user agent is {user_agent}'

处理错误

可以在视图函数中处理异常或错误,或者通过 Flask 提供的错误处理机制来处理应用中的错误

# try-except
# /divide/3/0
@app.route('/divide/<int:x>/<int:y>')
def divide(x, y):
    try:
        result = x / y
        return f'Result: {result}'
    except ZeroDivisionError:
        return 'Error: Division by zero', 400

# 全局错误
@app.errorhandler(404)
def not_found(error):
    return '消失了', 404

抛异常 01.png

正常 02.png

全局错误码 03.png

装饰器

  • @app.before_request:在每个请求处理之前运行的函数。
  • @app.after_request:在每个请求处理之后运行的函数。
  • @app.teardown_request:在请求结束后运行的函数,用于清理工作。
@app.before_request
def before_request():
    print('请求前')

@app.after_request
def after_request(response):
    print('请求后')
    return response

@app.teardown_request
def teardown_request(exception):
    print('请求结束,清理')

看到终端有输出对应的日志 04.png

视图函数返回的状态码

视图函数不仅可以返回内容,还可以指定 HTTP 状态码。

# 返回
@app.route('/status')
def status():
    return 'Everything is OK', 200

@app.route('/error')
def error():
    return Response('An error occurred', status=500)

工具栏 - 帮助 - 切换开发人员工具,可以打开开发工具,检查网络,确认确实返回了500的错误码

06.png

Demo

Flask

参考

  1. Flask 视图函数

前端图片技术深度解析:格式选择、渲染原理与性能优化

前言

在Web开发中,图片处理是影响用户体验和网站性能的关键因素。前端开发者每天都要面对一系列图片相关的技术决策:

  • 静态图片格式:选择PNG还是JPG追求更小体积?
  • 动态图像选择:GIF还是APNG?
  • 压缩策略:如何在保持视觉质量的前提下减小文件大小?
  • 格式应用:web端图片是否要转化成 WebP 还是 AVIF?

这些决策直接影响着首屏加载时间、带宽消耗和用户交互体验等核心指标,甚至将影响业务的商业转化率。图片处理其实也考验着一个前端的基本功,本文将深入解析常见图片格式的编码原理,结合浏览器渲染机制与现代Web标准,提供一套科学、可落地的图片优化实践方案。


一、图片格式

在选择图片格式之前首先要清楚这些图片格式的特点和区别

1. 类型

图片格式主要分为两种类型:位图和矢量图。我们常见的图片除了 SVG 都是位图

位图

位图是由像素点组成的,适合照片和复杂图像,其清晰度由分辨率和像素总量共同决定。

矢量图

矢量图是通过代码或者说是用数学公式描述的

  1. 优点是在任何缩放比例下都可以展示得很清晰而不会失真
  2. 缺点是细节的展示效果不够丰富,对于复杂图像来说,比如要达到照片的效果,若通过SVG进行矢量图绘制,需要大量的代码描述,生成文件就会非常大,并且浏览器解析和渲染对很耗性能,所以svg不适合细节丰富的图像,只适合图标和简单图形。

2. 压缩方式

图片格式不同的压缩方法由图像编码标准决定:

  1. JPG(有损压缩)

    • 原理:通过丢弃人眼不敏感的细节(如高频色彩信息)来减少文件体积。
    • 特点
      • 压缩率极高,适合照片类图像(色彩丰富、渐变多)。
      • 不支持透明度,多次保存会累积质量损失(“代际损失”)。
  2. PNG(无损压缩)

    • 原理:使用 DEFLATE 算法无损压缩像素数据,保留所有原始信息。
    • 特点
      • 适合图标、线条图等需要保留锐利边缘的图像。
      • 支持透明度,文件体积通常比 JPEG 大。
  3. WebP:集 JPEG 和 PNG 优点于一身。无损压缩比 PNG 小 26%,有损压缩比 JPEG 小 25-34%,并且支持透明通道。

  4. AVIF:更先进的压缩算法,压缩率比 WebP 还高 30%。但兼容性较差,目前浏览器支持率只有 86%。

而我们开发过程中经常对图片进行二次压缩(同格式),比如借助 TinyPng 工具,实际上是对图片进行有损压缩

  1. 量化减少颜色数量:将 1600 万色(24-bit)缩减到 256 色(8-bit)。
  2. 丢弃元数据:删除 EXIF、ICC 配置等非必要数据。
  3. 优化压缩算法:使用改进的 DEFLATE 算法更高效压缩数据。

虽然 PNG 本身是无损格式,但 TinyPNG 通过有损预处理+无损压缩的组合,实现了“视觉无损”的高压缩率。

3. 图片格式总结

格式 类型 压缩方式 透明度支持 动画支持 特点 适用场景
JPEG 位图 有损 高压缩率,适合照片,但压缩过度会失真 照片、复杂色彩图像
PNG 位图 无损 ✅ (Alpha) 支持透明,无损压缩,文件较大 透明图标、精确图形(如截图)
GIF 位图 无损(256色) ✅ (1-bit) 支持简单动画,但色彩有限 表情包、简单动画
WebP 位图 有损/无损 ✅ (Alpha) 现代格式,压缩效率高,支持动画和透明度 全能替代(JPEG/PNG/GIF)
AVIF 位图 有损/无损 ✅ (Alpha) 下一代压缩,比WebP更高效,但兼容性差 高质量图片、未来项目
APNG 位图 无损 ✅ (Alpha) PNG的动画扩展,支持全透明动画 高质量动画(替代GIF)
SVG 矢量图 无损 无限缩放,通过代码描述图形,支持CSS/JS控制 图标、LOGO、UI元素

二、浏览器渲染机制与性能影响

1. 关键渲染路径中的图片处理

graph TD
    A[HTML解析] --> B[发现<img>标签]
    B --> C{是否可见视口?}
    C -->|是| D[立即加载]
    C -->|否| E[延迟加载]
    D --> F[解码线程处理]
    F --> G[主线程布局计算]
    G --> H[绘制合成]

浏览器的图片渲染流程可以分为以下阶段:

1. 解析与加载

  • HTML 解析:浏览器解析 HTML 时遇到 <img> 标签,立即发起图片资源的网络请求(除非使用 loading="lazy" 延迟加载)。
  • 优先级控制:图片的加载优先级通常低于关键资源(如 CSS、JS),但可通过 fetchpriority="high" 调整。
  • 预加载优化:使用 <link rel="preload"> 提前加载关键图片。
2. 解码(Decoding)
  • 主线程阻塞:图片解码(将二进制数据转换为像素)默认在主线程执行,大图片可能导致主线程卡顿。

  • 异步解码:通过 img.decode() API 异步解码图片,避免阻塞主线程。

  • 格式影响

    • JPEG/PNG:解码复杂度较高。
    • WebP/AVIF:现代格式解码效率更高。
3. 布局(Layout)与绘制(Paint)
  • 布局计算:图片尺寸变化(未指定 width/height)会导致布局重排(Reflow)。
  • 图层分离:使用 will-change: transform 或 transform: translateZ(0) 将图片提升到独立图层,减少绘制区域。
4. 合成(Composite)
  • GPU 加速:某些 CSS 属性(如 transformopacity)触发 GPU 合成,跳过主线程直接由合成器处理。
  • 减少重绘:避免频繁修改图片尺寸或位置,触发不必要的重绘(Repaint)。

2. 性能瓶颈分析

  • 解码耗时:高分辨率图片消耗CPU资源(尤其是AVIF/WebP)
  • 布局抖动:未设置尺寸的图片引发回流(CLS问题)
  • 内存占用:4096x4096的RGBA图片占用67MB内存
  • 网络竞争:过多图片请求阻塞关键资源加载

实测数据

  • 未优化的2MB JPEG:加载时间≈800ms(4G网络)
  • 优化后的200KB WebP:加载时间≈150ms
  • LCP(最大内容绘制)提升300ms可使转化率提高5%

三、性能优化全方案

1. 格式选择策略

前面可以得出结论,avif压缩率最高,性能最好,但要考虑兼容性问题,而webp是当前最佳平衡选择,最佳实践我们可以通过代码检测图片格式兼容性。

服务端:浏览器端在发起图片请求时会带上当前浏览器支持的图片格式,可以在服务端判断后返回对应的图片格式。

image.png

// 动态格式检测
const acceptHeader = req.headers.accept || '';
const supportsWebP = acceptHeader.includes('webp');
const supportsAvif = acceptHeader.includes('avif');

function getBestFormat() {
  if (supportsAvif) return 'avif';
  if (supportsWebP) return 'webp';
  return 'jpg';
}

决策树

graph TD
    A[选择图片] --> B{是否需要动画?}
    B -->|是| C[GIF/WebP/AVIF]
    B -->|否| D{是否为复杂图像?}
    D -->|是| E[是否需要透明度]
    D -->|否| F[SVG]
    E -->|是| G[PNG/WebP/AVIF]
    E -->|否| H[JPEG/WebP/AVIF]
    C --> I[检测浏览器兼容性]
    F --> I
    G --> I
    H --> I

2. 响应式图片技术

响应式图片是现代Web开发的必备技术,可以根据设备屏幕大小和分辨率提供最合适的图片资源。

srcset 和 sizes 属性

<img
  src="image-400w.jpg"
  srcset="image-400w.jpg 400w, image-800w.jpg 800w, image-1200w.jpg 1200w"
  sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"
  alt="响应式图片示例" />

picture 元素实现格式回退

<picture>
  <source srcset="image.avif" type="image/avif" />
  <source srcset="image.webp" type="image/webp" />
  <img src="image.jpg" alt="图片描述" />
</picture>

3. 懒加载策略

原生懒加载

<img src="image.jpg" loading="lazy" alt="懒加载图片" />

交叉观察器实现

const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      observer.unobserve(img);
    }
  });
});

document.querySelectorAll('img[data-src]').forEach((img) => {
  observer.observe(img);
});

4. CDN与缓存优化

  • 使用CDN分发图片资源,减少网络延迟
  • 设置合理的缓存策略,常见图片可设置长期缓存
  • 使用内容哈希命名,便于缓存更新
# Nginx缓存配置示例
location ~* \.(jpg|jpeg|png|gif|webp|avif)$ {
    expires 30d;
    add_header Cache-Control "public, no-transform";
}

5. 图片预处理与自动化

构建时优化

使用webpack、gulp等工具在构建阶段自动处理图片:

// webpack配置示例
module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif)$/i,
        use: [
          {
            loader: 'image-webpack-loader',
            options: {
              mozjpeg: {
                progressive: true,
                quality: 65,
              },
              optipng: {
                enabled: false,
              },
              pngquant: {
                quality: [0.65, 0.9],
                speed: 4,
              },
              webp: {
                quality: 75,
              },
            },
          },
        ],
      },
    ],
  },
};

图片服务API

使用图片处理服务动态调整图片大小和格式: 例如通过阿里云OSS服务通过x-oss-process图片处理参数,将图片进行裁剪、质量变化、格式转化等操作

<!-- 使用OSS等服务 -->
<img src="https://oss-console-img-demo-cn-hangzhou-3az.oss-cn-hangzhou.aliyuncs.com/example.gif?x-oss-process=image/format,png" alt="优化图片" />

四、未来趋势展望

1. HTTP/3多路复用

HTTP/3基于QUIC协议,采用UDP传输,解决了HTTP/2中的队头阻塞问题。对于图片密集型应用,多路复用能显著提升并行下载效率,减少等待时间。

graph LR
    A[HTTP/2] --> B[单TCP连接]
    B --> C[队头阻塞]
    D[HTTP/3] --> E[多UDP连接]
    E --> F[避免队头阻塞]

2. 机器学习驱动的图像压缩

Google RAISR技术

RAISR (Rapid and Accurate Image Super Resolution) 使用机器学习算法,能在低分辨率图像基础上重建高质量图像,大幅减少传输数据量。

神经网络压缩

基于神经网络的压缩算法如Facebook的DLVC (Deep Learning Video Coding),可将压缩率提高30-50%,同时保持视觉质量。

3. WebCodecs API与GPU加速

WebCodecs API允许Web应用直接访问底层媒体编解码器,实现硬件加速:

// WebCodecs API示例
async function decodeImage(imageData) {
  const decoder = new ImageDecoder({
    data: imageData,
    type: 'image/avif',
  });

  const result = await decoder.decode();
  return result.image;
}

4. 新一代图像格式

JPEG XL

JPEG XL旨在替代JPEG,提供更高压缩率和更丰富的功能:

  • 比JPEG小60%,同时提高视觉质量
  • 支持无损转换现有JPEG
  • 支持动画、透明度和HDR

WebP 2

Google正在开发WebP的下一代版本,预计将进一步提高压缩效率,并增强动画支持。

结语

图片优化是性能工程的艺术,开发需要平衡视觉质量、加载速度与兼容性需求。通过深入理解格式特性、浏览器工作原理,并结合本文提供的全方位优化策略,可以显著提升Web应用的用户体验和业务指标。

随着新技术的不断涌现,图片优化的方法也在持续演进。作为前端开发者,我们需要不断学习和实践,将最佳图片处理方案融入日常开发流程中。

URL参数传递的两种方式:查询参数与路径参数详解

在现代Web开发中,URL设计是前后端交互的重要桥梁。当我们需要在URL中传递数据时,主要有两种方式:查询参数(Query Parameters)和路径参数(Path Parameters)。这两种方式各有特点,适用于不同场景。本文将通过清晰的对比和具体案例,帮助开发者理解它们的区别,并在实际项目中做出合理选择。

第一部分:基础概念与语法

1. 查询参数 (Query Parameters)

定义:查询参数是附加在URL末尾的键值对,以问号?开始,多个参数间用&连接。

语法格式http://example.com/path?参数1=值1&参数2=值2

实际示例http://localhost:3000/files?category=documents

在这个例子中:

  • 基本URL:http://localhost:3000/files
  • 查询参数:category=documents(表示文件类别为"documents")

2. 路径参数 (Path Parameters)

定义:路径参数是直接嵌入在URL路径中的变量部分,成为URL路径结构的一部分。

语法格式http://example.com/资源类型/资源ID/子资源

实际示例http://localhost:3000/chat/session-1743569839440

在这个例子中:

  • 基本URL:http://localhost:3000/chat
  • 路径参数:session-1743569839440(表示特定的聊天会话ID)

第二部分:技术实现与代码示例

1. 在不同框架中获取查询参数

Next.js实现

// app/files/page.tsx
"use client"
import { useSearchParams } from 'next/navigation';

export default function FilesPage() {
  const searchParams = useSearchParams();
  const category = searchParams.get('category') || 'all';
  
  return <div>显示{category}类别的文件</div>;
}

Express.js实现

// server.js
app.get('/files', (req, res) => {
  const category = req.query.category || 'all';
  res.send(`显示${category}类别的文件`);
});

2. 在不同框架中获取路径参数

Next.js实现

// app/chat/[id]/page.tsx
export default function ChatPage({ params }) {
  const sessionId = params.id; // 例如 "session-1743569839440"
  
  return <div>显示会话ID为{sessionId}的聊天记录</div>;
}

Express.js实现

// server.js
app.get('/chat/:id', (req, res) => {
  const sessionId = req.params.id;
  res.send(`显示会话ID为${sessionId}的聊天记录`);
});

第三部分:系统性对比分析

1. 功能与特性对比

特性 查询参数 (Query Parameters) 路径参数 (Path Parameters)
位置 URL末尾,?之后 嵌入在URL路径中
格式 键值对:?key=value 路径片段:/value
可选性 通常是可选的 通常是必需的
多值支持 支持(如?tags=js&tags=react 需要特殊处理
可见性 显式的键值关系,易于理解 隐含在路径中,需要了解API规则
URL长度 可能导致URL较长 通常产生更简洁的URL

2. 技术层面对比

技术方面 查询参数 (Query Parameters) 路径参数 (Path Parameters)
路由定义 单一路由可处理多种参数组合 通常需要为不同参数值定义路由模式
缓存影响 不同参数可能被视为同一资源的不同表示 不同路径通常被视为完全不同的资源
编码处理 需要正确处理URL编码(如空格、特殊字符) 通常需要符合URL路径规则,可能限制某些字符
安全性 容易在日志、历史记录中暴露 较不明显,但仍可见
状态保存 适合保存在书签中 同样适合保存在书签中

3. 业务与用户体验对比

业务方面 查询参数 (Query Parameters) 路径参数 (Path Parameters)
SEO优化 搜索引擎可能将带不同参数的URL视为同一页面 被视为不同的URL,可能有更好的SEO表现
用户友好性 参数意义较为直观 需要理解站点结构,但URL更简洁
分享便利性 长参数列表可能导致复制/分享问题 通常更简短,便于分享
语义清晰度 明确表示是参数 作为资源路径的一部分,语义更强

第四部分:适用场景分析

1. 查询参数的最佳使用场景

查询参数最适合以下情况:

  1. 筛选与排序

    • 示例:/products?category=electronics&sort=price&order=asc
    • 优势:不改变资源本身,只改变资源的查看方式
  2. 分页功能

    • 示例:/articles?page=2&limit=10
    • 优势:保持URL基础路径不变,便于实现分页控制
  3. 搜索操作

    • 示例:/search?q=javascript&type=blog
    • 优势:可以传递复杂的搜索条件
  4. 非必需参数

    • 示例:/dashboard?view=grid&theme=dark
    • 优势:参数可省略,使用默认值
  5. 状态保持

    • 示例:/map?lat=37.7749&lng=-122.4194&zoom=12
    • 优势:便于保存和恢复用户界面状态

2. 路径参数的最佳使用场景

路径参数最适合以下情况:

  1. 资源标识

    • 示例:/users/12345
    • 优势:明确表示访问特定资源
  2. 分层资源

    • 示例:/departments/sales/employees/john
    • 优势:表达资源的层次结构关系
  3. RESTful API设计

    • 示例:/api/v1/products/567
    • 优势:符合REST架构风格
  4. 必需参数

    • 示例:/invoices/INV-2021-001/download
    • 优势:参数是资源定位的必要部分
  5. 多语言支持

    • 示例:/zh-CN/docs/getting-started
    • 优势:使URL结构更有语义

第五部分:实际案例解析

1. 电子商务网站案例

产品列表页

  • 使用查询参数:/products?category=shoes&size=42&color=black&sort=price_asc
  • 原因:这些都是筛选条件,不改变我们正在查看的资源类型(产品)

产品详情页

  • 使用路径参数:/products/nike-air-max-2023
  • 原因:标识特定产品,是访问该资源的核心标识

2. 内容管理系统案例

文章列表

  • 使用查询参数:/articles?author=zhang&tag=technology&published_after=2023-01-01
  • 原因:这些参数用于筛选文章列表,不改变资源类型

特定文章

  • 使用路径参数:/articles/understanding-url-parameters
  • 原因:直接标识特定文章资源

3. 社交媒体应用案例

用户资料

  • 使用路径参数:/users/johndoe
  • 原因:标识特定用户资源

用户内容过滤

  • 混合使用:/users/johndoe/posts?year=2023&type=photo
  • 原因:路径参数标识资源,查询参数进行筛选

第六部分:设计决策指南

1. 选择查询参数的指标

  • 参数是可选的
  • 参数用于筛选现有资源集合
  • 参数可能有多个值
  • 参数不会改变所请求的资源基本类型
  • 需要向后兼容性(可以轻松添加新参数)

2. 选择路径参数的指标

  • 参数是必需的
  • 参数直接标识资源
  • 参数具有层次关系
  • URL需要对SEO友好
  • 构建RESTful API

3. 混合策略最佳实践

在许多情况下,同时使用两种参数类型是最佳选择:

/users/123/albums/456/photos?size=large&download=true
  • 路径参数:/users/123/albums/456/photos - 标识资源层次结构
  • 查询参数:?size=large&download=true - 控制资源的呈现和行为

第七部分:技术实现最佳实践

1. 查询参数实现建议

  1. 默认值处理

    // 不要在URL中显示默认值
    const page = parseInt(req.query.page || '1', 10);
    const limit = parseInt(req.query.limit || '20', 10);
    
  2. 参数验证

    const validSortFields = ['date', 'name', 'price'];
    const sort = validSortFields.includes(req.query.sort) 
      ? req.query.sort 
      : 'date';
    
  3. 保持URL简洁

    • 使用简短但有意义的参数名
    • 省略具有默认值的参数

2. 路径参数实现建议

  1. 使用有意义的标识符

    // 优先使用:
    app.get('/articles/:slug', ...);  // /articles/how-to-design-urls
    
    // 而不是:
    app.get('/articles/:id', ...);    // /articles/12345
    
  2. 处理参数验证

    app.get('/users/:userId', (req, res) => {
      const userId = req.params.userId;
      
      if (!/^[0-9a-f]{24}$/.test(userId)) {
        return res.status(400).send('无效的用户ID格式');
      }
      
      // 继续处理...
    });
    
  3. 考虑URL截断问题

    • 避免过长的路径参数
    • 重要资源使用短标识符

总结

URL参数设计是Web开发中的基础环节,直接影响用户体验、SEO表现和API可用性。查询参数和路径参数各有所长:

  • 查询参数灵活且可选,适合筛选、排序和状态保存,但可能导致冗长的URL。
  • 路径参数简洁且语义化,适合资源标识和层次结构,但相对固定且必需。

在实际应用中,我们常常需要结合使用这两种方式,以创建既直观又实用的URL结构。理解它们的区别和适用场景,是设计高质量Web应用的重要基础。

选择正确的URL参数传递方式,不仅关乎技术实现,更关乎产品体验和业务发展。希望本文的分析能够帮助您在下一个项目中做出更合理的URL设计决策。

svg按钮渐变边框

共用css

body {
    padding: 50px;
    background-color: black;
    color: white;
}

svg {
    --text_fill: orange;
    --svg_width: 120px;
    --svg_height: 40px;
    width: var(--svg_width);
    height: var(--svg_height);
    cursor: pointer;
    /* 创建图层 */
    will-change: transform;

    &:hover {
        --text_fill: #fed71a;
    }

    text {
        fill: var(--text_fill);
        font-size: 1rem;
        transform: translate(50%, 50%);
        text-anchor: middle;
        dominant-baseline: middle;
        stroke: yellowgreen;
        stroke-width: .5px;
        cursor: pointer;
    }

    rect {
        --stroke_width: 4px;
        width: calc(var(--svg_width) - var(--stroke_width));
        height: calc(var(--svg_height) - var(--stroke_width));
        stroke-width: var(--stroke_width);
        rx: calc(var(--svg_height)/2);
        x: calc(var(--stroke_width)/2);
        y: calc(var(--stroke_width)/2);
        fill: none;
        cursor: pointer;
    }
}

移入执行、移出暂停

<body>
    <svg style='position:absolute;left:-9999px;width:0;height:0;visibility:hidden;'>
        <defs>
            <linearGradient id='strokeColor1' x1='0%' y1='0%' x2='100%' y2='0%'>
                <stop offset='0%' stop-color="#00ccff" stop-opacity="1" />
                <stop offset='50%' stop-color="#d400d4" stop-opacity="1" />
                <stop offset='100%' stop-color="#ff00ff" stop-opacity=".7" />
            </linearGradient>
        </defs>
    </svg>

    <svg id="svg1">
        <text>渐变按钮</text>
        <rect stroke='url(#strokeColor1)' />
        <animateTransform id="ani1" href="#strokeColor1" attributeName='gradientTransform' dur='5s' type="rotate"
            form="0,.5,.5" to="360,.5,.5" repeatCount='indefinite' begin="indefinite" />
    </svg>
</body> 
<script>
    svg1.addEventListener('mouseover', function () {
        if (!this.beginMark) {
            ani1.beginElement();
            this.beginMark = true;
            return;
        }
        this.unpauseAnimations();
    })

    svg1.addEventListener('mouseleave', function () {
        this.pauseAnimations();
    })
</script>

svg1效果图

svg1.gif

移入暂停、移出执行

<body>
    <svg style='position:absolute;left:-9999px;width:0;height:0;visibility:hidden;'>
        <defs>
            <linearGradient id='strokeColor2' x1='0%' y1='0%' x2='100%' y2='0%'>
                <stop offset='0%' stop-color="#ec261b" />
                <stop offset='50%' stop-color="#ff9f43" />
                <stop offset='100%' stop-color="#ffe66d" stop-opacity="1" />
            </linearGradient>
        </defs>
    </svg>

    <svg id="svg2">
        <text>渐变按钮</text>
        <rect stroke='url(#strokeColor2)' />
        <animateTransform id="ani2" href="#strokeColor2" attributeName='gradientTransform' dur='5s' type="rotate"
            form="0,.5,.5" to="360,.5,.5" repeatCount='indefinite' begin="0s" />
    </svg>
</body>

<script>
    svg2.addEventListener('mouseover', function () {
        this.pauseAnimations();
    })
    svg2.addEventListener('mouseleave', function () {
        this.unpauseAnimations();
    })
</script>

sv2效果图

svg2.gif

总结

个人感觉svg实现渐变边框相比较css的实现来说,相对代码量更大一些,但是svg其实还有很多好玩的地方。 用svg来做渐变边框也是另外的一种思路,也许以后能够用的上。

10分钟教你用高德MCP搭建你的私人导游 🗺️

引言 🎯

曾经假期临近,但却因各种原因不想做旅行攻略?
曾经想去一个地方,但却不知道怎么安排行程?

现在,让我们一起来解决这个问题!


什么是MCP? 🤔

官方给出的解释:

The Model Context Protocol (MCP) is an open protocol that standardizes how applications provide context and tools to LLMs. Think of MCP as a plugin system for Cursor - it allows you to extend the Agent's capabilities by connecting it to various data sources and tools through standardized interfaces.

简单来说:MCP是一个能够让AI调用各种工具的协议


实现步骤 📝

1. 准备支持MCP协议的客户端 💻

想要实现这个功能,我们需要一个既支持AI又支持MCP协议的客户端(例如:Cursor、Claude、Cline)。这里我们选择 Cursor编辑器

cursor.jpg

2. 环境搭建 🛠️

由于需要调用高德提供的MCP server,我们需要安装 Node.js

⚠️ 重要提示:

  • 请尽量下载最新版本
  • 如之前已安装Node.js,建议升级至v22.14.0版本及以上

nodejs.jpg

3. 注册账号并获取Key 🔑

  1. 首先访问高德开放平台并注册登录账号

    gaode.jpg

    gaode2.jpg

  2. 进入控制台创建新应用

gaode3.jpg

  1. 添加Key
    • 点击添加Key
    • 输入Key名称
    • 选择Web应用
    • 提交

gaode4.jpg

  1. 保存生成的Key(后续需要使用)

gaode5.jpg

4. 接入高德MCP 🔌

  1. 配置MCP server
    • 在当前目录下创建 .cursor/mcp.json 文件
    • 或在全局目录下创建(不同系统目录位置不同)
    • 复制以下配置代码:
{
  "mcpServers": {
    "amap-maps": {
      "command": "npx",
      "args": ["-y", "@amap/amap-maps-mcp-server"],
      "env": {
        "AMAP_MAPS_API_KEY": "!!!这里替换成第三步最后获取到的Key!!!"
      }
    }
  }
}
  1. 在控制台执行安装命令:
npm i @amap/amap-maps-mcp-server -g

配置成功后,Cursor Setting中MCP项应显示如下:

amap.jpg

💡 小提示:如果遇到问题,重启可以解决80%的问题


开始使用 🚀

让我们以清明假期杭州3天2晚旅行攻略为例:

⚠️ 注意:需要使用Agent模式,而不是Ask模式

amap2.jpg


结语 📌

虽然AI能给出不错的建议,但推荐西湖醋鱼这种事,还是要靠人类的味蕾来判断啊!😉

JS拖动的原理

在 JavaScript 中实现元素的拖动效果,核心原理是通过监听鼠标事件(或触摸事件)来计算元素的位置变化。以下是详细的实现原理和步骤:


1. 核心事件

拖动需要处理三个关键事件:

  • mousedown(按下鼠标):标记拖动开始,记录初始位置。
  • mousemove(移动鼠标):实时计算元素新位置并更新。
  • mouseup(松开鼠标):结束拖动,移除事件监听。

如果是移动端,对应的事件为 touchstarttouchmovetouchend


2. 实现步骤

2.1 绑定 mousedown 事件

当用户在目标元素上按下鼠标时,记录:

  • 鼠标的初始位置clientX, clientY)。
  • 元素的初始位置offsetLeft, offsetTop)。
  • 鼠标相对于元素左上角的偏移量(用于保持拖动时的相对位置)。
element.addEventListener('mousedown', function(e) {
  // 1. 记录初始数据
  const startX = e.clientX;
  const startY = e.clientY;
  const elemLeft = element.offsetLeft;
  const elemTop = element.offsetTop;

  // 2. 计算鼠标在元素内的偏移量
  const offsetX = startX - elemLeft;
  const offsetY = startY - elemTop;

  // 3. 绑定 mousemove 和 mouseup 事件
  document.addEventListener('mousemove', onMouseMove);
  document.addEventListener('mouseup', onMouseUp);

  function onMouseMove(e) {
    // 计算新位置
    const newX = e.clientX - offsetX;
    const newY = e.clientY - offsetY;
    
    // 更新元素位置
    element.style.left = newX + 'px';
    element.style.top = newY + 'px';
  }

  function onMouseUp() {
    // 移除事件监听
    document.removeEventListener('mousemove', onMouseMove);
    document.removeEventListener('mouseup', onMouseUp);
  }
});

2.2 关键细节

  • 事件委托到 document
    mousemovemouseup 绑定到 document,而非元素本身。这样即使鼠标快速移动超出元素区域,仍能正常触发事件。

  • 性能优化
    避免在 mousemove 中频繁触发重排(如读取 offsetLeft),提前缓存初始值。

  • 边界限制(可选)
    可添加逻辑限制元素在容器内移动:

    const maxX = container.offsetWidth - element.offsetWidth;
    const maxY = container.offsetHeight - element.offsetHeight;
    newX = Math.max(0, Math.min(newX, maxX));
    newY = Math.max(0, Math.min(newY, maxY));
    

2.3 处理 CSS 定位

  • 元素必须设置为 position: absoluteposition: fixed,才能通过 lefttop 修改位置。
  • 使用 transform: translate() 实现位置变化(性能更优):
    element.style.transform = `translate(${newX}px, ${newY}px)`;
    

3. 完整代码示例

<div id="draggable" style="position: absolute; left: 0; top: 0;">拖动我</div>

<script>
  const element = document.getElementById('draggable');

  element.addEventListener('mousedown', startDrag);

  function startDrag(e) {
    e.preventDefault();
    
    const startX = e.clientX;
    const startY = e.clientY;
    const elemX = element.offsetLeft;
    const elemY = element.offsetTop;
    const offsetX = startX - elemX;
    const offsetY = startY - elemY;

    document.addEventListener('mousemove', onDrag);
    document.addEventListener('mouseup', stopDrag);

    function onDrag(e) {
      const newX = e.clientX - offsetX;
      const newY = e.clientY - offsetY;
      element.style.left = newX + 'px';
      element.style.top = newY + 'px';
    }

    function stopDrag() {
      document.removeEventListener('mousemove', onDrag);
      document.removeEventListener('mouseup', stopDrag);
    }
  }
</script>

4. 高级优化

  • 防抖(Debounce):减少 mousemove 事件的触发频率。
  • 请求动画帧(RAF):使用 requestAnimationFrame 优化动画流畅度。
  • 触摸事件支持:通过 touchstart/touchmove 兼容移动端。
  • 拖拽反馈:添加半透明效果或占位符提升用户体验。

5. 原生拖拽 API 对比

HTML5 提供了原生拖放 API(draggable 属性 + dragstart/dragover 事件),但:

  • 优点:支持跨元素拖放、文件拖拽上传。
  • 缺点:定制性较差,默认会显示半透明图像。

总结

通过监听鼠标事件、计算偏移量并更新元素位置,可以灵活实现自定义拖拽效果。相比原生 API,手动控制更适用于需要高度定制的场景(如游戏、复杂 UI 组件)。

Threejs绘制小兩伞快拿去送给你的女神

大家好!我是 [数擎 AI],一位热爱探索新技术的前端开发者,在这里分享前端和 Web3D、AI 技术的干货与实战经验。如果你对技术有热情,欢迎关注我的文章,我们一起成长、进步! 开发领域:前端开发 | AI 应用 | Web3D | 元宇宙
技术栈:JavaScript、React、ThreeJs、WebGL、Go
经验经验:6 年+ 前端开发经验,专注于图形渲染和 AI 技术
开源项目AI 简历元宇宙数字孪生

chrome-capture-2025-4-2.gif

1. SDF 函数(Signed Distance Functions)

SDF 是一种通过数学公式定义形状的方式,常用于计算距离场。我们使用了几个 SDF 函数来构建图形:

  • sdfCircle: 用于绘制圆形。
  • sdfEllipse: 用于绘制椭圆形。
  • sdfLine: 用于绘制线段。

每个 SDF 函数返回一个值,表示当前像素到形状的距离。如果这个距离小于某个阈值,则表示像素在形状内部。

float sdfCircle(vec2 center, float radius, vec2 coord) {
  vec2 offset = coord - center;
  return sqrt((offset.x * offset.x) + (offset.y * offset.y)) - radius;
}

2. 布尔操作函数

SDF 可以通过布尔运算进行组合,例如求并集、差集和交集。我们在代码中使用了以下几种操作:

  • sdfUnion: 返回两个形状的并集。
  • sdfDifference: 返回两个形状的差集。
  • sdfIntersection: 返回两个形状的交集。
float sdfUnion(float a, float b) { return min(a, b); }
float sdfDifference(float a, float b) { return max(a, -b); }
float sdfIntersection(float a, float b) { return max(a, b); }

这些运算让我们能够通过数学方式灵活地合成复杂的图形。

3. 渲染函数

render 函数负责将计算出的形状绘制到屏幕上。它通过 smoothstep 函数实现抗锯齿效果,并根据距离来调整颜色的透明度。

vec4 render(float d, vec3 color, float stroke) {
float anti = fwidth(d) * 1.0;
vec4 strokeLayer = vec4(vec3(0.05), 1.0 - smoothstep(-anti, anti, d - stroke));
vec4 colorLayer = vec4(color, 1.0 - smoothstep(-anti, anti, d));
return stroke < 0.000001 ? colorLayer : vec4(mix(strokeLayer.rgb, colorLayer.rgb, colorLayer.a), strokeLayer.a);
}

这个函数通过逐层混合不同颜色和透明度来呈现复杂的视觉效果。

4.动态条纹

我们还使用了正弦函数 sin() 来生成动态条纹效果。sin(uv.x * 40.0) 使得图案随时间变化,创造出条纹的动感效果。

vec2 sinuv = vec2(uv.x, (sin(uv.x _ 40.0) _ 0.02 + 1.0) * uv.y);

通过改变 time 参数,这些条纹会在场景中随着时间不断变化,增强动画效果的表现力。

5. 背景与图层混合

为了让图形与背景更好地融合,我们使用了图层混合和背景颜色的处理。每个图层根据其透明度逐渐与背景颜色混合,最终得出渲染结果。

vec3 bcol = vec3(1.0, 0.8, 0.7 - 0.07 _ p.y) _ (1.0 - 0.25 * length(p));
fragColor.rgb = mix(fragColor.rgb, layer0.rgb, layer0.a);
fragColor.rgb = mix(fragColor.rgb, layer1.rgb, layer1.a);
fragColor.rgb = mix(fragColor.rgb, layer2.rgb, layer2.a);

6. Gamma 校正

为了调整最终的颜色输出并确保其符合人眼的感知,采用了 Gamma 校正。通过将颜色值提升到 1.0 / 2.2 的幂次方,我们可以得到更为自然的视觉效果。

fragColor.rgb = pow(fragColor.rgb, vec3(1.0 / 2.2));

7. 完整代码

import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

// 1. 初始化Three.js基础场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000,
);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// 顶点着色器
const vertexShader = `
varying vec2 vUv;
void main() {
  vUv = uv;
  gl_Position = projectionMatrix * modelMatrix * viewMatrix * vec4(position, 1.0);
}
`;

// 片元着色器
const fragmentShader = `
uniform vec2 resolution;
uniform float time;
varying vec2 vUv;

float sdfCircle(vec2 center, float radius, vec2 coord) {
  vec2 offset = coord - center;
  return sqrt((offset.x * offset.x) + (offset.y * offset.y)) - radius;
}

float sdfEllipse(vec2 center, float a, float b, vec2 coord) {
  float a2 = a * a;
  float b2 = b * b;
  return (b2 * (coord.x - center.x) * (coord.x - center.x) +
         a2 * (coord.y - center.y) * (coord.y - center.y) - a2 * b2)/(a2 * b2);
}

float sdfLine(vec2 p0, vec2 p1, float width, vec2 coord) {
  vec2 dir0 = p1 - p0;
  vec2 dir1 = coord - p0;
  float h = clamp(dot(dir0, dir1)/dot(dir0, dir0), 0.0, 1.0);
  return (length(dir1 - dir0 * h) - width * 0.5);
}

float sdfUnion(float a, float b) { return min(a, b); }
float sdfDifference(float a, float b) { return max(a, -b); }
float sdfIntersection(float a, float b) { return max(a, b); }

vec4 render(float d, vec3 color, float stroke) {
  float anti = fwidth(d) * 1.0;
  vec4 strokeLayer = vec4(vec3(0.05), 1.0-smoothstep(-anti, anti, d - stroke));
  vec4 colorLayer = vec4(color, 1.0-smoothstep(-anti, anti, d));
  return stroke < 0.000001 ? colorLayer : 
    vec4(mix(strokeLayer.rgb, colorLayer.rgb, colorLayer.a), strokeLayer.a);
}

void main() {
  float size = min(resolution.x, resolution.y);
  float pixSize = 1.0 / size;
  vec2 uv = vUv;
  float stroke = pixSize * 1.5;
  
  // 适配宽高比
  float aspect = resolution.y / resolution.x;
  vec2 center = vec2(0.5, 0.5 * aspect);

  // 主要形状
  float a = sdfEllipse(vec2(0.5, center.y*2.0-0.34), 0.25, 0.25, uv);
  float b = sdfEllipse(vec2(0.5, center.y*2.0+0.03), 0.8, 0.35, uv);
  b = sdfIntersection(a, b);
  vec4 layer1 = render(b, vec3(0.32, 0.56, 0.53), fwidth(b) * 2.0);

  // 动态条纹
  vec4 layer2 = layer1;
  vec2 sinuv = vec2(uv.x, (sin(uv.x*40.0)*0.02 + 1.0)*uv.y);
  for (float i = 0.0; i < 10.0; i++) {
    float t = mod(time + 0.3 * i, 3.0) * 0.2;
    float r0 = (t - 0.15)/0.2 * 0.9 + 0.1;
    float r1 = (t - 0.15)/0.2 * 0.1 + 0.9;
    float r2 = (t - 0.15)/0.2 * 0.15 + 0.85;
    
    float e = sdfEllipse(vec2(0.5, center.y*2.0+0.37-t*r2), 0.7*r0, 0.35*r1, sinuv);
    float f = sdfEllipse(vec2(0.5, center.y*2.0+0.41-t), 0.7*r0, 0.35*r1, sinuv);
    f = sdfDifference(e, f);
    f = sdfIntersection(f, b);
    vec4 layer = render(f, vec3(1.0, 0.81, 0.27), 0.0);
    layer2 = mix(layer2, layer, layer.a);
  }

  // 手柄绘制
  float bottom = 0.08;
  float handleWidth = 0.01;
  float handleRadius = 0.04;
  float d = sdfCircle(vec2(0.5-handleRadius+0.5*handleWidth, bottom), handleRadius, uv);
  float c = sdfCircle(vec2(0.5-handleRadius+0.5*handleWidth, bottom), handleRadius-handleWidth, uv);
  d = sdfDifference(d, c);
  c = uv.y - bottom;
  d = sdfIntersection(d, c);
  c = sdfLine(vec2(0.5, center.y*2.0-0.05), vec2(0.5, bottom), handleWidth, uv);
  d = sdfUnion(d, c);
  c = sdfCircle(vec2(0.5, center.y*2.0-0.05), 0.01, uv);
  d = sdfUnion(c, d);
  c = sdfCircle(vec2(0.5-handleRadius*2.0+handleWidth, bottom), handleWidth*0.5, uv);
  d = sdfUnion(c, d);
  vec4 layer0 = render(d, vec3(0.404, 0.298, 0.278), stroke);

  // 背景混合
  vec2 p = (2.0*gl_FragCoord.xy-resolution.xy)/min(resolution.y, resolution.x);
  vec3 bcol = vec3(1.0,0.8,0.7-0.07*p.y)*(1.0-0.25*length(p));
  vec4 fragColor = vec4(bcol, 1.0);
  
  // 图层混合
  fragColor.rgb = mix(fragColor.rgb, layer0.rgb, layer0.a);
  fragColor.rgb = mix(fragColor.rgb, layer1.rgb, layer1.a);
  fragColor.rgb = mix(fragColor.rgb, layer2.rgb, layer2.a);
  
  // Gamma 校正
  fragColor.rgb = pow(fragColor.rgb, vec3(1.0/2.2));
  gl_FragColor = fragColor;
}
`;

// Three.js 材质创建
const material = new THREE.ShaderMaterial({
  vertexShader,
  fragmentShader,
  uniforms: {
    resolution: {
      value: new THREE.Vector2(window.innerWidth, window.innerHeight),
    },
    time: { value: 0 },
  },
});

// 3. 创建全屏平面并应用材质
const geometry = new THREE.PlaneGeometry(10, 10);
const plane = new THREE.Mesh(geometry, material);
scene.add(plane);

// 4. 相机位置和控制器
camera.position.z = 10;
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableZoom = false;

// 5. 响应窗口大小变化
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
  shaderMaterial.uniforms.iResolution.value.set(
    renderer.domElement.width,
    renderer.domElement.height,
  );
});

// 6. 动画循环
function animate() {
  requestAnimationFrame(animate);
  material.uniforms.time.value = performance.now() / 1000;
  renderer.render(scene, camera);
}
animate();

总结

利用了 SDF 技术绘制了多个形状,并通过布尔运算组合它们,进一步通过动态条纹和动画效果增加了复杂度。通过理解这些 Shader 代码,你将能更好地掌控图形渲染的细节,并应用到更复杂的 Three.js 项目中。

useDateFormat源码解析

深入解析 VueUse 的 useDateFormat:源码解读与使用示例

在前端开发中,日期格式化是一个常见的任务,无论是展示时间戳、格式化用户输入的日期,还是处理国际化时间显示,开发者都需要一个简单而灵活的工具。VueUse 是一个专为 Vue 3 设计的实用工具库,其中的 useDateFormat 提供了一种响应式的日期格式化解决方案。本文将深入分析 useDateFormat 的源码实现,并结合实际使用示例,帮助你更好地理解和应用这一工具。


useDateFormat 简介

useDateFormat 是一个基于 Vue 3 组合式 API 的 Hook,用于将日期对象或时间戳格式化为指定的字符串。它基于 JavaScript 的 Intl.DateTimeFormat API,支持灵活的格式化选项和国际化需求。基本用法如下:

import { useDateFormat } from '@vueuse/core'

const date = new Date()
const formatted = useDateFormat(date, 'YYYY-MM-DD HH:mm:ss')
console.log(formatted.value) // 输出类似 "2025-04-02 14:30:00"

在这个例子中,我们将当前日期格式化为 YYYY-MM-DD HH:mm:ss 的字符串。


源码解读

以下是 useDateFormat 的简化源码实现(基于 VueUse v10.x,具体实现请参考官方仓库):

import { ref, computed, watch, isRef } from 'vue'
import { isString, isDate } from '@vueuse/shared'

export function useDateFormat(date, format = 'YYYY-MM-DD', options = {}) {
  // 处理输入的日期
  const dateRef = isRef(date) ? date : ref(date)

  // 默认选项
  const {
    locales = 'en-US', // 默认语言环境
    ...intlOptions // 其他 Intl.DateTimeFormat 选项
  } = options

  // 格式化函数
  const formatter = (value, fmt) => {
    if (!value) return ''

    let dateValue = value
    if (isString(value)) {
      dateValue = new Date(value)
    } else if (!isDate(value)) {
      dateValue = new Date(value)
    }

    if (isNaN(dateValue.getTime())) return ''

    // 如果是自定义格式字符串
    if (fmt.includes('Y') || fmt.includes('M') || fmt.includes('D')) {
      return formatCustom(dateValue, fmt)
    }

    // 使用 Intl.DateTimeFormat 格式化
    return new Intl.DateTimeFormat(locales, intlOptions).format(dateValue)
  }

  // 自定义格式化逻辑
  const formatCustom = (date, fmt) => {
    const year = date.getFullYear()
    const month = String(date.getMonth() + 1).padStart(2, '0')
    const day = String(date.getDate()).padStart(2, '0')
    const hours = String(date.getHours()).padStart(2, '0')
    const minutes = String(date.getMinutes()).padStart(2, '0')
    const seconds = String(date.getSeconds()).padStart(2, '0')

    return fmt
      .replace('YYYY', year)
      .replace('MM', month)
      .replace('DD', day)
      .replace('HH', hours)
      .replace('mm', minutes)
      .replace('ss', seconds)
  }

  // 响应式格式化结果
  const output = computed(() => formatter(dateRef.value, format))

  return output
}

源码关键点解析

  1. 输入处理

    • date 参数可以是 Date 对象、时间戳、字符串或响应式 ref
    • 使用 isRef 判断输入是否为 ref,如果是则直接使用,否则包装为 ref
  2. 格式化逻辑

    • 如果 format 是自定义字符串(如 YYYY-MM-DD),调用 formatCustom 进行替换。
    • 如果 format 未包含自定义占位符,则使用 Intl.DateTimeFormat 进行格式化,支持国际化。
  3. 自定义格式化

    • formatCustom 通过 getFullYeargetMonth 等方法提取日期组件,并用 padStart 补齐两位数。
    • 支持的占位符包括 YYYY(年)、MM(月)、DD(日)、HH(小时)、mm(分钟)、ss(秒)。
  4. 响应式支持

    • 使用 computed 返回格式化结果,确保当 dateRefformat 变化时,输出自动更新。
  5. 国际化支持

    • 通过 localesintlOptions 参数,可以利用 Intl.DateTimeFormat 实现不同语言环境下的格式化。

使用示例

下面是一个实际的 Vue 组件示例,展示如何使用 useDateFormat 实现动态日期格式化:

<template>
  <div>
    <p>当前时间:{{ formattedDate }}</p>
    <input v-model="format" placeholder="输入格式,如 YYYY-MM-DD HH:mm:ss" />
    <button @click="updateDate">刷新时间</button>
  </div>
</template>

<script setup>
  import { ref } from 'vue'
  import { useDateFormat } from '@vueuse/core'

  const date = ref(new Date())
  const format = ref('YYYY-MM-DD HH:mm:ss')

  const formattedDate = useDateFormat(date, format)

  const updateDate = () => {
    date.value = new Date() // 更新日期,触发重新格式化
  }
</script>

<style scoped>
  input {
    margin: 10px;
    padding: 5px;
  }
  button {
    padding: 5px 10px;
  }
</style>

示例说明

  1. 功能

    • 显示当前时间的格式化结果。
    • 用户可以输入自定义格式(如 YYYY-MM-DDHH:mm:ss),实时更新显示。
    • 点击“刷新时间”按钮更新日期。
  2. 效果

    • dateformat 变化时,formattedDate 会自动重新计算并更新。
    • 输入 YYYY-MM-DD 会显示类似 2025-04-02,输入 HH:mm:ss 会显示类似 14:30:45

扩展与优化

  1. 国际化格式化: 使用 locales 参数支持不同语言环境:

    const formatted = useDateFormat(new Date(), 'YYYY-MM-DD', { locales: 'zh-CN' })
    console.log(formatted.value) // 输出类似 "2025-04-02"
    
  2. 使用 Intl 标准选项: 如果不需要自定义格式,可以直接使用 Intl.DateTimeFormat 的选项:

    const formatted = useDateFormat(new Date(), '', {
      locales: 'en-US',
      dateStyle: 'full',
    })
    console.log(formatted.value) // 输出类似 "Wednesday, April 2, 2025"
    
  3. 动态切换格式: 将 format 定义为响应式变量,动态切换格式:

    const format = ref('YYYY-MM-DD')
    const formatted = useDateFormat(new Date(), format)
    format.value = 'HH:mm:ss' // 切换为时间格式
    
  4. 错误处理: 如果传入无效日期,useDateFormat 会返回空字符串:

    const formatted = useDateFormat('invalid-date', 'YYYY-MM-DD')
    console.log(formatted.value) // 输出 ""
    

与其他工具的对比

相比传统的日期格式化库(如 moment.jsday.js),useDateFormat 有以下优势:

  • 轻量:无需引入额外的依赖,直接基于原生 API。
  • 响应式:与 Vue 的响应式系统无缝集成。
  • 灵活性:支持自定义格式和国际化。

但它也有局限性,例如不支持过于复杂的格式化规则(如 moment.jsfromNow),适合轻量级场景。


总结

useDateFormat 是 VueUse 中一个简单而实用的工具,通过结合自定义格式化和 Intl.DateTimeFormat,它为开发者提供了灵活的日期格式化能力。本文的源码分析和示例展示了它的核心原理和应用场景,希望能帮助你在项目中更高效地处理日期相关需求。

Sequelize模型初探

写在前面


大家好,我是一溪风月🥸,一名前端工程师,专注于前端,Node,Golang的知识分享,在最近这段时间在学习Node服务端开发的知识,学习服务端必然少不了数据库,少不了SQL语言,但是其实现代服务端开发中几乎不直接编写语句操作SQL了,大家都会选择一个叫做ORM的技术来实现原本庞杂的任务,结合Express和Koa最推荐的ORM库就是Sequlize了,今天我们就来开启这个库的学习,学习资料为官方文档,有兴趣可以直接去查看官方文档,当然这篇文章会尽量将我在学习中的疑惑讲清楚,弄明白,可能我的疑惑也是你的疑惑,好了,那让我们开始吧!

一.配置学习环境


首先我们来配置一下Sequlize所需要的环境,相信你已经安装了Node,我们直接跳过这一步来使用npm进行初始化

npm init -y

然后我们进行Sequelize的安装

npm install --save sequelize

这里的数据库我们选择MySQL,我们首选在数据库管理工具中新建一个数据库,我是用的是Navicat。

二.连接数据库


首先我们需要先在项目中新建一个文件用来引入Sequelize,进行数据库的连接,官方推荐了三种数据库的连接方式

const { Sequelize } = require('sequelize');

// 方法 1: 传递一个连接 URI
const sequelize = new Sequelize('sqlite::memory:') // Sqlite 示例
const sequelize = new Sequelize('postgres://user:pass@example.com:5432/dbname') // Postgres 示例

// 方法 2: 分别传递参数 (sqlite)
const sequelize = new Sequelize({
  dialect: 'sqlite',
  storage: 'path/to/database.sqlite'
});

// 方法 3: 分别传递参数 (其它数据库)
const sequelize = new Sequelize('database', 'username', 'password', {
  host: 'localhost',
  dialect: /* one of 'mysql' | 'postgres' | 'sqlite' | 'mariadb' | 'mssql' | 'db2' | 'snowflake' | 'oracle' */
});

我们选择最后一种,因为我觉的最后一种比较理解,并且我们使用的是MySQL,第二种是专门给Node内置的数据SQLite准备的。

const Sequelize = require("sequelize")
const sequelize = new Sequelize('sequlize_test', 'root', 'root', {
  host: 'localhost',
  dialect: 'mysql'
})

然后我们编写一个函数进行连接测试,看是否和我们本地的MySQL连接成功了,完整的代码就是这样。

const Sequelize = require("sequelize")
const sequelize = new Sequelize('sequelize_test', 'root', 'root', {
  host: 'localhost',
  dialect: 'mysql'
})

// 测试连接
async function testConnecton () {
  try {
    await sequelize.authenticate()
    console.log('Connection has been established successfully.')
  } catch (error) {
    console.error('Unable to connect to the database:', error)
  }
}

testConnecton();

执行一下node index.js 我们会看到了数据库成功连接了

其次Sequelize还提供了关闭连接的方法,如果我们执行一次短期脚本后就不用数据库了可以使用如下的方法继续宁关闭数据库的连接,但是我们大多数服务都是长期连接的就不需要这个方法

sequelize.close()

💡官方提示:一旦 sequelize.close() 被调用, 就不可能打开新的连接. 你将需要创建一个新的 Sequelize 实例以再次访问你的数据库。

三.记录日志


日志顾名思义就是在某个时间干了某件事,但是为什么要记录日志? 当然是为了在某些时间服务出了问题进行数据的排查,在Sequelize中我们可以这样来启用日志

 // 选择一种日志记录参数
  logging: console.log,                  // 默认值,显示日志函数调用的第一个参数
  logging: (...msg) => console.log(msg), // 显示所有日志函数调用参数
  logging: false,                        // 禁用日志记录
  logging: msg => logger.debug(msg),     // 使用自定义记录器(例如Winston 或 Bunyan),显示第一个参数
  logging: logger.debug.bind(logger)     // 使用自定义记录器的另一种方法,显示所有消息

以上这几种写法根据需求任选其一,在我们的代码中目前并没有使用日志库来记录,我们就可以这样写

const sequelize = new Sequelize('sequelize_test', 'root', 'root', {
  host: 'localhost',
  dialect: 'mysql',
  logging:console.log,
})

三.新数据库和现有数据库如何使用?


想要理解这个问题就要先了解一下什么是ORM,ORM 即对象关系映射(Object Relational Mapping),是一种编程技术,用于在面向对象编程语言(如 Python、Java、JavaScript 等)和关系型数据库(如 MySQL、PostgreSQL、Oracle 等)之间建立起一座桥梁,使得开发者能够以面向对象的方式来操作数据库,简单来说就是这样。

以Sequelize为例,本质上就是和数据库之间构建的一座桥梁,从具体的代码中来看就是类/方法对应具体数据库中的表,比如图中的User对应的就是数据库中User表的这种映射关系,而类的实例化对象对应的就是表中的一行数据。当我们没有数据库的时候我们需要新建一个数据库,我们就可以通过数据库工具比如Navicat来新建数据库,我们可以直接通过Sequelize来定义模型去创建对应的表,如果数据库中有数据Sequelize依然能够使用。

四.模型基础


模型是ORM中一个很重要的概念,理解了模型就理解了ORM,就像Express中的中间件一样,但是和上述的内容ORM中的User去对应数据库中的User表,但是在Sequelize中对应关系通常是这样的User->Users也就是说当我们数据库是空的,我们使用Sequelize定义了模型,它自动同步到数据库创建的表就是对应的复数形式,甚至有时候还会有这样的对应关系person->people是通过官方的一种规范来做转换的,但是我感觉挺反人类的,建议在定义模型的时候使用下面这种方式关掉。

sequelize.define('User', {
  // ... (属性)
}, {
  freezeTableName: true
});

当然也可以全局进行关闭,完整的代码就是这样。

const Sequelize = require("sequelize")
const sequelize = new Sequelize('sequelize_test', 'root', 'root', {
  host: 'localhost',
  dialect: 'mysql',
  logging: console.log,
  define: {
    freezeTableName: true
  }
})

当然我们也可以直接提供表名就相当于给表起了别名,其实这样更加方便。

sequelize.define('User', {
  // ... (属性)
}, {
  tableName: 'Employees'
});

那么现在我们来新建一个模型,我们直接使用更加简洁的define 方法来进行定义。

const User = sequelize.define('User', {
  firstName: {
    type: DataTypes.STRING,
    allowNull: false,
  },
  lastName: {
    type: DataTypes.STRINGdefaultValue:"志鹏"
  }

})

还有其他的模型创建方式可以去查看中文文档 模型基础 之后我们需要进行模型的同步才能在数据库中建立真实的表,对模型的同步有如下的几种:

  • User.sync() - 如果表不存在,则创建该表(如果已经存在,则不执行任何操作)
  • User.sync({ force: true }) - 将创建表,如果表已经存在,则将其首先删除
  • User.sync({ alter: true }) - 这将检查数据库中表的当前状态(它具有哪些列,它们的数据类型等),然后在表中进行必要的更改以使其与模型匹配.

我们因为是直接是通过Sequelize来创建的,所以我们使用第一种来进行同步。

 // 进行模型同步
async function modelSync () {
  await User.sync()
}
modelSync()

然后我们来执行一下代码,我们会发现上面的内容打印也生效了

通过数据库工具中查看也新增了User表。

我们也可以直接全局进行配置,来一次性同步全部模型

await sequelize.sync({ force: true });
console.log("所有模型均已成功同步.");

但是你可能会发现一个问题,为什么我们没有定义createdAtupdatedAt但是在数据库表中会自动增加创建时间和更新时间?这个是Sequelize默认的配置,我们如果不想要这两个字段也可以按照如下配置。

 // 不要忘记启用时间戳!
  timestamps: true,
  // 不想要 createdAt
  createdAt: false,
  // 想要 updatedAt 但是希望名称叫做 updateTimestamp
  updatedAt: 'updateTimestamp'

五.数据库安全检查


除了上面的同步表数据,还可以进行数据库表的删除,也可以删除全部表

// 删除单个表
await User.drop();
console.log("用户表已删除!");
// 删除全部表
await sequelize.drop();
console.log("所有表已删除!");

因为前面我们可以通过async同时设置设置force属性来强制同步表,并且也可以进行删除表,这在实际开发中是非常危险的,所以为了防止误删等情况我们往往需要进行数据库检查我们可以用match来进行正则匹配某个具体的数据库名称,当匹配成功就执行删除或者,同步,用法如下

// 仅当数据库名称以 '_test' 结尾时,它才会运行.sync()
sequelize.sync({ force: true, match: /_test$/ });

六.数据类型


我们在上述内容中定义了具体模型的字段,并且给不同的模型定义了不同的字段以及具体的类型,类型是通过导入如下的方式进行使用

const { DataTypes } = require("sequelize"); // 导入内置数据类型

字符串

DataTypes.STRING             // VARCHAR(255)
DataTypes.STRING(1234)       // VARCHAR(1234)
DataTypes.STRING.BINARY      // VARCHAR BINARY
DataTypes.TEXT               // TEXT
DataTypes.TEXT('tiny')       // TINYTEXT
DataTypes.CITEXT             // CITEXT          仅 PostgreSQL 和 SQLite.
DataTypes.TSVECTOR           // TSVECTOR        仅 PostgreSQL.

开发中对于姓名等固定的字符串我们可以使用STRING对于更多的文字我们可以使用TEXT

布尔

DataTypes.BOOLEAN            // TINYINT(1)

数字

DataTypes.INTEGER            // INTEGER
DataTypes.BIGINT             // BIGINT
DataTypes.BIGINT(11)         // BIGINT(11)

DataTypes.FLOAT              // FLOAT
DataTypes.FLOAT(11)          // FLOAT(11)
DataTypes.FLOAT(11, 10)      // FLOAT(11,10)

DataTypes.REAL               // REAL            仅 PostgreSQL.
DataTypes.REAL(11)           // REAL(11)        仅 PostgreSQL.
DataTypes.REAL(11, 12)       // REAL(11,12)     仅 PostgreSQL.

DataTypes.DOUBLE             // DOUBLE
DataTypes.DOUBLE(11)         // DOUBLE(11)
DataTypes.DOUBLE(11, 10)     // DOUBLE(11,10)

DataTypes.DECIMAL            // DECIMAL
DataTypes.DECIMAL(10, 2)     // DECIMAL(10,2)

无符号和零填充整数 - 仅限于MySQL/MariaDB,在 MySQL 和 MariaDB 中,可以将数据类型INTEGER, BIGINT, FLOATDOUBLE 设置为无符号或零填充(或两者),如下所示:

DataTypes.INTEGER.UNSIGNED
DataTypes.INTEGER.ZEROFILL
DataTypes.INTEGER.UNSIGNED.ZEROFILL
// 你还可以指定大小,即INTEGER(10)而不是简单的INTEGER
// 同样适用于 BIGINT, FLOAT 和 DOUBLE

日期

DataTypes.DATE       // DATETIME 适用于 mysql / sqlite, 带时区的TIMESTAMP 适用于 postgres
DataTypes.DATE(6)    // DATETIME(6) 适用于 mysql 5.6.4+. 支持6位精度的小数秒
DataTypes.DATEONLY   // 不带时间的 DATE

UUID

对于 UUID,使用 DataTypes.UUID. 对于 PostgreSQL 和 SQLite,它会是 UUID 数据类型;对于 MySQL,它则变成CHAR(36). Sequelize 可以自动为这些字段生成 UUID,只需使用 DataTypes.UUIDV1DataTypes.UUIDV4 作为默认值即可:

{
  type: DataTypes.UUID,
  defaultValue: DataTypes.UUIDV4 // 或 DataTypes.UUIDV1
}

常见列参数

 // 实例化将自动将 flag 设置为 true (如果未设置)
  flag: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true },

  // 日期的默认值 => 当前时间
  myDate: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },

  // 将 allowNull 设置为 false 将为该列添加 NOT NULL,
  // 这意味着如果该列为 null,则在执行查询时将从数据库引发错误.
  // 如果要在查询数据库之前检查值是否不为 null,请查看下面的验证部分.
  title: { type: DataTypes.STRING, allowNull: false },

  // 创建两个具有相同值的对象将引发错误.
  // unique 属性可以是布尔值或字符串.
  // 如果为多个列提供相同的字符串,则它们将形成一个复合唯一键.
  uniqueOne: { type: DataTypes.STRING,  unique: 'compositeIndex' },
  uniqueTwo: { type: DataTypes.INTEGER, unique: 'compositeIndex' },

  // unique 属性是创建唯一约束的简写.
  someUnique: { type: DataTypes.STRING, unique: true },

  // 继续阅读有关主键的更多信息
  identifier: { type: DataTypes.STRING, primaryKey: true },

  // autoIncrement 可用于创建 auto_incrementing 整数列
  incrementMe: { type: DataTypes.INTEGER, autoIncrement: true },

  // 你可以通过 'field' 属性指定自定义列名称:
  fieldWithUnderscores: { type: DataTypes.STRING, field: 'field_with_underscores' },

  // 可以创建外键:
  bar_id: {
    type: DataTypes.INTEGER,

    references: {
      // 这是对另一个模型的参考
      model: Bar,

      // 这是引用模型的列名
      key: 'id',

      // 使用 PostgreSQL,可以通过 Deferrable 类型声明何时检查外键约束.
      deferrable: Deferrable.INITIALLY_IMMEDIATE
      // 参数:
      // - `Deferrable.INITIALLY_IMMEDIATE` - 立即检查外键约束
      // - `Deferrable.INITIALLY_DEFERRED` - 将所有外键约束检查推迟到事务结束
      // - `Deferrable.NOT` - 完全不推迟检查(默认) - 这将不允许你动态更改事务中的规则
    }
  },

  // 注释只能添加到 MySQL,MariaDB,PostgreSQL 和 MSSQL 的列中
  commentMe: {
    type: DataTypes.INTEGER,
    comment: '这是带有注释的列'
  }
}, {
  sequelize,
  modelName: 'foo',

  // 在上面的属性中使用 `unique: true` 与在模型的参数中创建索引完全相同:
  indexes: [{ unique: true, fields: ['someUnique'] }]

七.总结与扩展


这篇文章到这里暂时就结束了,这篇文章我们认识了Sequelize这个数据库ORM,上述的内容主要来源本人通过官方文档的学习和理解,建议参考着官方文档进行学习:模型基础 在上述我们是通过define来映射表,其实我们也可以通过继承来实现模型创建,这两种方式是一样的,具体详情请参考文档,如下是本章节的的所有的代码

const { Sequelize, DataTypes } = require("sequelize");

// 配置数据库连接
const sequelize = new Sequelize('sequelize_test', 'root', 'root', {
    host: 'localhost',
    dialect: 'mysql',
    // 可根据需要调整日志级别,生产环境可设置为 false 以减少日志输出
    logging: console.log, 
    define: {
        freezeTableName: true
    }
});

// 定义 User 模型
const User = sequelize.define('User', {
    firstName: {
        type: DataTypes.STRING,
        allowNull: false,
    },
    lastName: {
        type: DataTypes.STRING
        defaultValue:"zzz"
    }
});

// 测试数据库连接
async function testConnection() {
    try {
        await sequelize.authenticate();
        console.log('Connection has been established successfully.');
        return true;
    } catch (error) {
        console.error('Unable to connect to the database:', error);
        return false;
    }
}

// 进行模型同步
async function syncModels() {
    try {
        await User.sync();
        console.log('Models synchronized successfully.');
    } catch (error) {
        console.error('Error synchronizing models:', error);
    }
}

// 主函数,按顺序执行操作
async function main() {
    const isConnected = await testConnection();
    if (isConnected) {
        await syncModels();
    }
}

// 调用主函数
main();
    

下一篇文章我们将讲解模型实例(映射到真实数据中就是具体某一行的数据)下篇文章见🚀~

使用uni-app框架 写电商商城前端h5静态网站模板项目-手机端-前端项目练习

以前用vue2 分享过一个电商商城前端静态网站项目-电脑端,需要的小伙伴还是很多的,最近又花了几天更新了一个 手机端的 电商商城h5项目,今天也分享一下实现方案。

对于以前写的 电商商城前端静态网站模板-电脑端,有兴趣的小伙伴 可以通过下方链接去考古一下:

jsonll.blog.csdn.net/article/det…

今天我们主要来分享一下 uni-app 写的 手机端的 电商商城前端h5静态网站

使用的技术

网站使用了 uni-app 框架开发,专注于 H5 移动端网页。通过 uni-app,开发者可以轻松构建响应式页面,并利用框架内置的 UI 组件快速搭建界面。该模板帮助开发者深入了解 uni-app 的使用方法,并快速实现常见的电商商城功能。

接下来说一下网站实现的内容: · 首页 、 分类 、 购物车、 个人中心页、 结算页等等

· 首页:展示热门商品、活动信息和分类导航

image.png

· 分类:用户可以搜索和筛选商品

image.png

· 购物车:用户可以查看已添加的商品并进行删除或修改数量

image.png

· 个人中心页:显示用户的个人信息、订单历史等

image.png

· 结算页:展示用户选择的商品和结算信息,支持填写地址、支付方式等

image.png

项目目录结构

image.png

首页代码:

<!-- 作者:json -->
<template>
<view class="index-container">
<!-- 搜索栏 -->
<view >
<search-bar @search="onSearch" />
</view>

<!-- 轮播图 -->
<swiper class="banner" circular :indicator-dots="true" :autoplay="true" :interval="3000" :duration="1000">
<swiper-item v-for="(item, index) in banners" :key="index">
<view class="banner-item" :style="{background: item.bg}"></view>
</swiper-item>
</swiper>

<!-- 热卖专区 -->
<view class="hot-section">
<view class="section-title">热卖专区</view>
<view class="hot-list">
<product-item 
v-for="(item, index) in hotProducts" 
:key="index" 
:product="{
...item,
sales: Math.floor(Math.random() * 1000) + 100
}" 
@click="goToDetail" 
@add-to-cart="addToCart"
/>
</view>
</view>

<!-- 猜你喜欢 -->
<view class="recommend-section">
<view class="section-title">猜你喜欢</view>
<view class="recommend-list">
<product-item 
v-for="(item, index) in recommendProducts" 
:key="index" 
:product="{
...item,
sales: Math.floor(Math.random() * 500) + 50
}" 
@click="goToDetail" 
@add-to-cart="addToCart"
/>
</view>
</view>
</view>
</template>

<script>
import SearchBar from '@/components/SearchBar.vue';
import ProductItem from '@/components/ProductItem.vue';
import cartService from '@/utils/cartService.js';
export default {
components: {
SearchBar,
ProductItem
},
data() {
return {
banners: [{
bg: 'linear-gradient(45deg, #ff9a9e 0%, #fad0c4 99%, #fad0c4 100%)'
}, {
bg: 'linear-gradient(120deg, #a1c4fd 0%, #c2e9fb 100%)'
}, {
bg: 'linear-gradient(120deg, #d4fc79 0%, #96e6a1 100%)'
}],
hotProducts: [{
id: 1,
name: '热卖商品1',
price: '99.00',
bg: 'linear-gradient(45deg, #ff9a9e 0%, #fad0c4 99%, #fad0c4 100%)'
}, {
id: 2,
name: '热卖商品2',
price: '199.00',
bg: 'linear-gradient(120deg, #a1c4fd 0%, #c2e9fb 100%)'
}],
recommendProducts: [{
id: 3,
name: '推荐商品1',
price: '89.00',
bg: 'linear-gradient(120deg, #d4fc79 0%, #96e6a1 100%)'
}, {
id: 4,
name: '推荐商品2',
price: '129.00',
bg: 'linear-gradient(45deg, #ff9a9e 0%, #fad0c4 99%, #fad0c4 100%)'
}]
}
},
methods: {
onSearch(e) {
console.log('搜索关键词:', e)
},
goToDetail(item) {
// 跳转到商品详情页,传递商品ID
uni.navigateTo({
url: `/pages/detail/index?id=${item.id || 1}`
});
},
addToCart(item) {
// 添加商品到购物车
console.log('添加到购物车:', item);

// 为商品添加必要的SKU信息
const product = {
...item,
skus: [{
name: item.name,
price: item.price
}]
};

// 调用购物车服务添加商品
const result = cartService.addToCart(product, 0, 1);

if (result) {
uni.showToast({
title: '已添加到购物车',
icon: 'success'
});
} else {
uni.showToast({
title: '添加失败',
icon: 'none'
});
}
}
}
}
</script>

<style lang="scss" scoped>
.index-container {
padding-bottom: 1.25rem;
}

.search-box {
padding: 0.5rem 1.25rem;
}

.banner {
height: 12rem;
margin: 0 1.25rem;
border-radius: 0.75rem;
overflow: hidden;

.banner-item {
height: 100%;
width: 100%;
}
}

.section-title {
font-size: 1rem;
font-weight: bold;
padding: 1.875rem 1.25rem 1.25rem;
}

.hot-list {
padding: 0 1.25rem;
display: flex;
gap: 1.25rem;

.hot-item {
flex: 1;
background: #fff;
border-radius: 0.75rem;
overflow: hidden;

.product-img {
height: 8rem;
width: 100%;
}

.product-info {
padding: 1rem;

.product-name {
font-size: 0.875rem;
color: #333;
display: block;
}

.product-price {
font-size: 1rem;
color: #409EFF;
font-weight: bold;
margin-top: 0.625rem;
display: block;
}
}
}
}

.recommend-list {
padding: 0 1.25rem;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.25rem;

.recommend-item {
background: #fff;
border-radius: 0.75rem;
overflow: hidden;

.product-img {
height: 8rem;
width: 100%;
}

.product-info {
padding: 1rem;

.product-name {
font-size: 0.875rem;
color: #333;
display: block;
}

.product-price {
font-size: 1rem;
color: #409EFF;
font-weight: bold;
margin-top: 0.625rem;
display: block;
}
}
}
}
</style>

这个手机端商城项目 代码还是挺多的。这里就不一一分享了。大概实现的功能 和 电脑端的 vue2写的那个 差不多~

后续 如果有小伙伴需要 ,慢慢我把这套前端电商商城项目 再结合后端 写成一个完整的项目。

好了,这个商商城前端h5静态网站模板项目 今天就分享到这里、

完整的代码,有兴趣的小伙伴可以通过下方获取:

wwwoop.com/home/Index/…

❌