前端教程

一、HTML教程

1.1 基本标签

待定

1.2 URL相关

1.2.1 Web中的Blob

在一般的Web开发中,很少会用到Blob,但Blob可以满足一些场景下的特殊需求。Blob,Binary Large Object的缩写,代表二进制类型的大对象。Blob的概念在一些数据库中有使用到,例如,MYSQL中的BLOB类型就表示二进制数据的容器。在Web中,Blob类型的对象表示不可变的类似文件对象的原始数据,通俗点说,就是Blob对象是二进制数据,但它是类似文件对象的二进制数据,因此可以像操作File对象一样操作Blob对象,实际上,File继承自Blob。

可以通过Blob的构造函数创建Blob对象: Blob(blobParts[, options]),参数说明:

参数 说明
blobParts 数组类型,数组中的每一项连接起来构成Blob对象的数据,数组中的每项元素可以是ArrayBuffer,ArrayBufferView,Blob,DOMString
options 可选项,字典格式类型,可以指定如下两个属性:
- type,默认值为"",它代表了将会被放入到blob中的数组内容的MIME类型
- endings,默认值为"transparent",用于指定包含行结束符\n的字符串如何被写入。它是以下两个值中的一个:"native",表示行结束符会被更改为适合宿主操作系统文件系统的换行符;"transparent",表示会保持blob中保存的结束符不变

例如:

var data1 = "a";
var data2 = "b";
var data3 = "<div style='color:red;'>This is a blob</div>";
var data4 = { "name": "abc" };

var blob1 = new Blob([data1]);
var blob2 = new Blob([data1, data2]);
var blob3 = new Blob([data3]);
var blob4 = new Blob([JSON.stringify(data4)]);
var blob5 = new Blob([data4]);
var blob6 = new Blob([data3, data4]);

console.log(blob1);  //输出:Blob {size: 1, type: ""}
console.log(blob2);  //输出:Blob {size: 2, type: ""}
console.log(blob3);  //输出:Blob {size: 44, type: ""}
console.log(blob4);  //输出:Blob {size: 14, type: ""}
console.log(blob5);  //输出:Blob {size: 15, type: ""}
console.log(blob6);  //输出:Blob {size: 59, type: ""}

size代表Blob对象中所包含数据的字节数。这里要注意,使用字符串和普通对象创建Blob时的不同,blob4使用通过JSON.stringify把data4对象转换成json字符串,blob5则直接使用data4创建,两个对象的size分别为14和15。blob4的size等于14很容易理解,因为JSON.stringify(data4)的结果为:"{"name":"abc"}",正好14个字节(不包含最外层的引号)。blob5的size等于15是如何计算而来的呢?实际上,当使用普通对象创建Blob对象时,相当于调用了普通对象的toString()方法得到字符串数据,然后再创建Blob对象。所以,blob5保存的数据是"[object Object]",是15个字节(不包含最外层的引号)。

Blob对象有一个slice方法,返回一个新的Blob对象,包含了源Blob对象中指定范围内的数据。使用方法:slice([start[, end[, contentType]]])。参数说明:

参数 说明
start 可选,代表Blob里的下标,表示第一个会被会被拷贝进新的Blob的字节的起始位置,如果传入的是一个负数,那么这个偏移量将会从数据的末尾从后到前开始计算
end 可选,代表的是Blob的一个下标,这个下标-1的对应的字节将会是被拷贝进新的Blob的最后一个字节,如果你传入了一个负数,那么这个偏移量将会从数据的末尾从后到前开始计算
contentType 可选,给新的Blob赋予一个新的文档类型,这将会把它的type属性设为被传入的值,它的默认值是一个空的字符串

例如:

var data = "abcdef";
var blob1 = new Blob([data]);
var blob2 = blob1.slice(0,3);

console.log(blob1);  // 输出:Blob {size: 6, type: ""}
console.log(blob2);  // 输出:Blob {size: 3, type: ""}

通过slice方法,从blob1中创建出一个新的blob对象,size等于3。

1.2.2 Blob使用场景

前面已经说过,File继承自Blob,因此我们可以调用slice方法对大文件进行分片长传。代码如下:

function uploadFile(file) {
    var chunkSize = 1024 * 1024;   // 每片1M大小
    var totalSize = file.size;
    var chunkQuantity = Math.ceil(totalSize/chunkSize);  //分片总数
    var offset = 0;  // 偏移量

    var reader = new FileReader();
    reader.onload = function(e) {
        var xhr = new XMLHttpRequest();
        xhr.open("POST","http://xxxx/upload?fileName="+file.name);
        xhr.overrideMimeType("application/octet-stream");

        xhr.onreadystatechange = function() {
            if(xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
                ++offset;
                if(offset === chunkQuantity) {
                    alert("上传完成");
                } else if(offset === chunkQuantity-1){
                    blob = file.slice(offset*chunkSize, totalSize);   // 上传最后一片
                    reader.readAsBinaryString(blob);
                } else {
                    blob = file.slice(offset*chunkSize, (offset+1)*chunkSize);
                    reader.readAsBinaryString(blob);
                }
            }else {
                alert("上传出错");
            }
        }

        if(xhr.sendAsBinary) {
            xhr.sendAsBinary(e.target.result);   // e.target.result是此次读取的分片二进制数据
        } else {
            xhr.send(e.target.result);
        }
    }
    var blob = file.slice(0, chunkSize);
    reader.readAsBinaryString(blob);
}

这段代码还可以进一步丰富,比如显示当前的上传进度,使用多个XMLHttpRequest对象并行上传对象(需要传递分片数据的位置参数给服务器端)等。

Blob URL是blob协议的URL,它的格式如:blob:http://XXX。Blob URL可以通过URL.createObjectURL(blob)创建。在绝大部分场景下,我们可以像使用Http协议的URL一样,使用Blob URL。常见的场景有:作为文件的下载地址和作为图片资源地址。

文件下载地址:

<script>
    function createDownloadFile() {
        var content = "Blob Data";
        var blob = new Blob([content]);
        var link = document.getElementsByTagName("a")[0];
        link.download = "file";
        link.href = URL.createObjectURL(blob);
    }
    window.onload = createDownloadFile;
</script>
<a>下载</a>

点击下载按钮,浏览器将会下载一个名为file的文件,文件的内容是:Blob Data。通过Blob对象,我们在前端代码中就可以动态生成文件,提供给浏览器下载。打开Chrome浏览器调试窗口,在Elements标签下可以看到生成的Blob URL为:

为图片文件创建一个Blob URL,赋值给标签:

<script>
    function handleFile(e) {
        var file = e.files[0];
        var blob = URL.createObjectURL(file);
        var img = document.getElementsByTagName("img")[0];
        img.src = blob;
        img.onload = function(e) {
            URL.revokeObjectURL(this.src);  // 释放createObjectURL创建的对象##
        }
    }
</script>
<input type="file" accept="image/*" onchange="handleFile(this)" /><br/>
<img style="width:200px;height:200px">

input中选择的图片会在里显示出来,如图所示:

同时,可以在Network标签栏,发现这个Blob URL的请求信息:

这个请求信息和平时我们使用的Http URL获取图片几乎完全一样。我们还可以使用Data URL加载图片资源:

<script>
    function handleFile(e) {
        var file = e.files[0];
        var fileReader = new FileReader();
        var img = document.getElementsByTagName("img")[0];
        fileReader.onload = function(e) {
            img.src = e.target.result;
        }
        fileReader.readAsDataURL(file);
    }
</script>

<input type="file" accept="image/*" onchange="handleFile(this)" /> <br/>
<img style="width:200px;height:200px">

FileReader的readAsDataURL生成一个Data URL,如图所示:

https://juejin.im/post/59e35d0e6fb9a045030f1f35

Data URL对大家来说应该并不陌生,Web性能优化中有一项措施:把小图片用base64编码直接嵌入到HTML文件中,实际上就是利用了Data URL来获取嵌入的图片数据。那么Blob URL和Data URL有什么区别呢?

Blob URL的长度一般比较短,但Data URL因为直接存储图片base64编码后的数据,往往很长,如上图所示,浏览器在显示Data URL时使用了省略号(…)。当显式大图片时,使用Blob URL能获取更好的可能性。Blob URL可以方便的使用XMLHttpRequest获取源数据,例如:

var blobUrl = URL.createObjectURL(new Blob(['Test'], {type: 'text/plain'}));
var x = new XMLHttpRequest();
// 如果设置x.responseType = 'blob',将返回一个Blob对象,而不是文本:
// x.responseType = 'blob';
x.onload = function() {
    alert(x.responseText);   // 输出 Test
};
x.open('get', blobUrl);
x.send();

对于Data URL,并不是所有浏览器都支持通过XMLHttpRequest获取源数据的。Blob URL只能在当前应用内部使用,把Blob URL复制到浏览器的地址栏中,是无法获取数据的。Data URL相比之下,就有很好的移植性,你可以在任意浏览器中使用。

除了可以用作图片资源的网络地址,Blob URL也可以用作其他资源的网络地址,例如html文件、json文件等,为了保证浏览器能正确的解析Blob URL返回的文件类型,需要在创建Blob对象时指定相应的type:

// 创建HTML文件的Blob URL
var data = "<div style='color:red;'>This is a blob</div>";
var blob = new Blob([data], { type: 'text/html' });
var blobURL = URL.createObjectURL(blob);

// 创建JSON文件的Blob URL
var data = { "name": "abc" };
var blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
var blobURL = URL.createObjectURL(blob);

2.2 多媒体标签

随着移动设备的飞速发展、各浏览器环境对Video支持的普及、Flash的没落,使用一个简单的VideoHTMLElement标签来替代原有的播放器插件进行视频媒体播放,帮我们更好的提高用户体验、增加更多的产品可能性已经日趋明显。

这篇文章我们从Video的几种应用场景、各环境中依然或将长期存在的问题、基于原生Video进行业务开发常见的问题等方面,一起学习了解更多的实践方向和可能存在的坑。

2.2.1 Video的几种应用场景

H5中视频播放,像我们以前使用IMG标签插入图片到网页里一样方便,写一个video设置一个src属性指定媒体源地址就可以实现媒体播放了,针对低版本浏览器video的HTML结构也提供了友好的兼容方案,只需要在video标签内写上不兼容时要展示的HTML内容即可。

<video src="https://chimee.org/vod/1.mp4" controls>
    您的浏览器不支持Video标签。
</video>

为了便于用户的操作,我们还设置了一个布尔型controls属性,告诉浏览器这个播放器渲染时需要显示控制条,在Chrome中渲染出的效果如下:

当然HTMLVideoElement的属性设置不只简单的提供了一个controls,还有更多的可配置项,比如:通过添加autoplay属性开启自动播放、通过添加muted设置为静音模式、通过给volume属性设置0.0~1.0之间的数值控制初始音量大小...

目前原生H5支持的媒体格式主要有MP4、OGG、WebM、M3U8等,基于媒体编码格式存在专利和产权归属问题,所以各大浏览器厂商之间对媒体格式的支持也各不相同:

MP4:

WebM:

OGG:

M3U8:

通过上面可以看到,目前就MP4各浏览器的兼容较好。这种浏览器之间媒体格式支持的差异化,使得我们必须得考虑不同用户环境的兼容问题。比较庆幸的是Video标准制定中也考虑了这些问题,比如在HTML代码的编写上我们可以在video标签内嵌套source标签来达成不同兼容环境下设定相应媒体源的需求:

<video controls>
    <source src="https://chimee.org/vod/2.webm">
    <source src="https://chimee.org/vod/2.ogg">
    <source src="https://chimee.org/vod/2.mp4">
    <source src='https://chimee.org/x.myvideoext' type='video/mp4; codecs="mp4v.20.8, mp4a.40.2"'>
    <p>当前环境不支持video标签。</p>
</video>

当我们设置了source,就可以让不同的浏览器环境按照自身的支持情况,找到能正常解码播放的媒体源进行播放,为了让浏览器更快速准确的匹配媒体资源,建议同时设定type属性,来明确说明媒体编码格式,以尽可能的减少不必要的等待。

另外,在编写javascript的时候,也可以通过HTMLVideoElement的canPlayType API进行当前环境的格式兼容判断:

let videoEl = document.createElement("video");

// 是否支持MP4
videoEl.canPlayType('video/mp4') !== '';

// 是否支持MP4&特定编码的
videoEl.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"') !== '';

// 是否支持webm&特定编码的
videoEl.canPlayType('video/webm; codecs="vp8, vorbis"') !== '';

// 是否支持ogg &特定编码的
videoEl.canPlayType('video/ogg; codecs="theora, vorbis"') !== '';

// 是否支持HLS的m3u8
videoEl.canPlayType('application/vnd.apple.mpegURL') !== '';

// 是否支持HLS的TS切片
videoEl.canPlayType('video/mp2t; codecs="avc1.42E01E,mp4a.40.2"') !== '';

使用canPlayType方法传入要检测的媒体类型或具体编码格式,我们将可能得到'maybe'、'probably'、''三个String值中的一个,当得到空字串的时候,可以确定为不支持。

一方面我们可以通过VideoElement的JS API进行状态的修改控制,来实现用户行为对剧情发展的影响带来不同场景间的切换;另一方面我们通过对VideoElement的事件监听可以响应状态的变化,当剧情发展到特定场景需要用户执行关键抉择。

到这里只是基于最基本的VideoAPI已经可以实现很多有意思的东西了。聪明的你一定想到了举一反三,实现更复杂的一些互动游戏了吧,比如各种风靡微信的密室逃脱...

如果我们接下来在Video的使用上更进一步,加入更宽泛的H5 API应用,脑洞或许可以更大。

在这里我们加入FileInput和FileReader API,让用户可以直接在网页中预览并播放自己选择的本地媒体文件。

let iptFileEl = document.querySelector('input[type="file"]');
let videoEl = document.querySelector('video');

iptFileEl.onchange = e =>{
    let file = iptFileEl.files && iptFileEl.files[0];
    playFile(file);
};

function playFile(file){
    if(file){
        let fileReader = new FileReader();
        fileReader.onload = evt => {
            if(FileReader.DONE == fileReader.readyState){
                videoEl.src = fileReader.result;
            }else{
                console.log('FileReader Error:', evt);
            }
        }
        fileReader.readAsDataURL(file);
    }else{
        videoEl.src = '';
    }
}

这里使用了比较简单粗暴的方案:当用户选择了媒体文件,那么直接使用FileReader得到本地文件的DataURI,并塞给video进行播放;虽然这个方案存在一些局限,但实用价值也还是挺高的,比如在线GIF编辑器https://gif.75team.com的视频转GIF功能就有用到类似的方案。

随着WebRTC的兴起,以往在WEB端很难实现的需求,也完全可以成为现实。比如通过浏览器提供的getUserMedia API,可以非常容易的开启摄像头并采集的视频流在页面上播放:

navigator.getUserMedia(
    { audio: false, video: true},
    function(stream) {
        let video = document.querySelector('video');
        video.srcObject = stream;
        // 当媒体头信息就绪,自动开始播放
        video.onloadedmetadata = () => video.play();
    },
    function(err) {
        alert('getUserMedia error: ' + err.message);
    }
);

既然能拿到摄像头视频流了,自然也就可以做更多的事情了,关于摄像头媒体流播放可以点这里尝试一下;而配合Canvas进行Video的画面的分析处理,我们也可以做更多有趣的尝试,比如:人脸识别...

而结合MediaRecorder API,也可以很方便的实现纯WEB前端的视频录制功能

直播行业的兴起,让更多的人了解了动态媒体流的存在,而WebRTC制定之初也主要是定位在多媒体实时通讯方向,这里面包含的MediaSource API让我们可以用JS创建动态媒体源,然后再通过任意异步方式往里appendBuffer,实现不停的拉流播放。

var video = document.querySelector('video');
var mediaSource = new MediaSource();
video.src = URL.createObjectURL(mediaSource);

mediaSource.addEventListener('sourceopen', function() {
    var sourceBuffer = mediaSource.addSourceBuffer('video/mp4; codecs="avc1.42E01E, mp4a.40.2"');
    fetchAB('https://nickdesaulniers.github.io/netfix/demo/frag_bunny.mp4', function (buf) {
        sourceBuffer.addEventListener('updateend', function () {
            mediaSource.endOfStream();
            video.play();
        });
        sourceBuffer.appendBuffer(buf);
    });
});

function fetchAB (url, cb) {
    var xhr = new XMLHttpRequest();
    xhr.open('get', url);
    xhr.responseType = 'arraybuffer';
    xhr.onload = function () { cb(xhr.response) };
    xhr.send();
};

异步拉流播放效果示例

前面我们在谈到原生Video支持的媒体格式类型时,了解到的M3U8动态切片格式是众多格式中适合直播流的媒体类型,但并不是所有浏览器都支持,而且眼下很多直播平台可能也还是在使用FLV比较多,但原生Video并不支持FLV,但是结合JS动态拉流、解码、编码、MediaSource API,也让WEB前端直接播放原生不支持的媒体格式成为了可能。

2.2.2 video标签的属性、方法

属性 说明
src 视频的地址(可以是url也可以是本地文件)
poster 视频封面,没有播放时显示的图片
preload 用来缓存大体积文件的,有三个可选值:
- "none"-不缓存;
- "auto"-缓存;
- "metadata"-只缓存文件元信息
autoplay 自动播放
loop 让文件循环播放
controls 显示标准的HTML5视频/音频播放器控制条、控制按钮
width 视频显示的宽度
height 视频显示的高度

html示例代码

<video id="media" src="http://www.sundxs.com/test.mp4" controls width="400px" heigt="400px"></video>
// audio和video都可以通过JS获取对象,JS通过id获取video和audio的对象
// 获取video对象
Media = document.getElementById("media");

HTMLVideoElement和HTMLAudioElement均继承自HTMLMediaElement。

Media.error;      // null:正常
Media.error.code; // 1.用户终止;2.网络错误;3.解码错误;4.URL无效

网络状态

使用 说明
Media.currentSrc; 返回当前资源的URL
Media.src = value; 返回或设置当前资源的URL
Media.canPlayType(type); 是否能播放某种格式的资源
Media.networkState; 0. 此元素未初始化;
1. 正常但没有使用网络;
2. 正在下载数据;
3. 没有找到资源
Media.load(); 重新加载src指定的资源
Media.buffered; 返回已缓冲区域,TimeRanges
Media.preload; - none:不预载
- metadata:预载资源信息
- auto:自动
使用 说明
Media.readyState; 1. HAVE_NOTHING
2. HAVE_METADATA
3. HAVE_CURRENT_DATA
4. HAVE_FUTURE_DATA
5. HAVE_ENOUGH_DATA
Media.seeking; 是否正在seeking
使用 说明
Media.currentTime = value; 当前播放的位置,赋值可改变位置
Media.startTime; 一般为0,如果为流媒体或者不从0开始的资源,则不为0
Media.duration; 当前资源长度,流返回无限
Media.paused; 是否暂停
Media.defaultPlaybackRate = value; 默认的回放速度,可以设置
Media.playbackRate = value; 当前播放速度,设置后马上改变
Media.played; 返回已经播放的区域,TimeRanges,关于此对象见下文
Media.seekable; 返回可以seek的区域TimeRanges
Media.ended; 是否结束
Media.autoPlay; 是否自动播放
Media.loop; 是否循环播放
Media.play(); 播放或者继续播放
Media.pause(); 暂停
使用 说明
Media.controls; 是否有默认控制条
Media.volume = value; 音量
Media.muted = value; 静音
使用 说明
TimeRanges.length; 区域段数
TimeRanges.start(index) 第index段区域的开始位置
TimeRanges.end(index) 第index段区域的结束位置
//相关事件
var eventTester = function(e){
    Media.addEventListener(e,function(){
        console.log((new Date()).getTime(),e)
    },false);
}
使用 说明
eventTester("loadstart"); 客户端开始请求数据
eventTester("progress"); 客户端正在请求数据
eventTester("suspend"); 延迟下载
eventTester("abort"); 客户端主动终止下载(不是因为错误引起)
eventTester("loadstart"); 客户端开始请求数据
eventTester("progress"); 客户端正在请求数据
eventTester("suspend"); 延迟下载
eventTester("abort"); 客户端主动终止下载(不是因为错误引起)
eventTester("error"); 请求数据时遇到错误
eventTester("stalled"); 网速失速
eventTester("play"); play()和autoplay开始播放时触发
eventTester("pause"); pause()触发
eventTester("loadedmetadata"); 成功获取资源长度
eventTester("loadeddata");
eventTester("waiting"); 等待数据,并非错误
eventTester("playing"); 开始回放
eventTester("canplay"); 可以播放,但中途可能因为加载而暂停
eventTester("canplaythrough"); 可以播放,歌曲全部加载完毕
eventTester("seeking"); 寻找中
eventTester("seeked"); 寻找完毕
eventTester("timeupdate"); 播放时间改变
eventTester("ended"); 播放结束
eventTester("ratechange"); 播放速率改变
eventTester("durationchange"); 资源长度改变
eventTester("volumechange"); 音量改变

2.2.3 audio标签属性和方法

属性 说明
src 音乐的URL
preload 预加载
- "none"-不缓存;
- "auto"-缓存;
- "metadata"-只缓存
autoplay 自动播放
loop 循环播放
controls 浏览器自带的控制条
<audio id="media" src="http://www.abc.com/test.mp3" controls></audio>

// audio可以直接通过new创建对象
Media = new Audio("http://www.abc.com/test.mp3");
// audio和video都可以通过标签获取对象
Media = document.getElementById("media");

audio标签的方法与video标签方法相同。

当你已经用新的元素将媒体嵌入HTML文档以后,你就可以用JavaScript代码采用编程的方式来控制它们。比如说,如果你想(重新)开始播放,可以写如下的代码:

var v = document.getElementsByTagName("video")[0];
v.play();

头一行是取得当前文档中第一个视频元素,下一行调用了该元素的play()方法, 这一方法在实现媒体元素的接口中定义。

控制一个HTML5音频播放器的播放、暂停、增减音量等则直接了当:

<audio id="demo" src="audio.mp3"></audio>
<div>
    <button onclick="document.getElementById('demo').play()">播放声音</button>
    <button onclick="document.getElementById('demo').pause()">暂停声音</button>
    <button onclick="document.getElementById('demo').volume+=0.1">提高音量</button>
    <button onclick="document.getElementById('demo').volume-=0.1">降低音量</button>
</div>

停止媒体播放很简单,只要调用pause()方法即可,然而浏览器还会继续下载媒体直至媒体元素被垃圾回收机制回收。

以下是即刻停止媒体下载的方法:

var mediaElement = document.getElementById("myMediaElementID");
mediaElement.pause();
mediaElement.src='';
// or
mediaElement.removeAttribute("src");

通过移除媒体元素的src属性(或者直接将其设为一个空字符串——这取决于具体浏览器), 你可以摧毁该元素的内部解码,从而结束媒体下载。removeAttribute()操作并不干净, 而将<video>元素的'src'属性设为空字符串可能会引起我们不想要的请求(Mozilla Firefox 22)。

媒体元素支持在媒体的内容中从当前播放位置移到某个特定点。这是通过设置元素的属性currentTime的值来达成的;有关元素属性的详细信息请看HTMLMediaElement。简单的设置那个你希望继续播放的以秒为单位时间值。

你可以使用元素的属性seekable来决定媒体目前能查找的范围。它返回一个你可以查找的TimeRanges时间对象。

var mediaElement = document.getElementById('mediaElementID');
mediaElement.seekable.start();  // 返回开始时间 (in seconds)
mediaElement.seekable.end();    // 返回结束时间 (in seconds)
mediaElement.currentTime = 122; // 设定在 122 seconds
mediaElement.played.end();      // 返回浏览器播放的秒数

在给一个<audio>或者<video>元素标签指定媒体的URI的时候,你可以选择性地加入一些额外信息来指定媒体将要播放的部分。要这样做的话,需要附加一个哈希标志("#"),后面跟着媒体片段的描述。

一条指定时间范围的语句:#t=[starttime][,endtime]

时间值可以被指定为秒数(如浮点数)或者为以冒号分隔时/分/秒格式(像2小时5分钟1秒表示为2:05:01)。

一些例子:

http://foo.com/video.ogg#t=10,20
指定视频播放范围为从第10秒到第20秒.
http://foo.com/video.ogg#t=,10.5
指定视频从开始播放到第10.5秒.
http://foo.com/video.ogg#t=,02:00:00
指定视频从开始播放到两小时.
http://foo.com/video.ogg#t=60
指定视频从第60秒播放到结束.

媒体元素URI中播放范围部分的规范已被加入到Gecko 9.0(Firefox 9.0 / Thunderbird 9.0 / SeaMonkey 2.6).当下,这是Geoko Media Fragments URI specification唯一实现的部分,并且只有是在非地址栏给媒体元素指定来源时才可使用。

2.3 canvas使用教程

<canvas></canvas>html5出现的新标签,像所有的dom对象一样它有自己本身的属性、方法和事件,其中就有绘图的方法,js能够调用它来进行绘图,最近在研读《html5与css3权威指南》下面对其中最好玩的canvas的学习做下读书笔记与实验。

context:context是一个封装了很多绘图功能的对象,获取这个对象的方法是

var context =canvas.getContext(“2d”);

也许这个2d勾起了大家的无限遐想,但是很遗憾的告诉你html5还只是个少女,不提供3d服务。

canvas元素绘制图像的时候有两种方法,分别是

context.fill()//填充
context.stroke()//绘制边框

style:在进行图形绘制前,要设置好绘图的样式

context.fillStyle//填充的样式
context.strokeStyle//边框样式
context.lineWidth//图形边框宽度

颜色的表示方式:

直接用颜色名称:”red” “green” “blue”
十六进制颜色值: “#EEEEFF”
rgb(1-255,1-255,1-255)
rgba(1-255,1-255,1-255,透明度)

和GDI是如此的相像,所以用过GDI的朋友应该很快就能上手

https://www.html.cn/doc/html/ref-canvas/

fillStyle属性设置或返回用于填充绘画的颜色、渐变或模式。默认值为#000000

语法: context.fillStyle=color|gradient|pattern;

描述
color 指示绘图填充色的CSS颜色值。默认值是#000000
gradient 用于填充绘图的渐变对象(线性或放射性)
pattern 用于填充绘图的pattern对象
<!DOCTYPE html>
<html>
<head> <meta charset="utf-8"> </head>
<body>

<canvas id="myCanvas" width="300" height="150" style="border:1px solid #d3d3d3;">您的浏览器不支持 HTML5 canvas标签。</canvas>
<script>
    var c=document.getElementById("myCanvas");
    var ctx=c.getContext("2d");
    ctx.fillStyle="#FF0000";
    ctx.fillRect(20,20,150,100);
</script>

</body></html>

rect()方法创建矩形。

提示:请使用stroke()fill()方法在画布上实际地绘制矩形。

JavaScript语法:context.rect(x,y,width,height);

参数 描述
x 矩形左上角的x坐标
y 矩形左上角的y坐标
width 矩形的宽度,以像素计
height 矩形的高度,以像素计
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body>

<canvas id="myCanvas" width="300" height="150" style="border:1px solid #d3d3d3;">您的浏览器不支持HTML5 canvas标签。</canvas>
<script>
    var c=document.getElementById("myCanvas");
    var ctx=c.getContext("2d");
    ctx.rect(20,20,150,100);
    ctx.stroke();
</script> 

</body></html>

fillRect()方法绘制"已填充"的矩形。默认的填充颜色是黑色。

提示:请使用fillStyle属性来设置用于填充绘图的颜色、渐变或模式。

JavaScript语法:context.fillRect(x,y,width,height);

参数参考rect()方法。

<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body>

<canvas id="myCanvas" width="300" height="150" style="border:1px solid #d3d3d3;">您的浏览器不支持HTML5 canvas标签。</canvas>
<script>
    var c=document.getElementById("myCanvas");
    var ctx=c.getContext("2d");
    ctx.fillRect(20,20,150,100);
</script>

</body></html>

strokeRect()方法绘制矩形(无填充)。笔触的默认颜色是黑色。

提示:请使用strokeStyle属性来设置笔触的颜色、渐变或模式。

JavaScript语法:context.strokeRect(x,y,width,height);

参数值参考rect()方法。

<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body>
<canvas id="myCanvas" width="300" height="150" style="border:1px solid #d3d3d3;">您的浏览器不支持 HTML5 canvas标签。</canvas>
<script>
    var c=document.getElementById("myCanvas");
    var ctx=c.getContext("2d");
    ctx.strokeRect(20,20,150,100);
</script>

</body></html>

clearRect()方法清空给定矩形内的指定像素。

JavaScript语法:context.clearRect(x,y,width,height);

参数值参考rect()方法.

<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body>

<canvas id="myCanvas" width="300" height="150" style="border:1px solid #d3d3d3;">您的浏览器不支持 HTML5 canvas标签。</canvas>
<script>
    var c=document.getElementById("myCanvas");
    var ctx=c.getContext("2d");
    ctx.fillStyle="red";
    ctx.fillRect(0,0,300,150);
    ctx.clearRect(20,20,100,50);
</script>

</body></html>

fill()方法填充当前的图像(路径)。默认颜色是黑色。

提示:请使用fillStyle属性来填充另一种颜色/渐变。

注意:如果路径未关闭,那么fill()方法会从路径结束点到开始点之间添加一条线,以关闭该路径(正如closePath()一样),然后填充该路径。

JavaScript语法:context.fill();

<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body>

<canvas id="myCanvas" width="300" height="150" style="border:1px solid #d3d3d3;">
    您的浏览器不支持 HTML5 canvas标签。
</canvas>
<script>
    var c=document.getElementById("myCanvas");
    var ctx=c.getContext("2d");
    ctx.rect(20,20,150,100);
    ctx.fillStyle="red";
    ctx.fill();
</script> 

</body>
</html>

stroke()方法会实际地绘制出通过moveTo()lineTo()方法定义的路径。默认颜色是黑色。

提示:请使用strokeStyle属性来绘制另一种颜色/渐变。

JavaScript语法:context.stroke();

<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body>

<canvas id="myCanvas" width="300" height="150" style="border:1px solid #d3d3d3;">
您的浏览器不支持HTML5 canvas标签。
</canvas>
<script>
    var c=document.getElementById("myCanvas");
    var ctx=c.getContext("2d");
    ctx.beginPath();
    ctx.moveTo(20,20);
    ctx.lineTo(20,100);
    ctx.lineTo(70,100);
    ctx.strokeStyle="red";
    ctx.stroke();
</script> 

</body></html>

beginPath()方法开始一条路径,或重置当前的路径。

提示:请使用stroke()方法在画布上绘制确切的路径。

JavaScript语法:context.beginPath();

<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body>

<canvas id="myCanvas" width="300" height="150" style="border:1px solid #d3d3d3;">
您的浏览器不支持HTML5 canvas标签。
</canvas>
<script>
    var c=document.getElementById("myCanvas");
    var ctx=c.getContext("2d");
    ctx.beginPath();              
    ctx.lineWidth="5";
    ctx.strokeStyle="green";  // 绿色路径
    ctx.moveTo(0,75);
    ctx.lineTo(250,75);
    ctx.stroke();  // 画 
    ctx.beginPath();
    ctx.strokeStyle="purple";  // 紫色的路径
    ctx.moveTo(50,0);
    ctx.lineTo(150,130);            
    ctx.stroke();  // 画
</script>

</body></html>

drawImage()方法在画布上绘制图像、画布或视频。drawImage()方法也能够绘制图像的某些部分,以及/或者增加或减少图像的尺寸。

JS语法:

在画布上定位图像:context.drawImage(img,x,y);

在画布上定位图像,并规定图像的宽度和高度:context.drawImage(img,x,y,width,height);

剪切图像,并在画布上定位被剪切的部分:context.drawImage(img,sx,sy,swidth,sheight,x,y,width,height);

参数 描述
img 规定要使用的图像、画布或视频
sx 可选,开始剪切的x坐标位置
sy 可选,开始剪切的y坐标位置
swidth 可选,被剪切图像的宽度
sheight 可选,被剪切图像的高度
x 在画布上放置图像的x坐标位置
y 在画布上放置图像的y坐标位置
width 可选。要使用的图像的宽度(伸展或缩小图像)
height 可选,要使用的图像的高度(伸展或缩小图像)
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body>

<p>要使用的图片:</p>
<img id="scream" src="https://img.php.cn/upload/study/001/000/003/5ca2c3e54927e954.jpg">
<p>画布:</p>
<canvas id="myCanvas" width="250" height="300" style="border:1px solid #d3d3d3;">您的浏览器不支持 HTML5 canvas标签。</canvas>
<script>
    var c=document.getElementById("myCanvas");
    var ctx=c.getContext("2d");
    var img=document.getElementById("scream");
    img.onload = function() {
        ctx.drawImage(img,10,10);
    }

    var img = new Image();
    img.src = "https://img.php.cn/upload/study/001/000/003/5ca2c3e54927e954.jpg";
    img.onload = function() {
        ctx.drawImage(img,10,10);
    }
</script>
</body></html>

width属性返回ImageData对象的宽度,以像素计。

JavaScript语法:imgData.width;

height属性返回ImageData对象的高度,以像素计。

JavaScript语法: imgData.height;

data属性返回一个对象,该对象包含指定的ImageData对象的图像数据。

对于ImageData对象中的每个像素,都存在着四方面的信息,即RGBA值:

R - 红色(0-255)
G - 绿色(0-255)
B - 蓝色(0-255)
A - alpha通道(0-255; 0是透明的,255是完全可见的)

color/alpha信息以数组形式存在,并存储于ImageData对象的data属性中。

示例:

<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body>

<canvas id="myCanvas" width="300" height="150" style="border:1px solid #d3d3d3;">您的浏览器不支持HTML5 canvas标签。</canvas>
<script>
    var c=document.getElementById("myCanvas");
    var ctx=c.getContext("2d");
    var imgData=ctx.createImageData(100,100);
    console.log("Height of imgData is: " + imgData.height);
    console("Width of imgData is: " + imgData.width);
    for (var i=0;i<imgData.data.length;i+=4) {
        imgData.data[i+0]=255;
        imgData.data[i+1]=0;
        imgData.data[i+2]=0;
        imgData.data[i+3]=255;
    }
    ctx.putImageData(imgData,10,10);
</script>
</body></html>

createImageData()方法创建新的空白ImageData对象。新对象的默认像素值transparent black。

对于ImageData对象中的每个像素,都存在着四方面的信息,即RGBA值:

R - 红色(0-255)
G - 绿色(0-255)
B - 蓝色(0-255)
A - alpha通道(0-255;0是透明的,255是完全可见的)

因此transparent black表示(0,0,0,0)。

color/alpha信息以数组形式存在,并且由于数组包含了每个像素的四条信息,所以数组的大小是ImageData对象的四倍:width*height*4。(获得数组大小有更简单的办法,就是使用ImageDataObject.data.length)

提示:在操作完成数组中的color/alpha信息之后,您可以使用putImageData()方法将图像数据拷贝回画布上。

JavaScript语法:

以指定的尺寸(以像素计)创建新的ImageData对象:var imgData=context.createImageData(width,height);

创建与指定的另一个ImageData对象尺寸相同的新ImageData对象(不会复制图像数据):var imgData=context.createImageData(imageData);

参数 描述
width ImageData对象的宽度,以像素计
height ImageData对象的高度,以像素计
imageData 另一个ImageData对象
<!DOCTYPE html>
<html>
<head> <meta charset="utf-8"> </head>
<body>

<canvas id="myCanvas" width="300" height="150" style="border:1px solid #d3d3d3;">您的浏览器不支持HTML5 canvas标签。</canvas>
<script>
    var c=document.getElementById("myCanvas");
    var ctx=c.getContext("2d");
    var imgData=ctx.createImageData(100,100);
    for (var i=0;i<imgData.data.length;i+=4) {
        imgData.data[i+0]=255;
        imgData.data[i+1]=0;
        imgData.data[i+2]=0;
        imgData.data[i+3]=255;
    }
    ctx.putImageData(imgData,10,10);
</script>

</body></html>

getImageData()方法返回ImageData对象,该对象拷贝了画布指定矩形的像素数据。

注意:ImageData对象不是图像,它规定了画布上一个部分(矩形),并保存了该矩形内每个像素的信息。

JavaScript语法: context.getImageData(x,y,width,height);

参数 描述
x 开始复制的左上角位置的x坐标(以像素计)
y 开始复制的左上角位置的y坐标(以像素计)
width 要复制的矩形区域的宽度
height 要复制的矩形区域的高度
<!DOCTYPE html>
<html>
<head> <meta charset="utf-8"> </head>
<body>

