NiYingfeng 的博客

记录技术、生活与思考

0%

编写可维护的 JavaScript 读书笔记

尼古拉斯(作者): 前端大牛工程师,目前在 Box 公司任职,之前是在雅虎将近工作 5 年。在雅虎期间,他是雅虎首页的前端技术主管,并且是 YUI 库的贡献者。 Nicholas 编写的技术书有:《Maintainable JavaScript | 编写可维护的 JavaScript》、《Professional JavaScript for Web Developers | JavaScript高级程序设计》、《High Performance JavaScript | 高性能JavaScript》、《Professional Ajax》。

任何开发者都不在乎某个文件的作者是谁,也没有必要花费额外的精力去理解代码的逻辑并且重新排版,因为所有代码的排版风格都是非常一致的,因为风格不一致会导致我们打开代码文件的第一件事情不是立即工作,而是进行缩进的排版整理。

能很容易的辨别出有问题的代码并且发现错误,如果所有的代码风格很像,当看到一段与众不同的代码时,很肯能问题就出在这里。

“程序是写给人读的,只是偶尔让计算机执行一下” - Donald Knuth

阅读全文 »

2017回顾

2017年已经过去近一月,才勉强的敲下键盘,憋出些字,寥寥的作为我的2017年总结,权当安抚一下自己对于职业生涯一直存在的那份危机感吧。

人的一生中不乏各类想法与计划,但对于个人来说,难在坚持。各种坚持的计划,最终放弃的借口总是比坚持的理由多的多,这也是自己今天开始写下总结的缘由,也送给各位准备坚持亦或在坚持各类计划的人。

工作

在百度的第三个年头,也算是学到最多的一年。入百度初期,在编程思想,逻辑思维以及代码架构上是自己学习上最大的收获,扩展了一个作为非计算机专业学生对于什么是真正的编程更好的阐释,各类形形色色的编码风格、思维逻辑,各类优秀的人才极大的扩宽了自己的视野。确实是,接触的越多才发现,自己不懂的也越多。

这第三个年头,简单概括自己工作就是:不断调整中的趋于稳定,不断学习中的获得进阶。从具体项目开发主力到前端负责人的转变,不断的学习与获得,不断的成长与进阶。虽说理想总是比现实丰满,但比自己预期的更为好了不少,知足常乐。

整体而言17年的工作自打一个85分吧。做的还不错,当然也有较大的缺点与遗憾。减少抱怨,主动解决问题,合理规划工作重点。应该成为对于17年的反思以及18年的注意事项。

生活

生活上最大的事情就是,我结婚了!有个小媳妇在此生中共同前进,感谢命运。一路相随,平凡而又不负此生,就是最大的幸运。这也是我对生活的理解。

也慢慢的开始学会承担,承担起作为一个家庭里男人的角色。一起学习,共同前进吧。

没有坚持的锻炼,也是表示可惜的,18年,坚持跑步,算是自己的一个目标吧。

今年也不断的在考虑人生的意义与责任,希望此生,能多看看未知,了解大千的世界。

学习

在学习上,今年有收获也有遗憾。

收获是接触了关于理财,经济学的相关知识,跳出了中国赌博式的理财,慢慢接触了所谓的价值投资。简单的理解了什么才是真正的资产。对此还是不断学习,放低姿态吧。关于理财,有个「小目标」,多少年后,能不为生计而工作。真正的思想的自由。

遗憾的就是,博文一断就是半年。当初坚持确实不容易,而断了坚持则如此轻松。要是说下半年就·工作巨忙,觉得也仅能作为一个借口的理由而已。好吧,重拾博文,从这篇总结开始。(再不写,服务器又该续费了!!!)

自我总结

  1. 合理性、重点性的规划做的不充分,缺乏结构化思维。
  2. 主动发现与解决问题的自我推动力相对还不足。
  3. 压力状态下的情绪管理做的相对不充分。
  4. 在生活与工作中沟通展现的积极与向上还不是很好。
  5. 事务的多角度切入、分析与思考做的相对不足。
  6. 真诚的表达情感较为缺乏。

2018展望

2018一眨眼,一个月已经过去,时间匆匆总算不等待我们回味一下生活的滋味,就早已远去。就如已逝去的2017。

目标

  1. 坚持读书:工作日的早上与周末,学会静下心来沉淀与吸收,多方面的摄取。希望能看30本+的各类书籍吧。
  2. 情绪管理:再次细读一次《情商》,学会控制情绪,理性的做好大脑的情绪管理。展现积极与向上的个人魅力。
  3. 技术力与领导力:多方面加强自身各项技能,强大内心。
  4. 积极主动:工作中、团队建设中、生活中,以积极的行动来展现。

最后谈谈 关于梦想

未知的世界,想去走走看看,感受一下。世界很大,未来很大,我们也应该放宽心胸,容纳百川!

HTTP基础知识

HyperText Transfer Protocol 超文本传输协议,这个可能是我们对于HTTP知晓的最广泛的一个知识点。用于分布式,协作式与超媒体信息系统的应用层协议。HTTP是万维网的数据通讯基础,设计初期目的仅仅是为了提供一种发布和接收HTML页面的方法。

HTTP是一个客户端(用户)和服务器(网站)请求和应答的标准(TCP)。通过使用浏览器、爬虫或者其它的工具,客户端发起一个HTTP请求到服务器上指定端口(默认端口为80)。我们称这个客户端为用户代理程序(user agent)。应答的服务器上存储着一些资源,比如HTML文件和图像。我们称这个应答服务器为源服务器(origin server)。在用户代理和源服务器中间可能存在多个“中间层”,比如代理服务器、网关或者隧道(tunnel)。

HTTP 0.9:一行协议

最早期 HTTP 为1991年诞生被设计用于从服务器获取 HTML 文档,

1
2
3
4
GET /xxx.html

<html>...

整个协议只有一行,GET请求方法以及文档路径,极为简单。当时的 HTTP 只支持 GET 方法,路径是文档在服务器的位置,即实际要获取的内容。请求以换行结束。

HTTP 0.9的简单功能:

  • 客户端/服务端、请求/响应协议
  • ASCII协议,运行与TCP/IP链接之上
  • 设计用于传输超文本文档(HTML)
  • 客户端与服务端的连接在每次请求之后关闭

HTTP 1.0:迅速发展标准

在1996年,一个更加全面,完整的协议规范,1.0正式发布

