Giter Site home page Giter Site logo

yanyixin.github.io's People

Contributors

yanyixin avatar

Stargazers

catcut avatar luxinlei avatar 前端胖头鱼 avatar Cherry avatar 赵文杰 avatar  avatar

Watchers

James Cloos avatar  avatar

Forkers

hsqcoollaughing

yanyixin.github.io's Issues

探索一个 React Native 应用是如何启动的 —— Ios 篇

上一篇讲到了在 Android 环境下一个 React Native 应用是如何启动的。这次咱们来探索一下 Ios 环境。

和上文的思路一样,咱们可以按照以下三个步骤来:

  1. 找到整个应用的入口文件
  2. 找到 native 端和 React Native 相连接的方法
  3. 找到 React Native 的入口文件

找到整个应用的入口文件

用 Xcode 打开项目目录下的 ios 文件夹,在和项目名称相对应的文件夹的下面找到 main.m 文件。此文件就是整个 ios 应用的入口。

ios 文件目录

打开 main.m 文件之后可以看到下面的代码:

#import <UIKit/UIKit.h>

#import "AppDelegate.h"

int main(int argc, char * argv[]) {
  @autoreleasepool {
    return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
  }
}

主函数 main 主要是调用 UIApplicationMain 函数。UIApplicationMain 函数是初始化程序的核心,它接收 4 个参数:
1、参数个数
2、参数内容
3、表示要创建的应用程序对象( app 的象征,该类必须是 UIApplication 或者它的子类)。如果传 nil 默认就表示 UIApplication 类。UIApplication 是单例模式,一个应用程序只有一个 UIApplication 对象或子对象;
4、表示给应用程序指定一个代理对象,该类必须遵守 UIApplicationDelegate 协议。

所以总的过程大概就是 UIApplicationMain 函数创建 UIApplication,然后再根据第四个参数创建 delegate 对象,并将该 delegate 对象赋值给 UIApplication 对象中的 delegate 属性。

这里再翻译一下,就是一个应用要有一个 UIApplication 作为主体来接收各类 events,然后还要有一个 UIApplicationDelegate 来处理 events。UIApplicationMain 函数会自动创建 UIApplication,所以咱们下面就来看一下 UIApplicationDelegate 是如何处理的。

找到 native 端和 React Native 相连接的方法

打开 AppDelegate.m 文件,native 和 js 连接的方法就在里面。

#import "AppDelegate.h"

#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  NSURL *jsCodeLocation;

  jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index.ios" fallbackResource:nil];

  RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
                                                      moduleName:@"AwesomeProject"
                                               initialProperties:nil
                                                   launchOptions:launchOptions];
  rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];

  self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
  UIViewController *rootViewController = [UIViewController new];
  rootViewController.view = rootView;
  self.window.rootViewController = rootViewController;
  [self.window makeKeyAndVisible];
  return YES;
}

@end

在 AppDelegate.m 里面最主要的一个方法是 didFinishLaunchingWithOptions,它是在程序刚刚启动的时候执行的,所以可以在这里做一些程序启动需要的准备工作。而且 AppDelegate 是全局的,我们也可以在这里写一些全局变量或者方法。

RCTRootView

RCTRootView 是一个 UIView 容器,就是用户所能看到的所有视图都源于 RCTRootView,它也将所有的 React Natvie 视图封装在了原生组件中。

  • initWithBundleURL
    当应用开始运行的时候,RCTRootView 将会从 initWithBundleURL 的参数中加载应用:(本地调试的时候是直接在本地服务器中的 index.ios 加载,发布时设置有所不同)

  • moduleName
    在 index.ios.js 中注册的应用的名字

  • initialProperties
    必须是NSDictionary的一个实例。这一字典参数会在内部被转化为一个可供 JS 组件调用的 JSON 对象。

找到 React Native 的入口文件

所以在本地调试的时候,initWithBundleURL 会加载本地打包的 index.ios 包。这个包就是我们的 js 文件。那么整个 js 的入口文件就是 index.ios.js 文件:

import { AppRegistry } from 'react-native'
import AwesomeProject from './js/app'

AppRegistry.registerComponent('AwesomeProject', () => AwesomeProject)

在入口文件中,我们注册了一个名为 AwesomeProject 的 app,然后所有的逻辑都会从这个入口开始写。

React Native 调试进阶篇

工欲善其事,必先利其器。有了一个顺手的调试工具和调试方法,能很大的提高开发效率。

本文对于一些基本的调试功能会简单的介绍一下他们的原理和使用方法,具体的操作可以看官网的介绍,上面已经讲的很清楚了,就不浪费时间再写一遍了。

本文主要讲一下如何使用 React Developer Tools 配合模拟器自带的调试功能来调试,以及如何 debug。

刷新功能

刷新应用

在 iOS 模拟器中按下Command⌘ + R,Android 模拟器上对应的则是 按两下 R。相当于开发者菜单中的 Reload js,即将你项目中 js 代码部分重新生成 bundle,然后传输给模拟器或手机。

如果在iOS模拟器中按下 Command⌘ + R 没啥感觉,则注意检查 Hardware 菜单中,Keyboard 选项下的 Connect Hardware Keyboard 是否被选中。

自动刷新应用

选择开发菜单中的 Enable Live Reload 可以开启自动刷新。

自动刷新

打开该功能之后,React Native 能够监听你的 js 变化,自动生成 bundle 然后传输到你的模拟器或者手机上。
但是需要注意的是,此功能和 Reload js 一样,每次刷新的时候都会返回到启动页面。

自动热更新功能

选择开发菜单中的 Enable Hot Reloading 可以开启自动热更新功能。

开启热更新功能

如果说上面的 Enable Live Reload 功能解放了你的双手,那么 Enable Hot Reloading 功能还解放了你的时间。当你每次保存修改好的代码的时候,Hot Reloading 功能便会生成此次修改代码的增量包,然后传输到手机或模拟器上以实现热加载。

并且,Enable Hot Reloading 的刷新功能是在当前页面刷新,而不是返回到启动页面。

Errors and Warnings

红屏或黄屏提示都只会在开发版本中显示,正式的离线包中是不会显示的。

Errors

应用内的报错会以全屏红色显示在应用中(调试模式下),我们称为红屏(red box)报错。你可以使用 console.error() 来手动触发红屏错误。

红屏警告

部分代码如下:

export default class AwesomeProject extends Component {
  
  constructor(props) {
    super(props);
    this.onPressLearnMore = this.onPressLearnMore.bind(this);
  }
  
  onPressLearnMore() {
    console.error("这是一个红屏警告");
  }
  
  render() {
    return (
      <View style={styles.container}>
        <Button
          style={styles.button}
          onPress={this.onPressLearnMore}
          title="点我"
        />
      </View>
    );
  }
}

Warnings

应用内的警告会以全屏黄色显示在应用中(调试模式下),我们称为黄屏(yellow box)报错。点击警告可以查看详情或是忽略掉。 和红屏报警类似,你可以使用 console.warn() 来手动触发黄屏警告。

黄屏警告

在默认情况下,开发模式中启用了黄屏警告。可以通过以下代码关闭:

console.disableYellowBox = true;
console.warn('YellowBox is disabled.');

也可以通过代码屏蔽指定的警告,像下面这样调用 ignoreWarnings 方法,参数为一个数组:

console.ignoredYellowBox = ['Warning: Each child'];

数组里面可以包含多个你想忽略的告警的关键词,关键词希望适当的精确,屏蔽掉一些不必要的警告。

注意: 个人认为没有必要完全关闭黄屏警告,因为一些黄屏警告是因为我们的代码不规范造成的,通过黄屏警告也可以知道我们的代码有哪些不规范的地方。

访问控制台日志

下面的两种方法不能同时运行,否则其中一种将会无效

在终端查看

可以在终端中运行如下命令来查看控制台的日志:

react-native log-ios
react-native log-android

终端查看log

如果是 Android 应用,无论是运行在模拟器或是真机上,都可以通过在终端命令行里运行 adb logcat *:S ReactNative:V ReactNativeJS:V 命令来查看。

在终端打印log.png

通过 Chrome 开发者工具查看

在开发者菜单中选择 Debug JS Remotely 选项,即可以开始在 Chrome 中调试 JavaScript 代码。

打开 Chrome 开发者工具

打开 Chrome 的开发者工具即可看到打印出来的日志。

在 Chrome 开发者工具的控制台查看 log

在 Chrome 开发者工具中我们也可以调试代码,给代码打断点,相信大家都知道相关的操作,这里就不再详细的讲了。

在真机上调试

如果在 Android 5.0+ 的设备上调试,将设备通过 USB 连接到电脑上后,可以使用 adb 命令行工具来设定从设备到电脑的端口转发:

adb reverse tcp:8081 tcp:8081 // 第一个是主机的端口号,第二个是真机/模拟器的端口号

如果设备 Android 版本在 5.0 以下,则可以在开发者菜单中选择 Dev Settings - Debug server host for device,然后在其中填入电脑的 IP地址:端口

然后就可以和上面的示例一样来调试啦。

Show Perf Monitor

该功能主要是对 UI 和 JavaScript 的 FPS 的监控。

FPS 是测量用于保存、显示动态视频的信息数量。 通俗来讲就是指动画或视频的画面数。 例如在电影视频及数字视频上,每一帧都是静止的图象;快速连续地显示帧便形成了运动的假象。 每秒钟帧数(FPS) 愈多,所显示的动作就会愈流畅。

打开性能监控

打开之后就能看到性能监控的数据了。具体关于 React Native 性能的分析可以看官网的介绍,现在本文暂不做详细的讲解,之后可能会专门开一个关于性能的文章。

性能监控的数据

React Devtools

需要 React Native 0.43 或者更高的版本

我们平时在开发页面的时候,都用过查找元素的功能,模拟器自带的调试功能个人感觉不是很友好,但是官方提供了 react-devtools 可以很好的来查找 Dom 元素。

如果要使用它,需要在全局安装:

npm install -g react-devtools

在项目文件的目录下运行启动命令:

react-devtools

这时会弹出 react-devtools 窗口,里面是我们页面的元素。 react-devtools 的优点是可以搭配模拟器的 Toggle Inspector 功能一起使用,效果棒棒哒,哈哈。

首先我们先开启模拟器的 Toggle Inspector 功能。

开启 Toggle Inspector功能

下面是使用方法:

react-devtools 的使用方法

打开调试功能之后我们能看到,调试功能的界面有很多 Tab,下面来解释一下各个 Tab 的功能和作用。

Inspect

顾名思义,这个 Tab 是用来查找元素的,点击想要检查的元素,就可以显示出来元素的位置、样式和层级关系等。

Inspect 示例

Perf

Perf 显示的是应用的性能监控界面。

【翻译】玩转 React 表单

React 提供了两种从 <form> 元素中获取值的标准方法。第一种方法是实现所谓的受控组件 (可以看我博客里发表的文章) ,第二种方法是使用 React 的 ref 属性。

受控组件很重,被展示的值和组件的 state 绑定是它的特性。我们通过执行一个附着在 form 元素上的 onChange 事件句柄,来更新被展示的值。onChange 函数更新 state 属性,进而更新 form 元素的值。

