无需申请自动送彩金68_白菜送彩金网站大全[无需申请秒送彩金]
做最好的网站
来自 无需申请自动 2019-10-19 08:44 的文章
当前位置: 无需申请自动送彩金68 > 无需申请自动 > 正文

致我们终将组件化的Web,离线网页应用

理解SVG坐标系和变换:视窗,viewBox和preserveAspectRatio

2015/09/23 · HTML5 · SVG

原文出处: SaraSoueidan   译文出处:Blueed(@Ivan_z3)   

SVG元素不像HTML元素一样由CSS盒模型管理。这使得我们可以更加灵活定位和变换这些元素-也许一眼看上去不太直观。然而,一旦你理解了SVG坐标系和变换,操纵SVG会非常简单并且很有意义。本篇文章中我们将讨论控制SVG坐标系的最重要的三个属性:viewport, viewBox, 和 preserveAspectRatio

这是本系列三篇文章中的第一篇,这篇文章讨论SVG中的坐标系和变换。

  • 理解SVG坐标系和变换(第一部分)-viewport,viewBox,和preserveAspectRatio
  • 理解SVG坐标系和变换(第二部分)-transform属性
  • 理解SVG坐标系和变换(第三部分)-建立新视窗

为了使文中的内容和解释更形象化,我创建了一个互动演示,你可以任意改变viewBox 和 preserveAspectRatio的值。

在线案例

这个例子只是主要内容的一小部分,所以看完请回来继续阅读这篇文章

致我们终将组件化的Web

2015/11/25 · HTML5 · 1 评论 · 组件化

原文出处: AlloyTeam   

这篇文章将从两年前的一次技术争论开始。争论的聚焦就是下图的两个目录分层结构。我说按模块划分好,他说你傻逼啊,当然是按资源划分。

图片 1 《=》图片 2

”按模块划分“目录结构,把当前模块下的所有逻辑和资源都放一起了,这对于多人独自开发和维护个人模块不是很好吗?当然了,那争论的结果是我乖乖地改回主流的”按资源划分“的目录结构。因为,没有做到JS模块化和资源模块化,仅仅物理位置上的模块划分是没有意义的,只会增加构建的成本而已。

虽然他说得好有道理我无言以对,但是我心不甘,等待他日前端组件化成熟了,再来一战!

而今天就是我重申正义的日子!只是当年那个跟你撕逼的人不在。

模块化的不足

模块一般指能够独立拆分且通用的代码单元。由于JavaScript语言本身没有内置的模块机制(ES6有了!!),我们一般会使用CMD或ADM建立起模块机制。现在大部分稍微大型一点的项目,都会使用requirejs或者seajs来实现JS的模块化。多人分工合作开发,其各自定义依赖和暴露接口,维护功能模块间独立性,对于项目的开发效率和项目后期扩展和维护,都是是有很大的帮助作用。

但,麻烦大家稍微略读一下下面的代码

JavaScript

