分类目录归档:JavaScript

JavaScript那点不可告人的事儿-。-!

客户端调起方案解析(scheme、intent uri、Universal Links)

移动互联时代,很多互联网服务都会同时具备网站以及移动客户端,很多人认为APP的能帮助建立更稳固的用户关系,于是经常会接到各种从浏览器、webview、短信、甚至是在其他APP中唤醒APP的运营需求。

运营推广场景

  • 微信、QQ等 -> 唤醒APP
    用户通过某APP分享了一条链接至微信或QQ,用户B点开该链接后,会引导用户B打开该APP或者下载该APP。
  • 浏览器 -> 唤醒APP
    用户A通过浏览器打开了某APP的M站或者官网,如果检测到A来自手机端,则会引导用户打开该APP或者下载该APP。
  • 短信、邮件、二维码等 -> 唤醒APP
    用户A打开了某APP的推广短信,邮件或者扫描二维码等,会引导用户打开该APP或者下载该APP。
  • 其他APP -> 唤醒APP
    用户A通过第三方APP分享了(任何可以分享信息的品台或工具:IM或者短信等)一条链接至用户B,用户B点开该链接后,链接会引导用户B打开指定APP或者下载指定APP。

APP服务化理念

所谓APP的服务化就是利用唤醒功能将APP的特定页面做为一个单独的服务或者内容,通过一定的渠道和载体传播出去,并且能够像传统的网页链接那样被一键唤醒。

更多关于APP服务化理念,推荐大家看看这篇文章

那么移动平台提供了哪些唤醒APP的方法呢?

如何唤醒APP

目前常见的唤醒APP方式有几种:

  • URL Scheme

URL Scheme是iOS,Android平台都支持,只需要原生APP开发时注册scheme, 那么用户点击到此类链接时,会自动唤醒APP,借助于URL Router机制,则还可以跳转至指定页面。比如:

<!-- 唤醒APP并跳转至指定的path页面 -->
<a href="<scheme>://<path>?<params>=<value>">打开APP</a>

<!-- JS设置iframe src跳转至指定的path页面 -->
//创建一个隐藏的iframe
var ifr = document.createElement('iframe');
ifr.src = '<scheme>://<path>?<params>=<value>';
ifr.style.display = 'none';
document.body.appendChild(ifr);
//记录唤醒时间
var openTime = +new Date();
window.setTimeout(function(){
    document.body.removeChild(ifr);
    //如果setTimeout 回调超过2500ms,则弹出下载
    if( (+new Date()) - openTime > 2500 ){
        window.location = '指定的下载页面';
    }
},2000)

这种方式是当期使用最广泛,也是最简单的,但是需要手机,APP支持URL Scheme
优点: 开发成本低,绝大多数都支持,web-native协议制定也简单。
缺点: 错误处理情况因平台不同,难以统一处理,部分APP会直接跳错误页(比如Android Chrome/41,iOS中老版的Lofter);也有的停留在原页面,但弹出提示“无法打开网页”(比如iOS7);iOS8以及最新的Android Chrome/43 目前都是直接停留在当前页,不会跳出错误提示。
支持情况: iOS在实际使用中,腾讯系的微信,QQ明确禁止使用,iOS9以后Safari不再支持通过js,iframe等来触发scheme跳转,并且还加入了确认机制,使得通过js超时机制来自动唤醒APP的方式基本不可用;Android平台则各个app厂商差异很大,比如Chrome从25及以后就同Safari情况一样。

  • Android intent

这是Android平台独有的,使用方式如下:

intent:
HOST/URI-path // Optional host 
#Intent; 
  package=[string]; 
  action=[string]; 
  category=[string]; 
  component=[string]; 
  scheme=[string]; 
end;

这里的HOST/URI-path, 与普通http URL 的host/path书写方式相同, package是Android APP的包名,其它参数如action、category、component不是很理解, 有兴趣可以去了解官方文档。代码如下:

<!-- 唤醒APP并跳转至指定的path页面 -->
<a href="intent://<role>/<path>#Intent;scheme=<scheme>;package=com. domain;end"">打开APP</a>

如果手机能匹配到相应的APP,则会直接打开;如没有安装,则会跳到手机默认的应用商店,比如Google原生系统Nexus 5,将会直接跳到Google Play,对于国内各厂商定制过的系统,则跳转到各自的默认应用商店,或者弹出商店供选择。intentscheme相对完善的一点是,提供一个打开失败去向URL的选项,可以通过指定参数S.browser_fallback_url来指定去向URL。比如打开APP动作,如果打开失败,则跳转到APP下载页,这对于国内的特殊网络环境,还是挺有用的。

  • Safari内置APP广告条

在页面Head中增加如下meta, 添加智能App广告条,可以自动判断是否已安装应用,只能用于Safari,在第三方应用中就不行了。

<meta"apple-itunes-app"content"app-id=myAppStoreID, affiliate-data=myAffiliateData, app-argument=myURL"
  • Android Chrome内置APP安装提示

这个是Mobile Chrome 43 beta新加入的特性,在用户浏览某一个网站多次后,如果Chrome发现该站点有原生APP,则会提示用户下载原生APP,此项特性开发者无法干预,完全是Google的推荐行为。

  • Universal Links

在2015年的WWDC大会上,Apple推出了iOS 9的一个功能:Universal Links通用链接。如果你的App支持Universal Links,那就可以访问HTTP/HTTPS链接直接唤起APP进入具体页面,不需要其他额外判断;如果未安装App,访问此通用链接时,可以一个自定义网页。

优点:

  • 唯一性:不像自定义的scheme,因为它使用标准的HTTP/HTTPS链接到你的web站点,所以它不会被其它的app所声明.另外,Custom URL scheme 因为是自定义的协议,所以在没有安装 app 的情况下是无法直接打开的,而Universal Links本身是一个HTTP/HTTPS链接,所以有更好的兼容性;
  • 安全:当用户的手机上安装了你的app,那么iOS将去你的网站上去下载你上传上去的说明文件(这个说明文件声明了APP可以打开哪些类型的http链接)。因为只有你自己才能上传文件到你网站的根目录,所以你的网站和你的app之间的关联是安全的;
  • 可变:当用户手机上没有安装你的app的时候,Universal Links也能够工作。如果你愿意,在没有安装APP的时候,用户点击链接,会在safari中展示你网站的内容;
  • 简单:一个URL链接,可以同时作用于网站和app,可以定义统一的web-native协议;
  • 私有:其它APP可以在不需要知道是否安装了的情况下和你的APP相互通信;

缺点:

  • 只支持iOS9及以上系统;当使用Universal Link打开APP之后,状态栏右上角会出现链接地址,点击它会取消Universal Link,需引导用户重新使用Safari再次打开该链接,弹出Safari内置APP广告条,再点击打开重新开启Universal Link。

