2017 年 3 月:15 个有趣的 JS 和 CSS 库 – 知乎专栏

admin阅读(3657)评论(0)

我们创办 Tutorialzine 的使命是让你及时的了解 Web 开发中最新、最酷的趋势。所以,每个月我们都会发布一些精心挑选的优秀 Web 开发资源,同时我们也相信,它们值得你的关注。

1. Propeller

Propeller 是基于 Bootstrap 与谷歌的 Material Design 的 CSS 组件框架。它包含了 25 个响应式组件,同时具备典型的 Material Design 动画。你可以将它作为 Bootstrap 的主题,或者完整的框架以及独立组件进行使用。

项目地址:【传送门

2. BaguetteBox

BaguetteBox 是一个利用纯 JavaScript 编写的库,它可用于创建响应式灯箱画廊效果。它是轻量级的,可自定义设置,并支持移动端,同时它还自带了相应的 CSS3 过渡。
我们最近利用这个库制作了 4 个免费的 Bootstrap Gallery 模板包,我想我们已经喜欢上它了。

项目地址:【传送门

3. Whitestorm

Whitestorm 是基于 Three.js 引擎用来开发 3D Web APPS 和游戏的框架。它为许多常见的 Three.js 任务提供了简单的封装,使搭建环境、创建对象、添加物件等操作更加的简单。官方也提供了示例项目,以及与 React 集成的工具,便于你迅速的开始工作。

项目地址:【传送门

4. Animatelo

Animatelo 是流行的 Animate.css 库的接口,它利用 Web Animation API 替代了 CSS 过渡,并将 Animate.css 库中的所有效果重现了出来。但它的 API 是基于 Javascript 方法的,而不是 CSS 类。同时,它也是轻量级的库,不依赖 jQuery,但在一些老版本的浏览器上或许要使用 polyfill。

项目地址:【传送门

5. FuseBox

FuseBox 是一个用于 JavaScript 和 CSS 的打包程序,并具备用于 TypeScript,Sass 等的附加组件。它的设计理念是简单与性能,也为 Webpack 提供了可行的替代解决方案。
为了让你快速开始,官方提供了 Angular 2 + TypeScriptReact + BabelVue.jsElectron 和其他版本的快速示例教程。

项目地址:【传送门

6. Yargs

Yargs 是使用 Node.js 构建功能齐全的命令行应用程序的框架。你可以轻松的配置命令、解析多个参数以及设置快捷操作方式。甚至,它还可以自动生成帮助菜单。

项目地址:【传送门

7. WebGradients

WebGradients 收集了一系列漂亮的色彩渐变,你可以将它们轻松的应用在任何 HTML 页面上。你可以通过快速预览,或是全屏查看可用的渐变色,然后仅需要一键复制 CSS 属性,即可将它们应用到你的项目中去。

项目地址:【传送门

8. Sticky-Kit

Sticky-kit 是一个 jQuery 插件,它可以将元素附加到页面上的某个区域,并使元素保持其边界。这样随着页面的滚动,在父容器中的特定元素将会保持始终可见。

项目地址:【传送门

9. ScrollDir

ScrollDir 是一款超轻量的,不依赖 JavaScript 的库,用于监测 CSS 中的滚动方向。它可以察看滚动条的移动方向,并根据你选择的元素来切换上/下方向的数据属性。同时,它忽略了小的滚动动作,力求创造一个平稳的体验。

项目地址:【传送门

10. Svgo

这是一个用于优化 SVG 文件的 Node.js 工具,它可以删除那些无用的 SVG 信息,例如编辑器元信息、注释、隐藏元素以及不影响渲染向量的其他属性。同时,它基于插件模式构建,所以你可以自由的选择想要删除的内容。

项目地址:【传送门

11. Store.js

Store.js 是用于本地存储的跨浏览器解决方案。近期,它的 2.0 版本也已发布,在更新了许多功能的同时,增加了一些额外的功能,例如数组/对象操作以及改进了过期选项。在上一期的每月 Web 开发资源列表中,我们为大家分享了一个名为 localForage 的库。它提供了许多与 Store.js 类似的功能,但是具有更多的 localStorage 类语法。喜欢的话,不妨来看看。

项目地址:【传送门

12. Snarkdown

Snarkdown 是一个使用 JavaScript 编写的超简单的 Markdown 解析器。诚然,它不是最复杂或功能最全的解析器,但它可能是最容易实现的。Snarkdown 只有 1kb 大小,且只有一种方法,使其成为速成项目的完美选择。

项目地址:【传送门

13. Unfetch

Fetch API 是 XMLHttpRequest 接口的现代重制,它为开发人员提供了一种更好的处理异步请求的方法。虽然,它支持大多数现代浏览器,但是 fetch() 方法在 IE 中仍然不可用。Unfetch 的出现便解决了 fetch() 方法在 IE 中不可用的问题,它提供了完整 fetch API 的子集,且 fetch polyfill 仅有 500 bytes。

项目地址:【传送门

14. Scrollanim

Scrollanim 是用于滚动动画的 Vanilla JavaScript 库。由于内置的 Animate.css 依赖关系,Scrollanim 提供了大量的自定义选项,单独的 HTML 和 JavaScript API 以及超过 50 种流畅的动画效果。

项目地址:【传送门

15. Neurojs

Neurojs 是一款用于在浏览器中进行深度学习 JavaScript 框架,它具备可以通过加强学习训练的全栈神经网络。例如,该项目就展示了一个很酷的 Demo,其中自动驾驶汽车在 2D 环境中学习导航。

项目地址:【传送门

感谢你的阅读。

注:

  1. 本文版权归原作者所有,仅用于学习与交流。
  2. 如需转载译文,烦请按下方注明出处信息,谢谢!

英文原文:15 Interesting JavaScript and CSS Libraries for March 2017
作者:Danny Markov
译者:IT程序狮
译文地址:jianshu.com/p/879be1dcb

来源: 2017 年 3 月:15 个有趣的 JS 和 CSS 库 – 知乎专栏

Houdini:CSS 领域最令人振奋的革新

admin阅读(4688)评论(0)

原文链接:Houdini: Maybe The Most Exciting Development In CSS You’ve Never Heard Of

已得到原文作者 Phil Walton 以及原文发布平台 smashingmag 授权翻译
想要使用某种 CSS 特性,但是因为浏览器兼容性问题而没法使用?更糟糕一点,所有浏览器都支持这种特性,但支持度不完全,在某些情况下会有 bug 出现、支持性不一致,甚至于完全不兼容。如果你曾经遇到过上述情况(我肯定你一定遇到过),那你得好好关注 Houdini

Houdini 是 W3C 新成立的一个任务小组,它的终极目标是实现 css 属性的完全兼容。Houdini 提出了一个前无古人的的设想:开放 CSS 的 API 给开发者,开发者可以通过这套接口自行扩展 CSS,并提供相应的工具允许开发者介入浏览器渲染引擎的样式和布局流程中。

但是……这意味着什么呢?这个方案靠谱吗?不管是现在还是将来,Houdini 对开发者进行扩展开发有什么帮助呢?

我将在后面的文章里尽可能地回答上面这三个问题。但在开始回答之前,要先搞清楚我们现在面临的问题是什么,以及为什么出现 Houdini 这样的解决方案。弄明白问题是什么之后,我再告诉大家 Houdini 将会如何解决这些问题,以及它目前的进展。最后,开发者们,你们还能了解具体如何做才能让 Houdini 尽早落地。

Houdini 能解决什么问题?

写文章也罢做 demo 也罢,每一次当我想要炫耀点 CSS 新花样的时候,总有人会说“这效果真是屌炸天!然而我们现在并不能好好用它,至少等个 10 年吧”。

虽然每每我都觉得这样的评论让人生气又没啥建设性,但依然承认大家的担心有理有据。纵观 CSS 历史,每一份特性草案都是经过了许多年才被广泛应用。而之所以会是“许多年”,就是因为要允许一个新特性被写入 CSS 标准需要经过一整套标准制定流程,然而就过了很多年……

我对于这个标准制定的流程肯定是毫无异议的,但你得承认,走完整个流程花儿都谢了。

flexbox 大概就是最好的例子,2009年,关于 flexbox 的草案第一次被提出,但直到今天开发者们还在抱怨着这个属性的浏览器兼容性问题。不过感谢上帝,随着现代浏览器的自动更新机制,这个问题总有解决的那一天。但是,按照现在的新属性发布流程,最新的浏览器也会和新属性提案仍然存在一个时间差。

不过同样在 web 世界里,对于如今的 JavaScript 来说好像不是什么事儿了:

在上面的流程图中,我们可以看到从想到一个新的 js 特性到运用在生产环境,大概只需要几天时间。讲真,我已经在使用 asyncawait 了,然而目前没有浏览器天生支持这两个方法吧。

估计现在你也大概感受到了 CSS 社区和 JS 社区的画风不同了:在 JS 社区里,你总能听到人们在抱怨它一天一个样 -- 跑得太快追得太累;而 CSS 社区,人们却在叹息着花精力去学习新事物是件多么吃力不讨好的事儿 -- 天知道什么时候才能用上新玩意呢。

既然如此,为什么我们不能用上 CSS Polyfill ?

脑海里闪过的第一个解决方案就是 CSS polyfill,只要 CSS polyfill 足够强大,相信 CSS 的发展速度赶超 JavaScript 不是梦。

然而,给 CSS 打补丁做起来有多难你都想不到,而且在大部分情况下,只要这么做了,性能肯定会受到影响。

JavaScript 是一种动态语言,它是如此之“动态”以至于有着让人惊叹的可扩展性,所以用 JavaScript 给 JavaScript 打补丁是可以轻松实现的,但 CSS 不是动态的呀。在某些情况下,你可以在构建过程中将一种形式的 CSS 转译成另一种形式(PostCSS 就是一个典型的例子)。而一旦你的补丁是依赖于的 DOM 结构、某一个元素的布局或者它的定位的话,那补丁程序就需要在本地运行了。

不幸的是,要在浏览器中实现这种方案挺难的。

让我们来看看一个 HTML 文档从被浏览器接收到显示在屏幕上的全过程,下面这张图里被标为蓝色的部分就是 JavaScript 可以染指的环节了:

作为开发者,看着这张图心都凉了,我们根本控制不了浏览器解析 HTML 和 CSS 的过程,只能看着它生成 DOMCSS object model (CSSOM)。没法控制级联(cascade)、没法控制浏览器布局元素的方式(layout)、也没法控制元素在屏幕上显示的过程(paint)、最后的合成(composite)也无能为力。

整个过程中,开发者能完全控制的唯一环节就是 DOM,另外 CSSOM 也可以部分控制到。即使如此,引用 Houdini 官网上的话来说,这种程度的暴露是“不确定的、兼容性不稳定的以及缺乏对关键特性的支持的”。

举个例子,浏览器中的 CSSOM 是不会告诉你它是如何处理跨源的样式表的,而且对于浏览器无法解析的 CSS 语句它的处理方式就是不解析了,也就是说——如果你要用 CSS polyfill 让浏览器去支持它尚且不支持的属性,那就不能在 CSSOM 这个环节做,只能自行解析一遍 DOM 树,找到 <style><link rel=”stylesheet”> 标签,获取其中的 CSS 样式、解析、重写,最后再加回 DOM 树中。

DOM 树都刷新了,得,渲染页面步骤重新走一遍。

(上图括号里面的文字:JavaScript Polyfills 只能去更改 DOM 和 CSSOM,大部分这样的操作,都会导致页面重新渲染。)

好吧,可能你会说这种别无选择的方法,也并不会对性能造成多大伤害(对某些网站来说,是的),但想想这个重渲染过程会多么频繁地发生,如果你的 polyfill 是需要应对页面上的所有交互呢?scroll 事件、窗口缩放、鼠标移动还有键盘输入……随时都会被触发的重渲染会把页面拖得无敌慢,用户绝对会发现的。

雪上加霜的是……大部分的 CSS polyfills 都是各有各的解析器和层级逻辑,而且“解析器”和“层级逻辑”又是两个非常复杂的东西,所以这些 polyfills 不是文件太大就是有太多 bug。

简要概括一下上面的内容:如果你想要浏览器做出它本来做不到事情(比如让它解析你给的样式,不管它能不能实现该样式),而渲染流程里你无法插手其他步骤,所以只能通过手动更新和改变 DOM 的方式。

如果我想要更改浏览器内部的渲染引擎呢?

我认为,这个问题是这篇文章的关键所在,如果你草草略过了前文,千万要在这里停下!仔细看这部分!

在看完上面那一节之后,我确定有些人干脆因噎废食地想“我不需要这个!我只想要中规中矩地写页面,并不想侵入浏览器内核然后实现一些特别前卫的效果”。

如果你是这么想的,那我强烈建议你看看自己这些年用于实现页面效果的技术。我们想要“干涉”浏览器解析样式的目的并不仅仅是为了炫技,更是为了框架作者以及开发者们能做到下面两件事情:

  • 统一跨浏览器行为,
  • 开发新特性或者给新特性打补丁,让人们可以立刻使用到新特性。

如果你曾使用过像 jQuery 那样的 JS 库,那你已经从中受惠了!事实上,良好的兼容性正是绝大多数前端库活着框架的卖点之一。Github 上受欢迎度排名前五的 JavaScript 和 DOM 仓库 — AngularJS、D3、 jQuery、 React 还有 Ember,面对使用它们的人来说,只要搞明白如何使用那些 API,就能成功达到想要的目的了,但是它们背后,在兼容各浏览器上下了多少功夫,恐怕是使用者几乎从未考虑过的。

现在,想想 CSS 在跨浏览器上的问题。甚至像 Bootstrap 或者 Foundation 这样宣称兼容性良好的 CSS 框架也都没有把浏览器 bug 标准化,而只是尽量去避免它们。不要以为 CSS 的兼容性问题只是个老毛病,就拿 flexbox 来说,我们也还面对着各种各样的跨浏览器兼容问题。(译者注:flexbox 的兼容问题,现在在主流的移动端页面开发上,已经有所缓解,译者曾整理过一个 gist 用于移动端 html 5 页面的 flexbox 效果,欢迎使用纠错)

