Quantcast
Channel: IT瘾javascript推荐
Viewing all 148 articles
Browse latest View live

为什么说DOM操作很慢

$
0
0

也可以在这里看: http://leozdgao.me/why-dom-slow/

一直都听说DOM很慢,要尽量少的去操作DOM,于是就想进一步去探究下为什么大家都会这样说,在网上学习了一些资料,这边整理出来。

首先,DOM对象本身也是一个js对象,所以严格来说,并不是操作这个对象慢,而是说操作了这个对象后,会触发一些浏览器行为,比如布局(layout)和绘制(paint)。下面主要先介绍下这些浏览器行为,阐述一个页面是怎么最终被呈现出来的,另外还会从代码的角度,来说明一些不好的实践以及一些优化方案。

浏览器是如何呈现一张页面的

一个浏览器有许多模块,其中负责呈现页面的是渲染引擎模块,比较熟悉的有WebKit和Gecko等,这里也只会涉及这个模块的内容。

先用文字大致阐述下这个过程:

  • 解析HTML,并生成一棵 DOM tree

  • 解析各种样式并结合DOM tree生成一棵 Render tree

  • 对Render tree的各个节点计算布局信息,比如box的位置与尺寸

  • 根据Render tree并利用浏览器的UI层进行绘制

其中DOM tree和Render tree上的节点并非一一对应,比如一个 display:none的节点就在会存在与DOM tree上,而不会出现在Render tree上,因为这个节点不需要被绘制。

上图是Webkit的基本流程,在术语上和Gecko可能会有不同,这里贴上Gecko的流程图,不过文章下面的内容都会统一使用Webkit的术语。

影响页面呈现的因素有许多,比如link的位置会影响首屏呈现等。但这里主要集中讨论与layout相关的内容。

paint是一个耗时的过程,然而layout是一个更耗时的过程,我们无法确定layout一定是自上而下或是自下而上进行的,甚至一次layout会牵涉到整个文档布局的重新计算。

但是 layout是肯定无法避免的,所以我们主要是 要最小化layout的次数。

什么情况下浏览器会进行layout

在考虑如何最小化layout次数之前,要先了解什么时候浏览器会进行layout。

layout(reflow)一般被称为布局,这个操作是用来计算文档中元素的位置和大小,是渲染前重要的一步。在HTML第一次被加载的时候,会有一次layout之外,js脚本的执行和样式的改变同样会导致浏览器执行layout,这也是本文的主要要讨论的内容。

一般情况下,浏览器的layout是lazy的,也就是说:在js脚本执行时,是不会去更新DOM的,任何对DOM的修改都会被暂存在一个队列中,在当前js的执行上下文完成执行后,会根据这个队列中的修改,进行一次layout。

然而有时希望在js代码中立刻获取最新的DOM节点信息,浏览器就不得不提前执行layout,这是导致DOM性能问题的主因。

如下的操作会打破常规,并触发浏览器执行layout:

  • 通过js获取需要计算的DOM属性

  • 添加或删除DOM元素

  • resize浏览器窗口大小

  • 改变字体

  • css伪类的激活,比如:hover

  • 通过js修改DOM元素样式且该样式涉及到尺寸的改变

我们来通过一个例子直观的感受下:

// Read
var h1 = element1.clientHeight;

// Write (invalidates layout)
element1.style.height = (h1 * 2) + 'px';

// Read (triggers layout)
var h2 = element2.clientHeight;

// Write (invalidates layout)
element2.style.height = (h2 * 2) + 'px';

// Read (triggers layout)
var h3 = element3.clientHeight;

// Write (invalidates layout)
element3.style.height = (h3 * 2) + 'px';

这里涉及一个属性 clientHeight,这个属性是需要计算得到的,于是就会触发浏览器的一次layout。我们来利用chrome(v47.0)的开发者工具看下(截图中的timeline record已经经过筛选,仅显示layout):

上面的例子中,代码首先修改了一个元素的样式,接下来读取另一个元素的 clientHeight属性,由于之前的修改导致当前DOM被标记为脏,为了保证能准确的获取这个属性,浏览器会进行一次layout(我们发现chrome的开发者工具良心的提示了我们这个性能问题)。

优化这段代码很简单,预先读取所需要的属性,在一起修改即可。

// Read
var h1 = element1.clientHeight;
var h2 = element2.clientHeight;
var h3 = element3.clientHeight;

// Write (invalidates layout)
element1.style.height = (h1 * 2) + 'px';
element2.style.height = (h2 * 2) + 'px';
element3.style.height = (h3 * 2) + 'px';

看下这次的情况:

下面再介绍一些其他的优化方案。

最小化layout的方案

上面提到的一个批量读写是一个,主要是因为获取一个需要计算的属性值导致的,那么哪些值是需要计算的呢?

这个链接里有介绍大部分需要计算的属性: http://gent.ilcore.com/2011/03/how-not-to-trigger-layout-in-webkit.html

再来看看别的情况:

面对一系列DOM操作

针对一系列DOM操作(DOM元素的增删改),可以有如下方案:

  • documentFragment

  • display: none

  • cloneNode

比如(仅以documentFragment为例):

var fragment = document.createDocumentFragment();
for (var i=0; i < items.length; i++){
  var item = document.createElement("li");
  item.appendChild(document.createTextNode("Option " + i);
  fragment.appendChild(item);
}
list.appendChild(fragment);

这类优化方案的核心思想都是相同的,就是 先对一个不在Render tree上的节点进行一系列操作,再把这个节点添加回Render tree,这样无论多么复杂的DOM操作,最终都只会触发一次layout

面对样式的修改

针对样式的改变,我们首先需要知道 并不是所有样式的修改都会触发layout,因为我们知道layout的工作是计算RenderObject的尺寸和大小信息,那么我如果只是改变一个颜色,是不会触发layout的。

这里有一个网站 CSS triggers,详细列出了各个CSS属性对浏览器执行layout和paint的影响。

像下面这种情况,和上面讲优化的部分是一样的,注意下读写即可。

elem.style.height = "100px"; // mark invalidated
elem.style.width = "100px";
elem.style.marginRight = "10px";

elem.clientHeight // force layout here

但是要提一下动画,这边讲的是js动画,比如:

function animate (from, to) {
  if (from === to) return

  requestAnimationFrame(function () {
    from += 5
    element1.style.height = from + "px"
    animate(from, to)
  })
}

animate(100, 500)

动画的每一帧都会导致layout,这是无法避免的,但是为了减少动画带来的layout的性能损失,可以将动画元素绝对定位,这样动画元素脱离文本流,layout的计算量会减少很多。

使用requestAnimationFrame

任何可能导致重绘的操作都应该放入 requestAnimationFrame

在现实项目中,代码按模块划分,很难像上例那样组织批量读写。那么这时可以把写操作放在 requestAnimationFrame的callback中,统一让写操作在下一次paint之前执行。

// Read
var h1 = element1.clientHeight;

// Write
requestAnimationFrame(function() {
  element1.style.height = (h1 * 2) + 'px';
});

// Read
var h2 = element2.clientHeight;

// Write
requestAnimationFrame(function() {
  element2.style.height = (h2 * 2) + 'px';
});

可以很清楚的观察到Animation Frame触发的时机,MDN上说是在paint之前触发,不过我估计是在js脚本交出控制权给浏览器进行DOM的invalidated check之前执行。

其他注意点

除了由于触发了layout而导致性能问题外,这边再列出一些其他细节:

缓存选择器的结果,减少DOM查询。这里要特别体下HTMLCollection。HTMLCollection是通过 document.getElementByTagName得到的对象类型,和数组类型很类似但是 每次获取这个对象的一个属性,都相当于进行一次DOM查询

var divs = document.getElementsByTagName("div");
for (var i = 0; i < divs.length; i++){  //infinite loop
  document.body.appendChild(document.createElement("div"));
}

比如上面的这段代码会导致无限循环,所以处理HTMLCollection对象的时候要最些缓存。

另外减少DOM元素的嵌套深度并优化css,去除无用的样式对减少layout的计算量有一定帮助。

在DOM查询时, querySelectorquerySelectorAll应该是最后的选择,它们功能最强大,但执行效率很差,如果可以的话,尽量用其他方法替代。

下面两个jsperf的链接,可以对比下性能。

https://jsperf.com/getelementsbyclassname-vs-queryselectorall/162
http://jsperf.com/getelementbyid-vs-queryselector/218

自己对View层的想法

上面的内容理论方面的东西偏多,从实践的角度来看,上面讨论的内容,正好是View层需要处理的事情。已经有一个库FastDOM来做这个事情,不过它的代码是这样的:

fastdom.read(function() {
  console.log('read');
});

fastdom.write(function() {
  console.log('write');
});

问题很明显,会导致 callback hell,并且也可以预见到像FastDOM这样的imperative的代码缺乏扩展性,关键在于用了 requestAnimationFrame后就变成了异步编程的问题了。要让读写状态同步,那必然需要在DOM的基础上写个Wrapper来内部控制异步读写,不过都到了这份上,感觉可以考虑直接上React了......

总之,尽量注意避免上面说到的问题,但如果用库,比如jQuery的话,layout的问题出在库本身的抽象上。像React引入自己的组件模型,用过virtual DOM来减少DOM操作,并可以在每次state改变时仅有一次layout,我不知道内部有没有用 requestAnimationFrame之类的,感觉要做好一个View层就挺有难度的,之后准备学学React的代码。希望自己一两年后会过来再看这个问题的时候,可以有些新的见解。

参考


懒人必备的移动端定宽网页适配方案

$
0
0

本文最初发布于我的个人博客: 咀嚼之味

如今移动设备的分辨率纷繁复杂。以前仅仅是安卓机拥有各种各样的适配问题,如今 iPhone 也拥有了三种主流的分辨率,而未来的 iPhone 7 可能又会玩出什么新花样。如何以不变应万变,用简简单单的几行代码就能支持种类繁多的屏幕分辨率呢?今天就给大家介绍一种懒人必备的移动端定宽网页适配方法。

首先看看下面这行代码:

<meta name="viewport" content="width=device-width, user-scalabel=no">

有过移动端开发经验的同学是不是对上面这句代码非常熟悉?它可能最常见的响应式设计的 viewport设置之一,而我今天介绍的这种方法也是利用了 meta 标签设置 viewport来支持大部分的移动端屏幕分辨率。

目标

  • 仅仅通过配置 <meta name="viewport">使得移动端网站只需要按照固定的宽度设计并实现,就能在任何主流的移动设备上都能看到符合设计稿的页面,包括 Android 4+、iPhone 4+。

测试设备

  • 三星 Note II (Android 4.1.2) - 真机

  • 三星 Note III (Android 4.4.4 - API 19) - Genymotion 虚拟机

  • iPhone 6 (iOS 9.1) - 真机

iPhone

iPhone 的适配比较简单,只需要设置 width即可。比如:

<!-- for iPhone --><meta name="viewport" content="width=320, user-scalable=no" />

这样你的页面在所有的 iPhone 上,无论是 宽 375 像素的 iPhone 6 还是宽 414 像素的 iPhone 6 plus,都能显示出定宽 320 像素的页面。

Android

Android 上的适配被戏称为移动端的 IE,确实存在着很多兼容性问题。Android 以 4.4 版本为一个分水岭,首先说一说相对好处理的 Android 4.4+

Android 4.4+

为了兼容性考虑,Android 4.4 以上抛弃了 target-densitydpi属性,它只会在 Android 设备上生效。如果对这个被废弃的属性感兴趣,可以看看下面这两个链接:

我们可以像在 iPhone 上那样设置 width=320以达到我们想要的 320px 定宽的页面设计。

<!-- for Android 4.4+ --><meta name="viewport" content="width=320, user-scalable=no" />

Android 4.0 ~ 4.3

作为 Android 相对较老的版本,它对 meta 中的 width 属性支持得比较糟糕。以三星 Note II 为例,它的 device-width 是 360px。如果设置 viewport 中的 width (以下简称 vWidth ) 为小于等于 360 的值,则不会有任何作用;而设置 vWidth为大于 360 的值,也不会使画面产生缩放,而是出现了横向滚动条。

想要对 Android 4.0 ~ 4.3 进行支持,还是不得不借助于 页面缩放,以及那个被废除的属性: target-densitydpi

target-densitydpi

target-densitydpi 一共有四种取值:low-dpi (0.75), medium-dpi (1.0), high-dpi (1.5), device-dpi。在 Android 4.0+ 的设备中,device-dpi 一般都是 2.0。我使用手头上的三星 Note II 设备 (Android 4.1.2) 进行了一系列实验,得到了下面这张表格:

target-densitydpiviewport: widthbody width屏幕可视范围宽度
low-dpi (0.75)vWidth <= 320270270
vWidth > 320vWidth*270
medium-dpi (1.0)vWidth <= 360360360
vWidth > 360vWidth*360
high-dpi (1.5)vWidth <= 320540540
320 < vWidth <= 540vWidth*vWidth*
vWidth > 540vWidth*540
device-dpi (2.0)**vWidth <= 320720720
320 < vWidth <= 720vWidth*vWidth*
vWidth > 720vWidth*720
  • vWidth*:指的是与 viewport 中设置的 width 的值相同。

  • device-dpi (2.0)**:在 Android 4.0+ 的设备中,device-dpi 一般都是 2.0。

首先可以看到 320px是个特别诡异的临界值,低于这个临界值后就会发生超出我们预期的事情。综合考虑下来,还是采用 target-densitydpi = device-dpi这一取值。如果你想要以 320px 作为页面的宽度的话,我建议你针对安卓 4.4 以下的版本设置 width=321

如果 body 的宽度超过屏幕可视范围的宽度,就会出现水平的滚动条。这并不是我们期望的结果,所以我们还要用到缩放属性 initial-scale。计算公式如下:

Scale = deviceWidth / vWidth

这样的计算式不得不使用 JS 来实现,最终我们就能得到适配 Android 4.0 ~ 4.3定宽的代码:

var match,
    scale,
    TARGET_WIDTH = 320;

if (match = navigator.userAgent.match(/Android (\d+\.\d+)/)) {
    if (parseFloat(match[1]) < 4.4) {
        if (TARGET_WIDTH == 320) TARGET_WIDTH++;
        var scale = window.screen.width / TARGET_WIDTH;
        document.querySelector('meta[name="viewport"]').setAttribute('content', 'width=' + TARGET_WIDTH + ', initial-scale = ' + scale + ', target-densitydpi=device-dpi');
    }
}

其中, TARGET_WIDTH就是你所期望的宽度,注意这段代码仅在 320-720px之间有效哦。

缩放中的坑

如果是 iPhone 或者 Android 4.4+ 的机器,在使用 scale 相关的属性时要非常谨慎,包括 initial-scale, maximum-scaleminimum-scale
要么保证 Scale = deviceWidth / vWidth,要么就尽量不用。来看一个例子:

Android 4.4+ 和 iPhone 在缩放时的行为不一致

在缩放比不能保证的情况下,即时设置同样的 widthinitial-scale后,两者的表现也是不一致。具体两种机型采用的策略如何我还没有探索出来,有兴趣的同学可以研究看看。最省事的办法就是在 iPhone 和 Android 4.4+ 上不设置 scale 相关的属性。

总结

结合上面所有的分析,你可以通过下面这段 JS 代码来对所有 iPhone 和 Android 4+ 的手机屏幕进行适配:

var match,
    scale,
    TARGET_WIDTH = 320;

if (match = navigator.userAgent.match(/Android (\d+\.\d+)/)) {
    if (parseFloat(match[1]) < 4.4) {
        if (TARGET_WIDTH == 320) TARGET_WIDTH++;
        var scale = window.screen.width / TARGET_WIDTH;
        document.querySelector('meta[name="viewport"]').setAttribute('content', 'width=' + TARGET_WIDTH + ', initial-scale = ' + scale + ', target-densitydpi=device-dpi');
    }
} else {
    document.querySelector('meta[name="viewport"]').setAttribute('content', 'width=' + TARGET_WIDTH);
}

如果你不希望你的页面被用户手动缩放,你还可以加上 user-scalable=no。不过需要注意的是,这个属性在部分安卓机型上是无效的哦。

其他参考资料

  1. Supporting Different Screens in Web Apps - Android Developers

  2. Viewport target-densitydpi support is being deprecated

附录 - 测试页面

有兴趣的同学可以拿这个测试页面来测测自己的手机,别忘了改 viewport哦。

<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=250, initial-scale=1.5, user-scalable=no"><title>Document</title><style>
        body {
            margin: 0;
        }

        div {
            background: #000;
            color: #fff;
            font-size: 30px;
            text-align: center;
        }

        .block {
            height: 50px;
            border-bottom: 4px solid #ccc;
        }

        #first  { width: 100px; }
        #second { width: 200px; }
        #third  { width: 300px; }
        #fourth { width: 320px; }
        #log { font-size: 16px; }
    </style></head><body><div id="first" class="block">100px</div><div id="second" class="block">200px</div><div id="third" class="block">300px</div><div id="fourth" class="block">320px</div><div id="log"></div><script>
        function log(content) {
            var logContainer = document.getElementById('log');
            var p = document.createElement('p');
            p.textContent = content;
            logContainer.appendChild(p);
        }

        log('body width:' + document.body.clientWidth)
        log(document.querySelector('[name="viewport"]').content)</script></body></html>

WebPack常用功能介绍

$
0
0

WebPack常用功能介绍

概述

Webpack是一款用户打包前端模块的工具。主要是用来打包在浏览器端使用的javascript的。同时也能转换、捆绑、打包其他的静态资源,包括css、image、font file、template等。个人认为它的优点就是易用,而且常用功能基本都有,另外可以通过自己开发loader和plugin来满足自己的需求。这里就尽量详细的来介绍下一些基本功能的使用。

安装

npm install webpack 

运行webpack

webpack需要编写一个config文件,然后根据这个文件来执行需要的打包功能。我们现在来编写一个最简单的config。新建一个文件,命名为webpack-config.js。config文件实际上就是一个Commonjs的模块。内容如下:

var webpack = require('webpack');
var path = require('path');
var buildPath = path.resolve(__dirname,"build");
var nodemodulesPath = path.resolve(__dirname,'node_modules');

var config = {
    //入口文件配置
    entry:path.resolve(__dirname,'src/main.js'),
    resolve:{
        extentions:["","js"]//当requrie的模块找不到时,添加这些后缀
    },
    //文件导出的配置
    output:{
        path:buildPath,
        filename:"app.js"
    }
}

module.exports = config;

我的目录结构是这样的:


webpack
    |---index.html
    |---webpack-config.js
    |---src
         |---main.js
         |---js
              |---a.js

main.js文件内容如下:


var a = require('./js/a');
a();
console.log('hello world');
document.getElementById("container").innerHTML = "<p>hello world</p>";

a.js文件内容如下:

module.exports = function(){
    console.log('it is a ');
}

然后我们执行如下的命令:


webpack --config webpack-config.js --colors

这样我们就能在目录里面看到一个新生成的目录build,目录结构如下:


webpack
    |---index.html
    |---webpack-config.js
    |---build
         |---app.js

然后引用app.js就Ok啦。main.js和模块a.js的内容就都打包到app.js中了。这就演示了一个最简单的把模块的js打包到一个文件的过程了。

介绍webpack config文件

webpack是根据config里面描述的内容对一个项目进行打包的。接着我们来解释下config文件中的节点分别代表什么意思。一个config文件,基本都是由以下几个配置项组成的。

  • entry

配置要打包的文件的入口;可以配置多个入口文件,下面会有介绍。

  • resolve

    配置文件后缀名,除了js,还有jsx、coffee等等。除了这个功能还可以配置其他有用的功能,由于我还不完全了解,有知道的朋友欢迎指教。

  • output

    配置输出文件的路径,文件名等。

  • module(loaders)

配置要使用的loader。对文件进行一些相应的处理。比如babel-loader可以把es6的文件转换成es5。
大部分的对文件的处理的功能都是通过loader实现的。loader就相当于gulp里的task。loader可以用来处理在入口文件中require的和其他方式引用进来的文件。loader一般是一个独立的node模块,要单独安装。

loader配置项:

    test: /\.(js|jsx)$/,//注意是正则表达式,不要加引号,匹配要处理的文件
    loader: 'eslint-loader',//要使用的loader,"-loader"可以省略
    include: [path.resolve(__dirname, "src/app")],//把要处理的目录包括进来
    exclude: [nodeModulesPath]//排除不处理的目录

目前已有的loader列表:
https://webpack.github.io/docs/list-of-loaders.html

一个module的例子:

module: {
    preLoaders: [
      {
        test: /\.(js|jsx)$/,
        loader: 'eslint-loader',
        include: [path.resolve(__dirname, "src/app")],
        exclude: [nodeModulesPath]
      },
    ],
    loaders: [
      {
        test: /\.(js|jsx)$/, //正则表达式匹配 .js 和 .jsx 文件
        loader: 'babel-loader?optional=runtime&stage=0',//对匹配的文件进行处理的loader 
        exclude: [nodeModulesPath]//排除node module中的文件
      }
    ]
}
  • plugins

    顾名思义,就是配置要使用的插件。不过plugin和loader有什么差别还有待研究。

来看一个使用plugin的例子:

plugins: [
    //压缩打包的文件
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        //supresses warnings, usually from module minification
        warnings: false
      }
    }),
    //允许错误不打断程序
    new webpack.NoErrorsPlugin(),
    //把指定文件夹xia的文件复制到指定的目录
    new TransferWebpackPlugin([
      {from: 'www'}
    ], path.resolve(__dirname,"src"))
  ]

目前已有的plugins列表:
http://webpack.github.io/docs/list-of-plugins.html

如何压缩输出的文件

plugins: [
    //压缩打包的文件
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        //supresses warnings, usually from module minification
        warnings: false
      }
    })]

如何copy目录下的文件到输出目录

copy文件需要通过插件"transfer-webpack-plugin"来完成。

安装:

npm install transfer-webpack-plugin  -save

配置:

var TransferWebpackPlugin = require('transfer-webpack-plugin');
//其他节点省略    
plugins: [
    //把指定文件夹下的文件复制到指定的目录
    new TransferWebpackPlugin([
      {from: 'www'}
    ], path.resolve(__dirname,"src"))
  ]

打包javascript模块

支持的js模块化方案包括:

  • ES6 模块

    import MyModule from './MyModule.js';

  • CommonJS

    var MyModule = require('./MyModule.js');

  • AMD

    define(['./MyModule.js'], function (MyModule) {
    });

上面已经演示了打包js模块,这里不再重复。ES6的模块需要配置babel-loader来先把处理一下js文件。
下面展示下打包ES模块的配置文件:


var webpack = require('webpack');
var path = require('path');
var buildPath = path.resolve(__dirname, 'build');
var nodeModulesPath = path.resolve(__dirname, 'node_modules');
var TransferWebpackPlugin = require('transfer-webpack-plugin');

var config = {
  entry: [path.join(__dirname, 'src/main.js')],
  resolve: {
    extensions: ["", ".js", ".jsx"]
    //node_modules: ["web_modules", "node_modules"]  (Default Settings)
  },
  output: {
    path: buildPath,    
    filename: 'app.js'  
  },
  plugins: [
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        warnings: false
      }
    }),
    new webpack.NoErrorsPlugin(),
    new TransferWebpackPlugin([
      {from: 'www'}
    ], path.resolve(__dirname,"src"))
  ],
  module: {
    preLoaders: [
      {
        test: /\.(js|jsx)$/,
        loader: 'eslint-loader',
        include: [path.resolve(__dirname, "src/app")],
        exclude: [nodeModulesPath]
      },
    ],
    loaders: [
      {
        test: /\.js$/, //注意是正则表达式,不要加引号
        loader: 'babel-loader?optional=runtime&stage=0',//babel模块相关的功能请自查,这里不做介绍
        exclude: [nodeModulesPath]
      }
    ]
  },
  //Eslint config
  eslint: {
    configFile: '.eslintrc' //Rules for eslint
  },
};

module.exports = config;

打包静态资源

  • css/sass/less

安装css-loader和style-loader


npm install css-loader --save -dev
npm install style-loader --save -dev

config配置:


var config = {
    entry:path.resolve(__dirname,'src/main.js'),
    resolve:{
        extentions:["","js"]
    },
    output:{
        path:buildPath,
        filename:"app.js"
    },
    module:{
        loaders:[{
            test:/\.css$/,
            loader:'style!css',
            exclude:nodemodulesPath
        }]
    }
}

style-loader会把css文件嵌入到html的style标签里,css-loader会把css按字符串导出,这两个基本都是组合使用的。打包完成的文件,引用执行后,会发现css的内容都插入到了head里的一个style标签里。
如果是sass或less配置方式与上面类似。

  • images

可以通过url-loader把较小的图片转换成base64的字符串内嵌在生成的文件里。
安装:


npm install url-loader --save -dev

config配置:

var config = {
    entry:path.resolve(__dirname,'src/main.js'),
    resolve:{
        extentions:["","js"]
    },
    output:{
        path:buildPath,
        filename:"app.js"
    },
    module:{
        loaders:[{
            test:/\.css$/,
            loader:'style!css',//
            exclude:nodemodulesPath
        },
        { test:/\.png$/,loader:'url-loader?limit=10000'}//限制大小小于10k的
        ]
    }
}

css文件内容:


#container{
    color: #f00;
    background:url(images/logo-201305.png);
    /*生成完图片会被处理成base64的字符串 注意:不要写'/images/logo-201305.png',否则图片不被处理*/
}
  • iconfont

内嵌iconfont的使用方法其实和上述处理png图片的方法一致。通过url-loader来处理。

config配置:


var config = {
    entry:path.resolve(__dirname,'src/main.js'),
    resolve:{
        extentions:["","js"]
    },
    output:{
        path:buildPath,
        filename:"app.js"
    },
    module:{
        loaders:[{
            test:/\.css$/,
            loader:'style!css',//
            exclude:nodemodulesPath
        },
        { test:/\.(png|woff|svg|ttf|eot)$/,loader:'url-loader?limit=10000'}//限制大小小于10k的
        ]
    }
}

css文件内容:


@font-face {font-family: 'iconfont';
src: url('fonts/iconfont.eot'); /* IE9*/
src: url('fonts/iconfont.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('fonts/iconfont.woff') format('woff'), /* chrome、firefox */
url('fonts/iconfont.ttf') format('truetype'), /* chrome、firefox、opera、Safari, Android, iOS 4.2+*/
url('fonts/iconfont.svg#iconfont') format('svg'); /* iOS 4.1- */
}

执行打包后会把字体文件都转换成base64字符串内容到文件里.
这里有个头疼的问题,就是每个浏览器支持的字体格式不一样,由于把全部格式的字体打包进去,造成不必要的资源浪费。

打包template

我一大包handlebars的模块为例,来演示下打包模块的过程。有的模板对应的loader,有可能没有现车的,恐怕要自己实现loader。

先安装必须的node模块

npm install handlebars-loader --save -dev
npm install handlebars -save//是必须的

config配置:


var config = {
    entry:path.resolve(__dirname,'src/main.js'),
    resolve:{
        extentions:["","js"]
    },
    output:{
        path:buildPath,
        filename:"app.js"
    },
    module:{
        loaders:[
        { test: /\.html$/, loader: "handlebars-loader" }
        ]
    }
}

新建一个模板文件tb.html,目录结构:

webpack
    |---index.html
    |---webpack-config.js
    |---src
         |---template
         |         |---tb.html
         |---main.js

main.js中调用模块的代码如下:


var template = require("./template/tp.html");
var data={say_hello:"it is handlebars"};
var html = template(data);
document.getElementById('tmpl_container').innerHTML = html;         

公用的模块分开打包

这需要通过插件“CommonsChunkPlugin”来实现。这个插件不需要安装,因为webpack已经把他包含进去了。
接着我们来看配置文件:

var config = {
    entry:{app:path.resolve(__dirname,'src/main.js'),
            vendor: ["./src/js/common"]},//【1】注意这里
    resolve:{
        extentions:["","js"]
    },
    output:{
        path:buildPath,
        filename:"app.js"
    },
    module:{
        loaders:[{
            test:/\.css$/,
            loader:'style!css',
            exclude:nodemodulesPath
        }
        ]
    },
    plugins:[
        new webpack.optimize.UglifyJsPlugin({
             compress: {
                warnings: false
             }
        }),
        //【2】注意这里  这两个地方市用来配置common.js模块单独打包的
        new webpack.optimize.CommonsChunkPlugin({
            name: "vendor",//和上面配置的入口对应
            filename: "vendor.js"//导出的文件的名称
        })
    ]
}

目录结构现在是这样的:


webpack
  |---index.html
  |---webpack-config.js
  |---src
     |---main.js
     |---js
          |---a.js    //a里面require了common
          |---common.js

执行webpack会生成app.js和common.js两个文件.

多个入口

config配置:

var config = {    
    entry:{
        m1:path.resolve(__dirname,'src/main.js'),
         m2:path.resolve(__dirname,'src/main1.js')
    },//注意在这里添加文件的入口
    resolve:{
        extentions:["","js"]
    },
    output:{
        path:buildPath,
        filename:"[name].js"//注意这里使用了name变量
    }    
}

webpack-dev-server

在开发的过程中个,我们肯定不希望,每次修改完都手动执行webpack命令来调试程序。所以我们可以用webpack-dev-server这个模块来取代烦人的执行命令。它会监听文件,在文件修改后,自动编译、刷新浏览器的页面。另外,编译的结果是保存在内存中的,而不是实体的文件,所以是看不到的,因为这样会编译的更快。它就想到与一个轻量的express服务器。
安装:


npm install webpack-dev-server --save -dev

config配置:


var config = {
    entry:path.resolve(__dirname,'src/main.js'),
    resolve:{
        extentions:["","js"]
    },
    //Server Configuration options
    devServer:{
        contentBase: '',  //静态资源的目录 相对路径,相对于当前路径 默认为当前config所在的目录
        devtool: 'eval',
        hot: true,        //自动刷新
        inline: true,    
        port: 3005        
    },
    devtool: 'eval',
    output:{
        path:buildPath,
        filename:"app.js"
    },
    plugins: [
        new webpack.HotModuleReplacementPlugin(),//这个好像也是必须的,虽然我还没搞懂它的作用
        new webpack.NoErrorsPlugin()
    ]
}

我的目录结构:


webpack
  |---index.html
  |---webpack-config.js//我把静态资源目录配置在了这里
  |---src
     |---main.js
     |---js
          |---a.js
          |---common.js

执行命令:


webpack-dev-server --config webpack-dev-config.js  --inline --colors

默认访问地址: http://localhost:3000/index.html(根据配置会不一样)

有一点需要声明,在index.html(引用导出结果的html文件)里直接引用“app.js”,不要加父级目录,因为此时app.js在内存里与output配置的目录无关:

<script type="text/javascript" src="app.js"></script>

详细文档在这里查看:
http://webpack.github.io/docs/webpack-dev-server.html

你有必要知道的 25 个 JavaScript 面试题

$
0
0

你有必要知道的 25 个 JavaScript 面试题

1、使用 typeof bar === "object"判断 bar是不是一个对象有神马潜在的弊端?如何避免这种弊端?

使用 typeof的弊端是显而易见的(这种弊端同使用 instanceof):

let obj = {};
let arr = [];

console.log(typeof obj === 'object');  //true
console.log(typeof arr === 'object');  //true
console.log(typeof null === 'object');  //true

从上面的输出结果可知, typeof bar === "object"并不能准确判断 bar就是一个 Object。可以通过 Object.prototype.toString.call(bar) === "[object Object]"来避免这种弊端:

let obj = {};
let arr = [];

console.log(Object.prototype.toString.call(obj));  //[object Object]
console.log(Object.prototype.toString.call(arr));  //[object Array]
console.log(Object.prototype.toString.call(null));  //[object Null]

另外,为了珍爱生命,请远离 ==

珍爱生命

[] === false是返回 false的。

2、下面的代码会在 console 输出神马?为什么?

(function(){
  var a = b = 3;
})();

console.log("a defined? " + (typeof a !== 'undefined'));   
console.log("b defined? " + (typeof b !== 'undefined'));

这跟变量作用域有关,输出换成下面的:

console.log(b); //3
console,log(typeof a); //undefined

拆解一下自执行函数中的变量赋值:

b = 3;
var a = b;

所以 b成了全局变量,而 a是自执行函数的一个局部变量。

3、下面的代码会在 console 输出神马?为什么?

var myObject = {
    foo: "bar",
    func: function() {
        var self = this;
        console.log("outer func:  this.foo = " + this.foo);
        console.log("outer func:  self.foo = " + self.foo);
        (function() {
            console.log("inner func:  this.foo = " + this.foo);
            console.log("inner func:  self.foo = " + self.foo);
        }());
    }
};
myObject.func();

第一个和第二个的输出不难判断,在 ES6 之前,JavaScript 只有函数作用域,所以 func中的 IIFE 有自己的独立作用域,并且它能访问到外部作用域中的 self,所以第三个输出会报错,因为 this在可访问到的作用域内是 undefined,第四个输出是 bar。如果你知道闭包,也很容易解决的:

(function(test) {
            console.log("inner func:  this.foo = " + test.foo);  //'bar'
            console.log("inner func:  self.foo = " + self.foo);
}(self));

如果对闭包不熟悉,可以戳此: 从作用域链谈闭包

4、将 JavaScript 代码包含在一个函数块中有神马意思呢?为什么要这么做?

换句话说,为什么要用立即执行函数表达式(Immediately-Invoked Function Expression)。

IIFE 有两个比较经典的使用场景,一是类似于在循环中定时输出数据项,二是类似于 JQuery/Node 的插件和模块开发。

for(var i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(i);  
    }, 1000);
}

