金汇移动开发团队

技术博客


  • 首页

  • 分类

  • 归档

  • 标签

  • 管理

解决cnpm私服无法安装react-native的问题

发表于 2018-11-06

起因

需要在RN项目中安装react-native的依赖,由于项目package.json中既有对私有库的依赖,也有对第三方库的依赖,因此本地镜像地址使用的是私有cnpm仓库(2.10.0)。但是在安装react-native的时候,其中一个RN依赖的库metro无法安装,报No compatible version found: metro的错误,导致RN也安装失败。

一些尝试

首先google一下错误信息,发现这也是个常见错误,但不同的库错误原因不一样,无法聚焦,所以没有继续深入看这个问题。

然后去cnpm私服的web端做了一个同步metro的操作http://cnpm-web.jinhui365.cn/sync/metro,发现报错ER_TRUNCATED_WRONG_VALUE_FOR_FIELD: Incorrect string value:,把这错误在google了一下,第一个结果中的问题与这个错误非常一致,最高赞的回复也很明确,是因为mysql的某个字段使用的是utf8编码,只能支持3个字节,改成utf8mb4编码就可以了。

处理步骤

修改cnpm源码

  1. 将数据库编码和表的编码都改成utf8mb4

  2. 修改models/module.js文件,在module定义的options中增加charset: 'utf8mb4'的配置

  3. 修改common/sequelize.js文件,config.database的定义中,增加dialectOptions: {charset:'utf8mb4'},的配置

  4. 在根目录执行bin/nodejsctl stop,bin/nodejsctl start重启服务。

  5. 尝试通过cnpm私服安装react-native,正常通过

React Native iOS本地私有库实践

发表于 2018-10-31

起因

Facebook官方对react-native(下面简称RN)在iOS端的集成方式为本地Pod库,这种方式有两个缺点:

  1. 项目更新RN依赖版本较为繁琐,需要对项目中的node_modules文件夹进行文件操作,这种方式不利于模块化和版本管理

  2. 对pod形式的库项目支持不友好,pod库的依赖只能支持name+version的形式,无法支持本地源码形式的依赖

综上,需要在公司私有Pod仓库中添加对RN的支持。

一些尝试

常规方式

常规方式Pod库的常规操作方式为先lint再push,这种方式在RN上会遇到很多错误,解决了一批错误后最终停止在一个x86_64的错误上:Undefined symbols for architecture x86_64,这是一个比较常见的编译错误,但是尝试了很多方式后仍然无法解决。这里记录下关键问题,供后续参考:

  • lint和push的时候需要增加几个关键选项:
1
2
3
--use-libraries		#不加会报一些c++库找不到的错误
--allow-warnings #不加只有warning也无法lint通过
--verbose #增加一些关键信息
  • 如果对本地仓库有依赖,sources要指定本地私有仓库,不然默认是查找Cocoapods官方仓库

曲线方式

另外一个思路是考虑先把RN整体打成framework,再集成到Pod库中。这种方式最终停止在一个错误:invalid bitcode signature,跟上面的问题一样,没有搞定。

手动操作Spec仓库方式

在没有思路的时候,在google尝试搜索关键词react-native podspec,发现一篇文章:私有Pods集成react-native库,里面分享通过pod ipc spec命令将.podspec文件转成.podspec.json格式文件,然后将代码和.podspec.json分别提交到代码仓库和Pod仓库的方式,绕过lint和push的常规步骤。

这里需要注意的是,代码提交到私有源码库和podspec提交到私有Pod库是互相独立的操作。可以只提交Podspec到私有Pod库,但是代码提交到到私有源码库有两个好处

1
2
1. 内部网络更新速度快
2. 可以修改代码后自定义版本

尝试了一下,虽然遇到一些编译错误,但不是整体编译不过,而是一些RN内部类找不到,说明至少RN放到Pod仓库了。沿着这个思路往下探索,最终解决。

其中RN依赖了一些第三方库,包括folly、yoga、glog等,对于RN依赖的第三方库,处理原则是如果在官方Pod仓库没有对应的版本,都需要以私有Pod的方式提供。具体需要处理哪些库,需要根据当前RN版本来判断(例如Folly的2016.10.31.00在官方库没有,但是2016.09.26.00却能支持)。

操作步骤

  1. 去https://github.com/facebook/react-native/tags下载对应版本的RN源码包,解压缩后将源码提交到本地仓库`git@gitlab.jinhui365.cn:mobile/react-native.git` master分支。这里需要注意的问题是要保证该版本所有文件都会提交,包括.开头的文和文件夹。
  2. 在gitlab上为新版本打一个tag,tag名称约定为v{版本号},例如版本号为0.57.4,则tag名称为v0.57.4。
  3. 进入源码根目录,执行pod ipc spec React.podspec >> React.podspec.json。这里需要注意的是不要污染本地源码仓库,可以在另外一份源码拷贝上操作。
  4. 打开React.podspec.json文件,将source字段修改为本地的RN源码仓库地址和对应地址

    1
    2
    3
    4
    "source": {
    "git": "git@gitlab.jinhui365.cn:mobile/react-native.git",
    "tag": "v0.57.4"
    }
  5. 将修改后的React.podspec.json放到私有Pod仓库对应的目录下提交。例如React/0.57.4/React.podspec.json

  6. 在源码根目录下,进入ReactCommon/yoga,执行pod ipc spec yoga.podspec >> yoga.podspec.json
  7. 打开yoga.podspec.json,将source字段修改为本地的RN源码仓库地址和对应地址,并将source_files字段修改为"ReactCommon/yoga/**/*.{cpp,h}",public_header_files字段修改为:"ReactCommon/yoga/yoga/{Yoga,YGEnums,YGMacros}.h"

  8. 将修改后的yoga.podspec.json放到私有Pod仓库对应的目录下提交。例如yoga/0.57.4.React/yoga.podspec.json

  9. 本地址行pod repo update {仓库名}

  10. 至此,可以在项目中通过name+version的形式引用RN的pod依赖。

0.53.3的问题

RN在0.53.3上有一些编译问题,李杨在React-Native混编学习#iOS环境搭建 提到过相关解决方案,已按照该方案经将源码修改后tag为0.53.4。可以通过引用0.53.4来规避这些问题。

私有库依赖私有库

金汇移动端目前负责的业务较多,不同的业务之间会复用工具库(如路由库、日志库等)。同一个业务功能,有可能需要在不同的App之间复用(如民工汇、支付SDK)。因此,依赖的路径为APP->业务库->非业务库。

iOS的依赖管理与Android不同。Android统一使用gradle文件来管理依赖,而iOS主项目使用Podfile管理,库项目使用podspec管理,并且库项目无法依赖本地文件。

因此新建了私有库PodLibTest来测试了下,编译通过。这里记录下常见命令:

1
2
3
4
5
pod lib create PodLibTest	#创建pod库项目
pod update --no-repo-update #更新当前pod项目依赖
pod repo update {pod仓库名称} #更新本地pod仓库
pod lib lint --use-libraries --allow-warnings --verbose --sources=git@gitlab.jinhui365.cn:iOS/JHJRSpecs.git,https://github.com/CocoaPods/Specs.git #这些参数上面提到过,比较关键
pod repo push JHJR PodLibTest.podspec --use-libraries --allow-warnings --verbose #同样关注下参数

总结

  1. Cocoapods在依赖管理方面不够完善,提升了iOS工程化的难度等级。因此需要iOS工程师对相关领域知识研究地更深入,才能达到与Android同水平的工程化程度。

  2. 面对非常见疑难问题,通过简单的关键词搜索可能不太好找到解决方案。需要从两个方面着手,一方面多尝试关键词组合,找到和你遇到同样问题的人写的文章或帖子;一方面深入研究官方文档货项目源码,从原理层着手分析问题。

React-Native混编学习

发表于 2018-05-08

React-Native混编学习

本篇主要涉及的是App和RN的混合开发环境搭建,对于基本的RN环境搭建请自行查阅文档。

这里需要着重注意的是全局依赖:

  • node v8.1.3(nvm管理)
  • react-native-cli 0.53.3(npm全局包)
  • nrm(npm全局包,主要是为了管理npm源,切换npm源为taobao)
  • gulp(npm全局包,主要是打包zip压缩)

最好的方式是使用react-native init生成一个新的RN项目,参考它的package.json依赖。这里我使用的是"react": "16.2.0" "react-native": "0.53.3"

APP配合

  • 环境搭建
    • 依赖如何引入
  • 接口定义
    • 获取当前用户信息
    • 获取当前环境状态
    • 结束RN activity
    • 路由跳转结构定义(互相跳转的接口)
  • RN热更部分完善(Android和iOS)
    • 资源包地址重定义
    • 热更代码完善
  • RN开发调试
    • 兼容开发环境,方便我们调试(Bundle地址,开发模式启动)
  • 打包脚本修改
    • 依赖如何注入
    • 编译问题解决
  • 部分常量配置
    • RN服务地址常量
    • RN主组件名称
    • Bundle地址
  • RN路由定义
  • 登陆验证问题

android环境搭建

基础部分

  • RNApp嵌入原生
  • 原生跳转RN路由
  • 原生和RN的通信
  • RN资源管理
  • RN开发调试
  • 其他问题
    • 原生加载RN白屏问题
    • 回退问题

RNApp嵌入原生

1. 依赖引入

引入node_modules/react-native/android

  • 主build.gradle 添加

    maven {
        // All of React Native (JS, Android binaries) is installed from npm
        url "$rootDir/../node_modules/react-native/android"
    }
    
  • app/build.gradle 添加 compile "com.facebook.react:react-native:0.53.3"

    后需改为我们的android包管理

2. MyApplication类继承ReactApplication

  • onCreate中添加SoLoader.init(this, false);
  • Override public ReactNativeHost getReactNativeHost() { return mReactNativeHost; }代码如下:

    private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
      @Nullable
      @Override
      protected String getJSBundleFile() { // 定义RN Bundle文件地址
        return super.getJSBundleFile(); // 默认地址为assets/index.android.bundle
      }
    
      @Override
      public boolean getUseDeveloperSupport() { // 定义DEBUG模式
          return BuildConfig.DEBUG;
      }
    
      @Override
      protected List<ReactPackage> getPackages() { // RN包载入
          return Arrays.<ReactPackage>asList(
                  new MainReactPackage(),
                  new CommPackage(); // 载入公共包,见原生和RN通信
          );
      }
    };
    

后需重新定义Bundle文件地址,使之能够热更。

3. RNActivity编写

public class MyReactActivity extends ReactActivity {

  public Bundle getBundle() { // 获取props入参
      return getIntent().getExtras();
  }

  protected @Nullable String getMainComponentName() { // 定义RN组件名称
      return "MyReactNativeApp";
  }

  @Override
  protected ReactActivityDelegate createReactActivityDelegate() { // 通过getLaunchOptions传值
      return new ReactActivityDelegate(this, getMainComponentName()) { 
          @Nullable
          @Override
          protected Bundle getLaunchOptions() {
              return getBundle();
          }
      };
  }

}

至此,通过startActivity就能够正常打开一个RNApp了。

原生跳转RN路由

这里跳转可以有很多方式,不过最根本的是如何通过原生将要跳转的路由传递给RN。
主要分为两大类方式:(见本篇原生和RN通信)

  • 主动传递
  • 被动传递