1
2
3
GET /xxx.html HTTP/1.0
User-Agent: xxxBrowser
Accept: */*
1
2
3
4
5
6
HTTP/1.0 200 OK
Content-Type: text/html
Server: XXXServer

<h1>It works</h1>

相对0.9而言的关键变化:

  • 请求可以由多行首部字段构成
  • 响应对象前添加响应状态行
  • 响应对象也有自己换行分隔的首部字段
  • 响应对象不仅限于超文本
  • 客户端与服务端的连接在每次请求之后关闭

HTTP 1.1:互联网标准

HTTP协议之所以能如此被广泛使用的主要原因为,在早期的HTTP标准中,只考虑协议的便捷性,而完全不考虑其性能问题,使得简单可用变成HTTP早期标准的准则。

HTTP 1.1标准理清之前版本中一些歧义内容,并且添加了许多的性能优化:

  • 持久化连接以支持连接重用
  • 分块编码以支持流式响应
  • 请求管道以支持并行请求处理
  • 字节服务以支持基于范围的资源内容请求
  • 改进的更好的缓存机制

HTTP 2.0:改进传输性能

HTTP的简单本质是其最初得以采用以及后期快速发展的关键,但是互联网的高速发展,其弊端开始显现。

HTTP工作组于2012年宣布开发HTTP2.0,目标为改进传输性能,实现低延迟和高吞吐量。

扩展一下:

HTTP为客户端与服务端之间最常用的协议,通过请求与响应的交换达成协议。
请求包含:方法,URI,版本协议,请求首部以及请求内容实体。
响应包含:版本协议,状态码,状态短语,响应首部和主体

HTTP方法

  • GET 获取资源 (HTTP1.0)
  • POST 传输实体主体 (HTTP1.0)
  • PUT 传输文件 (HTTP1.0)
  • HEAD 获取报文头部 (HTTP1.0)
  • DELETE 删除文件 (HTTP1.0)
  • OPTIONS 查询支持的方法 (HTTP1.1)
  • TRACE 追踪路径 (HTTP1.1)
  • CONNECT 使用隧道连接代理 (HTTP1.1)

通用:

image

请求:

image

响应:

image

image

TCP/IP与HTTP的关系

IP:Internet Protocol 因特网协议 负责互联网主机之间的路由选择和寻址

TCP:Transmission Control Protocol 传输协议负者在不可靠的传输信道上提供可靠的抽象层

在Internet上,HTTP通讯通常发生在TCP/IP连接之上,缺省端口是TCP 80,但其它的端口也是可用的。

一次HTTP创建的过程:

image

名词

  • SYN表示建立连接
  • FIN表示关闭连接
  • ACK表示响应
  • PSH表示有DATA数据传输
  • RST表示连接重置

三次握手

  1. SYN:客户端选着一个随机序列号x,发送一个SYN分组,其中可能包含其他的TCP标志和选项,而后进入SYN_SEND状态,等待服务器确认。
  2. SYN ACK:服务器接受到该SYN分组之后需要确认,便向x+1,并且使用另一个随机序列号y,追加自身的标志和选项,返回响应SYN ACK分组,此时服务器进入SYN_RECV状态
  3. ACK:客户端接收之后,对于x,y均加1,返回握手的随后一次ACK分组,客户端和服务器进入ESTABLISHED状态,完成三次握手。

四次挥手

  1. 客户端发送一个FIN分组,用来告知服务器关闭数据传输
  2. 服务器收到FIN分组后,响应一个ACK分组,告知客户端收到该关闭TCP的通知,但此时并未真正关闭TCP,可能仍有数据进行响应中
  3. 在数据全部传输完毕之后,服务器发送一个FIN分组给到客户端
  4. 客户端发回ACK分组确认,并将确认序号加1确认

慢启动

慢启动是一种TCP拥塞控制机制,当TCP建立完毕,开始传输数据时,为避免由于发送了过量的数据而导致阻塞会首先慢慢的对网路实际容量进行试探,不断的加大拥塞窗口来加大吞吐量。

TCP核心原理的影响

  • 三次握手增加一整次的往返时间
  • TCP慢启动被用到了每一次TCP链接
  • 流量控制与拥塞控制影响所有链接的吞吐量
  • 吞吐量由当前拥塞窗口大小控制

TCP性能检测清单

  • 服务器内核升级最新版本
  • 确保拥塞窗口大小为10
  • 慢启动重启:在连接空闲时禁用慢启动
  • 确保启动拥塞窗口缩放
  • 减少传输冗余数据
  • 压缩传输数据
  • 减少资源与用户的物理距离
  • 最大可能重用TCP连接

什么是HTTPS

Hypertext Transfer Protocol Secure 超文本传输安全协议,HTTP over TLS,简称HTTPS。

  1. 内容加密,建立一个信息安全通道保证数据传输的安全
  2. 身份认证确认网站的真实性
  3. 数据完整性防止内容被篡改

传输应用数据之前,客户端必须与服务端协商密钥、加密算法等信息,服务端还要把自己的证书发给客户端表明其身份,这些环节构成 TLS 握手过程

image

  1. TLS基于可靠的传输层TCP之上运行,意味着首先完成TCP的三次握手,一整次的往返
  2. TCP连接建立之后,客户端将自身支持的加密套件,运行的TLS协议版本等发送给服务器
  3. 服务器接受到数据之后,选择一组加密算法,并将自己的身份信息等以证书的形式返回相应,其中包含加密公钥,最为可选项,服务器也可以发送一个请求,要求客户端提供证书等信息
  4. 在两端确定了共同版本以及加密套件之后,客户端会生成一个对称秘钥,使用服务端的公钥进行加密发送给服务端,到此除了加密的对称秘钥,其余的均为明文形式发送
  5. 服务端在接受到数据之后,使用自身的私钥来解密获得对称秘钥,返回给客户端一个加密的响应
  6. 客户端利用对称秘钥解密,然后两方均使用对称秘钥进行加密通讯。

HTTP 2.0的进化之道

协议关注的问题与目标

  • 创建协商协议标准,应用层协议协商(ALPN),以便客户端能够从HTTP/1.0、HTTP/1.1、HTTP/2或其他非HTTP协议中做出选择
  • 与HTTP/1.1在请求方法、状态码乃至URI和绝大多数HTTP头部字段等方面保持高度兼容性。
  • 减少网络延迟,提高浏览器的页面加载速度
  • 支持现有HTTP应用场景,包括桌面和移动设备浏览器,网络API,不同规格的网络服务器和正向代理、反向代理服务器软件,防火墙,CDN等

Demo演示

同时请求 379 张图片来组成下面的大图片,其优势显而易见。

image

再看下请求

image

简单介绍

  • 帧、消息和流

与HTTP/1.1在连接中的明文请求不同,HTTP/2,将一个TCP连接分为若干个流(Stream),每个流中可以传输若干消息(Message),每个消息由若干最小的二进制帧(Frame)组成。

image

流:已建立连接上的双向字节流
消息:与逻辑消息对应的完整的一系列数据帧
帧:HTTP 2.0通讯的最小单位,每个帧包含帧首部,标识所属的流

所有的HTTP 2.0通讯都在一个TCP连接上完成,可以承载任意数量的双向数据流,每个数据流以消息的形式发送,而每个消息由一个或者多个帧组成,帧可以乱序发送,再由帧首部进行重新组装。

  • 多路复用&请求管道化

数据传输采用多路复用形式,多请求合并在同一TCP连接内

image

可以并行交错的发送请求,之间互不干扰。
可以并行交错的发送响应,之间互不干扰。
一个连接可并发多个请求与响应。
消除不必要的延迟。

  • HTTP首部字段压缩(HPACK算法)

  • 服务端推送(Server Push)

服务器可以对客户端的一个请求发送多个响应,例如对于请求一个HTML页面,无需浏览器解析之后在请求响应资源,在初期请求HTML页面时,服务端可以及时推送页面所需的资源内容。

HTTP知识对于性能优化的意义

所有应用都应该致力于消除或者减少不必要的网络延时,将传输的数据压缩至最小,是web性能最基础优化实践。

  • 减少DNS查找
  • 重用TCP连接
  • 减少HTTP重定向
  • 使用CDN
  • 去掉不必要的资源
  • 在客户端缓存资源
  • 压缩传输数据
  • 消除不必要的请求开销
  • 并行处理请求与响应
  • 针对版本进行优化措施

什么是webpack

个人的简单理解:早期对于grunt,gulp有一点的了解,对于这两者来说,更合适的是称之为工具,通过开发者的角度创建各类 Task,使用插件来构建一系列的自动化构建流程(值得注意的是这边指的是人工流水线),将其作为构建工具的角色来优化前端开发流程。而对于webpack来说,可以简单理解为自动化,也可以说是智能的模块化解决方案(意味着全自动化流水线),处理模块依赖分析的打包工具,自动计算分析所有的依赖关系以及加载顺序,而不是让开发者分析以及编写配置打包的依赖。

在此盗图两张

image

image

受益于 Webpack,使用JS来取代开发者的脑力劳动,来进行依赖与加载的管理。

上图镇楼! 万物皆模块

image

webpack 特点

  • 智能的分块打包(bundle),几乎可以处理所有的第三方库,无论是CommonJS,AMD还是基础的JS文件。
  • webpack本身只支持原生的JS模块,但是其loader转换器可以使其支持各类静态资源的依赖打包
    • JS (jsx或es6等 -> js)
    • CSS (less或scss等 -> css)
    • 各类图片
    • 各类模板

webpack CLI 和 webpack-dev-server

webpack CLI: 默认的交互方式,随webpack本身安装到本地,可以从命令行获取参数或者从配置文件获取参数传入webpack来进行打包。

1
2
3
4
5
6
7
8
9
$ npm install webpack
$ webpack // 使用 webpack.config.js 配置文件构建打包

// 添加package.json的scripts配置项
"scripts": {
"build": "webpack --config webpack.config.prod.js -p"
}

$ npm run build // 构建打包

webpack-dev-server: Nodejs的服务器,需要使用npm进行安装。

1
2
3
4
5
6
7
8
9
10
11
12
$ npm install webpack-dev-server
// 终端输入启动本地服务
// inline 选项会为入口页面添加热加载功能
// hot 选项则开启热替换(Hot Module Reloading)
$ webpack-dev-server --inline --hot

// 添加package.json的scripts配置项
"scripts": {
"start": "webpack-dev-server --inline --hot"
}

$ npm run start // 构建打包

最简单的配置实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var webpack = require('webpack');

module.exports = {
// 程序入口,打包依赖计算的起点
entry: './index.js',
// 输出的配置对象
output: {
path: __dirname,
filrname: 'bundle.js'
},
// 模块配置
module: {
// 依赖规则 配置loader以及解析器等选项
rules: [
{
test: /\.css$/,
loader: 'style-loader!css-loader'
}
]
}
}

配置介绍

入口与输出

entry

作为程序入口,标识应用的起点,打包依赖的起点。可为字符串,数组或对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 单入口
entry: './index.js',

// 数组,参数配置 webpack 提供的一个静态资源服务器,webpack-dev-server,监控项目中每一个文件的变化,实时的进行构建
entry: [
'webpack/hot/only-dev-server',
'./main.js'
]

// 多入口bundle
entry: {
home: './home.js',
list: './list.js',
...
}

context

推荐在配置中传递一个值,使配置独立于当前的执行路径

1
context: path.resolve(__dirname, 'app')

output

配置wenpack的如何输出以及输出位置,形式等,定义构建后的输出。
path 仅仅告诉Webpack结果存储在哪里,然而 publicPath 项则被许多Webpack的插件用于在生产模式下更新内嵌到css、html文件里的url值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
output: {
// 配置输出bundle的文件名
// 可以使用多个entry point,
// 还可使用标识符
// [hash],[chunkhash],[name],[id]等
filename: '[hash:8].bundle.js',

// 产出对应的绝对路径
path: path.resolve(__dirname, 'dist')

// publicPath: "https://cdn.example.com/assets/", // CDN(总是 HTTPS 协议)
// publicPath: "//cdn.example.com/assets/", // CDN (协议相同)
// publicPath: "/assets/", // 相对于服务(server-relative)
// publicPath: "assets/", // 相对于 HTML 页面
// publicPath: "../assets/", // 相对于 HTML 页面
// publicPath: "", // 相对于 HTML 页面(目录相同)
}

module

配置处理项目中不同类型的模块,配置各个类型的加载器,实现真正的万物皆模块形式。
noParse:忽略所匹配文件的依赖处理 确定某模块没有依赖 忽略的文件中不应该含有 import,require, define
rules:一个匹配loaders所处理的文件的拓展名的正则表达式(必须),规则将模块应用对应的loader,或者修改parser
Rule:每个规则由三部分组成,条件,结果和嵌套规则。

Rule 条件 条件的两种输入值

  • resource:请求文件的绝对路径,属性 test, include, exclude 和 resource
  • issuer: 被请求资源(requested the resource)的模块文件的绝对路径,属性 issuer

例如: 从 app.js 导入 “./style.css”,resource 是 /path/style.css. issuer 是 /path/app.js

Rule 结果 规则只有在条件匹配时使用

  • 应用的loader:应用在 resource 上的loader,属性:loader,options,use,兼容query,loaders
  • Parser:为模块创建解析器的选项对象,熟悉:parser

嵌套的Rule

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module: {
//
noParse: /jquery|lodash/,

// 加载器配置
use: [
{
loader: 'style-loader'
},
{
loader: 'css-loader',
options: {
importLoaders: 1
}
}
]
}

对于

1
2
3
4
{   
test: /\.css$/,
loader: 'style!css'
}

多个loader可以用在同一个文件上并且被链式调用,链式调用时从右到左执行且loader之间用“!”来分割。

  • Webpack在模块内部搜索在css的依赖项,即Webpack检查js文件是否有“require(‘myCssFile.css’)”的引用,如果它发现有css的依赖,Webpack将css文件交给“css-loader”去处理
  • css-loader加载所有的css文件以及css自身的依赖(如@import xxx)到JSON对象里,Webpack然后将处理结果传给“style-loader”
  • style-loader接受JSON值然后添加一个style标签并将其内嵌到html文件里

plugins

用于自定义webpack的多种构建方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var DashboardPlugin = require('webpack-dashboard/plugin');

// 配置添加插件
plugins: [
// 构建时优化插件
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
filename: 'vendor-[hash].min.js',
}),
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false,
drop_console: false,
}
}),
new ExtractTextPlugin({
filename: 'build.min.css',
allChunks: true,
}),
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
// 编译时(compile time)插件
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"',
}),
// webpack-dev-server 强化插件
new DashboardPlugin(),
new webpack.HotModuleReplacementPlugin(),
]

devServer

启动开发服务的配置, webpack-dev-server传入参数的配置形式。
等效于在使用CLI形式启动直接传参

经典相关阅读

文档的综合实例

官网一个详细的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
const path = require('path');

module.exports = {

entry: "./app/entry", // string | object | array
entry: ["./app/entry1", "./app/entry2"],
entry: {
a: "./app/entry-a",
b: ["./app/entry-b1", "./app/entry-b2"]
},
// 这里应用程序开始执行
// webpack 开始打包

output: {
// webpack 如何输出结果的相关选项

path: path.resolve(__dirname, "dist"), // string
// 所有输出文件的目标路径
// 必须是绝对路径(使用 Node.js 的 path 模块)

filename: "bundle.js", // string
filename: "[name].js", // 用于多个入口点(entry point)(出口点?)
filename: "[chunkhash].js", // 用于长效缓存
// 「入口分块(entry chunk)」的文件名模板(出口分块?)

publicPath: "/assets/", // string
publicPath: "",
publicPath: "https://cdn.example.com/",
// 输出解析文件的目录,url 相对于 HTML 页面

library: "MyLibrary", // string,
// 导出库(exported library)的名称

libraryTarget: "umd", // 通用模块定义
libraryTarget: "umd2", // 通用模块定义
libraryTarget: "commonjs2", // exported with module.exports
libraryTarget: "commonjs-module", // 使用 module.exports 导出
libraryTarget: "commonjs", // 作为 exports 的属性导出
libraryTarget: "amd", // 使用 AMD 定义方法来定义
libraryTarget: "this", // 在 this 上设置属性
libraryTarget: "var", // 变量定义于根作用域下
libraryTarget: "assign", // 盲分配(blind assignment)
libraryTarget: "window", // 在 window 对象上设置属性
libraryTarget: "global", // property set to global object
libraryTarget: "jsonp", // jsonp wrapper
// 导出库(exported library)的类型

/* 高级输出配置(点击显示) */
pathinfo: true, // boolean
// 在生成代码时,引入相关的模块、导出、请求等有帮助的路径信息。