上面的输出并不是你以为的0,1,2,3,4,而输出的全部是5,这时 IIFE 就能有用了:

for(var i = 0; i < 5; i++) {
    (function(i) {
      setTimeout(function() {
        console.log(i);  
      }, 1000);
    })(i)
}

而在 JQuery/Node 的插件和模块开发中,为避免变量污染,也是一个大大的 IIFE:

(function($) { 
        //代码
 } )(jQuery);

5、在严格模式('use strict')下进行 JavaScript 开发有神马好处?

  • 消除Javascript语法的一些不合理、不严谨之处,减少一些怪异行为;
  • 消除代码运行的一些不安全之处,保证代码运行的安全;
  • 提高编译器效率,增加运行速度;
  • 为未来新版本的Javascript做好铺垫。

6、下面两个函数的返回值是一样的吗?为什么?

function foo1()
{
  return {
      bar: "hello"
  };
}

function foo2()
{
  return
  {
      bar: "hello"
  };
}

在编程语言中,基本都是使用分号(;)将语句分隔开,这可以增加代码的可读性和整洁性。而在JS中,如若语句各占独立一行,通常可以省略语句间的分号(;),JS 解析器会根据能否正常编译来决定是否自动填充分号:

var test = 1 + 
2
console.log(test);  //3

在上述情况下,为了正确解析代码,就不会自动填充分号了,但是对于 returnbreakcontinue等语句,如果后面紧跟换行,解析器一定会自动在后面填充分号(;),所以上面的第二个函数就变成了这样:

function foo2()
{
  return;
  {
      bar: "hello"
  };
}

所以第二个函数是返回 undefined

7、神马是 NaN,它的类型是神马?怎么测试一个值是否等于 NaN?

NaN是 Not a Number 的缩写,JavaScript 的一种特殊数值,其类型是 Number,可以通过 isNaN(param)来判断一个值是否是 NaN

console.log(isNaN(NaN)); //true
console.log(isNaN(23)); //false
console.log(isNaN('ds')); //true
console.log(isNaN('32131sdasd')); //true
console.log(NaN === NaN); //false
console.log(NaN === undefined); //false
console.log(undefined === undefined); //false
console.log(typeof NaN); //number
console.log(Object.prototype.toString.call(NaN)); //[object Number]

ES6 中, isNaN()成为了 Number 的静态方法: Number.isNaN().

8、解释一下下面代码的输出

console.log(0.1 + 0.2);   //0.30000000000000004
console.log(0.1 + 0.2 == 0.3);  //false

JavaScript 中的 number 类型就是浮点型,JavaScript 中的浮点数采用IEEE-754 格式的规定,这是一种二进制表示法,可以精确地表示分数,比如1/2,1/8,1/1024,每个浮点数占64位。但是,二进制浮点数表示法并不能精确的表示类似0.1这样 的简单的数字,会有舍入误差。

由于采用二进制,JavaScript 也不能有限表示 1/10、1/2 等这样的分数。在二进制中,1/10(0.1)被表示为 0.00110011001100110011……注意 0011是无限重复的,这是舍入误差造成的,所以对于 0.1 + 0.2 这样的运算,操作数会先被转成二进制,然后再计算:

0.1 => 0.0001 1001 1001 1001…(无限循环)
0.2 => 0.0011 0011 0011 0011…(无限循环)

双精度浮点数的小数部分最多支持 52 位,所以两者相加之后得到这么一串 0.0100110011001100110011001100110011001100…因浮点数小数位的限制而截断的二进制数字,这时候,再把它转换为十进制,就成了 0.30000000000000004。

对于保证浮点数计算的正确性,有两种常见方式。

一是先升幂再降幂:

function add(num1, num2){
  let r1, r2, m;
  r1 = (''+num1).split('.')[1].length;
  r2 = (''+num2).split('.')[1].length;

  m = Math.pow(10,Math.max(r1,r2));
  return (num1 * m + num2 * m) / m;
}
console.log(add(0.1,0.2));   //0.3
console.log(add(0.15,0.2256)); //0.3756

二是是使用内置的 toPrecision()toFixed()方法, 注意,方法的返回值字符串。

function add(x, y) {
    return x.toPrecision() + y.toPrecision()
}
console.log(add(0.1,0.2));  //"0.10.2"

9、实现函数 isInteger(x)来判断 x 是否是整数

可以将 x转换成10进制,判断和本身是不是相等即可:

function isInteger(x) { 
    return parseInt(x, 10) === x; 
}

ES6 对数值进行了扩展,提供了静态方法 isInteger()来判断参数是否是整数:

Number.isInteger(25) // true
Number.isInteger(25.0) // true
Number.isInteger(25.1) // false
Number.isInteger("15") // false
Number.isInteger(true) // false

JavaScript能够准确表示的整数范围在 -2^532^53之间(不含两个端点),超过这个范围,无法精确表示这个值。ES6 引入了 Number.MAX_SAFE_INTEGERNumber.MIN_SAFE_INTEGER这两个常量,用来表示这个范围的上下限,并提供了 Number.isSafeInteger()来判断整数是否是安全型整数。

10、在下面的代码中,数字 1-4 会以什么顺序输出?为什么会这样输出?

(function() {
    console.log(1); 
    setTimeout(function(){console.log(2)}, 1000); 
    setTimeout(function(){console.log(3)}, 0); 
    console.log(4);
})();

这个就不多解释了,主要是 JavaScript 的定时机制和时间循环,不要忘了,JavaScript 是单线程的。详解可以参考 从setTimeout谈JavaScript运行机制

11、写一个少于 80 字符的函数,判断一个字符串是不是回文字符串

function isPalindrome(str) {
    str = str.replace(/\W/g, '').toLowerCase();
    return (str == str.split('').reverse().join(''));
}

这个题我在 codewars上碰到过,并收录了一些不错的解决方式,可以戳这里: Palindrome For Your Dome

12、写一个按照下面方式调用都能正常工作的 sum 方法

console.log(sum(2,3));   // Outputs 5
console.log(sum(2)(3));  // Outputs 5

针对这个题,可以判断参数个数来实现:

function sum() {
  var fir = arguments[0];
  if(arguments.length === 2) {
    return arguments[0] + arguments[1]
  } else {
    return function(sec) {
       return fir + sec;
    }
  }
}

13、根据下面的代码片段回答后面的问题

for (var i = 0; i < 5; i++) {
  var btn = document.createElement('button');
  btn.appendChild(document.createTextNode('Button ' + i));
  btn.addEventListener('click', function(){ console.log(i); });
  document.body.appendChild(btn);
}

1、点击 Button 4,会在控制台输出什么?

2、给出一种符合预期的实现方式

1、点击5个按钮中的任意一个,都是输出5

2、参考 IIFE。

14、下面的代码会输出什么?为什么?

var arr1 = "john".split(''); j o h n
var arr2 = arr1.reverse(); n h o j
var arr3 = "jones".split(''); j o n e s
arr2.push(arr3);
console.log("array 1: length=" + arr1.length + " last=" + arr1.slice(-1));
console.log("array 2: length=" + arr2.length + " last=" + arr2.slice(-1));

会输出什么呢?你运行下就知道了,可能会在你的意料之外。

MDN 上对于 reverse()的描述是酱紫的:

Description

The reverse method transposes the elements of the calling array object in place, mutating the array, and returning a reference to the array.

reverse()会改变数组本身,并返回原数组的引用。

slice的用法请参考: slice

15、下面的代码会输出什么?为什么?

console.log(1 +  "2" + "2");
console.log(1 +  +"2" + "2");
console.log(1 +  -"1" + "2");
console.log(+"1" +  "1" + "2");
console.log( "A" - "B" + "2");
console.log( "A" - "B" + 2);

输出什么,自己去运行吧,需要注意三个点:

  • 多个数字和数字字符串混合运算时,跟操作数的位置有关
console.log(2 + 1 + '3'); / /‘33’
console.log('3' + 2 + 1); //'321'
  • 数字字符串之前存在数字中的正负号(+/-)时,会被转换成数字
console.log(typeof '3');   // string
console.log(typeof +'3');  //number

同样,可以在数字前添加 '',将数字转为字符串

console.log(typeof 3);   // number
console.log(typeof (''+3));  //string
  • 对于运算结果不能转换成数字的,将返回 NaN
console.log('a' * 'sd');   //NaN
console.log('A' - 'B');  // NaN

这张图是运算转换的规则

运算符转换

16、如果 list 很大,下面的这段递归代码会造成堆栈溢出。如果在不改变递归模式的前提下修善这段代码?

var list = readHugeList();

var nextListItem = function() {
    var item = list.pop();

    if (item) {
        // process the list item...
        nextListItem();
    }
};

原文上的解决方式是加个定时器:

var list = readHugeList();

var nextListItem = function() {
    var item = list.pop();

    if (item) {
        // process the list item...
        setTimeout( nextListItem, 0);
    }
};

解决方式的原理请参考第10题。

17、什么是闭包?举例说明

可以参考此篇: 从作用域链谈闭包

18、下面的代码会输出什么?为啥?

for (var i = 0; i < 5; i++) {
  setTimeout(function() { console.log(i); }, i * 1000 );
}

请往前面翻,参考第4题,解决方式已经在上面了

19、解释下列代码的输出

console.log("0 || 1 = "+(0 || 1));
console.log("1 || 2 = "+(1 || 2));
console.log("0 && 1 = "+(0 && 1));
console.log("1 && 2 = "+(1 && 2));

逻辑与和逻辑或运算符会返回一个值,并且二者都是短路运算符:

  • 逻辑与返回第一个是 false的操作数 或者 最后一个是 true的操作数
console.log(1 && 2 && 0);  //0
console.log(1 && 0 && 1);  //0
console.log(1 && 2 && 3);  //3

如果某个操作数为 false,则该操作数之后的操作数都不会被计算

  • 逻辑或返回第一个是 true的操作数 或者 最后一个是 false的操作数
console.log(1 || 2 || 0); //1
console.log(0 || 2 || 1); //2
console.log(0 || 0 || false); //false

如果某个操作数为 true,则该操作数之后的操作数都不会被计算

如果逻辑与和逻辑或作混合运算,则逻辑与的优先级高:

console.log(1 && 2 || 0); //2
console.log(0 || 2 && 1); //1
console.log(0 && 2 || 1); //1

在 JavaScript,常见的 false值:

0, '0', +0, -0, false, '',null,undefined,null,NaN

要注意 空数组([])空对象({}):

console.log([] == false) //true
console.log({} == false) //false
console.log(Boolean([])) //true
console.log(Boolean({})) //true

所以在 if中, []{}都表现为 true

if

20、解释下面代码的输出

console.log(false == '0')
console.log(false === '0')

请参考前面第14题运算符转换规则的图。

21、解释下面代码的输出

var a={},
    b={key:'b'},
    c={key:'c'};

a[b]=123;
a[c]=456;

console.log(a[b]);

输出是 456,参考原文的解释:

The reason for this is as follows: When setting an object property, JavaScript will implicitly stringify the parameter value. In this case, since b and c are both objects, they will both be converted to [object Object]. As a result, a[b] anda[c] are both equivalent to a[[object Object]] and can be used interchangeably. Therefore, setting or referencing a[c] is precisely the same as setting or referencing a[b].

22、解释下面代码的输出

console.log((function f(n){return ((n > 1) ? n * f(n-1) : n)})(10));

结果是10的阶乘。这是一个递归调用,为了简化,我初始化 n=5,则调用链和返回链如下:

递归

23、解释下面代码的输出

(function(x) {
    return (function(y) {
        console.log(x);
    })(2)
})(1);

输出1,闭包能够访问外部作用域的变量或参数。

24、解释下面代码的输出,并修复存在的问题

var hero = {
    _name: 'John Doe',
    getSecretIdentity: function (){
        return this._name;
    }
};

var stoleSecretIdentity = hero.getSecretIdentity;

console.log(stoleSecretIdentity());
console.log(hero.getSecretIdentity());

getSecretIdentity赋给 stoleSecretIdentity,等价于定义了 stoleSecretIdentity函数:

var stoleSecretIdentity =  function (){
        return this._name;
}

stoleSecretIdentity的上下文是全局环境,所以第一个输出 undefined。若要输出 John Doe,则要通过 callapplybind等方式改变 stoleSecretIdentitythis指向(hero)。

第二个是调用对象的方法,输出 John Doe

25、给你一个 DOM 元素,创建一个能访问该元素所有子元素的函数,并且要将每个子元素传递给指定的回调函数。

函数接受两个参数:

  • DOM
  • 指定的回调函数

原文利用 深度优先搜索(Depth-First-Search) 给了一个实现:

function Traverse(p_element,p_callback) {
   p_callback(p_element);
   var list = p_element.children;
   for (var i = 0; i < list.length; i++) {
       Traverse(list[i],p_callback);  // recursive call
   }
}

文章参考:

25 Essential JavaScript Interview Questions

JavaScript中的立即执行函数表达式

Javascript 严格模式详解

声明:文中对问题的回答仅代表博主个人观点

转载请注明: 淡忘~浅思» 你有必要知道的 25 个 JavaScript 面试题

【译】使用 AngularJS 和 Electron 构建桌面应用

$
0
0

原文: Creating Desktop Applications With AngularJS and GitHub Electron

angular-electron-cover.png

GitHub 的 Electron框架(以前叫做 Atom Shell)允许你使用 HTML, CSS 和 JavaScript 编写跨平台的桌面应用。它是 io.js运行时的衍生,专注于桌面应用而不是 web 服务端。

Electron 丰富的原生 API 使我们能够在页面中直接使用 JavaScript 获取原生的内容。

这个教程向我们展示了如何使用 Angular 和 Electron 构建一个桌面应用。下面是本教程的所有步骤:

  1. 创建一个简单的 Electron 应用

  2. 使用 Visual Studio Code 编辑器管理我们的项目和任务

  3. 使用 Electron 开发(原文为 Integrate)一个 Angular 顾客管理应用(Angular Customer Manager App)

  4. 使用 Gulp 任务构建我们的应用,并生成安装包

创建你的 Electron 应用

起初,如果你的系统中还没有安装 Node,你需要先安装它。我们应用的结构如下所示:

project-structure.png

这个项目中有两个 package.json 文件。

  • 开发使用
    项目根目录下的 package.json 包含你的配置,开发环境的依赖和构建脚本。这些依赖和 package.json 文件不会被打包到生产环境构建中。

  • 应用使用
    app 目录下的 package.json 是你应用的清单文件。因此每当在你需要为你项目安装 npm 依赖的时候,你应该依照这个 package.json 来进行安装。

package.json 的格式和 Node 模块中的完全一致。你应用的启动脚本(的路径)需要在 app/package.json中的 main属性中指定。

app/package.json看起来是这样的:

{
  name: "AngularElectron", 
  version: "0.0.0", 
  main: "main.js" 
}

过执行 npm init命令分别创建这两个 package.json文件,也可以手动创建它们。通过在命令提示行里键入以下命令来安装项目打包必要的 npm 依赖:

npm install --save-dev electron-prebuilt fs-jetpack asar rcedit Q

创建启动脚本

app/main.js是我们应用的入口。它负责创建主窗口和处理系统事件。 main.js应该如下所示:

// app/main.js

// 应用的控制模块
var app = require('app'); 

// 创建原生浏览器窗口的模块
var BrowserWindow = require('browser-window');
var mainWindow = null;

// 当所有窗口都关闭的时候退出应用
app.on('window-all-closed', function () {
  if (process.platform != 'darwin') {
    app.quit();
  }
});

// 当 Electron 结束的时候,这个方法将会生效
// 初始化并准备创建浏览器窗口
app.on('ready', function () {

  // 创建浏览器窗口.
  mainWindow = new BrowserWindow({ width: 800, height: 600 });

  // 载入应用的 index.html
  mainWindow.loadUrl('file://' + __dirname + '/index.html');

  // 打开开发工具
  // mainWindow.openDevTools();
  // 窗口关闭时触发
  mainWindow.on('closed', function () {

    // 想要取消窗口对象的引用,如果你的应用支持多窗口,
    // 通常你需要将所有的窗口对象存储到一个数组中,
    // 在这个时候你应该删除相应的元素
    mainWindow = null;
  });
  
});

通过 DOM 访问原生

正如我上面提到的那样,Electron 使你能够直接在 web 页面中访问本地 npm 模块和原生 API。你可以这样创建 app/index.html文件:

<html><body> <h1>Hello World!</h1>
  We are using Electron <script>  document.write(process.versions['electron']) </script><script> document.write(process.platform) </script><script type="text/javascript"> 
     var fs = require('fs');
     var file = fs.readFileSync('app/package.json'); 
     document.write(file); </script></body> </html>

app/index.html是一个简单的 HTML 页面。在这里,它通过使用 Node’s fs (file system) 模块来读取 package.json文件并将其内容写入到 document body 中。

运行应用

一旦你创建好了项目结构、 app/index.htmlapp/main.jsapp/package.json,你很可能想要尝试去运行初始的 Electron 应用来测试并确保它正常工作。

如果你已经在系统中全局安装了 electron-prebuilt,就可以通过下面的命令启动应用:

electron app

在这里, electron是运行 electron shell 的命令, app是我们应用的目录名。如果你不想将 Election 安装到你全局的 npm 模块中,可以在命令提示行中通过下面命令使用本地 npm_modules文件夹下的 electron 来启动应用。

"node_modules/.bin/electron" "./app" 

尽管你可以这样来运行应用,但是我还是建议你在 gulpfile.js中创建一个 gulp task,这样你就可以将你的任务和 Visual Studio Code 编辑器相结合,我们会在下一部分展示。

// 获取依赖
var gulp        = require('gulp'), 
  childProcess  = require('child_process'), 
  electron      = require('electron-prebuilt');

// 创建 gulp 任务
gulp.task('run', function () { 
  childProcess.spawn(electron, ['./app'], { stdio: 'inherit' }); 
});

运行你的 gulp 任务: gulp run。我们的应用看起来会是这个样子:

electron-app

配置 Visual Studio Code 开发环境

Visual Studio Code 是微软的一款跨平台代码编辑器。VS Code 是基于 Electron 和 微软自身的 Monaco Code Editor 开发的。你可以在 这里下载到 Visual Studio Code。

在 VS Code 中打开你的 electron 应用。

open-application.png

配置 Visual Studio Code Task Runner

有很多自动化的工具,像构建、打包和测试等。我们大多从命令行中运行这些工具。VS Code task runner 使你能够将你自定义的任务集成到项目中。你可以在你的项目中直接运行 grunt,、gulp,、MsBuild 或者其他任务,这并不需要移步到命令行。

VS Code 能够自动检测你的 grunt 和 gulp 任务。按下 ctrl + shift + p然后键入 Run Task敲击回车便可。

run-task.png

你将从 gulpfile.jsgruntfile.js文件中获取所有有效的任务。

注意:你需要确保 gulpfile.js文件存在于你应用的根目录下。

run-task-gulp.png

ctrl + shift + b会从你任务执行器(task runner)中执行 build任务。你可以使用 task.json文件来覆盖任务集成。按下 ctrl + shift + p然后键入 Configure Task敲击回车。这将会在你项目中创建一个 .setting的文件夹和 task.json文件。要是你不止想要执行简单的任务,你需要在 task.json中进行配置。例如你或许想要通过按下 Ctrl + Shift + B来运行应用,你可以这样编辑 task.json文件:

{ "version": "0.1.0", "command": "gulp", "isShellCommand": true, "args": [ "--no-color" ], "tasks": [ 
    { "taskName": "run", "args": [], "isBuildCommand": true 
    } 
  ] 
} 

根部分声明命令为 gulp。你可以在 tasks部分写入你想要的更多任务。将一个任务的 isBuildCommand设置为 true 意味着它和 Ctrl + Shift + B进行了绑定。目前 VS Code 只支持一个顶级任务。

现在,如果你按下 Ctrl + Shift + Bgulp run将会被执行。

你可以在 这里阅读到更多关于 visual studio code 任务的信息。

调试 Electron 应用

打开调试面板点击配置按钮就会在 .settings文件夹内创建一个 launch.json文件,包含了调试的配置。

debug.png

我们不需要启动 app.js 的配置,所以移除它。

现在,你的 launch.json应该如下所示:

{ "version": "0.1.0", 
  // 配置列表。添加新的配置或更改已存在的配置。
  // 仅支持 "node" 和 "mono",可以改变 "type" 来进行切换。
  "configurations": [
    { "name": "Attach", "type": "node", 
      // TCP/IP 地址. 默认是 "localhost""address": "localhost", 
      // 建立连接的端口.
      "port": 5858, "sourceMaps": false 
     } 
   ] 
}

按照下面所示更改之前创建的 gulp run任务,这样我们的 electron 将会采用调试模式运行,5858 端口也会被监听。

gulp.task('run', function () { 
  childProcess.spawn(electron, ['--debug=5858','./app'], { stdio: 'inherit' }); 
}); 

在调试面板中选择 “Attach” 配置项,点击开始(run)或者按下 F5。稍等片刻后你应该就能在上部看到调试命令面板。

debug-star.png

创建 AngularJS 应用

第一次接触 AngularJS?浏览 官方网站或一些 Scotch Angular 教程

这一部分会讲解如何使用 AngularJS 和 MySQL 数据库创建一个顾客管理(Customer Manager)应用。这个应用的目的不是为了强调 AngularJS 的核心概念,而是展示如何在 GiHub 的 Electron 中同时使用 AngularJS 和 NodeJS 以及 MySQL 。

我们的顾客管理应用正如下面这样简单:

  • 顾客列表

  • 添加新顾客

  • 选择删除一个顾客

  • 搜索指定的顾客

项目结构

我们的应用在 app文件夹下,目录结构如下所示:

angular-project-structure.png

主页是 app/index.html文件。 app/scripts文件夹包含所有用在该应用中的关键脚本和视图。有许多方法可以用来组织应用的文件。

这里我更喜欢按照功能来组织脚本文件。每个功能都有它自己的文件夹,文件夹中有模板和控制器。获取更多关于目录结构的信息,可以阅读 AngularJS 最佳实践: 目录结构

在开始 AngularJS 应用之前,我们将使用 bower安装客户端方面的依赖。如果你还没有 Bower先要安装它。在命令提示行中将当前工作目录切换至你应用的根目录,然后依照下面的命令安装依赖。

bower install angular angular-route angular-material --save 

设置数据库

在这个例子中,我将使用一个名字为 customer-manager的数据库和一张名字为 customers的表。下面是数据库的导出文件,你可以依照这个快速开始。


CREATE TABLE `customer_manager`.`customers` ( 
  `customer_id` INT NOT NULL AUTO_INCREMENT, 
  `name` VARCHAR(45) NOT NULL, 
  `address` VARCHAR(450) NULL, 
  `city` VARCHAR(45) NULL, 
  `country` VARCHAR(45) NULL, 
  `phone` VARCHAR(45) NULL, 
  `remarks` VARCHAR(500) NULL, PRIMARY KEY (`customer_id`) 
);

创建一个 Angular Service 和 MySQL 进行交互

一旦你的数据库和表都准备好了,就可以开始创建一个 AngularJS service 来直接从数据库中获取数据。使用 node-mysql这个 npm 模块使 service 连接数据库——一个使用 JavaScript 为 NodeJs 编写的 MySQL 驱动。在你 Angular 应用的 app/目录下安装 node-mysql模块。

注意:我们将 node-mysql 模块安装到 app 目录下而不是应用的根目录,是因为我们需要在最终的 distribution 中包含这个模块。

在命令提示行中切换工作目录至 app文件夹然后按照下面所示安装模块:

npm install --save mysql 

我们的 angular service —— app/scripts/customer/customerService.js如下所示:

(function () {'use strict';
    var mysql = require('mysql');

    // 创建 MySql 数据库连接
    var connection = mysql.createConnection({
        host: "localhost",
        user: "root",
        password: "password",
        database: "customer_manager"
    });
    angular.module('app')
        .service('customerService', ['$q', CustomerService]);

    function CustomerService($q) {
        return {
            getCustomers: getCustomers,
            getById: getCustomerById,
            getByName: getCustomerByName,
            create: createCustomer,
            destroy: deleteCustomer,
            update: updateCustomer
        };

        function getCustomers() {
            var deferred = $q.defer();
            var query = "SELECT * FROM customers";
            connection.query(query, function (err, rows) {
                if (err) deferred.reject(err);
                deferred.resolve(rows);
            });
            return deferred.promise;
        }   

        function getCustomerById(id) {
            var deferred = $q.defer();
            var query = "SELECT * FROM customers WHERE customer_id = ?";
            connection.query(query, [id], function (err, rows) {
                if (err) deferred.reject(err);
                deferred.resolve(rows);
            });
            return deferred.promise;
        }     

        function getCustomerByName(name) {
            var deferred = $q.defer();
            var query = "SELECT * FROM customers WHERE name LIKE  '" + name + "%'";
            connection.query(query, [name], function (err, rows) {
                if (err) deferred.reject(err);
                deferred.resolve(rows);
            });
            return deferred.promise;
        }

        function createCustomer(customer) {
            var deferred = $q.defer();
            var query = "INSERT INTO customers SET ?";
            connection.query(query, customer, function (err, res) 
                if (err) deferred.reject(err);
                deferred.resolve(res.insertId);
            });
            return deferred.promise;
        }

        function deleteCustomer(id) {
            var deferred = $q.defer();
            var query = "DELETE FROM customers WHERE customer_id = ?";
            connection.query(query, [id], function (err, res) {
                if (err) deferred.reject(err);
                deferred.resolve(res.affectedRows);
            });
            return deferred.promise;
        }     

        function updateCustomer(customer) {
            var deferred = $q.defer();
            var query = "UPDATE customers SET name = ? WHERE customer_id = ?";
            connection.query(query, [customer.name, customer.customer_id], function (err, res) {
                if (err) deferred.reject(err);
                deferred.resolve(res);
            });
            return deferred.promise;
        }
    }
})();

customerService是一个简单的自定义 angular service,它提供了对表 customers的基础 CRUD 操作。直接在 service 中使用了 node 模块 mysql。如果你已经拥有了一个远程的数据服务,你也可以使用它来替代之。

控制器 & 模板

app/scripts/customer/customerController中的 customerController如下所示:

(function () {'use strict';
    angular.module('app')
        .controller('customerController', ['customerService', '$q', '$mdDialog', CustomerController]);
    function CustomerController(customerService, $q, $mdDialog) {
        var self = this; 

        self.selected = null;
        self.customers = [];
        self.selectedIndex = 0;
        self.filterText = null;
        self.selectCustomer = selectCustomer;
        self.deleteCustomer = deleteCustomer;
        self.saveCustomer = saveCustomer;
        self.createCustomer = createCustomer;
        self.filter = filterCustomer;   

        // 载入初始数据
        getAllCustomers();

        //----------------------
        // 内部方法
        //----------------------

        function selectCustomer(customer, index) {
            self.selected = angular.isNumber(customer) ? self.customers[customer] : customer;
            self.selectedIndex = angular.isNumber(customer) ? customer: index;
        }
        
        function deleteCustomer($event) {
            var confirm = $mdDialog.confirm()
                                   .title('Are you sure?')
                                   .content('Are you sure want to delete this customer?')
                                   .ok('Yes')
                                   .cancel('No')
                                   .targetEvent($event);

            $mdDialog.show(confirm).then(function () {
                customerService.destroy(self.selected.customer_id).then(function (affectedRows) {
                    self.customers.splice(self.selectedIndex, 1);
                });
            }, function () { });
        }

        function saveCustomer($event) {
            if (self.selected != null && self.selected.customer_id != null) {
                customerService.update(self.selected).then(function (affectedRows) {
                    $mdDialog.show(
                        $mdDialog
                            .alert()
                            .clickOutsideToClose(true)
                            .title('Success')
                            .content('Data Updated Successfully!')
                            .ok('Ok')
                            .targetEvent($event)
                    );
                });
            }
            else {
                //self.selected.customer_id = new Date().getSeconds();
                customerService.create(self.selected).then(function (affectedRows) {
                    $mdDialog.show(
                        $mdDialog
                            .alert()
                            .clickOutsideToClose(true)
                            .title('Success')
                            .content('Data Added Successfully!')
                            .ok('Ok')
                            .targetEvent($event)
                    );
                });
            }
        }    

        function createCustomer() {
            self.selected = {};
            self.selectedIndex = null;
        }      

        function getAllCustomers() {
            customerService.getCustomers().then(function (customers) {
                self.customers = [].concat(customers);
                self.selected = customers[0];
            });
        }
       
        function filterCustomer() {
            if (self.filterText == null || self.filterText == "") {
                getAllCustomers();
            }
            else {
                customerService.getByName(self.filterText).then(function (customers) {
                    self.customers = [].concat(customers);
                    self.selected = customers[0];
                });
            }
        }
    }

})();

我们的顾客模板( app/scripts/customer/customer.html)使用了 angular material 组件来构建 UI,如下所示:

<div style="width:100%" layout="row"><md-sidenav class="site-sidenav md-sidenav-left md-whiteframe-z2"
                md-component-id="left"
                md-is-locked-open="$mdMedia('gt-sm')"><md-toolbar layout="row" class="md-whiteframe-z1"><h1>Customers</h1></md-toolbar><md-input-container style="margin-bottom:0"><label>Customer Name</label><input required name="customerName" ng-model="_ctrl.filterText" ng-change="_ctrl.filter()"></md-input-container><md-list><md-list-item ng-repeat="it in _ctrl.customers"><md-button ng-click="_ctrl.selectCustomer(it, $index)" ng-class="{'selected' : it === _ctrl.selected }">
                    {{it.name}}</md-button></md-list-item></md-list></md-sidenav><div flex layout="column" tabIndex="-1" role="main" class="md-whiteframe-z2"><md-toolbar layout="row" class="md-whiteframe-z1"><md-button class="menu" hide-gt-sm ng-click="ul.toggleList()" aria-label="Show User List"><md-icon md-svg-icon="menu"></md-icon></md-button><h1>{{ _ctrl.selected.name }}</h1></md-toolbar><md-content flex id="content"><div layout="column" style="width:50%"><br /><md-content layout-padding class="autoScroll"><md-input-container><label>Name</label><input ng-model="_ctrl.selected.name" type="text"></md-input-container><md-input-container md-no-float><label>Email</label><input ng-model="_ctrl.selected.email" type="text"></md-input-container><md-input-container><label>Address</label><input ng-model="_ctrl.selected.address"  ng-required="true"></md-input-container><md-input-container md-no-float><label>City</label><input ng-model="_ctrl.selected.city" type="text" ></md-input-container><md-input-container md-no-float><label>Phone</label><input ng-model="_ctrl.selected.phone" type="text"></md-input-container></md-content><section layout="row" layout-sm="column" layout-align="center center" layout-wrap><md-button class="md-raised md-info" ng-click="_ctrl.createCustomer()">Add</md-button><md-button class="md-raised md-primary" ng-click="_ctrl.saveCustomer()">Save</md-button><md-button class="md-raised md-danger" ng-click="_ctrl.cancelEdit()">Cancel</md-button><md-button class="md-raised md-warn" ng-click="_ctrl.deleteCustomer()">Delete</md-button></section></div></md-content></div></div>

app.js 包含模块初始化脚本和应用的路由配置,如下所示:

(function () {'use strict';
    var _templateBase = './scripts';
    angular.module('app', ['ngRoute','ngMaterial','ngAnimate'
    ])
    .config(['$routeProvider', function ($routeProvider) {
            $routeProvider.when('/', {
                templateUrl: _templateBase + '/customer/customer.html' ,
                controller: 'customerController',
                controllerAs: '_ctrl'
            });
            $routeProvider.otherwise({ redirectTo: '/' });
        }
    ]);

})();