试想一下,你可以随心所欲地使用想用的 CSS 属性,在每个浏览器上,你的页面长得和你设想的一样(,这职业生涯得过的多欢脱啊。你看到的那些酷炫的属性都能在保证性能的前提下使用,比如网格布局( CSS grids)、对齐(CSS snap points )还有 sticky 定位…… 而要实现这一切,你只需要把代码从 Github 上下载下来而已。

好吧,上面描绘的是 Houdini 的蓝图,Houdini 小组想要将这个梦想实现。

也许你从未想过写个 CSS polyfill 或者开发一些实验性的特性,但你可能会希望其他人能做这些事情,毕竟一旦有了这些工具,受益的可是全部开发者啊。

Houdini 的目前进展

在前面,我曾提到过开发者是很难干涉浏览器的渲染过程的,除了 DOM 和 CSSOM 这两个环节外。

Houdini 小分队为了解决这个问题引入了一些新的标准,首次给了开发者介入另外几个渲染环节的权限。下面这张图片展示的是每个环节对应的新标准,开发者可以用这些标准来控制对应的环节。(注意:灰色区块是还在实现中的标准,目前暂时无法使用。)


在接下来的几节内容中,我将大概介绍一下每一种新规范以及它们能做到什么,其他规范这里就不再赘述了,感兴趣的朋友可以看这里

CSS Parsing API

CSS Parser API 还没有被写入规范,所以下面我要说的内容随时都会有变化,但是它的基本思想不会变:允许开发者自由扩展 CSS 词法分析器,引入新的结构(constructs),比如新的媒体规则、新的伪类、嵌套、@extends@apply 等等。

只要新的词法分析器知道如何解析这些新结构,CSSOM 就不会直接忽略它们,而是把这些结构放到正确的地方。

CSS 属性与值 API

我曾提过 CSS 已经有自定义属性了,这太让人兴奋了,CSS 将解锁多少新技能啊。CSS Properties and Values API 的出现进一步推动了自定义属性,它还允许自定义属性添加不同的类型,大大增强了自定义属性的能力。

在自定义属性中加入不同的类型是很棒啦,不过这个 API 最大的的卖点是,开发者可以在自定义属性上做!动!画!而仅凭现在的技术,我们是做不到的……

来看看这个例子:

body {
  --primary-theme-color: tomato;
  transition: --primary-theme-color 1s ease-in-out;
}
body.night-theme {
  --primary-theme-color: darkred;
}

night-theme 类被加到 <body> 元素上之后,页面所有有 –primary-theme-color 的元素属性值都会慢慢从 tomato 变成 darked 。如果今天你想要在自己的页面上实现这个效果,那就需要费劲儿的一个个给元素添加过渡动画。

译者注:为什么我满脑子想的都是性能,页面全部重绘似乎是不可避免得了,毕竟从 body 元素的颜色都换了嘛。不过话又说回来,主题都换了,重绘 (repaint) 也是理所应当的

这个 API 的另一个卖点是注册一个 “apply hook”,也就是开发者可以在 cascade 步骤结束的时候,通过一个钩子更改一个元素的自定义属性值,这个特性对于 polyfills 开发可是很有意义的。

CSS Typed OM

你可以把 CSS Typed OM 视为 CSSOM 2.0,它的目的在于解决目前模型的一些问题,并实现 CSS Parsing API 和 CSS 属性与值 API 相关的特性。

提升性能是 Typed OM 的另一重任。将现在 CSSOM 的字符串值转成有意义的 JS 表达式可以有效的提升性能。

CSS Layout API

开发者可以通过 CSS Layout API 实现自己的布局模块(layout module),这里的“布局模块”指的是display 的属性值。也就是说,这个 API 实现以后,开发者首次拥有了像 CSS 原生代码(比如 display:flexdisplay:table)那样的布局能力。

让我们来看一个用例,在 Masonry layout library 上大家可以看到开发者们是有多想实现各种各样的复杂布局,其中一些布局光靠 CSS 是不行的。虽然这些布局会让人耳目一新印象深刻,但是它们的页面性能往往都很差,在一些低端设备上性能问题犹为明显。

CSS Layout API 暴露了一个 registerLayout 方法给开发者,接收一个布局名(layout name)作为后面在 CSS 中使用的属性值,还有一个包含有这个布局逻辑的 JavaScript 类。假如你想要用这个方法定义一个 masonry 的类,可以这么写:

registerLayout('masonry', class {
  static get inputProperties() {
    return ['width', 'height']
  }
  static get childrenInputProperties() {
    return ['x', 'y', 'position']
  }
  layout(children, constraintSpace, styleMap, breakToken) {
    // Layout logic goes here.
  }
}

如果上面这个例子你看不明白也用不着担心。关键在下面的代码,当你下载好 masonry.js 后,将它加入你的站点,然后这么来写 CSS 你就能得到一个 masonry 布局的样式了:

body {
  display: layout('masonry');
}

CSS Paint API

CSS Paint API 和上面说到的 Layout API 非常相似。它提供了一个 registerPaint 方法,操作方式和 registerLayout 方法也很相似。当想要构建一个 CSS 图像的时候,开发者随时可以调用paint() 函数,也可以使用刚刚注册好的名字。

下面这段代码展示时如何画一个有颜色的圆型:

registerPaint('circle', class {
  static get inputProperties() { return ['--circle-color']; }
  paint(ctx, geom, properties) {
    // 改变填充颜色
    const color = properties.get('--circle-color');
    ctx.fillStyle = color;
    // 确定圆心和半径
    const x = geom.width / 2;
    const y = geom.height / 2;
    const radius = Math.min(x, y);
    // 画圆
    ctx.beginPath();
    ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
    ctx.fill();
  }
});

在 CSS 中可以这样使用它:

.bubble {
  --circle-color: blue;
  background-image: paint('circle');
}

你将在页面上看到一个以蓝色圆形为背景的元素,它的类是 .bubble,这个圆形背景将始终占满 .bubble 元素。

Worklets

你已经看过了一些和规范相关的代码(比如 registerLayoutregisterPaint),估计现在你想知道的是,这些代码得放在哪里呢?答案就是 worklet 脚本(工作流脚本文件)。

Worklets 的概念和 web worker 类似,它们允许你引入脚本文件并执行特定的 JS 代码,这样的 JS 代码要满足两个条件:第一,可以在渲染流程中调用;第二,和主线程独立。

Worklet 脚本严格控制了开发者所能执行的操作类型,这就保证了性能。

复合滚动和动画

虽然关于 composited scrolling and animation 还没有官方的规范出来,但它可以算是 Houdini 项目中相当广为人知且颇被期待的特性之一。在设想中,这个 API 将会使得开发者能在合成器(compositor)的 worklet (而不是在主线程)中执行程序,还能更改一个 DOM 元素的属性,不过是不会引起渲染引擎重新计算布局或者样式的属性,比如 transformopacity 或者滚动条位置(scroll offset)。

开发者可以通过这个 API 创建高性能的滚动和输入动画,比如滚动头效果、视差效果。你可以在 Github 上看到更多这个 API 试图实现的效果

虽说正式规范还没有确定,但 Chrome 已经在实验性工具中加上了它。事实上 Chrome 的工程师们正在使用这些 API 最终会暴露的语言基元(primitives)来实现 CSS snap pointssticky 定位。这说明了什么?Houdini API 的性能已经足够说服 Chrome 在它之上实现新特性了。单单这一点应该就能说服一直在担心性能问题的的你了吧。

Surma 在 Youtube 上发布了一个模拟 Twitter 应用头部滚动效果的 demo,源码可以在这里查看。

那么现在我们能做什么?

在开头我就说了,我认为所有的 web 开发者都应该关注 Houdini,这个项目将会大大改善我们的未来。即使你可能不会直接接触到 Houdini 规范,但你肯定也会间接享受到它为你带来的便利,毕竟很多特性将基于它被构建出来。

虽然所描绘的美妙未来暂时还不会到来,但它也不会有你想象中那么遥远。今年早些时候,所有主流浏览器厂商都派代表参加了 Houdini 在悉尼的线下会议,在那次会议上,厂商们对于 Houdini 的方向和进程基本都达到了一致。

相信从我说的这些话里,你应该能相信 Houdini 的到来只是时间问题,它一定会成为正式的规范。

浏览器也是软件之一,它当然不可能一次性把所有的特性全加上,肯定是有给特性划分重要程度的。而这个划分方式常常是取决于用户对某个特性的需求度。

所以如果你真的很在意浏览器上样式和布局的可扩展性,如果你真的很想活在 CSS 新特性一出就能直接用进项目的世界里,快去和浏览器的开发者团队联系,告诉他们你真的很需要 Houdini。

另一种参与方式是,把现在不容易或根本不可能实现的、但你希望有一天可以用 CSS 实现的效果列出来, W3C 有提供一个用例文档,你可以向那个 repo 提 pr。如果有的浏览器没有提供那样的文档,那干脆你来帮他们新建一个!

Houdini 项目小组(也可以说是 W3C 的所有成员)真的非常希望听到全世界 web 开发者的声音。事实上很多开发浏览器的人他们本身并不是职业的 web 开发者,像 c++ 程序员是真的不容易明白 web 开发者的痛点啊。

他们需要我们!

 

原文参考资料

特别鸣谢 Houdini 小组成员 Ian Kilpatrick 和 Shane Stephens 帮我 review 这篇文章。
感谢 @寸志 老师对译文的审阅。

来源: Houdini:CSS 领域最令人振奋的革新 – 前端外刊评论 – 知乎专栏

前端文本截断 | EFE Tech

admin阅读(2661)评论(1)

误区

在设计产品时,由于不少产品经理、工程师并没有「字符不一定等宽」的概念,往往会给出「超过 n 个字符截断显示,英文数字算一个字符,汉字算两个字符」这样的需求。要知道,这里面的问题有很多:

为了显示效果,前端往往会采用优先西文字体族的 font-family 设置,即西文字符用西文字体,汉字用中文字体,这就很容易使得文本的宽度不好根据字符数来控制。首先,非代码的内容本身就不一定适合用等宽西文字体显示。其次即使用了等宽西文字体,汉字也基本不可能正好是其两倍宽。满足这个需求的,只能放弃西文字体,让西文字符也使用中文字体,并且使用中易系列的几个字体了(比如 SimSun,也就是 Windows 下的「宋体」)。(丑不说,还只能满足 Windows 下的需求。)

这种需求甚至在很多时候还会和某些字符编码长度的概念产生混淆,催生「长度限制 n 个字节,其中英文数字算 1 字节、汉字算 2 字节」这样的奇葩说法。

顺便歪个楼,这种「西文等宽、汉字占两倍宽度」的需求正常情况下只会存在于程序员的代码编辑器里。如果你是这种强迫症晚期,又不想用中易宋体,可以考虑试试 Belleve 制作的 Inziu

思路和原理

对于前端来说,数据库存储的限制不应该是我们需要关心的问题。看下前面的「伪需求」,我们实际的需求往往是从视觉角度出发的「超出特定高度截断显示」或「超出特定行数阶段显示」两种。由于实现方式的差异,其实可以分为「单行截断」、「多行截断」、「按高度截断」几种。从成本和效果来看,有「实现难度」、「效果精确度」、「对内容是否有限制」、「是否能响应页面变化」这些需要考虑的细节。本文里不准备列各种实现的代码,仅谈谈一些相关的问题和思路。

要看一些现有的实现方案可以看这几篇:

text-overflow: ellipsis

我想这个没有什么好多说的,自从 Firefox 7 开始支持这个 CSS 属性以后,这已经成为了 99% 情况下实现单行文本截断的不二之选。实现难度几乎为零、截断效果精准、内容中也可以有图片、链接等其他内容,而且在宽度变化时能够自动响应,兼容性也非常好(当然在低版本 IE 下可能会遇到一些需要额外套一层元素的特殊情况)。要支持 Firefox 7 以下的版本怎么办?尽量把需求拍回去吧。实在不行再考虑别的方案。

但是如果附加上其他的需求,纯 CSS 的方案可能也有不能满足的情况。比如有时候我们可能想仅在文字被截断时才在鼠标移入后通过浮层显示全部文本,又有时行末有不能被截掉的但宽度不定的内容。

计算内容宽度

百度以前的 Tangram 库在 1.x 版本中有一个 textOverflow 方法,会根据给定的宽度对单行文本进行截断。大致的做法是计算每个字符的宽度,找到加上 ... 正好小于指定宽度的边界,然后截去后续字符。为了提高性能,预先计算并缓存了 ASCII 字符(不等宽)的宽度和一个汉字(汉字等宽)的宽度,其他字符再实时去计算。计算宽度时是在指定元素内添加了一个 div 元素,并继承了原元素的所有文字排版相关的 CSS 属性。但事实上如果内容中本来就混杂了各种不同样式的文本,计算起来可能并不准确(比如有 div:first-child::first-letter 上的样式)。这个方案当时是兼容所有浏览器的,但是处理的内容基本只能是纯文本,而且完备性也有一定缺陷。

同样,如果利用 scrollWidth 来判断内容是否横向溢出也是可行的,可以在溢出时不断截掉尾部的内容,直到剩余内容加上省略号可以完整显示。实现起来应该比前一种方案更简洁一些,也更准确,但前一种方案预先计算完宽度后截取内容时不需要再实时读取 UI 上的确切宽度,所以性能要比这种高一些。

计算内容行数

在 WebKit 浏览器下实现限制显示行数可以使用非标准实现 -webkit-line-clamp 这个 CSS 属性,这个也是大家熟知的。在移动端应用的场景可能还多一些,桌面端很难只支持 WebKit 浏览器。当 CSS 无法直接解决这个问题时,用 JavaScript 如何解决这个问题呢?

比较容易想到的是用高度除以行高,在不给定行高的情况下,需要通过 getComputedStyle 来获取实际行高。但当 line-height取默认值时计算值为 normal,数值并不一定是确定值。所以通过 line-height 进行计算适用于自行指定行高数值的场景。例如在Clamp.js 中,对 normal 值就是假设所有浏览器默认值为 1.2的来处理。更别说可能有超出行高的图片等内容,使得高度并非行高乘以行数。

除此之外,据我所知可以用来比较精确地判断内容行数的方法主要有下面两个。这类方法的特点是行高并不需要是一个固定值,比如中间有内嵌的图标改变了行高。暂且不讨论限定不确定高度的行数本身是否合理(因为我们显示内容时高度的限制往往并非来源于行数,而是来源于高度的限制),来看看具体的做法。

利用 Element.getClientRects()

根据测试,在 IE8+ 及其他现代浏览器下这个方法对于 display: inline 的元素有一个特性:调用结果返回的 DOMRectList 对象的 length 等于元素渲染后的行数。这样,我们可以把需要计算行数的内容放在一个 display: inline 的容器内(比如原来是<p> 元素内的文本,现在更改为 p > span 这种结构),对该 <span> 元素调用 elem.getClientRects().length 即可获得行数。

可是目前在 WebKit 下,有一个疑似的 bug:当这个 display: inline 的容器内有子元素,getClientRects 的结果会包含这些子元素的轮廓,导致计数错误。既然规范并没有详细描述这个方法的计算逻辑,为什么说是一个疑似 bug 呢?因为当给容器加上一些特定的样式,计算结果又会和我们预期的结果相符了。详情可参考这个 issuedemo

利用 Selection.modify()

这是一个非标准的 DOM 接口,但是 WebKit 和 Gecko 都进行了实现(IE/Edge 都不支持)。

大致原理是:当我们把选区定位到某个元素的开头,然后执行

selection.modify('extend', 'forward', 'lineboundary');

可以把选区扩展到一行的末尾,然后再用

selection.modify('extend', 'forward', 'character');

往后扩展一个字符,如果此时的 selection.focusNode 还在容器内,且 selection.focusOffset 有变化,说明下一行还有内容。循环往复就可得到指定元素的「行数」。

在浏览器兼容性上,显然这个方法也有较大的局限,仅比 CSS 方法多支持了 Firefox 而已。但比上一个方法的好处在于,由于可以立刻找到折行的字符位置,所以截取时不需要通过截调末尾内容反复重试行数。

计算内容高度

给容器指定高度以后,通过比较 scrollHeightclientHeight 可以方便地测试元素内容的高度是否溢出容器范围。如果超出了指定高度,反复截去尾部内容直到不再溢出。

截取内容

如果内容是纯文本,那么很简单,依次删除末尾字符,再检查内容是否超出宽度/行数/高度限制就行了。文本较长的话可以用二分法优化一下执行效率。同时如果缓存下内容,可以在内容区域宽度变大时,根据情况来重新填入之前截取掉的文本,做到类似 CSS 的自适应效果。

而如果内容中有其他的 HTML 元素,事情就没这么好办了。可行的方法是,始终找到剩余内容最后的叶子节点,如果是文本节点,删除末尾字符;否则直接移除该节点。宽度变大时如果要恢复之前的内容就没这么简单了,首先要保留之前所有移除元素的引用(因为上面可能有事件监听),然后文本可以重新填入,元素节点也要按之前删除前的 DOM 结构重新恢复。那么在之前移除时我们可能就需要记录每一步的操作,恢复时逆向执行回来。理论上是可行的,实现起来可能会复杂一些。

总结

可以看到,基于 CSS 的方案非常精确,而且在页面布局变化、浏览器视口大小变化时更容易响应,但只能满特定的场景。用 JS 的方案在灵活性上有时更胜一筹,但要做的工作就多了很多。而且如果需要处理的内容很多,用 JS 的方法可能会带来性能瓶颈,毕竟一般读取 UI 实际显示样式的接口调用代价都比较大。

来源: 前端文本截断 | EFE Tech

实现一个图片懒加载插件有多难? – 前端有多难? – SegmentFault

admin阅读(10960)评论(1)

Web 图片的懒加载就是通过读取img元素,然后获得img元素的data-src(也可以约定为其他属性名)属性的值,并赋予img的src,从而实现动态加载图片的机制。

这里需要注意的是: img在初始化的时候不要设置src属性,因为即使设置 src='' 浏览器也会尝试加载图片。

一个简单的图片懒加载共涉及两个方面,

1. HTML 约定

我们首先需要给准备实施懒加载的img元素添加指定的class 这里为m-lazyload ,同时将img src赋值给 data-src属性。
具体示例为:

<img class="m-lazyload" data-src="imgUrl">

2. JavaScript 实现

动态加载总共分为以下几个步骤,这里每个步骤都将被拆分为独立的函数

1. 添加页面滚动监听事件
window.addEventListener('scroll', _delay, false);

function _delay() {
  clearTimeout(delay);
  delay = setTimeout(function () {
    _loadImage();
  }, time);
}
2. 当触发监听事件时会执行 _loadImage 函数,该函数负责加载图片
function _loadImage() {
  for (var i = imgList.length; i--;) {
    var el = imgList[i];
    if (_isShow(el)) {
      el.src = el.getAttribute('data-src');
      el.className = el.className.replace(new RegExp("(\\s|^)" + _selector.substring(1, _selector.length) + "(\\s|$)"), " ");
      imgList.splice(i, 1);
    }
  }
}

虽然执行了_loadImage函数,但是我们得知道哪些图片需要被加载,这里的判断依据是什么呢?

依据就是判断该图片是否在当前窗口的可视区域内,在这里我们封装一个_isShow函数来实现

function _isShow(el) {
  var coords = el.getBoundingClientRect();
  return ( (coords.top >= 0 && coords.left >= 0 && coords.top) <= (window.innerHeight || document.documentElement.clientHeight) + parseInt(offset));
}

自此一个图片加载的闭环就形成了

当网页滚动的事件被触发 -> 执行加载图片操作 -> 判断图片是否在可视区域内 -> 在,则动态将data-src的值赋予该图片。

太简单了?

事实就是如此!!!

如此简单,不妨扩展一下

  1. 添加一些自定义参数,谁都喜欢自定义,不是吗?
  2. 支持iScroll, iScroll是一个高性能,资源占用少,无依赖,多平台的javascript滚动插件。
这里我们做了些优化
  1. 图片加载后移除选择器,避免重复判断。
  2. 缓存img元素结合,减少dom元素查询性能损耗
  3. 扩展prototype添加getNode方法,支持分页数据懒加载(由于我们之前缓存了dom元素)

OK!说了这么多,show me the code 吧!

(function () {
  var imgList = [],  // 页面所有img元素集合
    delay,   // setTimeout 对象
    offset,  //偏移量,用于指定图片距离可视区域多少距离,进行加载
    time,  // 延迟载入时间
    _selector; // 选择器 默认为 .m-lazyload

  function _isShow(el) {
    var coords = el.getBoundingClientRect();
    return ( (coords.top >= 0 && coords.left >= 0 && coords.top) <= (window.innerHeight || document.documentElement.clientHeight) + parseInt(offset));
  }

  function _loadImage() {
    for (var i = imgList.length; i--;) {
      var el = imgList[i];
      if (_isShow(el)) {
        el.src = el.getAttribute('data-src');
        el.className = el.className.replace(new RegExp("(\\s|^)" + _selector.substring(1, _selector.length) + "(\\s|$)"), " ");
        imgList.splice(i, 1);
      }
    }
  }

  function _delay() {
    clearTimeout(delay);
    delay = setTimeout(function () {
      _loadImage();
    }, time);
  }

  function ImageLazyload(selector, options) {
    var defaults = options || {};
    offset = defaults.offset || 0;
    time = defaults.time || 250;
    _selector = selector || '.m-lazyload';
    this.getNode();
    _delay();//避免首次加载未触发touch事件,主动触发一次加载函数
    if (defaults.iScroll) {
      defaults.iScroll.on('scroll', _delay);
      defaults.iScroll.on('scrollEnd', _delay);
    } else {
      window.addEventListener('scroll', _delay, false);
    }
  }
  ImageLazyload.prototype.getNode = function () {
    imgList = [];
    var nodes = document.querySelectorAll(_selector);
    for (var i = 0, l = nodes.length; i < l; i++) {
      imgList.push(nodes[i]);
    }
  };
})();
 

来源: 实现一个图片懒加载插件有多难? – 前端有多难? – SegmentFault

[ISUX译]响应式图像 – 腾讯ISUX – 社交用户体验设计

admin阅读(1931)评论(0)

响应式图像2 (2)

自从2010年Ethan Marcotte开始讨论响应式网页设计,开发者和设计师们竞相寻求处理响应式图片的方法。这的确是一个棘手的问题 ,因为我们对同一个网站在众多设备宽度下,使用同一图像源。你愿意在一个大显示屏上显示模糊地、马赛克状的图像?你愿意在你的手机上加载一个巨大的(虽然更漂亮的)图像?这个问题令人左右为难。

一群来自响应式问题社区组(RICG)的聪明家伙致力于解决这个难题,他们使picture元素和srcsetsizes属性纳入HTML 5.1规范草案 。因为我们无法预测用户在何地以及如何访问我们的网站,所以我们需要浏览器自身根据情况选择最好的图像。新规范将解决以下问题:

  • 基于设备象素比(device-pixel-radio)选择
  • 基于viewport选择
  • 基于Art direction(美术设计)选择
  • 基于图像格式选择

该规范中,img元素增加了两个新属性:srcsetsizes。srcset用来声明一组图像源,浏览器根据我们使用描述符指定的条件来选择图像。描述符x表示图像的像素密度,描述符w表示图像的宽度;浏览器使用这些信息从列表中选择合适的图像。sizes属性为浏览器提供将要显示图像的尺寸信息,srcset使用w描述符时必须包含此属性。这种方法尤其适用于可变宽度的图像,我将在后面详细讨论。

我们现在可以根据用户的viewport,提供不同质量或美术设计(art direction)的图像,无需借助复杂的服务器端设置。响应式图像将成为HTML规范的重要组成部分,所有浏览器终将都会支持此解决方案。

固定宽度图像:基于设备像素比选择

视网膜屏幕的广泛应用,使我们不仅需要考虑屏幕分辨率,而且也需要考虑像素密度。视网膜屏幕,4K显示器,UltraHD-它们都比相同尺寸的标准分辨率显示器填充了更多的像素。更多的像素=更清晰的图像。

有些图片不管屏幕尺寸,始终以固定宽度显示-如站点logo或人物简介图像,也就是说需要根据设备像素比来选择。浏览器将根据设备像素比来选择加载哪张图像。

srcset属性列出了浏览器可以选择加载的源图像池,是一个由逗号分隔的列表。x描述符表示图像的设备像素比。浏览器根据运行环境,利用这些信息来选择适当的图像。不理解srcset的浏览器会直接加载src属性中声明的图像。

<img srcset="crest-383.jpg 1.5x, crest-510.jpg 2x" src="crest-255.jpg" alt="[ISUX译]响应式图像" alt="USWNT crest" />

[ISUX译]响应式图像

网站logo就是固定宽度图像的一个例子,不管viewport的宽度如何,始终保持相同的宽度。不过,与内容相关的图片,通常也需要响应式,它们的大小往往随viewport改变。对于这类图像,还有更好的处理方法。

可变宽度的图像:基于viewport选择

对于可变宽度的图像,我们使用srcset搭配w描述符以及sizes属性 。w描述符告诉浏览器列表中的每个图象的宽度。sizes属性是一个包含两个值的,由逗号分隔的列表。根据最新规范,如果srcset中任何图像使用了w描述符,那么必须要设置sizes属性。

sizes属性有两个值:第一个是媒体条件;第二个是源图尺寸值,在特定媒体条件下,此值决定了图片的宽度。需要注意是,源图尺寸值不能使用百分比,vw是唯一可用的CSS单位。

<img srcset="uswnt-480.jpg 480w, 
             uswnt-640.jpg 640w, 
             uswnt-960.jpg 960w,
             uswnt-1280.jpg 1280w" 
     sizes="(max-width: 400px) 100vw, 
            (max-width: 960px) 75vw, 
            640px" 
     src="uswnt-640.jpg" alt="[ISUX译]响应式图像" alt="USWNT World Cup victory">
[ISUX译]响应式图像

上例中,我们告诉浏览器在viewport宽度小于400像素时,使图像的宽度与viewport等宽。在viewport宽度小于960像素时,使图像的宽度为viewport宽度的75%。当viewport大于960像素时,使图像的宽度为640像素。如果你不熟悉vw ,可以看看Tim Severien的大文viewport单位详解 。

浏览器利用srcsetsizes信息来选择最符合规定条件的图像。如果浏览器的viewport是600像素,图像最可能以75vw的宽度显示。浏览器将尝试加载第一张大于450像素(600*0.75)的图像,也就是uswnt-480.jpg。如果我的是dpr为2的Retina显示屏,那么浏览器就会尝试加载第一张大于900像素(600*0.75*2)的图像,也就是uswnt-960.jpg。我们无法确定究竟显示哪张图像,因为每个浏览器根据我们提供的信息挑选适当图像的算法是有差异的。(译者注:srcset和size列表是对浏览器的一个建议(hint),而非指令。例如,设备像素比(dpr)为1.5的设备,亦可用1x也可用2x的图像,由浏览器根据其能力、网络等因素来决定。)

前两个例子都是以不同质量显示相同的图像,仅用srcset属性就足够了。不必担心老旧浏览器,老旧浏览器会把它看作为一个普通的图像并从src中加载。如果你想在不同宽度下显示稍微不同的图像,比如在较窄屏幕下仅显示图像的关键部分,那么要使用picture元素。

picture:基于Art direction(美术设计)选择

picture元素就像是图像和其源的容器。浏览器仍然需要img元素,用来表明需要加载图片,如果没有img,那么什么都不会渲染。source为浏览器提供了要显示图像的供选版本。基于美术设计选择的适用场景为:在一个特定的转效点(breakpoint)需要显示一个特定的图像。使用picture元素选择图像,不会有歧义。

<picture>
  <source media="(min-width: 960px)" srcset="ticker-tape-large.jpg">
  <source media="(min-width: 575px)" srcset="ticker-tape-medium.jpg">
  <img src="ticker-tape-small.jpg" alt="[ISUX译]响应式图像" alt="USWNT ticker-tape parade">
</picture>
[ISUX译]响应式图像

在本例中,当viewport大于960像素时,会加载图像的风景模式版本(ticker-tape-large.jpg)。当viewport宽度大于575像素时,浏览器会加载图像的裁剪过的肖像模式版本(ticker-tape-medium.jpg)。而当宽度小于575像素时,加载的图像( ticker-tape-small.jpg)已经被裁剪成焦点仅在一个球员上了。

picture元素是向后兼容的;不支持picture元素的浏览器将显示img。图像的所有标准属性(如alt),应该作用在img上而不是picture上。

source:基于图片格式选择

最近几年出现了一些新的图片格式,这些新图像格式在较小的文件大小情况下保证了较好的图片质量。听起来还不错,但残酷的事实是没有一个新格式被所有浏览器支持。谷歌的WebP表现不错,但只有Chrome和Opera原声支持。JPEG-XR,最初被称为高清照片,是微软发布的一个专有图像格式,仅Internet Explorer支持。如果你想了解更多信息,可以查看Zoltan Hawryluk对这些新格式的深入研究

<picture>
  <source type="image/vnd.ms-photo" src="wwc2015.jxr">
  <source type="image/jp2" src="wwc2015.jp2">
  <source type="image/webp" src="wwc2015.webp">
  <img src="wwc2015.png" alt="[ISUX译]响应式图像" alt="WWC 2015">
</picture>

source的type属性用来指定每个图像的MIME类型,浏览器会选择第一个含有其支持的MIME类型的源。源的顺序是至关重要的,如果浏览器无法识别所有的图象类型,它会回退至原来的img元素。

现在可以使用这些东东吗?

在写这篇文章的时候, Firefox,Chrome和Opera的最新稳定版本均支持picture。Safari和IE本身均不支持picturesrcset的情况稍微好一点,Firefox、Chrome和Opera的最新稳定版本完全支持,Safari8和Internet Explorer Edge部分支持,可以使用x描述符用于根据分辨率切换,但不支持w描述符。Safari9已经完全支持srcset了(译者注)。

现有不少polyfills解决支持性问题,最知名的恐怕是Scott Jehl的picturefill。目前我(原作者)在我自己的网站上使用Alexander Farkas的respimage。目前的状况是,我们已对响应式图像的处理方案达成一致,并且这些解决方案逐渐被所有的主流浏览器实现。尽管该规范仍在不断完善之中,但原生的响应式解决方案离我们越来越近了。

原文地址:Using Responsive Images (Now)

来源: [ISUX译]响应式图像 – 腾讯ISUX – 社交用户体验设计

移动前端开发之viewport的深入理解 – 无双 – 博客园

admin阅读(5903)评论(0)

在移动设备上进行网页的重构或开发,首先得搞明白的就是移动设备上的viewport了,只有明白了viewport的概念以及弄清楚了跟viewport有关的meta标签的使用,才能更好地让我们的网页适配或响应各种不同分辨率的移动设备。

一、viewport的概念

通俗的讲,移动设备上的viewport就是设备的屏幕上能用来显示我们的网页的那一块区域,在具体一点,就是浏览器上(也可能是一个app中的webview)用来显示网页的那部分区域,但viewport又不局限于浏览器可视区域的大小,它可能比浏览器的可视区域要大,也可能比浏览器的可视区域要小。在默认情况下,一般来讲,移动设备上的viewport都是要大于浏览器可视区域的,这是因为考虑到移动设备的分辨率相对于桌面电脑来说都比较小,所以为了能在移动设备上正常显示那些传统的为桌面浏览器设计的网站,移动设备上的浏览器都会把自己默认的viewport设为980px或1024px(也可能是其它值,这个是由设备自己决定的),但带来的后果就是浏览器会出现横向滚动条,因为浏览器可视区域的宽度是比这个默认的viewport的宽度要小的。下图列出了一些设备上浏览器的默认viewport的宽度。

1

 

二、css中的1px并不等于设备的1px

在css中我们一般使用px作为单位,在桌面浏览器中css的1个像素往往都是对应着电脑屏幕的1个物理像素,这可能会造成我们的一个错觉,那就是css中的像素就是设备的物理像素。但实际情况却并非如此,css中的像素只是一个抽象的单位,在不同的设备或不同的环境中,css中的1px所代表的设备物理像素是不同的。在为桌面浏览器设计的网页中,我们无需对这个津津计较,但在移动设备上,必须弄明白这点。在早先的移动设备中,屏幕像素密度都比较低,如iphone3,它的分辨率为320×480,在iphone3上,一个css像素确实是等于一个屏幕物理像素的。后来随着技术的发展,移动设备的屏幕像素密度越来越高,从iphone4开始,苹果公司便推出了所谓的Retina屏,分辨率提高了一倍,变成640×960,但屏幕尺寸却没变化,这就意味着同样大小的屏幕上,像素却多了一倍,这时,一个css像素是等于两个物理像素的。其他品牌的移动设备也是这个道理。例如安卓设备根据屏幕像素密度可分为ldpi、mdpi、hdpi、xhdpi等不同的等级,分辨率也是五花八门,安卓设备上的一个css像素相当于多少个屏幕物理像素,也因设备的不同而不同,没有一个定论。

还有一个因素也会引起css中px的变化,那就是用户缩放。例如,当用户把页面放大一倍,那么css中1px所代表的物理像素也会增加一倍;反之把页面缩小一倍,css中1px所代表的物理像素也会减少一倍。关于这点,在文章后面的部分还会讲到。

在移动端浏览器中以及某些桌面浏览器中,window对象有一个devicePixelRatio属性,它的官方的定义为:设备物理像素和设备独立像素的比例,也就是 devicePixelRatio = 物理像素 / 独立像素。css中的px就可以看做是设备的独立像素,所以通过devicePixelRatio,我们可以知道该设备上一个css像素代表多少个物理像素。例如,在Retina屏的iphone上,devicePixelRatio的值为2,也就是说1个css像素相当于2个物理像素。但是要注意的是,devicePixelRatio在不同的浏览器中还存在些许的兼容性问题,所以我们现在还并不能完全信赖这个东西,具体的情况可以看下这篇文章

devicePixelRatio的测试结果:

14

 

三、PPK的关于三个viewport的理论

ppk大神对于移动设备上的viewport有着非常多的研究(第一篇第二篇第三篇),有兴趣的同学可以去看一下,本文中有很多数据和观点也是出自那里。ppk认为,移动设备上有三个viewport。

首先,移动设备上的浏览器认为自己必须能让所有的网站都正常显示,即使是那些不是为移动设备设计的网站。但如果以浏览器的可视区域作为viewport的话,因为移动设备的屏幕都不是很宽,所以那些为桌面浏览器设计的网站放到移动设备上显示时,必然会因为移动设备的viewport太窄,而挤作一团,甚至布局什么的都会乱掉。也许有人会问,现在不是有很多手机分辨率都非常大吗,比如768×1024,或者1080×1920这样,那这样的手机用来显示为桌面浏览器设计的网站是没问题的吧?前面我们已经说了,css中的1px并不是代表屏幕上的1px,你分辨率越大,css中1px代表的物理像素就会越多,devicePixelRatio的值也越大,这很好理解,因为你分辨率增大了,但屏幕尺寸并没有变大多少,必须让css中的1px代表更多的物理像素,才能让1px的东西在屏幕上的大小与那些低分辨率的设备差不多,不然就会因为太小而看不清。所以在1080×1920这样的设备上,在默认情况下,也许你只要把一个div的宽度设为300多px(视devicePixelRatio的值而定),就是满屏的宽度了。回到正题上来,如果把移动设备上浏览器的可视区域设为viewport的话,某些网站就会因为viewport太窄而显示错乱,所以这些浏览器就决定默认情况下把viewport设为一个较宽的值,比如980px,这样的话即使是那些为桌面设计的网站也能在移动浏览器上正常显示了。ppk把这个浏览器默认的viewport叫做 layout viewport这个layout viewport的宽度可以通过 document.documentElement.clientWidth 来获取。

然而,layout viewport 的宽度是大于浏览器可视区域的宽度的,所以我们还需要一个viewport来代表 浏览器可视区域的大小,ppk把这个viewport叫做 visual viewport。visual viewport的宽度可以通过window.innerWidth 来获取,但在Android 2, Oprea mini 和 UC 8中无法正确获取。

2      3

现在我们已经有两个viewport了:layout viewportvisual viewport。但浏览器觉得还不够,因为现在越来越多的网站都会为移动设备进行单独的设计,所以必须还要有一个能完美适配移动设备的viewport。所谓的完美适配指的是,首先不需要用户缩放和横向滚动条就能正常的查看网站的所有内容;第二,显示的文字的大小是合适,比如一段14px大小的文字,不会因为在一个高密度像素的屏幕里显示得太小而无法看清,理想的情况是这段14px的文字无论是在何种密度屏幕,何种分辨率下,显示出来的大小都是差不多的。当然,不只是文字,其他元素像图片什么的也是这个道理。ppk把这个viewport叫做 ideal viewport,也就是第三个viewport——移动设备的理想viewport。

ideal viewport并没有一个固定的尺寸,不同的设备拥有有不同的ideal viewport。所有的iphone的ideal viewport宽度都是320px,无论它的屏幕宽度是320还是640,也就是说,在iphone中,css中的320px就代表iphone屏幕的宽度。

4          5

但是安卓设备就比较复杂了,有320px的,有360px的,有384px的等等,关于不同的设备ideal viewport的宽度都为多少,可以到http://viewportsizes.com去查看一下,里面收集了众多设备的理想宽度。

再总结一下:ppk把移动设备上的viewport分为layout viewport  、 visual viewport   ideal viewport  三类,其中的ideal viewport是最适合移动设备的viewport,ideal viewport的宽度等于移动设备的屏幕宽度,只要在css中把某一元素的宽度设为ideal viewport的宽度(单位用px),那么这个元素的宽度就是设备屏幕的宽度了,也就是宽度为100%的效果。ideal viewport 的意义在于,无论在何种分辨率的屏幕下,那些针对ideal viewport 而设计的网站,不需要用户手动缩放,也不需要出现横向滚动条,都可以完美的呈现给用户。

 

四、利用meta标签对viewport进行控制

移动设备默认的viewport是layout viewport,也就是那个比屏幕要宽的viewport,但在进行移动设备网站的开发时,我们需要的是ideal viewport。那么怎么才能得到ideal viewport呢?这就该轮到meta标签出场了。

我们在开发移动设备的网站时,最常见的的一个动作就是把下面这个东西复制到我们的head标签中:

<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">

该meta标签的作用是让当前viewport的宽度等于设备的宽度,同时不允许用户手动缩放。也许允不允许用户缩放不同的网站有不同的要求,但让viewport的宽度等于设备的宽度,这个应该是大家都想要的效果,如果你不这样的设定的话,那就会使用那个比屏幕宽的默认viewport,也就是说会出现横向滚动条。

这个name为viewport的meta标签到底有哪些东西呢,又都有什么作用呢?

meta viewport 标签首先是由苹果公司在其safari浏览器中引入的,目的就是解决移动设备的viewport问题。后来安卓以及各大浏览器厂商也都纷纷效仿,引入对meta viewport的支持,事实也证明这个东西还是非常有用的。

在苹果的规范中,meta viewport 有6个属性(暂且把content中的那些东西称为一个个属性和值),如下:

width 设置layout viewport  的宽度,为一个正整数,或字符串”width-device”
initial-scale 设置页面的初始缩放值,为一个数字,可以带小数
minimum-scale 允许用户的最小缩放值,为一个数字,可以带小数
maximum-scale 允许用户的最大缩放值,为一个数字,可以带小数
height 设置layout viewport  的高度,这个属性对我们并不重要,很少使用
user-scalable 是否允许用户进行缩放,值为”no”或”yes”, no 代表不允许,yes代表允许

这些属性可以同时使用,也可以单独使用或混合使用,多个属性同时使用时用逗号隔开就行了。

此外,在安卓中还支持  target-densitydpi  这个私有属性,它表示目标设备的密度等级,作用是决定css中的1px代表多少物理像素

target-densitydpi 值可以为一个数值或 high-dpi 、 medium-dpi、 low-dpi、 device-dpi 这几个字符串中的一个

特别说明的是,当 target-densitydpi=device-dpi 时, css中的1px会等于物理像素中的1px。

因为这个属性只有安卓支持,并且安卓已经决定要废弃target-densitydpi  这个属性了,所以这个属性我们要避免进行使用  。

 

五、把当前的viewport宽度设置为 ideal viewport 的宽度

要得到ideal viewport就必须把默认的layout viewport的宽度设为移动设备的屏幕宽度。因为meta viewport中的width能控制layout viewport的宽度,所以我们只需要把width设为width-device这个特殊的值就行了。

<meta name="viewport" content="width=device-width">

下图是这句代码在各大移动端浏览器上的测试结果:

6

可以看到通过width=device-width,所有浏览器都能把当前的viewport宽度变成ideal viewport的宽度,但要注意的是,在iphone和ipad上,无论是竖屏还是横屏,宽度都是竖屏时ideal viewport的宽度。

这样的写法看起来谁都会做,没吃过猪肉,谁还没见过猪跑啊~,确实,我们在开发移动设备上的网页时,不管你明不明白什么是viewport,可能你只需要这么一句代码就够了。

可是你肯定不知道

<meta name="viewport" content="initial-scale=1">

这句代码也能达到和前一句代码一样的效果,也可以把当前的的viewport变为 ideal viewport。

呵呵,傻眼了吧,因为从理论上来讲,这句代码的作用只是不对当前的页面进行缩放,也就是页面本该是多大就是多大。那为什么会有 width=device-width 的效果呢?

要想清楚这件事情,首先你得弄明白这个缩放是相对于什么来缩放的,因为这里的缩放值是1,也就是没缩放,但却达到了 ideal viewport 的效果,所以,那答案就只有一个了,缩放是相对于 ideal viewport来进行缩放的,当对ideal viewport进行100%的缩放,也就是缩放值为1的时候,不就得到了 ideal viewport吗?事实证明,的确是这样的。下图是各大移动端的浏览器当设置了<meta name=”viewport” content=”initial-scale=1″> 后是否能把当前的viewport宽度变成 ideal viewport 的宽度的测试结果。

7

测试结果表明 initial-scale=1 也能把当前的viewport宽度变成 ideal viewport 的宽度,但这次轮到了windows phone 上的IE 无论是竖屏还是横屏都把宽度设为竖屏时ideal viewport的宽度。但这点小瑕疵已经无关紧要了。

但如果width 和 initial-scale=1同时出现,并且还出现了冲突呢?比如:

<meta name="viewport" content="width=400, initial-scale=1">

width=400表示把当前viewport的宽度设为400px,initial-scale=1则表示把当前viewport的宽度设为ideal viewport的宽度,那么浏览器到底该服从哪个命令呢?是书写顺序在后面的那个吗?不是。当遇到这种情况时,浏览器会取它们两个中较大的那个值。例如,当width=400,ideal viewport的宽度为320时,取的是400;当width=400, ideal viewport的宽度为480时,取的是ideal viewport的宽度。(ps:在uc9浏览器中,当initial-scale=1时,无论width属性的值为多少,此时viewport的宽度永远都是ideal viewport的宽度)

最后,总结一下,要把当前的viewport宽度设为ideal viewport的宽度,既可以设置 width=device-width,也可以设置 initial-scale=1,但这两者各有一个小缺陷,就是iphone、ipad以及IE 会横竖屏不分,通通以竖屏的ideal viewport宽度为准。所以,最完美的写法应该是,两者都写上去,这样就 initial-scale=1 解决了 iphone、ipad的毛病,width=device-width则解决了IE的毛病:

<meta name="viewport" content="width=device-width, initial-scale=1">

 

六、关于meta viewport的更多知识

1、关于缩放以及initial-scale的默认值

首先我们先来讨论一下缩放的问题,前面已经提到过,缩放是相对于ideal viewport来缩放的,缩放值越大,当前viewport的宽度就会越小,反之亦然。例如在iphone中,ideal viewport的宽度是320px,如果我们设置 initial-scale=2 ,此时viewport的宽度会变为只有160px了,这也好理解,放大了一倍嘛,就是原来1px的东西变成2px了,但是1px变为2px并不是把原来的320px变为640px了,而是在实际宽度不变的情况下,1px变得跟原来的2px的长度一样了,所以放大2倍后原来需要320px才能填满的宽度现在只需要160px就做到了。因此,我们可以得出一个公式:

visual viewport宽度 = ideal viewport宽度  / 当前缩放值

当前缩放值 = ideal viewport宽度  / visual viewport宽度

ps: visual viewport的宽度指的是浏览器可视区域的宽度。

大多数浏览器都符合这个理论,但是安卓上的原生浏览器以及IE有些问题。安卓自带的webkit浏览器只有在 initial-scale = 1 以及没有设置width属性时才是表现正常的,也就相当于这理论在它身上基本没用;而IE则根本不甩initial-scale这个属性,无论你给他设置什么,initial-scale表现出来的效果永远是1。

好了,现在再来说下initial-scale的默认值问题,就是不写这个属性的时候,它的默认值会是多少呢?很显然不会是1,因为当 initial-scale = 1 时,当前的layout viewport宽度会被设为 ideal viewport的宽度,但前面说了,各浏览器默认的 layout viewport宽度一般都是980啊,1024啊,800啊等等这些个值,没有一开始就是 ideal viewport的宽度的,所以 initial-scale的默认值肯定不是1。安卓设备上的initial-scale默认值好像没有方法能够得到,或者就是干脆它就没有默认值,一定要你显示的写出来这个东西才会起作用,我们不管它了,这里我们重点说一下iphone和ipad上的initial-scale默认值。

根据测试,我们可以在iphone和ipad上得到一个结论,就是无论你给layout viewpor设置的宽度是多少,而又没有指定初始的缩放值的话,那么iphone和ipad会自动计算initial-scale这个值,以保证当前layout viewport的宽度在缩放后就是浏览器可视区域的宽度,也就是说不会出现横向滚动条。比如说,在iphone上,我们不设置任何的viewport meta标签,此时layout viewport的宽度为980px,但我们可以看到浏览器并没有出现横向滚动条,浏览器默认的把页面缩小了。根据上面的公式,当前缩放值 = ideal viewport宽度  / visual viewport宽度,我们可以得出:

当前缩放值 = 320 / 980

也就是当前的initial-scale默认值应该是 0.33这样子。当你指定了initial-scale的值后,这个默认值就不起作用了。

总之记住这个结论就行了:在iphone和ipad上,无论你给viewport设的宽的是多少,如果没有指定默认的缩放值,则iphone和ipad会自动计算这个缩放值,以达到当前页面不会出现横向滚动条(或者说viewport的宽度就是屏幕的宽度)的目的。

11    12     13

 

2、动态改变meta viewport标签

第一种方法

可以使用document.write来动态输出meta viewport标签,例如:

document.write('<meta name="viewport" content="width=device-width,initial-scale=1">')

第二种方法

通过setAttribute来改变

<meta id="testViewport" name="viewport" content="width = 380">
<script>
var mvp = document.getElementById('testViewport');
mvp.setAttribute('content','width=480');
</script>

 

安卓2.3自带浏览器上的一个bug

复制代码
<meta name="viewport" content="width=device-width">

<script type="text/javascript">
alert(document.documentElement.clientWidth); //弹出600,正常情况应该弹出320
</script>

<meta name="viewport" content="width=600">

<script type="text/javascript">
alert(document.documentElement.clientWidth); //弹出320,正常情况应该弹出600
</script>
复制代码

测试的手机ideal viewport 宽度为320px,第一次弹出的值是600,但这个值应该是第行meta标签的结果啊,然后第二次弹出的值是320,这才是第一行meta标签所达到的效果啊,所以在安卓2.3(或许是所有2.x版本中)的自带浏览器中,对meta viewport标签进行覆盖或更改,会出现让人非常迷糊的结果。

 

七、结语

说了那么多废话,最后还是有必要总结一点有用的出来。

首先如果不设置meta viewport标签,那么移动设备上浏览器默认的宽度值为800px,980px,1024px等这些,总之是大于屏幕宽度的。这里的宽度所用的单位px都是指css中的px,它跟代表实际屏幕物理像素的px不是一回事。

第二、每个移动设备浏览器中都有一个理想的宽度,这个理想的宽度是指css中的宽度,跟设备的物理宽度没有关系,在css中,这个宽度就相当于100%的所代表的那个宽度。我们可以用meta标签把viewport的宽度设为那个理想的宽度,如果不知道这个设备的理想宽度是多少,那么用device-width这个特殊值就行了,同时initial-scale=1也有把viewport的宽度设为理想宽度的作用。所以,我们可以使用

<meta name="viewport" content="width=device-width, initial-scale=1">

来得到一个理想的viewport(也就是前面说的ideal viewport)。

为什么需要有理想的viewport呢?比如一个分辨率为320×480的手机理想viewport的宽度是320px,而另一个屏幕尺寸相同但分辨率为640×960的手机的理想viewport宽度也是为320px,那为什么分辨率大的这个手机的理想宽度要跟分辨率小的那个手机的理想宽度一样呢?这是因为,只有这样才能保证同样的网站在不同分辨率的设备上看起来都是一样或差不多的。实际上,现在市面上虽然有那么多不同种类不同品牌不同分辨率的手机,但它们的理想viewport宽度归纳起来无非也就 320、360、384、400等几种,都是非常接近的,理想宽度的相近也就意味着我们针对某个设备的理想viewport而做出的网站,在其他设备上的表现也不会相差非常多甚至是表现一样的。

来源: 移动前端开发之viewport的深入理解 – 无双 – 博客园

使用 SVG 输出 Octicon | EFE Tech

admin阅读(1558)评论(0)

原文:https://github.com/blog/2112-delivering-octicons-with-svg

GitHub.com 现在不再使用字体来输出图标了。我们把代码库中所有的 Octicon 替换成了 SVG 版本。虽然这些改动并不那么明显,但你马上就能体会到 SVG 图标的优点。

Octicon 上的对比

Octicon 上的对比

切换到 SVG 以后,图标会作为图片渲染而非文字,这使其在任何分辨率下都能很好地在各种像素值下显示。可以比较一下左侧放大后的字体版本和右侧清晰的 SVG 版本。

为何使用 SVG?

图标字体渲染问题

图标字体从来只是一种 hack。我们之前使用一个自定义字体,并将图标作为 Unicode 符号。这样图标字体就可以通过打包后的 CSS 来引入。只要简单地在任意元素上添加一个 class,图标就可以显示出来。然后我们只使用 CSS 就能即时改变图标的尺寸和颜色了。

不幸的是,虽然这些图标是矢量图形,但在 1x 显示屏下的渲染效果并不理想。在基于 WebKit 的浏览器下,图标可能会在某些窗口宽度下变得模糊。因为此时图标是作为文本输出的,本来用于提高文本可读性的次像素渲染技术反而使图标看起来糟糕许多。

对页面渲染的改进

因为我们直接将 SVG 注入 HTML(这也是我们选择这种方式更大的原因),所以不再会出现图标字体下载、缓存、渲染过程中出现的样式闪动。

页面闪动

页面闪动

可访问性

就像在《图标字体已死》一文中所述,有些用户会覆盖掉 GitHub 的字体。对于患有读写障碍的用户,某些特定字体是更加容易阅读的。对于修改字体的用户来说,我们基于字体的图标就被渲染成了空白方框。这搞乱了 GitHub 页面布局,而且也不提供任何信息。而不管字体覆盖与否,SVG 都可以正常显示。对于读屏器用户来说,SVG 能让我们选择是读出 alt 属性还是直接完全跳过。

图形尺寸更合适

我们目前对每个图标在所有尺寸下提供单一的图形。因为站点的加载依赖了图标字体的下载,我们曾被迫把图标集限制在最重要的 16px 尺寸下。这让每个符号在视觉上做出一些让步,因为我们是针对 16px 方格进行优化的。当在新页面或营销页上缩放这些图标时,显示的还是 16px 的版本。而 SVG 可以方便地 fork 全部的图标集,在我们指定的每个尺寸提供更合适的图形。当然对图标字体也可以这么做,但这样用户需要下载两倍的数据量,可能更多。

便于创作

打包自定义字体是复杂的。一些 web 应用因此而生,我们内部也自己搞了一个。而用 SVG 的话,添加一个新图标会变得像把一个 SVG 文件拖入一个目录这样轻而易举。

可添加动画效果

并非一定要加动画,而是有了添加动画的可能性。而且 SVG 动画也的确在例如预加载动画等地方有实际应用。

如何实现

Octicon 在整个 GitHub 的代码库中出现了约 2500 次。在用 SVG 之前,我们简单地用 <span class="octicon octicon-alert"></span> 这样简单的标签来引入。要切换到 SVG,我们先给添加了一个用来往 HTML 内直接注入 SVG 路径的 Rails helper。我们先用这个 helper 让员工测试了不同的 SVG 输出方式,然后才对外发布。

Helper 的用法

输入 <%= octicon(:symbol => "plus") %>

输出

<svg aria-hidden="true" class="octicon octicon-plus" width="12" height="16" role="img" version="1.1" viewBox="0 0 12 16">
    <path d="M12 9H7v5H5V9H0V7h5V2h2v5h5v2z"></path>
</svg>

我们的方案

可以看见,我们最终上线的方案是往页面 HTML 中直接注入 SVG。这样就可以灵活地实时调整 CSS 的 fill: 声明来修改颜色。

我们现在有一个 SVG 图形的目录而不是一个图标字体,我们通过挑选,将里面这些符号的路径用 helper 直接注入到 HTML 里。比如,通过 <%= octicon(:symbol => "alert") %> 来调用 helper 就可以的到一个警告图标。Helper 会查找同名的文件名,并且注入 SVG。

我们尝试过好几种在页面中添加 SVG 图标的方法,其中有些由于受到 GitHub 生产环境的限制而失败了。

  1. 外部 .svg 文件——最开始我们尝试提供一个单一的外部“SVG 仓库”,然后用 <use> 元素来引入 SVG 拼图中的单个图形。在我们当前的跨域安全策略和资源管道条件下,提供在外部提供 SVG 拼图很难做到。
  2. SVG 背景——这种方式无法实时调整图标的颜色。
  3. 用 <img> 与 src 属性来引入 SVG——这种方式无法实时调整图标的颜色。
  4. 将“SVG 仓库”整个嵌入到每个视图,然后使用 <use> ——把每个 SVG 都嵌入到整个 GitHub.com 的每个单页想想就不对,特别是有时候这个页面一个图标都没用到。

性能

在切换到 SVG 以后,我们还没发现页面加载和性能上有任何不良影响。我们之前曾预计渲染时间会大幅下降,但往往性能和人的感知更相关。由于 SVG 图标被渲染为了指定宽高的图像,页面也不再会像之前那样闪动了。

同时由于我们不再输出字体相关的 CSS,我们还能干掉一些多余的 CSS 代码

缺点和坑

  • Firefox 对 SVG 仍然有像素值计算的问题,虽然图标字体也有相同的问题。
  • 如果你需要 SVG 有背景色,你可能需要在外面包一层额外的 div。
  • 由于 SVG 是作为图片提供的,某些 CSS 的覆盖问题也需要重新考量。如果你看到我们的页面布局有任何奇怪的地方,请告知。
  • IE 浏览器下,需要对 svg 元素指定宽高属性,才能正常显示大小。
  • 在技术方案升级过程中,我们层同时输出 SVG 和图标字体。在我们仍然为每个 SVG 图标指定 font-family 时会导致 IE 崩溃。在完全转用 SVG 以后,这个问题就解决了。

总结

 

通过换掉图标字体,我们能更方便、更快速、更有可访问性地提供图标了。而且它们看起来也更棒了。享受吧。

PurifyCSS:移除单页应用中没有使用的 CSS

admin阅读(2812)评论(0)

PurifyCSS 是干啥的

  • 可以检查应用程序中在使用哪些 CSS 类,进而创建一个(排除了没有用上的 CSS )的文件
  • 也可以检测 JavaScript 中动态加载的 CSS 类
  • 除了可用于单页应用(single-page app)之外,PurifyCSS 也可以用于静态页面应用(static page app)。

可能减少的量

  • Bootstrap 文件大小 ~140k
  • 平均 Bootstrap 的使用率是: ~40% (最多)
  • Minified Bootstrap: ~117k characters.
  • Purified + Minified Bootstrap: ~27k characters

github主页:https://github.com/purifycss/purifycss

React Native:使用 JavaScript 构建应用

admin阅读(9052)评论(0)

原文:Introducing React Native: Building Apps with JavaScript

== 申明:转载请注明出处,尊重一下作者与译者的劳动:] ==

几个月前 Facebook 才刚向世人展露 React Native:一个用 JavaScript 来构建原生 iOS 应用的框架,现在它的 Beta 版官方源码就已被放上了 github。

人们用 JavaScript 和 HTML5 写 iOS 应用,再用 PhoneGap 进行封装,这样的工作模式有一阵子了,React Native 到底有何不同呢?

React Native 完全不是一码事,人们对其带来的两点倍感兴奋:

  • 在 React Native 中,虽然你的应用逻辑是用 JavaScript 所写,然而应用的 UI 可完全是原生的;因此你不能用 HTML5 来绘制 UI 了,这是一个妥协。

  • React 引入了一种非常大胆新颖的实现方式来构建用户界面。简单来说,应用的 UI 被简单地表示为当前应用的状态的一个方法。

React Native 的关键点是,它主要致力于为移动应用开发带来基于 React编程模式的巨大威力。它的目标并不是成为一个 “一次编写到处运行” 的跨平台工具,它的目标是 “在任何地方都用一个框架”。这是个很重要的差别。这篇教程只针对 iOS,但是一旦你学习到了它的概念创建 Android 应用对你来说应该也不是什么难事。

若你曾经写过 Objective-C 或 Swift,你可能对这项 JavaScript 来替代它们工作的技术不会感到特别兴奋。不过,作为一名 Swift 开发者,上述的第二点应该会激起你的兴趣。

通过 Swift,毫无疑问你已学到了更多函数式的编码算法和那些鼓励你转变或不变的技术。但是,在你构建 UI 的方式上,它与用 Objective-C 开发时并没多大不同:它仍是基于 UIKit 的实现。

通过虚拟 DOM 和反射机制等概念,React 将函数式编程直接带入 UI 层。

本篇教程以构建一个用于搜索英国城市房产登记项目的应用来向大家展示其工作原理。

如果你之前从未写过任何 JavaScript,别怕;此教程会为你讲解每一步的代码原理。React 用 CSS 属性定义样式,一般来说它是足够易读易懂的,但如果需要的话,你可以参考优秀的 Mozilla 开发网络参考

想要了解更多?继续阅读吧。

准备工作

React Native 框架已经在 Github 上建立了仓库,你可以通过 git 来克隆到本地,也可以选择下载 zip。一旦将它下载到了本地,在你开始写代码前,还有几件事要办。

React Native 用到了 Node.js,一个 JavaScript 的运行环境,用来构建你的 JavaScript 代码。如果你还未安装它,那是时候装一个了。

第一步,安装 Homebrew,按照 Homebrew 网站上的指示做,然后安装 Node.js,你可以通过终端窗口输入:

brew install node

接下来,用 homebrew 安装 watchman,一个来自 Facebook 的文件 watcher 工具:

brew install watchman

这是 React Native 用来观察代码变化并相应地作出重新构建用的,这好比每次你保存文件后 Xcode 就为你做了一次 build。

React Native 代码在运行之前先要解决它的依赖问题。打开终端窗口定位至 React Native 目录下,并执行:

npm install

这是在用 Node 的包管理器在获取依赖。这与 CocoaPods 和 Carthage 的功能类似。一旦命令运行成功,你会发现一个叫 node_modules 的文件夹被创建,大量的外部依赖文件就在其中。

最后一步是启动开发服务。只需在刚才的终端窗口中继续输入:

npm start

一旦执行,你会看到以下信息:

$ npm start

> react-native@0.1.0 start /Users/colineberhardt/Projects/react-native
> ./packager/packager.sh


 ===============================================================
 |  Running packager on port 8081.       
 |  Keep this packager running while developing on any JS         
 |  projects. Feel free to close this tab and run your own      
 |  packager instance if you prefer.                              
 |                                                              
 |     https://github.com/facebook/react-native                 
 |                                                              
 ===============================================================


React packager ready.

这就对了,我们有了一个好的开头!将运行着脚本的终端窗口暂时搁置一边,我们继续教程。

此时此刻,我建议你先跑一跑官方的例子,看看环境配置是否都正常运转。用 Xcode 打开 react-native/Examples/Movies 目录下的项目文件,编译运行,检查 Movies 应用有没有出现问题。

你好 React Native

在开始编写这款房产搜索应用之前,你将先创建一个极其简单的 Hello World 应用。在此过程中你将接触许多的组件和概念。

下载并解压缩这篇教程的起步项目,将其移至 react-native/Examples目录下。一旦解压完毕,用 Xcode 打开 PropertyFinder 项目文件。先不着急编译运行,你得先写一些 JavaScript。

用你的编辑器打开 PropertyFinderApp.js 在文件的开头加入以下这句:

'use strict';

用以启动严格模式,它将带来更加严格的错误处理,提醒禁止使用一些不太理想的 JavaScript 语言特性。简而言之,它有助于我们写出更好的 JavaScript 代码。

接下来,加入以下这行:

var React = require('react-native');

这将加载 react-native 模块并将其赋值给变量 React。React Native 采用与 Node.js 相同的模块加载技术,利用 require 方法导入。大致类似 Swift 中关联和导入库。

在 require 语句下方,加入以下:

var styles = React.StyleSheet.create({
  text: {
    color: 'black',
    backgroundColor: 'white',
    fontSize: 30,
    margin: 80
  }
});

这句为 “Hello World” 文本单独定义了一个样式。如果你之前做过 web 开发,那你大概都认得这些属性名称。React Native 用层叠样式表(CSS)来定义应用 UI。

回到应用本身!在同一文件中,在刚才的样式申明语句下加入以下代码:

class PropertyFinderApp extends React.Component {
  render() {
    return React.createElement(React.Text, {style: styles.text}, "Hello World!");
  }
}

是的,这是一个 JavaScript 类!

类概念在 ECMAScript 6 (ES6) 中才被引入。尽管 JavaScript 在不断地进化,web 开发者们仍然被老旧的浏览器的兼容性所限制。React Native 运行在 JavaScriptCore 中,因此,你可以运用现代语言的高级功能,而不必担心老旧浏览器不支持的问题。

PropertyFinderApp 扩展自 React.Component, React 最基础的 UI 组件。组件包含了不可变的属性,和可变的状态变量,并且曝露了一个用来渲染组件的方法。目前应用足以简单,只需一个渲染方法。

React Native 组件不属于 UIKit 类,但它们是有一些异曲同工。框架会将 React 组件树转化为所需的原生 UI。

最后,在文件底部加入:

React.AppRegistry.registerComponent('PropertyFinderApp', function() { return PropertyFinderApp });

AppRegistry 定义了一个指向应用的入口并且提供了根组件。

保存 PropertyFinderApp.js 的改动,并回到 Xcode。确保PropertyFinder 配置中指定过了具体的 iPhone 模拟器后,就可以开始编译运行了。没过几秒你就可以看见你的 “Hello World” 应用:

那个跑在模拟器中的 JavaScript 应用,被渲染成了原生 UI,根本看不见浏览器的存在!

还是信不过我?:] 你可以自己验证:在 Xcode 中,选择菜单 Debug\View Debugging\Capture View Hierarchy 看一看原生视图的层级结构。你在任何地方都找不到 UIWebView 的实例。

