1. 加载和执行
尽量将所有的<script>标签放在</body>标签之前,确保脚本执行前页面已经完成了渲染,避免脚本的下载阻塞其他资源(例如图片)的下载。
合并脚本,减少页面中的<script>标签
使用<script>标签的defer和async属性(两者的区别见这里)
通过Javascript动态创建<script>标签插入文档来下载,其不会影响页面其他进程
2.数据存取
由于作用域链的机制,访问局部变量比访问跨作用域变量更快,因此在函数中若要多次访问跨作用域变量,则可以用局部变量保存。
避免使用with语句,其会延长作用域链
嵌套的对象成员会导致引擎搜索所有对象成员,避免使用嵌套,例如window.location.href
对象的属性和方法在原型链的位置越深,访问的速度也越慢
3.Dom编程
进行大段HTML更新时,推荐使用innerHTML,而不是DOM方法
HTML集合是一个与文档中元素绑定的类数组对象,其长度随着文档中元素的增减而动态变化,因此避免在每次循环中直接读取HTML集合的length,容易导致死循环
使用节点的children属性,而不是childNodes属性,前者访问速度更快,且不包含空白文本和注释节点。
浏览器的渲染过程包括构建DOM树和渲染树,当DOM元素的几何属性变化时,需要重新构造渲染树,这一过程称为“重排”,完成重排后,浏览器会重新绘制受影响的部分到屏幕中,这一过程称为“重绘”。因此应该尽量合并多次对DOM的修改,或者先将元素脱离文档流(display:none、文档片段),应用修改后,再插入文档中。
每次浏览器的重排时都会产生消耗,大多数浏览器会通过队列化修改并批量执行来优化重排过程,可当访问元素offsetTop、scrollTop、clientTop、getComputedStyle等一系列布局属性时,会强制浏览器立即进行重排返回正确的值。因此不要在dom布局信息改变时,访问这些布局属性。
当修改同个元素多个Css属性时,可以使用CssText属性进行一次性修改样式,减少浏览器重排和重绘的次数
当元素发生动画时,可以使用绝对定位使其脱离文档流,动画结束后,再恢复定位。避免动画过程中浏览器反复重排文档流中的元素。
多使用事件委托,减少监听事件
4.算法和流程控制
for循环和while循环性能差不多,除了for-in循环最慢(其要遍历原型链)
循环中要减少对象成员及数组项的查询次数,可以通过倒序循环提高性能
循环次数大于1000时,可运用Duff Devices减少迭代次数
switch比if-else快,但如果具有很多离散值时,可使用数组或对象来构建查找表
递归可能会造成调用栈溢出,可将其改为循环迭代
如果可以,对一些函数的计算结果进行缓存
5.字符串和正则表达式
进行大量字符串的连接时,+和+=效率比数组的join方法要高
当创建了一个正则表达式对象时,浏览器会验证你的表达式,然后将其转化为一个原生代码程序,用户执行匹配工作。当你将其赋值给变量时,可以避免重复执行该步骤。
当正则进入使用状态时,首先要确定目标字符串的起始搜索位置(字符串的起始位置或正则表达式的lastIndex属性),之后正则表达式会逐个检查文本和正则模式,当一个特定的字元匹配失败时,正则表达式会试着回溯到之前尝试匹配的位置,然后尝试其他路径。如果正则表达式所有的可能路径都没有匹配到,其会将起始搜索位置下移一位,重新开始检查。如果字符串的每个字符都经历过检查,没有匹配成功,则宣布彻底失败。
当正则表达式不那么具体时,例如.和[\s\S]等,很可能会出现回溯失控的情况,在js中可以应用预查模拟原子组(?=(pattern))\1来避免不必要的回溯。除此之外,嵌套的量词,例如/(A+A+)+B/在匹配"AAAAAAAA"时可能会造成惊人的回溯,应尽量避免使用嵌套的量词或使用预查模拟原子组消除回溯问题。
将复杂的正则表达式拆分为多个简单的片段、正则以简单、必需的字元开始、减少分支数量|,有助于提高匹配的效率。
setTimeout(function(){ process(todo.shift()); if (todo.length > 0) { setTimeout(arguments.callee, 25); } else { callback(); } })
setTimeout(function(){ let start = +new Date(); do { process(todo.shift()); } while(todo.length > 0 && (+new Date() - start) < 50) if (todo.length > 0) { setTimeout(arguments.callee, 25); } else { callback(); } })
WebWork
进行计算
Expires: Mon,28 Jul 2018 23:30:30 GMT
eval
、Function
进行双重求值
Object
/Array
字面量定义,不要使用构造函数
if (i & 1) { className = 'odd'; } else { className = 'even'; }
Math
对象等
背景
这一个因为滚动条占据空间引起的bug, 查了一下资料, 最后也解决了,顺便研究一下这个属性, 做一下总结,分享给大家看看。
正文
昨天, 测试提了个问题, 现象是一个输入框的聚焦提示偏了, 让我修一下, 如下图:
image.png
起初认为是红框提示位置不对, 就去找代码看:
<Input
// ...
onFocus={() => setFocusedInputName('guidePrice')}
onBlur={() => setFocusedInputName('')}
/>
<Table
data-focused-column={focusedInputName}
// ...
/>
代码上没有什么问题, 不是手动设置的,而且, 在我和另一个同事, 还有PM的PC上都是OK的:
image.png
初步判断是,红框位置结算有差异, 差异大小大概是17px, 但是这个差异是怎么产生的呢?
就去测试小哥的PC上看, 注意到一个细节, 在我PC上, 滚动条是悬浮的:
image.png
在他PC上, 滚动条是占空间的:
image.png
在他电脑上, 手动把原本的 overscroll-y: scroll 改成 overscroll-y: overlay 问题就结局了。
由此判定是: 滚动条占据空间 引起的bug。
overscroll-y: overlay
CSS属性 overflow, 定义当一个元素的内容太大而无法适应块级格式化上下文的时候该做什么。它是 overflow-x 和overflow-y的 简写属性 。
/* 默认值。内容不会被修剪,会呈现在元素框之外 */
overflow: visible;
/* 内容会被修剪,并且其余内容不可见 */
overflow: hidden;
/* 内容会被修剪,浏览器会显示滚动条以便查看其余内容 */
overflow: scroll;
/* 由浏览器定夺,如果内容被修剪,就会显示滚动条 */
overflow: auto;
/* 规定从父元素继承overflow属性的值 */
overflow: inherit;
官方描述:
overlay 行为与 auto 相同,但滚动条绘制在内容之上而不是占用空间。 仅在基于 WebKit(例如,Safari)和基于Blink的(例如,Chrome或Opera)浏览器中受支持。
表现:
html {
overflow-y: overlay;
}
兼容性
没有在caniuse上找到这个属性的兼容性, 也有人提这个问题:
image.png
问题场景以及解决办法
1. 外部容器的滚动条
这里的外部容器指的是html, 直接加在最外层:
html {
overflow-y: scroll;
}
手动加上这个特性, 不论什么时候都有滚动宽度占据空间。
缺点: 没有滚动的时候也会有个滚动条, 不太美观。
优点: 方便, 没有兼容性的问题。
2. 外部容器绝对定位法
用绝对定位,保证了body的宽度一直保持完整空间:
html {
overflow-y: scroll; // 兼容ie8,不支持:root, vw
}
:root {
overflow-y: auto;
overflow-x: hidden;
}
:root body {
position: absolute;
}
body {
width: 100vw;
overflow: hidden;
}
3. 内部容器做兼容
.wrapper {
overflow-y: scroll; // fallback
overflow-y: overlay;
}
总结
个人推荐还是用 overlay, 然后使用scroll 做为兜底。
内容就这么多, 希望对大家有所启发。
文章如有错误, 请在留言区指正, 谢谢。
之前花了些时间将gatsby-theme-gitbook迁移到 Typescript,以获得在 VSCode 中更好的编程体验.
整体差不多已经完成迁移,剩下将 Gatsby 的 API 文件也迁移到 TS,这里可以看到 gatsby#21995 官方也在将核心代码库迁移到 Typescript,准备等待官方将核心代码库迁移完成,在迁移 API 文件.
这篇文章用XYShaoKang/gatsby-project-config,演示如何将 gatsby 迁移到 TypeScript,希望能帮到同样想要在 Gatsby 中使用 TS 的同学.
迁移步骤:
TS 配置
配置 ESLint 支持 TS
完善 GraphQL 类型提示
初始化项目
gatsby new gatsby-migrate-to-typescript XYShaoKang/gatsby-project-config
cd gatsby-migrate-to-typescript
yarn develop
TS 配置
安装typescript
添加typescript.json配置文件
修改 js 文件为 tsx
补全 TS 声明定义
安装typescript
yarn add -D typescript
添加配置文件tsconfig.json
// https://www.typescriptlang.org/v2/docs/handbook/tsconfig-json.html
{
"compilerOptions": {
"target": "esnext", // 编译生成的目标 es 版本,可以根据需要设置
"module": "esnext", // 编译生成的目标模块系统
"lib": ["dom", "es2015", "es2017"], // 配置需要包含的运行环境的类型定义
"jsx": "react", // 配置 .tsx 文件的输出模式
"strict": true, // 开启严格模式
"esModuleInterop": true, // 兼容 CommonJS 和 ES Module
"moduleResolution": "node", // 配置模块的解析规则,支持 node 模块解析规则
"noUnusedLocals": true, // 报告未使用的局部变量的错误
"noUnusedParameters": true, // 报告有关函数中未使用参数的错误
"experimentalDecorators": true, // 启用装饰器
"emitDecoratorMetadata": true, // 支持装饰器上生成元数据,用来进行反射之类的操作
"noEmit": true, // 不输出 js,源映射或声明之类的文件,单纯用来检查错误
"skipLibCheck": true // 跳过声明文件的类型检查,只会检查已引用的部分
},
"exclude": ["./node_modules", "./public", "./.cache"], // 解析时,应该跳过的路晋
"include": ["src"] // 定义包含的路径,定义在其中的声明文件都会被解析进 vscode 的智能提示
}
将index.js改成index.tsx,重新启动服务,查看效果.
其实 Gatsby 内置了支持 TS,不用其他配置,只要把index.js改成index.tsx就可以直接运行.添加 TS 依赖是为了显示管理 TS,而tsconfig.json也是这个目的,当我们有需要新的特性以及自定义配置时,可以手动添加.
补全 TS 声明定义
打开index.tsx,VSCode 会报两个错误,一个是找不到styled-components的声明文件,这个可以通过安装@types/styled-components来解决.
另外一个错误绑定元素“data”隐式具有“any”类型。,这个错误是因为我们在tsconfig.json中指定了"strict": true,这会开启严格的类型检查,可以通过关闭这个选项来解决,只是我们用 TS 就是要用它的类型检查的,所以正确的做法是给data定义类型.
下面来一一修复错误.
安装styled-components的声明文件
yarn add -D @types/styled-components
修改index.tsx
import React, { FC } from 'react'
import styled from 'styled-components'
import { graphql } from 'gatsby'
import { HomeQuery } from './__generated__/HomeQuery'
const Title = styled.h1`
font-size: 1.5em;
margin: 0;
padding: 0.5em 0;
color: palevioletred;
background: papayawhip;
`
const Content = styled.div`
margin-top: 0.5em;
`
interface PageQuery {
data: {
allMarkdownRemark: {
edges: Array<{
node: {
frontmatter: {
title: string
}
excerpt: string
}
}>
}
}
}
const Home: FC<PageQuery> = ({ data }) => {
const node = data.allMarkdownRemark.edges[0].node
const title = node.frontmatter?.title
const excerpt = node.excerpt
return (
<>
<Title>{title}</Title>
<Content>{excerpt}</Content>
</>
)
}
export default Home
export const query = graphql`
query HomeQuery {
allMarkdownRemark {
edges {
node {
frontmatter {
title
}
excerpt
}
}
}
}
`
这时候会出现一个新的错误,在excerpt: string处提示Parsing error: Unexpected token,这是因为 ESLint 还无法识别 TS 的语法,下面来配置 ESLint 支持 TS.
配置 ESLint 支持 TypeScript
安装依赖
yarn add -D @typescript-eslint/parser @typescript-eslint/eslint-plugin
配置.eslintrc.js
module.exports = {
parser: `@typescript-eslint/parser`, // 将解析器从`babel-eslint`替换成`@typescript-eslint/parser`,用以解析 TS 代码
extends: [
`google`,
`eslint:recommended`,
`plugin:@typescript-eslint/recommended`, // 使用 @typescript-eslint/eslint-plugin 推荐配置
`plugin:react/recommended`,
`prettier/@typescript-eslint`, // 禁用 @typescript-eslint/eslint-plugin 中与 prettier 冲突的规则
`plugin:prettier/recommended`,
],
plugins: [
`@typescript-eslint`, // 处理 TS 语法规则
`react`,
`filenames`,
],
// ...
}
在.vscode/settings.json中添加配置,让VSCode使用ESLint扩展格式化ts和tsx文件
// .vscode/settings.json
{
"eslint.format.enable": true,
"[javascript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"[javascriptreact]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"[typescript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"[typescriptreact]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
}
}
完善 GraphQL 类型提示
// index.tsx
import React, { FC } from 'react'
// ...
interface PageQuery {
data: {
allMarkdownRemark: {
edges: Array<{
node: {
frontmatter: {
title: string
}
excerpt: string
}
}>
}
}
}
const Home: FC<PageQuery> = ({ data }) => {
// ...
}
export default Home
export const query = graphql`
query HomeQuery {
allMarkdownRemark {
edges {
node {
frontmatter {
title
}
excerpt
}
}
}
}
`
我们看看index.tsx文件,会发现PropTypes和query结构非常类似,在Gatsby运行时,会把query查询的结果作为组件prop.data传入组件,而PropTypes是用来约束prop存在的.所以其实PropTypes就是根据query写出来的.
如果有依据query自动生成PropTypes的功能就太棒了.
另外一个问题是在query中编写GraphQL查询时,并没有类型约束,也没有智能提示.
总结以下需要完善的体验包括:
GraphQL 查询编写时的智能提示,以及错误检查
能够从 GraphQL 查询生成对应的 TypeScript 类型.这样能保证类型的唯一事实来源,并消除 TS 中冗余的类型声明.毕竟如果经常需要手动更新两处类型,会更容易出错,而且也并不能保证手动定义类型的正确性.
实现方式:
通过生成架构文件,配合Apollo GraphQL for VS Code插件,实现智能提示,以及错误检查
通过graphql-code-generator或者apollo生成 TS 类型定义文件
如果自己去配置的话,是挺耗费时间的,需要去了解graphql-code-generator的使用,以及Apollo的架构等知识.
不过好在社区中已经有对应的 Gatsby 插件集成了上述工具可以直接使用,能让我们不用去深究对应知识的情况下,达到优化 GraphQL 编程的体验.
尝试过以下两个插件能解决上述问题,可以任选其一使用
gatsby-plugin-codegen
gatsby-plugin-typegen
另外还有一款插件gatsby-plugin-graphql-codegen也可以生成 TS 类型,不过配置略麻烦,并且上述两个插件都可以满足我现在的需求,所以没有去尝试,感兴趣的可以尝试一下.
注意点:
Apollo不支持匿名查询,需要使用命名查询
第一次生成,需要运行Gatsby之后才能生成类型文件
整个项目内不能有相同命名的查询,不然会因为名字有冲突而生成失败
下面是具体操作
安装vscode-apollo扩展
在 VSCode 中按 Ctrl + P ( MAC 下: Cmd + P) 输入以下命令,按回车安装
ext install apollographql.vscode-apollo
方式一: 使用gatsby-plugin-codegen
gatsby-plugin-codegen默认会生成apollo.config.js和schema.json,配合vscode-apollo扩展,可以提供GraphQL的类型约束和智能提示.
另外会自动根据query中的GraphQL查询,生成 TS 类型,放在对应的tsx文件同级目录下的__generated__文件夹,使用时只需要引入即可.
如果需要在运行时自动生成 TS 类型,需要添加watch: true配置.
安装gatsby-plugin-codegen
yarn add gatsby-plugin-codegen
配置gatsby-config.js
// gatsby-config.js
module.exports = {
plugins: [
// ...
{
resolve: `gatsby-plugin-codegen`,
options: {
watch: true,
},
},
],
}
重新运行开发服务生成类型文件
yarn develop
如果出现以下错误,一般是因为没有为查询命名的缘故,给查询添加命名即可,另外配置正确的话,打开对应的文件,有匿名查询,编辑器会有错误提示.
fix-anonymous-operations.png
这个命名之后会作为生成的类型名.
修改index.tsx以使用生成的类型
gatsby-plugin-codegen插件会更具查询生成对应的查询名称的类型,保存在对应tsx文件同级的__generated__目录下.
import { HomeQuery } from './__generated__/HomeQuery' // 引入自动生成的类型
// ...
// interface PageQuery {
// data: {
// allMarkdownRemark: {
// edges: Array<{
// node: {
// frontmatter: {
// title: string
// }
// excerpt: string
// }
// }>
// }
// }
// }
interface PageQuery {
data: HomeQuery // 替换之前手写的类型
}
// ...
将自动生成的文件添加到.gitignore中
apollo.config.js,schema.json,__generated__能通过运行时生成,所以可以添加到.gitignore中,不用提交到 git 中.当然如果有需要也可以选择提交到 git 中.
# Generated types by gatsby-plugin-codegen
__generated__
apollo.config.js
schema.json
方式二: 使用gatsby-plugin-typegen
gatsby-plugin-typegen通过配置生成gatsby-schema.graphql和gatsby-plugin-documents.graphql配合手动创建的apollo.config.js提供GraphQL的类型约束和智能提示.
根据GraphQL查询生成gatsby-types.d.ts,生成的类型放在命名空间GatsbyTypes下,使用时通过GatsbyTypes.HomeQueryQuery来引入,HomeQueryQuery是由对应的命名查询生成
安装gatsby-plugin-typegen
yarn add gatsby-plugin-typegen
配置
// gatsby-config.js
module.exports = {
plugins: [
// ...
{
resolve: `gatsby-plugin-typegen`,
options: {
outputPath: `src/__generated__/gatsby-types.d.ts`,
emitSchema: {
'src/__generated__/gatsby-schema.graphql': true,
},
emitPluginDocuments: {
'src/__generated__/gatsby-plugin-documents.graphql': true,
},
},
},
],
}
//apollo.config.js
module.exports = {
client: {
tagName: `graphql`,
includes: [
`./src/**/*.{ts,tsx}`,
`./src/__generated__/gatsby-plugin-documents.graphql`,
],
service: {
name: `GatsbyJS`,
localSchemaFile: `./src/__generated__/gatsby-schema.graphql`,
},
},
}
重新运行开发服务生成类型文件
yarn develop
修改index.tsx以使用生成的类型
gatsby-plugin-codegen插件会更具查询生成对应的查询名称的类型,保存在对应tsx文件同级的__generated__目录下.
// ...
// interface PageQuery {
// data: {
// allMarkdownRemark: {
// edges: Array<{
// node: {
// frontmatter: {
// title: string
// }
// excerpt: string
// }
// }>
// }
// }
// }
interface PageQuery {
data: GatsbyTypes.HomeQueryQuery // 替换之前手写的类型
}
// ...
将自动生成的文件添加到.gitignore中
__generated__能通过运行时生成,所以可以添加到.gitignore中,不用提交到 git 中.当然如果有需要也可以选择提交到 git 中.
# Generated types by gatsby-plugin-codegen
__generated__
Canvas 是 HTML5 提供的一个用于展示绘图效果的标签. Canvas 原意为画布, 在 HTML 页面中用于展示绘图效果. 最早 Canvas 是苹果提出的一个方案, 今天已经在大多数浏览器中实现。
canvas 的使用领域
游戏
大数据可视化数据
banner 广告
多媒体
模拟仿真
远程操作
图形编辑
判断浏览器是否支持 canvas 标签
var canvas = document.getElementById('canvas')
if (canvas.getContext) {
console.log('你的浏览器支持Canvas!')
} else {
console.log('你的浏览器不支持Canvas!')
}
canvas 的基本用法
1、使用 canvas 标签, 即可在页面中开辟一格区域,可以设置其宽高,宽高为 300 和 150
<canvas></canvas>
2、获取 dom 元素 canvas
canvas 本身不能绘图. 是使用 JavaScript 来完成绘图. canvas 对象提供了各种绘图用的 api。
var cas = document.querySelector('canvas')
3、通过 cas 获取上下文对象(画布对象!)
var ctx = cas.getContext('2d')
4、通过 ctx 开始画画(设置起点 设置终点 连线-描边 )
ctx.moveTo(10, 10)
ctx.lineTo(100, 100)
ctx.stroke()
绘制线条
设置开始位置: context.moveTo( x, y )
设置终点位置: context.lineTo( x, y )
描边绘制: context.stroke()
填充绘制: context.fill()
闭合路径: context.closePath()
canvas 还可以设置线条的相关属性,如下:
CanvasRenderingContext2D.lineWidth 设置线宽.
CanvasRenderingContext2D.strokeStyle 设置线条颜色.
CanvasRenderingContext2D.lineCap 设置线末端类型,'butt'( 默认 ), 'round', 'square'.
CanvasRenderingContext2D.lineJoin 设置相交线的拐点, 'miter'(默认),'round', 'bevel',
CanvasRenderingContext2D.getLineDash() 获得线段样式数组.
CanvasRenderingContext2D.setLineDash() 设置线段样式.
CanvasRenderingContext2D.lineDashOffset 绘制线段偏移量.
封装一个画矩形的方法
function myRect(ctxTmp, x, y, w, h) {
ctxTmp.moveTo(x, y)
ctxTmp.lineTo(x + w, y)
ctxTmp.lineTo(x + w, y + h)
ctxTmp.lineTo(x, y + h)
ctxTmp.lineTo(x, y)
ctxTmp.stroke()
}
var cas = document.querySelector('canvas')
var ctx = cas.getContext('2d')
myRect(ctx, 50, 50, 200, 200)
绘制矩形
fillRect( x , y , width , height) 填充以(x,y)为起点宽高分别为 width、height 的矩形 默认为黑色
stokeRect( x , y , width , height) 绘制一个空心以(x,y)为起点宽高分别为 width、height 的矩形
clearRect( x, y , width , height ) 清除以(x,y)为起点宽高分别为 width、height 的矩形 为透明
绘制圆弧
绘制圆弧的方法有
CanvasRenderingContext2D.arc()
CanvasRenderingContext2D.arcTo()
6 个参数: x,y(圆心的坐标),半径,起始的弧度(不是角度 deg),结束的弧度,(bool 设置方向 ! )
var cas = document.querySelector('canvas')
var ctx = cas.getContext('2d')
ctx.arc(100, 100, 100, 0, degToArc(360))
ctx.stroke()
// 角度转弧度
function degToArc(num) {
return (Math.PI / 180) * num
}
绘制扇形
var cas = document.querySelector('canvas')
var ctx = cas.getContext('2d')
ctx.arc(300, 300, 200, degToArc(125), degToArc(300))
// 自动连回原点
ctx.closePath()
ctx.stroke()
function degToArc(num) {
return (Math.PI / 180) * num
}
制作画笔
声明一个变量作为标识
鼠标按下的时候,记录起点位置
鼠标移动的时候,开始描绘并连线
鼠标抬起的时候,关闭开关
点击查看效果图
var cas = document.querySelector('canvas')
var ctx = cas.getContext('2d')
var isDraw = false
// 鼠标按下事件
cas.addEventListener('mousedown', function () {
isDraw = true
ctx.beginPath()
})
// 鼠标移动事件
cas.addEventListener('mousemove', function (e) {
if (!isDraw) {
// 没有按下
return
}
// 获取相对于容器内的坐标
var x = e.offsetX
var y = e.offsetY
ctx.lineTo(x, y)
ctx.stroke()
})
cas.addEventListener('mouseup', function () {
// 关闭开关了!
isDraw = false
})
手动涂擦
原理和画布相似,只不过用的是clearRect()方法。
点击查看效果图
var cas = document.querySelector('canvas')
var ctx = cas.getContext('2d')
ctx.fillRect(0, 0, 600, 600)
// 开关
var isClear = false
cas.addEventListener('mousedown', function () {
isClear = true
})
cas.addEventListener('mousemove', function (e) {
if (!isClear) {
return
}
var x = e.offsetX
var y = e.offsetY
var w = 20
var h = 20
ctx.clearRect(x, y, w, h)
})
cas.addEventListener('mouseup', function () {
isClear = false
})
刮刮乐
首先需要设置奖品和画布,将画布置于图片上方盖住,
随机设置生成奖品。
当手触摸移动的时候,可以擦除部分画布,露出奖品区。
点击查看效果图
<div>
<img src="./images/2.jpg" alt="" />
<canvas width="600" height="600"></canvas>
</div>
css
img {
width: 600px;
height: 600px;
position: absolute;
top: 10%;
left: 30%;
}
canvas {
width: 600px;
height: 600px;
position: absolute;
top: 10%;
left: 30%;
border: 1px solid #000;
}
js
var cas = document.querySelector('canvas')
var ctx = cas.getContext('2d')
var img = document.querySelector('img')
// 加一个遮罩层
ctx.fillStyle = '#ccc'
ctx.fillRect(0, 0, cas.width, cas.height)
setImgUrl()
// 开关
var isClear = false
cas.addEventListener('mousedown', function () {
isClear = true
})
cas.addEventListener('mousemove', function (e) {
if (!isClear) {
return
}
var x = e.offsetX
var y = e.offsetY
ctx.clearRect(x, y, 30, 30)
})
cas.addEventListener('mouseup', function () {
isClear = false
})
function setImgUrl() {
var arr = ['./images/1.jpg', './images/2.jpg', './images/3.jpg', './images/4.jpg']
// 0-3
var random = Math.round(Math.random() * 3)
img.src = arr[random]
}
更多demo,请查看 github.com/Michael-lzg…
简单来说,v-if 的初始化较快,但切换代价高;v-show 初始化慢,但切换成本低
都是动态显示DOM元素
(1)手段:
v-if是动态的向DOM树内添加或者删除DOM元素;
v-show是通过设置DOM元素的display样式属性控制显隐;
(2)编译过程:
v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;
v-show只是简单的基于css切换;
(3)编译条件:
v-if是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始局部编译(编译被缓存?编译被缓存后,然后再切换的时候进行局部卸载);
v-show是在任何条件下(首次条件是否为真)都被编译,然后被缓存,而且DOM元素保留;
(4)性能消耗:
v-if有更高的切换消耗;
v-show有更高的初始渲染消耗;
(5)使用场景:
v-if适合运营条件不大可能改变;
v-show适合频繁切换。
虽然Vue 3还没有正式发布,但是热爱新技术的我早已按捺不住自己的内心,开始尝试在小项目中使用它了。
根据这篇《今日凌晨Vue3 beta版震撼发布,竟然公开支持脚手架项目!》我搭建了一个Vue 3的脚手架项目,用这种方式搭建的脚手架项目不仅仅只有vue是新版的,就连vue-router、vuex都是的。
给大家截一下package.json的图:
可以看到vue-router和vuex都已经开启4.0时代啦!
不过其实我并没有去了解过vue-router 4.0的新用法什么的,因为我觉得它不像vue 3.0都已经进行到beta的版本不会有特别大的变动。
而vue-router 4.0还是alpha的阶段,所以我认为现在去学习它有些为时尚早。但却就是它!差点酿成了一场惨剧。
旧版vue + vue-router的使用方式
假如你在路由里面定义了一个动态参数通常都会这么写:
{
path: '/:id'
}
然后用编程式导航的时候通常会这样去写:
this.$router.push('/123')
在组件中是这样获取这个参数的:
this.$route.params.id
我以为的新版vue + vue-router的使用方式
由于vue 3.0的Composition API中没有this了,所以我想到了通过获取组件实例的方式来获取$route:
import { defineComponent, getCurrentInstance } from 'vue'
export default defineComponent((props, context) => {
const { ctx } = getCurrentInstance()
console.log(ctx.$route)
})
没想到打印出来的居然是undefined!
这是咋回事呢?
于是我又打印了一遍ctx(ctx是当前组件上下文):
没有$的那些字段是我在组件中自己定义的变量,带$的这些就是vue内置的了,找了半天发现没有$route了,只剩下了一个$router,估计vue-router 4.0把当前路由信息都转移到$router里面去了。
带着猜想,我点开了$router:
currentRoute! 看名字的话感觉应该就是它了!于是乎我:
import { defineComponent, getCurrentInstance } from 'vue'
export default defineComponent((props, context) => {
const { ctx } = getCurrentInstance()
console.log(ctx.$router.currentRoute.value.params.id)
})
果然获取到了!好开心!
实际的新版vue + vue-router用法
在接下来的过程中我用ctx.$router代替了原来的this.$router、用ctx.$router.currentRoute.value代替了原先的this.$route。
尽管在接下来的进度中并没有出现任何的bug,程序一直都是按照我所设想的那样去运行的。
但在项目打包后却出现了意想不到的bug:在跳转路由的时候报了一个在undefined上面没有push的错误。
奇了怪了,在开发阶段程序都没有任何的报错怎么一打包就不行了呢?根据我多年的开发经验,我很快就定位到了是vue-router的错误。
难道这样写是错的吗?可是我打印了ctx,它里面明明有一个$router、$router里面明明就有currentRoute、currentRoute里面明明就有一个value、value里面明明就有params、params里面我一点开明明就看到了传过来的参数啊:
估计可能是vue-router的bug,果然alpha阶段的产物不靠谱,我开始后悔使用新版的vue脚手架项目了。
vue-router里的hooks
不过这时我突然灵光一现,vue 3不是受到了react hooks的启发才产生了Composition API的吗?
那么估计vue-router肯定也会受到react-router的启发了!
还好我学过react,果然技多不压身啊!估计里面肯定是有一个useXxx,就像这样:
import { useXxx } from 'vue-router'
那么应该是use什么呢?按理来说应该会尽量的和以前的API保持一定的联系,我猜应该是useRoute和useRouter吧!
为了验证我的想法,我打开了node_modules找到了vue-router的源码:
果不其然,在第2454和第2455行我发现它导出了useRoute和useRouter,那么就是它了:
import { defineComponent } from 'vue'
import { useRoute, useRouter } from 'vue-router'
export default defineComponent(_ => {
const route = useRoute()
const router = useRouter()
console.log(route.params.id)
router.push('/xxx/xxx')
})
使用这种方式不但可以成功跳转路由,也同样可以获取到路由传过来的参数,这次再打包试了一下,果然就没有之前的那个报错了。
结语
估计以后的vue全家桶要开启全民hooks的时代了,在翻看源码的同时我发现他们把一些示例都写在了vue-router/playground文件夹下了,在里面我发现了一些有趣的用法。
如果有时间的话我会仔细研究一下然后出一篇更加深入的文章给大家,当然如果已经有小伙伴等不及我出新文章的话可以直接进入vue-router-next的github地址:
https://github.com/vuejs/vue-router-next
它的示例都放在了playground这个文件夹下,期待你们研究明白后出一篇更加深入的文章!
将要分析的竞品排了个期,从最难最不熟悉的开始。为什么从最难的开始,可能是个人习惯吧,吃掉最难的那个,后面就会更上手。突然想起之前读的一本书「吃掉那只青蛙」,很不错的一本书,有时间去温习下。
一个产品,其实会有很多功能点,有核心的主要功能,也有一些辅助功能,也会有一些让你忽略,但关键时刻很需要的应急功能,而这些点都需要去整理出来。
这一点很重要,要先熟悉产品。如果对产品都不熟悉,那还是先不要做竞品分析。因为很难判断竞品的功能和风格是否也适合当前产品,因为对产品的不熟悉,会产生误判。
当然,产品的目标人群,产品定位,适用范围等等,都会影响产品分析。
所以,花时间熟悉自己负责的产品,是不能跳过的。
1. 制定时间规划
最好事先做好时间规划,可以有一整块的时间,这样分析产品时,思绪也会比较完整和连续,可以更专注。计算大概分析一个产品需要花费的时间,最好不要用零碎时间来做,这样只会增加时间上的代价,也会增加挫折感;
2. 确定分析的目的
在「竞品分析」中,想要得到的结论和重点是什么。比如重点可能是产品的报表功能、产品的代码审核功能等等,目的的确定能让分析更有针对性,减少干扰。无目的随意分析,得到的结果也会是零乱不堪,最后只是在浪费时间。
3. 寻找帮助者
每个产品,都有其不一样的特性和产品逻辑,你不一定能够完全 cover 到,甚至有些点就是比较难理解的,特别是偏技术性的名词,这时若有技术同学的帮助,就会如虎添翼。所以最好可以事先找一位产品相关的技术同学,询问这段时间是否有空,帮助你解答一些问题。
个人建议:能够在网上查到的资料,就不要先问人,除非时间成本特别高。一方面也是提升自己解决问题的能力,另一方面,也是节省彼此的时间。对方愿意帮你解决问题,不代表你要把所有问题一股脑倒给他,自己了解后再问,也是对对方的尊重,大家的时间都同样宝贵。
4. 其他tips
如果是内部公司产品,提前确认是否需要权限,提前申请好,减少正式开始后,还要等待审批时间。外部产品可以提前找好网站,可以咨询的客服入口,如果是付费竞品,咨询是否可以向财务申请报销等等。
1. 像个用户一样去使用产品
很多时候,设计师的职业病,会让我们过多注重视觉享受,而忽略作为用户,想要的有时候只是功能可用。今天不管你把「扫一扫」功能做得多美,美得像个艺术品一样,可是当扫码付款的时候,怎么也扫不出来,那种站在店家前面忐忑不安,怎么也无法完成付款,后面一堆人等你,你仿佛听见后面其他顾客窃窃私语地讨论着发生什么事情。那种场景我相信你不想经历,同样我们也不应该让用户来经历。
我的项目主管,一直都有提醒我,要像个小白来使用和设计我们的产品。这句建议,也一直在提醒着我。如果站在高姿态来俯视用户,我们就很难真正的「懂」用户,进而很难设计出真正满足用户需要的产品。
这是竞品分析,但是我们也需要转换自己的角色,变成用户。这样能更明白究竟竞品带给用户是便利,还是麻烦。有时适时抽离「设计师」的角色,会让你更能去体会用户的感受。
所以,先去用这个产品吧,然后才会有然后。
2. 如何去使用竞品
一个产品的使用,总是有它的使用场景,手机端的就更多样了,简直无所不在。B 端产品可能会相对少,一般是在办公场景或是特定场景。
可以像个编剧一样,给自己写点剧本,加点情节,塑造一个角色,假设竞品是电商方向,你可以想像,自己是一个刚毕业的社会新人,你可能没多少钱,你可能刚拿到你人生第一桶金,你想买件衣服犒劳自己,或许你会是数码控,你关注已久的佳能单反在双 11 中有优惠等等,然后再去预想接下去的情节,在购物方面会考虑的问题,或许是好用,或许是有趣等等。
也可以做任务式去使用产品,比如以电商为例,任务可以是买件喜欢的衣服,从搜索产品,到找到喜欢的衣服,添加购物车,提交订单,等待发货,收货,确认收货。这一个完整的流程走下来,就会体验产品功能是否好用,搜索结果是否符合预期等等。
3. 记录
使用产品的过程中,会遇到很多情况,有些是可预期的,有些是不可预期的。有些让人觉得很好用,有些却会让人受挫。将这些情况都记录下来,有助于分析产品的可用性程度和满意度。
上面这些只是举例说明,在竞品当中可能遇到的一些问题,也可以去反思自己的产品是否也会这样让用户感到困惑。有时候,太熟悉自己的产品,会自认为产品很完美,会理所当然认为「大家都这么认为」……
记录问题、原因,感受并截图为证(有必要可录屏),后期可追溯。写得越详细越好,后面整理的时候会更清晰。
4. 各个击破-功能了解
在熟悉整个产品后,就需要对产品的各个功能进行分析了解、梳理。了解竞品的核心功能是什么,核心功能在解决用户什么问题,是否真的解决了用户的痛点,其他功能又在整个产品当中充当什么样的角色。
将竞品的功能与本产品功能对比,不只是对比有无,更进一步地去想,为什么有这个功能,为什么没有这个功能,有或没有是否会提高用户的使用效率,用户的留存,用户的体验等等。
功能多不代表好,如果功能不能给用户带来益处,其实它的存在只是增加开发成本而已。
其实竞品分析中,最难的是总结归纳。做了一堆的分析后,结论是什么呢,这个结论如何写呢?
可以先从设立分析目的开始,找到中心轴线,然后再慢慢延展开来。在要做总结报告时,你会欣喜地发现最初设立目标是多么的重要。
文章来源:优设 作者:箴盐设计
财务和金融相关的应用是一个相对专业的分支,在这个领域当中创造体验优异的设计并不是一件简单的事情。身为资深设计师Taras Bakusevych 在这篇文章当中,分享了10个确保这类产品足够优秀的核心设计原则。
互联网时代的人们早就受够了信息爆炸,我们每天都会经系统推送、应用通知、微信、电话、短信等各类渠道收到大量消息。有多久你没有查收自己的邮箱?就算打开邮件,又有多少推荐内容让你有兴趣进一步了解?是 EDM 老了没用了?真正的原因,可能是我们一开始就错误地忽视了 EDM 设计。
对于 95 后以及更年轻的群体来说,EDM 确实是个上了年纪的概念。EDM(Email Direct Marketing)也叫 Email 营销、电子邮件营销。企业向目标客户发送 EDM 邮件,建立同目标客户的沟通渠道,向其直接传达相关信息,用来促进销售转化。
这个起源于上世纪 80 年代中期,正式诞生于 90 年代的早期互联网产物现在已经三十多岁了。时至今日,EDM 早已成为了全球公认的网络营销重要方法之一,其卓越效果为互联网人数十年的实践所证实。但 EDM 在我国的应用还处于非常低级的水平,不仅没有系统的理论,在实践中也存在许多误区。
在这样一个重视审美与强调更新及时的时代,EDM 邮件朴实无华的外表与「一旦发出就固定呈现」的内容特质显得有些格格不入。作为用户体验设计师,我们可以做什么让 EDM 不落伍呢?
首先,我们可以在设计层面上避免 EDM 邮件被邮箱软件识别为垃圾邮件,不带敏感词语或内容、淡化商业广告色彩、减少数字与附件使用都有助于降低被邮箱系统屏蔽的风险。我们更可以在全量发送前,对指定邮箱进行小范围测试以确保邮件发送成功率。
其次,从其历史来源来看,早期的 EDM 来源于垃圾邮件,这使人们对其本能地缺乏好感,存在排斥心理。因此 EDM 的节奏和时机必须做好控制,对邮件发送的各类数据做好统计,掌握用户的阅读习惯,能更好地提升邮件的打开率。
邮件内容需要设计为一定的格式来发送,常用的邮件格式包括纯文本格式、HTML 格式和 Rich Media 格式,或者是这些格式的组合。一般来说,HTML 格式和 Rich Media 格式的电子邮件比纯文本格式具有更好的体验效果。但 Rich Media 格式的电子邮件易造成邮件过大,并且无法确保用户在客户端均能够正常显示,所以在设计时我们优先选择 HTML 格式邮件。
与网页不同,我们无法针对不同设备做邮件内容相应的适配设计,兼顾设备特性的通用模版也就成为了设计时的必要关注点。对用户来说,一封邮件阅读体验很差,那么无论邮件的内容多么精彩、多么吸引人,最终的结果也可能只会被丢弃在一边。因此,我们通常会按照移动端尺寸对邮件界面进行设计,注意字体大小、最佳尺寸以及链接按钮的大小等。
除此以外,邮件中链接的定义也应得到我们充分的重视。由于邮件中的链接我们同样无法预先针对不同打开设备进行单独编辑,在有条件的情况下我们可以对链接所跳转的页面进行响应式设计以确保高质量的跨端浏览体验,或者我们也可以采用默认跳转路径而后重定向的传统方式。
EDM 营销与一般的营销方式最大的区别是:EDM 是一对一的沟通,让用户感觉到尊重,让他感觉到这是为他所建立并且是他所独享的沟通方式。在标题、正文的文案上强调「我」,在内容上也应如此。用户在意什么,我们就发送什么。把握住用户关注的信息,帮助用户收集支持 TA 做决策所需的信息。当我们发送邮件给用户,给予其操作行为的反馈或提醒时,不要浪费这最好的营销机会。优先提供给用户与之行为或特征相关的服务与帮助,其次通过个性化服务或产品推荐促进购买或注册转化,有助于我们将营销机会转化为实际销售成果。
做好个性化对 EDM 内容模型要求颇高,但从设计角度讲,我们完全可以以原子设计思维实现邮件内容模块的低成本创建与复用。以通用设计模块为「壳」,内容与组合规则为「核」,快速响应 EDM 的运营需求。
以上 5 点就是我结合近期项目经验所得。EDM 虽老,但设计可以让 EDM 老而弥新。祝经你精心设计的 EDM 邮件,一经发出,封封有回应
文章来源:优设 作者:鱼子酱聊设计
今天和大家聊一个很多朋友常年卡在 P5/P6 需要关心的命题——如何从业务出发打造具有商业价值还能兼顾用户体验的设计,此篇不谈理论,就通过 4 个经典的重量级产品案例就给大家安排明白啥是「一拳超人」式体验设计——就一个字「强」。
滴滴出行应该属于大家的高频使用 app,但是使用的功能一般还是集中在叫车流程,所以大家可能不太会关注到 CDX 设计团队一个非常核心的设计成果——xpanel。
简单来说 xpanel 就是一个附着于第一信息架构层级上,垂直 Y 轴且支持 X 轴拓展滑动的 Feed 卡片位。内容上分为「消息卡片」「主体卡片」「拓展卡片」三个维度,首屏保障除了「消息」与「主体」外三分之一「拓展卡片1」的露出。
但在简单的交互背后蕴藏的是基于业务的 UGD(用户增长设计)设计思考,这里引用 2018IXDC 会上滴滴主讲人的原话来说就是:
对特定场景垂直领域的深耕和挖掘,寻找「接触点」,帮助获取更多的功能、内容、服务、特性、品牌、运营甚至是喜好……进而实现业务的「有效增长」(转化、变现、留存)。
通俗一点解释就是 xpanel 利用主卡与拓展卡之间的信息架构关系,把拓展卡平衡的分为几类,比如「与产品功能相关的卡片」「与运营相关的卡片」等。
把本来被 LBS 地图一屏内抢占的空间通过简易的交互模式补偿回来了,这样既不打破用户的核心体验 focus 在地图与主卡上,同时又增强了运营、功能的玩法与拓展,可谓双赢。
根据这几年滴滴 xpanel 的线上应用,拓展卡片基本挖掘涵盖了以下场景的露出:优惠福利、出现卡券、会员体系、安全相关、出行提醒、拉新导流、运营活动等,未来可拓展的价值内容会更多。看着各路出行类 app 又纷纷长期沿用 xpanel 的设计,想必线上的数据反馈应该也是很正向的。
在上篇文章《多维度解析 | 抖音vs快手的产品设计策略差异》中的商业化模块里简要提及过抖音的 Topiew 超级广告位,这里单独拿出来和大家解析一下它究竟有多6。
从功能角度看,它是一个从开屏延续到端内视频信息流的广告位,占据了用户从进入抖音的第一视觉。
从交互角度看,topview 主要展现以开屏沉浸式视频 3s 播放→淡出互动转化组件 3s(完美融入原生视频信息流),剩余操作手势与功能等同原生视频信息流。
在这样一个有着 1 亿+第一曝光的产品位置,单纯只做常规静态开屏稳当入账不香吗?事实是抖音确实让它不香了,没有创新就没有新的收获。基于业务和当前产品形态下的交互模式使抖音有一个天时地利的优势——沉浸式体验,在这样的交互模式下给视频化的开屏提供了很好的承接入口。从开屏开启到融入信息流,在交互形态的切换中又为广告内容的播放时长赢得了更多时间。
更可怕的一点是 3s 播放后融入原生视频信息流中的 TopView 除了正常收割广告转化带来的单量,还可以通过右侧的主页链接轻松引流进行粉丝沉淀(今天就算你不买,先关注我,成为我的潜在用户,来日我再推一个新商品视频,你可以第一时间看见也许感兴趣就买单了)。
说完这些大家仔细回忆一下平常我们接触的有视频广告的视频平台,别说 60s、30s,15s 我们都嫌长,但为啥 TopView 显得相对没那么惹人烦呢(上次留的思考题)?个人认为除了抖音在选择合作品牌时会倾向符合平台气质的品牌合作(细数它合作过的品牌:Mac、宝马、林肯、vivo 等)保障广告质量和提供「跳过」外,直接融入信息淡出的互动组件会不仅会给用户新奇感,还会激发用户的互动欲望。
最后看一组数据(与宝马合作数据),曝光数:1.1 亿+;有效播放率:53.82%;点击率:13.26%。所以你猜一个最长可以展示 60s 的品牌视频内容、同时进行品牌粉丝沉淀、良好体验带来更高有效播放的亿级曝光广告位能值多少钱?
2016 年淘宝启动了一个项目要做一款内容化栏目——以视频为主,每晚更新一期,类比「一千零一夜」的故事。
那么在满满当当的淘宝运营区里该选择哪一个来试玩这个有趣的「新栏目」呢?是在头部的 10 宫格里再挤进去一个图标呢?还是在热门推荐里挤出一个 tab 呢?还是做一个悬浮的右下角的运营位?显然都不太合适。
根据这款产品每晚 6 点钟才可以使用,早上 7 点就会消失的游戏规则,最适配它的入口是一个不占界面原生空间,同时又有一定仪式感的位置。于是下拉 loading 的大空区成为了设计师们考虑的阵地。
△ 不知道这个banner为什么要排挤我
但地方选好了,又有了新顾虑。因为 iOS 的用户基本被系统洗脑了下拉手势,对于他们来说下拉=刷新,贸然在下拉刷新的手势基础上再叠加一个无关联的结果显然是有风险的。因此从交互上需要界定 2 个维度的指标来保障新栏目的体验。
反复试错 2 个指标数据的实际体验之后,新栏目有了安身之所,赐名「二楼」。进入「二楼」的整体交互和现在的短视频产品玩法基本雷同,全屏竖滑切换,小图标带货。下拉加载位的开发,从普通 loading 动效到运营位的植入基本被各类电商平台轻松复刻了,因此这一切看上去更没什么了得,但对于原创来说那毕竟是 4 年前。
谈到豆瓣我算是半个老用户了,豆瓣自身是个比较复杂的集合多条业务线分支(「小组」「同城」「阅读」「音影」……)的多生态产品,这里我们主要拿它 18 年 6.0 大改版中影音模块的详情页大改造来说事儿。
△ 可能有很多人已经忘记6.0前的豆瓣电影详情页长啥样了,带你回顾一下。
看完对比图,视力正常的朋友乍一看都能看出 6.0 版详情页整容得有多成功。但具体成功在哪里,可能不仅仅是好看这么简单。
大背景从海报上智能取色虽然不算是什么稀奇的做法,但是加了适度的渐变应用在这里也可以说是非常的恰到好处了。另外深底色和视觉比重加大的外链区都突显了「第三方播放」与「购票选座」的视觉感知。让用户沉浸在电影详情中并引导他们走向「豆瓣的主要收入来源之一——电影票分销与第三方视频播放产品引流」正好是 6.0 豆瓣改版一个「小小的目标」——更务实(商业化)。
从交互层面看,且不说评论头部吸底这个事情是不是也是因为 6.0 商业化的影响(评论区增加「话题」进行重点运营),这个交互本身我觉得还是很强大的。强大的体现在于良好的空间收纳能力与信息拓展能力。我给它起了个好听的名字叫-叠加上滑板(不好听也认了吧,毕竟也没有内部人员告诉我他们是不是起名字了)
这里可能又会有很多人质疑它与用户已洗脑的上滑手势之间的冲突,这点解释起来和上文淘宝「二楼」有些类似,区别是豆瓣并没有做上滑速度 or 距离的临界值,只是把滑动区域做了隔离。而对比它的效仿者 boss 直聘,人家倒是在交互上做了进一步优化,适配自己的产品情况做了上滑叠层卡隐藏和上滑距离临界值。
这个故事告诉我们,要抄也要抄得比人家的交互更优秀才不丢人昂。
文章来源:优设 作者:Nana的设计锦囊
蓝蓝设计的小编 http://www.lanlanwork.com