找大海的nimo

找大海的nimo 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

找大海的nimo 发布了文章 · 2019-06-03

图片懒加载及异常处理

在日常页面开发中,常常会涉及到图片的展示。有时候当图片资源过多时,我们希望能将图片延迟加载,同时当图片加载失败后,能用一张默认图片去代替其进行展示。其效果如图所示:

图片懒加载

一、图片懒加载

其实图片懒加载的核心思想很简单:

  1. 通过预先将图片的src资源指向一张小图片或空,并通过 data-src 来记录其实际图片地址。
  2. 通过延迟加载或监听滚动事件(图片出现在可视区域中), 将 data-src 属性值赋值给 src 实现图片的懒加载。
setTimeout({
    $imgs.each(function () {
        var $img = $(this);
        $img.src = $img.attr('data-src');
    });  
}, 0);

// 设置 500ms 防抖动,避免回调频繁执行,影响性能
$(window).on('scroll', _.debounce(function () {
    var $window = $(window); 
    var top = parseInt($window.height()) + parseInt($window.scrollTop());
    $imgs.each(function () {
        var $img = $(this);
        var oSrc = $img.attr('data-src');
        if (!oSrc) return;
        if ($img.offset().top < top) {
            $img.src = oSrc;
            $img.attr('data-src', '');
        }
}, 500));

二、图片加载错误处理

有些时候,由于网络请求或是资源问题,导致图片资源请求失败,这时图片会展示为非常难看的效果(破碎的图片)。这时可以通过监听图片的 onerror 时间来处理。

$img.onerror = function () {
    $img.src = 'default.jpg';
}

为了避免默认图片也加载失败时,图片资源无限执行的情况,可以利用 jQuery 的 one() api 接口绑定一个一次性的 onerror 事件。

$img.one('error', function () {
    $img.src = 'default.jpg';
});
tip: 在加载图片时,我们可以利用创建一个不在 渲染树 中的 Img 元素来加载图片资源。
var oSrc = $img.attr('data-src');
var img = document.createElement('img');
img.onload = function () {
    $img.src = oSrc;
};
img.onerror = function () {
    console.debug('图片加载失败:', img.src);
    // 此时 $img src 依然为默认图,如果图片是否替换判断不为 data-src ,可不进行清空
    $img.attr('data-src', '');
};
img.src = oSrc;

三、利用 background-size 属性代替 img 标签

大多数的时候,图片所处位置的宽高都是固定的。但是图片实际的宽高都是未知的,图片宽高的设置方式有:

  1. 直接将 img 的宽高设置为 img { width: 100%; height: 100%; }可能会出现图片严重失真。
  2. 通过设置最大宽高的方式 img { max-width: 100%; max-height: 100%; } 会出现上下或左右的留白,同时当资源图片宽高小于容器时,留白更多。

background-size: cover; 能够对图片最大程度的利用,不存在留白和图片失真的问题。不过该方式会存在图片资源的损失(图片不能完全展示)。

.img {
    background-repeat: no-repeat;
    background-size: cover;
    background-position: center;
}

四、利用 Vue 实现一个图片处理指令

export default function (el, binding) {
    setTimeout(() => {
        const img = document.createElement('img');
        img.onload = function () {
            el.style.backgroundImage = `url(${binding.value})`;
        };
        img.onerror = function () {
            console.debug('图片加载失败:', img.src);
        };
        img.src = binding.value;
    }, 0);
};

完整示例代码:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0">
    <title>图片懒加载处理</title>
    <style>
        .img-group {
            display: flex;
            width: 800px;
            flex-wrap: wrap;
            list-style-type: none;
        }
        .img-item {
            flex: 0 0 33.3%;
            height: 100px;
            background-color: aquamarine;
            background-position: center;
            background-repeat: no-repeat;
            background-size: cover;
        }
        .img-item:nth-child(odd) {
            background-color: chocolate;
        }
    </style>
</head>

<body>
    <div id="app"></div>

    <template id="tpl">
        <ul class="img-group">
            <li class="img-item" v-for="img in imgs" v-img=" img"></li>
        </ul>
    </template>

    <script data-original="https://cdn.jsdelivr.net/npm/vue"></script>
    <script>
    new Vue({
        el: '#app',
        template: '#tpl',
        data() {
            return {
                imgs: [
                    'http://inews.gtimg.com/newsapp_match/0/9167593089/0',
                    'http://inews.gtimg.com/newsapp_match/0/9167593090/0',
                    'http://inews.gtimg.com/newsapp_match/0/9167593092/0',
                    'http://inews.gtimg.com/newsapp_match/0/9167593093/0',
                    'http://inews.gtimg.com/newsapp_match/0/9167593095/0',
                    'http://inews.gtimg.com/newsapp_match/0/9167595616/0',
                    'http://inews.gtimg.com/newsapp_match/0/9167595617/0',
                    'http://inews.gtimg.com/newsapp_match/0/9167595618/0',
                    'http://inews.gtimg.com/newsapp_match/0/9167595619/0'
                ],
            };
        },
        directives: {
            img(el, binding) {
                setTimeout(() => {
                    const img = document.createElement('img');
                    img.onload = function() {
                        el.style.backgroundImage = `url(${binding.value})`;
                    };
                    img.onerror = function() {
                        console.debug('图片加载失败:', img.src);
                    };
                    img.src = binding.value;
                }, 0);
            },
        },
    });
    </script>
</body>

</html>

五、图片优化

如果是前端资源图片的话,还可以做图片做一些其他优化:

  • 压缩图片,降低图片大小 (智图:一个图片优化平台
  • 响应式请求图片资源(高清屏请求 @2x 图片 , 一般屏幕 请求 @x 图片,控制图片尺寸,从而缩减图片大小)
  • 减少图片 http 请求次数(雪碧图)
  • 利用字体库代替图标 (canvas 、svg 绘图替代图标)

具体细节说明可以参考: web前端优化之图片优化

查看原文

赞 0 收藏 0 评论 0

找大海的nimo 发布了文章 · 2019-06-03

图片懒加载及异常处理

在日常页面开发中,常常会涉及到图片的展示。有时候当图片资源过多时,我们希望能将图片延迟加载,同时当图片加载失败后,能用一张默认图片去代替其进行展示。其效果如图所示:

图片懒加载

一、图片懒加载

其实图片懒加载的核心思想很简单:

  1. 通过预先将图片的src资源指向一张小图片或空,并通过 data-src 来记录其实际图片地址。
  2. 通过延迟加载或监听滚动事件(图片出现在可视区域中), 将 data-src 属性值赋值给 src 实现图片的懒加载。
setTimeout({
    $imgs.each(function () {
        var $img = $(this);
        $img.src = $img.attr('data-src');
    });  
}, 0);

// 设置 500ms 防抖动,避免回调频繁执行,影响性能
$(window).on('scroll', _.debounce(function () {
    var $window = $(window); 
    var top = parseInt($window.height()) + parseInt($window.scrollTop());
    $imgs.each(function () {
        var $img = $(this);
        var oSrc = $img.attr('data-src');
        if (!oSrc) return;
        if ($img.offset().top < top) {
            $img.src = oSrc;
            $img.attr('data-src', '');
        }
}, 500));

二、图片加载错误处理

有些时候,由于网络请求或是资源问题,导致图片资源请求失败,这时图片会展示为非常难看的效果(破碎的图片)。这时可以通过监听图片的 onerror 时间来处理。

$img.onerror = function () {
    $img.src = 'default.jpg';
}

为了避免默认图片也加载失败时,图片资源无限执行的情况,可以利用 jQuery 的 one() api 接口绑定一个一次性的 onerror 事件。

$img.one('error', function () {
    $img.src = 'default.jpg';
});
tip: 在加载图片时,我们可以利用创建一个不在 渲染树 中的 Img 元素来加载图片资源。
var oSrc = $img.attr('data-src');
var img = document.createElement('img');
img.onload = function () {
    $img.src = oSrc;
};
img.onerror = function () {
    console.debug('图片加载失败:', img.src);
    // 此时 $img src 依然为默认图,如果图片是否替换判断不为 data-src ,可不进行清空
    $img.attr('data-src', '');
};
img.src = oSrc;

三、利用 background-size 属性代替 img 标签

大多数的时候,图片所处位置的宽高都是固定的。但是图片实际的宽高都是未知的,图片宽高的设置方式有:

  1. 直接将 img 的宽高设置为 img { width: 100%; height: 100%; }可能会出现图片严重失真。
  2. 通过设置最大宽高的方式 img { max-width: 100%; max-height: 100%; } 会出现上下或左右的留白,同时当资源图片宽高小于容器时,留白更多。

background-size: cover; 能够对图片最大程度的利用,不存在留白和图片失真的问题。不过该方式会存在图片资源的损失(图片不能完全展示)。

.img {
    background-repeat: no-repeat;
    background-size: cover;
    background-position: center;
}

四、利用 Vue 实现一个图片处理指令

export default function (el, binding) {
    setTimeout(() => {
        const img = document.createElement('img');
        img.onload = function () {
            el.style.backgroundImage = `url(${binding.value})`;
        };
        img.onerror = function () {
            console.debug('图片加载失败:', img.src);
        };
        img.src = binding.value;
    }, 0);
};

完整示例代码:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0">
    <title>图片懒加载处理</title>
    <style>
        .img-group {
            display: flex;
            width: 800px;
            flex-wrap: wrap;
            list-style-type: none;
        }
        .img-item {
            flex: 0 0 33.3%;
            height: 100px;
            background-color: aquamarine;
            background-position: center;
            background-repeat: no-repeat;
            background-size: cover;
        }
        .img-item:nth-child(odd) {
            background-color: chocolate;
        }
    </style>
</head>

<body>
    <div id="app"></div>

    <template id="tpl">
        <ul class="img-group">
            <li class="img-item" v-for="img in imgs" v-img=" img"></li>
        </ul>
    </template>

    <script data-original="https://cdn.jsdelivr.net/npm/vue"></script>
    <script>
    new Vue({
        el: '#app',
        template: '#tpl',
        data() {
            return {
                imgs: [
                    'http://inews.gtimg.com/newsapp_match/0/9167593089/0',
                    'http://inews.gtimg.com/newsapp_match/0/9167593090/0',
                    'http://inews.gtimg.com/newsapp_match/0/9167593092/0',
                    'http://inews.gtimg.com/newsapp_match/0/9167593093/0',
                    'http://inews.gtimg.com/newsapp_match/0/9167593095/0',
                    'http://inews.gtimg.com/newsapp_match/0/9167595616/0',
                    'http://inews.gtimg.com/newsapp_match/0/9167595617/0',
                    'http://inews.gtimg.com/newsapp_match/0/9167595618/0',
                    'http://inews.gtimg.com/newsapp_match/0/9167595619/0'
                ],
            };
        },
        directives: {
            img(el, binding) {
                setTimeout(() => {
                    const img = document.createElement('img');
                    img.onload = function() {
                        el.style.backgroundImage = `url(${binding.value})`;
                    };
                    img.onerror = function() {
                        console.debug('图片加载失败:', img.src);
                    };
                    img.src = binding.value;
                }, 0);
            },
        },
    });
    </script>
</body>

</html>

五、图片优化

如果是前端资源图片的话,还可以做图片做一些其他优化:

  • 压缩图片,降低图片大小 (智图:一个图片优化平台
  • 响应式请求图片资源(高清屏请求 @2x 图片 , 一般屏幕 请求 @x 图片,控制图片尺寸,从而缩减图片大小)
  • 减少图片 http 请求次数(雪碧图)
  • 利用字体库代替图标 (canvas 、svg 绘图替代图标)

具体细节说明可以参考: web前端优化之图片优化

查看原文

赞 0 收藏 0 评论 0

找大海的nimo 收藏了文章 · 2018-03-14

如果有人问你爬虫抓取技术的门道,请叫他来看这篇文章

本文首发于我的个人博客,同步发布于SegmentFault专栏,非商业转载请注明出处,商业转载请阅读原文链接里的法律声明。

web是一个开放的平台,这也奠定了web从90年代初诞生直至今日将近30年来蓬勃的发展。然而,正所谓成也萧何败也萧何,开放的特性、搜索引擎以及简单易学的html、css技术使得web成为了互联网领域里最为流行和成熟的信息传播媒介;但如今作为商业化软件,web这个平台上的内容信息的版权却毫无保证,因为相比软件客户端而言,你的网页中的内容可以被很低成本、很低的技术门槛实现出的一些抓取程序获取到,这也就是这一系列文章将要探讨的话题—— 网络爬虫

banner

有很多人认为web应当始终遵循开放的精神,呈现在页面中的信息应当毫无保留地分享给整个互联网。然而我认为,在IT行业发展至今天,web已经不再是当年那个和pdf一争高下的所谓 “超文本”信息载体 了,它已经是以一种 轻量级客户端软件 的意识形态的存在了。而商业软件发展到今天,web也不得不面对知识产权保护的问题,试想如果原创的高质量内容得不到保护,抄袭和盗版横行网络世界,这其实对web生态的良性发展是不利的,也很难鼓励更多的优质原创内容的生产。

未授权的爬虫抓取程序是危害web原创内容生态的一大元凶,因此要保护网站的内容,首先就要考虑如何反爬虫。

从爬虫的攻防角度来讲

最简单的爬虫,是几乎所有服务端、客户端编程语言都支持的http请求,只要向目标页面的url发起一个http get请求,即可获得到浏览器加载这个页面时的完整html文档,这被我们称之为“同步页”。

作为防守的一方,服务端可以根据http请求头中的User-Agent来检查客户端是否是一个合法的浏览器程序,亦或是一个脚本编写的抓取程序,从而决定是否将真实的页面信息内容下发给你。

这当然是最小儿科的防御手段,爬虫作为进攻的一方,完全可以伪造User-Agent字段,甚至,只要你愿意,http的get方法里, request header的 ReferrerCookie 等等所有字段爬虫都可以轻而易举的伪造。

此时服务端可以利用浏览器http头指纹,根据你声明的自己的浏览器厂商和版本(来自 User-Agent ),来鉴别你的http header中的各个字段是否符合该浏览器的特征,如不符合则作为爬虫程序对待。这个技术有一个典型的应用,就是 PhantomJS 1.x版本中,由于其底层调用了Qt框架的网络库,因此http头里有明显的Qt框架网络请求的特征,可以被服务端直接识别并拦截。

除此之外,还有一种更加变态的服务端爬虫检测机制,就是对所有访问页面的http请求,在 http response 中种下一个 cookie token ,然后在这个页面内异步执行的一些ajax接口里去校验来访请求是否含有cookie token,将token回传回来则表明这是一个合法的浏览器来访,否则说明刚刚被下发了那个token的用户访问了页面html却没有访问html内执行js后调用的ajax请求,很有可能是一个爬虫程序。

如果你不携带token直接访问一个接口,这也就意味着你没请求过html页面直接向本应由页面内ajax访问的接口发起了网络请求,这也显然证明了你是一个可疑的爬虫。知名电商网站Amazon就是采用的这种防御策略。

以上则是基于服务端校验爬虫程序,可以玩出的一些套路手段。

基于客户端js运行时的检测

现代浏览器赋予了JavaScript强大的能力,因此我们可以把页面的所有核心内容都做成js异步请求 ajax 获取数据后渲染在页面中的,这显然提高了爬虫抓取内容的门槛。依靠这种方式,我们把对抓取与反抓取的对抗战场从服务端转移到了客户端浏览器中的js运行时,接下来说一说结合客户端js运行时的爬虫抓取技术。

刚刚谈到的各种服务端校验,对于普通的python、java语言编写的http抓取程序而言,具有一定的技术门槛,毕竟一个web应用对于未授权抓取者而言是黑盒的,很多东西需要一点一点去尝试,而花费大量人力物力开发好的一套抓取程序,web站作为防守一方只要轻易调整一些策略,攻击者就需要再次花费同等的时间去修改爬虫抓取逻辑。

此时就需要使用headless browser了,这是什么技术呢?其实说白了就是,让程序可以操作浏览器去访问网页,这样编写爬虫的人可以通过调用浏览器暴露出来给程序调用的api去实现复杂的抓取业务逻辑。

其实近年来这已经不算是什么新鲜的技术了,从前有基于webkit内核的PhantomJS,基于Firefox浏览器内核的SlimerJS,甚至基于IE内核的trifleJS,有兴趣可以看看这里这里 是两个headless browser的收集列表。

这些headless browser程序实现的原理其实是把开源的一些浏览器内核C++代码加以改造和封装,实现一个简易的无GUI界面渲染的browser程序。但这些项目普遍存在的问题是,由于他们的代码基于fork官方webkit等内核的某一个版本的主干代码,因此无法跟进一些最新的css属性和js语法,并且存在一些兼容性的问题,不如真正的release版GUI浏览器运行得稳定。

这其中最为成熟、使用率最高的应该当属 PhantonJS 了,对这种爬虫的识别我之前曾写过一篇博客,这里不再赘述。PhantomJS存在诸多问题,因为是单进程模型,没有必要的沙箱保护,浏览器内核的安全性较差。另外,该项目作者已经声明停止维护此项目了。

如今Google Chrome团队在Chrome 59 release版本中开放了headless mode api,并开源了一个基于Node.js调用的headless chromium dirver库,我也为这个库贡献了一个centos环境的部署依赖安装列表

Headless Chrome可谓是Headless Browser中独树一帜的大杀器,由于其自身就是一个chrome浏览器,因此支持各种新的css渲染特性和js运行时语法。

基于这样的手段,爬虫作为进攻的一方可以绕过几乎所有服务端校验逻辑,但是这些爬虫在客户端的js运行时中依然存在着一些破绽,诸如:

基于plugin对象的检查

if(navigator.plugins.length === 0) {
    console.log('It may be Chrome headless');
}

基于language的检查

if(navigator.languages === '') {
    console.log('Chrome headless detected');
}

基于webgl的检查

var canvas = document.createElement('canvas');
var gl = canvas.getContext('webgl');

var debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
var vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
var renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);

if(vendor == 'Brian Paul' && renderer == 'Mesa OffScreen') {
    console.log('Chrome headless detected');
}

基于浏览器hairline特性的检查

if(!Modernizr['hairline']) {
    console.log('It may be Chrome headless');
}

基于错误img src属性生成的img对象的检查

var body = document.getElementsByTagName('body')[0];
var image = document.createElement('img');
image.src = 'http://iloveponeydotcom32188.jg';
image.setAttribute('id', 'fakeimage');
body.appendChild(image);
image.onerror = function(){
    if(image.width == 0 && image.height == 0) {
        console.log('Chrome headless detected');
    }
}

基于以上的一些浏览器特性的判断,基本可以通杀市面上大多数 Headless Browser 程序。在这一层面上,实际上是将网页抓取的门槛提高,要求编写爬虫程序的开发者不得不修改浏览器内核的C++代码,重新编译一个浏览器,并且,以上几点特征是对浏览器内核的改动其实并不小,如果你曾尝试过编译Blink内核Gecko内核你会明白这对于一个“脚本小子”来说有多难~

更进一步,我们还可以基于浏览器的 UserAgent 字段描述的浏览器品牌、版本型号信息,对js运行时、DOM和BOM的各个原生对象的属性及方法进行检验,观察其特征是否符合该版本的浏览器所应具备的特征。

这种方式被称为 浏览器指纹检查 技术,依托于大型web站对各型号浏览器api信息的收集。而作为编写爬虫程序的进攻一方,则可以在 Headless Browser 运行时里预注入一些js逻辑,伪造浏览器的特征。

另外,在研究浏览器端利用js api进行 Robots Browser Detect 时,我们发现了一个有趣的小技巧,你可以把一个预注入的js函数,伪装成一个Native Function,来看看下面代码:

var fakeAlert = (function(){}).bind(null);
console.log(window.alert.toString()); // function alert() { [native code] }
console.log(fakeAlert.toString()); // function () { [native code] }

爬虫进攻方可能会预注入一些js方法,把原生的一些api外面包装一层proxy function作为hook,然后再用这个假的js api去覆盖原生api。如果防御者在对此做检查判断时是基于把函数toString之后对[native code]的检查,那么就会被绕过。所以需要更严格的检查,因为bind(null)伪造的方法,在toString之后是不带函数名的,因此你需要在toString之后检查函数名是否为空。

这个技巧有什么用呢?这里延伸一下,反抓取的防御者有一种Robot Detect的办法是在js运行时主动抛出一个alert,文案可以写一些与业务逻辑相关的,正常的用户点确定按钮时必定会有一个1s甚至更长的延时,由于浏览器里alert会阻塞js代码运行(实际上在v8里他会把这个isolate上下文以类似进程挂起的方式暂停执行),所以爬虫程序作为攻击者可以选择以上面的技巧在页面所有js运行以前预注入一段js代码,把alertpromptconfirm等弹窗方法全部hook伪造。如果防御者在弹窗代码之前先检验下自己调用的alert方法还是不是原生的,这条路就被封死了。

反爬虫的银弹

目前的反抓取、机器人检查手段,最可靠的还是验证码技术。但验证码并不意味着一定要强迫用户输入一连串字母数字,也有很多基于用户鼠标、触屏(移动端)等行为的行为验证技术,这其中最为成熟的当属Google reCAPTCHA,基于机器学习的方式对用户与爬虫进行区分。

基于以上诸多对用户与爬虫的识别区分技术,网站的防御方最终要做的是封禁ip地址或是对这个ip的来访用户施以高强度的验证码策略。这样一来,进攻方不得不购买ip代理池来抓取网站信息内容,否则单个ip地址很容易被封导致无法抓取。抓取与反抓取的门槛被提高到了ip代理池经济费用的层面。

机器人协议

除此之外,在爬虫抓取技术领域还有一个“白道”的手段,叫做robots协议。你可以在一个网站的根目录下访问/robots.txt,比如让我们一起来看看github的机器人协议AllowDisallow声明了对各个UA爬虫的抓取授权。

不过,这只是一个君子协议,虽具有法律效益,但只能够限制那些商业搜索引擎的蜘蛛程序,你无法对那些“野爬爱好者”加以限制。

写在最后

对网页内容的抓取与反制,注定是一个魔高一尺道高一丈的猫鼠游戏,你永远不可能以某一种技术彻底封死爬虫程序的路,你能做的只是提高攻击者的抓取成本,并对于未授权的抓取行为做到较为精确的获悉。

这篇文章中提到的对于验证码的攻防其实也是一个较为复杂的技术难点,在此留一个悬念,感兴趣可以加关注期待后续文章进行详细阐述。

另外,欢迎对抓取方面感兴趣的朋友关注我的一个开源项目webster, 项目以Node.js 结合Chrome headless模式实现了一个高可用性网络爬虫抓取框架,借以chrome对页面的渲染能力, 可以抓取一个页面中 所有的js及ajax渲染的异步内容;并结合redis实现了一个任务队列,使得爬虫程序可以方便的进行横向、纵向的分布式扩展。部署起来很方便,我已经为webster提供了一个官方版的基础运行时docker镜像,如果你想先睹为快也可以试试这个webster demo docker镜像

查看原文

找大海的nimo 赞了文章 · 2016-11-04

前端进阶之路:如何高质量完成产品需求开发

写在前面

作为一个互联网前端老鸟,这么些年下来,做过的项目也不少。从最初的我的QQ中心QQ圈子,到后面的QQ群项目腾讯课堂。从几个人的项目,到近百号人的项目都经历过。

这期间,实现了很多的产品需求,也积累了一些经验。这里稍作总结,希望能给新入行的前端小伙伴们一些参考。

做好需求的关键点

要说如何做好一个需求,展开来讲,可以写好几篇文章,这里只挑重点来讲。

最基本的,就是把握好3W:what、when、how。

  • what:做什么?

  • when:完成时间?

  • how:如何完成?

需求场景假设

为了下文不至于太过枯燥,这里进行需求场景的模拟,下文主要围绕这个“需求”,从what、when、how 三个点展开来讲。

假设现在有个论坛的项目,产品经理小C提了个需求 “给论坛增加评论功能” 。作为 前端工程师 的小A接到需求后,该如何高质量的完成这个需求。

  • 项目名称:兴趣论坛。

  • 项目组主要成员:前端工程师小A,后台工程师小B,产品经理小C。

  • 产品需求:给论坛增加评论功能。

备注:此时我们脑海里浮现的应该是下面这张图。

clipboard.png

What:做什么?

可能有同学要拍案而起了:Are you kidding me?不就加个评论功能吗,我还能不知道该做啥?

答案很残酷:是的

根据过往经验,不少前端同学,包括一些前端老司机,做需求的时候,的确不知道自己究竟要做什么。导致这种情况发生的原因有哪些呢?

  1. 产品经理:提的需求不明确。

  2. 前端工程师:没做好需求确认。

情况1:产品需求不明确

说到产品需求不明确,前端的兄弟们估计可以坐一起开个诉苦大会,因为实在太常见了。典型的有“拍脑门需求”、“一句话需求”、“贴个图求照抄需求”。

回到之前的例子:给论坛增加个评论功能。

别看连原型图都贴出来了,其实这就是个典型的“需求不明确”。比如:

  • 是否需要支持富文本输入?

  • 是否需要支持社会化分享?

  • 发表评论后,评论怎么展示?

  • 。。。

也许经过一番确认,最终的需求会是下图所示。遇到这种情况,一定要做好需求确认,避免后期无意义的返工和延期。

clipboard.png

情况2:未做好需求确认

再次强调一下,无论何时,一定要做好需求确认。再有经验、再负责的产品经理,也几乎不可能提出“100%明确”的需求。

同样,回到上面的需求。

现在已经确认了,需要支持富文本输入、需要展示评论,这就够了吗?其实不够,还有很多需求细节需要进一步确认。比如:

  • 评论最大支持输入多少个字?(非常重要,关乎后台存储方案的设计)

  • 1个中文算1个字,多少个英文字母算1个字?(产品语言、技术语言 之间的沟通转换)

  • 输入内容过长,如何进行错误提示?(交互细节)

  • 输入内容过长,是否允许提交评论?如允许,是对评论内容进行截断后提交?(容错)

  • 用户未输入内容的情况下,评论框内默认提示文案是什么?(交互细节)

  • 。。。

可以、需要确认的内容太多,这里就不赘述。

看到这里,读者朋友们应该明白,为什么前面会说,几乎不存在“100%明确”的需求。

很多需求细节,同时也跟技术实现细节强相关,不能苛求产品经理都考虑到。这种情况下,作为开发者的我们应该主动找出问题,并与产品经理一起将细节敲定下来。

clipboard.png

When:完成时间?

一个同时有前端、后端参与的需求,精简后的需求生命周期,大概是这样的:

需求提出-->开发-->联调-->提交测试->需求发布

一个需求的实际发布时间,大部分时候取决于实际的开发工作量。如何评估开发工作量呢?最基本的,就是明确“做什么”,这也就是上一小节强调的内容。

这里我们假设:

  1. 需求已经明确,小A的开发工作量是3天,小B的开发工作量是3天。

  2. 假设小A 9月1号投入开发

那么,是不是9月3号下班前需求就可以发布了?

答案显然是:不能

要得出一个靠谱的完成时间,至少需要明确以下内容:

  • 前端、后台 各自的工作量。

  • 前端、后台 投入研发的时间点。

  • 前端、后台 联调的工作量、时间点。

  • 需求提交测试的时间。

  • 需求测试的工作量。

最终,需求的完成时间点可能如下:(跟预期的出入很大)

clipboard.png

对于需求完成时间的评估,实际情况远比上面说的要更复杂。比如需要考虑节假日、成员休假、多个需求并行开发、需求存在外部依赖项等。以后有机会再展开来讲。

How:如何完成?

完成需求容易,如果要高质量完成,那就需要费点功夫了。同样的,只挑一些重要的来讲

  • 明确需求、关键时间点

  • 严控开发、自测、提测质量

  • 及时暴露风险

  • 推动解决问题

  • 关注线上质量

明确需求/关键时间点

这块的重要性,再怎么强调也不为过。前面已经讲过了,这里不再赘述。

严控开发、自测、提测质量

作为一名合格的前端工程师,对自己的开发质量负责,这是最基本的要求。

要时常问自己:

  • 开发:是否严格按照需求文档完成功能的开发。

  • 联调:在与后台同学联调前,是否已经对照测试用例,对自己的模块进行了严格的自测。

  • 提测:提测前,是否已自测、联调通过;测试正式介入前,产品是否提前部署到测试环境,并进行初步的验证。

严格把控开发、自测、提测质量,这不但是能力,更是一种负责任的态度。如果能做到这点,不单节省大家的时间,还可以让其他人觉得自己比较“靠谱”。

备注:以下截图,是笔者之前一个需求的自测用例(非完整版)。同样是评论功能,自测用例将近50个。

clipboard.png

及时暴露风险

风险意识非常重要。在需求完成的过程中,经常会有各种意外的小插曲出现。对于前端同学,常见的有:

  • 视觉稿/交互稿未按时提供。

  • 需求变更。

  • 工作量评估不足。

  • 后台接口未按时、按质完成。

  • bug有好多,但修改不及时。

上面列举的项,都可能导致需求发布delay,要时刻要保持警惕。一旦出现可能可能导致delay的风险,要及时做好同步,准备好应对措施。

打个比方:

前面说到,小A 评估了3天的开发工作量。等到开发的第2天,发现之前工作量评估少了,至少需要4天才能完成。

这个时候,该怎么办呢?

相信不少同学都是这样处理的:咬咬牙,加加班,4天的活3天干,实在完不成了再说。

这样处理潜在的问题不小:

  1. 给自己增加了过重的负担。

  2. 没能让问题及早的暴露解决。

  3. 可能打乱项目的整体节奏。

更好的处理方式是:及时跟项目组成员同步风险,并落实确认相应解决方案。比如适当调整排期、砍掉部分优先级不高的功能等。

推动解决问题

对于一个职场人能力的评判,“解决问题”的能力,是很重要的一个评估标准。解决问题的能力如何体现呢?

举个例子,提测过程中,出现了不少bug,对于小A来说,该怎么办呢?这里分两种情况:

  • bug主要是小A的。

  • bug主要是小B的。

第一种情况很简单,自己的坑自己填,抓紧时间改bug,并做好事总结,降低后续需求的bug率。

第二种情况呢?如果小B比较配合,主动快速修复bug,那没什么好说的。但万一不是呢?

遇到这种情况,小A可能会想:“又不是我的bug,干嘛操那份闲心,需求如果delay的话,那也是小B的问题,跟我无关。”

可能不少同学的想法跟小A一样,这在笔者看来,略显消极,处理方式显得不够“职业化”。

为什么呢?

同在一个项目组,得要有团队意识、整体意识。需求延期,首先是所有需求相关人的责任,是要一起打板子的。然后,才会对具体的责任人进行问责。

回到前面的场景,小A更好的处理方式是:做好沟通工作,主动推进问题解决。

  1. 了解小B没有及时改bug的原因:有可能太忙、bug不好改、没有意识到那是自己的bug。

  2. 如可能,提供必要帮助:比如跟项目经理申请,这段时间小B集中精力改bug,暂不开发新需求

  3. 风险同步:如果小B真的不称职,尽快知会项目负责人,对小B进行批评教育,实在不行就换人。

关注线上质量

这一点非常重要,但又是容易被忽略的一点。需求发布上线,是个重要的里程碑,但并不意味着需求的终点,还得时刻关注以下事项:

  • 功能是否正常运行?

  • 各项指标是否正常?比如产品上报数据、性能监控数据、错误监控数据等。

  • 有哪些可以优化的点?优先级多高?

  • 。。。

只管功能开发,一旦需求上线,立刻做甩手掌柜,同样是缺乏责任意识的表现。试想一下,如果你是团队的老大,你会放心把重要的需求交给一个“甩手掌柜”吗。

写在后面

本文中,笔者主要从一个前端工程师的角度出发,谈了一些“高质量完成需求”的经验。里面提到的不少内容,放到其他岗位也是适用的。鉴于篇幅原因,很多细节都是点到为止,并没有深入展开。

方法论再多,最终还是需要人去落实。作为一名前端工程师,加强责任意识,主动承担,勤于总结,做社会主义合格的接班人。

查看原文

赞 16 收藏 65 评论 15

找大海的nimo 赞了文章 · 2016-04-09

JavaScript 实现 extend

最近在写移动端的项目,目前还没有引用任何库,所以很多的方法都要自己写。

用惯了jQuery,当函数参数是对象的时候,定义默认参数的时候会写一个defaultOptions对象,然后通过jQuery.extend将实参扩展到defaultOptions对象上。JavaScript是没有extend这个原生方法的,今日得闲,就自己实现一个吧。

先想想这个函数需要做什么。jQuery extend文档中描述的jQuery.extend原型是
jQuery.extend( target [, object1 ] [, objectN ] )

它的含义是将object1、object2...合并到target中,并返回合并过的target。这里面有两个点需要注意一下。

  • 合并是从后到前的;
  • target的值是被修改了的;
  • 是深拷贝的(若是参数对象中有属性是对象,也是会递归合并的);

其实若不想原始数据被更改也很简单,只要第一个参数传空对象就好了。

那,直接上代吗吧:

void function(global){
    var extend,
        _extend,
        _isObject;

    _isObject = function(o){
        return Object.prototype.toString.call(o) === '[object Object]';
    }

    _extend = function self(destination, source){
        for (var property in source) {
            if (source.hasOwnProperty(property)) {

                // 若sourc[property]是对象,则递归
                if (_isObject(source[property])) {

                    // 若destination没有property,赋值空对象
                    if (!destination.hasOwnProperty(property)) {
                        destination[property] = {};
                    };

                    // 对destination[property]不是对象,赋值空对象
                    if (!_isObject(destination[property])) {
                        destination[property] = {};
                    };

                    // 递归
                    self(destination[property], source[property]);
                } else {
                    destination[property] = source[property];
                };
            }
        }
    }

    extend = function(){
        var arr = arguments,
            result = {},
            i;

        if (!arr.length) return {};

        for (i = 0; i < arr.length; i++) {
            if (_isObject(arr[i])) {
                _extend(result, arr[i])
            };
        }

        arr[0] = result;
        return result;
    }

    global.extend = extend;
}(window)

这样似乎可以了。但是貌似有一个小问题,我们这里是按照参数顺序从左到右依次执行的,但是其实若是最后一个参数有的属性,前面的参数上的该属性都不需要再扩展了。其实前面的所有参数都是将自己身上有的属性而最后一个参数没有的属性补到最后一个参数上。既如此,是不是从参数列表的右侧开始扩展更好一些。

修改以后的代码:

void function(global){
    var extend,
        _extend,
        _isObject;

    _isObject = function(o){
        return Object.prototype.toString.call(o) === '[object Object]';
    }

    _extend = function self(destination, source) {
        var property;
        for (property in destination) {
            if (destination.hasOwnProperty(property)) {

                // 若destination[property]和sourc[property]都是对象,则递归
                if (_isObject(destination[property]) && _isObject(source[property])) {
                    self(destination[property], source[property]);
                };

                // 若sourc[property]已存在,则跳过
                if (source.hasOwnProperty(property)) {
                    continue;
                } else {
                    source[property] = destination[property];
                }
            }
        }
    }

    extend = function(){
        var arr = arguments,
            result = {},
            i;

        if (!arr.length) return {};

        for (i = arr.length - 1; i >= 0; i--) {
            if (_isObject(arr[i])) {
                _extend(arr[i], result);
            };
        }

        arr[0] = result;
        return result;
    }

    global.extend = extend;
}(window)

写代码总是那么有意思!这里面可以看到,只要result身上有的属性,都不需要再赋值了,嘿嘿。

当然,目前水平有限,这段代码肯定也还有着优化空间,若看官有任何建议,还请不吝赐教。

查看原文

赞 7 收藏 21 评论 6

找大海的nimo 关注了问题 · 2016-03-08

你们都在研究什么?(前端方向)

说说自己都在研究什么技术,有什么可以分享给我或者大家的? 在工作中都在做什么?

关注 8 回答 14

找大海的nimo 赞了文章 · 2015-11-19

GET请求和POST请求的区别

GET请求和POST请求的区别

经常遇到「既然GET请求可以做POST请求的事情,为什么还要区分GET和POST而不只使用一个请求?」的问题。作为在实际中被使用最广的两个请求方法,这个问题其实挺难回答的,但万物总有其根由,今天就追根究底。

查看RFC规范再加上之前查过的一些二手文章,整理了如下的观点:

  1. GET 被强制服务器支持

  2. 浏览器对URL的长度有限制,所以GET请求不能代替POST请求发送大量数据

  3. GET请求发送数据更小

  4. GET请求是安全的

  5. GET请求是幂等的

  6. POST请求不能被缓存

  7. POST请求相对GET请求是「安全」的

GET被强制服务器支持

All general-purpose servers MUST support the methods GET and HEAD. All other methods are OPTIONAL.

GET 通常用于请求服务器发送某个资源。在HTTP/1.1中,要求服务器实现此方法;POST请求方法起初是用来向服务器输入数据的。在HTTP/1.1中,POST方法是可选被实现的,没有明确规定要求服务器实现。

浏览器对URL的长度有限制,所以GET请求不能代替POST请求发送大量数据

RFC 2616 (Hypertext Transfer Protocol — HTTP/1.1) states in section 3.2.1 that there is no limit to the length of an URI (URI is the official term for what most people call a URL)

RFC 2616 中明确对 uri 的长度并没有限制。不过虽然在RFC中并没有对uri的长度进行限制,但是各大浏览器厂家在实现的时候限制了URL的长度,可查到的是IE对长度限制为2083;而chrome遇到长度很长的URL时,会直接崩溃

所以这条结论算是正确的。

GET请求发送数据更小

只能通过写代码验证了:下面第一个文件是服务器代码,作用是在客户端发送GET和POST请求的时候返回200状态码。第二个文件是客户端HTML文件,点击两个button,分别发送GET请求和POST请求。

import koa from 'koa'
import fs from 'mz/fs'


const app = koa()

app.use(function* (next) {
  if(this.path == '/test')
    return this.status = 200

  yield next
})

app.use(function* (next) {
  this.type = 'html'
  this.body = yield fs.readFile('./index.html')
  yield next
})

app.listen(8080)
console.log('koa server port: 8080')
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<button id="get">GET</button>
<button id="post">POST</button>
</body>
<script>
  function http(type) {
    return function (url) {
      var req = new XMLHttpRequest()
      req.open(type, url)

      req.send()
    }
  }

  var getDom = document.getElementById('get')
    , postDom = document.getElementById('post')
    , get = http('GET')
    , post = http('POST')


  getDom.addEventListener('click', function () {
    get('/test')
  })

  postDom.addEventListener('click', function () {
    post('/test')
  })


</script>
</html>

get-headers
post-headers

从上两张图可以看到POST请求的headers要比GET请求多了两个属性。所以这条结论其实也算是对的,不过从请求发送时间来看的话,其实两者并没有差别。

get-send-time.png
post-send-time

GET请求是安全的

Of the request methods defined by this specification, the GET, HEAD,OPTIONS, and TRACE methods are defined to be safe.

这里的安全指的是在规范的定义下,Get操作不会修改服务器的数据

GET请求是幂等的

A request method is considered "idempotent" if the intended effect on
the server of multiple identical requests with that method is the
same as the effect for a single such request. Of the request methods
defined by this specification, PUT, DELETE, and safe request methods
are idempotent.

从上面可以看到GET请求是安全的,在幂等性中说PUT和DELETE以及安全method都是幂等的,所以GET自然也被包括了。

POST请求不能被缓存

我们在实际使用过程中对HTTP请求的优化大多数都放在GET请求上,比如对没有数据变化的请求(网站中常用的静态文件)做缓存,在潜意识中认为只有GET请求才可以被缓存,所以从来也不会考虑POST请求的缓存优化,然而在RFC中提到GET和POST以及HEAD都是可以被缓存的。不过不要惊讶,之前不知道POST可以被缓存是因为标准也很无奈,浏览器的实现总是比标准厉害。

In general, safe methods thatdo not depend on a current or authoritative response are defined as cacheable; this specification defines GET, HEAD, and POST as cacheable, although the overwhelming majority of cache implementations only support GET and HEAD.

POST请求相对GET请求是「安全」的

这一点很多人都会质疑,被抓包之后的POST请求和GET请求是一样裸露的,所以更安全的说法是不对的。我这里所有的「安全」是相对的,因为GET请求有时候会直接反应在浏览器的地址栏,而现在的浏览器大多会记住曾经输入过的URL。试想如果你曾经在别人电脑上填过一个很私密的表单,那么你的这份记录很可能被连没什么电脑常识的人都一览无遗。

查看原文

赞 17 收藏 111 评论 5

找大海的nimo 关注了问题 · 2015-11-19

解决行内盒的 offsetHeight 大于其 font-size

由于字体的不同,可能造成行内盒的 offsetHeight 大于其 font-size

例:http://codepen.io/HaoyCn/pen/WQOXgj

请问以下各值如何计算:

  1. 该行内盒的内容区域高度

  2. 浏览器实际渲染出的字号

  3. 行距 L = line-height - AD 中 A 和 D 取值

第一个问题是为了区别行内盒的内容区域高度和盒高度。按 W3C 在计算行盒时候的说法,该行内盒高度应为 line-height,但比如在上面链接案例中,line-height 等于 font-size 即 32px。而 W3C 未定义行内盒内容区域高度,所以我猜想这个 offsetHeight 是否就是各用户代理采取的内容区域高度。

第二三个问题关乎块容器高度之计算。如下 HTML:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Line Height</title>
<style>
body {
    margin: 0;
    font: 32px/1 'Microsoft YaHei';
}
div {
    width: 400px;
    margin: 30px auto;
    outline: 1px solid black;
    background: #008E59;
}
img {
    height: 80px;
    margin-top: 10px;
}
</style>
</head>
<body>
    <div>
        <span>Some Text</span>
        <img data-original="picture.jpg" alt=""/>
    </div>
</body>
</html>

SPAN 盒基线 和 IMG 盒下外边距边缘是基于 DIV 盒基线对齐,DIV 盒总外高 93px,IMG 总外高 90px。多出的 3px 我认为应为 L/2 + D

根据 W3C,AD 是同字体的特性深度和高度紧密相关的,现查得:

Microsoft YaHei,Ascent 2167,Descent 536,Height 2703。

数据来源:
http://fontsgeek.com/ 查询结果
http://www.zhihu.com/question/20137844 厉向晨的回答

用比例换算 A 和 D 的时候,采用的是 offsetHeight 还是 font-size 呢?

我试着计算了,似乎取 offsetHeight 更接近。但换字体换字号后误差还会扩大(可能是我查阅到的数据有误>_<)。

谢谢您的关注和回答!

关注 3 回答 2

找大海的nimo 关注了问题 · 2015-11-11

123456

22aaaaaaaa

关注 14 回答 6

找大海的nimo 关注了问题 · 2015-11-11

123456

22aaaaaaaa

关注 14 回答 6

认证与成就

  • 获得 1 次点赞
  • 获得 5 枚徽章 获得 0 枚金徽章, 获得 2 枚银徽章, 获得 3 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-10-31
个人主页被 216 人浏览