是不是对其背后的运作原理感到好奇?在 Xcode 中打开 AppDelegate.m并定位到 application:didFinishLaunchingWithOptions: 这句,这个方法构建了一个 RCTRootView,它加载了 JavaScript 应用并渲染了其对应的 view。

当应用开始运行时,RCTRootView 从以下 URL 来加载应用:

http://localhost:8081/Examples/PropertyFinder/PropertyFinderApp.includeRequire.runModule.bundle

回想起在教程开始时当你在终端窗口中执行 npm start 了吧,它其实是开启了一个容器和服务用以处理以上请求。

在 Safari 中打开这个 URL,你可以看到应用的 JavaScript 代码。你应该能找到你的 “Hello World” 应用代码,被包围在 React Native 框架代码之中。

当你的应用运行时,这段代码会被框架的核心加载与执行。在你的应用中,会加载 PropertyFinderApp 组件,然后构建原生 UIKit 视图。你会在接下来的教程中学到更多相关的内容。

Hello World JSX

你的应用目前采用 React.createElement 来构建简单 UI,通过它 React 转变为相应的原生应用。你的 JavaScript 代码在当前格式下的可读性,不一会儿就会被更为复杂的多层嵌套的 UI 所迅速打破。

确保应用还在运行中,回到编辑器重新编辑 PropertyFinderApp.js。修改你的组件渲染方法中的 return 语句:

return <React.Text style={styles.text}>Hello World (Again)</React.Text>;

这是 JSX,或是 JavaScript 语法扩展,它采用类 HTML 语法来混入你的 JavaScript 代码中。如果你是一名 web 开发者,你会对此感到非常熟悉。我们将在接下来的通篇文章中都使用 JSX 语法。

保存 PropertyFinderApp.js 并回到模拟器中,按下 Cmd+R 你会发现你的应用刷新了界面将信息更新为了“Hello World (Again)”。

重新运行 React Native 应用方便至极,就像刷新浏览器一般简单!:]

从此只需关注 JavaScript 文件,一直保持着程序运行,在对PropertyFinderApp.js 修改保存后,简单的刷新调试。

Okay,“Hello World” 玩够了,接下来尝试写个真正的应用吧!

添加导航

房产查找应用采用的是那一套基于标准导航栈的行为体验,提供自 UIKit 的导航控制器。是时候添加这项行为了。

在 PropertyFinderApp.js 中,重新命名 PropertyFinderApp 为HelloWorld

class HelloWorld extends React.Component {

保持 “Hello World” 文本再显示一会儿,但它将不再是你的应用的根组件了。

接下来在 HelloWorld 组件下方增加以下类:

class PropertyFinderApp extends React.Component {
  render() {
    return (
      <React.NavigatorIOS
        style={styles.container}
        initialRoute={{
          title: 'Property Finder',
          component: HelloWorld,
        }}/>
    );
  }
}

这构建了一个导航控制器,应用了样式,设定了初始路由指向HelloWorld 组件。在 web 开发中,路由 是一项用来定义应用导航组织结构的技术:在此应用中,哪些页面,或路由被映射至 URLs。

在同一文件中,更新样式申明以包含 container 样式:

var styles = React.StyleSheet.create({
  text: {
    color: 'black',
    backgroundColor: 'white',
    fontSize: 30,
    margin: 80
  },
  container: {
    flex: 1
  }
});

稍后你会看到关于 flex: 1 的介绍。

让我们回到模拟器并且按下 Cmd+R 发现 UI 变成了这样:

这是导航控制器与其根视图,它目前为 “Hello World” 文本。棒极了 —— 你现在为你的应用添加了适宜的基础导航结构,该到时候添加“真正”的 UI 了!

创建搜索页

为工程新增名为 SearchPage.js 的文件,放置在与PropertyFinderApp.js 同一目录中。为此文件添加以下代码:

'use strict';

var React = require('react-native');
var {
  StyleSheet,
  Text,
  TextInput,
  View,
  TouchableHighlight,
  ActivityIndicatorIOS,
  Image,
  Component
} = React;

你已经在之前见过了严格模式的定义与 react-native 的导入,但接下来的赋值语句可是全新的。

这是解构赋值语句,它让你抽取出多个对象属性并用单条语句将其赋值给变量。所以,接下来你的代码中,可置 React 前缀。打个比方,你可以通过 React.StyleSheet 直接引用到 StyleSheet。解构赋值对于操作数组也是颇有用处,它也是非常值得学习的

回到 SearchPage.js 文件,添加以下代码:

var styles = StyleSheet.create({
  description: {
    marginBottom: 20,
    fontSize: 18,
    textAlign: 'center',
    color: '#656565'
  },
  container: {
    padding: 30,
    marginTop: 65,
    alignItems: 'center'
  }
});

同样的,这些是标准的 CSS 属性。像这样设置样式不如用界面构建工具来的直观,但它好过在你的 viewDidLoad() 方法中一个接着一个来设置视图属性!:]

