分类目录归档:未分类

新版Firefox无法播放Flash内容的问题

今日有部分用户反馈firefox无法播放视频,我看了一下,确实在firefox中播放器无法正常载入,但Chrome正常,排除播放器本身的问题,应该是嵌入播放器的HTML代码可能有些问题。
看一下这个代码

<object id="FPlayer" type="application/x-shockwave-flash" width="100%" height="100%" classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=10,0,0,0" align="middle">		  	<param name="allowScriptAccess" value="always" />		  	
  <param name="allowFullScreen" value="true" />		  	
  <param name="movie" value="//swf.ws.126.net/openplayer/v01/-0-2_MBOR278SK_MBP8E2U4J-vimg1_ws_126_net//image/snapshot_movie/2016/6/I/G/MBPAB1TIG-1423031805654.swf?isTEDPlay=1" />		  	
  <param name="quality" value="high" />		  	
  <param name="wmode" value="transparent" />		  	
  <embed src="//swf.ws.126.net/openplayer/v01/-0-2_MBOR278SK_MBP8E2U4J-vimg1_ws_126_net//image/snapshot_movie/2016/6/I/G/MBPAB1TIG-1423031805654.swf?isTEDPlay=1" id="FPlayer" quality="high" width="100%" height="100%" name="FPlayer" align="middle" allowscriptaccess="always" allowfullscreen="allowfullscreen" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" wmode="transparent" />		  	
</object>

摘自公开课一视频播放页播放器HTML代码
一瞟没啥问题,很经典的全兼容代码with w3c validate,但就是没有办法正常播放,因此决定使用排除法,排除了大部分内容之后,依旧无法正常播放,由于属性太多,决定改用增加变量的方式检测;首先找到一个可以正常播放的全兼容代码:

<object width="200" height="200" classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#4,0,0,0">	
  <param name="src" value="clock.swf" />	
  <param name="quality" value="high" />	
  <embed src="clock.swf" type="application/x-shockwave-flash" width="200" height="200" quality="high" pluginspage="http://www.macromedia.com/go/getflashplayer"></embed>
</object>

