Louis - 东篱之下


  • 首页

  • 分类

  • 归档

React笔记 - State and Lifecycle

发表于 2018-02-09

State and Lifecycle

直到现在,我们只学会了一种更新 UI 的方法,调用ReactDOM.render()来改变渲染出的组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function tick() {
const element = (
<div>
<h1>Hello, world!</h1>
<h2>It is {new Date().toLocaleTimeString()}.</h2>
</div>
);
ReactDOM.render(
element,
document.getElementById('root')
);
}
setInterval(tick, 1000);

在这一节当中,我们将学习如何真正的封装Clock组件,并且让它变得可以重复使用。

我们可以先把 clock 的样子封装一下,先将它的 DOM 结构构造出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Clock(props) {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {props.date.toLocaleTimeString()}.</h2>
</div>
);
}
function tick() {
ReactDOM.render(
<Clock date={new Date()} />,
document.getElementById('root')
);
}
setInterval(tick, 1000);

然而,这样的做法遗漏了一个最重要的要求:就是我们需要将Clock设置定时器并更新UI的这些功能都在Clock内部实现,理想状态下,我们只需要写一次Clock组件,它就应该可以在其他地方使用,并且自己更新自己。

1
2
3
4
ReactDOM.render(
<Clock />,
document.getElementById('root')
);

为了实现它,我们需要给Clock组件添加”state”。

State is similar to props, but it is private and fully controlled by the component.
state 与 props 相似,但是它是组件私有的,并且完全由组件控制。

之前提到过,class component比functional component要多一些特别的功能,state 就是其中之一。

Converting a Function to Class

通过以下5个步骤,你可以将Clock这样的 function 改写成 class:

  1. 用同样的名字,创建一个ES6 class,使其继承自React.Component
  2. 添加一个render()方法
  3. 将 function 的主体全部复制到render()方法中
  4. 将render()方法中的this.props全部替换成props
  5. 将之前声明的 function 删掉
1
2
3
4
5
6
7
8
9
10
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
</div>
);
}
}

现在,Clock就从一个 function 变成了一个 class,它也具备了local state和lifecycle hooks的额外功能。

Adding Local State to a Class

通过以下的三个步骤,我们可以将date从 props 中移动到 state 里:

  1. 将render()方法中的this.props.date替换为this.state.date

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Clock extends React.Component {
    render() {
    return (
    <div>
    <h1>Hello, world!</h1>
    <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
    </div>
    );
    }
    }
  2. 添加一个class constructor指定初始时的this.state:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Clock extends React.Component {
    constructor(props) {
    super(props);
    this.state = {date: new Date()};
    }
    render() {
    return (
    <div>
    <h1>Hello, world!</h1>
    <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
    </div>
    );
    }
    }

这里要注意我们是如何把props传递给父级构造函数的:

1
2
3
4
constructor(props) {
super(props);
this.state = {date: new Date()};
}

class components 必须调用父级构造函数,并且将props作为参数传递给它。

  1. 去掉Clock元素的date属性
    1
    2
    3
    4
    ReactDOM.render(
    <Clock />,
    document.getElementById('root')
    );

我们等会再加上定时器的相关代码,现在,代码应该像是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
ReactDOM.render(
<Clock />,
document.getElementById('root')
);

下面,我们将让Clock元素自己设定一个定时器,并且每秒钟更新一次。

Adding Lifecycle Methods to a Class

在有许多的组件构成的应用中,有一点是很重要的,就是能在组件被销毁时释放它们所占用的资源。我们想要在每次Clock被渲染到DOM的时候,启动一个定时器,这在 React 中被称为”mounting”。同样,在由Clock渲染的DOM被移除时,我们也需要清除掉这个定时器,这在 React 中被称为”unmounting”。
在组件的类中,我们可以声明特殊的方法,这些代码会在组件”mount”和”unmount”时被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() {
// runs after the component output has been rendered to the DOM
}
componentWillUnmount() {
// runs after the component has been removed from DOM
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}

这些方法称为”lifecycle hooks”,componentDidMount()方法会在组件被渲染到DOM后执行,在这个方法内设置定时器是很棒的:

1
2
3
4
5
6
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}

这里将setInterval()方法返回的 int 值存储在this.timerID中,方便在后面 unmount 的时候,对其使用clearInterval()方法。

While this.props is set up by React itself and this.state has a special meaning, you are free to add additional fields to the class manually if you need to store something that is not used for the visual output.

If you don’t use something in render(), it shouldn’t be in the state.

在componentWillUnmount()方法中,销毁掉定时器:

1
2
3
componentWillUnmount() {
clearInterval(this.timerID);
}

最后,我们要实现tick()方法:

1
2
3
4
5
tick() {
this.setState({
date: new Date()
});
}

这个方法会调用this.setState()方法,来安排更新组件的state。
好,我们现在再来回顾一下刚才发生了什么,以及方法被调用的顺序:

  1. 当<Clock />元素被传递给React.render()方法时,React 会调用Clock组件的构造函数,由于Clock需要展示当前的时间,所以在Clock的构造函数中,this.state初始化了一个包含当前时间的对象,然后我们对它进行更新;
  2. React 接着调用Clock组件的render()方法,这使得 React 知道要在屏幕上展示什么东西。接下来 React 会根据Clock的render()方法来更新 DOM;
  3. 当Clock的输出被插入到 DOM 中时,React 会调用componentDidMount()方法,在这个方法内部,Clock向浏览器请求设置一个定时器,每秒调用一次组件中的tick()方法;
  4. 每隔一秒钟,浏览器就会调用tick()方法,在这个方法内部,Clock组件通过调用包含当前时间对象的setState()方法来对 UI 进行更新。多亏了setState()方法,React 才能知道组件的状态已经改变了,这时会再次调用render()方法,来确定需要在屏幕上展示出什么。这次,render() 方法中的this.state.date已经不同了,所以渲染出的结果将会包含更新过的时间,React 会对 DOM 进行相对应的更新;
  5. 如果Clock组件从 DOM 中被移除了,React 会调用componentWillUnmount()方法来将定时器停止。

正确使用 State

对于setState()方法,你需要知道以下三件事情:

不要直接修改 State

举个例子,下面的代码不会对组件进行重新渲染:

1
2
// Wrong
this.state.comment = 'Hello';

正确的方法应该是,使用setState()方法对其进行修改:

1
2
// Correct
this.setState({comment: 'Hello'});

唯一能够对this.state进行赋值的地方是构造函数,除此之外,都不可以。

State 的更新有可能是异步的

出于性能方面的考虑,React 有可能将多个setState()请求整合成单独一次的更新。由于this.props和this.state都可能被异步更新,所以不要依赖它们的值来计算下一个 state。举例来说,下面的代码可能会导致计数器更新失败:

1
2
3
4
// Wrong
this.setState({
counter: this.state.counter + this.props.increment
});

要改正的话,需要用到第二种形式的setState()方法, 这个方法会接收函数作为参数,而不是以对象作为参数。这个匿名方法会接收上一个 state 作为第一个参数,将更新进行时的 props 作为第二个参数:

1
2
3
4
// Correct
this.setState((prevState, props) => ({
counter: prevState.counter + props.increment
}));

上面使用了arrow function,其实使用常规的函数也是一样的:

1
2
3
4
5
6
// Correct
this.setState(function(prevState, props){
return {
counter: prevState.counter + props.increment
};
});

State Updates are Merged

当你调用setState()方法的时候,React 会将你提供的对象并入(merge into)当前的状态中。举个例子,你的 state 可能会包含多个独立的变量:

1
2
3
4
5
6
7
constructor(props) {
super(props);
this.state = {
posts: [],
comments: []
};
}

然后,你可以分开调用setState()方法对它们单独进行更新:

1
2
3
4
5
6
7
8
9
10
11
12
13
componentDidMount() {
fetchPosts().then(response => {
this.setState({
posts: response.posts
});
});
fetchComments().then(response => {
this.setState({
comments: response.comments
});
});
}

The merging is shallow,所以this.setState({comments})会保持this.state.posts的完整,并且整个替换掉this.state.comments。


The Data Flows Down

不管是父级组件还是子级组件,都不能知道一个特定的组件到底是有状态的还是无状态的,它们也不应该关心它是定义成一个函数还是一个类。这就是为什么 state 经常被称作是本地的或者是被封装的,除了自己,别的组件是不能对其进行访问的。组件可以选择将自己的 state 作为 props 传递给它的子级组件:

1
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>

对于用户定义的组件,也是可以的:

1
<FormattedDate date={this.state.date} />

FormattedDate组件将会接收到一个包含在其 props 中的date对象,它不知道这个对象是来自Clock的 state,还是Clock的 props,或者是硬编码进去的:

1
2
3
function FormattedDate(props) {
return <h2>It is {props.date.toLocaleTimeString}.</h2>;
}

这通常被称作“自顶向下的”或“单向的”数据流,任何的 state 都永远是为某些特定的 component 所有的,并且任何的从那个 state 处得到的数据或者 UI 只能影响它们“下面的”组件。

如果你把一个组件树想象成由 props 构成的瀑布的花,每个组件的 state 就像一个额外的水源,它在某个任意的点上汇入,但是也将“顺流而下”。

