SegmentFault 前端随想最新的文章
2020-10-29T15:01:49+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
NodeJs爬虫框架-Spider
https://segmentfault.com/a/1190000037652977
2020-10-29T15:01:49+08:00
2020-10-29T15:01:49+08:00
GeoffZhu
https://segmentfault.com/u/geoffzhu
0
<h2>gz-spider</h2><p>一个基于Puppeteer和Axios的NodeJs爬虫框架 <a href="https://link.segmentfault.com/?enc=zeiigjANjvkjf60vIZDLiw%3D%3D.BsrGlphx0kcPeDjv8aGnXTyMMkSWV3kRkoqBSOdwawAcm%2BqnCunzQNalzRhg195Q" rel="nofollow">源码仓库</a></p><h3>为什么需要爬虫框架</h3><p>爬虫框架可以简化开发流程,提供统一规范,提升效率。一套优秀的爬虫框架会利用多线程,多进程,分布式,IP池等能力,帮助开发者快速开发出易于维护的工业级爬虫,长期受用。</p><h3>特性</h3><ul><li>可配置代理</li><li>支持任务重试</li><li>支持Puppeteer</li><li>异步队列服务友好</li><li>多进程友好</li></ul><h3>安装</h3><pre><code class="bash">npm i gz-spider --save</code></pre><h3>使用</h3><pre><code class="javascript">const spider = require('gz-spider');
// 每个爬虫是一个方法,需要通过setProcesser注册
spider.setProcesser({
['getGoogleSearchResult']: async (fetcher, params) => {
// fetcher.page是原始的puppeteer page,可以直接用于打开页面
let resp = await fetcher.axios.get(`https://www.google.com/search?q=${params}`);
// throw 'Retry', will retry this processer
// throw 'ChangeProxy', will retry this processer use new proxy
// throw 'Fail', will finish this processer with message(fail) Immediately
if (resp.status === 200) {
// Data processing start
let result = resp.data + 1;
// Data processing end
return result;
} else {
throw 'retry';
}
}
});
// 开始爬取
spider.getData('getGoogleSearchResult', params).then(userInfo => {
console.log(userInfo);
});
</code></pre><h3>配置</h3><p>框架由三部分组成,fetcher、strategy、processer。</p><h4>Fetcher</h4><pre><code class="javascript">spider.setFetcher({
axiosTimeout: 5000,
proxyTimeout: 180 * 1000
proxy() {
// 支持返回Promise,可以从远端拉取代理的配置
return {
host: '127.0.0.1',
port: '9000'
}
}
});</code></pre><ul><li><code>axiosTimeout</code>: [Number] 每次爬虫请求的超时时间</li><li><code>proxyTimeout</code>: [Number] 更新代理IP时间,代理IP有超时的场景使用,会重新执行proxy function,使用新的代理IP</li><li><p><code>proxy</code>: [Object | Function] 当 <code>proxy</code>是[Function], 支持异步,可以从远端拉取代理的配置</p><ul><li><code>proxy.host</code> [String]</li><li><code>proxy.port</code> [String]</li></ul></li></ul><h4>Strategy</h4><pre><code class="javascript">spider.setStrategy({
retryTimes: 2
});</code></pre><ul><li><code>retryTimes</code>: [Number] 最大重试次数</li></ul><h3>与任务队列结合使用</h3><h4>流程</h4><pre><code class="javascript">获取任务 -> `spider.getData(processerKey, processerIn)` -> 完成任务并带上处理好的数据</code></pre><h4>用MySql模拟任务队列</h4><ol><li>创建<code>spider-task</code>表, 至少包含<code>'id', 'status', 'processer_key', 'processer_input', 'processer_output'</code></li><li>写一个拉取未完成任务的接口, 例如 <code>GET /spider/task</code></li><li>写一个完成任务的接口, 例如 <code>PUT /spider/task</code></li></ol><pre><code class="javascript">const axios = require('axios');
while (true) {
// 获取任务
let resp = await axios.get('http://127.0.0.1:8080/spider/task');
if (!resp.data.task) break;
let { id, processerKey, processerInput } = resp.data.task;
let processerOutput = await spider.getData(processerKey, processerInput);
// 完成任务并带上处理好的数据
await axios.put('http://127.0.0.1:8080/spider/task', {
id, processerOutput,
status: 'success'
});
}</code></pre><h3>对爬虫的一些理解</h3><p>爬虫的运行方式就决定了它无法做到长久稳定和实时。在设计爬虫框架的时候,围绕的点是异步任务队列。工程上爬虫框架会提供一个高效的数据处理流水线,并可适配多种任务队列。</p><p>gz-spider分为三个组成部分,fetcher,strategy和processer。<br>fetcher抓取器,其中包含常用的http和puppeteer,并且可以挂各种类型的代理。<br>strategy策略中心,负责配置爬取失败后的各种策略。<br>processer负责从原始数据结构处理为目标数据的过程,也是爬虫框架用户要写的部分</p><h3>License</h3><p><a href="https://link.segmentfault.com/?enc=pdoJRFE522yFD8fDk2FRyA%3D%3D.UDZC8EUEt%2FLLtvGZbU5e8DakfRSqaYOCDhzuedQxA1RpJCGQZnXU6TKs6BA3B0yn" rel="nofollow">MIT</a></p>
如何判断web应用是否添加到主屏幕
https://segmentfault.com/a/1190000019172787
2019-05-14T10:44:02+08:00
2019-05-14T10:44:02+08:00
GeoffZhu
https://segmentfault.com/u/geoffzhu
1
<h2>如何判断web应用是否从桌面图标启动</h2>
<p>这就要说到web应用添加到桌面后的显示模式了,一共有这么多种,通过mainfest来控制。只要知道启动模式是什么,就能判断出是否从桌面启动。</p>
<p>fullscreen: 全屏显示, 所有可用的显示区域都被使用, 并且不显示状态栏chrome<br>standalone: 让这个应用看起来像一个独立的应用程序,包括具有不同的窗口,在应用程序启动器中拥有自己的图标等。这个模式中,用户代理将移除用于控制导航的UI元素,但是可以包括其他UI元素,例如状态栏。<br>minimal-ui: 该应用程序将看起来像一个独立的应用程序,但会有浏览器地址栏。 样式因浏览器而异。<br>browser: 这是默认的设置。该应用程序在传统的浏览器标签或新窗口中打开,具体实现取决于浏览器和平台。</p>
<h3>IOS桌面图标启动</h3>
<p>通过桌面图标启动后,<code>navigator.standalone</code>会等于<code>true</code>,只要判断这个变量就够了。</p>
<h3>安卓桌面图标启动</h3>
<p>通过桌面图标启动后,CSS的媒体查询是能够探测到的,换而用js写,下面的结果为<code>True</code>。</p>
<pre><code class="javascript">window.matchMedia('(display-mode: standalone)').matches</code></pre>
<h3>总结</h3>
<p>这里有我实现好的方法,也有<a href="https://link.segmentfault.com/?enc=qv9j6qXvzlw%2F4C6R6H%2BN1Q%3D%3D.ckRtrjQjdYJfTv3KhPCPIKMH%2F15wL4%2Bc5WYGTPbCryqCjCJWlNNQZs5Z1ATLnOxl" rel="nofollow">npm包</a>,引入后可直接用。非常小,非常简单<br><a href="https://link.segmentfault.com/?enc=EA3jzuRWxaxDzPz0odnAoA%3D%3D.4uZXT61EazLmewQyoCsjFCdk5uHl6g07joUrb2rPYnyLLofGeBpbP8%2FIGrsqtdJd" rel="nofollow">https://github.com/GeoffZhu/i...</a></p>
Service Worker一问一答
https://segmentfault.com/a/1190000015999229
2018-08-14T09:42:10+08:00
2018-08-14T09:42:10+08:00
GeoffZhu
https://segmentfault.com/u/geoffzhu
6
<h2>Service Worker</h2>
<blockquote>PWA的核心在于Service Worker,目前中文社区中关于Service Worker的知识深度普遍不够,难以应对实际项目中的问题。例如我想要知道在卸载sw(下文简称sw)后需不需要手动清理caches,搜索引擎是没有什么好答案的。这篇文章结合淘宝首页PWA的经验,分享出我认为非常有价值的关于Service Worker的知识点。</blockquote>
<h4>先从注册说起,sw应该在什么时候注册?</h4>
<p>一些教程是这样注册sw的</p>
<pre><code class="javascript">if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
}</code></pre>
<p>这样做会造成第一个问题,sw线程将加剧对CPU和内存的使用,并且sw内预缓存的资源是需要下载的,移动设备带宽有限,sw线程占用的同时,主进程带宽就变成了小水管了。</p>
<p>首次打开各种资源都非常宝贵,况且是渐进式,完全没有必要争第一次打开页面就要缓存资源。正确的做法是,页面加载完以后sw的事。</p>
<pre><code class="javascript">if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/sw.js');
});
}</code></pre>
<h4>我想注销所有已注册用户的sw,怎么做才最稳妥?</h4>
<p>并不是所有移动端浏览器都支持getRegistrations,getRegistration更靠谱,可以先尝试使用getRegistrations,如果无法使用再尝试getRegistration,如下。</p>
<pre><code class="javascript">var serviceWorker = navigator.serviceWorker;
serviceWorker.getRegistrations ? serviceWorker.getRegistrations().then(function(sws) {
sws.forEach(function(sw) {
sw.unregister();
});
}) : serviceWorker.getRegistration && serviceWorker.getRegistration().then(function(sw) {
sw && sw.unregister();
});</code></pre>
<h4>我注销了sw,之前留下的caches还需要自己动手处理吗?</h4>
<p>需要,cacheStorage虽然属于PWA规范API当中,但它是独立的,虽然注销了service worker,caches里垃圾不清掉,它就会一直留在那里了。这么清</p>
<pre><code class="javascript">window.caches && caches.keys && caches.keys().then(function(keys) {
keys.forEach(function(key) {
caches.delete(key);
});
});</code></pre>
<h4>该不该使用self.clients.claim?</h4>
<p>clients.claim的作用是使当前SW接管已经打开的所有标签页,使用场景是用户首次打开注册sw的页面时,还存在其他同域页面的浏览器标签的情况。之前打开的页面没有被接管,所以通过clients.claim接管已经打开但没受到控制的浏览器标签页面。</p>
<p>skipWaiting的使用场景是在sw更新时,因为有上一个sw正在控制着所有该站点的页面,新的sw在active后进入waiting状态,直到用户将所有该站点页面关闭,新的sw才上位。这跟Chrome和VScode的更新机制一样,在使用过程中有更新的时候,并不影响你继续使用老版本,而是在重启程序后,直接才变成新版。通过skipWaiting方法,可以直接让waiting状态的新sw替换掉老的sw,注意 <strong>还会自动接管上一个sw管辖的页面</strong>。</p>
<p>我是不推荐使用clients.claim的,首先出现不受控标签的情况相对比较少,况且首次加载速度尤其重要,能省点开销就省点吧。</p>
<h4>在sw里监听fetch事件,请求多过了一层sw,会有性能损耗吗?</h4>
<p>当然会,像下面这样搞,是万万不要的</p>
<pre><code class="javascript">self.addEventListener('fetch', event => {
event.respondWith(fetch(event.request));
});</code></pre>
<h4>为什么我在sw中postMessage到页面,页面无法收到message</h4>
<p>这是在测试serviceWorker的postMessage能力时,经常遇到的一个问题,想要找到原因就要从sw接管的页面说起。在sw.js中使用self.clients.matchAll方法获取当前serviceWorker实例所接管的所有标签页,注意是当前实例 <strong>已经接管的</strong>,并且sw.js中的代码只会执行一次,当sw.js代码中存在如下代码时</p>
<pre><code class="javascript">self.clients.matchAll()
.then(function (clients) {
clients.forEach(client => {
client.postMessage('这条消息不会被收到');
})
});</code></pre>
<p>clients一定是个空数组,所以永远也postMessage不到页面。那要如何才能在首次install就postMessage到页面上那?答案是self.skipWaiting,然后在activate事件中使用self.clients.matchAll,因为调用了skipWaiting,当前的sw在install以后会立刻avtivate并接管上一个sw的所有标签页,这样就能在新sw中拿到标签页postMessage了</p>
<pre><code class="javascript">self.skipWaiting()
self.addEventListener('activate', () => {
self.clients.matchAll()
.then(function (clients) {
clients.forEach(client => {
client.postMessage('skipWaiting让新的sw接管了页面,这样就可以收到');
})
});
})</code></pre>
5分钟实现一个Koa
https://segmentfault.com/a/1190000015153981
2018-06-03T19:25:42+08:00
2018-06-03T19:25:42+08:00
GeoffZhu
https://segmentfault.com/u/geoffzhu
1
<p><a href="https://link.segmentfault.com/?enc=pHbXxknV%2B9rT2kYjX8msvw%3D%3D.vhnTFiGDVR6eKRRUxzuT2ZHuX38vh4SjRem81vjiHue%2FCogAW6gm91LJWz%2F95wLfEPx3%2BNqm1h3d9779Br1%2FDg%3D%3D" rel="nofollow">原文地址</a></p>
<blockquote>周五组内同学讨论搞一些好玩的东西,有人提到了类似『5分钟实现koa』,『100行实现react』的创意,仔细想了以后,5分钟实现koa并非不能实现,遂有了这篇博客。</blockquote>
<h2>准备</h2>
<p>先打开koa官网,随意找出了一个代表koa核心功能的的demo就可以,如下</p>
<pre><code class="javascript">const Koa = require('koa');
const app = new Koa();
// x-response-time
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
});
// logger
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}`);
});
// response
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);</code></pre>
<p>最终要实现的效果是实现的一个5min-koa模块,直接将代码中第一行替换为<code>const Koa = require('./5min-koa');</code>,程序可以正常执行就可以了。</p>
<h2>Koa的核心</h2>
<p>通过koa官网得知,app.listen方法实际上是如下代码的简写</p>
<pre><code class="javascript">const http = require('http');
const Koa = require('koa');
const app = new Koa();
http.createServer(app.callback()).listen(3000);</code></pre>
<p>所以我们可以先把app.listen实现出来</p>
<pre><code class="javascript">class Koa {
constructor() {}
callback() {
return (req, res) => {
// TODO
}
}
listen(port) {
http.createServer(this.callback()).listen(port);
}
}</code></pre>
<p>koa的核心分为四部分,分别是</p>
<ul>
<li>context 上下文</li>
<li>middleware 中间件</li>
<li>request 请求</li>
<li>responce 响应</li>
</ul>
<h3>Context</h3>
<p>我们先来实现一个最简化版的context,如下</p>
<pre><code class="javascript">class Context {
constructor(app, req, res) {
this.app = app
this.req = req
this.res = res
// 为了尽可能缩短实现时间,我们直接使用原生的res和req,没有实现ctx上的ctx.request ctx.response
// ctx.request ctx.response只是在原生res和req上包装处理了一层
}
// 实现一些demo中使用到的ctx上代理的方法
get set() { return this.res.setHeader }
get method() { return this.req.method }
get url() { return this.req.url }
}</code></pre>
<p>这样就完成了一个最基本的Context,别看小,已经够用了。<br>每一次有新的请求,都会创建一个新的ctx对象。</p>
<h3>Middleware</h3>
<p>koa的中间件是一个异步函数,接受两个参数,分别是ctx和next,其中ctx是当前的请求上下文,next是下一个中间件(也是异步函数),这样想来,我们需要一个维护中间件的数组,每次调用app.use就是往数组中push一个一步函数。所以use方法实现如下</p>
<pre><code class="javascript">use(middleware) {
this.middlewares.push(middleware)
}</code></pre>
<p>每次有新的请求,我们都需要把这次请求的上下文灌进数组中的每一个中间件里。单单灌进ctx还不够,还要使每个中间件都能通过next函数调用到下一个中间件。当我们调用next函数时,一般是不需要传参数的,而被调用的中间件中一定会接收到ctx和next两个参数。</p>
<p>调用方不需要传参,被调用方却能接到参数,这让我立刻想到bind方法,只要将每一个中间件所需要的ctx和next都提前绑定好,问题就解决了。下面的代码就是通过bind方法,将用户传入的middleware列表转换成next函数列表</p>
<pre><code class="javascript">let bindedMiddleware = []
for (let i = middlewares.length - 1; i >= 0; i--) {
if (middlewares.length == i + 1) {
// 最后一个中间件,next方法设置为Promise.resolve
bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, Promise.resolve))
} else {
bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, bindedMiddleware[0]))
}
}</code></pre>
<p>最后我们就得到了一个next函数数组,也就是bindedMiddleware这个变量了。</p>
<h3>Request</h3>
<p>http.createServer中的回调函数,每次接收到请求的时候会被调用,所以我们在上面callback方法的TODO位置,编写处理请求的代码, 并将上面的middleware列表转next函数列表的代码放入其中。</p>
<pre><code class="javascript">function handleRequest(ctx, middlewares) {
if (middlewares && middlewares.length > 0) {
let bindedMiddleware = []
for (let i = middlewares.length - 1; i >= 0; i--) {
if (middlewares.length == i + 1) {
bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, Promise.resolve))
} else {
bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, bindedMiddleware[0]))
}
}
return bindedMiddleware[0]()
} else {
return Promise.resolve()
}
}</code></pre>
<h3>Responce</h3>
<p>我们简单出来下相应就好了,直接将ctx.body发送给客户端。</p>
<pre><code class="javascript">function handleResponse (ctx) {
return function() {
ctx.res.writeHead(200, { 'Content-Type': 'text/plain' });
ctx.res.end(ctx.body);
}
}</code></pre>
<h2>完成Koa类的实现</h2>
<p>koa的app实例上面带有on,emit等方法,这是node events模块实现好的东西。直接让Koa类继承自events模块就好了。<br>我们再将上面实现出来的handleRequest和handleResponse方法放入koa类的callback方法中,得到最终我们实现的Koa,一共58行代码,如下</p>
<pre><code class="javascript">const http = require('http');
const Emitter = require('events');
class Context {
constructor(app, req, res) {
this.app = app;
this.req = req;
this.res = res;
}
get set() { return this.res.setHeader }
get method() { return this.req.method }
get url() { return this.req.url }
}
class Koa extends Emitter{
constructor(options) {
super();
this.options = options
this.middlewares = [];
}
use(middleware) {
this.middlewares.push(middleware);
}
callback() {
return (req, res) => {
let ctx = new Context(this, req, res);
handleRequest(ctx, this.middlewares).then(handleResponse(ctx));
}
}
listen(port) {
http.createServer(this.callback()).listen(port);
}
}
function handleRequest(ctx, middlewares) {
if (middlewares && middlewares.length > 0) {
let bindedMiddleware = [];
for (let i = middlewares.length - 1; i >= 0; i--) {
if (middlewares.length == i + 1) {
bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, Promise.resolve));
} else {
bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, bindedMiddleware[0]));
}
}
return bindedMiddleware[0]();
} else {
return Promise.resolve();
}
}
function handleResponse (ctx) {
return function() {
ctx.res.writeHead(200, { 'Content-Type': 'text/plain' });
ctx.res.end(ctx.body);
}
}
module.exports = Koa;</code></pre>
<p>试试跑一下篇首的Demo,没什么问题。</p>
<h2>结语</h2>
<p>简版实现,码糙理不糙,展示出了koa核心的东西,但少了错误处理,也完全没有考虑性能啥的,需要完善的地方还很多很多。</p>
<p>笔者在写了这个5分钟koa以后去看了koa源码,发现实现思路基本就是这样,相信经过我的这个5分钟koa的洗礼,你去看koa源码一样小菜一碟。</p>
<p>Done!</p>
前端工程师应该知道的Linux知识(一)
https://segmentfault.com/a/1190000012721734
2018-01-05T19:58:15+08:00
2018-01-05T19:58:15+08:00
GeoffZhu
https://segmentfault.com/u/geoffzhu
6
<blockquote>随着业务架构演进,服务端和客户端的渲染层均交由FEer处理,越来越多的前端开始接触Linux,做一些安装部署NodeJS项目之类的工作,本篇的主要内容就是介绍Linux下的基础知识和常用命令,帮助前端工程师在服务器中自由自在的傲游。</blockquote>
<h2>发行版</h2>
<p>目前互联网公司使用的发行版主要是如下三个</p>
<ul>
<li>CentOS</li>
<li>Debian</li>
<li>Ubuntu</li>
</ul>
<p>如何知道一台机器的发行版?</p>
<pre><code class="bash">lsb_release -a</code></pre>
<p>想知道一台机器的基本信息(内核版本等)?</p>
<pre><code class="bash">uname -a</code></pre>
<h2>基本概念</h2>
<h3>用户与用户组</h3>
<p>在Linux下,每个用户都属于一个用户组。文件和目录的读写权限等会根据<code>用户</code>和<code>用户组</code>来区分。对于组外的用户,称之为<code>其他用户</code>`。</p>
<h3>目录</h3>
<p>在Linux下目录是一等公民,万物开始于<code>/</code>。我们先说明一下系统根目录下常规的一些文件夹都是做什么的?详情见这张表</p>
<table>
<thead><tr>
<th>目录名</th>
<th>简介</th>
</tr></thead>
<tbody>
<tr>
<td>/opt</td>
<td>第三方软件</td>
</tr>
<tr>
<td>/bin</td>
<td>一般用户可用一些执行文件</td>
</tr>
<tr>
<td>/sbin</td>
<td>一些系统执行文件</td>
</tr>
<tr>
<td>/tmp</td>
<td>临时文件</td>
</tr>
<tr>
<td>/home</td>
<td>一般用户的文件目录</td>
</tr>
<tr>
<td>/root</td>
<td>root用户的文件目录</td>
</tr>
<tr>
<td>/boot</td>
<td>内核文件和开机引导程序等</td>
</tr>
<tr>
<td>/dev</td>
<td>设备文件</td>
</tr>
<tr>
<td>/etc</td>
<td>各种软件的配置文件和启动脚本</td>
</tr>
<tr>
<td>/lib</td>
<td>一些系统依赖的库</td>
</tr>
<tr>
<td>/usr</td>
<td>一些系统软件所在目录</td>
</tr>
<tr>
<td>/var</td>
<td>一些系统的日志文件和缓存文件</td>
</tr>
</tbody>
</table>
<p>上面这张表中/boot目录以上的是我们一定要理解清楚的目录,至于/boot及以下的目录只需大概了解就好。</p>
<h2>包管理工具</h2>
<p>每个平台下有自己的包管理工具,作为一个常用Mac的前端工程师,你一定知道Homebrew,自己也常常<code>brew install</code>,安完就能在命令行中使用了,非常方便。Homebrew默认的源是在海外的,很慢很慢,所以大家一般会<a href="https://link.segmentfault.com/?enc=z8qimW7AOQ0Lk7r9Bo6hNw%3D%3D.nBXUJJe5PmE%2BJ6NAsHfabFmObS%2BCh9%2B2dTzzO7C%2FYrV0CTEVb6qXZiA%2Bt9dobvhIIAalP1P6zzvyM7zEihY0lw%3D%3D" rel="nofollow">替换为国内的源</a>,就跟npm切成淘宝源一个意思。</p>
<p>为什么扯这么多Mac上的东西那?原因在于,这套包管理机制和Linux各个发行版是一致的。各个发行版中推荐使用的包管理工具如下</p>
<ul>
<li>CentOS -> yum</li>
<li>Debian -> apt-get</li>
<li>Ubuntu -> apt-get</li>
</ul>
<p>通过包管理工具,可以轻松安装相应的包,直接就可以在命令行中使用。</p>
<p>其他还有一些相应格式安装包对应的安装工具,比如用于安装.deb文件的<code>dpkg</code>,还有用于安装.rpm文件的<code>rpm</code>。</p>
<p>包的其实也不是什么高深的东西?其实就是一个或多个拥有可执行权限的文件,比如我们执行<code>node</code>命令,实际上就是执行了一个可执行的二进制文件。</p>
<p>我们可以使用<code>which</code>命令查看可执行文件在哪里?例如<code>which node</code></p>
<h2>基本命令</h2>
<h3>帮助命令</h3>
<h4>man</h4>
<p>绝大部分的软件安装都为执行程序提供了帮助手册<br>编译安装的软件,可以配置/etc/man.config <br>指定man手册路径</p>
<p>man命令大多给出的文档非常详细,可能有的时候并不想详读,这时候推荐使用tldr查看帮助文档。tldr是一个在线的帮助文档工具,提供的文档比较社区化,简短易懂,悲伤的是Linux机器上默认是没有tldr的,需要自己安装下,详细文档在<a href="https://link.segmentfault.com/?enc=ed4ipDWlEksqDD0SsxRE0w%3D%3D.VgvAu8CIfRlLSF6e7uKLwg%3D%3D" rel="nofollow">这里</a>。</p>
<h3>目录&文件命令</h3>
<table>
<thead><tr>
<th>命令</th>
<th>简介</th>
<th>示例</th>
</tr></thead>
<tbody>
<tr>
<td>cp</td>
<td>复制文件/目录</td>
<td>
<code> cp path/to/file.ext path/to/copy.ext</code> <br><code>cp path/to/file.ext path/to/copy</code>
</td>
</tr>
<tr>
<td>rm</td>
<td>删除文件/目录</td>
<td>
<code>rm path/to/file</code> <br><code>rm -r path/to/folder</code>
</td>
</tr>
<tr>
<td>mv</td>
<td>移动文件/目录</td>
<td><code>mv source target</code></td>
</tr>
<tr>
<td>which</td>
<td>查找命了文件所在位置</td>
<td><code>which node</code></td>
</tr>
<tr>
<td>find</td>
<td>查找文件或目录</td>
<td>
<code>find root_path -name '*.ext'</code> <br><code>find root_path -size +500k -size -10MB</code>
</td>
</tr>
<tr>
<td>ln</td>
<td>创建文件/目录的链接</td>
<td><code>ln node-v6.2 node</code></td>
</tr>
<tr>
<td>touch</td>
<td>新建空文件</td>
<td><code>touch filename</code></td>
</tr>
<tr>
<td>file</td>
<td>查看文件类型</td>
<td><code>file filename</code></td>
</tr>
<tr>
<td>pwd</td>
<td>查看当前工作目录</td>
<td><code>pwd</code></td>
</tr>
<tr>
<td>cd</td>
<td>切换工作目录</td>
<td><code>cd /path</code></td>
</tr>
<tr>
<td>ls</td>
<td>显示目录内容</td>
<td><code>ls -lh</code></td>
</tr>
<tr>
<td>mkdir</td>
<td>创建新的目录</td>
<td><code>mkdir –p /directory</code></td>
</tr>
<tr>
<td>du</td>
<td>统计目录及文件的空间占用情况</td>
<td>
<code>du -sh</code> <br><code>du -h --max-depth=N path/to/folder</code>
</td>
</tr>
</tbody>
</table>
<p>Tips: 几个常见的命令行参数<br>-h -human-readable 以人能看懂的方式展示<br>-r 递归文件夹<br>-f -force 强制</p>
<h3>内容查看命令</h3>
<table>
<thead><tr>
<th>命令</th>
<th>简介</th>
<th>示例</th>
</tr></thead>
<tbody>
<tr>
<td>cat</td>
<td>查看显示文件内容</td>
<td><code>cat filename</code></td>
</tr>
<tr>
<td>more/less</td>
<td>分页查看文件内容</td>
<td><code>less filename</code></td>
</tr>
<tr>
<td>head/tail</td>
<td>查看文件开头/末尾的部分内容</td>
<td><code>tail -f web.log</code></td>
</tr>
<tr>
<td>wc</td>
<td>统计文件内容的单词数量等信息</td>
<td> </td>
</tr>
<tr>
<td>grep</td>
<td>检索过滤文件内容</td>
<td>
<code>`</code>cat package.json</td>
<td>grep 'vue'<code>`</code>
</td>
</tr>
</tbody>
</table>
<p>在文件查看命令中,我个人最推荐使用less命令,比较强大,基本可以满足我个人的所有需求<br><img src="/img/remote/1460000012721739" alt="image" title="image"></p>
<hr>
<blockquote>此为第一节全部内容,下一节我将详细介绍一些进程管理,CPU,内存使用量相关的命令,敬请期待。</blockquote>
<hr>
<p>Hi,我叫Geoff Zhu,是一名前端工程师,我会不定期的在这个博客中更新一些我写的东西,希望能对你有所帮助。这是我的<a href="https://link.segmentfault.com/?enc=xD9ZPEx1oYifP1R8FcwbUw%3D%3D.vs7POA1YFFVUaItKilemua9svC%2BW9VyZkTOTRu7B7%2BcpeyUlpp4WyInrgzghJZmW" rel="nofollow">Github</a>,如果可以给个star什么的,不胜感激。</p>
现代浏览器性能优化-CSS篇
https://segmentfault.com/a/1190000012643583
2017-12-29T19:00:56+08:00
2017-12-29T19:00:56+08:00
GeoffZhu
https://segmentfault.com/u/geoffzhu
10
<p>我来填坑了,CSS篇终于写出来了,如果你没看过前面的JS篇,可以<a href="https://link.segmentfault.com/?enc=6tMpvdNtbIEz8jwCTgUOQw%3D%3D.WH3Pne%2BCfo7yXzLUYjXGDaetr7XTjEkQYrVeSpkKCrCN69nI%2FvI%2Bjoz7CalDkcUk0NKnk%2BEKTG1aTNWg3MPVmQ%3D%3D" rel="nofollow">在这里观看</a>。</p>
<blockquote>众所周知,CSS的加载会阻塞浏览器渲染或是引起浏览器重绘,目前业界普遍推荐把CSS放到<code><head></code>中,防止在CSS还没加载完,DOM就已经绘制出来了,造成CSS加载完成后的重绘。那在现代浏览器中我们有没有办法提高首屏渲染速度那?</blockquote>
<p>你是不是经常在第一次打开某个网站的时候看到这种情况,本来的页面是这样的<br><img src="/img/remote/1460000012643588?w=1574&h=698" alt="" title=""></p>
<p>实际上刚加载出来的是这样的<br><img src="/img/remote/1460000012643589" alt="" title=""></p>
<p>字体文件没加载出来,或者加载的太慢了</p>
<h2>理解CSS解析过程</h2>
<p>以下面这段HTML为例,解释一遍CSS加载解析的过程。</p>
<pre><code class="html"><html>
<head>
<!-- headStyle.css中存在字体文件webfont.woff2 -->
<link rel="stylesheet" type="text/css" href="/headStyle.css">
</head>
<body>
<p>Text</p>
<link rel="stylesheet" type="text/css" href="/bodyEndStyle.css">
</body>
</html></code></pre>
<p>浏览器自上而下读取HTML文档,当发现headStyle.css的时候,停止Parser HTML,开始下载headStyle.css,解析headStyle.css的过程中发现字体文件webfont.woff2,开始下载webfont.woff2,并继续解析css生成CSSStyleSheet。解析完毕后,继续Parser HTML,当发现p标签时,会将p标签结合当前的CSSStyleSheet展示出来,此时用户屏幕中已经有p标签的内容了。当浏览器发现bodyEndStyle.css时,就会下载headStyle.css,解析CSS,然后更新CSSStyleSheet,这时会引起一次重绘。当字体下载完毕的时候也会引起一次重绘。</p>
<p>这个过程中,有两个非常严重的问题。一、如果headStyle.css文件很大,浏览器需要解析很多行CSS后才能还有个字体文件需要下载,其实此时已经很晚了,字体下载时间稍长一点,就会出现我前面截图提到的问题。二、bodyEndStyle.css中如果存在p标签对应的样式,那p标签的样式会在bodyEndStyle.css解析完成后,改变一次样式,很影响体验。</p>
<p>如何解决这些问题那?其中也会用到一些JS篇中提到的点,如果没看过,建议先看看。</p>
<h2>优化核心依旧是减少下载时间</h2>
<p>JS篇中的预先解析DNS(dns-prefetch)依旧适用,提前解析CSS文件所在域名的DNS。</p>
<h4>Preload</h4>
<p>因为CSS已经在head中,我们不需要为css加preload属性了,但是css中用到的字体文件,一定要在所有css之前proload上。</p>
<pre><code class="html"><link rel="preload" href="/webfont.woff2" as="font"></code></pre>
<h4>首页CSS内联,非必要CSS异步加载</h4>
<p>首页用到的CSS内联写在<code><head></code>中,其余CSS均采用异步加载,可以采用这种自己实现的加载CSS的方法,在合适的需要时加载需要的css</p>
<pre><code class="javascript">function LoadStyle(url) {
try {
document.createStyleSheet(url)
} catch(e) {
var cssLink = document.createElement('link');
cssLink.rel = 'stylesheet';
cssLink.type = 'text/css';
cssLink.href = url;
var head = document.getElementsByTagName('head')[0];
head.appendChild(cssLink)
}
}</code></pre>
<p>如果你使用webpack,那就更轻松了,使用import函数,大致如下</p>
<pre><code class="javascript">// 在a.js模块中直接引入css
import 'style.css'</code></pre>
<pre><code class="javascript">// 在需要a.js模块的地方
improt('path-of-a.js').then(module => {})</code></pre>
<p>webpack打包后,其实是把style.css打包进了a.js,在异步加载a.js的时候,会将style.css中的代码插入<code>haed</code>标签中。</p>
<h2>终极完美结构</h2>
<pre><code class="html"><!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Faster</title>
<link rel="dns-prefetch" href="//cdn.cn/">
<link rel="preload" href="//cdn.cn/webfont.woff2" as="font">
<link rel="preload" href="//cdn.cn/Page1-A.js" as="script">
<link rel="preload" href="//cdn.cn/Page1-B.js" as="script">
<link rel="prefetch" href="//cdn.cn/Page2.js">
<link rel="prefetch" href="//cdn.cn/Page3.js">
<link rel="prefetch" href="//cdn.cn/Page4.js">
<style type="text/css">
/* 首页用到的CSS内联 */
</style>
</head>
<body>
<script type="text/javascript" src="//cdn.cn/Page1-A.js" defer></script>
<script type="text/javascript" src="//cdn.cn/Page1-B.js" defer></script>
</body>
</html></code></pre>
<p>在<a href="#">JS篇</a>)中,我已经解释过这套结构中JS的执行顺序了,本篇只是加入了CSS和字体。至此,我心中终极完美的页面HTML结构就是这样了。</p>
<p>如果你对异步加载CSS的方案感兴趣,欢迎留言与我讨论!</p>
<h2>扩展阅读</h2>
<ul>
<li><a href="https://link.segmentfault.com/?enc=kx9bvy4ACa5G9twk56bTcw%3D%3D.Sla3bczqVstgyarzqpcsvHLrVXiKIvCV2%2BX2qplR4a239euDaYDc6%2Bxq3EBX5d5ox2g%2FvQHOgOFkG8rYsfPbRqsJBg9wyj1uYbOGdiFGzCPTIAMwxDOda4SZL%2B4rN4%2B6" rel="nofollow">浏览器的工作原理</a></li>
<li><a href="https://link.segmentfault.com/?enc=yU1CMP9IrMjzSCSQ%2FAk5gw%3D%3D.c1nUOrqXaLeENgobLlLbDDNCfWkRpqbh6RZRd3k5EZQaXD7DGlFA1HOdMcvgRm0s" rel="nofollow">关于Preload, 你应该知道些什么?</a></li>
<li><a href="https://link.segmentfault.com/?enc=P9fJoqoXFIxjgqB7VEd1cA%3D%3D.wOOtXr3BEfc2EliGqB64JLReEFbq5H04KsMGwhtOBA5NPlSxZ1T7aWt4G0B2B07s" rel="nofollow">Preload,Prefetch 和它们在 Chrome 之中的优先级</a></li>
</ul>
为localStorage增加过期时间和类型支持
https://segmentfault.com/a/1190000012079013
2017-11-20T13:51:42+08:00
2017-11-20T13:51:42+08:00
GeoffZhu
https://segmentfault.com/u/geoffzhu
1
<h2>Easy Storage</h2>
<blockquote><p>为localStorage和session增加类型支持和过期时间。</p></blockquote>
<h3>用法</h3>
<p>支持 String, Number, Array, Object.</p>
<table>
<thead><tr>
<th>Function</th>
<th>Example</th>
</tr></thead>
<tbody>
<tr>
<td>set</td>
<td>
<code>es.set('key', value, timeStamp</code>)</td>
</tr>
<tr>
<td>get</td>
<td><code>es.get('key')</code></td>
</tr>
<tr>
<td>remove</td>
<td><code>es.remove('key')</code></td>
</tr>
<tr>
<td>clear</td>
<td><code>es.clear()</code></td>
</tr>
</tbody>
</table>
<p>set方法的第三个参数是一个以毫秒为单位的时间,表示过多久过期。</p>
<h3>安装</h3>
<h4>CDN</h4>
<pre><code class="html"><script type="text/javascript" src="//unpkg.com/syt-easy-storage@0.0.3/index.js"></script>
<script type="text/javascript">
const es = new EasyStorage() // Default use localStorage, `new EasyStorage({type: 'session'})` for use sessionStorage
</script></code></pre>
<h4>NPM</h4>
<pre><code class="sh">npm install syt-easy-storage --save</code></pre>
<pre><code class="javascript">// ES
import EasyStorage 'syt-easy-storage'
// commonjs
const EasyStorage = require('syt-easy-storage')
const es = new EasyStorage()</code></pre>
<h4>VueJS</h4>
<pre><code class="sh">npm install syt-easy-storage --save</code></pre>
<pre><code class="javascript">// plugin.js
import VueEasyStorage 'syt-easy-storage/vue'
const easyStorage = new VueEasyStorage()
export default easyStorage
// entry.js
import Vue from 'vue'
import easyStorage 'path/of/plugin.js'
Vue.use(easyStorage)
// components file
this.$es.set('key', value)
this.$es.set('key', value, 24*60*60*1000)
this.$es.get('key')
</code></pre>
记录使用Performance API遇到的问题
https://segmentfault.com/a/1190000011850869
2017-11-03T19:46:17+08:00
2017-11-03T19:46:17+08:00
GeoffZhu
https://segmentfault.com/u/geoffzhu
2
<h2>记录使用Performance API遇到的问题</h2>
<blockquote><p>本文中Performance API指的是Navigation Timing API。这并不是一篇Navigation Timing API的介绍文章,而是我在使用中遇到的问题。</p></blockquote>
<p>我在开发中遇到Navigation Timing API中的connectStart等时间节点并不是标准时间戳,而是0或者一个很小的数值,导致指标数据计算出错,尤其是IOS设备。原因如下:</p>
<p>IOS设备通过浏览器的前进后退按钮进入的页面,Navigation Timing API数据中connectStart,responseEnd等数据可能为0或者是一个比较小的数值,并不是对应时间点的时间戳。究其原因,IOS设备通过缓存读取页面时,Navigation Timing的计算与安卓实现不一致。</p>
<p>如果你还想了解下Navigation Timing API,可以继续往下看</p>
<h4>Navigation Timing API</h4>
<p>Navigation Timing API中包含全部的页面加载中关键节点的时间,例如navigationStart,connectEnd,responseEnd等时间。<br>具体的相关API可以去<a href="https://link.segmentfault.com/?enc=pvyjUtXAwORaoQjd3sS8QQ%3D%3D.XbwjvDYakIwbBEfpMxLY1QuQOK1m68VyABK%2FYVgEudWk3nAtNw6mKOfq3zVk0C6PFnz0cPTMnVyGwUb8QXG2C%2FDibKVGyNGulcGANDgSyCk%3D" rel="nofollow">MDN</a>查看,<br>浏览器支持程度也非常不错,移动端IOS9及以上,Android4及以上都支持,桌面端IE9也都支持。</p>
<h5>一些常规的性能数据计算方法</h5>
<p>DNS时间 = domainLookupEnd - domainLookupStart<br>TCP时间 = connectEnd - connectStart<br>后端时间 = responseEnd - connectEnd</p>
<p>白屏时间 = domInteractive - navigationStart<br>整屏时间 = loadEventEnd - navigationStart</p>
<p>解析dom树耗时 = domComplete - domInteractive<br>request请求耗时 = responseEnd - responseStart</p>
<p>我们团队就是按照如上指标来做的各个时间的统计,做了各种测试,线下数据都没什么问题。上线了以后拿到的首批数据中,后端时间计算出来竟然有负值,尤其在IOS设备下,苦苦寻找原因,终于发现问题所在。</p>
<p>IOS设备通过浏览器的前进后退按钮进入的页面,Navigation Timing API数据中connectStart,responseEnd等数据可能为0或者是一个比较小的数值,并不是对应时间点的时间戳。</p>
<h5>关于首屏时间的定义</h5>
<p>根据Navigation Timing API的时间,是没有办法计算首屏时间的,首屏时间也并没有严格的定义,我们团队采用的首屏时间如下</p>
<p>首屏时间 = (dom解析完毕 && 所有首屏图片加载完毕 )- navigationStart</p>
<h5>标准</h5>
<table>
<thead><tr>
<th>属性</th>
<th>含义</th>
</tr></thead>
<tbody>
<tr>
<td>navigationStart</td>
<td>准备加载新页面的起始时间</td>
</tr>
<tr>
<td>redirectStart</td>
<td>如果发生了HTTP重定向,并且从导航开始,中间的每次重定向,都和当前文档同域的话,就返回开始重定向的timing.fetchStart的值。其他情况,则返回0</td>
</tr>
<tr>
<td>redirectEnd</td>
<td>如果发生了HTTP重定向,并且从导航开始,中间的每次重定向,都和当前文档同域的话,就返回最后一次重定向,接收到最后一个字节数据后的那个时间.其他情况则返回0</td>
</tr>
<tr>
<td>fetchStart</td>
<td>如果一个新的资源获取被发起,则 fetchStart必须返回用户代理开始检查其相关缓存的那个时间,其他情况则返回开始获取该资源的时间</td>
</tr>
<tr>
<td>domainLookupStart</td>
<td>返回用户代理对当前文档所属域进行DNS查询开始的时间。如果此请求没有DNS查询过程,如长连接,资源cache,甚至是本地资源等。 那么就返回 fetchStart的值</td>
</tr>
<tr>
<td>domainLookupEnd</td>
<td>返回用户代理对结束对当前文档所属域进行DNS查询的时间。如果此请求没有DNS查询过程,如长连接,资源cache,甚至是本地资源等。那么就返回 fetchStart的值</td>
</tr>
<tr>
<td>connectStart</td>
<td>返回用户代理向服务器服务器请求文档,开始建立连接的那个时间,如果此连接是一个长连接,又或者直接从缓存中获取资源(即没有与服务器建立连接)。则返回domainLookupEnd的值</td>
</tr>
<tr>
<td>(secureConnectionStart)</td>
<td>可选特性。用户代理如果没有对应的东东,就要把这个设置为undefined。如果有这个东东,并且是HTTPS协议,那么就要返回开始SSL握手的那个时间。 如果不是HTTPS, 那么就返回0</td>
</tr>
<tr>
<td>connectEnd</td>
<td>返回用户代理向服务器服务器请求文档,建立连接成功后的那个时间,如果此连接是一个长连接,又或者直接从缓存中获取资源(即没有与服务器建立连接)。则返回domainLookupEnd的值</td>
</tr>
<tr>
<td>requestStart</td>
<td>返回从服务器、缓存、本地资源等,开始请求文档的时间</td>
</tr>
<tr>
<td>responseStart</td>
<td>返回用户代理从服务器、缓存、本地资源中,接收到第一个字节数据的时间</td>
</tr>
<tr>
<td>responseEnd</td>
<td>返回用户代理接收到最后一个字符的时间,和当前连接被关闭的时间中,更早的那个。同样,文档可能来自服务器、缓存、或本地资源</td>
</tr>
<tr>
<td>domLoading</td>
<td>返回用户代理把其文档的 "current document readiness" 设置为 "loading"的时候</td>
</tr>
<tr>
<td>domInteractive</td>
<td>返回用户代理把其文档的 "current document readiness" 设置为 "interactive"的时候.</td>
</tr>
<tr>
<td>domContentLoadedEventStart</td>
<td>返回文档发生 DOMContentLoaded事件的时间</td>
</tr>
<tr>
<td>domContentLoadedEventEnd</td>
<td>文档的DOMContentLoaded 事件的结束时间</td>
</tr>
<tr>
<td>domComplete</td>
<td>返回用户代理把其文档的 "current document readiness" 设置为 "complete"的时候</td>
</tr>
<tr>
<td>loadEventStart</td>
<td>文档触发load事件的时间。如果load事件没有触发,那么该接口就返回0</td>
</tr>
<tr>
<td>loadEventEnd</td>
<td>文档触发load事件结束后的时间。如果load事件没有触发,那么该接口就返回0</td>
</tr>
</tbody>
</table>
带你玩转prefetch, preload, dns-prefetch,defer和async
https://segmentfault.com/a/1190000011577248
2017-10-16T17:48:01+08:00
2017-10-16T17:48:01+08:00
GeoffZhu
https://segmentfault.com/u/geoffzhu
65
<h2>现代浏览器性能优化-JS篇</h2>
<blockquote>众所周知,JS的加载和执行会阻塞浏览器渲染,所以目前业界普遍推荐把script放到</body>之前,以解决js执行时找不到dom等问题。但随着现代浏览器的普及,浏览器为我们提供了更多强大的武器,合理利用,方可大幅提高页面加载速度。</blockquote>
<h3>理解渲染过程(HTML Parser)</h3>
<p>首先我们从浏览器的角度解释一下从输入URL到页面展示经历了些什么,以如下html文档举例</p>
<pre><code><html>
<head>
<link rel="stylesheet" type="text/css" href="/style.css">
<script type="text/javascript" src="/header.js"></script>
</head>
<body>
<p>Text</p>
<script type="text/javascript" src="/main.js"></script>
</body>
</html></code></pre>
<p>浏览器自上而下读取html文档(此过程叫html parser),当发现style.css文件时,浏览器parser停下来去搞css,等style.css下载并解析完毕,浏览器继续parser。紧接着发现header.js, 于是html parser又停了,浏览器下载并执行完header.js,继续parser。此时屏幕上还什么都没有。...parser,发现<p>,遂将p中文字展示了出来。紧接着又发现main.js,浏览器又停下parser,下载并执行完main.js才继续parser,直到页面渲染完毕。</p>
<p>我们假设header.js中只有一行代码<code>console.log('header')</code>, 但服务器响应很慢,要10秒才能把它返回给浏览器,浏览器执行这段代码需要1ms,那在这 10s+1ms 内,页面将一直空白。浏览器执行JS的时间取决于代码质量和硬件,并不是前端工程师随便可以优化的,所以优化的重点在JS的下载时间。</p>
<h3>核心:减少JS下载时间</h3>
<h5>预先解析DNS</h5>
<p>非常简单,效果立竿见影,加快页面加载时间,多用于预解析CDN的地址的DNS</p>
<pre><code><!--在head标签中,越早越好-->
<link rel="dns-prefetch" href="//example.com"></code></pre>
<h5>Preload</h5>
<p>浏览器会在遇到如下link标签时,立刻开始下载main.js(不阻塞parser),并放在内存中,但不会执行其中的JS语句。<br>只有当遇到script标签加载的也是main.js的时候,浏览器才会直接将预先加载的JS执行掉。</p>
<pre><code><link rel="preload" href="/main.js" as="script"></code></pre>
<h5>Prefetch</h5>
<p>浏览器会在空闲的时候,下载main.js, 并缓存到disk。当有页面使用的时候,直接从disk缓存中读取。其实就是把决定是否和什么时间加载这个资源的决定权交给浏览器。</p>
<p>如果prefetch还没下载完之前,浏览器发现script标签也引用了同样的资源,浏览器会再次发起请求,这样会严重影响性能的,加载了两次,,所以不要在当前页面马上就要用的资源上用prefetch,要用preload。</p>
<pre><code><link href="main.js" rel="prefetch"></code></pre>
<h3>JS在什么时候执行的(defer和async)</h3>
<p>上面我们的例子中,script标签都是在没有多余属性的情况下执行的,只要下载过程结束,浏览器就会将JS执行掉。<br>defer和async是script标签的两个属性,用于在不阻塞页面文档解析的前提下,控制脚本的下载和执行。</p>
<p>defer,async与下载时机也有关,具体看这张图。<br><img src="http://segmentfault.com/img/bVcQV0" alt="" title=""></p>
<p>defer的执行时间是在所有元素解析完成之后,DOMContentLoaded 事件触发之前。</p>
<p>async的执行时间是在当前JS脚本下载完成后,所以多个async script是执行顺序是不固定的。async只能用于加载一些独立无依赖的代码,比如Google Analysis之类。</p>
<h3>完美的结构</h3>
<p>前面两节帮我们弄懂了JS的下载和执行时机,那什么样的页面才是完美符合现代浏览器的那?其实关键在于的preload和prefetch!提前告知浏览器,我们的网站马上要用的是什么,以后可能要用的是什么,浏览器才能更快的渲染页面。下面是一段实例代码</p>
<pre><code><!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Faster</title>
<link rel="dns-prefetch" href="//cdn.com/">
<link rel="preload" href="//js.cdn.com/currentPage-part1.js" as="script">
<link rel="preload" href="//js.cdn.com/currentPage-part2.js" as="script">
<link rel="preload" href="//js.cdn.com/currentPage-part3.js" as="script">
<link rel="prefetch" href="//js.cdn.com/prefetch.js">
</head>
<body>
<script type="text/javascript" src="//js.cdn.com/currentPage-part1.js" defer></script>
<script type="text/javascript" src="//js.cdn.com/currentPage-part2.js" defer></script>
<script type="text/javascript" src="//js.cdn.com/currentPage-part3.js" defer></script>
</body>
</html></code></pre>
<p>首先,Parser在遇到head中preload时开始下载JS,读到script标签的时候,如果已经下载完了,直接按顺序执行之。如果没下载完,则会等到下载完再执行。这样就可以在刚进入页面时开始非阻塞的下载JS代码了。</p>
<p>其次,页面会在空闲时,加载prefetch的JS,如果之后页面发生跳转,跳转的目标页面引入了prefetch.js,浏览器会直接从disk缓存中读取执行。</p>
<p>将script标签依然放在</body>之前,并增加defer标签,确保老浏览器兼容,并在所有DOM元素解析完成之后执行其中的代码。</p>
<p>至此,完美的HTML结构出炉了。</p>
<p>CSS的下载和解析一样会阻塞渲染,造成白屏,CSS中的字体文件更是影响首屏渲染关键因素之一,下一篇幅我会结合preload和prefetch,带你一起优化CSS,告诉你什么是最适合现代浏览器的CSS加载策略,期待的话,点个赞吧!</p>
window onerror 各浏览器下表现总结
https://segmentfault.com/a/1190000011041164
2017-09-06T15:45:24+08:00
2017-09-06T15:45:24+08:00
GeoffZhu
https://segmentfault.com/u/geoffzhu
16
<h2>window onerror 各浏览器下表现总结</h2>
<blockquote><p>做前端错误上报,必然离不开window onerror,但window onerror在不同设备上表现并不一致,浏览器为避免信息泄露,在一些情况下并不会给出详细的错误信息,本文的目的就是通过跑一些简单的小例子,验证onerror在不同浏览器下的具体表现。</p></blockquote>
<h3>准备</h3>
<p>我会在Mac, Windows, Android和IOS平台下分别进行测试并记录。为了模拟真实线上环境,我利用<a href="https://link.segmentfault.com/?enc=tUYawjWqYOz7Hm6Itkkd3A%3D%3D.nWKLow%2Btoaj8LHQv4VeTkOXN3n0fBBxjne6l%2FHzJYvgi9VQrZkJTKNwt9Da78x2A" rel="nofollow">GitHub Page</a>模拟线上静态文件服务器,通过其他设备访问此地址即可。</p>
<p><img src="/img/remote/1460000011041169" alt="" title=""></p>
<h3>测试用例</h3>
<p>预期得到错误<code>Uncaught ReferenceError: Name is not defined</code>,并打印onerror中的所有参数,其中包括行列号,Error对象中存在错误的堆栈信息等。</p>
<pre><code class="javascript">window.onerror = function(msg, url, line, col, error) {
// 直接将错误打印到控制台
console.log(arguments)
// 方便在未打开控制台的时候,记录错误对象
window.demoError = arguments
}
function makeError () {
var name = "geoff"
var msg = "Hi, " + Name
console.log(msg)
}
makeError()</code></pre>
<p>.<br>.<br>.<br>测试结果在最后,,,各个浏览器下执行的截图<br>.<br>.<br>.</p>
<h3>先看结论</h3>
<p>大多数现代浏览器对window onerror都支持良好。需要注意的点如下:</p>
<ol>
<li><p>IE10以下只有行号,没有列号, IE10有行号列号,但无堆栈信息。IE10以下,可以通过在onerror事件中访问window.event对象,其中有errorCharacter,也就是列号了。但不知为何,列号总是比真实列号小一些。</p></li>
<li><p>IOS下onerror表现非常统一,包含所有标准信息</p></li>
<li><p>安卓部分机型没有堆栈信息</p></li>
</ol>
<p>总之,浏览器关于onerror这件事,是这样的一个演化过程,最早因为页面中的js并不会很复杂,所以定位错误只需要一个行号就很容易找到,后面加上了列号,最后又加上了堆栈信息。</p>
<h3>实验数据</h3>
<h5>Mac (10.12.1)</h5>
<ol><li><p>Chrome 60.0.3112.90</p></li></ol>
<p><img src="/img/remote/1460000011041295" alt="" title=""></p>
<ol><li><p>Safari 10.0.1 (12602.2.14.0.7)</p></li></ol>
<p><img src="/img/remote/1460000011041296" alt="" title=""></p>
<ol>
<li><p>FireFox 47.0<br><img src="/img/remote/1460000011041297" alt="" title=""></p></li>
<li><p>QQ浏览器 (内核Chromium 48.0.2564.82)<br><img src="/img/remote/1460000011041298" alt="" title=""></p></li>
</ol>
<h5>Windows (win7)</h5>
<ol>
<li><p>Chrome 51.0.2704.106<br><img src="/img/remote/1460000011041299" alt="" title=""></p></li>
<li><p>FireFox 55.0<br><img src="/img/bVUuux?w=1308&h=366" alt="图片描述" title="图片描述"></p></li>
<li><p>IE9<br><img src="/img/remote/1460000011041300" alt="" title=""></p></li>
<li>
<p>IE10</p>
<pre><code>![](https://static.oschina.net/uploads/img/201709/06152130_7MWq.png)
</code></pre>
</li>
</ol>
<h5>Android (5.1)</h5>
<ol>
<li>
<p>Chrome (59.0.3071.92)</p>
<pre><code class="json">{
"0": "Uncaught ReferenceError: Name is not defined",
"1": "http://geoffzhu.cn/error-report/index.js",
"2": 14,
"3": 22,
"4": {}
}</code></pre>
</li>
<li>
<p>UC</p>
<pre><code class="json"> {
"0": "Uncaught ReferenceError: Name is not defined",
"1": "http://geoffzhu.cn/error-report/index.js",
"2": 14,
"3": 22,
"4": {}
}</code></pre>
</li>
<li>
<p>微信webview</p>
<pre><code class="json"> {
"0": "Uncaught ReferenceError: Name is not defined",
"1": "http://geoffzhu.cn/error-report/index.js",
"2": 14,
"3": 22,
"4": {}
}</code></pre>
</li>
</ol>
<h5>IOS (10.3.2)</h5>
<ol>
<li>
<p>Chrome</p>
<pre><code class="json">{
"0": "ReferenceError: Can't find variable: Name",
"1": "http://geoffzhu.cn/error-report/index.js",
"2": 14,
"3": 26,
"4": {
"line": 14,
"column": 26,
"sourceURL": "http://geoffzhu.cn/error-report/index.js"
}
}</code></pre>
</li>
<li><p>UC</p></li>
</ol>
<pre><code class="json"> {
"0": "ReferenceError: Can't find variable: Name",
"1": "http://geoffzhu.cn/error-report/index.js",
"2": 14,
"3": 26,
"4": {
"line": 14,
"column": 26,
"sourceURL": "http://geoffzhu.cn/error-report/index.js"
}
}
</code></pre>
<ol><li>
<p>微信webview</p>
<pre><code class="json">{
"0": "ReferenceError: Can't find variable: Name",
"1": "http://geoffzhu.cn/error-report/index.js",
"2": 14,
"3": 26,
"4": {
"line": 14,
"column": 26,
"sourceURL": "http://geoffzhu.cn/error-report/index.js"
}
}</code></pre>
</li></ol>
<h3>关于代码压缩和source-map</h3>
<p>我通过uglifyJs模拟webpack压缩的配置将上文中的index.js压缩,得到source-map,通过<a href="https://link.segmentfault.com/?enc=pz1ub2JszE64SZNyBy66DQ%3D%3D.B8ZYn%2F%2FXbp8edOs4%2Fm%2BEnb2%2BPEHEj51ooYcDM5JBKUCwicpMJ3Omo%2F6cNBHh1Cba" rel="nofollow">mozilla/source-map</a>的SourceMapConsumer接口,可以通过将转换后的行号列号传入Consumer得到原始错误位置信息。相应的node代码如下</p>
<pre><code class="javascript">var fs = require('fs')
var sourceMap = require('source-map')
// map文件
var rawSourceMapJsonData = fs.readFileSync('./dist/index.min.js.map', 'utf-8')
rawSourceMapJsonData = JSON.parse(rawSourceMapJsonData)
var consumer = new sourceMap.SourceMapConsumer(rawSourceMapJsonData);
// 打印出真实错误位置
console.log(consumer.originalPositionFor({line: 1, column: 220}))</code></pre>
Vue2 transition源码分析
https://segmentfault.com/a/1190000010121812
2017-07-10T15:17:44+08:00
2017-07-10T15:17:44+08:00
GeoffZhu
https://segmentfault.com/u/geoffzhu
5
<h2>Vue transition源码分析</h2>
<blockquote><p>本来打算自己造一个transition的轮子,所以决定先看看源码,理清思路。Vue的transition组件提供了一系列钩子函数,并且具有良好可扩展性。</p></blockquote>
<h3>了解构建过程</h3>
<p>既然要看源码,就先让Vue在开发环境跑起来,首先从GitHub clone下来整个项目,在文件<code>./github/CONTRIBUTING.md</code>中看到了如下备注,需要强调一下的是,npm run dev构建的是runtime + compiler版本的Vue。</p>
<pre><code class="sh"># watch and auto re-build dist/vue.js
$ npm run dev</code></pre>
<p>紧接着在package.json中找到dev对应的shell语句,就是下面这句</p>
<pre><code class="json">"scripts": {
"dev": "rollup -w -c build/config.js --environment TARGET:web-full-dev",
...
}</code></pre>
<p>Vue2使用rollup打包,-c 后面跟的是打包的配置文件(build/config.js),执行的同时传入了一个TARGET参数,web-full-dev。打开配置文件继续往里找。</p>
<pre><code class="javascript">...
const builds = {
...
'web-full-dev': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.js'),
format: 'umd',
env: 'development',
alias: { he: './entity-decoder' },
banner
},
...
}
</code></pre>
<p>从上面的构建配置中,找到构建入口为web/entry-runtime-with-compiler.js,它也就是umd版本vue的入口了。<br>我们发现在Vue的根目录下并没有web这个文件夹,实际上是因为Vue给path.resolve这个方法加了个alias, alias的配置在/build/alias.js中</p>
<pre><code class="javascript">module.exports = {
vue: path.resolve(__dirname, '../src/platforms/web/entry-runtime-with-compiler'),
compiler: path.resolve(__dirname, '../src/compiler'),
core: path.resolve(__dirname, '../src/core'),
shared: path.resolve(__dirname, '../src/shared'),
web: path.resolve(__dirname, '../src/platforms/web'),
weex: path.resolve(__dirname, '../src/platforms/weex'),
server: path.resolve(__dirname, '../src/server'),
entries: path.resolve(__dirname, '../src/entries'),
sfc: path.resolve(__dirname, '../src/sfc')
}</code></pre>
<p>web对应的目录为'../src/platforms/web',也就是src/platforms/web,顺着这个文件继续往下找。查看src/platforms/web/entry-runtime-with-compiler.js的代码,这里主要是处理将Vue实例挂载到真实dom时的一些异常操作提示,<br>,比如不要把vue实例挂载在body或html标签上等。但是对于要找的transition,这些都不重要,重要的是</p>
<pre><code class="javascript">import Vue from './runtime/index'</code></pre>
<p>Vue对象是从当前目录的runtime文件夹引入的。打开./runtime/index.js,先查看引入了哪些模块, 发现Vue是从src/core/index引入的,并看到platformDirectives和platformComponents,官方的指令和组件八九不离十就在这了。</p>
<pre><code class="javascript">import Vue from 'core/index'
...
...
import platformDirectives from './directives/index'
import platformComponents from './components/index'
...
// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop
</code></pre>
<p>在platformComponents中发现transtion.js,它export了一个对象,这个对象有name,props和rander方法,一个标准的Vue组件。至此算是找到了源码位置。</p>
<pre><code class="javascript">export default {
name: 'transition',
props: transitionProps,
abstract: true,
render (h: Function) {
...
}
}</code></pre>
<h3>transition实现分析</h3>
<p>从上一节的代码中,可以看到directives和components是保存在Vue.options里面的, 还需要注意一下后面的Vue.prototype.__patch__,因为transtion并不单单是以一个组件来实现的,还需要在Vue构造函数上打一些patch。</p>
<p>rander当中的参数h方法,就是Vue用来创建虚拟DOM的createElement方法,但在此组件中,并没有发现处理过度动画相关的逻辑,主要是集中处理props和虚拟DOM参数。因为transtion并不单单是以一个组件来实现的,它需要操作真实dom(未插入文档流)和虚拟dom,所以只能在Vue的构造函数上打一些patch了。</p>
<p>往回看了下代码,之前有一句<code>Vue.prototype.__patch__ = inBrowser ? patch : noop</code>,在patch相关的代码中找到了transition相关的实现。modules/transtion.js</p>
<p>这就是过渡动画效果相关的patch的源码位置。</p>
<pre><code class="javascript">export function enter (vnode: VNodeWithData, toggleDisplay: ?() => void) {
...
}
export function leave (vnode: VNodeWithData, rm: Function) {
...
}
export default inBrowser ? {
create: _enter,
activate: _enter,
remove (vnode: VNode, rm: Function) {
/* istanbul ignore else */
if (vnode.data.show !== true) {
leave(vnode, rm)
} else {
rm()
}
}
} : {}</code></pre>
<p>这个模块默认export的对象包括了三个生命周期函数create,activate,remove,这应该是Vue没有对外暴露的生命周期函数,create和activate直接运行的就是上面的enter方法,而remove执行了leave方法。</p>
<p>继续看最重要的是两个方法,enter和leave。通过在这两个方法上打断点得知,执行这两个方法的之前,vnode已经创建了真实dom, 并挂载到了vnode.elm上。其中这段代码比较关键</p>
<pre><code class="javascript">// el就是真实dom节点
beforeEnterHook && beforeEnterHook(el)
if (expectsCSS) {
addTransitionClass(el, startClass)
addTransitionClass(el, activeClass)
nextFrame(() => {
addTransitionClass(el, toClass)
removeTransitionClass(el, startClass)
if (!cb.cancelled && !userWantsControl) {
if (isValidDuration(explicitEnterDuration)) {
setTimeout(cb, explicitEnterDuration)
} else {
whenTransitionEnds(el, type, cb)
}
}
})
}</code></pre>
<p>首先给el添加了startClass和activeClass, 此时dom节点还未插入到文档流,推测应该是在create或activate勾子执行完以后,该节点被插入文档流的。nextFrame方法的实现如下, 如requestAnimationFrame不存在,用setTimeout代替</p>
<pre><code class="javascript">const raf = inBrowser && window.requestAnimationFrame
? window.requestAnimationFrame.bind(window)
: setTimeout
export function nextFrame (fn: Function) {
raf(() => {
raf(fn)
})
}</code></pre>
<p>这种方式的nextFrame实现,正如官方文档中所说的在下一帧添加了toClass,并remove掉startClass,最后在过渡效果结束以后,remove掉了所有的过渡相关class。至此‘进入过渡’的部分完毕。</p>
<p>再来看‘离开过渡’的方法leave,在leave方法中打断点,发现html标签的状态如下</p>
<pre><code class="html"><p>xxx</p>
<!----></code></pre>
<p><!----> 为vue的占位符,当元素通过v-if隐藏后,会在原来位置留下占位符。那就说明,当leave方法被触发时,原本的真实dom元素已经隐藏掉了(从vnode中被移除),而正在显示的元素,只是一个真实dom的副本。</p>
<p>leave方法关键代码其实和enter基本一致,只不过是将startClass换为了leaveClass等,还有处理一些动画生命周期的勾子函数。在动画结束后,调用了由组件生命周期remove传入的rm方法,把这个dom元素的副本移出了文档流。</p>
<p>如有错误,欢迎指正。</p>
<p>这篇并没有去分析Vue core相关的内容,推荐一篇讲Vue core非常不错的文章,对Vue构造函数如何来的感兴趣的同学可以看<a href="https://link.segmentfault.com/?enc=hX%2FDbasfKHaORIn6DL5ojA%3D%3D.YrDNcpQWVr7DBR6pfU5p9DnGH2gTlPzreWaC9E0JQTROJ9Ur3xgZIsLTxtfWJdP%2Bfsu7NXwkq7g8EaW4tPQB1o%2F65mKvhc2LF%2FW%2FbBeCEJ8%3D" rel="nofollow">这里</a></p>
小程序常用优化手段
https://segmentfault.com/a/1190000010094008
2017-07-07T17:16:02+08:00
2017-07-07T17:16:02+08:00
GeoffZhu
https://segmentfault.com/u/geoffzhu
7
<h2>小程序常见优化策略</h2>
<blockquote><p>最近也开发了两个小程序,在开发的过程中,总结一些优化心得</p></blockquote>
<ol>
<li><p>理解wx:if和hidden,并合理利用,官方文档中并没有明确说出hidden如何使用,要不是wx:if文档中提到,经常切换显示和隐藏的节点建议使用hidden,我都不知道还有这个属性。wx:if每次在显示的时候会重绘,而hidden不会。在我开发的项目中,有一个自定义的TabBar,使用hidden和wx:if会有明显的差异。</p></li>
<li><p>Input状态下隐藏input,应预留出键盘收起的时间,具体情况类似下图当点击页面中的取消按钮,页面输入框和mask会消失,因为输入框会消失,引起键盘收起。此时页面会有明显的抖动重绘。我的解决办法是点击取消后,先让键盘消失(大约500ms),再去隐藏input,体验会好很多<br><img src="/img/bVQv7f?w=200&h=356" alt="http://o80ronwlu.bkt.clouddn.com/blog-0702.jpeg" title="http://o80ronwlu.bkt.clouddn.com/blog-0702.jpeg"></p></li>
<li><p>如页面图片过多<br>小程序对用户内存使用进行了限制,如果一个页面的图片过多,会导致内存不足的内部错误,导致应用直接崩溃。解决方法,懒加载</p></li>
<li><p>图片预加载,在浏览器中常用的预加载方法,创建隐藏的image标签,在其他动态的image中如过同时使用此图片,微信会从缓存中读。</p></li>
<li><p>安卓下,没有异步操作,调用hideLoaing后,loading不会消失,猜测原因是showLoading还没有执行完,就调用hideLoading,导致hide失效</p></li>
</ol>
<pre><code class="javascript">wx.showLoading
同步代码
wx.hideLoading</code></pre>
<p>遇到这个问题的原因是,我习惯在onLoad函数中处理当前页面的所有异步请求和数据,所以习惯性的在所有页面的onLoad函数中都写上了loading,但是有一个页面中,并没有请求,而且从storage中读的数据。在IOS下,没什么问题,但在安卓下,loading不会消失。</p>
<p>有很多在Page对象生命周期上加预加载方法的,做接口预加载,但我认为这并不是一个好的优化点,可以在某几个高频页面尝试做,大范围的应用就实在不好了,随时可能被微信咔嚓</p>
<p>常规的一些优化手段,比如压缩代码,合并雪碧图也都是可以使用的。但在有些情况下,比如特别常用的小图标(自定义tabbar上的图标),建议用base64写在CSS中,虽然base64解析慢,但比网络加载图片要快的多。</p>
<p>合理利用各种优化手段,提升小程序体验。你一定会问,什么是合理那?这个只有自己拿捏了,没有思考和实践,查来看去也只能学会一些常规优化套路。要想做深度优化,必然要对业务场景和逻辑有深入理解。</p>
vue-xlsx-table: 在浏览器中查看xlsx或xls表格
https://segmentfault.com/a/1190000008754905
2017-03-20T00:25:07+08:00
2017-03-20T00:25:07+08:00
GeoffZhu
https://segmentfault.com/u/geoffzhu
8
<h2>vue-xlsx-table</h2>
<blockquote><p>无需上传,在浏览器中查看xlsx或xls表格,由js-xlsx驱动.<br><a href="https://link.segmentfault.com/?enc=QOAFfvmyHHnt9tgFoIEO%2Fw%3D%3D.Ic%2BrU%2FeZEtI%2BpTcQTJvHe%2FYfky3bW9XK4%2BnWiUhTbvrUr%2BbDTR5%2FHBcIIZywHb2G" rel="nofollow">在线例子</a></p></blockquote>
<p><a href="https://link.segmentfault.com/?enc=GQYpVmWSdlG8yABgADMyJg%3D%3D.iPP2RcMqHpFoXtmtRGTgMOX%2FntnBQgKbsGVeHKcw69WhDEH06r%2BxpfD6R7egMr8%2B" rel="nofollow"><img src="/img/remote/1460000008754908" alt="npm version" title="npm version"></a></p>
<h3>依赖</h3>
<ul><li><p>vue: ^2.0.0</p></li></ul>
<h3>用法</h3>
<h5>install</h5>
<pre><code class="sh"> npm install vue-xlsx-table --save</code></pre>
<h5>main.js</h5>
<pre><code class="javascript">import 'vue-xlsx-table/dist/style.css'
import vueXlsxTable from 'vue-xlsx-table'
Vue.use(vueXlsxTable, {rABS: false}) //HTML5 FileReader API 有两个方法可以读取本地文件 readAsBinaryString 和 readAsArrayBuffer, 默认rABS为true,也就是使用readAsBinaryString</code></pre>
<h5>file.vue</h5>
<pre><code class="vue"><template>
<div id="app">
<h1>vue-xlsx-table</h1>
<vue-xlsx-table @on-click-ok="handleOk"></vue-xlsx-table>
</div>
</template>
<script>
export default {
name: 'app',
methods: {
handleOk (convertedData) {
console.log(convertedData)
}
}
}
</script></code></pre>
<h3>开发</h3>
<pre><code>npm run dev //develop
npm run build //production</code></pre>
让CSS更完美: PostCSS-modules
https://segmentfault.com/a/1190000008135263
2017-01-16T15:35:54+08:00
2017-01-16T15:35:54+08:00
GeoffZhu
https://segmentfault.com/u/geoffzhu
2
<blockquote><p>译者注(<a href="https://link.segmentfault.com/?enc=s4Jmn4jtTRufUcCgKyXaeQ%3D%3D.diZAL0LP1WiNrdROv1Eh2kBY26Fvd59tYhiDn5DlD98%3D" rel="nofollow">GeoffZhu</a>): 这篇适合一些使用过预处理CSS的开发者,比如less,sass或stylus,如果你都没用过,那你一定不是个好司机。在PostCSS中早就可以使用CSS Modules了,该篇作者贡献了一个新工具,可以让更多开发者方便的使用最新的CSS Modules。</p></blockquote>
<p>我们和全局作用域的css斗争了多年,现在终于是时候结束它了。不管你用的是什么语言还是框架,CSS命名冲突将不再是个问题。我将给你展示一下<a href="https://link.segmentfault.com/?enc=RiGFBYoorG9mhrMDpR%2F3fg%3D%3D.Llr%2BglfDy87loiQ4JK5gJoka6vjQEc1%2Fb14SbWjC3JY%3D" rel="nofollow">PostCSS</a>和<a href="https://link.segmentfault.com/?enc=iQPOnWLwzRDQXs%2BKoQbjzw%3D%3D.3oceWyj%2B8daIJ9lxNFz90KYfLbFMaUBdn4%2BhZ8581KhZ2ZDNGxAc2Dzev2uUd3aa" rel="nofollow">PostCSS-modules</a>如何使用,并且可以在服务端使用它们。<br>CSS起初只是一个美化文档的工具,但是事情到1996年发生了变化。浏览器中不再单单只有文档了,即时通讯,各种软件,游戏,没什么是浏览器不能承载的。</p>
<blockquote><p>当今,我们在HTML和CSS方面已经走了很远很远,开发者们激发出了CSS所有的潜力,甚至创造出了一些CSS本身都快驾驭不了的东西。</p></blockquote>
<p>每一个有经验的开发者都知道 — 每次使用全局命名空间都是留下了一个产生bug的隐患,因为很快就可能出现类似命名冲突之类的问题,再加上其他方面(项目越来越大等)的影响,代码越来越不易维护。</p>
<p>对于CSS来说,这意味着有问题的布局。<a href="https://link.segmentfault.com/?enc=Uwbuc1dUjJ5XEf8lo6KXIw%3D%3D.LcU4kSDZPqa2jEq12LZzo2jxFPzHrp3%2BqU%2BuhWCKa87JIvp2hCUTML6TJfyA8mcaak232GOjbHMNf5Y72kbaFmfmOVB86HYahu3KgtlhN5gCI8PNUFuNXS8e9Vct%2B6qQ" rel="nofollow">CSS特异性</a>和CSS宽泛性之间,一直存在着如史诗般的对决。仅仅是因为每个选择器都可能会影响到那些不想被影响的元素,使之产生了冲突。</p>
<p>基本所有编程语言都支持局部作用域。和CSS朝夕相伴的JavaScript有AMD, CommonJS和最终确定的ES6 modules。但是我们并没有一个可以模块化CSS的方法。</p>
<p>对于一个高质量项目来说,独立的UI组件(也就是组件化)非常重要的 — 每个组件小巧独立,可以拼合成复杂的页面,这让我们节省了很多的工作。但是我们始终有一个疑问,如何防止全局命名冲突那?</p>
<h2>解决方法</h2>
<p>因为有前人的探寻,现在我们有<a href="https://link.segmentfault.com/?enc=ZTa5640BgCPJzfhoRCLiMA%3D%3D.ypZ0UmZztqrlMd27NK6bcb%2BpSf6SFXo8ZcMTngXT5HU%3D" rel="nofollow">Object-Oriented CSS</a>, <a href="https://link.segmentfault.com/?enc=882zFocmfaZMNi3qwsddCA%3D%3D.POiceatd7N2qa7T0Ha%2FzpoBfKxJrLyHyGiAjrDny0zY%3D" rel="nofollow">BEM</a>, <a href="https://link.segmentfault.com/?enc=SXmWH3ciQ9G%2BBPZd6jVUhQ%3D%3D.DCxMS2XNSRb2SW1ReBdmCazKVVYxRXSiNtY7uOhXS6s%3D" rel="nofollow">SMACSS</a>等等,这些都是非常棒并且非常有用的方法。他们通过增加前缀的办法,解决了命名冲突的问题。</p>
<p>通过增加前缀的办法解决命名冲突是个体力活(<a href="https://link.segmentfault.com/?enc=Ik1e8TzUSsw7I62RDQ%2F%2BXg%3D%3D.vkoYAPLjuxvAm%2Bsc6h8v5EabyF1lA1ptDWOLj5LZHZNTnohfa25SHFnGpbRjmoz0" rel="nofollow">manual mangling</a>)。我们手动的去编写长长的选择器。你也可以使用<a href="https://link.segmentfault.com/?enc=NSFO3IQ7FOjySsxmRyISbg%3D%3D.5IWd5X2zQnXVqCVEz7alF5QU9auhEOrqEzycNnPXZJgQ692S4oEyek866QgjZ48heo3y2z0MfrgBOhsq2l4vPRmGVBUA7D2yAkcpIdg2zVxbtnCPy1gK6EcjkTVtuJwh" rel="nofollow">预编译</a>的css语言,但是它们并没有从根本上解决问题(还是体力活)。下面是我们用BEM规范书写的一个独立组件(对于现有的除BEM之外的方法,思想上基本也是这样):</p>
<pre><code>/* 普通 CSS */
.article {
font-size: 16px;
}
.article__title {
font-size: 24px;
}
/* 使用css预处理语言 */
.article {
font-size: 16px;
&__title {
font-size: 24px;
}
}</code></pre>
<h2>CSS模块(CSS Modules)</h2>
<p>2015年出现了另外两种方法的实现。分别是<a href="https://link.segmentfault.com/?enc=r6pZ0Y%2BVwvksP9ZMwit0yQ%3D%3D.CNUmitL1LkVL9TLBTdBUJRlGn39zJduWCI5%2BP%2Fd6X0DFB81y2ZWR%2BlWZgrRGlj%2F5I2NmmPMHc7rUylraurI%2FicTNXQ8acpXi9WDPaNK3brk%3D" rel="nofollow">CSS-in-JS</a> 和 <a href="https://link.segmentfault.com/?enc=mXwsM%2BqZb0YzkNWnB5AgwA%3D%3D.4GRWZIj0QANYx7z0sScoLCTBNGzgaGCebMYKFuux9leLAEvbe7Amb5vso2Un9s1m" rel="nofollow">CSS Modules</a>。我们将主要谈论后者。</p>
<p>CSS模块允许你将所有css class自动打碎,这是CSS模块(CSS Modules)的默认设置。然后生成一个JSON文件(sources map)和原本的class关联:</p>
<pre><code>/* post.css */
.article {
font-size: 16px;
}
.title {
font-weight: 24px;
}</code></pre>
<p>上面的post.css将会被转换成类似下面这样:</p>
<pre><code>.xkpka {
font-size: 16px;
}
.xkpkb {
font-size: 24px;
}</code></pre>
<p>被打碎替换的classes将被保存在一个JSON对象中:</p>
<pre><code>`{ "article": "xkpka", "title": "xkpkb" } `</code></pre>
<p>在转换完成后,你可以直接引用这个JSON对象到项目中,这样就可以用之前写过的class名来直接使用它了。</p>
<pre><code>import styles from './post.json';
class Post extends React.Component {
render() {
return (
<div className={ styles.article }>
<div className={ styles.title }>…</div>
…
</div>
);
}
}</code></pre>
<blockquote><p>更多给力的功能, 可以看看 <a href="https://link.segmentfault.com/?enc=keWr%2FB2mciXn%2F4kP8Np5uA%3D%3D.%2FzULoLa0R0inxy%2FSh9v97RPlNpnzKQLAgnD2PYt9o4B%2FTmvbEfOcMH62KuaeZT8Z" rel="nofollow">这篇非常好的文章</a>.</p></blockquote>
<p>不光是保留了之前提到的几种方法的优点,还自动解决了组件css分离的问题。这就是CSS模块(CSS Modules),听起来非常不错吧!</p>
<p>到这里,我们有遇到了另一个问题: 我们现在的<a href="https://link.segmentfault.com/?enc=nQURrMivOhTQXv22HU2TIA%3D%3D.1huO51Ww5%2BsOrTGNJ7XMhT4OZD0DKbVzfwmnUDQzUIE%3D" rel="nofollow">CSS Modules相关工具</a>,只能在客户端(浏览器)使用,把它放到一个非Node.js的服务端环境中是十分十分困难的。</p>
<h2>PostCSS-modules</h2>
<p>为了在服务端和客户端都能使用CSS Modules,我写了个<a href="https://link.segmentfault.com/?enc=mie%2FTVazxEjg4%2FQ5sM7h1g%3D%3D.amiiaj9PJEHpYQJMP06%2FOX8cBgmndk89hr9Hqcv4%2BQPWfzmLcG8FGl%2FgU5j3o1oO" rel="nofollow">PostCSS-modules</a>,它是一个PostCSS插件,让你可以在服务端使用模块化的CSS,并且服务端语言可以是Ruby, PHP, Python 或者其他语言。</p>
<p><a href="https://link.segmentfault.com/?enc=DCRO6voNjdHKoLl%2F0i4pqQ%3D%3D.JO75q5uYVohyrcR3dUlDvRtyCDjawIr%2Fuc21tGPfUK0%3D" rel="nofollow">PostCSS</a>是一个CSS预处理器,它是用JS实现的。它支持静态检查CSS,支持变量和混入(mixins),能让你使用现在还未被浏览器支持的未来CSS语法,内联图像等等。例如使用最为广泛的<a href="https://link.segmentfault.com/?enc=CagssGvbm6VljFdk4l5%2BHQ%3D%3D.EgSZer6%2B0mUIRDAD4Ua6pXd88s2rT7eWfo4LdWdp%2FWgTpO%2BOZKhlzJGNoIDdDKAk" rel="nofollow">Autoprefixer</a>,它只是PostCSS的一个插件。</p>
<p>如果你使用Autoprefixer, 其实你早就在用PostCSS了。所以,添加<a href="https://link.segmentfault.com/?enc=e%2F6rb9yDAwH8T0KOzx%2FuRg%3D%3D.hc0qAGaOwDA9uX85rmdf2Qg1AeO4sKSI4oIPZCR1CVFeHDkcu%2B%2F%2Bkz4bQwmlL3MN" rel="nofollow">PostCSS-modules</a>到你的项目依赖列表,并不是一件难事。我先给你打个样(实例),用<a href="https://link.segmentfault.com/?enc=oUTOFQb91D68deCFoPMglQ%3D%3D.%2FdgBWETCvyE9HC5BFtbAcIuqbFUQMgVLMpVff4mGHkQ%3D" rel="nofollow">Gulp</a> and <a href="https://link.segmentfault.com/?enc=%2FE%2BB%2B8N%2FxkI8xvMKJ7gAOA%3D%3D.WKyZ%2FJ1LjI81aKD5yMDergW4GHrnEcpfSFKKvTYTYZI%3D" rel="nofollow">EJS</a>,其实你可以用任何语言做类似的事情。</p>
<pre><code>// Gulpfile.js
var gulp = require('gulp');
var postcss = require('gulp-postcss');
var cssModules = require('postcss-modules');
var ejs = require('gulp-ejs');
var path = require('path');
var fs = require('fs');
function getJSONFromCssModules(cssFileName, json) {
var cssName = path.basename(cssFileName, '.css');
var jsonFileName = path.resolve('./build', cssName + '.json');
fs.writeFileSync(jsonFileName, JSON.stringify(json));
}
function getClass(module, className) {
var moduleFileName = path.resolve('./build', module + '.json');
var classNames = fs.readFileSync(moduleFileName).toString();
return JSON.parse(classNames)[className];
}
gulp.task('css', function() {
return gulp.src('./css/post.css')
.pipe(postcss([
cssModules({ getJSON: getJSONFromCssModules }),
]))
.pipe(gulp.dest('./build'));
});
gulp.task('html', ['css'], function() {
return gulp.src('./html/index.ejs')
.pipe(ejs({ className: getClass }, { ext: '.html' }))
.pipe(gulp.dest('./build'));
});
gulp.task('default', ['html']);</code></pre>
<p>我们只需要执行gulp任务,就能得到转换后的CSS文件和JSON文件,然后就可以在EJS模版里面用了:</p>
<pre><code><article class="<%= className('post', 'article') %>">
<h1 class="<%= className('post', 'title') %>">Title</h1>
...
</article></code></pre>
<p>如果你想看看实际的代码,我在GitHub给你准备了个<a href="https://link.segmentfault.com/?enc=%2BBzCjte50TGZ3co2vftqDw%3D%3D.pY%2F8f0NnT6Fp6Qh%2B8F4ZNaNBfE8asvT33SPxm%2F5rsfbONljuMynbYg2ly%2BSTfiC79yO6pKB4RUJj%2F6U6%2BYtbrw%3D%3D" rel="nofollow">example</a>。更多的例子可以看<a href="https://link.segmentfault.com/?enc=%2FwGkdE5h14lIEolqvOAHEg%3D%3D.c%2B92ER9RX1d5ta1zGk63m2ojkV%2BXYhOwBxDAURzDxYfIvOC2PmwDqtRC1kQfGvxO" rel="nofollow">PostCSS-modules</a>和<a href="https://link.segmentfault.com/?enc=0Ka2kGhAS%2FeG%2FL3ReSGJlA%3D%3D.BFZZWagnM%2BXWMvxsLYD1pLCOSz6ect69s46wTK%2FLD3mPaI%2Ff9IgkhDy6wnu82F4g" rel="nofollow">CSS Modules</a></p>
<ul><li><ul><li><p>*</p></li></ul></li></ul>
<p>轻松编写可维护的CSS,没有臃肿的mixins。长长的前缀将成为历史,欢迎来到未来的CSS世界。</p>
一张图让你看懂Exress & Koa
https://segmentfault.com/a/1190000008120479
2017-01-14T15:39:33+08:00
2017-01-14T15:39:33+08:00
GeoffZhu
https://segmentfault.com/u/geoffzhu
0
<h2>一张图让你看懂Exress & Koa</h2>
<blockquote><p>之前我一直使用Express,对整体框架的思想也很清楚,就是把请求当成流,沿着中间件一层一层的过,每一层都可以对请求进行处理,最终处理完成的,返回给客户端。那Koa那?</p></blockquote>
<p>Koa官网明确说明,Koa为Express原班人马打造,为什么好好的Express不用,而去继续造轮子那?</p>
<p>看了些文档,画了这张图,展示Exress & Koa的设计思想的区别。</p>
<p><img src="/img/remote/1460000008120482?w=1000&h=800" alt="" title=""></p>
响应式邮件设计工具推荐
https://segmentfault.com/a/1190000008060361
2017-01-09T17:11:41+08:00
2017-01-09T17:11:41+08:00
GeoffZhu
https://segmentfault.com/u/geoffzhu
1
<blockquote><p>最近工作上需要做一些HTML邮件模版,就是在平时邮箱里收到的那种推广邮件。深入研究之后,才知道这坑有多深。</p></blockquote>
<h2>回到蛮荒时代</h2>
<p>最初我的理解是,既然是HTML,我大前端的看家本领!半小时搞定没啥问题。试了才知道,HTML邮件没有任何标准,而且不管是HTML和CSS全部都是被阉割了的(啥?你还想用JS?),并且不同邮件客户端之间差异也很大。对于div和float的处理也带有很多的不确定性,所以浮动布局,flexbox啥的一样也用不了,只能用90年代流行的表格布局,90年代啊!开发体验是奇差无比,更重要的是我并不想去学过时的table布局。</p>
<p>我这次的要做的是出模版,服务端套,还需要考虑到收件人可能用的是手机客户端查看的邮件,所以需要响应式。对于我来说,可以拖拽的邮件工具肯定是不合适的,它并不能很好的实现设计和需求,况且作为一名开发者应以它为耻。</p>
<h2>找工具</h2>
<h3><a href="https://link.segmentfault.com/?enc=n%2Bkw87RTPpaPhNTgqVpihA%3D%3D.rxA6a7RN4BlalEfqnNsAXhuf6DNF%2F8tDaCcpLdcnQqwKcLjhTpJvH7TJkfQhpn6X" rel="nofollow">1. foundation-emails</a></h3>
<p>首先GitHub找到了<a href="https://link.segmentfault.com/?enc=SYy%2F5VSjNqM1hBykxjbBcQ%3D%3D.w647tTzXvlCCxmhfkgzxI98aX65IycR65NOMH2v1qVYDGVeLqv04roNd6d6UGPAz" rel="nofollow">foundation-emails</a>,扫了一下文档和DEMO,感觉非常好用,还能用sass,狂笑!!等模版做完了,按照文档<code> npm run build </code> 文档上说这是把所有样式插到行内(没看到会压缩啊),当我执行之后,我看到的是这样的画面。<br><img src="/img/remote/1460000008060364?w=1594&h=764" alt="" title=""><br>还是要在build的之前往里面插入服务端模版引擎的循环语句?只能手动去格式化html代码再加循环?我首先试了试第一个在模版里面想替换的地方加几个模版引擎常用的<code> {{ }}</code>,结果build之后,直接就编译没了,再去GitHub看看,找到了这条还没有修复的<a href="https://link.segmentfault.com/?enc=bCRkHMuI%2Bz9xY6icn2hy9w%3D%3D.A6xpSunIUrZY8%2BjwslUcQdIsJNilO0ygbOQ1mnXJq7tnpSiAM7HW89WzDWyEWrTj3%2BAvsU0vnh1mItBsYl9XCg%3D%3D" rel="nofollow">issue</a>,看来大家都还没什么优雅的办法。</p>
<p>对于只需要替换个用户名或者只有少部分内容需要动态的邮件,foundation-emails是非常不错的选择,在其中使用响应式非常非常简单,格栅布局,就像这样</p>
<pre><code class="HTML"><columns large="6" small="12"></code></pre>
<h3><a href="https://link.segmentfault.com/?enc=HkWiZzUoMVvEbOYPPgNvUA%3D%3D.%2BhMTy349QSbbh4I9xVfsH%2BheLngAaFFXsJ8UyVjXMwY%3D" rel="nofollow">2. mjml</a></h3>
<p>在社区里找到了这款工具,用它写的模版大概是这样的, 也很不错。</p>
<pre><code class="HTML"><mjml>
<mj-body>
<mj-container>
<mj-section>
<mj-column>
<mj-image src="/assets/img/easy-and-quick.png" width="112"></mj-image>
<mj-text font-size="20px" color="#595959" align="center">Easy and Quick</mj-text>
</mj-column>
<mj-column>
<mj-image src="/assets/img/responsive.png" width="135"></mj-image>
<mj-text font-size="20px" color="#595959" align="center">Responsive</mj-text>
</mj-column>
</mj-section>
<mj-section>
<mj-column>
<mj-button background-color="#F45E43" font-size="15px">Discover</mj-button>
</mj-column>
</mj-section>
</mj-container>
</mj-body>
</mjml></code></pre>
<p>而且它还有客户端,客户端中可以导出html,导出的代码还算清新。动态替换的内容,我通过官方demo中发现用<code>[[ ]]</code>包裹就没什么事了。</p>
<h2>写在最后</h2>
<p>这篇就是推荐俩工具。如果需要,请留言留言,我可以出个小教程啥的,完!</p>
vue-event-calendar 事件日历插件
https://segmentfault.com/a/1190000007898857
2016-12-23T13:29:32+08:00
2016-12-23T13:29:32+08:00
GeoffZhu
https://segmentfault.com/u/geoffzhu
20
<h2>vue-event-calendar</h2>
<blockquote><p>vue-event-calendar是一款简单小巧的事件日历组件,针对Vue2开发。样式美观,且响应式。<br><a href="https://link.segmentfault.com/?enc=rQDiFJsUWaH3L6bFlvyZ4Q%3D%3D.8Z9V7%2FuFkkzqpk%2FctTOxT9KHP%2F2aA8o2m1t%2BV6VX7yHguBUXiXP1XbngvEgf%2Fp0Y" rel="nofollow">在线例子</a></p></blockquote>
<p><img src="/img/remote/1460000007898860?w=591&h=415" alt="" title=""></p>
<p><a href="https://link.segmentfault.com/?enc=w0uTSrYcsWzbuckctspdHQ%3D%3D.m0ZSzfgxAa2VIUWW5zqqU2nP00%2Bj7E1nVnT9gBPNhkh2Cq%2FFFEcTZCIptPIGMU57bk%2BcJNuyRrZ3g848ocTjLw%3D%3D" rel="nofollow"><img src="/img/remote/1460000007898861" alt="npm version" title="npm version"></a></p>
<h3>依赖</h3>
<ul><li>vue: ^2.0.0</li></ul>
<h3>使用方法</h3>
<h5>安装</h5>
<pre><code class="sh"> npm install vue-event-calendar --save</code></pre>
<h5>入口 Main.js</h5>
<pre><code class="javascript">import 'vue-event-calendar/dist/style.css' //1.1.10之后的版本,css被放在了单独的文件中,方便替换
import vueEventCalendar from 'vue-event-calendar'
Vue.use(vueEventCalendar, {locale: 'en'}) //可以设置语言,支持中文和英文</code></pre>
<h5>用法示例</h5>
<pre><code class="vue"><template>
<vue-event-calendar :events="demoEvents" @monthChanged="" @dayChanged=""></vue-event-calendar>
</template>
<script>
export default {
data () {
return {
demoEvents: [{
date: '2016/12/15',
title: 'eat',
desc: 'longlonglong description'
},{
date: '2016/11/12',
title: 'this is a title'
}]
}
},
methods: {
monthChange (month) {
console.log(month)
},
dayChange (day) {
console.log(day)
}
}
}
</script></code></pre>
<h3>自定义事件模版(可以允许你展示更多信息)</h3>
<p>vue-event-calendar允许自定义事件模版,但是这个功能需要Vue 2.1.0版本以上才可以使用。原因是我试用了2.1.0以上才有的新功能作用域插槽(Scoped Slots)。</p>
<pre><code class="vue"><template>
<vue-event-calendar :events="demoEvents">
<template scope="props">
<div v-for="(event, index) in props.showEvents" class="event-item">
<!-- 这里拿到的是传入的单个event所有数据 -->
{{event}}
</div>
</template>
</vue-event-calendar>
</template>
<script>
export default {
data () {
return {
demoEvents: [{
date: '2016/12/15',
title: 'eat',
desc: 'longlonglong description'
},{
date: '2016/11/12',
title: 'this is a title'
}]
}
}
}
</script></code></pre>
<h3>组件事件</h3>
<p>可以监听的事件有两个,选择了哪天和当月是哪月,当发生改变时,会触发监听函数。函数中的回调参数为改变后的日期。</p>
<pre><code><template>
<vue-event-calendar
:events="demoEvents"
@day-changed="handleDayChanged"
@month-changed="handleMonthChanged">
</vue-event-calendar>
</template></code></pre>
<h3>Options</h3>
<pre><code> // 当 Vue.use时, 可以设置的参数
{
locale: 'en',
color: 'black', //Set main color
className: 'Custom className for current clicked date', // (default: 'selected-day')
weekStartOn: 'week Start on which day' // Can be: 1, 2, 3, 4, 5, 6, 0 (default: 0)
}</code></pre>
<h3>API</h3>
<pre><code class="javascript">// 下个月
this.$EventCalendar.nextMonth()</code></pre>
<pre><code class="javascript">// 上个月
this.$EventCalendar.preMonth()</code></pre>
<pre><code class="javascript">//到指定日期
this.$EventCalendar.toDate('2016/11/12')</code></pre>
<p>可以看我写的<a href="https://link.segmentfault.com/?enc=twhnA8GYpqhfPCgixwb5Gg%3D%3D.wcygwHna9lJOPQPOlnqMkN%2FkrIXp8wzw2aC2WRkFmkEztPIC0qJBUqWFFLQn9%2B1DWVJ%2BlNcmC0roMlJOPwKGDg%3D%3D" rel="nofollow">Demo</a></p>
<h3>开发</h3>
<p>可以在github直接clone我的项目然后执行如下命令继续二次开发或发版,欢迎star&&issue</p>
<pre><code>npm run dev //develop
npm run build //production</code></pre>
<h3>Change log</h3>
<h6>1.3.6 -> 1.4.0</h6>
<ul>
<li>去除了当天的背景,改用一个在日期下面的小圆点替代</li>
<li>增加选中日期样式</li>
</ul>
(译)原生CSS网格布局学习笔记
https://segmentfault.com/a/1190000007651321
2016-11-30T17:50:04+08:00
2016-11-30T17:50:04+08:00
GeoffZhu
https://segmentfault.com/u/geoffzhu
1
<blockquote><p>注:此文是我翻译的第一篇技术文章。适合有一定CSS原生网格布局使用经验的开发者(读前需要先去了解一下原生CSS网格的语法),原生CSS网格布局(Native CSS grid)截止目前还没有被任何正式版本的浏览器实现。<a href="https://link.segmentfault.com/?enc=KQq1UXhXsVCpU6P4Mnjp1w%3D%3D.UqaK6gv94jmpHVADWZCx3hTPKwcRqAyN7uOA%2FOtxfhYfE9F126N3uzlDNoi37aWnivaH6Ivk3BITWV0xJrfeFQ%3D%3D" rel="nofollow">原文地址</a>,<a href="https://link.segmentfault.com/?enc=2x2oFxltptCTJApjs54tbg%3D%3D.hy%2FjEOIvGvF7VuisyFfDrLkSskehskOHd7wXwoIuR%2Bs%3D" rel="nofollow">译者Blog</a></p></blockquote>
<p>_以下是来自Oliver Williams的帖子. Oliver已经学习了相当长时间的原生CSS网格,可以说是在CSS网格方面有一定的发言权。在这篇文章中,他将以非同寻常的思路分析自己的CSS网格布局学习之路。我比较赞同他的想法,就是学习一门新技术的时候,把它们拆分成比较小的单元块并配上实例,一步一步的学习。这比直接学习<a href="https://link.segmentfault.com/?enc=KARhLIhW60Hs9NrN3Ce0lQ%3D%3D.yu9J%2BF%2B%2BgcZCUMGU2ggGXqUsggy6Ag2jvUUEo7IkV3mJVAHxLtGDe8ktXBdK%2BrKK%2Flo1o9WL5THoSKCn%2Fy4w1Q%3D%3D" rel="nofollow">网格布局的所有东西</a>要好太多了。</p>
<p>浏览器原生CSS网格预计会在2017年年初得到支持. <strong>在这之前你需要在浏览器中开启这个实验性的功能</strong> (Firefox实验版默认是开启的). <em><a href="https://link.segmentfault.com/?enc=UzX4gA%2FQuO4NNcnrPHrDmA%3D%3D.nCNbqf5M4%2Bz2XjfOTJgD2cKQnvL8Oa0ldLdkuNuow1LHO0u6DP39%2Fp%2FvG0YwWM7ymz%2FDP1WixW%2B4vOtTolRLqw%3D%3D" rel="nofollow">Chrome Canary</a>是当前最好的实现.</em> 同时,火狐有一个非常好的插件叫CSS Grid Inspector, 它能显示出网格的线,它是目前唯一可以在浏览器中运行的此类工具。</p>
<p>在 chrome的地址栏中输入<code>chrome://flags</code>, 找到 ‘实验性网络平台功能’ 并开启它. IE 和 Edge 实现的是一个比较老的网格标准,现在并不受支持。</p>
<h3>网格布局不是将零散的块拼到一起</h3>
<p>相信我,很快你就能掌握它的.</p>
<p><img src="/img/remote/1460000007651324?w=1080&h=712" alt="" title=""></p>
<p>网格布局只能像左边那样,以矩形的单元块组合起来。并不能像右图那样,由一堆零散的多边形(跟俄罗斯方块那样的块)拼凑。</p>
<h3>设计网格布局并不是为了取代弹性盒,相反,它是弹性盒的一种补充</h3>
<p>虽然网格布局和弹性盒在某些方面起到相似的作用,而且你可以发现,很多人用弹性盒来实现网格布局,但这并不是设计弹性盒的初衷。Jake Archibald的这篇博文值得一读_<a href="https://link.segmentfault.com/?enc=cyYWmhousCF54rIMtZm9Bg%3D%3D.7Av0ZRU3tiyyu7B8H%2Fa6sUVu0NLMTnE498yeekYpWqntqrAycYp4UWCk7%2BIctby0ZtSDPMWe%2Fq8nLZWrvADj%2BaJEmxuBYofFIccbPBZE8wI%3D" rel="nofollow">Don't use flexbox for overall page layout</a>。</p>
<p>这篇博文大概的意思是:</p>
<ul>
<li><p>Flexbox(弹性盒)是为一维布局设计的(行或列)。</p></li>
<li><p>CSS网格是为二维设计的.</p></li>
</ul>
<p>Rachel Andrews也 <a href="https://link.segmentfault.com/?enc=jgcJfo0Tr16Emcq3UiY%2BfQ%3D%3D.60rXepWq9K%2BT8WmG%2FpAS4mMYGfCmL0dQm%2Ftcv%2Fr7jIi2e1%2FvLiTVFWrfCxQSDHjwP9qhD%2Ftcd0aFp3ecebTzHpPKaKvXjmT02QvbRWlvcN0%3D" rel="nofollow">说过类似的话</a>:</p>
<blockquote><p>Flexbox(弹性盒)用于一维布局 – 也就是行或者列. 网格用于二维布局 – 也就是多行多列.</p></blockquote>
<p>它们可以很好的结合,你可以往弹性容器中放入网格,也可以在网格块中加入flex元素</p>
<p>来看个例子吧。 我们想在一个网格元素(grid item)里垂直居中一段文字, 但我们想要让背景(图片,颜色或渐变)覆盖整个的网格区域。 我们可以使用<code>align-items</code>属性,并把它的值设为center,但是如果这样背景并不会填满整个网格元素的区域。<code>align-items</code> 默认的值是 <code>stretch</code>-你不改变它,始终会填满整个空间的。我们把网格元素设为<code>align-items:center</code>并把网格元素(grid item)设置为一个弹性容器(flex container)。</p>
<pre><code>.grid {
align-items: stretch;
}
.griditem {
display: flex;
align-items: center;
}</code></pre>
<h3>给grid-column-end设置负值,意想不到的有用</h3>
<p>在小屏幕下,写一个12列的网格,所有格子的跨度都12列。</p>
<p>你可以用网格这样做:</p>
<pre><code>/* For small screens */
.span4, .span6, .spanAll {
grid-column-end: span 12;
}
/* For large screens */
@media (min-width: 650px) {
.span4 {
grid-column-end: span 4;
}
.span6 {
grid-column-end: span 6;
}
}</code></pre>
<p>这样的显示效果是没什么错误的,当使用CSS网格,重新定义列数非常简单。并且你可以通过设置<code>grid-column-end: -1;</code>来让你的页面始终是从左到右贯穿的。</p>
<pre><code>/* For small screens */
.span4, .span6, .spanAll {
grid-column-end: -1;
}</code></pre>
<p>在大屏幕上,你想要尽可能的接近12列,但是在移动端,一行大概是1~4列。用media来改变<code>grid-template-columns</code>是非常容易的。</p>
<pre><code>.grid {
grid-template-columns: 1fr 1fr;
}
@media (min-width: 700px) {
.grid {
grid-template-columns: repeat(12, 1fr);
}
}</code></pre>
<p>有一些元素,我们想让它贯穿整个视口,比如像 header, footer,和一些大图啥的。</p>
<p>对于小屏幕,我们可以这样写:</p>
<pre><code>.wide {
grid-column: 1 / 3; /* start at 1, end at 3 */
}</code></pre>
<p>不幸的是,当我们换到大屏的时候,一行12列,这些元素将仅仅占满前两列,并不会占满12列,我们需要定义新的<code>grid-column-end</code>,并且把他的值设为 <code>13</code>. 这种方式比较麻烦,还有一种简单的方式,<code>grid-column: 1 / -1;</code>,这样不论在什么屏幕尺寸下,它们都是占满整行的了。就像下面这样:</p>
<pre><code>.wide, .hero, .header, .footer {
grid-column: 1 / -1;
}</code></pre>
<p>See the Pen <a href="http://codepen.io/cssgrid/pen/wzgaRN/">Easier media queries with -1</a> by CSS GRID (<a href="http://codepen.io/cssgrid">@cssgrid</a>) on <a href="https://link.segmentfault.com/?enc=XYTg8TbEOezaD%2BwbY8sZDA%3D%3D.xmHt14R0zPD%2FJHAxFaPhgbBgLRCaPjF2F69uQJSCVJE%3D" rel="nofollow">CodePen</a>.</p>
<h3>网格区域可以命名,并使用一些隐含的名字</h3>
<p>使用<code>grid-template-areas</code>和<code>grid-line-numbers</code>是两种控制行数的属性,你也可以两个同时用。你可以使用那些隐含的行名去设置你的网格。</p>
<pre><code>.grid {
grid-template-areas: "main main sidebar sidebar";
}</code></pre>
<p>这段代码,我们能得到四个隐含名字,main-start, main-end, sidebar-start, 和 sidebar-end.</p>
<p>这可能很有用,如果你想重叠内容,无论是在几个网格区域或在一个特定分段的网格区域。</p>
<p>See the Pen <a href="http://codepen.io/cssgrid/pen/JRyJyY/">implicit line names with grid areas</a> by CSS GRID (<a href="http://codepen.io/cssgrid">@cssgrid</a>) on <a href="https://link.segmentfault.com/?enc=vlIUqvyuvFoOo3FVXe1oJw%3D%3D.7uSWe0C31RQ%2Bx5XDKTaqx5ttifoMk0CIp3nQUC6YXyU%3D" rel="nofollow">CodePen</a>.</p>
<h3>定义网格区域的另一种方式</h3>
<p>就像给网格的行命名,特殊的行名能用于设置网格区域,语法是这样的:</p>
<pre><code>.grid {
grid-template-areas:
"header header header"
"main main sidebar"
"footer footer footer";
} 、</code></pre>
<p>如果你的布局设计(太多列的布局!没列都要起名字,可能还需要空元素)中有很多空的区域,这种写法稍微有点麻烦。所以对于网格是有另一种写法的,在这种写法中,名字是什么无所谓,只要你合理利用到<code>[name-start]</code> 和 <code>[name-end]</code>,也能达到自己的布局目的。下面是一个例子:</p>
<pre><code>.grid {
display: grid;
grid-template-columns: 20px 100px [main-start] 1fr [main-end] 100px 20px;
grid-template-rows: 100px [main-start] 100px [main-end] 100px;
}
.griditem1 {
background-color: red;
grid-area: main;
}</code></pre>
<p>See the Pen <a href="http://codepen.io/cssgrid/pen/amyrVb/">Another way of defining grid-areas</a> by CSS GRID (<a href="http://codepen.io/cssgrid">@cssgrid</a>) on <a href="https://link.segmentfault.com/?enc=UcpJj%2BaxQ0d0ejGwPCoPmA%3D%3D.6HwpzzbsNyEOQUsJDsist7ibztrAxURyjOr06tthu%2BE%3D" rel="nofollow">CodePen</a>.</p>
<p>你可能并不想整个页面都用这种方式布局,但是如果你想要结合 <code>grid-area</code>来确定行数的话,它会非常适合。</p>
<h3>相等尺寸网格(equal sized box layout)使用vmin单位</h3>
<p>虽然你可以在CSS网格中使用任意尺寸的行或列,但是如果想要相等大小的格子并是响应式的,你就需要使用vmin单位了。</p>
<pre><code>.grid {
grid-template-columns: repeat(5, 20vw);
grid-template-rows: repeat(5, 20vh);
}</code></pre>
<p>这种布局在台式电脑和笔记本上基本都可以完美显示,但是在手机上,高度大于宽,内容将会溢出,产生出一个横向的滚动条。Dudley Storey写了篇blog说这件事<a href="https://link.segmentfault.com/?enc=rTScKFkmQGti8fKyFRmedw%3D%3D.nq%2FqivRSpbrh682enQVdZbDLPFAhuYe%2B0CdrMzI10X%2B3%2FN3bPpPrGW9vGVVfLhGNY8oGjiMqyQjvXUoKeq%2B6NeHTJXQjgRqw29DMTg9bXaI%3D" rel="nofollow">the usefulness of a lesser-known css unit: vmin</a>。这种方法,通过调整容器视口的百分比和内容位置,做到适配各种尺寸的屏幕。</p>
<pre><code>.gridcontainer {
display: grid;
width: 100vw;
height: 100vh;
justify-content: center;
align-content: center;
grid-template-columns: repeat(5, 20vmin);
grid-template-rows: repeat(5, 20vmin);
}</code></pre>
<p>See the Pen <a href="http://codepen.io/cssgrid/pen/amkobw/">Boxy Layout with CSS Grid and vmin</a> by CSS GRID (<a href="http://codepen.io/cssgrid">@cssgrid</a>) on <a href="https://link.segmentfault.com/?enc=rEmaMi91iNMFA3ggOcRfaQ%3D%3D.%2Fzhwkk4kLX1KIdJuM7ej%2BSbbXJqtslWFswrRnsH6Hfw%3D" rel="nofollow">CodePen</a>.</p>
<h3>绝对定位</h3>
<p>当我们绝对定位一个网格元素的时候,这个元素会跑到它的容器中,我们可以用grid-column 和 grid-row来定位它。正常情况下,绝对定位使元素脱离文档流,它最适合的使用场景就是想要让元素重叠,并不打乱其他布局元素。除非你为每个元素声明<code>grid-column-start</code> 和 <code>grid-row-start</code>,要不然即使使用了绝对定位,元素也是不会重叠的。</p>
<p>尝试删除这个例子中div的<code>position: absolute;</code>,思考grid-column 和 grid-row的值,也可以试试修改它们,你就明白是什么意思了。</p>
<p>See the Pen <a href="http://codepen.io/cssgrid/pen/NRayjx/">preserving auto-placement with position: absolute</a> by CSS GRID (<a href="http://codepen.io/cssgrid">@cssgrid</a>) on <a href="https://link.segmentfault.com/?enc=%2BwvVA43dA1jY7mffdcTePg%3D%3D.Sq7Gku5499FWd%2BE7mYhtwclI7Ixszg0lAVMKxFNOUuw%3D" rel="nofollow">CodePen</a>.</p>
<h3>改变网格元素(grid item)的顺序</h3>
<p>如果你使用过弹性盒(flexbox)的<code>order</code> 属性,那你已经知道一些相关的知识了。所有的网格元素都有一个默认的order值0。所以如果给一个网格元素设置 <code>order: 1;</code>,这个元素将在所有元素的后面。<br>你可以给<code>order</code>属性设置负值,让它跑到所有item的前面。</p>
<p>See the Pen <a href="http://codepen.io/cssgrid/pen/QKeqKd/">Order value</a> by CSS GRID (<a href="http://codepen.io/cssgrid">@cssgrid</a>) on <a href="https://link.segmentfault.com/?enc=k8x25hqYxTFofltiL6IPsA%3D%3D.Mvp3uvw5AjUmpkLZjcejaTDuglgVhMs48oGeTDG2ZOk%3D" rel="nofollow">CodePen</a>.</p>
<h3>grid中 minmax()的坑</h3>
<p>想不想要整行随着内容的宽度而变宽,直到他们达到最大宽度,这种情况你可能想尝试使用 <code>minmax()</code> :</p>
<pre><code>.grid {
display: grid;
grid-template-columns: repeat(3, minmax(1fr, 300px));
}</code></pre>
<p>不幸的是,像上面这样看似简单,实际上是不行的。如果max小于min的话,css会被忽略。在<code>minmax()</code>中<code>fr</code>不能使用。实际上实现这个需求很容易,在<code>grid-template-columns</code> 或 <code>grid-template-rows</code>中使用<code>auto</code>,这样item就可以随着内容增大而变大了。<br>See the Pen <a href="http://codepen.io/cssgrid/pen/ALQjAj/">The value of auto vs fr</a> by CSS GRID (<a href="http://codepen.io/cssgrid">@cssgrid</a>) on <a href="https://link.segmentfault.com/?enc=bYz%2BIuS1ptcFH14urAQjbw%3D%3D.JRQ6hF%2FSwms5RzqPCCq46PHstophu42zu3lGvcX9o8Q%3D" rel="nofollow">CodePen</a>.</p>
<p>我们可以设置一个 <code>max-width</code>:</p>
<pre><code>.grid {
display: grid;
grid-template-columns: repeat(3, auto);
}
.item {
max-width: 300px;
}</code></pre>
<p>See the Pen <a href="http://codepen.io/cssgrid/pen/jrGppp/">The limits of minmax</a> by CSS GRID (<a href="http://codepen.io/cssgrid">@cssgrid</a>) on <a href="https://link.segmentfault.com/?enc=Yc2ty%2FzkvQ5zDwyVW%2BLv4w%3D%3D.tNiPnkwgv051eDBWdC8AtgAIXk0%2BgP1v2SDB1pEYNso%3D" rel="nofollow">CodePen</a>.</p>
<p><code>minmax()</code>的运行方式和使用我还没有完全想出来,虽然如此,我还是写了一篇文章(译者注:Medium entitled是什么我没有理解清楚,原文:I wrote an entire post on Medium entitled) <a href="https://link.segmentfault.com/?enc=9NCUtZxOelreGfv2B3kWjg%3D%3D.1gIiRtjUOytsW4g9HB6jaAbZGx2EfRFxmgV%2FfXv%2BLFq4SY7tTibPiA3HYQIoDFeZXfYUcp6fHibDd%2FtfOclONElKIUcdYIBHgEoLJU4Qecs9eKdL7niL%2FsJGbF1X252f" rel="nofollow">The One Thing I Hate About Grid</a>.</p>
<h3>如果你给每一个网格线命名了的话,写布局将容易的多</h3>
<p>有多种办法供你选择,如果你就想多写点,你可以给多行设置多个名字。</p>
<pre><code>.grid {
grid-template-columns: [col1-start] 1fr [col1-end col2-start] 1fr [col2-end];
}</code></pre>
<p>最简单的命名约定使用网格自动编号。不是去写 [col2],而是写为<code>col 2</code></p>
<pre><code>.griditem1 {
grid-column-start: col 2;
}</code></pre>
<p>和<code>span</code>关键字组合使用,我们就不用去写column-start和column-end中的各种网格线数字了,这样能直观许多。</p>
<pre><code>.grid {
grid-template-columns: repeat(4, [col] 100px);
}
.griditem1 {
grid-column: col 2 / span 2;
}</code></pre>
<h3>fr单位为什么那个的重要,让你摆脱麻烦的计算</h3>
<p>想象一下一行上四等列这种布局,使用百分比是多么的容易<code>grid-template-columns: 25% 25% 25% 25%</code>。</p>
<p>但是当想用<code>grid-gap</code>属性的时候那?如果设置<code>grid-gap: 10px</code>,那么这一行上将有三个空隙,每个10px,整体的宽度就是100% + 30px,大于100%滚动条就出来了。虽然可以通过计算来解决,但是如果使用fr,这太容易了<code>grid-template-columns: 1fr 1fr 1fr 1fr</code></p>
<p>See the Pen <a href="http://codepen.io/cssgrid/pen/LRzgLJ/">fr unit vs percentage</a> by CSS GRID (<a href="http://codepen.io/cssgrid">@cssgrid</a>) on <a href="https://link.segmentfault.com/?enc=lGjElZewJghE0df9OLCMgQ%3D%3D.MG8F%2FYqxsuPzoGPsxq5%2B%2F855dYQwU%2BfCLWFi3Nf0qGM%3D" rel="nofollow">CodePen</a>.</p>
<h3>网格布局中第二个我较恶心的点</h3>
<p>没有办法强制自动布局算法留下一些行和列是空的。</p>
<p><code>grid-gap</code>可以让我们设置内容间的距离。<code>grid-row-gap</code>和<code>grid-column-gap</code>能设置行或列之间的间隙,可是如果我想让第一行和第二行相距10px,第二行和第三行相距50px,用现有的网格是没法实现的,除非建个空行占位。</p>
<p>你可能见到过像下面这样写<code>grid-template-area</code>的::</p>
<pre><code>grid-template-rows:
"header header header"
"main main main"
" . . ."
"secondary secondary secondary"
"footer footer footer";</code></pre>
<p>应该提供一个比较聪明的办法,让布局算法去做这件事。不幸的是,这样写也没用。此语法简单地表示,我们不想将第三行变成一个命名的网格区域。可是grid-template-rows将仍然在那结束。</p>
<h3>Some design advice: You don't necessarily need 12 columns (and columns need not be uniform in size)</h3>
<h3>一些设计上的建议: 你不一点需要12列网格 (每一列不一定大小一致)</h3>
<p>12列网格算是web design的默认配置了。Bootstrap引导大家用12列网格,导致很多框架都是12列网格。12既能被3整除也能被4整除,能让我们有更多种布局摆放方式。1行12列,1行6列,1行4列,1行3列,1行2列</p>
<p>虽然有些人喜欢每一个项目总是使用相同的网格,但是你应该去思考你真正需要的,有时候没有必要有更多的列,你应该建立一个网格,对针对你的内容去布局,而不是一个12列网格到处用。</p>
<p>看看这个例子 <a href="https://link.segmentfault.com/?enc=Jl52yaRBBTPwSOeYwcGO3w%3D%3D.8FI9LOzy1o6%2BxsGk3xTN56pDCSVXc0GpQglwh9vct5U%3D" rel="nofollow">Gridset</a>. Gridset是一个制作网格非常有用的工具, 但是原生CSS的网格不需要你使用任何工具,但是可以看看它展示的一些良好的网格设计。</p>
<p><img src="/img/remote/1460000007651325?w=900&h=619" alt="" title=""></p>
<p>看看我写的例子,CSS原生网格是多么的自由啊:</p>
<p>See the Pen <a href="http://codepen.io/cssgrid/pen/kkoZro/">text layout with CSS Grid module</a> by CSS GRID (<a href="http://codepen.io/cssgrid">@cssgrid</a>) on <a href="https://link.segmentfault.com/?enc=9fajt%2FtFM2vvxx1JwNHVKA%3D%3D.xBWWOLcltQQG0ATsovSPjsSb%2FkcZ4%2F2ExBRtxxfgiok%3D" rel="nofollow">CodePen</a>.</p>