Sue1024

Sue1024 查看完整档案

上海编辑西南财经大学  |  金融智能与信息管理 编辑  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

Sue1024 发布了文章 · 2018-08-28

TestCafe 搭建前端End-to-End自动化测试工具

What is Test Cafe

TestCafe is a node.js tool to automate end-to-end web testing, you can write tests in JS or TypeScript, run them and view results.
简言之, Testcafe就是一个可以自动化前端end-to-end测试的工具,我们可以使用简单的JS或者Typescript写测试用例。

Installation

preparation

需要提前安装NodeJS, 官网没有指定版本,本文基于NodeJS 8+撰写。

Install TestCafe

  1. Globally
    npm install -g testcafe
  2. Locally
    npm install --save-dev testcafe

建议使用本地安装,这样团队里其他人直接npm install便可安装相同版本的所有依赖。

How to run test cases

Command Line Interface

可以使用命令行执行单元测试

  1. 使用指定浏览器
    testcafe chrome tests.js
    testcafe path:/safari.app tests.js
  2. 使用所有安装的浏览器
    testcafe all tests.js
  3. headless mode
    Chrome 和 Firefox 支持 headless mode
    testcafe "chrome:headless" tests.js

更多信息请参考 Command Line Interface

Programming Interface

也可以写JS代码用Node执行单元测试,这也是本文着重推荐的方法,因为这个方式更加灵活。

  1. 引入工厂函数
    const createTestCafe = require('testcafe')
  2. 使用工厂函数获得TestCafe实例
    工厂函数接受三个参数, 分别是 host controlPanelPort servicePort,返回一个promise, 该promise的最终结果便是一个TestCafe实例

    createTestCafe('localhost', 1337, 1338)
    .then(testcafe => {
        /* ... */
    });

    TestCafe对外暴露三个方法,分别是: createBrowserConnectioncreateRunnerclose,详情请参考 TestCafe Class

  3. 调用 createRunner
    testcafe.createRunner返回一个Runner实例,该实例对外暴露多个方法,用于配置和执行测试任务,支持链式调用,比如:

    const runner = testcafe.createRunner();
    return runner
        .src(['test1.js', 'test2.js']) // 可以提前定义一个数组,或者将需要执行的文件保存在一个文件里,更加灵活,也可以配置文件夹
        .filter((testName, fixtureName, fixturePath) => {
            //Add some filters based on testName, fixtureName, fixturePath
         })
         .browsers(["chrome:headless"])
         .reporter('json', stream) // stream is the report file, like const stream = fs.createWriteStream('report.json');
         .run({
             selectorTimeout: 10000, // the timeout testcafe wait for an element to appear
             skipJsErrors: true // to ignore JS error
         });

    详情请参考 Runner Class

How to write test cases

Code Structure

Fixture

TestCafe使用fixture来组织测试用例,一个测试文件必须包含一个或多个fixture

fixture(fixtureName)
fixture `fixtureName`

可以指定fixture的开始页面:

fixture.page(url)
fixture.page `url`

Test Case

然后写测试用例

test
    .page `url`
    (testName, async t=> {
        /* Test Code */
    })

注意传入的参数t,它是 test controller,包含测试API和测试用例上下文,使用它我们可以调用 test actions, 处理浏览器对话框,等待,执行断言。

Make Test Step Common

也许你会注意到 t 是在测试用例中拿到的, 如果我们需要把一个公用的action抽出来,如何获得 t 呢?
TestCafe提供了一种直接引入t的方式,此时t不包含具体测试用例的上下文,但包含了测试API, 比如:

async login() {
    await t
        .typeText("#user", "name")
        .typeText("#pwd", "pwd")
        .click("#login")
}

看到这里,也许你对typeTextclick很陌生,没关系,后面会提到。

Test Hooks

Test Hooks, 执行在Test Case之前或之后

fixture.beforeEach(fn(t))
fixture.afterEach(fn(t))
test.before(fn(t))
test.after(fn(t))

注意 test.beforetest.after会覆盖fixture.beforeEachfixture.afterEach, Test Hooks 同样会拿到Test Controller实例。

Skip Tests

可以跳过某些test case 或者fixture

fixture.skip
test.skip

也可以指定只执行某些test case或fixture

fixture.only
test.only

Selectors

请参考Selectors

Actions

请参考Actions

Assertions

AssertionDescription
eqldeep equal
notEqlnot deep equal
okactual is true
notOkactual is false
containsArray or String or Object or promise contains
notContainsArray or String or Object or promise not contains
typeOftype check
notTypeOftype check
gtGreater than
gtegreater than or equal to
ltless than
lteless than or equal to
withinrange from start and finish
notWithinnot range from start and finish
matchregex check
notMatchregex check

详情请参考Assertion API

Tricks

ClientFunction

在之前的章节我们说在 test case 中, 我们可以执行 test controller 对外暴露的 action, 执行断言,获取上下文变量等等,但是关于 client side 的数据却无法直接拿到,比如:

fixture("Fixture")
    test('window', async t=> {
        await t.navigateTo("url");
        await t.expect(window.location.href).eql("url")
    })

会报出如下错误:

window is not defined

TestCafe 提供了ClientFunction构造函数,我们可以传入一个回调函数,在回调函数中可以访问 window

const getWindowLocation = ClientFunction(() => window.location)
fixture("Fixture")
    test('window', async t=> {
        await t.navigateTo("url");
        let location = await getWindowLocation();
        await t.expect(location.href).eql("url")
    })

Role

在很多网站中,具有不同角色的用户可以访问不同的界面或功能,TestCafe 提供了Role构造方法并且在 TestController 中暴露 UseRole方法以便切换角色。

  1. 首先引入 Role 构造函数

    import { Role } from 'testcafe'
  2. 创建 role

    const user = Role(Env.LOGIN_URL, async t => {
        await t
            .typeText("userInput", "name")
            .typeText("pwdInput", "123")
            .click("submitBtn");
    });
  3. 在 test case 中切换 role

    t.useRole(user)

How to debug

笔者认为TestCafe的调试功能不太成熟,只支持下一步等简单操作

t.debug()
查看原文

赞 0 收藏 0 评论 0

Sue1024 发布了文章 · 2018-08-27

TestCafe 前端 End-to-End 自动化测试工具

What is Test Cafe

TestCafe is a node.js tool to automate end-to-end web testing, you can write tests in JS or TypeScript, run them and view results.
简言之, Testcafe就是一个可以自动化前端end-to-end测试的工具,我们可以使用简单的JS或者Typescript写测试用例。

Installation

preparation

需要提前安装NodeJS, 官网没有指定版本,本文基于NodeJS 8+撰写。

Install TestCafe

  1. Globally
    npm install -g testcafe
  2. Locally
    npm install --save-dev testcafe

建议使用本地安装,这样团队里其他人直接npm install便可安装相同版本的所有依赖。

How to run test cases

Command Line Interface

可以使用命令行执行单元测试

  1. 使用指定浏览器
    testcafe chrome tests.js
    testcafe path:/safari.app tests.js
  2. 使用所有安装的浏览器
    testcafe all tests.js
  3. headless mode
    Chrome 和 Firefox 支持 headless mode
    testcafe "chrome:headless" tests.js

更多信息请参考 Command Line Interface

Programming Interface

也可以写JS代码用Node执行单元测试,这也是本文着重推荐的方法,因为这个方式更加灵活。

  1. 引入工厂函数
    const createTestCafe = require('testcafe')
  2. 使用工厂函数获得TestCafe实例
    工厂函数接受三个参数, 分别是 host controlPanelPort servicePort,返回一个promise, 该promise的最终结果便是一个TestCafe实例

    createTestCafe('localhost', 1337, 1338)
    .then(testcafe => {
        /* ... */
    });

    TestCafe对外暴露三个方法,分别是: createBrowserConnectioncreateRunnerclose,详情请参考 TestCafe Class

  3. 调用 createRunner
    testcafe.createRunner返回一个Runner实例,该实例对外暴露多个方法,用于配置和执行测试任务,支持链式调用,比如:

    const runner = testcafe.createRunner();
    return runner
        .src(['test1.js', 'test2.js']) // 可以提前定义一个数组,或者将需要执行的文件保存在一个文件里,更加灵活,也可以配置文件夹
        .filter((testName, fixtureName, fixturePath) => {
            //Add some filters based on testName, fixtureName, fixturePath
         })
         .browsers(["chrome:headless"])
         .reporter('json', stream) // stream is the report file, like const stream = fs.createWriteStream('report.json');
         .run({
             selectorTimeout: 10000, // the timeout testcafe wait for an element to appear
             skipJsErrors: true // to ignore JS error
         });

    详情请参考 Runner Class