为了展示所有的组件都是真正独立的,我们可以创建一个App组件来渲染3个<Clock />:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function App() {
return (
<div>
<Clock />
<Clock />
<Clock />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);

这样,每个Clock都会单独地设置自己的定时器并且更新。

在 React apps 中,考虑一个组件是有状态的还是无状态的,涉及到一个实现的细节,即这个组件是否会随着时间而发生变化。你既可以在有状态的组件中使用无状态的组件,也可以在无状态的组件中,使用有状态的组件,这都是可以的。

Git-分支管理

发表于 2018-01-30 | 分类于 Git

Git 分支管理

分支就是科幻电影里面的平行宇宙,当你正在电脑前努力学习Git的时候,另一个你正在另一个平行宇宙里努力学习SVN。

如果两个平行宇宙互不干扰,那对现在的你也没啥影响。不过,在某个时间点,两个平行宇宙合并了,结果,你既学会了Git又学会了SVN!
分支管理
分支在实际中有什么用呢?假设你准备开发一个新功能,但是需要两周才能完成,第一周你写了50%的代码,如果立刻提交,由于代码还没写完,不完整的代码库会导致别人不能干活了。如果等代码全部写完再一次提交,又存在丢失每天进度的巨大风险。

现在有了分支,就不用怕了。你创建了一个属于你自己的分支,别人看不到,还继续在原来的分支上正常工作,而你在自己的分支上干活,想提交就提交,直到开发完毕后,再一次性合并到原来的分支上,这样,既安全,又不影响别人工作。

其他版本控制系统如SVN等都有分支管理,但是用过之后你会发现,这些版本控制系统创建和切换分支比蜗牛还慢,简直让人无法忍受,结果分支功能成了摆设,大家都不去用。

但Git的分支是与众不同的,无论创建、切换和删除分支,Git在1秒钟之内就能完成!无论你的版本库是1个文件还是1万个文件。

创建与合并分支


在每次提交后,Git都把各个版本串成一条时间线,这条时间线就是一个分支。截止到目前,只有一条时间线,在Git里,这个分支叫主分支,即master分支。HEAD严格来说不是指向提交,而是指向master,master才是指向提交的,所以,HEAD指向的就是当前分支。

一开始的时候,master分支是一条线,Git用master指向最新的提交,再用HEAD指向master,就能确定当前分支,以及当前分支的提交点:
master
每次提交,master分支都会向前移动一步,这样,随着你不断提交,master分支的线也越来越长。

当我们创建新的分支,例如dev时,Git新建了一个指针叫dev,指向master相同的提交,再把HEAD指向dev,就表示当前分支在dev上:
dev
你看,Git创建一个分支很快,因为除了增加一个dev指针,改改HEAD的指向,工作区的文件都没有任何变化!

不过,从现在开始,对工作区的修改和提交就是针对dev分支了,比如新提交一次后,dev指针往前移动一步,而master指针不变:
new dev
假如我们在dev上的工作完成了,就可以把dev合并到master上。Git怎么合并呢?最简单的方法,就是直接把master指向dev的当前提交,就完成了合并:
合并
所以Git合并分支也很快!就改改指针,工作区内容也不变!

合并完分支后,甚至可以删除dev分支。删除dev分支就是把dev指针给删掉,删掉后,我们就剩下了一条master分支:
删除

好了,看懂了这些基本的流程,我们就开始实战吧:

首先,我们创建一个名为dev的分支,并切换到dev分支上去:

1
2
$ git checkout -b dev
Switched to a new branch 'dev'

这里的git checkout -b dev命令表示的是“创建dev分支并切换到dev分支”,这其实相当于两条命令:

1
2
3
$ git branch dev
$ git checkout dev
Switched to branch 'dev'

然后,使用git branch查看分支情况:

1
2
3
$ git branch
* dev
master

这条命令会返回所有的分支,并且在当前的分支上,会有一颗*号。
现在,我们就可以在dev分支上进行正常的提交了,比如我们再在readme.txt文件里加一行内容:

1
Creating a new branch is quick.

将这个改动提交到版本库中:

1
2
3
4
$ git add readme.txt
$ git commit -m 'add new branch'
[dev d19f6e8] add new branch
1 file changed, 2 insertions(+)

OK,现在假设我们在dev分支上的工作已经完成了, 现在我们回到master分支上来:

1
2
3
$ git checkout master
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.

切换回master分支后,我们可以看到,原先在dev分支上修改过的readme.txt文件并没有产生变化,还是那四句话:
切换回master
此时的指针状态应该是这样的:
当前指针
现在,我们需要把dev的工作成果合并到master分支上,我们需要git merge命令:

1
2
3
4
5
$ git merge dev
Updating a81ac4f..d19f6e8
Fast-forward
readme.txt | 2 ++
1 file changed, 2 insertions(+)

git merge命令用于将指定的分支(dev)合并到当前分支(master)上来,现在我们再看一下readme.txt文件的内容,发现多了一行内容了,已经和dev分支上一模一样了。
注意上面返回的信息中的Fast-forward消息,这意味着这次的合并模式是快进模式,就是将master的指针向前移动到了dev的指针上,这样的合并速度就非常快,当然不是每次都是快进模式的合并,还有其他的合并方式。
合并完成后,假设我们也不再需要dev分支了,我们可以执行命令,将其删除:

1
2
$ git branch -d dev
Deleted branch dev (was d19f6e8).

删除后,我们再查看一下版本库中分支的情况:

1
2
$ git branch
* master

现在,我们就只剩master这一个分支了。

解决冲突


人生不如意之事十之八九,合并分支往往也不是一帆风顺的。

准备新的feature1分支,继续我们的新分支开发:

1
2
$ git checkout -b feature1
Switched to a new branch 'feature1'

修改readme.txt文件的最后一行内容:

1
Creating a new branch is quick AND simple.

现在,提交这个分支的修改:

1
2
3
4
$ git add readme.txt
$ git commit -m 'AND simple'
[feature1 e412abc] AND simple
1 file changed, 1 insertion(+), 1 deletion(-)

好的,完成了feature1上的修改,我们现在回到master分支上来:

1
2
3
4
$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 1 commit.
(use "git push" to publish your local commits)

我们再次修改一下readme.txt文件,将最后一行改为:

1
Creating a new branch is quick & simple.

提交修改:

1
2
3
4
$ git add readme.txt
$ git commit -m '& simple'
[master 54d63e2] & simple
1 file changed, 1 insertion(+), 1 deletion(-)

现在,我们可以思考一下版本的指针状态,现在mater和feature1分支都有了新的提交,所以,应该是这样的:
分支状态
这种情况下,Git无法执行“快速合并”,只能试图把各自的修改合并起来,但这种合并就可能会有冲突,我们试试看:

1
2
3
4
$ git merge feature1
Auto-merging readme.txt
CONFLICT (content): Merge conflict in readme.txt
Automatic merge failed; fix conflicts and then commit the result.

从返回的信息可以看出来,Git告诉我们在合并时,readme.txt文件存在冲突,需要我们解决了冲突之后再提交结果。我们现在用git status看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ git status
On branch master
Your branch is ahead of 'origin/master' by 2 commits.
(use "git push" to publish your local commits)
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: readme.txt
no changes added to commit (use "git add" and/or "git commit -a")

我们现在再看看readme.txt里面的内容:
readme.txt
卧槽,居然变成这样了,要怎么办呢?
可以看到,Git使用了<<<<<<<,=======,>>>>>>>标记了不同分支的内容,我们现在将其改成如下内容:

1
Creating a new branch is quick and simple.

再执行提交操作:

1
2
3
$ git add readme.txt
$ git commit -m 'and simple'
[master f31c8e4] and simple

现在,master分支和feature1分支变成了下面的样子了:
分支状态
用带参数的git log命令,也可以看到合并的情况:

1
2
3
4
5
6
7
$ git log --graph --pretty=oneline --abbrev-commit
* f31c8e4 (HEAD -> master) and simple
|\
| * e412abc (feature1) AND simple
* | 54d63e2 & simple
|/
* d19f6e8 add new branch

最后,删除feature1分支:

1
2
$ git branch -d feature1
Deleted branch feature1 (was e412abc).

分支管理策略


通常,合并分支时,如果可能,Git会用Fast forward模式,但这种模式下,删除分支后,会丢掉分支信息。

如果要强制禁用Fast forward模式,Git就会在merge时生成一个新的commit,这样,从分支历史上就可以看出分支信息。

下面我们实战一下--no-ff方式的git merge:

首先,仍然创建并切换dev分支:

1
2
$ git checkout -b dev
Switched to a new branch 'dev'

对readme.txt修改后,进行提交:

1
2
3
4
$ git add readme.txt
$ git commit -m 'add merge'
[dev 5b54fb4] add merge
1 file changed, 1 insertion(+)

然后,我们切回master并且准备合并dev分支,这里要使用--no-ff参数:

1
2
3
4
5
6
7
8
$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 4 commits.
(use "git push" to publish your local commits)
$ git merge --no-ff -m 'merge with no-ff' dev
Merge made by the 'recursive' strategy.
readme.txt | 1 +
1 file changed, 1 insertion(+)

再次使用git log命令,查看一下分支的情况:

1
2
3
4
5
6
7
$ git log --graph --pretty=oneline --abbrev-commit
* 097a662 (HEAD -> master) merge with no-ff
|\
| * 5b54fb4 (dev) add merge
|/
* f31c8e4 and simple
...

可以看到,不使用Fast forward模式的情况下,分支情况是这样的:
分支情况

分支策略

分支的功能很强大,所以我们要遵循以下的几个原则来进行分支管理:

  1. master分支应该只用来进行版本的发布,平时不能在上面工作
  2. 干活都应该在dev上面,当需要发布稳定版本的时候,将dev合并到master上去
  3. 每个开发的小伙伴都应该从dev上分支出自己的版本,然后时不时地与dev进行合并就行了

##

Git-远程仓库

发表于 2018-01-29 | 分类于 Git

Git 远程仓库

Git是分布式版本控制系统,同一个Git仓库,可以分布到不同的机器上。怎么分布呢?最早,肯定只有一台机器有一个原始版本库,此后,别的机器可以“克隆”这个原始版本库,而且每台机器的版本库其实都是一样的,并没有主次之分。

实际情况往往是这样,找一台电脑充当服务器的角色,每天24小时开机,其他每个人都从这个“服务器”仓库克隆一份到自己的电脑上,并且各自把各自的提交推送到服务器仓库里,也从服务器仓库中拉取别人的提交。

完全可以自己搭建一台运行Git的服务器,不过现阶段,为了学Git先搭个服务器绝对是小题大作。好在这个世界上有个叫GitHub的神奇的网站,从名字就可以看出,这个网站就是提供Git仓库托管服务的,所以,只要注册一个GitHub账号,就可以免费获得Git远程仓库。

在继续阅读后续内容前,请自行注册GitHub账号。由于你的本地Git仓库和GitHub仓库之间的传输是通过SSH加密的,所以,需要一点设置:

第一步:创建SSH Key。打开terminal,创建SSH Key:

1
$ ssh-keygen -t -rsa -C "jsyzliuxb0918@gmail.com"

这里我会将默认的/Users/louis/.ssh/id_rsa改为/Users/louis/.ssh/github_rsa,这里是不需要设置passphrase的,如果不小心设置了的话,可以运行下面的命令进行更改:

1
$ ssh-keygen -p

这里会一步一步要求你修改你的密码的。如果一切顺利的话,可以在用户主目录里找到.ssh目录,里面有github_rsa和github_rsa.pub两个文件,这两个就是SSH Key的秘钥对,github_rsa是私钥,不能泄露出去,github_rsa.pub是公钥,可以放心地告诉任何人。使用下面的命令复制你的公钥:

1
$ pbcopy < ~/.ssh/github_rsa.pub

打开Github,点击头像进入Settings,在左侧选择SSH and GPG Keys:
Github

为什么GitHub需要SSH Key呢?因为GitHub需要识别出你推送的提交确实是你推送的,而不是别人冒充的,而Git支持SSH协议,所以,GitHub只要知道了你的公钥,就可以确认只有你自己才能推送。

当然,GitHub允许你添加多个Key。假定你有若干电脑,你一会儿在公司提交,一会儿在家里提交,只要把每台电脑的Key都添加到GitHub,就可以在每台电脑上往GitHub推送了。

最后友情提示,在GitHub上免费托管的Git仓库,任何人都可以看到喔(但只有你自己才能改)。所以,不要把敏感信息放进去。

如果你不想让别人看到Git库,有两个办法,一个是交点保护费,让GitHub把公开的仓库变成私有的,这样别人就看不见了(不可读更不可写)。另一个办法是自己动手,搭一个Git服务器,因为是你自己的Git服务器,所以别人也是看不见的。这个方法我们后面会讲到的,相当简单,公司内部开发必备。

确保你拥有一个GitHub账号后,我们就即将开始远程仓库的学习。

添加远程库


现在的情景是,你已经在本地创建了一个Git仓库后,又想在GitHub创建一个Git仓库,并且让这两个仓库进行远程同步,这样,GitHub上的仓库既可以作为备份,又可以让其他人通过该仓库来协作,真是一举多得。

首先,登陆GitHub,然后,在右上角找到“Create a new repo”按钮,创建一个新的仓库:
创建新仓库
在Repository name填入Learngit,其他保持默认设置,点击“Create repository”按钮,就成功地创建了一个新的Git仓库:
新仓库
目前,在GitHub上的这个Learngit仓库还是空的,GitHub告诉我们,可以从这个仓库克隆出新的仓库,也可以把一个已有的本地仓库与之关联,然后,把本地仓库的内容推送到GitHub仓库。

现在,我们根据GitHub的提示,在本地的Learngit仓库下运行命令:

1
$ git remote add origin git@github.com:LouisMelo/Learngit.git

添加后,远程库的名字就是origin,这是Git默认的叫法,也可以改成别的,但是origin这个名字一看就知道是远程库。
下面,就可以把本地库的所有内容推送到远程库上:

1
2
3
4
5
6
7
8
9
10
$ git push -u origin master
Counting objects: 27, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (22/22), done.
Writing objects: 100% (27/27), 2.26 KiB | 0 bytes/s, done.
Total 27 (delta 8), reused 0 (delta 0)
remote: Resolving deltas: 100% (8/8), done.
To github.com:LouisMelo/Learngit.git
* [new branch] master -> master
Branch master set up to track remote branch master from origin.

把本地库的内容推送到远程,用git push命令,实际上是把当前分支master推送到远程。

由于远程库是空的,我们第一次推送master分支时,加上了-u参数,Git不但会把本地的master分支内容推送的远程新的master分支,还会把本地的master分支和远程的master分支关联起来,在以后的推送或者拉取时就可以简化命令。

推送成功后,可以立刻在GitHub页面中看到远程库的内容已经和本地一模一样:
远程仓库
好了,从现在开始,如果本地有了新的提交,你就可以通过下面的命令将它推送到Github了:

1
$ git push origin master

分布式版本系统的最大好处之一是在本地工作完全不需要考虑远程库的存在,也就是有没有联网都可以正常工作,而SVN在没有联网的时候是拒绝干活的!当有网络的时候,再把本地提交推送一下就完成了同步,真是太方便了!

Git-时光机穿梭

发表于 2018-01-29 | 分类于 Git

Git 时光机穿梭

我们已经可以将文件添加到git仓库了,下面我们试着修改这个文本文件,看看会有什么结果:

1
2
Git is a distributed version control system.
Git is free software.

修改完成后,保存,然后运行 git status 看看有什么结果:

1
2
3
4
5
6
7
8
9
$ git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: readme.txt
no changes added to commit (use "git add" and/or "git commit -a")

这样,返回的信息就会告诉我们,现在有文件被修改了,但是还没有被提交(changes not staged for commit)。
虽然git status可以告诉我们当前仓库的状况,但是并不能清楚的告诉我们修改了哪些地方。这时候,我们就需要用到git diff命令:

1
2
3
4
5
6
7
8
9
$ git diff readme.txt
diff --git a/readme.txt b/readme.txt
index 23bc09e..9247db6 100644
--- a/readme.txt
+++ b/readme.txt
@@ -1,2 +1,2 @@
-Git is version control system.
+Git is a distributed version control system.
Git is free software.

现在,我们知道了readme.txt中只修改了一行内容。所以我们就可以安心的提交更改了:

1
$ git add readme.txt

这时候,我们再来看一下git仓库的状态,使用git status命令:

1
2
3
4
5
6
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: readme.txt

这时,系统告诉我们,将要被提交的修改包含了readme.txt文件。
现在我们就提交这次修改,使用git commit命令:

1
2
3
$ git commit -m 'add distributed'
[master 3b15474] add distributed
1 file changed, 1 insertion(+), 1 deletion(-)

提交完成后,我们再使用git status来查看一下仓库的状态:

1
2
3
$ git status
On branch master
nothing to commit, working tree clean

这时,返回的信息告诉我们,没有需要提交的修改,而且工作目录是干净的。

版本回退


现在我们已经学会了如何修改文件并且将修改提交到git仓库了,下面再联系一次:

1
2
Git is a distributed version control system.
Git is free software distributed under the GPL.

然后提交修改:

1
2
3
4
$ git add readme.txt
$ git commit -m 'append GPL'
[master 9b51472] append GPL
1 file changed, 1 insertion(+), 1 deletion(-)

像这样,你不断对文件进行修改,然后不断提交修改到版本库里,就好比玩RPG游戏时,每通过一关就会自动把游戏状态存盘,如果某一关没过去,你还可以选择读取前一关的状态。有些时候,在打Boss之前,你会手动存盘,以便万一打Boss失败了,可以从最近的地方重新开始。Git也是一样,每当你觉得文件修改到一定程度的时候,就可以“保存一个快照”,这个快照在Git中被称为commit。一旦你把文件改乱了,或者误删了文件,还可以从最近的一个commit恢复,然后继续工作,而不是把几个月的工作成果全部丢失。

好,现在我们回顾一下我们一共有多少个版本了:
版本1: add readme file

1
2
Git is a version control system.
Git is free software.

版本2: add distributed

1
2
Git is a distributed version control system.
Git is free software.

版本3: append GPL

1
2
Git is a distributed version control system.
Git is free software distributed under the GPL.

在git中,我们可以使用git log命令来查看历史记录,它可以告诉我们每次都修改了些什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ git log
commit 9b5147211afee2c3b233d2f74de5f4cec710977b (HEAD -> master)
Author: louis <louis@promote.cache-dns.local>
Date: Mon Jan 29 16:26:38 2018 +0800
append GPL
commit 3b1547438f09df37716d770a9919fce34405994f
Author: louis <louis@promote.cache-dns.local>
Date: Mon Jan 29 16:21:32 2018 +0800
add distributed
commit 7b235af1f06c6952dd4cd41eea8c77bb02be5dce
Author: louis <louis@promote.cache-dns.local>
Date: Mon Jan 29 14:55:59 2018 +0800
add readme file

如果觉得输出的信息太多了,可以试着使用git log --pretty=online,这样,就能看到最精简的信息了。

这里的commit 7b235af1f06c6952dd4cd41eea8c77bb02be5dce就是版本号了,和SVN不一样,git的版本号不是1,2,3,4这样递增的数字,而是一个SHA1计算出来的非常大的数字,用16进制表示。为什么commit id需要用这么一大串数字表示呢?因为Git是分布式的版本控制系统,后面我们还要研究多人在同一个版本库里工作,如果大家都用1,2,3,4作为版本号,那肯定就冲突了。

好了,现在我们知道了这些版本的历史记录之后,就可以启动时光穿梭机了。我们准备把readme.txt回退到上一个版本,即”add distributed”的那个版本,需要怎么做呢?
首先,必须知道当前版本是哪个版本,在git中,使用HEAD表示当前的版本,上一个版本就是HEAD^,上上个版本就是HEAD^^,但是如果是上100个版本呢?我们可以写成HEAD~100。
现在我们要把当前版本”append GPL”回退到上一个版本”add distributed”,我们需要使用到git reset命令:

1
2
$ git reset --hard HEAD^
HEAD is now at 3b15474 add distributed

--hard的含义,后续会讲到,先来看看readme.txt是否回到了add distributed版本:

1
2
3
$ cat readme.txt
Git is a distributed version control system.
Git is free software.

可以发现,现在我们确实回到了add distributed那个版本,我们还可以继续回到上一个版本,不过我们先停一下,使用git log查看一下版本记录:

1
2
3
4
5
6
7
8
9
10
11
12
$ git log
commit 3b1547438f09df37716d770a9919fce34405994f (HEAD -> master)
Author: louis <louis@promote.cache-dns.local>
Date: Mon Jan 29 16:21:32 2018 +0800
add distributed
commit 7b235af1f06c6952dd4cd41eea8c77bb02be5dce
Author: louis <louis@promote.cache-dns.local>
Date: Mon Jan 29 14:55:59 2018 +0800
add readme file

什么?!我们的append GPL的版本怎么不见了呢?好比你从21世纪坐时光穿梭机来到了19世纪,想再回去已经回不去了,肿么办?
办法其实还是有的,只要上面的命令行窗口还没有被关掉,你就可以顺着往上找啊找啊,找到那个append GPL的commit id是9b51472…,于是就可以指定回到未来的某个版本:

1
2
$ git reset --hard 9b51472
HEAD is now at 9b51472 append GPL

版本号没必要写全,前几位就可以了,Git会自动去找。当然也不能只写前一两位,因为Git可能会找到多个版本号,就无法确定是哪一个了。

再小心翼翼地看看readme.txt的内容:

1
2
3
$ cat readme.txt
Git is a distributed version control system.
Git is free software distributed under the GPL.

果然,我胡汉三又回来了。

Git的版本回退速度非常快,因为Git在内部有个指向当前版本的HEAD指针,当你回退版本的时候,Git仅仅是把HEAD从指向append GPL:
append GPL
改为指向add distributed:
add distributed
然后顺便把工作区的文件更新了。所以你让HEAD指向哪个版本号,你就把当前版本定位在哪。

现在,你回退到了某个版本,关掉了电脑,第二天早上就后悔了,想恢复到新版本怎么办?找不到新版本的commit id怎么办?

在Git中,总是有后悔药可以吃的。当你用$ git reset --hard HEAD^回退到add distributed版本时,再想恢复到append GPL,就必须找到append GPL的commit id。Git提供了一个命令git reflog用来记录你的每一次命令:

1
2
3
4
5
6
$ git reflog
9b51472 (HEAD -> master) HEAD@{0}: reset: moving to 9b51472
3b15474 HEAD@{1}: reset: moving to HEAD^
9b51472 (HEAD -> master) HEAD@{2}: commit: append GPL
3b15474 HEAD@{3}: commit: add distributed
7b235af HEAD@{4}: commit (initial): add readme file

从返回的信息可以看出来,append GPL的commit id为9b51472。这样,我们就又可以回到未来啦!

工作区和暂存区


Git和其他版本控制系统如SVN的一个不同之处就是有暂存区的概念。

先来看名词解释。

工作区(Working Directory)
就是你在电脑里能看到的目录,比如我的learngit文件夹就是一个工作区:
工作区

版本库(Git Repository)
工作区有一个隐藏目录.git,这个不算工作区,而是Git的版本库。

Git的版本库里存了很多东西,其中最重要的就是称为stage(或者叫index)的暂存区,还有Git为我们自动创建的第一个分支master,以及指向master的一个指针叫HEAD。
版本库
分支和HEAD的概念我们以后再讲。

前面讲了我们把文件往Git版本库里添加的时候,是分两步执行的:

第一步是用git add把文件添加进去,实际上就是把文件修改添加到暂存区;

第二步是用git commit提交更改,实际上就是把暂存区的所有内容提交到当前分支。

因为我们创建Git版本库时,Git自动为我们创建了唯一一个master分支,所以,现在,git commit就是往master分支上提交更改。

你可以简单理解为,需要提交的文件修改通通放到暂存区,然后,一次性提交暂存区的所有修改。

俗话说,实践出真知。现在,我们再练习一遍,先对readme.txt做个修改,比如加上一行内容:

1
2
3
Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.

然后,在工作区新增一个LICENSE文件:

1
2
$ touch LICENSE
$ nano LICENSE

现在,我们用git status查看一下仓库的状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: readme.txt
Untracked files:
(use "git add <file>..." to include in what will be committed)
LICENSE
no changes added to commit (use "git add" and/or "git commit -a")

返回的信息清楚的告诉了我们,readme.txt被修改了,而LICENSE是第一次出现,所以状态是Untracked。

现在,使用两次git add,把两个文件都添加后,再使用git status查看一下:

1
2
3
4
5
6
7
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: LICENSE
modified: readme.txt

现在,暂存区的状态就变成这样了:
暂存区
所以,git add命令实际上就是把要提交的所有修改放到暂存区(Stage),然后,执行git commit就可以一次性把暂存区的所有修改提交到分支。

1
2
3
4
$ git commit -m "understand how stage works"
[master 2e3b6fb] understand how stage works
2 files changed, 2 insertions(+)
create mode 100644 LICENSE

一旦提交成功后,这时你的工作区就是干净的了:

1
2
3
$ git status
On branch master
nothing to commit, working tree clean

现在,版本库就变成了这样:
版本库

管理修改


现在,我们已经掌握了暂存区的概念。接下来,我们讨论一下,为什么Git比其他版本控制系统设计的优秀,因为Git跟踪并管理的是修改,而非文件。
那什么是修改呢?比如你新增了一行,这就是一个修改;删除了一行,也是一个修改;更改了某些字符,也是一个修改;删了一些又加了一些,也是一个修改;甚至创建一个新文件,也算一个修改。
为什么说Git管理的是修改,而不是文件呢?我们还是做实验。第一步,对readme.txt做一个修改,比如加一行内容:

1
2
3
4
5
$ cat readme.text
Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.
Git tracks changes.

然后,添加到暂存区:

1
2
3
4
5
6
7
$ git add readme.txt
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: readme.txt

然后,再次修改readme.txt:

1
2
3
4
5
$ cat readme.txt
Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.
Git tracks changes of files.

提交:

1
2
3
$ git commit -m 'git tracks changes'
[master 43daa3e] git tracks changes
1 file changed, 1 insertion(+)

提交后,再看看状态:

1
2
3
4
5
6
7
8
9
$ git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: readme.txt
no changes added to commit (use "git add" and/or "git commit -a")

为什么这里第二次的修改没有被提交呢?
我们来回顾一下整个操作过程:
第一次修改 -> git add -> 第二次修改 -> git commit
我们前面讲了,Git管理的是修改,当你使用git add命令后,在工作区的第一次修改被放入暂存区,准备提交,但是,在工作区的第二次修改并没有放入暂存区,所以,git commit只负责把暂存区的修改提交了,也就是第一次的修改被提交了,第二次的修改不会被提交。
提交后,用git diff HEAD -- readme.txt命令可以查看工作区和版本库里面最新版本的区别:

1
2
3
4
5
6
7
8
9
10
11
$ git diff HEAD -- readme.txt
diff --git a/readme.txt b/readme.txt
index 76d770f..a9c5755 100644
--- a/readme.txt
+++ b/readme.txt
@@ -1,4 +1,4 @@
Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.
-Git tracks changes.
+Git tracks changes of files.

可见,第二次修改确实没有被提交。那怎么才能提交第二次的修改呢?只有继续使用git add将修改提交到暂存区,然后再使用git commit将暂存区的修改提交。现在可以了解到,每一次修改,如果不add到暂存区,那就不会加入到commit中。

撤销修改


自然,你是不会犯错的。不过现在是凌晨两点,你正在赶一份工作报告,你在readme.txt中添加了一行:

1
2
3
4
5
6
$ cat readme.txt
Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.
Git tracks changes of files.
My stupid boss still prefers SVN.

在你准备提交前,一杯咖啡起了作用,你猛然发现了“stupid boss”可能会让你丢掉这个月的奖金!

既然错误发现得很及时,就可以很容易地纠正它。你可以删掉最后一行,手动把文件恢复到上一个版本的状态。如果用git status查看一下:

1
2
3
4
5
6
7
8
9
$ git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: readme.txt
no changes added to commit (use "git add" and/or "git commit -a")

从返回的信息可以发现,你可以使用git checkout -- readme.txt来丢弃工作区的修改:

1
$ git checkout -- readme.txt

命令git checkout -- readme.txt意思就是,把readme.txt文件在工作区的修改全部撤销,这里有两种情况:

一种是readme.txt自修改后还没有被放到暂存区,现在,撤销修改就回到和版本库一模一样的状态;

一种是readme.txt已经添加到暂存区后,又作了修改,现在,撤销修改就回到添加到暂存区后的状态。

总之,就是让这个文件回到最近一次git commit或git add时的状态。

现在,看看readme.txt的文件内容:

1
2
3
4
5
$ cat readme.txt
Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.
Git tracks changes of files.

文件内容果然复原了。

git checkout -- file命令中的--很重要,没有--,就变成了“切换到另一个分支”的命令,我们在后面的分支管理中会再次遇到git checkout命令。

现在假定是凌晨3点,你不但写了一些胡话,还git add到暂存区了:

1
2
3
4
5
6
7
8
$ cat readme.txt
Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.
Git tracks changes of files.
My stupid boss still prefers SVN.
$ git add readme.txt

这时,使用git status查看一下当前的状态:

1
2
3
4
5
6
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: readme.txt

Git同样告诉我们,用命令git reset HEAD file可以把暂存区的修改撤销掉(unstage),重新放回工作区:

1
2
3
$ git reset HEAD readme.txt
Unstaged changes after reset:
M readme.txt

git reset命令既可以回退版本,也可以把暂存区的修改回退到工作区。当我们用HEAD时,表示最新的版本。

再用git status查看一下,现在暂存区是干净的,工作区有修改:

1
2
3
4
5
6
7
8
9
$ git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: readme.txt
no changes added to commit (use "git add" and/or "git commit -a")

还记得如何撤销工作区的修改吗?

1
2
3
4
5
$ git checkout -- readme.txt
$ git status
On branch master
nothing to commit (working directory clean)

现在,假设你不但改错了东西,还从暂存区提交到了版本库,怎么办呢?还记得版本回退一节吗?可以回退到上一个版本。不过,这是有条件的,就是你还没有把自己的本地版本库推送到远程。还记得Git是分布式版本控制系统吗?我们后面会讲到远程版本库,一旦你把“stupid boss”提交推送到远程版本库,你就真的惨了……

场景1:当你改乱了工作区某个文件的内容,想直接丢弃工作区的修改时,用命令git checkout -- file。

场景2:当你不但改乱了工作区某个文件的内容,还添加到了暂存区时,想丢弃修改,分两步,第一步用命令git reset HEAD file,就回到了场景1,第二步按场景1操作。

场景3:已经提交了不合适的修改到版本库时,想要撤销本次提交,参考版本回退一节,不过前提是没有推送到远程库。

删除文件


在Git中,删除也是一个修改操作,我们实战一下,先添加一个新文件test.txt到Git并且提交:

1
2
3
4
5
$ git add test.txt
$ git commit -m 'add test.txt'
[master e432478] add test.txt
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 test.txt

一般情况下,你通常直接在文件管理器中把没用的文件删了,或者用rm命令删了:

1
$ rm test.txt

这个时候,Git知道你删除了文件,因此,工作区和版本库就不一致了,git status命令会立刻告诉你哪些文件被删除了:

1
2
3
4
5
6
7
8
9
$ git status
On branch master
Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
deleted: test.txt
no changes added to commit (use "git add" and/or "git commit -a")

现在你有两个选择,一是确实要从版本库中删除该文件,那就用命令git rm删掉,并且git commit:

1
2
3
4
5
6
$ git rm test.txt
rm 'test.txt'
$ git commit -m 'remove test.txt'
[master cdaffb3] remove test.txt
1 file changed, 0 insertions(+), 0 deletions(-)
delete mode 100644 test.txt

现在,文件就从版本库中被删除了。

另一种情况是删错了,因为版本库里还有呢,所以可以很轻松地把误删的文件恢复到最新版本:

1
$ git checkout -- test.txt

git checkout其实是用版本库里的版本替换工作区的版本,无论工作区是修改还是删除,都可以“一键还原”。

Git-简介

发表于 2018-01-23 | 分类于 Git

Git 简介

安装Git


Linux

运行以下命令:

1
sudo apt-get install git

MacOS

  1. 使用Homebrew安装
  2. 使用Xcode中的安装Command Line Tools

Windows

在git-scm网站下载安装git

最后,需要对git进行初始配置:

1
2
git config --global user.name "Your name"
git conifg --global user.email "youremail@example.com"

创建版本库


版本库就是仓库,英文是repository。你可以简单理解成一个目录,这个目录里面的所有文件都可以被Git管理起来,每个文件的修改、删除,Git都能跟踪,以便任何时刻都可以追踪历史,或者在将来某个时刻可以“还原”。

创建一个版本库很简单:

1
2
3
4
$ mkdir louis-git
$ cd louis-git
$ pwd
/Users/louis/louis-git

pwd的作用是显示当前目录。
现在我们通过 git init 来初始化这个git仓库:

1
2
$ git init
Initialized empty Git repository in /Users/louis/louisgit/.git/

这样,我们就已经创建好一个git仓库了,可以从命令行中看出来,这个仓库现在还是 empty 的。下面我们就开始将文件添加到这个 Git repository。

把文件添加到版本库

首先需要说明的是,所有的版本控制系统,其实都只能跟踪文本文件的改动,比如TXT,网页,程序代码等,Git也是这样的。版本控制系统可以告诉你每次的改动,比如在第5行加了一个单词“Linux”,在第8行删了一个单词“Windows”。而图片、视频这些二进制文件,虽然也能由版本控制系统管理,但没法跟踪文件的变化,只能把二进制文件每次改动串起来,也就是只知道图片从100KB改成了120KB,但到底改了啥,版本控制系统不知道,也没法知道。

不幸的是,Microsoft的Word格式是二进制格式,因此,版本控制系统是没法跟踪Word文件的改动的。

因为文本是有编码的,比如中文有常用的GBK编码,强烈建议使用标准的UTF-8编码,所有语言使用同一种编码,既没有冲突,又被所有平台所支持。

现在我们在learngit目录下,创建一个新的文本文件,就叫做readme.txt,并在其中输入如下内容。

1
2
3
4
5
6
7
Git is version control system.
Git is free software.
```
下面我们要将这个文本文件添加到 git 仓库就需要以下两步:
第一步,使用`git add`命令
```bash
$ git add readme.txt

第二步,使用git commit命令

1
$ git commit -m 'add a readme file'

这样,就成功将readme.txt添加到了git仓库里了,可以说非常简单。

录制用户的音频 & 视频

发表于 2018-01-08

录制音频

许多浏览器现在都能访问用户的音频和视频输入。

简单的版本

最简易的方式就是让用户自己提供预先录制的文件。其实现步骤是:创建一个简单的文件输入元素,然后添加一个表示我们只接受音频文件的accept过滤器,在理想情况下,我们可以直接从麦克风获取这些文件。

1
<input type="file" accept="audio/*" captrue="microphone">

此方法在所有平台上都有效。在桌面平台上,它会提示用户通过文件系统上传文件(忽略microphone的条件)。在iOS上的Safari中,它会打开麦克风应用让您录制音频,然后将其传回网页;在Android上,它允许用户选择合适的应用来录制音频,并将其传回网页。

用户完成录制并返回网站后,您需要以某种方式掌握文件数据。 为 input 元素附加一个onchange事件,然后读取事件对象的files属性,便可快速获得访问权。

1
2
3
4
5
6
7
8
9
10
11
12
<input type="file" accept="audio/*" capture="microphone" id="recorder">
<audio id="player" controls></audio>
<script>
var recorder = document.getElementById('recorder');
var player = document.getElementById('player');
recorder.addEventListener('change', function(e) {
var file = e.target.files[0];
player.src = URL.createObjectURL(file);
});
</script>

在获得文件的访问权之后,便可以对其进行任意操作,比如:

  • 将其直接附加到一个audio元素,这样就能播放文件了
  • 将其下载至用户设备
  • 通过将其附加到一个XMLHttpRequest,上传至服务器
  • 通过Web Audio API传递文件

尽管使用 input 元素方法获得对音频数据访问权的情况普遍存在,却是最没有吸引力的方案。 因为我们真正需要的是获得对麦克风的访问权,直接在页面内提供良好的体验。

以交互方式访问麦克风

现代浏览器可直连麦克风,我们可以借此打造与网页完全集成的体验,让用户永远都不需要离开浏览器。

获得对麦克风的访问权

我们可以利用 WebRTC(Web Real-Time Communication) 规范中名为 getUserMedia() 的API直接访问麦克风。getUserMedia() 将提示用户授予对其相连麦克风(microphone)和摄像头(camera)的访问权。
如果授权成功了,该API会返回一个 Stream,其中包含了来自麦克风和摄像头的数据,然后我们将音频数据附加到一个 <audio> 元素、将其附加到一个网络音频 AudioContext或者使用 MediaRecorder API将其进行保存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<audio id="player" controls="controls"></audio>
<script>
var player = document.getElementById('player');
var handleSuccess = function(stream){
if (window.URL) {
player.src = window.URL.createObjectURL(stream);
} else {
player.src = stream;
}
};
navigator.mediaDevices.getUserMedia({audio:true, video:false}).then(handleSuccess);
</script>

在开启网页后,会询问是否允许访问麦克风设备,并且此时,点击 <audio> 控件的播放按钮,会实时的输出麦克风接收到的音频。
不过,这段代码的代码作用并不太大。我们所能做的太少了。

从麦克风获得原始数据

保存来自麦克风的数据

想要保存来自麦克风的数据,最简便的方法就是使用 MediaRecorder API。

MediaRecorder API 将获取 getUserMedia 创建的卡片流信息,然后渐进式的将卡片信息流中的数据保存到首选目的地。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<a id="download">Download</a>
<button id="stop">Stop</button>
<script>
let shouldStop = false;
let stopped = false;
const downloadLink = document.getElementById('download');
const stopButton = document.getElementById('stop');
stopButton.addEventListen('click', function(){
shouldStop = true;
});
var handleSuccess = function(stream){
const options = {mimeType:'video/webm;codecs=vp9'};
const recordedChunks = [];
const mediaRecorder = new MediaRecorder(stream, options);
mediaRecorder.addEventListener('dataavailable', function(e) {
if (e.data.size > 0) {
recordedChunks.push(e.data);
}
if(shouldStop === true && stopped === false) {
mediaRecorder.stop();
stopped = true;
}
});
mediaRecorder.addEventListener('stop', function() {
downloadLink.href = URL.createObjectURL(new Blob(recordedChunks));
downloadLink.download = 'acetest.wav';
});
mediaRecorder.start();
};
navigator.mediaDevices.getUserMedia({audio:true, video:false}).then(handleSuccess);
</script>

MongoDB Socket.io Chat

发表于 2018-01-05

MongoDB + Socket.io 搭建聊天室应用

搞了一天的时间,写个小结稍微总结一下过程中碰到的问题。

Socket.io

socket.io是聊天室应用的主要通信组件,它使得客户端和服务器可以实时双工的进行基于事件的通信,这是聊天室的基础。在socket.io的官网上,有一个很简单的聊天小应用,主要介绍了在express框架下使用socket.io搭建chat demo的过程。

在服务器端,主要有以下几个方法较为常用:

  1. io.on('connection', (socket) => {}): 在客户端建立连接的时候会触发这个函数,会通知服务器,有客户端已经连入聊天应用了。
  2. io.on('disconnect', () => {}): 在客户端关闭连接的时候会触发这个函数,告知服务器端,有个客户端已经断开连接了。
  3. io.emit('someEvent', object): 向所有人发送一个事件,并传递一个对象

微信小程序(一)

发表于 2018-01-02

代码构成

JSON配置

在自动创建的项目文件中,有4个json文件,我们依次来解释一下它们的功能。

小程序配置 app.json

app.json是对当前小程序的全局配置,包括了小程序的所有页面路径、界面表现、网络超时时间、底部tab等。对于自动创建的项目中的app.json文件,包括了pages和window两个字段:

  1. pages: 用来描述当前小程序的所有页面路径,为了让微信客户端知道当前你的小程序页面定义在哪个目录。也就是说,这就相当于一个路由配置,目的是为了给微信程序找到正确的页面路径;
  2. window: 小程序所有页面的顶部背景颜色,文字颜色的定义都在这个字段里
    其他的一些可能用到的app.json配置项为:
  3. tabBar: 设置底部tab的表现(这里底部的tab是用来导航的,而不是我需要的输入控件)
  4. networkTimeout: 设置网络超时时间
  5. debug: 设置是否开启debug模式

工具配置 project.config.json

这个配置文件是为了方便开发者使用的,比如开发者对于开发环境的一些配置,会记录在这个文件中,等到他换其他电脑或者重新安装开发工具的时候,只要载入同一个项目的代码包,那么开发者工具就会帮他恢复之前对于开发环境的个性化配置。

页面配置 page.json

在页面路径下的page.json主要用来控制与小程序页面相关的配置。app.json是全局配置文件,而page.json是某个页面的配置文件,这让某个单独的页面可以被独立定义。

WXML 模板

从事过网页编程的人都知道,网页编程采用的是 HTML + CSS + JS 这样的组合,其中 HTML 是用来描述当前这个页面的结构,CSS 用来描述页面的样子,JS 通常是用来处理这个页面和用户的交互。

同样道理,在小程序中也有同样的角色,其中 WXML 充当的就是类似 HTML 的角色。在pages/index/index.wxml文件中有如下的代码:

在这个文件中,我们可以看出来,大体的结构与HTML语句是很相似的,但是也有很多不一样的地方:

  1. 标签名称不一样: 在HTML语言中,通常使用的标签是div, p, span等,而在这里,小程序的WXML使用的是view, button, text这样的标签。这些标签是小程序为开发者包装好的基本能力,把一些常用的组件包装起来,提高开发者的效率。
  2. 多了一些wx:if这样的属性以及这样的表达式: 在一般的网页开发中,我们通常使用JS操作DOM,以引起界面的变化来响应用户的操作。微信小程序使用MVVM的开发模式,提倡把渲染和逻辑分开,就是不要再让JS直接操作DOM,而只需要管理状态即可,然后再通过一种模板语法来描述状态和界面结构的关系即可。通过的语法把一个变量绑定到界面上,称之为数据绑定。仅仅通过数据绑定还不够完整的描述状态和界面的关系,还需要if/else, for等控制能力,在小程序里边,这些控制能力都用wx:开头的属性来表达。

WXML 详细

WXML(WeiXin Markup Language)是框架设计的一套标签语言,结合基础组件和事件系统,可以构建出页面的结构。其具有以下的多项能力:

  1. 数据绑定:
  2. 列表渲染: wx:for
  3. 条件渲染: wx:if & wx:else
  4. 模板: 可以在模板中定义代码片段,然后在不同地方调用(可以将用户发言的message作为模板,多次调用)
  5. 事件: 将组件与事件处理函数进行绑定

WXSS 样式

WXSS 具有 CSS 大部分的特性,小程序在 WXSS 也做了一些扩充和修改。

  1. 小程序提供了专门的尺寸单位rpx: 开发者可以不用操心不同设备像素比的麻烦,底层会进行换算。
  2. 提供了全局的样式和局部的样式: 与app.json和page.json的概念相同,可以有app.wxss作为全局样式,也可以有page.wxss对单个页面样式进行修改。

JS 交互逻辑

一个服务仅仅只有界面展示是不够的,还需要和用户做交互:响应用户的点击、获取用户的位置等等。在小程序里边,我们就通过编写 JS 脚本文件来处理用户的操作。

小程序的能力

小程序的启动

微信客户端在打开小程序之前,会把整个小程序的代码包下载到本地。

紧接着通过app.json的pages字段就可以知道你当前的小程序的所有页面路径:

1
2
3
4
5
6
{
"pages":[
"pages/index/index",
"pages/logs/logs"
]
}

这个配置说明在自动创建的小程序项目里,定义了两个页面,分别位于pages/index/index以及pages/logs/logs。写在pages字段的第一个页面就是小程序的首页(不论命名如何)。

小程序启动之后,在app.js定义的App实例的onLaunch回调会被执行:

1
2
3
4
5
App({
onLaunch: function () {
// 小程序启动之后 触发
}
})

整个小程序只有一个App实例,是全部页面共享的。

程序与页面

我们可以观察到pages/logs/logs下其实是包括了4种文件的,微信客户端会现根据logs.json配置生产一个界面,顶部的颜色和文字都可以在这个json文件中定义好。紧接着客户端就会装置这个页面的 WXML 结构和 WXSS 样式。最后客户端会装在logs.js,你可以看到logs.js的大体内容就是:

1
2
3
4
5
6
7
8
Page({
data: { // 参与页面渲染的数据
logs: []
},
onLoad: function () {
// 页面渲染后 执行
}
})

Page是一个页面构造器,这个构造器就生成了一个页面。在生成页面的时候,小程序框架会结合data数据和index.wxml一起渲染出最终的结构,于是就看到了你看到的小程序的样子。在渲染完界面之后,页面实例就会收到一个onLoad的回调,你可以在这个回调里处理你的逻辑。

组件

小程序提供了丰富的基础组件给开发者,开发者可以像搭积木一样,组合各种组件拼成自己的小程序。

API

为了让开发者可以很方便的调起微信提供的能力,例如获取用户信息、微信支付等等,小程序提供了很多 API 给开发者去使用。

获取地理位置:

1
2
3
4
5
6
7
wx.getLocation({
type: 'wgs84',
success: (res) => {
var latitude = res.latitude
var longtitude = res.longtitude
}
})

调用微信扫一扫:

1
2
3
4
5
wx.scanCode({
success: (res) => {
console.log(res)
}
})

IFE前端 - Task0001

发表于 2017-10-25 | 分类于 前端

HTML、CSS基础教程


任务目的


掌握HTML、CSS基础知识、能够熟练运用HTML、CSS编写页面

Start


1. 建立你的第一个网页


1.1 期望达成

  • 了解什么是Web: Web 2.0是一种新的互联网方式,通过网络应用(Web Applications)促进网络上人与人间的信息交换和协同合作,其模式更加以用户为中心。典型的Web 2.0站点有:网络社区、网络应用程序、社交网站、博客、Wiki等等。
  • 了解什么事HTML: HTML 是一种标记语言(markup language)。它告诉浏览器如何显示内容。HTML把内容(文字,图片,语言,影片等等)和表现(这个内容是如何显示,比如文字用什么颜色显示等等)分开。HTML使用预先定义的元素集合来识别内容形态。 元素包含一个以上的标记来包含或者表达内容。标记利用尖括号表示,而结束标记(用来指示内容尾端)则在前面加上斜线。
  • 了解一些基本的HTML语句及标签: 、、

    …
  • 能够写出自己的第一个HTML

1.2 任务描述

创建一个HTML文件,比如task0001.html文件,在里面实现一些代码,实现你的第一个网页。

  • 一个一级标题
  • 一个无序列表
  • 一个二级标题
  • 一个段落
  • 一个图片

1.3 任务代码

1
2
3
4
5
6
7
8
9
10
11
12
<h1>Louismelo</h1>
<ul>
<li><a href="task0001.html">Homepage</a></li>
<li><a href="https://github.com/LouisMelo">Blog</a></li>
</ul>
<h2>Louis love snow 4-ever~</h2>
<p>昔日我如此苍老,现在却风华正茂!————Bob Dylan</p>
<img src="https://avatars3.githubusercontent.com/u/18358511?s=460&v=4" alt="">

2. 给你的网页加点样式


2.1 期望达成

  • 了解什么是CSS: Cascading Style Sheets是一种用来为结构化文档添加样式的计算机语言
  • 了解HTML与CSS是如何一起工作的: 首先浏览器会将标记语言和CSS转换成DOM,融合相应的文档内容和样式表,然后浏览器把DOM展示出来成为网页
    How TO Work
  • 了解基本的CSS语法: 每一条rule由一个selector作为开头,后面跟着花括号,花括号内的每一条语句称为一个declaration,每个declaration由一个property-value pairs构成
  • 尝试使用几个简单的CSS属性

2.2 任务描述

学习以下CSS是如何运作的,然后创建一个task0001.css文件,并在task0001.html中引入它。

  • 让一级标题的颜色变成蓝色
  • 二级标题的文字大小变成14px
  • 段落的文字大小变成12px,文字颜色是黄色,带一个黑色的背景色
  • 图片有一个红色的,2px粗的边框

2.3 任务代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
h1 {
color: dodgerblue;
}
h2 {
font-size: 14px;
}
p {
font-size: 12px;
color: yellow;
background-color: black;
width: 465px;
}
img {
border-style: solid;
border-width: 2px;
border-color: red;
}

3. 稍微放松一下


3.1 期望达成

  • 对于HTML及CSS的发展史有一个大概的了解
  • 明白HTML5和之前的版本大概有什么区别

4. CSS基础


4.1 期望达成

  • 掌握CSS各种选择器: simple selectors, attribute selectors, pseudo-classes and pseudo-elements, combinators and multiple selectors
  • 掌握CSS的继承、层叠、样式优先级机制

5. 让页面样式丰富起来


5.1 期望达成

  • 掌握文本、文字、链接相关的样式属性
  • 掌握背景属性
  • 掌握列表相关的样式属性
  • 深入了解行高属性

5.2 任务描述

快速实践以下文本相关的所有属性内容:

  • text-indent: Specify how much horizontal space should be left before the beginning of the first line of the text content.
  • text-transform: 对于英文生效,对于中文不生效,主要是大小,小写,首字母大写以及等宽字体几个option.
  • text-decoration: Sets/unsets text decorations on fonts (you’ll mainly use this to unset the default underline on links when styling them.)
  • text-align: The text-align property is used to control how text is aligned within its containing content box.
  • word-spacing: The letter-spacing and word-spacing properties allow you to set the spacing between letters and words in your text.对于中文而言,每个字之间的间距是由letter-spacing决定的
  • color: The color property sets the color of the foreground content of the selected elements (which is usually the text, but can also include a couple of other things, such as an underline or overline placed on text using the text-decoration property).
  • white-space: Define how whitespace and associated line breaks inside the element are handled.
  • font:
  • font-family:
  • font-size:
  • font-weight:
  • font-face: 可以使用自己定义的字体

6. 盒模型及定位


6.1 期望达成

  • 掌握块状元素、内联元素、和内联块元素的概念与区别
    • 块状(block)元素: <div>、<p>、<h1>、<form>、<ul>、<li>
      1. 每个块级元素都从新的一行开始,并且其后的元素也另起一行。(真霸道,一个块级元素独占一行)
      2. 元素的高度、宽度、行高以及顶和底边距都可以设置。
      3. 元素宽度在不设定的情况下,是它父容器的100%(和父级元素的宽度一致),除非设定一个宽度。
    • 内联(inline)元素: <span>、<a>、<label>、<strong>、<em>
      1. 和其他元素都在一行上。
      2. 元素的高度、宽度及顶部和底部边距不可设置。
      3. 元素的宽度就是它包含的文字或图片的宽度,不可改变.
    • 内联块元素: <img>、<input>
      1. 和其他元素都在一行上
      2. 元素的高度、宽度、行高以及顶和底边距都可以设置。
  • 掌握盒模型的所有概念,学会如何计算各种盒模型相关的数值: content、padding、margin
  • 掌握position的相关知识
    • 流动模型(flow): 默认的网页布局模式
      1. 块状元素都会在所处的包含元素内自上而下按顺序垂直延伸分布
      2. 内联元素都会在所处的包含元素内从左到右水平分布显示
    • 浮动模型(float): 让块状元素并排显示
    • 层模型(layer): 像是图像软件PhotoShop中非常流行的图层编辑功能一样
      1. 绝对定位: position: absolute 这条语句的作用将元素从文档流中拖出来,然后使用left、right、top、bottom属性相对于其最接近的一个具有定位属性的父包含块进行绝对定位。
      2. 相对定位: position: relative 它通过left、right、top、bottom属性确定元素在正常文档流中的偏移位置。相对定位完成的过程是首先按static(float)方式生成一个元素(并且元素像层一样浮动了起来),然后相对于以前的位置移动,移动的方向和幅度由left、right、top、bottom属性确定,偏移前的位置保留不动(其他元素依照偏移前的元素位置往后进行排列)。
      3. 固定定位: position: fixed 表示固定定位,与absolute定位类型类似,但它的相对移动的坐标是视图(屏幕内的网页窗口)本身。由于视图本身是固定的,它不会随浏览器窗口的滚动条滚动而变化,除非你在屏幕中移动浏览器窗口的屏幕位置,或改变浏览器窗口的显示大小,因此固定定位的元素会始终位于浏览器窗口内视图的某个位置,不会受文档流动影响。
  • 掌握float的相关知识: 浮动模型,让块状元素并排显示
  • 掌握基本的布局方式
    • 块级元素居中显示: margin: 0 auto 如果想要设置距离顶端的边距的话,可以设置为margin: 80px auto auto,这里必须要写成top|(left and right)|bottom的形式才可以。这里还有一个问题,就是当浏览器宽度缩小时,会形式左右的滚动条,而文字本身不会自适应。**解决方法: 将width: 600px替换为max-width: 600px,尤其是移动设备上的显示尤其重要。
    • 盒子模型的宽度: box-sizing: border-box会使得边框和内边距不再增加整个盒子的宽度,这能让我们更好的控制盒子的大小。
    • position: 太多了,详见这里!
    • inline-block与float: 它们都可以用来创建多个网格铺满浏览器,不过float的方式更加困难一点,需要在后面清楚浮动才可以。
  • 了解Grid、Flexbox等布局方式: 新的flexbox布局模式被用来重新定义CSS中的布局方式。很遗憾的是最近规范变动过多,导致各个浏览器对它的实现也有所不同。

React文档翻译 - QuickStart

发表于 2017-10-19 | 分类于 翻译

React 文档翻译

安装

React非常的灵活,你可以在许多项目中都使用它。你可以利用它创建新的app,你也可以逐渐地将它引入一个已有的代码库,而不用进行重写。

以下是一些我们开始的途径:

  • 尝试React
  • 创建一个新的App
  • 将React加入现有的App

尝试React

如果你只是想随便玩玩React,那么你可以使用CodePen,你可以尝试以这个简单的Hello World小程序作为开始。你不需要安装任何东西,直接试着修改它,看看运行结果就行了。


创建一个新的App

Create React App是创建一个新的React单页应用的最好的方式。它可以帮你搭建你的开发环境,这样你就可以使用那些最新的Javascript特性了。它提供了非常棒的开发体验,并且会优化你的最终生产版本。
PS: 你需要安装Node.js,并且版本>=6。

1
2
3
4
5
npm install -g create-react-app
create-react-app my-app
cd my-app
npm start

Create React App并不处理后端逻辑或者数据库,它仅仅创建了一个前端的构建pipeline,所以你可以结合任何的后端技术来使用它。它在底层使用了Babel以及Webpack这样的构建工具,但是并不需要你做任何额外的配置。

当你准备好将App部署到生产环境时,你可以运行npm run build来在build文件夹中创建一个优化过的版本。你可以通过以下链接学习更多关于Create React App的知识:

  1. 官方文档
  2. 用户指南

将React加入现有的App

要开始使用React,你不必重写你的应用程序。

我们建议将React加入到你的应用的某个小部分中去,例如一个独立的小控件,这样你可以知道它是否能很好的配合你的应用。

虽然React可以脱离build pipeline而使用,但是我们强烈推荐你能搭建这样一个环境,这会使你变得更加高效。如今典型的build pipeline包含了以下的几个部分:

  • 包管理器(package manager): 例如Yarn或者npm,它使得你可以使用大量同生态圈的第三方的包,并且可以轻松地对它们进行安装或升级。
  • 模块打包器(bundler): 例如webpack或者Browserify,它使你可以写出模块化的代码,并且将它们整合到一个个小的包中去,来优化载入时间。
  • 编译器(compiler): 例如Babel,它使得你写的最新的Javascript也能运行在旧的浏览器上。

安装Rea

我们推荐使用Yarn或者npm来管理前端的依赖。如果你刚刚接触包管理器,那么Yarn的文档将会是一个比较好的学习材料。

要使用Yarn安装React,执行以下命令:

1
2
yarn init
yarn add react react-dom

要使用npm安装React,执行以下命令:

1
2
npm init
npm install --save react react-dom

PS: Yarn和npm都是从npm的网站那里下载包的。

激活ES6以及JSX

我们推荐结合Babel使用React,这使得你可以在Javascript代码中使用ES6以及JSX。ES6是一套新的Javascript标准,它使得开发更加简洁。JSX是Javascript一个扩展,它可以与React配合的非常完美。

Babel安装说明解释了如何在不同的构建环境中配置Babel,你需要确保你安装了babel-preset-react以及babel-preset-env,并且在你的.babelrc配置文件中激活了它们,这样你就可以继续了。

使用ES6和JSX的Hello World

我们推荐你使用webpack或者Browserify,这样你可以写出模块化的代码,并且将它们整合到一个个小的包中去,来优化载入时间。

最简单的React例子就像这样:

1
2
3
4
5
6
7
import React from 'react';
import ReactDom from 'react-dom';
ReactDom.render(
<h1>Hello, world!</h1>,
document.getElementById('root')
);

这段代码渲染了一个id为root的DOM元素,所以你需要在你的HTML文件某处添加这样的一行代码: <div id="root"></div>。

相似地,你也可以使用React渲染你使用其他Javascript UI库写的现有App中的DOM元素。

开发版本与生产版本

默认情况下,React包含了许多有用的警告。这些警告在开发过程中十分有用。

但是,它们使得React的开发版本(development version)过大也过慢,所以在部署时,你需要使用生产版本(production version)。

了解如何辨别你的网站正使用正确版本的React,并且通过以下途径有效地配置生产build过程:

  • 使用Create React App
  • 使用Single-File Builds
  • 使用Brunch
  • 使用Browserify
  • 使用Rollup
  • 使用webpack

使用CDN

如果你不想要使用npm来管理客户端的包,react和react-dom也提供了CDN的版本:

1
2
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

上面的版本是用来为开发服务的,并不适合生产版本,简化并优化过的版本可以通过以下链接获得:

1
2
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>

如果需要加载特定版本的react和react-dom,可以用版本号替换掉链接中的16。

如果你是使用Bower的,那么React可以通过react包获得。

为什么需要crossorigin属性?

如果你通过CDN来使用React,那么我们推荐你将crossorigin属性按照如下设置:

1
<script crossorigin src="..."></script>

我们也推荐你检验你所使用的CDN设置了Access-Control-Allow-Origin: *的HTTP header:

这使得你在使用React 16或以后的版本的时候会有一个更好的错误处理的体验。

Hello World

开始学习React的最简单的方法就是使用我们在CodePen上给出的这个Hello World Example。你不需要安装任何东西,你只要在另一个标签页中打开它,并且跟着我们一起研究这个例子就可以了。如果你还是想使用一个本地的开发环境,请查看上一章的具体内容。

最简单的React example就像下面这样👇:

1
2
3
4
ReactDom.render(
<h1>Hello, world!</h1>,
document.getElementById('root')
);

这段代码渲染了一个内容为“Hello, world!”的标题。

接下来的几个部分将逐渐地引导你开始使用React。我们将会深入研究React应用的基础构建单元: 元素(elements)和组件(components)。一旦你掌握了它们,你就可以使用这些可复用的小“砖瓦”来构建复杂的应用程序了。


关于Javascript的小提示

React是一个Javascript库,所以这假设你对Javascript语言有一个基本的理解。如果你感觉自己并不是那么自信,那么你可以参考这个教程,这样你就可以更轻松地跟上我们的节奏了。

我们也将在例子中使用一些ES6语句,我们会试着用保守一点的方式去使用它,因为它相对来说还是比较新的,但是我们鼓励你熟悉一下这些知识: 箭头函数,类,模板字符串,let,const。

JSX简介

观察下面的变量声明方式:

1
const element = <h1>Hello, world!</h1>;

这个有趣的标签既不是字符串也不是HTML。

这样的语句叫做JSX,是Javascript的一个句法扩展。我们推荐结合React使用它来描述UI应该呈现的样子。JSX也许会让你联想到一个模板语言,不过它是拥有完整的Javascript功能的。

JSX产生了React “elements”,我们将会在下一节探索它们是如何被渲染为DOM的。下面的内容,可以让你学到JSX的基础知识,让你快速起步。

在JSX中嵌入表达式

你可以通过将代码包裹在花括号内,在JSX中嵌入任何的Javascript表达式。

比方说,2+2,user.firstName,或者formatName(user)都是有效的表达式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function formatName(user) {
return user.firstName + ' ' + user.lastName;
}
const user = {
firstName: 'Louis',
lastName: 'Melo'
};
const element = (
<h1>
Hello, {formatName(user)}!
</h1>
);
ReactDOM.render(
element,
document.getElementById('root')
);

我们将JSX分成了好几行来提高了它的可阅读性。虽然这不是必要的,但是当这样做的时候,我们也推荐你使用括号将它包裹住,防止 自动补全分号 的陷阱。

JSX也是一个表达式

在编译过后,JSX表达式会变成正常的Javascript对象。

这意味着,你可以在if语句以及for循环中使用JSX,并可以将它赋值给变量,将它作为参数接收,也可以通过方法返回它:

1
2
3
4
5
6
function getGreeting(user) {
if (user) {
return <h1>Hello, {formatName(user)}!</h1>;
}
return <h1>Hello, Stranger.</h1>;
}

使用JSX声明属性

你可以使用引号来将字符文字声明为属性:

1
const element = <div tabIndex="0"></div>;

你也可以使用花括号来嵌入Javascript表达式作为属性:

1
const element = <img src={user.avatarUrl}></img>;

在使用Javascript表达式和花括号作为属性的时候,不要再添加引号。你应该使用这二者的其一,而不是同时使用它们两个。

使用JSX声明子节点(children)

如果一个标签是空标签,你可以直接以/>作为结尾:

1
const element = <img src={user.avatarUrl} />;

JSX标签也有可能包含子节点:

1
2
3
4
5
6
const element = (
<div>
<h1>Hello!</h1>
<h2>Good to see you here.</h2>
</div>
);

JSX防止注入攻击

将用户输入嵌入在JSX是非常安全的:

1
2
3
const title = response.potentiallyMaliciousInput;
// This is safe:
const element = <h1>{title}</h1>;

默认情况下,React DOM在渲染之前就会(???)任何嵌套在JSX中的值,因此它保证了你永远不能注入任何没有在你的应用中显示声明的代码。一切都会在渲染之前被转换成字符串,这也有效阻止了XSS攻击(cross-site-scripting)。

JSX意味着对象

Babel将JSX编译为对React.createElement()方法的调用。

下面的这两个例子是完全一致的:

1
2
3
4
5
const element = (
<h1 className="greeting">
Hello, world!
</h1>
);
1
2
3
4
5
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);