iOS9开启Universal Links

首先,你必须有一个域名,且这个域名的网站需要支持https,然后拥有网站的上传到.well-known目录的权限(这个权限是为了上传一个Apple指定的文件apple-app-site-association),有了这个先决条件才能够继续下面的步骤:

  • 创建一个json格式的命名为apple-app-site-association文件,注意这个文件必须没有后缀名,文件名必须为`apple-app-site-association!!!
{
    "applinks": {
        "apps": [],
        "details": [
            {
                "appID": "9JA89QQLNQ.com.apple.wwdc", 
                "paths": [ "/wwdc/news/", "/videos/wwdc/2015/*"]
            },
            {
                "appID": "ABCD1234.com.apple.wwdc", 
                "paths": [ "*" ]
            }
        ]
    }
}

说明: appID = teamId.yourapp’s bundle identifier
paths = APP支持的路径列表,只有这些指定的路径的链接,才能被APP所处理,大小写敏感。举个例子,如果你的网站是www.domain.com,你的path写的是”/support/*”,那么当用户点击www.domain.com/support/<path>?<params>=<value>,就可以唤醒APP了,相反www.domain.com/other就不会。此外Apple为了方便开发者,提供了一个网址来验证我们编写的这个apple-app-site-association是否合法有效。

  • 激活Xcode工程中的Associated Domains能力,在其中的Domains中填入你想支持的域名(这里不是随便填的,是可以支持你需要的Universal Links的域名), 必须以applinks:为前缀,例如:applinks:www.domain.comApple将会在合适的时候,从这个域名请求apple-app-site-association文件。注意:当你打开Associated Domains后,Xcode会在你的工程中添加.entitlements文件,并且登录开发者中心,可以看到Associated Domains处于Enable状态。
  • AppDelegate里实现如下代理方法:
- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray *))restorationHandler {
    // NSUserActivityTypeBrowsingWeb 由Universal Links唤醒的APP
    if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) {
        NSURL *webpageURL = userActivity.webpageURL;
        NSString *host = webpageURL.host;
        if ([host isEqualToString:@"yohunl.com"]) {
            //进行我们需要的处理
        } else {
            [[UIApplication sharedApplication]openURL:webpageURL];
        }
    }
    return YES;
}

至此APP已经开启Universal Links,可以通过链接唤醒APP,并跳转至指定页面了。

  • Android App Links

在2015年的Google I/O大会上,Android M宣布了一个新特性:App Links让用户在点击一个普通web链接的时候可以打开指定APP的指定页面,前提是这个APP已经安装并且经过了验证,否则会显示一个打开确认选项的弹出框。在推动deep linking上Google和Apple可谓英雄所见略同,优缺点也大致相同,只支持Android M以上系统。

Android M开启Universal Links

开启Android App Links的方式也大致同iOS一致:

先决条件:
  1. 注册一个域名
  2. 域名的SSL通道
  3. 具有上传JSON文件到域名的能力
  4. Android Studio 1.3 Preview 及以上
  5. Gradle 版本 — com.android.tools.build:gradle:1.3.0-beta3 及以上
  6. 设置 compileSdkVersion 为 android-MNC 及以上
  7. buildToolsVersion — 23.0.0 rc2 及以上
  8. 创建一个json格式的web-app关联文件,如assetlinks.json,上传到web端
[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "example.com.puppies.app",
    "sha256_cert_fingerprints":
    ["14:6D:E9:83:C5:73:06:50:D8:EE:B9:95:2F:34:FC:64:16:A0:83:42:E6:1D:BE:A8:8A:04:96:B2:3F:CF:44:E5"]
  }
  },
  {
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "example.com.monkeys.app",
    "sha256_cert_fingerprints":
    ["14:6D:E9:83:C5:73:06:50:D8:EE:B9:95:2F:34:FC:64:16:A0:83:42:E6:1D:BE:A8:8A:04:96:B2:3F:CF:44:E5"]
  }
}]

其中
package_name: manifest中声明的包名。
sha256_cert_fingerprints: 可以使用如下命令生成APP的sha256指纹签名

// keystore中持有app release keys的app路径。
// 这个路径依赖于项目设置,因此不同的app是不同的。
keytool -list -v -keystore my-release-key.keystore

上传这个文件到服务器的.well-known/assetlinks.json,为了避免今后每个app链接请求都访问网络,安卓只会在app安装的时候检查这个文件。

  • 创建一个处理App Links的activity,这个activity的目的是为了实现一种这样的机制:负责捕获与解析深度链接,同时转发用户到正确的视图。同时配置激活App Links能力,如下所示:
<activity
  android:name="com.your.app.activity.ParseDeepLinkActivity"
  android:alwaysRetainTaskState="true"
  android:launchMode="singleTask"
  android:noHistory="true"
  android:theme="@android:style/Theme.Translucent.NoTitleBar">

  // 此处激活 App Links
  <intent-filter android:autoVerify="true">
      // 注意yourdomain.com 与 www.yourdomain.com 被看成两个不同的域名,因此你需要为每个域名添加一对http和https
    <data android:scheme="http" android:host="yourdomain.com" />
    <data android:scheme="https" android:host="yourdomain.com" />
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
  </intent-filter>
</activity>
  • 实现App Linksactivity的处理逻辑
public class ParseDeepLinkActivity extends Activity {
  public static final String PRODUCTS_DEEP_LINK = "/products";
  public static final String XMAS_DEEP_LINK = "/campaigns/xmas";

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    // Extrapolates the deeplink data
    Intent intent = getIntent();
    Uri deeplink = intent.getData();

    // Parse the deeplink and take the adequate action 
    if (deeplink != null) {
      parseDeepLink(deeplink);
    }
  }

  private void parseDeepLink(Uri deeplink) {
    // The path of the deep link, e.g. '/products/123?coupon=save90'
    String path = deeplink.getPath();

    if (path.startsWith(PRODUCTS_DEEP_LINK)) {
      // Handles a product deep link
      Intent intent = new Intent(this, ProductActivity.class);
      intent.putExtra("id", deeplink.getLastPathSegment()); // 123
      intent.putExtra("coupon", deeplink.getQueryParameter("coupon")); // save90
      startActivity(intent);
    } else if (XMAS_DEEP_LINK.equals(path)) {
      // Handles a special xmas deep link
      startActivity(new Intent(this, XmasCampaign.class));
    }  else {
      // Fall back to the main activity
      startActivity(new Intent(context, MainActivity.class));
    }
  }
}

至此APP已经开启App Links,可以通过链接唤醒APP,并跳转至指定页面了。

后记

总结以上各种方案,唤醒能力似乎都不是很完美,从长远技术趋势来看都是Deep Links,都需要

  • 一个支持HTTPS的web站

但面对移动互联网浪潮中海量APP的唤醒能力需求,一定会有创业公司来做这件事,比如国外的HoKoLinks,国内的魔窗,是自己造轮子,还是用轮子,各有利弊。

video.js 源码分析(组织结构、继承关系、运行机制、插件的运行机制、类的挂载方式)

组织结构

以下是video.js的源码组织结构关系,涉及控制条、菜单、浮层、进度条、滑动块、多媒体、音轨字幕、辅助函数集合等等。

├── control-bar
│   ├── audio-track-controls
│   │   ├── audio-track-button.js
│   │   └── audio-track-menu-item.js
│   ├── playback-rate-menu
│   │   ├── playback-rate-menu-button.js
│   │   └── playback-rate-menu-item.js
│   ├── progress-control
│   │   ├── load-progress-bar.js
│   │   ├── mouse-time-display.js
│   │   ├── play-progress-bar.js
│   │   ├── progress-control.js
│   │   ├── seek-bar.js
│   │   └── tooltip-progress-bar.js
│   ├── spacer-controls
│   │   ├── custom-control-spacer.js
│   │   └── spacer.js
│   ├── text-track-controls
│   │   ├── caption-settings-menu-item.js
│   │   ├── captions-button.js
│   │   ├── chapters-button.js
│   │   ├── chapters-track-menu-item.js
│   │   ├── descriptions-button.js
│   │   ├── off-text-track-menu-item.js
│   │   ├── subtitles-button.js
│   │   ├── text-track-button.js
│   │   └── text-track-menu-item.js
│   ├── time-controls
│   │   ├── current-time-display.js
│   │   ├── duration-display.js
│   │   ├── remaining-time-display.js
│   │   └── time-divider.js
│   ├── volume-control
│   │   ├── volume-bar.js
│   │   ├── volume-control.js
│   │   └── volume-level.js
│   ├── control-bar.js
│   ├── fullscreen-toggle.js
│   ├── live-display.js
│   ├── mute-toggle.js
│   ├── play-toggle.js
│   ├── track-button.js
│   └── volume-menu-button.js
├── menu
│   ├── menu-button.js
│   ├── menu-item.js
│   └── menu.js
├── popup
│   ├── popup-button.js
│   └── popup.js
├── progress-bar
│   ├── progress-control
│   │   ├── load-progress-bar.js
│   │   ├── mouse-time-display.js
│   │   ├── play-progress-bar.js
│   │   ├── progress-control.js
│   │   ├── seek-bar.js
│   │   └── tooltip-progress-bar.js
│   └── progress-bar.js
├── slider
│   └── slider.js
├── tech
│   ├── flash-rtmp.js
│   ├── flash.js
│   ├── html5.js
│   ├── loader.js
│   └── tech.js
├── tracks
│   ├── audio-track-list.js
│   ├── audio-track.js
│   ├── html-track-element-list.js
│   ├── html-track-element.js
│   ├── text-track-cue-list.js
│   ├── text-track-display.js
│   ├── text-track-list-converter.js
│   ├── text-track-list.js
│   ├── text-track-settings.js
│   ├── text-track.js
│   ├── track-enums.js
│   ├── track-list.js
│   ├── track.js
│   ├── video-track-list.js
│   └── video-track.js
├── utils
│   ├── browser.js
│   ├── buffer.js
│   ├── dom.js
│   ├── events.js
│   ├── fn.js
│   ├── format-time.js
│   ├── guid.js
│   ├── log.js
│   ├── merge-options.js
│   ├── stylesheet.js
│   ├── time-ranges.js
│   ├── to-title-case.js
│   └── url.js
├── big-play-button.js
├── button.js
├── clickable-component.js
├── close-button.js
├── component.js
├── error-display.js
├── event-target.js
├── extend.js
├── fullscreen-api.js
├── loading-spinner.js
├── media-error.js
├── modal-dialog.js
├── player.js
├── plugins.js
├── poster-image.js
├── setup.js
└── video.js

video.js的JavaScript部分都是采用面向对象方式来实现的。基类是Component,所有其他的类都是直接或间接集成此类实现。语法部分采用的是ES6标准。

继承关系

深入源码解读需要了解类与类之间的继承关系,直接上图。

  • 所有的继承关系
  • 主要的继承关系

运行机制

首先调用videojs启动播放器,videojs方法判断当前id是否已被实例化,如果没有实例化新建一个Player对象,因Player继承Component会自动初始化Component类。如果已经实例化直接返回Player对象。

videojs方法源码如下:

function videojs(id, options, ready) {
let tag;
// id可以是选择器也可以是DOM节点
if (typeof id === 'string') {
    if (id.indexOf('#') === 0) {
        id = id.slice(1);
    }
    //检查播放器是否已被实例化
    if (videojs.getPlayers()[id]) {
        if (options) {
            log.warn(`Player "${id}" is already initialised. Options will not be applied.`);
        }
        if (ready) {
            videojs.getPlayers()[id].ready(ready);
        }
        return videojs.getPlayers()[id];
    }
    // 如果播放器没有实例化,返回DOM节点
    tag = Dom.getEl(id);
} else {
    // 如果是DOM节点直接返回
    tag = id;
}
if (!tag || !tag.nodeName) {
    throw new TypeError('The element or ID supplied is not valid. (videojs)');
}
// 返回播放器实例
return tag.player || Player.players[tag.playerId] || new Player(tag, options, ready);
}
[]()

接下来我们看下Player的构造函数,代码如下:

constructor(tag, options, ready) {
    // 注意这个tag是video原生标签
    tag.id = tag.id || `vjs_video_${Guid.newGUID()}`;
    // 选项配置的合并
    options = assign(Player.getTagSettings(tag), options);
    // 这个选项要关掉否则会在父类自动执行加载子类集合
    options.initChildren = false;
    // 调用父类的createEl方法
    options.createEl = false;
    // 在移动端关掉手势动作监听
    options.reportTouchActivity = false;
    // 检查播放器的语言配置
    if (!options.language) {
        if (typeof tag.closest === 'function') {
            const closest = tag.closest('[lang]');
            if (closest) {
                options.language = closest.getAttribute('lang');
            }
        } else {
            let element = tag;
            while (element && element.nodeType === 1) {
                if (Dom.getElAttributes(element).hasOwnProperty('lang')) {
                    options.language = element.getAttribute('lang');
                    break;
                }
                element = element.parentNode;
            }
        }
    }
    // 初始化父类
    super(null, options, ready);
    // 检查当前对象必须包含techOrder参数
    if (!this.options_ || !this.options_.techOrder || !this.options_.techOrder.length) {
        throw new Error('No techOrder specified. Did you overwrite ' +
            'videojs.options instead of just changing the ' +
            'properties you want to override?');
    }
    // 存储当前已被实例化的播放器
    this.tag = tag;
    // 存储video标签的各个属性
    this.tagAttributes = tag && Dom.getElAttributes(tag);
    // 将默认的英文切换到指定的语言
    this.language(this.options_.language);
    if (options.languages) {
        const languagesToLower = {};
        Object.getOwnPropertyNames(options.languages).forEach(function(name) {
            languagesToLower[name.toLowerCase()] = options.languages[name];
        });
        this.languages_ = languagesToLower;
    } else {
        this.languages_ = Player.prototype.options_.languages;
    }
    // 缓存各个播放器的各个属性.
    this.cache_ = {};
    // 设置播放器的贴片
    this.poster_ = options.poster || '';
    // 设置播放器的控制
    this.controls_ = !!options.controls;
    // 默认是关掉控制
    tag.controls = false;
    this.scrubbing_ = false;
    this.el_ = this.createEl();
    const playerOptionsCopy = mergeOptions(this.options_);
    // 自动加载播放器插件
    if (options.plugins) {
        const plugins = options.plugins;
        Object.getOwnPropertyNames(plugins).forEach(function(name) {
            if (typeof this[name] === 'function') {
                this[name](plugins[name]);
            } else {
                log.error('Unable to find plugin:', name);
            }
        }, this);
    }
    this.options_.playerOptions = playerOptionsCopy;
    this.initChildren();
    // 判断是不是音频
    this.isAudio(tag.nodeName.toLowerCase() === 'audio');
    if (this.controls()) {
        this.addClass('vjs-controls-enabled');
    } else {
        this.addClass('vjs-controls-disabled');
    }
    this.el_.setAttribute('role', 'region');
    if (this.isAudio()) {
        this.el_.setAttribute('aria-label', 'audio player');
    } else {
        this.el_.setAttribute('aria-label', 'video player');
    }
    if (this.isAudio()) {
        this.addClass('vjs-audio');
    }
    if (this.flexNotSupported_()) {
        this.addClass('vjs-no-flex');
    }

    if (!browser.IS_IOS) {
        this.addClass('vjs-workinghover');
    }
    Player.players[this.id_] = this;
    this.userActive(true);
    this.reportUserActivity();
    this.listenForUserActivity_();
    this.on('fullscreenchange', this.handleFullscreenChange_);
    this.on('stageclick', this.handleStageClick_);
}

在Player的构造器中有一句super(null, options, ready);实例化父类Component。我们来看下Component的构造函数:

constructor(player, options, ready) {
    // 之前说过所有的类都是继承Component,不是所有的类需要传player
    if (!player && this.play) {
        // 这里判断调用的对象是不是Player本身,是本身只需要返回自己
        this.player_ = player = this; // eslint-disable-line
    } else {
        this.player_ = player;
    }
    this.options_ = mergeOptions({}, this.options_);
    options = this.options_ = mergeOptions(this.options_, options);
    this.id_ = options.id || (options.el && options.el.id);
    if (!this.id_) {
        const id = player && player.id && player.id() || 'no_player';
        this.id_ = `${id}_component_${Guid.newGUID()}`;
    }
    this.name_ = options.name || null;
    if (options.el) {
        this.el_ = options.el;
    } else if (options.createEl !== false) {
        this.el_ = this.createEl();
    }
    this.children_ = [];
    this.childIndex_ = {};
    this.childNameIndex_ = {};
    // 知道Player的构造函数为啥要设置initChildren为false了吧
    if (options.initChildren !== false) {
        // 这个initChildren方法是将一个类的子类都实例化,一个类都对应着自己的el(DOM实例),通过这个方法父类和子类的DOM继承关系也就实现了
        this.initChildren();
    }
    this.ready(ready);
    if (options.reportTouchActivity !== false) {
        this.enableTouchActivity();
    }
}

插件的运行机制

插件的定义

import Player from './player.js';
// 将插件种植到Player的原型链
const plugin = function(name, init) {
  Player.prototype[name] = init;
};
// 暴露plugin接口
videojs.plugin = plugin;

插件的运行

// 在Player的构造函数里判断是否使用了插件,如果有遍历执行
if (options.plugins) {
    const plugins = options.plugins;
    Object.getOwnPropertyNames(plugins).forEach(function(name) {
    if (typeof this[name] === 'function') {
        this[name](plugins[name]);
    } else {
        log.error('Unable to find plugin:', name);
    }
    }, this);
}

控制条是如何运行的

Player.prototype.options_ = {
  // 此处表示默认使用html5的video标签
  techOrder: ['html5', 'flash'],
  html5: {},
  flash: {},
  // 默认的音量,官方代码该配置无效有bug,我们已修复,
  defaultVolume: 0.85,
  // 用户的交互时长,比如超过这个时间表示失去焦点
  inactivityTimeout: 2000,
  playbackRates: [],
  // 这是控制条各个组成部分,作为Player的子类
  children: [
    'mediaLoader',
    'posterImage',
    'textTrackDisplay',
    'loadingSpinner',
    'bigPlayButton',
    'progressBar',
    'controlBar',
    'errorDisplay',
    'textTrackSettings'
  ],
  language: navigator && (navigator.languages && navigator.languages[0] || navigator.userLanguage || navigator.language) || 'en',
  languages: {},
  notSupportedMessage: 'No compatible source was found for this media.'
};

Player类中有个children配置项,这里面是控制条的各个组成部分的类。各个UI类还有子类,都是通过children属性链接的。

UI与JavaScript对象的衔接

video.js里都是组件化实现的,小到一个按钮大到一个播放器都是一个继承了Component类的对象实例,每个对象包含一个el属性,这个el对应一个DOM实例,el是通过createEl生成的DOM实例,在Component基类中包含一个方法createEl方法,子类也可以重写该方法。类与类的从属关系是通过children属性连接。

那么整个播放器是怎么把播放器的UI加载到HTML中的呢?在Player的构造函数里可以看到先生成el,然后初始化父类遍历Children属性,将children中的类实例化并将对应的DOM嵌入到player的el属性中,最后在Player的构造函数中直接挂载到video标签的父级DOM上。

if (tag.parentNode) {
  tag.parentNode.insertBefore(el, tag);
}

这里的tag指的是video标签。

类的挂载方式

上文有提到过UI的从属关系是通过类的children方法连接的,但是所有的类都是关在Component类上的。这主要是基于对模块化的考虑,通过这种方式实现了模块之间的通信。

存储

static registerComponent(name, comp) {
    if (!Component.components_) {
      Component.components_ = {};
    }

    Component.components_[name] = comp;
    return comp;
}

获取

static getComponent(name) {
    if (Component.components_ && Component.components_[name]) {
      return Component.components_[name];
    }

    if (window && window.videojs && window.videojs[name]) {
      log.warn(`The ${name} component was added to the videojs object when it should be registered using videojs.registerComponent(name, component)`);
      return window.videojs[name];
    }
}

在Componet里有个静态方法是registerComponet,所有的组件类都注册到Componet的components_属性里。

例如控制条类ControlBar就是通过这个方法注册的。

Component.registerComponent('ControlBar', ControlBar);

在Player的children属性里包括了controlBar类,然后通过getComponet获取这个类。

.filter((child) => {
  const c = Component.getComponent(child.opts.componentClass ||
                                 toTitleCase(child.name));

  return c && !Tech.isTech(c);
})

ES6 Generators函数基础知识/概念讲解,入门教程

Javascript ES6最令人振奋人心的众多新特性之一是一个新品种函数,它叫作generator。这个名字有一点怪异,但是行为看起来要更怪异一些。本文旨在解释它的基本工作原理,同时让你逐渐理解对于未来JS它们为何如此强大!

define('CONCATENATE_SCRIPTS', false);

Run-To-Completion(运行到结束)

我们通常对于传统函数(function)的预期就是从“运行到结束”,那么首先让我们来观察一下,generators与传统函数的差异。

无论你有没有意识到,你已经对function建立了相当牢固的基础认知,那就是:一旦一个函数(function)开始运行,它必须运行结束,其它的JS代码才能继续运行。

栗子🌰:

setTimeout(function () {
    console.log("Hello World");
}, 1);

function foo() {
    // 提示:没事不要像这样作死
    for (var i = 0; i <= 1E10; i++) {
        console.log(i);
    }
}

foo();
// 0..1E10
// "Hello World"

看这里,for循环会运行相当长一段时间才能结束,至少超过1毫秒以上,但是我们用来“Hello World”的计时器回调函数却不能打断foo()函数的运行,所以它只能安静的等待foo()函数运行结束才能轮到它执行。

如果 foo() 函数的运行可以被打断,感觉会不会很刺激? 这不会导致一场浩劫?

对于多线程变成来说,这毫无以为将是一场噩梦挑战,但是万分幸运的是在JS战场上这不是问题,因为JS向来是单线程(任何时间都只有一个命令/函数在执行)。

注意:Web Workers是一种多线程机制,它可以让JS运行在完全独立的子线程中,并且与主线程并行运算。但是这不会引发多线程并发症,原因是两个线程只能通过正常的异步事件进行通信,它是遵循事件循环规则的,就像“run-to-completion”要求的那样,一次执行一个命令/函数。

Run..Stop..Run(走走..停停..走走)

伴随ES6 generators,我们有了不同的函数类型,它可以在运行过程中暂停,可能暂停一次或者多次,稍后还可以恢复运行,其它的代码则可以在它暂停时运行。

如果你曾经听说过并发或线程编程,你也许会见过这个术语——“cooperative”(可合作的),简单的理解就是一个进程(本文指函数)自己选择何时将接受一个打断操作,那么它将可以和其它代码进行合作。

ES6 generator函数在它的并发行为中是“cooperative”(可合作的)。你可以在generator函数内部使用新关键字“yield”暂停函数运行,任何方法都不可以在外部暂停一个generator的运行;当它遇到一个“yield”时将暂停自己。

但是,一旦一个generator通过“yield”暂停住自己,它没办法自己恢复自己的运行,必须通过一个外在的操作恢复generator的运行。接下来我们将看到这一切是如何发生的。

所以,简单来看,一个generator可以多次停止/恢复运行,次数有你来决定。事实上,你可以让一个generator进行无限循环(就像臭名昭著的 while (true) {..} ),那么它将永远不会停止。虽然这通常是一个疯狂的操作或者一个错误,然而一个完全清醒的generator函数有时可能正是你需要的!

更重要的是,对于一个generator函数来说,伴随着它的运行,暂停/恢复命令不仅仅是一个操作,它同时可以进行双向数据传输,将数据传入/传出generator函数。对于一个正常的函数,你在开始执行时获取到参数,然后在运行结束时return一个结果值。但是对于generator函数,你可以通过“yeild”多次把信息传出,同时可以通过恢复命令再把信息传入。

翠花,上语法!

让我们深入体会一下这些新语法和扣人心弦的generator函数。

首先,新的声明语法:

function *foo() {
    // ...
}

看到那个*了吗?看起来既陌生又新颖。对于其他的语言,它看起来就像那恐怖的指针。但是不要慌张!这就仅仅是一个标记,告诉你这是一个generator函数。

你可能在其它文章或者文档中看到有这样使用的“function* foo() {}”,而不是“function *foo () {}”,区别在于*旁的空格。这两个写法都可以,但是我决定使用“function *foo () {}”,因为我觉得这样更准确一些,所以文中我都是这样使用的。

现在,我们来谈谈generator函数的内容如何写。Generator函数基本上就是正常的JS函数。在generator函数内部只有很少量的语法需要学习。

我们主要需要打交道的新东东就是上面提到的关键字“yield”。yield__ 被称为“yield 表达式”(而并不是一个声明),因为当我们恢复generator时,我们将向内部回传值,而传回的值就是 yield__ 表达式的结果。

栗子🌰:

function *foo() {
    var x = 1 + (yield "foo");
    console.log(x);
}

当执行到yield “foo”表达式时generator函数将暂停,同时把“foo”字符串值传出,无论generator何时恢复,恢复时传回的值将作为yield “foo”表达式的结果执行+1的运算然后赋值给变量x。

看到双向通信了吗?你发送值“foo”出去,暂停自己,稍后的某个时刻(可能是瞬间、可能是很久)generator将被恢复同时获得一个返回值。这就好像是yield执行了一个请求来获取返回值一样。

在一些情况下,你可以在表达式/声明中只使用yield来进行暂停,相当于一个假象的的undefined值被传出。🌰:

// note: foo(..) 不是一个 generator!!
function foo(x) {
    console.log("x: " + x);
}

function *bar() {
    yield; // 只是暂停
    foo( yield ); // 暂停并等待一个参数被传入foo函数
}

Generator Iterator(generator迭代器)

Iterator是一个特殊的行为类型,实际上是一种设计模式,其中我们可以通过调用next()依次步入一个有序值序列的每一个值。想象一下在一个含有5个值得数组([1, 2, 3, 4, 5])上使用迭代器的场景。第一个next()将返回1,第二个next()将返回2,以此类推。当所有值都被返回之后,next()将返回 null 或者 false 或者 其它的信号通知你已经迭代了数组中的所有值。

我们用来从外部控制generator函数的方式是去构造一个generator iterator并和它交互。这听起来就像看起来一样难懂。看看这无聊的🌰吧:

function *foo() {
    yield 1;
    yield 2;
    yield 3;
    yield 4;
    yield 5;
}

为了步入 *foo() generator函数的所有值,我们需要构造一个iterator(迭代器)。怎么搞?很简单!

 var it = foo();

什么!我们像正常调用函数一样调用generator函数,实际上并没有执行它的内部代码。

这可能有一点让你头大。你也可能很想知道,为什么不是 var it = new foo()。额,这原因很复杂,也不在我们讨论的范畴。。。

所以现在,想要去迭代我们的generator函数,只需这样做:

var message = it.next();

这将通过yield 1向我们返回 1,但是这并不是我们获取的唯一值。

console.log(message); // { value: 1, done: false }

我们实际上通过next()获取到的是一个object(对象),它包含一个 value 属性来承载 yield 的输出,一个布尔值 done 属性标明是否generator函数已经运行结束。

让我们继续运行我们的迭代器:

console.log( it.next() ); // { value: 2, done: false }
console.log( it.next() ); // { value: 3, done: false }
console.log( it.next() ); // { value: 4, done: false }
console.log( it.next() ); // { value: 5, done: false }

搞笑的是,当我们获取到数值 5 的时候,done 属性依然是 false。这是因为在技术上,generator函数并没有运行结束。我们仍然需要去执行最后一次 next() 指令,同时如果我们传入一个值,它将最为yield 5表达式的返回值。只有这样generator函数才完全执行完。

所以,现在:

console.log( it.next() ); // { value: undefined, done: true }

所以,最后的返回值表明我们的generator函数完全执行结束,但是结果里并没有返回值(因为我们已经消耗掉所有的 yield__ )。

你这时可能会好奇,能否在一个generator函数中使用 return,如果我这样做了,返回值能否通过value属性被传出。

能。。。

function *foo() {
    yield 1;
    return 2;
}

var it = foo();

console.log( it.next() ); // { value: 1, done: false }
console.log( it.next() ); // { value: 2, done: true }

也不能。。。

在generator函数中依赖return返回值可能并不是一个好方法,因为当通过 for..of (下面会讲到)的方式迭代generator函数时,最后一个 return 值会被遗弃掉。

为了完整起见,让我们整体看下当迭代一个generator函数时双向通信是如何进行的:

function *foo(x) {
    var y = 2 * (yield (x + 1));
    var z = yield (y / 3);
    return (x + y + z);
}

var it = foo( 5 );

// note: 这里并没有向next()传入任何数据
console.log( it.next() );      // {value: 6, done: false}
console.log( it.next( 12 ) );  // {value: 8, done: false}
console.log( it.next( 13 ) );  // {value: 42, done: true}

可以看到,我们仍然可以通过初始化迭代器实例时传入参数( 我们示例中的 x ),就像平时使用传统函数时那样,赋值 x 等于5;

对于第一个 next(..) ,我们不传入任何值。为何?因为目前尚无 yield 表达式接收传值。

但是如果我们执意向第一个 next(..) 传入参数,什么都不会发生。它只是一个被忽略的参数。ES6表示在这个示例中generator函数会忽略这个未被使用的参数。(注意:这样写会有兼容性问题,请具体测试)。

yield(x + 1) 将传出数值 6。第二个next(12) 调用会向等待中的 yield(x + 1) 表达式传入值 12,所以 y = 12 * 2 = 24。接下来随后的 yield(y / 3) 相当于 yield(24 / 3)将向外部传出值 8 ,第三个 next(13) 向等待中的 yield(y / 3)表达式传入值 13,使 z = 13。

最后,返回  x + y + z = 5 + 24 + 13 = 42,42也被当做generator函数的最后一个值被返回。

for..of

ES6在语法层面同样拥抱这种迭代模式,为迭代器的执行提供最直接的支持:for..of 循环。

栗子🌰:

function *foo() {
    yield 1;
    yield 2;
    yield 3;
    yield 4;
    yield 5;
    return 6;
}

for (var v of foo()) {
    console.log( v );
}
// 1 2 3 4 5

console.log( v ); // 仍然是 5,而不是 6。尼玛

如你所见,由foo()创建的迭代器会自动被 foo..of 循环捕获并进行迭代,一次迭代获取一个值,直到 done: true 出现。只要 done 仍然等于 false,value属性值便会被提取并赋值给迭代变量(这里指 v)。一旦 done 等于 true,循环迭代将结束(同时不会对最后的返回值 value 最任何处理,如果有的话)。

如上所述,你可以发现 for..of 循环会忽略并遗弃返回值6。同样,因为没有一个显式调用 next(),for..of循环在这种情况下将不能使用。

概述

好吧,这就是它的基础知识。如果仍然有一丝头大也不要担忧。大家第一次都是这样。

很自然你会惊奇这个新东东能为的编程做些什么。当然,对于它我们还有更多需要了解的。我们还只是撕开了封面而已。所以我们必须潜入深处去发现它的强大。

原文:https://davidwalsh.name/es6-generators

继续阅读

javascript数组完全随机排列(洗牌算法、shuffle算法)

关于错误算法的前言:(可以当做废话跳过,不谢)

  • 采用javascript的sort方法实现的随机洗牌算法是错误的。
  • 基于冒泡排序、插入排序等常规排序算法实现的随机洗牌算法是错误的。

采用排序实现的随机洗牌算法具体错误原理本文就不描述了,尔等可以用结尾的测试代码自行实验。大体来讲最终排序的结果是受排序前的状态影响,可以理解为排序前是正叙的话,结果也整体偏向于正序,排序前是倒序的话,结果也偏向于倒序。

进入主题:

随机算法代码:

时间复杂度为O(n).

function shuffle(array) {
  var m = array.length, t, i;

  // 处理余下待随机排序的元素…
  while (m) {

    // 取到一个带排序的元素…
    i = Math.floor(Math.random() * m--);

    // 把它和当前剩余元素序列的最后一个元素交换位置.
    t = array[m];
    array[m] = array[i];
    array[i] = t;
  }

  return array;
}

在上面的算法里,我们每一次循环从前 len – i 个元素里随机一个位置,将这个元素和第 len – i 个元素进行交换,迭代直到 i = len – 1 为止。

洗牌算法、shuffle算法 洗牌算法、shuffle算法 洗牌算法、shuffle算法 洗牌算法、shuffle算法 洗牌算法、shuffle算法

为了证明这个算法的正确性,我们设计一个测试的方法。假定这个排序算法是正确的,那么,将这个算法用于随机数组 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],如果算法正确,那么每个数字在每一位出现的概率均等。因此,将数组重复洗牌足够多次,然后将每次的结果在每一位相加,最后对每一位的结果取平均值,这个平均值应该约等于 (0 + 9) / 2 = 4.5,测试次数越多次,每一位上的平均值就都应该越接近于 4.5。所以我们简单实现测试代码如下:

function shuffle(array) {
  var m = array.length, t, i;

  // 处理余下待随机排序的元素…
  while (m) {

    // 取到一个带排序的元素…
    i = Math.floor(Math.random() * m--);

    // 把它和当前剩余元素序列的最后一个元素交换位置.
    t = array[m];
    array[m] = array[i];
    array[i] = t;
  }

  return array;
}

var arr = [0,1,2,3,4,5,6,7,8,9]; 
var res = [0,0,0,0,0,0,0,0,0,0]; 

var t = 10000; 
for(var i = 0; i < t; i++) { 
  var sorted = shuffle(arr.slice(0));
  sorted.forEach(function(o,i){ res[i] += o; }); 
}

res = res.map(function(o) { 
  return o / t; 
});

console.log(res);

测试结果:

40E72288-BCAE-4FA7-959E-7F5873EF6C5C

随机性的数学归纳法证明

对 n 个数进行随机:

  1. 首先我们考虑 n = 2 的情况,根据算法,显然有 1/2 的概率两个数交换,有 1/2 的概率两个数不交换,因此对 n = 2 的情况,元素出现在每个位置的概率都是 1/2,满足随机性要求。
  2. 假设有 i 个数, i >= 2 时,算法随机性符合要求,即每个数出现在 i 个位置上每个位置的概率都是 1/i。
  3. 对于 i + 1 个数,按照我们的算法,在第一次循环时,每个数都有 1/(i+1) 的概率被交换到最末尾,所以每个元素出现在最末一位的概率都是 1/(i+1) 。而每个数也都有 i/(i+1) 的概率不被交换到最末尾,如果不被交换,从第二次循环开始还原成 i 个数随机,根据 2. 的假设,它们出现在 i 个位置的概率是 1/i。因此每个数出现在前 i 位任意一位的概率是 (i/(i+1)) * (1/i) = 1/(i+1),也是 1/(i+1)。
  4. 综合 1. 2. 3. 得出,对于任意 n >= 2,经过这个算法,每个元素出现在 n 个位置任意一个位置的概率都是 1/n。

参考文章:

  • https://bost.ocks.org/mike/shuffle/
  • https://www.h5jun.com/post/array-shuffle.html

彻底理解0.1 + 0.2不等于0.3的原理

抛两个著名的js运算坑:

  1. 0.1 + 0.2 === 0.30000000000000004
  2. 1000000000000000128 === 1000000000000000129

IEEE 754 Floating-point

众所周知JS仅有Number这个数值类型,而Number采用的时IEEE 754 64位双精度浮点数编码。而浮点数表示方式具有以下特点:

  1. 浮点数可表示的值范围比同等位数的整数表示方式的值范围要大得多;
  2. 浮点数无法精确表示其值范围内的所有数值,而有符号和无符号整数则是精确表示其值范围内的每个数值;
  3. 浮点数只能精确表示m*2e的数值;
  4. 当biased-exponent为2e-1-1时,浮点数能精确表示该范围内的各整数值;
  5. 当biased-exponent不为2e-1-1时,浮点数不能精确表示该范围内的各整数值。

由于部分数值无法精确表示(存储),于是在运算统计后偏差会愈见明显。

想了解更多浮点数的知识可参考以下文章:

Why 0.1 + 0.2 === 0.30000000000000004?

在浮点数运算中产生误差值的示例中,最出名应该是0.1 + 0.2 === 0.30000000000000004了,到底有多有名?看看这个网站就知道了http://0.30000000000000004.com/。也就是说不仅是JavaScript会产生这种问题,只要是采用IEEE 754 Floating-point的浮点数编码方式来表示浮点数时,则会产生这类问题。下面我们来分析整个运算过程。

  1. 0.1 的二进制表示为 1.1001100110011001100110011001100110011001100110011001 1(0011)+ * 2^-4;
  2. 当64bit的存储空间无法存储完整的无限循环小数,而IEEE 754 Floating-point采用round to nearest, tie to even的舍入模式,因此0.1实际存储时的位模式是0-01111111011-1001100110011001100110011001100110011001100110011010;
  3. 0.2 的二进制表示为 1.1001100110011001100110011001100110011001100110011001 1(0011)+ * 2^-3;
  4. 当64bit的存储空间无法存储完整的无限循环小数,而IEEE 754 Floating-point采用round to nearest, tie to even的舍入模式,因此0.2实际存储时的位模式是0-01111111100-1001100110011001100110011001100110011001100110011010;
  5. 实际存储的位模式作为操作数进行浮点数加法,得到 0-01111111101-0011001100110011001100110011001100110011001100110100。转换为十进制即为0.30000000000000004。

Why 0.7 * 180===125.99999999998?

  1. 0.7实际存储时的位模式是0-01111111110-0110011001100110011001100110011001100110011001100110;
  2. 180实际存储时的位模式是0-10000000110-0110100000000000000000000000000000000000000000000000;
  3. 实际存储的位模式作为操作数进行浮点数乘法,得到0-10000000101-1111011111111111111111111111111111111111101010000001。转换为十进制即为125.99999999998。

Why 1000000000000000128 === 1000000000000000129?

  1. 1000000000000000128实际存储时的位模式是0-10000111010-1011110000010110110101100111010011101100100000000001;
  2. 1000000000000000129实际存储时的位模式是0-10000111010-1011110000010110110101100111010011101100100000000001;
  3. 因此1000000000000000128和1000000000000000129的实际存储的位模式是一样的。

Solution

到这里我们都理解只要采取IEEE 754 FP的浮点数编码的语言均会出现上述问题,只是它们的标准类库已经为我们提供了解决方案而已。而JS呢?显然没有。坏处自然是掉坑了,而好处恰恰也是掉坑了:)

针对不同的应用需求,我们有不同的实现方式。

Solution 0x00 – Simple implementation

对于小数和小整数的简单运算可用如下方式

function numAdd(num1/*:String*/, num2/*:String*/) { 
    var baseNum, baseNum1, baseNum2; 
    try { 
        baseNum1 = num1.split(".")[1].length; 
    } catch (e) { 
        baseNum1 = 0; 
    } 
    try { 
        baseNum2 = num2.split(".")[1].length; 
    } catch (e) { 
        baseNum2 = 0;
    } 
    baseNum = Math.pow(10, Math.max(baseNum1, baseNum2)); 
    return (num1 * baseNum + num2 * baseNum) / baseNum; 
};

Solution 0x01 – math.js

若需要复杂且全面的运算功能那必须上math.js,其内部引用了decimal.js和fraction.js。功能异常强大,用于生产环境上妥妥的!

Solution 0x02 – D.js

D.js算是我的练手项目吧,截止本文发表时D.js版本为V0.2.0,仅实现了加、减、乘和整除运算而已,bug是一堆堆的,但至少解决了0.1+0.2的问题了。

var sum = D.add(0.1, 0.2)
console.log(sum + '') // 0.3

var product = D.mul("1e-2", "2e-4")
console.log(product + '') // 0.000002

var quotient = D.div(-3, 2)
console.log(quotient + '') // -(1+1/2)

解题思路:

  1. 由于仅位于Number.MIN_SAFE_INTEGER和Number.MAX_SAFE_INTEGER间的整数才能被精准地表示,也就是只要保证运算过程的操作数和结果均落在这个阀值内,那么运算结果就是精准无误的;
  2. 问题的关键落在如何将小数和极大数转换或拆分为Number.MIN_SAFE_INTEGER至Number.MAX_SAFE_INTEGER阀值间的数了;
  3. 小数转换为整数,自然就是通过科学计数法表示,并通过右移小数点,减小幂的方式处理;(如0.000123 等价于 123 * 10-6)
  4. 而极大数则需要拆分,拆分的规则是多样的。
    1. 按因式拆分:假设对12345进行拆分得到 5 * 2469;
    2. 按位拆分:假设以3个数值为一组对12345进行拆分得到345和12,而实际值为12*1000 + 345。
      就我而言,1 的拆分规则结构不稳定,而且不直观;而 2 的规则直观,且拆分和恢复的公式固定。
  5. 余数由符号位、分子和分母组成,而符号与整数部分一致,因此只需考虑如何表示分子和分母即可。
  6. 无限循环数则仅需考虑如何表示循环数段即可。(如10.2343434则分成10.23 和循环数34和34的权重即可)

得到编码规则后,那就剩下基于指定编码如何实现各种运算的问题了。

  1. 基于上述的数值编码规则如何实现加、减运算呢?
  2. 基于上述的数值编码规则如何实现乘、除运算呢?(其实只要加、减运算解决了,乘除必然可解,就是效率问题而已)
  3. 基于上述的数值编码规则如何实现其它如sin、tan、%等数学运算呢?

另外由于涉及数学运算,那么将作为add、sub、mul和div等入参的变量保持如同数学公式运算数般纯净(Persistent/Immutable Data Structure)是必须的,那是否还要引入immutable.js呢?(D.js现在采用按需生成副本的方式,可预见随着代码量的增加,这种方式会导致整体代码无法维护)

Conclusion

依照我的尿性,D.js将采取不定期持续更新的策略(待我理解Persistent/Immutable Data Structure后吧:))。欢迎各位指教!

尊重原创,转载请注明来自:http://www.cnblogs.com/fsjohnhuang/p/5115672.html ^_^肥子John

CommonJS exports 与 module.exports 的区别

我们只需知道三点即可知道 exportsmodule.exports 的区别了:

  1. exports 是指向的 module.exports 的引用
  2. module.exports 初始值为一个空对象 {},所以 exports 初始值也是 {}
  3. require() 返回的是 module.exports 而不是 exports

所以:

  • 我们通过
      var name = 'nswbmw';
      exports.name = name;
      exports.sayName = function() {
        console.log(name);
      }

    exports 赋值其实是给 module.exports 这个空对象添加了两个属性而已,上面的代码相当于:

      var name = 'nswbmw';
      module.exports.name = name;
      module.exports.sayName = function() {
        console.log(name);
      }
  • 我们通常这样使用 exportsmodule.exports

    一个简单的例子,计算圆的面积:

    使用 exports

    app.js

      var circle = require('./circle');
      console.log(circle.area(4));

    circle.js

      exports.area = function(r) {
        return r * r * Math.PI;
      }

    使用 module.exports

    app.js

      var area = require('./area');
      console.log(area(4));

    area.js

      module.exports = function(r) {
        return r * r * Math.PI;
      }

    上面两个例子输出是一样的。你也许会问,为什么不这样写呢?

    app.js

      var area = require('./area');
      console.log(area(4));

    area.js

      exports = function(r) {
        return r * r * Math.PI;
      }

    运行上面的例子会报错。这是因为,前面的例子中通过给 exports 添加属性,只是对 exports 指向的内存做了修改,而

      exports = function(r) {
        return r * r * Math.PI;
      }

    其实是对 exports 进行了覆盖,也就是说 exports 指向了一块新的内存(内容为一个计算圆面积的函数),也就是说 exportsmodule.exports 不再指向同一块内存,也就是说此时 exportsmodule.exports 毫无联系,也就是说 module.exports 指向的那块内存并没有做任何改变,仍然为一个空对象 {} ,也就是说 area.js 导出了一个空对象,所以我们在 app.js 中调用 area(4) 会报 TypeError: object is not a function 的错误。

    所以,一句话做个总结:当我们想让模块导出的是一个对象时, exportsmodule.exports 均可使用(但 exports 也不能重新覆盖为一个新的对象),而当我们想导出非对象接口时,就必须也只能覆盖 module.exports

  • 我们经常看到这样的用写法:
      exports = module.exports = somethings

    上面的代码等价于

      module.exports = somethings
      exports = module.exports

    原因也很简单, module.exports = somethings 是对 module.exports 进行了覆盖,此时 module.exportsexports 的关系断裂,module.exports 指向了新的内存块,而 exports 还是指向原来的内存块,为了让 module.exportsexports 还是指向同一块内存或者说指向同一个 “对象”,所以我们就 exports = module.exports

javascript判断ie浏览器版本号

msie = int((/msie (d+)/.exec((navigator.userAgent).toLowerCase()) || [])[1]);
/* IE 11改变了它的userAgent格式,所以要进一步判断 */
if (isNaN(msie)) {
    msie = int((/trident/.*; rv:(d+)/.exec((navigator.userAgent).toUpperCase()) || [])[1]);
}

js手动实现英文字母大小写转换

一.将英文字母转换成小写

function manualLowercase(s) {
    s.replace(/[A-Z]/g, function(ch) {return String.fromCharCode(ch.charCodeAt(0) | 32);});
}

二.将英文字母转换成大写

function manualUppercase(s) {
    s.replace(/[a-z]/g, function(ch) {return String.fromCharCode(ch.charCodeAt(0) & ~32);});
}

js获取年、月、日、时、分、秒,实时更新变化脚本

平时总遇到实时时间的脚本需求,没办法总结一个以供copy

var _date = new Date(),
    _year = _date.getFullYear(),
   _month = (_date.getMonth() + 1) < 10 ? '0' + (_date.getMonth() + 1) : (_date.getMonth() + 1),
     _day = _date.getDate() < 10 ? '0' + _date.getDate() : _date.getDate(),
   _hours = _date.getHours() < 10 ? '0' + _date.getHours() : _date.getHours(),
 _minutes = _date.getMinutes() < 10 ? '0' + _date.getMinutes() : _date.getMinutes(),
 _seconds = _date.getSeconds() < 10 ? '0' + _date.getSeconds() : _date.getSeconds();

setInterval(function() {
    document.getElementById('J_clock').innerHTML = _year + '年' + _month + '月' + _day + '日' + _hours + '时' + _minutes + '分' + _seconds + '秒';
}, 1000);