这里我采用的是主动传递中的Props传递。

在原生开启RNApp的时候,可以传递一个Bundle作为最初的Props,路由及部分参数信息通过这个Bundle带给RN。

Android部分

  • ReactActivityDelegate类

    protected void loadApp(String appKey) {
      if (mReactRootView != null) {
        throw new IllegalStateException("Cannot loadApp while app is already running.");
      }
      mReactRootView = createRootView();
      mReactRootView.startReactApplication(
        getReactNativeHost().getReactInstanceManager(),
        appKey,
        getLaunchOptions()); // 在这里有个getLaunchOptions方法,就是传递Bundle的地方
      getPlainActivity().setContentView(mReactRootView);
    }
    
    protected @Nullable Bundle getLaunchOptions() {
      return null;
    }
    
  • MyReactActivity类
    我们这里通过在MyReactActivity类里重载getLaunchOptions方法,传递Bundle(参考RNApp嵌入原生代码)

RN部分
RN这里采用了react-native-router-flux作为路由管理插件。这里需要注意的是版本问题,测试发现"react-native-router-flux": "^4.0.0-beta.25", "react-navigation": "^1.0.0-beta.22"可用。

相关代码如下:

import React from 'react';
import { Router, Scene, Actions } from 'react-native-router-flux';
import { getUserInfo, finishActivity } from './communication'

import PageOne from './modules/PageOne'
import PageTwo from './modules/PageTwo'
import PageThree from './modules/PageThree'
import PageFour from './modules/PageFour'

export default class App extends React.Component {
  constructor(props) {
    super(props);
    console.log("RN启动");
  }

  componentDidMount(){
    const rnKey = this.props.rnKey || "PageOne";
    Actions.reset(rnKey, this.props);
  }

  // 导航栏回退方法
  onBack () {
    let popRouter = Actions.pop();
    !popRouter && finishActivity();
  }

  render() {
    return (
      <Router>
        <Scene key="root" hideNavBar={true}>
          <Scene key="PageOne" back={true} hideNavBar={false} component={PageOne} title="PageOne" onBack={() => this.onBack()}/>
          <Scene key="PageTwo" back={true} hideNavBar={false} component={PageTwo} title="PageTwo" onBack={() => this.onBack()}/>

          {/* 用户信息获取,用户已登陆 */}
          <Scene key="PageThree" back={true} hideNavBar={false} component={PageThree} title="PageThree" onBack={() => this.onBack()}/>
          <Scene key="PageFour" back={true} hideNavBar={false} component={PageFour} title="PageFour" onBack={() => this.onBack()}/>
        </Scene>
      </Router>
    )
  }
}

原生和RN的通信

上面说道通信分为两种,主动传递和被动传递。这里的主动/被动是以原生为参照。具体的可以参考和原生端通信,这里只贴关键部分的代码。如下:

Android部分

  • Package类

    package xxx;
    
    import com.facebook.react.ReactPackage;
    import com.facebook.react.bridge.NativeModule;
    import com.facebook.react.bridge.ReactApplicationContext;
    import com.facebook.react.uimanager.ViewManager;
    
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;
    
    public class CommPackage implements ReactPackage {
        public CommModule mModule;
    
        /**
        * 创建Native Module
        * @param reactContext
        * @return
        */
        @Override
        public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
            List<NativeModule> modules = new ArrayList<>();
            mModule = new CommModule(reactContext);
            modules.add(mModule);
            return modules;
        }
    
        @Override
        public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
            return Collections.emptyList();
        }
    }
    
  • module类

    public class CommModule extends ReactContextBaseJavaModule {
    
        private ReactApplicationContext mContext;
        public static final String MODULE_NAME = "commModule";
        public static final String EVENT_NAME = "nativeCallRn";
        public static final String EVENT_NAME1 = "getPatchImgs";
        /**
        * 构造方法必须实现
        * @param reactContext
        */
        public CommModule(ReactApplicationContext reactContext) {
            super(reactContext);
            this.mContext = reactContext;
        }
    
        /** 主动传递
        * Native调用RN
        * @param msg
        */
        public void nativeCallRn(String msg) {
            mContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
                    .emit(EVENT_NAME,msg);
        }
    
        /**
        * Callback 方式
        * rn调用Native,并获取返回值
        * @param msg
        * @param callback
        */
        @ReactMethod
        public void rnCallNativeFromCallback(String msg, Callback callback) {
    
            // 1.处理业务逻辑...
            String result = "处理结果:" + msg;
            // 2.回调RN,即将处理结果返回给RN
            callback.invoke(result);
        }
    
        /**
        * Promise
        * @param msg
        * @param promise
        */
        @ReactMethod
        public void rnCallNativeFromPromise(String msg, Promise promise) {
    
            Log.e("---","adasdasda");
            // 1.处理业务逻辑...
            String result = "处理结果:" + msg;
            // 2.回调RN,即将处理结果返回给RN
            promise.resolve(result);
        }
    
        /**
        * 向RN传递常量
        */
        @Nullable
        @Override
        public Map<String, Object> getConstants() {
            Map<String,Object> params = new HashMap<>();
            params.put("Constant","我是常量,传递给RN");
            return params;
        }
    
        @ReactMethod
        public void startActivityFromJS(String path, ReadableMap params){
            try{
                Activity currentActivity = getCurrentActivity();
    
                if(null!=currentActivity){
                    Map intentParams = ((ReadableNativeMap) params).toHashMap();
    
                    <!--startActivity-->
                }
            }catch(Exception e){
                throw new JSApplicationIllegalArgumentException(
                        "不能打开Activity : "+e.getMessage());
            }
        }
    
        @ReactMethod
        public void dataToJS(Callback successBack, Callback errorBack){
            try{
                Activity currentActivity = getCurrentActivity();
                String result = currentActivity.getIntent().getStringExtra("data");
    
                successBack.invoke(result);
            }catch (Exception e){
                errorBack.invoke(e.getMessage());
            }
        }
    }
    

最后在MyApplication中注入,查看上面嵌入RNApp部分的【载入公共包,见原生和RN通信】注释。

RN部分
RN部分正常引用调用就好,代码如下:

import { NativeModules } from 'react-native';

const { commModule } = NativeModules;

export function startActivity (appPath, params = {}) {
  if(appPath) {
    commModule.startActivityFromJS(appPath, params);
  }
}

export function finishActivity () {
  commModule.activityFinish();
}

export function getUserInfo() {
  return commModule.getUserInfo()
}

export function getEnv() {}

// test
export function rnCallNativeFromCallback(msg, callback) {
  commModule.rnCallNativeFromCallback(msg, callback);
}

export function rnCallNativeFromPromise(msg) {
  return commModule.rnCallNativeFromPromise(msg);
}

export function getConstans() {
  console.log(commModule.test)
}

RN资源管理

测试发现如果将打包的Bundle文件和资源文件放到统一目录下,就可以正常引用资源。
参考命令react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/assets

有需要做热更的可以将资源放到SD卡中,只需要指定以下Bundle文件路径,将资源和Bundle文件放到一起就好。

RN开发调试

这里只说我的调试方式,更详细的请查阅调试文档。

  1. 在AndroidManifest.xml中添加<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />启用调试模式。
  2. 通过npm start启动RN服务,即node node_modules/react-native/local-cli/cli.js start。
  3. 真机安装APP进入RNApp界面,摇动手机可以打开开发者列表,通过设置Dev Host可以连接RN服务。Reload可以重载RNApp。
  4. 通过在Android Studio输出中检索React可以查看RN的输出(包括console)。

这里的调试基于HOST Bundle获取方式上,也就是说你的Bundle获取地址应当是默认的super.getJSBundleFile()。

其他问题

  1. 原生加载RN白屏问题
    白屏是因为加载Bundle文件过慢导致的,这个网上有很多的解释了。这里我的解决办法是在App开启动画的Activity里执行了一次加载。

    private void preLoadReactNative() {
        // 1.创建ReactRootView
        ReactRootView rootView = new ReactRootView(this);
        rootView.startReactApplication(
                ((ReactApplication) getApplication()).getReactNativeHost().getReactInstanceManager(),
                "MyReactNativeApp",
                null);
    }
    
  2. 返回按键问题
    这里的返回键分为两种,一种是手机硬件后退按键,另一种是导航栏后退按键。手机后退这里无需做处理,导航栏后退如下处理:

    • RN

      onBack () {
        let popRouter = Actions.pop();
        !popRouter && finishActivity(); // 判断是否为RN首页,返回原生上个页面。
      }
      
    • 原生见本篇finishActivity方法。

部分代码参考ReactNativeApp项目

iOS环境搭建

基于Android环境搭建,iOS部署大同小异,这里仅介绍不同的部分。

依赖安装

修改Podfile,添加如下内容:(环境配置参考集成到现有原生应用)

# 'node_modules'目录一般位于根目录中
# 但是如果你的结构不同,那你就要根据实际路径修改下面的`:path`
pod 'React', :path => '../node_modules/react-native', :subspecs => [
    'Core',
    'CxxBridge', # 如果RN版本 >= 0.45则加入此行
    'DevSupport', # 如果RN版本 >= 0.43,则需要加入此行才能开启开发者菜单
    # 这里注意一下!!!添加这些解决react-navigation的native module not be null的问题
    'ART',
    'RCTActionSheet',
    'RCTGeolocation',
    'RCTImage',
    'RCTNetwork',
    'RCTPushNotification',
    'RCTSettings',
    'RCTText',
    'RCTVibration',
    'RCTWebSocket', # 这个模块是用于调试功能的
    'RCTLinkingIOS',
]
# 如果你的RN版本 >= 0.42.0,则加入下面这行
pod "yoga", :path => "../node_modules/react-native/ReactCommon/yoga"

# 如果RN版本 >= 0.45则加入下面三个第三方编译依赖
pod 'DoubleConversion', :podspec => '../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec'
pod 'GLog', :podspec => '../node_modules/react-native/third-party-podspecs/GLog.podspec' // 这里修改glog为GLog
pod 'Folly', :podspec => '../node_modules/react-native/third-party-podspecs/Folly.podspec'

end

然后运行pod install安装依赖

这里是离线的包,怎么去合并到打包流程?(可以通过npm install去解决,不过比较麻烦)

RNApp嵌入原生

和安卓一样,需要写一个类似于activity的壳子供给RN。

ReactView.h

#import <Foundation/Foundation.h>

@interface ReactView : UIViewController

@end

ReactView.m

#import "ReactView.h"
#import <React/RCTRootView.h>
#import <React/RCTBridgeModule.h>
#import "commModule.h" # 这里是RN和原生交互所需要的一些方法,可先去掉

@interface ReactView()<UINavigationBarDelegate>
@end
@implementation ReactView

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.

    # 这里的http和localhost要给一下权限
    NSString * strUrl = @"http://localhost:8081/index.bundle?platform=ios&dev=true";    
    NSURL * jsCodeLocation = [NSURL URLWithString:strUrl];

    # 除http之外,和安卓一样,也可以通过bundle
    RCTRootView * rootView = [[RCTRootView alloc] 
                                initWithBundleURL:jsCodeLocation
                                moduleName:@"MyReactNativeApp"
                                initialProperties:nil
                                launchOptions:nil];
    self.view = rootView;
}

#pragma mark - 导航条处理,取消RN的导航栏
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
    [navigationController setNavigationBarHidden:YES animated:YES];
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

