1 | const [state, setState] = useState(state) |
1 | import React, { useState } from 'react' |
每次渲染都是独立的闭包:当点击更新状态的时候,函数组件都会重新被调用,那么每次渲染都是独立的,取到的值不会受后面操作的影响。
惰性初始化 state:如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用。
useMemo会在组件第一次渲染时候执行一次,返回第一个参数函数的返回值。之后会在其依赖的变量发生改变时再次执行,也就是说useMemo只会在其中一个依赖项发生更改时重新计算memoizedValue的值。此优化有助于避免在每个渲染上进行昂贵的计算。
1 | const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]) |
用法
1 | import React, { useState, useMemo } from 'react' |
在上面的例子中,如果我们不使用useMemo,当我修改a的时候,exampleB组件也会触发更新,这是非常没有必要的,所以我们可以利用useMemo的特性,之后在其依赖发生变化时才重新触发更新,这对一直值的改变也可以生效。
useCallback跟useMemo比较类似,但它返回的是缓存的函数。
1 | // 这个fn函数会被缓存,只有当依赖发生变化时,fn函数才会重新去定义。 |
(state, action) => newState 的reducer
,并返回与 dispatch
方法配对的当前状态。1 | const [state, dispatch] = useReducer(reducer, initialState, initFn) |
用法
1 | import React, { useReducer } from 'react' |
1 | const value = useContext(MyContext) |
接受上下文对象(从中React.createContext返回的值)并返回该上下文的当前上下文值。当前上下文值由树中调用组件上方value最近的prop 确定<MyContext.Provider>。
用法
1 | // App.js |
当我们的函数具有副作用的操作,例如ajax,一般都是在 class
组件的 componentDidMount
或者 componentDidUpdate
等生命周期中进行操作。而在函数组件中是没有这些生命周期的概念的,只能 return
想要渲染的元素。 但是现在,在函数组件中也有执行副作用操作的地方了,就是使用 useEffect
函数。
该函数接受两个参数
1 | import React, { useState, useEffect } from 'react'; |
当我们函数返回一个函数时,就是说想要在componentWillMount
生命周期去进行事件的移除
Hook 允许我们按照代码的用途分离他们, 而不是像生命周期函数那样。React 将按照 effect 声明的顺序依次调用组件中的 每一个 effect。
1 | import React, { useState, useEffect } from 'react'; |
与 useEffect
Hooks 类似,都是执行副作用操作。但是它是在所有 DOM 更新完成后触发。可以用来执行一些与布局相关的副作用,比如获取 DOM 元素宽高,窗口滚动距离等等。
1 | import React, { useState, useLayoutEffect } from 'react'; |
1 | const refContainer = useRef(initialValue) |
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传递的参数(initialValue)。返回的对象将存留在整个组件的生命周期中。
1 | import React, { useState, useRef } from 'react'; |
自定义 Hook 是一种约定,而不是一种功能。如果函数的名字以 use 开头,并且调用了其他的 Hook,则就称其为一个自定义 Hook。
1 | import React, { useState } from 'react'; |
我们常说:vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
那么到底如何实现,我们先看看vue的核心流程,如图:
简单总结
首先我们要知道双向绑定的几个核心东西
Observe用来把data变成响应式
Dep是依赖管理器,每一个data属性都有一个dep来管理他们的依赖
Watcher本质上就是我们所说的依赖。
Compile函数用来编译模板,触发依赖收集
假设我们有下面这样的数据
1 | <body> |
接下来我们一步一步来实现吧~
1 | // MyVue.js |
上面的代码不难理解,关键是收集依赖这一步,这时候我们要实现一个Dep
Dep依赖管理器很简单,从上面可以看出它只有addDep添加依赖和notify通知依赖更新两个方法
1 | // Dep |
接下来我们来实现Watcher
Watcher(订阅者)本质上就是依赖,在上面的例子里,依赖就是模板中的那些标签。也就是说<p></p>
这个标签可以理解为是name属性的依赖,当我们去修改name时,去通知这个依赖重新渲染。
现在回到应该如何触发收集依赖,在上面的例子里,我们收集依赖是在get里实现,所有我们Watcher要主动去获取这个属性,这样依赖就被收集了
Watcher本质上就是依赖,在上面的例子里,依赖就是模板中的那些标签。也就是说,<p></p>
这个标签可以理解为是name属性的依赖,当我们去修改name时,我们要通知这个依赖重新渲染。
在收集依赖的过程,要触发get方法,所以我们可以知道,在Watcher里,要有一个key属性,去主动触发get收集。
1 | // Watcher |
上面就是Watcher函数的实现,现在再捋一遍代码。首先触发依赖收集是在编译里,编译的过程中,匹配到含有双括号、my-text、的这些标签,会分配给它们一个Watcher,这时就会触发Watcher的constructor方法,会先执行Dep.target = this把当前的Watcher赋值给Dep.target,再执行this.vm[this.key]主动触发key的get属性,收集依赖,最后再把Dep.target置为空,这样就把依赖收集了。由于js是单线程的,同一时间只有一个依赖会被收集,所以采用全局的Dep.target方法去实现是没有问题的。
其实Compile才是最关键的一步,它的作用就是把模板编译,收集依赖的过程。
1 | // Compile |
以上就是Compile的实现,其实大部分都是辅助函数,核心逻辑就是递归循环el的所有子节点,判断对应的插值表达式,执行对应的方法即可。update方法主要是更新dom,以及收集依赖。
最后,我们只要在new MyVue时候初始化Compile函数即可。
1 | <html lang="en"> |
1 | // Myvue.js |
1 | // compile.js |
其实写代码、写框架、任何事情都有套路,设计模式,就是写代码中的常⻅见套路,有些写法可能我们⽇常都在使⽤,只是我们没有发觉,下面总结一下前端相关的设计模式。
定义:在这种模式中,是一种一对多的依赖关系,当一个对象特定状态发生改变后,所有依赖它的对象都将会得到通知,这种模式一般以事件对象的形式传递消息。
订阅者(Subscriber)把自己要订阅的事件注册(Subscribe)到调度中心(Topic),当发布者(Publisher)发布该事件(Publish topic)到调度中心,也就是该事件触发时,再由调度中心通过事件回调通知订阅者。
ps: js里的dom事件监听,vue中的 on源码都是使用这样的模式;优点:减少代码耦合。
定义:保证⼀个类仅有⼀个实例,并提供⼀个访问它的全局访问点。实现的⽅法为先判断实例存在与否,如果存在则直接返回,如果不存在就创建了再返回,这就确保了⼀个类只有 ⼀个实例对象。
element、adtd大部分的弹窗组件,都是使用这种单例模式,保证全局唯一。
1 | var Singleton = function (name) { |
定义:定义⼀系列的算法,把他们⼀个个封装起来,并且使他们可以相互替换。
比如说,我们有一个需求是根据员绩效来计算获得的股权比例,绩效为B的能拿到的股权为0.04,绩效为A的能拿到0.08。。。
1 | // 正常写法 |
这种写法可以让代码更加整洁,如果后期有新增不同的等级,只需加上对应的策略就可以,常见应用:表单验证。
element-ui里的表单组件示例就是使用这种模式。
1 | <template> |
定义:为⼀个对象提供⼀个代⽤品或占位符,以便控制对它的访问。
常见示例:
定义:通过⼀个中介者对象,其他所有的相关对象都通过该中介者对象来通信,⽽不是相互引⽤,当其中的⼀个对象发⽣改变时,只需要通知中介者对象即可。通过中介者模式可以解除对象与对象之间的紧耦合关系。可以有效的是减少耦合。
常见示例:
定义:在不改变对象⾃身的基础上,在程序运⾏期间给对象动态地添加⽅法。
常见示例:
1 | class CustomizedForm extends React.Component {} |
定义:调用一个方法,实际上多个方法被调用。
常见示例:
定义:提供创建对象的接⼝,把成员对象的创建⼯作转交给⼀个外部对象,好处在于消除对象之间的耦合。
常见示例:
1 | function createInstance (name,age){ |
享元(flyweight)模式是⼀种⽤于性能优化的模式,“fly”在这⾥是苍蝇的意思,意为蝇量级。享元模 式的核⼼是运⽤共享技术来有效⽀持⼤量细粒度的对象。 如果系统中因为创建了⼤量类似的对象 ⽽导致内存占⽤过⾼,享元模式就⾮常有⽤了。在 JavaScript 中,浏览器特别是移动端的浏览器 分配的内存并不算多,如何节省内存就成了⼀件⾮常有意义的事情。
常见示例:
下面用一个图书馆来做示例:每一本书都有唯一的ISBN,Author、Title、checkoutDate、checkoutMember等属性。
1 | // BOOk的构造函数 |
程序刚开始可能没问题,但是随着时间的增加,图书可能大批量增加,并且每种图书都有不同的版本和数量,你将会发现系统变得越来越慢。几千个book对象在内存里可想而知,我们需要用享元模式来优化。
优化:我们可以将数据分成内部和外部两种数据,和book对象相关的数据(title, author 等)可以归结为内部属性,而(checkoutDate, checkoutMember等)可以归结为外部属性。这样,同一本书里共享同一个对象了,因为不管谁借的书,只要书是同一本书,基本信息是一样的:
1 | // 享元模式优化代码 |
通过这种方式,我们做到了将同一种图书的相同信息保存在一个bookmanager对象里,而且只保存一份。
实现享元模式的关键是把内部状态和外部状态分离开来。有多少种内部状态的组合,系统中便最多存在多少个共享对象,而外部状态储存在共享对象的外部,对于一个程序使用了大量的相似对象有很大的优化。
职责链模式是个链式结构,请求在链中的节点之间依次传递,直到有一个对象能处理该请求为止。如果没有任何对象处理该请求的话,那么请求就会从链中离开。
假设有一个售卖⼿机的业务,经过分别交纳 500 元定⾦和 200 元定⾦的两轮预定后(订单已 在此时⽣成),现在已经到了正式购买的阶段。平台会针对⽀付过定⾦的⽤户有⼀定的优惠政策,需求如下
1 | /** |
上面代码使用臃肿,下面我们用职责链模式重构这段代码
1 | var order500 = function(orderType, pay, stock){ |
通过改进,我们可以⾃由灵活地增加、移除和修改链中的节点顺序
将一个类(对象)的接口(方法或者属性)转化成另外一个接口以满足用户需求,使类(对象)之间接口的不兼容问题通过适配器得以解决。
示例:
1 | /** |
1 | Function.prototype.myCall = function (content = window, ...rest) { |
实现原理:
fn
,值为调用myCall
的对象。content里的fn()
方法,此时this
指向已发生改变,指向content
。myCall
方法里的对象临时存在content.fn
里,然后在content
里执行调用,改变this执行。1 | Function.prototype.myApply = function (content = window, rest) { |
实现原理:同上,只是参数获取的方式不同。
1 | Function.prototype.myBind = function (content = window, ...rest) { |
bind方法返回的是一个函数,而且这个函数也可以继续接受参数。
实现原理:
this
为than
,定义bindFn
函数,返回myCall
改变this
指向。bindFn
函数原型,返回bindFn
。1 | function debounce (fn, wait) { |
1 | function throttle (fn, wait) { |
1 | // 该方法没有考虑一些边缘情况,实际上更推荐使用loadash函数 |
1 | // 原理:能在实例的原型对象链中找到该构造函数的prototype属性所指向的 原型对象,就返回true |
1 | // 构造器函数 |
对于创建一个对象来说,更推荐使用字面量的方式创建对象(无论性能上还是可读性)。因为你使用
new Object()
的方式创建对象需要通过作用域链一层层找到Object
,但是你使用字面量的方式就没这个问题。
1 | function Parent () { |
1 | function curry(fn, args) { |
1 | function fibonacci (n) { |
优化的方法可以利用数组的把值缓存下来
1 | let fibonacci = function() { |
另外使用动态规划的空间复杂度更低,也是更推荐的解法
1 | function fibonacci(n) { |
compose的返回值还是一个函数,像’洋葱圈’似的,由内向外,逐步调用。
1 | function compose(...funcs) { |
1 | class Vue { |
1 | // 简单版 |
1 | function findMaxRepeatStr (testStr) { |
1 | /** |
1 | /** |
1 | /** |
1 | /** |
vw
来做移动端适配,是目前比较流行的做法。(来自大漠老师的文章)如何在Vue项目中使用vw实现移动端适配直入主题,我们先实现效果,再细说具体的内容。
1 | yarn add |
postcss.config.js
1 | module.exports = { |
index.html
1 | <meta |
在实际撸码过程,不需要进行任何的计算,直接在代码中根据UI稿写px, 如
1 | .test { |
编译出来的CSS:
1 | .test { |
1 | // utils.js |
解决@import
引入路径问题, 配合postcss-url
让你引入文件变得更轻松
处理文件,比如图片文件、字体文件等引用路径
让你在编码时不再需要考虑任何浏览器前缀的问题,可以专心撸码
压缩和清理CSS代码。在webpack中,cssnano和css-loader捆绑在一起,所以不需要自己加载它. 要将postcss-zindex设置为false, 否则z-index的值就会重置为1
插件主要用来把px单位转换为vw、vh、vmin或者vmax这样的视窗单位,也是vw适配方案的核心插件之一。
在配置中需要配置相关的几个关键参数:
1 | "postcss-px-to-viewport": { |
处理元素容器宽高比
处理移动端1px的解决方案,该插件主要使用的是border-image和background来做1px的相关处理
以下内容转自:Vue 项目性能优化 — 实践指南(网上最全 / 详细)(部分转载,省略掉部分内容。)
Vue 框架通过数据双向绑定和虚拟 DOM 技术,帮我们处理了前端开发中最脏最累的 DOM 操作部分, 我们不再需要去考虑如何操作 DOM 以及如何最高效地操作 DOM;但 Vue 项目中仍然存在项目首屏优化、Webpack 编译配置优化等问题,所以我们仍然需要去关注 Vue 项目性能方面的优化,使项目具有更高效的性能、更好的用户体验。
本文内容分为以下部分组成:
v-if是真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
v-show就简单得多,不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 display 属性进行切换。
所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。
computed:是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;
watch:更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;
运用场景:
(1)v-for 遍历必须为 item 添加 key
在列表数据进行遍历渲染时,需要为每一项 item 设置唯一 key 值,方便 Vue.js 内部机制精准找到该条列表数据。当 state 更新时,新的状态值和旧的状态值对比,较快地定位到 diff 。
(2)v-for 遍历避免同时使用 v-if
v-for 比 v-if 优先级高,如果每一次都需要遍历整个数组,将会影响速度,尤其是当之需要渲染很小一部分的时候,必要情况下应该替换成 computed 属性。
推荐:
1 | <ul> |
不推荐:
1 | <ul> |
Vue 会通过 Object.defineProperty 对数据进行劫持,来实现视图响应数据的变化,然而有些时候我们的组件就是纯粹的数据展示,不会有任何改变,我们就不需要 Vue 来劫持我们的数据,在大量数据展示的情况下,这能够很明显的减少组件初始化的时间,那如何禁止 Vue 劫持我们的数据呢?可以通过 Object.freeze 方法来冻结一个对象,一旦被冻结的对象就再也不能被修改了。
1 | export default { |
Vue 组件销毁时,会自动清理它与其它实例的连接,解绑它的全部指令及事件监听器,但是仅限于组件本身的事件。 如果在 js 内使用 addEventListene 等方式是不会自动销毁的,我们需要在组件销毁时手动移除这些事件的监听,以免造成内存泄露,如:
1 | created() { |
对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载。这样对于页面加载性能上会有很大的提升,也提高了用户体验。我们在项目中使用 Vue 的 vue-lazyload 插件。
Vue 是单页面应用,可能会有很多的路由引入 ,这样使用 webpcak 打包后的文件很大,当进入首页时,加载的资源过多,页面会出现白屏的情况,不利于用户体验。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应的组件,这样就更加高效了。这样会大大提高首屏显示的速度,但是可能其他的页面的速度就会降下来。
路由懒加载:
1 | const Answer = (resolve) => { |
我们在项目中经常会需要引入第三方插件(UI组件库),如果我们直接引入整个插件,会导致项目的体积太大,我们可以借助 babel-plugin-component ,然后可以只引入需要的组件,以达到减小项目体积的目的。
如果你的应用存在非常长或者无限滚动的列表,那么需要采用窗口化的技术来优化性能,只需要渲染少部分区域的内容,减少重新渲染组件和创建 dom 节点的时间。 你可以参考以下开源项目 vue-virtual-scroll-list和vue-virtual-scroller 来优化这种无限列表的场景的。
服务端渲染是指 Vue 在客户端将标签渲染成的整个 html 片段的工作在服务端完成,服务端形成的 html 片段直接返回给客户端这个过程就叫做服务端渲染。
(1)服务端渲染的优点:
(2)服务端渲染的缺点:
如果你的项目的 SEO 和 首屏渲染是评价项目的关键指标,那么你的项目就需要服务端渲染来帮助你实现最佳的初始加载性能和 SEO,具体的 Vue SSR 如何实现,可以参考作者的另一篇文章《Vue SSR 踩坑之旅》。如果你的 Vue 项目只需改善少数营销页面(例如 /, /about,/contact等)的 SEO,那么你可能需要预渲染,在构建时 (build time) 简单地生成针对特定路由的静态 HTML 文件。优点是设置预渲染更简单,并可以将你的前端作为一个完全静态的站点,具体你可以使用prerender-spa-plugin就可以轻松地添加预渲染 。
在 vue 项目中除了可以在webpack.base.conf.js
中url-loader
中设置 limit 大小来对图片处理,对小于 limit 的图片转化为 base64 格式,其余的不做操作。所以对有些较大的图片资源,在请求资源的时候,加载会很慢,我们可以用image-webpack-loader
来压缩图片:
(1)首先,安装 image-webpack-loader :
npm install image-webpack-loader --save-dev
(2)然后,在 webpack.base.conf.js 中进行配置:
1 | { |
如果项目中没有去将每个页面的第三方库和公共模块提取出来,则项目会存在以下问题:
所以我们需要将多个页面的公共代码抽离成单独的文件,来优化以上问题 。Webpack 内置了专门用于提取多个Chunk 中的公共部分的插件 CommonsChunkPlugin,我们在项目中 CommonsChunkPlugin 的配置如下:
1 | // 所有在 package.json 里面依赖的包,都会被打包进 vendor.js 这个文件中。 |
当使用 DOM 内模板或 JavaScript 内的字符串模板时,模板会在运行时被编译为渲染函数。通常情况下这个过程已经足够快了,但对性能敏感的应用还是最好避免这种用法。
预编译模板最简单的方式就是使用单文件组件——相关的构建设置会自动把预编译处理好,所以构建好的代码已经包含了编译出来的渲染函数而不是原始的模板字符串。
如果你使用 webpack,并且喜欢分离 JavaScript 和模板文件,你可以使用 vue-template-loader,它也可以在构建过程中把模板文件转换成为 JavaScript 渲染函数。
我们知道JavaScript语言的执行是单线的。单线程,就是指一次只执行一个任务,如果有多个任务,就必须排队,前面一个任务完成,才能执行后面任务。
这种模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等待,会拖延整个程序的执行。
但是js的特性异步、非阻塞,解决了上述的问题。js将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)。
JavaScript执行机制:
当我们调用一个方法的时候,js会生成一个与这个方法相对应的执行环境,也叫执行上下文,这个执行环境存在着这个方法的私有作用域、参数、this对象等等。因为js是单线程的,同一时间只能执行一个方法,所以当一系列的方法被依次调用的时候,js会先解析这些方法,把其中的同步任务按照执行顺序排队到一个地方,这个地方叫做执行栈。
JavaScript是单线程的,那么这个单线程就成为主线程。而事件循环在主线程执行完执行栈代码后,才执行的。所以主线程代码执行时间过长,会阻塞事件循环的执行。只有当执行栈为空的时候(同步代码执行完毕),才会执行事件循环来观察有哪些事件回调需要执行,当事件循环检测到任务队列有事件就读取出回调放到执行栈由主线程执行。
任务队列也有时称叫消息队列、回调队列。
Js 中,有两类任务队列:宏任务队列(macro tasks)和微任务队列(micro tasks)。宏任务队列可以有多个,微任务队列只有一个。那么什么任务,会分到哪个队列呢?
浏览器的 Event Loop 遵循的是 HTML5 标准,而 NodeJs 的 Event Loop 遵循的是 libuv。 有所区别。
Event Loop(事件循环)中,每一次循环称为 tick, 每一次tick的任务如下:
ps:setTimeout,setInterval,其回调函数会被放到下一个宏任务队列中。
1 | console.log('1'); |
上面代码执行过程:
第一轮事件循环
第二轮事件循环
第三轮事件循环与第二轮一样,输出:6 7 8
事件循环发现所有任务都已经处理完毕,此时程序执行结束
全部的输出:1 5 9 2 3 4 6 7 8
JavaScript运行机制-事件循环与任务队列
总结:JavaScript异步、事件循环与消息队列、微任务与宏任务
JavaScript 异步、栈、事件循环、任务队列
JavaScript运行机制和事件循环
浏览器与服务器通信的方式为应答模式,即是:浏览器发起HTTP请求 – 服务器响应该请求。那么浏览器第一次向服务器发起该请求后拿到请求结果,会根据响应报文中HTTP头的缓存标识,决定是否缓存结果,是则将请求结果和缓存标识存入浏览器缓存中。
浏览器缓存位置分为四种,其优先级顺序如下:
Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。
Memory Cache 即内存中的缓存,其特点是容量小、读取高效、持续性短,会随着进程的释放而释放。
Disk Cache 即磁盘中的缓存,其特点是容量大、读取缓慢、持续性长,任何资源都能存储到磁盘中。
浏览器会把哪些文件丢进内存中?哪些丢进硬盘中?
对于大文件来说,大概率是不存储在内存中的,反之优先.
当前系统内存使用率高的话,文件优先存储进硬盘
Push Cache(推送缓存)是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂,在Chrome浏览器中只有5分钟左右,同时它也并非严格执行HTTP头中的缓存指令。
总结特点:
如果以上四种缓存都没有命中的话,那么只能发起请求来获取资源了。
浏览器每次在向服务器发起 HTTP 请求获得资源后,可能会根据不同情况(可能是代码控制如 Service Worker、Push Cache,也可能是根据 HTTP Header 的缓存标识字段)将资源缓存起来。
浏览器缓存策略分为强制缓存和协商缓存,是通过设置 HTTP Header 来实现的。
强制缓存就是向浏览器缓存查找该请求结果,并根据该结果的缓存规则来决定是否使用该缓存结果的过程。强缓存可以通过设置两种 HTTP Header 实现:Expires 和 Cache-Control;强制缓存的情况主要有三种
这是 HTTP 1.0 的字段,表示缓存到期时间,是一个绝对的时间 (当前时间+缓存时间),如
1 | Expires: Thu, 10 Nov 2017 08:45:11 GMT |
如果客户端的时间小于Expires的值时,直接使用缓存结果,不需要再次请求。
缺点:
已知Expires的缺点之后,在HTTP/1.1中,增加了一个字段Cache-control,该字段表示资源缓存的最大有效时间(相对值),在该时间内,客户端不需要向服务器发送请求。
指令 | 作用 |
---|---|
public | 所有的内容都可以被缓存 (包括客户端和代理服务器, 如 CDN) |
private | 所有的内容只有客户端才可以缓存,代理服务器不能缓存。默认值。 |
max-age | 最大有效时间(max-age=30表示缓存30秒后就过期。) |
s-maxage | 覆盖max-age,作用一样,只在代理服务器中生效。 |
must-revalidate | 如果超过了 max-age 的时间,浏览器必须向服务器发送请求,验证资源是否还有效。 |
no-store | 所有内容都不走缓存,包括强制和协商缓存。 |
no-cache | 资源被缓存,但是立即失效,下次会发起请求验证资源是否过期 既要求客户端缓存内容的,只是是否使用这个内容由后续的对比来决定。 |
max-stale=30 | 30秒内,即使缓存过期吗,也使用该缓存。 |
min-fresh=30 | 希望在30秒内获取最新的响应。 |
PS:这些值可以混合使用,例如 Cache-control:public, max-age=2592000
Expires 是http1.0的产物,Cache-Control是http1.1的产物,两者同时存在的话,Cache-Control优先级高于Expires
协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程;协商缓存的情况主要有二种
协商缓存可以通过设置两种 HTTP Header 实现:Last-Modified 和 ETag 。
过程:
1.服务器通过 Last-Modified 字段告知客户端,资源最后一次被修改的时间,例如Last-Modified: Mon, 10 Nov 2018 09:10:11 GMT
2.浏览器将这个值和内容一起记录在缓存数据库中。
3.下一次请求相同资源时,会在请求头中将上次的Last-Modified
的值写入到请求头的If-Modified-Since
字段。
4.服务器会将If-Modified-Since
的值与Last-Modified
字段进行对比。如果相等,则表示未修改,响应304;反之,则表示修改了,响应 200 状态码,并返回数据。
缺陷:
Etag是服务器端在响应请求时用来说明资源在服务器端的唯一标识。与之对应的是 If-None-Match字段,在协商缓存过程中,浏览器发送的 HTTP 请求中 Header 中会带上 If-None-Match 字段,值为该缓存资源 Etag 属性的值。
当服务器端接收到带有 If-None-Match 的请求时,则会将 If-None-Match 的值与被请求资源的唯一标识做对比。如果相同,说明资源没有新的修改,则响应 HTTP Status Code 304,浏览器会继续使用缓存资源;如果不同,则说明资源被修改过,则响应 HTTP Status Code 200,并返回最新的资源。
下面用一张流程图来完整说明当浏览器发起 HTTP 请求时缓存机制的过程:
Cache-Control: no-cache
对于频繁变动的资源,首先需要使用Cache-Control: no-cache
使浏览器每次都请求服务器,然后配合 ETag 或者 Last-Modified 来验证资源是否有效。这样的做法虽然不能节省请求数量,但是能显著减少响应数据大小。
Cache-Control: max-age=31536000
通常在处理这类资源时,给它们的 Cache-Control 配置一个很大的 max-age=31536000 (一年),这样浏览器之后请求相同的 URL 会命中强制缓存。而为了解决更新的问题,就需要在文件名(或者路径)中添加 hash, 版本号等动态字符,之后更改动态字符,从而达到更改引用 URL 的目的,让之前的强制缓存失效 (其实并未立即失效,只是不再使用了而已)。
在线提供的类库 (如 jquery-3.3.1.min.js, lodash.min.js 等) 均采用这个模式。
所谓用户行为对浏览器缓存的影响,指的就是用户在浏览器如何操作时,会触发怎样的缓存策略。主要有 3 种:
在项目里,产品经理需要统计产品用户使用行为,使用习惯,从而去了解用户群体。升级和迭代产品,使其更加贴近用户。前端也需要实现性能监控和异常监控,从而优化代码,处理异常问题等。
前端监控可以分为三类:数据监控、性能监控和异常监控。
每一个决策,每一个迭代都需要分析各种数据,数据中往往会有我们需要的答案。数据监控,就是监听用户的行为,常见的监控项有:
性能监控指的是监听前端的性能,主要包括监听网页或者说产品在用户端的体验。常见的性能监控项包括:
由于产品的前端代码在执行过程中也会发生异常,报错受网络,机型,业务逻辑影响而且大部分错误难以还原现场。及时的上报异常情况,可以避免线上故障的继续发生。
面对用户的反馈,开发经常感到困惑:到底有多卡,哪个步骤卡?是个别现象还是大面积都受到了影响?白屏时页面请求的返回码是多少?是被运行商劫持还是CDN出了问题?能让用户用Charles配合抓个包么?如何做有针对性的优化?优化的结果怎么去衡量?
为了解决这些痛点,我们需要对客户端服务进行基于用户行为的监控。
这里以首屏时间为例,这里有一种可行的测量方案,准确率在99成以上。
备注:在做时间相关测量时,不能使用setTimeout和setInterval方法,因为在单线程执行引擎中,异步队列的执行是不能确保执行时间的。
1 |
|
要实现前端监控,前端就需要埋点和上报数据、后端数据处理及分析。
目前常见的前端埋点方法分为三种:代码埋点、可视化埋点和无埋点(全埋点)。
代码埋点,就是以嵌入代码的形式进行埋点,比如需要监控用户的点击事件,会选择在用户点击时,插入一段代码,保存这个监听行为或者直接将监听行为以某一种数据格式直接传递给服务器。
这种方式容易入侵业务代码。
通过可视化交互的手段,代替上述的代码埋点。将业务代码和埋点代码分离,提供一个可视化交互的页面,输入为业务代码,通过这个可视化系统,可以在业务代码中自定义的增加埋点事件等等,最后输出的代码耦合了业务代码和埋点代码。缺点就是可以埋点的控件有限,不能手动定制。
埋点并不是说不需要埋点,而是全部埋点,前端的任意一个事件都被绑定一个标识,所有的事件都别记录下来。通过定期上传记录文件,配合文件解析,解析出来我们想要的数据,并生成可视化报告供专业人员分析因此实现“无埋点”统计。
埋点方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
代码埋点 | 可以按照业务上报详细 定制化的数据 | 需要开发人员参与,更新维护成本高,无法获得历史数据 | 对上下文理解要求较高的业务数据 |
全埋点 | 对发人员依赖低,仅需嵌入一次SDK 可以全量上报通用数据,拿到历史数据 | 数量量太大,占用更多资源 无法收集业务上下文数据 给后续数据筛选和分析带来一定的难度 | 上下文相对独立的、通用的数据 |
可视化埋点 | 对开发人员依赖低 可以按照业务需求上报数据 对上下文数据有一定收集能力 | 标记事件有一定的操作难度,事件需要被更新时无法获得历史数据,界面变化时标记的元素可能失效 | 业务上下文数据相对简单,操作交互比较固定的界面 |
PS:现在一般都使用无埋点。
PV(page view)即页面浏览量或点击量,是衡量一个网站或网页用户访问量。具体的说,PV值就是所有访问者在24小时(0点到24点)内看了某个网站多少个页面或某个网页多少次。PV是指页面刷新的次数,每一次页面刷新,就算做一次PV流量。
IP可以理解为独立IP的访问用户,指1天内使用不同IP地址的用户访问网站的数量,同一IP无论访问了几个页面,独立IP数均为1。但是假如说两台机器访问而使用的是同一个IP,那么只能算是一个IP的访问。
是指通过互联网访问、浏览这个网页的自然人。访问您网站的一台电脑客户端为一个访客。00:00-24:00内相同的客户端只被计算一次。一天内同个访客多次访问仅计算一个UV。
Performance
接口可以获取到当前页面中与性能相关的信息,下面对Performance
的属性一一注释
Performance下有四个属性
1 | Performance.navigation = { |
performance.getEntries() 获取静态资源的数组列表
performance.getEntriesByName(name) 根据资源的name获取相应的数据(如上图中的name)
performance.getEntriesByType(entryType) 根据资源的name获取相应的数据(如上图中的entryType)
1 | const t = performance.timing |
1 | const entries = window.performance.getEntries() // 这个函数返回的将是一个数组,包含了页面中所有的 HTTP 请求 |
在讨论优化页面性能、提高页面加载速度的之前,我们先了解下浏览器渲染页面的原理及过程。
先看看浏览器工作大流程
1 | HTML Source CSS |
浏览器从上到下读取标签,把他们分解成节点,从而创建 DOM 。
当浏览器发现任何与节点相关的样式时,比如:外部,内部,或行内样式,立即停止渲染 DOM ,并利用这些节点创建 CSSOM。这就是 CSS “渲染阻塞“ 的由来。
CSSOM 的构建会阻塞页面的渲染,因此我们想尽早加载样式。
浏览器不断构建 DOM / CSSOM 节点,直到发现外部或者行内的脚本。
由于脚本可能需要访问或操作之前的 HTML 或样式,我们必须等待它们构建完成。
因此浏览器必须停止解析节点,完成构建 CSSOM,执行脚本,然后再继续。这就是 JavaScript 被称作“解析器阻塞”的原因。脚本只能等到先前的 CSS 节点构建完成。
一旦所有节点已被解析,DOM 和 CSSOM 准备合并,浏览器便会构建渲染树。
布局阶段需要确定页面上所有元素的大小和位置。
最终的渲染阶段,会真正地光栅化屏幕上的像素,把页面呈现给用户。
注意:
当Render Tree中元素的规模尺寸,布局,隐藏等改变而需要重新构建。这就称为回流。每个页面至少需要一次回流,就是在页面第一次加载的时候。回流会从html这个root frame开始递归往下,依次计算所有的结点几何尺寸和位置。
屏幕的一部分要重画,比如某个CSS的背景色变了。但是元素的几何尺寸没有变时,该过程成为重绘。
回流必将引起重绘,而重绘不一定会引起回流。
回流发生的情况:
回流是一个很耗性能的过程。我们要提供渲染性能,就要减少回流和重绘的操作。
要尽可能早的加载样式,尽可能晚的加载脚本。原因是脚本执行之前,需要 HTML 和 CSS 解析完成,因此,样式尽可能的往顶部放,当底部脚本开始执行之前,样式有足够的时间完成计算。
直接使用script,html会按照顺序来加载并执行脚本,在脚本加载执行的过程中,会阻塞后续的DOM渲染。所以script提供了async和defer属性来解决上述问题。
异步加载的方式:defer、async。
defer
1 | <script type="text/javascript" src="2.js" defer ></script> |
就算1.js加载用时比2.js短,但因为defer的限制,所以1.js只能等前边的脚本执行完毕后才能执行
async
1 | <script type="text/javascript" src="2.js" async ></script> |
注意:DOMContentLoaded事件的触发并不受async脚本加载的影响,在脚本加载完之前,就已经触发了DOMContentLoaded
async的执行是加载完成就会去执行,而不像defer那样要等待所有的脚本加载完后按照顺序执行。
普通script
文档解析的过程中,如果遇到script脚本,就会停止页面的解析进行下载(但是Chrome会做一个优化,如果遇到script脚本,会快速的查看后边有没有需要下载其他资源的,如果有的话,会先下载那些资源,然后再进行下载script所对应的资源,这样能够节省一部分下载的时间)。
资源的下载是在解析过程中进行的,虽然1.js脚本会很快的加载完毕,但是他前边的2.js并没有加载执行,所以他只能处于一个挂起的状态,等待2.js执行完毕后再执行。
当这两个脚本都执行完毕后,才会继续解析页面。
defer
文档解析时,遇到设置了defer的脚本,就会在后台进行下载,但是并不会阻止文档的渲染,当页面解析&渲染完毕后。
会等到所有的defer脚本加载完毕并按照顺序执行,执行完毕后会触发DOMContentLoaded事件。
async
async脚本会在加载完毕后执行。
async脚本的加载不计入DOMContentLoaded事件统计。
通过将静态资源(例如javascript,css,图片等等)缓存到离用户很近的相同网络运营商的CDN节点上,不但能提升用户的访问速度,还能节省服务器的带宽消耗,降低负载。
资源预加载是另一个性能优化技术,我们可以使用该技术来预先告知浏览器某些资源可能在将来会被使用到。
例如,我们将来可从 baidu.com 获取图片或音频资源,那么可以在文档顶部的 标签中加入以下内容。
1 | <meta http-equiv="x-dns-prefetch-control" content="on"> |
1 | <link rel="dns-prefetch" href="//baidu.com"> |
以下内容转自:https://www.cnblogs.com/Wayou/p/goodui.html
英文原文地址:https://goodui.org/
单列布局能够让对全局有更好的掌控。同时用户也可以一目了然内容。而多列而已则会有分散用户注意力的风险使你的主旨无法很好表达。最好的做法是用一个有逻辑的叙述来引导用户并且在文末给出你的操作按钮。
给用户一份精美小礼品这样的友好举动再好不过了。具体来讲,送出礼品也是之有效的获得客户忠诚度的战术,这是建立在人们互惠准则上的。而这样做所带来的好处也是显而易见的,会让你在往后的活动进展(不管是推销,产品更新还是再次搞活动)中更加顺利。
在整个产品开发期间我们会有意无意地创建很多模块,版面或者元素,而它们的功能可能有些是重叠的。此种情况表明界面已经过度设计了。时刻警惕这些冗余的功能模块,它无用且降低了电脑性能。此外,界面上模块越多,用户的学习成本就越大。所以请考虑重构你的界面使它足够精简。
在获得项目机会或提高项目转化率时客户的好评是一种极为有效的手段。当潜在客户看到其他人对你的服务给予好评时,项目机会会大增。所以试着提供一些含金量高的证据证明这些好评是真实可信的。
多次重复主旨口号这种方法适用于界面很长或者分页的情况。首先你肯定不想满屏刷出相同的信息,这样会让人生厌。但当页面足够长的时候这些重复就显示自然多了并且也不显得拥挤。所在在页面顶部放一个按钮然后在页面底部再适当放个突出的按钮的做法没有什么不妥。这样当用户到达页面底部在思考接下来该做什么的时候,你提供的按钮就可以获得一个潜在的合同或者即使用户不需要你的服务这个按钮也可以起到过滤的作用。
诸如颜色,层次及模块间的对比这些视觉上的设计可以很好地帮助用户使用产品:他时刻知道当前所处的页面以及可以转到哪些页面。要传达这样一个好的界面,你就需要将可点击的元素(比如连接,按钮),可选择的元素(比如单选多选框)以及普通的文字明显区分开来。在下图的例子中,我将点击操作的元素设置为蓝色,选中的当前元素为黑色。这样适当的设计可以让用户很方面地在产品的各模块间切换。但千万不要把这三种元素设计得混乱不堪。
当展示许多项服务时,给出一个重磅的推荐项是个不错的做法,尽管推荐的设置无法满足所有用户。这么做是有理论依据的,一些研究已经揭示了这么一种现象:当面临的选择越多时,用户就越难做出决定。所以你可以高亮某个选项来帮助用户做出选择。
假设你刚点击了一个连接或者按钮,撤销操作可以让操作流畅自然,这也符合人类的本能。而每次操作都弹一个确定框则好像是在质问用户你明白不明白这个操作会产生什么后果。我还是更习惯假设用户每次操作都是正确的,其实只有极少数情况下才会发生误操作。所以,为了防止误操作而设计的确认窗口其实是不人性化的,用户每次操作都需要进行毫无意义的确定。所以请考虑在你的产品里实现撤销操作来增加用户的操作友好度吧。
你是想把产品做成大众化的呢还是有精确的适用人群?在产品定位上你需要更精确些。通过不断了解目标客户的需求及标准,你能把产品做得更好得到更多与客户交流的机会,并且让客户觉得你很专业,在这方面是独家提供的优质服务。把产品定位得精确的风险就是可能缩小了目标潜在客户的范围,也使自身变得不那么全能。但这种做得更专业的精神却反过来会赢得信任,权威。
你可以通过不确定而颤抖的声音来表达传递自己的意思,当然也可以通过很自信的方式表达。如果你在界面中的表述用语多以问号结束,比如”也许”,”可能”,”感兴趣?” 或者”想要试试么?”,那么你完全还可以把语气变得更坚定一些。不过万事无绝对,或许适当放松措词让用户有自行思考的余地也是可以的。
把主要功能区从界面中突出显示出来效果会好很多。使你的主要口号醒目有很多种方法。通过明暗色调的对比来突显。通过为元素添加阴影渐变等效果让界面富有层次感来张显主题。最后,你甚至可以在色相环上专门选择互补色(比如黄色与紫色)来设计你的界面,以达到突出重心的目的。综合所有这些,最后得到的界面会使你的主要意图与界面其他元素有明显的区分,得到完美的呈现。
指明你的地区,所提供的服务,产品来自哪里意义重大,同时也将与客户的沟通引入了一个更具体带有地域特色的场景中。指出具体来自哪里,国家,省分及城市,也是一种在进行自我介绍或产品展示时被常常提及的。当你在界面设计中实现这点时,让人觉得非常友好。同时指明区域也会隐形提高产品的声誉,好上加好。
人生性就懒惰,在填写表单时也是同样的道理,没人愿意填写一大堆表单字段。表单中每个字段都会有失去用户的风险。不是每个人打字都很快速的,并且在移动设备上进行输入更是相当麻烦的事情。问下自己表单中是不是每个字段都必需,然后尽量减少表单中的字段。如果你确实需要一大堆信息让用户填写,试着将它们分散在不同页面,在表单提交后还可以继续补充。过多字段很容易让整个表单显示臃肿,当然想简洁也很容易,只放少数字段。
你使用的任何一个下拉框都会对用户造成信息的隐藏而需要额外的操作才能显示。如果这些信息是贯穿整个操作所必需的,那你最好把它展示出来做得更显而易见一点。下拉框最好用在选择日期,省份等约定俗成的地方。对于程序中重要的选项最好还是不要做成下拉形式。
一个平淡无奇行文无疑会让用户失去兴趣而继续阅读。是的,单列滚动的长页面是不错的,但是我们应该适当地设置一些小节,并且环环相扣,来提高用户的兴趣使其继续阅读。但需要注意的是节与节之间的留白不要太大。
为了满足各式用户的需求,在页面上放些链接链到这里链到那里是常见的做法。如果你的主要目的是想让用户点击页面最后那个下载按扭什么的话,就需要三思了。因为用户可能点击了其他链接离开页面了。所以你需要注意页面的链接数量,最好将用于导航与用于操作的链接用样式区分开。尽量移除页面不需要的链接会让用户点击到你的功能按钮。
现如今大多界面当中已经呈现了各色样式的进度条或者标明状态的图标,比如邮件有已读或未读的状态,电子帐单有支付或未支付的状态。而在界面上呈现这样的状态对于用户来说是很有必要的。这样用户就可以知道某些操作是否成功,接下来准备进行怎样的操作。
试想界面上有这样两个按钮:一个是”获取折扣”,另一个是”立即注册”。我敢打赌大多数人会点击第一个,因为第二个按扭让人感觉不到有利可图,并且”注册”让人联想到填不完的表单。也就是说让用户感受到获利的按钮更有可能被点击。这种让用户感到好处的文字信息也可放在按钮旁边,不一定要做为按钮的标题。当然,正常的按钮还是有用处的,一般用于重复性操作频繁的地方。
不用说直接在元素身上进行操作是更直观明了的方式。比如在一个列表中,我们想让用户对每个条目进行操作那么就把按钮放到当前条目上,而不要把放到列表之外。再比如就是直接点击元素就进入编辑状态(比如页面上的地址信息点击后可以进行编辑)。这种方式比传统的选中再点击相应的按钮进行操作要简洁省事得多。当然,对于一般性的操作本身就不需要有什么上下文的,就没必要这么做了,比如页面上的前进,后退按扭。
在一个足够大的宽屏界面上最好还是直接给出表单,这比点击按钮再弹出表单要好很多。首先减少了点击操作,流程变得简洁也节省了时间。其次,直接呈现出表单可以让用户知道表单有多长,其实也是在告诉用户注册花不了多少时间。当然,这条规则适合注册表单非常简单的情况。
用户进行操作过程中,界面上的元素会经常出现,隐藏,打开,关闭,放大缩小移位等。给这些元素增加些自然的动画,淡入淡出效果不但美观,也更符合实际,本来元素尺寸位置的变化就是一个需要时间的动画过程。但要注意动画时间不要设置过长,那样会让想尽快完成操作的用户不耐烦。
与其让用户马上注册,何不让用户先进行一些体验式的操作呢。这个体验过程可以展示程序的功能,特性等。一旦用户通过简单几步的操作了解了程序的价值所在,那么它会更愿意填写注册表单的。这种循序渐进的引导可以尽量推迟用户注册的时间但又可以让用户在没注册的情况下进行个性化设置等简单操作。
过多边框会喧宾夺主。不用说,边框确实在划分区域进行版块设置时有很大的作用,但同时其明显的线条也会吸引走用户的注意力。为了达到划分版块又不转移用户注意力的目的,在排版时可以将界面上不同区域的元素通过空白进行分组,用上不同的背景色,将文字对齐方式进行统一,还有就是为不同区域设置不同的样式。当使用所见即所得的界面设计工具时,我们经常在界面上方便地拖出很多区块来,这些区块多了就会显得杂乱无章。所以我们又会到处放些横线来分界。一个更好的做法是将区块垂直对齐,这样做不会让那些多余的线条来扰乱视觉。
市场就是这样的,用户永远只关心自身利益而产品特性对他们来说倒不是那么重要。只有利益才更直观地体现出使用产品所带来的价值。Chris Guillebeau在他的著作《100美元起家》中指出,相比压力,冲突,烦心事和未知的未来,人们在乎得更多的是爱,金钱,认同感和自由支配的空闲时间。所以我相信在展示产品特性时回归到利益上是必要的。
界面上经常需要呈现不同数量的数据,从0,1,10,100到10000+等。这里存在个普遍的问题就是:在程序最开始使用的0条数据到过度到有数据之前,该如何进行显示界面。这也是我们经常忽视了的地方。当程序初始没有数据时,用户看到的就是一片空白,此时用户可能不知道该进行哪些操作。利用好没有数据的初始界面可以让用户学习和熟悉如何使用程序,在程序中创建数据。力臻完美永远是我们追求的目标,界面设计也不例外。
将界面做成默认用户选中想要使用你的产品,意味着如果用户真的需要使用,那么可以直接点击确定而不需要额外点选了。当然,也有另一种做法就是默认不选中服务,用户需要的话可以手动点选。前者这种设计更好的原因有两点。一是用户不需要额外点选,非常省事,因为默认设置为用户需要我们的产品或服务。二是这种做法某种程度上是在向用户推荐产品,暗示了其他人都选择了我们。当然,将界面设计成默认选择的样子多少存在点争议,有点强迫用户的感觉。如果你想道德一点,你可以要么把让用户选择的文字写得模棱两可,要么使用双重否定这样不那么直白的描述,这两种方式都可以让用户觉得没有那么强的感觉是被强迫选择使用产品的。
自从Donald Norman的一系列著作面世后,界面设计中尽量保持一致性成了一个普遍遵循的准则。在设计中保持一致性可以减少用户的学习成本,用户不需要学习新的操作。当我们点击按钮,或者进行拖拽操作,我们期望这样的操作在整个程序的各个界面都是一致的,会得到相似的结果出来。反之我们需要新情境下重新学习某种操作会产生何种结果。可以在很多方面下功夫来实现一个一致的界面,包括颜色,方向,元素的表现形式,位置,大小,形状等。不过在让界面变得一致之前,记住一点,适当的打破整体的一致性也是可取的。这偶尔的不一致性的设计用在你需要强调的地方可以起到很大的作用。所以世事无绝对,我们应遵从一致的设计准则,但适当地打破这种常规。
适当的默认值和预先填充好的表单字段可以大量减少用户的工作量。在节省用户宝贵的时间上面,这是种非常常见的做法,可以帮助用户快速填完表单或者注册信息。
界面设计中遵从约定的准则跟之前的界面一致性准则很相似。如果我们遵从了界面设计中的一些约定,用户用起来会很方便。相反,不一致和没有遵从约定的设计则会提高学习成本。有了界面设计中这些约定,我们想都不用想就知道界面右上角(大多数情况下)的叉叉是关闭程序用的,或者点击一个按钮后我们能够预测到将会发生什么。当然,约定是会过时的,随着时间的推移,同样的操作也有可能被赋予新的含义。但要记住,当你在界面中打破这些常规时一定要目的明确,并且出发点是好的。
我们喜欢成功,没有谁愿意失败。根据心理学得到的可靠结论,人们一般更顷向于避免失去拥有的东西而不是获得新的利益。这一结论也适用于产品的设计和推广中。试着说明你的产品会帮助客户维护他的利益,保持健康,社会地位等要好过告诉客户这个产品会带来一些他未曾拥有的东西。比如保险公司,它是在销售我们出事之后可以得到的大笔赔偿呢还是在强调可以帮助我们避免失去拥有的财产?
具有层次的设计可以将界面上重要的部分与不次要部分区分开来。要让界面层次分明,可以在这些方面做文章:对齐方式,间距,颜色,缩进,字体大小,元素尺寸等。当所有这些调整运用得适当时,可以提高整个界面的可读性。相比在一个很直白的界面上用户一眼就可以从上瞟到底的设计,这样分明的设计也可以让用户放慢速度来慢慢阅读。这样也使界面更有特色一些。就好比一次旅行,你可以乘坐高铁快速到达景区(从页面顶部瞟到底部),但你也可以慢行以欣赏沿途风光。所以层次分明的设计让眼睛有可以停留的地方,而不是对着空白单调的一个屏幕。
将各个功能项分组合并起来可以提高程序的可用性。有点常识的人都知道刀子和叉子,或者打开文件和关闭文件是放在一起的。将功能相近的元素放在一起也符合逻辑,符合我们平时的认知。
在处理表单时,最好立即检测出用户所填写内容是否符合要求然后给出验证消息。这样错误一出现能就能得到改正。相反,提交后再检查表单会给出错误消息,会让用户感到乏力又要重复之前的工作。
对用户输入的数据,尽量放宽限制,包括格式,大小写什么的。这样做可以更人性化一点,也使得界面更加友好。一个再恬当不过的例子就是让用户输入电话号码的时候,用户有很多种输入方式,带括号的,带破折号的,带空格的,带区号和不带区号的等等。如果你在代码中来处理这些格式的问题,代价也只是你一个人多写几行代码而以,却可以减少无数用户的工作量。
适当的紧迫感是个有效的战术可以让用户立即做出决定而不是等上个十天半个月。重要的是这种战术屡试不爽,因为它暗示了资源的紧缺或者活动的时间有限,今天可以买,但明天可能就无法这么低价了。另一方面,这一战术也让用户感到会错失一次大好的机会,再一次,应用了人们害怕失去的本性。当然,这种战术会被一些人嗤之以鼻,认为是不耿直的做法。不过,这只是种战术而以,并且在保持合法性的前提下应用也无伤大雅。所以请不要为了营销而在界面上制造紧迫的假象。
物以稀为贵。饥饿营销给出的信息就是东西不多,只剩几件,明天再来,可能没了。你去比较一下批发与限量版的东西他们的价格差距有多大就知道了。回过头来看,那些批发商或者大零售商,他们也使用饥饿营销,以获得更好的销量。但在软件行业,我们经常会忘记有饥饿营销这回事。因为数字产品是可以很容易拷贝复制的,不存在缺货的情况。其实,在界面设计中,也可以将其运用起来与现实中的资源紧缺进行联系。想想一次网上研讨会的门票,想想你一个月可以服务的人数限制,这些信息都可以告知用户是有限的。
这一界面设计中的经典准则是有心理学依据的,相比要让某人回想想某样东西,从一堆东西中认出某样东西会更容易些。辨识出一样东西只需要我们稍微回忆一下,通过一些线索就可以完成。而回想则需要我们全面搜索自己的大脑。也许这也是为什么试卷上选择题会比简答题做得快的原因。所以试着在界面上展示一些用户之前涉及到的信息让他们进行选择,而不是让他们想半天然后自己填写。
像链接,表单的输入框还有按钮等,如果尺寸做得大一点则点击起来更方便容易些。根据费特定律,使用像鼠标这样的外设来点击需要一些时间,特别是元素比较小的情况下,时间会更多。鉴于此,最好还是把你的表单输入框,按钮等做大点。抑或者你可以保持原有的设计不变,只是把元素可点击区域(也就是热区)增大。这样的一个典型例子是手机设备上的文本链接和导航菜单,它们文字不一定很大但背景是拉伸的,在很宽范围内点击都有效。
速度很重要。页面加载速度和UI对操作的响应速度都直接关系到用户是否有耐心继续等下去。无疑地每多一秒种的等待都会失去一些用户或者项目机会。一个好的解决之道当然就是优化你的页面和图片。除此之外还可以运用心理学让这个等待时间显得不那么长。具体来说有两种技巧。一是显示进度条,二是展示其他相关或有趣的东西来吸引用户的注意力(就好比你沿着传送带走走总比傻站在原地盯着一个位置看要好得多吧)。
当你的程序广为流传,应该考虑下高级用户的感受。人们总是试图为一些重复性的操作找到更快捷的方法,快捷键就应运而生了。相比在界面上点来点去,快捷键无疑大大提高工作效率。一个好的例子就是现今流行于各个主流程序中的J(后退)K(前进)快捷键组合,比如在Gmail,Twitter和Tumblr中。按钮固然好,但快捷键会锦上添花。
]]>为了准确无误地将数据送达目标处,TCP协议采用了三次握手策略。用TCP协议把数据包送出去后,TCP不会对传送后的情况置之不理,它一定会向对方确认是否成功送达。
首先我们要先了解下TCP/IP协议族的分层管理
TCP/IP协议族按层次分别分为:应用层、传输层、网络层和数据链路层
IP地址是IP协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。
DNS域名解析,说得简单点就是将好记的域名解析成IP,服务由DNS服务器完成,是把域名解析到一个IP地址,然后在此IP地址的主机上将一个子目录与域名绑定。互联网中的地址是数字的IP地址,域名解析的作用主要就是为了便于记忆。
每个过程中间还会发生很多事情,下面单独分析每个过程发生的细节
DNS服务
DNS服务是和HTTP协议位于应用层的协议。它提供域名到IP地址之间的解析服务。
操作系统会先检查本地hosts文件是否有这个网址映射关系,如果有就调用这个IP地址映射,完成域名解析。
否则,查找本地DNS解析器缓存,如果查找到则返回。
否则,查找本地DNS服务器,如果查找到则返回。
否则,则发起一个递归DNS解析请求,一层一层逐级向上查询找到IP地址。
补充
这里是有缓存相关的知识,涉及浏览器缓存、DNS缓存,缓存内容的有效性决定是否需要发送HTTP请求。在这里不在细说。
HTTP协议生成针对目标 Web 服务器的 HTTP 请求报文,请求报文由请求行、请求头部、数据体3个部分组成。
拿到了服务器的 IP 地址以及 HTTP 请求报文,就开始与服务器建立 TCP 连接了。
这样,客户端与服务器两者就建立了连接。
一个HTTP请求过程如下:
数据经过应用层、传输层、网络层、链路层逐层封装,传输到下一个目的地。
然后接着从链路层得到数据后逐层的解包,最终将数据发送服务端。
浏览器拿到服务端响应的内容后,开始解析其中的html文件,遇到js/css/img等资源连接,就向服务器端去请求下载
浏览器根据html、css、js渲染DOM树,绘制等。
补充说明
《图解HTTP》
《HTTP权威指南》
下面讲解XSS相关知识内容。
XSS叫做(Cross Site Script)跨站脚本。
黑客通过“HTML注入”篡改网页,插入恶性脚本,当用户在浏览网页时,实现控制用户浏览器行为的一种攻击方式。
常见的危害有:盗取用户信息,钓鱼,制作蠕虫等。
存储型
常见实例,黑客通过Web应用留言回复功能插入XSS脚本,当其他用户访问查看时即触发脚本代码。
1 | <!--图片src加载错误,执行onerror函数触发脚本--> |
反射型
DOM型
DOM型XSS和反射性XSS基本上是一样的,DOM型XSS是通过前端js将XSS脚本插入到DOM里(innterHTML)。比如把url上的锚点参数输出到html,从而触发XSS
XSS类型 | 存储型 | 反射型 | DOM型 |
---|---|---|---|
触发过程 | 黑客构造XSS脚本,用户访问携带XSS脚本的页面 | 用户访问携带XSS脚本的页面 | 用户访问携带XSS脚本的页面 |
数据存储 | 数据库 | URL | URL |
输出类型 | 后端服务器 | 后端服务器 | 前端js |
输出位置 | HTTP响应中 | HTTP响应中 | 动态构造的DOM节点 |
DOM型XSS 攻击中,取出和执行恶意代码由浏览器端完成,属于前端 js 自身的安全漏洞。
反射型XSS 攻击中,取出和执行恶意代码由服务端完成,服务端的安全漏洞。
下面讲解CSRF相关知识内容。
CSRF叫做(Cross-site request forgery)跨站请求伪造。
利用用户已登录的身份,在用户毫不知情的情况下,以用户的名义完成非法操作。常见的危害有:执行恶意操作(被转账、被发垃圾评论)等。
正常转账过程
异常转账过程(攻击)
下面讲解点击劫持相关知识内容。
通过覆盖不可见的框架(iframe)误导用户点击而造成的攻击行为。
下面讲解URL跳转相关知识内容。
借助未验证的URL跳转,将应用程序引导到不安全的第三方页面(攻击者),从而导致的安全问题。
流程
下面讲解SQL注入相关知识内容。
SQL注入叫做(SQL Injection)
是一种常见的Web安全漏洞,攻击者利用这个漏洞,可以访问或修改数据,利用潜在的数据库漏洞进行攻击。
过程
数据和代码未分离,既数据当做了代码来执行
获取数据库信息
下面讲解命令注入相关知识内容。
《Web白帽子》
]]>为了有效防止这些弊端,为了保证数据安全,所有HTTPS就派上用场了。
HTTPS并非是应用层的一种新协议,只是HTTP通信接口部分用SSL和TLS协议代替。
一般情况,HTTP直接和TCP通信,当使用SSL时,则变成先和SSL通信,再由SSL和TCP通信。
对SSL讲解之前,先简单了解一下加密方法。SSL采用一种叫做公开密钥加密的加密处理方式。
近代的加密方式中加密算法是公开的,而密钥是保密的,通过这种方式得以保证加密方法的安全性。
加密和解密都会用到密钥。没有密钥就无法对密钥解密,反过来说,任何人只要持有密钥就能解密了。如果密钥被攻击者获取,那加密也就失去了意义。
加密和解密同用一个密钥的方式称为共享密钥加密,也叫对称密钥加密
在HTTP请求中,发送密钥就会有被窃听的风险,但不发送,对方就不能解密。如果密钥能安全发送,那数据也应该能安全送达。
公开密钥加密方式解决了共享密钥加密的困难,
公开密钥加密使用一对非对称的密钥。(私有密钥和公开密钥)
私有密钥:只有自己知道,其他任何人都不能知道。
公开密钥:公开密钥可以随意发布,任何人都可以获得。
使用公开密钥加密方式,发送密文的一方使用对方的公开密钥进行加密处理,对方收到被加密的信息后,再使用自己的私有密钥进行解密。利用这种方式,不需要发送用来解密的私有密钥,也不必担心密钥被攻击者窃听而盗走。
原因:公开密钥加密处理起来比共享密钥加密方式更为复杂,若在通信时使用公开密钥加密方式,效率会很低。
HTTPS采用共享密钥加密和公开密钥加密两者并用的混合加密机制。在交换密钥环节使用公开密钥加密方式,之后的建立通信交换报文阶段则使用共享密钥加密方式。
公开密钥加密方式还是存在一些问题。那就是无法证明公开密钥本身就是货真价实的公开密钥。或许公开密钥在传输过程中,真正的公开密钥已经被攻击者替换掉了。
解决上述问题,可以使用由数字证书认证机构(CA,Certificate Authority)和其他相关机关颁发的公开密钥证书。数字证书认证机构处于客户端与服务器双方都可信赖的第三方机构的立场上。
通俗的讲:服务器自身发行的公开密钥没有权威性,无法证明公开密钥是自身发行的。
补充:
此处认证机关的公开密钥必须安全地转交给客户端。所以大多数浏览器在发布版本时,会先在内部植入常用的认证机关的公开密钥。(不需要传输)
HTTPS使用SSL和TLS这两个协议。
下面是HTTPS通信步骤
通俗理解:
总结:相比HTTP协议,HTTPS 协议增加了很多握手、加密解密等流程,虽然过程很复杂,但其可以保证数据传输的安全。为了保证数据的安全,很有必要使用HTTPS。
《图解HTTP》
《HTTP权威指南》
防抖(debounce)和节流(throttle)都是用来控制某个函数在一定时间内执行多少次,从而实现性能优化。
定义:是指连续触发事件但是在 n 秒中只执行一次函数。节流会稀释函数的执行频率。
定义:就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。
在开发过程中,会遇到处理频率很高的事件或着连续的事件,比如像window的resize/scroll 事件调用某个函数A。
如果不进行处理,这个时候伴随着resize 就会执行无数个函数A ,若函数 是个很复杂的函数,需要较多的运算执行时间,响应速度跟不上触发频率,往往会出现延迟,导致假死或者卡顿感,并且很多情况下并不是需要“如此”夸张的频繁调用函数A。
1.针对scroll方法写一个例子。(假设时间间隔2s)
2.节流(throttle)和 去抖(debounce) 的应用场景应该是分的很清楚的
underscore.js是一个很精干的库,压缩后只有5.2KB。它提供了几十种函数式编程的方法,弥补了标准库的不足,大大方便了JavaScript的编程
underscore.js封装好的去抖和节流是考虑了高级配置的下面我们来看看如何使用。
1 | // _.throttle( function, wait, options ) |
underscore.js中针对这个方法接收三个参数:
{ leading: false },表示scroll开始的那次回调会被忽略。
如果不传,就表示scroll开始的时候会触发function,scroll停止后还会触发一次function。
{ trailing: false },表示scroll结束后的那次回调会被忽略。
1 | // _.debounce( function, wait, immediate ) |
underscore.js中针对这个方法接收三个参数:
true,表示scroll开始时即调用这个函数。(并且在 waite 的时间之内,不会再次调用,即触发于开始边界)。
false(不传),触发于结束边界。
基于开发版本(1.7.0)的源码,加上了一些注释以帮助理解。
1 | /** |
1 | /** |
js性能优化underscore throttle 与 debounce 节流
浅谈 Underscore.js 中 _.throttle 和 _.debounce 的差异
JavaScript 函数节流和函数去抖应用场景辨析
URL又叫做统一资源定位符(Uniform Resource Lovator),它支持多种协议:HTTP、FTP…
URL是有标准的格式,所有的浏览器和Web服务器都遵循这个标准的格式,保障了我们使用的浏览器能够访问服务器里特定的资源。
URL的详细格式:schema://host[:port#]/path/…/[?query-string][#anchor]
从上面我们知道了什么是URL,知道URL包含很多协议,其中HTTP就是我们Web使用的协议,下面内容讲解什么HTTP协议相关知识。
HTTP又叫做超文本传输协议(Hyper Text Transfer Protocol),它是Web通讯最常用的协议。
HTTP报文是是比较重要的内容,其中包含请求报文和响应报文。简单的来说,HTTP报文都由简单的三部分组成(起始行,头,主体)。HTTP协议中的请求和响应报文中必定包含HTTP首部(起始行,头),首部内容为客户端和服务器分别处理请求和响应提供所需要的信息。下来来看看请求和响应报文的内容。
1 | GET /sugrec?pre=1&p=3 HTTP/1.1 ---请求行(Request Line) |
1 | HTTP/1.1 200 OK ---状态行(Status Line) |
类别 | 解释 | |
---|---|---|
1XX | 信息性状态码 | 接受的请求正在处理 |
2XX | 成功状态码 | 请求正常处理完成 |
3XX | 重定向状态码 | 需要进行附加操作 |
4XX | 客户端错误状态码 | 服务器无法处理请求 |
5XX | 服务器错误状态码 | 服务器处理请求出错 |
状态码详细解析
HTTP首部字段是构成HTTP报文的重要要素之一。无论是请求还是响应都会使用首部字段,它能起到传递额外重要信息的作用。
请求报文和响应报文都会使用的首部字段。
从客户端向服务器发送请求报文时使用的首部,补充了请求的附加内容、客户端信息、响应内容相关优先级等信息。
从服务器向客户端返回响应报文时使用的首部,补充了响应的附加内容,也会要求客户端附加额外的内容信息。
针对请求报文和响应报文的实体部分使用的首部。补充了资源内容更新时间等与实体有关的信息。
由于字段内容多且复杂,具体字段细节不在细说。
补充:
请求头部有两个比较重要的字段我们可以了解一下。
《图解HTTP》
《Web白帽子》
LocalStorage
与SessionStorage
的关系、以及Cookie
与它们的区别。 Html5中的Web Storage
包括了两种存储方式:sessionStorage 和 localStorage
。
sessionStorage
用于本地存储一个会话(session)
中的数据,这些数据只有在同一个会话中的页面才能访问并且当会话结束后数据也随之销毁。因此sessionStorage
不是一种持久化的本地存储,仅仅是会话级别的存储。- 而
localStorage
用于持久化的本地存储,除非主动删除数据,否则数据是永远不会过期的。
Cookie
是小甜饼的意思。顾名思义,cookie
确实非常小,它的大小限制为4KB左右,是网景公司的前雇员 Lou Montulli 在1993年3月的发明。它的主要用途有保存登录信息,比如你登录某个网站市场可以看到“记住密码”,这通常就是通过在Cookie
中存入一段辨别用户身份的数据来实现的。
Cookie
:一般由服务器生成,可设置失效时间。如果在浏览器端生成Cookie,默认是关闭浏览器后失效。localStorage
:除非被清除,否则永久保存。sessionStorage
:仅在当前会话下有效,关闭页面或浏览器后被清除。Cookie
:4KB左右。localStorage 和 sessionStorage
:一般为5MB。Cookie
:每次都会携带在HTTP头中,如果使用cookie保存过多数据会带来性能问题。localStorage 和 sessionStorage
:仅在客户端(即浏览器)中保存,不参与和服务器的通信。总的来说:Web Storage
的概念和cookie
相似,区别是它是为了更大容量存储设计的。Cookie
的大小是受限的,并且每次你请求一个新的页面的时候Cookie
都会被发送过去,这样无形中浪费了带宽。
另外cookie
还需要指定作用域,不可以跨域调用。
但是Cookie
也是不可以或缺的:Cookie
的作用是与服务器进行交互,作为HTTP规范的一部分而存在 ,而Web Storage
仅仅是为了在本地“存储”数据而生。
用途:将value存储到key字段
用法:.setItem( key, value)
代码示例:
1 | sessionStorage.setItem("key", "value1") |
用途:获取指定key本地存储的值
用法:.getItem(key)
代码示例:
1 | let value = sessionStorage.getItem("key") |
用途:删除指定key本地存储的值
用法:.removeItem(key)
代码示例:
1 | sessionStorage.removeItem("key") |
用途:清除所有的key/value
用法:.clear()
代码示例:
1 | sessionStorage.clear() |
.
操作和[]
web Storage不但可以用自身的setItem,getItem
等方便存取,也可以像普通对象一样用点(.)
操作符,及[]
的方式进行数据存储,像如下的代码:
1 | var storage = window.localStorage |
sessionStorage 和 localStorage
提供的key() 和 length
可以方便的实现存储的数据遍历
代码示例:
1 | var storage = window.localStorage |
storage
还提供了storage
事件,当键值
改变或者clear
的时候,就可以触发storage
事件,如下面的代码就添加了一个storage事件改变的监听:
1 | if(window.addEventListener){ |
for in
测试1 | let a = ['a','b','c'] |
for of
测试1 | let a = ['a','b','c'] |
for in
的问题for in
遍历的实际上是对象的属性名称。一个Array
数组也是一个对象,数组中的每个元素的索引被视为属性名称,所以我们可以看到使用for...in
循环Array
数组时,拿到的其实是每个元素的索引。
1 | let a = ['a','b','c'] |
如上所示,当我们为a多手动添加一个属性name
的时候,for...in
循环会把name属性也包括在内,而Array
的length
属性却不包括在内。
for...of
循环则不存在上述的问题,它只循环集合本身的元素。这就是为什么引入for...of
循环。所以for...of
循环功能比较好,推荐使用
for-of
循环的几个特征
for-in
循环的所有缺陷。forEach
不同的是,它可以正确响应break、continue 和 return
语句。<meta>
的一些配置。一个常用的针对移动网页优化过的页面的 viewport meta 标签大致如下:
1 | <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"> |
width
:控制 viewport 的大小,如 device-width 为设备的宽度。height
:和 width 相对应,指定高度。initial-scale
:初始缩放比例,也即是当页面第一次 load 的时候缩放比例。maximum-scale
:允许用户缩放到的最大比例。minimum-scale
:允许用户缩放到的最小比例。user-scalable
:用户是否可以手动缩放。下面是移动端 <meta>
的通用设置
1 | <!-- 避免IE使用兼容模式 --> |