React.createElement()帮助你执行了一些bug检查,但是更重要的是,它创建了如下的一个对象:

1
2
3
4
5
6
7
8
// 注意:这个结构已被简化
const element = {
type: 'h1',
props: {
className: 'greeting',
children: 'Hello, world!'
}
};

这样的对象被称为“React elements”。你可以将它们认作是对于你想要在屏幕上看到的东西的描述。React会读取这些对象,然后用它们来构建DOM,并且保持它们的状态更新。

我们将在下一节研究如何将React elements渲染到DOM。

Tip:
在Atom编辑器中,可以搜索language-babel来使得编辑器支持对于ES6和JSX的高亮显示。

渲染元素

元素是React Apps的最小构建单元。

一个元素描述了你想要在屏幕上看到的东西:

1
const element = <h1>Hello, world!</h1>;

与浏览器DOM元素不同的是,React elements就是普通的对象,其创建也十分简单。React DOM关心的是更新DOM以匹配React elements。

请注意: 这里有可能会造成与“components”概念的混淆,我们将在下一节介绍components,elements其实是构成components的东西。


将元素渲染进DOM中

假定有一个<div>在你的HTML文件的某处吧:

1
<div id="root"></div>

我们将这个称为一个“root” DOM节点,因为它其中的所有东西都将由React DOM管理。

