赖小赖小赖

赖小赖小赖 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑

笨鸟。

个人动态

赖小赖小赖 发布了文章 · 2017-10-19

Puppeteer 初探

我们日常使用浏览器或者说是有头浏览器时的步骤为:启动浏览器、打开一个网页、进行交互。

无头浏览器指的是我们使用脚本来执行以上过程的浏览器,能模拟真实的浏览器使用场景。

有了无头浏览器,我们就能做包括但不限于以下事情:

  • 对网页进行截图保存为图片或 pdf
  • 抓取单页应用(SPA)执行并渲染(解决传统 HTTP 爬虫抓取单页应用难以处理异步请求的问题)
  • 做表单的自动提交、UI的自动化测试、模拟键盘输入等
  • 用浏览器自带的一些调试工具和性能分析工具帮助我们分析问题
  • 在最新的无头浏览器环境里做测试、使用最新浏览器特性
  • 写爬虫做你想做的事情(奸笑

无头浏览器很多,包括但不限于:

  • PhantomJS, 基于 Webkit
  • SlimerJS, 基于 Gecko
  • HtmlUnit, 基于 Rhnio
  • TrifleJS, 基于 Trident
  • Splash, 基于 Webkit

这里主要介绍 Google 提供的无头浏览器(headless Chrome), 他基于 Chrome DevTools protocol 提供了不少高度封装的接口方便我们控制浏览器。

简单的代码示例

为了能使用 async/await 等新特性,需要使用 v7.6.0 或更高版本的 Node.

启动/关闭浏览器、打开页面

    // 启动浏览器
    const browser = await puppeteer.launch({
        // 关闭无头模式,方便我们看到这个无头浏览器执行的过程
        // headless: false,
        timeout: 30000, // 默认超时为30秒,设置为0则表示不设置超时
    });

    // 打开空白页面
    const page = await browser.newPage();

    // 进行交互
    // ...

    // 关闭浏览器
    // await browser.close();

设置页面视窗大小

    // 设置浏览器视窗
    page.setViewport({
        width: 1376,
        height: 768,
    });

输入网址

    // 地址栏输入网页地址
    await page.goto('https://google.com/', {
        // 配置项
        // waitUntil: 'networkidle', // 等待网络状态为空闲的时候才继续执行
    });

保存网页为图片

打开一个网页,然后截图保存到本地:

await page.screenshot({
    path: 'path/to/saved.png',
});

完整示例代码

保存网页为 pdf

打开一个网页,然后保存 pdf 到本地:

await page.pdf({
     path: 'path/to/saved.pdf',
    format: 'A4', // 保存尺寸
});

完整示例代码

执行脚本

要获取打开的网页中的宿主环境,我们可以使用 Page.evaluate 方法:

// 获取视窗信息
const dimensions = await page.evaluate(() => {
    return {
        width: document.documentElement.clientWidth,
        height: document.documentElement.clientHeight,
        deviceScaleFactor: window.devicePixelRatio
    };
});
console.log('视窗信息:', dimensions);

// 获取 html
// 获取上下文句柄
const htmlHandle = await page.$('html');

// 执行计算
const html = await page.evaluate(body => body.outerHTML, htmlHandle);

// 销毁句柄
await htmlHandle.dispose();

console.log('html:', html);

Page.$ 可以理解为我们常用的 document.querySelector, 而 Page.$$ 则对应 document.querySelectorAll

完整示例代码

自动提交表单

打开谷歌首页,输入关键字,回车进行搜索:

// 地址栏输入网页地址
await page.goto('https://google.com/', {
    waitUntil: 'networkidle', // 等待网络状态为空闲的时候才继续执行
});

// 聚焦搜索框
// await page.click('#lst-ib');
await page.focus('#lst-ib');

// 输入搜索关键字
await page.type('辣子鸡', {
   delay: 1000, // 控制 keypress 也就是每个字母输入的间隔
});

// 回车
await page.press('Enter');

完整示例代码

复杂点的代码示例

每一个简单的动作连接起来,就是一连串复杂的交互,接下来我们看两个更具体的示例。

抓取单页应用: 模拟饿了么外卖下单

传统的爬虫是基于 HTTP 协议,模拟 UserAgent 发送 http 请求,获取到 html 内容后使用正则解析出需要抓取的内容,这种方式面对服务端渲染直出 html 的网页时非常便捷。

但遇到单页应用(SPA)时,或遇到登录校验时,这种爬虫就显得比较无力。

而使用无头浏览器,抓取网页时完全使用了人机交互时的操作,所以页面的初始化完全能使用宿主浏览器环境渲染完备,不再需要关心这个单页应用在前端初始化时需要涉及哪些 HTTP 请求。

无头浏览器提供的各种点击、输入等指令,完全模拟人的点击、输入等指令,也就再也不用担心正则写不出来了啊哈哈哈

当然,有些场景下,使用传统的 HTTP 爬虫(写正则匹配) 还是比较高效的。

在这里就不再详细对比这些差异了,以下这个例子仅作为展示模拟一个完整的人机交互:使用移动版饿了么点外卖。

先看下效果:

代码比较长就不全贴了,关键是几行:

const puppeteer = require('puppeteer');
const devices = require('puppeteer/DeviceDescriptors');
const iPhone6 = devices['iPhone 6'];

console.log('启动浏览器');
const browser = await puppeteer.launch();

console.log('打开页面');
const page = await browser.newPage();

// 模拟移动端设备
await page.emulate(iPhone6);

console.log('地址栏输入网页地址');
await page.goto(url);

console.log('等待页面准备好');
await page.waitForSelector('.search-wrapper .search');

console.log('点击搜索框');
await page.tap('.search-wrapper .search');

await page.type('麦当劳', {
    delay: 200, // 每个字母之间输入的间隔
});

console.log('回车开始搜索');
await page.tap('button');

console.log('等待搜素结果渲染出来');
await page.waitForSelector('[class^="index-container"]');

console.log('找到搜索到的第一家外卖店!');
await page.tap('[class^="index-container"]');


console.log('等待菜单渲染出来');
await page.waitForSelector('[class^="fooddetails-food-panel"]');


console.log('直接选一个菜品吧');
await page.tap('[class^="fooddetails-cart-button"]');

// console.log('===为了看清楚,傲娇地等两秒===');
await page.waitFor(2000);
await page.tap('[class^=submit-btn-submitbutton]');

// 关闭浏览器
await browser.close();

关键步骤是:

  • 加载页面
  • 等待需要点击的 DOM 渲染出来后点击
  • 继续等待下一步需要点击的 DOM 渲染出来再点击

关键的几个指令:

  • page.tap(或 page.click) 为点击
  • page.waitForSelector 意思是等待指定元素出现在网页中,如果已经出现了,则立即继续执行下去, 后面跟的参数为 selector 选择器,与我们常用的 document.querySelector 接收的参数一致
  • page.waitFor 后面可以传入 selector 选择器、function 函数或 timeout 毫秒时间,如 page.waitFor(2000) 指等待2秒再继续执行,例子中用这个函数暂停操作主要是为了演示

以上几个指令都可接受一个 selector 选择器作为参数,这里额外介绍几个方法:

  • page.$(selector) 与我们常用的 document.querySelector(selector) 一致,返回的是一个 ElementHandle 元素句柄
  • page.$$(selector) 与我们常用的 document.querySelectorAll(selector) 一致,返回的是一个数组

在有头浏览器上下文中,我们选择一个元素的方法是:

const body = document.querySelector('body');
const bodyInnerHTML = body.innerHTML;
console.log('bodyInnerHTML: ', bodyInnerHTML);

而在无头浏览器里,我们首先需要获取一个句柄,通过句柄获取到环境中的信息后,销毁这个句柄。

// 获取 html
// 获取上下文句柄
const bodyHandle = await page.$('body');
// 执行计算
const bodyInnerHTML = await page.evaluate(dom => dom.innerHTML, bodyHandle);
// 销毁句柄
await bodyHandle.dispose();
console.log('bodyInnerHTML:', bodyInnerHTML);

除此之外,还可以使用 page.$eval:

const bodyInnerHTML = await page.$eval('body', dom => dom.innerHTML);
console.log('bodyInnerHTML: ', bodyInnerHTML);

page.evaluate 意为在浏览器环境执行脚本,可传入第二个参数作为句柄,而 page.$eval 则针对选中的一个 DOM 元素执行操作。

完整示例代码

导出批量网页:下载图灵图书

我在 图灵社区 上买了不少电子书,以前支持推送到 mobi 格式到 kindle 或推送 pdf 格式到邮箱进行阅读,不过经常会关闭这些推送渠道,只能留在网页上看书。

对我来说不是很方便,而这些书籍的在线阅读效果是服务器渲染出来的(带了大量标签,无法简单抽取出好的排版),最好的方式当然是直接在线阅读并保存为 pdf 或图片了。

借助浏览器的无头模式,我写了个简单的下载已购买书籍为 pdf 到本地的脚本,支持批量下载已购买的书籍。

使用方法,传入帐号密码和保存路径,如:

$ node ./demo/download-ituring-books.js '用户名' '密码' './books'

注意:puppeteerPage.pdf() 目前仅支持在无头模式中使用,所以要想看有头状态的抓取过程的话,执行到 Page.pdf() 这步会先报错:

所以启动这个脚本时,需要保持无头模式:

const browser = await puppeteer.launch({
    // 关闭无头模式,方便我们看到这个无头浏览器执行的过程
    // 注意若调用了 Page.pdf 即保存为 pdf,则需要保持为无头模式
    // headless: false,
});

看下执行效果:

我的书架里有20多本书,下载完后是这样子:

完整示例代码

无头浏览器还能做什么?

无头浏览器说白了就是能模拟人工在有头浏览器中的各种操作,那自然很多人力活,都能使用无头浏览器来做(比如上面这个下载 pdf 的过程,其实是人力打开每一个文章页面,然后按 ctrl+pcommand+p 保存到本地的自动化过程)。

那既然用自动化工具能解决的事情,就不应该浪费重复的人力劳动了,所以我们还可以做:

  • 自动化工具
    如自动提交表单,自动下载
  • 自动化 UI 测试
    如记录下正确 DOM 结构或截图,然后自动执行指定操作后,检查 DOM 结构或截图是否匹配(UI 断言)
  • 定时监控工具
    如定时截图发周报,或定时巡查重要业务路径下的页面是否处于可用状态,配合邮件告警
  • 爬虫
    如传统 HTTP 爬虫怕不到的地方,就可配合无头浏览器渲染能力来做
  • etc

感谢阅读!
(全文完)

查看原文

赞 15 收藏 24 评论 3

赖小赖小赖 发布了文章 · 2017-10-14

重新理解 z-index

z-index 在我们日常编写 CSS 样式的时候很常见,自己就写过或看过别人写过诸如 z-index: 9999; 这种属性。可为什么有时候设置了 z-index 死活不生效,为什么有时候 z-index 表现总是奇奇怪怪的呢?

这篇文章结合一些简单的 Demo 和大家一起温习下 z-index 吧。

快速理解 z-index (层叠顺序)

z-index 决定了具备 position 为非 static 属性的元素(有定位的元素)及其后代元素的层叠顺序。

当元素的 position 为 static (默认值) 时,排在后面的元素,叠在最上层;

7860c758-9815-47c8-85ef-c7157109bf05.png
Demo 示例

元素的 position 为 static 时,而其他兄弟元素为非 static 时,则该元素处于最底层。

48eb1e47-59d5-4647-94c7-230178fe5d20.png
Demo 示例

当元素的 position 为非 static(默认值) 时,z-index 层叠规则生效,值越大则在越上层。

1ceed4e1-1740-4c61-a7e1-e344e069d816.png
Demo 示例

z-index(层叠顺序)和 float(浮动)的关系

浮动元素比较特殊,它处于最底层和层叠层之间。

7a6c9051-e46d-4717-a247-6fcdcf96f917.png
Demo 示例

深入理解 stacking context (层叠上下文)

什么是层叠上下文

在一个层叠上下文内,我们按照以下顺序堆叠元素:

  • 建立层叠上下文的父元素处于最底层
  • 元素定位 position 都是 static 时,排列在后的元素堆叠在上面
  • 元素定位 position 为非 static 时,若 z-index 为 auto 或数值相等,则排列在后的元素堆叠在上面
  • 元素定位 position 为非 static 时,若 z-index 为数值且不等,则数值大的元素堆叠在上面
  • 浮动元素处于position 为 static 和非 static 的元素之间

何时产生层叠上下文

以下场景发生时,会创建层叠上下文:

  • HTML 根元素
  • 元素定位 position 为 absolute 或 relative,且 z-index 不是 auto
  • 元素定位 position 为 fixed 或 sticky
  • 元素是 flexbox 的子元素,且 z-index 不是 auto
  • 元素的透明度 opacity 小于1
  • 元素的 mix-blend-mode 值不是 normal
  • 元素的以下属性的值不是 none:

    • transform
    • filter
    • perspective
    • clip-path
    • mask / mask-image / mask-border
  • 元素的 isolution 属性值为 isolate
  • 元素的 -webkit-overflow-scrolling 属性为 touch
  • 元素的 will-change 属性具备会创建层叠上下文的值

当元素的 z-index 为 auto(默认值) 时,不建立新的局部层叠上下文,且层叠级别继承自父元素的层叠级别。

这里我们暂且只关注和 z-index 有关的层叠上下文,即元素定位 position 为 absolute 或 relative,且 z-index 不是 auto 的层叠上下文。

层叠上下文可嵌套但互相独立

当元素的 position 为非 static 且 z-index 为数值时,会建立新的局部层叠上下文,此时这个上下文内的元素之间,可根据 z-index 的值互相对比,但这个上下文里的后代元素的 z-index 不和这个上下文外部的元素的 z-index 进行对比,也就是所有的层叠上下文都是独立的

元素的 z-index 值,都是相对于当前上下文的比较,脱离了上下文,z-index 不再有对比意义。

b6b96136-4278-41d5-bffb-4a4d111dfdd4.png
Demo 示例

建立层叠上下文

先来看个简单的结构,

<div id="box-container">
  box-container relative
    <div id="box-red">
    box-red relative
      <div id="box-red-child">box-red-child absolute</div>   
    </div>
    <div id="box-green">
    box-green relative
      <div id="box-green-child">box-green-child absolute</div>
    </div> 
</div>

关键样式是:

#box-container {
    position: relative;      
}
#box-red {
    position: relative;     
} 
#box-green {
    position: relative;
}
#box-red-child {
    position: absolute; top: 30px; left: 100px;  
}
#box-green-child {   
    position: absolute; top: 40px; left: 10px;    
}