最后是我们的首页 app/index.html

<html lang="en" ng-app="app"><title>Customer Manager</title><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"gt;<meta name="description" content=""><meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no" /><!-- build:css assets/css/app.css --><link rel="stylesheet" href="../bower_components/angular-material/angular-material.css" /><link rel="stylesheet" href="assets/css/style.css" /><!-- endbuild --><body><ng-view></ng-view><!-- build:js scripts/vendor.js --><script src="../bower_components/angular/angular.js"></script><script src="../bower_components/angular-route/angular-route.js"></script><script src="../bower_components/angular-animate/angular-animate.js"></script><script src="../bower_components/angular-aria/angular-aria.js"></script><script src="../bower_components/angular-material/angular-material.js"></script><!-- endbuild --><!-- build:app scripts/app.js --><script src="./scripts/app.js"></script><script src="./scripts/customer/customerService.js"></script><script src="./scripts/customer/customerController.js"></script><!-- endbuild --></body></html>

如果你已经如上面那样配置过 VS Code task runner 的话,使用 gulp run命令或者按下 Ctrl + Shif + B来启动你的应用。

angular-app.png

构建 AngularJS 应用

为了构建我们的 Angular 应用,需要安装 gulp-uglify, gulp-minify-cssgulp-usemin依赖包。

npm install --save gulp-uglify gulp-minify-css gulp-usemin

打开你的 gulpfile.js并且引入必要的模块。

  var childProcess = require('child_process'); 
  var electron     = require('electron-prebuilt'); 
  var gulp         = require('gulp'); 
  var jetpack      = require('fs-jetpack'); 
  var usemin       = require('gulp-usemin'); 
  var uglify       = require('gulp-uglify');

  var projectDir = jetpack; 
  var srcDir     = projectDir.cwd('./app'); 
  var destDir    = projectDir.cwd('./build');

如果构建目录已经存在的话,清理一下它。

gulp.task('clean', function (callback) { 
  return destDir.dirAsync('.', { empty: true }); 
});

复制文件到构建目录。我们并不需要使用复制功能来复制 angular 应用的代码,在下一部分中 usemin将会为我们做这件事请:

gulp.task('copy', ['clean'], function () { 
    return projectDir.copyAsync('app', destDir.path(), { 
        overwrite: true, matching: [ './node_modules/**/*', '*.html', '*.css', 'main.js', 'package.json' 
       ] 
    }); 
});

我们的构建任务将使用 gulp.src() 获取 app/index.html 然后传递给 usemin。然后它会将输出写入到构建目录并且把 index.html 中的引用用优化版代码替换掉 。

注意: 千万不要忘记在 app/index.html 像这样定义 usemin 块:

<!-- build:js scripts/vendor.js --><script src="../bower_components/angular/angular.js"></script><script src="../bower_components/angular-route/angular-route.js"></script><script src="../bower_components/angular-animate/angular-animate.js"></script><script src="../bower_components/angular-aria/angular-aria.js"></script><script src="../bower_components/angular-material/angular-material.js"></script><!-- endbuild --><!-- build:app scripts/app.js --><script src="./scripts/app.js"></script><script src="./scripts/customer/customerService.js"></script><script src="./scripts/customer/customerController.js"></script><!-- endbuild -->

构建任务如下所示:

gulp.task('build', ['copy'], function () { 
  return gulp.src('./app/index.html') 
    .pipe(usemin({ 
      js: [uglify()] 
    })) 
    .pipe(gulp.dest('build/')); 
});

为发行(distribution)做准备

在这一部分我们将把 Electron 应用打包至生产环境。在根目录创建构建脚本 build.windows.js。这个脚本用于 Windows 上。对于其他平台来说,你应该创建那个平台特定的脚本并且根据平台来运行。

可以在 node_modules/electron-prebuilt/dist目录中找到一个典型的 electron distribution。这里是构建 electron 应用的步骤:

  • 我们首要的任务是复制 electron distribution 到我们的 dist目录。

  • 每一个 electron distribution 都包含一个默认的应用在 dist/resources/default_app中 。我们需要用我们最终构建的应用来替换它。

  • 为了保护我们的应用源码和资源,你可以选择将你的应用打包成一个 asar 归档,这会改变一点你的源码。一个 asar 归档是一个简单的类似 tar 的格式,它会将你所有的文件拼接成单个文件,Electron 可以在不解压整个文件的情况下从中读取任意文件。

注意:这一部分描述的是 windows 平台下的打包。其他平台中的步骤是一样的,只是路径和使用的文件不一样而已。你可以在 github 中获取 OSx 和 linux 的完整构建脚本。

安装构建 electron 必要的依赖: npm install --save q asar fs-jetpack recedit

接下来,初始化我们的构建脚本,如下所示:

var Q = require('q'); 
var childProcess = require('child_process'); 
var asar = require('asar'); 
var jetpack = require('fs-jetpack');
var projectDir;
var buildDir; 
var manifest; 
var appDir;

function init() { 
    // 项目路径是应用的根目录
    projectDir = jetpack; 
    // 构建目录是最终应用被构建后放置的目录
    buildDir = projectDir.dir('./dist', { empty: true }); 
    // angular 应用目录
    appDir = projectDir.dir('./build'); 
    // angular 应用的 package.json 文件
    manifest = appDir.read('./package.json', 'json'); 
    return Q(); 
} 

这里我们使用 fs-jetpack node 模块进行文件操作。它提供了更灵活的文件操作。

复制 Electron Distribution

electron-prebuilt/dist复制默认的 electron distribution 到我们的 dist 目录

function copyElectron() { 
     return projectDir.copyAsync('./node_modules/electron-prebuilt/dist', buildDir.path(), { overwrite: true }); 
} 

清理默认应用

你可以在 resources/default_app文件夹内找到一个默认的 HTML 应用。我们需要用我们自己的 angular 应用来替换它。按照下面所示移除它:

注意:这里的路径是针对 windows 平台的。对于其他平台过程是一致的,只是路径不一样而已。在 OSX 中路径应该是 Contents/Resources/default_app

function cleanupRuntime() { 
     return buildDir.removeAsync('resources/default_app'); 
}

创建 asar 包

function createAsar() { 
     var deferred = Q.defer(); 
     asar.createPackage(appDir.path(), buildDir.path('resources/app.asar'), function () { 
         deferred.resolve(); 
     }); 
     return deferred.promise; 
}

这将会把你 angular 应用的所有文件打包到一个 asar 包文件里。你可以在 dist/resources/目录中找到 asar 文件。

替换为自己的应用资源

下一步是将默认的 electron icon 替换成你自己的,更新产品的信息然后重命名应用。

function updateResources() {
    var deferred = Q.defer();

    // 将你的 icon 从 resource 文件夹复制到构建文件夹下
    projectDir.copy('resources/windows/icon.ico', buildDir.path('icon.ico'));

    // 将 Electron icon 替换成你自己的
    var rcedit = require('rcedit');
    rcedit(buildDir.path('electron.exe'), {'icon': projectDir.path('resources/windows/icon.ico'),'version-string': {'ProductName': manifest.name,'FileDescription': manifest.description,
        }
    }, function (err) {
        if (!err) {
            deferred.resolve();
        }
    });
    return deferred.promise;
}
// 重命名 electron exe 
function rename() {
    return buildDir.renameAsync('electron.exe', manifest.name + '.exe');
}

创建原生安装包

你可以使用 wix 或 NSIS 创建 windows 安装包。这里我们尽可能使用更小更灵活的 NSIS,它很适合网络应用。使用 NSIS 可以创建支持应用安装时需要的任何事情的安装包。

在 resources/windows/installer.nsis 中创建 NSIS 脚本

!include LogicLib.nsh
    !include nsDialogs.nsh

    ; --------------------------------
    ; Variables
    ; --------------------------------

    !define dest "{{dest}}"
    !define src "{{src}}"
    !define name "{{name}}"
    !define productName "{{productName}}"
    !define version "{{version}}"
    !define icon "{{icon}}"
    !define banner "{{banner}}"

    !define exec "{{productName}}.exe"

    !define regkey "Software\${productName}"
    !define uninstkey "Software\Microsoft\Windows\CurrentVersion\Uninstall\${productName}"

    !define uninstaller "uninstall.exe"

    ; --------------------------------
    ; Installation
    ; --------------------------------

    SetCompressor lzma

    Name "${productName}"
    Icon "${icon}"
    OutFile "${dest}"
    InstallDir "$PROGRAMFILES\${productName}"
    InstallDirRegKey HKLM "${regkey}" ""

    CRCCheck on
    SilentInstall normal

    XPStyle on
    ShowInstDetails nevershow
    AutoCloseWindow false
    WindowIcon off

    Caption "${productName} Setup"
    ; Don't add sub-captions to title bar
    SubCaption 3 " "
    SubCaption 4 " "

    Page custom welcome
    Page instfiles

    Var Image
    Var ImageHandle

    Function .onInit

        ; Extract banner image for welcome page
        InitPluginsDir
        ReserveFile "${banner}"
        File /oname=$PLUGINSDIR\banner.bmp "${banner}"

    FunctionEnd

    ; Custom welcome page
    Function welcome

        nsDialogs::Create 1018

        ${NSD_CreateLabel} 185 1u 210 100% "Welcome to ${productName} version ${version} installer.$\r$\n$\r$\nClick install to begin."

        ${NSD_CreateBitmap} 0 0 170 210 ""
        Pop $Image
        ${NSD_SetImage} $Image $PLUGINSDIR\banner.bmp $ImageHandle

        nsDialogs::Show

        ${NSD_FreeImage} $ImageHandle

    FunctionEnd

    ; Installation declarations
    Section "Install"

        WriteRegStr HKLM "${regkey}" "Install_Dir" "$INSTDIR"
        WriteRegStr HKLM "${uninstkey}" "DisplayName" "${productName}"
        WriteRegStr HKLM "${uninstkey}" "DisplayIcon" '"$INSTDIR\icon.ico"'
        WriteRegStr HKLM "${uninstkey}" "UninstallString" '"$INSTDIR\${uninstaller}"'

        ; Remove all application files copied by previous installation
        RMDir /r "$INSTDIR"

        SetOutPath $INSTDIR

        ; Include all files from /build directory
        File /r "${src}\*"

        ; Create start menu shortcut
        CreateShortCut "$SMPROGRAMS\${productName}.lnk" "$INSTDIR\${exec}" "" "$INSTDIR\icon.ico"

        WriteUninstaller "${uninstaller}"

    SectionEnd

    ; --------------------------------
    ; Uninstaller
    ; --------------------------------

    ShowUninstDetails nevershow

    UninstallCaption "Uninstall ${productName}"
    UninstallText "Don't like ${productName} anymore? Hit uninstall button."
    UninstallIcon "${icon}"

    UninstPage custom un.confirm un.confirmOnLeave
    UninstPage instfiles

    Var RemoveAppDataCheckbox
    Var RemoveAppDataCheckbox_State

    ; Custom uninstall confirm page
    Function un.confirm

        nsDialogs::Create 1018

        ${NSD_CreateLabel} 1u 1u 100% 24u "If you really want to remove ${productName} from your computer press uninstall button."

        ${NSD_CreateCheckbox} 1u 35u 100% 10u "Remove also my ${productName} personal data"
        Pop $RemoveAppDataCheckbox

        nsDialogs::Show

    FunctionEnd

    Function un.confirmOnLeave

        ; Save checkbox state on page leave
        ${NSD_GetState} $RemoveAppDataCheckbox $RemoveAppDataCheckbox_State

    FunctionEnd

    ; Uninstall declarations
    Section "Uninstall"

        DeleteRegKey HKLM "${uninstkey}"
        DeleteRegKey HKLM "${regkey}"

        Delete "$SMPROGRAMS\${productName}.lnk"

        ; Remove whole directory from Program Files
        RMDir /r "$INSTDIR"

        ; Remove also appData directory generated by your app if user checked this option
        ${If} $RemoveAppDataCheckbox_State == ${BST_CHECKED}
            RMDir /r "$LOCALAPPDATA\${name}"
        ${EndIf}

    SectionEnd

build.windows.js文件中创建一个叫做 createInstaller的函数,如下所示:

function createInstaller() {
    var deferred = Q.defer();

    function replace(str, patterns) {
        Object.keys(patterns).forEach(function (pattern) {
            console.log(pattern)
              var matcher = new RegExp('{{' + pattern + '}}', 'g');
            str = str.replace(matcher, patterns[pattern]);
        });
        return str;
    }

    var installScript = projectDir.read('resources/windows/installer.nsi');

    installScript = replace(installScript, {
        name: manifest.name,
        productName: manifest.name,
        version: manifest.version,
        src: buildDir.path(),
        dest: projectDir.path(),
        icon: buildDir.path('icon.ico'),
        setupIcon: buildDir.path('icon.ico'),
        banner: projectDir.path('resources/windows/banner.bmp'),
    });
    buildDir.write('installer.nsi', installScript);

    var nsis = childProcess.spawn('makensis', [buildDir.path('installer.nsi')], {
        stdio: 'inherit'
    });

    nsis.on('error', function (err) {
        if (err.message === 'spawn makensis ENOENT') {
            throw "Can't find NSIS. Are you sure you've installed it and"
            + " added to PATH environment variable?";
        } else {
            throw err;
        }
    });

    nsis.on('close', function () {
        deferred.resolve();
    });

    return deferred.promise;

}

你应该安装了 NSIS,并且确保它在你的路径中是可用的。 creaeInstaller函数会读取安装包脚本并且依照 NSIS 运行时使用 makensis命令来执行。

将他们组合到一起

创建一个函数把所有的片段放在一起,为了使 gulp 任务可以获取到然后输出它:

function build() { 
    return init()
            .then(copyElectron) 
            .then(cleanupRuntime) 
            .then(createAsar) 
            .then(updateResources) 
            .then(rename) 
            .then(createInstaller); 
}
module.exports = { build: build };

接着,在 gulpfile.js中创建 gulp 任务来执行这个构建脚本:

var release_windows = require('./build.windows'); 
var os = require('os'); 
gulp.task('build-electron', ['build'], function () { 
    switch (os.platform()) { 
        case 'darwin': 
        // 执行 build.osx.js 
        break; 
        case 'linux': 
        //执行 build.linux.js 
        break; 
        case 'win32': 
        return release_windows.build(); 
    } 
}); 

运行下面命令,你应该就会得到最终的产品:

gulp build-electron

你最终的 electron 应用应该在 dist目录中,并且目录结构应该和下面是相似的:

总结

Electron 不仅仅是一个支持打包 web 应用成为桌面应用的原生 web view。它现在包含 app 的自动升级、Windows 安装包、崩溃报告、通知和一些其它有用的原生 app 功能——所有的这些都通过 JavaScript API 调用。

到目前为止,很大范围的应用使用 electron 创建,包括聊天应用、数据库管理器、地图设计器、协作设计工具和手机原型等。

下面是 Github Electron 的一些有用的资源:

Hybrid APP架构设计思路

$
0
0

关于Hybrid模式开发app的好处,网络上已有很多文章阐述了,这里不展开。

本文将从以下几个方面阐述Hybrid app架构设计的一些经验和思考。

原文及讨论请到 github issue

通讯

作为一种跨语言开发模式,通讯层是Hybrid架构首先应该考虑和设计的,往后所有的逻辑都是基于通讯层展开。

Native(以Android为例)和H5通讯,基本原理:

  • Android调用H5:通过webview类的 loadUrl方法可以直接执行js代码,类似浏览器地址栏输入一段js一样的效果

    webview.loadUrl("javascript: alert('hello world')");
  • H5调用Android:webview可以拦截H5发起的任意url请求,webview通过约定的规则对拦截到的url进行处理(消费),即可实现H5调用Android

    var ifm = document.createElement('iframe');
    ifm.src = 'jsbridge://namespace.method?[...args]';

JSBridge即我们通常说的桥协议,基本的通讯原理很简单,接下来就是桥协议具体实现。

P.S:注册私有协议的做法很常见,我们经常遇到的在网页里拉起一个系统app就是采用私有协议实现的。app在安装完成之后会注册私有协议到OS,浏览器发现自身不能识别的协议(http、https、file等)时,会将链接抛给OS,OS会寻找可识别此协议的app并用该app处理链接。比如在网页里以 itunes://开头的链接是Apple Store的私有协议,点击后可以启动Apple Store并且跳转到相应的界面。国内软件开发商也经常这么做,比如支付宝的私有协议 alipay://,腾讯的 tencent://等等。

桥协议的具体实现

由于JavaScript语言自身的特殊性(单进程),为了不阻塞主进程并且保证H5调用的有序性,与Native通讯时对于需要获取结果的接口(GET类),采用类似于JSONP的设计理念:

hybrid jsbridge1

类比HTTP的request和response对象,调用方会将调用的api、参数、以及请求签名(由调用方生成)带上传给被调用方,被调用方处理完之后会吧结果以及请求签名回传调用方,调用方再根据请求签名找到本次请求对应的回调函数并执行,至此完成了一次通讯闭环。

H5调用Native(以Android为例)示意图:

hybrid jsbridge2

Native(以Android为例)调用H5示意图:

hybrid jsbridge3

基于桥协议的api设计(HybridApi)

jsbridge作为一种通用私有协议,一般会在团队级或者公司级产品进行共享,所以需要和业务层进行解耦,将jsbridge的内部细节进行封装,对外暴露平台级的API。

以下是笔者剥离公司业务代码后抽象出的一份HybridApi js部分的实现,项目地址:

hybrid-js

另外,对于Native提供的各种接口,也可以简单封装下,使之更贴近前端工程师的使用习惯:

// /lib/jsbridge/core.js
function assignAPI(name, callback) {
    var names = name.split(/\./);
    var ns = names.shift();

    var fnName = names.pop();
    var root = createNamespace(JSBridge[ns], names);

    if(fnName) root[fnName] = callback || function() {};
}

增加api:

// /lib/jsbridge/api.js
var assign = require('./core.js').assignAPI;
...
assign('util.compassImage', function(path, callback, quality, width, height) {
    JSBridge.invokeApp('os.getInfo', {
        path: path,
        quality: quality || 80,
        width: width || 'auto',
        height: height || 'auto',
        callback: callback
    });
});

H5上层应用调用:

// h5/music/index.js
JSBridge.util.compassImage('http://cdn.foo.com/images/bar.png', function(r) {
    console.log(r.value); // => base64 data
});

界面与交互(Native与H5职责划分)

本质上,Native和H5都能完成界面开发。几乎所有hybrid的开发模式都会碰到同样的一个问题:哪些由Native负责哪些由H5负责?

这个回到原始的问题上来:我们为什么要采用hybrid模式开发?简而言之就是同时利用H5的跨平台、快速迭代能力以及Native的流畅性、系统API调用能力。

根据这个原则,为了充分利用二者的优势,应该尽可能地将app内容使用H5来呈现,而对于js语言本身的缺陷,应该使用Native语言来弥补,如转场动画、多线程作业(密集型任务)、IO性能等。即总的原则是H5提供内容,Native提供容器,在有可能的条件下对Android原生webview进行优化和改造(参考阿里Hybrid容器的JSM),提升H5的渲染效率。

但是,在实际的项目中,将整个app所有界面都使用H5来开发也有不妥之处,根据经验,以下情形还是使用Native界面为好:

关键界面、交互性强的的界面使用Native

因H5比较容易被恶意攻击,对于安全性要求比较高的界面,如注册界面、登陆、支付等界面,会采用Native来取代H5开发,保证数据的安全性,这些页面通常UI变更的频率也不高。

对于这些界面,降级的方案也有,就是HTTPS。但是想说的是在国内的若网络环境下,HTTPS的体验实在是不咋地(主要是慢),而且只能走现网不能走离线通道。

另外,H5本身的动画开发成本比较高,在低端机器上可能有些绕不过的性能坎,原生js对于手势的支持也比较弱,因此对于这些类型的界面,可以选择使用Native来实现,这也是Native本身的优势不是。比如要实现下面这个音乐播放界面,用H5开发门槛不小吧,留意下中间的波浪线背景,手指左右滑动可以切换动画。

layout ui1

导航组件采用Native

导航组件,就是页面的头组件,左上角一般都是一个back键,中间一般都是界面的标题,右边的话有时是一个隐藏的悬浮菜单触发按钮有时则什么也没有。

移动端有一个特性就是界面下拉有个回弹效果,头不动body部分跟着滑动,这种效果H5比较难实现。

再者,也是最重要的一点,如果整个界面都是H5的,在H5加载过程中界面将是白屏,在弱网络下用户可能会很疑惑。

所以基于这两点,打开的界面都是Native的导航组件+webview来组成,这样即使H5加载失败或者太慢用户可以选择直接关闭。

在API层面,会相应的有一个接口来实现这一逻辑(例如叫 JSBridge.layout.setHeader),下面代码演示定制一个只有back键和标题的导航组件:

// /h5/pages/index.js
JSBridge.layout.setHeader({
    background: {
        color: '#00FF00',
        opacity: 0.8
    },
    buttons: [
        // 默认只有back键,并且back键的默认点击处理函数就是back()
        {
            icon: '../images/back.png',
            width: 16,
            height: 16,
            onClick: function() {
                // todo...
                JSBridge.back();
            }
        },
        {
            text: '音乐首页',
            color: '#00FF00',
            fontSize: 14,
            left: 10
        }
    ]
});

上面的接口,可以满足绝大多数的需求,但是还有一些特殊的界面,通过H5代码控制生成导航组件这种方式达不到需求:

layout ui2

如上图所示,界面含有tab,且可以左右滑动切换,tab标题的下划线会跟着手势左右滑动。大多见于app的首页(mainActivity)或者分频道首页,这种界面一般采用定制webview的做法:定制的导航组件和内容框架(为了支持左右滑动手势),H5打开此类界面一般也是开特殊的API:

// /h5/pages/index.js
// 开打音乐频道下“我的音乐”tab
JSBridge.view.openMusic({'tab': 'personal'});

这种打开特殊的界面的API之所以特殊,是因为它内部要么是纯Native实现,要么是和某个约定的html文件绑定,调用时打开指定的html。假设这个例子中,tab内容是H5的,如果H5是SPA架构的那么 openMusic({'tab': 'personal'})则对应 /music.html#personal这个url,反之多页面的则可能对应 /mucic-personal.html

至于一般的打开新界面,则有两种可能:

  • app内H5界面

    指的是由app开发者开发的H5页面,也即是app的功能界面,一般互相跳转需要转场动画,打开方式是采用Native提供的接口打开,例如:
    
    JSBridge.view.openUrl({
        url: '/music-list.html',
        title: '音乐列表'
    });
    再配合下面即将提到的离线访问方式,基本可以做到模拟Native界面的效果。
    
  • 第三方H5页面

    指的是app内嵌的第三方页面,一般由`a`标签直接打开,没有转场动画,但是要求打开webview默认的历史列表,以免打开多个链接后点回退直接回到Native主界面。
    

系统级UI组件采用Native

基于以下原因,一些通用的UI组件,如alert、toast等将采用Native来实现:

  • H5本身有这些组件,但是通常比较简陋,不能和APP UI风格统一,需要再定制,比如alert组件背景增加遮罩层

  • H5来实现这些组件有时会存在坐标、尺寸计算误差,比如笔者之前遇到的是页面load异常需要调用对话框组件提示,但是这时候页面高度为0,所以会出现弹窗“消失”的现象

  • 这些组件通常功能单一但是通用,适合做成公用组件整合到HybridApi里边

下面代码演示H5调用Native提供的UI组件:

JSBridge.ui.toast('Hello world!');

默认界面采用Native

由于H5是在H5容器里进行加载和渲染,所以Native很容易对H5页面的行为进行监控,包括进度条、loading动画、404监控、5xx监控、网络诊断等,并且在H5加载异常时提供默认界面供用户操作,防止APP“假死”。

下面是微信的5xx界面示意:

webview monitor

设计H5容器

Native除了负责部分界面开发和公共UI组件设计之外,作为H5的runtime,H5容器是hybrid架构的核心部分,为了让H5运行更快速稳定和健壮,还应当提供并但不局限于下面几方面。

H5离线访问

之所以选择hybrid方式来开发,其中一个原因就是要解决webapp访问慢的问题。即使我们的H5性能优化做的再好服务器在牛逼,碰到蜗牛一样的运营商网络你也没辙,有时候还会碰到流氓运营商再给webapp插点广告。。。哎说多了都是泪。

离线访问,顾名思义就是将H5预先放到用户手机,这样访问时就不会再走网络从而做到看起来和Native APP一样的快了。

但是离线机制绝不是把H5打包解压到手机sd卡这么简单粗暴,应该解决以下几个问题:

  1. H5应该有线上版本

    作为访问离线资源的降级方案,当本地资源不存在的时候应该走现网去拉取对应资源,保证H5可用。另外就是,对于H5,我们不会把所有页面都使用离线访问,例如活动页面,这类快速上线又快速下线的页面,设计离线访问方式开发周期比较高,也有可能是页面完全是动态的,不同的用户在不同的时间看到的页面不一样,没法落地成静态页面,还有一类就是一些说明类的静态页面,更新频率很小的,也没必要做成离线占用手机存储空间。
    
  2. 开发调试&抓包

    我们知道,基于file协议开发是完全基于开发机的,代码必须存放于物理机器,这意味着修改代码需要push到sd卡再看效果,虽然可以通过假链接访问开发机本地server发布时移除的方式,但是个人觉得还是太麻烦易出错。
    

为了实现同一资源的线上和离线访问,Native需要对H5的静态资源请求进行拦截判断,将静态资源“映射”到sd卡资源,即实现一个处理H5资源的本地路由,实现这一逻辑的模块暂且称之为 Local Url Router,具体实现细节在文章后面。

H5离线动态更新机制

将H5资源放置到本地离线访问,最大的挑战就是本地资源的动态更新如何设计,这部分可以说是最复杂的了,因为这同时涉及到H5、Native和服务器三方,覆盖式离线更新示意图如下:

workflow

解释下上图,开发阶段H5代码可以通过手机设置HTTP代理方式直接访问开发机。完成开发之后,将H5代码推送到管理平台进行构建、打包,然后管理平台再通过事先设计好的长连接通道将H5新版本信息推送给客户端,客户端收到更新指令后开始下载新包、对包进行完整性校验、merge回本地对应的包,更新结束。

其中,管理平台推送给客户端的信息主要包括项目名(包名)、版本号、更新策略(增量or全量)、包CDN地址、MD5等。

通常来说,H5资源分为两种,经常更新的业务代码和不经常更新的框架、库代码和公用组件代码,为了实现离线资源的共享,在H5打包时可以采用分包的策略,将公用部分单独打包,在本地也是单独存放,分包及合并示意图:

multi package

Local Url Router

离线资源更新的问题解决了,剩下的就是如何使用离线资源了。

上面已经提到,对于H5的请求,线上和离线采用相同的url访问,这就需要H5容器对H5的资源请求进行拦截“映射”到本地,即 Local Url Router

Local Url Router主要负责H5静态资源请求的分发(线上资源到sd卡资源的映射),但是不管是白名单还是过滤静态文件类型,Native拦截规则和映射规则将变得比较复杂。这里, 阿里去啊app的思路就比较赞,我们借鉴一下,将映射规则交给H5去生成:H5开发完成之后会扫描H5项目然后生成一份线上资源和离线资源路径的映射表(souce-router.json),H5容器只需负责解析这个映射表即可。

H5资源包解压之后在本地的目录结构类似:

$ cd h5 && tree
.
├── js/
├── css/
├── img/
├── pages
│   ├── index.html
│   └── list.html
└── souce-router.json

souce-router.json的数据结构类似:

{"protocol": "http","host": "o2o.xx.com","localRoot": "[/storage/0/data/h5/o2o/]","localFolder": "o2o.xx.com","rules": {"/index.html": "pages/index.html","/js/": "js/"
    }
}

H5容器拦截到静态资源请求时,如果本地有对应的文件则直接读取本地文件返回,否则发起HTTP请求获取线上资源,如果设计完整一点还可以考虑同时开启新线程去下载这个资源到本地,下次就走离线了。

下图演示资源在app内部的访问流程图:

url router

其中proxy指的是开发时手机设置代理http代理到开发机。

数据通道

  • 上报

由于界面由H5和Native共同完成,界面上的用户交互埋点数据最好由H5容器统一采集、上报,还有,由页面跳转产生的浏览轨迹(转化漏斗),也由H5容器记录和上报

  • ajax代理

因ajax受同源策略限制,可以在hybridApi层对ajax进行统一封装,同时兼容H5容器和浏览器runtime,采用更高效的通讯通道加速H5的数据传输

Native对H5的扩展

主要指扩展H5的硬件接口调用能力,比如屏幕旋转、摄像头、麦克风、位置服务等等,将Native的能力通过接口的形式提供给H5。

综述

最后来张图总结下,hybrid客户端整体架构图:

hybrid architecture

其中的 Synchronize Service模块表示和服务器的长连接通信模块,用于接受服务器端各种推送,包括离线包等。 Source Merge Service模块表示对解压后的H5资源进行更新,包括增加文件、以旧换新以及删除过期文件等。

可以看到,hybrid模式的app架构,最核心和最难的部分都是H5容器的设计。

JavaScript 方法的4种调用模式

$
0
0

函数(Function)是JavaScript的基本模块单元,JavaScript的代码重用, 信息隐藏,对象组合等都可以借助函数来实现。 JavaScript中的函数有4种调用模式:

  • 方法调用(Method Invocation Pattern)
  • 函数调用(Function Invocation Pattern)
  • 构造函数调用(Constructor Invocation Pattern)
  • apply调用(Apply Invocation Pattern)

与其他语言不通,JavaScript的函数是一种特殊的对象 (事实上,JavaScript函数是一级对象)。 这意味着函数可以有属性,可以有方法,也可以传参、返回、赋值给变量、加入数组。 与对象不同的是函数可以被调用。

既然是对象便会有原型。我们知道通过花括号语法创建的对象的原型是 Object.prototype。 通过函数语法( function关键字或 new Function())创建的函数,其原型自然是 Function.prototype。 而 Function.prototype的原型是 Object.prototype

注意上述『原型』指的是原型链上的隐式原型,区别于 prototype原型属性。

调用模式

我们知道在函数里可见的名称包括:函数体内声明的变量、函数参数、来自外部的闭包变量。 此外还有两个: thisarguments

this在面向对象程序设计中非常重要,而它的值在JavaScript中取决于 调用模式。 JavaScript中的函数有4种调用模式:方法调用、函数调用、构造函数调用、apply调用。

arguments是一个类数组变量(array like),拥有 length属性并可以取下标, 它存着所有参数构成的有序数组。 在JavaScript中,函数调用与函数签名不一致(个数不正确、类型不正确定) 时不会产生运行时错。少了的参数会被置为 undefined,多了的参数会被忽略。

方法调用模式

在面向对象程序设计中,当函数(Function)作为对象属性时被称为方法(Method)。 方法被调用时 this会被绑定到对应的对象。在JavaScript中有两种语法可以完成方法调用: a.func()a['func']()

var obj = {
    val: 0,
    count: function(i){
        this.val += i;
        console.log(this.val);
    }
};

obj.count();    // 1
obj.count();    // 2

值得注意的是, thisobj的绑定属于极晚绑定(very late binding), 绑定发生在调用的那一刻。这使得JavaScript函数在被重用时有极大的灵活性。

函数调用模式

当函数不是对象属性时,它就会被当做函数来调用,比如 add(2,3)。 此时 this绑定到了全局对象 global

在[那些 JavaScript 的优秀特性][elegant-js]一文中曾提到, JavaScript的编译(预处理)需要 global对象来链接所有全局名称。

其实 this绑定到 global是JavaScript的一个设计错误(可以说是最严重的错误), 它导致了对象方法不能直接调用内部函数来做一些辅助工作, 因为内不函数里的 this的绑定到了 global。 所以如果要重新设计语言,方法调用的 this应该绑定到上一级函数的 this

然而共有方法总是需要调用内部辅助函数,于是产生了这样一个非常普遍的解决方案:

man.love = function(){
    var self = this;
    function fuck(){
        self.happy++;
    }
    function marry(){
        self.happy--;
    }
    fuck() && marry();
}

有些场景下用 Function.prototype.bind会更加方便:

man.love = function(){
    function fuck(girl1, girl2, ...){
        this.happy++;
    }
    fuck.bind(this)();
    ...
}