(在看到下面的文章之前,如果你只是想看相应的示例代码:请移步这里

受控组件示例:

import React, { Component } from 'react';

class ControlledCompExample extends Component {
  constructor() {
    super();
    this.state = {
      fullName: ''
    }
  }
  handleFullNameChange = (e) => {
    this.setState({
      fullName: e.target.value
    })
  }
  handleSubmit = (e) => {
    e.preventDefault();
    console.log(this.state.fullName)
  }
  render() {
    return (
      <div>
        <form onSubmit={this.handleSubmit}>
          <label htmlFor="fullName">Full Name</label>
          <input
            type="text"
            value={this.state.fullName}
            onChange={this.handleFullNameChange}
            name="fullName" />
          <input type="submit" value="Submit" />
        </form>
      </div>
    );
  }
}

export default ControlledCompExample;

input 的值是 this.state.fullName (在第7行和第26行)。 onChange 函数是 handleFullNameChange (第 10 - 14 行和第 27 行)。

受控组件最主要的优势是:
1、便于验证用户的输入
2、可以根据受控组件的值动态地渲染其他组件。例如:一个用户在下拉列表中选择的值(如“dog” 或者 “cat” )可以控制在 form 中渲染的其他 form 组件(例如:一个设置品种的复选框)

受控组件的缺点是要写大量的代码。你需要通过 props 把 state 属性传递给 form 元素,还需要一个函数来更新这个属性的值。

对于单一表单元素来说这真的不是什么问题 —— 但是如果你需要一个庞大并且复杂的表单(不需要动态渲染或者实时验证),过度使用受控表单会让你书写成吨的代码。

从 form 元素取值的简便的方法是使用 ref 属性。我们用不同的方式来应对不同的 form 元素和组件结构,所以这篇文章剩下的内容分为以下几个部分。

1、文本输入框、数字输入框和选择框
2、子组件通过 props 传值给父组件
3、 Radio 标签集合
4、 Checkbox 标签集合

1、文本输入框、数字输入框和选择框

使用 ref 的最简单的例子是文本和数字 input 元素。我们在 input 的 ref 属性里添加一个把 input 本身作为参数的箭头函数。我喜欢把参数命名为和元素本身一样的的名字,就像下面的第三行那个样子:

<input
  type="text"
  ref={input => this.fullName = input} />

由于该参数是 input 元素本身的别名,你可以随心所欲地为它命名:

<input
  type="number"
  ref={cashMoney => this.amount = cashMoney} />

接着你可以拿到该参数,并将它赋值给当前 class 内 this 关键字上挂载的属性(译者注:这里的 class 指的是 JSX 所处的 React 组件 class)。input(例如: DOM 节点)可以通过 this.fullNamethis.amount 来读取。它的值可以通过 this.fullName.valuethis.amount.value 来读取。

选择元素也可以用相同的方法(例如:下拉列表)。

<select
  ref={select => this.petType = select}
  name="petType">
  <option value="cat">Cat</option>
  <option value="dog">Dog</option>
  <option value="ferret">Ferret</option>
</select>

选择元素的值可以通过 this.petType.value 获取。

2、子组件通过 props 传值给父组件

通过受控组件,父组件获取子组件的值十分简单 —— 父组件中已经有这个值了(译者注:在父组件中定义)!它被传递给子组件。同时 onChange 方法也被传给子组件,用户通过与 UI 互动(译者注:触发 onChange)来更新该值。

你可以在我上篇文章的受控组件示例中看到它是如何运行的。

虽然该值已经存在于受控组件的父组件中,但是当使用 ref 的时候却不是这样。使用 ref 的时候,该值存在于 DOM 节点自身当中,必须向上与父组件通信。

要将该值从子组件传给父组件,父组件需要向子组件传递一个 钩子 。然后子组件将节点挂载到 钩子 上, 以便父组件读取。

在我们更深入的探讨之前先来看一些代码。

import React, { Component } from 'react';

class RefsForm extends Component {
  handleSubmit = (e) => {
    e.preventDefault();
    console.log('first name:', this.firstName.value);
    this.firstName.value = 'Got ya!';
  }
  render() {
    return (
      <div>
        <form onSubmit={this.handleSubmit}>
          <CustomInput
            label={'Name'}
            firstName={input => this.firstName = input} />
          <input type="submit" value="Submit" />
        </form>
      </div>
    );
  }
}

function CustomInput(props) {
  return (
    <div>
      <label>{props.label}:</label>
      <input type="text" ref={props.firstName}/>
    </div>
  );
}

export default RefsForm;

通过上面的代码,可以看到一个 form 组件 RefForm 和一个叫做 CustomInput 的 input 组件。通常,箭头函数都是在 input 自身上面,但是从这(15 - 27 行)可以看到它是通过 props 传递的。由于箭头函数存在于父组件中,所以 this.firstName 中的 this 指向父组件。

input 子组件的值被赋给父组件的 this.firstName 属性,所以父组件可以获得子组件的值。现在,父组件中的 this.firstName 指的是子组件中的 DOM 节点(例如: CustomInput 中的 input)。

父组件不仅可以访问 input 中的 DOM 节点,还可以在父组件内给节点的值赋值。在上文的第 7 行可以看到例子。一旦表单被提交, input 的值就被设置为 “Got ya!” 。

这种方式有点让人摸不着头脑,所以请仔细揣摩并敲代码实践一下,直至完全理解。

你可能会写出来更好的 radio 和 checkbox  受控组件,但是如果你真的想要用 `ref` ,那么接下来的两部分会帮到你。

3、 Radio 标签集合

不像 text 和 number 这类 input 元素,radio 元素是成组出现的。每组中的元素都有相同的 name 属性,就像这样:

<form>
  <label>
    Cat
    <input type="radio" value="cat" name="pet" />
  </label>
  <label>
    Dog
    <input type="radio" value="dog" name="pet" />
  </label>
  <label>
    Ferret
    <input type="radio" value="ferret" name="pet" />
  </label>
  <input type="submit" value="Submit" />
</form>

在 “pet” radio 标签集合中有三个选项 —— “cat”、“dog” 和 “ferret”。

由于我们关心的是整个集合的元素,所以给每个 radio 设置 ref 并不是一个好主意。遗憾的是,没有 DOM 节点是包含了 radio 集合的。

可以通过下面的三步来检索出 radio 集合的值:
1、在 form 标签上设置 ref (下面的第20行)。
2、从 form 中取出这个 radio 集合。然后它应该是 pet 集合(下面的第9行)。

  • 此处返回一个节点列表和一个值。在这种情况下,这个节点列表包含三个 input 节点和被选中的值。
  • 需要注意的是这个节点列表是个类数组,它没有数组的方法。在下一部分中还有更多关于这个话题的内容。
    3、使用 . 方法来获取这个集合的值(下面的第13行)。
import React, { Component } from 'react';

class RefsForm extends Component {

  handleSubmit = (e) => {
    e.preventDefault();

    //  从 form 中取出节点列表
    //  它是一个类数组,没有数组的方法
    const { pet } = this.form;

    // radio 标签集合有 value 属性
    // 查看打印出来的数据
    console.log(pet, pet.value);
  }

  render() {
    return (
      <div>
        <form
          onSubmit={this.handleSubmit}
          ref={form => this.form = form}>
          <label>
            Cat
            <input type="radio" value="cat" name="pet" />
          </label>
          <label>
            Dog
            <input type="radio" value="dog" name="pet" />
          </label>
          <label>
            Ferret
            <input type="radio" value="ferret" name="pet" />
          </label>
          <input type="submit" value="Submit" />
        </form>
      </div>
    );
  }
}

export default RefsForm;

如果你正在用子组件写一个表单也是可行的。尽管组件中会有更多的逻辑,但是从 radio 集合中获取值的方法是不变的。

import React, { Component } from 'react';

class RefsForm extends Component {
  handleSubmit = (e) => {
    e.preventDefault();

    //  从 form 中取出节点列表
    //  它是一个类数组,没有数组的方法
    const { pet } = this.form;

    // radio 标签集合有 value 属性
    // 查看打印出来的数据
    console.log(pet, pet.value);
  }

  render() {
    return (
      <div>
        <form
          onSubmit={this.handleSubmit}
          ref={form => this.form = form}>
          <RadioSet
            setName={'pet'}
            setOptions={['cat', 'dog', 'ferret']} />
          <input type="submit" value="Submit" />
        </form>
      </div>
    );
  }
}

function RadioSet(props) {
  return (
    <div>
      {props.setOptions.map(option => {
        return (
          <label
            key={option}
            style={{textTransform: 'capitalize'}}>
            {option}
            <input
              type="radio"
              value={option}
              name={props.setName} />
          </label>
        )
      })}
    </div>
  );
}

export default RefsForm;

4、 Checkbox 标签集合

和 radio 标签集合不一样, Checkbox 标签集合可能有多个值。导致获取这些值会比获取 radio 标签集合的值难一些。

可以通过下面的五步来检索出 checkbox 标签集合被选中的值:

1、在 form 标签上设置 ref (下面的第27行)。
2、从 form 中取出这个checkbox 集合。然后它应该是 pet 集合(第9行)。

  • 此处返回一个节点列表和一个值
  • 需要注意的是这个节点列表是一个类数组,它没有数组的方法,然后我们就需要进行下面的这一步 ...
    3、把这个节点列表转换成一个数组,然后就可以使用数组的方法了(第 12 行的 checkboxArray )。
    4、使用 Array.filter() 获取选中的 checkbox (第 15 行的 checkedCheckboxes )。
    5、使用 Array.map() 获取选中的 checkbox 的唯一的值(第 19 行的 checkedCheckboxesValues
import React, { Component } from 'react';

class RefsForm extends Component {
  handleSubmit = (e) => {
    e.preventDefault();

    //  从 form 中取出节点列表
    //  它是一个类数组,没有数组的方法
    const { pet } = this.form;

    // 把节点列表转换成一个数组
    const checkboxArray = Array.prototype.slice.call(pet);

    // 仅取出被选中的 checkbox
    const checkedCheckboxes = checkboxArray.filter(input => input.checked);
    console.log('checked array:', checkedCheckboxes);

    // 使用 .map() 方法从每个被选中的 checkbox 中把值取出来
    const checkedCheckboxesValues = checkedCheckboxes.map(input => input.value);
    console.log('checked array values:', checkedCheckboxesValues);
  }

  render() {
    return (
      <div>
        <form
          onSubmit={this.handleSubmit}
          ref={form => this.form = form}>
          <label>
            Cat
            <input type="checkbox" value="cat" name="pet" />
          </label>
          <label>
            Dog
            <input type="checkbox" value="dog" name="pet" />
          </label>
          <label>
            Ferret
            <input type="checkbox" value="ferret" name="pet" />
          </label>
          <input type="submit" value="Submit" />
        </form>
      </div>
    );
  }
}

export default RefsForm;

使用子组件写 checkbox 的方法和上一部分中写 radio 的方法是一样的。

import React, { Component } from 'react';

class RefsForm extends Component {
  handleSubmit = (e) => {
    e.preventDefault();

    //  从 form 中取出节点列表
    //  它是一个类数组,没有数组的方法
    const { pet } = this.form;

    // 把节点列表转换成一个数组
    const checkboxArray = Array.prototype.slice.call(pet);

    // 仅取出被选中的 checkbox
    const checkedCheckboxes = checkboxArray.filter(input => input.checked);
    console.log('checked array:', checkedCheckboxes);

    // 使用 .map() 方法从每个被选中的 checkbox 中把值取出来
    const checkedCheckboxesValues = checkedCheckboxes.map(input => input.value);
    console.log('checked array values:', checkedCheckboxesValues);
  }

  render() {
    return (
      <div>
        <form
          onSubmit={this.handleSubmit}
          ref={form => this.form = form}>
          <CheckboxSet
            setName={'pet'}
            setOptions={['cat', 'dog', 'ferret']} />
          <input type="submit" value="Submit" />
        </form>
      </div>
    );
  }
}

function CheckboxSet(props) {
  return (
    <div>
      {props.setOptions.map(option => {
        return (
          <label
            key={option}
            style={{textTransform: 'capitalize'}}>
            {option}
            <input
              type="checkbox"
              value={option}
              name={props.setName} />
          </label>
        )
      })}
    </div>
  );
}

export default RefsForm;

结论

如果你不需要:

1、实时监视 form 元素的值(例如:为了基于用户的输入渲染之后的组件)
2、实时执行自定义验证方法

那么使用 ref 方法获取 form 元素的值是一个很好的方法。

大多数情况下,越过受控组件使用 ref 最主要的价值是会写更少的代码。 checkbox ( radio 其次)是一个特例。对于 checkbox ,使用 ref 省下的代码量是很少的,所以无法说是使用受控组件好还是 ref 好。

嗨, 你知道 this 吗?

在平时的代码中,相信大家经常用到 this,可是你真的明白此 this 真的是你认为的 this 吗?今天柚子君总结了一下平时用到的 this 的场景,大家走过路过不要错过啊~

首先咱们先来看一下《JavaScript 高级程序设计》上是怎么说的。

this 对象是在运行时基于函数的执行环境绑定的:在全局函数中,this 等于 windows,而当函数被作为某个对象的方法调用时,this 等于那个对象。

还有一种情况,在《深入理解 ES6》一书中写道:

如果箭头函数被非箭头函数包含,则 this 绑定的是最近一层非箭头函数的 this且不能通过 call()apply()bind() 方法来改变 this 的值

首先看一下非箭头函数的情况:

一、普通函数调用

这是一个普通的函数声明,在这种情况下,this 是指向 window 的.

    var test = '哈哈哈';
    function thisHandler() {
	    console.log('test:',this.test,'this:',this);
    }
    thisHandler() // test: 哈哈哈 this: window

其实上面的代码就相当于 window 调用 thisHandler(),所以这时 this 指向 window

    var b = '哈哈哈';
    function thisHandler() {
	    console.log('b:',this.b,'this:',this);
    }
    
    window.thisHandler() // b: 哈哈哈 this: window

二、作为对象的方法调用

当作为对象的方法被调用时,this 这时就指向调用它的对象。

var thisHandler = {
  name: "柚子",
  test: function(){
    console.log('my name:',this.name); 
  }
}
thisHandler.test() // my name: 柚子

再来一个栗子🌰:

var thisHandler = {
  name: "柚子",
  fn: {
	name: '芒果',
	test: function() {
       console.log('my name:',this.name); 
    }
  }
}
thisHandler.fn.test() // my name: 芒果

这时 this 指向的是对象 fn 了,所以,关于对象调用这一点明白了吗,如果明白了,那没关系,接着看下一个强化题😏:

var name = '柚子'
var thisHandler = {
  name: "芒果",
  fn: {
    name: '糖果',
	test: function(){
      console.log('my name:',this.name); 
    }
  }
}
var testHandler = thisHandler.fn.test
testHandler() 

🍭 这里是一秒钟分割线 🍭

哒哒哒,答对了,这里的 this 指向的 window,那么这是为什么呢,哪位小盆友来回答一下。
举手:

上面说到了,this 指向的是最后调用它的对象,第一步是赋值给了 testHandler,最后执行的那一句相当于 window.testHandler()。所以这里的 this 指向的是 window。最后输出的就是 my name: 柚子

哒哒哒,真聪明,来闯下一关~

三、构造函数的调用

var name = '柚子'
function Bar() {
  this.name = '芒果'
}

var handlerA = new Bar();
console.log(handlerA.name);  // 芒果
console.log(name) // 柚子

其实要明白为什么会是这样一个结果,咱们就要来聊聊 new 做了哪些事情。

  • 创建类的实例。这步是把一个空的对象的 __proto__ 属性设置为 Bar.prototype
  • 初始化实例。函数 Bar 被传入参数并调用,关键字 this 被设定为该实例。
  • 返回实例。

弄明白了 new 的工作内容,自然而然的也明白了上面输出的原因。

Bar() 中的 this 指向对象 handlerA,并不是全局对象。

四、apply / call 调用

关于 apply,可以看一下 MDN 关于 apply() 方法的说明

使用 apply 方法可以改变 this 的指向。如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动指向全局对象(浏览器中就是window对象)。

var name = '芒果';
var thisHandler = {
  name: "柚子",
  test: function(){
    console.log('my name:',this.name); 
  }
};

thisHandler.test(); //  my name: 柚子
thisHandler.test.apply(); // my name: 芒果

下面是箭头函数的舞台~

四、箭头函数

在《深入理解 ES6》一书中可以知道箭头函数和普通函数的一个不同之处就在于 this 的绑定。

箭头函数中没有 this 绑定,必须通过查找作用域链来决定其值。如果箭头函数被非箭头函数包含,则 this 绑定的是最近一层非箭头函数的 this;否则,this 的值会被设置为 undefined

var name = '柚子'
var thisHandler = {
  name: '芒果',
  test:() => {
    console.log('my name:',this.name,'this:',this); 
  }
}

thisHandler.test(); // my name: 柚子 this: Window

这时 this 不是指向 thisHandler,而是 Window

关于 this 的使用和体会还是要在平时运用中理解,先了解其原理,那么在使用的时候就如鱼得水啦。

参考:

探索一个 React Native 应用是如何启动的 —— Android 篇

当拿到一个项目的时候,我们要知道项目是如何运行起来的,这样方便我们更能理解项目的原理,也能帮助我们以后排错和优化。

本文的大致思路可以分为三步:

  • 找到整个应用的入口文件
  • 找到 native 端和 React Native 相连接的方法
  • 找到 React Native 的入口文件

根据以上三步,就可以知道整个项目是如何运行的了。下面我们一起来寻找吧~

用 Android Stutio 打开项目根目录下的 Android 文件夹,我们能看到下面的文件分布:

android文件夹

  • app 文件夹里面是 Android 的配置文件
  • app 文件下面的文件都是我们需要的原生组件

整个应用的入口文件

打开 app - manifest,我们看到里面有一个 AndroidManifest.xml 文件。此文件是整个应用的入口,配置了程序运行所必须的组件、启动位置以及一些其他的信息。

打开 AndroidManifest.xml 文件我们能看到下面的内容:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.myapp"
    android:versionCode="1"
    android:versionName="1.0">

    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
    <uses-permission android:name="android.permission.SYSTEM_OVERLAY_WINDOW" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.WAKE_LOCK"/>
    <uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION"/>

    <uses-sdk
        android:minSdkVersion="16"
        android:targetSdkVersion="22" />
    <application
      android:name=".MainApplication"
      android:allowBackup="true"
      android:label="@string/app_name"
      android:icon="@mipmap/ic_launcher"
      android:theme="@style/AppTheme">
      <activity
        android:name=".MainActivity"
        android:label="@string/app_name"
        android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
        android:screenOrientation="portrait"
        android:windowSoftInputMode="adjustResize">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
      </activity>
      <activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
    </application>

</manifest>

我们看到在这个文件中主要有以下节点:

  • manifest
  • uses-permission
  • uses-sdk
  • application
  • activity

下面就各个节点来做一个介绍:

AndroidManifest.xml 文件里主要是一个 manifest 节点,然后包裹其他的节点,通过配置不同的属性来得到我们想要的效果。

manifest

  • xmlns:android
    它是用来定义 android 的命名空间的,一般为 http://schemas.android.com/apk/res/android。有了它之后,就能够使用 Android 中各种的标准属性了。

  • package
    是本项目内 java 主程序包的包名,也是一个应用进程的默认名称。

  • android:versionCode
    interger 值。设备程序通过它来识别版本信息,代表 app 更新过多少次,1 代表 1 次,再更新的话就设置为 2,以此类推等等。

  • android:versionName
    这个值是给用户看的,设备可以是 1.0 版本,后续更新的话可以设置为 1.1、1.2 等等。

uses-permission

通过字面意思我们可以大致的认为它是需要和用户进行操作的。其实就是,app 安装的时候需要向用户索要的一些权限,用户可以同意也可以拒绝。

  • android:name
    即需要使用的权限的名字,可以是系统自带的权限,也可以是自定义的权限。

uses-sdk

uses-sdk 是用来设置 appandroid 系统的兼容性的。我们用了以下两个配置项,配置项的值是一个代表 Android API Level 参数的整数。每个 API Level 对应着一个 Android 系统的版本。下图为官方给出的 API Level 和 Android 系统版本的对照表。

android level 对照表

  • android:minSdkVersion
    一个用于指定应用运行所需最低 API 级别的整数。 如果系统的 API 级别低于该属性中指定的值,Android 系统将阻止用户安装应用。

  • android:targetSdkVersion
    一个用于指定应用的目标 API 级别的整数。如果未设置,其默认值与为 minSdkVersion 指定的值相等。一般情况下应该将这个属性的值设置为最新的 API level 值,这样我们才可以利用新版本系统上的新特性。

Application

每个 Android 应用程序启动的时候都会初始化一个 Application 类。我们可以通过它来定义一些全局的和一些上下文都要用到的变量和方法。

Application 的生命周期是整个应用中最长的,可以说它的生命周期就是整个应用的生命周期。

  • android:name
    一般情况下系统会自动创建一个 Application 类。如果我们需要自定义一个 Application 类的话,需要创建一个类并且继承 Application。然后在 name 属性中写上自定义的 Application 类名即可。

  • android:allowBackup
    Android API Level 8 及其以上 Android 系统提供了为应用程序数据的备份和恢复功能。此属性就是控制该功能的开关。系统默认为 true。当 allowBackup 标志为 true 时,用户可通过 adb backupadb restore 来进行对应用数据的备份和恢复。

  • android:label
    显示给用户的 APP 的名称

  • android:icon
    APP 的图标所在的路径。

  • android:theme
    它给所有的 Activity 定义了一个默认的主题风格,也可以在自己的 theme 里设置。

Activity

每个 Activity 都会获得一个用于绘制其用户界面的窗口。窗口通常会充满屏幕,但也可小于屏幕并浮动在其他窗口之上。

  • android:name
    如果第一个字节是小数点,那么会自动的在类别名称前加上这个项目的 package 名称。看到上面的例子中,咱们把 android:name 设为了 .MainActivity ,所以项目读取的时候应该为 com.myapp.MainActivity

  • android:label
    Activity 的标题,可以覆盖 Application 的 label。

  • android:configChanges
    当手机进行横屏和竖屏的切换时,是否调用 onConfigurationChanged() 方法。
    如果不设置此属性,当 Android 手机旋转后,会把当前 Activity 杀掉,然后根据方向重新加载这个Activity,就会从 onCreate 开始重新加载。
    如果设置了此属性,当手机旋转后,当前 Activity 就会调用 onConfigurationChanged() 方法,而不是调用 onCreate 方法。

  • android:screenOrientation
    activity 显示的模式。
    默认为 unspecified:由系统自动判断显示方向
    landscape 横屏模式,宽度比高度大
    portrait 竖屏模式, 高度比宽度大
    user 模式,用户当前首选的方向
    behind 模式:和该Activity下面的那个Activity的方向一致(在Activity堆栈中的)
    sensor 模式:有物理的感应器来决定。如果用户旋转设备这屏幕会横竖屏切换
    nosensor 模式:忽略物理感应器,这样就不会随着用户旋转设备而更改了

  • android:windowSoftInputMode
    activity 主窗口与软键盘的交互模式,可以用来避免输入法面板遮挡问题,Android1.5 后的一个新特性。
    adjustResize:该 Activity 总是调整屏幕的大小以便留出软键盘的空间

注意: 看到上面的代码里 activity 内还有 intent-filter 元素,说明这个 activity 会在应用启动的时候第一个被执行。

intent-filter

  • action
    android:name 值为 android.intent.action.MAIN,表明此 Activity 是作为应用程序的入口。

  • category
    android:name 值为 android.intent.category.LAUNCHER,表明应用程序是否显示在程序列表里。

native 端和 React Native 相连接的方法

MainActivity

通过上面的一些介绍,我们可以知道,有 intent-filter 节点,并且它的 actionandroid:nameandroid.intent.action.MAIN 的 Activity 为本项目的入口。那我们就可以顺藤摸瓜,一步一步的往下走。

打开 com.myapp 下的 MainActivity 文件,找到 onCreate 方法。这个方法主要是显示悬浮窗,并且加载 App。如果无权使用悬浮窗,则会提醒用户授权。

protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
        if (getUseDeveloperSupport() && Build.VERSION.SDK_INT >= 23) {
            if (Settings.canDrawOverlays(this)) { // 如果有权限使用悬浮窗,则显示,并且执行 loadapp 方法
                SplashScreen.show(this,true);
                loadapp();
            }else{
                Toast.makeText(MainActivity.this, "当前无权限使用悬浮窗,请授权!", Toast.LENGTH_SHORT).show();
                Intent serviceIntent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
                startActivityForResult(serviceIntent,ALERT_WINDOW_PERMISSION_CODE);
            }
        } else {
            SplashScreen.show(this,true);
            loadapp();
        }

    }

加载 app 的方法是 loadapp() 方法:

protected void loadapp() {
        
        mReactRootView = new ReactRootView(this); 
        ReactInstanceManagerBuilder builder = ReactInstanceManager.builder()
                .setApplication(getApplication())
                .setJSMainModuleName("index.android")
                .addPackage(new MainReactPackage())
                .addPackage(new RNJavaReactPackage())
                .addPackage(new LinearGradientPackage())
                .addPackage(new SvgPackage())
                .setUseDeveloperSupport(BuildConfig.DEBUG)
                .setInitialLifecycleState(LifecycleState.RESUMED);

        File bundleFile = new File(getExternalCacheDir(),"offline/index.android.bundle");
        if(bundleFile.exists()){
            builder.setJSBundleFile(bundleFile.getAbsolutePath());
        } else {
            builder.setBundleAssetName("index.android.bundle");
        }
        mReactInstanceManager = builder.build();
        mReactRootView.startReactApplication(mReactInstanceManager, "MyApp", null);
        setContentView(mReactRootView);
    }
  • 第一步 mReactRootView = new ReactRootView(this) 的意思是实例化 ReactRootView,并将它作为 Activity 的根 view
  • setApplication 的意思是把 native 端的 Application 对象赋给 React Native。
  • setJSMainModuleName 的参数是 js 的入口文件 —— index.android.jsindex.android.js 文件的路径是相对于 package.json 的路径。
  • addPackage 是自定义 React Native 的 Android 组件。
  • setUseDeveloperSupport 表示是否启用开发者模式。这里写 BuildConfig.DEBUG 就可以自动根据 gradle 构建的类型(debug或release)来决定。
  • mReactRootView.startReactApplication 方法是启动整个 React Native 程序。
  • setContentView 方法是将 mReactRootView 作为子布局加载到 Activity 中。

React Native 的入口文件

AppRegistry 是JS运行所有React Native应用的入口。通过 AppRegistry.registerComponent 来注册项目的根组件,然后原生系统才可以加载应用的代码包并且在启动完成之后通过调用 AppRegistry.runApplication 来真正运行应用。

import { AppRegistry } from 'react-native'
import MyApp from './js/app'

AppRegistry.registerComponent('MyApp', () => MyApp)

理解JavaScript原型


title: 理解JavaScript原型
date: 2017-07-30 20:42:42
tags:

JavaScript的原型是JavaScript中的重要一环,根据网上的一些资料和自己的理解,对原型做一个解释。

##prototype属性的引入
对象可以使用new操作符后跟一个构造函数来创建的。构造函数如下:

function Person(age) {
  this.name = "meng";
  this.age = age;
}

构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头。要创建一个新的实例对象的时候,可以使用new操作符。

var person1 = new Person(12);
var person2 = new Person(13);

在构建的两个实例对象的时候,person1和person2有一个共有属性name;当改变其中一个实例对象的name的时候,另一个不会发生改变。

person1.name = "jing";
console.log(person1.name);  //jing
console.log(person2.name);  //meng

每个实例对象都有自己的属性和方法的副本,改变其中的一个并不会影响另一个,这就造成了资源和空间的浪费,也无法实现数据的共享。

为了解决上面的问题,作者Brendan Eich决定使用构造函数设置一个prototype属性,可以让所有对象实例共享它所包含的属性和方法。

上面的例子可以写成:

function Person(age) {
  this.age = age;
}
Person.prototype.name = "meng";

var person1 = new Person(12);
var person2 = new Person(13);
Person.prototype.name = "jing";
console.log(person1.name);  //jing
console.log(person2.name);  //jing

这时person1和person2就共享了Person.prototype.name这个属性,只要其中一个改变,就会同时影响两个实例对象。

##原型是什么
只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。所有原型对象都会获得一个指向prototype对象的constructor属性。如图:

14805248662081.jpg
拿上面的例子来说,Person.prototype.constructor又指向了Person。

####proto、prototype和constructor
每一个生成的对象都有一个__proto__属性,当调用构造函数创建一个新的实例之后,__proto__就指向了构造函数的原型对象,即构造函数的prototype属性。

下图所示:当定义person1是一个数组时,person1自带一个__proto__属性。
14805256492639.jpg

下图所示:当定义Person构造函数时,Person自带一个prototype属性。prototype属性自带一个constructor属性,至于其他的属性,都是从Object继承而来的。

14805260237657.jpg
下图所示:当new一个实例对象person1时,_proto_就指向了构造函数Person的原型对象,即构造函数的prototype属性。可以对比一个person1的_proto_属性在实例化之前和之后的变化。

14805259038416.jpg

下图是展示了Person构造函数、Person的原型属性以及Person现有的两个实例之间的关系。
14805245325304.jpg

##new运算符的工作原理
new 运算符接受一个函数F及其参数:new F(arguments...)。这一过程分为三步:

创建类的实例。这步是把一个空的对象的__proto__属性设置为 F.prototype
初始化实例。函数F被传入参数并调用,关键字this被设定为该实例。
返回实例。
现在我们知道了new是怎么工作的,我们可以用JS代码实现一下:

function New (f) {
    var n = { '__proto__': f.prototype }; /*第一步*/
    return function () {
        f.apply(n, arguments);            /*第二步*/
        return n;                         /*第三步*/
    };
}

##hasOwnProperty函数

hasOwnProperty用来判断该属性属于实例自定义的属性而不是原型链上的属性。

function Person(age) {
  this.age = age;
}
Person.prototype.name = "meng";

var person1 = new Person(12);
person1.hasOwnProperty('age');  //true
person1.hasOwnProperty('name'); //false

##JavaScript引擎如何来查找属性

以下代码展示了JS引擎如何查找属性:

function getProperty(obj, prop) {
    if (obj.hasOwnProperty(prop))
        return obj[prop]

    else if (obj.__proto__ !== null)
        return getProperty(obj.__proto__, prop)

    else
        return undefined
}

通过上面的代码可以看出来,js先查找自身有没有该属性,如果没有的话,就查找__proto__属性指向的原型对象中有没有,如果没有的话,就去查它的原型的原型中有没有,一直到原型链的最顶端为止。

参考:
阮一峰:Javascript继承机制的设计**
JavaScript的原型机制

【翻译】在哪获取数据:componentWillMount vs componentDidMount

前段时间被问到这个问题,就 google 里一下,发现这篇文章讲到了重点,就翻译出来和大家一起分享。

原文链接:Where to Fetch Data: componentWillMount vs componentDidMount

校对:Cherry

当你需要为 React 组件获取一些数据的时候,你会从什么地方获取呢?

这个问题一直都存在。

有两个常见的获取数据的地方:

  • componentWillMount
  • componentDidMount

我们应该都很清楚,render 方法肯定不是一个获取数据或者做任何有关异步操作的好地方,它会以某种方式改变 state,也可能会引起副作用。

让我们来看看这两种常见的选项以及他们的优缺点。

componentWillMount

这个方法在组件第一次渲染之前被调用,所以猛一看会觉得这是写数据请求的最好地方。

但是这里有一个“陷阱”:在组件渲染之前获取数据的异步调用不会返回出来数据。就是说组件将至少一次会伴随着空数据被渲染。

这是没有办法为了等待数据返回而去“暂停”渲染。你不能在 setTimeout 方法里从 componentWillMount 中返回一个 promise。 处理这个问题的正确方式是设置组件的初始 state 来确保组件的有效渲染。

在 ES6 风格的 class 组件中,它的构造函数和 componentWillMount 的功能是一样的,所以,如果你已经有一个构造函数方法,那就可以直接把代码写在这里。

componentDidMount

等到 componentDidMount 被调用的时候,组件已经被渲染一次了。

事实证明,componentDidMount发送数据请求的最好的位置,有以下两个原因:

1、使用 DidMount 能够清晰的看出,直到初始渲染之后,数据才会被加载。这就提醒你要适当的去设置初始的 state,就不会因为 state 为 undefined 而导致错误

2、如果你需要在服务端(SSR/同构/其他的流行用语)渲染应用,那么 componentWillMount 将会被调用两次 —— 一次在服务端,一次在客户端,当然,这应该是你不愿意看到的。那么把数据加载的代码放在 componentDidMount 中能确保只会从客户端获取数据。

总结

我希望在哪加载数据的这个问题已经讲清楚了。如果你仍然不确定调用 AJAX 请求和加载数据的最好方法是什么,请阅读我的这篇文章 在 React 中发送 AJAX 请求

react native 在 mac os 系统下的环境配置

这里建议大家看英文版的安装文档,因为有一些步骤中文版的和英文版的不一样。下面以 react-native 官方提供的例子来分别说一下 android 和 ios 的环境配置过程。

一、必须安装的软件

无论是 android 还是 ios 都需要安装下面的软件。

Homebrew

Mac 系统的包管理器,用于安装 NodeJS 和一些其他必需的工具软件。可以查看官网链接,在控制台中输入下面的一条命令即可:

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

检查是否安装成功:

brew -v

之后控制台会输出 Homebrew 的版本信息:

yanmengdeiMac:~ yanmeng$ brew -v
Homebrew 1.3.3
Homebrew/homebrew-core (git revision 94fe; last commit 2017-09-25)

Node 和 Watchman

Watchman 是由 Facebook 提供的监视文件系统变更的工具。安装此工具可以提高开发时的性能(packager 可以快速捕捉文件的变化从而实现实时刷新)。

上一步我们已经安装了 Homebrew,那 Node 和 Watchman 就可以用它来安装了。具体命令如下:

brew install node
brew install watchman

注意:请确保你的 node 版本号在 4 及其以上版本。

二、安装 React Native 和示例项目

安装 React Native

在 bash 中执行下面的命令:

npm install -g react-native-cli

安装项目文件

在 bash 中执行下面的命令:

react-native init AwesomeProject
cd AwesomeProject

三、安装工具

1、IOS

a: 真机

b: 模拟器

其实 ios 的环境安装非常之简单,只要安装 Xcode IDE 就可以了。ios 大法好啊。可以通过 App Store 或是到 Apple 开发者官网上下载。这一步骤会同时安装 Xcode IDEXcode 的命令行工具。需要注意的是,React Native 目前需要 Xcode 8.0 或更高版本。
安装好之后,打开 Preferences,点击 Locations,检查一下版本号是否正确。

Xcode 检查版本号

ok 了之后,就可以进行第四步

2、Android

a: 真机

打开 USB 调试

确保你的设备已经打开了 USB 调试。

检查是否连接成功

在 bash 中执行下面的命令来检查手机是否已经和电脑连接:

adb devices

如果连接成功了,输入上面的那条命令之后,就会出现下面的信息:

真机连接

注意:在连接真机的时候需要把虚拟机关闭,确保只连接一个设备

如果连接成功了,可以进行第四步

b: 模拟器

安装软件 Android Studio

Android Studio 需要 Java SE Development Kit (JDK) 的支持。你可以在命令行中输入 javac -version 来查看你当前安装的 JDK 版本。如果版本不符合要求,可以到 官网上下载。

java 下载

java 环境配置好之后,接下来就是要从 官网 下载 Android Studio

注意: 在安装过程中,在 Install Type 这一步的时候最好选择 Custom。因为可以自定义一些设置和安装的组件。

需要更改的地方

选择好之后,点击 next,进入下一步,这时记得勾选下图中用红圈圈标出的两项。箭头指向的路径就是组件安装的路径。

sdk下载的地方.png

这两项选择好之后,一路 next 就可以啦。

安装好 Android Studio 之后,还要下载需要的 sdk。如下图,点击 Configure 下的 SDK Manager,进入 sdk 配置页面。

SDK Manager

在窗口的右下角勾选 show-package-details。然后在 Android 6.0 (Marshmallow) 下勾选以下这些选项:

  • Google APIs
  • Android SDK Platform 23
  • Source for Android 23
  • Intel x86 Atom System Image
  • Google APIs Intel x86 Atom_64 System Image

选中之后,点击 Apply,就可以开始下载了。

android-6.0配置

下一步是进入 SDK Tools 窗口。同样,在窗口的右下角勾选 show-package-details。找到 Android SDK Build-Tools 并展开它,选择 23.0.1,点击 Apply,就可以开始下载了。

SDK-Tools.png

到目前为止,需要的 sdk 已经下载好了,下面咱们就开始安装虚拟机了。虚拟机目前有两种方式,一种是在 Android Studio 中安装它自带的虚拟机;一种是使用 genymotion 虚拟机。在这里推荐第二种,简单快捷方便,所以下面主要介绍一下 genymotion 的下载和安装~

genymotion

genymotion 的下载地址:https://www.genymotion.com/download/ 。 这里需要登陆一下,才能进入到下载的页面。记得把自己的账号记住,下面还会用到。

genymotion 依赖 virtualboxvirtualbox 的下载地址在这里: https://www.virtualbox.org/wiki/Downloads

先安装 virtualbox,然后再安装 genymotion。下载下来的会有两个包,记得都要安装一下。

需要安装两个软件

打开 genymotion 的时候会让登陆一下,就是你刚刚记住的账号。然后就可以安装虚拟器了。如下图,点击 Add,可以选择一个手机类型和安卓系统的版本进行安装。

添加虚拟器

选择虚拟器

下载的过程还是挺慢的,需要耐心等待。下载好之后就能看到有一个手机模拟器出现在你本地的列表中啦。

成功添加虚拟器

三、ANDROID_HOME 变量配置

React Native 工具需要配置一些环境变量以便能够用本地代码构建 app。

首先在控制台中输入下面的命令,打开 .bash_profile 文件。如果没有的话,可以新建一个。新建文件的命令是 touch .bash_profile

open ~/.bash_profile

.bash_profile 文件中添加一下变量:

export ANDROID_HOME=~/Library/Android/sdk
export PATH=$PATH:$ANDROID_HOME/tools
export PATH=$PATH:$ANDROID_HOME/platform-tools

注意: 如果你是通过 Android Studio 安装的 sdk,那默认的位置就是 ~/Library/Android/sdk。否则的话就是安装的时候你自己定义的位置。编辑好之后,记得保存。

配置文件.png

保存之后,在控制台执行下面的命令,让配置生效。

source ~/.bash_profile

检查配置是否生效,可以在控制台执行下面的命令。

echo $ANDROID_HOME

配置android-home变量.png

四、执行任务

在第二步中我们已经安装好 react-native 和项目啦,现在让我们把项目运行起来~

ios

在 ios 端运行的话,在项目所在的目录下执行下面的命令就可以了:

react-native run-ios

android

在 android 端运行的话,先打开 Genymotion,然后再打开一个安卓的虚拟机,然后在你的项目目录下,执行下面的命令就可以了:

react-native run-android

五、可能遇到的问题

1、SDK location not found

A problem occurred configuring project ':app'.
> SDK location not found. Define location with sdk.dir in the local.properties file or with an ANDROID_HOME environment variable.

遇到的问题.png

如果遇到这个问题,stackoverflow 上的答案在这里

2、adb: command not found

adb找不到

adb 全名 Andorid Debug Bridge。这是一个 Debug 工具,是要连接开发电脑和你的调试手机的。

如果出现这个错误,可能就是没有配置 Android 的环境变量,或者配置了环境变量,但是没有让它生效。关于这两项的配置,可以参考上面的 ANDROID_HOME 变量配置 这一部分。

3、No connected devices

没有连接中的设备

这个看英文也知道是什么意思啦,就是没有连接中的设备。如果用的是模拟机的话,就要打开模拟机;如果是真机的话,记得要连接上手机。
可以运行 adb devices 来检查连接中的设备。

连接设备.jpeg

4、No bundle url present

No bundle url present. Make sure you’re running a packager server or have included a .jsbundle file in your application bundle.

No bundle url present

在 ios 运行的时候如果遇到这种情况,就把项目目录下 ios 文件夹中的 build 文件删除,然后再运行就可以了。

删除build文件

commit提交规范

采用 Angular 的 commit 规范。不写 commit message 不允许提交代码。

commit 基本格式

分为三部分:headerbodyfooter
header是必须的,bodyfooter 可以省略。

<type>(<scope>): <subject>
// 空一行
<body>
// 空一行
<footer>

header

<type>(<scope>): <subject>

header 包含三个字段: type(必须)、scope(可选)和 subject(必须)

type

用于说明本次提交的类型,包含下面几种情况:

  • feat:新增feature
  • fix:修复 bug
  • perf: 提高代码性能的更改
  • docs: 仅仅修改了文档。如:README
  • style: 仅仅修改了代码样式,并不改变代码逻辑。如:缩进、空格和逗号等。
  • refactor: 代码重构。没有增加新功能,也没有修改 bug。
  • test: 测试用例相关。
  • build: 构建流程或者依赖库的变动。
  • revert: 回滚到上一个版本
  • deps: 升级依赖
scope

用于说明 commit 影响的范围,比如数据层、控制层、视图层等等,视项目不同而不同。

修改文件的范围(包括但不限于 doc, middleware, core, config, plugin)

subject

此次 commit 的简短描述。建议不超过 50 个字符。

常用表述语:

  • add 添加
  • change 改变
  • update 更新
  • remove 移动
  • delete 删除

注意: 使用第一人称现在时,比如使用 change 而不是 changed 或 changes。

body

针对本次 commit 的详细描述,可以多行。需要描述的信息包括:

  • 代码变动的动机
  • 与以前行为的对比

footer

footer 的内容只会在以下两种情况的时候使用:

不兼容的变动

如果是和之前不兼容的变动,应该以 BREAKING CHANGE 为开头。比如:

// 哪些被改变了
BREAKING CHANGE: isolate scope bindings definition has changed and
    the inject option for the directive controller injection was removed.
    
    To migrate the code follow the example below:
    
    Before:
    
    scope: {
      myAttr: 'attribute',
      myBind: 'bind',
      myExpression: 'expression'
    }
    
    After:
    
    scope: {
      myAttr: '@',
      myBind: '@',
      myExpression: '&'
    }
    
    // 改变的原因
    The removed `inject` wasn't generaly useful for directives so there should be no code using it.

关闭 issue

如果此次改动是为了关闭某个 issue,则可以在 footer 中关闭。例如:

Closes #234

或者关闭多个 issue:

Closes #123, #245, #992

revert

如果 commit 用于撤销以前的 commit,则必须以 revert: 开头,后面跟着被撤销 Commit 的 Header。

revert: feat(pencil): add 'graphiteWidth' option
This reverts commit 667ecc1654a317a13331b17617d973392f415f02.

body 的格式是固定的:This reverts commit <hash>hash 是被撤销的 commit 的 SHA 标识符。

栗子

  1. 解决 bug:
fix($compile): couple of unit tests for IE9

Older IEs serialize html uppercased, but IE9 does not...
Would be better to expect case insensitive, unfortunately jasmine does
not allow to user regexps for throw expectations.

Closes #392
Breaks foo.bar api, foo.baz should be used instead

2.修改标点符号

style($location): add couple of missing semi colons
  1. 新增功能
feat($compile): simplify isolate scope bindings

Changed the isolate scope binding options to:
  - @attr - attribute binding (including interpolation)
  - =model - by-directional model binding
  - &expr - expression execution binding

This change simplifies the terminology as well as
number of choices available to the developer. It
also supports local name aliasing from the parent.

BREAKING CHANGE: isolate scope bindings definition has changed and
the inject option for the directive controller injection was removed.

To migrate the code follow the example below:

Before:

scope: {
  myAttr: 'attribute',
  myBind: 'bind'
}

After:

scope: {
  myAttr: '@',
  myBind: '@'
}

The removed `inject` wasn't generaly useful for directives so there should be no code using it.

Git分支规范

  • 基本原则:master为保护分支,不直接在master上进行代码修改和提交。
  • 分支命名规范:
    • 分支版本命名规则:分支类型_分支发布时间_ 分支功能。比如:feature_20170401_fairy_flower
    • 分支类型包括:featurebugfixrefactor 三种类型,即新功能开发bug 修复代码重构
    • 时间使用年月日进行命名,不足 2 位补 0
    • 分支功能命名使用 snake case 命名法,即下划线命名。

commit 工具流

commitizen

全局安装 commitizen

npm i -g commitizen

然后,在项目目录里,运行下面的命令,使其支持 Angular 的 Commit message 格式。

commitizen init cz-conventional-changelog --save --save-exact

以后凡是用到 git commit 的地方都用 git cz。然后就会出现生成规范的 git message 的选项:

git-cz.jpeg

validate-commit-msg

validate-commit-msg 用来检查 commit message 是否符合规范。

1、安装 validate-commit-msg 依赖:

npm i validate-commit-msg -S

2、使用 githooks 来校验:

安装 Husky

Husky 继承了 Git 下所有的钩子,在触发钩子的时候,Husky 可以阻止不合法的 commit, push 等等。

npm i Husky -S

配置参数:

// package.json

{
    "script": {
        "commitmsg": "validate-commit-msg"
    }
}

之后提交代码的时候可以用 git cz 来提交,有一个整体的流程来提醒你用规范的 commit message 来说明本次的提交信息。不过我建议还是用 git commit -m "message" 来提交,这样可以强制自己记住提交规范,不用依赖外部帮助,便于自己养成良好的习惯。

生成 change log

如果你的提交信息都符合 Angular 的规范,那么这些信息就会出现在 change log 里面。

只有 typefeatfixcommit 会出现在 change log 中,其他的不会出现,当然你也可以自己自定义。

如何生成 change log

安装依赖 npm i conventional-changelog -S

在 package.json 中配置下面的命令:

{
  "script": {
      "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0"
   }
}

然后运行命令:npm run changelog CHANGELOG.md 文件就会出现在你的项目根目录下面。

参考

《深入理解 ES6》笔记 — 模块

什么是模块

自动运行在严格模式下并且没有办法退出运行的 JavaScript 代码。在模块的顶部, this 的值是 undefined;模块不支持 HTML 代码风格的代码注释。模块仅导入和导出你需要的绑定。

导出

export 导出

// 导出数据
export const NAME = "柚子";

 // 这个 age 变量是此模块私有的
let age = 18;

 // 导出函数
export function add(a, b) {
    return a + b;
}

function subtract(a, b) {
    return a - b
}
 // 导出引用
export subtract;

任何未显式到处的变量、函数或类都是模块私有的,外部无法访问。

导入

import 导入

语法:

import { NAME, add, subtract } from './example.js';
// 此语句的含义是,从 example.js 文件中导入需要的标识符。
// 可以导入一个或多个

add(1, 2) // 3
 
NAME = 1; // 抛错,不能给导入的绑定重新赋值

导入整个模块

    import * as example from './example.js';
    
    cconsole.log(example.NAME); // 柚子
    example.add(1, 2) // 3

这种导入格式被称为命名空间导入;在 example.js 文件中不存在 example 对象,所以它作为 example.js 中所有导出成员的命名空间对象而被创建。

 import { NAME } from './example.js';
 import { add } from './example.js';
 import { subtract } from './example.js';

不管 import 语句中把一个模块写了几次,该模块都只对执行一次。

注意: export 语句不允许出现在 if 语句中,也不能在一条语句中使用 import,只能在顶部使用它。

if(isTrue) {
    export add; // 这样写会报错
}
function importSomething() {
    import { add } from './example.js'; // 这样写会报错
}

重命名

通过 as 关键字来指定函数在模块外应该被叫做什么名字

 // example.js
function sum(a, b) {
    return a + b
}
export { sum as add }; // 这里 sum 是本地名称,add 是导出时使用的名称
    
// app.js
import { add } from './example.js';
// 在导入的时候,必须要使用 add
// example.js
export function sum(a, b) {
    return a + b
}
    
// app.js
import { sum as add } from './example.js';
// 在导入时用 add 来重命名 sum

add(1, 2) // 3

默认值

每个模块只能有一个默认的导出值。
default 表示这是一个默认的导出。

// 导出默认值
export default function(a, b) {
    return a + b;
}

也可以是:

// 导出默认值
function sum(a, b) {
    return a + b;
}
export default sum; 

如果要导入默认值的话,就不能加中括号了:

import sum from './example.js';

如果文件中既有默认值,也有非默认值:

 export let age = 18;
 
 export default function(a, b) {
     return a + b;
 }

那么导入的时候就需要这样写:

import add, { age } from './example.js';
// 在 import 语句中,默认值必须排在非默认值之前

结尾

所以,看完了上面的解析之后,现在知道下面的写法是什么意思了吧。

import React, { Component, PropTypes } from 'react';

export default class App extends Component {
    
}

Angular之路--带你来搭建Webpack 2 + Angular 4项目

上个月Angular发布了4.0.0版本,少年们,赶快学起来吧,这篇文章带领大家搭建一个简单的Angular应用,会尽量详细的把每个点都解释到。

首先我选择了用webpack2来作为打包工具,选择wenpack2的理由不言而喻。这里假设你已经了解webpack2的一些原理,下面开始来学习吧~

详细代码可以看我的git项目

一、配置 webpack

首先新建一个项目文件夹

mkdir angular-dream
cd angular-dream

在控制台中输入命令 npm init ,创建 package.json 文件。如图:

创建package.json文件
在控制台中可以一路回车。当然,这里我命名了项目的名称为 angular-dream ,还有一些其他的信息。

创建好之后用编辑器(我使用的是webstorm)打开这个项目。

package.json 文件的配置

关于 package.json 文件里面的一些参数的含义,可以参考阮一峰老师写的这篇文章

{
  "name": "angular2-dream",
  "version": "1.0.0",
  "description": "Hello Angular2",
  "scripts": {
    "start": "webpack-dev-server --config config/webpack.dev.js --progress",
    "test": "karma start",
    "build": "webpack --config config/webpack.dev.js --progress --profile --bail",
    "webpack": "webpack",
    "rimraf": "rimraf"
  },
  "keywords": [
    "angular2",
    "webpack"
  ],
  "author": "[email protected]",
  "license": "MIT",
  "dependencies": {
    "@angular/animations": "~4.0.1",
    "@angular/common": "~4.0.0",
    "@angular/compiler": "~4.0.0",
    "@angular/core": "^4.0.1",
    "@angular/forms": "~4.0.1",
    "@angular/http": "~4.0.1",
    "core-js": "^2.4.1",
    "rxjs": "5.2.0",
    "zone.js": "^0.8.5"
  },
  "devDependencies": {
    "reflect-metadata": "^0.1.10",
    "html-webpack-plugin": "^2.28.0",
    "@angular/compiler-cli": "~4.0.1",
    "@angular/platform-browser": "~4.0.1",
    "@angular/platform-browser-dynamic": "~4.0.1",
    "@angular/platform-server": "~4.0.1",
    "@angular/router": "~4.0.1",
    "@angularclass/hmr": "^1.2.2",
    "@angularclass/hmr-loader": "^3.0.2",
    "@types/jasmine": "^2.5.43",
    "@types/node": "^6.0.45",
    "angular2-template-loader": "^0.6.0",
    "awesome-typescript-loader": "^3.0.4",
    "bootstrap": "^4.0.0-alpha.6",
    "bootstrap-sass": "^3.3.7",
    "css-loader": "^0.26.1",
    "extract-text-webpack-plugin": "2.0.0-beta.5",
    "file-loader": "^0.9.0",
    "font-awesome": "^4.7.0",
    "html-loader": "^0.4.3",
    "postcss-loader": "^1.3.1",
    "raw-loader": "^0.5.1",
    "style-loader": "^0.13.1",
    "to-string-loader": "^1.1.5",
    "ts-helpers": "^1.1.2",
    "url-loader": "^0.5.7",
    "webpack": "2.2.0",
    "webpack-dev-server": "2.2.0-rc.0",
    "webpack-merge": "^2.4.0",
    "typescript": "^2.2.2"
  }
}
  • @angular/compiler - Angular的模板编译器。 它会理解模板,并且把模板转化成代码,以供应用程序运行和渲染。 开发人员通常不会直接跟这个编译器打交道,而是通过platform-browser-dynamic或离线模板编译器间接使用它。
  • @angular/platform-browser - 与DOM和浏览器相关的每样东西,特别是帮助往DOM中渲染的那部分。 这个包还包含bootstrapStatic方法,用来引导那些在产品构建时需要离线预编译模板的应用程序
  • @angular/platform-browser-dynamic - 为应用程序提供一些提供商和bootstrap方法,以便在客户端编译模板。不要用于离线编译。 我们使用这个包在开发期间引导应用,以及引导plunker中的范例。
  • core-js - 为全局上下文(window)打的补丁,提供了ES2015(ES6)的很多基础特性。 我们也可以把它换成提供了相同内核API的其它填充库。 一旦所有的“主流浏览器”都实现了这些API,这个依赖就可以去掉了。
  • reflect-metadata - 一个由Angular和TypeScript编译器共享的依赖包。

tsconfig.json 文件的配置

在项目的根目录下创建 tsconfig.json 文件。

浏览器不能直接执行 TypeScript ,需要用编译器转译成JavaScript,而且编译器需要进行一些配置。 tsconfig.json 的配置就是指导编译器如何生成JavaScript文件。

{
  "compilerOptions": {
    "declaration": false,
    "module": "commonjs", // 组织代码的方式
    "target": "es5", // 编译目标平台
    "moduleResolution": "node",
    "sourceMap": true, // 把ts文件变异成js文件时,是否生成对应的SourceMap文件
    "emitDecoratorMetadata": true, // 让TypeScript支持为带有装饰器的声明生成元数据
    "experimentalDecorators": true, // 是否启用实验性装饰器特性
    "noImplicitAny": true,
    "lib": ["dom", "es6"],
    "suppressImplicitAnyIndexErrors": true
  },
  "exclude": [
    "node_modules",
    "dist"
  ],
  "awesomeTypescriptLoaderOptions": {
    "forkChecker": true,
    "useWebpackText": true
  },
  "compileOnSave": false,
  "buildOnSave": false
}

noImplicitAny 标志是 true 并且TypeScript编译器无法推断出类型时,它仍然会生成JavaScript文件。 但是它也会报告一个错误。 很多饱经沧桑的程序员更喜欢这种严格的设置,因为类型检查能在编译期间捕获更多意外错误。

创建 webpack.config.js文件

在根目录下创建 webpack.config.js文件

module.exports = require('./config/webpack.dev.js');

现在在控制台中执行 npm install 命令,安装项目的依赖。

二、Polyfills

配置好上述的几个文件之后呢,我们在项目中的根目录下创建一个 src 文件夹。

src 文件夹的下面新建一个 polyfills.ts 文件。

polyfills.ts 文件里引入了运行Angular应用时所需的一些标准js。

import 'core-js/es6/symbol';
import 'core-js/es6/object';
import 'core-js/es6/function';
import 'core-js/es6/parse-int';
import 'core-js/es6/parse-float';
import 'core-js/es6/number';
import 'core-js/es6/math';
import 'core-js/es6/string';
import 'core-js/es6/date';
import 'core-js/es6/array';
import 'core-js/es6/regexp';
import 'core-js/es6/map';
import 'core-js/es6/set';
import 'core-js/es6/weak-map';
import 'core-js/es6/weak-set';
import 'core-js/es6/typed';

/** Evergreen browsers require these. **/
import 'core-js/es6/reflect';

import 'core-js/es7/reflect';

/***************************************************************************************************
 * Zone JS is required by Angular itself.
 */
import 'zone.js/dist/zone';

import 'ts-helpers';

if (process.env.ENV === 'production') {
  // Production
} else {
  // Development and test
  Error['stackTraceLimit'] = Infinity;
  require('zone.js/dist/long-stack-trace-zone');
}

三、Vendor

src 文件夹的下面新建一个 vendor.ts 文件。

vendor.ts 文件里面引入了一些第三方的依赖。

// Angular
//包含所有提供商依赖
import '@angular/platform-browser';
import '@angular/platform-browser-dynamic';
import '@angular/compiler';
import '@angular/core';  // 存放核心代码,如变化监测机制,依赖注入机制,渲染,装饰器等。
import '@angular/common';
import '@angular/http';
import '@angular/router';

// RxJS
import 'rxjs/Observable';
import 'rxjs/Subscription';
import 'rxjs/Subject';
import 'rxjs/BehaviorSubject';

// Bootsctrap
import 'bootstrap/dist/css/bootstrap.css';
import 'font-awesome/css/font-awesome.css';

四、Main

src 文件夹的下面新建一个 main.ts 文件。

main.ts 文件中,我们指定了项目的根模块为 AppModule

import {AppModule} from './app/app.module';
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";

platformBrowserDynamic().bootstrapModule(AppModule);

// platformBrowserDynamic().bootstrapModule()方法来编译启用AppModule模块
// 根据当前的运行环境,如操作系统、浏览器,来初始化一个运行环境,然后从这个环境里面运行AppModule。

五、config

在根目录下创建一个 config 文件夹

helpers.js

config 文件夹下面创建一个 helpers.js 文件。

在这里请注意入口 polyfills,vendorapp 的先后顺序。

var path = require('path');
var _root = path.resolve(__dirname, '..');
function root(args) {
  args = Array.prototype.slice.call(arguments, 0);
  return path.join.apply(path, [_root].concat(args));
}
exports.root = root;

webpack.common.js

config 文件夹下面创建一个 webpack.common.js 文件。

const helpers = require('./helpers');
var webpack = require('webpack');
var HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: {
    'polyfills': './src/polyfills.ts', // 运行Angular时所需的一些标准js
    'vendor': './src/vendor.ts', // Angular、Lodash、bootstrap.css......
    'app': './src/main.ts' // 应用代码
  },
  resolve: { // 解析模块路径时的配置
    extensions: ['.ts', '.js'] // 制定模块的后缀,在引入模块时就会自动补全
  },
  module: {
    rules: [ // 告诉webpack每一类文件需要使用什么加载器来处理
      {
        test   : /\.ts$/,
        loaders: ['awesome-typescript-loader', 'angular2-template-loader']
        //awesome-typescript-loader - 一个用于把TypeScript代码转译成ES5的加载器,它会由tsconfig.json文件提供指导
        //angular2-template-loader - 用于加载Angular组件的模板和样式
      }, {
        test: /\.json$/,
        use : 'json-loader'
      }, {
        test: /\.styl$/,
        loader: 'css-loader!stylus-loader'
      }, {
        test   : /\.css$/,
        loaders: ['to-string-loader', 'css-loader']
      }, {
        test: /\.html$/,
        use: 'raw-loader',
        exclude: [helpers.root('src/index.html')]
        //html - 为组件模板准备的加载器
      }, {
        test:/\.(jpg|png|gif)$/,
        use:"file-loader"
      }, {
        test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
        use : "url-loader?limit=10000&minetype=application/font-woff"
      }, {
        test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
        use : "file-loader"
      }
    ]
  },
  plugins: [
    //热替换
    new webpack.HotModuleReplacementPlugin(),
    new webpack.optimize.CommonsChunkPlugin({
      name: ['vendor', 'polyfills']
      //多个html共用一个js文件,提取公共代码
    }),
    
    new HtmlWebpackPlugin({
      template: './src/index.html'
      // 自动向目标.html文件注入script和link标签
    })
  ]
};