How to write test cases

Code Structure

Fixture

TestCafe使用fixture来组织测试用例,一个测试文件必须包含一个或多个fixture

fixture(fixtureName)
fixture `fixtureName`

可以指定fixture的开始页面:

fixture.page(url)
fixture.page `url`

Test Case

然后写测试用例

test
    .page `url`
    (testName, async t=> {
        /* Test Code */
    })

注意传入的参数t,它是 test controller,包含测试API和测试用例上下文,使用它我们可以调用 test actions, 处理浏览器对话框,等待,执行断言。

Make Test Step Common

也许你会注意到 t 是在测试用例中拿到的, 如果我们需要把一个公用的action抽出来,如何获得 t 呢?
TestCafe提供了一种直接引入t的方式,此时t不包含具体测试用例的上下文,但包含了测试API, 比如:

async login() {
    await t
        .typeText("#user", "name")
        .typeText("#pwd", "pwd")
        .click("#login")
}

看到这里,也许你对typeTextclick很陌生,没关系,后面会提到。

Test Hooks

Test Hooks, 执行在Test Case之前或之后

fixture.beforeEach(fn(t))
fixture.afterEach(fn(t))
test.before(fn(t))
test.after(fn(t))

注意 test.beforetest.after会覆盖fixture.beforeEachfixture.afterEach, Test Hooks 同样会拿到Test Controller实例。

Skip Tests

可以跳过某些test case 或者fixture

fixture.skip
test.skip

也可以指定只执行某些test case或fixture

fixture.only
test.only

Selectors

请参考Selectors

Actions

请参考Actions

Assertions

AssertionDescription
eqldeep equal
notEqlnot deep equal
okactual is true
notOkactual is false
containsArray or String or Object or promise contains
notContainsArray or String or Object or promise not contains
typeOftype check
notTypeOftype check
gtGreater than
gtegreater than or equal to
ltless than
lteless than or equal to
withinrange from start and finish
notWithinnot range from start and finish
matchregex check
notMatchregex check

详情请参考Assertion API

Tricks

ClientFunction

在之前的章节我们说在 test case 中, 我们可以执行 test controller 对外暴露的 action, 执行断言,获取上下文变量等等,但是关于 client side 的数据却无法直接拿到,比如:

fixture("Fixture")
    test('window', async t=> {
        await t.navigateTo("url");
        await t.expect(window.location.href).eql("url")
    })

会报出如下错误:

window is not defined

TestCafe 提供了ClientFunction构造函数,我们可以传入一个回调函数,在回调函数中可以访问 window

const getWindowLocation = ClientFunction(() => window.location)
fixture("Fixture")
    test('window', async t=> {
        await t.navigateTo("url");
        let location = await getWindowLocation();
        await t.expect(location.href).eql("url")
    })

Role

在很多网站中,具有不同角色的用户可以访问不同的界面或功能,TestCafe 提供了Role构造方法并且在 TestController 中暴露 UseRole方法以便切换角色。

  1. 首先引入 Role 构造函数

    import { Role } from 'testcafe'
  2. 创建 role

    const user = Role(Env.LOGIN_URL, async t => {
        await t
            .typeText("userInput", "name")
            .typeText("pwdInput", "123")
            .click("submitBtn");
    });
  3. 在 test case 中切换 role

    t.useRole(user)

How to debug

笔者认为TestCafe的调试功能不太成熟,只支持下一步等简单操作

t.debug()
查看原文

赞 1 收藏 1 评论 1

Sue1024 发布了文章 · 2018-08-27

TestCafe 搭建前端End-to-End自动化测试工具

What is Test Cafe

TestCafe is a node.js tool to automate end-to-end web testing, you can write tests in JS or TypeScript, run them and view results.
简言之, Testcafe就是一个可以自动化前端end-to-end测试的工具,我们可以使用简单的JS或者Typescript写测试用例。

Installation

preparation

需要提前安装NodeJS, 官网没有指定版本,本文基于NodeJS 8+撰写。

Install TestCafe

  1. Globally
    npm install -g testcafe
  2. Locally
    npm install --save-dev testcafe

建议使用本地安装,这样团队里其他人直接npm install便可安装相同版本的所有依赖。

How to run test cases

Command Line Interface

可以使用命令行执行单元测试

  1. 使用指定浏览器
    testcafe chrome tests.js
    testcafe path:/safari.app tests.js
  2. 使用所有安装的浏览器
    testcafe all tests.js
  3. headless mode
    Chrome 和 Firefox 支持 headless mode
    testcafe "chrome:headless" tests.js

更多信息请参考 Command Line Interface

Programming Interface

也可以写JS代码用Node执行单元测试,这也是本文着重推荐的方法,因为这个方式更加灵活。

  1. 引入工厂函数
    const createTestCafe = require('testcafe')
  2. 使用工厂函数获得TestCafe实例
    工厂函数接受三个参数, 分别是 host controlPanelPort servicePort,返回一个promise, 该promise的最终结果便是一个TestCafe实例

    createTestCafe('localhost', 1337, 1338)
    .then(testcafe => {
        /* ... */
    });

    TestCafe对外暴露三个方法,分别是: createBrowserConnectioncreateRunnerclose,详情请参考 TestCafe Class

  3. 调用 createRunner
    testcafe.createRunner返回一个Runner实例,该实例对外暴露多个方法,用于配置和执行测试任务,支持链式调用,比如:

    const runner = testcafe.createRunner();
    return runner
        .src(['test1.js', 'test2.js']) // 可以提前定义一个数组,或者将需要执行的文件保存在一个文件里,更加灵活,也可以配置文件夹
        .filter((testName, fixtureName, fixturePath) => {
            //Add some filters based on testName, fixtureName, fixturePath
         })
         .browsers(["chrome:headless"])
         .reporter('json', stream) // stream is the report file, like const stream = fs.createWriteStream('report.json');
         .run({
             selectorTimeout: 10000, // the timeout testcafe wait for an element to appear
             skipJsErrors: true // to ignore JS error
         });

    详情请参考 Runner Class

How to write test cases

Code Structure

Fixture

TestCafe使用fixture来组织测试用例,一个测试文件必须包含一个或多个fixture

fixture(fixtureName)
fixture `fixtureName`

可以指定fixture的开始页面:

fixture.page(url)
fixture.page `url`

Test Case

然后写测试用例

test
    .page `url`
    (testName, async t=> {
        /* Test Code */
    })

注意传入的参数t,它是 test controller,包含测试API和测试用例上下文,使用它我们可以调用 test actions, 处理浏览器对话框,等待,执行断言。

Make Test Step Common

也许你会注意到 t 是在测试用例中拿到的, 如果我们需要把一个公用的action抽出来,如何获得 t 呢?
TestCafe提供了一种直接引入t的方式,此时t不包含具体测试用例的上下文,但包含了测试API, 比如:

async login() {
    await t
        .typeText("#user", "name")
        .typeText("#pwd", "pwd")
        .click("#login")
}

看到这里,也许你对typeTextclick很陌生,没关系,后面会提到。

Test Hooks

Test Hooks, 执行在Test Case之前或之后

fixture.beforeEach(fn(t))
fixture.afterEach(fn(t))
test.before(fn(t))
test.after(fn(t))

注意 test.beforetest.after会覆盖fixture.beforeEachfixture.afterEach, Test Hooks 同样会拿到Test Controller实例。

Skip Tests

可以跳过某些test case 或者fixture

fixture.skip
test.skip

也可以指定只执行某些test case或fixture

fixture.only
test.only

Selectors

请参考Selectors

Actions

请参考Actions

Assertions