构造函数调用模式

Classically inspired syntax obscures the language’s true prototypal natur.

JavaScript中,那些用来 new对象的函数成为构造函数。

JavaScript采用原型继承方式。这意味着一个对象可以从另一个对象直接继承属性, JavaScript是class free的~ 但JavaScript为了迎合主流的基于类声明的继承方式, 同时也给出了构造函数机制:使用 new关键字,便会创建一个对象, 根据 prototype属性创建原型链,并以该对象为 this执行指定的(构造)函数。

function Man(name, age){
    this.sex = 'man';
    this.name = name;
    this.age = age;
}
Man.prototype.fuck = function(girl1, girl2, ...){}
var man = new Man();
man.fuck();

当构造函数有很多参数时,它们的顺序很难记住,所以通常使用对象进行传参:

var man = new Man({
    name: 'bob',
    age: 18
});

给参数起名字以达到顺序无关的做法在Python中也存在,但JavaScript的对象传参还将带来另一个好处: JSON兼容。因为JavaScript常常需要数据库(例如MongoDB)或网络( application/json)传来的JSON数据,这一点使得对象构造非常方便。

Apply 调用模式

JavaScript函数是一种特殊的对象,而对象可以有属性和方法。 其中的 apply方法提供了一个更加特殊的调用方式。 它接受两个参数:第一个是该函数要绑定的 this,第二个是参数数组。

var args = [girl1, girl2];
var animal = new Animal();
Man.prototype.fuck.apply(animal, args);

Apply使得一个方法可以用不同的对象对象来调用,比如 animal也可以用 Man的方式来 fuck

[elegant-js]: {% post_url 2016-01-18-elegant-javascript %}

浏览器同源政策及其规避方法

$
0
0

浏览器安全的基石是"同源政策"( same-origin policy)。很多开发者都知道这一点,但了解得不全面。

本文详细介绍"同源政策"的各个方面,以及如何规避它。

一、概述

1.1 含义

1995年,同源政策由 Netscape 公司引入浏览器。目前,所有浏览器都实行这个政策。

最初,它的含义是指,A网页设置的 Cookie,B网页不能打开,除非这两个网页"同源"。所谓"同源"指的是"三个相同"。

  • 协议相同
  • 域名相同
  • 端口相同

举例来说, http://www.example.com/dir/page.html这个网址,协议是 http://,域名是 www.example.com,端口是 80(默认端口可以省略)。它的同源情况如下。

  • http://www.example.com/dir2/other.html:同源
  • http://example.com/dir/other.html:不同源(域名不同)
  • http://v2.www.example.com/dir/other.html:不同源(域名不同)
  • http://www.example.com:81/dir/other.html:不同源(端口不同)

1.2 目的

同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。

设想这样一种情况:A网站是一家银行,用户登录以后,又去浏览其他网站。如果其他网站可以读取A网站的 Cookie,会发生什么?

很显然,如果 Cookie 包含隐私(比如存款总额),这些信息就会泄漏。更可怕的是,Cookie 往往用来保存用户的登录状态,如果用户没有退出登录,其他网站就可以冒充用户,为所欲为。因为浏览器同时还规定,提交表单不受同源政策的限制。

由此可见,"同源政策"是必需的,否则 Cookie 可以共享,互联网就毫无安全可言了。

1.3 限制范围

随着互联网的发展,"同源政策"越来越严格。目前,如果非同源,共有三种行为受到限制。

(1) Cookie、LocalStorage 和 IndexDB 无法读取。

(2) DOM 无法获得。

(3) AJAX 请求不能发送。

虽然这些限制是必要的,但是有时很不方便,合理的用途也受到影响。下面,我将详细介绍,如何规避上面三种限制。

二、Cookie

Cookie 是服务器写入浏览器的一小段信息,只有同源的网页才能共享。但是,两个网页一级域名相同,只是二级域名不同,浏览器允许通过设置 document.domain共享 Cookie。

举例来说,A网页是 http://w1.example.com/a.html,B网页是 http://w2.example.com/b.html,那么只要设置相同的 document.domain,两个网页就可以共享Cookie。


document.domain = 'example.com';

现在,A网页通过脚本设置一个 Cookie。


document.cookie = "test1=hello";

B网页就可以读到这个 Cookie。


var allCookie = document.cookie;

注意,这种方法只适用于 Cookie 和 iframe 窗口,LocalStorage 和 IndexDB 无法通过这种方法,规避同源政策,而要使用下文介绍的PostMessage API。

三、iframe

如果两个网页不同源,就无法拿到对方的DOM。典型的例子是 iframe窗口和 window.open方法打开的窗口,它们与父窗口无法通信。

比如,父窗口运行下面的命令,如果 iframe窗口不是同源,就会报错。


document.getElementById("myIFrame").contentWindow.document
// Uncaught DOMException: Blocked a frame from accessing a cross-origin frame.

上面命令中,父窗口想获取子窗口的DOM,因为跨源导致报错。

反之亦然,子窗口获取主窗口的DOM也会报错。


window.parent.document.body
// 报错

如果两个窗口一级域名相同,只是二级域名不同,那么设置上一节介绍的 document.domain属性,就可以规避同源政策,拿到DOM。

对于完全不同源的网站,目前有三种方法,可以解决跨域窗口的通信问题。

  • 片段识别符(fragment identifier)
  • window.name
  • 跨文档通信API(Cross-document messaging)

3.1 片段识别符

片段标识符(fragment identifier)指的是,URL的 #号后面的部分,比如 http://example.com/x.html#fragment#fragment。如果只是改变片段标识符,页面不会重新刷新。

父窗口可以把信息,写入子窗口的片段标识符。


var src = originURL + '#' + data;
document.getElementById('myIFrame').src = src;

子窗口通过监听 hashchange事件得到通知。


window.onhashchange = checkMessage;

function checkMessage() {
  var message = window.location.hash;
  // ...
}

同样的,子窗口也可以改变父窗口的片段标识符。


parent.location.href= target + "#" + hash;

3.2 window.name

浏览器窗口有 window.name属性。这个属性的最大特点是,无论是否同源,只要在同一个窗口里,前一个网页设置了这个属性,后一个网页可以读取它。

父窗口先打开一个子窗口,载入一个不同源的网页,该网页将信息写入 window.name属性。


window.name = data;

接着,子窗口跳回一个与主窗口同域的网址。


location = 'http://parent.url.com/xxx.html';

然后,主窗口就可以读取子窗口的 window.name了。


var data = document.getElementById('myFrame').contentWindow.name;

这种方法的优点是, window.name容量很大,可以放置非常长的字符串;缺点是必须监听子窗口 window.name属性的变化,影响网页性能。

3.3 window.postMessage

上面两种方法都属于破解,HTML5为了解决这个问题,引入了一个全新的API:跨文档通信 API(Cross-document messaging)。

这个API为 window对象新增了一个 window.postMessage方法,允许跨窗口通信,不论这两个窗口是否同源。

举例来说,父窗口 http://aaa.com向子窗口 http://bbb.com发消息,调用 postMessage方法就可以了。


var popup = window.open('http://aaa.com', 'title');
popup.postMessage('Hello World!', 'http://aaa.com');

postMessage方法的第一个参数是具体的信息内容,第二个参数是接收消息的窗口的源(origin),即"协议 + 域名 + 端口"。也可以设为 *,表示不限制域名,向所有窗口发送。

子窗口向父窗口发送消息的写法类似。


window.opener.postMessage('Nice to see you', 'http://bbb.com');

父窗口和子窗口都可以通过 message事件,监听对方的消息。


window.addEventListener('message', function(e) {
  console.log(e.data);
},false);

message事件的事件对象 event,提供以下三个属性。

  • event.source:发送消息的窗口
  • event.origin: 消息发向的网址
  • event.data: 消息内容

下面的例子是,子窗口通过 event.source属性引用父窗口,然后发送消息。


window.addEventListener('message', receiveMessage);
function receiveMessage(event) {
  event.source.postMessage('Nice to see you!', '*');
}

event.origin属性可以过滤不是发给本窗口的消息。


window.addEventListener('message', receiveMessage);
function receiveMessage(event) {
  if (event.origin !== 'http://bbb.com') return;
  if (event.data === 'Hello World') {
      event.source.postMessage('Hello', event.origin);
  } else {
    console.log(event.data);
  }
}

3.4 LocalStorage

通过 window.postMessage,读写其他窗口的 LocalStorage 也成为了可能。

下面是一个例子,主窗口写入iframe子窗口的 localStorage


window.onmessage = function(e) {
  if (e.origin !== 'http://bbb.com') {
    return;
  }
  var payload = JSON.parse(e.data);
  localStorage.setItem(payload.key, JSON.stringify(payload.data));
};

上面代码中,子窗口将父窗口发来的消息,写入自己的LocalStorage。

父窗口发送消息的代码如下。


var win = document.getElementsByTagName('iframe')[0].contentWindow;
var obj = { name: 'Jack' };
win.postMessage(JSON.stringify({key: 'storage', data: obj}), 'http://bbb.com');

加强版的子窗口接收消息的代码如下。


window.onmessage = function(e) {
  if (e.origin !== 'http://bbb.com') return;
  var payload = JSON.parse(e.data);
  switch (payload.method) {
    case 'set':
      localStorage.setItem(payload.key, JSON.stringify(payload.data));
      break;
    case 'get':
      var parent = window.parent;
      var data = localStorage.getItem(payload.key);
      parent.postMessage(data, 'http://aaa.com');
      break;
    case 'remove':
      localStorage.removeItem(payload.key);
      break;
  }
};

加强版的父窗口发送消息代码如下。


var win = document.getElementsByTagName('iframe')[0].contentWindow;
var obj = { name: 'Jack' };
// 存入对象
win.postMessage(JSON.stringify({key: 'storage', method: 'set', data: obj}), 'http://bbb.com');
// 读取对象
win.postMessage(JSON.stringify({key: 'storage', method: "get"}), "*");
window.onmessage = function(e) {
  if (e.origin != 'http://aaa.com') return;
  // "Jack"
  console.log(JSON.parse(e.data).name);
};

四、AJAX

同源政策规定,AJAX请求只能发给同源的网址,否则就报错。

除了架设服务器代理(浏览器请求同源服务器,再由后者请求外部服务),有三种方法规避这个限制。

  • JSONP
  • WebSocket
  • CORS

4.1 JSONP

JSONP是服务器与客户端跨源通信的常用方法。最大特点就是简单适用,老式浏览器全部支持,服务器不用做任何改造。

它的基本思想是,网页通过添加一个 <script>元素,向服务器请求JSON数据,这种做法不受同源政策限制;服务器收到请求后,将数据放在一个指定名字的回调函数里传回来。

首先,网页动态插入 <script>元素,由它向跨源网址发出请求。


function addScriptTag(src) {
  var script = document.createElement('script');
  script.setAttribute("type","text/javascript");
  script.src = src;
  document.body.appendChild(script);
}

window.onload = function () {
  addScriptTag('http://example.com/ip?callback=foo');
}

function foo(data) {
  console.log('Your public IP address is: ' + data.ip);
};

上面代码通过动态添加 <script>元素,向服务器 example.com发出请求。注意,该请求的查询字符串有一个 callback参数,用来指定回调函数的名字,这对于JSONP是必需的。

服务器收到这个请求以后,会将数据放在回调函数的参数位置返回。


foo({"ip": "8.8.8.8"
});

由于 <script>元素请求的脚本,直接作为代码运行。这时,只要浏览器定义了 foo函数,该函数就会立即调用。作为参数的JSON数据被视为JavaScript对象,而不是字符串,因此避免了使用 JSON.parse的步骤。

8.2 WebSocket

WebSocket是一种通信协议,使用 ws://(非加密)和 wss://(加密)作为协议前缀。该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信。

下面是一个例子,浏览器发出的WebSocket请求的头信息(摘自 维基百科)。


GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

上面代码中,有一个字段是 Origin,表示该请求的请求源(origin),即发自哪个域名。

正是因为有了 Origin这个字段,所以WebSocket才没有实行同源政策。因为服务器可以根据这个字段,判断是否许可本次通信。如果该域名在白名单内,服务器就会做出如下回应。


HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

8.3 CORS

CORS是跨源资源分享(Cross-Origin Resource Sharing)的缩写。它是W3C标准,是跨源AJAX请求的根本解决方法。相比JSONP只能发 GET请求,CORS允许任何类型的请求。

下一篇文章,我会详细介绍,如何通过CORS完成跨源AJAX请求。

(完)

文档信息


兼容所有浏览器的 DOM 载入事件

$
0
0

本文就页面载入问题讨论 DOMContentLoadedloadreadyState等DOM事件的浏览器兼容性, 并给出怎样绑定DOM载入事件以兼容所有的浏览器。 接着介绍jQuery对该问题的实现源码,以及jQuery中 $(document).ready()$(window).load()方法的区别。

在讨论页面载入事件之前,首先需要区分的两个概念:DOM就绪和渲染结束。

  • DOM就绪是指浏览器已经接收到整个HTML并且DOM解析完成;
  • 渲染结束是指浏览器已经接收到HTML中引用的所有样式文件、图片文件、以及Iframe等资源并渲染结束。

DOM API 提供的事件

DOM API 在页面载入问题上主要提供了三个接口:

  • DOMContentLoaded事件;
  • load事件;
  • document.readyState属性,及其对应的 readystatechange事件。

我们看看这三者有什么区别:

DOMContentLoaded

document.addEventListener("DOMContentLoaded", function(event) {
    console.log("DOM ready!");
});

页面文档(DOM)完全加载并解析完毕之后,会触发 DOMContentLoaded事件, HTML文档不会等待样式文件,图片文件,Iframe页面的加载。 此时DOM元素可能还未渲染结束,位置大小等状态可能不正确, 但DOM树已被创建,多数JavaScript已经操作DOM并完成功能了。 所以绝大多数场景下都应当使用 DOMContentLoaded事件, jQuery也采用了这种实现。

This (DOMContentLoaded) event fires after the HTML code has been fully retrieved from the server, the complete DOM tree has been created and scripts have access to all elements via the DOM API. – molily.de

其实样式文件的加载会阻塞后续脚本执行, 此时多数浏览器都会推迟 DOMContentLoaded事件的触发, 在 样式表的载入会延迟DOM载入事件一文中详细地讨论了这一点。

考虑到IE8及以下不支持该事件,因此我们需要后面的两个 DOM 事件作为Fallback。

load

document.addEventListener("load", function(event) {
    console.log("All resources finished loading!");
});

页面完全载入时触发 load事件,此时所有的图片等资源文件都已完全接收并完成渲染。 因此 load总是在 DOMContentLoaded之后触发。 load事件没有任何兼容性问题。 load常常被作为最终的Fallback。

注意IE8及以下不支持 addEventListener,需要使用 attachEvent来绑定事件处理函数。 详见: DOM 事件与 jQuery 源码:捕获与冒泡一文。

document.readyState

document.readyState属性用来表征DOM的加载状态, 该属性值发生变化时会触发 redystatechange事件。 document.readyState属性有三种取值:

  • "loading":DOM在加载过程中;
  • "interactive":DOM就绪但资源仍在加载中;
  • "complete":DOM加载完成。

由于IE8支持 document.readyState属性,也常常用来在IE8中作为 DOMContentLoaded的Fallback。

注意IE8以前的IE不支持 document.readyState属性。 可以执行 document.documentElement.doScroll("left"), 当DOM未就绪时执行该方法会抛出错误,以此检测DOM是否就绪。

jQuery 方法

jQuery提供了三种方法来提供页面载入事件:

  1. $(document).ready(callback):在DOM就绪时执行回调,返回值为 document构成的jQuery集合。
  2. $(function(){}):这是最常用的写法,参数与返回值同上。
  3. $(window).load():DOM就绪,并且页面渲染结束(图片等资源已接收完成)时执行回调。

更多jQuery函数 $()的用法请参考 jQuery中$()函数有几种用法一文,本文不再赘述。

上述三个方法在事实上相当于只有两个: .ready().load()

.ready()方法的实现在这里: https://github.com/jquery/jquery/blob/master/src/core/ready.js

if ( document.readyState === "complete" ||
    ( document.readyState !== "loading" && !document.documentElement.doScroll ) ) {
    // Handle it asynchronously to allow scripts the opportunity to delay ready
    window.setTimeout( jQuery.ready );
} else {
    // Use the handy event callback
    document.addEventListener( "DOMContentLoaded", completed );
    // A fallback to window.onload, that will always work
    window.addEventListener( "load", completed );
}

.load()就是DOM load的包装,不再赘述。 DOM 事件与 jQuery 源码:捕获与冒泡一文详述了jQuery如何包装DOM事件。

参考阅读

样式表的载入会延迟DOM载入事件

$
0
0

绝大多数情况下我们总是让JavaScript在DOM载入后再开始执行。 不管是直接用 DOM API 实现还是使用 jQuery,最终都是 DOMContentLoaded事件在起作用。 本文讨论一个我们习以为常却很少了解的问题: 样式文件的载入会延迟脚本执行,以及 DOMContentLoaded事件的触发。

DOMContentLoaded事件

页面文档(DOM)完全加载并解析完毕之后,会触发 DOMContentLoaded事件, HTML文档不会等待样式文件,图片文件,Iframe页面的加载。 但DOM树已被创建,多数JavaScript已经操作DOM并完成功能了。

This (DOMContentLoaded) event fires after the HTML code has been fully retrieved from the server, the complete DOM tree has been created and scripts have access to all elements via the DOM API. – molily.de

然而在绝大多数场景下,样式文件的载入会延迟 DOMContentLoaded事件的触发。 其实这样的行为正是开发者所希望的,为什么呢?

事实上,老版本的IE中 DOMContentLoaded事件存在兼容性问题。 参见: 兼容所有浏览器的 DOM 载入事件

浏览器为何延迟DOMContentLoaded

对于很多脚本而言,它们被编写时就希望在样式载入之后再开始执行。 JavaScript的作者往往会假设CSS规则已经生效,尤其是在进行一些显示相关的操作时, 比如需要得到DOM元素的位置和大小的场景。

事实上,在多数浏览器中 DOMContentLoaded事件的触发会考虑到外部样式文件的载入, 以及在HTML中脚本标签和样式标签的相对位置。 如果脚本位于样式之后,浏览器通常会认为该脚本依赖于样式的渲染结果, 也就更倾向于延迟脚本的执行(直到样式渲染结束)。

不同浏览器的行为

既然浏览器有时会延迟 DOMContentLoaded事件, 但是何时会延迟 DOMContentLoaded事件,还取决于行内脚本还是外部脚本,以及脚本与样式标签的相对位置。 不同的浏览器渲染引擎也有不同的行为。 在 http://molily.de/domcontentloaded/一文中对该问题有详尽的阐述和测试, 本文取其结论。

下表描述了各种情况下脚本是否会被延迟执行,进而延迟触发 DOMContentLoaded事件。

渲染引擎样式表之前的脚本样式表之后的外部脚本样式表之后的行内脚本
Presto (Opera)
Webkit (Safari, Chrome)
Gecko (Firefox)
Trident (MSIE) 

HTML5标准及最佳实践

其实 DOMContentLoaded是Firefox中最先提出的, 此后JavaScript社区发现它确实比 load事件(要求所有资源完全载入)更好, 于是Apple和Opera相继开始支持该事件。 但不同浏览器的实现方式有所区别,于是产生了上表所示的复杂情况。

在HTML5标准中情况有所好转: DOMContentLoaded是一个纯DOM事件,与样式表无关。 与此同时,HTML5要求:

  • 脚本执行前,出现在当前 <script>之前的 <link rel="stylesheet">必须完全载入。
  • 脚本执行会阻塞DOM解析。

这样的话,假如脚本和样式一起放在HTML <head>中, DOM解析到 <script>标签时会阻塞DOM解析,开始如下操作:

  1. 获取当前 <script>的脚本文件;
  2. 获取并载入前面的所有 <link rel="stylesheet">
  3. 执行当前脚本文件。

这些操作完成之后才能继续进行DOM解析,解析完毕时再触发 DOMContentLoaded事件。 如果将样式和脚本都放到 <head>中,会使浏览器在渲染 <body>前载入并执行所有样式和脚本。 页面的显示延迟会很高,多数情况下用户体验都很糟糕。 因此在HTML5标准的HTML页面中,最佳实践应当是: 所有样式放在 <head>中;所有脚本放在 <body>最后。

jQuery文档中也推荐这样的实践方式。

参考阅读

异步脚本载入提高页面性能

$
0
0

可能很多人都知道JavaScript的载入和渲染会暂停DOM解析,但可能仍缺乏直观的体验。 本文通过几个例子详述脚本对页面渲染的影响,以及如何使用异步脚本载入策略提供页面性能和用户体验。 包括在脚本载入缓慢或错误时尽早显示整个页面内容,以及早点结束浏览器忙提示(进度条、旋转图标、状态栏等)。

DOM 渲染流程

要理解异步脚本载入的用处首先要了解浏览器渲染DOM的流程,以及各阶段用户体验的差别。 一般地,一个包含外部样式表文件和外部脚本文件的HTML载入和渲染过程是这样的:

  1. 浏览器下载HTML文件并开始解析DOM。
  2. 遇到样式表文件 link[rel=stylesheet]时,将其加入资源文件下载队列,继续解析DOM。
  3. 遇到脚本文件时,暂停DOM解析并立即下载脚本文件。
  4. 下载结束后立即执行脚本,在脚本中可访问当前 <script>以上的DOM。
  5. 脚本执行结束,继续解析DOM。
  6. 整个DOM解析完成,触发 DOMContentLoaded事件。

上述步骤只是大致的描述,你可能还会关心下面两个问题:

  • 资源文件下载队列。样式表、图片等资源文件的下载不会暂停DOM解析。浏览器会并行地下载这些文件,但通常会限制并发下载数,一般为3-5个。可以在开发者工具的Network标签页中看到。
  • 执行脚本文件前,浏览器可能会等待该 <script>之前的样式下载完成并渲染结束。详见 外部样式表与DOMContentLoaded事件延迟一文。

脚本载入暂停DOM渲染

脚本载入真的会暂停DOM渲染吗?非常真切。 比如下面的HTML中,在脚本后面还有一个 <h1>标签。

<!DOCTYPE html><html><body><h1>Hello</h1><script src="/will-not-stop-loading.js"></script> <h1>World!</h1></body></html>

我们编写服务器端代码(见本文最后一章),让 /will-not-stop-loading.js始终处于等待状态。 此时页面的显示效果:

js block dom render

脚本等待下载完成的过程中,后面的 World不会显示出来。直到该脚本载入完成或超时。 试想如果你在 <head>中有这样一个下载缓慢的脚本,整个 <body>都不会显示, 势必会造成空白页面持续相当长的时间。 所以 较好的实践方式是将脚本放在 <body>尾部。

很多被墙的网站加载及其缓慢就是因为DOM主体前有脚本被挡在墙外了。

DOMContentLoaded 延迟

既然脚本载入会暂停DOM渲染,OK我们把脚本都放在 <body>尾部。 这时页面可以被显示出来了, 但是在脚本载入前, DOMContentLoaded事件仍然不会触发。 请看:

<!DOCTYPE html><html><body><h1>Hello</h1><h1>World!</h1><script>
    document.addEventListener('DOMContentLoaded', function(){
      alert('DOM loaded!');
    });</script><script src="/will-not-stop-loading.js"></script> </body></html>

这时 Wrold!会显示,但浏览器忙指示器仍在旋转。 这是因为 DOM 仍然没有解析完成,毕竟最后一个 <script>标签还未获取到嘛! 当然 DOMContentLoaded事件也就不会触发。 DOM loaded!对话框也不会弹出来。

dom not loaded with script pending

直到超时错误发生, DOMContentLoaded才会触发(在我的Chrome里超时用了好几分钟!), 此时对话框也会弹出:

dom-loaded-as-script-timeout

浏览器忙提示

本文关心的核心问题是页面性能和用户体验,现在来考虑一个问题:

对于非必须的页面脚本,在它的载入过程中如何取消浏览器的忙提示。

首先想到的办法一定是从HTML中干掉那些 <script>,然后在JavaScript中动态插入 <script>标签。 比如:

var s = document.createElement('script');
s.src = "/will-not-stop-loading.js";
document.body.appendChild(s);