webpack.dev.js

config 文件夹下面创建一个 webpack.dev.js 文件。

var webpackMerge = require('webpack-merge');
var commonConfig = require('./webpack.common.js');
const helpers = require('./helpers');

module.exports = webpackMerge(commonConfig, {
  output   : {
    path      : helpers.root('dist'),
    publicPath: '/',
    filename  : '[name].js'
  },
  devServer: {
    port              : 8080,
    historyApiFallback: true
  }
});

至此,现在的目录结构就如下图所示:


因为我们还没有创建 AppModule ,所以 main.ts 文件会被标红。

六、根模块 AppModule

基本的配置已经完成啦,现在我们来创建根模块~

src 文件下面新建一个 app 文件夹,

创建 app.component.ts

app 文件夹下面新建 app.component.ts 文件

import { Component } from "@angular/core";

@Component({
  selector   : 'root-app',
  templateUrl: './app.component.html'
})
export class AppComponent {
  constructor() {}
}

创建 app.component.html

app 文件夹下面新建 app.component.html 文件

<h1 class="title">Hello Angular2</h1>

<router-outlet></router-outlet>

创建 app.routes.ts

这里我们用一下路由来完成页面之间的跳转

import { Routes } from '@angular/router';
import { AppComponent } from "./app.component";
export const routes: Routes = [ // Routes类型的数组
  {
    path      : 'index',
    component : AppComponent
  },{
    path      : '',
    redirectTo: 'index',
    pathMatch : 'full'
  }
];