chunkFilename: "[id].js",
chunkFilename: "[chunkhash].js", // 长效缓存(/guides/caching)
// 「附加分块(additional chunk)」的文件名模板

jsonpFunction: "myWebpackJsonp", // string
// 用于加载分块的 JSONP 函数名

sourceMapFilename: "[file].map", // string
sourceMapFilename: "sourcemaps/[file].map", // string
// 「source map 位置」的文件名模板

devtoolModuleFilenameTemplate: "webpack:///[resource-path]", // string
// 「devtool 中模块」的文件名模板

devtoolFallbackModuleFilenameTemplate: "webpack:///[resource-path]?[hash]", // string
// 「devtool 中模块」的文件名模板(用于冲突)

umdNamedDefine: true, // boolean
// 在 UMD 库中使用命名的 AMD 模块

crossOriginLoading: "use-credentials", // 枚举
crossOriginLoading: "anonymous",
crossOriginLoading: false,
// 指定运行时如何发出跨域请求问题

/* 专家级输出配置(自行承担风险) */

devtoolLineToLine: {
test: /\.jsx$/
},
// 为这些模块使用 1:1 映射 SourceMaps(快速)

hotUpdateMainFilename: "[hash].hot-update.json", // string
// 「HMR 清单」的文件名模板

hotUpdateChunkFilename: "[id].[hash].hot-update.js", // string
// 「HMR 分块」的文件名模板

