注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

token过期自动跳转到登录页面

vue
这几天项目提测,测试给我提了个bug,说token过期,路由应该自动跳转到登陆页面,让用户重新登录。先说下一些前置条件, 1:我公司的token时效在生产环境设置为一个小时,当token过期,所有接口都直接返回 2:每次路由跳转都会对token进行判断,设置了...
继续阅读 »

这几天项目提测,测试给我提了个bug,说token过期,路由应该自动跳转到登陆页面,让用户重新登录。先说下一些前置条件,
1:我公司的token时效在生产环境设置为一个小时,当token过期,所有接口都直接返回
2:每次路由跳转都会对token进行判断,设置了一个全局的beforeEach钩子函数,如果token存在就跳到你所需要的页面,否则就直接跳转到登录页面,让用户登录重新存取token


接口返回的信息
{
code:10009,
msg:'token过期',
data:null
}
全局的路由钩子函数
router.beforeEach(async(to, from, next) => {
//获取token
// determine whether the user has logged in
const hasToken = getToken()

if (hasToken) {
//token存在,如果当前跳转的路由是登录界面
if (to.path === '/login') {
// if is logged in, redirect to the home page
next({ path: '/' })
NProgress.done()
} else {
//在这里,就拉去用户权限,判断用户是否有权限访问这个路由
} catch (error) {
// remove token and go to login page to re-login
await store.dispatch('user/resetToken')
Message.error(error || 'Has Error')
next(`/login?redirect=${to.path}`)
NProgress.done()
}
} else {
//token不存在
if (whiteList.indexOf(to.path) !== -1) {
//如果要跳转的路由在白名单里,则跳转过去
next()
} else {
//否则跳转到登录页面
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
})

所以我直接在对所有的请求进行拦截,当响应的数据返回的code是10009,就直接清空用户信息,重新加载页面。我对代码简化了下,因为用户在登录时就会把token,name以及权限信息存在store/user.js文件里,所以只要token过期,把user文件的信息清空。这样,在token过期后,刷新页面或者跳转组件时,都会调用全局的beforeEach判断,当token信息不存在就会直接跳转到登录页面


import axios from 'axios'
import { MessageBox, Message } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'

const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API,
timeout: 5000
})
//发送请求时把token携带过去
service.interceptors.request.use(
config => {
if (store.getters.token) {
config.headers['sg-token'] = getToken()
}
return config
},
error => {
console.log(error)
return Promise.reject(error)
}
)

service.interceptors.response.use(
response => {
console.log(response.data)
const res = response.data

// token过期,重返登录界面
if (res.code === 10009) {
store.dispatch('user/logout').then(() => {
location.reload(true)
})
}
return res
},
error => {
console.log('err' + error) // for debug
Message({
message: error.msg,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)

export default service

好啦,关于token的分享就到这里了,以上代码根据你们项目的情况换成你们的数据,有错误欢迎指出来!


作者:阿狸要吃吃的
链接:https://juejin.cn/post/6947970204320137252

收起阅读 »

Vue3,我决定不再使用Vuex

vue
在开发基于Vue3的项目中发现我们可以不再依赖Vuex也能很方便的来管理数据,只需要通过Composition Api可以快捷的建立简单易懂的全局数据存储. 创建State 通过reactive我们来创建state,暴露的IState是用来方便其他文件来接受S...
继续阅读 »

在开发基于Vue3的项目中发现我们可以不再依赖Vuex也能很方便的来管理数据,只需要通过Composition Api可以快捷的建立简单易懂的全局数据存储.


创建State


通过reactive我们来创建state,暴露的IState是用来方便其他文件来接受State对象


import { reactive } from 'vue'

export interface IState {
code: string
token: string
user: any
}

export const State: IState = {
code: '',
token: '',
user: {}
}

export function createState() {
return reactive(State)
}


创建Action


我们来创建Action来作为我们修改State的方法


import { reactive } from 'vue'
import { IState } from './state'

function updateCode(state: IState) {
return (code: string) => {
state.code = code
}
}

function updateToken(state: IState) {
return (token: string) => {
state.token = token
}
}

function updateUser(state: IState) {
return (user: any) => {
state.user = user
}
}

/**
* 创建Action
* @param state
*/
export function createAction(state: IState) {
return {
updateToken: updateToken(state),
updateCode: updateCode(state),
updateUser: updateUser(state)
}
}

通过暴露的IState我们也可以实现对State的代码访问.


创建Store


创建好StateAction后我们将它们通过Store整合在一起.


import { reactive, readonly } from 'vue'
import { createAction } from './action'
import { createState } from './state'

const state = createState()
const action = createAction(state)

export const useStore = () => {
const store = {
state: readonly(state),
action: readonly(action)
}

return store
}

这样我们就可以在项目中通过调用useStore访问和修改State,因为通过useStore返回的State是通过readonly生成的,所以就确认只有Action可以对其进行修改.


// 访问state
const store = useStore()
store.state.code

// 调用action
const store = useStore()
store.action.updateCode(123)

这样我们就离开了Vuex并创建出了可是实时更新的数据中心.


持久化存储


很多Store中的数据还是需要实现持久化存储,来保证页面刷新后数据依然可用,我们主要基于watch来实现持久化存储


import { watch, toRaw } from 'vue'

export function createPersistStorage<T>(state: any, key = 'default'): T {
const STORAGE_KEY = '--APP-STORAGE--'

// init value
Object.entries(getItem(key)).forEach(([key, value]) => {
state[key] = value
})

function setItem(state: any) {
const stateRow = getItem()
stateRow[key] = state
const stateStr = JSON.stringify(stateRow)
localStorage.setItem(STORAGE_KEY, stateStr)
}

function getItem(key?: string) {
const stateStr = localStorage.getItem(STORAGE_KEY) || '{}'
const stateRow = JSON.parse(stateStr) || {}
return key ? stateRow[key] || {} : stateRow
}

watch(state, () => {
const stateRow = toRaw(state)
setItem(stateRow)
})

return readonly(state)
}

通过watchtoRaw我们就实现了statelocalstorage的交互.


只需要将readonly更换成createPersistStorage即可


export const useStore = () => {
const store = {
state: createPersistStorage<IState>(state),
action: readonly(action)
}

return store
}

这样也就实现了对Store数据的持久化支持.


作者:程序员紫菜苔
链接:https://juejin.cn/post/6898504898380464142

收起阅读 »

TypeScript 函数的重载

函数的重载 什么是函数重载呢?允许函数接收不同数量或类型的参数时,做出不同的处理。比如说这个例子: function double(x: number | string): number | string { if (typeof x === 'num...
继续阅读 »

函数的重载


什么是函数重载呢?允许函数接收不同数量或类型的参数时,做出不同的处理。比如说这个例子:


function double(x: number | string): number | string {
if (typeof x === 'number') {
return x * 2;
} else {
return x + ', ' + x;
}
}

本来这个函数,期望输入是 number,输出就是 number。或者输入是 string,输出就是 string。但是我们使用这种联合类型来书写的话,就会存在一个问题。


image-20211011230002191.png


那如何解决这个问题呢?我们可以使用函数的重载


function double(x: number): number; // 输入是 number 类型,输出也是 number 类型
function double(x: string): string;
function double(x: number | string): number | string {
if (typeof x === 'number') {
return x * 2;
} else {
return x + ', ' + x;
}
}

let d = double(1);

image-20211011230140623.png
需要注意的是,函数重载是从上往下匹配,如果有多个函数定义,有包含关系的话,需要把精确的,写在最前面。


习题-根据函数重载知识,完善下面代码块


function paramType (param: ______): string;
function paramType (param: string): string;
function paramType (param: string | number): string {
return typeof param;
};

paramType('panda');
paramType(10);

答案:number


解析:


重载允许一个函数接收不同数量或类型的参数,然后做不同处理。


// 函数声明
function paramType (param: ______): string;
function paramType (param: string): string;
// 函数实现
function paramType (param: string | number): string {
return typeof param;
};

在函数实现中参数的类型为 string | number,故答案为 number;


资料-高阶函数


在维基百科中对于高阶函数的定义是这样的:



在数学和计算机科学中,高阶函数是至少满足下列一个条件的函数:



  • 接受一个或多个函数作为输入

  • 输出一个函数



在 JavaScript 的世界中,大家应该都有听说过「函数是一等公民」( first-class citizens )的说法。这里实际上是指在 JavaScript 中,函数被视为 Object 类型,我们可以将函数( function )作为参数传递给其他函数,并且也可以将函数作为其他函数的返回值传递。


因为函数是对象,所以 JavaScript 天然就支持高阶函数的写法。


或许你对高阶函数感到陌生,但实际上在日常的代码编写中,我们一定都用到过高阶函数。


接受一个或多个函数作为输入


我们在日常的学习和开发中,一定遇到过使用回调函数( callback )的场景。回调函数是在完成所有其他操作后,在操作结束时执行的函数。我们通常将回调函数作为最后一个参数,用匿名函数的形式传入。在拥有异步操作的场景中,支持回调函数的传入是至关重要的。


例如我们发送一个 Ajax 请求,我们通常需要在服务器响应完成后进行一些操作,同时在 Web 端,一些需要等待用户响应的行为,如点击、键盘输入等场景也需要用到回调函数。我们看下面的例子


let $submitButton = document.querySelector('#submit-button');

$submitButton.addEventListener('click', function() {
alert('您点击了提交按钮!');
});

这里我们通过将匿名函数作为参数的形式将它传递了 addEventListener 函数。我们也可以改造一下:


let $submitButton = document.querySelector('#submit-button');

let showAlert = function() {
alert('您点击了提交按钮!');
}

$submitButton.addEventListener('click', showAlert);

请注意,这里我们给 addEventListener 传递参数的时候,使用的是 showAlert 而不是 showAlert()。在没有括号的时候,我们传递的是函数本身,而有括号的话,我们传递的是函数的执行结果。


这里将具名函数( named function )作为参数传递给其他函数的能力也为我们使用纯函数(pure functions )提供了很大的想象空间,我们可以定义一个小型的 纯函数库 ,其中的每个纯函数都可以作为参数被复用至多处。


将函数作为结果返回


我们来假想一种场景:假如你拥有一个个人网站,在里面写了很多篇文章。在你的文章中经常介绍你的个人网站,网址是 myblog.com,后来你的站点域名变成了 my-blog.com。这时候你需要将文章中的 myblog.com 替换为 my-blog.com。你或许会这样做:


let replaceSiteUrl = function(text) {
return text.replace(/myblog\.com/ig, 'my-blog.com');
}

在域名变更后,你又想更改网站名称,你可能会这么做:


let replaceSiteName = function(text) {
return text.replace(/MySite/ig, 'MyBlog');
}

上述做法是行之有效的,但是你或许会烦于每次信息变更都要写一个新的函数来适配,而且上述两段代码看起来相似度极高。这时候我们可以考虑使用 高阶函数 来复用这段代码:


let replaceText = function(reg, newText, source){
return function(source) {
return source.replace(reg, newText);
}
}

let replaceSiteUrl = replaceText(/myblog\.com/ig, 'my-blog.com');

console.log(replaceSiteUrl('My site url is https://myblog.com')); // My site url is https://my-blog.com

在上述代码中,我们用到了 JavaScript 函数并不关心他们收到多少个参数的特性,如果没有传递会自动忽略并且认为是 undefined。


总结


高阶函数看起来并没有那么神秘,它是我们日常很自然而然就用到的场景。高阶函数可以帮助我们将一些通用的场景抽象出来,达到多处复用的结果。这也是一种良好的编程习惯。


链接:https://juejin.cn/post/7029481950691737630

收起阅读 »

js 有哪些内置对象

全局的对象( global objects )或称标准内置对象,不要和 "全局对象(global object)" 混淆。 这里说的全局的对象是说在全局作用域里的对象。全局作用域中的其他对象可以由用户的脚本创建或由宿主程序提供。 js 中的内置对象主要指的是...
继续阅读 »

全局的对象( global objects )或称标准内置对象,不要和 "全局对象(global object)" 混淆。


这里说的全局的对象是说在全局作用域里的对象。全局作用域中的其他对象可以由用户的脚本创建或由宿主程序提供。



js 中的内置对象主要指的是在程序执行前存在全局作用域里的由 js 定义的一些全局值属性、函数和用来实例化其他对象的构造函数对象。一般我们经常用到的如全局变量值 NaN、undefined,全局函数如 parseInt()、parseFloat() 用来实例化对象的构造函数如 Date、Object 等,还有提供数学计算的单体内置对象如 Math 对象。



标准内置对象的分类


(1)值属性,这些全局属性返回一个简单值,这些值没有自己的属性和方法。


   例如 Infinity、NaN、undefined、null 字面量

(2)函数属性,全局函数可以直接调用,不需要在调用时指定所属对象,执行结束后会将结果直接返回给调用者。


   例如 eval()、parseFloat()、parseInt() 等

(3)基本对象,基本对象是定义或使用其他对象的基础。基本对象包括一般对象、函数对象和错误对象。


   例如 Object、Function、Boolean、Symbol、Error 等

(4)数字和日期对象,用来表示数字、日期和执行数学计算的对象。


   例如 Number、Math、Date 

(5)字符串,用来表示和操作字符串的对象。


   例如 String、RegExp

(6)可索引的集合对象,这些对象表示按照索引值来排序的数据集合,包括数组和类型数组,以及类数组结构的对象。


例如 Array

(7)使用键的集合对象,这些集合对象在存储数据时会使用到键,支持按照插入顺序来迭代元素。


   例如 Map、Set、WeakMap、WeakSet

(8)矢量集合,SIMD 矢量集合中的数据会被组织为一个数据序列。


   例如 SIMD 等

(9)结构化数据,这些对象用来表示和操作结构化的缓冲区数据,或使用 JSON 编码的数据。


   例如 JSON 等

(10)控制抽象对象


   例如 Promise、Generator 等

(11)反射


   例如 Reflect、Proxy

(12)国际化,为了支持多语言处理而加入 ECMAScript 的对象。


   例如 Intl、Intl.Collator 等

(13)WebAssembly


(14)其他


例如 arguments

链接:https://juejin.cn/post/7029486810745012260

收起阅读 »

为什么的我的z-index不生效了??

最近开发时遇到了一个有趣的现象z-index&transform等连用造成了z-index不生效,因此想借此机会记录一下学习成果。本篇文章偏概念性,请在专业人士的监督下食用。Stacking Context 层叠上下文这是 HTML 中的一个三维概念(...
继续阅读 »

最近开发时遇到了一个有趣的现象z-index&transform等连用造成了z-index不生效,因此想借此机会记录一下学习成果。

本篇文章偏概念性,请在专业人士的监督下食用。

Stacking Context 层叠上下文

这是 HTML 中的一个三维概念(举个不太合适的🌰类似皮影戏?)用图片更容易明白。

假设这是我们看到的一个页面(左图),但是实际上他是通过这样展现的(右图),我们肉眼所见的画面可能是多个层叠加展示的,由此就会涉及到不同层的层级问题。

stacking context - mdn - 链接

Stacking Context 的创建

这里只简单介绍一下常用的 Stacking Context 。

  1. 天生具有 Stacking Context 的元素: HTML

  2. 常用混搭款:

    1. position的值为relative/absolute && z-index !== 'auto'会产生 Stacking Context ;
    2. position的值为fixed/sticky
    3. display: flex/inline-flex/grid && z-index !== 'auto'
    4. transform/filter !== 'none'
    5. opacity < 1
    6. -webkit-overflow-scrolling: touch

Stacking Context 的关系

  1. Stacking Context 可以嵌套,但内部的 Stacking Context 将受制于外部的 Stacking Context;
  2. Stacking Context 和兄弟元素相互独立;
  3. 元素的层叠次序是被包含在父元素的 Stacking Context 中的;
  4. 通常来说,最底层的 Stacking Context 是 <HTML>标签创建的。

层叠次序

在一个层叠上下文内,不同元素的层叠次序如下图所示(由内到外):

  1. 最底层的是当前层叠上下文的装饰性内容,如背景颜色、边框等;
  2. 其次是负值的 z-index;
  3. 然后是布局相关的内容,如块状盒子、浮动盒子;
  4. 接着是内容相关的元素,如inline水平盒子;
  5. 再接着是 z-index:auto/0/不依赖 z-index 的(子)层叠上下文;
  6. 最上面的就是 z-index 值为正的元素;

概括来说,z-index为正 > z-index:auto/z-index:0/不依赖z-index的层叠上下文 > 内容 > 布局 > 装饰。

选自张鑫旭《CSS世界》图7-7

回应标题:为什么我的 z-index 不生效了?

这个标题内容非常的宽泛,我们提供如下的解题思路:

  1. 是否配合使用了可用的 postion / 弹性布局 / grid布局?
    1. 没配合自然不生效。
  2. z-index: -1; 为什么没有生效?
    1. 检查你对应的父元素是否也创建了 Stacking Context,大概率是的,根据 #层叠顺序那一章可以知道,负的z-index的次序是高于当前层叠上下文的背景的;
    2. 解决方案:取消父元素的 Stacking Context / 元素外包裹一层新的元素。

作者:vivi_chen
链接:https://juejin.cn/post/7028858045882957838

收起阅读 »

【译】3 个能优化网站可用性但被忽视的细节

根据 Adobe 的调查显示,给定 15 分钟的时间浏览内容,三分之二的用户更愿意将时间花费在视觉上吸引人的内容。用户也希望网站能在至少 5 秒内加载。因此,设计一个速度快、满意度高的网站(或应用)应成为每个设计师关注的重点。 视觉设计是很难被忽视的,因为我们...
继续阅读 »

根据 Adobe 的调查显示,给定 15 分钟的时间浏览内容,三分之二的用户更愿意将时间花费在视觉上吸引人的内容用户也希望网站能在至少 5 秒内加载。因此,设计一个速度快、满意度高的网站(或应用)应成为每个设计师关注的重点。


视觉设计是很难被忽视的,因为我们作为设计师,喜欢设计视觉上吸引人的东西。虽然美学非常重要,但在有限的时间内,设计师往往倾向于放弃可用性。优化应用/网站的可用性需要你深入地了解客户的目标。网站的可以性可以通过不同的方式来衡量,举例如下:



  1. UI 的清晰度有多高?

  2. 页面上的“障碍“有多少?

  3. 导航是否遵循逻辑结构?


让我们谈谈这些可以提高可用性的细节。


1. 更少的选择


作出选择是耗费精力的,所以为用户理清或甚至是排除不必要的障碍能减少所谓的分析瘫痪。分析瘫痪是指用户因为要考虑的选择太多而感到困惑或沮丧。



根据心理学家 Mark Lepper 和 Sheena Iyengar 的研究指出,更多的选择往往会导致更少的销售额。他们分析了 754 名消费者在面临多种选择时的行为,研究是这样进行的:


在第一天,他们在一家高档食品超市中摆了 24 种果酱,但在第二天,桌子上只摆有 6 种果酱。结果显示摆有 24 种果酱的桌子收到了更多的关注,但第二天的销量却比第一天的来得更好。这个现象可以用一句话来说明:“选择泛滥(spoilt for choice)”。


过量的选择将导致分析瘫痪。用户无法做出决策,因为他们面临太多的选择了。当你考虑那些各种各样的果酱时,它们之中可能某些比较便宜,而另一些可能味道更好等等。我们的大脑将尝试“解码”哪个选择最物有所值。这需要时间和思考,所以结果是转换的速度降低了,甚至导致用户放弃做出选择。



深入阅读:希克定律。希克定律指出,做出决定所需的时间会随着选项的数量增加而增加。该定律证实了减少选择数量能提高转化率这个概念。正如他们所说,“少即是多”



解决方法:个性化的内容


预期设计(由 Spotify,Netflix 和 Google 采用)能帮助使用者减少决策疲劳,其中人工智能(AI)能用于预测用户想要什么。应用和网站展示的“最受欢迎”的栏目就是例子之一,背后的逻辑是:因为其他的用户对这件商品感兴趣,所以你也可能对它感兴趣。


对于零售网站来说,另一种方式是整合“最畅销的商品”或“心愿单”,例如亚马逊的“购买此商品的客户也购买了……”推荐引擎。



冷知识:亚马逊的推荐引擎占其总收入的 30%。人工智能根据用户搜索历史和购物篮中的商品来预测用户想要购买的商品。




2. 极简导航


对于包含多个类别和子类的的网站,导航应成为用户体验(UX)的重中之重,尤其是在移动设备上。移动端网站难以导航,更容易导致分析瘫痪。


为了提升可用性,菜单中包含的项目数量应维持在 7 个以内(这同样适用于下拉菜单)。这样做还能更容易地指示用户所在的位置,降低用户跳出率。


为什么?因为用户时常会忘记他们之前在做什么,尤其是当他们打开多个标签页的时候!


3. 在导航中显示当前位置


进度追踪器能指示用户在界面中的当前位置。根据 Kantar 和 Lightspeed Research 的研究指出,这些指示器能提高用户参与度和客户满意度。


典型的网络冲浪者(或应用程序用户)通常会在一时之内打开多个标签页(或应用程序),因此他们很容易忘记在某个标签页中未完成的任务。有时侯,分析瘫痪的困境是由用户自己造成的!


设计师应该意识到他们的应用程序或网站不会是用户使用的唯一应用程序或网站,当用户面临太多打开着的标签页时,这通常会导致健忘。一个标注用户所在位置的指示器是非常有帮助的。否则,用户可能不仅会忘记他们在做什么,而且会完全不再注意它。


解决方法:面包屑导航


面包屑用于表示用户在哪里,以及他们来自哪里。你可能听说过《汉赛尔和格莱特》这个经典童话故事,这对兄妹用面包屑帮助他们找到回家的路,也避免了他们在森林中绕圈子。


面包屑导航能描述用户的路径。你在亚马逊,NewEgg 和其他的一些需要展示大量内容的线上零售网站都能看到这一点。这能帮助用户记得他们上次所在的位置(如果他们中途因任何原因离开屏幕),并帮助他们在遇到死胡同时找到回去的路。



结论


总的来说,你可以通过帮助用户专注于重要的事情来有效的提高网站的可用性; 温和地引导他们,在必要时进行总结,并优化用户体验以确保用户能找到他们想找的东西。


作者:披着狼皮的羊_
链接:https://juejin.cn/post/7028491022107672613

收起阅读 »

setTimeout的执行你真的了解吗?

setTimeout的创建和执行 我们知道setTimeout是一个延时器,它会在规定的时间后延迟执行回调函数,这篇文章就来说说setTimeout它是怎么执行的。 首先我们知道消息队列是用来存储宏任务的,并且主线程会按照顺序取出队列里的任务依次执行,所以为了...
继续阅读 »

setTimeout的创建和执行


我们知道setTimeout是一个延时器,它会在规定的时间后延迟执行回调函数,这篇文章就来说说setTimeout它是怎么执行的。


首先我们知道消息队列是用来存储宏任务的,并且主线程会按照顺序取出队列里的任务依次执行,所以为了保证setTimeout能够在规定的时间内执行,setTimeout创建的任务不会被添加到消息队列里,与此同时,浏览器还维护了另外一个队列叫做延迟消息队列,该队列就是用来存放延迟任务,setTimeout创建的任务会被存放于此,同时它会被记住创建时间,延迟执行时间。


然后我们看下具体例子:


setTimeout(function showName() { console.log('showName') }, 1000)
setTimeout(function showName() { console.log('showName1') }, 1000)
console.log('martincai')

以上例子执行是这样:



  • 1.从消息队列中取出宏任务进行执行(首次任务直接执行)

  • 2.执行setTimeout,此时会创建一个延迟任务,延迟任务的回调函数是showName,发起时间是当前的时间,延迟时间是第二个参数1000ms,然后该延迟任务会被推入到延迟任务队列

  • 3.执行console.log('martincai')代码

  • 4.从延迟队列里去筛选所有已过期任务(当前时间 >= 发起时间 + 延迟时间),然后依次执行


所以我们可以看到showName和showName1同时执行,原因是两个延迟任务都已经过期


循环源码:


void MainTherad(){
for(;;){
// 执行消息队列中的任务
Task task = task_queue.takeTask();
ProcessTask(task);

// 执行延迟队列中的任务
ProcessDelayTask()

if(!keep_running) // 如果设置了退出标志,那么直接退出线程循环
break;
}
}

删除延迟任务


clearTimeout是windows下提供的原生方法,用于删除特定的setTimeout的延迟任务,我们在定义setTimeout的时候返回值就是当前任务的唯一id值,那么clearTimeout就会拿着id在延迟消息队列里查找对应的任务,将其踢出队列即可


setTimeout的几个注意点:



  1. setTimeout会受到消息队列里的宏任务的执行时间影响,上面我们可以看到延迟消息队列的任务会在消息队列的弹出的当前任务执行完之后再执行,所以当前任务的执行时间会阻碍到setTimeout的延迟任务的执行时间


  function showName() {
setTimeout(function show() {
console.log('show')
}, 0)
for (let i = 0; i <= 5000; i++) {}
}
showName()

这里去执行一遍可以发现setTimeout并不是在0ms左右执行,中间会有明显的延迟,因为setTimeout在执行的时候首先会将任务放入到延迟消息队列里,等到showName执行完之后,才会去延迟队列里去查找已过期的任务,这里setTimeout任务会被showName耽误



  1. setTimeout嵌套下会有4ms的延迟


Chrome会把嵌套5层以上的setTimeout后当作阻塞方法,在第6次调用setTimeout的时候会自动将延时器更改为至少4ms的延迟时间



  1. 未激活的页面的setTimeout更改为至少1000ms


当前tab页面不在active状态的时候,setTimeout的延迟至少会被更改1000ms,这样做是为了减少性能消耗和电量消耗



  1. 延迟时间有最大值


目前Chrome、Firefox等主流浏览器都是用32bit去存储延时时间,所以最大值是2的31次方 - 1


  setTimeout(() => {
console.log(1)
}, 2 ** 31)

以上代码会立即执行


链接:https://juejin.cn/post/7028836586745757710
收起阅读 »

从22行有趣的源码库中,我学到了 callback promisify 化的 Node.js 源码实现

我们经常会在本地git仓库切换tags,或者git仓库切换tags。那么我们是否想过如果获取tags呢。本文就是学习 remote-git-tags 这个22行代码的源码库。源码不多,但非常值得我们学习。 阅读本文,你将学到: 1. Node 加载采用什么模块...
继续阅读 »

我们经常会在本地git仓库切换tags,或者git仓库切换tags。那么我们是否想过如果获取tags呢。本文就是学习 remote-git-tags 这个22行代码的源码库。源码不多,但非常值得我们学习。


阅读本文,你将学到:


1. Node 加载采用什么模块
2. 获取 git 仓库所有 tags 的原理
3. 学会调试看源码
4. 学会面试高频考点 promisify 的原理和实现
5. 等等

刚开始先不急着看上千行、上万行的源码。源码长度越长越不容易坚持下来。看源码讲究循序渐进。比如先从自己会用上的百来行的开始看。


我之前在知乎上回答过类似问题。


一年内的前端看不懂前端框架源码怎么办?


简而言之,看源码


循序渐进
借助调试
理清主线
查阅资料
总结记录

2. 使用


import remoteGitTags from 'remote-git-tags';

console.log(await remoteGitTags('https://github.com/lxchuan12/blog.git'));
//=> Map {'3.0.5' => '6020cc35c027e4300d70ef43a3873c8f15d1eeb2', …}

3. 源码



Get tags from a remote Git repo



这个库的作用是:从远程仓库获取所有标签。


原理:通过执行 git ls-remote --tags repoUrl (仓库路径)获取 tags


应用场景:可以看有哪些包依赖的这个包。
npm 包描述信息


其中一个比较熟悉的是npm-check-updates



npm-check-updates 将您的 package.json 依赖项升级到最新版本,忽略指定的版本。



还有场景可能是 github 中获取所有 tags 信息,切换 tags 或者选定 tags 发布版本等,比如微信小程序版本。


看源码前先看 package.json 文件。


3.1 package.json


// package.json
{
// 指定 Node 以什么模块加载,缺省时默认是 commonjs
"type": "module",
"exports": "./index.js",
// 指定 nodejs 的版本
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"scripts": {
"test": "xo && ava"
}
}

众所周知,Node 之前一直是 CommonJS 模块机制。 Node 13 添加了对标准 ES6 模块的支持。


告诉 Node 它要加载的是什么模块的最简单的方式,就是将信息编码到不同的扩展名中。
如果是 .mjs 结尾的文件,则 Node 始终会将它作为 ES6 模块来加载。
如果是 .cjs 结尾的文件,则 Node 始终会将它作为 CommonJS 模块来加载。


对于以 .js 结尾的文件,默认是 CommonJS 模块。如果同级目录及所有目录有 package.json 文件,且 type 属性为module 则使用 ES6 模块。type 值为 commonjs 或者为空或者没有 package.json 文件,都是默认 commonjs 模块加载。


关于 Node 模块加载方式,在《JavaScript权威指南第7版》16.1.4 Node 模块 小节,有更加详细的讲述。此书第16章都是讲述Node,感兴趣的读者可以进行查阅。


3.2 调试源码


# 推荐克隆我的项目,保证与文章同步,同时测试文件齐全
git clone https://github.com/lxchuan12/remote-git-tags-analysis.git
# npm i -g yarn
cd remote-git-tags && yarn
# VSCode 直接打开当前项目
# code .

# 或者克隆官方项目
git clone https://github.com/sindresorhus/remote-git-tags.git
# npm i -g yarn
cd remote-git-tags && yarn
# VSCode 直接打开当前项目
# code .

用最新的VSCode 打开项目,找到 package.jsonscripts 属性中的 test 命令。鼠标停留在test命令上,会出现 运行命令调试命令 的选项,选择 调试命令 即可。


调试如图所示:


调试如图所示


VSCode 调试 Node.js 说明如下图所示:


VSCode 调试 Node.js 说明


跟着调试,我们来看主文件。


3.3 主文件仅有22行源码


// index.js
import {promisify} from 'node:util';
import childProcess from 'node:child_process';

const execFile = promisify(childProcess.execFile);

export default async function remoteGitTags(repoUrl) {
const {stdout} = await execFile('git', ['ls-remote', '--tags', repoUrl]);
const tags = new Map();

for (const line of stdout.trim().split('\n')) {
const [hash, tagReference] = line.split('\t');

// Strip off the indicator of dereferenced tags so we can override the
// previous entry which points at the tag hash and not the commit hash
// `refs/tags/v9.6.0^{}` → `v9.6.0`
const tagName = tagReference.replace(/^refs\/tags\//, '').replace(/\^{}$/, '');

tags.set(tagName, hash);
}

return tags;
}

源码其实一眼看下来就很容易懂。


3.4 git ls-remote --tags


支持远程仓库链接。


git ls-remote 文档


如下图所示:


ls-remote


获取所有tags git ls-remote --tags https://github.com/vuejs/vue-next.git


把所有 tags 和对应的 hash值 存在 Map 对象中。


3.5 node:util


Node 文档



Core modules can also be identified using the node: prefix, in which case it bypasses the require cache. For instance, require('node:http') will always return the built in HTTP module, even if there is require.cache entry by that name.



也就是说引用 node 原生库可以加 node: 前缀,比如 import util from 'node:util'


看到这,其实原理就明白了。毕竟只有22行代码。接着讲述 promisify


4. promisify


源码中有一段:


const execFile = promisify(childProcess.execFile);

promisify 可能有的读者不是很了解。


接下来重点讲述下这个函数的实现。


promisify函数是把 callback 形式转成 promise 形式。


我们知道 Node.js 天生异步,错误回调的形式书写代码。回调函数的第一个参数是错误信息。也就是错误优先。


我们换个简单的场景来看。


4.1 简单实现


假设我们有个用JS加载图片的需求。我们从 这个网站 找来图片。


examples
const imageSrc = 'https://www.themealdb.com/images/ingredients/Lime.png';

function loadImage(src, callback) {
const image = document.createElement('img');
image.src = src;
image.alt = '公众号若川视野专用图?';
image.style = 'width: 200px;height: 200px';
image.onload = () => callback(null, image);
image.onerror = () => callback(new Error('加载失败'));
document.body.append(image);
}

我们很容易写出上面的代码,也很容易写出回调函数的代码。需求搞定。


loadImage(imageSrc, function(err, content){
if(err){
console.log(err);
return;
}
console.log(content);
});

但是回调函数有回调地狱等问题,我们接着用 promise 来优化下。


4.2 promise 初步优化


我们也很容易写出如下代码实现。


const loadImagePromise = function(src){
return new Promise(function(resolve, reject){
loadImage(src, function (err, image) {
if(err){
reject(err);
return;
}
resolve(image);
});
});
};
loadImagePromise(imageSrc).then(res => {
console.log(res);
})
.catch(err => {
console.log(err);
});

但这个不通用。我们需要封装一个比较通用的 promisify 函数。


4.3 通用 promisify 函数


function promisify(original){
function fn(...args){
return new Promise((resolve, reject) => {
args.push((err, ...values) => {
if(err){
return reject(err);
}
resolve(values);
});
// original.apply(this, args);
Reflect.apply(original, this, args);
});
}
return fn;
}

const loadImagePromise = promisify(loadImage);
async function load(){
try{
const res = await loadImagePromise(imageSrc);
console.log(res);
}
catch(err){
console.log(err);
}
}
load();

需求搞定。这时就比较通用了。


这些例子在我的仓库存放在 examples 文件夹中。可以克隆下来,npx http-server .跑服务,运行试试。


examples


跑失败的结果可以把 imageSrc 改成不存在的图片即可。


promisify 可以说是面试高频考点。很多面试官喜欢考此题。


接着我们来看 Node.js 源码中 promisify 的实现。


4.4 Node utils promisify 源码


github1s node utils 源码


源码就暂时不做过多解释,可以查阅文档。结合前面的例子,其实也容易理解。


utils promisify 文档


const kCustomPromisifiedSymbol = SymbolFor('nodejs.util.promisify.custom');
const kCustomPromisifyArgsSymbol = Symbol('customPromisifyArgs');

let validateFunction;

function promisify(original) {
// Lazy-load to avoid a circular dependency.
if (validateFunction === undefined)
({ validateFunction } = require('internal/validators'));

validateFunction(original, 'original');

if (original[kCustomPromisifiedSymbol]) {
const fn = original[kCustomPromisifiedSymbol];

validateFunction(fn, 'util.promisify.custom');

return ObjectDefineProperty(fn, kCustomPromisifiedSymbol, {
value: fn, enumerable: false, writable: false, configurable: true
});
}

// Names to create an object from in case the callback receives multiple
// arguments, e.g. ['bytesRead', 'buffer'] for fs.read.
const argumentNames = original[kCustomPromisifyArgsSymbol];

function fn(...args) {
return new Promise((resolve, reject) => {
ArrayPrototypePush(args, (err, ...values) => {
if (err) {
return reject(err);
}
if (argumentNames !== undefined && values.length > 1) {
const obj = {};
for (let i = 0; i < argumentNames.length; i++)
obj[argumentNames[i]] = values[i];
resolve(obj);
} else {
resolve(values[0]);
}
});
ReflectApply(original, this, args);
});
}

ObjectSetPrototypeOf(fn, ObjectGetPrototypeOf(original));

ObjectDefineProperty(fn, kCustomPromisifiedSymbol, {
value: fn, enumerable: false, writable: false, configurable: true
});
return ObjectDefineProperties(
fn,
ObjectGetOwnPropertyDescriptors(original)
);
}

promisify.custom = kCustomPromisifiedSymbol;

5. ES6+ 等知识


文中涉及到了Mapfor of、正则、解构赋值。


还有涉及封装的 ReflectApplyObjectSetPrototypeOfObjectDefinePropertyObjectGetOwnPropertyDescriptors 等函数都是基础知识。



作者:若川
链接:https://juejin.cn/post/7028731182216904740

收起阅读 »

3D 穿梭效果?使用 CSS 轻松搞定

背景 周末在家习惯性登陆 Apex,准备玩几盘。在登陆加速器的过程中,发现加速器到期了。 我一直用的腾讯网游加速器,然而点击充值按钮,提示最近客户端升级改造,暂不支持充值(这个操作把我震惊了~)。只能转头下载网易 UU 加速器。 打开 UU 加速器首页,映入眼...
继续阅读 »

背景


周末在家习惯性登陆 Apex,准备玩几盘。在登陆加速器的过程中,发现加速器到期了。


我一直用的腾讯网游加速器,然而点击充值按钮,提示最近客户端升级改造,暂不支持充值(这个操作把我震惊了~)。只能转头下载网易 UU 加速器


打开 UU 加速器首页,映入眼帘的是这样一幅画面:


11.gif


瞬间,被它这个背景图吸引。


出于对 CSS 的敏感,盲猜了一波这个用 CSS 实现的,至少也应该是 Canvas。打开控制台,稍微有点点失望,居然是一个 .mp4文件:



再看看 Network 面板,这个 .mp4 文件居然需要 3.5M?



emm,瞬间不想打游戏了。这么个背景图,CSS 不能搞定么


使用 CSS 3D 实现星际 3D 穿梭效果


这个技巧,我在 奇思妙想 CSS 3D 动画 | 仅使用 CSS 能制作出多惊艳的动画? 也有提及过,感兴趣的可以一并看看。


假设我们有这样一张图形:



这张图先放着备用。在使用这张图之前,我们会先绘制这样一个图形:


<div class="g-container">
<div class="g-group">
<div class="item item-right"></div>
<div class="item item-left"></div>
<div class="item item-top"></div>
<div class="item item-bottom"></div>
<div class="item item-middle"></div>
</div>
</div>

body {
background: #000;
}
.g-container {
position: relative;
}
.g-group {
position: absolute;
width: 100px;
height: 100px;
left: -50px;
top: -50px;
transform-style: preserve-3d;
}
.item {
position: absolute;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, .5);
}
.item-right {
background: red;
transform: rotateY(90deg) translateZ(50px);
}
.item-left {
background: green;
transform: rotateY(-90deg) translateZ(50px);
}
.item-top {
background: blue;
transform: rotateX(90deg) translateZ(50px);
}
.item-bottom {
background: deeppink;
transform: rotateX(-90deg) translateZ(50px);
}
.item-middle {
background: rgba(255, 255, 255, 0.5);
transform: rotateX(180deg) translateZ(50px);
}

一共设置了 5 个子元素,不过仔细看 CSS 代码,其中 4 个子元素都设置了 rotateX/Y(90deg/-90deg),也就是绕 X 轴或者 Y 轴旋转了 90°,在视觉上是垂直屏幕的一张平面,所以直观视觉上我们是不到的,只能看到一个平面 .item-middle


我将 5 个子 item 设置了不同的背景色,结果如下:



现在看来,好像平平无奇,确实也是。


不过,见证奇迹的时候来了,此时,我们给父元素 .g-container 设置一个极小的 perspective,譬如,设置一个 perspective: 4px,看看效果:


.g-container {
position: relative;
+ perspective: 4px;
}
// ...其余样式保持不变

此时,画风骤变,整个效果就变成了这样:



由于 perspective 生效,原本的平面效果变成了 3D 的效果。接下来,我们使用上面准备好的星空图,替换一下上面的背景颜色,全部都换成同一张图,神奇的事情发生了:



由于设置的 perspective 非常之下,而每个 item 的 transform: translateZ(50px) 设置的又比较大,所以图片在视觉上被拉伸的非常厉害。但是整体是充满整个屏幕的。


接下来,我们只需要让视角动起来,给父元素增加一个动画,通过控制父元素的 translateZ() 进行变化即可:


.g-container{
position: relative;
perspective: 4px;
perspective-origin: 50% 50%;
}

.g-group{
position: absolute;
// ... 一些定位高宽代码
transform-style: preserve-3d;
+ animation: move 8s infinite linear;
}

@keyframes move {
0%{
transform: translateZ(-50px) rotate(0deg);
}
100%{
transform: translateZ(50px) rotate(0deg);
}
}


看看,神奇美妙的星空穿梭的效果就出来了,Amazing:



美中不足之处在于,动画没能无限衔接上,开头和结尾都有很大的问题。


当然,这难不倒我们,我们可以:



  1. 通过叠加两组同样的效果,一组比另一组通过负的 animation-delay 提前行进,使两组动画衔接起来(一组结束的时候另外一组还在行进中)

  2. 再通过透明度的变化,隐藏掉 item-middle 迎面飞来的突兀感

  3. 最后,可以通过父元素的滤镜 hue-rotate 控制图片的颜色变化


我们尝试修改 HTML 结构如下:


<div class="g-container">
<div class="g-group">
<div class="item item-right"></div>
<div class="item item-left"></div>
<div class="item item-top"></div>
<div class="item item-bottom"></div>
<div class="item item-middle"></div>
</div>
<!-- 增加一组动画 -->
<div class="g-group">
<div class="item item-right"></div>
<div class="item item-left"></div>
<div class="item item-top"></div>
<div class="item item-bottom"></div>
<div class="item item-middle"></div>
</div>
</div>


修改后的核心 CSS 如下:


.g-container{
perspective: 4px;
position: relative;
// hue-rotate 变化动画,可以让图片颜色一直变换
animation: hueRotate 21s infinite linear;
}

.g-group{
transform-style: preserve-3d;
animation: move 12s infinite linear;
}
// 设置负的 animation-delay,让第二组动画提前进行
.g-group:nth-child(2){
animation: move 12s infinite linear;
animation-delay: -6s;
}
.item {
background: url(https://z3.ax1x.com/2021/08/20/fLwuMd.jpg);
background-size: cover;
opacity: 1;
// 子元素的透明度变化,减少动画衔接时候的突兀感
animation: fade 12s infinite linear;
animation-delay: 0;
}
.g-group:nth-child(2) .item {
animation-delay: -6s;
}
@keyframes move {
0%{
transform: translateZ(-500px) rotate(0deg);
}
100%{
transform: translateZ(500px) rotate(0deg);
}
}
@keyframes fade {
0%{
opacity: 0;
}
25%,
60%{
opacity: 1;
}
100%{
opacity: 0;
}
}
@keyframes hueRotate {
0% {
filter: hue-rotate(0);
}
100% {
filter: hue-rotate(360deg);
}
}

最终完整的效果如下,星空穿梭的效果,整个动画首尾相连,可以一直无限下去,几乎没有破绽,非常的赞:



上述的完整代码,你可以猛击这里:CSS 灵感 -- 3D 宇宙时空穿梭效果


这样,我们就基本还原了上述见到的网易 UU 加速器首页的动图背景。


更进一步,一个图片我都不想用


当然,这里还是会有读者吐槽,你这里不也用了一张图片资源么?没有那张星空图行不行?这张图我也懒得去找。


当然可以,CSS YYDS。这里我们尝试使用 box-shadow,去替换实际的星空图,也是在一个 div 标签内实现,借助了 SASS 的循环函数:


<div></div>

@function randomNum($max, $min: 0, $u: 1) {
@return ($min + random($max)) * $u;
}

@function randomColor() {
@return rgb(randomNum(255), randomNum(255), randomNum(255));
}

@function shadowSet($maxWidth, $maxHeight, $count) {
$shadow : 0 0 0 0 randomColor();

@for $i from 0 through $count {
$x: #{random(10000) / 10000 * $maxWidth};
$y: #{random(10000) / 10000 * $maxHeight};


$shadow: $shadow, #{$x} #{$y} 0 #{random(5)}px randomColor();
}

@return $shadow;
}

body {
background: #000;
}

div {
width: 1px;
height: 1px;
border-radius: 50%;
box-shadow: shadowSet(100vw, 100vh, 500);
}

这里,我们用 SASS 封装了一个函数,利用多重 box-shadow 的特性,在传入的大小的高宽内,生成传入个数的点。


这样,我们可以得到这样一幅图,用于替换实际的星空图:



我们再把上述这个图,替换实际的星空图,主要是替换 .item 这个 class,只列出修改的部分:


// 原 CSS,使用了一张星空图
.item {
position: absolute;
width: 100%;
height: 100%;
background: url(https://z3.ax1x.com/2021/08/20/fLwuMd.jpg);
background-size: cover;
animation: fade 12s infinite linear;
}

// 修改后的 CSS 代码
.item {
position: absolute;
width: 100%;
height: 100%;
background: #000;
animation: fade 12s infinite linear;
}
.item::after {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 1px;
height: 1px;
border-radius: 50%;
box-shadow: shadowSet(100vw, 100vh, 500);
}

这样,我们就实现了这样一个效果,在不借助额外资源的情况下,使用纯 CSS 实现上述效果:



CodePen Demo -- Pure CSS Galaxy Shuttle 2


通过调整动画的时间,perspective 的值,每组元素的 translateZ() 变化距离,可以得到各种不一样的观感和效果,感兴趣的读者可以基于我上述给的 DEMO 自己尝试尝试。


作者:chokcoco
链接:https://juejin.cn/post/7028757824695959588

收起阅读 »

freeze、seal、preventExtensions对比

在Object常用的方法中,Object.freeze和Object.seal对于初学者而言,是两个较为容易混淆的概念,常常傻傻分不清两者的区别和应用场景 概念 先看看两者定义 Object.freeze在MDN中的定义 Object.freeze() 方法...
继续阅读 »

Object常用的方法中,Object.freezeObject.seal对于初学者而言,是两个较为容易混淆的概念,常常傻傻分不清两者的区别和应用场景


概念


先看看两者定义


Object.freeze在MDN中的定义



Object.freeze() 方法可以冻结一个对象。一个被冻结的对象再也不能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。此外,冻结一个对象后该对象的原型也不能被修改。freeze() 返回和传入的参数相同的对象。



Object.seal在MDN中的定义



Object.seal()方法封闭一个对象,阻止添加新属性并将所有现有属性标记为不可配置。当前属性的值只要原来是可写的就可以改变。



从两者定义可以得到两者差异:Object.freeze核心是冻结,强调的是不可修改。Object.seal核心是封闭,强调的是不可配置,不影响老的属性值修改


差异


定义一个对象,接下来的对比围绕这个对象进行


"use strict";
const obj = {
name: "nordon"
};

使用Object.freeze


Object.freeze(obj);
Object.isFrozen(obj); // true
obj.name = "wy";

使用Object.isFrozen可以检测数据是否被Object.freeze冻结,返回一个Boolean类型数据


此时对冻结之后的数据进行修改,控制台将会报错:Uncaught TypeError: Cannot assign to read only property 'name' of object '#<Object>'


使用Object.seal


Object.seal(obj);
Object.isSealed(obj); // true
obj.name = "wy";

使用Object.isSealed可以检测数据是否被Object.seal封闭,返回一个Boolean类型数据


此时修改name是成功的,此时的obj也成功被修改


注意:若是不开启严格模式,浏览器会采用静默模式,不会在控制台抛出异常信息


共同点


主要以Object.seal演示


不可删除


delete obj.name;

控制台将会抛出异常:Uncaught TypeError: Cannot delete property 'name' of #<Object>


不可配置


可以修改原有的属性值


Object.defineProperty(obj, 'name', {
value: 'wy'
})

不可增加新的属性值


Object.defineProperty(obj, "age", {
value: 12,
});

控制台将会抛出异常:Uncaught TypeError: Cannot define property age, object is not extensible


深层嵌套


两者对于深层嵌套的数据都表现为:无能为力


定义一个嵌套的对象


"use strict";
const obj = {
name: "nordon",
info: {
foo: 'bar'
}
};

对于obj而言,无论是freeze还是seal,操作info内部的数据都无法做到对应的处理


obj.info.msg = 'msg'

数据源obj被修改,不受冻结或者冰封的影响


若是想要做到嵌套数据的处理,需要递归便利数据源处理,此操作需要注意:数据中包含循环引用时,将会触发无限循环


preventExtensions


最后介绍一下Object.preventExtensions,为何这个方法没有放在与Object.freezeObject.seal一起对比呢?因为其和seal基本可保持一致,唯一的区别就是可以delete属性,因此单独放在最后介绍


看一段代码


"use strict";
const obj = {
name: "nordon",
};

Object.preventExtensions(obj);
Object.isExtensible(obj); // false, 代表其不可扩展

delete obj.name;

作者:Nordon
链接:https://juejin.cn/post/7028389571561947172

收起阅读 »

【喵猫秀秀秀】用CSS向你展示猫立方!!

前言 这次,我们用vue2+scss,带大家来实现一个六面体的猫立方。 本次的逻辑我们不适用任何的js代码,仅仅只依靠css来完成。 所以,通过本片文章,你可以收获一些css动画相关的技巧。 先看看效果 预习 本次我们要用到的知识点 transform ...
继续阅读 »

前言


这次,我们用vue2+scss,带大家来实现一个六面体的猫立方。


本次的逻辑我们不适用任何的js代码,仅仅只依靠css来完成。


所以,通过本片文章,你可以收获一些css动画相关的技巧。


先看看效果


cat3D.gif


预习


本次我们要用到的知识点



  1. transform


解释:transform 属性向元素应用 2D 或 3D 转换。该属性允许我们对元素进行旋转、缩放、移动或倾斜。


要用哪个,可以对着这个表格查


image.png



  1. transform-style


解释:transform--style属性指定嵌套元素是怎样在三维空间中呈现。


注意:  使用此属性必须先使用 transform 属性.



  1. transition


解释:transition 属性是一个简写属性,用于设置四个过渡属性:



  • transition-property

  • transition-duration

  • transition-timing-function

  • transition-delay


注释:请始终设置 transition-duration 属性,否则时长为 0,就不会产生过渡效果。


分析


我们先拆解下这个猫3D的特点,它有以下特点



  1. 它一直在不停的转

  2. 它由两个六面体组成,外面一个,里面一个

  3. 鼠标靠近外面的六面体,六面体的六个面会往外扩,露出里面的小六面体


开始


1.因为我们做的是六面体,有2个六面体,一个在里面,一个在外面。2个六面体,12个面,先准备12张猫主子的图片。


image.png



  1. 然后我们新建img3D.vue文件,开干


image.png


步骤一


先来完成第一个特点不停的转


cat3D1.gif
代码如下:


<template>
<div>
<div class="container">
</div>
</div>
</template>

<script>

</script>

<style lang="scss" scoped>
* {
margin: 0px;
padding: 0px;
}

.container {
position: relative;
margin: 0px auto;
margin-top: 9%;
margin-left: 42%;
width: 200px;
height: 200px;
transform: rotateX(-30deg) rotateY(-80deg);
transform-style: preserve-3d;
animation: rotate 15s infinite;
/* 我这里用一个边框线来更容易看到效果 */
border: 1px solid red;
}

/* 旋转立方体 */
@keyframes rotate {
from {
transform: rotateX(0deg) rotateY(0deg);
}

to {
transform: rotateX(360deg) rotateY(360deg);
}
}
</style>

步骤二


弄外面的六面体,并且六面体在鼠标悬停的时候,需要往外扩
效果如下:


cat3D2.gif


<template>
<div>
<div class="container">
<!-- 外层立方体 -->
<div class="outer-container">
<div class="outer-top">
<img :src="cat1" />
</div>
<div class="outer-bottom">
<img :src="cat2" />
</div>
<div class="outer-front">
<img :src="cat3" />
</div>
<div class="outer-back">
<img :src="cat4" />
</div>
<div class="outer-left">
<img :src="cat5" />
</div>
<div class="outer-right">
<img :src="cat6" />
</div>
</div>
</div>
</div>
</template>

<script>
import cat1 from "@/assets/cats/cat1.png";
import cat2 from "@/assets/cats/cat2.png";
import cat3 from "@/assets/cats/cat3.png";
import cat4 from "@/assets/cats/cat4.png";
import cat5 from "@/assets/cats/cat5.png";
import cat6 from "@/assets/cats/cat6.png";

export default {
data() {
return {
cat1,
cat2,
cat3,
cat4,
cat5,
cat6,
};
},
};
</script>

<style lang="scss" scoped>
...
/* 设置图片可以在三维空间里展示 */
.container .outer-container {
transform-style: preserve-3d;
}
/* 设置图片尺寸固定宽高200px */
.outer-container img {
width: 200px;
height: 200px;
}

/* 外层立方体样式 */
.outer-container .outer-top,
.outer-container .outer-bottom,
.outer-container .outer-right,
.outer-container .outer-left,
.outer-container .outer-front,
.outer-container .outer-back {
position: absolute;
top: 0;
left: 0;
width: 200px;
height: 200px;
border: 1px solid #fff;
opacity: 0.3;
transition: all 0.9s;
}

/* 全靠transform 属性撑起来了外面的六面体 */
.outer-top {
transform: rotateX(90deg) translateZ(100px);
}

.outer-bottom {
transform: rotateX(-90deg) translateZ(100px);
}

.outer-front {
transform: rotateY(0deg) translateZ(100px);
}

.outer-back {
transform: translateZ(-100px) rotateY(180deg);
}

.outer-left {
transform: rotateY(90deg) translateZ(100px);
}

.outer-right {
transform: rotateY(-90deg) translateZ(100px);
}

/* 鼠标悬停 外面的六面体,六条边都往外扩出去,并且透明度变浅了,猫猫图片更清晰了 */
.cube:hover .outer-top {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateX(90deg) translateZ(200px);
}

.container:hover .outer-bottom {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateX(-90deg) translateZ(200px);
}

.container:hover .outer-front {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateY(0deg) translateZ(200px);
}

.container:hover .outer-back {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: translateZ(-200px) rotateY(180deg);
}

.container:hover .outer-left {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateY(90deg) translateZ(200px);
}

.container:hover .outer-right {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateY(-90deg) translateZ(200px);
}
</style>


步骤三


弄里面的六面体,这个六边形比较简单,没有移入移出,鼠标悬停等的样式效果
效果如下:


cat3D.gif
代码如下:


<template>
<div>
<div class="container">
<!-- 外层立方体 -->
...
<!-- 内层立方体 -->
<div class="inner-container">
<div class="inner-top">
<img :src="cat7" />
</div>
<div class="inner-bottom">
<img :src="cat8" />
</div>
<div class="inner-front">
<img :src="cat9" />
</div>
<div class="inner-back">
<img :src="cat10" />
</div>
<div class="inner-left">
<img :src="cat11" />
</div>
<div class="inner-right">
<img :src="cat12" />
</div>
</div>
</div>
</div>
</template>

<script>
...
import cat7 from "@/assets/cats/cat7.png";
import cat8 from "@/assets/cats/cat8.png";
import cat9 from "@/assets/cats/cat9.png";
import cat10 from "@/assets/cats/cat10.png";
import cat11 from "@/assets/cats/cat11.png";
import cat12 from "@/assets/cats/cat12.png";

export default {
data() {
return {
...
cat7,
cat8,
cat9,
cat10,
cat11,
cat12,
};
},
};
</script>

<style lang="scss" scoped>
...
/* 设置图片可以在三维空间里展示 */
.container .outer-container,
.container .inner-container {
transform-style: preserve-3d;
}

...

/* 设置里面的六面体样式 */
/* 嵌套的内层立方体样式 */
.inner-container > div {
position: absolute;
top: 35px;
left: 35px;
width: 130px;
height: 130px;
}
/* 里面的六面体尺寸宽高设置130px */
.inner-container img {
width: 130px;
height: 130px;
}

.inner-top {
transform: rotateX(90deg) translateZ(65px);
}

.inner-bottom {
transform: rotateX(-90deg) translateZ(65px);
}

.inner-front {
transform: rotateY(0deg) translateZ(65px);
}

.inner-back {
transform: translateZ(-65px) rotateY(180deg);
}

.inner-left {
transform: rotateY(90deg) translateZ(65px);
}

.inner-right {
transform: rotateY(-90deg) translateZ(65px);
}
</style>

最后完整代码:


<template>
<div>
<div class="container">
<!-- 外层立方体 -->
<div class="outer-container">
<div class="outer-top">
<img :src="cat1" />
</div>
<div class="outer-bottom">
<img :src="cat2" />
</div>
<div class="outer-front">
<img :src="cat3" />
</div>
<div class="outer-back">
<img :src="cat4" />
</div>
<div class="outer-left">
<img :src="cat5" />
</div>
<div class="outer-right">
<img :src="cat6" />
</div>
</div>
<!-- 内层立方体 -->
<div class="inner-container">
<div class="inner-top">
<img :src="cat7" />
</div>
<div class="inner-bottom">
<img :src="cat8" />
</div>
<div class="inner-front">
<img :src="cat9" />
</div>
<div class="inner-back">
<img :src="cat10" />
</div>
<div class="inner-left">
<img :src="cat11" />
</div>
<div class="inner-right">
<img :src="cat12" />
</div>
</div>
</div>
</div>
</template>

<script>
import cat1 from "@/assets/cats/cat1.png";
import cat2 from "@/assets/cats/cat2.png";
import cat3 from "@/assets/cats/cat3.png";
import cat4 from "@/assets/cats/cat4.png";
import cat5 from "@/assets/cats/cat5.png";
import cat6 from "@/assets/cats/cat6.png";
import cat7 from "@/assets/cats/cat7.png";
import cat8 from "@/assets/cats/cat8.png";
import cat9 from "@/assets/cats/cat9.png";
import cat10 from "@/assets/cats/cat10.png";
import cat11 from "@/assets/cats/cat11.png";
import cat12 from "@/assets/cats/cat12.png";

export default {
data() {
return {
cat1,
cat2,
cat3,
cat4,
cat5,
cat6,
cat7,
cat8,
cat9,
cat10,
cat11,
cat12,
};
},
};
</script>

<style lang="scss" scoped>
* {
margin: 0px;
padding: 0px;
}

.container {
position: relative;
margin: 0px auto;
margin-top: 9%;
margin-left: 42%;
width: 200px;
height: 200px;
transform: rotateX(-30deg) rotateY(-80deg);
transform-style: preserve-3d;
animation: rotate 15s infinite;
/* 我这里用一个边框线来更容易看到效果 */
/* border: 1px solid red; */
}

/* 旋转立方体 */
@keyframes rotate {
from {
transform: rotateX(0deg) rotateY(0deg);
}

to {
transform: rotateX(360deg) rotateY(360deg);
}
}
/* 设置图片可以在三维空间里展示 */
.container .outer-container,
.container .inner-container {
transform-style: preserve-3d;
}
/* 设置图片尺寸固定宽高200px */
.outer-container img {
width: 200px;
height: 200px;
}

/* 外层立方体样式 */
.outer-container .outer-top,
.outer-container .outer-bottom,
.outer-container .outer-right,
.outer-container .outer-left,
.outer-container .outer-front,
.outer-container .outer-back {
position: absolute;
top: 0;
left: 0;
width: 200px;
height: 200px;
border: 1px solid #fff;
opacity: 0.3;
transition: all 0.9s;
}

/* 全靠transform 属性撑起来了外面的六面体 */
.outer-top {
transform: rotateX(90deg) translateZ(100px);
}

.outer-bottom {
transform: rotateX(-90deg) translateZ(100px);
}

.outer-front {
transform: rotateY(0deg) translateZ(100px);
}

.outer-back {
transform: translateZ(-100px) rotateY(180deg);
}

.outer-left {
transform: rotateY(90deg) translateZ(100px);
}

.outer-right {
transform: rotateY(-90deg) translateZ(100px);
}

// 鼠标悬停 外面的六面体,六条边都往外扩出去,并且透明度变浅了,猫猫图片更清晰了
.cube:hover .outer-top {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateX(90deg) translateZ(200px);
}

.container:hover .outer-bottom {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateX(-90deg) translateZ(200px);
}

.container:hover .outer-front {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateY(0deg) translateZ(200px);
}

.container:hover .outer-back {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: translateZ(-200px) rotateY(180deg);
}

.container:hover .outer-left {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateY(90deg) translateZ(200px);
}

.container:hover .outer-right {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateY(-90deg) translateZ(200px);
}

/* 设置里面的六面体样式 */
/* 嵌套的内层立方体样式 */
.inner-container > div {
position: absolute;
top: 35px;
left: 35px;
width: 130px;
height: 130px;
}
/* 里面的六面体尺寸宽高设置130px */
.inner-container img {
width: 130px;
height: 130px;
}

.inner-top {
transform: rotateX(90deg) translateZ(65px);
}

.inner-bottom {
transform: rotateX(-90deg) translateZ(65px);
}

.inner-front {
transform: rotateY(0deg) translateZ(65px);
}

.inner-back {
transform: translateZ(-65px) rotateY(180deg);
}

.inner-left {
transform: rotateY(90deg) translateZ(65px);
}

.inner-right {
transform: rotateY(-90deg) translateZ(65px);
}
</style>


都看到这里了,求各位观众大佬们点个赞再走吧,你的赞对我非常重要



收起阅读 »

一款强大到没朋友的图片编辑插件,爱了爱了!

前言 最近用户提出了一个新的需求,老师可以批改学生的图片作业,需要对图片进行旋转、缩放、裁剪、涂鸦、标注、添加文本等。乍一听,又要掉不少头发。有没有功能强大的插件实现以上功能,让我有更多的时间去阻止女票双十一剁手呢?答案当然是有的。 效果展示涂鸦 裁剪 ...
继续阅读 »

前言


最近用户提出了一个新的需求,老师可以批改学生的图片作业,需要对图片进行旋转、缩放、裁剪、涂鸦、标注、添加文本等。乍一听,又要掉不少头发。有没有功能强大的插件实现以上功能,让我有更多的时间去阻止女票双十一剁手呢?答案当然是有的。


效果展示

涂鸦



涂鸦2.jpg


裁剪


裁剪.jpg


标注


标注2.jpg


旋转


旋转2.jpg


滤镜


1636088844(1).jpg


是不是很强大!还有众多功能我就不一一展示了。那么还等什么,跟我一起用起来吧~


安装


npm i tui-image-editor
// or
yarn add tui-image-editor

使用

快速体验



复制以下代码,将插件引入到自己的项目中。


<template>
<div class="drawing-container">
<div id="tui-image-editor"></div>
</div>

</template>
<script>
import "tui-image-editor/dist/tui-image-editor.css";
import "tui-color-picker/dist/tui-color-picker.css";
import ImageEditor from "tui-image-editor";
export default {
data() {
return {
instance: null,
};
},
mounted() {
this.init();
},
methods: {
init() {
this.instance = new ImageEditor(
document.querySelector("#tui-image-editor"),
{
includeUI: {
loadImage: {
path: "https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c1d7a1feb60346449c1a64893888989a~tplv-k3u1fbpfcp-watermark.image",
name: "image",
},
initMenu: "draw", // 默认打开的菜单项
menuBarPosition: "bottom", // 菜单所在的位置
},
cssMaxWidth: 1000, // canvas 最大宽度
cssMaxHeight: 600, // canvas 最大高度
}
);
document.getElementsByClassName("tui-image-editor-main")[0].style.top = "45px"; // 图片距顶部工具栏的距离
},
},
};
</script>


<style lang="scss" scoped>
.drawing-container {
height: 900px;
}
</style>


可以看到活生生的图片编辑工具就出现了,是不是很简单:


初始效果.jpg


国际化


由于是老外开发的,默认的文字描述都是英文,这里我们先汉化一下:


const locale_zh = {
ZoomIn: "放大",
ZoomOut: "缩小",
Hand: "手掌",
History: '历史',
Resize: '调整宽高',
Crop: "裁剪",
DeleteAll: "全部删除",
Delete: "删除",
Undo: "撤销",
Redo: "反撤销",
Reset: "重置",
Flip: "镜像",
Rotate: "旋转",
Draw: "画",
Shape: "形状标注",
Icon: "图标标注",
Text: "文字标注",
Mask: "遮罩",
Filter: "滤镜",
Bold: "加粗",
Italic: "斜体",
Underline: "下划线",
Left: "左对齐",
Center: "居中",
Right: "右对齐",
Color: "颜色",
"Text size": "字体大小",
Custom: "自定义",
Square: "正方形",
Apply: "应用",
Cancel: "取消",
"Flip X": "X 轴",
"Flip Y": "Y 轴",
Range: "区间",
Stroke: "描边",
Fill: "填充",
Circle: "圆",
Triangle: "三角",
Rectangle: "矩形",
Free: "曲线",
Straight: "直线",
Arrow: "箭头",
"Arrow-2": "箭头2",
"Arrow-3": "箭头3",
"Star-1": "星星1",
"Star-2": "星星2",
Polygon: "多边形",
Location: "定位",
Heart: "心形",
Bubble: "气泡",
"Custom icon": "自定义图标",
"Load Mask Image": "加载蒙层图片",
Grayscale: "灰度",
Blur: "模糊",
Sharpen: "锐化",
Emboss: "浮雕",
"Remove White": "除去白色",
Distance: "距离",
Brightness: "亮度",
Noise: "噪音",
"Color Filter": "彩色滤镜",
Sepia: "棕色",
Sepia2: "棕色2",
Invert: "负片",
Pixelate: "像素化",
Threshold: "阈值",
Tint: "色调",
Multiply: "正片叠底",
Blend: "混合色",
Width: "宽度",
Height: "高度",
"Lock Aspect Ratio": "锁定宽高比例",
};

this.instance = new ImageEditor(
document.querySelector("#tui-image-editor"),
{
includeUI: {
loadImage: {
path: "https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c1d7a1feb60346449c1a64893888989a~tplv-k3u1fbpfcp-watermark.image",
name: "image",
},
initMenu: "draw", // 默认打开的菜单项
menuBarPosition: "bottom", // 菜单所在的位置
locale: locale_zh, // 本地化语言为中文
},
cssMaxWidth: 1000, // canvas 最大宽度
cssMaxHeight: 600, // canvas 最大高度
}
);

效果如下:


汉化.jpg


自定义样式


默认风格为暗黑系,如果想改成白底,或者想改变按钮的大小、颜色等样式,可以使用自定义样式。


const customTheme = {
"common.bi.image": "", // 左上角logo图片
"common.bisize.width": "0px",
"common.bisize.height": "0px",
"common.backgroundImage": "none",
"common.backgroundColor": "#f3f4f6",
"common.border": "1px solid #333",

// header
"header.backgroundImage": "none",
"header.backgroundColor": "#f3f4f6",
"header.border": "0px",

// load button
"loadButton.backgroundColor": "#fff",
"loadButton.border": "1px solid #ddd",
"loadButton.color": "#222",
"loadButton.fontFamily": "NotoSans, sans-serif",
"loadButton.fontSize": "12px",
"loadButton.display": "none", // 隐藏

// download button
"downloadButton.backgroundColor": "#fdba3b",
"downloadButton.border": "1px solid #fdba3b",
"downloadButton.color": "#fff",
"downloadButton.fontFamily": "NotoSans, sans-serif",
"downloadButton.fontSize": "12px",
"downloadButton.display": "none", // 隐藏

// icons default
"menu.normalIcon.color": "#8a8a8a",
"menu.activeIcon.color": "#555555",
"menu.disabledIcon.color": "#ccc",
"menu.hoverIcon.color": "#e9e9e9",
"submenu.normalIcon.color": "#8a8a8a",
"submenu.activeIcon.color": "#e9e9e9",

"menu.iconSize.width": "24px",
"menu.iconSize.height": "24px",
"submenu.iconSize.width": "32px",
"submenu.iconSize.height": "32px",

// submenu primary color
"submenu.backgroundColor": "#1e1e1e",
"submenu.partition.color": "#858585",

// submenu labels
"submenu.normalLabel.color": "#858585",
"submenu.normalLabel.fontWeight": "lighter",
"submenu.activeLabel.color": "#fff",
"submenu.activeLabel.fontWeight": "lighter",

// checkbox style
"checkbox.border": "1px solid #ccc",
"checkbox.backgroundColor": "#fff",

// rango style
"range.pointer.color": "#fff",
"range.bar.color": "#666",
"range.subbar.color": "#d1d1d1",

"range.disabledPointer.color": "#414141",
"range.disabledBar.color": "#282828",
"range.disabledSubbar.color": "#414141",

"range.value.color": "#fff",
"range.value.fontWeight": "lighter",
"range.value.fontSize": "11px",
"range.value.border": "1px solid #353535",
"range.value.backgroundColor": "#151515",
"range.title.color": "#fff",
"range.title.fontWeight": "lighter",

// colorpicker style
"colorpicker.button.border": "1px solid #1e1e1e",
"colorpicker.title.color": "#fff",
};

this.instance = new ImageEditor(
document.querySelector("#tui-image-editor"),
{
includeUI: {
loadImage: {
path: "https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c1d7a1feb60346449c1a64893888989a~tplv-k3u1fbpfcp-watermark.image",
name: "image",
},
initMenu: "draw", // 默认打开的菜单项
menuBarPosition: "bottom", // 菜单所在的位置
locale: locale_zh, // 本地化语言为中文
theme: customTheme, // 自定义样式
},
cssMaxWidth: 1000, // canvas 最大宽度
cssMaxHeight: 600, // canvas 最大高度
}
);

效果如下:


自定义样式.jpg


按钮优化


通过自定义样式,我们看到右上角的 Load 和 Download 按钮已经被隐藏了,接下来我们再隐藏掉其他用不上的按钮(根据业务需要),并添加一个保存图片的按钮。


<template>
<div class="drawing-container">
<div id="tui-image-editor"></div>
<el-button class="save" type="primary" size="small" @click="save">保存</el-button>
</div>

</template>

// ...
methods: {
init() {
this.instance = new ImageEditor(
document.querySelector("#tui-image-editor"),
{
includeUI: {
loadImage: {
path: "https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c1d7a1feb60346449c1a64893888989a~tplv-k3u1fbpfcp-watermark.image",
name: "image",
},
menu: ["resize", "crop", "rotate", "draw", "shape", "icon", "text", "filter"], // 底部菜单按钮列表 隐藏镜像flip和遮罩mask
initMenu: "draw", // 默认打开的菜单项
menuBarPosition: "bottom", // 菜单所在的位置
locale: locale_zh, // 本地化语言为中文
theme: customTheme, // 自定义样式
},
cssMaxWidth: 1000, // canvas 最大宽度
cssMaxHeight: 600, // canvas 最大高度
}
);
document.getElementsByClassName("tui-image-editor-main")[0].style.top ="45px"; // 调整图片显示位置
document.getElementsByClassName("tie-btn-reset tui-image-editor-item help") [0].style.display = "none"; // 隐藏顶部重置按钮
},
// 保存图片,并上传
save() {
const base64String = this.instance.toDataURL(); // base64 文件
const data = window.atob(base64String.split(",")[1]);
const ia = new Uint8Array(data.length);
for (let i = 0; i < data.length; i++) {
ia[i] = data.charCodeAt(i);
}
const blob = new Blob([ia], { type: "image/png" }); // blob 文件
const form = new FormData();
form.append("image", blob);
// upload file
},
}

<style lang="scss" scoped>
.drawing-container {
height: 900px;
position: relative;
.save {
position: absolute;
right: 50px;
top: 15px;
}
}
</style>

效果如下:


按钮优化.jpg


可以看到顶部的重置按钮,以及底部的镜像和遮罩按钮都已经不见了。右上角多了一个我们自己的保存按钮,点击按钮,可以获取到 base64 文件和 blob 文件。


完整代码


<template>
<div class="drawing-container">
<div id="tui-image-editor"></div>
<el-button class="save" type="primary" size="small" @click="save">保存</el-button>
</div>

</template>
<script>
import 'tui-image-editor/dist/tui-image-editor.css'
import 'tui-color-picker/dist/tui-color-picker.css'
import ImageEditor from 'tui-image-editor'
const locale_zh = {
ZoomIn: '放大',
ZoomOut: '缩小',
Hand: '手掌',
History: '历史',
Resize: '调整宽高',
Crop: '裁剪',
DeleteAll: '全部删除',
Delete: '删除',
Undo: '撤销',
Redo: '反撤销',
Reset: '重置',
Flip: '镜像',
Rotate: '旋转',
Draw: '画',
Shape: '形状标注',
Icon: '图标标注',
Text: '文字标注',
Mask: '遮罩',
Filter: '滤镜',
Bold: '加粗',
Italic: '斜体',
Underline: '下划线',
Left: '左对齐',
Center: '居中',
Right: '右对齐',
Color: '颜色',
'Text size': '字体大小',
Custom: '自定义',
Square: '正方形',
Apply: '应用',
Cancel: '取消',
'Flip X': 'X 轴',
'Flip Y': 'Y 轴',
Range: '区间',
Stroke: '描边',
Fill: '填充',
Circle: '圆',
Triangle: '三角',
Rectangle: '矩形',
Free: '曲线',
Straight: '直线',
Arrow: '箭头',
'Arrow-2': '箭头2',
'Arrow-3': '箭头3',
'Star-1': '星星1',
'Star-2': '星星2',
Polygon: '多边形',
Location: '定位',
Heart: '心形',
Bubble: '气泡',
'Custom icon': '自定义图标',
'Load Mask Image': '加载蒙层图片',
Grayscale: '灰度',
Blur: '模糊',
Sharpen: '锐化',
Emboss: '浮雕',
'Remove White': '除去白色',
Distance: '距离',
Brightness: '亮度',
Noise: '噪音',
'Color Filter': '彩色滤镜',
Sepia: '棕色',
Sepia2: '棕色2',
Invert: '负片',
Pixelate: '像素化',
Threshold: '阈值',
Tint: '色调',
Multiply: '正片叠底',
Blend: '混合色',
Width: '宽度',
Height: '高度',
'Lock Aspect Ratio': '锁定宽高比例'
}

const customTheme = {
"common.bi.image": "", // 左上角logo图片
"common.bisize.width": "0px",
"common.bisize.height": "0px",
"common.backgroundImage": "none",
"common.backgroundColor": "#f3f4f6",
"common.border": "1px solid #333",

// header
"header.backgroundImage": "none",
"header.backgroundColor": "#f3f4f6",
"header.border": "0px",

// load button
"loadButton.backgroundColor": "#fff",
"loadButton.border": "1px solid #ddd",
"loadButton.color": "#222",
"loadButton.fontFamily": "NotoSans, sans-serif",
"loadButton.fontSize": "12px",
"loadButton.display": "none", // 隐藏

// download button
"downloadButton.backgroundColor": "#fdba3b",
"downloadButton.border": "1px solid #fdba3b",
"downloadButton.color": "#fff",
"downloadButton.fontFamily": "NotoSans, sans-serif",
"downloadButton.fontSize": "12px",
"downloadButton.display": "none", // 隐藏

// icons default
"menu.normalIcon.color": "#8a8a8a",
"menu.activeIcon.color": "#555555",
"menu.disabledIcon.color": "#ccc",
"menu.hoverIcon.color": "#e9e9e9",
"submenu.normalIcon.color": "#8a8a8a",
"submenu.activeIcon.color": "#e9e9e9",

"menu.iconSize.width": "24px",
"menu.iconSize.height": "24px",
"submenu.iconSize.width": "32px",
"submenu.iconSize.height": "32px",

// submenu primary color
"submenu.backgroundColor": "#1e1e1e",
"submenu.partition.color": "#858585",

// submenu labels
"submenu.normalLabel.color": "#858585",
"submenu.normalLabel.fontWeight": "lighter",
"submenu.activeLabel.color": "#fff",
"submenu.activeLabel.fontWeight": "lighter",

// checkbox style
"checkbox.border": "1px solid #ccc",
"checkbox.backgroundColor": "#fff",

// rango style
"range.pointer.color": "#fff",
"range.bar.color": "#666",
"range.subbar.color": "#d1d1d1",

"range.disabledPointer.color": "#414141",
"range.disabledBar.color": "#282828",
"range.disabledSubbar.color": "#414141",

"range.value.color": "#fff",
"range.value.fontWeight": "lighter",
"range.value.fontSize": "11px",
"range.value.border": "1px solid #353535",
"range.value.backgroundColor": "#151515",
"range.title.color": "#fff",
"range.title.fontWeight": "lighter",

// colorpicker style
"colorpicker.button.border": "1px solid #1e1e1e",
"colorpicker.title.color": "#fff",
};
export default {
data() {
return {
instance: null
}
},
mounted() {
this.init()
},
methods: {
init() {
this.instance = new ImageEditor(document.querySelector('#tui-image-editor'), {
includeUI: {
loadImage: {
path: 'https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c1d7a1feb60346449c1a64893888989a~tplv-k3u1fbpfcp-watermark.image',
name: 'image'
},
menu: ['resize', 'crop', 'rotate', 'draw', 'shape', 'icon', 'text', 'filter'], // 底部菜单按钮列表 隐藏镜像flip和遮罩mask
initMenu: 'draw', // 默认打开的菜单项
menuBarPosition: 'bottom', // 菜单所在的位置
locale: locale_zh, // 本地化语言为中文
theme: customTheme // 自定义样式
},
cssMaxWidth: 1000, // canvas 最大宽度
cssMaxHeight: 600 // canvas 最大高度
})
document.getElementsByClassName('tui-image-editor-main')[0].style.top = '45px' // 调整图片显示位置
document.getElementsByClassName(
'tie-btn-reset tui-image-editor-item help'
)[0].style.display = 'none' // 隐藏顶部重置按钮
},
// 保存图片,并上传
save() {
const base64String = this.instance.toDataURL() // base64 文件
const data = window.atob(base64String.split(',')[1])
const ia = new Uint8Array(data.length)
for (let i = 0; i < data.length; i++) {
ia[i] = data.charCodeAt(i)
}
const blob = new Blob([ia], { type: 'image/png' }) // blob 文件
const form = new FormData()
form.append('image', blob)
// upload file
}
}
}
</script>


<style lang="scss" scoped>
.drawing-container {
height: 900px;
position: relative;
.save {
position: absolute;
right: 50px;
top: 15px;
}
}
</style>


总结


以上就是 tui.image-editor 的基本使用方法,相比其他插件,tui.image-editor 的优势是功能强大,简单易上手。


插件固然好用,但本人也发现一个小 bug,当放大图片,用手掌拖动显示位置,再点击重置按钮时,图片很可能就消失不见了。解决办法有两个,一是改源码,在重置之前,先调用 resetZoom 方法,还原缩放比列;二是自己做一个重置按钮,点击之后调用 this.init 方法重新进行渲染。



收起阅读 »

超详细讲解页面加载过程

说一说从输入URL到页面呈现发生了什么?(知识点) ❝ 这个题可以说是面试最常见也是一道可以无限难的题了,一般面试官出这道题就是为了考察你的前端知识的深度与广度。 ❞ 1.浏览器接受URL开启网络请求线程(涉及到:浏览器机制,线程与进程等) 2.开启网络线...
继续阅读 »

说一说从输入URL到页面呈现发生了什么?(知识点)




这个题可以说是面试最常见也是一道可以无限难的题了,一般面试官出这道题就是为了考察你的前端知识的深度与广度。




1.浏览器接受URL开启网络请求线程(涉及到:浏览器机制,线程与进程等)


2.开启网络线程到发出一个完整的http请求(涉及到:DNS解析,TCP/IP请求,5层网络协议等)


3.从服务器接收到请求到对应后台接受到请求(涉及到:负载均衡,安全拦截,后台内部处理等)


4.后台与前台的http交互(涉及到:http头,响应码,报文结构,cookie等)


5.缓存问题(涉及到:http强缓存与协商缓存等)(请看上一篇文章[这些浏览器面试题,看看你能回答几个?](juejin.cn/post/702653…


6.浏览器接受到http数据包后的解析流程(涉及到html词法分析,解析成DOM树,解析CSS生成CSSOM树,合并生成render渲染树。然后layout布局,painting渲染,复合图层合成,GPU绘制,等)


在浏览器地址栏输入URL


当我们在浏览器地址栏输入URL地址后,浏览器会开一个线程来对我们输入的URL进行解析处理。


浏览器中的各个进程及作用:(多进程)



  • 浏览器进程:负责管理标签页的创建销毁以及页面的显示,资源下载等。

  • 第三方插件进程:负责管理第三方插件。

  • GPU进程:负责3D绘制与硬件加速(最多一个)。

  • 渲染进程:负责页面文档解析(HTML,CSS,JS),执行与渲染。(可以有多个)


DNS域名解析


为什么需要DNS域名解析?


因为我们在浏览器中输入的URL通常是一个域名,并不会直接去输入IP地址(纯粹因为域名比IP好记),但我们的计算机并不认识域名,它只知道IP,所以就需要这一步操作将域名解析成IP。


URL组成部分



  • protocol:协议头,比如http,https,ftp等;

  • host:主机域名或者IP地址;

  • port:端口号;

  • path:目录路径;

  • query:查询的参数;

  • hash:#后边的hash值,用来定位某一个位置。


解析过程



  • 首先会查看浏览器DNS缓存,有的话直接使用浏览器缓存

  • 没有的话就查询计算机本地DNS缓存(localhost)

  • 还没有就询问递归式DNS服务器(就是网络提供商,一般这个服务器都会有自己的缓存)

  • 如果依然没有缓存,那就需要通过 根域名服务器 和TLD域名服务器 再到对应的 权威DNS服务器 找记录,并缓存到 递归式服务器,然后 递归服务器 再将记录返回给本地


「⚠️注意:」




DNS解析是非常耗时的,如果页面中需要解析的域名过多,是非常影响页面性能的。考虑使用dns与加载或减少DNS解析进行优化。




发送HTTP请求


拿到了IP地址后,就可以发起HTTP请求了。HTTP请求的本质就是TCP/IP的请求构建。建立连接时需要**「3次握手」进行验证,断开链接也同样需要「4次挥手」**进行验证,保证传输的可靠性


3次握手



  • 第一次握手:客户端发送位码为 SYN = 1(SYN 标志位置位),随机产生初始序列号 Seq = J 的数据包到服务器。服务器由 SYN = 1(置位)知道,客户端要求建立联机。

  • 第二次握手:服务器收到请求后要确认联机信息,向客户端发送确认号Ack = (客户端的Seq +1,J+1),SYN = 1,ACK = 1(SYN,ACK 标志位置位),随机产生的序列号 Seq = K 的数据包。

  • 第三次握手:客户端收到后检查 Ack 是否正确,即第一次发送的 Seq +1(J+1),以及位码ACK是否为1。若正确,客户端会再发送 Ack = (服务器端的Seq+1,K+1),ACK = 1,以及序号Seq为服务器确认号J 的确认包。服务器收到后确认之前发送的 Seq(K+1) 值与 ACK= 1 (ACK置位)则连接建立成功。


3次握手.gif


「直白理解:」


(客户端:hello,你是server么?服务端:hello,我是server,你是client么 客户端:yes,我是client 建立成功之后,接下来就是正式传输数据。)


4次挥手



  • 客户端发送一个FIN Seq = M(FIN置位,序号为M)包,用来关闭客户端到服务器端的数据传送。

  • 服务器端收到这个FIN,它发回一个ACK,确认序号Ack 为收到的序号M+1。

  • 服务器端关闭与客户端的连接,发送一个FIN Seq = N 给客户端。

  • 客户端发回ACK 报文确认,确认序号Ack 为收到的序号N+1。


4次挥手.gif


「直白理解:」


(主动方:我已经关闭了向你那边的主动通道了,只能被动接收了 被动方:收到通道关闭的信息 被动方:那我也告诉你,我这边向你的主动通道也关闭了 主动方:最后收到数据,之后双方无法通信)


五层网络协议


1、应用层(DNS,HTTP):DNS解析成IP并发送http请求;


2、传输层(TCP,UDP):建立TCP连接(3次握手);


3、网络层(IP,ARP):IP寻址;


4、数据链路层(PPP):封装成帧;


5、物理层(利用物理介质传输比特流):物理传输(通过双绞线,电磁波等各种介质)。


「OSI七层框架:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层」


服务器接收请求做出响应


HTTP 请求到达服务器,服务器进行对应的处理。 最后要把数据传给浏览器,也就是返回网络响应。


跟请求部分类似,网络响应具有三个部分:响应行、响应头和响应体。


响应完成之后怎么办?TCP 连接就断开了吗?


不一定。这时候要判断Connection字段, 如果请求头或响应头中包含Connection: Keep-Alive, 表示建立了持久连接,这样TCP连接会一直保持,之后请求统一站点的资源会复用这个连接。否则断开TCP连接, 请求-响应流程结束。


状态码


状态码是由3位数组成,第一个数字定义了响应的类别,且有五种可能取值:



  • 1xx:指示信息–表示请求已接收,继续处理。

  • 2xx:成功–表示请求已被成功接收、理解、接受。

  • 3xx:重定向–要完成请求必须进行更进一步的操作。

  • 4xx:客户端错误–请求有语法错误或请求无法实现。

  • 5xx:服务器端错误–服务器未能实现合法的请求。 平时遇到比较常见的状态码有:200, 204, 301, 302, 304, 400, 401, 403, 404, 422, 500(分别表示什么请自行查找)。


服务器返回相应文件


请求成功后,服务器会返回相应的网页,浏览器接收到响应成功的报文后便开始下载网页,至此,网络通信结束。


浏览器解析渲染页面


浏览器在接收到HTML,CSS,JS文件之后,它是如何将页面渲染在屏幕上的?


render.png


解析HTML构建DOM Tree


浏览器在拿到服务器返回的网页之后,首先会根据顶部定义的DTD类型进行对应的解析,解析过程将被交给内部的GUI渲染线程来处理。


「DTD(Document Type Definition)文档类型定义」


常见的文档类型定义


//HTML5文档定义
<!DOCTYPE html>
//用于XHTML 4.0 的严格型 
<!DOCTYPE HTMLPUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> 
//用于XHTML 4.0 的过渡型 
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> 
//用于XHTML 1.0 的严格型 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 
//用于XHTML 1.0 的过渡型
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

HTML解释器的工作就是将网络或者本地磁盘获取的HTML网页或资源从字节流解释成DOM树🌲结构


HTML解释器.png


通过上图可以清楚的了解这一过程:首先是字节流,经过解码之后是字符流,然后通过词法分析器会被解释成词语(Tokens),之后经过语法分析器构建成节点,最后这些节点被组建成一颗 DOM 树。


对于线程化的解释器,字符流后的整个解释、布局和渲染过程基本会交给一个单独的渲染线程来管理(不是绝对的)。由于 DOM 树只能在渲染线程上创建和访问,所以构建 DOM 树的过程只能在渲染线程中进行。但是,从字符串到词语这个阶段可以交给单独的线程来做,Chrome 浏览器使用的就是这个思想。在解释成词语之后,Webkit 会分批次将结果词语传递回渲染线程。


这个过程中,如果遇到的节点是 JS 代码,就会调用 JS引擎 对 JS代码进行解释执行,此时由于 JS引擎GUI渲染线程 的互斥,GUI渲染线程 就会被挂起,渲染过程停止,如果 JS 代码的运行中对DOM树进行了修改,那么DOM的构建需要从新开始


如果节点需要依赖其他资源,图片/CSS等等,就会调用网络模块的资源加载器来加载它们,它们是异步的,不会阻塞当前DOM树的构建


如果遇到的是 JS 资源URL(没有标记异步),则需要停止当前DOM的构建,直到 JS 的资源加载并被 JS引擎 执行后才继续构建DOM


解析CSS构建CSSOM Tree


CSS解释器会将CSS文件解释成内部表示结构,生成CSS规则树,这个过程也是和DOM解析类似的,CSS 字节转换成字符,接着词法解析与法解析,最后构成 CSS对象模型(CSSOM) 的树结构


构建渲染树(Render Tree)


DOM TreeCSSOM Tree都构建完毕后,接着将它们合并成渲染树(Render Tree)渲染树 只包含渲染网页所需的节点,然后用于计算每个可见元素的布局,并输出给绘制流程,将像素渲染到屏幕上。


渲染(布局,绘制,合成)



  • 计算CSS样式 ;

  • 构建渲染树 ;

  • 布局,主要定位坐标和大小,是否换行,各种position overflow z-index属性 ;

  • 绘制,将图像绘制出来。


这个过程比较复杂,涉及到两个概念: reflow(回流)和repain(重绘)。DOM节点中的各个元素都是以盒模型的形式存在,这些都需要浏览器去计算其位置和大小等,这个过程称为relow;当盒模型的位置,大小以及其他属性,如颜色,字体,等确定下来之后,浏览器便开始绘制内容,这个过程称为repain。页面在首次加载时必然会经历reflow和repain。reflow和repain过程是非常消耗性能的,尤其是在移动设备上,它会破坏用户体验,有时会造成页面卡顿。所以我们应该尽可能少的减少reflow和repain。


这里Reflow和Repaint的概念是有区别的:


(1)Reflow:即回流。一般意味着元素的内容、结构、位置或尺寸发生了变化,需要重新计算样式和渲染树。


(2)Repaint:即重绘。意味着元素发生的改变只是影响了元素的一些外观之类的时候(例如,背景色,边框颜色,文字颜色等),此时只需要应用新样式绘制这个元素就可以了。


回流的成本开销要高于重绘,而且一个节点的回流往往回导致子节点以及同级节点的回流, 所以优化方案中一般都包括,尽量避免回流。


「回流一定导致重绘,但重绘不一定会导致回流」


「合成(composite)」


最后一步合成( composite ),这一步骤浏览器会将各层信息发送给GPU,GPU将各层合成,显示在屏幕上


普通图层和复合图层


可以简单的这样理解,浏览器渲染的图层一般包含两大类:普通图层以及复合图层


首先,普通文档流内可以理解为一个复合图层(这里称为默认复合层,里面不管添加多少元素,其实都是在同一个复合图层中)


其次,absolute布局(fixed也一样),虽然可以脱离普通文档流,但它仍然属于默认复合层


然后,可以通过硬件加速的方式,声明一个新的复合图层,它会单独分配资源 (当然也会脱离普通文档流,这样一来,不管这个复合图层中怎么变化,也不会影响默认复合层里的回流重绘)


可以简单理解下:「GPU中,各个复合图层是单独绘制的,所以互不影响」,这也是为什么某些场景硬件加速效果一级棒


可以Chrome源码调试 -> More Tools -> Rendering -> Layer borders中看到,黄色的就是复合图层信息。



收起阅读 »

VS Code settings.json 10 个高(装)阶(杯)配置!

1. 隐藏活动栏 VS Code 左侧图标列表是“活动栏”,我们可以点击图标跳转到各个模块,我们可以通过配置 workbench.activityBar.visible 来控制活动栏的显示; 如果你想恢复显示,可以自定义快捷键来再次显示这块空间; 如何设置...
继续阅读 »

1. 隐藏活动栏


VS Code 左侧图标列表是“活动栏”,我们可以点击图标跳转到各个模块,我们可以通过配置 workbench.activityBar.visible 来控制活动栏的显示;


image.png


如果你想恢复显示,可以自定义快捷键来再次显示这块空间;


image.png


如何设置快捷键:keybindings


我们可以用 Ctrl+B 来隐藏/显示文件资源管理器,用 Ctrl+Alt+B 来隐藏/显示活动栏;


虽然,你也可以在命令面板 Ctrl+Shift+P 中搜索,不过使用快捷键就更有装杯效果~


活动栏在隐藏状态下,我们也可以通过快捷键跳转到不同的工作空间,比如 Ctrl+Shift+E(跳转到文件资源管理器)、Ctrl+Shift+X(跳转到扩展)、Ctrl+Shift+H(搜索和替换)等


2. AI 编码


GitHub Copilot 是 VS Code 的一个扩展,可在你编写代码时生成片段代码;


由于它是人工智能、机器学习,有可能会产生一些你不喜欢的代码,但是请别仇视它,毕竟 AI 编码是未来趋势!


image.png


处于隐私考虑,建议不要在工作中使用 Copilot,但是可以在个人项目中使用它,有趣又有用,尤其是对于单元测试;


可以在 settings.json 中配置 Copilot;


3. 字体与缩放


这个不多做解释,根据自己的需求进行文字大小及缩放比例的配置;


image.png


当然,你不一定要在 settings.json 中去编写这个配置,也可以在可选项及输入配置窗口进行配置。


4. 无拖拽/删除确认


如果你对自己的编程技能足够自信,或者对 VS Code 的 Ctrl+Z 足够自信,你可以配置取消删除确认;因为拖拽/删除确认有时也会干扰思路~


image.png


image.png


5. 自更新绝对路径


VS Code 的最佳功能之一是它的文件导入很友善,使用绝对路径,例如:@/components/Button../../Button 更让人舒适;


当移动文件重新组织目录时,希望 VS Code 能自动更新文件的路径?你可以配置它们:


image.png


请注意,您需要在 .tsconfig/.jsconfig 文件中配置路径才能使用绝对路径导入。


6. 保存执行


配置过 ESLint 保存修正的应该都知道这个配置。这个非常强大,出了 fixAll,还能 addMissingImports 补充缺少的 Imports,或者其它你想在保存后执行的行为;


image.png


这个配置就像是编程魔法~


7. CSS 格式化


你可能已经在使用 Stylelint 了,如果没有,请在配置中设置它!


image.png


另一个设置是 editor.suggest.insertMode,当设置为“replace”时,意味着——当你选择一个提示并按 Tab 或 Enter 时,将替换整个文本为提示,这非常有用。


8. 开启 Emmet


你可能熟悉 Emmet —— Web 开发人员必备工具包,如果没有,请设置它;虽然它内置于 VS Code,但必须手动配置启用;


image.png


9. Tailwind CSS


Tailwind CSS 是一个功能类优先的 CSS 框架,它集成了诸如 flexpt-4text-center 和 rotate-90 这样的的类,它们能直接在脚本标记语言中组合起来,构建出任何设计。


虽然它目前尚未内置在 VS Code 中,但可作为免费的 VS Code 扩展进行安装使用,还可以配置附加设置增强它的功能!


image.png


10. 单击打开文件


VS Code 默认用户界面,有个奇怪的现象,它需要双击才能从文件资源管理器中打开文件。


单击一下得到的是奇怪的“预览”模式,当你单击下一个文件时,第一个文件就会消失。这就像只有一个标签。


image.png


需要进行这个配置,关闭后,单击将在新选项卡中打开文件。问题解决了~


将配置用 Settings Sync 进行同步,去哪都能个性化、自定义!酷的!


image.png




以上就是本篇分享,你有啥压箱底的 VS Code-settings.json 配置吗?欢迎评论留言,分享交流 (#^.^#)



收起阅读 »

总结 scripts 阻塞 HTML 解析

看了一些类似文章,里面有这样一些表述:解析 HTML,DOM 解析等;我们统一下表述:下载完 HTML 文件后,浏览器会解析 HTML,目的是为了构建 DOM 结构 或 生成 DOM 树。 内联 scripts <html> <head&...
继续阅读 »

看了一些类似文章,里面有这样一些表述:解析 HTML,DOM 解析等;我们统一下表述:下载完 HTML 文件后,浏览器会解析 HTML,目的是为了构建 DOM 结构 或 生成 DOM 树


内联 scripts


<html>
<head></head>
<body>
 <script>
console.log('irene')
 </script>
</body>
</html>

解析 HTML 过程中遇到 内联 scripts 会暂停解析,先执行 scripts,然后继续解析 HTML。


普通外联 scripts


<script src="index.js"></script>

解析 HTML 过程中遇到 普通外联 scripts 会暂停解析,发送请求并执行 scripts,然后继续解析 HTML。如下图所示,绿色表示 HTML 解析;灰色表示 HTML 解析暂停;蓝色表示 scripts 下载;粉色表示 scripts 执行。


image.png


defer scripts


<script defer src="index.js"></script>

解析 HTML 过程中遇到 defer scripts 不会停止解析,scripts 也会并行下载;等整个 HTML 解析完成后按引用 scripts 的顺序执行。defer scripts 在 DOMContentLoaded 事件触发之前执行。defer 属性只能用于外联 scripts。


image.png
浏览器并行下载多个 defer scripts,文件小的 scripts 很可能先下载完,defer 属性除了告诉浏览器不去阻塞 HTML 解析,同时还保证了defer scripts 的相对顺序。即使 small.js 先下载完,它还是得等到 long.js 执行完再去执行。


async scripts


<script async src="index.js"></script>

解析 HTML 过程中遇到 async scripts 不会停止解析,scripts 也会并行下载;scripts 下载完之后开始执行,阻塞 HTML 解析。async scripts 的执行顺序和它的引用顺序不一定相同。async scripts 可能在 DOMContentLoaded 事件触发之前或之后执行。如果 HTML 先解析完 async scripts 才下载完成,此时 DOMContentLoaded 事件已经触发, async scripts 很有可能来不及监听 DOMContentLoaded 事件。async 属性只能用于外联 scripts。


image.png
浏览器并行下载多个 async scripts,文件小的 scripts 很可能先下载完,先下载完就先执行了,它无法保证按 async scripts 的引用顺序执行。


defer VS async


在实践中,defer 用于需要整个 DOM 或其相对执行顺序很重要的 scripts。而 async 则用于独立的 scripts,如计数器或广告,而它们的相对执行顺序并不重要。


dynamic scripts


let script = document.createElement('script');
script.src = "/article/script-async-defer/long.js";
document.body.append(script);

脚本一旦被 append 到文档中就开始下载,动态脚本在默认情况下表现的像 async scripts,即先下载完先执行;可以显示设置 script.async = false,这样 scripts 的执行顺序就会和 defer scripts 表现的一致。


这两篇文章中,文一说 defer scripts 会阻塞 HTML 解析,文二说 defer scripts 不会阻塞 HTML 解析。其实两者的想法是一致的:即 defer scripts 的下载不会阻塞 HTML 解析,且执行是在构建完 DOM 之后;之所以有两种不同的表述是因为文一定义阻塞 HTML 解析的标准:是否在 DOMContentLoaded 之前执行,在之前执行就是阻塞 HTML 解析,否则就是不会;defer scripts 是在构建完 DOM 之后,DOMContentLoaded 之前执行的,所有文一认为 defer scripts 会阻塞 HTML 解析。文二说 defer scripts 不会阻塞 HTML 解析就很好理解了。


作者:小被子
链接:https://juejin.cn/post/7027673904927735822

收起阅读 »

手把手教你封装一个日期格式化的工具函数

最近还是在做那个练习的小项目,做完接收数据并渲染到页面上的时候,发现后端小伙伴又在给我找活干了欸,单纯的渲染这当然是小kiss啦,可这个字段是个什么东西? "createTime" : "2021-01-17T13:32:06.381Z", "lastLogi...
继续阅读 »

最近还是在做那个练习的小项目,做完接收数据并渲染到页面上的时候,发现后端小伙伴又在给我找活干了欸,单纯的渲染这当然是小kiss啦,可这个字段是个什么东西?


"createTime" : "2021-01-17T13:32:06.381Z",
"lastLoginTime" : "2021-01-17T13:32:06.381Z"

直接CV到百度,查出来这一串是一种时间格式,下面放上它的解释:



T表示分隔符,Z表示的是UTC.
UTC:世界标准时间,在标准时间上加上8小时,即东八区时间,也就是北京时间。


另:还有别的时间格式和时间戳,想了解的小伙伴可以百度了解一下哦,免得跟我一样,看到了才想着去百度了解,事先了解一下,没坏处的。



了解完了,现在我应该做的,就是将这个时间变成我们大家看得懂的那种格式,并将它渲染到页面上。


开始上手


JavaScript中,处理日期和时间,当然要用到我们的Date对象,所以我们先来写出这个函数的雏形:


const formateDate = (value)=>{
let date = new Date(value)
}

下面要做的应该是定义日期的格式了,这里我用的是yyyy-MM-dd hh:mm:ss


let fmt = 'yyyy-MM-dd hh:mm:ss'

因为年月日时分秒这里都是两位或者两位以上的,所以在获取的时候我是这样定义的:


const o = {
'y+': date.getFullYear(),
'M+': date.getMonth()+1,
'd+': date.getDate(),
'h+': date.getHours(),
'm+': date.getMinutes(),
's+': date.getSeconds()
}

首先先解释一下getMonth()+1,去查看Date文档就知道,这个函数的返回是0-11,我们正常月份都是1-12,所以加上1,才是正确的月份。


定义了规则之后,我们循环它,应该就可以得到我们想要的结果了吧。


for(let k in o ){
if(new RegExp(`(${k})`).test(fmt)){
const val = o[k] + ''
fmt = fmt.replace(RegExp.$1,val)
}
}

我们继续来解释一下代码,首先fmt.replace是代表我们要做一个替换,RegExp.$1就是获取到上面的值表达式内容,将这个内容,换成val中的值,之所以上面加了一个空字符串,是为了将val变成字符串的形式,以防再出纰漏。


$1.png


我们渲染上去,看看结果如何?


秒未补零.png


日期被我们成功的转化为了,我们能看得懂的东西,但是我们可以看到,秒这里,只有一位,也就是说,在秒只有个位数的情况下,我们应该给予它一个补零的操作。



不光是秒,其他也应该是这个道理哦!



关于补零


补零的话,有两种方式,先来说说笨笨的这种吧:


我们去判断这个字符串的长度,如果是1,我们就加个零,如果不是1,那么就不用加。


var a = '6'
a.length = 1?'0'+a:a // '06'

再来说个略微比这个高级一点的:


我们需要两位,所以直接给字符串补上两个零,再用substr去分割一下字符串,就能得到我们想要的了。


var b = '6'
var result = ('00'+b).substr(b.length) // '06'

那么我们去改一下上面的代码,就得到了下面的函数:


const formateDate = (value)=>{
let date = new Date(value)
let fmt = 'yyyy-MM-dd hh:mm:ss'
const o = {
'y+': date.getFullYear(),
'M+': date.getMonth()+1,
'd+': date.getDate(),
'h+': date.getHours(),
'm+': date.getMinutes(),
's+': date.getSeconds()
}
for(let k in o ){
if(new RegExp(`(${k})`).test(fmt)){
const val = o[k] + ''
fmt = fmt.replace(RegExp.$1,RegExp.$1.length==1?val:('00'+val).substr(val.length))
}
}
return fmt
}

在刷新一下网页,看看我们成功了没!


补零结束.png


成功是成功了,但是我们发现,前面的年竟然被干掉了,他也变成了两位的样子,这可不行啊,我们定义的年份格式可是四位的。


这可咋整.webp


但是别慌,这个只需要把年份单独的去做判断,不与其他2位的格式一起进行操作就能解决啦,所以我们最终的函数是这样的:


const formateDate = (value)=>{
let date = new Date(value)
let fmt = 'yyyy-MM-dd hh:mm:ss'
if(/(y+)/.test(fmt)){
fmt = fmt.replace(RegExp.$1,date.getFullYear())
}
const o = {
'M+': date.getMonth()+1,
'd+': date.getDate(),
'h+': date.getHours(),
'm+': date.getMinutes(),
's+': date.getSeconds()
}
for(let k in o ){
if(new RegExp(`(${k})`).test(fmt)){
const val = o[k] + ''
fmt = fmt.replace(RegExp.$1,RegExp.$1.length==1?val:('00'+val).substr(val.length))
}
}
return fmt
}

看一下结果吧:


image.png


如果再严谨一点,可以再给函数加个参数,传递一个rule,这样方便我们后期进行调整数据格式,在定义格式的时候用||就好了。


let fmt = '传入的新格式' || '默认的格式
收起阅读 »

用CSS告诉你为何大橘为重!!

本期我们通过vite+scss去完成一个橘猫心情变化的创意动画,这里的逻辑我们将不使用任何js代码,仅依靠css来完成,所以,通过本期的动画,你可以到一些css动画和绘制的一些技巧。废话不多说,先康康效果~ 还比较可爱吧。当鼠标(鱼)移入出,橘子闷闷不乐,无...
继续阅读 »

本期我们通过vite+scss去完成一个橘猫心情变化的创意动画,这里的逻辑我们将不使用任何js代码,仅依靠css来完成,所以,通过本期的动画,你可以到一些css动画和绘制的一些技巧。废话不多说,先康康效果~


VID_20211030_184225.gif


还比较可爱吧。当鼠标(鱼)移入出,橘子闷闷不乐,无精打采的。但当鼠标(鱼)移入,橘子一看见最喜欢的鱼立马就开心了,连天气都变好了,对,这只橘子就是这么馋,变成胖橘是有原因的。


好了,我们马上就要进入正文了,我们会从基础搭建,太阳,云,猫的绘制和动画去了解制作这个动画的流程。


正文


1.搭建与结构


yarn add vite sass sass-loader

我们是用vite和sass去完成项目的构建,和样式的书写,所以我们先安装下他们。


<div id="app">
<div class="warrper">
<div class="sun"></div>
<div class="cloud"></div>
<div class="cat">
<div class="eye left"><div class="eye-hide"></div></div>
<div class="eye right"><div class="eye-hide"></div></div>
<div class="nose"></div>
<div class="mouth"></div>
</div>
</div>
</div>

在html我们先写出结构来。div#app作为主界面去填满一屏,而div.warrper就作为主要内容的展示区域也就是那个圆圈。然后,在圆圈里面我们放太阳div.sun,云朵div.cloud,猫div.cat,当然猫里面还有眼睛鼻子嘴巴这些,至于猫的耳朵就用两个伪类做个三角形去实现。


2.变量与界面


$cat:rgb(252, 180, 125);

:root{
--bgColor:rgb(81, 136, 168);
--eyeHideTop:0px;
--cloudLeft:45%;
--mouthRadius:10px 10px 0 0;
}
#app{
width: 100%;
height: 100vh;
position: relative;
display: flex;
justify-content: center;
align-items: center;
background-image: repeating-linear-gradient(0deg, hsla(340,87%,75%,0.2) 0px, hsla(340,87%,75%,0.2) 30px,transparent 30px, transparent 60px),repeating-linear-gradient(90deg, hsla(340,87%,75%,0.2) 0px, hsla(340,87%,75%,0.2) 30px,transparent 30px, transparent 60px),linear-gradient(90deg, rgb(255,255,255),rgb(255,255,255));
}

.warrper{
width: 320px;
height: 320px;
border-radius: 50%;
border: 10px solid white;
position: relative;
overflow: hidden;
background-color: var(--bgColor);
transition: background-color 1s linear;
cursor:url("./assets/fish.png"),default;
&:hover{
--bgColor:rgb(178, 222, 247);
--eyeHideTop:-20px;
--cloudLeft:100%;
--mouthRadius:0 0 10px 10px;
}
}

我们先定义猫的主色调,还有一些要变化的颜色和距离,因为我们移入将通过css3去改变这些属性,来达到某些动画的实现。


我们期望的是,当鼠标移入圆圈后,天空变晴,云朵退散,猫开心充满精神,所以,bgColor:天空颜色,eyeHideTop猫的眼皮y轴距离,cloudLeft云朵x轴偏移距离,mouthRadius猫嘴巴的圆角值。目前来说,当鼠标移入div.warrper后,这些值都会发生变化。另外,我自定义了鼠标图标移入圆圈变成了一条鱼(即cursor:url(图片地址))。这里的hover后的值是我事先算好的,如果大家重新开发别的动画可以一边做一边算。


微信截图_20211030200310.png


3.太阳与云朵


.sun{
width: 50px;
height: 50px;
position: absolute;
background-color: rgb(255, 229, 142);
border:7px solid rgb(253, 215, 91);
border-radius: 50%;
left: 55%;
top: 14%;
box-shadow: 0 0 6px rgb(255, 241, 48);
}

太阳我们就画个圆圈定好位置,然后用box-shadow投影去完成一点发光的效果。


微信截图_20211030200343.png


然后,我们再开始画云朵~


.cloud{
width: 100px;
height: 36px;
background-color: white;
position: absolute;
transition: left .6s linear;
left: var(--cloudLeft);
top: 23%;
border-radius: 36px;
animation: bouncy 2s ease-in-out infinite;
&::before{
content: '';
width: 50px;
height: 50px;
background-color: white;
border-radius: 50%;
position: absolute;
top: -23px;
left: 18px;
}
&::after{
content: '';
width: 26px;
height: 26px;
background-color: white;
border-radius: 50%;
position: absolute;
top: -16px;
left: 56px;
}
}

@keyframes bouncy {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}

云朵很简单,我们就是画一个圆角矩形,然后用两个伪类画一个大圆和小圆叠在一起就非常像云了,另外,我们再加个animation动画,让他时大时小,有动的感觉。


微信截图_20211030200357.png


4.橘猫与动画


.cat{
width: 180px;
height: 160px;
background-color: $cat;
position: absolute;
bottom: -20px;
left: 50%;
margin-left: -90px;
animation: wait 2s ease-in-out infinite;
&::after,
&::before{
content: '';
display: block;
border-style: solid;
border-width: 20px 30px;
position: absolute;
top: -30px;
}
&::after{
right: 0;
border-color: transparent $cat $cat transparent;
}
&::before{
left: 0;
border-color: transparent transparent $cat $cat;
}
.eye{
width: 42px;
height: 42px;
border-radius: 50%;
position: absolute;
top: 30px;
background:white;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
.eye-hide{
height: 20px;
position: absolute;
top: var(--eyeHideTop);
left: -2px;
right:-2px;
background-color: $cat;
transition: top .5s ease-in-out;
z-index: 2;
}
&::before{
content: "";
height: 36px;
width: 36px;
background-color:black;
border-radius: 50%;
}
&::after{
content: "";
width: 24px;
height: 24px;
background-color: white;
border-radius: 50%;
position: absolute;
right: 0px;
top: 0px;
}
&.left{
left: 24px;
}
&.right{
right: 24px;
}
}
.nose{
width: 0;
height: 0;
border-top: 7px solid rgb(248, 226, 226);
border-left: 7px solid transparent;
border-right: 7px solid transparent;
position: absolute;
left: 50%;
margin-left: -7px;
top: 70px;
}
.mouth{
width: 26px;
height: 20px;
background-color: rgb(255, 217, 217);
position: absolute;
top: 85px;
left: 50%;
margin-left: -13px;
border-radius: var(--mouthRadius);
transition: border-radius .2s linear;
overflow: hidden;
&::after,
&::before{
content: "";
position: absolute;
display: block;
top: 0;
border-top: 7px solid white;
border-left: 2px solid transparent;
border-right: 2px solid transparent;
}
&::after{
right: 5px;
}
&::before{
left: 5px;
}
}
}

@keyframes wait{
0% {
bottom: -20px;
}
50% {
bottom: -25px;
}
100% {
bottom: -20px;
}
}

我们可以实现分解出,耳朵(伪类)+ 一双眼睛 + 鼻子 + 嘴(包含两颗尖牙) = 猫。


通过以上代码就不难看出主要都是在使用绝对定位来完成,面部器官的摆放。绝大部分都是css基础代码来实现的。唯一可以注意的点,就是耳朵这个三角形,我们是通过伪类实现,将它不设置宽高,而主是通过border-width+boder-color这个技巧去绘制出三角形的,算是个css小技巧吧,后面的鼻子和嘴巴里的尖牙都是这个小技巧来实现的。


另外,还要说的是那双眼睛,我们用先填充白底再分别用伪类去实现里面的黑底圆和白色小圆,肯定有同学问了为什么不用border是实现白色圆框,就不用浪费一个伪类去完成黑底圆了?因为我们用了overflow: hidden,他多余隐藏的内容是border以下的元素,而border边框可以无损,那么他的伪类能盖不住他的border,这样显得眼皮垂下的圆圈还是很大不自然,所以我们又造了一个伪类去实现他的黑底,让外圆不使用border了。


剩下的就是做一个等待的animation动画给猫,让他上下移动着,来实现不停的呼吸的效果。


微信截图_20211030200539.png


这样一直无精打采的橘猫就完成了。因为在第一部分,我们事先已经把移入后改变的变量算好了,现在把鼠标移入,效果就出现咯~


微信截图_20211030200546.png


结语


讲到这里我们就已经完成了这个动画了,不得不说,看见食物这么激动不愧都叫他胖橘!


这里有我这个动画【I Like Fish】codepen地址可以看到演示和代码,有兴趣的小伙伴可以康康。


本期还是比较侧重基础和动画创意的,主要是新手向,大佬勿喷,经常用css写写动画挺有意思的,不仅可以熟悉基本功,而且会迸发出很多创意来,也是一种锻炼自己的学习方式吧,多练习下,大家一起加油鸭~



收起阅读 »

你需要知道的 19 个 console 实用调试技巧

众所周知,浏览器的开发者工具为我们提供了强大的调试系统,可以用来查看DOM树结构、CSS样式调试、动画调试、JavaScript代码断点调试等。今天我们就来看看console调试的那些实用的调试技巧。 如今,我们项目的开发通常会使用React、Vue等前端框...
继续阅读 »

众所周知,浏览器的开发者工具为我们提供了强大的调试系统,可以用来查看DOM树结构、CSS样式调试、动画调试、JavaScript代码断点调试等。今天我们就来看看console调试的那些实用的调试技巧。


如今,我们项目的开发通常会使用React、Vue等前端框架,前端调试也变得更加有难度,除了使用React Dev Tools,Vue Dev Tools等插件之外,我们使用最多的就是console.log(),当然多数情况下,console.log()就能满足我们的需求,但是当数据变得比较复杂时,console.log()就显得有些单一。其实console对象为我们提供了很多打印的方法,下面是console对象包含的方法(这里使用的是Chrome浏览器,版本为 95.0.4638.54(正式版本) (arm64)):


image.png


console 对象提供了浏览器控制台调试的接口,我们可以从任何全局对象中访问到它,如果你平时只是用console.log()来输出一些变量,那你可能没有用过console那些强大的功能。下面带你用console玩玩花式调试。


一、基本打印


1. console.log()


console.log()就是最基本、最常用的用法了。它可以用在JavaScript代码的任何地方,然后就可以浏览器的控制台中看到打印的信息。其基本使用方法如下:


let name = "CUGGZ";
let age = 18;
console.log(name) // CUGGZ
console.log(`my name is: ${name}`) // CUGGZ
console.log(name, age) // CUGGZ 18
console.log("message:", name, age) // message: CUGGZ 18

除此之外,console.log()还支持下面这种输出方式:


let name = "CUGGZ";
let age = 18;
let height = 180;
console.log('Name: %s, Age: %d', name, age) // Name: CUGGZ, Age: 18
console.log('Age: %d, Height: %d', age, height) // Age: 18, Height: 180

这里将后面的变量赋值给了前面的占位符的位置,他们是一一对应的。这种写法在复杂的输出时,能保证模板和数据分离,结构更加清晰。不过如果是简单的输出,就没必要这样写了。在console.log中,支持的占位符格式如下:



  • 字符串:%s

  • 整数:%d

  • 浮点数:%f

  • 对象:%o或%O

  • CSS样式:%c


可以看到,除了最基本的几种类型之外,它还支持定义CSS样式:


let name = "CUGGZ";
console.log('My Name is %cCUGGZ', 'color: skyblue; font-size: 30px;')

打印结果如下(好像并没有什么卵用):


image.png


这个样式打印可能有用的地方就是打印图片,用来查看图片是否正确:


console.log('%c ','background-image:url("http://iyeslogo.orbrand.com/150902Google/005.gif");background-size:120% 120%;background-repeat:no-repeat;background-position:center center;line-height:60px;padding:30px 120px;');

打印结果如下:


image.png


严格地说,console.log()并不支持打印图片,但是可以使用CSS的背景图来打印图片,不过并不能直接打印,因为是不支持设置图片的宽高属性,所以就需要使用line-heigh和padding来撑开图片,使其可以正常显示出来。


我们可以使用console.log()来打印字符画,就像知乎的这样:


image.png


可以使用字符画在线生成工具,将生成的字符粘贴到console.log()即可。在线工具:mg2txt。我的头像生成效果如下,中间的就是生成的字符:


image.png


除此之外,可以看到,当占位符表示一个对象时,有两种写法:%c或者%C,那它们两个有什么区别呢?当我们指定的对象是普通的object对象时,它们两个是没有区别的,如果是DOM节点,那就有有区别了,来看下面的示例:


image.png


可以看到,使用 %o 打印的是DOM节点的内容,包含其子节点。而%O打印的是该DOM节点的对象属性,可以根据需求来选择性的打印。


2. console.warn()


console.warn() 方法用于在控制台输出警告信息。它的用法和console.log是完全一样的,只是显示的样式不太一样,信息最前面加一个黄色三角,表示警告:


const app = ["facebook", "google", "twitter"];
console.warn(app);

打印样式如下:


image.png


3. console.error()


console.error()可以用于在控制台输出错误信息。它和上面的两个方法的用法是一样的,只是显示样式不一样:


const app = ["facebook", "google", "twitter"];
console.error(app)

image.png


需要注意,console.exception() 是 console.error() 的别名,它们功能是相同的。


当然,console.error()还有一个console.log()不具备的功能,那就是打印函数的调用栈:


function a() {
b();
}
function b() {
console.error("error");
}
function c() {
a();
}
c();

打印结果如下:


image.png


可以看到,这里打印出来了函数函数调用栈的信息:b→a→c。


console对象提供了专门的方法来打印函数的调用栈(console.trace()),这个下面会介绍到。


4. console.info()


console.info()可以用来打印资讯类说明信息,它和console.log()的用法一致,打印出来的效果也是一样的:


image.png


二、打印时间


1. console.time() & console.timeEnd()


如果我们想要获取一段代码的执行时间,就可以使用console对象的console.time() 和console.timeEnd()方法,来看下面的例子:


console.time();

setTimeout(() => {
console.timeEnd();
}, 1000);

// default: 1001.9140625 ms

它们都可以传递一个参数,该参数是一个字符串,用来标记唯一的计时器。如果页面只有一个计时器时,就不需要传这个参数 ,如果有多个计时器,就需要使用这个标签来标记每一个计时器:


console.time("timer1");
console.time("timer2");

setTimeout(() => {
console.timeEnd("timer1");
}, 1000);

setTimeout(() => {
console.timeEnd("timer2");
}, 2000);

// timer1: 1004.666259765625 ms
// timer2: 2004.654052734375 ms

2. console.timeLog()


这里的console.timeLog()上面的console.timeEnd()类似,但是也有一定的差别。他们都需要使用console.time()来启动一个计时器。然后console.timeLog()就是打印计时器当前的时间,而console.timeEnd()是打印计时器,直到结束的时间。下面来看例子:


console.time("timer");

setTimeout(() => {
console.timeLog("timer")
setTimeout(() => {
console.timeLog("timer");
}, 2000);
}, 1000);

// timer: 1002.80224609375 ms
// timer: 3008.044189453125 ms

而使用console.timeEnd()时:


console.time("timer");

setTimeout(() => {
console.timeEnd("timer")
setTimeout(() => {
console.timeLog("timer");
}, 2000);
}, 1000);


打印结果如下:


image.png


可以看到,它会终止当前的计时器,所以里面的timeLog就无法在找到timer计数器了。
所以两者的区别就在于,是否会终止当前的计时。


三、分组打印


1. console.group() & console.groupEnd()


这两个方法用于在控制台创建一个信息分组。 一个完整的信息分组以 console.group() 开始,console.groupEnd() 结束。来看下面的例子:


console.group();
console.log('First Group');
console.group();
console.log('Second Group')
console.groupEnd();
console.groupEnd();

打印结果如下:


image.png


再来看一个复杂点的:


console.group("Alphabet")
console.log("A");
console.log("B");
console.log("C");
console.group("Numbers");
console.log("One");
console.log("Two");
console.groupEnd("Numbers");
console.groupEnd("Alphabet");

打印结果如下:


image.png


可以看到,这些分组是可以嵌套的。当前我们需要调试一大堆调试输出,就可以选择使用分组输出,


2. console.groupCollapsed()


console.groupCollapsed()方法类似于console.group(),它们都需要使用console.groupEnd()来结束分组。不同的是,该方法默认打印的信息是折叠展示的,而group()是默认展开的。来对上面的例子进行改写:


console.groupCollapsed("Alphabet")
console.log("A");
console.log("B");
console.log("C");
console.groupCollapsed("Numbers");
console.log("One");
console.log("Two");
console.groupEnd("Numbers");
console.groupEnd("Alphabet");

其打印结果如下:


image.png


可以看到,和上面方法唯一的不同就是,打印的结果被折叠了,需要手动展开来看。


四、打印计次


1. console.count()


可以使用使用console.count()来获取当前执行的次数。来看下面的例子:


for (i = 0; i < 5; i++) {
console.count();
}

// 输出结果如下
default: 1
default: 2
default: 3
default: 4
default: 5

它也可以传一个参数来进行标记(如果为空,则为默认标签default):


for (i = 0; i < 5; i++) {
console.count("hello");
}

// 输出结果如下
hello: 1
hello: 2
hello: 3
hello: 4
hello: 5

这个方法主要用于一些比较复杂的场景,有时候一个函数被多个地方调用,就可以使用这个方法来确定是否少调用或者重复调用了该方法。


2. console.countReset()


顾名思义,console.countReset()就是重置计算器,它会需要配合上面的console.count()方法使用。它有一个可选的参数label:



  • 如果提供了参数label,此函数会重置与label关联的计数,将count重置为0。

  • 如果省略了参数label,此函数会重置默认的计数器,将count重置为0。


console.count(); 
console.count("a");
console.count("b");
console.count("a");
console.count("a");
console.count();
console.count();

console.countReset();
console.countReset("a");
console.countReset("b");

console.count();
console.count("a");
console.count("b");

打印结果如下:


default:1
a:1
b:1
a:2
a:3
default:2
default:3
default:1
a:1
b:1

五、其他打印


1. console.table()


我们平时使用console.log较多,其实console对象还有很多属性可以使用,比如console.table(),使用它可以方便的打印数组对象的属性,打印结果是一个表格。console.table() 方法有两个参数,第一个参数是需要打印的对象,第二个参数是需要打印的表格的标题,这里就是数组对象的属性值。来看下面的例子:


const users = [ 
{
"first_name":"Harcourt",
"last_name":"Huckerbe",
"gender":"Male",
"city":"Linchen",
"birth_country":"China"
},
{
"first_name":"Allyn",
"last_name":"McEttigen",
"gender":"Male",
"city":"Ambelókipoi",
"birth_country":"Greece"
},
{
"first_name":"Sandor",
"last_name":"Degg",
"gender":"Male",
"city":"Mthatha",
"birth_country":"South Africa"
}
]

console.table(users, ['first_name', 'last_name', 'city']);

打印结果如下:


image.png


通过这种方式,可以更加清晰的看到数组对象中的指定属性。


除此之外,还可以使用console.table()来打印数组元素:


const app = ["facebook", "google", "twitter"];
console.table(app);

打印结果如下:
image.png
通过这种方式,我们可以更清晰的看到数组中的元素。


需要注意,console.table() 只能处理最多1000行,因此它可能不适合所有数据集。但是也能适用于多数场景了。


2. console.clear()


console.clear() 顾名思义就是清除控制台的信息。当清空控制台之后,会打印一句:“Console was clered”:


image.png


当然,我们完全可以使用控制台的清除键清除控制台:


image.png


3. console.assert()


console.assert()方法用于语句断言,当断言为 false时,则在信息到控制台输出错误信息。它的语法如下:


console.assert(expression, message)

它有两个参数:



  • expression: 条件语句,语句会被解析成 Boolean,且为 false 的时候会触发message语句输出;

  • message: 输出语句,可以是任意类型。



该方法会在expression条件语句为false时,就会打印message信息。当在特定情况下才输出语句时,就可以使用console.assert()方法。


比如,当列表元素的子节点数量大于等于100时,打印错误信息:


console.assert(list.childNodes.length < 100, "Node count is > 100");

其输出结果如下图所示:


image.png


4. console.trace()


console.trace()方法可以用于打印当前执行的代码在堆栈中的调用路径。它和上面的console.error()的功一致,不过打印的样式就和console.log()是一样的了。来看下面的例子:


function a() {
b();
}
function b() {
console.trace();
}
function c() {
a();
}
c();

打印结果如下:


image.png


可以看到,这里输出了调用栈的信息:b→a→c,这个堆栈信息是从调用位置开始的。


5. console.dir()


console.dir()方法可以在控制台中显示指定JavaScript对象的属性,并通过类似文件树样式的交互列表显示。它的语法如下:


console.dir(object);

它的参数是一个对象,最终会打印出该对象所有的属性和属性值。


在多数情况下,使用consoledir()和使用console.log()的效果是一样的。但是当打印元素结构时,就会有很大的差异了,console.log()打印的是元素的DOM结构,而console.dir()打印的是元素的属性:


image.png


image.png


6. console.dirxml()


console.dirxml()方法用于显示一个明确的XML/HTML元素的包括所有后代元素的交互树。 如果无法作为一个element被显示,那么会以JavaScript对象的形式作为替代。 它的输出是一个继承的扩展的节点列表,可以让你看到子节点的内容。其语法如下:


console.dirxml(object);

该方法会打印输出XML元素及其后代元素,对于XML和HTML元素调用console.log()和console.dirxml()是等价的。


image.png


7. console.memory


console.memory是console对象的一个属性,而不是一个方法。它可以用来查看当前内存的使用情况,如果使用过多的console.log()会占用较多的内存,导致浏览器出现卡顿情况。


image.png



收起阅读 »

淦,为什么 "???".length !== 3

不知道你是否遇到过这样的疑惑,在做表单校验长度的需求中,发现不同字符 length 可能大小不一。比如标题中的 "𠮷" length 是 2(需要注意📢,这并不是一个中文字!)。 '吉'.length // 1 '𠮷'.length // 2 '❤'.le...
继续阅读 »

不知道你是否遇到过这样的疑惑,在做表单校验长度的需求中,发现不同字符 length 可能大小不一。比如标题中的 "𠮷" length 是 2(需要注意📢,这并不是一个中文字!)。


'吉'.length
// 1

'𠮷'.length
// 2

'❤'.length
// 1

'💩'.length
// 2

要解释这个问题要从 UTF-16 编码说起。


UTF-16


ECMAScript® 2015 规范中可以看到,ECMAScript 字符串使用的是 UTF-16 编码。



定与不定: UTF-16 最小的码元是两个字节,即使第一个字节可能都是 0 也要占位,这是固定的。不定是对于基本平面(BMP)的字符只需要两个字节,表示范围 U+0000 ~ U+FFFF,而对于补充平面则需要占用四个字节 U+010000~U+10FFFF



在上一篇文章中,我们有介绍过 utf-8 的编码细节,了解到 utf-8 编码需要占用 1~4 个字节不等,而使用 utf-16 则需要占用 2 或 4 个字节。来看看 utf-16 是怎么编码的。


UTF-16 的编码逻辑


UTF-16 编码很简单,对于给定一个 Unicode 码点 cp(CodePoint 也就是这个字符在 Unicode 中的唯一编号):



  1. 如果码点小于等于 U+FFFF(也就是基本平面的所有字符),不需要处理,直接使用。

  2. 否则,将拆分为两个部分 ((cp – 65536) / 1024) + 0xD800((cp – 65536) % 1024) + 0xDC00 来存储。



Unicode 标准规定 U+D800...U+DFFF 的值不对应于任何字符,所以可以用来做标记。



举个具体的例子:字符 A 的码点是 U+0041,可以直接用一个码元表示。


'\u0041'
// -> A

A === '\u0041'
// -> true

Javascript 中 \u 表示 Unicode 的转义字符,后面跟着一个十六进制数。


而字符 💩 的码点是 U+1f4a9,处于补充平面的字符,经过 👆 公式计算得到两个码元 55357, 56489 这两个数字用十六进制表示为 d83d, dca9,将这两个编码结果组合成代理对。


'\ud83d\udca9'
// -> '💩'

'💩' === '\ud83d\udca9'
// -> true

由于 Javascript 字符串使用 utf-16 编码,所以可以正确将代理对 \ud83d\udca9 解码得到码点 U+1f4a9


还可以使用 \u + {},大括号中直接跟码点来表示字符。看起来长得不一样,但他们表示的结果是一样的。


'\u0041' === '\u{41}'
// -> true

'\ud83d\udca9' === '\u{1f4a9}'
// -> true


可以打开 Dev Tool 的 console 面板,运行代码验证结果。



所以为什么 length 判断会有问题?


要解答这个问题,可以继续查看 规范,里面提到:在 ECMAScript 操作解释字符串值的地方,每个元素都被解释为单个 UTF-16 代码单元。



Where ECMAScript operations interpret String values, each element is interpreted as a single UTF-16 code unit.



所以像💩 字符实际上占用了两个 UTF-16 的码元,也就是两个元素,所以它的 length 属性就是 2。(这跟一开始 JS 使用 USC-2 编码有关,当初以为 65536 个字符就可以满足所有需求了)


但对于普通用户而言,这就完全没办法理解了,为什么明明只填了一个 '𠮷',程序上却提示占用了两个字符长度,要怎样才能正确识别出 Unicode 字符长度呢?


我在 Antd Form 表单使用的 async-validator 包中可以看到下面这段代码


const spRegexp = /[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]/g;

if (str) {
val = value.replace(spRegexp, '_').length;
}

当需要进行字符串长度的判断时,会将码点范围在补充平面的字符全部替换为下划线,这样长度判断就和实际显示的一致了!!!


ES6 对 Unicode 的支持


length 属性的问题,主要还是最初设计 JS 这门语言的时候,没有考虑到会有这么多字符,认为两个字节就完全可以满足。所以不止是 length,字符串常见的一些操作在 Unicode 支持上也会表现异常


下面的内容将介绍部分存在异常的 API 以及在 ES6 中如何正确处理这些问题。


for vs for of


例如使用 for 循环打印字符串,字符串会按照 JS 理解的每个“元素”遍历,辅助平面的字符将会被识别成两个“元素”,于是出现“乱码”。


var str = '👻yo𠮷'
for (var i = 0; i < str.length; i ++) {
console.log(str[i])
}

// -> �
// -> �
// -> y
// -> o
// -> �
// -> �

而使用 ES6 的 for of 语法就不会。


var str = '👻yo𠮷'
for (const char of str) {
console.log(char)
}

// -> 👻
// -> y
// -> o
// -> 𠮷

展开语法(Spread syntax)


前面提到了使用正则表达式,将辅助平面的字符替换的方式来统计字符长度。使用展开语法也可以得到同样的效果。


[...'💩'].length
// -> 1

slice, split, substr 等等方法也存在同样的问题。


正则表达式 u


ES6 中还针对 Unicode 字符增加了 u 描述符。


/^.$/.test('👻')
// -> false

/^.$/u.test('👻')
// -> true

charCodeAt/codePointAt


对于字符串,我们还常用 charCodeAt 来获取 Code Point,对于 BMP 平面的字符是可以适用的,但是如果字符是辅助平面字符 charCodeAt 返回结果就只会是编码后第一个码元对于的数字。


'羽'.charCodeAt(0)
// -> 32701
'羽'.codePointAt(0)
// -> 32701

'😸'.charCodeAt(0)
// -> 55357
'😸'.codePointAt(0)
// -> 128568

而使用 codePointAt 则可以将字符正确识别,并返回正确的码点。


String.prototype.normalize()


由于 JS 中将字符串理解成一串两个字节的码元序列,判断是否相等是根据序列的值来判断的。所以可能存在一些字符串看起来长得一模一样,但是字符串相等判断结果确是 false


'café' === 'café'
// -> false

上面代码中第一个 café 是有 cafe 加上一个缩进的音标字符\u0301组成的,而第二个 café 则是由一个 caf + é 字符组成的。所以两者虽然看上去一样,但码点不一样,所以 JS 相等判断结果为 false


'cafe\u0301'
// -> 'café'

'cafe\u0301'.length
// -> 5

'café'.length
// -> 4

为了能正确识别这种码点不一样,但是语意一样的字符串判断,ES6 增加了 String.prototype.normalize 方法。


'cafe\u0301'.normalize() === 'café'.normalize()
// -> true

'cafe\u0301'.normalize().length
// -> 4

总结


这篇文章主要是我最近重新学习编码的学习笔记,由于时间仓促 && 水平有限,文章中必定存在大量不准确的描述、甚至错误的内容,如有发现还请善意指出。❤️



收起阅读 »

「如何优雅的不写注释?」每个工程师都要不断追求的漫漫长路

引言 ✨ 作为一个软件开发者,我们需要进行协同,在这个协同的过程中我们需要让其他开发者可以读懂我们的代码,所以注释可能就变成了一个重要的代码解读标注手段。 说到这,大家可能就会感知到注释确实很重要,他可以是开发者有效沟通的渠道,但是我在这里想表达的是注释其实并...
继续阅读 »

引言 ✨


作为一个软件开发者,我们需要进行协同,在这个协同的过程中我们需要让其他开发者可以读懂我们的代码,所以注释可能就变成了一个重要的代码解读标注手段。


说到这,大家可能就会感知到注释确实很重要,他可以是开发者有效沟通的渠道,但是我在这里想表达的是注释其实并不是银弹,也不是最好的手段,在我的眼里,注释更多承担的是一个兜底的作用。


于是便有了本篇文章,我想通过更多亲身经历和书籍参考来阐述证明我的观点:



  • 什么是好注释

  • 什么是坏注释

  • 怎么不通过注释提高编码可读性「易于上手版」

  • 挖一个关于前端架构的大坑「待我能力提升」



注意我并不是贬低使用注释,而是想给大家推销一种思想,即更多的用代码阐述自己的想法,把注释作为一种保底手段,而不是银弹。而且我切实的知道很多历史因素导致代码极其难以阅读,利用注释去表达信息也是没有办法的。但是希望大家读完本文之后可以更多去考量如何让代码更可读,而不是怎么写注释



不堪回首的摸爬滚打 🌧️


image.png


我毕业工作这一年以来,一直在不断加深对整洁编码的理解,我也经历过迷惑和自我怀疑。但是通过不断的实践,思考,回顾,我渐渐的有了自己的理解。


希望大家在阅读完成我的经历与摸爬滚打后,有所收获,如果觉得没啥参考性,就当作看了一场平平无奇的故事。


求学 🎓


曾经在求学过程中,大家的老师应该都会鼓励大家去多些注释去阐述你的编码思想,我便一直认为写注释是一个标准,是我在开发中必须要去做的事情。


然而现在我回过头去看曾经老师的教诲,我会觉得是出于以下三点考虑:



  • 对于一个计算机初学者,写注释可以让你去梳理你的编码思路

  • 写注释的时候也会对代码逻辑进行思考,类比伪代码

  • 很现实的一点,帮助老师理解你的代码,毕竟有的代码不写注释老师都不知道判卷的时候该给分还是不给分


实习 🧱


之后我便开始了我的第一段职业生涯,大四实习,我来到我现在所在的公司呆了两个月,做的也是比较简单的工作,改一改前人留下的遗产「bug」,到即将返校的时候,我接到了一个开发任务,当时我开发了一天,晚上进行 code-review,我记得当时我信心满满,给每个子方法和关键变量都写了注释,本来信心满满,但是最后我的代码被前辈批到自闭,其中不乏关于代码设计和命名相关的建议,也是在这次 review 中我听到了一句足以毁灭我学习生涯所建立的世界观的话:



好的代码是不需要注释的



工作 💻


工作之后,最开始我接触的项目也是一个恶臭代码的重灾区了,充斥着各种难以阅读的代码逻辑,但是那时候我还没有一个评判好坏的能力,一度去怀疑自己。



是不是我太菜了,才读不懂别人那些高端的代码



没错,我真的这么想过,甚至十分的自我怀疑,但是后期我经历了几件事情让我对这个自我怀疑的想法消除了:



  • 在导师的指导下对代码的反复 code-review 并重写



当时我们发现该项目存在需求遗漏,于是这个需求便来到了我的头上,即使项目紧急,导师还是给我细心 review,最后这个功能我重写了三四次,也让我对什么样的代码是好的有了一个粗略的概念。




  • 对某一个模块的完全重新设计与编码



经历了从设计到评审,再到编码,最后 review 的过程。




  • 相关书籍的阅读



本篇文章不做书籍推荐了,只是表达对于如何整洁编码是存在很多前人经验与指导原则存在的,新人可以优先阅读《代码整洁之道




  • 丰富的自我实践与思考



在我后来参与产线内部平台建设,负责安全运维大模块,负责冬奥会项目过程中也是不断的在追求整洁编码。思考,实践,回顾,一直伴随着我的职业道路,对于代码如何编写的更整洁也渐渐有了自己的想法。



在工作的过程中,我对于“好的代码是不需要注释的”这句话的理解也在不断加深。当然,如果对于某些难以处理的遗留问题,注释也是一个不错的方法对其进行注解描述。


总结 🔍


最开始我觉得注释是必要的,后经过经验的积累,前辈的教导,自己的学习,不断的思考与回顾,到现在有了自己的一套思想。当然我不会去说我的思想是正确的,可能过几年之后我会回来打我自己的脸,其实想法的改变,也能代表一种成长吧~


有关注释的杂七杂八 🌲


image.png



别给糟糕的代码加注释,重新写吧




  • 什么也比不上放置良好的注释来的有用

  • 什么也不会比乱七八糟的注释更有本事搞乱一个模块

  • 什么也不会比陈旧,提供错误信息的注释更具破坏性


若编程语言有足够的表达力,或者我们长于用这些语言来表达意图,就不那么需要注释 —— 也许根本不需要。


上面的话引自《代码整洁之道》。但是从事这个行业越久我越无法否认其正确性,我们必须要知道的一件事是代码具有实时性,即你现在项目中的代码总是当前最新的,否则也无法正确运行。然而上面的注释我们根本无法知道是什么时候写的,不具备实时性。



  • 代码是在变动,演化的。然而注释并不能随之变动

  • 程序员不会长期维护注释

  • 注释会撒谎,而代码不会

  • 不准确的注释比没注释坏的多

  • ...


所以我的想法很坚定,注释无法美化糟糕的代码,与其花时间为糟糕的代码编写解释不如花时间把糟糕的代码变得整洁。



用代码来阐述思想一直是最好的办法。



当然总有些注释是必须的或是有利的,还有一些注释是有害的,下面和大家聊一聊什么是好注释,什么是坏注释。


好注释 🌈



  • 法律信息



比如版权或者著作权的声明




  • 提供信息的注释



比如描述一个方法的返回值,但是其实可以利用函数名来传达信息




  • 阐释



把某些晦涩难懂的参数或者返回值翻译成某种可读的形式,更好的方式是让参数和返回值自身就足够清楚




  • TODO 注释



这个可能大家都会经常用,用来记录我们哪里还有任务没有完成




  • 放大



比如一个看似不起眼却很重要的变量,我们可以用注释凸显它的重要性




  • ...


坏注释 😈



  • 喃喃自语



只有作者读的懂的注释,当你打算开始写注释,就要讲清楚原委,和读者有良好的沟通




  • 多余的注释



有的注释写不写没啥作用,很简单的方法都要写注释,甚至读代码都比看注释快




  • 误导性注释



程序员都已经够辛苦了,你还要用注释欺骗人家




  • 循规式注释



要求每个方法每个变量都要有注释,很多废话只会扰乱读者




  • 位置标记



比如打了一堆 ****** 来标注位置,这个我上学的时候经常干




  • 废话注释



毫无作用的废话




  • 注释掉的代码



很多读者会想,代码依然留在那一定有原因,最后不敢删除畏手畏脚




  • 信息过多的注释



注释中包含很多无关的细节,其实对读者完全没有必要




  • ...


优雅的不写注释 🌿


image.png


首先我再次阐述之前说过的话,编码实际上是一种社会行为,是需要沟通的。而如何让我们不借助注释来阐述我们的思想,其实是需要我们长期探索并在实践中积累经验的,从我的经验与视角出发,其实让我们的代码库更加整洁其实主要从以下两个方面考量:



  • 整洁编码

  • 前端架构


下面我分开来讲~



注意,编码不是一个人的事情,在我眼里如何做到团队成员编码风格的相近才是最具成效且需要长期努力的任务,也是相对理想且难以做到的。正所谓,就算我们写的代码很烂,但是烂的我们的成员可以相互理解,也是一种优秀「瞎说的,哈哈哈,代码可维护性还是要团队成员一起追求的」。



整洁编码 📚


首先我先引用几位前辈的话,带大家感受一下,什么样的代码是整洁的:



  • Bjarne:我喜欢优雅和高效的代码,代码的逻辑应当直接了当,叫缺陷难以隐藏们。尽量减少依赖关系,使之便于维护,依据某种分层战略完善错误处理代码,性能调至最优,省得引诱别人做没有规矩的优化,搞出一堆混乱出来,整洁的代码只做好一件事。

  • Grady: 整洁的代码简单直接,整洁的代码从不隐藏设计者的意图,充满干净利落的抽象和直截了当的控制语句。


对于整洁编码可以先简单总结:



  • 尽量减少依赖关系,便于维护

  • 简单直接,充满了干净利落逻辑处理和直截了当的控制语句。

  • 能够全部运行通过,并配有单元测试和验收测试

  • 没有重复的代码

  • 写的代码能够完全提现我们的设计理念「这个可以通过类、方法、属性的命名,代码逻辑编码的清晰来体现



在我们日常编码中,命名和函数可以说是我们最常接触的,也是最能影响我们代码整洁度的。于是本文中,我将围绕这两个方向为大家介绍几种易于上手的整洁编码方案。



下文参考我之前写过的一篇文章:关于整洁代码与重构的摸爬滚打


命名 🌟



  • 只要命名需要通过注释来补充,就表示我们的命名还是存在问题

  • 所有的命名都要有实际意义,命名会告诉你它为什么存在,它做什么事情,应该怎么用


比如列举一段曾经上学的时候可能写出的代码:


#include <stdio.h>  
int main(){
printf("Hello, C! \n");
int i = 10;
int m = 1;
for(int j = 0; j < i; j+=m){
for(int n = 0; n< i-j;n++){
printf("*");
}
printf("\n");
}
return 0;
}

我们看这里命名都是一大堆 i,m,n,j之类的根本不知道这些变量用来干嘛,其实这段代码最后仅仅打印出来的是 * 组成的直角三角形。但是当时写代码我确实就是这样,i, j,m,n等等字母用了一遍,也不包含什么语义上的东西,变量命名就是字母表里面选。


当然现在的命名就高端多了,开始从词典里面找了,还要排列组合,比如 getUserisAdmin。语义上提升了,通过命名我们也可以直观的判断这个方法是干嘛的,这个变量是干嘛的。


这样看其实命名是很有学问的事情,下面我开始列举几点命名中可以快速提升代码整洁度的方法:



  • 避免引起误导



不要用不该作为变量的专有名词来为变量命名,一组账号accountList ,却不是List类型,也是存在误导。命名过于相似:比如 XYZHandlerForAORBORC 和XYZControllerForAORBORDORC,谁能一眼就看出来呢~




  • 做有意义的区分


let fn = (array1,array2) =>{ 
for(let i =0 ;i<array1.length;i++){
array2[i] = array1[i];
}
}


比如上面 array1array2 就不是有意义的区分,这只是一个赋值操作,完全可以是 sourceArrayDesArray

再比如 起的名字:userInfouserData都是这种的,我们很难读懂这两个有啥子区别,这种区分也没啥意义,说白了这只是单词拼写的区分,而不是在语义上区分开了。




  • 使用读的出来的名称



编程是社会活动,免不了与人交流对话,使用难以轻松读出来的声音会导致你的思想难于传达。并且人类的大脑中有专门处理语言的区域,可以辅助你理解问题,不加以运用简直暴殄天物。简单举个例子:getYMDHMS,这个方法就是获取时间,然而就是难以阅读,是不好的命名。




  • 使用可以搜索的名称



之前的代码,我用个 i 作为变量。如果代码很长,我这想要追踪一下这个i的变化,简直折磨。同理我不喜欢直接以 valuedatainfo 等单词直接做变量,因为他们经常以其他变量的组成部分出现,难以追踪。




  • 程序中有意义的数字或者字符串应该用常量进行替换,方便查找


export const DEFAULT_ORDERBY = '-updateTime' 
export const DEFAULT_CHECKEDNUM = 0


比如采用上面的方式,既可以让代码更加语义化也方便集中修改




  • 类名和对象名应为名词或名词词组,方法名应为动词或动词词组



比如我们常用的 updatexxxfilteredXXX 都是这样的命名规则




  • 属性命名添加有用必要的语境,但是短名称如果足够用清楚,就比长名称好,别添加不必要的语境

  • 每个概念对应一个词



比如 taglabelticketworkOrder 各种混着用岂不是乱糟糟的,这读者容易混淆,也会为以后造成负担,也可能会隐藏一些 bug。所以我们在项目开发前可以确定一个名词术语表来避免这种情况发生。




  • ...


函数 🌟



大师写代码是在讲故事,而不是在写程序。




  • 短小:20封顶最佳

  • 函数的缩进层级尽可能的少

  • 函数参数尽量少

  • 使用具有描述性的函数名



当然函数越短小,功能越集中,就越便于取好名字




  • 抽取异常处理,即 try-catch 就是一个函数 ,函数应该只做一件事,错误处理就是一件事

  • 标识参数丑陋不堪


const updateList = (flag) {
if(flag){
// ...
} else {
// ...
}
}


比如一个方法,定义成上面这个样子,我们很难通过方法定义直接了解其能力以及参数的含义。




  • 函数名是动词,参数是名词,并保证顺序



比如 saveField(name)assertExpectedEqualsActual(expected,actual)




  • 无副作用



比如一个方法名是 updateList,后来者应该顺理成章的认为这个方法只会更新列表,然而开发者在这个方法中加入了其他的逻辑,可能导致后来者在使用这个方法后导致副作用,而代码报错无法正常运行。




  • 重复是软件中一切邪恶的根源,拒绝重复的代码

  • ...


写代码和写文章一样,先去想你要写什么,最后再去打磨,初稿也许粗糙无序,那就要斟酌推敲,直到达成心中的样子。编程艺术也是语言设计的艺术。


前端架构 🎋



本人现在工作一年有余,一年半不足,对于前端架构并不能很好的输出给大家,所以在此给大家先挖一个大坑,本章节中如有错误理解,请大家不吝赐教,与我探讨交流,感谢。



首先,我先解释一下我为什么要把前端架构放在这样的一篇文章中,其实是存在两条原因:



  • 从个人开发角度来看,优秀的前端架构可以增强代码的维护性



试想一个组织结构恶臭的项目,一定会影响你的阅读的,杂乱不堪的组件划分原则,不清晰的边界通通都会成为巨大的阻力。




  • 最近换了组,到了天擎终端平台组,新的 leader 也分享了很多关于组件化的经验与理解



浅薄无知的小寒草🌿,在线求鞭策。



那么,大家在提到前端架构的时候,会想到什么呢,我反正会想到以下几点:



  • 组件化

  • 架构模式

  • 规范 / 约定

  • 分层

  • ...


下面我逐条来讲~


架构模式 ✨


组件化我先跳过,最后再说,先说说架构模式,大家脑子里一定会想到 MVVMMVC 等模式,比如我们常用的 Vue 框架中的 MVVM ,以及普遍在 Spring 那一套中被提及并在在 nest.js 中有所应用的 MVC。但是关于架构模式前端说的可能还是相对较少,我的水平也有限,而且说起来可能就会跑题了,于是也不在本文过多赘述。


规范&约定 ✨


关于规范或者约定,常见的包括:



  • 目录结构

  • 命名规范

  • 术语表

  • ...


其实这几点我们很好理解,我们会通过约定或者脚手架等方式来规范化我们的目录结构,使我们同一个产线下项目的目录结构保证一致。以及我们在开发前的设计阶段可能也需要出具一份术语表,这个前文也听到过一个含义用一个确定的词来表示,否则可能会导致代码的混乱。


关于命名规范,首先我们需要去约定一个统一的命名规则,我们常见的是变量命名为小驼峰,文件命名为连字符。但是这个命名规范其实我们可以做的事情不止这些,比如我说几个例子:



  • 前端命名规范是小驼峰,服务端命名是下划线,我们怎么处理让前端编码中屏蔽掉命名规则差异。

  • 同一个含义我们可以用很多命名来表示,比如:handleStaffUpdate / updateStaff。在项目初期我们完全可以对其进行约束使用哪种命名风格,以让我们项目一致性加强。

  • ...


分层 ✨


关于分层,大家的差异可能会比较大,比如我们可能会把我们的前端项目分为以下几层:



  • 业务层

  • 服务层

  • 模型层「可能有也可能没有」


业务层就是我们比较熟悉的,各种业务代码。


服务层「server」不知道大家的项目中有没有,我们项目使用 grpc 接口,由于接口粒度较高,我们通常会在 server 层对接口再次处理,合并,或者在这个层去完成一些服务端不合理设计的屏蔽。


模型层「model」不常有,但是一些复杂的又需要复用的逻辑可能有这个层,就相当于逻辑的抽象,脱离于视图,之后如果我们需要复用这里的逻辑,而视图不同,我们就可以使用这个 model


合理的分层可以让我们的项目更清晰,减少代码冗杂,提升可维护性。


组件化 ✨


其实组件化一直都是前端架构中的大课题,首先我们可以通过组件化能得到什么,其实最重要的可能就是:



  • 复用


不知道大家的项目有没有统计代码复用率,我们是有的,而且这也是前端工程质量很重要的一个指标。然而在追求组件化的过程中其实我们很少会拥有一个衡量标准:



  • 什么情况需要拆分组件

  • 什么情况不需要拆分组件


团队对这个问题没有一个统一认知的情况下很容易造成:



  • 五花八门的组件拆分原则导致代码结构混乱

  • 无效的组件拆分导致文件过多,维护困难

  • 过深的组件嵌套层级「经历过的人一定会对此深恶痛绝」

  • ...


其实我最开始的时候也喜欢把组件按照很细的粒度进行拆分,想的是总会有用到的时候嘛,但是从架构整洁的角度出发,过细或者过于粗糙的组件拆分都会导致维护困难,复用困难等问题,现在的我可能更会从复用性角度出发:



  • 这个东西会不会复用


只从复用性考量很容易的就会把组件区分为两大类:



  • 需要复用的组件

  • 几乎不会被复用的组件



注意我没有说什么组件是肯定不会被复用的,而是几乎不会被复用。



所以我们就可以坐下来思考,把我们工作中常见的场景拎出来,过一遍,因为我们工作的业务场景不同,所以我肯定还是以我的业务场景出发,那么我可以把我的组件分成几种:



  • page 组件

  • layout 组件

  • 业务组件


其中我认为,page 组件是几乎不会复用的组件,layout 组件和业务组件在我眼里是可以复用的组件。



这只是很粗糙的的区分,之后还有很多问题:



  • 如何把业务组件写的好用

  • 如何确定一个组件的边界

  • ...


这些我们就要从消费者角度考量了。



当然其实组件化也可以和分层一起考虑,因为组件其实也会有层级,比如:



  • 基础 ui 组件[参考element-ui]

  • 基础业务组件


基础业务组件也可以按照是否跨模块等原则继续进行分层,这个可以按照大家的业务场景自行考量。


总结 ✨


从实际经验出发,合理的架构确实是项目易于维护「从而优雅的不写注释🌿」,而这是一个自顶向下分析决策的过程,本章节篇幅有限,加上我水平有限,无法在此过多赘述,还请大家持续期待我的分享。


结束语 ☀️


image.png


那么本篇文章就结束了,涵盖了我个人经历上的摸爬滚打,解析什么样的注释是好的,什么样的注释是坏的,并从编码整洁度与前端架构的角度出发来考量如何提升代码的可维护性。以此来论述我的观点:



注释不是维护代码的银弹,而便于维护的代码需要从整洁编码前端架构两个「或者更多」层面入手。



我工作的时间不长也不短了,已经一年出头了,我一直秉承着编码是社会性工作,需要协同合作,代码的可维护性也是一名职业软件工程师需要持续追求的观点。


思考,实践,回顾的过程没有停歇,我在此也希望大家多思考,作为一名工程师我们需要追求的不仅仅只有:



收起阅读 »

Node 之一个进程的死亡

人固有一死,一个 Node 进程亦是如此,总有万般不愿也无法避免。从本篇文章我们看看一个进程灭亡时如何从容离去。 一个 Node 进程,除了提供 HTTP 服务外,也绝少不了跑脚本的身影。跑一个脚本拉取配置、处理数据以及定时任务更是家常便饭。在一些重要流程中能...
继续阅读 »

人固有一死,一个 Node 进程亦是如此,总有万般不愿也无法避免。从本篇文章我们看看一个进程灭亡时如何从容离去。


一个 Node 进程,除了提供 HTTP 服务外,也绝少不了跑脚本的身影。跑一个脚本拉取配置、处理数据以及定时任务更是家常便饭。在一些重要流程中能够看到脚本的身影:



  1. CI,用以测试、质量保障及部署等

  2. Cron,用以定时任务

  3. Docker,用以构建镜像


如果在这些重要流程中脚本出错无法及时发现问题,将有可能引发更加隐蔽的问题。如果在 HTTP 服务出现问题时,无法捕获,服务异常是不可忍受的。


最近观察项目镜像构建,会偶尔发现一两个镜像虽然构建成功,但容器却跑不起来的情况究其原因,是因为 一个 Node 进程灭亡却未曾感知到的问题


Exit Code



什么是 exit code?



exit code 代表一个进程的返回码,通过系统调用 exit_group 来触发。


POSIX 中,0 代表正常的返回码,1-255 代表异常返回码,在业务实践中,一般主动抛出的错误码都是 1。在 Node 应用中调用 API process.exitCode = 1 来代表进程因期望外的异常而中断退出。


这里有一张关于异常码的附表 Appendix E. Exit Codes With Special Meanings


异常码在操作系统中随处可见,以下是一个关于 cat 进程的异常以及它的 exit code,并使用 strace 追踪系统调用。


$ cat a
cat: a: No such file or directory

# 使用 strace 查看 cat 的系统调用
# -e 只显示 write 与 exit_group 的系统调用
$ strace -e write,exit_group cat a
write(2, "cat: ", 5cat: ) = 5
write(2, "a", 1a) = 1
write(2, ": No such file or directory", 27: No such file or directory) = 27
write(2, "\n", 1
) = 1
exit_group(1) = ?
+++ exited with 1 +++

strace 追踪进程显示的最后一行可以看出,该进程的 exit code 是 1,并把错误信息输出到 stderr (stderr 的 fd 为2) 中


如何查看 exit code


strace 中可以来判断进程的 exit code,但是不够方便过于冗余,更无法第一时间来定位到异常码。


有一种更为简单的方法,通过 echo $? 来确认返回码


$ cat a
cat: a: No such file or directory

$ echo $?
1

$ node -e "preocess.exit(52)"
$ echo $?
52

未曾感知的痛苦何在: throw new ErrorPromise.reject 区别


以下是两段代码,第一段抛出一个异常,第二段 Promise.reject,两段代码都会如下打印出一段异常信息,那么两者有什么区别?


function error () {
throw new Error('hello, error')
}

error()

// Output:

// /Users/shanyue/Documents/note/demo.js:2
// throw new Error('hello, world')
// ^
//
// Error: hello, world
// at error (/Users/shanyue/Documents/note/demo.js:2:9)

async function error () {
return new Error('hello, error')
}

error()

// Output:

// (node:60356) UnhandledPromiseRejectionWarning: Error: hello, world
// at error (/Users/shanyue/Documents/note/demo.js:2:9)
// at Object.<anonymous> (/Users/shanyue/Documents/note/demo.js:5:1)
// at Module._compile (internal/modules/cjs/loader.js:701:30)
// at Object.Module._extensions..js (internal/modules/cjs/loader.js:712:10)

在对上述两个测试用例使用 echo $? 查看 exit code,我们会发现 throw new Error()exit code 为 1,而 Promise.reject() 的为 0。


从操作系统的角度来讲,exit code 为 0 代表进程成功运行并退出,然而此时即使有 Promise.reject,操作系统也会视为它执行成功。


这在 DockerfileCI 中执行脚本时将留有安全隐患。


Dockerfile 在 Node 镜像构建时的隐患


当使用 Dockerfile 构建镜像或者 CI 时,如果进程返回非0返回码,构建就会失败。


这是一个浅显易懂的含有 Promise.reject() 问题的镜像,我们从这个镜像来看出问题所在。


FROM node:12-alpine

RUN node -e "Promise.reject('hello, world')"

构建镜像过程如下,最后两行提示镜像构建成功:即使在构建过程打印出了 unhandledPromiseRejection 信息,但是镜像仍然构建成功。


$ docker build -t demo .
Sending build context to Docker daemon 33.28kB
Step 1/2 : FROM node:12-alpine
---> 18f4bc975732
Step 2/2 : RUN node -e "Promise.reject('hello, world')"
---> Running in 79a6d53c5aa6
(node:1) UnhandledPromiseRejectionWarning: hello, world
(node:1) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
(node:1) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
Removing intermediate container 79a6d53c5aa6
---> 09f07eb993fe
Successfully built 09f07eb993fe
Successfully tagged demo:latest

但如果是在 node 15 镜像内,镜像会构建失败,至于原因以下再说。


FROM node:15-alpine

RUN node -e "Promise.reject('hello, world')"

$ docker build -t demo .
Sending build context to Docker daemon 2.048kB
Step 1/2 : FROM node:15-alpine
---> 8bf655e9f9b2
Step 2/2 : RUN node -e "Promise.reject('hello, world')"
---> Running in 4573ed5d5b08
node:internal/process/promises:245
triggerUncaughtException(err, true /* fromPromise */);
^

[UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "hello, world".] {
code: 'ERR_UNHANDLED_REJECTION'
}
The command '/bin/sh -c node -e "Promise.reject('hello, world')"' returned a non-zero code: 1

Promise.reject 脚本解决方案


能在编译时能发现的问题,绝不要放在运行时。所以,构建镜像或 CI 中需要执行 node 脚本时,对异常处理需要手动指定 process.exitCode = 1 来提前暴露问题


runScript().catch(() => {
process.exitCode = 1
})

在构建镜像时,Node 也有关于异常解决方案的建议:



(node:1) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag --unhandled-rejections=strict (see nodejs.org/api/cli.htm…). (rejection id: 1)



根据提示,--unhandled-rejections=strict 将会把 Promise.reject 的退出码设置为 1,并在将来的 node 版本中修正 Promise 异常退出码。


而下一个版本 Node 15.0 已把 unhandled-rejections 视为异常并返回非0退出码。


$ node --unhandled-rejections=strict error.js 

Signal


在外部,如何杀死一个进程?答:kill $pid


而更为准确的来说,一个 kill 命令用以向一个进程发送 signal,而非杀死进程。大概是杀进程的人多了,就变成了 kill。



The kill utility sends a signal to the processes specified by the pid operands.



每一个 signal 由数字表示,signal 列表可由 kill -l 打印


# 列出所有的 signal
$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX

这些信号中与终端进程接触最多的为以下几个,其中 SIGTERM 为 kill 默认发送信号,SIGKILL 为强制杀进程信号


在 Node 中,process.on 可以监听到可捕获的退出信号而不退出。以下示例监听到 SIGINT 与 SIGTERM 信号,SIGKILL 无法被监听,setTimeout 保证程序不会退出


console.log(`Pid: ${process.pid}`)

process.on('SIGINT', () => console.log('Received: SIGINT'))
// process.on('SIGKILL', () => console.log('Received: SIGKILL'))
process.on('SIGTERM', () => console.log('Received: SIGTERM'))

setTimeout(() => {}, 1000000)

运行脚本,启动进程,可以看到该进程的 pid,使用 kill -2 97864 发送信号,进程接收到信号并未退出


$ node signal.js
Pid: 97864
Received: SIGTERM
Received: SIGTERM
Received: SIGTERM
Received: SIGINT
Received: SIGINT
Received: SIGINT

容器中退出时的优雅处理


当在 k8s 容器服务升级时需要关闭过期 Pod 时,会向容器的主进程(PID 1)发送一个 SIGTERM 的信号,并预留 30s 善后。如果容器在 30s 后还没有退出,那么 k8s 会继续发送一个 SIGKILL 信号。如果古时皇帝白绫赐死,教你体面。


其实不仅仅是容器,CI 中脚本也要优雅处理进程的退出。


当接收到 SIGTERM/SIGINT 信号时,预留一分钟时间做未做完的事情。


async function gracefulClose(signal) {
await new Promise(resolve => {
setTimout(resolve, 60000)
})

process.exit()
}

process.on('SIGINT', gracefulClose)
process.on('SIGTERM', gracefulClose)

这个给脚本预留时间是比较正确的做法,但是如果是一个服务有源源不断的请求过来呢?那就由服务主动关闭吧,调用 server.close() 结束服务


const server = http.createServer(handler)

function gracefulClose(signal) {
server.close(() => {
process.exit()
})
}

process.on('SIGINT', gracefulClose)
process.on('SIGTERM', gracefulClose)

总结



  1. 当进程结束的 exit code 为非 0 时,系统会认为该进程执行失败

  2. 通过 echo $? 可查看终端上一进程的 exit code

  3. Node 中 Promise.reject 时 exit code 为 0

  4. Node 中可以通过 process.exitCode = 1 显式设置 exit code

  5. 在 Node12+ 中可以通过 node --unhandled-rejections=strict error.js 执行脚本,视 Promise.rejectexit code 为 1,在 Node15 中修复了这一个问题

  6. Node 进程退出时需要优雅退出

  7. k8s 关闭 POD 时先发一个 SIGTERM 信号,留 30s 时间处理未完成的事,如若 POD 没有正常退出,30s 过后发送 SIGKILL 信号


收起阅读 »

CSS 奇技淫巧 | 巧妙实现文字二次加粗再加边框

需求背景 - 文字的二次加粗 今天遇到这样一个有意思的问题: 在文字展示的时候,利用了 font-weight: bold 给文字进行加粗,但是觉得还是不够粗,有什么办法能够让文字更粗一点呢? emm,不考虑兼容性的话,答案是可以利用文字的 -webkit...
继续阅读 »

需求背景 - 文字的二次加粗


今天遇到这样一个有意思的问题:



  1. 在文字展示的时候,利用了 font-weight: bold 给文字进行加粗,但是觉得还是不够粗,有什么办法能够让文字更粗一点呢?


emm,不考虑兼容性的话,答案是可以利用文字的 -webkit-text-stroke 属性,给文字二次加粗。


MDN - webkit-text-stroke: 该属性为文本字符添加了一个边框(笔锋),指定了边框的颜色, 它是 -webkit-text-stroke-width-webkit-text-stroke-color 属性的缩写。


看下面的 DEMO,我们可以利用 -webkit-text-stroke,给文字二次加粗:


<p>文字加粗CSS</p>
<p>文字加粗CSS</p>
<p>文字加粗CSS</p>
<p>文字加粗CSS</p>

p {
font-size: 48px;
letter-spacing: 6px;
}
p:nth-child(2) {
font-weight: bold;
}
p:nth-child(3) {
-webkit-text-stroke: 3px red;
}
p:nth-child(4) {
-webkit-text-stroke: 3px #000;
}

对比一下下面 4 种文字,最后一种利用了 font-weight: bold-webkit-text-stroke,让文字变得更为



CodePen Demo -- font-weight: bold 和 -webkit-text-stroke 二次加粗文字


如何给二次加粗的文字再添加边框?


OK,完成了上述第一步,事情还没完,更可怕的问题来了。


现在文字要在二次加粗的情况下,再添加一个不同颜色的边框。


我们把原本可能可以给文字添加边框的 -webkit-text-stroke 属性用掉了,这下事情变得有点棘手了。这个问题也可以转变为,如何给文字添加 2 层不同颜色的边框?


当然,这也难不倒强大的 CSS(SVG),让我们来尝试下。


尝试方法一:使用文字的伪元素放大文字


第一种尝试方法,有点麻烦。我们可以对每一个文字进行精细化处理,利用文字的伪元素稍微放大一点文字,将原文字和访达后的文字贴合在一起。



  1. 将文字拆分成一个一个独立元素处理

  2. 利用伪元素的 attr() 特性,利用元素的伪元素实现同样的字

  3. 放大伪元素的字

  4. 叠加在原文字之下


上代码:


<ul>
<li data-text="文">文</li>
<li data-text="字">字</li>
<li data-text="加">加</li>
<li data-text="粗">粗</li>
<li data-text="C">C</li>
<li data-text="S">S</li>
<li data-text="S">S</li>
</ul>

ul {
display: flex;
flex-wrap: nowrap;
}

li {
position: relative;
font-size: 64px;
letter-spacing: 6px;
font-weight: bold;
-webkit-text-stroke: 3px #000;

&::before {
content: attr(data-text);
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
color: red;
-webkit-text-stroke: 3px #f00;
z-index: -1;
transform: scale(1.15);
}
}

可以简单给上述效果加个动画,一看就懂:



CodePen Demo -- 利用伪元素给加粗文字添加边框


看着不错,但是实际上仔细观察,边框效果很粗糙,文字每一处并非规则的被覆盖,效果不太能接受:



尝试方法二:利用 text-shadow 模拟边框


第一种方法宣告失败,我们继续尝试第二种方式,利用 text-shadow 模拟边框。


我们可以给二次加粗的文字添加一个文字阴影:


<p>文字加粗CSS</p>

p {
font-size: 48px;
letter-spacing: 6px;
font-weight: bold;
-webkit-text-stroke: 1px #000;
text-shadow: 0 0 2px red;
}

看看效果:


image


好吧,这和边框差的也太远了,它就是阴影。


不过别着急,text-shadow 是支持多重阴影的,我们把上述的 text-shadow 多叠加几次:


p {
font-size: 48px;
letter-spacing: 6px;
font-weight: bold;
-webkit-text-stroke: 1px #000;
- text-shadow: 0 0 2px red;
+ text-shadow: 0 0 2px red,0 0 2px red,0 0 2px red,0 0 2px red,0 0 2px red,0 0 2px red,0 0 2px red,0 0 2px red,0 0 2px red,0 0 2px red;
}


Wow,不仔细看的话,利用这种叠加多层 text-shadow 的方式,还真的非常像边框!


当然,如果我们放大来看,瑕疵就比较明显了,还是能看出是阴影:



CodePen Demo -- 利用 text-shadow 给文字添加边框


尝试方法四:利用多重 drop-shadow()


在尝试了 text-shadow 之后,自然而然的就会想到多重 filter: drop-shadow(),主观上认为会和多重 text-shadow 的效果应该是一致的。


不过,实践出真知。


在实际测试中,发现利用 filter: drop-shadow() 的效果比多重 text-shadow 要好,模糊感会弱一些:


p {
font-weight: bold;
-webkit-text-stroke: 1px #000;
filter:
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red);
}

效果如下:


image


我们甚至可以利用它制作文字二次加粗后的多重边框:


p {
font-weight: bold;
-webkit-text-stroke: 1px #000;
filter:
drop-shadow(0 0 0.2px red)
// 重复 N 次
drop-shadow(0 0 0.2px red)
drop-shadow(0 0 0.25px blue)
// 重复 N 次
drop-shadow(0 0 0.25px blue);
}

效果如下:



然而,在不同屏幕下(高清屏和普通屏),drop-shadow() 的表现效果差别非常之大,实则也难堪重用。


我们没有办法了吗?不,还有终极杀手锏 SVG。


尝试方法四:利用 SVG feMorphology 滤镜给文字添加边框


其实利用 SVG 的 feMorphology 滤镜,可以非常完美的实现这个需求。


这个技巧,我在 有意思!不规则边框的生成方案 这篇文章中也有提及。


借用 feMorphology 的扩张能力给不规则图形添加边框


直接上代码:


<p>文字加粗CSS</p>

<svg width="0" height="0">
<filter id="dilate">
<feMorphology in="SourceAlpha" result="DILATED" operator="dilate" radius="2"></feMorphology>
<feFlood flood-color="red" flood-opacity="1" result="flood"></feFlood>
<feComposite in="flood" in2="DILATED" operator="in" result="OUTLINE"></feComposite>

<feMerge>
<feMergeNode in="OUTLINE" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</svg>

p {
font-size: 64px;
letter-spacing: 6px;
font-weight: bold;
-webkit-text-stroke: 2px #000;
filter: url(#dilate);
}

效果如下:



我们可以通过 SVG feMorphology 滤镜中的 radius 控制边框大小,feFlood 滤镜中的 flood-color 控制边框颜色。并且,这里的 SVG 代码可以任意放置,只需要在 CSS 中利用 filter 引入即可。


本文不对 SVG 滤镜做过多的讲解,对 SVG 滤镜原理感兴趣的,可以翻看我上述提到的文章。


至此,我们就完美的实现了在已经利用 font-weight: bold-webkit-text-stroke 的基础上,再给文字添加不一样颜色的边框的需求。


放大了看,这种方式生成的边框,是真边框,不带任何的模糊:



CodePen Demo -- 利用 SVG feMorphology 滤镜给文字添加边框


最后


OK,本文到此结束,介绍了一些 CSS 中的奇技淫巧去实现文字二次加粗后加边框的需求,实际需求中,如果不是要求任意字都要有这个效果,其实我更推荐切图大法,高保真,不丢失细节。


当然,可能还有更便捷更有意思的解法,欢迎在评论区不吝赐教。


希望本文对你有所帮助 :)


想 Get 到最有意思的 CSS 资讯,千万不要错过我的公众号 -- iCSS前端趣闻 😄


更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。


如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。


作者:chokcoco
链接:https://juejin.cn/post/7023940690476269605

收起阅读 »

大道至简,繁在人心:在浏览器控制台安装npm包是什么操作?

  我们都知道,npm 是 JavaScript 世界的包管理工具,并且是 Node.js 平台的默认包管理工具。通过 npm 可以安装、共享、分发代码,管理项目依赖关系。虽然作为命令行工具的 npm 近年来逐渐式微,但是作为广泛使用的存储库的 npm,却依然...
继续阅读 »

  我们都知道,npm 是 JavaScript 世界的包管理工具,并且是 Node.js 平台的默认包管理工具。通过 npm 可以安装、共享、分发代码,管理项目依赖关系。虽然作为命令行工具的 npm 近年来逐渐式微,但是作为广泛使用的存储库的 npm,却依然如日中天,还是世界上最大的软件注册表


  通常,我们通过npm install xxx在 React、Vue、Angular 等现代前端项目中安装依赖,但是前端项目在本质上还是运行在浏览器端的 HTML、JavaScript 和 CSS,那么,我们有办法在浏览器控制台直接安装 npm 包并使用吗?


  如果你对这个问题感兴趣,不妨跟着我通过本文一探究竟,也许最终你会发现:越是“复杂”的东西,其原理越趋向“简单”


通过 <script /> 引入 cdn 资源


  在浏览器控制台安装 npm 包,看起来是个天马行空的想法,让人觉得不太切实际。如果我换一个方式进行提问:如何在浏览器/HTML 中引入 JavaScript 呢?也许你马上就有了答案:<script />标签。没错,我们的第一步就是通过 <script />标签在 HTML 页面上引入 cdn 资源。


  那么,又该如果在控制台在页面上插入<script />标签来引入 CDN 资源呢?这个问题可难不倒你


// 在页面中插入<script />标签
const injectScript = (url) => {
const script = document.createElement('script');
script.src = url;
document.body.appendChild(script);
};

  我们还得在资源引入后以及出现错误时,给用户一些提示:


script.onload = () => {
console.log(pkg_name_origin, ' 安装成功。');
};
script.onerror = () => {
console.log(pkg_name_origin, ' 安装失败。');
};

  这么以来,我们就可以直接在控制台引入 cdn 资源了,你可以再额外补充一些善后工作的处理逻辑,比如把<script />标签移除。当然,你也完全可以通过创建<link />标签来引入css样式库,这里不过多赘述。


根据包名安装 npm 包


  上面实现了通过<script /> 引入 cdn 资源,但是我们安装 npm 包一般都是通过npm install后面直接跟包名来完成的,显然单靠<script />的方式难以达到我们的饿预期,那么,有没有一种方式,可以将我们的包名直接转换成 cdn 资源地址呢?


  答案当然是:有。否则我写个屁啊 🤔,cdnjs就提供了这样的能力。


  cdnjs 提供了一个简单的 API,允许任何人快速查询 CDN 上的资源。具体使用读者可参考官方链接,这里给出一个根据包名查询 CDN 资源链接的示例,可以直接在浏览器地址栏打开这个链接查看:https://api.cdnjs.com/libraries?search=jquery,这是一个 get 请求,你将看到类似下面的页面,数组的第一项为名称/功能最相近的资源的最新 CDN 资源地址


jquery


  是以,根据包名搜索 cdn 资源 URL 便有如下的实现:


const cdnjs = async (name) => {
const searchPromise = await fetch(
`https://api.cdnjs.com/libraries?search=${name}`,
// 不显示referrer的任何信息在请求头中
{ referrerPolicy: 'no-referrer' }
);
const { results, total } = await searchPromise.json();
if (total === 0) {
console.error('Sorry, ', name, ' not found, please try another keyword.');
return;
}

// 取结果中最相关的一条
const { name: exactName, latest: url } = results[0];
if (name !== exactName) {
// 如果名称和你传进来的不一样
console.log(name, ' not found, import ', exactName, ' instead.');
}
// 通过<script />标签插入
injectScript(url);
};

安装特定版本的 npm 包


  我们在 npm 中还可以通过类似npm install jquery@3.5.1的语法安装特定版本的 npm 包,而 cdnjs 只能返回特定版本的详细信息(不含 cdn 资源链接)。


  UNPKG在此时可以帮我们一个大忙。unpkg 是一个快速的全球内容分发网络,适用于 npm 上的所有内容。使用它可以使用以下 URL 快速轻松地从任何包加载任何文件unpkg.com/:package@:version/:file


  例如,访问https://unpkg.com/jquery@3.5.1会自动重定向到https://unpkg.com/jquery@3.5.1/dist/jquery.js,并返回v3.5.1版本的jQuery文件内容(如果不带版本号,会返回最新的资源):


jquery_unpkg


  也就是说,我们可以将https://unpkg.com/包名直接丢给<script />标签来加载资源:


const unpkg = (name) => {
injectScript(`https://unpkg.com/${name}`);
};

完整代码


  将上面的代码简单整理,并通过一个统一的入口方法npmInstall进行调用:


// 存储原始传入的名称
let pkg_name_origin = null;
const npmInstall = (originName) => {
// Trim string
const name = originName.trim();
pkg_name_origin = name;
// 三种引入方式
// 如果是一个有效的URL,直接通过<script />标签插入
if (/^https?:\/\//.test(name)) return injectScript(name);
// 如果指定了版本,尝试使用unpkg加载
if (name.indexOf('@') !== -1) return unpkg(name);
// 否则,尝试使用cdnjs搜索
return cdnjs(name);
};

// 在页面中插入<script />标签
const injectScript = (url) => {
const script = document.createElement('script');
script.src = url;
script.onload = () => {
console.log(pkg_name_origin, ' 安装成功。');
};
script.onerror = () => {
console.log(pkg_name_origin, ' 安装失败。');
};
document.body.appendChild(script);
// document.body.removeChild(script);
};

const unpkg = (name) => {
injectScript(`https://unpkg.com/${name}`);
};

const cdnjs = async (name) => {
const searchPromise = await fetch(
`https://api.cdnjs.com/libraries?search=${name}`,
// 不显示referrer的任何信息在请求头中
{ referrerPolicy: 'no-referrer' }
);
const { results, total } = await searchPromise.json();
if (total === 0) {
console.error('Sorry, ', name, ' not found, please try another keyword.');
return;
}

// 取结果中最新的一条
const { name: exactName, latest: url } = results[0];
if (name !== exactName) {
console.log(name, ' not found, import ', exactName, ' instead.');
}
// 通过<script />标签插入
injectScript(url);
};

  我们可以使用类似npmInstall('moment')的方式在控制台进行调用:


console


  下面这些调用方式自然也是支持的:


npmInstall('jquery'); // 直接引入
npmInstall('jquery@2'); // 指定版本
npmInstall('https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'); // cdn地址

不每次都写这些函数行不行


  看了上面的操作,确实很简单,但是也许你会说:每次要使用时,我都得在控制台定义和调用函数,有些麻烦,不每次都写这些函数行不行?那自然是行的啦,你完全可以自己写一个浏览器插件,将这些JS代码注入页面,详情可参考7分钟学会写一个浏览器插件——突破某SDN未登录禁止复制的限制


  如果你实在不想写,其实有人已经为你写好了,那便是Console Importer,它可以让你的浏览器控制台成为更强大的实验场



  • 使用示例:


import



  • 效果图:


Console Importer



链接:Console Importer | Chrome 插件地址



可以干什么


  那么,本文介绍的方法和工具到底有什么用呢?


  平时开发中,我们经常会想要在项目里尝试一些操作或者验证一些库的方法、打印结果,通过本文的学习,以后你完全可以直接在控制台引入loadsh、moment、jQuery、React 等来进行使用和验证,减少在项目中进行console.log验证后再删除的频次。



  • 你可以通过引入jQuery方便的进行一些项目、页面中的DOM操作;

  • 你可以通过引入axios进行一些简单的接口请求;

  • 你可以通过引入moment.js来验证一些时间格式化方法的使用;

  • 你可以通过引入loadsh并调用它的方法完成一些便捷的计算;

  • ...


可以学到什么


unpkg


  unpkg 是一个内容源自 npm 的前端常用全球快速 CDN,它能以快速、简洁、优雅的方式提供任意包、任意文件的访问,在流行的类库、框架文档中常常能看到它的身影。使用方式一般是unpkg.com/:package@:version/:file。或者更简洁一点:https://unpkg.com/包名,包名包含版本号时,你将获得对应版本的js文件,不包含版本号时,你将获得这个库的最新版js文件。


cdnjs


  cdnjs是一种免费的开源 CDN 服务,受到超过 12.5% 的网站的信任,每月处理超过 2000 亿次请求,由 Cloudflare 提供支持。它类似 Google CDN 和微软CDN服务,但是速度比这二者更加快。CDNJS 上提供了众多 JavaScript 库,你可以直接在网页上引用这些 JS 文件,实现用户浏览网站的最佳速度体验。


  你还可以通过它的查询APIhttps://api.cdnjs.com/libraries?search=xxx进行特定库的cdn地址的查找,这个API还会给你返回一些你所查询的库的替代品


大道至简,繁在人心


  越是“复杂”的东西,其原理也许越是趋向“简单”,大道至简,繁在人心,祝每一个努力攀登者,终能豁然开朗,释然于心。


作者:獨釣寒江雪
链接:https://juejin.cn/post/7023916328637431816

收起阅读 »

微信小程序统一分享,全局接管页面分享消息的一些技巧

分享功能非常的重要,当某一个功能或文章打动用户的时候,能把这个小程序分享出去,就能带来裂变传播的效果。 全局接管分享事件 而随着功能越来越多,页面越来越多,每一个页面都需要添加分享的回调方法吗? onShareAppMessage: function () {...
继续阅读 »

分享功能非常的重要,当某一个功能或文章打动用户的时候,能把这个小程序分享出去,就能带来裂变传播的效果。


全局接管分享事件


而随着功能越来越多,页面越来越多,每一个页面都需要添加分享的回调方法吗?


onShareAppMessage: function () {
return {
title: '分享的标题',
path: '分享的页面路径'
}
},

有没有办法能全局统一接管分享呢?写一次,所有页面就都可以分享了。


能!


由于onShareAppMessage是一个函数,在用户点击右上角...时触发,或者<button open-type='share'>时触发。所以我们只要在这之前替换掉这个函数就可以了。


通过wx.onAppRoute(cb)这个方法,我们可以监听到微信小程序页面栈的变化。


//在小程序启动时添加全局路由变化的监听
onLaunch(){
wx.onAppRoute(()=>{
console.log('page stack changed');
console.log(getCurrentPages());
});
}

onAppRoute会在页面栈改变后被触发,这个时候通过getCurrentPages()方法,我们可以拿到小程序中全部的页面栈。


数组最后一个就是当前页面


image.png


现在直接给当前页面这个对象赋值onShareAppMessage即可


var pages = getCurrentPages();
var curPage = pages[pages.length-1];

curPage.onShareAppMessage=()=>{
return {
title:"被接管了"
}
}

再分享时我们就会发现被接管了


image.png


获取当前页面的地址


page参数不传的话,默认转发出去就是当前页面的地址。当然通过curPage.route也可以获取该页面地址。


var pages = getCurrentPages();
var curPage = pages[pages.length-1];

curPage.onShareAppMessage=()=>{
return {
title:"被接管了",
page:curPage.route
}
}

小技巧


如果就这样分享出去,用户打开的时候,就会直接展示这个分享的页面。直接返回,或者左滑屏幕,都会直接退出到聊天界面。用户主动分享一次产生的裂变不容易,我希望这个分享带来的价值最大化,让接到分享的微信用户看到更多页面的话怎么办呢?


永远先进首页,首页检查启动参数后再跳转相关页面


curPage.onShareAppMessage=()=>{
return {
title:"被接管了",
page:"/pages/home/home?url="+curPage.route
}
}


作者:大帅老猿
链接:https://juejin.cn/post/7024046727820738573

收起阅读 »

我阅读源码的五步速读法

阅读代码是程序员最重要的技能之一,我们每天都在读同事的代码或者第三方库的代码,那怎么高效的阅读代码呢?分享下我的源码阅读方法。 我的阅读源码的方法分为五步: 第一步,通过文档和测试用例了解代码的功能 阅读源码前要先了解代码的功能,可以通过文档或者测试用例,了解...
继续阅读 »

阅读代码是程序员最重要的技能之一,我们每天都在读同事的代码或者第三方库的代码,那怎么高效的阅读代码呢?分享下我的源码阅读方法。


我的阅读源码的方法分为五步:


第一步,通过文档和测试用例了解代码的功能


阅读源码前要先了解代码的功能,可以通过文档或者测试用例,了解代码做了什么,输入和输出是什么。


了解功能是阅读源码的基础,后面才会有方向感。


第二步,自己思考功能的实现方式


知道了源码有啥功能之后,要先思考下如果自己实现会怎么做。有个大概的思路就行。


如果想不通可以看下源码用到了哪些依赖库,这些依赖库都有啥功能,再想下应该怎么实现。


如果还想不通也没关系,重要的是要先自己思考下实现方式。


第三步,粗读源码理清实现思路


你已经有了一个大概的实现思路,然后再去读源码,看下它是怎么实现的。和你思路类似的地方很快就可以掠过去,而且印象也很深,和你思路不一样的地方,通过读代码搞清楚它的实现思路。


这一步不用关心细节,知道某段代码是干啥的就行,关键是和自己的思路做 diff,理清它的整体实现思路。


第四步,通过 debugger 理清实现细节


粗读源码理清了实现思路之后,对于一些部分的具体实现可能还不是很清楚,这时候就可以通过 debugger 来断点调试了。


构造一个能触发该功能的测试用例,在关心的代码处打一个断点,通过 debugger 运行代码。


这时候你已经知道这部分代码是干啥的了,单步调试也很容易理清每一条语句的功能,这样一条语句一条语句的搞懂之后,你就很容易能把这部分代码的实现细节理清楚。


这样一部分一部分的通过 debugger 理清细节实现之后,你就对整体代码的思路和细节的实现都有了比较好的掌握。


第五步,输出文章来讲述源码实现思路


当你觉得对源码的实现有了比较好的掌握的时候,可以输出一篇文章的方式来讲述源码的整体思路。


因为可能会有一些部分是你没注意到的,而在输出的过程中,会进行更全面的思考,这时候如果发现了一些没有读到的点,可以再通过前面几步去阅读源码,直到能清晰易懂的把源码的实现讲清楚。这样才算真正的把代码读懂了。


这就是我觉得比较高效的阅读源码的方法。


总结


我阅读源码的方法分为五步:



  1. 通过文档和测试用例了解代码的功能

  2. 自己思考功能的实现方式

  3. 粗读源码理清实现思路

  4. 通过 debugger 理清实现细节

  5. 输出文章来讲述源码实现思路


这五步缺一不可:



  • 缺了第一步,不了解功能就开始读源码,那读代码会没有方向感

  • 缺了第二步,不经过思考直接读源码,理解代码实现思路的效率会降低

  • 缺了第三步,不理清整体思路就开始 debugger,会容易陷入细节,理不清整体的思路

  • 缺了第四步,不 debugger 只大概理下整体思路,这样不能从细节上真正理清楚

  • 缺了第五步,不通过输出文章来检验,那是否真的理清了整体思路和实现细节是没底的


当然,这是我个人的阅读源码的方法,仅供参考。


作者:zxg_神说要有光
链接:https://juejin.cn/post/7024084789929967646

收起阅读 »

复杂web动画,不慌,选择 web Animations API

说动前端动画,我们熟知的有两种 CSS 动画 (requestAnimation/setTimeout/setInterval + 属性改变) 动画 当然有人可能会说canvas动画,从运动本质了还是第二种。 今天说的是第三种 Web Animations...
继续阅读 »

说动前端动画,我们熟知的有两种



  1. CSS 动画

  2. (requestAnimation/setTimeout/setInterval + 属性改变) 动画


当然有人可能会说canvas动画,从运动本质了还是第二种。


今天说的是第三种 Web Animations API, 也有简称为 WAAPI 的。


与纯粹的声明式CSS不同,JavaScript还允许我们动态地将属性值设置为持续时间。 对于构建自定义动画库和创建交互式动画,Web动画API可能是完成工作的完美工具。


举两个栗子


落球


点击之后,球体下落


ballFall2.gif


const ballEl = document.querySelector(".ball");
ballEl.addEventListener("click", function () {
let fallAni = ballEl.animate({
transform: ['translate(0, 0)', 'translate(20px, 8px)', 'translate(50px, 200px)']
}, {
easing: "cubic-bezier(.68,.08,.89,-0.05)",
duration: 2000,
fill: "forwards"
})
});

直播的世界消息或者弹幕


这是一个我们项目中一个实际的例子, 直播的弹幕。

我们需要消息先运动到屏幕中间,消息最少需要在停留2秒,如果消息过长,消息还需要 匀速滚动 ,之后再滑出屏幕。



  1. 滑入

  2. 暂停,如果消息过长,消息还需要匀速滚动

  3. 滑出


难点就在于,暂停阶段,消息滚动的时间并不是确定的,需要计算。 这个时候,纯CSS3的动画,难度就有些高了,采用 Web Animations API,天然的和JS亲和,那就简单多了。


先看看效果
longDan2.gif


shortDan.gif


代码也就简单的分为三段:滑入,暂停,滑出。

因为其天然支持Promise, 代码很简洁,逻辑也很清晰。


async function startAnimate() {
// 滑入
const totalWidth = stageWidth + DANMU_WITH;
const centerX = stageWidth * 0.5 - DANMU_WITH * 0.5;
const kfsIn = {
transform: [`translateX(${totalWidth}px)`, `translateX(${centerX}px)`]
}
await danmuEl.animate(kfsIn, {
duration: 2000,
fill: 'forwards',
easing: 'ease-out'
}).finished;

// 暂停部分
const contentEl = danmuEl.querySelector(".danmu-content");
const itemWidth = contentEl.getBoundingClientRect().width;
const gapWidth = Math.max(0, itemWidth - DANMU_WITH);
const duration = Math.max(0, Math.floor(gapWidth / 200) * 1000);

const translateX = duration > 0 ? gapWidth : 0;
const kfsTxt = {
transform: [`translateX(0px)`, `translateX(-${gapWidth}px)`]
};
await contentEl.animate(kfsTxt, {
duration,
delay: 2000,
fill: 'forwards',
easing: 'linear',
}).finished;

// 滑出
const kfsOut = {
transform: [`translateX(${centerX}px)`, `translateX(-${DANMU_WITH}px)`]
};
await danmuEl.animate(kfsOut, {
duration: 2000,
fill: "forwards",
easing: 'ease-in'
}).finished;

if (danmuEl) {
stageEl.removeChild(danmuEl);
}
isAnimating = false
}

web Animations API 两个核心的对象



  1. KeyframeEffect 描述动画属性

  2. Animation 控制播放


KeyframeEffect


描述动画属性的集合,调用keyframesAnimation Effect Timing Properties。 然后可以使用 Animation 构造函数进行播放。


其有三种构建方式,着重看第二种,参数后面说。



new KeyframeEffect(target, keyframes);

new KeyframeEffect(target, keyframes, options)

new KeyframeEffect(source)



当然我们可以显示的去创建 KeyframeEffect, 然后交付给Animation去播放。 但是我们通常不需要这么做, 有更加简单的API, 这就是接后面要说的 Element.animate


看一个KeyframeEffect复用的例子,new KeyframeEffect(kyEffect)基于当前复制,然后多处使用。


const box1ItemEl = document.querySelector(".box1");
const box2ItemEl = document.querySelector(".box2");

const kyEffect = new KeyframeEffect(null, {
transform: ['translateX(0)', 'translateX(200px)']
},
{ duration: 3000, fill: 'forwards' })

const ky1 = new KeyframeEffect(kyEffect);
ky1.target = box1ItemEl;

const ky2 = new KeyframeEffect(kyEffect);
ky2.target = box2ItemEl;

new Animation(ky1).play();
new Animation(ky2).play();


kf2.gif


Animation


提供播放控制、动画节点或源的时间轴。 可以接受使用 KeyframeEffect 构造函数创建的对象作为参数。


const box1ItemEl = document.querySelector(".box1");

const kyEffect = new KeyframeEffect(box1ItemEl, {
transform: ['translateX(0)', 'translateX(200px)']
},
{ duration: 3000, fill: 'forwards' })

const ani1 = new Animation(kyEffect);
ani1.play();

ani1.gif


常用的方法



Animation 事件监听


监听有两种形式:



  1. event 方式


因其继承于EventTarget,所有依旧有两种形式


animation.onfinish = function() {
element.remove();
}

animation.addEventListener("finish", function() {
element.remove();
}


  1. Promise形式


animation.finished.then(() =>
element.remove()
)

比如一个很有用的场景,所有动画完成后:


Promise.all( element.getAnimations().map(ani => ani.finished)
).then(function() {
// do something cool
})

常用事件回调




便捷的 Element.animate


任何 Element都具备该方法, 其语法:



animate(keyframes, options)



其参数和 new KeyframeEffect(target, keyframes, options)的后两个参数基本一样, 返回的是一个Animation对象。


第一个参数 keyframes


keyframes有两种形式,一种是数组形式,一种是对象形式。


数组形式


一组对象(关键帧) ,由要迭代的属性和值组成。

关键帧的偏移可以通过提供一个offset来指定 ,值必须是在 [0.0, 1.0] 这个区间内,且须升序排列。简单理解就是进度的百分比的小数值。


element.animate([ { opacity: 1 },
{ opacity: 0.1, offset: 0.7 },
{ opacity: 0 } ],
2000);

并非所有的关键帧都需要设置offset。 没有指定offset的关键帧将与相邻的关键帧均匀间隔。


对象形式


一个包含key-value键值的对象需要包含动画的属性和要循环变化的值数组


element.animate({
opacity: [ 0, 0.9, 1 ],
offset: [ 0, 0.8 ], // [ 0, 0.8, 1 ] 的简写
easing: [ 'ease-in', 'ease-out' ],
}, 2000);

第二个参数 options


new KeyframeEffect(target, keyframes, options)的第三个参数基本一致,但是多了一个可选属性,就是id,用来标记动画,也方便 在Element.getAnimations结果中精确的查找。





















































后续四个特性相对高级,掌握好了可以玩出花来,本章主要讲基本知识,后续会出高级版本。


更多细节可以参见 KeyframeEffect


Element.getAnimations


我们通过Element.animate或者创建Animation给Element添加很多动画,通过这个方法可以获得所有Animation的实例。


在需要批量修改参数,或者批量停止动画的时候,那可是大杀器。


比如批量暂停动画:


box1ItemEl.getAnimations()
.forEach(el=> el.pause()) // 暂停全部动画

优势



  1. 相对css动画更加灵活

  2. 相对requestAnimation/setTimeout/setInterval 动画,性能更好,代码更简洁

  3. 天然支持Promise,爽爽爽!!!


你有什么理由拒绝她呢?


对比 CSS Animation


动画参数属性键对照表


参数设置值上的区别



  1. duration 参数只支持毫秒

  2. 迭代次数无限使用的是 JS的Infinity,不是字符串 "infinite"

  3. 默认动画的贝塞尔是linear,而不是css的ease


兼容性


整体还不错,Safari偏差。

如果不行, 加个垫片 web-animations-js


我们在实际的桌面项目上已经使用,非常灵活, nice!
image.png


总结


web Animations API 和 css动画,不是谁替换谁。结合使用,效果更佳。


复杂的逻辑动画,因为web Animations API和JS天然的亲和力,是更优的选择。



收起阅读 »

小程序原理 及 优化

小程序使用的是双线程 在这种架构中,视图层使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境 > 两者都是独立的模块,并不具备数据直接共享的通道。视图层和逻辑层的数据传输,要由 Native 的 JS...
继续阅读 »

小程序使用的是双线程



在这种架构中,视图层使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境 >




两者都是独立的模块,并不具备数据直接共享的通道。视图层和逻辑层的数据传输,要由 NativeJSBrigde 做中转



小程序的启动过程



  • 1、小程序初始化: 微信初始化小程序环境:包括 Js 引擎WebView 进行初始化,并注入公共基础库。 这步是微信做的,在用户打开小程序之前就已经准备好了,是小程序运行环境预加载。

  • 2、下载小程序代码包 对小程序业务代码包进行下载:下载的不是小程序的源代码,而是编译、压缩、打包之后的代码。

  • 3、加载小程序代码包 对下载完成对代码包进行注入执行。 此时,app.js、页面所在的 Js 文件和所有其他被require 的 Js 文件会被自动执行一次,小程序基础库会完成所有页面的注册。

  • 4、初始化小程序首页 拉取数据,从逻辑层传递到视图层,进行渲染


setData 的工作原理



  • 1、调用setData方法;

  • 2、逻辑层会执行一次 JSON.stringify 来去除掉 setData 数据中不可传输的部分,将待传输数据转换成字符串并拼接到特定的JS脚本, 并通过 evaluateJavascript 执行脚本将数据传输到渲染层。

  • 3、渲染层接收到后, WebView JS 线程会对脚本进行编译,得到待更新数据后进入渲染队列等待 WebView 线程空闲时进行页面渲染。

  • 4、WebView 线程开始执行渲染时,将 data setData 数据套用在WXML 片段上,得到一个新节点树。经过新虚拟节点树与当前节点树的 diff 对比,将差异部分更新到UI视图。最后,将 setData 数据合并到 data 中,并用新节点树替换旧节点树,用于下一次重渲染


小程序官方性能指标



  • 1、首屏时间不超过 5 秒

  • 2、渲染时间不超过 500ms

  • 3、每秒调用 setData 的次数不超过 20 次

  • 4、setData 的数据在 JSON.stringify 后不超过 256kb

  • 5、页面 WXML 节点少于 1000 个,节点树深度少于 30 层子节点数不大于 60 个

  • 6、所有网络请求都在 1 秒内返回结果;


小程序优化


1、分包并且使用


分包预加载(通过配置 preloadRule) 将访问率低的页面放入子包里,按需加载;启动时需要访问的页面及其依赖的资源文件应放在主包中。 不需要等到用户点击到子包页面后在下载子包,而是可以根据后期数据,做子包预加载,将用户在当先页可能点击的子包页面先加载,当用户点击后直接跳转;可以根据用户网络类型来判断的,当用户处于网络条件好时才预加载;


image.png


2、采用独立分包技术(感觉开普勒黄金流程源码可以独立分包)


主包+子包的方式,,如果要跳到子包里,还是会加载主包然后加载子包;采用独立分包技术,区别于子包,和主包之间是无关的,在功能比较独立的子包里,使用户只需下载分包资源;


3、异步请求可以在页面onLoad就加载


4、注意利用缓存


利用storage API, 对变动频率比较低的异步数据进行缓存,二次启动时,先利用缓存数据进行初始化渲染,然后后台进行异步数据的更新


5、及时反馈


及时对需要用户等待的交互操作进行反馈,避免用户以为小程序卡了 先反馈,再请求。比如说,点赞的按钮,可以先改变按钮的样式,再 发起异步请求。


6、可拆分的部分尽量使用自定义组件


自定义组件的更新并不会影响页面上其他元素的更新,各个组件具有独立的逻辑空间、数据、样式环境及 setData 调用


7、避免不当的使用onPageScroll


避免在onPageScroll 中执行复杂的逻辑,避免在onPageScroll中频繁使用setData,避免在onPageScroll中 频繁查询几点信息(selectQuery


8、减少在代码包中直接嵌入的资源文件;图片放在cdn,使用适当的图片格式


9、setData 优化


(1)与界面渲染无关的数据最好不要设置在 data 中,可以考虑设置在 page 对象的其他字段下;


this.setData({ 
a: '与渲染有关的字符串',
b: '与渲染无关的字符串'
})
// 可以优化为
this.setData({
a: '与渲染有关的字符串'
})
this.b = '与渲染无关的字符串'

(2)不要过于频繁调用 setData,将多次 setData 合并成一次 setData 调用


(3)数据通信的性能与数据量正相关,因而如果有一些数据字段不在界面中展示数据结构比较复杂包含长字符串,则不应使用setData来设置这些数据


(4)列表局部更新 在更新列表的某一个数据时。不要用 setData 进行全部数据的刷新。查找对应 id 的那条数据的下标(index是不会改变的),用 setData 进行局部刷新


this.setData({ 
`list[${index}]` = newList[index]
})

(5)切勿在后台页面进行setData(就是不要再页面跳转后使用setData) 页面跳转后,代码逻辑还在执行,此时多个webview是共享一个js进程;后台的setData操作会抢占前台页面的渲染资源;


10、避免过多的页面节点数


页面初始渲染时,渲染树的构建、计算节点几何信息以及绘制节点到屏幕的时间开销都跟页面节点数量成正相关关系,页面节点数量越多,渲染耗时越长。


每次执行 setData 更新视图,WebView JS 线程都要遍历节点树计算新旧节点数差异部分。当页面节点数量越多,计算的时间开销越大,减少节点树节点数量可以有效降低重渲染的时间开销。


11、事件使用不当


(1)去掉不必要的事件绑定(WXML中的bindcatch),从而减少通信的数据量和次数;
(2)事件绑定时需要传输targetcurrentTargetdataset,因而不要在节点的data前缀属性中放置过大的数据


12、逻辑后移,精简业务逻辑


就比如咱们生成分享图片,再比如领取新人券的时候将是否是新人是否符合风控条件和最终领券封装为一个接口


13、数据预拉取(重要


小程序官方为开发者提供了一个在小程序冷启动时提前拉取第三方接口的能力 developers.weixin.qq.com/miniprogram… 预拉取能够在小程序冷启动的时候通过微信后台提前向第三方服务器拉取业务数据,当代码包加载完时可以更快地渲染页面,减少用户等待时间,从而提升小程序的打开速度


14、跳转时预拉取


可以在发起跳转前(如 wx.navigateTo 调用前),提前请求下一个页面的主接口并存储在全局 Promise 对象中,待下个页面加载完成后从 Promise 对象中读取数据即可


15、非关键渲染数据延迟请求


小程序会发起一个聚合接口请求来获取主体模块的数据,而非主体模块的数据则从另一个接口获取,通过拆分的手段来降低主接口的调用时延,同时减少响应体的数据量,缩减网络传输时间。


16、分屏渲染


在 主体模块 的基础上再度划分出 首屏模块 和 非首屏模块(比如京挑好货的猜你喜欢模块),在所有首屏模块都渲染完成后才会渲染非首屏模块和非主体模块,以此确保首屏内容以最快速度呈现


17、接口聚合,请求合并(主要解决小程序中针对 API 调用次数的限制)


在小程序中针对 API 调用次数的限制: wx.request (HTTP 连接)的最大并发限制是 10 个; wx.connectSocket (WebSocket 连接)的最大并发限制是 5 个;


18、事件总线,替代组件间数据绑定的通信方式


通过事件总线(EventBus),也就是发布/订阅模式,来完成由父向子的数据传递


19、大图裁剪为多块加载


20、长列表优化


(1)不要每次加载更多的时候 都用concat
每获取到新一页数据时,就把它们concatlist上去,这样就会导致每次setData时的list越来越长越来越长,渲染速度也就越来越慢
(2)分批setData,减少一次setData的数量。不要一次性setData list,而是把每一页一批一批地set Data到这个list中去


this.setData({ 
['feedList[' + (page - 1) + ']']: newVal,
})

(3)运用官方的 IntersectionObserver.relativeToViewport 将超出或者没进入可视区的 部分卸载掉(适用于一次加载很多的列表数据,超出了两屏高度所展示的内容)


image.png


this.extData.listItemContainer.relativeToViewport({ top: showNum * windowHeight, bottom: showNum * windowHeight }) 
.observe(`#list-item-${this.data.skeletonId}`, (res) => {
let { intersectionRatio } = res
if (intersectionRatio === 0) {
console.log('【卸载】', this.data.skeletonId, '超过预定范围,从页面卸载')
this.setData({
showSlot: false
})
} else {
console.log('【进入】', this.data.skeletonId, '达到预定范围,渲染进页面')
this.setData({
showSlot: true,
height: res.boundingClientRect.height
})
}
})

21、合理运用函数的防抖与节流,防止出现重复点击及重复请求出现 为避免频繁setData和渲染,做了防抖函数,时间是600ms


作者:甘草倚半夏
链接:https://juejin.cn/post/7023671521075806244

收起阅读 »

Vue 开发规范(下)

提供组件 API 文档 使用 Vue.js 组件的过程中会创建 Vue 组件实例,这个实例是通过自定义属性配置的。为了便于其他开发者使用该组件,对于这些自定义属性即组件API应该在 README.md 文件中进行说明。 为什么?良好的文档可以让开发者比较容易的...
继续阅读 »

提供组件 API 文档


使用 Vue.js 组件的过程中会创建 Vue 组件实例,这个实例是通过自定义属性配置的。为了便于其他开发者使用该组件,对于这些自定义属性即组件API应该在 README.md 文件中进行说明。


为什么?

良好的文档可以让开发者比较容易的对组件有一个整体的认识,而不用去阅读组件的源码,也更方便开发者使用。

组件配置属性即组件的 API,对于组件的用户来说他们更感兴趣的是 API 而不是实现原理。

正式的文档会告诉开发者组件 API 变更以及向后的兼容性情况

README.md 是标准的我们应该首先阅读的文档文件。代码托管网站(GitHub、Bitbucket、Gitlab 等)会默认在仓库中展示该文件作为仓库的介绍。


怎么做?


在模块目录中添加 README.md 文件:


range-slider/
├── range-slider.vue
├── range-slider.less
└── README.md
在 README 文件中说明模块的功能以及使用场景。对于 vue 组件来说,比较有用的描述是组件的自定义属性即 API 的描述介绍。


提供组件 demo


添加 index.html 文件作为组件的 demo 示例,并提供不同配置情况的效果,说明组件是如何使用的。


为什么?

demo 可以说明组件是独立可使用的。

demo 可以让开发者预览组件的功能效果。

demo 可以展示组件各种配置参数下的功能。


对组件文件进行代码校验


代码校验可以保持代码的统一性以及追踪语法错误。.vue 文件可以通过使用 eslint-plugin-html插件来校验代码。你可以通过 vue-cli 来开始你的项目,vue-cli 默认会开启代码校验功能。


为什么?

保证所有的开发者使用同样的编码规范。

更早的感知到语法错误。


怎么做?


为了校验工具能够校验 *.vue文件,你需要将代码编写在

ESLint
ESLint 需要通过 ESLint HTML 插件来抽取组件中的代码。


通过 .eslintrc 文件来配置 ESlint,这样 IDE 可以更好的理解校验配置项:


{
"extends": "eslint:recommended",
"plugins": ["html"],
"env": {
"browser": true
},
"globals": {
"opts": true,
"vue": true
}
}
运行 ESLint


eslint src/**/*.vue


JSHint
JSHint 可以解析 HTML(使用 --extra-ext命令参数)和抽取代码(使用 --extract=auto命令参数)。


通过 .jshintrc 文件来配置 ESlint,这样 IDE 可以更好的理解校验配置项。


{
"browser": true,
"predef": ["opts", "vue"]
}
运行 JSHint


jshint --config modules/.jshintrc --extra-ext=html --extract=auto modules/
注:JSHint 不接受 vue 扩展名的文件,只支持 html。


只在需要时创建组件


为什么?


Vue.js 是一个基于组件的框架。如果你不知道何时创建组件可能会导致以下问题:



  • 如果组件太大, 可能很难重用和维护;

  • 如果组件太小,你的项目就会(因为深层次的嵌套而)被淹没,也更难使组件间通信;


怎么做?



  • 始终记住为你的项目需求构建你的组件,但是你也应该尝试想到它们能够从中脱颖而出(独立于项目之外)。如果它们能够在你项目之外工作,就像一个库那样,就会使得它们更加健壮和一致。

  • 尽可能早地构建你的组件总是更好的,因为这样使得你可以在一个已经存在和稳定的组件上构建你的组件间通信(props & events)。


规则



  • 首先,尽可能早地尝试构建出诸如模态框、提示框、工具条、菜单、头部等这些明显的(通用型)组件。总之,你知道的这些组件以后一定会在当前页面或者是全局范围内需要。

  • 第二,在每一个新的开发项目中,对于一整个页面或者其中的一部分,在进行开发前先尝试思考一下。如果你认为它有一部分应该是一个组件,那么就创建它吧。

  • 最后,如果你不确定,那就不要。避免那些“以后可能会有用”的组件污染你的项目。它们可能会永远的只是(静静地)待在那里,这一点也不聪明。注意,一旦你意识到应该这么做,最好是就把它打破,以避免与项目的其他部分构成兼容性和复杂性。


Vue 组件规范


<!-- iview 等第三方公共组件,推荐大写开头 -->
<Button> from the top</Button>
<Row>
<Col span="24">
</Col>
</Row>


/** * 公共组件 项目内,自己开发的 推荐p开头 * import pLinkpage from 'public/module/linkage' */


<p-linkage v-model="form.pcarea"></p-linkage>


/** * 非公共组件 项目内,自己开发的推荐v开头 * import vSearch from './search' */
<v-search @search="params = $event"></v-search>

自闭合组件


在单文件组件、字符串模板和 JSX 中没有内容的组件应该是自闭合的——但在 DOM 模板里永远不要这样做。


自闭合组件表示它们不仅没有内容,而且刻意没有内容。其不同之处就好像书上的一页白纸对比贴有“本页有意留白”标签的白纸。而且没有了额外的闭合标签,你的代码也更简洁。


不幸的是,HTML 并不支持自闭合的自定义元素——只有官方的“空”元素。所以上述策略仅适用于进入 DOM 之前 Vue 的模板编译器能够触达的地方,然后再产出符合 DOM 规范的 HTML。


// 反例
<!-- 在单文件组件、字符串模板和 JSX 中 -->
<MyComponent></MyComponent>


<!-- 在 DOM 模板中 -->
<my-component/>
// 好例子
<!-- 在单文件组件、字符串模板和 JSX 中 -->
<MyComponent/>


<!-- 在 DOM 模板中 -->
<my-component></my-component>

作者:_Battle
链接:https://juejin.cn/post/7023549490372182052/

收起阅读 »

Vue 开发规范(中)

上一篇:https://www.imgeek.org/article/825358938将 this 赋值给 component 变量 在 Vue.js 组件上下文中,this指向了组件实例。因此当你切换到了不同的上下文时,要确保 this 指向一个可用的 c...
继续阅读 »

上一篇:https://www.imgeek.org/article/825358938

将 this 赋值给 component 变量


在 Vue.js 组件上下文中,this指向了组件实例。因此当你切换到了不同的上下文时,要确保 this 指向一个可用的 component 变量。


换句话说,如果你正在使用 ES6 的话,就不要再编写 var self = this; 这样的代码了,您可以安全地使用 Vue 组件。


为什么?



  • 使用 ES6,就不再需要将 this 保存到一个变量中了。

  • 一般来说,当你使用箭头函数时,会保留 this 的作用域。(译者注:箭头函数没有它自己的 this 值,箭头函数内的 this 值继承自外围作用域。)

  • 如果你没有使用 ES6,当然也就不会使用 箭头函数 啦,那你必须将 “this” 保存到到某个变量中。这是唯一的例外。


怎么做?


<script type="text/javascript"> 
export default { methods: { hello() { return 'hello'; }, printHello() { console.log(this.hello()); }, }, };
</script>
<!-- 避免 -->
<script type="text/javascript">
export default { methods: { hello() { return 'hello'; }, printHello() { const self = this; // 没有必要 console.log(self.hello()); }, }, };
</script>

组件结构化


按照一定的结构组织,使得组件便于理解。


为什么?



  • 导出一个清晰、组织有序的组件,使得代码易于阅读和理解。同时也便于标准化。

  • 按首字母排序 properties、data、computed、watches 和 methods 使得这些对象内的属性便于查找。

  • 合理组织,使得组件易于阅读。(name; extends; props, data 和 computed; components; watch 和 methods; lifecycle methods 等)。

  • 使用 name 属性。借助于 vue devtools 可以让你更方便的测试。

  • 合理的 CSS 结构,如 BEM 或 rscss - 详情?。

  • 使用单文件 .vue 文件格式来组件代码。


怎么做?


组件结构化


<template lang="html">
<div class="Ranger__Wrapper">
<!-- ... -->
</div>
</template>

<script type="text/javascript"> 
export default {
// 不要忘记了 name 属性 name: 'RangeSlider',
// 组合其它组件 extends: {},
// 组件属性、变量 props: { bar: {},
// 按字母顺序 foo: {}, fooBar: {}, },
// 变量 data() {}, computed: {},
// 使用其它组件 components: {},
// 方法 watch: {}, methods: {},
// 生命周期函数 beforeCreate() {}, mounted() {}, };
</script>

<style scoped> .Ranger__Wrapper { /* ... */ } </style>

组件事件命名


Vue.js 提供的处理函数和表达式都是绑定在 ViewModel 上的,组件的每一个事件都应该按照一个好的命名规范来,这样可以避免不少的开发问题,具体可见如下 为什么。


为什么?



  • 开发者可以随意给事件命名,即使是原生事件的名字,这样会带来迷惑性。

  • 过于宽松的事件命名可能与 DOM 模板不兼容。


怎么做?



  • 事件名也使用连字符命名。

  • 一个事件的名字对应组件外的一组意义操作,如:upload-success、upload-error 以及 dropzone-upload-success、dropzone-upload-error (如果需要前缀的话)。

  • 事件命名应该以动词(如 client-api-load) 或是 形容词(如 drive-upload-success)结尾。(出处)


避免 this.$parent


Vue.js 支持组件嵌套,并且子组件可访问父组件的上下文。访问组件之外的上下文违反了基于模块开发的第一原则。因此你应该尽量避免使用 this.$parent。


为什么?



  • 组件必须相互保持独立,Vue 组件也是。如果组件需要访问其父层的上下文就违反了该原则。

  • 如果一个组件需要访问其父组件的上下文,那么该组件将不能在其它上下文中复用。


怎么做?



  • 通过 props 将值传递给子组件。

  • 通过 props 传递回调函数给子组件来达到调用父组件方法的目的。

  • 通过在子组件触发事件来通知父组件。


谨慎使用 this.$refs


Vue.js 支持通过 ref 属性来访问其它组件和 HTML 元素。并通过 this.$refs 可以得到组件或 HTML 元素的上下文。在大多数情况下,通过 this.$refs来访问其它组件的上下文是可以避免的。在使用的的时候你需要注意避免调用了不恰当的组件 API,所以应该尽量避免使用 this.$refs


为什么?



  • 组件必须是保持独立的,如果一个组件的 API 不能够提供所需的功能,那么这个组件在设计、实现上是有问题的。

  • 组件的属性和事件必须足够的给大多数的组件使用。


怎么做?



  • 提供良好的组件 API

  • 总是关注于组件本身的目的。

  • 拒绝定制代码。如果你在一个通用的组件内部编写特定需求的代码,那么代表这个组件的 API 不够通用,或者你可能需要一个新的组件来应对该需求。

  • 检查所有的 props 是否有缺失的,如果有提一个 issue 或是完善这个组件。

  • 检查所有的事件。子组件向父组件通信一般是通过事件来实现的,但是大多数的开发者更多的关注于 props 从忽视了这点。

  • Props向下传递,事件向上传递!。以此为目标升级你的组件,提供良好的 API 和 独立性。

  • 当遇到 propsevents 难以实现的功能时,通过 this.$refs来实现。

  • 当需要操作 DOM 无法通过指令来做的时候可使用 this.$ref 而不是 JQuery、document.getElement*、document.queryElement

  • 基础使用准则是,能不用ParseError: KaTeX parse error: Expected 'EOF', got '就' at position 5: refs就̲尽量不用,如果用,尽量不要通过refs操作状态,可以通过$refs调用methods


<!-- 推荐,并未使用 this.$refs -->
<range :max="max" :min="min" @current-value="currentValue" :step="1"></range>

<!-- 使用 this.$refs 的适用情况-->
<modal ref="basicModal">
<h4>Basic Modal</h4>
<button class="primary" @click="$refs.basicModal.hide()">Close</button>
</modal>
<button @click="$refs.basicModal.open()">Open modal</button>

<!-- Modal component -->
<template>
<div v-show="active">
<!-- ... -->
</div>
</template>

<script> 
export default { // ... data() { return { active: false, }; }, methods: { open() { this.active = true; }, hide() { this.active = false; }, }, // ... };
</script>
<!-- 这里是应该避免的 -->
<!-- 如果可通过 emited 来做则避免通过 this.$refs 直接访问 -->
<template>
<range :max="max" :min="min" ref="range" :step="1"></range>
</template>

<script>
export default { // ... methods: { getRangeCurrentValue() { return this.$refs.range.currentValue; }, }, // ... };
</script>

使用组件名作为样式作用域空间


Vue.js 的组件是自定义元素,这非常适合用来作为样式的根作用域空间。可以将组件名作为 CSS 类的命名空间。


为什么?



  • 给样式加上作用域空间可以避免组件样式影响外部的样式。

  • 保持模块名、目录名、样式根作用域名一样,可以很好的将其关联起来,便于开发者理解。


怎么做?


使用组件名作为样式命名的前缀,可基于 BEM 或 OOCSS 范式。同时给 style 标签加上 scoped 属性。加上 scoped 属性编译后会给组件的 class 自动加上唯一的前缀从而避免样式的冲突。


<style scoped> /* 推荐 */ 
.MyExample { }
.MyExample li { }
.MyExample__item { }
/* 避免 */
.My-Example { }
/* 没有用组件名或模块名限制作用域, 不符合 BEM 规范 */
</style>


作者:_Battle
链接:https://juejin.cn/post/7023548108214648863

收起阅读 »

Vue 开发规范(上)

基于模块开发 始终基于模块的方式来构建你的 app,每一个子模块只做一件事情。 Vue.js 的设计初衷就是帮助开发者更好的开发界面模块。一个模块是应用程序中独立的一个部分。 怎么做? 每一个 Vue 组件(等同于模块)首先必须专注于解决一个单一的问题,独立的...
继续阅读 »

基于模块开发


始终基于模块的方式来构建你的 app,每一个子模块只做一件事情。


Vue.js 的设计初衷就是帮助开发者更好的开发界面模块。一个模块是应用程序中独立的一个部分。


怎么做?


每一个 Vue 组件(等同于模块)首先必须专注于解决一个单一的问题,独立的、可复用的、微小的 和 可测试的。


如果你的组件做了太多的事或是变得臃肿,请将其拆分成更小的组件并保持单一的原则。一般来说,尽量保证每一个文件的代码行数不要超过 100 行。也请保证组件可独立的运行。比较好的做法是增加一个单独的 demo 示例。


Vue 组件命名


组件的命名需遵从以下原则:



  • 有意义的: 不过于具体,也不过于抽象

  • 简短: 2 到 3 个单词

  • 具有可读性: 以便于沟通交流


同时还需要注意:




  • 必须符合自定义元素规范: 使用连字符分隔单词,切勿使用保留字。




  • app- 前缀作为命名空间: 如果非常通用的话可使用一个单词来命名,这样可以方便于其它项目里复用。




为什么?


组件是通过组件名来调用的。所以组件名必须简短、富有含义并且具有可读性。


如何做?


<!-- 推荐 -->
<app-header></app-header>
<user-list></user-list>
<range-slider></range-slider>

<!-- 避免 -->
<btn-group></btn-group> <!-- 虽然简短但是可读性差. 使用 `button-group` 替代 -->
<ui-slider></ui-slider> <!-- ui 前缀太过于宽泛,在这里意义不明确 -->
<slider></slider> <!-- 与自定义元素规范不兼容 -->

组件表达式简单化


Vue.js 的表达式是 100% 的 Javascript 表达式。这使得其功能性很强大,但也带来潜在的复杂性。因此,你应该尽量保持表达式的简单化。


为什么?




  • 复杂的行内表达式难以阅读。




  • 行内表达式是不能够通用的,这可能会导致重复编码的问题。




  • IDE 基本上不能识别行内表达式语法,所以使用行内表达式 IDE 不能提供自动补全和语法校验功能。




怎么做?


如果你发现写了太多复杂并难以阅读的行内表达式,那么可以使用 method 或是 computed 属性来替代其功能。


<!-- 推荐 -->
<template>
<h1>
{{ `${year}-${month}` }}
</h1>
</template>
<script type="text/javascript"> export default { computed: { month() { return this.twoDigits((new Date()).getUTCMonth() + 1); }, year() { return (new Date()).getUTCFullYear(); } }, methods: { twoDigits(num) { return ('0' + num).slice(-2); } }, }; </script>

<!-- 避免 -->
<template>
<h1>
{{ `${(new Date()).getUTCFullYear()}-${('0' + ((new Date()).getUTCMonth()+1)).slice(-2)}` }}
</h1>
</template>

组件 props 原子化


虽然 Vue.js 支持传递复杂的 JavaScript 对象通过 props 属性,但是你应该尽可能的使用原始类型的数据。尽量只使用 JavaScript 原始类型(字符串、数字、布尔值)和函数。尽量避免复杂的对象。


为什么?



  • 使得组件 API 清晰直观。

  • 只使用原始类型和函数作为 props 使得组件的 API 更接近于 HTML(5) 原生元素。

  • 其它开发者更好的理解每一个 prop 的含义、作用。

  • 传递过于复杂的对象使得我们不能够清楚的知道哪些属性或方法被自定义组件使用,这使得代码难以重构和维护。


怎么做?


组件的每一个属性单独使用一个 props,并且使用函数或是原始类型的值。







验证组件的 props


在 Vue.js 中,组件的 props 即 API,一个稳定并可预测的 API 会使得你的组件更容易被其他开发者使用。


组件 props 通过自定义标签的属性来传递。属性的值可以是 Vue.js 字符串(:attr="value" 或 v-bind:attr="value")或是不传。你需要保证组件的 props 能应对不同的情况。


为什么?


验证组件 props 可以保证你的组件永远是可用的(防御性编程)。即使其他开发者并未按照你预想的方法使用时也不会出错。


怎么做?



  • 提供默认值。

  • 使用 type 属性校验类型。

  • 使用 props 之前先检查该 prop 是否存在。


<template>
<input type="range" v-model="value" :max="max" :min="min">
</template>
<script type="text/javascript">
export default {
props: {
max: { type: Number, // 这里添加了数字类型的校验 default() { return 10; }, },
min: { type: Number, default() { return 0; }, },
value: { type: Number, default() { return 4; }, },
},
};
</script>

作者:_Battle
链接:https://juejin.cn/post/7023188232368029710

收起阅读 »

带你理解scoped、>>>、/deep/、::v-deep的原理

前言 平时开发项目我们在使用第三方插件时,必须使用element-ui的某些组件需要修改样式时,老是需要加上/deep/深度选择器,以前只是知道这样用,但是还不清楚他的原理。还有平时每个组件的样式都会加上scoped,但是也不知道他具体的原理。今天我就带大家理...
继续阅读 »

前言


平时开发项目我们在使用第三方插件时,必须使用element-ui的某些组件需要修改样式时,老是需要加上/deep/深度选择器,以前只是知道这样用,但是还不清楚他的原理。还有平时每个组件的样式都会加上scoped,但是也不知道他具体的原理。今天我就带大家理解理解理解


1. Scoped CSS的原理


1.1 区别


先带大家看一下无设置Scoped与设置Scoped的区别在哪


无设置Scoped


<div class="login">登录</div>
<style>
.login {
width: 100px;
height: 100px
}
</style>

打包之后的结果是跟我们的代码一摸一样的,没有区别。


设置Scoped


<div class="login">登录</div>
<style scoped>
.login {
width: 100px;
height: 100px
}
</style>

打包之后的结果是跟我们的代码就有所区别了。如下:


<div data-v-257dda99b class="login">登录</div>
<style scoped>
.login[data-v-257dda99b] {
width: 100px;
height: 100px
}
</style>


我们通过上面的例子,不难发现多了一个data-v-hash属性,也就是说加了scoped,PostCSS给一个组件中的所有dom添加了一个独一无二的动态属性,然后,给CSS选择器额外添加一个对应的属性选择器来选择该组件中dom,这种做法使得样式只作用于含有该属性的dom——组件内部dom,可以使得组件之间的样式不互相污染。



1.2 原理


Vue的作用域样式 Scoped CSS 的实现思路如下:



  1. 为每个组件实例(注意:是组件的实例,不是组件类)生成一个能唯一标识组件实例的标识符,我称它为组件实例标识,简称实例标识,记作 InstanceID;

  2. 给组件模板中的每一个标签对应的Dom元素(组件标签对应的Dom元素是该组件的根元素)添加一个标签属性,格式为 data-v-实例标识,示例:<div data-v-e0f690c0="" >

  3. 给组件的作用域样式 <style scoped> 的每一个选择器的最后一个选择器单元增加一个属性选择器 原选择器[data-v-实例标识] ,示例:假设原选择器为 .cls #id > div,则更改后的选择器为 .cls #id > div[data-v-e0f690c0]


1.3 特点




  1. 将组件的样式的作用范围限制在了组件自身的标签,即:组件内部,包含子组件的根标签,但不包含子组件的除根标签之外的其它标签;所以 组件的css选择器也不能选择到子组件及后代组件的中的元素(子组件的根元素除外);



    因为它给选择器的最后一个选择器单元增加了属性选择器 [data-v-实例标识] ,而该属性选择器只能选中当前组件模板中的标签;而对于子组件,只有根元素 即有 能代表子组件的标签属性 data-v-子实例标识,又有能代表当前组件(父组件)的 签属性 data-v-父实例标识,子组件的其它非根元素,仅有能代表子组件的标签属性 data-v-子实例标识





  2. 如果递归组件有后代选择器,则该选择器会打破特性1中所说的子组件限制,从而选中递归子组件的中元素;



    原因:假设递归组件A的作用域样式中有选择器有后代选择器 div p ,则在每次递归中都会为本次递归创建新的组件实例,同时也会为该实例生成对应的选择器 div p[data-v-当前递归组件实例的实例标识],对于递归组件的除了第一个递归实例之外的所有递归实例来说,虽然 div p[data-v-当前递归组件实例的实例标识] 不会选中子组件实例(递归子组件的实例)中的 p 元素(具体原因已在特性1中讲解),但是它会选中当前组件实例中所有的 p 元素,因为 父组件实例(递归父组件的实例)中有匹配的 div 元素;





2. >>>、/deep/、::v-deep深度选择器的原理


2.1 例子


实际开发中遇到的例子:当我们开发一个页面使用了子组件的时候,如果这时候需要改子组件的样式,但是又不影响其他页面使用这个子组件的样式的时候。比如:


父组件:Parent.vue


<template>
<div class="parent" id="app">
<h1>我是父组件</h1>
<div class="gby">
<p>我是一个段落</p>
</div>

<child></child>
</div>
</template>

<style scoped>
.parent {
background-color: green;
}

.gby p {
background-color: red;
}
// 把子组件的背景变成红色,原组件不变
.child .dyx p {
background-color: red;
}
</style>

子组件:Child.vue


<template>
<div class="child">
<h1>我是子组件</h1>
<div class="dyx">
<p>我是子组件的段落</p>
</div>
</div>
</template>

<style scoped>
.child .dyx p {
background-color: blue;
}
</style>

这时候我们就会发现没有效果。但是如果我们使用>>>/deep/::v-deep三个深度选择器其中一个就能实现了。看代码:


<template>
<div class="parent" id="app">
<h1>我是父组件</h1>
<div class="gby">
<p>我是一个段落</p>
</div>

<child></child>
</div>
</template>

<style scoped>
.parent {
background-color: green;
}

.gby p {
background-color: red;
}
// 把子组件的背景变成红色,原组件不变
::v-deep .child .dyx p {
background-color: red;
}
</style>

2.2 原理


如果你希望 scoped 样式中的一个选择器能够选择到子组 或 后代组件中的元素,我们可以使用 深度作用选择器,它有三种写法:



  • >>>,示例: .gby div >>> #dyx p

  • /deep/,示例: .gby div /deep/ #dyx p.gby div/deep/ #dyx p

  • ::v-deep,示例: .gby div::v-deep #dyx p.gby div::v-deep #dyx p


它的原理与 Scoped CSS 的原理基本一样,只是第3步有些不同(前2步一样),具体如下:



  1. 为每个组件实例(注意:是组件的实例,不是组件类)生成一个能唯一标识组件的标识符,我称它为实例标识,记作 InstanceID;

  2. 给组件模板中的每一个标签对应的Dom元素(组件标签对应的Dom元素是该组件的根元素)添加一个标签属性,格式为 data-v-实例标识,示例:<div data-v-e0f690c0="" >

  3. 给组件的作用域样式 <style scoped> 的每一个深度作用选择器前面的一个选择器单元增加一个属性选择器[data-v-实例标识] ,示例:假设原选择器为 .cls #id >>> div,则更改后的选择器为 .cls #id[data-v-e0f690c0] div


因为Vue不会为深度作用选择器后面的选择器单元增加 属性选择器[data-v-实例标识],所以,后面的选择器单元能够选择到子组件及后代组件中的元素;



收起阅读 »

手摸手教你用webpack搭建TS开发环境

前言 最近在学习typescript,也就是我们常说的TS,它是JS的超集。具体介绍就不多说了,今天主要是带大家用webpack从零搭建一个TS开发环境。直接用传统的tsc xx.ts文件进行编译的话太繁琐,不利于我们开发,经过这次手动配置,我们也能知道vue...
继续阅读 »

前言


最近在学习typescript,也就是我们常说的TS,它是JS的超集。具体介绍就不多说了,今天主要是带大家用webpack从零搭建一个TS开发环境。直接用传统的tsc xx.ts文件进行编译的话太繁琐,不利于我们开发,经过这次手动配置,我们也能知道vue3内部对TS的webpack进行了怎样的配置,废话不多说进入正题。


Node 编译TS


先讲讲如何运行ts文件吧,最传统的方式当然是直接输入命令



tsc xxx.ts



当然你必须得先安装过ts,如果没有请执行以下命令



npm install typescript -g



安装后查看下版本



tsc --version



这样我们就能得到编译后的js文件了,然后我们可以通过node指令



node xxx.js



进行查看,当然也可以新建一个HTML页面引入编译后的js文件


我们从上可以发现有点小复杂,那可不可以直接通过Node直接编译TS呢?接来下就是介绍这种方法

使用ts-node 就可以得到我们想要的效果

安装



npm install ts-node -g



另外ts-node需要依赖 tslib 和 @types/node 两个包,也需要下载



npm install tslib @types/node -g



现在,我们可以直接通过 ts-node 来运行TypeScript的代码



ts-node xxx.ts



如果遇到很多ts文件,那我们用这种方法也会觉得繁琐,所以我们最好是用webpack搭建一个支持TS开发环境,这样才是最好的解决方案。


webpack搭建准备工作


先新建一个文件夹

下载 webpack webpack-cli



npm install webpack webpack-cli -D



下载 ts tsloader(编译ts文件)



npm install typescript ts-loader -D



下载 webpack-dev-server(搭建本地服务器)



npm install webpack-dev-server -D



下载 html模板插件



npm install html-webpack-plugin -D



初始化webpack



npm init



初始化ts



tsc --init



新建配置文件 webpack.config.js


初始化后文件结构如下图所示,当然还有一些测试ts和html需要自己手动创建下
image.png


webpack 配置


配置之前我们先去package.json中添加两个运行和打包指令


image.png


webpack.config.js


代码中有详细说明哦


const path = require('path')//引入内置path方便得到绝对路径
const HtmlWebpackPlugin = require('html-webpack-plugin')//引入模板组件


module.exports = {
mode: 'development',//开发模式
entry: './src/main.ts',//入口文件地址
output: {
path: path.resolve(__dirname, "./dist"),//出口文件,即打包后的文件存放地址
filename: 'bundle.js' //文件名
},
devServer: {

},
resolve: {
extensions:['.ts', '.js', '.cjs', '.json'] //配置文件引入时省略后缀名
},
module: {
rules: [
{
test: /\.ts$/, //匹配规则 以ts结尾的文件
loader: 'ts-loader' //对应文件采用ts-loader进行编译
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './index.html' //使用模板地址
})
]
}

配置完成我们可以进行测试了,执行指令



npm run serve



打包指令



npm run build



End


看完的话点个赞吧~~


QQ图片20200210181218.jpg



收起阅读 »

用 JS 写算法时你应该知道的——数组不能当队列使用!!

在初学 JS 时,发现数组拥有 shift()、unshift()、pop()、push() 这一系列方法,而不像 Java 或 CPP 中分别引用队列、栈等数据结构,还曾偷偷窃喜。现在想想,这都是以高昂的复杂度作为代价的QAQ。 举个例子 - BFS 一般队...
继续阅读 »

在初学 JS 时,发现数组拥有 shift()unshift()pop()push() 这一系列方法,而不像 Java 或 CPP 中分别引用队列、栈等数据结构,还曾偷偷窃喜。现在想想,这都是以高昂的复杂度作为代价的QAQ。


举个例子 - BFS


一般队列的应用是在 BFS 题目中使用到。BFS(Breath First Search)广度优先搜索,作为入门算法,基本原理大家应该都了解,这里不再细说。


LeetCode 1765. 地图中的最高点



给你一个大小为 m x n 的整数矩阵 isWater ,它代表了一个由 陆地 和 水域 单元格组成的地图。


如果 isWater[i][j] == 0 ,格子 (i, j) 是一个 陆地 格子。
如果 isWater[i][j] == 1 ,格子 (i, j) 是一个 水域 格子。
你需要按照如下规则给每个单元格安排高度:



  • 每个格子的高度都必须是非负的。

  • 如果一个格子是是 水域 ,那么它的高度必须为 0 。

  • 任意相邻的格子高度差 至多 为 1 。当两个格子在正东、南、西、北方向上相互紧挨着,就称它们为相邻的格子。(也就是说它们有一条公共边)


找到一种安排高度的方案,使得矩阵中的最高高度值 最大 。


请你返回一个大小为 m x n 的整数矩阵 height ,其中 height[i][j] 是格子 (i, j) 的高度。如果有多种解法,请返回 任意一个 。



常规 BFS 题目,从所有的水域出发进行遍历,找到每个点离水域的最近距离即可。常规写法,三分钟搞定。


/**
* @param {number[][]} isWater
* @return {number[][]}
*/
var highestPeak = function(isWater) {
// 每个水域的高度都必须是0
// 一个格子离最近的水域的距离 就是它的最大高度
let n = isWater.length, m = isWater[0].length;
let height = new Array(n).fill().map(() => new Array(m).fill(-1));
let q = [];
for (let i = 0; i < n; i++) {
for (let j = 0; j < m; j++) {
if (isWater[i][j] === 1) {
q.push([i, j]);
height[i][j] = 0;
}
}
}
let dir = [[0, 1], [0, -1], [1, 0], [-1, 0]];
while (q.length) {
for (let i = q.length - 1; i >= 0; i--) {
let [x, y] = q.shift();
for (let [dx, dy] of dir) {
let nx = x + dx, ny = y + dy;
if (nx < n && nx >= 0 && ny < m && ny >= 0 && height[nx][ny] === -1) {
q.push([nx, ny]);
height[nx][ny] = height[x][y] + 1;
}
}
}
}
return height;
};

然后,超时了……


调整一下,


/**
* @param {number[][]} isWater
* @return {number[][]}
*/
var highestPeak = function(isWater) {
// 每个水域的高度都必须是0
// 一个格子离最近的水域的距离 就是它的最大高度
let n = isWater.length, m = isWater[0].length;
let height = new Array(n).fill().map(() => new Array(m).fill(-1));
let q = [];
for (let i = 0; i < n; i++) {
for (let j = 0; j < m; j++) {
if (isWater[i][j] === 1) {
q.push([i, j]);
height[i][j] = 0;
}
}
}
let dir = [[0, 1], [0, -1], [1, 0], [-1, 0]];
while (q.length) {
let tmp = [];
for (let i = q.length - 1; i >= 0; i--) {
let [x, y] = q[i];
for (let [dx, dy] of dir) {
let nx = x + dx, ny = y + dy;
if (nx < n && nx >= 0 && ny < m && ny >= 0 && height[nx][ny] === -1) {
tmp.push([nx, ny]);
height[nx][ny] = height[x][y] + 1;
}
}
}
q = tmp;
}
return height;
};

ok,这回过了,而且打败了 90% 的用户。


image.png


那么问题出在哪里呢?shift()!!!


探究 JavaScript 中 shift() 的实现


在学习 C++ 的时候,队列作为一个先入先出的数据结构,入队和出队肯定都是O(1)的时间复杂度,用链表


让我们查看下 V8 中 shift() 的源码


简单实现就是


function shift(arr) {
let len = arr.length;
if (len === 0) {
return;
}
let first = arr[0];
for (let i = 0; i < len - 1; i++) {
arr[i] = arr[i + 1];
}
arr.length = len - 1;
return first;
}

所以,shift()O(N) 的!!! 吐血 QAQ


同理,unshift() 也是 O(N) 的,不过,pop()push()O(1),也就是说把数组当做栈是没有问题的。


我就是想用队列怎么办!


没想到作为一个 JSer,想好好地用个队列都这么难……QAQ


找到了一个队列实现,详情见注释。


/*

Queue.js

A function to represent a queue

Created by Kate Morley - http://code.iamkate.com/ - and released under the terms
of the CC0 1.0 Universal legal code:

http://creativecommons.org/publicdomain/zero/1.0/legalcode

*/

/* Creates a new queue. A queue is a first-in-first-out (FIFO) data structure -
* items are added to the end of the queue and removed from the front.
*/
function Queue(){

// initialise the queue and offset
var queue = [];
var offset = 0;

// Returns the length of the queue.
this.getLength = function(){
return (queue.length - offset);
}

// Returns true if the queue is empty, and false otherwise.
this.isEmpty = function(){
return (queue.length == 0);
}

/* Enqueues the specified item. The parameter is:
*
* item - the item to enqueue
*/
this.enqueue = function(item){
queue.push(item);
}

/* Dequeues an item and returns it. If the queue is empty, the value
* 'undefined' is returned.
*/
this.dequeue = function(){

// if the queue is empty, return immediately
if (queue.length == 0) return undefined;

// store the item at the front of the queue
var item = queue[offset];

// increment the offset and remove the free space if necessary
if (++ offset * 2 >= queue.length){
queue = queue.slice(offset);
offset = 0;
}

// return the dequeued item
return item;

}

/* Returns the item at the front of the queue (without dequeuing it). If the
* queue is empty then undefined is returned.
*/
this.peek = function(){
return (queue.length > 0 ? queue[offset] : undefined);
}

}

把最初代码中的数组改为 Queue,现在终于可以通过了。:)



收起阅读 »

如何“优雅”地修改 node_modules 下的代码?

在实际开发过程中当我们遇到 node_modules 中的 A 包有 bug 时候,通常开发者有几个选择: 方法一:给 A 包提 issue 等待他人修复并发布:做好石沉大海或修复周期很长的准备。 方法二:给 A 包提 mr 自行修复并等待发布:很棒,不过你最...
继续阅读 »

在实际开发过程中当我们遇到 node_modules 中的 A 包有 bug 时候,通常开发者有几个选择:


方法一:给 A 包提 issue 等待他人修复并发布:做好石沉大海或修复周期很长的准备。


方法二:给 A 包提 mr 自行修复并等待发布:很棒,不过你最好祈祷作者发版积极,并且新版本向下兼容。


方法三:把 A 包的源码拖出来自己维护:有点暴力且事后维护成本较高,不过应急时也能勉强接受。


等等,可如果出问题的包是“幽灵依赖”呢,比如项目的依赖链是: A -> B -> C,此时 C 包有 bug。那么上面三个方法的改动需要同时影响到 A、B、C 三个包,修复周期可能就更长了,可是你今晚就要上线啊,这可怎么办?


1


上线要紧,直接手动修改 node_modules 下的代码给缺陷包打个临时补丁吧,可问题又来了,改动只能在本地生效,构建却在云端, 积极的同学开始写起了脚本,然后陷入一个个坑里...


上述场景下即可考虑使用 patch-package 这个包,假设我们现在的源码结构如下所示:


├── node_modules  
│ └── lodash
│ └── toString.js
├── src
│ └── app.js // 依赖 lodash 的 toString 方法
└── package.json

node_modules/lodash/toString.js


var baseToString = require('./_baseToString')

function toString(value) {
return value == null ? '' : baseToString(value);
}

module.exports = toString;

src/app.js


const toString = require('lodash/toString')
console.log(toString(123));

假设现在需要修改 node_modules/lodash/toString.js 文件,只需要遵循以下几步即可“优雅”完成修改:


第一步:安装依赖


yarn add patch-package postinstall-postinstall -D

第二步:修改 node_modules/lodash/toString.js 文件


function toString(value) {
console.log('it works!!!'); // 这里插入一行代码
return value == null ? '' : baseToString(value);
}

module.exports = toString;

第三步:生成修改文件


npx patch-package lodash

这一步运行后会生成 patches/lodash+4.17.21.patch,目录结构变成下面这样:


├── node_modules  
│ └── lodash
│ └── toString.js
├── patches
│ └── lodash+4.17.21.patch
├── src
│ └── app.js
└── package.json

其中 .patch 文件内容如下:


diff --git a/node_modules/lodash/toString.js b/node_modules/lodash/toString.js
index daaf681..8308e76 100644
--- a/node_modules/lodash/toString.js
+++ b/node_modules/lodash/toString.js
@@ -22,6 +22,7 @@ var baseToString = require('./_baseToString');
* // => '1,2,3'
*/
function toString(value) {
+ console.log('it works!!!');
return value == null ? '' : baseToString(value);
}

第四步:修改 package.json 文件


"scripts": {
+ "postinstall": "patch-package"
}

最后重装一下依赖,测试最终效果:


rm -rf node_modules
yarn
node ./src/app.js

// it works!!!
// 123

可以看到,即便重装依赖,我们对 node_modules 下代码的修改还是被 patch-package 还原并最终生效。


至此我们便完成一次临时打补丁的操作,不过这并非真正优雅的长久之计,长期看还是需要彻底修复第三方包缺陷并逐步移除项目中的 .patch 文件。


作者:王力国
链接:https://juejin.cn/post/7022252841116893215

收起阅读 »

封装一个底部导航

前言 在我们日常项目开发中,我们在做移动端的时候会涉及到地步导航功能,所以封装了这个底部导航组件。 底部导航 BottomNav组件属性 1. value选中值(即选中BottomNavPane的name值)值为字符串类型非必填默认为第一个BottomNavP...
继续阅读 »

前言


在我们日常项目开发中,我们在做移动端的时候会涉及到地步导航功能,所以封装了这个底部导航组件。

底部导航


BottomNav组件属性


1. value
选中值(即选中BottomNavPane的name值)
值为字符串类型
非必填默认为第一个BottomNavPane的name

2. lazy
未显示的内容面板是否延迟渲染
值为布尔类型
默认为false

样式要求
组件外面需要包裹可以相对定位的元素,增加样式:position: relative

BottomNavPane组件属性


1. name
英文名称
值为字符串类型
必填

2. icon
导航图标名称
值为字符串类型
值需要与src/assets/icon目录下svg文件的名称一致(name值不含“.svg”后缀)
必填

3. label
导航图标下面显示的文字
值为字符串类型
必填

4. scroll
是否有滚动条
值为布尔类型
默认值为:true

示例


<template>
<div class="bottom-nav-wrap">
<BottomNav v-model="curNav" :lazy="true">
<BottomNavPane name="home" label="首页" icon="home">
<h1>首页内容</h1>
</BottomNavPane>
<BottomNavPane name="oa" label="办公" icon="logo">
<h1>办公内容</h1>
</BottomNavPane>
<BottomNavPane name="page2" label="我的" icon="user">
<h1>个人中心</h1>
</BottomNavPane>
</BottomNav>
</div>
</template>

<script>
import { BottomNav, BottomNavPane } from '@/components/m/bottomNav'

export default {
name: 'BottomNavDemo',
components: {
BottomNav,
BottomNavPane
},
data () {
return {
curNav: ''
}
}
}
</script>

<style lang="scss" scoped>
.bottom-nav-wrap {
position: absolute;
top: $app-title-bar-height;
bottom: 0;
left: 0;
right: 0;
}
</style>

BottomNav.vue


<template>
<div class="bottom-nav">
<div class="nav-pane-wrap">
<slot></slot>
</div>
<div class="nav-list">
<div class="nav-item"
v-for="info in navInfos"
:key="info.name"
:class="{active: info.name === curValue}"
@click="handleClickNav(info.name)">
<Icon class="nav-icon" :name="info.icon"></Icon>
<span class="nav-label">{{info.label}}</span>
</div>
</div>
</div>
</template>
<script>
import { generateUUID } from '@/assets/js/utils.js'
export default {
name: 'BottomNav',
props: {
// 选中导航值(导航的英文名)
value: String,
// 未显示的内容面板是否延迟渲染
lazy: {
type: Boolean,
default: false
}
},
data () {
return {
// 组件实例的唯一ID
id: generateUUID(),
// 当前选中的导航值(导航的英文名)
curValue: this.value,
// 导航信息数组
navInfos: [],
// 导航面板vue实例数组
panes: []
}
},
watch: {
value (val) {
this.curValue = val
},
curValue (val) {
this.$eventBus.$emit('CHANGE_NAV' + this.id, val)
this.$emit('cahnge', val)
}
},
mounted () {
this.calcPaneInstances()
},
beforeDestroy () {
this.$eventBus.$off('CHANGE_NAV' + this.id)
},
methods: {
// 计算导航面板实例信息
calcPaneInstances () {
if (this.$slots.default) {
const paneSlots = this.$slots.default.filter(vnode => vnode.tag &&
vnode.componentOptions && vnode.componentOptions.Ctor.options.name === 'BottomNavPane')
const panes = paneSlots.map(({ componentInstance }) => componentInstance)
const navInfos = paneSlots.map(({ componentInstance }) => {
// console.log(componentInstance.name, componentInstance)
return {
name: componentInstance.name,
label: componentInstance.label,
icon: componentInstance.icon
}
})
this.navInfos = navInfos
this.panes = panes
if (!this.curValue) {
if (navInfos.length > 0) {
this.curValue = navInfos[0].name
}
} else {
this.$eventBus.$emit('CHANGE_NAV' + this.id, this.curValue)
}
}
},
// 导航点击事件处理方法
handleClickNav (val) {
this.curValue = val
}
}
}
</script>
<style lang="scss" scoped>
.bottom-nav {
display: flex;
flex-direction: column;
height: 100%;
.nav-pane-wrap {
flex: 1;
}
.nav-list {
flex: none;
display: flex;
height: 90px;
background-color: #FFF;
align-items: center;
border-top: 1px solid $base-border-color;
.nav-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
line-height: 1;
text-align: center;
color: #666;
.nav-icon {
font-size: 40px;/*yes*/
}
.nav-label {
margin-top: 6px;
font-size: 24px;/*yes*/
}
&.active {
position: relative;
color: $base-color;
}
}
}
}
</style>

BottomNavPane.vue


<template>
<div v-if="canInit" class="bottom-nav-pane" v-show="show">
<Scroll v-if="scroll">
<slot></slot>
</Scroll>
<slot v-else></slot>
</div>
</template>
<script>
import Scroll from '@/components/base/scroll'

export default {
name: 'BottomNavPane',
components: {
Scroll
},
props: {
// 页签英文名称
name: {
type: String,
required: true
},
// 页签显示的标签
label: {
type: String,
required: true
},
// 图标名称
icon: {
type: String,
required: true
},
// 是否有滚动条
scroll: {
type: Boolean,
default: true
}
},
data () {
return {
// 是否显示
show: false,
// 是否已经显示过
hasShowed: false
}
},
computed: {
canInit () {
return (!this.$parent.lazy) || (this.$parent.lazy && this.hasShowed)
}
},
created () {
this.$eventBus.$on('CHANGE_NAV' + this.$parent.id, val => {
if (val === this.name) {
this.show = true
this.hasShowed = true
} else {
this.show = false
}
})
}
}
</script>
<style lang="scss" scoped>
.bottom-nav-pane {
height: 100%;
position: relative;
}
</style>

/**
* 底部图标导航组件
*/
import BaseBottomNav from './BottomNav.vue'
import BaseBottomNavPane from './BottomNavPane.vue'
export const BottomNav = BaseBottomNav
export const BottomNavPane = BaseBottomNavPane


「欢迎在评论区讨论」



收起阅读 »

你知道为何跨域中会发送 options 请求?

同源策略 同源策略是一个重要的安全策略,它用于限制一个 origin 的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。 简单说,当我们访问一个网站时,浏览器会对源地址的不同部分(协议://域名:端口)做检查...
继续阅读 »

同源策略



同源策略是一个重要的安全策略,它用于限制一个 origin 的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。



简单说,当我们访问一个网站时,浏览器会对源地址的不同部分(协议://域名:端口)做检查。比如防止利用它源的存储信息(Cookies...)做不安全的用途。


跨域 CORS


但凡被浏览器识别为不同源,浏览器都会认为是跨域,默认是不允许的。


比如:试图在 http://127.0.0.1:4000 中,请求 http://127.0.0.1:3000 的资源会出现如下错误:


跨域错误


这也是前端 100% 在接口调试中会遇到的问题。


同源和跨域的判断规则


简单请求和复杂请求



相信都会在浏览器的 Network 中看到两个同样地址的请求,有没有想过这是为什么呢?这是因为在请求中,会分为 简单请求复杂请求


简单请求:满足如下条件的,将不会触发跨域检查:



  • 请求方法为:GETPOSTHEAD

  • 请求头:AcceptAccept-LanguageContent-LanguageContent-Type


其中 Content-Type 限定为 :text/plain、multipart/form-data、application/x-www-form-urlencoded


我们可以更改同源规则,看下如下示例:



http://127.0.0.1:4000/ 下,请求 http://127.0.0.1:3000 不同端口的地址



简单请求


域名不同,这已经跨域了。但由于请求方法为 GET,符合 简单请求,请求将正常工作。


复杂请求:不满足简单请求的都为复杂请求。在发送请求前,会使用 options 方法发起一个 预检请求(Preflight) 到服务器,以获知服务器是否允许该实际请求。


模拟一个跨域请求:


// 端口不同,content-type 也非限定值
axios.post(
'http://127.0.0.1:3000/test/cors',
{},
{
headers: {
'content-type': 'application/json',
},
}
);

能看到在请求之前浏览器会事先发起一个 Preflight 预检请求


Preflight


这个 预检请求 的请求方法为 options,同时会包含 Access-Control-xxx 的请求头:


options请求信息


当然,此时服务端没有做跨域处理(示例使用 express 起的服务,预检请求默认响应 200),就会出现浏览器 CORS 的错误警告。


跨域错误


如何解决跨域


对于跨域,前端再熟悉不过,百度搜索能找到一堆解决方法,关键词不是 JSONP,或者添加些 Access-Control-XXX 响应头。


本篇将详细说下后一种方式,姑且称为:服务端解决方案。


为 options 添加响应头


express 举例,首先对 OPTIONS 方法的请求添加这些响应头,它将根据告诉浏览器根据这些属性进行跨域限制:


app.use(function (req, res, next) {
if (req.method == 'OPTIONS') {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, PUT, POST, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'content-type');
res.status(200).end();
}
});

如果你不对 预检接口 做正确的设置,那么后续一切都是徒劳。


打个比方:如果 Access-Control-Allow-Methods 只设置了 POST,如果客户端请求方法为 PUT,那么最终会出现跨域异常,并会指出 PUT 没有在预检请求中的 Access-Control-Allow-Methods 出现:


跨域方法错误
所以,以后读懂跨域异常对于正确的添加服务端响应信息非常重要。另外:GET、POST、HEAD 属于简单请求的方法,所以即使不在 Access-Control-Allow-Methods 定义也不碍事(如果不对请指出)


正式的跨域请求


随后对我们代码发出的请求额外添加跨域响应头(这需要和前面的预检接口一致)


if (req.method == 'OPTIONS') {
//...
} else {
// http://127.0.0.1:3000/test/cors
res.setHeader('Access-Control-Allow-Origin', '*');
next();
}

最后能看到我们等请求正常请求到了:


跨域请求


对于跨域请求头的说明


上例出现了我们经常见到的三个:Access-Control-Allow-OriginAccess-Control-Allow-MethodsAccess-Control-Allow-Headers


参考 cors 库,另外还有其他用于预检请求的响应头:



下面将对上面这些头做个说明。


Access-Control-Allow-Origin


预检请求正常请求 告知浏览器被允许的源。支持通配符“*”,但不支持以逗号“,”分割的多源填写方式。


如果尝试些多个域名,则会出现如下错误:



Response to preflight request doesn't pass access control check: The 'Access-Control-Allow-Origin' header contains multiple values 'aaa,bbb', but only one is allowed.



多源错误


另外,也不建议 Access-Control-Allow-Origin 以通配符方式定义,这样会增加安全隐患,最好以请求方的 origin 来赋值。


const origin = req.headers.origin;
res.setHeader('Access-Control-Allow-Origin', origin || '*');
// 因为会随着客户端请求的 Origin 变化,所以标识 Vary,让浏览器不要缓存
res.setHeader('Vary', 'Origin');

Access-Control-Allow-Methods


被允许的 Http 方法,按照需要填写,支持多个,例如: GET , HEAD , PUT , PATCH , POST , DELETE


由于判断 简单请求 之一的 HTTP 方法默认为 GETPOSTHEAD ,所以这些即使不在 Access-Control-Allow-Methods 约定,浏览器也是支持的。


比如:如果服务端定义 PUT 方法,而客户端发送的方法为 DELETE,则会出现如下错误:


res.setHeader('Access-Control-Allow-Methods', 'PUT');


Method DELETE is not allowed by Access-Control-Allow-Methods in preflight response.



方法错误


Access-Control-Allow-Headers


预检接口 告知客户端允许的请求头。


简单请求 约定的请求头默认支持: AcceptAccept-LanguageContent-LanguageContent-Typetext/plain、multipart/form-data、application/x-www-form-urlencoded


如果客户端的请求头不在定义范围内,则会报错:



Request header field abc is not allowed by Access-Control-Allow-Headers in preflight response.



请求头错误


需要将此头调整为:


res.setHeader('Access-Control-Allow-Headers', 'content-type, abc');

Access-Control-Max-Age


定义 预检接口 告知客户端允许的请求头可以缓存多久。


默认时间规则:



  • 在 Firefox 中,上限是 24 小时 (即 86400 秒)。

  • 在 Chromium v76 之前, 上限是 10 分钟(即 600 秒)。

  • 从 Chromium v76 开始,上限是 2 小时(即 7200 秒)。

  • Chromium 同时规定了一个默认值 5 秒。

  • 如果值为 -1,表示禁用缓存,则每次请求前都需要使用 OPTIONS 预检请求。


比如设置为 5 秒后,客户端在第一次会发送 预检接口 后,5 秒内将不再发送 预检接口


res.setHeader('Access-Control-Max-Age', '5');

缓存示例


Access-Control-Allow-Credentials


跨域的请求,默认浏览器不会将当前地址的 Cookies 信息传给服务器,以确保信息的安全性。如果有需要,服务端需要设置 Access-Control-Allow-Credentials 响应头,另外客户端也需要开启 withCredentials 配置。


// 客户端请求
axios.post(
'http://127.0.0.1:3000/test/cors',
{},
{
headers: {
'content-type': 'application/json',
abc: '123',
},
withCredentials: true,
}
);

// 所有请求
res.setHeader('Access-Control-Allow-Credentials', 'true');

需要注意的是,Access-Control-Allow-Origin 不能设置通配符“*”方式,会出现如下错误:


不支持通配符
这个 Access-Control-Allow-Origin 必须是当前页面源的地址。


Access-Control-Expose-Headers


Access-Control-Allow-Credentials 类似,如果服务端有自定义设置的请求头,跨域的客户端请求在响应信息中是接收不到该请求头的。


// 服务端
res.setHeader('def', '123');

axios
.post(
'http://127.0.0.1:3000/test/cors',
{},
{
headers: {
'content-type': 'application/json',
abc: '123',
},
withCredentials: true,
}
)
.then((data) => {
console.log(data.headers.def); //undefined
});

需要在服务端设置 Access-Control-Expose-Headers 响应头,并标记哪些头是客户端能获取到的:


res.setHeader('Access-Control-Expose-Headers', 'def');
res.setHeader('def', '123');

Access-Control-Request-Headers


我试了半天没找到 Access-Control-Request-Headers 的使用示例,其实它是根据当前请求的头拼接得到的。


如果客户端的请求头为:


{
"content-type": "application/json",
"abc": "123",
"xyz": "123",
},

那么浏览器最后会在 预检接口 添加一个 Access-Control-Request-Headers 的头,其值为:abc,content-type,xyz。然后服务端再根据 Access-Control-Allow-Headers 告诉浏览器服务端的请求头支持说明,最后浏览器判断是否会有跨域错误。


另外,对于服务端也需要针对 Access-Control-Request-HeadersVary 处理:


res.setHeader('Vary', 'Origin' + ', ' + req.headers['access-control-request-headers']);

如此,对于跨域及其怎么处理头信息会有个基本的概念。希望在遇到类似问题能有章法的解决,而非胡乱尝试。


作者:Eminoda
链接:https://juejin.cn/post/7021077647417409550

收起阅读 »

移动端常见问题汇总,拿来吧你!

1px适配方案 某些时候,设计人员希望 1px在手机显示的就是1px,这也是....闲的,但是我们也要满足他们的需求, 这时候我们可以利用缩放来达到目的 .border_1px:before{    content: '';  ...
继续阅读 »

1px适配方案


某些时候,设计人员希望 1px在手机显示的就是1px,这也是....闲的,但是我们也要满足他们的需求,


这时候我们可以利用缩放来达到目的


.border_1px:before{
   content: '';
   position: absolute;
   top: 0;
   height: 1px;
   width: 100%;
   background-color: #000;
   transform-origin: 0% 0%;
}
@media only screen and (-webkit-min-device-pixel-ratio:2){
   .border_1px:before{
       transform: scaleY(0.5);
  }
}
@media only screen and (-webkit-min-device-pixel-ratio:3){
   .border_1px:before{
       transform: scaleY(0.33);
  }
}


设置一个专门的class来处理1px的问题,利用伪类给其添加



  • -webkit-min-device-pixel-ratio 获取像素比

  • transform: scaleY(0.5) 垂直方向缩放,后面的数字是倍数


图片模糊问题


.avatar{
   background-image: url(conardLi_1x.png);
}
@media only screen and (-webkit-min-device-pixel-ratio:2){
   .avatar{
       background-image: url(conardLi_2x.png);
  }
}
@media only screen and (-webkit-min-device-pixel-ratio:3){
   .avatar{
       background-image: url(conardLi_3x.png);
  }
}

根据不一样的像素比,准备不一样的图片,正常来说是1px图片像素 对应1px物理像素,图片的显示就不会模糊啦,但是这样的情况不多,不是非常重要,特殊需求的图,我们不这么做。


滚动穿透问题


wt1.gif


移动端的网站,我们是经常会有一些弹出框出现的,这样的弹出框,在上面滑动,会导致我们后面的整个页面发生移动,这个问题怎么解决呢??


body{
   position:fixed;
   width:100%;
}

给body添加position:fixed就可以使滚动条失效,这里弹框的显示和隐藏,我们利用JS进行控制,而且添加上position:fixed的一瞬间,可以看到页面一下回到0,0的位置,因为fixed是根据可视区定位的。


键盘唤起


main{
   padding: 2rem 0;
   /* height: 2000px; */
   position: absolute;
   top: 60px;
   bottom: 60px;
   overflow-y: scroll;
   width: 100%;
   -webkit-overflow-scrolling: touch;
}

当底部根据页面进行fixed定位的时候,键盘弹出一瞬间,fixed会失效,变成类似absoult,让main的内容无滚动,就不会连带fixed一起动了


并且为了保证如丝般顺滑:



  • -webkit-overflow-scrolling: touch;


移动端的神奇操作


IOS下的一些设置 和 安卓下的一些设置


添加到主屏幕后的标题


<meta name="apple-mobile-web-app-title" content="标题"> 

image.png


添加到主屏后的APP图标


<link href="short_cut_114x114.png" rel="apple-touch-icon-precomposed">


  • 一般我们只需要提供一个114*114的图标即可


启用webApp全屏模式


<meta name="apple-mobile-web-app-capable" content="yes" /> 
<meta name="apple-touch-fullscreen" content="yes" />



  • apple-mobile-web-app-capable


    删除默认的苹果工具栏和菜单栏,默认为no




  • apple-touch-fullscreen


    全屏显示




移动端手机号码识别


<meta name="format-detection" content="telephone=no" />


  • safari会对一些可能是手机号码的数字,进行识别,我们可以利用上面的方式,禁止识别


手动开启拨打电话功能


<a href="tel:13300000000">13300000000</a>


  • 在手机上点击这个链接,可以直接拨打电话


手动开启短信功能


<a href="sms:13300000000">13300000000</a>


  • 在手机上点击这个链接,可以跳转去短信页面,给该手机号发送消息


移动端邮箱识别


<meta name="format-detection" content="email=no" /> 

手动开启邮箱发送功能


<a href="mailto:854121000@qq.com">发送邮件</a>


  • 调用邮箱发送功能


优先启用最新版本IE和chrome


<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /> 

移动端默认样式




  • 移动端默认字体



    1. 数字 和 英文字体 可使用Helvetica字体,IOS 和 Android都有这个字体

    2. 手机系统都有自己默认的字体,直接使用默认的


    body{
       font-family:Helvetica;
    }



  • 字体大小


    如果只是适配手机,可以使用px




  • IOS系统中,链接、按钮等点击会有灰色遮罩


    a,button,input,textarea{-webkit-tap-highlight-color: rgba(0,0,0,0)}



  • 去除圆角


    button,input{
       -webkit-appearance:none;
       border-radius: 0;
    }



  • 禁止文本缩放


    html{
        -webkit-text-size-adjust: 100%;
    }
收起阅读 »

你真的了解border-radius吗?

水平半径和垂直半径 现在很多人都不知道我们平常使用的圆角值是一种缩写,例如我们平常写的top圆角10px就是一种缩写: border-top-left-radius:10px; 等同于 border-top-left-radius:10px 10px; 其中...
继续阅读 »

水平半径和垂直半径


现在很多人都不知道我们平常使用的圆角值是一种缩写,例如我们平常写的top圆角10px就是一种缩写:


border-top-left-radius:10px; 等同于 border-top-left-radius:10px 10px; 

其中,第一个值表示水平半径,第二个值表示圆角垂直半径;


例如:


    <style>
.talk-dialog {
position: relative;
background: deepskyblue;
width: 100px;
height: 100px;
margin: 0 auto;
border-top-left-radius: 30px 80px;
}
</style>

 <div class="talk-dialog"></div>

image.png


那么border-radius的写法应该怎么去写呢??它的水平半径和垂直半径是通过 斜杠 区分。 例如:


border-radius: 30px / 40px;


表示四个角的圆角水平半径都是30px,垂直半径是40px;


border-radius斜杠前后都支持1-4个值,以下多个值得写法为:


border-radius:10px 20px / 5% 20% 3% 10%;(左上+右下,右上+左下, / 左上,右上,右下,左下)


重叠问题


难道你认为这就完了,border-radius你彻底搞懂了??其实不然!


我们来看看下面一个列子:


<!DOCTYPE html>
<html lang="en">
<head>

<style>
.talk-dialog {
position: relative;
background: red;
width: 100px;//重点关注
height: 100px;//重点关注
border-radius: 30px / 80px; //重点关注
margin: 50px auto;
}

.talk-dialog1 {
position: relative;
background: deepskyblue;
width: 100px;//重点关注
height: 100px;//重点关注
border-top-left-radius: 30px 80px; //重点关注
margin: 10px auto;
}
</style>

</head>

<body>
<div class="talk-dialog"></div>
<div class="talk-dialog1"></div>
</body>
</html>

我们的容器大小宽为100px,高为100px, 问大家一个问题!


border-radius: 30px / 80px; 与 border-top-left-radius: 30px 80px; 两个不同的容器的 top-left的圆角大小一样吗???


image.png


大家或许这样看不出来,我们修改为绝对布局,两个元素重叠在一起看看是否左上角可以完美重叠?


image.png


答案揭晓: 圆角效果是不一样的,因为我们容器的垂直高度为100px,我们border-radius:30px / 80px设置以后,我们元素的高度不足以放下两个半轴为80px(80+80=160)的椭圆,如果这种场景不做约束,曲线就会发生一定的重叠,因此css 规范对圆角曲线重叠问题做了额外的渲染设定,具体算法如下:


f=min(L宽度/S宽度,L高度/S高度),L为容器宽高,S为半径之和,


这里计算我们的例子:f=min(100/60,100/160)=0.625 , f的值小于1,则所有的圆角值都要乘以f


因此:border-radius: 30px / 80px;


左上角值等同于:


border-top-left-radius:18.75px 50px;


细节



  • border-radius 不支持负值

  • 圆角以外的区域不可点击

  • border-radius没有继承性,因此父元素设置了border-radius,子元素依旧是直角效果,要想达到圆角效果,需要加overflow:hidden。(重要,工作中常用)

  • border-radius 也支持transition过渡效果


高级用法案例:


image.png


代码:


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="utf-8" />
<title></title>
<link rel="icon" href="data:;base64,=" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no" />
<meta name=" theme-color" content="#000000" />
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<style>
.radius {
width: 150px;
height: 150px;
border-radius: 70% 30% 30% 70% / 60% 40% 60% 40%;
object-fit: cover;
object-position: right;
}

.demo {
position: relative;
width: 150px;
height: 150px;
margin: 10px auto;
}

.radius-1 {
width: 150px;
height: 150px;
object-fit: cover;
object-position: right;
background: deepskyblue;
color: #fff;
font-size: 40px;
text-align: center;
line-height: 120px;
border-bottom-right-radius: 100%;
}

.talk {
padding: 10px;
background: deepskyblue;
border-radius: .5em;
color: #fff;
position: relative;
z-index: 0;
}

.talk::before {
content: "";
position: absolute;
width: 15px;
height: 10px;
color: deepskyblue;
border-top: 10px solid;
border-top-left-radius: 80%;
left: 0;
bottom: 0;
margin-left: -12px;
-ms-transform: skewX(-30deg) scaleY(1.3);
transform: skewX(-30deg) scaleY(1.3);
z-index: -1;
}
</style>

</head>

<body>
<div class="demo demo1">
<img class="radius" src="./1.jpg" />
</div>
<div class="demo demo2">
<div class="radius-1">1</div>
</div>
<div class="demo demo3">
<div class="talk">border-radius圆角效果实现。</div>
</div>
</body>

</html>

结语:


欢迎大家多提宝贵意见,一赞一回,如果本文让你get 到知识,请不要吝啬你的star!



收起阅读 »

写给vue转react的同志们(5)

写给vue转react的同志们(4)我们知道 React 中使用高阶组件(下面简称HOC)来复用一些组件的逻辑。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。具体而言,高阶组件是参数为组件,返回值为新组件...
继续阅读 »

写给vue转react的同志们(4)
我们知道 React 中使用高阶组件(下面简称HOC)来复用一些组件的逻辑。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。具体而言,高阶组件是参数为组件,返回值为新组件的函数。


组件是将 props 转换为 UI,而高阶组件是将组件转换为另一个组件。


const EnhancedComponent = higherOrderComponent(WrappedComponent);

上面出自 React 官方文档。


那在 Vue 中 复用组件逻辑实际上比较简单,利用 Mixins 混入复用组件逻辑,当 Mixins 中的逻辑过多时(比如方法和属性),在项目当中使用时追述源代码会比较麻烦,因为他在混入后没有明确告诉你哪个方法是被复用的。


//mixins.js(Vue 2 举例)
export defalut {
data() {
return {
text: 'hello'
}
}
}
// a.vue
import mixins from './mixins.js'
export defalut {
mixins: [mixins]
computed: {
acitveText() {
return `来自mixins的数据:${this.text}`
}
}
}
复制代码

可以看到除了在开头引入并挂载混入,并没有看到this.text是从哪里来的,混入虽然好用,但当逻辑复杂时,其阅读起来是有一定困难的。


那你想在 Vue 中强行使用像 React 那样的高阶组件呢?那当然可以。只是 Vue 官方不怎么推崇 HOC,且 Mixins 本身可以实现 HOC 相关功能。


简单举个例子:


// hoc.js
import Vue from 'Vue'

export default const HOC = (component, text) => {
return Vue.component('HOC', {
render(createElement) {
return createElement(component, {
on: { ...this.$listeners },
props: {
text: this.text
}
})
},
data() {
return {
text: text,
hocText: 'HOC'
}
},
mounted() {
// do something ...
console.log(this.text)
console.log(this.hocText)
}
})
}

使用高阶组件:


// user.vue
<template>
<userInfo/>
</template>

<script>
import HOC from './hoc.js'
// 引入某个组件
import xxx from './xxx'

const userInfo = HOC(xxx, 'hello')

export default {
name: 'user',
components: {
userInfo
}
}
</script>



是不是相比 Mixins 更加复杂一点了?在 Vue 中使用高阶组件所带来的收益相对于 Mixins 并没有质的变化。不过话又说回来,起初 React 也是使用 Mixins 来完成代码复用的,比如为了避免组件的非必要的重复渲染可以在组件中混入 PureRenderMixin


const PureRenderMixin = require('react-addons-pure-render-mixin')
const component = React.createClass({
mixins: [PureRenderMixin]
})


后来 React 使用shallowCompare 来 替代 PureRenderMixin


const shallowCompare = require('react-addons-shallow-compare')
const component = React.createClass({
shouldComponentUpdate: (nextProps, nextState) => {
return shallowCompare(nextProps, nextState)
}
})


这需要你自己在组件中实现 shouldComponentUpdate 方法,只不过这个方法具体的工作由 shallowCompare 帮你完成,我们只需调用即可。


再后来 React 为了避免总是要重复调用这段代码,React.PureComponent 应运而生,总之 React 在慢慢将 Mixins 脱离开来,这对他们的生态系统并不是特别的契合。当然每种方案都各有千秋,只是是否适合自己的框架。


那我们回归 HOC,在 React 中如何封装 HOC 呢?


实际上我在往期篇幅有提到过:
点击传送


但是我还是简单举个例子:


封装 HOC:


// hoc.js
export default const HOC = (WrappedComponent) => {
return Class newComponent extends WrappedComponent {
constructor(props) {
super(props)
// do something ...
this.state = {
text: 'hello'
}
}
componentDidMount() {
super.componentDidMount()
// do something ...
console.log('this.state.text')
}
render() {
// init render
return super.render()
}
}
}



使用 HOC:


// user.js
import HOC from './hoc.js'
class user extends React.Component {
// do something ...
}
export defalut HOC(user)


装饰器写法更为简洁:


import HOC from './hoc.js'
@HOC
class user extends React.Component {
// do something ...
}
export defalut user


可以看到无论 Vue 还是 React 亦或是 HOC 或 Mixins 他们都是为了解决组件逻辑复用应运而生的,具体使用哪一种方案还要看你的项目契合度等其他因素。


技术本身并无好坏,只是会随着时间推移被其他更适合的方案取代,技术迭代也是必然的,相信作为一个优秀的程序员也不会去讨论一个技术的好或坏,只有适合与否。


作者:饼干_
链接:https://juejin.cn/post/7020215941422137381

收起阅读 »

写给vue转react的同志们(4)

下一篇应各位老爷要求,这篇文章开始拥抱hooks,本文将从vue3与react 17.x(hooks)对比来感受两大框架的同工异曲之处。 今天的主题:vue3与react 定义与修改数据vue3与react 计算属性vue3与react 实现监听 vue3与r...
继续阅读 »

下一篇

应各位老爷要求,这篇文章开始拥抱hooks,本文将从vue3react 17.x(hooks)对比来感受两大框架的同工异曲之处。


今天的主题:

vue3与react 定义与修改数据

vue3与react 计算属性

vue3与react 实现监听


vue3与react hooks 定义与修改数据


实际上两者都是偏hooks的写法,这样的高灵活性的组合,相信大部分人还是觉得香的,无论是以前的vue options或是react class的写法都是比较臃肿且复用性较差的(相较于hooks)。下面举个例子对比一下。


vue3





react


import { useState } from 'react';
function App() {
const [todos, setTodos] = useState({
age: 25,
sex: 'man'
})
const setObj = () => {
setTodos({
...todos,
age: todos.age + 1
})
}
return (

{todos.age}


{todos.sex}



);
}



通过比较上述代码可以看到vue3react hooks基本写法是差不多的,只是vue提倡template写法,react提倡jsx写法,模板的写法并不影响你js逻辑的使用,所以不论框架再怎么变化,js也是我们前端的铁饭碗,请各位务必掌握好!


vue3与react 计算属性


计算属性这一块是为了不让我们在模板处写上太过复杂的运算,这是计算属性存在的意义。vue3中提供了computed方法,react hook提供了useMemo让我们实现计算属性(没有类写法中可以使用get来实现计算属性具体可看往期文章)


vue3






react


import { useMemo, useState } from 'react'
function App() {
const [obj, setObj] = useState({
age: 25,
sex: 'man'
})
const people = useMemo(() => {
return `this people age is ${obj.age} and sex is ${obj.sex}`
}, [obj])
return (

age: {obj.age}


sex: {obj.sex}


info: {people}



)
}


可以看到对比两大框架的计算属性,除了模板书写略有不同其他基本神似,都是hooks写法,通过框架内部暴露的某个方法去实现某个操作,这样一来追述和定位错误时也更加方便,hooks大概率就是现代框架的趋势,它不仅让开发者的代码可以更加灵活的组合复用,数据和方法来源也更加容易定位清晰。


vue3与react 实现监听


vue3watch被暴露成一个方法通过传入对应监听的参数以及回调函数实现,react中也有类似的功能useEffect,实际上他和componentDidMountcomponentDidUpdate 和 componentWillUnmount 具有相同的用途,只不过被合并成了一个 API。看例子:


vue3



import { ref, watch } from 'vue'
export default {
setup() {
const count = ref(0)
watch(count,
(val) => {
console.log(val)
},
{ immediate: true, deep: true }
)
function setCount() {
count.value ++
}
return {
count,
setCount
}
}

}

react


import { useState, useEffect } from 'react'
function App() {
const [count, setCount] = useState(0)
const setCount = () => {
setCount(count + 1)
}
useEffect(() => {
console.log(count)
})
return (

count: {count}



)
}

可以看到,vue3整体趋势是往hooks靠,不难看出来未来不论哪种框架大概率最终都会往hooks靠,react hooks无疑是给了我们巨大的启发,函数式编程会越来越普及,从远古时期的传统三大金刚html、css、script就能产出一个页面到现在的组件化,一个js即可是一个页面。


总结


函数式编程是趋势,但其实有很多老项目都是基于vue2.xoptions写法或者react class的写法还是居多,把这些项目迁移迭代到最新才是头疼的事情,当然选择适合现有项目的技术体系才是最重要的。


作者:饼干_
链接:https://juejin.cn/post/6991765115150270478

收起阅读 »

写给vue转react的同志们(3)

下一篇我们都知道vue上手比较容易是因为他的三标签写法以及对指令的封装,他更像一个做好的包子你直接吃。 相比react他的纯js写法,相对来说自由度更高,这也意味着很多东西你需要自己手动封装,所以对新手没那么友好,所以他更像面粉,但可以制作更多花样的食物。 今...
继续阅读 »

下一篇

我们都知道vue上手比较容易是因为他的三标签写法以及对指令的封装,他更像一个做好的包子你直接吃。


相比react他的纯js写法,相对来说自由度更高,这也意味着很多东西你需要自己手动封装,所以对新手没那么友好,所以他更像面粉,但可以制作更多花样的食物。


今天的主题

react 计算属性

react ref


react 计算属性


我们知道vue中有提供computed让我们来实现计算属性,只要依赖改变就会发生变化,那么react中是没有提供的,这里我们需要自己手动实现计算属性。简单举例一下:


vue 计算属性






react 计算属性(类写法)


class App extends React.Component {
constructor(props) {
super(props)
this.state = {
msg: 'hello react'
}
}
get react_computed() {
return this.state.msg
}
componentDidMount() {
setTimeout(() => {
this.setState({
msg: 'hello react change'
})
}, 2000)
}
render() {
return (

{ this.react_computed }

)
}

}


可以看到react中我们手动定义了get来让他获取msg的值,从而实现了计算属性,实际上vue中的computed也是基于get和set实现的,get中收集依赖,在set中派发更新。


react ref


vue中的ref使用起来也是非常简单在对应组件上标记即可获取组件的引用,那么react中呢?
react中当然也可以像vue一样使用,但官方并不推荐字符串的形式来使用ref,并且在react16.x后的版本移除了。


看一段大佬描述:



  • 它要求 React 跟踪当前呈现的组件(因为它无法猜测this)。这让 React 变慢了一点。

  • 它不像大多数人所期望的那样使用“渲染回调”模式(例如),因为 ref 会因为DataGrid上述原因而被放置。

  • 它不是可组合的,即如果一个库在传递的子组件上放置了一个引用,用户不能在它上面放置另一个引用。回调引用是完全可组合的。


举例:


vue ref






react ref


class App extends React.Component {
myRef = React.createRef()
constructor(props) {
super(props)
}
render() {
return (

// 正常使用

// 回调使用(可组合)
this['' + index]} />
// 调用api(react16.x)


)
}

}

vue中的ref我们不必多言,看看react的,官方更推荐第三种用法(react16.x),第二种用法在更新过程中会被执行两次,通过在外部定义箭头函数使用即可,但是大多情况都是无关紧要。第一种用法在react16.x后的版本被废弃了。


总结


都到这篇了,相信你转型react上手业务基本没问题了,后续将慢慢深入两大框架的对比,重点叙述react,vue辅之。


我是饼干,让我们一起成长。最后别忘记点赞关注收藏三连击🌟


作者:饼干_
链接:https://juejin.cn/post/6979061382415122462

收起阅读 »

写给vue转react的同志们(2)

下一篇react中想实现类似vue中的插槽 首先,我个人感觉jsx的写法比模板写法要灵活些,虽然没有像vue那样有指令,这就是为啥vue会上手简单点,因为他就像教科书一样教你怎么使用,而react纯靠你手写表达式来实现。 如果你想实现类似插槽的功能,其实大部分...
继续阅读 »

下一篇

react中想实现类似vue中的插槽


首先,我个人感觉jsx的写法比模板写法要灵活些,虽然没有像vue那样有指令,这就是为啥vue会上手简单点,因为他就像教科书一样教你怎么使用,而react纯靠你手写表达式来实现。


如果你想实现类似插槽的功能,其实大部分UI框架也可以是你自己定义的组件,例如ant desgin的组件,他的某些属性是可以传jsx来实现类似插槽的功能的,比如:


import React from 'react'
import { Popover } from 'antd'

class App extends React.Component {
constructor(props) {
super(props)
this.state = {
content: (

你好,这里是react插槽



)
}
}
render() {
const { content } = this.state
return (


悬浮


)
}
}

上面这样就可以实现类似插槽的功能,这点上确实是比vue灵活些,不需要在结构里在加入特定的插槽占位。
如果是vue的话就可能是这样:




大家可以自己写写demo去体会一下。


单向数据流与双向绑定


我们知道vue中通过发布订阅模式实现了响应式,把inputchange封装成v-model实现双向绑定,react则没有,需要你自己通过this.setState去实现,这点上我还是比较喜欢v-model能省不少事。


虽说单向数据流更清晰,但实际大部分人在开发中出现bug依旧要逐个去寻找某个属性值在哪些地方使用过,尤其是当表单项很多且校验多的时候,代码会比vue多不少,所以大家自行衡量,挑取合适自己或团队的技术栈才是最关键的,不要盲目追求所谓的新技术。


举个例子(简写):


react


import React from 'react'
import { Form, Input, Button } from 'antd'

const FormItem = Form.Item

class App extends React.Component {
constructor(props) {
super(props)
}

onChange(key, e) {
this.setState({
[key] : e
})
}

onClick = () => {
console.log('拿到了:',this.state.username)
}

render() {
return (





vue(简写)





其实乍一看也差不了多少,vue这种options的写法其实会比较清晰一点,react则需要你自己去划分功能区域。


css污染


vue中可以使用scoped来防止样式污染,react没有,需要用到.module.css,原理都是一样的,通过给类名添加一个唯一hash值来标识。


举个例子:


react(简写):


xxx.module.css

.xxx {
background-color: red;
}

xxx.css

.xxx {
background-color: blue;
}

xxx.jsx
import React from 'react'
import style form './xxx.module.css'
import './xxx.css'
class App extends React.Component {
render(){
return (


blue


red


)
}
}


vue


xxx.css
.xxx {
background-color: red;
}

xxx.vue

export default {
methods: {
click() {
this.$router.push('yyy')
}
}
}



yyy.vue

export default {
methods: {
click() {
this.$router.push('xxx')
}
}
}




上面只是简单举个例子,页面之间的样式污染主要是因为css默认是全局生效的,所以无论scoped也好或是module.css也好,都会解析成ast语法树,通过添加hash值生成唯一样式。


总结


无论vue也好,react也好不变的都是js,把js基础打牢才是硬道理。


作者:饼干_
链接:https://juejin.cn/post/6972099403213438984

收起阅读 »

写给vue转react的同志们(1)

学习一个框架最好的办法就是从业务做起。首先我们要弄清做业务需要什么知识点去支持 今天的主题:react 是怎么样传输数据的react 怎么封装组件react 的生命周期 实际上vue熟练的同学们,我觉得转react还是比较好上手的,就是要适应他的纯js的写法以...
继续阅读 »

学习一个框架最好的办法就是从业务做起。首先我们要弄清做业务需要什么知识点去支持


今天的主题:

react 是怎么样传输数据的

react 怎么封装组件

react 的生命周期


实际上vue熟练的同学们,我觉得转react还是比较好上手的,就是要适应他的纯js的写法以及jsx等,个人认为还是比较好接受的,其实基本上都一样,只要弄清楚数据怎么传输怎么处理,那剩下的jsx大家都会写了吧。


react 组件通讯


这里我们来跟vue对比一下。比如
在vue中父子组件传值(简写):


// 父组件
data: {
testText:'这是父值'
}
methods:{
receive(val) {
console.log('这是子值:', val)
}
}
<child :testText="testText" @childCallBack="receive"/>
// 子组件
props: {
testText: {
type: String,
default: ''
}
}
methods:{
handleOn(){
this.$emit('childCallBack', '我是子组件')
}
}
<template>
<div @click="handleOn">{{testText}}</div>
</template>

在react中父子组件传值:


// 父组件
export default class Father extends React.Component {
constructor(props) {
super(props)
this.state = {
testText: '这是父值'
}
receive = (val) => {
console.log('这是子值:', val)
}
render(){
return(
<div>
<Son childCallBack={this.receive} testText={testText}/>
</div>
)
}
}
}
// 子组件
export default class Son extends React.Component {
constructor(props) {
super(props)
}
render() {
const { testText } = this.props
return (
<div>
父组件传过来的testText:{testText}
<div onClick={this.receiveFather}>
点我从子传父
</div>
</div>
)
}
receiveFather = () => {
this.props.childCallBack('我是子组件')
}
}

可以看到react 和 vue 其实相差不大,都是通过props去进行父传子的通讯,然后通过一个事件把子组件的数据传给父组件。聪明的同学肯定注意到react里我用了箭头函数赋给了一个变量了。如果不这样写,this的指向是不确定的,也可以在标签上这样写this.receiveFather.bind(this),不过这样写的坏处就是对性能有影响,可以在constructor中一次绑定即可。但还是推荐箭头函数的写法。(封装组件其实跟这个八九不离十了,就不再叙述)


react 单向数据流


我们都知道vue里直接v-model 然后通过this.属性名就可以访问和修改属性了,这是vue劫持了get和set做了依赖收集和派发更新,但是react里没有这种东西,你不能直接通过this.state.属性名去修改值,需要通过this.setState({"属性名":"属性值"}, callback(回调函数)),你在同一地方修改属性是没办法立刻拿到修改后的属性值,需要通过setState的回调拿到。我还是比较喜欢vue的双向绑定(手动狗头)。


react 的生命周期


我们都知道vue的生命周期(create、mounted、update、destory),其实react也差不多,他们都是要把某个html的div替换并挂载渲染的。
列举比较常用的:


constructor()
constructor()中完成了React数据的初始化,它接受两个参数:props和context,当想在函数内部使用这两个参数时,需使用super()传入这两个参数。这个就当于定义初始数据,熟悉vue的同学你可以把他当成诸如data、methods等。
注意:只要使用了constructor()就必须写super(),否则会导致this指向错误。


componentWillMount()
componentWillMount()一般用的比较少,它更多的是在服务端渲染时使用。它代表的过程是组件已经经历了constructor()初始化数据后,但是还未渲染DOM时。这个相当于vue的created啦,vue中可以通过在这个阶段用$nextTick去操作dom(不建议),不知道react有没有类似的api呢?


componentDidMount()
组件第一次渲染完成,此时dom节点已经生成,可以在这里调用ajax请求,返回数据setState后组件会重新渲染,这个就相当于vue的mounted阶段啦。


componentWillUnmount ()
在此处完成组件的卸载和数据的销毁。这个相当于vue中的beforeDestory啦,clear你在组件中所有的setTimeout,setInterval,移除所有组建中的监听 removeEventListener。


componentWillUpdate (nextProps,nextState)
组件进入重新渲染的流程,这里可以拿到改变后的数据值(相当于vue中updated)。


componentDidUpdate(prevProps,prevState)
组件更新完毕后,react只会在第一次初始化成功会进入componentDidmount,之后每次重新渲染后都会进入这个生命周期,这里可以拿到prevProps和prevState,即更新前的props和state,(相当于vue中的beforeupdated)。


render()
render函数会插入jsx生成的dom结构,react会生成一份虚拟dom树,在每一次组件更新时,在此react会通过其diff算法比较更新前后的新旧DOM树,比较以后,找到最小的有差异的DOM节点,并重新渲染。这里就是你写页面的地方。


总结


小细节

react 中使用组件第一个字母需大写

react 万物皆可 props

mobx 很香🐶

react中没有指令(如v-if、v-for等)需自己写三目运算符或so on~



总结一下,从vue转react还是比较好上手的(react中还有函数式写法我没有说,感兴趣可以看看),个人认为弄清楚数据通讯以及生命周期对应的钩子使用场景等,其实基本就差不多啦。但是还有很多细节需要同学们在实际业务中去发现和解决。react只是框架,大家js基础还是要打好的。祝各位工作顺利,准时发薪。🐶

收起阅读 »

手把手教你利用XSS攻击

前两天我收到安全部门的一个通知:高风险XSS攻击漏洞。 我们部门首先确定风险来源,并给出了解决方案。前端部分由我解决,并紧急修复上线。 一:那么什么是XSS攻击呢? 人们经常将跨站脚本攻击(Cross Site Scripting)缩写为CSS,但...
继续阅读 »

前两天我收到安全部门的一个通知:高风险XSS攻击漏洞。


906501ADEAF08AD26A3F225744EA44BB.jpg





我们部门首先确定风险来源,并给出了解决方案。前端部分由我解决,并紧急修复上线。


5C92478016448CBE2BB5650DAEB40955.jpg



一:那么什么是XSS攻击呢?


人们经常将跨站脚本攻击(Cross Site Scripting)缩写为CSS,但这会与层叠样式表(Cascading Style Sheets,CSS)的缩写混淆。因此,有人将跨站脚本攻击缩写为XSS。恶意攻击者往Web页面里插入恶意html代码,当用户浏览该页之时,嵌入其中Web里面的html代码会被执行,从而达到恶意用户的特殊目的。主要指的自己构造XSS跨站漏洞网页或者寻找非目标机以外的有跨站漏洞的网页。XSS是web安全最为常见的攻击方式,在近年来,常居web安全漏洞榜首。


光看这个定义,很多同学一定不理解是什么意思,下面我会模拟XSS攻击,同学们应该就知道怎么回事了。
在模拟XSS攻击之前,我们先来看看XSS攻击的分类。


二:XSS攻击有几种类型呢?


①反射型XSS攻击(非持久性XSS攻击)

②存储型XSS攻击(持久型XSS攻击)

③DOM-based型XSS攻击


三:接下来我们将模拟这几种XSS攻击


第一种:反射型XSS攻击(非持久性XSS攻击)


反射型XSS攻击一般是攻击者通过特定手法,诱使用户去访问一个包含恶意代码的URL,当受害者点击这些专门设计的链接的时候,恶意代码会直接在受害者主机上的浏览器执行。此类XSS攻击通常出现在网站的搜索栏、用户登录口等地方,常用来窃取客户端Cookies或进行钓鱼欺骗。


下面我们来看一个例子:


image.png


这是一个普通的点击事件,当用户点击之后,就执行了js脚本,弹窗了警告。


image.png


你会说,这能代表啥,那如果这段脚本是这样的呢?


image.png


当浏览器执行这段脚本,就盗用了用户的cookie信息,发送到了自己指定的服务器。你想想他接下来会干什么呢?


第二种:存储型XSS攻击(持久型XSS攻击)


攻击者事先将恶意代码上传或者储存到漏洞服务器中,只要受害者浏览包含此恶意代码的页面就会执行恶意代码。这意味着只要访问了这个页面的访客,都有可能会执行这段恶意脚本,因此存储型XSS攻击的危害会更大。此类攻击一般出现在网站留言、评论、博客日志等交互处,恶意脚本存储到客户端或者服务端的数据库中。


增删改查在web管理系统中中很常见,我们找到一个新增功能页面,这以一个富文本输入框为例,输入以下语句,点击保存,再去查看详情,你觉得会发生什么?


image.png


没错,如果是前端的同学或许已经猜到了,h是浏览器的标签,这样传给服务器,服务器再返回给前端,浏览器渲染的时候,会把第二行当成h1标签来渲染,就会出现以下效果,第二行文字被加粗加大了。


image.png


这里我只是输入了普通的文本,而近几年随着互联网的发展,出现了很多h5多媒体标签,那要是我利用它们呢?
不清楚的同学,可自行打开W3cschool网站查看:


image.png


黑客是怎么攻击我们的呢?黑客会自己写一些脚本,来获取我们的cookies敏感等信息,然后他发送到他自己的服务器,当他拿到我们这些信息后,就能绕过前端,直接调后端的接口,比如提现接口,想想是不是很恐怖!!!


image.png


这里我利用一个在线远程网站来模拟XSS攻击。地址如下:
svg.digi.ninja/xss.svg**
目前网站还能访问,同学们可以自己体验一下,如果后期链接失效不可访问了,同学们可以重新找一个,或者自己手写一个脚本,然后伪装成svg上传到自己的服务器。
我们在地址栏输入上面这个地址,来看看实际效果,提示你已经触发了XSS攻击。
image.png


当我们点击确定,出现了一个黑人,哈哈哈,恭喜你,你银行卡里的钱已经全被黑客取走了。这就是黑客得逞后的样子,他得逞后还在嘲讽你。


image.png


接下来,我们利用多媒体标签和这个脚本来攻击我们实际的的网站。


这里记得在地址前面加上//表示跨越,如图:


image.png
当我们点击保存之后,再去查看详情页面发现。


image.png


哦豁,刚刚那个网站的场景在我们的web管理系统里面触发了,点击确定,那个小黑人又来嘲讽你了。


image.png


这脚本在我们的管理系统成功运行,并获取了我们的敏感信息,就可以直接绕过前端,去直接掉我们后端银行卡提现接口了。并且这类脚本由于保存在服务器中,并存着一些公共区域,网站留言、评论、博客日志等交互处,因此存储型XSS攻击的危害会更大。


第三种:DOM-based型XSS攻击


客户端的脚本程序可以动态地检查和修改页面内容,而不依赖于服务器端的数据。例如客户端如从URL中提取数据并在本地执行,如果用户在客户端输入的数据包含了恶意的JavaScript脚本,而这些脚本没有经过适当的过滤或者消毒,那么应用程序就可能受到DOM-based型XSS攻击。


下面我们来看一个例子


image.png


这段代码的意思是点击提交之后,将输入框中的内容渲染到页面。效果如下面两张图。


①在输入框中输入内容


image.png


②点击确定,输入框中的内容渲染到页面


image.png


那如何我们输内容是不是普通文本,而是恶意的脚本呢?


image.png


没错,恶意的脚本在渲染到页面的时候,没有被当成普通的文本,而是被当成脚本执行了。
image.png


总结:XSS就是利用浏览器不能识别是普通的文本还是恶意代码,那么我们要做的就是阻止恶意代码执行,比如前端的提交和渲染,后端接口的请求和返回都要对此类特殊标签做转义和过滤处理,防止他执行脚本,泄露敏感的数据。感兴趣的同学可以根据我上面的步骤,自己去模拟一个XSS攻击,让自己也体验一次当黑客的感觉。



收起阅读 »

产品经理又开始为难我了???我。。。。

最近做项目的时候,就是产品经理给的图总是很大,不压缩。每天要处理这些图片真的很累哇。于是一怒之下写下了这个**「vscode 插件」。「插件核心功能是压缩,然后上传图片」。 压缩的网站其实就是「tinypng」** 这个网站然后图片压缩后,然后再上传到cdn上...
继续阅读 »

最近做项目的时候,就是产品经理给的图总是很大,不压缩。每天要处理这些图片真的很累哇。于是一怒之下写下了这个**「vscode 插件」「插件核心功能是压缩,然后上传图片」。 压缩的网站其实就是「tinypng」** 这个网站然后图片压缩后,然后再上传到cdn上,然后然后这个压缩过的url 直接放到我们的粘贴板上。下面跟着我的步伐一步一步来写实现它。 先看效果图:


演示gif 图


演示gif 图


效率对比


开发这个主要是提高团队开发效率, 绝不是为了炫技。 看图:


image-20211017224316386


image-20211017224316386


需求分析



  1. 可在vscodde的setting中配置上传所需的参数,可以根据个人的需求单独进行配置;

  2. 2.在开发过程中可在编辑器中直接选择图片并上传到阿里云将图片链接填写到光标位置;


中文文档




一个好的文档可以帮助我们更容易的开发:如果英文比较好的同学可以直接看Vscode英文文档,这里api会比较全,可以找到更简洁的方案实现功能; 不过我的话,还是花很久时间找了这篇比较全的中文文档




搭建项目


vscode 插件的开发需要全局安装脚手架:


 npm install -g yo generator-code

安装成功后,直接使用对应命令 「yo code」 来生成一个插件工程:


vscode开始这个页面


vscode开始这个页面


这就开始脚手架页面了,可以选择自己习惯的配置。输入对应的配置 然后 就创建了对应的项目了。


我们看下项目结构:


插件结构


插件结构


插件运行


这时候我们先要去测试下我的这个插件到底是不是能够成功运行。在项目根目录按住F5 然后运行 「vscode extension」 ,这时候会出现一个新的vscode 窗口,但是我这里遇到的一个问题就是这个:


插件


插件


我大概理解了下就是vscode 插件的依赖版本比较低:


目前是:


插件


插件


这上面说的很清楚 vscode扩展指定 与其兼容的 vscode 版本兼容 很显然我这里太高了, 给他降级。然后给他换成1.60.2 完美解决


插件运行——成功演示


ok, 怎么查看自己查看插件有没有成功运行呢, 分为3步



  1. F5 开始调试 —— 产生一个新的调试窗口

  2. 在新的窗口—— command + shift + P 找到 hello word

  3. 点击运行看见弹窗 显示 表示弹窗运行成功


直接看下面的gif 图吧:


gif 演示


gif 演示


插件开发——配置参数


配置插件的属性面板, 这个主要是要在package.json 配置一些参数


配置参数


配置参数


第一个参数我们稍后再讲其实就是对应你注册的自定义command, 下面的配置 其实就是对应插件属性面板一些参数,然后你可以通过vscode 的一些api 可以获得你配置的这些参数


下面我是我配置的参数,你可以会根据插件自定义去调整


"properties": {
    "upload_image.domain": {
      "type": "string",
      "default": "",
      "description": "设置上传域名"
    },
    "upload_image.accessKey": {
      "type": "string",
      "default": "",
      "description": "设置oss上传accessKey"
    },
    "upload_image.secretKey": {
      "type": "string",
      "default": "",
      "description": "设置oss上传secretKey"
    },
    "upload_image.scope": {
      "type": "string",
      "default": "",
      "description": "设置oss上传上传空间"
    },
    "upload_image.gzip": {
      "type": "boolean",
      "default": "true",
      "description": "是否启用图片压缩"
    }
  }

大概就是这几个参数, 然后我们测试下同样打开f5 然后在新窗口 找到设置然后找到扩展, 设置项其实就是对应我们的 上面的**「title」**


压缩图片。


我们看下效果:


效果


效果


插件开发——配置右键菜单


这个功能描述大概就是,你在写的时候突然要上传,直接点击鼠标右键,然后直接选择图片。 对就是这个简单的东西,做东西需要从用户的角度考虑,一定要爽,能省一步是一步。呵呵哈哈哈


这个配置其实就是在 还是在刚才的**「package.json」** 上继续配置:


"menus": {
    "editor/context": [
      {
        "when": "editorFocus",
        "command": "extension.choosedImage",
        "group": "navigation"
      }
    ]
  }

when:就是你鼠标在编辑的时候


command: 就是自定义的事件,我叫他选择图片, 这个其实就是在extension.js 注册的事件名字 tips: 就是对应的事件名称


let texteditor = vscode.commands.registerTextEditorCommand(
  'extension.choosedImage', ... )

这个其实就是在extension .js 注册对应的事件名,这里的**「事件名」** 一定要和 「package.json」 中文件对应不然会出不来的。 给大家演示下:


图片


图片


重启插件 按下f5 然后按下右键就有我们自定义的右键菜单了。但是问题来了我们按住右键 是不是得弹出一个选择图片的框哇,不然怎么上传对吧?


打开图片上传 弹框


强大的vscode支持了内置的api, 支持打开:


const uri = await vscode.window.showOpenDialog({
    canSelectFolders: false,
    canSelectMany: false,
    filters: {
      images: ['png', 'jpg','apng','jpeg','gif','webp'],
    },
  }); 

就是这个 api, 你可以过滤出想要的图片, 在filters 里面,然后呢 吐出给我们的是对应图片的路径。


我们看下效果:图片选择


读取图片数据


其实这个时候我们我们已经有了图片的路径,这时候就要利用 **「node.js」**的fs 模块 去读取 这个图片的数据 buffer ,这个其实为了方便我们将图片上传到oss 上。 代码如下:


const uri = await vscode.window.showOpenDialog({
    canSelectFolders: false,
    canSelectMany: false,
    filters: {
      images: ['png', 'jpg','apng','jpeg','gif','webp'],
    },
  }); 
let imgBuffer =  await fs.readFile(uri[0].path);

这里还涉及到一个就是说: 本地图片的名字 进行加密, 不能上传到oss 各种中文啥的, 显示的我们很不专业哇


所以这里写了一个MD5的转换


function md5Name(name) {
 const index = name.lastIndexOf('.')
 const sourceFileName = name.substring(0, index)
 const suffix = name.substring(index)
 const fileName = md5(sourceFileName + Date.now()) + suffix
 return fileName.toLowerCase()
}

就是将名字搞成花里胡哨的样子,呵呵哈哈哈!


图片压缩


我们得到图片的buffer 数据后其实要对图片要支持压缩, 其实社区里面有很多方案, 这里的话我调研的很多还是决定使用tinfiy, 他也有对应的**「node.js」** 使用的他主要理由主要是看下面这张图:


apng


apng


对的这家伙支持**「apng」, 其他的不是很清楚。 但是他不是免费的一个人一个月免费「500」** 次, 思考了下还行,我们也用不到辣么多次最终还是考虑用它去实现。


安装


安装npm包并添加到您应用的依赖中,您就可以使用Node.js客户端:


npm install --save tinify

认证


您必须提供您的API密钥来使用API。您可以通过注册您的姓名和Email地址来获取API密钥。 请秘密保存API密钥。


const tinify = require("tinify");
tinify.key = "YOUR_API_KEY";

这个的话其实就是你的邮箱去注册一下,然后把你对应的**「key」** 去激活其实就可以了


如图


如图


其实就是下面这个你的key 设置激活就好了


tinify压缩图片


您可以上传任何JPEG或PNG图片到Tinify API来进行压缩。我们将自动检测图片类型并相应的使用TinyPNG或TinyJPG引擎进行优化。 只要上传文件或提供图片URL,就会开始压缩。


您可以选择一个本地文件作为源并写入到另一个文件中。


const source = tinify.fromFile("unoptimized.webp");
source.toFile("optimized.webp");

您还可以从缓冲区(buffer)(二进制字符串)上传图片并获取压缩后图片的数据。


const fs = require("fs");
fs.readFile("unoptimized.jpg", function(err, sourceData) {
  if (err) throw err;
  tinify.fromBuffer(sourceData).toBuffer(function(err, resultData) {
    if (err) throw err;
    // ...
  });
});


代码实现


function compressBuffer(sourceData, key = 'xxx') {
 return new Promise((resolve,reject) => {
  tinify.key = key;
  tinify.fromBuffer(sourceData).toBuffer(function(err, resultData) {
   if(resultData) {
    resolve(resultData)
   }
   if (err) {
    reject(err);
   }
   // ...
  });
 })
}

基于他这个封装了一个promise, 这个**「fromBuffer」** , 到 「toBuffer」 是真的好用。 哈哈哈哈很香,记得一定要设置key 不然promise 直接会报错的, 设置key的方法 就在上面👆🏻, 然后这样其实我们就获得了压缩的图片数据了。


上传图片到oss


这里的话其实有的使用七牛云、 有的使用阿里云。去上传图片,或者是ajax 去上传其实都可以


一般都是要获取token 啥的以及各种签名信息,然后直接上传就好了, 然后呢你就可以获得一张图片地址了。代码我就不展示了, 都是前端应该都懂。这里我说下我遇到的一些问题



  1. 第一个就是js 跑的 是node js 的环境, 如果使用**「FormData」** 这个类的话 他直接会报找不到, 这个方法是 undefined, 还有**「fetch」**, 所以说要去安装对应node js 包 ,我这里使用的是 「cross-fetch」「form-data」


这里我说一下配置的问题就是你在扩展中如何获得的你配置的参数:


"configuration": [
   {
    "title": "压缩图片",
    "properties": {
     "upload_image.secretKey": {
      "type": "string",
      "default": "",
      "description": "设置tinify的ApIKey"
     },
     "upload_image.secretTokenUrl": {
      "type": "string",
      "default": "",
      "description": "设置得物的tokenUrl"
     }
    }
   }
  ]

每个属性前面对应的 upload_image 其实你在扩展中你可以通过:


const imageConfig =  vscode.workspace.getConfiguration('upload_image')

然后你就可以拿到配置了,upload_image 后面的属性 其实对应的就是对象中的key 然后呢你就可以对吧操作了


这个东西还是具体项目, 具体分析,你们自己 可以针对自己的项目去配置


插件开发——图片链接写入编辑器中


通过上面的方法已经可以获得图片上传后的链接,接下来就是将链接写入编辑器中: 首先判断编辑器选择位置,editor.selection中可以获得光标位置、光标选择首尾位置。若光标有选中内容则editBuilder.replace替换选中内容,否则editBuilder.insert在光标位置插入图片链接:


// 将图片链接写入编辑器
function addImageUrlToEditor(url) {
 let editor = vscode.window.activeTextEditor
 if (!editor) {
   return
 }
 const { start, end, active } = editor.selection
 if (start.line === end.line && start.character === end.character) {
   // 在光标位置插入内容
   const activePosition = active
   editor.edit((editBuilder) => {
  editBuilder.insert(activePosition, url)
   })
 } else {
   // 替换内容
   const selection = editor.selection
   editor.edit((editBuilder) => {
  editBuilder.replace(selection, url)
   })
 }
}

插件发布


到这里,其实一整个vscode插件 其实已经可以开发完成了, 然后我们要把他进行打包发布到vscode 的应用市场


创建账号


我是直接github 登录创建, 首先我们进入文档中提到的主页,完成验证登录后创建一个组织。


创建一个组织


创建一个组织


创建发布者


进入下面这个页面 marketplace.visualstudio.com/manage/publ…** 插件发布者, 给大家看下我的:


发布者


发布者


打包发布


首先全局 安装脚手架


npm install -g vsce

然后 cd 到当前插件目录 使用下面命令


$ cd myExtension
$ vsce package
# myExtension.vsix generated

这里的打包会报一些error:


第一个就是插件的package.json 增加发布者


"publisher": "Fly",

如果给插件加图标: 其实在项目中创建一个文件夹: image 然后把图片放进去: 同时也要在package.json 中配置


"icon": "images/dewu.jpeg",

这里可能有⚠️,不过没什么关系,继续跑就完事了


warn


warn


最后的话其实就是要写readme ,不然 不让你发布。


打包上传


一切准备就绪: 命令行 输入


vsce package 

然后项目中就会出现:


照片


照片


然后可以把这个东西拖到页面这个页面


marketplace.visualstudio.com/manage/publ…


上传


上传


然后点击上传就好了,你就可以在vscode 插件商场可以看到自己写的插件了


插件


作者:Fly
链接:https://juejin.cn/post/7020052159999770632

收起阅读 »

TypeScript 想更深入一层?我推荐自定义 transformer 的 compiler api

现在 JS 的很多库都用 typescript 写了,面试也几乎必问 typescript,可能你对 ts 的各种语法和内置高级类型都挺熟悉了,对 ts 的配置、命令行的使用也没啥问题,但总感觉对 ts 的理解没那么深,苦于没有很好的继续提升的方式。这时候我推...
继续阅读 »

现在 JS 的很多库都用 typescript 写了,面试也几乎必问 typescript,可能你对 ts 的各种语法和内置高级类型都挺熟悉了,对 ts 的配置、命令行的使用也没啥问题,但总感觉对 ts 的理解没那么深,苦于没有很好的继续提升的方式。这时候我推荐你研究下 typescript compiler api


typescript 会把 ts 源码 parse 成 AST,然后对 AST 进行各种转换,之后生成 js 代码,在这个过程中会对 AST 进行类型检查。typescript 把这整个流程封装到了 tsc 的命令行工具里,平时我们一般也是通过 tsc 来编译 ts 代码和进行类型检查的。


但其实 ts 除了提供 tsc 的命令行工具外,也暴露了很多 api,同时也能自定义 transformer。这就像 babel 可以编译 esnext、ts 语法到 js,可以写 babel 插件来转换代码,也暴露了各种 api 一样。只不过 typescript transformer 的生态远远比不上 babel 插件,知道的人也比较少。


其实 typescript transformer 能做到一些 babel 插件做不到的事情:

babel 是从 ts、exnext 等转 js,生成的 js 代码里会丢失类型信息,不能生成 ts 代码。

babel 只是转换 ts 代码,并不会进行类型检查。


这两个 babel 插件做不到的事情,通过 typescript transformer 都可以做到。


而且,学会 typescript compiler 的 api 能够帮助你深入 typescript 的编译流程,更好的掌握 typescript。


说了这么多,我们通过一个例子来入门下 typescript transformer 吧。


案例描述


这样一段 ts 代码:


type IsString<T> = T extends string ? 'Yes' : 'No';

type res = IsString<true>;
type res2 = IsString<'aaa'>;

我们希望能把 res 和 res2 的类型的值算出来,通过注释加在后面。


像这样:


type IsString<T> = T extends string ? 'Yes' : 'No';

type res = IsString<true> //No;
type res2 = IsString<'aaa'> //Yes;

这个案例既用到了 transformer api,又用到了类型检查的 api。


下面我们来分析下思路:


思路分析


我们首先要把 ts 代码 parse 成 AST,然后通过 AST 找到要转换的节点,这里是 TypeReference 节点。


可以用 astexplorer.net 看一下:



IsString 是一个 TypeReference,也就是引用了别的类型,然后有 typeName 是 IsString 和类型参数 typeArguments,这里的类型参数就是 true。


是不是很像一个函数调用,这就是高级类型的本质,通过把类型参数传到引用的高级类型里求出最终的类型。


然后我们找到 TypeReference 的节点之后就可以通过 type checker 的 api 来求出类型值,之后创建一个注释节点添加到后面就行了。


转换完 AST,再把它打印成 ts 代码字符串。


思路就是这样,接下来我们具体来实现下,也熟悉下 ts 的 api。


代码实现


parse 代码成 AST 需要先指定要编译的文件和编译参数(createProgram 的 api),然后就可以拿到不同文件的 AST 了(getSourceFile 的 api)。


const ts = require("typescript");

const filename = "./input.ts";
const program = ts.createProgram([filename], {}); // 第二个参数是 compiler options,就是配置文件里的那些

const sourceFile = program.getSourceFile(filename);

这里的 sourceFile 就是 AST 的根结点。


接下来我们要对 AST 进行转换,使用 transform 的 api:


const  { transformed } = ts.transform(sourceFile, [
function (context) {
return function (node) {
return ts.visitNode(node, visit);

function visit(node) {
if (ts.isTypeReferenceNode(node)) {
// ...
}
return ts.visitEachChild(node, visit, context)
}
};
}
]);

transform 要传入遍历的 AST 以及 transfomerFactory。

AST 就是上面 parse 出的 sourceFile。

transformerFactory 可以拿到 context 中的很多 api 来用,它的返回值就是转换函数 transformer。


transformer 参数是 node,返回值是修改后的 node。


要修改 node 就要遍历 node,使用 visit api 和 vistEachChild 的 api,过程中根据类型过滤出 TypeReference 的节点。


之后对 TypeReference 节点做如下转换:


if (ts.isTypeReferenceNode(node)) {
const type = typeChecker.getTypeFromTypeNode(node);

if (type.value){
ts.addSyntheticTrailingComment(node, ts.SyntaxKind.SingleLineCommentTrivia, type.value);
}
}

也就是通过 typeCheker 来拿到 IsString 这个类型的最终类型值,然后通过 addSyntheticTrailingComment 的 api 在后面加一个注释。


其中用到的 typeChecker 是通过 getTypeChecker 的 api 拿到的:


const typeChecker = program.getTypeChecker();

这样就完成了我们的转换 ts AST 的目的。


然后通过 printer 把 AST 打印成 ts 代码。


const printer =ts.createPrinter();

const code = printer.printNode(false, transformed[0], transformed[0]);

console.log(code);

这样就可以了,我们来测试下。


测试之前,全部代码放这里了:


const ts = require("typescript");

const filename = "./input.ts";
const program = ts.createProgram([filename], {}); // 第二个参数是 compiler options,就是配置文件里的那些

const sourceFile = program.getSourceFile(filename);

const typeChecker = program.getTypeChecker();

const { transformed } = ts.transform(sourceFile, [
function (context) {
return function (node) {
return ts.visitNode(node, visit);
function visit(node) {
if (ts.isTypeReferenceNode(node)) {
const type = typeChecker.getTypeFromTypeNode(node);

if (type.value){
ts.addSyntheticTrailingComment(node, ts.SyntaxKind.SingleLineCommentTrivia, type.value);
}
}
return ts.visitEachChild(node, visit, context)
}
};
}
]);

const printer =ts.createPrinter();

const code = printer.printNode(false, transformed[0], transformed[0]);

console.log(code);

测试效果


经测试,我们达到了求出类型添加到后面的注释里的目的



复盘


激不激动,这是我们第一个 ts transformer 的例子,虽然功能比较简单,但是我们也学会了如何对 ts 代码做 parse、 transform,print,以及 type check。


其实 babel 也有 parse、transform、generate 这 3 步,但没有 type check 的过程,也不能打印成 ts 代码。


用 compiler api 的过程中你会发现原来高级类型就是一个 typeReference,需要传入 typeArguments 来求值的,从而对高级类型的理解更深了。


总结


对 typescript 语法和配置比较熟悉后,想更进一步的话,可以学习下 compiler 的 api 来深入 ts 的编译流程。它包括 transfomer、type checker 等 api,可以达到像 babel 插件一样的转换 ts 代码的目的,而且还能做类型检查。


我们通过一个例子来熟悉了下 typescript 的编译流程和 transformer 的写法。


当你需要修改 ts 代码然后生成 ts 代码的时候,babel 是做不到的,它只能生成 js 代码,这时候可以考虑下 typescript 的自定义 transformer。


而且用 typescript compiler api 能够加深你对 ts 编译流程和类型检查的理解。


ts compiler api 尤其是其中的自定义 transformer 是 typescript 更进一层的不错的方向。



收起阅读 »

JavaScript之彻底理解EventLoop

在正式学习Event Loop之前,先需要解决几个问题:什么是同步与异步?JavaScript是一门单线程语言,那如何实现异步?同步任务和异步任务的执行顺序如何?异步任务是否存在优先级? 同步与异步 计算机领域中的同步与异步和我们现实社会的同步和异步正好相反。...
继续阅读 »

在正式学习Event Loop之前,先需要解决几个问题:

什么是同步与异步?

JavaScript是一门单线程语言,那如何实现异步?

同步任务和异步任务的执行顺序如何?

异步任务是否存在优先级?


同步与异步


计算机领域中的同步与异步和我们现实社会的同步和异步正好相反。现实中的同步,就是同时进行,突出的是"同",比如看足球比赛的时候吃着零食,两件事情同时发生;异步就是不同时。但计算机中与现实存在一定差异。


举个栗子


天气冷了,早上刚醒来想喝点热水暖暖身子,但这每天起早贪黑996,晚上回来太累躺下就睡,没开水啊,没法子,只好急急忙忙去烧水。


现在早上太冷了啊,不由得在被窝里面多躺了一会,收拾的时间紧紧巴巴,不能空等水开,于是我便趁此去洗漱,收拾自己。
洗漱完,水开了,喝到暖暖的热水,舒服啊!


舒服完,开启新的996之日,打工人出发!


烧水和洗漱是在同时间进行的,这就是计算机中的异步


计算机中的同步是连续性的动作,上一步未完成前,下一步会发生堵塞,直至上一步完成后,下一步才可以继续执行。例如:只有等水开,才能喝到暖暖的热水。


单线程却可以异步?


JavaScript的确是一门单线程语言,但是浏览器UI是多线程的,异步任务借助浏览器的线程和JavaScript的执行机制实现。
例如,setTimeout就借助浏览器定时器触发线程的计时功能来实现。


浏览器线程



  1. GUI渲染线程

    • 绘制页面,解析HTML、CSS,构建DOM树等

    • 页面的重绘和重排

    • 与JS引擎互斥(JS引擎阻塞页面刷新)



  2. JS引擎线程

    • js脚本代码执行

    • 负责执行准备好的事件,例如定时器计时结束或异步请求成功且正确返回

    • 与GUI渲染线程互斥



  3. 事件触发线程

    • 当对应的事件满足触发条件,将事件添加到js的任务队列末尾

    • 多个事件加入任务队列需要排队等待



  4. 定时器触发线程

    • 负责执行异步的定时器类事件:setTimeout、setInterval等

    • 浏览器定时计时由该线程完成,计时完毕后将事件添加至任务队列队尾



  5. HTTP请求线程

    • 负责异步请求

    • 当监听到异步请求状态变更时,如果存在回调函数,该线程会将回调函数加入到任务队列队尾




同步与异步执行顺序



  1. JavaScript将任务分为同步任务和异步任务,同步任务进入主线中中,异步任务首先到Event Table进行回调函数注册。

  2. 当异步任务的触发条件满足,将回调函数从Event Table压入Event Queue中。

  3. 主线程里面的同步任务执行完毕,系统会去Event Queue中读取异步的回调函数。

  4. 只要主线程空了,就会去Event Queue读取回调函数,这个过程被称为Event Loop


举个栗子




  • setTimeout(cb, 1000),当1000ms后,就将cb压入Event Queue。

  • ajax(请求条件, cb),当http请求发送成功后,cb压入Event Queue。



EventLoop执行流程


Event Loop执行的流程如下:
在这里插入图片描述


下面一起来看一个例子,熟悉一下上述流程。


// 下面代码的打印结果?
// 同步任务 打印 first
console.log("first");
setTimeout(() => {
// 异步任务 压入Event Table 4ms之后cb压入Event Queue
console.log("second");
},0)
// 同步任务 打印last
console.log("last");
// 读取Event Queue 打印second

常见异步任务

DOM事件

AJAX请求

定时器setTimeoutsetlnterval

ES6Promise


异步任务的优先级


下面继续来看一个案例:


setTimeout(() => {
console.log(1);
}, 1000)
new Promise(function(resolve){
console.log(2);
for(var i = 0; i < 10000; i++){
i == 99 && resolve();
}
}).then(function(){
console.log(3)
});
console.log(4)

按照上面的学习:
可以很轻松得出案例的打印结果:2,4,1,3



Promise定义部分为同步任务,回调部分为异步任务



将案例代码在控制台运行,最终返回结果却有些出人意料:


在这里插入图片描述


刚看到如此结果,我的第一感觉是,setTimeout函数1s触发太慢导致它加入Event Queue的时间晚于Promise.then


于是我修改了setTimeout的回调时间为0(浏览器最小触发时间为4ms),但结果仍为发生改变。


那么也就意味着,JavaScript的异步任务是存在优先级的。


宏任务和微任务


JavaScript除了广义上将任务划分为同步任务和异步任务,还对异步任务进行了更精细的划分。异步任务又进一步分为微任务和宏任务。


在这里插入图片描述




  • history traversal任务(h5当中的历史操作)

  • process.nextTicknodejs中的一个异步操作)

  • MutationObserverh5里面增加的,用来监听DOM节点变化的)



宏任务和微任务分别有各自的任务队列Event Queue,即宏任务队列和微任务队列。


Event Loop执行过程


了解到宏任务与微任务过后,我们来学习宏任务与微任务的执行顺序。

代码开始执行,创建一个全局调用栈,script作为宏任务执行

执行过程过同步任务立即执行,异步任务根据异步任务类型分别注册到微任务队列和宏任务队列

同步任务执行完毕,查看微任务队列

若存在微任务,将微任务队列全部执行(包括执行微任务过程中产生的新微任务)

若无微任务,查看宏任务队列,执行第一个宏任务,宏任务执行完毕,查看微任务队列,重复上述操作,直至宏任务队列为空


更新一下Event Loop的执行顺序图:


在这里插入图片描述


总结


在上面学习的基础上,重新分析当前案例:


setTimeout(() => {
console.log(1);
}, 1000)
new Promise(function(resolve){
console.log(2);
for(var i = 0; i < 10000; i++){
i == 99 && resolve();
}
}).then(function(){
console.log(3)
});
console.log(4)

分析过程见下图:
在这里插入图片描述



收起阅读 »

优雅的使用注释

代码千万行,注释第一行。 代码不规范,同事泪两行。 前言 注释相信小伙伴们都不陌生,但是就是这个小小的注释就像项目文档一样让许多小伙伴又爱又恨。不喜欢写注释,又讨厌别人不写注释。在此我们将讨论 JavaScript 和 CSS 的注释,希望通过这篇文章,让你...
继续阅读 »

代码千万行,注释第一行。

代码不规范,同事泪两行。



前言


注释相信小伙伴们都不陌生,但是就是这个小小的注释就像项目文档一样让许多小伙伴又爱又恨。不喜欢写注释,又讨厌别人不写注释。在此我们将讨论 JavaScriptCSS 的注释,希望通过这篇文章,让你重拾对注释的喜爱,让编码的乐趣如星辰大海。


一、语法


1.1 CSS 注释


/* css 注释 */

1.2 JavaScript 注释


// 单行注释

/**
* 多行注释,注意第一行最好用两个 *
* ...
*/

/*
当然,除了两端的 * 必须加以外,其他的 * 不加也行
...
*/


二、基本使用


2.1 单行注释


一般情况下,单行注释会出现在代码的正上方,起到提示的作用:


/* 用注释备注 CSS 类名的功能 */

/* 顶部组件 */
.hd {
position: fixed;
width: 100vw;
}

/* 版心 */
.container {
margin: 16px auto;
width: 1200px;
}

// 用单行注释备注简单的信息

const userName = ""; // 用户名
const userAvatar = ""; // 用户头像

// xxx函数
const myFunction = () => {};

2.2 多行注释


多行注释一般用于需要备注的信息过多的情况,常常出没于 JavaScript 函数的附近。首先提出一个问题:为什么要用到多行注释,用单行注释不香吗?下面就来看看下面的代码:


// xxx函数
const myFunction = ({ id, name, avatar, list, type }) => {
// 此处省略 30 行代码
};

小伙伴们可能看到了,一个传入五个参数,内部数行代码的函数竟然只有短短的一行注释,也许你开发的时候能记住这个函数的用途以及参数的类型以及是否必传等,但是如果你隔了一段时间再回头看之前的代码,那么简短的注释就可能变成你的困扰。 更不用说没有注释,不写注释一时爽,回看代码火葬场。 写注释的目的在于提高代码的可读性。相比之下,下面的注释就清晰的多:


/**
* 调整滚动距离
* 用于显示给定 id 元素
* @param id string 必传 元素 id
* @param distance number 非必传 距离视口最顶部距离(避免被顶部固定定位元素遮挡)
* @returns null
*/
export const scrollToShowElement = (id = "", distance = 0) => {
return () => {
if (!id) {
return;
};

const element = document.getElementById(id);
if (!element) {
return;
};

const top = element?.offsetTop || 0;
window.scroll(0, top - distance);
};
};

对于复杂的函数,函数声明上面要加上统一格式的多行注释,同时内部的复杂逻辑和重要变量也需要加上单行注释,两者相互配合,相辅相成。函数声明的多行注释格式一般为:


/**
* 函数名称
* 函数简介
* @param 参数1 参数1数据类型 是否必传 参数1描述
* @param 参数2 参数2数据类型 是否必传 参数2描述
* @param ...
* @returns 返回值
*/

多行注释的优点是清晰明了,缺点是较为繁琐(可以借助编辑器生成 JavaScript 函数注释模板)。建议逻辑简单的函数使用单行注释,逻辑复杂的函数和公共/工具函数使用多行注释。


当然,一个好的变量/函数名也能降低阅读者的思考成本,可以移步到我的文章:《优雅的命名 🧊🧊》


三、进阶使用


无论是 css 还是 JavaScript 中,当代码越来越多的时候,也使得寻找要改动的代码时变得越来越麻烦。所以我们有必要对代码按模块进行整理,并在每个模块的顶部用注释,结束时使用空行进行分割。


 /* 以下代码仅为示例 */

/* 模块1 */
/* 类名1 */
.class-a {}

/* 类名2 */
.class-b {}

/* 类名3 */
.class-c {}

/* 模块2 */
/* 类名4 */
.class-d {}

/* 类名5 */
.class-e {}

/* ... */

// 以下代码仅为示例

// 模块1
// 变量1
const value1 = "";
// 变量2
const value2 = "";
// 变量3
const value3 = "";
// 函数1
const myFunction1 = () => {};

// 模块2
// 变量4
const value4 = "";
// 变量5
const value5 = "";
// 变量6
const value6 = "";
// 函数2
const myFunction2 = () => {};

// ...

效果有了,但是似乎不太明显,因此我们在注释中增加 - 或者 = 来进行分割试试:


 /* ------------------------ 模块1 ------------------------ */
/* 类名1 */
.class-a {}

/* 类名2 */
.class-b {}

/* 类名3 */
.class-c {}

/* ------------------------ 模块2 ------------------------ */
/* 类名4 */
.class-d {}

/* 类名5 */
.class-e {}

/* ... */

// 以下代码仅为示例

/* ======================== 模块1 ======================== */
// 变量1
const value1 = "";
// 变量2
const value2 = "";
// 变量3
const value3 = "";
// 函数1
const myFunction1 = () => {};

/* ======================== 模块2 ======================== */
// 变量4
const value4 = "";
// 变量5
const value5 = "";
// 变量6
const value6 = "";
// 函数2
const myFunction2 = () => {};

// ...

能直观的看出,加长版的注释分割效果更好,区分度更高。高质量的代码往往需要最朴实无华的注释进行分割。其中 JavaScript 的注释“分割线”建议使用多行注释。




“华丽的”分割线:


 /* ------------------------ 华丽的分割线 ------------------------ */

/* ======================== 华丽的分割线 ======================== */

四、扩展


工欲善其事,必先利其器。下面我要推荐几款 VSCode 编辑器关于注释的插件。


4.1 Better Comments


Better Comments.png


插件介绍:可以改变注释的颜色,有四种高亮的颜色(默认为红色、橙色、绿色、蓝色)和一种带删除线的黑色。颜色可以在插件配置里面修改。下图为实例颜色和本人在项目中的用法,一个注释对应一种情况。


注释的默认颜色.png


喜欢花里胡哨的coder们必备插件,有效提高注释的辨析度和美感,从此爱上注释。其改变注释颜色只需要加上一个或多个字符即可,开箱即用。


// ! 红色的高亮注释,双斜线后加英文叹号     !     配置
// todo 橙色的高亮注释,双斜线后加 todo 函数
// * 绿色的高亮注释,双斜线后加 * 变量
// ? 蓝色的高亮注释,双斜线后加英文问号 ? 组件
// // 黑色带删除线的注释,双斜线后加双斜线 // 说明

4.2 koroFileHeader


koroFileHeader.png


插件介绍:文件头部添加注释,在光标处添加函数注释,一键添加佛祖保佑永无BUG、神兽护体等注释图案。


koroFileHeader 说明.png


4.3 JavaScript Comment Snippet


JavaScript Comment Snippet.png


插件介绍:可以快速生成 JavaScript 注释,冷门但是好用。


JavaScript Comment Snippet 使用.gif
JavaScript Comment Snippet 使用.png
JavaScript Comment Snippet 使用.png


结语


不得不说注释在编码过程中真的相当重要,为了写出更优雅,更易于维护的代码,我们也应当把最重要的信息写到注释里。一个项目的 README.markdown 和项目中的注释就喜像是项目的 说明书 一样,能让非项目开发者更快的读懂代码的含义以及编码的思想。让代码成就我们,让代码改变世界,让注释,伴我同行!



收起阅读 »

技术总结 | 前端萌新现在上车Docker,还来得及么?

序言 作为一名爱学习的前端攻城狮,在当下疯狂内卷的大环境🐱, 不卷一卷Docker是不是有点说不过去,再加上现在我司前端部署项目大部分都是Docker,所以现在赶紧上车, 跟着Up主来look look,欢迎有big old指正 Q:你能说一下你怎么看待Do...
继续阅读 »

序言


作为一名爱学习的前端攻城狮,在当下疯狂内卷的大环境🐱, 不卷一卷Docker是不是有点说不过去,再加上现在我司前端部署项目大部分都是Docker,所以现在赶紧上车, 跟着Up主来look look,欢迎有big old指正



  • Q:你能说一下你怎么看待DockerDocker能干什么么

  • A:Docker是一个便携的应用容器, 用来自动化测试和持续集成、发布


大家在面试的时候是不是这么回答的😂,恭喜你答对了,但是不够完整,现在来结合文档和Demo具体看看,Docker到底能干啥


概念


什么是Docker


Docker就好比是一个集装箱,里面装着各式各类的货物。在一艘大船上,可以把货物规整的摆放起来。并且各种各样的货物被集装箱标准化了,集装箱和集装箱之间不会互相影响。


有人觉得Docker是一台虚拟机,但是这种想法是错误的,直接上图


8931c4f83956f72f924f9c30aee3c40.png



上图差异,左图虚拟机的Guest OS层和Hypervisor层在Docker中被Docker Engine层所替代。虚拟机的Guest OS即为虚拟机安装的操作系统,它是一个完整操作系统内核;虚拟机的Hypervisor层可以简单理解为一个硬件虚拟化平台,它在Host OS是以内核态的驱动存在的。



三大核心概念


镜像(image)


镜像是创建docker容器的基础,docker镜像类似于虚拟机镜像,可以将它理解为一个面向docker引擎的只读模块,包含文件系统


创建镜像的方式



  1. 使用Dockerfile Build镜像

  2. 拉取Docker官方镜像


容器(container)


容器是从镜像创建的应用运行实例,容器之间是相互隔离、互不可见的。可以把容器看做一个简易版的linux系统环境(包括root权限、进程空间、用户空间和网络空间等),以及运行在这个环境上的应用打包而成的应用盒子。


可以利用docker create命令创建一个容器,创建后的的容器处于停止状态,可以使用docker start命令来启动它。也可以运行docker run命令来直接从镜像启动运行一个容器。docker run = docker creat + docker start


当利用docker run创建并启动一个容器时,docker在后台的标准操作包括:


(1)检查本地是否存在指定的镜像,不存在就从公有仓库下载。

(2)利用镜像创建并启动一个容器。

(3)分配一个文件系统,并在只读的镜像层外面挂载一层可读写层。

(4)从宿主机配置的网桥接口中桥接一个虚拟的接口到容器中。

(5)从地址池中配置一个IP地址给容器。

(6)执行用户指定的应用程序。

(7)执行完毕后容器终止。


仓库(Repository)


安装Docker后,可用通过官方提供的registry镜像来搭建一套本地私有仓库环境。


下载registry镜像:


6246e8b8ff24b58a0bdf74ab6de1e30.png


基础操作


安装Docker


linux安装Docker


8d75417f1599a5e7716278780346a8e.png


windows安装docker


推荐安装Docker Desktop 飞机票


image.png


拉取镜像


# 拉取镜像
>>> docker pull ubuntu
Using default tag: latest
latest: Pulling from library/ubuntu
f3ef4ff62e0d: Pull complete
Digest: sha256:a0d9e826ab87bd665cfc640598a871b748b4b70a01a4f3d174d4fb02adad07a9
Status: Downloaded newer image for ubuntu:latest
docker.io/library/ubuntu:latest

# 查看本地所有镜像
>>> docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest 597ce1600cf4 13 days ago 72.8MB
hello latest 8b9d88b05a48 2 weeks ago 231MB
centos latest 5d0da3dc9764 4 weeks ago 231MB
docker/getting-started latest 083d7564d904 4 months ago 28MB

# 删除镜像
>>> docker rmi ubuntu
Untagged: ubuntu:latest
Untagged: ubuntu@sha256:a0d9e826ab87bd665cfc640598a871b748b4b70a01a4f3d174d4fb02adad07a9
Deleted: sha256:597ce1600cf4ac5f449b66e75e840657bb53864434d6bd82f00b172544c32ee2
Deleted: sha256:da55b45d310bb8096103c29ff01038a6d6af74e14e3b67d1cd488c3ab03f5f0d


创建容器


#创建容器
>>> docker create --name my-ubuntu ubuntu
2da5d12e9cbaed77d90d23f5f5436215ec511e20607833a5a674109c13b58f48

#启动容器
>>> docker start 2da5d

#查看所有容器
>>> docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
2da5d12e9cba ubuntu "bash" About a minute ago Exited (0) 31 seconds ago my-ubuntu

#删除容器
>>> docker rm 2da5d

#创建并进入容器
>>> docker run --name my-ubuntu2 -it ubuntu
root@552c7c73dcf6:/#
#进入容器后就可以在容器内部执行脚本了

# 进入正在运行的容器
>>> docker exec -it 2703b1 sh
/ #


编排Dockerfile


Dockerfile是一个创建镜像所有命令的文本文件, 包含了一条条指令和说明, 每条指令构建一层, 通过docker build命令,根据Dockerfile的内容构建镜像,因此每一条指令的内容, 就是描述该层如何构建.有了Dockefile, 就可以制定自己的docker镜像规则,只需要在Dockerfile上添加或者修改指令, 就可生成docker镜像.


FROM ubuntu          #构造的新镜像是基于哪个镜像
MAINTAINER Up_zhu #维护者信息
RUN yum install nodejs #构建镜像时运行的shell命令
WORKDIR /app/my-app #设置工作路径
EXPOSE 8080 #指定于外界交互的端口,即容器在运行时监听的端口
ENV MYSQL_ROOT_PASSWORD 123456 #设置容器内环境变量
ADD ./config /app/config #拷贝文件或者目录到镜像,如果是URL或者压缩包会自动下载或者自动解压
COPY ./dist /app/my-app
VOLUME /etc/mysql #定义匿名卷

实战



基于vite项目打镜像,发布



新建Dockerfile


FROM nginx
COPY ./dist/ /usr/share/nginx/html/
COPY ./nginx/default.conf /etc/nginx/conf.d/default.conf

新建nginx配置文件


# nginx/default.conf
server {
listen 80;
server_name localhost;

#charset koi8-r;
access_log /var/log/nginx/host.access.log main;
error_log /var/log/nginx/error.log error;

location / {
root /usr/share/nginx/html;
index index.html index.htm;
}

#error_page 404 /404.html;

# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}


打镜像


image.png


查看本地镜像


>>> docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
my-vite latest cc015756264b About a minute ago 133MB

启动容器


image.png


现在可以访问地址来验证是否成功


image.png


查看本地正在运行的容器


image.png


文末


是不是很Easy呢?我们从上面可以看出,Docker 的功能是十分强大的,除此之外,我们还可以拉取一些 UbuntuApache 等镜像, 也可以自己定制一下镜像,发布到Docker Hub.


image.png


当然!本文介绍的只是Docker的基础功能,小编能力到此,还需继续学习~



收起阅读 »

实现无感刷新token,我是这样做的

前言 最近在做需求的时候,涉及到登录token,产品提出一个问题:能不能让token过期时间长一点,我频繁的要去登录。 前端:后端,你能不能把token 过期时间设置的长一点。 后端:可以,但是那样做不安全,你可以用更好的方法。 前端:什么方法? 后端:给你...
继续阅读 »

前言



最近在做需求的时候,涉及到登录token,产品提出一个问题:能不能让token过期时间长一点,我频繁的要去登录。


前端:后端,你能不能把token 过期时间设置的长一点。


后端:可以,但是那样做不安全,你可以用更好的方法。


前端:什么方法?


后端:给你刷新token的接口,定时去刷新token


前端:好,让我思考一下



需求



当token过期的时候,刷新token,前端需要做到无感刷新token,即刷token时要做到用户无感知,避免频繁登录。实现思路


方法一


后端返回过期时间,前端判断token过期时间,去调用刷新token接口


缺点:需要后端额外提供一个token过期时间的字段;使用了本地时间判断,若本地时间被篡改,特别是本地时间比服务器时间慢时,拦截会失败。


方法二


写个定时器,定时刷新token接口


缺点:浪费资源,消耗性能,不建议采用。


方法三


在响应拦截器中拦截,判断token 返回过期后,调用刷新token接口



实现



axios的基本骨架,利用service.interceptors.response进行拦截


import axios from 'axios'

service.interceptors.response.use(
  response => {
    if (response.data.code === 409) {
        return refreshToken({ refreshToken: localStorage.getItem('refreshToken'), token: getToken() }).then(res => {
          const { token } = res.data
          setToken(token)
          response.headers.Authorization = `${token}`
        }).catch(err => {
          removeToken()
          router.push('/login')
          return Promise.reject(err)
        })
    }
    return response && response.data
  },
  (error) => {
    Message.error(error.response.data.msg)
    return Promise.reject(error)
  })



问题解决



问题一:如何防止多次刷新token


我们通过一个变量isRefreshing 去控制是否在刷新token的状态。


import axios from 'axios'

service.interceptors.response.use(
  response => {
    if (response.data.code === 409) {
      if (!isRefreshing) {
        isRefreshing = true
        return refreshToken({ refreshToken: localStorage.getItem('refreshToken'), token: getToken() }).then(res => {
          const { token } = res.data
          setToken(token)
          response.headers.Authorization = `${token}`
        }).catch(err => {
          removeToken()
          router.push('/login')
          return Promise.reject(err)
        }).finally(() => {
          isRefreshing = false
        })
      }
    }
    return response && response.data
  },
  (error) => {
    Message.error(error.response.data.msg)
    return Promise.reject(error)
  })

问题二:同时发起两个或者两个以上的请求时,其他接口怎么解决


当第二个过期的请求进来,token正在刷新,我们先将这个请求存到一个数组队列中,想办法让这个请求处于等待中,一直等到刷新token后再逐个重试清空请求队列。那么如何做到让这个请求处于等待中呢?为了解决这个问题,我们得借助Promise。将请求存进队列中后,同时返回一个Promise,让这个Promise一直处于Pending状态(即不调用resolve),此时这个请求就会一直等啊等,只要我们不执行resolve,这个请求就会一直在等待。当刷新请求的接口返回来后,我们再调用resolve,逐个重试。最终代码:


import axios from 'axios'

// 是否正在刷新的标记
let isRefreshing = false
//重试队列
let requests = []
service.interceptors.response.use(
response => {
//约定code 409 token 过期
if (response.data.code === 409) {
if (!isRefreshing) {
isRefreshing = true
//调用刷新token的接口
return refreshToken({ refreshToken: localStorage.getItem('refreshToken'), token: getToken() }).then(res => {
const { token } = res.data
// 替换token
setToken(token)
response.headers.Authorization = `${token}`
// token 刷新后将数组的方法重新执行
requests.forEach((cb) => cb(token))
requests = [] // 重新请求完清空
return service(response.config)
}).catch(err => {
//跳到登录页
removeToken()
router.push('/login')
return Promise.reject(err)
}).finally(() => {
isRefreshing = false
})
} else {
// 返回未执行 resolve 的 Promise
return new Promise(resolve => {
// 用函数形式将 resolve 存入,等待刷新后再执行
requests.push(token => {
response.headers.Authorization = `${token}`
resolve(service(response.config))
})
})
}
}
return response && response.data
},
(error) => {
Message.error(error.response.data.msg)
return Promise.reject(error)
}
)

作者:远航_
链接:https://juejin.cn/post/7018439775476514823

收起阅读 »