在样式下方添加组件自身代码:

class SearchPage extends Component {
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.description}>
          Search for houses to buy!
        </Text>
        <Text style={styles.description}>
          Search by place-name, postcode or search near your location.
        </Text>
      </View>
    );
  }
}

render 是 JSX 和与之提供的结构的重大示例。透过这样的方式,你可以对这些组件构建成的 UI 一目了然:一个容器两个文本标签。

最后,我们在文件的结尾处添加:

module.exports = SearchPage;

用以输出 SearchPage 类,它规定了其它文件允许引用的部分。

接着的步骤是要更新应用的路由以便初始化路由。

打开 PropertyFinderApp.js,在文件顶部 require 方法后加入:

var SearchPage = require('./SearchPage');

PropertyFinderApp 类中的 render 方法内,更新 initialRoute 以引用新增页面,如下所示:

component: SearchPage

此刻你可以移除 HelloWorld 类和与之相关的样式了。如过你愿意,你将再也不需要那段代码了。

回到模拟器,敲打 Cmd+R 查看新 UI:

这就是运用了的新组件:SearchPage,你刚刚添加的。

用 Flexbox 定义样式

到目前为止,你已经见过了基础的 CSS 属性诸如 margins,paddings 和 color,但你可能不太熟悉 flexbox, 这是不久前才被加入 CSS 规范的属性,它对于应用 UI 排版布局那是相当的有用处。