在这个结构下,顶级容器 #box-container 和两个子容器定位 position 为 relative,但没有设置 z-index,所以未形成新的层叠上下文,#box-red-child 继承 #red-box 的 z-index,同理 #box-green-child 继承 #green-box 的 z-index, 而按照已有的层叠上下文的规则,#box-green 层叠在 #box-red 上层,所以出现了,#box-green-child 层叠在 #box-red-child 上层。

3f54f086-bbbf-4528-a9d1-08ec4759e582.png
Demo 示例

那我们想让 #box-red-child 出现在 #box-green-child 上层的话,则只需要在他们之间,创建一个新的层叠上下文: 正确设置 position 和 z-index。
修改一下关键样式:

#box-container {
    position: relative;  
    z-index: 1; /* 创建新的层叠上下文 */
}
#box-red {
    position: relative;     
} 
#box-green {
    position: relative;
}
#box-red-child {
    position: absolute; top: 30px; left: 100px;  
    z-index: 2; /* 参与层叠上下文的计算 */
}
#box-green-child {   
    position: absolute; top: 40px; left: 10px;    
    z-index: 1; /* 参与层叠上下文的计算 */
}

ef4dce56-a8d2-4981-a563-0bdc6d933625.png
Demo 示例

总结

总结 z-index 关键的几点:

  • 理解并找到元素所在的层叠上下文
  • 层叠上下文中的元素,z-index 可互相对比
  • 层叠上下文是独立的,且可嵌套的
  • 想知道 z-index 的影响效果,关键是找到元素最近的层叠上下文,再对比 z-index 大小