sourcePrefix: "\t", // string
// 包内前置式模块资源具有更好可读性
},

module: {
// 关于模块配置

rules: [
// 模块规则(配置 loader、解析器等选项)

{
test: /\.jsx?$/,
include: [
path.resolve(__dirname, "app")
],
exclude: [
path.resolve(__dirname, "app/demo-files")
],
// 这里是匹配条件,每个选项都接收一个正则表达式或字符串
// test 和 include 具有相同的作用,都是必须匹配选项
// exclude 是必不匹配选项(优先于 test 和 include)
// 最佳实践:
// - 只在 test 和 文件名匹配 中使用正则表达式
// - 在 include 和 exclude 中使用绝对路径数组
// - 尽量避免 exclude,更倾向于使用 include

issuer: { test, include, exclude },
// issuer 条件(导入源)

enforce: "pre",
enforce: "post",
// 标识应用这些规则,即使规则覆盖(高级选项)

loader: "babel-loader",
// 应该应用的 loader,它相对上下文解析
// 为了更清晰,`-loader` 后缀在 webpack 2 中不再是可选的
// 查看 webpack 1 升级指南。

options: {
presets: ["es2015"]
},
// loader 的可选项
},

{
test: "\.html$"

use: [
// 应用多个 loader 和选项
"htmllint-loader",
{
loader: "html-loader",
options: {
/* ... */
}
}
]
},

{ oneOf: [ /* rules */ ] },
// 只使用这些嵌套规则之一

{ rules: [ /* rules */ ] },
// 使用所有这些嵌套规则(合并可用条件)

{ resource: { and: [ /* 条件 */ ] } },
// 仅当所有条件都匹配时才匹配

{ resource: { or: [ /* 条件 */ ] } },
{ resource: [ /* 条件 */ ] },
// 任意条件匹配时匹配(默认为数组)

{ resource: { not: /* 条件 */ } }
// 条件不匹配时匹配
],

/* 高级模块配置(点击展示) */
noParse: [
/special-library\.js$/
],
// 不解析这里的模块

unknownContextRequest: ".",
unknownContextRecursive: true,
unknownContextRegExp: /^\.\/.*$/,
unknownContextCritical: true,
exprContextRequest: ".",
exprContextRegExp: /^\.\/.*$/,
exprContextRecursive: true,
exprContextCritical: true,
wrappedContextRegExp: /.*/,
wrappedContextRecursive: true,
wrappedContextCritical: false,
// 配置
},

resolve: {
// 解析模块请求的选项
// (不适用于对 loader 解析)

modules: [
"node_modules",
path.resolve(__dirname, "app")
],
// 用于查找模块的目录

extensions: [".js", ".json", ".jsx", ".css"],
// 使用的扩展名

alias: {
// 模块别名列表

"module": "new-module",
// 起别名:"module" -> "new-module" 和 "module/path/file" -> "new-module/path/file"

"only-module$": "new-module",
// 起别名 "only-module" -> "new-module",但不匹配 "module/path/file" -> "new-module/path/file"

"module": path.resolve(__dirname, "app/third/module.js"),
// 起别名 "module" -> "./app/third/module.js" 和 "module/file" 会导致错误
// 模块别名相对于当前上下文导入
},
/* 可供选择的别名语法(点击展示) */
alias: [
{
name: "module",
// 旧的请求

alias: "new-module",
// 新的请求

onlyModule: true
// 如果为 true,只有 "module" 是别名
// 如果为 false,"module/inner/path" 也是别名
}
],
/* 高级解析选项(点击展示) */
symlinks: true,
// 遵循符号链接(symlinks)到新位置

descriptionFiles: ["package.json"],
// 从 package 描述中读取的文件

mainFields: ["main"],
// 从描述文件中读取的属性
// 当请求文件夹时

aliasFields: ["browser"],
// 从描述文件中读取的属性
// 以对此 package 的请求起别名

enforceExtension: false,
// 如果为 true,请求必不包括扩展名
// 如果为 false,请求可以包括扩展名

moduleExtensions: ["-module"],
enforceModuleExtension: false,
// 类似 extensions/enforceExtension,但是用模块名替换文件

unsafeCache: true,
unsafeCache: {},
// 为解析的请求启用缓存
// 这是不安全,因为文件夹结构可能会改动
// 但是性能改善是很大的

cachePredicate: (path, request) => true,
// predicate function which selects requests for caching

plugins: [
// ...
]
},

performance: {
hints: "warning", // 枚举
hints: "error", // 性能提示中抛出错误
hints: false, // 关闭性能提示
maxAssetSize: 200000, // 整数类型(以字节为单位)
maxEntrypointSize: 400000, // 整数类型(以字节为单位)
assetFilter: function(assetFilename) {
// 提供资源文件名的断言函数
return assetFilename.endsWith('.css') || assetFilename.endsWith('.js');
}
},

devtool: "source-map", // enum
devtool: "inline-source-map", // 嵌入到源文件中
devtool: "eval-source-map", // 将 SourceMap 嵌入到每个模块中
devtool: "hidden-source-map", // SourceMap 不在源文件中引用
devtool: "cheap-source-map", // 没有模块映射(module mappings)的 SourceMap 低级变体(cheap-variant)
devtool: "cheap-module-source-map", // 有模块映射(module mappings)的 SourceMap 低级变体
devtool: "eval", // 没有模块映射,而是命名模块。以牺牲细节达到最快。
// 通过在浏览器调试工具(browser devtools)中添加元信息(meta info)增强调试
// 牺牲了构建速度的 `source-map' 是最详细的。

context: __dirname, // string(绝对路径!)
// webpack 的主目录
// entry 和 module.rules.loader 选项
// 相对于此目录解析

target: "web", // 枚举
target: "webworker", // WebWorker
target: "node", // node.js 通过 require
target: "async-node", // Node.js 通过 fs and vm
target: "node-webkit", // nw.js
target: "electron-main", // electron,主进程(main process)
target: "electron-renderer", // electron,渲染进程(renderer process)
target: (compiler) => { /* ... */ }, // 自定义
// 包(bundle)应该运行的环境
// 更改 块加载行为(chunk loading behavior) 和 可用模块(available module)