React Native 采用了 css-layout 库,一个 JavaScript 对于 flexbox 标准的实现,它将布局转化成 C (for iOS) 和 Java (for Android)。

它很了不起,因为 Facebook 已将其作为一个独立的项目来进行开发维护,旨在多种语言。自此诞生了不少新奇的应用,比如在 SVG 上应用 flexbox 布局(是的,那是我的作品…但噢不,我难尝饱眠已久)。

在你的应用中,容器拥有默认的列布局,这意味着它的子元素是按垂直堆叠排列的,像这样:

这被称为主轴,它的排列可垂直可水平。

每个子元素的垂直位置,由其 margin,height 和 padding 来共同决定。容器也设置了 alignItems 属性为 center,这将影响其水平轴交叉轴上的排列,导致文本居中。

该为其加上输入框和按钮了。打开 SearchPage.js,在第二个 Text 元素的闭合标签后加入以下代码:

<View style={styles.flowRight}>
  <TextInput
    style={styles.searchInput}
    placeholder='Search via name or postcode'/>
  <TouchableHighlight style={styles.button}
      underlayColor='#99d9f4'>
    <Text style={styles.buttonText}>Go</Text>
  </TouchableHighlight>
</View>
<TouchableHighlight style={styles.button}
    underlayColor='#99d9f4'>
  <Text style={styles.buttonText}>Location</Text>