层叠上下文,有没有一种函数作用域的感觉?

查看原文

赞 0 收藏 3 评论 1

赖小赖小赖 发布了文章 · 2017-10-14

获取网页指定元素的原生方法回顾

那是个夜黑风高的夜晚,我遇到了一个按钮:

<button type="submit">搜索</button>

嗯,我要选中它,我敲下了一行代码:

const submitButton = document.querySelector('button[type="submit"]');

这对于精通 document.querySelector 的函数名书写方式的我来说,简直就像吃下四两饭一样简单!

但是。

我们知道,document.querySelector 接收一个选择器字符串,返回第一个匹配的 DOM 元素,所以如果页面上只有一个 button[type="submit"] 或者这个 button[type="submit"] 在 html 中是第一个时,我这个方法是无懈可击的。

然后,我发现页面上竟然存在两个 button[type="submit"] 类型的按钮(黑人问号???)。

我对比了一下差异:

<button type="submit">提交</button>
<button type="submit">搜索</button>

先不八卦为什么页面上有两个差不多的按钮,但能初步判定的是,他们长得很像,嗯。

那么,问题来了,我怎么选中那个搜索框,我把问题抛给了自以为是的自己,得到了几个回答。

分身 A:改用 selectorSelectorAll 拿到第二个不就行了嘛!

const allSubmitButton = document.querySelectorAll('button[type="submit"]');
const submitButton = allSubmitButton[1];

这方法貌似不错,但万一这两个按钮对调位置了怎么办?

分身 B: 那选择器写全一点,也匹配『搜索』这两个字不就可以把『提交』那个按钮给拍除掉了嘛!

const submitButton = document.querySelector('button[type="submit"][innerText="搜索"]');

打印出来一看,嗯,怎么输出是 null???

分身 B: 那个,那个。。。innerText 是我编出来的,我不知道正确的要怎么写。

嗯,虽然 B 写错了语法(哪个程序员会不写错语法?),但限定了两个条件,type="submit"innerText="搜索", 能唯一匹配搜索按钮了,这个思路是对的。

那么,正确的语法是什么呢?

苦苦搜索半天,无果。

分身 C: 那就是写不出这样的选择器吧,还是用 JS 来做吧 :)

分身 A: 这么简单的需求,CSS 是万能的,为什么要用 Javascript?

分身 B: 对,Hail CSS!

。。。那我们就借此机会温习一下找到一个 DOM 元素有哪些方法吧。

浏览器原生提供的几个找到 DOM 元素的方法

document.getElementById

Id 为网页全局唯一。

document.getElementById('id');

注意,与其他方法不一样,getElementById 只能被全局对象 document 调用,否则会报错:

document.getElementById('parentId').getElementById('childId');
// Uncaught TypeError: document.getElementById(...).getElementById is not a function

因为 Id 是全局唯一的,所以上面第二种写法自然也显得没有必要。

Id 对大小写敏感(除非文档被声明为特殊类型如: XHTML, XUL),所以以下两种写法并不等同:

document.getElementById('id');
documen.getElementById('ID');

element.getElementsByClassName

这里的 element 指代网页中有效的 DOM 元素,包含 document

返回一个 HTMLCollection

// 匹配 class 包含 'submit' 的元素
document.getElementsByClassName('submit');

// 匹配 class 包含 'submit' 和 'button' 的元素
document.getElementsByClassName('submit button');

// 级联
cons selectedElement = document.getElementById('app-container');
selectedElement.getElementsByClassName('submit button');

element.getElementsByName

用法和 getElementsByClassName 相似, 返回一个 HTMLCollection

element.getElementsByTagName

用法和 getElementsByClassName 相似, 返回一个 HTMLCollection

element.querySelector

再熟悉不过的接口了,传入 CSS 选择器,返回第一个找到的元素。

element.querySelectorAll

以上返回数组的方法,getElementsByNamegetElementsByTagNamegetElementsByTagNameNSquerySelectorAll 返回的都是 Array-like 的 HTMLCollection

要像对数组一样对 HTMLCollection 进行操作(使用数组的 forEachfilter 等方法),则需要将他们转化为数组:

const buttonCollection = document.getElementsByClassName('submit');

// const buttonArray = Array.prototype.slice.call(buttonCollection);
const buttonArray = [].slice.call(buttonCollection);

buttonArray.forEach(item => {
    // do something
});

querySelector 与 getElementsByName、getElementsByClassName 和 getElementsByTagName 的细微差异

注意,对于都是返回 HTMLCollection 的方法,与 querySelector 不同的是,getElementsByNamegetElementsByClassNamegetElementsByTagName 返回的 HTMLCollection 是一个引用类型的 HTMLCollection

也就是:

  1. querySelectorAll 获取的是当时文档中匹配选择器的 DOM 元素,不包含后面插入到文档中的元素,如果文档有更新,则需要重新获取
  2. getElementsByNamegetElementsByClassNamegetElementsByTagName 则能随时反映文档中匹配对应规则的元素,包含后面插入到文档中的元素,如果文档有更新,不需要重新获取

这个说法有点类似拷贝和引用的关系。

话不多说,我们直接写点代码做个测试:

const createElement = (func) => {
    const key = 'myCustomElement';
    let element;
    switch (func) {
        case 'getElementsByName':
            element = document.createElement('div');
            element.setAttribute('name', key);
            break;
        case 'getElementsByClassName':
            element = document.createElement('div');
            element.setAttribute('class', key);
            break;
        case 'getElementsByTagName':
            element = document.createElement(key);
            break;
        case 'querySelectorAll':
            element = document.createElement('div');
            element.className = key;
            break;
        default:
            element = document.createElement('div');
    }

    return element;
}

const getCollection = (root, func) => {
    const key = 'myCustomElement';
    let element;
    if (func === 'getElementsByName') {
        return document.getElementsByName(key);
    }
    if (func === 'querySelectorAll') {
        return root.querySelectorAll(`.${key}`);
    }
    return root[func](key);
}


const testFunc = (func) => {
    const result = [];

    // 避免 getElementsByClassName 和 querySelectorAll 在统一环境的影响,创建一个独立的容器测试
    const container = document.createElement('div');
    document.body.append(container);

    // 1. 插入一个
    container.append(createElement(func));

    // 2. 看下现在有多少个
    const collection = getCollection(container, func);
    result.push(collection.length);

    // 3. 继续插入一个
    container.append(createElement(func));

    // 4. 看下现在有多少个
    result.push(collection.length);

    return result;
};

