yanyixin / yanyixin.github.io Goto Github PK
View Code? Open in Web Editor NEW我的博客项目
Home Page: https://yanyixin.github.io
我的博客项目
Home Page: https://yanyixin.github.io
上一篇讲到了在 Android 环境下一个 React Native 应用是如何启动的。这次咱们来探索一下 Ios 环境。
和上文的思路一样,咱们可以按照以下三个步骤来:
用 Xcode 打开项目目录下的 ios 文件夹,在和项目名称相对应的文件夹的下面找到 main.m 文件。此文件就是整个 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 是如何处理的。
打开 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 是一个 UIView 容器,就是用户所能看到的所有视图都源于 RCTRootView,它也将所有的 React Natvie 视图封装在了原生组件中。
initWithBundleURL
当应用开始运行的时候,RCTRootView 将会从 initWithBundleURL 的参数中加载应用:(本地调试的时候是直接在本地服务器中的 index.ios 加载,发布时设置有所不同)
moduleName
在 index.ios.js 中注册的应用的名字
initialProperties
必须是NSDictionary的一个实例。这一字典参数会在内部被转化为一个可供 JS 组件调用的 JSON 对象。
所以在本地调试的时候,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 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
的刷新功能是在当前页面刷新,而不是返回到启动页面。
红屏或黄屏提示都只会在开发版本中显示,正式的离线包中是不会显示的。
应用内的报错会以全屏红色显示在应用中(调试模式下),我们称为红屏(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>
);
}
}
应用内的警告会以全屏黄色显示在应用中(调试模式下),我们称为黄屏(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
如果是 Android 应用,无论是运行在模拟器或是真机上,都可以通过在终端命令行里运行 adb logcat *:S ReactNative:V ReactNativeJS:V
命令来查看。
在开发者菜单中选择 Debug JS Remotely
选项,即可以开始在 Chrome 中调试 JavaScript 代码。
打开 Chrome 的开发者工具即可看到打印出来的日志。
在 Chrome 开发者工具中我们也可以调试代码,给代码打断点,相信大家都知道相关的操作,这里就不再详细的讲了。
如果在 Android 5.0+ 的设备上调试,将设备通过 USB 连接到电脑上后,可以使用 adb 命令行工具来设定从设备到电脑的端口转发:
adb reverse tcp:8081 tcp:8081 // 第一个是主机的端口号,第二个是真机/模拟器的端口号
如果设备 Android 版本在 5.0 以下,则可以在开发者菜单中选择 Dev Settings - Debug server host for device
,然后在其中填入电脑的 IP地址:端口
。
然后就可以和上面的示例一样来调试啦。
该功能主要是对 UI 和 JavaScript 的 FPS 的监控。
FPS 是测量用于保存、显示动态视频的信息数量。 通俗来讲就是指动画或视频的画面数。 例如在电影视频及数字视频上,每一帧都是静止的图象;快速连续地显示帧便形成了运动的假象。 每秒钟帧数(FPS) 愈多,所显示的动作就会愈流畅。
打开之后就能看到性能监控的数据了。具体关于 React Native 性能的分析可以看官网的介绍,现在本文暂不做详细的讲解,之后可能会专门开一个关于性能的文章。
需要 React Native 0.43 或者更高的版本
我们平时在开发页面的时候,都用过查找元素的功能,模拟器自带的调试功能个人感觉不是很友好,但是官方提供了 react-devtools 可以很好的来查找 Dom 元素。
如果要使用它,需要在全局安装:
npm install -g react-devtools
在项目文件的目录下运行启动命令:
react-devtools
这时会弹出 react-devtools 窗口,里面是我们页面的元素。 react-devtools 的优点是可以搭配模拟器的 Toggle Inspector 功能一起使用,效果棒棒哒,哈哈。
首先我们先开启模拟器的 Toggle Inspector 功能。
下面是使用方法:
打开调试功能之后我们能看到,调试功能的界面有很多 Tab,下面来解释一下各个 Tab 的功能和作用。
顾名思义,这个 Tab 是用来查找元素的,点击想要检查的元素,就可以显示出来元素的位置、样式和层级关系等。
Perf 显示的是应用的性能监控界面。
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 标签集合
使用 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.fullName
和 this.amount
来读取。它的值可以通过 this.fullName.value
和 this.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
获取。
通过受控组件,父组件获取子组件的值十分简单 —— 父组件中已经有这个值了(译者注:在父组件中定义)!它被传递给子组件。同时 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` ,那么接下来的两部分会帮到你。
不像 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行)。
.
方法来获取这个集合的值(下面的第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;
和 radio 标签集合不一样, Checkbox 标签集合可能有多个值。导致获取这些值会比获取 radio 标签集合的值难一些。
可以通过下面的五步来检索出 checkbox 标签集合被选中的值:
1、在 form
标签上设置 ref (下面的第27行)。
2、从 form 中取出这个checkbox 集合。然后它应该是 pet
集合(第9行)。
checkboxArray
)。Array.filter()
获取选中的 checkbox (第 15 行的 checkedCheckboxes
)。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
的场景,大家走过路过不要错过啊~
首先咱们先来看一下《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
,可以看一下 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
的使用和体会还是要在平时运用中理解,先了解其原理,那么在使用的时候就如鱼得水啦。
当拿到一个项目的时候,我们要知道项目是如何运行起来的,这样方便我们更能理解项目的原理,也能帮助我们以后排错和优化。
本文的大致思路可以分为三步:
根据以上三步,就可以知道整个项目是如何运行的了。下面我们一起来寻找吧~
用 Android Stutio 打开项目根目录下的 Android 文件夹,我们能看到下面的文件分布:
打开 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>
我们看到在这个文件中主要有以下节点:
下面就各个节点来做一个介绍:
AndroidManifest.xml
文件里主要是一个 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 等等。
通过字面意思我们可以大致的认为它是需要和用户进行操作的。其实就是,app 安装的时候需要向用户索要的一些权限,用户可以同意也可以拒绝。
android:name
uses-sdk
是用来设置 app
对 android
系统的兼容性的。我们用了以下两个配置项,配置项的值是一个代表 Android API Level
参数的整数。每个 API Level
对应着一个 Android 系统的版本。下图为官方给出的 API Level
和 Android 系统版本的对照表。
android:minSdkVersion
一个用于指定应用运行所需最低 API 级别的整数。 如果系统的 API 级别低于该属性中指定的值,Android 系统将阻止用户安装应用。
android:targetSdkVersion
一个用于指定应用的目标 API
级别的整数。如果未设置,其默认值与为 minSdkVersion
指定的值相等。一般情况下应该将这个属性的值设置为最新的 API level
值,这样我们才可以利用新版本系统上的新特性。
每个 Android 应用程序启动的时候都会初始化一个 Application 类。我们可以通过它来定义一些全局的和一些上下文都要用到的变量和方法。
Application 的生命周期是整个应用中最长的,可以说它的生命周期就是整个应用的生命周期。
android:name
一般情况下系统会自动创建一个 Application 类。如果我们需要自定义一个 Application 类的话,需要创建一个类并且继承 Application。然后在 name 属性中写上自定义的 Application 类名即可。
android:allowBackup
Android API Level 8
及其以上 Android 系统提供了为应用程序数据的备份和恢复功能。此属性就是控制该功能的开关。系统默认为 true。当 allowBackup
标志为 true
时,用户可通过 adb backup
和 adb restore
来进行对应用数据的备份和恢复。
android:label
显示给用户的 APP 的名称
android:icon
APP 的图标所在的路径。
android:theme
它给所有的 Activity 定义了一个默认的主题风格,也可以在自己的 theme 里设置。
每个 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
会在应用启动的时候第一个被执行。
action
android:name
值为 android.intent.action.MAIN
,表明此 Activity 是作为应用程序的入口。
category
android:name
值为 android.intent.category.LAUNCHER
,表明应用程序是否显示在程序列表里。
通过上面的一些介绍,我们可以知道,有 intent-filter
节点,并且它的 action
的 android:name
为 android.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.js
,index.android.js
文件的路径是相对于 package.json
的路径。addPackage
是自定义 React Native 的 Android 组件。setUseDeveloperSupport
表示是否启用开发者模式。这里写 BuildConfig.DEBUG
就可以自动根据 gradle 构建的类型(debug或release)来决定。mReactRootView.startReactApplication
方法是启动整个 React Native 程序。setContentView
方法是将 mReactRootView
作为子布局加载到 Activity 中。AppRegistry
是JS运行所有React Native应用的入口。通过 AppRegistry.registerComponent
来注册项目的根组件,然后原生系统才可以加载应用的代码包并且在启动完成之后通过调用 AppRegistry.runApplication
来真正运行应用。
import { AppRegistry } from 'react-native'
import MyApp from './js/app'
AppRegistry.registerComponent('MyApp', () => MyApp)
test
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
属性。如图:
拿上面的例子来说,Person.prototype.constructor又指向了Person。
####proto、prototype和constructor
每一个生成的对象都有一个__proto__
属性,当调用构造函数创建一个新的实例之后,__proto__
就指向了构造函数的原型对象,即构造函数的prototype
属性。
下图所示:当定义person1是一个数组时,person1自带一个__proto__
属性。
下图所示:当定义Person构造函数时,Person自带一个prototype
属性。prototype
属性自带一个constructor
属性,至于其他的属性,都是从Object继承而来的。
下图所示:当new
一个实例对象person1时,_proto_
就指向了构造函数Person的原型对象,即构造函数的prototype
属性。可以对比一个person1的_proto_
属性在实例化之前和之后的变化。
下图是展示了Person构造函数、Person的原型属性以及Person现有的两个实例之间的关系。
##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__属性指向的原型对象中有没有,如果没有的话,就去查它的原型的原型中有没有,一直到原型链的最顶端为止。
前段时间被问到这个问题,就 google 里一下,发现这篇文章讲到了重点,就翻译出来和大家一起分享。
原文链接:Where to Fetch Data: componentWillMount vs componentDidMount
校对:Cherry
当你需要为 React 组件获取一些数据的时候,你会从什么地方获取呢?
这个问题一直都存在。
有两个常见的获取数据的地方:
我们应该都很清楚,render
方法肯定不是一个获取数据或者做任何有关异步操作的好地方,它会以某种方式改变 state,也可能会引起副作用。
让我们来看看这两种常见的选项以及他们的优缺点。
这个方法在组件第一次渲染之前被调用,所以猛一看会觉得这是写数据请求的最好地方。
但是这里有一个“陷阱”:在组件渲染之前获取数据的异步调用不会返回出来数据。就是说组件将至少一次会伴随着空数据被渲染。
这是没有办法为了等待数据返回而去“暂停”渲染。你不能在 setTimeout
方法里从 componentWillMount
中返回一个 promise。 处理这个问题的正确方式是设置组件的初始 state 来确保组件的有效渲染。
在 ES6 风格的 class
组件中,它的构造函数和 componentWillMount
的功能是一样的,所以,如果你已经有一个构造函数方法,那就可以直接把代码写在这里。
等到 componentDidMount
被调用的时候,组件已经被渲染一次了。
事实证明,componentDidMount
是发送数据请求的最好的位置,有以下两个原因:
1、使用 DidMount 能够清晰的看出,直到初始渲染之后,数据才会被加载。这就提醒你要适当的去设置初始的 state
,就不会因为 state 为 undefined 而导致错误
2、如果你需要在服务端(SSR/同构/其他的流行用语)渲染应用,那么 componentWillMount
将会被调用两次 —— 一次在服务端,一次在客户端,当然,这应该是你不愿意看到的。那么把数据加载的代码放在 componentDidMount
中能确保只会从客户端获取数据。
我希望在哪加载数据的这个问题已经讲清楚了。如果你仍然不确定调用 AJAX 请求和加载数据的最好方法是什么,请阅读我的这篇文章 在 React 中发送 AJAX 请求
这里建议大家看英文版的安装文档,因为有一些步骤中文版的和英文版的不一样。下面以 react-native 官方提供的例子来分别说一下 android 和 ios 的环境配置过程。
无论是 android 还是 ios 都需要安装下面的软件。
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)
Watchman 是由 Facebook 提供的监视文件系统变更的工具。安装此工具可以提高开发时的性能(packager 可以快速捕捉文件的变化从而实现实时刷新)。
上一步我们已经安装了 Homebrew
,那 Node 和 Watchman 就可以用它来安装了。具体命令如下:
brew install node
brew install watchman
注意:请确保你的 node 版本号在 4 及其以上版本。
在 bash 中执行下面的命令:
npm install -g react-native-cli
在 bash 中执行下面的命令:
react-native init AwesomeProject
cd AwesomeProject
其实 ios
的环境安装非常之简单,只要安装 Xcode IDE
就可以了。ios
大法好啊。可以通过 App Store 或是到 Apple 开发者官网上下载。这一步骤会同时安装 Xcode IDE
和 Xcode
的命令行工具。需要注意的是,React Native
目前需要 Xcode 8.0
或更高版本。
安装好之后,打开 Preferences
,点击 Locations
,检查一下版本号是否正确。
ok 了之后,就可以进行第四步。
确保你的设备已经打开了 USB 调试。
在 bash 中执行下面的命令来检查手机是否已经和电脑连接:
adb devices
如果连接成功了,输入上面的那条命令之后,就会出现下面的信息:
注意:在连接真机的时候需要把虚拟机关闭,确保只连接一个设备
如果连接成功了,可以进行第四步。
安装软件 Android Studio
Android Studio
需要 Java SE Development Kit (JDK)
的支持。你可以在命令行中输入 javac -version
来查看你当前安装的 JDK
版本。如果版本不符合要求,可以到 官网上下载。
java
环境配置好之后,接下来就是要从 官网 下载 Android Studio
。
注意: 在安装过程中,在 Install Type
这一步的时候最好选择 Custom。因为可以自定义一些设置和安装的组件。
选择好之后,点击 next,进入下一步,这时记得勾选下图中用红圈圈标出的两项。箭头指向的路径就是组件安装的路径。
这两项选择好之后,一路 next 就可以啦。
安装好 Android Studio
之后,还要下载需要的 sdk
。如下图,点击 Configure
下的 SDK Manager
,进入 sdk
配置页面。
在窗口的右下角勾选 show-package-details
。然后在 Android 6.0 (Marshmallow)
下勾选以下这些选项:
选中之后,点击 Apply
,就可以开始下载了。
下一步是进入 SDK Tools
窗口。同样,在窗口的右下角勾选 show-package-details
。找到 Android SDK Build-Tools
并展开它,选择 23.0.1
,点击 Apply,就可以开始下载了。
到目前为止,需要的 sdk
已经下载好了,下面咱们就开始安装虚拟机了。虚拟机目前有两种方式,一种是在 Android Studio
中安装它自带的虚拟机;一种是使用 genymotion
虚拟机。在这里推荐第二种,简单快捷方便,所以下面主要介绍一下 genymotion
的下载和安装~
genymotion
的下载地址:https://www.genymotion.com/download/ 。 这里需要登陆一下,才能进入到下载的页面。记得把自己的账号记住,下面还会用到。
genymotion
依赖 virtualbox
,virtualbox
的下载地址在这里: https://www.virtualbox.org/wiki/Downloads
先安装 virtualbox
,然后再安装 genymotion
。下载下来的会有两个包,记得都要安装一下。
打开 genymotion
的时候会让登陆一下,就是你刚刚记住的账号。然后就可以安装虚拟器了。如下图,点击 Add
,可以选择一个手机类型和安卓系统的版本进行安装。
下载的过程还是挺慢的,需要耐心等待。下载好之后就能看到有一个手机模拟器出现在你本地的列表中啦。
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
。否则的话就是安装的时候你自己定义的位置。编辑好之后,记得保存。
保存之后,在控制台执行下面的命令,让配置生效。
source ~/.bash_profile
检查配置是否生效,可以在控制台执行下面的命令。
echo $ANDROID_HOME
在第二步中我们已经安装好 react-native 和项目啦,现在让我们把项目运行起来~
在 ios 端运行的话,在项目所在的目录下执行下面的命令就可以了:
react-native run-ios
在 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.
如果遇到这个问题,stackoverflow
上的答案在这里。
2、adb: command not found
adb
全名 Andorid Debug Bridge
。这是一个 Debug 工具,是要连接开发电脑和你的调试手机的。
如果出现这个错误,可能就是没有配置 Android 的环境变量,或者配置了环境变量,但是没有让它生效。关于这两项的配置,可以参考上面的 ANDROID_HOME 变量配置 这一部分。
3、No connected devices
这个看英文也知道是什么意思啦,就是没有连接中的设备。如果用的是模拟机的话,就要打开模拟机;如果是真机的话,记得要连接上手机。
可以运行 adb devices
来检查连接中的设备。
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.
在 ios 运行的时候如果遇到这种情况,就把项目目录下 ios 文件夹中的 build 文件删除,然后再运行就可以了。
Where to Fetch Data: componentWillMount vs componentDidMount
采用 Angular 的 commit 规范。不写
commit message
不允许提交代码。
分为三部分:header
、body
和 footer
。
header
是必须的,body
和 footer
可以省略。
<type>(<scope>): <subject>
// 空一行
<body>
// 空一行
<footer>
<type>(<scope>): <subject>
header 包含三个字段: type(必须)、scope(可选)和 subject(必须)
用于说明本次提交的类型,包含下面几种情况:
用于说明 commit 影响的范围,比如数据层、控制层、视图层等等,视项目不同而不同。
修改文件的范围(包括但不限于 doc, middleware, core, config, plugin)
此次 commit 的简短描述。建议不超过 50 个字符。
常用表述语:
注意: 使用第一人称现在时,比如使用 change 而不是 changed 或 changes。
针对本次 commit 的详细描述,可以多行。需要描述的信息包括:
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,则可以在 footer 中关闭。例如:
Closes #234
或者关闭多个 issue:
Closes #123, #245, #992
如果 commit 用于撤销以前的 commit,则必须以 revert:
开头,后面跟着被撤销 Commit 的 Header。
revert: feat(pencil): add 'graphiteWidth' option
This reverts commit 667ecc1654a317a13331b17617d973392f415f02.
body 的格式是固定的:This reverts commit <hash>
。hash
是被撤销的 commit 的 SHA 标识符。
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
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.
feature_20170401_fairy_flower
feature
、 bugfix
、refactor
三种类型,即新功能开发
、bug 修复
和代码重构
snake case
命名法,即下划线命名。全局安装 commitizen
npm i -g commitizen
然后,在项目目录里,运行下面的命令,使其支持 Angular 的 Commit message 格式。
commitizen init cz-conventional-changelog --save --save-exact
以后凡是用到 git commit 的地方都用 git cz
。然后就会出现生成规范的 git message
的选项:
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"
来提交,这样可以强制自己记住提交规范,不用依赖外部帮助,便于自己养成良好的习惯。
如果你的提交信息都符合 Angular 的规范,那么这些信息就会出现在 change log
里面。
只有 type
为 feat
和 fix
的 commit
会出现在 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
文件就会出现在你的项目根目录下面。
自动运行在严格模式下并且没有办法退出运行的 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
发布了4.0.0
版本,少年们,赶快学起来吧,这篇文章带领大家搭建一个简单的Angular
应用,会尽量详细的把每个点都解释到。
首先我选择了用webpack2
来作为打包工具,选择wenpack2
的理由不言而喻。这里假设你已经了解webpack2
的一些原理,下面开始来学习吧~
详细代码可以看我的git项目
首先新建一个项目文件夹
mkdir angular-dream
cd angular-dream
在控制台中输入命令 npm init
,创建 package.json
文件。如图:
在控制台中可以一路回车。当然,这里我命名了项目的名称为 angular-dream
,还有一些其他的信息。
创建好之后用编辑器(我使用的是webstorm)打开这个项目。
关于 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
文件。
浏览器不能直接执行 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
文件
module.exports = require('./config/webpack.dev.js');
现在在控制台中执行 npm install
命令,安装项目的依赖。
配置好上述的几个文件之后呢,我们在项目中的根目录下创建一个 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');
}
在 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';
在 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
文件。
在这里请注意入口 polyfills
,vendor
和 app
的先后顺序。
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;
在 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标签
})
]
};
在 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
文件会被标红。
基本的配置已经完成啦,现在我们来创建根模块~
在 src
文件下面新建一个 app
文件夹,
在app
文件夹下面新建 app.component.ts
文件
import { Component } from "@angular/core";
@Component({
selector : 'root-app',
templateUrl: './app.component.html'
})
export class AppComponent {
constructor() {}
}
在 app
文件夹下面新建 app.component.html
文件
<h1 class="title">Hello Angular2</h1>
<router-outlet></router-outlet>
这里我们用一下路由来完成页面之间的跳转
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
文件夹下面新建 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
之旅吧~
参考:
我的服务器信息是: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,可以先更新一下源列表。
sudo apt-get update
sudo apt-get install nginx
sudo /etc/init.d/nginx start
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 命令的知识,可以查看此文章
访问 Ubuntu 云服务器公网 IP。
若服务正常,显示结果如下。
sudo apt-get update
sudo apt-get install mysql-server
安装过程中将会让你设置密码
sudo mysql_secure_installation
安装完成后,输入命令:netstat -anp ,会发现 3306 端口正在被监听,此时已可以编写 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
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
sudo /etc/init.d/php7.1-fpm start
sudo netstat -tunpl | grep php-fpm
以上结果表明 PHP-FPM 默认配置的监听端口为 9000,只需修改配置,将 PHP 解析的请求转发到 127.0.0.0:9000
处理即可。
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
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 的下载或者渲染,影响用户体验。
在平时编码中,我们都习惯把一些通用的方法提出来放在一个文件里,哪个地方需要用到就引用,这样能够很好的梳理页面的逻辑,维护代码的成本也降低了不少。所以模块化给我们带来的好处是显而易见的。
现有的一些模块化方案有以下几种:
下面我就自身的理解对这几种方案做一个对比和总结:
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
模块的概念不符合。
node
的模块遵循 CommonJS
规范。在服务器端,依赖是保存在本地硬盘的,所以读取的速度非常快,使用同步加载不会有什么影响。
看一下 CommonJS
的语法:
// header.js
module.exports = {
title: '我是柚子'
};
// main.js
var header = require('./header');
这里的 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'
]
}
之后调用这个模块的时候,就会从 exports 中取值,即使再执行,也不会再执行改模块,而是从缓存中取值,返回的是第一次运行的结果,除非手动清除缓存。
// 删除指定模块的缓存
delete require.cache[moduleName];
// 删除所有模块的缓存
Object.keys(require.cache).forEach(function(key) {
delete require.cache[key];
})
缓存是根据绝对路径识别模块的,如果同一个模块放在不同的路径下,还是会重新加载这个模块。
require 命令第一次执行的时候,会加载并执行整个脚本,然后在内存中生成此脚本返回的 exports 对象。
ES6
模块是动态引用,并且不会缓存值。
ES6
模块在对脚本静态分析的时候,遇到 import
就会生成一个只读引用,等到脚本真正执行的时候,再根据这个只读引用,到被加载的那个模块里取值,所以说 ES6
模块是动态引用。
从依赖中引入的模块变量是一个地址引用,是只读的,可以为它新增属性,可是不能重新赋值。
// lib.js
export let obj = {};
// main.js
import { obj } from './lib';
obj.prop = 123; // OK
obj = {}; // TypeError
// fs.js
module.exports = {
readfile: 'readfile'
}
// main.js
import {readfile} from 'fs';
上面的写法不对,因为 commonjs 是运行时加载的, es 6 模块是编译时加载的,所以在编译的时候,无法确认readfile 接口。
在用 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'}
又称异步加载模块(Asynchronous Module Definition)
js
文件的异步加载,避免网页失去响应如果在浏览器环境,就需要在服务端加载模块,那么采用同步加载的方法就会影响用户体验,所以浏览器端一般采用 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());
})
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 的时候,有时候就觉得会用,但是不明白为什么这样用。导致在 debug 的时候,无法快速的 debug 出原因。而且 Redux 的源码也不复杂,暴露出来的只有 5 个 API,可以作为很好的阅读源码的开端,所以在这里很开心可以和大家一起来探索 Redux。如果有些讲的不准确的地方,欢迎大家提出来;也特别希望大家积极的讨论,迸发出更多想法。
要了解 Redux,就要从 Flux 说起。可以认为 Redux 是 Flux **的一种实现。那 Redux 是为什么被提出来呢?就要提一下 MVC 了。
说到 Flux,我们就不得不要提一下 MVC 框架。
MVC 框架将应用分为 3 个部分:
用户请求先到达 Controller,然后 Controller 调用 Model 获得数据,再把数据交给 View。这个想法是很理想的想法。在实际的框架应用中,大部分都是允许 View 和 Model 直接通信的。当项目变的越来越大的时候,这种不同模块之间的依赖关系就变得“不可预测”了,所以就变成了下面这样子。
虽然这张图有夸大的嫌疑,但是也说明了 MVC 在大型项目下,容易造成数据混乱的问题。
所以,Flux 诞生了。在写这篇文章之前,我查阅很多资料,有些说 Flux **替代了 MVC 框架,我则不这么认为。个人觉得,Flux **更严格的控制了 MVC 的数据流走向。下面咱们来看看 Flux 是如何严格控制数据流的。
一个 Flux 应用包含四个部分:
通过上图可以看出来,Flux 的特点就是单向数据流:
所以在 Flux 体系下,如果想要驱动界面,只能派发一个 Store,别无他法。在这种规矩下,如果想要追溯一个应用的逻辑就变得很轻松了。而且这种**解决了 MVC 中无法杜绝 View 和 Model 之间的直接对话的问题。
这里就不具体讲关于 Flux 的例子了,如果想要更了解 Flux ,可以看一下阮一峰老师的 Flux 架构入门教程。
Redux 是 Flux 的一种实现,意思就是除了“单向数据流”之外,Redux 还强调三个基本原则:
在 Flux 中,应用可以拥有多个 Store,但是分成多个 Store 容易造成数据冗余,数据一致性不太好处理,而且 Store 之间可能还会有依赖,增加了应用的复杂度。所以 Redux 对这个问题的解决方法就是:整个应用只有一个 Store。
就是不能直接修改状态。如果想要修改状态,只能通过派发一个 Action 对象来完成。
这里说的纯函数就是 Reducer。按照 redux 作者 Dan 的说法:Redux = Reducer + Flux
。
下面咱们根据例子来了解一下 Reudx 在 React 中的应用。
创建一个 Redux 应用需要下面几部分:
他们分别是什么意思呢?下面我们来举一个例子:
比如下面是商场某品牌鞋子的展示柜:
店长来视察,发现鞋子2
放的太高了,而且这款鞋还是店里的主推款,放在这个位置不适合宣传,就让店员把鞋子 2 往下挪两排
,放下去之后,店长看着舒服多了。
其实通过上面的例子,我们现在就很好解释 Redux 了:
所以整个过程可以是下面这样:
Store
决定了 View
,然后用户的交互产生了 Action
,Reducer
根据接收到的 Action
执行任务,从而改变 Store
中的 state
,最后展示到 View
上。那么,Reducer
如何接收到动作(Action
)信号的呢?伴随着这个问题,咱们来看一个例子。
了解了 Redux 中各个部分代表的意思,下面咱们来通过一个计数器的例子进一步了解一下 Redux 的原理(具体代码可以看 GitHub)。我们想要的最终效果如下:
根据上面的思路,可以分别把 Action 和 Reducer 定义为:
那么我们来创建 Action 和 Reducer 这两个文件:
首先我们创建一个 ActionTypes.js
和 Actions.js
这两个文件。ActionType 代表的就是 Action 的类型,可以看到它是一个常量。在 Actions.js
中,我们定义了两个 Action 构造函数,他们返回的都是一个简单对象 (plain object
),而且每个对象必须包含 type 属性。
可以看出来 Action 明确表达了我们想要做的事情(加和减)。
可能有些同学会问,在 Action 中,有时候也会 return 一个 function,不是简单对象。其实这个时候,是中间件拦截了 Action,如果是 function,就执行中间件中的方法。但是咱们这次不讲中间件,所以就先忽略这种情况。
可以看到 Reducer 是一个纯函数。它接收两个参数 state 和 Action,根据接收到的 state 和 Action 来判断自己需要对当前的 state 做哪些操作,并且返回新的 state。
在 Reducer 中我们给了 state 一个默认的值,这就是我们的初始 state。关于 Redux 是如何返回初始值的,继续往下看。
Action 和 Reducer 都有了,那怎么让他们两个联系起来呢?下面咱们看一下 Redux 中的精华部分 - Store
。
首先我们先创建 Store:
在 store.js
中,我们把 reducer 传给 createStore
方法并且执行了它,来创建 Store。这个方法是 Redux 的精髓所在。
下面看一下 createStore
的源码部分:
createStore 接收三个参数:
reducer{Function}
state{any}
(可选参数)enhancer{Function}
(可选参数)返回一个对象,这个对象包含五个方法,咱们目前先只关注前三个方法:
在整个 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
这个库了,可以大大的简化代码的书写,但是咱们先不讲这个库,来自己实现一下。
大家都知道,在 React 中我们都是使用 props 来传递数据的。整个 React 应用就是一个组件树,一层一层的往下传递数据。
但是如果在一个多层嵌套的组件结构中,只有最里层的组件才需要使用这个数据,导致中间的组件都需要帮忙传递这个数据,我们就要写很多次 props,这样就很麻烦。
好在 React 提供了一个叫做 context
的功能,可以很好的解决和这个问题。
所谓 context 就是“上下文环境”,让一个树状组件上所有组件都能访问一个共同的对象,为了完成这个任务,需要上下级组件的配合。
首先是上级组件宣称自己支持 context,并且提供给一个函数来返回代表 context 的对象。
然后,子组件只要宣称自己需要这个 context,就可以通过 this.context
来访问这个共同的对象。
所以我们可以利用 React 的 context,把 Store 挂在它上面,就可以实现全局共享 Store 了。
了解了如何在 React **享 Store,那咱们就动手来实现一下吧~
Provider
,顾名思义,它是提供者,在这个例子中,它是 context 的提供者。
就像下面这样来使用:
Provider
提供了一个函数 getChildContext
,这个函数返回的是就是代表 context 的对象。在调用 Store 的时候可以从 context 中获取:this.context.store
。
Provider
为了声明自己是 context 的提供者,还需要指定 Provider
的 childContextTypes
属性(需要和 getChildContext
对其)。
只有具备上面两个特点,Provider
才有可能访问到 context。
好了,Provider
组件咱们已经完成了,下面咱们就可以把 context 挂到整个应用的顶层组件上了。
进入整个应用的入口文件 index.js:
我们把 Store 作为 props 传递给了 Provider
组件,Provider 组件把 Store 挂在了 context 上。所以下面我们就要从 context 中来获取 Store 了。
下面是我们整个计数器应用的骨架部分。
我们先把页面渲染出来:
在上面的组件中,我们做了两件事情:
如何声称自己需要 context 呢?
contextTyp
e 赋值,值的类型和 Provider 中提供的 context 的类型一样。this.context
来调用 context 了。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,并且赋值。在组件被卸载的时候,我们取消订阅。
上面这样就完成了订阅功能。这时候再运行程序,可以发现页面上就会显示最新的数字了。
在这个例子中,可以看出来我们可以抽象出来很多逻辑,比如 Provider
,还有订阅 store 变化的功能。其实这些 react-redux 都已经帮我们做好了。
在咱们这个例子中,只是简单的实现了一下 react-redux
部分功能。具体的大家可以到官网上去看。
下面咱们来总结一下 redux 和 react 结合使用的整个数据流:
good~ 我们已经全部完成了整个应用。现在大家了解 Redux 的运行原理 了吗?
具体代码可以到 GitHub 查看。
参考资料:
本文永久链接:
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.