<canvas id="myCanvas" width="300" height="150" style="border:1px solid #d3d3d3;">您的浏览器不支持HTML5 canvas标签。</canvas>
<script>
    var c=document.getElementById("myCanvas");
    var ctx=c.getContext("2d");
    ctx.fillStyle="red";
    ctx.fillRect(10,10,50,50);
    function copy() {
        var imgData=ctx.getImageData(10,10,50,50);
        ctx.putImageData(imgData,10,70);
    }
</script>
<button onclick="copy()">复制</button>

</body></html>

putImageData()方法将给定ImageData对象的数据绘制到画布上。如果提供矩形,则仅绘制该矩形的像素。此方法不受画布变换矩阵的影响。

JavaScript语法

void ctx.putImageData(imageData,dx,dy);
void ctx.putImageData(imageData,dx,dy,dirtyX,dirtyY,dirtyWidth,dirtyHeight);
参数 描述
imageData 一个ImageData包含的像素值的阵列对象
dx 将图像数据放置在目标画布中的水平位置(x坐标)
dy 将图像数据放置在目标画布中的垂直位置(y坐标)
dirtyX 可选的将从中提取图像数据的左上角的水平位置(x坐标),默认为0
dirtyY 可选的将从中提取图像数据的左上角的垂直位置(y坐标),默认为0
dirtyWidth 可选的要绘制的矩形的宽度。默认为图像数据的宽度
dirtyHeight 可选的要绘制的矩形的高度。默认为图像数据的高度
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>html中文网</title>
</head>
<body>
    <canvas id="canvas"></canvas>
    <script>
        var canvas = document.getElementById('canvas');
        var ctx = canvas.getContext('2d');
        function putImageData(ctx, imageData, dx, dy,
            dirtyX, dirtyY, dirtyWidth, dirtyHeight) {
            var data = imageData.data;
            var height = imageData.height;
            var width = imageData.width;
            dirtyX = dirtyX || 0;
            dirtyY = dirtyY || 0;
            dirtyWidth = dirtyWidth !== undefined? dirtyWidth: width;
            dirtyHeight = dirtyHeight !== undefined? dirtyHeight: height;
            var limitBottom = dirtyY + dirtyHeight;
            var limitRight = dirtyX + dirtyWidth;
            for (var y = dirtyY; y < limitBottom; y++) {
                for (var x = dirtyX; x < limitRight; x++) {
                    var pos = y * width + x;
                    ctx.fillStyle = 'rgba(' + data[pos*4+0]
                        + ',' + data[pos*4+1]
                        + ',' + data[pos*4+2]
                        + ',' + (data[pos*4+3]/255) + ')';
                    ctx.fillRect(x + dx, y + dy, 1, 1);
                }
            }
        }
        ctx.fillRect(0, 0, 100, 100);
        var imagedata = ctx.getImageData(0, 0, 100, 100);
        putImageData(ctx, imagedata, 150, 0, 50, 50, 25, 25);
</script>
</body></html>

二、css教程

2.1 盒子模型

当对一个文档进行布局(lay out)的时候,浏览器的渲染引擎会根据标准之一的CSS基础框盒模型(CSS basic box model),将所有元素表示为一个个矩形的盒子(box)。CSS决定这些盒子的大小、位置以及属性(例如颜色、背景、边框尺寸...)。

每个盒子由四个部分(或称区域)组成,其效用由它们各自的边界(Edge)所定义。如图,与盒子的四个组成区域相对应,每个盒子有四个边界:内容边界(Content edge)、内边距边界(Padding Edge)、边框边界(Border Edge)、外边框边界(Margin Edge)。

在CSS盒子模型(Box Model)规定了元素处理元素的几种方式:

widthheight:内容的宽度、高度(不是盒子的宽度、高度)

padding:内边距

border:边框

margin:外边距

CSS盒模型和IE盒模型的区别:

  1. 在标准盒子模型中,width和height指的是内容区域的宽度和高度。增加内边距、边框和外边距不会影响内容区域的尺寸,但是会增加元素框的总尺寸。
  2. IE盒子模型中,width和height指的是内容区域+border+padding的宽度和高度。

<body>标签有必要强调一下。很多人以为<body>标签占据的是整个页面的全部区域,其实是错误的,正确的理解是这样的:整个网页最大的盒子是<document>,即浏览器。而<body><document>的儿子。浏览器给<body>默认的margin大小是8个像素,此时<body>占据了整个页面的一大部分区域,而不是全部区域。来看一段代码。

<style type="text/css">
    div{
        width: 100px;
        height: 100px;
        border: 1px solid red;
        padding: 20px;
        margin: 30px;
    }
</style>
<div>有生之年</div>
<div>狭路相逢</div>

上面的代码中,我们对div标签设置了边距等信息。打开google浏览器,按住F12,显示效果如下:

下面这两个盒子,真实占有宽高,都是302*302:

// 盒子1:
.box1{
   width: 100px;
   height: 100px;
   padding: 100px;
   border: 1px solid red;
}
// 盒子2:
.box2{
    width: 250px;
   height: 250px;
   padding: 25px;
   border: 1px solid red;
}

上面这两个盒子的盒模型图如下:

padding区域也有颜色,padding就是内边距。padding的区域有背景颜色,css2.1前提下,并且背景颜色一定和内容区域的相同。也就是说,background-color将填充所有border以内的区域。效果如下:

padding是4个方向的,所以我们能够分别描述4个方向的padding。方法有两种,第一种写小属性;第二种写综合属性,用空格隔开。小属性的写法:

padding-top: 30px;
padding-right: 20px;
padding-bottom: 40px;
padding-left: 100px;

综合属性的写法:(上、右、下、左)(顺时针方向,用空格隔开。margin的道理也是一样的)

padding:30px 20px 40px 100px;

如果写了四个值,则顺序为:上、右、下、左。如果只写了三个值,则顺序为:上、右、下。左和右一样。如果只写了两个值,比如说:padding: 30px 40px;,则顺序等价于:30px 40px 30px 40px;。要懂得,用小属性层叠大属性。比如:

padding: 20px;
padding-left: 30px;

上面的padding对应盒子模型为:

下面的写法:

padding-left: 30px;
padding: 20px;

第一行的小属性无效,因为被第二行的大属性层叠掉了。一些元素,默认带有padding,比如ul标签。如下:

上图显示,不加任何样式的ul,也是有40px的padding-left。所以,我们做站的时候,为了便于控制,总是喜欢清除这个默认的padding。

可以使用*进行清除:

*{
    margin: 0;
    padding: 0;
}

但是,*的效率不高,所以我们使用并集选择器,罗列所有的标签(不用背,有专业的清除默认样式的样式表):

body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td{
    margin:0;
    padding:0;
}

border就是边框。边框有三个要素:像素(粗细)、线型、颜色。颜色如果不写,默认是黑色。另外两个属性不写,要命了,显示不出来边框。

border-style:border的所有的线型如下:

比如border:10px ridge red;这个属性,在chrome和firefox、IE中有细微差别:(因为可以显示出效果,因此并不是兼容性问题,只是有细微差别而已)

如果追求极高的页面还原度,那么不能使用css来制作边框。就要用到图片,就要切图了。所以,比较稳定的border-style就几个:solid、dashed、dotted。

border拆分:border是一个大综合属性。比如说:border:1px solid red;,就是把4个边框,都设置为1px宽度、线型实线、red颜色。

border属性是能够被拆开的,有两大种拆开的方式:

  1. 按三要素拆开:border-width、border-style、border-color。(一个border属性是由三个小属性综合而成的)
  2. 按方向拆开:border-top、border-right、border-bottom、border-left。

现在我们明白了:一个border属性,是由三个小属性综合而成的。如果某一个小属性后面是空格隔开的多个值,那么就是上右下左的顺序。举例如下:

border-width:10px 20px;
border-style:solid dashed dotted;
border-color:red green blue yellow;

效果如下:

按三要素拆:

border-width:10px;    //边框宽度
border-style:solid;   //线型
border-color:red;     //颜色。

等价于:

border:10px solid red;

按方向来拆:

border-top:10px solid red;
border-right:10px solid red;
border-bottom:10px solid red;
border-left:10px solid red;

等价于:

border:10px solid red;

按三要素和方向来拆: (就是把每个方向的,每个要素拆开。3*4 = 12)

border-top-width:10px;
border-top-style:solid;
border-top-color:red;
border-right-width:10px;
border-right-style:solid;
border-right-color:red;
border-bottom-width:10px;
border-bottom-style:solid;
border-bottom-color:red;
border-left-width:10px;
border-left-style:solid;
border-left-color:red;

等价于:

border:10px solid red;

用小属性层叠大属性: 举例如下:

为了实现上方效果,写法如下:

border:10px solid red;
border-right-color:blue;

为了实现上方效果,写法如下:

border:10px solid red;
border-style:solid dashed;

border可以没有:border:none;,可以某一条边没有:border-left: none;,也可以调整左边边框的宽度为0:border-left-width: 0;

举例:利用border属性画一个三角形(小技巧) 步骤如下:

  1. 当我们设置盒子的width和height为0时,此时效果如下:

  1. 然后将border的底部取消

  1. 最后设置border的左边和右边为白色:

定位(position)有四个值:static,relative,absolutefixed

static: 一般如果我们不设置position的话它的默认值就是static,这个时候lefttopbottomright是不起作用的,现在有如下两个div,他们的关系是兄弟关系:

.red{
    width:200px;
    height:200px;
    left:100px;
    top:100px;
    background-color:red;
}
.blue{
    width:100px;
    height:100px;
    background-color:blue;
}

效果图:

可以发现,并没有什么变化。红色方块的lefttop加不加都一样。

relative: 给红色方块加个position:relative:

.red{
    position:relative;
    width:200px;
    height:200px;
    left:100px;
    top:100px;
    background-color:red;
}

嘿嘿,变这样了:

可以发现,红色方块跑到蓝色方块的右边了,左边缘和顶边缘都距离原来100px,但是蓝色方块还是在原来的地方不动,现在可以得出一个结论:使用相对定位给元素加left/top/right/buttom元素会以原来的位置为基础加上这些值,即以原来的位置为基础定位,并且没有脱离文档流

absolute:position改为absolute:

.red{
    position:absolute;
    width:200px;
    height:200px;
    left:100px;
    top:100px;
    background-color:red;
}

效果图:

可以发现蓝色方块如我们所愿移动到了红色方块的上面,说明红色方块已经脱离文档流。虽然红色方块的位移和relative一样但是,红色方块位移的参考不再是原来的位置而是body只不过红色方块的位置刚好在body的最左上角,刚好碰巧位移一样,上面的这个例子可能看不出来,让我们来改改代码。

首先将html结构改成:

<div class="red">
    <div class="blue"></div>
</div>

然后红色方块的absolute改回relative

效果图:

然后蓝色方块代码改为:

.blue{
    position:absolute;
    top:100px;
    left:100px;
    width:100px;
    height:100px;
    background-color:blue;
}

可以发现在给蓝色方块添加position:absolute前,蓝色方块像我们想的那样在红色方块的左上角;当我们给蓝色方块添加position:absolute并且添加lefttop时,蓝色方块就跑到了红色方块的右下角。

那么这次这个蓝色方块是以谁为参考进行位移的?刚刚你说的以body为参考又是什么情况?

好,我这里就给大家说清楚,当给一个元素设置position:absolute时,这个元素的位置就是以他父代元素position不为static的元素作为参考,如果他的父代元素都是position:static那就以body作为参考。刚刚红色方块的情况就是他父代元素没有position不为static的元素,所以只能以body作为参考。

fixed: 现在让我们再来创建一个绿色方块:

.green{
    position:fixed;
    top:150px;
    left:150px;
    width:100px;
    height:100px;
    background-color:green;
}

效果图:

这个看起来貌似没有什么特别的,现在我们来给body加个height:2000px(这个高度随意,但是要使浏览器右边出现滚动条),然后把浏览器的滚动条往下拉,一个神奇的事情发生了:绿色方块固定在我们定义的位置上屏幕上不动了!

这个fixed我们见得最多的就是网页中顽固的小广告,不管我们怎么拖拽滚动条,它总是固定在那,就是一个升级版的absolute

z-index: 说到定位,肯定少不了z-index。用上面的例子来说z-index就是灵魂飘的高度,设置得越大,自然就飘得越高,既然扯到了灵魂,z-index肯定是对活人(static)无效的了。

正常情况下(没有加z-index),元素是按照后来居上原则进行堆叠。在这个例子上的html元素是这样的:

<div class="red">
        <div class="blue"></div>
    </div>
<div class="green"></div>

按照后来居上原则,红色方块最先被浏览器渲染到,所以在最下面,其次到蓝色,最后到绿色。如果我们想让红色方块显示到前面我们可以给它加个z-index:1,结果:

发现绿色方块“消失了”。其实绿色方块并没有消失,你可以将滚动条往下拉,或者看盒子模型:

可以发现它并没有消失,只是被盖在了红色方块下面。由于红色方块与蓝色方块是父子关系,红色的上来了,蓝色肯定的上来啊。

2.2 css基本属性

2.2.1 背景属性

CSS背景属性主要有以下几个

该属性设置背景颜色

<div class="box"></div>
.box{
    width: 300px;
    height: 300px;
    background-color: palevioletred;
}

设置元素的背景图像。元素的背景是元素的总大小,包括填充和边界(不包括外边距)。默认情况下background-image属性放置在元素的左上角,如果图像不够大的话会在垂直和水平方向平铺图像,如果图像大小超过元素大小从图像的左上角显示元素大小的那部分。

<div class="box"></div>
.box{
    width: 600px;
    height: 600px;
    background-image: url("images/img1.jpg");
}

该属性设置如何平铺背景图像

.box{
    width: 600px;
    height: 600px;
    background-color: #fcc;
    background-image: url("images/img1.jpg");
    background-repeat: no-repeat;
}

该属性设置背景图像的大小

.box{
    width: 600px;
    height: 600px;
    background-image: url("images/img1.jpg");
    background-repeat: no-repeat;
    background-size: 100% 100%;
}

该属性设置背景图像的起始位置,其默认值是:0% 0%。

.box{
    width: 600px;
    height: 600px;
    background-color: #fcc;
    background-image: url("images/img1.jpg");
    background-repeat: no-repeat;
    background-position: center;
}

该属性设置背景图像是否固定或者随页面滚动。简单来说就是一个页面有滚动条的话,滑动滚动条背景是固定的还是随页面滑动的。

属性值:

background简写属性在一个声明中设置所有的背景属性。可以设置如下属性:

background-color
background-position
background-size
background-repeat
background-attachment
background-image

如果不设置其中的某个值,也不会出问题,取默认值,比如:background:url('smiley.gif') no-repeat; 也是允许的

2.2.2 字体属性

CSS字体属性定义字体,加粗,大小,文字样式。

规定文本的颜色

