SegmentFault chenhao.ch最新的文章
2017-02-15T14:51:39+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
HTTP请求中的Keep-Alive模式详解
https://segmentfault.com/a/1190000008359278
2017-02-15T14:51:39+08:00
2017-02-15T14:51:39+08:00
chenhao_ch
https://segmentfault.com/u/chenhao_ch
2
<h2>介绍</h2>
<p>最近遇到一个诡异问题,使用Fiddler替换某一个接口请求时,页面请求该接口后,请求一直没有结束的问题。经过定位,确定是HTTP中某些header字段导致的。本篇就是关于这几个字段的介绍。</p>
<h2>Keep-Alive模式</h2>
<p>我们都知道HTTP是基于TCP的,每一个HTTP请求都需要进行三步握手。如果一个页面对某一个域名有多个请求,就会进行频繁的建立连接和断开连接。所以HTTP 1.0中出现了<code>Connection: keep-alive</code>,用于建立长连接,即我们所说的Keep-Alive模式。下图是普通模式和长连接模式的请求对比: <br><img src="/img/bVJeM5?w=450&h=280" alt="请求对比" title="请求对比"></p>
<blockquote><p>HTTP/1.0中默认使用Connection: close。在HTTP/1.1中已经默认使用Connection: keep-alive。</p></blockquote>
<p>通过对比可以看出,Keep-Alive模式更加高效,因为避免了连接建立和释放的开销。但是,如果一个连接是不会断开的,那么多个请求之间如何进行区分呢?也就是说浏览器是如何知道当前请求已经完成了呢?为了解决这个问题,HTTP对header中又添加了一个<code>Content-Length</code>字段。</p>
<h2>Content-Length</h2>
<p><code>Content-Length</code>表示实体内容的长度。浏览器通过这个字段来判断当前请求的数据是否已经全部接收。 <br>所以,当浏览器请求的是一个静态资源时,即服务器能明确知道返回内容的长度时,可以设置<code>Content-Length</code>来控制请求的结束。但当服务器并不知道请求结果的长度时,如一个动态的页面或者数据,<code>Content-Length</code>就无法解决上面的问题,这个时候就需要用到<code>Transfer-Encoding</code>字段。</p>
<h2>Transfer-Encoding</h2>
<p><code>Transfer-Encoding</code>是指传输编码,还有一个类似的字段叫做:<code>Content-Encoding</code>。两者的区别是<code>Content-Encoding</code>用于对实体内容的压缩编码,比如<code>Content-Encoding: gzip</code>;<code>Transfer-Encoding</code>则改变了报文的格式,比如上面的问题中,当服务端无法知道实体内容的长度时,就可以通过指定<code>Transfer-Encoding: chunked</code>来告知浏览器当前的编码是将数据分成一块一块传递的。当然, 还可以指定<code>Transfer-Encoding: gzip, chunked</code>表明实体内容不仅是gzip压缩的,还是分块传递的。最后,当浏览器接收到一个长度为0的chunked时, 知道当前请求内容已全部接收。</p>
<h2>总结</h2>
<p>好了,关于Keep-Alive模式的内容讲完了。回到最开始说的遇到的问题。当Fiddler替换的结果中,header指定的<code>Transfer-Encoding: chunked</code>或者<code>Connection: keep-alive</code>或者<code>Content-Length</code>的值和正文长度不符时,浏览器是不能正确解析替换后的结果的。解决方法是:将替换结果header中的<code>Transfer-Encoding</code>, <code>Content-Length</code>都删除,并且设置<code>Connection: close</code>来关闭请求的长连接模式,这样,Fiddler替换后的结果就是一个普通的连接请求结果,可以正常被浏览器解析。</p>
扩展开发过程中的自动更新实现
https://segmentfault.com/a/1190000008126003
2017-01-15T14:17:35+08:00
2017-01-15T14:17:35+08:00
chenhao_ch
https://segmentfault.com/u/chenhao_ch
7
<h2>介绍</h2>
<p>最近业务上需要开发扩展来实现某些功能。在开发过程中,遇到每次修改完代码,都需要手动点击<code>chrome://extensions</code>页面的<code>Reload</code>,才能更新扩展的问题,十分影响开发体验。于是花了点时间,把开发扩展的构建过程的<code>hot reload</code>搞定了。 <br>具体代码见:<a href="https://link.segmentfault.com/?enc=wxD0AisdXh%2FZpaJJO5PhfQ%3D%3D.%2BaJWlvNk3vn0WjTCTK7INBPr0MN5I59oizNqY5QoXq9vOBKV366a9d7doBDbYmVvSnYXl1fwg%2Fz3mbywhZONeA%3D%3D" rel="nofollow">https://github.com/chenhao-ch...</a></p>
<h2>原理/思路/过程</h2>
<p>根据自己的习惯,本次还是选用<code>gulp + webpack</code>来构建,界面部分使用<code>Vue.js</code>作为技术栈。</p>
<h3>构建结果输出到硬盘</h3>
<p>根据页面开发的习惯,搭建好构建逻辑后,就遇到了第一个问题:</p>
<blockquote><p>扩展调试需要一个本地目录,而webpack启用dev-server后,构建结果是输出到内存中的。</p></blockquote>
<p>经过一段时间的调查,发现<code>webpack-dev-server</code>并没有提供构建到硬盘的功能!!!也就是说,我们要输出到硬盘,只能我们自己写逻辑来实现了。 </p>
<p>当然我们也可以不启动<code>webpack-dev-server</code>。当时热加载的实现是需要用到<code>socket</code>的,这个在<code>webpack-dev-server</code>中已经封装好了。为了修改的尽量少,建议还是使用<code>webpack-dev-server</code>的好。 </p>
<p>为了找到解决方法,在网上找了很久,试了一堆方法,都不是很理想。最后找到了一种相对简单的方法来解决,就是利用<code>webpack plugin</code>的运行时生命周期来解决。简单点说,就是当<code>webpack</code>的构建结束(包括增量构建)时,会触发一个<code>emit</code>事件,在<code>emit</code>中我们可以将构建结果拿到,然后通过<code>fs</code>模块输出到硬盘上。代码如下:</p>
<pre><code class="Javascript">// gulp.js
// 构建过程
gulp.task('webpack-build-dev', ['clean'], function() {
process.env.NODE_ENV = 'development';
var port = 3007;
// 对每一个入口都添加dev server。
for (var e in webpackDevConfig.entry) {
webpackDevConfig.entry[e].push(`webpack-dev-server/client?http://localhost:${port}`, 'webpack/hot/dev-server');
}
// 根据dev配置开始构建
var compiler = webpack(webpackDevConfig);
// 在构建结束时,运行emit事件
compiler.plugin('emit', (compilation, callback) => {
// 每次构建结束,都会触发该方法。
const assets = compilation.assets;
let file, data, fileDir;
Object.keys(assets).forEach(key => {
file = path.resolve(__dirname, './build/' + key);
fileDir = path.dirname(file);
if (!fs.existsSync(fileDir)) {
fs.mkdirSync(fileDir);
}
data = assets[key].source();
fs.writeFileSync(file, data); // 将构建结果同步的写到硬盘中
});
callback();
});
// 启动服务器
var server = new devServer(compiler, {});
server.listen(port, '0.0.0.0', function() {});
});</code></pre>
<p>可以看出,我们主要通过了<code>compiler.plugin('emit',() => {})</code> 这段代码来实现编译结果输出到硬盘,关于<code>webpack</code>的<code>emit</code>详见<a href="https://link.segmentfault.com/?enc=VnZQNBwGBFDqDdBS0VgKnQ%3D%3D.iX%2Ffdmt208MG186ZMlVOrIyYvFPHB3sJQIbljo9LOLlpUFSEEoPO3uARG1nADdOZgRLiA%2BECHJrX0X0%2BDcs9CQ%3D%3D" rel="nofollow">https://webpack.github.io/doc...</a>,这里不详细解释。</p>
<h3>自动更新扩展</h3>
<p>构建结果可以输出到硬盘后,就可以开始调试了。这个时候又遇到第二个问题:</p>
<blockquote><p>修改代码后,会触发构建,但是Chrome中的扩展并没有自动更新</p></blockquote>
<p>这个问题花了很多时间,最后把<code>webpack</code>的<code>hotModuleReplaceMentPlugin</code>插件的原理搞明白后,才搞定的。 </p>
<p>我们都知道,要是<code>webpack</code>的<code>hot module replace</code>,需要引入<code>hotModuleReplaceMentPlugin</code>,并且启动<code>webpack-dev-server</code>。那么为什么要这样做呢?我画一个图简单说明下<code>webpack hot module replace</code>的原理。</p>
<p><img src="/img/remote/1460000008126006?w=703&h=363" alt="webpackDevServer流程" title="webpackDevServer流程"></p>
<p>这里说下整个流程:当启动<code>webpack</code>构建时,会对每一个入口都注入<code>webpackDevServer</code>的部分代码,我这里就叫<code>webpackDevServer(client)</code>好了。 这个代码中有一个<code>socket</code>,运行后会和本地服务器的<code>socket</code>接口进行链接。当本地服务器关闭时,在页面的<code>DevTools</code>中我们会看到页面有不断再尝试链接<code>sockjs-node/info</code>就是一个<code>socket</code>链接。</p>
<p>然后我们修改代码,<code>webpack</code>中会自动进行构建,然后通知到<code>webpackDevServer</code>,并通过<code>socket</code>通知到<code>webpackDevServer(client)</code>。然后,<code>webpackDevServer(client)</code>就会通过<code>postMessage</code>通知到页面。让<code>hotModule</code>进行去更新。这里的更新就有部分模块更新的逻辑了,这里不细讲。</p>
<p>回到我们的问题上,我们要实现代码修改后,自动更新扩展,涉及两步:自动触发构建 & 构建结束后,扩展自动更新。可以看出,第一步不需要做任何操作就可以实现。那么第二步,我们可以利用<code>webpackDevServer</code>过程中的<code>postMessage</code>。 </p>
<p>我的做法时,在<code>background</code>中多引入一个<code>reload.js</code>。 代码如下:</p>
<pre><code class="Javascript">// reload.js
// 实现webpackHotUpdate消息的监听
window.addEventListener('message', (e) => {
if (typeof event.data === 'string' && event.data.indexOf('webpackHotUpdate') === 0) {
// 当监听到webpackHotUpdate事件时,扩展重新安装
chrome.runtime.reload();
}
});</code></pre>
<p>其中<code>chrome.runtime.reload();</code>就是Chrome官方提供的更新扩展方法,会自动更新整个扩展,包括<code>background</code>和<code>contentscript</code>。</p>
<p>然后在构建过程中把<code>reload.js</code>引入到<code>background</code>中。和业务逻辑进行隔离。</p>
<pre><code class="Javascript">// gulpfile.js
// 迁移dev阶段的reload.js文件,以实现自动更新
gulp.task('move-dev', ['clean'], function () {
// 迁移自动刷新扩展功能代码
gulp.src(path.resolve(__dirname, './config/reload.js'))
.pipe(gulp.dest(buildPath));
var manifest = require('./src/manifest.json');
manifest.background.scripts.push('./reload.js');
fs.writeFileSync(path.resolve(__dirname, './build/manifest.json'), JSON.stringify(manifest, null, 2));
});</code></pre>
<p>这样子,热加载的过程就变成下图这样:<br><img src="/img/remote/1460000008126007?w=676&h=363" alt="extensions reload" title="extensions reload"></p>
<h3>多contentScript问题</h3>
<p>解决了上面的两个问题,其实已经解决了扩展的构建,调试,热加载问题。但是,一个扩展是可以有多个<code>content script</code>的,还需要在构建上做支持。我通过下面这种方法来解决。<br>将每一个<code>contentscript</code>作为一个业务,并约定一下的目录结构:</p>
<pre><code>│ background.js
│ manifest.json
├─biz
│ └─count
│ background.js
│ contentscript.js
│ contentscript.vue
├─common
│ log.js
│ message.js
│ onMessage.js
└─_locales</code></pre>
<p>其中<code>biz</code>中的子目录都是一个业务,比如<code>count</code>就是一个业务。如果业务目录中存在<code>contentscript.js</code>,就会在构建时作为一个入口,构建出一个独立的<code>[业务].js</code>作为注入代码。而<code>background.js</code>可以通过<code>import</code>把每一个业务的<code>background.js</code>都引入。如此这般,构建结果目录结构就是:</p>
<pre><code>│ background.js
│ count.js
│ manifest.json
├─sourcemap
│ background.js.map
│ count.js.map
└─_locales</code></pre>
<p>然后还实现了<code>message.js</code>和<code>onMessage.js</code>用于解决<code>background</code>只能注册<code>message</code>监听一次的问题。统一不同业务的<code>message</code>通信。 </p>
<p>最后放上这个部分的构建代码:</p>
<pre><code class="Javascript">// webpack.dev.config.js
module.exports = {
entry: {
background: [
// 默认只有background.js一个entry,contentScript入口有构建运行时,根据biz目录确定
path.resolve(__dirname, '../src/background.js')
]
},
...
plugins: [
new webpack.HotModuleReplacementPlugin() // 启用热加载
],
devtool: '#source-map', // sourcemap方便调试
watch: true // watch 文件变化
};
// gulp.js
var webpackDevConfig = require('./config/webpack.dev.config.js');
// 根据biz目录下的文件夹名字,生成对应的contentscript entry
gulp.task('createEntry', function() {
var bizDir = path.resolve(__dirname, './src/biz/');
var allBiz = fs.readdirSync(bizDir);
var entrys = {};
var entryName = [];
// 根据biz目录下的文件夹名字,生成对应的contentscript entry
allBiz.forEach(function(b) {
var bp = path.resolve(bizDir, b);
if (fs.statSync(bp).isDirectory()) {
if (fs.statSync(path.resolve(bp, 'contentscript.js')).isFile()) {
entryName.push(b);
entrys[b] = [path.resolve(bp, 'contentscript.js')]; // 添加业务的contetscript.js为entry
}
}
});
console.log(`${getTime()} 添加入口: ${entryName}`);
entrys['background'] = webpackDevConfig.entry.background;
webpackDevConfig.entry = entrys; // 更新entry
});
</code></pre>
<h2>总结</h2>
<p><code>webpack</code>用很长时间,一直觉得掌握的不够,经过这一次的研究,不仅搞定了扩展的自动更新,而且因为解决构建问题所绕过的弯路,把<code>webpack</code>参见的功能也基本摸清了,收获颇多。都说在解决问题中成长才是最好的成长,的确是这样。</p>
EditorConfig使用介绍——解决markdown文件行尾空格自动删除的问题
https://segmentfault.com/a/1190000007599845
2016-11-25T14:33:35+08:00
2016-11-25T14:33:35+08:00
chenhao_ch
https://segmentfault.com/u/chenhao_ch
16
<h2>前言</h2>
<p>使用VSCode作为IDE开发工具已经有一段时间,期间一直有一个很困扰我的问题,就是关于行尾空格的自动删除。 <br>一般情况下,都需要对源码中的行尾的多余空格进行删除,所以我有设置自动删除行尾空格。但是当我编辑markdown文件时,行尾空格也会被删除。 <br>WTF!! <br>markdown文件不是通过行尾三个空格来实现换行的吗,为什么编辑器要自动去除它空格。网上查了很久也没找到能解决这个问题的方法。 <br>直到最近,看到一个叫<code>.editorconfig</code>文件,一下子就想到了可以使用这个来解决。下面就介绍下<code>.editorconfig</code>。</p>
<h2>EditorConfig是什么</h2>
<p>顾名思义,EditorConfig就是编辑器配置,就是指统一不同编辑器的代码风格的配置。举个例子:比如我们要控制一个多人维护的项目缩进统一用2个空格。那么每个人的IDE都是不同的,一种方法是几个人约定好各自配置自己的IDE,但是如果每个人又维护着多个项目,几个项目间就会相互影响。所以更好的办法是由项目本身来控制代码风格。也就是使用EditorConfig来控制。 </p>
<p>EditorConfig包含一个用于定义代码格式的文件和一批编辑器插件,这些插件是让编辑器读取配置文件并以此来格式化代码。 <br>EditorConfig的官网: <a href="https://link.segmentfault.com/?enc=ZmXHlMod077A17lVmoR5Tw%3D%3D.MXXZyoQvqZpm%2BDCoL%2FjJLmikdniuVn%2F5pdFAzZQPa7c%3D" rel="nofollow">http://editorconfig.org/</a></p>
<h2>EditorConfig的配置和使用</h2>
<p>先来看下EditorConfig长什么样子:</p>
<pre><code class="ini"># EditorConfig is awesome: http://EditorConfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
# Matches multiple files with brace expansion notation
# Set default charset
[*.{js,py}]
charset = utf-8
# 4 space indentation
[*.py]
indent_style = space
indent_size = 4
# Tab indentation (no size specified)
[Makefile]
indent_style = tab
# Indentation override for all JS under lib directory
[lib/**.js]
indent_style = space
indent_size = 2
# Matches the exact files either package.json or .travis.yml
[{package.json,.travis.yml}]
indent_style = space
indent_size = 2</code></pre>
<p>上面的代码就是EditorConfig的配置demo。 <br>当用IDE打开一个文件时,EditorConfig插件会在打开文件的目录和其每一级父节点查找<code>.editorconfig</code>文件,直到找到一个配置了<code>root = true</code>的配置文件。</p>
<h3>文件格式详情</h3>
<p>EditorConfig文件使用INI格式。斜杠(<code>/</code>)作为路径分隔符,<code>#</code>或者<code>;</code>作为注释。路径支持通配符:</p>
<table>
<thead><tr>
<th align="left">通配符</th>
<th align="left">说明</th>
</tr></thead>
<tbody>
<tr>
<td align="left">*</td>
<td align="left">匹配除/之外的任意字符</td>
</tr>
<tr>
<td align="left">**</td>
<td align="left">匹配任意字符串</td>
</tr>
<tr>
<td align="left">?</td>
<td align="left">匹配任意单个字符</td>
</tr>
<tr>
<td align="left">[name]</td>
<td align="left">匹配name字符</td>
</tr>
<tr>
<td align="left">[!name]</td>
<td align="left">不匹配name字符</td>
</tr>
<tr>
<td align="left">[s1,s2,s3]</td>
<td align="left">匹配给定的字符串</td>
</tr>
<tr>
<td align="left">[num1..num2]</td>
<td align="left">匹配num1到mun2直接的整数</td>
</tr>
</tbody>
</table>
<p>EditorConfig支持以下属性:</p>
<table>
<thead><tr>
<th align="left">属性</th>
<th align="left">说明</th>
</tr></thead>
<tbody>
<tr>
<td align="left">indent_style</td>
<td align="left">缩进使用<code>tab</code>或者<code>space</code>
</td>
</tr>
<tr>
<td align="left">indent_size</td>
<td align="left">缩进为<code>space</code>时,缩进的字符数</td>
</tr>
<tr>
<td align="left">tab_width</td>
<td align="left">缩进为<code>tab</code>时,缩进的宽度</td>
</tr>
<tr>
<td align="left">end_of_line</td>
<td align="left">换行符的类型。<code>lf</code>, <code>cr</code>, <code>crlf</code>三种</td>
</tr>
<tr>
<td align="left">charset</td>
<td align="left">文件的charset。有以下几种类型:<code>latin1</code>, <code>utf-8</code>, <code>utf-8-bom</code>, <code>utf-16be</code>, <code>utf-16le</code>
</td>
</tr>
<tr>
<td align="left">trim_trailing_whitespace</td>
<td align="left">是否将行尾空格自动删除</td>
</tr>
<tr>
<td align="left">insert_final_newline</td>
<td align="left">是否使文件以一个空白行结尾</td>
</tr>
<tr>
<td align="left">root</td>
<td align="left">表明是最顶层的配置文件,发现设为true时,才会停止查找.editorconfig文件</td>
</tr>
</tbody>
</table>
<h2>总结</h2>
<p>最后,回到上面遇到的问题,怎么解决呢? <br>在项目根目录添加文件<code>.editorconfig</code>,内容如下:</p>
<pre><code class="ini"># http://editorconfig.org
root = true
[*]
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
</code></pre>
<p>这样就解决了我的问题。当然,每个项目都要添加。。。。。。</p>
Vue 2.0源码学习
https://segmentfault.com/a/1190000007484936
2016-11-15T10:54:34+08:00
2016-11-15T10:54:34+08:00
chenhao_ch
https://segmentfault.com/u/chenhao_ch
48
<h2>Vue2.0介绍</h2>
<p>从去年9月份了解到Vue后,就被他简洁的API所吸引。1.0版本正式发布后,就在业务中开始使用,将原先jQuery的功能逐步的进行迁移。 <br>今年的10月1日,Vue的2.0版本正式发布了,其中核心代码都进行了重写,于是就专门花时间,对Vue 2.0的源码进行了学习。本篇文章就是2.0源码学习的总结。 </p>
<p>先对Vue 2.0的新特性做一个简单的介绍:</p>
<ul>
<li><p><strong>大小 & 性能</strong>。Vue 2.0的线上包gzip后只有12Kb,而1.0需要22Kb,react需要44Kb。而且,Vue 2.0的性能在react等几个框架中,性能是最快的。</p></li>
<li><p><strong>VDOM</strong>。实现了Virtual DOM, 并且将静态子树进行了提取,减少界面重绘时的对比。与1.0对比性能有明显提升。</p></li>
<li><p><strong>template & JSX</strong>。众所周知,Vue 1.0使用的是template来实现模板,而React使用了JSX实现模板。关于template和JSX的争论也很多,很多人不使用React就是因为没有支持template写法。Vue 2.0对template和JSX写法都做了支持。使用时,可以根据具体业务细节进行选择,可以很好的发挥两者的优势。就这一点,Vue已经超过React了。</p></li>
<li><p><strong>Server Render</strong>。2.0还对了Server Render做了支持。这一点并没有在业务中使用,不做评价。</p></li>
</ul>
<p>Vue的最新源码可以去 <a href="https://link.segmentfault.com/?enc=0xZj%2FsiU537mucijFYQ0kQ%3D%3D.2TJtcw0%2Fd%2FOqVuyNrS4oj3rhzPS6E9TlT0XRPrUyjAY%3D" rel="nofollow">https://github.com/vuejs/vue</a> 获得。本文讲的是 2.0.3版本,2.0.3可以去 <a href="https://link.segmentfault.com/?enc=rUpduQGNWtl7zX50R1o6zA%3D%3D.i5CeHPJ%2F%2Bs4MZR%2FvYPq28J1QhJfWxyNPY%2FCCfp9hFi%2BjyVYWsGAmat7Pxi%2FEAsVX" rel="nofollow">https://github.com/vuejs/vue/...</a> 这里获得。 </p>
<p>下面开始进入正题。首先从生命周期开始。</p>
<h2>生命周期</h2>
<p><img src="/img/bVEs9r" alt="图片描述" title="图片描述"></p>
<p>上图就是官方给出的Vue 2.0的生命周期图,其中包含了Vue对象生命周期过程中的几个核心步骤。了解了这几个过程,可以很好的帮助我们理解Vue的创建与销毁过程。<br>从图中我们可以看出,生命周期主要分为4个过程:</p>
<ul>
<li><p><strong>create</strong>。<code>new Vue</code>时,会先进行create,创建出Vue对象。</p></li>
<li><p><strong>mount</strong>。根据el, template, render方法等属性,会生成DOM,并添加到对应位置。</p></li>
<li><p><strong>update</strong>。当数据发生变化后,会重新渲染DOM,并进行替换。</p></li>
<li><p><strong>destory</strong>。销毁时运行。</p></li>
</ul>
<p>那么这4个过程在源码中是怎么实现的呢?我们从<code>new Vue</code>开始。</p>
<h2>new Vue</h2>
<p>为了更好的理解new的过程,我整理了一个序列图: </p>
<p><img src="http://img.alicdn.com/tps/TB1KBdBOXXXXXcFXFXXXXXXXXXX-559-668.jpg" alt="new Vue序列图" title="new Vue序列图"></p>
<p>new Vue的过程主要涉及到三个对象:vm、compiler、watcher。其中,vm表示Vue的具体对象;compiler负责将template解析为AST render方法;watcher用于观察数据变化,以实现数据变化后进行re-render。 </p>
<p>下面来分析下具体的过程和代码: <br>首先,运行<code>new Vue()</code>的时候,会进入代码<code>src/core/instance/index.js</code>的Vue构造方法中,并执行<code>this._init()</code>方法。在<code>_init</code>中,会对各个功能进行初始化,并执行<code>beforeCreate</code>和<code>created</code>两个生命周期方法。核心代码如下:</p>
<pre><code class="javascript">initLifecycle(vm)
initEvents(vm)
callHook(vm, 'beforeCreate')
initState(vm)
callHook(vm, 'created')
initRender(vm)</code></pre>
<blockquote><p>这个过程有一点需要注意: <br>beforeCreate和created之间只有initState,和官方给出的生命周期图并不完全一样。这里的initState是用于初始化data,props等的监听的。</p></blockquote>
<p>在<code>_init</code>的最后,会运行<code>initRender</code>方法。在该方法中,会运行<code>vm.$mount</code>方法,代码如下:</p>
<pre><code class="javascript">if (vm.$options.el) {
vm.$mount(vm.$options.el)
}</code></pre>
<blockquote><p>这里的<code>vm.$mount</code>可以在业务代码中调用,这样,new 过程和 mount过程就可以根据业务情况进行分离。</p></blockquote>
<p>这里的<code>$mount</code>在<code>src/entries/web-runtime-with-compiler.js</code>中,主要逻辑是根据el, template, render三个属性来获得AST render方法。代码如下:</p>
<pre><code class="javascript">if (!options.render) { // 如果有render方法,直接运行mount
let template = options.template
if (template) { // 如果有template, 获取template参数对于的HTML作为模板
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) { // 如果没有template, 且存在el,则获取el的outerHTML作为模板
template = getOuterHTML(el)
}
if (template) { // 如果获取到了模板,则将模板转化为render方法
const { render, staticRenderFns } = compileToFunctions(template, {
warn,
shouldDecodeNewlines,
delimiters: options.delimiters
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
}
}
return mount.call(this, el, hydrating)</code></pre>
<blockquote><p>这个过程有三点需要注意: <br>compile时,将最大静态子树提取出来作为单独的AST渲染方法,以提升后面vNode对比时的性能。所以,当存在多个连续的静态标签时,可以在外边添加一个静态父节点,这样,staticRenderFns数目可以减少,从而提升性能。 <br>Vue 2.0中的模板有三种引用写法:el, template, render(JSX)。其中的优先级是render > template > el。 <br>el, template两种写法,最后都会通过compiler转化为render(JSX)来运行,也就是说,直接写成render(JSX)是性能最优的。当然,如果使用了构建工具,最终生成的包就是使用的render(JSX)。这样子,在源码上就可以不用过多考虑这一块的性能了,直接用可维护性最好的方式就行。</p></blockquote>
<p>将模板转化为render,用到了<code>compileToFunctions方法</code>,该方法最后会通过<code>src/compiler/index.js</code>文件中的<code>compile</code>方法,将模板转化为AST语法结构的render方法,并对静态子树进行分离。 </p>
<p>完成render方法的生成后,会进入<code>_mount</code>(src/core/instance.lifecycle.js)中进行DOM更新。该方法的核心逻辑如下:</p>
<pre><code class="javascript">vm._watcher = new Watcher(vm, () => {
vm._update(vm._render(), hydrating)
}, noop)</code></pre>
<p>首先会new一个watcher对象,在watcher对象创建后,会运行传入的方法<code>vm._update(vm._render(), hydrating)</code>(watcher的逻辑在下面的watcher小节中细讲)。其中的<code>vm._render()</code>主要作用就是运行前面compiler生成的render方法,并返回一个vNode对象。这里的vNode就是一个虚拟的DOM节点。</p>
<p>拿到vNode后,传入<code>vm._update()</code>方法,进行DOM更新。</p>
<h2>VDOM</h2>
<p>上面已经讲完了<code>new Vue</code>过程中的主要步骤,其中涉及到template如何转化为DOM的过程,这里单独拿出来讲下。先上序列图:</p>
<p><img src="http://img.alicdn.com/tps/TB1XVxdOXXXXXXsaFXXXXXXXXXX-630-646.jpg" alt="virtual DOM" title="virtual DOM"></p>
<p>从图中可以看出,从template到DOM,有三个过程:</p>
<ul>
<li><p><strong>template -> AST render</strong> (compiler解析template)</p></li>
<li><p><strong>AST render -> vNode</strong> (render方法运行)</p></li>
<li><p><strong>vNode -> DOM</strong> (vdom.patch)</p></li>
</ul>
<p>首先是template在compiler中解析为AST render方法的过程。上一节中有说到,<code>initRender</code>后,会调用到<code>src/entries/web-runtime-with-compiler.js</code>中的<code>Vue.prototype.$mount</code>方法。在<code>$mount</code>中,会获取template,然后调用<code>src/platforms/web/compiler/index.js</code>的<code>compileToFunctions</code>方法。在该方法中,会运行<code>compile</code>将template解析为多个render方法,也就是AST render。这里的<code>compile</code>在文件<code>src/compiler/index.js</code>中,代码如下:</p>
<pre><code class="javascript">const ast = parse(template.trim(), options) // 解析template为AST
optimize(ast, options) // 提取static tree
const code = generate(ast, options) // 生成render 方法
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}</code></pre>
<p>可以看出,<code>compile</code>方法就是将template以AST的方式进行解析,并转化为render方法进行返回。</p>
<p>再看第二个过程:AST render -> vNode。这个过程很简单,就是将AST render方法进行运行,获得返回的vNode对象。 </p>
<p>最后一步,vNode -> DOM。该过程中,存在vNode的对比以及DOM的添加修改操作。 <br>在上一节中,有讲到<code>vm._update()</code>方法中对DOM进行更新。<code>_update</code>的主要代码如下:</p>
<pre><code class="javascript">// src/core/instance/lifecycle.js
if (!prevVnode) {
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
vm.$el = vm.__patch__(vm.$el, vnode, hydrating) // 首次添加
} else {
vm.$el = vm.__patch__(prevVnode, vnode) // 数据变化后触发的DOM更新
}</code></pre>
<p>可以看出,无论是首次添加还是后期的update,都是通过<code>__patch__</code>来更新的。这里的<code>__patch__</code>核心步骤是在<code>src/core/vdom/patch.js</code>中的<code>patch</code>方法进行实现,源码如下:</p>
<pre><code class="javascript">function patch (oldVnode, vnode, hydrating, removeOnly) {
if (!oldVnode) {
...
} else {
...
if (!isRealElement && sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) // diff并更新DOM。
} else {
elm = oldVnode.elm
parent = nodeOps.parentNode(elm)
...
if (parent !== null) {
nodeOps.insertBefore(parent, vnode.elm, nodeOps.nextSibling(elm)) // 添加element到DOM。
removeVnodes(parent, [oldVnode], 0, 0)
}
...
}
}
...
}</code></pre>
<p>首次添加很简单,就是通过insertBefore将转化好的element添加到DOM中。如果是update,则会调动<code>patchVnode()</code>。最后来看下<code>patchVnode</code>的代码:</p>
<pre><code class="javascript">function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
...
const elm = vnode.elm = oldVnode.elm
const oldCh = oldVnode.children
const ch = vnode.children
...
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) { // 当都存在时,更新Children
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) { // 只存在新节点时,即添加节点
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) { // 只存在老节点时,即删除节点
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) { // 删除了textContent
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) { // 修改了textContent
nodeOps.setTextContent(elm, vnode.text)
}
}</code></pre>
<p>其中有调用了<code>updateChildren</code>来更新子节点,代码如下:</p>
<pre><code class="javascript">function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
...
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
...
}
}
...
}</code></pre>
<p>可以看到<code>updateChildren</code>中,又通过<code>patchVnode</code>来更新当前节点。梳理一下,<code>patch</code>通过<code>patchVnode</code>来更新根节点,然后通过<code>updateChildren</code>来更新子节点,具体子节点,又通过<code>patchVnode</code>来更新,通过一个类似于递归的方式逐个节点的完成对比和更新。</p>
<blockquote><p>Vue 2.0中对如何去实现VDOM的思路是否清晰,通过4层结构,很好的实现了可维护性,也为实现server render, weex等功能提供了可能。拿server render举例,只需要将最后的<code>vNode -> DOM</code> 改成 <code>vNode -> String</code> 或者 <code>vNode -> Stream</code>, 就可以实现server render。剩下的compiler和Vue的核心逻辑都不需要改。</p></blockquote>
<h2>Watcher</h2>
<p>我们都知道MVVM框架的特征就是当数据发生变化后,会自动更新对应的DOM节点。使用MVVM之后,业务代码中就可以完全不写DOM操作代码,不仅可以将业务代码聚焦在业务逻辑上,还可以提高业务代码的可维护性和可测试性。那么Vue 2.0中是怎么实现对数据变化的监听的呢?照例,先看序列图:<br><img src="http://img.alicdn.com/tps/TB1oYxwOXXXXXbvXFXXXXXXXXXX-659-737.jpg" alt="watcher" title="watcher"></p>
<p>可以看出,整个Watcher的过程可以分为三个过程。</p>
<ul>
<li><p>对state设置setter/getter</p></li>
<li><p>对vm设置好Watcher,添加好state 触发 setter时的执行方法</p></li>
<li><p>state变化触发执行</p></li>
</ul>
<p>前面有说过,在生命周期函数<code>beforeCreate</code>和<code>created</code>直接,会运行方法<code>initState()</code>。在<code>initState</code>中,会对Props, Data, Computed等属性添加Setter/Getter。拿Data举例,设置setter/getter的代码如下:</p>
<pre><code class="javascript">function initData (vm: Component) {
let data = vm.$options.data
...
// proxy data on instance
const keys = Object.keys(data)
let i = keys.length
while (i--) {
...
proxy(vm, keys[i]) // 设置vm._data为代理
}
// observe data
observe(data)
}</code></pre>
<p>通过调用<code>observe</code>方法,会对data添加好观察者,核心代码为:</p>
<pre><code class="javascript">Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend() // 处理好依赖watcher
...
}
return value
},
set: function reactiveSetter (newVal) {
...
childOb = observe(newVal) // 对新数据重新observe
dep.notify() // 通知到dep进行数据更新
}
})</code></pre>
<p>这个时候,对data的监听已经完成。可以看到,当data发生变化的时候,会运行<code>dep.notify()</code>。在<code>notify</code>方法中,会去运行watcher的<code>update</code>方法,内容如下:</p>
<pre><code class="javascript">update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
run () {
if (this.active) {
const value = this.get()
}
...
}</code></pre>
<p><code>update</code>方法中,queueWatcher方法的目的是通过<code>nextTicker</code>来执行<code>run</code>方法,属于支线逻辑,就不分析了,这里直接看<code>run</code>的实现。<code>run</code>方法其实很简单,就是调用<code>get</code>方法,而<code>get</code>方法会通过执行<code>this.getter()</code>来更新DOM。 </p>
<p>那么<code>this.getter</code>是什么呢?本文最开始分析<code>new Vue</code>过程时,有讲到运行<code>_mount</code>方法时,会运行如下代码:</p>
<pre><code class="javascript">vm._watcher = new Watcher(vm, () => {
vm._update(vm._render(), hydrating)
}, noop)</code></pre>
<p>那么<code>this.getter</code>就是这里Watcher方法的第二个参数。来看下<code>new Watcher</code>的代码:</p>
<pre><code class="javascript">export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: Object = {}
) {
...
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
...
this.value = this.lazy
? undefined
: this.get()
}
}</code></pre>
<p>可以看出,在<code>new Vue</code>过程中,Watcher会在构造完成后主动调用<code>this.get()</code>来触发<code>this.getter()</code>方法的运行,以达到更新DOM节点。</p>
<p>总结一下这个过程:首先<code>_init</code>时,会对Data设置好setter方法,setter方法中会调用<code>dep.notify()</code>,以便数据变化时通知DOM进行更新。然后<code>new Watcher</code>时,会将更新DOM的方法进行设置,也就是<code>Watcher.getter</code>方法。最后,当Data发生变化的时候,<code>dep.notify()</code>运行,运行到<code>watcher.getter()</code>时,就会去运行render和update逻辑,最终达到DOM更新的目的。</p>
<h2>总结与收获</h2>
<p>刚开始觉得看源码,是因为希望能了解下Vue 2.0的实现,看看能不能得到一些从文档中无法知道的细节,用于提升运行效率。把主要流程理清楚后,的确了解到一些,这里做个整理:</p>
<ul>
<li><p>el属性传入的如果不是element,最后会通过<code>document.querySelector</code>来获取的,这个接口性能较差,所以,el传入一个element性能会更好。</p></li>
<li><p><code>$mount</code>方法中对<code>html</code>,<code>body</code>标签做了过滤,这两个不能用来作为渲染的根节点。</p></li>
<li><p>每一个组件都会从<code>_init</code>开始重新运行,所以,当存在一个长列表时,将子节点作为一个组件,性能会较差。</p></li>
<li><p><code>*.vue</code>文件会在构建时转化为<code>render</code>方法,而<code>render</code>方法的性能比指定<code>template</code>更好。所以,源码使用<code>*.vue</code>的方式,性能更好。</p></li>
<li><p>如果需要自定义<code>delimiters</code>,每一个组件都需要单独指定。</p></li>
<li><p>如果是<code>*.vue</code>文件,指定<code>delimiters</code>是失效的,因为<code>vue-loader</code>对<code>*.vue</code>文件进行解析时,并没有将<code>delimiters</code>传递到<code>compiler.compile()</code>中。(这一点不确定是bug还是故意这样设计的)。</p></li>
</ul>
那些年在异步代码上所做的努力
https://segmentfault.com/a/1190000006732882
2016-08-27T09:54:17+08:00
2016-08-27T09:54:17+08:00
chenhao_ch
https://segmentfault.com/u/chenhao_ch
0
<h2>介绍</h2>
<p>写过JS代码的同学应该都知道,JS是单线程的,当出现异步逻辑时,就需要使用一些技巧来实现。最常见的方法就是使用回调方法。</p>
<h2>回调方法</h2>
<p>比如我们要实现一个功能:1s后运行逻辑,再过3s运行另外一段逻辑。使用回调方法可以这样写:</p>
<pre><code class="javascript">// 方法一 ,嵌套回调
// 模拟异步逻辑
function delay(time, callback) {
setTimeout(function() {
callback(time);
}, time);
}
// 过1000ms后输出日志,再过3000ms后输出日志。
delay(1000, function(time1) {
console.log('%s ms后运行', time1);
delay(3000, function(time2) {
console.log('%s ms后运行', time2);
});
});</code></pre>
<p>运行上面的代码,可以得到我们想要的结果:1s后输出了日志,再过3s又输出日志。但是如果逻辑复杂下去,会出现很深的回调方法嵌套问题,使得代码不可维护。为了使异步代码更清晰,就出现了Promise。</p>
<h2>Promise</h2>
<p>还是拿上面的例子来实现:</p>
<pre><code class="javascript">// 方法二 promise
function sleep(time) {
return function() {
return new Promise((resolve) => setTimeout(resolve, time));
};
}
sleep(1000)().then(function() {
console.log('1000ms后运行');
}).then(sleep(3000)).then(function() {
console.log('3000ms后运行');
});</code></pre>
<p>运行代码后,还是可以得到和使用回调方法实现时一样的效果。分析Promise实现的代码,可以发现,它对嵌套回调进行了改进,将原先横向发展的代码,改成了纵向发展,但是并没有解决本质问题。使用Promise的代码,异步逻辑变成了一堆then方法,语义还是不够清晰。那么有没有更好的写法呢? <br>在ES6中,提出了generator方法,可以做到使用同步代码来实现异步功能。下面我们来重点介绍下generator。</p>
<h2>generator</h2>
<h3>generator介绍</h3>
<p>generator是ES6中新提出的函数类型,最大的特点就是函数的执行可以被控制。<br>举一个最简单的generator例子:</p>
<pre><code class="javascript">function *idMarker() {
var i = 1;
while(true) {
yield i++;
}
}
var ids = idMarker();
ids.next().value; // 1
ids.next().value; // 2
ids.next().value; // 3</code></pre>
<p>上面的例子中,当运行了generator函数<code>idMarker()</code>后,函数体并没有开始运行,而是直接返回了一个迭代器<code>ids</code>。当迭代器运行<code>next()</code>后,会返回第一次运行到<code>yield</code>或者<code>return</code>时的返回值以格式<code>{value:1,done:false}</code>进行返回。其中value就是yield后面的值,done表示当前迭代器还没有结束迭代。从上面的代码可以看出,generator函数的运行过程可以在外边被控制,也就是说使用generator可以做到以下功能的实现:</p>
<pre><code class="javascript">运行A逻辑;
通过yield语句,暂停A,并开始异步逻辑B;
等B运行完成,再继续运行A;</code></pre>
<h3>generator实现异步逻辑</h3>
<p>从上面的例子可以看出,使用generator可以解决Promise的问题,代码如下:</p>
<pre><code class="javascript">// 方法三 generator
function sleep(time) {
return new Promise((resolve) => setTimeout(function() {
resolve(time);
}, time));
}
function run(gen) {
return new Promise(function(resolve, reject) {
gen = gen();
onFulfilled();
function onFulfilled(res) {
var ret = gen.next(res);
next(ret);
}
function next(ret) {
if (ret.done) return resolve(ret.value);
ret.value.then(onFulfilled);
}
});
}
function *syncFn() {
var d1 = yield sleep(1000);
console.log('%s ms后运行', d1);
var d2 = yield sleep(3000);
console.log('%s ms后运行', d2);
}
run(syncFn);</code></pre>
<p>下面我们来分析下上面代码的逻辑: <br>首先运行<code>run(syncFn);</code>, 会运行到<code>gen = gen();</code>。这个时候gen变成了generator的迭代器。 <br>通过运行<code>onFulfilled</code>中的<code>gen.next(res)</code>,代码开始运行<code>syncFn</code>中的<code>sleep(1000)</code>。此时,<code>gen.next(res)</code>会返回<code>syncFn</code>中yield获得的值,即<code>sleep</code>方法返回的promise对象,并在<code>next</code>方法中对该promise设置了<code>then(onFulfilled)</code>。 也就是说,当<code>sleep(1000)</code>返回的promise运行结束后,会运行<code>then</code>中的<code>onFulfilled</code>。而<code>onFulfilled</code>会继续运行<code>syncFn</code>的迭代器。这样子,虽然<code>syncFn</code>中的异步逻辑,就会逐步执行了。</p>
<h3>co</h3>
<p>co是一个使用generator和yield来解决异步嵌套的工具库。它的实现类似于上面例子中的run方法。向co传入一个generator方法后,就会开始逐步执行其中的异步逻辑。<br>举个例子,我们需要读取三个文件并将文件内容依次打印出来。使用co的写法就是:</p>
<pre><code class="JavaScript">var fs = require('fs');
var co = require('co');
function readFile(path) {
return function (cb) {
fs.readFile(path, {encoding: 'utf8'}, cb);
};
}
co(function* () {
var dataA = yield readFile('a.js');
console.log(dataA);
var dataB = yield readFile('b.js');
console.log(dataB);
var dataC = yield readFile('c.js');
console.log(dataC);
}).catch(function (err) {
console.log(err);
});</code></pre>
<h3>yield 与 yield*</h3>
<p><code>yield</code>语句还可以使用<code>yield*</code>,两则存在细微的差别。举个例子:</p>
<pre><code class="javascript">// 数组
function* GenFunc() {
yield [1, 2];
yield* [3, 4];
yield "56";
yield* "78";
}
var gen = GenFunc();
console.log(gen.next().value); // [1, 2]
console.log(gen.next().value); // 3
console.log(gen.next().value); // 4
// generator函数
function* Gen1() {
yield 2;
yield 3;
}
function* Gen2() {
yield 1;
yield* Gen1();
yield 4;
}
var g2 = Gen2();
console.log(g2.next().value); // 1
console.log(g2.next().value); // 2
console.log(g2.next().value); // 3
console.log(g2.next().value); // 4
// 对象
function* GenFunc() {
yield {a: '1', b: '2'};
yield* {a: '1', b: '2'};
}
var gen = GenFunc();
console.log(gen.next()); // { value: { a: '1', b: '2' }, done: false }
console.log(gen.next()); // TypeError: undefined is not a function</code></pre>
<p>从上面几个例子可以看出,yield 与 yield<em> 的区别在于:yield 只是返回右侧对象的值,而 yield</em> 则将函数委托(delegate)到另一个生成器( Generator)或可迭代的对象(如字符串、数组和类数组 arguments,以及 ES6 中的 Map、Set 等)。也就是说,yield*会逐个调用右侧可迭代对象的next方法。</p>
<h2>async await</h2>
<p>上面讲完了如何使用generator来解决异步代码的问题,可以看出,使用generator后,异步逻辑的代码基本和同步逻辑的代码差不多了。但是,generator的运行需要co来支持,所有最后又出现了<code>async</code>和<code>await</code>。<code>async</code>和<code>await</code>其实就是generator的语法糖。举个例子:</p>
<pre><code class="javascript">// generator
function *syncFn() {
var d1 = yield sleep(1000);
console.log('%s ms后运行', d1);
var d2 = yield sleep(3000);
console.log('%s ms后运行', d2);
}</code></pre>
<p>上面的代码使用async改写就是:</p>
<pre><code class="javascript">// async
function async syncFn() {
var d1 = await sleep(1000);
console.log('%s ms后运行', d1);
var d2 = await sleep(3000);
console.log('%s ms后运行', d2);
}</code></pre>
<p>可以看出,async的语法其实就是将generator中的<code>*</code>替换成<code>async</code>,将<code>yield</code>替换成<code>await</code>。</p>
<h3>async和await的优势</h3>
<p>有了generator,为什么还要提出async呢?因为async有以下几点优势:</p>
<ol>
<li><p>async的语义更清晰</p></li>
<li><p>async方法自带执行器,运行时和普通方法一样。而generator的运行依赖于co</p></li>
<li><p>await后面的方法可以是任意方法。而co现在了yield后面的方法必须是Promise</p></li>
</ol>
<h2>总结</h2>
<p>总结一下异步代码的发展过程:</p>
<ol>
<li><p>【回调函数】最基本的解决方法,将异步结束函数以参数的方式传递到异步函数中,也就是使用回调函数的方式来实现异步逻辑。</p></li>
<li><p>【Promise】为了解决回调函数的横向发展问题,定义了Promise。</p></li>
<li><p>【generator】Promise虽然解决了异步代码横向发展问题,可是使用Promise语义不够清晰,代码会呈现纵向发展趋势,所以,ES6中出现了generator函数来解决异步代码问题。</p></li>
<li><p>【async】generator函数基本上解决了异步代码问题,但是generator函数的运行却被外部控制着。最后提出了async,实现了generator + co的功能,而且语义更加清晰。</p></li>
</ol>
koa入门
https://segmentfault.com/a/1190000006732849
2016-08-27T09:49:54+08:00
2016-08-27T09:49:54+08:00
chenhao_ch
https://segmentfault.com/u/chenhao_ch
3
<h2>介绍</h2>
<p>koa是一个相对于express来说,更小,更健壮,更富表现力的Web框架。koa通过组合不同的generator来避免繁琐的回调函数调用。koa的核心库没有绑定任何的中间件,仅仅提供了一个轻量优雅的函数库,使得编写Web应用变得得心应手。</p>
<h2>使用</h2>
<p>在项目目录路径下运行命令<code>npm install --save-dev koa</code>就可以在本地安装koa模块。 <br>安装完成后,写一个hello world来验证是否生效。</p>
<h3>hello world</h3>
<p>hello world代码十分简单。</p>
<pre><code class="javascript">const koa = require('koa');
const app = koa();
app.use(function *() {
this.body = 'hello world';
});
app.listen(3000);</code></pre>
<p>上面8行代码就实现了koa的hello world。第4行调用的<code>app.use()</code>传入了一个generator方法,就是koa中间件的基本实现。koa应用的实现就是由一个一个的中间件来实现。每一个中间件都是一个generator方法,通过yield语句,将一个一个中间件逻辑级联起来。举个例子:</p>
<pre><code class="javascript">const koa = require('koa');
const app = koa();
// x-response-time
app.use(function *(next) {
console.log('line 1');
const start = new Date;
yield next;
console.log('line 5');
const ms = new Date - start;
this.set('X-Response-Time', `${ms}ms`);
});
// log time
app.use(function *(next) {
console.log('line 2');
const start = new Date;
yield next;
console.log('line4');
const ms = new Date - start;
console.info('%s %s : %s ms', this.method, this.url, ms);
});
// response
app.use(function *(next) {
console.log('line3');
this.body = 'hello world';
});
app.listen(3000);</code></pre>
<p>运行上面的代码后,当有一个请求被触发时,控制台的log就会打印如下日志(请忽略上面代码中的info日志):</p>
<pre><code>line 1
line 2
line 3
line 4
line 5</code></pre>
<p>可以看出,koa中间件的运行,被yield分割成了两段来运行,运行的时间顺序如下图: <br><img src="/img/remote/1460000006762496" alt="koa 中间件的运行" title="koa 中间件的运行"></p>
<p>koa框架本身的功能十分简单,koa应用的功能都是通过中间件来实现的,下面我们来介绍常用的几个koa中间件。</p>
<h3>koa-static</h3>
<p>koa-static是管理静态文件请求的中间件。比如要请求html,JS,图片的静态文件时,就可以使用koa-static来实现。<br>举个例子,比如项目根目录下得static目录用于存放静态文件,那么如下代码就可以实现该目录的静态文件请求</p>
<pre><code class="javascript">const path = require('path');
const staticServer = require('koa-static');
app.use(staticServer(path.join(__dirname, 'static')));</code></pre>
<h3>koa-router</h3>
<p>koa-router是一个路由中间件,用法如下:</p>
<pre><code class="javascript">const router = require('koa-router')();
// 监听url请求
router.get('/list', function *() {
// ...
});
router.post('/user/register', function *() {
// ...
});
</code></pre>
<h3>koa-safe-jsonp</h3>
<p>JSONP格式返回的中间件,用法:</p>
<pre><code class="javascript">const jsonp = require('koa-safe-jsonp');
const router = require('koa-router');
const koa = require('koa');
const app = koa();
jsonp(app);
router.get('./list', function *() {
const list = [];
this.json = list;
});</code></pre>
<h3>koa-session</h3>
<p>session管理的中间件,用法:</p>
<pre><code class="javascript">const session = require('koa-session');
app.use(session(app));
router.post('./user/login', function *() {
this.session.user = user;
});</code></pre>
<h3>koa-onerror</h3>
<p>koa-onerror用于格式化异常情况的页面输出。用法:</p>
<pre><code class="javascript">const onerror = require('koa-onerror');
const koa = require('koa');
const app = koa();
onerror(app);</code></pre>
<h2>中间件实现</h2>
<p>上面列出的常用中间件都是十分常用的中间件,但是在业务开发过程中,我们会根据业务场景实现一些专门的中间件,那么如何开发一个中间件呢? <br>现在我们来讲下如何实现koa的中间件。比如我们要实现一个统计请求响应实现的功能,就可以开发一个统计请求响应时间的中间件来使用。 <br>基本从上面的例子中,可以看出,中间件都是通过<code>app.use()</code>来注册的,而<code>app.use()</code>方法的参数是一个generator方法。所以, koa中间件需要返回一个generator方法,所以实现代码如下:</p>
<pre><code class="javascript">// middleware/timer.js
module.exports = function() {
return function *(next) {
const path = this.path;
const start = new Date;
yield next;
const end = new Date;
console.log(`${path} response time: ${ end - start }ms`);
}
};
// server.js
const timer = require('./middleware/timer.js');
app.use(timer());</code></pre>
<p>上面这段代码,就实现了统计请求响应时间的功能。功能十分简单,<code>middleware/timer.js</code>中直接返回了一个generator方法,在里面分别统计请求开始时间和结束时间,并打印时间差。 <br>我们可以将中间件设计的稍微复杂一点,比如我们可以控制过滤掉一部分请求不打印,打印格式也可以进行控制。这个时候我们需要传入一些参数来控制中间件的功能,代码如下:</p>
<pre><code class="javascript">// middleware/timer.js
module.exports = function timer(options) {
return function *timer() {
let start = new Date;
const path = this.path;
const method = this.request.method;
if ( method !== options.filter.method ) start = 0;
yield next;
const end = new Date;
if(start !== 0 && end - start > options.filter.min) {
console.log(options.format.replace(/:url/g, path).replace(/:time/g, `${ end - start }ms`));
}
}
};
// server.js
const timer = require('./middleware/timer.js');
// GET 请求超过100ms时,打印日志,日志格式: url time
app.use(timer({
format: ':url :time'
filter: {
min: 100
method: 'GET'
}
}));</code></pre>
<h2>koa源码理解</h2>
<p>最前面介绍的时候有说到,koa的核心库十分简单,没有绑定任何中间件。所以,在最后说一下koa的源码。 <br>koa.js的源码可以去<a href="https://link.segmentfault.com/?enc=LJ9vTRbq80lS8EBQBrF1wA%3D%3D.G2irGEZzngQdDRaDLMVA8v0%2FsKUu%2BSzR12jdUnFTcT0%3D" rel="nofollow">https://github.com/koajs/koa</a>获得。 <br>koa.js的源码有4个文件,分别是<code>lib/application.js</code>, <code>lib/context.js</code>, <code>lib/request.js</code>, <code>lib/response.js</code>。从名字中可以看出来,<code>context.js</code>, <code>request.js</code>, <code>response.js</code>分别是上下文环境对象,request对象,response对象,而<code>application.js</code>就是koa.js的核心代码。 <br>在上面的例子中,都使用了两个接口,<code>use</code>和<code>listen</code>。下面我们先介绍下这两个方法, 下面是<code>application.js</code>中这两个方法的源码:</p>
<pre><code class="javascript">// ...
app.listen = function(){
debug('listen');
var server = http.createServer(this.callback());
return server.listen.apply(server, arguments);
};
// ...
app.use = function(fn){
if (!this.experimental) {
// es7 async functions are not allowed,
// so we have to make sure that `fn` is a generator function
assert(fn && 'GeneratorFunction' == fn.constructor.name, 'app.use() requires a generator function');
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
};
// ...</code></pre>
<p>可以看出来,<code>use</code>的作用就是将传入的中间件generator方法放到<code>this.middleware</code>中。<code>listen</code>接口的作用其实就是启动了一个server,并将请求处理设置为 <code>this.callback()</code>的返回方法。 <br>然后我们来看下<code>this.callback</code>怎么写的:</p>
<pre><code class="javascript">// ...
app.callback = function(){
if (this.experimental) {
console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.')
}
var fn = this.experimental
? compose_es7(this.middleware)
: co.wrap(compose(this.middleware));
var self = this;
if (!this.listeners('error').length) this.on('error', this.onerror);
return function(req, res){
res.statusCode = 404;
var ctx = self.createContext(req, res);
onFinished(res, ctx.onerror);
fn.call(ctx).then(function () {
respond.call(ctx);
}).catch(ctx.onerror);
}
};
// ...</code></pre>
<p>其中的核心代码是这两句:</p>
<pre><code class="javascript">// ...
co.wrap(compose(this.middleware));
// ...
return function(req, res){
// ...
fn.call(ctx).then(function () {
respond.call(ctx);
}).catch(ctx.onerror);
}
// ...</code></pre>
<p>其中<code>compose(this.middleware)</code>的作用是将传入的中间件数组合并成层层调用的generator函数。<code>co.wrap()</code>方法的作用是将generator数转化成一个自执行的函数。最后的<code>fn.call(ctx)</code>就开始逐步执行中间件了。</p>
一口气完成electron的入门学习
https://segmentfault.com/a/1190000006207600
2016-08-08T18:44:02+08:00
2016-08-08T18:44:02+08:00
chenhao_ch
https://segmentfault.com/u/chenhao_ch
38
<h2>介绍</h2>
<p>目前,使用前端技术开发桌面应用已经越来越成熟,这使得前端同学也可以参与桌面应用的开发。目前类似的工具有electron,NW.js等。这里我们着重介绍下electron。</p>
<h2>electron开发</h2>
<p>electron是基于Node.js和Chromium做的一个工具。electron是的可以使用前端技术实现桌面开发,并且支持多平台运行。下面来讲下如何使用electron开发桌面app。</p>
<h3>hello world</h3>
<p>一个最简单的electron项目包含三个文件, <code>package.json</code>, <code>index.html</code>, <code>main.js</code>。 <br><code>package.json</code>是Node.js项目的配置文件,<code>index.html</code>是桌面应用的界面页面,<code>main.js</code>是应用的启动入口文件。其中,核心的文件是<code>inde.html</code>和<code>main.js</code>,下面来讲下这两个文件的细节。<br><code>index.html</code>是应用的展示界面,代码如下:</p>
<pre><code class="html"><!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World!</title>
</head>
<body>
<h1>Hello World!</h1>
We are using node <script>document.write(process.versions.node)</script>,
Chrome <script>document.write(process.versions.chrome)</script>,
and Electron <script>document.write(process.versions.electron)</script>.
</body>
</html></code></pre>
<p><code>main.js</code>是electron应用的入口文件。主要用户启动electron的界面。可以通过以下代码来启动electron界面。</p>
<pre><code class="javascript">const electron = require('electron');
const { app } = electron;
const { BrowserWindow } = electron;
let win;
function createWindow() {
// 创建窗口并加载页面
win = new BrowserWindow({width: 800, height: 600});
win.loadURL(`file://${__dirname}/index.html`);
// 打开窗口的调试工具
win.webContents.openDevTools();
// 窗口关闭的监听
win.on('closed', () => {
win = null;
});
}
app.on('ready', createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (win === null) {
createWindow();
}
});</code></pre>
<p>上面的代码就实现了一个hello world的electron应用。代码写完后,需要运行代码看看效果,这个时候需要使用<code>electron-prubuilt</code>来运行代码。 <br>首先,我们需要安装<code>electron-prubuilt</code>包。可以通过命令<code>npm install --saved-dev electron-prebuilt</code>进行安装。安装完成后,项目本地就可以使用<code>electron</code>命令,直接运行命令<code>electron .</code>就可以看到<code>hello world</code>的效果。或者可以在<code>package.json</code>中设置scripts,方便项目的运行。 <br>具体代码可以去<a href="https://link.segmentfault.com/?enc=Wfc17ZXJMKoIijUyNBCzBA%3D%3D.eHBz3DenCuhP28HMiWqJJhummWGaTML2w3z3vDFotsQ4hZ5aF%2Fpd5ZnFyhsV7pN3Zc24zUmeuCKVLFnPVrtkcQ%3D%3D" rel="nofollow">这里</a>获取。</p>
<h3>主进程与渲染进程</h3>
<p>electron中,由<code>package.json</code>中的main.js运行出来的进程为主进程(Main Process)。主进程用于创建GUI界面以便web页面的展示。electron由Chromium负责页面的显示,所以当创建一个页面时,就会对应的创建渲染进程(Renderer Process)。 <br>主进程通过创建<code>BrowserWindow</code>对象来创建web显示页面,<code>BrowserWindow</code>运行在他自己的渲染进程中。当<code>BrowserWindow</code>被销毁时,对应的渲染进程也会终止。 </p>
<p>在渲染进程中,直接调用原生的GUI接口是十分危险的。如果你想在渲染进程中使用原生的GUI的功能,需要让渲染进程与主进程进行通信,再由主进程去调用对应接口。那么主进程和渲染进程是如何进行通信的呢? <br>electron中,主进程与渲染进程的通信存在多种方法。这里介绍一种,通过<code>ipcMain</code>和<code>ipcRenderer</code>对象,以消息的方式进行通信。 <br>先来看下主进程如何向渲染进程发信息。 <br>首先,渲染进程通过接口<code>ipcRenderer.on()</code>来监听主进程的消息信息。主进程通过接口<code>BrowserWindow.webContents.send()</code>向所有渲染进程发送消息。相关代码如下:</p>
<pre><code>// renderer.js
// 引入ipcRenderer对象
const electron = require('electron');
const ipcRenderer = electron.ipcRenderer;
// 设置监听
ipcRenderer.on('main-process-messages', (event, message) => {
console.log('message from Main Process: ' , message); // Prints Main Process Message.
});
// main.js
// 当页面加载完成时,会触发`did-finish-load`事件。
win.webContents.on('did-finish-load', () => {
win.webContents.send('main-process-messages', 'webContents event "did-finish-load" called');
});</code></pre>
<p>那么渲染进程需要给主进程发生消息该如何做呢?</p>
<pre><code>// renderer.js
ipcRenderer.on('asynchronous-reply', (event, arg) => {
console.log('asynchronous-reply: %O %O', event, arg);
});
ipcRenderer.send('asynchronous-message', 'hello');
// main.js
ipcMain.on('asynchronous-message', (event, arg) => {
// 返回消息
event.sender.send('asynchronous-reply', 'ok');
});</code></pre>
<p>上面的代码是异步监听过程。 主进程设置好监听,渲染进程通过<code>ipcRenderer.send()</code>发送消息。主进程获得消息后,通过<code>event.sender.send()</code>返回信息。返回信息也是异步的,所以渲染进程也设置了监听。 <br>另外,electron还提供了一种同步的消息传递方式。代码如下:</p>
<pre><code>// renderer.js
console.log('synchronous-message: ', ipcRenderer.sendSync('synchronous-message', 'hello'));
// main.js
ipcMain.on('synchronous-message', (event, arg) => {
event.returnValue = 'ok';
});</code></pre>
<p>主进程与渲染进程相互传递数据的例子可以去<a href="https://link.segmentfault.com/?enc=T3beunBqS5fosBtkwDbe6w%3D%3D.6RWvJmrFY2ocE8dS8mkWf8VDUZVDdWOr18BZlwE6bJ1CXNvaFAbisaS%2FUIdCxLeDMAabQHCW33wyWBIyUR%2Belw%3D%3D" rel="nofollow">这里</a>获取。</p>
<h3>调试</h3>
<p>一个app的开发过程,会用到代码调试,那么electron应该如何进行调试呢? <br>electron中,渲染进程因为是Chromium的页面,所以可以使用DevTools进行调试。启动DevTools的方式是:在main.js中,创建好<code>BrowserWindow</code>后,通过接口<code>BrowserWindow.webContents.openDevTools();</code>来打开页面的DevTools调试工具,进行页面调试,具体的调试方法和使用Chrome进行调试一致。 <br>主进程是基于Node.js的,所以electron的调试和Node.js类似。这里说下在VS Code中如何进行electron主进程的调试。<br>第一步,设置VS Code的tasks,用于启动electron。相关配置如下:</p>
<pre><code>{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "0.1.0",
"command": "electron",
"isShellCommand": true,
"showOutput": "always",
"suppressTaskName": true,
"args": ["--debug=5858", "."]
}</code></pre>
<p>其中,<code>--debug=5858</code>是用于调试Node.js的端口。<br>第二步,设置VS Code项目的调试配置。可以生成launch.json,内容如下:</p>
<pre><code>{
"version": "0.2.0",
"configurations": [ {
"name": "Attach",
"type": "node",
"address": "localhost",
"port": 5858,
"request": "attach"
}
]
}</code></pre>
<p>其中的<code>port:5858</code>的端口信息需要和上面的<code>--debug=5858</code>端口保持一致。 <br>配置完成后,便可以开始调试主进程。 <br>首先启动electron项目。 <br>因为上面配置了task,所以可以使用VS Code的task进行启动。按下快捷键<code>shift + command + p</code>可以启动命令,输入<code>task electron</code>命令,回车,就可以运行<code>electron</code>的task进行项目的启动。 <br>项目启动后,再开始设置主进程代码的断点。在VS Code的调试界面中设置断点,并点击运行。这个时候,操作启动的electron应用,当运行到断点所在代码时,就可以触发断点调试。</p>
<h3>扩展功能</h3>
<p>electron除了使用前端技术实现界面展示的功能外,还提供了大量的桌面环境接口。比如支持Notification,Jump List, dock menu等。具体支持哪些桌面接口以及如何使用,可以去<a href="https://link.segmentfault.com/?enc=7qQosTOF1Qx%2F7VTIjh7l2Q%3D%3D.3FObsQhF1oZ%2Bx5sOVAZ2H1ASUWcQ7L3o%2BkOiGI04hf9q1UiA8efly2CXA3bFqSGWbNnYuqbX7u393bxUrjsB6OKJwlCh8u05eScVs8cPvhY%3D" rel="nofollow">http://electron.atom.io/docs/...</a> 了解。</p>
<h2>打包</h2>
<p>完成功能代码后,我们需要将代码打成可运行的包。可以使用<code>electron-packager</code>来进行应用打包,<code>electron-packager</code>支持windows, Mac, linux等系统。具体介绍可以去<a href="https://link.segmentfault.com/?enc=yi4AZA8WBKl%2FcvbYlJ%2FoOg%3D%3D.Lw0oiK7urvFDvz8cUnwppGk4b805Xp6r6PVFvSsUHpEa1siQP6xQIrZOYNQQNtlE3xBCAhyV21kLt%2FKtLCrbAg%3D%3D" rel="nofollow">https://github.com/electron-u...</a> 了解。<br>打包的步骤很简单,只需要两步:</p>
<ol>
<li><p>全局安装Node.js模块<code>electron-packager</code>。</p></li>
<li><p>运行命令: <code>electron-packager <sourcedir> <appname> --platform=<platform> --arch=<arch> [optional flags...]</code>。 其中platform可以取<code>darwin</code>, <code>linux</code>, <code>mas</code>, <code>win32</code>4个值;arch可以取<code>ia32</code>, <code>x64</code>两个值。 <br>需要注意,打包后,代码中的所有路径都必须使用绝对路径,通过<code>${__dirname}</code>获得app的根路径,然后拼接成绝对路径。</p></li>
</ol>
页面动画知识点整理
https://segmentfault.com/a/1190000005705820
2016-06-13T14:21:11+08:00
2016-06-13T14:21:11+08:00
chenhao_ch
https://segmentfault.com/u/chenhao_ch
1
<p>平时工作中会遇到需要实现一些存在动画的页面。这里对动画的实现知识做一个整理。<br>页面动画的实现可以分为两类:CSS动画、Canvas动画、JavaScript动画。JavaScript动画没啥好讲的,这里就不整理了。</p>
<h2>CSS动画</h2>
<p>CSS3中提供了一个属性<code>transition</code>,用来实现CSS样式的平滑变化。举个例子:</p>
<pre><code class="CSS">.box {
width: 100px;
height: 100px;
background: red;
transition: width 1s;
}
.box:hover {
width: 300px;
}</code></pre>
<p>当鼠标hover到<code>.box</code>元素时,元素会在1s内逐渐的将宽度变化到300px。<br>具体效果可以去<a href="https://link.segmentfault.com/?enc=MQS6CXwaXYBiAfgUG2E3ag%3D%3D.cBnV7kEUAKqT5%2FNg06V86l65FPjXa3HnSzHD5gIRWzLK%2FbfkJnUsw7asOC%2FiVWXF" rel="nofollow">这里</a>查看。</p>
<p>使用<code>transition</code>可以实现较为简单的动画。如果需要实现比较复杂的动画,可以使用<code>amination</code>来实现。举个例子:</p>
<pre><code class="CSS">@keyframes cssAmination {
0% {background: red; transform: skew(0deg);}
25% {background: yellow; transform: skew(-20deg);}
50% {background: blue; transform: skew(0deg);}
75% {background: green; transform: skew(20deg);}
100% {background: red; transform: skew(0deg);}
}
.amin {
animation: cssAmination 1s infinite ease;
}</code></pre>
<p>在上的例子中,首先由<code>keyframes</code>定义一个动画叫做: <code>cssAnimation</code>。在<code>cssAnimation</code>中定义了动画过程中关键的5帧。每一帧都设置了当前帧的样式特征。然后在<code>.amin</code>节点上设置了动画属性<code>animation</code>,并将其设为前面定义的动画<code>cssAnimation</code>,每一次动画1秒,<code>infinite</code>表示无限循环,<code>ease</code>表示缓动方式,两个关键帧之间的变化是<code>ease</code>方式逐步变化的。<br>具体效果可以到<a href="https://link.segmentfault.com/?enc=4wo%2BKtxldnj%2FlfQmypzxLg%3D%3D.i5ViLZ5aAcjw7m3pavrF716sbuT8NWJUoruxT7sXqx7ipM8pBgcb9ifPahT25%2BNZ" rel="nofollow">这里</a>查看</p>
<p><code>animation</code>的缓动函数有很多类型的值,有一个值比较特别就是<code>step[n[, start | end]]</code>。<code>step</code>的效果是将<code>keyframes</code>中的每一个关键帧之间的切换并不是逐步变化的,而是到达某一关键帧后直接变化成新的关键帧样式,并保持不变,直到下一关键帧。所以使用<code>step</code>可以实现CSS3的帧动画。写法如下:</p>
<pre><code class="CSS">@keyframes cssFrameAmination {
0% {background-position: 0 0;}
25% {background-position: -100px 0;}
50% {background-position: -200px 0;}
75% {background-position: -300px 0;}
100% {background-position: -400px 0;}
}
.amin-frame {
background: url("./sprite.png") 0 0 no-repeat;
animation: cssFrameAmination 1s infinite step(5, start);
}</code></pre>
<p>在上面的例子中,设置动画<code>cssFrameAmination</code>,其中每一关键帧都是精灵动画图片的一帧图片。然后在<code>animation</code>中设置<code>animation-timing-function</code>为<code>step(5, start)</code>表示动画分5帧。</p>
<p>有关CSS3动画相关的知识细节可以去<a href="https://link.segmentfault.com/?enc=cH6qgc3VsYgZMwF6n1c2pQ%3D%3D.rFX1ysEICKFlEiJaw2oxf%2BVUDGCn44O%2FahOE%2B%2BG07g%2F8HiKr1NKolCxrz64PdbHUezDjcwl0P4f3vwE0rdMUEw%3D%3D" rel="nofollow">这里</a>了解。</p>
<h2>Canvas</h2>
<p><code>canvas</code>是一个HTML标签,用于提供给脚本进行画图图形的绘制。<code>canvas</code>的绘制主要由<code>CanvasRenderingContext2D</code>的实例来进行绘制。<code>CanvasRenderingContext2D </code>可以通过<code>canvas</code>DOM对象的<code>getContext</code>获得,代码如下:</p>
<pre><code class="javascript">const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');</code></pre>
<blockquote><p><code>getContext</code>的参数是指在画布上绘制的类型,'2d'表示绘制二维图形。目前三维还没有实现,所以参数只支持'2d'。</p></blockquote>
<h3>绘制图形</h3>
<p><code>canvas</code>的上下文提供了众多的绘制方法。当你绘制一个图形时,基本思路是这样的:</p>
<ol>
<li><p>调用<code>save</code>方法保存之前的样式状态</p></li>
<li><p>调用<code>beginPath</code>表示开始设置路径</p></li>
<li><p>调用<code>fillStyle</code>, <code>strokeStyle</code>等对接下来的路径进行样式设置</p></li>
<li><p>调用<code>moveTo</code>,<code>lineTo</code>, <code>rect</code>, <code>arc</code>等设置路径</p></li>
<li><p>调用<code>closePath</code>闭合路径</p></li>
<li><p>调用<code>fill</code>或者<code>stroke</code>对路径进行绘制</p></li>
<li><p>调用<code>restore</code>恢复之前保存的样式状态</p></li>
</ol>
<p>上面过程中的<code>save</code>和<code>restore</code>的作用是将已经设置的样式进行保存和恢复。当存在多个图形时,前面的样式如果不恢复为默认样式,会影响到第二个图形的样式。使用<code>save</code>和<code>restore</code>可以保证每一个图形在绘制开始时,都是默认的样式。当然,你也可以不调用<code>save</code>和<code>restore</code>,而是通过将前面已经设置过的所有样式进行逐个的还原。 <br><code>save</code>可以保存的样式类型有:</p>
<ul>
<li><p>当前应用的变形(即移动,旋转和缩放)</p></li>
<li><p>strokeStyle, fillStyle, globalAlpha, lineWidth, lineCap, lineJoin, miterLimit, shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor, globalCompositeOperation 的值</p></li>
<li><p>当前的裁切路径(clipping path)</p></li>
</ul>
<p>步骤5<code>closePath</code>尽量不要忘记。原因和<code>save</code>,<code>restore</code>类似,如果忘记调用<code>closePath</code>就会导致前后图形间多绘制一根线。<br>我写了一个时钟的例子:<a href="https://link.segmentfault.com/?enc=bkR0F7Z67N%2FbLEOEWAta0A%3D%3D.rFZuWvZKm3nRXfYUjWRjzePT3Vy2RizyS7tVtgVGGUn7JawOV7S7%2Bgu4diDsKhTa" rel="nofollow">github</a></p>
<p>下面对各类接口做了一个整理 <br><strong>样式设置</strong></p>
<table>
<thead><tr>
<th align="left">接口名</th>
<th align="left">接口描述</th>
</tr></thead>
<tbody>
<tr><td align="left"><strong>颜色</strong></td></tr>
<tr>
<td align="left">fillStyle</td>
<td align="left">图形填充颜色</td>
</tr>
<tr>
<td align="left">strokeStyle</td>
<td align="left">图形轮廓颜色</td>
</tr>
<tr>
<td align="left">globalAlpha</td>
<td align="left">图形全局透明度</td>
</tr>
<tr><td align="left"><strong>阴影</strong></td></tr>
<tr>
<td align="left">shadowOffsetX, shadowOffsetY</td>
<td align="left">阴影方向</td>
</tr>
<tr>
<td align="left">shadowBlur</td>
<td align="left">设定阴影的模糊程度</td>
</tr>
<tr>
<td align="left">shadowColor</td>
<td align="left">阴影的颜色值</td>
</tr>
<tr><td align="left"><strong>线型</strong></td></tr>
<tr>
<td align="left">lineWidth</td>
<td align="left">线条宽度(int)</td>
</tr>
<tr>
<td align="left">lineCap</td>
<td align="left">线条末端样式(butt: 平直; round: 添加半圆; square: 添加方形)</td>
</tr>
<tr>
<td align="left">lineJoin</td>
<td align="left">设置线条间的接合处(bevel: 斜角; round: 圆角; miter: 尖角)</td>
</tr>
<tr>
<td align="left">miterLimit</td>
<td align="left">两线相交时尖角最大长度(lineJoin:miter时生效,过长不显示)</td>
</tr>
<tr>
<td align="left">getLineDash</td>
<td align="left">返回当前虚线样式(数组)</td>
</tr>
<tr>
<td align="left">setLineDash</td>
<td align="left">设置虚线样式(数组)</td>
</tr>
<tr>
<td align="left">lineDashOffset</td>
<td align="left">设置虚线样式起始偏移量</td>
</tr>
<tr><td align="left"><strong>渐变</strong></td></tr>
<tr>
<td align="left">createLinearGradient(x1, y1, x2, y2)</td>
<td align="left">线性渐变</td>
</tr>
<tr>
<td align="left">createRadialGradient(x1, y1, r1, x2, y2, r2)</td>
<td align="left">圆渐变, 渐变反向是从圆心向外发散</td>
</tr>
<tr>
<td align="left">gradient.addColorStop(position, color)</td>
<td align="left">对生成的gradient对象添加结束颜色。position是中间过程,取值0~1</td>
</tr>
<tr><td align="left"><strong>图案样式</strong></td></tr>
<tr>
<td align="left">createPattern(imageOrCanvas, type)</td>
<td align="left">创建图片填充对象。image必须是已加载完毕的;type: repeat, repeat-x, repeat-y, no-repeat</td>
</tr>
</tbody>
</table>
<p><strong>路径</strong></p>
<table>
<thead><tr>
<th align="left">接口名</th>
<th align="left">描述</th>
</tr></thead>
<tbody>
<tr>
<td align="left">moveTo(x, y)</td>
<td align="left">移动路径绘制的起始点</td>
</tr>
<tr>
<td align="left">beginPath()</td>
<td align="left">新建一条路径</td>
</tr>
<tr>
<td align="left">closePath()</td>
<td align="left">闭合路径</td>
</tr>
<tr>
<td align="left">lineTo(x, y)</td>
<td align="left">从开始位置绘制路径到目标位置</td>
</tr>
<tr>
<td align="left">rect(x, y, width, height)</td>
<td align="left">绘制矩形路径</td>
</tr>
<tr>
<td align="left">arc(x, y, radius, startAngle, endAngle, anticlockwise)</td>
<td align="left">绘制圆弧:x,y为圆心;radius为半径;startAngle,endAngle为起止位置;anticlockwise为反向(顺时针,逆时针)</td>
</tr>
<tr>
<td align="left">arcTo(x1, y1, x2, y2, radius)</td>
<td align="left">绘制圆弧,并连接控制点</td>
</tr>
<tr>
<td align="left">quadraticCurveTo(cp1x, cp1y, x, y)</td>
<td align="left">x,y为结束点; cp1x,xp1y为控制点</td>
</tr>
<tr>
<td align="left">bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)</td>
<td align="left">x,y为结束点;cp1x,cp1y为控制点1; cp2x,cp2y为控制点2</td>
</tr>
<tr>
<td align="left">clip()</td>
<td align="left">裁剪区域,区域外的不会发生绘制</td>
</tr>
</tbody>
</table>
<p><strong>绘制</strong></p>
<table>
<thead><tr>
<th align="left">接口名</th>
<th align="left">描述</th>
</tr></thead>
<tbody>
<tr>
<td align="left">fillRect(x, y, width, height)</td>
<td align="left">绘制填充矩形,等同于rect(); fill();</td>
</tr>
<tr>
<td align="left">strokeRect(x, y, width, height)</td>
<td align="left">绘制矩形边框。等同于rect(); stroke()</td>
</tr>
<tr>
<td align="left">fill()</td>
<td align="left">填充路径的内容区域</td>
</tr>
<tr>
<td align="left">stroke()</td>
<td align="left">通过路径线条绘制图形轮廓</td>
</tr>
</tbody>
</table>
<p><strong>清除</strong></p>
<table>
<thead><tr>
<th align="left">接口名</th>
<th align="left">描述</th>
</tr></thead>
<tbody><tr>
<td align="left">clearRect(x, y, width, height)</td>
<td align="left">清除指定矩形区域</td>
</tr></tbody>
</table>
<p><strong>文字</strong></p>
<table>
<thead><tr>
<th align="left">接口名</th>
<th align="left">描述</th>
</tr></thead>
<tbody>
<tr>
<td align="left">font</td>
<td align="left">设置文字样式,同css的font</td>
</tr>
<tr>
<td align="left">textAlign</td>
<td align="left">对其方式</td>
</tr>
<tr>
<td align="left">textBaseLine</td>
<td align="left">基线对其</td>
</tr>
<tr>
<td align="left">direction</td>
<td align="left">文本方向</td>
</tr>
<tr>
<td align="left">fillText(text, x, y [, maxWidth])</td>
<td align="left">绘制文字填充内容</td>
</tr>
<tr>
<td align="left">strokeText(text, x, y [, maxWidth])</td>
<td align="left">绘制文字边框内容</td>
</tr>
<tr>
<td align="left">measureText(text)</td>
<td align="left">返回文本的信息</td>
</tr>
</tbody>
</table>
<p><strong>样式保存</strong></p>
<table>
<thead><tr>
<th align="left">接口名</th>
<th align="left">描述</th>
</tr></thead>
<tbody>
<tr>
<td align="left">save()</td>
<td align="left">保存当前样式</td>
</tr>
<tr>
<td align="left">restore()</td>
<td align="left">恢复之前保存样式</td>
</tr>
</tbody>
</table>
<h3>绘制图片</h3>
<p><code>canvas</code>虽然可以绘制图形,但是最常用的应该是绘制图片。图片的绘制和图形的绘制类似。 <br><code>canvas</code>使用接口<code>drawImage()</code>进行接口绘制,接口定义如下:</p>
<pre><code>drawImage(image, x, y, width, height, dx, dy, dWidth, dHeight);</code></pre>
<p>其中的参数定义如下:</p>
<ul>
<li><p><code>image</code>可以使HTMLImageElement, HTMLVideoElement(Video元素的某一帧), HTMLCanvasElement, ImageBitmap。</p></li>
<li><p><code>x</code>, <code>y</code>是指图片截取的起始位置。</p></li>
<li><p><code>width</code>, <code>height</code>是指图片截取的宽高。</p></li>
<li><p><code>dx</code>, <code>dy</code>是目标在Canvas中的起始坐标。</p></li>
<li><p><code>dWidth</code>, <code>dHeight</code>用于控制canvas绘制的图片的缩放大小。</p></li>
</ul>
<h3>图片变形</h3>
<p><code>canvas</code>还可以和CSS一样对图形进行变形转化。接口列表如下:</p>
<table>
<thead><tr>
<th align="left">接口名字</th>
<th align="left">描述</th>
</tr></thead>
<tbody>
<tr>
<td align="left">translate(x, y)</td>
<td align="left">偏移。x,y是偏移量</td>
</tr>
<tr>
<td align="left">rotate(angle)</td>
<td align="left">旋转角度,顺时针</td>
</tr>
<tr>
<td align="left">scale(x, y)</td>
<td align="left">缩放。x, y分别是横轴,纵轴的缩放比例</td>
</tr>
<tr>
<td align="left">transform(m11, m12, m21, m22, dx, dy)</td>
<td align="left">变形矩阵转化</td>
</tr>
</tbody>
</table>
<h3>图形重叠处理</h3>
<p>前面的例子中,当两个图形重叠后,都是由后面绘制的图形覆盖住前面绘制的图形。有时候需要改变这种情况。这种时候就可以使用<code>globalCompositeOperation</code>来进行设置(还可以用来遮盖,清除某些区域)。具体参数可以去<a href="https://link.segmentfault.com/?enc=GI5SZhyzmEMCIOuycwSGXQ%3D%3D.rpCs4yVvtpoxBP%2FbypA%2BYxk6PxITLw7fEqgIvnC0GKpNPmygfzPWP8VC65m4SumwTDAFeSu9AbZXWuLoWxL9dgE%2FzzYTFWXhXxkjZys2fVFPgvzRgVJIXEeL65bytJId1IehuW20GwTU7DQiK3dayg%3D%3D" rel="nofollow">这里</a>查看</p>
<pre><code>globalCompositeOperation: type</code></pre>
<h3>动画实现</h3>
<p>使用上面的接口可以在canvas上绘制图片,但是都是固定的。当我们不断的对canvas进行重绘时,就可以达到动画的而效果。 </p>
<p>动画的帧率达到60帧每秒时,也就是16ms没帧时,动画过程是流畅的。所以我们要对动画过程的绘制进行控制。有三个方法可以进行控制:</p>
<ul>
<li><p><code>setInterval</code>。设置每16ms执行一次绘制过程。但是该方法存在一个问题,开始运行绘制函数的时间点可能处于某一帧的快结束时间点。这个时候绘制过程需要小于16ms才可以达到流畅。</p></li>
<li><p><code>setTimeout</code>。和<code>setInterval</code>类似。</p></li>
<li><p><code>requestAnimationFrame</code>。该方法会在浏览器每一次绘制结束后调用一次。使用该方法可以很好的避免<code>setInterval</code>和<code>setTimeout</code>出现的运行绘制函数时间不在每一帧开始的时间点。</p></li>
</ul>
<h3>Canvas性能</h3>
<ol>
<li><p>创建一个离屏canvas, 预先对复杂图形进行绘制。</p></li>
<li><p>避免浮点数的坐标点, 使用Math.floor()对坐标取整。</p></li>
<li><p>不要使用<code>drawImage</code>去缩放图片。</p></li>
<li><p>使用多canvas绘制复杂场景。</p></li>
<li><p>使用CSS设置大背景图。</p></li>
</ol>
<h3>Canvas调试</h3>
<p>查了很多资料,发现Chrome 44版本之前是有Canvas调试功能的,但是Chrome 44之后,将Canvas调试功能去除了,并以扩展接口的方式提供功能。找了很久没有找到调试Canvas的扩展。另外,Firefox有提供专门的Canvas调试面板。试用了下,功能太少,对定位问题并没什么软用。<br>所以,关于调试的问题,只能试用传统的设断点,并逐步运行看效果进行调试。</p>
Flexbox属性介绍
https://segmentfault.com/a/1190000005025361
2016-04-28T14:24:02+08:00
2016-04-28T14:24:02+08:00
chenhao_ch
https://segmentfault.com/u/chenhao_ch
2
<h2>介绍</h2>
<p>flexbox是HTML5中提出的新的布局方式。<br>使用时,需要将父子节点的<code>display</code>都设置为<code>flex</code>。<br>每个参赛的效果可以参考<a href="http://codepen.io/enxaneta/full/adLPwv">这里</a></p>
<h2>父节点属性</h2>
<h3>flex-direction</h3>
<p>控制子界点的方向。一共有4个值:</p>
<pre><code>.container {
flex-direction: row; // default 从左往右,靠父节点左边
flex-direction: row-reverse; // 从右往左,靠父节点右边
flex-direction: column; // 从上往下,靠父节点顶部
flex-direction: column-reverse; // 从下往上,靠父节点底部
}</code></pre>
<h3>flex-wrap</h3>
<p>控制子元素是否是现在在一行中。如果设置为显示到一行中,但是子元素的宽度和大于父节点宽度,那么子元素会对应缩小。</p>
<pre><code>.container {
flex-wrap: nowrap; // default 显示到一行中
flex-wrap: wrap; // 多行显示,行顺序顺序显示
flex-wrap: wrap-reverse; // 多行显示,行顺序倒序显示
}</code></pre>
<h3>flex-flow</h3>
<p><code>flex-direction</code>和<code>flex-wrap</code>两个的简写。</p>
<pre><code>flex-flow: [flex-direction] [flex-wrap]</code></pre>
<h3>align-items</h3>
<p>控制子元素的上下对齐方式。一共有5个值。</p>
<pre><code>.container {
align-items: flex-start; // 容器顶部对齐
align-items: flex-end; // 容器底部对齐
align-items: center; // 中间对齐
align-items: baseline; // 子元素文字baseline对齐
align-items: stretch; // 上下对齐,会改变子容器的高度
}</code></pre>
<h3>justfy-content</h3>
<p>父容器同向多余空间的展示方式。有6个值。</p>
<pre><code>.container {
justfy-content: flex-start; // 子元素靠前,多余空间靠后
justfy-content: flex-end; // 子元素靠后,多余空间靠前
justfy-content: center; // 子元素剧中,多余空间平均放前后
justfy-content: space-between; // 多余空间放子元素中间
justfy-content: space-around; // 多余空间环绕元素周边
}</code></pre>
<h3>align-content</h3>
<p>类似于<code>justfy-content</code>, 管理父容器垂直方向多余空间的展示。6个值。</p>
<pre><code>.container {
align-content: flex-start; // 子元素靠前,
align-content: flex-end; // 子元素靠后
align-content: center; // 子元素居中,多余空间放前后
align-content: stretch; // 子元素铺满整个父容器
align-content: space-between; // 多余空间放子元素中间
align-content: space-round; // 多余空间环绕元素周边
}</code></pre>
<h2>子节点属性</h2>
<h3>algin-self</h3>
<p>同<code>algin-item</code>,不过<code>algin-self</code>是作用于子元素自己。应该有6个值</p>
<pre><code>.item {
align-self: auto; // default 默认模式,按照父节点的algin-item来显示
align-self: flex-start; // 顶部对齐
align-self: flex-end; // 底部对齐
align-self: center; // 居中对齐
align-self: baseline; // baseline对齐
align-self: stretch; // 填充满
}</code></pre>
<h3>flex-grow</h3>
<p>子元素间的大小比例。</p>
<pre><code>.item {
width: 100px;
}
.item01 {
flex-grow: 1;
}
.item02 {
flex-grow: 2;
}</code></pre>
<h3>flex-shrink</h3>
<p>超出父容易后的收缩比率。 举个例子:父容器400px;有三个子元素,都是200px;当设置第三个子元素<code>flex-shrink</code>为3时,子元素的收缩比率就是1:1:3。<br>一共超出父元素200px;那么收缩的比率是40px:40px:120px; 也就是说子元素的实际展示宽是160px, 160px, 80px。</p>
<pre><code>.container {
width: 400px;
}
.item {
width: 200px;
}
.item03 {
flex-shrink: 3;
}</code></pre>
<h3>flex-basis</h3>
<p>如果所有子元素的基准值之和大于剩余空间,则会根据每项设置的基准值,按比率伸缩剩余空间</p>
<pre><code>.item {
flex-basis: 10px;
}</code></pre>
<h3>flex</h3>
<p><code>flex-grow</code>, <code>flex-shrink</code>, <code>flex-basis</code>三者的简写。</p>
<pre><code>.item {
flex-grow: [flex-grow] [flex-shrink] [flex-basis];
}</code></pre>
<h3>order</h3>
<p>控制子元素的顺序。order会改变子元素排序的先后。</p>
<pre><code>.item01 {
order: 2;
}
.item02 {
order: 1;
}</code></pre>
Ajax与Fetch
https://segmentfault.com/a/1190000004871100
2016-04-05T16:35:34+08:00
2016-04-05T16:35:34+08:00
chenhao_ch
https://segmentfault.com/u/chenhao_ch
4
<h2>介绍</h2>
<p>页面中需要向服务器请求数据时,基本上都会使用Ajax来实现。Ajax的本质是使用XMLHttpRequest对象来请求数据。XMLHttpRequest的使用如下:</p>
<pre><code class="JavaScript">var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onload = function() {
console.log(xhr.response);
};
xhr.onerror = function() {
console.error('error');
};
xhr.send();</code></pre>
<p>可以看出,XMLHttpRequest对象是通过事件的模式来实现返回数据的处理的。目前还有一个是采用Promise方式来处理数据的,这个技术叫做Fetch。</p>
<h2>Fetch的使用</h2>
<p>使用Fetch实现请求的最基本代码:</p>
<pre><code class="JavaScript">fetch(url).then(function (response) {
return response.json(); // json返回数据
}).then(function (data) {
console.log(data); // 业务逻辑
}).catch(function (e) {
console.error('error');
})</code></pre>
<p>使用ES6的箭头函数后,可以更加简洁:</p>
<pre><code class="JavaScript">fetch(url).then(response => response.json())
.then(data => console.log(data))
.catch(e => console.error('error'));</code></pre>
<p>还可以使用ES7的async/await进一步简化代码:</p>
<pre><code class="JavaScript">try {
let response = await fetch(url);
let data = response.json();
console.log(data);
} catch(e) {
console.log('error');
}</code></pre>
<p>这样,异步的请求可以按照同步的写法来写了。</p>
<h2>Fetch修改head信息</h2>
<p>fetch方法中还有第二个参数,第二个参数是用于修改请求的Head信息的。可以在里面指定method是GET还是POST;如果是跨域的话,可以指定mode为cors来解决跨域问题。</p>
<pre><code class="JavaScript">var headers = new Headers({
"Origin": "http://taobao.com"
});
headers.append("Content-Type", "text/plain");
var init = {
method: 'GET',
headers: headers,
mode: 'cors',
cache: 'default'
};
fetch(url, init).then(response => response.json())
.then(data => console.log(data))
.catch(e => console.error('error'));</code></pre>
Web Worker
https://segmentfault.com/a/1190000004369037
2016-01-26T19:30:23+08:00
2016-01-26T19:30:23+08:00
chenhao_ch
https://segmentfault.com/u/chenhao_ch
3
<h2>介绍</h2>
<p>大家都知道,Javascript是单线程的,所以如果页面中的Javascript有大量计算的话,很容易阻塞页面的动画或者交互响应。<br>HTML5中的Web Worker就使Javascript的多线程编程成为可能。</p>
<h2>使用</h2>
<p>Web Worker是一段运行在后台的Javascript代码,不会影响页面性能。我们可以通过<code>new Worker</code>来创建一个Web Worker。</p>
<pre><code class="Javascript">// 创建Web Worker
var worker = new Worker("task.js");
// 向Web Worker传递消息
worker.postMessage( {
index: 1,
msg: 'Hello task'
} );
//设置postMessage的监听
worker.onmessage = function(message) {
var data = message.data;
console.log(JSON.stringify(data));
// terminate 方法用于停止worker的继续运行
worker.terminate();
};
worker.onerror = function(error){
worker.terminate();
console.log(error.filename, error.lineno, error.message);
}</code></pre>
<p>下面是task.js的代码</p>
<pre><code class="Javascript">onmessage = function(message) {
var data = message.data;
data.msg = 'Hello main';
postMessage(data);
}</code></pre>
<p>从上面的例子中,可以看出,Web Worker与主页面之间通过postMessage来完成通信。</p>
<h2>总结</h2>
<p>在Web Worker中,没有window,document,DOM等对象。但是可以使用navigator,location,XMLHttpRequest等对象。<br>上面的限制导致了Web Worker一般用于有耗时较长的业务中,比如有大量计算的页面。</p>
微数据
https://segmentfault.com/a/1190000004267041
2016-01-07T17:20:48+08:00
2016-01-07T17:20:48+08:00
chenhao_ch
https://segmentfault.com/u/chenhao_ch
0
<h2>微数据是什么</h2>
<p>一个页面的数据,不单单是给用户看的,其中的一些数据还要提供给机器去识别。由于机器的智能水平有限,所以就出现了微数据这种技术,专门用于显示机器识别的数据。</p>
<h2>微数据有什么用</h2>
<p>知道了微数据是提供给机器识别的数据,那么微数据有什么用呢?微数据可以给节点提供额外的数据,类似于<code>data-*</code>属性,但是<code>data-*</code>只是纯粹的属性,不含有任何其他意义。页面节点添加了微数据后,机器就可以准确识别页面元素所代表的信息,比如大众点评页面有用户评价信息,那么将评价节点添加微数据后,搜索引擎就能够识别,并应用到搜索结果中;比如一个用户介绍页面,添加了微数据后,机器就能识别里面的具体信息,导出为名片。</p>
<h2>如何书写微数据</h2>
<p>微数据的属性有<code>itemscope</code>, <code>itemprop</code>, <code>itemtype</code>, <code>itemid</code>。</p>
<ul>
<li><p><code>itemscope</code>定义一组项,项中有多个键值对。</p></li>
<li><p><code>itemprop</code>定义一个键值对。值可以说URL,或者文字</p></li>
<li><p><code>itemtype</code>定义类型,值为URL</p></li>
<li><p><code>itemid</code>定义一个全局标识符,比如ISBN,身份证<br>当页面需要显示一本书的信息时,可以这样去写HTML:</p></li>
</ul>
<pre><code class="HTML"><div itemscope itemtype="http://vocab.example.com/book" itemid="URN:ISBN:9787115275790">
<span itemprop="name">JavaScript高级程序设计(第3版)</span>
<span itemprop="author">Nicholas C. Zakas</span>
</div></code></pre>
<p>上面的例子中,指定了该节点是一个book类型,isbn是9787115275790,书的名字是《JavaScript高级程序设计(第3版)》,作者是Nicholas C. Zakas。<br>有一个页面可以将微数据转化为json格式的机器识别数据,地址是: <a href="https://link.segmentfault.com/?enc=2WP3VGutQVBjlw5C6%2BJBiA%3D%3D.pYEjRd2g5oGh%2BMKdQE2%2F05VmRxvcbd8tBMXpNkd459fhriB6A5Szaivr5LjIf9yY" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=focVg4%2BdPjPI3unCpKtoww%3D%3D.dpxAQ5FouxupfTQPCGu9O16EsF9GJQWhezWw6O7%2BH4DrgxB1o9KzDW%2F2uAzQkZmD" rel="nofollow">https://foolip.org/microdatajs/live/</a>。</p>
<h2>写在最后</h2>
<p>微数据目前浏览器的兼容还不好,对页面展示也不存在影响,普通业务都不会使用。但是在一些特殊的业务场景下,可以很好的提升用户体验。</p>
image的srcset属性
https://segmentfault.com/a/1190000004254111
2016-01-05T19:50:40+08:00
2016-01-05T19:50:40+08:00
chenhao_ch
https://segmentfault.com/u/chenhao_ch
1
<h2>介绍</h2>
<p>响应式页面中经常用到根据屏幕密度设置不同的图片。这个时候肯定会用到image标签的srcset属性。srcset属性用于设置不同屏幕密度下,image自动加载不同的图片。用法如下:</p>
<pre><code class="html"><img src="image-128.png" srcset="image-256.png 2x" /></code></pre>
<p>使用上面的代码,就能实现在屏幕密度为1x的情况下加载image-128.png, 屏幕密度为2x时加载image-256.png。</p>
<h2>新标准</h2>
<p>按照上面的实现,不同的屏幕密度都要设置图片地址,目前的屏幕密度有1x,2x,3x,4x四种,如果每一个图片都设置4张图片的话,太麻烦了。所以就有了新的srcset标准。代码如下:</p>
<pre><code class="html"><img src="image-128.png"
srcset="image-128.png 128w, image-256.png 256w, image-512.png 512w"
sizes="(max-width: 360px) 340px, 128px" /></code></pre>
<p>其中srcset指定图片的地址和对应的图片质量。sizes用来设置图片的尺寸零界点。<br>对于srcset里面出现了一个w单位,可以理解成图片质量。如果可视区域小于这个质量的值,就可以使用,当然,浏览器会自动选择一个最小的可用图片。<br>sizes语法如下:</p>
<pre><code>sizes="[media query] [length], [media query] [length] ... "</code></pre>
<p>上面例子中的sizes就是指默认显示128px, 如果视区宽度大于360px, 则显示340px。</p>
<h2>总结</h2>
<p>img的srcset属性方便的解决了页面图片适应不同屏幕密度的情况。目前除了IE没有兼容到,已经全部都兼容了,可以放心使用。</p>
伪类与伪元素
https://segmentfault.com/a/1190000004246691
2016-01-04T20:15:53+08:00
2016-01-04T20:15:53+08:00
chenhao_ch
https://segmentfault.com/u/chenhao_ch
1
<h2>介绍</h2>
<p>伪类和伪元素是css中很常见的两个概念,利用的好,能够很方便的实现一些特殊效果。伪类与伪元素由于用法相近,导致平时使用并不能很好的区分这两个概念。<br>伪类(<code>pseudo-classes</code>): 伪类类似于class,用于对已有元素增加特殊状态,比如<code>:hover</code>, <code>:active</code>, <code>:lang</code>, <code>:first-child</code>等。<br>伪元素(<code>pseudo-elements</code>):伪元素类似于element,用于在DOM中增加一个特殊的element节点,比如<code>:after</code>, <code>:before</code>, <code>:first-line</code>等。</p>
<h2>使用</h2>
<h3>伪类</h3>
<p>伪类包括<code>:hover</code>, <code>:active</code>, <code>:link</code>, <code>:focus</code>, <code>:visited</code>, <code>:lang</code>, <code>:first-child</code>。其中每一个的作用如下图:<br><img src="/img/bVrYU0" alt="图片描述" title="图片描述"><br>因为伪类在项目中经常使用,这里不做demo 。</p>
<h3>伪元素</h3>
<p>伪元素包括:first-line, :first-letter, :before, :after。具体作用见下图<br><img src="/img/bVrYU1" alt="图片描述" title="图片描述"><br>:first-line对元素第一行设置特殊样式。相当于将元素第一行看作是一个独立的元素进行设置样式。<br>:first-letter对元素第一个字进行设置样式。相当于将元素第一个字当作一个独立的元素进行样式设置。</p>
<pre><code class="css">p:first-letter {
font-size: 28px;
}
p:first-line {
color: red;
}</code></pre>
<p>效果如图:<br><img src="/img/bVrYU3" alt="图片描述" title="图片描述"><br>:before在元素前添加一个元素。<br>:after在元素后添加一个元素。<br>有一些效果会有一个小尖头,可以很方便的用:before,:after来实现。</p>
<pre><code class="css">.box:before {
content: 'before';
font-size: 12px;
display: inline-block;
width: 21px;
height: 21px;
border: 1px solid red;
border-right: 0;
border-top: 0;
transform: rotate(45deg);
position: relative;
top: 2px;
left: -20px;
}
.box:after {
content: 'after';
font-size: 12px;
display: inline-block;
width: 21px;
height: 21px;
border: 1px solid red;
border-left: 0;
border-bottom: 0;
transform: rotate(45deg);
position: relative;
top: 3px;
left: 20px;
}</code></pre>
<p><img src="/img/bVrYU5" alt="图片描述" title="图片描述"></p>
CORS跨域资源共享
https://segmentfault.com/a/1190000004070442
2015-11-30T14:43:06+08:00
2015-11-30T14:43:06+08:00
chenhao_ch
https://segmentfault.com/u/chenhao_ch
4
<h2>介绍</h2>
<p>CORS(Cross-Origin Resource Sharing)是指跨域资源共享,用于解决前端跨域问题。跨域问题最参见的方法就是使用<code>JSONP</code>,但是很多跨域问题<code>JSONP</code>是无法解决的,比如</p>
<ol>
<li><p>POST跨域请求</p></li>
<li><p><code>'script error'</code>的脚本错误提示</p></li>
<li><p><code>canvas</code>中无法获得跨域图片的信息<br>如果使用<code>CORS</code>,上面几个问题都可以解决。</p></li>
</ol>
<h2>CORS的原理</h2>
<p>服务端对<code>header</code>设置一个<code>Access-Control-Allow-Origin: *</code>,开启跨域请求。<code>*</code>表示接受所有域名的请求。也可以指定特定的域名 <code>Access-Control-Allow-Origin: http://www.client.com</code>。<br>我专门整理了一个使用<code>CORS</code>解决跨域的例子。可以去<a href="https://link.segmentfault.com/?enc=yNofNzcE1MIbod2xxxHGxw%3D%3D.p96XPdH9xSCr%2F4LnrZx6eloNm5yLMEZLhmt9BExr21f4f0kg%2Fhz3PfhV7Y71wom8fyDFq7KWKRwU%2BR816OS%2FEw%3D%3D" rel="nofollow">github</a>上下载。<br>在<code>github</code>的例子中,需要设置<code>host</code>, 将<code>www.client.com</code>和<code>www.server.com</code>都指向本地。然后使用本地服务器来测试。</p>
<pre><code>127.0.0.1 www.client.com
127.0.0.1 www.server.com</code></pre>
<p>其中<code>www.client.com</code>是指当前页面的域名,<code>www.server.com</code>是第三方的域名。</p>
<h2>CORS解决普通跨域请求</h2>
<p>github的例子中,<code>scripts/client.js</code>中对<code>www.server.com</code>域名发送<code>ajax</code>请求。当<code>www.server.com</code>没有对<code>header</code>的<code>Access-Control-Allow-Origin</code>进行设置时,请求是报错的。如图:</p>
<p><img src="/img/bVre1o" alt="ajax跨域请求报错" title="ajax跨域请求报错"></p>
<p>设置<code>Access-Control-Allow-Origin</code>后,可以正常获得数据。如下图,第一张是返回的结果,第二张是<code>headers</code>信息。</p>
<p><img src="/img/bVre4b" alt="clipboard.png" title="clipboard.png"></p>
<p><img src="/img/bVre4c" alt="clipboard.png" title="clipboard.png"></p>
<h2>CORS解决跨域脚本报错问题</h2>
<p>同源策略影响到的不仅仅是<code>ajax</code>请求跨域接口,对跨域资源(包括js,图片等)也会有限制。比如<code>www.client.com</code>域名下页面请求了一个<code>www.server.com</code>域名下的<code>js</code>资源<code>server.js</code>,<code>js</code>资源可以被正常加载和运行。但是当<code>js</code>资源中发生错误,有<code>error</code>抛出时,<code>www.client.com</code>中是无法知道<code>error</code>的细节的。<code>demo</code>中<code>index.html</code>页面对<code>onerror</code>进行了处理,输出错误信息。但是当跨域资源<code>www.server.com/demo/cors/scripts/server.js</code>中抛出异常时,只能显示<code>'script error'</code>信息。如图:</p>
<p><img src="/img/bVre4i" alt="clipboard.png" title="clipboard.png"></p>
<p>要支持跨域脚本<code>error</code>信息的输出,需要两步:</p>
<ol>
<li><p>服务器对<code>js</code>资源的<code>header</code>做<code>Access-Control-Allow-Origin</code>的设置</p></li>
<li><p><code>script</code>标签中添加属性<code>crossorigin="anonymous"</code><br>加上上面的逻辑后,错误信息就可以正常输出了:</p></li>
</ol>
<p><img src="/img/bVre4m" alt="clipboard.png" title="clipboard.png"></p>
<h2>CORS解决跨域图片信息获取</h2>
<p>图片作为一种资源,也会受到同源策略的影响。比如,<code>www.client.com</code>域名下的页面,通过<code>canvas</code>绘制了一个<code>www.server.com</code>域名的图片,当使用<code>canvas</code>的<code>toBlob()</code>, <code>toDataURL()</code>, <code>getImageData()</code>时,会有<code>error</code>发生。如图:</p>
<p><img src="/img/bVre4n" alt="clipboard.png" title="clipboard.png"></p>
<p>如果对图片的<code>header</code>设置了<code>Access-Control-Allow-Origin</code>,就可以正常调用这些方法。</p>
HTML5幻灯片库reveal.js使用
https://segmentfault.com/a/1190000004055732
2015-11-27T20:14:45+08:00
2015-11-27T20:14:45+08:00
chenhao_ch
https://segmentfault.com/u/chenhao_ch
15
<h2>介绍</h2>
<p>最近在准备一个团队内部的分享,觉得<code>powerpoint</code>写幻灯片太麻烦,效率太低。作为前端,就想到是否可以使用页面来做幻灯片。于是就去搜索了下有没有HTML5实现幻灯片的工具。经过对比,最后选择了了<code>reveal.js</code>来实现幻灯片。<br><code>reveal.js</code>是一个用于实现幻灯片效果的库。使用该库。<br>github地址:<a href="https://link.segmentfault.com/?enc=9x6Erh0xXQw5gnxDbhDuuw%3D%3D.Fq3jLNFWqpNEY%2BHrbSt0uEICj5eUFdKjXi5gdrCDhEMZWIROFdVrgkd3KqA6sDhY" rel="nofollow">github</a><br>提供了页面编辑功能:<a href="https://link.segmentfault.com/?enc=NPvBaHhHOzHLTbu5g%2F1mpw%3D%3D.tN27X2ayxrLYUY6K7IUeHuJIDdP9JHIHIV7rQClTUgQ%3D" rel="nofollow">slides.com</a><br>官方demo: <a href="https://link.segmentfault.com/?enc=jfX1IGh%2FZUv6wONQkqnW%2FA%3D%3D.ddvZoIyxbXiOwX3nyYABQeHE3kImzXDt0XXQEeci1pzpsoYpbso%2FYfo2hGDX%2BMC%2B" rel="nofollow">demo</a><br>我自己做的PPT地址:<a href="https://link.segmentfault.com/?enc=drPCSTtvStuOHpUGxaADAw%3D%3D.uxOkrOzarFQXlR59ObkJSkGAM3iVw50ArRwKkRUDC28GP9J3r9KTgtzvEaY%2BpKcphwaX0y25wGRV145yK7smfA%3D%3D" rel="nofollow">vuejs-ppt</a></p>
<h2>特点</h2>
<p><code>reveal.js</code>有一下几个特点:</p>
<ul>
<li><p>支持标签来区分每一页幻灯片</p></li>
<li><p>可以使用markdown来写内容</p></li>
<li><p>支持pdf的导出</p></li>
<li><p>支持演说注释</p></li>
<li><p>提供JavaScript API来控制页面</p></li>
<li><p>提供了多个默认主题和切换方式</p></li>
</ul>
<h2>幻灯片实现步骤</h2>
<ol>
<li><p>从<a href="https://link.segmentfault.com/?enc=TPI%2FZF%2B%2FcJiSd%2FsTw7pikA%3D%3D.GHDwahZHJ3mlysQI6JdzFobYK4Te%2B9BjG%2F1CzOQDqGax2Vj46wsbbxnTq5dBwL3r" rel="nofollow">reveal.js</a>上下载压缩包,并解压</p></li>
<li><p>进入<code>reveal.js</code>文件夹,直接修改<code>index.html</code>文件就可以</p></li>
<li><p>编辑后好,打开页面就可以看到PPT的内容。 按下<code>S</code>键,会打开时间,下一张PPT,Notes等信息的页面,方便演示PPT</p></li>
</ol>
<h2>幻灯片内容实现方法</h2>
<p>幻灯片的内容需要包含在<code><div class="reveal"> <div class="slides"></code>的标签中。<br>一个<code>section</code>是一页幻灯片。当<code>section</code>包含在<code>section</code>中时,是一个纵向的幻灯片。<br>怎么理解呢? 可以这样理解:横向的幻灯片代表一章,纵向的幻灯片代表一章中的一节。那么横向的幻灯片在播放时是左右切换的,而纵向的幻灯片是上下切换的。For example:</p>
<pre><code class="html"><div class="reveal">
<div class="slides">
<section>Single Horizontal Slide</section>
<section>
<section>Vertical Slide 1</section>
<section>Vertical Slide 2</section>
</section>
</div>
</div></code></pre>
<h3>html实现内容</h3>
<h4>标题和正文</h4>
<p><code>section</code>中的内容就是幻灯片的内容,你可以使用<code>h2</code>标签标示<code>title</code>, <code>p</code>表示内容。需要红色的字体可以直接设置<code>style</code>的<code>color</code>为<code>red</code>。<br>当某一页需要特殊背景色,可以使用<code>data-background</code>在<code>section</code>上设置, <code>data-background-transition</code>表示背景的过渡效果。For example:</p>
<pre><code class="html"><section data-background-transition="zoom" data-background="#dddddd"></code></pre>
<p>如果需要正文一段一段出现。可以使用<code>fragment</code>。For Example:</p>
<pre><code class="html"><p class="fragment"></p></code></pre>
<h4>代码</h4>
<p><code>reveal.js</code>使用<code>highlight.js</code>来支持代码高亮。可以直接写code标签来实现, <code>data-trim</code>表示去除多余的空格。For Example:</p>
<pre><code class="html"><pre><code data-trim>
console.log('hello reveal.js!');
</code></pre></code></pre>
<h4>注释</h4>
<p>在演说时,会用到注释,对于注释,可以通过<aside class="notes">来实现。For Example:</p>
<pre><code class="html"><aside class="notes">
这里是注释。
</aside></code></pre>
<p>在幻灯片页面,按下<code>s</code>键,就可以调出注释页面,注释页面包含了当前幻灯片,下一章幻灯片,注释,以及幻灯片播放时间。</p>
<h3>markdown实现</h3>
<p><code>reveal.js</code>不仅支持html表示来实现内容, 还可以通过<code>markdown</code>来实现内容。使用<code>markdown</code>实现内容时,需要对<code>section</code>标示添加<code>data-markdown</code>属性,然后将<code>markdown</code>内容写到一个<code>text/template</code>脚本中。For Example:</p>
<pre><code class="html"><section data-markdown>
<script type="text/template">
## Page title
A paragraph with some text and a [link](http://hakim.se).
</script>
</section></code></pre>
<p>背景色,<code>fragment</code>功能的实现,可以通过注释来实现。For Example:</p>
<pre><code class="html"><section data-markdown>
<script type="text/template">
<!-- .slide: data-background="#ff0000" -->
- Item 1 <!-- .element: class="fragment" data-fragment-index="2" -->
- Item 2 <!-- .element: class="fragment" data-fragment-index="1" -->
</script>
</section></code></pre>
<h4>外置md文件</h4>
<p><code>reveal.js</code>可以引用一个外置的<code>markdown</code>文件来解析。For Example:</p>
<pre><code class="html"><section data-markdown="example.md"
data-separator="^\n\n\n"
data-separator-vertical="^\n\n"
data-separator-notes="^Note:"
data-charset="iso-8859-15">
</section></code></pre>
<h4>分页实现</h4>
<p>一个<code>markdown</code>文件中可以连续包含多个章内容。可以在<code>section</code>中通过属性<code>data-separator</code>, <code>data-separator-vertical</code>来划分章节。For Example:</p>
<pre><code class="html"><section data-separator="---" data-separator-vertical="--" >
<script type="text/template">
# 主题1
- 主题1-内容1
- 主题1-内容2
--
## 主题1-内容1
内容1-细节1
--
## 主题1-内容2
内容1-细节2
---
# 主题2
</script>
</section></code></pre>
<h4>注释</h4>
<p>对<code>section</code>添加 <code>data-separator-notes="^Note:"</code>属性,就可以指定<code>Note:</code>后面的内容为当前幻灯片的注释。For Example:</p>
<pre><code class="html"># Title
## Sub-title
Here is some content...
Note:
This will only display in the notes window.</code></pre>
<h2>PDF导出</h2>
<p>可以利用浏览器保存为pdf的功能来实现pdf的转化。步骤是</p>
<ol>
<li><p>再url后面添加<code>print-pdf</code>参数,访问后,页面会去加载打印用的css文件,页面效果就是pdf的样式。</p></li>
<li><p>右键选择打印。设置为保存pdf。</p></li>
</ol>
<blockquote><p>我试过保存pdf的功能,有内容会重叠,怀疑是样式没有处理好。</p></blockquote>
<h2>多主题</h2>
<p><code>reveal.js</code>提供了多种样式。可以通过引用不同的主题来实现。具体主题查看<code>reveal.js/css/theme</code>下的css文件。</p>
<h2>总结</h2>
<p>用<code>reveal.js</code>来实现幻灯片,可以只关注内容,忽略各种切换效果。而且可以使用<code>markdown</code>来实现,大大提高了编写效率。对于最后生成的html文件,可以部署到服务器,这样只需要网络就可以进行分享,不需要使用U盘拷来拷去了。</p>
Vue.js基本语法的介绍
https://segmentfault.com/a/1190000004012600
2015-11-18T16:57:26+08:00
2015-11-18T16:57:26+08:00
chenhao_ch
https://segmentfault.com/u/chenhao_ch
5
<h2>介绍</h2>
<p>前段时间接触到一个库叫做Vue.js, 个人感觉很棒,所以整理了一篇博文做个介绍。<br>Vue读音/vju:/,和view类似。是一个数据驱动的web界面库。Vue.js只聚焦于视图层,可以很容易的和其他库整合。代码压缩后只有24kb。<br>可以去<a href="https://link.segmentfault.com/?enc=NYPCjmluHubTGXZoKr1OPQ%3D%3D.yeKpmcL66qmbclypR6oUqn4qXdSXoIwU9PFeXu37JGE%3D" rel="nofollow">这里下载</a>。自己整理了一个Vue.js的demo,<a href="https://link.segmentfault.com/?enc=4kmGI%2BlVecwOswiDnoPcCg%3D%3D.fZ76TRXBgtwoiQC8mLtn%2Fl3lcfQT5iBPmychcaqrJgupRdkqMQkJfZhGSAfKDLOU" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=6j7guYC5V1M77nJan2FOFQ%3D%3D.xR49Vt8fajMoyBM%2FNFE%2B1iTMuUosQvFXfs2UqgvgulZGcgHku4nNzmcrSqM5s6yB" rel="nofollow">https://github.com/chenhao-ch/demo-vue</a></p>
<h3>快速入门</h3>
<p>以下代码是Vue.js最简单的例子, 当 <code>input</code> 中的内容变化时,<code>p</code> 节点的内容会跟着变化。</p>
<pre><code class="html"><!-- html -->
<div id="demo">
<p>{{message}}</p>
<input v-model="message">
</div></code></pre>
<pre><code class="javascript">new Vue({
el: '#demo',
data: {
message: 'Hello Vue.js!'
}
})</code></pre>
<h2>语法介绍</h2>
<h3>数据绑定</h3>
<p>数据绑定就是指将js中的变量自动更新到html中。如下代码, message的默认值是“Hello Vue.js!”, 那么当页面启动时,html中的默认值会被设置成“Hello Vue.js”</p>
<pre><code class="html"><!-- html -->
<div id="demo">
<p>{{message}}</p>
<input v-model="message">
</div></code></pre>
<pre><code class="javascript">new Vue({
el: '#demo',
data: {
message: 'Hello Vue.js!'
}
})</code></pre>
<p>如果要输出原生的html,可以使用三个大括号来实现</p>
<pre><code class="html"><p>{{{messageHtml}}}</p></code></pre>
<p>也可以做表达式的绑定</p>
<pre><code class="html"><div>{{length - 1}}</div>
<div>{{isShow ? 'block' : 'none'}}</div></code></pre>
<h3>过滤器</h3>
<p>表达式后面可以添加过滤器,对输出的数据进行过滤。</p>
<pre><code class="html"><div>{{ message | capitalize }}</div></code></pre>
<h4>自定义过滤器</h4>
<p>Vue.js运行自己定义过滤器。比如:</p>
<pre><code class="javascript">Vue.filter('wrap', function (value, begin, end) {
return begin + value + end;
})</code></pre>
<pre><code class="html"><!-- 'vue' => 'before vue after' -->
<span>{{ message | wrap 'before' 'after' }}</span></code></pre>
<h3>指令</h3>
<p>指令是特殊的带有前缀 <code>v-</code> 的特性。当表达式的值发生变化时,响应应用特定的行为到DOM。</p>
<pre><code class="html"><!-- 当greeting为true时,才显示p节点 -->
<p v-if="greeting">hello</p>
<!-- 绑定href属性为js中url的值 -->
<a v-bind:href="url"></a>
<!-- 绑定事件,btnClick是js方法 -->
<button v-on:click="btnClick"></button></code></pre>
<p><code>bind</code> , <code>on</code> 指令可以进行缩写</p>
<pre><code class="html"><a v-bind:href="url"></a>
<a :href="url"></a>
<button v-on:click="btnClick"></button>
<button @click="btnClick"></button></code></pre>
<h4>自定义指令</h4>
<pre><code class="js">Vue.directive('demo', {
bind: function () {
// 准备工作
// 例如,添加事件处理器或只需要运行一次的高耗任务
},
update: function (newValue, oldValue) {
// 值更新时的工作
// 也会以初始值为参数调用一次
},
unbind: function () {
// 清理工作
// 例如,删除 bind() 添加的事件监听器
}
})</code></pre>
<h3>html模板</h3>
<p>Vue.js支持对js对象进行判断(if), 循环(for)输出。类似于前端模板。</p>
<pre><code class="html"><!-- 判断,如果ok为true,则显示yes, 如果为false, 显示no -->
<h1 v-if="ok">Yes</h1>
<h1 v-else>No</h1>
<!-- 类似于v-if, v-if是是否加节点, v-show是display是否为none -->
<h1 v-show="ok">Hello!</h1>
<!-- 循环, 对items属性进行循环。 track-by指item的是否重复,对重复的进行服用 -->
<!-- 循环中, $index表示数组第n个元素; $key表示对象的key -->
<ul id="example-1">
<li v-for="item in items" track-by="_uid">
{{ $index }} : {{ item.message }}
</li>
</ul></code></pre>
<pre><code class="javascript">var example1 = new Vue({
el: '#example-1',
data: {
items: [
{_uid: '1', message: 'Foo' },
{_uid: '2', message: 'Bar' }
]
}
})</code></pre>
<h3>样式绑定</h3>
<p>样式也可以根据js中的变量来动态确定。</p>
<pre><code class="html"><!-- isA 为true时, class多一个class-a -->
<div class="static" v-bind:class="{ 'class-a': isA, 'class-b': isB }"></div>
<!-- classA, classB 两个变量的值设置成class -->
<div v-bind:class="[classA, classB]">
<!-- 绑定style, 自动添加前缀,styleObject里面是style键值对 -->
<div v-bind:style="styleObject"></div></code></pre>
<h3>事件绑定</h3>
<p>可以使用 <code>v-on</code> 指令来监听DOM事件。</p>
<pre><code class="html"><div id="example-2">
<button v-on:click="say('hi', $event)">Say Hi</button>
<button v-on:click="say('what', $event)">Say What</button>
</div></code></pre>
<pre><code class="javascript">new Vue({
el: '#example-2',
methods: {
say: function (msg, event) {
alert(msg);
event.preventDefault();
}
}
})</code></pre>
<p>常见的阻止冒泡,禁止默认行为等event方法可以通过修饰符来快速处理。</p>
<pre><code class="html"><!-- 禁止冒泡 -->
<a v-on:click.stop='do'></a>
<!-- 禁止冒泡和默认行为 -->
<a @click.stop.prevent="do"></a></code></pre>
<p>对特殊按键生效也可以使用修饰符</p>
<pre><code class="html"><!-- keyCode是13的时候出发。 -->
<input v-on:keyup.13="submit" />
<input v-on:keyup.enter="submit" />
<!-- 支持的键名有: enter,tab,delete,esc,space,up,down,left,right --></code></pre>
<h3>组件</h3>
<p>组件系统是 Vue.js 另一个重要概念,因为它提供了一种抽象,让我们可以用独立可复用的小组件来构建大型应用。</p>
<h4>注册</h4>
<p>通过<code>Vue.extend()</code>来定义一个组件,<code>Vue.component()</code>来注册组件。</p>
<pre><code class="html"><div id="box">
<tree></tree>
</div></code></pre>
<pre><code class="javascript">// 定义
var Tree = Vue.extend({
template: '<div>This is a tree!</div>'
});
// 注册
Vue.component('tree', Tree);
// 开始渲染
new Vue({
el: '#box'
});
// 定义,注册可以合并成一步。下面的代码和上面一样
Vue.component('tree', {
template: '<div>This is a tree!</div>'
});
new Vue({
el: '#box'
});</code></pre>
<p>渲染结果为:</p>
<pre><code class="html"><div id="box">
<div>This is a tree!</div>
</div></code></pre>
<p>还可以进行局部注册</p>
<pre><code class="javascript">var Child = Vue.extend({ /* ... */ })
var Parent = Vue.extend({
template: '...',
components: {
'my-component': Child
}
})</code></pre>
<h4>props</h4>
<p>props用于父组件向子组件传递数据。</p>
<pre><code class="javascript">Vue.component('child', {
props: ['childMsg'],
// prop 可以用在模板内
// 可以用 `this.msg` 设置
template: '<span>{{ childMsg }}</span>'
});</code></pre>
<pre><code class="html"><child child-msg="hello"></child></code></pre>
<p>动态props, 当父组件的数据变化时,需要通知子组件跟着变化。</p>
<pre><code class="html"><input v-model="parentMsg" />
<child v-bind:child-msg="parentMsg"></child></code></pre>
<h4>父子组件通信</h4>
<p>当父组件数据变化时,可以通过props来通知子组件,子组件状态变化时,可以利用事件的冒泡来通知父组件。<br>子组件可以用 <code>this.$parent</code> 访问它的父组件。父组件有一个数组 <code>this.$children</code>,包含它所有的子元素。<br>例子:</p>
<pre><code class="html"><!-- 子组件模板 -->
<template id="child-template">
<input v-model="msg">
<button v-on:click="notify">Dispatch Event</button>
</template>
<!-- 父组件模板 -->
<div id="events-example">
<p>Messages: {{ messages | json }}</p>
<child></child>
</div></code></pre>
<pre><code class="javascript">// 注册子组件
// 将当前消息派发出去
Vue.component('child', {
template: '#child-template',
data: function () {
return { msg: 'hello' }
},
methods: {
notify: function () {
if (this.msg.trim()) {
this.$dispatch('child-msg', this.msg) // 触发child-msg事件
this.msg = ''
}
}
}
})
// 启动父组件
// 将收到消息时将事件推入一个数组
var parent = new Vue({
el: '#events-example',
data: {
messages: []
},
// 在创建实例时 `events` 选项简单地调用 `$on`
events: {
'child-msg': function (msg) { // 监听到 child-msg事件
// 事件回调内的 `this` 自动绑定到注册它的实例上
this.messages.push(msg) // messages改变自动修改html内容
}
}
})</code></pre>
<p>上面这种写法child-msg事件触发后,会冒泡到父组件,并执行父组件的<code>child-msg</code> events。<br>但是上面的的执行流程不够直观,可以在html中通过指定on事件来实现event的监听。下面是全部代码:</p>
<pre><code class="html"><template id="child-template">
<input v-model="msg">
<button v-on:click="notify">Dispatch Event</button>
</template>
<!-- 父组件模板 -->
<div id="events-example">
<p>Messages: {{ messages | json }}</p>
<!-- 当child-msg触发时, 执行父组件的handleIt方法。 -->
<child v-on:child-msg="handleIt"></child>
</div>
<script>
// 注册子组件
// 将当前消息派发出去
Vue.component('child', {
template: '#child-template',
data: function () {
return { msg: 'hello' }
},
methods: {
notify: function () {
if (this.msg.trim()) {
this.$dispatch('child-msg', this.msg) // 触发child-msg事件
this.msg = ''
}
}
}
});
// 启动父组件
// 将收到消息时将事件推入一个数组
var parent = new Vue({
el: '#events-example',
data: {
messages: []
},
methods: {
handleIt: function(msg) {
this.messages.push(msg);
}
}
})
</script>
</code></pre>
<h2>构建大型应用</h2>
<p>在典型的 Vue.js 项目中,我们会把界面拆分为多个小组件,每个组件在同一地方封装它的 CSS 样式,模板和 JavaScript 定义,这么做比较好。如上所述,使用 Webpack 或 Browserify 以及合适的源码转换器,我们可以这样写组件:<br><img src="http://cn.vuejs.org/images/vue-component.png" alt="vue文件" title="vue文件"><br>当然也可以使用预处理器,<br><img src="http://cn.vuejs.org/images/vue-component-with-pre-processors.png" alt="vue预处理文件" title="vue预处理文件"></p>
函数节流
https://segmentfault.com/a/1190000002701805
2015-04-23T14:12:41+08:00
2015-04-23T14:12:41+08:00
chenhao_ch
https://segmentfault.com/u/chenhao_ch
6
<h2>函数节流介绍</h2>
<p>页面在绑定resize,keydown或者mousemove这些能连续触发的事件时,用户只要很常规的操作,就能连续触发多次绑定的方法。当绑定方法里面存在大量的类似于DOM操作这种极其消耗性能的代码时,会直接导致页面运行的卡顿。这个时候就会用到函数节流。</p>
<h2>函数节流的实现</h2>
<p>函数节流最普通的实现就是通过取摩操作来过滤部分执行。参考代码如下</p>
<pre><code>javascript</code><code>var mousemoveCount = 0;
function mousemoveListener(e){
mousemoveCount++;
if(mousemoveCount % 2 === 0){
return;
}
console.info('业务逻辑');
}
</code></pre>
<p>当第一次触发并执行mousemoveListener事件时,会打印“业务逻辑”;紧接着第二次执行mousemoveListener事件时,由于mousemoveCount为2,会直接return掉,并不会打印“业务逻辑”。这样子,就实现了函数节流,存在复杂计算的业务逻辑运行次数减半了。</p>
<p>但是这种实现存在两个问题:</p>
<ol>
<li>方法的执行频率(或者说帧率)是不可控的。比如mousemove事件,执行频率由鼠标移动速度决定,由上面这种方式实现,频率还是由鼠标移动速度决定。</li>
<li>最后一次触发可能未执行。比如当最后一次触发事件时,mousemoveCount是偶数,那么会直接return。如果业务需要最后一次必须执行业务逻辑,则会存在bug。</li>
</ol>
<p>所以就有了下面的优化实现(throttle和debounce)。</p>
<h2>throttle实现</h2>
<p>throttle又叫函数节流,思路是控制某一个时间段(执行周期)内触发的事件,只会执行一次业务逻辑。代码如下:</p>
<pre><code>javascript</code><code>var lastMousemoveTime = 0, mousemoveTime = 100;
function mousemoveListener(e){
var now = new Date().getTime();
if(now - lastMousemoveTime <= mousemoveTime) {
return;
}
lastMousemoveTime = now;
setTimeout(function(){
console.info('业务逻辑');
}, mousemoveTime);
}
</code></pre>
<p>第一次触发mousemove会设置100ms后执行业务逻辑,在这之后的100ms里面触发的mousemove都不会触发业务逻辑。相当于控制了mousemove事件100ms触发一次,也就是10帧。</p>
<p>使用这种实现(throttle),可以做到触发频率可控。但当业务希望连续的触发事件只在之后一次触发后才执行业务逻辑,比如resize事件,只希望窗口变化结束后才进行业务逻辑的运行,throttle实现就不适用了。这个时候就需要使用到debounce</p>
<h2>debounce实现</h2>
<p>debounce又叫函数去抖动,思路是业务逻辑在resize不在触发后才执行。代码如下:</p>
<pre><code>javascript</code><code>var resizeTimer = null;
function resizeListener(e){
if(resizeTimer) {
clearTimerout(resizeTimer);
}
resizeTimer = setTimeout(function(){
console.info('业务逻辑');
}, 100);
}
</code></pre>
<p>但resize连续快速触发时,业务逻辑并不会执行。只有当最后一次触发resize后100ms,才执行业务逻辑。这种情况就能实现只在最后一次resize触发业务计算了。</p>
<p>underscore 中已经对throttle和debounce做了实现和封装, 有兴趣可以去查看源码。</p>