Bundle地址重定义

NSString *cachePath = @"XXX";
cachePath = [cachePath stringByAppendingPathComponent:@"index.ios.bundle"];
NSURL *jsCodeLocation = [NSURL URLWithString:cachePath];

中间遇到了几个编译问题导致build失败。问题与解决方法如下:

  • React Native iOS: Could not build module ‘yoga’: ‘algorithm’ file not found

  • RCTReconnectingWebSocket.h文件的#import <fishhook/fishhook.h> 显示error: ‘fishhook/fishhook.h’ file not found

    "scripts": {
        "postinstall": "sed -i '' 's#<fishhook/fishhook.h>#\"fishhook.h\"#g' ./node_modules/react-native/Libraries/WebSocket/RCTReconnectingWebSocket.m"
    }
    
  • resolveRCTValueAnimatedNode
    sed -i '' 's/#import <RCTAnimation\\/RCTValueAnimatedNode.h>/#import \"RCTValueAnimatedNode.h\"/' ./node_modules/react-native/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.h

这样配个正常的iOS跳转地址,就可以正常跳转到RN了。这里的RN界面最好用一件简单的hello world测试下。

编译问题怎么合到打包代码中?

原生和RN的通信

通信一样是原生提供一下方法,可供给RN调用,直接上代码。

commModule.h

#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>

@interface commModule : NSObject <RCTBridgeModule>

@end

commModule.m

#import "commModule.h"
#import <React/RCTConvert.h>

@implementation commModule

RCT_EXPORT_MODULE();

RCT_EXPORT_METHOD(rnCallNativeFromCallback:(NSString *)msg callback:(RCTResponseSenderBlock)callback)
{
    NSString *result = [@"处理结果:" stringByAppendingString:msg];
    callback(@[[NSNull null], result]);
}

RCT_REMAP_METHOD(rnCallNativeFromPromise, msg:(NSString *)msg
                findEventsWithResolver:(RCTPromiseResolveBlock)resolve
                rejecter:(RCTPromiseRejectBlock)reject)
{
    NSString *result = [@"处理结果:" stringByAppendingString:msg];
    resolve(result);
}

RCT_REMAP_METHOD(getUserInfo, msg:(NSDictionary *)msg
                resolver:(RCTPromiseResolveBlock)resolve
                rejecter:(RCTPromiseRejectBlock)reject)
{

    resolve(@{@"name": @"iOS"});
}

RCT_EXPORT_METHOD(startActivityFromJS:(NSString *)path params:(NSDictionary *)params)
{
    # 路由跳转,注意UI主线程
}

RCT_EXPORT_METHOD(activityFinish)
{
    # 结束RN finish,注意UI主线程
}

- (NSDictionary *)getConstants
{
    int n = arc4random_uniform(100);
    return @{ @"test": @"test" };
}

@end

这样就可以在RN端调用了,调用方法和Android一样。

RN资源管理

测试发现如果将打包的Bundle文件和资源文件放到统一目录下,就可以正常引用资源。
参考命令react-native bundle --platform ios --dev false --entry-file index.ios.js --bundle-output ReactNative/ios/index.ios.bundle --assets-dest ReactNative/ios

有需要做热更的可以指定以下Bundle文件路径,将资源和Bundle文件放到一起就好。

RN开发调试

这里只说我的调试方式,更详细的请查阅调试文档。

  1. 通过initWithBundleURL方式连接本地RN服务
  2. 通过npm start启动RN服务,即node node_modules/react-native/local-cli/cli.js start。
  3. command + R可以在RN界面Reload RN
  4. 通过在Xcode输出中检索React可以查看RN的输出(包括console)。

遗留问题

  • android项目依赖maven { url "$rootDir/../node_modules/react-native/android" }
  • iOS项目依赖怎么合并到打包脚本中
  • iOS编译问题如何在流程中解决
  • 登陆验证问题

资源地址

服务器172.16.192.249/mobile_node/魏晓峰/RN

ESLint入门与实践初步

发表于 2017-10-25

ESLint 入门与实践


一、主流JavaScript检查调研

JSLint、JSHint、JSCS、ESLint 四种工具有相同的基本方式工作。它们都有一套用户分析、报告js文件错误的规则。他们都可以通过npm安装、通过命令行使用。下面比对一下各自的优缺点

JSLint

JSLint 是其中最老的工具。在2002年 Douglas Crockford,业内尊称“道爷”开发了该工具,根据其经验,强制使用js语言中精粹的部分。如果你同意这些精粹,JSLint能成为一个好的工具。

优点

  • 参数配置完成,可以直接使用

缺点

  • JSLint 不存在配置文件,如果想改变参数设置,那就存在问题
  • 有限的配置选项,许多规则不能禁掉
  • 不能增加个性化规则
  • 很难弄清楚哪个规则引起的错误

JSHint

作为一个可配置的JSLint版本而被开发出来

优点

  • 大多是参数可以配置
  • 支持配置文件,在大项目中容易使用
  • 已经支持需要类库,像jQuery、QUnit、NodeJS、Mocha等
  • 支持基本的ES6

缺点

  • 难于知道哪个规则产生错误
  • 存在两类选项:强制选项和松散选项。使得配置有些混乱
  • 不支持自定义规则

JSCS

JSCS是一个代码风格检查器。这意味着它仅仅匹配代码格式的问题,不匹配潜在的bugs、errors。因此,跟其他工具相比缺少灵活性,但是如果你仅仅强制检查代码风格,JSCS也是一个好的工具。

优点

  • 支持自定义报告,更容易与其他工具集成
  • 如果你遵循一种可用的代码风格,配置项和准备好的配置文件使其容易启动
  • 在报告中存在标记包含规则名字,所以很容易指出哪个规则造成了错误
  • 通过自定义插件进行拓展

缺点

  • 仅仅检查代码风格的问题。JSCS不检查潜在存在的bugs,例如不适用的变量、偶然的全局变量等等
  • 四个工具中最慢,但是在使用中不是一个问题

ESLint

ESLint是最新出来的工具,是一个用来识别 ECMAScript 并且按照规则给出报告的代码检测工具,使用它可以避免低级错误和统一代码的风格。它被设计的容易拓展、拥有大量的自定义规则、容易的通过插件来安装。它给出准确的输出,而且包括规则名,这样可以知道哪个规则造成了错误。

优点

  • 灵活:任何规则都可以开启闭合,以及有些规则有些额外配置
  • 很容易拓展和有需要可用插件
  • 容易理解产出
  • 包含了在其他检查器中不可用的规则,使得ESLint在错误检查上更有用
  • 支持ES6,唯一支持JSX的工具
  • 支持自定义报告

缺点

  • 需要一些配置
  • 速度慢,但不是主要问题

总结

最终推荐 ESLint。JSLint是严格和不可配置的,而JSHint缺少拓展机制。JSCS如果仅仅用于代码风格检验是一个好的选择,但是ESLint不仅可以进行代码风格的检验,而且可以检查代码中的bug和其他问题。
如果使用ES6,ESLint也是明显的选择。在上面提到的工具中,ESLint对ES6支持的最广泛。

二、ESLint 安装

安装

npm i -g eslint
npm install eslint --save-dev

初始化配置文件

接下来新建一个配置文件.eslintrc.js,或者执行eslint --init来自动生成。这条命令会类似npm init出现一系列对话问答

eslint_init.png-178.4kB

通过结果产生基本的配置内容,内容如下(不包含注释,注释是我写的)

module.exports = {
    "env": {
        "browser": true
    },
    "extends": "eslint:recommended",
    "rules": {
        // 强制使用一致的缩进: error级别,四空格
        "indent": [
            "error",
            4
        ],
        // 强制使用unix风格的换行:error级别
        "linebreak-style": [
            "error",
            "unix"
        ],
        // 使用双引号“”,而不是‘’
        "quotes": [
            "error",
            "double"
        ],
        // 强制行位分号
        "semi": [
            "error",
            "always"
        ]
    }
};

还可以选择在package.json 中设置 eslintConfig属性。但是不推荐。

个人建议, 使用单独的 .eslintrc.* 配置文件,这样别人一看代码结构就知道使用了 eslint 来校验代码,最好是js文件,json文件是不支持注释的。

默认情况下,eslint 会在所有父级文件夹中寻找配置文件,一直找到根目录为止。如果希望 eslint 不要继续往外寻找配置文件了则这样配置:"root": true

.eslintignore 忽略文件

你可以通过在项目根目录创建一个 .eslintignore 文件告诉 ESLint 去忽略特定的文件和目录。

  • 以 # 开头的行被当作注释,不影响忽略模式。
  • 路径是相对于 .eslintignore 的位置或当前工作目录。这也会影响通过–ignore-pattern传递的路径。
  • 忽略模式同 .gitignore 规范
  • 除了 .eslintignore 文件中的模式,ESLint总是默认忽略 /node_modules/ 和 /bower_components/中的文件。

三、ESLint 配置

配置执行环境

配置文件中可以自由的指定执行环境,浏览器 或 nodejs。可以同时指定多个!

module.exports = {
  env: {
    browser: true,
    node: true,
    es6: true
  },
};

配置全局变量

只配置环境是远远不够的,不同环境之间还会有不同环境变量。ESLint 可以在配置文件或注释中指定额外的全局变量,false表明变量只读

// .eslintrc.js
module.exports = {
  globals: {
    var1: true,
    var2: true,
  },
};

配置匹配规则

在配置文件中可以设置一些规则。这些规则的等级有三种:

  • “off” 或者 0: 关闭规则。
  • “warn” 或者 1: 打开规则,并且作为一个警告(不影响 exit code)。
  • “error” 或者 2: 打开规则,并且作为一个错误(exit code 将会是1)。

配置文件支持灵活的多种方式

  • .eslintrc.* 配置文件中统一配置

    // .eslintrc.js
    module.exports = {
      rules: {
        eqeqeq: 'off',
        curly: 'error',
      },
    };
    
  • 文件中的注释和跳过规则

    /* eslint-disable */
    // 中间的代码,会被跳过检查所有的代码
    /* eslint-enable */
    
    /* eslint-disable no-alert, no-console */
    // 中间的代码,会被跳过检查列举的而两个规则
    /* eslint-enable no-alert, no-console */
    
    // eslint-disable-next-line
    console.log('test')
    

继承规则

使用配置文件设置规则是,既可以选择只在rules中设置,还可以选择从他处继承。我们可以将定义好规则的.eslintrc.js文件存储到一个公共的位置,比如public-eslintrc.js:

module.exports = {
  extends: 'eslint:recommended',
  env: {
    node: true,
  },
  rules: {
    'no-console': 'off',
    'indent': [ 'error', 2 ],
    'quotes': [ 'error', 'single' ],
  },
};

然后将原来的.eslintrc.js文件改成这样:

module.exports = {
  extends: './public-eslintrc.js',
};

我们还可以使用已经发布到 NPM 上的 ESLint 配置,这些配置的模块名一般以eslint-config-为前缀,比如我在学习ESLint时自己编写的一个配置名为eslint-config-xxx。要使用这个配置,先执行以下命令安装它:

npm install -g eslint-config-xxx

用于我们的eslint命令是全局安装的,所有用到的eslint-config-*模块也必须全局安装,否则将无法正确载入。这是一个已知的Bug,参考这里:Error: Cannot read config package for shareable config using global eslint #4822