AssertionDescription
eqldeep equal
notEqlnot deep equal
okactual is true
notOkactual is false
containsArray or String or Object or promise contains
notContainsArray or String or Object or promise not contains
typeOftype check
notTypeOftype check
gtGreater than
gtegreater than or equal to
ltless than
lteless than or equal to
withinrange from start and finish
notWithinnot range from start and finish
matchregex check
notMatchregex check

详情请参考Assertion API

Tricks

ClientFunction

在之前的章节我们说在 test case 中, 我们可以执行 test controller 对外暴露的 action, 执行断言,获取上下文变量等等,但是关于 client side 的数据却无法直接拿到,比如:

fixture("Fixture")
    test('window', async t=> {
        await t.navigateTo("url");
        await t.expect(window.location.href).eql("url")
    })

会报出如下错误:

window is not defined

TestCafe 提供了ClientFunction构造函数,我们可以传入一个回调函数,在回调函数中可以访问 window

const getWindowLocation = ClientFunction(() => window.location)
fixture("Fixture")
    test('window', async t=> {
        await t.navigateTo("url");
        let location = await getWindowLocation();
        await t.expect(location.href).eql("url")
    })

Role

在很多网站中,具有不同角色的用户可以访问不同的界面或功能,TestCafe 提供了Role构造方法并且在 TestController 中暴露 UseRole方法以便切换角色。

  1. 首先引入 Role 构造函数

    import { Role } from 'testcafe'
  2. 创建 role

    const user = Role(Env.LOGIN_URL, async t => {
        await t
            .typeText("userInput", "name")
            .typeText("pwdInput", "123")
            .click("submitBtn");
    });
  3. 在 test case 中切换 role

    t.useRole(user)

How to debug

笔者认为TestCafe的调试功能不太成熟,只支持下一步等简单操作

t.debug()
查看原文

赞 0 收藏 0 评论 0

Sue1024 收藏了文章 · 2018-07-13

React-StepPitGuide《React踩坑指南》- React基础知识

React-StepPitGuide《React踩坑指南》

十行代码九个警告八个错误竟然敢说七日精通六天学会五湖四海也不见如此三心二意之项目经理简直一等下流。

自己总结的关于React学习心得,欢迎大家与我交流React
Github:TYRMars

gitbook经常出问题,所以迁到自建的站点了。

React 基础知识

图片描述

查看原文

Sue1024 赞了文章 · 2018-07-12

React-StepPitGuide《React踩坑指南》- React基础知识

React-StepPitGuide《React踩坑指南》

十行代码九个警告八个错误竟然敢说七日精通六天学会五湖四海也不见如此三心二意之项目经理简直一等下流。

自己总结的关于React学习心得,欢迎大家与我交流React
Github:TYRMars

gitbook经常出问题,所以迁到自建的站点了。

React 基础知识

图片描述

查看原文

赞 216 收藏 248 评论 3

Sue1024 发布了文章 · 2018-07-12

NodeJS + Lighthouse + Gulp 搭建自动化网站性能测试工具

假设你还不知道Lighthouse是什么

Lighthouse is an open-source, automated tool for improving the quality of web pages. You can run it against any web page, public or requiring authentication. It has audits for performance, accessibility, progressive web apps, and more.

You can run Lighthouse in Chrome DevTools, from the command line, or as a Node module. You give Lighthouse a URL to audit, it runs a series of audits against the page, and then it generates a report on how well the page did. From there, use the failing audits as indicators on how to improve the page. Each audit has a reference doc explaining why the audit is important, as well as how to fix it.

Lighthouse 是Google公司旗下一个开源的、可自动化检测网站质量的工具,界面友好、操作简单、使用方式多样、视角全面,可以用它来测试任意网页,普通用户、QA、开发都可以快速上手。

启动姿势

难度系数 +1

使用Lighthouse的方式有很多种,最简单的,可以使用 Chrome 的开发者工具,步骤如下:

  1. 打开 Chrome 浏览器
  2. 按F12
  3. 在弹出来的窗口中打开 audits 标签
  4. 点击 Perform an audit...勾选全部
  5. Run audit

难度系数+2

也可以使用命令行。

  1. 安装Node
  2. 安装Lighthouse npm install -g lighthouse
  3. 在命令行中run lighthouse <url>

以上两种使用方式都不是本文的重点,如果想深入了解,可参照 Run Lighthouse in DevTools

难度系数+3

由于最近在学习 NodeJS, 因此笔者决定使用 Node 8 + Gulp 来跑 lighthouse,为了提高结果的准确性,每次task都跑10次 lighthouse, 并且只关心结果指标中的 first-meaningful-paint 毫秒数,最终取10次的平均值,为了可视化与可读性,最终的结果以网页的形式展示,用户可在网页上看到每次执行 Lighthouse 之后 first-meaningful-paint 的毫秒数,也可以看到平均值,如果用户对某次执行的细节感兴趣,可以点击链接察看。最终的结果长这个样子:

clipboard.png

clipboard.png

环境搭建

安装 Node 8

安装依赖包

npm i lighthouse --save-dev
npm i chrome-launcher --save-dev
npm i fs-extra --save-dev
npm i gulp --save-dev

配置

在项目根目录下创建Lighthouse的配置文件 lighthouse-config.js, 这里我们全部使用默认配置,使用默认配置需在配置文件中声明 extends: 'lighthouse:default'

module.exports = {
    extends: 'lighthouse:default'
}

如果读者需要了解更详细的配置选项,可参考:

  1. Lighthouse 这篇大部分内容是关于命令行的,命令行参数同样可用于Node
  2. throttling这篇是关于网络模拟的
  3. Default Config 具体的默认配置参数
  4. Web Page Test 模拟不同的网速
  5. Emulation 模拟不同的设备

Coding

在项目根目录下创建 gulpfile.js,首先引入所有依赖的工具:

const gulp = require('gulp');
const lighthouse = require('lighthouse');
const chromeLauncher = require('chrome-launcher');
const printer = require('lighthouse/lighthouse-cli/printer');
const Reporter = require('lighthouse/lighthouse-core/report/report-generator');
const fs = require('fs-extra');
const config = require('./lighthouse-config.js');

在开始使用 lighthouse 之前,首先创建一个写入文件的方法, 用于最后生成自定义的 report 文件:

async function write(file, report) {
    try {
        await fs.outputFile(file, report);
    } catch (e) {
        console.log("error while writing report ", e);
    }
}

调用 Lighthouse 之前,我们需要首先启动一个 Chrome 的 instance ,并将端口号提供给 Lighthouse 。--headless表示不打开 browser 窗口。

async function launchChrome() {
    let chrome;
    try {
        chrome =  await chromeLauncher.launch({
          chromeFlags: [
            "--disable-gpu",
            "--no-sandbox",
            "--headless"
          ],
          enableExtensions: true,
          logLevel: "error"
        });
        console.log(chrome.port)
        return {
            port: chrome.port,
            chromeFlags: [
                "--headless"
            ],
            logLevel: "error"
         }
    } catch (e) {
        console.log("Error while launching Chrome ", e);
    }
}    

Chrome 实例启动之后,我们就可以调用 Lighthouse , 调用时,须提供需要进行性能测试的网站,参数,以及前文创建好的配置,参数包含了 Chrome 启动端口,启动方式(是否 headless 等信息)。

async function lighthouseRunner(opt) {
    try {
        return await lighthouse("https://www.baidu.com", opt, config);
    } catch (e) {
        console.log("Error while running lighthouse");
    }
}     

Lighthouse 的返回结果是一个包含性能测试结果, 最终版的配置参数, 指标分组等信息的 json 对象,读者可以参考 Understanding Results 获得更深入的理解。
由于这里我们需要使用 Lighthouse 官方的模板生成报告,因此调用官方提供的方法,注意第一个参数传入 result.lhr, 第二个参数声明生成 html 报告(还可以生成 csv 等格式的报告)。

function genReport(result) {
    return Reporter.generateReport(result.lhr, 'html');
}

下面我们写一个将上面几个方法串起来的函数,首先启动一个 Chrome 实例, 然后将 Chrome 实例的某些参数传递给 Lighthouse,使用 lighthouse 跑出来的结果生成报告,并写入 html 文件, html文件应带有时间戳和执行顺序作为唯一标识。start 方法返回结果中的first-meaningful-paint(这是我们最关心的指标,读者可根据自身需要替换,具体指标可参考 Lighthouse)。