console.log('getElementsByName', testFunc('getElementsByName')); // [1, 2]
console.log('getElementsByClassName', testFunc('getElementsByClassName')); // [1, 2]
console.log('getElementsByTagName', testFunc('getElementsByTagName')); // [1, 2]
console.log('querySelectorAll', testFunc('querySelectorAll')); // [1, 1] // 注意这个输出

// 输出的是:
/*
    getElementsByName [1, 2]
    getElementsByClassName [1, 2]
    getElementsByTagName [1, 2]
    querySelectorAll [1, 1]
*/

还有什么方法可以根据 innerText 找到指定元素

回到本文讨论的问题,貌似浏览器原生并没有提供类似 document.getElementByInnerText

找了一圈貌似没有,那只能借用 Javascript 了。

回忆一下需要找到的元素是:

<button type="submit">搜索</button>

使用原生 Javascript

可以使用 querySelectorAll

let foundElement = null;
const elementsCollection = document.querySelectorAll('button[type="submit"]');
const elementArray = [].slice.call(elementsCollection);
/*
// 使用 Array.forEach 遍历,缺点是找到后没法 break
elementArray.forEach(element => {
    if (element.innerText.trim() === '搜索') {
        foundElement = element;
    }

    // 或者使用正则
    /*
    if (/^\s{0,}搜索\s{0,}$/.test(element.innerText)) {
        foundElement = element;
    }
    */
});
*/

/*
// 或使用 for-loop 找到后提前 break
const len = inputElementArray.length;
for (let i = 0; i < len; i++) {
    if (elementArray[i].innerText.trim()) {
        foundElement = elementArray[i];
        break;
    }
}
*/

// 或使用 filter
const foundElementArray = elementArray.filter(element => element.innerText.trim() === '搜索');
if (foundElementArray.length) {
    foundElement = foundElementArray[0];
}

题外话:innerText 与 innerHTML、textContent 的差异?

简而言之:

innerText 获取元素第一层的文本内容,不包含标签;
innerHTML 如其名获取内部 HTML 片段,包含标签;
textContent 类似 innerText,不过会在去掉标签后将嵌套的纯文本内容也获取出来。

MDN textContent's difference from innerText and innerHTML

查看原文

赞 1 收藏 3 评论 0

赖小赖小赖 回答了问题 · 2016-07-02

ajax怎么做分页

将数据拼到一个数组里,比如[1, 2, 3, 4, 5, 6, 7, 8]

假定每页只有2两个数据,则第一页取1,2, 第二页取3, 4啊。

这里有4页的数据,如果加了第五页的9,10 则 push 到数组里。

即检查如果数据里有需要的数据,则直接返回显示内容,否则 ajax 拉取数据后拼到数组里。

关注 7 回答 4

赖小赖小赖 回答了问题 · 2016-07-02

解决添加<!doctype html>声明后,js获取不到css属性值

改成 get_x1.style.height = '200px';

关注 5 回答 4

赖小赖小赖 发布了文章 · 2016-07-02

警告:小心巨型数字的溢出异常

今天遇到个问题:后端设置了一个 id=32132132132132112(数字) 在 cookie 中。

我为了偷懒,用了个第三方组件去解析 cookie,然后发现这个数字被解析出来变成了 32132132132132110

百思不得其解,这么一个简单的 cookie parse 的组件怎么会发生这么奇怪的问题。

翻了下源码发现有这么一句:JSON.parse(xxx)
果不其然,JSON.parse(32132132132132112) 的输出是 32132132132132110

很明显,这应该是溢出的问题,JS 能处理的数字都是浮点数,超出范围则会忽略了。

JS中整数的数字范围是 -2^53~2^53, 超出则视为 2^53 处理,注意这个并不会报错,所以如果你在使用巨型数字前没有意识到这个问题,意味着项目上线后,你将可能丢失用户数据。

clipboard.png

所以遇到这种问题,要么限制数字输入在可控范围内,要么在不需要计算的时候,将这种数字串存为字符串,要么使用一些能处理巨型计算的库来解决问题,比如 https://github.com/jtobey/javascript-bignumhttps://www.npmjs.com/package/json-bigint

参考资料:https://www.irt.org/script/1031.htm

查看原文

赞 1 收藏 0 评论 0

赖小赖小赖 发布了文章 · 2016-04-07

我试过的一套键盘鼠标控制多个电脑的办法

电脑太多真是烦恼。

工位上有一个 iMac、一个 Macbook、两台 Windows,一共五个显示器。。。

为了装逼,我不能在操作不同电脑的时候切换多套的鼠标对吧,而且我有个入门版的红轴键盘了啪啪啪起来很爽的。

那么一套键鼠,怎么控制这么多电脑呢?介绍几个我尝试过的方案。

TeamViewer 远程桌面

第一次用上这个是在大学的时候,那时候实验室的电脑特别卡,又不想背着电脑到实验室,就用了这款远程控制电脑的 TeamViewer 软件,所幸的是它只跨平台的,网络不错的话,连上自己的机器玩远程桌面是爽爽的。

当然更多的时候它的使用场景竟然是这样的:想看一个电影,回家前提前远程点下载。。。

如果网路够快的话,不想在多套键鼠之间切换,或者这电脑压根就不在身边的话,用这款远程桌面软件是爽爽的。

不过后来有了百度云、Dropbox 这类云同步产品,也就很少使用这款软件了。

这款软件个人版是免费的啦。

PowerSync 连接线

这货是工作后公司直接送的,场景是两台 Windows, 一个是办公机器一个是开发机器,网络是不通的,分别插上 USB 后就可以使用一套键鼠,共享剪贴板、拖拽文件。
不过这东西体验我感觉略差,经常掉线。而且连着它重启电脑的时候,系统总会以为它是启动盘而报错(需要拔掉才重启)。
还一个缺点是只能连接两台 Windows(毕竟一根线只有两个端口)。

Synergy 多屏幕共享

因为现在开发都是在 Mac 系统上,但偶尔需要用到 Windows,多台机器多套键鼠的切换实在是太难受。

TeamView 适合远程桌面,PowerSync 又只能连接 Windows,所以我找到了这款神器,感觉非常好用。

下载 Synergy 后,将具备键鼠的电脑设置为 Server 服务器,同时保证其他电脑处于同一个局域网,并将其他电脑设置为 Client 客户端。注意多个电脑要都要下载最新版本的软件(版本差异可能带来问题)。

同一台电脑使用拓展屏幕还是需要屏幕拓展线的,但多个电脑之间的连接,使用这款软件设置好屏幕的相对位置,就做到了多台电脑,多个屏幕使用同一套键鼠,还共享剪贴板!

不过,这款软件是收费的。个人版一百块就能搞定,支持支付宝。

脑补[这是一张一套键鼠、四个电脑、五个屏幕的工位图]

(怎么感觉我这篇文章都是广告)

为了减轻广告嫌疑,我就不贴链接了,自己 Google 去吧(23333我真是太贱了)。

查看原文

赞 1 收藏 0 评论 0

赖小赖小赖 发布了文章 · 2016-03-19

FEDay 参会小记

clipboard.png
活动地址:http://fequan.com/2016/

注意:英文不好,小记也带有自己理解,部分内容可能包含严重的个人想法,不会按原话翻译,随机写得也乱,建议等几天找录播或 PPT 得到源文理解。

使用React、Redux和Node.js构建通用应用

作者网站是:http://www.stepanp.com/

先简单说个同构(通用)应用的概念:前端服务器端功能双向映射

过去,rails 中 路由、校验、视图都在 rails 中完成,js 只是用于做一些动画

现在,用 backbone 来构建单页不刷新页面,javascript 负责了路由、校验、视图等功能,成为了主角(目录结构仿照 rails)

介绍了 todomvc 看看同一种 todo 功能,用不同的类库来实现会是怎么写的