然后将.eslintrc.js文件改成这样:

module.exports = {
  extends: 'xxx',
};

四、执行

命令行执行

eslint [options] file.js [file.js] [dir]

这条命令中ESLint会自动找到当前目录下的.eslintrc.*文件

注意eslint merge.js --fix添加--fix结尾可以zi自动修复有橙色扳手图标的规则

配置到编译器

本文已intelliJ IDEA为例

eslint_idea.png-246.4kB
eslint_idea2.png-59.6kB

使用自动化任务

gulp-eslint

五、实践 && gulp-eslint

安装与介绍

gulp-eslint,是继承了 ESLint 功能的 gulp 插件,使用起来很简单,类似于命令行操作,直接安装

npm install gulp-eslint --save-dev

静态检查自动化任务

有了上面的基础,下面我们实现一个基础的自动化检查代码的gulp任务

  • 1.安装依赖

    {
      "name": "eslint_learning",
      "version": "0.0.0",
      "description": "eslint_learning",
      "private": false,
      "dependencies": {
        "eslint-plugin-html": "^3.2.2",
        "gulp": "3.9.1",
        "gulp-eslint": "1.0.0"
      }
    }
    
  • 2.编写 eslint 配置文件

    {
      "ecmaFeatures": {
        "modules": true
      },
      "plugins": [
          "html"
      ],
      "env": {
        "es6": true,
        "browser": true
      },
      "rules": {
        "no-console": 2,
      }
    }
    
  • 3.编写 gulpfile 配置文件,添加自动化任务

    var gulp = require('gulp'),
            eslint = require('gulp-eslint');
    
        gulp.task('lint', function() {
            return gulp.src(['js/**/*.js', 'index.html'])
                //.pipe(eslint({configFle:"./.eslintrc"}))
                .pipe(eslint())
                .pipe(eslint.format())
        });
    
        gulp.task('default', ['lint'], function() {
            // This will only run if the lint task is successful...
    });
    
  • 4.测试内容:index.html 中的javascript

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>eslint demo</title>
    </head>
    <body>
        <div>hello world</div>    
        <script src="js/index.js"></script>
        <script type="text/javascript">
            var a = 1;
        </script>
    </body>
    </html>
    
  • 5.测试内容 js/index.js

    let a = 1;
    //console.log('ddd')
    
  • 6.运行任务,查看结果

    gulp lint
    

六、git hooks

git 给我们提供了很多钩子函数,用于给不同的命令如,git commit指令提供类似回调函数,会在对应的指令的恰当时机执行。本文中模拟的是在代码编写完毕执行git commit命令之前的钩子pre-commit

githooks1.png-78kB

直接自定义 pre-commit

我们去掉文件末尾的.sample部分,使 git 能够识别。重写里面的内容

#!/bin/sh
#执行gulp任务,并将结果输出到临时文件
gulp lint | tee check.log

#检查gulp的check任务是否执行失败
if grep "warning" check.log || grep "error" check.log
then
echo -e "\033[31m Code check fail! Please try again! \033[0m"
rm check.log
exit 1
else
echo -e "\033[32m Code check success! \033[0m"
rm check.log
fi

然后安装插件 eslint-plugin-html

sudo npm install eslint-plugin-html --save-dev

接下来我们尝试一下带着问题提交,

git commit -m

果然,git 按照我们代码中写的那样,首先提示我们有错误,然后打断了提交流程

githooks2.png-136.5kB

使用 插件

第一步:然后安装插件 eslint-plugin-html

sudo npm install eslint-plugin-html --save-dev

安装完这个插件,会默认修改前文提到过的pre-commit.sample为pre-commit。

第二步:修改 package.json,添加pre-commit字段,设置git commit操作之前的任务

{
  "name": "eslint_learning",
  "version": "1.2.1",
  "description": "eslint_learning",
  "private": false,
  "scripts": {
    "lints": "num=`gulp lint | grep 'problem'|wc -l`;if [ $num -gt 0 ]; then echo wrongEslint;exit 2; else exit 0;fi",
    "precommit-msg": "echo 'Pre-commit checks...' && exit 0"
  },
  "dependencies": {
    "eslint-plugin-html": "^3.2.2",
    "gulp": "3.9.1",
    "gulp-eslint": "1.0.0"
  },
  "pre-commit": [
    "precommit-msg",
    "lints"
  ],
  "devDependencies": {
    "pre-commit": "^1.2.2"
  }
}

可见,我们设置了两个前置任务

第三部:命令保存退出。接下来我们尝试一下带着问题提交,

git commit -m

命令行提示如下内容

xxxdeMacBook-Pro:test1 xxx$ git commit -m "ddd"
Pre-commit checks...
wrongEslint
pre-commit:
pre-commit: We've failed to pass the specified git pre-commit hooks as the `lints`
pre-commit: hook returned an exit code (1). If you're feeling adventurous you can
pre-commit: skip the git pre-commit hooks by adding the following flags to your commit:
pre-commit:
pre-commit:   git commit -n (or --no-verify)
pre-commit:
pre-commit: This is ill-advised since the commit is broken.
pre-commit:

八、推荐文章

ESLint中文网

ElastAlert日志监控预警

发表于 2017-10-11

新增

短信报警优化

新增post报警方式,可用于发送动态短信

1
2
3
4
5
6
7
8
9
10
11
# 报警方式
alert:
- "post"

http_post_url: "http://adminhome.jinhui365.cn/sendSmsForAppAlert"

# phoneList 手机号,逗号分割的字符串。例如:"15235446827,15235446827"
# content 短信内容。${}代表动态内容
http_post_static_payload:
phoneList: "15235446827"
content: "${@timestamp}, IOS ${l} 级别日志报警。版本${v},手机型号${d},日志数${num_hits}."

动态内容为邮件内容,目前仅支持首层的key字段

举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
邮件内容为:
@timestamp: 2018-01-16T02:38:08.340Z
@version: 1
_id: AWD81Lv0TU1e05j5IHmu
_index: ios-2018.01.16
_type: logs
arg: {
"appkey": "jh28a4c4bc6734f58b",
"branchNo": "88",
"client": "iOS",
"encrypt": 0,
"fundAccount": "881125524",
"signcode": "2FBDC7A4842E30B8F9ACD112261ECF28",
"timestamp": "1504442929685",
"token": "u4ftBaBm-xg=",
"uid": "1861574",
"version": "5.15.0"
}
c: iOS
d: iPhone 6s Plus
host: ubuntu
i: B6C1AB45-C3CC-4C60-BD10-259778C6699B
ip: 223.104.95.169 贵阳市 移动
l: warn
message: request failed:0 /receipt/list
n: WiFi
num_hits: 3182
num_matches: 3
o: 中国移动
p: com.jinhui365.iphone-pay
path: /data/node-dev-tools/logs/iOS.log
s: 10.3.1
t: 1504442928.55
uid: 1861574
v: 5.15.0

1
2
content:"测试,测试,这个是个测试!message:${message},num_hits:${num_hits}"
发送的短信为:测试,测试,这个是个测试!message:request failed:0 /receipt/list,num_hits:3182

jira日志报警记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 报警标题
alert_subject: "{0} {1} {2} {3}"
alert_subject_args:
- "c"
- "v"
- "l"
- "@timestamp"

# 报警方式
alert:
- "jira"

# 配置(规则文件中不写)
jira_server: "http://jira.jinhui365.cn"
jira_project: "ALERT"
jira_account_file: "/home/jhjr/jinhui/java/elaticalert/java_rules/jira_acct.txt"

# 目前只支持Task,受限于jira
jira_issuetype: "Task"

# jira issue优先级(默认普通级别。可选0-3,数字越小优先级越高)
jira_priority: 0

# 添加关注者(会有jira邮件发送到对应邮箱)
jira_watchers:
- xfwei
- yli

结构

  • ElastAlert Kibana plugin
  • Elastalert Server
  • Elastalert

安装

  1. 根据ElastAlert Server安装教程进行kibana插件和ElastAlert Server安装.
  2. 根据ElastAlert官方网站安装Elastalert.

配置

Elastalert Server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"appName": "elastalert-server",
"port": 3030, //指定指定elastalert server端口,为kibana插件提供服务
"elastalertPath": "/opt/elastalert/elastalert", //指定elastalert路径
"verbose": true,
"es_debug": true,
"debug": false,
"rulesPath": { //elastalert报警规则存储相对路径
"relative": true,
"path": "/rules"
},
"templatesPath": { //规则模板相对路径
"relative": true,
"path": "/rule_templates"
},
"dataPath": { //测试数据存储路径
"relative": true, folder.
"path": "/server_data"
}
}

详细配置见Elastalert Server 配置

ElastAlert

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
es_host: 10.0.0.219
es_port: 9200

# 规则文件夹
rules_folder: rules

# 查询频率
run_every:
minutes: 1

# 查询时间片(可覆盖)
buffer_time:
minutes: 60

# 邮箱配置
smtp_host: mail.rxhui.com
smtp_port: 25

smtp_auth_file: /opt/elastalert/config/smtp_auth_file.yaml
email_reply_to: monitor@rxhui.com
from_addr: monitor@rxhui.com

# 写回kibana中的索引
writeback_index: elastalert_status

# 重发机制
alert_time_limit:
days: 2

详细配置见Elastalert 配置

使用教程

打开方式

  1. 该功能嵌入至kibana中,打开kibana:http://10.0.0.219:5601左上角setting右侧有个展开按钮,可供选择进入elastalert功能。
  2. 直接输入http://10.0.0.219:5601/app/elastalert进入。

基本操作

操作动态图

  • + New Rule可添加一条新的规则
  • 点击规则可以删除和修改
  • 规则页面右侧有一些模板可以点击展示
  • 规则页面左上角退出,右上角分别为[测试],[保存],测试完成后右侧会有输出

注意

1.添加新的规则需测试后才能保存,若直接保存可能因规则错误导致监听停止。
2.添加新的规则如果不想继续添加,记得将目录页该规则删除。
3.若因错误操作导致监听停止,可删除错误规则文件后访问http://10.0.0.219:3030/status/control/start重新启动监听,通过http://10.0.0.219:3030/status查看监听状态

ElastAlert 配置和规则说明

ElastAlert 配置

参数  说明 备注
es_host  Elasticsearch的host地址
es_port Elasticsearch的端口号 默认为9200
rules_folder 规则文件夹的名称
run_every 用来设置定时向elasticsearch发送请求
buffer_time 用来设置请求李时间字段的范围
realert 设定报警后的一段时间内忽略报警 默认为1分钟,可以设置为0
query_delay 减去查询所花的时间
writeback_index elastalert产生的日志在elasticsearch中创建的索引
alert_time_limit 失败重试的时间设置
es_send_get_body_as 查询Elasticsearch的请求方式 默认为get

ElastAlert 规则

参数 说明 备注
name 规则名称 英文,不能包含中文
type 报警规则检查类型
alert 报警的方式
index 监视的索引
filter 检索的条件
realert 设置n时间内只警报一次
email 若报警有email方式,为收邮件的邮箱
aggregation 聚合日志,能够攒齐了一段时间的警告再上报。也可以用schedule定时间发送这一段时间的所有警告 可以考虑是否使用
import 可以引用公共部分 后续的规则多了之后考虑将公共部分抽出

