前端到底该如何安全的实现“记住密码”?
在 web
应用里,“记住密码”这个小小的功能,可是咱用户的贴心小棉袄啊,用起来超级方便!但话说回来,咱们得怎样做才能既让用户享受这便利,又能牢牢护住他们的数据安全呢?这可得好好琢磨一番哦!接下来,咱们就来聊聊,有哪些靠谱的方法能实现“记住密码”这个功能,而且安全性也是杠杠的!
1. 使用 localStorage
localStorage
是一种持久化存储方式,数据在浏览器关闭后仍然存在。适用于需要长期保存的数据。
示例代码
// 生成对称密钥
async function generateSymmetricKey() {
const key = await crypto.subtle.generateKey(
{
name: "AES-GCM",
length: 256,
},
true,
["encrypt", "decrypt"]
);
return key;
}
// 加密数据
async function encryptData(data, key) {
const iv = crypto.getRandomValues(new Uint8Array(12));
const encryptedData = await crypto.subtle.encrypt(
{
name: "AES-GCM",
iv: iv,
},
key,
new TextEncoder().encode(data)
);
return { iv, encryptedData };
}
// 解密数据
async function decryptData(encryptedData, key, iv) {
const decryptedData = await crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: iv,
},
key,
encryptedData
);
return new TextDecoder().decode(decryptedData);
}
// 保存用户信息
async function saveUserInfo(username, password) {
const key = await generateSymmetricKey();
const { iv, encryptedData } = await encryptData(password, key);
localStorage.setItem('username', username);
localStorage.setItem('password', JSON.stringify({ iv, encryptedData }));
// 密钥可以存储在更安全的地方,如服务器端
}
// 获取用户信息
async function getUserInfo() {
const username = localStorage.getItem('username');
const { iv, encryptedData } = JSON.parse(localStorage.getItem('password'));
const key = await generateSymmetricKey(); // 这里应使用同一个密钥
const password = await decryptData(encryptedData, key, iv);
return { username, password };
}
// 示例:用户登录时调用
async function login(username, password, rememberMe) {
if (rememberMe) {
await saveUserInfo(username, password);
}
// 其他登录逻辑
}
// 示例:页面加载时自动填充
window.onload = async function() {
const userInfo = await getUserInfo();
if (userInfo.username && userInfo.password) {
document.getElementById('username').value = userInfo.username;
document.getElementById('password').value = userInfo.password;
}
};
2. 使用 sessionStorage
sessionStorage
是一种会话级别的存储方式,数据在浏览器关闭后会被清除。适用于需要临时保存的数据。
示例代码
// 保存用户信息
async function saveUserInfoSession(username, password) {
const key = await generateSymmetricKey();
const { iv, encryptedData } = await encryptData(password, key);
sessionStorage.setItem('username', username);
sessionStorage.setItem('password', JSON.stringify({ iv, encryptedData }));
// 密钥可以存储在更安全的地方,如服务器端
}
// 获取用户信息
async function getUserInfoSession() {
const username = sessionStorage.getItem('username');
const { iv, encryptedData } = JSON.parse(sessionStorage.getItem('password'));
const key = await generateSymmetricKey(); // 这里应使用同一个密钥
const password = await decryptData(encryptedData, key, iv);
return { username, password };
}
// 示例:用户登录时调用
async function loginSession(username, password, rememberMe) {
if (rememberMe) {
await saveUserInfoSession(username, password);
}
// 其他登录逻辑
}
// 示例:页面加载时自动填充
window.onload = async function() {
const userInfo = await getUserInfoSession();
if (userInfo.username && userInfo.password) {
document.getElementById('username').value = userInfo.username;
document.getElementById('password').value = userInfo.password;
}
};
3. 使用 IndexedDB
IndexedDB 是一种更为复杂和强大的存储方式,适用于需要存储大量数据的场景。
示例代码
// 打开数据库
function openDatabase() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('UserDatabase', 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
db.createObjectStore('users', { keyPath: 'username' });
};
request.onsuccess = (event) => {
resolve(event.target.result);
};
request.onerror = (event) => {
reject(event.target.error);
};
});
}
// 保存用户信息
async function saveUserInfoIndexedDB(username, password) {
const key = await generateSymmetricKey();
const { iv, encryptedData } = await encryptData(password, key);
const db = await openDatabase();
const transaction = db.transaction('users', 'readwrite');
const store = transaction.objectStore('users');
store.put({ username, iv, encryptedData });
// 密钥可以存储在更安全的地方,如服务器端
}
// 获取用户信息
async function getUserInfoIndexedDB(username) {
const db = await openDatabase();
return new Promise((resolve, reject) => {
const transaction = db.transaction('users', 'readonly');
const store = transaction.objectStore('users');
const request = store.get(username);
request.onsuccess = async (event) => {
const result = event.target.result;
if (result) {
const key = await generateSymmetricKey(); // 这里应使用同一个密钥
const password = await decryptData(result.encryptedData, key, result.iv);
resolve({ username: result.username, password });
} else {
resolve(null);
}
};
request.onerror = (event) => {
reject(event.target.error);
};
});
}
// 示例:用户登录时调用
async function loginIndexedDB(username, password, rememberMe) {
if (rememberMe) {
await saveUserInfoIndexedDB(username, password);
}
// 其他登录逻辑
}
// 示例:页面加载时自动填充
window.onload = async function() {
const username = 'exampleUsername'; // 从某处获取用户名
const userInfo = await getUserInfoIndexedDB(username);
if (userInfo && userInfo.username && userInfo.password) {
document.getElementById('username').value = userInfo.username;
document.getElementById('password').value = userInfo.password;
}
};
4. 使用 Cookie
Cookie 是一种简单的存储方式,适用于需要在客户端和服务器之间传递少量数据的场景。需要注意的是,Cookie 的安全性较低,建议结合 HTTPS 和 HttpOnly 属性使用。
示例代码
// 设置 Cookie
function setCookie(name, value, days) {
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
const expires = "expires=" + date.toUTCString();
document.cookie = name + "=" + value + ";" + expires + ";path=/";
}
// 获取 Cookie
function getCookie(name) {
const nameEQ = name + "=";
const ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
// 保存用户信息
async function saveUserInfoCookie(username, password) {
const key = await generateSymmetricKey();
const { iv, encryptedData } = await encryptData(password, key);
setCookie('username', username, 7);
setCookie('password', JSON.stringify({ iv, encryptedData }), 7);
// 密钥可以存储在更安全的地方,如服务器端
}
// 获取用户信息
async function getUserInfoCookie() {
const username = getCookie('username');
const { iv, encryptedData } = JSON.parse(getCookie('password'));
const key = await generateSymmetricKey(); // 这里应使用同一个密钥
const password = await decryptData(encryptedData, key, iv);
return { username, password };
}
// 示例:用户登录时调用
async function loginCookie(username, password, rememberMe) {
if (rememberMe) {
await saveUserInfoCookie(username, password);
}
// 其他登录逻辑
}
// 示例:页面加载时自动填充
window.onload = async function() {
const userInfo = await getUserInfoCookie();
if (userInfo.username && userInfo.password) {
document.getElementById('username').value = userInfo.username;
document.getElementById('password').value = userInfo.password;
}
};
5. 使用 JWT(JSON Web Token)
JWT 是一种常用的身份验证机制,特别适合在前后端分离的应用中使用。JWT 可以安全地传递用户身份信息,并且可以在客户端存储以实现“记住密码”功能。
示例代码
服务器端生成 JWT
假设你使用 Node.js 和 Express 作为服务器端框架,并使用 jsonwebtoken
库来生成和验证 JWT。
const express = require('express');
const jwt = require('jsonwebtoken');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json());
const SECRET_KEY = 'your_secret_key';
// 用户登录接口
app.post('/login', (req, res) => {
const { username, password } = req.body;
// 验证用户名和密码
if (username === 'user' && password === 'password') {
// 生成 JWT
const token = jwt.sign({ username }, SECRET_KEY, { expiresIn: '1h' });
res.json({ token });
} else {
res.status(401).json({ message: 'Invalid credentials' });
}
});
// 受保护的资源
app.get('/protected', (req, res) => {
const token = req.headers['authorization'];
if (!token) {
return res.status(401).json({ message: 'No token provided' });
}
jwt.verify(token, SECRET_KEY, (err, decoded) => {
if (err) {
return res.status(401).json({ message: 'Failed to authenticate token' });
}
res.json({ message: 'Protected resource', user: decoded.username });
});
});
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
客户端存储和使用 JWT
在客户端,可以使用 localStorage
或 sessionStorage
来存储 JWT,并在后续请求中使用。
// 用户登录
async function login(username, password, rememberMe) {
const response = await fetch('/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (response.ok) {
const token = data.token;
if (rememberMe) {
localStorage.setItem('token', token);
} else {
sessionStorage.setItem('token', token);
}
} else {
console.error(data.message);
}
}
// 获取受保护的资源
async function getProtectedResource() {
const token = localStorage.getItem('token') || sessionStorage.getItem('token');
if (!token) {
console.error('No token found');
return;
}
const response = await fetch('/protected', {
method: 'GET',
headers: {
'Authorization': token
}
});
const data = await response.json();
if (response.ok) {
console.log(data);
} else {
console.error(data.message);
}
}
// 示例:用户登录时调用
document.getElementById('loginButton').addEventListener('click', async () => {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const rememberMe = document.getElementById('rememberMe').checked;
await login(username, password, rememberMe);
});
// 示例:页面加载时自动填充
window.onload = async function() {
await getProtectedResource();
};
总结
如上示例,展示了如何使用 localStorage
、sessionStorage
、IndexedDB、Cookie 和 JWT 来实现“记住密码”功能。每种方式都有其适用场景和安全考虑,大家可以根据具体需求选择合适的实现方式。
欢迎在评论区留言讨论~
Happy coding! 🚀
来源:juejin.cn/post/7397284874652942363
uniapp 地图如何添加?你要的教程来喽!
地图在 app 中使用还是很广泛的,常见的应用常见有:
1、获取自己的位置,规划路线。
2、使用标记点进行标记多个位置。
3、绘制多边形,使用围墙标记位置等等。
此篇文章就以高德地图为例,以上述三个常见需求为例,教大家如何在 uniapp 中添加地图。
作为一个不管闲事的前端姑娘,我就忽略掉那些繁琐的账号申请,假设需要的信息问项目经理都要来了,如果你没有现成的信息,还需要申请,请查看:
去高德地图注册账号,根据官网指示获取 key。然后就正式开始前端 uniapp + 高德地图之旅啦!
一、地图配置
在使用地图之前需要配置一下你的地图账号信息,找到项目中的 manifest.json 文件,打开 web 配置,如图:
此处是针对 h5 端,如果我们要打包 安卓和 IOS app 需要配置对应的key信息,如图:
如果这些信息没有人给你提供,就需要自己去官网注册账号实名认证获取。
二、地图使用
2.1、使用标记点进行标记多个位置,具体效果图如下:
<template>
<view class="map-con">
<map style="width: 100%; height: 300px;"
:latitude="latitude"
:longitude="longitude"
:markers="covers"
:scale="12">
</map>
</view>
</template>
<script>
export default {
data() {
return {
longitude: '116.473115',
latitude: '39.993207',
covers: [{
id: 1,
longitude: "116.474595",
latitude: "40.001321",
iconPath: '/static/images/point.png',
},
{
id: 1,
longitude: "116.274595",
latitude: "40.101321",
iconPath: '/static/images/point.png',
},
{
id: 1,
longitude: "116.374595",
latitude: "40.101321",
iconPath: '/static/images/point.png',
},
{
id: 1,
longitude: "116.374595",
latitude: "40.011321",
width: 44,
height: 50,
iconPath:'/static/images/point.png',
}
]
}
}
}
</script>
注意:
看着代码很简单,运行在 h5 之后一切正常,但是运行在安卓模拟器的时候,发现自定义图标没有起作用,显示的是默认标记点。
iconpath 的路径不是相对路径,没有 ../../ 这些,直接根据官网提示写图片路径,虽然模拟器不显示但是真机是正常的。
2.2、绘制多边形,使用围墙标记位置等等。
<template>
<view class="map-con">
<map style="width: 100%; height: 400px;" :latitude="latitude" :longitude="longitude" :scale="11"
:polygons="polygon" :markers="covers">
</map>
</view>
</template>
<script>
export default {
data() {
return {
longitude: '116.304595',
latitude: '40.053207',
polygon: [{
fillColor: '#f00',
strokeColor: '#0f0',
strokeWidth: 3,
points: [{
latitude: '40.001321',
longitude: '116.304595'
},
{
latitude: '40.101321',
longitude: '116.274595'
},
{
latitude: '40.011321',
longitude: '116.374595'
}
]
}],
covers: [{
id: 1,
width: 30,
height: 33,
longitude: "116.314595",
latitude: "40.021321",
iconPath: '/static/images/point.png',
}, ]
}
}
}
</script>
更多样式配置我们去参考官网,官网使用文档写的很细致,地址为:
uniapp 官网:uniapp.dcloud.net.cn/component/m…
三、易错点
1、地图已经显示了,误以为地图未展示
左下角有高德地图标识,就说明地图已经正常显示了,此时可以使用鼠标进行缩放,或设置地图的缩放比例或者修改下地图中心点的经纬度。
2、标记点自定义图标不显示
marker 中的 iconPath 设置标记点的图标路径,可以使用相对路径、base64 等,但是在 h5 查看正常,app 打包之后就不能正常显示了,务必参考官网。
3、uni.getLocation 无法触发
在调试模式中,调用 uni.getLocation 无法触发,其中的 success fail complete 都无法执行,不调用的原因是必须在 https 环境下,所以先保证是在 https 环境下。****
四、有可用插件吗?
uniapp 插件:ext.dcloud.net.cn/search?q=ma…
搜索地图插件的时候,插件挺多的,有免费的也有付费的,即使使用插件也是需要需要注册第三方地图账号的。
我个人认为 uniapp 已经将第三方地图封装过了,使用挺便捷的,具体是否使用插件就根据项目实际情况定。
来源:juejin.cn/post/7271942371637559348
告别轮询,SSE 流式传输可太香了!
今天想和大家分享的一个技术是 SSE 流式传输 。如标题所言,通过 SSE 流式传输的方式可以让我们不再通过轮询的方式获取服务端返回的结果,进而提升前端页面的性能。
对于需要轮询的业务场景来说,采用 SSE 确实是一个更好的技术方案。
接下来,我将从 SSE 的概念、与 Websocket 对比、SSE 应用场景多个方面介绍 SSE 流式传输,感兴趣的同学一起来了解下吧!
什么是 SSE 流式传输
SSE 全称为 Server-sent events , 是一种基于 HTTP 协议的通信技术,允许服务器主动向客户端(通常是Web浏览器)发送更新。
它是 HTML5 标准的一部分,设计初衷是用来建立一个单向的服务器到客户端连接,使得服务器可以实时地向客户端发送数据。
这种服务端实时向客户端发送数据的传输方式,其实就是流式传输。
我们在与 ChatGPT 交互时,可以发现 ChatGPT 的响应总是间断完成。细扒 ChatGPT 的网络传输模式,可以发现,用的也是流式传输。
SSE 流式传输的好处
在 SSE 技术出现之前,我们习惯把需要等待服务端返回的过程称为长轮询。
长轮询的实现其实也是借助 http 请求来完成,一个完整的长轮询过程如下图所示:
从图中可以发现,长轮询最大的弊端是当服务端响应请求之前,客户端发送的所有请求都不会被受理。并且服务端发送响应的前提是客户端发起请求。
前后端通信过程中,我们常采用 ajax 、axios 来异步获取结果,这个过程,其实也是长轮询的过程。
而同为采用 http 协议通信方式的 SSE 流式传输,相比于长轮询模式来说,优势在于可以在不需要客户端介入的情况下,多次向客户端发送响应,直至客户端关闭连接。
这对于需要服务端实时推送内容至客户端的场景可方便太多了!
SSE 技术原理
1. 参数设置
前文说到,SSE 本质是一个基于 http 协议的通信技术。
因此想要使用 SSE 技术构建需要服务器实时推送信息到客户端的连接,只需要将传统的 http 响应头的 contentType 设置为 text/event-stream 。
并且为了保证客户端展示的是最新数据,需要将 Cache-Control 设置为 no-cache 。
在此基础上,SSE 本质是一个 TCP 连接,因此为了保证 SSE 的持续开启,需要将 Connection 设置为 keep-alive 。
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
完成了上述响应头的设置后,我们可以编写一个基于 SSE 流式传输的简单 Demo 。
2. SSE Demo
服务端代码:
const express = require('express');
const app = express();
const PORT = 3000;
app.use(express.static('public'));
app.get('/events', function(req, res) {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
let startTime = Date.now();
const sendEvent = () => {
// 检查是否已经发送了10秒
if (Date.now() - startTime >= 10000) {
res.write('event: close\ndata: {}\n\n'); // 发送一个特殊事件通知客户端关闭
res.end(); // 关闭连接
return;
}
const data = { message: 'Hello World', timestamp: new Date() };
res.write(`data: ${JSON.stringify(data)}\n\n`);
// 每隔2秒发送一次消息
setTimeout(sendEvent, 2000);
};
sendEvent();
});
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
客户端代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SSE Example</title>
</head>
<body>
<h1>Server-Sent Events Example</h1>
<div id="messages"></div>
<script>
const evtSource = new EventSource('/events');
const messages = document.getElementById('messages');
evtSource.onmessage = function(event) {
const newElement = document.createElement("p");
const eventObject = JSON.parse(event.data);
newElement.textContent = "Message: " + eventObject.message + " at " + eventObject.timestamp;
messages.appendChild(newElement);
};
</script>
</body>
</html>
当我们在浏览器中访问运行在 localhost: 3000 端口的客户端页面时,页面将会以 流式模式 逐步渲染服务端返回的结果:
需要注意的是,为了保证使用 SSE 通信协议传输的数据能被客户端正确的接收,服务端和客户端在发送数据和接收数据应该遵循以下规范:
服务端基本响应格式
SSE 响应主要由一系列以两个换行符分隔的事件组成。每个事件可以包含以下字段:
data:事件的数据。如果数据跨越多行,每行都应该以data:开始。
id:事件的唯一标识符。客户端可以使用这个ID来恢复事件流。
event:自定义事件类型。客户端可以根据不同的事件类型来执行不同的操作。
retry:建议的重新连接时间(毫秒)。如果连接中断,客户端将等待这段时间后尝试重新连接。
字段之间用单个换行符分隔,而事件之间用两个换行符分隔。
客户端处理格式
客户端使用 EventSource 接口监听 SSE 消息:
const evtSource = new EventSource('path/to/sse');
evtSource.onmessage = function(event) {
console.log(event.data); // 处理收到的数据
};
SSE 应用场景
SSE 作为基于 http 协议由服务端向客户端单向推送消息的通信技术,对于需要服务端主动推送消息的场景来说,是非常适合的:
SSE 兼容性
可以发现,除了 IE 和低版本的主流浏览器,目前市面上绝大多数浏览器都支持 SSE 通信。
SSE 与 WebSocket 对比
看完 SSE 的使用方式后,细心的同学应该发现了:
SSE 的通信方式和 WebSocket 很像啊,而且 WebSocket 还支持双向通信,为什么不直接使用 WebSocket ?
下表展示了两者之间的对比:
特性/因素 | SSE | WebSockets |
---|---|---|
协议 | 基于HTTP,使用标准HTTP连接 | 单独的协议(ws:// 或 wss://),需要握手升级 |
通信方式 | 单向通信(服务器到客户端) | 全双工通信 |
数据格式 | 文本(UTF-8编码) | 文本或二进制 |
重连机制 | 浏览器自动重连 | 需要手动实现重连机制 |
实时性 | 高(适合频繁更新的场景) | 非常高(适合高度交互的实时应用) |
浏览器支持 | 良好(大多数现代浏览器支持) | 非常好(几乎所有现代浏览器支持) |
适用场景 | 实时通知、新闻feed、股票价格等需要从服务器推送到客户端的场景 | 在线游戏、聊天应用、实时交互应用 |
复杂性 | 较低,易于实现和维护 | 较高,需要处理连接的建立、维护和断开 |
兼容性和可用性 | 基于HTTP,更容易通过各种中间件和防火墙 | 可能需要配置服务器和网络设备以支持WebSocket |
服务器负载 | 适合较低频率的数据更新 | 适合高频率消息和高度交互的场景 |
可以发现,SSE 与 WebSocket 各有优缺点,对于需要客户端与服务端高频交互的场景,WebSocket 确实更适合;但对于只需要服务端单向数据传输的场景,SSE 确实能耗更低,且不需要客户端感知。
参考文档
来源:juejin.cn/post/7355666189475954725
高德地图 JS API key 的保护,安全密钥的使用方案
背景
因为高德地图的 key 被盗用,导致额度不耗尽。增加了不必要的成本,所以对 key 的保护尤为重要。
目前情况
现在项目中使用高德地图是直接将 key 写在代码中。
在调用高德 api 的时候,key 会明文拼接在请求地址上,因此会被别有用心的人利用。
解决方案
业务运营多年,高德地图的 key 已是多年前创建的,所以第一步就是创建一个新的 key。
明文密钥配合域名白名单
2021年12月02日以后创建的 key 需要配合安全密钥一起使用,而且添加了域名白名单配置。
项目代码做个简单的修改即可:
如果在域名白名单中的调用接口能正常使用,如域名不在白名单中,则提示没有权限。
从此看已经起到了限制作用,但实际是防君子不防小人的方案。不建议在生产环境使用,至于原因,你琢磨琢磨。
代理转发请求
因为需要 key 需要配合安全密钥一起使用,不然就会提示没有权限,所以只需要将安全密钥“隐藏”起来就可以了。
请求会将 key 和安全密钥明文拼接在一起,为了将安全密钥“隐藏”起来,只需要将请求代理到自己的服务器上,然后在服务器上将安全密钥拼接上。
以 Nginx 为例:
项目代码配置代理地址即可:
到处,完美收官。
后记
个人项目,可以随意玩耍。公司项目凡是涉及到钱财的东西都要谨慎一些,不要低估灰产的能力。
来源:juejin.cn/post/7405777954516025370
整理最近的生活
Hi,见到你真好 :)
写在开始
自从去年 8月 搬到 上海 之后,就很少再写文章,很多的思路都是断断续续,导致也不知道该写点什么,所以最后是是草稿攒了一堆,大纲整整齐齐,内容空空如也,甚是尴尬。
时间长了后,逐渐就有点开摆的心态。也有点理解为什么很多技术同学突然就不更新了。🫨
不过最近好在有些念头又开始跃跃欲试,故想着写一些东西,活跃活跃生锈的脑子🫡。
故本篇,其实算是一个随记,想到哪里就写到哪里,不包含任何技术指南。
搬家三两事
背景
因为当时是需要从北京搬到上海,家里还有两只猫以及超级多的行李,故需要考虑的事情有下面几点:
- 琐碎的行李怎么处理?
- 如何将 两只猫 安全的送到上海?
- 升降桌、工学椅、冰柜、猫厕所等大件怎么处理?
头脑风暴
- 两只猫托运走
成本过高,一只平均需要 1000+ ,以及需要疫苗齐全,以及需要提前7天以上准备。
- 升降桌、工学椅、冰柜出二手
出二手 = 5折以下,最主要是刚买没半年。😑
- 行李快递走?
货拉拉跨城搬运+快递一部分。
上述总费用: 托运🐱2000 + 二手折损(3000) + 货拉拉(4000+) + 动态费用1000 = 9500 左右。
备注点:
- 托运猫的安全性;
- 如果喊货拉拉,那就不需要二手回收;
算完上述费用之后,我忍不住拍了一下家里两只,嘴里嚷嚷着:要不是你两,我何至于如此!!!
最终解决
小红书约了一个跨城搬家师傅,车型依维柯(长4.9,宽1.9,高2.2),最后只花了 3500 解决了。
关于爱好的倒腾
世界奇奇怪怪,生活慢慢悠悠。
有时候会想,人的一生难得有几个爱好,那可能就会包含折腾 电子小垃圾:)
下面列一下今年折腾过的一些小垃圾:
Pixel 7
绿色- 戴尔
U2723QX
ikbc
高达97键盘ipadAir
M1丐版PS5
国行双手柄SlimStudio Display
带升降- 富士
XT-3
、35-1.4镜头 MacMini M1 16+256
丐版
关于相机
一直以来,其实都比较喜欢富士相机的直出,主要因为自己较懒。所以对于学习修图,实在是提不起感觉,而对于摄像的技巧,也只是草草了解几个构图方式,也谈不上研究,故富士就比较适合我这种[懒人]。
但其实这个事情的背景是,在最开始接触相机时,大概是21年,那时想买富士 xs-10
,结果因为疫情缺货,国内溢价到了1w,属实是离谱。所以当时就买了佳能 M6 Mark2
。
等过了半年左右,觉得佳能差点意思,就又换了索尼的 A7C
,对焦嘎嘎猛,主打的就是一个激情拍摄,后期就是套个滤镜就行🫡。
等新手福利期一过,时间线再往后推半年,相机开始吃土,遂出了二手👀。
来上海后,又想起了相机,故在闲鱼上又收了一个富士XT3,属于和xs20同配置(少几个滤镜),主打的就是一个拍到就是赚到(当然现在也是吃土🤡)。
关于PS5
因为21年淘过一个 PS4 Pro
,平时也是处于吃灰,玩的最多的反而是 双人成行(现在也没和老婆打完,80%)😬。
但抵不住冲动的心,没事就会看几眼 PS5
,为此,老婆专门买了一个,以解我没事的念想。
不过真得知快递信息后,还是心里有点不舍。就以家里还有一个在吃土,买这个没啥用为由退掉🫨。
抵得过暂时,抵不过长久,过了一段时间,念头又上来了,没事又翻起了pdd和二手鱼。
于是,在一个风和日丽的中午,激情下单。
结果卖家的名字居然和我只有一字之差,真的是造化弄人。🤡
到手之后,每周的愿望就是能在周末打一会游戏,结果很难有实现过,唯一一次畅玩 [潜水员戴夫🐟],结果导致身心疲惫。
ps: 截止写完这篇时候,最近在爽玩黑神话悟空,故真正实现了使用。
最近抽空在打大镖客2和黑悟空,遂带上几张图。
关于小主机
之前因为只有一个笔记本,上下班都需要背笔记本,遇到冬天还好,到了夏天,就非常反感,故最近诞生出了买一个 小主机 的想法。
因为不玩pc游戏,故 winodws
系天然不用选择,当然也就不用考虑同事推荐的 零刻 这种小主机,直接去搞一个 mac mini
即可。
最后综合对比了一下,觉得 Mac Mini(M1-16/256)
即可,闲鱼只需要3000左右即可。
对
M1
及以后的Mac设备而言,RAM>CPU>固态 ,故cpu
的性能对我而言足矣。故而以
16g
作为标准,而存储方面,可以考虑外接固态做扩展盘解决,这套方案是性价比最高的。🫡
有了上面的结论之后,就直接去闲鱼收,最后 3100 收了一个 MacMini M1/16g
。
然后又在京东买了一个 硬盘盒子(海康威视20Gbps)+雷神固态(PR5000/读写4.5k)。
本来想去买一个 40Gbps
的硬盘盒,结果一看价,399起步,有点夸张(我盘才4xx),遂放弃。
Tips:
- 不要轻易把系统装到扩展固态(特别是硬盘盒速度不够40Gbps时)上。
- 开机引导会变慢(如果一直不关机当我没说);
- 如果硬盘散热跟不上,系统会卡顿,如同ANR了一样;
- 因为
MacMini
雷电口 不支持usb3.2 Gen2x2
,在不满足雷电口的情况下,最快读写 只有1k。故导致硬盘速度只能到达 1k 读写,也就是硬盘盒的一半速度。
最后全家福如下:
关于显示器
最近一年连着用过了 3 个显示器,价位一路从 1K -> 1.2W,也算又多了一点点经验分享。
先说背景:因为没有游戏与高刷需求,更多的是追求接近Mac体验(用同事话说就是,被果子惯得),故下面的评测仅限于特定范围。
本次的参照物:MacBook Pro14寸自带的 XDR显示器、峰值亮度
1600nt
;
Redmi 27/4k (1.5k)
HDR400、65W Typc、95%P3、E<2
全功能typc、支持kvm,色彩模式有forMac。
塑料机身,但设计挺好看的,观感不错,for Mac模式 能接近 70%
体验;
仔细对比,与mac屏幕差距最大的是通透度与色准问题,如果说MacBook是 100%
通透,红米只有 60-70%
之间;
后期偶尔会出现连接不稳定,屏幕闪烁问题,时好时不好,猜测可能与 typc65w
电压不稳有关。
综合来说,这个价位,性价比非常之高。
戴尔U2723QX (3k)
HDR400、90W Typc、98%P3
全功能typc、支持kvm、接口大满贯,多到发指。
全金属机身,屏幕是 IPS Black
,也就是 LG 的 NanoIPS
面板,不过也算是一块老屏幕了。
整体体验下来不错,有接近MacBook 80%
的体验,色准什么的都很ok,在使用过程中,也没遇见过任何问题。
综合来说,算是 普通消费级代码显示器的王者 ,不过如果要提性价比,可能不如同款屏幕的 LG。
至于戴尔的售后,因为没体验过,所以难评。
Studio Display (1.2w)
5k分辨率、600nits、A13、六扬声器、4麦克风、自带摄像头
接口方面:雷电3(40G) + 3 x typc(10G)
Mac系列的最佳搭配,最接近 MacBook 的色准,非常通透,95%+
的接近水平,亮度差点,不过已经够了;
整体来说,如果对屏幕要求,或者眼睛比较敏感,那么这个显示器是比较不错的选择(别忘了开原彩+自动亮度,看习惯了后,还是很舒服)。
至于不足之处,可能就只有价格这一个因素。
家用设备的倒腾
来上海之后,家庭设备倒腾的比较少,少有的几个物件是:
- 小米除湿机 22L;
- 追觅洗地机
H30Mix
; - 小米55寸 MiniLed 电视;
关于除湿机
当时刚来上海,作为一个土生土长的北方人,遇到黄梅天,那感觉,简直浑身难受,一个字 黏,两个字 闷热。
故当时紧急下单了一款除湿机,一晚上可以除满满一桶,实测大概4-5L,最高档开1小时左右,家里基本就可以感受到干爽,比较适合40m的小家使用。再说说缺点:
- 吵!
- 如果没有接下水管,可能需要隔两天换一次水;
再说说现状,成功实现 100%
吃土状态,几乎毫无悬念,因为空调更省事。。。
最后给一些北方人首次去南方居住的建议:
- 不要选3层以下,太潮;
- 注意白天看房,看看光线如何;
- 注意附近切记不是建筑工地等等;
- 上海合租比较便宜,自如挺合适;
关于洗地机
刚到上海时,之前的 米家扫拖机器人 也一起带过来了,但因为实在 版本太旧,逐渐不堪大用 ,只能用勉强可用这个词来形容,而且特别容易 积攒头发加手动加水清洁 ,时间久了,就比较烦。
故按照我的性格,没事就在看新的替代物件,最开始锁定的是追觅扫拖机器人,但最后经过深思熟虑,觉得家里太小(60m) ,故扫地机器人根本转不开腿,可能咣咣撞椅子了,故退而求其次,去看洗地机。入手了追觅的 H30mix
,洗地吸尘都能干。
最后经过实际证明:洗地机就老老实实洗地,吸尘还是交给专门的吸尘器,主要是拆卸太麻烦🤡。故家里本来已经半个身子准备退休的德尔玛又被强行续命了一波。
再说优点,真的很好用,拖完地放架子上自动洗烘一体,非常方便。实测拖的比我干净,唯一缺点就是,每天洗完需要手动倒一下脏水(不倒可能会反味)。
关于电视
来上海后,一直想换个电视打游戏用,就没事看了看电视。因为之前的 Tcl
邮给了岳父老房子里,于是按耐不住的心又开始躁动了,故某个夜晚就看了下电视,遂对比后下单了小米的55寸 miniLed。考虑到家里地方不是很大,故也顺带卖了一个可移动的支架。
现在电视的价格是真的便宜,Miniled 都被小米干到了
2k
附近,但一分钱一分货,纸面参数终究是纸面参数,最后看看实际观感,也就那样。
关于一些想法
人生不过几十载,如果工作要占用掉最宝贵的20年华,那未免太过于糟糕。
不知为何,最近总感觉上班的时间过得尤为快,每周过了周二后,周五的下一步就又要到了,下个月初也越来越近了。
来上海后,几乎每天都会和老婆晚上下楼走走,近的时候绕着小区,远的时候绕着小区外面的路。起初刚来上海时,脑子里依然会有过几年会北京的想法,但在上海有段时间后,这个想法就变得没那么重了,直到现在,我两都变成了留在上海也许更好(仔细算了算)😐。
写在最后
兜兜转转,这篇也是写了近一个月,属于是想起来写一点,接下来会更新的频繁一点。
下次再见,朋友们 👋
关于我
我是 Petterp ,一个 Android 工程师。如果本文,你觉得写的还不错,不妨点个赞或者收藏,你的支持,是我持续创作的最大鼓励!
来源:juejin.cn/post/7406258856953790515
if-else嵌套太深怎么办?
在前端开发中,if-else
嵌套过深往往会导致代码可读性下降、维护难度增加,甚至引发潜在的逻辑错误。本文将从一个典型的深层 if-else
嵌套案例出发,逐步分析并探讨多种优化策略,帮助开发者解决这一问题。
一、深层 if-else
嵌套的案例
假设我们正在开发一个处理订单状态的功能,根据订单的不同状态执行相应的操作。下面是一个典型的 if-else
嵌套过深的代码示例:
function processOrder(order) {
if (order) {
if (order.isPaid) {
if (order.hasStock) {
if (!order.isCanceled) {
// 处理已付款且有库存的订单
return 'Processing paid order with stock';
} else {
// 处理已取消的订单
return 'Order has been canceled';
}
} else {
// 处理库存不足的订单
return 'Out of stock';
}
} else {
// 处理未付款的订单
return 'Order not paid';
}
} else {
// 处理无效订单
return 'Invalid order';
}
}
****
这段代码展示了多个条件的嵌套判断,随着条件的增多,代码的层级不断加深,使得可读性和可维护性大幅降低。
二、解决方案
1. 使用早返回
早返回是一种有效的方式,可以通过尽早退出函数来避免不必要的嵌套。
function processOrder(order) {
if (!order) {
return 'Invalid order';
}
if (!order.isPaid) {
return 'Order not paid';
}
if (!order.hasStock) {
return 'Out of stock';
}
if (order.isCanceled) {
return 'Order has been canceled';
}
return 'Processing paid order with stock';
}
通过早返回,条件判断被简化为一系列独立的判断,减少了嵌套层级,代码更直观。
2. 使用对象字面量或映射表
当条件判断基于某个特定的值时,可以利用对象字面量替代 if-else
。
const orderStatusActions = {
'INVALID': () => 'Invalid order',
'NOT_PAID': () => 'Order not paid',
'OUT_OF_STOCK': () => 'Out of stock',
'CANCELED': () => 'Order has been canceled',
'DEFAULT': () => 'Processing paid order with stock',
};
function processOrder(order) {
if (!order) {
return orderStatusActions['INVALID']();
}
if (!order.isPaid) {
return orderStatusActions['NOT_PAID']();
}
if (!order.hasStock) {
return orderStatusActions['OUT_OF_STOCK']();
}
if (order.isCanceled) {
return orderStatusActions['CANCELED']();
}
return orderStatusActions['DEFAULT']();
}
使用对象字面量将条件与行为进行映射,使代码更加模块化且易于扩展。
3. 使用策略模式
策略模式可以有效应对复杂的多分支条件,通过定义一系列策略类,将不同的逻辑封装到独立的类中。
class OrderProcessor {
constructor(strategy) {
this.strategy = strategy;
}
process(order) {
return this.strategy.execute(order);
}
}
class PaidOrderStrategy {
execute(order) {
if (!order.hasStock) {
return 'Out of stock';
}
if (order.isCanceled) {
return 'Order has been canceled';
}
return 'Processing paid order with stock';
}
}
class InvalidOrderStrategy {
execute(order) {
return 'Invalid order';
}
}
class NotPaidOrderStrategy {
execute(order) {
return 'Order not paid';
}
}
// 使用策略模式
const strategy = order ? (order.isPaid ? new PaidOrderStrategy() : new NotPaidOrderStrategy()) : new InvalidOrderStrategy();
const processor = new OrderProcessor(strategy);
processor.process(order);
策略模式将不同逻辑分散到独立的类中,避免了大量的 if-else
嵌套,增强了代码的可维护性。
4. 使用多态
通过多态性,可以通过继承和方法重写替代 if-else
条件分支。
优化后的代码:
class Order {
process() {
throw new Error('This method should be overridden');
}
}
class PaidOrder extends Order {
process() {
if (!this.hasStock) {
return 'Out of stock';
}
if (this.isCanceled) {
return 'Order has been canceled';
}
return 'Processing paid order with stock';
}
}
class InvalidOrder extends Order {
process() {
return 'Invalid order';
}
}
class NotPaidOrder extends Order {
process() {
return 'Order not paid';
}
}
// 通过多态处理订单
const orderInstance = new PaidOrder(); // 根据order实例化相应的类
orderInstance.process();
多态性允许我们通过不同的子类实现不同的逻辑,从而避免在同一个函数中使用大量的 if-else
。
5. 使用函数式编程技巧
函数式编程中的 map
, filter
, 和 reduce
可以帮助我们避免复杂的条件判断。
优化后的代码:
const orderProcessors = [
{condition: (order) => !order, process: () => 'Invalid order'},
{condition: (order) => !order.isPaid, process: () => 'Order not paid'},
{condition: (order) => !order.hasStock, process: () => 'Out of stock'},
{condition: (order) => order.isCanceled, process: () => 'Order has been canceled'},
{condition: () => true, process: () => 'Processing paid order with stock'},
];
const processOrder = (order) => orderProcessors.find(processor => processor.condition(order)).process();
通过 find
和 filter
等函数式编程方法,我们可以避免嵌套的 if-else
语句,使代码更加简洁和易于维护。
三、总结
if-else
嵌套过深的问题是前端开发中常见的挑战。通过本文提供的多种解决方案,如早返回、对象字面量、策略模式、多态和函数式编程技巧,开发者可以根据实际需求选择合适的优化方案,从而提高代码的可读性、可维护性和性能。
希望这些方法能对你的开发工作有所帮助,欢迎在评论区分享你的经验与想法!
来源:juejin.cn/post/7406538050228633641
Oracle开始严查Java许可!
0x01、
前段时间在论坛里就看到一个新闻,说“Oracle又再次对Java下手,开始严查Java许可,有企业连夜删除JDK”,当时就曾在网上引起了一阵关注和讨论。
这不最近在科技圈又看到有媒体报道,Oracle再次严查,对于Java许可和版权的审查越来越严格了。
其实很早之前就有看到新闻报道说,甲骨文公司Oracle已经开始将Java纳入其软件许可审查中,并且对一些公司的Java采用情况开启审计,目的是找出那些处于不合规边缘或已经违规的客户。
之前主要还是针对一些小公司发出过审查函件,而现在,甚至包括财富200强在内的一些组织或公司都收到了来自Oracle有关审查方面的信件。
0x02、
还记得去年上半年的时候,Oracle就曾发布过一个PDF格式的新版Java SE收费政策《Oracle Java SE Universal Subscription Global Price List (PDF)》。
打开那个PDF,在里面可以看到Oracle新的Java SE通用订阅全球价目表:
表格底部还举了一个具体计费的例子。
比方说一个公司有28000名总雇员,里面可能包含有23000名全职、兼职、临时雇员,以及5000其他类型员工(比如说代理商、合约商、咨询顾问),那这个总价格是按如下方式进行计算:
28000 * 6.75/年
合着这个新的收费标准是直接基于公司里总的员工数来进行计算的,而不仅仅是使用Java SE的员工数。
这样一来,可能就会使企业在相同软件的的使用情况下会多出不少费用,从而增加软件成本。
看到这里不得不说,Oracle接手之后把Java的商业化运作这块整得是明明白白的。
0x03、
众所周知,其实Java最初是由Sun公司的詹姆斯·高斯林(James Gosling,后来也被称为Java之父)及其团队所研发的。
并且最开始名字并不叫Java,而是被命名为:Oak,这个名字得自于 Gosling 想名字时看到了窗外的一棵橡树。
就在 Gosling 的团队即将发布成果之前,又出了个小插曲——Oak 竟然是一个注册商标。Oak Technology(OAKT)是一家美国半导体芯片制造商,Oak 是其注册商标。
既然不能叫Oak,那应该怎么命名好呢?
后来 Gosling 看见了同事桌上有一瓶咖啡,包装上写着 Java,于是灵感一现。至此,Java语言正式得名,并使用至今。
1995年5月,Oak语言才更名为Java(印度尼西亚爪哇岛的英文名称,因盛产咖啡而闻名),并于当时的SunWorld大会上发布了JAVA 1.0,而且那句“Write Once,Run Anywhere”的slogan也是那时候推出的。
此后,Java语言一直由Sun公司来进行维护开发,一直到早期的JDK 7。
2009年4月,Oracle以74亿美元现金收购了Sun公司,至此一代巨头基本没落。
与此同时,Java商标也被列入Oracle麾下,成为了Oracle的重要资源。
众所周知,Oracle接手Java之后,就迅速开始了商业化之路的实践,也于后续推出了一系列调整和改革的操作。
其实Oracle早在2017年9月就宣布将改变JDK版本发布周期。新版本发布周期中,一改原先以特性驱动的发布方式,而变成了以时间为驱动的版本迭代。
也即:每6个月会发布一个新的Java版本,而每3年则会推出一个LTS版本。
而直到前段时间,Java 22都已经正式发布了。
0x04、
那针对Oracle这一系列动作,以及新的定价策略和订阅问题,有不少网友讨论道,那就不使用Oralce JDK,切换到OpenJDK,或者使用某些公司开源的第三方JDK。
众所周知,OpenJDK是一个基于GPL v2 许可的开源项目,自Java 7开始就是Java SE的官方参考实现。
既然如此,也有不少企业或者组织基于OpenJDK从而构建了自己的JDK版本,这些往往都是基于OpenJDK源码,然后增加或者说定制一些自己的专属内容。
比如像阿里的Dragonwell,腾讯的Kona,AWS的Amazon Corretto,以及Azul提供的Zulu JDK等等,都是这类典型的代表。
它们都是各自根据自身的业务场景和业务需求并基于OpenJDK来打造推出的开源JDK发行版本,像这些也都是可以按需去选用的。
文章的最后,也做个小调查:
大家目前在用哪款JDK和版本来用于开发环境或生产环境的呢?
注:本文在GitHub开源仓库「编程之路」 github.com/rd2coding/R… 中已经收录,里面有我整理的6大编程方向(岗位)的自学路线+知识点大梳理、面试考点、我的简历、几本硬核pdf笔记,以及程序员生活和感悟,欢迎star。
来源:juejin.cn/post/7405845617282449462
网页也能像 QQ 一样发出右下角消息?轻松实现桌面通知!
网页也能像 QQ 一样发出右下角消息?轻松实现桌面通知!
大家好,我是蒜鸭。今天我们来聊聊如何让网页像 QQ 那样在右下角弹出消息通知。这个功能不仅能提升用户体验,还能增加网站的互动性。让我们一起探索如何在网页中实现这个酷炫的功能吧!
1. 为什么需要网页通知?
在当今信息爆炸的时代,获取用户注意力变得越来越困难。传统的网页通知方式,如弹窗或页面内提示,往往会打断用户的浏览体验。而类似 QQ 那样的右下角消息通知,既能及时传递信息,又不会过分干扰用户,可以说是一种相当优雅的解决方案。
实现这种通知功能,我们有两种主要方式:使用 Web Notifications API 或自定义 CSS+JavaScript 实现。接下来,我们将详细探讨这两种方法的实现过程、优缺点以及适用场景。
2. 使用 Web Notifications API
2.1 Web Notifications API 简介
Web Notifications API 是现代浏览器提供的一个强大功能,它允许网页向用户发送通知,即使在用户没有打开网页的情况下也能工作。这个 API 的使用非常简单,但功能却十分强大。
2.2 基本实现步骤
- 检查浏览器支持
- 请求用户授权
- 创建并显示通知
让我们来看看具体的代码实现:
// 检查浏览器是否支持通知
if ("Notification" in window) {
// 请求用户授权
Notification.requestPermission().then(function (permission) {
if (permission === "granted") {
// 创建并显示通知
var notification = new Notification("Hello from Web!", {
body: "这是一条来自网页的通知消息",
icon: "path/to/icon.png"
});
// 点击通知时的行为
notification.onclick = function() {
window.open("https://example.com");
};
}
});
}
2.3 优点和注意事项
优点:
– 原生支持,无需额外库
– 可以在用户未浏览网页时发送通知
– 支持富文本和图标
注意事项:
– 需要用户授权,一些用户可能会拒绝
– 不同浏览器的显示样式可能略有不同
– 过度使用可能会引起用户反感
3. 自定义 CSS+JavaScript 实现
如果你想要更多的样式控制,或者希望通知始终显示在网页内,那么使用自定义的 CSS+JavaScript 方案可能更适合你。
3.1 基本思路
- 创建一个固定位置的 div 元素作为通知容器
- 使用 JavaScript 动态创建通知内容
- 添加动画效果使通知平滑显示和消失
3.2 HTML 结构
<div id="notification-container"></div>
3.3 CSS 样式
#notification-container {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 9999;
}
.notification {
background-color: #f8f8f8;
border-left: 4px solid #4CAF50;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
padding: 16px;
margin-bottom: 10px;
width: 300px;
opacity: 0;
transform: translateX(100%);
transition: all 0.3s ease-in-out;
}
.notification.show {
opacity: 1;
transform: translateX(0);
}
.notification-title {
font-weight: bold;
margin-bottom: 5px;
}
.notification-body {
font-size: 14px;
}
3.4 JavaScript 实现
function showNotification(title, message, duration = 5000) {
const container = document.getElementById('notification-container');
const notification = document.createElement('div');
notification.className = 'notification';
const titleElement = document.createElement('div');
titleElement.className = 'notification-title';
titleElement.textContent = title;
const bodyElement = document.createElement('div');
bodyElement.className = 'notification-body';
bodyElement.textContent = message;
notification.appendChild(titleElement);
notification.appendChild(bodyElement);
container.appendChild(notification);
// 触发重绘以应用初始样式
notification.offsetHeight;
// 显示通知
notification.classList.add('show');
// 设置定时器移除通知
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => {
container.removeChild(notification);
}, 300);
}, duration);
}
// 使用示例
showNotification('Hello', '这是一条自定义通知消息');
3.5 优点和注意事项
优点:
– 完全可定制的外观和行为
– 不需要用户授权
– 可以轻松集成到现有的网页设计中
注意事项:
– 仅在用户浏览网页时有效
– 需要考虑移动设备的适配
– 过多的通知可能会影响页面性能
4. 高级技巧和最佳实践
4.1 通知分级
根据通知的重要性进行分级,可以使用不同的颜色或图标来区分:
function showNotification(title, message, level = 'info') {
// ... 前面的代码相同
let borderColor;
switch(level) {
case 'success':
borderColor = '#4CAF50';
break;
case 'warning':
borderColor = '#FFC107';
break;
case 'error':
borderColor = '#F44336';
break;
default:
borderColor = '#2196F3';
}
notification.style.borderLeftColor = borderColor;
// ... 后面的代码相同
}
// 使用示例
showNotification('成功', '操作已完成', 'success');
showNotification('警告', '请注意...', 'warning');
showNotification('错误', '出现问题', 'error');
4.2 通知队列
为了避免同时显示过多通知,我们可以实现一个简单的通知队列:
const notificationQueue = [];
let isShowingNotification = false;
function queueNotification(title, message, duration = 5000) {
notificationQueue.push({ title, message, duration });
if (!isShowingNotification) {
showNextNotification();
}
}
function showNextNotification() {
if (notificationQueue.length === 0) {
isShowingNotification = false;
return;
}
isShowingNotification = true;
const { title, message, duration } = notificationQueue.shift();
showNotification(title, message, duration);
setTimeout(showNextNotification, duration + 300);
}
// 使用示例
queueNotification('通知1', '这是第一条通知');
queueNotification('通知2', '这是第二条通知');
queueNotification('通知3', '这是第三条通知');
4.3 响应式设计
为了确保通知在各种设备上都能正常显示,我们需要考虑响应式设计:
@media (max-width: 768px) {
#notification-container {
left: 20px;
right: 20px;
bottom: 20px;
}
.notification {
width: auto;
}
}
4.4 无障碍性考虑
为了提高通知的可访问性,我们可以添加 ARIA 属性和键盘操作支持:
function showNotification(title, message, duration = 5000) {
// ... 前面的代码相同
notification.setAttribute('role', 'alert');
notification.setAttribute('aria-live', 'polite');
const closeButton = document.createElement('button');
closeButton.textContent = '×';
closeButton.className = 'notification-close';
closeButton.setAttribute('aria-label', '关闭通知');
closeButton.addEventListener('click', () => {
notification.classList.remove('show');
setTimeout(() => {
container.removeChild(notification);
}, 300);
});
notification.appendChild(closeButton);
// ... 后面的代码相同
}
5. 性能优化与注意事项
在实现网页通知功能时,我们还需要注意以下几点:
- 防抖和节流:对于频繁触发的事件(如实时通知),使用防抖或节流技术可以有效减少不必要的通知显示。
- 内存管理:确保在移除通知时,同时清理相关的事件监听器和 DOM 元素,避免内存泄漏。
- 优雅降级:对于不支持 Web Notifications API 的浏览器,可以降级使用自定义的 CSS+JavaScript 方案。
- 用户体验:给用户提供控制通知显示的选项,如允许用户设置通知的类型、频率等。
- 安全考虑:在使用 Web Notifications API 时,确保只在 HTTPS 环境下请求权限,并尊重用户的权限设置。
网页通知是一个强大的功能,能够显著提升用户体验和网站的互动性。无论是使用 Web Notifications API 还是自定义的 CSS+JavaScript 方案,都能实现类似 QQ 那样的右下角消息通知。选择哪种方式取决于你的具体需求和目标用户群。通过合理使用通知功能,你可以让你的网站变得更加生动和用户友好。
来源:juejin.cn/post/7403283321793314850
localhost和127.0.0.1的区别是什么?
今天在网上逛的时候看到一个问题,没想到大家讨论的很热烈,就是标题中这个:
localhost和127.0.0.1的区别是什么?
前端同学本地调试的时候,应该没少和localhost打交道吧,只需要执行 npm run 就能在浏览器中打开你的页面窗口,地址栏显示的就是这个 http://localhost:xxx/index.html
可能大家只是用,也没有去想过这个问题。
联想到我之前合作过的一些开发同学对它们俩的区别也没什么概念,所以我觉得有必要普及下。
localhost是什么呢?
localhost是一个域名,和大家上网使用的域名没有什么本质区别,就是方便记忆。
只是这个localhost的有效范围只有本机,看名字也能知道:local就是本地的意思。
张三和李四都可以在各自的机器上使用localhost,但获取到的也是各自的页面内容,不会相互打架。
从域名到程序
要想真正的认清楚localhost,我们还得从用户是如何通过域名访问到程序说起。
以访问百度为例。
1、当我们在浏览器输入 baidu.com 之后,浏览器首先去DNS中查询 baidu.com 的IP地址。
为什么需要IP地址呢?打个比方,有个人要寄快递到你的公司,快递单上会填写:公司的通讯地址、公司名称、收件人等信息,实际运输时快递会根据通信地址进行层层转发,最终送到收件人的手中。网络通讯也是类似的,其中域名就像公司名称,IP地址就像通信地址,在网络的世界中只有通过IP地址才能找到对应的程序。
DNS就像一个公司黄页,其中记录着每个域名对应的IP地址,当然也有一些域名可能没做登记,就找不到对应的IP地址,还有一些域名可能会对应多个IP地址,DNS会按照规则自动返回一个。我们购买了域名之后,一般域名服务商会提供一个域名解析的功能,就是把域名和对应的IP地址登记到DNS中。
这里的IP地址从哪里获取呢?每台上网的电脑都会有1个IP地址,但是个人电脑的IP地址一般是不行的,个人电脑的IP地址只适合内网定位,就像你公司内部的第几栋第几层,公司内部人明白,但是直接发给别人,别人是找不到你的。如果你要对外部提供服务,比如百度这种,你就得有公网的IP地址,这个IP地址一般由网络服务运营商提供,比如你们公司使用联通上网,那就可以让联通给你分配一个公网IP地址,绑定到你们公司的网关服务器上,网关服务器就像电话总机,公司内部的所有网络通信都要通过它,然后再在网关上设置转发规则,将网络请求转发到提供网络服务的机器上。
2、有了IP地址之后,浏览器就会向这个IP地址发起请求,通过操作系统打包成IP请求包,然后发送到网络上。网络传输有一套完整的路由协议,它会根据你提供的IP地址,经过路由器的层层转发,最终抵达绑定该IP的计算机。
3、计算机上可能部署了多个网络应用程序,这个请求应该发给哪个程序呢?这里有一个端口的概念,每个网络应用程序启动的时候可以绑定一个或多个端口,不同的网络应用程序绑定的端口不能重复,再次绑定时会提示端口被占用。通过在请求中指定端口,就可以将消息发送到正确的网络处理程序。
但是我们访问百度的时候没有输入端口啊?这是因为默认不输入就使用80和443端口,http使用80,https使用443。我们在启动网络程序的时候一定要绑定一个端口的,当然有些框架会自动选择一个计算机上未使用的端口。
localhost和127.0.0.1的区别是什么?
有了上边的知识储备,我们就可以很轻松的搞懂这个问题了。
localhost是域名,上文已经说过了。
127.0.0.1 呢?是IP地址,当前机器的本地IP地址,且只能在本机使用,你的计算机不联网也可以用这个IP地址,就是为了方便开发测试网络程序的。我们调试时启动的程序就是绑定到这个IP地址的。
这里简单说下,我们经常看到的IP地址一般都是类似 X.X.X.X 的格式,用"."分成四段。其实它是一个32位的二进制数,分成四段后,每一段是8位,然后每一段再转换为10进制的数进行显示。
那localhost是怎么解析到127.0.0.1的呢?经过DNS了吗?没有。每台计算机都可以使用localhost和127.0.0.1,这没办法让DNS来做解析。
那就让每台计算机自己解决了。每台计算机上都有一个host文件,其中写死了一些DNS解析规则,就包括 localhost 到 127.0.0.1 的解析规则,这是一个约定俗成的规则。
如果你不想用localhost,那也可以,随便起个名字,比如 wodehost,也解析到 127.0.0.1 就行了。
甚至你想使用 baidu.com 也完全可以,只是只能自己自嗨,对别人完全没有影响。
域名的等级划分
localhost不太像我们平常使用的域名,比如 http://www.juejin.cn 、baidu.com、csdn.net, 这里边的 www、cn、com、net都是什么意思?localhost为什么不需要?
域名其实是分等级的,按照等级可以划分为顶级域名、二级域名和三级域名...
顶级域名(TLD):顶级域名是域名系统中最高级别的域名。它位于域名的最右边,通常由几个字母组成。顶级域名分为两种类型:通用顶级域名和国家顶级域名。常见的通用顶级域名包括表示工商企业的.com、表示网络提供商的.net、表示非盈利组织的.org等,而国家顶级域名则代表特定的国家或地区,如.cn代表中国、.uk代表英国等。
二级域名(SLD):二级域名是在顶级域名之下的一级域名。它是由注册人自行选择和注册的,可以是个性化的、易于记忆的名称。例如,juejin.cn 就是二级域名。我们平常能够申请到的也是这种。目前来说申请 xxx.com、xxx.net、xxx.cn等等域名,其实大家不太关心其顶级域名com\net\cn代表的含义,看着简短好记是主要诉求。
三级域名(3LD):三级域名是在二级域名之下的一级域名。它通常用于指向特定的服务器或子网。例如,在blog.example.com中,blog就是三级域名。www是最常见的三级域名,用于代表网站的主页或主站点,不过这只是某种流行习惯,目前很多网站都推荐直接使用二级域名访问了。
域名级别还可以进一步细分,大家可以看看企业微信开放平台这个域名:developer.work.weixin.qq.com,com代表商业,qq代表腾讯,weixin代表微信,work代表企业微信,developer代表开发者。这种逐层递进的方式有利于域名的分配管理。
按照上边的等级定义,我们可以说localhost是一个顶级域名,只不过它是保留的顶级域,其唯一目的是用于访问当前计算机。
多网站共用一个IP和端口
上边我们说不同的网络程序不能使用相同的端口,其实是有办法突破的。
以前个人博客比较火的时候,大家都喜欢买个虚拟主机,然后部署个开源的博客程序,抒发一下自己的感情。为了挣钱,虚拟主机的服务商会在一台计算机上分配N多个虚拟主机,大家使用各自的域名和默认的80端口进行访问,也都相安无事。这是怎么做到的呢?
如果你有使用Nginx、Apache或者IIS等Web服务器的相关经验,你可能会接触到主机头这个概念。主机头其实就是一个域名,通过设置主机头,我们的程序就可以共用1个网络端口。
首先在Nginx等Web程序中部署网站时,我们会进行一些配置,此时在主机头中写入网站要使用的域名。
然后Nginx等Web服务器启动的时候,会把80端口占为己有。
然后当某个网站的请求到达Nginx的80端口时,它会根据请求中携带的域名找到配置了对应主机头的网络程序。
然后再转发到这个网络程序,如果网络程序还没有启动,Nginx会把它拉起来。
私有IP地址
除了127.0.0.1,其实还有很多私有IP地址,比如常见的 192.168.x.x。这些私有IP地址大部分都是为了在局域网内使用而预留的,因为给每台计算机都分配一个独立的IP不太够用,所以只要局域网内不冲突,大家就可劲的用吧。你公司可以用 192.168.1.1,我公司也可以用192.168.1.1,但是如果你要访问我,就得通过公网IP进行转发。
大家常用的IPv4私有IP地址段分为三类:
A类:从10.0.0.0至10.255.255.255
B类:从172.16.0.0至172.31.255.255
C类:从192.168.0.0至192.168.255.255。
这些私有IP地址仅供局域网内部使用,不能在公网上使用。
--
除了上述三个私有的IPv4地址段外,还有一些保留的IPv4地址段:
用于本地回环测试的127.0.0.0至127.255.255.255地址段,其中就包括题目中的127.0.0.1,如果你喜欢也可以给自己分配一个127.0.0.2的IP地址,效果和127.0.0.1一样。
用于局域网内部的169.254.0.0至169.254.255.255地址段,这个很少接触到,如果你的电脑连局域网都上不去,可能会看到这个IP地址,它是临时分配的一个局域网地址。
这些地址段也都不能在公网上使用。
--
近年来,还有一个现象,就是你家里或者公司里上网时,光猫或者路由器对外的IPv4地址也不是公网IP了,这时候获得的可能是一个类似 100.64.x.x 的地址,这是因为随着宽带的普及,运营商手里的公网IP也不够了,所以运营商又加了一层局域网,而100.64.0.0 这个网段是专门分给运营商做局域网用的。如果你使用阿里云等公有云,一些云产品的IP地址也可能是这个,这是为了将客户的私有网段和公有云厂商的私有网段进行有效的区分。
--
其实还有一些不常见的专用IPv4地址段,完整的IP地址段定义可以看这里:http://www.iana.org/assignments…
IPv6
你可能也听说过IPv6,因为IPv4可分配的地址太少了,不够用,使用IPv6甚至可以为地球上的每一粒沙子分配一个IP。只是喊了很多年,大家还是喜欢用IPv4,这里边原因很多,这里就不多谈了。
IPv6地址类似:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX
它是128位的,用":"分成8段,每个X是一个16进制数(取值范围:0-F),IPv6地址空间相对于IPv4地址有了极大的扩充。比如:2001:0db8:3c4d:0015:0000:0000:1a2f:1a2b 就是一个有效的IPv6地址。
关于IPv6这里就不多说了,有兴趣的可以再去研究下。
关注萤火架构,加速技术提升!
来源:juejin.cn/post/7321049446443417638
url请求参数带有特殊字符“%、#、&”时,参数被截断怎么办?
是的,最近又踩坑了!
事情是这样的,我们测试小姐姐在一个全局搜索框里输入了一串特殊字符“%%%”,然后进行搜索,结果报错了。而输入常规字符,均可以正常搜索。
一排查,发现特殊字符“%%%”并未成功传给后端。
我们的这个全局搜索功能是需要跳转页面才能查看到搜索结果的。所以,搜索条件是作为参数拼接在页面url上的。
正常的传参:
当输入的是特殊字符“%、#、&”时,参数丢失
也就是说,当路由请求参数带有浏览器url中的特殊含义字符时,参数会被截断,无法正常获取参数。
那么怎么解决这个问题呢?
方案一:encodeURIComponent/decodeURIComponent
拼接参数时,利用encodeURIComponent()进行编码,接收参数时,利用decodeURIComponent()进行解码。
// 编码
this.$router.push({path: `/crm/global-search/search-result?type=${selectValue}&text=${encodeURIComponent(searchValue)}`});
// 解码
const text = decodeURIComponent(this.$route.query.text)
此方法对绝大多数特殊字符都适用,但是唯独输入“%”进行搜索时不行,报错如下。
所以在编码之前,还需进行一下如下转换:
this.$router.push({path: `/crm/global-search/search-result?type=${selectValue}&text=${encodeURIComponent(encodeSpecialChar(searchValue))}`});
/**
* @param {*} char 字符串
* @returns
*/
export const encodeSpecialChar = (char) => {
// #、&可以不用参与处理
const encodeArr = [{
code: '%',
encode: '%25'
},{
code: '#',
encode: '%23'
}, {
code: '&',
encode: '%26'
},]
return char.replace(/[%?#&=]/g, ($) => {
for (const k of encodeArr) {
if (k.code === $) {
return k.encode
}
}
})
}
方案二: qs.stringify()
默认情况下,qs.stringify()方法会使用encodeURIComponent方法对特殊字符进行编码,以保证URL的合法性。
const qs = require('qs');
const searchObj = {
type: selectValue,
text: searchValue
};
this.$router.push({path: `/crm/global-search/search-result?${qs.stringify(searchObj)}`});
使用了qs.stringify()方法,就无需使用encodeSpecialChar方法进行转换了。
来源:juejin.cn/post/7332048519156776979
前端代码重复度检测
在前端开发中,代码的重复度是一个常见的问题。重复的代码不仅增加了代码的维护成本,还可能导致程序的低效运行。为了解决这个问题,有许多工具和技术被用来检测和消除代码重复。其中一个被广泛使用的工具就是jscpd
。
jscpd简介
jscpd
是一款开源的JavaScript
的工具库,用于检测代码重复的情况,针对复制粘贴的代码检测很有效果。它可以通过扫描源代码文件,分析其中的代码片段,并比较它们之间的相似性来检测代码的重复度。jscpd
支持各种前端框架和语言,包括HTML、CSS和JavaScript等150种的源码文件格式。无论是原生的JavaScript、CSS、HTML代码,还是使用typescript
、scss
、vue
、react
等代码,都能很好的检测出项目中的重复代码。
开源仓库地址:github.com/kucherenko/jscpd/tree/master
如何使用
使用jscpd
进行代码重复度检测非常简单。我们需要安装jscpd
。可以通过npm
或yarn
来安装jscpd
。
npm install -g jscpd
yarn global add jscpd
安装完成后,我们可以在终端运行jscpd命令,指定要检测的代码目录或文件。例如,我们可以输入以下命令来检测当前目录下的所有JavaScript文件:
jscpd .
指定目录检测:
jscpd /path/to/code
在命令行执行成功后的效果如下图所示:
简要说明一下对应图中的字段内容:
- Clone found (javascript):
显示找到的重复代码块,这里是javascript文件。并且会显示重复代码在文件中具体的行数,便于查找。 - Format:文件格式,这里是 javascript,还可以是 scss、markup 等。
- Files analyzed:已分析的文件数量,统计被检测中的文件数量。
- Total lines:所有文件的总行数。
- Total tokens:所有的token数量,一行代码一般包含几个到几十个不等的token数量。
- Clones found:找到的重复块数量。
- Duplicated lines:重复的代码行数和占比。
- Duplicated tokens:重复的token数量和占比。
- Detection time:检测耗时。
工程配置
以上示例是比较简单直接检测单个文件或文件夹。当下主流的前端项目大多都是基于脚手架生成或包含相关前端工程化的文件,由于很多文件是辅助工具如依赖包、构建脚本、文档、配置文件等,这类文件都不需要检测,需要排除。这种情况下的工程一般使用配置文件的方式,通过选项配置规范 jscpd
的使用。
jscpd
的配置选项可以通过以下两种方式创建,增加的内容都一致无需区分对应的前端框架。
在项目根目录下创建配置文件 .jscpd.json
,然后在该文件中增加具体的配置选项:
{
"threshold": 0,
"reporters": ["html", "console", "badge"],
"ignore": ["**/__snapshots__/**"],
"absolute": true
}
也可直接在 package.json
文件中添加jscpd
:
{
...
"jscpd": {
"threshold": 0.1,
"reporters": ["html", "console", "badge"],
"ignore": ["**/__snapshots__/**"],
"absolute": true,
"gitignore": true
}
...
}
简要介绍一下上述配置字段含义:
- threshold:表示重复度的阈值,超过这个值,就会输出错误报警。如阈值设为 10,当重复度为18.1%时,会提示以下错误❌,但代码的检测会正常完成。
ERROR: jscpd found too many duplicates (18.1%) over threshold (10%)
- reporters:表示生成结果检测报告的方式,一般有以下几种:
- console:控制台打印输出
- consoleFull:控制台完整打印重复代码块
- json:输出
json
格式的报告 - xml:输出
xml
格式的报告 - csv:输出
csv
格式的报告 - markdown:输出带有
markdown
格式的报告 - html:生成
html
报告到html
文件夹 - verbose:输出大量调试信息到控制台
- ignore:检测忽略的文件或文件目录,过滤一些非业务代码,如依赖包、文档或静态文件等
- format:需要进行重复度检测的源代码格式,目前支持150多种,我们常用的如 javascript、typescript、css 等
- absolute:在检测报告中使用绝对路径
除此之外还有很多其他的配置,有兴趣的可以看源码文档中有详细的介绍。
检测报告
完成以上jscpd
配置后执行以下命令即可输出对应的重复检测报告。运行完毕后,jscpd
会生成一个报告,展示每个重复代码片段的信息。报告中包含了重复代码的位置、相似性百分比和代码行数等详细信息。通过这些信息,我们可以有针对性的进行代码重构。
jscpd ./src -o 'report'
项目中的业务代码通常会选择放在 ./src
目录下,所以可以直接检测该目录下的文件,如果是放在其他目录下根据实际情况调整即可。
通过命令行参数-o 'report'
输出检测报告到项目根目录下的 report
文件夹中,这里的report
也可以自定义其他目录名称,输出的目录结构如下所示:
生成的报告页面如下所示:
项目概览数据:
具体重复代码的位置和行数:
默认检测重复代码的行数(5行)和tokens(50)比较小,所以产生的重复代码块可能比较多,在实际使用中可以针对检测范围进行设置,如下设置参数供参考:
- 最小tokens:
--min-tokens
,简写-k
- 最小行数:
--min-lines
,简写-l
- 最大行数:
--max-lines
,简写-x
jscpd ./src --min-tokens 200 --min-lines 20 -o 'report'
为了更便捷的使用此命令,可将这段命令集成到 package.json
中的 scripts
中,后续只需执行 npm run jscpd
即可执行检测。如下所示:
"scripts": {
...
"jscpd": "jscpd ./src --min-tokens 200 --min-lines 20 -o 'report'",
...
}
忽略代码块
上面所提到的ignore
可以忽略某个文件或文件夹,还有一种忽略方式是忽略文件中的某一块代码。由于一些重复代码在实际情况中是必要的,可以使用代码注释标识的方式忽略检测,在代码的首尾位置添加注释,jscpd:ignore-start
和 jscpd:ignore-end
包裹代码即可。
在js代码中使用方式:
/* jscpd:ignore-start */
import lodash from 'lodash';
import React from 'react';
import {User} from './models';
import {UserService} from './services';
/* jscpd:ignore-end */
在CSS和各种预处理中与js中的用法一致:
/* jscpd:ignore-start */
.style {
padding: 40px 0;
font-size: 26px;
font-weight: 400;
color: #464646;
line-height: 26px;
}
/* jscpd:ignore-end */
在html代码中使用方式:
<!--
// jscpd:ignore-start
-->
<meta data-react-helmet="true" name="theme-color" content="#cb3837"/>
<link data-react-helmet="true" rel="stylesheet" href="https://static.npmjs.com/103af5b8a2b3c971cba419755f3a67bc.css"/>
<link data-react-helmet="true" rel="apple-touch-icon" sizes="120x120" href="https://static.npmjs.com/58a19602036db1daee0d7863c94673a4.png"/>
<link data-react-helmet="true" rel="icon" type="image/png" href="https://static.npmjs.com/b0f1a8318363185cc2ea6a40ac23eeb2.png" sizes="32x32"/>
<!--
// jscpd:ignore-end
-->
总结
jscpd
是一款强大的前端本地代码重复度检测工具。它可以帮助开发者快速发现代码重复问题,简单的配置即可输出直观的代码重复数据,通过解决重复的代码提高代码的质量和可维护性。
使用jscpd
我们可以有效地优化前端开发过程,提高代码的效率和性能。希望本文能够对你了解基于jscpd
的前端本地代码重复度检测有所帮助。
看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~
专注前端开发,分享前端相关技术干货,公众号:南城大前端(ID: nanchengfe)
来源:juejin.cn/post/7288699185981095988
2种纯前端换肤方案
前言
换肤功能是一项普遍的需求,尤其是在夜晚,用户更倾向于使用暗黑模式。在我负责的公司项目中,每个项目都有换肤功能的需求。
过去,我主要使用 SCSS 变量,并利用其提供的函数,如 @each
、map-get
来实现换肤功能。但因其使用成本高,只能适用于SCSS项目,于是后来我改用 CSS 变量来实现换肤。这样无论是基于 LESS 的 React 项目,还是基于 SCSS 的 Vue 项目,都能应用换肤功能。并且使用时只需调用var
函数,降低了使用成本。
Demo地址:github.com/cwjbjy/vite…
1. 一键换肤
1. 前置知识
CSS变量:声明自定义CSS属性,它包含的值可以在整个文档中重复使用。属性名需要以两个减号(--)开始,属性值则可以是任何有效的 CSS 值
--fontColor:'#fff'
Var函数:用于使用CSS变量。第一个参数为CSS变量名称,第二个可选参数作为默认值
color: var(--fontColor);
CSS属性选择器:匹配具有特定属性或属性值的元素。例如[data-theme='black'],将选择所有 data-theme 属性值为 'black' 的元素
2. 定义主题色
1. 新建src/assets/theme/theme-default.css
这里定义字体颜色与布局的背景色,更多CSS变量可根据项目的需求来定义
[data-theme='default'] {
/* 字体 */
--font-primary: #fff;
--font-highlight: #434a50;
/* 布局 */
--background-header: #2f3542;
--background-aside: #545c64;
--background-main: #0678be;
}
2. 新建src/assets/theme/theme-black.css
再定义一套暗黑主题色
[data-theme='black'] {
/* 字体 */
--font-primary: #fff;
--font-highlight: #434a50;
/* 布局 */
--background-header: #303030;
--background-aside: #303030;
--background-main: #393939;
}
3. 新建src/assets/theme/index.css
在index.css文件中导出全部主题色
@import './theme-default.css';
@import './theme-black.css';
4. 引入全局样式
在入口文件引入样式,比如我这里是main.tsx
import '@/assets/styles/theme/index.css';
3. 在html标签上增加自定义属性
修改index.html,在html标签上增加自定义属性data-theme
<html lang="en" data-theme="default"></html>
这里使用data-theme是为了被CSS属性选择器[data-theme='default']选中,也可更换为其他自定义属性,只需与CSS属性选择器对应上即可。
4. 修改CSS主题色
关键点:监听change事件,使用document.documentElement.setAttribute动态修改data-theme属性,然后CSS属性选择器将自动选择对应的css变量
<template>
<div>
<select name="pets" @change="handleChange">
<option value="default">默认色</option>
<option value="black">黑色</option>
</select>
<div>登录页面</div>
</div>
</template>
<script setup lang="ts">
const handleChange = (e: Event) => {
window.document.documentElement.setAttribute('data-theme', (e.target as HTMLSelectElement).value);
};
</script>
<style lang="scss">
body {
color: var(--font-primary);
background-color: var(--background-main);
}
</style>
效果图,默认色:
效果图,暗黑色:
5. 修改JS主题色
切换主题色,除了需要修改css样式,有时也需在js文件中修改样式,例如修改echarts的配置文件,来改变柱状图、饼图等的颜色。
1. 新建src/config/theme.js
定义图像的颜色,这里定义字体的颜色,默认情况下字体为黑色,暗黑模式下,字体为白色
const themeColor = {
default: {
font: '#333',
},
black: {
font: '#fff',
},
};
export default themeColor;
2. 修改vue文件
关键点:
- 定义主题色TS类型,规定默认和暗黑两种:
type ThemeTypes = 'default' | 'black';
- 定义theme响应式变量,用来记录当前主题色:
const theme = ref<ThemeTypes>('default');
- 监听change事件,将选中的值赋给theme:
theme.value = selectTheme;
- 使用watch进行监听,如果theme改变,则重新绘制echarts图形
完整的vue文件:
<template>
<div>
<select name="pets" @change="handleChange">
<option value="default">默认色</option>
<option value="black">黑色</option>
</select>
<div>登录页面</div>
<div ref="echartRef" class="myChart"></div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue';
import themeColor from '@/config/theme';
import * as echarts from 'echarts';
type ThemeTypes = 'default' | 'black';
const echartRef = ref<HTMLDivElement | null>(null);
const theme = ref<ThemeTypes>('default');
const handleChange = (e: Event) => {
const selectTheme = (e.target as HTMLSelectElement).value as ThemeTypes;
theme.value = selectTheme;
window.document.documentElement.setAttribute('data-theme', selectTheme);
};
const drawGraph = () => {
let echartsInstance = echarts.getInstanceByDom(echartRef.value!);
if (!echartsInstance) {
echartsInstance = echarts.init(echartRef.value);
}
echartsInstance.clear();
var option = {
color: ['#3398DB'],
title: {
text: '柱状图',
left: 'center',
textStyle: {
color: themeColor[theme.value].font,
},
},
xAxis: [
{
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
axisLabel: {
show: true,
color: themeColor[theme.value].font,
},
nameTextStyle: {
color: themeColor[theme.value].font,
},
},
],
yAxis: [
{
type: 'value',
axisLabel: {
show: true,
color: themeColor[theme.value].font,
},
nameTextStyle: {
color: themeColor[theme.value].font,
},
},
],
series: [
{
name: '直接访问',
type: 'bar',
barWidth: '60%',
data: [10, 52, 200, 334, 390, 330, 220],
},
],
};
echartsInstance.setOption(option);
};
onMounted(() => {
drawGraph();
});
watch(theme, () => {
drawGraph();
});
</script>
<style lang="scss">
body {
color: var(--font-primary);
background-color: var(--background-main);
}
.myChart {
width: 300px;
height: 300px;
}
</style>
2. 一键变灰
在特殊的日子里,网页有整体变灰色的需求。可以使用filter 的 grayscale() 改变图像灰度,值在 0% 到 100% 之间,值为0%展示原图,值为100% 则完全转为灰度图像
body {
filter: grayscale(1); //1相当于100%
}
结尾
本文只是介绍大概的思路,更多的功能可根据业务增加。例如将主题色theme存储到pinia上,应用到全局上;将主题色存储到localStorage上,在页面刷新时,防止主题色恢复默认。
本文可结合以下文章阅读:
如果有更多的换肤方案,欢迎在留言区留言讨论。我会根据留言区内容实时更新。
来源:juejin.cn/post/7342527074526019620
实现一个支持@的输入框
近期产品期望在后台发布帖子或视频时,需要添加 @用户 的功能,以便用户收到通知,例如“xxx在xxx提及了您!”。然而,现有的开源库未能满足我们的需求,例如 ant-design 的 Mentions 组件:
但是不难发现跟微信飞书对比下,有两个细节没有处理。
- @用户没有高亮
- 在删除时没有当做一个整体去删除,而是单个字母删除,首先不谈用户是否想要整体删除,在这块有个模糊查询的功能,如果每删一个字母之后去调接口查询数据库造成一些不必要的性能开销,哪怕加上防抖。
然后也是找了其他的库都没达到产品的期望效果,那么好,自己实现一个,先看看最终实现的效果:
封装之后使用:
<AtInput
height={150}
onRequest={async (searchStr) => {
const { data } = await UserFindAll({ nickname: searchStr });
return data?.list?.map((v) => ({
id: v.uid,
name: v.nickname,
wechatAvatarUrl: v.wechatAvatarUrl,
}));
}}
onChange={(content, selected) => {
setAtUsers(selected);
}}
/>
那么实现这么一个输入框大概有以下几个点:
- 高亮效果
- 删除/选中用户时需要整体删除
- 监听@的位置,复制给弹框的坐标,联动效果
- 最后我需要拿到文本内容,并且需要拿到@那些用户,去做表单提交
大多数文本输入框我们会使用input,或者textarea,很明显以上1,2两点实现不了,antd也是使用的textarea,所以也是没有实现这两个效果。所以这块使用富文本编辑,设置contentEditable,将其变为可编辑去做。输入框以及选择器的dom就如下:
<div style={{ height, position: 'relative' }}>
{/* 编辑器 */}
<div
id="atInput"
ref={atRef}
className={'editorDiv'}
contentEditable
onInput={editorChange}
onClick={editorClick}
/>
{/* 选择用户框 */}
<SelectUser
options={options}
visible={visible}
cursorPosition={cursorPosition}
onSelect={onSelect}
/>
</div>
实现思路:
- 监听输入@,唤起选择框。
- 截取@xxx的xxx作为搜素的关键字去查询接口
- 选择用户后需要将原先输入的 @xxx 替换成 @姓名,并且将@的用户缓存起来
- 选择文本框中的姓名时需要变为整体选中状态,这块依然可以给标签设置为不可编辑状态就可实现,contentEditable=false,即可实现整体删除,在删除的同时需要将当前用户从之前缓存的@过的用户数组删除
- 那么可以拿到输入框的文本,@的用户, 最后将数据抛给父组件就完成了
以上提到了监听@文本变化,通常绑定onChange事件就行,但是还有一种用户通过点击移动光标,这块需要绑定change,click两个时间,他们里边的逻辑基本一样,只需要额外处理点击选中输入框中用户时,整体选中g功能,那么代码如下:
const onObserveInput = () => {
let cursorBeforeStr = '';
const selection: any = window.getSelection();
if (selection?.focusNode?.data) {
cursorBeforeStr = selection.focusNode?.data.slice(0, selection.focusOffset);
}
setFocusNode(selection.focusNode);
const lastAtIndex = cursorBeforeStr?.lastIndexOf('@');
setCurrentAtIdx(lastAtIndex);
if (lastAtIndex !== -1) {
getCursorPosition();
const searchStr = cursorBeforeStr.slice(lastAtIndex + 1);
if (!StringTools.isIncludeSpacesOrLineBreak(searchStr)) {
setSearchStr(searchStr);
fetchOptions(searchStr);
setVisible(true);
} else {
setVisible(false);
setSearchStr('');
}
} else {
setVisible(false);
}
};
const selectAtSpanTag = (target: Node) => {
window.getSelection()?.getRangeAt(0).selectNode(target);
};
const editorClick = async (event) => {
onObserveInput();
// 判断当前标签名是否为span 是的话选中当做一个整体
if (e.target.localName === 'span') {
selectAtSpanTag(e.target);
}
};
const editorChange = (event) => {
const { innerText } = event.target;
setContent(innerText);
onObserveInput();
};
每次点击或者文本改变时都会去调用onObserveInput,以上onObserveInput该方法中主要做了以下逻辑:
- 在此之前需要先了解 Selection的一些方法
- 通过getSelection方法可以获取光标的偏移位置,那么可以截取光标之前的字符串,并且使用lastIndexOf从后向前查找最后一个“@”符号,并记录他的下标,那么有了【光标之前的字符串】,【@的下标】就可以拿到到@之后用于过滤用户的关键字,并将其缓存起来。
- 唤起选择器,并通过关键字去过滤用户。这块涉及到一个选择器的位置,直接使用window.getSelection()?.getRangeAt(0).getBoundingClientRect()去获取光标的位置拿到的是光标相对于窗口的坐标,直接用这个坐标会有问题,比如滚动条滚动时,这个选择器发生位置错乱,所以这块同时去拿输入框的坐标,去做一个相减,这样就可以实现选择器跟着@符号联动的效果。
const getCursorPosition = () => {
// 坐标相对浏览器的坐标
const { x, y } = window.getSelection()?.getRangeAt(0).getBoundingClientRect() as any;
// 获取编辑器的坐标
const editorDom = window.document.querySelector('#atInput');
const { x: eX, y: eY } = editorDom?.getBoundingClientRect() as any;
// 光标所在位置
setCursorPosition({ x: x - eX, y: y - eY });
};
选择器弹出后,那么下面就到了选择用户之后的流程了,
/**
* @param id 唯一的id 可以uid
* @param name 用户姓名
* @param color 回显颜色
* @returns
*/
const createAtSpanTag = (id: number | string, name: string, color = 'blue') => {
const ele = document.createElement('span');
ele.className = 'at-span';
ele.style.color = color;
ele.id = id.toString();
ele.contentEditable = 'false';
ele.innerText = `@${name}`;
return ele;
};
/**
* 选择用户时回调
*/
const onSelect = (item: Options) => {
const selection = window.getSelection();
const range = selection?.getRangeAt(0) as Range;
// 选中输入的 @关键字 -> @郑
range.setStart(focusNode as Node, currentAtIdx!);
range.setEnd(focusNode as Node, currentAtIdx! + 1 + searchStr.length);
// 删除输入的 @关键字
range.deleteContents();
// 创建元素节点
const atEle = createAtSpanTag(item.id, item.name);
// 插入元素节点
range.insertNode(atEle);
// 光标移动到末尾
range.collapse();
// 缓存已选中的用户
setSelected([...selected, item]);
// 选择用户后重新计算content
setContent(document.getElementById('atInput')?.innerText as string);
// 关闭弹框
setVisible(false);
// 输入框聚焦
atRef.current.focus();
};
选择用户的时候需要做的以下以下几点:
- 删除之前的@xxx字符
- 插入不可编辑的span标签
- 将当前选择的用户缓存起来
- 重新获取输入框的内容
- 关闭选择器
- 将输入框重新聚焦
最后
在选择的用户或者内容发生改变时将数据抛给父组件
const getAttrIds = () => {
const spans = document.querySelectorAll('.at-span');
let ids = new Set();
spans.forEach((span) => ids.add(span.id));
return selected.filter((s) => ids.has(s.id));
};
/** @的用户列表发生改变时,将最新值暴露给父组件 */
useEffect(() => {
const selectUsers = getAttrIds();
onChange(content, selectUsers);
}, [selected, content]);
完整组件代码
输入框主要逻辑代码:
let timer: NodeJS.Timeout | null = null;
const AtInput = (props: AtInputProps) => {
const { height = 300, onRequest, onChange, value, onBlur } = props;
// 输入框的内容=innerText
const [content, setContent] = useState<string>('');
// 选择用户弹框
const [visible, setVisible] = useState<boolean>(false);
// 用户数据
const [options, setOptions] = useState<Options[]>([]);
// @的索引
const [currentAtIdx, setCurrentAtIdx] = useState<number>();
// 输入@之前的字符串
const [focusNode, setFocusNode] = useState<Node | string>();
// @后关键字 @郑 = 郑
const [searchStr, setSearchStr] = useState<string>('');
// 弹框的x,y轴的坐标
const [cursorPosition, setCursorPosition] = useState<Position>({
x: 0,
y: 0,
});
// 选择的用户
const [selected, setSelected] = useState<Options[]>([]);
const atRef = useRef<any>();
/** 获取选择器弹框坐标 */
const getCursorPosition = () => {
// 坐标相对浏览器的坐标
const { x, y } = window.getSelection()?.getRangeAt(0).getBoundingClientRect() as any;
// 获取编辑器的坐标
const editorDom = window.document.querySelector('#atInput');
const { x: eX, y: eY } = editorDom?.getBoundingClientRect() as any;
// 光标所在位置
setCursorPosition({ x: x - eX, y: y - eY });
};
/**获取用户下拉列表 */
const fetchOptions = (key?: string) => {
if (timer) {
clearTimeout(timer);
timer = null;
}
timer = setTimeout(async () => {
const _options = await onRequest(key);
setOptions(_options);
}, 500);
};
useEffect(() => {
fetchOptions();
// if (value) {
// /** 判断value中是否有at用户 */
// const atUsers: any = StringTools.filterUsers(value);
// setSelected(atUsers);
// atRef.current.innerHTML = value;
// setContent(value.replace(/<\/?.+?\/?>/g, '')); //全局匹配内html标签)
// }
}, []);
const onObserveInput = () => {
let cursorBeforeStr = '';
const selection: any = window.getSelection();
if (selection?.focusNode?.data) {
cursorBeforeStr = selection.focusNode?.data.slice(0, selection.focusOffset);
}
setFocusNode(selection.focusNode);
const lastAtIndex = cursorBeforeStr?.lastIndexOf('@');
setCurrentAtIdx(lastAtIndex);
if (lastAtIndex !== -1) {
getCursorPosition();
const searchStr = cursorBeforeStr.slice(lastAtIndex + 1);
if (!StringTools.isIncludeSpacesOrLineBreak(searchStr)) {
setSearchStr(searchStr);
fetchOptions(searchStr);
setVisible(true);
} else {
setVisible(false);
setSearchStr('');
}
} else {
setVisible(false);
}
};
const selectAtSpanTag = (target: Node) => {
window.getSelection()?.getRangeAt(0).selectNode(target);
};
const editorClick = async (e?: any) => {
onObserveInput();
// 判断当前标签名是否为span 是的话选中当做一个整体
if (e.target.localName === 'span') {
selectAtSpanTag(e.target);
}
};
const editorChange = (event: any) => {
const { innerText } = event.target;
setContent(innerText);
onObserveInput();
};
/**
* @param id 唯一的id 可以uid
* @param name 用户姓名
* @param color 回显颜色
* @returns
*/
const createAtSpanTag = (id: number | string, name: string, color = 'blue') => {
const ele = document.createElement('span');
ele.className = 'at-span';
ele.style.color = color;
ele.id = id.toString();
ele.contentEditable = 'false';
ele.innerText = `@${name}`;
return ele;
};
/**
* 选择用户时回调
*/
const onSelect = (item: Options) => {
const selection = window.getSelection();
const range = selection?.getRangeAt(0) as Range;
// 选中输入的 @关键字 -> @郑
range.setStart(focusNode as Node, currentAtIdx!);
range.setEnd(focusNode as Node, currentAtIdx! + 1 + searchStr.length);
// 删除输入的 @关键字
range.deleteContents();
// 创建元素节点
const atEle = createAtSpanTag(item.id, item.name);
// 插入元素节点
range.insertNode(atEle);
// 光标移动到末尾
range.collapse();
// 缓存已选中的用户
setSelected([...selected, item]);
// 选择用户后重新计算content
setContent(document.getElementById('atInput')?.innerText as string);
// 关闭弹框
setVisible(false);
// 输入框聚焦
atRef.current.focus();
};
const getAttrIds = () => {
const spans = document.querySelectorAll('.at-span');
let ids = new Set();
spans.forEach((span) => ids.add(span.id));
return selected.filter((s) => ids.has(s.id));
};
/** @的用户列表发生改变时,将最新值暴露给父组件 */
useEffect(() => {
const selectUsers = getAttrIds();
onChange(content, selectUsers);
}, [selected, content]);
return (
<div style={{ height, position: 'relative' }}>
{/* 编辑器 */}
<div id="atInput" ref={atRef} className={'editorDiv'} contentEditable onInput={editorChange} onClick={editorClick} />
{/* 选择用户框 */}
<SelectUser options={options} visible={visible} cursorPosition={cursorPosition} onSelect={onSelect} />
</div>
);
};
选择器代码
const SelectUser = React.memo((props: SelectComProps) => {
const { options, visible, cursorPosition, onSelect } = props;
const { x, y } = cursorPosition;
return (
<div
className={'selectWrap'}
style={{
display: `${visible ? 'block' : 'none'}`,
position: 'absolute',
left: x,
top: y + 20,
}}
>
<ul>
{options.map((item) => {
return (
<li
key={item.id}
onClick={() => {
onSelect(item);
}}
>
<img src={item.wechatAvatarUrl} alt="" />
<span>{item.name}</span>
</li>
);
})}
</ul>
</div>
);
});
export default SelectUser;
以上就是实现一个支持@用户的输入框功能,就目前而言,比较死板,不支持自定义颜色,自定义选择器等等,未来,可以进一步扩展功能,例如添加@用户的高亮样式定制、支持键盘快捷键操作等,从而提升用户体验和功能性。
来源:juejin.cn/post/7357917741909819407
Vue.js 自动路由:告别手动配置,让开发更轻松!
在使用 Vue.js 开发项目的时候,我最头疼的就是创建路由,尤其是项目越来越大的时候,管理 route.ts
或 route.js
文件简直是一场噩梦!
我曾经做过一个页面超级多的项目,每新增一个页面就要更新路由,删除页面也要更新路由文件,不然就会报错,真是烦死人了!
所以,我开始寻找自动生成路由的方法。我在网上搜了很久,但大部分结果都是针对 Webpack 和 Vue 2 的,很难找到适合我的方案。最后,我在 Vue 的 GitHub 仓库的讨论区里提问,终于找到了答案!
那就是 Unplugin Vue Router
! 它可以为 Vue 3 实现基于文件的自动路由,而且支持 TypeScript,设置起来也超级简单! 虽然官方说它还在实验阶段,但用起来已经很方便了。
创建项目,安装插件
首先,我们创建一个新的 Vue 项目。 相信大家都很熟悉用 Vue CLI 创建项目了,这里就不赘述了,不熟悉的小伙伴可以去看看 Vue.js 官网的快速入门指南。
pnpm create vue@latest
我创建项目的时候选择了 TypeScript 和 Vue Router,这样它就会自动生成一些页面和路由。
然后,进入项目目录,安装依赖。我最近开始用 pnpm
来管理依赖,感觉还不错。
pnpm add -D unplugin-vue-router
接下来,更新 vite.config.ts
文件, 注意要把插件放在第 0 个位置 。
import { fileURLToPath, URL } from "node:url";
import VueRouter from "unplugin-vue-router/vite";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
VueRouter({
/* options */
}),
// ⚠️ Vue must be placed after VueRouter()
vue(),
],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
});
然后,更新 env.d.ts
文件,让编辑器能够识别插件的类型。
/// <reference types="vite/client" />
/// <reference types="unplugin-vue-router/client" />
最后,更新路由文件 src/router/index.ts
。
import { createRouter, createWebHistory } from "vue-router";
import { routes, handleHotUpdate } from "vue-router/auto-routes";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
});
if (import.meta.hot) {
handleHotUpdate(router);
}
export default router;
创建页面,自动生成路由
现在,我们可以创建 src/pages
目录了,在这个目录下创建的 Vue 组件会自动变成路由和页面,就像 Nuxt 一样方便!
我们先在 src\pages\about.vue
创建一个关于页面:
<template>
<div>This is the about page</div>
</template>
然后在 src\pages\index.vue
创建首页:
<template>
<div>This is Home Page</div>
</template>
运行 pnpm dev
启动开发服务器,点击 “Home” 链接就会跳转到首页,点击 “About” 链接就会跳转到关于页面。
怎么样,是不是很方便? 如果你不熟悉路由文件夹结构,可以看看这个文档: uvr.esm.is/guide/file-…
动态路由
我们再来试试创建带参数的动态路由。在 src/pages/blog/[id].vue
创建一个组件,内容如下:
<script setup>
const { id } = useRoute().params;
</script>
<template>
<div>This is the blog post with id: {{ id }}</div>
</template>
再次运行 pnpm dev
,然后访问 http://localhost:5173/blog/6
,你就会看到以下内容:
是不是很神奇? 希望这篇简短的博客能帮助你在 Vue.js 的旅程中更轻松地创建路由!
来源:juejin.cn/post/7401354593588199465
JS类型判断的四种方法,你掌握了吗?
引言
JavaScript中有七种原始数据类型和几种引用数据类型,本文将清楚地介绍四种用于类型判断的方法,分别是typeOf
、instanceOf
、Object.prototype.toString.call()
、Array.isArray()
,并介绍其使用方法和判定原理。
typeof
- 可以准确判断除
null
之外的所有原始类型,null
会被判定成object function
类型可以被准确判断为function
,而其他所有引用类型都会被判定为object
let s = '123' // string
let n = 123 // number
let f = true // boolean
let u = undefined // undefined
let nu = null // null
let sy = Symbol(123) // Symbol
let big = 1234n // BigInt
let obj = {}
let arr = []
let fn = function() {}
let date = new Date()
console.log(typeof s); // string typeof后面有无括号都行
console.log(typeof n); // number
console.log(typeof f); // boolean
console.log(typeof u); // undefined
console.log(typeof(sy)); // symbol
console.log(typeof(big)); // bigint
console.log(typeof(nu)); // object
console.log(typeof(obj)); // object
console.log(typeof(arr)); // object
console.log(typeof(date)); // object
console.log(typeof(fn)); // function
判定原理
typeof是通过将值转换为二进制之后,判断其前三位是否为0:都是0则为object,反之则为原始类型。因为原始类型转二进制,前三位一定不都是0;反之引用类型被转换成二进制前三位一定都是0。
null
是原始类型却被判定为object
就是因为它在机器中是用一长串0来表示的,可以把这看作是一个史诗级的bug。
所以用typeof
判断接收到的值是否为一个对象时,还要注意排除null的情况:
function isObject() {
if(typeof(o) === 'object' && o !== null){
return true
}
return false
}
你丢一个值给typeof
,它会告诉你这个字值是什么类型,但是它无法准确告诉你这是一个Array
或是Date
,若想要如此精确地知道一个对象类型,可以用instanceof
告诉你是否为某种特定的类型
instanceof
只能精确地判断引用类型,不能判断原始类型
console.log(obj instanceof Object);// true
console.log(arr instanceof Array);// true
console.log(fn instanceof Function);// true
console.log(date instanceof Date);// true
console.log(s instanceof String);// false
console.log(n instanceof Number);// false
console.log(arr instanceof Object);// true
判定原理
instanceof
既能把数组判定成Array
,又能把数组判定成Object
,究其原因是原型链的作用————顺着数组实例 arr 的隐式原型一直找到了 Object 的构造函数,看下面的代码:
arr.__proto__ = Array.prototype
Array.prototype.__proto__ = Object.prototype
所以我们就知道了,instanceof
能准确判断出一个对象是否为某种类型,就是依靠对象的原型链来查找的,一层又一层地判断直到找到null
为止。
手写instanceOf
根据这个原理,我们可以手写出一个instanceof
:
function myinstanceof(L, R) {
while(L != null) {
if(L.__proto__ === R.prototype){
return true;
}
L = L.__proto__;
}
return false;
}
console.log(myinstanceof([], Array)) // true
console.log(myinstanceof({}, Object)) // true
对象的隐式原型 等于 构造函数的显式原型!可看文章 给我三分钟,带你完全理解JS原型和原型链前言
Object.prototype.toString.call()
可以判断任何数据类型
在浏览器上执行这三段代码,会得到'[object Object]'
,'[object Array]'
,'[object Number]'
var a = {}
Object.prototype.toString.call(a)
var a = {}
Object.prototype.toString.call(a)
var a = 123
Object.prototype.toString.call(a)
原型上的toString的内部逻辑
调用Object.prototype.toString
的时候执行会以下步骤: 参考官方文档:带注释的 ES5
- 如果此值是
undefined
类型,则返回‘[object Undefined]’
- 如果此值是
null
类型,则返回‘[object Null]’
- 将 O 作为
ToObject(this)
的执行结果。toString
执行过程中会调用一个ToObject
方法,执行一个类似包装类的过程,我们访问不了这个方法,是JS自己用的 - 定义一个
class
作为内部属性[[class]]
的值。toString可以读取到这个值并把这个值暴露出来让我们看得见 - 返回由
"[object"
和class
和"]"
组成的字符串
为什么结合call就能准确判断值类型了呢?
① 首先我们要知道Object.prototype.toString(xxx)
往括号中不管传递什么返回结果都是'[object Object]'
,因为根据上面五个步骤来看,它内部会自动执行ToObject()
方法,xxx
会被执行一个类似包装类的过程然后转变成一个对象。所以单独一个Object.prototype.toString(xxx)
不能用来判定值的类型
② 其次了解call方法的核心原理就是:比如foo.call(obj)
,利用隐式绑定的规则,让obj对象拥有foo这个函数的引用,从而让foo函数的this指向obj,执行完foo函数内部逻辑后,再将foo函数的引用从obj上删除掉。手搓一个call的源码就是这样的:
// call方法只允许被函数调用,所以它应该是放在Function构造函数的显式原型上的
Function.prototype.mycall = function(context) {
// 判断调用我的那个哥们是不是函数体
if (typeof this !== 'function') {
return new TypeError(this+ 'is not a function')
}
// this(函数)里面的this => context对象
const fn = Symbol('key') // 定义一个独一无二的fn,防止使用该源码时与其他fn产生冲突
context[fn] = this // 让对象拥有该函数 context={Symbol('key'): foo}
context[fn]() // 触发隐式绑定
delete context[fn]
}
③ 所以Object.prototype.toString.call(xxx)
就相当于 xxx.toString()
,把toString()方法放在了xxx对象上调用,这样就能精准给出xxx的对象类型
toString方法有几个版本:
{}.toString()
得到由"[object" 和 class 和 "]" 组成的字符串
[].toString()
数组的toString方法重写了对象上的toString方法,返回由数组内部元素以逗号拼接的字符串
xx.toString()
返回字符串字面量,比如
let fn = function(){};
console.log( fn.toString() ) // "function () {}"
Array.isArray(x)
只能判断是否是数组,若传进去的x是数组,返回true,否则返回false
总结
typeOf:原始类型除了null都能准确判断,引用类型除了function能准确判断其他都不能。依靠值转为二进制后前三位是否为0来判断
instanceOf:只能把引用类型丢给它准确判断。顺着对象的隐式原型链向上比对,与构造函数的显式原型相等返回true,否则false
Object.prototype.toString.call():可以准确判断任何类型。要了解对象原型的toString()内部逻辑和call()的核心原理,二者结合才有精准判定的效果
Array.isArray():是数组则返回true,不是则返回false。判定范围最狭窄
来源:juejin.cn/post/7403288145196580904
学TypeScript必然要了解declare
背景
declare关键字是为了服务TypeScript的。TypeScript是什么在这里就不多介绍了,但是我们要知道ts文件是需要TypeScript编译器转换为js文件才可以执行,并且在编译阶段就会进行类型检查。但是在TypeScript中并不支持js可识别的所有类型,例如我们使用第三方库JQuery,我们通过一下方法获取一个id为‘foo’的标签元素。
$('#foo');
// or
jQuery('#foo');
然而在ts文件中,使用底下就会爆出一条红线提示到:Cannot find name '$'
因此,需要declare来声明,告诉TypeScript编译器该标识符已存在,通过编译时的检查并在开发时提供类型提示。
定义
在 TypeScript 中,declare关键字告诉编译器存在一个对象(并且可以在代码中引用)。它向 TypeScript 编译器声明该对象。简而言之,它允许开发人员使用在其他地方声明的对象。
注:编译器不会将declare语句编译为 JavaScript。对比下面两段代码:
// declare声明了一个名为 myGlobal 的全局变量,并指定其类型为 any。
// 该声明并不会生成真正的 JavaScript 代码,而只是告诉 TypeScript 编译器该变量存在。
declare var myGlobal: any;
// 给 myGlobal 赋值为 42。
myGlobal = 42;
console.log(myGlobal); // 42
// 直接声明了一个名为 myGlobal 的全局变量,并指定其类型为 any。这会生成真正的 JavaScript 代码。
var myGlobal: any;
// 给 myGlobal 赋值为 42。
myGlobal = 42;
console.log(myGlobal); // 42
使用
- declare var 声明全局变量
- declare function 声明全局方法
- declare class 声明全局类
- declare enum 声明全局枚举类型
- declare namespace 声明(含有子属性的)全局对象
- declare global 扩展全局变量
- declare module 扩展模块
声明文件
通常,在使用第三方库或模块时,有两种方式引入声明文件:
- 全局声明:如果第三方库或模块是全局可访问的,你可以在整个项目的任何地方直接使用它们,而无需显式导入。此时,你只需要确保在 TypeScript 项目中正确引入了相应的声明文件。一般情况下,TypeScript 会自动查找并加载全局声明文件。如果没有自动加载,你可以使用 /// 的方式在具体的源文件中将声明文件引入。
- 模块导入:如果第三方库或模块是通过模块化方式提供的,你需要使用 import 语句将其导入到你的代码中,同时也需要确保相应的声明文件被正确引入。在这种情况下,你可以使用 import 或 require 来引入库,并且不需要显式地引入声明文件,因为 TypeScript 编译器会根据模块的导入语句自动查找和加载相应的声明文件。
有很多第三方库提供了声明文件,可以在packages.json文件中查看。types表示类型声明文件是哪一个。
可以使用 @types 统一管理第三方库的声明文件。@types 的使用方式很简单,直接用 npm 安装对应的声明模块即可,以 jQuery 举例:
npm install @types/jquery --save-dev
来源:juejin.cn/post/7402811318816702515
厉害了,不用js就能实现文字中间省略号
今天发现一个特别有意思的效果,并进行解析,就是标题的效果
参考链接
如果要实现这个功能,我想很多人第一时间想到的都是用js去计算dom容器和文字之间是否溢出吧?但今天带来一个用css实现的效果,不用自己计算,只需要寥寥几行(bushi)就可以实现让人头疼的文字中间省略号功能。
实现思路
1. 简单实现
在用css实现的时候我们不妨用这个思路想想,设置一个当前显示文字span伪元素的width为50%,浮动到当前span上面,并且设置direction: rtl;
显示右边文字,不就可以很简单的实现这个功能了?让我们试试:
<style>
.wrap {
width: 200px;
border: 1px solid white;
}
.test-title {
display: block;
color: white;
overflow: hidden;
height: 20px;
}
.test-title::before {
content: attr(title);
width: 50%;
float: right;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
direction: rtl;
}
</style>
<body>
<div class="wrap">
<span class="test-title" title="这是一行文字文字文字文字文字文字文字文字文字文字文字文字文字">
这是一行文字文字文字文字文字文字文字文字文字文字文字文字文字
</span>
</div>
</body>
💡 此处应有图
2. 优化效果
在上面我们已经看到,其实效果我们已经实现了,现在文字中间已经有了省略号了!但是这其中其实有一个弊端不知道大家有没有发现,那就是文本不溢出的情况呢?伪元素是不是会一直显示在上面?这该怎么办?难道我们需要用js监听文本不溢出的情况然后手动隐藏吗?
既然是用css来进行实现,那么我们当然不能用这种方式了。这里原作者用了一种很取巧,但也很好玩的一种方法,让我们来看看吧!
既然我们上面实现的是文本溢出的情况,那么当文本不溢出的时候我们直接显示文字不就行了?你可能想说:“这不是废话吗?但我现在不就是不知道怎么判断吗? ”。hhhhh对,那我们就要用css来想想,css该怎么判断呢?我就不卖关子了,让我们想想,我们给文本的容器添加一个固定宽度,那么当文本溢出的时候会发生什么呢?是不是会换行,高度变大呢,那么当我们设置两个文本元素,一个是正常样式,一个是我们上方的溢出样式。等文本不溢出没换行的时候,显示正常样式,当文本溢出高度变大的时候显示溢出样式可以吗?让我们试试吧
<!DOCTYPE html>
<html lang="en">
<head>
<style>
body {
background: #333;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
margin: 0;
font-size: 14px;
}
.wrap {
width: 300px;
background: #333;
/* 设置正常行高,并隐藏溢出画面 */
height: 2em;
overflow: hidden;
line-height: 2;
position: relative;
text-align: -webkit-match-parent;
resize: horizontal;
}
.normal-title {
/* 设置最大高度为双倍行高使其可以换行 */
display: block;
max-height: 4em;
}
.test-title {
position: relative;
top: -4em;
display: block;
color: white;
overflow: hidden;
height: 2em;
text-align: justify;
background: inherit;
overflow: hidden;
}
.test-title::before {
content: attr(title);
width: 50%;
float: right;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
direction: rtl;
}
</style>
</head>
<body>
<div class="wrap">
<span class="normal-title">这是一行文字文字文字文字文字文字文字文字文字文字文字文字文字</span>
<span class="test-title" title="这是一行文字文字文字文字文字文字文字文字文字文字文字文字文字">
这是一行文字文字文字文字文字文字文字文字文字文字文字文字文字
</span>
</div>
</body>
</html>
大家都试过了吧?那让我们来讲一下这段代码的实现:
实现方式:简单来说这段代码实现的就是一个覆盖的效果,normal-title元素平常是普通高度(1em),等到换行之后就会变成2em,那么我们的溢出样式test-title怎么实现的覆盖呢?这主要依赖于test-title的top属性,让我们这样子想,当normal-title高度为1em的时候,test-title的top为-2em,那么这时候因为wrap的hidden效果,所以test-title是看不到的。那么当normal-title的高度为2em的时候呢?test-title刚好就会覆盖到normal-title上面,所以我们刚好可以看到test-title的省略号效果。
这就是完整的实现过程和方式,css一些取巧的判断方式总会让我们大开眼界,不断学习,方得始终。
来源:juejin.cn/post/7401812292211081226
1. 使用openai api实现一个智能前端组件
0. 注意
本文只是提供一个思路,由于现在大模型正在飞速发展,整个生态在不久的将来或许会发生巨大的变化,文章中的代码仅供参考。
1. 一个简单的示例
假设当前时间是2023年12月28日
,时间段选择器通过理解用户输入表述,自动设置值。
可以看到组件正确理解了用户想要设置的时间。
2.原理简介
graph TD
输入文字描述 --> 请求语言模型接口 --> 处理语言模型响应 --> 功能操作
其实原理很简单,就是通过代码的方式问模型问题,然后让他回答。这和我们使用chatgpt
一样的。
3. 实现
输入描述就不说了,就是输入框。关键在于请求和处理语言模型的接口。
最简单的就是直接使用api
请求这些大模型的官方接口,但是我们需要处理各种平台之间的接口差异和一些特殊问题。这里我使用了一个开发语言模型应用的框架LangChain。
3.1. LangChain
简单的说,这是一个面向语言处理模型的编程框架,从如何输入你的问题,到如何处理回答都有规范的工具来实现。
// 这是一个最简单的例子
import { OpenAI } from "langchain/llms/openai";
import { ChatOpenAI } from "langchain/chat_models/openai";
// 初始化openai模型
const llm = new OpenAI({
temperature: 0.9,
});
// 准备一个输入文本
const text =
"What would be a good company name for a company that makes colorful socks?";
// 输入文本,获取响应
const llmResult = await llm.predict(text);
//=> 响应一段文本:"Feetful of Fun"
整个框架主要就是下面三个部分组成:
graph LR
A["输入模板(Prompt templates)"] --- B["语言模型(Language models)"] --- C["输出解释器(Output parsers)"]
Prompt templates
:输入模板分一句话(not chat)
和对话(chat)
模式,区别就是输入一句话和多句话,而且对话模式中每句话有角色区分是谁说的,比如人类
、AI
、系统
。这里简单介绍一下非对话模式下怎么创建输入模板。
import { PromptTemplate } from "langchain/prompts";
// 最简单的模板生成,使用fromTemplate传入一句话
// 可以在句子中加入{}占位符表示变量
const oneInputPrompt = PromptTemplate.fromTemplate(
`You are a naming consultant for new companies.
What is a good name for a company that makes {product}?`
);
// 也可以直接实例化设置
const twoInputPrompt = new PromptTemplate({
inputVariables: ["adjective"],
template: "Tell me a {adjective} joke.",
});
// 如果你想要这样和模型对话
// 先给出几个例子,然后在问问题
Respond to the users question in the with the following format:
Question: What is your name?
Answer: My name is John.
Question: What is your age?
Answer: I am 25 years old.
Question: What is your favorite color?
Answer:
// 可以使用FewShotPromptTemplate
// 创建一些模板,字段名随便你定
const examples = [
{
input:
"Could the members of The Police perform lawful arrests?",
output: "what can the members of The Police do?",
},
{
input: "Jan Sindel's was born in what country?",
output: "what is Jan Sindel's personal history?",
},
];
// 输入模板,包含变量就是模板要填充的
const prompt = `Human: {input}\nAI: {output}`;
const examplePromptTemplate = PromptTemplate.fromTemplate(prompt);
// 创建example输入模板
const fewShotPrompt = new FewShotPromptTemplate({
examplePrompt: examplePromptTemplate,
examples,
inputVariables: [], // no input variables
});
console.log(
(await fewShotPrompt.formatPromptValue({})).toString()
);
// 输出
Human: Could the members of The Police perform lawful arrests?
AI: what can the members of The Police do?
Human: Jan Sindel's was born in what country?
AI: what is Jan Sindel's personal history?
// 还有很多可以查询官网
Language models
: 语言模型同样分为LLM(大语言模型)
和chat模型
,其实两个差不多,就是输入多少和是否可以连续对话的区别。
import { OpenAI } from "langchain/llms/openai";
const model = new OpenAI({ temperature: 1 });
// 可以添加超时
const resA = await model.call(
"What would be a good company name a company that makes colorful socks?",
{ timeout: 1000 } // 1s timeout
);
// 注册一些事件回调
const model = new OpenAI({
callbacks: [
{
handleLLMStart: async (llm: Serialized, prompts: string[]) => {
console.log(JSON.stringify(llm, null, 2));
console.log(JSON.stringify(prompts, null, 2));
},
handleLLMEnd: async (output: LLMResult) => {
console.log(JSON.stringify(output, null, 2));
},
handleLLMError: async (err: Error) => {
console.error(err);
},
},
],
});
// 还有一些配置可以参考文档
Output parsers
: 顾名思义就是处理输出的模块,当语言模型回答了一段文字程序是很难提取出有用信息的, 我们通常需要模型返回一个程序可以处理的答案,比如JSON
。虽然叫输出解释器,实际上是在输入信息中加入一些额外的提示,让模型能够按照需求格式输出。
// 这里用StructuredOutputParser,结构化输出解释器为例
// 使用StructuredOutputParser创建一个解释器
// 定义了输出有两个字段answer、source
// 字段的值是对这个字段的描述在
const parser = StructuredOutputParser.fromNamesAndDescriptions({
answer: "answer to the user's question",
source: "source used to answer the user's question, should be a website.",
});
// 使用RunnableSequence,批量执行任务
const chain = RunnableSequence.from([
// 输入包含了两个变量,一个是结构化解释器的“格式说明”,一个是用户的问题
PromptTemplate.fromTemplate(
"Answer the users question as best as possible.\n{format_instructions}\n{question}"
),
new OpenAI({ temperature: 0 }),
parser,
]);
// 与模型交互
const response = await chain.invoke({
question: "What is the capital of France?",
format_instructions: parser.getFormatInstructions(),
});
// 响应 { answer: 'Paris', source: 'https://en.wikipedia.org/wiki/Paris' }
// 输入的模板是这样
Answer the users question as best as possible. // 这句话就是prompt的第一句
// 下面一大段是StructuredOutputParser自动加上的,大概就是告诉模型json的标准格式应该是什么
The output should be formatted as a JSON instance that conforms to the JSON schema below.
As an example, for the schema {{"properties": {{"foo": {{"title": "Foo", "description": "a list of strings", "type": "array", "items": {{"type": "string"}}}}}}, "required": ["foo"]}}}}
the object {{"foo": ["bar", "baz"]}} is a well-formatted instance of the schema. The object {{"properties": {{"foo": ["bar", "baz"]}}}} is not well-formatted.
Here is the output schema:
```
{"type":"object","properties":{"answer":{"type":"string","description":"answer to the user's question"},"sources":{"type":"array","items":{"type":"string"},"description":"sources used to answer the question, should be websites."}},"required":["answer","sources"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}
```
// 这段就是调用的时候传入的问题
What is the capital of France?
// 还有很多不同的解释器
// 如StringOutputParser字符串输出解释器
// JsonOutputFunctionsParser json函数输出解释器等等
除了这三部分,还有一些方便程序操作的一些功能模块,比如记录聊天状态的Memory
模块,知识库模块Retrieval
等等,这些官网有比较完整的文档,深度的使用后面再来探索。
3.2. 简单版本
// 初始化语言模型
// 这里使用的openai
const llm = new OpenAI({
openAIApiKey: import.meta.env.VITE_OPENAI_KEY,
temperature: 0,
});
function App() {
const [res, setRes] = useState<string>();
const [from] = Form.useForm();
return (
<>
<div>结果:{res}</div>
<Form wrapperCol={{ span: 6 }} form={from}>
<Form.Item label="输入描述">
<Input.Search
onSearch={async (value) => {
setRes("正在请求");
// 直接对话模型
const text =
`现在是${dayjs().format("YYYY-MM-DD")},${value},开始结束时间是什么。请用这个格式回答{startTime: '开始时间', endTime: '结束时间'}`;
// 简单预测文本
const llmResult = await llm.predict(text);
const response = JSON.parse(llmResult)
// 解析
const { startTime, endTime } = response;
// 设置
from.setFieldsValue({
times: [dayjs(startTime), dayjs(endTime)],
});
setRes(llmResult)
}}
enterButton={<Button type="primary">确定</Button>}
/>
</Form.Item>
<Form.Item label="时间段" name="times">
<DatePicker.RangePicker />
</Form.Item>
</Form>
</>
);
}
export default App;
前面虽然能实现功能,但是有很多边界条件无法考虑到,比如有的模型无法理解你这个返回格式是什么意思,或者你有很多个字段那你就要写一大串输入模板。
3.3. 使用结构化输出解释器
// 修改一下onSearch
setRes("正在请求");
// 定义输出有两个字段startTime、endTime
const parser = StructuredOutputParser.fromNamesAndDescriptions({
startTime: "开始时间,格式是YYYY-MM-DD HH:mm:ss",
endTime: "结束时间,格式是YYYY-MM-DD HH:mm:ss",
});
const chain = RunnableSequence.from([
// 输入模板
PromptTemplate.fromTemplate(
`{format_instructions}\n现在是${dayjs().format(
"YYYY-MM-DD"
)},{question},开始结束时间是什么`
),
llm,
parser,
]);
const response = await chain.invoke({
question: value,
// 把输出解释器的提示放入输入模板中
format_instructions: parser.getFormatInstructions(),
});
// 这个时候经过结构化解释器处理,返回的就是json
setRes(JSON.stringify(response));
const { startTime, endTime } = response;
from.setFieldsValue({
times: [dayjs(startTime), dayjs(endTime)],
});
对于大型一点的项目,使用langChain
的api
可以更规范的组织我们的代码。
// 完整代码
import { OpenAI } from "langchain/llms/openai";
import { useState } from "react";
import {
PromptTemplate,
} from "langchain/prompts";
import { StructuredOutputParser } from "langchain/output_parsers";
import { RunnableSequence } from "langchain/runnables";
import { Button, DatePicker, Form, Input } from "antd";
import "dayjs/locale/zh-cn";
import dayjs from "dayjs";
const llm = new OpenAI({
openAIApiKey: import.meta.env.VITE_OPENAI_KEY,
temperature: 0,
});
function App() {
const [res, setRes] = useState<string>();
const [from] = Form.useForm();
return (
<>
<div>结果:{res}</div>
<Form wrapperCol={{ span: 6 }} form={from}>
<Form.Item label="输入描述">
<Input.Search
onSearch={async (value) => {
setRes("正在请求");
const parser = StructuredOutputParser.fromNamesAndDescriptions({
startTime: "开始时间,格式是YYYY-MM-DD HH:mm:ss",
endTime: "结束时间,格式是YYYY-MM-DD HH:mm:ss",
});
const chain = RunnableSequence.from([
PromptTemplate.fromTemplate(
`{format_instructions}\n现在是${dayjs().format(
"YYYY-MM-DD"
)},{question},开始结束时间是什么`
),
llm,
parser,
]);
const response = await chain.invoke({
question: value,
format_instructions: parser.getFormatInstructions(),
});
setRes(JSON.stringify(response));
const { startTime, endTime } = response;
from.setFieldsValue({
times: [dayjs(startTime), dayjs(endTime)],
});
}}
enterButton={<Button type="primary">确定</Button>}
/>
</Form.Item>
<Form.Item label="时间段" name="times">
<DatePicker.RangePicker />
</Form.Item>
</Form>
</>
);
}
export default App;
4.总结
这篇文章只是我初步使用LangChain
的一个小demo
,在智能组件上面,大家其实可以发挥更大的想象去发挥。还有很多组件可以变成自然语言驱动的。
随着以后大模型的小型化,专门化,我相信肯定会涌现更多的智能组件。
来源:juejin.cn/post/7317440781588840486
Vue.js 自动路由:告别手动配置,让开发更轻松!
在使用 Vue.js 开发项目的时候,我最头疼的就是创建路由,尤其是项目越来越大的时候,管理 route.ts
或 route.js
文件简直是一场噩梦!
我曾经做过一个页面超级多的项目,每新增一个页面就要更新路由,删除页面也要更新路由文件,不然就会报错,真是烦死人了!
所以,我开始寻找自动生成路由的方法。我在网上搜了很久,但大部分结果都是针对 Webpack 和 Vue 2 的,很难找到适合我的方案。最后,我在 Vue 的 GitHub 仓库的讨论区里提问,终于找到了答案!
那就是 Unplugin Vue Router
! 它可以为 Vue 3 实现基于文件的自动路由,而且支持 TypeScript,设置起来也超级简单! 虽然官方说它还在实验阶段,但用起来已经很方便了。
创建项目,安装插件
首先,我们创建一个新的 Vue 项目。 相信大家都很熟悉用 Vue CLI 创建项目了,这里就不赘述了,不熟悉的小伙伴可以去看看 Vue.js 官网的快速入门指南。
pnpm create vue@latest
我创建项目的时候选择了 TypeScript 和 Vue Router,这样它就会自动生成一些页面和路由。
然后,进入项目目录,安装依赖。我最近开始用 pnpm
来管理依赖,感觉还不错。
pnpm add -D unplugin-vue-router
接下来,更新 vite.config.ts
文件, 注意要把插件放在第 0 个位置 。
import { fileURLToPath, URL } from "node:url";
import VueRouter from "unplugin-vue-router/vite";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
VueRouter({
/* options */
}),
// ⚠️ Vue must be placed after VueRouter()
vue(),
],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
});
然后,更新 env.d.ts
文件,让编辑器能够识别插件的类型。
/// <reference types="vite/client" />
/// <reference types="unplugin-vue-router/client" />
最后,更新路由文件 src/router/index.ts
。
import { createRouter, createWebHistory } from "vue-router";
import { routes, handleHotUpdate } from "vue-router/auto-routes";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
});
if (import.meta.hot) {
handleHotUpdate(router);
}
export default router;
创建页面,自动生成路由
现在,我们可以创建 src/pages
目录了,在这个目录下创建的 Vue 组件会自动变成路由和页面,就像 Nuxt 一样方便!
我们先在 src\pages\about.vue
创建一个关于页面:
<template>
<div>This is the about page</div>
</template>
然后在 src\pages\index.vue
创建首页:
<template>
<div>This is Home Page</div>
</template>
运行 pnpm dev
启动开发服务器,点击 “Home” 链接就会跳转到首页,点击 “About” 链接就会跳转到关于页面。
怎么样,是不是很方便? 如果你不熟悉路由文件夹结构,可以看看这个文档: uvr.esm.is/guide/file-…
动态路由
我们再来试试创建带参数的动态路由。在 src/pages/blog/[id].vue
创建一个组件,内容如下:
<script setup>
const { id } = useRoute().params;
</script>
<template>
<div>This is the blog post with id: {{ id }}</div>
</template>
再次运行 pnpm dev
,然后访问 http://localhost:5173/blog/6
,你就会看到以下内容:
是不是很神奇? 希望这篇简短的博客能帮助你在 Vue.js 的旅程中更轻松地创建路由!
来源:juejin.cn/post/7401354593588199465
学TypeScript必然要了解declare
背景
declare关键字是为了服务TypeScript的。TypeScript是什么在这里就不多介绍了,但是我们要知道ts文件是需要TypeScript编译器转换为js文件才可以执行,并且在编译阶段就会进行类型检查。但是在TypeScript中并不支持js可识别的所有类型,例如我们使用第三方库JQuery,我们通过一下方法获取一个id为‘foo’的标签元素。
$('#foo');
// or
jQuery('#foo');
然而在ts文件中,使用底下就会爆出一条红线提示到:Cannot find name '$'
因此,需要declare来声明,告诉TypeScript编译器该标识符已存在,通过编译时的检查并在开发时提供类型提示。
定义
在 TypeScript 中,declare关键字告诉编译器存在一个对象(并且可以在代码中引用)。它向 TypeScript 编译器声明该对象。简而言之,它允许开发人员使用在其他地方声明的对象。
注:编译器不会将declare语句编译为 JavaScript。对比下面两段代码:
// declare声明了一个名为 myGlobal 的全局变量,并指定其类型为 any。
// 该声明并不会生成真正的 JavaScript 代码,而只是告诉 TypeScript 编译器该变量存在。
declare var myGlobal: any;
// 给 myGlobal 赋值为 42。
myGlobal = 42;
console.log(myGlobal); // 42
// 直接声明了一个名为 myGlobal 的全局变量,并指定其类型为 any。这会生成真正的 JavaScript 代码。
var myGlobal: any;
// 给 myGlobal 赋值为 42。
myGlobal = 42;
console.log(myGlobal); // 42
使用
- declare var 声明全局变量
- declare function 声明全局方法
- declare class 声明全局类
- declare enum 声明全局枚举类型
- declare namespace 声明(含有子属性的)全局对象
- declare global 扩展全局变量
- declare module 扩展模块
声明文件
通常,在使用第三方库或模块时,有两种方式引入声明文件:
- 全局声明:如果第三方库或模块是全局可访问的,你可以在整个项目的任何地方直接使用它们,而无需显式导入。此时,你只需要确保在 TypeScript 项目中正确引入了相应的声明文件。一般情况下,TypeScript 会自动查找并加载全局声明文件。如果没有自动加载,你可以使用 /// 的方式在具体的源文件中将声明文件引入。
- 模块导入:如果第三方库或模块是通过模块化方式提供的,你需要使用 import 语句将其导入到你的代码中,同时也需要确保相应的声明文件被正确引入。在这种情况下,你可以使用 import 或 require 来引入库,并且不需要显式地引入声明文件,因为 TypeScript 编译器会根据模块的导入语句自动查找和加载相应的声明文件。
有很多第三方库提供了声明文件,可以在packages.json文件中查看。types表示类型声明文件是哪一个。
可以使用 @types 统一管理第三方库的声明文件。@types 的使用方式很简单,直接用 npm 安装对应的声明模块即可,以 jQuery 举例:
npm install @types/jquery --save-dev
来源:juejin.cn/post/7402811318816702515
前端如何将git的信息打包进html
为什么要做这件事
- 定制化项目我们没有参与或者要临时更新客户的包的时候,需要查文档才知道哪个是最新分支
- 当测试环境打包的时候,不确定是否是最新的包,需要点下功能看是否是最新的代码,不能直观的看到当前打的是哪个分支
- 多人开发的时候,某些场景下可能被别人覆盖了,需要查下jenkins或者登录服务器看下
实现效果
如下,当打开F12,可以直观的看到打包日期、分支、提交hash、提交时间
如何做
主要是借助 git-revision-webpack-plugin的能力。获取到git到一些后,将这些信息注入到变量中,html读取这个变量即可
1. 安装dev的依赖
npm install --save-dev git-revision-webpack-plugin
2. 引入依赖并且初始化
const { GitRevisionPlugin } = require('git-revision-webpack-plugin')
const gitRevisionPlugin = new GitRevisionPlugin()
3. 注入变量信息
我这里用的是vuecli,可直接在chainWebpack中注入,当然你可以使用DefinePlugin进行声明
config.plugin('html').tap((args) => {
args[0].banner = {
date: dayjs().format('YYYY-MM-DD HH:mm:ss'),
branch: gitRevisionPlugin.branch(),
commitHash: gitRevisionPlugin.commithash(),
lastCommitDateTime: dayjs(gitRevisionPlugin.lastcommitdatetime()).format('YYYY-MM-DD HH:mm:ss'),
}
return args
});
4. 使用变量
在index.html的头部插入注释
<!-- date <%= htmlWebpackPlugin.options.banner.date %> -->
<!-- branch <%= htmlWebpackPlugin.options.banner.branch %> -->
<!-- commitHash <%= htmlWebpackPlugin.options.banner.commitHash %> -->
<!-- lastCommitDateTime <%= htmlWebpackPlugin.options.banner.lastCommitDateTime %> -->
5. 查看页面
6. 假如你用的是vuecli
当你使用的是vuecli,构建完后会发现index.html上这个注释丢失了
原因如下
vueCLi 对html的打包用的html-webpack-plugin,默认在打包的时候会把注释删除掉
修改vuecli的配置如下可以解决,将removeComments设置为false即可
module.exports = {
// 其他配置项...
chainWebpack: config => {
config
.plugin('html')
.tap(args => {
args[0].minify = {
removeComments: false,
// 其他需要设置的参数
};
return args;
});
},
};
7. 假如你想给qiankun的子应用添加一些git的注释信息,可以在meta中添加
<meta name="description"
content="Date:<%= htmlWebpackPlugin.options.banner.date %>,Branch:<%= htmlWebpackPlugin.options.banner.branch %>,commitHash: <%= htmlWebpackPlugin.options.banner.commitHash %>,lastCommitDateTime:<%= htmlWebpackPlugin.options.banner.lastCommitDateTime %>">
渲染在html上如下,也可以快速的看到子应用的构建时间和具体的分支
总结
- 借助git-revision-webpack-plugin的能力读取到git的一些信息
- 将变量注入在html中或者使用DefinePlugin进行声明变量
- 读取变量后显示在html上或者打印在控制台上可以把关键的信息保留,方便我们排查问题
来源:juejin.cn/post/7403185402347634724
码农的畅想:年入10个小目标
本文将以自己真实的创业项目为例,给大家分享如何写一个用于融资的BP(商业计划书)。
使命愿景
我们为小型艺培机构或个体老师提供好用的招生引流工具和教学课件,让老师能够更加专注教学和提升服务体验。
- 使命:让天下没有难做的艺术培训,通过艺术培训提升人类的幸福感。
- 愿景:成为艺培行业的贝壳(线下培训蜗牛艺术中心类似链家,线上平台艺培助理类似贝壳),实现年营收一百亿。
产品及服务
线下业务为蜗牛艺术中心,以提供融合了绘本阅读、艺术创作和图形编程的跨学科美育培训为主,同时也开设书法、舞蹈和音乐等品类的培训。
线上业务为艺培助理,是美术、音乐和舞蹈等培训机构教研、招生及运营的好帮手,引流产品为3D画展,现金产品为海报设计,利润产品为课程加盟。
- 载体:小程序、网页应用和APP。
- 服务:3D展厅、海报设计、拼团招生和课程加盟。
- 策略:移动端优先,通过海报设计和课程研发大赛获取目标用户,借助AIGC技术提升生产力,用产品力说话,靠口碑裂变。
团队
- 陈XX:创始人 CEO 产研负责人 美团技术专家 毕业于交大和某军校 曾在798当过美术馆长 有5年以上线下艺培经验。
- 杨XX:运营合伙人 毕业于交大和暨南大学,和创始人认识了16年 并一起经营了一家跨境电商服务公司,实现年营收500多万。
- 熊XX:教研合伙人 清华美院硕士和美育研究所委员 曾任探月学院美育教研负责人 和创始人认识了10年,三年前就一起尝试过创业,拥有15年艺培经验。
财务顾问:xxx,创始人的亲戚,曾任上市公司董事长助理,北大光华管理学院硕士。
行业背景
从21年底开始,国家大力限制学科培训,并于23年底教育部下发通知要大力推进跨学科美育,艺术教育将迎来大爆发,2025年的市场规模将由之前预期的2000亿增长为3000亿。
虽然新生儿的人口相比高峰下降了近一半,但艺术教育中偏兴趣的低龄目标学生(2到12岁),五年内也能维持在一个亿左右,而偏应试的大龄目标学生(12到19岁),也有一个亿左右,还有正在高速发展的成人艺培培训。
所以,整个艺培市场的规模,还能保持年复20%左右的增长,未来五年至少能达到5000亿(仅考虑低龄艺培市场,若人均5000元,渗透率50%即可达到)。
存在痛点
但由于经济下行,家长对课程和服务体验的要求越来越高,艺培机构普遍招生很难,急需技术赋能传统教育机构,提升产品服务标准程度、提高管理效率和坪效、缩短回本周期,技术的完善应用,将给予连锁教育机构实现标准化、规模化的机会。
相比学科培训,艺培的标准化要难很多,而且做服务的天花板很低,最多只能达到10亿的级别,巨头看不上,小团队又搞不定,目前还没有平台能够给艺培机构,提供系统化的通用解决方案,简化机构的日常工作,让机构能够更多的关注学生及家长,做好最核心的教学服务。
近几年,移动互联网发展已经非常成熟,服务艺培机构招生运营某个环节的软件,在市面上已经有了很多,不论是拼团招生的工具,还是海报设计的平台,或是校区管理的saas软件,都只能部分解决艺培机构的需求。
竞品及我们的优势
从海报设计看,有稿定设计、美图设计室、创客贴、爱设计、图怪兽等知名的设计平台,只有稿定设计的艺培模板素材相对丰富,但都不是专门面向艺培机构的,模板和素材不够丰富,且机构使用海报模板后,还需结合自己的招生运营方案做较大调整。我们提供的海报都是我们线下培训门店真实用于招生的海报,并且通过设计大赛,让用户也能为其他用户提供海报及素材,所以使用海报模板后,只需简单改一改品牌、logo和图片即可投入使用。
从3D展厅看,目前很多公司提供的展厅以面向政府企业的宣传为主,比如党建展览、历史回顾等,多数不支持实时在3D模型中去动态加载图片,使用成本比较高。我们提供3D展厅,是专门面向艺培机构的,可以实时动态修改画展中的作品,只要在后台替换了图片,系统无需上线,用户再次打开,展现的就是最新作品,我们可以做到一个展厅最低只需10元,甚至可以作为免费引流的工具。
从拼团招生看,市面上有很多第三方的招生公司,他们一般提供的方案是198元6次课,学生购买拼团课的钱,机构一分钱也拿不到,并且课次太多,机构的转化率也不高。我通过师训教会机构自己按照流程去组织拼团活动即可,我们的收费不到竞品的十分之一,甚至为了引流,可以完全免费。
从课程加盟看,目前市面美术做的比较好的有本来计画和小央美,但他们提供的都是传统的美术课程,并且软件使用的是第三方的课件系统,用户体验比较差;我们提供的是教育部23年底倡导的跨学科美育课程,不仅课程辨识度高,而且我们将艺术创作与绘本阅读及图形编程结合,其他机构很难模仿。
项目现状及展望
当前,线下培训方向:我们已经在深圳开了一家门店并实现盈利,近期正在郑州和北京各开一家分店(都有一定学生基础,基于已有的店进行跨学科升级),招聘了5个全职的美术老师和3个实习生;线上培训方向:艺培助理的小程序和网站均已上线,海报设计、拼团招生和3D展厅均已正式投入使用,近期正在接入一个合作的课程加盟老师,将带来一千个种子用户,日活突破一千(每天都需使用课件进行备课和上课)。
接下来的计划是,先融资500到1000万,组建10人的产研团队,根据种子用户的反馈,优化系统和收费方案,组织海报设计和课程研发大赛,实现线上业务月营收突破一百万;同时,在多个大城市打造10家线下培训旗舰店,实现月营收突破200万,为开展师训和课程加盟做好准备。
后续将会根据发展进行多轮融资,不断完善艺培助理的功能和服务,比如打造社区、增加招聘板块、提供短视频、支持直播等,为1000万个学生提供个性化的3D展厅(每个每年20元),发展100万个艺培助理会员(年费300元),同时在大城市开设100家直营门店,并为5000家艺培机构提供课程加盟,年营收达到:100020万+ 100300万 + 300100万 + 50004万 = 10亿。
资金及项目规划
- 股权架构:创始人 45% 联合创始人 25% 运营合伙人+技术合伙人20% 其他员工股权池 10%。
- 融资需求:500~1000万,出让20%的股权。
- 资金使用:50%用于搭建产研团队,30%用于为海报设计和课程制作大赛提供奖金,20%用于投放广告。
- 未来融资:一年后实现月营收100万,启动A轮融资2000~5000万,扩大规模,实现月营收一千万,三年后启动B轮融资一到三亿元,实现月营收五千万,五年后启动C轮融资,业务多元化,实现月营收一亿以上。
总结
小富靠勤,大富靠命。要坚信,命运永远掌握在自己手中。
随着这两年大模型技术的突飞猛进,很多简单重复的智力工作将被AI替代,大家将有更多的时间去丰富自己的精神需要,艺培行业的市场规模一定可以超过万亿。
如今,美术在线培训的美术宝和钢琴在线培训的vip陪练,都已经实现了年营收超过20亿,我们作为oMo模式的先行者,未来五年实现年营收10个亿只是个小目标。
我相信,这个商业计划能够实现的可能性很大的,在此分享给大家,即使我没有实现,肯定也会有其他人可以实现。
来源:juejin.cn/post/7376925694613274674
还在用 来当作空格?别忽视他对样式的影响!
许久没有更新博客了,今天就抽空来分享下之前遇到个有意思的现象~
奇怪的现象,被换行的单词
在一次新需求完工之后,进行国际化样式优化时,我发现了一个奇怪的现象:即使页面元素有word-wrap:break-word;
样式属性,单词也照样会被直接裁断换行。
这又是为什么嘞?细细分析页面元素,突然发现或许这与之前的踩过的坑:特殊的不换行空格有关?!
来复现吧!
那我们马上就来试一试!
<style>
.normal_style{
width:70px;
height:200px;
margin-right:150px;
border:1px solid red;
/* 👇表示 如果一个单词超出行长度,要截取换行,其他默认;👇 */
word-wrap:break-word;
}
</style>
<div style="display:flex;">
<div class='normal_style'>This is a long a long sentence</div>
<div class='normal_style'>This is a long a long sentence</div>
</div>
很明显,单词直接被强行换行拆分了!
那会不会是页面解析的时候,把
连同其他单词一起,当作一长串单词来处理了,所以才不换行的嘞?
你知道空格转义符有几种写法吗?
那我们就再来试试!不使用
转而使用其他空格转义符呢?
其实除了
,还有其他很多种空格转义符。
1. 半角空格
 
它才是典型的“半角空格”,全称是En Space,en是字体排印学的计量单位,为em宽度的一半。根据定义,它等同于字体度的一半(如16px字体中就是8px)。名义上是小写字母n的宽度。此空格传承空格家族一贯的特性:透明的,此空格有个相当稳健的特性,就是其占据的宽度正好是1/2个中文宽度,而且基本上不受字体影响。
2. 全角空格
 
从这个符号到下面, 我们就很少见到了, 它叫“全角空格”,全称是Em Space,em是字体排印学的计量单位,相当于当前指定的点数。例如,1 em在16px的字体中就是16px。此空格也传承空格家族一贯的特性:透明的,此空格也有个相当稳健的特性,就是其占据的宽度正好是1个中文宽度,而且基本上不受字体影响。
3. 窄空格
 
窄空格,全称是Thin Space。我们不妨称之为“瘦弱空格”,就是该空格长得比较瘦弱,身体单薄,占据的宽度比较小。它是em之六分之一宽。
4. 零宽不连字
‌
它叫零宽不连字,全称是Zero Width Non Joiner,简称“ZWNJ”,是一个不打印字符,放在电子文本的两个字符之间,抑制本来会发生的连字,而是以这两个字符原本的字形来绘制。Unicode中的零宽不连字字符映射为“”(zero width non-joiner,U+200C),HTML字符值引用为:
5. 零宽连字
‍
它叫零宽连字,全称是Zero Width Joiner,简称“ZWJ”,是一个不打印字符,放在某些需要复杂排版语言(如阿拉伯语、印地语)的两个字符之间,使得这两个本不会发生连字的字符产生了连字效果。零宽连字符的Unicode码位是U+200D (HTML: )。
再次尝试复现- 
<style>
.normal_style{
width:70px;
height:200px;
margin-right:150px;
border:1px solid red;
/* 👇表示 如果一个单词超出行长度,要截取换行,其他默认;👇 */
word-wrap:break-word;
}
</style>
<div style="display:flex;">
<div class='normal_style'>This is a long a long sentence</div>
<div class='normal_style'>This is a long a long sentence</div>
<div class='normal_style'>This is a long a long sentence</div>
</div>
我们可以看到  
进行转义的话,单词截取换行是正常的!所以,真凶就是
特殊的不换行空格!
如何修订?
因为这个提示框是使用公司自制的 UI 组件实现的,而之所以使用
进行转义是为了修订XSS注入。(对,这个老东西现在没人维护,还是我去啃源码加上的,使用了公共的转义方法)。最后就简单去修改这个公共方法吧!使用了最贴近
宽度的空格转义符: 
!
来源:juejin.cn/post/7403953367766859828
数据大屏的解决方案
1. 使用缩放比例适配各种设备(适用16*9比例的屏幕分辨率)
- 封装一个获取缩放比例的工具函数
/**
* 大屏效果需要满足16:9的屏幕比例,才能达到完美的大屏适配效果
* 其他比例的大屏效果,不能铺满整个屏幕
* @param {*} w 设备宽度 默认 1920
* @param {*} h 设备高度 默认 1080
* @returns 返回值是缩放比例
*/
export function getScale(w = 1920, h = 1080) {
const ww = window.innerWidth / w
const wh = window.innerHeight / h
return ww < wh ? ww : wh
}
- 在
vue
中使用方案如下
<template>
<div class="full-screen-container">
<div id="screen">
大屏展示的内容
</div>
</div>
</template>
<script>
import { getScale } from "@/utils/tool";
import screenfull from "screenfull";
export default {
name: "cockpit",
mounted() {
if (screenfull && screenfull.enabled && !screenfull.isFullscreen) {
screenfull.request();
}
this.setFullScreen();
},
methods: {
setFullScreen() {
const screenNode = document.getElementById("screen");
// 非标准设备(笔记本小于1920,如:1366*768、mac 1432*896)
if (window.innerWidth < 1920) {
screenNode.style.left = "50%";
screenNode.style.transform = `scale(${getScale()}) translate(-50%, 0)`;
} else if (window.innerWidth === 1920) {
// 标准设备 1920 * 1080
screenNode.style.left = 0;
screenNode.style.transform = `scale(1) translate(0, 0)`;
} else {
// 大屏设备(4K 2560*1600)
screenNode.style.left = "50%";
screenNode.style.transform = `scale(${getScale()}) translate(-50%, 0)`;
}
// 监听视口变化
window.addEventListener("resize", () => {
if (window.innerWidth === 1920) {
screenNode.style.left = 0;
screenNode.style.transform = `scale(1) translate(0, 0)`;
} else {
screenNode.style.left = "50%";
screenNode.style.transform = `scale(${getScale()}) translate(-50%, 0)`;
}
});
},
},
};
</script>
<style lang="scss">
.full-screen-container {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
background-color: #131a2b;
#screen {
position: fixed;
width: 1920px;
height: 1080px;
top: 0;
transform-origin: left top;
color: #fff;
}
}
</style>
- mac设备上的屏幕分辨率,在适配的时候,可能不是那么完美,以短边缩放为准,所以宽度到达百分之百后,高度不会铺满
- 1432*896 13寸mac本
- 2560*1600
4k
屏幕
2. 使用第三方插件来实现数据大屏(mac设备会产生布局错落)
- 建议在全屏容器内使用百分比搭配flex进行布局,以便于在不同的分辨率下得到较为一致的展示效果。
- 使用前请注意将
body
的margin
设为0,否则会引起计算误差,全屏后不能完全充满屏幕。 - 使用方式
1. npm install @jiaminghi/data-view
2. yarn add @jiaminghi/data-view
// 在vue项目中的main.js入口文件,将自动注册所有组件为全局组件
import {fullScreenContainer} from '@jiaminghi/data-view'
Vue.use(fullScreenContainer)
<template>
<dv-full-screen-container>
要展示的数据大屏内容
这里建议高度使用百分比来布局,而且要考虑mac设备适配问题,防止百分比发生布局错乱
需要注意的点是,一个是宽度,一个是字体大小,不产生换行
</dv-full-screen-container>
</template>
<script>
import screenfull from "screenfull";
export default {
name: "cockpit",
mounted() {
if (screenfull && screenfull.enabled && !screenfull.isFullscreen) {
screenfull.request();
}
}
};
</script>
<style lang="scss">
#dv-full-screen-container {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
background-color: #131a2b;
}
</style>
- 插件地址
3. 效果图
来源:juejin.cn/post/7372105071573663763
从编程语言的角度,JS 是不是一坨翔?一个来自社区的暴力观点
给前端以福利,给编程以复利。大家好,我是大家的林语冰。
00. 写在前面
毋庸置疑,JS 的历史包袱确实罄竹难书。用朱熹的话说,天不生 ES6,JS 万古如长夜。
就技术细节而言,ES6 之前,JS 保留了若干臭名昭著的反人类设计,包括但不限于:
typeof null
的抽象泄露==
的无理要求undefined
不是关键词- 其他雷区......
幸运的是,ES5 的“阉割模式”(strict mode)把一大坨 JS 的反人类设计都屏蔽了,后 ES6 时代的 JS 焕然一新。
所以,本期我们就从编程语言的宏观设计来思考,先不纠结 JS 极端情况的技术瑕疵,探讨一下 JS 作为地球上人气最高的编程语言,设计哲学上到底有何魅力?
免责声明:上述统计数据来源于 GitHub 社区,可能存在统计学偏差,仅供粉丝参考。
01. 标准化
编程语言人气排行榜屈居亚军的是 Python,那我们就用 JS 来打败 Python。
根据 MDN 电子书,JS 的核心语言是 ECMAScript,是一门由 ECMA TC39 委员会标准化的编程语言。并不是所有的编程语言都有标准化流程,JS 恰好是“天选之子”,后 ES6 的提案流程也相对稳定,虽然最近突然增加了 stage 2.7,但整体标准化无伤大雅。
值得一提的是,JS 的标准是向下兼容的,用户友好。JS 的大多数技术债务已经诉诸“阉割模式”禁用,而向下兼容则避免了近未来的 ESNext 出现主版本级别的破坏性更新。
举个栗子,ES2024 最新支持的数组分组提案,出于兼容性考虑,提案一度从 Array.prototype.group()
修改为 Object.groupBy()
。
可以看到,JS 的向下兼容设计正是为了防止和某些遗留代码库产生命名冲突,降低迁移成本。
换而言之,JS 不会像 Python 2 升级 Python 3 那样,让用户承担语言迭代伴生的兼容性税,学习成本和心智负担相对较小。
02. 动态类型
编程语言人气排行榜屈居季军的是 TS,那我们就用 JS 来打败 TS。
JS 和 TS 都是 ECMAScript 的超集,TS 则是 JS 的超集。简而言之,TS ≈ JS + 静态类型系统。
JS 和 TS 区别在于动态类型 vs 静态类型,不能说孰优孰劣,只能说各有千秋。我的个人心证是,静态类型对于工程化生态而言不可或缺,比如 IDE 或 Linting,但对于编程应用则不一定,因为没有静态类型,JS 也能开发 Web App。
可以看到,因为 TS 是 JS 的超集,所以虽然没有显式的类型注解,上述代码也可作为 TS 代码,只不过 TS 和 JS 类型检查的粒度和时机并不一致。
一个争论点在于,静态类型的编译时检查有利于服务大型项目,但其实在大型项目中,一般会诉诸单元测试保障代码质量和回归测试。严格而言,静态类型之于大型项目的充要条件并不成立。
事实上,剥离静态类型注解的源码,恰恰体现了编程的本质和逻辑,这也是后来的静态类型语言偏爱半自动化的智能类型系统,因为有的类型注解可能是画蛇添足,而诉诸智能的类型推论可以解放程序猿的生产力。
03. 面向对象
编程语言人气排行榜第四名是 Java,那我们就用 JS 来打败 Java。
作为被误解最深的语言,前 ES6 的 JS 有一个普遍的误区:JS 不是面向对象语言,原因在于 ES5 没有 class。
这种“思想钢印”哪里不科学呢?不科学的地方在于,面向对象编程是面向对象,而不是 面向类。换而言之,类不是面向对象编程的充要条件。
作为经典的面向对象语言,Java 不同于 C艹,不支持多继承。如果说多继承的 C艹 是“面向对象完备”的,那么能且仅能支持单继承的 Java,其面向对象也一定有不完备的地方,需要诉诸其他机制来弥补。
JS 的面向对象是不同于经典类式继承的原型机制,为什么无类、原型筑基的 JS 也能实现多继承的 C艹 的“面向对象完备”呢?搞懂这个问题,才能深入理解“对象”的本质。
可以看到,JS 虽然没有类,但也通过神秘机制具备面向对象的三大特征。如果说 JS 不懂类,那么经典面向对象语言可能不懂面向对象编程,只懂“面向类编程”。
JS 虽然全称 JavaScript,但其实和 Java 一龙一猪,原型筑基的 JS 没有类也问题不大。某种意义上,类可以视为 JS 实现面向对象的语法糖。很多人知道封装、继承和多态的面向对象特性,但不知道面向对象的定义和思想,所以才会把类和“面向对象完备”等同起来,认为 JS 不是面向对象的语言。
04. 异步编程
编程语言人气排行榜第五名是 C#,那我们就用 JS 来打败 C#。
一般认为,JS 是一门单线程语言,有且仅有一个主线程。JS 有一个基于事件循环的并发模型,这个模型与 C# 语言等模型一龙一猪。
在 JS 中,当一个函数执行时,只有在它执行完毕后,JS 才会去执行任何其他的代码。换而言之,函数执行不会被抢占,而是“运行至完成”(除了 ES6 的生成器函数)。这与 C 语言不同,在 C 语言中,如果函数在线程中运行,它可能在任何位置被终止,然后在另一个线程中运行其他代码。
单线程的优势在于,大部分情况下,JS 不需要考虑多线程某些让人头大的复杂处理。因为我只会 JS,所以无法详细说明多线程的痛点,比如竞态、死锁、线程间通信(消息、信道、队列)、Actor 模型等。
但 JS 的单线程确实启发了其他支持多线程的语言,比如阮一峰大大的博客提到,Python 的 asyncio 模块只存在一个线程,跟 JS 一样。
在异步编程方面,JS 的并发模型其实算是奇葩,因为主流的编程语言都支持多线程模型,所以学习资料可以跨语言互相借鉴,C# 关于多线程的文档就有 10 页,而单线程的 JS 就像非主流的孤勇者,很多异步的理论都用不上,所以使用起来较为简单。
05. 高潮总结
JS 自诞生以来就是一种混合范式的“多面手语言”,这是 JS 二十年来依然元气满满的根本原因。
举个栗子,你可能已经见识过“三位一体”的神奇函数了:
可以看到,在 ES6 之前,JS 中的函数其实身兼数职,正式由于 JS 是天生支持混合范式导致的。
前 JS 时代的先驱语言部分是单范式语言,比如纯粹的命令式过程语言 C 语言无法直接支持面向对象编程,而 C艹 11、Java 8 等经典面向对象语言则渐进支持函数式编程等。
后 JS 时代的现代编程语言大都直接拥抱混合范式设计,比如 TypeScript 和 Rust 等。作为混合范式语言,JS 是一种原型筑基、动态弱类型的脚本语言,同时支持面向对象编程、函数式编程、异步编程等编程范式或编程风格。
粉丝请注意,你可能偶尔会看到“JS 是一门解释型语言”的说法,其实后 ES6 时代的 JS 已经不再是纯粹的解释型语言了,也可能是 JIT(即时编译)语言,这取决于宿主环境中 JS 引擎或运行时的具体实现。
值得一提的是,混合范式语言的优势在于博采众家之长,有助于塑造攻城狮的开放式编程思维和心智模型;缺陷在于不同于单范式语言,混合范式语言可能支持了某种编程范式,但没有完全支持。(PS:编程范式完备性的判定边界是模糊的,所以偶尔也解读为风格或思维。)
因此,虽然 JS 不像 PHP 一样是地球上最好的语言,但 JS 作为地表人气最高的语言,背后是有深层原因的。
参考文献
- 阮一峰日志:ruanyifeng.com/blog/2019/1…
- MDN:developer.mozilla.org
- C# 异步编程:learn.microsoft.com/zh-cn/dotne…
来源:juejin.cn/post/7392787221097545780
此生最佩服数学家
大家好啊,我是董董灿。
之前在和不少小伙伴聊天时,都时不时的提到,搞人工智能尤其是搞算法,数学是一座很难跨过去的砍,数学太难了。
我本身也不是数学专业的,在搞AI算法的过程中,也确实遇到了很多数学问题。
用大学学的那点线性代数、概率论和微积分的知识,来推一些枯燥的数学公式,就好像是拄着拐杖去跑马拉松,虽然查查资料磨蹭磨蹭也能弄出来,但是感觉很费劲。
数学真的就是算法的基石,数学能力强、抽象能力强的人,有时候在学算法时,就像降维打击,他们会从很不可思议的角度来论证,某某算法确实是好的。
我之前见过一个同事,中科大少年班毕业,数学专业的(数学水平很高,至少比我高),有一次在和他讨论某个算法的实现时,他全程用一种我听不懂的话在说,我当时是记了一些关键字,回到工位查了很久很久。
被打击了。
数学公式和理论对我而言是枯燥的,但是数学故事是有趣的。今天,就说一个与数学相关的故事,号称“数学史上的三次危机”。
很早就听说过这个说法了,前几天在查概率论相关的资料时,突然想起来,分享一下。
在很多数学专业的学生来看,这种说法并不严谨。在中外文献中,并不存在所谓的“数学史上的三次危机”,这种说法更多的出现在科普文章以及流行度较高的民间科学杂志上。
1、第一次数学危机:长方形的对角线是什么?
在古希腊时期,毕达哥拉斯学派统治的年代,人们对于数学的认识就是:一切都是数字,这里说的数字,是我们现在理解的有理数,也就是1、2、3这种。
那时的人们认为,万事万物都可以用有理数来衡量,在一个数轴上,任何一个点都可以用一个确定的有理数字来表示。
可突然有一天,一个人站出来说,边长为 1 的正方形的对角线,在数轴上就表示不出来。
人们慌了,毕达哥拉斯学派更慌了。
对这个问题他们百思不得其解,随着问题传播的越来越广,人们开始担心,引以为傲的“一切都是数字”的数学理论,是不是有可能是错误的。
这引起了第一次数学理论基础的危机。
毕达哥拉斯学派,不允许这种不和谐的声音出现,来诋毁自己的地位,但是,当时的他们又解决不了这个问题。
于是,他们秉着解决不了这个人提出的问题,就解决了这个人的原则,把提出这个问题的人解决了。
这次危机持续了很久,直到人们提出了无理数,并且接受了无理数的存在,第一次数学危机才得以解决。
2、第二次数学危机:兔子到底能不能追上乌龟?
这是关于龟兔赛跑的故事。
有人说,如果乌龟先跑,兔子后跑。
当兔子跑到乌龟已跑出距离的一半时,乌龟又前进了一段距离,而当兔子又跑到这一段距离的时候,乌龟此时又前进了一段距离,就这样无穷无尽的跑下去,兔子永远也追不上乌龟。
这就是“龟兔赛跑”悖论,这个悖论直接导致了当时数学界的恐慌。
悖论很反直觉,但是好像又无懈可击。
人们钻研了很久,却始终找不出问题出在什么地方。
当时的人们认为:数学完了,这么简单的问题都解决不了,数学根本不靠谱。
这就好像现在有人告诉你,高铁永远追不上骑自行车的人,但是你又没办法反驳一样。
明知是错的,我却无能为力。
这便是人们津津乐道的第二次数学危机,并且直接导致了无穷与极限的发展,以及后来微积分思想的发展。
到现在,无穷级数和微积分的数学根基已经很牢固了,但是如果回过头来,如果你想反驳一下这个悖论,你应该怎么说呢?
或许你可以这么说:
世界上没有这样的兔子和乌龟,可以活无穷长的时间,这是因为时间是不可能无穷拆分的。
那如果有人继续反驳问你,你怎么证明时间是不可以无穷拆分的呢?
你就说,那是物理学家的事,不是数学家的事,让物理学家思考去吧。
反正,在无穷与极限的概念的发展中,这次危机也算是渡过了。
3、第三次数学危机:理发师该不该给自己理发
一个村子里有个理发师,突然有一天这个理发师贴了一个公告说:我只给这个村子里不给自己理发的人理发。
然后有个人跑上门问他:那你自己的头发应该谁来理呢?
理发师懵了。
如果他给自己理发,那他就不是不给自己理发的人,他就不应该给自己理发。
如果他不给自己理发,那么他就是不给自己理发的人,他就应该给自己理发。
也就说,如果存在两个互相独立的集合,一个是给自己理发的人,一个是不给自己理发的人,那么理发师属于哪个集合呢?
这就是著名的罗素悖论。
这个悖论的威力在于,当时一个著名的数学家要发表一本数学著作,在收到罗素关于这个悖论的描述后尴尬地说:我以为数学的大厦已经盖好了,没想到地基还这么不牢固。
这个问题通俗点讲就是,你可以说出一件事,如果这件事是真的,那么它就是假的,如果他是假的,那么他就是真的。
数学里的自相矛盾,然而它却符合康托尔关于集合的定义。
这个问题的解决好像是一位大佬级别的数学家,在研究了一段时间后说:不存在这样理发师,他说的话不能当做数学公理,从源头上解决了这个问题。
但是这个悖论促进促进了集合论的进一步发展。
三次数学危机,每一次都让人惶恐不安,但事实却是,每一次都极大的促进了当时数学理论的发展。
好啦,故事就分享到这。
说回AI,AI的发展绝对离不开数学,这也是为什么华为愿意花大价钱雇佣很多数学家、物理学家搞基础研究,阿里每年搞全球数学竞赛,吸纳全球数学精英。
三体里有句话,如果一旦外星文明打来,我们能与之拼一拼的绝对不是火箭大炮,而是基础物理学理论,核弹都得益于数学物理,更何况其他呢。
如果此时你正在高数课堂上,请你打起精神好好听课,没准未来拯救世界的重任就落到了你的肩上。🙃
一直很膜拜数学、物理大佬,如果有数学物理专业的大佬,可在下面留言,让小弟膜拜下~
来源:juejin.cn/post/7294619778987622411
你真的了解圣杯和双飞翼布局吗?
前言
圣杯和双飞翼布局作为面试常考的题目之一,相信大家肯定都会有着自己的一套知识储备,但是,你真的了解这两种经典布局吗?本文将介绍这两种经典布局产生的来龙去脉,以及实现这两种布局的一些方式,希望大家能够喜欢。
为什么需要圣杯和双飞翼布局
大家思考一个问题,这样一种布局,你该怎么处理呢?
常规
情况下,我们的布局思路应该这样写,从上到下,从左到右
:
<div>header</div>
<div>
<div>left</div>
<div>main</div>
<div>right</div>
</div>
<div>footer</div>
这样的三栏布局也没有什么问题,但是我们要知道一个网站的主要内容就是中间
的部分,比如像掘金:
那么对于用户来说,他们当然是希望最中间的部分首先加载出来的,能看到最重要的内容,但是因为浏览器加载dom的机制是按顺序
加载的,浏览器从HTML文档的开头开始,逐步解析
并构建文档对象模型,所以,我们想让main
首先加载出来的话,那就将它前置
,然后通过一些CSS的样式,将其继续展示出上面的三栏布局的样式:
<div>header</div>
<div>
<div>main</div>
<div>left</div>
<div>right</div>
</div>
<div>footer</div>
这就是所谓的圣杯
布局,最早是Matthew Levine 在2006年1月30日在In Search of the Holy Grail 这篇文章中提出来的;
那么完整
的效果
、实现代码
如下:
<style>
.header,
.footer {
width: 100%;
height: 200px;
background-color: pink;
}
.container {
padding: 0 100px 0 100px;
overflow: hidden;
}
.col {
position: relative;
float: left;
height: 400px;
}
.left {
background-color: green;
width: 100px;
margin-left: -100%;
left: -100px;
}
.main {
width: 100%;
background-color: blue;
}
.right {
width: 100px;
background-color: green;
margin-left: -100px;
right: -100px;
}
</style>
<div class="header">header</div>
<div class="container">
<div class="main col">main</div>
<div class="left col">left</div>
<div class="right col">right</div>
</div>
<div class="footer">footer</div>
如上述代码所示,,使用了相对定位
、浮动
和负值margin
,将left和right装到main的两侧,所以顾名:圣杯
;
但是呢,圣杯是有问题
的,在某些特殊的场景下,比如说,left和right盒子的宽度过宽
的情况下,圣杯就碎掉
了,比如将上述代码的left和right盒子的宽度改为以500px
为基准:
<style>
.header,
.footer {
width: 100%;
height: 200px;
background-color: pink;
}
.container {
padding: 0 500px 0 500px;
overflow: hidden;
}
.col {
position: relative;
float: left;
height: 400px;
}
.left {
background-color: green;
width: 500px;
margin-left: -100%;
left: -500px;
}
.main {
width: 100%;
background-color: blue;
}
.right {
width: 500px;
background-color: green;
margin-left: -500px;
right: -500px;
}
</style>
<div class="header">header</div>
<div class="container">
<div class="main col">main</div>
<div class="left col">left</div>
<div class="right col">right</div>
</div>
<div class="footer">footer</div>
正常情况下,布局还是依然正常
,只是两侧宽了
而已:
但是我们将整个窗口缩小,圣杯就碎掉了:
原因是因为 padding: 0 500px 0 500px;
,当整个窗口的最大宽度已经小于
左右两边的padding共1000px
,left和right就被挤下去了;
于是针对这种情况,淘宝UED的玉伯大大提出来了双飞翼布局
,效果和圣杯布局一样,只是他将其比作一只鸟,左翅膀、中间、右翅膀;
相比于圣杯布局,双飞翼布局在原有的main盒子再加了一层div:
<div>header</div>
<div>
<div><div>main</div></div>
<div>left</div>
<div>right</div>
</div>
<div>footer</div>
实际的效果
和代码
如下,哪怕再怎么缩,都不会被挤下去:
<style>
.header,
.footer {
width: 100%;
height: 200px;
background-color: pink;
}
.container {
padding: 0;
overflow: hidden;
}
.col {
float: left;
height: 400px;
}
.left {
background-color: green;
width: 500px;
margin-left: -100%;
}
.main {
width: 100%;
background-color: blue;
}
.main-in {
margin: 0 500px 0 500px;
}
.right {
width: 500px;
background-color: green;
margin-left: -500px;
}
</style>
<div class="header">header</div>
<div class="container">
<div class="main col">
<div class="main-in">main</div>
</div>
<div class="left col">left</div>
<div class="right col">right</div>
</div>
<div class="footer">footer</div>
圣杯布局实现方式补充
上面介绍了一种圣杯布局的实现方式,这里再介绍一种用绝对定位
的,这种方法其实也能避免
上述说的当左右两侧的盒子过于宽时,圣杯被挤破
的情况:
<style>
.header,
.footer {
width: 100%;
height: 200px;
background-color: pink;
}
.container {
position: relative;
padding: 0 100px;
}
.col {
height: 400px;
}
.left {
background-color: green;
width: 100px;
position: absolute;
left: 0;
top: 0;
}
.main {
width: 100%;
background-color: blue;
}
.right {
width: 100px;
background-color: green;
position: absolute;
right: 0;
top: 0;
}
</style>
<div class="header">header</div>
<div class="container">
<div class="main col">main</div>
<div class="left col">left</div>
<div class="right col">right</div>
</div>
<div class="footer">footer</div>
双飞翼布局实现方式补充
也是使用绝对定位
的:
<style>
.header,
.footer {
width: 100%;
height: 200px;
background-color: pink;
}
.container {
position: relative;
}
.col {
height: 400px;
}
.left {
background-color: green;
width: 500px;
position: absolute;
top: 0;
left: 0;
}
.main {
width: calc(100% - 1000px);
background-color: blue;
margin-left: 500px;
}
.main-in {
/* margin: 0 500px 0 500px; */
}
.right {
width: 500px;
background-color: green;
position: absolute;
top: 0;
right: 0;
}
</style>
<div class="header">header</div>
<div class="container">
<div class="main col">
<div class="main-in">main</div>
</div>
<div class="left col">left</div>
<div class="right col">right</div>
</div>
<div class="footer">footer</div>
其它普通的三列布局的实现
flex布局实现
<style>
* {
padding: 0;
margin: 0;
}
.header,
.footer {
width: 100%;
height: 200px;
background-color: blue;
}
.container {
display: flex;
}
.left {
width: 100px;
height: 300px;
background-color: pink;
}
.main {
flex: 1;
width: 100%;
height: 300px;
background-color: green;
}
.right {
width: 100px;
height: 300px;
background-color: pink;
}
</style>
<div class="header">header</div>
<div class="container">
<div class="left col">left</div>
<div class="main col">main</div>
<div class="right col">right</div>
</div>
<div class="footer">footer</div>
绝对定位实现
<style>
* {
padding: 0;
margin: 0;
}
.header,
.footer {
width: 100%;
height: 200px;
background-color: blue;
}
.container {
position: relative;
padding: 0 100px;
}
.left {
width: 100px;
height: 300px;
background-color: pink;
position: absolute;
top: 0;
left: 0;
}
.main {
width: 100%;
height: 300px;
background-color: green;
}
.right {
width: 100px;
height: 300px;
background-color: pink;
position: absolute;
top: 0;
right: 0;
}
</style>
<div class="header">header</div>
<div class="container">
<div class="left col">left</div>
<div class="main col">main</div>
<div class="right col">right</div>
</div>
<div class="footer">footer</div>
总结
现在真正了解到圣杯布局、双飞翼布局和普通三列布局的思想了吗?虽然它们三者最终的效果可能一样,但是实际的思路,优化也都不一样,希望能对你有所帮助!!!!感谢支持
来源:juejin.cn/post/7405467437564428299
前端中的“+”连接符,居然有鲜为人知的强大功能!
故事背景:"0"和"1"布尔判断
这几天开发,遇到一个问题:根据后端返回的isCritical判断页面是否展示【关键标签】
很难受的是,后端的isCritical的枚举值是字符串
”0“: 非关键
”1“ :关键
这意味着前端得自己转换一下,于是我写出了这样的代码
// html
<van-icon v-if="getCriticalStatus(it.isCritical)" color="#E68600"/>
// js
const getCriticalStatus = (critical: string) => {
if (critical.value === "1") return true;
return false;
}
我以为我这样写很简单了,没想到同事看到后,说我这样写麻烦了,于是给我改了一下代码
// html
<van-icon v-if="+it.isCritical" color="#E68600"/>
我大惊失色脱水缩合,这就行了?看来,我还是小看"+"运算符了!
"+"的常见使用场景
前端对"+"连字符一定不陌生,它的算术运算符功能和字符串连接功能,我们用脚趾头也能敲出来。
算术运算符
在 JavaScript 中,+
是最常见的算术运算符之一,可以用来执行加法运算。
let a = 5;
let b = 10;
let sum = a + b;
// sum的值为15
字符串连接符
+
还可以用来连接字符串。
let firstName = "石";
let lastName = "小石";
let fullName = firstName + " " + lastName; // fullName的值为"石小石"
如果是数字和字符连接,它会把数字转成字符
const a = 1
const b = "2"
const c = a + b; // c的值为字符串"12"
"+"的高级使用场景
除了上述的基本使用场景,其实它还有一些冷门但十分使用的高级使用场景。
URL编码中的空格
在 URL 编码中,+
字符可以表示空格,尤其是在查询字符串中。
http://shixiaoshi.com/search?query=hello+world
上面的代码中,hello+world
表示查询 hello world
,其中的 +
会被解码为一个空格。
但要注意的是,现代 URL 编码规范中推荐使用 %20
表示空格,而不是 +
。
一元正号运算符
+
的高级用法,再下觉得最牛逼的地方就是可以作为一元运算符使用!
+
作为一元运算符时,可以将一个值转换为数字(如果可能的话)。
let str = "123";
let num = +str;
// num的值为123,类型为number
这一用法在处理表单输入时特别有用,因为表单输入通常是字符串类型。
let inputValue = "42";
let numericValue = +inputValue; // 将字符串转换为数字42
那么回到文章开头的问题,我们看看下面的代码为什么可以生效
// html
<van-icon v-if="getCriticalStatus(it.isCritical)" color="#E68600"/>
// js
const getCriticalStatus = (critical: string) => {
if (critical.value === "1") return true;
return false;
}
// html 优化后的代码
<van-icon v-if="+it.isCritical" color="#E68600"/>
由于it.isCritical的值是字符"0"或"1",通过"+it.isCritical"转换后,其值是数字0或1,而恰好0可以当false使用,1可以当true使用!因此,上述代码可以生效!
JavaScript 中的类型转换规则会将某些值隐式转换为布尔值:
- 假值 :在转换为布尔值时被视为
false
的值,包括:false
、0
(数字零)、-0
(负零)、""
(空字符串)、null
、undefined
、NaN
(非数字)
- 真值 :除了上述假值外,所有其他值在转换为布尔值时都被视为
true
。
一元正号运算符的原理
通过上文,我们知道:当使用 +
操作符时,JavaScript 会尝试把目标值转换为数字,它遵循以下规则:。
转换规则
数字类型:
如果操作数是数字类型,一元正号运算符不会改变其值。
例如:+5
还是 5
。
// 数字类型
console.log(+5); // 5(数字)
字符串类型:
如果字符串能够被解析为有效的数字,则返回相应的数字。
如果字符串不能被解析为有效的数字(如含有非数字字符),则返回 NaN
(Not-a-Number)。
例如:+"123"
返回 123
,+"abc"
返回 NaN
。
// 字符串类型
console.log(+"42"); // 42
console.log(+"42abc"); // NaN
布尔类型:
true
会被转换为 1
。
false
会被转换为 0
。
// 布尔类型
console.log(+true); // 1
console.log(+false); // 0
null:
null
会被转换为 0
。
// null
console.log(+null); // 0
undefined:
undefined
会被转换为 NaN
。
// undefined
console.log(+undefined); // NaN
对象类型:
对象首先会通过内部的 ToPrimitive
方法被转换为一个原始值,然后再进行数字转换。通常通过调用对象的 valueOf
或 toString
方法来实现,优先调用 valueOf
。
// 对象类型
console.log(+{}); // NaN
console.log(+[]); // 0
console.log(+[10]); // 10
console.log(+["10", "20"]); // NaN
底层原理
不重要,简单说说:
在 JS引擎内部,执行一元正号运算符时,实际调用了 ToNumber
抽象操作,这个操作试图将任意类型的值转换为数字。ToNumber
操作依据 ECMAScript 规范中的规则,将不同类型的值转换为数字。
总结
一元正号运算符 +
是一个简便的方法,用于将非数字类型转换为数字。
如果你们后端返回字符串0和1,你需要转换成布尔值,使用"+"简直不要太爽
// isCritical 是字符串"0"或"1"
<van-icon v-if="+isCritical" color="#E68600"/>
或者处理表单输入时用
let inputValue = "42";
let value = +inputValue; // 将字符串转换为数字42
来源:juejin.cn/post/7402076531294863360
还在封装 xxxForm,xxxTable 残害你的同事?试试这个工具
之前写过一篇文章 我理想中的低代码开发工具的形态,已经吐槽了各种封装 xxxForm,xxxTable 的行为,这里就不啰嗦了。今天再来看看我的工具达到了什么程度。
多图预警。。。
以管理后台一个列表页为例
选择对应的模板
截图查询区域,使用 OCR 初始化查询表单的配置
截图表头,使用 OCR 初始化 table 的配置
使用 ChatGPT 翻译中文字段
生成代码
效果
目前我们没有写一行代码,就已经达到了如下的效果
下面是一部分生成的代码
import { reactive, ref } from 'vue'
import { IFetchTableListResult } from './api'
interface ITableListItem {
/**
* 决算单状态
*/
settlementStatus: string
/**
* 主合同编号
*/
mainContractNumber: string
/**
* 客户名称
*/
customerName: string
/**
* 客户手机号
*/
customerPhone: string
/**
* 房屋地址
*/
houseAddress: string
/**
* 工程管理
*/
projectManagement: string
/**
* 接口返回的数据,新增字段不需要改 ITableListItem 直接从这里取
*/
apiResult: IFetchTableListResult['result']['records'][0]
}
interface IFormData {
/**
* 决算单状态
*/
settlementStatus?: string
/**
* 主合同编号
*/
mainContractNumber?: string
/**
* 客户名称
*/
customerName?: string
/**
* 客户手机号
*/
customerPhone?: string
/**
* 工程管理
*/
projectManagement?: string
}
interface IOptionItem {
label: string
value: string
}
interface IOptions {
settlementStatus: IOptionItem[]
}
const defaultOptions: IOptions = {
settlementStatus: [],
}
export const defaultFormData: IFormData = {
settlementStatus: undefined,
mainContractNumber: undefined,
customerName: undefined,
customerPhone: undefined,
projectManagement: undefined,
}
export const useModel = () => {
const filterForm = reactive<IFormData>({ ...defaultFormData })
const options = reactive<IOptions>({ ...defaultOptions })
const tableList = ref<(ITableListItem & { _?: unknown })[]>([])
const pagination = reactive<{
page: number
pageSize: number
total: number
}>({
page: 1,
pageSize: 10,
total: 0,
})
const loading = reactive<{ list: boolean }>({
list: false,
})
return {
filterForm,
options,
tableList,
pagination,
loading,
}
}
export type Model = ReturnType<typeof useModel>
这就是用模板生成的好处,有规范,随时可以改,而封装 xxxForm,xxxTable 就是一个黑盒。
原理
下面大致说一下原理
首先是写好一个个模版,vscode 插件读取指定目录下模版显示到界面上
每个模版下可能包含如下内容:
选择模版后,进入动态表单配置界面
动态表单是读取 config/schema.json 里的内容进行动态渲染的,目前支持 amis、form-render、formily
配置表单是为了生成 JSON 数据,然后根据 JSON 数据生成代码。所以最终还是无法避免的使用私有的 DSL ,但是生成后的代码是没有私有 DSL 的痕迹的。生成代码本质是 JSON + EJS 模版引擎编译 src 目录下的 ejs 文件。
为了加快表单的配置,可以自定义脚本进行操作
这部分内容是读取 config/preview.json 内容进行显示的
选择对应的脚本方法后,插件会动态加载 script/index.js 脚本,并执行里面对应的方法
以 initColumnsFromImage 方法为例,这个方法是读取剪贴板里的图片,然后使用百度 OCR 解析出文本,再使用文本初始化表单
initColumnsFromImage: async (lowcodeContext) => {
context.lowcodeContext = lowcodeContext;
const res = await main.handleInitColumnsFromImage();
return res;
},
export async function handleInitColumnsFromImage() {
const { lowcodeContext } = context;
if (!lowcodeContext?.clipboardImage) {
window.showInformationMessage('剪贴板里没有截图');
return lowcodeContext?.model;
}
const ocrRes = await generalBasic({ image: lowcodeContext!.clipboardImage! });
env.clipboard.writeText(ocrRes.words_result.map((s) => s.words).join('\r\n'));
window.showInformationMessage('内容已经复制到剪贴板');
const columns = ocrRes.words_result.map((s) => ({
slot: false,
title: s.words,
dataIndex: s.words,
key: s.words,
}));
return { ...lowcodeContext.model, columns };
}
反正就是可以根据自己的需求定义各种各样的脚本。比如使用 ChatGPT 翻译 JSON 里的指定字段,可以看我的上一篇文章 TypeChat、JSONSchemaChat实战 - 让ChatGPT更听你的话
再比如要实现把中文翻译成英文,然后英文使用驼峰语法,这样就可以将中文转成英文代码变量,下面是实现的效果
选择对应的命令菜单后 vscode 插件会加载对应模版里的脚本,然后执行里面的 onSelect 方法。
main.ts 代码如下
import { env, window, Range } from 'vscode';
import { context } from './context';
export async function bootstrap() {
const clipboardText = await env.clipboard.readText();
const { selection, document } = window.activeTextEditor!;
const selectText = document.getText(selection).trim();
let content = await context.lowcodeContext!.createChatCompletion({
messages: [
{
role: 'system',
content: `你是一个翻译家,你的目标是把中文翻译成英文单词,请翻译时使用驼峰格式,小写字母开头,不要带翻译腔,而是要翻译得自然、流畅和地道,使用优美和高雅的表达方式。请翻译下面用户输入的内容`,
},
{
role: 'user',
content: selectText || clipboardText,
},
],
});
content = content.charAt(0).toLowerCase() + content.slice(1);
window.activeTextEditor?.edit((editBuilder) => {
if (window.activeTextEditor?.selection.isEmpty) {
editBuilder.insert(window.activeTextEditor.selection.start, content);
} else {
editBuilder.replace(
new Range(
window.activeTextEditor!.selection.start,
window.activeTextEditor!.selection.end,
),
content,
);
}
});
}
使用了 ChatGPT。
再来看看,之前生成管理后台 CURD 页面的时候,连 mock 也一起生成了,主要逻辑放在了 complete 方法里,这是插件的一个生命周期函数。
因为 mock 服务在另一个项目里,所以需要跨目录去生成代码,这里我在 mock 服务里加了个接口返回 mock 项目所在的目录
.get(`/mockProjectPath`, async (ctx, next) => {
ctx.body = {
status: 200,
msg: '',
result: __dirname,
};
})
生成代码的时候请求这个接口,就知道往哪个目录生成代码了
const mockProjectPathRes = await axios
.get('http://localhost:3001/mockProjectPath', { timeout: 1000 })
.catch(() => {
window.showInformationMessage(
'获取 mock 项目路径失败,跳过更新 mock 服务',
);
});
if (mockProjectPathRes?.data.result) {
const projectName = workspace.rootPath
?.replace(/\\/g, '/')
.split('/')
.pop();
const mockRouteFile = path.join(
mockProjectPathRes.data.result,
`${projectName}.js`,
);
let mockFileContent = `
import KoaRouter from 'koa-router';
import proxy from '../middleware/Proxy';
import { delay } from '../lib/util';
const Mock = require('mockjs');
const { Random } = Mock;
const router = new KoaRouter();
router{{mockScript}}
module.exports = router;
`;
if (fs.existsSync(mockRouteFile)) {
mockFileContent = fs.readFileSync(mockRouteFile).toString().toString();
const index = mockFileContent.lastIndexOf(')') + 1;
mockFileContent = `${mockFileContent.substring(
0,
index,
)}{{mockScript}}\n${mockFileContent.substring(index)}`;
}
mockFileContent = mockFileContent.replace(/{{mockScript}}/g, mockScript);
fs.writeFileSync(mockRouteFile, mockFileContent);
try {
execa.sync('node', [
path.join(
mockProjectPathRes.data.result
.replace(/\\/g, '/')
.replace('/src/routes', ''),
'/node_modules/eslint/bin/eslint.js',
),
mockRouteFile,
'--resolve-plugins-relative-to',
mockProjectPathRes.data.result
.replace(/\\/g, '/')
.replace('/src/routes', ''),
'--fix',
]);
} catch (err) {
console.log(err);
}
mock 项目也可以通过 vscode 插件快速创建和使用
上面展示的模版都放在了 github.com/lowcode-sca… 仓库里,照着 README 步骤做就可以使用了。
来源:juejin.cn/post/7315242945454735414
领导:你加的水印怎么还能被删掉的,扣工资!
故事是这样的
领导:小李,你加的水印怎么还能被删掉的?这可是关乎公司信息安全的大事!这种疏忽怎么能不扣工资呢?
小李:领导,请您听我解释一下!我确实按照常规的方法加了水印,可是……
领导:(打断)但是什么?难道这就是你对公司资料的保护吗?
小李:我也不明白,按理说水印是无法删除的,我会再仔细检查一下……
领导:我不能容忍这样的失误。这种安全隐患严重影响了我们的机密性。
小李焦虑地试图解释,但领导的目光如同刀剑一般锐利。他决定,这次一定要找到解决方法,否则,这将是一场职场危机……
水印组件
小李想到antd中有现成的水印组件,便去研究了一下。即使删掉了水印div,水印依然存在,因为瞬间又生成了一个相同的水印div。他一瞬间想到了解决方案,并开始了重构水印组件。
原始代码
//app.vue
<template>
<div>
<Watermark text="前端百事通">
<div class="content"></div>
</Watermark>
</div>
</template>
<script setup>
import Watermark from './components/Watermark.vue';
</script>
<style scoped>
.content{
width: 400px;
height: 400px;
background-color: aquamarine;
}
</style>
//watermark.vue
<template>
<div ref="watermarkRef" class="watermark-container">
<slot>
</slot>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue';
const watermarkRef=ref(null)
const props = defineProps({
text: {
type: String,
default: '前端百事通'
},
fontSize: {
type: Number,
default: 14
},
gap: {
type: Number,
default: 50
},
rotate: {
type: Number,
default: 45
}
})
onMounted(() => {
addWatermark()
})
const addWatermark = () => {
const { rotate, gap, text, fontSize } = props
const color = 'rgba(0, 0, 0, 0.3)'; // 可以从props中传入
const watermarkContainer = watermarkRef.value;
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
const font=fontSize+'px DejaVu Sans Mono'
// 设置水印文字的宽度和高度
const metrics = context.measureText(text);
const canvasWidth=metrics.width+gap
canvas.width=canvasWidth
canvas.height=canvasWidth
// 绘制水印文字
context.translate(canvas.width/2,canvas.height/2)
context.rotate((-1 * rotate * Math.PI / 180));
context.fillStyle = color;
context.font=font
context.textAlign='center'
context.textBaseline='middle'
context.fillText(text,0,0)
// 将canvas转为图片
const url = canvas.toDataURL('image/png');
// 创建水印元素并添加到容器中
const watermarkLayer = document.createElement('div');
watermarkLayer.style.position = 'absolute';
watermarkLayer.style.top = '0';
watermarkLayer.style.left = '0';
watermarkLayer.style.width = '100%';
watermarkLayer.style.height = '100%';
watermarkLayer.style.pointerEvents = 'none';
watermarkLayer.style.backgroundImage = `url(${url})`;
watermarkLayer.style.backgroundRepeat = 'repeat';
watermarkLayer.style.zIndex = '9999';
watermarkContainer.appendChild(watermarkLayer);
}
</script>
<style>
.watermark-container {
position: relative;
width: 100%;
height: 100%;
overflow: auto;
}
</style>
防篡改思路
- 监听删除dom操作,在删除dom操作的瞬间重新生成一个相同的dom元素
- 监听修改dom样式操作
- 不能使用onMounted,改为watchEffect进行监听操作
使用MutationObserver监听整个区域
let ob
onMounted(() => {
ob=new MutationObserver((records)=>{
console.log(records)
})
ob.observe(watermarkRef.value,{
childList:true,
attributes:true,
subtree:true
})
})
onUnmounted(()=>{
ob.disconnect()
})
在删除水印div之后,打印一下看看records是什么。
在修改div样式之后,打印一下records
很明显,如果是删除,我们就关注removedNodes字段,如果是修改,我们就关注attributeName字段。
onMounted(() => {
ob=new MutationObserver((records)=>{
for(let item of records){
//监听删除
for(let ele of item.removedNodes){
if(ele===watermarkDiv){
generateFlag.value=!generateFlag.value
return
}
}
//监听修改
if(item.attributeName==='style'){
generateFlag.value=!generateFlag.value
return
}
}
})
ob.observe(watermarkRef.value,{
childList:true,
attributes:true,
subtree:true
})
})
watchEffect(() => {
//generateFlag的用处是让watchEffect收集这个依赖
//通过改变generateFlag的值来重新调用生成水印的函数
generateFlag.value
if(watermarkRef.value){
addWatermark()
}
})
最终,小李向领导展示了新的水印组件,取得了领导的认可和赞许,保住了工资。
全剧终。
文章同步发表于前端百事通公众号,欢迎关注!
来源:juejin.cn/post/7362309246556356647
终于搞懂类型声明文件.d.ts和declare了,原来用处如此大
项目中的.d.ts和declare
最近开发项目,发现公司代码里都有一些.d.ts后缀的文件
还有一些奇奇怪怪的declare代码
秉持着虚心学习的态度,我向同事请教了这些知识点,发现这些东西居然蛮重要的。于是,我根据自己的理解,把这些知识简单总结一下。
类型声明文件.d.ts
为什么需要 .d.ts
文件?
如果我们在ts项目中使用第三方库时,如果这个库内置类型声明文件.d.ts,我们在写代码时可以获得对应的代码补全、接口提示等功能。
比如,我们在index.ts中使用aixos时:
当我们引入axios时,ts会检索aixos的package.json文件,并通过其types属性查找类型声明文件,查找到index.d.ts这个文件后,就会根据其内部配置进行语法提示。
但是如果某个库没有内置类型声明文件时,我们使用这个库,不会获得Ts的语法提示,甚至会有类型报错警告
像这种没有内置类型声明文件的,我们就可以自己创建一个xx.d.ts的文件来自己声明,ts会自动读取到这个文件里面的内容的。比如,我们在index.ts中使用"vue-drag",会提示缺少声明文件。
由于这个库没有@types/xxxx声明包,因此,我们可以在项目内自定义一个vueDrag.d.ts声明文件。
// vueDrag.d.ts
declare module 'vue-drag'
这个时候,就不会报错了,没什么警告了。
第三方库的默认类型声明文件
当我们引入第三方库时,ts会自动检索aixos的package.json文件,并通过其types属性查找类型声明文件,查找到index.d.ts这个文件后,就会根据其内部配置进行语法提示。比如,我们刚才说的axios
- "typings"与"types"具有相同的意义,也可以使用它。
- 主声明文件名是index.d.ts并且位置在包的根目录里(与index.js并列),你就不需要使用"types"属性指定了。
第三方库的@types/xxxx类型声明文件
如express这类框架,它们的开发时Ts还没有流行,自然没有使用Ts进行开发,也自然不会有ts的类型声明文件。如果你想引入它们时也获得Ts的语法提示,就需要引入它们对应的声明文件npm包了。
使用声明文件包,不用重构原来的代码就可以在引入这些库时获得Ts的语法提示
比如,我们安装express对应的声明文件包后,就可以获得相应的语法提示了。
npm i --save-dev @types/express
@types/express包内的声明文件
.d.ts声明文件
通过上述的几个示例,我们可以知道.d.ts文件的作用和@types/xxxx包一致,@type/xxx需要下载使用,而.d.ts是我们自己创建在项目内的。
.d.ts文件除了可以声明模块,也可以用来声明变量。
例如,我们有一个简单的 JavaScript 函数,用于计算两个数字的总和:
// math.js
const sum = (a, b) => a + b
export { sum }
TypeScript 没有关于函数的任何信息,包括名称、参数类型。为了在 TypeScript 文件中使用该函数,我们在 d.ts 文件中提供其定义:
// math.d.ts
declare function sum(a: number, b: number): number
现在,我们可以在 TypeScript 中使用该函数,而不会出现任何编译错误。
.ts 是标准的 TypeScript 文件。其内容将被编译为 JavaScript。
*.d.ts 是允许在 TypeScript 中使用现有 JavaScript 代码的类型定义文件,其不会编译为 JavaScript。
shims-vue.d.ts
shims-vue.d.ts
文件的主要作用是声明 Vue 文件的模块类型,使得 TypeScript 能够正确地处理 .vue
文件,并且不再报错。通常这个文件会放在项目的根目录或 src
目录中。
shims-vue.d.ts
文件的内容一般长这样:
// shims-vue.d.ts
declare module '*.vue' {
import { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}
declare module '*.vue'
: 这行代码声明了一个模块,匹配所有以.vue
结尾的文件。*
是通配符,表示任意文件名。import { DefineComponent } from 'vue';
: 引入 Vue 的DefineComponent
类型。这是 Vue 3 中定义组件的类型,它具有良好的类型推断和检查功能。const component: DefineComponent<{}, {}, any>;
: 定义一个常量component
,它的类型是DefineComponent
,并且泛型参数设置为{}
表示没有 props 和 methods 的基本 Vue 组件类型。any
用来宽泛地表示组件的任意状态。export default component;
: 将这个组件类型默认导出。这样,当你在 TypeScript 文件中导入.vue
文件时,TypeScript 就知道导入的内容是一个 Vue 组件。
declare
.d.ts 文件中的顶级声明必须以 “declare” 或 “export” 修饰符开头。
通过declare声明的类型或者变量或者模块,在include包含的文件范围内,都可以直接引用而不用去import或者import type相应的变量或者类型。
- declare声明一个类型
declare type Asd {
name: string;
}
- declare声明一个模块
declare module '*.css';
declare module '*.less';
declare module '*.png';
.d.ts文件顶级声明declare最好不要跟export同级使用,不然在其他ts引用这个.d.ts的内容的时候,就需要手动import导入了
在.d.ts文件里如果顶级声明不用export的话,declare和直接写type、interface效果是一样的,在其他地方都可以直接引用
declare type Ass = {
a: string;
}
type Bss = {
b: string;
};
来源:juejin.cn/post/7402891257196691468
前端:金额高精度处理
Decimal 是什么
想必大家在用js 处理 数字的 加减乘除的时候,或许都有遇到过 精度不够 的问题,还有那些经典的面试题 0.2+0.1 !== 0.3,
至于原因,那就是 js 计算底层用的是 IEEE 754 ,精度上有限制,
那么Decimal.js 就是帮助我们解决 js中的精度失准的问题。
原理
- 它的原理就是将数字用字符串表示,字符串在计算机中可以说是无限的。
- 并使用基于字符串的算术运算,以避免浮点数运算中的精度丢失。它使用了一种叫做十进制浮点数算术(Decimal Floating Point Arithmetic)的算法来进行精确计算。
- 具体来说,decimal.js库将数字表示为一个字符串,其中包含整数部分、小数部分和一些其他的元数据。它提供了一系列的方法和运算符,用于执行精确的加减乘除、取模、幂运算等操作。
精度丢失用例
const a = 31181.82
const b = 50090.91
console.log(a+b) //81272.73000000001
Decimal 的引入 与 加减乘除
- 如何引入
npm install --save decimal.js // 安装
import Decimal from "decimal.js" // 具体文件中引入
- 加
let a = 1
let b = 6
// a 与 b 可以是 任何类型,Decimal 内部会自己处理兼容
// 下面两种都可以 可以带 new 也不可以不带 new(推荐带new)
let res = new Decimal(a).add(new Decimal(b))
let res = Decimal(a).add(Decimal(b))
- 减
let a = "4"
let b = "8"
// a 与 b 可以是 任何类型,Decimal 内部会自己处理兼容
// 下面两种都可以 可以带 new 也不可以不带 new
let res = new Decimal(a).sub(new Decimal(b))
let res = Decimal(a).sub(Decimal(b))
- 乘
let a = 1
let b = 6
// a 与 b 可以是 任何类型,Decimal 内部会自己处理兼容
// 下面两种都可以 可以带 new 也不可以不带 new
let res = new Decimal(a).mul(new Decimal(b))
let res = Decimal(a).mul(Decimal(b))
- 除
let a = 1
let b = 6
// a 与 b 可以是 任何类型,Decimal 内部会自己处理兼容
// 下面两种都可以 可以带 new 也不可以不带 new
let res = new Decimal(a).div(new Decimal(b))
let res = Decimal(a).div(Decimal(b))
注意
上面的结果是 一个 Decimal 对象,你可以转换成 Number 或则 String
let res = Decimal(a).div(Decimal(b)).toNumber() // 结果转换成 Number
let res = Decimal(a).div(Decimal(b)).toString() // 结果转换成 String
关于保存几位小数相关
//查看有几位小数 (注意不计算 小数点 最后 末尾 的 0)
y = new Decimal(987000.000)
y.sd() // '3' 有效位数
y.sd(true) // '6' 总共位数
// 保留 多少个位数 (小数位 会补0)
x = 45.6
x.toPrecision(5) // '45.600'
// 保留 多少位有效位数(小数位 不会补0,是计算的有效位数)
x = new Decimal(9876.5)
x.toSignificantDigits(6) // '9876.5' 不会补0 只是针对有效位数
// 保留几位小数 , 跟 js 中的 number 一样
toFixed
x = 3.456
// 向下取整
x.toFixed(2, Decimal.ROUND_DOWN) // '3.45' (舍入模式 向上0 向下1 四舍五入 4,7)
// 向上取整
Decimal.ROUND_UP
//四舍五入
ROUND_HALF_UP //(主要)
// 使用例子
let num2 = 0.2
let num3 = 0.1
let res = new Decimal(num2).add(new Decimal(num3)).toFixed(2, Decimal.ROUND_HALF_UP)
console.log(res); //返回值是字符串类型
超过 javascript 允许的数字
如果使用超过 javascript 允许的数字的值,建议传递字符串而不是数字,以避免潜在的精度损失。
new Decimal(1.0000000000000001); // '1'
new Decimal(88259496234518.57); // '88259496234518.56'
new Decimal(99999999999999999999); // '100000000000000000000'
new Decimal(2e308); // 'Infinity'
new Decimal(1e-324); // '0'
new Decimal(0.7 + 0.1); // '0.7999999999999999'
可读性
与 JavaScript 数字一样,字符串可以包含下划线作为分隔符以提高可读性。
x = new Decimal("2_147_483_647");
其它进制的数字
如果包含适当的前缀,则也接受二进制、十六进制或八进制表示法的字符串值。
x = new Decimal("0xff.f"); // '255.9375'
y = new Decimal("0b10101100"); // '172'
z = x.plus(y); // '427.9375'
z.toBinary(); // '0b110101011.1111'
z.toBinary(13); // '0b1.101010111111p+8'
x = new Decimal(
"0b1.1111111111111111111111111111111111111111111111111111p+1023"
);
// '1.7976931348623157081e+308'
最后:希望本篇文章能帮到您!
来源:juejin.cn/post/7405153695507234867
视差滚动效果实现
视差滚动是一种在网页设计和视频游戏中常见的视觉效果技术,它通过在不同速度上移动页面或屏幕上的多层图像,创造出深度感和动感。
这种效果通过前景、中景和背景以不同的速度移动来实现,使得近处的对象看起来移动得更快,而远处的对象移动得较慢。
在官网中适当的使用视差效果,可以增加视觉吸引力,提高用户的参与度,从而提升网站和品牌的形象。本文通过JavaScript、CSS多种方式并在React框架下进行了视差效果的实现,供你参考指正。
实现方式
1、background-attachment
通过配置该 CSS 属性值为fixed
可以达到背景图像的位置相对于视口固定,其他元素正常滚动的效果。但该方法的视觉表现单一,没有纵深,缺少动感。
.parallax-box {
width: 100%;
height: 100vh;
background-image: url("https://picsum.photos/800");
background-size: cover;
background-attachment: fixed;
display: flex;
justify-content: center;
align-items: center;
}
2、Transform 3D
在 CSS 中使用 3D 变换效果,通过将元素划分至不同的纵深层级,在滚动时相对视口不同距离的元素,滚动所产生的位移在视觉上就会呈现越近的元素滚动速度越快,相反越远的元素滚动速度就越慢。
为方便理解,你可以想象正开车行驶在公路上,汽车向前移动,你转头看向窗外,近处的树木一闪而过,远方的群山和风景慢慢的渐行渐远,逐渐的在视野中消失,而天边的太阳却只会在很长的一段距离细微的移动。
.parallax {
perspective: 1px; /* 设置透视效果,为3D变换创造深度感 */
overflow-x: hidden;
overflow-y: auto;
height: 100vh;
}
.parallax__group {
transform-style: preserve-3d; /* 保留子元素3D变换效果 */
position: relative;
height: 100vh;
}
.parallax__layer {
position: absolute;
inset: 0;
display: flex;
justify-content: center;
align-items: center;
}
/* 背景层样式,设置为最远的层 */
.parallax__layer--back {
transform: translateZ(-2px) scale(3);
z-index: 1;
}
/* 中间层样式,设置为中等距离的层 */
.parallax__layer--base {
transform: translateZ(-1px) scale(2);
z-index: 2;
}
/* 前景层样式,设置为最近的层 */
.parallax__layer--front {
transform: translateZ(0px);
z-index: 3;
}
实现原理
通过设置 perspective 属性,为整个容器创建一个 3D 空间。
使用 transform-style: preserve-3d 保持子元素的 3D 变换效果。
将内容分为多个层(背景、中间、前景),使用 translateZ() 将它们放置在 3D 空间的不同深度。
对于较远的层(如背景层),使用 scale() 进行放大,以补偿由于距离产生的视觉缩小效果。
当用户滚动页面时,由于各层位于不同的 Z 轴位置,它们会以不同的速度移动,从而产生视差效果。
3、ReactScrollParallax
想得到更炫酷的滚动视差效果,纯 CSS 的实现方式就会有些吃力。
如下是在 React 中实现示例,通过监听滚动事件,封装统一的视差组件,来达到多样的动画效果。
const Parallax = ({ children, effects = [], speed = 1, style = {} }) => {
// 状态hooks:用于存储动画效果的当前值
const [transform, setTransform] = useState("");
useEffect(() => {
if (!Array.isArray(effects) || effects.length === 0) {
console.warn("ParallaxElement: effects should be a non-empty array");
return;
}
const handleScroll = () => {
// 计算滚动进度
const scrollProgress =
(window.scrollY /
(document.documentElement.scrollHeight - window.innerHeight)) *
speed;
let transformString = "";
// 处理每个效果
effects.forEach((effect) => {
const { property, startValue, endValue, unit = "" } = effect;
const value =
startValue +
(endValue - startValue) * Math.min(Math.max(scrollProgress, 0), 1);
switch (property) {
case "translateX":
case "translateY":
transformString += `${property}(${value}${unit}) `;
break;
case "scale":
transformString += `scale(${value}) `;
break;
case "rotate":
transformString += `rotate(${value}${unit}) `;
break;
// 更多的动画效果...
default:
console.warn(`Unsupported effect property: ${property}`);
}
});
// 更新状态
setTransform(transformString);
};
window.addEventListener("scroll", handleScroll);
// 初始化位置
handleScroll();
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, [effects, speed]);
// 渲染带有计算样式的子元素
return <div style={{ ...style, transform }}>{children}</div>;
};
在此基础上你可以添加缓动函数
使动画效果更加平滑;以及使用requestAnimationFrame
获得更高的动画性能。
requestAnimationFrame 带来的性能提升
同步浏览器渲染周期:requestAnimationFrame 会在浏览器下一次重绘之前调用指定的回调函数。这确保了动画更新与浏览器的渲染周期同步,从而产生更流畑的动画效果。
提高性能:与使用 setInterval 或 setTimeout 相比,requestAnimationFrame 可以更高效地管理动画。它只在浏览器准备好进行下一次重绘时才会执行,避免了不必要的计算和重绘。
优化电池使用:在不可见的标签页或最小化的窗口中,requestAnimationFrame 会自动暂停,这可以节省 CPU 周期和电池寿命。
适应显示器刷新率:requestAnimationFrame 会自动适应显示器的刷新率。这意味着在 60Hz、120Hz 或其他刷新率的显示器上,动画都能保持流畑。
避免丢帧:由于与浏览器的渲染周期同步,使用 requestAnimationFrame 可以减少丢帧现象,特别是在高负荷情况下。
更精确的时间控制:requestAnimationFrame 提供了一个时间戳参数,允许更精确地控制动画的时间。
4、组件库方案
在当前成熟的前端生态中,想要获得精彩的视差动画效果,你可以通过现有的开源组件库来高效的完成开发。
以下是一些你可以尝试的主流组件库:
引用参考
How to create parallax scrolling with CSS
来源:juejin.cn/post/7406161967617163301
阶层必然会分化,但维度不只有金钱
前言
Hi 你好,我是东东拿铁,一个在“玩游戏”的后端程序员。
先问大家一个问题,如果阶层分化是必然的,你还会有玩下去人生这个“无限游戏”的动力吗?
让我们从一个游戏说起。
1996年,通过计算机建模理解社会演化的思潮在学术界正兴,美国布鲁金斯学会的艾伯斯坦和阿克斯特尔设计了一个关于财富分配的游戏,命名为“糖人世界”(Sugarscape)。
他们设计出一个模拟的地形图,深色区域含糖量高,浅色区域含糖量少,而白色区域则不产糖,对应资源富裕区、有限区、贫困区和沙漠区。
糖在被吃掉以后过一段时间会再长出来。然后他们会随机丢一些小糖人上去——这些小糖人遵循几个简单规则:
- 看四周6个方格,找到含糖量最高的区域,移动过去吃糖;
- 每天会消耗一定的糖(新陈代谢),如果消耗大于产出,则会死掉出局;
- 每个糖人的天赋、视力和新陈代谢是随机的。有人天生视力好,别人看1格,自己看4格,比较占优势;有人则比别人消耗少,别人每天消耗2格,他只要1格,可理解为体力好。还有一些天生富二代,携带更多糖出生。
一开始的时候,大家都差不多,最富裕的24个人有10块糖;但跑着跑着,不均衡开始出现。在第189回合以后,贫富差距出现了,最富裕的2人有225块糖,而有131个人只有1块。
注:横轴为财富数,纵轴为人数。
具体细节大家可以自行了解,但游戏告诉我们一个结论:在一个流动、开放的社会里,阶层分化是稳定且可预期的。
游戏如此,现实世界也如此。
阶级分化
我看过一部分书,比如《皮囊》、《活着》、《许三观卖血记》、《兄弟》,也听过很多耳熟能详的作品比如《平凡的世界》、《人世间》,我特别喜欢类似的作品。
虽然自己出生在城市,生活的时代早已和历史上的时代有所不同,也不需要为物质、精神需求所忧虑,但在某些时候,却又能感同身受。
因为这些优秀的作品背后有一个共性,就是关注普通人的生活与命运。
投射现实,我们大概率属于拥有更少糖的那部分小糖人。
比如,出生时拥有的糖不够多,又或者生在离糖山更远的地方。
当然,现实生活比游戏复杂的多,但我们能做的事情,也比小糖人能做的多。
比如
- 小糖人无法学习,我们可以
- 小糖人走到糖山,需要走很多步,但是我们有网络,交通工具
- 游戏中的糖山,是不会移动的,但是我们时代和机会是在不断变化的
所以,阶层固化是必然的,但是个体的命运却不是。
阶级分化,到底是分化了什么。
我之前肤浅的认为,阶级分化,无非就是财富的分化。
我有一个很有钱的阿姨,她的女儿比我小一些,之前一起吃饭的时候,聊起买衣服的话题。有一个服装品牌叫“优衣库”,大家应该都听过。经济实惠,质量虽然赶不上大品牌,但也还可以,我现在去商场也会习惯性的去优衣库逛逛。
有一次一起吃饭,我记着我们聊到优衣库的时候,她面露难色的说了一句:“优衣库的衣服能穿吗。”
毕竟平常接触到的非常有钱的人的机会不多,即使十年过去了,这件事给正在上学的我留下的印象非常深刻。
毕竟我们就是要让一部分人先富起来,先富带动后富,所以分化就是财富的分化。
心智
最近在看古典老师的书《跃迁》,这本书里举的一个例子,让我对分化,有了不一样的看法。
原文如下:
我在当GRE老师的时候,曾经教过收费很高的一对一英语私教班,学生家长一般分成两种:一种是真的很有钱,不在乎钱的家长;有一些则是收入中等,希望孩子有出息,一咬牙花大价钱的家长。对于前者,我倾尽全力让自己对得起这个价格;对于后者,我则更加苦口婆心,小心谨慎,偶尔还开个小灶,我知道这些钱对这个家庭意味着什么。
每次,我都会跟接孩子的家长聊聊孩子的学习进度。我会说“你们家孩子词汇量还不够,要把这6000词汇尽快背完,然后阅读分才会好。”这个时候,我经常收到两种回答。那些中产阶层家庭的家长会说:“听到没有!要听古典老师的话!回去好好背,好不好?”孩子温顺地点头。而有些真正聪明的家长则会笑着说:“古典老师,我们家孩子就是不爱背单词,但是他喜欢阅读。我们进度不需要那么赶,你能不能陪他多读点儿有趣的英文书?”后面这种回答,震撼了我。
这段描述给我了非常大的冲击,毕竟教育是每一个人都会经历的,我不禁回忆起了我上学的时候,想想看,如果当我们遇到单词量不够的时候,父母怎么面对这个问题,我们又是如何面对这个问题呢?
无论是初中、高中、还是大学,在我想提高单词量的时候,我会选择打开常用词汇的小册子,打开当时很流行的什么“百词斩”等app,开始背单词。
父母应该会问,单词背的怎么样了,老师布置的任务都完成了吗?
所以最后我印象最深的词汇就是“abandon”。
我从初中开始便开始住校,直到大学毕业,住校的时间足足有十年。初中、高中寄宿制的生活,接受的是军事化、填鸭式的教育,我们只管上课、做题,如果你表示不满,老师就会让你去操场跑几圈。
直到近几年我才明白,我才知道住校期间的那段学习,有多么的低效。
现在我也当了父亲,有了孩子后,我时常在考虑,相比于我们这一代的放养式教育,我应该如何把我学会的技巧、方法和道理传递给孩子,让他更高效的成长。
比如我教会他100种背单词的技巧,告诉他艾宾浩斯遗忘曲线记忆法?还是便利贴贴满冰箱让他实时看到,利用碎片化时间记忆?还是让他像我一样借助一些APP辅助记忆?
在我眼里这些技巧确实是对的,也都是有一定效果的,但是孩子真的能够听进去,学会这些吗?我总是感觉到,这并不是父母能做到最好的。
我之前肤浅的认为,阶级分化,无非就是财富的分化,毕竟我们就是要让一部分人先富起来,带动后富,所以分化的就是分化的财富。
而古典老师的这个例子,让我看到了不同阶级的家庭,对于背单词这件事情不一样的选择。
前者,被动接受,按部就班,最后成长成为一个优秀的员工。
后者,主动选择,试图寻找最高效的方式,创造一些传统教育教不会的学习思维。
父母在做,孩子在看。
教育与学习,不同家庭会有如此大的差异,那么个人成长呢,工作、甚至人生选择呢,不同的思维又会对我们产生多大的影响呢?
看到这里,你是否对阶级分化有了不一样的看法,我发现分化的不仅是财富,还有心智。
影响心智的因素
是什么导致了心智的分化?看了许多书,也看过很多大佬给出的建议和方法,我想从两个方面来聊聊,一个是自控力,一个是思维带宽。
自控力
先从自控力说起,自控力是什么,《自控力》中是这样定义的:
自控力是控制自己的注意力、情绪和欲望的能力,由 “我要做”“我不要”“我想要” 这三种力量组成。
我要做:是为了更好的未来做自己不喜欢的事情的能力,比如坚持学习、完成工作任务等;
我不要:是即使面对诱惑也能说 “不” 的能力,像抵制美食、视频的诱惑等;
我想要:是记住自己真正想要的东西的能力,它能让我们在面对短期诱惑时,不忘长期目标。
你如果白天面临较多需要自控的事情,比如美食、工作、阅读等,下班回到家一旦自控力耗尽,就很容易放纵,开始打游戏、刷视频。
一次放纵对于富人来说也许不算是损失,对于穷人来说则会浪费很多宝贵的机会。
普通人并非不懂得延迟满足,只是他们对自己延迟满足的肌肉的操控力,早就被诱惑消耗得所剩无几。
刚刚在我写着这篇文章,不自觉地拿起了手机刷小红书,幸亏写的是这个主题,我及时的意识到又把我拉了回来。
但是,《自控力》书中还提到了“自控力肌肉”这个概念,的角度解释过这个问题。自控力如肌肉,用多了会疲劳。
说一个最近的例子,上周孩子睡觉一直很晚,几乎要到晚上十点多才能入睡,为了保证阅读时间,晚上收拾完看书,睡觉的时候几乎都在十二点以后了。
但实际上,我能够看书的时间依然很少,我总会用“我都这么晚看书了,不如先放松一下”的心态,开始去刷视频或者一些别的东西,然而回过神来,一个小时就过去了。
思维带宽
《稀缺》这本书中提出了“思维带宽”的概念,思维带宽是指心智的容量,它由认知能力和执行控制力构成。
当你处于稀缺状态时,无论是时间稀缺、金钱稀缺还是其他资源稀缺,你的注意力会高度集中在稀缺的事物上,这就导致心智容量被过度占据。
这种情况下,你用于处理其他事务的认知能力和执行控制力就会减弱。
例如,普通人可能会因过于关注当下的金钱问题,而在做决策时忽视了对未来的投资和规划;工作忙碌的人可能因专注于眼前的紧急任务,而忽略了重要但不紧急的事情,如锻炼身体、陪伴家人等。
如果总想着怎么换个大房子,怎么换个好车子,哪里有时间思考什么个人发展、儿女教育呢?发展战略显然才是核心。如果说贫穷是一种“思维带宽”的稀缺,注意力资源就变得非常重要,提升认知时大部分都是反人性的,需要巨大的带宽。
所以,如果你注意力稀缺,即使你知道要做些什么,也会陷入战术勤奋、战略懒惰的困局。
在《贫穷的本质》这本书里,作者阿比吉特和埃斯特观察到很多捐赠者的本意,是希望穷人将捐款用在教育、健康上,实际却往往被花在了消费品、奢侈品上,因为穷人和富人处于不同的自控力和心智资源层面。
如果把这种根据社会学尺度观察到的贫富现象平移到我们身边,就会发现,其实贫穷早就不是一个财富数字,而是一种稀缺的心理状态。
正视自控力,提升思维带宽
幸运的是,上面的两种能力,都是可以通过训练来提升的。
对于自控力,我们可以通过如下方式训练我们的自控力肌肉:
- 训练大脑,比如冥想、深呼吸等方式
- 调整生活方式,比如充足的睡眠、健康的饮食、充足的锻炼
- 改变思维方式,比如接纳自己的情绪,而不是压抑。养成成长型思维等
甚至,我们还可以借助一些工具,比如Flora等。
对于思维带宽,我们也有不少方法,避免稀缺,提升我们的思维带宽
- 进行时间规划,避免紧急重要的任务投入太多时间。对重要不紧急的事情,每周固定留出一部分时间,避免因为时间带来的负担
- 对于支出制定预算,明确收支范围,避免因为财务紧张陷入金钱稀缺的状态
- 学会留白,拒绝不重要的活动、聚会,而是什么都不做,让自己自由思考、放松等,或者是拒绝一份996的工作
- 培养正确的认知,认识到稀缺对于我们心态的影响,当真的面临时间、金钱的压力时,提醒自己不要陷入狭隘的视角,从更广阔的角度去看问题
方法很多,就不一一列举了。
说在最后
好了,文章到这里就要结束了,感谢你能看到最后。
从看到小糖人游戏带给我的结论时,我先是感到无奈与不甘,因为这个结论,对于一个普通人来说,实在是太让人失望了。
洋洋洒洒这么多,还是想给自己泼一盆冷水,那便是我们即使不断的提升我们的心智,我们大大大概率也不能够拥有完全足够的财富。
我只能说,通过心智的不断提升,即使财富无法追上那20%的人们,也可以在思维、心智上完成跃迁。只有这样,财富或许才会到来。
因为相信,所以看见,让我们继续玩人生这个无限游戏吧。
来源:juejin.cn/post/7405889205476032552
为什么vue:deep、/deep/、>>>样式能穿透到子组件
为什么vue:deep、/deep/、>>>样式能穿透到子组件
在scoped标记的style中,只要涉及三方组件,那deep符号会经常被使用,用来修改外部组件的样式。
小试牛刀
不使用deep
要想修改三方组件样式,只能添加到scoped之外,弊端是污染了全局样式,后续可能出现样式冲突。
<style lang="less">
.container {
.el-button {
background: #777;
}
}
使用 /deep/ deprecated
.container1 {
/deep/ .el-button {
background: #000;
}
}
使用 >>> deprecated
.container2 >>> .el-button {
background: #222;
}
当在vue3使用/deep/
或者>>>
、::v-deep
,console面板会打印警告信息:
the >>> and /deep/ combinators have been deprecated. Use :deep() instead.
由于/deep/
或者>>>
在less或者scss中存在兼容问题,所以不推荐使用了。
使用:deep
.container3 {
:deep(.el-button) {
background: #444;
}
}
那么问题来了,如果我按以下的方式嵌套deep,能生效吗?
.container4 {
:deep(.el-button) {
:deep(.el-icon) {
color: #f00;
}
}
}
源码解析
/deep/或>>>会被编译为什么
编译后的代码为:
.no-deep .container1[data-v-f5dea59b] .el-button { background: #000; }
源代码片段:
if (
n.type === 'combinator' &&
(n.value === '>>>' || n.value === '/deep/')
) {
n.value = ' '
n.spaces.before = n.spaces.after = ''
warn(
`the >>> and /deep/ combinators have been deprecated. ` +
`Use :deep() instead.`,
)
return false
}
当vue编译样式时,先将样式解析为AST对象,例如deep/ .el-button
会被解析为Selector对象,/deep/ .el-button
解析后生成的Selector包含的字段:
{ type: 'combinator', value: '/deep/' }
然后将n.value由/deep/
替换为空
。所以转换出来的结果,.el-button直接变为.container
下的子样式。
:deep会被编译为什么?
编译后的代码:
.no-deep .container3[data-v-f5dea59b] .el-button { background: #444; }
源代码片段:
// .foo :v-deep(.bar) -> .foo[xxxxxxx] .bar
let last: selectorParser.Selector['nodes'][0] = n
n.nodes[0].each(ss => {
selector.insertAfter(last, ss)
last = ss
})
// insert a space combinator before if it doesn't already have one
const prev = selector.at(selector.index(n) - 1)
if (!prev || !isSpaceCombinator(prev)) {
selector.insertAfter(
n,
selectorParser.combinator({
value: ' ',
}),
)
}
selector.removeChild(n)
还是以.container4 :deep(.el-button)
为例,当解析到:deep符号式,selector快照为
parent为.container4 :deep(.el-button)
,当前selector的type正好为伪类标识pseudo
,nodes节点包含一个.el-button
。
经过递归遍历,生成的selector结构为.container4 :deep(.el-button).el-button
,
最后一行代码selector.removeChild(n)
会将:deep(.el-button)
移出,所以输出的最终样式为.container4 .el-button
。
如果样式为:deep(.el-button) { :deep(.el-icon) { color: #f00 } }
,当遍历.el-icon时找不到ancestor,所以直接将:deep(.el-icon)
作为其icon时找不到ancestor,其结果为:
.no-deep .container4[data-v-f5dea59b] .el-button :deep(.el-icon) { color: #f00; }
因此,deep是不支持嵌套的。
结尾
插个广告,麻烦各位大佬为小弟开源项目标个⭐️,点点关注:
- react-native-mapa, react native地图组件库
- mapboxgl-syncto-any,三维地图双屏联动
来源:juejin.cn/post/7397285315822632997
前端经理岗的面试技巧
本人最早是2017年,开始在上市公司作为高级经理,管理大前端团队,包括安卓、iOS和前端工程师,但这么多年过去了,我的管理工作还有很多需要改进的地方。
不管我是继续创业,还是去上班,我都需要考虑前端团队管理的事情。所以,本文我将结合过去多年的前端管理实践和面试经历,分享我对前端管理的思考、前端管理的高频面试题和一些学习资料。
关于前端经理岗的思考
在我成为程序员之前,我是北京军区38集团军某部的指挥军官,在部队最多时管理过130多人,在聚焦谈前端经理岗之前,我先放大聊一下管理。
什么是管理
管理是一个多维度的过程,既有对项目的管理,也有对人员的管理,还有向上管理、同级管理、自我管理和向下管理。
对于前端经理岗的管理,需要以人员管理为主,但也要以项目管理为本,而前端架构岗,则以项目管理为主,但需兼顾人员管理。
狭义的管理是指一定组织中的管理者,通过计划、组织、领导、协调和控制等手段,对组织所拥有的资源进行有效的整合和利用,以达成组织既定目标的过程。
技术团队管理的方法论
技术团队的管理是指运用一系列的管理原则和实践,对技术人员进行组织、指导和协调,以实现团队目标的过程。
技术团队管理的关键在于平衡技术发展、团队建设和业务需求之间的关系,确保团队的高效运作和创新能力的持续发展。
通过询问国内几个主流的大模型和查阅相关的资料,我最推荐的是一个美团同事在团队管理方面的总结的方法论。
他不仅在IGT、腾讯和新美大工作期间,参加了各种培训和大佬分享,平时还阅读了二十多本团队管理有关的书籍,并且在美团也做了多年的技术团队管理。
他采用的是自底向上方式,先是将所有知识打碎,然后重新归类汇总。先列举出了六十多种实践或方法,然后将它们划分成不同模块,并且思考这些模块之间的关系,最终建立一个相对完整且自洽的体系。
他将团队管理的整个体系分为两个维度,十个模块。有了这个体系,我们就能够以更高的视角,来看待团队管理中的各种事务,并且有针对性地加以改善。
上面的图,只是列了一下技术管理相关工作大纲,但要开展好管理工作,推荐的还是 PDCA 管理,这是美国质量管理专家休哈特博士首先提出的,由戴明采纳、宣传,也被称为“戴明环”。
它是全面质量管理所应遵循的科学程序,不仅在质量管理体系中运用,也适用于一切循序渐进的管理工作。它通过循环不断地进行计划(plan)、执行(do)、检查(check)和处理(ack) ,以达到不断完善过程和提高结果的目标。
PDCA 的管理可以总结为四阶段及八步骤:
- P阶段--计划:即根据顾客的要求和组织的方针,为提供结果建立必要的目标和过程。
- 步骤一:选择课题,分析现状,找出问题
- 步骤二:设定目标,分析产生问题的原因
- 步骤三:提出各种方案并确定最佳方案,区分主因和次因
- 步骤四:根据已知的内外部信息,设计出具体的行动方法、方案
- D阶段--执行:即按照预定的计划、标准,努力实现预期目标的过程。
- 步骤五:设计出具体的行动方法、方案,进行布局,采取有效的行动
- C阶段--检查:即确认实施方案是否达到了目标。
- 步骤六:效果检查,检查验证、评估效果
- A阶段--处理
- 步骤七:对已被证明的有成效的措施,要进行标准化,以便以后的执行和推广。
- 步骤八:问题总结,处理遗留问题,为开展新一轮的PDCA循环提供依据。
我在 PDCA 上加了扩展,变成了 O + PDCA + R:其中 O 是机会,只有发现并抓住了机会,才能开展 PDCA 的管理过程;而 R 则是Review,需要结合周报、月报、季度和年终总结定期进行总结复盘。
我的管理复盘
先谈一下我对管理认知:管理可以理解为管人理事的缩写,其中管人,包括选用育留开;而理事,则包括对公司及部门的战略理解,所负责或参与项目的规划、实施和复盘,整体的质量、效率和体验的提升。
因为我是国防生,大学期间就开始接受部队系统的管理培训,所以看了很多管理相关的书籍,我的管理理念和风格依然受部队的管理很大。
很多人会误解部队的管理是简单粗暴的命令式管理,但我党我军管理的精髓是民主集中制,既有民主,又要确保集中,注重以身作则、平等待人和思想工作。
所以,我实际工作中,不管是直线管理团队,还是通过项目管理相关同事,都会坚持民主集中制的原则,倡导用规则管理团队,并鼓励大家从团队出发,完善或提出规则,让大家都能参与到管理中。
大家若能参与到团队管理规则的制定,并且管理者能否以身作则,奖惩公平,树立起规则的权威,这样的团队才比较容易做到上下同心,营造出平等、自驱、高效和快乐的团队氛围,团队业绩也会更好。
管理团队的前期,我一般会制定三个原则:
- 每周跑一个3公里(女生只需1.5公里);
- 每周抄写10个英语词汇(每月安排一个同学提供);
- 每周写一篇技术博客或学习笔记。
做不到的需要交10元团建费(一般由女同学负责管理),在每周的技术分享会上用来买水果糕点吃。
制定第一个原则的考虑是,很多码农缺乏运动,身体亚健康的比例很高,另外对男女生的要求不同,也是让大家知道待人着想。而第二个原则呢,则是现在技术更新很快,很多新技术及遇到的问题,只有英文资料。最后呢,则是提升大家的写作基本功和让大家养成持续学习的习惯。
这三个原则执行起来后,我就会鼓励部分同学提出一些新的管理规则,然后在周会上提出,大家匿名表决,对于规则的完善建议也是如此,比如参加会议迟到也要交10元团建费或加一些奖励的规则。
回顾过去几年的管理工作,我觉得《高效能力的七个习惯》对我的启示很大,要想做好前端团队的管理,先从做好自我管理入手,然后主动做好向上管理,接着兼顾组织目标和团队个人目标做好向下管理,但也不要忘了关注协作方相关的管理工作。
一般一个月以后,团队能够按照规则良好管理起来后,我就会把重心过渡到核心项目的管理和架构上面,既会做前沿技术的调研,又会参与一些紧急项目的代码开发。
对我来说,我不愿成为一个官僚式的纯管理者,而是要主动拥抱前沿技术,具备大型复杂项目的管理和架构能力,这样在职场上才能保持持续的竞争力。
前端经理岗的高频面试题
这几年不管我是面试开发岗,还是前端经理岗或总监岗,有一些共同问题高频出现,但不同的岗位回答各有侧重,才能提升通过面试的概率。
本节我既会针对一些高频的共同问题,也会针对只有前端经理这个岗位才可能遇到的问题,分享一些我的思考。
请先做一下自我介绍?
不管是面试什么行业或什么岗位,也不管是第几轮面试,我们面试需要回答的第一个问题就是介绍自己。
很多人都会简单准备一个自我介绍,并且每次面试的介绍都一样。要想面试过程更顺利,我们最好根据面试公司的业务、岗位要求、面试官等情况,适当地调整自我介绍。
比如面试腾讯TEG 大模型的开发岗,我会这么介绍自己:我是XXX,从XX毕业已经12年了,最开始在外企以开发操作系统为主,16年开始转型做大前端开发和架构工作,技术栈以react 为主(因为JD说了项目用的是react),19年开始自学深度学习,过去三年都在做模型应用开相关的工作,特别是在美团,从零实现了一个web推理引擎。
而面试另外一个上市公司的管理岗,我则会这么介绍自己:我是XXX,从XX毕业后,一直都从事管理相关的工作,最开始在外企做操作系统时,负责团队的新人培训和团建工作,2016年进入互联网行业后,在做好项目架构和管理的基础上,还经常需要做团队管理,最多时管理过35人的大前端团队,既有前端开发、也有安卓和iOS开发,因为在部队接受过系统的指挥军官培训,我的管理遵循毛主席提出的民主集中制原则,以团队共同制定的规则管理各项工作,团队良好运转后,我会把精力聚焦在项目架构优化和研发质量和效率的提升上。
如何克服管理遇到的挑战?
技术团队管理面临的挑战多种多样,包括但不限于沟通不畅、资源分配不合理、团队成员技能不匹配、项目延期等,在面试的过程中,我们挑选一两个例子进行展开即可。
最难凝聚的,是人心:信任和凝聚力是团队成功的基石。每个团队成员都有自己的思想和需求,如何将这些不同的个体凝聚成一个有战斗力的集体,是每位管理者必须面对的挑战。管理者需要倾听员工的声音,尊重他们的意见,关心他们的成长。只有当员工感到被尊重和重视时,他们才会真心投入到工作中,与团队共同进退。
最难驾驭的,是情绪:管理者不仅是团队的领航者,更是情绪的调控者。一个稳定、积极的情绪状态能够激发团队的士气,反之,则可能引发团队的动荡。情绪的传染力极强,管理者的每一次情绪失控都可能成为团队士气的“病毒”。
最难把握的,是人性:孔子说:“己所不欲,勿施于人。” 管理者应将心比心,尊重员工的个人价值和需求。同时,通过合理的激励机制,激发员工的积极性和创造力。人性是不变的,但人心是流动的。管理者需要通过不断观察和学习,把握员工的心理变化,适时调整管理策略。
最难管理的,是期望:员工和上级都对管理者有着不同的期望,这些期望往往难以完全满足。上级领导希望团队能够不断超越,达成更高的业绩目标,而员工则希望得到更多的关注和支持。管理者需要深入了解各方的期望,并制定合理的策略来满足他们。
最难摆正的,是心态:管理者需要摆正自己的心态,从“保姆”转变为“教练”。通过引导和激励,帮助团队成员提升自我管理的能力,而不是替他们解决所有问题。应给予团队成长的空间和时间,而不是急于求成。
最难排解的,是委屈:管理者需要有宽广的胸襟,能够承受来自各方的压力和误解。在管理的过程中,管理者常常处于上下夹击的境地,既要面对上级的高要求,又要应对下属的期望和不满。应有谦逊和自省的精神,面对挑战和压力时,能够坦然接受,积极寻求解决方案。
分享你工作中最大的成果?
技术管理的最大成果不仅体现在项目交付或技术创新上,更在于其对整个组织的长远影响。可以从以下角度挑一两个展开:
- 业务项目:带领团队按时、按预算、高质量完成了一个或多个关键项目,满足了业务需求和客户期望,并通过技术驱动,促进了公司业务增长,如增加了用户数量、提升了用户活跃度或实现了收入增长。
- 技术创新:推动了技术创新,如引入新的技术栈、开发新的产品功能或改进现有的技术方案,从而提升了产品的竞争力和市场份额。
- 流程优化:改进了开发和运维流程,如引入敏捷开发、持续集成/持续部署(CI/CD)等,提高了工作效率和产品质量,在面对突发事件或危机时,能够迅速做出反应,采取有效措施,最大限度地减少了损失,并从中恢复过来。
- 人才培养:建立了积极向上的团队文化,增强了团队的凝聚力和员工的归属感,通过培训和指导,成功地培养了技术和管理方面的后备人才,为公司未来的发展打下了坚实的基础。
如何去做好这个管理岗位?
朝哪个方向走,判断的核心是深刻理解市场、业务的趋势。这其中,要对技术的未来做判断,对产品的未来做判断,相对而言,大部分人都能看到技术的发展趋势,困难的是判断未来的产品形态。
要有效地管理前端团队,需要从团队组建与选拔、技能提升与培训、项目管理与协作、质量控制与测试以及团队氛围与文化等多个方面入手。
通过明确团队目标、合理分配任务、持续培训、建立有效的沟通机制和加强团队文化建设,可以提升团队的整体效率和协作能力。
回答这个问题时,最好以互动问答的方式进行,多向面试官了解相关的情况,然后针对岗位职责和存在问题,进行适当的展开。
你为何离职?之后有何规划
回答离职原因和后续规划的问题时,重要的是要保持诚实、积极,并展现出你对新机会的热情和期待。
离职原因
- 诚实但策略性:选择离职原因时,应避免负面评价前雇主,而是强调个人成长和职业发展的需求。
- 具体原因与改进:即使是因为薪资、工作环境或领导问题离职,也应表达出你从中学到了什么,以及你如何准备在新工作中避免类似问题。
后续规划
- 职业目标:清晰地表达你的职业目标,展示你对前端技术领域的热情和承诺。
- 技能提升:强调你计划如何继续提升自己的技能,包括学习新技术和参与项目实践 。
- 对新机会的期望:表达你对新公司的期望,包括你希望在新角色中实现的目标和对公司的贡献。
通过上述策略,你不仅可以有效地回答离职原因和后续规划的问题,还能够给面试官留下一个积极、有目标的印象。
相关的学习资料
主要推荐一些我阅读过的书籍或看过的视频。
书籍:
- 《高效能人士的七个习惯》
- 《卓有成效的管理者》
- 《领导梯队》
- 《重新定义团队:谷歌如何工作》
- 《OKR工作法》
视频:
总结
人,是企业最宝贵的财富,甚至更胜于商业模式。管理能力的核心包含两方面,一是针对人,对人性的了解程度以及沟通能力。二是对事,是否有强大的统筹规划协调能力。
这是对于广义上管理能力的定义,对于技术团队,其本身有一些特殊性,所以在管理技术团队的时候,要充分考虑到这些特殊性。
管理是一件很复杂的事情,但是我认为管理技术团队相对并不复杂,可能是大多数技术人员都还是比较单纯吧。
技术团队的管理者,特别是中层管理者,其实就是个夹心层,经常受夹包气。其实你想想,作为一个承上启下的职位,压力同时来自于下面和上面,收夹包气也就正常了。
但是如果我们能够科学的规划任务,坚持以事驱动人,事前做计划,事中做追踪,事后做分析,调动团队积极性,我想再困难的任务也能够分解成一个一个不困难的小任务,分而破之。
对于团队内的一些声音和反馈的问题,能够耐心倾听、加以思考和用心解决,成员自然能够很好地完成管理者布置的任务。
关于面试相关话题,欢迎大家在评论区或加我微信交流:waxyysys82。
来源:juejin.cn/post/7396575930964934708
程序员:全栈的痛你不知道
上周一个同事直接对我开喷,骂我无能,说:“你怎么一个人就搞不定所有系统呢?”,我半支烟纵横IT江湖14余年,还是第一次被人这么嫌弃。
事情缘由
某公司的业务线特别多,有个业务线前后端项目共计上百个,半支烟带着1个大前端、1个Android外包、1个iOS外包在支撑业务线的发展。
突然,有一天大前端同事有事不在,运营同事找到我开发功能,我说要等等,我现在一个人搞不懂所有的端口。此时,运营同事一着急就上头,直接质问我,为什么你不能一个人搞定所有端口?
我当时立马怒怼,我说我一个人确实无法同时搞定IT基建、搞定后端、搞定H5、搞定Android、搞定 iOS、搞定PC、搞定小程序、搞定自动化爬虫。如果觉得我无能,你可以找个全部能搞定的过来。
然后,就是各种撕逼......
这个事情对我还是很触动,倒不是说跟同时互撕了一顿。只是觉得,现在的IT环境真的是看的人后背发凉,不但机会少,对人的要求还特别的高。
在想想前些时间,某高校降低要求大量扩招计算机专业学生,简直是坑学生啦。
全栈的优势
半支烟2010年毕业于计算机专业,工作14余年,后端干过JAVA、Python、Golang,大前端干过React、Vue、Android、iOS,还搞过IT基建运维。
半支烟对全栈还算有些理解,下面说说全栈的优势吧。
个人觉得,最好的技能人才是一专多能,这个绝对毋庸置疑。就是要在某个领域精通之后,在别的领域持续开花结果。说到底还是要做一个全栈的技术人。
全栈的优势非常多,比如:
- 在中小企业,一个人胜任多个岗位,可保饭碗无忧。
- 全栈人解决问题更快,因为全栈人的视角更加全面。
- 可以做一个独立开发者。
- 可以从事各种副业。
- 如果还会懂一些产品运营,那直接可以开个赚钱的小公司了。
全栈的痛
虽然全栈有一些优点,但是全栈的痛点也非常明显,比如:
- 全栈人要学习的技能或者知识非常多,但人的精力是有限的,无法真正做到每个技能栈都非常熟悉。
- 全栈人找工作会招人嫌弃,尤其是大厂会觉得你不是专业的螺丝钉,经常用某个领域的一些八股文去否定你。
- 很对人虽说是全栈,但是没有站在解决问题的角度去思考,而只是作为一个会多个技术栈的工具人。这样的思想其实偏离了全栈的初衷。
个人建议
个人觉得,全栈对个人职业发展很有优势,我建议在精通一个领域后做一个全栈人。
我这里说的全栈,不只是IT技术栈,还有更多的是产品运营思维。任何时候全栈人都应该用解决问题、推动事情往前发展的思维去做事。
当前大环境不乐观,未来也未必乐观,中小企业都偏向找全栈人,大公司偏向找专业高级螺丝钉。虽说背点八股文对找工作有优势,但是将来将一文不值。
因为AI发展太迅速了,获取知识已经变更更加便捷。我更不建议做一个高级螺丝钉,那样只会成为工具人,最后失业时一无所有。
我建议,不管你在哪里企业,自己的成长要放在第一位。
尤其在当下这个AI时代,可以让IT人更轻松的成为全栈人,我们应该把握机会,让自己成为一个优秀的超级个体,努力搞出点自己的事业来。
来源:juejin.cn/post/7406254193433100351
8年前端总结和感想
8年前端总结和感想
本文是我前端工作 8 年的一些总结和感想
主要记录下个人点滴、前端知识点、场景应用、未来的憧憬以及个人规划,供自己以后查漏补缺,也欢迎同道朋友交流学习。
自我介绍
我是一名工作在非知名公司的 8 年前端,双非普通本科,自动化专业(非计算机)。目前也在努力的提升自己,积极找工作状态。虽然工作已经 8 年了,但没待过超 10 人的前端团队,更没有开发过千万级、亿级流量的应用的经历,也是一种遗憾。
16 年才工作那会儿使用原生 JS 和 JQ
比较多,经常写 CSS
动画,主要做企业建站,自学了 PHP
和 ReactNative
,还学过 krpano
的使用;17 年到 19 年,主要使用 react
、 React Native
做 H5 和 跨端开发,也维护过老 NG2.x
项目;19年到22年主要使用 vue、react 做hybrid APP
及小程序,自学了electron
、node
、MongoDB
;22 年至今主要从事 B 端的开发,C端也有部分,也主要是 react
和 vue
相关技术栈;
前端应用场景
前端是直面浏览器的,也可以说是直面用户的,我们的应用场景远广泛于后端,用到的 UI 组件库、插件、基础框架、基础语言也非常繁杂,所以在面试和自我学习提升的时候需要准备的东西非常多,有种学不动的感觉。
常见的应用场景就是 PC
浏览器,需要我们掌握一些兼容不同版本和不同浏览器的知识点,一般采取渐进增强或者优雅降级去处理;当然现在很多公司已经不做IE的兼容了,复杂度降低很多;同时大部分新项目都会使用 postcss
去 autoprefixer
,给 css 加兼容的前缀。其他的就是做后台的表单为主了,用的基本上都是 antd design
或 element ui
。当然复杂点的要涉及到网页编辑器、Low Code、No Code等。
另一个主要场景就是手机浏览器
和 APP
了,H5 WebAPP
会遇到 Android
、IOS
的一些样式和行为需要兼容、列表和图片懒加载问题,还有调用原生的 SDK 进行地图、OCR、拍照、扫一扫等功能进行开发;为了更好的体验,还会选择 RN、flutter 进行跨端开发;复杂的处理场景一般涉及原生端,例如聊天室、直播等。
另一个场景就是小程序,其实还是写H5,主要是使用微信或者支付宝等相关 SDK 去实现,看官网就行了,文档也比较全。
还有一些是做 H5 小游戏,要求数学逻辑思维和算法更好点,初级一点的用 canvas+css3
去做,好一点的用游戏引擎去做,一般是 egret
、 Laya
、 createjs
、 threejs
、 cocos
。
还有一些场景是做TV端的,有的是基于PC浏览器的,有些是套壳APP,一般使用 AntV
、 echarts
做图表数据展示,3D一般用 threejs
去做。
还有一些做桌面应用的,一般来说桌面应用大多基于 C,但一些简单应用前端可以使用 electron
去进行桌面端开发,一般也用于大屏可视化,做数据展示的,当然我们熟悉的 vscode
也基于 electron 开发。
还有一些是做 AR
、 VR
、 3D全景图
的,一般使用 WebGL3D引擎
:threejs
、 babylon.js
、 playcanvas
等,还可以用 css3d-engine
、 krpano
、 pano2vr
去做。
还有一些场景是做 web3
的 DAPP
(去中心化应用程序),大部分是做区块链和数字藏品的,推荐的技术是 Solidity
、 web3.js
、 Ethers.js
。
前端网络
我们前端不管是开发 PC、 H5、小程序、 HyBrid App 还是其他应用,始终离不开是浏览器和 web-view
,与浏览器交互就要了解基础的 HTTP、网络安全、 nginx
方面的知识。
浏览器
浏览器的发展简史和市场份额竞争有空可以自行了解,首先我们要了解计算机架构:
- 底层是机器硬件结构:简单的来说就是电脑主机+各种
IO
设备;复杂的来说有用于输入的鼠标键盘,用于输出的显示器、打印等设备,用于控制计算机的控制器和 CPU(核心大脑),用于存储的硬盘、内存,用于计算的CPU和GPU; - 中层是操作系统:常见的就是
Windows
、MAC OS
、Linux
、CentOS
,手机就是安卓和 IOS(当然还有华为的鸿蒙);可以了解内存分配、进程和线程管理等方面知识。 - 上层就是我们最熟悉的应用程序:有系统自带的核心应用程序、浏览器、各个公司和开发者开发的各种应用。
前端开发必要了解的就是chrome浏览器,可以说大部分开发基于此浏览器去做的。需要了解 进程和线程
概念、了解 chrome
多进程架构(浏览器主进程
、GPU进程
、网络进程
、渲染进程
、插件进程
)。
其中最主要的是要了解主进程,包含多个线程:GUI渲染线程
、JS引擎线程(V8引擎)
、定时触发器线程
、事件线程
、异步HTTP请求线程
。其中 V8
引擎又是核心,需要了解其现有架构:
了解 JS 编译成机器可以识别的机器码的过程:简单的说就是把 JS 通过 Lexer
词法分析器分解成一系列词法 tokens
,再通过 Parser 语法分析为语法树 AST
,再通过 Bytecode Generator
把语法树转成二进制代码 Bytecode
,二进制代码再通过实时编译 JST
编译成机器能识别的汇编代码 MachineCode
去执行。
代码的执行必然会占用大量的内存,那如何自动的把不需要使用的变量回收就叫作 GC 垃圾回收
,有空可以了解其算法和回收机制。
HTTP
对于Http,我们前端首先需要了解其网络协议分层: OSI七层协议
、 TCP/IP四层协议
和 五层协议
,这有助于我们了解应用层和传输层及网络层的工作流程;同时我们也要了解应用层的核心 http1.0
、 http1.1
、 http2.0
及 https
的区别;还要了解传输层的 TCP
、 UDP
、 webSocket
。
- 在前后端交互方面必须了解
GET
和POST
的请求方式,以及浏览器返回状态200
、3xx
、4xx
、5xx
的区别;还有前后端通信传输的request header
头、响应报文体response body
,通信的session
和cookie
。 - 网络安全方面需要了解
https
,了解非对称算法rsa
和对称算法des
,登录认证的JWT(JSON Web Token)
;同时也需要了解怎么防范XSS
、CSRF
、SQL注入
、URL跳转漏洞
、点击劫持
和OS命令注入攻击
。
Nginx
我们的网页都是存储在 web 服务器上的,公司一般都会进行 nginx
的配置,可以对资源进行 gzip
压缩,redirect
重定向,解决 CROS
跨域问题,配置 history
路由拦截。技术方面,我们还要了解其安装、常用命令、反向代理、正向代理和负载均衡。
前端三剑客
前端简单的说就是在写 html
、 css
和 js
的,一般来说 js 我们会更多关注,其实 html 和 css 也大有用处。
HTML
html 的历史可以自行了解,我们需要更关注 文档声明
、各种 标签元素
、 块级元素及非块级元素
、 语义化
、 src与href的区别
、 WebStorage
和 HTML5
的新特性。复杂的页面和功能会更依赖于我们的 canvas
。
css
css 方面主要了解布局相关 盒子模型
、 position
、 伪类和伪元素
、 css选择器优先级
、 各种 水平垂直居中
方法、 清除浮动
、 CSS3新特性
、 CSS动画
、 响应式布局
相关的 rem
、 flex
、 @media
。当然也有部分公司非常重视用户的交互体验和 UI 效果,那会更依赖我们 CSS3 的使用。
JS
js 在现代开发过程中确实是最重要的,我们更关心其底层原理、使用的方法、异步的处理及 ES6
的使用。
- 在底层方面我们需要了解其
作用域及作用域链
、闭包
、this绑定
、原型和原型链
、继承和类
、属性描述符defineProperty
和事件循环Event Loop
。
可以详看我写的javascript随笔
- 在使用方面我们需要了解
值和类型
的判断、内置类型的null
、undefined
、boolean
、number
、string
、object
和symbol
,其中对象类型是个复杂类型,数组
、函数
、Date
、RegExp
等都是一个对象;数组的各种 API 是我们开发中最常用的,了解Dom操作
的API也是必要的。 ES6
方面要了解let、const声明
、块作用域
、解构赋值
、箭头函数
、class
、promise
、async await
、Set
、WeakSet
、Map
、WeakMap
、proxy
和Reflect
。
可以详看我写的(ES6+)随笔
TypeScript
在前端的使用越来越广泛,如果要搞NodeJS
基本上是标配了,而且也是大厂的标配,还是有必要学习下的。要了解TypeScript
的安装配置、基本语法、Type
、泛型<T>
、Class
、Interface
、Enum
、命名空间
和模块
。
可以详看我写的typescript随笔
前端框架
我们在开发过程中直接操作 dom 已经不多了,有的公司可能还要部分维护 JQ,但大多都在使用 React
、 Vue
、 Angular
这三个基础前端框架,很多其他跨平台框架及 UI 组件库都基于此,目前来说国内 React 和 Vue 是绝对的主流,我本人就更擅长React。
React
开发 react,也就是在写 all in js
,或者说是 JSX
,那就必须了解其底层 JSX 是如何转化成虚拟节点 VDom
的。在转换 jsx 转换成 VDom,VDom在转换成真实 Dom,react 的底层做了很多优化,其中大家熟悉的就是 Fiber
、 diff
、 生命周期
以及 事件绑定
。
那我们写 react 都是在写组件化的东西, 组件通信
的各种方式也是需要了解的;还要了解 PureComponent
、 memo
、 forwardRef
等组件类的方法;了解 createElement
、 cloneElement
、 createContext
等工具类的方法;了解 useState
、 useEffect
、 useMemo
、 useCallback
、 useRef
等hooks的使用;还有了解 高阶组件HOC
及自定义 hooks
。
了解 react16
、 react17
、 react18
做了哪些优化。
Vue
vue 方面,我们需要了解 MVVM
原理、 template
的解析、数据的 双向绑定
、vue2 和 vue3 的响应式原理
、其数据更新的 diff
算法;使用方面要了解其生命周期
、组件通信
的各种方式和 vue3
的新特性。
前端工程化
上面写到了前端框架,在使用框架开发的过程中,我们必不可少的在整个开发过程向后端看齐,工程化的思想也深入前端。代码提交时可以使用git的钩子hooks进行流水线的自动化拉取,然后使用 webpack
、 rollup
、 gulp
以及 vite
进行代码编译打包,最后使用 jenkins
、 AWS
、 阿里云效
等平台进行自动化部署,完成整个不同环境的打包部署流程。
可以详看我写的webpack随笔 和 使用rollup搭建工具库并上传npm
webpack
在代码编译打包这块儿, webpack是最重要的,也是更复杂的,所以我们有必要多了解它。
在基础配置方面,我们要了解 mode
、 entry
、 output
、 loader
和 plugin
,其中 loader 和 plugin 是比较复杂的,webpack 默认只支持 js,那意味着要使用 es6 就要用 babel-loader
,css 方面要配置 css-loader
、 style-loader
、 less-loader
、 sass-loader
等,图片文件等资源还要配置 file-loader
;
plugin
方面要配置 antd
的相关配置、清空打包目录的 clean-webpack-plugin
、多线程打包的 HappyPack
、分包的 splitChunks
等等配置。
在不同环境配置方面要基于 cross-env
配置 devServer
和 sourcemap
。
在构建优化方面要配置按需加载
、 hash
、 cache
、 noParse
、 gzip压缩
、 tree-shaking
和 splitChunks
等。
幸运的是,现在很多脚手架都自动的帮你配置了很多,并且支持你选择什么模版去配置。
环境部署
环境部署方面,第一家公司我用的软件 FileZilla
进行手动上传 FTP
服务器,虽然也没出过啥错,但不智能,纯依靠人工,如果项目多,时间匆忙,很容易部署错环境,而且还要手动备份数据。后面学了点终端命令,使用 SSH
远程上传文件,其实还没有软件上传来的直接,也容易出错。后面换了公司,也用上了 CI/CD
持续集成,其本质就是平台帮你自动的执行配置好的命令,有 git
拉取代码的命令、npm run build
的打包命令,最后 SSH 远程存到服务器的目录文件,并重启 nginx
的 web 服务器。
CI/CD
可让持续自动化和持续监控贯穿于应用的整个生命周期(从集成和测试阶段,到交付和部署)。
后端服务
为了更好的完成整个应用,了解后端技术也是必要的,我们可以从 nodejs
、MongoDB
、MySQL
等入手。如果有余力,了解 java
、c#
、c++
也可以帮助我们更好的开发安卓和 IOS 应用。前后端都通了话,不管对于我们工作、面试、接活儿或者做独立开发者都是很必要的。
node
node 这方面,我们了解常用模块
和 Event Loop
是必要的,框架可以选择 express
、 koa
、 egg
,还有我最近刚学的NestJS
也非常不错。
形而上学
了解完上面的文章,基本上你就了解了整个前端大体的开发流程、所需的知识点、环境的部署、线上网络安全。但如果需要进阶且不局限于前端和后端,我们需要了解数据结构
、 设计模式
、 算法
和 英语
。
数据结构
常见的数据结构有8种: 数组
、 栈
、 队列
、 链表
、 树
、 散列表
、 堆
和 图
。
可以详看我写的算法随笔-数据结构(栈)
可以详看我写的算法随笔-数据结构(队列)
设计模式
设计模式方面我们需要了解:
- 六大原则:
单一职责原则
、开放封闭原则
、里氏替换原则
、依赖倒置原则
、接口隔离原则
和迪米特原则(最少知道原则)
- 创建型设计模式:
单例模式
、原型模式
、工厂模式
、抽象工厂模式
和建造者模式
- 结构型设计模式:
适配器模式
、装饰器模式
、代理模式
、外观模式
、桥接模式
、组合模式
和享元模式
- 行为型设计模式:
观察者模式
、迭代器模式
、策略模式
、模板方法模式
、职责链模式
、命令模式
、备忘录模式
、状态模式
、访问者模式
、中介者模式
和解释器模式
算法
算法方面我们需要了解:
- 基础概念:
时间复杂度
和空间复杂度
- 排序方法:初级排序的
选择排序
、插入排序
和冒泡排序
,高级排序的快速排序
、归并排序
、堆排序
- 搜索:
深度优先搜索
和广度优先搜索
- 其他:
递归
、分治
、回溯
、动态规划
和贪心算法
可以详看我写的算法随笔-基础知识
英语
学生时代,觉得英语离我们挺远,进社会就用不到了。现在发现学好英语非常有用,我们可以入职福利待遇比较好的外企、可以更好的看懂文档、甚至起个文件名和变量名都好的多。最近我也在用多邻国学英语,目标是能进行简单的商务交流和国外旅游,还能在未来辅导下孩子英语作业。
前端未来
目前,初级前端确实饱和了,各个公司对前端已经不像我入职第一家公司那样简单就可以找到工作的了,尤其是在这个各种卷的环境里,我们不得不多学习更多前端方面的知识。对于初学者,我建议更多的了解计算机基础、js原理、框架的底层;对于已经工作一俩年想提升的,不妨多学点跨端、跨平台技术,还有后端的一些技术;对于工作多年想让未来路子更宽的,不得不内卷的学习更多的应用场景所需要的知识。
关于AI,我觉得并不是会代替我们的工具,反而缩小了我们和资深前端的距离。我们可以借助AI翻译国外的一些技术文档,学习更新的技术;可以帮我们进行代码纠错;还可以帮助我们实现复杂的算法和逻辑;善用 AI
,让它成为我们的利器;
感想和个人规划
前端很复杂,并不是像很多后端所说的那么简单,处理的复杂度和应对多样的客户群都是比较大的挑战。资深的前端能很快的完成任务需求开发、并保证代码质量,并搭建更好的基础架构,但就业行情的不景气让我一度很迷茫,我们大龄程序员的出路在哪里,经验就不值钱了嘛?
对于未来,我会更多的学习英语、学习后端,向独立开发者转型。
谨以此文,献给未来的自己和同道中人!
来源:juejin.cn/post/7387420922809942035
市场行情再差,也不要选择996,它真的会吞噬你
前言
最近,一连有两个朋友和我聊天,说新公司996,自己很犹豫,是不是应该换个工作。
我的回答是,快走,一秒都不要犹豫。
但在目前市场行情不佳的情况下,似乎也很难让人下定决心放弃一份已有的工作。
恰巧,我也曾经入职过一家996的公司,而且也只有这么一家。我想把这段经历分享出来,希望能对你有一点点帮助。
重复的工作,会限制人的视野。其实在世界上,还存在着无法想象的广袤天地。
满怀憧憬
2019年,我终于决定从当前的小公司离职,准备换一家更大规模的公司。
经过几轮面试,没想到很顺利的拿到一家公司的offer,涨薪50%,单纯从薪资角度来说,达到了我的预期,及时入职前,HR给我说,我们这边996,你是否能够接受,我也很果断的接了offer。
那时候想法很简单,谁给的钱多,我就去哪里。初入职场的我们,大概率都是这么想的。
由于换工作换的很快,我还没来得及搬家,前公司坐标丰台区,并且我住的地方离地铁站很远,需要坐公交车。
早上七点,起床洗漱,抓紧时间下楼去赶7:20的公交车。七点四十坐上地铁,在八点五十左右到达望京附近,一路小跑在九点前完成打卡。
前面也说了,既然是996,晚上九点才能下班,事实上九点公司根本没人走,但是我是刚入职,自然不会跟着他们一起加班。下班后打车回家,一般在十点半到家,结束一天的工作。
太可怕了,我除了睡觉,剩下的时间不是在上班,就是在上班的路上。
上班第二天,联系了自如的管家,我早下班去看了周边三个地方的房子,准备搬到这附近来,最后还交了定金,这样每天起码能省下通勤的时间。
大家可以看到,这时候我还是很想留下来的,但接下来的两天,我便意识到,此地不宜久留。
为了加班而加班
上面讲过,因为我是新人,加上住的地方离着公司很远,所以我都是九点就赶紧下班跑了。
和我同时进入公司的还有一个哥们,就叫他阿文吧。
阿文挺平易近人,对待工作也挺踏实。他告诉我他就住在离公司两三公里的地方,所以即使是新人,他下班都是和他们那些老员工一起下班。
那么老员工下班是几点呢,他说一般大家都是11点才走,然后第二天早上再晚点来公司,而他依然会九点按时上班。
中午吃饭的时候,大家普遍也是坐着不动,等到12点半才陆陆续续去楼下吃饭。
第一周的周六加班,我七点下班了。周一的时候,阿文告诉我,周六他加班到11点才走。
我是一个不会把工作往后拖的人,有工作就抓紧完成,但问题是白天的时间大家感觉也不是很忙,虽然是996,入职一周了,没有人强调过接下来项目的紧迫性,手头的工作也是一些边边角角的功能。
此时的我已经心里交瘁了,一周的早起晚归,加上比较迷茫的工作内容,已经让我有了要辞职的想法。
接下来的一周,依然是熟悉的内耗与纠结,同时也感到迷茫,如果在这里996下去,对自己的职业生涯,能有帮助吗?
最终,入职半个月后,我决定离开这家公司。
996背后是什么
开头说到,996的公司直到今天依然存在,甚至存在于一些外包公司。
大部分实行996的公司,背后一定处于基建建设差、管理能力不足,战略目标不清的状态。为了兜底,强制实行996试图弥补这两个缺陷。
作为一个开发人员,你对自己一天中高效的工作时间,心里是有一定的预估的。
以我和之前身边同事为例,仅从代码产出的角度来说,一天高效写代码4-5小时,已经相当不错了。
长期加班,强制实行996,一定会出现无效加班的现象。因为持续的加班,会触发我们基因中的“调节回路”,降低我们的效率。
调节回路具体是什么,可以看下我之前写的这篇文章。四个字解释明白,常常说的程序员35岁危机,到底说的是什么?
996可怕的不是长达12小时(可能更多)在公司的时间,可怕的是996会极大的丧失我们对于新事物的接受力,影响我们的思考能力。
关注研发效能
软件开发是一个创造性很高的过程,开发者之间的效率相差很大。就比如,10x程序员的生产效率可以达到普通开发者的10倍。其实,不仅是个人,团队间的效率相差也很大。相比工作时长而言,我们更应该关注的是研发效能。
研发效能,是团队能够持续为用户产生有效价值的效率,包括有效性
(Effectiveness)、效率(Efficiency)和可持续性(Sustainability)三方面。简单来说,就是开发者是否能够长期既快又准地产生用户价值。
软件开发流程本质上就是一套流水线,比如需求设计、技术设计、开发、构建、测试、发布、部署、验证。
其中,流水线的每一套流程都可以细化拆分,找到提高效能的方式。
举个例子,不知道你有没有过部署服务的经验,最基础的部署方式,是手动在本地打包,通过工具把打包的程序上传,替换,再重启服务。
但随着服务器数量越来越多,手动的方式会占用大量时间。
部署的方式也持续在升级,从虚拟化到容器化,再到容器编排,开发人员可以轻松的管理大规模的容器集群。
假设不关注效能,只是招人、996来完成日益增长的大规模部署,得需要多少人来完成这件事呢?
说在最后
好了,文章到这里就要结束了,感谢你能看到最后。
有些人,工作对于他们而言,只是在固定的时间出现在固定的地点,做着固定的事情——重复,重复,再重复。他们或许想的是,“就这样吧,反正也没的选”。
高效的完成工作,留出更多时间给自己。
来源:juejin.cn/post/7398150089556606988
同事一行代码,差点给我整破防了!
大家好,我是多喝热水。
最近开发公司项目的时候遇到一个哭笑不得的问题,知道真相的我差点破防!
还原现场
周一开周会的时候正常评审需求,在演示的过程中发生了一点小插曲,我们的聚合搜索功能它不能正常使用了,搜到的内容还是首次加载的数据,如下:
看到这种情况,我下意识的以为是后端返回的数据的问题,所以结束会议后我就着手排查了,如下:
结果发现后端的数据是没问题的,这我就很奇怪了,其他的 tab 都能正常展示数据,为什么就只有综合出现了问题?
开始排查
因为这个无限滚动组件是我封装的,所以我猜测会不会是这个组件出了什么问题?
但经过排查我发现,这个组件接收到的数据是没问题的。
那就很奇怪了,我传递的参数是正确的,后端返回的数据也是没问题的,凭什么你不能正常渲染?
直到我看到了这一行代码,我沉默了:
woc,你小子在代码里下毒!
看到这里我基本上可以确定就是这个 index 搞的鬼,在我尝试把它修改成 item.id 后,搜索功能就能正常使用了,如下:
问题复盘
为什么用 id 就正常了?
这里涉及到 React 底层 diff 算法的优化,有经验的小伙伴应该知道,React 源码中判断两个节点是否是同一个节点就是通过这个 key 属性来判断的,key 相同的话会直接复用旧的节点,如下:
这也就解释了为什么切换 tab 后列表中始终都是旧数据,因为我们使用了 index 作为 key,而 index 它是会重复的,新 index 和旧 index 对比,两者相等,React 就直接复用了旧的节点!
但 id 就不一样了,id 我们可以确保它就是唯一的,不会发生重复!
哎,排查问题半小时,解决问题只花 3 秒钟,我 tm.....
这个故事告诉我们:
一定不要在循环节点的时候使用 index 作为 key!
一定不要在循环节点的时候使用 index 作为 key!
一定不要在循环节点的时候使用 index 作为 key!
养成好习惯,特别是这种数据会动态变化的场景!!!
来源:juejin.cn/post/7391744516111564852
切!我又不是第一次没人要🤡
我和你一样都经历过 家里蹲 狗都嫌
的尴尬时期,每天早上起来拿着手机不断刷着招聘软件,
海投几百份还是杳无音讯
,在BOSS直拒
、前程堪忧
、失联招聘
、猎空
之间反复横跳...
还经历了十分灰暗的阶段,焦虑导致出现躯体化反应(头痛、严重失眠、吃不下东西等)
整夜整夜睡不着,躺下脑子都是工作、面试、人生选择
带来的压力
不想出门社交,害怕面试。
其实,我想跟你说:裸辞并不是终点。
1.裸辞/辞职并不是终点
当我扛着我的键盘收拾东西离开工位,第一次对辞职的 “人走茶凉”
有了实感,
下午六点跟对我很好的前辈们告了别,公司离地铁有点远,和往常不一样,天还没黑,有黄昏相伴
三号线还是这么挤 还有点闷。
算起来这是第二次辞职,但第一次辞职找了一个礼拜就顺利入职了
,这次好像有点久,今年大家都在说被裁员
、大环境差
,同学领证成家的也不少。
我意识到人与人的节奏不同
,而我好像又一次走到了岔路口
,上一次这么慌张还是在高考前
即便我从来没后悔过离职
这个决定,但还是会因为面试带来的压力感到局促不安
每次离职就像是一场查漏补缺
的大考,对勇气
,对储蓄
,对知识点
的大考
唯有拆迁
和认亲
能打破这场突如其来的考验。。。啊不是。。。我想说:
唯有行动能打破僵局!!
行动!!!去吃个冰淇淋!!。。。果然有灵感了
短暂的欢愉
后,是与台灯的昼夜相守,与简历
的交织缠绵
(简历编。。不是。。写不出来呀!!!)
反复改了几版之后确实多了一些“打招呼”的机会,但是实际面试机会还是屈指可数呀,
切!又不是第一次没人要🤡,拒绝我的多的去了,得从巴黎排到广州...
继续努力,等待运气,厚积薄发
2.当知识脱离了考试,真理和美丽才慢慢浮现
2.1 心态调整(分享一下最近对我有帮助的书)
- 《见识》 - 吴军
这是第一年出来工作,遇上了很好的领导送我的书,每当迷茫的时候再拿出来翻翻有了不一样的感悟,很多我们看上去非做不可的事情,其实想通了并没有那么重要,无论在职场上还是在生活中,提高效率都需要从拒绝伪工作
开始,有些苦是可以不用吃的,苦难并非造就人类
。
幸福是目的,成功是手段
- 《意志力》 - 罗伊·鲍迈斯特
技术行业的人都知道学习是个漫长/终身的事情,跟考公考研短期爆发式集中不同,我们更需要坚持
、长期
一点点做下去,我认识到所有人的意志力
都是有限的,使用就会消耗,压力
也并非与动力
画等号,人也跟机器一样需要“充电”
和合理分配,每个人的节奏和身体承受能力也不同。
- 《被讨厌的勇气》 - 岸见一郎、古贺史健编著
在心情动荡的时期,这本书就像开了一盏加热灯
一样在一旁无声陪伴,那会我就像婴儿
一样无意识地
紧紧抓着自己的头发,直到我睁开眼看见了、意识到了,放下禁锢着工作、生活、交友的课题的手,更能轻松地赶路了。
生活的方式千千万,人生的意义,由我自己决定
- 《法律常识全知道》 - 李桥
读书的时候没有一门跟社会
接轨的课程,毕业了也一直专研技术,导致一毕业不知道劳动合同/租房
有什么坑,把仲裁和维权
看得过于艰难,法律条例
密密麻麻 一时间不知从何下手,这本书就很适合我这种来一线城市
打工没什么社会经验
的小白,用简单的案例植入“NPC游戏”攻略,和《影响力》这本书加一起简直就是进城防骗指南
哈哈哈
免费法律援助电话:12348
2.2 前端学习路线图:
各位摸鱼的小伙伴下次见,这篇便是我的2023年终总结:
裸辞不是终点,唯有行动才能打破僵局,当知识脱离了考试,真理和美丽才慢慢浮现。
参考资料:
来源:juejin.cn/post/7312304122535133220
这两年,我把28年以来欠的亏都吃完了...
前言
很长一段时间没有总结一下过去几个月的状态了,今天思绪万千,脑海中浮现了很多经历和生活片段,我把它记录下来。顺便今天聊一聊认知突破,分享我在买房这段时间吃过的亏,也希望作为你的前车之鉴。
买房
21年
底的时候,那时刚好毕业三年,也正是互联网公司996最流行的阶段,由于平时我不怎么花钱,也很少买衣服,上网买东西是个矛盾体,需要花很多时间对比,经常看了一件东西很久,最后又不买。加上比较高强度的工作状态,两点一线,可以说是没时间花钱,再加上自己把钱都拿去理财了,也赚了几万块,最后一共攒了几十万下来。我从小就立志要走出农村,而且认为以后有女朋友结婚也要房子,加上当时花比较多时间在理财上面,那时候其实行情已经不好了,工作上没什么突破,比较迷茫,于是想着干脆就把钱花出去了,自己也就有动力去搞各种路子尝试赚钱。在没有经过任何对比之后就在佛山买了一套房子,房价正是高峰的时候,于是我成功站岗!因为这个契机,躲过了持续了2年多的低迷股市,却没躲过低迷的房地产。
while(true) { 坑++ }
我买的是期房
,当时不知道期房会有这么多坑,比如期间不确定开发商会不会破产,我这个开发商(龙光)就差点破产了,房产证无着落,相当于花了200w
买了一个无证的房子,这辈子就算是搭进去了。
对于整个购房过程也是很懵逼,对流程完全不熟悉,当时去翻了政府规划文件,看那个地段后续有没有涨价空间,然后跟着亲戚介绍的销售转圈圈,当时说给我免3年物业费
,合计也有几万块。在签合同之前销售都有说可以给到,但由于第一次没有录音,导致在签合同的时候销售反口,不承认,我们也没有证据,最后吃了哑巴亏。
开始的时候谈好了一个价格167w
,然后销售私下打电话给我洗脑说我给点辛苦费1.5w
,他可以向领导申请多几万块优惠。我知道这是他们的销售套路,但是架不住给我优惠5w
啊,中间反复拉扯最后说给他8k
,采用线下现金交易的方式。这一次我有录音了,因为私底下交易没有任何痕迹,也不合法,所以留了一手,也成为我后面维权时争取话语权的基础。
中介佣金是很乐观的,当时由于我亲戚推荐我去,销售承诺税前有4w
,当时看中这个返佣也促使我火急火燎的交了定金。现在3年
过去了,这个佣金依旧没有到账,我一度怀疑是中介搞ABC套路把我这个钱💰吃了,其他邻居的推荐佣金都到了账,加上现在地产商没钱了,同时跟那个亲戚有些过节,这个返佣更是遥遥无期。最后通过上面的录音获得了一丝话语权,知道了这个钱还在开发商手上,一直没有拨款下来到中介公司。下面是部分聊天记录:
不接受微信语音沟通,文字可以留给自己思考的时间,同时也更好收集证据。
然后去找相关人员把信息拉出来给我看,显示开发商未付款状态,这个状态维持2年
了,目前看来只能再等下去。
签合同的时候,有个律师所说是协助我们签合同、备案、办房产证等各种边缘工作,糊里糊涂交了700元
律师费,不交不行,甚至律师所连发票都没有给,而我都没有意识到这个最基本的法律法规问题。现在交房了可以办理房产证了,拿证下来也就80块
登记费,居然收我700
,其他业主有些是600多,400多
,顿时觉得智商受到了侮辱,看了网上铁头各种打假的视频,我觉得自己也应该勇敢发声。现在也在收集商家各种违规证据,提交给相关部门解决。
后面市场监督管理局收到投诉,应该是有协商,意识到没有给我们发票,过来几天之后才把发票补过来,开票日期不是付款时候的2022年
,而是2024年
,明显属于偷税了。目前跟他要发票的应该只有我,估算2300
多户业主都没有开发票的。
当时我首付需要50w
,自己手上不够,我爸干建筑一辈子,辛苦供我们两个孩子上了大学,山上建了两层楼,手里没钱。我妈是一辈子没打过工,消极派,说出来没几句好话,家里不和睦的始作俑者,更不可能有钱支持。所以我还有20w
是首付贷,也就是跟开发商借的,利率10%
,这个利息很高了。销售当时说可以优惠到5%,但是优惠金额是补贴到总房价里面去,其实这也是他们的一种销售套路,这亏我也吃了,2年之后我连本带息还24w
。当时认为自己应该一年左右能还完,但是实际远远高估自己的能力,买完房子接着我爸又生病在医院待了几个月,前后花了十几万,人生一下子跌入了谷底。
从头再来
后面2023
一年,夫妻出去创业,很多人不赞同,期间遇到了不少小人诋毁我们两夫妻,当时我老婆还在怀孕,但我们最后都熬过来了,还生了一个儿子,6斤多。期间一年赚了十几万,但是开支也大,加上父母要养,我爸还要吃药,房子要供,最后还是选择了先稳定下来,我重新回到了职场,空窗一年后在这个环境下拿了一个还不错的offer,同时也想自己沉淀一下。
自从有了宝宝之后,生活似乎都往好的方面发展,出版社找我出书,为了契合自己的职业发展,我选择了写书《NestJS全栈开发秘籍》
,从2023年11月份
开始,迄今快半年了,在收尾阶段,希望尽快与各位读者们见面。同时,等了3年的房子也收房了,由于是高层,质量相对其他邻居好,没有出现成片天花掉下来或者漏水的情况。我们经常都说他是天使宝宝
,是来报恩的。
由于我们公司技术部门是属于后勤支持性质的,技术变化不大,Vue2+微前端和React
管理系统那一套,没有太多的新技术扩展,意味着不确定也大。业务发展不好考虑的是减少这些部门的开支,所以不出意外最近也迎来了降薪。这不是最可怕的,对于我们技术人来讲,最可怕的是我认为在业务中成长停滞了,或者没有业务来锻炼技术,所以在业余时间也选择了参与一些开源项目,如hello-alog
开源算法书的代码贡献,并且这也是选择写书的原因。很简单地说,当下一个面试官问到我的时候,我不可能什么都讲不出来,最经典的问题就是:在这个公司期间你做过最有成就感的事情是什么?现在,我有了答案!
哲学
我的人生哲学是不断改变,拥抱不确定性!这么看来,我的确在这些年上了不少当,吃了不少亏,把自己搞的很累,甚至连累到家里人。但,用我老婆经常说的一句话:人生这么长,总是要经历点什么,再说现在也没有很差。的确,不断将自己处于变化之中,当不确定性降临到普罗大众时,我们唯一的优势,就是更加从容!
总结
人们还在行走,我们的故事还在继续~
来源:juejin.cn/post/7349136892333981711
火山引擎谭待:没必要将AI和云对立,大模型是“云2.0”的组成部分
8月21日,2024火山引擎AI创新巡展(上海站)如期举行。火山引擎总裁谭待在媒体采访中表示,大模型是云的构成部分,不应把AI和云对立起来。否则,AI很容易演化为项目制商业模式,很难做大做强。
谭待认为,在公共云上调用大模型API,本质仍是云端PaaS,只是相较于DevOps等PaaS的重要程度更高、Workload更大,或者也可以叫“云2.0”。
谭待表示,大模型时代,火山引擎的定位没有变,仍是云服务厂商。但在当前时代,云服务厂商要在做好降本增效,通过规模优势把成本降低的同时,做好大模型,助力企业做好创新。
以往,企业决策者很难评估云的质量优劣,只能看看PPT。但在当前,评价大模型的优劣,下载APP体验即可得到最直观的了解。
对于大模型的具体应用,许多细分场景,如文档、客服等等,企业可以慢慢应用到自身实际运用中去,而没必要“一步到位”上大系统,这样一来,无论在可感知性、可量化性,还是决策的容易性方面,都会更高,从而真正加速大模型的落地。
对于媒体提出的“云计算市场格局是否已相对清晰”的问题,谭待表达了不同看法。他认为,2012年到2013年的中国电商市场,当时大部分的观点也认为市场格局已相对固化,但随着新的业态不断涌现,如今的电商市场仍然竞争激烈。
谭待表示,随着数字化的GDP占比越来越高,云对于数字化转型和发展也越来越重要,企业未来一定会用到云,而今天中国的云市场只有几千亿的规模,还远没有见顶。他预判,未来的中国云市场发展到万亿规模,只是时间问题,并且一定是多云成为主流。
云市场的本质是规模经济,规模大意味着更强的竞争力、更好的弹性、更低的成本。火山引擎决定进入云市场,很重要的一点是因为“内外复用”,火山引擎与抖音是资源并池的,两者相加的规模,在中国排在前列,火山引擎完全有信心越做越好。
同时,火山引擎还要抓住AI这一重大技术变革机遇,目前在用户量层面,豆包APP是中国最大的AIGC应用,而对于大模型来说,只有更多的用户使用量,才能锻炼出最好的技术。
谭待表示目前全球大模型价格已处于合理价位,有利于真正促进应用生态繁荣。尽管单价较低,但随着日均tokens的不断攀升,特别是多模态模型的普及,客户的各类场景中都会出现Agent,tokens的消耗量会非常大,从而创造很大价值,即使按现在的价格来看,也是非常有吸引力的市场。(作者:李双)
收起阅读 »前端开发,不应止于前端
重新思考前端开发以及自己未来的规划。先说结论,再展开。
- 烂程序员关心的是代码,好的程序员关心的是数据结构和它们之间的关系。—— Torvalds
- 编程也是内容创作,认清自己是哪个层级的创作者。
亲身经历
公司在去年选择了一款 Plasmic 这个低码平台,希望未来由业务部的人来使用低码平台构建页面,由开发部的人负责研究平台的使用方法、解决复杂的问题。后来,公司有 100+ 个问卷需要制作,并且这个业务是在内网中,不方便使用第三方的表单服务。这 100 个问卷中,有几十个在另一个 Vue 系统中有实现过,还有一些可能需要新增。我推荐让开发部来写这部分代码,但领导还是决定让业务部门的人使用低码平台(基于 NextJS)来构建。理由是这些问卷在另一个 Vue 中有实现过(我实现的),照着页面抄就行了。
在业务部的人经过一个星期的折腾之后,做了几十个问卷,最终因为难以维护而停止。这几十个问卷每个都是单独的页面,每个页面都需要对接口,需要一个问题一个问题的拖拽然后配置。在检查的时候,出现了各种各样的问题,量表出现问题时,还得一个问题一个问题的排查,排查起来不如重做。更何况,业务部的人也不会排查页面,她们没有什么编程基础。
这个问题并不难解决,只是这个决策让人有点令人匪夷所思。我的解决步骤大致分为两个步骤:将 NextJS 中的 Form 的编辑变成声明式的、对 Vue 中的 Form 进行迁移。
将 Form 的编辑变成声明式的,也就是用 JSON 来配置。例如在 Vue 中可以使用 FormCreate、在 NextJS 中可以使用 nice-form-react。我其实并不喜欢拖拽式的低码,拖拽之后再通过点击来配置其实很费时间,在经过我的实践发现,这种声明式的配置配合 GPT 来使用非常妙,只需要把配置相关的要求告诉他,就可以让他完成大部分的工作。前两天发现有类似的文章:低代码与大语言模型的探索实践。把编写 Form 页面变成编写 JSON 文件,复杂度直线下降。业务部的人可以直接通过 ChatGPT 生成 JSON,我只需要给他们一个工具来校验和展示 JSON 所对应的量表即可。
将 Vue 中的 Form 转换到 Next 中,有些人可能会考虑思考 FormCreate 底层的逻辑,然后在 NextJS 中再实现一遍,这样就能无缝迁移。并不需要这么做,这么做工作量非常大,只需要看数据结构怎么转换即可。
总结:Vue 中的 FormCreate 经过了魔改,支持根据题目算分。在 NextJS 先实现类似的声明式构建的机制,然后再将数据库中的 JSON 配置进行转换。
最终,加上业务部的配合,使用 GPT 来生成 JSON 文件,两三天的时间就全部迁移完成。大胆预测一下,拖拽式低码平台后面会被懂 DSL 的平台所替代,利用语义化的 DSL 配合大语言模型,效率应该会非常高。
重新思考前端开发
Linux 的创始人 Torvalds 曾说过一句话:“烂程序员关心的是代码,好的程序员关心的是数据结构和它们之间的关系”。
上面的声明式的 Form 工具是我构建的,由业务部的人来使用,当他们有什么需求的时候,我能很容易知道现有的结构能否实现。将 React 想象成一个更复杂的声明式的 Form 工具,那么熟悉它底层原理的人应该也可以拓展它的边界。
于是我尝试着阅读了一些 React 相关的源码,根据《🚀 万字好文 —— 手把手教你实现史上功能最丰富的简易版 react》构建了一个 mini-react。理解了 React 本质上是 DSL + 高效更新树的算法。JSX 作为 DSL 来引入组件化和简化树形结构的编写,Fiber 树和 Diff 算法来高效的更新树。
沿着树形结构稍微发散一下,就知道为什么会有 React Native 了。在网页中使用 DOM 树,在安卓中使用 View,在 IOS 中使用 UIView,这些都可以被称为布局树。当然除了更新视图,还有很多需要兼容的问题,例如:
我还继续沿着树形结构去探索 React 的可能性,想到了 Svg、Canvas、Three.js 这几种技术。我并不是特别了解这几种技术,只是简单的 check 是否有人将 React 用于这些方向,最后发现 React Three Fiber 这个项目。Three.js 中需要渲染场景树,Fiber 树现有的逻辑为什么不能用,非常好奇作者是如何将 Fiber 应用于 Three.js、为什么需要再实现一个 Fiber。
学会用 React 写页面非常简单,从零基础开始学,最多几天就能写出基本的页面,最麻烦的可能就是调 CSS,但是这些问题的复杂度都并不高,多搜索多尝试多问 GPT,基本也花不了多长时间就能解决。
但如果我们遇到的需求是制作 Su7 在线预览、制作 Win98 在线版、制作 Figma(通过 Webasmbly 与浏览器 Canvas 交互,让用户在浏览器端体验到了 Native 软件能力),那我们应该怎么办?薪资水平上不去的原因有部分原因就是这个了吧。我想到的解决方案是:重视源码阅读、重视数据结构和算法、甚至重视底层的数学。虽然上大学的时候就一直听说这句话,但是一直感悟不深。
在我重视了源码阅读之后,开始尝试去阅读 React 源码,然后顺着树形数据结构更新这个核心概念,延展出了 React Three Fiber。假设将时间线往前移动几年,并且我的设计和开发能力足够,可能我就成为了 React Three Fiber 的 Creator。也就是说,我顺着这样的思路再往后学习,可能也会发现一些新的场景,然后做出一些还不错的工具。
在 React 源码中有很多对于树这种数据结构的操作,让我重拾了对数据结构和算法的乐趣。在了解到还有 React Three Fiber 之后,我意识到常见的 Web 页面只是 2D 上交互,未来可能还有很多 3D 交互(比如在 Vision Pro 上),我想自学一下计算机图形学(在这之前可能得补一下线性代数、物理的光学)。好像挺多人推荐 GAMES101: 现代计算机图形学入门 这门课的。
内容创作者
作为一个前端开发者,有时候会逛到一些特别好看官网,就会特别羡慕这些开发者那么强。如:ONEUPSTUDIO、GSAP。后面我发现,网站好看与否很大程度上取决于设计师,软件好不好用取决于产品,功能能否实现取决于程序员。
开发者的职责是用代码将他们的想法表达出来,或者制作更好的工具帮助他们去创作和表达。工具之间的区别在于表现力、使用场景和使用范围大小,并且这些工具通常都是声明式的。设计师能使用 Webflow 这样的软件开开发出好看的网页,但是前端开发不一定能做到。作为程序员,最好的做法是制作更好的工具,如果直接表达想法,就会写出维护性和拓展性很差的代码。就像我上面提到的 Form,我应该提供一些制作 Form 的工具,而不是一个页面一个页面的绘制。
我们常见的 CSS 就是表现力非常强大的工具集,里面包含了 Table、Flex、Grid 等等工具。在没有 Flex 的时候,我们可以用 table-cell 这种这种的办法来实现垂直居中,但是在很多场景下使用起来比较麻烦,这就是工具的表现力差。后来引入 Flex,可以轻松的实现垂直居中。
程序员也是一种内容创作者,我大致的划分了一下互联网上的内容创作者,大致有如下的观点(不一定准确):
- 内容创作者可以大致分为:数学/物理、框架/库、软件/游戏/网页、视频/图片/文字
- 内容创作者如果从上到下排布,创作人数会呈现金字塔形状,从上到下创作难度从高到低、内容的数量会由少到多。
- 上层创作者为下层创作者提供基础设施(声明式的工具),并且可以简化下层创作的难度,但有可能会让下层创作者失去工作。
我在写 mini-react 的时候,相当于自己处于「框架/库」的内容创作者,我尝试直接在 codepen 中找一些有意思的作品,然后尽量去提供相关的机制(渲染、Diff、Hook)。作为「框架/库」的内容创作的的时候,可以很容易分析出整个软件的边界,我能知道 mini-react 能否做到某些功能。具体的页面绘制可以交给另一个层级的内容创作者,也可以尝试去复用他们的作品。
不同人有不同的喜好和擅长点。我很喜欢好看的软件,但我在绘画和艺术方面没有非常高的热情 Passion,往「数学/物理」这个方向去拓展应该会更适合我。有些人可能喜欢画画,会往「视频/图片/文字」这个方向,最终成为设计师。
过早优化是万恶之源
“过早优化是万恶之源”来源于 Donald Knuth 的《计算机编程艺术》一书,其原话是:
The real problem is that programmers have spent far too much time worrying about efficiency in the wrong places and at the wrong times; premature optimization is the root of all evil (or at least most of it) in programming.
真正的问题在于程序员在错误的地方和错误的时间花费了太多时间去关注效率;过早优化是万恶之源(或至少是大多数问题的根源)。
在学习 mini-react 的时候,我想到了一些问题:
- React 中还有很多种 API 和配置,那我是不是要都实现一遍?
- React 有很多优化手段,我是否都要看看?
答案都是“不”。通过 React 能实现的网页,用 JQuery 也能实现。这些优化手段和 API 是通常都针对非常具体的场景,其通用性是比较低的。在掌握了整个 React 的核心原理之后,遇到这些场景再去学习会更好。它们大多也是基于核心原理去拓展的,理解起来并不会非常耗时间。比如 React Hooks 和 Fiber 树紧密关联。
半年计划
用以终为始的眼光来看,我十年之后还会在现在这家公司吗?答案是绝对不会。不在沉默中死亡,就在沉默中爆发。继续在公司苟着也只是将问题隐藏起来,所以还是决定了离职。
现在找工作难度越来越大,前端更是重灾区,一个岗位可能几十个人投。在这种环境下,调整好自己的心态比较关键。苦思冥想后,想到了一句可以让自己保持 Passion 的话:“找不到工作这段时间就是提升能力,找到工作就是幸运的”。
最近看到一篇博文非常喜欢,这里转载一下《前端开发的瓶颈与未来之路》 中的一段文字:
我在 2018 年有幸参加了 TypeScirpt 的推广大会,TypeScript 的作者 Anders Hejlsberg 亲自主讲。一位将近 60 岁的程序员在讲台上滔滔不绝的讲技术方案,TS 的设计理念。你真的很难想像这样一位处于「知天命」阶段的老头子(实际上很年轻)讲的东西。
QA 环节有个年轻小伙问到 Anders「在中国做程序员很累、很难应该怎么坚持下去(类似这样的描述,细节记不清楚了)」的问题。
Anders 几乎毫不犹豫的说出了「Passion」这个单词。我瞬间就被打动了。因为在此之前我对于「激情」这个词的认识还停留在成功人士的演讲说辞层面,当 Anders 亲口说出 Passion 一词的时候,让人感觉真的是一字千金。
这里还有一些我非常喜欢的文章,给大家推荐一下:
来源:juejin.cn/post/7399273700117479474
一个普通人的27岁
致工作三年即将27岁的自己
这是一篇自己的碎碎念、即回顾自己以前的成长经历、也小小的持有一下对未来的期待。
我是一个双非本科从事于Java开发的一名普普通通的码农、不同于大多数人的27岁、大部分人在这个年龄都已经工作了4/5年、而我也恰恰刚刚满三年而已。
读书
小时候的记忆很模糊、很少关于有父母的记忆、从小的印象就是他们在很远的地方打工、那边还有一个从未谋面的哥哥、小时候的记忆更多是和爷爷奶奶在一起,爷爷在我记事起、他就很忙、很少在家里也或许是我不记事或者缺少了这部分的记忆。
在小时候的记忆里、住在茅草屋里面、那个时候家里还没有完全通电、印象里经常点煤油灯、这个时间段应该是02/03年的时候、记忆里这个时候家里养了一头牛、是一头老黄牛。家里在需要耕田播种的时候、不管风吹日晒、都能看见爷爷在田里一边驾驭着黄牛、嘴边一直在说什么、应该是教导牛牛该怎么走以便使的犁田犁的更好。记不清了、只知道每次遇到下雨的时候、爷爷披着蓑衣带着一个草帽、颇有一些武林大侠的气息。
那个时候家里有一条很凶很凶的狗、幺爷爷家里还是一条白猫、年龄比我那个时候都大。这条很凶的狗已经不记得长啥样了、甚至什么时候去世的都没有印象。
关于这条狗不多的记忆就是、它很凶、但凡看见我们在地坝(四川话:门前小院的意思、通常用于晒一些农作物的地方、或者夜晚乘凉的地方)里面打闹。它都会狂吠不止、这是对它的一个记忆。
还有一个记忆就是,记得是在某一个夏天、在屋后发现了一只野兔、这个时候不记得是不是爸妈在家了、全家都在追这个野兔、追了好久、这条狗也在追、有一个画面就是在我小时候的眼里那个农田的岸边很高、这个直接就从岸边往下面的田里跳下去、连续跳了好几个这样的梯田、那个姿势在我眼里好帅好帅、现在都记得很清楚。最后这个野兔是被抓住了、炸了酥肉、那个味道真的很香、现在都记忆深刻。毕竟小时候家里都是吃猪油、用很小很小的一块、煎出油炒菜。
幺爷爷在的猫是条白猫,印象里是一条抓老鼠的好手、但是不知道它什么死的、只记得大概有十三岁左右。
奶奶有风湿心脏病、那个时候总会吃一些药和一些偏方、记忆里有这么一幕、爷爷把刚出生的小狗狗杀掉、给奶奶治病、嘴馋的我也要吃、结果吃了一口就闷住了。
奶奶在我的记忆里有个画面就是我不想去读书、在上学的路上跑到了一个斜坡上、就如同那个时候黑白电视机上播放的游击战争片一样、以为自己躲在这里他们指定找不到、当然了最后肯定少不了一顿打。印象里只被奶奶打了这一次。
奶奶是在06年走的、不到六十岁。记忆特别深,当时哥哥从东北回老家也快一年了、那是在一个夜晚、哥哥先去睡了、我和其他堂哥堂姐在家里看电视、电视里播放的是洪金宝主演的、是一部战争片、大概就是他们去越南救什么人、有一个人在飞机上跳伞的时候说要倒数十个数、然后打开伞、结果这个嘴吃的人没数到就摔死了。里面有个画面用草杀人、后面还依葫芦画瓢学过这个东西。
奶奶走的时候、爷爷是第一个发现的、我记得爷爷发现之后、我去把哥哥喊醒了、然后我就一直在哭。虽然当时不知道死亡意味着什么、就是在哭、那个时候我上三年级了。奶奶走的那天的天气很好、我还记得我捡了一个螺母回家、后来我把这个螺母扔掉了、当时就想如果不捡这个螺母就好了、奶奶就不会走了。
第一次见哥哥的时候是在一个夏天、爸妈把他从东北送了回来、打算让他家里面读书、当然读书的地方现在已经垮掉了。那个时候家里的公路还是泥巴路、泥巴路大概还是前一年挖机采挖的、挖坏了几个秧田。他们在回来的前一年、写了一封信寄回来、内容是什么记不住了、只记得有一封信、分别向爷爷奶奶以及我都写了点东西。初次见面的时候很陌生、眼前这个和我差不多高有点黑的就是我哥、我的关注并没有在他的身上、更多的是他们提的袋子里面、因为有一袋子糖。
当然了小时候的记忆还有几个画面、就埋藏在心里吧、为什么说上面的狗狗很凶、因为他在我堂姐的脸上留下了印子、现在都能看见。
奶奶走掉之后、我和我哥就去了东北、因为家里没人会做饭了。就去东北读书、东北的记忆说不上多好、校园霸凌确实存在、我也是被霸凌的那一员、我不像我哥哥那样、他们总是有勇气、那个时候的我没有。
在东北这三年、父母总是在吵架打架、住在平房里面、附近都是和父母一样的体力劳动者、他们一闹附近的人都会知道。我们的右边住了一个也是一个外出务工者、他们的有个女儿、比我和我哥都大、长得很白。在我们的左边也是一户外地务工者、不过是东三省的、不是四川的、这家的女主人好像很贤惠很好看、长得也很白。
在这期间、四川发生了很大的一件事、汶川地震、当时我记得我和附近的小孩偷偷跑去上网、结果附近的网吧都没网、然后回家就看到电视上到处都在播放新闻、去上学的时候、学校组织了捐款、我捐了五块钱。
小学结束之后、过完了六年级的暑假我就被送回到了老家、走的时候是和爸爸在工地上的工友一路的、正好他要回家。他和我们是一个地方的,记得大概是午饭后、叫了一辆出租车、我就和这个工友上了车、爸爸的这个工友坐在了副驾、我坐在了后排、送行的人有几个、车窗升起、行驶了一段路后、眼泪就落下了、大概知道了以后又不会在爸妈身边了、也不知道为什么没有哭出声、就和电视里面一样。这就是小学的记忆。
大概走了三四天、回到家了、就开始上初中了。
报名的时候见到了很多小学同学、他们很容易就认出我来了、然而我并没有很快的认出他们、他们说我五官没什么变化、很好认。
初中是在一所民办初中读的、我们这边的公立学校很水、很乱、上课打牌抽烟都存在、老师也不会管。而且离我们也很远。民办学校离我家很近、这里的校长和附近的家长都很熟悉、自然而然的就去读了、自然而然的也会听到这样的交代、娃儿不听话不好好读书就整哦。初中的算是目前为止的小高光、因为那个时候自己还算聪明、成绩也还算可以、被当着升学的苗子重点关注。当然最后也还算争气、以A+1
的成绩考进县一中、我们这一届也还算争气、有一个去了同济大学、算是历史最好的一届了,当然这个学校现在也垮了。
高中的时候流传出了一个梗、你的数学不会是体育老师教的吧、那个时候会自嘲、我初中的时候、不止是数学是体育老师教的、历史和物理也是体育老师教的、这个老师还没上过高中。
高中是lol
很火的时候、那个时候脱离了棍棒教育的我、理所当然的沉迷了进去、高一上学期还好、棍棒教育的习惯还在、期末考试全年级2000多人我考了200名左右、班上第五名好像。
学习态度的变化不止是因为lol
、还记得当时班上有个人说我很努力、所以成绩这样、当时不知道是脑子抽了还是咋了就认为别人在说我笨、然后就慢慢放弃了之前的学习方式、再加上联盟的影响、自然而然的成绩一落千丈、后来也就去复读了。
复读这一年没什么特别的记忆、涨了几十分、去了一所双非学校。还是没有做到高一班主任说的那样、你好好读上个一本不成问题。当时学校的升学率是前60
名可以上川大的程度、200
名左右上一个一本好像确实不是什么问题。但也确实没做到。
上了大学就和大部分人一样、加部门、当班干部、实际上就是混吃等死。不同的是大二那年、由于初中埋下的病因、做了双侧股骨头置换手术、这一下就把家里面掏空了、手术是在北京做的、花了20+、是在18年、三月一号做的左腿三月14号下午14:17做的右腿、刚检测出来的时候很崩溃、出了诊室就哭了、因为知道这么大笔钱家里出不起、当时借住在北京的姐姐家,在十五楼窗口处、恐惧战胜了勇气、没有跳下去。
查出来的时候就告知了父母、父母当时在深圳上班、我一个人去的北京找的姐姐、父亲先赶过来、看见父亲憔悴的面庞、自己也彻底取消了跳下去的想法、太憔悴了、没见过这个样子的父亲、也无法去想象如果跳了父亲会咋样、只知道那个时候父亲的头发白了很多、然后开始秃头了。
做手术的那几天恰逢过年期间、医院的人很多、见识了人生百态、有的人痛苦呻吟着想活下去,有的人沉默不语想离开人世,坐在轮椅上的时候、被推出去透透风、看见了一个和我一般大的人、少了一条腿,那个时候心里想着都是苦命人。不同于大一暑假工被晒的黢黑的我,在学校看到一个老外、老外的黑衬托出我的白,那个时候由于被晒的黢黑心情很糟糕,见到这个交换生之后得到了极大的安慰。
因为这个手术需要人照顾、学校是上下铺、因此休学一年、手术很顺利、在我们眼里是一个天大的事情、在医生眼里如果一个小手术一般、就和普通的感冒差不多。术后也会恢复的很好、有一段时间是长短腿、走路一瘸一拐的、过了两个多月吧就彻底正常了。到目前为止至少没什么问题。唱跳rap不打篮球。
后面的大学时光就很平平无奇、本以为就和之前的师兄师姐一样正常大学然后毕业、后面就遇到了口罩事件、在学校都没有好好学习、在家里怎么可能会好好学习、真的是在混吃等死、大学期间没有什么特别的记忆、唯一的印象就是大一老校区是一群室友、大二搬到新校区、又换了一批室友、寝室从原来和其他专业的混寝、变成了同专业的混寝、但是由于休学一年、复学的时候又被安排到新的寝室、又换了一批室友、读了一年这一批室友毕业了、我大四的时候又换了一批室友。也就是一年一批室友。也算是独一份了。不过后面的都没怎么联系了。
这就是整个读书生涯了。还有很多画面就埋藏在心底吧。
工作
毕业之后、第一年认识了一个老师、养鱼达人、第一次约她出来玩、就问我用什么去接他、给了刚毕业的我一个暴击。于是呼在工作上加把力,从刚毕业的几千块不到一年的时间就破万了。也就是在22年左右吧。这个时候总觉得自己谈恋爱应该有点底气了。可在24年又给了我一个暴击。也就是今年。
在整个22年里面、由于工作还行、有大量的自由时间、在b上学习了尚硅谷的mysql和jvm课程、在慕课网上学习Java高并发课程、还算充实、虽然工作上用到的不多。
在22年、养了一只猫取名壹贰、是只三花、很粘人,也很喜欢、但我把它放在老家了。我的头像就是它很可爱吧。
在23年里、由于之前的学习累积、总觉得要记录一下、避免用的时候又到处找、就开始了写博客这个过程、博客更新的速度很稳定、生活节奏也很稳定、每天下班之后、买菜回家做晚饭和第二天中午的午餐、厨艺和刀工得到了大涨,每天晚上还能学习两小时、从周末开始选题、工作日开始编码、验证写博客、一切都有条不紊的进行着,生活节奏很稳定、窗外的阳光也很温暖。
23年发生了一件事、就是爷爷走了、遗憾的是没有带个孙媳妇回去让他看一眼、爷爷是五月份走的、守灵的那个晚上、睡在爷爷旁边、没有丝毫的害怕、下葬的那一天、没有哭但全是遗憾。至此带我长大的两个人都离开了人世。
在23年11月份的时候、认识了一个菇凉、她的名字很好听、长得也很好看、她的生活多姿多彩、现在都觉得她活得很多姿多彩。就和大家想的那样、慢慢的喜欢上了这个人、好巧不巧的是她对我也有点点意思吧、然后就约着出来玩、一起看电影、一起跨年等等、初期总是美好的、回忆也是。她不会做饭、总是吃外卖、我会让她点菜然后我做好了带给她吃、无论什么时候会送她回家然后自己再回家、每次见面都会给她准备一点零食或者小惊喜、理所当然的我们在一起了、直到过完年之后的某一个周末、我朋友约我们出去玩、在晚上回来的时候、我朋友买了房子(和女朋友一起买的)、刚好又说到这个问题、我就说了一句以后我们也一起买、用公积金带款、然后就因为这个问题讨论了一周、直到最后的分手。
具体的细节问题就不说了。我工作三年攒了一些钱、家里修房子我出了一点钱。一时间我家肯定是拿不出来的、我想让他给我点时间、结果不愿意、她之前有过很长一段时间的恋情被分手、大概是害怕再浪费时间、也能理解。
刚分手那段时间、感觉像是丢了半条命。心态很崩溃、觉得自己很差劲、一眼望到了头、好像也成不了家。掘金的更新速度就能看出来影响,虽然在一起的时间不长、三月份分的手、到现在为止有些时候都会因为这件事emo。
分手之前很喜欢做饭、分手之后再也没做过饭、看着那些为了给她做菜买的调味品以及打包盒、总是别有一番滋味、有时候总觉得自己要是当时做的再好一点就好了。在这期间看了一些心理学相关的书、也学会了一些调整自己的方法。
分手这段时间里、激情消费买了辆车、自驾去了一趟若尔盖大草原、草原很好看、自此身上的积蓄被自己花得差不多了。不止如此、由于工作上没有任何发展、总是干一些和Java无关的事情、甚至打算让我做嵌入式开发和大模型这一类工作,职业发展也看到了头。
整理生活这段时间丢了很多东西、总感觉自己也把自己丢了、好在慢慢的把自己拼好重新捡起来了。
下一个月也就马上27岁了、看着身边同龄的人要么成家、要么即将成家、要么事业有成、自己还是孤家寡人,多多少少也很羡慕。
站在生活这条十字路口、迷茫、彷徨、不安、焦虑每隔一段时间都会出现在自己身边、好在自己的调整能力得到了极大的提升、看书总归是有用的。
古人云:三十而立、至少现在看来、在这有限的时间里很难立起来了、但总要去试试、说不定就成了呢。
未来会是什么样子的呢?不知道,能把自己的生活过好就已经很不错了。感知幸福是一种能力、感知焦虑也是。
对生活的感悟如同总有千言万语、却有一种如鲠在喉的感觉。不知道命运会给我带来什么样的生活?不管怎么样都坦然接受吧。期待吗?期待吧。
写到这里、感受万千、内心细腻的人总是容易伤春悲秋。
回顾过往、就如同这篇文字一样、普普通通平平无奇、都无法用鸡肋来形容。但相信生活不会辜负每一个好好生活的人、始终对未来抱有期待与憧憬。不管最终如何、终将相信我们都会过上自己想要的生活。
最后给自己定一个目标吧:
- 坚持写博客、写到35岁,我相信自己会一直从事计算机行业的!
- 健健康康的活到退休。
窗外的天空很蓝、阳光很温暖、最近的心情也很好、希望您也是。
谢谢您能看到这里,祝君心想事成、万事顺遂。
来源:juejin.cn/post/7396609176744886310
fabric.js 实现服装/商品定制预览效果
大家好,我是秦少卫,vue-fabric-editor 开源图片编辑项目的作者,很多开发者有问过我如何使用 fabric.js 实现商品定制的预览效果,今天跟大家分享一下实现思路。
预览图:
简单介绍大部分开发这类产品的开发者,都会提到一个关键词叫做 POD ,按需定制,会通过设计工具简单的对产品进行颜色、图片的修改后,直接下单,获得自己独一无二的商品。
POD是什么?
按需定制(Print On Demand,简称POD),是一种订单履约方式,卖家提前设计好商品模板上架到销售平台,出单后,同步订到给供应商进行生产发货。
使用 fabric.js 实现商品定制预览,有 4 种实现方式
方式一:镂空 PNG 素材
这种方式最简单方便,只需要准备镂空的png素材,将图层放置在顶部不可操作即可,定制的图案在图层底部,进行拖拽修改即可,优点是简单方便,缺点是只能针对一个部位操作。
方式二:png阴影 + 色块 + 图案叠加
如果要进一步实现多个部位的定制设计,不同部位使用不同的定制图,第一种方案就无法满足了,那么可以采用透明阴影 + 色块叠加图案的方式来实现多个位置的定制。
例如这样的商品,上下需要 2 张不同的定制图案。
我们需要准备透明的阴影素材在最上方,下方添加色块区域并叠加图案:
最底部放上原始的图片即可。
方式三:SVG + 图案/颜色填充
fabric.js 支持导入 svg图片,如果是SVG形式的设计文件,只需要导入到编辑器中,对不同区域修改颜色或者叠加图案就可以。
方式四:平面图 + 3D 贴图
最后一种是平面图设计后,将平面图贴图到 3D 模型,为了效果更逼真,需要增加光源、法线等贴图,从实现上并不会太复杂,只是运营成本比较高,每一个 SKU 都需要做一个 3D模型。
参考 Demo:
结束
以上就是fabric.js 实现服装/商品定制预览效果的 4 种思路,如果你正在开发类似产品,也可以使用开源项目快速构建你的在线商品定制工具。
来源:juejin.cn/post/7403245452215386150
前端转产品一年总结!
截止至本月,我从前端研发转岗为产品经理已经一年左右了,这篇文章就讲讲我这一年中的一些经验和总结。希望能为同样身为前端也有想法转产品的你提供一点帮助。
为什么转产品
工作原因
我是22年毕业的,在刚刚毕业的那段时间,我还是对前端开发很有热情的,经常把工作中的一些内容给总结成文章在掘金发布,而且开发时我会比其他人更加注重整体的代码质量和实现的完整度,毕竟也要写文章嘛,输出倒逼输入。但是随着工作年限的增加(其实也没多少),我渐渐的感觉公司的业务没有什么挑战性了,而且最开始同事们还比较注重代码质量,但随着业务越来越复杂,排期越来越紧张,代码质量慢慢的就开始妥协了。
而且有一些功能实现方式比我想象的复杂很多,但是他们依然执着的要引用一些我不认可的框架或者库,代码这件事大家应该清楚,写的爽是很重要的,如果一个项目的实现都是我不喜欢的实现方式,为了一些虚无缥缈的性能优化把代码写的很复杂。
不知道大家有没有用过 SWR 这个请求库,我曾经写过一篇文章介绍这个工具 《都什么时代还在发传统请求?来看看 SWR 如何用 React Hook 实现优雅请求》,这是我很喜欢的一个请求库。它可以帮助我们在组件中获取请求数据时,不用在父组件获取后一个个通过 props 传递,而是直接在子组件中获取请求数据,并且不用重复请求。
但我有个同事认为直接使用 SWR 性能不好,因为每个组件都调用 hook 去请求,虽然 SWR key 是一样的,那肯定有个判重机制,这个判重机制对性能有影响。我当时就很无语,我说判重机制不就是用避免重复请求这件事情的嘛。更无语的是,他的解决方法时在父组件中使用 react context 创建上下文,通过 SWR 获取数据,将 SWR 中的数据传入 Context,然后在子组件中消费 Context。我当时就觉得这种写法就是恶心,脱裤子放屁行为,这么写何必用 SWR,直接用 Axios 请求后传子组件得了,为什么不直接在子组件里面调用 SWR 拿数据?
后面我与他掰扯,说我们按照 SWR 官方文档的方式来用就好了,无需多此一举,他的观点是 “SWR 又不是 React 官方的。他们的用法也不见得合适”,其他前端同事也没有很大异议,后续就是那段代码依然合并进去了,而且后续他都是这么写的。除此还有很多在 js 代码中使用单字母简写的变量,问就是跟xxx语言学的,我个人是觉得前端项目的变量名还是写全称,尽可能完整,让代码可读性更强,但我也懒得去争了,一旦大家的代码规范不同,那对我来说写代码就是很难受的一件事情了。关于这件事情我希望能在评论区看到大家的看法。
职业规划
其次是职业规划,这个倒不是说我最初就决定了想走产品这条路,而是我不太想一直走前端的路子了。我最初之所以选择前端开发的岗位,是由于我的大学专业是计算机相关,并且在我毕业前有过一段创业的经历,在那段经历中我担任的就是产品经理+前端开发的角色,因此找工作时也自然而然的投递前端相关的岗位。
但慢慢的,我在工作中感受不到前端技术很大的热情,脑袋里的灵感越来越少,工作上的代码写起来越来越乏味,而且社区里还是各种面试拷打,八股文,算法题最吃香。我是真的很讨厌八股文,我知道基础对于前端来说很重要,但还是讨厌,想到一旦要跳槽就要重拾八股文,我就觉得真的很无聊,所以我心理也埋下了要转产品的种子。
兴趣驱动
前面有讲到我在大学期间有创业过一阵子,当时我们一边做外包项目,一边做自己的一些校园产品,不论是网页还是移动端的产品,各个主流组件库都门清儿,而且也画过原型图,对一个产品从零到一的流程比较清晰,自己也对设计一个优秀的产品有执念,虽然至今还不算完成,但毕竟如果一件事情能让你有成就感,那么它就会推着你向前走。
怎么转的产品
在去年八月份,我们公司的产品要做一个大版本的迭代,很多功能都需要重新设计实现。在一个新产品诞生的初期,研发是依赖非常产品经理的产品文档,而产品经理则需要进行产品调研,概念设计等等流程才能写出一份产品方案,这就导致了产品有很大的产品方案设计的工作量,而研发只能先做一些基础框架搭建,写写技术方案,一旦这些简单工作做完就必须停下来等产品出文档,十几个研发两三天没活儿干,对于一个企业来说肯定是无法接受的。因此我们的领导就开始招聘产品经理,与此同时也在内部会议时主动问到有没有愿意尝试一下产品的工作。我本身就与公司里的产品比较熟悉,又有这么一个机会,于是我就毛遂自荐,并且当时在会上主动提出参与的也就我一个,于是就顺理成章的开始了研发向产品的转变。
遇到的问题
相信点进这篇文章的同学,有很多都是有过转岗为产品的想法的,那么我就说说从我自身出发,在从研发转为产品之后遇到了哪些问题是我没有意料到的。
专业知识
当研发时,每一次产品需求评审会上,我会去看这次新增了什么概念,思考这个需求如何实现,产品画的原型图交互流程有没有可以优化的地方,似乎自己对需求的理解能力还是很强的,也能够理解业务。
例如一个微信朋友圈的功能,产品经理提出放在发现页面,然后单击右上角加号可以选择照片发布,长按加号可以直接发布文字,那么我就会想长按这个操作是不是不够直观呢,会不会有用户很长时间了都没有发现这里是可以长按直接发布文字的?于是我就提出问题与产品探讨,长此以往,我会觉得我们的产品有时候考虑问题也不是特别全面嘛,给了我一种我上也行的错觉。
到了实际转变为产品经理后,你就会发现你要做的不只是思考朋友圈的发布是从个入口进去,而是老板给了你一个需求,是用户如何在微信中分享自己的动态。那这时候你就要想:
- “我是不是加一个类似 QQ 空间的功能?这个功能就叫动态?还是空间?还是叫微博?还是叫圈子?”
- “这个功能要放在我的页面还是聊天页面,还是说新增一个底部栏?”
- “这个功能和微博或 QQ 空间有什么本质上的不同?”
或者评审时有同事或领导问你“为什么要叫朋友圈?我觉得叫朋友圈我完全 get 不到是什么意思?”,你应该如何应对?
我在刚刚转产品的初期几乎每次评审都是心惊胆颤,因为我很难接住同事问的问题,而且我也不是一个性格刚猛的人,可以接受讨论但不太喜欢很激烈的争论。这个问题出在两个方面:
一是刚刚转产品,我对于过往的一些概念只是了解但没有深入了解。例如领导问道:“xxx功能为什么要这么设计?”,这个功能并不是一个新功能,而是一个存在已久的功能,但是设计上有些许不合理,但是你并不清楚这个功能具体的上下文,那么人家一问,我也只能支支吾吾。这样的经历在有过几次后就会给人一种你不够专业的感觉,你连系统里的功能设计都不清楚,你还配当产品经理?
二是对自己的职责定位及权限边界还不够清晰,以上面朋友圈的例子来说,我觉得我拍板不了朋友圈具体的名称,我得去找更有经验的同事一起讨论,虽然同事会给你提供意见参考,最终的决定还是得你来做。在方案评审时,发现领导对朋友圈这个概念设计有很多疑问,例如问你:“为什么不参考竞品?”,“这个入口放在发现页也太隐蔽了吧,我作为用户完全不会点进去?”
诸如此类的问题,刚开始我是完全接不住的,总是想着领导说的也有道理,那我就按照他的想法改吧。这样一来,领导提出的意见一旦比较尖锐,你就只能顺从,说明你没有自己的想法,没有自己的主见,即使有想法也不能坚持。尤其是你还没有足够的经验,更容易被人质疑。此时如果没有一颗大心脏,没有一个非常全面的调研和思考,就容易变得很不自信,从而丧失对产品这个岗位的工作热情。
刚刚转产品的那个月我非常煎熬,每次做方案和评审前都特别紧张,有时候睡觉前都在想要是每天同事问了这个问题要怎么回答?
与研发的对接的边界
在我还是前端的时候,我单纯的认为很多边界情况就是需要前端去考虑的,例如页面的加载状态是骨架屏还是加载动画,表单字段在输入时是失焦时触发表单验证还是点击提交按钮时触发表单验证,返回上一个页面是否需要保留页面状态,文本溢出时的省略效果如何。
但当我自己转为产品后,似乎这些情况还是得我来考虑,每天需要面对各种研发提出的各种组件边界情况如何处理,很多边界情况如果你的文档中没有写,那么研发可能就不考虑了,不做任何处理,直到问题被领导或用户发现了,这时候只要你文档中没有提,那多半就要背锅了。一个你自认为通用的空数据页,你这个页面画了图,那个页面没有画空页面的图,那么研发就可以理所当然的在这个页面没有数据时啥都不展示。
这个现象主要出现在产品初期。随着产品迭代到一定程度,产品团队和研发团队通常会达成共识。此时前端的组件已经覆盖了大多数边界场景,产品团队也了解研发团队可能在哪些地方容易疏忽,因此需要特别强调这些方面。
时间分配
前面讲到研发会时不时找你对各种事情,这就导致了你平时可能很少有大段的时间专注于做一件事,每天上午研发问几个小问题,下午问几个小问题,一天就这么过去了。相比于写代码,有时思路清晰的话就可以一整天都在写,遇到问题了再停下来查一查想一想,相对来说还是会有大片的时间去做事。
对于我个人来说,有时候一个需求给到我,我可能一时半会儿没有一个好的思路,这时候你即便怎么想,怎么查都很难有突破,但有时你睡个午觉,或者放空一下,就突然有解决方向了。这导致了我做为产品经理时比较少进入心流状态,我需要思考,需要头脑风暴,但有时候想不出来也不能僵在那儿,而一件事情悬而未决又让我很难受,就容易有焦虑的情绪。
职业规划的变化
虽然很多人说技术人员越老越不吃香,产品就不一样了,可以积累经验,未来的道路也更加广阔,有机会走的更远。话虽如此,但实际上对于前端开发来说,基本上工作的两三年就基本上对整体的前端技术栈有个深入使用的经验,无非就是 React 和 Vue 以及相关的生态,,再深入点就是一些特殊协议或者可视化方面的更深入的技术,在跳槽时,只要技术栈匹配基本都可以尝试。但是产品经理的话,通常都会要求有同个赛道的经验,例如做社交产品的公司,当然希望找有社交产品经验的产品经理,而产品的赛道可就多了
前端转产品的优势
前面提了这么多研发转产品可能遇到的问题,也不能光抽巴掌不喂糖,所以下面再讲下前端转产品的优势,这些点大家可能会更有共鸣一些。
技术背景
关于这一点,有很多网上的文章唱反调说你有研发背景,肯定满脑子技术思维,做产品的时候创新能力肯定不行,你被你技术背景束缚啦。
这一点从我个人出发,我只能说产品懂技术绝对是一个优势,无论是从功能设计,到与研发沟通,天然的少了一层隔阂,尤其我们公司的产品是基础软件,用户群体本身就是相对懂技术的人。刚刚转为产品经理时,产品相关专业能力虽然不足,考虑事情不周全,但是懂技术起码给你兜了底,你不会有过于天马行空的想法,研发在你面前不能也信口开河,大致的实现成本,排期时间你能够做到心里有数。
前端思维
前端思维和技术背景还是有些不同的,前端和后端对产品的理解方向有些不同,前端偏向交互,后端更偏向业务。因此这里我将技术背景和前端思维分开讲。
不同公司使用的原型工具可能不同,我们公司是直接由产品来出高保真原型,因此我们使用的工具是 Figma,
在我转岗产品一个月左右 《👨💻 Figma 协作设计工具:前端开发者视角快速上手指南》 我就写了一篇关于 Figma 的使用文章,我认为我的 Figma 上手速度是非常快的,这可能由两方面的因素。
- 一是我大学的时候就有自学平面设计,对于 PPT,PS 这类创意类工具的使用方式比较了解。
- 二就是前端思维了,因为做原型其实和用代码写页面的思路是一致的,你看到了一个页面,首先你会想这个页面会由哪些组件组成,这些组件是否已经实现过了?如果没有实现过是否可以抽离到组件库进行复用?
Figma 这个工具是我转产品后,公司才开始使用的,因此大家都不熟悉,那么交互设计的大头就落在我身上,我就拉着我们的 UI 同学一起搭建了一个组件库,这个组件库中包含了常用的所有表单组件,表格组件,还有一些页面组件,其中我根据过往的经验为组件配置了很多参数和插槽,保证插件的拓展性。
如果我没有前端相关的经验,我敢肯定从头要去了解这套组件体系,并熟练的去构建出一套兼容性好的组件库。是要花很多时间的。
后续我们还用上了 Figma 的分支管理功能,这一点和 Git 的分支管理也有些许类似,逻辑上的快速迁移学习为我上手相关工具增速了很多。
对未来的思考
虽然在当前公司已经当了一年的产品经理了,大部分的事情都可以轻松 cover,也算是处于舒适圈了,但是心里还是会焦虑,毕竟目前只是身处一个小公司,而且在产品方面的专业能力又难以评估,网上想要学习关于产品经理的相关专业知识几乎都是卖课的,而且我愈发觉得产品相关的知识不是看任何书籍或课程可以快速提升的,产品经理相关的能力以及可能碰到的问题都是要在解决实际需求的过程中去提升,而且专业的能力与产品的赛道息息相关,你在一个赛道的积累可能换个赛道就派不上用场了,因此还是得持续学习,让自己不论做什么行业都能保持竞争力。
代码的话现在偶尔写写自己感兴趣的小项目,只是没那么多了,公司内偶尔也需要我去支援研发。如果大家有有意思的小项目,小比赛欢迎拉我一起去折腾(不做外包)。
来源:juejin.cn/post/7395559155686604809
哭了,朋友当韭菜被割惨了
最近我的朋友,被某些知识付费坑得很惨。全程毫无干货可言。内容仅仅只适用于初级、或者说部分中级的程序员。为此,我的朋友交了大几千的学费,却收获甚微。
当然,你可能说,是你的朋友问题啊?你朋友烂泥扶不上墙,学习方法不对,别人都有很多成功的案例。什么offer收到手酸,外包入大厂。
我买这些课就是为了学习,入门一些语言。知识付费很合理呀!!
于是我跟我朋友在微信彻夜长谈,有了如下分析
先说结论
请擦亮你的慧眼,你的一分一毫来之不易。不到迫不得已,才当学费
为什么这么说?
首先,不管你是想就业,还是想学习一些新的技术,网上都有例子,github上也会有前沿的项目提供学习。
类型 | 结论 |
---|---|
学习新技术 | 某项技术开源出来,作为技术的布道者,恨不得你免费过去学习,然后你再发一篇文章,越来越多人学习你的技术。 |
就业 | 简历包装无非就是抄抄抄,抄别人的优秀代码。github开源项目就非常合适 |
其次,你学费,一定要做到利益最大化。必须要有以下两点
- 能学到大部分人都学不到的技术亮点。记住,是大部分人,一定要做到差异化
- 能学到优秀的学习方法,push你前进。
开启慧眼
现在市面的学习机构,鱼龙混杂。,B站大学,某识xin球,某ke时jian 甚至,在某音上,都有那种连麦做模拟面试,然后引导你付费学习。
就业环境不好,买方市场竞争激烈,某些人就抓住你的焦虑心理,坑你一把。回想你的求学生涯,是否也有类似被坑经历?醒醒吧,少年。能救你的,只有你自己。
当然,小海也会有潜龙。不可否认,知识付费为我们提供了便利性。
- 原本散乱无章的知识点,人家给你整理好了,你尽管就是学习,实践
- 面对焦虑,你觉得很迷茫,需要一个人指点你前进
- 能认识更多同样诉求的人,为以后学习,就业,甚至做生意提供可能
但是,某些不法分子,就是抓住你的这个心理,疯狂ge你韭菜。什么10块钱知识手册,19.9面试题,100块钱的项目视频。天天一大早,就转发一些公众号到你群上,dddd。
这些内容,不是说没有用。我们讨论适合人群,这类东西不适合中高级程序员。
说那么多,你得学会判断这个人是不是大佬
你都可以简历包装,为什么‘大佬’就不会是被包装的
那就稍微整理一下,哪些是真大佬,伪大佬
真伪大佬
某佬 | 博客 | 开源项目 | 学习人群 | 是否顺眼 |
---|---|---|---|---|
伪大佬 | 面试题居多,很多基础内容,没有干货 | 无,或者很少。动不动就是商城,博客 | 应届生占比较多 | 可能顺眼 |
真大佬 | 博客、论坛内容干货。整理分类完善,你能学到东西 | 有,某些大项目的贡献,同时也有优秀开源项目 | 应届生,中高级都有 | 大多数不顺眼,因为实在优秀 |
就学习人群做一个说明
- 在就业容易程度上,相对于初中高级别的程序员,应届生无论从考察的内容,招聘的人数。都会容易丢丢。
- 他说跟着他学,offer赢麻了。但是其中,找到工作的大多数都是应届生
就这些点,我们其实可以能判断个大概了。
记住,你想知识付费。一定要摸清他的底细,不能认为他说得都是对的。人家也是会包装的
你的hello world
或许每个程序员的第一行代码,都是
print("hello world")
我想说的是,请你记住你的初心。
- 转行过来当程序员,就是为了狠狠赚他一笔
- 喜欢写代码,苦中作乐
情况每个人都不太一样,这里不细说。明白你是谁,你还是否有动力能坚持下去。明白这一点,远比你在迷茫的时候病急乱投医更为重要,请勿过度焦虑
为此,后面会说一下如何学习,以及找工作如何不被骗
力量大会
事关钱包的问题,我们都得谨慎谨慎。就业市场那恶劣,朋友找不到工作还被坑了一把。骗子实在可恶。请你先自身强大,先自己找出问题,不花冤枉钱,避免传销式编程
如有雷同,纯属巧合,没有针对任何人,也没有动某些人的饭碗。
来源:juejin.cn/post/7357231056288055336
揭秘小米手机被疯狂吐槽的存储扩容技术
前段时间,在小米14的发布会上,雷布斯公布了名为“Xiaomi Ultra Space存储扩容”的技术,号称可以在512G的手机中再搞出来16G,256G的手机中再搞出8G。对于普通用户来说,能多得一些存储空间,无异是个很好的福利,不过也有网友说这是以损害存储使用寿命为代价的,那么真相到底如何呢?这篇文章我就从技术角度来给大家详细分析下。
认识闪存
首先让我们来了解一些手机存储的基本知识。
手机存储使用的是闪存技术,其本质和U盘、固态硬盘都是一样的。
在闪存中读写的基本单位是页(Page),比页更大的概念是块(Block),一个块会包含很多页。
虽然读写的基本单位都是页,但是写实际操作的很可能是块,这是为什么呢?
这要从删除谈起,在闪存中删除数据时不会立即删除页上的数据,而只是给页打上一个空闲的标签。这是因为谁也不知道这个页什么时候会再写入数据,这样处理起来比较简单快速。
再看写操作,如果写入分配的页是因为删除而空闲的,数据并不能立即写入,根据闪存的特性,此时需要先把页上之前存储的数据擦除,然后才能写入;但是闪存中擦除操作的基本单位是块,此时就需要先把整个块中的有效数据读出来,然后再擦除块,最后再向块中写入修改后的整块数据;这整个操作称为“读-改-写”。当然如果写入分配的页是空白的,并不需要先进行擦除,此时直接写入就可以了。
预留空间
小米这次抠出来的存储空间来源于一个称为“预留空间”的区域,它的英文全称是Over Provisio,简称 OP。
那么“预留空间”是什么呢?我将通过5个方面来介绍它的用途,让大家近距离认识下。
提高写入速度
在上面介绍闪存的基本知识时,我们谈到闪存的写操作存在一种“读-改-写”的情况,因为额外的读和擦除操作,这种方法的耗时相比单纯的写入会增加不少,闪存使用的时间越长,空白的空间越少,这种操作越容易出现,闪存的读写性能下降的越快。
为了提升写入的性能,我们可以先将新数据写入到预留空间,此时上层系统就可以认为已经写入完成,然后我们在后台将预留空间中的新数据和原数据块中需要保留的数据合并到一个新的数据块中,这样就避免了频繁的读-修改-写操作,从而可以大大提高写入速度。
垃圾回收和整理
在上面介绍闪存的基本知识时,我们还谈到删除数据并不是立即清除空间,而是给数据页打一个标签,这样做的效率比较高。这样做就像我们标记了垃圾,但是并没有把它们运走,时间久了,这些垃圾会占用很多的空间。这些垃圾空间就像一个个的小碎片,所以有时也把这个问题称为碎片化问题。
虽然我们可以通过“读-改-写”操作来重新利用这些碎片空间,包括通过异步的“读-改-写”操作来提升上层应用的写入效率,但无疑还是存在写入的难度,实际写入之前还是要先进行擦除。
为了解决上述问题,聪明的设计师们又想到了新办方法:让存储器在后台自动检测、自动整理存储中的数据碎片,而不是等到写入数据时再进行整理。
考虑到闪存的读擦写特性,当需要移除数据块中部分碎片或者将不同数据碎片合并时,就得把需要保留的数据先放到一个临时空间中,以免数据出现丢失,待存储中的数据块准备好之后再重新写入,预留空间就可以用作这个临时空间。
磨损均衡
闪存中每个块的写入次数都是有限制的,超过这个限制,块就可能会变得不可靠,不能再被使用。这就是我们通常所说的闪存的磨损。
为了尽可能延长闪存的使用寿命,我们需要尽量均匀地使用所有的闪存块,确保每个块的使用频率大致相同。这就是磨损均衡的主要目标。
假设我们发现块A的使用频率过高,我们需要将它的数据移动到没怎么用过的块B去,以达到磨损均衡的目的。首先,我们需要读取块A中的数据,然后将这些数据暂时存储到预留空间。然后,我们擦除块A,将它标记为空闲。最后,我们从预留空间中取出数据,写入到块B。实际上,磨损均衡的策略比这更复杂,不仅仅是看使用频率,还需要考虑其他因素,比如块的寿命,数据的重要性等。
可以看到,预留空间在这个过程中起到了临时存储数据的作用。
不过你可能会问,为什么不直接将块A的数据复制到块B,而需要一个临时空间?
这是因为在实际操作中直接复制块A的数据到块B会带来一些问题和限制。
假如直接进行这种数据复制,那么在数据从块A复制到块B的过程中,块A和块B中都会存在一份相同的数据,如果有其他进程在这个过程中访问了这份数据,可能会产生数据一致性的问题。此外,如果移动过程中发生意外中断,如电源故障,可能会导致数据在块B中只复制了一部分,而块A中的数据还未被擦除,这样就可能导致数据丢失或者数据不一致的问题。
而如果我们使用预留空间,也就是引入一个第三方,就可以缓解这些问题。我们先将数据从块A复制到预留空间,然后擦除块A,最后再将预留空间中的数据写入到块B。在这个过程中,我们可以借助预留空间来实现一些原子性的机制,来保证数据不会丢失和数据的一致性。
错误校正
预留空间还可以用来存储错误校正码(ECC)。如果在读取数据时发现有错误,可以用错误校正码来修复这些错误,提高数据的可靠性。
很多同学可能也不了解这个错误校正码的来龙去脉,这里多说几句。
我们知道计算机中的数据最终都是二进制的0和1,0和1使用硬件比较好表达,比如我们使用高电压表示1,低电压表示0。但是硬件有时候会出错,本来写进去的是1,读出来的却是0。为了解决这个问题,设计师们就搞出来个错误校正码,这个校正码是使用某些算法基于要存储的数据算出来的,存储数据的时候把它一起保存起来。读取数据的时候再使用相同的算法进行计算,如果两个校正码对不上,就说明存储的数据出现错误了。然后ECC算法可以通过计算知道是哪一位出现了错误,改正它就可以恢复正确的数据了。
注意ECC能够修正的二进制位数有限,因为可以修复的位数越多,额外需要的存储空间也越大,具体能修复几位要考虑出现坏块的概率以及数据的重要性。
坏块管理
当闪存单元变为坏块时,预留空间可以提供新的闪存单元来替代坏块,此时读取对应数据时不再访问坏块,而是通过映射表转到预留空间中读取,从而保证数据的存储和读取不受影响,提高了固态硬盘的可靠性和耐用性。
综上所述,预留空间在提升固态硬盘性能,延长其使用寿命,提高数据的可靠性等方面发挥着重要的作用。
小米的优化
根据公开资料,小米将预留空间的占比从6.9%压缩到了约3%。
那么小米是怎么做到的呢?以下是官方说法:
小米在主机端也基于文件管理深度介入了 UFS 的资源管理,通过软件实现“数据非必要不写入(UFS)”,通过软件 + 固件实现“写入数据非必要不迁移”,减少写入量的同时也实现了更好的 wear-leveling 和 WAF
还有一张图:
优化解读
这里用了一些术语,文字也比较抽象,我这里解读下:
UFS(Universal Flash Storage)即通用闪存存储,可以理解为就是手机中的存储模块。
“数据非必要不写入(UFS)”也就是先把数据写入到缓冲区,然后等收到足够的数据之后(比如1页),再写入闪存单元,这样就可以减少闪存单元的擦写次数,自然就能延长闪存单元的使用寿命,推迟坏块的产生。这个缓冲区类似于计算机的内存,如果突然掉电可能会丢失一部分数据,但是对于手机来说,突然掉电这个情况发生的几率极低,所以小米在这里多缓存点数据对数据丢失的影响很小,不过还是需要注意缓冲空间有限,这个值也不能太大,具体多少小米应该经过大量测试之后做了评估。
“写入数据非必要不迁移” 没有细说怎么做的,大概率说的是优化磨损均衡、垃圾回收和整理策略,没事别瞎整理,整理的时候尽量少擦写,目的还是延长闪存单元的使用寿命。
“增加坏块预留” 小米可以根据用户的使用情况调整坏块预留区的大小,比如用户是个重度手机使用狂,他用1年相当于别人用4年,小米系统就会增加坏块预留区,以应对擦写次数增加带来的坏块几率增加。注意这个调整是在云端实现的,如果手机不联网,这个功能还用不上。
wear-leveling:就是上面提到的磨损均衡,小米优化了均衡算法,减少擦写。
WAF:写放大,Write Amplification Factor,缩写WAF。写放大就是上面提到的“读-改-写”操作引起的,因为擦除必须擦掉整个块的数据,所以上层系统只需要写一个页的情况下,底层存储可能要重写一个块,从页到块放大了写操作的数据量。因为闪存的寿命取决于擦除次数,所以写放大会影响到闪存的使用寿命。
概括来说就是,小米从存储的预留空间中抠出来一部分作为用户存储,不过预留空间的减小,意味着坏块管理、错误纠正等可以使用的空间变小,这些空间变小会减少存储的使用寿命,所以小米又通过各种算法延缓了手机存储的磨损速度,如此则对大家的使用没有什么影响,而用户又能多得一些存储空间。
小米的测试结果
对于大家担心小米手机存储的寿命问题,小米手机系统软件部总监张国全表示:“按照目前重度用户的模型来评估,在每天写入40GB数据的条件下, 256GB的扩容芯片依然可以保证超过10年, 512GB可以超过20年,请大家放心。”
同时一般固态硬盘往往都拥有5年的质保,而很多消费者往往会5年之内更换手机。因此按着这个寿命数据来看,普通消费者并不用太担心“扩容芯片”的寿命问题。所以如果你的手机用不了10年,可以不用担心这个问题。
当然更多的测试细节,小米并没有透漏,比如读写文件的大小等。不过按照小米的说法,存储的供应商也做了测试,没有什么问题。这个暂时只能相信小米是个负责任的企业,做好了完备的测试。
最后小米搞了这个技术,申请了专利,但是又把标准和技术方案贡献给了UFS协会,同时还要求存储芯片厂商设置了半年的保护期,也就是说技术可以分享给大家,但是请大家体谅下原创的辛苦,所以半年后其它手机厂商才能用上。
大家猜一下半年后其它手机厂商会跟进吗?
来源:juejin.cn/post/7297423930225639465
差生文具多,这么些年,为了写代码我花了多少钱?
背景
转眼写代码有10多年了,林林总总花费了很多的钱,现在主要按照4大件来盘点下我都买了啥。
电脑
acer 4741g ¥4500+
这是我入门时的一款电脑,整体配置在当时还是属于中等的。
当时用的编辑器还是notepad++,在这个配置下,还是可以愉快的编码的。
mac air 2013 ¥8800+
当时被苹果的放进信封的广告创意所折服,这也是我的第一台apple,在之后就一直用苹果了。到手后的感觉是,薄,确实薄,大概只有我宏基的1/3-1/4厚。
当时apple的简洁,快速,很少的配置,让我在环境变量上苦苦挣扎的心酸得以释放。以后也不用比较各种笔记本参数了
mac book pro13 2015 ¥9000+
当时买这台的原因是因为air进水了,经常死机,修了2次后,又坏了。一怒之下,直接买了一台。
换了新的retina屏之后,色彩质量和效果都提升了不少,对比原来的air性能也是拉升了超级多。但是因为是16上半年买的,所以没体验到toch bar,到现在都没体验过。。。
这是我真正意义上的第一台十分满意的电脑,大概是当时的理想型了。
公司电脑
2016年下半年进了一家创业公司,公司配置了mac book pro,比我的配置还高,所以之后一直就是用公司的。
2021年换新公司,公司配了thinkpad,又一次开始用win。然后又被win各种打败,有时又有了换回mac的想法。
当前–mac book pro14 2021 ¥21999
主要入手的原因是公司的电脑我觉得太慢了。当时开发小程序,脚手架是公司自己的,每次打包都是全量的,没有缓存。所以每次打包短则7,8分钟,长则10多分钟。加上切分支/安装依赖(如果两个分支依赖版本不同,需要强制更新),导致我每天花费大量的时间等待上。
同事早于我入手了M1,反馈巨好,于是我也买了,想着配置拉的满点,但是还是高估了自己的钱包,低估了苹果的价格,只能退而求其次的选择了中档配置。
每次看着低低的负载,都是满满的安全感。
另外m1是支持stable diffusion的,所以偶尔我也会炼丹
显示器
dell U2424H ¥1384
其实在写代码之前也买过几台显示器,但是以程序员视角来说,第一台是这台。原因是当时公司也是这个型号,主要是能旋转,谁能拒绝一台自由旋转的显示器呢?
而且dell的质量和做工都不错,在当时是十分喜欢的。
小米 Redmi 27 ¥789.9
dell那台显示器是放在家里的,公司也需要显示器,而且自带设备每个月可以补贴100,所以就入手了这款,原因无他:便宜,也够大。
但是用久了,发现也有些问题。例如失真等,但是真的便宜,
厂家送寄,但因为合作内容没谈拢,本周寄回
键盘
当前-cherry G80-3000 ¥689
一把真正可以用到包浆的键盘,大多数看到这个键盘的感觉应该都是黄色,而不是原本的白色,不知道是不是材质的问题,极其容易变黄。同时由于键帽又不变黄,所以呈现了诡异的脏脏的颜色。
因为本身机械键盘的高度,所以建议加个手托比较好。各种轴也齐全,任君选择。
目前这个键盘在家里游戏了,毕竟是个全键盘
当前–京造C2 ¥253
选择这个键盘的原因嘛,同事有了,并且是一个带灯的键盘。手感比cherry硬一些,但还属于是能接受的程度,整体延迟比较低(也可能是因为有线的原因)。目前是在办公室使用的一款,当前这篇文章就是用这个敲出来的。
鼠标
总览
鼠标其实留在手边的不太多,大多数都是消耗品了,这么些年,各种有用过。大概用了不下10个鼠标,我只挑2个重点的说吧。
微软ie 3.0 ¥359
这是我用过最好的鼠标,没有之一。握感极佳,用久了也不累,比其他的鼠标都舒服万分。
当前–apple magic trapad ¥899
mac用户的最终归属就是板子,如果你刚开始用mac,那么建议直接用板子吧。支持原生手势操作,各种mac本身触控板的事情都完美适用,真正的跟你的电脑和为一体。
欢迎评论区留言你的设备
如上所述,我这年的大头是电脑,消耗品是鼠标、,那么你都花了多少钱呢?
来源:juejin.cn/post/7395473411651682343