创建 app.module.ts

app 文件夹下面新建 app.module.ts 文件

import { AppComponent } from './app.component';
import { routes } from './app.routes';
import { BrowserModule } from "@angular/platform-browser";
import { FormsModule } from "@angular/forms";
import { RouterModule } from "@angular/router";
import { NgModule } from "@angular/core";
//@NgModule装饰器用来为模块定义元数据
@NgModule({ // @NgModule 用来定义模块用的装饰器
  declarations: [AppComponent], // 导入模块所依赖的组件、指令等,用于指定这个模块的视图类
  imports: [
    BrowserModule, //包含了commonModule和applicationModule模块,封装在浏览器平台运行时的一些工具库
    FormsModule,  // 表单相关的组件指令等,包含了[(ngModel)]
    RouterModule.forRoot(routes,{useHash: false}), // RouterModule.forRoot()方法来创建根路由模块
  ], // 导入当前模块所需要的其他模块
  bootstrap: [AppComponent], // 标记出引导组件
  //把这个AppComponent标记为引导 (bootstrap) 组件。当Angular引导应用时,它会在DOM中渲
  //染AppComponent,并把结果放进index.html的元素内部。
})
export class AppModule { }

六、宿主页面

src 文件夹下面新建 index.html 文件

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Angular2 Hello Word</title>
    <base href="/">
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
    <root-app>Loading...</root-app>