externals: ["react", /^@angular\//],
externals: "react", // string(精确匹配)
externals: /^[a-z\-]+($|\/)/, // 正则
externals: { // 对象
angular: "this angular", // this["angular"]
react: { // UMD
commonjs: "react",
commonjs2: "react",
amd: "react",
root: "React"
}
},
externals: (request) => { /* ... */ return "commonjs " + request }
// 不要遵循/打包这些模块,而是在运行时从环境中请求他们

stats: "errors-only",
stats: { //object
assets: true,
colors: true,
errors: true,
errorDetails: true,
hash: true,
// ...
},
// 精确控制要显示的 bundle 信息

devServer: {
proxy: { // proxy URLs to backend development server
'/api': 'http://localhost:3000'
},
contentBase: path.join(__dirname, 'public'), // boolean | string | array, static file location
compress: true, // enable gzip compression
historyApiFallback: true, // true for index.html upon 404, object for multiple paths
hot: true, // hot module replacement. Depends on HotModuleReplacementPlugin
https: false, // true for self-signed, object for cert authority
noInfo: true, // only errors & warns on hot reload
// ...
},

plugins: [
// ...
],
// 附加插件列表


/* 高级配置(点击展示) */

resolveLoader: { /* 等同于 resolve */ }
// 独立解析选项的 loader

profile: true, // boolean
// 捕获时机信息

bail: true, //boolean
// 在第一个错误出错时抛出,而不是无视错误。

cache: false, // boolean
// 禁用/启用缓存

watch: true, // boolean
// 启用观察

watchOptions: {
aggregateTimeout: 1000, // in ms
// 将多个更改聚合到单个重构建(rebuild)

poll: true,
poll: 500, // 间隔单位 ms
// 启用轮询观察模式
// 必须用在不通知更改的文件系统中
// 即 nfs shares(译者注:Network FileSystem,最大的功能就是可以透過網路,讓不同的機器、不同的作業系統、可以彼此分享個別的檔案 ( share file ))
},

node: {
/* TODO */
},

recordsPath: path.resolve(__dirname, "build/records.json"),
recordsInputPath: path.resolve(__dirname, "build/records.json"),
recordsOutputPath: path.resolve(__dirname, "build/records.json"),
// TODO

}

安卓

  • 微信: 微信基本屏蔽大部分应用的 scheme 形式来唤起本地应用,目前有以下两种降级处理方案

    1. 与微信合作,将己方的 scheme 添加入微信白名单,则可以通过跳转到应用宝页面,再在该页面左上角点击打开按钮跳转到本地应用的目标页面(问题:整体操作流程过长,点击按钮后中间还隐藏下载应用宝的坑,实在是无语)
    2. 前端检测为微信环境,使用引导其他浏览器打开的形式处理,引导到其他浏览器再进行调起(问题:微信对此不是很友好,略有警告分险,不过一般微信不太较真)
  • 一般应用: 如手百,QQ浏览器,chrome,UC 基本均可以使用 scheme 的形式进行调起。

  • 特殊应用: 安卓低版本,或者某些特殊应用(未做详细统计),会对 scheme 会被屏蔽阻止,而导致调起无反应。

iOS

  • iOS 9+: 均可以使用Universal Link的形式调起我方APP,包括微信等,无需像安卓一样使用降级方案。(但存在一个较大问题为:由于Universal Link 本身就是一个 https 链接,在使用该链接唤起本地应用之后,应用左上角与右上角会出现返回之前的应用和跳转 Universal Link 的箭头,当我点击右上角前进的箭头后,会使用 safari打开Universal Link的页面,并且在原应用中再也无法调起目标应用,目前该情况的处理方法为 引导用户使用safari打开,下拉页面,使用苹果自身的提示框进行打开)

  • iOS 8-: 不支持Universal Link,部分可以使用scheme的形式调起,而绝大多数应用为屏蔽scheme,降级方案为尝试调起,并且跳转苹果商店的应用页面

基本概念

Generator Function(生成器函数),为一种可以从中退出,可以重新进入的,并且环境中的变量依旧会被保留的函数。粗略感觉有点类似于闭包存储的功能效果,不过Generator Function的使用场景不仅仅与此。

以下为 MDN 对 Generator Function 的大致描述:

调用一个生成器函数,并不会马上执行其主体方法,而是会生成返回一个该生成器函数的迭代器(Iterator)对象。当这个迭代器的 next 方法被调用时,生成器函数的主体会被执行到第一个 yield 表达式,该表达式定义了迭代器的返回值。或者被 yield* 委派至另一个生成器函数的迭代器对象。next 方法调用之后返回一个具有value属性 和一个done属性的对象,分别表示产出值和是否是最后的产出值。

简单的示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 一个生成器函数
function* idFactory(){
var id = 0;
yield id++;
}

// 生成一个迭代器对象
var maker = idFactory();

// 调用迭代器对象的next方法,获得返回值对象
maker.next(); // {value: 0, done: false}
// 第一个yield下去无代码执行
maker.next(); // {value: undefined, done: true}

function* & yield

function*为生成器函数的声明,而yeild则为生成器函数内部的关键词,作为该生成器函数产生的迭代器对象调用next方法时返回明确的期望的返回值,并从迭代器对象中暂停,之后某一时间仍能再次进入该迭代器,犹如打了个高级的断点的return。

或许你会疑惑,上面例子中,按期望来说,由于仅有一次yeild的执行,在第一次调用next后,返回值中便期望 done 为 true,却返回 false,并且在再次调用之后,done 才变为 true。

这块就需要对于next做简单的理解:

  1. next 在遇到 yield 之后,暂定后续操作,便将 yield 后的表达式作为返回对象的 value 值,done 为 false
  2. 再次调用 next,便往下执行,直到遇到下一个 yield
  3. 如遇到 return,便将 return 的表达式作为返回对象的 value 值,done 为 true,迭代器关闭
  4. 如往下执行中无 yield,也无 return,则返回 value 为 undefined,done 为 true,迭代器关闭
  5. 对于已经关闭的迭代器,调用next,始终返回value 为 undefined,done 为 true

所以说,暂停之后,是无法去判断接下来是否还会运行到 yield,故而,第一个例子中,会在再次调用之后,明确此刻迭代器被关闭之后,done就有明确的返回值为true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 一个生成器函数
function* idFactory(){
var id = 0;
yield id++;
yield id++;
return 2;
yield id++;
}

// 生成一个迭代器对象
var maker = idFactory();

// 调用迭代器对象的next方法,获得返回值对象
maker.next(); // {value: 0, done: false}
maker.next(); // {value: 1, done: false}
maker.next(); // {value: 2, done: true}
maker.next(); // {value: undefined, done: true}

若需要一个类似的无限迭代器,则需要暂停之后有所逻辑执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 一个生成器函数
function* idFactory(){
var id = 0;
while(1) {
yield id++;
}
}

// 生成一个迭代器对象
var maker = idFactory();

// 调用迭代器对象的next方法,获得返回值对象
maker.next(); // {value: 0, done: false}
maker.next(); // {value: 1, done: false}
maker.next(); // {value: 2, done: false}

yield*

先来看一则小实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function* idFactory(){
var id = 0;
yield id++;
yield idFactory2();
yield id++;
}
function* idFactory2(){
yield 'X';
}

var maker = idFactory();
maker.next(); // {value: 0, done: false}
maker.next(); // {value: idFactory2, done: false}
maker.next(); // {value: 1, done: false}

迭代器在第二处的yield上,会返回一个迭代器,而非进入迭代器进行迭代,这边需要使用到yield*来进行处理,使得第二处yield进入内部的迭代器产生迭代。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function* idFactory(){
var id = 0;
yield id++;
yield* idFactory2();
yield id++;
}
function* idFactory2(){
yield 'X';
}

var maker = idFactory();
maker.next(); // {value: 0, done: false}
maker.next(); // {value: 'X', done: false}
maker.next(); // {value: 1, done: false}

关于yield* 一个迭代对象,相当于在此处分别将该可迭代对象依次分开 yield 出去而已。并且除了生成器函数生成的迭代器可作为此处的迭代对象,包括数组,字符串等也可以作为迭代对象来进行使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function* idFactory(){
var id = 0;
yield id++;
yield* "abc";
yield* ['x','y'];
yield id++;
}
var maker = idFactory();