div{ color:red;}
div{ color:#ff0000;}
div{ color:rgb(255,0,0);}
div{ color:rgba(255,0,0,.5);}

设置文本的大小,字体大小的值可以是绝对或相对的大小。如果你不指定一个字体的大小,默认大小和普通文本段落一样,是16像素(16px=1em)。

h1 {font-size:40px;}
h2 {font-size:30px;}
p {font-size:14px;}

设置文本的粗细

H1 {font-weight:normal;}
div{font-weight:bold;}
p{font-weight:900;}

指定文本的字体样式

font-family属性指定一个元素的字体,font-family可以把多个字体名称作为一个"回退"系统来保存。如果浏览器不支持第一个字体,则会尝试下一个。

注意:每个值用逗号分开,如果字体名称包含空格,它必须加上引号。

font-family:"Microsoft YaHei","Simsun","SimHei";

2.2.3 文本属性

指定元素文本的水平对齐方式。

h1 {text-align:center}
h2 {text-align:left}
h3 {text-align:right}

text-decoration属性规定添加到文本的修饰,下划线、上划线、删除线等。

h1 {text-decoration:overline}
h2 {text-decoration:line-through}
h3 {text-decoration:underline}

text-transform属性控制文本的大小写。

h1 {text-transform:uppercase;}
h2 {text-transform:capitalize;}
p {text-transform:lowercase;}

text-indent属性规定文本块中首行文本的缩进。

注意:负值是允许的,如果值是负数,将第一行左缩进。

p{
    text-indent:50px;
}

2.2.4 列表属性

在HTML中,有两种类型的列表:

无序列表:列表项标记用特殊图形(如小黑点、小方框等)

有序列表:列表项的标记有数字或字母

list-style-type属性设置列表项标记的类型。

ul.a {list-style-type: circle;}
ul.b {list-style-type: square;}

list-style-image属性使用图像来替换列表项的标记。

ul { list-style-image: url('sqpurple.gif'); }

list-style-position属性指示如何相对于对象的内容绘制列表项标记。

ul { list-style-position: inside; }

list-style简写属性在一个声明中设置所有的列表属性。可以设置的属性(按顺序):list-style-type,list-style-position,list-style-image。可以不设置其中的某个值,比如"list-style:circle inside;" 也是允许的。未设置的属性会使用其默认值。

ul { list-style: none;}

2.2.5 表格属性

使用CSS可以使HTML表格更美观。

指定CSS表格边框,使用border属性。

下面的例子指定了一个表格的Th和TD元素的黑色边框:

table, th, td { border: 1px solid black; }

border-collapse属性设置表格的边框是否被折叠成一个单一的边框或隔开:

table { border-collapse:collapse; }
table,th, td { border: 1px solid black; }

Width和height属性定义表格的宽度和高度。下面的例子是设置100%的宽度,50像素的th元素的高度的表格:

table { width:100%; } 
th { height:50px; }

表格中的文本对齐和垂直对齐属性。text-align属性设置水平对齐方式,向左,右,或中心;

td { text-align:right; }
td { height:50px; vertical-align:bottom; } # 垂直对齐属性设置垂直对齐,比如顶部,底部或中间:

如果在表的内容中控制空格之间的边框,应使用td和th元素的填充属性:td { padding:15px; }

下面的例子指定边框的颜色,和th元素的文本和背景颜色:

table, td, th { border:1px solid green; } 
th { background-color:green; color:white; }

2.2.7 其他属性

letter-spacing属性增加或减少字符间的空白(字符间距)

h1 {letter-spacing:2px}
h2 {letter-spacing:-3px}

css选择器和常用属-性行高

设置行高,负值是不允许的

p{
  height: 30px;
  line-height: 30px;
}

white-space属性指定元素内的空白怎样处理。

p {
white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
}

vertical-align属性设置一个元素的垂直对齐方式。该属性定义行内元素的基线相对于该元素所在行的基线的垂直对齐。允许指定负长度值和百分比值。这会使元素降低而不是升高。在表单元格中,这个属性会设置单元格框中的单元格内容的对齐方式。

img{ vertical-align:middle; }

2.3、css选择符

2.3.1 元素选择符

选择符 名称 说明
*{ sRules } 通配选择符 选定所有对象
E{ sRules } 类型选择符 以文档语言对象类型作为选择符
E#myid{ sRules } ID选择符 以唯一标识符id属性等于myidE对象作为选择符
E.myclass{ sRules } 类选择符 class属性包含myclassE对象作为选择符,类选择符可以同时定义多个,如:.a.b{color:#f00;}

2.3.2 关系选择符

选择符 名称 介绍
E F{ sRules } 包含选择符 选择所有被E元素包含的F元素
E>F{ sRules } 子选择符 选择所有作为E元素的子元素F,与包含选择符不同的是,子选择符只能命中子元素,而不能命中孙辈
E+F{ sRules } 相邻选择符 选择紧贴在E元素之后F元素,与兄弟选择符不同的是,相邻选择符只会命中符合条件的相邻的兄弟元素
E~F{ sRules } 兄弟选择符 选择E元素后面的所有兄弟元素F

2.3.3 属性选择符

选择符 简介
E[att]{ sRules } 选择具有att属性的E元素,如img[alt]{margin:10px;}
E[att="val"]{ sRules } 选择具有att属性且属性值等于valE元素
E[att~="val"]{ sRules } 选择具有att属性且属性值为一用空格分隔的字词列表,其中一个等于valE元素(包含只有一个值且该值等于val的情况)
E[att^="val"]{ sRules } 选择具有att属性且属性值为以val开头的字符串的E元素
E[att$="val"]{ sRules } 选择具有att属性且属性值为以val结尾的字符串的E元素
E[att*="val"]{ sRules } 选择具有att属性且属性值为包含val的字符串的E元素
E[att|="val"]{ sRules } 选择具有att属性且属性值为以val开头并用连接符"-"分隔的字符串的E元素

2.3.4 伪类选择符

选择符 简介
E:link{ sRules } 设置超链接a在未被访问前的样式,如a:link{}
E:visited{ sRules } 设置超链接a在其链接地址已被访问过时的样式,如a:visited{}
E:hover{ sRules } 设置元素在其鼠标悬停时的样式,如a:hover{}
E:active{ sRules } 设置元素在被用户激活(在鼠标点击与释放之间发生的事件)时的样式,如a:active{}。如果需要给超链接定义:访问前,鼠标悬停,当前被点击,已访问这4种伪类效果,而又没有按照一致的书写顺序,不同的浏览器可能会有不同的表现,a:link{};a:visited{};a:hover{};a:active{}
E:focus{ sRules } 设置对象在成为输入焦点(该对象的onfocus事件发生)时的样式
E:lang(fr){ sRules } 匹配使用特殊语言的E元素,如p:lang(zh-cn){color:#f00;}
E:not(s){ sRules } 匹配不含有s选择符的元素E
E:root{ sRules } 匹配E元素在文档的根元素,在HTML中,根元素永远是HTML
E:first-child{ sRules } 匹配父元素的第一个子元素,要使该属性生效,E元素必须是某个元素的子元素,E的父元素最高是body,即E可以是body的子元素,是同一级别,如li:first-child{sRules}
E:last-child{ sRules } 匹配父元素的最后一个子元素E
E:only-child{ sRules } 匹配父元素仅有的一个子元素E
E:nth-child(n){ sRules } 匹配父元素的第n个子元素E,假设该子元素不是E,则选择符无效。要使该属性生效,E元素必须是某个元素的子元素,E的父元素最高是body,即E可以是body的子元素.该选择符允许使用一个乘法因子(n)来作为换算方式,比如我们想选中所有的偶数子元素E,那么选择符可以写成:nth-child(2n)
E:nth-last-child(n){ sRules } 匹配父元素的倒数第n个子元素E,假设该子元素不是E,则选择符无效.要使该属性生效,E元素必须是某个元素的子元素,E的父元素最高是body,即E可以是body的子元素.该选择符允许使用一个乘法因子(n)来作为换算方式,比如我们想选中倒数第一个子元素E,那么选择符可以写成:nth-last-child(1)
E:first-of-type{ sRules } 匹配同类型中的第一个同级兄弟元素E.要使该属性生效,E元素必须是某个元素的子元素,E的父元素最高是html,即E可以是html的子元素,也就是说E可以是body.该选择符总是能命中父元素的第1个为E的子元素,不论第1个子元素是否为E
E:last-of-type{ sRules } 匹配同类型中的最后一个同级兄弟元素E
E:only-of-type{ sRules } 匹配同类型中的唯一的一个同级兄弟元素E
E:nth-of-type(n){ sRules } 匹配同类型中的第n个同级兄弟元素E
E:nth-last-of-type(n){ sRules } 匹配同类型中的倒数第n个同级兄弟元素E
E:empty{ sRules } 匹配没有任何子元素(包括text节点)的元素E
E:checked{ sRules } 匹配用户界面上处于选中状态的元素E,用于input typeradiocheckbox时,如input:checked+span{background:#f00;}
E:enabled{ sRules } 匹配用户界面上处于可用状态的元素E
E:disabled{ sRules } 匹配用户界面上处于禁用状态的元素E
E:target{ sRules } 匹配相关URL指向的E元素,URL后面跟锚点#,指向文档内某个具体的元素.这个被链接的元素就是目标元素(target element),:target选择器用于选取当前活动的目标元素
@page :first{ sRules } 设置在打印时页面容器第一页使用的样式,仅用于@page规则.该伪类选择符只允许定义margin,orphans,widowspage breaks相关属性
@page :left{ sRules } 设置页面容器位于装订线左边的所有页面使用的样式,仅用于@page规则,该伪类选择符只允许定义margin,padding,borderbackground属性
@page :right{ sRules } 设置页面容器位于装订线右边的所有页面使用的样式,仅用于@page规则,该伪类选择符只允许定义margin,padding,border和background属性

2.2.5 伪对象选择符

选择符 简介
E:first-letter{ sRules } 设置对象内的第一个字符的样式,此伪对象仅作用于块对象。内联对象要使用该伪对象,必须先将其设置为块级对
E::first-line{ sRules } 设置对象内的第一行的样式,此伪对象仅作用于块对象,内联对象要使用该伪对象,必须先将其设置为块级对象
E:before{ sRules } 设置在对象前(依据对象树的逻辑结构)发生的内容,用来和content属性一起使用,并且必须定义content属性
E:after{ sRules } 设置在对象后(依据对象树的逻辑结构)发生的内容,用来和content属性一起使用,并且必须定义content属性
E:selection{ sRules } 设置对象被选择时的样式

CSS3将伪对象选择符(Pseudo-Element Selectors)前面的单个冒号(:)修改为双冒号(::)用以区别伪类选择符(Pseudo-Classes Selectors).

三、javascript教程

3.1 Javascript教程

3.2 Browser对象

3.2.1 Window对象

Window对象表示浏览器中打开的窗口,如果文档包含框架(<frame><iframe>标签),浏览器会为HTML文档创建一个window对象,并为每个框架创建一个额外的window对象。

属性 描述
closed 返回窗口是否已被关闭
defaultStatus 设置或返回窗口状态栏中的默认文本
document Document对象的只读引用
frames 返回窗口中所有命名的框架,该集合是Window对象的数组,每个Window对象在窗口中含有一个框架
history History对象的只读引用
innerHeight 返回窗口的文档显示区的高度
innerWidth 返回窗口的文档显示区的宽度
localStorage 在浏览器中存储key/value对,没有过期时间
length 设置或返回窗口中的框架数量
location 用于窗口或框架的Location对象
name 设置或返回窗口的名称
navigator Navigator对象的只读引用
opener 返回对创建此窗口的窗口的引用
outerHeight 返回窗口的外部高度,包含工具条与滚动条
outerWidth 返回窗口的外部宽度,包含工具条与滚动条
pageXOffset 设置或返回当前页面相对于窗口显示区左上角的X位置
pageYOffset 设置或返回当前页面相对于窗口显示区左上角的Y位置
parent 返回父窗口
screen Screen对象的只读引用
screenLeft 返回相对于屏幕窗口的x坐标
screenTop 返回相对于屏幕窗口的y坐标
screenX 返回相对于屏幕窗口的x坐标
sessionStorage 在浏览器中存储key/value对,在关闭窗口或标签页之后将会删除这些数据
screenY 返回相对于屏幕窗口的y坐标
self 返回对当前窗口的引用,等价于Window属性
status 设置窗口状态栏的文本
top 返回最顶层的父窗口
方法 描述
alert() 显示带有一段消息和一个确认按钮的警告框,alert("Hello\nHow are you?");
atob() 解码一个base-64编码的字符串,window.atob("UlVOT09C")
btoa() 创建一个base-64编码的字符串,window.btoa("RUNOOB")
blur() 把键盘焦点从顶层窗口移开,window.blur();
clearInterval() 取消由setInterval()设置的timeout,window. clearInterval(item);
clearTimeout() 取消由setTimeout()方法设置的timeout,window.clearTimeout(item);
close() 关闭浏览器窗口,windwo.close();
confirm() 显示带有一段消息以及确认按钮和取消按钮的对话框,var r=confirm("按下按钮!");if(r==true){x="你按下了\"确定\"按钮!";}else{x="你按下了\"取消\"按钮!";}
focus() 把键盘焦点给予一个窗口,window.focus();
getSelection() 返回一个Selection对象,表示用户选择的文本范围或光标的当前位置
getComputedStyle() 获取指定元素的CSS样式,window.getComputedStyle(element,pseudoElement)
matchMedia() 该方法用来检查media query语句,它返回一个MediaQueryList对象,window.matchMedia("(max-width: 700px)")
moveBy() 可相对窗口的当前坐标把它移动指定的像素,window.moveBy(x,y)
moveTo() 把窗口的左上角移动到一个指定的坐标,window.moveTo(x,y)
open() 打开一个新的浏览器窗口或查找一个已命名的窗口,window.open(URL,name,specs,replace)
URL:可选,打开指定的页面的URL,如果没有指定URL,打开一个新的空白窗口
name:可选,指定target属性或窗口的名称,可以选的值
 - _blank:URL加载到一个新的窗口,默认
 - _parent:URL加载到父框架
 - _self:URL替换当前页面
 - _top:URL替换任何可加载的框架集
name:窗口名称
specs:可选,一个逗号分隔的项目列表,支持以下值:
 - channelmode=yes|no|1|0:是否要在影院模式显示window,默认是没有的,仅限IE浏览器
 - directories=yes|no|1|0:是否添加目录按钮,默认是肯定的,仅限IE浏览器
 - fullscreen=yes|no|1|0:浏览器是否显示全屏模式,默认是没有的,在全屏模式下的window,还必须在影院模式,仅限IE浏览器
 - height=pixels:窗口的高度,最小值为100
 - left=pixels:该窗口的左侧位置
 - location=yes|no|1|0:是否显示地址字段,默认值是yes
 - menubar=yes|no|1|0:是否显示菜单栏,默认值是yes
 - resizable=yes|no|1|0:是否可调整窗口大小,默认值是yes
 - scrollbars=yes|no|1|0:是否显示滚动条,默认值是yes
 - status=yes|no|1|0:是否要添加一个状态栏,默认值是yes
 - titlebar=yes|no|1|0:是否显示标题栏,被忽略,除非调用HTML应用程序或一个值得信赖的对话框.默认值是yes
 - toolbar=yes|no|1|0:是否显示浏览器工具栏,默认值是yes
 - top=pixels:窗口顶部的位置,仅限IE浏览器
 - width=pixels:窗口的宽度,最小值为100
* replace:Optional,Specifies规定了装载到窗口的URL是在窗口的浏览历史中创建一个新条目,还是替换浏览历史中的当前条目,支持下面的值:true,URL替换浏览历史中的当前条目和false,URL在浏览历史中创建新的条目
print() 打印当前窗口的内容,window.print()
prompt() 显示可提示用户输入的对话框,window.prompt(msg,defaultText)
resizeBy() 按照指定的像素调整窗口的大小,window.resizeBy(width,height),可以为正,也可以为负
resizeTo() 把窗口的大小调整到指定的宽度和高度,window.resizeTo(width,height),像素单位
scrollBy() 按照指定的像素值来滚动内容,window.scrollBy(xnum,ynum)
scrollTo() 把内容滚动到指定的坐标,window.scrollTo(xpos,ypos)
setInterval() 按照指定的周期(以毫秒计)来调用函数或计算表达式,window.setInterval(function(){ alert("Hello");},3000);
setTimeout() 在指定的毫秒数后调用函数或计算表达式,window.setTimeout(function(){alert("Hello");},3000);
stop() 停止页面载入,window.stop()

3.2.2 Navigator对象

Navigator对象包含有关浏览器的信息。

属性 说明
appCodeName 返回浏览器的代码名
appName 返回浏览器的名称
appVersion 返回浏览器的平台和版本信息
cookieEnabled 返回指明浏览器中是否启用cookie的布尔值
platform 返回运行浏览器的操作系统平台
userAgent 返回由客户机发送服务器的user-agent头部的值

Navigator对象方法

方法 描述
javaEnabled() 指定是否在浏览器中启用Java,navigator.javaEnabled()

3.2.3 Screen对象

Screen对象包含有关客户端显示屏幕的信息

属性 说明
availHeight 返回屏幕的高度(不包括Windows任务栏)
availWidth 返回屏幕的宽度(不包括Windows任务栏)
colorDepth 返回目标设备或缓冲器上的调色板的比特深度
height 返回屏幕的总高度
pixelDepth 返回屏幕的颜色分辨率(每象素的位数)
width 返回屏幕的总宽度

3.2.4 History对象

History对象包含用户(在浏览器窗口中)访问过的URL,History对象是window对象的一部分,可通过window.history属性对其进行访问。

属性 说明
length 返回历史列表中的网址数
方法 说明
back() 加载history列表中的前一个URL,history.back()
forward() 加载history列表中的下一个URL,history.forward()
go() 加载history列表中的某个具体页面,history.go(number|URL)

3.2.5 Location对象

Location对象包含有关当前URL的信息,Location对象是window对象的一部分,可通过window.Location属性对其进行访问。

属性 描述
hash 返回一个URL的锚部分
host 返回一个URL的主机名和端口
hostname 返回URL的主机名
href 返回完整的URL
pathname 返回的URL路径名
port 返回一个URL服务器使用的端口号
protocol 返回一个URL协议
search 返回一个URL的查询部分
方法 说明
assign() 载入一个新的文档,location.assign(URL)
reload() 重新载入当前文档,location.reload(forceGet)
replace() 用新的文档替换当前文档,location.replace(newURL)

3.2.6 存储对象

Web存储API提供了sessionStorage(会话存储)和localStorage(本地存储)两个存储对象来对网页的数据进行添加、删除、修改、查询操作。

  1. localStorage用于长久保存整个网站的数据,保存的数据没有过期时间,直到手动去除。
  2. sessionStorage用于临时保存同一窗口(或标签页)的数据,在关闭窗口或标签页之后将会删除这些数据。
属性 描述
length 返回存储对象中包含多少条数据。
方法 描述
key(n) 返回存储对象中第n个键的名称
getItem(keyname) 返回指定键的值
setItem(keyname,value) 添加键和值,如果对应的值存在,则更新该键对应的值
removeItem(keyname) 移除键
clear() 清除存储对象中所有的键

3.3 事件解读

3.3.1 事件基本概念

事件是指在文档或者浏览器中发生的一些特定交互瞬间,比如打开某一个网页,浏览器加载完成后会触发load事件,当鼠标悬浮于某一个元素上时会触发hover事件,当鼠标点击某一个元素时会触发click事件等等。事件处理就是当事件被触发后,浏览器响应这个事件的行为,而这个行为所对应的代码即为事件处理程序。

3.3.2 事件操作:监听与移除监听

浏览器会根据一些事件作出相对应的事件处理,事件处理的前提是需要监听事件,监听事件的方法主要有以下三种:

即在HTML元素里直接填写与事件相关的属性,属性值为事件处理程序。示例如下:

<button onclick="console.log('You clicked me!');"></button>

onclick对应着click事件,所以当按钮被点击后,便会执行事件处理程序,即控制台输出You clicked me!。

不过我们需要指出的是,这种方式将HTML代码与JavaScript代码耦合在一起,不利于代码的维护,所以应该尽量避免使用这样的方式。

通过直接设置某个DOM节点的属性来指定事件和事件处理程序,上代码:

const btn = document.getElementById("btn");
btn.onclick = function(e) {
    console.log("You clicked me!");
};

上面示例中,首先获得btn这个对象,通过给这个对象添加onclick属性的方式来监听click事件,这个属性值对应的就是事件处理程序。这段程序也被称作DOM0级事件处理程序。

标准的事件监听函数如下:

const btn = document.getElementById("btn");
btn.addEventListener("click", () => {
    console.log("You clicked me!");
}, false);

上面的示例表示先获得表示节点的btn对象,然后在这个对象上面添加了一个事件监听器,当监听到click事件发生时,则调用回调函数,即在控制台输出You clicked me!addEventListener函数包含了三个参数false,第三个参数的含义在后面的事件触发三个阶段之后再讲解。这段程序也被称作DOM2级事件处理程序。IE9+、FireFox、Safari、Chrome和Opera都是支持DOM2级事件处理程序的

在为某个元素绑定了一个事件后,如果想接触绑定,则需要用到removeEventListener方法。看如下例子:

const handler = function() {
    // handler logic
}
const btn = document.getElementById("btn");

btn.addEventListener("click", handler);
btn.removeEventListener("click", handler);

需要注意的是,绑定事件的回调函数不能是匿名函数,必须是一个已经被声明的函数,因为解除事件绑定时需要传递这个回调函数的引用。

3.3.3 事件触发过程

事件流描述了页面接收事件的顺序。现代浏览器事件流包含三个过程,分别是捕获阶段、目标阶段和冒泡阶段,下图形象地说明这个过程:

下面就详细地讲解这三个过程。

当我们对DOM元素进行操作时,比如鼠标点击、悬浮等,就会有一个事件传输到这个DOM元素,这个事件从Window开始,依次经过docuemnthtmlbody,再不断经过子节点直到到达目标元素,从Window到达目标元素父节点的过程称为捕获阶段,注意此时还未到达目标节点。

捕获阶段结束时,事件到达了目标节点的父节点,最终到达目标节点,并在目标节点上触发了这个事件,这就是目标阶段。需要注意的是,事件触发的目标节点为最底层的节点。比如下面的例子:

<div>
    <p>你猜,目标在这里还是<span>那里</span>。</p>
</div>

当我们点击“那里”的时候,目标节点是<span></span\>,点击“这里”的时候,目标节点是<p></p\>,而当我们点击<p></p\>区域之外,<div></div\>区域之内时,目标节点就是<div></div\>

当事件到达目标节点之后,就会沿着原路返回,这个过程有点类似水泡从水底浮出水面的过程,所以称这个过程为冒泡阶段。

现在再看addEventListener(eventName,handler,useCapture)函数。第三个参数是useCapture,代表是否在捕获阶段进行事件处理,如果是false,则在冒泡阶段进行事件处理,如果是true,在捕获阶段进行事件处理,默认是false。这么设计的主要原因是当年微软和netscape之间的浏览器战争打得火热,netscape主张捕获方式,微软主张冒泡方式,W3C采用了折中的方式,即先捕获再冒泡。

上面我们讲了事件的冒泡机制,我们可以利用这一特性来提高页面性能,事件委托便事件冒泡是最典型的应用之一。何谓“委托”?在现实中,当我们不想做某件事时,便“委托”给他人,让他人代为完成。JavaScript中,事件的委托表示给元素的父级或者祖级,甚至页面,由他们来绑定事件,然后利用事件冒泡的基本原理,通过事件目标对象进行检测,然后执行相关操作。看下面例子:

<ul id="list">
    <li>Item 1</li>
    <li>Item 2</li>
    <li>Item 3</li>
    <li>Item 4</li>
    <li>Item 5</li>
</ul>

// JavaScript
var list = document.getElementById("list");
list.addEventListener("click", function(e) {
    console.log(e.target);
});

上面的例子中,5个列表项的点击事件均委托给了父元素<ul id="list" ></ul\>

先看看事件委托的可行性。有人会问,当事件不是加在某个元素上的,如何在这个元素上触发事件呢?我们就是利用事件冒泡的机制,事件流到达目标元素后会向上冒泡,此时父元素接收到事件流便会执行事件执行程序。有人又会问,被委托的父元素下面如果有很多子元素,怎么知道事件流来自于哪个子元素呢?这个我们可以从事件对象中的target属性获得。事件对象下面会详细讲解。

我们再来看看为什么需要事件委托。

减少事件绑定:上面的例子中,也可以分别给每个列表项绑定事件,但利用事件委托的方式不仅省去了一一绑定的麻烦,也提升了网页的性能,因为每绑定一个事件便会增加内存使用
可以动态监听绑定:上面的例子中,我们对5个列表项进行了事件监听,当删除一个列表项时不需要单独删除这个列表项所绑定的事件,而增加一个列表项时也不需要单独为新增项绑定事件。

看了上面的例子和解释,我们可以看出事件委托的核心就是监听一个DOM中更高层、更不具体的元素,等到事件冒泡到这个不具体元素时,通过event对象的target属性来获取触发事件的具体元素。

事件委托是事件冒泡的一个应用,但有时候我们并不希望事件冒泡。比如下面的例子:

const ele = document.getElementById("ele");
ele.addEventListener("click", function() {
    console.log("ele-click");
}, false);

document.addEventListener("click", function() {
    console.log("document-click");
}, false);

我们本意是当点击ele元素区域时显示"ele-click",点击其他区域时显示"document-click"。但是我们发现点击ele元素区域时会依次显示"ele-click""document-click"。那是因为绑定在ele上的事件冒泡到了document上。想要解决这个问题,只需要加一行代码:

const ele = document.getElementById("ele");
ele.addEventListener("click", function(e) {
    console.log("ele-click");
    e.stopPropagation(); // 阻止事件冒泡
}, false);

document.addEventListener("click", function(e) {
    console.log("document-click");
}, false);

我们还能用e.cancelBubble=true来替代e.stopPropagation()。网上的说法是cancelBubble仅仅适用于IE,而stopPropagation适用于其他浏览器。但根据我实验的结果,现代浏览器(IE9及以上、Chrome、FF等)均同时支持这两种写法。

3.3.4 event对象

Event对象代表事件的状态,比如事件在其中发生的元素、键盘按键的状态、鼠标的位置、鼠标按钮的状态。当一个事件被触发的时候,就会创建一个事件对象。

我们用下面的代码打印出事件对象:

<div id="list">
    <li>Item 1</li>
    <li>Item 2</li>
</div>
<script>
    var list = document.getElementById("list");
    list.addEventListener("click", function(e) {
        console.log(e);
    });
</script>

运行结果如下:

参考: https://developer.mozilla.org/zh-CN/docs/Web/API/Event

属性 说明
Event.bubbles 只读,一个布尔值,用来表示该事件是否在DOM中冒泡
Event.cancelable 只读,一个布尔值,用来表示这个事件是否可以取消
Event.composed 只读,一个布尔值,用来表示这个事件是否可以在阴影DOM和常规DOM之间的边界上浮动
Event.currentTarget 只读,当前注册事件的对象的引用。这是一个这个事件目前需要传递到的对象,这个值会在传递的途中进行改变
Event.deepPath 一个由事件流经过了的DOM Node组成的Array
Event.defaultPrevented 只读,一个布尔值,表示了是否已经执行过了event.preventDefault()
Event.eventPhase 只读,指示事件流正在处理哪个阶段
Event.explicitOriginalTarget 只读,事件的原始目标(Mozilla内核特定属性)
Event.originalTarget 只读,在任何重定向之前,事件的原始目标(Mozilla内核特定属性)
Event.returnValue 一个非标准的替代方案(从旧版本的Microsoft Internet Explorer)到Event.preventDefault()Event.defaultPrevented
Event.scoped 只读,一个Boolean,表示给定的事件是否会通过阴影进入到标准的DOM中,此属性已重命名为composed
Event.srcElement 非标准别名(Microsoft Internet Explorer的旧版本)Event.target
Event.target 只读,对事件起源目标的引用
Event.timeStamp 只读,事件创建时的时间戳,毫秒级别。按照规定,这个时间戳是距离某个特定时刻的差值,但实际上在浏览器中此处的事件戳的定义有所不同。另外正在开展工作将其改为DOMHighResTimeStamp,在浏览器中此处的时间戳是距离该页面打开时刻的大小
Event.type 只读,事件的类型(不区分大小写)
Event.isTrusted 只读,指明事件是否是由浏览器(当用户点击实例后)或者由脚本(使用事件的创建方法,例如event.initEvent)启动
方法 说明
event.initEvent 通过DocumentEvent的接口给被创建的事件初始化某些值
event.preventDefault 取消事件(如果该事件可取消)
event.stopImmediatePropagation 对这个特定的事件而言,没有其他监听器被调用,这个事件既不会添加到相同的元素上,也不会添加到以后将要遍历的元素上(例如在捕获阶段)
event.stopPropagation 停止事件冒泡
Event.getPreventDefault() 未标准化,返回Event.defaultPrevented的值,用Event.defaultPrevented代替

下面介绍一些比较常用的属性和方法。

以上面的例子说明,当点击<li>Item1</li>时,target就是<li>Item1</li>元素,而currentTarget是<divid="list"></div>。

pageX/Y与clientX/Y一般情况下会相同,只有出现滚动条时才不一样。

3.4 常用组件

metismenu组件

layui组件

Data Table组件

四、vue教程

参考地址:https://cn.vuejs.org/v2/guide/typescript.html

4.1 基础

4.1.1 介绍

Vue是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue被设计为可以自底向上逐层应用,Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue也完全能够为复杂的单页应用提供驱动。

如果你想在深入学习 Vue 之前对它有更多了解,制作了一个视频,带您了解其核心概念和一个示例工程。

尝试Vue.js最简单的方法是使用JSFiddle上的Hello World例子。你可以在浏览器新标签页中打开它,跟着例子学习一些基础用法,或者你也可以创建一个.html文件,然后通过如下方式引入Vue:

<!-- 开发环境版本,包含了有帮助的命令行警告 -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

或者:

<!-- 生产环境版本,优化了尺寸和速度 -->
<script src="https://cdn.jsdelivr.net/npm/vue"></script>

安装教程给出了更多安装Vue的方式。请注意我们不推荐新手直接使用vue-cli,尤其是在你还不熟悉基于Node.js的构建工具时。

如果你喜欢交互式的东西,你也可以查阅这个Scrimba上的系列教程,它揉合了录屏和代码试验田,并允许你随时暂停和播放。

Vue.js的核心是一个允许采用简洁的模板语法来声明式地将数据渲染进DOM的系统:

<div id="app">
    {{ message }}
</div>
var app = new Vue({
    el: '#app',
    data: {
        message: 'Hello Vue!'
    }
})

我们已经成功创建了第一个Vue应用!看起来这跟渲染一个字符串模板非常类似,但是Vue在背后做了大量工作。现在数据和DOM已经被建立了关联,所有东西都是响应式的。我们要怎么确认呢?打开你的浏览器的JavaScript控制台(就在这个页面打开),并修改app.message的值,你将看到上例相应地更新。

除了文本插值,我们还可以像这样来绑定元素特性:

<div id="app-2">
    <span v-bind:title="message">
        鼠标悬停几秒钟查看此处动态绑定的提示信息!
    </span>
</div>
var app2 = new Vue({
    el: '#app-2',
    data: {
        message: '页面加载于 ' + new Date().toLocaleString()
    }
})

你看到的v-bind特性被称为指令,指令带有前缀 v-,以表示它们是Vue提供的特殊特性。可能你已经猜到了,它们会在渲染的DOM上应用特殊的响应式行为。在这里,该指令的意思是:"将这个元素节点的title特性和Vue实例的message属性保持一致"。如果你再次打开浏览器的JavaScript控制台,输入app2.message='新消息',就会再一次看到这个绑定了title特性的HTML已经进行了更新。

控制切换一个元素是否显示也相当简单:

<div id="app-3">
    <p v-if="seen">现在你看到我了</p>
</div>
var app3 = new Vue({
    el: '#app-3',
    data: {
        seen: true
    }
})

继续在控制台输入app3.seen = false,你会发现之前显示的消息消失了。这个例子演示了我们不仅可以把数据绑定到DOM文本或特性,还可以绑定到DOM结构。此外,Vue也提供一个强大的过渡效果系统,可以在Vue插入/更新/移除元素时自动应用过渡效果。

还有其它很多指令,每个都有特殊的功能。例如v-for指令可以绑定数组的数据来渲染一个项目列表:

<div id="app-4">
    <ol>
        <li v-for="todo in todos">
            {{ todo.text }}
        </li>
    </ol>
</div>
var app4 = new Vue({
    el: '#app-4',
    data: {
        todos: [
            { text: '学习 JavaScript' },
            { text: '学习 Vue' },
            { text: '整个牛项目' }
        ]
    }
})

在控制台里,输入app4.todos.push({ text: '新项目' }),你会发现列表最后添加了一个新项目。

为了让用户和你的应用进行交互,我们可以用 v-on指令添加一个事件监听器,通过它调用在Vue实例中定义的方法:

<div id="app-5">
    <p>{{ message }}</p>
    <button v-on:click="reverseMessage">反转消息</button>
</div>
var app5 = new Vue({
    el: '#app-5',
    data: {
        message: 'Hello Vue.js!'
    },
    methods: {
        reverseMessage: function () {
            this.message = this.message.split('').reverse().join('')
        }
    }
})

注意在reverseMessage方法中,我们更新了应用的状态,但没有触碰DOM——所有的DOM操作都由Vue来处理,你编写的代码只需要关注逻辑层面即可。Vue还提供了v-model指令,它能轻松实现表单输入和应用状态之间的双向绑定。

<div id="app-6">
    <p>{{ message }}</p>
    <input v-model="message">
</div>
var app6 = new Vue({
    el: '#app-6',
    data: {
        message: 'Hello Vue!'
    }
})

组件系统是Vue的另一个重要概念,因为它是一种抽象,允许我们使用小型、独立和通常可复用的组件构建大型应用。仔细想想,几乎任意类型的应用界面都可以抽象为一个组件树:

在Vue里,一个组件本质上是一个拥有预定义选项的一个Vue实例。在Vue中注册组件很简单:

// 定义名为 todo-item 的新组件
Vue.component('todo-item', {
  template: '<li>这是个待办项</li>'
})
var app = new Vue(...)

现在你可以用它构建另一个组件模板:

<ol>
    <!-- 创建一个 todo-item 组件的实例 -->
    <todo-item></todo-item>
</ol>

但是这样会为每个待办项渲染同样的文本,这看起来并不炫酷。我们应该能从父作用域将数据传到子组件才对。让我们来修改一下组件的定义,使之能够接受一个prop:

Vue.component('todo-item', {
    // todo-item组件现在接受一个
    // "prop",类似于一个自定义特性。
    // 这个prop名为todo。
    props: ['todo'],
    template: '<li>{{ todo.text }}</li>'
})

现在,我们可以使用v-bind指令将待办项传到循环输出的每个组件中:

<div id="app-7">
    <ol>
        <!--
            现在我们为每个todo-item提供todo对象
            todo对象是变量,即其内容可以是动态的。
            我们也需要为每个组件提供一个“key”,稍后再
            作详细解释。
        -->
        <todo-item
            v-for="item in groceryList"
            v-bind:todo="item"
            v-bind:key="item.id"
        ></todo-item>
    </ol>
</div>
Vue.component('todo-item', {
    props: ['todo'],
    template: '<li>{{ todo.text }}</li>'
})

var app7 = new Vue({
    el: '#app-7',
    data: {
        groceryList: [
            { id: 0, text: '蔬菜' },
            { id: 1, text: '奶酪' },
            { id: 2, text: '随便其它什么人吃的东西' }
        ]
    }
})

尽管这只是一个刻意设计的例子,但是我们已经设法将应用分割成了两个更小的单元。子单元通过prop接口与父单元进行了良好的解耦。我们现在可以进一步改进<todo-item>组件,提供更为复杂的模板和逻辑,而不会影响到父单元。

HTML例子:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Vue测试实例</title>
    <script src="https://cdn.staticfile.org/vue/2.4.2/vue.min.js"></script>
</head>
<body>
    <div id="vue_det">
        <h1>site : {{site}}</h1>
        <h1>url : {{url}}</h1>
        <h1>{{details()}}</h1>
    </div>
    <script type="text/javascript">
        var vm = new Vue({
            el: '#vue_det',
            data: {
                site: "测试实例",
                url: "www.baidu.com",
                alexa: "10000"
            },
            methods: {
                details: function() {
                    return  this.site + " - 国内的搜索引擎!";
                }
            }
        })
    </script>
</body>
</html>

4.1.2 Vue实例

每个Vue应用都是通过用Vue函数创建一个新的Vue实例开始的:

var vm = new Vue({
  // 选项
})

虽然没有完全遵循MVVM模型,但是Vue的设计也受到了它的启发。因此在文档中经常会使用vm(ViewModel的缩写)这个变量名表示Vue实例。当创建一个Vue实例时,你可以传入一个选项对象。

一个Vue应用由一个通过new Vue创建的根Vue实例,以及可选的嵌套的、可复用的组件树组成。举个例子,一个todo应用的组件树可以是这样的:

根实例
└─ TodoList
   ├─ TodoItem
   │  ├─ DeleteTodoButton
   │  └─ EditTodoButton
   └─ TodoListFooter
      ├─ ClearTodosButton
      └─ TodoListStatistics

所有的Vue组件都是Vue实例,并且接受相同的选项对象(一些根实例特有的选项除外)。

当一个Vue实例被创建时,它将data对象中的所有的属性加入到Vue的响应式系统中。当这些属性的值发生改变时,视图将会产生“响应”,即匹配更新为新的值。

// 我们的数据对象
var data = { a: 1 }

// 该对象被加入到一个 Vue 实例中
var vm = new Vue({
    data: data
})

// 获得这个实例上的属性
// 返回源数据中对应的字段
vm.a == data.a // => true

// 设置属性也会影响到原始数据
vm.a = 2
data.a // => 2

// ……反之亦然
data.a = 3
vm.a // => 3

当这些数据改变时,视图会进行重渲染。值得注意的是只有当实例被创建时就已经存在于data中的属性才是响应式的。也就是说如果你添加一个新的属性,比如:

vm.b = 'hi'

那么对b的改动将不会触发任何视图的更新。如果你知道你会在晚些时候需要一个属性,但是一开始它为空或不存在,那么你仅需要设置一些初始值。比如:

data: {
  newTodoText: '',
  visitCount: 0,
  hideCompletedTodos: false,
  todos: [],
  error: null
}

这里唯一的例外是使用Object.freeze(),这会阻止修改现有的属性,也意味着响应系统无法再追踪变化。

var obj = {
  foo: 'bar'
}
Object.freeze(obj)

new Vue({
    el: '#app',
    data: obj
})
<div id="app">
    <p>{{ foo }}</p>
    <!-- 这里的foo不会更新! -->
    <button v-on:click="foo = 'baz'">Change it</button>
</div>

除了数据属性,Vue实例还暴露了一些有用的实例属性与方法。它们都有前缀$,以便与用户定义的属性区分开来。例如:

var data = { a: 1 }
var vm = new Vue({
  el: '#example',
  data: data
})

vm.$data === data // => true
vm.$el === document.getElementById('example') // => true

// $watch是一个实例方法
vm.$watch('a', function (newValue, oldValue) {
  // 这个回调将在vm.a改变后调用
})

以后你可以在API参考中查阅到完整的实例属性和方法的列表。

每个Vue实例在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到DOM并在数据变化时更新DOM等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。

比如created钩子可以用来在一个实例被创建之后执行代码:

new Vue({
    data: {
        a: 1
    },
    created: function () {
        // this指向vm实例
        console.log('a is: ' + this.a)
    }
})
// => "a is: 1"

也有一些其它的钩子,在实例生命周期的不同阶段被调用,如mountedupdateddestroyed。生命周期钩子的this上下文指向调用它的Vue实例。不要在选项属性或回调上使用箭头函数,比如created:() => console.log(this.a)vm.$watch('a', newValue => this.myMethod())。因为箭头函数并没有this,this会作为变量一直向上级词法作用域查找,直至找到为止,经常导致Uncaught TypeError: Cannot read property of undefined或Uncaught TypeError: this.myMethod is not a function之类的错误。

下图展示了实例的生命周期。你不需要立马弄明白所有的东西,不过随着你的不断学习和使用,它的参考价值会越来越高。

Vue实例生命周期

4.1.3 模板语法

Vue.js使用了基于HTML的模板语法,允许开发者声明式地将DOM绑定至底层Vue实例的数据。所有Vue.js的模板都是合法的HTML,所以能被遵循规范的浏览器和HTML解析器解析。在底层的实现上,Vue将模板编译成虚拟DOM渲染函数。结合响应系统,Vue能够智能地计算出最少需要重新渲染多少组件,并把DOM操作次数减到最少。如果你熟悉虚拟DOM并且偏爱JavaScript的原始力量,你也可以不用模板,直接写渲染(render)函数,使用可选的JSX语法。

数据绑定最常见的形式就是使用“Mustache”语法(双大括号)的文本插值:

<span>Message: {{ msg }}</span>

Mustache标签将会被替代为对应数据对象上msg属性的值。无论何时,绑定的数据对象上msg属性发生了改变,插值处的内容都会更新。通过使用v-once指令,你也能执行一次性地插值,当数据改变时,插值处的内容不会更新。但请留心这会影响到该节点上的其它数据绑定:

<span v-once>这个将不会改变: {{ msg }}</span>

双大括号会将数据解释为普通文本,而非HTML代码。为了输出真正的HTML,你需要使用v-html指令:

<p>Using mustaches: {{ rawHtml }}</p>
<p>Using v-html directive: <span v-html="rawHtml"></span></p>

这个span的内容将会被替换成为属性值rawHtml,直接作为HTML——会忽略解析属性值中的数据绑定。注意,你不能使用v-html来复合局部模板,因为Vue不是基于字符串的模板引擎。反之,对于用户界面(UI),组件更适合作为可重用和可组合的基本单位。

你的站点上动态渲染的任意HTML可能会非常危险,因为它很容易导致XSS攻击。请只对可信内容使用HTML插值,绝不要对用户提供的内容使用插值。

Mustache语法不能作用在HTML特性上,遇到这种情况应该使用v-bind指令:

<div v-bind:id="dynamicId"></div>

对于布尔特性(它们只要存在就意味着值为true),v-bind工作起来略有不同,在这个例子中:

<button v-bind:disabled="isButtonDisabled">Button</button>

如果isButtonDisabled的值是nullundefinedfalse,则disabled特性甚至不会被包含在渲染出来的<button>元素中。

迄今为止,在我们的模板中,我们一直都只绑定简单的属性键值。但实际上,对于所有的数据绑定,Vue.js都提供了完全的JavaScript表达式支持。

{{ number + 1 }}
{{ ok ? 'YES' : 'NO' }}
{{ message.split('').reverse().join('') }}
<div v-bind:id="'list-' + id"></div>

这些表达式会在所属Vue实例的数据作用域下作为JavaScript被解析。有个限制就是,每个绑定都只能包含单个表达式,所以下面的例子都不会生效。

<!-- 这是语句,不是表达式 -->
{{ var a = 1 }}
<!-- 流控制也不会生效,请使用三元表达式 -->
{{ if (ok) { return message } }}

模板表达式都被放在沙盒中,只能访问全局变量的一个白名单,如Math和Date。你不应该在模板表达式中试图访问用户定义的全局变量。

指令(Directives)是带有v-前缀的特殊特性。指令特性的值预期是单个JavaScript表达式(v-for是例外情况,稍后我们再讨论)。指令的职责是,当表达式的值改变时,将其产生的连带影响,响应式地作用于DOM。回顾我们在介绍中看到的例子:

<p v-if="seen">现在你看到我了</p>

这里,v-if指令将根据表达式seen的值的真假来插入/移除<p>元素。

一些指令能够接收一个“参数”,在指令名称之后以冒号表示。例如,v-bind 指令可以用于响应式地更新HTML特性:

<a v-bind:href="url">...</a>

在这里href是参数,告知v-bind指令将该元素的href特性与表达式url的值绑定。

另一个例子是v-on指令,它用于监听DOM事件:

<a v-on:click="doSomething">...</a>

在这里参数是监听的事件名。我们也会更详细地讨论事件处理。

可以用方括号括起来的JavaScript表达式作为一个指令的参数:

<!-- 注意,参数表达式的写法存在一些约束,如之后的“对动态参数表达式的约束”章节所述。-->
<a v-bind:[attributeName]="url"> ... </a>

这里的attributeName会被作为一个JavaScript表达式进行动态求值,求得的值将会作为最终的参数来使用。例如,如果你的Vue实例有一个data属性attributeName,其值为"href",那么这个绑定将等价于v-bind:href。同样地,你可以使用动态参数为一个动态的事件名绑定处理函数:

<a v-on:[eventName]="doSomething"> ... </a>

在这个示例中,当eventName的值为"focus"时,v-on:[eventName]将等价于v-on:focus

对动态参数的值的约束:动态参数预期会求出一个字符串,异常情况下值为null。这个特殊的null值可以被显性地用于移除绑定。任何其它非字符串类型的值都将会触发一个警告。

对动态参数表达式的约束:动态参数表达式有一些语法约束,因为某些字符,如空格和引号,放在HTML attribute名里是无效的。例如:

<!-- 这会触发一个编译警告 -->
<a v-bind:['foo' + bar]="value"> ... </a>

变通的办法是使用没有空格或引号的表达式,或用计算属性替代这种复杂表达式。

在DOM中使用模板时(直接在一个HTML文件里撰写模板),还需要避免使用大写字符来命名键名,因为浏览器会把attribute名全部强制转为小写:

<!--
在DOM中使用模板时这段代码会被转换为v-bind:[someattr]。
除非在实例中有一个名为“someattr”的property,否则代码不会工作。
-->
<a v-bind:[someAttr]="value"> ... </a>

修饰符(modifier)是以半角句号.指明的特殊后缀,用于指出一个指令应该以特殊方式绑定。例如,.prevent修饰符告诉v-on指令对于触发的事件调用event.preventDefault():

<form v-on:submit.prevent="onSubmit">...</form>

在接下来对v-onv-for等功能的探索中,你会看到修饰符的其它例子。

v-前缀作为一种视觉提示,用来识别模板中Vue特定的特性。当你在使用Vue.js为现有标签添加动态行为(dynamic behavior)时,v-前缀很有帮助,然而,对于一些频繁用到的指令来说,就会感到使用繁琐。同时,在构建由Vue管理所有模板的单页面应用程序(SPA-single page application)时,v-前缀也变得没那么重要了。因此,Vue为v-bindv-on这两个最常用的指令,提供了特定简写:

v-bind缩写

<!-- 完整语法 -->
<a v-bind:href="url">...</a>
<!-- 缩写 -->
<a :href="url">...</a>
…

v-on缩写

<!-- 完整语法 -->
<a v-on:click="doSomething">...</a>
<!-- 缩写 -->
<a @click="doSomething">...</a>
…

它们看起来可能与普通的HTML略有不同,但:@对于特性名来说都是合法字符,在所有支持Vue的浏览器都能被正确地解析。而且,它们不会出现在最终渲染的标记中。缩写语法是完全可选的,但随着你更深入地了解它们的作用,你会庆幸拥有它们。

4.1.4 计算属性和侦听器

模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的。在模板中放入太多的逻辑会让模板过重且难以维护。例如:

<div id="example">
  {{ message.split('').reverse().join('') }}
</div>

在这个地方,模板不再是简单的声明式逻辑。你必须看一段时间才能意识到,这里是想要显示变量message的翻转字符串。当你想要在模板中多次引用此处的翻转字符串时,就会更加难以处理。所以,对于任何复杂逻辑,你都应当使用计算属性。

<div id="example">
    <p>Original message: "{{ message }}"</p>
    <p>Computed reversed message: "{{ reversedMessage }}"</p>
</div>
var vm = new Vue({
    el: '#example',
    data: {
        message: 'Hello'
    },
    computed: {
        // 计算属性的 getter
        reversedMessage: function () {
            // `this` 指向 vm 实例
            return this.message.split('').reverse().join('')
        }
    }
})

这里我们声明了一个计算属性reversedMessage。我们提供的函数将用作属性vm.reversedMessagegetter函数:

console.log(vm.reversedMessage) // => 'olleH'
vm.message = 'Goodbye'
console.log(vm.reversedMessage) // => 'eybdooG'

你可以打开浏览器的控制台,自行修改例子中的vm。vm.reversedMessage的值始终取决于vm.message的值。你可以像绑定普通属性一样在模板中绑定计算属性。Vue知道vm.reversedMessage依赖于vm.message,因此当vm.message发生改变时,所有依赖vm.reversedMessage的绑定也会更新。而且最妙的是我们已经以声明的方式创建了这种依赖关系:计算属性的getter函数是没有副作用(side effect)的,这使它更易于测试和理解。

你可能已经注意到我们可以通过在表达式中调用方法来达到同样的效果:

<p>Reversed message: "{{ reversedMessage() }}"</p>
// 在组件中
methods: {
    reversedMessage: function () {
        return this.message.split('').reverse().join('')
    }
}

我们可以将同一函数定义为一个方法而不是一个计算属性。两种方式的最终结果确实是完全相同的。然而,不同的是计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。这就意味着只要message还没有发生改变,多次访问reversedMessage计算属性会立即返回之前的计算结果,而不必再次执行函数。

这也同样意味着下面的计算属性将不再更新,因为Date.now()不是响应式依赖:

computed: {
    now: function () {
        return Date.now()
    }
}

相比之下,每当触发重新渲染时,调用方法将总会再次执行函数。我们为什么需要缓存?假设我们有一个性能开销比较大的计算属性A,它需要遍历一个巨大的数组并做大量的计算。然后我们可能有其他的计算属性依赖于A。如果没有缓存,我们将不可避免的多次执行A的getter!如果你不希望有缓存,请用方法来替代。

Vue提供了一种更通用的方式来观察和响应Vue实例上的数据变动:侦听属性。当你有一些数据需要随着其它数据变动而变动时,你很容易滥用watch——特别是如果你之前使用过AngularJS。然而,通常更好的做法是使用计算属性而不是命令式的watch回调。细想一下这个例子:

<div id="demo">{{ fullName }}</div>
var vm = new Vue({
    el: '#demo',
    data: {
        firstName: 'Foo',
        lastName: 'Bar',
        fullName: 'Foo Bar'
    },
    watch: {
        firstName: function (val) {
            this.fullName = val + ' ' + this.lastName
        },
        lastName: function (val) {
            this.fullName = this.firstName + ' ' + val
        }
    }
})

上面代码是命令式且重复的。将它与计算属性的版本进行比较:

var vm = new Vue({
    el: '#demo',
    data: {
        firstName: 'Foo',
        lastName: 'Bar'
    },
    computed: {
        fullName: function () {
            return this.firstName + ' ' + this.lastName
        }
    }
})

好得多了,不是吗?

计算属性默认只有getter,不过在需要时你也可以提供一个setter:

// ...
computed: {
    fullName: {
        // getter
        get: function () {
            return this.firstName + ' ' + this.lastName
        },
        // setter
        set: function (newValue) {
            var names = newValue.split(' ')
            this.firstName = names[0]
            this.lastName = names[names.length - 1]
        }
    }
}
// ...

现在再运行vm.fullName='John Doe'时,setter会被调用,vm.firstNamevm.lastName也会相应地被更新。

虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。这就是为什么Vue通过watch选项提供了一个更通用的方法,来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。例如:

<div id="watch-example">
    <p>
        Ask a yes/no question:
        <input v-model="question">
    </p>
    <p>{{ answer }}</p>
</div>
<!-- 因为AJAX库和通用工具的生态已经相当丰富,Vue核心代码没有重复 -->
<!-- 提供这些功能以保持精简。这也可以让你自由选择自己更熟悉的工具。 -->
<script src="https://cdn.jsdelivr.net/npm/axios@0.12.0/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash@4.13.1/lodash.min.js"></script>
<script>
var watchExampleVM = new Vue({
    el: '#watch-example',
    data: {
        question: '',
        answer: 'I cannot give you an answer until you ask a question!'
    },
    watch: {
        // 如果question发生改变,这个函数就会运行
        question: function (newQuestion, oldQuestion) {
            this.answer = 'Waiting for you to stop typing...'
            this.debouncedGetAnswer()
        }
    },
    created: function () {
        // _.debounce是一个通过Lodash限制操作频率的函数。
        // 在这个例子中,我们希望限制访问yesno.wtf/api的频率
        // AJAX请求直到用户输入完毕才会发出。想要了解更多关于
        // _.debounce函数 (及其近亲_.throttle)的知识,
        // 请参考:https://lodash.com/docs#debounce
        this.debouncedGetAnswer = _.debounce(this.getAnswer, 500)
    },
    methods: {
        getAnswer: function () {
            if (this.question.indexOf('?') === -1) {
                this.answer = 'Questions usually contain a question mark. ;-)'
                return
            }
            this.answer = 'Thinking...'
            var vm = this
            axios.get('https://yesno.wtf/api')
                .then(function (response) {
                vm.answer = _.capitalize(response.data.answer)
                })
                .catch(function (error) {
                vm.answer = 'Error! Could not reach the API. ' + error
                })
        }
    }
})
</script>

在这个示例中,使用watch选项允许我们执行异步操作(访问一个API),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。除了watch选项之外,您还可以使用命令式的vm.$watch API

4.1.5 Class与Style 绑定

操作元素的class列表和内联样式是数据绑定的一个常见需求。因为它们都是属性,所以我们可以用v-bind处理它们:只需要通过表达式计算出字符串结果即可。不过,字符串拼接麻烦且易错。因此,在将v-bind用于class和style时,Vue.js 做了专门的增强。表达式结果的类型除了字符串之外,还可以是对象或数组。

我们可以传给v-bind:class一个对象,以动态地切换class:

<div v-bind:class="{ active: isActive }"></div>

上面的语法表示active这个class存在与否将取决于数据属性isActivetruthiness

你可以在对象中传入更多属性来动态切换多个class。此外,v-bind:class指令也可以与普通的class属性共存。当有如下模板:

<div
  class="static"
  v-bind:class="{ active: isActive, 'text-danger': hasError }"
></div>

和如下 data:

data: {
  isActive: true,
  hasError: false
}

结果渲染为:<div class="static active"></div>。当isActive或者hasError变化时,class列表将相应地更新。例如,如果hasError的值为true,class列表将变为"static active text-danger"。

绑定的数据对象不必内联定义在模板里:

<div v-bind:class="classObject"></div>
data: {
    classObject: {
        active: true,
        'text-danger': false
    }
}

渲染的结果和上面一样。我们也可以在这里绑定一个返回对象的计算属性。这是一个常用且强大的模式:

<div v-bind:class="classObject"></div>
data: {
  isActive: true,
  error: null
},
computed: {
    classObject: function () {
        return {
            active: this.isActive && !this.error,
            'text-danger': this.error && this.error.type === 'fatal'
        }
    }
}

我们可以把一个数组传给v-bind:class,以应用一个class列表:

<div v-bind:class="[activeClass, errorClass]"></div>
data: {
    activeClass: 'active',
    errorClass: 'text-danger'
}

渲染为:<div class="active text-danger"></div>,如果你也想根据条件切换列表中的class,可以用三元表达式:

<div v-bind:class="[isActive ? activeClass : '', errorClass]"></div>

这样写将始终添加errorClass,但是只有在isActivetruthy时才添加activeClass

不过,当有多个条件class时这样写有些繁琐。所以在数组语法中也可以使用对象语法:

<div v-bind:class="[{ active: isActive }, errorClass]"></div>

这个章节假设你已经对Vue组件有一定的了解。当然你也可以先跳过这里,稍后再回过头来看。

当在一个自定义组件上使用class属性时,这些class将被添加到该组件的根元素上面。这个元素上已经存在的class不会被覆盖。例如,如果你声明了这个组件:

Vue.component('my-component', {
    template: '<p class="foo bar">Hi</p>'
})

然后在使用它的时候添加一些class:

<my-component class="baz boo"></my-component>

HTML 将被渲染为:<p class="foo bar baz boo">Hi</p>,对于带数据绑定class也同样适用:

<my-component v-bind:class="{ active: isActive }"></my-component>

isActivetruthy时,HTML将被渲染成为:<p class="foo bar active">Hi</p>

v-bind:style的对象语法十分直观——看着非常像CSS,但其实是一个JavaScript对象。CSS属性名可以用驼峰式(camelCase)或短横线分隔(kebab-case,记得用引号括起来)来命名:

<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
data: {
    activeColor: 'red',
    fontSize: 30
}

直接绑定到一个样式对象通常更好,这会让模板更清晰:

<div v-bind:style="styleObject"></div>
data: {
    styleObject: {
        color: 'red',
        fontSize: '13px'
    }
}

同样的,对象语法常常结合返回对象的计算属性使用。

v-bind:style的数组语法可以将多个样式对象应用到同一个元素上:<div v-bind:style="[baseStyles, overridingStyles]"></div>

当v-bind:style使用需要添加浏览器引擎前缀的CSS属性时,如transform,Vue.js会自动侦测并添加相应的前缀。

可以为style绑定中的属性提供一个包含多个值的数组,常用于提供多个带前缀的值,例如:

<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>

这样写只会渲染数组中最后一个被浏览器支持的值。在本例中,如果浏览器支持不带浏览器前缀的flexbox,那么就只会渲染display: flex

4.1.6 条件渲染

v-if指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回truthy值的时候被渲染。

<h1 v-if="awesome">Vue is awesome!</h1>

也可以用v-else添加一个“else块”:

<h1 v-if="awesome">Vue is awesome!</h1>
<h1 v-else>Oh no 😢</h1>

因为v-if是一个指令,所以必须将它添加到一个元素上。但是如果想切换多个元素呢?此时可以把一个<template>元素当做不可见的包裹元素,并在上面使用v-if。最终的渲染结果将不包含<template>元素。

<template v-if="ok">
    <h1>Title</h1>
    <p>Paragraph 1</p>
    <p>Paragraph 2</p>
</template>

你可以使用v-else指令来表示v-if的“else块”:

<div v-if="Math.random() > 0.5">
    Now you see me
</div>
<div v-else>
    Now you don't
</div>

v-else元素必须紧跟在带v-if或者v-else-if的元素的后面,否则它将不会被识别。

v-else-if,顾名思义,充当v-if的“else-if块”,可以连续使用:

<div v-if="type === 'A'">
    A
</div>
<div v-else-if="type === 'B'">
    B
</div>
<div v-else-if="type === 'C'">
    C
</div>
<div v-else>
    Not A/B/C
</div>

类似于v-else,v-else-if也必须紧跟在带v-if或者v-else-if的元素之后。

Vue会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。这么做除了使Vue变得非常快之外,还有其它一些好处。例如,如果你允许用户在不同的登录方式之间切换:

<template v-if="loginType === 'username'">
  <label>Username</label>
  <input placeholder="Enter your username">
</template>
<template v-else>
  <label>Email</label>
  <input placeholder="Enter your email address">
</template>

那么在上面的代码中切换loginType将不会清除用户已经输入的内容。因为两个模板使用了相同的元素,<input>不会被替换掉——仅仅是替换了它的placeholder。这样也不总是符合实际需求,所以Vue为你提供了一种方式来表达“这两个元素是完全独立的,不要复用它们”。只需添加一个具有唯一值的key属性即可:

<template v-if="loginType === 'username'">
    <label>Username</label>
    <input placeholder="Enter your username" key="username-input">
</template>
<template v-else>
    <label>Email</label>
    <input placeholder="Enter your email address" key="email-input">
</template>

现在,每次切换时,输入框都将被重新渲染。注意,<label>元素仍然会被高效地复用,因为它们没有添加key属性。

另一个用于根据条件展示元素的选项是v-show指令。用法大致一样:

<h1 v-show="ok">Hello!</h1>

不同的是带有v-show的元素始终会被渲染并保留在DOM中。v-show只是简单地切换元素的CSS属性display。注意,v-show不支持<template>元素,也不支持v-else

v-if是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。v-if也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。相比之下,v-show就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于CSS进行切换。

一般来说,v-if有更高的切换开销,而v-show有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用v-show较好;如果在运行时条件很少改变,则使用v-if较好。

不推荐同时使用v-if和v-for。请查阅风格指南以获取更多信息。当v-ifv-for一起使用时,v-for具有比v-if更高的优先级。请查阅列表渲染指南以获取详细信息。

4.1.7 列表渲染

我们可以用v-for指令基于一个数组来渲染一个列表。v-for指令需要使用item in items形式的特殊语法,其中items是源数据数组,而item则是被迭代的数组元素的别名。

<ul id="example-1">
    <li v-for="item in items">
        {{ item.message }}
    </li>
</ul>
var example1 = new Vue({
    el: '#example-1',
    data: {
        items: [
            { message: 'Foo' },
            { message: 'Bar' }
        ]
    }
})

v-for块中,我们可以访问所有父作用域的属性。v-for还支持一个可选的第二个参数,即当前项的索引。

<ul id="example-2">
    <li v-for="(item, index) in items">
        {{ parentMessage }} - {{ index }} - {{ item.message }}
    </li>
</ul>
var example2 = new Vue({
    el: '#example-2',
    data: {
        parentMessage: 'Parent',
        items: [
            { message: 'Foo' },
            { message: 'Bar' }
        ]
    }
})

你也可以用of替代in作为分隔符,因为它更接近JavaScript迭代器的语法:

<div v-for="item of items"></div>

你也可以用v-for来遍历一个对象的属性。

<ul id="v-for-object" class="demo">
    <li v-for="value in object">
        {{ value }}
    </li>
</ul>
new Vue({
    el: '#v-for-object',
    data: {
        object: {
            title: 'How to do lists in Vue',
            author: 'Jane Doe',
            publishedAt: '2016-04-10'
        }
    }
})

你也可以提供第二个的参数为property名称(也就是键名):

<div v-for="(value, name) in object">
    {{ name }}: {{ value }}
</div>

还可以用第三个参数作为索引:

<div v-for="(value, name, index) in object">
    {{ index }}. {{ name }}: {{ value }}
</div>

在遍历对象时,会按Object.keys()的结果遍历,但是不能保证它的结果在不同的JavaScript引擎下都一致。

当Vue正在更新使用v-for渲染的元素列表时,它默认使用“就地更新”的策略。如果数据项的顺序被改变,Vue将不会移动DOM元素来匹配数据项的顺序,而是就地更新每个元素,并且确保它们在每个索引位置正确渲染。这个默认的模式是高效的,但是只适用于不依赖子组件状态或临时DOM状态(例如:表单输入值)的列表渲染输出。

为了给Vue一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,你需要为每项提供一个唯一key属性:

<div v-for="item in items" v-bind:key="item.id">
    <!-- 内容 -->
</div>

建议尽可能在使用v-for时提供key attribute,除非遍历输出的DOM内容非常简单,或者是刻意依赖默认行为以获取性能上的提升。因为它是Vue识别节点的一个通用机制,key并不仅与v-for特别关联。后面我们将在指南中看到,它还具有其它用途。

不要使用对象或数组之类的非基本类型值作为v-forkey。请用字符串或数值类型的值。

Vue将被侦听的数组的变异方法进行了包裹,所以它们也将会触发视图更新。这些被包裹过的方法包括:push(),pop(),shift(),unshift(),splice(),sort()reverse()。你可以打开控制台,然后对前面例子的items数组尝试调用变异方法。比如example1.items.push({ message: 'Baz' })

变异方法,顾名思义,会改变调用了这些方法的原始数组。相比之下,也有非变异(non-mutating method)方法,例如filter()concat()slice()。它们不会改变原始数组,而总是返回一个新数组。当使用非变异方法时,可以用新数组替换旧数组:

example1.items = example1.items.filter(function (item) {
    return item.message.match(/Foo/)
})

你可能认为这将导致Vue丢弃现有DOM并重新渲染整个列表。幸运的是,事实并非如此。Vue为了使得DOM元素得到最大范围的重用而实现了一些智能的启发式方法,所以用一个含有相同元素的数组去替换原来的数组是非常高效的操作。

由于JavaScript的限制,Vue不能检测以下数组的变动:

  1. 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
  2. 当你修改数组的长度时,例如:vm.items.length = newLength

举个例子:

var vm = new Vue({
    data: {
        items: ['a', 'b', 'c']
    }
})
vm.items[1] = 'x' // 不是响应性的
vm.items.length = 2 // 不是响应性的

为了解决第一类问题,以下两种方式都可以实现和vm.items[indexOfItem] = newValue相同的效果,同时也将在响应式系统内触发状态更新:

// Vue.set
Vue.set(vm.items, indexOfItem, newValue)
// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)

你也可以使用vm.$set实例方法,该方法是全局方法Vue.set的一个别名:

vm.$set(vm.items, indexOfItem, newValue)

为了解决第二类问题,你可以使用splice:

vm.items.splice(newLength)

还是由于JavaScript的限制,Vue不能检测对象属性的添加或删除:

var vm = new Vue({
    data: {
        a: 1
    }
})
// vm.a现在是响应式的

vm.b = 2
// vm.b不是响应式的

对于已经创建的实例,Vue不允许动态添加根级别的响应式属性。但是,可以使用Vue.set(object, propertyName, value)方法向嵌套对象添加响应式属性。例如,对于:

var vm = new Vue({
    data: {
        userProfile: {
            name: 'Anika'
        }
    }
})

你可以添加一个新的age 属性到嵌套的userProfile对象:Vue.set(vm.userProfile, 'age', 27),你还可以使用vm.$set实例方法,它只是全局Vue.set的别名:vm.$set(vm.userProfile, 'age', 27)

有时你可能需要为已有对象赋值多个新属性,比如使用Object.assign()_.extend()。在这种情况下,你应该用两个对象的属性创建一个新的对象。所以,如果你想添加新的响应式属性,不要像这样:

Object.assign(vm.userProfile, {
    age: 27,
    favoriteColor: 'Vue Green'
})

你应该这样做:

vm.userProfile = Object.assign({}, vm.userProfile, {
    age: 27,
    favoriteColor: 'Vue Green'
})

有时,我们想要显示一个数组经过过滤或排序后的版本,而不实际改变或重置原始数据。在这种情况下,可以创建一个计算属性,来返回过滤或排序后的数组。例如:

<li v-for="n in evenNumbers">{{ n }}</li>
data: {
    numbers: [ 1, 2, 3, 4, 5 ]
},
computed: {
    evenNumbers: function () {
        return this.numbers.filter(function (number) {
            return number % 2 === 0
        })
    }
}

在计算属性不适用的情况下(例如,在嵌套v-for循环中)你可以使用一个方法:

<li v-for="n in even(numbers)">{{ n }}</li>
data: {
    numbers: [ 1, 2, 3, 4, 5 ]
},
methods: {
    even: function (numbers) {
        return numbers.filter(function (number) {
            return number % 2 === 0
        })
    }
}

v-for也可以接受整数。在这种情况下,它会把模板重复对应次数。

<div>
    <span v-for="n in 10">{{ n }} </span>
</div>

结果:1 2 3 4 5 6 7 8 9 10

类似于v-if,你也可以利用带有v-for<template>来循环渲染一段包含多个元素的内容。比如:

<ul>
    <template v-for="item in items">
        <li>{{ item.msg }}</li>
        <li class="divider" role="presentation"></li>
    </template>
</ul>

注意我们不推荐在同一元素上使用v-ifv-for。更多细节可查阅风格指南

当它们处于同一节点,v-for的优先级比v-if更高,这意味着v-if将分别重复运行于每个v-for循环中。当你只想为部分项渲染节点时,这种优先级的机制会十分有用,如下:

<li v-for="todo in todos" v-if="!todo.isComplete">
    {{ todo }}
</li>

上面的代码将只渲染未完成的todo。而如果你的目的是有条件地跳过循环的执行,那么可以将v-if置于外层元素(或<template>)上。如:

<ul v-if="todos.length">
    <li v-for="todo in todos">
        {{ todo }}
    </li>
</ul>
<p v-else>No todos left!</p>

这部分内容假定你已经了解组件相关知识。你也完全可以先跳过它,以后再回来查看。在自定义组件上,你可以像在任何普通元素上一样使用v-for

<my-component v-for="item in items" :key="item.id"></my-component>

当在组件上使用v-for时,key现在是必须的。然而,任何数据都不会被自动传递到组件里,因为组件有自己独立的作用域。为了把迭代数据传递到组件里,我们要使用prop:

<my-component
  v-for="(item, index) in items"
  v-bind:item="item"
  v-bind:index="index"
  v-bind:key="item.id"
></my-component>

不自动将item注入到组件里的原因是,这会使得组件与v-for的运作紧密耦合。明确组件数据的来源能够使组件在其他场合重复使用。下面是一个简单的todo列表的完整例子:

<div id="todo-list-example">
    <form v-on:submit.prevent="addNewTodo">
        <label for="new-todo">Add a todo</label>
        <input
            v-model="newTodoText"
            id="new-todo"
            placeholder="E.g. Feed the cat"
        >
        <button>Add</button>
    </form>
    <ul>
        <li
            is="todo-item"
            v-for="(todo, index) in todos"
            v-bind:key="todo.id"
            v-bind:title="todo.title"
            v-on:remove="todos.splice(index, 1)"
        ></li>
    </ul>
</div>

注意这里的is="todo-item"属性。这种做法在使用DOM模板时是十分必要的,因为在<ul>元素内只有<li>元素会被看作有效内容。这样做实现的效果与<todo-item\>相同,但是可以避开一些潜在的浏览器解析错误。查看DOM模板解析说明来了解更多信息。

Vue.component('todo-item', {
    template: '\
        <li>\
            {{ title }}\
            <button v-on:click="$emit(\'remove\')">Remove</button>\
        </li>\
    ',
    props: ['title']
})

new Vue({
    el: '#todo-list-example',
    data: {
        newTodoText: '',
        todos: [
            {
                id: 1,
                title: 'Do the dishes',
            },{
                id: 2,
                title: 'Take out the trash',
            },{
                id: 3,
                title: 'Mow the lawn'
            }
        ],
        nextTodoId: 4
    },
    methods: {
        addNewTodo: function () {
            this.todos.push({
                id: this.nextTodoId++,
                title: this.newTodoText
            })
            this.newTodoText = ''
        }
    }
})

4.1.7 事件处理

可以用v-on指令监听DOM事件,并在触发时运行一些JavaScript代码。示例:

<div id="example-1">
    <button v-on:click="counter += 1">Add 1</button>
    <p>The button above has been clicked {{ counter }} times.</p>
</div>
var example1 = new Vue({
    el: '#example-1',
    data: {
        counter: 0
    }
})

然而许多事件处理逻辑会更为复杂,所以直接把JavaScript代码写在v-on指令中是不可行的。因此v-on还可以接收一个需要调用的方法名称。示例:

<div id="example-2">
    <!-- greet是在下面定义的方法名 -->
    <button v-on:click="greet">Greet</button>
</div>
var example2 = new Vue({
    el: '#example-2',
    data: {
        name: 'Vue.js'
    },
    // 在methods对象中定义方法
    methods: {
        greet: function (event) {
            // this在方法里指向当前Vue实例
            alert('Hello ' + this.name + '!')
            // event是原生DOM事件
            if (event) {
                alert(event.target.tagName)
            }
        }
    }
})

// 也可以用JavaScript直接调用方法
example2.greet() // => 'Hello Vue.js!'

除了直接绑定到一个方法,也可以在内联JavaScript语句中调用方法:

<div id="example-3">
    <button v-on:click="say('hi')">Say hi</button>
    <button v-on:click="say('what')">Say what</button>
</div>
new Vue({
    el: '#example-3',
    methods: {
        say: function (message) {
            alert(message)
        }
    }
})

有时也需要在内联语句处理器中访问原始的DOM事件。可以用特殊变量$event把它传入方法:

<button v-on:click="warn('Form cannot be submitted yet.', $event)">
    Submit
</button>
// ...
methods: {
    warn: function (message, event) {
        // 现在我们可以访问原生事件对象
        if (event) event.preventDefault()
        alert(message)
    }
}

在事件处理程序中调用event.preventDefault()event.stopPropagation()是非常常见的需求。尽管我们可以在方法中轻松实现这点,但更好的方式是:方法只有纯粹的数据逻辑,而不是去处理DOM事件细节。

为了解决这个问题,Vue.js为v-on提供了事件修饰符。之前提过,修饰符是由点开头的指令后缀来表示的:.stop,.prevent,.capture,.self,.once.passive

<!-- 阻止单击事件继续传播 -->
<a v-on:click.stop="doThis"></a>

<!-- 提交事件不再重载页面 -->
<form v-on:submit.prevent="onSubmit"></form>

<!-- 修饰符可以串联 -->
<a v-on:click.stop.prevent="doThat"></a>

<!-- 只有修饰符 -->
<form v-on:submit.prevent></form>

<!-- 添加事件监听器时使用事件捕获模式 -->
<!-- 即内部元素触发的事件先在此处理,然后才交由内部元素进行处理 -->
<div v-on:click.capture="doThis">...</div>

<!-- 只当在 event.target 是当前元素自身时触发处理函数 -->
<!-- 即事件不是从内部元素触发的 -->
<div v-on:click.self="doThat">...</div>

使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用v-on:click.prevent.self会阻止所有的点击,而v-on:click.self.prevent只会阻止对元素自身的点击。

<!-- 点击事件将只会触发一次 -->
<a v-on:click.once="doThis"></a>

不像其它只能对原生的DOM事件起作用的修饰符,.once修饰符还能被用到自定义的组件事件上。如果你还没有阅读关于组件的文档,现在大可不必担心。Vue还对应addEventListener中的passive选项提供了.passive修饰符。

<!-- 滚动事件的默认行为(即滚动行为)将会立即触发 -->
<!-- 而不会等待onScroll完成  -->
<!-- 这其中包含event.preventDefault()的情况 -->
<div v-on:scroll.passive="onScroll">...</div>

这个.passive修饰符尤其能够提升移动端的性能。不要把.passive.prevent一起使用,因为.prevent将会被忽略,同时浏览器可能会向你展示一个警告。请记住,.passive会告诉浏览器你不想阻止事件的默认行为。

在监听键盘事件时,我们经常需要检查详细的按键。Vue允许为v-on在监听键盘事件时添加按键修饰符:

<!-- 只有在key是Enter时调用vm.submit() -->
<input v-on:keyup.enter="submit">

你可以直接将KeyboardEvent.key暴露的任意有效按键名转换为kebab-case来作为修饰符。

<input v-on:keyup.page-down="onPageDown">

在上述示例中,处理函数只会在$event.key等于PageDown时被调用。

keyCode的事件用法已经被废弃了并可能不会被最新的浏览器支持。使用keyCode特性也是允许的:<input v-on:keyup.13="submit">

为了在必要的情况下支持旧浏览器,Vue 提供了绝大多数常用的按键码的别名:.enter,.tab,.delete(捕获“删除”和“退格”键),.esc,.space,.up,.down,.left,.right。有一些按键(.esc以及所有的方向键)在IE9中有不同的key值, 如果你想支持IE9,这些内置的别名应该是首选。

你还可以通过全局config.keyCodes对象自定义按键修饰符别名:

// 可以使用v-on:keyup.f1
Vue.config.keyCodes.f1 = 112

可以用如下修饰符来实现仅在按下相应按键时才触发鼠标或键盘事件的监听器:.ctrl,.alt,.shift,.meta

注意:在Mac系统键盘上,meta对应command键(⌘)。在Windows系统键盘meta对应Windows徽标键(⊞)。在Sun操作系统键盘上,meta对应实心宝石键(◆)。在其他特定键盘上,尤其在MIT和Lisp机器的键盘、以及其后继产品,比如Knight键盘、space-cadet键盘,meta被标记为“META”。在Symbolics键盘上,meta被标记为“META”或者“Meta”。

例如:

<!-- Alt + C -->
<input @keyup.alt.67="clear">

<!-- Ctrl + Click -->
<div @click.ctrl="doSomething">Do something</div>

请注意修饰键与常规按键不同,在和keyup事件一起用时,事件触发时修饰键必须处于按下状态。换句话说,只有在按住ctrl的情况下释放其它按键,才能触发keyup.ctrl。而单单释放ctrl也不会触发事件。如果你想要这样的行为,请为ctrl换用keyCode:keyup.17

.exact修饰符允许你控制由精确的系统修饰符组合触发的事件。

<!-- 即使Alt或Shift被一同按下时也会触发 -->
<button @click.ctrl="onClick">A</button>

<!-- 有且只有Ctrl被按下的时候才触发 -->
<button @click.ctrl.exact="onCtrlClick">A</button>

<!-- 没有任何系统修饰符被按下的时候才触发 -->
<button @click.exact="onClick">A</button>

.left,.right.middle这些修饰符会限制处理函数仅响应特定的鼠标按钮。

你可能注意到这种事件监听的方式违背了关注点分离(separation of concern)这个长期以来的优良传统。但不必担心,因为所有的Vue.js事件处理方法和表达式都严格绑定在当前视图的ViewModel上,它不会导致任何维护上的困难。实际上,使用v-on有几个好处:

  1. 扫一眼HTML模板便能轻松定位在JavaScript代码里对应的方法。
  2. 因为你无须在JavaScript里手动绑定事件,你的ViewModel代码可以是非常纯粹的逻辑,和DOM完全解耦,更易于测试。
  3. 当一个ViewModel被销毁时,所有的事件处理器都会自动被删除。你无须担心如何清理它们。

4.1.8 表单输入绑定

你可以用v-model指令在表单<input><textarea><select>元素上创建双向数据绑定。它会根据控件类型自动选取正确的方法来更新元素。尽管有些神奇,但v-model本质上不过是语法糖。它负责监听用户的输入事件以更新数据,并对一些极端场景进行一些特殊处理。

v-model会忽略所有表单元素的valuecheckedselected特性的初始值而总是将Vue实例的数据作为数据来源。你应该通过JavaScript在组件的data选项中声明初始值。

v-model在内部为不同的输入元素使用不同的属性并抛出不同的事件:

  1. texttextarea元素使用value属性和input事件;
  2. checkboxradio使用checked属性和change事件;
  3. select字段将value作为prop并将change作为事件。

对于需要使用输入法(如中文、日文、韩文等)的语言,你会发现v-model不会在输入法组合文字过程中得到更新。如果你也想处理这个过程,请使用input事件。

<input v-model="message" placeholder="edit me">
<p>Message is: {{ message }}</p>
<span>Multiline message is:</span>
<p style="white-space: pre-line;">{{ message }}</p>
<br>
<textarea v-model="message" placeholder="add multiple lines"></textarea>

在文本区域插值(<textarea>{{text}}</textarea>)并不会生效,应用v-model来代替。

单个复选框,绑定到布尔值:

<input type="checkbox" id="checkbox" v-model="checked">
<label for="checkbox">{{ checked }}</label>

多个复选框,绑定到同一个数组:

<div id='example-3'>
    <input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
    <label for="jack">Jack</label>
    <input type="checkbox" id="john" value="John" v-model="checkedNames">
    <label for="john">John</label>
    <input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
    <label for="mike">Mike</label>
    <br>
    <span>Checked names: {{ checkedNames }}</span>
</div>
new Vue({
    el: '#example-3',
    data: {
        checkedNames: []
    }
})
<div id="example-4">
    <input type="radio" id="one" value="One" v-model="picked">
    <label for="one">One</label>
    <br>
    <input type="radio" id="two" value="Two" v-model="picked">
    <label for="two">Two</label>
    <br>
    <span>Picked: {{ picked }}</span>
</div>
new Vue({
    el: '#example-4',
    data: {
        picked: ''
    }
})

单选时:

<div id="example-5">
    <select v-model="selected">
        <option disabled value="">请选择</option>
        <option>A</option>
        <option>B</option>
        <option>C</option>
    </select>
    <span>Selected: {{ selected }}</span>
</div>
new Vue({
    el: '...',
    data: {
        selected: ''
    }
})

如果v-model表达式的初始值未能匹配任何选项,<select>元素将被渲染为“未选中”状态。在iOS中,这会使用户无法选择第一个选项。因为这样的情况下,iOS不会触发change事件。因此,更推荐像上面这样提供一个值为空的禁用选项。

多选时(绑定到一个数组):

<div id="example-6">
    <select v-model="selected" multiple style="width: 50px;">
        <option>A</option>
        <option>B</option>
        <option>C</option>
    </select>
    <br>
    <span>Selected: {{ selected }}</span>
</div>
new Vue({
    el: '#example-6',
    data: {
        selected: []
    }
})

v-for渲染的动态选项:

<select v-model="selected">
    <option v-for="option in options" v-bind:value="option.value">
        {{ option.text }}
    </option>
</select>
<span>Selected: {{ selected }}</span>
new Vue({
    el: '...',
    data: {
        selected: 'A',
        options: [
            { text: 'One', value: 'A' },
            { text: 'Two', value: 'B' },
            { text: 'Three', value: 'C' }
        ]
    }
})

对于单选按钮,复选框及选择框的选项,v-model绑定的值通常是静态字符串(对于复选框也可以是布尔值):

<!-- 当选中时,picked为字符串"a" -->
<input type="radio" v-model="picked" value="a">

<!-- toggle为true或false -->
<input type="checkbox" v-model="toggle">

<!-- 当选中第一个选项时,selected为字符串"abc" -->
<select v-model="selected">
  <option value="abc">ABC</option>
</select>

但是有时我们可能想把值绑定到Vue实例的一个动态属性上,这时可以用v-bind实现,并且这个属性的值可以不是字符串。

<input
    type="checkbox"
    v-model="toggle"
    true-value="yes"
    false-value="no"
>
// 当选中时
vm.toggle === 'yes'
// 当没有选中时
vm.toggle === 'no'

这里的true-valuefalse-value特性并不会影响输入控件的value特性,因为浏览器在提交表单时并不会包含未被选中的复选框。如果要确保表单中这两个值中的一个能够被提交,(比如“yes”或“no”),请换用单选按钮。

<input type="radio" v-model="pick" v-bind:value="a">
// 当选中时
vm.pick === vm.a
<select v-model="selected">
    <!-- 内联对象字面量 -->
    <option v-bind:value="{ number: 123 }">123</option>
</select>
// 当选中时
typeof vm.selected // => 'object'
vm.selected.number // => 123

在默认情况下,v-model在每次input事件触发后将输入框的值与数据进行同步(除了上述输入法组合文字时)。你可以添加lazy修饰符,从而转变为使用change事件进行同步:

<!-- 在“change”时而非“input”时更新 -->
<input v-model.lazy="msg" >

如果想自动将用户的输入值转为数值类型,可以给v-model添加number修饰符:

<input v-model.number="age" type="number">

这通常很有用,因为即使在type="number"时,HTML输入元素的值也总会返回字符串。如果这个值无法被parseFloat()解析,则会返回原始的值。

如果要自动过滤用户输入的首尾空白字符,可以给v-model添加trim修饰符:

<input v-model.trim="msg">

HTML原生的输入元素类型并不总能满足需求。幸好,Vue的组件系统允许你创建具有完全自定义行为且可复用的输入组件。这些输入组件甚至可以和v-model一起使用!要了解更多,请参阅组件指南中的自定义输入组件

4.1.9 组件基础

这里有一个Vue组件的示例:

// 定义一个名为 button-counter 的新组件
Vue.component('button-counter', {
    data: function () {
        return {
            count: 0
        }
    },
    template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})

组件是可复用的Vue实例,且带有一个名字:在这个例子中是<button-counter >。我们可以在一个通过new Vue创建的Vue根实例中,把这个组件作为自定义元素来使用:

<div id="components-demo">
    <button-counter></button-counter>
</div>
new Vue({ el: '#components-demo' })

因为组件是可复用的Vue实例,所以它们与new Vue接收相同的选项,例如datacomputedwatchmethods以及生命周期钩子等。仅有的例外是像el这样根实例特有的选项。

你可以将组件进行任意次数的复用:

<div id="components-demo">
    <button-counter></button-counter>
    <button-counter></button-counter>
    <button-counter></button-counter>
</div>

注意当点击按钮时,每个组件都会各自独立维护它的count。因为你每用一次组件,就会有一个它的新实例被创建。

当我们定义这个<button-counter >组件时,你可能会发现它的data并不是像这样直接提供一个对象:

data: {
    count: 0
}

取而代之的是,一个组件的data选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝:

data: function () {
    return {
        count: 0
    }
}

如果Vue没有这条规则,点击一个按钮就可能会像如下代码一样影响到其它所有实例。

通常一个应用会以一棵嵌套的组件树的形式来组织:

例如,你可能会有页头、侧边栏、内容区等组件,每个组件又包含了其它的像导航链接、博文之类的组件。为了能在模板中使用,这些组件必须先注册以便Vue能够识别。这里有两种组件的注册类型:全局注册和局部注册。至此,我们的组件都只是通过Vue.component全局注册的:

Vue.component('my-component-name', {
    // ... options ...
})

全局注册的组件可以用在其被注册之后的任何(通过new Vue)新创建的Vue根实例,也包括其组件树中的所有子组件的模板中。

早些时候,我们提到了创建一个博文组件的事情。问题是如果你不能向这个组件传递某一篇博文的标题或内容之类的我们想展示的数据的话,它是没有办法使用的。这也正是prop的由来。

Prop是你可以在组件上注册的一些自定义特性。当一个值传递给一个prop特性的时候,它就变成了那个组件实例的一个属性。为了给博文组件传递一个标题,我们可以用一个props选项将其包含在该组件可接受的prop列表中:

Vue.component('blog-post', {
    props: ['title'],
    template: '<h3>{{ title }}</h3>'
})

一个组件默认可以拥有任意数量的prop,任何值都可以传递给任何prop。在上述模板中,你会发现我们能够在组件实例中访问这个值,就像访问data中的值一样。

一个prop被注册之后,你就可以像这样把数据作为一个自定义特性传递进来:

<blog-post title="My journey with Vue"></blog-post>
<blog-post title="Blogging with Vue"></blog-post>
<blog-post title="Why Vue is so fun"></blog-post>

然而在一个典型的应用中,你可能在data里有一个博文的数组:

new Vue({
    el: '#blog-post-demo',
    data: {
        posts: [
            { id: 1, title: 'My journey with Vue' },
            { id: 2, title: 'Blogging with Vue' },
            { id: 3, title: 'Why Vue is so fun' }
        ]
    }
})

并想要为每篇博文渲染一个组件:

<blog-post
    v-for="post in posts"
    v-bind:key="post.id"
    v-bind:title="post.title"
></blog-post>

如上所示,你会发现我们可以使用v-bind来动态传递prop。这在你一开始不清楚要渲染的具体内容,比如从一个API获取博文列表的时候,是非常有用的。到目前为止,关于prop你需要了解的大概就这些了,如果你阅读完本页内容并掌握了它的内容,我们会推荐你再回来把prop读完。

当构建一个<blog-post >组件时,你的模板最终会包含的东西远不止一个标题:

<h3>{{ title }}</h3>

最最起码,你会包含这篇博文的正文:

<h3>{{ title }}</h3>
<div v-html="content"></div>

然而如果你在模板中尝试这样写,Vue会显示一个错误,并解释道every component must have a single root element(每个组件必须只有一个根元素)。你可以将模板的内容包裹在一个父元素内,来修复这个问题,例如:

<div class="blog-post">
    <h3>{{ title }}</h3>
    <div v-html="content"></div>
</div>

看起来当组件变得越来越复杂的时候,我们的博文不只需要标题和内容,还需要发布日期、评论等等。为每个相关的信息定义一个prop会变得很麻烦:

<blog-post
    v-for="post in posts"
    v-bind:key="post.id"
    v-bind:title="post.title"
    v-bind:content="post.content"
    v-bind:publishedAt="post.publishedAt"
    v-bind:comments="post.comments"
></blog-post>

所以是时候重构一下这个<blog-post >组件了,让它变成接受一个单独的post prop:

<blog-post
    v-for="post in posts"
    v-bind:key="post.id"
    v-bind:post="post"
></blog-post>
Vue.component('blog-post', {
    props: ['post'],
    template: `
        <div class="blog-post">
            <h3>{{ post.title }}</h3>
            <div v-html="post.content"></div>
        </div>
    `
})

上述的这个和一些接下来的示例使用了JavaScript的模板字符串来让多行的模板更易读。它们在IE下并没有被支持,所以如果你需要在不(经过Babel或TypeScript之类的工具)编译的情况下支持IE,请使用折行转义字符取而代之。

现在,不论何时为post对象添加一个新的属性,它都会自动地在<blog-post >内可用。

在我们开发<blog-post >组件时,它的一些功能可能要求我们和父级组件进行沟通。例如我们可能会引入一个辅助功能来放大博文的字号,同时让页面的其它部分保持默认的字号。在其父组件中,我们可以通过添加一个postFontSize数据属性来支持这个功能:

new Vue({
    el: '#blog-posts-events-demo',
    data: {
        posts: [/* ... */],
        postFontSize: 1
    }
})

它可以在模板中用来控制所有博文的字号:

<div id="blog-posts-events-demo">
    <div :style="{ fontSize: postFontSize + 'em' }">
        <blog-post
            v-for="post in posts"
            v-bind:key="post.id"
            v-bind:post="post"
        ></blog-post>
    </div>
</div>

现在我们在每篇博文正文之前添加一个按钮来放大字号:

Vue.component('blog-post', {
    props: ['post'],
    template: `
        <div class="blog-post">
            <h3>{{ post.title }}</h3>
            <button>
                Enlarge text
            </button>
            <div v-html="post.content"></div>
        </div>
        `
})

问题是这个按钮不会做任何事:

<button>
    Enlarge text
</button>

当点击这个按钮时,我们需要告诉父级组件放大所有博文的文本。幸好Vue实例提供了一个自定义事件的系统来解决这个问题。父级组件可以像处理native DOM事件一样通过v-on监听子组件实例的任意事件:

<blog-post
    ...
    v-on:enlarge-text="postFontSize += 0.1"
></blog-post>

同时子组件可以通过调用内建的$emit方法并传入事件名称来触发一个事件:

<button v-on:click="$emit('enlarge-text')">
    Enlarge text
</button>

有了这个v-on:enlarge-text="postFontSize += 0.1"监听器,父级组件就会接收该事件并更新postFontSize的值。

有的时候用一个事件来抛出一个特定的值是非常有用的。例如我们可能想让<blog-post >组件决定它的文本要放大多少。这时可以使用$emit的第二个参数来提供这个值:

<button v-on:click="$emit('enlarge-text', 0.1)">
    Enlarge text
</button>

然后当在父级组件监听这个事件的时候,我们可以通过$event访问到被抛出的这个值:

<blog-post
    ...
    v-on:enlarge-text="postFontSize += $event"
></blog-post>

或者,如果这个事件处理函数是一个方法:

<blog-post
    ...
    v-on:enlarge-text="onEnlargeText"
></blog-post>

那么这个值将会作为第一个参数传入这个方法:

methods: {
    onEnlargeText: function (enlargeAmount) {
        this.postFontSize += enlargeAmount
    }
}

自定义事件也可以用于创建支持v-model的自定义输入组件。记住:

<input v-model="searchText">

等价于:

<input
    v-bind:value="searchText"
    v-on:input="searchText = $event.target.value"
>

当用在组件上时,v-model则会这样:

<custom-input
    v-bind:value="searchText"
    v-on:input="searchText = $event"
></custom-input>

为了让它正常工作,这个组件内的<input>必须:

  1. 将其value特性绑定到一个名叫valueprop
  2. 在其input事件被触发时,将新的值通过自定义的input事件抛出

写成代码之后是这样的:

Vue.component('custom-input', {
    props: ['value'],
    template: `
        <input
            v-bind:value="value"
            v-on:input="$emit('input', $event.target.value)"
        >
    `
})

现在v-model就应该可以在这个组件上完美地工作起来了:<custom-input v-model="searchText"></custom-input>

到目前为止,关于组件自定义事件你需要了解的大概就这些了,如果你阅读完本页内容并掌握了它的内容,我们会推荐你再回来把自定义事件读完。

和HTML元素一样,我们经常需要向一个组件传递内容,像这样:

<alert-box>
    Something bad happened.
</alert-box>

幸好,Vue自定义的<slot>元素让这变得非常简单:

Vue.component('alert-box', {
    template: `
        <div class="demo-alert-box">
            <strong>Error!</strong>
            <slot></slot>
        </div>
    `
})

如你所见,我们只要在需要的地方加入插槽就行了——就这么简单! 到目前为止,关于插槽你需要了解的大概就这些了,如果你阅读完本页内容并掌握了它的内容,我们会推荐你再回来把插槽读完。

有的时候,在不同组件之间进行动态切换是非常有用的,比如在一个多标签的界面来回切换,可以通过Vue的<component>元素加一个特殊的is特性来实现:

<!-- 组件会在 `currentTabComponent` 改变时改变 -->
<component v-bind:is="currentTabComponent"></component>

在上述示例中,currentTabComponent可以包括

  1. 已注册组件的名字,或
  2. 一个组件的选项对象

你可以在这里查阅并体验完整的代码,或在这个版本了解绑定组件选项对象,而不是已注册组件名的示例。

有些HTML元素,诸如<ul><ol><table><select>,对于哪些元素可以出现在其内部是有严格限制的。而有些元素,诸如<li><tr><option>,只能出现在其它某些特定的元素内部。

这会导致我们使用这些有约束条件的元素时遇到一些问题。例如:

<table>
    <blog-post-row></blog-post-row>
</table>

这个自定义组件<blog-post-row\>会被作为无效的内容提升到外部,并导致最终渲染结果出错。幸好这个特殊的is特性给了我们一个变通的办法:

<table>
    <tr is="blog-post-row"></tr>
</table>

需要注意的是如果我们从以下来源使用模板的话,这条限制是不存在的:

  1. 字符串(例如:template: '...')
  2. 单文件组件(.vue)
  3. <script type="text/x-template">

到这里,你需要了解的解析DOM模板时的注意事项——实际上也是Vue的全部必要内容,大概就是这些了。

4.2深入了解组件

4.2.1 组件注册

该页面假设你已经阅读过了组件基础。如果你还对组件不太了解,推荐你先阅读它。

在注册一个组件的时候,我们始终需要给它一个名字。比如在全局注册的时候我们已经看到了:

Vue.component('my-component-name', { /* ... */ })

该组件名就是Vue.component的第一个参数。

你给予组件的名字可能依赖于你打算拿它来做什么。当直接在DOM中使用一个组件(而不是在字符串模板或单文件组件)的时候,我们强烈推荐遵循W3C规范中的自定义组件名(字母全小写且必须包含一个连字符)。这会帮助你避免和当前以及未来的HTML元素相冲突。你可以在风格指南中查阅到关于组件名的其它建议。

定义组件名的方式有两种:

  1. 使用kebab-case
Vue.component('my-component-name', { /* ... */ })

当使用kebab-case(短横线分隔命名)定义一个组件时,你也必须在引用这个自定义元素时使用kebab-case,例如<my-component-name >

  1. 使用PascalCase
Vue.component('MyComponentName', { /* ... */ })

当使用PascalCase(首字母大写命名)定义一个组件时,你在引用这个自定义元素时两种命名法都可以使用。也就是说<my-component-name ><MyComponentName\>都是可接受的。注意,尽管如此,直接在DOM(即非字符串的模板)中使用时只有kebab-case是有效的。

到目前为止,我们只用过Vue.component来创建组件:

Vue.component('my-component-name',{
    // ... 选项 ...
})

这些组件是全局注册的。也就是说它们在注册之后可以用在任何新创建的Vue根实例(new Vue)的模板中。比如:

Vue.component('component-a', { /* ... */ })
Vue.component('component-b', { /* ... */ })
Vue.component('component-c', { /* ... */ })

new Vue({ el: '#app' })
<div id="app">
    <component-a></component-a>
    <component-b></component-b>
    <component-c></component-c>
</div>

在所有子组件中也是如此,也就是说这三个组件在各自内部也都可以相互使用。

全局注册往往是不够理想的。比如,如果你使用一个像webpack这样的构建系统,全局注册所有的组件意味着即便你已经不再使用一个组件了,它仍然会被包含在你最终的构建结果中。这造成了用户下载的JavaScript的无谓的增加。在这些情况下,你可以通过一个普通的JavaScript对象来定义组件:

var ComponentA = { /* ... */ }
var ComponentB = { /* ... */ }
var ComponentC = { /* ... */ }

然后在components选项中定义你想要使用的组件:

new Vue({
  el: '#app',
  components: {
    'component-a': ComponentA,
    'component-b': ComponentB
  }
})

对于components对象中的每个属性来说,其属性名就是自定义元素的名字,其属性值就是这个组件的选项对象。注意局部注册的组件在其子组件中不可用。例如,如果你希望ComponentAComponentB中可用,则你需要这样写:

var ComponentA = { /* ... */ }

var ComponentB = {
  components: {
    'component-a': ComponentA
  },
  // ...
}

或者如果你通过Babel和webpack使用ES2015模块,那么代码看起来更像:

import ComponentA from './ComponentA.vue'

export default {
  components: {
    ComponentA
  },
  // ...
}

注意在ES2015+中,在对象中放一个类似ComponentA的变量名其实是ComponentA: ComponentA的缩写,即这个变量名同时是:

  1. 用在模板中的自定义元素的名称
  2. 包含了这个组件选项的变量名

如果你没有通过import/require使用一个模块系统,也许可以暂且跳过这个章节。如果你使用了,那么我们会为你提供一些特殊的使用说明和注意事项。

如果你还在阅读,说明你使用了诸如Babel和webpack的模块系统。在这些情况下,我们推荐创建一个components目录,并将每个组件放置在其各自的文件中。

然后你需要在局部注册之前导入每个你想使用的组件。例如,在一个假设的ComponentB.jsComponentB.vue文件中:

import ComponentA from './ComponentA'
import ComponentC from './ComponentC'

export default {
  components: {
    ComponentA,
    ComponentC
  },
  // ...
}

现在ComponentAComponentC都可以在ComponentB的模板中使用了。

可能你的许多组件只是包裹了一个输入框或按钮之类的元素,是相对通用的。我们有时候会把它们称为基础组件,它们会在各个组件中被频繁的用到。所以会导致很多组件里都会有一个包含基础组件的长列表:

import BaseButton from './BaseButton.vue'
import BaseIcon from './BaseIcon.vue'
import BaseInput from './BaseInput.vue'

export default {
  components: {
    BaseButton,
    BaseIcon,
    BaseInput
  }
}

而只是用于模板中的一小部分:

<BaseInput
  v-model="searchText"
  @keydown.enter="search"
/>
<BaseButton @click="search">
  <BaseIcon name="search"/>
</BaseButton>

幸好如果你使用了webpack(或在内部使用了webpack的Vue CLI 3+),那么就可以使用require.context只全局注册这些非常通用的基础组件。这里有一份可以让你在应用入口文件(比如src/main.js)中全局导入基础组件的示例代码:

import Vue from 'vue'
import upperFirst from 'lodash/upperFirst'
import camelCase from 'lodash/camelCase'

const requireComponent = require.context(
  // 其组件目录的相对路径
  './components',
  // 是否查询其子目录
  false,
  // 匹配基础组件文件名的正则表达式
  /Base[A-Z]\w+\.(vue|js)$/
)

requireComponent.keys().forEach(fileName => {
  // 获取组件配置
  const componentConfig = requireComponent(fileName)

  // 获取组件的 PascalCase 命名
  const componentName = upperFirst(
    camelCase(
      // 获取和目录深度无关的文件名
      fileName
        .split('/')
        .pop()
        .replace(/\.\w+$/, '')
    )
  )

  // 全局注册组件
  Vue.component(
    componentName,
    // 如果这个组件选项是通过 `export default` 导出的,
    // 那么就会优先使用 `.default`,
    // 否则回退到使用模块的根。
    componentConfig.default || componentConfig
  )
})

记住全局注册的行为必须在根Vue实例(通过new Vue)创建之前发生。这里有一个真实项目情景下的示例。

4.2.2 Prop

HTML中的特性名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。这意味着当你使用DOM中的模板时,camelCase(驼峰命名法)的prop名需要使用其等价的kebab-case(短横线分隔命名)命名:

Vue.component('blog-post', {
  // 在 JavaScript 中是 camelCase 的
  props: ['postTitle'],
  template: '<h3>{{ postTitle }}</h3>'
})
<!-- 在 HTML 中是 kebab-case 的 -->
<blog-post post-title="hello!"></blog-post>

重申一次,如果你使用字符串模板,那么这个限制就不存在了。

到这里,我们只看到了以字符串数组形式列出的prop:

props: ['title', 'likes', 'isPublished', 'commentIds', 'author']

但是,通常你希望每个prop都有指定的值类型。这时,你可以以对象形式列出prop,这些属性的名称和值分别是prop各自的名称和类型:

props: {
  title: String,
  likes: Number,
  isPublished: Boolean,
  commentIds: Array,
  author: Object,
  callback: Function,
  contactsPromise: Promise // or any other constructor
}

这不仅为你的组件提供了文档,还会在它们遇到错误的类型时从浏览器的JavaScript控制台提示用户。你会在这个页面接下来的部分看到类型检查和其它prop验证

像这样,你已经知道了可以像这样给prop传入一个静态的值:

<blog-post title="My journey with Vue"></blog-post>

你也知道prop可以通过v-bind动态赋值,例如:

<!-- 动态赋予一个变量的值 -->
<blog-post v-bind:title="post.title"></blog-post>

<!-- 动态赋予一个复杂表达式的值 -->
<blog-post
  v-bind:title="post.title + ' by ' + post.author.name"
></blog-post>

在上述两个示例中,我们传入的值都是字符串类型的,但实际上任何类型的值都可以传给一个prop。

<!-- 即便42是静态的,我们仍然需要v-bind来告诉Vue -->
<!-- 这是一个JavaScript表达式而不是一个字符串。-->
<blog-post v-bind:likes="42"></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:likes="post.likes"></blog-post>
<!-- 包含该prop 没有值的情况在内,都意味着true。-->
<blog-post is-published></blog-post>

<!-- 即便false是静态的,我们仍然需要v-bind来告诉Vue -->
<!-- 这是一个JavaScript表达式而不是一个字符串。-->
<blog-post v-bind:is-published="false"></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:is-published="post.isPublished"></blog-post>
<!-- 即便数组是静态的,我们仍然需要v-bind来告诉Vue -->
<!-- 这是一个JavaScript表达式而不是一个字符串。-->
<blog-post v-bind:comment-ids="[234, 266, 273]"></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:comment-ids="post.commentIds"></blog-post>
传入一个对象
<!-- 即便对象是静态的,我们仍然需要v-bind来告诉 Vue -->
<!-- 这是一个JavaScript表达式而不是一个字符串。-->
<blog-post
  v-bind:author="{
    name: 'Veronica',
    company: 'Veridian Dynamics'
  }"
></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:author="post.author"></blog-post>

如果你想要将一个对象的所有属性都作为prop传入,你可以使用不带参数的v-bind(取代v-bind:prop-name)。例如,对于一个给定的对象post:

post: {
  id: 1,
  title: 'My Journey with Vue'
}

下面的模板:

<blog-post v-bind="post"></blog-post>

等价于:

<blog-post
  v-bind:id="post.id"
  v-bind:title="post.title"
></blog-post>

所有的prop都使得其父子prop之间形成了一个单向下行绑定:父级prop的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。

额外的,每次父级组件发生更新时,子组件中所有的prop都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变prop。如果你这样做了,Vue会在浏览器的控制台中发出警告。

这里有两种常见的试图改变一个prop的情形:

  1. 这个prop用来传递一个初始值;这个子组件接下来希望将其作为一个本地的prop数据来使用。在这种情况下,最好定义一个本地的data属性并将这个prop用作其初始值:
props: ['initialCounter'],
data: function () {
  return {
    counter: this.initialCounter
  }
}
  1. 这个prop以一种原始的值传入且需要进行转换。在这种情况下,最好使用这个prop的值来定义一个计算属性:
props: ['size'],
computed: {
  normalizedSize: function () {
    return this.size.trim().toLowerCase()
  }
}

注意在JavaScript中对象和数组是通过引用传入的,所以对于一个数组或对象类型的prop来说,在子组件中改变这个对象或数组本身将会影响到父组件的状态。

我们可以为组件的prop指定验证要求,例如你知道的这些类型。如果有一个需求没有被满足,则Vue会在浏览器控制台中警告你。这在开发一个会被别人用到的组件时尤其有帮助。

为了定制prop的验证方式,你可以为props中的值提供一个带有验证需求的对象,而不是一个字符串数组。例如:

Vue.component('my-component', {
  props: {
    // 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
    propA: Number,
    // 多个可能的类型
    propB: [String, Number],
    // 必填的字符串
    propC: {
      type: String,
      required: true
    },
    // 带有默认值的数字
    propD: {
      type: Number,
      default: 100
    },
    // 带有默认值的对象
    propE: {
      type: Object,
      // 对象或数组默认值必须从一个工厂函数获取
      default: function () {
        return { message: 'hello' }
      }
    },
    // 自定义验证函数
    propF: {
      validator: function (value) {
        // 这个值必须匹配下列字符串中的一个
        return ['success', 'warning', 'danger'].indexOf(value) !== -1
      }
    }
  }
})

prop验证失败的时候,(开发环境构建版本的)Vue将会产生一个控制台的警告。

注意那些prop会在一个组件实例创建之前进行验证,所以实例的属性(如datacomputed等)在defaultvalidator函数中是不可用的。

type可以是下列原生构造函数中的一个:String,Number,Boolean,Array,Object,Date,FunctionSymbol

额外的,type还可以是一个自定义的构造函数,并且通过instanceof来进行检查确认。例如,给定下列现成的构造函数:

function Person (firstName, lastName) {
  this.firstName = firstName
  this.lastName = lastName
}

你可以使用:

Vue.component('blog-post', {
  props: {
    author: Person
  }
})

来验证author prop的值是否是通过new Person创建的。

一个非prop特性是指传向一个组件,但是该组件并没有相应prop定义的特性。

因为显式定义的prop适用于向一个子组件传入信息,然而组件库的作者并不总能预见组件会被用于怎样的场景。这也是为什么组件可以接受任意的特性,而这些特性会被添加到这个组件的根元素上。

例如,想象一下你通过一个Bootstrap插件使用了一个第三方的<bootstrap-date-input >组件,这个插件需要在其<input\>上用到一个data-date-picker特性。我们可以将这个特性添加到你的组件实例上:

<bootstrap-date-input data-date-picker="activated"></bootstrap-date-input>

然后这个data-date-picker="activated"特性就会自动添加到<bootstrap-date-input >的根元素上。

想象一下<bootstrap-date-input >的模板是这样的:

<input type="date" class="form-control">

为了给我们的日期选择器插件定制一个主题,我们可能需要像这样添加一个特别的类名:

<bootstrap-date-input
  data-date-picker="activated"
  class="date-picker-theme-dark"
></bootstrap-date-input>

在这种情况下,我们定义了两个不同的class的值:

  1. form-control,这是在组件的模板内设置好的
  2. date-picker-theme-dark,这是从组件的父级传入的

对于绝大多数特性来说,从外部提供给组件的值会替换掉组件内部设置好的值。所以如果传入type="text"就会替换掉type="date"并把它破坏!庆幸的是,classstyle特性会稍微智能一些,即两边的值会被合并起来,从而得到最终的值:form-control,date-picker-theme-dark

如果你不希望组件的根元素继承特性,你可以在组件的选项中设置inheritAttrs: false。例如:

Vue.component('my-component', {
  inheritAttrs: false,
  // ...
})

这尤其适合配合实例的$attrs属性使用,该属性包含了传递给一个组件的特性名和特性值,例如:

{
  required: true,
  placeholder: 'Enter your username'
}

有了inheritAttrs: false$attrs,你就可以手动决定这些特性会被赋予哪个元素。在撰写基础组件的时候是常会用到的:

Vue.component('base-input', {
  inheritAttrs: false,
  props: ['label', 'value'],
  template: `
    <label>
      {{ label }}
      <input
        v-bind="$attrs"
        v-bind:value="value"
        v-on:input="$emit('input', $event.target.value)"
      >
    </label>
  `
})

注意inheritAttrs: false选项不会影响styleclass的绑定。

这个模式允许你在使用基础组件的时候更像是使用原始的HTML元素,而不会担心哪个元素是真正的根元素:

<base-input
  v-model="username"
  required
  placeholder="Enter your username"
></base-input>

4.2.3 自定义事件

不同于组件和prop,事件名不存在任何自动化的大小写转换。而是触发的事件名需要完全匹配监听这个事件所用的名称。举个例子,如果触发一个camelCase名字的事件:

this.$emit('myEvent')

则监听这个名字的kebab-case版本是不会有任何效果的:

<!-- 没有效果 -->
<my-component v-on:my-event="doSomething"></my-component>

不同于组件和prop,事件名不会被用作一个JavaScript变量名或属性名,所以就没有理由使用camelCasePascalCase了。并且v-on事件监听器在DOM模板中会被自动转换为全小写(因为HTML是大小写不敏感的),所以v-on:myEvent将会变成v-on:myevent——导致myEvent不可能被监听到。

因此,我们推荐你始终使用kebab-case的事件名。

一个组件上的v-model默认会利用名为valueprop和名为input的事件,但是像单选框、复选框等类型的输入控件可能会将value特性用于不同的目的。model选项可以用来避免这样的冲突:

Vue.component('base-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean
  },
  template: `
    <input
      type="checkbox"
      v-bind:checked="checked"
      v-on:change="$emit('change', $event.target.checked)"
    >
  `
})

现在在这个组件上使用v-model的时候:

<base-checkbox v-model="lovingVue"></base-checkbox>

这里的lovingVue的值将会传入这个名为checkedprop。同时当<base-checkbox >触发一个change事件并附带一个新的值的时候,这个lovingVue的属性将会被更新。

注意你仍然需要在组件的props选项里声明checked这个prop

你可能有很多次想要在一个组件的根元素上直接监听一个原生事件。这时,你可以使用v-on.native修饰符:

<base-input v-on:focus.native="onFocus"></base-input>

在有的时候这是很有用的,不过在你尝试监听一个类似<input\>的非常特定的元素时,这并不是个好主意。比如上述<base-input >组件可能做了如下重构,所以根元素实际上是一个<label\>元素:

<label>
  {{ label }}
  <input
    v-bind="$attrs"
    v-bind:value="value"
    v-on:input="$emit('input', $event.target.value)"
  >
</label>

这时,父级的.native监听器将静默失败。它不会产生任何报错,但是onFocus处理函数不会如你预期地被调用。

为了解决这个问题,Vue提供了一个$listeners属性,它是一个对象,里面包含了作用在这个组件上的所有监听器。例如:

{
  focus: function (event) { /* ... */ }
  input: function (value) { /* ... */ },
}

有了这个$listeners属性,你就可以配合v-on="$listeners"将所有的事件监听器指向这个组件的某个特定的子元素。对于类似<input\>的你希望它也可以配合v-model工作的组件来说,为这些监听器创建一个类似下述inputListeners的计算属性通常是非常有用的:

Vue.component('base-input', {
  inheritAttrs: false,
  props: ['label', 'value'],
  computed: {
    inputListeners: function () {
      var vm = this
      // `Object.assign` 将所有的对象合并为一个新对象
      return Object.assign({},
        // 我们从父级添加所有的监听器
        this.$listeners,
        // 然后我们添加自定义监听器,
        // 或覆写一些监听器的行为
        {
          // 这里确保组件配合 `v-model` 的工作
          input: function (event) {
            vm.$emit('input', event.target.value)
          }
        }
      )
    }
  },
  template: `
    <label>
      {{ label }}
      <input
        v-bind="$attrs"
        v-bind:value="value"
        v-on="inputListeners"
      >
    </label>
  `
})

现在<base-input >组件是一个完全透明的包裹器了,也就是说它可以完全像一个普通的<input\>元素一样使用了:所有跟它相同的特性和监听器的都可以工作。

在有些情况下,我们可能需要对一个prop进行“双向绑定”。不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以修改父组件,且在父组件和子组件都没有明显的改动来源。

这也是为什么我们推荐以update:myPropName的模式触发事件取而代之。举个例子,在一个包含title prop的假设的组件中,我们可以用以下方法表达对其赋新值的意图:

this.$emit('update:title', newTitle)

然后父组件可以监听那个事件并根据需要更新一个本地的数据属性。例如:

<text-document
  v-bind:title="doc.title"
  v-on:update:title="doc.title = $event"
></text-document>

为了方便起见,我们为这种模式提供一个缩写,即.sync修饰符:

<text-document v-bind:title.sync="doc.title"></text-document>

注意带有.sync修饰符的v-bind不能和表达式一起使用(例如v-bind:title.sync=”doc.title + ‘!’”是无效的)。取而代之的是,你只能提供你想要绑定的属性名,类似v-model

当我们用一个对象同时设置多个prop的时候,也可以将这个.sync修饰符和v-bind配合使用:

<text-document v-bind.sync="doc"></text-document>

这样会把doc对象中的每一个属性(如title)都作为一个独立的prop传进去,然后各自添加用于更新的v-on监听器。

v-bind.sync用在一个字面量的对象上,例如v-bind.sync=”{ title: doc.title }”,是无法正常工作的,因为在解析一个像这样的复杂表达式的时候,有很多边缘情况需要考虑。

4.2.4 插槽

Vue实现了一套内容分发的API,这套API的设计灵感源自Web Components规范草案,将<slot\>元素作为承载分发内容的出口。它允许你像这样合成组件:

<navigation-link url="/profile">
  Your Profile
</navigation-link>

然后你在<navigation-link >的模板中可能会写为:

<a
  v-bind:href="url"
  class="nav-link"
>
  <slot></slot>
</a>

当组件渲染的时候,<slot></slot\>将会被替换为“Your Profile”。插槽内可以包含任何模板代码,包括HTML:

<navigation-link url="/profile">
    <!-- 添加一个 Font Awesome 图标 -->
    <span class="fa fa-user"></span>
    Your Profile
</navigation-link>

甚至其它的组件:

<navigation-link url="/profile">
    <!-- 添加一个图标的组件 -->
    <font-awesome-icon name="user"></font-awesome-icon>
    Your Profile
</navigation-link>

如果<navigation-link >没有包含一个<slot\>元素,则该组件起始标签和结束标签之间的任何内容都会被抛弃。

当你想在一个插槽中使用数据时,例如:

<navigation-link url="/profile">
    Logged in as {{ user.name }}
</navigation-link>

该插槽跟模板的其它地方一样可以访问相同的实例属性(也就是相同的“作用域”),而不能访问<navigation-link >的作用域。例如url是访问不到的:

<navigation-link url="/profile">
    Clicking here will send you to: {{ url }}
    <!--
    这里的url会是undefined,因为"/profile"是
     _传递给_<navigation-link>的而不是
    在<navigation-link>组件*内部*定义的。
    -->
</navigation-link>

作为一条规则,请记住:父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。

有时为一个插槽设置具体的后备(也就是默认的) 内容是很有用的,它只会在没有提供内容的时候被渲染。例如在一个<submit-button >组件中:

<button type="submit">
  <slot></slot>
</button>

我们可能希望这个<button\>内绝大多数情况下都渲染文本“Submit”。为了将“Submit”作为后备内容,我们可以将它放在<slot\>标签内:

<button type="submit">
  <slot>Submit</slot>
</button>

现在当我在一个父级组件中使用<submit-button >并且不提供任何插槽内容时:

<submit-button></submit-button>

后备内容“Submit”将会被渲染:

<button type="submit">
    Submit
</button>

但是如果我们提供内容:

<submit-button>
    Save
</submit-button>

则这个提供的内容将会被渲染从而取代后备内容:

<button type="submit">
    Save
</button>

有时我们需要多个插槽。例如对于一个带有如下模板的<base-layout >组件:

<div class="container">
    <header>
        <!-- 我们希望把页头放这里 -->
    </header>
    <main>
        <!-- 我们希望把主要内容放这里 -->
    </main>
    <footer>
        <!-- 我们希望把页脚放这里 -->
    </footer>
</div>

对于这样的情况,<slot\>元素有一个特殊的特性:name。这个特性可以用来定义额外的插槽:

<div class="container">
    <header>
        <slot name="header"></slot>
    </header>
    <main>
        <slot></slot>
    </main>
    <footer>
        <slot name="footer"></slot>
    </footer>
</div>

一个不带name<slot\>出口会带有隐含的名字“default”

在向具名插槽提供内容的时候,我们可以在一个<template\>元素上使用v-slot指令,并以v-slot的参数的形式提供其名称:

<base-layout>
    <template v-slot:header>
        <h1>Here might be a page title</h1>
    </template>

    <p>A paragraph for the main content.</p>
    <p>And another one.</p>

    <template v-slot:footer>
        <p>Here's some contact info</p>
    </template>
</base-layout>

现在<template\>元素中的所有内容都将会被传入相应的插槽。任何没有被包裹在带有v-slot<template\>中的内容都会被视为默认插槽的内容。

然而,如果你希望更明确一些,仍然可以在一个<template\>中包裹默认插槽的内容:

<base-layout>
    <template v-slot:header>
        <h1>Here might be a page title</h1>
    </template>

    <template v-slot:default>
        <p>A paragraph for the main content.</p>
        <p>And another one.</p>
    </template>

    <template v-slot:footer>
        <p>Here's some contact info</p>
    </template>
</base-layout>

任何一种写法都会渲染出:

<div class="container">
    <header>
        <h1>Here might be a page title</h1>
    </header>
    <main>
        <p>A paragraph for the main content.</p>
        <p>And another one.</p>
    </main>
    <footer>
        <p>Here's some contact info</p>
    </footer>
</div>

注意v-slot只能添加在<template\>上(只有一种例外情况),这一点和已经废弃的slot特性不同。

有时让插槽内容能够访问子组件中才有的数据是很有用的。例如,设想一个带有如下模板的<current-user >组件:

<span>
  <slot>{{ user.lastName }}</slot>
</span>

我们想让它的后备内容显示用户的名,以取代正常情况下用户的姓,如下:

<current-user>
  {{ user.firstName }}
</current-user>

然而上述代码不会正常工作,因为只有<current-user >组件可以访问到user而我们提供的内容是在父级渲染的。

为了让user在父级的插槽内容中可用,我们可以将user作为<slot\>元素的一个特性绑定上去:

<span>
    <slot v-bind:user="user">
        {{ user.lastName }}
    </slot>
</span>

绑定在<slot\>元素上的特性被称为插槽prop。现在在父级作用域中,我们可以给v-slot带一个值来定义我们提供的插槽prop的名字:

<current-user>
    <template v-slot:default="slotProps">
        {{ slotProps.user.firstName }}
    </template>
</current-user>

在这个例子中,我们选择将包含所有插槽prop的对象命名为slotProps,但你也可以使用任意你喜欢的名字。

在上述情况下,当被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用。这样我们就可以把v-slot直接用在组件上:

<current-user v-slot:default="slotProps">
    {{ slotProps.user.firstName }}
</current-user>

这种写法还可以更简单。就像假定未指明的内容对应默认插槽一样,不带参数的v-slot被假定对应默认插槽:

<current-user v-slot="slotProps">
  {{ slotProps.user.firstName }}
</current-user>

注意默认插槽的缩写语法不能和具名插槽混用,因为它会导致作用域不明确:

<!-- 无效,会导致警告 -->
<current-user v-slot="slotProps">
    {{ slotProps.user.firstName }}
    <template v-slot:other="otherSlotProps">
        slotProps is NOT available here
    </template>
</current-user>

只要出现多个插槽,请始终为所有的插槽使用完整的基于<template\>的语法:

<current-user>
    <template v-slot:default="slotProps">
        {{ slotProps.user.firstName }}
    </template>

    <template v-slot:other="otherSlotProps">
        ...
    </template>
</current-user>

作用域插槽的内部工作原理是将你的插槽内容包括在一个传入单个参数的函数里:

function (slotProps) {
  // 插槽内容
}

这意味着v-slot的值实际上可以是任何能够作为函数定义中的参数的JavaScript表达式。所以在支持的环境下(单文件组件或现代浏览器),你也可以使用ES2015解构来传入具体的插槽prop,如下:

<current-user v-slot="{ user }">
    {{ user.firstName }}
</current-user>

这样可以使模板更简洁,尤其是在该插槽提供了多个prop的时候。它同样开启了prop重命名等其它可能,例如将user重命名为person:

<current-user v-slot="{ user: person }">
    {{ person.firstName }}
</current-user>

你甚至可以定义后备内容,用于插槽propundefined的情形:

<current-user v-slot="{ user = { firstName: 'Guest' } }">
    {{ user.firstName }}
</current-user>

动态指令参数也可以用在v-slot上,来定义动态的插槽名:

<base-layout>
    <template v-slot:[dynamicSlotName]>
        ...
    </template>
</base-layout>

v-onv-bind一样,v-slot也有缩写,即把参数之前的所有内容(v-slot:)替换为字符#。例如v-slot:header可以被重写为#header:

<base-layout>
    <template #header>
        <h1>Here might be a page title</h1>
    </template>

    <p>A paragraph for the main content.</p>
    <p>And another one.</p>

    <template #footer>
        <p>Here's some contact info</p>
    </template>
</base-layout>

然而,和其它指令一样,该缩写只在其有参数的时候才可用。这意味着以下语法是无效的:

<!-- 这样会触发一个警告 -->
<current-user #="{ user }">
  {{ user.firstName }}
</current-user>

如果你希望使用缩写的话,你必须始终以明确插槽名取而代之:

<current-user #default="{ user }">
    {{ user.firstName }}
</current-user>

插槽prop允许我们将插槽转换为可复用的模板,这些模板可以基于输入的prop渲染出不同的内容。这在设计封装数据逻辑同时允许父级组件自定义部分布局的可复用组件时是最有用的。

例如,我们要实现一个<todo-list >组件,它是一个列表且包含布局和过滤逻辑:

<ul>
    <li
        v-for="todo in filteredTodos"
        v-bind:key="todo.id"
    >
        {{ todo.text }}
    </li>
</ul>

我们可以将每个todo作为父级组件的插槽,以此通过父级组件对其进行控制,然后将todo作为一个插槽prop进行绑定:

<ul>
  <li
    v-for="todo in filteredTodos"
    v-bind:key="todo.id"
  >
    <!--
    我们为每个 todo 准备了一个插槽,
    将 `todo` 对象作为一个插槽的 prop 传入。
    -->
    <slot name="todo" v-bind:todo="todo">
      <!-- 后备内容 -->
      {{ todo.text }}
    </slot>
  </li>
</ul>

现在当我们使用<todo-list >组件的时候,我们可以选择为todo定义一个不一样的<template\>作为替代方案,并且可以从子组件获取数据:

<todo-list v-bind:todos="todos">
    <template v-slot:todo="{ todo }">
        <span v-if="todo.isComplete">✓</span>
        {{ todo.text }}
        </template>
</todo-list>

这只是作用域插槽用武之地的冰山一角。想了解更多现实生活中的作用域插槽的用法,我们推荐浏览诸如Vue Virtual ScrollerVue PromisedPortal Vue等库。

4.2.5 动态组件&异步组件

我们之前曾经在一个多标签的界面中使用is特性来切换不同的组件:

<component v-bind:is="currentTabComponent"></component>

当在这些组件之间切换的时候,你有时会想保持这些组件的状态,以避免反复重渲染导致的性能问题。

重新创建动态组件的行为通常是非常有用的,但是在这个案例中,我们更希望那些标签的组件实例能够被在它们第一次被创建的时候缓存下来。为了解决这个问题,我们可以用一个<keep-alive>元素将其动态组件包裹起来。

<!-- 失活的组件将会被缓存!-->
<keep-alive>
    <component v-bind:is="currentTabComponent"></component>
</keep-alive>

现在这个Posts标签保持了它的状态(被选中的文章)甚至当它未被渲染时也是如此。你可以在这个fiddle查阅到完整的代码。

注意这个<keep-alive>要求被切换到的组件都有自己的名字,不论是通过组件的name选项还是局部/全局注册。

在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了简化,Vue允许你以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义。Vue只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染。例如:

Vue.component('async-example', function (resolve, reject) {
    setTimeout(function () {
        // 向resolve回调传递组件定义
        resolve({
            template: '<div>I am async!</div>'
        })
        }, 1000)
})

如你所见,这个工厂函数会收到一个resolve回调,这个回调函数会在你从服务器得到组件定义的时候被调用。你也可以调用reject(reason)来表示加载失败。这里的setTimeout是为了演示用的,如何获取组件取决于你自己。一个推荐的做法是将异步组件和webpack的code-splitting功能一起配合使用:

Vue.component('async-webpack-example', function (resolve) {
    // 这个特殊的require语法将会告诉webpack
    // 自动将你的构建代码切割成多个包,这些包
    // 会通过Ajax请求加载
  require(['./my-async-component'], resolve)
})

你也可以在工厂函数中返回一个Promise,所以把webpack 2和ES2015语法加在一起,我们可以写成这样:

Vue.component(
  'async-webpack-example',
  // 这个import函数会返回一个Promise对象。
  () => import('./my-async-component')
)

当使用局部注册的时候,你也可以直接提供一个返回Promise的函数:

new Vue({
    // ...
    components: {
        'my-component': () => import('./my-async-component')
    }
})

这里的异步组件工厂函数也可以返回一个如下格式的对象:

const AsyncComponent = () => ({
    // 需要加载的组件(应该是一个Promise对象)
    component: import('./MyComponent.vue'),
    // 异步组件加载时使用的组件
    loading: LoadingComponent,
    // 加载失败时使用的组件
    error: ErrorComponent,
    // 展示加载时组件的延时时间。默认值是200(毫秒)
    delay: 200,
    // 如果提供了超时时间且组件加载也超时了,
    // 则使用加载失败时使用的组件。默认值是:Infinity
    timeout: 3000
})

注意如果你希望在Vue Router的路由组件中使用上述语法的话,你必须使用Vue Router 2.4.0+版本。

4.2.6 处理边界情况

这里记录的都是和处理边界情况有关的功能,即一些需要对Vue的规则做一些小调整的特殊情况。不过注意这些功能都是有劣势或危险的场景的。我们会在每个案例中注明,所以当你使用每个功能的时候请稍加留意。

在绝大多数情况下,我们最好不要触达另一个组件实例内部或手动操作DOM元素。不过也确实在一些情况下做这些事情是合适的。

在每个new Vue实例的子组件中,其根实例可以通过$root属性进行访问。例如,在这个根实例中:

// Vue 根实例
new Vue({
    data: {
        foo: 1
    },
    computed: {
        bar: function () { /* ... */ }
    },
    methods: {
        baz: function () { /* ... */ }
    }
})

所有的子组件都可以将这个实例作为一个全局store来访问或使用。

// 获取根组件的数据
this.$root.foo
// 写入根组件的数据
this.$root.foo = 2
// 访问根组件的计算属性
this.$root.bar
// 调用根组件的方法
this.$root.baz()

对于demo或非常小型的有少量组件的应用来说这是很方便的。不过这个模式扩展到中大型应用来说就不然了。因此在绝大多数情况下,我们强烈推荐使用Vuex来管理应用的状态。

$root类似,$parent属性可以用来从一个子组件访问父组件的实例。它提供了一种机会,可以在后期随时触达父级组件,以替代将数据以prop的方式传入子组件的方式。

在绝大多数情况下,触达父级组件会使得你的应用更难调试和理解,尤其是当你变更了父级组件的数据的时候。当我们稍后回看那个组件的时候,很难找出那个变更是从哪里发起的。

另外在一些可能适当的时候,你需要特别地共享一些组件库。举个例子,在和JavaScript API进行交互而不渲染HTML的抽象组件内,诸如这些假设性的Google地图组件一样:

<google-map>
    <google-map-markers v-bind:places="iceCreamShops"></google-map-markers>
</google-map>

这个<google-map>组件可以定义一个map属性,所有的子组件都需要访问它。在这种情况下<google-map-markers>可能想要通过类似this.$parent.getMap的方式访问那个地图,以便为其添加一组标记。你可以在这里查阅这种模式。

请留意,尽管如此,通过这种模式构建出来的那个组件的内部仍然是容易出现问题的。比如,设想一下我们添加一个新的<google-map-region>组件,当<google-map-markers>在其内部出现的时候,只会渲染那个区域内的标记:

<google-map>
    <google-map-region v-bind:shape="cityBoundaries">
        <google-map-markers v-bind:places="iceCreamShops"></google-map-markers>
    </google-map-region>
</google-map>

那么在<google-map-markers>内部你可能发现自己需要一些类似这样的hack:

var map = this.$parent.map || this.$parent.$parent.map

很快它就会失控。这也是我们针对需要向任意更深层级的组件提供上下文信息时推荐依赖注入的原因。

尽管存在prop和事件,有的时候你仍可能需要在JavaScript里直接访问一个子组件。为了达到这个目的,你可以通过ref特性为这个子组件赋予一个ID引用。例如:

<base-input ref="usernameInput"></base-input>

现在在你已经定义了这个ref的组件里,你可以使用:

this.$refs.usernameInput

来访问这个<base-input>实例,以便不时之需。比如程序化地从一个父级组件聚焦这个输入框。在刚才那个例子中,该<base-input>组件也可以使用一个类似的ref提供对内部这个指定元素的访问,例如:

<input ref="input">

甚至可以通过其父级组件定义方法:

methods: {
    // 用来从父级组件聚焦输入框
    focus: function () {
        this.$refs.input.focus()
    }
}

这样就允许父级组件通过下面的代码聚焦<base-input>里的输入框:

this.$refs.usernameInput.focus()

refv-for一起使用的时候,你得到的引用将会是一个包含了对应数据源的这些子组件的数组。

$refs只会在组件渲染完成之后生效,并且它们不是响应式的。这仅作为一个用于直接操作子组件的“逃生舱”——你应该避免在模板或计算属性中访问$refs

在此之前,在我们描述访问父级组件实例的时候,展示过一个类似这样的例子:

<google-map>
    <google-map-region v-bind:shape="cityBoundaries">
        <google-map-markers v-bind:places="iceCreamShops"></google-map-markers>
    </google-map-region>
</google-map>

在这个组件里,所有<google-map>的后代都需要访问一个getMap方法,以便知道要跟哪个地图进行交互。不幸的是,使用$parent属性无法很好的扩展到更深层级的嵌套组件上。这也是依赖注入的用武之地,它用到了两个新的实例选项:provideinject

provide选项允许我们指定我们想要提供给后代组件的数据/方法。在这个例子中,就是<google-map>内部的getMap方法:

provide: function () {
    return {
        getMap: this.getMap
    }
}

然后在任何后代组件里,我们都可以使用inject选项来接收指定的我们想要添加在这个实例上的属性:

inject: ['getMap']

你可以在这里看到完整的示例。相比$parent来说,这个用法可以让我们在任意后代组件中访问getMap,而不需要暴露整个<google-map>实例。这允许我们更好的持续研发该组件,而不需要担心我们可能会改变/移除一些子组件依赖的东西。同时这些组件之间的接口是始终明确定义的,就和props一样。

实际上,你可以把依赖注入看作一部分“大范围有效的prop”,除了:

  1. 祖先组件不需要知道哪些后代组件使用它提供的属性
  2. 后代组件不需要知道被注入的属性来自哪里

然而,依赖注入还是有负面影响的。它将你应用程序中的组件与它们当前的组织方式耦合起来,使重构变得更加困难。同时所提供的属性是非响应式的。这是出于设计的考虑,因为使用它们来创建一个中心化规模化的数据跟使用$root做这件事都是不够好的。如果你想要共享的这个属性是你的应用特有的,而不是通用化的,或者如果你想在祖先组件中更新所提供的数据,那么这意味着你可能需要换用一个像Vuex这样真正的状态管理方案了。

你可以在API参考文档学习更多关于依赖注入的知识。

现在,你已经知道了$emit的用法,它可以被v-on侦听,但是Vue实例同时在其事件接口中提供了其它的方法。我们可以:

  1. 通过$on(eventName, eventHandler)侦听一个事件
  2. 通过$once(eventName, eventHandler)一次性侦听一个事件
  3. 通过$off(eventName, eventHandler)停止侦听一个事件

你通常不会用到这些,但是当你需要在一个组件实例上手动侦听事件时,它们是派得上用场的。它们也可以用于代码组织工具。例如,你可能经常看到这种集成一个第三方库的模式:

// 一次性将这个日期选择器附加到一个输入框上
// 它会被挂载到DOM上。
mounted: function () {
    // Pikaday是一个第三方日期选择器的库
    this.picker = new Pikaday({
        field: this.$refs.input,
        format: 'YYYY-MM-DD'
    })
},
// 在组件被销毁之前,
// 也销毁这个日期选择器。
beforeDestroy: function () {
    this.picker.destroy()
}

这里有两个潜在的问题:

  1. 它需要在这个组件实例中保存这个picker,如果可以的话最好只有生命周期钩子可以访问到它。这并不算严重的问题,但是它可以被视为杂物。
  2. 我们的建立代码独立于我们的清理代码,这使得我们比较难于程序化地清理我们建立的所有东西。

你应该通过一个程序化的侦听器解决这两个问题:

mounted: function () {
    var picker = new Pikaday({
        field: this.$refs.input,
        format: 'YYYY-MM-DD'
    })

    this.$once('hook:beforeDestroy', function () {
        picker.destroy()
    })
}

使用了这个策略,我甚至可以让多个输入框元素同时使用不同的 Pikaday,每个新的实例都程序化地在后期清理它自己:

mounted: function () {
    this.attachDatepicker('startDateInput')
    this.attachDatepicker('endDateInput')
},
methods: {
    attachDatepicker: function (refName) {
        var picker = new Pikaday({
            field: this.$refs[refName],
            format: 'YYYY-MM-DD'
        })

        this.$once('hook:beforeDestroy', function () {
            picker.destroy()
        })
    }
}

查阅这个fiddle可以了解到完整的代码。注意,即便如此,如果你发现自己不得不在单个组件里做很多建立和清理的工作,最好的方式通常还是创建更多的模块化组件。在这个例子中,我们推荐创建一个可复用的<input-datepicker>组件。

想了解更多程序化侦听器的内容,请查阅实例方法/事件相关的API

组件是可以在它们自己的模板中调用自身的。不过它们只能通过name选项来做这件事:

name: 'unique-name-of-my-component'

当你使用Vue.component全局注册一个组件时,这个全局的ID会自动设置为该组件的name选项。

Vue.component('unique-name-of-my-component', {
    // ...
})

稍有不慎,递归组件就可能导致无限循环:

name: 'stack-overflow',
template: '<div><stack-overflow></stack-overflow></div>'

类似上述的组件将会导致“max stack size exceeded”错误,所以请确保递归调用是条件性的(例如使用一个最终会得到falsev-if)。

假设你需要构建一个文件目录树,像访达或资源管理器那样的。你可能有一个<tree-folder>组件,模板是这样的:

<p>
    <span>{{ folder.name }}</span>
    <tree-folder-contents :children="folder.children"/>
</p>

还有一个<tree-folder-contents>组件,模板是这样的:

<ul>
    <li v-for="child in children">
        <tree-folder v-if="child.children" :folder="child"/>
        <span v-else>{{ child.name }}</span>
    </li>
</ul>

当你仔细观察的时候,你会发现这些组件在渲染树中互为对方的后代和祖先——一个悖论!当通过Vue.component全局注册组件的时候,这个悖论会被自动解开。如果你是这样做的,那么你可以跳过这里。

然而,如果你使用一个模块系统依赖/导入组件,例如通过webpack或Browserify,你会遇到一个错误:

Failed to mount component: template or render function not defined.

为了解释这里发生了什么,我们先把两个组件称为AB。模块系统发现它需要A,但是首先A依赖B,但是B又依赖A,但是A又依赖B,如此往复。这变成了一个循环,不知道如何不经过其中一个组件而完全解析出另一个组件。为了解决这个问题,我们需要给模块系统一个点,在那里“A反正是需要B的,但是我们不需要先解析B。”

在我们的例子中,把<tree-folder>组件设为了那个点。我们知道那个产生悖论的子组件是<tree-folder-contents> 组件,所以我们会等到生命周期钩子beforeCreate时去注册它:

beforeCreate: function () {
    this.$options.components.TreeFolderContents = require('./tree-folder-contents.vue').default
}

或者,在本地注册组件的时候,你可以使用 webpack 的异步 import:

components: {
    TreeFolderContents: () => import('./tree-folder-contents.vue')
}

这样问题就解决了!

inline-template这个特殊的特性出现在一个子组件上时,这个组件将会使用其里面的内容作为模板,而不是将其作为被分发的内容。这使得模板的撰写工作更加灵活。

<my-component inline-template>
    <div>
        <p>These are compiled as the component's own template.</p>
        <p>Not parent's transclusion content.</p>
    </div>
</my-component>

内联模板需要定义在Vue所属的DOM元素内。

不过,inline-template会让模板的作用域变得更加难以理解。所以作为最佳实践,请在组件内优先选择template选项或.vue文件里的一个<template>元素来定义模板。

另一个定义模板的方式是在一个<script>元素中,并为其带上text/x-template的类型,然后通过一个id将模板引用过去。例如:

<script type="text/x-template" id="hello-world-template">
    <p>Hello hello hello</p>
</script>
Vue.component('hello-world', {
    template: '#hello-world-template'
})

x-template需要定义在Vue所属的DOM元素外。

这些可以用于模板特别大的demo或极小型的应用,但是其它情况下请避免使用,因为这会将模板和该组件的其它定义分离开。

感谢Vue的响应式系统,它始终知道何时进行更新(如果你用对了的话)。不过还是有一些边界情况,你想要强制更新,尽管表面上看响应式的数据没有发生改变。也有一些情况是你想阻止不必要的更新。

如果你发现你自己需要在 Vue 中做一次强制更新,99.9%的情况,是你在某个地方做错了事。

你可能还没有留意到数组或对象的变更检测注意事项,或者你可能依赖了一个未被Vue的响应式系统追踪的状态。然而,如果你已经做到了上述的事项仍然发现在极少数的情况下需要手动强制更新,那么你可以通过$forceUpdate来做这件事。

渲染普通的HTML元素在Vue中是非常快速的,但有的时候你可能有一个组件,这个组件包含了大量静态内容。在这种情况下,你可以在根元素上添加v-once特性以确保这些内容只计算一次然后缓存起来,就像这样:

Vue.component('terms-of-service', {
    template: `
        <div v-once>
            <h1>Terms of Service</h1>
            ... a lot of static content ...
        </div>
    `
})

再说一次,试着不要过度使用这个模式。当你需要渲染大量静态内容时,极少数的情况下它会给你带来便利,除非你非常留意渲染变慢了,不然它完全是没有必要的——再加上它在后期会带来很多困惑。例如,设想另一个开发者并不熟悉v-once或漏看了它在模板中,他们可能会花很多个小时去找出模板为什么无法正确更新。

4.3 过渡&动画

4.3.1 进入/离开&列表过渡

  1. CSS过渡和动画中自动应用class
  2. 可以配合使用第三方CSS动画库,如Animate.css
  3. 在过渡钩子函数中使用JavaScript直接操作DOM
  4. 可以配合使用第三方JavaScript动画库,如Velocity.js

在这里,我们只会讲到进入、离开和列表的过渡,你也可以看下一节的管理过渡状态。

Vue提供了transition的封装组件,在下列情形中,可以给任何元素和组件添加进入/离开过渡:条件渲染(使用v-if),条件展示(用v-show),动态组件,组件根节点。

这里是一个典型的例子:

<div id="demo">
    <button v-on:click="show = !show">
        Toggle
    </button>
    <transition name="fade">
        <p v-if="show">hello</p>
    </transition>
</div>
new Vue({
    el: '#demo',
    data: {
        show: true
    }
})
.fade-enter-active, .fade-leave-active {
    transition: opacity .5s;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
    opacity: 0;
}

当插入或删除包含在transition组件中的元素时,Vue将会做以下处理:

  1. 自动嗅探目标元素是否应用了CSS过渡或动画,如果是,在恰当的时机添加/删除CSS类名。
  2. 如果过渡组件提供了JavaScript钩子函数,这些钩子函数将在恰当的时机被调用。
  3. 如果没有找到JavaScript钩子并且也没有检测到CSS过渡/动画,DOM操作(插入/删除)在下一帧中立即执行。(注意:此指浏览器逐帧动画机制,和Vue的nextTick概念不同)

  4. 过渡的类名

在进入/离开的过渡中,会有6个class切换。

  1. v-enter:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。
  2. v-enter-active:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。
  3. v-enter-to: 定义进入过渡的结束状态。在元素被插入之后下一帧生效(与此同时v-enter被移除),在过渡/动画完成之后移除。
  4. v-leave: 定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。
  5. v-leave-active:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。
  6. v-leave-to: 定义离开过渡的结束状态。在离开过渡被触发之后下一帧生效(与此同时v-leave被删除),在过渡/动画完成之后移除。

对于这些在过渡中切换的类名来说,如果你使用一个没有名字的<transition>,则v-是这些类名的默认前缀。如果你使用了<transition name="my-transition">,那么v-enter会替换为my-transition-enter

v-enter-activev-leave-active可以控制进入/离开过渡的不同的缓和曲线,在下面章节会有个示例说明。

常用的过渡都是使用CSS过渡。下面是一个简单例子:

<div id="example-1">
    <button @click="show = !show">
        Toggle render
    </button>
    <transition name="slide-fade">
        <p v-if="show">hello</p>
    </transition>
</div>
new Vue({
    el: '#example-1',
    data: {
        show: true
    }
})
/* 可以设置不同的进入和离开动画 */
/* 设置持续时间和动画函数 */
.slide-fade-enter-active {
    transition: all .3s ease;
}
.slide-fade-leave-active {
    transition: all .8s cubic-bezier(1.0, 0.5, 0.8, 1.0);
}
.slide-fade-enter, .slide-fade-leave-to
    /* .slide-fade-leave-active for below version 2.1.8 */ {
    transform: translateX(10px);
    opacity: 0;
}

CSS动画用法同CSS过渡,区别是在动画中v-enter类名在节点插入DOM后不会立即删除,而是在animationend事件触发时删除。

示例:(省略了兼容性前缀)

<div id="example-2">
    <button @click="show = !show">Toggle show</button>
    <transition name="bounce">
        <p v-if="show">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris facilisis enim libero, at lacinia diam fermentum id. Pellentesque habitant morbi tristique senectus et netus.</p>
    </transition>
</div>
new Vue({
    el: '#example-2',
    data: {
        show: true
    }
})
.bounce-enter-active {
    animation: bounce-in .5s;
}
.bounce-leave-active {
    animation: bounce-in .5s reverse;
}
@keyframes bounce-in {
    0% {
        transform: scale(0);
    }
    50% {
        transform: scale(1.5);
    }
    100% {
        transform: scale(1);
    }
}

我们可以通过以下特性来自定义过渡类名:enter-class,enter-active-class,enter-to-class,leave-class,leave-active-classleave-to-class

他们的优先级高于普通的类名,这对于Vue的过渡系统和其他第三方CSS动画库,如Animate.css结合使用十分有用。示例:

<link href="https://cdn.jsdelivr.net/npm/animate.css@3.5.1" rel="stylesheet" type="text/css">

<div id="example-3">
  <button @click="show = !show">
    Toggle render
  </button>
  <transition
    name="custom-classes-transition"
    enter-active-class="animated tada"
    leave-active-class="animated bounceOutRight"
  >
    <p v-if="show">hello</p>
  </transition>
</div>
new Vue({
  el: '#example-3',
  data: {
    show: true
  }
})

Vue为了知道过渡的完成,必须设置相应的事件监听器。它可以是transitionendanimationend,这取决于给元素应用的CSS规则。如果你使用其中任何一种,Vue能自动识别类型并设置监听。

但是,在一些场景中,你需要给同一个元素同时设置两种过渡动效,比如animation很快的被触发并完成了,而transition效果还没结束。在这种情况中,你就需要使用type特性并设置animationtransition来明确声明你需要Vue监听的类型。

在很多情况下,Vue可以自动得出过渡效果的完成时机。默认情况下,Vue会等待其在过渡效果的根元素的第一个transitionendanimationend事件。然而也可以不这样设定——比如,我们可以拥有一个精心编排的一系列过渡效果,其中一些嵌套的内部元素相比于过渡效果的根元素有延迟的或更长的过渡效果。

在这种情况下你可以用<transition>组件上的duration属性定制一个显性的过渡持续时间(以毫秒计):

<transition :duration="1000">...</transition>

你也可以定制进入和移出的持续时间:

<transition :duration="{ enter: 500, leave: 800 }">...</transition>

可以在属性中声明JavaScript钩子

<transition
    v-on:before-enter="beforeEnter"
    v-on:enter="enter"
    v-on:after-enter="afterEnter"
    v-on:enter-cancelled="enterCancelled"

    v-on:before-leave="beforeLeave"
    v-on:leave="leave"
    v-on:after-leave="afterLeave"
    v-on:leave-cancelled="leaveCancelled"
>
    <!-- ... -->
</transition>
// ...
methods: {
    // 进入中
    beforeEnter: function (el) {
        // ...
    },
    // 当与CSS结合使用时
    // 回调函数done是可选的
    enter: function (el, done) {
        // ...
        done()
    },
    afterEnter: function (el) {
        // ...
    },
    enterCancelled: function (el) {
        // ...
    },

    // 离开时
    beforeLeave: function (el) {
        // ...
    },
    // 当与CSS结合使用时
    // 回调函数done是可选的
    leave: function (el, done) {
        // ...
        done()
    },
    afterLeave: function (el) {
        // ...
    },
    // leaveCancelled只用于v-show中
    leaveCancelled: function (el) {
        // ...
    }
}

这些钩子函数可以结合CSS transitions/animations使用,也可以单独使用。

当只用JavaScript过渡的时候,在enterleave中必须使用done进行回调。否则,它们将被同步调用,过渡会立即完成。

推荐对于仅使用JavaScript过渡的元素添加v-bind:css="false",Vue会跳过CSS的检测。这也可以避免过渡过程中CSS的影响。

一个使用Velocity.js的简单例子:

<!-- Velocity和jQuery.animate的工作方式类似,也是用来实现JavaScript动画的一个很棒的选择 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js"></script>

<div id="example-4">
    <button @click="show = !show">
        Toggle
    </button>
    <transition
        v-on:before-enter="beforeEnter"
        v-on:enter="enter"
        v-on:leave="leave"
        v-bind:css="false"
    >
        <p v-if="show">
            Demo
        </p>
    </transition>
</div>
new Vue({
    el: '#example-4',
    data: {
        show: false
    },
    methods: {
        beforeEnter: function (el) {
            el.style.opacity = 0
            el.style.transformOrigin = 'left'
        },
        enter: function (el, done) {
            Velocity(el, { opacity: 1, fontSize: '1.4em' }, { duration: 300 })
            Velocity(el, { fontSize: '1em' }, { complete: done })
        },
        leave: function (el, done) {
            Velocity(el, { translateX: '15px', rotateZ: '50deg' }, { duration: 600 })
            Velocity(el, { rotateZ: '100deg' }, { loop: 2 })
            Velocity(el, {
                rotateZ: '45deg',
                translateY: '30px',
                translateX: '30px',
                opacity: 0
                }, { complete: done })
        }
    }
})

可以通过appear特性设置节点在初始渲染的过渡

<transition appear>
    <!-- ... -->
</transition>

这里默认和进入/离开过渡一样,同样也可以自定义CSS类名。

<transition
    appear
    appear-class="custom-appear-class"
    appear-to-class="custom-appear-to-class" (2.1.8+)
    appear-active-class="custom-appear-active-class"
>
    <!-- ... -->
</transition>

自定义JavaScript钩子:

<transition
    appear
    v-on:before-appear="customBeforeAppearHook"
    v-on:appear="customAppearHook"
    v-on:after-appear="customAfterAppearHook"
    v-on:appear-cancelled="customAppearCancelledHook"
>
    <!-- ... -->
</transition>

在上面的例子中,无论是appear特性还是v-on:appear钩子都会生成初始渲染过渡。

我们之后讨论多个组件的过渡,对于原生标签可以使用v-if/v-else。最常见的多标签过渡是一个列表和描述这个列表为空消息的元素:

<transition>
    <table v-if="items.length > 0">
        <!-- ... -->
    </table>
    <p v-else>Sorry, no items found.</p>
</transition>

可以这样使用,但是有一点需要注意:

当有相同标签名的元素切换时,需要通过key特性设置唯一的值来标记以让Vue区分它们,否则Vue为了效率只会替换相同标签内部的内容。即使在技术上没有必要,给在<transition>组件中的多个元素设置key是一个更好的实践。

示例:

<transition>
    <button v-if="isEditing" key="save">
        Save
    </button>
    <button v-else key="edit">
        Edit
    </button>
</transition>

在一些场景中,也可以通过给同一个元素的key特性设置不同的状态来代替v-ifv-else,上面的例子可以重写为:

<transition>
    <button v-bind:key="isEditing">
        {{ isEditing ? 'Save' : 'Edit' }}
    </button>
</transition>

使用多个v-if的多个元素的过渡可以重写为绑定了动态属性的单个元素过渡。例如:

<transition>
    <button v-if="docState === 'saved'" key="saved">
        Edit
    </button>
    <button v-if="docState === 'edited'" key="edited">
        Save
    </button>
    <button v-if="docState === 'editing'" key="editing">
        Cancel
    </button>
</transition>

可以重写为:

<transition>
    <button v-bind:key="docState">
        {{ buttonMessage }}
    </button>
</transition>
// ...
computed: {
    buttonMessage: function () {
        switch (this.docState) {
            case 'saved': return 'Edit'
            case 'edited': return 'Save'
            case 'editing': return 'Cancel'
        }
    }
}

同时生效的进入和离开的过渡不能满足所有要求,所以Vue提供了过渡模式

  1. in-out:新元素先进行过渡,完成之后当前元素过渡离开。
  2. out-in:当前元素先进行过渡,完成之后新元素过渡进入。

out-in重写之前的开关按钮过渡:

<transition name="fade" mode="out-in">
    <!-- ... the buttons ... -->
</transition>

只用添加一个简单的特性,就解决了之前的过渡问题而无需任何额外的代码。in-out模式不是经常用到,但对于一些稍微不同的过渡效果还是有用的。

多个组件的过渡简单很多 - 我们不需要使用key特性。相反,我们只需要使用动态组件:

<transition name="component-fade" mode="out-in">
    <component v-bind:is="view"></component>
</transition>
new Vue({
    el: '#transition-components-demo',
    data: {
        view: 'v-a'
    },
    components: {
        'v-a': {
            template: '<div>Component A</div>'
        },
        'v-b': {
            template: '<div>Component B</div>'
        }
    }
})
.component-fade-enter-active, .component-fade-leave-active {
    transition: opacity .3s ease;
}
.component-fade-enter, .component-fade-leave-to
    /* .component-fade-leave-active for below version 2.1.8 */ {
    opacity: 0;
}

目前为止,关于过渡我们已经讲到:单个节点和同一时间渲染多个节点中的一个。那么怎么同时渲染整个列表,比如使用v-for?在这种场景中,使用<transition-group>组件。在我们深入例子之前,先了解关于这个组件的几个特点:

  1. 不同于<transition>,它会以一个真实元素呈现:默认为一个<span>。你也可以通过tag特性更换为其他元素。
  2. 过渡模式不可用,因为我们不再相互切换特有的元素。
  3. 内部元素总是需要提供唯一的key属性值。
  4. CSS过渡的类将会应用在内部的元素中,而不是这个组/容器本身。

  5. 列表的进入/离开过渡

现在让我们由一个简单的例子深入,进入和离开的过渡使用之前一样的CSS类名。

<div id="list-demo" class="demo">
    <button v-on:click="add">Add</button>
    <button v-on:click="remove">Remove</button>
    <transition-group name="list" tag="p">
        <span v-for="item in items" v-bind:key="item" class="list-item">
            {{ item }}
        </span>
    </transition-group>
</div>
new Vue({
    el: '#list-demo',
    data: {
        items: [1,2,3,4,5,6,7,8,9],
        nextNum: 10
    },
    methods: {
        randomIndex: function () {
            return Math.floor(Math.random() * this.items.length)
        },
        add: function () {
            this.items.splice(this.randomIndex(), 0, this.nextNum++)
        },
        remove: function () {
            this.items.splice(this.randomIndex(), 1)
        },
    }
})
.list-item {
    display: inline-block;
    margin-right: 10px;
}
.list-enter-active, .list-leave-active {
    transition: all 1s;
}
.list-enter, .list-leave-to
    /* .list-leave-active for below version 2.1.8 */ {
    opacity: 0;
    transform: translateY(30px);
}

这个例子有个问题,当添加和移除元素的时候,周围的元素会瞬间移动到他们的新布局的位置,而不是平滑的过渡,我们下面会解决这个问题。

<transition-group>组件还有一个特殊之处。不仅可以进入和离开动画,还可以改变定位。要使用这个新功能只需了解新增的v-move特性,它会在元素的改变定位的过程中应用。像之前的类名一样,可以通过name属性来自定义前缀,也可以通过move-class属性手动设置。

v-move对于设置过渡的切换时机和过渡曲线非常有用,你会看到如下的例子:

<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.14.1/lodash.min.js"></script>

<div id="flip-list-demo" class="demo">
    <button v-on:click="shuffle">Shuffle</button>
    <transition-group name="flip-list" tag="ul">
        <li v-for="item in items" v-bind:key="item">
            {{ item }}
        </li>
    </transition-group>
</div>
new Vue({
    el: '#flip-list-demo',
    data: {
        items: [1,2,3,4,5,6,7,8,9]
    },
    methods: {
        shuffle: function () {
            this.items = _.shuffle(this.items)
        }
    }
})
.flip-list-move {
    transition: transform 1s;
}

这个看起来很神奇,内部的实现,Vue使用了一个叫FLIP简单的动画队列使用transforms将元素从之前的位置平滑过渡新的位置。

我们将之前实现的例子和这个技术结合,使我们列表的一切变动都会有动画过渡。

<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.14.1/lodash.min.js"></script>

<div id="list-complete-demo" class="demo">
    <button v-on:click="shuffle">Shuffle</button>
    <button v-on:click="add">Add</button>
    <button v-on:click="remove">Remove</button>
    <transition-group name="list-complete" tag="p">
        <span
            v-for="item in items"
            v-bind:key="item"
            class="list-complete-item"
        >
            {{ item }}
        </span>
    </transition-group>
</div>
new Vue({
    el: '#list-complete-demo',
    data: {
        items: [1,2,3,4,5,6,7,8,9],
        nextNum: 10
    },
    methods: {
        randomIndex: function () {
            return Math.floor(Math.random() * this.items.length)
        },
        add: function () {
            this.items.splice(this.randomIndex(), 0, this.nextNum++)
        },
        remove: function () {
            this.items.splice(this.randomIndex(), 1)
        },  
        shuffle: function () {
            this.items = _.shuffle(this.items)
        }
    }
})
.list-complete-item {
    transition: all 1s;
    display: inline-block;
    margin-right: 10px;
}
.list-complete-enter, .list-complete-leave-to
    /* .list-complete-leave-active for below version 2.1.8 */ {
    opacity: 0;
    transform: translateY(30px);
}
.list-complete-leave-active {
    position: absolute;
}

需要注意的是使用FLIP过渡的元素不能设置为display: inline。作为替代方案,可以设置为display: inline-block或者放置于flex

FLIP动画不仅可以实现单列过渡,多维网格也同样可以过渡

通过data属性与JavaScript通信,就可以实现列表的交错过渡:

<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js"></script>

<div id="staggered-list-demo">
    <input v-model="query">
    <transition-group
        name="staggered-fade"
        tag="ul"
        v-bind:css="false"
        v-on:before-enter="beforeEnter"
        v-on:enter="enter"
        v-on:leave="leave"
    >
        <li
            v-for="(item, index) in computedList"
            v-bind:key="item.msg"
            v-bind:data-index="index"
            >{{ item.msg }}</li>
    </transition-group>
</div>
new Vue({
    el: '#staggered-list-demo',
    data: {
        query: '',
        list: [
            { msg: 'Bruce Lee' },
            { msg: 'Jackie Chan' },
            { msg: 'Chuck Norris' },
            { msg: 'Jet Li' },
            { msg: 'Kung Fury' }
        ]
    },
    computed: {
        computedList: function () {
            var vm = this
            return this.list.filter(function (item) {
                return item.msg.toLowerCase().indexOf(vm.query.toLowerCase()) !== -1
            })
        }
    },
    methods: {
        beforeEnter: function (el) {
            el.style.opacity = 0
            el.style.height = 0
        },
        enter: function (el, done) {
            var delay = el.dataset.index * 150
            setTimeout(function () {
                Velocity(
                    el,
                    { opacity: 1, height: '1.6em' },
                    { complete: done }
                )
            }, delay)
        },
        leave: function (el, done) {
            var delay = el.dataset.index * 150
            setTimeout(function () {
                Velocity(
                    el,
                    { opacity: 0, height: 0 },
                    { complete: done }
                )
            }, delay)
        }
    }
})

过渡可以通过Vue的组件系统实现复用。要创建一个可复用过渡组件,你需要做的就是将<transition>或者<transition-group>作为根组件,然后将任何子组件放置在其中就可以了。

使用template的简单例子:

Vue.component('my-special-transition', {
    template: '\
        <transition\
            name="very-special-transition"\
            mode="out-in"\
            v-on:before-enter="beforeEnter"\
            v-on:after-enter="afterEnter"\
        >\
            <slot></slot>\
        </transition>\
    ',
    methods: {
        beforeEnter: function (el) {
            // ...
        },
        afterEnter: function (el) {
            // ...
        }
    }
})

函数式组件更适合完成这个任务:

Vue.component('my-special-transition', {
    functional: true,
    render: function (createElement, context) {
        var data = {
            props: {
                name: 'very-special-transition',
                mode: 'out-in'
            },
            on: {
                beforeEnter: function (el) {
                    // ...
                },
                afterEnter: function (el) {
                    // ...
                }
            }
        }
        return createElement('transition', data, context.children)
    }
})

在Vue中即使是过渡也是数据驱动的!动态过渡最基本的例子是通过name特性来绑定动态值。

<transition v-bind:name="transitionName">
    <!-- ... -->
</transition>

当你想用Vue的过渡系统来定义的CSS过渡/动画在不同过渡间切换会非常有用。

所有过渡特性都可以动态绑定,但我们不仅仅只有特性可以利用,还可以通过事件钩子获取上下文中的所有数据,因为事件钩子都是方法。这意味着,根据组件的状态不同,你的JavaScript过渡会有不同的表现。

<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js"></script>

<div id="dynamic-fade-demo" class="demo">
    Fade In: <input type="range" v-model="fadeInDuration" min="0" v-bind:max="maxFadeDuration">
    Fade Out: <input type="range" v-model="fadeOutDuration" min="0" v-bind:max="maxFadeDuration">
    <transition
        v-bind:css="false"
        v-on:before-enter="beforeEnter"
        v-on:enter="enter"
        v-on:leave="leave"
    >
        <p v-if="show">hello</p>
    </transition>
    <button
        v-if="stop"
        v-on:click="stop = false; show = false"
        >Start animating</button>
    <button
        v-else
        v-on:click="stop = true"
        >Stop it!</button>
</div>
new Vue({
    el: '#dynamic-fade-demo',
    data: {
        show: true,
        fadeInDuration: 1000,
        fadeOutDuration: 1000,
        maxFadeDuration: 1500,
        stop: true
    },
    mounted: function () {
        this.show = false
    },
    methods: {
        beforeEnter: function (el) {
            el.style.opacity = 0
        },
        enter: function (el, done) {
            var vm = this
            Velocity(el,
                { opacity: 1 },
                {
                    duration: this.fadeInDuration,
                    complete: function () {
                        done()
                        if (!vm.stop) vm.show = false
                    }
                }
            )
        },
        leave: function (el, done) {
            var vm = this
            Velocity(el,
                { opacity: 0 },
                {
                    duration: this.fadeOutDuration,
                    complete: function () {
                        done()
                        vm.show = true
                    }
                }
            )
        }
    }
})

最后,创建动态过渡的最终方案是组件通过接受props来动态修改之前的过渡。一句老话,唯一的限制是你的想象力。

4.3.2 状态过渡

Vue的过渡系统提供了非常多简单的方法设置进入、离开和列表的动效。那么对于数据元素本身的动效呢,比如:数字和运算,颜色的显示,SVG节点的位置和元素的大小和其他的属性。

这些数据要么本身就以数值形式存储,要么可以转换为数值。有了这些数值后,我们就可以结合Vue的响应式和组件系统,使用第三方库来实现切换元素的过渡状态。

通过侦听器我们能监听到任何数值属性的数值更新。可能听起来很抽象,所以让我们先来看看使用GreenSock一个例子:

<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.20.3/TweenMax.min.js"></script>

<div id="animated-number-demo">
    <input v-model.number="number" type="number" step="20">
    <p>{{ animatedNumber }}</p>
</div>
new Vue({
    el: '#animated-number-demo',
    data: {
        number: 0,
        tweenedNumber: 0
    },
    computed: {
        animatedNumber: function() {
            return this.tweenedNumber.toFixed(0);
        }
    },
    watch: {
        number: function(newValue) {
            TweenLite.to(this.$data, 0.5, { tweenedNumber: newValue });
        }
    }
})

当你把数值更新时,就会触发动画。这个是一个不错的演示,但是对于不能直接像数字一样存储的值,比如CSS中的color的值,通过下面的例子我们来通过Tween.js和Color.js实现一个例子:

<script src="https://cdn.jsdelivr.net/npm/tween.js@16.3.4"></script>
<script src="https://cdn.jsdelivr.net/npm/color-js@1.0.3"></script>

<div id="example-7">
    <input
        v-model="colorQuery"
        v-on:keyup.enter="updateColor"
        placeholder="Enter a color"
    >
    <button v-on:click="updateColor">Update</button>
    <p>Preview:</p>
    <span
        v-bind:style="{ backgroundColor: tweenedCSSColor }"
        class="example-7-color-preview"
        ></span>
    <p>{{ tweenedCSSColor }}</p>
</div>
var Color = net.brehaut.Color

new Vue({
    el: '#example-7',
    data: {
        colorQuery: '',
        color: {
            red: 0,
            green: 0,
            blue: 0,
            alpha: 1
        },
        tweenedColor: {}
    },
    created: function () {
        this.tweenedColor = Object.assign({}, this.color)
    },
    watch: {
        color: function () {
            function animate () {
                if (TWEEN.update()) {
                    requestAnimationFrame(animate)
                }
            }

            new TWEEN.Tween(this.tweenedColor)
                .to(this.color, 750)
                .start()

            animate()
        }
    },
    computed: {
        tweenedCSSColor: function () {
            return new Color({
                red: this.tweenedColor.red,
                green: this.tweenedColor.green,
                blue: this.tweenedColor.blue,
                alpha: this.tweenedColor.alpha
                }).toCSS()
        }
    },
    methods: {
        updateColor: function () {
            this.color = new Color(this.colorQuery).toRGB()
            this.colorQuery = ''
        }
    }
})
.example-7-color-preview {
    display: inline-block;
    width: 50px;
    height: 50px;
}

就像Vue的过渡组件一样,数据背后状态过渡会实时更新,这对于原型设计十分有用。当你修改一些变量,即使是一个简单的SVG多边形也可实现很多难以想象的效果。

demo背后的代码可以通过这个fiddle进行详阅。

管理太多的状态过渡会很快的增加Vue实例或者组件的复杂性,幸好很多的动画可以提取到专用的子组件。我们来将之前的示例改写一下:

<script src="https://cdn.jsdelivr.net/npm/tween.js@16.3.4"></script>

<div id="example-8">
    <input v-model.number="firstNumber" type="number" step="20"> +
    <input v-model.number="secondNumber" type="number" step="20"> =
    {{ result }}
    <p>
        <animated-integer v-bind:value="firstNumber"></animated-integer> +
        <animated-integer v-bind:value="secondNumber"></animated-integer> =
        <animated-integer v-bind:value="result"></animated-integer>
    </p>
</div>
// 这种复杂的补间动画逻辑可以被复用
// 任何整数都可以执行动画
// 组件化使我们的界面十分清晰
// 可以支持更多更复杂的动态过渡策略。
Vue.component('animated-integer', {
    template: '<span>{{ tweeningValue }}</span>',
    props: {
        value: {
            type: Number,
            required: true
        }
    },
    data: function () {
        return {
            tweeningValue: 0
        }
    },
    watch: {
        value: function (newValue, oldValue) {
            this.tween(oldValue, newValue)
        }
    },
    mounted: function () {
        this.tween(0, this.value)
    },
    methods: {
        tween: function (startValue, endValue) {
            var vm = this
            function animate () {
                if (TWEEN.update()) {
                    requestAnimationFrame(animate)
                }
            }

            new TWEEN.Tween({ tweeningValue: startValue })
                .to({ tweeningValue: endValue }, 500)
                .onUpdate(function () {
                    vm.tweeningValue = this.tweeningValue.toFixed(0)
                })
                .start()

            animate()
        }
    }
})

// 所有的复杂度都已经从Vue的主实例中移除!
new Vue({
    el: '#example-8',
    data: {
        firstNumber: 20,
        secondNumber: 40
    },
    computed: {
        result: function () {
            return this.firstNumber + this.secondNumber
        }
    }
})

我们能在组件中结合使用这一节讲到各种过渡策略和Vue内建的过渡系统。总之,对于完成各种过渡动效几乎没有阻碍。

只要一个动画,就可以带来生命。不幸的是,当设计师创建图标、logo和吉祥物的时候,他们交付的通常都是图片或静态的SVG。所以,虽然GitHub的章鱼猫、Twitter的小鸟以及其它许多logo类似于生灵,它们看上去实际上并不是活着的。

Vue可以帮到你。因为SVG的本质是数据,我们只需要这些动物兴奋、思考或警戒的样例。然后Vue就可以辅助完成这几种状态之间的过渡动画,来制作你的欢迎页面、加载指示、以及更加带有情感的提示。

Sarah Drasner展示了下面这个demo,这个 demo结合了时间和交互相关的状态改变:

4.3.3 深入响应式原理

现在是时候深入一下了!Vue最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的JavaScript对象。而当你修改它们时,视图会进行更新。这使得状态管理非常简单直接,不过理解其工作原理同样重要,这样你可以避开一些常见的问题。在这个章节,我们将研究一下Vue响应式系统的底层的细节。

当你把一个普通的JavaScript对象传入Vue实例作为data选项,Vue将遍历此对象所有的属性,并使用Object.defineProperty把这些属性全部转为getter/setterObject.defineProperty是ES5中一个无法shim的特性,这也就是Vue不支持IE8以及更低版本浏览器的原因。

这些getter/setter对用户来说是不可见的,但是在内部它们让Vue能够追踪依赖,在属性被访问和修改时通知变更。这里需要注意的是不同浏览器在控制台打印数据对象时对getter/setter的格式化并不同,所以建议安装vue-devtools来获取对检查数据更加友好的用户界面。

每个组件实例都对应一个watcher实例,它会在组件渲染的过程中把“接触”过的数据属性记录为依赖。之后当依赖项的setter触发时,会通知watcher,从而使它关联的组件重新渲染。

受现代JavaScript的限制(而且Object.observe也已经被废弃),Vue无法检测到对象属性的添加或删除。由于Vue会在初始化实例时对属性执行getter/setter转化,所以属性必须在data对象上存在才能让Vue将它转换为响应式的。例如:

var vm = new Vue({
    data:{
        a:1
    }
})

// vm.a是响应式的
vm.b = 2

// vm.b是非响应式的

对于已经创建的实例,Vue不允许动态添加根级别的响应式属性。但是,可以使用Vue.set(object, propertyName, value)方法向嵌套对象添加响应式属性。例如,对于:

Vue.set(vm.someObject, 'b', 2)

还可以使用vm.$set实例方法,这也是全局Vue.set方法的别名:

this.$set(this.someObject,'b',2)

有时你可能需要为已有对象赋值多个新属性,比如使用Object.assign()_.extend()。但是,这样添加到对象上的新属性不会触发更新。在这种情况下,你应该用原对象与要混合进去的对象的属性一起创建一个新的对象。

// 代替Object.assign(this.someObject, { a: 1, b: 2 })
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })

也有一些数组相关的注意事项,之前已经在列表渲染中讲过。

由于Vue不允许动态添加根级响应式属性,所以你必须在初始化实例前声明所有根级响应式属性,哪怕只是一个空值:

var vm = new Vue({
    data: {
        // 声明message为一个空值字符串
        message: ''
    },
    template: '<div>{{ message }}</div>'
})
// 之后设置message
vm.message = 'Hello!'

如果你未在data选项中声明message,Vue将警告你渲染函数正在试图访问不存在的属性。

这样的限制在背后是有其技术原因的,它消除了在依赖项跟踪系统中的一类边界情况,也使Vue实例能更好地配合类型检查系统工作。但与此同时在代码可维护性方面也有一点重要的考虑:data 对象就像组件状态的结构(schema)。提前声明所有的响应式属性,可以让组件代码在未来修改或给其他开发人员阅读时更易于理解。

可能你还没有注意到,Vue在更新DOM时是异步执行的。只要侦听到数据变化,Vue将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个watcher被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和DOM操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue刷新队列并执行实际(已去重的)工作。Vue在内部对异步队列尝试使用原生的Promise.thenMutationObserversetImmediate,如果执行环境不支持,则会采用setTimeout(fn, 0)代替。

例如,当你设置vm.someData = 'new value',该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的DOM状态来做点什么,这就可能会有些棘手。虽然Vue.js通常鼓励开发人员使用“数据驱动”的方式思考,避免直接接触DOM,但是有时我们必须要这么做。为了在数据变化之后等待Vue完成更新DOM,可以在数据变化之后立即使用Vue.nextTick(callback)。这样回调函数将在DOM更新完成后被调用。例如:

<div id="example">{{message}}</div>
var vm = new Vue({
    el: '#example',
    data: {
        message: '123'
    }
})
vm.message = 'new message' // 更改数据
vm.$el.textContent === 'new message' // false
Vue.nextTick(function () {
    vm.$el.textContent === 'new message' // true
})

在组件内使用vm.$nextTick()实例方法特别方便,因为它不需要全局Vue,并且回调函数中的this将自动绑定到当前的Vue 实例上:

Vue.component('example', {
    template: '<span>{{ message }}</span>',
    data: function () {
        return {
            message: '未更新'
        }
    },
    methods: {
        updateMessage: function () {
            this.message = '已更新'
            console.log(this.$el.textContent) // => '未更新'
            this.$nextTick(function () {
                console.log(this.$el.textContent) // => '已更新'
            })
        }
    }
})

因为$nextTick()返回一个Promise对象,所以你可以使用新的ES2017 async/await语法完成相同的事情:

methods: {
    updateMessage: async function () {
        this.message = '已更新'
        console.log(this.$el.textContent) // => '未更新'
        await this.$nextTick()
        console.log(this.$el.textContent) // => '已更新'
  }
}

4.4 Vue Router

4.4.1 起步

用Vue.js+Vue Router创建单页应用,是非常简单的。使用Vue.js,我们已经可以通过组合组件来组成应用程序,当你要把Vue Router添加进来,我们需要做的是将组件(components)映射到路由(routes),然后告诉Vue Router在哪里渲染它们。下面是个基本例子:

<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>

<div id="app">
  <h1>Hello App!</h1>
  <p>
    <!-- 使用router-link组件来导航. -->
    <!-- 通过传入to属性指定链接. -->
    <!-- <router-link>默认会被渲染成一个<a>标签 -->
    <router-link to="/foo">Go to Foo</router-link>
    <router-link to="/bar">Go to Bar</router-link>
  </p>
  <!-- 路由出口 -->
  <!-- 路由匹配到的组件将渲染在这里 -->
  <router-view></router-view>
</div>
// 0. 如果使用模块化机制编程,导入Vue和VueRouter,要调用Vue.use(VueRouter)

// 1. 定义(路由)组件。
// 可以从其他文件import进来
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }

// 2. 定义路由
// 每个路由应该映射一个组件。其中"component"可以是
// 通过Vue.extend()创建的组件构造器,
// 或者,只是一个组件配置对象。
// 我们晚点再讨论嵌套路由。
const routes = [
  { path: '/foo', component: Foo },
  { path: '/bar', component: Bar }
]

// 3. 创建router实例,然后传routes配置
// 你还可以传别的配置参数,不过先这么简单着吧。
const router = new VueRouter({
  routes // (缩写)相当于routes: routes
})

// 4. 创建和挂载根实例。
// 记得要通过router配置参数注入路由,
// 从而让整个应用都有路由功能
const app = new Vue({
  router
}).$mount('#app')