async function run(timestamp, num) {
    let chromeOpt = await launchChrome();
    let result = await lighthouseRunner(chromeOpt);
    let report = genReport(result);
    await printer.write(report, 'html', `./cases/lighthouse-report@${timestamp}-${num}.html`);
    return result.lhr.audits['first-meaningful-paint'].rawValue;
    await chrome.kill();
}

下面, 我们可以正式开始写一个 gulp task 啦,首先获得当前时间戳,用于最终生成的报告命名,然后声明一个数组,用于记录每次跑 Lighthouse 生成的 first-meaningful-paint 毫秒数,然后跑10次 Lighthouse, 使用提前创建的模板文件,根据这10的结果,生成一个汇总报告,这里,笔者使用了Lighthouse对外暴露的工具函数进行字符串的替换。

gulp.task('start', async function() {
  let timestamp = Date.now();
  let spent = [];
  for(let i=0; i<5; i++) {
      spent.push(await run(timestamp, i));
  }
  let template = await fs.readFileSync('./summary/template/template.html', 'utf-8');
  let summary = Reporter.replaceStrings(template, [{
      search: '%%TIME_SPENT%%',
      replacement: JSON.stringify(spent)
  }, {
      search: '%%TIMESTAMP%%',
      replacement: timestamp
  }]);
  write(`./summary/report/summary@${timestamp}.html`, summary)
})

最后的最后, 执行:

gulp start

万事大吉。
附上汇总界面的模板源码:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
  <title>Lighthouse Summary Report</title>
  <style>
    body {
      font-family: sans-serif;
    }
    table {
      margin: auto;
    }
    tr {
      border: 1px solid grey;
    }
    h1 {
       text-align: center;
       margin: 30px auto 50px auto
    }
  </style>
</head>
<body>
<table>
  <h1>Performance Summary Report</h1>
  <tr>
    <th>
      Case No.
    </th>
    <th>
      First Meaningful Paint
    </th>
    <th>
      Link To Details
    </th>
  </tr>
  <tbody id="tableBody">
  </tbody>
</table>
<script>
let timespent = %%TIME_SPENT%%;
let timestamp = %%TIMESTAMP%%;
let tableBody = document.getElementById("tableBody");
let content = '';
for(let i=0; i < timespent.length; i++) {
  content += `<tr style="border: 1px solid grey">
    <td>
      ${i+1}
    </td>
    <td>
      ${timespent[i]}
    </td>
    <td>
      <a href="../../cases/lighthouse-report@${timestamp}-${i}.html">View Details</a>
    </td>
  </tr>`
}
let total = timespent.reduce((i, j) => {
  return i + j;
})
let count = timespent.filter(function(i) { return i}).length
content += `<tr>
<td>
  AVG
</td>
<td>
${total / count}
</td>
</tr>`
tableBody.innerHTML = content;
</script>
</body>
</html>

源码地址

把最重要的放到最后 附上github源码
lighthouse-node

查看原文

赞 7 收藏 4 评论 1

Sue1024 发布了文章 · 2018-05-21

HTML5 Canvas(实战:绘制饼图2 Tooltip)

继上一篇HTML5 Canvas(实战:绘制饼图)之后,笔者研究了一下如何给饼图加鼠标停留时显示的提示框。

Plot对象

在开始Coding之前,笔者能够想到的最easy的方式,就是给饼图的每一个区域添加mousemove事件,鼠标在其上移动时则显示对应的提示框,so easy!可事实不是这样子滴~

我们肉眼上看上去是一块一块的东西,canvas并没有真的把它们分成一块一块的HTMLElement,我们只能给canvas绑定事件。那么如何得知鼠标当前停留在哪块区域呢,可以通过计算鼠标位置与圆心连线与基准线给的夹角是否在区域的起始角度与终止角度之间,为此,我们需要保存每个区域的角度信息。
为了方便保存,创建一个构造函数Plot。

function Plot(start, end, color, data) {
  this.start = start;
  this.end = end;
  this.color = color;
  this.data = data;
}

可以将上一篇文章中的绘制图例方法和绘制饼图区域的方法都放进Plot的原型链中


Plot.prototype.drawLegend = function() {
  ctx.fillRect(legend_posX, legend_posY, legend_width, legend_height);
  ctx.font = 'bold 12px Arial';
  var percent = this.data.label + ' : ' + (this.data.portion * 100).toFixed(2) + '%';
  ctx.fillText(percent, legend_textX, legend_textY);
}
Plot.prototype.drawPlot = function() {
  ctx.fillStyle = this.color;
  ctx.beginPath();
  ctx.moveTo(center.x, center.y);
  ctx.arc(center.x, center.y, radius, this.start, this.end, false);
  ctx.closePath();
  ctx.fill();
}

定制的Tooltip

在上一篇文章 HTML5 Canvas(实战:绘制饼图) 可以看出,在我们的最初设计中,Tooltip上显示的内容是可以定制化的,用户可以设定一个如下的模板:

Year: {{year}}, Data: {{data}}

我们的目标是将上面的模板转化成:

Year: 2017, Data: 3000

新建一个工具方法,接受template字符串,以及鼠标当前停留plot中的数据,返回实际显示的字符串:

function replaceAttr(text, data) {
    while (text.indexOf("{{") != -1) {
      var start = text.indexOf("{{"),
          end = text.indexOf("}}"),
          attr = text.substring(start + 2, end);
      text = text.replace("{{" + attr + "}}", data[attr]);
    }
    return text;
}

注意,从代码中可以看出,不要习惯性的在{{}}之间加入空格。

鼠标在哪

为了判断鼠标停留的区域,我们需要完成如下两步:

  1. 计算鼠标位置和圆心之间的弧度angle
  2. 遍历plots,判断angle是否位于某一个plotstartAngleendAngle之间,如果找到了这个plot,判断这个plot是否是上一次的鼠标所在的区域,如果是,说明没有必要绘制Tooltip,如果不是,重绘图表。假如没有找到对应的区域,说明鼠标不在canvas的饼图区域,可能指向图例、标题或者空白区域,此时应该清空全局变量currentPlot并重绘画布。

关于如何判断鼠标位置与圆心之间的弧度,小编画了如下的一个饼图,只能帮到这儿了...

function getAngle(cx, cy, mx, my) {
    var x = Math.abs(cx - mx),
        y = Math.abs(cy - my),
        z = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)),
        cos = y / z,
        radina = Math.acos(cos);

    if (mx > cx && my > cy) {
      return radina;
    } else if (mx < cx && my > cy) {
      return Math.PI / 2 + radina;
    } else if (mx > cx && my < cy) {
      return 3 * Math.PI / 2 - radina
    } else {
      return 3 * Math.PI / 2 + radina
    }
}
function onMouseMove(e) {
    var ex = e.pageX - cv.offsetLeft,
        ey = e.pageY - cv.offsetTop;
    var angle = getAngle(center.x, center.y, ex, ey);
    for (let i = 0; i < plots.length; i++) {
      if (plots[i].start < angle && plots[i].end > angle) {
        if (currentPlot != plots[i]) {
          currentPlot = plots[i];
          draw();
        }
        return;
      }
    }
    currentPlot = null;
    draw();
  }

现在我们知道了鼠标当前停留的位置,也可以定制要提示的文字,现在可以绘制提示框啦,以下代码有些累赘,计算过程也有些问题,笔者改天再重新算算~