</TouchableHighlight>

目前你已增加了两个顶级视图:一个拥有着一文本输入框和一按钮,另一个只包含了一按钮。一会儿再跟你解释如何定义这些元素样式的。

接下来,添加相应的样式到你的 styles 定义中:

flowRight: {
  flexDirection: 'row',
  alignItems: 'center',
  alignSelf: 'stretch'
},
buttonText: {
  fontSize: 18,
  color: 'white',
  alignSelf: 'center'
},
button: {
  height: 36,
  flex: 1,
  flexDirection: 'row',
  backgroundColor: '#48BBEC',
  borderColor: '#48BBEC',
  borderWidth: 1,
  borderRadius: 8,
  marginBottom: 10,
  alignSelf: 'stretch',
  justifyContent: 'center'
},
searchInput: {
  height: 36,
  padding: 4,
  marginRight: 5,
  flex: 4,
  fontSize: 18,
  borderWidth: 1,
  borderColor: '#48BBEC',
  borderRadius: 8,
  color: '#48BBEC'
}

格外小心它的格式,每一个样式属性定义块后必须用逗号来隔开。这意味着在 container 的样式定义结尾处也要有逗号。

这些样式将应用在你刚添加的输入框和按钮上。

回到模拟器按下 Cmd+R 以刷新 UI:

文本域和 “Go” 按钮要并列一排,所以你要将其都包裹在同一容器中,并给容器增加 flexDirection: 'row' 的样式。不要为输入框和按钮设置具体的宽度,取而代之的是给它们一个 flex 值。文本域设置为 flex: 4,与此同时将按钮设置为 flex: 1;,所以它们的宽度是 4:1 比例。

你也许注意到了这些按钮,并不是真正的按钮!:] 在 UIKit 中,原生按钮比标签更不易点击,因此 React Native 团队决定直接在 JavaScript 中构建按钮来的更容易一些。在你的应用中,按钮使用TouchableHighlight 来构建,这是一个 React Native 组件,当它被点击时它会变得透明从而使得其底层的颜色显露出来。

最后,完善应用搜索页面还差一张房子的图片。在 location 按钮的TouchableHighlight 组件后加入:

<Image source={require('image!house')} style={styles.image}/>

现在,在样式列表中增加图片相应的样式,别忘了它与它之前的样式定义之间要加入逗号。

image: {
  width: 217,
  height: 138
}

语句 require('image!house') 用以引用定位在应用静态资源目录下的图片资源的。在 Xcode 中,打开 Images.xcassets 你会发现以上代码引用的 “house” 图标。

回到模拟器按下 Cmd+R 欣赏你的新 UI 吧:

现在你的应用看上去不错,但缺少相应的功能。所以当务之急是增加应用的状态和表现行为。

增加组件状态

每一个 React 组件都有自己的状态对象,它以键值的形式作为存储。在一个组件被渲染之前,我们必须先设置它的初始状态。

在 SearchPage.js 中,为 SearchPage 类加入以下代码,加在render() 之前:

constructor(props) {
  super(props);
  this.state = {
    searchString: 'london'
  };
}

组件当前拥有了 state 变量,其中的 searchString 被设置成了一个初始值:london

看看怎么使用这个组件状态的。在 render 中,用以下代码替换TextInput 元素:

<TextInput
  style={styles.searchInput}
  value={this.state.searchString}
  placeholder='Search via name or postcode'/>

此处将 TextInput 的 value 属性 —— 那是向用户展示文本用的 —— 设置为了当下 searchString 状态变量的值。这只是用以处理初始状态的设置,那接下来当用户编辑本文又会发生什么呢?

第一步是为此动作创建一个事件方法。在 SearchPage 类定义中增加如下方法:

onSearchTextChanged(event) {
  console.log('onSearchTextChanged');
  this.setState({ searchString: event.nativeEvent.text });
  console.log(this.state.searchString);
}

这可从事件的 text 属性获取值并且用它来更新组件状态,同时也增加了一些打印信息的代码用以测试。

为使文本变化时方法被调用,回到在 render 方法中的 TextInput 元素上,我们为其增添 onChange 属性,它看起来成了这样:

<TextInput
  style={styles.searchInput}
  value={this.state.searchString}
  onChange={this.onSearchTextChanged.bind(this)}
  placeholder='Search via name or postcode'/>

只要用户一改变输入文本,onChange 即刻被触发,onSearchTextChanged 方法被执行。

在你刷新应用前我们再做一步操作:将下列打印语句加入到 render()顶部,return 之前:

console.log('SearchPage.render');

你稍后会对这些打印语句有所了解的!:]

回到模拟器,按下 Cmd+R,你应该看到输入框内的初始文本显示为 “london” 并且一旦对其进行编辑一些调试语句就被打印在了 Xcode 的控制台里。

查看以上截图,打印语句的顺序有些奇怪:

  1. 初始调用 render() 以创建视图。
  2. 当文本改变时执行 onSearchTextChanged()
  3. 接着更新组件状态反应在新的输入框文本上,后者又触发了另一轮 render。
  4. onSearchTextChanged() 在日志中输出新的搜索字符串。

无论何时,只要是应用一更新任意组件的状态,它都将会触发整个 UI 进行重新渲染,在此期间,所有组件的 render 方法都会调用执行。这是一个很赞的思路,因为它完全地对渲染逻辑进行了解耦,这些逻辑由影响 UI 的状态变化所产生。

在其它众多的 UI 框架中,要么是由你负责手动地更新基于 UI 的状态变化,要么是利用某种绑定框架来创建一个的应用状态和它的 UI 呈现之间内在关联。

看,举个例子,我的文章:实现在 ReactiveCocoa 中的 MVVM 模式

用 React,你不必再为究竟是哪一部分的 UI 也许受到了状态变化的影响而感到担心,整个 UI 都像是作为一个你的应用状态的方法函数来进行简单传递。

这一刻你可能会指出这套概念当中的一个缺点。没错,就是 —— 性能。

当然了你不可能删除整个 UI 再重新生成,每次一有状态改变就这么做?这可是 React 聪明之处。每一次 UI 渲染,它都会将渲染方法返回的视图树与当前的 UIKit 视图进行比对。比对的结果中相差异的部分才是 React 需要对当前视图做出更新的部分,也就是说,只有改动的一小部分才会被真正的重新渲染生成。

令人刮目相看的是这新奇的理念使得 ReactJS 如此独特 —— 虚拟 DOM (文档对象模型, web 文档的可视树)和反射机制 —— 运用在 iOS 应用之中。

你可以到时候再回头看这些东西,我们仍要继续完善当前的 app。删除打印信息语句,你不再需要它们来扰乱你的代码了。

开始搜索

为了实现搜索功能你需要在 “Go” 按钮的点击事件上做些文章。创建一个合适的 API 请求,并且为用户在此数据查询的等待过程中提供一个可视化的指示界面。

在 SearchPage.js 中,更新初始状态:

this.state = {
  searchString: 'london',
  isLoading: false
};

新增的 isLoading 属性用来跟踪数据查询是否完成。

将以下逻辑加入到 render 的开头:

var spinner = this.state.isLoading ?
  ( <ActivityIndicatorIOS
      hidden='true'
      size='large'/> ) :
  ( <View/>);

这是一个三元的 if 判断语句,它的执行结果不是增加一个动态指示器就是一个空视图,这取决于组件的 isLoading 状态。因为整个组件每次都会渲染,所以你可以对 JSX 和 JavaScript 逻辑代码随意混用。

JSX 在 return 中定义的搜索 UI 里,加入下列一行,位于 Image 下方:

{spinner}

现在,在 TouchableHighlight 包裹着的 “Go” 文本视图上增加以下属性:

onPress={this.onSearchPressed.bind(this)}

接着,在 SearchPage 类里添加以下方法:

_executeQuery(query) {
  console.log(query);
  this.setState({ isLoading: true });
}

onSearchPressed() {
  var query = urlForQueryAndPage('place_name', this.state.searchString, 1);
  this._executeQuery(query);
}

_executeQuery() 会执行最终的查询,但现在我们先让它简单地输出一些信息在控制台里,并将 isLoading 设置设置为 true 以便 UI 更新。

当 “Go” 按钮被点击时,onSearchPressed() 将被执行以开展查询。

最后,在 SearchPage 类申明之前加入以下工具类方法:

function urlForQueryAndPage(key, value, pageNumber) {
  var data = {
      country: 'uk',
      pretty: '1',
      encoding: 'json',
      listing_type: 'buy',
      action: 'search_listings',
      page: pageNumber
  };
  data[key] = value;

  var querystring = Object.keys(data)
    .map(key => key + '=' + encodeURIComponent(data[key]))
    .join('&');

  return '//api.nestoria.co.uk/api?' + querystring;
};

这个方法不依赖于 SearchPage,所以它以一个独立的函数而不是一个类方法的形式存在。它一开始创建了基于参数的查询字符串在 data 之中,接下来它将数据转换成要求的字符串格式,name=value结队成组并用 & 字符连接着。这里的 => 语法是表示一个箭头函数,这是新近加入的 JavaScript 语言中用以创建匿名函数的简要语法。

回头看模拟器,按下 Cmd+R 重新载入应用并点击 “Go” 按钮,你将能看到旋转的动态指示器。查看 Xcode 控制台,你将看到如下:

菊花图展现在视图中数据请求 URL 显示在控制台日志里。复制粘贴这段 URL 到你的浏览器中看看结果如何。你将会看到大段的 JSON 对象,别担心 —— 你不必了解它!你将编写代码解析它。

下一步我们在应用中处理这项请求。

处理 API 请求

还是在 SearchPage.js 中,更新初始状态,在构造方法中加入message 变量:

this.state = {
  searchString: 'london',
  isLoading: false,
  message: ''
};

在 render 中,在 UI 底部加入:

<Text style={styles.description}>{this.state.message}</Text>

它为向用户显示了信息条数。

在 SearchPage 类中,加入以下代码到 _executeQuery() 结尾处:

fetch(query)
  .then(response => response.json())
  .then(json => this._handleResponse(json.response))
  .catch(error => 
     this.setState({
      isLoading: false,
      message: 'Something bad happened ' + error
   }));

这里采用了 fetch 函数,它是 Web API 中的一部分,提供了大量改进的针对 XMLHttpRequest 的 API。异步响应将以一个 promise 的形式返回,在成功回路中将解析的 JSON 提供给一个接下来要新增的方法。

最后一步是增加下列方法到 SearchPage

_handleResponse(response) {
  this.setState({ isLoading: false , message: '' });
  if (response.application_response_code.substr(0, 1) === '1') {
    console.log('Properties found: ' + response.listings.length);
  } else {
    this.setState({ message: 'Location not recognized; please try again.'});
  }
}

这将清除 isLoading 状态和请求成功后显示在调试日志内的房产数量信息。

保存你的修改,然后回到模拟器刷新之,试着搜索关键字 “london”,你应该能在日志中看到 20 处房产已被找到的信息。接下来试着搜索一个不存在的地点,比如 “narnia”,迎接你的将是以下信息:

到了展现这伦敦真实位置的 20 处房产的时刻了。

展示结果

创建一个 SearchResults.js 文件,加入以下代码:

'use strict';

var React = require('react-native');
var {
  StyleSheet,
  Image, 
  View,
  TouchableHighlight,
  ListView,
  Text,
  Component
} = React;

对,没错,一个包含着 react-native 模块的 require 语句,还有一个解构赋值。这些都在之前提过了对吧?

接着添加组件自身:

class SearchResults extends Component {

  constructor(props) {
    super(props);
    var dataSource = new ListView.DataSource(
      {rowHasChanged: (r1, r2) => r1.guid !== r2.guid});
    this.state = {
      dataSource: dataSource.cloneWithRows(this.props.listings)
    };
  }

  renderRow(rowData, sectionID, rowID) {
    return (
      <TouchableHighlight
          underlayColor='#dddddd'>
        <View>
          <Text>{rowData.title}</Text>
        </View>
      </TouchableHighlight>
    );
  }

  render() {
    return (
      <ListView
        dataSource={this.state.dataSource}
        renderRow={this.renderRow.bind(this)}/>
    );
  }

}

以上代码使用到了更加特殊的组件 —— ListView —— 它在一个可滚动的容器中展现多排数据,类似 UITableView。你可以通过ListView.DataSource 和一个方法来向 ListView 提供数据,这个方法每一行数据提供 UI 的实现。

构建数据源时,你应当提供一个能两两比较行 id 的方法。ListView 用此在核对进程中来明确列表数据的具体改动。在这种情况下,Nestoria 房产的 API 所返回的 guid 属性,正好符合这一目的。

现在增加模块输出定义在文件底部:

module.exports = SearchResults;

增加以下代码至 SearchPage.js 文件顶部位置,插入在用以请求 React 的 require 之下:

var SearchResults = require('./SearchResults');

这样做使得我们在 SearchPage 中得以运用刚才添加的SearchResults 类。

修改当前的 _handleResponse 方法,将 console.log 语句替换为下列代码:

this.props.navigator.push({
  title: 'Results',
  component: SearchResults,
  passProps: {listings: response.listings}
});

以上代码将为你导航到新增的 SearchResults 组件并通过 API 请求来为列表传递数据。使用 push 方法可保证搜索结果页被推入之导航栈中,这意味着你可以通过返回按钮来回到根目录。

回到模拟器刷新并搜索,展现在你面前的是一份房产列表:

很高兴得到了我们期望已久的房产列表,但这列表展示略挫,让我们来让它变的更好一些。

接触样式

这些 React Native 代码自此应该一回生二回熟了,以此以下教程可以两步并作一步来快速讲解。

在 SearchResults.js 中的解构赋值语句后添加以下样式定义:

var styles = StyleSheet.create({
  thumb: {
    width: 80,
    height: 80,
    marginRight: 10
  },
  textContainer: {
    flex: 1
  },
  separator: {
    height: 1,
    backgroundColor: '#dddddd'
  },
  price: {
    fontSize: 25,
    fontWeight: 'bold',
    color: '#48BBEC'
  },
  title: {
    fontSize: 20,
    color: '#656565'
  },
  rowContainer: {
    flexDirection: 'row',
    padding: 10
  }
});