模板可以在 rails 中写,也可以在 javascript 中写,但很快就会失控,你如何判断这个功能模板应该在哪端写,类似功能的模板你怎么复用?

如果前后端都是 JS 的话,是不是就解决了这个问题?前后端共享路由、验证、视图逻辑

那么问题又来了,后端并没有 DOM,怎么用前端的写法来呈现 DOM 结构?

写一个虚拟 DOM !前后端代码共享,React 应运而生。

优点:

  • 共享代码(代码习惯一致,我们不再需要在前后端切换语言)

  • 提升性能(计算逻辑放在后端执行,做到直出效果)

  • 利于 SEO(后端吐出静态 HTML,必然带来了 SEO 效果)

那么,我们用什么技术栈来做这些事情呢?

  • React(视图模板共享)

  • Webpack + babel + family(构建工具)

  • ReactRouter (路由)

  • Redux(存储)

React 有了虚拟 DOM ,所以我们可以在服务器端很舒服地写 DOM 组件逻辑
接下来介绍 react 基础语法,几段代码示例,略。
需要注意的是,服务器端可以写虚拟 DOM 的逻辑,但事件绑定是需要到了浏览器端才能真正绑定的,可以理解为服务器端只为我们生成了模板,到了浏览器服端后不需要重新计算虚拟 DOM 的结构,用服务器端算好的 DOM 结构直接渲染,并直接添加事件绑定后即可。

构建工具用 webpack打包 + babel 转换 ES6/7 自然是目前最好的选择啦。

路由用 ReactRouter,声明式路由
浏览器端使用 ReactRouter 的browser history 功能,服务器端则使用 match 功能(考虑浏览器端禁用 JS 后,页面还能否渲染出来)。

存储用 redux,一个请求到来创建一个组件的同时,创建一个 store(即时创建,方便销毁避免内存泄露)

把旧数据缓存到 window.__DATA__,初始化时考虑使用缓存数据,感觉这个做法比较传统,初始化组件时在数据返回前先使用这份旧数据

拉取数据,单页应用必然是使用 ajax 异步去拉数据,这里讲师推荐了 isomorphic-fetch 模块来做 ajax 请求,讲师说这是同构应用中拉取数据的关键,我没明白什么意思,回头翻 PPT。。。

最后作者展望了下 JS 的未来,大一统不同的终端(iOS/Android等),希望能用 JS 来做不同端上视图、验证、路由等功能。呼吁我们去做这种事情,但我觉得不大可能。。。

微信Web APP开发最佳实践

介绍 JS-SDK,说的东西在微信开发文档中都有写,我就懒得打了,略。

手机机型统计图,作者贴了张微信安卓客户端机型前十的分布图,基本都是低端机,那叫一个惨,这个等 PPT 出来直接看吧。

介绍 X5,优点的为的是抹平不同 Android 版本不同 Webview 的坑,缺点是带来自己的 X5 坑(有点多)。

这里我体会比较深了,微信缓存清理比较蛋疼,客户端做的强缓存,对于普通用户来讲节省流量,对于我们开发来讲就繁琐了,微信设置中清缓存不一定有效。可以考虑 URL 中加时间戳的办法(记得 HTML 也要加,否则都不去拉 HTML 了)

黑科技://triggerWebViewCacheCleanup 来清缓存!

布局方面,flex 只是部分支持,建议用 autoprefixer 等工具来补,只支持类似 -webkit-box 这种老写法,只能怪 X5 用的 webkit 内核版本太老了。

动画效果,伪元素不能使用动画效果,这个我也深有体会,建议用实体标签来模拟。

视频播放,controlrs 是不能隐藏的(除非你有白名单),ontimeupdate 事件可以触发,但 currentTime 是不准确的,autoplay 是不能使用的,这个是 iOS 普遍的问题,不经用户交互是无法自动播放媒体的,那监听 WXJSBridgeReady 事件再播放就好了(用户有交互)。

cookie 和 localStorage 失效的问题,听到后我都惊呆了。。。原因不明。这是不可靠的存储,不要太过于依赖。建议是 cookie 和 localStorage 都存一份。

UIWebView 有个 bug: 手势右滑和点返回按钮关闭 Webview 是行为不一致的,建议用 hash 来做历史记录管理。

介绍 WeUI, 微信风格的 UI,与客户端体验一致,这个自己去微信 github 上看 demo 吧,略。

WeUI 有 jquery/react/vue 版本,这点很赞。

微信调试一件套,网页授权、JS-SDK 模拟、集成 Chrome DevTools、代理、weinre 远程调试。这些在微信开发者中心有介绍,略。

X5 升级,改用了 Blink 内核,到现在 3 月 19 号未知,灰了大约 70%,计划到月底全量。

X5-Blink 有哪些新特性呢?

  • Chrome inspect

  • 标准的缓存策略

  • 完整支持 flex

  • canvas 支持 CSS 设置背景色了

  • filter: blur 模糊可以用了

  • 优化了动画卡顿问题

  • 伪元素支持动画了

  • 尼玛,PPT 太快,我刚肚子饿了还几点没记录到,别打我

升级后,很多坑自动没了,但我觉得肯定也会带来更多坑。XXX 年微信开发经验的人,终于又成为了零年开发经验的人,重新走上了踩坑之路。

React tips

Container Component

父子组件之间的数据传递、渲染、错误捕获,接入外部的 Store

先来看看 UI Component 需要什么

  • 有数据/状态(data/state)

  • UI 逻辑

  • 可重用

  • 需要测试

React 组件在 ComponentDidMount 时发起 ajax 数据,设置 setState 后,更新组件数据,重新渲染。

错误捕获的做法是,将 error 传进 state 中 this.setState({error: null}); 判断有 error 显示 Error 组件
处理 loading 的做法也是,将 loading 传进去 this.setState({isLoaded: false}); 判断有 isLoaded 显示菊花

对于 UI Component 来说,它不在需要关心数据哪里来的,只关心将 Container Component 传递下来的数据,进行渲染(无状态)。

我对讲师将的 Container Component 的理解是:Container Component 专门负责管理数据,然后将数据传递给子组件,也就是 UI Component(单向数据流)

多个组件需要复用统一份数据,所以我们需要有一个地方来存储通用数据,这就是 Store.

通过拿到一个 action ,改变了一个 store, 再将数据传递回去,这是一个简单的函数行为,而 flux 的实现就过于冗余,所以有了 redux。

Flux ReduceStore

其实就是监听到一个 action 将传过来的 state 进行合并,然后把新的 state 传递回去。

Functional *

强调无状态的组件(stateless component),一个组件就是一个函数,接受输入,将渲染结果输出。
如果一个父组件里面要产生很多子组件,那你就该将这个组件抽离出来,变成一个函数,给父组件使用。
这里讲师拿 underscore 的 _render 做反例,问题在于 render 嵌套太深,不好调试。
而无状态组件,只关注输入,保证输出。

所以建议是,组件嵌套不要太深,保证同时只有一个组件,接收一个输入,具有一份输出。(好绕)

这里讲的比较抽象,需要看了 PPT demo 代码才好理解,略(别打我)。

Decorator/HOC 高内聚组件

使用装饰器,不改变原有组件的前提下,加一下修饰包装后,返回一个新的组件(被赋予了额外的组件或行为)
好处就是,UI 层可以专心做渲染的事情,通过添加额外的装饰器,来产生不同功能的定制化组件,那么这个 UI 层组件,就做到了可重用。

map/filter/reduce

很多数组操作用 forEach 都可以完成,下面三个也都是数组具备的的方法,只是一些小技巧而已。
用 map 替代 forEach, 简化数据
用 filter 替代 forEach, 筛选数据
用 reduce 快速产生一个新对象