不贴图了,标签页上的图标确实在旋转,和上一小节中的图一样 :(

那么等 DOMContentLoaded会后再来插入呢?

document.addEventListener('DOMContentLoaded', function(){
    var s = document.createElement('script');
    s.src = "/will-not-stop-loading.js";
    document.body.appendChild(s);
});

上述代码仍然无法阻止浏览器忙提示。这充分说明浏览器JavaScript执行是单线程的,DOM事件机制也不例外。

异步加载脚本

为了阻止浏览器忙提示,应当可以使用异步加载脚本的策略。先看一个简单的示例:

setTimeout(function(){
    var s = document.createElement('script');
    s.src = "/will-not-stop-loading.js";
    document.body.appendChild(s);
});

setTimeout未指定第二个参数(延迟时间),会立即执行第一个参数传入的函数。 但是JavaScript引擎会将该函数插入到执行队列的末尾。 这意味着正在进行的DOM渲染过程完全结束后(此时浏览器忙提示当然会消失),才会调用上述函数。 看图:

async script loading

其中 /will-not-stop-loading.js仍处于 pending状态,但浏览器忙提示已经消失。 然而在Chrome中,如果插入 <script>时仍有其他资源正在载入,那么上述做法仍然达不到效果 (浏览器会判别为页面仍未完全载入)。 总之: 异步加载脚本来禁止浏览器忙提示的关键在于让DOM先加载完毕

最佳实践

不要沮丧,在实际的项目中有两种成熟的办法可以禁止浏览器忙提示。

AJAX + Eval

使用AJAX获取脚本内容,并用Eval来运行它。 因为AJAX一般不会触发浏览器忙提示,脚本执行只可能让浏览器暂停响应也不会触发忙提示。

首先在需要异步加载的脚本设置 type="text/defered-script",并用 data-src代替 src防止浏览器直接去获取:

<script type="text/async-script" data-src="http://foo.com/bar.js">

然后在站点的公共代码中加入『异步脚本加载器』:

$('[type="text/defered-script"]').each(function(idx, el){
    $.get(el.dataset.src, eval);
});

注意:使用AJAX GET脚本文件时不要设置 Content-Type: "application/javascript" (包括 jQuery.getScript)。 这会使浏览器发现你是在加载脚本,进而触发忙提示指示器。 当然,如果此时页面已然载入完毕,任何AJAX都不会触发忙提示了。

上述方法的缺点在于,一旦被引入的JavaScript中需要以相对路径的方式载入其他JavaScript就会引发错误。 因为被Eval的脚本中,当前路径变成了页面所在路径,不再是原来的 <script>src所指的路径。 这在使用第三方库时非常常见。

Load 事件

既然禁止浏览器忙指示器的关键在于让DOM加载完毕,那就绑定页面载入完毕的事件: load。 例如:

$(window).load(function(){
    $('script[type="text/async-script"]').each(function(idx, el){
        var $script = $('<script>');
        if(el.dataset.src) $script.attr('src', el.dataset.src);
        else $script.html(el.text);
        $script.appendTo('body');
        el.remove();
    });
});
  • 对于外部 <script>,生成一个新的包含正确 src<script>
  • 对于行内 <script>,生成一个新的包含正确内容的 <script>type默认即为 "application/javascript"

该方法采用DOM中 <script>加载的方式,没有AJAX+Eval改变脚本中当前路径的缺点。 http://harttleland.com中的Google Analytics、MathJax等脚本都采用这种处理方式。

服务器工具

本文所做实验服务器端都使用Node.js写成:

const http = require("http");
const fs = require('fs');
const port = 4001;

var server = http.createServer(function(req, res) {
    switch (req.url) {
        case '/':
            var html = fs.readFileSync('./index.html', 'utf8');
            res.setHeader("Content-Type", "text/html");
            res.end(html);
            break;
        case '/will-not-stop-loading.js':
            break;
    }
});

server.listen(port, e =>
    console.log(`listening to port: ${port}`));

参考阅读

404错误处理:重定向还是直接404?

$
0
0

小型网站开发通常会使用某种Web应用框架,比如类似Spring、Express、Django等框架。 这些框架会给出自定义错误页面的方式。当404发生时Web框架会渲染并返回对应的错误页面。 这是最自然和直接的错误处理方式,但有时我们希望错误页面可以单独Serve,比如放到CDN上。 本文档依据RFC 2616(HTTP 1.1)比较几种常见的404错误处理方法:

  • 返回具有404信息的页面,同时给出404状态码。

    Google、Github、Facebook、Amazon、Linkedin。

  • 重定向(302/303)至错误URL,该URL给出具有404信息的页面。

    百度、淘宝、腾讯

状态码及其语义

使用最广的HTTP标准当属1999年发布的HTTP/1.1, R. T. Fielding 在2000年的博士论文 “Architectural Styles and the Design of Network-based Software Architectures” 中重申如何正确使用HTTP语义以及REST架构风格。 符合标准的HTTP消息拥有透明的、自描述的语义,通信方遵循一致的标准才使得整个互联网得以高效地运行。

HTTP状态码对用户不可见,因为Web页面返回给用户之后页面代码并非直接显示给用户, 而是经由用户代理(User Agent,比如浏览器、爬虫等)处理。 状态码的重要性在于它告诉用户代理当前页面的状态,借此用户代理才能做出正确的操作: 比如刷新当前页面,或者访问另一个URL,重置或重新提交表单等等。

HTTP状态码有很多,本文档只关心其中的404,200,以及302/303状态码。

200

请求成功。对于GET,应当返回被请求资源的实体;对于POST,应当返回操作的结果。

RFC 2616: 10.2.1

The request has succeeded. The information returned with the response
is dependent on the method used in the request, for example:

GET   an entity corresponding to the requested resource is sent in
          the response;
POST  an entity describing or containing the result of the action;

302

被请求的资源暂时位于另一个URI处,并且对于非HEAD/GET请求, 用户代理在重定向前必须询问用户确认。

RFC 2616: 10.3.3

The requested resource resides temporarily under a different URI.
Since the redirection might be altered on occasion, the client SHOULD
continue to use the Request-URI for future requests. This response
is only cacheable if indicated by a Cache-Control or Expires header
field.

RFC 1945 和 RFC 2068 规定客户端不允许更改请求的方法。但很多浏览器会将302当做303来处理。

303

被请求的资源暂时位于另一个URI处,并且应当以GET方法去请求那个资源。

RFC 2616: 10.3.4

The response to the request can be found under a different URI and
SHOULD be retrieved using a GET method on that resource. This method
exists primarily to allow the output of a POST-activated script to
redirect the user agent to a selected resource. The new URI is not a
substitute reference for the originally requested resource. The 303
response MUST NOT be cached, but the response to the second
(redirected) request might be cacheable.

404

服务器未能找到URI所标识的资源。也常被用于服务器希望隐藏请求被拒绝的具体原因。 例如403、401可能会被统一处理为404。

RFC 2616: 10.4.5

The server has not found anything matching the Request-URI. No
indication is given of whether the condition is temporary or
permanent. 

This status code is commonly used when the server does not wish to
reveal exactly why the request has been refused, or when no other
response is applicable.

直接404

直接404是常见Web框架的通用做法:对于每一个用户请求遍历所有可能的路由, 如果任何一个控制器都不处理该请求,则Web服务器返回一个具有404状态码的HTTP响应。

404状态码告诉用户代理(User Agent)请求中的URI所标识的资源不存在, 用户代理将该HTTP响应的主体(往往是HTML)显示给用户, 该HTML页面是终端用户可读的,通常也会包含Not Found信息,以及一些有用的链接。

爬虫是一种特殊的用户代理,通常用于搜索引擎。当搜索引擎的爬虫发现某URI返回404状态码时, 会认为该URI已经失效而不对它进行索引,或者将该URI标识的已索引资源移除。 所以是网站迁移时,如果一个旧的URI会位于一个新的URI处,应当使用301重定向来告知搜索引擎: 该资源并未失效只是位置发生变化。

当我们无法控制服务器返回301时,也可在返回的 HTML中使用特殊标记 来告知用户代理这是一个301,这在迁移静态站点时非常有用。

404的一个问题在于不支持CDN。如果为了提升性能使用CDN服务, 将 404.html文件托管到CDN提供商,访问该文件显然会返回200状态码。 因为CDN服务器认为你所访问的文件存在。

重定向至404页面

在国内网站中更常见的方式是将所有错误重定向至错误页面,比如 error.html。 当用户代理访问 error.html时服务器返回状态码为200,这便是神奇的 200 Not Found。 200 Not Found显然不符合HTTP语义标准,下面从搜索引擎和CDN两个方面评价该方法的优劣:

搜索引擎非常不友好。当一个页面Not Found时爬虫并不知情,因为它收到的状态码是303。 于是跟随重定向并索引了错误页面 error.html。这意味着该网站会有大量的URL都拥有同样的内容(404页面), 网站会因此受到搜索引擎的惩罚而排名下降。

另外作为程序员吐槽一下200 Not Found给调试带来的不便:

  • 脚本、样式或 AJAX 发生 404 时 Console 不显示任何错误;
  • Accept为JSON或JavaScript的AJAX请求会得到HTML响应体;
  • <script>标签404会使整个HTML失效。

国内不少公司选择重定向的错误处理方式也必有其优点:

  • 固定的错误页面可以直接托管于CDN,通过CDN统计和脚本的方式来统计错误。
  • 固定的URL可以支持更加松耦合的架构,只需约定重定向URL即可构建前后端通用的错误处理。

参考

揭秘babel的魔法之class魔法处理

$
0
0

2017年,很多人已经开始接触ES6环境,并且早已经用在了生产当中。我们知道ES6在大部分浏览器还是跑不通的,因此我们使用了伟大的Babel来进行编译。很多人可能没有关心过,经过Babel编译之后,我们华丽的ES6代码究竟变成了什么样子?

这篇文章,针对Babel对ES6里面“类class”的编译进行分析,你可以在线 测试编译结果,毕竟纸上得来终觉浅,自己动手,才能真正体会其中的奥秘。

另外,如果你还不明白JS中原型链等OOP相关知识,建议出门左转找到经典的《JS高级程序设计》来补课;如果你对JS中,通过原型链来实现继承一直云里雾里,安利一下我的同事,前端著名网红 颜海镜大大早在2014年的文章

为什么使用选择Babel

Babel:The compiler for writing next generation JavaScript;
我们知道,现在大部分浏览器或者类似NodeJS的javascript引擎还不能直接支持ES6语法。但这并不构成障碍,比如Babel的出现,使得我们在生产环境中书写ES6代码成为了现实,它工作原理是编译ES6的新特性为老版本的ES5,从而得到宿主环境的支持。

Class例子

在这篇文章中,我会讲解Babel如何处理ES6新特性:Class,这其实是一系列语法糖的实现。

Old school方式实现继承

在探究ES6之前,我们先来回顾一下ES5环境下,我们如何实现类的继承:

// Person是一个构造器
function Person(name) {
    this.type = 'Person';
    this.name = name;
}

// 我们可以通过prototype的方式来加一条实例方法
Person.prototype.hello = function() {
    console.log('hello ' + this.name);
}

// 对于私有属性(Static method),我们当然不能放在原型链上了。我们可以直接放在构造函数上面
Person.fn = function() {
    console.log('static');
};

我们可以这么应用:

var julien = new Person('julien');
var darul = new Person('darul');
julien.hello(); // 'hello julien'
darul.hello(); // 'hello darul'
Person.fn(); // 'static'

// 这样会报错,因为fn是一个私有属性
julien.fn(); //Uncaught TypeError: julien.fn is not a function

New school方式(ES6)实现继承

在ES6环境下,我们当然迫不及待地试一试Class:

class Person {
    constructor(name) {
        this.name = name;
        this.type="person"
    }
    hello() {
        console.log('hello ' + this.name);
    }
    static fn() {
        console.log('static');
    };
}

这样写起来当然很cool,但是经过Babel编译,我们的代码是什么样呢?

Babel transformation

我们一步一步来看,

Step1: 定义
我们从最简单开始,试试不加任何方法和属性的情况下,

Class Person{}

被编译为:

function _classCallCheck(instance, Constructor) {
    // 检查是否成功创建了一个对象
    if (!(instance instanceof Constructor)) {  
        throw new TypeError("Cannot call a class as a function"); 
    } 
}

var Person = function Person() {
    _classCallCheck(this, Person);
};

你可能会一头雾水,_classCallCheck是什么?其实很简单,它是为了保证调用的安全性。
比如我们这么调用:

// ok
new p = new Person();

是没有问题的,但是直接调用:


// Uncaught TypeError: Cannot call a class as a function
Person();

就会报错,这就是_classCallCheck所起的作用。具体原理自己看代码就好了,很好理解。

我们发现,Class关键字会被编译成构造函数,于是我们便可以通过new来实现实例的生成。

Step2:Constructor探秘
我们这次尝试加入constructor,再来看看编译结果:

class Person() {
    constructor(name) {  
        this.name = name;
        this.type = 'person'
    }
}

编译结果:

var Person = function Person(name) {
    _classCallCheck(this, Person);
    this.type = 'person';
    this.name = name;
};

看上去棒极了,我们继续探索。

Step3:增加方法
我们尝试给Person类添加一个方法:hello:

class Person {
    constructor(name) {
        this.name = name;
        this.type = 'person'
    }

    hello() {
        console.log('hello ' + this.name);
    }
}

编译结果(已做适当省略):

// 如上,已经解释过
function _classCallCheck.... 

// MAIN FUNCTION
var _createClass = (function () { 
    function defineProperties(target, props) { 
        for (var i = 0; i < props.length; i++) { 
            var descriptor = props[i]; 
            descriptor.enumerable = descriptor.enumerable || false; 
            descriptor.configurable = true; 
            if ('value' in descriptor) 
            descriptor.writable = true; 
            Object.defineProperty(target, descriptor.key, descriptor); 
        } 
    } 
    return function (Constructor, protoProps, staticProps) { 
        if (protoProps) 
            defineProperties(Constructor.prototype, protoProps); 
        if (staticProps) 
            defineProperties(Constructor, staticProps); 
        return Constructor; 
    }; 
})();

var Person = (function () {
    function Person(name) {
        _classCallCheck(this, Person);

        this.name = name;
    }

    _createClass(Person, [{
        key: 'hello',
        value: function hello() {
            console.log('hello ' + this.name);
        }
    }]);

    return Person;
})();

Oh...no,看上去有很多需要消化!不要急,我尝试先把他精简一下,并加上注释,你就会明白核心思路:

var _createClass = (function () {   
    function defineProperties(target, props) { 
        // 对于每一个定义的属性props,都要完全拷贝它的descriptor,并扩展到target上
    }  
    return defineProperties(Constructor.prototype, protoProps);    
})();

var Person = (function () {
    function Person(name) { // 同之前... }

    _createClass(Person, [{
        key: 'hello',
        value: function hello() {
            console.log('hello ' + this.name);
        }
    }]);

    return Person;
})();

如果你不明白defineProperty方法, 请参考这里

现在,我们知道我们添加的方法:

hello() {
    console.log('hello ' + this.name);
}

被编译为:

_createClass(
    Person, [{
    key: 'hello',
    value: function hello() {
        console.log('hello ' + this.name);
    }
}]);

而_createClass接受2个-3个参数,分别表示:

参数1 => 我们要扩展属性的目标对象,这里其实就是我们的Person
参数2 => 需要在目标对象原型链上添加的属性,这是一个数组
参数3 => 需要在目标对象上添加的属性,这是一个数组

这样,Babel的魔法就一步一步被揭穿了。

总结

希望这篇文章能够让你了解到Babel是如何初步把我们ES6 Class语法编译成ES5的。下一篇文章我会继续介绍Babel如何处理子类的Super(), 并会通过一段函数桥梁,使得ES5环境下也能够继承ES6定义的Class。

JavaScript 启动性能瓶颈分析与解决方案

$
0
0

JavaScript 启动性能瓶颈分析与解决方案翻译自 Addy Osmani 的 JavaScript Start-up Performance,从属于笔者的 Web 前端入门与工程实践。本文已获得原作者授权,为InfoQ中文站特供稿件,首发地址为 这里;如需转载,请与InfoQ中文站联系。随着现代 Web 技术的发展与用户交互复杂度的增加,我们的网站变得日益臃肿,也要求着我们不断地优化网站性能以保证友好的用户体验。本文作者则着眼于 JavaScript 启动阶段优化,首先以大量的数据分析阐述了语法分析、编译等步骤耗时占比过多是很多网站的性能瓶颈之一。然后作者提供了一系列用于在现代浏览器中进行性能评测的工具,还分别从开发者工程实践与 JavaScript 引擎内部实现的角度阐述了应当如何提高解析与编译速度。

在 Web 开发中,随着需求的增加与代码库的扩张,我们最终发布的 Web 页面也逐渐膨胀。不过这种膨胀远不止意味着占据更多的传输带宽,其还意味着用户浏览网页时可能更差劲的性能体验。浏览器在下载完某个页面依赖的脚本之后,其还需要经过语法分析、解释与运行这些步骤。而本文则会深入分析浏览器对于 JavaScript 的这些处理流程,挖掘出那些影响你应用启动时间的罪魁祸首,并且根据我个人的经验提出相对应的解决方案。回顾过去,我们还没有专门地考虑过如何去优化 JavaScript 解析/编译这些步骤;我们预想中的是解析器在发现 <script>标签后会瞬时完成解析操作,不过这很明显是痴人说梦。下图是对于 V8 引擎工作原理的概述:

下面我们深入其中的关键步骤进行分析。

到底是什么拖慢了我们应用的启动时间?

在启动阶段,语法分析,编译与脚本执行占据了 JavaScript 引擎运行的绝大部分时间。换言之,这些过程造成的延迟会真实地反应到用户可交互时延上;譬如用户已经看到了某个按钮,但是要好几秒之后才能真正地去点击操作,这一点会大大影响用户体验。

上图是我们使用 Chrome Canary 内置的 V8 RunTime Call Stats 对于某个网站的分析结果;需要注意的是桌面浏览器中语法解析与编译占用的时间还是蛮长的,而在移动端中占用的时间则更长。实际上,对于 Facebook, Wikipedia, Reddit 这些大型网站中语法解析与编译所占的时间也不容忽视:

上图中的粉色区域表示花费在 V8 与 Blink's C++ 中的时间,而橙色和黄色分别表示语法解析与编译的时间占比。Facebook 的 Sebastian Markbage 与 Google 的 Rob Wormald 也都在 Twitter 发文表示过 JavaScript 的语法解析时间过长已经成为了不可忽视的问题,后者还表示这也是 Angular 启动时主要的消耗之一。

随着移动端浪潮的涌来,我们不得不面对一个残酷的事实:移动端对于相同包体的解析与编译过程要花费相当于桌面浏览器2~5倍的时间。当然,对于高配的 iPhone 或者 Pixel 这样的手机相较于 Moto G4 这样的中配手机表现会好很多;这一点提醒我们在测试的时候不能仅用身边那些高配的手机,而应该中高低配兼顾:

上图是部分桌面浏览器与移动端浏览器对于 1MB 的 JavaScript 包体进行解析的时间对比,显而易见的可以发现不同配置的移动端手机之间的巨大差异。当我们应用包体已经非常巨大的时候,使用一些现代的打包技巧,譬如代码分割,TreeShaking,Service Workder 缓存等等会对启动时间有很大的影响。另一个角度来看,即使是小模块,你代码写的很糟或者使用了很糟的依赖库都会导致你的主线程花费大量的时间在编译或者冗余的函数调用中。我们必须要清醒地认识到全面评测以挖掘出真正性能瓶颈的重要性。

JavaScript 语法解析与编译是否成为了大部分网站的瓶颈?

我曾不止一次听到有人说,我又不是 Facebook,你说的 JavaScript 语法解析与编译到
底会对其他网站造成什么样的影响呢?对于这个问题我也很好奇,于是我花费了两个月的时间对于超过 6000 个网站进行分析;这些网站囊括了 React,Angular,Ember,Vue 这些流行的框架或者库。大部分的测试是基于 WebPageTest 进行的,因此你可以很方便地重现这些测试结果。 光纤接入的桌面浏览器大概需要 8 秒的时间才能允许用户交互,而 3G 环境下的 Moto G4 大概需要 16 秒 才能允许用户交互。

大部分应用在桌面浏览器中会耗费约 4 秒的时间进行 JavaScript 启动阶段(语法解析、编译、执行)

而在移动端浏览器中,大概要花费额外 36% 的时间来进行语法解析:

另外,统计显示并不是所有的网站都甩给用户一个庞大的 JS 包体,用户下载的经过 Gzip 压缩的平均包体大小是 410KB,这一点与 HTTPArchive 之前发布的 420KB 的数据基本一致。不过最差劲的网站则是直接甩了 10MB 的脚本给用户,简直可怕。

通过上面的统计我们可以发现,包体体积固然重要,但是其并非唯一因素,语法解析与编译的耗时也不一定随着包体体积的增长而线性增长。总体而言小的 JavaScript 包体是会加载地更快(忽略浏览器、设备与网络连接的差异),但是同样 200KB 的大小,不同开发者的包体在语法解析、编译上的时间却是天差地别,不可同日而语。

现代 JavaScript 语法解析 & 编译性能评测

Chrome DevTools

打开 Timeline( Performance panel ) > Bottom-Up/Call Tree/Event Log 就会显示出当前网站在语法解析/编译上的时间占比。如果你希望得到更完整的信息,那么可以打开 V8 的 Runtime Call Stats。在 Canary 中,其位于 Timeline 的 Experims > V8 Runtime Call Stats 下。

Chrome Tracing

打开 about:tracing 页面,Chrome 提供的底层的追踪工具允许我们使用 disabled-by-default-v8.runtime_stats来深度了解 V8 的时间消耗情况。V8 也提供了 详细的指南来介绍如何使用这个功能。

WebPageTest


WebPageTest 中 Processing Breakdown 页面在我们启用 Chrome > Capture Dev Tools Timeline 时会自动记录 V8 编译、EvaluateScript 以及 FunctionCall 的时间。我们同样可以通过指明 disabled-by-default-v8.runtime_stats的方式来启用 Runtime Call Stats。

更多使用说明参考我的 gist

User Timing

我们还可以使用 Nolan Lawson 推荐的 User Timing API来评估语法解析的时间。不过这种方式可能会受 V8 预解析过程的影响,我们可以借鉴 Nolan 在 optimize-js 评测中的方式,在脚本的尾部添加随机字符串来解决这个问题。我基于 Google Analytics 使用相似的方式来评估真实用户与设备访问网站时候的解析时间:

DeviceTiming

Etsy 的 DeviceTiming工具能够模拟某些受限环境来评估页面的语法解析与执行时间。其将本地脚本包裹在了某个仪表工具代码内从而使我们的页面能够模拟从不同的设备中访问。可以阅读 Daniel Espeset 的 Benchmarking JS Parsing and Execution on Mobile Devices一文来了解更详细的使用方式。

我们可以做些什么以降低 JavaScript 的解析时间?

  • 减少 JavaScript 包体体积。我们在上文中也提及,更小的包体往往意味着更少的解析工作量,也就能降低浏览器在解析与编译阶段的时间消耗。

  • 使用代码分割工具来按需传递代码与懒加载剩余模块。这可能是最佳的方式了,类似于 PRPL这样的模式鼓励基于路由的分组,目前被 Flipkart, Housing.com 与 Twitter 广泛使用。

  • Script streaming: 过去 V8 鼓励开发者使用 async/defer来基于 script streaming实现 10-20% 的性能提升。这个技术会允许 HTML 解析器将相应的脚本加载任务分配给专门的 script streaming 线程,从而避免阻塞文档解析。V8 推荐尽早加载较大的模块,毕竟我们只有一个 streamer 线程。

  • 评估我们依赖的解析消耗。我们应该尽可能地选择具有相同功能但是加载地更快的依赖,譬如使用 Preact 或者 Inferno 来代替 React,二者相较于 React 体积更小具有更少的语法解析与编译时间。Paul Lewis 在最近的 一篇文章中也讨论了框架启动的代价,与 Sebastian Markbage 的 说法不谋而合:最好地评测某个框架启动消耗的方式就是先渲染一个界面,然后删除,最后进行重新渲染。第一次渲染的过程会包含了分析与编译,通过对比就能发现该框架的启动消耗。

如果你的 JavaScript 框架支持 AOT(ahead-of-time)编译模式,那么能够有效地减少解析与编译的时间。Angular 应用就受益于这种模式:

现代浏览器是如何提高解析与编译速度的?

不用灰心,你并不是唯一纠结于如何提升启动时间的人,我们 V8 团队也一直在努力。我们发现之前的某个评测工具 Octane 是个不错的对于真实场景的模拟,它在微型框架与冷启动方面很符合真实的用户习惯。而基于这些工具,V8 团队在过去的工作中也实现了大约 25% 的启动性能提升:

本部分我们就会对过去几年中我们使用的提升语法解析与编译时间的技巧进行阐述。

代码缓存

Chrome 42 开始引入了所谓的 代码缓存的概念,为我们提供了一种存放编译后的代码副本的机制,从而当用户二次访问该页面时可以避免脚本抓取、解析与编译这些步骤。除以之外,我们还发现在重复访问的时候这种机制还能避免 40% 左右的编译时间,这里我会深入介绍一些内容:

  • 代码缓存会对于那些在 72 小时之内重复执行的脚本起作用。

  • 对于 Service Worker 中的脚本,代码缓存同样对 72 小时之内的脚本起作用。

  • 对于利用 Service Worker 缓存在 Cache Storage 中的脚本,代码缓存能在脚本首次执行的时候起作用。

总而言之,对于主动缓存的 JavaScript 代码,最多在第三次调用的时候其能够跳过语法分析与编译的步骤。我们可以通过 chrome://flags/#v8-cache-strategies-for-cache-storage来查看其中的差异,也可以设置  js-flags=profile-deserialization运行 Chrome 来查看代码是否加载自代码缓存。不过需要注意的是,代码缓存机制仅会缓存那些经过编译的代码,主要是指那些顶层的往往用于设置全局变量的代码。而对于类似于函数定义这样懒编译的代码并不会被缓存,不过 IIFE 同样被包含在了 V8 中,因此这些函数也是可以被缓存的。

Script Streaming

Script Streaming允许在后台线程中对异步脚本执行解析操作,可以对于页面加载时间有大概 10% 的提升。上文也提到过,这个机制同样会对同步脚本起作用。

这个特性倒是第一次提及,因此 V8 会允许所有的脚本,即使阻塞型的 <script src=''>脚本也可以由后台线程进行解析。不过缺陷就是目前仅有一个 streaming 后台线程存在,因此我们建议首先解析大的、关键性的脚本。在实践中,我们建议将 <script defer>添加到 <head>块内,这样浏览器引擎就能够尽早地发现需要解析的脚本,然后将其分配给后台线程进行处理。我们也可以查看 DevTools Timeline 来确定脚本是否被后台解析,特别是当你存在某个关键性脚本需要解析的时候,更需要确定该脚本是由 streaming 线程解析的。

语法解析 & 编译优化

我们同样致力于打造更轻量级、更快的解析器,目前 V8 主线程中最大的瓶颈在于所谓的非线性解析消耗。譬如我们有如下的代码片:

(function (global, module) { … })(this, function module() { my functions })

V8 并不知道我们编译主脚本的时候是否需要 module这个模块,因此我们会暂时放弃编译它。而当我们打算编译 module时,我们需要重分析所有的内部函数。这也就是所谓的 V8 解析时间非线性的原因,任何一个处于 N 层深度的函数都有可能被重新分析 N 次。V8 已经能够在首次编译的时候搜集所有内部函数的信息,因此在未来的编译过程中 V8 会忽略所有的内部函数。对于上面这种 module形式的函数会是很大的性能提升,建议阅读 The V8 Parser(s) — Design, Challenges, and Parsing JavaScript Better来获取更多内容。V8 同样在寻找合适的分流机制以保证启动时能在后台线程中执行 JavaScript 编译过程。

预编译 JavaScript?

每隔几年就有人提出引擎应该提供一些处理预编译脚本的机制,换言之,开发者可以使用构建工具或者其他服务端工具将脚本转化为字节码,然后浏览器直接运行这些字节码即可。从我个人观点来看,直接传送字节码意味着更大的包体,势必会增加加载时间;并且我们需要去对代码进行签名以保证能够安全运行。目前我们对于 V8 的定位是尽可能地避免上文所说的内部重分析以提高启动时间,而预编译则会带来额外的风险。不过我们欢迎大家一起来讨论这个问题,虽然 V8 目前专注于提升编译效率以及推广利用 Service Worker 缓存脚本代码来提升启动效率。我们在 BlinkOn7 上与 Facebook 以及 Akamai 也讨论过 预编译相关内容

Optimize JS 优化

类似于 V8 这样的 JavaScript 引擎在进行完整的解析之前会对脚本中的大部分函数进行预解析,这主要是考虑到大部分页面中包含的 JavaScript 函数并不会立刻被执行。

预编译能够通过只处理那些浏览器运行所需要的最小函数集合来提升启动时间,不过这种机制在 IIFE 面前却反而降低了效率。尽管引擎希望避免对这些函数进行预处理,但是远不如 optimize-js这样的库有作用。optimize-js 会在引擎之前对于脚本进行处理,对于那些立即执行的函数插入圆括号从而保证更快速地执行。这种预处理对于 Browserify, Webpack 生成包体这样包含了大量即刻执行的小模块起到了非常不错的优化效果。尽管这种小技巧并非 V8 所希望使用的,但是在当前阶段不得不引入相应的优化机制。

总结

启动阶段的性能至关重要,缓慢的解析、编译与执行时间可能成为你网页性能的瓶颈所在。我们应该评估页面在这个阶段的时间占比并且选择合适的方式来优化。我们也会继续致力于提升 V8 的启动性能,尽我所能!

延伸阅读

Web性能优化

$
0
0

1 Web性能优化

Web网站的性能细线在几个方面:

  • 网站首页加载速度

  • 动画的流畅度

通过分析浏览器的渲染原理、资源对渲染的影响,得出优化网站性能的办法。

2 查看性能的工具

Chrome的 Timeline面板录制网页加载的过程,分析记录浏览器渲染过程中每个过程的耗时。

2.1 录制时注意事项

  1. 禁用浏览器缓存: Network Tab下的 disable cache

  2. 关闭Chrome扩展或者启用隐身模式

  3. 根据使用场景,模拟真实的网络加载情况: Network Tab下的 throttling下拉按钮

2.2 Timeline工具的各个组成

  • Main Thread中可以看到页面渲染的整个过程及耗时

图片描述

3 浏览器渲染原理

图片描述

3.1 DOM树构建

DOM树的构建过程

  1. 根据HTML文档的内容,根据标签进行分词 Token

  2. 根据 Token生产对应的节点 Node

  3. 将节点根据嵌套关系组合为一棵对象节点树 DOM

浏览器解析文档对象模型 DOM增量进行的,无需等待整个HTML文档加载完毕,便可以开始解析 DOM

CSSOM解析会阻塞 HTML Parser;JavaScript脚本文件 执行会阻塞HTML解析; CSS、JavaScript、Images和Font等静态资源的异步加载的,渲染页面与CSS解析与JavaScript执行会有相互的依赖

图片描述
图片描述

3.2 CSSOM树的构建

CSSOM的解析依赖于 选择器,选择器的匹配是从内到外的。所以选择器嵌套层次越深,匹配的时间会越长。

CSSOM只解析可视部分 body标签中的内容,将所有匹配的元素共同构建一个 CSSOM树, 从根节点一次向下,所有节点的属性向下继承

图片描述

3.3 RenderTree树的构建

利用DOM和CSSOM组合构建生成RenderTree,对应 Recaculate Style

RenderTree中包含所有渲染网页必须的节点

无需渲染的节点不会被添加到RenderTree中,如 headdisplay:none;的节点

visibility: hidden;的节点会添加到RenderTree中

图片描述

3.4 Layout

Layout利用渲染树的信息,计算渲染树中所有节点在页面上的 位置和大小

类似绘画中各个元素位置摆放及尺寸规划

会引起页面重新Layout的操作: 所有改变节点位置和大小的操作

  • 屏幕旋转

  • 浏览器视窗改变

  • 与大小、位置相关的CSS属性

  • 增加与删除DOM元素

Layout操作比较耗时,对于动画中频繁引起Layout的操作(元素位置移动), 最好使用transform代替,可以使用GPU进行动画处理(将Layout重绘在GPU完成)

图片描述

viewport

如果页面 body元素设置的宽度为 100%,并且根元素 html没有明确设置宽度绝对值, 此时 body元素的宽度等于 viewport的宽度 vw

  • 使用 meta标签可以设置浏览器 viewport的尺寸。 <meta name="viewport" content="width=device-width">

  • device-width为浏览器的理想视口(屏幕的物理分辨率)

  • 在移动端,如果不设置 device-width,默认 viewport宽度为980px, 导致文字很小,需要放大

viewport相当于可视内容布局的容器

3.5 Paint

填充Layout中的具体内容和样式,将Layout生成的区域填充为最终显示在屏幕上的像素

3.6 总结

  1. 浏览器通过 GET请求获取网页HTML,同时将增量解析HTML文档,生成 DOM

  2. 解析 DOM节点树时,对于需要加载的资源 全部执行异步加载,但是 CSS的解析、 JavaScript的执行与 font文件的下载会阻塞HTML Parser

  3. 局部 DOM树与 CSSOM树构建完成后, 立即组装 RenderTree进行渲染

图片描述

4 资源对渲染的影响

页面中加载的资源主要包括: cssjs脚本文件和 font字体与 images静态资源,不同资源类型对渲染的影响不同。

4.1 浏览器渲染页面的时机

增量解析解析 DOM树,并且完成相应 CSSOM解析后(RenderTree依赖于 DOM树, CSSOM树),开始直接渲染页面。

4.2 CSS加载会阻塞初次渲染

图片描述

4.3 非关键资源

对于首页无关的样式,需要使用适当的方式避免其阻塞初次渲染:

  • document.write()会阻塞页面初次渲染

  • 使用 media=print媒体查询,虽然加载样式表,但只针对打印时才应用该样式,不会阻塞初次渲染。

  • 通过 DOMAPI引入CSS,可以避免阻塞。

  • CSS中 <link rel="preload" href="index_print.css" as="style" onload="this.rel='stylesheet'">

图片描述

图片描述

图片描述

图片描述

4.4 JS文件

图片描述

  • 输出:先输出 Hello,10s之后再输出 World。JS脚本 执行会阻塞 HTML Parser,但是 HTML Parser是增量解析的, 并且CSS样式的解析会阻塞JS脚本执行,当解析完 Hello时,生成对应 DOM节点,并且完成其 CSSOM,直接开始渲染 Hello节点。

  • 脚本执行完成后再解析后续的 World

JS脚本执行会阻塞HTML Parser;

CSS解析会阻塞JS脚本执行:js可能会读、写CSSOM

虽然JS会阻塞HTML Parser解析; 但是浏览器的资源异步加载机制 Preload会异步加载 head标签内的资源

图片描述

图片描述

图片描述

4.5 非关键JS资源解析阻塞的优化方案

  • 将JS资源文件放在文档底部,延迟JS的执行(但是存在必须解析完HTML才能加载JS资源,相较于 head标签中加载会慢)

  • 使用 defer延迟脚本执行: scipt标签的 defer属性,脚本会在HTML文档解析完毕后再开始执行; defer的脚本在执行时严格按照HTML文档中出现的顺序执行---优势可以提早加载JS资源,但是解析完HTML再执行

  • 使用 async异步执行脚本:

    • script标签有 async属性时,脚本执行不会阻塞HTML Parser,只要脚本加载完毕便开始执行

    • async的脚本,不会严格按照在HTML文档中的顺序执行

    • async适用于无依赖的外部独立资源(注意不要错误操作状态)

图片描述
图片描述

4.6 font字体文件

  • font字体文件会阻塞内容渲染
    图片描述

4.7 图片资源

图片资源的加载不会阻塞渲染,但是最好在HTML标签中设置图片的高度和宽度,可以在 Layout时留出图片渲染的空间,避免页面的抖动

5 优化关键渲染路径

优化目标是将下列三个指标压缩到最低:

  • 关键资源数---初次渲染时依赖的资源

  • 关键资源的体积最小---压缩文件或图片

  • 关键资源网络来回数---网络传输资源消耗很多时间

图片描述
图片描述
图片描述
图片描述

6 其余优化过程

  • HTTP2可以在传输HTML页面后向客户端推送页面内包含的资源

  • 减少资源的大小:压缩

  • 减少请求的来回时间

图片描述
图片描述


微豆 - Vue 2.0 实现豆瓣 Web App 教程

$
0
0

微豆 Vdo

一个使用 Vue.js 与 Material Design 重构 豆瓣的项目。

项目网站 http://vdo.ralfz.com/

GitHub https://github.com/RalfZhang/Vdo

gif

快速使用

# 克隆项目到本地
git clone https://github.com/RalfZhang/Vdo.git

# 安装依赖
npm install

# 在 localhost:8080 启动项目
npm run dev

教程

安装 vue-cli 脚手架

运行如下命令,即可创建一个名为 my-project 的 vue 项目,并且通过本地 8080 端口启动服务

npm install -g vue-cli
vue init webpack my-project
cd my-project
npm install
npm run dev

在运行 vue init webpack my-project后,会依次要求输入以下配置内容

  • 项目名称

  • 项目描述

  • 作者

  • 选择 Vue 构建:运行+编译 或 仅运行时

  • 是否安装 vue-loader

  • 是否使用 ESLint

    • 如果是,请选择模式:标准模式、AirBNB 模式、自定义

  • 是否使用 Karma + Mocha 的单元测试

  • 是否使用 Nightwatch e2e 测试

图片描述

安装完成后,即可看到以下文件结构:

.
|-- build                            // 项目构建相关代码
|   |-- build.js                     // 生产环境构建代码
|   |-- check-version.js             // 检查 node、npm 等版本
|   |-- dev-client.js                // 热重载相关
|   |-- dev-server.js                // 构建本地服务器
|   |-- utils.js                     // 构建工具相关
|   |-- webpack.base.conf.js         // webpack 基础配置(出入口和 loader)
|   |-- webpack.dev.conf.js          // webpack 开发环境配置
|   |-- webpack.prod.conf.js         // webpack 生产环境配置
|-- config                           // 项目开发环境配置
|   |-- dev.env.js                   // 开发环境变量
|   |-- index.js                     // 项目一些配置变量(开发环境接口转发将在此配置)
|   |-- prod.env.js                  // 生产环境变量
|   |-- test.env.js                  // 测试环境变量
|-- src                              // 源码目录
|   |-- components                   // vue 公共组件
|   |-- store                        // vuex 的状态管理
|   |-- App.vue                      // 页面入口文件
|   |-- main.js                      // 程序入口文件,加载各种公共组件
|-- static                           // 静态文件,比如一些图片,json数据等
|-- test                             // 自动化测试相关文件
|-- .babelrc                         // ES6语法编译配置
|-- .editorconfig                    // 定义代码格式
|-- .eslintignore                    // ESLint 检查忽略的文件
|-- .eslistrc.js                     // ESLint 文件,如需更改规则则在此文件添加
|-- .gitignore                       // git 上传需要忽略的文件
|-- README.md                        // 项目说明
|-- index.html                       // 入口页面
|-- package.json                     // 项目基本信息
.

ESLint 配置

ESLint 配置在根目录的 .eslintrc.js里。
正常情况下,ESLint 报错是因为你的代码不符合现有的 ESLint 规范。
如果你的情况实在不想被 ESLint 报错,我举出两个解决方案,来处理 ESLint 报错问题。

注:本例使用 AirBNB ESLint 规则。
例:通过 npm run dev启动服务,打开 ./src/main.js,添加一句 console.log('abc'),结果如下:

import Vue from 'vue';
import App from './App';
import store from './vuex/store';
/* import router from './router';*/

// 添加此句
console.log('abc')

/* eslint-disable no-new */
new Vue({
  el: '#app',
  /* router,*/
  template: '<App/>',
  components: { App },
  store,
});

注:为做演示,句末未添加分号。

保存 main.js文件后,页面与终端均提示如下错误:

 ERROR  Failed to compile with 1 errors 
 error  in ./src/main.js⚠  http://eslint.org/docs/rules/no-console  Unexpected console statement
  C:\Users\Ralf\Documents\code\ralfz\vue\test\vue02\src\main.js:8:1
  console.log('abc')
   ^✘  http://eslint.org/docs/rules/semi        Missing semicolon
  C:\Users\Ralf\Documents\code\ralfz\vue\test\vue02\src\main.js:8:19
  console.log('abc')
                     ^
✘ 2 problems (1 error, 1 warning)
Errors:
  1  http://eslint.org/docs/rules/semi
Warnings:
  1  http://eslint.org/docs/rules/no-console
 @ multi ./build/dev-client ./src/main.js

以上输出表明出现两个问题:

  1. 警告:不允许 console 语句。

  2. 错误:句末未加分号。

解决问题 1

  • .eslintrc.js文件中的 rules键名下添加 'no-console': 'off',,即关闭 console 警告。

解决问题 2

  • 你可以选择继续在 .eslintrc.js文件中添加关闭句末分号判定的规则。

  • 或者,也可以把 package.json文件中的 script下的 lint命令改为
    "lint": "eslint --fix *.js *.vue src/* test/unit/specs/* test/e2e/specs/*"

即自动修复。值得注意的是,自动修复不能解决所有问题,有时也不甚完美,可以多试几次体会下 fix 的效果。

做完更改后,重新运行 npm run dev即可看到无问题报告,并且 console语句后已经自动加上了分号。

静态页面开发

此时,浏览器应该已经打开了 localhost:8080 页面。

在此情况下,请尝试更改 /src/App.vue/src/components/Hello.vue文件中 <template>标签内的内容,保存后即可立即看到浏览器页面已自动更新了你做出的改动。

接下来,你需要去阅读并学习 Vue.js 教程页面,务必熟悉 基础部分的内容,掌握 组件章节。

熟悉之后,便可以完成基础的静态页面(或者说是组件)设计工作。

本项目使用了基于 Vue 2.0 和 Material Desigin 的 UI 组件库 Muse-UI

提示: ./src/components文件夹多用于保存公用组件。至于页面组件,推荐在新建 ./src/view文件夹后存放于此。

vue-router 2 使用

当一个个静态组件完成后,需要按照路由组织这些组件文件。

请前往 vue-router 2 介绍阅读 基础部分教程,并可以边阅读边配置路由。

路由文件是 ./src/router.index.js

本项目中使用了 HTML5 History 模式,路由配置比较简单,可以参考。

API 请求转发配置

至此,你应该已经完成了所有的静态页面的工作,接下来我们准备搭建请求,为后面的 xhr 请求做好准备。

  1. 打开 http://api.douban.com/v2/movie/in_theaters查看接口数据,留意此地址。

  2. ./config/index.js中的 proxyTable配置代理:

    proxyTable: {'/api': {
            target: 'http://api.douban.com/v2',
            changeOrigin: true,
            pathRewrite: {'^/api': ''
            }
        }
    }
  3. 重新启动 npm run dev,打开 localhost:8080/api/movie/in_theaters查看结果是否与直接请求豆瓣 API 相同。

  4. 本应该使用了以下 API:

    • /v2/movie/search?q={text}电影搜索api;

    • /v2/movie/in_theaters正在上映的电影;

    • /v2/movie/coming_soon即将上映的电影;

    • /v2/movie/subject/:id单个电影条目信息。

更多请参考 豆瓣电影 API文档。

这样我们就可以在应用中调用 /api/movie/in_theaters来访问 http://api.douban.com/v2/movie/in_theaters,从而解决跨域的问题。

使用 axios

axios 库使用起来相当简单。

你可以在单个组件中尝试引入并调用:

import axios from 'axios';
axios.get('/v2/movie/in_theaters', { 'city': '广州' })
    .then((result) => {
        console.log(result);
    });

这里,可以用返回的 result去更新 data(){ }return的数据。

更多 axios 用法请参考 文档

使用 Vuex 并分离代码

为了试代码更加结构化,我们应当将数据请求和视图分离。

这一节中,我们有两个任务要做:

  1. 分离数据请求层逻辑。

  2. 使用 Vuex 管理状态。

将二者放到同一节中主要是因为二者再同一目录下,我们来查看 ./store文件夹的结构:

.
|-- store                          // 数据处理根目录
|   |-- movies                     // 单个电影模块文件夹
|   |   |-- api.js                 // 电影模块对外开放的接口
|   |   |-- module.js              // Vuex 模块
|   |   |-- type.js                // Vuex 操作 key
|   |-- base.js                    // 基础方法
|   |-- store.js                   // Vuex 入口
.

针对第一个任务:

  • base.js存放封装的基础请求函数

  • **/api.js存放该模块下公开的请求函数

针对第二个任务,我们需要先了解 Vuex。

请查看 Vuex 文档,了解其 核心概念

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 也集成到 Vue 的官方调试工具 devtools extension,提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能。

其实在我看来,Vuex 相当于某种意义上设置了读写权限的全局变量,将数据保存保存到该“全局变量”下,并通过一定的方法去读写数据。(希望这能帮助你理解 Vuex)

为了方便模块化管理:

  • 我将 store.js作为入口文件,去挂载各个模块;

  • /movies/文件夹下为电影相关的模块;

  • /movies/moudule.js为电影模块的主要 Vuex 文件;

  • /movies/type.js使用常量替代 Mutation 事件类型的实现。

到此便完成了所有开发上的基础问题。

发布

  1. 运行 npm run build,即可在生成的 /dist文件夹下看到所有文件。

  2. 将文件复制到你的服务器上某个目录(我的是 /var/www/Vdo/dist),按照下一节配置 Nginx 即可

提示:可以使用 scp命令将本地文件拷贝至服务器,例如 scp -P 20 -r dist user@host:/target/location

附:配置与开启 Nginx

注:以下以 CentOS 为例

  1. 安装 Nginx: yum install nginx

  2. 打开 /etc/nginx/conf.d/default.conf

  3. 替换全文为本项目 /doc/nginx.conf文件中的内容

  4. 运行 nginx

提示:

  1. 403 Forbidden错误可能是由于文件和文件夹权限引起的,请用 chmod把存放 index.html的所有路径上的文件夹权限设置为 755,并将 index.html文件权限设置成 644 即可。

  2. 更改 Nginx 配置文件后,可以使用 nginx -s reload命令刷新。

结语

至此,主体工作已经完成。

欢迎 Star 本项目。

https://github.com/RalfZhang/Vdo

感谢&参考

License

MIT

工作中经常用到github上优秀、实用、轻量级、无依赖的插件和库

$
0
0

原文收录在我的 GitHub博客 ( https://github.com/jawil/blog),喜欢的可以关注最新动态,大家一起多交流学习,共同进步,以学习者的身份写博客,记录点滴。

按照格式推荐好用的插件有福利哦,说不定会送1024论坛邀请码,好自为之,你懂的,嘿嘿嘿。

由于github的issues没有TOC菜单栏导航,所以这里方便大家查看,先安利一款Chrome浏览器的插件,感谢github用户@BBcaptain 推荐。 点击我呀,进入商店,自备梯子,如果不会翻墙,赶紧转行。。。

效果预览,是不是很方便,图片较多,建议等待一会或者多刷新几下:

Echo.js – 简单易用的图片延迟加载插件

github: https://github.com/toddmotto/...

官方网站: https://toddmotto.com/echo-js...

star:3k+

Install:

npm:npm install echo-js
bower:bower install echojs

大小:2KB

功能介绍:
  Echo.js 是一个独立的延迟加载图片的 JavaScript 插件。Echo.js 不依赖第三方库,压缩后不到1KB大小。 延迟加载是提高网页首屏显示速度的一种很有效的方法,当图片元素进入窗口可视区域的时候,它就会改变图像的 src 属性,从服务端加载所需的图片,这也是一个异步的过程。
  
Demo:
效果预览地址: https://jawil.github.io/demo/...
Demo源码: https://github.com/jawil/jawi...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。






Lazyr.js – 延迟加载图片(Lazy Loading)

github: https://github.com/callmecavs...

官方网站: http://callmecavs.com/layzr.j...

star:5k+

Install:

npm:npm install layzr.js --save

大小:2.75 KB

功能介绍:
  Lazyr.js 是一个小的、快速的、现代的、相互间无依赖的图片延迟加载库。通过延迟加载图片,让图片出现在(或接近))视窗才加载来提高页面打开速度。这个库通过保持最少选项并最大化速度。

Demo:
跟上面的Echo.js用法类似,喜欢的可以自行去尝试,这里就不再演示了,我一般用Echo.js。






better-scroll.js – 小巧,灵活的 JavaScript 模拟滚动条的插件

github: https://github.com/ustbhuangy...

官方网站: https://ustbhuangyi.github.io...

star:300+

Install:

npm install better-scroll --save-dev
import BScroll from 'better-scroll';

大小:24 KB

功能介绍:
  better-scroll 是一个只有24.8KB的 JavaScript 模拟浏览器自带滚动条的插件,是在 iscroll开源的基础上进行优化的一款插件,简单好用,轻巧高性能,功能强大,API通俗易懂,是一款优秀的scroll插件,抛弃原生滚动条,从现在做起。

Demo:
效果预览地址: https://jawil.github.io/demo/... (PC端切换到移动模式)
Demo源码: https://github.com/jawil/webp...
注:在ustbhuangyi的源码下改进了一下,做成多页面,技术栈:webpack2+vue.js2+sass+axios

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。






better-picker – 一款轻量级IOS风格的JavaScript选择器

github: https://github.com/ustbhuangy...

官方网站: http://ustbhuangyi.github.io/...

star:200+

Install:

npm: npm install better-picker --save-dev
import Picker from 'better-picker'

大小:46.5 KB

功能介绍:
  移动端最好用的的筛选器组件,高仿 ios 的 UIPickerView ,非常流畅的体验,原生 JS 实现,不依赖任何插件和第三方库。
Demo:
效果预览地址: http://ustbhuangyi.github.io/...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。






Sortable – 一款用于实现元素拖拽排序的功能的插件

github: https://github.com/RubaXa/Sor...

官方网站: http://rubaxa.github.io/Sorta...

star:9k+

Install:

Bower: bower install sortablejs --save
npm: npm install sortablejs 

大小:5 KB

功能介绍:
 Sortable:现代浏览器上用于实现元素拖拽排序的功能,支持 Meteor, AngularJS, React,不依赖 jQuery这玩意。

Demo:
效果预览地址: http://rubaxa.github.io/Sorta...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。







slick – 功能异常强大的一个图片滑动切换效果库

github: https://github.com/kenwheeler...

官方网站: http://kenwheeler.github.io/s...

star:17k+

Install:

Bower: bower install slick-carousel --save
npm: npm install slick-carousel
CDNs:
https://cdnjs.com/libraries/slick-carousel
https://www.jsdelivr.com/projects/jquery.slick

大小:40 KB

功能介绍:
 slick 是一个功能异常强大的一个图片滑动切换效果库,接口丰富,支持各种动画和各种样式的切换滑动,唯一的缺点就是 基于jQuery,基本废了,现在没人喜欢用jQuery,该淘汰了。。。支持 RequireJS 以及 Bower 安装。

Demo:
效果预览地址: http://kenwheeler.github.io/s...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。







swipe – 非常轻量级的一个图片滑动切换效果库

github: https://github.com/lyfeyaj/Sw...

官方网站: http://lyfeyaj.github.io/swip...

star:200+

Install:

Bower: bower install swipe-js  --save
npm: npm install swipe-js 

大小:5 KB

功能介绍:
 swipe:非常轻量级的一个图片滑动切换效果库, 性能良好, 尤其是对手机的支持, 压缩后的大小约 5kb。可以结合 jQuery、RequireJS 使用。

Demo:
效果预览地址: http://lyfeyaj.github.io/swipe/

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。






Slideout.js – 触摸滑出式 Web App 导航菜单

github: https://github.com/mango/slid...

官方网站: https://slideout.js.org/https://github.com/t4t5/sweet...

官方网站: http://t4t5.github.io/sweetal...

star:15k+

Install:

bower:bower install sweetalert
npm:npm install sweetalert<script src="dist/sweetalert.min.js"></script><link rel="stylesheet" type="text/css" href="dist/sweetalert.css">

大小:16 KB

功能介绍:
  Sweet Alert 是一个替代传统的 JavaScript Alert 的漂亮提示效果。SweetAlert 自动居中对齐在页面中央,不管您使用的是台式电脑,手机或平板电脑看起来效果都很棒。另外提供了丰富的自定义配置选择,可以灵活控制。

Demo:
效果预览地址: http://t4t5.github.io/sweetal...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。

类似插件:limonte/sweetalert2,好像这个最近还在更新,这个感觉更漂亮,大同小异,这里不多做介绍。

github: https://github.com/limonte/sw...

官方网站: https://limonte.github.io/swe...






Awesomplete.js - 比datalist更强大更实用,零依赖的简单自动补全插件

github: https://github.com/leaverou/a...

官方网站: http://leaverou.github.io/awe...

star:5k+

Install:
`npm: npm install awesomplete
`

大小:5 KB

功能介绍:
 Awesomplete 是一款超轻量级的,可定制的,简单的自动完成插件,零依赖,使用现代化标准构建。你可以简单地添加 awesomplete 样式,让它自动处理(你仍然可以通过指定 HTML 属性配置更多选项),您可以用几行 JS 代码,提供更多的自定义。

Demo:
效果预览地址: http://leaverou.github.io/awe...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。

Cleave.js – 自动格式化表单输入框的文本内容

github: https://github.com/nosir/clea...

官方网站: http://nosir.github.io/cleave...

star:6k+

Install:

npm:npm install --save cleave.js
bower:bower install --save cleave.js

大小:11.1 KB

功能介绍:
  Cleave.js 有一个简单的目的:帮助你自动格式输入的文本内容。 这个想法是提供一个简单的方法来格式化您的输入数据以增加输入字段的可读性。通过使用这个库,您不需要编写任何正则表达式来控制输入文本的格式。然而,这并不意味着取代任何验证或掩码库,你仍应在后端验证数据。它支持信用卡号码、电话号码格式(支持各个国家)、日期格式、数字格式、自定义分隔符,前缀和块模式等,提供 CommonJS/AMD 模式以及ReactJS 组件端口。

Demo:
效果预览地址: http://nosir.github.io/cleave...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。
输入201748自动格式化成2017-04-08,是不是很方便





Immutable.js – JavaScript 不可变数据集合(Facebook出品)

github: https://github.com/facebook/i...

官方网站: http://facebook.github.io/imm...

star:18k+

Install:

npm install immutable --S-D

大小:60 KB

功能介绍:
  不可变数据是指一旦创建就不能被修改的数据,使得应用开发更简单,允许使用函数式编程技术,比如惰性评估。Immutable JS 提供一个惰性 Sequence,允许高效的队列方法链,类似 map 和 filter ,不用创建中间代表。Immutable.js 提供持久化的列表、堆栈、Map, 以及 OrderedMap 等,最大限度地减少需要复制或缓存数据。

Demo:

<script src="immutable.min.js"></script><script>
var map1 = Immutable.Map({a:1, b:2, c:3});
var map2 = map1.set('b', 50);
map1.get('b'); // 2
map2.get('b'); // 50</script>

更多信息和探讨请移步,这里不多做介绍: facebook immutable.js 意义何在,使用场景?






Popmotion.js – 小巧,灵活的 JavaScript 运动引擎

github: https://github.com/Popmotion/...

官方网站: https://popmotion.io/

star:3k+

Install:

npm install --save popmotion
import { tween } from 'popmotion';

大小:12 KB

功能介绍:
  Popmotion 是一个只有12KB的 JavaScript 运动引擎,可以用来实现动画,物理效果和输入跟踪。原生的DOM支持:CSS,SVG,SVG路径和DOM属性的支持,开箱即用。Popmotion 网站上有很多很赞的效果,赶紧去体验一下。

Demo:
效果预览地址: http://codepen.io/popmotion/p...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。







Dynamics.js - 创建逼真的物理动画的 JS 库

github: https://github.com/michaelvil...

官方网站: http://dynamicsjs.com/

star:6k+

Install:

npm: npm install dynamics.js
bower: bower install dynamics.js

大小:20 KB

功能介绍:
  Popmotion 是一个只有12KB的 JavaScript 运动引擎,可以用来实现动画,物理效果和输入跟踪。原生的DOM支持:CSS,SVG,SVG路径和DOM属性的支持,开箱即用。Popmotion 网站上有很多很赞的效果,赶紧去体验一下。

Demo:
效果预览地址: http://dynamicsjs.com/

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。






Rainyday.js – 使用 JavaScript 实现雨滴效果

github: https://github.com/maroslaw/r...

官方网站: http://maroslaw.github.io/rai...

star:5k+

Install:

在github的dist目录下载rainyday.min.js

大小:10 KB

功能介绍:
 Rainyday.js 背后的想法是创建一个 JavaScript 库,利用 HTML5 Canvas 渲染一个雨滴落在玻璃表面的动画。Rainyday.js 有功能可扩展的 API,例如碰撞检测和易于扩展自己的不同的动画组件的实现。它是一个使用 HTML5 特性纯 JavaScript 库,支持大部分现代浏览器。

Demo:
效果预览地址: http://maroslaw.github.io/rai...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。

Swiper – 经典的移动触摸滑块轮播插件

github: https://github.com/nolimits4w...

官方网站: http://idangero.us/swiper/https://github.com/daniel-lun...

官方网站: http://daniel-lundin.github.i...

star:5k+

Install:

bower:bower install snabbt.js
npm:npm install snabbt.js
CDNs:
https://cdnjs.com/libraries/snabbt.js
http://www.jsdelivr.com/#!snabbt

大小:16 KB

功能介绍:
 Snabbt.js 是一个简约的 JavaScript 动画库。它会平移,旋转,缩放,倾斜和调整你的元素。通过矩阵乘法运算,变换等可以任何你想要的方式进行组合。最终的结果通过 CSS3 变换矩阵设置。

Demo:
效果预览地址: http://daniel-lundin.github.i...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。






imagesLoaded – 检测网页中的图片是否加载完成

github: https://github.com/desandro/i...

官方网站: http://imagesloaded.desandro....

star:6k+

Install:

Bower: bower install imagesloaded --save
npm: npm install imagesloaded
CDNs:<script src="https://unpkg.com/imagesloaded@4.1/imagesloaded.pkgd.min.js"></script><script src="https://unpkg.com/imagesloaded@4.1/imagesloaded.pkgd.js"></script>

大小:5 KB

功能介绍:
 imagesLoaded 是一个用于来检测网页中的图片是否载入完成的 JavaScript 工具库。支持回调的获取图片加载的进度,还可以绑定自定义事件。可以结合 jQuery、RequireJS 使用。

Demo:
效果预览地址: http://codepen.io/desandro/fu...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。






Fort.js – 时尚、现代的表单填写进度提示效果

github: https://github.com/idriskhenc...

官方网站: https://github.com/idriskhenc...

star:800+

Install:

CDN:
css:
https://cdnjs.cloudflare.com/ajax/libs/Fort.js/2.0.0/fort.min.css
js:
https://cdnjs.cloudflare.com/ajax/libs/Fort.js/2.0.0/fort.min.js

大小:6 KB

功能介绍:
  Fort.js 是一款用于时尚、现代的表单填写进度提示效果的 JavaScript 库,你需要做的就是添加表单,剩下的任务就交给 Fort.js 算法了,使用非常简单。提供了Default、Gradient、Sections 以及 Flash 四种效果,满足开发的各种场合需要。

Demo:
效果预览地址: http://idriskhenchil.github.i...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。






MagicSuggest – Bootstrap 主题的多选组合框

github: https://github.com/nicolasbiz...

官方网站: http://nicolasbize.com/magics...

star:1k+

Install:

github自行进行下载

大小:21.8 KB

功能介绍:
  MagicSuggest 是专为 Bootstrap 主题开发的多选组合框。它支持自定义呈现,数据通过 Ajax 异步获取,使用组件自动过滤。它允许空间免费项目,也有动态加载固定的建议。

Demo:
效果预览地址: http://nicolasbize.com/magics...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。






Numeral.js – 格式化和操作数字的 JavaScript 库

github: https://github.com/adamwdrape...

官方网站: http://numeraljs.com/

star:4k+

Install:

npm: npm install numeral
CDNs:<script src="//cdnjs.cloudflare.com/ajax/libs/numeral.js/2.0.6/numeral.min.js"></script>

大小:10 KB

功能介绍:
   Numeral.js 是一个用于格式化和操作数字的 JavaScript 库。数字可以格式化为货币,百分比,时间,甚至是小数,千位,和缩写格式,功能十分强大。支持包括中文在内的17种语言。

Demo:

var myNumeral = numeral(1000);

var value = myNumeral.value();
// 1000

var myNumeral2 = numeral('1,000');

var value2 = myNumeral2.value();
// 1000






Draggabilly – 轻松实现拖放功能(Drag & Drop)

github: https://github.com/desandro/d...

官方网站: http://draggabilly.desandro.c...

star:2k+

Install:

Bower: bower install draggabilly --save
npm: npm install draggabilly
CDNs:<script src="https://npmcdn.com/draggabilly@2.1/dist/draggabilly.pkgd.min.js"></script><script src="https://npmcdn.com/draggabilly@2.1/dist/draggabilly.pkgd.js"></script>

大小:5 KB

功能介绍:
 Draggabilly 是一个很小的 JavaScript 库,专注于拖放功能。只需要简单的设置参数就可以在你的网站用添加拖放功能。兼容 IE8+ 浏览器,支持多点触摸。可以灵活绑定事件,支持 RequireJS 以及 Bower 安装。

Demo:
效果预览地址: http://draggabilly.desandro.com/

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。






Quill – 可以灵活自定义的开源的富文本编辑器

github: https://github.com/quilljs/qu...

官方网站: https://quilljs.com

star:12k+

Install:

npm: npm install quill
CDNs:<!-- Main Quill library --><script src="//cdn.quilljs.com/1.0.0/quill.js" type="text/javascript"></script><script src="//cdn.quilljs.com/1.0.0/quill.min.js" type="text/javascript"></script><!-- Theme included stylesheets --><link href="//cdn.quilljs.com/1.0.0/quill.snow.css" rel="stylesheet"><link href="//cdn.quilljs.com/1.0.0/quill.bubble.css" rel="stylesheet"><!-- Core build with no theme, formatting, non-essential modules --><link href="//cdn.quilljs.com/1.0.0/quill.core.css" rel="stylesheet"><script src="//cdn.quilljs.com/1.0.0/quill.core.js" type="text/javascript"></script>

大小:需求不同,大小不同

功能介绍:
  Quill 的建立是为了解决现有的所见即所得(WYSIWYG)的编辑器本身就是所见即所得(指不能再扩张)的问题。如果编辑器不正是你想要的方式,这是很难或不可能对其进行自定义以满足您的需求。

  Quill 旨在通过把自身组织成模块,并提供了强大的 API 来构建额外的模块来解决这个问题。它也并没有规定你用样式来定义编辑器皮肤。Quill 还提供了所有你希望富文本编辑器说用于的功能,包括轻量级封装,众多的格式化选项,以及广泛的跨平台支持。

Demo:
效果预览地址: https://quilljs.com/

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。






basket.js – 基于 LocalStorage 的资源加载器

github: https://github.com/addyosmani...

官方网站: https://addyosmani.com/basket...

star:2k+

Install:

Bower: bower install basket.js --save
npm: npm install basket.js

大小:4 KB

功能介绍:
 basket.js是一款基于 LocalStorage 的资源加载器,可以用来缓存 script 和 css, 手机端使用速度快于浏览器直接缓存。
 
Demo:
效果预览地址: https://addyosmani.com/basket...
更多示例请查看官方文档






scrollReveal.js – 使元素以非常酷帅的方式进入画布 (Viewpoint)

github: https://github.com/jlmakes/sc...

官方网站: https://scrollrevealjs.org/https://github.com/moment/mom...

官方网站: http://momentjs.com/

star:30k+

Install:

bower install moment --save # bower
npm install moment --save   # npm
yarn add moment             # Yarn
Install-Package Moment.js   # NuGet
spm install moment --save   # spm
meteor add momentjs:moment  # meteor

大小:16.6 KB

功能介绍:
  moment.js是一个轻量级的JavaScript库日期解析、验证操作,格式化日期的库。

Demo:
效果预览地址: http://momentjs.com/

Demo效果预览:
这是一个GIF动图,不信,你看第一行的日期,时间在走。






infinite-scroll – 一款滚动加载按需加载的轻量级插件

github: https://github.com/infinite-s...

官方网站: http://www.infinite-scroll.co...

star:4k+

Install:

github自行下载

大小:20 KB

功能介绍:
 infinite-scroll是一款滚动加载,滚动到最下到自动加载的轻量级JavaScript插件,简单实用,按需加载提高用户体验,非常适合移动端使用,配合上面的图片懒加载如虎添翼。

Demo:
效果预览地址: http://www.dazeddigital.com/

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。






欢迎大家按照格式补充,持续更新,有什么好用的轮子赶紧滚起来吧!

推荐有福利,送1024论坛邀请码,嘿嘿嘿。


Web项目如何防止客户端重复发送请求

$
0
0

在Web项目中,有一些请求或操作会对数据产生影响(比如新增、删除、更新),针对这类请求一般都需要做一些保护,以防止用户有意或无意的重复发起这样的请求导致的数据错乱。

本文总结了一些防止客户端重复发送请求的方法。

方法一:JS监听Form的onsubmit事件

在经典场景下,浏览器通过Form发送请求。因此只需要在Form onsubmit时将Submit按钮disable,就能够防止用户双击导致的重复请求(这种问题一般发生在年纪大的用户身上,他们分不清单击和双击)。

但是随着前端的发展,Form以外的请求方式也越来越多,比如利用各种前端框架(Vue、AngularJs、Backbone等)写的App,他们更多的采用的是ajax的方式和后端交互。那么前端开发人员必须在开发时针对每个代表 发起请求的UI元素做处理,像Form一样,在发起请求的时候把相关UI元素 禁用掉。

而有些交互方式则可能连代表 发起请求的UI元素都没有,比如Segmentfault的markdown编辑器就是在一边输入的时候一边保存的。那么这时就需要前端代码采用其他手段来控制重复请求的发生。

优点:

  1. 不需要后端写代码

缺点:

  1. 不存在统一的解决方案,必须针对每种情况写处理代码

  2. 无法控制浏览器刷新发起的重复请求

  3. 前端开发人员忘记写相关代码

  4. 无法控制恶意的重复请求,比如绕过浏览器直接发起

方法二:Http Status Code 302(后端重定向)

服务端采用重定向的方式,防止用户刷新浏览器发出重复请求。这是比较经典的后端控制重复请求的方式,因为一旦 重定向成功后,用户刷新浏览器所刷新的是那个重定向地址,而不是数据操作地址。

优点:

  1. 不需要写前端代码

缺点:

  1. 在还未响应302之前,所发起的重复请求,比如:用户快速的双击、刷新浏览器

  2. 在某些前端程序里(比如SPA),不能使用重定向

  3. 后端开发人员忘记写相关代码

  4. 无法控制恶意的重复请求,比如绕过浏览器直接发起

方法三:结合方法一和方法二

结合方法一和方法二的话倒是可以解决大部分问题,但是解决不了以下问题:

  1. 在还未响应302之前,用户刷新浏览器导致的重复请求

  2. 有些场景下压根不能使用重定向

  3. 前、后端开发人员忘记写相关代码

  4. 无法控制恶意的重复请求,比如绕过浏览器直接发起

方法四:token方式

token的流程是这样的:

  1. 在浏览器发送请求前,先到服务端索要token

  2. 浏览器发送请求时,将token一并提交

  3. 服务端检查请求是否携带token、token是否有效(比如是否正确、是否过期)。如果不正确则响应失败;如果正确则销毁token,继续业务逻辑。

关键点在于:

  1. 每个token都是一次性且有过期时间的,能够防止token前端程序bug造成的重复利用和无限利用,能够避免前端开发人员代码bug。

  2. 服务器要求请求必须携带token,能够避免前端开发人员漏写相关代码

那么token是以怎样的形式传输的呢?我认为有以下两种方式:

Cookie

推荐使用这种方式,因为浏览器每次都会将cookie携带在请求里一并发出,所以前端发送请求的代码都不需要修改,只要在发送请求前问服务器拿token就行了。

比如在进入Form页面时,服务器将token以cookie的形式一并携带在响应中,那么前端Form提交时,就会将cookie一并携带在请求中,前端的代码一点都不需要修改。

json

前端发起ajax请求像后端拿token,后端以json的形式返回token,前端发送请求时将token携带在请求中,后端检验。

这种方式比Cookie稍微麻烦的地方是,前端必须写一些代码来保存这个token,然后在发送请求的地方要写一些代码把token携带在请求里。

优点

  1. 前端代码可以写的少一些,比如禁用UI元素的代码可以不写

  2. 能够解决在还未响应302之前,用户刷新浏览器导致的重复请求

  3. 适应有些场景下压根不能使用重定向

缺点

  1. 前、后端开发人员忘记写相关代码。这个真的解决不了。

  2. 无法控制通过脚本运行的,具有整套流程的恶意请求。这种请求在程序看来完全合法,但却属于恶意行为,针对这类恶意行为的防控属于另一个话题,本人不懂,所以在这里就不多讲了。

H5与Native交互之JSBridge技术

$
0
0

做过混合开发的很多人都知道Ionic和PhoneGap之类的框架,这些框架在web基础上包了一层Native,然后通过Bridge技术使得js可以调用视频、位置、音频等功能。本文就是介绍这层Bridge的交互原理,通过阅读本文你可以了解到js与ios及android底层的通讯原理及JSBridge的封装技术及调试方法。

一、原理篇

下面分别介绍IOS和Android与Javascript的底层交互原理

IOS

在讲解原理之前,首先来了解下iOS的UIWebView组件,先来看一下苹果官方的介绍:

You can use the UIWebView class to embed web content in your application. To do so, you simply create a UIWebView object, attach it to a window, and send it a request to load web content. You can also use this class to move back and forward in the history of webpages, and you can even set some web content properties programmatically.

上面的意思是说UIWebView是一个可加载网页的对象,它有浏览记录功能,且对加载的网页内容是可编程的。说白了UIWebView有类似浏览器的功能,我们使用可以它来打开页面,并做一些定制化的功能,如可以让js调某个方法可以取到手机的GPS信息。

但需要注意的是,Safari浏览器使用的浏览器控件和UIwebView组件并不是同一个,两者在性能上有很大的差距。幸运的是,苹果发布iOS8的时候,新增了一个WKWebView组件,如果你的APP只考虑支持iOS8及以上版本,那么你就可以使用这个新的浏览器控件了。

原生的UIWebView类提供了下面一些属性和方法

属性:

  • loading:是否处于加载中
  • canGoBack:A Boolean value indicating whether the receiver can move backward. (只读)
  • canGoForward:A Boolean value indicating whether the receiver can move forward. (只读)
  • request:The URL request identifying the location of the content to load. (read-only)

方法:

  • loadData:Sets the main page contents, MIME type, content encoding, and base URL.
  • loadRequest:加载网络内容
  • loadHTMLString:加载本地HTML文件
  • stopLoading:停止加载
  • goBack:后退
  • goForward:前进
  • reload:重新加载
  • stringByEvaluatingJavaScriptFromString:执行一段js脚本,并且返回执行结果

Native(Objective-C或Swift)调用Javascript方法

Native调用Javascript语言,是通过 UIWebView组件的 stringByEvaluatingJavaScriptFromString方法来实现的,该方法返回js脚本的执行结果。

// Swift
webview.stringByEvaluatingJavaScriptFromString("Math.random()")
// OC
[webView stringByEvaluatingJavaScriptFromString:@"Math.random();"];

从上面代码可以看出它其实就是调用了 window下的一个对象,如果我们要让native来调用我们js写的方法,那这个方法就要在 window下能访问到。但从全局考虑,我们只要暴露一个对象如JSBridge对native调用就好了,所以在这里可以对native的代码做一个简单的封装:

//下面为伪代码
webview.setDataToJs(somedata);
webview.setDataToJs = function(data) {
 webview.stringByEvaluatingJavaScriptFromString("JSBridge.trigger(event, data)")
}

Javascript调用Native(Objective-C或Swift)方法

反过来,Javascript调用Native,并没有现成的API可以直接拿来用,而是需要间接地通过一些方法来实现。UIWebView有个特性:在UIWebView内发起的所有网络请求,都可以通过delegate函数在Native层得到通知。这样,我们就可以在UIWebView内发起一个自定义的网络请求,通常是这样的格式:jsbridge://methodName?param1=value1&param2=value2

于是在UIWebView的delegate函数中,我们只要发现是jsbridge://开头的地址,就不进行内容的加载,转而执行相应的调用逻辑。

发起这样一个网络请求有两种方式:1. 通过localtion.href;2. 通过iframe方式;
通过location.href有个问题,就是如果我们连续多次修改window.location.href的值,在Native层只能接收到最后一次请求,前面的请求都会被忽略掉。

使用iframe方式,以唤起Native APP的分享组件为例,简单的封闭如下:

var url = 'jsbridge://doAction?title=分享标题&desc=分享描述&link=http%3A%2F%2Fwww.baidu.com';
var iframe = document.createElement('iframe');
iframe.style.width = '1px';
iframe.style.height = '1px';
iframe.style.display = 'none';
iframe.src = url;
document.body.appendChild(iframe);
setTimeout(function() {
    iframe.remove();
}, 100);

然后Webview就可以拦截这个请求,并且解析出相应的方法和参数。如下代码所示:

func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
        print("shouldStartLoadWithRequest")
        let url = request.URL
        let scheme = url?.scheme
        let method = url?.host
        let query = url?.query
        if url != nil && scheme == "jsbridge" {
            print("scheme == \(scheme)")
            print("method == \(method)")
            print("query == \(query)")

            switch method! {
                case "getData":
                    self.getData()
                case "putData":
                    self.putData()
                case "gotoWebview":
                    self.gotoWebview()
                case "gotoNative":
                    self.gotoNative()
                case "doAction":
                    self.doAction()
                case "configNative":
                    self.configNative()
                default:
                    print("default")
            }
            return false
        } else {
            return true
        }
    }

Android

在android中,native与js的通讯方式与ios类似,ios中的通过schema方式在android中也是支持的。

javascript调用native方式

目前在android中有三种调用native的方式:

1.通过schema方式,使用 shouldOverrideUrlLoading方法对url协议进行解析。这种js的调用方式与ios的一样,使用iframe来调用native代码。
2.通过在webview页面里直接注入原生js代码方式,使用 addJavascriptInterface方法来实现。
在android里实现如下:

class JSInterface {
    @JavascriptInterface //注意这个代码一定要加上
    public String getUserData() {
        return "UserData";
    }
}
webView.addJavascriptInterface(new JSInterface(), "AndroidJS");

上面的代码就是在页面的window对象里注入了 AndroidJS对象。在js里可以直接调用

alert(AndroidJS.getUserData()) //UserDate

3.使用prompt,console.log,alert方式,这三个方法对js里是属性原生的,在android webview这一层是可以重写这三个方法的。一般我们使用prompt,因为这个在js里使用的不多,用来和native通讯副作用比较少。

class YouzanWebChromeClient extends WebChromeClient {
    @Override
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
        // 这里就可以对js的prompt进行处理,通过result返回结果
    }
    @Override
    public boolean onConsoleMessage(ConsoleMessage consoleMessage) {

    }
    @Override
    public boolean onJsAlert(WebView view, String url, String message, JsResult result) {

    }

}