报警类型

  • any:只要有匹配就报警;
  • blacklist:compare_key字段的内容匹配上 blacklist数组里任意内容;
  • whitelist:compare_key字段的内容一个都没能匹配上whitelist数组里内容;
  • frequency:在相同 query_key条件下,timeframe 范围内有num_events个被过滤出 来的异常;
  • change:在相同query_key条件下,compare_key字段的内容,在timeframe范围内 发生变化;
  • spike:在相同query_key条件下,前后两个timeframe范围内数据量相差比例超过spike_height。其中可以通过spike_type设置具体涨跌方向是up,down,both 。还可以通过threshold_ref设置要求上一个周期数据量的下限,threshold_cur设置要求当前周期数据量的下限,如果数据量不到下限,也不触发;
  • flatline:timeframe 范围内,数据量小于threshold 阈值;
  • new_term:fields字段新出现之前terms_window_size(默认30天)范围内最多的terms_size (默认50)个结果以外的数据;
  • cardinality:在相同 query_key条件下,timeframe范围内cardinality_field的值超过 max_cardinality 或者低于min_cardinality

报警方式

  • Command
  • email
  • jira
  • post

具体规则书写查看文章末尾规则模板

ElastAlert

查询方式

  • query_string
    查询

    1
    2
    3
    filter:
    - query_string:
    query: "username: bob"

    query_string类型和Lucene的查询规则一致,具体细节可查看Lucene Query
    也可以通过将kibana上面的json格式转化为yaml的格式查询

  • term
    精确匹配键值对

    1
    2
    3
    filter:
    - terms:
    field: ["value1", "value2"]
  • terms
    键值对匹配多个值

  • wildcard
    标准的 shell 通配符

  • range
    范围

    1
    2
    3
    4
    5
    filter:
    - range:
    status_code:
    from: 500
    to: 599
  • Negation, and, or
    与或非

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    filter:
    - or:
    - term:
    field: "value"
    - wildcard:
    field: "foo*bar"
    - and:
    - not:
    term:
    field: "value"
    - not:
    term:
    _type: "something"

以上规则在文档ElastAlert Filters中皆有详细描述

规则模板

复杂的query_string查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# 时间标准
# seconds: 0~60
# minutes: 0~60
# hours: 0~60
# days: n
type: frequency

# 规则名称
name: android_log_test

# 轮询频率,run_every建议小于timeframe
run_every:
minutes: 1

# 轮转日志块,buffer_time大于timeframe,且建议为timeframe的整数倍(2-3倍)
buffer_time:
minutes: 15

# 触发标准,再timeframe时间内发生num_events数量的事件,触发报警
# 事件发生时间范围
timeframe:
minutes: 5
# 事件发生数量
num_events: 2

# 查询索引,参照kibana中的索引
index: android-*

# 检索条件
# 更详细的查询:http://lucene.apache.org/core/2_9_4/queryparsersyntax.html
# query: "message: \"load patch error\" AND message: lo?d"
# message=="load patch error" && message: "lo?d"

# + - && || ! ( ) { } [ ] ^ " ~ * ? : \
# 以上字符需转义

# 基本键值对查询
# query: "key: \"value\""
# 反例:query: "key: value1 value2 value3" 这将查询key==value1 || 包含value2的log || 包含value3的log

# 键值对模糊查询用通配符
# query:"key: v*e"

# 与或非
# key1 AND key2
# key1 OR key2
# NOT key1

# 分组查询: ()代表分组
# query: "title:(return AND \"pink panther\")" title=="return" && title="pink panther" (return可以用通配符,被引号引住的pink panther不能用通配)
# (key1 OR key2) AND key3


filter:
- query:
query_string:
query: "message: \"load patch error\" AND message: lo?d"

# 规定n个时间内不会多次收到相同日志,frequency方式下该字段建议定义为timeframe的整数倍
realert:
minutes: 5

# 报警方式
alert:
- "email"
# - "command"

# phoneList为,分割的手机号字符串,content为接收手机内容
# command: ["curl", "-X", "POST", "--header", "Content-Type: application/json", "--header", "Accept: */*", "http://adminhome.jinhui365.cn:8009/sendSms?phoneList=15235446827&content=测试"]

email:
- "yli@rxhui.com"
# - "15235446827@139.com"

与或非查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# 时间标准
# seconds: 0~60
# minutes: 0~60
# hours: 0~60
# days: n
type: frequency

# 规则名称
name: android_log_test

# 轮询频率,run_every建议小于timeframe
run_every:
minutes: 1

# 轮转日志块,buffer_time大于timeframe,且建议为timeframe的整数倍(2-3倍)
buffer_time:
minutes: 15

# 触发标准,再timeframe时间内发生num_events数量的事件,触发报警
# 事件发生时间范围
timeframe:
minutes: 5
# 事件发生数量
num_events: 2

# 查询索引,参照kibana中的索引
index: android-*

# 检索条件
# 翻译kibana查询语句,特殊字符@要用“”包起来
# message=="text data message" && @version=="1" && (uid=="1111" || !v=="5.22.0")

filter:
- and:
- query:
match:
message:
query: "text data message"
type: "phrase"
- query:
match:
"@version":
query: "1"
type: "phrase"
- or:
- query:
match:
uid:
query: "1111"
type: "phrase"
- not:
- query:
match:
v:
query: "5.22.0"
type: "phrase"

# 规定n个时间内不会多次收到相同日志,frequency方式下该字段建议定义为timeframe的整数倍
realert:
minutes: 5

# 报警方式
alert:
- "email"
# - "command"

# phoneList为,分割的手机号字符串,content为接收手机内容
# command: ["curl", "-X", "POST", "--header", "Content-Type: application/json", "--header", "Accept: */*", "http://adminhome.jinhui365.cn:8009/sendSms?phoneList=15235446827&content=测试"]

email:
- "yli@rxhui.com"
# - "15235446827@139.com"

ElastAlert Server API

This server exposes the following REST API’s:

  • GET /

    Exposes the current version running

  • GET /status

    Returns either ‘SETUP’, ‘READY’, ‘ERROR’, ‘STARTING’, ‘CLOSING’, ‘FIRST_RUN’ or ‘IDLE’ depending on the current ElastAlert process status.

  • GET /status/control/:action

    Where :action can be either ‘start’ or ‘stop’, which will respectively start or stop the current ElastAlert process.

  • [WIP] GET /status/errors

    When /status returns ‘ERROR’ this returns a list of errors that were triggered.

  • GET /rules

    Returns a list of directories and rules that exist in the rulesPath (from the config) and are being run by the ElastAlert process.

  • GET /rules/:id

    Where :id is the id of the rule returned by GET /rules, which will return the file contents of that rule.

  • POST /rules/:id

    Where :id is the id of the rule returned by GET /rules, which will allow you to edit the rule. The body send should be:

    1
    2
    3
    4
    {
    // Required - The full yaml rule config.
    "yaml": "..."
    }
  • DELETE /rules/:id

    Where :id is the id of the rule returned by GET /rules, which will delete the given rule.

  • GET /templates

    Returns a list of directories and templates that exist in the templatesPath (from the config) and are being run by the ElastAlert process.

  • GET /templates/:id

    Where :id is the id of the template returned by GET /templates, which will return the file contents of that template.

  • POST /templates/:id

    Where :id is the id of the template returned by GET /templates, which will allow you to edit the template. The body send should be:

    1
    2
    3
    4
    {
    // Required - The full yaml template config.
    "yaml": "..."
    }
  • DELETE /templates/:id

    Where :id is the id of the template returned by GET /templates, which will delete the given template.

  • POST /test

    This allows you to test a rule. The body send should be:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
          {
    // Required - The full yaml rule config.
    "rule": "...",

    // Optional - The options to use for testing the rule.
    "options": {

    // Can be either "all", "schemaOnly" or "countOnly". "all" will give the full console output.
    // "schemaOnly" will only validate the yaml config. "countOnly" will only find the number of matching documents and list available fields.
    "testType": "all",

    // Can be any number larger than 0 and this tells ElastAlert over a period of how many days the test should be run
    "days": "1"

    // Whether to send real alerts
    "alert": false
    }
    }
    ```

    - **[WIP] GET `/config`**

    Gets the ElastAlert configuration from `config.yaml` in `elastalertPath` (from the config).

    - **[WIP] POST `/config`**

    Allows you to edit the ElastAlert configuration from `config.yaml` in `elastalertPath` (from the config). The required body to be send will be edited when the work on this API is done.

    ## ElastAlert监控规律
    ElastAlert根据config中的run_every设置的时间频率去轮询,每次查询的时间块都是buffer_time

    基本查询规律:
    配置: run_every:20s, buffer_time:1min

    当前时间1月17日9时启动监控
    当前时间 日志时间块
    9:00:00 8:59:00~9:00:00
    9:00:20 9:00:00~9:00:20
    9:00:40 9:00:00~9:00:40
    9:01:00 9:00:00~9:01:00
    9:01:20 9:01:00~9:01:20
    9:01:40 9:01:00~9:01:40
    9:02:00 9:01:00~9:02:00
    `

    传送门

  • Bitsensor博客网站
  • ElastAlert Kibana Plugin
  • Elastalert Server
  • Elastalert Github
  • ElastAlert官方网站

ECMAScript 2015 语法分享

发表于 2017-10-11

第一部分:前言

一、为什么要使用 ES6

ES6代表了未来,对未来理应拥抱。为从以下几个角度来看,ES6的推广势在必行:

  • 解放开发效率

    • 新特性的合理使用,优雅而简洁
    • 减少第三方库的依赖
    • 可维护性提升,代码量减少
  • 面向未来。

    • 向标准靠拢
    • 官方支持
    • 迟早要学
  • 其他方面

    • 提升技术先进性
    • 促进技术交流,提高技术氛围
    • 编程激情
    • 整合部分历史代码的好机会

二、Nodejs各版本对应的ES6支持情况

1、如果你想一览Node不同版本对所有ES6的特性支持情况,就可以参看node.green这个网站

可以看到
6.11.2 - 99%
6.4.0 - 95%
5.12.0 - 59%

2、可以安转es-checker工具,通过该工具查看node支持的es6语法

sudo npm install es-checker -g

三、实用特性使用情况

特性 推荐程度
arrows ★★★
enhanced object literals ★★★
template strings ★★★
destructuring ★★
default + rest + spread ★★★
promises ★★★
math + number + string + array + object APIs ★★★
let + const ★★★
iterators + for..of ★★
tail calls ★★
modules ★★
map + set + weakmap + weakset ★★
generators ★
classes ★
binary and octal literals ★
symbols ★
module loaders ☆
proxies ☆
subclassable built-ins ☆
reflect api ☆
unicode ☆

第二部分 ES6 的实用特性

一、使用 let 和 const

let: 用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效

const: 声明一个只读的常量。一旦声明,变量指向的那个内存地址不得改动。而且,const一旦声明变量,该变量就必须立即初始化,不能留到以后赋值。同样,声明的变量,只在const命令所在的代码块内有效。一般用const声明常亮。

特点1:拥有块级作用域