只用React构建的应用通常只有一个单一的根DOM节点。如果你是将React整合进一个已有的app,那么你可以有任意多个隔离的根DOM节点。

要渲染一个React element为一个根DOM节点,需要将两者都传递给ReactDOM.render()方法:

1
2
3
4
5
const element = <h1>Hello, world</h1>;
ReactDOM.render(
element,
document.getElementById('root')
);

更新被渲染的元素

React elements是无法修改的(immutable)。一旦你创建了一个元素,你就不能改变它的子节点或是属性。一个元素就像一部电影里的单独一帧: 它代表了某个特定时间点的用户界面。

根据我们到目前为止学到的知识,更新用户界面的唯一方法是: 创建一个新的元素并将它传递给ReactDOM.render()方法。

思考下面这个时钟的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function tick() {
const element = (
<div>
<h1>Hello, world!</h1>
<h2>It is {new Date().toLocaleTimeString()}.</h2>
</div>
);
ReactDOM.render(
element,
document.getElementById('root')
);
}
setInterval(tick, 1000);

每隔一秒钟,setInterval()的回调方法就会调用一次ReactDOM.render()方法。

注意,在实践中,大多数的React apps都只调用一次ReactDOM.render()方法,在下一节我们将会学习这些代码是如何被封装(encapsulated)到有状态的components中去的。