</body>
</html>

好啦,此时的项目目录结构就是下图所示:

接下来运行 npm start 开始你的 Angular 之旅吧~

参考:

建站全记录(一)在 Ubuntu16.04.1 服务器上搭建 LNMP 服务

我的服务器信息是:Ubuntu Server 16.04.1 LTS 64位。

LNMP 环境代表 Linux 系统下 Nginx + MySQL + PHP 网站服务器架构。

在 Ubuntu 环境下可以通过 Apt-get 快速安装软件。比如:

sudo apt-get install 软件名称

如果想要查看已经安装软件的信息,可输入下面的命令:

  • 可通过命令 sudo dpkg -L 软件名查看软件包所在的目录以及该软件包中的所有文件。
  • 可通过命令 sudo dpkg -l 软件名查看软件包的版本信息。

更详细的可以查看腾讯云的文档

Nginx

为了获取最新的 Nginx,可以先更新一下源列表。

sudo apt-get update

安装 Nginx

sudo apt-get install nginx

启动 Ngnix 服务

sudo /etc/init.d/nginx start

命令行中测试 Nginx 服务是否正常运行。

wget http://127.0.0.1

若服务正常,显示结果如下。

ubuntu@VM-0-63-ubuntu:~$ wget http://127.0.0.1
--2018-05-26 19:39:51--  http://127.0.0.1/
Connecting to 127.0.0.1:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 612 [text/html]
Saving to: 'index.html'