Plot.prototype.drawTooltip = function() {
    var text = replaceAttr(op.tooltip.template, this.data);
    var width_tooltipText = ctx.measureText(text).width,
        height_tooltipText = parseInt(op.tooltip.font.size, 10),
        angle = (this.start + this.end) / 2 / (2 * Math.PI) *360;
    var tan = Math.tanh(angle),
        x = 0,
        y = 0;

    if (angle < 90)((x = radius / 2 * tan + center.x) || true) && ((y = -radius / 2 + center.y) || true)
    else if (angle > 90 && angle < 180)((x = radius / 2 * tan + center.x) || true) && ((y = radius / 2 + center.y) || true)
    else if (angle > 180 && angle < 270)((x = -radius / 2 * tan + center.x) || true) && ((y = radius / 2 + center.y) || true)
    else if (angle > 270 && angle < 360)((x = -radius / 2 * tan + center.x) || true) && ((y = -radius / 2 + center.y) || true)
    var tooltip_box_x = x - radius / 4,
        tooltip_box_y = y,
        tooltip_box_width = width_tooltipText + 10,
        tooltip_box_height = height_tooltipText + 10,
        tooltip_text_x = x - radius / 4 + 5,
        tooltip_text_y = y + 10 + 2;
    ctx.fillStyle = 'white';
    ctx.fillRect(tooltip_box_x, tooltip_box_y, tooltip_box_width, tooltip_box_height);
    ctx.fillStyle = '#000';
    ctx.fillText(text, tooltip_text_x, tooltip_text_y);
}

每次重绘Tooltip时都需要重绘饼图,而startAngleendAngle在每次绘制时都会修改,因此绘制前需要重置。

function clear() {
    ctx.clearRect(0, 0, cv.width, cv.height);
    startAngle = 0;
    endAngle = 0;
    cv.onmousemove = null;
}

最终我们的draw方法~

function draw() {
    clear();
    title_text = op.title.text;
    ctx.font = op.title.font.weight + " " + op.title.font.size + "px " + op.title.font.family;
    title_width = ctx.measureText(title_text).width;
    title_height = op.title.font.size;
    title_position = {
      x: (width, title_width) / 2,
      y: 20 + title_height
    };
    ctx.fillText(title_text, title_position.x, title_position.y);
    radius = (height - title_height - title_position.y - 20) / 2;
    center = {
      x: radius + 20,
      y: radius + 30 + title_position.y
    };
    legend_width = op.legend.font.size * 2.5;
    legend_height = op.legend.font.size * 1.2;
    legend_posX = center.x * 2 + 20;
    legend_posY = 80;
    legend_textX = legend_posX + legend_width + 5;
    legend_textY = legend_posY + op.legend.font.size * 0.9;
    ctx.strokeStyle = 'grey';
    ctx.lineWidth = 3;
    ctx.strokeRect(0, 0, width, height);

    for (var i = 0, len = data_c.length; i < len; i++) {
      endAngle += data_c[i].portion * 2 * Math.PI;
      var plot = new Plot(startAngle, endAngle, data_c[i].color, data_c[i])
      plots.push(plot);
      plot.drawPlot();
      startAngle = endAngle;
      legend_posY += (10 + legend_height);
      legend_textY += (10 + legend_height);
      plot.drawLegend();
    }
    if (currentPlot) {
      currentPlot.drawTooltip();
    }
    cv.onmousemove = onMouseMove;
}

成品图:

源码地址:https://github.com/Sue1024/ca...

查看原文

赞 1 收藏 4 评论 0

Sue1024 发布了文章 · 2018-05-07

HTML5 Canvas(基础知识)

最近笔者在学习HTML5的新元素<canvas>,会分享一些基础知识以及小例子,最终使用<canvas>实现一个绘制简单图表(条形图、线图或者饼图)的js库,会更新一到两篇文章~
下面我们开始吧~

确认宽度和高度

我们首先应该指定<canvas>标签即画布的宽度和高度属性,并在开始和闭合标签之间添加后备信息:

<html>
  <head>
    <meta charset="utf-8">
  </head>
  <body>
    <canvas id="canvas" width="500" height="500">Canvas is not supported.</canvas>
  </body>
</html>

取得绘图上下文

调用canvasgetContext()方法,这个方法接收一个参数,即上下文的名字:

var canvas = document.getElementById("canvas");
if(canvas.getContext) {
  var context = canvas.getContext("2d")
}

上述例子中,我们在调用getContext()方法时,首先检测其是否存在,这是由于有的浏览器遇到HTML规范之外的标签时,也会创建一个DOM对象,比如我们在Chrome中尝试以下代码:

<ppp id="ppp"></ppp>
document.getElementById("ppp");
//<ppp id="ppp"></ppp>

因此即使当前浏览器不能兼容HTML规范中的canvas元素,同样会创建DOM对象,可其中却不存在getContext()
context上下文对象中包含了绘图需要的一系列属性和方法,大家在阅读本文时,记得区分属性和方法,改变属性会影响到后续的绘图效果,而调用方法往往是一次性的。

绘制简单的2D图形

坐标原点

2D上下文的坐标默认开始于左上角,原点坐标为(0, 0),使用translate(x, y)可以更改坐标原点。

填充

填充,就是用指定的样式,比如颜色、渐变或图像填充指定区域,对应的上下文属性为fillStyle,默认值为#000000, 比如:

context.fillStyle = "orange";
描边

描边就是在指定区域边缘画指定样式的线,比如:

context.strokeStyle = "grey";
绘制矩形

矩形是唯一一个可以在2D上下文中直接话的形状,其他的都需要绘制路径,可以使用3个方法直接绘制矩形。

fillRect(x, y, width, height)

可以绘制一个由fillStyle指定填充样式的矩形,比如:

context.translate(100, 100)
context.fillStyle = "#99cccc";
context.fillRect(-100, -50, 200, 100);
context.fillStyle="#3399cc";
context.fillRect(-60, -30, 120, 60);

strokeRect(x, y, width, height)

可以绘制一个由strokeStylelineWidthlineCaplineJoin等属性指定描边样式的矩形,比如:

context.strokeStyle = "#99cccc";
context.lineWidth = "50";
context.lineJoin = "bevel";
context.strokeRect(0, 0, 400, 200);

属性名含义取值
lineCap线条末端的形状butt平头round圆头square方头
lineWidth线条宽度整数
lineJoin线条相交的方式round圆交bevel斜交mitter斜接
clearRect(x, y, width, height)

可以清楚画布上的指定区域,比如第一个例子中的两个矩形,我们将中间一小块清除:

context.translate(100, 100)
context.fillStyle = "#99cccc";
context.fillRect(-100, -50, 200, 100);
context.fillStyle="#3399cc";
context.clearRect(-60, -30, 120, 60);

绘制路径

使用路径我们可以绘制出比较复杂的图形,开始绘制前,首先执行:

context.beginPath();

结束绘制时,执行:

context.closePath();

以下我们列举了绘制路径的几个方法:

直线

lineTo(x, y),从当前游标至(x, y)画一条直线。

移动游标

moveTo(x, y) 将游标移动至(x, y),移动过程中不画线。比如:

context.beginPath();
context.moveTo(10, 10);
context.lineTo(50, 40);
context.moveTo(50, 50);
context.lineTo(100, 90);
context.stroke();
context.closePath();

弧线或圆形
arc(x, y, radius, startAngle, endAngle, counterclockwise)

从startAngle到endAngle绘制一条以(x, y)为圆心,radius为半径的弧线,其中startAngle和endAngle用弧度表示,couterclockwise为false时,顺时针画弧线, 反之,逆时针画弧线。

var canvas = document.getElementById("canvas");
if(canvas.getContext) {
  var context = canvas.getContext("2d");
  context.beginPath();
  context.arc(400, 400, 50, arcUnit()*30, arcUnit()*180, false);
  context.stroke();
}
function arcUnit() {
  return Math.PI/180;
}

arcTo(startX, startY, endX, endY, radius)

arcTo()方法将利用当前坐标、起点(startX,startY)和终点(endX,endY)这三个点所形成的夹角,绘制一段与夹角的两边相切并且半径为radius的圆上的弧线。弧线的起点就是当前坐标所在边与圆的切点,弧线的终点就是终点(endX,endY)所在边与圆的切点,并且绘制的弧线是两个切点之间长度最短的那个圆弧。此外,如果当前端点不是(startX,startY),arcTo()方法还将添加一条当前端点到(startX,startY)的直线线段。
如果大家还记得高中数学的话,我们应该可以猜到,使用这个方法画弧线大致有三种情况:
** 只有一种半径可以使弧线两端正好在起点和终点
** 如果半径过大,画不出弧线
** 如果半径较小,必定会有一条当前端点到起点的直线
我们举三个例子:
首先定义一些通用的