Native调用javascript方式

在android里是使用webview的 loadUrl进行调用的,如:

// 调用js中的JSBridge.trigger方法
webView.loadUrl("javascript:JSBridge.trigger('webviewReady')");

二、库的封装

js调用native的封装

上面我们了解了js与native通讯的底层原理,所以我们可以封装一个基础的通讯方法 doCall来屏蔽android与ios的差异。

YouzanJsBridge = {
    doCall: function(functionName, data, callback) {
        var _this = this;
        // 解决连续调用问题
        if (this.lastCallTime && (Date.now() - this.lastCallTime) < 100) {
            setTimeout(function() {
                _this.doCall(functionName, data, callback);
            }, 100);
            return;
        }
        this.lastCallTime = Date.now();
        data = data || {};
        if (callback) {
            $.extend(data, { callback: callback });
        }
        if (UA.isIOS()) {
            $.each(data, function(key, value) {
                if ($.isPlainObject(value) || $.isArray(value)) {
                    data[key] = JSON.stringify(value);
                }
            });
            var url = Args.addParameter('youzanjs://' + functionName, data);
            var iframe = document.createElement('iframe');
            iframe.style.width = '1px';
            iframe.style.height = '1px';
            iframe.style.display = 'none';
            iframe.src = url;
            document.body.appendChild(iframe);
            setTimeout(function() {
                iframe.remove();
            }, 100);
        } else if (UA.isAndroid()) {
            window.androidJS && window.androidJS[functionName] && window.androidJS[functionName](JSON.stringify(data));
        } else {
            console.error('未获取platform信息,调取api失败');
        }
    }
}