index.html             100%[=========================>]     612  --.-KB/s    in 0s

2018-05-26 19:39:51 (58.0 MB/s) - 'index.html' saved [612/612]

关于 wget 命令的知识,可以查看此文章

浏览器中测试 Nginx 服务是否正常运行。

访问 Ubuntu 云服务器公网 IP。
若服务正常,显示结果如下。

WX20180526-194830@2x.png

安装配置 MySQL

安装 MySQL

sudo apt-get update
sudo apt-get install mysql-server

配置

安装过程中将会让你设置密码

sudo mysql_secure_installation

端口查看

安装完成后,输入命令:netstat -anp ,会发现 3306 端口正在被监听,此时已可以编写 PHP 脚本来连接数据库。

安装配置 PHP

安装 php

sudo apt-add-repository ppa:ondrej/php
sudo apt-get update
sudo apt-get install php7.1 php7.1-fpm

输入 php -v 检查是否安装成功,如果显示下面的信息,则表示安装成功。

PHP 7.1.17-1+ubuntu16.04.1+deb.sury.org+1 (cli) (built: May  5 2018 04:55:21) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.1.0, Copyright (c) 1998-2018 Zend Technologies
    with Zend OPcache v7.1.17-1+ubuntu16.04.1+deb.sury.org+1, Copyright (c) 1999-2018, by Zend Technologies