maker.next(); // {value: 0, done: false}
maker.next(); // {value: "a", done: false}
maker.next(); // {value: "b", done: false}
maker.next(); // {value: "c", done: false}
maker.next(); // {value: "x", done: false}
maker.next(); // {value: "y", done: false}
maker.next(); // {value: 1, done: false}

唯一需要注意的是,由于 yield* 本身为表达式,其返回值便是可迭代对象迭代完毕(done为true)时的返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function* idFactory(){
var id = 0;
yield id++;
yield* idFactory2();
yield id++;
}
function* idFactory2(){
yield 'x';
return 'y'
}
var maker = idFactory();

maker.next(); // {value: 0, done: false}
maker.next(); // {value: "x", done: false}
maker.next(); // {value: 1, done: false}

当 yield* 所迭代的迭代器,迭代完毕之后,此次 next 的调用会继续执行,而不是抛出 {value: ‘y’, done: true},所以为以上所示。

总结

简单的来说,关于ES6中Generator Function有以下几个特征:

  • 通过关键词 function* 进行声明,
  • 调用之后并非执行其主体代码,而是返回该生成器函数的一个迭代器对象
  • 迭代器对象通过next方法来进行迭代运行
  • 迭代的返回值对象,每次运行以yield进行定义的返回值
  • 对于生成器中的yield*来说,可以吧需要的yield的值委托给另一个新的迭代器或者可迭代的对象进行迭代。

相关阅读:

MDN function*

MDN yield*

最初知晓Redux这个概念就是在学习React的时候,简单的来说就是合理的来管理你react项目中的state变化操作。不过在开始时,一看其Action、Reducer、Store以及使用示例,表示真的会一脸懵逼。于是最近再比较详细的学习下Redux。

Redux 简介

这里为最详细的官方文档 - Redux官方文档,这个文档介绍的非常非常详细,建议花大半天的时间与示例代码一起学习,基本就能完全理解Redux。

首先要说的是虽然很多人知晓Redux是在React的相关内容中,不过需要清楚,Redux与React确实是没有半毛钱关系,完全无依赖,均是独立的类库而已。就像你隔壁家的大婶的侄女和你大伯邻居家的儿子的关系一样,然而人家可以一见钟情那就是另外一回事了。

再说对于React来说其最大的优势就是严格控制了开发者对与DOM的直接操作,一直以来前端性能上很大的一个弊端就是开发者不合理以及频繁的去操作DOM,React以其高效的虚拟DOM对象、diff算法等,使得其以最优的方案来控制解决DOM的操作。

另外一个层面就是React将整个框架以及内部的组件视为一个个状态机,通过state来进行对虚拟DOM的控制以至来触发真正的DOM操作,不过这就会使得在一些复杂应用中带来另外一些问题,对于state的控制方面,越来越多的state,将在项目开发、版本迭代变中产生强相关和越来越不可控。这就意味着人为的去操作一个个state,渐渐开始变得像先前开发人员独自操作DOM一样让人难以忍受,就好像React变成了曾经它最痛恨的那种形式。杂乱的state操作最后就导致具体某一个state在某个时刻或者某个地方别改变,变得非常难以去查询。

相对于此问题的解决方案慢慢应运而生,Redux便是一种解决方案(但是相对来说Redux与React是没有关系的)。Redux强制在每一个应用中仅能使用整体单一的一个reducer生成唯一的一个store,来对state进行存储与操作,并且需要开发者独立定义各类形式的action,在触发某一action后通过所注册的reducer才能对某一状态进行操作。

简单的说就是将state的操作进行封装,禁止开发者随意使用,必须通过触发某一action操作,来触发相对应注册的reducer来对store内部相应的state进行操作。项目初期它不能提高我们对于项目的开发效率,不过后期以及项目维护阶段,它能使我们的项目大大增强了可维护性和可扩展性,使其变成一个真正健硕的项目工程。

Redux 基础

Action

action 意味着一个将数据中应用传递到store的操作定义,为一种对已发生事件的表达。纯粹的action仅仅只是一种定义,一般需要通过store.dispatch()实际出发才会生效。

内部的type字段为默认的定义Action类型的字段

1
2
3
4
{
type: 'ADD_ITEM',
text: 'add an item'
}

亦或创建Action函数,以便于更好的移植和复用

1
2
3
4
5
6
function addItem(text){
return {
type: 'ADD_ITEM',
text
}
}

这便可以称为一个Action,当然任意Action,需要被dispatch之后才会生效。

Reducer

Action 只是对一个已发生行为进行描述,而Reducer则是处理触发了某一个实例Action进行更新state的一个过程的定义。简单的说就是某一个Action被触发之后,Action内部的数据会被带入到注册的Reducer内执行,根据Action的type类型进行对应的store变更操作。

首先,我们需要定义一个整体的state结构,这块内容需要在开发前进行思考组织。

1
2
3
4
5
6
7
8
9
10
{
todos: [
{
text: 'item 1'
},
{
text: 'item 2'
}
]
}

随即,编写纯函数Reducer,来接受旧state,action,返回新的state。

纯函数意味着禁止在Reducer中做:

  • 修改传入的参数
  • 执行具有副作用的操作,如跳转,各类其他操作
  • 调用非纯函数,如事件函数,随即函数等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const initialState = {
todos: []
}

function todoAPP(state = initialState, action){
switch(action.type){
case 'ADD_ITEM':
return {
todos:[
...state.todos,
{
text: action.text
}
]
}
}
}

对于实际应用中,对应的action数量会很多,在一个reducer中会使得switch结构维护性过差,所以都会使用到拆分reducer,最后使用combineReducers进行合并生成最终的reducer。

Store

Redux应用只有唯一的store,通过应用唯一的最终Reducer生成而来。store是对state进行管理的中间件,当某一action被触发传入store的dispatch,触发注册的Reducer来改变store中对应的state。

1
const store = createStore(todoAPP)

store职责为:

  • 维持应用的state
  • 提供getState方法来获取state
  • 提供dispatch方法来
  • 提供subscribe进行注册事件
  • 使用注册事件的返回值来注销事件

发起action,触发改变state

1
store.dispatch(addItem('item 3'))

整体来说,Redux的核心便是,控制开发者随意变更state,使用action来抽象具体的操作行为,由reducer生成的store进行对整体的state进行控制,在dispatch接受具体的action数据后来管理对于state的变更。

Redux 思想

  • 单一数据源,应用整体归结到一个state,存储应用在唯一的store中。state设计的扁平化,抽象化是确保其高效与可维护的的重要因素
  • state为只读,触发action是唯一改变state的方式
  • 纯函数来编写reducers,改变state

当然,还要包括异步的Action实现,Reducer的复用与性能,combineReducers高级用法等均可以在中文文档中获得答案,以及GitHub上有非常丰富的示例。

水平居中

  • 元素使用固定宽度,margin左右值设为auto。(元素须为确定宽度)
  • 父元素设置text-align:center,子元素设置display:inline-block。(需要处理inline-block带来的兼容性问题)
  • 父元素偏移外层区域的50%宽度,子元素偏移父元素的50%宽度,达到居中效果。(实现较为复杂,需要固定父级宽度)
  • 元素偏移外层区域的50%,负margin来偏移元素的一半宽度。(实现较为复杂,需要固定宽度)
  • CSS3的fit-content方法:width: fit-content;(兼容性问题)
  • Flex实现:justify-content: center;(兼容性问题)