require([ 'Tmpl!../tmpl/list.html','lib/qqapi','module/position','module/refresh','module/page','module/net' ], function(listTmpl, QQapi, Position, Refresh, Page, NET){ var foo = '', bar = []; QQapi.report(); Position.getLocaiton(function(data){ //... }); var init = function(){ bind(); NET.get('/cgi-bin/xxx/xxx',function(data){ renderA(data.banner); renderB(data.list); }); }; var processData = function(){ }; var bind = function(){ }; var renderA = function(){ }; var renderB = function(data){ listTmpl.render('#listContent',processData(data)); }; var refresh = function(){ Page.refresh(); }; // app start init(); });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
require([
    'Tmpl!../tmpl/list.html','lib/qqapi','module/position','module/refresh','module/page','module/net'
], function(listTmpl, QQapi, Position, Refresh, Page, NET){
    var foo = '',
        bar = [];
    QQapi.report();
    Position.getLocaiton(function(data){
        //...
    });
    var init = function(){
        bind();
        NET.get('/cgi-bin/xxx/xxx',function(data){
            renderA(data.banner);
            renderB(data.list);
        });
    };
    var processData = function(){
    };
    var bind = function(){
    };
    var renderA = function(){
    };
    var renderB = function(data){
        listTmpl.render('#listContent',processData(data));
    };
    var refresh = function(){
        Page.refresh();
    };
    // app start
    init();
});

上面是具体某个页面的主js,已经封装了像Position,NET,Refresh等功能模块,但页面的主逻辑依旧是”面向过程“的代码结构。所谓面向过程,是指根据页面的渲染过程来编写代码结构。像:init -> getData -> processData -> bindevent -> report -> xxx 。 方法之间线性跳转,你大概也能感受这样代码弊端。随着页面逻辑越来越复杂,这条”过程线“也会越来越长,并且越来越绕。加之缺少规范约束,其他项目成员根据各自需要,在”过程线“加插各自逻辑,最终这个页面的逻辑变得难以维护。

图片 3

开发需要小心翼翼,生怕影响“过程线”后面正常逻辑。并且每一次加插或修改都是bug泛滥,无不令产品相关人员个个提心吊胆。

 页面结构模块化

基于上面的面向过程的问题,行业内也有不少解决方案,而我们团队也总结出一套成熟的解决方案:Abstractjs,页面结构模块化。我们可以把我们的页面想象为一个乐高机器人,需要不同零件组装,如下图,假设页面划分为tabContainer,listContainer和imgsContainer三个模块。最终把这些模块add到最终的pageModel里面,最终使用rock方法让页面启动起来。

图片 4
(原过程线示例图)

图片 5
(页面结构化示例图)

下面是伪代码的实现

JavaScript

require([ 'Tmpl!../tmpl/list.html','Tmpl!../tmpl/imgs.html','lib/qqapi','module/refresh','module/page' ], function(listTmpl, imgsTmpl, QQapi, Refresh, Page ){ var tabContainer = new RenderModel({ renderContainer: '#tabWrap', data: {}, renderTmpl: "<li soda-repeat='item in data.tabs'>{{item}}</li>", event: function(){ // tab's event } }); var listContainer = new ScrollModel({ scrollEl: $.os.ios ? $('#Page') : window, renderContainer: '#listWrap', renderTmpl: listTmpl, cgiName: '/cgi-bin/index-list?num=1', processData: function(data) { //... }, event: function(){ // listElement's event }, error: function(data) { Page.show('数据返回异常[' data.retcode ']'); } }); var imgsContainer = new renderModel({ renderContainer: '#imgsWrap', renderTmpl: listTmpl, cgiName: '/cgi-bin/getPics', processData: function(data) { //... }, event: function(){ // imgsElement's event }, complete: function(data) { QQapi.report(); } }); var page = new PageModel(); page.add([tabContainer,listContainer,imgsContainer]); page.rock(); });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
require([
    'Tmpl!../tmpl/list.html','Tmpl!../tmpl/imgs.html','lib/qqapi','module/refresh','module/page'
], function(listTmpl, imgsTmpl, QQapi, Refresh, Page ){
 
    var tabContainer = new RenderModel({
        renderContainer: '#tabWrap',
        data: {},
        renderTmpl: "<li soda-repeat='item in data.tabs'>{{item}}</li>",
        event: function(){
            // tab's event
        }
    });
 
    var listContainer = new ScrollModel({
        scrollEl: $.os.ios ? $('#Page') : window,
        renderContainer: '#listWrap',
        renderTmpl: listTmpl,
        cgiName: '/cgi-bin/index-list?num=1',
        processData: function(data) {
            //...
        },
        event: function(){
            // listElement's event
        },
        error: function(data) {
            Page.show('数据返回异常[' data.retcode ']');
        }
    });
 
    var imgsContainer = new renderModel({
        renderContainer: '#imgsWrap',
        renderTmpl: listTmpl,
        cgiName: '/cgi-bin/getPics',
        processData: function(data) {
            //...
        },
        event: function(){
            // imgsElement's event
        },
        complete: function(data) {
           QQapi.report();
        }
    });
 
    var page = new PageModel();
    page.add([tabContainer,listContainer,imgsContainer]);
    page.rock();
 
});

我们把这些常用的请求CGI,处理数据,事件绑定,上报,容错处理等一系列逻辑方法,以页面块为单位封装成一个Model模块。

这样的一个抽象层Model,我们可以清晰地看到该页面块,请求的CGI是什么,绑定了什么事件,做了什么上报,出错怎么处理。新增的代码就应该放置在相应的模块上相应的状态方法(preload,process,event,complete…),杜绝了以往的无规则乱增代码的行文。并且,根据不同业务逻辑封装不同类型的Model,如列表滚动的ScrollModel,滑块功能的SliderModel等等,可以进行高度封装,集中优化。

现在基于Model的页面结构开发,已经带有一点”组件化“的味道。每个Model都带有各自的数据,模板,逻辑。已经算是一个完整的功能单元。但距离真正的WebComponent还是有一段距离,至少满足不了我的”理想目录结构“。

 WebComponents 标准

我们回顾一下使用一个datapicker的jquery的插件,所需要的步奏:

  1. 引入插件js

  2. 引入插件所需的css(如果有)

  3. copy 组件的所需的html片段

  4. 添加代码触发组件启动

现阶段的“组件”基本上只能达到是某个功能单元上的集合。他的资源都是松散地分散在三种资源文件中,而且组件作用域暴露在全局作用域下,缺乏内聚性很容易就会跟其他组件产生冲突,如最简单的css命名冲突。对于这种“组件”,还不如上面的页面结构模块化。

于是W3C按耐不住了,制定一个WebComponents标准,为组件化的未来指引了明路。

下面以较为简洁的方式介绍这份标准,力求大家能够快速了解实现组件化的内容。(对这部分了解的同学,可以跳过这一小节)

1. <template>模板能力

模板这东西大家最熟悉不过了,前些年见的较多的模板性能大战artTemplate,juicer,tmpl,underscoretemplate等等。而现在又有mustachejs无逻辑模板引擎等新入选手。可是大家有没有想过,这么基础的能力,原生HTML5是不支持的(T_T)。

而今天WebComponent将要提供原生的模板能力

XHTML

<template id="datapcikerTmpl"> <div>我是原生的模板</div> </template>

1
2
3
<template id="datapcikerTmpl">
<div>我是原生的模板</div>
</template>

template标签内定义了myTmpl的模板,需要使用的时候就要innerHTML= document.querySelector('#myTmpl').content;可以看出这个原生的模板够原始,模板占位符等功能都没有,对于动态数据渲染模板能力只能自力更新。

2. ShadowDom 封装组件独立的内部结构

ShadowDom可以理解为一份有独立作用域的html片段。这些html片段的CSS环境和主文档隔离的,各自保持内部的独立性。也正是ShadowDom的独立特性,使得组件化成为了可能。

JavaScript

var wrap = document.querySelector('#wrap'); var shadow = wrap.createShadowRoot(); shadow.innerHTML = '<p>you can not see me </p>'

1
2
3
var wrap = document.querySelector('#wrap');
var shadow = wrap.createShadowRoot();
shadow.innerHTML = '<p>you can not see me </p>'

在具体dom节点上使用createShadowRoot方法即可生成其ShadowDom。就像在整份Html的屋子里面,新建了一个shadow的房间。房间外的人都不知道房间内有什么,保持shadowDom的独立性。

3. 自定义原生标签

初次接触Angularjs的directive指令功能,设定好组件的逻辑后,一个<Datepicker />就能引入整个组件。如此狂炫酷炸碉堡天的功能,实在令人拍手称快,跃地三尺。

JavaScript

var tmpl = document.querySelector('#datapickerTmpl'); var datapickerProto = Object.create(HTMLElement.prototype); // 设置把我们模板内容我们的shadowDom datapickerProto.createdCallback = function() { var root = this.createShadowRoot(); root.appendChild(document.importNode(tmpl.content, true)); }; var datapicker = docuemnt.registerElement('datapicker',{ prototype: datapickerProto });

1
2
3
4
5
6
7
8
9
10
11
12
var tmpl = document.querySelector('#datapickerTmpl');
var datapickerProto = Object.create(HTMLElement.prototype);
 
// 设置把我们模板内容我们的shadowDom
datapickerProto.createdCallback = function() {
    var root = this.createShadowRoot();
    root.appendChild(document.importNode(tmpl.content, true));
};
 
var datapicker = docuemnt.registerElement('datapicker',{
    prototype: datapickerProto
});

Object.create方式继承HTMLElement.prototype,得到一个新的prototype。当解析器发现我们在文档中标记它将检查是否一个名为createdCallback的方法。如果找到这个方法它将立即运行它,所以我们把克隆模板的内容来创建的ShadowDom。

最后,registerElement的方法传递我们的prototype来注册自定义标签。

上面的代码开始略显复杂了,把前面两个能力“模板”“shadowDom”结合,形成组件的内部逻辑。最后通过registerElement的方式注册组件。之后可以愉快地<datapicker></datapicker>的使用。

4. imports解决组件间的依赖

XHTML

<link rel="import" href="datapciker.html">

1
<link rel="import" href="datapciker.html">

这个类php最常用的html导入功能,HTML原生也能支持了。

WebComponents标准内容大概到这里,是的,我这里没有什么Demo,也没有实践经验分享。由于webComponents新特性,基本上除了高版本的Chrome支持外,其他浏览器的支持度甚少。虽然有polymer帮忙推动webcompoents的库存在,但是polymer自身的要求版本也是非常高(IE10 )。所以今天的主角并不是他。

我们简单来回顾一下WebCompoents的四部分功能:

1 .<template>定义组件的HTML模板能力

  1. Shadow Dom封装组件的内部结构,并且保持其独立性

  2. Custom Element 对外提供组件的标签,实现自定义标签

  3. import解决组件结合和依赖加载

 组件化实践方案

官方的标准看完了,我们思考一下。一份真正成熟可靠的组件化方案,需要具备的能力。

“资源高内聚”—— 组件资源内部高内聚,组件资源由自身加载控制

“作用域独立”—— 内部结构密封,不与全局或其他组件产生影响

“自定义标签”—— 定义组件的使用方式

“可相互组合”—— 组件正在强大的地方,组件间组装整合

“接口规范化”—— 组件接口有统一规范,或者是生命周期的管理

个人认为,模板能力是基础能力,跟是否组件化没有强联系,所以没有提出一个大点。

既然是实践,现阶段WebComponent的支持度还不成熟,不能作为方案的手段。而另外一套以高性能虚拟Dom为切入点的组件框架React,在facebook的造势下,社区得到了大力发展。另外一名主角Webpack,负责解决组件资源内聚,同时跟React极度切合形成互补。

所以【Webpack】 【React】将会是这套方案的核心技术。

不知道你现在是“又是react webpack”感到失望图片 6,还是“太好了是react webpack”不用再学一次新框架的高兴图片 7。无论如何下面的内容不会让你失望的。

一,组件生命周期

图片 8

React天生就是强制性组件化的,所以可以从根本性上解决面向过程代码所带来的麻烦。React组件自身有生命周期方法,能够满足“接口规范化”能力点。并且跟“页面结构模块化”的所封装抽离的几个方法能一一对应。另外react的jsx自带模板功能,把html页面片直接写在render方法内,组件内聚性更加紧密。

由于React编写的JSX是会先生成虚拟Dom的,需要时机才真正插入到Dom树。使用React必须要清楚组件的生命周期,其生命周期三个状态:

Mount: 插入Dom

Update: 更新Dom

Unmount: 拔出Dom

mount这单词翻译增加,嵌入等。我倒是建议“插入”更好理解。插入!拔出!插入!拔出!默念三次,懂了没?别少看黄段子的力量,

图片 9

组件状态就是: 插入-> 更新 ->拔出。

然后每个组件状态会有两种处理函数,一前一后,will函数和did函数。

componentWillMount()  准备插入前

componentDidlMount()  插入后

componentWillUpdate() 准备更新前

componentDidUpdate()  更新后

componentWillUnmount() 准备拔出前

因为拔出后基本都是贤者形态(我说的是组件),所以没有DidUnmount这个方法。

另外React另外一个核心:数据模型props和state,对应着也有自个状态方法

getInitialState()     获取初始化state。

getDefaultProps() 获取默认props。对于那些没有父组件传递的props,通过该方法设置默认的props

componentWillReceiveProps()  已插入的组件收到新的props时调用

还有一个特殊状态的处理函数,用于优化处理

shouldComponentUpdate():判断组件是否需要update调用

加上最重要的render方法,React自身带的方法刚刚好10个。对于初学者来说是比较难以消化。但其实getInitialStatecomponentDidMountrender三个状态方法都能完成大部分组件,不必望而却步。

回到组件化的主题。

一个页面结构模块化的组件,能独立封装整个组件的过程线

图片 10

我们换算成React生命周期方法:

图片 11

 

组件的状态方法流中,有两点需要特殊说明:

1,二次渲染:

由于React的虚拟Dom特性,组件的render函数不需自己触发,根据props和state的改变自个通过差异算法,得出最优的渲染。

请求CGI一般都是异步,所以必定带来二次渲染。只是空数据渲染的时候,有可能会被React优化掉。当数据回来,通过setState,触发二次render

 

2,componentWiillMount与componentDidMount的差别

和大多数React的教程文章不一样,ajax请求我建议在WillMount的方法内执行,而不是组件初始化成功之后的DidMount。这样能在“空数据渲染”阶段之前请求数据,尽早地减少二次渲染的时间。

willMount只会执行一次,非常适合做init的事情。

didMount也只会执行一次,并且这时候真实的Dom已经形成,非常适合事件绑定和complete类的逻辑。

 

 二,JSX很丑,但是组件内聚的关键!

WebComponents的标准之一,需要模板能力。本是以为是我们熟悉的模板能力,但React中的JSX这样的怪胎还是令人议论纷纷。React还没有火起来的时候,大家就已经在微博上狠狠地吐槽了“JSX写的代码这TM的丑”。这其实只是Demo阶段JSX,等到实战的大型项目中的JSX,包含多状态多数据多事件的时候,你会发现………….JSX写的代码还是很丑。

图片 12
(即使用sublime-babel等插件高亮,逻辑和渲染耦合一起,阅读性还是略差)

为什么我们会觉得丑?因为我们早已经对“视图-样式-逻辑”分离的做法潜移默化。

基于维护性和可读性,甚至性能,我们都不建议直接在Dom上面绑定事件或者直接写style属性。我们会在JS写事件代理,在CSS上写上classname,html上的就是清晰的Dom结构。我们很好地维护着MVC的设计模式,一切安好。直到JSX把他们都糅合在一起,所守护的技术栈受到侵略,难免有所抵制。

 

但是从组件化的目的来看,这种高内聚的做法未尝不可。

下面的代码,之前的“逻辑视图分离”模式,我们需要去找相应的js文件,相应的event函数体内,找到td-info的class所绑定的事件。

对比起JSX的高度内聚,所有事件逻辑就是在本身jsx文件内,绑定的就是自身的showInfo方法。组件化的特性能立马体现出来。

(注意:虽然写法上我们好像是HTML的内联事件处理器,但是在React底层并没有实际赋值类似onClick属性,内层还是使用类似事件代理的方式,高效地维护着事件处理器)

再来看一段style的jsx。其实jsx没有对样式有硬性规定,我们完全可遵循之前的定义class的逻辑。任何一段样式都应该用class来定义。在jsx你也完全可以这样做。但是出于组件的独立性,我建议一些只有“一次性”的样式直接使用style赋值更好。减少冗余的class。

XHTML

<div className="list" style={{background: "#ddd"}}> {list_html} </div>

1
2
3
<div className="list" style={{background: "#ddd"}}>
   {list_html}
</div>

或许JSX内部有负责繁琐的逻辑样式,可JSX的自定义标签能力,组件的黑盒性立马能体验出来,是不是瞬间美好了很多。

JavaScript

render: function(){ return ( <div> <Menus bannerNums={this.state.list.length}></Menus> <TableList data={this.state.list}></TableList> </div> ); }

1
2
3
4
5
6
7
8
render: function(){
    return (
      <div>
         <Menus bannerNums={this.state.list.length}></Menus>
         <TableList data={this.state.list}></TableList>
      </div>
   );
}

虽然JSX本质上是为了虚拟Dom而准备的,但这种逻辑和视图高度合一对于组件化未尝不是一件好事。

 

学习完React这个组件化框架后,看看组件化能力点的完成情况

“资源高内聚”—— (33%)  html与js内聚

“作用域独立”—— (50%)  js的作用域独立

“自定义标签”—— (100%)jsx

“可相互组合”—— (50%)  可组合,但缺乏有效的加载方式

“接口规范化”—— (100%)组件生命周期方法

 

Webpack 资源组件化

对于组件化的资源独立性,一般的模块加载工具和构建流程视乎变得吃力。组件化的构建工程化,不再是之前我们常见的,css合二,js合三,而是体验在组件间的依赖于加载关系。webpack正好符合需求点,一方面填补组件化能力点,另一方帮助我们完善组件化的整体构建环境。

首先要申明一点是,webpack是一个模块加载打包工具,用于管理你的模块资源依赖打包问题。这跟我们熟悉的requirejs模块加载工具,和grunt/gulp构建工具的概念,多多少少有些出入又有些雷同。

图片 13

首先webpak对于CommonJS与AMD同时支持,满足我们模块/组件的加载方式。

JavaScript

require("module"); require("../file.js"); exports.doStuff = function() {}; module.exports = someValue;

1
2
3
4
require("module");
require("../file.js");
exports.doStuff = function() {};
module.exports = someValue;

JavaScript

define("mymodule", ["dep1", "dep2"], function(d1, d2) { return someExportedValue; });

1
2
3
define("mymodule", ["dep1", "dep2"], function(d1, d2) {
    return someExportedValue;
});

当然最强大的,最突出的,当然是模块打包功能。这正是这一功能,补充了组件化资源依赖,以及整体工程化的能力

根据webpack的设计理念,所有资源都是“模块”,webpack内部实现了一套资源加载机制,可以把想css,图片等资源等有依赖关系的“模块”加载。这跟我们使用requirejs这种仅仅处理js大大不同。而这套加载机制,通过一个个loader来实现。

 

JavaScript

// webpack.config.js module.exports = { entry: { entry: './index.jsx', }, output: { path: __dirname, filename: '[name].min.js' }, module: { loaders: [ {test: /.css$/, loader: 'style!css' }, {test: /.(jsx|js)?$/, loader: 'jsx?harmony', exclude: /node_modules/}, {test: /.(png|jpg|jpeg)$/, loader: 'url-loader?limit=10240'} ] } };

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// webpack.config.js
module.exports = {
    entry: {
     entry: './index.jsx',
    },
    output: {
        path: __dirname,
        filename: '[name].min.js'
    },
    module: {
        loaders: [
            {test: /.css$/, loader: 'style!css' },
            {test: /.(jsx|js)?$/, loader: 'jsx?harmony', exclude: /node_modules/},
            {test: /.(png|jpg|jpeg)$/, loader: 'url-loader?limit=10240'}
        ]
    }
};

上面一份简单的webpack配置文件,留意loaders的配置,数组内一个object配置为一种模块资源的加载机制。test的正则为匹配文件规则,loader的为匹配到文件将由什么加载器处理,多个处理器之间用分隔,处理顺序从右到左。

 

style!css,css文件通过css-loader(处理css),再到style-loader(inline到html)的加工处理流。

jsx文件通过jsx-loader编译,‘?’开启加载参数,harmony支持ES6的语法。

图片资源通过url-loader加载器,配置参数limit,控制少于10KB的图片将会base64化。

 资源文件如何被require?

JavaScript

// 加载组件自身css require('./slider.css'); // 加载组件依赖的模块 var Clip = require('./clipitem.js'); // 加载图片资源 var spinnerImg = require('./loading.png');

1
2
3
4
5
6
// 加载组件自身css
require('./slider.css');
// 加载组件依赖的模块
var Clip = require('./clipitem.js');
// 加载图片资源
var spinnerImg = require('./loading.png');

在webpack的js文件中我们除了require我们正常的js文件,css和png等静态文件也可以被require进来。我们通过webpack命令,编译之后,看看输出结果如何:

JavaScript

webpackJsonp([0], { /* 0 */ /***/ function(module, exports, __webpack_require__) { // 加载组件自身css __webpack_require__(1); // 加载组件依赖的模块 var Clip = __webpack_require__(5); // 加载图片资源 var spinnerImg = __webpack_require__(6); /***/ }, /* 1 */ /***/ function(module, exports, __webpack_require__) { /***/ }, /* 2 */ /***/ function(module, exports, __webpack_require__) { exports = module.exports = __webpack_require__(3)(); exports.push([module.id, ".slider-wrap{rn position: relative;rn width: 100%;rn margin: 50px;rn background: #fff;rn}rnrn.slider-wrap li{rn text-align: center;rn line-height: 20px;rn}", ""]); /***/ }, /* 3 */ /***/ function(module, exports) { /***/ }, /* 4 */ /***/ function(module, exports, __webpack_require__) { /***/ }, /* 5 */ /***/ function(module, exports) { console.log('hello, here is clipitem.js') ; /***/ }, /* 6 */ /***/ function(module, exports) { module.exports = "data:image/png;base64,iVBORw0KGg......" /***/ } ]);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
webpackJsonp([0], {
/* 0 */
/***/ function(module, exports, __webpack_require__) {
          // 加载组件自身css
          __webpack_require__(1);
          // 加载组件依赖的模块
          var Clip = __webpack_require__(5);
          // 加载图片资源
          var spinnerImg = __webpack_require__(6);
/***/ },
/* 1 */
/***/ function(module, exports, __webpack_require__) {
 
/***/ },
/* 2 */
/***/ function(module, exports, __webpack_require__) {
          exports = module.exports = __webpack_require__(3)();
          exports.push([module.id, ".slider-wrap{rn position: relative;rn width: 100%;rn margin: 50px;rn background: #fff;rn}rnrn.slider-wrap li{rn text-align: center;rn line-height: 20px;rn}", ""]);
 
/***/ },
/* 3 */
/***/ function(module, exports) {
 
/***/ },
 
/* 4 */
/***/ function(module, exports, __webpack_require__) {
/***/ },
 
/* 5 */
/***/ function(module, exports) {
          console.log('hello, here is clipitem.js') ;
/***/ },
/* 6 */
/***/ function(module, exports) {
          module.exports = "data:image/png;base64,iVBORw0KGg......"
/***/ }
]);

webpack编译之后,输出文件视乎乱糟糟的,但其实每一个资源都被封装在一个函数体内,并且以编号的形式标记(注释)。这些模块,由webpack的__webpack_require__内部方法加载。入口文件为编号0的函数index.js,可以看到__webpack_require__加载其他编号的模块。

css文件在编号1,由于使用css-loader和style-loader,编号1-4都是处理css。其中编号2我们可以看我们的css的string体。最终会以内联的方式插入到html中。

图片文件在编号6,可以看出exports出base64化的图片。

 组件一体输出

JavaScript

// 加载组件自身css require('./slider.css'); // 加载组件依赖的模块 var React = require('react'); var Clip = require('../ui/clipitem.jsx'); // 加载图片资源 var spinnerImg = require('./loading.png'); var Slider = React.createClass({ getInitialState: function() { // ... }, componentDidMount: function(){ // ... }, render: function() { return ( <div> <Clip data={this.props.imgs} /> <img className="loading" src={spinnerImg} /> </div> ); } }); module.exports = Slider;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 加载组件自身css
require('./slider.css');
// 加载组件依赖的模块
var React = require('react');
var Clip = require('../ui/clipitem.jsx');
// 加载图片资源
var spinnerImg = require('./loading.png');
var Slider = React.createClass({
    getInitialState: function() {
        // ...
    },
    componentDidMount: function(){
        // ...
    },
    render: function() {
        return (
            <div>
               <Clip data={this.props.imgs} />
               <img className="loading" src={spinnerImg} />
            </div>
        );
    }
});
module.exports = Slider;

如果说,react使到html和js合为一体。

那么加上webpack,两者结合一起的话。js,css,png(base64),html 所有web资源都能合成一个JS文件。这正是这套方案的核心所在:组件独立一体化。如果要引用一个组件,仅仅require('./slider.js') 即可完成。

 

加入webpack的模块加载器之后,我们组件的加载问题,内聚问题也都成功地解决掉

“资源高内聚”—— (100%) 所有资源可以一js输出

“可相互组合”—— (100%)  可组合可依赖加载

 

 CSS模块化实践

很高兴,你能阅读到这里。目前我们的组件完成度非常的高,资源内聚,易于组合,作用域独立互不污染。。。。等等图片 14,视乎CSS模块的完成度有欠缺。

那么目前组件完成度来看,CSS作用域其实是全局性的,并非组件内部独立。下一步,我们要做得就是如何让我们组件内部的CSS作用域独立。

这时可能有人立马跳出,大喊一句“德玛西亚!”,哦不,应该是“用sass啊傻逼!”。可是项目组件化之后,组件的内部封装已经很好了,其内部dom结构和css趋向简单,独立,甚至是破碎的。LESS和SASS的一体式样式框架的设计,他的嵌套,变量,include,函数等丰富的功能对于整体大型项目的样式管理非常有效。但对于一个功能单一组件内部样式,视乎就变的有点格格不入。“不能为了框架而框架,合适才是最好的”。视乎原生的css能力已经满足组件的样式需求,唯独就是上面的css作用域问题。

 

这里我给出思考的方案: classname随便写,保持原生的方式。编译阶段,根据组件在项目路径的唯一性,由【组件classname 组件唯一路径】打成md5,生成全局唯一性classname。正当我要写一个loader实现我的想法的时候,发现歪果仁已经早在先走一步了。。。。

这里具体方案参考我之前博客的译文:

之前我们讨论过JS的模块。现在通过Webpack被加载的CSS资源叫做“CSS模块”?我觉得还是有问题的。现在style-loader插件的实现本质上只是创建link[rel=stylesheet]元素插入到document中。这种行为和通常引入JS模块非常不同。引入另一个JS模块是调用它所提供的接口,但引入一个CSS却并不“调用”CSS。所以引入CSS本身对于JS程序来说并不存在“模块化”意义,纯粹只是表达了一种资源依赖——即该组件所要完成的功能还需要某些asset。

因此,那位歪果仁还扩展了“CSS模块化”的概念,除了上面的我们需要局部作用域外,还有很多功能,这里不详述。具体参考原文 

非常赞的一点,就是cssmodules已经被css-loader收纳。所以我们不需要依赖额外的loader,基本的css-loader开启参数modules即可

JavaScript

//webpack.config.js ... module: { loaders: [ {test: /.css$/, loader: 'style!css?modules&localIdentName=[local]__[name]_[hash:base64:5]' }, ] } ....

1
2
3
4
5
6
7
8
//webpack.config.js
...  
    module: {
        loaders: [
            {test: /.css$/, loader: 'style!css?modules&localIdentName=[local]__[name]_[hash:base64:5]' },
        ]  
    }
....

modules参数代表开启css-modules功能,loaclIdentName为设置我们编译后的css名字,为了方便debug,我们把classname(local)和组件名字(name)输出。当然可以在最后输出的版本为了节省提交,仅仅使用hash值即可。另外在react中的用法大概如下。

JavaScript

var styles = require('./banner.css'); var Banner = new React.createClass({ ... render: function(){ return ( <div> <div className={styles.classA}></div> </div> ) } });

1
2
3
4
5
6
7
8
9
10
11
var styles = require('./banner.css');
var Banner = new React.createClass({
    ...
    render: function(){
        return (
            <div>
                <div className={styles.classA}></div>
            </div>
        )
    }
});

最后这里关于出于对CSS一些思考,

关于css-modules的其它功能,我并不打算使用。在内部分享【我们竭尽所能地让CSS变得复杂】中提及:

我们项目中大部分的CSS都不会像boostrap那样需要变量来设置,身为一线开发者的我们大概能够感受到:设计师们改版UI,绝对不是简单的换个色或改个间距,而是面目全非的全新UI,这绝对不是一个变量所能解决的”维护性“。

反而项目实战过程中,真正要解决的是:在版本迭代过程中那些淘汰掉的过期CSS,大量地堆积在项目当中。我们像极了家中的欧巴酱不舍得丢掉没用的东西,因为这可是我们使用sass或less编写出具有高度的可维护性的,肯定有复用的一天。

这些堆积的过期CSS(or sass)之间又有部分依赖,一部分过期没用了,一部分又被新的样式复用了,导致没人敢动那些历史样式。结果现网项目迭代还带着大量两年前没用的样式文件。

组件化之后,css的格局同样被革新了。可能postcss才是你现在手上最适合的工具,而不在是sass。

 

到这里,我们终于把组件化最后一个问题也解决了。

“作用域独立”—— (100%) 如同shadowDom作用域独立

 

到这里,我们可以开一瓶82年的雪碧,好好庆祝一下。不是吗?

图片 15

 

 组件化之路还在继续

webpack和react还有很多新非常重要的特性和功能,介于本文仅仅围绕着组件化的为核心,没有一一阐述。另外,配搭gulp/grunt补充webpack构建能力,webpack的codeSplitting,react的组件通信问题,开发与生产环境配置等等,都是整套大型项目方案的所必须的,限于篇幅问题。可以等等我更新下篇,或大家可以自行查阅。

但是,不得不再安利一下react-hotloader神器。热加载的开发模式绝对是下一代前端开发必备。严格说,如果没有了热加载,我会很果断地放弃这套方案,即使这套方案再怎么优秀,我都讨厌react需要5~6s的编译时间。但是hotloader可以在我不刷新页面的情况下,动态修改代码,而且不单单是样式,连逻辑也是即时生效。

图片 16

如上在form表单内。使用热加载,表单不需要重新填写,修改submit的逻辑立刻生效。这样的开发效率真不是提高仅仅一个档次。必须安利一下。

 

或许你发现,使用组件化方案之后,整个技术栈都被更新了一番。学习成本也不少,并且可以预知到,基于组件化的前端还会很多不足的问题,例如性能优化方案需要重新思考,甚至最基本的组件可复用性不一定高。后面很长一段时间,需要我们不断磨练与优化,探求最优的前端组件化之道。

至少我们可以想象,不再担心自己写的代码跟某个谁谁冲突,不再为找某段逻辑在多个文件和方法间穿梭,不再copy一片片逻辑然后改改。我们每次编写都是可重用,可组合,独立且内聚的组件。而每个页面将会由一个个嵌套组合的组件,相互独立却相互作用。

 

对于这样的前端未来,有所期待,不是很好吗

至此,感谢你的阅读。

1 赞 6 收藏 1 评论

图片 17

使用 Service Worker 做一个 PWA 离线网页应用

2017/10/09 · JavaScript · PWA, Service Worker

原文出处: 人人网FED博客   

在上一篇《我是怎样让网站用上HTML5 Manifest》介绍了怎么用Manifest做一个离线网页应用,结果被广大网友吐槽说这个东西已经被deprecated,移出web标准了,现在被Service Worker替代了,不管怎么样,Manifest的一些思想还是可以借用的。笔者又将网站升级到了Service Worker,如果是用Chrome等浏览器就用Service Worker做离线缓存,如果是Safari浏览器就还是用Manifest,读者可以打开这个网站感受一下,断网也是能正常打开。

SVG画布

canvas是绘制SVG内容的一块空间或区域。理论上,画布在所有维度上都是无限的。所以SVG可以是任意尺寸。然而,SVG通过有限区域展现在屏幕上,这个区域叫做viewport。SVG中超出视窗边界的区域会被裁切并且隐藏。

1. 什么是Service Worker

Service Worker是谷歌发起的实现PWA(Progressive Web App)的一个关键角色,PWA是为了解决传统Web APP的缺点:

(1)没有桌面入口

(2)无法离线使用

(3)没有Push推送

那Service Worker的具体表现是怎么样的呢?如下图所示:

图片 18

Service Worker是在后台启动的一条服务Worker线程,上图我开了两个标签页,所以显示了两个Client,但是不管开多少个页面都只有一个Worker在负责管理。这个Worker的工作是把一些资源缓存起来,然后拦截页面的请求,先看下缓存库里有没有,如果有的话就从缓存里取,响应200,反之没有的话就走正常的请求。具体来说,Service Worker结合Web App Manifest能完成以下工作(这也是PWA的检测标准):

图片 19

包括能够离线使用、断网时返回200、能提示用户把网站添加一个图标到桌面上等。

视窗

视窗是一块SVG可见的区域。你可以把视窗当做一个窗户,透过这个窗户可以看到特定的景象,景象也许完整,也许只有一部分。

SVG的视窗类似访问当前页面的浏览器视窗。网页可以是任何尺寸;它可以大于视窗宽度,并且在大多数情况下都比视窗高度要高。然而,每个时刻只有一部分网页内容是透过视窗可见的。

整个SVG画布可见还是部分可见取决于这个canvas的尺寸以及preserveAspectRatio属性值。你现在不需要担心这些;我们之后会讨论更多的细节。

你可以在最外层<svg>元素上使用widthheight属性声明视窗尺寸。

<!-- the viewport will be 800px by 600px --> <svg width="800" height="600"> <!-- SVG content drawn onto the SVG canvas --> </svg>

1
2
3
4
<!-- the viewport will be 800px by 600px -->
<svg width="800" height="600">
    <!-- SVG content drawn onto the SVG canvas -->
</svg>

在SVG中,值可以带单位也不可以不带。一个不带单位的值可以在用户空间中通过用户单位声明。如果值通过用户单位声明,那么这个值的数值被认为和px单位的数值一样。这意味着上述例子将被渲染为800px*600px的视窗。

你也可以使用单位来声明值。SVG支持的长度单位有:emexpxptpccmmmin和百分比。

一旦你设定最外层SVG元素的宽高,浏览器会建立初始视窗坐标系和初始用户坐标系。

2. Service Worker的支持情况

Service Worker目前只有Chrome/Firfox/Opera支持:

图片 20

Safari和Edge也在准备支持Service Worker,由于Service Worker是谷歌主导的一项标准,对于生态比较封闭的Safari来说也是迫于形势开始准备支持了,在Safari TP版本,可以看到:

图片 21

在实验功能(Experimental Features)里已经有Service Worker的菜单项了,只是即使打开也是不能用,会提示你还没有实现:

图片 22

但不管如何,至少说明Safari已经准备支持Service Worker了。另外还可以看到在今年2017年9月发布的Safari 11.0.1版本已经支持WebRTC了,所以Safari还是一个上进的孩子。

Edge也准备支持,所以Service Worker的前景十分光明。

初始坐标系

初始视窗坐标系是一个建立在视窗上的坐标系。原点(0,0)在视窗的左上角,X轴正向指向右,Y轴正向指向下,初始坐标系中的一个单位等于视窗中的一个”像素”。这个坐标系统类似于通过CSS盒模型在HTML元素上建立的坐标系。

初始用户坐标系是建立在SVG画布上的坐标系。这个坐标系一开始和视窗坐标系完全一样-它自己的原点位于视窗左上角,x轴正向指向右,y轴正向指向下。使用viewBox属性,初始用户坐标系统-也称当前坐标系,或使用中的用户空间-可以变成与视窗坐标系不一样的坐标系。我们在一下节中讨论如何改变坐标系。

到现在为止,我们还没有声明viewBox属性值。SVG画布的用户坐标系统和视窗坐标系统完全一样。

下图中,视窗坐标系的”标尺”是灰色的,用户坐标系(viewBox)的是蓝色的。由于它们在这个时候完全相同,所以两个坐标系统重合了。图片 23

上面SVG中的鹦鹉的外框边界是200个单位(这个例子中是200个像素)宽和300个单位高。鹦鹉基于初始坐标系在画布中绘制。

新用户空间(即,新当前坐标系)也可以通过在容器元素或图形元素上使用transform属性来声明变换。我们将在这篇文章的第二部分讨论关于变换的内容,更多细节在第三部分和最后部分中讨论。

3. 使用Service Worker

Service Worker的使用套路是先注册一个Worker,然后后台就会启动一条线程,可以在这条线程启动的时候去加载一些资源缓存起来,然后监听fetch事件,在这个事件里拦截页面的请求,先看下缓存里有没有,如果有直接返回,否则正常加载。或者是一开始不缓存,每个资源请求后再拷贝一份缓存起来,然后下一次请求的时候缓存里就有了。

viewBox

我喜欢把viewBox理解为“真实”坐标系。首先,它是用来把SVG图形绘制到画布上的坐标系。这个坐标系可以大于视窗也可以小于视窗,在视窗中可以整体可见或部分可见。

在之前的章节里,这个坐标系-用户坐标系-和视窗坐标系完全一样。因为我们没有把它声明成其他坐标系。这就是为什么所有的定位和绘制看起来是基于视窗坐标系的。因为我们一旦创建视窗坐标系(使用widthheight),浏览器默认创建一个完全相同的用户坐标系。

你可以使用viewBox属性声明自己的用户坐标系。如果你选择的用户坐标系统和视窗坐标系统宽高比(高比宽)相同,它会延伸来适应整个视窗区域(一分钟内我们就来讲个例子)。然而,如果你的用户坐标系宽高比不同,你可以用preserveAspectRatio属性来声明整个系统在视窗内是否可见,你也可以用它来声明在视窗中如何定位。我们会在下个章节里讨论这一情况的细节和例子。在这一章里,我们只讨论viewBox的宽高比符合视窗的情况-在这些例子中,preserveAspectRatio不产生影响。

在我们讨论这些例子前,我们回顾一下viewBox的语法。

(1)注册一个Service Worker

Service Worker对象是在window.navigator里面,如下代码:

JavaScript

window.addEventListener("load", function() { console.log("Will the service worker register?"); navigator.serviceWorker.register('/sw-3.js') .then(function(reg){ console.log("Yes, it did."); }).catch(function(err) { console.log("No it didn't. This happened: ", err) }); });

1
2
3
4
5
6
7
8
9
window.addEventListener("load", function() {
    console.log("Will the service worker register?");
    navigator.serviceWorker.register('/sw-3.js')
    .then(function(reg){
        console.log("Yes, it did.");
    }).catch(function(err) {
        console.log("No it didn't. This happened: ", err)
    });
});

在页面load完之后注册,注册的时候传一个js文件给它,这个js文件就是Service Worker的运行环境,如果不能成功注册的话就会抛异常,如Safari TP虽然有这个对象,但是会抛异常无法使用,就可以在catch里面处理。这里有个问题是为什么需要在load事件启动呢?因为你要额外启动一个线程,启动之后你可能还会让它去加载资源,这些都是需要占用CPU和带宽的,我们应该保证页面能正常加载完,然后再启动我们的后台线程,不能与正常的页面加载产生竞争,这个在低端移动设备意义比较大。

还有一点需要注意的是Service Worker和Cookie一样是有Path路径的概念的,如果你设定一个cookie假设叫time的path=/page/A,在/page/B这个页面是不能够获取到这个cookie的,如果设置cookie的path为根目录/,则所有页面都能获取到。类似地,如果注册的时候使用的js路径为/page/sw.js,那么这个Service Worker只能管理/page路径下的页面和资源,而不能够处理/api路径下的,所以一般把Service Worker注册到顶级目录,如上面代码的”/sw-3.js”,这样这个Service Worker就能接管页面的所有资源了。

viewBox语法

viewBox属性接收四个参数值,包括:<min-x>, <min-y>, width 和 height

CSS

viewBox = <min-x> <min-y> <width> <height>

1
viewBox = <min-x> <min-y> <width> <height>

<min-x> 和 <min-y> 值决定viewBox的左上角,widthheight决定视窗的宽高。这里要注意视窗的宽高不一定和父<svg>元素的宽高一样。<width><height>值为负数是不合法的。值为0的话会禁止元素的渲染。

注意视窗的宽度也可以在CSS中设置为任何值。例如:设置width:100%会让SVG视窗在文档中自适应。无论viewBox的值是多少,它会映射为外层SVG元素计算出的宽度值。

设置viewBox的例子如下:

<!-- The viewBox in this example is equal to the viewport, but it can be different --> <svg width="800" height="600" viewBox="0 0 800 600"> <!-- SVG content drawn onto the SVG canvas --> </svg>

1
2
3
4
<!-- The viewBox in this example is equal to the viewport, but it can be different -->
<svg width="800" height="600" viewBox="0 0 800 600">
    <!-- SVG content drawn onto the SVG canvas -->
</svg>

如果你之前在其他地方看到过viewBox,你也许会看到一些解释说你可以用viewBox属性通过缩放或者变化使SVG图形变换。这是真的。我将深入探究并且告诉你甚至可以使用viewBox来切割SVG图形。

理解viewBox和视窗之间差异最好的方法是亲身观察。所以让我们看一些例子。我们将从viewBox和viewport的宽高比相同的例子开始,所以我们还不需要深入了解preserveAspectRatio

(2)Service Worker安装和激活

注册完之后,Service Worker就会进行安装,这个时候会触发install事件,在install事件里面可以缓存一些资源,如下sw-3.js:

JavaScript

const CACHE_NAME = "fed-cache"; this.addEventListener("install", function(event) { this.skipWaiting(); console.log("install service worker"); // 创建和打开一个缓存库 caches.open(CACHE_NAME); // 首页 let cacheResources = ["]; event.waitUntil( // 请求资源并添加到缓存里面去 caches.open(CACHE_NAME).then(cache => { cache.addAll(cacheResources); }) ); });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const CACHE_NAME = "fed-cache";
this.addEventListener("install", function(event) {
    this.skipWaiting();
    console.log("install service worker");
    // 创建和打开一个缓存库
    caches.open(CACHE_NAME);
    // 首页
    let cacheResources = ["https://fed.renren.com/?launcher=true"];
    event.waitUntil(
        // 请求资源并添加到缓存里面去
        caches.open(CACHE_NAME).then(cache => {
            cache.addAll(cacheResources);
        })
    );
});

通过上面的操作,创建和添加了一个缓存库叫fed-cache,如下Chrome控制台所示:

图片 24

Service Worker的API基本上都是返回Promise对象避免堵塞,所以要用Promise的写法。上面在安装Service Worker的时候就把首页的请求给缓存起来了。在Service Worker的运行环境里面它有一个caches的全局对象,这个是缓存的入口,还有一个常用的clients的全局对象,一个client对应一个标签页。

在Service Worker里面可以使用fetch等API,它和DOM是隔离的,没有windows/document对象,无法直接操作DOM,无法直接和页面交互,在Service Worker里面无法得知当前页面打开了、当前页面的url是什么,因为一个Service Worker管理当前打开的几个标签页,可以通过clients知道所有页面的url。还有可以通过postMessage的方式和主页面互相传递消息和数据,进而做些控制。

install完之后,就会触发Service Worker的active事件:

JavaScript

this.addEventListener("active", function(event) { console.log("service worker is active"); });

1
2
3
this.addEventListener("active", function(event) {
    console.log("service worker is active");
});

Service Worker激活之后就能够监听fetch事件了,我们希望每获取一个资源就把它缓存起来,就不用像上一篇提到的Manifest需要先生成一个列表。

你可能会问,当我刷新页面的时候不是又重新注册安装和激活了一个Service Worker?虽然又调了一次注册,但并不会重新注册,它发现”sw-3.js”这个已经注册了,就不会再注册了,进而不会触发install和active事件,因为当前Service Worker已经是active状态了。当需要更新Service Worker时,如变成”sw-4.js”,或者改变sw-3.js的文本内容,就会重新注册,新的Service Worker会先install然后进入waiting状态,等到重启浏览器时,老的Service Worker就会被替换掉,新的Service Worker进入active状态,如果不想等到重新启动浏览器可以像上面一样在install里面调skipWaiting:

JavaScript

this.skipWaiting();

1
this.skipWaiting();

与viewport宽高比相同的viewBox

我们从一个简单的例子开始。这个例子中的viewBox的尺寸是视窗尺寸的一半。在这个例子中我们不改变viewBox的原点,所以<min-x><min-y>都设置成0。viewBox的宽高是viewport宽高的一半。这意味着我们保持宽高比。

<svg width="800" height="600" viewBox="0 0 400 300"> <!-- SVG content drawn onto the SVG canvas --> </svg>

1
2
3
<svg width="800" height="600" viewBox="0 0 400 300">
    <!-- SVG content drawn onto the SVG canvas -->
</svg>

所以,viewBox="0 0 400 300"到底有什么用呢?

  • 它声明了一个特定的区域,canvas横跨左上角的点(0,0)到点(400,300)
  • SVG图像被这个区域裁切
  • 区域被拉伸(类似缩放效果)来充满整个视窗。
  • 用户坐标系被映射到视窗坐标系-在这种情况下-一个用户单位等于两个视窗单位。

下面的图片展示了在我们例子中把上面的viewBox应用到<svg> 画布中的效果。灰色单位代表视窗坐标系,蓝色坐标系代表viewBox建立的用户坐标系。

图片 25

任何在SVG画布中画的内容都会被对应到新的用户坐标系中。

我喜欢像Google地图一样通过viewBox把SVG画布形象化。在Google地图中你可以在特定区域缩放;这个区域是唯一可见的,并且在浏览器视窗中按比例增加。然而,你知道地图的剩余部分还在那里,但是不可见因为它超出视窗的边界-被裁切了。

现在让我们试着改变<min-x><min-y>的值。都设置为100。你可以设置成任何你想要的值。宽高比还是和视窗的宽高比一样。

<svg width="800" height="600" viewBox="100 100 200 150"> <!-- SVG content drawn onto the SVG canvas --> </svg>

1
2
3
<svg width="800" height="600" viewBox="100 100 200 150">
    <!-- SVG content drawn onto the SVG canvas -->
</svg>

添加viewBox="100 100 200 150"的效果和之前例子中一样都是裁切的效果。图形被裁切然后拉伸来充满整个视窗区域。

图片 26

再一次,用户坐标系被映射到视窗坐标系-200用户单位映射为800视窗单位因此每个用户单位等于四个视窗单位。结果像你看到的那样是放大的效果。

另外注意,在这个时候,为<min-x><min-y>声明非0的值对图形有变换的效果;更加特别的是,SVG 画布看起来向上拉伸100个单位,向左拉伸100个单位(transform="translate(-100 -100)")。

的确,作为规范说明,viewBox属性的影响在于用户代理自动添加适当的变换矩阵来把用户空间中具体的矩形映射到指定区域的边界(通常是视窗)”

这是一个很棒的说明我们之前已经提到的内容的方法:图形被裁切然后被缩放以适应视窗。这个说明随后增加了一个注释:“在一些情况下用户代理在缩放变换之外需要增加一个移动变换。例如,在最外层的svg元素上,如果viewBox属性对<min-x><min-y>声明非0值得那么就需要移动变换。”

为了更好演示移动变换,让我们试着给<min-x><min-y>添加-100。移动效果类似transform="translate(100 100)";这意味着图形会在切割和缩放后移动到右下方。回顾倒数第二个裁切尺寸为400*300的例子,添加新的无效<min-x><min-y>值,新的代码如下:

<svg width="800" height="600" viewBox="-100 -100 300 200"> <!-- SVG content drawn onto the SVG canvas --> </svg>

1
2
3
<svg width="800" height="600" viewBox="-100 -100 300 200">
    <!-- SVG content drawn onto the SVG canvas -->
</svg>

给图形添加上述viewBox transformation的结果如下图所示:图片 27

注意,与transform属性不同,因为viewBox自动添加的tranfomation不会影响有vewBox属性的元素的x,y,宽和高等属性。因此,在上述例子中展示的带有width,heightviewBox属性的svg元素,widthheight属性代表添加viewBox 变换之前的坐标系中的值。在上述例子中你可以看到初始(灰色)viewport坐标系甚至在<svg>上使用了viewBox属性后仍然没有影响。

另一方面,像tranform属性一样,它给所有其他属性和后代元素建立了一个新的坐标系。你还可以看到在上述例子中,用户坐标系是新建立的-它不是保持像初始用户坐标系和使用viewBox前的视窗坐标系一样。任何<svg>后代会在这个的用户坐标系中定位和确定尺寸,而不是初始坐标系。

最后一个viewBox的例子和前一个类似,但是它不是切割画布,我们将在viewport里扩展它并看它如何影响图形。我们将声明一个宽高比视窗大的viewBox,并依然保持viewport的宽高比。我们在下一章里讨论不同的宽高比。

在这个例子中,我们将viewBox的尺寸设为viewport的1.5倍。

<svg width="800" height="600" viewBox="0 0 1200 900"> <!-- SVG content drawn onto the SVG canvas --> </svg>

1
2
3
<svg width="800" height="600" viewBox="0 0 1200 900">
    <!-- SVG content drawn onto the SVG canvas -->
</svg>

现在用户坐标系会被放大到1200*900。它会被映射到视窗坐标系,用户坐标系中的每一个单位水平方向上等于视窗坐标系中的viewport-width / viewBox-width,竖直方向上等于viewport-height / viewBox-height。这意味着,在这种情况下,每一个用户坐标系中的x-units等于viewport坐标系中的0.66x-units,每个用户y-unit映射成0.66的viewport y-units。

当然,理解这些最好的方法是把结果视觉化。viewBox被缩放到适应下图所示的viewport。因为图形在画布里基于新的用户坐标系绘制的,而不是视窗坐标系,它看起来比视窗小。图片 28

到目前为止,我们所有的例子的宽高比都和视窗一致。但是如果viewBox中声明的宽高比和视窗中的不一样会发生什么呢?例如,试想我们把视窗的尺寸设为1000*500。宽高比不再和视窗的一样。在例子中使用viewBox="0 0 1000 500"的结果如下图:图片 29

用户坐标系。因此图形在视窗中定位:

  • 整个viewBox适应视窗。
  • 保持viewBox的宽高比。viewBox没有被拉伸来覆盖视窗区域。
  • viewBox在视窗中水平垂直居中。

这是默认表现。那用什么控制表现呢?如果我们想改变视窗中viewBox的位置呢?这就需要用到preserveAspectRatio属性了。

(3)fetch资源后cache起来

如下代码,监听fetch事件做些处理:

JavaScript

this.addEventListener("fetch", function(event) { event.respondWith( caches.match(event.request).then(response => { // cache hit if (response) { return response; } return util.fetchPut(event.request.clone()); }) ); });

1
2
3
4
5
6
7
8
9
10
11
12
this.addEventListener("fetch", function(event) {
    event.respondWith(
        caches.match(event.request).then(response => {
            // cache hit
            if (response) {
                return response;
            }
            return util.fetchPut(event.request.clone());
        })
    );
});

先调caches.match看一下缓存里面是否有了,如果有直接返回缓存里的response,否则的话正常请求资源并把它放到cache里面。放在缓存里资源的key值是Request对象,在match的时候,需要请求的url和header都一致才是相同的资源,可以设定第二个参数ignoreVary:

JavaScript

caches.match(event.request, {ignoreVary: true})

1
caches.match(event.request, {ignoreVary: true})

表示只要请求url相同就认为是同一个资源。

上面代码的util.fetchPut是这样实现的:

JavaScript

let util = { fetchPut: function (request, callback) { return fetch(request).then(response => { // 跨域的资源直接return if (!response || response.status !== 200 || response.type !== "basic") { return response; } util.putCache(request, response.clone()); typeof callback === "function" && callback(); return response; }); }, putCache: function (request, resource) { // 后台不要缓存,preview链接也不要缓存 if (request.method === "GET" && request.url.indexOf("wp-admin") < 0 && request.url.indexOf("preview_id") < 0) { caches.open(CACHE_NAME).then(cache => { cache.put(request, resource); }); } } };

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let util = {
    fetchPut: function (request, callback) {
        return fetch(request).then(response => {
            // 跨域的资源直接return
            if (!response || response.status !== 200 || response.type !== "basic") {
                return response;
            }
            util.putCache(request, response.clone());
            typeof callback === "function" && callback();
            return response;
        });
    },
    putCache: function (request, resource) {
        // 后台不要缓存,preview链接也不要缓存
        if (request.method === "GET" && request.url.indexOf("wp-admin") < 0
              && request.url.indexOf("preview_id") < 0) {
            caches.open(CACHE_NAME).then(cache => {
                cache.put(request, resource);
            });
        }
    }
};

需要注意的是跨域的资源不能缓存,response.status会返回0,如果跨域的资源支持CORS,那么可以把request的mod改成cors。如果请求失败了,如404或者是超时之类的,那么也直接返回response让主页面处理,否则的话说明加载成功,把这个response克隆一个放到cache里面,然后再返回response给主页面线程。注意能放缓存里的资源一般只能是GET,通过POST获取的是不能缓存的,所以要做个判断(当然你也可以手动把request对象的method改成get),还有把一些个人不希望缓存的资源也做个判断。

这样一旦用户打开过一次页面,Service Worker就安装好了,他刷新页面或者打开第二个页面的时候就能够把请求的资源一一做缓存,包括图片、CSS、JS等,只要缓存里有了不管用户在线或者离线都能够正常访问。这样我们自然会有一个问题,这个缓存空间到底有多大?上一篇我们提到Manifest也算是本地存储,PC端的Chrome是5Mb,其实这个说法在新版本的Chrome已经不准确了,在Chrome 61版本可以看到本地存储的空间和使用情况:

图片 30

其中Cache Storage是指Service Worker和Manifest占用的空间大小和,上图可以看到总的空间大小是20GB,几乎是unlimited,所以基本上不用担心缓存会不够用。

preserveAspectRatio属性

preserveAspectRatio属性强制统一缩放比来保持图形的宽高比。

如果你用不同于视窗的宽高比定义用户坐标系,如果像我们在之前的例子中看到的那样浏览器拉伸viewBox来适应视窗,宽高比的不同会导致图形在某些方向上扭曲。所以如果上一个例子中的viewBox被拉伸以在所有方向上适应视窗,图形看起来如下:图片 31

当给viewBox设置0 0 200 300的值时扭曲显而易见(显然这很不理想),这个值小于视窗尺寸。我故意选择这个尺寸从而让viewBox匹配鹦鹉边界盒子的尺寸。如果浏览器拉伸图像来适应整个视窗,看起来会像下面这样:图片 32

preserveAspectRatio属性让你可以在保持宽高比的情况下强制统一viewBox的缩放比,并且如果不想用默认居中你可以声明viewBox在视窗中的位置。

(4)cache html

上面第(3)步把图片、js、css缓存起来了,但是如果把页面html也缓存了,例如把首页缓存了,就会有一个尴尬的问题——Service Worker是在页面注册的,但是现在获取页面的时候是从缓存取的,每次都是一样的,所以就导致无法更新Service Worker,如变成sw-5.js,但是PWA又要求我们能缓存页面html。那怎么办呢?谷歌的开发者文档它只是提到会存在这个问题,但并没有说明怎么解决这个问题。这个的问题的解决就要求我们要有一个机制能知道html更新了,从而把缓存里的html给替换掉。

Manifest更新缓存的机制是去看Manifest的文本内容有没有发生变化,如果发生变化了,则会去更新缓存,Service Worker也是根据sw.js的文本内容有没有发生变化,我们可以借鉴这个思想,如果请求的是html并从缓存里取出来后,再发个请求获取一个文件看html更新时间是否发生变化,如果发生变化了则说明发生更改了,进而把缓存给删了。所以可以在服务端通过控制这个文件从而去更新客户端的缓存。如下代码:

JavaScript

this.addEventListener("fetch", function(event) { event.respondWith( caches.match(event.request).then(response => { // cache hit if (response) { //如果取的是html,则看发个请求看html是否更新了 if (response.headers.get("Content-Type").indexOf("text/html") >= 0) { console.log("update html"); let url = new URL(event.request.url); util.updateHtmlPage(url, event.request.clone(), event.clientId); } return response; } return util.fetchPut(event.request.clone()); }) ); });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
this.addEventListener("fetch", function(event) {
 
    event.respondWith(
        caches.match(event.request).then(response => {
            // cache hit
            if (response) {
                //如果取的是html,则看发个请求看html是否更新了
                if (response.headers.get("Content-Type").indexOf("text/html") >= 0) {
                    console.log("update html");
                    let url = new URL(event.request.url);
                    util.updateHtmlPage(url, event.request.clone(), event.clientId);
                }
                return response;
            }
 
            return util.fetchPut(event.request.clone());
        })
    );
});

通过响应头header的content-type是否为text/html,如果是的话就去发个请求获取一个文件,根据这个文件的内容决定是否需要删除缓存,这个更新的函数util.updateHtmlPage是这么实现的:

JavaScript

let pageUpdateTime = { }; let util = { updateHtmlPage: function (url, htmlRequest) { let pageName = util.getPageName(url); let jsonRequest = new Request("/html/service-worker/cache-json/" pageName ".sw.json"); fetch(jsonRequest).then(response => { response.json().then(content => { if (pageUpdateTime[pageName] !== content.updateTime) { console.log("update page html"); // 如果有更新则重新获取html util.fetchPut(htmlRequest); pageUpdateTime[pageName] = content.updateTime; } }); }); }, delCache: function (url) { caches.open(CACHE_NAME).then(cache => { console.log("delete cache "

  • url); cache.delete(url, {ignoreVary: true}); }); } };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
let pageUpdateTime = {
 
};
let util = {
    updateHtmlPage: function (url, htmlRequest) {
        let pageName = util.getPageName(url);
        let jsonRequest = new Request("/html/service-worker/cache-json/" pageName ".sw.json");
        fetch(jsonRequest).then(response => {
            response.json().then(content => {
                if (pageUpdateTime[pageName] !== content.updateTime) {
                    console.log("update page html");
                    // 如果有更新则重新获取html
                    util.fetchPut(htmlRequest);
                    pageUpdateTime[pageName] = content.updateTime;
                }
            });
        });
    },
    delCache: function (url) {
        caches.open(CACHE_NAME).then(cache => {
            console.log("delete cache " url);
            cache.delete(url, {ignoreVary: true});
        });
    }
};

代码先去获取一个json文件,一个页面会对应一个json文件,这个json的内容是这样的:

JavaScript

{"updateTime":"10/2/2017, 3:23:57 PM","resources": {img: [], css: []}}

1
{"updateTime":"10/2/2017, 3:23:57 PM","resources": {img: [], css: []}}

里面主要有一个updateTime的字段,如果本地内存没有这个页面的updateTime的数据或者是和最新updateTime不一样,则重新去获取 html,然后放到缓存里。接着需要通知页面线程数据发生变化了,你刷新下页面吧。这样就不用等用户刷新页面才能生效了。所以当刷新完页面后用postMessage通知页面:

JavaScript

let util = { postMessage: async function (msg) { const allClients = await clients.matchAll(); allClients.forEach(client => client.postMessage(msg)); } }; util.fetchPut(htmlRequest, false, function() { util.postMessage({type: 1, desc: "html found updated", url: url.href}); });

1
2
3
4
5
6
7
8
9
let util = {
    postMessage: async function (msg) {
        const allClients = await clients.matchAll();
        allClients.forEach(client => client.postMessage(msg));
    }
};
util.fetchPut(htmlRequest, false, function() {
    util.postMessage({type: 1, desc: "html found updated", url: url.href});
});

并规定type: 1就表示这是一个更新html的消息,然后在页面监听message事件:

JavaScript

if("serviceWorker" in navigator) { navigator.serviceWorker.addEventListener("message", function(event) { let msg = event.data; if (msg.type === 1 && window.location.href === msg.url) { console.log("recv from service worker", event.data); window.location.reload(); } }); }

1
2
3
4
5
6
7
8
9
if("serviceWorker" in navigator) {
    navigator.serviceWorker.addEventListener("message", function(event) {
        let msg = event.data;
        if (msg.type === 1 && window.location.href === msg.url) {
            console.log("recv from service worker", event.data);
            window.location.reload();
        }  
    });
}

然后当我们需要更新html的时候就更新json文件,这样用户就能看到最新的页面了。或者是当用户重新启动浏览器的时候会导致Service Worker的运行内存都被清空了,即存储页面更新时间的变量被清空了,这个时候也会重新请求页面。

需要注意的是,要把这个json文件的http cache时间设置成0,这样浏览器就不会缓存了,如下nginx的配置:

JavaScript

location ~* .sw.json$ { expires 0; }

1
2
3
location ~* .sw.json$ {
    expires 0;
}

因为这个文件是需要实时获取的,不能被缓存,firefox默认会缓存,Chrome不会,加上http缓存时间为0,firefox也不会缓存了。

还有一种更新是用户更新的,例如用户发表了评论,需要在页面通知service worker把html缓存删了重新获取,这是一个反过来的消息通知:

JavaScript

if ("serviceWorker" in navigator) { document.querySelector(".comment-form").addEventListener("submit", function() { navigator.serviceWorker.controller.postMessage({ type: 1, desc: "remove html cache", url: window.location.href} ); } }); }

1
2
3
4
5
6
7
8
9
10
if ("serviceWorker" in navigator) {
    document.querySelector(".comment-form").addEventListener("submit", function() {
            navigator.serviceWorker.controller.postMessage({
                type: 1,
                desc: "remove html cache",
                url: window.location.href}
            );
        }
    });
}

Service Worker也监听message事件:

JavaScript

const messageProcess = { // 删除html index 1: function (url) { util.delCache(url); } }; let util = { delCache: function (url) { caches.open(CACHE_NAME).then(cache => { console.log("delete cache "

  • url); cache.delete(url, {ignoreVary: true}); }); } }; this.addEventListener("message", function(event) { let msg = event.data; console.log(msg); if (typeof messageProcess[msg.type] === "function") { messageProcess[msg.type](msg.url); } });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const messageProcess = {
    // 删除html index
    1: function (url) {
        util.delCache(url);
    }
};
 
let util = {
    delCache: function (url) {
        caches.open(CACHE_NAME).then(cache => {
            console.log("delete cache " url);
            cache.delete(url, {ignoreVary: true});
        });
    }
};
 
this.addEventListener("message", function(event) {
    let msg = event.data;
    console.log(msg);
    if (typeof messageProcess[msg.type] === "function") {
        messageProcess[msg.type](msg.url);
    }
});

根据不同的消息类型调不同的回调函数,如果是1的话就是删除cache。用户发表完评论后会触发刷新页面,刷新的时候缓存已经被删了就会重新去请求了。

这样就解决了实时更新的问题。

本文由无需申请自动送彩金68发布于无需申请自动,转载请注明出处:致我们终将组件化的Web,离线网页应用

关键词: 免费送彩金