React只会进行有必要的更新

React DOM会将元素及其子节点与之前的相比较,并且只会对DOM进行有必要的更新,将DOM变到所需要的状态。

这一点,你可以从上一个例子中看出来,利用浏览器工具,尽管我们每隔一秒都创建一个描述整个UI树的元素,但是只有内容因更新DOM而发生改变的文本节点发生了变化。

根据我们的经验,相比于思考如何随着时间改变UI,思考在给定时刻UI所要呈现的样子会消除非常多的bug。

Components and Props

Components使你能够将UI分成独立且可重复利用的小块,并且独立地思考每一小块UI。

从概念上讲,components就像是JavaScript中的方法,它们能够接受任意的输入(这里被称为props),并且返回描述了在屏幕上呈现何物的React elements。


Functional and Class Components

定义一个component的最简单的方法就是编写一个Javascript方法:

1
2
3
function Welcome(props){
retrun <h1>Hello, {props.name}</h1>;
}

这个方法就是一个有效的React component,因为它接受了一个单独的props,并且返回了一个React element,我们将这种组件叫做方法性组件(functional components),因为它在字面上就是一个JavaScript方法。

你也可以使用一个ES6 Class来顶一个component:

1
2
3
4
5
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}

从React的视角来看,以上的两个components是同等的。

Classes具有一些额外的特性,我们将在下一章进行讨论。在那之前,我们将使用functional components,因为它比较简单明了。


Rendering a component

之前,我们只碰到过表示DOM标签的React elements:

1
const element = <div />;

然而,elements也可以表示用户定义的components:

1
const element = <Welcome name="Snow" />;

当React见到一个表示用户定义的componet的element时,它就将JSX的属性以一个单独的对象传递给这个component,我们将这个对象叫做props。

举个例子,这段代码将会打印 Hello, snow:

1
2
3
4
5
6
7
8
9
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
const element = <Welcome name="snow" />;
ReactDOM.render(
element,
document.getElementById('root')
);

让我们重新看一下这个例子中发生了什么:

  1. 我们使用 <Welcome name="snow" /> 这个元素,调用了 ReactDOM.render() 方法
  2. React以 {name: 'snow'} 作为props,调用了 Welcome 组件
  3. 我们的 Welcome 组件返回了 <h1>Hello, snow</h1> 元素作为结果
  4. ReactDOM快速地更新了DOM
12
Louis Melo

Louis Melo

平凡的人生千篇一律,而不凡的人生万里挑一。

19 日志
5 分类
© 2018 Louis Melo
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.2