注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

React 你是真的骚啊,一个组件就有这么多个设计模式🙄🙄🙄

web
React 真的是太灵活了,写它就感觉像是在写原生 JavaScript 一样,一个功能你可以有多种实现方式,例如你要实现动态样式,只要你愿意去做,你会有很多种解决方案,这可能也就是 React 会比 Vue 相对来说比较难一点的原因,这或许也就是这么喜欢 R...
继续阅读 »

React 真的是太灵活了,写它就感觉像是在写原生 JavaScript 一样,一个功能你可以有多种实现方式,例如你要实现动态样式,只要你愿意去做,你会有很多种解决方案,这可能也就是 React 会比 Vue 相对来说比较难一点的原因,这或许也就是这么喜欢 React 的原因了吧,毕竟它可是我见一个爱一个的技术之一🤣🤣🤣


也正是因为这个原因,在 React 中编写一个组件就给我们编写一个组件提供了多种方式,那么在接下来的文章中我们就来讲解一下这几种组件的设计模式。


Mixin设计模式


在上一篇文章中有讲解到了 JavaScript 中的 Mixin,如果对这个设计模式不太理解的可以通过这篇文章进行学习 来学习一下 JavaScript 中的 Mixin


如何在多个组件之间共享代码,是开发者们在学习 React 是最先问的问题之一,你可以使用组件组合来实现代码重构,你也可以定义一个组件并在其他几个组件中使用它。


如何用组合来解决某个模式并不是显而易见的,React 受函数式编程的影响,但是它进入了由面向对象库主导的领域(hooks 出现以前),为了解决这个问题,React 团队在这加上了 Mixin,它的目标就是当你不确定如何使用组合解决想用的问题时,为你提供一种在组件之间重用代码。


React 最主流构建 Component 的方法是利用 createClass 创建,顾名思义,就是创造一个包含 React 方法 Class 类。


Mixin危害


React 官方文档 Mixins Considered Harmful 中提到了 Mixin 带来的危害,主要有以下几个方面:



  • Mixin 可能会相互依赖,相互耦合,不利于代码维护;

  • 不同的 Mixin 中的方法可能会相互冲突;

  • Mixin非常多时,组件是可以感知到的,甚至还要为其做相关处理,这样会给代码造成滚雪球式的复杂性;


装饰器模式


装饰器是一种特殊的声明,可以附加到类声明、方法、访问器、属性或参数上,装饰者使用 @+函数名 形式来修改类的行为。如果你对装饰器不太了解,你可以通过这一篇文章 TS的装饰器你再学不会我可就要报警了哈 进行学习。


现在我们来看看在 React 中怎么使用装饰器,我们现在有这样的一个需求,就是为被装饰的页面或组件设置统一的背景颜色和自定义颜色,完整代码具体如下:


import React, { Component } from "react";

interface Params {
background: string;
size?: number;
}

function Controller(params: Params) {
return function (
WrappedComponent: React.ComponentClass,
): React.ComponentClass {
WrappedComponent.prototype.render = function (): React.ReactNode {
return <div>但使龙城飞将在,不教胡马度阴山</div>;
};

return class Page extends Component {
render(): React.ReactNode {
const { background, size = 16 } = params;
return (
<div style={{ backgroundColor: background, fontSize: size }}>
<WrappedComponent {...this.props}></WrappedComponent>
</div>

);
}
};
};
}

@Controller({ background: "pink", size: 100 })
class App extends Component {
render(): React.ReactNode {
return <div>牛逼</div>;
}
}

export default App;

这段代码的具体输出如下所示:


image.png


在上面的代码中,Controller 装饰器会接收 App 组件,其中 WrappedComponent 就是我们的 App 组件,在这里我们通过修改原型方法 render 将其的返回值修改了,并对其进行了一层包裹。


所以 App 组件在使用了类装饰器,不仅可以修改了原来的 DOM,还对外层多加了一层包裹,理解起来就是接收需要装饰的类为参数,返回一个新的内部类。恰与 HOC 的定义完全一致。所以,可以认为作用在类上的 decorator 语法糖简化了高阶组件的调用。


高阶组件


HOC 高阶组件模式是 React 比较常用的一种包装强化模式之一,你也可以看作 React 对装饰模式的一种实现,高阶组件就是一个函数,并且该函数接收一个组件作为参数,并返回一个新的组件,它是一种设计模式,这种设计模式是由 React 自身的特性产生的结果。


高阶组件主要解决了以下问题,具体如下:



  • 复用逻辑: 高阶组件就像是一个加工 React 组件的工厂,你需要向该工厂提供一个坯子,它可以批量地对你送进来的组件进行加工,包装处理,还可以根据你的需求定制不同的产品;

  • 强化props: 高阶组件返回的组件,可以劫持上一层传过来的 props,染回混入新的 props,来增强组件的功能;

  • 控制渲染: 劫持渲染是 hoc 中的一个特性,在高阶组件中,你可以对原来的组件进行条件渲染,节流渲染,懒加载等功能;


HOC的实现方式


常用的高阶组件有两种方式,它们分别是 正向属性代理反向继承,接下来我们来看看这两者的区别。


正向属性代理


所谓正向属性代理,就是用组件包裹一层代理组件,在代理组件上,我们可以代理所有传入的 props,并且觉得如何渲染。实际上这种方式生成的高阶组件就是原组件的父组件,父组件对子组件进行一系列强化操作,上面那个装饰器的例子就是一个 HOC 正向属性代理的实现方式。


对比原生组件增强的项主要有以下几个方面:



  • 可操作所有传入的props: 可以对其传入的 props 进行条件渲染,例如权限控制等;

  • 可以操作组件的生命周期;

  • 可操作组件的 static 方法,但是需要手动处理,或者引入第三方库;

  • 获取 refs;

  • 抽象 state;


反向继承


反向继承其实是一个函数接收一个组件作为参数传入,并返回了一个继承自该传入的组件的类,并且在该类的 render() 方法中返回 super.render() 方法,能通过 this 访问到源组件的生命周期propsstaterender等,相比属性代理它能操作更多的属性。


两者区别



  • 属性代理是从组合的角度出发,这样有利于从外部操作被包裹的组件,可以操作的对象是 props,或者加一层拦截器或者控制器等;

  • 方向继承则是从继承的角度出发,是从内部去操作被包裹的组件,也就是可以操作组件内部的 state,生命周期,render 函数等;


具体实例代码如下所示:


function Controller(WrapComponent: React.ComponentClass) {
return class extends WrapComponent {
public state: State;
constructor(props: any) {
super(props);
this.state = {
nickname: "moment",
};
}

render(): React.ReactNode {
return super.render();
}
};
}

interface State {
nickname: string;
}

@Controller
class App extends Component {
public state: State = {
nickname: "你小子",
};
render(): React.ReactNode {
return <div>{this.state.nickname}</div>;
}
}

反向继承主要有以下优点:



  • 可以获取组件内部状态,比如 state,props,生命周期事件函数;

  • 操作由 render() 输出的 React 组件;

  • 可以继承静态属性,无需对静态属性和方法进行额外的处理;


反向继承也存在缺点,它和被包装的组件强耦合,需要知道被包装的组件内部的状态,具体是做什么,如果多个反向继承包裹在一起,状态会被覆盖。


HOC的实现


HOC 的实现方式按照上面讲到的两个分类一样,来分别讲解这两者有什么写法。


操作 props


该功能由属性代理实现,它可以对传入组件的 props 进行增加、修改、删除或者根据特定的 props 进行特殊的操作,具体实现代码如下所示:


import React, { Component } from "react";

interface Params {
background: string;
size?: number;
}

function Controller(params: Params) {
return function (
WrappedComponent: React.ComponentClass,
): React.ComponentClass {
WrappedComponent.prototype.render = function (): React.ReactNode {
return <div>但使龙城飞将在,不教胡马度阴山</div>;
};

return class Page extends Component {
render(): React.ReactNode {
const { background, size = 16 } = params;
return (
<div style={{ backgroundColor: background, fontSize: size }}>
<WrappedComponent {...this.props}></WrappedComponent>
</div>

);
}
};
};
}

@Controller({ background: "pink", size: 100 })
class App extends Component {
render(): React.ReactNode {
return <div>牛逼</div>;
}
}

export default App;

抽离state控制组件更新


高阶组件可以将 HOCstate 配合起来,控制业务组件的更新,在下面的代码中,我们将 inputvalue 提取到 HOC 中进行管理,使其变成受控组件,同时不影响它使用 onChange 方法进行一些其他操作,具体代码如下所示:


function Controller(WrappedComponent) {
return class extends React.Component {
constructor(props) {
super(props);
this.state = {
name: "",
};
this.onChange = this.onChange.bind(this);
}

onChange = (event) => {
this.setState({
name: event.target.value,
});
};

render() {
const newProps = {
value: this.state.name,
};
return (
<WrappedComponent
onChange={() =>
this.onChange}
{...this.props}
{...newProps}
/>

);
}
};
}

class App extends React.Component {
render() {
return (
<div>
<h1>{this.props.value}</h1>
<input name="name" {...this.props} />
</div>

);
}
}

export default Controller(App);

获取 Refs 实例


使用高阶组件后,获取到的 ref 实例实际上是最外层的容器组件,而非原组件,但是很多情况下我们需要用到原组件的 ref,我们先来看下面的代码,具体代码如下所示:


function Controller(WrappedComponent) {
return class Page extends React.Component {
render() {
const { ref, ...rest } = this.props;
return <WrappedComponent {...rest} ref={ref} />;
}
};
}

class Input extends React.Component {
render() {
return <input />;
}
}

class App extends React.Component {
constructor(props) {
super(props);
this.ref = React.createRef();
}
componentDidMount() {
console.log(this.ref);
}
render() {
return <Input ref={this.ref} />;
}
}

export default Controller(App);

image.png


通过查看控制台输出,你会发现获取到的是整个 Input 组件,那么有什么办法可以获取到 input 这个真实的 DOM 呢?


在之前的例子中我们可以通过 props 传递,一层一层传递给 input 原生组件来获取,具体代码如下:


class Input extends React.Component {
render() {
return <input ref={this.props.inputRef} />;
}
}

注意,因为传参不能传 ref,所以这里要修改一下


image.png


当然你也可以利用父组件的回调,具体代码如下:


class App extends React.Component {
constructor(props) {
super(props);
this.ref = React.createRef();
}
componentDidMount() {
console.log(this.ref);
}
render() {
return <Input inputRef={(e) => (this.ref = e)} />;
}
}

最终的代码如下图所示,这里展示了以上两个方法具体代码,如下图所示:


image.png


通过查看浏览器输出,两者都能成功输出原生的 ref 实例


image.png


React 给我们提供了一个 forwardRef 来帮助我们进行 refs 传递,这样我们在高阶组件上获取的 ref 实例就是原组件的 ref 了,而不需要手动传递,我们只需要修改一下 Input 组件代码即可,具体如下:


const Input = React.forwardRef((props, ref) => {
return <input type="text" ref={ref} />;
});

image.png


这样我们就获取到了原始组件的 ref 实例啦!


获取原组件的 static 方法


当待处理的组件为 class 组件时,通过属性代理实现的高阶组件可以获取到原组件的 static 方法,具体实现代码如下所示:


function Controller(WrappedComponent) {
return class Page extends React.Component {
componentDidMount() {
WrappedComponent.moment();
}
render() {
const { ref, ...rest } = this.props;
return <WrappedComponent {...rest} ref={ref} />;
}
};
}

class App extends React.Component {
static moment() {
console.log("你好骚啊");
}
render() {
return <div>你小子</div>;
}
}

export default Controller(App);

你好骚啊 正常输出


image.png


反向继承操作 state


因为我们高阶组件继承了传入组件,那么就是能访问到this了,有了 this 我们就能操作和读取 state,也就不用像属性代理那么复杂还要通过 props 回调来操作 state


反向继承的基本实现方法就是原组件继承 Component,再在高阶组件中通过把原组件传参,再生成一个继承自原组件的组件。


image.png


具体实例代码如下所示:


function Controller(WrappedComponent) {
return class Page extends WrappedComponent {
componentDidMount() {
console.log(`组件挂载时 this.state 的状态为`, this.state);
setTimeout(() => {
this.setState({ nickname: "你个叼毛" });
}, 1000);
// this.setState({ nickname: 1 });
}
render() {
return super.render();
}
};
}

class App extends React.Component {
constructor() {
super();
this.state = {
nickname: "你小子",
};
}
render() {
return <h1>{this.state.nickname}</h1>;
}
}

export default Controller(App);

代码具体输出如下图所示,当组件挂载完成之后经过一秒,state 状态发生改变:


image.png


劫持原组件生命周期


因为反向继承方法实现的是高阶组件继承原组件,而返回的新组件属于原组件的子类,子类的实例方法会覆盖父类的,具体实例代码如下所示:


function Controller(WrappedComponent) {
return class Page extends WrappedComponent {
componentDidMount() {
console.log("生命周期方法被劫持啦");
}
render() {
return super.render();
}
};
}

class App extends React.Component {
componentDidMount() {
console.log("原组件");
}
render() {
return <h1>你小子</h1>;
}
}

export default Controller(App);

代码的具体输出如下图所示:


image.png


render props 模式


render props 的核心思想是通过一个函数将组件作为 props 的形式传递给另外一个函数组件。函数的参数由容器组件提供,这样的好处就是将组件的状态提升到外层组件中,具体实例代码如下所示:


const Home = (props) => {
console.log(props);
const { children } = props;
return <div>{children}</div>;
};

const App = () => {
return (
<div>
<Home admin={true}>
<h1>你小子</h1>
<h1>小黑子</h1>
</Home>
</div>

);
};

export default App;

具体的代码运行结果如下图所示:


image.png


虽然这样能实现效果,但是官方说这是一个傻逼行为,因此官方更推荐使用 React 官方提供的 Children 方法,具体实例代码如下所示:


const Home = (props) => {
console.log(props);
const { children } = props;
return <div>{React.Children.map(children, (node) => node)}</div>;
};

const App = () => {
return (
<div>
<Home admin={true}>
<h1>你小子</h1>
<h1>小黑子</h1>
</Home>
</div>

);
};

export default App;

具体更多信息请参考 官方文档


实际上,我们经常使用的 context 就是使用的 render props 模式。


反向状态回传


这个组件的设计模式很叼很骚,就是你可以通过 render props 中的状态,提升到当前组件中也就是把容器组件内的状态,传递给父组件,具体示例代码如下所示:


import React, { useRef, useEffect } from "react";

const Home = (props) => {
console.log(props);
const dom = useRef();
const getDomRef = () => dom.current;
const handleClick = () => {
console.log("小黑子");
};
const { children } = props;
return (
<div ref={dom}>
<div>{children({ getDomRef, handleClick })}</div>
<div>{React.Children.map(children, (node) => node)}</div>
</div>

);
};

const App = () => {
const childRef = useRef(null);
useEffect(() => {
const dom = childRef.current();
dom.style.background = "red";
dom.style.fontSize = "100px";
}, [childRef]);

return (
<div>
<Home admin={true}>
{({ getDomRef, handleClick }) => {
childRef.current = getDomRef;

return <div onClick={handleClick}>你小子</div>;
}}
</Home>
</div>

);
};

export default App;

在运行代码之后,我们首先点击一下 div 元素,具体有如下输出,请看下图:


image.png


你会看到成功的在父组件操作到了子组件的 ref 实例了,还获取到了子组件的 handleClick 函数并成功调用了。


提供者模式


考虑一下这个场景,就好像爷爷要给孙子送吃的,按照之前的例子中,要通过 props 的方式把吃的送到孙子手中,你首先要经过儿子手中,再由儿子传给孙子,那万一儿子偷吃了呢?孙子岂不是饿死了.....


为了解决这个问题,React 提供了 Context 提供者模式,它可以直接跳过儿子直接把吃的送到孙子手上,具体实例代码如下所示:


import React, { createContext, useContext } from "react";

const ThemeContext = createContext({ nickname: "moment" });

const Foo = () => {
const theme = useContext(ThemeContext);
return <h1>{theme.nickname}</h1>;
};

const Home = () => {
const theme = useContext(ThemeContext);
return <h1>{theme.nickname}</h1>;
};

const App = () => {
const theme = useContext(ThemeContext);

return (
<div>
{
<ThemeContext.Provider
value={{
nickname: "你小子",
}}
>

<Foo />
</ThemeContext.Provider>
}
{
<ThemeContext.Provider
value={{
nickname: "首页",
}}
>

<Home />
</ThemeContext.Provider>
}
<div>{theme.nickname}</div>
</div>

);
};

export default App;

代码输出如下图所示:


image.png


到这里本篇文章也就结束了,Hooks 的就不讲啦,在这篇文章中有讲到一点,喜欢的可以看看 如何优雅设地计出不可维护的 React 组件


参考资料



总结


不管是使用高阶组件、render propscontext亦或是 Hooks,它们都有不同的使用场景,不能说哪个好用,哪个不好用,这就要根据到你的业务场景了,最后不得不说,React,你是真的骚啊......


最后希望这篇文章对你有帮助,如果错漏,欢迎留言指出,最后祝大嘎假期快来!


作者:Moment
来源:juejin.cn/post/7230461901356154940
收起阅读 »

别再删到手抽筋!JS中删除数组元素指南

web
作为一名前端开发工程师,我们经常需要在 JavaScript 中操作数组,其中比较常见的操作便是对数组进行元素的添加、删除和修改。在这篇文章中,我会详细介绍JS中所有删除数组元素的方法。 删除数组元素之splice() splice()方法可以向数组任意位置插...
继续阅读 »

cover.png


作为一名前端开发工程师,我们经常需要在 JavaScript 中操作数组,其中比较常见的操作便是对数组进行元素的添加、删除和修改。在这篇文章中,我会详细介绍JS中所有删除数组元素的方法。


删除数组元素之splice()


splice()方法可以向数组任意位置插入或者删除任意数量的元素,同时返回被删除元素组成的一个数组。


const arr = ['a', 'b', 'c', 'd', 'e'];
arr.splice(1, 2);//删除数组下标为1、2的元素
console.log(arr); // ["a", "d", "e"]

通过上述代码,可以看到元素'b'和'c'已被删除,被删除的元素以数组形式返回。需要注意的是,该方法会改变原数组,因此使用时应该谨慎。


删除数组元素之filter()


filter() 方法创建一个新数组,其包含通过所提供函数实现的测试的所有元素。它不会改变原始数组。


const arr = [10, 2, 33, 5];
const newArr = arr.filter(item => item !== 2);//过滤掉值为2的元素
console.log(newArr); //[10, 33, 5]

以上代码展示了如何使用 filter() 方法删除数组内某些元素。其中箭头函数 (item) => item !== 2 表示过滤掉数组元素中值为2的元素。


删除数组元素之pop()


pop() 方法用于删除并返回数组的最后一个元素。


const arr = [1, 2, 3];
const lastItem = arr.pop(); //删除元素3,lastItem为3
console.log(lastItem); //3
console.log(arr); //[1, 2]

通过上述代码可以看到,使用 pop() 方法可以非常容易地删除数组的最后一个元素。


删除数组元素之shift()


shift() 方法用于删除并返回数组的第一个元素。


const arr = [1, 2, 3];
const firstItem = arr.shift(); //删除元素1,firstItem为1
console.log(firstItem); //1
console.log(arr); //[2, 3]

与pop()类似, shift() 方法也是从数组中删除元素。但与 pop() 不同的是,它从数组头部开始删除。


删除数组元素之splice()、slice()和concat()组合操作


刚才已经讲到了 splice()方法的删除功能,现在我们还可以将slice()concat() 结合起来使用进行删除。


let arr = ['a', 'b', 'c', 'd', 'e'];
arr = arr.slice(0, 1).concat(arr.slice(2));//删除数组下标为1的元素
console.log(arr);//["a", "c", "d", "e"]

通过以上代码可以看出,使用 slice() 方法获取要删除的元素前面和后面的元素,最后使用 concat() 将两个数组合并成为一个新的数组。


删除数组元素之使用ES6中的扩展运算符


在ES6中,spread operator扩展运算符是用来展开一个可迭代对象,比如用于函数调用时的展开数组等。


let arr = ['a', 'b', 'c', 'd', 'e'];
arr = [...arr.slice(0, 1), ...arr.slice(2)];//删除数组下标为1的元素
console.log(arr);//["a", "c", "d", "e"]

通过以上代码可以看出,使用ES6中的扩展运算符(...)也可以方便地删除数组内某些元素。


总结


不同方法适用于不同情境,具体的使用应该根据情况而定。总体而言, splice()filter() 是两个最常用的方法,pop()shift() 则适合删除特定位置的元素。而在多种情况下,不同的操作组合也能实现有效删除。至于如何更好地使用这些方法,还需要根据实际情况进行深入应用和理解。


希望本文对你有所帮助,同时也欢迎拓展其他新颖的删除数组元素的方法。


作者:𝑺𝒉𝒊𝒉𝑯𝒔𝒊𝒏𝒈
来源:juejin.cn/post/7230460443189690405
收起阅读 »

移动端旅行网站页面

web
一、布局 1、首页 (1)头部 iconfont的使用和代码优化 iconfont.css中修改路径 引入iconfont.css import text-align: center(文字水平居中) 优化: 变量复用:src/assets/styles/...
继续阅读 »

一、布局


1、首页


(1)头部


iconfont的使用和代码优化



  • iconfont.css中修改路径

  • 引入iconfont.css import


text-align: center(文字水平居中)


优化:


  1. 变量复用:src/assets/styles/varibles.styl中定义变量 $变量名=值。在style中引入样式,@import(样式中引入样式需加@符号)

  2. 路径名过长:在css中引入其他的css想使用@符号,必须在@符号前加上~(@表示src目录)

  3. 路径别名:


build/webpack.base.conf.js->resolve->alias->创建别名


resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
//创建别名
'styles': resolve('src/assets/styles'),
'common':resolve('src/common')
}
}

报错原因:修改webpack中的内容后需要重启服务器


(2)轮播图


Swiper插件原理


Swiper是一款基于JavaScript的开源滑动插件,可以用于制作各种类型的轮摇图、滑动菜单、图片预览等。Swiper 的原理主要是通过监听用户的手势操作来实现滑动效果,同时利用CSS3动画和过渡效果来实现平滑的过渡和动画效果。



  1. 监听手势操作


Swiper通过监听用户的手势操作来实现滑动效果,具体包括touchstart、touchmove.touchend符事件。在touchstart 事件中,Swiper记录下用户的触摸起始位置及时间,touchmove事件中,Swiper根据用户移动的距离和时间计算出滑动速度和方向,从而控制滑动的行为;touchend事件中,Swiper根据滑动的距离和速度来判断是否进行下一张图片的切换。



  1. 切换图片


Swiper通过获取当前显示的图片索引及方向来计算出下一张图片的索引,并通过CSS3过渡效果来实现平滑的图片切换。同时,Swiper可以支持多种不同的切换效果,包括淡入淡出、渐变、滑动、翻转等,



  1. 响应式设计


Swiper支持响应式设计,可以根据不同的设备尺寸和屏幕方向来自动调整轮播图的大小和样式,从而提供更好的用户体验。同时,Swiper还支持自定义参数配置,可以灵活地控制轮播图的各种属性和行为。


问题


网速慢时图片没有加载出来造成的抖动(使用padding-bottom占位):


overflow: hidden
height:0
padding-bottom: 31.25% //高宽比

显示分页:


<!-- template -->
<swiper :options="swiperOption" v-if="showSwiper">
<!-- slides -->
<!-- 循环实现轮播图 -->
<swiper-slide v-for="item of list" :key="item.id">
<img class="swiper-img" :src="item.imgUrl" alt="">
</swiper-slide>
<!-- 插槽 -->
<div class="swiper-pagination" slot="pagination"></div>
</swiper>

// script
export default {
name: 'HomeSwiper',
props: {
list:Array
},
data() {
return {
swiperOption: {
// 设置显示轮播图分页
pagination: ".swiper-pagination",
//设置轮播图循环展示
loop: true,
// 设置自动轮播及间隔时间
autoplay: 3000
},
}
},
computed: {
// 列表为空时不显示轮播图
showSwiper() {
return this.list.length;
}
}
}

样式穿透



  • >>>

  • /deep/

  • ::v-deep



  1. 引入第三方组件库(如element-ui、element-plus),修改第三方组件库的样式。

  2. 样式文件中使用了 scoped 属性,但是为了确保每个组件之间不存在相互影响所以不能去除


/* style */
.wrapper >>> .swiper-pagination-bullet
background-color: #fff

插槽slot


通过slot插槽将页面中具体的数据传递给swiper组件,希望组件的内容可以被父组件定制的时候,使用slot向组件传递内容。


(3)图标区域


图标的轮播实现


使用computed计算功能实现图标的多页显示(容量为8,多出来的的图标换页显示):


computed: {
pages () {
// 创建二维数组pages
const pages = [];
// 对图标列表使用forEach循环
this.list.forEach((item, index) => {
// 计算页码
const page = Math.floor(index/8);
if (!pages[page]){
pages[page] = []
}
pages[page].push(item)
});
// pages[0]中存储在第一页显示的图标,以此类推
return pages
},
showIcon() {
return this.list.length
}
}

优化:


(1)希望文字过多时有…提示:


css中添加:


overflow: hidden
white-space:nowrap
text-overflow:ellipsis

(2)重复代码封装:


借助stylus提供的mixin对代码进行封装:


src/assets/varibles.styl中定义ellipse方法,在css中@import 文件,直接使用ellipse()


mixin(混入)

它提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。


使用场景: 不同组件中经常会用到一些相同或相似的代码,这些代码的功能相对独立。可以通过mixin 将相同或相似的代码提出来。


缺点:



  1. 变量来源不明确

  2. 多 mixin 可能会造成命名冲突(解决方式:Vue 3的组合API)

  3. mixin 和组件出现多对多的关系,使项目复杂度变高。


(4)推荐


text-indent:文字缩进


使用ellipsis()不起作用:父元素添加min-width: 0


(5)周末游


(6)Ajax获取首页数据


安装axios,引入axios


import axios from 'axios'
methods: {
getHomeInfo() {
// 使用axios.get()请求一个URL,返回的对象是一个promise对象,使用.then()获取
axios.get('/api/index.json?city='+this.city)
.then(this.getHomeInfoSucc);
},

getHomeInfoSucc(res) {
res = res.data;
if (res.ret && res.data) {
const data = res.data
// this.city = data.city
this.swiperList = data.swiperList
this.iconList = data.iconList
this.recommendList = data.recommendList
this.weekendList = data.weekendList
}
}
}

转发机制(开发环境的转发)



  • 只有static文件夹下的内容才可以被外部访问到

  • 现在用的是本地模拟的接口地址,假如代码要上线,肯定不能填成这样的一个地址,那么就需要在上线之前把这块儿的东西都重新替换成API这种格式,上线之前去改动代码儿是有风险的

  • 转发机制:webpack-dev-server提供 proxyTable 配置项,config/index.js 中proxyTable


module.exports = {
dev: {
// Paths
assetsSubDirectory: 'static',
assetsPublicPath: '/',
// 请求api目录时将请求转发到当前服务器的8080端口上,但是路径替换为/static/mock
proxyTable: {
'/api':{
target: 'http://localhost:8080',
pathRewrite: {
'^/api': '/static/mock'
}
}
}
}


  • 整个首页发送一个ajax请求而不是每个组件发送一个


(7)首页父子组件数据传递


父->子:属性传值,子组件props:{}接收


轮播图默认显示最后一张图


原因:


还没有接收ajax数据时,swiper接收的数据是一个空数组,当接受ajax数据后, swiperList变成真正的数据项。再传给home-swiper这个组件的时候,它才获取到新的数据,然后重新渲染了新数据对应的很多的幻灯片。因为swiper的初始化创建是根据空数组创建的,所以会导致默认显示的是所有的这个页面中的最后一个页面。


解决:


让swiper的初次创建由完整的数据来创建,而不是由那个空数组来创建。只需要写一个v-if,再写一个list.length。当传递过来的list是个空数组的时候,v-if的值是false,所以swiper不会被创建。只有等真正的数据过来了之后才会被创建。由于模板中尽量少出现逻辑性代码,所以创建一个计算属性computed,计算 list.length。


取消轮播图自动播放:autoplay: false


2、城市选择页


(1)路由配置


路由跳转:


import Vue from 'vue'
import Router from 'vue-router'
import Home from '../pages/home/Home'
import City from '../pages/city/City'
import Detail from '../pages/detail/Detail'

Vue.use(Router)

export default new Router({
routes: [{
path: '/',
name: 'Home',
component: Home
},
{
path: '/city',
name: 'City',
component: City
},
{
// 动态路由
path: '/detail/:id',
name: 'Detail',
component: Detail
}
],
scrollBehavior (to, from, savedPosition) {
return { x: 0, y: 0 }
}
})

使用:


<router-link to="/city">
<div class="header-right">
{{this.city}}
<span class="iconfont arrow-icon">&#xe600;</span>
</div>

</router-link>

(2)搜索框


定义一个keyword数据,与搜索框使用v-model做双向数据绑定。


<div class="search">
<input v-model="keyword" class="search-input" type="text" placeholder="输入城市名或拼音" />
</div>

City.vue父组件向Search.vue组件传值(cities),Search.vue接收cities


使用watch监听keyword的改变(使用节流)


解决匹配城市过多无法滚动问题


import Bscroll,在mounted中创建一个BScroll, this.scroll = new BScroll(this.$refs.search),通过$ref获取需要滚动的元素。


<!--搜索结果显示框-->
<div class="search-content" ref="search" v-show="keyword">
<ul>
<!--解决删除输入列表依然存在的问题-->
<li class="search-item border-bottom" v-for="item in list" :key="item.id" @click="handleCityClick(item.name)">{{item.name}}</li>
<li class="search-item border-bottom" v-show="hasNoData">没有找到匹配数据</li>
</ul>
</div>

优化


1.解决删除输入列表依然存在的问题:v-show = "keyword"

2.没有找到匹配项时,显示“没有找到匹配数据”:v-show = "!this.list.length"

双向数据绑定原理(搜索时使用)




  • 概念:

    Vue 中双向绑定是一个指令v-model,可以绑定一个响应式数据到视图,同时视图的变化能改变该值。v-model是语法糖,默认情况下相当于:value@inputv-bind:valuev-on:input),使用v-model可以减少大量繁琐的事件处理代码,提高开发效率。




  • 使用:

    通常在表单项上使用v-model,还可以在自定义组件上使用,表示某个值的输入和输出控制。




  • 原理:

    v-model是一个指令,双向绑定实际上是Vue 的编译器完成的,通过输出包含v-model模版的组件渲染函数,实际上还是value属性的绑定及input事件监听,事件回调函数中会做相应变量的更新操作。




(3)列表


引入区块滚动


子元素使用float,父元素开启BFC(overflow:hidden)去除高度塌陷。


使列表区域无法滚动(position:absolute + overflow:hidden),然后使用better-scroll插件


better-scroll



  • 安装better-scroll包。

  • 需要 better-scroll 包裹的所有元素最外层需要使用一个div包裹,并设置一个ref属性方便创建scroll。

  • 创建scroll


<div class="list" ref="wrapper">list中的元素<div>

import Bscroll from 'better-scroll'
// 写在mounted钩子函数中,此时页面挂载完成,可以操作DOM元素。
mounted() {
this.scroll = new Bscroll(this.$refs.wrapper)
}

$ref


ref属性:获取DOM。


在vue中ref可以以属性的形式添加给标签或者组件:



  • ref 写在标签上时:this.$refs.ipt 获取的是添加了ref="ipt"标签对应的dom元素;

  • ref 写在组件上时:this.$refs['component'] 获取到的是添加了ref="component"属性的这个组件。


$refs 是所有注册过 ref 的集合(对象);若是遍历的ref,则对应$refs是个数组集合


注意:$refs不是响应式的,只在组件渲染完成之后才填充。所以想要获取DOM数据的更新要使用 this.$nextTick()


字母表和城市列表字母的联动


兄弟组件传值:


Alphabet组件将值传递给父组件City.vue,父组件将值传递给子组件List.vue实现字母表和城市列表字母的联动。


为每个字母绑定一个onclick事件,在方法中使用this.$emit向外传递change事件。


<!--Alphabet.vue-->
<template>
<ul class="list" >
<li class="item" v-for="key in letters"
:key="key"
:ref="key"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
@click="handleLetterClick"
>

{{key}}
</li>
</ul>
</template>


// Alphabet.vue
methods: {
handleLetterClick (e) {
// 向外(City.vue)传递事件
this.$emit("change", e.target.innerText);
}
}

父组件


<!--City.vue-->
<city-alphabet :cities="cities" @change="handleLetterChange"></city-alphabet>

// City.vue
data () {
return {
letter: '' // 被点击的字母
}
},
methods: {
handleLetterChange (letter) {
this.letter = letter;
// console.log(letter);
}
}

<!-- 向List组件传值letter -->
<city-list :cities="cities" :hot="hotCities" :letter="letter"></city-list>

// List.vue接收letter
props: {
hot: Array,
cities: Object,
letter: String
},

使用watch监听letter变化,当letter发生变化时,调用this.scroll.scrollToElement()方法将区域自动滚动到指定区域,在template中给每一个area区域加一个:ref='key',通过this.$refs[this.letter][0]获取值为this.letter的DOM元素。


<div class="area" v-for="(item, key) in cities" :key="key" :ref="key" >
<div class="title border-topbottom">{{ key }}</div>
<div class="item-list">
<div class="item border-bottom" v-for="innerItem in item" :key="innerItem.id" @click="handleCityClick(innerItem.name)">{{innerItem.name}}</div>
</div>
</div>

watch: {
letter () {
if (this.letter) {
// 获取值为`this.letter`的DOM元素
const element = this.$refs[this.letter][0]
// 将区域自动滚动到指定区域
this.scroll.scrollToElement(element)
}
}
}

上下拖拽字母表touch

为字母表绑定三个事件:


@touchstart="handleTouchStart"  //手指开始触摸时设置this.touchStatus = true
@touchmove="handleTouchMove" //在true时对触摸事件做处理
@touchend="handleTouchEnd" //手指结束触摸时设置this.touchStatus = false

// 构建字母数组
computed: {
// 构建字母数组["A", "B", ……, "Z"]
letters () {
const letters = [];
for (let i in this.cities){
letters.push(i);
}
return letters;
}
},
methods: {
handleLetterClick (e) {
// 向外(City.vue)传递事件
this.$emit("change", e.target.innerText);

},
handleTouchStart () {
this.torchStatus = true;
},
handleTouchMove (e) {
if (this.torchStatus) {
// 如果timer已经存在,去除timmer(即上一次的事件还未执行完毕又出发了下一次事件,就用下一次事件覆盖上一次的事件)
if (this.timer){
clearTimeout(this.timer)
}
// 否则就创建一个timer
//节流,将操作延迟16ms执行,如果上一个操作间隔小于16ms,则清除上一个操作,直接执行这次操作,减少handleTouchMove的使用频率
this.timer = setTimeout(() => {
const touchY = e.touches[0].clientY - 79; // 当前手指触摸位置与头部下沿的距离,79px为头部的高度
const index = Math.floor((touchY - this.startY) / 20); //当前手指触摸位置的元素
// 合法时向父元素emit change事件
if (index >= 0 && index < this.letters.length){
this.$emit('change', this.letters[index]);
}
}, 16)

}
},
handleTouchEnd () {
this.torchStatus = false
}
}

优化

1、将字母A到头部下沿的距离的计算放在updated生命周期钩子函数中


初始时cities值为0,当Ajax获取数据后,cities的值才发生变化,AlphaBet才被渲染出来, 当往alphabet里面传的数据发生变化的时候,alphabet这个组件儿就会重新渲染。之后,updated这个生命周期钩子就会被执行。这个时候,页面上已经展示出了城市字母列表里所有的内容,去获取A这个字母所在的dom对应的offsettop的值就没有任何的问题了。


2、节流


如果timer已经存在,去除timmer(即上一次的事件还未执行完毕又出发了下一次事件,就用下一次事件覆盖上一次的事件)。


否则就创建一个timer,将操作延迟16ms执行,如果上一个操作间隔小于16ms,则清除上一个操作,直接执行这次操作,减少handleTouchMove的使用频率。


3、使用Vuex实现首页和城市选择页面的数据共享



  1. 安装vuex

  2. 创建src/store/index.js文件


import Vue from 'vue'
import Vuex from 'vuex'
import state from './state'
import mutations from './mutations'

Vue.use(Vuex)

export default new Vuex.Store({
state,
// actions: {
// changeCity (ctx, city) {
// ctx.commit('changeCity', city)
// }
// },
mutations
})


  1. main.js中引入store


import store from './store'


  1. 在mainjs中创建根实例时将store传入,store会被派发到每个子组件中,每个子组件中都可以使用this.$store获取到 store。


new Vue({
el: '#app',
router,
store,
components: { App },
template: '<App/>'
})

(1)Vuex


概念


Vuex 是 Vue 专用的状态管理库,它以全局方式集中管理应用的状态,并以相应的规则保证状态以一种可预测的方式发生变化。主要解决的问题是多组件之间状态共享。


image.png


核心概念



  • State:存放核心数据

  • Action:异步方法

  • Mutation:同步的对数据的改变

  • Getter:类似于计算属性,提供新的数据,避免数据的冗余

  • Module:模块化,拆分


image.png


项目中Vuex的使用



  • state存放当前城市CurCity

  • 为每个城市元素都绑定一个onclick事件(获取改变的city),点击城市

  • 在List组件中调用dispatch方法->触发Action

  • 在store/index.js中增加一个actions对象(接收city),使用commit调用mutation

  • mutations中,令state.city = city,完成


// List.vue
this.$store.dispatch('changeCity', city)

// store/index.js
actions: {
changeCity (ctx, city) {
// 使用Commit调用Mutation
ctx.commit('changeCity', city)
}

},
mutations: {
changeCity (state, city) {
state.city = city;
try {
localStorage.city = city
} catch (e) {}
}
}

image.png


(2)单页面与多页面


实现单页面跳转的方式



  1. 标签实现



  • a标签

  • router-link标签


<a href="#/xxx" />
<router-link to="/xxx" />


  1. 函数实现



  • 传统的window.location.href

  • 使用vue-router中的router对象 (点击城市后自动跳转到首页)
    step1 定义一个实现onclik事件的组件


<a onClick={this.goTo} />

step2 在goTo函数中实现跳转


goTo = () => {
// 方案1 传统的window.location.href
window.location.href = "#/xxx"

// 方案2 使用vue-router中的router对象
this.$router.push('/');
}

项目中,点击城市后跳转到首页:this.$router.push


methods: {
handleCityClick (city) {
this.$router.push('/');
}
},

(3)localStorage(state和mutations中使用)


//state.js
let defaultCity = "上海"
try {
if (localStorage.city){
defaultCity = localStorage.city
}
}catch (e) {}

export default{
city: defaultCity
}

// mutation.js
export default{
changeCity (state, city) {
state.city = city;
try {
localStorage.city = city
} catch (e) {}

}
}

(4)keep-alive优化网页性能


每次切换回组件时,组件就会被重新渲染,mounted生命周期钩子就会被重新执行,ajax数据就会被重新获取,导致性能低。


<!-- App.vue -->
<keep-alive>
<router-view/>
</keep-alive>

使用keep-alive后会多出两个生命周期钩子函数activated和deactivated
activated在每次切换回组件时调用,因此可以在activated中定义方法,在城市切换时重新发送ajax请求。


4、景点详情页


(1)动态路由及banner分支


为recommend组件的li标签外部包裹一个router-link标签,li标签样式改变,解决方式:


将li标签直接替换为router-link标签, 加入一个tag=“li”的属性,动态绑定::to = "'/detail/' + item.id",在router/index.js设置Detail路由


<!-- recommend.vue -->
<router-link tag="li"
class="item border-bottom"
v-for="item in list"
:key="item.id"
:to="'/detail/' + item.id">

</router-link>

// router/index.js
{
path: '/detail/:id',
name: 'Detail',
component: Detail
}

图片底部渐变效果:


background-image: linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.8))

(2)公用图片画廊组件拆分


将画廊组件变成一个公用组件src/common/gallary/Gallary.vue


画廊组件:图片轮播+页码显示


使用swiper插件实现轮播功能,使用swiper插件的 paginationType: 'fraction',将分页器样式设置为页码。

‘bullets’  圆点(默认)
‘fraction’  分式 
‘progress’  进度条
custom’ 自定义

创建路径别名,重启服务器。


一开始将gallary显示为隐藏状态,再次显示时计算宽度出现问题,swiper无法正确显示,解决:


data () {
return {
showGallary: true,
swiperOptions: {
pagination: '.swiper-pagination',

// 将分页器样式设置为页码
paginationType: 'fraction',

// swiper监听到此元素或父级元素发生DOM变化时,就会自动刷新一次,解决宽度计算问题。
observeParents:true,
observer:true,

loop:true,
autoplay: false
}
}
}

(3)实现header渐隐渐现效果


methods: {
handleScroll () {
// 获取当前页面的滚动条纵坐标位置
const top = document.documentElement.scrollTop;
// top > 60 时开始逐渐显示header,top > 40 时一直显示header
if (top > 60){
const opacity = top / 140;
opacity > 1 ? 1 : opacity;
this.opacityStyle = {
opacity
};
this.showAbs = false;
}
else{
this.showAbs = true
}
}
},
activated () {
window.addEventListener('scroll', this.handleScroll)
},


(4)对全局事件的解绑(window对象)


// 页面即将被隐藏时执行
deactivated () {
window.removeEventListener('scroll', this.handleScroll)
}

(5)使用递归组件实现详情页列表


(6)使用Ajax获取动态数据


获得动态路由参数(id)并将其传递给后端、


getDetailInfo () {
axios.get('/api/detail.json',{
params: {
id: this.lastRouteId
}
}).then(this.handleGetDetailInfoSucc)
}

(7)组件跳转时页面定位


scrollBehavior (to, from, savedPosition) {
return { x: 0, y: 0 }
}

(8)在项目中加入基础动画


在点击画廊组件时增加渐隐渐现的动画效果


//FadeAnimation.vue

<template>
<transition>
<slot></slot>
</transition>
</template>
<script>
export default{
name: "FadeAnimation"
}
</script>
<style lang="stylus" scoped>
@import '~styles/varibles.styl'
.v-enter, .v-leave-to
opacity: 0
.v-enter-active, .v-leave-active
transition: opacity .5s
</style>

common-gallary 作为插槽插入到fade-animation中:


<!--Banner.vue-->

<fade-animation>
<common-gallary
:imgs="bannerImgs"
v-show="showGallary"
@close="handleGallaryClose"
>

</common-gallary>
</fade-animation>

二、优化



  • 网速慢时图片没有加载出来造成的抖动(使用padding-bottom占位)

  • 重复代码封装:借助stylus提供的mixin对代码进行封装

  • 节流:触摸滑动字母表&搜索框中输入

  • keep-alive

  • 对全局事件解绑


作者:树上结了个小橙子
来源:juejin.cn/post/7222627262399365180
收起阅读 »

快看一看,你是不是这样使用的vscode

web
俗话说:“工欲善其事,必先利其器”。想要高效的编写代码,自然要先熟练的使用一款工具。 vscode设置简体中文 使用国外的工具,头等大事自然是必不可少的汉化。 按快捷键 ‘ctrl+shift+p’,在顶部出现的输入框输入‘configure language...
继续阅读 »

俗话说:“工欲善其事,必先利其器”。想要高效的编写代码,自然要先熟练的使用一款工具。


vscode设置简体中文


使用国外的工具,头等大事自然是必不可少的汉化。


按快捷键 ‘ctrl+shift+p’,在顶部出现的输入框输入‘configure language’,按回车,选择‘zh-cn’。此时,会自动安装中文插件,然后重新打开vscode就可以看到中文界面了。

image.png


vscode实用插件


选择vscode的原因,除了它的轻量之外,自然少不了它丰富的插件库。


1. Auto Rename Tag


自动修改匹配的html标签。在修改标签的时候,是不是需要修改完开始标签之后还需要修改结束标签。安装Auto Rename Tag,以后只需要修改一个标签就可以了,四舍五入就等于减少一半工作量啊。


2. Prettier


代码格式化插件,一键格式化代码,也可以设置保存自动格式化。我会将我的配置放在文章末尾。


3. code runner


可以直接js文件,在控制台输出结果。在写一些小算法的时候再也不用频繁刷新页面打印了。


image.png


4. Turbo Console Log


快捷添加 console.log,一键 注释、启用、删除 所有 console.log。调试js时候大概都会用console.log,每次手敲都很麻烦。


ctrl + alt + l 选中变量之后,生成 console.log
alt + shift + c 注释所有 console.log
alt + shift + u 启用所有 console.log
alt + shift + d 删除所有 console.log

注意,只能注释、启用、删除ctrl + alt + l生成的console.log。如果有小伙伴安装了印象笔记,ctrl + alt + l和印象笔记是冲突的。


5. css-auto-prefix


自动添加 CSS 私有前缀。
比如写完transform样式,会自动添加-webkit-、-moz-等样式。


配置


接下来便是无处不在的配置了,将我的配置贴出来,供大家参考。


文件->首选项->设置->工作台->设置编辑器,将editor的ui改为json,将配置直接粘贴进去


{
"eslint.enable": true,
"eslint.run": "onType",
"eslint.options": {
"extensions": [
".js",
".vue",
".jsx",
".tsx"
]
},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"workbench.iconTheme": "material-icon-theme",
"workbench.colorTheme": "Monokai",
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"workbench.settings.editor": "json",
"editor.tabSize": 2,
//失去焦点后自动保存
"files.autoSave": "onFocusChange",
// #值设置为true时,每次保存的时候自动格式化;
"editor.formatOnSave": true,
// 在使用搜索功能时,将这些文件夹/文件排除在外
"search.exclude": {
"**/node_modules": true,
"**/bower_components": true,
"**/target": true,
"**/logs": true,
},
// #让vue中的js按"prettier"格式进行格式化
"vetur.format.defaultFormatter.html": "js-beautify-html",
"vetur.format.defaultFormatter.js": "prettier",
"vetur.format.defaultFormatterOptions": {
"js-beautify-html": {
// #vue组件中html代码格式化样式
"wrap_attributes": "force-aligned", //也可以设置为“auto”,效果会不一样
"wrap_line_length": 200,
"end_with_newline": false,
"semi": false,
"singleQuote": true
},
"prettier": {
"semi": false,
"singleQuote": true
}
},
"[jsonc]": {
"editor.defaultFormatter": "vscode.json-language-features"
}
}

快捷键


熟练的时候快捷键,可以更高效的提升敲代码的效率。我将我常用的快捷键列出,供大家参考。


ctrl + d 选中一个词 鼠标双击也可以
ctrl + f 搜索
ctrl + p 快速打开文件
ctrl + shift + [ 折叠区域代码
ctrl + shift + ] 展开区域代码
ctrl + shift + k 删除一行代码,不过我更喜欢用ctrl+x,因为一只手就可以操作

作者:前端手记
来源:juejin.cn/post/7226248402799345719
收起阅读 »

手写一个类似博客的个人主页 css动画效果多

web
手写一个好看的个人主页 效果图 文字,图片加上各种动画显示,使页面更加美观。然后给头像也加上了一个动画,使头像实现一个一直在上下浮动的效果。媒体组件logo,添加事件hover,置顶会变颜色。按钮添加active激活样式。 最好打开码上掘金看效果图,更明显,...
继续阅读 »

手写一个好看的个人主页


效果图


文字,图片加上各种动画显示,使页面更加美观。然后给头像也加上了一个动画,使头像实现一个一直在上下浮动的效果。媒体组件logo,添加事件hover,置顶会变颜色。按钮添加active激活样式。
image.png
最好打开码上掘金看效果图,更明显,因为显示框问题,这样看布局有点问题。


整体布局


头部


包含一个logo,和一个 导航栏。
logo我这里用了一段文字替代,大家可以自己替换。
header都处于同一行,于是我采用了弹性布局。因为一个左一个右,就可以使用justify-content: space-between;然后logo我给它设置了一个从左往右的动画,时间为1s。导航栏添加了一个从上往下的动画,不过注意的是,我们可以看到每一个导航栏元素是递进往上升的。


实现导航栏元素递进往上升的关键。<a href="#" class="item" style="--i:1">Home</a>我们给每个导航元素都添加了css属性,然后通过这个属性, calc(.2S * var(--i)) 计算每个不同元素的延迟时间,这样我们就可以看到这种延迟效果。
image.png


.header {
position: fixed;
/* 将导航栏固定在页面顶部 */
top: 0;
left: 0;
width: 100%;
padding: 20px 10%;
/* 设置导航栏内边距 */
background: transparent;
/* 设置导航栏背景为透明 */
display: flex;
/* 将导航栏的子元素设置为flex布局 */
justify-content: space-between;
/* 将导航栏子元素分散对齐 */
align-items: center;
/* 将导航栏子元素垂直居中对齐 */
z-index: 100;
/* 将导航栏设置为最上层 */
}

/* 设置导航栏Logo的样式 */
.logo {
font-size: 25px;
/* 设置字体大小 */
color: #fff;
/* 设置字体颜色 */
text-decoration: none;
/* 取消下划线 */
font-weight: 600;
/* 设置字体粗细 */
cursor: default;
/* 设置鼠标样式为默认 */
opacity: 0;
/* 设置初始透明度为0 */
animation: slideRight 1s ease forwards;
/* 设置动画效果 */
}

/* 设置导航栏链接的样式 */
.navbar a {
display: inline-block;
/* 将链接设置为块级元素 */
font-size: 18px;
/* 设置字体大小 */
color: #fff;
/* 设置字体颜色 */
text-decoration: none;
/* 取消下划线 */
font-weight: 500;
/* 设置字体粗细 */
margin-left: 35px;
/* 设置左侧间距 */
opacity: 0;
/* 设置初始透明度为0 */
transition: 0.3s;
/* 设置过渡效果 */
animation: slideTop 1s ease forwards;
/* 设置动画效果 */
animation-delay: calc(.2S * var(--i));
/* 设置动画延迟时间 */
}

/* 设置导航栏链接的鼠标悬停和点击样式 */
.navbar a:hover,
.navbar a:active {
color: #b7b2a9;
/* 设置字体颜色 */
}


<header class="header">
<!-- 网站Logo -->
<a href="#" class="logo">
This is a Logo!
</a>

<!-- 导航栏 -->
<nav class="navbar">
<!-- 导航栏选项1 -->
<a href="#" class="item" style="--i:1">Home</a>
<!-- 导航栏选项2 -->
<a href="#" class="item" style="--i:2">About</a>
<!-- 导航栏选项3 -->
<a href="#" class="item" style="--i:3">Skills</a>
<!-- 导航栏选项4 -->
<a href="#" class="item" style="--i:4">Me</a>
</nav>

</header>

主页部分


主页部分包含文字区域和头像区域。


在文字区域里有一个打印机效果输出文字,可以看我上一篇文章。html手写一个打印机效果-从最基础到学会。然后给每个文字设置不同的动画,比如第一个h1我们让它从上往下,然后第二个h1我们让它从下往上,在他们中间的h1我们让它从左向右出现。在文字区域还有一块是一些media的组件logo,这个我是通过一个js引入的库。然后这些logo跟导航栏元素大致相同,我们也给他们定义了一个css属性,可以让他们相继出现。然后按钮也是添加向上的动画,给定一个延迟时间。


头像区域,我们给头像设置了两个动画,其实动画非常简单,一个其实就是为了显示头像,另一个实现头像上下浮动的效果。


 /* 定义放大的动画 */
@keyframes zoomIn {
0% {
transform: scale(0);
}

100% {
transform: scale(1);
opacity: 1;
}
}

/* 定义图片浮动的动画 */
@keyframes floatImg {
0% {
transform: translateY(0);
}

50% {
transform: translateY(-36px);
}

100% {
transform: translateY(0);
}
}

<link rel="stylesheet" href="https://unpkg.com/boxicons@2.1.4/css/boxicons.min.css">

 <!-- 主页部分 -->

<section class="home">
<!-- 主页内容 -->
<div class="home-content">
<!-- 主页标题 -->
<h3>Hello,It's Me</h3>
<h1>Welcome To Know Me!</h1>
<!-- 主页小标题 -->
<h3>个人介绍
<!-- 小标题下的文本 -->
<span class="text">
</span>
</h3>
<!-- 主页正文 -->
<p>越努力,越幸运!!!Lucky!</p>
<!-- 社交媒体链接 -->
<div class="social-media">
<!-- 社交媒体链接1 -->
<a href="#" style="--i:7"><i class="bx bxl-tiktok"></i></a>
<!-- 社交媒体链接2 -->
<a href="#" style="--i:8"><i class="bx bxl-facebook-circle"></i></a>
<!-- 社交媒体链接3 -->
<a href="#" style="--i:9"><i class="bx bxl-google"></i></a>
<!-- 社交媒体链接4 -->
<a href="#" style="--i:10"><i class="bx bxl-linkedin-square"></i></a>
</div>
<!-- 主页按钮 -->
<a href="#" class="btn">Learn More</a>
</div>
<!-- 主页图片 -->
<div class="home-img">
<img src="https://img.wxcha.com/m00/54/ed/69d26be4a4ac700e27c2d9cf85472b8c.jpg" alt="">
</div>
</section>

整体代码


动画的代码


  /*动画*/
@keyframes blink {

from,
to {
border-color: transparent;
/* 透明边框颜色 */
}

50% {
border-color: white;
/* 白色边框颜色 */
}
}

/* 定义向右滑动的动画 */
@keyframes slideRight {
0% {
transform: translateX(-100px);
}

100% {
transform: translateX(0px);
opacity: 1;
}
}

/* 定义向左滑动的动画 */
@keyframes slideLeft {
0% {
transform: translateX(100px);
}

100% {
transform: translateX(0px);
opacity: 1;
}
}

/* 定义向上滑动的动画 */
@keyframes slideTop {
0% {
transform: translateY(100px);
}

100% {
transform: translateY(0px);
opacity: 1;
}
}

/* 定义向下滑动的动画 */
@keyframes slideBottom {
0% {
transform: translateY(-100px);
}

100% {
transform: translateY(0px);
opacity: 1;
}
}

/* 定义放大的动画 */
@keyframes zoomIn {
0% {
transform: scale(0);
}

100% {
transform: scale(1);
opacity: 1;
}
}

/* 定义图片浮动的动画 */
@keyframes floatImg {
0% {
transform: translateY(0);
}

50% {
transform: translateY(-36px);
}

100% {
transform: translateY(0);
}
}

源码


链接自取
掘金/个人页面 · Mr-W-Y-P/Html-css-js-demo - 码云 - 开源中国 (gitee.com)


作者:Mr-Wang-Y-P
来源:juejin.cn/post/7225444782331592764
收起阅读 »

css水滴登录界面

web
前言 今天我们来分享一款非常有趣的登录界面,它使用HTML和CSS制作,具有动态的水波纹效果,让用户在登录时感受到了一股清凉之感。 基本html框架 <!DOCTYPE html> <html lang="en"> <head&...
继续阅读 »

前言


今天我们来分享一款非常有趣的登录界面,它使用HTML和CSS制作,具有动态的水波纹效果,让用户在登录时感受到了一股清凉之感。


基本html框架


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="water.css">
<link rel="stylesheet" href="form.css">
</head>

<body>
<div class="main">
<form>
<p>用户名<br />
<input type="text" class="textinput" placeholder="请输入用户名" />
</p>
<p>密码<br />
<input type="password" class="textinput" placeholder="请输入密码" />
</p>
<p>
<input id="remember" type="checkbox" /><label for="smtxt">记住密码</label>
</p>
<p>
<input type="submit" value="登录" />
</p>
<p class="txt">还没有账户?<a href="#">注册</a></p>
</form>
</div>
</body>
</html>

首先,我们来看HTML代码。这个登录界面包含一个表单,用户需要在表单中输入用户名和密码。我们使用p标签创建输入框,并设置class属性以便后续的CSS样式设置。此外,我们还在表单中添加了一个“记住密码”的复选框和一个登录按钮,同时还有一个注册链接。


表单样式


form{            
opacity: 0.8;
text-align: center;
padding: 0px 100px;
border-radius: 10px;
margin: 120px auto;
}

p {
-webkit-text-stroke: 1px #8e87c3;
}

对表单整体进行样式定义,使其位于水滴内部,p标签内文镂空。


.textinput{
height: 40px;
font-size: 15px;
width: 100px;
padding: 0 35px;
border: none;
background: rgba(250, 249, 249, 0.532);
box-shadow: inset 4px 4px 10px rgba(160, 162, 158, 0.814), 4px 4px 10px rgba(117, 117, 117, 0.3), 15px 15px 30px rgba(72, 70, 70, 0.193), inset -2px -2px 10px rgba(255, 254, 254, 0.873);
border-radius: 50px;
-webkit-text-stroke: 0px;
color: saddlebrown;
outline-style: none;
}

对输入框进行样式定义,取消镂空字体样式,取消轮廓线,设置阴影实现水滴一般效果。


input[type="submit"]{
width: 110px;
height: 40px;
text-align: center;
outline-style: none;
border-style: none;
border-radius: 50px;
background: rgb(31, 209, 218);
-webkit-text-stroke: 0px;
box-shadow: inset 4px 4px 10px rgba(160, 162, 158, 0.814), 4px 4px 10px rgba(117, 117, 117, 0.3), 15px 15px 30px rgba(72, 70, 70, 0.193), inset -2px -2px 10px rgba(255, 254, 254, 0.873);
}

我们使用了input[type="submit"] 选择器来选中提交按钮,并设置了按钮的大小、文本对齐方式、圆角和背景等样式,去除了轮廓线。同样采用了阴影来设置按钮,使其具有气泡一般的感觉,并设置背景色。


input[type="submit"]:hover {
background-color: rgb(31, 218, 78);
}

这段代码是用来为按钮添加鼠标悬停效果的。我们使用了input[type="submit"]:hover选择器来选中鼠标悬停在按钮上时的状态,并设置了背景颜色。当用户悬停在按钮上时,按钮的背景颜色会改变,非常引人注目。


a {
text-decoration: none;
color: rgba(236, 20, 20, 0.433);
-webkit-text-stroke: 1px;
}

a:hover {
text-decoration: underline;
}

提交按钮底部注册文字样式,采用镂空字体样式,鼠标移至该元素上方时,添加下划线。


* {
margin: 0;
padding: 0;
}
body {
background: skyblue;
}

这段代码是对所有元素的外边距和内边距进行清零,以便更好地控制元素的位置和大小,设置了整个页面的背景颜色为天蓝色。


.main {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 400px;
height: 400px;
box-sizing: border-box;
border-radius: 50%;
background: transparent;
box-shadow: inset 15px 10px 40px rgba(158, 158, 158, 0.303), 10px 10px 20px rgba(117, 117, 117, 0.3), 15px 15px 30px rgba(72, 70, 70, 0.193), inset -10px -10px 20px rgba(233, 229, 229, 0.873);
animation: move 6s linear infinite;
}

这段代码采用绝对定位,以便更好地控制它的位置。left: 50%; top: 50%; 将元素的左上角定位在页面的中心位置。通过transform属性将元素向左上角移动50%,以便让元素的中心位置与页面中心位置重合。设置元素的宽度和高度为400像素。background: transparent; 将元素的背景设置为透明色。box-shadow: inset 15px 10px 40px rgba(158, 158, 158, 0.303), 10px 10px 20px rgba(117, 117, 117, 0.3), 15px 15px 30px rgba(72, 70, 70, 0.193), inset -10px -10px 20px rgba(233, 229, 229, 0.873); 设置元素的阴影效果,包括内阴影和外阴影。animation: move 6s linear infinite; 为元素添加动画效果,其中move 是动画名称,6s是动画时长,linear是动画速度曲线,infinite是动画循环次数。


.main::after {
position: absolute;
content: "";
width: 40px;
height: 40px;
background: rgba(254, 254, 254, 0.667);
left: 80px;
top: 80px;
border-radius: 50%;
animation: move2 6s linear infinite;
filter:blur(1px);
}

.main::before {
position: absolute;
content: "";
width: 20px;
height: 20px;
background: rgba(255, 255, 255, 0.5);
left: 130px;
top: 70px;
border-radius: 50%;
animation: move3 6s linear infinite;
filter:blur(1px);
}

这段代码是对两个小球进行样式定义,将伪元素的定位方式设置为绝对定位,以便更好地控制它的位置,设置伪元素的宽度和高度一个为20px,一个为40px。设置伪元素的背景颜色为半透明白色。left,top 设置伪元素的左上角定位在主体元素的中心位置,设置伪元素的边框半径为50%,以便将其设置为圆形。animation: move2 6slinear infinite; 为伪元素添加动画效果,其中 move2 是动画名称,6s 是动画时长,linear 是动画速度曲线,infinite 是动画循环次数,另一个伪元素同理。
接下来是动画定义:


@keyframes move {
50% {
border-radius: 42% 58% 49% 51% / 52% 36% 64% 48% ;
}
75% {
border-radius: 52% 48% 49% 51% / 43% 49% 51% 57% ;
}
25% {
border-radius: 52% 48% 59% 41% / 43% 49% 51% 57% ;
}
}

@keyframes move2 {
25% {
left: 80px;
top: 110px;
}
50% {
left: 50px;
top: 80px;
}
75% {
left: 80px;
top: 120px;
}
}

@keyframes move3 {
25% {
left: 100px;
top: 90px;
}
50% {
left: 110px;
top: 75px;
}
75% {
left: 130px;
top: 100px;
}
}

这段代码定义了三个不同的动画,分别是move、move2和move3。move动画,它控制了元素的边框半径在不同时间点的变化。在这个动画中,元素的边框半径分别在25%、50%和75%的时间点进行了变化。move2move3动画,控制了一个伪元素的位置在不同时间点的变化。在这个动画中,伪元素的位置分别在25%、50%和75%的时间点进行了变化。


结语


以上便是全部代码,喜欢的可以自取,样式不

作者:codePanda
来源:juejin.cn/post/7225623397144199228
好看可以自行更改😜。

收起阅读 »

可视化大屏:vue-autofit 一行搞定自适应

web
可视化大屏适配/自适应现状 可视化大屏的适配是一个老生常谈的话题了,现在其实不乏一些大佬开源的自适应插件、工具但是我为什么还要重复造轮子呢?因为目前市面上适配工具每一个都无法做到完美的效果,做出来的东西都差不多,最终实现效果都逃不出白边的手掌心,可以解决白边问...
继续阅读 »

可视化大屏适配/自适应现状


可视化大屏的适配是一个老生常谈的话题了,现在其实不乏一些大佬开源的自适应插件、工具但是我为什么还要重复造轮子呢?因为目前市面上适配工具每一个都无法做到完美的效果,做出来的东西都差不多,最终实现效果都逃不出白边的手掌心,可以解决白边问题的,要么太过于复杂,要么会影响dom结构。


三大常用方式




  1. vw/vh方案



    1. 概述:按照设计稿的尺寸,将px按比例计算转为vwvh

    2. 优点:可以动态计算图表的宽高,字体等,灵活性较高,当屏幕比例跟 ui 稿不一致时,不会出现两边留白情况

    3. 缺点:每个图表都需要单独做字体、间距、位移的适配,比较麻烦




  2. scale方案



    1. 概述:也是目前效果最好的一个方案

    2. 优点:代码量少,适配简单 、一次处理后不需要在各个图表中再去单独适配.

    3. 缺点:留白,据说有事件热区偏移,但是我目前没有发现有这个问题,即使是地图也没有




  3. rem + vw vh方案



    1. 概述:这名字一听就麻烦,具体方法为获得 rem 的基准值 ,动态的计算html根元素的font-size ,图表中通过 vw vh 动态计算字体、间距、位移等

    2. 优点:布局的自适应代码量少,适配简单

    3. 缺点:留白,有时图表需要单独适配字体




基于此背景,我决定要造一个简单又好用的轮子。


解决留白问题


留白问题是在使用scale时才会出现,而其他方式实现起来又复杂,效果也不算太理想,总会破坏掉原有的结构,可能使元素挤在一起,所以我们还是选择使用scale方案,不过这次要做出一点小小的改变。


常用分辨率


首先来看一下我的拯救者的分辨率:


image-20230420141240837 它可以代表从1920往下的分辨率


我们可以发现,比例分别是:1.77、1.6、1.77、1.6、1.33... 总之,没有特别夸张的宽高比。


计算补齐白边所需的px


只要没有特别夸张的宽高比,就不会出现特别宽或者特别高的白边,那么我们能不能直接将元素宽高补过去?也就是说,当屏幕右侧有白边时,我们就让宽度多出一个白边的px,当屏幕下方有白边时,我们就让高度多出一个白边的px。


很喜欢CSGO玩家的一句话:"啊?"


先想一下,如果此时按宽度比例缩放,会在下方留下白边,所以设置一下它的高度,设置多少呢?比如 scale==0.8 ,也就是说整个#app缩小了0.8倍,我们需要将高扩大多少倍才可以回到原来的大小呢?


QQ录屏20230420144111


emmm.....


算数我最不在行了,启动高材生


image-20230420143742913


原来是八分之十,我vue烧了。


当浏览器窗口比设计稿大或者小的时候,就应该触发缩放,但是比例不一定,如果按照scale等比缩放时,宽度从1920缩小0.8倍也就是1536,而高度缩小0.8也就是743,如果此时浏览器高度过高,那么就会出现下方的白边,根据高材生所说的,缩小0.8后只需要放大八分之十就可以变回原大小,所以以现在的高度743*1.25=928,使宽度=928px就可以完全充满白边!


真的是这样吗?感觉哪里不对劲...


是浏览器高度!我忽略了浏览器高度,我可以直接使用浏览器高度乘以1.25然后再缩放达0.8!就是 1 !


也就是说 clientHeight / scale 就等于我们需要的高度!


我们用代码试一试


function keepFit(designWidth, designHeight, renderDom) {
 let clientHeight = document.documentElement.clientHeight;
 let clientWidth = document.documentElement.clientWidth;
 let scale = 1;
 if (clientWidth / clientHeight < designWidth / designHeight) {
   scale = (clientWidth / designWidth)
   document.querySelector(renderDom).style.height = `${clientHeight / scale}px`;
} else {
   scale = (clientHeight / designHeight)
   document.querySelector(renderDom).style.width = `${clientWidth / scale}px`;
}
 document.querySelector(renderDom).style.transform = `scale(${scale})`;
}

上面的代码可能看起来乱糟糟的,我来解释一下:


参数分别是:设计稿的宽高和你要适配的元素,在vue中可以直接传#app。


下面的if判断的是宽度固定还是高度固定,当屏幕宽高比小于设计宽高比时,


我们把高度写成 clientHeight / scale ,宽度也是同理。


最终效果


将这段代码放到App.vue的mounted运行一下


autofit


如上图所示:我们成功了,我们仅用了1 2 3 4....这么几行代码,就做到了足以媲美复杂写法的自适应!


我把这些东西封装了一个npm包:vue-autofit ,开箱即用,欢迎下载!


亲手打造集成工具:vue-autofit


这是一款可以使你的项目一键自适应的工具 github源码👉go



  • 从npm下载


npm i vue-autofit


  • 引入


import autofit from 'vue-autofit'


  • 快速开始


autofit.init()


默认参数为1920*929(即去掉浏览器头的1080), 直接在大屏启动时调用即可




  • 使用


export default {  
 mounted() {
 autofit.init({
       designHeight: 1080,
       designWidth: 1920,
       renderDom:"#app",
       resize: true
  })
},
}


以上使用的是默认参数,可根据实际情况调整,参数分别为


   * - renderDom(可选):渲染的dom,默认是 "#app",必须使用id选择器 
  * - designWidth(可选):设计稿的宽度,默认是 1920
  * - designHeight(可选):设计稿的高度,默认是 929 ,如果项目以全屏展示,则可以设置为1080
  * - resize(可选):是否监听resize事件,默认是 true


结语


诺克萨斯即将崛起


作者:德莱厄斯
来源:juejin.cn/post/7224015103481118757
收起阅读 »

上手 Vue 新的状态管理 Pinia,一篇文章就够了

web
Vuex 作为一个老牌 Vue 状态管理库,大家都很熟悉了 Pinia 是 Vue.js 团队成员专门为 Vue 开发的一个全新的状态管理库,并且已经被纳入官方 github 为什么有 Vuex 了还要再开发一个 Pinia ? 先来一张图,看下当时对于 Vu...
继续阅读 »

Vuex 作为一个老牌 Vue 状态管理库,大家都很熟悉了


Pinia 是 Vue.js 团队成员专门为 Vue 开发的一个全新的状态管理库,并且已经被纳入官方 github


为什么有 Vuex 了还要再开发一个 Pinia ?


先来一张图,看下当时对于 Vuex5 的提案,就是下一代 Vuex5 应该是什么样子的


微信图片_20220314212501.png


Pinia 就是完整的符合了他当时 Vuex5 提案所提到的功能点,所以可以说 Pinia 就是 Vuex5 也不为过,因为它的作者就是官方的开发人员,并且已经被官方接管了,只是目前 Vuex 和 Pinia 还是两个独立的仓库,以后可能会合并,也可能独立发展,只是官方肯定推荐的是 Pinia


因为在 Vue3 中使用 Vuex 的话需要使用 Vuex4,还只能作为一个过渡的选择,存在很大缺陷,所以在 Componsition API 诞生之后,也就设计了全新的状态管理 Pinia


Pinia 和 Vuex


VuexStateGettesMutations(同步)、Actions(异步)


PiniaStateGettesActions(同步异步都支持)


Vuex 当前最新版是 4.x



  • Vuex4 用于 Vue3

  • Vuex3 用于 Vue2


Pinia 当前最新版是 2.x



  • 即支持 Vue2 也支持 Vue3


就目前而言 Pinia 比 Vuex 好太多了,解决了 Vuex 的很多问题,所以笔者也非常建议直接使用 Pinia,尤其是 TypeScript 的项目


Pinia 核心特性



  • Pinia 没有 Mutations

  • Actions 支持同步和异步

  • 没有模块的嵌套结构

    • Pinia 通过设计提供扁平结构,就是说每个 store 都是互相独立的,谁也不属于谁,也就是扁平化了,更好的代码分割且没有命名空间。当然你也可以通过在一个模块中导入另一个模块来隐式嵌套 store,甚至可以拥有 store 的循环依赖关系



  • 更好的 TypeScript 支持

    • 不需要再创建自定义的复杂包装器来支持 TypeScript 所有内容都类型化,并且 API 的设计方式也尽可能的使用 TS 类型推断



  • 不需要注入、导入函数、调用它们,享受自动补全,让我们开发更加方便

  • 无需手动添加 store,它的模块默认情况下创建就自动注册的

  • Vue2 和 Vue3 都支持

    • 除了初始化安装和SSR配置之外,两者使用上的API都是相同的



  • 支持 Vue DevTools

    • 跟踪 actions, mutations 的时间线

    • 在使用了模块的组件中就可以观察到模块本身

    • 支持 time-travel 更容易调试

    • 在 Vue2 中 Pinia 会使用 Vuex 的所有接口,所以它俩不能一起使用

    • 但是针对 Vue3 的调试工具支持还不够完美,比如还没有 time-travel 功能



  • 模块热更新

    • 无需重新加载页面就可以修改模块

    • 热更新的时候会保持任何现有状态



  • 支持使用插件扩展 Pinia 功能

  • 支持服务端渲染


Pinia 使用


Vue3 + TypeScript 为例


安装


npm install pinia

main.ts 初始化配置


import { createPinia } from 'pinia'
createApp(App).use(createPinia()).mount('#app')

在 store 目录下创建一个 user.ts 为例,我们先定义并导出一个名为 user 的模块


import { defineStore } from 'pinia'
export const userStore = defineStore('user', {
state: () => {
return {
count: 1,
arr: []
}
},
getters: { ... },
actions: { ... }
})

defineStore 接收两个参数


第一个参数就是模块的名称,必须是唯一的,多个模块不能重名,Pinia 会把所有的模块都挂载到根容器上

第二个参数是一个对象,里面的选项和 Vuex 差不多



  • 其中 state 用来存储全局状态,它必须是箭头函数,为了在服务端渲染的时候避免交叉请求导致的数据状态污染所以只能是函数,而必须用箭头函数则为了更好的 TS 类型推导

  • getters 就是用来封装计算属性,它有缓存的功能

  • actions 就是用来封装业务逻辑,修改 state


访问 state


比如我们要在页面中访问 state 里的属性 count


由于 defineStore 会返回一个函数,所以要先调用拿到数据对象,然后就可以在模板中直接使用了


如下这样通过 store.xxx 使用,是具备响应式的


<template>
<div>{{ store.count }}</div>
</template>
<script lang="ts" setup>
import { userStore } from '../store'
const store = userStore()
// 解构
// const { count } = userStore()
</script>


比如像注释中的解构出来使用,也可以用,只是这样拿到的数据不是响应式的,如果要解构还保持响应式就要用到一个方法 storeToRefs(),示例如下


<template>
<div>{{ count }}</div>
</template>
<script lang="ts" setup>
import { storeToRefs } from 'pinia'
import { userStore } from '../store'
const { count } = storeToRefs(userStore())
</script>


原因就是 Pinia 其实是把 state 数据都做了 reactive 处理,和 Vue3 的 reactive 同理,解构出来的也不是响应式,所以需要再做 ref 响应式代理


getters


这个和 Vuex 的 getters 一样,也有缓存功能。如下在页面中多次使用,第一次会调用 getters,数据没有改变的情况下之后会读取缓存


<template>
<div>{{ myCount }}</div>
<div>{{ myCount }}</div>
<div>{{ myCount }}</div>
</template>

注意两种方法的区别,写在注释里了


getters: {
// 方法一,接收一个可选参数 state
myCount(state){
console.log('调用了') // 页面中使用了三次,这里只会执行一次,然后缓存起来了
return state.count + 1
},
// 方法二,不传参数,使用 this
// 但是必须指定函数返回值的类型,否则类型推导不出来
myCount(): number{
return this.count + 1
}
}

更新和 actions


更新 state 里的数据有四种方法,我们先看三种简单的更新,说明都写在注释里了


<template>
<div>{{ user_store.count }}</div>
<button @click="handleClick">按钮</button>
</template>
<script lang="ts" setup>
import { userStore } from '../store'
const user_store = userStore()
const handleClick = () => {
// 方法一
user_store.count++

// 方法二,需要修改多个数据,建议用 $patch 批量更新,传入一个对象
user_store.$patch({
count: user_store.count1++,
// arr: user_store.arr.push(1) // 错误
arr: [ ...user_store.arr, 1 ] // 可以,但是还得把整个数组都拿出来解构,就没必要
})

// 使用 $patch 性能更优,因为多个数据更新只会更新一次视图

// 方法三,还是$patch,传入函数,第一个参数就是 state
user_store.$patch( state => {
state.count++
state.arr.push(1)
})
}
</script>


第四种方法就是当逻辑比较多或者请求的时候,我们就可以封装到示例中 store/user.ts 里的 actions 里


可以传参数,也可以通过 this.xx 可以直接获取到 state 里的数据,需要注意的是不能用箭头函数定义 actions,不然就会绑定外部的 this 了


actions: {
changeState(num: number){ // 不能用箭头函数
this.count += num
}
}

调用


const handleClick = () => {
user_store.changeState(1)
}

支持 VueDevtools


打开开发者工具的 Vue Devtools 就会发现 Pinia,而且可以手动修改数据调试,非常方便


image.png


模拟调用接口


示例:


我们先定义示例接口 api/user.ts


// 接口数据类型
export interface userListType{
id: number
name: string
age: number
}
// 模拟请求接口返回的数据
const userList = [
{ id: 1, name: '张三', age: 18 },
{ id: 2, name: '李四', age: 19 },
]
// 封装模拟异步效果的定时器
async function wait(delay: number){
return new Promise((resolve) => setTimeout(resolve, delay))
}
// 接口
export const getUserList = async () => {
await wait(100) // 延迟100毫秒返回
return userList
}

然后在 store/user.ts 里的 actions 封装调用接口


import { defineStore } from 'pinia'
import { getUserList, userListType } from '../api/user'
export const userStore = defineStore('user', {
state: () => {
return {
// 用户列表
list: [] as userListType // 类型转换成 userListType
}
},
actions: {
async loadUserList(){
const list = await getUserList()
this.list = list
}
}
})

页面中调用 actions 发起请求


<template>
<ul>
<li v-for="item in user_store.list"> ... </li>
</ul>

</template>
<script lang="ts" setup>
import { userStore } from '../store'
const user_store = userStore()
user_store.loadUserList() // 加载所有数据
</script>


跨模块修改数据


在一个模块的 actions 里需要修改另一个模块的 state 数据


示例:比如在 chat 模块里修改 user 模块里某个用户的名称


// chat.ts
import { defineStore } from 'pinia'
import { userStore } from './user'
export const chatStore = defineStore('chat', {
actions: {
someMethod(userItem){
userItem.name = '新的名字'
const user_store = userStore()
user_store.updateUserName(userItem)
}
}
})

user 模块里


// user.ts
import { defineStore } from 'pinia'
export const userStore = defineStore('user', {
state: () => {
return {
list: []
}
},
actions: {
updateUserName(userItem){
const user = this.list.find(item => item.id === userItem.id)
if(user){
user.name = userItem.name
}
}
}
})

结语


如果本文对你有一点点帮助,点个赞支持一下吧,你的每一个【赞】都是我创作的最大动力,感谢支持 ^_^


更多前端文章,或者加入前端交流群,欢迎关注公众号【沐华说技术】,大家一起共同交流和进步呀





往期精彩


【保姆级】Vue3 开发文档


Vue3的8种和Vue2的12种组件通信,值得收藏


作者:沐华
来源:juejin.cn/post/7075491793642455077
收起阅读 »

怎么实现微信扫码登录

web
最近在给企业健康管理系统做一个微信扫码登录的功能,借此机会总结下微信登录这个技术点。 网站应用微信登录是基于 OAuth2.0 协议标准构建的。OAuth 协议规范了五种授权模式,Authorization Code、PKCE、Client CreDentia...
继续阅读 »

最近在给企业健康管理系统做一个微信扫码登录的功能,借此机会总结下微信登录这个技术点。


网站应用微信登录是基于 OAuth2.0 协议标准构建的。OAuth 协议规范了五种授权模式,Authorization Code、PKCE、Client CreDentials、Device Code 和 Refresh Token。微信目前只支持 authorization_code 模式。


微信网站应用接入基础知识


第一步需要先到微信开放平台注册一个开发者账号,并创建一个微信登录网站应用,然后获得AppIDAppSecret


微信的authorization_code模式:



  1. 发起微信授权登录请求



// 请求格式
https://open.weixin.qq.com/connect/qrconnect?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect



  1. 用户扫码授权之后,微信会重定向到回调地址并且给予一个临时票据code;

  2. 后端拿codeAppIDAppSecret通过 API 换取access_token;

  3. 通过access_token进行接口调用,换取用户基本信息

  4. 根据用户信息中的 openId 查询是否已经和系统用户绑定


流程图:


微信官方


关于 state 参数:


state参数: state 参数会在用户授权成功后和code一起携带给 redirect URL。主要用来做 CSRF 防范。



redirect_url?code=CODE&state=STATE

关于 code:
Code 的超时时间为 10 分钟,一个 code 只能成功换取一个 access_token 即失效。


关于展示形式


微信登录有两种展示形式,一种是弹窗打开登录二维码,另一种是将二维码嵌套在自己网页内


我们的设计


交互流程


login_flow.png


前端工作流程:


1、二维码展示,请求/wechat/qrcode 地址获取二维码地址,返回的参数有 state 字段(重要)。


Note:后端生成一个 uuid state,并存储在 Redis 中用来检测用户的扫码状态


2、在当前二维码页面轮询/wechat/qrcode/status/{state} 接口,判断是否已授权、未绑定、已绑定三种状态。


1)未授权,继续轮询

2)已授权已绑定,根据/wechat/qrcode/status/{state} 接口返回的租户、登录凭证,调用登录接口/login/wechat。

3)已授权未绑定,进去用户绑定微信流程

> 用户绑定微信流程
请求/wechat/bind/sms/send 接口,发送用户绑定微信的验证码
请求/wechat/bind 接口,绑定用户。
根据/wechat/bind 接口返回的租户、凭证信息,调用登录接口/login/wechat。

参考文档


网站应用微信登录开发指南


博客原文


iloveu.xyz/2023/04/25/…


作者:贾克森
来源:juejin.cn/post/7225867003720974373
收起阅读 »

微信小程序背景音频开发

web
最近又新开发了一款听书类的小程序,现在一阶段已基本完工。代码已开源,链接在文章结尾。欢迎star。 本期给大家讲解一下关于背景音频开发的一些基本业务场景和踩坑。 1.需求拆解 先来看一张图: 从图中可以看到,基本的业务包含以下几个部分 播放 暂停 切换上一...
继续阅读 »

最近又新开发了一款听书类的小程序,现在一阶段已基本完工。代码已开源,链接在文章结尾。欢迎star。


本期给大家讲解一下关于背景音频开发的一些基本业务场景和踩坑。


1.需求拆解


先来看一张图:


image.png
从图中可以看到,基本的业务包含以下几个部分



  1. 播放

  2. 暂停

  3. 切换上一个音频

  4. 切换下一个音频

  5. 拖动进度条改变音频进度

  6. 音频进度时间

  7. 音频总时间

  8. 在音频列表切换任意的音频


还有一个需求就是在小程序退出以后,还会播放:


image.png
在上图里看到,当背景音频播放的时候,会出现上方这个小图标:


image.png
同样的,在手机的通知栏里面,长这样子:
image.png


接下来我们动手实现一下整个功能。


2. 技术分析


要想实现背景音频,我们需要使用微信小程序提供的一个API:getBackgroundAudioManager(),因为我这里使用的UNI开发的,所以直接贴的是UNI的文档了。具体方法参数可以查看文档。


这里注意以下几个点:



  • ios App平台,背景播放需在manifest.json -> app-plus -> distribute -> ios 节点添加 "UIBackgroundModes":["audio"] 才能保证音乐可以后台播放(打包成ipa生效)

  • 小程序平台,需在manifest.json 对应的小程序节点下,填写"requiredBackgroundModes": ["audio"]。发布小程序时平台会审核。

  • 在page.json中添加"UIBackgroundModes":["audio"]

  • 配置完以后,重新编译一下项目。


3. 功能实现


3.1 播放slider


1.获取音频数据


在进入播放音频页面的时候,默认获取一下第一个需要播放的音频。我这里是是根据音频的id去获取音频的详情信息:


/**
* @description: 获取专辑声音详情信息
* @returns {*}
*/

const getTrackInfo = async (trackId: number) => {
try {
const res = await albumsService.getTrackInfo(trackId)
trackInfo.value = res.data
audios.trackId = res.data?.id as number;
createBgAudioManager()
} catch (error) {
console.log(error)
}
}
onLoad((options) => {
const { trackId } = options
audios.trackId = trackId
getTrackInfo(trackId)
})

getTrackInfo返回的的数据里面长这样的:


image.png
播放音频需要设置红色框标识的字段。


2.创建音频


请求成功,拿到音频详情数据,就需要创建背景音频。


// 初始化背景音频控件
const bgAudioManager = uni.getBackgroundAudioManager();

/**
* @description: 修改音频地址
* @returns {*}
*/

const createBgAudioManager = () => {
// 音频测试地址
// innerAudioContext.src = 'https://bjetxgzv.cdn.bspapp.com/VKCEYUGU-hello-uniapp/2cc220e0-c27a-11ea-9dfb-6da8e309e0d8.mp3';
if (bgAudioManager) {

// 若原先的音频未暂停,则先暂停
if (!bgAudioManager.paused) {
// stop和pause是不一样的,stop直接停止播放,然后从头开始
bgAudioManager.stop();
}
bgAudioManager.title = trackInfo.value?.trackTitle;
bgAudioManager.coverImgUrl = trackInfo.value?.coverUrl
// 设置了src以后会自动播放
bgAudioManager.src = trackInfo.value?.mediaUrl;
// bgAudioManager.autoplay = true;
initAudio(bgAudioManager)
}
}


这里注意一下,title是必须要设置的。不然音频不播放。当设置了src以后,音频会自动进行播放,无需设置autoPlay。



3.获取播放进度和音频总长度


在上面的函数里面,有个initAudio函数。


/**
* @description: 初始化音频相关的方法
* @param {*} ctx
* @returns {*}
*/

const initAudio = (ctx: any) => {
ctx.onTimeUpdate((e) => {
// 当拖动进度条的时候不需要更新进度,使用seek方法
if(!sliders.isDraging) {
// 获取当前进度
const currentTime:number = ctx.currentTime
// 跟新音频进度和slider进度
if (currentTime) {
sliders.progressTime = ctx.currentTime
audios.currentTime = formatTime(currentTime);
}
}
})
ctx.onCanplay(() => {
setTimeout(() => {
console.log('音频长度', bgAudioManager.duration);
// 音频长度,时分秒格式
const duration = bgAudioManager.duration
audios.duration = formatTime(duration);
// 进度条长度=音频长度
sliders.max = duration
}, 300)
})
ctx.onPlay(() => {
audios.playStatus = true
})
ctx.onPause(() => {
audios.playStatus = false
})
ctx.onEnded((e) => {
// 播放结束自动切换到下一首歌
nextAudio()
})
}

onCanplay中,我们可以获取音频的总长度,注意这里的setTimeout必须加,不然获取不到duration。这里需要把秒格式和时分秒的格式都保存下来。


onTimeUpdate中,我们可以获取到当前音频播放的进度。然后实时更新进度条。


接下来就是进度条的实现了。


这里我用的是uni内置组件中的slider组件。


<slider
step="1"
activeColor="#f86442"
block-color="#fff"
block-size="10"
:min="0"
:max="sliders.max"
:value="sliders.progressTime"
@change="sliderChange"
@touchstart="handleSliderMoveStart "
@touchend="handleSliderMoveEnd"
/>


max必须设置,也就是音频的总长度。
value值是当前音频播放的进度。


4.拖动进度条


当拖动进度条的时候,触发sliderChange事件,改变音频的进度。



/**
* @description: 进度条改变事件
* @returns {*}
*/

const sliderChange = (e) => {
console.log(e);
// 拖动slider的值
const position = e.detail.value
seekAudio(position)
}
/**
* 音频跳转
*/

const seekAudio = (position: number) => {
bgAudioManager.seek(position)
// 修改当前进度
audios.currentTime = formatTime(position)
sliders.progressTime = position
}

这里通过seek方法来设置进度的跳转。


当拖动进度条的时候,在onTimeUpdate中也在修改进度。两个之间会打架。所以这里我们在onTimeUpdate中使用isDraging字段来控制。当鼠标按下和抬起的时候来控制isDraging的值,不让onTimeUpdate修改进度。


/**
* @description: 开始拖动进度条事件
* @returns {*}
*/

const handleSliderMoveStart = () => {
sliders.isDraging = true
}
/**
* @description: 结束拖动进度条时间
* @returns {*}
*/

const handleSliderMoveEnd = () => {
sliders.isDraging = false
}

// 此逻辑在前面的代码有了
ctx.onTimeUpdate((e) => {
// 当拖动进度条的时候不需要更新进度,使用seek方法
if(!sliders.isDraging) {
...
}
})

3.2 播放暂停


播放和暂停就非常简单了。


通过playStatus字段来控制播放和暂停按钮的样式切换即可。


其次是事件:


/**
* @description: 暂停音频
* @returns {*}
*/

const pauseAudio = () => {
bgAudioManager.pause() // 停止
}

/**
* @description: 播放音频
* @returns {*}
*/

const playAudio = () => {
bgAudioManager.play() // 播放
}

// 在钩子函数监听

ctx.onPlay(() => {
audios.playStatus = true
})
ctx.onPause(() => {
audios.playStatus = false
})

3.3 音频列表渲染和切换


image.png


这个列表怎么渲染的我就不讲了。这里还有个下拉刷新和上拉加载更多的功能。


当点击某个音频,获取对应的id,然后请求接口获取对应的音频详情。接口和流程跟之前的一样。唯一注意的是,当我们点击的是正在播放的一个音频的话,啥也不要做。还有一个注意的点,当切换音频的时候需要先暂停,然后再设置src和别的属性。



const createBgAudioManager = () => {

if (bgAudioManager) {

// 若原先的音频未暂停,则先暂停
if (!bgAudioManager.paused) {
// stop和pause是不一样的,stop直接停止播放,然后从头开始
bgAudioManager.stop();
}
bgAudioManager.title = trackInfo.value?.trackTitle;
bgAudioManager.coverImgUrl = trackInfo.value?.coverUrl
// 设置了src以后会自动播放
bgAudioManager.src = trackInfo.value?.mediaUrl;
// bgAudioManager.autoplay = true;
initAudio(bgAudioManager)
}
}

注意这里的暂停不是pause,是stop。


3.4 上一个下一个切换


当切换上一个和下一个音频的时候,逻辑也是需要票拿到对应的id,然后请求音频详细数据。


/**
* @description: 切换上一首音频
* @returns {*}
*/

const prevAudio = () => {
// 判断是不是第一首,是则提示
const firstId = audioList.value[0]?.trackId
if (firstId === audios.trackId) {
uni.showToast({
title : "当前已经是第一首了",
icon : "none"
})
return;
}
// 获取上一首的id
// 从播放列表寻找
let id = 0;
audioList.value.forEach((item, index) => {
if (item.trackId === audios.trackId) {
id = audioList.value[index - 1]?.trackId
}
})

getTrackInfo(id)
}
/**
* @description: 切换下一首音频
* @returns {*}
*/

const nextAudio = () => {
// 判断是不是最后一首。是则提示
const len = audioList.value.length
const lastId = audioList.value[len - 1]?.trackId
if (lastId === audios.trackId) {
uni.showToast({
title : "当前播放列表已是最新的了,请加载更多",
icon : "none"
})
return;
}
// 获取下一首的id
// 从播放列表寻找
let id = 0;
audioList.value.forEach((item, index) => {
if (item.trackId === audios.trackId) {
id = audioList.value[index + 1]?.trackId
}
})
getTrackInfo(id)
}

这里只需要注意的是,如果是第一个和最后一个音频,需要做特殊处理。


3.5 播放结束


最后,当某个音频播放结束的时候,直接请求nextAudio函数即可。


ctx.onEnded((e) => {
// 播放结束自动切换到下一首歌
nextAudio()
})

到此为止,我想要的功能基本全部实现了。


4. 更多功能



  • 实时上报播放进度

  • 音频地址防盗

  • 付费,免费体验功能


5. 代码地址


完整代码地址参考:gitee.com/xiumubai/li…


作者:白哥学前端
来源:juejin.cn/post/7226228585371041848
收起阅读 »

从解决一个页面请求太多的问题开始的

web
一、写在前面   上周测试同事给我提了个bug。他说在公司运营系统某个编辑页面中,一个post请求调用太多次了,想让我看看怎么回事。我刚听他讲这个事情时心里有点不屑一顾,觉得能有多少次啊,大惊小怪的。然而当我在测试环境中打开那个页面一看,直呼好家伙!这个页面...
继续阅读 »

一、写在前面




  上周测试同事给我提了个bug。他说在公司运营系统某个编辑页面中,一个post请求调用太多次了,想让我看看怎么回事。我刚听他讲这个事情时心里有点不屑一顾,觉得能有多少次啊,大惊小怪的。然而当我在测试环境中打开那个页面一看,直呼好家伙!这个页面调用了30次相同的请求,属实有点离谱的!


image-20230409202804026.png
  既然情况属实,那么肯定是需要优化一下的。我打开项目代码全局搜索这个请求,发现是在全局公用的一个 Upload 组件的created方法里面调用的。这个请求发送的目的是获取图片上传 oss 系统的签名。因为这个页面一共有30个 Upload 组件,所以整个页面渲染完成后会调用30次接口!!我接着查看接口请求返回的数据,发现签名的有效期是1小时。每次请求的发送又会重新刷新了这个签名和有效时间。但是为什么最先调用接口的 Upload 组件还能上传图片成功,这我还不知道。


  我灵机一动,如果把这个获取签名的方法单纯抽取出来。第一次调用方法后将返回数据缓存下来,后面请求时岂不美哉!但实际操作时发现事情没我想象的那么简单。。。


二、我的解决方案1.0




  一开始我的方案是使用 Vuex 缓存接口返回的签名数据,Upload 组件每次都先从 Vuex 中 state 中查找签名数据 cosConfig,如果没找到再去请求接口。大致的流程如下图:


image-20230410231612623.png


  在捋清楚后逻辑之后,我开始新写 Vuex 的 state 和对应的 mutation了。当我写完代码后一运行,发现这个也能还是依旧调用了30次请求。这让我我很是纳闷啊!!!无奈只好debugger语句开始一行行代码进行调试。
经过一小段时间的调试,问题被我发现了。那就是:签名数据的异步获取。这个签名数据是通过调用后端接口异步返回给前端的。当这个页面存在30个 Upload 组件时,每个组件都会在自己的 created 生命周期函数里先查找了 Vuex 中有没有缓存的签名数据。当页面第一次渲染时,vuex 中肯定是没有签名数据的。所以每个 Upload 组件都会找不到签名数据,然后每个组件都会继续调用接口获取签名数据。等获取到了签名之后,签名配置数据再缓存在 Vuex 中,也就没有意义了。所以方案一失败!!


三、我的解决方案2.0




  我需要承认的是平时困于重复性业务的开发中,很少去处理稍微复杂一点的问题,脑子容易混沌。我在发现方案1.0失败了之后,开始想其他的解决方案。通过 google 的无私帮助下,我找到了这篇文章([vue中多个相同组件重复请求的问题?]),完全就是和我一样的问题嘛。我进去看了第一个赞最多的回答,清晰透彻!主要的解决方案就是运用设计模式中的单例模式,把 Upload 组件中的获取签名的方案单独抽出来。这样子页面上不管有多少个 Upload 组件,调用的获取签名的方法都是同一个。这样子就可以在这个方法里面做文章了。


  那么要做什么文章呢?我们假设这个获取上传图片签名的方法名叫做 getCosConfig,无论多少个 Upload 组件,都是调用同一个 getCosConfig 方法。那么在这个方法外部添加一个缓存对象 cacheConfig,组件每次先从这个缓存对象查找存不存在配置数据。如果存在直接获取缓存对象,如果不存在就调用接口获取。


  但光是这样效果还是和方案1.0结果一样的,同样会调用30次接口。所以我们还需要加一个计数器变量 count。count 的初始值是0,Upload 组件每次发送请求时都会给 count 加1。这样子当我们发现是第一次请求时就去调用接口,不是第一次的话就等待,直到第一次请求结束获得数据。逻辑流程图如下:


image-20230415123202746.png


四、我的解决方案2.1




  到此,本以为这个问题完美解决,但是我突然发现这个接口有入参的!这个页面调用的30个接口中,其中两个剩余的28个参数是不同的。我赶忙去查询了接口文档,发现这个接口是用于获取图片上传的签名,并且不同的业务模块的存储位置是不同的。那么自然返回的上传签名也是不同的,这也意味着原来的 cosConfig 的数据结构是不对的。因为原来的一级对象结构会导致不同业务模块的签名数据混乱了,搞不好弄成了p0级的线上bug。想到这里我心里一凉,感慨还好我细心多瞅了一眼。


  既然问题已经定位到了,那么解决方案2.1自然而然也出来了,只要改造一下 co sConfig 和 count 的结构即可,增加一个key,变成二级的对象。最后我的代码成品如下:


image.png


image.png


五、总结




  最后总结一下,数据结构和设计原则的学习看似虚无缥缈,实际上能够帮助我们解决复杂度很高的问题。通过结合我们日常的开发工作,我们才能感受到这些知识的魅力,也会让我们更加有动力去提高我们的水平。


六、评论区其他方案推荐




 之前写文章都是自娱自乐,没啥人看。这篇文章不知道怎么看的人挺多,评论的朋友也不少。评论区也提出了不少其他方案和业界通用的解决方案,让我见识到了自己知识面的狭窄。我也总结一下供有需要的人使用:


1.【业务维度】在上传图片时再去获取服务端的token,不需要提前去获取。


2.【技术维度】一些请求库自带了去重的功能,例如vue-query。


3.【技术维度】缓存池的概念和处理,这个老哥写的很好【你不知道的promise】设计一个支持并发的前端接口缓存


4.【技术维度】使用异步单例模式,将请求的Promise缓存下来,再次调用函数的时候返回这个Promise。这篇文章讲的不错,给大家推荐一下高级异步模式 - Promise 单例


image.png


作者:徐徐徐叨叨
来源:juejin.cn/post/7222096611635003451
收起阅读 »

简述 js 的代码整洁之道

web
前言 为什么代码要整洁? 代码质量与整洁度成正比。有的团队在赶工期的时候,不注重代码的整洁,代码写的越来越糟糕,项目越来越混乱,生产力也跟着下降,那就必须找更多人来提高生产力,开发成本越来越高。 整洁的代码是怎样的? 清晰表达意图、消除重复、简单抽象、能通过测...
继续阅读 »

前言


为什么代码要整洁?


代码质量与整洁度成正比。有的团队在赶工期的时候,不注重代码的整洁,代码写的越来越糟糕,项目越来越混乱,生产力也跟着下降,那就必须找更多人来提高生产力,开发成本越来越高。


整洁的代码是怎样的?


清晰表达意图、消除重复、简单抽象、能通过测试。
换句话说:具有可读性、可重用性和可重构性。


命名




  1. 名副其实:不使用缩写、不使用让人误解的名称,不要让人推测。


    // bad: 啥?
    const yyyymmdstr = moment().format("YYYY/MM/DD");
    // bad: 缩写
    const cD = moment().format("YYYY/MM/DD");

    // good:
    const currentDate = moment().format("YYYY/MM/DD");

    const locations = ["Austin", "New York", "San Francisco"];

    // bad:推测l是locations的项
    locations.forEach(l => doSomeThing(l));

    // good
    locations.forEach(location => doSomeThing(location));



  2. 使用方便搜索的名称:避免硬编码,对数据用常量const记录。


    // bad: 86400000指的是?
    setTimeout(goToWork, 86400000);

    // good: 86400000是一天的毫秒数
    const MILLISECONDS_PER_DAY = 60 * 60 * 24 * 1000;
    setTimeout(goToWork, MILLISECONDS_PER_DAY);



  3. 类名应该是名词,方法名应该是动词。


    // bad
    function visble() {}

    // good
    function getVisble() {}



  4. 多个变量属于同一类型的属性,那就他们整合成一个对象。同时省略多余的上下文。


    // bad:可以整合
    const carMake = "Honda",
    const carModel = "Accord",
    const carColor = "Blue",

    // bad: 多余上下文
    const Car = {
    carMake: "Honda",
    carModel: "Accord",
    carColor: "Blue",
    };

    // good
    const Car = {
    make: "Honda",
    model: "Accord",
    color: "Blue",
    };



其他:




  • 不要写多余的废话,比如theMessagethe可以删除。




  • 统一术语。比如通知一词,不要一会在叫notice,一会叫announce




  • 用读得通顺的词语。比如getElementById就比 useIdToGetElement好读。




函数(方法)




  • 删除重复的代码,don't repeat yourself。很多地方可以注意dry,比如偷懒复制了某段代码、try...catch或条件语句写了重复的逻辑。


     // bad
    try {
    doSomeThing();
    clearStack();
    } catch (e) {
    handleError(e);
    clearStack();
    }
    // good
    try {
    doSomeThing();
    } catch (e) {
    handleError(e);
    } finally {
    clearStack();
    }



  • 形参不超过三个,对测试函数也方便。多了就使用对象参数。




    • 同时建议使用对象解构语法,有几个好处:



      1. 能清楚看到函数签名有哪些熟悉,

      2. 可以直接重新命名,

      3. 解构自带克隆,防止副作用,

      4. Linter检查到函数未使用的属性。




     // bad
    function createMenu(title, body, buttonText, cancellable) {}

    // good
    function createMenu({ title, body, buttonText, cancellable }) {}



  • 函数只做一件事,代码读起来更清晰,函数就能更好地组合、测试、重构。


     // bad: 处理了输入框的change事件,并创建文件的切片,并保存相关信息到localStorage
    function handleInputChange(e) {
    const file = e.target.files[0];
    // --- 切片 ---
    const chunkList = [];
    let cur = 0;
    while (cur < file.size) {
    chunkList.push({
    chunk: file.slice(cur, cur + size)
    });
    cur += size;
    }
    // --- 保存信息到localstorage ---
    localStorage.setItem("file", file.name);
    localStorage.setItem("chunkListLength", chunkList.length);
    }

    // good: 将三件事分开写,同时自顶而下读,很舒适
    function handleInputChange(e) {
    const file = e.target.files[0];
    const chunkList = createChunk(file);
    saveFileInfoInLocalStorage(file, chunkList);
    }
    function createChunk(file, size = SLICE_SIZE) {
    const chunkList = [];
    let cur = 0;
    while (cur < file.size) {
    chunkList.push({
    chunk: file.slice(cur, cur + size)
    });
    cur += size;
    }
    return chunkList
    }
    function saveFileInfoInLocalStorage(file, chunkList) {
    localStorage.setItem("file", file.name);
    localStorage.setItem("chunkListLength", chunkList.length);
    }



  • 自顶向下地书写函数,人们都是习惯自顶向下读代码,如,为了执行A,需要执行B,为了执行B,需要执行C。如果把A、B、C混在一个函数就很难读了。(看前一个的例子)。




  • 不使用布尔值来作为参数,遇到这种情况时,一定可以拆分函数。


     // bad
    function createFile(name, temp) {
    if (temp) {
    fs.create(`./temp/${name}`);
    } else {
    fs.create(name);
    }
    }

    // good
    function createFile(name) {
    fs.create(name);
    }

    function createTempFile(name) {
    createFile(`./temp/${name}`);
    }



  • 避免副作用。




    • 副作用的缺点:出现不可预期的异常,比如用户对购物车下单后,网络差而不断重试请求,这时如果添加新商品到购物车,就会导致新增的商品也会到下单的请求中。




    • 集中副作用:遇到不可避免的副作用时候,比如读写文件、上报日志,那就在一个地方集中处理副作用,不要在多个函数和类处理副作用。




    • 其它注意的地方:



      • 常见就是陷阱就是对象之间共享了状态,使用了可变的数据类型,比如对象和数组。对于可变的数据类型,使用immutable等库来高效克隆。

      • 避免用可变的全局变量。




    // bad:注意到cart是引用类型!
    const addItemToCart = (cart, item) => {
    cart.push({ item, date: Date.now() });
    };

    // good
    const addItemToCart = (cart, item) => {
    return [...cart, { item, date: Date.now() }];
    };



  • 封装复杂的判断条件,提高可读性。


     // bad
    if (!(obj => obj != null && typeof obj[Symbol.iterator] === 'function')) {
    throw new Error('params is not iterable')
    }

    // good
    const isIterable = obj => obj != null && typeof obj[Symbol.iterator] === 'function';
    if (!isIterable(promises)) {
    throw new Error('params is not iterable')
    }



  • 在方法中有多条件判断时候,为了提高函数的可扩展性,考虑下是不是可以使用能否使用多态性来解决。


     // 地图接口可能来自百度,也可能来自谷歌
    const googleMap = {
    show: function (size) {
    console.log('开始渲染谷歌地图', size));
    }
    };
    const baiduMap = {
    render: function (size) {
    console.log('开始渲染百度地图', size));
    }
    };

    // bad: 出现多个条件分支。如果要加一个腾讯地图,就又要改动renderMap函数。
    function renderMap(type) {
    const size = getSize();
    if (type === 'google') {
    googleMap.show(size);
    } else if (type === 'baidu') {
    baiduMap.render(size);
    }
    };
    renderMap('google')

    // good:实现多态处理。如果要加一个腾讯地图,不需要改动renderMap函数。
    // 细节:函数作为一等对象的语言中,作为参数传递也会返回不同的执行结果,也是“多态性”的体现。
    function renderMap (renderMapFromApi) {
    const size = getSize();
    renderMapFromApi(size);
    }
    renderMap((size) => googleMap.show(size));



其他




  • 如果用了TS,没必要做多余类型判断。




注释




  1. 一般代码要能清晰的表达意图,只有遇到复杂的逻辑时才注释。


     // good:由于函数名已经解释不清楚函数的用途了,所以注释里说明。
    // 在nums数组中找出 和为目标值 target 的两个整数,并返回它们的数组下标。
    const twoSum = function(nums, target) {
    let map = new Map()
    for (let i = 0; i < nums.length; i++) {
    const item = nums[i];
    const index = map.get(target - item)
    if (index !== undefined){
    return [index, i]
    }
    map.set(item, i)
    }
    return []
    };

    // bad:加了一堆废话
    const twoSum = function(nums, target) {
    // 声明map变量
    let map = new Map()
    // 遍历
    for (let i = 0; i < nums.length; i++) {
    const item = nums[i];
    const index = map.get(target - item)
    // 如果下标为空
    if (index !== undefined){
    return [index, i]
    }
    map.set(item, i)
    }
    return []
    };



  2. 警示作用,解释此处不能修改的原因。


    // hack: 由于XXX历史原因,只能调度一下。
    setTimeout(doSomething, 0)



  3. TODO注释,记录下应该做但还没做的工作。另一个好处,提前写好命名,可以帮助后来者统一命名风格。


    class Comment {
    // todo: 删除功能后期实现
    delete() {}
    }



  4. 没用的代码直接删除,不要注释,反正git提交历史记录可以找回。


    // bad: 如下,重写了一遍两数之和的实现方式

    // const twoSum = function(nums, target) {
    // for(let i = 0;i<nums.length;i++){
    // for(let j = i+1;j<nums.length;j++){
    // if (nums[i] + nums[j] === target) {
    // return [i,j]
    // }
    // }
    // }
    // };
    const twoSum = function(nums, target) {
    let map = new Map()
    for (let i = 0; i < nums.length; i++) {
    const item = nums[i];
    const index = map.get(target - item)
    if (index !== undefined){
    return [index, i]
    }
    map.set(item, i)
    }
    return []
    };



  5. 避免循规式注释,不要求每个函数都要求jsdoc,jsdoc一般是用在公共代码上。


    // bad or good?
    /**
    * @param {number[]} nums
    * @param {number} target
    * @return {number[]}
    */

    const twoSum = function(nums, target) {}



对象




  • 多使用getter和setter(getXXX和setXXX)。好处:



    • 在set时方便验证。

    • 可以添加埋点,和错误处理。

    • 可以延时加载对象的属性。


    // good
    function makeBankAccount() {
    let balance = 0;

    function getBalance() {
    return balance;
    }

    function setBalance(amount) {
    balance = amount;
    }

    return {
    getBalance,
    setBalance
    };
    }

    const account = makeBankAccount();
    account.setBalance(100);



  • 使用私有成员。对外隐藏不必要的内容。


    // bad
    const Employee = function(name) {
    this.name = name;
    };

    Employee.prototype.getName = function getName() {
    return this.name;
    };
    const employee = new Employee("John Doe");
    delete employee.name;
    console.log(employee.getName()); // undefined


    // good
    function makeEmployee(name) {
    return {
    getName() {
    return name;
    }
    };
    }




solid




  • 单一职责原则 (SRP) - 保证“每次改动只有一个修改理由”。因为如果一个类中有太多功能并且您修改了其中的一部分,则很难预期改动对其他功能的影响。


    // bad:设置操作和验证权限放在一起了
    class UserSettings {
    constructor(user) {
    this.user = user;
    }

    changeSettings(settings) {
    if (this.verifyCredentials()) {
    // ...
    }
    }

    verifyCredentials() {
    // ...
    }
    }
    // good: 拆出验证权限的类
    class UserAuth {
    constructor(user) {
    this.user = user;
    }

    verifyCredentials() {
    // ...
    }
    }

    class UserSettings {
    constructor(user) {
    this.user = user;
    this.auth = new UserAuth(user);
    }

    changeSettings(settings) {
    if (this.auth.verifyCredentials()) {
    // ...
    }
    }
    }



  • 开闭原则 (OCP) - 对扩展放开,但是对修改关闭。在不更改现有代码的情况下添加新功能。比如一个方法因为有switch的语句,每次出现新增条件时就要修改原来的方法。这时候不如换成多态的特性。


    // bad: 注意到fetch用条件语句了,不利于扩展
    class AjaxAdapter extends Adapter {
    constructor() {
    super();
    this.name = "ajaxAdapter";
    }
    }

    class NodeAdapter extends Adapter {
    constructor() {
    super();
    this.name = "nodeAdapter";
    }
    }

    class HttpRequester {
    constructor(adapter) {
    this.adapter = adapter;
    }

    fetch(url) {
    if (this.adapter.name === "ajaxAdapter") {
    return makeAjaxCall(url).then(response => {
    // transform response and return
    });
    } else if (this.adapter.name === "nodeAdapter") {
    return makeHttpCall(url).then(response => {
    // transform response and return
    });
    }
    }
    }

    function makeAjaxCall(url) {
    // request and return promise
    }

    function makeHttpCall(url) {
    // request and return promise
    }

    // good
    class AjaxAdapter extends Adapter {
    constructor() {
    super();
    this.name = "ajaxAdapter";
    }

    request(url) {
    // request and return promise
    }
    }

    class NodeAdapter extends Adapter {
    constructor() {
    super();
    this.name = "nodeAdapter";
    }

    request(url) {
    // request and return promise
    }
    }

    class HttpRequester {
    constructor(adapter) {
    this.adapter = adapter;
    }

    fetch(url) {
    return this.adapter.request(url).then(response => {
    // transform response and return
    });
    }
    }



  • 里氏替换原则 (LSP)




    • 两个定义



      • 如果S是T的子类,则T的对象可以替换为S的对象,而不会破坏程序。

      • 所有引用其父类对象方法的地方,都可以透明的替换为其子类对象。

      •     也就是,保证任何父类对象出现的地方,用其子类的对象来替换,不会出错。下面的例子是经典的正方形、长方形例子。




    // bad: 用正方形继承了长方形
    class Rectangle {
    constructor() {
    this.width = 0;
    this.height = 0;
    }

    setColor(color) {
    // ...
    }

    render(area) {
    // ...
    }

    setWidth(width) {
    this.width = width;
    }

    setHeight(height) {
    this.height = height;
    }

    getArea() {
    return this.width * this.height;
    }
    }

    class Square extends Rectangle {
    setWidth(width) {
    this.width = width;
    this.height = width;
    }

    setHeight(height) {
    this.width = height;
    this.height = height;
    }
    }

    function renderLargeRectangles(rectangles) {
    rectangles.forEach(rectangle => {
    rectangle.setWidth(4);
    rectangle.setHeight(5);
    const area = rectangle.getArea(); // BAD: 返回了25,其实应该是20
    rectangle.render(area);
    });
    }

    const rectangles = [new Rectangle(), new Rectangle(), new Square()];// 这里替换了
    renderLargeRectangles(rectangles);

    // good: 取消正方形和长方形继承关系,都继承Shape
    class Shape {
    setColor(color) {
    // ...
    }

    render(area) {
    // ...
    }
    }

    class Rectangle extends Shape {
    constructor(width, height) {
    super();
    this.width = width;
    this.height = height;
    }

    getArea() {
    return this.width * this.height;
    }
    }

    class Square extends Shape {
    constructor(length) {
    super();
    this.length = length;
    }

    getArea() {
    return this.length * this.length;
    }
    }

    function renderLargeShapes(shapes) {
    shapes.forEach(shape => {
    const area = shape.getArea();
    shape.render(area);
    });
    }

    const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];
    renderLargeShapes(shapes);



  • 接口隔离原则 (ISP) - 定义是"客户不应被迫使用对其而言无用的方法或功能"。常见的就是让一些参数变成可选的。


     // bad
    class Dog {
    constructor(options) {
    this.options = options;
    }

    run() {
    this.options.run(); // 必须传入 run 方法,不然报错
    }
    }

    const dog = new Dog({}); // Uncaught TypeError: this.options.run is not a function

    dog.run()

    // good
    class Dog {
    constructor(options) {
    this.options = options;
    }

    run() {
    if (this.options.run) {
    this.options.run();
    return;
    }
    console.log('跑步');
    }
    }



  • 依赖倒置原则(DIP) - 程序要依赖于抽象接口(可以理解为入参),不要依赖于具体实现。这样可以减少耦合度。


     // bad
    class OldReporter {
    report(info) {
    // ...
    }
    }

    class Message {
    constructor(options) {
    // ...
    // BAD: 这里依赖了一个实例,那你以后要换一个,就麻烦了
    this.reporter = new OldReporter();
    }

    share() {
    this.reporter.report('start share');
    // ...
    }
    }

    // good
    class Message {
    constructor(options) {
    // reporter 作为选项,可以随意换了
    this.reporter = this.options.reporter;
    }

    share() {
    this.reporter.report('start share');
    // ...
    }
    }
    class NewReporter {
    report(info) {
    // ...
    }
    }
    new Message({ reporter: new NewReporter });



其他




  • 优先使用 ES2015/ES6 类而不是 ES5 普通函数。




  • 多使用方法链。




  • 多使用组合而不是继承。




错误处理




  • 不要忽略捕获的错误。而要充分对错误做出反应,比如console.error()到控制台,提交错误日志,提醒用户等操作。




  • 不要漏了catch promise中的reject。




格式


可以使用eslint工具,这里就不展开说了。


最后


接受第一次愚弄


让程序一开始就做到整洁,并不是一件很容易的事情。不要强迫症一样地反复更改代码,因为工期有限,没那么多时间。等到下次需求更迭,你发现到代码存在的问题时,再改也不迟。


入乡随俗


每个公司、项目的代码风格是不一样的,会有与本文建议不同的地方。如果你接手了一个成熟的项目,建议按照此项目的风格继续写代码(不重构的话)。因为形成统一的代码风格也是一种代码整洁。



参考:



  1. 《代码整洁之道》

  2. github.com/ryanmcdermo…
    (里面有很多例子。有汉化但没更新)


作者:xuwentao
来源:juejin.cn/post/7224382896626778172

收起阅读 »

ES6 Class类,就是构造函数语法糖?

web
一、Class 类可以看作是构造函数的语法糖 ES6引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。constructor()方法,这就是构造方法,而this关键字则代表实例对象。类的所有方法都定义在类的prototype...
继续阅读 »

一、Class 类可以看作是构造函数的语法糖



ES6引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。constructor()方法,这就是构造方法,而this关键字则代表实例对象。类的所有方法都定义在类的prototype属性上面,方法前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。使用的时候,类必须使用new调用跟构造函数的用法完全一致。



  • 类不存在变量提升



    class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}

toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
var p = new Point(1, 2);

通过代码证明:


    class Point {
// ...
}

typeof Point // "function"
Point === Point.prototype.constructor // true

类的数据类型就是函数,类本身就指向构造函数。



constructor: 方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor()方法,如果没有显式定义,一个空的constructor()方法会被默认添加。



class Point {
}

// 等同于
class Point {
constructor() {}
}

取值函数(getter)和存值函数(setter)


        class Person {
constructor(name, age) {
this.name = name
this.age = age
}

get nl() {
return this.age
}

set nl(value) {
this.age = value
}
}
let p = new Person('fzw', 25)
console.log(p.nl);
p.nl = 44
console.log(p.nl);

class表达式


        let person = new class {
constructor(name) {
this.name = name;
}

sayName() {
console.log(this.name);
}
}('张三');

person.sayName(); // "张三"


上面代码中,person是一个立即执行的类的实例。


二、静态方法、静态属性



类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。



         class Foo {
static classMethod() {
this.baz(); // 'hello'
return '我被调用了';
}
static baz() {
console.log('hello');
}
baz() {
console.log('world');
}
}

console.log(Foo.classMethod()); // 我被调用了

var foo = new Foo();
foo.classMethod() // TypeError: foo.classMethod is not a function

注意 如果静态方法包含this关键字,这个this指的是类,而不是实例。静态方法可以与非静态方法重名。


        class Foo {
static classMethod() {
return 'hello';
}
}

class Bar extends Foo {
}

Bar.classMethod() // 'hello'

父类的静态方法,可以被子类继承。父类Foo有一个静态方法,子类Bar可以调用这个方法。
静态方法也是可以从super对象上调用的。


        class Foo {
static classMethod() {
return 'hello';
}
}

class Bar extends Foo {
static classMethod() {
// super在静态方法之中指向父类
return super.classMethod() + ', too';
}
}

console.log(Bar.classMethod());

注意 super 在静态方法之中指向父类。


静态属性



static 关键词修饰,可继承使用



         class MyClass {
static myStaticProp = 42;
constructor() {
console.log(MyClass.myStaticProp); // 42
}
}
class Bar extends MyClass {
}
new MyClass()
console.log(Bar.myStaticProp);

三、私有方法和私有属性



#修饰属性或方法,私有属性和方法只能在类的内部使用。
私有属性也可以设置 getter 和 setter 方法
私有属性和私有方法前面,也可以加上static关键字,表示这是一个静态的私有属性或私有方法。



    class Counter {
#xValue = 0;

constructor() {
console.log(this.#x);
}

get #x() { return this.#xValue; }
set #x(value) {
this.#xValue = value;
}
}

四、class 继承




  • Class 可以通过extends关键字实现继承,让子类继承父类的属性和方法。

  • ES6 规定,子类必须在constructor()方法中调用super(),如果不调用super()方法,子类就得不到自己的this对象。调用super()方法会执行一次父类构造函数。

  • 在子类的构造函数中,只有调用super()之后,才可以使用this关键字,



        class Foo {
constructor() {
console.log(1);
}
}

class Bar extends Foo {
constructor(color) {
// this.color = color; // ReferenceError
super();
this.color = color; // 正确
}
}

const bar = new Bar('blue');
console.log(bar); // Bar {color: 'blue'}

super 关键字



super这个关键字,既可以当作函数使用,也可以当作对象使用。



  • super作为函数调用时,代表父类的构造函数。只能用在子类的构造函数之中,用在其他地方就会报错。

  • super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。



作为对象,普通方法中super指向父类的原型对象


    class A {
constructor() {
this.x = 1;
}
print() {
console.log(this.x);
}
}

class B extends A {
constructor() {
super();
this.x = 2;
}
m() {
super.print();
}
}

let b = new B();
b.m() // 2

注意:
在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前的子类实例。


作为对象,静态方法之中,这时super将指向父类


        class Parent {
static myMethod(msg) {
console.log('static', msg);
}

myMethod(msg) {
console.log('instance', msg);
}
}

class Child extends Parent {
static myMethod(msg) {
// super 代表父类
super.myMethod(msg);
}

myMethod(msg) {
// super 代表父类原型对象
super.myMethod(msg);
}
}

Child.myMethod(1); // static 1

var child = new Child();
child.myMethod(2); // instance 2

extends 关键字


168fb9a3828f9cb4_tplv-t2oaga2asx-zoom-in-crop-mark_4536_0_0_0.awebp
    // 1、构造器原型链
Child.__proto__ === Parent; // true
Parent.__proto__ === Function.prototype; // true
Function.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null; // true
// 2、实例原型链
child.__proto__ === Child.prototype; // true
Child.prototype.__proto__ === Parent.prototype; // true
Parent.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null; // true


extends 继承,主要就是:



  1. 把子类构造函数(Child)的原型(__proto__)指向了父类构造函数(Parent),

  2. 把子类实例child的原型对象(Child.prototype) 的原型(__proto__)指向了父类parent的原型对象(Parent.prototype)。


这两点也就是图中用不同颜色标记的两条线。



子类构造函数Child继承了父类构造函数Parent的里的属性,使用super调用的。




作者:f_人生如戏
来源:juejin.cn/post/7225511164125855781
收起阅读 »

深拷贝的终极实现

web
引子 通过本文可以学习到深拷贝的三种写法的实现思路与性能差异 首先,我们要理解什么是深拷贝,以及为什么要实现深拷贝 深拷贝是什么 通俗来讲,深拷贝就是深层的拷贝一个变量值; 为什么要实现深拷贝 因为在拷贝引用值时,由于复制一个变量只是将其指向要复制变量的引...
继续阅读 »

引子



通过本文可以学习到深拷贝的三种写法的实现思路与性能差异



首先,我们要理解什么是深拷贝,以及为什么要实现深拷贝


深拷贝是什么


通俗来讲,深拷贝就是深层的拷贝一个变量值;


为什么要实现深拷贝


因为在拷贝引用值时,由于复制一个变量只是将其指向要复制变量的引用内存地址,他们并没有完全的断开,而使用就可以实现深拷贝将其完全拷贝为两个单独的存在,指向不同的内存地址;


如何实现深拷贝


一行实现


let deepClone = JSON.parse(JSON.stringify(obj))

这种是最简单的实现方法,虽然这个方法适用于常规,但缺点是无法拷贝 Date()或是RegExp()
 


简单实现


function deepClone(obj) {
// 判断是否是对象
if (typeof obj !== 'object') return obj
// 判断是否是数组 如果是数组就返回一个新数组 否则返回一个新对象
var newObj = obj instanceof Array ? [] : {};
// 遍历obj
for (var key in obj) {
// 将key值拷贝,再层层递进拷贝对象的值
newObj[key] = deepClone(obj[key]);
}
// 返回最终拷贝完的值
return newObj;
}

对于普通的值(如数值、字符串、布尔值)和常见的引用类型(如对象和数组),这个写法完全够用。


但是这个写法有个缺陷,就是无法正确拷贝 Date()  和  RegExp()  等实例对象,因为少了对这些引用类型的特殊处理


普通版


function deepClone(origin, target) {
let tar = target || {};
for (var key in origin) {
if (origin.hasOwnProperty(key)) {
if (typeof origin[key] === 'object' && origin[key] !== null) {
tar[key] = Array.isArray(origin[key]) ? [] : {};
deepClone(origin[key], tar[key]);
} else {
tar[key] = origin[key];
}
}
}
return tar;
}

这个深拷贝方法通过判断属性的值类型,实现了对 对象数组 以及 DateRegExp 等引用类型对象的递归拷贝,同时也考虑了拷贝基本类型值的情况,能够满足大多数场景的要求。


最终版


为什么还有最终版?

上面的案例,可以应对一般场景。


但是对于有两个对象相互拷贝的场景,会导致循环的无限递归,造成死循环!



Uncaught RangeError: Maximum call stack size exceeded



场景:


image.png


如何解决无限递归的问题?

首先我们要了解 WeakMap()
WeakMap的键名所指向的对象,不计入垃圾回收机制;


而通过 WeakMap 记录已经拷贝过的对象,能防止循环引用导致的无限递归;



WeakMap 的设计目的在于,有时我们想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用



代码

利用 WeakMap() 在属性遍历完绑定,并在每次循环时获取当前键名,如果存在则返回数据,不存在则拷贝


function deepClone(origin, hashMap = new WeakMap()) {
// 判断是否是对象
if (origin == undefined || typeof origin !== 'object') return origin;
// 判断是否是Date类型
if (origin instanceof Date) return new Date(origin);
if (origin instanceof RegExp) return new RegExp(origin);

// 判断是否是数组
const hashKey = hashMap.get(origin);
// 如果是数组
if (hashKey) return hashKey;

// 从原型上复制一个值
// *:利用原型构造器获取新的对象 如: [], {}
const target = new origin.constructor();
// 将对象存入map
hashMap.set(origin, target);
// 循环遍历当前层数据
for (let k in origin) {
// 判断当前属性是否为引用类型
if (origin.hasOwnProperty(k)) {
target[k] = deepClone(origin[k], hashMap);
}
}
return target;
}

我们再来看一下使用最新版后的两个对象互相拷贝:


image.png


可以看到,通过使用 WeakMap 记录已经拷贝的对象,有效防止循环引用导致的栈溢出错误,是功能最完备的深拷贝实现。


总结


深拷贝可以完全拷贝一个对象,生成两个独立的且相互不影响的对象。


明白各种深拷贝实现的思路和性能差异,可以在不同场景选用最优的方案。


作者:Shrimpsss
来源:juejin.cn/post/7226181917997547576
收起阅读 »

关于前端实现上传文件这个功能,我只能说so easy!

web
前言 在web前端开发中,文件上传属于很常见的功能,不论是图片、还是文档等等资源,或多或少会有上传的需求。一般都是从添加文件开始,然后读取文件信息,再通过一定的方式将文件上传到服务器上,以供后续展示或下载使用。 下面简单介绍几种上传的方法 简单文件上传 文件上...
继续阅读 »

前言


在web前端开发中,文件上传属于很常见的功能,不论是图片、还是文档等等资源,或多或少会有上传的需求。一般都是从添加文件开始,然后读取文件信息,再通过一定的方式将文件上传到服务器上,以供后续展示或下载使用。


下面简单介绍几种上传的方法


简单文件上传


文件上传的传统形式,是使用表单元素 file


<input type="file" id="file-uploader">

你可以添加 change 事件监听器读取 event.target.files 文件对象。


const fileUploader = document.getElementById('file-uploader')
fileUploader.addEventListener('change', (e) => {
const files = e.target.files
console.log('files', files)
})

多个文件上传


使用 multiple 属性


<input type="file" id="file-uploader" multiple />

文件元数据


在成功上传文件内容后,您可能需要显示该文件内容。对于图片,如果我们在上传后不立即将上传的图片显示给用户,则会感到困惑。


每当上传文件时,File 对象都会包含元数据信息,如文件名称、大小、上次更新时间、类型等。此信息可用于进一步验证和决策。


const fileUploader = document.getElementById('file-uploader')

// 侦听更改事件并读取元数据
fileUploader.addEventListener('change', (e) => {
// 获取文件列表数组
const files = e.target.files

// 循环浏览文件并获取元数据
for (const file of files) {
const name = file.name
const type = file.type ? file.type: 'NA'
const size = file.size
const lastModified = file.lastModified
console.log({ file, name, type, size, lastModified })
}
})

上传前预览图像


我们准备一个上传文件控件,并为预览所选文件准备 img 元素,结构如下:


<input type="file" id="fileInput" />

<img id="preview" />

getElementById() 方法可以获取这两个元素:


const fileEle = document.getElementById('fileInput')
const previewEle = document.getElementById('preview')

使用 URL.createObjectURL() 方法


URL.createObjectURL() 方法包含一个表示参数中给出的对象的 URL。这个新的 URL 对象表示指定的 File对象或 Blob 对象。


fileEle.addEventListener('change', function (e) {
// 获取所选文件
const file = e.target.files[0]

// 创建引用该文件的新 URL
const url = URL.createObjectURL(file)

// 设置预览元素的源
previewEle.src = url
})

使用 FileReader 的 readAsDataURL() 方法



  • 使用 FileReader 对象将文件转换为二进制字符串。然后添加 load 事件侦听器,以获得成功文件上传的二进制字符串。

  • FileReader.readAsDataURL() 方法用于读取指定的 BlobFile对象。


// 获取 FileReader 的实例
const reader = new FileReader()

fileUploader.addEventListener('change', (e) => {
const files = e.target.files
const file = files[0]

// 上传后获取文件对象,以 URL 二进制字符串的形式读取数据
reader.readAsDataURL(file)

// 加载后,对字符串进行处理
reader.addEventListener('load', (e) => {
// 设置预览元素的源
previewEle.src = reader.result
})
})

accept 属性


使用 accept 属性来限制要上传的文件类型。


<input type="file" id="file-uploader" accept=".jpg, .png" multiple>

上面示例中,浏览器将只允许具有 .jpg 和 .png 的文件类型。


验证文件大小


我们读取了文件的大小元数据,可以使用它进行文件大小验证。您可以允许用户上传高达 1MB 的图像文件。


// 文件上载更改事件的侦听器
fileUploader.addEventListener('change', (event) => {
// 读取文件大小
const file = event.target.files[0]
const size = file.size

let msg = ''

// 检查文件大小是否大于 1MB,提示对应消息。
if (size > 1024 * 1024) {
msg = `<span style="color: red;">允许的文件大小为 1MB。您尝试上载的文件属于${returnFileSize(size)}</span>`
} else {
msg = `<span style="color: green;"> ${returnFileSize(size)} 文件已成功上载。 </span>`
}

// 向用户显示消息
feedback.innerHTML = msg
})

显示文件上传进度


更好的可用性是让用户了解文件上传进度。XMLHttpRequest 第二版还定义了一个 progress 事件,可以用来制作进度条。


先在页面中放置一个 progress 标签


<label id="progress-label" for="progress"></label>
<progress id="progress" value="0" max="100" value="0">0</progress>

定义 progress 事件的回调函数


const reader = new FileReader()

reader.addEventListener('progress', (e) => {
if (e.loaded && e.total) {
// 计算完成百分比
const percent = (e.loaded / e.total) * 100
// 将值设置为进度组件
progress.value = percent
}
})

上传目录



有一个非标准属性 webkitdirectory,使我们能够上传整个目录。
虽然最初仅针对基于 WebKit 的浏览器实施,但 WebkitDirectory 在微软 Edge 以及 Firefox 50 及以后也可用。然而,即使它有相对广泛的支持,它仍然不是标准的,不应该使用,除非你别无选择。



<input type="file" id="file-uploader" webkitdirectory />

拖放上传


主要的 JS 如下:


const dropZone = document.getElementById('drop-zone')
const content = document.getElementById('content')

dropZone.addEventListener('dragover', event => {
event.stopPropagation()
event.preventDefault()
event.dataTransfer.dropEffect = 'copy'
})
dropZone.addEventListener('drop', event => {
// 获取文件
const files = event.dataTransfer.files
// ..
})

用对象处理文件


使用 URL.createObjectURL() 方法从文件创建一个唯一的 URL。使用 URL.revokeObjectURL() 方法释放它。



DOM 和 URL.createObjectURL()URL.revokeObjectURL() 方法允许您创建简单的 URL 字符串,可用于引用任何可以使用 DOM 文件对象引用的数据,包括用户计算机上的本地文件。



示例:


<div>
<h1>使用 Object URL</h1>
<input type="file" id="file-uploader" accept=".jpg, .jpeg, .png" >
<div id="image-grid"></div>
</div>

const fileUploader = document.getElementById('file-uploader')
const reader = new FileReader()
const imageGrid = document.getElementById('image-grid')

fileUploader.addEventListener('change', (event) => {
const files = event.target.files
const file = files[0]

const img = document.createElement('img')
imageGrid.appendChild(img)
img.src = URL.createObjectURL(file)
img.alt = file.name
})

总结



  1. 表单元素 file

  2. 文件元数据

  3. 上传前预览图像

  4. URL.createObjectURL() 方法

  5. 使用 FileReader 的 readAsDataURL() 方法

  6. accept
    作者:整天想死的鱼
    来源:juejin.cn/post/7224402365452238906
    属性

收起阅读 »

十个高阶Javascript知识及用法

web
hi,今天给大家整理了十个Javascript的高级知识,希望对你有所帮助 1. 高阶函数 高阶函数是指接受一个或多个函数作为参数,并/或者返回一个函数的函数。这种技巧可以用于将函数组合起来,实现函数的复用。 // 高阶函数示例:将一个数组中的所有元素相加 ...
继续阅读 »

hi,今天给大家整理了十个Javascript的高级知识,希望对你有所帮助




1. 高阶函数


高阶函数是指接受一个或多个函数作为参数,并/或者返回一个函数的函数。这种技巧可以用于将函数组合起来,实现函数的复用。


// 高阶函数示例:将一个数组中的所有元素相加
function add(...args) {
return args.reduce((a, b) => a + b, 0);
}
function addArrayElements(arr, fn) {
return fn(...arr);
}
const arr = [1, 2, 3, 4, 5];
const sum = addArrayElements(arr, add);
console.log(sum); // 15

2. 纯函数


纯函数是指没有副作用(不改变外部状态)并且输出仅由输入决定的函数。纯函数可以更容易地进行单元测试和调试,并且可以更好地支持函数式编程的概念。


// 纯函数示例:将一个数组中的所有元素转换为字符串
function arrToString(arr) {
return arr.map(String);
}
const arr = [1, 2, 3, 4, 5];
const strArr = arrToString(arr);
console.log(strArr); // ["1", "2", "3", "4", "5"]

3. 闭包


闭包是指一个函数可以访问其定义范围之外的变量。这种技巧可以用于将变量“私有化”,从而避免全局变量的滥用。


// 闭包示例:使用闭包实现计数器
function makeCounter() {
let count = 0;
return function() {
count++;
console.log(count);
};
}
const counter = makeCounter();
counter(); // 1
counter(); // 2
counter(); // 3

4. 柯里化


柯里化是指将一个接受多个参数的函数转换为一个接受一个参数的函数序列的技巧。这种技巧可以用于将函数变得更加通用化。


// 柯里化示例:将一个接受多个参数的函数转换为一个接受一个参数的函数序列
function add(a) {
return function(b) {
return a + b;
};
}
const add5 = add(5);
console.log(add5(10)); // 15
console.log(add5(20)); // 25

5. 函数组合


函数组合是指将多个函数组合成一个函数的技巧。这种技巧可以用于将多个函数的输出传递给下一个函数,实现函数的复用。


// 函数组合示例:将多个函数组合成一个函数
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
function compose(...fns) {
return function(x, y) {
return fns.reduce((acc, fn) => fn(acc, y), x);
};
}
const addAndMultiply = compose(add, multiply);
console.log(addAndMultiply(2, 3)); // 15

6. 函数记忆化


函数记忆化是指使用缓存来保存函数的结果,从而避免重复计算。这种技巧可以用于提高函数的性能。


// 函数记忆化示例:使用缓存来保存函数的结果
function memoize(fn) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args);
if (cache[key]) {
return cache[key];
}
const result = fn(...args);
cache[key] = result;
return result;
};
}
function add(a, b) {
console.log("Calculating sum...");
return a + b;
}
const memoizedAdd = memoize(add);
console.log(memoizedAdd(2, 3)); // Calculating sum... 5
console.log(memoizedAdd(2, 3)); // 5 (from cache)

7. 类和继承


类和继承是指使用面向对象编程的概念来组织代码的技巧。这种技巧可以用于使代码更加模块化和可维护。


// 类和继承示例:使用类和继承实现动物类和猫类
class Animal {
constructor(name, age) {
this.name = name;
this.age = age;
}
speak() {
console.log("I am an animal.");
}
}
class Cat extends Animal {
constructor(name, age, color) {
super(name, age);
this.color = color;
}
speak() {
console.log("Meow!");
}
}
const cat = new Cat("Fluffy", 2, "black");

8. Generator


Generator 是一种特殊的函数,它可以暂停和恢复执行,并且可以用来生成迭代器。
示例代码:


function* generate() {
yield 1;
yield 2;
yield 3;
}
const iterator = generate();
console.log(iterator.next()); // 输出 { value: 1, done: false }
console.log(iterator.next()); // 输出 { value: 2, done: false }
console.log(iterator.next()); // 输出 { value: 3, done: false }
console.log(iterator.next()); // 输出 { value: undefined, done: true }

9. Proxy


Proxy 是一种对象代理机制,它可以拦截对象的访问、赋值、删除等操作,并且可以用来实现数据校验、数据缓存等功能。
示例代码:


const user = {
name: 'John',
age: 30,
};
const proxy = new Proxy(user, {
get(target, key) {
console.log(`Getting ${key} value.`);
return target[key];
},
set(target, key, value) {
console.log(`Setting ${key} value to ${value}.`);
target[key] = value;
},
});
console.log(proxy.name); // 输出 "Getting name value." 和 "John"
proxy.age = 40; // 输出 "Setting age value to 40."

10. Reflect


Reflect 是一种对象反射机制,它提供了一系列操作对象的方法,并且可以用来替代一些原来只能通过 Object 上的方法来实现的功能。
示例代码:


const user = {
name: 'John',
age: 30,
};
console.log(Reflect.has(user, 'name')); // 输出 true
console.log(Reflect.get(user, 'name')); // 输出 "John"
console.log(Reflect.set(user, 'age', 40)); // 输出 true
console.log(user.age); // 输出 40

image.png


作者:一条小尾鱼
来源:juejin.cn/post/7222838155605639226
收起阅读 »

打造高性能CSS的九个技巧

web
大佬:你的CSS的写的质量太低,看的我难受。 萌新:那要怎么样? 大佬:自己去优化一下。 萌新:额。。。CSS还能怎么样优化? 咳咳。。。咱们进入正题。 当前端项目想做到极致的性能与体验,优化CSS代码是非常重要的一环。优化CSS代码能够减少页面加载时间,提...
继续阅读 »

大佬:你的CSS的写的质量太低,看的我难受。


萌新:那要怎么样?


大佬:自己去优化一下。


萌新:额。。。CSS还能怎么样优化?


218585ea773deab374b233c6f64dda23.jpeg


咳咳。。。咱们进入正题。


当前端项目想做到极致的性能与体验,优化CSS代码是非常重要的一环。优化CSS代码能够减少页面加载时间,提升性能,改善用户体验。前端的同学有没有想过如何在项目中把CSS这一环优化?


下面我将给大家介绍优化CSS的9个技巧。


1. 使用简洁的选择器


选择器越短,浏览器匹配就越快。因此在编写CSS时,应该尽可能使用简洁的选择器。例如,优先使用类选择器和标签选择器,而不是id选择器和属性选择器。应该避免使用通配符选择器。


/* 优化前的代码 */
#sidebar ul li a:hover {
color: red;
}
/* 优化后的代码 */
.sidebar a:hover {
color: red;
}

2. 避免嵌套过深


嵌套过深会增加选择器的复杂度,影响浏览器性能,同时也使得代码难以维护。为了避免嵌套过深,可以采用命名约定,或者使用后代选择器代替嵌套。


/* 优化前的代码 */
#header .nav ul li a {
color: red;
}
/* 优化后的代码 */
.header-nav-link {
color: red;
}

3. 减少重复的样式


重复的样式会让CSS文件变得臃肿,增加文件大小,影响页面加载速度。如果某些样式被多处引用,可以将其定义为一个class或者使用CSS变量。


/* 优化前的代码 */
#sidebar h3 {
font-size: 1.5rem;
color: #333;
margin-bottom: 1rem;
}
#main h3 {
font-size: 1.5rem;
color: #333;
margin-bottom: 1rem;
}
/* 优化后的代码 */
.heading {
font-size: 1.5rem;
color: #333;
margin-bottom: 1rem;
}

4. 避免使用昂贵的属性


有些CSS属性会影响浏览器性能,例如position、float、display等。应该尽可能避免使用这些属性,或者使用更轻量级的替代方案。


/* 优化前的代码 */
#header {
position: absolute;
top: 0;
left: 0;
}
/* 优化后的代码 */
.header {
position: sticky;
top: 0;
z-index: 999;
}

5. 压缩CSS文件


压缩CSS文件是一种简单而有效的优化方式。压缩CSS文件可以删除注释和空格等无用代码,减少文件大小,加快页面加载速度。可以使用在线压缩工具或者构建工具自动压缩CSS文件。


例如:


/* 压缩前的代码 */
.header {
position: sticky;
top: 0;
z-index: 999;
}
/* 压缩后的代码 */
.header{position:sticky;top:0;z-index:999;}

6. 单独使用!important


!important能够优先级最高控制CSS属性值,但这种方法很容易过分使用,在大的CSS文件中成为代码的混乱来源。尝试尽可能避免使用它们,只在必要的情况下使用。


7. 避免使用通配符选择器


通配符选择器(*)会匹配所有元素,这样的选择器不仅速度慢,而且可能会导致CSS规则被某些你不想匹配的元素使用。因此,尽量避免使用通配符选择器。如果必须使用,也应该在选择器中增加额外的限制条件来提高精确度。


8.使用CSS继承


CSS继承能够将子元素的样式设置为与其父元素相同的属性值。这种方法不仅简单,而且可以减少代码量和增强代码的可读性。使用继承可以减少你的样式表中的重复代码。


9. 使用CSS预处理器


CSS预处理器(如Sass、Less、Stylus)能够让你使用变量、嵌套、函数、注释等高级功能,从而更加简洁、易于维护的方式编写CSS代码。预处理器将会自动将这些代码转换为标准CSS语法,这样能够降低代码量和复杂度,提高开发效率。


作者:白椰子
来源:juejin.cn/post/7223598443666964517
收起阅读 »

⏰⏰ 手把手实现一个进度条时钟,麻麻再也不用担心我把时间看茬了!

web
前言 挂钟大家都知道吧,它通过时针、分针和秒针来表示时间,想当初小学刚开始教怎么看时钟的完全看不懂。今天带大家一步步实现一个类进度条时钟的效果,更直观的知晓当前的时间。 本文将会带大家学到以下知识点: 垂直水平居中方式 gap 属性搭配 flex 布局 实现...
继续阅读 »

前言


挂钟大家都知道吧,它通过时针、分针和秒针来表示时间,想当初小学刚开始教怎么看时钟的完全看不懂。今天带大家一步步实现一个类进度条时钟的效果,更直观的知晓当前的时间。


本文将会带大家学到以下知识点:



  1. 垂直水平居中方式

  2. gap 属性搭配 flex 布局 实现等边距

  3. Date 日期函数的使用及注意点

  4. CSS 变量的简单应用

  5. svgcircle 标签的用法

  6. stroke-dashoffset 属性和 stroke-dasharray 属性能够的用法


样式重置


首先老规矩,我们将 CSS 样式重置 ,方便各个浏览器统一展示效果。


* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

背景调整


接下来通过添加 min-height: 100vh 属性,将 body 限制为 视口大小 且通过 overflow: hidden 来将 超出部分隐藏


body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background-color: #2f363e;
}

这里为了将我们的时钟在屏幕中间展示,我们需要使用 flex 布局body 设置为 水平垂直居中 。同样的,小伙伴们还可以使用 light-heighttransform 等手段实现。


时间绘制


接下来我们要准备 4 个 div ,用来作为展示时间的容器。


<body>
<div id="time">
<div class="circle">
<div id="hours">00</div>
</div>
<div class="circle">
<div id="minutes">00</div>
</div>
<div class="circle">
<div id="seconds">00</div>
</div>
<div class="ap">
<div id="ampm">00</div>
</div>
</div>

</body>

然后给其宽高。


#time {
display: flex;
color: #fff;
}
#time .circle {
position: relative;
width: 150px;
height: 150px;
}

细心的小伙伴一定注意到了这段 CSS 中有个比较特别的属性:gap。这有什么用呢?


我们来看看 MDN 对 gap 属性的描述:



CSS gap 属性是用来设置网格行与列之间的间隙(gutters),该属性是 row-gap 和 column-gap 的简写形式。



gap 属性它适用于 multi-column elements, flex containers, grid containers,也就是多列布局、弹性布局以及网格布局中(知识点++)。因此这里我们用 flex 布局 搭配 gap 是完全行得通的。


我们看看此时的效果如何:


微信截图_20221011180851.png


我们发现最后一个用来表示上午下午的字体有点大,我们将其调小,同时通过 translateY 属性将该元素偏移 -20px(负数表示向上偏移,正数表示向下偏移),和时间做区分。


#time .ap {
position: relative;
font-size: 1em;
transform: translateY(-20px);
}

微信截图_20221011182321.png


这样美观了许多,主次分明。


动态时间


接下来我们要让时间变成实时的,因此我们要用到 Javascript 的 Date 函数了。



  1. 通过 getHours() 获取当前小时数。

  2. 通过 getMinutes() 获取当前分钟数。

  3. 通过 getSeconds() 获取当前秒钟数。


这里有个注意点,通过以上三个方法获取的时间值,都是不带前缀 0 的,并且是 number 类型。什么意思呢?


比如现在是下午的 14:07:04 ,通过 getMinutes() 获取的分钟数是 7 而不是 07,同理,通过 getSeconds()getHours() 获取的时间也是如此。


因此为了美观,我们需要手动给 10 以内 的时间值 补一个 0。具体怎么做呢?这里我用到了字符类型的 padStart 方法,它传递两个参数,分别是数字最后要填充到这个指定的位数,以及用来填充的字符。


为了将 7 变成 07,我们需要将 number 类型的 7 变为字符串类型的 '7',然后执行 '7'.padStart(2, 0)。


除此之外,对于 AM 以及 PM 的区分,我们通过判断 getHours() 的返回值是否大等于 12。如果大于则为 PM,否则是 AM。


处理完数据之后通过修改 innerHTML 的值来改变页面上展现的时间,同时通过 setInterval 来不断执行该操作,实现实时更新时间的效果。


<script>
let hours = document.querySelector('#hours');
let minutes = document.querySelector('#minutes');
let seconds = document.querySelector('#seconds');
let ampm = document.querySelector('#ampm');

setInterval(() => {
let h = `${new Date().getHours() % 12}`.padStart(2, 0);
let m = `${new Date().getMinutes()}`.padStart(2, 0);
let s = `${new Date().getSeconds()}`.padStart(2, 0);
let am = h >= 12 ? 'PM' : 'AM';

hours.innerHTML = h + '\n<div class="tip">HOURS</div>';
minutes.innerHTML = m + '\n<div class="tip">MINUTES</div>';
seconds.innerHTML = s + '\n<div class="tip">SECONDS</div>';
ampm.innerHTML = am;
}, 1000);
</script>

注意点小结:



  1. getHours、getMinutes 以及 getSeconds 返回 number 类型的值(不带前缀 0)

  2. padStart 是字符类型的方法,注意先将类型转为 string 再进行调用。


我们来看看效果:


20221011_190750.gif


画圆


接下来我们要对每个时间的容器都画一个圆的效果,这里我使用了 svg 标签。原因下文会提。


有的小伙伴可能对 svg 标签比较陌生,确实平时开发的时候用的比较少,实际上它和我们普通的标签差不多,而且也能用过 CSS 设置它的一些属性。


这里画圆我们需要用到 circle 标签,其中,cxcy 属性共同确定了一个圆心的位置,r 属性表示待绘制圆的半径。


<div id="time">
<div class="circle" style="--clr: #ff2972">
<svg>
<circle cx="70" cy="70" r="70"></circle>
</svg>
<div id="hours">00</div>
</div>

<div class="circle" style="--clr: #fee800">
<svg>
<circle cx="70" cy="70" r="70"></circle>
</svg>
<div id="minutes">00</div>
</div>

<div class="circle" style="--clr: #04fc43">
<svg>
<circle cx="70" cy="70" r="70"></circle>
</svg>
<div id="seconds">00</div>
</div>

</div>

微信截图_20221011192308.png


可以看到默认情况下,circle 标签的背景色是黑色的,我们给它点样式。


#time .circle svg circle {
width: 100%;
height: 100%;
fill: transparent;
stroke: #191919;
stroke-width: 4;
}

fill 属性表示当前填充 circle 标签应当用什么颜色(实际上就是背景色的意思)。


stroke 属性表示绘制一个边线(实际上就是边框)。


stroke-width 属性一般搭配 stroke 属性一起用,表示边线的宽度。


微信截图_20221011192621.png


经过这么一手修改后已经有模有样了,但是别急,最麻烦的部分要来了。接下来我们要模拟时间进度条了。


模拟进度条


这里我们模拟进度条需要在每个 svg 标签下再添加一个 circle 标签。


<div id="time">
<div class="circle" style="--clr: #ff2972">
<svg>
<circle cx="70" cy="70" r="70"></circle>
<circle cx="70" cy="70" r="70" id="hh"></circle>
</svg>
<div id="hours">00</div>
</div>

<div class="circle" style="--clr: #fee800">
<svg>
<circle cx="70" cy="70" r="70"></circle>
<circle cx="70" cy="70" r="70" id="mm"></circle>
</svg>
<div id="minutes">00</div>
</div>

<div class="circle" style="--clr: #04fc43">
<svg>
<circle cx="70" cy="70" r="70"></circle>
<circle cx="70" cy="70" r="70" id="ss"></circle>
</svg>
<div id="seconds">00</div>
</div>

</div>

同时,通过 CSS 变量给这个新增的 circle 标签各不相同的颜色。


#time .circle svg circle:nth-child(2) {
stroke: var(--clr);
}

此时效果如下:


微信截图_20221011193127.png


接下来我们要用到一个新的属性:stroke-dasharray


我们来看看 MDN 对 stroke-dasharray 属性的介绍:



属性 stroke-dasharray 可控制用来描边的点划线的图案范式。



是不是有点懵,我们看看下面这张动图:


20221011_193806.gif


这是 stroke-dasharray 属性值从 0 开始不断增加的效果。那我们就知道了,实际上这个属性就是控制点划线的长度用的。


那它和我们的进度条有什么关系呢?


不要急,要实现我们的进度条,还需要另一个属性:stroke-dashoffset


我们来看看 MDN 对 stroke-dashoffset 属性的介绍:



属性 stroke-dashoffset 指定了 dash 模式到路径开始的距离。



话不多说,上图:


20221011_194553.gif


这是 stroke-dashoffset 属性值从 0 开始不断增加的效果,是不是很像进度条在跑?


我们就是通过 Javascript 动态修改 stroke-dashoffset 来达到进度条跟着时间一起跑的效果!


let hh = document.querySelector('#hh');
let mm = document.querySelector('#mm');
let ss = document.querySelector('#ss');

setInterval(() => {
...
hh.style.strokeDashoffset = 440 - (440 * h) / 12;
mm.style.strokeDashoffset = 440 - (440 * m) / 60;
ss.style.strokeDashoffset = 440 - (440 * s) / 60;
}, 1000);

20221011_195356.gif


码上掘金


SumXiMRX - 码上掘金 (juejin.cn)


Github 源码地址


juejin-demo/digital-clock-demo at main · catwatermelon/juejin-demo (github.com)


结束语


本文就到此结束了,希望大家共同努力,早日拿下 CSS 💪💪。


如果文中有不对的地方,或是大家有不同的见解,欢迎指出 🙏🙏。


如果大家觉得所有收获,欢迎一键三连💕💕。


作者:CatWatermelon
来源:juejin.cn/post/7153836297218424863
收起阅读 »

原来Promise 还可以这样用?

web
举个例子 需求 组件b初始化某个用到的库,只有在初始化完成后才能调用其API,不然会报错。a页面负责调用 上代码 // a.vue <template> <div> 这是a页面 <childB ref="chi...
继续阅读 »

举个例子


需求


组件b初始化某个用到的库,只有在初始化完成后才能调用其API,不然会报错。a页面负责调用


上代码


// a.vue
<template>
<div>
这是a页面
<childB ref="childB" />
</div>

</template>
<script>
import childB from './b'
export default {
mounted() {
setTimeout(() => {
this.$refs.childB.play()
}, 3000)
},
components: {
childB,
},
}
</script>

// b.vue
<template>
<div>这是b页面</div>
</template>

<script>
export default {
data() {
return {
flag: false,
}
},
created() {
this.init()
},
methods: {
init() {
setTimeout(() => {
this.flag = true
}, 2000)
},
play() {
if (!this.flag) return console.log('not init')
console.log('ok')
},
},
}
</script>



以上代码为模拟初始化,用setTimeout代替,实际开发中使用是一个回调函数,那么我页面a也是用setTimeout?写个5秒?10秒?有没有解决方案呢?


解决方案


那肯定是有的,我们可以这样写……


// a.vue
<template>
<div>
这是a页面
<childB ref="childB" />
</div>

</template>
<script>
import childB from './b'
export default {
mounted() {
this.init()
},
methods: {
init() {
setTimeout(() => {
this.$refs.childB.play()
}, 2000)
},
},
components: {
childB,
},
}
</script>

// b.vue
<template>
<div>这是b页面</div>
</template>

<script>
export default {
methods: {
play() {
console.log('ok')
},
},
}
</script>


相信这也是最常见也是大多数人使用的方案了,但是我觉得把b组件中的代码写到了a页面中,假如有多个b组件,那么a页面中要写多好的b组件代码。容易造成代码混淆、冗余,发生异常的错误,阻塞进程,这显然是不能接受的。


思考


我们能不能用promise来告诉我们是否已经完成初始呢?


答案当然是可以的!


下面我们改造一下代码


// a.vue
<template>
<div>
这是a页面
<childB ref="childB" />
</div>

</template>
<script>
import childB from './b'
export default {
mounted() {
const { init, play } = this.$refs.childB
init().then(play)
},
components: {
childB,
},
}
</script>

// b.vue
<template>
<div>这是b页面</div>
</template>

<script>
export default {
methods: {
init() {
return new Promise(resolve => {
setTimeout(() => {
resolve()
}, 2000)
})
},
play() {
console.log('ok')
},
},
}
</script>


嗯~ o( ̄▽ ̄)o 果然nice,干净整洁,一气呵成!


不足


init在a页面mounted时候才触发,感觉太晚了。能不能在b组件created时候自行触发呢?


哈哈,当然可以了!


我们再改造一下代码


// a.vue
<template>
<div>
这是a页面
<childB ref="childB" />
</div>

</template>
<script>
import childB from './b'

export default {
mounted() {
this.$refs.childB.play()
},
components: {
childB,
},
}
</script>

// b.vue
<template>
<div>这是b页面</div>
</template>

<script>
function getPromiseWait() {
let success, fail
const promise = new Promise((resolve, reject) => {
success = resolve
fail = reject
})
return { promise, resolve: success, reject: fail }
}
const { promise, resolve } = getPromiseWait()
export default {
created() {
this.init()
},
methods: {
init() {
setTimeout(() => {
resolve('hello')
}, 2000)
},
async play() {
const res = await promise
console.log('ok', res)
},
},
}
</script>



完美


我们在b组件中生成一个promise来控制是否init完成,a页面只需要直接调用b组件的play方法即可。如有需要还可以在resolve传递参数,通过then回调函授拿到数据,Pro

作者:𝓼𝓹𝓻𝓲𝓽𝓮𝓐𝓹𝓮
来源:juejin.cn/post/7225127360445841466
mise YYDS!

收起阅读 »

WEB前端奇淫巧计-消除异步的传染性

web
简介 大家好今天给大家介绍一个关于异步的比较恶心的东西也许大家在开发中也曾遇到过只不过解决起来比较棘手废话不多说直接上代码 async function getUser() { return await fetch('https://my-json-ser...
继续阅读 »


简介


大家好
今天给大家介绍一个关于异步的比较恶心的东西
也许大家在开发中也曾遇到过
只不过解决起来比较棘手
废话不多说直接上代码


async function getUser() {
return await fetch('https://my-json-server.typicode.com/typicode/demo/profile').then((resp) => resp.json())
}

async function m1(){
//other works
return await getUser()
}

async function m2(){
//other works
return await m1()
}

async function m3(){
//other works
return await m2()
}

async function main() {
const res = await m3()
console.log('res', res)
}
main()

经过观察上述代码有没有发现
一旦一个函数使用 async await
其他函数调用这个函数进行异步操作时,也要加上async await
突然有没有觉得有那么一丝丝小恶心
我们今天的目的就是把以上的async await去掉也能达到同样的效果


function getUser() {
return fetch('https://my-json-server.typicode.com/typicode/demo/profile')
}

function m1() {
//other works
return getUser()
}

function m2() {
//other works
return m1()
}

function m3() {
//other works
return m2()
}

function main() {
const res = m3()
console.log('res', res)
}
main()

就像以上代码调用,也能实现同样的效果
是不是一下子有点懵懵的
这其实是一个大厂的内部晋升题,还是有点小难度的
这个问题在一些框架的底层也会常遇到
我来带你逐步探讨


解决问题


不难发现通过以上直接去掉async await是无法得到原来的结果的
因为它会返回一个promise 对象,无法使res得到真实的数据
这里我先说一下大概思路
首先fetch会返回一个promise,但是在请求时就想对结果进行操作,显然是不可能的
这时候我们需要在fetch没返回我们想要的数据前先终止函数运行,等拿到正确的数据后我们再运行函数
是不是听到这个过程也是一头雾水呀
先别着急
继续往下看
如果想要函数终止运行有个办法那就是抛出异常 throw error
然后等fetch返回数据data后,对数据进行缓存
缓存后开始函数的运行,
最后交付data
看一下流程图
image.png
整体流程大概就是这样
为了方便理解,我化简一下上述代码


function main() {
const res = fetch('https://my-json-server.typicode.com/typicode/demo/profile')
console.log('res', res)//res要得到一个data数据而不是一个promise对象
}
main()

我们都知道fetch实际返回一个promise对象
此时返回的是一个promise
image.png
在不改变main函数体的情况下使得res是我们想要的数据而不是promise
下面是我们想要的数据
image.png
那我们就得想办法更改main的调用方式


function main() {
const res = fetch('https://my-json-server.typicode.com/typicode/demo/profile')
console.log('res', res)//res要得到一个data数据而不是一个promise对象
}
function run(func){
//瓜瓜一顿操作,使得fetch返回真实的数据而不是promise
}
run(main)

根据上述讲的流程,我们来看一下run函数的具体过程
注释我已经写的很详细了
大家认真看哦


function run(func) {
let cache = []//缓存的列表,由于可能不止一个fetch,所以要用一个list
let i = 0;//缓存列表的下标
const _originalFetch = window.fetch//储存原先的fetch
window.fetch = (...args) => {//重写fetch函数,这个fetch要么抛出异常,要么返回真实的数据
if (cache[i]) {//判断一下缓存是否存在,如果存在就返回真实的数据或抛出异常
if (cache[i].status === 'fulfilled') {
return cache[i].data
} else if (cache[i].status === 'rejected') {
throw cache[i].err
}
}
const result = {
status: 'pending',
data: null,
err: null
}
cache[i++] = result//添加缓存
//发送请求
//真实的fetch调用
const prom = _originalFetch(...args).then(resp => resp.json()).then(resp => {
//等待返回结果,然后修改缓存
result.status = 'fulfilled'
result.data = resp
}, err => {
result.status = 'rejected'
result.data = err
})
//如果没有缓存,就添加缓存和抛出异常
throw prom
//这里为什么会抛出真实fetch返回的promise,主要是因为外面会用到这个promise然后等待拿到最终结果
}
try {
//在try里调用func也就是上述的main函数
//由于main里面有fetch,且第一次没有缓存,所以会抛出一个异常
func()

} catch (err) {
//从这里捕获到异常
//这里的err就是上述fetch返回的promise

if (err instanceof Promise) {//验证一下是不是promise
const reRun = () => {
i = 0//重置一下下标
func()
}
err.then(reRun, reRun)//待promise返回结果后重新执行func,也就是重新执行main
//这次执行已经有缓存了,并且返回中有了正确的结果,所以重写的fetch会返回真实的数据
}
}
}

通过这么一个函数调用main,就可以使得在不改变main函数体的情况下使得fetch返回真实的数据而不是promise对象
是不是感到很神奇
我们来看下完整代码


function getUser() {
return fetch('https://my-json-server.typicode.com/typicode/demo/profile')
}

function m1() {
//other works
return getUser()
}

function m2() {
//other works
return m1()
}

function m3() {
//other works
return m2()
}

function main() {
const res = m3()
console.log('res', res)
}

function run(func) {
let cache = []//缓存的列表
let i = 0;//缓存下标
const _originalFetch = window.fetch//储存原先的fetch
window.fetch = (...args) => {//重写fetch函数
if (cache[i]) {
if (cache[i].status === 'fulfilled') {
return cache[i].data
} else if (cache[i].status === 'rejected') {
throw cache[i].err
}
}
const result = {
status: 'pending',
data: null,
err: null
}
cache[i++] = result
//发送请求
const prom = _originalFetch(...args).then(resp => resp.json()).then(resp => {
result.status = 'fulfilled'
result.data = resp
}, err => {
result.status = 'rejected'
result.data = err
})
throw prom
}
try {
func()
} catch (err) {
//什么时候引发重新执行function
if (err instanceof Promise) {
const reRun = () => {
i = 0
func()
}
err.then(reRun, reRun)
}
}
}
run(main)

此时执行的结果,就是我们想要的结果
image.png
没错就是这样,nice



在框架中的应用


其实在react这个应用很常见
我们先来看一段代码


const userResource = getUserResource()
function ProfilePage() {
return (
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails />
</Suspense>

)
}
function ProfileDetails(){
const user = userResource.read();
return <h1>{user.name}</h1>
}
ReactDOM.render(<ProfilePage/>, document.getElementById("root"));

别急别急我来稍微翻译下
这段代码的意思是在ProfileDetails没加载到数据前先显示Loading profile...
待ProfileDetails加载到数据就渲染 {user.name}
他是怎么实现的呢
如果放在vue里面ProfileDetails必须为一个异步函数
而在这里的实现方案与我上述讲述的类似
我们来验证一下
在ProfileDetails打印1


function ProfileDetails(){
console.log(1)//在这里输出一个1
const user = userResource.read();
return <h1>{user.name}</h1>
}

输出结果是这样的
image.png
为什么会输出两个1呢
原因就和我们上述代码类似
在userResource.read()第一次执行它会抛出一个错误
第二次是已经拿到数据
所以它执行了两遍,最终拿到了数据
我们在函数里手动抛出一个promise


function ProfileDetails(){
throw new Promise((resolve)=>{})//我们在这里抛出一个promise,且函数体里没有执行resolve()
const user = userResource.read();
return <h1>{user.name}</h1>
}

你会发现页面一直展示Loading profile...
image.png
因为我们抛出的promise,一直没有resolve,也就是等待不了结果返回,所以它只会渲染Loading profile...保持不变
肿么样,神奇吧,你学费了嘛
有兴趣的可以一起学习交流,有什么问题也可以联系我
小编微信:buouyupro


作者:布偶鱼
来源:juejin.cn/post/7223937161707716669
收起阅读 »

前端枚举最佳规范——优雅可能也会过时

web
痛点很久很久以前,我在ts项目中使用枚举是这样的export enum GENDER {      MALE = 1,      FEMALE = 2,  }export const GEN...
继续阅读 »

痛点

很久很久以前,我在ts项目中使用枚举是这样的

export enum GENDER {  
   MALE = 1,  
   FEMALE = 2,  
}

export const GENDER_MAP: Record<GENDER, string> = {  
  [GENDER.MALE]: '男',  
  [GENDER.FEMALE]: '女',  
}

// 可能还会写一个给select组件使用的options
export const GENDER_OPTIONS = [  
  {  
       label: '男',  
       value: GENDER.MALE  
  },  
  {  
       label: '女',  
       value: GENDER.FEMALE,  
  }  
];

封装

淦!好麻烦。封装一下,暂时只封装了一个js版本的,想做成ts版本的请自行更改

import { invert, isArray } from 'lodash';

class Enum {
 constructor(enumsName) {
   this.enumsName = enumsName;
   this.enums = {};
}
 // 设置枚举项
 setItem(desc, value) {
   this.enums[desc] = value;
   return this;
}
 findEnumItem(value) {
   return Object.keys(this.enums).find(
    (desc) => this.getValueFromDesc(desc) === value,
  );
}
 // 根据值获取描述
 getValueFromDesc(desc) {
   return this.enums[desc];
}
 getDescriptionFromValue(value, separator = ',') {
   if (isArray(value)) {
     return value.map((item) => this.findEnumItem(item)).join(separator);
  } else {
     return this.findEnumItem(value);
  }
}
 // 获取枚举第一项的值
 getFirstValue() {
   const enums = this.getEnums();
   return enums[Object.keys(enums)[0]];
}
 // 自定义转换枚举数组格式
 transformEnums(formatTarget) {
   // formatTarget 数组第一项是描述属性名称,第二项是枚举值属性名称
   if (isArray(formatTarget) && formatTarget.length === 2) {
     const [keyName, valueName] = formatTarget;
     return Object.entries(this.enums).map((item) => {
       const [desc, value] = item;
       return {
        [keyName]: desc,
        [valueName]: value,
      };
    });
  } else {
     return [];
  }
}
 // 获取 options
 getOptions() {
   return Object.entries(this.enums).map((item) => {
     const [desc, value] = item;
     return {
       label: desc,
       value,
    };
  });
}
 // 获取描述和值反转对象,输出 {value: desc}
 getInvertEnums() {
   return invert(this.enums);
}
 is(enumsName) {
   return this.enumsName === enumsName;
}
}
export default Enum;

export const getEnums = (enumsName, enums) =>
 enums.find((enumItem) => enumItem.is(enumsName));

使用

ok,咋使用呢?首先当然要创建枚举啦

// commonEnum.js
import Enum from '@/utils/enum';

// 我习惯把同一个模块或功能中的枚举全都塞到一个数组中
export const COMMON_ENUMS = [
   new Enum('性别').setItem('男', 1).setItem('女', 2),
   ...
];

在使用到的地方,先获取你想要的枚举

import {getEnums} from '@/utils/enum';
import {COMMON_ENUMS} from '@/enum/commonEnum';

const genderEnums = getEnums('性别', COMMON_ENUMS);

接下来分一些场景来举例一些使用方法:

  1. 作某个字段的值映射

{
   title: '性别',  
   dataIndex: 'gender',  
   render: (gender) => genderEnums.getDescriptionFromValue(gender),
}
  1. select组件中需要传入选项

<Select  
   name="gender"  
   label="性别"
   options={genderEnums.getOptions}
/>

其余方法就留给大家自己探索吧,我觉得这个封装已经可以涵盖大部分的场景了,你觉得呢?

作者:AliPaPa
来源:juejin.cn/post/7221820151397335077

收起阅读 »

前端实现点击选词功能

web
今天有一个需求,点击选中某个英文单词,然后对这个单词做一些处理,例如高亮背景、查看一些详细信息等等,今天简单实现了一下,效果如下:(支持移动端,chrome和sarafi浏览器均能正常使用。语言🚀 vue3 + typescript) 选词 由于要动态添加给...
继续阅读 »

今天有一个需求,点击选中某个英文单词,然后对这个单词做一些处理,例如高亮背景、查看一些详细信息等等,今天简单实现了一下,效果如下:(支持移动端,chrome和sarafi浏览器均能正常使用。语言🚀 vue3 + typescript)


highlight.gif


选词


由于要动态添加给某些单词动态添加一些标签,我们这里可以考虑使用v-html


首先我们先编写一下简单的结构


<script setup lang="ts">
</script>

<template>
<div class="container" v-html="shortArticle"></div>
</template>


<style>
.container {
font-size: 18px;
}
</style>

然后,我们将需要处理的短文变换为span标签包裹,这里的思路是按照空格划分,然后添加span结构,最后拼接到一起返回。这里有一些边缘条件要考虑,比如can't(whichyes!等等,按照空格划分出来的数据有一点问题。


截屏2023-04-19 20.48.19.png


如果不做处理的话,一些标点符号也会高亮出来,就不太正确了。下面是处理逻辑,整体比较简单,就不解释了。


function addElementSpan(str: string): string {
return str
.split(' ')
.map((item) => {
const { start, word, end } = getWord(item)
return `${start}<span>${word}</span>${end} `
})
.join('')
}

function getWord(str: string) {
let word = ''
let start = ''
let end = ''
let j = str.length - 1
let i = 0

while (i < str.length) {
if (/^[a-zA-Z]$/.test(str[i])) {
break
}
start = start + str[i]
i += 1
}

while (j >= 0) {
if (/^[a-zA-Z]$/.test(str[j])) {
break
}
end = str[j] + end
j -= 1
}

word = str.slice(i, j + 1)

// 处理数字
if (!word && start === end) {
start = ''
}

return {
start,
word,
end
}
}

现在我们来实现效果


<script setup lang="ts">
import { computed } from 'vue'
import { addElementSpan } from './utils'

const str = `It works fine if you move the navbar outside the header. See below. For the reason, according to MDN: The element is positioned according to the normal flow of the document, and then offset
relative to its flow root and containing block based on the values of top, right, bottom, and
left. For the containing block: The containing block is the ancestor to which the element is
relatively positioned So, when I do not misunderstand, the navbar is positioned at offset 0
within the header as soon as it is scrolled outside the viewport (which, clearly, means, you
can't see it anymore).`


const shortArticle = computed(() => {
return addElementSpan(str)
})

function setColor(event: any) {
// console.log(event.target.innerText) 获取选中的文本
event.target?.classList.add('word_highlight')
}
</script>

<template>
<div class="container" @click="setColor($event)" v-html="shortArticle"></div>
</template>


<style>
.word_highlight {
background-color: red;
}
</style>


在父亲元素上添加点击事件,触发事件点击之后,调用setColor函数,高亮背景(添加class)


不过有一点小小的问题,点击div的空白区域或者非英文单词区域会直接整个背景变成红色,控制台打印event.target.innerText可以发现它的值为整个文本,所以我们可以根据判断打印的文本长度和需要设置的文本长度是否一致来解决这个问题。(ps:⬆️面的示例代码str字符串使用了反引号 模板字符串 ,直接使用下面会影响结果)


function setColor(event: any) {
// console.log(event.target.innerText)
if(str !== event.target.innerText){
event.target?.classList.add('word_highlight')
}
}

对于event.target不太了解的伙伴可以看这篇文章 ➡️ Event.target - Web API 接口参考 | MDN (mozilla.org)


(和event.target类似的还有一个属性event.currentTarget,不太了解的伙伴可以看下这篇文章 ➡️ Event.currentTarget - Web API 接口参考 | MDN (mozilla.org),它俩的区别是event.target指的是事件触发的元素,而event.currentTarget指的是事件绑定的元素)


功能拓展


这里只是演示了一下比较简单的背景高亮效果,有需求的伙伴可以自己拓展一下。


比如类似于掘金的拼写错误提示框


截屏2023-04-19 21.16.20.png


如果要实现滑动选词的话,可以参考这个博主的文章 ➡️ 鼠标选中文本划词高亮、再次选中划词取消高亮效果


作者:笨笨狗吞噬者
来源:juejin.cn/post/7223733256688025661
收起阅读 »

因为写不出拖拽移动效果,我恶补了一下Dom中的各种距离

web
背景 最近在项目中要实现一个拖拽头像的移动效果,一直对JS Dom拖拽这一块不太熟悉,甚至在网上找一个示例,都看得云里雾里的,发现遇到最大的拦路虎就是JS Dom各种各样的距离,让人头晕眼花,看到一个距离属性,大脑中的印象极其模糊,如同有一团雾一样,不知其确切...
继续阅读 »

背景


最近在项目中要实现一个拖拽头像的移动效果,一直对JS Dom拖拽这一块不太熟悉,甚至在网上找一个示例,都看得云里雾里的,发现遇到最大的拦路虎就是JS Dom各种各样的距离,让人头晕眼花,看到一个距离属性,大脑中的印象极其模糊,如同有一团雾一样,不知其确切含义。果然是基础不牢,地动山摇。今天决心夯实一下基础,亲自动手验证一遍dom各种距离的含义。


JS Dom各种距离释义


下面我们进入正题, 笔者不善于画图, 主要是借助浏览器开发者工具,通过获取的数值给大家说明一下各种距离的区别。


第一个发现 window.devicePixelRatio 的存在


本打算用截图软件丈量尺寸,结果发现截图软件显示的屏幕宽度与浏览器开发者工具获取的宽度不一致,这是为什么呢?



  • 截图软件显示的屏幕宽度是1920


image.png



  • window.screen.width显示的屏幕宽度是1536


image.png


这是怎么回事?原来在PC端,也存在一个设备像素比的概念。它告诉浏览器一个css像素应该使用多少个物理像素来绘制。要说设备像素比,得先说一下像素和分辨率这两个概念。



  • 像素
    屏幕中最小的色块,每个色块称之为一个像素(Pixel)


image.png



image.png



  • 设备像素比


设备像素比的定义是:


window.devicePixelRatio =显示设备物理像素分辨率显示设备CSS像素分辨率\frac{显示设备物理像素分辨率}{显示设备CSS像素分辨率}


根据设备像素比的定义, 如果知道显示设备横向的css像素值,根据上面的公式,就能计算出显示设备横向的物理像素值。


显示设备宽度物理像素值= window.screen.width * window.devicePixelRatio;

设备像素比在我的笔记本电脑上显示的数值是1.25, 代表一个css逻辑像素对应着1.25个物理像素。


image.png


我前面的公式计算了一下,与截图软件显示的像素数值一致。这也反过来说明,截图软件显示的是物理像素值。


image.png



  • window.devicePixelRatio 是由什么决定的 ?


发现是由笔记本电脑屏幕的缩放设置决定的,如果设置成100%, 此时window.screen.width与笔记本电脑的显示器分辨率X轴方向的数值一致,都是1920(如右侧图所示), 此时屏幕上的字会变得比较小,比较伤视力。





  • 逻辑像素是为了解决什么问题?


逻辑像素是为了解决屏幕相同,分辨率不同的两台显示设备, 显示同一张图片大小明显不一致的问题。比如说两台笔记本都是15英寸的,一个分辨率是1920*1080,一个分辨率是960*540, 在1920*1080分辨率的设备上,每个格子比较小,在960*540分辨率的设备上,每个格子比较大。一张200*200的图片,在高分率的设备上看起来会比较小,在低分辨率的设备上,看起来会比较大。观感不好。为了使同样尺寸的图片,在两台屏幕尺寸一样大的设备上,显示尺寸看起来差不多一样大,发明了逻辑像素这个概念。规定所有电子设备呈现的图片等资源尺寸统一用逻辑像素表示。然后在高分辨率设备上,提高devicePixelRatio, 比如说设置1920*1080设备的devicePixelRatio(dpr)等于2, 一个逻辑像素占用两个格子,在低分辨率设备上,比如说在960*540设备上设置dpr=1, 一个css逻辑像素占一个格子, 这样两张图片在同样的设备上尺寸大小就差不多了。通常设备上的逻辑像素是等于物理像素的,在高分辨率设备上,物理像素是大于逻辑像素数量的。由此也可以看出,物理像素一出厂就是固定的,而设备的逻辑像素会随着设备像素比设置的值不同而改变。但图片的逻辑像素值是不变的。


document.body、document.documentElement和window.screen的宽高区别


差别是很容易辨别的,如下图所示:



  • document.body -- body标签的宽高

  • document.documentElement -- 网页可视区域的宽高(不包括滚动条)

  • window.screen -- 屏幕的宽高


image.png



  • 网页可视区域不包括滚动条


如下图所示,截图时在未把网页可视区域的滚动条高度计算在内的条件下, 截图工具显示的网页可视区域高度是168, 浏览器显示的网页可视区域的高度是167.5, 误差0.5,由于截图工具是手动截图,肯定有误差,结果表明,网页可视区域的高度 不包括滚动条高度。宽度同理。


image.png



  • 屏幕和网页可视区域的宽高区别如下:


屏幕宽高是个固定值,网页可视区域宽高会受到缩放窗口影响。


image.png



  • 屏幕高度和屏幕可用高度区别如下:


屏幕可用高度=屏幕高度-屏幕下方任务栏的高度,也就是:


window.screen.availHeight = window.screen.height - 系统任务栏高度

image.png


scrollWidth, scrollLeft, clientWidth关系


scrollWidth(滚动宽度,包含滚动条的宽度)=scrollLeft(左边卷去的距离)+clientWidth(可见部分宽度);
// 同理
scrollHeight(滚动高度,包含滚动条的高度)=scrollTop(上边卷去的距离)+clientHeight(可见部分高度);

需要注意的是,上面这三个属性,都取的是溢出元素的父级元素属性。而不是溢出元素本身。本例中溢出元素是body(document.body),其父级元素是html(document.documentElement)。另外,


溢出元素的宽度(document.body.scrollWidth)=父级元素的宽度(document.documentElement.scrollWidth) - 滚动条的宽度(在谷歌浏览器上滚动条的宽度是19px)

image.png


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- <meta name="viewport" content="width=device-width, initial-scale=1.0"> -->
<title>JS Dom各种距离</title>
<style>
html,
body {
margin: 0;
}

body {
width: 110%;
border: 10px solid blue;
}

.rect {
height: 50px;
background-color: green;
}
</style>
</head>

<body>
<div id="rect" class="rect"></div>
</body>

</html>

元素自身和父级元素的scrollWidth和scrollLeft关系?


从下图可以看出:



  • 元素自身没有X轴偏移量,元素自身的滚动宽度不包含滚动条

  • 父级元素有X轴便宜量, 父级元素滚动宽度包含滚动条
    image.png


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- <meta name="viewport" content="width=device-width, initial-scale=1.0"> -->
<title>JS Dom各种距离</title>
<style>
div {
border: 1px solid #000;
width: 200px;
height: 600px;
padding: 10px;
background-color: green;
margin: 10px;
}
</style>
</head>

<body>
<div class="rect"> 111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
</div>
</body>
<script>
</script>
</html>

offsetWidth和clientWidth的关系?


offsetWidth和clientWidth的共同点是都包括 自身宽度+padding , 不同点是offsetWidth包含border


如下图所示:



  • rect元素的clientWidth=200px(自身宽度) + 20px(左右padding) = 220px

  • rect元素的offsetWidth=200px(自身宽度) + 20px(左右padding) + 2px(左右boder) = 222px


image.png


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- <meta name="viewport" content="width=device-width, initial-scale=1.0"> -->
<title>JS Dom各种距离</title>
<style>
div {
border: 1px solid #000;
width: 200px;
height: 100px;
padding: 10px;
background-color: green;
margin: 10px;
}
</style>
</head>

<body>
<div class="rect">111111111111111111111111111111111111111111111111</div>
</body>
<script>


</script>

</html>

event.clientX,event.clientY, event.offsetX 和 event.offsetY 关系


代码如下,给rect元素添加一个mousedown事件,打印出事件源的各种位置值。


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- <meta name="viewport" content="width=device-width, initial-scale=1.0"> -->
<title>JS Dom各种距离</title>
<style>
html,
body {
margin: 0;
}

body {
width: 200px;
padding: 10px;
border: 10px solid blue;
}

.rect {
height: 50px;
background-color: green;
}
</style>
</head>

<body>

<div id="rect" class="rect"></div>


</body>
<script>
const rectDom = document.querySelector('#rect');

rectDom.addEventListener('mousedown', ({ offsetX, offsetY, clientX, clientY, pageX, pageY, screenX, screenY }) => {
console.log({ offsetX, offsetY, clientX, clientY, pageX, pageY, screenX, screenY });
})
</script>

</html>

我们通过y轴方向的高度值,了解一下这几个属性的含义。 绿色块的高度是50px, 我们找个特殊的位置(绿色块的右小角)点击一下,如下图所示:



  • offsetY=49, 反推出这个值是相对于元素自身的顶部的距离

  • clientY=69, body标签的border-top是10,paiding是10, 反推出这个值是相对网页可视区域顶部的距离

  • screenY=140,目测肯定是基于浏览器窗口,


所以它们各自的含义,就很清楚了。


image.png


事件源属性表示的距离
event.offsetX、event.offsetY鼠标相对于事件源元素(srcElement)的X,Y坐标,
event.clientX、event.clientY鼠标相对于浏览器窗口可视区域的X,Y坐标(窗口坐标),可视区域不包括工具栏和滚动偏移量。
event.pageX、event.pageY鼠标相对于文档坐标的x,y坐标,文档坐标系坐标 = 视口坐标系坐标 + 滚动的偏移量
event.screenX、event.screenY鼠标相对于用户显示器屏幕左上角的X,Y坐标


  • pageX和clientX的关系


我们点击下图绿色块的右下角,把pageX和clientX值打印出来。如下图所示:



  • 可视区域的宽度是360,点击点的clientX=359(由于是手动点击,有误差也正常)

  • 水平方向的偏移量是56

  • pageX是415,360+56=416,考虑到点击误差,可以推算出 ele.pageX = ele.clientX + ele.scrollLeft


image.png


getBoundingClientRect获取的top,bottom,left,right的含义


从下图可以看出,上下左右这四个属性,都是相对于浏览器可视区域左上角而言的。



从下图可以看出,当有滚动条出现的时候,right的值是359.6,而不是360+156(x轴的偏移量), 说明通过getBoundingClientRect获取的属性值是不计算滚动偏移量的,是相对浏览器可视区域而言的。


image.png


想移动元素,mouse和drag事件怎么选?


mouse事件相对简单,只有mousedown(开始),mousemove(移动中),mouseup(结束)三种。与之对应的移动端事件是touch事件,也是三种touchstart(手指触摸屏幕), touchmove(手指在屏幕上移动), touchend(手指离开屏幕)。


相对而言, drag事件就要丰富一些。



  • 被拖拽元素事件


事件名触发时机触发次数
dragstart拖拽开始时触发一次1
drag拖拽开始后反复触发多次
dragend拖拽结束后触发一次1


  • 目标容器事件


事件名触发时机触发次数
dragenter被拖拽元素进入目标时触发一次1
dragover被拖拽元素在目标容器范围内时反复触发多次
drop被拖拽元素在目标容器内释放时(前提是设置了dropover事件)1

想要移动一个元素,该如何选择这两种事件类型呢? 选择依据是:


类型选择依据
mouse事件1. 要求丝滑的拖拽体验 2. 无固定的拖拽区域 3. 无需传数据
drag事件1. 拖拽区域有范围限制 2. 对拖拽流畅性要求不高 3. 拖拽时需要传数据

现在让我们写个拖拽效果


光说不练假把式, 扫清了学习障碍后,让我们自信满满地写一个兼容PC端和移动端的拖动效果。不积跬步无以至千里,幻想一口吃个胖子,是不现实的。这一点在股市上体现的淋漓尽致。都是有耐心的人赚急躁的人的钱。所以,要我们沉下心来,打牢基础,硬骨头啃一点就会少一点,步步为营,稳扎稳打,硬骨头也会被啃成渣。



<!DOCTYPE html>
<html lang="en">
<head>
    
<meta charset="UTF-8" />
    
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
    
<title>拖拽水潭</title>
<style>
.water {
position: absolute;
width: 100px;
height: 100px;
border-radius: 100px;
cursor: grab;
z-index: 10;
}
</style>
</head>

<body>
<img class="water" src="./water.png" alt="" />  
</body>

<script>
let evtName = getEventName();
// 确保图片加载完
window.onload = () => {
// 鼠标拖拽图片时,拖拽点距离图片X和Y轴方向的距离
let offsetX = 0, offsetY = 0;
const water = document.querySelector(".water");

const moveAt = ({ pageX, pageY }) => {
water.style.cssText = `left:${pageX - offsetX}px;top:${pageY - offsetY}px;`;
};

water.addEventListener(evtName.start, (event) => {
// 图片的偏移距离是针对图片边界, 不能把图片边界到鼠标点击图片位置的距离算在内
// 否则移动图片结束后,就会出现向下,向右非自然的偏移
offsetX = event.clientX - water.getBoundingClientRect().left;
offsetY = event.clientY - water.getBoundingClientRect().top;

// 设置初始偏移
moveAt(event);

// 监听鼠标相对于可视窗口移动的距离
document.addEventListener(evtName.move, moveAt);
});

// 拖动停止时,释放document上绑定的移动事件
// 不然移动鼠标,不拖拽时白白产生性能开销
water.addEventListener(evtName.end, () =>
document.removeEventListener(evtName.move, moveAt);
});
};

// 区分是移动端还是PC端移动事件
function getEventName() {
if ("ontouchstart" in window) {
return {
start: "touchstart",
move: "touchmove",
end: "touchend",
};
} else {
return {
start: "mousedown",
move: "mousemove",
end: "mouseup",
};
}
}
</script>
</html>

彩蛋


在chrome浏览器上发现一个奇怪的现象,设置的border值是整数,计算出来的值却带有小数


image.png


而当border值是4的整数倍的时候,计算值是正确的


image.png


看了这篇文章解释说,浏览器可能只能渲染具有整数物理像素的border值,不是整数物理像素的值时,计算出的是近似border值。这个解释似乎讲得通,在设备像素比是window.devicePixelRatio=1.25的情况下, 1px对应的是1.25物理像素, 1.25*4的倍数才是整数,所以设置的逻辑像素是4的整数倍数,显示的渲染计算值与设置值一致,唯一让人不理解的地方,为什么padding,margin,width/height却不遵循同样的规则。


作者:去伪存真
来源:juejin.cn/post/7225206098692407355
收起阅读 »

让我看看你们公司的代码规范都是啥样的?

web
我这里提供一份我自己在使用的项目代码规范,当然我这里比较简陋,有补充的可以打在评论区,我丰富到文章里去。 1.组件命名规范 components下的组件命名规范遵循大驼峰命名规范。 示例:conpnents/AlbumItemCard/AlbumItemCar...
继续阅读 »

我这里提供一份我自己在使用的项目代码规范,当然我这里比较简陋,有补充的可以打在评论区,我丰富到文章里去。


1.组件命名规范


components下的组件命名规范遵循大驼峰命名规范。


示例:conpnents/AlbumItemCard/AlbumItemCard.vue



小驼峰式命名法(lower camel case): 第一个单词以小写字母开始;第二个单词的首字母大写,例如:myName




大驼峰式命名法(upper camel case): 每一个单字的首字母都采用大写字母,例如:MyName



2.目录命名规范


pages下的文件命名规范:遵循小驼峰命名规范。


示例:pages/createAlbum/createAlbum.vue


3.CSS命名规范


class命名规范为中划线。


示例:


<template>
<view class="gui-padding">
...
</view>
</template>
<style lang="scss" scoped>
.gui-padding {
...
}
</style>

css使用scss进行书写。


4.代码注释规范


行内注释://


函数注释:


/**
* @description: 加深颜色值
* @param {string} color 颜色值字符串
* @returns {*} 返回处理后的颜色值
*/

export function getDarkColor(color: string, level: number) {
const reg = /^#?[0-9A-Fa-f]{6}$/
if (!reg.test(color)) return ElMessage.warning('输入错误的hex颜色值')
const rgb = hexToRgb(color)
for (let i = 0; i < 3; i++)
rgb[i] = Math.round(20.5 * level + rgb[i] * (1 - level))
return rgbToHex(rgb[0], rgb[1], rgb[2])
}

接口注释:


/**
* @description 获取后台用户分页列表(带搜索)
* @param page
* @param limit
* @param username
* @returns {<PageRes<AclUser.ResAclUserList>>}
* @docs https://xxxx
*/

export function getAclUserList(params: AclUser.ReqAclUserListParams) {
return http.get<PageRes<AclUser.ResAclUserList>>(
`/admin/acl/user/${params.pageNum}/${params.pageSize}`,
{ username: params.username },
)
}

5.接口书写规范


4.1 接口定义规范:


接口全部写在api目录下面,按照功能划分,分为不同的目录。


比如搜索接口,定义在api/search/index.ts下面。


4..2 接口书写规范:


统一使用类方法,内部方法定义每个接口,最后统一export,接口使用到的类型全部定义在同级目录的interfaces.ts文件中。比如搜索相关的接口:


import Service from '../../utils/request'
import { SearchItemInterface, SearchPageResponseInterface, SearchParamsInterface } from "./interfaces"

class CateGory extends Service {

/**
* @description 搜索功能
* @param {SearchParamsInterface} params 二级分类Id
*/

// 搜索
getSearchAlbumInfo(params: SearchParamsInterface) {
return this.post<SearchPageResponseInterface<SearchItemInterface[]>>({
url: '/api/search/albumInfo',
data: params
})
}
/**
* @description: 获取搜索建议
* @param {string} keyword 搜索关键字
* @return {*}
*/

getSearchSuggestions(keyword: string) {
return this.get<string[]>({
url: `/api/search/albumInfo/completeSuggest/${keyword}`,
loading:false
})
}

}

export const search = new CateGory()

4.3 接口类型定义:


// 搜索参数
export interface SearchParamsInterface {
keyword: string;
category1Id?: number | null;
category2Id?: number | null;
category3Id?: number | null;
attributeList?: string[] | null;
order?: string | null;
pageNo?: number;
pageSize?: number;
}
// 搜索结果item向接口
export interface SearchItemInterface {
id: number;
albumTitle: string;
albumIntro: string;
announcerName: string;
coverUrl: string;
includeTrackCount: number;
isFinished: string;
payType: string
createTime: string;
playStatNum: number;
collectStatNum: number;
buyStatNum: number;
albumCommentStatNum: number;
}

4.4 接口引用


所有export的类接口方法都在api/index.ts中统一引入:


export { courseService } from './category/category'
export { albumsService } from './albums/albums'
export { search } from './search/search'

在页面中使用:


<script>
import { courseService } from "../../api"
/**
* @description: 获取所有分类
* @returns {*}
*/

const getCategoryList = async () => {
try {
const res = await courseService.findAllCategory()
} catch (error) {
console.log(error)
}
}
</script>

6.分支命名规范


分支管理命名规范解释
master 主分支master稳定版本分支,上线完成回归后后,由项目技术负责人从 release 分支合并进来,并打 tag
test 测试分支test/版本号示例:test/1.0.0测试人员使用分支,测试时从 feature 分支合并进来,支持多人合并
feature 功能开发分支feature/功能名称示例:feature/blog新功能开发使用分支,基于master建立
bugfix修复分支bugfix/功能名称示例:fix/blog紧急线上bug修复使用分支,基于master建立
release 上线分支release/版本号示例:release/0.1.0用于上线的分支,基于 master 建立,必须对要并入的 分支代码进行 Code review 后,才可并入上线

7.代码提交规范


作者:白哥学前端
来源:juejin.cn/post/7224408845685522492
tbody>
前缀解释示例
feat新功能feat: 添加新功能
fix修复fix: 修改bug
docs文档变更docs: 更新文档
style代码样式变更style: 修改样式
refactor重构refactor: 重构代码
perf性能优化perf: 优化了性能
test增加测试test: 单元测试
revert回退revert: 回退代码
build打包build: 打包代码
chore构建过程或辅助工具的变动chore: 修改构建
收起阅读 »

html手写一个打印机效果-从最基础到学会

web
手写一个打印机效果 啥叫打印机效果,话不多说,直接上效果。我们可以自己写入一段文本然后通过html的方式,让它跟打印机一样,一个一个的打印到页面,并且还可以一个一个的删除。在这里我先浅说一下,我们的实现技巧,定时器setTimeout控制时间,然后for循环遍...
继续阅读 »

手写一个打印机效果


啥叫打印机效果,话不多说,直接上效果。我们可以自己写入一段文本然后通过html的方式,让它跟打印机一样,一个一个的打印到页面,并且还可以一个一个的删除。在这里我先浅说一下,我们的实现技巧,定时器setTimeout控制时间,然后for循环遍历写入到页面上。


封装的打印js
main(str,text)直接传入要写入的数组对象和要写入的元素。
copy.js 下载到本地引入然后调用它就可以了
image.png


代码


先拿到我们要写入的元素,然后设置好我们要写入的内容。


 var text = document.querySelector('.text');
var str = ['你好 ,我是一名刚入坑不久的大三在校生。', '现在学习都是为了将来的工作。', '希望能够得到大家的鼓励,谢谢!']

基础代码一


首先这里,我们先实现一个只有一段文字的实现效果。实现思路就是通过计时器,控制好时间,每次写入的文字通过str[0].substr(0, k)拿到,需要注意的是,因为是异步任务,回退的时候,我们的时间要设置好,加上写入完的时间1000 + 200 * str[0].length)


  写入
for (let j = 0; j < str[0].length; j++) {
// 使用 setTimeout 函数来实现每个字符的延时输出
setTimeout(() => {
text.innerHTML = str[0].substr(0, j) // 显示当前字符串的前 j 个字符
}, 200 * j) // 延迟时间为 200 毫秒乘以 j,即每个字符间隔 200 毫秒
}

// 回退
// 在所有字符输出完成后,等待 1000 毫秒后开始回退
setTimeout(() => {
for (let k = str[0].length, i = 0; k >= 0; k--, i++) {
// 使用 setTimeout 函数来实现每个字符的延时输出
setTimeout(() => {
text.innerHTML = str[0].substr(0, k) // 显示当前字符串的前 k 个字符
}, 200 * i) // 延迟时间为 200 毫秒乘以 i,即每个字符间隔 200 毫秒
}
}, 1000 + 200 * str[0].length) // 等待时间为 1000 毫秒加上所有字符输出的延时时间

基础代码二 错误代码


首先这个代码是错误的
为了能让大家更好的看到错误的效果,于是我把这个代码也上传了。大家可以看到,在这里,页面上的文字总是会莫名奇怪的出现删除,根本不是我们想要的。其实我们也只是对上面一个代码进行了一个for循环遍历,却出现了这样的效果。其实这导致的原因就是setTimeout是异步任务,时间没有控制好。即每个字符串的打印和删除都是异步任务,无法保证它们的执行顺序。因此,可能会出现多个字符串的打印和删除任务交错执行的情况,导致效果不符合预期。



 // 即每个字符串的打印和删除都是异步任务,无法保证它们的执行顺序。因此,可能会出现多个字符串的打印和删除任务交错执行的情况,导致效果不符合预期。
// 整个str 这是一个有问题的代码 因为计算时间太麻烦了 都是异步任务
// for (let s = 0; s < str.length; s++) {
// // 写入
// for (let j = 0; j < str[s].length; j++) {
// setTimeout(() => {
// text.innerHTML = str[s].substr(0, j)
// }, 200 * j)
// }
// // 回退
// setTimeout(() => {
// for (let k = str[s].length, i = 0; k >= 0; k--, i++) {
// setTimeout(() => {
// text.innerHTML = str[s].substr(0, k)
// }, 200 * i)

// }
// }, 1000 + 200 * str[s].length)
// }

基础代码三


为了解决上面的问题,我们使用了函数封装并且使用了回调函数实现我们想要的效果。我们将打印和删除都封装成一个含有回调函数的函数,为什么要含有回调函数呢?这是为了我们下面对一个字符串打印和删除的函数做封装。打我们打印完一个字符串时,我们才会执行删除。所有我们将删除函数放到打印的回调函数中去执行。然后我们将打印整个字符串数组进行封装,因为我们在删除的里面也有一个回调函数,那么我们可以在这个回调函数里去执行打印下一条字符串,这样就防止了控制时间不准确的问题。


 // 打印字符串
function printText(str, callback) {
var i = 0;
var timer = setInterval(function () {
text.innerHTML = str.substr(0, i); // 将字符串的前缀赋值给显示文本的元素
i++;
if (i > str.length) { // 如果已经打印完整个字符串
clearInterval(timer); // 停止定时器
callback && callback(); // 调用回调函数
}
}, 200); // 每 200 毫秒打印一个字符
}

// 删除字符串
function deleteText(str, callback) {
var i = str.length;
var timer = setInterval(function () {
text.innerHTML = str.substr(0, i); // 将字符串的前缀赋值给显示文本的元素
i--;
if (i < 0) { // 如果已经删除到空字符串
clearInterval(timer); // 停止定时器
callback && callback(); // 调用回调函数
}
}, 200); // 每 200 毫秒删除一个字符
}

// 打印和删除字符串
function printAndDeleteText(str, callback) {
printText(str, function () { // 先打印字符串
setTimeout(function () {
deleteText(str, callback); // 等待 1 秒后再删除字符串
}, 1000);
});
}

// 循环遍历字符串数组,依次打印和删除字符串
function printAndDeleteAllText(strArr) {
function printAndDeleteNext(i) {
if (i >= strArr.length) { // 如果已经处理完所有字符串
printAndDeleteNext(0); // 重新从头开始处理
} else {
printAndDeleteText(strArr[i], function () { // 先打印字符串
i++;
printAndDeleteNext(i); // 递归调用自身,处理下一个字符串
});
}
}
printAndDeleteNext(0); // 开始处理第一个字符串
}
// 开始打印和删除字符串数组中的所有字符串
printAndDeleteAllText(str)

最优代码


其实我们做了,这么多,最后就是为了解决异步任务。
所以我这里直接采用Promiseasync await解决上面的问题。我们通过Promise解决实现打印和删除的异步任务。我们通过async await封装整个运行函数,解决了定时器异步问题,不用再计算时间,又难有算不出来。


 // 最终版 封装 解决异步任务
function writeText(t, delay = 200) {
return new Promise((resolve, reject) => {
setTimeout(() => {
text.innerHTML = t; // 显示当前字符串 t
resolve(); // Promise 完成
}, delay) // 延迟 delay 毫秒后执行
})
}

async function main(str) {
while (true) { // 无限循环
for (let j = 0; j < str.length; j++) {
// 写入
for (let i = 0; i <= str[j].length; i++) {
await writeText(str[j].substr(0, i)) // 显示当前字符串的前 i 个字符
}
// 回退
// 回退前先等一秒
await new Promise((resolve, reject) => {
setTimeout(() => {
resolve(); // 等待 1000 毫秒后 Promise 完成
}, 1000) // 等待 1000 毫秒
})
for (let i = str[j].length; i >= 0; i--) {
await writeText(str[j].substr(0, i), 200) // 显示当前字符串的前 i 个字符,间隔 200 毫秒
}
}
}
}
main(str)

源码


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>打印机效果</title>
<style>
.container {
display: flex;
/* 使用 flex 布局 */
flex-direction: column;
/* 垂直布局 */
align-items: center;
/* 水平居中 */
justify-content: center;
/* 垂直居中 */
height: 100vh;
/* 高度占满整个视口 */
}

h1 {
font-size: 3rem;
/* 字体大小 */
margin-bottom: 2rem;
/* 底部间距 */
text-align: center;
/* 居中对齐 */
}

.text {
font-size: 2rem;
/* 字体大小 */
font-weight: bold;
/* 字体加粗 */
text-align: center;
/* 居中对齐 */
border-right: 2px solid black;
/* 添加光标效果 */
white-space: nowrap;
/* 不换行 */
overflow: hidden;
/* 隐藏超出部分 */
animation: blink 0.5s step-end infinite;
/* 添加光标闪烁效果 */
height: 3rem;
/* 设置一个固定的高度 */
}


@keyframes blink {

from,
to {
border-color: transparent;
/* 透明边框颜色 */
}

50% {
border-color: black;
/* 黑色边框颜色 */
}
}
</style>
</head>

<body>
<div class="container">
<h1>逐字打印和删除文字效果</h1>
<p class="text"></p>
</div>
</body>
<script>
var text = document.querySelector('.text');
var str = ['你好 ,我是一名刚入坑不久的大三在校生。', '现在学习都是为了将来的工作。', '希望能够得到大家的鼓励,谢谢!']


// 写入
// for (let j = 0; j < str[0].length; j++) {
// // 使用 setTimeout 函数来实现每个字符的延时输出
// setTimeout(() => {
// text.innerHTML = str[0].substr(0, j) // 显示当前字符串的前 j 个字符
// }, 200 * j) // 延迟时间为 200 毫秒乘以 j,即每个字符间隔 200 毫秒
// }

// // 回退
// // 在所有字符输出完成后,等待 1000 毫秒后开始回退
// setTimeout(() => {
// for (let k = str[0].length, i = 0; k >= 0; k--, i++) {
// // 使用 setTimeout 函数来实现每个字符的延时输出
// setTimeout(() => {
// text.innerHTML = str[0].substr(0, k) // 显示当前字符串的前 k 个字符
// }, 200 * i) // 延迟时间为 200 毫秒乘以 i,即每个字符间隔 200 毫秒
// }
// }, 1000 + 200 * str[0].length) // 等待时间为 1000 毫秒加上所有字符输出的延时时间


// 即每个字符串的打印和删除都是异步任务,无法保证它们的执行顺序。因此,可能会出现多个字符串的打印和删除任务交错执行的情况,导致效果不符合预期。
// 整个str 这是一个有问题的代码 因为计算时间太麻烦了 都是异步任务
// for (let s = 0; s < str.length; s++) {
// // 写入
// for (let j = 0; j < str[s].length; j++) {
// setTimeout(() => {
// text.innerHTML = str[s].substr(0, j)
// }, 200 * j)
// }
// // 回退
// setTimeout(() => {
// for (let k = str[s].length, i = 0; k >= 0; k--, i++) {
// setTimeout(() => {
// text.innerHTML = str[s].substr(0, k)
// }, 200 * i)

// }
// }, 1000 + 200 * str[s].length)
// }


// 最终版 封装 解决异步任务
function writeText(t, delay = 200) {
return new Promise((resolve, reject) => {
setTimeout(() => {
text.innerHTML = t; // 显示当前字符串 t
resolve(); // Promise 完成
}, delay) // 延迟 delay 毫秒后执行
})
}

async function main(str) {
while (true) { // 无限循环
for (let j = 0; j < str.length; j++) {
// 写入
for (let i = 0; i <= str[j].length; i++) {
await writeText(str[j].substr(0, i)) // 显示当前字符串的前 i 个字符
}
// 回退
// 回退前先等一秒
await new Promise((resolve, reject) => {
setTimeout(() => {
resolve(); // 等待 1000 毫秒后 Promise 完成
}, 1000) // 等待 1000 毫秒
})
for (let i = str[j].length; i >= 0; i--) {
await writeText(str[j].substr(0, i), 200) // 显示当前字符串的前 i 个字符,间隔 200 毫秒
}
}
}
}
main(str)
</script>

</html>

作者:Mr-Wang-Y-P
来源:juejin.cn/post/7225178555827191868
收起阅读 »

URL刺客现身,竟另有妙用!

web
工作中大家会接触到形形色色的 url,有些完美遵循格式,有些却像刺客一样,冷不丁的给你一刀。 先介绍下我的惨痛经历,给大家避避坑,最后告诉大家一个 url 刺客的妙用。 刺客介绍 1. iOS WKWebview 刺客 此类刺客手段单一,只会影响 iOS WK...
继续阅读 »

工作中大家会接触到形形色色的 url,有些完美遵循格式,有些却像刺客一样,冷不丁的给你一刀。


先介绍下我的惨痛经历,给大家避避坑,最后告诉大家一个 url 刺客的妙用。


刺客介绍


1. iOS WKWebview 刺客


此类刺客手段单一,只会影响 iOS WKWebview



  • 空格


运营人员由于在通讯工具中复制粘贴,导致前面多了一个空格,没有仔细检查,直接录入了后台管理系统。



  • 中文


运营人员为了方便自身统计,直接在url中加入中文,录入了后台管理系统。


现象均为打开一个空白页,常见的处理手段如下:



  • 将参数里的中文URIEncode

  • 去掉首尾空格


const safeUrl = (url: string) => {
const index = url.indexOf('?');

if (index === -1) return url.trim();

// 这行可以用任意解析参数方法替代,仅代表要拿到参数,不考虑兼容性的简单写法
const params = new URLSearchParams(url.substring(index));
const paramStr = Object.keys(params)
.map((key: string) => {
return `${key}=${encodeURIComponent(params[key])}`;
})
.join('&');

const formatUrl = url.substring(0, index + 1) + paramStr;

return formatUrl.trim();
};

可以看到虽然这里提出了一个 safeUrl 方法,但如果业务中大量使用 window.location.href , window.location.replace, 之类的方法进行跳转,替换起来会比较繁琐.


再比如在 Hybrid App 的场景中,虽然都是跳转,打开新的 webview ,还是在本页面跳转会是不同的实现,所以在业务内提取一个公共的跳转方法更有利于健壮性和拓展性。


值得注意的是,如果链接上的中文可能是用于统计的,在上报打点时,应该将其值(前端/服务端处理均可)进行 URIDecode,否则运营人员会在后台看到一串串莫名其妙的 %XX ,会非常崩溃(别问我怎么知道的,可能只是伤害过太多运营)


2. 格式刺客


格式刺客指的是,不管何种原因,不知何种场景,就是不小心配错了,打错了,漏打了等。


比如:https://www.baidu.com 就被打成了 htps://www.baidu.com、www.baidu.com 等。


// 检查URL格式是否正确
function isValidUrl(url: string): boolean {
const urlPattern = new RegExp(
"^(https?:\/\/)?" + // 协议
"(([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,})" + // 域名
"(:[0-9]{1,5})?" + // 端口号
"(\/.*)?$", // 路径
"i"
);
return urlPattern.test(url);
}

以上是一个很基础的判断,但是实际的应用场景中,有可能会需要填写相对路径,或者自定义的 scheme ,比如 wx:// ,所以检验的宽松度可以自行把握。


在校验到 url 配置可能存在问题时,可以上报到 sentry 或者其他异常监控平台,这样就可以比用户上报客服更早的发现潜在问题,避免长时间的运营事故。


3. 异形刺客


这种刺客在视觉上让人无法察觉,只有在跳转后才会让人疑惑不已。他也是最近被产品同学发现的,以下是当时的现场截图:



一段平平无奇的文本,跟着一段链接,视觉上无任何异常。


经过对跳转后的地址进行分析,发现了前面居然有一个这样的字符%E2%80%8B,好奇的在控制台中进行了尝试。




一个好家伙,这是什么,两个单引号吗?并不是,对比了很常用的 '%2B' ,单引号是自带的,那么我到底看到了什么,魔鬼嘛~


在进行了一番检索后知道了这种字符被称为零宽空格,他还有以下兄弟:



  • \u202b-\u202f

  • \ufeff

  • \u202a-\u202e


具体含义可以看看参考资料,这一类字符完全看不见,但是却对程序的运行产生了恶劣的影响。


可以使用这个语句去掉


str.replace(/[\u200b-\u200f\uFEFF\u202a-\u202e]/g, "");

刺客的妙用


头一天还被刺客气的瑟瑟发抖。第二天居然发现刺客的妙用。


场景:




  • 产品要求在微信环境隐藏标题




我方前端工程师:



  • 大手一挥,发功完毕,准备收工


document.title = '';

测试:




  • 来看看,页面A标题隐藏不了




我方前端工程师:


啊?怎么回事,本地调试还是好的,发上去就不行了,为什么页面A不可以,另外一个页面B只是参数变了变就行。


架构师出手:


页面A包含了开放标签,导致设置空Title失效,空,猛然想起了刺客,快用起来!


function setTitle(title: string) {
if (title) {
document.title = title;
} else {
document.title = decodeURIComponent('%E2%80%8B');
}
}

果然有效,成功解决了一个疑难杂症,猜测是微信里有不允许设置标题为空的机制,会在某些标签存在的时候被触发。(以上场景在 Android 微信 Webview 中可复现)


小结


以上只是工作中碰到 url 异常的部分场景和处理方案,如果小伙伴们也有类似的经历,可以在评论区中分享,帮助大家避坑,感谢朋友们的阅读,笔芯~


参考资料:


零宽字符 - 掘金LvLin


什么零宽度字符,以及零宽度字符在JavaScript中的应用 - 掘金whosmeya


作者:windyrain
来源:juejin.cn/post/7225133152490094651
收起阅读 »

关于 Emoji 你不知道的事

web
2022 年,支付宝上线了生僻字键盘,解决了“身份认证”环节中普通输入法经常打不出生僻字的问题。生僻字键盘是蚂蚁集团生僻字解决方案的一部分,本系列将持续分享其背后的字符编码科普文章,包括不限于:《文字是如何显示在屏幕上的?》、《字符编码工作者都在做什么》,敬请...
继续阅读 »

2022 年,支付宝上线了生僻字键盘,解决了“身份认证”环节中普通输入法经常打不出生僻字的问题。生僻字键盘是蚂蚁集团生僻字解决方案的一部分,本系列将持续分享其背后的字符编码科普文章,包括不限于:《文字是如何显示在屏幕上的?》、《字符编码工作者都在做什么》,敬请期待。


本文作者是蚂蚁集团前端工程师醉杭(👉 点击查看醉杭的成长故事),本篇将介绍 Emoji 的编码逻辑,以及如何在代码中正确处理 Emoji 。蚂蚁集团前端工程师七柚封装了字符处理 js 库,已开源,欢迎使用~ github.com/alipay/char…



结论先行



  • 基本 emoji 和常用 Unicode 字符毫无区别


每个 emoji 用对应一个 Unicode 码位,如:🌔 U+1F314 (对应 JS 中 UTF-16 编码是:"\uD83C\uDF14"),汉字 𠇔 U+201D4,对应 JS 中的 UTF-16 编码是"\uD840\uDDD4"



  • emoji 有特殊的修饰、拼接等规则


在某些 emoji 字符后增加一个肤色修饰符改变 emoji 的肤色、可以将多个 emoji 通过连接符拼接成一个emoji,这些特殊规则使得在代码中判定 emoji 的长度、截取和对 emoji 做其他处理都比较困难。需要澄清的是:用一个 Unicode 字符修饰前一个字符不是 emoji 独有的,其他 Unicode 字符也存在,如:Ü,由大写字母U(U+0055),后面跟一个连音符号(U+0308)组成。



  • 术语


码点/码位:Unicode 编码空间中的一个编码,如,汉字𠇔的码位是 201D4,通常表示为:U+201D4


起源


1982 年,卡内基美隆大学是首次在电子公告里中使:-)表情符号。后续在日本手机短信中盛行,并称为颜文字(日语:かおもじ,英文:emoticon),颜文字仍然是普通的文本字符。
1999 年,栗田穰崇 (Shigetaka Kurita) 发明了 e-moji (え-もじ),并设计了 176 个 emoji 符号,共 6 种颜色,分辨率为 12x12。
image.png
纽约博物馆馆藏:最初的 176 个 emoji


2010 年,Unicode 正式收录了 emoji,为每个 emoji 分配了唯一的码点。
2011 年,Apple 在 iOS 中加入了标准的 emoji 输入键盘,2 年后安卓系统也引入了 emoji 键盘。


Unicode


Unicode 中原本就收录了很多有意义的字符,如:㎓、𐦖、☳,大家还可以查看 Unicode 1 号平面的埃及象形文字区 (U+13000–U+1342F)。收录 emoji 对 Unicode 来说没有挑战,技术上是完全兼容的。
image.png
Unicode 象形文字区节选


Emoji 的编码


基本 emoji



基本 emoji 是指在 Unicode 编码表中用 1 个唯一码位表示的 emoji



最简单的 emoji 就是 Unicode 表中的一个字符,和我们常用的 Unicode 字符没有区别。多数基本 emoji 都被分配到 Unicode 编码表 1 号平面的 U+1F300–1F6FFU+1F900–1FAFF 两个区域,完整的列表请看15.0/emoji-sequences.txt
image.png
Unicode 中 emoji 的码位


我们常见的 emoji 是彩色的,而常见的字体是黑色的。字符的颜色取决于字体文件,如果你愿意,你也可以把其常见的汉字字体设计成彩色的。iOS/MacOS 的Apple Color Emoji字体是一种 160x160 的点阵字体, Android 的Noto Emoji是一种 128x128 的点阵字体,而 Windows 使用的 Segoe UI Emoji 是一种矢量彩色字体。


为什么同一个 emoji 在不同设备、不同软件中显示不同?
不同设备、软件使用了不同的 emoji 字体所以显示效果不同。Unicode 只是约定了码点到 emoji 的映射关系,并没有约定 emoji 图形,每个 emoji 字体文件可以按照自己的想法设计 emoji。
image.png
同一个 emoji 在不同软件上的显示效果


为什么在钉钉中发送**[憨笑]**会显示成image.png
早期包含 Unicode emoji 的字体还没广泛普及,你给对方发一个 emoji 符号😄,如果没对方设备有对应的字体看到的会是**?**
为了解决缺失 emoji 字体导致大家显示不一致的问题(或者为了方便自定义自己的**伪emoji**——为了方便描述,把软件自定义的图片称作伪 emoji),很多软件自己开发了能向下兼容的解决方案,如钉钉。该自定义方案与 Unicode 编码没有关系,只是将特殊的字符串与一张图片映射起来,当对方发送[xx]字符串时,将它显示成对应的图片
早期支付宝的转账备注功能中也定义了自己的伪emoji伪emoji的好处是向下兼容,如果使用标准的Unicode emoji 可能会导致别的系统无法处理(如:做了汉字正则校验),导致转账失败;弊端是不通用,别的系统通常不支持另一个系统定义的伪emoji,直接将[xx]文本显示出来,如:收银台在支付界面就会直接显示转账备注的伪 emoji 文本[xx]
image.png


字素集


字素集(grapheme cluster)在 Unicode 中通常一个码点对应一个字符,但是 Unicode 引入了特定的机制允许多个 Unicode 码点组合成一个字形符号。这样由于多个码点组合成的一个字符称作字素集。
比如Ü是一个字素集,是由两个码点组成:大写字母 U(U+0055),后面跟一个连音符号(U+0308)。再比如:'曙󠄀'.length=3'🤦🏼‍♂️'.length=7,前者由基本的字符加上一个变体选择符️ VS-17 (见后文)组成,后者由多个基础 emoji 修饰符、连接符组成。
点开有惊喜Ų̷̡̡̨̫͍̟̯̣͎͓̘̱̖̱̣͈͍̫͖̮̫̹̟̣͉̦̬̬͈͈͔͙͕̩̬̐̏̌̉́̾͑̒͌͊͗́̾̈̈́̆̅̉͌̋̇͆̚̚̚͠ͅ[左边是一个.length 为 65 的字素集,它是不可分割的一个字符]


在 Unicode 的规范中要求所有软件(本编辑器、文本渲染、搜索等)将一个字素集当做不可分割的整体,即:当做一个单一的字符对待。
image.png
Unicode 处理的难点就在于字素集,下文均与该定义有关,开发者的噩梦都源自该概念。不能简单地通过 .length 读取字符串的长度;如果想截取字符串的前 10 个字符,也不能简单的使用.substring(0, 10),因为这可能会截断 emoji 字符;反转字符串也非常困难,U+263A U+FE0F 有意义,反转之后 U+FE0F U+263A 却没有意义,后文会介绍正确的处理方式。


变体选择符️


Variation Selector(又叫异体字选择器),是 Unicode 中定义的一种修饰符机制。一个基本字符后接上一个异体字选择器组成一个异体字。背景是:一个字符可能会有多个不同的变体,这些变体本质上是同一个字符,具有同样的含义,由于地区、文化差异导致他们演变成了不同的书写形式。Unicode 为变体字分配了同一个码点,如果想要显示特定的书写形式可以在字符后紧接着一个异体字选择器指定。
image.pngimage.png就是变体字。需要澄清的是,并非所有相似的字符都按照异性字的形式合并成了一个码点,就是分别分配了不同的码点,理论上这两个字符也可以合并变体字共用一个码点。
在 Unicode 中引入彩色的 emoji 前就已经定义了一些黑色的图形符号,引入彩色 emoji 后,新的 emoji 与黑色的符号具有相同的含义,于是共用了同一个 Unicode 码点,可在字符后接上一个 VS 指定要显示那个版本。
常用的 VS 有 16 个 VS-1 ~ VS-16,对应的 Unicode 是(U+FE00~U+FE0F),其中 VS-15(U+FE0E)用于指定显示黑色的 text 版本,VS-16(U+FE0F)用于指定显示彩色的 emoji 版本。


默认显示VS-15 修饰符VS-16 修饰符
U+2702✂︎U+2702 U+FE0E✂︎U+2702 U+FE0F ✂️
U+2620☠︎U+2620 U+FE0E☠︎U+2620 U+FE0F ☠️
U+26A0⚛︎U+26A0 U+FE0E⚛︎U+26A0 U+FE0F ⚛️
U+2618☘︎U+2618 U+FE0E☘︎U+2618 U+FE0F ☘️

可以动手验证一下



image.png



  • ✂ 不含修饰符'\u2702'

  • ✂︎ 含 VS-15'\u2702\uFE0E'

  • ✂️ 含 VS-16'\u2702\uFE0F'



为什么把黑色的剪刀 ✂︎ 粘贴到 Chrome 搜索栏中显示成彩色,把彩色剪刀 ✂️ 复制到 Chrome 的 Console 中显示成黑色?
image.png image.png
我们通过 VS 符号告诉软件要显示成指定的异体字符,但是软件可以不听我们的,软件可能会强制指定特定的字体,如果该字体中只包含一种异体字符的字形数据那就只会显示该字形。


肤色修饰符


大多数人形相关的 Emoji 默认是黄色的,在 2015 年为 emoji 引入肤色支持。没有为每种肤色的 emoji 组合分配新的码点,而是引入了五个新码点作为修饰符:1F3FB 🏻、1F3FC 🏼、1F3FD 🏽、1F3FE 🏾、1F3FF 🏿 。肤色修饰符追加到现有的 emoji 后面则形成新的变种,如:👋 U+1F44B+ 🏽U+1F3FD= 👋🏽



  • 👋 在 JavaScript 中 UTF-16 值是'\uD83D\uDC4B'

  • **🏽 **在 JavaScript 中 UTF-16 值是'\uD83C\uDFFD'


组合在一起'\uD83D\uDC4B\uD83C\uDFFD'就得到了 👋🏽
image.png


5 种肤色修饰符的取值是基于菲茨帕特里克度量,因此叫做 EMOJI MODIFIER FITZPATRICK。肤色度量共有 6 个取值,但在 emoji 中前两个颜色合并成了一个。
image.png
最终 280 个人形 emoji 就产生了 1680 种肤色变种,这是五种不同肤色的舞者:🕺🕺🏻🕺🏼🕺🏽🕺🏾🕺🏿


零宽度连接符(ZWJ)


Unicode 通过多个基础 emoji 组合的形式表示某些复杂 emoji。组合的方式是在两个 emoji 之间添加一个U+200D,即:零宽度连接符(ZERO-WIDTH JOINER,简写为 ZWJ),如:



  • 👩 + ZWJ+ 🌾 = 👩‍🌾


image.png
下面是一些例子,完整的组合列表参考:Unicode 15.0/emoji-zwj-sequences.txt




  • 👩 + ✈️ → 👩‍✈️

  • 👨 + 💻 → 👨‍💻

  • 👰 + ♂️ → 👰‍♂️

  • 🐻 + ❄️ → 🐻‍❄️

  • 🏴 + ☠️ → 🏴‍☠️

  • 🏳️ + 🌈 → 🏳️‍🌈

  • 👨 + 🦰 → 👨‍🦰 (有意思的是:发色是通过 ZWJ 组合基础 emoji 实现,而肤色则是用肤色修饰符实现)

  • 👨🏻 + 🤝 + 👨🏼 → 👨🏻‍🤝‍👨🏼

  • 👨 + ❤️ + 👨 → 👨‍❤️‍👨

  • 👨 + ❤️ + 💋 + 👨 → 👨‍❤️‍💋‍👨

  • 👨 + 👨 + 👧 → 👨‍👨‍👧

  • 👨 + 👨 + 👧 + 👧 → 👨‍👨‍👧‍👧



可惜,有些 emoji 不是通过 ZWJ 组全 emoji 实现的,可能是因为没有赶上 ZWJ 定义的时机




  • 🌂 + 🌧 ≠ ☔️

  • 💄 + 👄 ≠ 💋

  • 🐴 + 🌈 ≠ 🦄

  • 👁 + 👁 ≠ 👀

  • 👨 + 💀 ≠ 🧟

  • 👩 + 🔍 ≠ 🕵️‍♀️

  • 🦵 + 🦵 + 💪 + 💪 + 👂 + 👂 + 👃 + 👅 + 👀 + 🧠 ≠ 🧍



旗帜·双字母连字


Unicode 中包含国旗符号,每个国旗也没有分配独立的码点,而是由双字符连字(ligature)来表示。(但 Windows 平台因为某些原因不支持显示,如果你是用 Windows 平台的浏览器阅读本文,只能说抱歉了)



  • 🇺 + 🇳 = 🇺🇳

  • 🇷 + 🇺 = 🇷🇺

  • 🇮 + 🇸 = 🇮🇸

  • 🇿 + 🇦 = 🇿🇦

  • 🇯 + 🇵 = 🇯🇵


这里的🇦 ~ 🇿不是字母,而是地区标识符,对应的码点是U+1F1E6~U+1F1FF,可以随意复制并组合,如果是合法的组合会显示成一个国家的旗帜。你可以在 MacOS 的 FontBook 中打开 Apple Color Emoji 查看到这些码点以及各个地区的旗帜符号
image.png image.png
完整地区标识符如下,你可以动手组合试一试:
🇦 🇧 🇨 🇩 🇪 🇫 🇬 🇭 🇮 🇯 🇰 🇱 🇲 🇳 🇴 🇵 🇶 🇷 🇸 🇹 🇺 🇻 🇼 🇽 🇾 🇿


标签序列


在 Unicode 中称作 Emoji Tag Sequence。在 Unicode 中U+E0020~ U+E007F 95 个码点表示的是Unicode 中不可见的控制符号,其中从E0061~E007A的 26 个码点分别表示小写的拉丁字符(不是常规的拉丁字母,而是 emoji 相关的控制字符),对应关系如下:




  • U+E0061 - TAG LATIN SMALL LETTER A

  • U+E0062 - TAG LATIN SMALL LETTER B



...




  • U+E007A - TAG LATIN SMALL LETTER Z



前文的双字母连字机制支持将两个地区标识符连接在一起表示一个旗帜符号。标签序列与之类似,是 Unicode 中定义的一种更复杂的连接方式,格式是:基础emoji+ 一串拉丁标签字符(U+E0061~U+E007A) + 结束符(U+E007F)
如:🏴 + gbeng + U+E007F = 🏴󠁧󠁢󠁥󠁮󠁧󠁿
其中 🏴 是基础 emoji U+1F3F4,_gbeng _分别代表对应的拉丁控制字符: g(U+E0067)b(U+E0062)e(U+E0065) n(U+E006E)g(U+E0067)U+E007F表示结束符,全称是 TAG CANCEL


/**
* 根据地区缩写返回对应的emoji
* 如:flag('gbeng') -> 🏴󠁧󠁢󠁥󠁮󠁧󠁿
*/

function flag(letterStr) {
const BASE_FLAG = '🏴';
const TAG_CANCEL = String.fromCodePoint(0xE007F);

// 将普通字母字符序列转换为"标签拉丁字符"序列
const tagLatinStr = (letterStr.toLowerCase().split('').map(letter => {
const codePoint = letter.charCodeAt(0) - 'a'.charCodeAt(0) + 0xE0061;
return String.fromCodePoint(codePoint);
})).join('');


return BASE_FLAG + tagLatinStr + TAG_CANCEL;
}

目前用这种方式表示的 emoji 共有三个



  • 🏴 + gbeng + U+E007F = 🏴󠁧󠁢󠁥󠁮󠁧󠁿 英格兰旗帜,完整序列:1F3F4 E0067 E0062 E0065 E006E E0067 E007F

  • 🏴 + gbsct + U+E007F = 🏴󠁧󠁢󠁳󠁣󠁴󠁿 苏格兰旗帜,完整序列:1F3F4 E0067 E0062 E0073 E0063 E0074 E007F

  • 🏴 + gbwls + U+E007F = 🏴󠁧󠁢󠁷󠁬󠁳󠁿 威尔士旗帜,完整序列:1F3F4 E0067 E0062 E0077 E006C E0073 E007F


键位符


共有 12 个键位符 #️⃣ *️⃣ 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣,规则是这样的:井号、星号和数字,加 U+FE0F 变成 emoji,再加上U+20E3变成带方框的键位符。







      • FE0F + 20E3 = *️⃣






  • + FE0F + 20E3 = #️⃣



  • 0 + FE0F + 20E3 = 0️⃣

  • ...


U+FE0F是前文提到的变体选择符中的VS-16,表示显示为 emoji 形态。JavaScript 中'\u0030'表示数字'0', '\u0030\ufe0f'则表示它的 emoji 变体,两者在 zsh 的 console 中显示效果不同,.length的值也不同。
image.png image.png


小结


一共有七种 emoji 造字法



  1. 基础emoji,单个码点表示一个emoji 🧛 U+1F9DB

  2. 单个码点 + 变体选择符 ⚛️ = ⚛︎ U+26A0 + U+FE0F

  3. 皮肤修饰符 🤵🏽 = 🤵 U+1F935 + 🏽 U+1F3FD

  4. **ZWJ连接符 ** 👨‍💻 = 👨 + ZWJ + 💻

  5. 旗帜符号 🇨🇳 = 🇨 + 🇳

  6. **标签序列 ** 🏴󠁧󠁢󠁳󠁣󠁴󠁿 = 🏴 + gbsct + U+E007F

  7. **键位序列 ** *️⃣ = * + U+FE0F + U+20E3


前四种方法也可以组合使用,可构造非常复杂的 emoji



U+1F6B5 🚵 个人山地骑行



  • U+1F3FB 浅色皮肤

  • U+200D ZWJ

  • U+2640 ♀️女性标志

  • U+FE0F 变体标志
    = 🚵🏻‍♀️ 浅色皮肤的女性山地骑行



/**
* 显示一个字符种所有的Unicode码点
*/

function codePoints(str) {
const result = [];
for(let i = 0; i < str.length; i ++) {
result.push(str.codePointAt(i).toString(16).toUpperCase());
}
return result;
}
codePoints('🚵🏻‍♀️') => ['1F6B5', 'DEB5', '1F3FB', 'DFFB', '200D', '2640', 'FE0F']

如何在代码中正确处理 emoji?


emoji 引入的问题


'中国人123'.length = 6'工作中👨‍💻'.length = 8
emoji 给编程带来的主要问题是视觉上看到的字符长度(后文称作视觉 length)与代码中获取的长度(后文称作技术 length)不相同,使得字符串截取等操作返回非预期内的结果,如:
'工作中👨‍💻'.substr(0,5) => '工作中👨''工作中👨‍💻'.substr(5)' => '‍💻'


本质上在 emoji 出现之前 Unicode 编码就遇到了该问题,只不过 emoji 的普及让该问题更普遍。有的 emoji 长度为 1,有的长度可以达到 15。问题的根源是 Unicode 中可以用多个码点表示一个 emoji,如果所有 emoji 都用一个 Unicode 码点表示就不存在该问题。
image.png


解法:视觉 length VS. 技术 length


解法显而易见,只要能将字符串中所有的字符元素按照视觉上看到的情况准确拆分,即:准确拆解字符串中的所有字素集
下述伪代码是要实现的效果,很多开源工具库就在做同样的事情,搜:Grapheme Cluster 即可。找到一个JavaScript版的grapheme-splitter,但是数据已经过时(勿用)。


const vs = new VisualString('工作中👨‍💻');
// vs.length => 4; // 视觉长度
// vs.physicalLength => 8; // 字符串长度
// vs[0] => 工
// vs[3] => 👨‍💻 // 按照所见即所得的方式拆分字符

// 字素集方法
// vs.substr(3,1) => 👨‍💻 // 截取字符

// 字素集属性
// vs[3].physicalLength => 5 // 物理长度
// vs[3].isEmoji => true // 是否是emoji

我们将产出工具库中将要提供这些能力



  1. 判断一个字符串中是否包含 emoji

  2. 将一个字符串准确拆分成若干个字素集

    • 每个字素集包含这些属性:isEmojiphysicalLength



  3. 按照字素集对字符串做截取操作

    • 基础截取: new VisualString('👨123👨‍💻').substr(1, 4) => '123👨‍💻'

    • 限定物理长度截取:new VisualString('👨123👨‍💻').substr(1, 4, 6) => '123',最后一个参数6代表最大物理长度,其中'123👨‍💻'.length = 8,如果限定最大物理长度6则只能截取到'123'备注:在产品体验上我们遵循“所见即所得”,但是在后端系统中传输和存储时候要遵循物理长度的限制,因此需要提供限定物理长度的截取能力。




版本兼容问题


如果 A 向 B 发送了一个组合 emoji「工作👨‍💻123」,B 的系统或软件中版本低(兼容的 Unicode 版本低)不支持该组合 emoji,看到的可能会是「工作👨💻123」。
用看到的是👨‍💻还是👨💻取决于用户的操作系统、软件和字体,我们提供的 JS 库无法感知到用户最终看到的是什么。我们提供的 JS 库会按照最新 Unicode 规范实现,无论用户看到的是什么都会把它当成一个字符(准确地说是字素集),即:
const vs = new VisualString('工作👨💻123'); vs.length => 6; vs[2] => '👨💻'
有办法可以一定程度上解决上述问题,但是我们觉得可能不解决才是正确的做法。


一个彩蛋


最后希望你使用 emoji 愉快 😄
发现 emoji 的维护者彻底贯彻「众生平等」,除了推出了不同肤色的 emoji 外,竟还设计了一个 Pregnant Man :)
image.png 🤰🫃🫄🏼
以上是分别是 woman、man、person,emoji 的新趋势是设计中性的 emoji




参考



作者:支付宝体验科技
来源:juejin.cn/post/7225074892357173308
收起阅读 »

一个神奇的小工具,让URL地址都变成了"ooooooooo"

web
发现一个很有创意的小工具网站,如封面图所示功能很简单,就是将一个URL地址转换为都是 ooooooooo 的样子,通过转换后的地址访问可以转换回到原始地址,简单流程如下图所示。转换的逻辑有点像短链平台一样,只不过这个是将你的URL地址变的很长长长长,但是看着都...
继续阅读 »

发现一个很有创意的小工具网站,如封面图所示功能很简单,就是将一个URL地址转换为都是 ooooooooo 的样子,通过转换后的地址访问可以转换回到原始地址,简单流程如下图所示。转换的逻辑有点像短链平台一样,只不过这个是将你的URL地址变的很长长长长,但是看着都是 ooooooooo,很好奇是如何实现的,所以查阅了源码,本文解读其核心实现逻辑,很有趣且巧妙的实现了这个功能。



前置知识点


在正式开始前,先了解一些需要学习的知识点。因为涉及到两个地址其实也就是字符串之间的转换,会用到一些编码和解码的能力。


将字符转为utf8数组,转换后的每个字符都有一个特定的唯一数值,比如 http 转换后的 utf8 格式数组即是 [104, 116, 116, 112]


    toUTF8Array(str) {
var utf8 = [];
for (var i = 0; i < str.length; i++) {
var charcode = str.charCodeAt(i);
if (charcode < 0x80) utf8.push(charcode);
else if (charcode < 0x800) {
utf8.push(0xc0 | (charcode >> 6),
0x80 | (charcode & 0x3f));
}
else if (charcode < 0xd800 || charcode >= 0xe000) {
utf8.push(0xe0 | (charcode >> 12),
0x80 | ((charcode >> 6) & 0x3f),
0x80 | (charcode & 0x3f));
}
else {
i++;
charcode = ((charcode & 0x3ff) << 10) | (str.charCodeAt(i) & 0x3ff)
utf8.push(0xf0 | (charcode >> 18),
0x80 | ((charcode >> 12) & 0x3f),
0x80 | ((charcode >> 6) & 0x3f),
0x80 | (charcode & 0x3f));
}
}
console.log(utf8, 'utf8');
return utf8;
}

上面是编码,对应下面的则是解码,将utf8数组转换为字符串,比如 [99, 111, 109] 转换后的 utf8 格式数组即是 com


    Utf8ArrayToStr(array) {
var out, i, len, c;
var char2, char3;

out = "";
len = array.length;
i = 0;
while (i < len) {
c = array[i++];
switch (c >> 4) {
case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
// 0xxxxxxx
out += String.fromCharCode(c);
break;
case 12: case 13:
// 110x xxxx 10xx xxxx
char2 = array[i++];
out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F));
break;
case 14:
// 1110 xxxx 10xx xxxx 10xx xxxx
char2 = array[i++];
char3 = array[i++];
out += String.fromCharCode(((c & 0x0F) << 12) |
((char2 & 0x3F) << 6) |
((char3 & 0x3F) << 0));
break;
}
}

return out;
}

将 Number 对象以 4 进制的形式表示为字符串,toString 用的比较多,但是里面传入参数的场景比较少,这个参数 radix 是一个可选的参数,用于指定转换的进制数,范围为 2 ~ 36,如果未传入该参数,则默认使用 10 进制。


n.toString(4)

在字符串左侧填充指定字符,直到字符串达到指定长度。基本语法为 str.padStart(targetLength [, padString])



  • targetLength:必需,指定期望字符串的最小长度,如果当前字符串小于这个长度,则会在左侧使用 padString 进行填充,直到字符串达到指定长度。

  • padString:可选,指定用于填充字符串的字符,默认为 " "(空格)。


str.padStart(4, '0')

URL 编码/解码


下面正式开始URL编码的逻辑,核心的逻辑如下:



  • 转换为utf8数组

  • 转换为4进制并左侧补0到4位数

  • 分割转换为字符串数组

  • 映射到o的不同形式

  • 再次拼接为字符串,即转换完成后的URL


// 获取utf8数组
let unversioned = this.toUTF8Array(url)
// 转换为base 4字符串
// padstart非常重要!否则会丢失前导0
.map(n => n.toString(4).padStart(4, "0"))
// 转换为字符数组
.join("").split("")
// 映射到o的不同形式
.map(x => this.enc[parseInt(x)])
// 连接成单个字符串
.join("")

上面有两个关键点解释一下,首先映射到o的不同形式这个是什么意思呢?其实转换后的o并不是一种“o”,而是4种,只不过我们肉眼看到的效果很像,通过 encodeURI 转换后的字符可以看出来。


encodeURI('o-ο-о-ᴏ')
// o-%CE%BF-%D0%BE-%E1%B4%8F

这里其实也解释了为什么上面为什么是转换为4进制和左侧补0到四位数。因为上面代码定义的 this.enc 如下,因为总共只有四种“o”,4进制只会产生0,1,2,3,这样就可以将转换后的utf8字符一一对应上这几种特殊的“o”。


enc = ["o", "ο", "о", "ᴏ"] 

最后的效果举例转换 http 这个字符:



  • 转换为utf8数组:[ 104, 116, 116, 112 ]

  • 转换为4进制并左侧补0到4位数:['1220', '1310', '1310', '1300']

  • 分割转换为字符串数组:['1', '2', '2', '0', '1', '3', '1', '0', '1', '3', '1', '0', '1', '3', '0', '0']

  • 映射到o的不同形式:[ 'ο', 'о', 'о', 'o', 'ο', 'ᴏ', 'ο', 'o', 'ο', 'ᴏ', 'ο', 'o', 'ο', 'ᴏ', 'o', 'o' ]

  • 再次拼接为字符串,即转换完成后的URL:οооoοᴏοoοᴏοoοᴏoo


到此整个转换编码的过程就结束了,看完后是不是觉得设计的很不错,编码完后就是解码,解码就是将上面的过程倒序来一遍,恢复到最原始的URL地址。这里要注意一点的是每次解析4个字符且parseInt以4进制的方式进行解析。


// 获取url的base 4字符串表示
let b4str = ooo.split("").map(x => this.dec[x]).join("")

let utf8arr = []
// 每次解析4个字符
// 记住添加前导0的填充
for (let i = 0; i < b4str.length; i += 4)
utf8arr.push(parseInt(b4str.substring(i, i + 4), 4))
// 返回解码后的字符串
return this.Utf8ArrayToStr(utf8arr)

最后


到此就核心实现代码就分享结束了,看完是不是感觉并没有很复杂,基于此设计或许可以延伸出其他的字符效果,有兴趣的也可以试试看。将转码后的地址分享给你的朋友们一定会带来不一样的惊喜。


以下将官网源码运行在码上掘金,方便大家体验。



下面是我转换的一个AI小工具地址,点击看看效果吧~


ooooooooooooooooooooooo.ooo/ooooοооoοᴏο…


看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~


专注前端开发,分享前端相关技术干货,公众号:南城大前端(ID: nanchengfe)


作者:南城FE
来源:juejin.cn/post/7225573912670191677
收起阅读 »

十分钟,带你了解 Vue3 的新写法

web
最近因为项目需要,不得不学习一下 Vue3。于是花了 4 个小时,把 Vue3 过了一遍。现在我来带你快速了解 Vue3 的写法。 本文的目的,是为了让已经有 Vue2 开发经验的 人 ,快速掌握 Vue3 的写法。 因此, 本篇假定你已经掌握 Vue 的核心...
继续阅读 »

最近因为项目需要,不得不学习一下 Vue3。于是花了 4 个小时,把 Vue3 过了一遍。现在我来带你快速了解 Vue3 的写法。


本文的目的,是为了让已经有 Vue2 开发经验的 ,快速掌握 Vue3 的写法。


因此, 本篇假定你已经掌握 Vue 的核心内容 ,只为你介绍编写 Vue3 代码,需要了解的内容。


一、Vue3 里 script 的三种写法


首先,Vue3 新增了一个叫做组合式 api 的东西,英文名叫 Composition API。因此 Vue3 的 script 现在支持三种写法,


1、最基本的 Vue2 写法


<template>
<div>{{ count }}</div>
<button @click="onClick">
增加 1
</button>
</template>

<script>
export default {
data() {
return {
count: 1,
};
},
methods: {
onClick() {
this.count += 1;
},
},
}
</script>

2、setup() 属性


<template>
<div>{{ count }}</div>
<button @click="onClick">
增加 1
</button>
</template>

<script>
import { ref } from 'vue';
export default {

// 注意这部分
setup() {
let count = ref(1);
const onClick = () => {
count.value += 1;
};
return {
count,
onClick,
};
},

}
</script>

3、<script setup>


<template>
<div>{{ count }}</div>
<button @click="onClick">
增加 1
</button>
</template>

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

const count = ref(1);
const onClick = () => {
count.value += 1;
};
</script>

正如你看到的那样,无论是代码行数,还是代码的精简度,<script setup> 的方式是最简单的形式。


如果你对 Vue 很熟悉,那么,我推荐你使用 <script setup> 的方式。


这种写法,让 Vue3 成了我最喜欢的前端框架。


如果你还是前端新人,那么,我推荐你先学习第一种写法。


因为第一种写法的学习负担更小,先学第一种方式,掌握最基本的 Vue 用法,然后再根据我这篇文章,快速掌握 Vue3 里最需要关心的内容。


第一种写法,跟过去 Vue2 的写法是一样的,所以我们不过多介绍。


第二种写法,所有的对象和方法都需要 return 才能使用,太啰嗦。除了旧项目,可以用这种方式体验 Vue3 的新特性以外,我个人不建议了解这种方式。反正我自己暂时不打算精进这部分。


所以,接下来,我们主要介绍的,也就是 <script setup> ,这种写法里需要了解的内容。


注意: <script setup> 本质上是第二种写法的语法糖,掌握了这种写法,其实第二种写法也基本上就会了。(又多了一个不学第二种写法的理由)。


二、如何使用 <script setup> 编写组件


学习 Vue3 并不代表你需要新学习一个技术,Vue3 的底层开发思想,跟 Vue2 是没有差别的。


V3 和 V2 的区别就像是,你用不同的语言或者方言说同一句话。


所以我们需要关心的,就是 Vue2 里的内容,怎么用 Vue3 的方式写出来。


1、data——唯一需要注意的地方


整个 data 这一部分的内容,你只需要记住下面这一点。


以前在 data 中创建的属性,现在全都用 ref() 声明。


template 中直接用,在 script 中记得加 .value


在开头,我就已经写了一个简单的例子,我们直接拿过来做对比。


1)写法对比


 // Vue2 的写法

<template>
<div>{{ count }}</div>
<button @click="onClick">
增加 1
</button>
</template>

<script>
export default {
data() {
return {
count: 1,
};
},
methods: {
onClick() {
this.count += 1;
},
},
}
</script>

 // Vue3 的写法

<template>
<div>{{ count }}</div>
<button @click="onClick">
增加 1
</button>
</template>

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

// 用这种方式声明
const count = ref(1);

const onClick = () => {
// 使用的时候记得 .value
count.value += 1;
};
</script>

2)注意事项——组合式 api 的心智负担


a、ref 和 reactive

Vue3 里,还提供了一个叫做 reactiveapi


但是我的建议是,你不需要关心它。绝大多数场景下,ref 都够用了。


b、什么时候用 ref() 包裹,什么时候不用。

要不要用ref,就看你的这个变量的值改变了以后,页面要不要跟着变。


当然,你可以完全不需要关心这一点,跟过去写 data 一样就行。


只不过这样做,你在使用的时候,需要一直 .value


c、不要解构使用

在使用时,不要像下面这样去写,会丢失响应性。


也就是会出现更新了值,但是页面没有更新的情况


// Vue3 的写法
<template>
<div>{{ count }}</div>
<button @click="onClick">
增加 1
</button>
</template>

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

const count = ref(1);
const onClick = () => {
// 不要这样写!!
const { value } = count;
value += 1;
};
</script>

注意: 学习 Vue3 就需要考虑像这样的内容,徒增了学习成本。实际上这些心智负担,在学习的过程中,是可以完全不需要考虑的。


这也是为什么我推荐新人先学习 Vue2 的写法。


2、methods


声明事件方法,我们只需要在 script 标签里,创建一个方法对象即可。


剩下的在 Vue2 里是怎么写的,Vue3 是同样的写法。


// Vue2 的写法
<template>
<div @click="onClick">
这是一个div
</div>
</template>

<script>
export default {
methods: {
onClick() {
console.log('clicked')
},
},
}
</script>

// Vue3 的写法
<template>
<div @click="onClick">
这是一个div
</div>
</template>

<script setup>

// 注意这部分
const onClick = () => {
console.log('clicked')
}

</script>

3、props


声明 props 我们可以用 defineProps(),具体写法,我们看代码。


1)写法对比


// Vue2 的写法
<template>
<div>{{ foo }}</div>
</template>

<script>
export default {
props: {
foo: String,
},
created() {
console.log(this.foo);
},
}
</script>

// Vue3 的写法
<template>
<div>{{ foo }}</div>
</template>

<script setup>

// 注意这里
const props = defineProps({
foo: String
})

// 在 script 标签里使用
console.log(props.foo)
</script>

2)注意事项——组合式 api 的心智负担


使用 props 时,同样注意不要使用解构的方式。


<script setup>
const props = defineProps({
foo: String
})

// 不要这样写
const { foo } = props;
console.log(foo)
</script>

4、emits 事件


props 相同,声明 emits 我们可以用 defineEmits(),具体写法,我们看代码。


// Vue2 的写法
<template>
<div @click="onClick">
这是一个div
</div>
</template>

<script>
export default {

emits: ['click'], // 注意这里
methods: {
onClick() {
this.$emit('click'); // 注意这里
},
},

}
</script>

// Vue3 的写法
<template>
<div @click="onClick">
这是一个div
</div>
</template>

<script setup>

// 注意这里
const emit = defineEmits(['click']);

const onClick = () => {
emit('click') // 注意这里
}

</script>

5、computed


直接上写法对比。


// Vue2 的写法
<template>
<div>
<span>{{ value }}</span>
<span>{{ reversedValue }}</span>
</div>
</template>

<script>
export default {
data() {
return {
value: 'this is a value',
};
},
computed: {
reversedValue() {
return value
.split('').reverse().join('');
},
},
}
</script>

// Vue3 的写法
<template>
<div>
<span>{{ value }}</span>
<span>{{ reversedValue }}</span>
</div>
</template>

<script setup>
import {ref, computed} from 'vue'
const value = ref('this is a value')

// 注意这里
const reversedValue = computed(() => {
// 使用 ref 需要 .value
return value.value
.split('').reverse().join('');
})

</script>

6、watch


这一部分,我们需要注意一下了,Vue3 中,watch 有两种写法。一种是直接使用 watch,还有一种是使用 watchEffect


两种写法的区别是:




  • watch 需要你明确指定依赖的变量,才能做到监听效果。




  • watchEffect 会根据你使用的变量,自动的实现监听效果。




1)直接使用 watch


// Vue2 的写法
<template>
<div>{{ count }}</div>
<div>{{ anotherCount }}</div>
<button @click="onClick">
增加 1
</button>
</template>

<script>
export default {
data() {
return {
count: 1,
anotherCount: 0,
};
},
methods: {
onClick() {
this.count += 1;
},
},
watch: {
count(newValue) {
this.anotherCount = newValue - 1;
},
},
}
</script>

// Vue3 的写法
<template>
<div>{{ count }}</div>
<div>{{ anotherCount }}</div>
<button @click="onClick">
增加 1
</button>
</template>

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

const count = ref(1);
const onClick = () => {
count.value += 1;
};

const anotherCount = ref(0);

// 注意这里
// 需要在这里,
// 明确指定依赖的是 count 这个变量
watch(count, (newValue) => {
anotherCount.value = newValue - 1;
})

</script>

2)使用 watchEffect


// Vue2 的写法
<template>
<div>{{ count }}</div>
<div>{{ anotherCount }}</div>
<button @click="onClick">
增加 1
</button>
</template>

<script>
export default {
data() {
return {
count: 1,
anotherCount: 0,
};
},
methods: {
onClick() {
this.count += 1;
},
},
watch: {
count(newValue) {
this.anotherCount = newValue - 1;
},
},
}
</script>

// Vue3 的写法
<template>
<div>{{ count }}</div>
<div>{{ anotherCount }}</div>
<button @click="onClick">
增加 1
</button>
</template>

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

const count = ref(1);
const onClick = () => {
count.value += 1;
};

const anotherCount = ref(0);

// 注意这里
watchEffect(() => {
// 会自动根据 count.value 的变化,
// 触发下面的操作
anotherCount.value = count.value - 1;
})

</script>

7、生命周期


Vue3 里,除了将两个 destroy 相关的钩子,改成了 unmount,剩下的需要注意的,就是在 <script setup> 中,不能使用 beforeCreatecreated 两个钩子。


如果你熟悉相关的生命周期,只需要记得在 setup 里,用 on 开头,加上大写首字母就行。


// 选项式 api 写法
<template>
<div></div>
</template>

<script>
export default {
beforeCreate() {},
created() {},

beforeMount() {},
mounted() {},

beforeUpdate() {},
updated() {},

// Vue2 里叫 beforeDestroy
beforeUnmount() {},
// Vue2 里叫 destroyed
unmounted() {},

// 其他钩子不常用,所以不列了。
}
</script>

// 组合式 api 写法
<template>
<div></div>
</template>


<script setup>
import {
onBeforeMount,
onMounted,

onBeforeUpdate,
onUpdated,

onBeforeUnmount,
onUnmounted,
} from 'vue'

onBeforeMount(() => {})
onMounted(() => {})

onBeforeUpdate(() => {})
onUpdated(() => {})

onBeforeUnmount(() => {})
onUnmounted(() => {})
</script>

三、结语


好了,对于快速上手 Vue3 来说,以上内容基本已经足够了。


这篇文章本身不能做到帮你理解所有 Vue3 的内容,但是能帮你快速掌握 Vue3 的写法。


如果想做到对 Vue3 的整个内容心里有数,还需要你自己多看看 V

作者:Wetoria
来源:juejin.cn/post/7225267685763907621
ue3 的官方文档。

收起阅读 »

九个超级好用的 Javascript 技巧

web
作者:shichuan 文末彩蛋等你揭晓 🤫 前言 在实际的开发工作过程中,积累了一些常见又超级好用的 Javascript 技巧和代码片段,包括整理的其他大神的 JS 使用技巧,今天筛选了 9 个,以供大家参考。 1、动态加载 JS 文件 在一些特殊的场景...
继续阅读 »

作者:shichuan


文末彩蛋等你揭晓 🤫



前言


在实际的开发工作过程中,积累了一些常见又超级好用的 Javascript 技巧和代码片段,包括整理的其他大神的 JS 使用技巧,今天筛选了 9 个,以供大家参考。


1、动态加载 JS 文件


在一些特殊的场景下,特别是一些库和框架的开发中,我们有时会去动态的加载 JS 文件并执行,下面是利用 Promise 进行了简单的封装。


function loadJS(files, done) {
// 获取head标签
const head = document.getElementsByTagName('head')[0];
Promise.all(files.map(file => {
return new Promise(resolve => {
// 创建script标签并添加到head
const s = document.createElement('script');
s.type = "text/javascript";
s.async = true;
s.src = file;
// 监听load事件,如果加载完成则resolve
s.addEventListener('load', (e) => resolve(), false);
head.appendChild(s);
});
})).then(done); // 所有均完成,执行用户的回调事件
}

loadJS(["test1.js", "test2.js"], () => {
// 用户的回调逻辑
});

上面代码核心有两点,一是利用 Promise 处理异步的逻辑,而是利用 script 标签进行 js 的加载并执行。


2、实现模板引擎


下面示例用了极少的代码实现了动态的模板渲染引擎,不仅支持普通的动态变量的替换,还支持包含 for 循环,if 判断等的动态的 JS 语法逻辑,具体实现逻辑在笔者另外一篇文章《面试官问:你能手写一个模版引擎吗?》做了非常详详尽的说明,感兴趣的小伙伴可自行阅读。


// 这是包含了js代码的动态模板
var template =
'My avorite sports:' +
'<%if(this.showSports) {%>' +
'<% for(var index in this.sports) { %>' +
'<a><%this.sports[index]%></a>' +
'<%}%>' +
'<%} else {%>' +
'<p>none</p>' +
'<%}%>';
// 这是我们要拼接的函数字符串
const code = `with(obj) {
var r=[];
r.push("My avorite sports:");
if(this.showSports) {
for(var index in this.sports) {
r.push("<a>");
r.push(this.sports[index]);
r.push("</a>");
}
} else {
r.push("<span>none</span>");
}
return r.join("");
}`

// 动态渲染的数据
const options = {
sports: ["swimming", "basketball", "football"],
showSports: true
}
// 构建可行的函数并传入参数,改变函数执行时this的指向
result = new Function("obj", code).apply(options, [options]);
console.log(result);

3、利用 reduce 进行数据结构的转换


有时候前端需要对后端传来的数据进行转换,以适配前端的业务逻辑,或者对组件的数据格式进行转换再传给后端进行处理,而 reduce 是一个非常强大的工具。


const arr = [
{ classId: "1", name: "张三", age: 16 },
{ classId: "1", name: "李四", age: 15 },
{ classId: "2", name: "王五", age: 16 },
{ classId: "3", name: "赵六", age: 15 },
{ classId: "2", name: "孔七", age: 16 }
];

groupArrayByKey(arr, "classId");

function groupArrayByKey(arr = [], key) {
return arr.reduce((t, v) => (!t[v[key]] && (t[v[key]] = []), t[v[key]].push(v), t), {})
}

很多很复杂的逻辑如果用 reduce 去处理,都非常的简洁。


4、添加默认值


有时候一个方法需要用户传入一个参数,通常情况下我们有两种处理方式,如果用户不传,我们通常会给一个默认值,亦或是用户必须要传一个参数,不传直接抛错。


function double() {
return value *2
}

// 不传的话给一个默认值0
function double(value = 0) {
return value * 2
}

// 用户必须要传一个参数,不传参数就抛出一个错误

const required = () => {
throw new Error("This function requires one parameter.")
}
function double(value = required()) {
return value * 2
}

double(3) // 6
double() // throw Error

listen 方法用来创建一个 NodeJS 的原生 http 服务并监听端口,在服务的回调函数中创建 context,然后调用用户注册的回调函数并传递生成的 context。下面我们以前看下 createContext 和 handleRequest 的实现。


5、函数只执行一次


有些情况下我们有一些特殊的场景,某一个函数只允许执行一次,或者绑定的某一个方法只允许执行一次。


export function once (fn) {
// 利用闭包判断函数是否执行过
let called = false
return function () {
if (!called) {
called = true
fn.apply(this, arguments)
}
}
}

6、实现 Curring


JavaScript 的柯里化是指将接受多个参数的函数转换为一系列只接受一个参数的函数的过程。这样可以更加灵活地使用函数,减少重复代码,并增加代码的可读性。


function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
};
}
};
}

function add(x, y) {
return x + y;
}

const curriedAdd = curry(add);

console.log(curriedAdd(1)(2)); // 输出 3
console.log(curriedAdd(1, 2)); // 输出 3

通过柯里化,我们可以将一些常见的功能模块化,例如验证、缓存等等。这样可以提高代码的可维护性和可读性,减少出错的机会。


7、实现单例模式


JavaScript 的单例模式是一种常用的设计模式,它可以确保一个类只有一个实例,并提供对该实例的全局访问点,在 JS 中有广泛的应用场景,如购物车,缓存对象,全局的状态管理等等。


let cache;
class A {
// ...
}

function getInstance() {
if (cache) return cache;
return cache = new A();
}

const x = getInstance();
const y = getInstance();

console.log(x === y); // true

8、实现 CommonJs 规范


CommonJS 规范的核心思想是将每个文件都看作一个模块,每个模块都有自己的作用域,其中的变量、函数和对象都是私有的,不能被外部访问。要访问模块中的数据,必须通过导出(exports)和导入(require)的方式。


// id:完整的文件名
const path = require('path');
const fs = require('fs');
function Module(id){
// 用来唯一标识模块
this.id = id;
// 用来导出模块的属性和方法
this.exports = {};
}

function myRequire(filePath) {
// 直接调用Module的静态方法进行文件的加载
return Module._load(filePath);
}

Module._cache = {};
Module._load = function(filePath) {
// 首先通过用户传入的filePath寻址文件的绝对路径
// 因为再CommnJS中,模块的唯一标识是文件的绝对路径
const realPath = Module._resoleveFilename(filePath);
// 缓存优先,如果缓存中存在即直接返回模块的exports属性
let cacheModule = Module._cache[realPath];
if(cacheModule) return cacheModule.exports;
// 如果第一次加载,需要new一个模块,参数是文件的绝对路径
let module = new Module(realPath);
// 调用模块的load方法去编译模块
module.load(realPath);
return module.exports;
}

// node文件暂不讨论
Module._extensions = {
// 对js文件处理
".js": handleJS,
// 对json文件处理
".json": handleJSON
}

function handleJSON(module) {
// 如果是json文件,直接用fs.readFileSync进行读取,
// 然后用JSON.parse进行转化,直接返回即可
const json = fs.readFileSync(module.id, 'utf-8')
module.exports = JSON.parse(json)
}

function handleJS(module) {
const js = fs.readFileSync(module.id, 'utf-8')
let fn = new Function('exports', 'myRequire', 'module', '__filename', '__dirname', js)
let exports = module.exports;
// 组装后的函数直接执行即可
fn.call(exports, exports, myRequire, module,module.id,path.dirname(module.id))
}

Module._resolveFilename = function (filePath) {
// 拼接绝对路径,然后去查找,存在即返回
let absPath = path.resolve(__dirname, filePath);
let exists = fs.existsSync(absPath);
if (exists) return absPath;
// 如果不存在,依次拼接.js,.json,.node进行尝试
let keys = Object.keys(Module._extensions);
for (let i = 0; i < keys.length; i++) {
let currentPath = absPath + keys[i];
if (fs.existsSync(currentPath)) return currentPath;
}
};

Module.prototype.load = function(realPath) {
// 获取文件扩展名,交由相对应的方法进行处理
let extname = path.extname(realPath)
Module._extensions[extname](this)
}

上面对 CommonJs 规范进行了简单的实现,核心解决了作用域的隔离,并提供了 Myrequire 方法进行方法和属性的加载,对于上面的实现,笔者专门有一篇文章《38 行代码带你实现 CommonJS 规范》进行了详细的说明,感兴趣的小伙伴可自行阅读。


9、递归获取对象属性


如果让我挑选一个用的最广泛的设计模式,我会选观察者模式,如果让我挑一个我所遇到的最多的算法思维,那肯定是递归,递归通过将原始问题分割为结构相同的子问题,然后依次解决这些子问题,组合子问题的结果最终获得原问题的答案。


const user = {
info: {
name: "张三",
address: { home: "Shaanxi", company: "Xian" },
},
};

// obj是获取属性的对象,path是路径,fallback是默认值
function get(obj, path, fallback) {
const parts = path.split(".");
const key = parts.shift();
if (typeof obj[key] !== "undefined") {
return parts.length > 0 ?
get(obj[key], parts.join("."), fallback) :
obj[key];
}
// 如果没有找到key返回fallback
return fallback;
}

console.log(get(user, "info.name")); // 张三
console.log(get(user, "info.address.home")); // Shaanxi
console.log(get(user, "info.address.company")); // Xian
console.log(get(user, "info.address.abc", "fallback")); // fallback

上面挑选了 9 个笔者认为比较有用的 JS 技巧,希望对大家有所帮助。


🎁 文末彩蛋 >>


码上掘金编程比赛火热进行中,同时为大家推出「报名礼 & 完赛奖」活动~

报名即有机会瓜分上百万掘金矿石奖池!提交作品更可参与精美奖品的抽取哦!


🎁 抽奖攻略请戳这里

🎡 更多大赛特别活动请看这里




尾部关注.gif


扫码关注公众号 👆 追更不迷路


作者:字节前端
来源:juejin.cn/post/7223938976158957624
收起阅读 »

用CSS给健身的女朋友做一个喝水记录本

web
我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第2篇文章,点击查看活动详情 前言 事情是这样的,由于七八月份的晚上时不时就坐在地摊上开始了喝酒撸串的一系列放肆的长肉肉项目。 这不,前段时间女朋友痛下决心(心血来潮)地就去报了一个健身的私教班,按照教练给...
继续阅读 »

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第2篇文章,点击查看活动详情


前言


事情是这样的,由于七八月份的晚上时不时就坐在地摊上开始了喝酒撸串的一系列放肆的长肉肉项目。
这不,前段时间女朋友痛下决心(心血来潮)地就去报了一个健身的私教班,按照教练给的饮食计划中,其中有一项是每天需要喝 2.6L 的水来促进体内的新陈代谢。
作为伴侣肯定要十分支持的呀,不过因为平时工作也是十分费脑筋的,不会专门去记录每天喝了多少水,特别容易忘记。所以做了这个喝水记录本给她。


开发需求


整体的开发需求和前言里描述的差不多,整体功能拆分一下就非常清晰了。


一、定义变量



  1. 大杯子:我们需要一个总量目标,用于定义每天的计划值。

  2. 小杯子:一个单次目标,我们不会一次接一大桶水来喝,即使用小杯子喝水时,每个杯子的刻度值。


二、逻辑整合



  1. 点击每个小杯子时,从大杯子的总量中扣除小杯子的刻度并记录,对应UI水位升高。

  2. 首次点击小杯子时,展示百分率刻度值,提升水位。

  3. 当完成目标值后,隐藏剩余水量的文字。

  4. "清空"按钮,消除本地记录值,恢复UI水位,展示剩余量。


创建流程和主要代码


 此模块代码是应用于小程序使用的,所以代码部分使用wx框架。(下面有普通代码部分)


wxml


构造整体布局,布局和制作大杯子和小杯子。


在上一段开发需求部分中提到的隐藏内容时,注意不要使用 wx:if 直接删除整个标签,这样会导致画面跳动,无法实现动画的平滑过渡。


用三元运算符隐藏文字可以实现较好的过渡


<view class="body">
<text class="h1">喝水记录本</text>
<text class="h3">今日目标: 2.6升 </text>

<view class="cup">
<view class="remained" style="height: {{remainedH}}px">
<text class="span">{{isRemained ? liters : ''}}</text>
<text class="small">{{isRemained ? '剩余' : ''}}</text>
</view>

<view class="percentage" style="{{percentageH}}">{{isPercentage ? percentage : ''}}</view>
</view>

<text class="text">请选择喝水的杯子</text>

<view class="cups">
<view class="cup cup-small" bindtap="cups" data-ml="700">700 ml</view>
<view class="cup cup-small" bindtap="cups" data-ml="400">400 ml</view>
<view class="cup cup-small" bindtap="cups" data-ml="600">600 ml</view>
<view class="cup cup-small" bindtap="cups" data-ml="500">500 ml</view>
<view class="cup cup-small" bindtap="cups" data-ml="50">50 ml</view>
<view class="cup cup-small" bindtap="cups" data-ml="100">100 ml</view>
<view class="cup cup-small" bindtap="cups" data-ml="150">150 ml</view>
<view class="cup cup-small" bindtap="cups" data-ml="300">300 ml</view>
</view>

<view class="cancle" bindtap="update">清空</view>
</view>

wxss


css就是简单的画杯子和布局,值得说的就是往大杯子里加水的动画 transition 一下就可以了


.body {
height: 108vh;
background-color: #3494e4;
color: #fff;
display: flex;
flex-direction: column;
align-items: center;
box-sizing: border-box;
}

.h1 {
margin: 10px 0 0;
}

.h3 {
font-weight: 400;
margin: 10px 0;
}

.cup {
background-color: #fff;
border: 4px solid #144fc6;
color: #144fc6;
border-radius: 0 0 40px 40px;
height: 330px;
width: 150px;
margin: 30px 0;
display: flex;
flex-direction: column;
overflow: hidden;
}

.cup.cup-small {
height: 95px;
width: 50px;
border-radius: 0 0 15px 15px;
background-color: rgba(255, 255, 255, 0.9);
cursor: pointer;
font-size: 14px;
align-items: center;
justify-content: center;
text-align: center;
margin: 5px;
transition: 0.3s ease;
}

.cup.cup-small.full {
background-color: #6ab3f8;
color: #fff;
}

.cups {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
width: 280px;
}

.remained {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
flex: 1;
transition: 0.3s ease;
}

.remained .span {
font-size: 20px;
font-weight: bold;
}

.remained .small {
font-size: 12px;
}

.percentage {
background-color: #6ab3f8;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 30px;
height: 0;
transition: 0.3s ease;
box-sizing: border-box;
}

.text {
text-align: center;
margin: 0 0 5px;
}

.cancle {
cursor: pointer;
}

js


逻辑注释写在了代码中


Page({
data: {
liters: '2.6L',
isPercentage: true,
isRemained: true,
percentage: '',
percentageH: 'height: 0',
RemainedH: 0,
goal: 2600
},

// 每次进入页面后加载记录的值,执行动画
onShow() {
this.setData({ goal: Number(wx.getStorageSync('goal')) })
this.updateBigCup(2600 - this.data.goal)
},

// 点击小杯子时的触发逻辑
cups(data) {
const ml = Number(data.currentTarget.dataset.ml);
const goal = this.data.goal - ml;
const total = 2600 - goal;
this.setData({ goal })
wx.setStorageSync("goal", goal);
this.updateBigCup(total)
},

// 更新 UI 数据
updateBigCup(total) {
const { goal } = this.data;
if (goal != 2600) {
this.setData({
isPercentage: true,
percentage: `${(total / 2600 * 100).toFixed(0)}%`,
percentageH: `height: ${total / 2600 * 330}px`
})
}

if (goal <= 0) {
this.setData({
remainedH: 0,
isRemained: false,
})
} else {
this.setData({
isRemained: true,
liters: `${goal / 1000}L`
})
}
},

// 清空记录值
update() {
wx.removeStorage({ key: 'goal' })
this.setData({
goal: 2600,
isPercentage: false,
isRemained: true,
remainedH: 0,
percentageH: 'height: 0px',
liters: '2.6L'
})
}
})

码上掘金


  上面的代码部分主要用于小程序使用,码上掘金可在网页中使用。



结语


  感谢大家能看到这里!!本篇的代码本身没有什么技术含量,可能是比较会偏向实用性的一篇,对!是有一些的对吧!可以自己改装成Chrome插件使用会更方便更实用。啥?你问我为什么不直接写Chrome插件?有没有一种可能不是我不想,而是😭。


  好啦,如果你身边有健身的朋友也可以给他做

作者:dudoit
来源:juejin.cn/post/7147529288164573192
一个哦~再次谢谢大家

收起阅读 »

制作了一个图片像素风转换器

web
制作了一个图片像素风转换器,可以将图片转换成像素风格,并可转换为 css box-shadow 进行输出。前排先放效果图、转换器地址和 GitHub 地址: 转化器地址:pixel.heyfe.org/ GitHub 地址:github.com/ZxBing...
继续阅读 »

制作了一个图片像素风转换器,可以将图片转换成像素风格,并可转换为 css box-shadow 进行输出。前排先放效果图、转换器地址和 GitHub 地址:


blog-mosaic-converter-44.gif


转化器地址:pixel.heyfe.org/


GitHub 地址:github.com/ZxBing0066/…


转换器功能


转换器会将传入的图片转换为像素风格,并将像素风格的图片以 box-shadow 进行转换,借助 box-shadow,我们可以直接用 css 来渲染该图片,且可以通过 box-shadow 的一些特性来达成一些比较好玩的效果,比如用间隙来加重像素风格:


blog-mosaic-converter-84.png


或者直接将间隙拉到顶,达成类似点阵图的效果:


blog-mosaic-converter-55.png


又或者借助 border-radius,实现圆点图效果:


blog-mosaic-converter-71.png


制作出想要的效果后,可以在右侧点击 复制 box-shadow 样式 按钮复制其样式。


实现原理


关于 box-shadow 实现像素图的原理之前有一篇文章中有提到,这里不再赘述。此处大概说一下图片转换为像素图再转为 box-shadow 的过程。


转换器在拿到图片后,会将图片绘制在一个非常小的画布中,以此来降低图片的精度,然后将画布中绘制的低精度图片进行二次渲染,渲染到较大的画布中,此时由于图片被拉伸,就会形成一定的像素效果。随后为了将像素效果图转换为 box-shadow,转换器会去读取画布中的绘制信息,将其生成为一组二维数组,再根据其中的颜色转换为 box-shadow 中的属性。至此转换器的功能就完成了。


当然其中还有一些细节(浏览器会默认启用平滑绘制导致像素效果消失等问题),本篇不打算细说,会在下篇专门写一篇来讲一下具体实现。


最后


本转换器原先是在码上掘金挑战赛某次文章中构想 ,然后在第二次制作类似效果时干脆使用脚本来完成了,最近有空就将其稍微优化了一下进行开源。目前一些细节还有点欠缺,待改进。


再贴一下地址:


转化器地址:pixel.heyfe.org/


GitHub 地址:github.com/ZxBing0066/…


相关文章



作者:嘿嘿Z
来源:juejin.cn/post/7150465824690536484
收起阅读 »

【记】滑动拼图验证码在搜索中的作用

开头验证码应用于我们生活、工作的方方面面,比如注册登录账号、支付订单、修改密码等。下面我是在一次项目中利用滑动拼图验证码和搜索功能“合作共赢”的记录。验证码展示具体实现前端代码// 引入js<script src="captcha.js?appid=XX...
继续阅读 »

开头

验证码应用于我们生活、工作的方方面面,比如注册登录账号、支付订单、修改密码等。下面我是在一次项目中利用滑动拼图验证码和搜索功能“合作共赢”的记录。

验证码展示



具体实现

前端代码

// 引入js
<script src="captcha.js?appid=XXX"></script>
<script>
kg.captcha({
// 绑定弹窗按钮
button: "#captchaButton",

// 验证成功事务处理
success: function (e) {
// 验证成功,直接提交表单
// form1.submit();
console.log(e);
},

// 验证失败事务处理
failure: function (e) {
console.log(e);
},

// 点击刷新按钮时触发
refresh: function (e) {
console.log(e);
}
});
</script>

<a id="captchaButton"></a>


验证结果说明


 

字段名
数据类型   描述   
 

code
 

number
 

返回code信息
 

msg
 

string
 

验证结果信息
 

rid
 

number
 

用户的验证码应用id
 

sense
 

number
 

是否开启无感验证,0-关闭,1-开启
 

token
 

string
 

验证成功才有:token
 

weight
 

number
 

错误严重性,0正常错误,可以继续操作,1一般错误,刷新/重新加载拼图,2严重错误,错误次数过多拒绝访问


Python代码

from wsgiref.simple_server import make_server
from KgCaptchaSDK import KgCaptcha
def start(environ, response):
# 填写你的 AppId,在应用管理中获取
AppID = "AppId"
# 填写你的 AppSecret,在应用管理中获取
AppSecret = "AppSecret"
request = KgCaptcha(AppID, AppSecret)
# 填写应用服务域名,在应用管理中获取
request.appCdn = "https://cdn.kgcaptcha.com"
# 请求超时时间,秒
request.connectTimeout = 10
# 用户id/登录名/手机号等信息,当安全策略中的防控等级为3时必须填写
request.userId = "kgCaptchaDemo"
# 使用其它 WEB 框架时请删除 request.parse,使用框架提供的方法获取以下相关参数
parseEnviron = request.parse(environ)
# 前端验证成功后颁发的 token,有效期为两分钟
request.token = parseEnviron["post"].get("kgCaptchaToken", "") # 前端 _POST["kgCaptchaToken"]
# 客户端IP地址
request.clientIp = parseEnviron["ip"]
# 客户端浏览器信息
request.clientBrowser = parseEnviron["browser"]
# 来路域名
request.domain = parseEnviron["domain"]
# 发送请求
requestResult = request.sendRequest()
if requestResult.code == 0:
# 验证通过逻辑处理
html = "验证通过"
else:
# 验证失败逻辑处理
html = f"{requestResult.msg} - {requestResult.code}"
response("200 OK", [("Content-type", "text/html; charset=utf-8")])
return [bytes(str(html), encoding="utf-8")]
httpd = make_server("0.0.0.0", 8088, start) # 设置调试端口 http://localhost:8088/
httpd.serve_forever()


最后

SDK开源地址:KgCaptcha (KgCaptcha) · GitHub,顺便做了一个演示:凯格行为验证码在线体验

收起阅读 »

仿抖音左右歪头图片选择

web
在线体验 项目 github 仓库 前一阵子在刷抖音时,看到一个通过左右歪头选择两侧图片的视频,感觉很有趣。顿时想到了 n 年前的face-api.js,那就基于这个来做吧。总体做好后,有很多细节需要改进,不够细腻丝滑。 1. 需求分析 直接开搞吧! ...
继续阅读 »

在线体验


项目 github 仓库


ezgif-4-7883a8f8e5.gif



前一阵子在刷抖音时,看到一个通过左右歪头选择两侧图片的视频,感觉很有趣。顿时想到了 n 年前的face-api.js,那就基于这个来做吧。总体做好后,有很多细节需要改进,不够细腻丝滑。



1. 需求分析


直接开搞吧!



  1. 页面基本布局,左右两侧图片,而且有缩放和移动动画

  2. 需要打开摄像头,获取视频流,通过 video 展现出来

  3. 需要检测人脸是向哪一侧歪头


2. 具体实现


2.1 页面布局和 animation 动画


这个不难,布局好后,就是添加 css 动画,我这里写的很粗糙,不细腻,但勉强能用,例如下面 leftHeartMove 为中间的小爱心向左侧移动动画


.heart {
width: 30px;
height: 30px;
padding: 4px;
box-sizing: border-box;
border-radius: 50%;
background-color: #fff;
position: absolute;
top: -15px;
left: 50%;
transform: translateX(-50%) rotateZ(0deg) scale(1);
animation: leftHeartMove 0.5s linear;
animation-fill-mode: forwards;
z-index: 2;
}

@keyframes leftHeartMove {
from {
top: -15px;
left: 50%;
transform: translateX(-50%) rotateZ(0deg) scale(1);
}

to {
top: 65px;
left: -13%;
transform: translateX(-50%) rotateZ(-15deg) scale(1.2);
}
}

2.2 打开摄像头并显示


注意点



  1. 关于 h5navigator.mediaDevices.getUserMedia 这个 api,本地开发localhost是可以拉起摄像头打开提示的,线上部署必须是https节点才行,http不能唤起打开摄像头


WX20221128-221028@2x.png




  1. 关于获取到视频流后,video视频播放,需要镜面翻转,这个可以通过 css 的transform: rotateY(180deg)来翻转




  2. 关于video播放不能在手机上竖屏全屏,可以给 video 设置 cssobject-fit:cover来充满屏幕




<video id="video" class="video" playsinline autoplay muted></video>

.video {
width: 100%;
height: 100%;
transform: rotateY(180deg);
object-fit: cover;
}


  • 获取摄像头视频流


async getUserMedia() {
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
try {
// https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#examples
const stream = await navigator.mediaDevices.getUserMedia({
audio: false,
video: true,
video: {
facingMode: "user", // 前置摄像头
// facingMode: { exact: "environment" },// 后置摄像头
width: { min: 1280, max: 1920 },
height: { min: 720, max: 1080 },
},
});

return Promise.resolve(stream);
} catch (error) {
return Promise.reject();
}
}

const errorMessage =
"This browser does not support video capture, or this device does not have a camera";
alert(errorMessage);
}


  • video 播放视频流


async openCamera(e) {
try {
const stream = await this.getUserMedia();
this.video.srcObject = stream;
this.video.onloadedmetadata = async () => {
this.video.play();
};
} catch (error) {
console.log(error);
alert("打开摄像头失败");
}
}


  • 关闭视频


async closeCamera() {
// https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack/stop
const tracks = this.video.srcObject.getTracks();

tracks.forEach((track) => {
track.stop();
});

this.video.srcObject.srcObject = null;
}

2.3 检测人脸左右倾斜


landmarks.png


通过face-api.js拿到人脸landmarks特征数据后,可以直接拿到左右眼的数据,分别通过求 Y 轴方向的平均值,然后比较这个平均值,便可以简单得出人脸向左还是向右倾斜,简单吧,角度都不用求了!


<div style="position: relative;width: 100%;height: 100%;">
<video
id="video"
class="video"
playsinline
autoplay
muted
style="object-fit:cover"
>
</video>
<canvas id="overlay" class="overlay"></canvas>
</div>

.video {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 0;
transform: rotateY(180deg);
}

.overlay {
position: absolute;
top: 0;
left: 0;
}


  • 加载模型


import * as faceapi from "face-api.js";

async loadWeight() {
// 加载模型
await faceapi.nets.ssdMobilenetv1.load(
"./static/weights/ssd_mobilenetv1_model-weights_manifest.json"
);
// 加载人脸68特征模型数据
await faceapi.nets.faceLandmark68Net.load(
"./static/weights/face_landmark_68_model-weights_manifest.json"
);
// await faceapi.nets.faceExpressionNet.load(
// "/static/weights/face_expression_model-weights_manifest.json"
// );
// await faceapi.nets.faceRecognitionNet.load(
// "./static/weights/face_recognition_model-weights_manifest.json"
// );
await faceapi.nets.ageGenderNet.load(
"./static/weights/age_gender_model-weights_manifest.json"
);

console.log("模型加载完成");
}


  • 计算人脸左右倾斜


handleFaceLeftOrRight(landmarks) {
const DIFF_NUM = 15; // 偏差
let leftEye = landmarks.getLeftEye(); // 左眼数据
let rightEye = landmarks.getRightEye(); // 右眼数据
// let nose = landmarks.getNose();

let leftEyeSumPoint = leftEye.reduce((prev, cur) => ({
x: prev.x + cur.x,
y: prev.y + cur.y,
}));

let rightEyeSumPoint = rightEye.reduce((prev, cur) => ({
x: prev.x + cur.x,
y: prev.y + cur.y,
}));

// let noseSumPoint = nose.reduce((prev, cur) => ({
// x: prev.x + cur.x,
// y: prev.y + cur.y,
// }));

let leftEyeAvgPoint = {
x: leftEyeSumPoint.x / leftEye.length,
y: leftEyeSumPoint.y / leftEye.length,
};

let rightEyeAvgPoint = {
x: rightEyeSumPoint.x / leftEye.length,
y: rightEyeSumPoint.y / leftEye.length,
};

// let noseAvgPoint = {
// x: noseSumPoint.x / leftEye.length,
// y: noseSumPoint.y / leftEye.length,
// };

// console.log(leftEyeAvgPoint, rightEyeAvgPoint, noseAvgPoint);
let diff = Math.abs(leftEyeAvgPoint.y - rightEyeAvgPoint.y);

return diff > DIFF_NUM
? leftEyeAvgPoint.y > rightEyeAvgPoint.y
? "left"
: "right"
: "center";
}


  • 处理 video 视频


async handleVideoFaceTracking(cb) {
if (this.closed) {
window.cancelAnimationFrame(this.raf);
return;
}

const options = new faceapi.SsdMobilenetv1Options();

let task = faceapi.detectAllFaces(this.video, options);
task = task.withFaceLandmarks().withAgeAndGender();
const results = await task;

// overlay为canvas元素
// video即为video元素
const dims = faceapi.matchDimensions(this.overlay, this.video, true);
const resizedResults = faceapi.resizeResults(results, dims);

// console.log("options==>", options);
// console.log("resizedResults==>", resizedResults);
cb && cb(resizedResults);

this.raf = requestAnimationFrame(() => this.handleVideoFaceTracking(cb));
}

3. 参考资料




  1. face-api.js




  2. getUserMedia MDN




作者:sRect
来源:juejin.cn/post/7171081395551338503
收起阅读 »

假如:a===1 && a===2 && a===3; 那么 a 是什么?

web
前言 文章提供视频版啦,点击直接查看 hello,大家好,我是 sunday。 今天遇到了一个非常有意思的问题,跟大家分享一下。 咱们来看这段代码: a===1 && a===2 && a===3 假设上面的表达式成立,...
继续阅读 »

前言



文章提供视频版啦,点击直接查看



hello,大家好,我是 sunday


今天遇到了一个非常有意思的问题,跟大家分享一下。


咱们来看这段代码:


a===1 && a===2 && a===3 

假设上面的表达式成立,那么问:a 是什么?


正文


ok,我们来说一下这个问题的解答。


想要解决这个问题,那么我们首先要知道 JavaScript 中的类型转换和比较运算符的优先级。


JavaScript 中,表达式的运算顺序是 从左到右。因此,在这个表达式中,先执行 a===1 的比较运算符,如果它返回 false,整个表达式就会返回 false,也就是逻辑中断。


如果 a 的值是 1,则比较运算符返回 true,那么就会继续执行下一个逻辑运算符 &&,接着执行 a===2 的比较运算符,如果它返回 false,则整个表达式返回 false,逻辑中断。


以此类推,以此类推,所以 a 的值应该是动态变化的,并且应该依次为 1、2、3。只有这样才会出现 a===1 && a===2 && a===3; 返回 true 的情况。


那么 如何让 a 的值动态变化,就是咱们解决这个问题的关键。


我们在 一小时读完《JavaScript权威指南(第7版)》上一小时读完《深入理解现代 JavaScript》,彻底掌握 ES6 之后 JavaScript 新特性! 中都讲到过,对象的方法存在 get 标记,一旦方法存在 get 标记,那么我们就可以像调用对象的属性一样,调用这个方法。


那么说到这里,肯定很多小伙伴都想到这个问题怎么解决了。


我们直接来看代码:


 const obj = {
 // get 标记
 get a() {
   this.value = this.value || 1;
   return this.value++;
}
};

console.log(obj.a === 1 && obj.a === 2 && obj.a === 3); // true

在这段代码中,我们创建了一个对象 obj,它包含一个被 get 标记的方法 a。那么此时只要执行 obj.a 就会调用 a 方法,完成 value 自增的操作。从而得到咱们期望的结果。


总结


这是一个非常有意思的问题。除了上面这种方案之后,还有很多其他的实现方案。大家可以开动脑筋,想一想别的方案都有什么呢?


答案留在评论区,咱们

作者:LGD_Sunday
来源:juejin.cn/post/7223586933881421861
一起来讨论下哦~~~

收起阅读 »

浅析小程序蓝牙技术

web
认识蓝牙 蓝牙技术是一种无线数据和语音通信开放的全球规范,它是基于低成本的近距离无线连接,为固定和移动设备建立通信环境的一种特殊的近距离无线技术连接。 传统蓝牙和低功耗蓝牙 根据蓝牙的发展历程,将蓝牙普遍分为两种规格,即传统蓝牙模块(BT) 和低功耗蓝牙模...
继续阅读 »

认识蓝牙



蓝牙技术是一种无线数据和语音通信开放的全球规范,它是基于低成本的近距离无线连接,为固定和移动设备建立通信环境的一种特殊的近距离无线技术连接。



传统蓝牙和低功耗蓝牙


根据蓝牙的发展历程,将蓝牙普遍分为两种规格,即传统蓝牙模块(BT)低功耗蓝牙模块(BLE)。传统蓝牙模块常用在对数据传输带宽有一定要求的场景上。低功耗蓝牙是从蓝牙4.0起支持的协议,特点是耗电极低、传输速度更快,常用在对续航要求较高且只需小数据量传输的各种智能电子产品中。


技术指标经典蓝牙BT低功耗蓝牙BLE
无线电频率2.4GHz2.4GHz
距离10米最大100米
发送数据所需时间100ms<3ms
响应延时约100ms6ms
安全性64/128-bit及用户自定义的应用层128-bitAES及用户自定义的应用层
能耗100%(ref)1%-50%
空中传输数据速率1-3Mb/s1Mb/s
主要用途手机,游戏机,耳机,音箱,汽车和PC等鼠标,键盘,手表,体育健身,医疗保健,智能穿戴设备,汽车,家用电子等
适用场景较高数据量传输、对传输带宽有要求续航要求较高、数据量小

蓝牙技术目前已经发展到5.0+版本,为现阶段最高级的蓝牙协议标准。BLE技术更契合新时代物联网的需求:更快、更省、更远、更便捷,也是我们小程序开发者在物联网项目最常用的技术。


蓝牙通信概述


低功耗蓝牙协议给设备定义了若干角色,其中最主要的角色是:外围设备(Peripheral)中心设备(Central)。




  • 外围设备:用来提供数据,通过不停地向外广播数据,让中心设备发现自己。




  • 中心设备:扫描外围设备,发现有外围设备存在后,可以与之建立连接,之后就可以使用外围设备提供的服务(Service)。




在两个蓝牙设备建立连接之后,双方的数据交互是基于一个叫做 GATT (Generic Attribute Profile,通用属性配置文件) 的规范,根据该规范可以定义出一个配置文件(Profile),描述该蓝牙设备提供的服务(Service)。


在整个通信过程中,有三个最主要的概念:配置文件(Profile)服务(Service)特征(Characteristic)


Characteristic:在 GATT 规范中最小的逻辑数据单元。实际上,在与蓝牙设备打交道,主要就是通过读写 Characteristic 的 value 完成。Characteristic 是通过一个 16bit 或 128bit 的 UUID 唯一标识。


Service:可以理解为蓝牙设备提供的服务,一个蓝牙设备可以提供多个服务,比如电量信息服务、系统信息服务等。每个 Service 又包含多个 Characteristic 特性值,比如电量信息服务就会有个 Characteristic 表示电量数据。同时也有一个 16bit 或 128bit 的 UUID 唯一标识该服务。


Profile:并不真实存在于蓝牙设备中,它只是被蓝牙标准预先定义的一些 Service 的集合。如果蓝牙设备之间要相互兼容,它们只要支持相同的 Profile 即可。一个蓝牙设备可以支持多个 Profile。


Desciptor: 描述符是描述特征值的已定义属性。例如,Desciptor 可指定人类可读的描述、特征值的取值范围或特定于特征值的度量单位。每个 Desciptor 由一个 UUID 唯一标识。


总结:每个蓝牙设备可能提供多个 Service,每个 Service 可能有多个 Characteristic,根据蓝牙设备的协议,用对应的 Characteristic 进行读写,即可达到与其通信的目的。


蓝牙开发实践


蓝牙通信过程介绍



整体上看,蓝牙通信的开发主要分为三部分:



  1. 蓝牙资源和状态管理:包括蓝牙生命周期管理、蓝牙状态管理(开关、适配器、设备连接、数据接收等)、错误异常处理。

  2. 搜寻外围设备并建立连接:包括搜寻设备、监听设备发现、处理获取到的设备信息、连接/断开设备等。

  3. 读写数据:包括寻找目标服务和特征值、订阅特征值、监听并接收设备数据、分包处理数据等。


蓝牙数据读写


在小程序蓝牙开发联调中,推荐使用TLV协议对数据进行封包,TLV协议(Tag、Length、Value)是常见的一种面向物联网的通讯协议,对于不同的传输场景,甚至演变出混合型、指针型、循环型等不同类型的格式。


比如,在实践中往往只需要最简单的L-TLV格式,以下使用十六进制(Hex)表示:



  • 数据包总长(L)

  • 数据的类型Tag/Type(T)

  • Value的长度Length(L)

  • 数据的值Value(V)


[0x07, 0x01, 0x01, 0x01, 0x02, 0x01, 0x01]
[数据总长,typelength,value,typelength,value]

举例


假设业务规定各字段type如下


字段名称type字段类型备注
account0x00String账号
Password0x01String密码

想要向设备传输一条写入account的指令,value为ABC。


ABC 通过 UTF-8 编码转 Hex String 分别是0x41、0x42、0x43。


那么数据包总长6字节,type是0,value总长3字节。


字符集编码


实际业务场景中,如果需要传输中文字符,则需要通过协商好的字符集进行转换。


常见字符集有:ASCII字符集、GB2312字符集、GBK字符集、 GB18030字符集、Unicode字符集等。


字符集描述
ASCII美国信息交换标准代码是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言
GB2312中国人民通过对 ASCII 编码的中文扩充改造,产生了 GB2312 编码,可以表示6000多个常用汉字。
GBK汉字实在是太多了,包括繁体和各种字符,于是产生了 GBK 编码,它包括了 GB2312 中的编码,同时扩充了很多。
GB18030中国是个多民族国家,各个民族几乎都有自己独立的语言系统,为了表示那些字符,继续把 GBK 编码扩充为 GB18030 编码。
Unicode每个国家都像中国一样,把自己的语言进行编码,于是出现了各种各样的编码,如果你不安装相应的编码,就无法解释相应编码想表达的内容。终于,有个叫 ISO 的组织看不下去了。他们一起创造了一种编码 Unicode ,这种编码非常大,大到可以容纳世界上任何一个文字和标志。
UTF-8、 UTF-16Unicode 在网络传输中,出现了两个标准 UTF-8 和 UTF-16,分别每次传输 8个位和 16个位。

比如小写字母a,ASCII编码对应的Hex值是0x61,而GB2312字符集编码对应的Hex值是253631


将文本字符串转换为Hex字符串的时候,不同的字符集编码对应的Hex值不一样,所以小程序与蓝牙设备应当使用同一套字符集编码。推荐统一使用Unicode的UTF-8标准。


以下是字符转换示例:


// 中文转UTF-8
encodeURI('好').replace(/%/g, ''); // 'E5A5BD'

// UTF-8转中文
hex2String('E5A5BD'); // '好'

/**
* * read UTF-8
* @param { number[] } arr
* @returns {string}
*/

const readUTF = (arr: number [] ) => {
let UTF = '';
const _arr = arr;
for (let i = 0; i < _arr.length; i++) {
// 10进制转2进制
const one = _arr[i].toString(2);
const v = one.match(/^1+?(?=0)/);
if (v && one.length == 8) {
const bytesLength = v[0].length;
let store = _arr[i].toString(2).slice(7 - bytesLength);
for (let st = 1; st < bytesLength; st++) {
store += _arr[st + i].toString(2).slice(2);
}
// 二进制序列转charCode,再拼接
UTF += String.fromCharCode(parseInt(store, 2));
i += bytesLength - 1;
} else {
UTF += String.fromCharCode(_arr[i]);
}
}
return UTF;
};

/**
* * transfer hex to string
* @param { string } str
* @returns {string}
*/

const hex2String = (hex: string) => {
const buf = [];
// 转10进制数组
for (let i = 0; i < hex.length; i += 2) {
buf.push(parseInt(hex.substring(i, i + 2), 16));
}

return readUTF(buf);
};

蓝牙分包


但是实际场景往往不是传输几个字母这么简单。虽然小程序不会对写入数据包大小做限制,但与蓝牙设备传输数据时,数据量超过 MTU (最大传输单元) 容易导致系统错误,所以要主动对数据进行分片传输。


参考各小程序开放平台文档:


开放平台文档描述
微信小程序在与蓝牙设备传输数据时,需要注意 MTU(最大传输单元)。如果数据量超过 MTU 会导致错误,建议根据蓝牙设备协议进行分片传输。Android设备可以调用 wx.setBLEMTU 进行 MTU 协商。在 MTU 未知的情况下,建议使用 20 字节为单位传输。
飞书小程序蓝牙设备特征值对应的值,为 16 进制字符串,限制在 20 字节内
支付宝小程序写入特征值需要使用 16 进制的字符串,并限制在 20 字节内。
Taro小程序不会对写入数据包大小做限制,但系统与蓝牙设备会限制蓝牙4.0单次传输的数据大小,超过最大字节数后会发生写入错误,建议每次写入不超过20字节。若单次写入数据过长,iOS 上存在系统不会有任何回调的情况(包括错误回调)。

分包的过程,需要用到 ArrayBuffer



ArrayBuffer 对象用来表示通用的、固定长度的原始二进制数据缓冲区。


ArrayBuffer 是对固定长度的连续内存空间的引用。



在 Web 开发中,当我们处理文件时(创建,上传,下载),经常会遇到二进制数据。另一个典型的应用场景是图像处理。这些都可以通过 JavaScript 进行处理,而且二进制操作性能更高。


ArrayBuffer 只是一个内存区域,里面存储着一些原始的字节序列,它和普通的Array完全不是一个概念,它的长度是固定的,无法增加或减少,也无法直接用buffer[index]进行访问。


要想写入值、遍历它或者访问单个字节,需要使用视图(View) 进行操作,以下为一些常用的视图:


Uint8Array :将 ArrayBuffer 中的每个字节视为 0 到 255 之间的单个数字(每个字节是 8 位,因此只能容纳那么多)。称为 “8 位无符号整数”。


Uint16Array:将每 2 个字节视为一个 0 到 65535 之间的整数。称为 “16 位无符号整数”。


所有这些视图(Uint8Array,Uint32Array 等)的通用术语是 TypedArray(类型化数组)。它们都享有同一组方法和属性,类似于常规数组,具有索引,并且是可迭代的。


实际上,不同平台的小程序API定义的数据接口,都多少会用到ArrayBuffer



微信小程序-写入特征值



飞书小程序-获取设备信息


但也不排除有些操作,开发平台已经帮忙处理了



飞书小程序-写入特征值


因此学习并使用 ArrayBuffer,可以:




  1. 方便操作分包,方便读取设备返回的数据、向设备写入数据。




  2. 在不同小程序平台灵活处理,更好地兼容




回到主题,蓝牙分包的思路是:


Text String --> Hex String --> ArrayBuffer(分包)


举个例子,上文中想要向设备传输一条写入password的指令,value为bytedance123456789ABC


[数据总长,type,length,value] MTU为20字节
[0x14, 0x01, 0x11, 0x62, 0x79, 0x74, ...] 第一个包 bytedance12345678
[0x07, 0x01, 0x04, 0x39, 0x41, 0x42, ...] 第二个包 9ABC

设备端会将多个相同type的包的值追加,而不是覆盖。


如何与设备端协商分包交互机制?



  1. 规定服务、特征值UUID,建议不同操作使用不同的UUID,读、写、订阅分开。

  2. 遵循TLV协议,双方协商好Type对应的字段类型和含义。

  3. 双方使用同一套字符编码集。

  4. 约定连在一起的两次(或多次)相同类型的设置,应该把它们的值追加连接,而不是覆盖

  5. 可约定在一次涉及业务逻辑的通信过程中,发送“开始”和“结束”的蓝牙包,告知设备处于这两个信号之间的蓝牙包为一次完整的通信数据流。

  6. 双方共同约定一个超时时间,若在此时间内由于各种原因未能完成读/写通信,则认为通信失败,小程序端必须给予用户友好提示。


问题排查手段


在开发过程中可能会遇到调用API失败、连接断开等问题



  1. 检查API调用顺序


小程序的蓝牙API使用起来比较简单,但是需要严格遵循一定的调用顺序(参考上文的流程图)。比如检查是否在开关蓝牙适配器之外进行操作,或者是否在特征值发生变化后才进行事件监听等



  1. 对比测试



  • 业务小程序、开放平台官方蓝牙demo 对比

  • 开放平台(非微信)官方蓝牙demo、微信官方demo 对比

  • 同厂商设备、同芯片、同蓝牙模组,多台设备对比

  • iOS、Android,蓝牙调试软件 与小程序的对比 (iOS:LightBlue,Android:BLE调试宝、nRF Connect)


经过以上对比测试,基本可以缩小问题范围,定位问题究竟是出在哪一方。但并不百分之百准确。




  1. 一些Tips:



    • 设备Server端在自定义特征值UUID时未遵循GATT的Attribute Structure,而蓝牙服务iOS的实现会比Android更严格。

    • 外围设备使用deviceId作为唯一标识,但iOS 和 Android在拿到的信息上有所差异。Android上获取到的deviceId为设备MAC地址,iOS上则是系统根据外围设备 MAC 地址及发现设备的时间生成的 UUID,因此deviceId不能硬编码。

    • 蓝牙模块比较耗费系统资源,做好生命周期管理必不可少,比如建立连接和断开连接应该成对出现,如果未能及时关闭连接释放资源,容易导致连接异常。另外,大多数蓝牙模组只支持单链路,最大连接数量为1,若未能及时断开连接,必然出现设备搜寻不到或连接不上的情况。




  2. 日志排查




作为小程序的开发者,很多疑难问题往往不能直观看出。如果你有对应的资源可以联系到开放平台的维护人员,即可拿到日志。我们项目组曾与飞书开放平台建立蓝牙专项问题解决渠道,结合开平和设备端同学捕获的日志,可以加快排查速度。


参考文章


http://www.bluetooth.com/learn-about…
http://www.cnblogs.com/chusiyong/p…
http://www.jianshu.com/p/62eb2f540…
zh.javascript.info/arraybuffer…


作者:HenryZheng
来源:juejin.cn/post/7221794170868351034
收起阅读 »

HTML5+CSS3小实例:闪亮的玻璃图标悬浮效果

web
HTML5+CSS3实现闪亮的玻璃图标悬浮效果,光与玻璃的碰撞,好有质感的玻璃图标。 先看效果: 源代码: <!DOCTYPE html> <html> <head> <meta http-equiv="c...
继续阅读 »

HTML5+CSS3实现闪亮的玻璃图标悬浮效果,光与玻璃的碰撞,好有质感的玻璃图标。


先看效果:



源代码:


<!DOCTYPE html>
<html>

<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">

<title>闪亮的玻璃图标悬浮效果</title>
<link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/4.7.0/css/font-awesome.css" rel="stylesheet">
<link rel="stylesheet" href="../css/5.css">
</head>

<body>
<div class="container">
<div class="color"></div>
<div class="color"></div>
<div class="color"></div>
<ul>
<li>
<a href="#"><i class="fa fa-qq" aria-hidden="true"></i></a>
</li>
<li>
<a href="#"><i class="fa fa-weixin" aria-hidden="true"></i></a>
</li>
<li>
<a href="#"><i class="fa fa-weibo" aria-hidden="true"></i></a>
</li>
<li>
<a href="#"><i class="fa fa-tencent-weibo" aria-hidden="true"></i></a>
</li>
<li>
<a href="#"><i class="fa fa-telegram" aria-hidden="true"></i></a>
</li>
</ul>
</div>
</body>

</html>

*{
margin: 0;
padding: 0;
/* 这个是告诉浏览器:你想要设置的边框和内边距的值是包含在总宽高内的 */
box-sizing: border-box;
}
body{
/* 溢出隐藏 */
overflow: hidden;
}
.container{
position: absolute;
width: 100%;
/* 100%窗口高度 */
height: 100vh;
/* 弹性布局 水平垂直居中 */
display: flex;
justify-content: center;
align-items: center;
/* 渐变背景 */
background: linear-gradient(to bottom,#2193b0,#6dd5ed);
}
.container::before{
content: "";
position: absolute;
bottom: 0px;
width: 100%;
height: 50%;
z-index: 1;
/* 背景模糊 */
backdrop-filter: blur(5px);
border-top: 1px solid rgba(255,255,255,0.5);
}
.container .color{
position: absolute;
/* 模糊滤镜 数值越大越模糊 */
filter: blur(200px);
}
.container .color:nth-child(1){
background-color: #fd746c;
width: 800px;
height: 800px;
top: -450px;
}
.container .color:nth-child(2){
background-color: #cf8bf3;
width: 600px;
height: 600px;
bottom: -150px;
left: 100px;
}
.container .color:nth-child(3){
background-color: #fdb99b;
width: 400px;
height: 400px;
bottom:50px;
right:100px;
}
ul{
position: relative;
display: flex;
z-index: 2;
}
ul li{
position: relative;
list-style: none;
margin: 10px;
}
ul li a{
position: relative;
width: 80px;
height: 80px;
display: inline-block;
border-radius: 10px;
display: flex;
justify-content: center;
align-items: center;
text-decoration: none;
color: #fff;
font-size: 32px;
border: 1px solid rgba(255,255,255,0.4);
border-right: 1px solid rgba(255,255,255,0.2);
border-bottom: 1px solid rgba(255,255,255,0.2);
/* 阴影 */
box-shadow: 0px 5px 45px rgba(0,0,0,0.1);
/* 背景模糊 */
backdrop-filter: blur(2px);
/* 动画过渡 */
transition: all 0.5s;
overflow: hidden;
}
ul li a:hover{
/* 鼠标移入元素沿Y轴上移 */
transform: translateY(-20px);
}
ul li a::before{
content: "";
position: absolute;
top: 0px;
left: 0px;
width: 50px;
height: 100%;
background-color: rgba(255,255,255,0.5);
/* 元素沿X轴45度横切,沿X轴右移150px */
transform: skewX(45deg) translateX(150px);
/* 动画过渡 */
transition: all 0.5s;
}
ul li a:hover::before{
/* 元素沿X轴45度横切,沿X轴左移150px */
transform: skewX(45deg) translateX(-150px);
}

作者:艾恩小灰灰
来源:juejin.cn/post/7091339314352619557
收起阅读 »

前端获取电池信息

web
今日正能量: 当奇怪的需求越做越多的时候,证明你的眼光也正在变得广阔。 产品经理:加个需求,用户电脑设备如果快没电,我要暖心的告诉他该插上电源。 前端攻城狮:。。。他电脑不会自己提醒吗? 产品经理:你做不做? 前端攻城狮:做! 前言 随着技术的日益发展,w...
继续阅读 »

今日正能量: 当奇怪的需求越做越多的时候,证明你的眼光也正在变得广阔。



产品经理:加个需求,用户电脑设备如果快没电,我要暖心的告诉他该插上电源。


前端攻城狮:。。。他电脑不会自己提醒吗?


产品经理:你做不做?


前端攻城狮:做!


屏幕截图 2023-04-17 221002.png


前言


随着技术的日益发展,web前端技术远比我们想象的强大。浏览器允许网站获取用户设备的电池状态信息,例如电量百分比,剩余电量,充电状态等等。我们可以使用这些信息,根据用户设备的电量调整我们的应用行为。在这篇中,我们将探讨如何在前端中获取电池信息,用到的就是关于 Battery Status API。


Battery Status API的使用


Battery Status API 是一个 Web API,允许 Web 应用程序访问用户设备的电池状态信息。使用这个 API,我们可以在不安装任何应用程序的情况下,从 Web 浏览器直接读取设备的电量信息。


获取设备电池信息的主要步骤如下:


// 请求电池信息
navigator.getBattery().then(function (battery) {
// 后续代码
})

将返回一个 Promise 对象,它会解析为一个 BatteryManager 对象,我们可以使用它来读取设备的电池属性。


navigator.getBattery().then(function (battery) {
// 获取设备电量剩余百分比
var level = battery.level //最大值为1,对应电量100%
console.log('Level: ' + level * 100 + '%')

// 获取设备充电状态
var charging = battery.charging
console.log('充电状态: ' + charging)

// 获取设备完全充电需要的时间
var chargingTime = battery.chargingTime
console.log('完全充电需要的时间: ' + chargingTime)

// 获取设备完全放电需要的时间
var dischargingTime = battery.dischargingTime
console.log('完全放电需要的时间: ' + dischargingTime)
})

监听电池状态变化


为了更好地反映用户设备的电池状态,我们可以在前端中添加事件来监视电池状态的变化。例如,当设备的电池电量改变时,会触发事件。一些给大家列举几个常用事件:


navigator.getBattery().then(function (battery) {
// 添加事件,当设备电量改变时触发
battery.addEventListener('levelchange', function () {
console.log('电量改变: ' + battery.level)
})

// 添加事件,当设备充电状态改变时触发
battery.addEventListener('chargingchange', function () {
console.log('充电状态改变: ' + battery.charging)
})

// 添加事件,当设备完全充电需要时间改变时触发
battery.addEventListener('chargingtimechange', function () {
console.log('完全充电需要时间: ' + battery.chargingTime)
})

// 添加事件,当设备完全放电需要时间改变时触发
battery.addEventListener('dischargingtimechange', function () {
console.log('完全放电需要时间: ' + battery.dischargingTime)
})
})

兼容性


兼容性方面,Battery Status API 并不适用于所有的设备和操作系统,开发人员需要进行兼容性处理,以确保我们的应用可以在所有的设备上运行。以下是该API对应的兼容性视图:


屏幕截图 2023-04-17 220020.png


通过 Battery Status API 获取设备电池信息是一种很强大的方法,可以根据设备电池状态来优化应用程序的行为。需要注意的是,此 API 不适用于所有设备和操作系统,并且某些设备生产商可能不允许共享电池信息。


作者:白椰子
来源:juejin.cn/post/7222996459833622565
收起阅读 »

情侣空间动态时间效果,你学废了吗?

web
前言 中秋这天刚好碰上和女朋友在一起的 五周年 了,想来五年风风雨雨仍然好好的,挺是感慨,也挺满足的。qq情侣空间也毫不意外的准点报时了,闲来无事点进去看了看,瞥到一个动效,觉得很有意思,于是打算自己动手实现一下,也算是尝试了。 动效长这样: 码上掘金 动态...
继续阅读 »

前言


中秋这天刚好碰上和女朋友在一起的 五周年 了,想来五年风风雨雨仍然好好的,挺是感慨,也挺满足的。qq情侣空间也毫不意外的准点报时了,闲来无事点进去看了看,瞥到一个动效,觉得很有意思,于是打算自己动手实现一下,也算是尝试了。


动效长这样:


Video_20220912_014016_213.gif


码上掘金


动态日期特效 - 码上掘金 (juejin.cn)


思路解析


日期函数的使用频率可以说是很高了,不管是原生手写也好,还是用的 day.js 这种第三方库,在业务开发中我们经常需要处理日期进行展示。由于这边的时间处理不复杂,因此我们直接手写一个就好了。


获取年月日我们分别使用 getFullYear()getMonth() 还有 getDate() ,需要注意的是,很多新手小伙伴经常会把 getDay() 误以为是获取日的功能,实际上是用的 getDate() 实现的,还有一个需要注意的地方是,获取月份的函数 getMonth() ,获取的时间范围是 (0,11) ,没错,它是从 0 开始的,最后为了展示我们还需要让它 +1


时分秒我们分别使用 getHours()getMinutes() 还有 getSeconds() 三个方法,其中,分和秒的函数返回的是 (0, 59),也就是说不超过两位数的话,输出形式是个位数,我们需要手动补 0,可以通过字符串拼接过 padStart() 实现。


最后,我们观察一下这个效果,实际上它只有最后一个数字是向上淡出的,也就是说我们只要处理这个数字就好了,那么问题就简单了。


我们先将秒的个位和十位分开,将它们分为两个部分单独展示,这样我们就可以单独处理这个数字的特效了。


向上淡出,你第一思路是什么?


对了,是定位+透明,我们就用这个思路试一试。


一开始给它设置为相对定位 position: relative。接下来实现动效,为了让它不断的有这么个淡出效果,我们自然而然想到要使用动画,从当前位置开始,结束的时候增大透明度并且向上移动,逻辑很快就写好了。


涉及知识点


1. Date 日期类




  • Date.prototype.getDate():根据本地时间,返回一个指定的 Date 对象为一个月中的哪一日(1-31)。




  • Date.prototype.getFullYear():根据本地时间,返回一个指定的 Date 对象的完整年份(四位数年份)。




  • Date.prototype.getHours():根据本地时间,返回一个指定的 Date 对象的小时(023)。




  • Date.prototype.getMinutes():根据本地时间,返回一个指定的 Date 对象的分钟数(059)。




  • Date.prototype.getMonth():根据本地时间,返回一个指定的 Date 对象的月份(011),0 表示一年中的第一月。




  • Date.prototype.getSeconds():根据本地时间,返回一个指定的 Date 对象的秒数(059)。




2. 时间补零


getMinutes()getSeconds() 获取的时间是没有前缀零的,我们可以判断一下,如果时间小于 10 ,则用 0 拼接。


也可以使用 padStart() 进行补零操作。



关于 padStart() 的更多用法,详见:String.prototype.padStart() - JavaScript | MDN (mozilla.org)



结束语


相信不少小伙伴像我一样,因为行业原因,工作中动效开发频率很低,这块的实战经验也很薄弱,为了以后能更好的搬砖,我们应该从小 demo 开始,不断的去练习提升,基础进阶两手抓。


作者:CatWatermelon
来源:juejin.cn/post/7142412506815250445
收起阅读 »

如何接入小程序订阅消息?

web
更新完微信服务号的模板消息之后,我又赶紧把微信小程序的订阅消息给实现了!之前我一直以为微信小程序也是要企业才能申请,没想到小程序个人就能申请。 消息推送平台🔥推送下发【邮件】【短信】【微信服务号】【微信小程序】【企业微信】【钉钉】等消息类型。 gitee....
继续阅读 »

更新完微信服务号的模板消息之后,我又赶紧把微信小程序的订阅消息给实现了!之前我一直以为微信小程序也是要企业才能申请,没想到小程序个人就能申请。



消息推送平台🔥推送下发【邮件】【短信】【微信服务号】【微信小程序】【企业微信】【钉钉】等消息类型




本想着跟微信服务号的模板消息一样,我去申请一个「测试号」,就能下发微信小程序的订阅消息了。但微信小程序的订阅消息一直不支持「测试号」下发。


于是,我就注册了一个微信小程序,拿到我的小程序appIdsecret,并在微信后台创建了几个订阅消息的模板。



紧接着,这个把小程序的账号就接入到消息推送平台的账号管理体系下:



写几行代码得到刚才创建的模板,顺便跟前端来个简单的交互:




改几行代码,把具体调用微信的逻辑给补上,有SDK的加持下这种代码就是10min就完成了,非常简单。


image-20221208202228360


实现小程序的订阅消息推送,我花的时间最长就花在这下面啦:


1、拥有自己的小程序(拥有调试基础)


2、让自己的登录到这个小程序里(得到openId)


3、小程序弹窗让我能授权给微信发送订阅消息(得到推送权限)


小程序的账号我已经创建好了,但是小程序是没有任何内容的。于是我就在小程序的后台点点点,顺便看看小程序一般是怎么开发的。于是,我就看到了微信小程序的开发者工具:


developers.weixin.qq.com/miniprogram…


下载了之后,这个小工具给我推荐「云开发」,只要花点钱我就可以调用云函数了。虽然要19.9块,有点肉疼。但省时间的事,我咬咬牙就上了。



完了以后,我在小程序工具箱里翻了好几个模板,看看有没有我想要的功能:登录小程序获取openId、弹窗让我授权模板发送消息。你别说,真给我翻到一个:



我是没做过小程序的,自然就不会小程序开发,于是就只能摸石头过河了。花了一天多,发现我在这个demo项目里获取的openId就是死活的调不通小程序(报错就说不合法openId)。


经过漫长的调试,我忍不了了,再这样耗下去是不行的了。我直接去GitHub看看有没有现成的demo,随便一搜,还真的有。



github.com/zhangkaizha…


直接将「wxapp」文件下导入到小程序的开发工具里,一看,还真能用,代码又少。回看同步openId的代码,原来是要调接口请求微信做鉴权的呀。



于是我在消息推送平台里也临时写了接口进行鉴权,在小程序调用登录的时候改下入参就完事咯。




经过登录凭证校验之后,我们就能拿到openId,把订阅消息的权限界面给唤起,点击允许,就能在消息推送平台下发送一条小程序的订阅消息啦。




代码方面我就不细说啦,感兴趣的同学可以把项目搞下来玩玩,源码都是有的。这几天还在疯狂更新中,看看目前的消息渠道接入的情况吧?


如果想学Java项目的,强烈推荐我的开源项目消息推送平台Austin(8K stars) ,可以用作毕业设计,可以用作校招,可以看看生产环境是怎么推送消息的。开源项目消息推送平台austin仓库地址:



消息推送平台🔥推送下发【邮件】【短信】【微信服务号】【微信小程序】【企业微信】【钉钉】等消息类型





作者:Java3y
来源:juejin.cn/post/7223728273007493176
收起阅读 »

关于如何写一个有层次感的css遮罩效果

web
前言 写了这么多天游戏了,最近也想换换口味写一点有趣的CSS动效,这次我为大家带来的就是一个纯css的动态效果,层次感遮罩。在现在审美疲劳的大时代环境背景下,对页面的设计也开始越来越追求他的层次感和立体感,有时候因为一个好的板块动效,可以拉高整体的页面颜值增添...
继续阅读 »

前言


写了这么多天游戏了,最近也想换换口味写一点有趣的CSS动效,这次我为大家带来的就是一个纯css的动态效果,层次感遮罩。在现在审美疲劳的大时代环境背景下,对页面的设计也开始越来越追求他的层次感和立体感,有时候因为一个好的板块动效,可以拉高整体的页面颜值增添出页面的高级感,让网页用户有一种油然而生的成为这个页面用户是一种非常自豪事情的感觉。(当然如果甲方觉得麻垮你说的再牛B也是白搭)


那么接下来我们马上开始今天的代码解析


实现步骤


创建出基本元素


不难看出页面元素其实就是一个边框加一段文字,但这个边框并非一个div加上border属性这么简单,这里我用的是伪元素


先写好html标签,这里用阿a标签什么的都可以。


<a href="#">荆棘鸟QAQ</a>

用css把整个页面变成灰色的,并且为了突出文字,把a标签居中,文字变为白色,再加上一个文字阴影。


body {
font-family: "黑体";
background-color: #555;
}

a {
color: #fffbf1;
text-shadow: 0 20px 25px #2e2e31;
font-size: 80px;
font-weight: bold;
text-decoration: none;
position: absolute;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
}

image.png


添加伪类


这个就是重头戏了,因为层次效果全靠after和before这两个伪类表现出来。


减少冗余代码,所以先把两个伪类的共同样式写出来。伪类的高读要根据文字大小进行改变,所以直接用padding配合em来规定伪类的高度。宽度直接用100%


a:before,
a:after {
content: '';
padding: .9em .4em;
position: absolute;
left: 50%;
width: 100%;
top: 50%;
display: block;
border: 15px solid skyblue;
transform: translateX(-50%) translateY(-50%) rotate(0deg);
animation: 10s infinite alternate ease-in-out tipsy;
}

image.png


边框有了,现在就需要把动态效果给加上,让它先动起来


@keyframes tipsy {
0%{
transform: translateX(-50%) translateY(-50%) rotate(0deg);
}
100% {
transform: translateX(-50%) translateY(-50%) rotate(360deg);
}
}

遮罩效果.gif


如动图所示,现在表框完全就是盖在文字上的,还没有什么特别之处。因为并没有将层级给区分开来,这也很简单,把before这个伪元素的层级给下调就行啦
a:before {
z-index: -1;
}

当然层级改变了,但上一层的边框会把下一层的边框给覆盖住,所以还得让部分边框变得透明


a:before {
border-color: skyblue skyblue rgba(0, 0, 0, 0) rgba(0, 0, 0, 0);
z-index: -1;
}

a:after {
border-color: rgba(0, 0, 0, 0) rgba(0, 0, 0, 0) skyblue skyblue;
}

遮罩效果2.gif


现在就可以看到已经有层次感啦,但是还可以加上一点边框阴影让他更加立体


a:before {
border-color: skyblue skyblue rgba(0, 0, 0, 0) rgba(0, 0, 0, 0);
z-index: -1;
}

a:after {
border-color: rgba(0, 0, 0, 0) rgba(0, 0, 0, 0) skyblue skyblue;
box-shadow: 25px 25px 25px rgba(46, 46, 49, .8);
}

遮罩效果3.gif


那么到这就已经全部完成啦,以上就是本次代码的全部解析,下面我将把所有代码放在在线代码里供大家修改体验

在线代码



往期精彩


关于我写了一个海底掘金挑战游戏juejin.cn/post/714464…


关于我随手写了个掘金相关的游戏juejin.cn/post/714232…


关于我帮领导的孩子写了一个小游戏参赛这种事juejin.cn/post/714115…


关于我抽不到月饼礼盒于是用代码做了一个(纯代码文本) juejin.cn/post/714047…


关于我仿做了个steam很火的《Helltaker》游戏juejin.cn/post/712149…


作者:Gatsby
来源:juejin.cn/post/7144912266855940132
收起阅读 »

前端应该知道的浏览器中的内存知识

web
为了保证我们网页的稳定性,浏览器的内存知识对我们来说是十分必要的,我们不应该只考虑网页打开时的性能,也应该考虑网页长时间挂载下的稳定性。 本次梳理以Chrome为例。 chrome的内存限制 堆内存的限制是由 V8 来设置的。 存在限制 64位系统 物理内存...
继续阅读 »

为了保证我们网页的稳定性,浏览器的内存知识对我们来说是十分必要的,我们不应该只考虑网页打开时的性能,也应该考虑网页长时间挂载下的稳定性。


本次梳理以Chrome为例。


chrome的内存限制


堆内存的限制是由 V8 来设置的。


存在限制


64位系统
物理内存 > 16G => 最大堆内存限制为4G
物理内存 <= 16G => 最大堆内存限制为2G

32位系统
最大堆内存限制为1G


堆内存是计算机系统中,当多个程序同时运行时,为了这些进程能够共享数据、交换信息而把它们的数据存放在一个连续的区域。它是一个连续的内存区域,在物理上并不存在。



何为内存


内存(Memory)是计算机的重要部件,也称内存储器和主存储器,它用于暂时存放CPU中的运算数据,以及与硬盘等外部存储器交换的数据。它是外部存储器与CPU进行沟通的桥梁,计算机中所有程序的运行都在内存中进行,内存性能的强弱影响计算机整体发挥的水平。只要计算机开始运行,操作系统就会把需要运算的数据从内存调到CPU中进行运算,当运算完成,CPU将结果传送出来。


所以内存的运行决定计算机整体运行快慢。


为何限制


Chrome之所以限制了内存的大小,表面上的原因是V8最初是作为浏览器的JavaScript引擎而设计,不太可能遇到大量内存的场景,而深层次的原因:则是由于V8的垃圾回收机制的限制。


由于V8需要保证JavaScript应用逻辑与垃圾回收器所看到的不一样,V8在执行垃圾回收时会阻塞 JavaScript应用逻辑,直到垃圾回收结束再重新执行JavaScript应用逻辑,这种行为被称为“全停顿”(stop-the-world)。


若V8的堆内存为1.5GB,V8做一次小的垃圾回收需要50ms以上,做一次非增量式的垃圾回收甚至要1秒以上。这样浏览器将在1s内失去对用户的响应,造成假死现象。如果有动画效果的话,动画的展现也将显著受到影响。


chrome网页是如何占用内存的


chrome之所以很吃内存,是因为chrome使用了多进程机制,每一个chrome的标签页以及每一个扩展,都是独立的进程。在目前的chrome进程架构里,访问一个网站至少包含四个进程:一个浏览器进程、一个GPU进程、一个渲染进程和一个网络进程。除此之外还有包含多个插件进程组成chrome的进程架构。



1. V8


V8 是google 开发的开源高性能 javascript引擎,V8引擎用C++语言开发,被用在Google的chrome浏览器,android 浏览器js引擎默认也用V8。


​ V8最初是为了提高web浏览器中的JavaScript运行性能设计的。为了提升性能,V8将JavaScript代码翻译为更高效的机器语言,而不是使用解释程序。它通过实现一个JIT(Just-In-Time,即时) 编译器来将JavaScript代码编译为机器语言,就像很多现代JavaScript引擎如SpiderMonkey或Rhino(Mozilla)做的那样。V8和它们主要的区别是它不会生成字节码或其他中间代码。


1.1 V8如何执行JavaScript



V8执行js的主要流程如下:



  • 准备执行JS需要的基础环境

  • 解析源码生成ast和作用域

  • 依据ast和作用域生成字节码

  • 解释器解释执行字节码

  • 监听热点代码

  • 编译器优化热点代码为二进制的机器码

  • 反优化二进制机器代码


1.1.1 准备执行JS需要的基础环境


这些基础环境包括:



  • 堆空间和栈空间

  • 全局执行上下文

  • 全局作用域

  • 内置函数

  • 宿主环境提供的扩展函数和对象

  • 事件循环系统


1. 堆空间


堆空间是一种树形的存储结构,用来存储对象类型的离散的数据,以及一些占用内存比较大的数据。


存在堆空间的:



  • 函数

  • 数组

  • 在浏览器中还有 window 对象

  • document 对象等


2. 栈空间


栈空间主要是用来管理 JavaScript 函数调用的,栈是内存中连续的一块空间,同时栈结构是“先进后出”的策略。


特点:



  • 先进后出

  • 空间连续

  • 查找效率非常高


函数调用过程中,什么会存在栈里:



  • 原生类型

  • 引用到的对象的地址

  • 函数的执行状态

  • this 值等


3. 全局执行上下文


V8 初始化了基础的存储空间之后,接下来就需要初始化全局执行上下文和全局作用域。 当 V8 开始执行一段可执行代码时,会生成一个执行上下文来维护执行当前代码所需要的变量声明、this 指向等。


执行上下文中主要包含:



  • 变量环境

  • 词法环境:包含了使用 let、const 等变量的内容

  • this 关键字


全局执行上下文在 V8 的生存周期内是不会被销毁的,它会一直保存在堆中。


4. 全局作用域


var x = 5
{
let y = 2
const z = 3
}

这段代码在执行时,会有两个对应的作用域,一个是全局作用域,另外一个是括号内部的作用域,但是这些内容都会保存到全局执行上下文中。


5. 内置函数


JavaScript的内置函数是浏览器内核自带的,不用任何函数库引入就可以直接使用的函数。JavaScript内置函数一共可分为五类:



  • 常规函数

  • 数组函数

  • 日期函数

  • 数学函数

  • 字符串函数


6. 宿主环境提供的扩展函数和对象


什么是宿主环境?


宿主环境可以是浏览器中的渲染进程,可以是 Node.js 进程, 也可以是其他的定制开发的环境,而这些宿主则提供了很多 V8 执行 JavaScript 时所需的基础功能部件。


7. 事件循环系统(Event Loop)


V8 还需要有一个主线程,用来执行 JavaScript 和执行垃圾回收等工作。


V8 是寄生在宿主环境中的,它并没有自己的主线程,而是使用宿主所提供的主线程,V8 所执行的代码都是在宿主的主线程上执行的。


在执行完代码之后,为了让线程继续运行,通常的做法是在代码中添加一个循环语句,在循环语句中监听下个事件。


如果主线程正在执行一个任务,这时候又来了一个新任务,那么这种情况下就需要引入一个任务队列,这个任务队列是放在了事件触发线程,让新任务暂存到任务队列中,等当前的任务执行结束之后,再从消息队列中取出正在排队的任务。当执行完一个任务之后,我们的事件循环系统会重复这个过程,继续从消息队列中取出并执行下个任务。


事件循环系统主要用来处理任务的排队和任务的调度。


1.1.2 解析源码生成ast和作用域


V8接收到JavaScript源代码后,解析器(Parser)会对其进行词法分析和语法分析,结构化JavaScript字符串,生成AST(抽象语法树)。


解析代码需要时间,所以 JavaScript 引擎会尽可能避免完全解析源代码文件。另一方面,在一次用户访问中,页面中会有很多代码不会被执行到,比如,通过用户交互行为触发的动作。


正因为如此,所有主流浏览器都实现了惰性解析(Lazy Parsing)。解析器不必为每个函数生成 AST(Abstract Syntax tree,抽象语法树),而是可以决定“预解析”(Pre-parsing)或“完全解析”它所遇到的函数。


预解析会检查源代码的语法并抛出语法错误,但不会解析函数中变量的作用域或生成 AST。完全解析则将分析函数体并生成源代码对应的 AST 数据结构。相比正常解析,预解析的速度快了 2 倍。


生成 AST 主要经过两个阶段:分词和语义分析。AST 旨在通过一种结构化的树形数据结构来描述源代码的具体语法组成,常用于语法检查(静态代码分析)、代码混淆、代码优化等。


V8 的 AST 表示方式


1.1.3 依据ast和作用域生成字节码


V8 引入 JIT(Just In Time,即时编译)技术,通过 Ignition 基线编译器快速生成字节码进行执行。


字节码是机器码的抽象,字节码可以直接被优化编译器 TurboFan 用于生成图(TurboFan 对代码的优化基于图),避免优化编译器在优化代码时需要对 JavaScript 源代码重新进行解析。


1.1.4 优化编译器 TurboFan


解释器执行字节码过程中,如果发现代码被重复执行,监控机器人会把这段代码标记为热点代码。热点代码会丢给优化编译器编译成二进制代码,然后优化。下次再执行时就执行这段优化后的二进制代码。


1.1.5 反优化


JS 语言是动态语言,非常之灵活,对象的结构和属性在运行时是可以发生改变的,设想一个问题,如果热代码在某次执行的时候,突然其中的某个属性被修改了,那么编译成机器码的热代码还能继续执行吗?


答案是肯定不能。这个时候就要使用到优化编译器的反优化了,他会将热代码退回到 AST 这一步,这个时候解释器会重新解释执行被修改的代码,如果代码再次被标记为热代码,那么会重复执行优化编译器的这个步骤。


1.2 内存管理


内存是计算机中重要的部件之一,它是与CPU进行沟通的桥梁。


计算机中所有程序的运行都是在内存中进行的,因此内存的性能对计算机的影响非常大。


高效的程序离不开内存的有效管理,内存管理的优势:



  • 减少内存分配

  • 回收开销

  • 避免内存碎片

  • 定位内存位置

  • 方便内存整理

  • 跟踪内存使用


1.2.1 V8 引擎的内存结构


因为 JavaScript 是单线程,所以 V8 在每个上下文都使用一个进程,如果你使用 Service Worker ,它也会为每个 Service Worker 生成一个新的进程。



Service Worker:一个服务器与浏览器之间的中间人角色,如果网站中注册了service worker那么它可以拦截当前网站所有的请求,进行判断(需要编写相应的判断程序),如果需要向服务器发起请求的就转给服务器,如果可以直接使用缓存的就直接返回缓存不再转给服务器,从而大大提高浏览体验。



一个正在运行的程序是由 V8 进程分配的内存来表示的,这被称为 Resident Set(常驻集)。这些内存会进一步划分成不同的部分。


一个 V8 进程的内存通常由以下几个块构成:



  1. **新生代内存区(new space)

    **大多数的对象都会被分配在这里,这个区域很小但是垃圾回收比较频繁;

  2. 老生代内存区(old space)

    属于老生代,这里只保存原始数据对象,这些对象没有指向其他对象的指针;

  3. **大对象区(large object space)

    **这里存放体积超越其他区大小的对象,每个对象有自己的内存,垃圾回收其不会移动大对象区;

  4. 代码区(code space)

    代码对象,会被分配在这里。唯一拥有执行权限的内存;

  5. map 区(map space)

    存放 Cell 和 Map,每个区域都是存放相同大小的元素,结构简单。


如下图:



Heap Memory(堆内存)


这是 V8 引擎存储对象(Object)和动态数据(Dynamic Data)的地方。这也是程序对于内存区域中最大的一块地方,同时**垃圾回收( GC )**也发生在这里。并不是整个 Heap (堆)内存都进行垃圾回收,只有新空间(New Space)和旧空间(Old Space)由垃圾回收管理。


整个堆内存被划分为以下几个部分:




  • 新空间:是新对象存活的地方,这些对象的生命周期都很短。这个空间很小,由两个 Semi-Space 组成,类似与 JVM 中的 S0 和 S1。

    我们将会在后面的内容看到它。新空间的大小是由两个 V8 中的标志位来控制: min_semi_space_size(Initial) 和 max_semi_space_size(Max) 。




  • 旧空间:在新空间中存活了两个 minor GC 周期的对象,会被迁移到这里。

    这个空间由 Major GC(Mark-Sweep & Mark-Compact) 管理。我们也会在后面内容中看到它。旧空间的大小也是由两个 V8 中的标志位来控制:nitial_old_space_size(Initial) 和 max_old_space_size(Max) 。

    旧空间被分成两个部分:




  • 旧指针空间:这些存活下来的的对象都包含了指向其他对象的指针。




  • 旧数据空间:这些对象只包含数据,没有指向其他对象的指针。在新空间中存活两个 minor GC 周期后,String,已经装箱的数字,未装箱的双精度数组会被迁移到这里。




  • 大型对象空间(Large object space):大于其他空间大小限制的对象存放在这里。每个对象都有自己的内存区域,这里的对象不会被垃圾回收器移动。




  • 代码空间(Code-space):这是即时编译器(JIT)存储已经编译的代码块的地方。这是唯一可执行内存的空间(尽管代码可能被分配到大型对象空间(Large object space),那也是可以执行的)。




  • 单元空间(Cell Space),属性单元空间(Property Cell Space)和映射空间(Map Space):这些空间分别存放 Cell,PropertyCell 和 Map。这些空间包含的对象大小相同,并且对对象类型有些限制,可以简化回收工作。




每个空间(除了大型对象空间)都由一组 Page 组成。一个 page 是由操作系统分配的一个连续内存块,大小为 1MB。


Stack(栈)


每个 V8 进程都有一个栈(Stack),这里保存静态数据的地方,比如:方法/函数框架,原型对象的值(Primitive value)和指针。栈(Stack)内存的大小由 V8 的标志位来设置:stack_size。



  1. 全局作用域被保存在 Stack 中的 Global frame 中。

  2. 每个函数调用都做为 frame 块添加到 Stack 中。

  3. 所有的局部变量,包括参数和返回值都保持在 Stack 的函数 frame 上。

  4. 所有的原型类型的数据,比如 int 和 String,都直接保持在 Stack 上。(是的,JavaScript 中 String 是原型数据)

  5. 所有的对象类型,比如 Employee 和 Function 都保存在 Heap 中,并且通过 Stack 上的指针来引用。(函数在 JavaScript 中也是对象。)

  6. 从当前函数中调用的函数被压入了 Stack 的顶部。

  7. 当函数返回是,它的 frame 块将会从 Stack 中移除。

  8. 一旦主进程完成,Heap 上的对象就不再有任何来自 Stack 的指针,这些对象将成为孤儿。

  9. 除非显式的复制,否则其他对象中的所有对象引用都是通过指针完成的。


正如你所看到的,Stack 是自动管理的,而且是由操作系统而不是 V8 本身完成的。因此我们不必担心 Stack 的问题。另一方面,Heap 不是由操作系统自动管理的,由于 Heap 是程序内存块中最大的内存空间,并且保存动态数据,所以它的空间使用会指数级增长,从而导致我们的程序内存耗尽。


1.2.2 V8 内存的使用


我们通过一段代码来看JS程序被执行时是如何使用内存的。


class Employee {
constructor(name, salary, sales) {
this.name = name;
this.salary = salary;
this.sales = sales;
}
}

const BONUS_PERCENTAGE = 10;

function getBonusPercentage(salary) {
const percentage = (salary * BONUS_PERCENTAGE) / 100;
return percentage;
}

function findEmployeeBonus(salary, noOfSales) {
const bonusPercentage = getBonusPercentage(salary);
const bonus = bonusPercentage * noOfSales;
return bonus;
}

let john = new Employee("John", 5000, 5);
john.bonus = findEmployeeBonus(john.salary, john.sales);
console.log(john.bonus);

动图封面


就像你看到的那样:



  1. 全局作用域被保存在 Stack 中的 Global frame 中。

  2. 每个函数调用都做为 frame 块添加到 Stack 中。

  3. 所有的局部变量,包括参数和返回值都保持在 Stack 的函数 frame 上。

  4. 所有的原型类型的数据,比如 int 和 String,都直接保持在 Stack 上。

  5. 所有的对象类型,比如 Employee 和 Function 都保存在 Heap 中,并且通过 Stack 上的指针来引用。

  6. 从当前函数中调用的函数被压入了 Stack 的顶部。

  7. 当函数返回是,它的 frame 块将会从 Stack 中移除。

  8. 一旦主进程完成,Heap 上的对象就不再有任何来自 Stack 的指针,这些对象将成为孤儿。

  9. 除非显式的复制,否则其他对象中的所有对象引用都是通过指针完成的


Stack 是自动管理的,而且是由操作系统而不是 V8 本身完成的。因此我们不必担心 Stack 的问题。另一方面,Heap 不是由操作系统自动管理的,由于 Heap 是程序内存块中最大的内存空间,并且保存动态数据,所以它的空间使用会指数级增长,从而导致我们的程序内存耗尽。


而且 Heap 中的内存也会随着时间的推移,变得支离破碎,从而拖慢程序。这时候就需要垃圾回收发挥作用了。


1.3 垃圾回收 Garbage collection



垃圾回收是指回收那些在应用程序中不再引用的对象,当一个对象无法从根节点访问这个对象就会做为垃圾回收的候选对象。这里的根对象可以为全局对象、局部变量,无法从根节点访问指的也就是不会在被任何其它活动对象所引用。



我们知道了 V8 是如何分配内存的,现在让我们来看看它是如何自动管理 Heap 内存的,这对程序的性能非常重要。


当程序试图在 Heap 中分配超过可用的内存时,就会遇到内存不足的错误,整个页面都会崩溃。


一个不正确的 Heap 内存管理也可能导致内存泄漏。


V8 引擎通过垃圾回收来管理 Heap 内存。简单来说,就是释放孤立(orphan)对象使用的内存。比如,一个对象并没有直接或者间接被 Stack 中的指针所引用,就会释放相应内存为新对象腾出空间。


V8 的垃圾回收器负责回收未使用的内存,以便 V8 进程重新使用。


1.3.1 如何判断非活跃对象


判断对象是否是活跃的一般有两种方法,引用计数法和可访问性分析法。


1. 引用计数法


V8中并没有使用这种方法,因为每当有引用对象的地方,就加1,去掉引用的地方就减1,这种方式无法解决A与B循环引用的情况,引用计数都无法为0,导致无法完成gc。


2. 可访问性分析法


V8中采用了这种方法,将一个称为GC Roots的对象(在浏览器环境中,GC Roots可以包括:全局的window对象,所有原生dom节点集合等等)作为所有初始存活的对象集合,从这个对象出发,进行遍历,遍历到的就认为是可访问的,为活动对象,需要保留;如果没有遍历到的对象,就是不可访问的,这些就是非活动对象,可能就会被垃圾回收


在浏览器环境中,GC Root 有很多,通常包括了以下几种 (但是不止于这几种):



  • 全局的 window 对象(位于每个 iframe 中)。

  • 文档 DOM 树,由可以通过遍历文档到达的所有原生 DOM 节点组成。

  • 存放栈上变量。



1.3.2 代际假说


代际假说(The Generational Hypothesis)是垃圾回收领域中的一个重要术语,它有两个特点




  1. 大部分对象在内存中存活时间很短,比如函数内部声明变量,块级作用域中的变量等,这些代码块执行完分配的内存就会被清掉。




  2. 不死的对象会活得更久,比如全局的window、Dom、全局api等对象。




基于代际假说的理论,在V8引擎中,垃圾回收算法被分为两种,一个是Major GC,主要使用了Mark-Sweep & Mark-Compact算法,针对的是堆内存中的老生代进行垃圾回收;


另外一个是Minor GC,主要使用了Scavenger算法,针对于堆内存中的新生代进行垃圾回收。



注:

所谓老生代指的就是那些存活时间很久没有被清理的对象,而新生代指的是存活时间很短的对象。



1.3.3 Scavenger算法


是在新生代内存中使用的算法,速度更快,空间占用更多的算法。New space区域分为了两个半区,分别为from-space和to-space。不断经过下图中的过程,在两个空间的角色互换中,完成垃圾回收的过程。每次都会有对象复制的操作,为了控制这里产生的时间成本和执行效率,往往新生代的空间并不大。同时为了避免长时间之后,某些对象会一直积压在新生代区域,V8制定了晋升机制,满足任一条件就会被分配到老生代的内存区中。




  1. 经历一次Scavenger算法后,仍未被标记清除的对象。




  2. 进行复制的对象大于to space空间大小的25%。





1.3.4 Mark-Sweep & Mark-Compact算法


是老生代内存中的垃圾回收算法,标记-清除 & 标记-整理,老生代里面的对象一般占用空间大,而且存活时间长,如果也用Scavenger算法,复制会花费大量时间,而且还需要浪费一半的空间。



  • 标记-清除过程:也就是可访问性分析法,从GC Root开始遍历,标记完成后,就直接进行垃圾数据的清理工作。




  • 标记-整理过程:清除算法后会产生大量不连续的内存碎片,碎片过多会导致后面大对象无法分配到足够的空间,所以需要进行整理,第一步的标记是一样的,但标记完成活跃对象后,并不是进行清理,而是将所有存活的对象向一端移动,然后清理掉这端之外的内存。



1.3.5 优化策略


由于 JavaScript 是运行在主线程之上的,因此,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。这种行为叫做全停顿(Stop-The-World)。


STW(全停顿)会造成系统周期性的卡顿,对实时性高的和与时间相关的任务执行成功率会有非常大的影响,例如:js逻辑需要执行动画,刚好碰到gc的过程,会导致整个动画卡顿,用户体验极差。


为了降低这种STW导致的卡顿和性能不佳,V8引擎中目前的垃圾回收器名为Orinoco,经过多年的不断精细化打磨和优化,已经具备了多种优化手段,极大地提升了GC整个过程的性能及体验。



Orinoco 是 V8 GC 项目的代号,它利用并行,增量和并发的技术进行垃圾回收,来释放主线程。



1.3.6 并行回收


简单来讲,就是主线程执行一次完整的垃圾回收时间比较长,开启多个辅助线程(web-worker)来并行处理,整体的耗时会变少,所有线程执行GC的时间点是一致的,js代码也不会有影响,不同线程只需要一点同步的时间,在新生代里面执行的就是并行策略。



1.3.7 增量回收


并行策略说到底还是STW(全停顿)的机制,如果老生代里面存放一些大对象,处理这些依然很耗时,Orinoco又增加了增量回收的策略。将标记工作分解成小块,插在主线程不同的任务之间执行,类似于React fiber的分片机制,等待空闲时间分配。这里需要满足两个实现条件:


1. 随时可以暂停和启动,暂停要保存当前的结果,等下一次空闲时机来才能启动。


2. 暂停时间内,如果已经标记好的数据被js代码修改了,回收器要能正确地处理。



下面要讲到的就是Orinoco引入了三色标记法来解决随时启动或者暂停且不丢之前标记结果的问题。


1.3.8 三色标记法


三色标记法的规则如下:


1. 最开始所有对象都是白色状态


2. 从GC Root遍历所有可到达的对象,标记为灰色,放入待处理队列


3. 从待处理队列中取出灰色对象,将其引用的对象标记为灰色放入待处理队列,自身标记为黑色


4. 重复3中动作,直到灰色对象队列为空,此时白色对象就是垃圾,进行回收。



垃圾回收器可以依据当前内存中有没有灰色节点,来判断整个标记是否完成,如果没有灰色节点了,就可以进行清理工作了。如果还有灰色标记,当下次恢复垃圾回收器时,便从灰色的节点开始继续执行。


下面将要解决由于js代码导致对象引用发生变化的情况,Orinoco借鉴了写屏障的处理办法。


1.3.9 写屏障(write-barrier)


一旦对象发生变化时,如何精确地更新标记的结果,我们可以分析下一般js执行过程中带来的对象的变化有哪些,其实主要有2种:


1. 标记过的黑色或者灰色的对象不再被其他对象所引用。


2. 引入新的对象,新的对象可能是白色的,面临随时被清除的危险,导致代码异常。


第一种问题不大,在下次执行gc的过程中会被再次标记为白色,最后会被清空掉;


第二种就使用到了写屏障策略,一旦有黑色对象引用到了白色对象,系统会强制将白色对象标记成为灰色对象,从而保证了下次gc执行时状态的正确,这种模式也称为强三色原则。


1.3.10 并发回收


虽说三色标记法和写屏障保证了增量回收的机制可以实现,但依然改变不了需要占用主线程的情况,一旦主线程繁忙,垃圾回收依然会影响性能,所以增加了并发回收的机制。


V8里面的并发机制相对复杂,简化来看,当主线程运行代码时,辅助线程并发进行标记,当标记完成后,主线程执行清理的过程时,辅助线程也并行执行。



1.4 D8


D8 是一个十分有用的调试工具,你能够把它看成是 debug for V8 的缩写。我们可以应用 d8 来查看 V8 在执行 JavaScript 过程中的各种两头数据,例如作用域、AST、字节码、优化的二进制代码、垃圾回收的状态,还能够应用 d8 提供的公有 API 查看一些外部信息。


该工具的下载教程和使用方式:blog.csdn.net/heyYouU/art…


2. memory cache


在我们使用强缓存+协商缓存的时候,我们会将一部分资源放在内存中缓存起来。


内存中的缓存,主要包含的是当前中页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。


我们上面谈到了,V8对堆内存的大小做了限制,如果超过了限制会导致网络崩溃的现象,那么我们的memory cache的占用内存受不受V8的约束呢。


当然是受约束的,如果要缓存大量的资源,还得需要用到磁盘缓存。


参考: blog.csdn.net/qiwoo_weekl…


作者:黑色的枫
来源:juejin.cn/post/7221793823704514620
收起阅读 »

必须会的前端基础通用优化方法

web
为什么要做优化? 虽然现在网速越来越快,客户端性能越来越好。但是还是有很多人在使用老旧的设备,不稳定的或者2G、3G网络。另外用户现在对应用的体验要求也越来越高,用户不仅会拿你的应用和同行业的竞争对手去做比较,并且会跟使用过的做的最好的应用去比较。 优化应用的...
继续阅读 »

为什么要做优化?


虽然现在网速越来越快,客户端性能越来越好。但是还是有很多人在使用老旧的设备,不稳定的或者2G、3G网络。另外用户现在对应用的体验要求也越来越高,用户不仅会拿你的应用和同行业的竞争对手去做比较,并且会跟使用过的做的最好的应用去比较。


优化应用的性能可以提升用户体验从而提高留存率,转化率。谷歌、微软和亚马逊的研究都表明,性能可以直接转换成收入。比如,Bing搜索网页时延迟2000ms会导致每用户收入减少4.3%。BBC发现他们的网站加载时间每增加一秒,他们就会失去10%的用户。


下面分享一些操作简单但是效果明显的优化方法。


1、使用HTTP 2.0


HTTP 2.0通过支持首部字段压缩和多路复用技术,让应用更有效地利用网络资源,减少感知的延迟时间。


二进制分帧机制是HTTP 2.0大幅度提高网页性能的核心,它定义了如何封装HTTP消息并在客户端与服务器之间传输。HTTP 1.x的版本都是通过文本的方式传递数据,而HTTP 2.0将传输的信息分割为更小的消息和帧,并对它们采用二进制格式的编码。


HTTP 2.0在二进制分帧的基础上实现了多路复用技术,可以在同一连接上同时发送多个请求和响应,解决了HTTP 1.x的队头阻塞问题,提高了并行处理能力和性能,突破了HTTP 1.x中每个连接每次只交付一个响应的限制。


HTTP 2.0使用HPACK算法对请求和响应头部进行压缩,减少了数据传输量, 可以显著减少每个请求的开销,提高了网络传输速度。而且,它还支持服务器到客户端的主动推动推送机制。


体验demo:http2.akamai.com/demo


开启http2的方法也非常简单,下面以nginx为例


server {
-      listen       443 ssl;
+      listen       443 ssl http2;
       ...
}

2、缓存资源


浏览器发出的所有HTTP请求首先会转至浏览器缓存,用于检查是否存在可满足请求的有效缓存响应。如果存在匹配,则从缓存中读取响应,从而消除网络延迟和传输产生的数据成本。


HTTP缓存是一种提高负载性能的有效方式,因为它减少了不必要的网络请求。所有浏览器都支持该功能,并且不需要太多设置。默认情况下,大部分Web服务器内置支持设置缓存相关表头的设置。


3、缩小和压缩传输的资源


对传输的资源进行缩小和压缩可以有效减少负载大小,进而缩短页面加载时间。


像webpack中已经内置了缩小代码的插件,不需要做额外的工作就可以直接使用,可以删除空格和不需要的代码。


压缩是使用压缩算法修改数据的过程。目前使用最广泛的压缩格式是Gzip,但可以有限考虑使用Brotli,2015年谷歌推出的Brotli压缩算法能在Gzip的基础上将数据再压缩20~25%,现在大部分的浏览器已经支持这种压缩格式,国外很多站点已经开始使用,但是国内还没有开始大规模的应用。很多托管平台、CDN和反向代理服务器默认情况下都会对资产进行压缩编码,或者经过简单的配置就可以轻松实现。下面以Express为例配置一下动态压缩。


const express = require('express');
const compression = require('compression');

const app = express();

app.use(compression());
app.use(express.static('public'));

const listener = app.listen(process.env.PORT, () => {
    console.log(`Your app is listening on port ${listener.address().port}`)
})


4、使用CDN(内容分发网络)


由离用户更近的服务器向用户提供数据,可以显著减少每次TCP连接的网络延迟,增大吞吐量。选择一个可靠的CDN服务提供商进行简单的配置就可以,如阿里云、腾讯云、百度云等。


5、图片处理


对图片处理可以很好得对图片就行优化,经过图片处理优化的图像可以节省40%~80%的大小。虽然通过构建脚本也可以实现图片处理的效果,但在实践中一般使用第三方提供的图像CDN,第三方图像CDN也可以提供更多形式的图像处理方式。通过向文件地址传递参数来获取合适的图像,而不是直接获取原文件。


比如在chrome浏览器中使用WebP格式图片,WebP是由谷歌开发的一种新型图片格式,相比JPEG和PNG格式,WebP图片可以更好地压缩图片大小,从而提高页面加载速度。


6、优先加载关键资源


优先加载关键资源,延迟加载次要资源。优先加载关键资源可以减少页面加载时间,加快页面的渲染速度,提高用户体验。可以对网站进行分析,确定哪些资源是关键资源,然后将非关键资源设置为延迟加载。


7、利用chrome性能工具


Chrome浏览器的Lighthouse扩展程序可以对网站进行测试并生成一个性能报告。Lighthouse生成的报告包含了网站性能、可访问性、最佳实践和SEO等方面的评估结果,以及优化建议。分析测试结果,找出需要改进的方面,并根据建议进行优化。


总之,前端优化是提高用户体验、提高网站性能、减少成本和支持更多设备的关键因素之一。上述优化方法可以帮助开发人员优化应用程序的性能,提高用户体验和满意度,从而提高留存率和转化率,增加收入。


作者:liupl
来源:juejin.cn/post/7219241334926180410
收起阅读 »

项目很大,得忍一下

web
背景 常和我们的客户端厮混,也经常陪他们发版,每次发版编译打包都可以在那边玩一局游戏了。一边幸灾乐祸,一边庆幸h5编译还好挺快的,直到我们的项目也发展成了*山,巨石项目。由于线上要给用户查看历史的推广活动,所以很多老的业务项目都还是留在项目中,导致我们的rou...
继续阅读 »

背景


常和我们的客户端厮混,也经常陪他们发版,每次发版编译打包都可以在那边玩一局游戏了。一边幸灾乐祸,一边庆幸h5编译还好挺快的,直到我们的项目也发展成了*山,巨石项目。由于线上要给用户查看历史的推广活动,所以很多老的业务项目都还是留在项目中,导致我们的router层爆炸,打包速度直线下降,开发过程中,开了hmr稍微有点改动也要等个几秒钟,恨不得立刻重启一个新项目。但是现实告诉你,忍住,别吐,后面还有更多的业务活动加进来。那么怎么解决这个问题呢,这个时候mp的思路是个不错的选择。


关键点


打包慢,本质原因是依赖庞大,组件过多。开发过程中,我们开新的业务组件时,往往和其他业务组件是隔离的,那么我们打包的时候是不是可以把那些不相干的业务组件隔离出去,当然可以。打包工具,从入口开始进行扫描,单页面的模块引入基本都是借助router,所以,关键的是如果我们能够控制router的数量,其实就能够控制编译和打包规模了。


问题


router在vue项目中我们常用的是全家桶的库vue-router,vue-router最多提供了懒加载,动态引入功能并不支持。有小伙伴说router的引入路径可不可以动态传入,我只能说小伙子你很机智,但是vue-router并不支持动态的引入路径。因此我们换个思路,就是在入口的位置控制router的规模,通过不同规模的router实例来实现router的动态引入。当然这需要我们对router库进行一定改造,使其变的灵活易用


一般的router


通常的router如下:



// router.js

/*global require*/

const Vue = require('vue')

const Router = require('vue-router')

Vue.use(Router)

const routes = [

{

path: '/routermap',

component: (resolve) => require(['../containers/map.vue'], resolve),

name: 'routermap',

desc: '路由列表'

},

{

path: '/',

component: (resolve) => require(['../containers/index.vue'], resolve)

},

{

path: '*',

component: (resolve) => require(['../containers/nofound.vue'], resolve),

name: 'defaultPage',

desc: '默认页'

}

]

const router = new Router({

mode: 'history',

routes

})

router.afterEach((to, from) => {

///

})

export default router

// 引入 entry.js

import router from './router.js'

router.beforeEach((to, from, next) => {

///

next()

})

router.afterEach(function(to, from) {

///

})

new Vue({

el: '#app',

template: '<App/>',

router,

})


我们可以不断的往routes数组中添加新的router item来添加新的业务组件,这也是我们的项目不断变大的根本,这样既不好维护,也会导致后面的编译效率


易于维护和管理的router


其实好的管理和维护本质就是分门别类,把类似功能的放在一起,而不是一锅粥都放在一起,这样基本就能解决追踪维护的功能,对应router管理其实也不是很复杂,多建几个文件夹就行如下:


router.png


对应routes/index.js代码如下:



import testRouter from './test.js'

const routes = [

{

path: '/map',

component: (resolve) => require(['../containers/map.vue'], resolve),

name: 'map',

desc: '路由列表'

},

{

path: '/',

component: (resolve) => require(['../containers/index.vue'], resolve)

},

...testRouter,

// 可以扩展其他router

{

path: '*',

component: (resolve) => require(['../containers/nofound.vue'], resolve),

name: 'defaultPage',

desc: '默认页'

}

]

// test.js

/**

* 测试相关页面路由映射

*/


/*global require*/

export default [

{

path: '/test/tools',

name: 'testTools',

component: resolve => require(['@test/tools/index.vue'], resolve),

desc: '测试工具'

}

]


我们通过把router分为几个类别的js,然后在通过router item的数组展开合并,就做到了分门别类,虽然看似简单,但是可以把管理和维护效果提升几个数量级。


支持mp的router


虽然上面支持了易于管理和维护,但是实际上我们如果只是单一入口的话,导出的还是一个巨大的router。那么如何支持多入口呢,其实也不用想的过于复杂,我们让类似test.js的router文件既支持router item的数组导出,也支持类似routes/index.js一样的router实例导出即可。所谓既能分也能合才是最灵活的,这里我们可以利用工厂模式做一个factory.js,如下:



/**

* app 内的页面路由映射

*/


/*global require*/

const Vue = require('vue')

const Router = require('vue-router')

Vue.use(Router)

const RouterFactory = (routes) => {

return new Router({

mode: 'history',

routes: [

{

path: '/map',

component: (resolve) => require(['../containers/map.vue'], resolve),

name: 'map',

desc: '路由列表'

},

{

path: '/',

component: (resolve) => require(['../containers/index.vue'], resolve)

},

...routes,

{

path: '*',

component: (resolve) => require(['../containers/nofound.vue'], resolve),

name: 'defaultPage',

desc: '默认页'

}

]

})

}

export default RouterFactory


这个factory.js产出的router实例和routes/index.js一模一样所以我们只需组装一下test.js即可,如下:



/*global require*/

import RouterFactory from './factory'

export const testRouter = [

{

path: '/test/tools',

name: 'testTools',

component: resolve => require(['@test/tools/index.vue'], resolve),

desc: '测试工具'

}

]

export default RouterFactory(developRouter)

// routes/index.js的引入变化一下即可

import testRouter from './test.js'

// 修改为=》

import { testRouter } from './test.js'


那么我们的入口该如何修改呢?也很简单:



// testEntry.js

import router from './routes/test.js'

router.beforeEach((to, from, next) => {

///

next()

})

router.afterEach(function(to, from) {

///

})

new Vue({

el: '#app',

template: '<App/>',

router,

})


我们建立了一个新的入口文件 testEntry.js 这个入口只引入了test相关的模块组件


如何灵活的和编译命令做配合呢


根据上面,我们进行mp改造的基础已经做好,关于如何多入口编译webpack或者其他打包里面都是基础知识,这里就不多赘述。这里主要聊一下如何灵活的配合命令做编译和部署。


既然router我们都可以分为不同的文件,编译文件我们同样可以拆分为不同的文件,这也使得我们的命令可以灵活多变,这里我们以webpack做为示例:


config.png


config1.png


config2.png


config3.png


根据上图示例 我们的webpack的配置文件仅仅改动了entry,我们稍微改造一下build.js,使其能够接受不同的编译命令:



// build.js

let page = 'all'

if (process.argv[2]) {

page = process.argv[2]

}

let configMap = {

'all': require('./webpack.prod.conf'),

'app': require('./webpack.app.conf')

}

let webpackConfig = configMap[page]

// dev-server.js

let page = 'all'

if (process.argv[2]) {

page = process.argv[2]

}

let configMap = {

'all': require('./webpack.dev.conf'),

'app': require('./webpack.app.dev.conf')

}

let webpackConfig = configMap[page]


对应的脚本配置:



// package.json

"scripts": {

"dev": "node build/dev-server.js",

"build": "node build/build.js",

"build:app": "node build/build.js app",

"dev:app": "node build/dev-server.js app"

},


以上app对应test。最后,我们只需要在命令行执行相应命令,即可实现我们可控的router规模的开发,基本随便来新的需求,咱都可以夜夜做新郎,怎么搞都是飞速。当然部署的话我们也可以单独执行一部分页面的部署命令到单独的域名,换个思路也可以作为一种预发测试的部署方法。



#
整体项目的开发编译

npm run dev

#
单独的app,也即test项目的开发编译

npm run dev:app

#
整体项目的部署

npm run build

#
单独的app,也即test项目的部署

npm run build:app


结语


以上,即如何利用mp思路,提高我们的编译开发效率。时常有人会在提高网页性能的时候说到mp,但mp本质上并不能提高页面的性能,比如白屏优化。而路由中使用懒加载其实才是提高部分网页性能的出力者,关于白屏优化,本篇文章不作展开讨论。


作者:CodePlayer
来源:juejin.cn/post/7218866717739696183
收起阅读 »

周末闲来无事,做了一个能动的宣传页

web
最近在用可画(canva),制作一些素材,海报活动页面,这不情人节快到了吗?基于海报模版,设计自己的页面倒是简单,但是都是静态页面,想着能不能让页面的元素都动起来(Everybody跟我一起嗨嗨嗨!!)。 两个方案 纯CSS animate库 CSS基于ani...
继续阅读 »

创建项目

最近在用可画(canva),制作一些素材,海报活动页面,这不情人节快到了吗?基于海报模版,设计自己的页面倒是简单,但是都是静态页面,想着能不能让页面的元素都动起来(Everybody跟我一起嗨嗨嗨!!)。


两个方案


纯CSS animate库


CSS基于animate库



  1. 利用animate动效,给页面上所有的image和text元素加上className,借助--var全局css变量属性,给元素依次加上delay、duration、index序号、初始化信息rotate、offset、easing等等,我会在码上掘金给一个css的demo版本。CSS版本相对简单一些,只需要循环给所有元素加上对应动画,计算执行时间,延迟时间,页面就可以动起来了。


// 定义的数据结构 Image\Text
[{
"id": "Image/Text-xx",
"type": "Image/Text",
"name": "图片/文本",
"css": {
"top": 0,
"left": 0,
"width": 414,
"height": 736,
"zIndex": 1,
"opacity": 1,
"fontSize": 18,
},
"animationObj": {
{
"delay": 1000,
"duration": 3030,
"type": "flipInY",
"easing": '',
"index": 8,
"rotate_angle": -6.6,
"offset": -112.5,
}
},
"value": "文本内容",
"src": "https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/935920813a0c4151bbf452ef3c53ab7f~tplv-k3u1fbpfcp-watermark.image"
}]


码上掘金-CSS版


下面是纯css的版本:
code.juejin.cn/pen/7123482…


JS animejs库


animejs库


使用JS的关键就是编写对应帧属性,通过时间轴timeline方法给元素加上动画。现在js版本还只是一个demo中的demo,下次再给jym,感兴趣的jy可以自己想想。


时间轴可让你将多个动画同步在一起。
默认情况下,添加到时间轴的每个动画都会在上一个动画结束时开始。

<div class="demo-content params-inheritance-demo">
<div class="line">
<div class="square shadow"></div>
<div class="square el" style="transform: translateX(0px) scale(1); opacity: 0.5;"></div>
</div>
<div class="line">
<div class="circle shadow"></div>
<div class="circle el" style="transform: translateX(7.22878e-10px) scale(1); opacity: 0.5;"></div>
</div>
<div class="line">
<div class="triangle shadow"></div>
<div class="triangle el" style="transform: translateX(2.30924px) scale(1.00924) rotate(180deg); opacity: 0.5;"></div>
</div>
</div>

<script src="https://lib.baomitu.com/animejs/3.2.1/anime.min.js"></script>


.demo-content {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
width: 290px;
height: 100%;
}
.line {
width: 100%;
padding: 1px 0px;
}
.square,
.circle {
pointer-events: none;
position: relative;
width: 28px;
height: 28px;
margin: 1px;
background-color: #005bb7;
font-size: 14px;
}
.triangle {
pointer-events: none;
position: relative;
width: 0;
height: 0;
border-style: solid;
border-width: 0 14px 24px 14px;
border-color: transparent transparent #005bb7 transparent;
}
.shadow {
position: absolute;
opacity: .2;
}

var tl = anime.timeline({
targets: '.params-inheritance-demo .el',
delay: function(el, i) { return i * 200 },
duration: 500,
easing: 'easeOutExpo',
direction: 'alternate',
loop: true
});

tl
.add({
translateX: 250,
// override the easing parameter
easing: 'spring',
})
.add({
opacity: .5,
scale: 2
})
.add({
// override the targets parameter
targets: '.params-inheritance-demo .el.triangle',
rotate: 180
})
.add({
translateX: 0,
scale: 1
});

code.juejin.cn/pen/7123478…


码上掘金太卡了吧,能不能优化下


作者:一起重学前端
来源:juejin.cn/post/7123482707983613965
收起阅读 »

本地运行的前端代码,如何让他人访问

web
有时候,我前端写好了项目,想要给其他人看一下效果,可以选择将代码部署到test环境,也可以选择让外部通过ip来访问,不过前提是在同一个局域网下(比如连接同一个WiFi),下面介绍第二种方式。 修改dev命令 首先我们需要先修改host地址,此处以vue3项目举...
继续阅读 »

有时候,我前端写好了项目,想要给其他人看一下效果,可以选择将代码部署到test环境,也可以选择让外部通过ip来访问,不过前提是在同一个局域网下(比如连接同一个WiFi),下面介绍第二种方式。


修改dev命令


首先我们需要先修改host地址,此处以vue3项目举例


image.png


页面启动之后如下


image.png


正常情况下,script下的dev命令是不会指定host的,我们可以在下面看到Local的地址为默认的127.0.0.1,此时把这个网址发给别人肯定跑不起来。


所以我们可以指定host,比如0.0.0.0,允许所有ip访问


"dev": "vite --host=0.0.0.0",

修改完host后,windows系统的话,我们还需要关闭防火墙(苹果不需要)。重新启动项目可以看到


QQ截图20230406204123(1)(1).png


Network那里的网址,打马赛克的地方其实就是本机的ip地址,window输入cmd打开命令提示符,然后输入ipconfig即可查到ip地址,苹果的话,点击wifi小图标,同时按住option键即可查到ip地址。


在其他电脑或者手机访问


浏览器中输入url即可看到相关页面,此方法也适用于手机端调试


Screenshot_2023-04-06-20-51-03-21_439a3fec0400f89.jpg


作者:笨笨狗吞噬者
来源:juejin.cn/post/7218916720323706935
收起阅读 »

知道尤雨溪为什么要放弃 $ 语法糖提案么?

web
前言 最近看到一篇文章: 《最新,Vue 中的响应性语法糖已废弃》 本文标题中的 $ 语法糖指的就是上文中的响应式语法糖 (Reactivity Transform),那为什么不写 Reactivity Transform 呢?因为这个名实在是太长了… 看了一...
继续阅读 »

前言


最近看到一篇文章:


《最新,Vue 中的响应性语法糖已废弃》


本文标题中的 $ 语法糖指的就是上文中的响应式语法糖 (Reactivity Transform),那为什么不写 Reactivity Transform 呢?因为这个名实在是太长了…


看了一圈评论发现大家觉得被废弃是因为分不清是正常变量还是响应式变量的居多:



下面这个评论说的有一定道理:



Vue 的官网现在已经变成这样了:



以后会不会变成这样:



23次方,一共8种不一样的写法。不对,无虚拟 DOM 模式只能用 Composition API,所以应该不到 8 种写法,你看这不就分裂了嘛!虽说这几种不同的写法也能看懂吧,但每个人都有不同的偏好不同的写法总归不太好。而且你能保证 Vue 不会又改写法吗?Vue 总是受人启发:受 Angular 启发的双向绑定、受 React 启发的虚拟 DOM、受 React Hooks 启发的 Composition API、受 Svelte 启发的语法糖(一开始用的是 Svelte 的 label 写法)、受 Solid 启发的 Vapor Mode无虚拟 DOM 模式




  • 高情商:集百家之长

  • 低情商:方案整合商




开玩笑的哈~ Vue 还是有很多自己的东西的,不过它确实老是抄袭各种框架受各种框架的启发,太杂糅了。今天受这个框架启发做出来这种新 feature、明天又受那个框架启发做出来了另一种新 feature… 估计等 Vue4 出来的时候肯定又是受到了什么其他框架的启发…


我在《无虚拟 DOM 版 Vue 即将到来》这篇文章下看到这样一条评论:



大家觉得这个人说的有没有道理呢?反正我现在感觉 Vue 的各个方案有点太杂糅了,有点像是方案整合商集百家之长,以后指不定就发展成这样了:



当你去网上搜索一些解决方案时,能看到数十种不同的写法是一种什么体验……


不过这条评论真的是高情商:





  • 低情商:Vue 这是啥流行抄啥

  • 高情商:只用 Vue 就能体会到各种流行的技术趋势




跑题了,咱们来说一说 $ 语法糖,它可绝不只有分不清到底是不是响应式变量这一个缺点,它的缺点比优点多得多,我们来具体分析一下。


分析


我们也不要一上来就说这个语法糖有多么多么的不好,如果真这么不好的话尤总也不至于费这么大劲来推动这个提案了对不?这个语法糖在某些情况下确实会大幅改善我们的开发体验,但在另一些情况下不仅不会帮助我们改善体验,反而会增加我们的心智负担,我们来看下面这个案例:


let x = $(0)
let y = $(0)

const update = e => {
 x = e.x
 y = e.y
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))

$watch([x, y], ([x, y]) => console.log(x, y))

看上去很美好是不是,我们终于不用再写 .value 了。



如果看不明白这种写法的话可能是之前没有对其进行过了解


建议先阅读一下这篇《Vue3又出新语法 到底何时才能折腾完?》



不过像这种逻辑我们通常都会提取出去封装成一个函数,因为有可能有很多个组件都用到了获取鼠标位置这个逻辑,你不想在每个用到该逻辑的组件里都复制一遍相同的逻辑吧?那我们就这样:


// useMouse.js
export const useMouse = (dom = window) => {
  let x = $(0)
  let y = $(0)

  const update = e => {
    x = e.x
    y = e.y
}
  onMounted(() => dom.addEventListener('mousemove', update))
  onUnmounted(() => dom.removeEventListener('mousemove', update))

  return { x, y }
}

import { useMouse } from './useMouse.js'

let { x, y } = useMouse()

$watch([x, y], ([x, y]) => console.log(x, y))

如果这么写你就会惊讶的发现根本不生效,因为编译过后就相当于:


import { ref } from 'vue'

export const useMouse = (dom = window) => {
 let x = ref(0)
 let y = ref(0)

 const update = e => {
   x.value = e.x
   y.value = e.y
}
 onMounted(() => dom.addEventListener('mousemove', update))
 onUnmounted(() => dom.removeEventListener('mousemove', update))

 return {
   x: x.value,
   y: y.value
}
}

这就相当于把一个普通值给 return 出去了,普通值是没法在取值或改值时运行一些其他逻辑的,所以我们还不能把值直接 return 出去,而是把这个响应式变量本身给 return 出去:


import { ref } from 'vue'

export const useMouse = (dom = window) => {
 let x = ref(0)
  let y = ref(0)

  const update = e => {
   x.value = e.x
    y.value = e.y
}
  onMounted(() => dom.addEventListener('mousemove', update))
  onUnmounted(() => dom.removeEventListener('mousemove', update))

  return { x, y }
}

所以编译必须还要有还原的功能,把响应式的值给还原成响应式变量:


export const useMouse = (dom = window) => {
 let x = $(0)
 let y = $(0)

 const update = e => {
   x = e.x
   y = e.y
}
 onMounted(() => dom.addEventListener('mousemove', update))
 onUnmounted(() => dom.removeEventListener('mousemove', update))

 return $$({ x, y })
}

但这样又要写 .value 了:


import { useMouse } from './useMouse.js'

let { x, y } = useMouse()

console.log(x.value, y.value)

因为编译器是分析不出来一个函数的返回值到底是不是响应式变量的,所以就又得引入一个 API 来告诉编译器这个函数的返回值有响应式变量:


import { useMouse } from './useMouse.js'

let { x, y } = $fromRefs(useMouse())

console.log(x, y)

大家不觉得这样很麻烦吗?而且搞出那么多莫名其妙的 $ 、$$ 变量。写一堆这玩意真的没感觉比 .value 好到哪去,而且我们还要随时记得某个变量是响应式的,不然在传递的过程中就有可能失去响应性:


// logValue.js
// 接收一个响应式变量并在其变化时将其打印出来

export const logValue = arg => { // 在提案中并未找到如何用语法糖转换函数的参数
 // 也就是说在这种情况下可能没有什么完美的解决方案 那就又要写 .value 了:
 console.log(arg.value)
 // 不过也不是没有解决方案 我们可以用 $computed 来关联一下:
 let argument = $computed(() => arg.value)
 // 这样就可以不用写 .value 了:
 console.log(argument)
 // 但缺点就是太麻烦了 参数少的时候还可以 参数多的时候还能每个都这么写吗?
 // 而且还要为变量取个不同的名字 这对于我们这些英文不好的人来说简直就是场灾难
 $watch(argument, value => console.log(value))
}

import { logValue } from './logValue.js'

let a = $(0)

logValue(a) // 这么传就错啦
logValue($$(a)) // 一定要写成这样

// 假如有函数是需要响应式变量和普通变量混着传的:
let b = 0
logValue($$(a), b, { a: $$(a), b }) // 写成这样真的很乱

还有需要把 ref 变量传给 reactive 字段的情况:


let a = $(0)

const obj = reactive({ a })

console.log(obj.a) // 0
a++
console.log(obj.a) // 还是 0


// 必须写成这样
const obj = reactive({ a: $$(a) })
console.log(obj.a) // 0
a++
console.log(obj.a) // 1

所以说语法糖只能某些情况下改善我们的开发体验,前提就是你不要把响应式变量传来传去的。但 Vue3 的核心卖点之一不就是 Composition API 么?中文官网管这个叫组合式 API,关键词是组合Vue 还把提取出去的可复用函数叫 Composables,翻译过来就是可组合的,如果不把响应式变量传来传去那还组合个P呀!


这个问题可不是只有 Vue 有,来看下 Solid.js 吧:


import { createSignal } from 'solid'

export const useMouse = (dom = window) => {
 const [x, setX] = createSignal(0)
 const [y, setY] = createSignal(0)

 dom.addEventListener('mousemove', ({ x, y }) => {
   setX(x)
   setY(y)
})

 return {
   x: x(),
   y: y()
}
}

同样会有响应式值与响应式变量的问题,只不过就是把 .value 变成了 ()


// 假如有个响应式变量 a

// 打印的是响应式值
console.log(a.value) // Vue
console.log(a()) // Solid

//打印的是响应式变量
console.log(a) // Vue & Solid

是不是看过很多文章说 Solid.js 和 React Hooks 很像、写起来很舒服、什么比 React 还 react 之类的文章?实际上真的就只是 API 设计的相似而已,只要我们想,我们同样也可以把 Vue 的 API 封装成 React 那样:


import { ref } from 'vue'

const useState = value => {
 const result = ref(value)
 const getter = () => result.value
 const setter = newValue => result.value = newValue
 return [getter, setter]
}

const [num, setNum] = useState(0)
setNum(1)

那是不是这样封装一下,Vue 也变得比 React 还 react 了?应该不难看出这只是在自欺欺人罢了,我们传值时照样还得区分到底应该传的是响应式变量本身还是响应式变量的值。


Vue2 为何没这个问题


不知大家有没有思考过:为什么 Vue2 时代大家从来就没听说过丢失响应性、没听过要出什么语法糖之类的问题呢?听过最多有关于语法糖的可能就是 v-model 的双向绑定功能其实就是 @input="xxx" + :value="xxx" 的语法糖。


这是因为 Vue2 时代用的都是 this.xxx,咱们所有的响应式变量全都挂载到了 this 上。取值时 this.xxx 会触发 getter、改值时 this.xxx = xxx 会触发 setter


你可以简单的理解成这样:


// 用 Vue3 来写一段伪代码
import { reactive, watchEffect } from 'vue'

const this = reactive({
a: 1,
b: 2,
c: 3
})

watchEffect(() => console.log(this.a))
this.a++

当然这只是一段伪代码,真这么写是会报错的:



因为 this 是一个关键字,正因为它是一个关键字所以咱们用 this.xxx 才会显得这么的自然。而我们现在的响应式变量都需要自己起名,自己起的名不是关键字,所以用 xx.xxx 就老觉得麻烦,就老想给它解构:


import { reactive, watchEffect, toRefs } from 'vue'

const user = reactive({
name: 'AngularBaby',
age: 34,
beautiful: true
})

console.log(user.name) // 有些人觉得这样写很麻烦
const { name } = user // 就老想给它解构
console.log(name) // 结果就是失去了响应性

// 想要保持响应性 写法就变得更麻烦了
const { name } = toRefs(user)
console.log(name.value)

而且之前用 this 还有一个显著的好处就是只要写法正确,操作 this 上的属性就不用担心响应式的问题,没有那么多心智负担。甚至有人会简单的理解为只要是 this.xxx 就一定会有响应:


export default {
data () {
return { a: 1 }
},
mounted () {
this.a = 2 // 没有心智负担 因为我们知道自己是在改变 this 上的属性
this.a++ // 正确改变 this 上的属性就会存在响应

let b = 2 // 也没有心智负担 因为我们知道这不是 this 上的属性
b++ // 我们不会期待这段代码会有任何的响应
}
}

这样很容易区分哪些是响应式变量而哪些不是,即使有人真的写成了这样:


export default {
data () {
return { a: 1 }
},
mounted () {
let { a } = this
a++ // 我们不会期待这段代码会有任何的响应
}
}

这里也很容易能够看出来我们这样并没有修改 this 上的属性,所以并不会正确响应也是理所应当的一件事。


还有复用逻辑,Vue2 时代有很多人用 Mixins 来复用逻辑:


import mouse from 'mouse.mixin.js'
import position from 'position.mixin.js'

export default {
mixins: [mouse, position],
mounted () {
this.x // 哪来的 x ?
this.y // 哪来的 y ?
// 除了 xy 还有没有其他的未知 this.xxx ?
}
}

可以看到 Mixins 存在很多的弊端,比方说数据来源不清晰、容易产生冲突变量之类的。如果不去看源码的话谁能知道 this.x 到底是 mouse 中的 x 还是 position 的 x 呢?正是由于 Vue2 没有一个完美的复用机制,所以尤大才下定决心将 Vue3 改造成函数式。但函数式没了 this 就又失去了 Vue2 时期的那种… 我不知该怎么形容 Vue2时期的 this.xxx 哈,舒服?自然?反正我是比较喜欢 this.xxx 这种写法的,虽然这种写法是受 Angular 启发(集百家之长)


而且我还比较喜欢的一点就是一些全局挂载的属性:


this.$el
this.$refs
this.$nextTick(() => { /* ... */ })

直接 this.$xxx 就出来了,不用引,既方便又快捷。当然这种方式也有不少坏处,比方说容易被覆盖、不利于 Tree Shaking 之类的…


但我还真的蛮喜欢这种写法的:


// main.js
import Vue from 'vue'

Vue.prototype.$toast = msg => { /* ... */ }

this.$toast('Success!')

如今就会变得就稍麻烦一些:


import toast from './toast.js'

toast('Success!')

虽说后者其实更好,但有没有这样一种可能:既恢复到 Vue2 时期用 this 的便捷、又能享受到 Vue3 组合式的好处:


// 幻想中的写法

this.$data.a = 1 // 相当于 Vue2 时期的 data: { a: 1 } 最终会挂载到 this 上变成 this.a
this.$computed.b = () => this.a * 2 // 相当于 Vue2 时期的 computed: { b () { return this.a * 2 } } 最终会挂载到 this 上变成 this.b

this.$watch.b = value => console.log(value) // 相当于 Vue2 时期的 watch: { b: value => console.log(value) }

let timer
this.$mounted = () => {
timer = setInterval(() => this.a++, 1000)
}
this.$unMounted = () => clearInterval(timer)

复用逻辑:


// 幻想中的写法

import useMouse from './useMouse.js'

({ x: this.$computed.x, y: this.$computed.y } = useMouse())
this.$effect = () => console.log(this.x, this.y)

// 如果用数组解构将会更加的便捷
[this.$computed.x, this.$computed.y] = useMouse()
this.$effect = () => console.log(this.x, this.y)

这样我们的心智负担就又能回到 this 时期了:只要改变 this 属性就会存在响应,否则就无响应,那这个方案有实现的可能吗?在 ES5 时代无可能,但在 ES6 Proxy 的加持下我认为还是可以实现的,那么接下来我们就来试一下。


实验


首先我们回顾一下 Vue3.0 没有 setup 语法糖时期的写法:


<template></template>

<script>
import { defineComponent } from 'vue'

export default defineComponent({
setup () {
console.log(this) // undefined
}
})
</script>

原版的 this 指向为 undefined,那我们怎么改变它的指向呢?我们可以自己写一个 defineComponent


// defineComponent.js

import { defineComponent, reactive } from 'vue'

export default options => {
const { setup } = options
if (typeof setup === 'function') {
options.setup = setup.bind(reactive({}))
}
return defineComponent(options)
}

这样 setup 的指向就变成了 reactive({}),当我们在操作 this 的时候就相当于在操作 reactive({})。但这样并不能满足我们的需求,我们想要的是当我们 this.$data.a 的时候会在 this 上挂载个 a 属性,所以我们要把 reactive 换成一个 Proxy


// createThis.js
import { defineComponent, reactive } from 'vue'

const createData = target => new Proxy({}, {
get: (_, key) => Reflect.get(target, key),
set (_, key, value) {
if (Reflect.getOwnPropertyDescriptor(target, key)) {
console.error(`this.$data.${key} is already defined!`)
return false
}
return Reflect.set(target, key, value)
}
})

export default () => {
const that = reactive({})
const $data = createData(that)
return new Proxy(that, {
get (target, key) {
if (key === '$data') {
return $data
}
return Reflect.get(target, key)
},
set (target, key, value) {
if (key === '$data') {
return console.warn('this.$data is readonly!')
}
return Reflect.set(target, key, value)
}
})
}

// defineComponent.js

import { defineComponent } from 'vue'
import createThis from './createThis.js'

export default options => {
const { setup } = options
if (typeof setup === 'function') {
const that = createThis()
options.setup = (...args) => {
setup.apply(that, args)
return that
}
}
return defineComponent(options)
}

也就是说我们利用 Proxy 来把 $data 给代理出去了,当我们访问 $data 的时候其实已经是另一个代理对象了,在这个代理对象上设置的属性全部都设置到 this 上。this 现在就相当于 reactive({}),所以 this.$data.a = 1就相当于 reactive({ a: 1 }),我们来试一下:



完美运行,只要你能搞懂上面的那段代码,那么接下来的 $computed$watch$watchEffect$readonly$shallow$nextTick$mounted$unMounted 等一大堆 API 相信你也知道该怎么做了,我就不在这里占用过多的篇幅了。这里直接用码上掘金贴上源码及用法,向大家展示一下可行性:



当然这源码并不是把所有 API 都实现了,目前只实现了 this.$datathis.$computedthis.$watchthis.$mounted 等几个常用的 API 供大家参考,感兴趣的可以去把全部的 API 都实现一下,我这里犯懒就先不实现那么全乎了。



这么好的东西为啥犯懒不实现呢?因为这玩意有一定的弊端。对了,掘金好像在文章中屏蔽了来自码上掘金alert,必须点查看详情才能看到。为了防止大家也犯懒不点进去看,这里直接给大家贴上动图:



我们的写法类似于下面这样:


export default defineComponent({
setup () {
this.$data.count = 0
this.$watch.count = (value, oldValue) => alert(`验证 this.$watch:按钮上的值将会从 ${oldValue} 变为 ${value}`)

this.$computed.doubleCount = () => this.count * 2
this.$watch.doubleCount = value => alert(`验证 this.$computed:${this.count} 的双倍是 ${value}`

this.$mounted = () => alert('验证 this.$mounted:已挂载')
}
})

怎么样,是不是很好玩?我是蛮喜欢这种 this 混合着函数式的写法。但刚刚说了这玩意有一定的弊端,只能拿来当玩具玩玩所以我才懒得实现的那么全乎。那么它究竟有多大的弊端呢?


弊端


Vue3 比 Vue2 更优秀的一个点是支持 tree shaking,在你仅仅只用了 Vue 的某几项功能的情况下打包体积会小很多。但我们刚刚的做法无疑是开了历史的倒车,又回去了!并且随着 Vue3.2 的崛起,setup 语法糖得到了大多数人的认可,因为它确实很方便。但这样我们就无法修改 this 指向了:


<template>
<h1>{{ a }}</h1>
</template>

<script setup>
this.$data.a = 1 // 怎么修改 this 指向
</script>

有人可能会说加个函数不就得了:


<template>
<h1>{{ a }}</h1>
</template>

<script setup>
import setup from './setup.js'

setup(() => {
this.$data.a = 1
})
</script>

这样虽然可以修改 this 指向,但随之而来的就是 <template> 模板里面访问不到 a 这个变量了,除非我们写成这样:


<template>
<h1>{{ a }}</h1>
</template>

<script setup>
import { toRefs } from 'vue'
import setup from './setup.js'

const { a, b, c, d, e, f } = toRefs(setup(() => {
this.$data.a = 1
this.$data.b = 2
this.$data.c = 3
this.$data.d = 4
this.$data.e = 5
this.$data.f = 6
}))
</script>

我相信没人会愿意写成这样,所以我们必须借助 babel 插件来完成编译,思路是把 this 编译成 reactive({}),类似于下面这样:


// 编译前
this.$data.a = 1
this.$data.b = 2
this.$data.c = 3
this.$data.d = 4
this.$data.e = 5
this.$data.f = 6

// 编译后
import { reactive } from 'vue'
import createThis from 'createThis.js'
import createData from 'createData.js'

const that = createThis(reactive({}))
createData(that)

that.$data.a = 1
that.$data.b = 2
that.$data.c = 3
that.$data.d = 4
that.$data.e = 5
that.$data.f = 6

不过这样还是会引入我们刚刚写的那些代码,虽然代码量并不高,但如果压根就不引入任何额外的代码才好,所以如果能编译成这样才是最完美的:


// 编译前
this.$data.a = 1
this.$data.b = 2
this.$data.c = 3
this.$data.d = 4
this.$data.e = 5
this.$data.f = 6

console.log(this.a)

// 编译后
import { reactive } from 'vue'

const that = reactive({
a: 1,
b: 2,
c: 3,
d: 4,
e: 5,
f: 6
})

console.log(that.a)

但如果这样编译的话又有可能发生如下情况:


import useXxx from './useXxx'

this.$data.a = 1

useXxx.call(this)

这样会被编译成:


import { reactive } from 'vue'
import useXxx from './useXxx'

const that = reactive({ a: 1 })

useXxx.call(that)

万一这个 useXxx 里写了这样一段逻辑:


// useXxx.js

expurt default function () {
this.$watch.a = value => console.log(value)
}

这样就不会按照我们所期待方式去运行了,因为在编译后就相当于:


// 伪代码

const obj = reactive({ a: 1 })

useXxx.call(obj)

function useXxx () {
this.$watch.a = value => console.log(value)
}

这样会直接报错,因为 reactive({ a: 1 }).$watch 是 undefinedundefined.a 会报错,所以并没有特别完美的解决方案。最好是检测如果没把 this 作为参数传走或者没有哪个函数用了 fn.call(this) 来把 this 指向当前上下文的话,就按照最完美的方式(不引入任何杂七杂八的代码)编译。否则就引入一点运行时,反正也没多少:


// 编译前
import useMouse from 'useMouse'

this.$data.a = 1
this.$watch.a = value => console.log(value)

this.$mounted = () => window.addEventListener(...)
this.$unmounted = () => window.removeEventListener(...)

[this.$computed.x, this.$computed.y] = useMouse.call(this)

// 编译后
import { reactive } from 'vue'
import createThis from 'createThis'
import createData from 'createData'
import createWatch from 'createWatch'
import createMounted from 'createMounted'
import createUnmounted from 'createUnmounted'

const that = createThis(reactive({
a: 1
}))
createData(that)
createWatch(that)
createMounted(that)
createUnmounted(that)

that.$data.a = 1
that.$watch.a = value => console.log(value)

that.$mounted = () => window.addEventListener(...)
that.$unmounted = () => window.removeEventListener(...)

[that.$computed.x, that.$computed.y] = useMouse.call(that)

但仔细一想还是有可能有 bug,比方说你这个组件里没用到 this.$readonly,但 useMouse 用了的话,那岂不是又要报错。那就在 Vue 组件之外也编译,如果在外面有用到 this.$xxx,那就在相应的位置:


// 编译前
export default function useMouse () {
this.$readonly.a = 1
}

// 编译后
import createReadonly from 'createReadonly'

export default function useMouse () {
createReadonly(this)
this.$readonly.a = 1
}

缺陷


这种写法不仅仅是有弊端,还有一个非常严重的缺陷。虽然刚刚我们设想了一下用编译的方案来解决弊端的可能,但有个最大的缺陷是连编译都无法解决的。这个最大的缺陷就是对 TS 的支持,如果不用 TS 还好,但如果你的项目里有用 TS,那么这种写法就完全没法用:



不知怎么才能让 TS 也支持这种想法,查了国内外很多资料,最后找到了这两篇文章:



《TypeScript plugin 实践 —— 类型,编辑器与业务价值》


《基于 TypeScript 的开发者体验增强 - 朝夕相处却始终被忽视的领域》



也不知道这个 TS Language Service 有没有可能能够实现我们这种语法,感兴趣的小伙伴可以好好研究一下。我们目前只实现了运行时方案,但编译方案才是未来。写这篇文章的目的是希望给大家提供一个思路,看看大家觉得这个想法怎么样。万一大家觉得这个想法非常好,把它推给官方,官方实现了呢?



当然上述的那些话也可能仅仅只是过于美好的想象,现实很有可能是压根儿就没有人对这个想法感兴趣,官方也认为这是在开历史的倒车并且对 TS 支持不好不予实现。



往期精彩文章



作者:Veev
来源:juejin.cn/post/7222874734185922597
收起阅读 »

Low-Code,一定“low”吗?

web
作者:京东保险 吴凯 前言 低代码是一组数字技术工具平台,基于图形化拖拽、参数化配置等更为高效的方式,实现快速构建、数据编排、连接生态、中台服务。通过少量代码或不用代码实现数字化转型中的场景应用创新。本文将重点介绍低代码相关知识,包括低代码的定义与意义、相关概...
继续阅读 »

作者:京东保险 吴凯


前言


低代码是一组数字技术工具平台,基于图形化拖拽、参数化配置等更为高效的方式,实现快速构建、数据编排、连接生态、中台服务。通过少量代码或不用代码实现数字化转型中的场景应用创新。本文将重点介绍低代码相关知识,包括低代码的定义与意义、相关概念、行业发展等,同时介绍京东的低代码工具,期望能帮助大家更好地认识与理解低代码。


一、低代码介绍


2014年,Forrester(著名研究咨询机构)提出“低代码”的术语,定义为“利用很少或几乎不需要写代码就可以快速开发应用,并可以快速配置和部署的一种技术和工具”。或者说是“(能力)多(出品)快(质量)好(功夫)省”。



这个定义体现出低代码的核心价值:


1、低代码开发平台能够实现业务应用的快速交付。低代码开发的重点是开发应用快,不像传统意义上仅仅是一个应用的开发,而是通过可视化的开发,达到“设计及交付”的目的,提高开发效率。


2、低代码开发平台能够降低业务应用的开发成本。低代码开发投入更低,主要体现在开发时间短,可以快速配置和部署,同时也更容易使非开发人员上手。


二、我们为什么用低代码


低代码可以降本增效,一方面低代码的出现避免了“反复造轮子”的问题,其通过可视化的编程方式实现“千人千面”的效果,驱使技术回归本源--支持业务。另一方面低代码的生命周期贯穿整个软件开发周期(设计、开发、测试、交付),周期上的各角色都可以在同一个低代码开发平台上紧密协作,由传统的开发方式变为敏捷开发,实现了快速交付的目的。


低代码的使用场景:


1、构建新的SaaS应用,而借助低代码平台可以快速有效地构建、测试和推出应用。低代码与SaaS的结合,可以为企业提供独特的业务解决方案。


2、基于Web的门户网站是提供自助服务的数字化工具。使用低代码开发平台,更简单、更快速地构建个性化应用,打造数字化平台。


3、历史系统的迁移或升级。基于低代码技术:一方面,最大限度地保留遗留系统的代码,保留其“公共数据服务”;另一方面,基于遗留系统的开发环境和能力构建相应的“功能适配器”,然后在此基础上,通过低代码技术快速定制新业务和流程的交互式UI与业务逻辑。


4、应用复杂性低,业务流程相对简单,95%的应用场景可以通过低代码完成。



三、低代码会使程序员失业吗


回答这个问题,我们首先需要搞明白:低代码和零代码的区别。作为程序员,大家都会把低代码认为是零代码,这也是会被误解程序员失业的原因之一。


低代码,意味着反复迭代的代码质量高,在必要的时候,也会进行代码的编写;BUG更少,减少了测试环节的工作量。


零代码,字面意思:完全不需要任何代码即可完成应用开发,从软件开发效率看,**零代码是低代码的最终形态。**零代码平台由于采用全部都是封装模块进行搭建,所有控件都已经被固化了,所以用零代码平台搭建的系统想要进行扩展是有些困难的。


现实是,编码的最终目的是支持业务,业务逻辑的复杂与否依旧需要人来掌握,低代码只是写的少,并不是不写代码,这并不会导致程序员的失业



四、低代码的行业现状


2021年11月11日,Forrester发布《The State Of Low-Code Platforms In China》,这是低代码概念提出者第一次将视角聚焦在中国。Forrester认为,低代码目前在国内主要应用于银行、保险、零售、医疗、政府、制造、电信和建筑行业。比如,为了针对各个业务单元量身定制各种业务需求,中国建设银行采用云枢为其分布式开发团队构建统一的低代码开发平台(LCDP)。另外,报告指出:中国企业数字化转型过程中,有58%的决策者正在采用低代码工具进行软件构建,另有16%的决策者计划采用低代码。


目前,国内的低代码开发平台不断涌现,Forrester划分了9类低代码平台厂商:


▪数字流程自动化(BPM):炎黄盈动(AWS PaaS)、奥哲(云枢)


▪公有云:阿里巴巴(宜搭)、百度(爱速搭)、华为(应用魔方)、微软(Power Platform)、腾讯(微搭)


▪面向专业开发者的低代码开发平台:ClickPaaS、葡萄城(活字格)、Mendix、Outsystems


▪面向业务开发者的低代码开发平台:捷德(Joget DX)、轻流


▪AI/机器学习:第四范式(HyperCycle)


▪BI:帆软(简道云)


▪协作管理:泛微(E-Builder)


▪流程自动化机器人(RPA):云扩(ViCode)、来也(流程创造者)


▪数字化运营平台:博科(Yigo)、金蝶(金蝶云·苍穹)、浪潮(iGIX)、用友(YonBIP)


由此可知,中国的低代码市场正在飞速发展,各种低代码工具的发布问世,也意味着低代码未来将成为主流的开发方式。


五、业内的低代码平台


1、Out-System


OutSytems 作为国外著名的低代码开发平台,出发点就是简化整个应用开发和交付的过程,让开发人员可以快速响应市场的需求变化。通过可视化和模型驱动的开发方式,大幅减少时间和成本。并通过预构建的连接器加速集成后端系统,同时还提供了一个集中式的控制台来管理应用的版本、发布以及部署。


OutSytems 生成的应用可以不依赖于 OutSytems 运行。数据是直接存储到数据库,这样就可以通过任何标准的 ETL、 BI或其他第三方数据工具来访问数据。


官网:

http://www.outsystems.com/demos/


2、阿里-云凤蝶


云凤蝶是蚂蚁金服体验技术部的重点研发项目,是面向中后台产品的快速研发平台,主要用户面向工程师,使用场景专注在标准化的中后台产品研发,目标是为了提高效率。


云凤蝶的核心思路是将组件生产和组件组装这两部分工作进行职责分离,通过建立一条组件组装流水线,打通 npm 组件的一键导入流程,从而完成一条产业链式的分工协作,最终实现规模化的快速生产。


淘系的“乐高”系统以及蚂蚁金服的“金蝉”系统、“云凤蝶”系统成微阿里系主要的低代码开发工具。


3、京东-星链


星链是京东科技消金基础研发部开发的一款研发效能提升工具,主要为面向后端服务研发需求,因此前端简洁可视化开发界面需要满足极致的细节,并依赖其自身后端的能力来实现用户的低代码。


核心概念:


VMS可视化微服务应用,是星链的基本单元,同时VMS也是一种模型,各种配置均在模型中。支持京东中间件(JSF、定时任务、JMQ,缓存服务、分布式配置等),服务流程编排,DEBUG调试等;


Serverless部署,星链的部署及配置均由系统自动分配。用户只需关注系统的开发,资源的使用情况。


地址:jddlink.jd.com/


结论


低代码,一定不“low”,却更low-code。


参考:


2021年低代码平台中国市场现状分析报告

http://www.authine.com/report/56.h…



作者:京东云开发者
来源:juejin.cn/post/7217449801633808439
收起阅读 »

使用fabric从零开始打造互动白板(一)

web
最近由于公司业务需要,原本使用第三方的互动白板功能,准备自行实现。一方面为了节省开支,另一方面自行实现定制化也更强一些,能够和现有业务更好的结合,用以满足第三方互动白板无法实现的一些功能。 一、功能整理 既然需求明确了,于是就开始着手整理白板所需的功能。由于...
继续阅读 »

最近由于公司业务需要,原本使用第三方的互动白板功能,准备自行实现。一方面为了节省开支,另一方面自行实现定制化也更强一些,能够和现有业务更好的结合,用以满足第三方互动白板无法实现的一些功能。



一、功能整理


既然需求明确了,于是就开始着手整理白板所需的功能。由于我们的直播课都是大班课,只需要讲师在白板上操作,用户端只用来展示,也就不需要太多复杂的功能,结合几个第三方互动白板,归纳整理出了如下几个需要实现的功能点:



  • 自由画笔

  • 文字书写

  • 橡皮擦

  • 画三角、圆形、矩形

  • 画直线和箭头

  • 清空画布

  • 撤销重做

  • 画布缩放

  • 插入PPT图片及切换控制


二、技术选择


观察了现有的互动白板,都是在Canvas进行操作,为了节约开发时间于是找到了fabric这个库,这个库本身就实现了不少功能,可以大大的减少开发时间。


结合我熟悉的技术栈,最终选定了使用Vite+Vue3+TypeScript进行demo版本的构建。


相关代码放在github上,链接地址:使用vite+typescript+fabric创建的互动白板项目


三、页面结构


参考其他白板的布局进行了页面结构的搭建。白板使用一个容器进行包裹,左侧是工具区域,包括画笔、橡皮擦、画线、清空画布等各种绘制工具;左下角是撤销、重做和画布缩放控制区域;右上角是插入PPT文件控制区域;右下角是PPT控制区域。最后提供了一个容器进行的白板预览。


效果图如下:


demo.png


页面结构代码如下:


<template>
<div>
<div class="canvas-wrap">
<div class="tool-box-out">
<ToolBox></ToolBox>
</div>
<div class="redo-undo-box">
<RedoUndo></RedoUndo>
</div>
<div class="zoom-controller-box">
<ZoomController></ZoomController>
</div>
<div class="room-controller-box" v-show="!isPreviewShow">
<div class="page-controller-mid-box">
<div className="page-preview-cell" @click="insertPPT">
<img style="width: 28px" :src="folder" alt="文件"/>
</div>
</div>
</div>
<div class="page-controller-box" v-show="isShowPPTControl">
<div className="page-controller-mid-box">
<PageController></PageController>
<div className="page-preview-cell" @click="handlePreviewState(true)">
<img :src="pages" alt="PPT预览"/>
</div>
</div>
</div>
<div class="preview-controller-box" v-show="isShowPPTControl&&isPreviewShow">
<PreviewController @handlePreviewState="handlePreviewState"></PreviewController>
</div>
<canvas id="canvas" width="800" height="450"></canvas>
</div>
<div class="canvas-wrap">
<canvas id="canvas2" width="800" height="450"></canvas>
</div>
</div>
</template>

四、初始化白板


为了方便后续使用,这里对fabric进行封装,后续拓展也能更加灵活。相关代码如下:


import { fabric } from "fabric";

class FabricCanvas {
constructor(canvasId: string) {

// 初始化画布,默认可绘制
this.canvas = new fabric.Canvas(canvasId, {
isDrawingMode: true,
selection: false,
includeDefaultValues: false, // 转换成json对象,不包含默认值
});
}
}

使用示例:


const canvas = new FabricCanvas('canvas');

五、工具栏相关功能实现


页面框架搭建完成之后,就开始各种功能的开发。这里将fabric封装成一个类,所有需要实现的方法在类中实现,方便后续灵活调用。


选择


选择功能实现比较简单,只需要关闭绘制模式,然后将画布设置可选中。相关代码如下:


this.canvas.isDrawingMode = false;
this.canvas.selection = true;
// 设置鼠标光标
this.canvas.defaultCursor = 'auto';

自由画笔


fabric提供了各种丰富的笔刷,实现自由绘制这里调用了PencilBrush类,并将isDrawingMode设置为true即可。相关代码如下:


  public drawFreeDraw() {
this.canvas.freeDrawingBrush = new fabric.PencilBrush(this.canvas);
this.canvas.freeDrawingBrush.color = '#ff0000'
this.canvas.freeDrawingBrush.width = 5
this.canvas.freeDrawingCursor = 'default'
this.canvas.isDrawingMode = true;
}

使用示例:


const canvas = new FabricCanvas('canvas');
// 自由画笔
canvas.drawFreeDraw();

文字书写


文字输入使用fabric提供的IText方法,实现文字编辑和修改,并且让输入框自动获取焦点,方便输入。相关代码如下:


  public drawText(text: string, options?: ITextOptions): void {
const textObj = new fabric.IText(text, {
editingBorderColor: '#ff0000',
padding: 5,
...options
});
this.canvas.add(textObj);
this.canvas.defaultCursor = 'text'
this.currentShape = textObj;
// 文本打开编辑模式
textObj.enterEditing();
// 文本编辑框获取焦点
textObj.hiddenTextarea.focus()
this.setActiveObject(textObj);
}

使用示例:


const canvas = new FabricCanvas('canvas');
// 绘制文本
canvas.drawText('Hello World!', { left: 50, top: 250, fontSize: 24, fill: 'red' })

橡皮擦


fabric内置了EraserBrush用来实现橡皮擦功能,不过默认构建排除了该功能,避免库文件过大。如需使用,需要进入node_modules/fabric目录执行下面的命令重新构建:


node build.js modules=ALL exclude=gestures,accessors requirejs minifier=uglifyjs

构建完成之后就可以使用EraserBrush来实现橡皮擦功能了,相关代码如下:


public eraser(options?: any): void {
this.canvas.freeDrawingBrush = new fabric.EraserBrush(this.canvas, options);
this.canvas.freeDrawingBrush.width = 10
this.canvas.freeDrawingCursor = 'default'
this.canvas.isDrawingMode = true;
}

使用示例:


const canvas = new FabricCanvas('canvas');
// 橡皮擦
canvas.erase({ width: 10 });

画三角、圆形、矩形


画三角形、圆形、矩形方法相似,直接调用fabric封装的对应方法即可。


这里以绘制矩形为例,相关代码实现如下:


public drawRect(options: IRectOptions): void {
const rect = new fabric.Rect({ ...this.options, ...options });
this.canvas.add(rect);
this.currentShape = rect;
this.canvas.defaultCursor = 'crosshair'
}

使用示例:


const canvas = new FabricCanvas('canvas');
// 绘制矩形
canvas.drawRect({ left: 50, top: 150, width: 100, height: 50, fill: 'green', stroke: 'black' });

画直线和箭头


画直线功能fabric直接内置了,调用对应的方法即可,这里重点讲画箭头。画箭头的本质是在直线的一端加上一个三角形,根据起始点使用三角函数计算好三角形的方向,这样就组合成一个箭头了。这里搬运网友的画箭头方法,直接对画直线功能进行拓展,并封装成fabric中的功能模块,方便后续调用。相关代码如下:


import { fabric } from 'fabric';

fabric.Arrow = fabric.util.createClass(fabric.Line, {
type: 'arrow',
superType: 'drawing',
initialize(points: number[], options: any) {
if (!points) {
const { x1, x2, y1, y2 } = options;
points = [x1, y1, x2, y2];
}
options = options || {};
this.callSuper('initialize', points, options);
},
_render(ctx: any) {
this.callSuper('_render', ctx);
ctx.save();
const xDiff = this.x2 - this.x1;
const yDiff = this.y2 - this.y1;
const angle = Math.atan2(yDiff, xDiff);
ctx.translate((this.x2 - this.x1) / 2, (this.y2 - this.y1) / 2);
ctx.rotate(angle);
ctx.beginPath();
// Move 5px in front of line to start the arrow so it does not have the square line end showing in front (0,0)
ctx.moveTo(5, 0);
ctx.lineTo(-5, 5);
ctx.lineTo(-5, -5);
ctx.closePath();
ctx.fillStyle = this.stroke;
ctx.fill();
ctx.restore();
},
});

fabric.Arrow.fromObject = (options: any, callback: any) => {
const { x1, x2, y1, y2 } = options;
return callback(new fabric.Arrow([x1, y1, x2, y2], options));
};

export default fabric.Arrow;

封装好的代码,直接导入调用即可。相关代码如下:


import Arrow from "./objects/Arrow";
// 绘制箭头
public drawArrow(x1: number, y1: number, x2: number, y2: number, options?: ILineOptions) {
const arrow = new Arrow([x1, y1, x2, y2], { ...this.options, ...options });
this.canvas.add(arrow);
this.currentShape = arrow;
this.canvas.defaultCursor = 'crosshair'
}

使用示例:


const canvas = new FabricCanvas('canvas');
// 绘制矩形
canvas.drawArrow(10, 50, 100, 50, { stroke: 'blue', strokeWidth: 2 })

通过鼠标绘制图形


实际使用白板过程中,上面这些图形、线条的绘制都是通过鼠标拖动进行,这样更加灵活一些。


通过鼠标绘制图形,需要对鼠标的mouse:downmouse:movemouse:up事件进行监听,相关代码如下:


// 监听鼠标事件
this.canvas.on("mouse:down", this.onMouseDown.bind(this));
this.canvas.on("mouse:move", this.onMouseMove.bind(this));
this.canvas.on("mouse:up", this.onMouseUp.bind(this));

这里以绘制矩形为例,实现通过通过鼠标绘制一个矩形框。



  1. 当鼠标按下时,在鼠标按下的地方绘制一个宽高为0的矩形。相关代码如下:


// 是否处于绘制状态
private isDrawing = false;
// 鼠标起点坐标x
private startX = 0;
// 鼠标起点多表y
private startY = 0;

// 鼠标按下事件处理函数
private onMouseDown(event: IEvent) {
// 如果当前有活动的元素则不进行后续绘制
const activeObject = this.canvas.getActiveObject();
if (!event.pointer || activeObject) return;

// 切换成绘制状态
this.isDrawing = true;
// 记录当前坐标点
const { x, y } = event.pointer;
this.startX = x;
this.startY = y;

// 在当前坐标绘制一个矩形
this.drawRect({
left: x,
top: y,
width: 0,
height: 0,
});
}


  1. 在鼠标移动的过程中,动态的修改矩形的宽高,并实时渲染。相关代码如下:


// 鼠标移动事件处理函数
private onMouseMove(event: IEvent) {
if (!this.isDrawing || !event.pointer || !this.currentShape) return;

// 计算宽高
const { x, y } = event.pointer;
const width = x - this.startX;
const height = y - this.startY;

// 设置宽高
this.currentShape.set({
width,
height,
});

// 更新画布
this.canvas.renderAll();
}


  1. 当鼠标抬起后,改变绘制状态。相关代码如下:


// 鼠标抬起事件处理函数
private onMouseUp() {
this.isDrawing = false;
this.currentShape = null;
}

如果想要更加灵活的在各种图形和线条中自由的进行切换,并通过鼠标绘制,在提供的demo中也进行了对应的封装。
相关代码请在github中进行查看,对fabric的各种功能封装


清空画布


清空画布直接调用画布的清除方法即可,相关代码如下:


// 清空画布
public clearCanvas() {
this.canvas.clear();
}

不过该方法会清除画布上包含背景的所有内容,如果不想画布背景也被清除,可以遍历画布上的所有对象进行移除。相关代码如下:


// 移除所有对象
public removeAllObject() {
this.canvas.getObjects().forEach((obj) => {
this.canvas.remove(obj);
});
}

六、工具栏布局
将工具栏封装成ToolBox组件,并在组件中实现各种工具的切换。


工具栏显示效果
组件布局代码如下:


<template>
<div class="tool-mid-box-left">
<div class="tool-box-cell-box-left" v-for="item in tools" :key="item.shapeType">
<div class="tool-box-cell"
@click="clickAppliance(item.shapeType)">
<img :src="item.shapeType === currentShapType ? item.iconActive : item.icon" :alt="item.name"/>
</div>
</div>
<div class="tool-box-cell-box-left">
<div class="tool-box-cell"
@click="clickClear">
<img :src="clear" alt="清屏"/>
</div>
</div>
</div>
</template>

相关功能事件实现的代码如下:


const currentShapType = ref<string>("pencil")

// 设置当前工具
function clickAppliance(type: DrawingTool) {
currentShapType.value = type;
canvas?.value.setDrawingTool(type)
}

// 清屏事件处理
function clickClear() {
canvas?.value.clearCanvas()
}

设置当前绘制工具


// 设置绘图工具
public setDrawingTool(tool: DrawingTool) {
if(this.drawingTool === tool) return;
this.canvas.isDrawingMode = false;
this.canvas.selection = false;

this.drawingTool = tool;
if (tool === "pencil") {
this.drawFreeDraw();
} else if (tool === "eraser") {
this.eraser();
} else if (tool === "select") {
this.canvas.selection = true;
this.canvas.defaultCursor = 'auto'
}
}

其他功能说明


为避免文章太长,撤销重做、画布缩放、插入PPT图片及切换控制等功能的实现在后续文章中介绍。


如果等不及,可以直接在github上查看相关代码实现:使用vite+typescript+fabric创建的互动白板项目


六、参考资料



作者:江阳小道
来源:juejin.cn/post/7221348552513077305
收起阅读 »

new 一个对象时,js 做了什么?

web
前言 在 JavaScript 中, 通过 new 操作符可以创建一个实例对象,而这个实例对象继承了原对象的属性和方法。因此,new 存在的意义在于它实现了 JavaScript 中的继承,而不仅仅是实例化了一个对象。 new 的作用 我们先通过例子来了解 n...
继续阅读 »

前言


在 JavaScript 中, 通过 new 操作符可以创建一个实例对象,而这个实例对象继承了原对象的属性和方法。因此,new 存在的意义在于它实现了 JavaScript 中的继承,而不仅仅是实例化了一个对象。


new 的作用


我们先通过例子来了解 new 的作用,示例如下:


function Person(name) {
this.name = name
}
Person.prototype.sayName = function () {
console.log(this.name)
}
const t = new Person('小明')
console.log(t.name) // 小明
t.sayName() // 小明

从上面的例子中我们可以得出以下结论:





  • new 通过构造函数 Person 创建出来的实例对象可以访问到构造函数中的属性。




  • new 通过构造函数 Person 创建出来的实例可以访问到构造函数原型链中的属性,也就是说通过 new 操作符,实例与构造函数通过原型链连接了起来。





构造函数 Person 并没有显式 return 任何值(默认返回 undefined),如果我们让它返回值会发生什么事情呢?


function Person(name) {
this.name = name
return 1
}
const t = new Person('小明')
console.log(t.name) // 小明

在上述例子中的构造函数中返回了 1,但是这个返回值并没有任何的用处,得到的结果还是和之前的例子完全一样。我们又可以得出一个结论:



构造函数如果返回原始值,那么这个返回值毫无意义。



我们再来试试返回对象会发生什么:


function Person(name) {
this.name = name
return {age: 23}
}
const t = new Person('小明')
console.log(t) // { age: 23 }
console.log(t.name) // undefined

通过上面这个例子我们可以发现,当返回值为对象时,这个返回值就会被正常的返回出去。我们再次得出了一个结论:



构造函数如果返回值为对象,那么这个返回值会被正常使用。



总结:这两个例子告诉我们,构造函数尽量不要返回值。因为返回原始值不会生效,返回对象会导致 new 操作符没有作用。


实现 new


首先我们要清楚,在使用 new 操作符时,js 做了哪些事情:



  1. js 在内部创建了一个对象

  2. 这个对象可以访问到构造函数原型上的属性,所以需要将对象与构造函数连接起来

  3. 构造函数内部的this被赋值为这个新对象(即this指向新对象)

  4. 返回原始值需要忽略,返回对象需要正常处理


知道了步骤后,我们就可以着手来实现 new 的功能了:


function _new(fn, ...args) {
const newObj = Object.create(fn.prototype);
const value = fn.apply(newObj, args);
return value instanceof Object ? value : newObj;
}

测试示例如下:


function Person(name) {
this.name = name;
}
Person.prototype.sayName = function () {
console.log(this.name);
};

const t = _new(Person, "小明");
console.log(t.name); // 小明
t.sayName(); // 小明

以上就是关于 JavaScript 中 new 操作符的作用,以及如何来实现一

作者:codinglin
来源:juejin.cn/post/7222274630395379771
个 new 操作符。

收起阅读 »