基于此进行多次修改,验证出是object的type=”application/x-shockwave-flash”会影响firefox正常解析embed标签,看了一下object与embed元素的差异(链接:http://w3help.org/zh-cn/causes/HO8001)发现,实际上embed与object单独均可以达到全浏览器兼容的效果,而差异仅仅是是否支持w3c校验,因此flash代码嵌入其实可以不用这么复杂。但这只算解决了问题,并不清楚为什么触发了此类bug,继续查找原因,找到object标签的定义(链接:https://developer.mozilla.org/en-US/docs/Web/HTML/Element/object),发现如下内容:

The content type of the resource specified by data. At least one of data and type must be defined.

data与type至少需要被定义,但定义了type没有定义data可能会导致浏览器尝试载入一个空资源,而出现问题,由于没有看firefox的具体实现逻辑,可能不是太准确,感兴趣的同学可以尝试看一下实现,但在使用flash播放器的时候尽量单独全兼容的代码,以避免此类问题发生。

nginx配置备记

先来写一个最常用的配置:location

Syntax:	location [ = | ~ | ~* | ^~ ] uri { ... }
location @name { ... }
Default:	—
Context:	server, location
来源:http://nginx.org/en/docs/http/ngx_http_core_module.html#location

nginx配置中占大头,可以对请求进行各种处理
location的生效顺序:先匹配字符串的,后匹配正则的,字符串按照最长匹配来匹配,但正则只要匹配成功就直接返回,如果在正则表达式中没有匹配,则用在字符串匹配中匹配的最合适的那个规则,但还有一个例外是:
如果字符串匹配使用了”^~”,如果匹配到则不会再去匹配正则
匹配方式: 
空 开头匹配
=  完全匹配
~ 正则匹配(区分大小写)
~* 正则匹配(不区分大小写)
^~ 开头匹配(若匹配正则将被忽略)

正则表达式:
nginx采用的是PCRE正则,文档一大堆,直接查就行,和平时使用的正则是一样的。

location的Context为server,location,因此location是可以嵌套的。

再看一下常用的配置: rewrite

Syntax:	rewrite regex replacement [flag];
Default:	—
Context:	server, location, if
来源:http://nginx.org/en/docs/http/ngx_http_rewrite_module.html

rewrite主要是改写当前url,当然是可以选择显式或者隐式改写,以满足多种需求:比如跳转,静态化,转发等
规则比较简单 会正则表达式就可以,匹配到的内容可以在replacement中使用,例如:
rewrite ^/article/(\d+)\.htm /article.htm?id=$1;

如何修复IE8浏览器下@font-face触发兼容性视图模式的Bug

源自于公开课官网的问题:https://open.163.com
本来是想打算解决公开课APM报警异常的,歪打正着看到了这个问题,就看了一下这个问题:
现象是使用Win7的IE8打开页面之后会提示

163.com的问题导致lnternet Explorer使用兼容性视图刷新网页

这个问题也是坑,百度结果不靠谱,Google又不知道怎么翻译搜索好,搞了好久,大概思路都是在crash,ie8,win7,Compatibility Mode等关键词上,其实也没有考虑别的太多的方面。由于实在是没有思路,所以来了点儿暴力招数:一个一个删除页面上的资源,找出触发Bug的情况,然后再进行分析。
最后定位到一个样式表文件在载入之后会触发页面白屏,最后又通过暴力二分法找到了受影响的语句(其实很快就找到了问题),发现@font-face在注释掉,或者不使用@font-face定义的font-family就不会出现问题。最后改变思路查找问题范围更改到font-face,ie8,crash上,结果找到了一些端倪:

I'm having a problem where icon fonts are causing IE8 to go into Compatibility Mode. And correspondingly, if IE8 is forced into Edge mode (eg. via ) then IE will crash.
来自于:https://stackoverflow.com/questions/25319643/icon-font-causes-compatibility-mode-in-ie8/29441642#29441642

从这来看这个Bug的触发是来自于IconFont使用了Private Use Area的原因,再来看一下Private Use Area是什么东西:

In Unicode, a Private Use Area (PUA) is a range of code points that, by definition, will not be assigned characters by the Unicode Consortium.[1] Currently, three private use areas are defined: one in the Basic Multilingual Plane (U+E000–U+F8FF), and one each in, and nearly covering, planes 15 and 16 (U+F0000–U+FFFFD, U+100000–U+10FFFD). The code points in these areas cannot be considered as standardized characters in Unicode itself. They are intentionally left undefined so that third parties may define their own characters without conflicting with Unicode Consortium assignments. Under the Unicode Stability Policy,[2] the Private Use Areas will remain allocated for that purpose in all future Unicode versions.
来自于:https://en.wikipedia.org/wiki/Private_Use_Areas

正常情况下其实使用Private Use Area去定义自定义符号是完全没有问题,甚至是一个正常的行为,由于IE8的具体实现我们不得而知,所以只能研究到什么行为导致的,所以解决方案也就很清楚了,避开使用Private Use Area就可以解决问题.

帮你纠正处理二进制数据时的想法

这个问题源自于我一年前做加解密的时候遇到的问题,结果最近IOS同学在做加解密时竟然遇到同样的问题,特记录一下也给各位普及一下,防止进入死胡同出不来。
一般我们在处理二进制数据时会直接读到二进制数据,自然也不会考虑到编码的问题,但在Web的海洋当中二进制传输发展较慢(主要是二进制数据处理支持较慢),所以在传输数据时习惯采用俗称的编码方式传输,这块就会出现一个问题:编码成什么样的内容?无非就是编码成字符串形式,这样就形成了字符串与二进制数据的一个对应关系,但其中有一种比较蛋疼的编码方式是直接将二进制数据本身使用字符串进行表示,比如00100011B=>23H=>”23″,11001101B=>ABH=>”AB”; WTF,这种很容易误导,以为将字符串直接转换成二进制就可以,比如前端同学会采用:

let someone = new Blob("AB", {type: "application/octet-binary"});
const fr = new FileReader();
fr.addEventListener('load',(e)=>{
    console.log(e.target.result);//retulr is ArrayBuffer
    //...do something
});
fr.readAsArrayBuffer(someone);

IOS同学可能会用:

NSData * data; 
NSString *result = [[NSString alloc] initWithData:data  encoding:NSUTF8StringEncoding]; 

直接坑死,关键地方在于这两种处理方式都是采用了字符串编码方式处理,读取的是编码后的二进制数据,也就是说是原始二进制存储数据。这种存储方式其实就和编码关系很大,如果给你传递的是GBK方式的字符串,那对应的数据又不一样了,这种直接处理的思维很容易让人陷在其中无法走出,只需要将字符串当成十六进制的数字处理,这个问题就直接解决了:

//”AB”=>ABH=>11001101B

Chrome下自签名证书提示无效的问题

其实之前就已经发现有这个问题,突然用本地调试的时候就提示证书无效,之前用的还好好的,也没有多想什么。今天刚好需要做新的项目,由于项目本身走全站HTTPS,为了能更快发现掺杂的非HTTPS资源,因此想要在本地配置一个自签名证书,发现怎么配置都不对,由于还忘记了当时配置自签名证书的方法,因此搞了好久,为了防止忘记,特地记录一下。
我看了很多关于自签名的文章,去找签名方法,但配置到apache中总会报错启动失败,这是由于签发的证书真是自签名证书,其实没有CA签名过程,具体看下例:

$ openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt

使用的是server.key签名server.csr生成server.crt文件,之前没有发现到,其实这个流程本身有点奇怪,因为本身自签名就不是认证的合法签名,这样签名之后生成的是一个CA签名,而不是域名签名。然而我老是用这种方式进行域名签名,死活不行也是很正常的。
因为我之前已经生成过ROOT CA,因此我只需要生成一个私钥、对应的证书请求文件、并使用ROOT CA生成签名证书就可以了,生成ROOT CA的方式如上面所述,在此就不在阐述

  openssl x509 -res -days 3650 -in reqFile -CA caCrtFile -CAkey caPrivateKeyFile -CAserial caSerialFile -out outCrtFile

其实一句话就可以对reqFile签名,但这种方式从Chrome58之后就不能再骗过Chrome浏览器了,会提示证书无效,也就是第一句话所说的问题。找到一篇日志解决此问题: https://medium.com/@kennychen_49868/chrome-58%E4%B8%8D%E5%85%81%E8%A8%B1%E6%B2%92%E6%9C%89san%E7%9A%84%E8%87%AA%E7%B0%BD%E6%86%91%E8%AD%89-12ca7029a933 原因及解决方法讲的很清楚,可以阅读链接文章,在此直接贴解决方案
只需要再创建一个extFile 具体内容为:

authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost

签名的命令行中多加一个参数 -extfile extfilePath

  openssl x509 -res -days 3650 -in reqFile -CA caCrtFile -CAkey caPrivateKeyFile -CAserial caSerialFile -extfile extFile -out outCrtFile

签名之后的证书就可以让Chrome重新显示绿锁。

如何在Linux下调试iOS Safari/Safari Webview

移动端的开发越来越多,在移动端上真机调试Web应用也是一个对于开发者而言比较强烈的刚需。google就做了一个叫ios_webkit_debug_proxy的东西,大部分博客或者文章对它的介绍基本上偏向于这个工具是让喜爱Chrome的开发者在不采用safari的调试工具下调试IOS Web应用,但我觉得这个工具更适合的定位是,非Mac调试IOS Web应用的开发者。
废话不多说,以Ubuntu为例,大致讲解如何在Linux系统上安装该程序。
git地址是:https://github.com/google/ios-webkit-debug-proxy
这个程序是多平台的,所以无论在Linux上还是Mac OS上甚至Windows上都可以使用,大部分安装方法都大同小异。好多前端开发者还是喜欢Windows下开发,不过我就不讲述Windows下的安装方法,请自行研究。
Github的Readme中讲述的非常简单,

On Linux or Mac:

sudo apt-get install autoconf automake libusb-dev libusb-1.0-0-dev libplist-dev libplist++-dev usbmuxd libtool libimobiledevice-dev

git clone https://github.com/google/ios-webkit-debug-proxy.git
cd ios-webkit-debug-proxy

./autogen.sh
make
sudo make install

但如果觉得安装确实是这么简单的,那实在是太天真了(我也天真过。。。),如果你按照这个方法安装,则会提示依赖版本过低,无法安装。所以如果想使用这个软件,先要安装好相关依赖。

ios-webkit-debug-proxy 主要依赖libimobiledevice,libusbmuxd,libplist这三个库,但libimobiledevice安装会异常的辛苦,因为这个东西的依赖稍微有些多

Sources and Dependencies:

libimobiledevice-1.2.0.tar.bz2
ifuse-1.1.3.tar.bz2
libplist-1.12.tar.bz2
libusbmuxd-1.0.10.tar.bz2
usbmuxd-1.1.0.tar.bz2 (needs libusb >= 1.0.3)
ideviceinstaller-1.1.0.tar.bz2
libideviceactivation-1.0.0.tar.bz2

这些都需要下载下来,地址在http://www.libimobiledevice.org/
make && sudo make install

git clone https://github.com/google/ios-webkit-debug-proxy.git

make && sudo make install

注意的是不要认为机器上已经有一些lib就不下载,根据我踩到的坑,基本上都要手动编译安装,要不然各种问题。

特别是libusbmuxd 这个在ubuntu操作系统中的默认版本相当低,容易出错又没有办法连接,务必注意。

./ios-webkit-debug-proxy 执行一下
连接机器
http://localhost:9222
点击所要调试的页面 enjoy.

Javascript变量作用域

众所周知,在ES6之前JS中的变量作用域是函数级的,不具有块级作用域,而这句话也仅限于对Javascript语言本身而言的,聪明的开发人员可以通过闭包可以创建出伪块级作用域用来模拟这种其他语言一般都有的块级作用域.就像这样:

function foo(){
 for(var a=0;a<10;a++){}
 console.log(a);//10
 for(a=0;a<10;a++){}
}
foo();
function foo(){
 (function(){
  for(var a=0;a<10;a++){}
 })();
 console.log(a);//undefined
}
foo();

然而es2015给你提供了更好的方案:

function foo(){
 "use strict";
 for(let a=0;a<10;a++){}
 console.log(a); //undefined
}
foo();

但写本文的主要目的与上面的关系不是太大,本文主要想探究函数级的作用域问题

function foo(){
 var a=0;
 function b(){
  console.log('b');
 }
 function bar(){
  console.log(a);//a
  b();//b
 }
 bar();
}
foo();

执行这一段程序之后可以发现函数里的所有变量和函数是可以被该函数以及该函数内部的子函数所访问的,稍微改一下代码

function foo(){
 var a=0;
 function b(){
  console.log('b');
 }
 function bar(){
  console.log(a);//a
  b();//b
 }
 bar.call(d);
}
function d(){
}
foo();

发现输出的结果是相同的,上面这个例子是为了证明解释器在查找变量的时候不是通过this定位查找的,因为在第一个例子中this为window(strict mode 为undefined),第二个例子中为d函数,这个现象的具体原因可以通过观察实际的作用域链去解释.
这段程序在运行到bar()时会将foo()指针压栈,开始执行bar,但解释bar发现有在foo中的同名变量c,这样就会将bar中使用的变量形成一个闭包,在作用域中同时会出现自己的c和闭包中的c.
很多人包括我在这个地方就会有一个疑问:闭包包的是引用变量还是整个函数?
不管闭包包了引用变量还是整个函数,对与开发者而言并没有什么两样,因为你就算只剩下引用部分剩余都被回收了我也不会受什么影响,而这个问题的影响范围只是GC是不是会部分回收,我想大部分开发者并不会去关心这个问题.然而这件事请其实是非常严重的,因为现有的GC机制是只要引用存在,整个函数就不能被GC回收,所以实际情况下闭包是包的整个函数,但你并不能访问到不存在引用的成员,这也非常符合常理.
通过以上的分析,我们就可以解释下面这个稍微复杂一点的例子的作用域范围了

function Foo(){
 var a=1;
 this.a=2;
 function geta(){
  "use strict";
  console.log(a); //1
  console.log(this.a); //Cannot read property 'a' of undefined
 }
 this.geta=function(){
  console.log(a);//1
  console.log(this.a);//2
 }
 geta();
}
foo=new Foo();
foo.geta();

总体来说,可以简单概括成一句话:this是this,作用域是作用域.不能将this和作用域混淆,这样以来哪些可以访问到,哪些不能被访问,很容易就能区分出来,也能很快理解为啥好多其他人写的程序里面会出现apply或call,这肯定是因为里面用到了this,而和作用域就扯不上任何关系了.

Riot.js源码分析

上一篇日志简单的介绍了一下Riot.js这个东西,这三周以来我一直再看React的源码,依然对它的batchUpdate一头雾水,所以决定先分析一下Riot.js.

Riot.js同样使用了VirtualDOM,但与React不同的是,它的DOM刷新并不像React那样是BatchUpdate做一下DOM与VirtualDOM diff,而是直接修改nodeText,这样的一个好处是在于算法不复杂,只需要遍历所有节点,相应进行插入就可以.

Riot同样具有自定义节点的功能,一般情况下我们不使用Riot.tag2进行自定义节点的定义,而是通过Riot提供的语法进行开发,最后使用浏览器/服务器端编译自动生成,但最后执行还是避免不了Riot.tag2,因此首先从该方法开始分析.

riot.tag2 = function(name, html, css, attrs, fn, bpair) {
//如果具有CSS,则将CSS插入到&lt;head&gt;&lt;/head&gt;中
if (css &amp;&amp; injectStyle) injectStyle(css)
//注册自定义标签
__tagImpl[name] = { name: name, tmpl: html, attrs: attrs, fn: fn }
return name
}

Riot的注册标签流程非常简单,仅做了两步动作:插入CSS,存储到__tagImpl对象当中
注册完自定义标签之后就可以使用riot.mount将自定义标签载入进实际的DOM节点.

riot.mount = function(selector, tagName, opts) {

var els,
allTags,
tags = []

//添加Riot标签

function addRiotTags(arr) {
var list = ''
//遍历每一个对象,添加Riot标记
each(arr, function (e) {
list += ', *[' + RIOT_TAG + '="' + e.trim() + '"]'
})
//返回标记List
return list
}

//返回所有标签

function selectAllTags() {
//返回自定义标签列表
var keys = Object.keys(__tagImpl)
//返回新的自定义标签列表
return keys + addRiotTags(keys)
}

function pushTags(root) {
var last

//如果root标签存在
if (root.tagName) {
//如果root标签还未初始化
if (tagName &amp;&amp; (!(last = getAttr(root, RIOT_TAG)) || last != tagName))
//设置RIOT_TAG
setAttr(root, RIOT_TAG, tagName)
//插入到root中
var tag = mountTo(root, tagName || root.getAttribute(RIOT_TAG) || root.tagName.toLowerCase(), opts)
//如果插入成功,则push到tags列表中
if (tag) tags.push(tag)
} else if (root.length)
//每一个对象都进行mountTo操作
each(root, pushTags)
}
// ----- mount code ----
//如果初始化属性存在(对象)
if (typeof tagName === T_OBJECT) {
opts = tagName
tagName = 0
}

//标签检测:字符串
if (typeof selector === T_STRING) {
//全部节点
if (selector === '*')
// 选取所有自定义节点
selector = allTags = selectAllTags()
else
// 选取当前传入节点
selector += addRiotTags(selector.split(','))

// 选择器选择DOM节点
els = selector ? $$(selector) : []
}
else
// 传入的是DOM对象,直接选取
els = selector

// 如果第二个参数为节点
if (tagName === '*') {
// 获取所有节点
tagName = allTags || selectAllTags()
// 如果是单个节点
if (els.tagName)
els = $$(tagName, els)
else {
var nodeList = []
选取所有_el节点下的tagName节点
each(els, function (_el) {
nodeList.push($$(tagName, _el))
})
els = nodeList
}
// get rid of the tagName
tagName = 0
}

//单个节点
if (els.tagName)
pushTags(els)
else
//批量节点
each(els, pushTags)
//返回节点对象
return tags
}

从代码中可以看出,mount方法主要完成分析DOM节点并依次生成自定义标签的过程.而具体的生成逻辑则在mountTo方法中.

function mountTo(root, tagName, opts) {
//从注册的标签库对象中获取
var tag = __tagImpl[tagName],
// 存储root中的innerHTML,实为yield
innerHTML = root._innerHTML = root._innerHTML || root.innerHTML
// 清空innerHTML
root.innerHTML = ''
//自定义标签和容器都存在,生成tag对象
if (tag &amp;&amp; root) tag = new Tag(tag, { root: root, opts: opts }, innerHTML)
//tag对象生成成功
if (tag &amp;&amp; tag.mount) {
//插入到文档中
tag.mount()
// 添加该tag对象到__virtualDom列表中
if (!contains(__virtualDom, tag)) __virtualDom.push(tag)
}
//返回tag对象
return tag
}

mountTo做了两项工作:存储&清空当前节点,注册新tag到virtualDOM中,然后将其余工作交给了Tag类:

 

function Tag(impl, conf, innerHTML) {

//使tag具有事件功能
var self = riot.observable(this),
//继承传入的opts
opts = inherit(conf.opts) || {},
//创建DOM容器
dom = mkdom(impl.tmpl),
//父节点
parent = conf.parent,
//是循环节点
isLoop = conf.isLoop,
//
hasImpl = conf.hasImpl,
//获取外置数据
item = cleanUpData(conf.item),
//表达式列表
expressions = [],
//子节点列表
childTags = [],
//DOM节点
root = conf.root,
//扩展方法
fn = impl.fn,
//标签名称
tagName = root.tagName.toLowerCase(),
//属性
attr = {},
propsInSyncWithParent = []
//若已经存在,则卸载
if (fn &amp;&amp; root._tag) root._tag.unmount(true)

// 标记为尚未载入
this.isMounted = false
//是否为循环节点
root.isLoop = isLoop

// 保持tag对象的引用,这样可以多次进行该标签的装载
root._tag = this

// 为该tag对象分配唯一id,这样可以提升virtualDOM的渲染效果
//至于如何提升,在随后的分析中将看到
defineProperty(this, '_riot_id', ++__uid)

//在tag对象上注册这些引用
extend(this, { parent: parent, root: root, opts: opts, tags: {} }, item)

// 遍历每一个属性
each(root.attributes, function(el) {
var val = el.value
// 将具有表达式的属性放入attr对象中
if (tmpl.hasExpr(val)) attr[el.name] = val
})

//dom中具有内容并且不存在特殊节点
if (dom.innerHTML &amp;&amp; !/^(select|optgroup|table|tbody|tr|col(?:group)?)$/.test(tagName))
// 将yeild替换为innerHTML中的内容
dom.innerHTML = replaceYield(dom.innerHTML, innerHTML)

// options
function updateOpts() {
var ctx = hasImpl &amp;&amp; isLoop ? self : parent || self

// update opts from current DOM attributes
each(root.attributes, function(el) {
opts[toCamel(el.name)] = tmpl(el.value, ctx)
})
// recover those with expressions
each(Object.keys(attr), function(name) {
opts[toCamel(name)] = tmpl(attr[name], ctx)
})
}

function normalizeData(data) {
for (var key in item) {
if (typeof self[key] !== T_UNDEF &amp;&amp; isWritable(self, key))
self[key] = data[key]
}
}

function inheritFromParent () {
if (!self.parent || !isLoop) return
each(Object.keys(self.parent), function(k) {
// some properties must be always in sync with the parent tag
var mustSync = !contains(RESERVED_WORDS_BLACKLIST, k) &amp;&amp; contains(propsInSyncWithParent, k)
if (typeof self[k] === T_UNDEF || mustSync) {
// track the property to keep in sync
// so we can keep it updated
if (!mustSync) propsInSyncWithParent.push(k)
self[k] = self.parent[k]
}
})
}

defineProperty(this, 'update', function(data) {

//防止节点的核心方法被覆盖
data = cleanUpData(data)
//处理父节点的内置属性
inheritFromParent()
//处理非可写属性与undefined属性
if (data &amp;&amp; typeof item === T_OBJECT) {
normalizeData(data)
item = data
}

//浅拷贝
extend(self, data)
//属性名转换
updateOpts()
//触发update事件(开始更新)
self.trigger('update', data)
//更新表达式
update(expressions, self)
//触发updated事件(更新完毕)
self.trigger('updated')
return this
})

defineProperty(this, 'mixin', function() {
each(arguments, function(mix) {
mix = typeof mix === T_STRING ? riot.mixin(mix) : mix
each(Object.keys(mix), function(key) {
// bind methods to self
if (key != 'init')
self[key] = isFunction(mix[key]) ? mix[key].bind(self) : mix[key]
})
// init method will be called automatically
if (mix.init) mix.init.bind(self)()
})
return this
})

defineProperty(this, 'mount', function() {

//设置名转换
updateOpts()

// 添加自定义函数
if (fn) fn.call(self, opts)

// 递归处理表达式
parseExpressions(dom, self, expressions)

// 加载自定义子节点
toggle(true)

// 获取自定义属性,提取表达式
if (impl.attrs || hasImpl) {
walkAttributes(impl.attrs, function (k, v) { setAttr(root, k, v) })
parseExpressions(self.root, self, expressions)
}

//自定义节点不是子节点也非循环节点,执行更新
if (!self.parent || isLoop) self.update(item)

//仅限内部使用的mount事件之后的事件,开发者需要使用mounted事件
self.trigger('before-mount')

if (isLoop &amp;&amp; !hasImpl) {
// 从loop节点中取出实际节点
self.root = root = dom.firstChild

} else {
//插入到root节点中
while (dom.firstChild) root.appendChild(dom.firstChild)
if (root.stub) self.root = root = parent.root
}

// 插入到root节点中
if (isLoop)
parseNamedElements(self.root, self.parent, null, true)

// 触发顶层自定义节点的mount事件,设置定义节点的已加载标记
if (!self.parent || self.parent.isMounted) {
self.isMounted = true
self.trigger('mount')
}
// 触发当前节点的mount事件,同时设置父节点和当前节点的已加载标记
else self.parent.one('mount', function() {
if (!isInStub(self.root)) {
self.parent.isMounted = self.isMounted = true
self.trigger('mount')
}
})
})

defineProperty(this, 'unmount', function(keepRootTag) {
//待卸载节点
var el = root,
//父节点
p = el.parentNode,
ptag

//触发卸载前事件
self.trigger('before-unmount')

// 从virtualDom中删除
__virtualDom.splice(__virtualDom.indexOf(self), 1)

//具有相关其他节点
if (this._virts) {
each(this._virts, function(v) {
v.parentNode.removeChild(v)
})
}

//存在父节点
if (p) {

if (parent) {
ptag = getImmediateCustomParentTag(parent)
// 从父节点中删除所有该标签节点
if (isArray(ptag.tags[tagName]))
each(ptag.tags[tagName], function(tag, i) {
if (tag._riot_id == self._riot_id)
ptag.tags[tagName].splice(i, 1)
})
else
ptag.tags[tagName] = undefined
}

else
//删除当前节点内首节点
while (el.firstChild) el.removeChild(el.firstChild)

//删除根节点中的el
if (!keepRootTag)
p.removeChild(el)
else
// 仅删除riot控制,不删除节点
remAttr(p, 'riot-tag')
}

self.trigger('unmount')
toggle()
self.off('*')
self.isMounted = false
// somehow ie8 does not like `delete root._tag`
root._tag = null

})

function toggle(isMount) {

// mount/unmount children
each(childTags, function(child) { child[isMount ? 'mount' : 'unmount']() })

// listen/unlisten parent (events flow one way from parent to children)
if (parent) {
var evt = isMount ? 'on' : 'off'

// the loop tags will be always in sync with the parent automatically
if (isLoop)
parent[evt]('unmount', self.unmount)
else
parent[evt]('update', self.update)[evt]('unmount', self.unmount)
}
}

// 插入节点,
parseNamedElements(dom, this, childTags)

}

上面的部分主要工作是为tag对象添加相关方法属性,为生成DOM处理做准备,准备完成之后会调用parseNamedElements函数,parseNamedElements函数主要是调用递归函数walk

function parseNamedElements(root, tag, childTags, forceParsingNamed) {
//递归匿名函数
walk(root, function(dom) {
//为element节点
if (dom.nodeType == 1) {
//循环DOM
dom.isLoop = dom.isLoop || (dom.parentNode &amp;&amp; dom.parentNode.isLoop || getAttr(dom, 'each')) ? 1 : 0

// 存在自定义节点
if (childTags) {
//获取自定义tag
var child = getTag(dom)
//存在自定义tag并且不是循环DOM
if (child &amp;&amp; !dom.isLoop)
//初始化tag并push到childTags列表
childTags.push(initChildTag(child, {root: dom, parent: tag}, dom.innerHTML, tag))
}
//
if (!dom.isLoop || forceParsingNamed)
setNamed(dom, tag, [])
}
})

}

//递归函数
function walk(dom, fn) {
// 如果DOM节点存在
if (dom) {
//如果执行fn返回false,结束当前递归操作,返回上一层
if (fn(dom) === false) return
else {
//选取第一个子节点
dom = dom.firstChild

while (dom) {
//递归执行
walk(dom, fn)
//移到下一个节点
dom = dom.nextSibling
}
}
}
}

递归处理主要处理以下两部分:
自定义子节点
具有name属性的节点

处理完之后,tag对象将返回,继续回到调用处向下执行tag.mount,实际代码在new Tag()时被添加进,主要提取所有表达式,将提取处的表达式传给update函数进行表达式计算,update函数如下:

function update(expressions, tag) {
//遍历每一个表达式
each(expressions, function(expr, i) {
//表达式节点
var dom = expr.dom,
//表达式属性值(如果是属性节点)
attrName = expr.attr,
//计算表达式
value = tmpl(expr.expr, tag),
//表达式节点的父节点
parent = expr.dom.parentNode
//fix 布尔表达式
if (expr.bool)
value = value ? attrName : false
//fix 非布尔表达式
else if (value == null)
value = ''

// textarea节点的特殊处理
if (parent &amp;&amp; parent.tagName == 'TEXTAREA') value = ('' + value).replace(/riot-/g, '')

// 无变化
if (expr.value === value) return
expr.value = value

// 表达式所在节点是文本节点
if (!attrName) {
//替换当前值
dom.nodeValue = '' + value // #815 related
return
}

// 删除原来的属性
remAttr(dom, attrName)
// 如果是函数
if (isFunction(value)) {
//绑定事件
setEventHandler(attrName, value, dom, tag)

// 具有逻辑的expression
} else if (attrName == 'if') {
var stub = expr.stub,
add = function() { insertTo(stub.parentNode, stub, dom) },
remove = function() { insertTo(dom.parentNode, dom, stub) }

// 添加到DOM节点
if (value) {
if (stub) {
add()
dom.inStub = false
// avoid to trigger the mount event if the tags is not visible yet
// maybe we can optimize this avoiding to mount the tag at all
if (!isInStub(dom)) {
walk(dom, function(el) {
if (el._tag &amp;&amp; !el._tag.isMounted) el._tag.isMounted = !!el._tag.trigger('mount')
})
}
}
// 从DOM节点删除
} else {
stub = expr.stub = stub || document.createTextNode('')
// if the parentNode is defined we can easily replace the tag
if (dom.parentNode)
remove()
// otherwise we need to wait the updated event
else (tag.parent || tag).one('updated', remove)

dom.inStub = true
}
// show / hide 属性特殊处理
} else if (/^(show|hide)$/.test(attrName)) {
if (attrName == 'hide') value = !value
dom.style.display = value ? '' : 'none'

// value属性特殊处理
} else if (attrName == 'value') {
dom.value = value

// <img src="{ expr }" alt="" /> 特殊处理
} else if (startsWith(attrName, RIOT_PREFIX) &amp;&amp; attrName != RIOT_TAG) {
if (value)
setAttr(dom, attrName.slice(RIOT_PREFIX.length), value)
//普通属性
} else {
if (expr.bool) {
dom[attrName] = value
if (!value) return
}
//Object无法赋值到属性当中
if (typeof value !== T_OBJECT) setAttr(dom, attrName, value)

}

})

}

执行完update之后,DOM中的数据已经更新完毕,接着会执行mount的后续操作

分析中可以发现,riot对于text节点和属性节点的控制能力相对好,而对于动态删除DOM节点,则会出现一些问题,而React的batchUpdate算法可以diff节点级的变化,而快速做出反应.riot对DOM节点的控制不是增加删除级别的控制,而是css级别的控制,这有区别于React.

riot的数据更新不是自动化的,需要通过手动去维护DOM与数据模型之间的数据,避免出现不同步的问题,而React的update是自动化的,但从以前使用类React框架的经验看来,完全的自动update还是不可靠,组件之间的通信还是免不了手动更新,因此这个东西有的时候感觉比较鸡肋,如果需要的话riot也可以整合进相关功能.

总体来说,riot.js框架代码量少,易理解,二次开发的成本比较低.所以比较适合定制化程度高的项目与小型项目快速开发.

Riot.js初探

正如Riot.js宣传标语:

类似 React 的微型 UI 库

与React相同,Riot 同样提供的是V层解决方案,与React不同的是,Riot非常轻量,也非常容易与其他类库结合来提升整个项目的开发效率.

似乎现在都意识到DOM过于复杂,直接去维护DOM与数据之间的关系是一件相当浪费资源的事情,因此Virtual DOM自然而然就成为了一个大部分开发者比较关注的焦点.Riot同样使用了Virtual DOM来解决update的问题.

Riot.js为国内开发者提供了一个非常好的本地化帮助文档,我感觉都不需要再下面多啰嗦几句了.

官方中文:http://riotjs.com/zh/

Riot为快速开发小型Web应用提供了极佳的解决方案,总结起来有以下几个方面:

1.由于小型Web应用逻辑相对不复杂,所需的功能并不多,而同样有维护需求.Riot.js提供的自定义标签可以使代码具有组件特性,整个应用维护与阅读都非常友好.

2.特别是对于移动端应用开发,Riot.js提供全平台支持,加上体积小巧,因此在流量是金的移动端上更有实际意义.

3.小型Web应用不会配备太多人员进行开发,因此如何进行快速开发是非常重要的问题,Riot.js语法简单,API数量少,开发人员可以边看文档边进行开发,并迅速熟悉Riot.js.

 

前端架构:Flux架构初探

最近查找React Transaction的相关资料的时候,无意中看到了Flux这个东西,简单看了一下,突然想到了我以前稍微想过的一个前端架构.

以前是这个想的:

(Controller) ->   (View)

所有关于操作视图的逻辑都单独抽出来,Controller负责处理所有非视图逻辑,这样做的一个好处就是可以将视图部分和逻辑部分分隔开,修改逻辑或者视图并不需要在一堆代码中寻找,只需要找到相应视图变化点,切入修改非常方便.

data=controller.requestData();//获取数据

data=controller.transData(data);

controller.bind(‘click’,view.getButton1(),function(){ controller.doBtn1Click();});//监听

view.renderAll(data);//更新全部视图

view.renderHead(data);//更新头部视图

但这样的一个缺陷就在于大部分关于DTO的逻辑会掺在Controller当中,整个业务逻辑十分不透明,

然后改成了这样:

(Data Model)  ->  (Controller) -> (View)

将数据模型单独抽离出来,Controller部分将更加地清晰

controller.init(); //初始化controller

controller.bind(‘click’,view.getButton1(),function(){ controller.doBtn1Click();});//监听

renderData=dataModel.getData(); //从数据模型获取数据

view.renderAll(renderData);// 完成视图

读了Flux之后,按照这种思想来了一套我所想的Flux

 

其实在前端领域,没有什么真正的所谓的MVC,Flux则从这种MVC的思想框当中跳出来,提出了有一个更适合前端架构的一个架构模型,我也用过类似React的一个前端框架,正如React本身所介绍的那样,React仅仅是一个View层解决方案,如果单使用React代码还是一团糟,其实这种理解是建立在一个范围上的,如果单看View层,React其实已经做到了视图分离,控制器自行去指派各种事件,视图与数据模型的同步也都做得不错,看起来已经像是一个比较小的MVC,但从前端整体看来,这种分离还是不够透彻,各个部分的逻辑还是只能写在render之前,双向数据绑定经常会出现数据不同步的情况(React本身也不支持双向数据绑定),所以最终React只能成为一个View层解决方案.

Flux则提供了一种新的思路,使得数据流向真正的成为了一个单向的过程,这样的一个最好的优点肯定是稳定性要好很多,数据不同步几乎不可能发生(没控制好也可能发生),下面稍微讲一下我理解的Flux架构(有些地方不太一样),所以一下称为这种前端架构.

这种前端架构主要是由Dispatcher,Store,View,Action四部分组成,实际上一切的开始都是Action触发的,所以顺序是Action->Dispatcher->Store->View,但Action的始终还是View,所以我把它放到了最后.

Dispatcher是事件分派器,起初数据仓库里面并没有数据,由事件分派器通过分析事件,将所需的数据列表或者事件发给Store,事件的来源是Action.在Flux中,数据请求是在Action中发送的,但我将这一步移到了Store中,目的是让Store的数据仓库除了具备数据仓库,又具备数据获取的功能.如果可能,前端的数据仓库与后端数据仓库也可以保持1:1的同步状态.

Store是数据仓库,数据仓库包含大部分的DTO逻辑,数据的相关操作是由事件分派器所传出的指令操作的.数据的推送是通过事件,但此时的事件分派器是被弱化的,与Dispatcher不同,是一种属于View的事件机制.

View是视图,所有视图的数据来源都是Store,按照规定,View在任何时刻都不应该主动访问Store去获取数据,而是通过触发Action再通过Dispatcher分配最终间接获取数据,Store也不应该提供对外接口供View访问,这样就可以保证数据的一致性.

为了让整个前端架构的View层更加详细,下面再引入React,扩充View.

State是Store数据与render数据的暂存区,由于React可以保障renderData与State之间的同步关系,所以我们只需要做到State与Store之间的同步关系就可以,这种解决方案就是通过上述的Flux去维持.而中间的React VitrualDOM能使View更加高效灵活.