垂直居中

  • 将line-height设置成和height值一样。(仅限于单行文字与图片居中)
  • 绝对定位,并且定位高度top:50%,负margin偏移高度的一半。(需要固定高度)
  • 模拟table样式,父级元素设置display:table,子元素设置diapaly:table-cell,vertical-align:middle即可。(增加结构的复杂度,IE6,7下不支持)
  • 添加一个空标签,高度为父元素的50%,负margin为元素高度的一半。(需要固定高度,并且添加无意义标签)
  • 添加一个空标签,将其与目标元素,display:inline-block,vertical-align:middle,然后借助该空元素的高度设置为父级的100%,使得目标元素实现垂直居中。(多添加空标签)
  • Flex实现:align-items: center;(兼容性问题)

等高对齐

  • 背景图片的假等高展现,使用多色背景图填充容器背景来伪装成等高。(并非真正的等高,背景伪装)
  • 多层元素嵌套并且偏移,实现背景色上的等高,内部嵌入相关元素,随一高度变化,外层背景元素均会被撑开到最大元素等高。(实现结构过于复杂)
  • 使用边框来伪装另一列的背景色。(多列会有问题)
  • 所有列中使用正的上、下padding和负的上、下margin,并在所有列外面加上一个容器,并设置overflow:hiden把溢出背景切掉。(底部边框会有问题,使用添加一个元素来模拟边框效果)
  • 模拟表格布局来实现等高。(IE低版本不兼容)
  • Flex实现:align-items: stretch;(兼容性问题)

来源: http://www.w3cplus.com/

问题1: 大部分iOS浏览器中 click 时间有延迟感,没有及时触发绑定事件。

问题2:使用touchend,或者zepto的tap事件,出现一些奇观的穿透点击触发事件

主要原因:iOS历史上一直存在页面的默认事件-双击放大,导致对于浏览器判断此次是否为双击时间需要设置一定的等待时间,故而click点击事件相对于touchend时间会延迟300ms左右,以保证此次确定为单次的点击事件。

FastClick为用来解决消除移动浏览器上的点击事件与触摸事件之间的300ms。以下为其简单的代码逻辑。

1.首先,在初始化时 对touch click各类事件各绑定一个监听函数 保证触发时在同个事件中为最早触发的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
function FastClick(layer, options) {
var oldOnClick;
options = options || {};

this.trackingClick = false; // 当前的点击是否是被标记的
this.trackingClickStart = 0; // 点击被标记时的时间戳
this.targetElement = null; // 被标记点击的元素

this.touchStartX = 0; // touch开始时的横坐标值
this.touchStartY = 0; // touch开始时的纵坐标值
this.lastTouchIdentifier = 0; // 最后一次touch的id, 为Touch.identifier.
this.touchBoundary = options.touchBoundary || 10; // 标识 Touchmove 的分界线, 如超过后则取消触发点击.

this.layer = layer; // 绑定FastClick的layer.
this.tapDelay = options.tapDelay || 200; // 判断为 tap(touchstart 和 touchend) 事件之间最小时间间隔
this.tapTimeout = options.tapTimeout || 700; // 判断为 tap 的最大时间间隔

if (FastClick.notNeeded(layer)) {
return;
}

// 部分老版本安卓没有 Function.prototype.bind
function bind(method, context) {
return function() { return method.apply(context, arguments); };
}

// 添加实例的几个事件方法
var methods = ['onMouse', 'onClick', 'onTouchStart', 'onTouchMove', 'onTouchEnd', 'onTouchCancel'];
var context = this;
for (var i = 0, l = methods.length; i < l; i++) {
context[methods[i]] = bind(context[methods[i]], context);
}

// 更具需要来设置事件
if (deviceIsAndroid) {
layer.addEventListener('mouseover', this.onMouse, true);
layer.addEventListener('mousedown', this.onMouse, true);
layer.addEventListener('mouseup', this.onMouse, true);
}

layer.addEventListener('click', this.onClick, true);
layer.addEventListener('touchstart', this.onTouchStart, false);
layer.addEventListener('touchmove', this.onTouchMove, false);
layer.addEventListener('touchend', this.onTouchEnd, false);
layer.addEventListener('touchcancel', this.onTouchCancel, false);

// 对于浏览器不支持事件 stopImmediatePropagation的hack一下 (e.g. Android 2)
// 这是 FastClick 通常如何阻止的形式 点击事件在冒泡到 FastClick layer 注册的回调方法之前就被取消。

if (!Event.prototype.stopImmediatePropagation) {
layer.removeEventListener = function(type, callback, capture) {
var rmv = Node.prototype.removeEventListener;
if (type === 'click') {
rmv.call(layer, type, callback.hijacked || callback, capture);
} else {
rmv.call(layer, type, callback, capture);
}
};

// 重写layer的绑定事件方法 单独处理click
layer.addEventListener = function(type, callback, capture) {
var adv = Node.prototype.addEventListener;
if (type === 'click') {
adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) {
if (!event.propagationStopped) {
callback(event);
}
}), capture);
} else {
adv.call(layer, type, callback, capture);
}
};
}

// 如果事件句柄已经被绑定在元素的 onclick 属性上, 会在 FastClick的onClick事件之前触发
// 通过创建用户定义事件句柄函数,将其添加到监听中来解决
if (typeof layer.onclick === 'function') {

// 低于3.2的Android浏览器需要将 layer.onclick 指向新的引用
// 如果直接传递,会有有一定问题
oldOnClick = layer.onclick;
layer.addEventListener('click', function(event) {
oldOnClick(event);
}, false);
layer.onclick = null;
}
}

2.在点击过程中,会在 touchstart 初始化记录一些数据,用于之后的逻辑判断处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
FastClick.prototype.onTouchStart = function(event) {
var targetElement, touch, selection;

// 忽略多点 touch, 否则 在FastClick元素上的双指触摸放大缩小会被阻止 (issue #111).
if (event.targetTouches.length > 1) {
return true;
}

targetElement = this.getTargetElementFromEventTarget(event.target);
touch = event.targetTouches[0];

if (deviceIsIOS) {

// 在iOS中 只有通过的事件才会触发文案的选择取消 (issue #49)
selection = window.getSelection();
if (selection.rangeCount && !selection.isCollapsed) {
return true;
}

if (!deviceIsIOS4) {

// iOS下奇怪的事件是当 alert 或者 confirm 对话框是被点击事件的回调方法打开的(issue #23):
if (touch.identifier && touch.identifier === this.lastTouchIdentifier) {
event.preventDefault();
return false;
}

this.lastTouchIdentifier = touch.identifier;

// 如果目标元素是一个滚动元素的子元素时 (使用了 -webkit-overflow-scrolling: touch) 并且:
// 1) 用户在滚动的layer上滚动
// 2) 用户使用tap来停止滚动
// 那么 event.target 的最后的 touchend 事件将是用户最后手指下面的元素的事件
// 当滚动开始时, 会触发 FastClick 将点击事件传递到那个layer - unless a check
// 除非在发送合成点击事件前检测其父级layer并非是滚动的 (issue #42).
this.updateScrollParent(targetElement);
}
}

// 为重要标识 标识此次的触发行为为指定的fastclick
this.trackingClick = true;
this.trackingClickStart = event.timeStamp;
this.targetElement = targetElement;

this.touchStartX = touch.pageX;
this.touchStartY = touch.pageY;

// 防止双击触发fast click (issue #36)
if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {
event.preventDefault();
}

return true;
};

3.若有触发 touchmove 则使用自定义的函数进行判断,并且确认是否此次touch 不作为click 处理

1
2
3
4
5
6
7
8
9
10
11
12
13
FastClick.prototype.onTouchMove = function(event) {
if (!this.trackingClick) {
return true;
}

// 如果此次 touch 算作移动 , 取消该点击的跟踪
if (this.targetElement !== this.getTargetElementFromEventTarget(event.target) || this.touchHasMoved(event)) {
this.trackingClick = false;
this.targetElement = null;
}

return true;
};