上面android端我们使用了addJavascriptInterface方法来注入一个AndroidJS对象。

项目通用方法抽象

在项目的实践中,我们逐渐抽象出一些通用的方法,这些方法基本上都是可以满足项目的需求。如下所示:

1.getData(datatype, callback, extra) H5从Native APP获取数据

使用场景:H5需要从Native APP获取某些数据的时候,可以调用这个方法。

参数类型是否必须示例值说明
datatypeStringuserInfo数据类型
callbackFunction回调函数
extraObject传递给Native APP的数据对象

示例代码:

JSBridge.getData('userInfo',function(data) {
    console.log(data);
});

2.putData(datatype, data) H5告诉Native APP一些数据

使用场景:H5告诉Native APP一些数据,可以调用这个方法。

参数类型是否必须示例值说明
datatypeStringuserInfo数据类型
dataObject{ username: 'zhangsan', age: 20 }传递给Native APP的数据对象

示例代码:

JSBridge.putData('userInfo', {
    username: 'zhangsan',
    age: 20
});

3.gotoWebview(url, page, data) Native APP新开一个Webview窗口,并打开相应网页

参数类型是否必须示例值说明
urlStringhttp://www.youzan.com网页链接地址,一般都只要传递URL参数就可以了
pageStringweb网页page类型,默认为web
dataObject额外参数对象

示例代码:

// 示例1:打开一个网页
JSBridge.gotoWebview('http://www.youzan.com');

// 示例2:打开一个网页,并且传递额外的参数给Native APP
JSBridge.gotoWebview('http://www.youzan.com', 'goodsDetail', {
    goods_id: 10000,
    title: '这是商品的标题',
    desc: '这是商品的描述'
});

4.gotoNative(page, data) 从H5页面跳转到Native APP的某个原生界面

参数类型是否必须示例值说明
pageStringloginPageNative页面标示符,例如loginPage
dataObject{ username: 'zhangsan', age: 20 }额外参数对象

示例代码:

// 示例1:打开Native APP登录页面
JSBridge.gotoNative('loginPage');

// 示例2:打开Native APP登录页面,并且传递用户名给Native APP
JSBridge.gotoNative('loginPage', {
    username: '张三'
});

5.doAction(action, data) 功能上的一些操作

参数类型是否必须示例值说明
actionStringcopy操作功能类型,例如分享、复制
dataObject{ content: '这是要复制的内容' }额外参数

示例代码:

// 示例1:调用Native APP复制一段文本到剪切板
JSBridge.doAction('copy', {
    content: '这是要复制的内容'
});

// 示例2:调用Native APP的分享组件,分享当前网页到微信
JSBridge.doAction('share', {
    title: '分享标题',
    desc: '分享描述',
    link: 'http://www.youzan.com',
    imgs_url: 'http://wap.koudaitong.com/v2/common/url/create?type=homepage&index%2Findex=&kdt_id=63077&alias=63077'
});

三、调试篇

使用Safari进行UIWebView的调试

(1)首先需要打开Safari的调试模式,在Safari的菜单中,选择“Safari”→“Preference”→“Advanced”,勾选上“Show Develop menu in menu bar”选项,如下图所示。
2-1
(2)打开真机或iPhone模拟器的调试模式,在真机或iPhone模拟器中打开设置界面,选择“Safari”→“高级”→“Web检查器”,选择开启即可,如下图所示。
2-2
(3)将真机通过USB连上电脑,或者开启模拟器,Safari的“Develop”菜单下便会多出相应的菜单项,如图所示。

Paste_Image.png

(4)Safari连接上UIWebView之后,我们就可以直接在Safari中直接修改HTML、CSS,以及调试Javascript。

Paste_Image.png

四、参考链接

本文由 @kk @劲风 共同创作

vue快速入门的三个小实例

$
0
0

1.前言

用vue做项目也有一段时间了,之前也是写过关于vue和webpack构建项目的相关文章,大家有兴趣可以去看下 webpack+vue项目实战(一,搭建运行环境和相关配置)(这个系列一共有5篇文章,这是第一篇,其它几篇文章链接就不贴了)。但是关于vue入门基础的文章,我还没有写过,那么今天就写vue入门的三个小实例,这三个小实例是我刚接触vue的时候的练手作品,难度从很简单到简单,都是入门级的。希望能帮到大家更好的学习和了解vue,也是让自己能够复习一下vue。如果发现文章写得有什么不好,写错了,或者有什么建议!欢迎大家指点迷津!

1.本篇文章使用的vue版本是 2.4.2,大家要注意版本问题
2.现在我也是假设您有基础的html,css,javascript的知识,也已经看过了 官网的基本介绍,对vue有了一个大概的认识了,了解了常用的vue指令(v-model,v-show,v-if,v-for,v-on,v-bind等)!如果刚接触前端的话,你看着文章可能会蒙圈,建议先学习基础,掌握了基础知识再来看!
3.下面的实例,建议大家边看文章边动手做!这样思路会非常清晰,不易混乱!也不会觉得文章长!如果只看文章,你可能未必会看完,因为文章我讲得比较细,比较长!

2.什么是vue

vue是现在很火的一个前端MVVM框架,它以数据驱动和组件化的思想构建,与angular和react并称前端三大框架。相比angular和react,vue更加轻巧、高性能、也很容易上手。大家也可以移步,看一下vue的介绍和核心功能 官网介绍。简单粗暴的理解就是:用vue开发的时候,就是操作数据,然后vue就会处理,以数据驱动去改变DOM(不知道有没有理解错,理解错了指点下)。
下面就是一个最简单的说明例子

代码如下

html

<div id="app"><p>{{ message }}</p><input v-model="message"></div>

js

new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})

栗子

相信也不难理解,就是 input绑定了 message这个值,然后在 input修改的时候, message就改了,由于双向绑定,同时页面的html( {{ message }})进行了修改!
好,下面进入例子学习!

3.选项卡

运行效果

clipboard.png

原理分析和实现

这个很简单,无非就是一个点击切换显示而已。但是大家也要实现。如果这个看明白了,再看下面两个!这个实例应该只是一个热身和熟悉的作用!

这个的步骤只有一步,原理也没什么。我直接在代码打注释,看了注释,大家就明白了!

完整代码

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

<meta charset="UTF-8"><title>Title</title>

</head>
<style>

body{
    font-family:"Microsoft YaHei";
}
#tab{
    width: 600px;
    margin: 0 auto;
}
.tab-tit{
    font-size: 0;
    width: 600px;
}
.tab-tit a{
    display: inline-block;
    height: 40px;
    line-height: 40px;
    font-size: 16px;
    width: 25%;
    text-align: center;
    background: #ccc;
    color: #333;
    text-decoration: none;
}
.tab-tit .cur{
    background: #09f;
    color: #fff;
}
.tab-con div{
    border: 1px solid #ccc;
    height: 400px;
    padding-top: 20px;
}

</style>
<body>
<div id="tab">

<div class="tab-tit"><!--点击设置curId的值  如果curId等于0,第一个a添加cur类名,如果curId等于1,第二个a添加cur类名,以此类推。添加了cur类名,a就会改变样式 @click,:class ,v-show这三个是vue常用的指令或添加事件的方式--><a href="javascript:;" @click="curId=0" :class="{'cur':curId===0}">html</a><a href="javascript:;" @click="curId=1" :class="{'cur':curId===1}">css</a><a href="javascript:;" @click="curId=2" :class="{'cur':curId===2}">javascript</a><a href="javascript:;" @click="curId=3" :class="{'cur':curId===3}">vue</a></div><div class="tab-con"><!--根据curId的值显示div,如果curId等于0,第一个div显示,其它三个div不显示。如果curId等于1,第二个div显示,其它三个div不显示。以此类推--><div v-show="curId===0">
        html<br/></div><div v-show="curId===1">
        css</div><div v-show="curId===2">
        javascript</div><div v-show="curId===3">
        vue</div></div>

</div>
</body>
<script src="vue.min.js"></script>
<script>

new Vue({
    el: '#tab',
    data: {
        curId: 0
    },
    computed: {},
    methods: {},
    mounted: function () {
    }
})

</script>
</html>

4.统计总价

运行效果

clipboard.png

原理分析和实现

首先,还是先把布局写好,和引入vue,准备vue实例,这个不多说,代码如下