配置 php

sudo vim /etc/php/7.1/fpm/php.ini  

输入 /fix_pathinfo 搜索,将 cgi.fix_pathinfo=1 改为cgi.fix_pathinfo=0

sudo vim /etc/php7.1/fpm/pool.d/www.conf

找到 listen = /run/php/php7.1-fpm.sock 修改为 listen = 127.0.0.1:9000。使用 9000 端口。

然后重启 php 环境:

service php7.1-fpm stop
 service php7.1-fpm start

Nginx 与 PHP-FPM 集成

启动 PHP-FPM

sudo /etc/init.d/php7.1-fpm start

输入命令查看 PHP-FPM 默认配置

sudo netstat -tunpl | grep php-fpm

屏幕快照 2018-05-27 下午8.41.42.png

以上结果表明 PHP-FPM 默认配置的监听端口为 9000,只需修改配置,将 PHP 解析的请求转发到 127.0.0.0:9000 处理即可。

修改 Nginx 配置

sudo vim /etc/nginx/sites-available/default

将里面 php 的配置修改为下面的配置:

server {
        listen 8080;
        server_name  localhost;

        root /var/www/html;

        server_name _;

        location / {
                root  html;
                index  index.html index.htm index.php;
                try_files $uri $uri/ =404;
        }

        location ~ \.php$ {
                root html;
                fastcgi_pass 127.0.0.1:9000;
                #fastcgi_pass unix:/var/run/php7.1-fpm.sock;
                fastcgi_index index.php;
                fastcgi_param SCRIPT_FILENAME       $document_root$fastcgi_script_name;
                include fastcgi_params;
        }
}

保存文件之后重启服务:

sudo /etc/init.d/nginx restart 
sudo /etc/init.d/php7.1-fpm restart

环境配置验证

用以下命令在 web 目录下创建 index.php:

sudo vim /usr/share/nginx/www/index.php

然后在 index.php 中加入下面的内容:

<?php
echo "<title>Test Page</title>";
echo "hello world";
?>

在浏览器中,访问 Ubuntu 云服务器公网 IP ,查看环境配置是否成功。如果页面可以显示“ hello world ”,说明配置成功。

对前端模块化的理解

平时写代码的时候,知道如果导出变量,如何引入变量。可见模块化就在我们的身边,可是为什么前端会引入模块化的概念,以及为什么有同步加载和异步加载呢?

为什么要模块化

在之前的项目中,如果没有模块化的概念,很多变量都有重名或者不小心重新赋值的危险。而且用 script 有可能阻塞 HTML 的下载或者渲染,影响用户体验。

在平时编码中,我们都习惯把一些通用的方法提出来放在一个文件里,哪个地方需要用到就引用,这样能够很好的梳理页面的逻辑,维护代码的成本也降低了不少。所以模块化给我们带来的好处是显而易见的。

  • 分离: 代码需要分离成小块,以便能为人所理解。
  • 可组合性: 在一个文件中编码,被许多其他文件重复使用。这提升了代码库的灵活性。
  • 解决全局变量重名问题
  • 提高复用性

现有的一些模块化方案有以下几种:

  • ES 6 模块
  • Commonjs
  • AMD
  • CMD

下面我就自身的理解对这几种方案做一个对比和总结:

ES6 Module

  • 编译时就能确定模块的依赖关系

ES6 模块遇到 import 命令时,不会去执行模块,而是生成一个引用,等用到的时候,才去模块中取值。因为是动态引用,所以不存在缓存的问题。可以看一下下面的例子:

// util.js
export let env = 'qa';
setTimeout(() => env = 'local', 1000);

// main.js
import {env} from './util';
console.log('env:', env);

setTimeout(() => console.log('new env:', env), 1500);

执行 main.js,会输出下面的结果:

// env: qa
// new env: local

可以看出 ES6 模块是动态的取值,不会缓存运行的结果。

目前浏览器尚未支持 ES6 模块 ,所以需要使用 babel 转换,大家可以在 Babel 提供的 REPL 在线编译器 中查看编译后的结果。

// es 6
import {add} from './config';

// es 5
'use strict';
var _config = require('./config');

可以看出,最后转换成 require 的方式了。ES6 模块在浏览器和服务器端都可以用,ES6 模块的设计**是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。

import 命令具有提升效果,会提升到整个模块的头部,首先执行,所以不能把 import 写在表达式里面。这和 ES 6 模块的概念不符合。

CommonJS

  • 用于服务器端
  • 只能在运行时确定
  • 同步加载
  • 模块加载的顺序,按照其在代码中出现的顺序。

node 的模块遵循 CommonJS 规范。在服务器端,依赖是保存在本地硬盘的,所以读取的速度非常快,使用同步加载不会有什么影响。

看一下 CommonJS 的语法:

// header.js
module.exports = {
    title: '我是柚子'
};

// main.js
var header = require('./header');

module

这里的 module 代表的是当前模块,它是一个对象,把它打印出来是下面的结果:

{
Module {
  id: '/Users/yanmeng/2017FE/css-animation/js/b.js',
  exports: { item: 'item' },
  parent:
   Module {
     id: '.',
     exports: {},
     parent: null,
     filename: '/Users/yanmeng/2017FE/css-animation/js/main.js',
     loaded: false,
     children: [ [Circular] ],
     paths:
      [ '/Users/yanmeng/2017FE/css-animation/js/node_modules',
        '/Users/yanmeng/2017FE/css-animation/node_modules',
        '/Users/yanmeng/2017FE/node_modules',
        '/Users/yanmeng/node_modules',
        '/Users/node_modules',
        '/node_modules' ] },
  filename: '/Users/yanmeng/2017FE/css-animation/js/b.js',
  loaded: false,
  children: [],
  paths:
   [ '/Users/yanmeng/2017FE/css-animation/js/node_modules',
     '/Users/yanmeng/2017FE/css-animation/node_modules',
     '/Users/yanmeng/2017FE/node_modules',
     '/Users/yanmeng/node_modules',
     '/Users/node_modules',
     '/node_modules' 
    ] 
}
  • id 是该模块的 id
  • loaded 代表改模块是否加载完毕
  • exports 是一个对象,里面有模块输出的各个接口。

之后调用这个模块的时候,就会从 exports 中取值,即使再执行,也不会再执行改模块,而是从缓存中取值,返回的是第一次运行的结果,除非手动清除缓存。

// 删除指定模块的缓存
delete require.cache[moduleName];

// 删除所有模块的缓存
Object.keys(require.cache).forEach(function(key) {
  delete require.cache[key];
})

缓存是根据绝对路径识别模块的,如果同一个模块放在不同的路径下,还是会重新加载这个模块。

require

require 命令第一次执行的时候,会加载并执行整个脚本,然后在内存中生成此脚本返回的 exports 对象。

ES6 模块与 CommonJS 模块的差异

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

ES6 模块是动态引用,并且不会缓存值。

ES6 模块在对脚本静态分析的时候,遇到 import 就会生成一个只读引用,等到脚本真正执行的时候,再根据这个只读引用,到被加载的那个模块里取值,所以说 ES6 模块是动态引用。
从依赖中引入的模块变量是一个地址引用,是只读的,可以为它新增属性,可是不能重新赋值。

// lib.js
export let obj = {};

// main.js
import { obj } from './lib';

obj.prop = 123; // OK
obj = {}; // TypeError

import 命令加载 CommonJS 模块

// fs.js
module.exports = {
    readfile: 'readfile'
}

// main.js
import {readfile} from 'fs';

上面的写法不对,因为 commonjs 是运行时加载的, es 6 模块是编译时加载的,所以在编译的时候,无法确认readfile 接口。

require 命令加载 ES6 模块

在用 require 命令加载 es6 模块的时候,ES6 模块的所有输出接口,会成为输入对象的属性。

// es.js
let foo = {bar:'my-default'};
export default foo;
foo = null;

// cjs.js
const es_namespace = require('./es');
console.log(es_namespace.default);
// {bar:'my-default'}

AMD

又称异步加载模块(Asynchronous Module Definition)

  • 依赖前置
  • 比较适合浏览器环境
  • 实现 js 文件的异步加载,避免网页失去响应
  • 管理模块之间的依赖性,便于代码的编写和维护
  • 代表库: RequireJS

如果在浏览器环境,就需要在服务端加载模块,那么采用同步加载的方法就会影响用户体验,所以浏览器端一般采用 AMD 规范。

它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。

// lib.js
  define('[./util.js]', function(util){
      function bar() {
          util.log('it is sunshine');
      };
      return {
          bar: bar
      };
  });
  
// main.js
require(['./lib.js'], function(lib){
   console.log(lib.bar());
})

CMD

  • 依赖就近
  • 需要用到依赖的时候才申明
  • 代表库: Sea.js

Sea.js 实现了这个规范,Sea.js 遇到依赖后只会去下载 JS 文件,并不会执行,而是等到所有被依赖的 JS 脚本都下载完以后,才从头开始执行主逻辑。因此被依赖模块的执行顺序和书写顺序完全一致。

define(function(require, exports, module) {
    var a = require('./a')
    a.doSomething()
    // ...
    var b = require('./b') 
    b.doSomething()
    // ...
})

本文只是浅显的介绍了一些模块的概念和用法,关于 ES6 模块、 CommonJs 的循环加载和 ES 6 模块和 CommonJs的互相引用,大家可以动手实践一下,会受益匪浅。

参考:

通过了解 Redux 简单源码,掌握 Redux 数据流原理

先祭上本文的思维导图:

一、为什么讲 Redux

在项目中用 Redux 的时候,有时候就觉得会用,但是不明白为什么这样用。导致在 debug 的时候,无法快速的 debug 出原因。而且 Redux 的源码也不复杂,暴露出来的只有 5 个 API,可以作为很好的阅读源码的开端,所以在这里很开心可以和大家一起来探索 Redux。如果有些讲的不准确的地方,欢迎大家提出来;也特别希望大家积极的讨论,迸发出更多想法。

二、Redux 为什么会出现

要了解 Redux,就要从 Flux 说起。可以认为 Redux 是 Flux **的一种实现。那 Redux 是为什么被提出来呢?就要提一下 MVC 了。

1、MVC

说到 Flux,我们就不得不要提一下 MVC 框架。

MVC 框架将应用分为 3 个部分:

  • View:视图,展示用户界面
  • Controller:管理应用的行为和数据,响应用户的输入(经常来自 View)和更新状态的指令(经常来自 Model)
  • Model:管理数据,大部分业务逻辑也在 Model 中

用户请求先到达 Controller,然后 Controller 调用 Model 获得数据,再把数据交给 View。这个想法是很理想的想法。在实际的框架应用中,大部分都是允许 View 和 Model 直接通信的。当项目变的越来越大的时候,这种不同模块之间的依赖关系就变得“不可预测”了,所以就变成了下面这样子。

虽然这张图有夸大的嫌疑,但是也说明了 MVC 在大型项目下,容易造成数据混乱的问题。

所以,Flux 诞生了。在写这篇文章之前,我查阅很多资料,有些说 Flux **替代了 MVC 框架,我则不这么认为。个人觉得,Flux **更严格的控制了 MVC 的数据流走向。下面咱们来看看 Flux 是如何严格控制数据流的。

2、Flux

一个 Flux 应用包含四个部分:

  • Dispatcher,处理动作分发,维持 Store 之间的依赖关系
  • Store,负责存储数据和处理数据相关逻辑
  • Action,触发 Dispatcher
  • View,视图,负责显示用户界面

通过上图可以看出来,Flux 的特点就是单向数据流

  • 用户在 View 层发起一个 Action 对象给 Dispatcher
  • Dispatcher 接收到 Action 并要求 Store 做相应的更新
  • Store 做出相对应更新,然后发出一个 change 事件
  • View 接收到 change 事件后,更新页面

所以在 Flux 体系下,如果想要驱动界面,只能派发一个 Store,别无他法。在这种规矩下,如果想要追溯一个应用的逻辑就变得很轻松了。而且这种**解决了 MVC 中无法杜绝 View 和 Model 之间的直接对话的问题。

这里就不具体讲关于 Flux 的例子了,如果想要更了解 Flux ,可以看一下阮一峰老师的 Flux 架构入门教程

4、Redux 诞生

Redux 是 Flux 的一种实现,意思就是除了“单向数据流”之外,Redux 还强调三个基本原则:

  • 唯一的 store(Single Source of Truth)
  • 保持状态只读(State is read-only)
  • 数据改变只能通过纯函数完成(Changes are made with pure functions)

a. 唯一的 store

在 Flux 中,应用可以拥有多个 Store,但是分成多个 Store 容易造成数据冗余,数据一致性不太好处理,而且 Store 之间可能还会有依赖,增加了应用的复杂度。所以 Redux 对这个问题的解决方法就是:整个应用只有一个 Store。

b. 保持状态只读

就是不能直接修改状态。如果想要修改状态,只能通过派发一个 Action 对象来完成。

c. 数据改变只能通过纯函数完成

这里说的纯函数就是 Reducer。按照 redux 作者 Dan 的说法:Redux = Reducer + Flux

三、在 React 中应用 Redux

下面咱们根据例子来了解一下 Reudx 在 React 中的应用。

1、Redux 中的数据流动

创建一个 Redux 应用需要下面几部分:

  • Actions
  • Reducers
  • Store

他们分别是什么意思呢?下面我们来举一个例子:
比如下面是商场某品牌鞋子的展示柜:

鞋子展示柜

店长来视察,发现鞋子2放的太高了,而且这款鞋还是店里的主推款,放在这个位置不适合宣传,就让店员把鞋子 2 往下挪两排,放下去之后,店长看着舒服多了。

鞋子展示柜

其实通过上面的例子,我们现在就很好解释 Redux 了:

  • View: 鞋子摆放在鞋架上的整体效果
  • Action: 店长给店员分配的任务(往下挪鞋子)
  • Reducers: 具体任务的实施者(把鞋子往下挪两排)
  • Store: 鞋子在鞋架上的具体位置

所以整个过程可以是下面这样:

Store 决定了 View,然后用户的交互产生了 ActionReducer 根据接收到的 Action 执行任务,从而改变 Store 中的 state,最后展示到 View 上。那么,Reducer 如何接收到动作(Action)信号的呢?伴随着这个问题,咱们来看一个例子。

2、Redux 实践

了解了 Redux 中各个部分代表的意思,下面咱们来通过一个计数器的例子进一步了解一下 Redux 的原理(具体代码可以看 GitHub)。我们想要的最终效果如下:

根据上面的思路,可以分别把 Action 和 Reducer 定义为:

  • 动作(Action): 加
  • 执行者(Reducer): 加 1

那么我们来创建 Action 和 Reducer 这两个文件:

Actions

首先我们创建一个 ActionTypes.jsActions.js 这两个文件。ActionType 代表的就是 Action 的类型,可以看到它是一个常量。在 Actions.js 中,我们定义了两个 Action 构造函数,他们返回的都是一个简单对象 (plain object),而且每个对象必须包含 type 属性。

可以看出来 Action 明确表达了我们想要做的事情(加和减)。

可能有些同学会问,在 Action 中,有时候也会 return 一个 function,不是简单对象。其实这个时候,是中间件拦截了 Action,如果是 function,就执行中间件中的方法。但是咱们这次不讲中间件,所以就先忽略这种情况。

Reducer

可以看到 Reducer 是一个纯函数。它接收两个参数 state 和 Action,根据接收到的 state 和 Action 来判断自己需要对当前的 state 做哪些操作,并且返回新的 state。

在 Reducer 中我们给了 state 一个默认的值,这就是我们的初始 state。关于 Redux 是如何返回初始值的,继续往下看。

Action 和 Reducer 都有了,那怎么让他们两个联系起来呢?下面咱们看一下 Redux 中的精华部分 - Store

createStore

首先我们先创建 Store:

store.js 中,我们把 reducer 传给 createStore 方法并且执行了它,来创建 Store。这个方法是 Redux 的精髓所在。

下面看一下 createStore 的源码部分:

createStore 接收三个参数:

  • reducer{Function}
  • state{any}(可选参数)
  • enhancer{Function}(可选参数)

返回一个对象,这个对象包含五个方法,咱们目前先只关注前三个方法:

  • dispatch
  • subscribe
  • getState

在整个 createStore 中,只执行了 dispatch({ type: ActionTypes.INIT }) 这一句代码。那 dispatch 做了什么呢?

我省略了一些代码,这是 dispatch 方法的核心代码。它接收一个 action 对象,并且把 createStore 接收到的 state 参数和通过 dispatch 方法传进来的 Action 参数,传给了 Reducer 并且执行,然后把 reducer 返回的 state 赋值给 currentState。最后执行订阅队列中的方法。

createStore 方法一上来就执行了 dispatch({ type: ActionTypes.INIT })。这句话的意思咱们现在也清楚了,它的主要目的就是初始化 state。

现在咱们已经把 Action 和 Reducer 联系起来了。可以看到,在 createStore 方法中,它维护一个变量 currentState,通过 dispatch 方法来更新 currentState 变量。外部如果想要获取 currentState,只需要调用 createStore 暴露出来的 getState 方法即可:

getState 方法是获取当前的 currentState 变量,如果想要实时获取 state,那就需要注册监听事件,每次 dispatch 的时候,就都会执行一遍这个事件。

现在咱们来梳理一下思路:

  • Action:此次动作的目的
  • Reducer:根据接收到的 Action 命令来做具体的操作
  • Store:把 Action 传给 Reducer,并且更新 state。然后执行订阅队列中的方法。

Redux 和 React 是两个独立的产品,但是如果两个结合使用,就不得不提 react-redux 这个库了,可以大大的简化代码的书写,但是咱们先不讲这个库,来自己实现一下。

2、store 和 context 结合

大家都知道,在 React 中我们都是使用 props 来传递数据的。整个 React 应用就是一个组件树,一层一层的往下传递数据。

但是如果在一个多层嵌套的组件结构中,只有最里层的组件才需要使用这个数据,导致中间的组件都需要帮忙传递这个数据,我们就要写很多次 props,这样就很麻烦。

好在 React 提供了一个叫做 context 的功能,可以很好的解决和这个问题。

所谓 context 就是“上下文环境”,让一个树状组件上所有组件都能访问一个共同的对象,为了完成这个任务,需要上下级组件的配合。

首先是上级组件宣称自己支持 context,并且提供给一个函数来返回代表 context 的对象。

然后,子组件只要宣称自己需要这个 context,就可以通过 this.context 来访问这个共同的对象。

所以我们可以利用 React 的 context,把 Store 挂在它上面,就可以实现全局共享 Store 了。

了解了如何在 React **享 Store,那咱们就动手来实现一下吧~

Provider

Provider,顾名思义,它是提供者,在这个例子中,它是 context 的提供者。

就像下面这样来使用:

Provider 提供了一个函数 getChildContext,这个函数返回的是就是代表 context 的对象。在调用 Store 的时候可以从 context 中获取:this.context.store

Provider 为了声明自己是 context 的提供者,还需要指定 ProviderchildContextTypes 属性(需要和 getChildContext 对其)。

只有具备上面两个特点,Provider 才有可能访问到 context。

好了,Provider 组件咱们已经完成了,下面咱们就可以把 context 挂到整个应用的顶层组件上了。

进入整个应用的入口文件 index.js

我们把 Store 作为 props 传递给了 Provider 组件,Provider 组件把 Store 挂在了 context 上。所以下面我们就要从 context 中来获取 Store 了。

消费者

下面是我们整个计数器应用的骨架部分。

我们先把页面渲染出来:

在上面的组件中,我们做了两件事情:

  • 第一件事情是:声称自己需要 context
  • 第二件事情是:初始化 state。

如何声称自己需要 context 呢?

  • 首先是需要给 App 组件的 contextType 赋值,值的类型和 Provider 中提供的 context 的类型一样。
  • 然后在构造函数中加上 context,这样组件的其他部分就可以通过 this.context 来调用 context 了。
  • 然后是初始化 state。看代码可以知道,我们调用了挂在 context 上的 Store 的 getState 方法。

上面我们了解过,getState 方法返回的就是 createStore 方法中维护的那个变量。在 createStore 执行的时候,就已经初始化过了这个变量。

接下来我们给“加号”加上具体动作。

我们想要把数字加一,所以就有一个“加”的动作,这个动作就是一个 Action,这个 Action 就是 addAction。如果想要触发这个动作,就需要执行 dispatch 方法。

通过 dispatch 方法,把 Action 对象传给了 Reducer,经过处理,Reducer 会返回一个加 1 的新 state。

其实现在 Store 中的数据已经是最新的了,可以我们看到页面上还没有更新。那我们如何能获取到最新的 state 呢?

订阅

就像关注公众号一样,我只需要在最开始的时候订阅一下,之后每次有更新,我都会收到推送。

这个时候就要使用 Store 的 subscribe 方法了。顾名思义,就是我要订阅 state 的变化。我们先看一下代码怎么写:

在组件的 componentDidMount 生命周期中,我们调用了 store 的 subscribe 方法,每次 state 更新的时候,都会去调用 onChange 方法;在 onChange 方法中,我们会取得最新的 state,并且赋值。在组件被卸载的时候,我们取消订阅。

上面这样就完成了订阅功能。这时候再运行程序,可以发现页面上就会显示最新的数字了。

react-redux

在这个例子中,可以看出来我们可以抽象出来很多逻辑,比如 Provider,还有订阅 store 变化的功能。其实这些 react-redux 都已经帮我们做好了。

  • Provider: 提供包含 store 的 context
  • connect: 把 state 转化为内层组件的 props,监听 state 的变化,组件性能优化

在咱们这个例子中,只是简单的实现了一下 react-redux 部分功能。具体的大家可以到官网上去看。

总结

下面咱们来总结一下 redux 和 react 结合使用的整个数据流:

good~ 我们已经全部完成了整个应用。现在大家了解 Redux 的运行原理 了吗?

具体代码可以到 GitHub 查看。

参考资料:

本文永久链接

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.