var context = canvas.getContext("2d");
var currentPoint = {
    x: 0,
    y: 0
};
var startPoint = {
    x: 50, 
    y: 50
};
var endPoint = {
    x: 100,
    y: 0
};

然后绘制参考线

context.moveTo(currentPoint.x, currentPoint.y);
context.lineTo(startPoint.x, startPoint.y);
context.lineTo(endPoint.x, endPoint.y);
context.strokeStyle = "red";
context.stroke();

画第一条弧线

context.moveTo(currentPoint.x, currentPoint.y);
context.arcTo(startPoint.x, startPoint.y, endPoint.x, endPoint.y, 80);
context.strokeStyle = "grey";
context.stroke();

context.arcTo(startPoint.x, startPoint.y, endPoint.x, endPoint.y, 120);

context.arcTo(startPoint.x, startPoint.y, endPoint.x, endPoint.y, 40);

曲线
二次贝塞尔曲线

quadraticCurveTo(cpX, cpY, x, y)
上述方法可以画一条从当前位置到(x, y), 以(cpX, cpY)为控制点的贝塞尔曲线

三次贝塞尔曲线

bezierCurveTo(cpX1, cpY1, cpX2, cpY2, x, y)
上述方法可以画一条从当前位置到(x, y), 以(cpX1, cpY1)和(cpX2, cpY2)为控制点的贝塞尔曲线。

二次贝塞尔与三次贝塞尔的绘制涉及到比较复杂的数学运算,笔者在此就忽略啦...
当然,大部分前端人士可能都跟笔者一样,只希望能画出一条优美的曲线,并不关心实现细节,那么一般认为什么样的曲线才算是优美的曲线呢:

  1. 对称的曲线
  2. 有一个适度的弧度

根据以上规则,我们写一个工具方法:

function drawCurvePath( ctx, start, end, curveness ) {
  var cp = [
    ( start.x + end.x ) / 2 - ( start.y - end.y ) * curveness,
    ( start.y + end.y ) / 2 - ( end.x - start.x ) * curveness
  ];
  ctx.moveTo( start.x, start.y );
  ctx.quadraticCurveTo( 
    cp[ 0 ], cp[ 1 ],
    end.x, end.y
  );
  ctx.stroke();
}

以上参考自用canvas绘制一个曲线动画——深入理解贝塞尔曲线,大家可以前往了解更深入的贝塞尔曲线画法。

矩形

使用rect(x, y, width, height)可以绘制一个左上角坐标为(x, y),宽width,高height的矩形路径。

context.rect(300, 300, 100, 200);
context.stroke();

绘制文本

在绘制本文之前,如果有必要,我们首先应该指定context的几个属性, 比如:

context.font = "bold 14px Arial"; // 格式同css中指定字体样式
context.textAlign = "center"; // start end center left right
context.textBaseline = "middle"; // top hanging middle alphabetic ideographic bottom

fillText(text, x, y, maxWidth)使用fillStyle属性显示文本,strokeText(text, x, y, maxWidth)使用strokeStyle属性为文本描边。
使用measureText(text)方法可以获得文本的宽度。如果我们并不清楚指定的宽度够不够显示当前字体设置下的一段文字,可以使用如下方法:

var fontSize = 50;
var maxWidth = 100;
context.font = "bold " + fontSize+"px Arial";
var text = "Hello World!";
while(context.measureText(text).width > maxWidth) {
    fontSize--;
    context.font = "bold " + fontSize+"px Arial";
}
context.fillText(text, 50, 50, maxWidth);

变换

旋转

rotate(angle)

context.rotate(Math.PI/4)
context.fillText(text, 50, 50, maxWidth);

缩放

scale(scaleX, scaleY)

context.scale(1.2, 1.2);
context.fillText(text, 50, 50, maxWidth);
context.scale(0.5, 0.5);
context.fillText(text, 50, 50, maxWidth);

移动坐标原点

translate(x, y)
假如我们要绘制一个对称图形,移动坐标原点将会大大简化对坐标的计算。

矩阵变换

使用transform(scaleX,skewX,skewY,scaleY,transX,transY)可以进行矩阵变换,其实以上讲的三个方法本质上都在调用矩阵变换,从参数名中可以看出,它们分别表示X轴方向的缩放,Y轴方向的缩放,X轴方向的斜切,Y轴方向的斜切,X轴方向的偏移量,Y轴方向的偏移量。默认值分别为1 0 0 1 0 0。
我们现在使用transform()重新定义一遍之前的三个方法:
rotate

function rotate (ctx, degree) {
var unit = Math.PI/180;
ctx.transform(Math.cos(degree*unit),Math.sin(degree*unit),-Math.sin(degree*unit),Math.cos(degree*unit),0,0)
}

scale

function scale (ctx, scale) {
ctx.transform(scale.x, 0, 0, scale.y, 0, 0);
}

translate

function translate (ctx, translate) {
ctx.transform(1, 0, 0, 1, translate.x, translate.y);
}

我们现在绘制一段文本,先平移(50, 50),然后缩放2倍,最后旋转30度。

context.fillText(text, 50, 50, maxWidth);
translate(context, {x: 50, y: 50});
scale(context, {x: 2, y: 2});
rotate(context, 30);
context.fillText(text, 50, 50, maxWidth);


每次执行transform()都是基于上一次的结果,并不是初始状态,很多时候我们想执行的是基于初始状态旋转、平移或缩放,而非上一次的状态,此时我们可以使用setTransfrom(scaleX,skewX,skewY,scaleY,transX,transY),此方法首先会重置变换矩阵,然后执行transform()
更加详细的内容可以参考html5 Canvas画图教程26:用transform来实现位移,缩放,旋转等

绘制图像

使用drawImage(image, x1, y1, width1, height1, x2, y2, width2, height2)可以将图像的指定部分按照指定的大小绘制到画布指定的位置上。

参数含义
image要绘制的图像,可以是HTMLImageElement,也可以是canvas
x1源图像的x坐标
y1源图像的y坐标
width1源图像的宽度
height1源图像的高度
x2画布的x坐标
y2画布的y坐标
width2图像在画布上显示的宽度
height2图像在画布上显示的高度

绘制阴影

如果要给形状或路径加上阴影,我们要在绘制前设置context对象的以下属性:

属性含义
shadowColor阴影颜色
shadowOffsetXx轴偏移量
shadowOffsetYy轴偏移量
shadowBlur模糊的像素数
context.shadowColor = "grey";
context.shadowOffsetX = "20";
context.shadowBlur = "5";
context.fillText(text, 50, 50, maxWidth);

渐变

线性渐变

使用createLinearGradient(startX, startY, endX, endY)来创建线性渐变,这个方法确认了渐变的起始和方向,然后我们通过addColorStop(position, color)来添加渐变的颜色,position是0到1的数字。一下笔者画一个超级喜欢的条纹渐变:

var grad = context.createLinearGradient(50, 50, 200, 200)
grad.addColorStop(0, "grey");
grad.addColorStop(0.3, "grey");
grad.addColorStop(0.3, "red");
grad.addColorStop(0.5, "red");
grad.addColorStop(0.5, "orange");
grad.addColorStop(0.7, "orange");
context.fillStyle = grad;
context.fillRect(50, 50, 150, 150);

径向渐变

使用createRadialGradient(centerX1, centerY1, radius1, centerX2, centerY2, radius2)创建径向渐变。

图像重复

使用createPattern(image, repeatType)可以绘制重复的图像,用来填充或描边,第一个参数是要重复的HTMLImageElement或canvas或video,第二个参数表示如何重复该图像,可取repeatrepeat-xrepeat-yno-repeat

查看原文

赞 3 收藏 3 评论 0

Sue1024 发布了文章 · 2018-05-07

Javascript 变量、作用域和内存问题

基本类型和引用类型的值

基本类型值:简单的数据段 ,五种基本类型(Number Boolean String Null Undefined)的值都是基本类型值,基本类型的值在内存中大小固定,因此保存在栈内存中。
引用类型值:可能由多个值构成的对象。不能操作引用类型的内存空间。保存在堆内存中。

动态的属性

引用类型值

我们可以为引用类型的值添加、修改、删除属性和方法,比如:

var cat = new Animal();
cat.name = "cat";
cat.speak = function()  {
  alert(this.name);
};
cat.speak(); // cat
基本类型值