// 现在,应用已经启动了!

通过注入路由器,我们可以在任何组件内通过this.$router访问路由器,也可以通过this.$route访问当前路由:

// Home.vue
export default {
  computed: {
    username() {
      // 我们很快就会看到params是什么
      return this.$route.params.username
    }
  },
  methods: {
    goBack() {
      window.history.length > 1 ? this.$router.go(-1) : this.$router.push('/')
    }
  }
}

该文档通篇都常使用router实例。留意一下this.$routerrouter使用起来完全一样。我们使用this.$router的原因是我们并不想在每个独立需要封装路由的组件中都导入路由。你可以看看这个在线的例子

要注意,当<router-link\>对应的路由匹配成功,将自动设置class属性值.router-link-active。查看API 文档学习更多相关内容。

4.4.2 动态路由匹配

我们经常需要把某种模式匹配到的所有路由,全都映射到同个组件。例如,我们有一个User组件,对于所有ID各不相同的用户,都要使用这个组件来渲染。那么,我们可以在vue-router的路由路径中使用“动态路径参数”(dynamic segment)来达到这个效果:

const User = {
  template: '<div>User</div>'
}

const router = new VueRouter({
  routes: [
    // 动态路径参数 以冒号开头
    { path: '/user/:id', component: User }
  ]
})

现在呢,像/user/foo/user/bar都将映射到相同的路由。

一个“路径参数”使用冒号:标记。当匹配到一个路由时,参数值会被设置到this.$route.params,可以在每个组件内使用。于是,我们可以更新User的模板,输出当前用户的ID:

const User = {
  template: '<div>User {{ $route.params.id }}</div>'
}

你可以看看这个在线例子

你可以在一个路由中设置多段“路径参数”,对应的值都会设置到$route.params中。例如:

模式 匹配路径 $route.params
/user/:username /user/evan `{ username: 'evan' }``
/user/:username/post/:post_id /user/evan/post/123 `{ username: 'evan', post_id: '123' }``

除了$route.params外,$route对象还提供了其它有用的信息,例如,$route.query(如果URL中有查询参数)、$route.hash等等。你可以查看API 文档的详细说明。

提醒一下,当使用路由参数时,例如从/user/foo导航到/user/bar,原来的组件实例会被复用。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会再被调用。

复用组件时,想对路由参数的变化作出响应的话,你可以简单地watch(监测变化)$route对象:

const User = {
  template: '...',
  watch: {
    '$route' (to, from) {
      // 对路由变化作出响应...
    }
  }
}

或者使用引入的beforeRouteUpdate导航守卫:

const User = {
  template: '...',
  beforeRouteUpdate (to, from, next) {
    // react to route changes...
    // don't forget to call next()
  }
}

常规参数只会匹配被/分隔的URL片段中的字符。如果想匹配任意路径,我们可以使用通配符(*):

{
  // 会匹配所有路径
  path: '*'
}
{
  // 会匹配以 `/user-` 开头的任意路径
  path: '/user-*'
}

当使用通配符路由时,请确保路由的顺序是正确的,也就是说含有通配符的路由应该放在最后。路由{ path: '*' }通常用于客户端404错误。如果你使用了History模式,请确保正确配置你的服务器。

当使用一个通配符时,$route.params内会自动添加一个名为pathMatch参数。它包含了URL通过通配符被匹配的部分:

// 给出一个路由 { path: '/user-*' }
this.$router.push('/user-admin')
this.$route.params.pathMatch // 'admin'
// 给出一个路由 { path: '*' }
this.$router.push('/non-existing')
this.$route.params.pathMatch // '/non-existing'

vue-router使用path-to-regexp作为路径匹配引擎,所以支持很多高级的匹配模式,例如:可选的动态路径参数、匹配零个或多个、一个或多个,甚至是自定义正则匹配。查看它的文档学习高阶的路径匹配,还有这个例子展示vue-router怎么使用这类匹配。

有时候,同一个路径可以匹配多个路由,此时,匹配的优先级就按照路由的定义顺序:谁先定义的,谁的优先级就最高。

4.4.3 嵌套路由

实际生活中的应用界面,通常由多层嵌套的组件组合而成。同样地,URL中各段动态路径也按某种结构对应嵌套的各层组件,例如:

/user/foo/profile                     /user/foo/posts
+------------------+                  +-----------------+
| User             |                  | User            |
| +--------------+ |                  | +-------------+ |
| | Profile      | |  +------------>  | | Posts       | |
| |              | |                  | |             | |
| +--------------+ |                  | +-------------+ |
+------------------+                  +-----------------+

借助vue-router,使用嵌套路由配置,就可以很简单地表达这种关系。接着上节创建的app:

<div id="app">
  <router-view></router-view>
</div>
const User = {
  template: '<div>User {{ $route.params.id }}</div>'
}

const router = new VueRouter({
  routes: [
    { path: '/user/:id', component: User }
  ]
})

这里的<router-view\>是最顶层的出口,渲染最高级路由匹配到的组件。同样地,一个被渲染组件同样可以包含自己的嵌套<router-view\>。例如,在User组件的模板添加一个<router-view\>:

const User = {
  template: `
    <div class="user">
      <h2>User {{ $route.params.id }}</h2>
      <router-view></router-view>
    </div>
  `
}

要在嵌套的出口中渲染组件,需要在VueRouter的参数中使用children配置:

const router = new VueRouter({
  routes: [
    { path: '/user/:id', component: User,
      children: [
        {
          // 当/user/:id/profile匹配成功,
          // UserProfile会被渲染在User的<router-view>中
          path: 'profile',
          component: UserProfile
        },
        {
          // 当/user/:id/posts匹配成功
          // UserPosts会被渲染在User的<router-view>中
          path: 'posts',
          component: UserPosts
        }
      ]
    }
  ]
})

要注意,以/开头的嵌套路径会被当作根路径。这让你充分的使用嵌套组件而无须设置嵌套的路径。你会发现,children配置就是像routes配置一样的路由配置数组,所以呢,你可以嵌套多层路由。

此时,基于上面的配置,当你访问/user/foo时,User的出口是不会渲染任何东西,这是因为没有匹配到合适的子路由。如果你想要渲染点什么,可以提供一个空的子路由:

const router = new VueRouter({
  routes: [
    {
      path: '/user/:id', component: User,
      children: [
        // 当/user/:id匹配成功,
        // UserHome会被渲染在User的<router-view>中
        { path: '', component: UserHome },

        // ...其他子路由
      ]
    }
  ]
})

提供以上案例的可运行代码请移步这里

4.4.4 编程式的导航

除了使用<router-link >创建a标签来定义导航链接,我们还可以借助router的实例方法,通过编写代码来实现。

注意:在Vue实例内部,你可以通过$router访问路由实例。因此你可以调用this.$router.push

想要导航到不同的URL,则使用router.push方法。这个方法会向history栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,则回到之前的URL。当你点击<router-link >时,这个方法会在内部调用,所以说,点击<router-link :to="..."\>等同于调用router.push(...)

声明式 编程式
<router-link :to="..."\> router.push(...)

该方法的参数可以是一个字符串路径,或者一个描述地址的对象。例如:

// 字符串
router.push('home')

// 对象
router.push({ path: 'home' })

// 命名的路由
router.push({ name: 'user', params: { userId: '123' }})

// 带查询参数,变成/register?plan=private
router.push({ path: 'register', query: { plan: 'private' }})

注意:如果提供了path,params会被忽略,上述例子中的query并不属于这种情况。取而代之的是下面例子的做法,你需要提供路由的name或手写完整的带有参数的path:

const userId = '123'
router.push({ name: 'user', params: { userId }}) // -> /user/123
router.push({ path: `/user/${userId}` }) // -> /user/123
// 这里的params不生效
router.push({ path: '/user', params: { userId }}) // -> /user

同样的规则也适用于router-link组件的to属性。

可选的在router.pushrouter.replace中提供onCompleteonAbort回调作为第二个和第三个参数。这些回调将会在导航成功完成(在所有的异步钩子被解析之后)或终止(导航到相同的路由、或在当前导航完成之前导航到另一个不同的路由)的时候进行相应的调用。在3.1.0+,可以省略第二个和第三个参数,此时如果支持Promise,router.pushrouter.replace将返回一个Promise

注意: 如果目的地和当前路由相同,只有参数发生了改变(比如从一个用户资料到另一个/users/1 -> /users/2),你需要使用beforeRouteUpdate来响应这个变化(比如抓取用户信息)。

跟router.push很像,唯一的不同就是,它不会向history添加新记录,而是跟它的方法名一样——替换掉当前的history记录。

声明式 编程式
<router-link :to="..." replace\> router.replace(...)

这个方法的参数是一个整数,意思是在history记录中向前或者后退多少步,类似window.history.go(n)。例子:

// 在浏览器记录中前进一步,等同于history.forward()
router.go(1)

// 后退一步记录,等同于history.back()
router.go(-1)

// 前进3步记录
router.go(3)

// 如果history记录不够用,那就默默地失败呗
router.go(-100)
router.go(100)

你也许注意到router.pushrouter.replacerouter.gowindow.history.pushStatewindow.history.replaceStatewindow.history.go好像,实际上它们确实是效仿window.history API的。

因此,如果你已经熟悉Browser History APIs,那么在Vue Router中操作history就是超级简单的。还有值得提及的,Vue Router的导航方法(pushreplacego)在各类路由模式(historyhashabstract)下表现一致。

4.4.5 命名路由

有时候,通过一个名称来标识一个路由显得更方便一些,特别是在链接一个路由,或者是执行一些跳转的时候。你可以在创建Router实例的时候,在routes配置中给某个路由设置名称。

const router = new VueRouter({
  routes: [
    {
      path: '/user/:userId',
      name: 'user',
      component: User
    }
  ]
})

要链接到一个命名路由,可以给router-linkto属性传一个对象:

<router-link :to="{ name: 'user', params: { userId: 123 }}">User</router-link>

这跟代码调用router.push()是一回事:

router.push({ name: 'user', params: { userId: 123 }})

这两种方式都会把路由导航到/user/123路径。完整的例子请移步这里。

4.4.6 命名视图

有时候想同时(同级)展示多个视图,而不是嵌套展示,例如创建一个布局,有sidebar(侧导航)和main(主内容)两个视图,这个时候命名视图就派上用场了。你可以在界面中拥有多个单独命名的视图,而不是只有一个单独的出口。如果router-view没有设置名字,那么默认为default。

<router-view class="view one"></router-view>
<router-view class="view two" name="a"></router-view>
<router-view class="view three" name="b"></router-view>

一个视图使用一个组件渲染,因此对于同个路由,多个视图就需要多个组件。确保正确使用components配置(带上s):

const router = new VueRouter({
  routes: [
    {
      path: '/',
      components: {
        default: Foo,
        a: Bar,
        b: Baz
      }
    }
  ]
})

以上案例相关的可运行代码请移步这里

我们也有可能使用命名视图创建嵌套视图的复杂布局。这时你也需要命名用到的嵌套router-view组件。我们以一个设置面板为例:

/settings/emails                                       /settings/profile
+-----------------------------------+                  +------------------------------+
| UserSettings                      |                  | UserSettings                 |
| +-----+-------------------------+ |                  | +-----+--------------------+ |
| | Nav | UserEmailsSubscriptions | |  +------------>  | | Nav | UserProfile        | |
| |     +-------------------------+ |                  | |     +--------------------+ |
| |     |                         | |                  | |     | UserProfilePreview | |
| +-----+-------------------------+ |                  | +-----+--------------------+ |
+-----------------------------------+                  +------------------------------+
  1. Nav只是一个常规组件。
  2. UserSettings是一个视图组件。
  3. UserEmailsSubscriptionsUserProfileUserProfilePreview是嵌套的视图组件。

注意:我们先忘记HTML/CSS具体的布局的样子,只专注在用到的组件上。

UserSettings组件的<template\>部分应该是类似下面的这段代码:

<!-- UserSettings.vue -->
<div>
  <h1>User Settings</h1>
  <NavBar/>
  <router-view/>
  <router-view name="helper"/>
</div>

嵌套的视图组件在此已经被忽略了,但是你可以在这里找到完整的源代码。然后你可以用这个路由配置完成该布局:

{
  path: '/settings',
  // 你也可以在顶级路由就配置命名视图
  component: UserSettings,
  children: [{
    path: 'emails',
    component: UserEmailsSubscriptions
  }, {
    path: 'profile',
    components: {
      default: UserProfile,
      helper: UserProfilePreview
    }
  }]
}

一个可以工作的示例的demo在这里。

4.4.7 重定向和别名

重定向也是通过routes配置来完成,下面例子是从/a重定向到/b:

const router = new VueRouter({
  routes: [
    { path: '/a', redirect: '/b' }
  ]
})

重定向的目标也可以是一个命名的路由:

const router = new VueRouter({
  routes: [
    { path: '/a', redirect: { name: 'foo' }}
  ]
})

甚至是一个方法,动态返回重定向目标:

const router = new VueRouter({
  routes: [
    { path: '/a', redirect: to => {
      // 方法接收目标路由作为参数
      // return重定向的字符串路径/路径对象
    }}
  ]
})

注意导航守卫并没有应用在跳转路由上,而仅仅应用在其目标上。在下面这个例子中,为/a路由添加一个beforeEachbeforeLeave守卫并不会有任何效果。

“重定向”的意思是,当用户访问/a时,URL将会被替换成/b,然后匹配路由为/b,那么“别名”又是什么呢?/a的别名是/b,意味着,当用户访问/b时,URL会保持为/b,但是路由匹配则为/a,就像用户访问/a一样。

上面对应的路由配置为:

const router = new VueRouter({
  routes: [
    { path: '/a', component: A, alias: '/b' }
  ]
})

“别名”的功能让你可以自由地将UI结构映射到任意的URL,而不是受限于配置的嵌套路由结构。

更多高级用法,请查看例子

4.4.8 路由组件传参

在组件中使用$route会使之与其对应路由形成高度耦合,从而使组件只能在某些特定的URL上使用,限制了其灵活性。

使用props将组件和路由解耦:

  1. 取代与$route的耦合
const User = {
  template: '<div>User {{ $route.params.id }}</div>'
}
const router = new VueRouter({
  routes: [
    { path: '/user/:id', component: User }
  ]
})
  1. 通过props解耦
const User = {
  props: ['id'],
  template: '<div>User {{ id }}</div>'
}
const router = new VueRouter({
  routes: [
    { path: '/user/:id', component: User, props: true },

    // 对于包含命名视图的路由,你必须分别为每个命名视图添加props选项:
    {
      path: '/user/:id',
      components: { default: User, sidebar: Sidebar },
      props: { default: true, sidebar: false }
    }
  ]
})

这样你便可以在任何地方使用该组件,使得该组件更易于重用和测试。

如果props被设置为true,route.params将会被设置为组件属性。

如果props是一个对象,它会被按原样设置为组件属性。当props是静态的时候有用。

const router = new VueRouter({
  routes: [
    { path: '/promotion/from-newsletter', component: Promotion, props: { newsletterPopup: false } }
  ]
})

你可以创建一个函数返回props。这样你便可以将参数转换成另一种类型,将静态值与基于路由的值结合等等。

const router = new VueRouter({
  routes: [
    { path: '/search', component: SearchUser, props: (route) => ({ query: route.query.q }) }
  ]
})

URL /search?q=vue会将{query: 'vue'}作为属性传递给SearchUser组件。

请尽可能保持props函数为无状态的,因为它只会在路由发生变化时起作用。如果你需要状态来定义props,请使用包装组件,这样Vue才可以对状态变化做出反应。更多高级用法,请查看例子

4.4.9 HTML5 History模式

vue-router默认hash模式—— 使用URL的hash来模拟一个完整的URL,于是当URL改变时,页面不会重新加载。

如果不想要很丑的hash,我们可以用路由的history模式,这种模式充分利用history.pushState API来完成URL跳转而无须重新加载页面。

const router = new VueRouter({
  mode: 'history',
  routes: [...]
})

当你使用history模式时,URL就像正常的url,例如http://yoursite.com/user/id,也好看!不过这种模式要玩好,还需要后台配置支持。因为我们的应用是个单页客户端应用,如果后台没有正确的配置,当用户在浏览器直接访问http://oursite.com/user/id就会返回404,这就不好看了。

所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果URL匹配不到任何静态资源,则应该返回同一个index.html页面,这个页面就是你app依赖的页面。

<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteBase /
  RewriteRule ^index\.html$ - [L]
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule . /index.html [L]
</IfModule>

除了mod_rewrite,你也可以使用FallbackResource

location / {
  try_files $uri $uri/ /index.html;
}
const http = require('http')
const fs = require('fs')
const httpPort = 80

http.createServer((req, res) => {
  fs.readFile('index.htm', 'utf-8', (err, content) => {
    if (err) {
      console.log('We cannot open "index.htm" file.')
    }

    res.writeHead(200, {
      'Content-Type': 'text/html; charset=utf-8'
    })

    res.end(content)
  })
}).listen(httpPort, () => {
  console.log('Server listening on: http://localhost:%s', httpPort)
})

对于Node.js/Express,请考虑使用connect-history-api-fallback中间件

安装IIS UrlRewrite,在你的网站根目录中创建一个web.config文件,内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <system.webServer>
    <rewrite>
      <rules>
        <rule name="Handle History Mode and custom 404/500" stopProcessing="true">
          <match url="(.*)" />
          <conditions logicalGrouping="MatchAll">
            <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
            <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
          </conditions>
          <action type="Rewrite" url="/" />
        </rule>
      </rules>
    </rewrite>
  </system.webServer>
</configuration>
#Caddy
rewrite {
    regexp .*
    to {path} /
}

在你的firebase.json中加入:

{
  "hosting": {
    "public": "dist",
    "rewrites": [
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  }
}

给个警告,因为这么做以后,你的服务器就不再返回404错误页面,因为对于所有路径都会返回index.html文件。为了避免这种情况,你应该在Vue应用里面覆盖所有的路由情况,然后在给出一个404页面。

const router = new VueRouter({
  mode: 'history',
  routes: [
    { path: '*', component: NotFoundComponent }
  ]
})

或者,如果你使用Node.js服务器,你可以用服务端路由匹配到来的URL,并在没有匹配到路由的时候返回404,以实现回退。更多详情请查阅Vue服务端渲染文档

4.4.10 导航守卫

“导航”表示路由正在发生改变。正如其名,vue-router提供的导航守卫主要用来通过跳转或取消的方式守卫导航。有多种机会植入路由导航过程中:全局的,单个路由独享的,或者组件级的。记住参数或查询的改变并不会触发进入/离开的导航守卫。你可以通过观察$route对象来应对这些变化,或使用beforeRouteUpdate的组件内守卫。

你可以使用router.beforeEach注册一个全局前置守卫:

const router = new VueRouter({ ... })

router.beforeEach((to, from, next) => {
  // ...
})

当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫resolve完之前一直处于等待中。

每个守卫方法接收三个参数:

  1. to: Route,即将要进入的目标路由对象
  2. from: Route,当前导航正要离开的路由
  3. next: Function,一定要调用该方法来resolve这个钩子。执行效果依赖next方法的调用参数。
  4. next(): 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是confirmed(确认的)。
  5. next(false): 中断当前的导航。如果浏览器的URL改变了(可能是用户手动或者浏览器后退按钮),那么URL地址会重置到from路由对应的地址。
  6. next('/')或者next({ path: '/' }): 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。你可以向next传递任意位置对象,且允许设置诸如replace: truename: 'home'之类的选项以及任何用在router-linkto proprouter.push中的选项。
  7. next(error): 如果传入next的参数是一个Error实例,则导航会被终止且该错误会被传递给router.onError()注册过的回调。

确保要调用next方法,否则钩子就不会被resolved

在2.5.0+你可以用router.beforeResolve注册一个全局守卫。这和router.beforeEach类似,区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用。

你也可以注册全局后置钩子,然而和守卫不同的是,这些钩子不会接受next函数也不会改变导航本身:

router.afterEach((to, from) => {
  // ...
})

你可以在路由配置上直接定义beforeEnter守卫:

const router = new VueRouter({
  routes: [
    {
      path: '/foo',
      component: Foo,
      beforeEnter: (to, from, next) => {
        // ...
      }
    }
  ]
})

这些守卫与全局前置守卫的方法参数是一样的。

最后,你可以在路由组件内直接定义以下路由导航守卫:beforeRouteEnter,beforeRouteUpdate,beforeRouteLeave

const Foo = {
  template: `...`,
  beforeRouteEnter (to, from, next) {
    // 在渲染该组件的对应路由被confirm前调用
    // 不能 获取组件实例this
    // 因为当守卫执行前,组件实例还没被创建
  },
  beforeRouteUpdate (to, from, next) {
    // 在当前路由改变,但是该组件被复用时调用
    // 举例来说,对于一个带有动态参数的路径/foo/:id,在/foo/1和/foo/2之间跳转的时候,
    // 由于会渲染同样的Foo组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
    // 可以访问组件实例this
  },
  beforeRouteLeave (to, from, next) {
    // 导航离开该组件的对应路由时调用
    // 可以访问组件实例this
  }
}

beforeRouteEnter守卫不能访问this,因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建。不过,你可以通过传一个回调给next来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数。

beforeRouteEnter (to, from, next) {
  next(vm => {
    // 通过 `vm` 访问组件实例
  })
}

注意beforeRouteEnter是支持给next传递回调的唯一守卫。对于beforeRouteUpdatebeforeRouteLeave来说,this已经可用了,所以不支持传递回调,因为没有必要了。

beforeRouteUpdate (to, from, next) {
  // just use `this`
  this.name = to.params.name
  next()
}

这个离开守卫通常用来禁止用户在还未保存修改前突然离开。该导航可以通过next(false)来取消。

beforeRouteLeave (to, from , next) {
  const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
  if (answer) {
    next()
  } else {
    next(false)
  }
}
  1. 导航被触发。
  2. 在失活的组件里调用离开守卫。
  3. 调用全局的beforeEach守卫。
  4. 在重用的组件里调用beforeRouteUpdate守卫。
  5. 在路由配置里调用beforeEnter
  6. 解析异步路由组件。
  7. 在被激活的组件里调用beforeRouteEnter
  8. 调用全局的beforeResolve守卫。
  9. 导航被确认。
  10. 调用全局的afterEach钩子。
  11. 触发DOM更新。
  12. 用创建好的实例调用beforeRouteEnter守卫中传给next的回调函数。

4.4.11 路由元信息

定义路由的时候可以配置meta字段:

const router = new VueRouter({
  routes: [
    {
      path: '/foo',
      component: Foo,
      children: [
        {
          path: 'bar',
          component: Bar,
          // a meta field
          meta: { requiresAuth: true }
        }
      ]
    }
  ]
})

那么如何访问这个meta字段呢?首先,我们称呼routes配置中的每个路由对象为路由记录。路由记录可以是嵌套的,因此,当一个路由匹配成功后,他可能匹配多个路由记录。例如,根据上面的路由配置,/foo/bar这个URL将会匹配父路由记录以及子路由记录。

一个路由匹配到的所有路由记录会暴露为$route对象(还有在导航守卫中的路由对象)的$route.matched数组。因此,我们需要遍历$route.matched来检查路由记录中的meta字段。

下面例子展示在全局导航守卫中检查元字段:

router.beforeEach((to, from, next) => {
  if (to.matched.some(record => record.meta.requiresAuth)) {
    // this route requires auth, check if logged in
    // if not, redirect to login page.
    if (!auth.loggedIn()) {
      next({
        path: '/login',
        query: { redirect: to.fullPath }
      })
    } else {
      next()
    }
  } else {
    next() // 确保一定要调用 next()
  }
})

4.4.12 过渡动效

<router-view >是基本的动态组件,所以我们可以用<transition\>组件给它添加一些过渡效果:

<transition>
  <router-view></router-view>
</transition>

Transition的所有功能 在这里同样适用。

上面的用法会给所有路由设置一样的过渡效果,如果你想让每个路由组件有各自的过渡效果,可以在各路由组件内使用<transition\>并设置不同的name

const Foo = {
  template: `
    <transition name="slide">
      <div class="foo">...</div>
    </transition>
  `
}

const Bar = {
  template: `
    <transition name="fade">
      <div class="bar">...</div>
    </transition>
  `
}

还可以基于当前路由与目标路由的变化关系,动态设置过渡效果:

<!-- 使用动态的transition name -->
<transition :name="transitionName">
  <router-view></router-view>
</transition>
// 接着在父组件内
// watch $route决定使用哪种过渡
watch: {
  '$route' (to, from) {
    const toDepth = to.path.split('/').length
    const fromDepth = from.path.split('/').length
    this.transitionName = toDepth < fromDepth ? 'slide-right' : 'slide-left'
  }
}

查看完整例子请移步这里

4.4.13 数据获取

有时候,进入某个路由后,需要从服务器获取数据。例如,在渲染用户信息时,你需要从服务器获取用户的数据。我们可以通过两种方式来实现:

  1. 导航完成之后获取:先完成导航,然后在接下来的组件生命周期钩子中获取数据。在数据获取期间显示“加载中”之类的指示。
  2. 导航完成之前获取:导航完成前,在路由进入的守卫中获取数据,在数据获取成功后执行导航。

从技术角度讲,两种方式都不错——就看你想要的用户体验是哪种。

当你使用这种方式时,我们会马上导航和渲染组件,然后在组件的created钩子中获取数据。这让我们有机会在数据获取期间展示一个loading状态,还可以在不同视图间展示不同的loading状态。

假设我们有一个Post组件,需要基于$route.params.id获取文章数据:

<template>
  <div class="post">
    <div v-if="loading" class="loading">
      Loading...
    </div>

    <div v-if="error" class="error">
      {{ error }}
    </div>

    <div v-if="post" class="content">
      <h2>{{ post.title }}</h2>
      <p>{{ post.body }}</p>
    </div>
  </div>
</template>
export default {
  data () {
    return {
      loading: false,
      post: null,
      error: null
    }
  },
  created () {
    // 组件创建完后获取数据,
    // 此时data已经被observed了
    this.fetchData()
  },
  watch: {
    // 如果路由有变化,会再次执行该方法
    '$route': 'fetchData'
  },
  methods: {
    fetchData () {
      this.error = this.post = null
      this.loading = true
      // replace getPost with your data fetching util / API wrapper
      getPost(this.$route.params.id, (err, post) => {
        this.loading = false
        if (err) {
          this.error = err.toString()
        } else {
          this.post = post
        }
      })
    }
  }
}

通过这种方式,我们在导航转入新的路由前获取数据。我们可以在接下来的组件的beforeRouteEnter守卫中获取数据,当数据获取成功后只调用next方法。

export default {
  data () {
    return {
      post: null,
      error: null
    }
  },
  beforeRouteEnter (to, from, next) {
    getPost(to.params.id, (err, post) => {
      next(vm => vm.setData(err, post))
    })
  },
  // 路由改变前,组件就已经渲染完了
  // 逻辑稍稍不同
  beforeRouteUpdate (to, from, next) {
    this.post = null
    getPost(to.params.id, (err, post) => {
      this.setData(err, post)
      next()
    })
  },
  methods: {
    setData (err, post) {
      if (err) {
        this.error = err.toString()
      } else {
        this.post = post
      }
    }
  }
}

在为后面的视图获取数据时,用户会停留在当前的界面,因此建议在数据获取期间,显示一些进度条或者别的指示。如果数据获取失败,同样有必要展示一些全局的错误提醒。

4.4.14 滚动行为

使用前端路由,当切换到新路由时,想要页面滚到顶部,或者是保持原先的滚动位置,就像重新加载页面那样。vue-router能做到,而且更好,它让你可以自定义路由切换时页面如何滚动。

注意: 这个功能只在支持history.pushState的浏览器中可用。

当创建一个Router实例,你可以提供一个scrollBehavior方法:

const router = new VueRouter({
  routes: [...],
  scrollBehavior (to, from, savedPosition) {
    // return期望滚动到哪个的位置
  }
})

scrollBehavior方法接收tofrom路由对象。第三个参数savedPosition当且仅当popstate导航(通过浏览器的前进/后退按钮触发)时才可用。这个方法返回滚动位置的对象信息,长这样:

{ x: number, y: number }
{ selector: string, offset? : { x: number, y: number }} (offset只在2.6.0+支持)

如果返回一个falsy的值,或者是一个空对象,那么不会发生滚动。举例:

scrollBehavior (to, from, savedPosition) {
  return { x: 0, y: 0 }
}

对于所有路由导航,简单地让页面滚动到顶部。返回savedPosition,在按下后退/前进按钮时,就会像浏览器的原生表现那样:

scrollBehavior (to, from, savedPosition) {
  if (savedPosition) {
    return savedPosition
  } else {
    return { x: 0, y: 0 }
  }
}

如果你要模拟“滚动到锚点”的行为:

scrollBehavior (to, from, savedPosition) {
  if (to.hash) {
    return {
      selector: to.hash
    }
  }
}

我们还可以利用路由元信息更细颗粒度地控制滚动。查看完整例子请移步这里

你也可以返回一个Promise来得出预期的位置描述:

scrollBehavior (to, from, savedPosition) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({ x: 0, y: 0 })
    }, 500)
  })
}

将其挂载到从页面级别的过渡组件的事件上,令其滚动行为和页面过渡一起良好运行是可能的。但是考虑到用例的多样性和复杂性,我们仅提供这个原始的接口,以支持不同用户场景的具体实现。

4.4.15 路由懒加载

当打包构建应用时,JavaScript包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。结合Vue的异步组件和Webpack的代码分割功能,轻松实现路由组件的懒加载。

首先,可以将异步组件定义为返回一个Promise的工厂函数(该函数返回的Promise应该resolve组件本身):

const Foo = () => Promise.resolve({ /* 组件定义对象 */ })

第二,在 Webpack 2中,我们可以使用动态import语法来定义代码分块点(split point):

import('./Foo.vue') // 返回 Promise

注意:如果您使用的是Babel,你将需要添加syntax-dynamic-import插件,才能使Babel可以正确地解析语法。

结合这两者,这就是如何定义一个能够被Webpack自动代码分割的异步组件。

const Foo = () => import('./Foo.vue')

在路由配置中什么都不需要改变,只需要像往常一样使用Foo:

const router = new VueRouter({
  routes: [
    { path: '/foo', component: Foo }
  ]
})

有时候我们想把某个路由下的所有组件都打包在同个异步块(chunk)中。只需要使用命名chunk,一个特殊的注释语法来提供chunk name(需要Webpack > 2.4)。

const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue')
const Bar = () => import(/* webpackChunkName: "group-foo" */ './Bar.vue')
const Baz = () => import(/* webpackChunkName: "group-foo" */ './Baz.vue')

Webpack会将任何一个异步模块与相同的块名称组合到相同的异步块中。

4.5 vuex

我在使用基于vue.js 2.0的UI框架ElementUI开发网站的时候,就遇到了这种问题:一个页面有很多表单,我试图将表单写成一个单文件组件,但是表单(子组件)里的数据和页面(父组件)按钮交互的时候,它们之间的通讯很麻烦:

<!--父组件中引入子组件-->
<template>
  <div>
    <a href="javascript:;" @click="show = true">点击</a>
    <t-dialog :show.sync="show"></t-dialog>
  </div>
</template>

<script>
import dialog from './components/dialog.vue'
export default {
  data(){
    return {
      show:false
    }
  },
  components:{
    "t-dialog":dialog
  }
}
</script>


<!--子组件-->
<template>
  <el-dialog :visible.sync="currentShow"></el-dialog>
</template>

<script>
export default {
  props:['show'],
  computed:{
      currentShow:{
          get(){
              return this.show
          },
          set(val){
              this.$emit("update:show",val)
          }
      }
  }
}
</script>

之所以这么麻烦,是因为父组件可以通过props给子组件传递参数,但子组件内却不能直接修改父组件传过来的参数。这时候,使用vuex就可以比较方便的解决这种问题了:

<!--父组件中引入子组件-->
<template>
  <div>
    <a href="javascript:;" @click="$store.state.show = true">点击</a>
    <t-dialog></t-dialog>
  </div>
</template>

<script>
import dialog from './components/dialog.vue'
export default {
  components:{
    "t-dialog":dialog
  }
}
</script>


<!--子组件-->
<template>
  <el-dialog :visible.sync="$store.state.show"></el-dialog>
</template>

<script>
export default {}
</script>

是不是方便了许多,这就是vuex最简单的应用。

4.5.1 安装使用vuex

首先在开发环境中安装vuex:

npm install vuex --save

然后,在main.js中加入:

import vuex from 'vuex'
Vue.use(vuex);
var store = new vuex.Store({//store对象
    state:{
        show:false
    }
})

再然后,在实例化Vue对象时加入store对象:

new Vue({
  el: '#app',
  router,
  store,//使用store
  template: '<App/>',
  components: { App }
})

完成到这一步,上述例子中的$store.state.show就可以使用了。

4.5.3 modules

前面为了方便,我们把store对象写在了main.js里面,但实际上为了便于日后的维护,我们分开写更好,我们在src目录下,新建一个store文件夹,然后在里面新建一个index.js:

import Vue from 'vue'
import vuex from 'vuex'
Vue.use(vuex);

export default new vuex.Store({
    state:{
        show:false
    }
})
那么相应的 , 在 main.js 里的代码应该改成 :

//vuex
import store from './store'

new Vue({
  el: '#app',
  router,
  store,//使用store
  template: '<App/>',
  components: { App }
})

这样就把store分离出去了,那么还有一个问题是:这里$store.state.show无论哪个组件都可以使用,那组件多了之后,状态也多了,这么多状态都堆在store文件夹下的index.js不好维护怎么办?

我们可以使用vuexmodules,把store文件夹下的index.js改成:

import Vue from 'vue'
import vuex from 'vuex'
Vue.use(vuex);

import dialog_store from '../components/dialog_store.js'; // 引入某个store对象

export default new vuex.Store({
    modules: {
        dialog: dialog_store
    }
})

这里我们引用了一个dialog_store.js,在这个js文件里我们就可以单独写dialog组件的状态了:

export default {
    state:{
        show:false
    }
}

做出这样的修改之后,我们将之前我们使用的$store.state.show统统改为$store.state.dialog.show即可。

如果还有其他的组件需要使用vuex,就新建一个对应的状态文件,然后将他们加入store文件夹下的index.js文件中的modules中。

modules: {
    dialog: dialog_store,
    other: other,//其他组件
}

4.5.4 mutations

前面我们提到的对话框例子,我们对vuex的依赖仅仅只有一个$store.state.dialog.show一个状态,但是如果我们要进行一个操作,需要依赖很多很多个状态,那管理起来又麻烦了!

mutations登场,问题迎刃而解:

export default {
    state:{//state
        show:false
    },
    mutations:{
        switch_dialog(state){ // 这里的state对应着上面这个state
            state.show = state.show?false:true;
            // 你还可以在这里执行其他的操作改变state
        }
    }
}

使用mutations后,原先我们的父组件可以改为:

<template>
  <div id="app">
    <a href="javascript:;" @click="$store.commit('switch_dialog')">点击</a>
    <t-dialog></t-dialog>
  </div>
</template>

<script>
import dialog from './components/dialog.vue'
export default {
  components:{
    "t-dialog":dialog
  }
}
</script>

使用$store.commit('switch_dialog')来触发mutations中的switch_dialog方法。

这里需要注意的是:

mutations中的方法是不分组件的,假如你在dialog_stroe.js文件中的定义了switch_dialog方法,在其他文件中的一个switch_dialog方法,那么$store.commit('switch_dialog')会执行所有的switch_dialog方法。
mutations里的操作必须是同步的。

你一定好奇,如果在mutations里执行异步操作会发生什么事情,实际上并不会发生什么奇怪的事情,只是官方推荐,不要在mutationss里执行异步操作而已。

4.5.5 actions

多个state的操作,使用mutations会来触发会比较好维护,那么需要执行多个mutations就需要用action了:

export default {
    state:{//state
        show:false
    },
    mutations:{
        switch_dialog(state){//这里的state对应着上面这个state
            state.show = state.show?false:true;
            // 你还可以在这里执行其他的操作改变state
        }
    },
    actions:{
        switch_dialog(context){ // 这里的context和我们使用的$store拥有相同的对象和方法
            context.commit('switch_dialog');
            // 你还可以在这里触发其他的mutations方法
        },
    }
}

那么,在之前的父组件中,我们需要做修改,来触发action里的switch_dialog方法:

<template>
  <div id="app">
    <a href="javascript:;" @click="$store.dispatch('switch_dialog')">点击</a>
    <t-dialog></t-dialog>
  </div>
</template>

<script>
import dialog from './components/dialog.vue'
export default {
  components:{
    "t-dialog":dialog
  }
}
</script>

使用$store.dispatch('switch_dialog')来触发action中的switch_dialog方法。官方推荐,将异步操作放在action中。

4.5.6 getters

gettersvue中的computed类似,都是用来计算state然后生成新的数据(状态)的。

还是前面的例子,假如我们需要一个与状态show刚好相反的状态,使用vue中的computed可以这样算出来:

computed(){
    not_show(){
        return !this.$store.state.dialog.show;
    }
}

那么,如果很多很多个组件中都需要用到这个与show刚好相反的状态,那么我们需要写很多很多个not_show,使用getters就可以解决这种问题:

export default {
    state:{//state
        show:false
    },
    getters:{
        not_show(state){ // 这里的state对应着上面这个state
            return !state.show;
        }
    },
    mutations:{
        switch_dialog(state){ // 这里的state对应着上面这个state
            state.show = state.show?false:true;
            // 你还可以在这里执行其他的操作改变state
        }
    },
    actions:{
        switch_dialog(context){// 这里的context和我们使用的$store拥有相同的对象和方法
            context.commit('switch_dialog');
            //你还可以在这里触发其他的mutations方法
        },
    }
}

我们在组件中使用$store.state.dialog.show来获得状态show,类似的,我们可以使用$store.getters.not_show来获得状态not_show

注意: $store.getters.not_show的值是不能直接修改的,需要对应的state发生变化才能修改。

4.5.7 mapStatemapGettersmapActions

很多时候,$store.state.dialog.show$store.dispatch('switch_dialog')这种写法又长又臭,很不方便,我们没使用vuex的时候,获取一个状态只需要this.show,执行一个方法只需要this.switch_dialog就行了,使用vuex使写法变复杂了?

使用mapStatemapGettersmapActions就不会这么复杂了。以mapState为例:

<template>
  <el-dialog :visible.sync="show"></el-dialog>
</template>

<script>
import {mapState} from 'vuex';
export default {
  computed:{
    // 这里的三点叫做:扩展运算符
    ...mapState({
      show:state=>state.dialog.show
    }),
  }
}
</script>

相当于:

<template>
  <el-dialog :visible.sync="show"></el-dialog>
</template>

<script>
import {mapState} from 'vuex';
export default {
  computed:{
    show(){
        return this.$store.state.dialog.show;
    }
  }
}
</script>

mapGettersmapActionsmapState类似,mapGetters一般也写在computed中,mapActions一般写在methods中。

五、HTML样例

5.1 自适应列表布局

<!DOCTYPE html>
<html lang="en">
<head>
    <title>inline-block自适应列表布局</title>
    <style>
        .box{width:50%; padding:20px; margin:20px auto; background-color:#f0f3f9; text-align:justify;padding-bottom: 0;}
        .list{width:120px; display:inline-block; padding-bottom:20px; text-align:center; vertical-align:top;}
        i.list{padding-bottom:0;}
        .fix-justify{font-size:0;}
    </style>
</head>

<body>
    <h1>inline-block自适应列表布局实例页面</h1>
    <div class="demo">
        <div class="box">
            <div class="list"><img style="width:100px;" src="https://ss2.baidu.com/6ONYsjip0QIZ8tyhnq/it/u=2569254890,3462267034&fm=58" /><br />这是一段文字描述</div>
            <div class="list"><img style="width:100px;" src="https://ss2.baidu.com/6ONYsjip0QIZ8tyhnq/it/u=2569254890,3462267034&fm=58" /><br />这是一段文字描述</div>
        </div>
    </div>

</body>
</html>

5.2 CSS3卡牌旋转滑动效果

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" type="text/css" href="http://caibaojian.com/demo/reset.css"/>
    <title>Document</title>
</head>
<body>

    <style type="text/css">
        .demo {
            position: relative;
            height: 320px;
            width: 100%
        }

        .demo .item {
            position: absolute;
            left: 50%;
            bottom: 0;
            -webkit-transform: translate3d(-50%, 0, 0);
            transform: translate3d(-50%, 0, 0);
            -webkit-transition: all 0.6s;
            transition: all 0.6s;
            width: 222px;
            height: 300px;
            /*border: 1px solid #ccc;*/
            text-align: center;
            line-height: 300px;
            z-index: 1;
            font-size: 74px;
            color: #fff;
        }

        .demo .item_cur {
            z-index: 3
        }

        .demo .item_0 {
            -webkit-transform: translate3d(-104%, 12%, 0) scale3d(0.5, 0.5, 1);
            transform: translate3d(-104%, 12%, 0) scale3d(0.5, 0.5, 1)
        }

        .demo .item_1 {
            -webkit-transform: translate3d(-81%, 9%, 0) scale3d(0.7, 0.7, 1);
            transform: translate3d(-81%, 9%, 0) scale3d(0.7, 0.7, 1);
            z-index: 2
        }

        .demo .item_3 {
            -webkit-transform: translate3d(-20%, 9%, 0) scale3d(0.7, 0.7, 1);
            transform: translate3d(-20%, 9%, 0) scale3d(0.7, 0.7, 1);
            z-index: 2
        }

        .demo .item_4 {
            -webkit-transform: translate3d(4%, 12%, 0) scale3d(0.5, 0.5, 1);
            transform: translate3d(4%, 12%, 0) scale3d(0.5, 0.5, 1)
        }

        .demo_btn{
            text-align: center;
        }
    </style>

    <div class="demo">
        <div class="item item_0">1</div>
        <div class="item item_1">2</div>
        <div class="item item_cur">3</div>
        <div class="item item_3">4</div>
        <div class="item item_4">5</div>
    </div>

    <div class="demo_btn">
        <a href="javascript:;" class="left" title="向左移动">&lt;&lt;</a>
        <a href="javascript:;" class="right" title="向右移动">&gt;&gt;</a>
        <p>(PC下可点击按钮切换,移动端可左右滑动切换)</p>
    </div>

    <script type="text/javascript" src="http://mat1.gtimg.com/libs/zepto/1.1.6/zepto.min.js"></script>
    <script type="text/javascript" src="http://mat1.gtimg.com/news/representative/js/touch.js"></script>

    <script type="text/javascript">
    (function(){
        var getRandomColor = function(){
            return '#'+Math.floor(Math.random()*16777215).toString(16);
        }

        var egg_change = function(type){
            var $demo = $('.demo'),
                index = parseInt( $demo.attr('index_cur')||2 ),
                $item = $('.demo .item'),
                len = $item.length;

            if( type=='left' ){
                index = (index+1)%len;
            }else{
                index = (index-1+len)%len;
            }
            $demo.attr('index_cur', index);

            $item.removeClass('item_0 item_1 item_3 item_4 item_cur');

            $item.eq( (index-2+len)%len ).addClass('item_0');
            $item.eq( (index-1+len)%len ).addClass('item_1');
            $item.eq(index).addClass('item_cur');
            $item.eq( (index+1)%len ).addClass('item_3');
            $item.eq( (index+2)%len ).addClass('item_4');
        }

        $('.item').each(function(){
            $(this).css('background-color', getRandomColor());
        })

        $('.demo').on('swipeLeft', function(){
            egg_change( 'left' );
        }).on('swipeRight', function(){
            egg_change( 'right' );
        });

        $('.demo_btn').on('click', '.left', function(){
            egg_change( 'left' );
        }).on('click', '.right', function(){
            egg_change( 'right' );
        })
    })();
    </script>
    <script type="text/javascript" src="//caibaojian.com/demo/base.js"></script>
</body>
</html>

5.3 旋转相册和立方体相册

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>旋转相册</title>
    <style type="text/css">
body,div,p,ul,ol,li,dl,dt,dd,table,tr,td,form,hr,fieldset,h1,h2,h3,h4,h5,h6,img,input{
    margin:0;
    padding:0;
}
body{
    background: black;
}
.content{
    width: 200px;
    height: 250px;
    position: relative;
    margin:200px auto 0;
    perspective: 1500px;
}
.box{
    width: 200px;
    height: 250px;
    transform-style: preserve-3d;
    transform:rotateX(-30deg);
    animation:photo 15s linear infinite;
}
.box:hover{
    animation:photo 15s linear infinite paused;
}
.box img{
    width: 200px;
    height: 250px;
    position: absolute;
    left: 0;
    top: 0;
    transform-style: preserve-3d;
    transition: all 1s;
}
.box img:nth-child(1){
    transform:translateZ(280px);
}
.box img:nth-child(2){
    transform:rotateY(40deg) translateZ(280px);
}
.box img:nth-child(3){
    transform:rotateY(80deg) translateZ(280px);
}
.box img:nth-child(4){
    transform:rotateY(120deg) translateZ(280px);
}
.box img:nth-child(5){
    transform:rotateY(160deg) translateZ(280px);
}
.box img:nth-child(6){
    transform:rotateY(200deg) translateZ(280px);
}
.box img:nth-child(7){
    transform:rotateY(240deg) translateZ(280px);
}
.box img:nth-child(8){
    transform:rotateY(280deg) translateZ(280px);
}
.box img:nth-child(9){
    transform:rotateY(320deg) translateZ(280px);
}

.box img:nth-child(1):hover{
    transform:translateZ(280px) scale(1.2);
}
.box img:nth-child(2):hover{
    transform:rotateY(40deg) translateZ(280px) scale(1.2);
}
.box img:nth-child(3):hover{
    transform:rotateY(80deg) translateZ(280px) scale(1.2);
}
.box img:nth-child(4):hover{
    transform:rotateY(120deg) translateZ(280px) scale(1.2);
}
.box img:nth-child(5):hover{
    transform:rotateY(160deg) translateZ(280px) scale(1.2);
}
.box img:nth-child(6):hover{
    transform:rotateY(200deg) translateZ(280px) scale(1.2);
}
.box img:nth-child(7):hover{
    transform:rotateY(240deg) translateZ(280px) scale(1.2);
}
.box img:nth-child(8):hover{
    transform:rotateY(280deg) translateZ(280px) scale(1.2);
}
.box img:nth-child(9):hover{
    transform:rotateY(320deg) translateZ(280px) scale(1.2);
}

@keyframes photo{
    0%{
        transform:rotateX(-30deg) rotateY(0deg);
    }
    100%{
        transform:rotateX(-30deg) rotateY(360deg);
    }
}
</style>
</head>
<body>
<div class="content">
    <div class="box">
        <img src="https://pbs.twimg.com/media/EEzFRnEU4AA2jEJ?format=jpg&name=large" alt=""/>
        <img src="https://pbs.twimg.com/media/EEzFRnGVAAELC9o?format=jpg&name=large" alt=""/>
        <img src="https://pbs.twimg.com/media/EEzFRnEU4AA2jEJ?format=jpg&name=large" alt=""/>
        <img src="https://pbs.twimg.com/media/EEzFRnGVAAELC9o?format=jpg&name=large" alt=""/>
        <img src="https://pbs.twimg.com/media/EEzFRnEU4AA2jEJ?format=jpg&name=large" alt=""/>
        <img src="https://pbs.twimg.com/media/EEzFRnGVAAELC9o?format=jpg&name=large" alt=""/>
        <img src="https://pbs.twimg.com/media/EEzFRnEU4AA2jEJ?format=jpg&name=large" alt=""/>
        <img src="https://pbs.twimg.com/media/EEzFRnGVAAELC9o?format=jpg&name=large" alt=""/>
        <img src="https://pbs.twimg.com/media/EEzFRnEU4AA2jEJ?format=jpg&name=large" alt=""/>
    </div>
</div>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>Page Title</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
        ul{
            list-style-type: none;
            margin: 0;
            padding: 0;
        }
        .box{
            width: 300px;
            height: 300px;
            margin: 150px auto;
            position: relative;
            font-size: 50px;
            transform-style: preserve-3d;
            /*动画效果*/
            animation: rotate 10s linear infinite;
        }
        .box>div{
            width: 300px;
            height: 300px;
            position: absolute;
        }
        li{
            float: left;
            width: 90px;
            height: 90px;
            margin: 5px;
            border-radius: 20px;
            line-height: 90px;
            text-align: center;
        }
        .dv1 li{
            background-color: orange;
            transform: skewZ(60deg);
        }
        .dv1{
            background-color: transparent;
            transform: rotateY(90deg) translateZ(150px);
        }
        .dv2{
            background-color: transparent;
            transform: rotateY(-90deg) translateZ(150px);
        }
        .dv3{
            background-color: transparent;
            transform: rotateX(90deg) translateZ(150px);
        }
        .dv4{
            background-color: transparent;
            transform: rotateX(-90deg) translateZ(150px);
        }
        .dv5{
            background-color: transparent;
            transform:translateZ(150px);
        }
        .dv6{
            background-color: transparent;
            transform:translateZ(-150px);
        }

        .dv2 li{
            background-color: green;
        }
        .dv3 li{
            background-color: greenyellow;
        }
        .dv4 li{
            background-color: palevioletred;
        }
        .dv5 li{
            background-color: pink;
        }
        .dv6 li{
            background-color: yellow;
        }
        @keyframes rotate{
            0%{
                transform: rotateY(0deg) rotateX(0deg);
            }
            100%{
                transform: rotateY(135deg) rotateX(45deg);
            }
        }
    </style>
</head>
<body>
    <div class="box">
        <div class="dv1"><ul>
            <li>1</li>
            <li>2</li>
            <li>3</li>
            <li>4</li>
            <li>5</li>
            <li>6</li>
            <li>7</li>
            <li>8</li>
            <li>9</li>
        </ul></div>
        <div class="dv2"><ul>
                <li>1</li>
                <li>2</li>
                <li>3</li>
                <li>4</li>
                <li>5</li>
                <li>6</li>
                <li>7</li>
                <li>8</li>
                <li>9</li>
            </ul></div>
        <div class="dv3"><ul>
                <li>1</li>
                <li>2</li>
                <li>3</li>
                <li>4</li>
                <li>5</li>
                <li>6</li>
                <li>7</li>
                <li>8</li>
                <li>9</li>
            </ul></div>
        <div class="dv4"><ul>
                <li>1</li>
                <li>2</li>
                <li>3</li>
                <li>4</li>
                <li>5</li>
                <li>6</li>
                <li>7</li>
                <li>8</li>
                <li>9</li>
            </ul></div>
        <div class="dv5"><ul>
                <li>1</li>
                <li>2</li>
                <li>3</li>
                <li>4</li>
                <li>5</li>
                <li>6</li>
                <li>7</li>
                <li>8</li>
                <li>9</li>
            </ul></div>
        <div class="dv6"><ul>
                <li>1</li>
                <li>2</li>
                <li>3</li>
                <li>4</li>
                <li>5</li>
                <li>6</li>
                <li>7</li>
                <li>8</li>
                <li>9</li>
            </ul></div>
    </div>
</body>
</html>
<html>
<head>
<title>二十面体</title>
<style type="text/css">
html, body {
height: 100%;
margin: 0;
background: rgba(0, 0, 0,1);
}
.sharp {
animation: ani 4s linear infinite;
}
div {
transform-style: preserve-3d;
transform: translate(-50%, -50%) rotate3d(0, 1, 0, 72deg);
position: absolute;
left: 50%;
top: 50%;
}
span { /*利用边框做一个三角形*/
border-color: transparent transparent rgba(255, 255, 255, 0.5) transparent;/*每个span下方设置颜色,其余边透明*/
border-width: 173.2px 100px;
border-style: solid;
width: 0;
height: 0;
position: absolute;
left: 50%;
margin-left: -100px;
top: 50%;
margin-top: -346.4px;
}

span:before { /*利用边框在span中做一个三角形,实现嵌套,让span变成边框,span:before变成要显示的东西*/
content: '';
border-color: transparent transparent rgba(0, 123, 123, 0.5) transparent;/*设置每面的颜色*/
border-width: 165.2px 92px;
border-style: solid;
width: 0;
height: 0;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
margin-top: 4px;
}

div span:nth-child(1) {
transform: rotate3d(0, 1, 0, 72deg) rotate3d(1, 0, 0, 51.5deg);
}

div span:nth-child(2) {
transform: rotate3d(0, 1, 0, 144deg) rotate3d(1, 0, 0, 51.5deg);
}

div span:nth-child(3) {
transform: rotate3d(0, 1, 0, 216deg) rotate3d(1, 0, 0, 51.5deg);
}

div span:nth-child(4) {
transform: rotate3d(0, 1, 0, 288deg) rotate3d(1, 0, 0, 51.5deg);
}

div span:nth-child(5) {
transform: rotate3d(0, 1, 0, 360deg) rotate3d(1, 0, 0, 51.5deg);
}

.sharp div:nth-child(1) {
transform: translateY(51px) rotateY(108deg) rotateX(116deg) translateZ(31px);
}

.sharp div:nth-child(2) {
transform: translateY(51px) rotateY(180deg) rotateX(116deg) translateZ(31px);
}

.sharp div:nth-child(3) {
transform: translateY(51px) rotateY(252deg) rotateX(116deg) translateZ(31px);
}

.sharp div:nth-child(4) {
transform: translateY(51px) rotateY(324deg) rotateX(116deg) translateZ(31px);
}

.sharp div:nth-child(5) {
transform: translateY(51px) rotateY(396deg) rotateX(116deg) translateZ(31px);
}

@keyframes ani {
100% {
transform: rotateY(360deg);
}
}
</style>
</head>
<body>
<div class="sharp">
<div>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
<div>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
<div>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
<div>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
<div>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
<div>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
</div>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>CSS3 魔方</title>
    <!-- 样式部分全写这里 -->
    <style> 
    .wrap {
        transform-style: preserve-3d;
        width: 300px;  height: 300px;
        position: relative;  /* 定位起点元素 */
        border-top:solid 1px gray;  /* x 轴 */
        border-left:solid 1px gray;  /* y 轴 */
        /* 倾斜一点方能见立体效果 */
        transform: rotateX(-30deg) rotateY(-30deg);
    }

    /* z 轴正方向 */
    .zaxis_p {
        position:absolute;
        width : 300px;
        height:1px; 
        border-top:solid 1px gray;
        /* xy面上,90度立起来就是 z */
        transform: rotateY(-90deg);
        /* 立起来的旋转点 */
        transform-origin:0 0 0;
    }

    /* z 轴负方向 */
    .zaxis_n {
        position:absolute;
        width : 300px;
        height:1px; 
        border-top:dashed 1px gray; /*(虚线)*/
        transform: rotateY(90deg);
        transform-origin:0 0 0;
    }

    .block {
        position: absolute;
        margin: 0 auto; 
        border:solid 2px black;
        border-radius:3px;
        /* 宽高包含边框 */
        box-sizing:border-box;
        transform-origin:0 0 0;
    }

    .cube {
        position: absolute;
        /* 子元素版面是需要三维空间的 */
        transform-style: preserve-3d;  
    }

    .magicBox {
        position: absolute;
        transform-style: preserve-3d; 
    }

    </style>

<script>
/** 版面 block 类
 * direct 方向
 * color  颜色
 * size   边长大小
**/
function Block(direct, color, size){
    this.direct = direct;
    this.color = color;
    this.size = size;
    // 绘制过的 UI 元素对象
    this.Element = null;

    // 在父容器中绘制
    this.DrawIn = function(cubeElement){
        var e = this.Element || document.createElement('div');
        e.style.width = this.size + "px";
        e.style.height = this.size + "px";

        var top = (this.direct == 'down' ? this.size : 0);
        var left = (this.direct == 'right' ? this.size : 0);

        e.style.top = top + "px";
        e.style.left = left + "px";
        e.style.backgroundColor = this.color;

        var rx = (this.direct == 'up' || this.direct == 'down' ? -90 : 0);
        var ry = (this.direct == 'left' || this.direct == 'right' ? 90 : 0);;
        var tz = (this.direct == 'back' ? this.size : 0);

        e.style["transform"] = "rotateX(" + rx + "deg) rotateY(" + ry + "deg) translateZ(-" + tz + "px)";
        e.className = "block";
        this.Element = e;
        cubeElement.appendChild(e);
    };
}

/** 魔方格 Cube 类
 * blockSize 为魔方格的边长代表大小
 * directColorArray 为指定方向与颜色的数组
 *                  形式为 [direct,color,direct,color,...] 
 * x,y,z 为在魔方中的坐标,未指定时默认为0,0,0
**/
function Cube(blockSize, directColorArray, x, y, z){
    this.x = x | 0;
    this.y = y | 0;
    this.z = z | 0;
    this.blockSize = blockSize;
    this.blocks = [];
    /* 根据参数建立 Block 对象 */
    for(var i=0; i < directColorArray.length / 2; i++){
        this.blocks.push(new Block(directColorArray[ i*2 ], directColorArray[ i*2 + 1 ], this.blockSize));
    }

    // 绘制过的 UI 元素对象
    this.Element = null;

    // 在父容器中绘制
    this.DrawIn = function(boxElement, x, y, z){
        this.x = x | this.x;
        this.y = y | this.y;
        this.z = z | this.z;
        var e = this.Element || document.createElement('div');
        e.style.width = this.blockSize + "px";
        e.style.height = this.blockSize + "px"; 
        e.style["transform"] = this.FormatTransform();
        e.className = "cube"; 

        for(var i=0; i < this.blocks.length; i++) { 
            this.blocks[i].DrawIn(e);
        }

        this.Element = e;

        boxElement.appendChild(e);
    };

    this.Rotate = function(axis, turn, dimension){
        if(!this.Element) return;
        // 重绘魔方格
        this.ReDrawBlocks(axis, turn);
        // 转换坐标
        this.TransCoordinate(axis, turn, dimension);

        // 先停止动画效果,逆向 90 度,此时外观跟旋转前一致
        this.Element.style["transition"] = "";
        var rotateDegs = new Object();
        rotateDegs[axis] = (turn == 'left' ? -90 : 90); 
        this.Element.style["transform"] = this.FormatTransform(rotateDegs);
        // 旋转原点旋转的层都需要以魔方的中心点旋转
        // 旋转原点是以元素自身来计算的,因所有魔方格都是从(0,0,0)平衡的,因此计算结果都一样
        var centerX = this.blockSize * dimension / 2;
        var centerY = this.blockSize * dimension / 2;
        var centerZ = -this.blockSize * dimension / 2;
        this.Element.style["transformOrigin"] = centerX + "px " + centerY + "px " + centerZ + "px";

        // 这样才能触发动画
        setTimeout(function(obj){
            return function(){
                obj.Element.style["transform"] = obj.FormatTransform();
                obj.Element.style["transition"] = "transform 0.5s";  // 0.3 秒
            };
        }(this), 1);
    }

    /** 坐标转换
     * axis 轴向
     * turn 转向
     * dimension 阶数
     **/
    this.TransCoordinate = function(axis, turn, dimension){
        if(axis == 'x'){             
            if( turn == 'left' ){
                var oriy = this.y;
                this.y = this.z;
                this.z = dimension - 1 - oriy;
            } else {
                var oriz = this.z;
                this.z = this.y;
                this.y = dimension - 1 - oriz;
            }
        } else if(axis == 'y'){
            if( turn == 'right' ){
                var orix = this.x;
                this.x = this.z;
                this.z = dimension - 1 - orix;
            } else {
                var oriz = this.z;
                this.z = this.x;
                this.x = dimension - 1 - oriz;
            }
        } else if(axis == 'z'){
            if( turn == 'right' ){
                var orix = this.x;
                this.x = this.y;
                this.y = dimension - 1 - orix;
            } else {
                var oriy = this.y;
                this.y = this.x;
                this.x = dimension - 1 - oriy;
            }
        }
    }

   /** 将各 block 调整位置,重绘魔方格
    * axis 轴向
    * turn 转向
    **/
    this.ReDrawBlocks = function(axis, turn){
        var xyzDirects = [];
        xyzDirects['x'] = ["front", "up", "back", "down"];
        xyzDirects['y'] = ["front", "right", "back", "left"];
        xyzDirects['z'] = ["up", "right", "down", "left"];
        var curDirects = xyzDirects[axis];

        for(var i=0; i < this.blocks.length; i++) {
            var index = curDirects.indexOf( this.blocks[i].direct );
            if(index > -1){
                var newIndex = turn == 'left' ? (index + 1) % 4 : (index + 4 - 1) % 4;
                this.blocks[i].direct = curDirects[newIndex];
                this.blocks[i].DrawIn(this.Element);
            }   
        }
    }


    // 格式仳 transform 属性
    // css3 把旋转与平移混一起(真不好用)
    this.FormatTransform = function (rotateDegs){
        var rotatePart = "rotateX(0deg) rotateY(0deg) rotateZ(0deg)";
        if(rotateDegs){
            rotatePart = "rotateX(" + (rotateDegs.x | 0) + "deg) rotateY(" + (rotateDegs.y | 0) + "deg) rotateZ(" + (rotateDegs.z | 0) + "deg)";
        }  

        return rotatePart + " translate3d(" + (this.x * this.blockSize) + "px," + (this.y * this.blockSize) + "px,-" + (this.z * this.blockSize) + "px) ";     
    }
}

/** 魔方 MagicBox 类
 * dimension 阶数
 * blockSize 每小格大小
 **/
function MagicBox(dimension, blockSize){
    this.dimension = dimension;
    this.blockSize = blockSize;
    this.cubes = [];

    this.MakeDefaultCubes = function(){
        this.cubes = [];
        for(var x=0; x < this.dimension; x++){
            for(var y=0; y < this.dimension; y++){
                for(var z=0; z < this.dimension; z++){
                    var cube = this.MakeDefaultCube(x, y, z);
                    if(cube){
                      this.cubes.push(cube);
                    }
                }
            }
        }
    };

    /* 根据魔方格在阶数中的位置生成魔方格,魔方内部的格子忽略 */
    this.MakeDefaultCube = function(x, y, z){
        var max = this.dimension - 1;
        var dc = [];
        if(x == 0) dc.push("left", "orange"); else if(x == max) dc.push("right", "red");
        if(y == 0) dc.push("up", "yellow"); else if(y == max) dc.push("down", "white");           
        if(z == 0) dc.push("front", "blue"); else if(z == max) dc.push("back", "green");
        if(dc.length == 0) return null;
        var cube = new Cube(this.blockSize, dc, x, y, z);
        return cube;
    }

    // 构造时自动产生初始魔方格
    this.MakeDefaultCubes();
    // 绘制过的 UI 元素对象
    this.Element = null;
    // 在父容器中绘制
    this.DrawIn = function(domElement){
        var e = this.Element || document.createElement('div');
        e.style.width = (this.dimension * this.blockSize) + "px";
        e.style.height = (this.dimension * this.blockSize) + "px"; 
        e.className = "magicBox"; 

        for(var i=0; i < this.cubes.length; i++) {
            this.cubes[i].DrawIn(e);
        }
        this.Element = e;
        domElement.appendChild(e);
    };

    /** MagicBox.Rotate 旋转
      * axis 轴向
      * level 层
      * turn 转向
     **/
    this.Rotate = function(axis, level, turn){
        for(var i=0; i < this.cubes.length; i++) {
            if(this.cubes[i][axis] == level) {    // 该轴该层的才旋转
                this.cubes[i].Rotate(axis, turn, this.dimension);
            }
        }
    };
}

function onload(){

    //* 魔方绘制示例
    var magicBox = new MagicBox(3, 50);
    magicBox.DrawIn( document.querySelector(".wrap") );

    var rotates = GenRotateActions(3, 5);
    for(var i=0; i<rotates.length; i++){   
        setTimeout(function(magicBox, rotate){
            return function(){
                magicBox.Rotate(rotate.axis, rotate.level, rotate.turn);
            };
        }(magicBox, rotates[i]), 3000 + 800 * i);
    }

    for(var i=0; i<rotates.length; i++){   
        setTimeout(function(magicBox, rotate){
            return function(){
                magicBox.Rotate(rotate.axis, rotate.level, (rotate.turn == 'left' ? 'right' : 'left'));
            };
        }(magicBox, rotates[rotates.length -1 - i]), 1000 + 8800 + 800 * i);
    }
}

/** 产生一个指定数量的旋转序列数组
 * dimension 魔方阶数
 * count 序列数量
 **/
function GenRotateActions(dimension, count){
    var result = [];
    for(var i=0; i<count; i++){
        result[i] = {
            axis  : ['x','y','z'][Math.floor(Math.random() * 3)],
            level : Math.floor(Math.random() * dimension),
            turn  : ['left','right'][Math.floor(Math.random() * 2)]
        };
    }
    return result;
}
</script>
</head>

<body style="padding:300px;" onload="onload()">
  <div class="wrap">
    <div class="zaxis_p"></div>
    <div class="zaxis_n"></div> 
  </div>
</body>

</html>
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title></title>
<style type="text/css">
.eyes{
perspective: 1000px;
}
.box{
/*设置3D效果*/
transform-style: preserve-3d;
/*盒子的大小*/
width: 200px;
height: 200px;
background: red;
/*设置盒子的位置,便于观察*/
margin: 400px auto;
/*复合方式设置动画 三者分别为:动画名 执行一次时间 执行方式*/
animation: zhuan 3s ease;
/*令动画无限执行下去*/
animation-iteration-count: infinite;
animation-timing-function: linear;
}
.box div{
width: 200px;
height: 200px;
opacity: 1;
/*设置过渡*/
transition: all 1s ease 0s;
position: absolute;
}
/*调整位置,制作成一个六边形*/
.box .div1{
background: green;
transform: translateZ(100px);
}
.box .div2{
background: green;
transform: translateZ(-100px);
}
.box .div3{
background: green;
transform: rotateX(90deg) translateZ(100px);
}
.box .div4{
background: green;
transform: rotateX(270deg) translateZ(100px);
}
.box .div5{
background: green;
transform: rotateY(-90deg) translateZ(100px);
}
.box .div6{
background: green;
transform: rotateY(90deg) translateZ(100px);
}
/*添加3D旋转效果 让其绕X、Y轴同时旋转90度*/
@keyframes zhuan{
from{
transform: rotateX(0deg) rotateZ(0deg) rotateY(0deg);
}
to{
transform: rotateX(360deg) rotateZ(360deg) rotateY(360deg);
}
}
/*给正方体添加一个hover效果*/
.box:hover .div1{
transform: translateZ(200px);
}
.box:hover .div2{
transform: translateZ(-200px);
}
.box:hover .div3{
transform: rotateX(90deg) translateZ(200px);
}
.box:hover .div4{
transform: rotateX(270deg) translateZ(200px);
}
.box:hover .div5{
transform: rotateY(-90deg) translateZ(200px);
}
.box:hover .div6{
transform: rotateY(90deg) translateZ(200px)
}
</style>
</head>
<body>
<div class="eyes">
<div class="box">
<div class="div1"><img src="images/1.jpg" width="100%" height="100%"></div>
<div class="div2"><img src="images/2.jpg" width="100%" height="100%"></div>
<div class="div3"><img src="images/3.jpg" width="100%" height="100%"></div>
<div class="div4"><img src="images/4.jpg" width="100%" height="100%"></div>
<div class="div5"><img src="images/5.jpg" width="100%" height="100%" ></div>
<div class="div6"><img src="images/6.jpg" width="100%" height="100%" ></div>
</div>
</div>
</body>
</html>