let、const是一种新的变量申明方式,它允许你把变量作用域控制在块级里面。但是在ES5中,块级作用域起不了任何作用。下面列举两种常见的错误场景,然后你会发现会用ES6是这么轻松的就避免犯错!

  • 第一种常见场景:内层变量可能会覆盖外层变量

    var a = 2
    {
           var a = 1;
    }
    a // 1,一不小心发生了同名变量的覆盖
    
    let a = 2
    {
           let a = 1;
    }
    a // 2 ,es6通过块级作用域避免了覆盖的发生
    
  • 第二种常见场景:用来计数的循环变量泄露为全局变量

    for(var i = 0; i < 3; i++ ) {}
    console.log(i);//3,可见,此处的 i 已然成为了全局变量
    
    for(let i=0;i<3;i++) {}
    console.log(i);//使用ES6 抛出异常 ReferenceError: i is not defined
    

特点2:没有变量提升

ES6明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。首先看一个看变量在同一个块作用域内的情况

// 发生了变量提升
if(true) {
  console.log(x); // undefined
  var x = 'hello';
}

// 不准许在变量声明之前使用
if(true) {
  console.log(x); // ReferenceError
  let x = 'hello';
}

这个特性杜绝了我们日常编码中随处声明变量的恶习,强制要求我们养成提前声明变量的习惯。变量的声明和使用在同一作用域下如此,在不同作用域下更是如此:

{{{{
      {
      	console.log(insane); // 报错,提前使用了变量
      }
      let insane = 'Hello World'
    }}}};

{{{{
    	let insane = 'Hello World'
      {
      	console.log(insane); // hello world
      }
    }}}};

特点3:不允许在相同作用域内,重复声明同一个变量

// 正常
function () {
  var a = 10;
  var a = 1;
  // a ,1
}
// 报错
function () {
  let a = 10;
  var a = 1;
}

// 报错
function () {
  let a = 10;
  let a = 1;
}

function func(arg) {
  let arg; // 报错
}

function func(arg) {
  {
    let arg; // 不报错
  }
}

二、解构赋值

ES6 允许按照一定模式,从数组、对象中提取值,对变量进行赋值,这被称为解构。如果等号的右边不是可遍历的结构,如 {}, undefined, 数字常量值等,那么将会报错。下面列举三种最常见的结构场景

1. 对象的解构赋值

如果变量名与属性名(key)一致,则会对应的赋值,不论位置顺序

let { bar, foo } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"

let { foo, bar } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb",即使顺序变了,还是赋值给了对应的key

如果变量名与属性名不一致,必须写成下面这样