todos.reduce((todos, todo) => {
     todos[todo.id] = todo;
     return todos;
}, {})

这几点技巧,加上 ES6 的箭头函数来用,很多时候一行代码就能处理完,能让我们的代码更加简洁、可读。

讲师最后推荐了一个链接给大家看看:GitHub - ReactiveX/learnrx: A series of interactive exercises for learning Microsoft's Reactive Extensions Library for Javascript.GitHub - ReactiveX/learnrx: A series of interactive exercises for learning Microsoft's Reactive Extensions Library for Javascript.

这个讲题给我最大的体会就是,去深入思考如何真正得做可重用的组件化,同时让这份代码更加可读。

下一代Web技术运用

这个主题没听全,期待其他同学补充,略。

一个前端的自我修养

开场拿出 react/angular/week 调侃:你不会敢说你会前端?
前端在于难学,而是大家不知道怎么学。如何成为像 hax 一样的前端?

20% 知识:JS、DOM、Device API、CSS、DOM、HTML、HTTP、jQuery、React ...
80% 能力:编程能力、架构能力、工程能力

编程能力:解决问题的能力(基础)
架构能力:一定规模后带来架构问题
工程问题:关于人的问题,怎么让一个团队里的人怎么协作好

怎么提升知识与能力?

知识的学习

你写代码的初心是什么,你期望把程序写过来的感觉是什么?

建立并弄清楚自己的知识体系。

为了区分好 id 和 name 的区别,winter 借了十本书去查阅这个问题。

举例怎么建立自己的知识体系。

  • 寻找线索

    • 比如学 JS, 控制台输入 for (var k in window) 看看有哪些属性,你了解这些东西么,查阅书籍能找到么?

    • 比如学 python,你了解全局有哪些东西么?

    • 看附录

    • 查源码

    • 反射

  • 建立联系

    • childNodes/parentNode nextSibling/previousSibling 等有哪些关联,你理解么?顾名思义,举一反三,串起来学习。

    • 美感

    • 完备性(比如 jQuery 有 append 就有 prepend)

    • 操作同组数据(setTimeout/setInterval/clearTimeout/clearInterval)

  • 归类

    • 画思维导图,例如你知道 zepto 有哪些 API 可以用么,能分类整理完整为一棵树么?

      • ajax

      • collection

      • dom

      • util

    • 见到前端争议的时候,你如何追本溯源找到知识?比如你知道闭包的解释,谁说的对么?

      • 查 wiki 看历史,谁定义了闭包这个概念:P J Landin

      • 查 Google 论文, P J 在一个期刊上发了篇文章,看看原文怎么定义闭包的

      • 闭包两部分:

        • 环境部分 environment

        • 控制(表达式)部分 control part

      • 通过辩证地最本溯源得到正确的判断,不断获得自信和社区声誉,这对于新人来说非常重要(摆脱新手 拍死前浪)

    • 追本溯源

      • 论文

      • 邮件列表

      • 代码提交记录(commits/issues)

  • 挑战

    • 推翻旧知识,建立新知识,这是一个循环的过程,不断完善知识体系

能力的培养

能力培养没有捷径,但有投入的技巧。

推荐一本教材:《C++程序设计语言》(为什么是教材不叫书?因为没有习题)

推荐一本书:《黑客与画家》

习题很重要。习题很重要。习题很重要。

靠自然的提升不太可能,需要刻意的大量的训练。

  • 主动性

    • 被动的 996 不会有成长

    • 每天主动再加几个小时学习才会有帮助

  • 习惯养成

    • 保持在学习区、痛苦区工作(摆脱舒适区)

  • 系统训练

    • 一万个小时理论太难,那从二十个小时开始呢?

HTTP/2时代的Web性能

作者是 @foobartel (新浪微博和推特) http://foobartel.com/

开场白:Nobody likes to wait.(含义:我们等待了 HTTP/2 太多年。)

开一个网站只需要 2-3 秒,用户就会觉得快。超过 4 秒没打开,50% 的用户就会选择离开,超过 8-10 秒,用户就会离开。

所以在 2-3 秒内打开页面,用户基本就不会抱怨。

不要以为很多用户都在用 wifi 打开网页速度很快,我们要考虑 3G/GPRS 甚至是离线的用户网络,所以更快才是更好。

现有的性能优化技巧:文件合并、精灵图片、内联图片、域名共享

有哪些地方可以优化?

渲染树的过程,DOM 布局与绘制(重绘):

  1. 等待 DOM 和 CSSDOM 构建渲染树

  2. 渲染树节点

  3. 计算布局、定位和尺寸

  4. 绘制

渲染 CSS/JS/HTML,避免或减少各种阻塞问题

每个请求都带有开销,请求顺序优化。

最佳渲染路径:让内容更快的出现在页面上(减少白屏时间)

迎接 HTTP/2

HTTP/0.9
HTTP/1.0 1996
HTTP/1.1 1999

带宽的线性提高并没有带来页面加载速度的线性提高,这是协议带来的问题(连接数有限,RTT 请求时间固定开销)

RTT 相比带宽,对性能的影响更大。

SPDY/HTTP/2

  1. 都支持多路复用

  2. 都支持头部压缩

  3. 都支持服务器端推送

  4. HTTP2 支持优先级请求

  5. HTTP2 向后兼容 HTTP/1.1

HTTP/1.1 中很多的优化技巧已经成为反模式。

HTTP/1.1 中我们为了减少体积和请求数,会做很多的合并。
HTTP/2 中同一个域名只会使用同一个 TCP 连接多路复用(不需要资源合并、资源内嵌)

  1. 不需要 CDN combo 合并了

  2. 不需要 JS/CSS 内嵌了

  3. 不需要使用雪碧图了

  4. HTTP/1.1 下很多优化技巧我们都可以抛弃,因为请求已经很廉价

HTTP/1.1 中我们为了打破浏览器并发请求次数的显示,将多个资源分布在不同的域名下。
HTTP/2 中,请求廉价,同域复用连接(理论上是无限的),所以我们要做域名收归。减少 DNS 解析反而成为了正确)

HTTP/2 中减少资源请求,域名分发不再必要,但HTTP/1.1 其他已有的优化技巧,还是需要继续使用,如 DNS 查找、减少重定向、使用 CDN、重用 CDN、开启压缩、开启缓存等。所幸的是,HTTP/2 是向后兼容的。

如何开启 HTTP/2

  1. 开启 SSL/TLS

  2. 服务器开启 支持,如 apache 的 mod_http2 模块

  3. 检查客户端支持,caniuse.com 查兼容程度

查看原文

赞 2 收藏 5 评论 2

赖小赖小赖 发布了文章 · 2016-03-04

使用 React 写个简单的活动页面运营系统 - 设计篇

介绍这个工具前不得不先介绍一下积木系统。

积木系统是 imweb 团队出品、为产品运营而生的一套活动页面发布系统,详细介绍见 PPT

简单可以这么理解它的理念:

  1. 一个页面 = 一个模板 + 多个组件

  2. 一个组件 = 一份代码 + 一份数据

  3. 一个组件开发一次,复用多次

  4. 一个页面使用多个组件拼装后,实时预览、快速发布上线

此前在阿里实习的时候也接触过一个叫 TMS(淘宝内容管理系统)的系统, 专门用于快速搭建电商运营活动页面.

这种系统可统一理解为运营活动页面发布系统。

这种系统有以下特点:

  1. 静态数据或轻后台数据(轻量 CGI)

  2. 单页(多图、图文混合偏多)

  3. 组件粒度小,可灵活拼装页面

  4. 活动页面需要快速发布上线