4.touchend 时,判断此次时间是否符合click条件 合成click事件来进行模拟触发保证绑定在该元素上的点击事件能在touchend时刻立即触发, 阻止事件冒泡以及去掉默认事件使得浏览器自身的延迟的点击事件不在触发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
FastClick.prototype.onTouchEnd = function(event) {
var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement;

if (!this.trackingClick) {
return true;
}

// 阻止构造的快速双击 (issue #36)
if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {
this.cancelNextClick = true;
return true;
}

if ((event.timeStamp - this.trackingClickStart) > this.tapTimeout) {
return true;
}

// 重置错误的input上的点击取消 (issue #156).
this.cancelNextClick = false;

this.lastClickTime = event.timeStamp;

trackingClickStart = this.trackingClickStart;
this.trackingClick = false;
this.trackingClickStart = 0;

// 在一些iOS设备中, 如果目标元素的层正处于过渡变换或者滚动的时候,
// 其对于事件的支持是无效的除非再次手动检测.
// See issue #57; also filed as rdar://13048589 .
if (deviceIsIOSWithBadTarget) {
touch = event.changedTouches[0];

// 在某些情况下 elementFromPoint 未负数, 所以阻止将目标元素设置为null
targetElement = document.elementFromPoint(touch.pageX - window.pageXOffset, touch.pageY - window.pageYOffset) || targetElement;
targetElement.fastClickScrollParent = this.targetElement.fastClickScrollParent;
}

// 为label标签的时候 findControl 就近查找元素
targetTagName = targetElement.tagName.toLowerCase();
if (targetTagName === 'label') {
forElement = this.findControl(targetElement);
if (forElement) {
this.focus(targetElement);
if (deviceIsAndroid) {
return false;
}

targetElement = forElement;
}
} else if (this.needsFocus(targetElement)) {
// 需要聚焦后模拟点击的

// Case 1 快速双击情况: touch触发在一会会之前 (基于测试 100ms 左右 for issue #36) 出发聚焦.
// 尽早返回并且不设置target element的引用 以便后续的点击可被通过.
// Case 2: 当document处于一个iframe内部时,所有的输入元素都如此 (issue #37).
if ((event.timeStamp - trackingClickStart) > 100 || (deviceIsIOS && window.top !== window && targetTagName === 'input')) {
this.targetElement = null;
return false;
}

this.focus(targetElement);
this.sendClick(targetElement, event);

// iOS 4 选择元素直接pass
if (!deviceIsIOS || targetTagName !== 'select') {
this.targetElement = null;
event.preventDefault();
}

return false;
}

if (deviceIsIOS && !deviceIsIOS4) {

// 父级为滚动layer的情况(usually initiated by a fling - issue #42).
scrollParent = targetElement.fastClickScrollParent;
if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) {
return true;
}
}

// 阻止真实点击通过 - 除非是标记的元素
// real clicks or if it is in the whitelist in which case only non-programmatic clicks are permitted.
if (!this.needsClick(targetElement)) {
event.preventDefault();
this.sendClick(targetElement, event);
}

return false;
};

FastClick.prototype.sendClick = function(targetElement, event) {
var clickEvent, touch;

// 在一些安卓设备下,需要先进行聚焦,否则无法调用合成的click事件 (#24)
if (document.activeElement && document.activeElement !== targetElement) {
document.activeElement.blur();
}

touch = event.changedTouches[0];

// 合成一个点击事件, 添加一个额外属性以便于跟踪
// 合成事件 还真没见过
clickEvent = document.createEvent('MouseEvents');
clickEvent.initMouseEvent(this.determineEventType(targetElement), true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);
clickEvent.forwardedTouchEvent = true;
targetElement.dispatchEvent(clickEvent);
};

总结:FastClick 经典之处在于,初始化时绑定第一时间触发的touch事件,在touchend时便判断此次行为是否可作为click,如果此次touch满足click事件,则在touchend事件中直接合成click并且进行触发,然后再touchend中阻止事件冒泡和默认事件等,来防止默认延迟的click事件延迟后又被触发。

移动性能

简单总结对于移动端上,前端开发中可以注意的性能点。各点都有利有弊,仍需按照各自情况权衡考虑。

加载时性能优化考虑

图片

  • 图片压缩,适度考虑有损压缩,包括考虑WebP图片格式
  • CSS Sprites 或者 base64编码代替图片(文件变大,还需解码),须注意使用场景以及图片大小
  • 使用CSS3来绘制简单的图形与图像
  • 图片资源依照可视区域进行按需加载
  • 小型大批量的小ICON 考虑使用字体形式展现处理
  • 使用 srcset 屏幕密度现实对应尺寸图片

资源

  • CSS 中避免 @import 引入
  • 确保静态资源优化压缩
  • 非首屏静态资源使用延时加载
  • SPA考虑延迟加载非首屏业务模块资源
  • 缓存一切可以缓存的资源,使用MD5形式标识文件

请求

  • 初始请求资源数 < 4 (Android支持4个,iOS 5后可支持6个)
  • 初始请求资源gzip后总体积 < 50kb
  • 开启Keep-Alive链路复用
  • 数据离线化,考虑将数据缓存在 localStorage以及indexDB 中

运行时性能优化考虑

  • HTML结构语义化控制与优化,保持简单高效
  • 使用CSS3动画来代替JS动画,考虑GPU加速情况
  • Display,Float等部分属性会影响页面的渲染,须合理使用
  • 事件委托代理使用
  • 考虑操作中的重绘与重排情
  • 缓存DOM选择与计算处理,减少高级选择器以及通配选择器的使用
  • 注意点击事件是300ms左右延时问题(由于需要判断是否是双击时间导致)
  • 对于高频触发事件(scroll / resize / touchmove等),进行节流与消抖
  • 避免在低端机上的效果使用,考虑优雅降级以及渐进增强的形式

贴一张腾讯优化法则图

image

再看一遍雅虎军规

  • Minimize HTTP Requests - 减少HTTP请求
  • Use a Content Delivery Network - 利用CDN技术
  • Add an Expires or a Cache-Control Header - 设置头文件过期或者静态缓存
  • Gzip Components - Gzip压缩
  • Put Stylesheets at the Top - 把CSS放顶部
  • Put Scripts at the Bottom - 把JS放底部
  • Avoid CSS Expressions - 避免CSS表达式
  • Make JavaScript and CSS External - 使用JS和CSS外链
  • Reduce DNS Lookups - 减少DNS查找
  • Minify JavaScript and CSS - 压缩JS和CSS的体积
  • Avoid Redirects - 避免重定向
  • Remove Duplicate Scripts - 删除重复脚本
  • Configure ETags - 配置ETags
  • Make Ajax Cacheable - 缓存Ajax
  • Flush the Buffer Early - 尽早的释放缓冲
  • Use GET for AJAX Requests - 用GET方式进行AJAX请求
  • Post-load Components - 延迟加载组件
  • Preload components - 预加载组件
  • Reduce the Number of DOM Elements - 减少DOM元素数量
  • Split Components Across Domains - 跨域分离组件
  • Minimize the Number of iframes - 减少iframe数量
  • No 404s - 避免404页面
  • Reduce Cookie Size - 减小Cookie
  • Use Cookie-free Domains for Components - 对组件使用无Cookie的域名
  • Minimize DOM Access - 减少DOM的访问次数
  • Develop Smart Event Handlers - 开发灵活的事件处理句柄
  • Choose < link >over @import - 使用< link >而非 @import
  • Avoid Filters - 避免过滤器的使用
  • Optimize Images - 优化图片
  • Optimize CSS Sprites - 优化CSS Sprites
  • Don’t Scale Images in HTML - 不要在HTML中缩放图片
  • Make favicon. ico Small and Cacheable - 缩小favicon. ico的大小并缓存它
  • Keep Components under 25K - 保证组件在25K以下
  • Pack Components into a Multipart Document - 将组件打包进一个多部分的文档中