// 希望将对象中的foo属性赋值给变量baz
var { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"

结构失败,则变量赋值为undefined

let { baz } = { foo: "aaa", bar: "bbb" };
baz // undefined

究其原理,其实对象的解构赋值是下面形式的简写:

let { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb" };  

也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者

let { foo: baz } = { foo: "aaa", bar: "bbb" };
baz // "aaa"
foo // error: foo is not defined

上面代码中,foo是匹配的模式,baz才是变量。真正被赋值的是变量baz,而不是模式foo。本段开始提到的{foo}其实是{foo:foo}的简写

在对象中使用解构赋值的好处很多,比如下文中,我们通过传递某个配置对象类完成某些赋值目的

function init(options) {
      var id = options.uid;
      var cid = options.cid;
      var timeout = options.timeout;
      var protocol = options.protocol

      // code to init
}

var options = {
      id: '101',
      cid: 'xxx',
      timeout: '60',
      protocol: 'http'
}

init(options)

这种方式实现起来很好,已经被许多JS开发者所采用。 只是我们必须看函数内部,才知道函数预期需要哪些参数。结合解构赋值,我们就可以在函数声明中清晰地表示这些参数:

function init(param, {id, cid, timeout, protocal}) {
    // code to init
}

var options = {
    id: '101',
    cid: 'xxx',
    timeout: '60',
    protocol: 'http'
}

init(param, options)

在该函数中,我们没有传入一个配置对象,而是以对象解构赋值的方式,给它传参数。这样做不仅使这个函数更加简明,可读性也更高。

注意如果函数调用时,参数被省略掉且没有设置默认值,则会抛出错误

函数解构和默认值组合使用的一个难点:再请问下面两种写法有什么差别?

// 写法一
function m1({x = 0, y = 0} = {}) {
  return [x, y];
}

// 写法二
function m2({x, y} = { x: 0, y: 0 }) {
  return [x, y];
}

上面两种写法都对函数的参数设定了默认值,区别是:
写法一函数参数的默认值是空对象,但是设置了对象解构赋值的默认值;
写法二函数参数的默认值是一个有具体属性的对象,但是没有设置对象解构赋值的默认值。

// 函数没有参数的情况
m1() // [0, 0]
m2() // [0, 0]

// x和y都有值的情况
m1({x: 3, y: 8}) // [3, 8]
m2({x: 3, y: 8}) // [3, 8]

// x有值,y无值的情况
m1({x: 3}) // [3, 0]
m2({x: 3}) // [3, undefined]

// x和y都无值的情况
m1({}) // [0, 0];
m2({}) // [undefined, undefined]

m1({z: 3}) // [0, 0]
m2({z: 3}) // [undefined, undefined]

本质上来说,如果实参有值的话,参数的默认值就不会生效。如果参数的默认值不生效,那么解构就无法发生。

2. 数组的解构赋值

const arr = [1, 2, 3, 4];

// bad
const first = arr[0];
const second = arr[1];

// good
const [first, second] = arr;

数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值
和对象解构一样,如果解构不成功,变量的值就等于undefined

3. 函数返回值的解构

函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便。

function getVal() {
  return [ 1, 2 ];
}

let [x,y] = getVal();//函数返回值的解构
console.log(x ,y);// 1, 2

本质上,这种写法属于“模式匹配”,只要等号两边的模式相同或部分相同,左边的(部分)变量就会被赋予对应的值。下面是一些使用嵌套数组进行解构的例子。

// 嵌套
let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3

// ... 运算
let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]

let [head, ...tail, tailest] = [1, 2, 3, 4];
// SyntaxError: Rest element must be last element in array

4. 解构赋值使用默认值

解构赋值允许指定默认值。ES6 内部使用严格相等运算符===,判断一个位置是否有值。所以,如果一个数组成员不严格等于undefined,默认值是不会生效的

let [foo = true] = [];
foo // true

let [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'
let [x, y = 'b'] = ['a', 'undefined']; // x='a', y='undefined'
let [x, y = 'b'] = ['a', null]; // x='a', y=null

var {x, y = 5} = {x: 1};
x // 1
y // 5

默认值可以引用解构赋值的其他变量,但该变量必须已经声明。

let [x = 1, y = x] = [];     // x=1; y=1
let [x = 1, y = x] = [2];    // x=2; y=2
let [x = 1, y = x] = [1, 2]; // x=1; y=2
let [x = y, y = 1] = [];     // ReferenceError

三、字符串扩展

1. 模板文本

模板字符串(template string)是增强版的字符串,用反引号()标识。它可以当作普通字符串使用,也可以用来定义多行字符串。在字符串中嵌入变量,需要将变量名写在${}`之中。

首先,让我们看看 ES5 中拼接字符串的方式

var name = 'feng' , age = '25';
var result = 'hello: ' + name + ', your name is ' + age;
// hello: feng, your name is 25

再看看 ES6 的实现方式

var name = 'feng' , age = '25';
var result = `hello: ${name}, your name is ${age}`
// hello: feng, your name is 25

模板中使用对象

let obj = {x:1,y:2};
console.log(`Your total is: ${obj.x + obj.y}`); // Your total is 3

模板中使用函数调用

function fn() {
    return "Hello World";
}

`foo ${fn()} bar`
// foo Hello World bar

这样做省略了很多影响阅读的+ ,,直接在反引号中使用变量书写,很美观和便利!

2. 多行字符串

ES6 的多行字符串是一个非常实用的功能。在 ES5 中,我们不得不使用以下方法来表示多行字符串

var multStr = 'Then took the other, as just as fair,\n\t'
    + 'And having perhaps the better claim\n\t'
    + 'Because it was grassy and wanted wear,\n\t'
    + 'Though as for that the passing there\n\t'
    + 'Had worn them really about the same,\n\t';

然而在 ES6 中,仅仅用反引号就可以解决了:

var multStr = `Then took the other, as just as fair,
    And having perhaps the better claim
    Because it was grassy and wanted wear,
    Though as for that the passing there
    Had worn them really about the same,`;

如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中

let tt = `
<ul>
    <li>first</li>
    <li>second</li>
</ul>
`;
console.log(tt)

输出成如下内容

<ul>
    <li>first</li>
    <li>second</li>
</ul>

四、函数扩展

1. 默认参数

ES6 之前,不能直接为函数的参数指定默认值,只能采用变通的方法。项目中通过||来实现默认参数

var link = function (height, color) {
    var height = height || 50;
    var color = color || 'red';
    console.log(height + ' : ' + color)
}

link();//50 red
link(10, 'blue');// 10 blue 1

目前来说是正常的,调用该函数时,没有传入实参,会用默认值。但是如果我们传入的参数本身会通过类型转换为false(比如0 或者null)就会有问题:系统忽略掉了我们的入参,反而使用了默认值!

link(0, 'blue');//50 blue

代码默认值是50,调用时希望设置为0,但是还是输出了默认值50。

在 ES6 中,我们通过如下方式来完成默认参数的设置,即直接写在参数定义的后面。我们甚至可以让默认值是一个函数(惰性求值)

var link = function (height = 50, color = 'red') {
    console.log(height + ' : ' + color)
}

link(10, 'blue');//10: blue
link(0, blue);//0 blue
link();// 50 : red

还有一点需要着重介绍一下:默认参数可以和解构赋值默认值结合使用

function foo({x, y = 5}) {
  console.log(x, y);
}

foo({}) // undefined, 5
foo({x: 1}) // 1, 5
foo({x: 1, y: 2}) // 1, 2
foo() // TypeError: Cannot read property 'x' of undefined

上面代码使用了对象的解构赋值默认值,而没有使用函数参数的默认值。只有当函数foo的参数是一个对象时,变量x和y才会通过解构赋值而生成。如果函数foo调用时参数不是对象(或者不能转换成对象),变量x和y就不会生成,从而报错。并且如果参数对象没有y属性,y的默认值5才会生效。

请注意上面例子与下面两种写法的区别

function foo({x=1, y = 5}) {
  console.log(x, y);
}
foo() // TypeError: Cannot match against 'undefined' or 'null'.解构失败

function foo({x=1, y = 5} = {}) {
  console.log(x, y);
}
foo() // 1 5  双重默认值:首先因为实参为空,所以函数参数默认值{}生效。然后才是解构赋值的默认值生效

2. Rest 不定参数

ES6 引入 rest参数(形式为...变量名),用于获取函数的多余参数,rest参数搭配的变量是一个数组,该变量将多余的参数放入数组中。用来取代额外的arguments对象了。

// arguments变量的写法
function sortNumbers() {
    return Array.prototype.slice.call(arguments).sort();
}  
// rest参数的写法
let sortNumbers = (...numbers) => numbers.sort();

利用 rest参数,可以向该函数传入任意数目的参数。下面这个例子中,其中…x代表了所有传入add函数的参数。

//将所有参数相加的函数
function add(...x){
   return x.reduce((m, n)=> m + n);
} 
console.log(add(1,2,3));//输出:6
console.log(add(1,2,3,4,5));//输出:15                                              

rest参数中的变量代表一个数组,所以数组特有的方法都可以用于这个变量。
rest参数之后不能再有其他参数(即,只能是最后一个参数),否则会报错。
一个函数声明只能允许有一个 rest参数

3. 箭头函数

我们知道在JS中回调是经常的事,而一般回调又以匿名函数的形式出现,每次都需要写一个function(){}甚是繁琐。当引入箭头操作符=>后可以方便地写回调了。

它简化了函数的书写。操作符左边为输入的参数,而右边则是进行的操作以及返回的值。即, Inputs => outputs。

let array = [1, 2, 3];

//传统写法
array.forEach(function(v) {
    console.log(v);
});

//ES6
//使用函数体形式
array.forEach(v => {
    console.log(v)
});
// 或者直接使用更简洁的表达式
array.forEach(v => console.log(v));    

如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。

var f = () => 5;
var sum = (num1, num2) => num1 + num2;

除了上面的写法的改变,剪头函数使用this时也和我们以前大不相同:以前我们使用闭包,this总是预期之外地产生改变。而箭头函数的迷人之处在于,this的指向是固定的。身处箭头函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。

举个例子,实现一个功能,点击某个按钮之后,调用当前模块的sendData()方法:

首先看看ES5中的处理方式

var polyglot = {
    name : "feng",
    fruits : ["apple", "orange", "watermelon"],
    introduce : function () {
        const self = this;
        this.fruits.forEach(function(item) {
            console.log(this)
            console.log("My name is " + self.name + ", I eat " + item + ".");
        });
    }
}

polyglot.introduce();

在introduce里, this.name是undefined(浏览器环境中forEach的匿名回调中this指向window)。在回调函数外面,也就是forEach中, 它指向了polyglot对象。在这种情形下我们总是希望在函数内部this和函数外部的this指向同一个对象。在ES6中就不需要用 _this = this完成这个需求

let polyglot = {
    name : "feng",
    fruits : ["apple", "orange", "watermelon"],
    introduce : function () {
        this.fruits.forEach((item) => {
            console.log("My name is " + this.name + ", I eat " + item + ".");
        });
    }
}

再看个例子,来验证一下(需在浏览器环境验证)

function foo() {
  setTimeout(() => {
    console.log('id:', this.id);
  }, 100);
}

var id = 21;

foo.call({ id: 42 });
// id: 42

上面代码中,setTimeout的参数是一个箭头函数,这个箭头函数的定义生效是在foo函数生成时,而它的真正执行要等到100毫秒后。如果是普通函数,执行时this应该指向全局对象window,这时应该输出21。但是,箭头函数导致this总是指向函数定义生效时所在的对象(本例是{id: 42}),所以输出的是42

本质上来说,this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数

1.简单的、单行的、不会复用的函数,建议采用箭头函数。如果函数体较为复杂,行数较多,还是应该采用传统的函数写法。而且如果箭头函数有多个参数,必须用圆括号包裹
2.不可以使用arguments对象,该对象在函数体内不存在,外层函数的对应变量
3.由于箭头函数没有自己的this,所以当然也就不能用call()、apply()、bind()这些方法去改变this的指向

五、数组扩展

1. 扩展运算符…

扩展运算符(spread)是三个点(…),它好比 rest参数的逆运算。rest参数将一个参数转换成数组,而本文中的扩展运算符则负责将一个数组转为用逗号分隔的参数序列。

...[1, 2, 3]
// 1 2 3
1, ...[2, 3, 4], 5
// 1 2 3 4 5
[...document.querySelectorAll('div')]
// [<div>, <div>, <div>]

var arr1 = ['a', 'b'];
var arr2 = ['c'];
[...arr1, ...arr2]// [ 'a', 'b', 'c' ]

下面看几个实际使用案例

  • 使用扩展运算符拷贝数组

    // bad
    const len = items.length;
    const itemsCopy = [];
    let i;
    
    for (i = 0; i < len; i++) {
     itemsCopy[i] = items[i];
    }
    
    // good
    const itemsCopy = [...items];
    
  • 替代数组的 apply方法
    原来在ES5中,因为方法或者函数不支持数组参数, 如Math.max/Array.push(),而必须使用apply(array)的场景,都可以直接使用扩展运算符 …

    // ES5 的写法
    function f(x, y, z) {
      // ...
    }
    var args = [0, 1, 2];
    f.apply(null, args);
    
    // ES6的写法
    function f(x, y, z) {
      // ...
    }
    var args = [0, 1, 2];
    f(...args);
    
    var myArray = [1, 2, 3, 4];
    Math.max(myArray); //error
    Math.max.apply(Math, myArray);// 4,ES5写法
    Math.max(...myArray);//4,ES6写法
    
  • 与解构赋值结合

    let test = [1,2,3]
    let [a, ...rest] = test;
    a //1
    

如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错

2. Array.from

Array.from()可以将各种值转为真正的数组,并且还提供map功能。这实际上意味着,只要有一个原始的数据结构,你就可以先对它的值进行处理,然后转成规范的数组结构,进而就可以使用数量众多的数组方法。

let arrayLike = {
    '0': 'a',
    '1': 'b',
    '2': 'c',
    length: 3
};

// ES5的写法
var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']

// ES6的写法
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']

还有,在查找一组DOM节点时

const foo = document.querySelectorAll('.foo');
const nodes = Array.from(foo);

还有,上文说过使用rest参数将参数转换成数组,从而避免使用arguments对象。因为arguments是类数组对象,所以还可以通过Array.from来完成转换工作。

function foo() {
  var args = Array.from(arguments);
  // ...
}

Array.from的第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组。

Array.from(arrayLike, x => x * x);
// 等同于
Array.from(arrayLike).map(x => x * x);

Array.from([1, 2, 3], (x) => x * x)
// [1, 4, 9]


Array.from({ length: 2 }, () => 'jack')
// ['jack', 'jack']

类数组对象本质特征只有一点:任何有length属性的对象。
Array.from({ length: 3 });// [ undefined, undefined, undefined ]

3. fill

fill方法使用给定值,填充一个数组。该方法用于空数组的初始化非常方便。要注意,数组中已有的元素,会被全部抹去。

['a', 'b', 'c'].fill(7)
// [7, 7, 7]

new Array(3).fill(7)
// [7, 7, 7]

六、对象扩展

1. 对象属性的简写

如果对象的键值和变量名是一致的,ES6允许仅用变量名来初始化这个对象,而不是定义冗余的键值对。这时,属性名为变量名, 属性值为变量的值

var foo = 'foo';
var bar = 'bar';
var baz = {
    foo,
    bar
};
// 等同于
var baz = {
    foo: foo,
    bar: bar
};

2 对象方法的简写

var o = {
  method(name) {
    return "Hello!" + name;
  }
};

// 等同于
var o = {
  method: function(name) {
    return "Hello!" + name;
  }
};

3. 对象导出属性的简写

module.exports = { getItem, setItem, clear };
// 等同于
module.exports = {
  getItem: getItem,
  setItem: setItem,
  clear: clear
};

4 新增:object.assign()

用于对象的合并,将源对象的所有可枚举属性,复制到目标对象。方法的第一个参数是目标对象,后面的参数都是源对象。如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。

var target = { a: 1, b: 1 };
var source1 = { b: 2, c: 2 };
var source2 = { c: 3 };

Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}

需要注意的是,这个方法施行的不是类似merge的操作,而是简单的同名key的的简单覆盖,而不是添加

var target = { 
    a: 1, 
    b: {
        key: {
            inner_key:0
        }
    }
};
var source1 = { 
    b: {
        key:{
            inner_key:1,
            inner_key2:2,
            inner_key3:3
        }
    } 
};

Object.assign(target, source1);
// target { a: 1, b: { key: { inner_key: 1, inner_key2: 2, inner_key3: 3 } } }

Object.assign方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。这个对象的任何变化,都会反映到目标对象上面。

5. Object.setPrototypeOf

用来设置一个对象的prototype对象,返回参数对象本身。它是 ES6 正式推荐的设置原型对象的方法。

Object.setPrototypeOf(object, prototype)

举一个例子

let proto = {};
let obj = { x: 10 };
Object.setPrototypeOf(obj, proto); // 将proto对象设置为obj的原型

proto.y = 20;
proto.z = 40;

obj.x // 10
obj.y // 20
obj.z // 40

6. Object.getPrototypeOf

该方法与Object.setPrototypeOf方法配套,用于读取一个对象的原型对象。下面是一个例子。

function Rectangle() {
  // ...
}

var rec = new Rectangle();

Object.getPrototypeOf(rec) === Rectangle.prototype
// true

Object.setPrototypeOf(rec, Object.prototype);
Object.getPrototypeOf(rec) === Rectangle.prototype
// false

7. Object.keys(),Object.values(),Object.entries()

Object.keys方法,返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名。
Object.values方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值。
Object.entries方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对数组。

let {keys, values, entries} = Object;
let obj = { a: 1, b: 2, c: 3 };

for (let key of keys(obj)) {
  console.log(key); // 'a', 'b', 'c'
}

for (let value of values(obj)) {
  console.log(value); // 1, 2, 3
}

for (let [key, value] of entries(obj)) {
  console.log([key, value]); // ['a', 1], ['b', 2], ['c', 3]
}

七、Classes

JavaScript 语言中,生成实例对象的传统方法是通过构造函数。下面是一个例子。

function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.toString = function () {
  return '(' + this.x + ', ' + this.y + ')';
};

var p = new Point(1, 2);

ES6 提供了更接近传统面相对象语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。

//定义类
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  // 方法之间不需要逗号分隔,加了会报错
  toString() {
    return `${this.x}{this.y}`
  }
}

类必须使用new调用,否则会报错。

类的静态方法

所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。

class Foo {
  static classMethod() {
    return 'hello';
  }
}

Foo.classMethod() // 'hello'

var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function

父类的静态方法,可以被子类继承

Class的继承

Class 可以通过extends关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}

class ColorPoint extends Point {
  constructor(x, y, color) {
    // 调用父类的 constructor(x, y) 
    // 相当于Point.prototype.constructor.call(this,x,y)
    super(x, y); 
    this.color = color;
  }

  toString() {
    return this.color + ' ' + super.toString(); // 调用父类的toString()
  }
}

super关键字,它在这里表示父类的构造函数。子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。

ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。而 ES6 的继承机制完全不同,实质是先创造父类的实例对象this(super()方法),然后再用子类的构造函数修改this。

Object.getPrototypeOf方法可以用来从子类上获取父类。因此,可以使用这个方法判断,一个类是否继承了另一个类。

Object.getPrototypeOf(ColorPoint) === Point
// true

八、Modules(模块)

使用 import 取代 require

// bad
const moduleA = require('moduleA');
const func1 = moduleA.func1;
const func2 = moduleA.func2;

// good
import { func1, func2 } from 'moduleA';

使用 export 取代 module.exports

// point.js
module "point" {
    export class Point {
        constructor (x, y) {
            public x = x;
            public y = y;
        }
    }
}

// myapp.js
//声明引用的模块
module point from "/point.js";
//这里可以看出,尽管声明了引用的模块,还是可以通过指定需要的部分进行导入
import Point from "point";

var origin = new Point(0, 0);
console.log(origin);

参考文章

ES6编程规范
ES手册传送门

upload successful

webpack体积优化

发表于 2017-10-11

基础

从webpack文件上来看,主要用到的有entry,output,resolve,module,plugins

  • entry入口
    用来写入口文件,SPA一般是一个入口。当然也有vendor写在这里。
  • output出口
    主要为webpack经过你设计的规则构建后输出的文件,指定输出位置,文件名等配置。
  • resolve解析
    定制你的解析规则,后面会说到。
  • module模块
    主要是装载器loaders书写的地方,webpack2已经改为rules。
  • plugins插件

详细请看配置

output

output除了用来指定输出位置外,还有一些hash和chunk的配置。hash是随机生成的,每一次都会改变。chunk这里指的是webpack分割的代码块,webpack在编译过程中会解析内容,通过内容生成chunkhash,chunkhash相对hash来说是相对不变的,我们也是利用这一点来做浏览器缓存。这里webpack提供了hash生成的算法,chunkhash失效时间等配置,一般来说我们不会用到。

主要用到的东西就是path,publicpath,filename,chunkFilename
path指的是webpack构建完成后的输出地址
publicpath指的是资源的访问地址
filename是生成文件的名字
chunkFilename非入口的文件名

resolve

resolve是webpack可定制的解析,他的配置决定了你在require或者import一个包的时候,webpack去哪里找这个文件。
resolve.modules指定查找包的文件夹,可用绝对路径和文件名。文件名的查找规则和node_modules查找规则一致。
resolve.descriptionFiles指定查找包的描述文件,一般为package.json
resolve.mainFields指定描述文件中的入口字段,一般为main

一般的解析就是通过modules查找到包,然后找包内的描述文件package.json,然后根据包内的描述文件入口字段mainFields来引入js文件
似乎mainFields指定的字段不支持Array类型,也就是说package.json中的main字段得是个String,这样你需要引入多个文件的时候就需要做个入口文件了。

loader和plugins的区别

其实他们两没有什么可比性,放在这里只是想说明一下他们在webpack中扮演的角色。
webpack是一个插件式的架构,采用Tapable事件流。
loader其实只是在webpack编译过程中的某一个时期执行,他的作用就是对符合规则的文件进行转换,比如常用的less-loader、css-loader、style-loader,less-loader先将less语法转为css,css-loader支持我们require引用css和处理css内部的import和url,style-loader则是将css文件转为style注入到页面中。loader支持链式调用,前面提到的例子就是将less转为style的一个链式调用。除此之外,各个loader之间是互不影响。
loader的调用方式分为三种,分别是命令行调用,内联调用和在配置文件webpack.config.js中配置。个人比较推荐第三种,这样对源码的影响将会是最小的。
plugins不同于loader,他贯穿整个webpack编译。利用webpack提供的很多hook,在不同的时期触发相应的操作,功能也多种多样。
感兴趣的可以了解一下webpack之loader和plugin简介和webpack 源码解析。

常用的loader

  • 样式:style-loader、css-loader、less-loader、sass-loader等
  • 文件:raw-loader、file-loader 、url-loader、json-loader等
  • 编译:babel-loader、coffee-loader 、ts-loader等
  • 校验测试:mocha-loader、jshint-loader 、eslint-loader等

常用的plugin

  • DefinePlugin
    全局常量定义
  • UglifyJsPlugin
    代码丑化
  • OccurenceOrderPlugin
  • HtmlWebpackPlugin
    自动生成html5文件
  • CommonsChunkPlugin
    公共代码整合
  • CompressionWebpackPlugin
    gzip压缩

详细请看awesome-webpack、webpack之loader和plugin简介、webpack2.2中文文档

webpack执行流程

  • entry-option
    初始化option
  • run
    开始编译
  • make
    从entry开始递归的分析依赖,对每个依赖模块进行build
  • before-resolve - after-resolve
    对其中一个模块位置进行解析
  • build-module
    开始构建 (build) 这个module,这里将使用文件对应的loader加载
  • normal-module-loader
    对用loader加载完成的module(是一段js代码)进行编译,用 acorn 编译,生成ast抽象语法树。
  • program
    开始对ast进行遍历,当遇到require等一些调用表达式时,触发call require
    事件的handler执行,收集依赖,并。如:AMDRequireDependenciesBlockParserPlugin等
  • seal
    所有依赖build完成,下面将开始对chunk进行优化,比如合并,抽取公共模块,加hash
  • bootstrap
    生成启动代码
    emit
    把各个chunk输出到结果文件

包体积优化

包分析

通过插件webpack-bundle-analyzer进行分析。

npm i webpack-bundle-analyzer --save

这个插件在使用后会开启一个网页,一般为localhost:8888,当然这个可以配置。通过图像展示我们打包的每个文件组成,各个部分体积,总体积等一些信息。

详细的配置可以查看npm上面的文档,这里我也是用了npm的案例配置。

PS:个人认为优化还是要基于对项目的熟悉上,否则可能会无从下手。

lodash 优化

npm install babel-plugin-lodash lodash-webpack-plugin --save

const LodashModuleReplacementPlugin = require('lodash-webpack-plugin');

const config = {
  plugins: [
    new LodashModuleReplacementPlugin({
      path: true,
      flattening: true
    })
  ]
};

.babelrc
plugins: ['transform-runtime', 'lodash'],

懒加载

npm i bundle-loader --save

require("bundle-loader?lazy&name=my-chunk!./file.js");
OR
import XXX from "bundle-loader?lazy&[name=[name]]!./file.js"

CDN优化

CDN优化依赖一个包管理器Bower

Bower算是一个很老的前端包管理器,虽说它叫包管理器,其实他只是提供了扁平化的下载和记录功能。

目前Bower已宣告终止开发,前端模块管理全面移向npm。

这里npm3已经号称是JavaScript的包管理器,而不仅仅是node的包管理器了。:)

#### 安装Bower

npm i -g bower

Bower部分命令

bower的命令和npm大同小异,以下是几个常用的命令:

  • 初始化,生成bower.json文件,功效和package.json差不多
    bower init
  • 查找包,当然也可以通过官网查找
    bower search XXX
  • 安装包,规则和npm一致,可以下载对应版本的包
    bower install XXX@XX --save

webpack和bower的连接

通过插件bower-webpack-plugin进行webpack和bower的连接。

npm i bower-webpack-plugin --save

这里其实他做的很类似resolve的alise,就是通过插件指定了require/import一个bower包的时候的地址和文件。

以下是它的配置:

var BowerWebpackPlugin = require("bower-webpack-plugin");
new BowerWebpackPlugin({
  modulesDirectories: ["bower_components"],
  manifestFiles:"bower.json",
  excludes: /.*\.less/
}),

通过modulesDirectories指向bower的依赖文件夹,manifestFiles指向入口文件。这里一般配置一般都是固定的,详细的配置可以查看NPM文档。
下面是bootstrap的bower.json文件,可以看出它是通过main字段来引入对应文件的。

bower包的引入和正常的npm包一样引入,通过import或者require,测试发现还是require效果好一点,少一点坑。

在升级到webpack2之后,发现这个插件已经不支持了,转采用bower-resolve-webpack-plugin
它的配置参考github文档,发现只能加载js文件,废弃!不过可以借鉴一下它写插件

bootstrap

bootstrap-loader用来解决bootstrap加载
查找网上webpack引入bootstrap一致推荐bootstrap-loader,测试后发现引入这个东西bootstrap会占用将近150kb的空间,记录一下
vendor占用544kb
app占用158kb

安装
npm install bootstrap-loader --save
npm install bootstrap-sass --save
npm install css-loader node-sass resolve-url-loader sass-loader style-loader url-loader --save

加载
require('bootstrap-loader')

详细请看如何把bootstrap用webpack打包

另一种办法是通过bower引入,但是bower中bootstrap给的入口文件有一个less,无法解决,只能修改less为css。这样bootstrap占用会只有不到50kb
vendor占用390kb
app占用190kb


webpack2 tree-shaking

node的import语法允许我们只引入我们需要的方法,这对于js的内存占用是一种极大的提升。同时在webpack打包的时候也利用了这种特性,只打包我们引入的方法。但是这一点和babel的转码会有冲突,具体原因看这里如下:

tree-shaking 是指借助es6 import export 语法静态性的特点来删掉export但是没有import过的东西,babel会在编译转化es6代码时把import export转换为cmd的module.export
原文请看Tree-shaking with webpack 2 and Babel 6。

我们为了利用webpack2的这种特性,需要做一些配置,见原文。
经过测试,我发现这个特性能减少的包体积量几乎可以忽略不计。难怪会有此讨论->如何评价 Webpack 2 新引入的 Tree-shaking 代码优化技术?

打包速度优化

flag

webpack2入坑

  1. 入口和出口其实改动不大
  2. resolve改动会大一点,不过因为我们不经常用,所以不用太关注,这里需要注意的就是以下几点:
    • resolve.extensions配置,数组不需要在第一位加空字符串了
    • resolve.root, resolve.fallback, resolve.modulesDirectories合并为resolve.modules
  3. module语法改动
    • module.loaders改为module.rules
    • 不再支持简写loader,也就是说css以后要写成css-loader
    • 链式调用style!css!less改为use[]
  4. plugins改动
    这个就需要看各个插件的支持了,有部分插件是只支持1不支持2的,还有webpack内置插件的有部分语法改动,需要用的时候查看。改动最大的就是extract-text-webpack-plugin,不升级无法使用。

webpack2主要的变更就是对ES6的支持更好了,当然我升级到2以后除了踩了很多语法坑和插件坑之外,并没有体验到所谓的性能提升,很尴尬。利用了tree shaking特性后包的体积也并没有减小多少。只能说webpack2比1更大的规范化,并且目前webpack已经升级到了3.5,语法已经稳定,市面上的各类资料和插件也都是webpack2更完善、支持更好。

传送门

webpack之loader和plugin简介

webpack 源码解析

webpack中文官网

webpack入门(一)——webpack 介绍

[译] Webpack 2 有哪些新东西

Git 使用入门

发表于 2017-06-15 | 分类于 Git

Git 客户端安装

讲真,这个对大家应该都没有难度。。。提供下载地址,自己玩

Mac 安装

  • 图形化的Git安装工具 点我
  • HomeBrew 安装

Windows安装

Git For Windows 提供命令行 Git Bash 和图形化 Git GUI


阅读全文 »

Git 使用分享

发表于 2017-06-15 | 分类于 Git

Git 几个重要的概念

  • 本地仓库
    Git 中大部分都是针对本地仓库的操作,完全不用担心污染远程仓库,比如 git add ,git commit ,git reset,checkout 等等。
  • 远程仓库
    服务端的代码仓库,用于多人协作开发。。。每个开发人员的本地仓库都是一份完整的远程仓库备份。
阅读全文 »

ButterKnife 框架使用详解

发表于 2017-05-04 | 分类于 Android

前言:

findViewById 是每一个 Android 开发者的必经之路,同样也是噩梦。为了提高开发效率,准备在项目中使用 ButterKnife(PS:感谢JakeWharton大神),本文基于8.5.1 版本官方文档翻译,不同的版本在用法上可能会差异,实际使用时,请参考官网文档。
Github 地址
官方文档

阅读全文 »
123
jinhui-mobile

jinhui-mobile

stay hungry stay foolish

21 日志
3 分类
10 标签
RSS
GitHub
© 2018 jinhui-mobile
由 Hexo 强力驱动
主题 - NexT.Pisces