积木系统已经经受了多个项目的考验,目前也启动了 2.0 的开发计划, 作者 @江源 也曾在 PPT 中提到有开源的计划,大家可以期待一下。

在这里我写了一套类似的 Pager 系统,设计理念大同小异,只不过是想尝试用新的技术栈快速实现。

项目地址是: https://github.com/laispace/pager

安装环境比较麻烦,先来快速预览下它的功能。

创建一个页面, 添加可复用的组件,进行可视化编辑:

设置页面信息:

生成页面,可本地下载预览:

发布上线,同步到远程机器:

接下来,直接访问 http://pages.laispace.com/demo-page2/ 就可以看到发布的页面了。

当我把原型写出来的时候我却发现,ES6 和 React 带来的一系列特性,让我觉得代码写起来爽到飞起,所以给大家分享下有趣的东西。

目前这个代号为 Pager 的系统只实现了简单的 组件编译/页面生成/页面发布 的功能, 还不能用于生产环境.

所以本文先给大家介绍下设计思路 :( 项目完成后, 再给大家细细介绍它的实现.

项目设计

发布一个页面上线的流程

这个流程的角色主要对应是产品运营经理, 所以操作必须简单.

  1. 新建页面, 配置页面基础信息(标题/分享信息等)

  2. 在页面中添加组件并配置组件数据(实时预览/页面大小可拖拽)

  3. 新窗口打开预览页面(预览效果就是生成后的页面,需要与线上发布版本一致)

  4. 下载页面到本地(不使用一键发布, 自行下载代码使用其他系统发布)

  5. 发布页面到服务器(一键发布, 需保证服务器配置好了对应目录的访问权限)

开发一个组件的流程

这个流程的角色主要对应是前端开发, 需要保证开发模式足够舒畅.

  1. 新建组件, 编写组件代码

  2. 打开组件预览页面

  3. 修改组件配置和代码

  4. 监听修改, 实时预览更新

  5. 开发完成,同步到系统中(重新编译, 覆盖上一个版本)

项目模块划分

系统承载多个项目, 项目中配置归属这个项目的页面在发布时的一些配置信息.

一个页面由多个组件构成, 每个组件为一个文件夹, 组件间相互独立, 本地开发完成后, 编译并导入到系统中.

注意:绿色为已有功能, 目前只提供了页面创建相关功能, 还没有鉴权/版本控制等模块, 所以还不能用于生产环境.

接口设计

虽然前后端都自己写, 可以采用自己喜欢的接口方式. 但考虑到语义化和拓展性, 还是建议使用前后端分离的 restful 接口形式.

一个名词对应一个资源, 一个动词对应一个操作:

  • 增加一个组件, POST /components/

  • 删除一个组件, DELETE /components/:Id

  • 查找所有组件, GET /components/

  • 查找一个组件, GET /compnents/:Id

  • 修改一个组件, PUT /components/:Id

数据模型

前后端通信是 JSON 数据格式, 同时使用 mongoose 定义一些数据模型, 方便快速地增删查改, 建立项目原型.

像嵌套比较深的数据, 有时我们并不想定义太多, 那直接用一个 Mixed 类型就可以解决, 比如一个页面中包含多个组件, 每个组件其实是有自己的数据格式的, 我这里并不想用两张表来存储(类似外键), 所以直接在一个页面下就存储了这个页面需要的所有数据:

import mongoose from 'mongoose';
const Schema = mongoose.Schema;
const schema = new mongoose.Schema({
  name: String,
  description: String,
  components: [Schema.Types.Mixed], // 组件, 混合的数据格式
  project: String,
  config: Object
});

页面输入

  1. 页面信息(title + meta + link + script)

一个 html 页面, 从上往下是:

    • title 页面标题

    • meta 页面元信息

    • link/style 外联或内联样式(自定义样式方便快速修复UI问题而不需要重新发布代码版本)

    • script 外联或内联脚本(自定义脚本方便快速添加上报点等非固话的操作)

    1. 多个模块(component + data)

    每个组件都有自己的模板, 对应一套数据, 遵循组件粒度化,一个模板套一份数据的原则.

    1. 发布配置(publishIp+publishDir+rsync)

    不同的项目下生成不同的页面, 最终使用 rsync 将页面目录同步到远程机器, 远程机器使用 nginx/apache 配置下代理, 就实现了页面发布.

    注意: rsync 权限, 建议在远程服务器上创建对应的目录, 给予 rsync 账户只能访问这个目录, 以免带来不必要的安全问题.

    编码小结

    这个项目使用 React+ES6 写的, 和大家分享一些小心得.

    React 单向数据流降低程序复杂度

    我对 React 最重要的理解是单向的自顶向下的组件嵌套和数据流动, 带来了数据的一致性保障. 对于一些不是非常复杂的单页应用, 其实一个页面就是一个组件, 不需要用太多的 flux/redux 等方案也足矣.

    state = {name: 'simple', age: 18}
    addAge = () => {
      
        this.setState({
          
            age: this.state.age++
      
        })
    
}
    render : () => {
    
        return (
        
            <div>

                名字:<div>{this.state.name} </div>

                年龄:<div>{this.state.age} </div>
                     
                <button onClick={this.addAge}>点击加一岁</button>
                         
            </div>

            )

    }

    大胆使用ES6/7

    ES6 带来了非常多的特性, 我在使用的过程中感觉比较好玩的是以下几个.

    • import 带来真正的模块化

    • async/await 同步方式写异步代码

    • @decorator 无侵入的装饰器

    • ()=>{} 箭头函数简化代码、保留 this 作用域

    • babel+webpack 为新特性保驾护航

    import 带来真正的模块化

    模块化的方案, 以前有 AMD/CMD 甚至是 UMD, 遇上不同的项目就可以用到不同的模块化方案, 自然带有不同的学习成本.

    ES6 提供的 import/export 带来的是更舒畅的模块化, 就像在写 python 一样, 一个文件就是一个模块, 纯粹.

    有了 babel 将 ES6 无缝地转化为 ES5 代码后, 我觉得如果不考虑转化后的代码体积偏大的问题, 我们在项目中就应该拥抱 ES6.

    如果需要兼容以前的 AMD/CMD 模块, 配上 webpack 使用即可.

    // 导入全部
    import path from 'path';
    import Component from '../models/component';
    // 导入局部
    import { getComponent, getComponents } from '../utils/resources';

    async/await 同步方式写异步代码

    是异步的操作就应该使用 promise, 配合 ES7 的 async/await 语法糖, 舒服地编写同步的代码风格表示异步的操作, 爽.

    首先需要定义多个异步操作,返回 Promise:

    const findOnePage = (pageId) => new Promise((resolve, reject) => {
      Page.findOne({_id: pageId}).then(page => {
        resolve(page);
      });
    });
    const findOneProjectByName = (name) => new Promise((resolve, reject) => {
      Project.findOne({name: name}).then(project => {
        resolve(project);
      });
    });

    接着使用 await 获取异步操作的结果:

    const page = await findOnePage(pageId);
    const project = await findOneProjectByName(page.project);

    可以看到, 在使用 async/await 时, 少了回调, 少了嵌套, 代码更加易读. 当然这里的代价是我们需要封装好供 await 使用的 promise(我觉得这里还是挺麻烦的), 不过我们再也看不到回调地狱了, 我们甚至可以不使用 yield/generator 而直接过渡到 async/await 了.

    ES7? ES6 都没普及, 你 TM 叫我用 ES7?

    这不是有 babel 嘛~ 用吧!

    @decorator 使用无侵入的装饰器

    装饰器其实也就是一个语法糖, 尝试这么理解: 我们有 A/B/C 三个函数分别做了三个操作, 现在假设我们突然想在这些函数里头打印一些东西.

    去改动三个函数当然可以, 但更好的方式是定一个一个 @D 装饰器, 装饰到三个函数前面, 这样他们除了执行原有功能外, 还能执行我们注入进去的操作.

    比如我在项目中, 不同的页面都需要用到 snackbar(操作提示框), 每个页面都是一样的, 没有必要在每个页面都写一样的代码, 只需要将这个组件以及对应的方法封装为一个装饰器, 注入到每个页面组件中, 那么每个页面组件就可以直接使用这个 snackbar(操作提示框) 了.

    function withSnackbar (ComposedComponent) {
      return class withSnackbar extends Component {
        // ...
        render() {
          return (
            <div>
              <ComposedComponent {...this.props} />
              <Snackbar {...this.state}/>
            </div>
          );
        }
    
      };
    }
    import withStyles from '../../decorators/withStyles';
    import withViewport from '../../decorators/withViewport';
    import withSnackbar from '../../decorators/withSnackbar';
    
    // 装饰器
    @withViewport
    @withStyles(styles)
    @withSnackbar
    class Page extends Component {
        // ...
    }

    箭头函数简化代码、保留 this 作用域

    匿名函数使用箭头函数可以这么写:

    const emptyFunction = () = > { /*do nothing*/ };   

    有了箭头函数, 妈妈再也不怕 this 突变了...

    const socket = io('http://localhost:9999');
        socket.on('connect', () => {
          socket.on('component', (data) => {
              // 这里的 this 不会突变到指向 window
              this.showSnackbar('本地组件已更新, 自动刷新');
              this.getComponent(data.project, data.component);
            }
          });
        });

    大胆使用fetch

    使用 fetch 加 await 替代 XHR.

    fetch 比起 xhr, 做的事情是一样的, 只是接口更加语义化, 且支持 Promise.

    配合 async/await 使用的话, 那叫一个酸爽!

    try {
          const res = await fetch(`/api/generate/`, {
            method: 'post',
            // 指定请求头
            headers: {
              'Accept': 'application/json',
              'Content-Type': 'application/json'
            },
            // 指定请求体
            body: JSON.stringify(data)
          });
          // 返回的是一个 promise, 使用 await 去等待异步结果
          const json = await res.json();
          if (json.retcode === 0) {
            this.showSnackbar('生成成功');
          } else {
            this.showSnackbar('生成失败');
          }
        } catch (error) {
          console.error(error);
          this.showSnackbar('生成失败');
        }

    开发组件实时刷新

    本地开发一个组件时, 监听文件变化, 使用 WebSocket 通知页面更新.

    起一个 socket 服务, 监听文件变化:

    async function watchResources() {
      var io = require('socket.io')(9999);
      io.on('connection', function (socket) {
        event.on('component', (component) => {
          socket.emit('component', component);
        });
      });
    
      console.log('watching: ', path.join(__dirname, '../src/resources/**/*'));
      watch(path.join(__dirname, '../src/resources/**/*')).then(watcher => {
        watcher.on('changed', (filePath) => {
          console.log('file changed: ', filePath);
          // [\/\\] 是为了兼容 windows 下路径分隔的反斜杠
          const re = /resources[\/\\](.*)[\/\\]components[\/\\](.*)[\/\\](.*)/;
          const results = filePath.match(re);
          if (results && results[1] && results[2]) {
            event.emit('component', {
              project: results[1],
              component: results[2]
            });
          }
        });
      });
    }

    预览组件的页面监听文件变化, 变化后重新向服务器拉取最新编译好的组件, 进行更新.

    componentDidMount = () => {
        const socket = io('http://localhost:9999');
        socket.on('connect', () => {
          socket.on('component', (data) => {
            if ((data.project === this.state.component.project) && (data.component === this.state.component.name)) {
              console.log('component changed: ', data.project, data.component);
              this.showSnackbar('本地组件已更新, 自动刷新');
              // 重新向服务器拉取最新编译好的组件, 进行更新
              this.getComponent(data.project, data.component);
            }
          });
        });
      }

    子页面数据实时更新

    生成页面时需要预览页面, 为了避免页面样式被系统样式影响, 应该使用内嵌 iframe 的方式来隔离样式.

    父页面使用 postMessage 与子页面进行通信:

    const postPageMessage = (page) => {
      document.getElementById('pagePreviewIframe').contentWindow.postMessage({
        type: 'page',
        page: page
      }, '*');
    }

    子页面监听父页面数据变化, 更新页面:

    window.addEventListener("message", (event) =>  {
          // if(event.origin !== 'http://localhost:3000') return;
          console.log('previewPage receives message', event);
          if (event.data.type === 'page') {
            this.setState({
              page: event.data.page
            });
          }   
        }, false);

    本文是项目设计介绍, 欢迎大家多多指正. 等我把鉴权功能和版本管理加上,就可以用于生产环境啦, 敬请期待.

    查看原文

    赞 9 收藏 52 评论 0

    赖小赖小赖 发布了文章 · 2016-01-19

    使用 nvm 一键切换多版本 node

    在不同环境下有时候不同项目需要切换不同版本的 node。

    在同一个机器上切换过 node (特定是 windows 环境)遇到过各种各样莫名其妙的问题,就知道能无痛一键切换 node 是多么开心的事情了。

    尝试过很多工具,最后总结出最理想的就是 nvm 这货了。

    以下几个步骤亲测在 mac/linux 上都很好用,如果是 windows 的话,也有 nvm-windows 可以选择。

    nvm 官方地址:https://github.com/creationix/nvm

    nvm-windows 官方地址:https://github.com/coreybutler/nvm-windows

    简单总结下步骤:

    下载 nvm

    $ curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.30.1/install.sh | bash

    启动 nvm
    $ . ~/.nvm/nvm.sh

    安装特定版本的 node
    $ nvm install 4.4.2

    使用特定版本的 node
    $ nvm use 4.2.4

    现在就可以看到 node 版本已经切换
    $ node -v
    $ npm -v

    注意这种切换只在当前 shell 环境有效,也就是下次重新打开 shell 的话,就退回默认的 node 版本了。

    重启 shell 后需要再重启 nvm 切换 node 版本。

    这样虽然麻烦了点,但是非常灵活。

    觉得重启麻烦,可以把以下配置加到 ~/.bashrc、 ~/.profile 或 ~/.zshrc 文件中:
    export NVM_DIR="$HOME/.nvm"
    [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"

    注意:
    文章里的 nvm 说的不是 https://www.npmjs.com/package/nvm
    而是 https://github.com/creationix/nvm
    虽然都叫 nvm 但不是同一个东西:
    感觉是通过 npm 来切换 node/npm 就带有问题
    而完全是用系统的 shell 来切换,则更为安全

    有同学说 n 怎么样,我最开始也是用的这个工具,优点是简单轻量,直接通过 npm install -g n 开箱即用,当它会接管全局安装的一些依赖,可能带来一些冲突(我正是因为这个才转用了 nvm)。nvm 不使用 npm 安装,有自己的安装和配置方式,有自己独立的目录管理依赖,安装繁琐一些,但使用起来感觉是更为灵活。
    nvm 和 n 的对比,可以看看这篇文章:http://taobaofed.org/blog/2015/11/17/nvm-or-n/

    查看原文

    赞 0 收藏 6 评论 0

    认证与成就

    • 获得 63 次点赞
    • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

    擅长技能
    编辑

    (゚∀゚ )
    暂时没有

    开源项目 & 著作
    编辑

    (゚∀゚ )
    暂时没有

    注册于 2012-11-11
    个人主页被 962 人浏览