<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Title</title><style>
        .fl{
            float: left;
        }
        .fr{
            float: right;
        }
       blockquote, body, dd, div, dl, dt, fieldset, form, h1, h2, h3, h4, h5, h6, img, input, li, ol, p, table, td, textarea, th, ul {
            margin: 0;
            padding: 0;
        }
       .clearfix{
          zoom: 1;
       }
        .clearfix:after {
            clear: both;
        }
        .clearfix:after {
            content: '.';
            display: block;
            overflow: hidden;
            visibility: hidden;
            font-size: 0;
            line-height: 0;
            width: 0;
            height: 0;
        }
        a{
            text-decoration: none;
            color: #333;
        }
        img{vertical-align: middle;}
        .page-shopping-cart {
            width: 1200px;
            margin: 50px auto;
            font-size: 14px;
            border: 1px solid #e3e3e3;
            border-top: 2px solid #317ee7; }
        .page-shopping-cart .cart-title {
            color: #317ee7;
            font-size: 16px;
            text-align: left;
            padding-left: 20px;
            line-height: 68px; }
        .page-shopping-cart .red-text {
            color: #e94826; }
        .page-shopping-cart .check-span {
            display: block;
            width: 24px;
            height: 20px;
            background: url("shopping_cart.png") no-repeat 0 0; }
        .page-shopping-cart .check-span.check-true {
            background: url("shopping_cart.png") no-repeat 0 -22px; }
        .page-shopping-cart .td-check {
            width: 70px; }
        .page-shopping-cart .td-product {
            width: 460px; }
        .page-shopping-cart .td-num, .page-shopping-cart .td-price, .page-shopping-cart .td-total {
            width: 160px; }
        .page-shopping-cart .td-do {
            width: 150px; }
        .page-shopping-cart .cart-product-title {
            text-align: center;
            height: 38px;
            line-height: 38px;
            padding: 0 20px;
            background: #f7f7f7;
            border-top: 1px solid #e3e3e3;
            border-bottom: 1px solid #e3e3e3; }
        .page-shopping-cart .cart-product-title .td-product {
            text-align: center;
            font-size: 14px; }
        .page-shopping-cart .cart-product-title .td-check {
            text-align: left; }
        .page-shopping-cart .cart-product-title .td-check .check-span {
            margin: 9px 6px 0 0; }
        .page-shopping-cart .cart-product {
            padding: 0 20px;
            text-align: center; }
        .page-shopping-cart .cart-product table {
            width: 100%;
            text-align: center;
            font-size: 14px; }
        .page-shopping-cart .cart-product table td {
            padding: 20px 0; }
        .page-shopping-cart .cart-product table tr {
            border-bottom: 1px dashed #e3e3e3; }
        .page-shopping-cart .cart-product table tr:last-child {
            border-bottom: none; }
        .page-shopping-cart .cart-product table .product-num {
            border: 1px solid #e3e3e3;
            display: inline-block;
            text-align: center; }
        .page-shopping-cart .cart-product table .product-num .num-do {
            width: 24px;
            height: 28px;
            display: block;
            background: #f7f7f7; }
        .page-shopping-cart .cart-product table .product-num .num-reduce span {
            background: url("shopping_cart.png") no-repeat -40px -22px;
            display: block;
            width: 6px;
            height: 2px;
            margin: 13px auto 0 auto; }
        .page-shopping-cart .cart-product table .product-num .num-add span {
            background: url("shopping_cart.png") no-repeat -60px -22px;
            display: block;
            width: 8px;
            height: 8px;
            margin: 10px auto 0 auto; }
        .page-shopping-cart .cart-product table .product-num .num-input {
            width: 42px;
            height: 28px;
            line-height: 28px;
            border: none;
            text-align: center; }
        .page-shopping-cart .cart-product table .td-product {
            text-align: left;
            font-size: 12px;
            line-height: 20px; }
        .page-shopping-cart .cart-product table .td-product img {
            border: 1px solid #e3e3e3;
            margin-right: 10px; }
        .page-shopping-cart .cart-product table .td-product .product-info {
            display: inline-block;
            vertical-align: middle; }
        .page-shopping-cart .cart-product table .td-do {
            font-size: 12px; }
        .page-shopping-cart .cart-product-info {
            height: 50px;
            line-height: 50px;
            background: #f7f7f7;
            padding-left: 20px; }
        .page-shopping-cart .cart-product-info .delect-product {
            color: #666; }
        .page-shopping-cart .cart-product-info .delect-product span {
            display: inline-block;
            vertical-align: top;
            margin: 18px 8px 0 0;
            width: 13px;
            height: 15px;
            background: url("shopping_cart.png") no-repeat -60px 0; }
        .page-shopping-cart .cart-product-info .product-total {
            font-size: 14px;
            color: #e94826; }
        .page-shopping-cart .cart-product-info .product-total span {
            font-size: 20px; }
        .page-shopping-cart .cart-product-info .check-num {
            color: #333; }
        .page-shopping-cart .cart-product-info .check-num span {
            color: #e94826; }
        .page-shopping-cart .cart-product-info .keep-shopping {
            color: #666;
            margin-left: 40px; }
        .page-shopping-cart .cart-product-info .keep-shopping span {
            display: inline-block;
            vertical-align: top;
            margin: 18px 8px 0 0;
            width: 15px;
            height: 15px;
            background: url("shopping_cart.png") no-repeat -40px 0; }
        .page-shopping-cart .cart-product-info .btn-buy {
            height: 50px;
            color: #fff;
            font-size: 20px;
            display: block;
            width: 110px;
            background: #ff7700;
            text-align: center;
            margin-left: 30px; }
        .page-shopping-cart .cart-worder {
            padding: 20px; }
        .page-shopping-cart .cart-worder .choose-worder {
            color: #fff;
            display: block;
            background: #39e;
            width: 140px;
            height: 40px;
            line-height: 40px;
            border-radius: 4px;
            text-align: center;
            margin-right: 20px; }
        .page-shopping-cart .cart-worder .choose-worder span {
            display: inline-block;
            vertical-align: top;
            margin: 9px 10px 0 0;
            width: 22px;
            height: 22px;
            background: url("shopping_cart.png") no-repeat -92px 0; }
        .page-shopping-cart .cart-worder .worker-info {
            color: #666; }
        .page-shopping-cart .cart-worder .worker-info img {
            border-radius: 100%;
            margin-right: 10px; }
        .page-shopping-cart .cart-worder .worker-info span {
            color: #000; }

        .choose-worker-box {
            width: 620px;
            background: #fff; }
        .choose-worker-box .box-title {
            height: 40px;
            line-height: 40px;
            background: #F7F7F7;
            text-align: center;
            position: relative;
            font-size: 14px; }
        .choose-worker-box .box-title a {
            display: block;
            position: absolute;
            top: 15px;
            right: 16px;
            width: 10px;
            height: 10px;
            background: url("shopping_cart.png") no-repeat -80px 0; }
        .choose-worker-box .box-title a:hover {
            background: url("shopping_cart.png") no-repeat -80px -22px; }
        .choose-worker-box .worker-list {
            padding-top: 30px;
            height: 134px;
            overflow-y: auto; }
        .choose-worker-box .worker-list li {
            float: left;
            width: 25%;
            text-align: center;
            margin-bottom: 30px; }
        .choose-worker-box .worker-list li p {
            margin-top: 8px; }
        .choose-worker-box .worker-list li.cur a {
            color: #f70; }
        .choose-worker-box .worker-list li.cur a img {
            border: 1px solid #f70; }
        .choose-worker-box .worker-list li a:hover {
            color: #f70; }
        .choose-worker-box .worker-list li a:hover img {
            border: 1px solid #f70; }
        .choose-worker-box .worker-list li img {
            border: 1px solid #fff;
            border-radius: 100%; }</style></head><body><div class="page-shopping-cart" id="shopping-cart"><h4 class="cart-title">购物清单</h4><div class="cart-product-title clearfix"><div class="td-check fl"><span class="check-span fl check-all"></span>全选</div><div class="td-product fl">商品</div><div class="td-num fl">数量</div><div class="td-price fl">单价(元)</div><div class="td-total fl">金额(元)</div><div class="td-do fl">操作</div></div><div class="cart-product clearfix"><table><tbody><tr><td class="td-check"><span class="check-span"></span></td><td class="td-product"><img src="testimg.jpg" width="98" height="98"><div class="product-info"><h6>【斯文】甘油&nbsp;|&nbsp;丙三醇</h6><p>品牌:韩国skc&nbsp;&nbsp;产地:韩国</p><p>规格/纯度:99.7%&nbsp;&nbsp;起定量:215千克</p><p>配送仓储:上海仓海仓储</p></div><div class="clearfix"></div></td><td class="td-num"><div class="product-num"><a href="javascript:;" class="num-reduce num-do fl"><span></span></a><input type="text" class="num-input" value="3"><a href="javascript:;" class="num-add num-do fr"><span></span></a></div></td><td class="td-price"><p class="red-text">¥<span class="price-text">800</span>.00</p></td><td class="td-total"><p class="red-text">¥<span class="total-text">800</span>.00</p></td><td class="td-do"><a href="javascript:;" class="product-delect">删除</a></td></tr><tr><td class="td-check"><span class="check-span check-true"></span></td><td class="td-product"><img src="testimg.jpg" width="98" height="98"><div class="product-info"><h6>【斯文】甘油&nbsp;|&nbsp;丙三醇</h6><p>品牌:韩国skc&nbsp;&nbsp;产地:韩国</p><p>规格/纯度:99.7%&nbsp;&nbsp;起定量:215千克</p><p>配送仓储:上海仓海仓储</p></div><div class="clearfix"></div></td><td class="td-num"><div class="product-num"><a href="javascript:;" class="num-reduce num-do fl"><span></span></a><input type="text" class="num-input" value="1"><a href="javascript:;" class="num-add num-do fr"><span></span></a></div></td><td class="td-price"><p class="red-text">¥<span class="price-text">800</span>.00</p></td><td class="td-total"><p class="red-text">¥<span class="total-text">800</span>.00</p></td><td class="td-do"><a href="javascript:;" class="product-delect">删除</a></td></tr></tbody></table></div><div class="cart-product-info"><a class="delect-product" href="javascript:;"><span></span>删除所选商品</a><a class="keep-shopping" href="#"><span></span>继续购物</a><a class="btn-buy fr" href="javascript:;">去结算</a><p class="fr product-total">¥<span>1600</span></p><p class="fr check-num"><span>2</span>件商品总计(不含运费):</p></div><div class="cart-worder clearfix"><a href="javascript:;" class="choose-worder fl"><span></span>绑定跟单员</a><div class="worker-info fl"></div></div></div></body><script src="vue.min.js"></script><script>
    new Vue({
        el:'#shopping-cart',
        data:{

        },
        computed: {},
        methods:{
            
        }
    })
</script></html>

然后准备下列表数据,根据下面表格的箭头

clipboard.png

所以大家就知道吗,下面的数据大概是涨这样

productList:[
    {'pro_name': '【斯文】甘油 | 丙三醇',//产品名称
        'pro_brand': 'skc',//品牌名称
        'pro_place': '韩国',//产地
        'pro_purity': '99.7%',//规格
        'pro_min': "215千克",//最小起订量
        'pro_depot': '上海仓海仓储',//所在仓库
        'pro_num': 3,//数量
        'pro_img': '../../images/ucenter/testimg.jpg',//图片链接
        'pro_price': 800//单价
    }
]

准备了这么多,大家可能想到,还缺少一个,就是记录产品是否有选中,但是这个字段,虽然可以在上面那里加,但是意义不大,比如在平常项目那里!后台的数据不会这样返回,数据库也不会有这个字段,这个字段应该是自己添加的。代码如下

new Vue({
    el:'#shopping-cart',
    data:{
        productList:[
            {'pro_name': '【斯文】甘油 | 丙三醇',//产品名称
                'pro_brand': 'skc',//品牌名称
                'pro_place': '韩国',//产地
                'pro_purity': '99.7%',//规格
                'pro_min': "215千克",//最小起订量
                'pro_depot': '上海仓海仓储',//所在仓库
                'pro_num': 3,//数量
                'pro_img': '../../images/ucenter/testimg.jpg',//图片链接
                'pro_price': 800//单价
            }
        ]
    },
    computed: {},
    methods:{

    },
    mounted:function () {
        //为productList添加select(是否选中)字段,初始值为true
        this.productList.map(function(item){item.select=true;console.log(item)})
    }
})
          

步骤1

为了着重表示我修改了什么地方,代码我现在只贴出修改的部分,大家对着上面的布局,就很容易知道我改的是什么地方了!下面也是这样操作!

点击增加和减少按钮(箭头指向地方),所属列的金额改变(红框地方)
clipboard.png

执行步骤1之前,要先把列表的数据给铺出来。利用v-for指令。代码如下

<tr v-for="item in productList"><td class="td-check"><span class="check-span"></span></td><td class="td-product"><img :src="item.pro_img" width="98" height="98"><div class="product-info"><h6>{{item.pro_name}}</h6><p>品牌:{{item.pro_brand}}&nbsp;&nbsp;产地:{{item.pro_place}}</p><p>规格/纯度:{{item.pro_purity}}&nbsp;&nbsp;起定量:{{item.pro_min}}</p><p>配送仓储:{{item.pro_depot}}</p></div><div class="clearfix"></div></td><td class="td-num"><div class="product-num"><a href="javascript:;" class="num-reduce num-do fl" @click="item.pro_num--"><span></span></a><input type="text" class="num-input" v-model="item.pro_num"><a href="javascript:;" class="num-add num-do fr" @click="item.pro_num++"><span></span></a></div></td><td class="td-price"><p class="red-text">¥<span class="price-text">{{item.pro_price.toFixed(2)}}</span></p></td><td class="td-total"><p class="red-text">¥<span class="total-text">{{item.pro_price*item.pro_num}}</span>.00</p></td><td class="td-do"><a href="javascript:;" class="product-delect">删除</a></td></tr>

这样,列表的数据就有了!

clipboard.png

也可以发现, clipboard.png这两个按钮的功能已经实现了,后面的金额也会发生变化!是不是感到很惊喜!其实这里没什么特别的,就是因为输入框利用v-model绑定了数量( pro_num),然后两个按钮分别添加了事件 @click="item.pro_num--"和@ click="item.pro_num++"。比如刚开始pro_num是3,点击 clipboard.pngpro_num就变成2,点击 clipboard.png
pro_num就变成4,然后后面的金额会改改,是因为 {{item.pro_price*item.pro_num}}。只要pro_price或者pro_num的值改变了,整一块也会改变,视图就会刷新,我们就能看到变化(这些事情是vue做的,这就是MVVM的魅力,数据驱动视图改变)。

步骤2

点击所属列选择按钮(箭头指向地方),总计的金额(红框地方)和已选产品的列数(蓝框地方)和全选(黄框地方)会改变(如果已经全选了,全选按钮自动变成全选,如果没有全选,全选按钮,自动取消全选)!

clipboard.png

首先,选择与取消选择,在这里只有两个操作(其实只有一个:改变这条记录的 select字段)。

clipboard.png

然后改变 clipboard.png,如果这条记录 selectfalse,就显示 clipboard.png,否则就显示 clipboard.png
代码如下

<td class="td-check"><span class="check-span" @click="item.select=!item.select" :class="{'check-true':item.select}"></span></td>

其实就是等于添加了 @click="item.select=!item.select" :class="{'check-true':item.select}"这里。点击这个,这条数据的 select字段就取反(true->false或者false->true)。然后 :class="{'check-true':item.select}",就会根据这条数据的 select字段进行判断,是否添加 check-true类名,如果 select字段为true,就添加类名,显示 clipboard.png。否则不添加类名,显示
clipboard.png

然后, clipboard.png全选按钮,是否变成 clipboard.png。这里用一个computed(计算属性)就好。代码如下

html

<div class="td-check fl"><span class="check-span fl check-all" :class="{'check-true':isSelectAll}"></span>全选</div>

js

computed: {
    isSelectAll:function(){
        //如果productList中每一条数据的select都为true,返回true,否则返回false;
        return this.productList.every(function (val) { return val.select});
    }
}

代码我解释下,就是计算属性中,定义的isSelectAll依赖productList。只要productList改变,isSelectAll的返回值就会改变,然后 :class="{'check-true':isSelectAll}"根绝isSelectAll返回值是否添加 'check-true'类名,显示对应的样式!
最后, clipboard.png,这里的多少件产品和总价,也是使用计算属性,有了上一步的基础,给出代码,大家一看就明白了!
html

<p class="fr product-total">¥<span>{{getTotal.totalPrice}}</span></p><p class="fr check-num"><span>{{getTotal.totalNum}}</span>件商品总计(不含运费):</p>

js

computed: {
    //检测是否全选
    isSelectAll:function(){
        //如果productList中每一条数据的select都为true,返回true,否则返回false;
        return this.productList.every(function (val) { return val.select});
    },
    //获取总价和产品总件数
    getTotal:function(){
        //获取productList中select为true的数据。
        var _proList=this.productList.filter(function (val) { return val.select}),totalPrice=0;
        for(var i=0,len=_proList.length;i<len;i++){
            //总价累加
            totalPrice+=_proList[i].pro_num*_proList[i].pro_price;
        }
        //选择产品的件数就是_proList.length,总价就是totalPrice
        return {totalNum:_proList.length,totalPrice:totalPrice}
    }
},

代码很简单,html根据getTotal返回值显示数据,getTotal依赖productList的数据,只要productList改变,返回值会改变,视图也会改变!

clipboard.png

步骤3

点击全选按钮(箭头指向部分),会自动的对产品进行全选或者取消全选,下面的总计也会发生改变

clipboard.png
做到这一步,大家应该知道,全选或者取消全选,就是改变记录的 select。但是怎么知道现在的列表有没有全选呢?这个很贱,不需要在操作函数(全选与取消全选函数)里面遍历,大家应该还记得第二步的计算属性 isSelectAll(为true就是全选,否则不是全选),把这个传进操作函数就好,然后操作函数,根据参数,决定执行全选,还是取消全选操作。代码如下!
html

<div class="td-check fl"><span class="check-span fl check-all" :class="{'check-true':isSelectAll}" @click="selectProduct(isSelectAll)"></span>全选</div>

js

 methods: {
    //全选与取消全选
    selectProduct:function(_isSelect){
        //遍历productList,全部取反
        for (var i = 0, len = this.productList.length; i < len; i++) {
            this.productList[i].select = !_isSelect;
        }
    }
},

clipboard.png

步骤4

点击删除产品,会删除已经选中的,全选按钮和下面的总计,都会变化!点击每条记录后面的删除,会删除当前的这条记录。全选按钮和下面的总计,也都会变化!

clipboard.png

首先,点击删除产品,删除已经选中。这个大家知道了怎么做了!就是遍历productList,如果哪条记录的select为true,就删除。
然后,点击每条记录后面的删除,删除当前的这条记录。这个在html遍历productList的时候。顺便带上索引,然后把索引当成参数,传进操作函数,然后根据索引参数,删除productList的哪一条记录。即可实现!代码如下!
html

<!--遍历的时候带上索引--><tr v-for="(item,index) in productList"><td class="td-check"><span class="check-span" @click="item.select=!item.select" :class="{'check-true':item.select}"></span></td><td class="td-product"><img :src="item.pro_img" width="98" height="98"><div class="product-info"><h6>{{item.pro_name}}</h6><p>品牌:{{item.pro_brand}}&nbsp;&nbsp;产地:{{item.pro_place}}</p><p>规格/纯度:{{item.pro_purity}}&nbsp;&nbsp;起定量:{{item.pro_min}}</p><p>配送仓储:{{item.pro_depot}}</p></div><div class="clearfix"></div></td><td class="td-num"><div class="product-num"><a href="javascript:;" class="num-reduce num-do fl" @click="item.pro_num--"><span></span></a><input type="text" class="num-input" v-model="item.pro_num"><a href="javascript:;" class="num-add num-do fr" @click="item.pro_num++"><span></span></a></div></td><td class="td-price"><p class="red-text">¥<span class="price-text">{{item.pro_price.toFixed(2)}}</span></p></td><td class="td-total"><p class="red-text">¥<span class="total-text">{{item.pro_price*item.pro_num}}</span>.00</p></td><td class="td-do"><a href="javascript:;" class="product-delect" @click="deleteOneProduct(index)">删除</a></td></tr>
...<a class="delect-product" href="javascript:;" @click="deleteProduct"><span></span>删除所选商品</a>

js

//删除已经选中(select=true)的产品
deleteProduct:function () {
    this.productList=this.productList.filter(function (item) {return !item.select})
},
//删除单条产品
deleteOneProduct:function (index) {
    //根据索引删除productList的记录
    this.productList.splice(index,1);
},

完整代码

样式图片

clipboard.png

<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Title</title><style>
            .fl {
                float: left;
            } 
            .fr {
                float: right;
            }
            blockquote, body, dd, div, dl, dt, fieldset, form, h1, h2, h3, h4, h5, h6, img, input, li, ol, p, table, td, textarea, th, ul {
                margin: 0;
                padding: 0;
            }
            .clearfix {
                zoom: 1;
            }
            .clearfix:after {
                clear: both;
            }
            .clearfix:after {
                content: '.';
                display: block;
                overflow: hidden;
                visibility: hidden;
                font-size: 0;
                line-height: 0;
                width: 0;
                height: 0;
            }
            a {
                text-decoration: none;
                color: #333;
            }
            img {
                vertical-align: middle;
            }
            .page-shopping-cart {
                width: 1200px;
                margin: 50px auto;
                font-size: 14px;
                border: 1px solid #e3e3e3;
                border-top: 2px solid #317ee7;
            }
            .page-shopping-cart .cart-title {
                color: #317ee7;
                font-size: 16px;
                text-align: left;
                padding-left: 20px;
                line-height: 68px;
            }
            .page-shopping-cart .red-text {
                color: #e94826;
            }
            .page-shopping-cart .check-span {
                display: block;
                width: 24px;
                height: 20px;
                background: url("shopping_cart.png") no-repeat 0 0;
            }
            .page-shopping-cart .check-span.check-true {
                background: url("shopping_cart.png") no-repeat 0 -22px;
            }
            .page-shopping-cart .td-check {
                width: 70px;
            }
            .page-shopping-cart .td-product {
                width: 460px;
            }
            .page-shopping-cart .td-num, .page-shopping-cart .td-price, .page-shopping-cart .td-total {
                width: 160px;
            }
            .page-shopping-cart .td-do {
                width: 150px;
            }
            .page-shopping-cart .cart-product-title {
                text-align: center;
                height: 38px;
                line-height: 38px;
                padding: 0 20px;
                background: #f7f7f7;
                border-top: 1px solid #e3e3e3;
                border-bottom: 1px solid #e3e3e3;
            }
            .page-shopping-cart .cart-product-title .td-product {
                text-align: center;
                font-size: 14px;
            }
            .page-shopping-cart .cart-product-title .td-check {
                text-align: left;
            }
            .page-shopping-cart .cart-product-title .td-check .check-span {
                margin: 9px 6px 0 0;
            }
            .page-shopping-cart .cart-product {
                padding: 0 20px;
                text-align: center;
            }
            .page-shopping-cart .cart-product table {
                width: 100%;
                text-align: center;
                font-size: 14px;
            }
            .page-shopping-cart .cart-product table td {
                padding: 20px 0;
            }
            .page-shopping-cart .cart-product table tr {
                border-bottom: 1px dashed #e3e3e3;
            }
            .page-shopping-cart .cart-product table tr:last-child {
                border-bottom: none;
            }
            .page-shopping-cart .cart-product table .product-num {
                border: 1px solid #e3e3e3;
                display: inline-block;
                text-align: center;
            }
            .page-shopping-cart .cart-product table .product-num .num-do {
                width: 24px;
                height: 28px;
                display: block;
                background: #f7f7f7;
            }
            .page-shopping-cart .cart-product table .product-num .num-reduce span {
                background: url("shopping_cart.png") no-repeat -40px -22px;
                display: block;
                width: 6px;
                height: 2px;
                margin: 13px auto 0 auto;
            }
            .page-shopping-cart .cart-product table .product-num .num-add span {
                background: url("shopping_cart.png") no-repeat -60px -22px;
                display: block;
                width: 8px;
                height: 8px;
                margin: 10px auto 0 auto;
            }
            .page-shopping-cart .cart-product table .product-num .num-input {
                width: 42px;
                height: 28px;
                line-height: 28px;
                border: none;
                text-align: center;
            }
            .page-shopping-cart .cart-product table .td-product {
                text-align: left;
                font-size: 12px;
                line-height: 20px;
            }
            .page-shopping-cart .cart-product table .td-product img {
                border: 1px solid #e3e3e3;
                margin-right: 10px;
            }
            .page-shopping-cart .cart-product table .td-product .product-info {
                display: inline-block;
                vertical-align: middle;
            }
            .page-shopping-cart .cart-product table .td-do {
                font-size: 12px;
            }
            .page-shopping-cart .cart-product-info {
                height: 50px;
                line-height: 50px;
                background: #f7f7f7;
                padding-left: 20px;
            }
            .page-shopping-cart .cart-product-info .delect-product {
                color: #666;
            }
            .page-shopping-cart .cart-product-info .delect-product span {
                display: inline-block;
                vertical-align: top;
                margin: 18px 8px 0 0;
                width: 13px;
                height: 15px;
                background: url("shopping_cart.png") no-repeat -60px 0;
            }
            .page-shopping-cart .cart-product-info .product-total {
                font-size: 14px;
                color: #e94826;
            }
            .page-shopping-cart .cart-product-info .product-total span {
                font-size: 20px;
            }
            .page-shopping-cart .cart-product-info .check-num {
                color: #333;
            }
            .page-shopping-cart .cart-product-info .check-num span {
                color: #e94826;
            }
            .page-shopping-cart .cart-product-info .keep-shopping {
                color: #666;
                margin-left: 40px;
            }
            .page-shopping-cart .cart-product-info .keep-shopping span {
                display: inline-block;
                vertical-align: top;
                margin: 18px 8px 0 0;
                width: 15px;
                height: 15px;
                background: url("shopping_cart.png") no-repeat -40px 0;
            }
            .page-shopping-cart .cart-product-info .btn-buy {
                height: 50px;
                color: #fff;
                font-size: 20px;
                display: block;
                width: 110px;
                background: #ff7700;
                text-align: center;
                margin-left: 30px;
            }
            .page-shopping-cart .cart-worder {
                padding: 20px;
            }
            .page-shopping-cart .cart-worder .choose-worder {
                color: #fff;
                display: block;
                background: #39e;
                width: 140px;
                height: 40px;
                line-height: 40px;
                border-radius: 4px;
                text-align: center;
                margin-right: 20px;
            }
            .page-shopping-cart .cart-worder .choose-worder span {
                display: inline-block;
                vertical-align: top;
                margin: 9px 10px 0 0;
                width: 22px;
                height: 22px;
                background: url("shopping_cart.png") no-repeat -92px 0;
            }
            .page-shopping-cart .cart-worder .worker-info {
                color: #666;
            }
            .page-shopping-cart .cart-worder .worker-info img {
                border-radius: 100%;
                margin-right: 10px;
            }
            .page-shopping-cart .cart-worder .worker-info span {
                color: #000;
            }
            .choose-worker-box {
                width: 620px;
                background: #fff;
            }
            .choose-worker-box .box-title {
                height: 40px;
                line-height: 40px;
                background: #F7F7F7;
                text-align: center;
                position: relative;
                font-size: 14px;
            }
            .choose-worker-box .box-title a {
                display: block;
                position: absolute;
                top: 15px;
                right: 16px;
                width: 10px;
                height: 10px;
                background: url("shopping_cart.png") no-repeat -80px 0;
            }
            .choose-worker-box .box-title a:hover {
                background: url("shopping_cart.png") no-repeat -80px -22px;
            }
            .choose-worker-box .worker-list {
                padding-top: 30px;
                height: 134px;
                overflow-y: auto;
            }
            .choose-worker-box .worker-list li {
                float: left;
                width: 25%;
                text-align: center;
                margin-bottom: 30px;
            }
            .choose-worker-box .worker-list li p {
                margin-top: 8px;
            }
            .choose-worker-box .worker-list li.cur a {
                color: #f70;
            }
            .choose-worker-box .worker-list li.cur a img {
                border: 1px solid #f70;
            }
            .choose-worker-box .worker-list li a:hover {
                color: #f70;
            }
            .choose-worker-box .worker-list li a:hover img {
                border: 1px solid #f70;
            }
            .choose-worker-box .worker-list li img {
                border: 1px solid #fff;
                border-radius: 100%;
            }</style></head><body><div class="page-shopping-cart" id="shopping-cart"><h4 class="cart-title">购物清单</h4><div class="cart-product-title clearfix"><div class="td-check fl"><span class="check-span fl check-all" :class="{'check-true':isSelectAll}" @click="selectProduct(isSelectAll)"></span>全选</div><div class="td-product fl">商品</div><div class="td-num fl">数量</div><div class="td-price fl">单价(元)</div><div class="td-total fl">金额(元)</div><div class="td-do fl">操作</div></div><div class="cart-product clearfix"><table><tbody><!--遍历的时候带上索引--><tr v-for="(item,index) in productList"><td class="td-check"><span class="check-span" @click="item.select=!item.select" :class="{'check-true':item.select}"></span></td><td class="td-product"><img :src="item.pro_img" width="98" height="98"><div class="product-info"><h6>{{item.pro_name}}</h6><p>品牌:{{item.pro_brand}}&nbsp;&nbsp;产地:{{item.pro_place}}</p><p>规格/纯度:{{item.pro_purity}}&nbsp;&nbsp;起定量:{{item.pro_min}}</p><p>配送仓储:{{item.pro_depot}}</p></div><div class="clearfix"></div></td><td class="td-num"><div class="product-num"><a href="javascript:;" class="num-reduce num-do fl" @click="item.pro_num--"><span></span></a><input type="text" class="num-input" v-model="item.pro_num"><a href="javascript:;" class="num-add num-do fr" @click="item.pro_num++"><span></span></a></div></td><td class="td-price"><p class="red-text">¥<span class="price-text">{{item.pro_price.toFixed(2)}}</span></p></td><td class="td-total"><p class="red-text">¥<span class="total-text">{{item.pro_price*item.pro_num}}</span>.00</p></td><td class="td-do"><a href="javascript:;" class="product-delect" @click="deleteOneProduct(index)">删除</a></td></tr></tbody></table></div><div class="cart-product-info"><a class="delect-product" href="javascript:;" @click="deleteProduct"><span></span>删除所选商品</a><a class="keep-shopping" href="#"><span></span>继续购物</a><a class="btn-buy fr" href="javascript:;">去结算</a><p class="fr product-total">¥<span>{{getTotal.totalPrice}}</span></p><p class="fr check-num"><span>{{getTotal.totalNum}}</span>件商品总计(不含运费):</p></div></div></body><script src="vue.min.js"></script><script>
        new Vue({
            el: '#shopping-cart',
            data: {
                productList: [
                    {'pro_name': '【斯文】甘油 | 丙三醇',//产品名称
                        'pro_brand': 'skc',//品牌名称
                        'pro_place': '韩国',//产地
                        'pro_purity': '99.7%',//规格
                        'pro_min': "215千克",//最小起订量
                        'pro_depot': '上海仓海仓储',//所在仓库
                        'pro_num': 3,//数量
                        'pro_img': '../../images/ucenter/testimg.jpg',//图片链接
                        'pro_price': 800//单价
                    },
                    {
                        'pro_name': '【斯文】甘油 | 丙三醇',//产品名称
                        'pro_brand': 'skc',//品牌名称
                        'pro_place': '韩国',//产地
                        'pro_purity': '99.7%',//规格
                        'pro_min': "215千克",//最小起订量
                        'pro_depot': '上海仓海仓储',//所在仓库
                        'pro_num': 3,//数量
                        'pro_img': '../../images/ucenter/testimg.jpg',//图片链接
                        'pro_price': 800//单价
                    },
                    {
                        'pro_name': '【斯文】甘油 | 丙三醇',//产品名称
                        'pro_brand': 'skc',//品牌名称
                        'pro_place': '韩国',//产地
                        'pro_purity': '99.7%',//规格
                        'pro_min': "215千克",//最小起订量
                        'pro_depot': '上海仓海仓储',//所在仓库
                        'pro_num': 3,//数量
                        'pro_img': '../../images/ucenter/testimg.jpg',//图片链接
                        'pro_price': 800//单价
                    }
                ]
            },
            computed: {
                //检测是否全选
                isSelectAll:function(){
                    //如果productList中每一条数据的select都为true,返回true,否则返回false;
                    return this.productList.every(function (val) { return val.select});
                },
                //获取总价和产品总件数
                getTotal:function(){
                    //获取productList中select为true的数据。
                    var _proList=this.productList.filter(function (val) { return val.select}),totalPrice=0;
                    for(var i=0,len=_proList.length;i<len;i++){
                        //总价累加
                        totalPrice+=_proList[i].pro_num*_proList[i].pro_price;
                    }
                    //选择产品的件数就是_proList.length,总价就是totalPrice
                    return {totalNum:_proList.length,totalPrice:totalPrice}
                }
            },
            methods: {
                //全选与取消全选
                selectProduct:function(_isSelect){
                    //遍历productList,全部取反
                    for (var i = 0, len = this.productList.length; i < len; i++) {
                        this.productList[i].select = !_isSelect;
                    }
                },
                //删除已经选中(select=true)的产品
                deleteProduct:function () {
                    this.productList=this.productList.filter(function (item) {return !item.select})
                },
                //删除单条产品
                deleteOneProduct:function (index) {
                    //根据索引删除productList的记录
                    this.productList.splice(index,1);
                },
            },
            mounted: function () {
                var _this=this;
                //为productList添加select(是否选中)字段,初始值为true
                this.productList.map(function (item) {
                    _this.$set(item, 'select', true);
                })
            }
        })</script></html>  

5.todoList

运行效果

clipboard.png

原理分析和实现

首先,还是先把布局写好,和引入vue,准备vue实例,这个不多说,代码如下

<!DOCTYPE html><html><head><meta charset="UTF-8"><title></title><style>
            body{font-family: "微软雅黑";font-size: 14px;}
            input{font-size: 14px;}
            body,ul,div,html{padding: 0;margin: 0;}
            .hidden{display: none;}
            .main{width: 800px;margin: 0 auto;}
            li{list-style-type: none;line-height: 40px;position: relative;border: 1px solid transparent;padding: 0 20px;}
            li .type-span{display: block;width: 10px;height: 10px;background: #ccc;margin: 14px 10px 0 0 ;float: left;}
            li .close{position: absolute;color: #f00;font-size: 20px;line-height: 40px;height: 40px;right: 20px;cursor: pointer;display: none;top: 0;}
            li:hover{border: 1px solid #09f;}
            li:hover .close{display: block;}
            li .text-keyword{height: 40px;padding-left: 10px;box-sizing: border-box;margin-left: 10px;width: 80%;display: none;}
            .text-keyword{box-sizing: border-box;width: 100%;height: 40px;padding-left: 10px;outline: none;}</style></head><body><div id="app" class="main"><h2>小目标列表</h2><div class="list"><h3>添加小目标</h3><input type="text" class="text-keyword" placeholder="输入小目标后,按回车确认"/><p>共有N个目标</p><p><input type="radio" name="chooseType" checked="true"/><label>所有目标</label><input type="radio" name="chooseType"/><label>已完成目标</label><input type="radio" name="chooseType"/><label>未完成目标</label></p></div><ul><li class="li1"><div><span class="type-span"></span><span>html5</span><span class="close">X</span></div></li><li class="li1"><div><span class="type-span"></span><span>css3</span><span class="close">X</span></div></li></ul></div></body><script src="vue2.4.2.js"></script><script type="text/javascript">
    new Vue({
        el: "#app",
        data: {
        },
        computed:{
        },
        methods:{
        }
    });</script></html>

布局有了,相当于一个骨架就有了,下面实现功能,一个一个来

步骤1

输入并回车,多一条记录。下面的记录文字也会改变

clipboard.png

首先,大的输入框回车要添加纪录,那么输入框必须绑定一个值和一个添加纪录的方法。
代码如下:
然后,下面的记录也要改变,所以,下面的记录也要帮一个值,因为这个记录可能会有多个,这个值就是一个数组,也可以看到,记录除了名称,还有记录是否完成的状态,所以,绑定记录的这个值肯定是一个对象数组!代码如下
最后,记录文字 clipboard.png要改变。这个只是一个当前记录的长度即可!

为了着重表示我修改了什么地方,代码我现在只贴出修改的部分,大家对着上面的布局,就很容易知道我改的是什么地方了!下面也是这样操作!

html代码

<!--利用v-model把addText绑定到input--><input type="text" class="text-keyword" placeholder="输入小目标后,按回车确认" @keyup.13='addList' v-model="addText"/><p>共有{{prolist.length}}个目标</p><!--v-for遍历prolist--><li class="li1" v-for="list in prolist"><div><span class="type-span"></span><span>{{list.name}}</span><span class="close">X</span></div></li>

js代码

new Vue({
    el: "#app",
    data: {
        addText:'',
        //name-名称,status-完成状态
       prolist:[
               {name:"HTML5",status:false},
               {name:"CSS3",status:false},
               {name:"vue",status:false},
               {name:"react",status:false}
        ]
    },
    computed:{
    },
    methods:{
        addList(){
            //添加进来默认status=false,就是未完成状态
            this.prolist.push({
                name:this.addText,
                status:false
            });
            //添加后,清空addText
            this.addText="";
        }
    }
});

测试一下,没问题

clipboard.png

步骤2

点击切换,下面记录会改变

clipboard.png

看到三个选项,也很简单,无非就是三个选择,一个是所有的目标,一个是所有已经完成的目标,一个是所有没完成的目标。
首先.新建一个新的变量(newList),储存prolist。遍历的时候不再遍历prolist,而是遍历newList。改变也是改变newList。
然后.选择所有目标的时候,显示全部prolist,把prolist赋值给newList。
然后.选择所有已经完成目标的时候,只显示prolist中,status为true的目标,把prolist中,status为true的项赋值给newList,
最后.选择所有未完成目标的时候,只显示status为false的目标,把prolist中,status为false的项赋值给newList。

代码如下

html

<ul><li class="li1" v-for="list in newList"><div><span class="status-span"></span><span>{{list.name}}</span><span class="close" @click='delectList(index)'>X</span></div></li></ul>

js

new Vue({
    el: "#app",
    data: {
        addText:'',
        //name-名称,status-完成状态
       prolist:[
               {name:"HTML5",status:false},
               {name:"CSS3",status:false},
               {name:"vue",status:false},
               {name:"react",status:false}
        ],
        newList:[]
    },
    computed:{
        noend:function(){
            return this.prolist.filter(function(item){
                return !item.status
            }).length;
        }
    },
    methods:{
        addList(){
            //添加进来默认status=false,就是未完成状态
            this.prolist.push({
                name:this.addText,
                status:false
            });
            //添加后,清空addText
            this.addText="";
        },
        chooseList(type){
            //type=1时,选择所有目标
            //type=2时,选择所有已完成目标
            //type=3时,选择所有未完成目标
            switch(type){
                case 1:this.newList=this.prolist;break;
                case 2:this.newList=this.prolist.filter(function(item){return item.status});break;
                case 3:this.newList=this.prolist.filter(function(item){return !item.status});break;
            }
        },
        delectList(index){
            //根据索引,删除数组某一项
            this.prolist.splice(index,1);
            //更新newList  newList可能经过this.prolist.filter()赋值,这样的话,删除了prolist不会影响到newList  那么就要手动更新newList
            this.newList=this.prolist;
        },
    },
    mounted(){
        //初始化,把prolist赋值给newList。默认显示所有目标
        this.newList=this.prolist;
    }
});

运行结果

clipboard.png

步骤3

红色关闭标识,点击会删除该记录。前面按钮点击会切换该记录完成状态,颜色也改变,记录文字也跟着改变

clipboard.png

首先点击红色关闭标识,点击会删除该记录。这个应该没什么问题,就是删除prolist的一条记录!
然后前面按钮点击会切换该记录完成状态。这个也没什么,就是改变prolist的一条记录的status字段!
最后记录文字的改变,就是记录prolist中status为false的有多少条,prolist中status为true的有多少条而已

html代码

<!--如果noend等于0,就是全部完成了就显示‘全部完成了’,如果没有就是显示已完成多少条(prolist.length-noend)和未完成多少条(noend)--><p>共有{{prolist.length}}个目标,{{noend==0?"全部完成了":'已完成'+(prolist.length-noend)+',还有'+noend+'条未完成'}}</p>
<ul><li class="li1" v-for="(list,index) in newList"><div><span class="status-span" @click="list.status=!list.status" :class="{'status-end':list.status}"></span><span>{{list.name}}</span><span class="close" @click='delectList(index)'>X</span></div></li></ul>

js

new Vue({
    el: "#app",
    data: {
        addText:'',
        //name-名称,status-完成状态
       prolist:[
               {name:"HTML5",status:false},
               {name:"CSS3",status:false},
               {name:"vue",status:false},
               {name:"react",status:false}
        ],
        newList:[]
    },
    computed:{
        //计算属性,返回未完成目标的条数,就是数组里面status=false的条数
        noend:function(){
            return this.prolist.filter(function(item){
                return !item.status
            }).length;
        }
    },
    methods:{
        addList(){
            //添加进来默认status=false,就是未完成状态
            this.prolist.push({
                name:this.addText,
                status:false
            });
            //添加后,清空addText
            this.addText="";
        },
        chooseList(type){
            switch(type){
                case 1:this.newList=this.prolist;break;
                case 2:this.newList=this.prolist.filter(function(item){return item.status});break;
                case 3:this.newList=this.prolist.filter(function(item){return !item.status});break;
            }
        },
        delectList(index){
            //根据索引,删除数组某一项
            this.prolist.splice(index,1);
            //更新newList  newList可能经过this.prolist.filter()赋值,这样的话,删除了prolist不会影响到newList  那么就要手动更新newList
            this.newList=this.prolist;
        },
    },
    mounted(){
        this.newList=this.prolist;
    }
});

运行结果

clipboard.png

步骤4

文字双击会出现输入框,可输入文字,如果回车或者失去焦点,就改变文字,如果按下ESC就恢复原来的文字

clipboard.png

首先.双击出现输入框,就是双击文字后,给当前的li设置一个类名(‘ eidting’),然后写好样式。当li出现这个类名的时候,就出现输入框,并且隐藏其它内容。
然后.回车或者失去焦点,就改变文字这个只需要操作一个,就是把类名(‘ eidting’)清除掉。然后输入框就会隐藏,其它内容显示!
最后.按下ESC就恢复原来的文字,就是出现输入框的时候,用一个变量(‘ beforeEditText’)先保存当前的内容,然后按下了ESC,就把变量(‘ beforeEditText’)赋值给当前操作的值!

代码如下:

html

<ul><li class="li1" v-for="(list,index) in newList" :class="{'eidting':curIndex===index}"><div><span class="status-span" @click="list.status=!list.status" :class="{'status-end':list.status}"></span><span @dblclick="curIndex=index">{{list.name}}</span><span class="close" @click='delectList(index)'>X</span></div><input type="text" class="text2" v-model='list.name' @keyup.esc='cancelEdit(list)' @blur='edited' @focus='editBefore(list.name)' @keyup.enter='edited'/></li></ul>

css(加上)

li div{display: block;}
li.eidting div{display: none;}
li .text2{height: 40px;padding-left: 10px;box-sizing: border-box;margin-left: 10px;width: 80%;display: none;}
li.eidting .text2{display: block;}

js

methods:{
        addList(){
            //添加进来默认status=false,就是未完成状态
            this.prolist.push({
                name:this.addText,
                status:false
            });
            //添加后,清空addText
            this.addText="";
        },
        chooseList(type){
            //type=1时,选择所有目标
            //type=2时,选择所有已完成目标
            //type=3时,选择所有未完成目标
            switch(type){
                case 1:this.newList=this.prolist;break;
                case 2:this.newList=this.prolist.filter(function(item){return item.status});break;
                case 3:this.newList=this.prolist.filter(function(item){return !item.status});break;
            }
        },
        delectList(index){
            //根据索引,删除数组某一项
            this.prolist.splice(index,1);
            //更新newList  newList可能经过this.prolist.filter()赋值,这样的话,删除了prolist不会影响到newList  那么就要手动更新newList
            this.newList=this.prolist;
        },
        //修改前
        editBefore(name){
            //先记录当前项(比如这一项,{name:"HTML5",status:false})
            //beforeEditText="HTML5"
            this.beforeEditText=name;
        },
        //修改完成后
        edited(){
            //修改完了,设置curIndex="",这样输入框就隐藏,其它元素就会显示。因为在li元素 写了::class="{'eidting':curIndex===index}"  当curIndex不等于index时,eidting类名就清除了!
            //输入框利用v-model绑定了当前项(比如这一项,{name:"HTML5",status:false})的name,当在输入框编辑的时候,比如改成‘HTML’,实际上当前项的name已经变成了‘HTML’,所以,这一步只是清除eidting类名,隐藏输入框而已
            //还有一个要注意的就是虽然li遍历的是newList,比如改了newList的这一项({name:"HTML5",status:false}),比如改成这样({name:"HTML",status:true})。实际上prolist的这一项({name:"HTML5",status:false}),也会被改成({name:"HTML",status:true})。因为这里是一个对象,而且公用一个堆栈!修改其中一个,另一个会被影响到
            this.curIndex="";
        },
        //取消修改
        cancelEdit(val){
            //上面说了输入框利用v-model绑定了当前项(比如这一项,{name:"HTML5",status:false})的name,当在输入框编辑的时候,比如改成‘HTML’,实际上当前项的name已经变成了‘HTML’,所以,这一步就是把之前保存的beforeEditText赋值给当前项的name属性,起到一个恢复原来值得作用!
            val.name=this.beforeEditText;
            this.curIndex="";
        }
 },

运行结果

clipboard.png

还有一个小细节,大家可能注意到了,就是双击文字,出来输入框的时候,还要自己手动点击一下,才能获得焦点,我们想双击了,输入框出来的时候,自动获取焦点,怎么办?自定义指令就行了!

computed:{...},
methods:{...},
mounted(){...},
directives:{"focus":{
        update(el){
            el.focus();
        }
    }
}

然后html 调用指令

<input type="text" class="text2" v-model='list.name' @keyup.esc='cancelEdit(list)' @blur='edited' @focus='editBefore(list.name)' @keyup.enter='edited' v-focus/>

完整代码

<!DOCTYPE html><html><head><meta charset="UTF-8"><title></title><style>
            body{font-family: "微软雅黑";font-size: 14px;}
            input{font-size: 14px;}
            body,ul,div,html{padding: 0;margin: 0;}
            .hidden{display: none;}
            .main{width: 800px;margin: 0 auto;}
            li{list-style-type: none;line-height: 40px;position: relative;border: 1px solid transparent;padding: 0 20px;}
            li .status-span{display: block;width: 10px;height: 10px;background: #ccc;margin: 14px 10px 0 0 ;float: left;}
            li .status-span.status-end{
                background: #09f;
            }
            li .close{position: absolute;color: #f00;font-size: 20px;line-height: 40px;height: 40px;right: 20px;cursor: pointer;display: none;top: 0;}
            li:hover{border: 1px solid #09f;}
            li:hover .close{display: block;}
            li div{display: block;}
            li.eidting div{display: none;}
            li .text2{height: 40px;padding-left: 10px;box-sizing: border-box;margin-left: 10px;width: 80%;display: none;}
            li.eidting .text2{display: block;}
            li .text-keyword{height: 40px;padding-left: 10px;box-sizing: border-box;margin-left: 10px;width: 80%;display: none;}
            .text-keyword{box-sizing: border-box;width: 100%;height: 40px;padding-left: 10px;outline: none;}</style></head><body><div id="app" class="main"><h2>小目标列表</h2><div class="list"><h3>添加小目标</h3><input type="text" class="text-keyword" placeholder="输入小目标后,按回车确认" @keyup.13='addList' v-model="addText"/><!--如果noend等于0,就是全部完成了就显示‘全部完成了’,如果没有就是显示已完成多少条(prolist.length-noend)和未完成多少条(noend)--><p>共有{{prolist.length}}个目标,{{noend==0?"全部完成了":'已完成'+(prolist.length-noend)+',还有'+noend+'条未完成'}}</p><p><input type="radio" name="chooseType" checked="true" @click='chooseList(1)'/><label>所有目标</label><input type="radio" name="chooseType" @click='chooseList(2)'/><label>已完成目标</label><input type="radio" name="chooseType" @click='chooseList(3)'/><label>未完成目标</label></p></div><ul><li class="li1" v-for="(list,index) in newList" :class="{'eidting':curIndex===index}"><div><span class="status-span" @click="list.status=!list.status" :class="{'status-end':list.status}"></span><span @dblclick="curIndex=index">{{list.name}}</span><span class="close" @click='delectList(index)'>X</span></div><input type="text" class="text2" v-model='list.name' @keyup.esc='cancelEdit(list)' @blur='edited' @focus='editBefore(list.name)' @keyup.enter='edited' v-focus/></li></ul></div></body><script src="vue2.4.2.js"></script><script type="text/javascript">
    new Vue({
        el: "#app",
        data: {
            addText:'',
            //name-名称,status-完成状态
           prolist:[
                   {name:"HTML5",status:false},
                   {name:"CSS3",status:false},
                   {name:"vue",status:false},
                   {name:"react",status:false}
            ],
            newList:[],
            curIndex:'',
               beforeEditText:""
        },
        computed:{
            //计算属性,返回未完成目标的条数,就是数组里面status=false的条数
            noend:function(){
                return this.prolist.filter(function(item){
                    return !item.status
                }).length;
            }
        },
        methods:{
            addList(){
                //添加进来默认status=false,就是未完成状态
                this.prolist.push({
                    name:this.addText,
                    status:false
                });
                //添加后,清空addText
                this.addText="";
            },
            chooseList(type){
                //type=1时,选择所有目标
                //type=2时,选择所有已完成目标
                //type=3时,选择所有未完成目标
                switch(type){
                    case 1:this.newList=this.prolist;break;
                    case 2:this.newList=this.prolist.filter(function(item){return item.status});break;
                    case 3:this.newList=this.prolist.filter(function(item){return !item.status});break;
                }
            },
            delectList(index){
                //根据索引,删除数组某一项
                this.prolist.splice(index,1);
                //更新newList  newList可能经过this.prolist.filter()赋值,这样的话,删除了prolist不会影响到newList  那么就要手动更新newList
                this.newList=this.prolist;
            },
            //修改前
            editBefore(name){
                //先记录当前项(比如这一项,{name:"HTML5",status:false})
                //beforeEditText="HTML5"
                this.beforeEditText=name;
            },
            //修改完成后
            edited(){
                //修改完了,设置curIndex="",这样输入框就隐藏,其它元素就会显示。因为在li元素 写了::class="{'eidting':curIndex===index}"  当curIndex不等于index时,eidting类名就清除了!
                //输入框利用v-model绑定了当前项(比如这一项,{name:"HTML5",status:false})的name,当在输入框编辑的时候,比如改成‘HTML’,实际上当前项的name已经变成了‘HTML’,所以,这一步只是清除eidting类名,隐藏输入框而已
                //还有一个要注意的就是虽然li遍历的是newList,比如改了newList的这一项({name:"HTML5",status:false}),比如改成这样({name:"HTML",status:true})。实际上prolist的这一项({name:"HTML5",status:false}),也会被改成({name:"HTML",status:true})。因为这里是一个对象,而且公用一个堆栈!修改其中一个,另一个会被影响到
                this.curIndex="";
            },
            //取消修改
            cancelEdit(val){
                //上面说了输入框利用v-model绑定了当前项(比如这一项,{name:"HTML5",status:false})的name,当在输入框编辑的时候,比如改成‘HTML’,实际上当前项的name已经变成了‘HTML’,所以,这一步就是把之前保存的beforeEditText赋值给当前项的name属性,起到一个恢复原来值得作用!
                val.name=this.beforeEditText;
                this.curIndex="";
            }
        },
        mounted(){
            //初始化,把prolist赋值给newList。默认显示所有目标
            this.newList=this.prolist;
        },
        directives:{
        "focus":{
            update(el){
                el.focus();
            }
        }
    }
    });</script></html>

6.小结

好了,三个小实例在这里就说完了!别看文章这么长,其实都是基础,可能是我比较啰嗦而已!如果大家能熟透这几个小实例,相信用vue做项目也是信手拈来。基础的语法在这里了,有了基础,高级的写法也不会很难学习!如果以后,我有什么要分享的,我会继续分享。最后一句老话,如果觉得我哪里写错了,写得不好,欢迎指点!

Viewing all 148 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>