以上定义了每一行即将要被渲染的列表样式。

接着用以下代码替换 renderRow() 方法:

renderRow(rowData, sectionID, rowID) {
  var price = rowData.price_formatted.split(' ')[0];

  return (
    <TouchableHighlight onPress={() => this.rowPressed(rowData.guid)}
        underlayColor='#dddddd'>
      <View>
        <View style={styles.rowContainer}>
          <Image style={styles.thumb} source= />
          <View  style={styles.textContainer}>
            <Text style={styles.price}>£{price}</Text>
            <Text style={styles.title} 
                  numberOfLines={1}>{rowData.title}</Text>
          </View>
        </View>
        <View style={styles.separator}/>
      </View>
    </TouchableHighlight>
  );
}

这对返回的价格数据做了一点操作,将其 “300,000 GBP” 固定格式中的 GBP 后缀给去掉。然后渲染每一行 UI 采用的是你已熟烂于心的技术了。这一次,略缩图的数据是以 URL 的方式提供的,React Native 不会在主线程对其进行解码。

注意 TouchableHighlight 组件的 onPress 属性,它利用了箭头函数来获取每一行的 guid

最后我们增加 press 的处理方法:

rowPressed(propertyGuid) {
  var property = this.props.listings.filter(prop => prop.guid === propertyGuid)[0];
}

这个方法能定位哪一行房产正被用户点击中。目前它还起不了作用,一会我们再来修复它。但现在,我们来欣赏下你的作品吧。

回到模拟器刷新,来看看你的搜索结果页吧:

看上去好很多了 —— 虽然这让人感到惊奇谁能住的起伦敦!

到了添加应用最后的视图的时候了。

房产详细页视图

增加新文件 PropertyView.js 到项目中,接着在文件中加入代码:

'use strict';

var React = require('react-native');
var {
  StyleSheet,
  Image, 
  View,
  Text,
  Component
} = React;

我敢确定这些都已无需再过多介绍,你在梦里都做到了!:]

接下来增添下列样式:

var styles = StyleSheet.create({
  container: {
    marginTop: 65
  },
  heading: {
    backgroundColor: '#F8F8F8',
  },
  separator: {
    height: 1,
    backgroundColor: '#DDDDDD'
  },
  image: {
    width: 400,
    height: 300
  },
  price: {
    fontSize: 25,
    fontWeight: 'bold',
    margin: 5,
    color: '#48BBEC'
  },
  title: {
    fontSize: 20,
    margin: 5,
    color: '#656565'
  },
  description: {
    fontSize: 18,
    margin: 5,
    color: '#656565'
  }
});

再来增加组件:

class PropertyView extends Component {

  render() {
    var property = this.props.property;
    var stats = property.bedroom_number + ' bed ' + property.property_type;
    if (property.bathroom_number) {
      stats += ', ' + property.bathroom_number + ' ' + (property.bathroom_number > 1
        ? 'bathrooms' : 'bathroom');
    }

    var price = property.price_formatted.split(' ')[0];

    return (
      <View style={styles.container}>
        <Image style={styles.image} 
            source= />
        <View style={styles.heading}>
          <Text style={styles.price}>£{price}</Text>
          <Text style={styles.title}>{property.title}</Text>
          <View style={styles.separator}/>
        </View>
        <Text style={styles.description}>{stats}</Text>
        <Text style={styles.description}>{property.summary}</Text>
      </View>
    );
  }
}

第一部分的 render() 方法处理数据操作。通常来讲,由 API 返回来的数据往往不是很完美,所以我们要写一些简单的逻辑来处理得到我们想要的数据格式。

其余的 render 不用说也很简单,它是一个组件的不可变状态的方法。

最后我们在文件底部加上输出:

module.exports = PropertyView;

回到 SearchResults.js 并增加 require 语句在文件头部,紧随 React require 这一行:

var PropertyView = require('./PropertyView');

接下来更新 rowPressed() 将导航转向你新增的 PropertyView

rowPressed(propertyGuid) {
  var property = this.props.listings.filter(prop => prop.guid === propertyGuid)[0];

  this.props.navigator.push({
    title: "Property",
    component: PropertyView,
    passProps: {property: property}
  });
}

你懂的,回到模拟器按下 Cmd+R,一路搜索查看下来,你会看到:

负担的起的生活才是最好滴 —— 那个软垫真好看!(译者注:作者你又调皮了)

至此你的应用已基本完成。还差一步就是允许用户搜索其周边房产。

基于地理位置的搜索

在 Xcode 中,打开 Info.plist 新增一个键,通过点击右键选择 Add Row 来完成。键名为 NSLocationWhenInUseUsageDescription,并用取值为:

PropertyFinder would like to use your location to find nearby properties

这是你完成后 plist 文件的样子:

这副键值是用来提示用户确认是否要使用他们的地理位置的。

打开 SearchPage.js,定位到 “Location” 按钮的TouchableHighlight 上,加入以下属性值:

onPress={this.onLocationPressed.bind(this)}

当你点击这个按钮时,它将执行 onLocationPressed —— 接下来要添加的。

在 SearchPage 类中添加:

onLocationPressed() {
  navigator.geolocation.getCurrentPosition(
    location => {
      var search = location.coords.latitude + ',' + location.coords.longitude;
      this.setState({ searchString: search });
      var query = urlForQueryAndPage('centre_point', search, 1);
      this._executeQuery(query);
    },
    error => {
      this.setState({
        message: 'There was a problem with obtaining your location: ' + error
      });
    });
}

用户当前位置通过 navigator.geolocation 被取回,这是一个被 Web API 定义的接口,所以它对于曾在浏览器当中用过定位服务的人来说非常熟悉了。React Native 框架提供了自身对原生 iOS 定位服务的 API 实现。

如果当前位置被成功捕获,将执行第一个箭头函数,它向 Nestoria 发起了一个查询请求。如果出错,它将显示一个基本的报错信息。

一旦你更改了 plist 文件,就必须重启应用才能生效。Cmd+R 无效了这回 —— 抱歉。在 Xcode 停止运行应用,重新构建运行。

在使用基于定位服务的搜索时,你的具体位置必须是在 Nestoria 房产公司的数据库中。从模拟器的菜单选项中选择 Debug\Location\Custom Location... 并输入维度 55.02 和经度 -1.42,这坐标来自英格兰北部的一座海滨小镇,我称之为故乡!

虽然不如伦敦 —— 但起码住的起!:]

接下来该做什么?

恭喜你完成了第一个 React Native 应用!你可以下载本教程的最终完成项目跑起来试试。

如果你来自 Web 界,你会发现用 JavaScript 和 React定义界面和导航并从中获得完整的原生 UI 是多么地容易。但如果你是原生应用的开发者,我希望你能从 React Native 获得一点感官上的启发:快速应用迭代,现代的 JavaScript 语言和清晰的 CSS 样式。

也许你希望用这套框架来写你下一个应用?亦或,你仍坚持 Swift 或 Objective-C? 无论你选择哪条路,我都希望你能从这篇文章中受益,并且能为你今后的项目中带来一些想法。

如果对本篇教程有任何的意见或建议,请加入论坛来讨论。(译者注:抱歉,我没发现论坛链接,估计作者说的是文章下方的留言区啦)

阅读详情 -> React Native:使用 JavaScript 构建应用 – 肉山·察.

1px on retina | EFE Tech

admin阅读(5841)评论(0)

一直以来我们实现边框的方法都是设置 border: 1px solid #ccc,但是在retina屏上因为设备像素比的不同,边框在移动设备上的表现也不相同:1px可能会被渲染成1.5px, 2px, 2.5px, 3px....,在用户体验上略差,所以现在要解决的问题就是在retina屏幕实现1px边框。

如果你去google类似问题,诚然会找到所谓的”答案“,然后很开森的用到项目中了。运气好的话,Yeah成功模拟1px了,运气不好了可能遇到各种奇葩的表现让你抓狂。

这篇文章总结了目前常用的模拟1px的方法,并分析各个方法的利弊。

实现方案

1、软图片

‘软图片’,即通过CSS渐变模拟,代码如下:

.retina(@top: transparent, @right: transparent, @bottom: transparent, @left: transparent, @w: 1px) {
    @media only screen and (-webkit-min-device-pixel-ratio: 2),
    only screen and (min-device-pixel-ratio: 2) {
        border: none;
        background-image:
            linear-gradient(180deg, @top, @top 50%, transparent 50%),
            linear-gradient(270deg, @right, @right 50%, transparent 50%),
            linear-gradient(0deg, @bottom, @bottom 50%, transparent 50%),
            linear-gradient(90deg, @left, @left 50%, transparent 50%);
        background-size: 100% @w, @w 100%, 100% @w, @w 100%;
        background-repeat: no-repeat;
        background-position: top, right top,  bottom, left top;
    }
}

这段代码可能是从网上找到的出现最频繁的代码了,但是这样写是有兼容问题的,

测试小米2自带浏览器、手机百度、百度浏览器都显示不出上边框,如图:

测试小米2 chrome浏览器正常,如图:

这种情况我们会考虑是不是没有写浏览器前缀-webkit-的原因,好,我们加上:

background-image:
     -webkit-linear-gradient(180deg, @top, @top 50%, transparent 50%),
     -webkit-linear-gradient(270deg, @right, @right 50%, transparent 50%),
     -webkit-linear-gradient(0, @bottom, @bottom 50%, transparent 50%),
     -webkit-linear-gradient(90deg, @left, @left 50%, transparent 50%);

再次检测小米2自带浏览器、手机百度、百度浏览器、chrome,这一次表现都一致!慢着好像有些不对:

怎么会这样呢??看样子是渐变方向不对,通过调整渐变方向得到结果:加上-webkit私有前缀的0deg的渐变方向是从左向右,而规范定义的0deg的渐变方向是自下而上

知道原因了,我们再改改代码吧:

background-image:
    -webkit-linear-gradient(270deg, @top, @top 50%, transparent 50%),
    -webkit-linear-gradient(180deg, @right, @right 50%, transparent 50%),
    -webkit-linear-gradient(90deg, @bottom, @bottom 50%, transparent 50%),
    -webkit-linear-gradient(0, @left, @left 50%, transparent 50%);
background-image:
    linear-gradient(180deg, @top, @top 50%, transparent 50%),
    linear-gradient(270deg, @right, @right 50%, transparent 50%),
    linear-gradient(0deg, @bottom, @bottom 50%, transparent 50%),
    linear-gradient(90deg, @left, @left 50%, transparent 50%);

Done!

优点:

  • 可以实现单个、多个边框,大小、颜色可以配置
  • 对比下面介绍的其他方法,这个方法兼容性比较好,实现效果也相对不错

缺点:

  • 很明显代码特别长
  • 无法实现圆角
  • 使用时可能需要配合 padding,如设置子元素的背景可能会挡住父元素所设置的1px软图片
  • 如果有背景颜色,要写成background-color,不然会不小心覆盖掉
  • 对于非 retina 屏,需要写 border: 1px solid #f00 进行适配

2、缩放

‘缩放’,即使用css transform缩放一半的大小,代码如下:

.transform-scale {
    position: relative;
    &:after,
    &:before {
        content: '';
        position: absolute;
        left: 0;
        top: 0;
        height: 1px;
        width: 100%;
        -webkit-transform: scaleY(0.5);
        transform: scaleY(0.5);
        -webkit-transform-origin: 0 0;
        transform-origin: 0 0;
        background: #f00;
    }
    &:after {
        top: auto;
        bottom: 0;
        -webkit-transform-origin: 0 100%;
        transform-origin: 0 100%;
    }
}

优点:

  • 实现单线条简单
  • 大小、颜色可以配置

缺点:

  • 无法实现圆角
  • 四条边框比较纠结
  • 依赖DOM,可能会与已有样式冲突,如常用的clearfix

3、阴影

.shadow {
    -webkit-box-shadow:0 1px 1px -1px rgba(255, 0, 0, 0.5);
    box-shadow:0 1px 1px -1px rgba(255, 0, 0, 0.5);
}

没觉得这个方法好用,模拟的效果差强人意,颜色也不好配置,不推荐

4、0.5px

终于等来了0.5px,虽然只有IOS8+才支持

// IOS8 hairline
.hairline(@color, @style:solid) {
    @media (-webkit-min-device-pixel-ratio: 2) {
        border: 0.5px @style @color;
    }
}

优点:

  • “原生”,支持圆角~

缺点:

  • 目前只有IOS8+才支持,在IOS7及其以下、安卓系统都是显示为0px

5、viewport&&rem

再谈mobile web retina 下 1px 边框解决方案介绍了viewport结合rem解决设备像素比的问题,即让我们像以前写1倍像素那样写页面。

如在devicePixelRatio=2下设置<meta>

<meta name="viewport" content="initial-scale=0.5, maximum-scale=0.5, minimum-scale=0.5, user-scalable=no">

再设置rem,假设header的高度是30px(设备像素比为1的情况):

html {
    font-size: 20px;
}
header {
    height: 3rem;
}

没有具体实践过,不知道有神马坑~

PS:淘宝、美团移动端页面都是采用这个方式实现的

6、border-image

使用的背景图片:

代码:

.border-image-1px {
    border-width: 1px 0px;
    -webkit-border-image: url(border.png) 2 0 stretch;
    border-image: url(border.png) 2 0 stretch;
}

优点:

  • 额,,,

缺点:

  • 大小、颜色更改不灵活
  • 放到PS里面看边框,是有点模糊的(因为带有颜色部分是1px,在retina屏幕上拉伸到2px肯定会有点模糊)

总结

1、0.5px,相信浏览器肯定是会慢慢支持的;目前而言,如果能用的话,可以hack一下;

2、阴影,border-image的方案不建议使用(用了你就知道。。。)

3、背景图片和缩放可以在项目中配合使用,如单个线条使用缩放,四条框用背景图片模拟,额,如果要圆角的话,无能无力了

其他

Demo

 

1px Demo – jsbin

阅读详情 -> 1px on retina | EFE Tech.

前端开发相关广告投放 更专业 更精准

联系我们