然而为基本类型的值添加属性和方法是无效的。

var name = "Sue";
name.age = 18;
alert(name.age); //undefined

复制变量值

基本类型值
var num1 = 5;
var num2 = 5;

num1与num2的内存空间是完全独立的,对一方的改变不会影响到另一方。

引用类型值
var obj1 = new Object();
var obj2 = obj1;
obj2.name = "Sue";
alert(obj1.name); // Sue

当我们将对象obj1复制给obj2时,只是创建了一个指针副本,这个指针副本与obj1指向同一个保存在堆内存中的对象。因此改变一方,另一方也会发生相应的改变。

传递参数

实参与形参
var num = 2
function add(num1, num2) {
  return num1 + num2;
}
add(1, num);

在上述代码中,add(1, num)传入的参数是实参,而arguments[]总是获取由实参串起来的参数值,在函数体中的num1num2是形参,相当于声明了两个局部变量,指向arguments[0]arguments[1]

按值传递

ECMAScript 中所有函数的参数都是按值传递的,把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样(无论是基本类型还是引用类型)。

function changeStuff(num, obj1, obj2)
{
    num = num * 10;
    obj1.item = "changed";
    obj2 = {item: "changed"};
}
var num = 10;
var obj1 = {item: "unchanged"};
var obj2 = {item: "unchanged"};
changeStuff(num, obj1, obj2);
console.log(num);   // 10
console.log(obj1.item);    // changed
console.log(obj2.item);    // unchanged

以上的例子是怎么说明ECMAScript中函数的参数都是按值传递的呢?
首先基本数据类型,全局变量num复制自身给参数num,二者是完全独立的,改动不会相互影响。
关于对象,我们似乎看到了函数内部对obj1对象属性的改动反应到了函数外部,而obj2重新赋值为另一个对象却没有反应到外部,这是为什么呢?书中解释得有点简单,笔者找了一下资料,原来传入对象的时候,其实传入的是对象在内存中的地址,当在函数中改变对象的属性时,是在同一个区域进行操作,所以会在函数外反映出来,然而,如果对这个局部变量重新赋值,内存中的地址改变,就不会对函数外的对象产生影响了,这种思想称为 call by sharing。

 However, since the function has access to the same object as the caller (no copy is made), mutations to those objects, if the objects are mutable, within the function are visible to the caller, which may appear to differ from call by value semantics. Mutations of a mutable object within the function are visible to the caller because the object is not copied or cloned — it is shared. Wikipedia

检测类型

哪种基本数据类型

使用typeof可以辨认StringNumberUndefinedBooleanObject还有函数。

typeof("name"); //string
typeof(18); //number
typeof(undefined); //undefined
typeof(null); //object
typeof(true); //boolean
typeof(new Array()); //object
typeof(Array); //function

正则表达式在某些浏览器中typeof返回结果为object,某些返回function

什么类型的对象

instanceof可以判断是否是给定类型的实例

var a = new Array;
a instanceof Array; //true

使用instanceof测试基本数据类型时,用于返回false

执行环境和作用域

执行环境(execution context)

定义了变量或函数有权访问的其他数据。每个执行环境都有一个与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中。

全局执行环境

全局执行环境是最外围的一个执行环境。在Web 浏览器中,全局执行环境被认为是window 对象,因此所有全局变量和函数都是作为window 对象的属性和方法创建的。某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局执行环境直到应用程序退出,例如关闭网页或浏览器时才会被销毁)。

函数执行环境

当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,将控制器返还给之前的执行环境。

作用域链(scope chain)

当代码在一个环境中执行时,会创建变量对象的一个作用域链,以保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在环境的变量对象,对于全局执行环境,就是window对象,对于函数执行环境,就是该函数的活动对象。作用域链的后续,是该函数对象的[[scope]]属性(全局执行环境没有后续)。

函数对象

在一个函数被定义时,会创建这个函数对象的[[scope]]属性,指向这个函数的外围。

活动对象

在一个函数被调用时,会创建一个活动对象,首先将该函数的形参和实参(arguments)添加进该活动对象,然后添加函数体内声明的变量和函数(提前声明,在刚进入该函数执行环境时,值为undefined),这个活动对象将作为该函数执行环境作用域链的最前端。
关于JS的提前声明机制,我们举个例子证明一下:

function add (num1){
    console.log(num2);
    var num3 = 4;
    return num1 + num2;
}
add(1); //undefined 5

上述代码中,我们在变量声明前使用它,却没有跑出ReferenceError,说明函数执行时,一开始,num2就已经声明了。

内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数。这些环境之间的联系是线性、有次序的。每个环境都可以向上搜索作用域链,以查询变量和函数名;但任何环境都不能通过向下搜索作用域链而进入另一个执行环境。
我们举一个例子,顺便理解一下前面的概念:

var num = 10;
function add (num1, num2) {
  function preAdd(num) {
    var pre = 1;
    return pre  + num;
  num1 = preAdd(num1);
  var result = num1 + num2 + num;
  return result;
}
add(10, 20);

开始执行add()时,首先创建一个执行上下文,然后创建一个活动对象,将argumentsnum1num2prepreAdd()result保存在活动对象中,并将活动对象放在作用域链的前端,执行上下文取得add保存的[[scope]],并将其放入作用域链的后端,然后执行到preAddpreAdd创建一个执行上下文,并压入栈顶,创建一个活动对象,保存argumentsnumpre ,放在作用域链的前端,取得preAdd[[scope]],放入作用域链的后端。当编译器开始解析pre时,首先从preAdd作用域链的前端开始找,找到了立刻停止。当编译器开始解析result = num1 + num2 + num,由于在add的作用域链前端(局部变量)中没有该变量,因此继续在作用域后端中寻找,并最终在全局变量中找到了num

延长作用域链

with

在块作用域内,将指定变量放在作用域链的前端

try-catch

创建一个新的变量对象,其中包含的是被抛出的错误对象的声明,将这个对象放在作用域链的最前端,catch执行结束后,作用域链恢复。

没有块级作用域

ECMAScript中没有块级作用域,因此块的执行环境与其外部的执行环境相同。

声明变量

使用var 声明的变量会自动被添加到最接近的环境中。如果初始化变量时没有使用var 声明,该变量会自动被添加到全局环境(严格模式下,这样写会抛错)。

查询标识符

当对一个变量进行读取或修改操作时,我们首先要搜索到它,搜索的顺序如图:
标识符解析是沿着作用域链一级一级地搜索标识符的过程。搜索过程始终从作用域链的前端开始,然后逐级地向后回溯,直至找到标识符为止(如果找不到标识符,通常会导致错误发生)。

性能

垃圾收集

Javascript具有自动垃圾收集机制,周期性地回收那些不再使用的变量,并释放其占用的内存。

标记清除(mark-and-sweep)

这是Javascript中最常用的垃圾收集方式,当变量进入环境时,将其标记为“进入环境”,离开环境时,标记为“离开环境”。理论上,不可以回收标记为“进入环境”的变量。

可以使用任何方式来标记变量。比如,可以通过翻转某个特殊的位来记录一个变量何时进入环境,或者使用一个“进入环境的”变量列表及一个“离开环境的”变量列表来跟踪哪个变量发生了变化。说到底,如何标记变量其实并不重要,关键在于采取什么策略。

引用计数(reference counting)

不太常见,跟踪记录每个值被引用的次数。
当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1。如果同一个值又被赋给另一个变量,则该值的引用次数加1。相反,如果包含对这个值引用的变量又取得了另外一个值或当它们的生命期结束的时候,要给它们所指向的对象的引用计数减1。当这个值的引用次数变成0 时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。

var a = new Cat(); // 1
var b = a; // 2
var c = b; // 3
b = new Dog(); // 2
c = new Fox(); // 1
a = new Object(); // 0

这样看起来,引用计数法似乎没什么问题,然而,当遇到循环引用时,就跪了。。。

var a = new Object(); //a指向的Object的引用次数+1
var b = new Object(); //b指向的Object的引用次数+1
a.another = b; //b指向的Object的引用次数+1
b.another = a; //a指向的Object的引用次数+1

此时,两个对象的引用次数都为2,用于都不会变为0,永远都不会被GC,浪费内存。
由于引用计数存在上述问题,因此早在Navigator 4.0就放弃了这一策略,但循环引用带来的麻烦却依然存在。
IE 中有一部分对象并不是原生JavaScript 对象。例如,BOM 和DOM 中的对象就是使用C++以COM(Component Object Model,组件对象模型)对象的形式实现的,COM的垃圾回收策略是引用计数法,因此只要涉及到COM对象,就会存在循环引用的问题,举一个例子:

var element = document.getElementById("some_element");
var myObject = new Object();
myObject.element = element;
element.someObject = myObject;

IE9 把BOM 和DOM 对象都转换成了真正的JavaScript 对象。这样,就避免了
两种垃圾收集算法并存导致的问题。

管理内存

由于系统分配给浏览器的内存比较小(比桌面应用小),而内存限制势必会影响网页性能,因此Javascript中,优化内存占用是一个必要的问题,最佳方式就是只保留必要的数据。局部变量会在离开执行环境后自动解除引用,而后被GC,因此我们只需在不再需要某个全局变量时,将其设为null,来解除它对内存的引用(即解除引用dereferencing),适用于大多数全局变量和全局对象的属性。
针对上一节的例子,我们可以使用同样的方法:

myObject.element = null;
element.someObject = null
查看原文

赞 2 收藏 2 评论 0

Sue1024 发布了文章 · 2018-05-07

Javascript JSON

JSON只是一种字符串数据格式,使用它的不仅仅是Javascript。

语法

JSON可以表示三种类型的值:简单值、对象、数组

简单值

以下是JSON可以辨识的简单值例子:

5
"Hello World!"
false
null

*注意:

  1. undefined不被支持
  2. 字符串必须使用双引号,单引号会导致语法错误

对象

在Javascript中,我们可以用对象字面量的方式创建一个对象:

var person = {
    name: "Sue",
    age:  "18"
}

但JSON要求给属性加双引号,上述对象转换成合法的JSON字符串格式:

{
    "name": "Sue",
    "age":  "18"
}

*注意:

  1. 属性加双引号
  2. 没有变量声明
  3. 没有末尾分号
  4. 属性的值可以是前面介绍的简单值,也可以是对象或数组
  5. 2、3点同样适用于简单值和数组

数组

在Javascript中,我们可以创建一个如下数组:

var names = ["Sue", "Jane", "Ben"]

上述数组转换成合法的JSON字符串格式:

["Sue", "Jane", "Ben"]

我们可以将数组、简单值、对象嵌套使用,创建更加复杂的JSON字符串,比如:

{
    "teachers": [
        { "name": "Sue", "age": 18, "students": [1, 45, 60]},
        { "name": "Ben", "age": 25, "students": [2, 31, 40]}
    ],
    "students": [
        { "id": 1, "name": "Jane"},
        { "id": 2, "name": "Lee"}
    ]
}

解析与序列化

在JSON之前,web传输格式化数据都是使用XML,Javascript要想拿到XML中的数据,要先将其转换成DOM,而后从中提取数据,相比之下,JSON就显得非常简单。

eval

早期解析JSON一般使用eval(),比如:

eval({"name": "Sue"})
{name: "Sue"}

但这种方式存在风险,因为服务器返回的数据很有可能存在恶意代码,只要被解析成合法的Javascript语法,就会被执行,比如:

eval("alert('Sue')")

因此不建议使用eval()来解析JSON字符串。
早期浏览器中,可以使用https://github.com/douglascro...

JSON对象

在IE 8+、 Firefox 3.5+、 Safari 4+、 Chrome、Opera 10.5+中,ECMAScript定义了全局对象JSON。

stringify

用于把Javascript对象序列化为JSON字符串。
*注意:

  1. 值为undefined的属性会被忽略
  2. 函数及原型成员会被忽略
  3. 不包含缩进和空格字符(比如逗号后的空格)

举个例子:

function Person(name, secret) {
    this.name = name;
    this.secret = secret;
    this.makeFriends = function() {
        return this.secret;
    }
}
Person.prototype.sayHi = function() {
    return this.name;
}
var me = new Person("Sue", undefined)
me.makeFriends()
// undefined
me.sayHi()
// "Sue"
var meJSON = JSON.stringify(me);
// "{"name":"Sue"}"

上述例子中,我们创建了一个Person实例me,其中secret属性为undefined,包含makeFriends方法和一个原型成员sayHi,这些都没有被添加到生成的JSON字符串中。
这个方法可以使我们不必在乎Javascript语法与JSON语法的差异,尽管创建合法的Javascript对象。

parse

用于把JSON字符串序列化为Javascript值。

var meCopy = JSON.parse(meJSON)
// {name: "Sue"}

*注意:memeCopy是两个独立的对象,实际编程中,可以使用stringifyparse实现对象的拷贝。

stringify 选项

JSON.stringify()的第一个参数是要序列化的对象,后面还可以加两个参数,分别是过滤器和选项。

  1. 过滤器

过滤器可以是数组,也可以是函数,如果是数组,JSON.stringify()的结果中将保留数组中的属性,比如:

function Person(name, age, secret) {
    this.name = name;
    this.age = age;
    this.secret = secret;
}
var me = new Person("Sue", 18, "none");
var meJSON = JSON.stringify(me, ["name", "age"])
meJSON
// "{"name":"Sue","age":18}"

如果过滤器是一个函数,会给这个函数传入两个参数,分别是属性名和属性值,根据需要返回要添加到JSON中的属性值,如果为undefined,会移除该属性,比如:

var me = new Person("sue", 25, ["girl", "1024", "Beauty"])
function process(key, value) {
    switch(key) {
        case "name":
            return value.charAt(0).toUpperCase() + value.slice(1);
        case "age":
            return 18;
        case "secret":
            return undefined;
        default:
            return value;
    }
}
var meJSON = JSON.stringify(me, process)
meJSON
// "{"name":"Sue","age":18}"
  1. 字符串缩进

第三个参数用于控制结果中的缩进和空白符,如果这个参数是一个数值,那么它表示每个级别缩进的空格数,比如:

var meJSON = JSON.stringify(me, process, 4)
undefined
meJSON
"{
    "name": "Sue",
    "age": 18
}"

*注意:

  1. 只要传入了缩进数,结果就会包含换行符,因为只缩进却不换行没什么意义。
  2. 最大缩进为10,如果超过10, 转换为10

如果第三个参数是一个字符串,那么将使用这个字符串作为缩进,这个字符串可以是制表符或其他任意字符,如果超过10位,则仅前10位有效,比如:

var meJSON = JSON.stringify(me, process, "\t")
meJSON
"{
    "name": "Sue",
    "age": 18
}"
var meJSON = JSON.stringify(me, process, "****")
meJSON
"{
****"name": "Sue",
****"age": 18
}"
  1. toJSON

当需要为某种对象添加自定义的序列化方法时,可以给对象定义toJSON()方法,比如:

Person.prototype.toJSON = function() {
    return this.name;
}
var me = new Person("sue", 25, ["girl", "1024", "Beauty"])
JSON.stringify(me, process, "\t")
// ""sue""

上述例子中,我们在Person的原型中添加了toJSON方法,发现调用JSON.stringify时,只序列化了name属性。
序列化的内部顺序如下:
No. 1 如果存在toJSON(),而且它能返回有效的值,则调用它,否则,返回对象本身
No. 2 如果提供了过滤器参数,则基于第一步返回的值调用过滤器
No. 3 序列化第二步的返回值
No. 4 如果提供了缩进,则格式化第三步的返回值

parse 选项

JSON.parse()可以接收第二个参数,这个参数是一个还原函数,将在每个键值对上调用,这个函数接收两个参数,分别是键和值,返回处理过的值,比如:

var birth = new Date(1993, 10, 24)
JSON.stringify(birth)
// ""1993-11-23T16:00:00.000Z""
var birthJSON = JSON.stringify(birth)
var birthCopy = JSON.parse(birthJSON, function(key, value) {
    return new Date(value)
})
birthCopy.getFullYear()
// 1993
查看原文

赞 1 收藏 1 评论 0

认证与成就

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

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2018-05-06
个人主页被 615 人浏览