Miniflux 需要自己部署在服务器上,它提供了多种安装方法,最简单的就是 docker 方式,避免手动配置环境及数据库等步骤。
我的系统环境:Ubuntu server 20.04
Miniflux 官网:https://miniflux.app/
GitHub 主页:https://github.com/miniflux/v2
这里介绍通过 docker compose 安装的方法,关于 docker 环境的部署参考我的教程:https://blog.niekun.net/archives/2742.html#title-1
新建 miniflux 文件夹用来放置相关配置:
mkdir miniflux
cd miniflux
然后建立 docker-compose.yml 配置文件,内容如下:
version: '3.4'
services:
miniflux:
image: ${MINIFLUX_IMAGE:-miniflux/miniflux:latest}
container_name: miniflux
restart: always
ports:
- "18080:8080"
depends_on:
- db
environment:
- DATABASE_URL=postgres://miniflux:secret@db/miniflux?sslmode=disable
- BASE_URL=https://miniflux.your.domain
- RUN_MIGRATIONS=1
- CREATE_ADMIN=1
- ADMIN_USERNAME=admin
- ADMIN_PASSWORD=test123
- DEBUG=1
# Optional health check:
# healthcheck:
# test: ["CMD", "/usr/bin/miniflux", "-healthcheck", "auto"]
db:
image: postgres:latest
container_name: postgres
restart: always
environment:
- POSTGRES_USER=miniflux
- POSTGRES_PASSWORD=secret
volumes:
- miniflux-db:/var/lib/postgresql/data
healthcheck:
test: ["CMD", "pg_isready", "-U", "miniflux"]
interval: 10s
start_period: 30s
volumes:
miniflux-db:
其中需要自行根据实际情况修改一些内容:
敏感的环境变量值可以单独放在同配置文件路径下的 .env
文件中,上面的 docker 安装教程中有介绍。
注意 DATABASE_URL 地址中的 postgres 用户名密码对应于 POSTGRES_USER 和 POSTGRES_PASSWORD 的值,需要保持一致。
然后就可以启动容器:
docker-compose up -d
首先如果需要解析到二级域名下,先要在 ns 服务端添加二级域名的 A 记录,然后才能正常解析 url。
使用域名访问 miniflux 服务,需要通过主机使用的反代软件配置解析,我服务器使用的是 nginx,下面介绍配置方法。
给 nginx 配置添加如下内容:
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name miniflux.your.domain;
include my-server/ssl;
location / {
proxy_pass http://127.0.0.1:18080;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_http_version 1.1;
}
}
以上配置需要根据实际情况修改监听端口及 domain,由于之前配置的 miniflux 的映射端口是 18080,所以反代到本地的对应端口即可。
以上 miniflux 和 nginx 反代配置完成后应该就可以访问 https://miniflux.your.domain 了。用户名密码就是 docker 配置文件中定义的管理员账户及密码:
如果某一篇文章不显示全文,只需要点击文章顶部的 download 按钮即可加载全文:
miniflux 提供了丰富的 api 接口可供二次开发使用,通过简单的请求就可以获取到文章的各种信息,返回数据为 json 格式。
官方 api 文档参考:https://miniflux.app/docs/api.html
]]>这里提供的方法仅供用于前端开发测试。
首先登陆移动端网站:https://m.jd.com
右键点击 inspect 进入开发者界面,然后点击网页右上角的登录键根据提示需要输入账户信息登录。
登录成功后在开发栏切换到 network 选项,并且在 filter 中输入 log.gif
过滤出需要的信息:
选中第一个 log 文件,点击 header 栏,在其中找到 cookie 项:
复制其中的内容到剪贴板。
找到开发页面下方的 console 栏,我们通过简单的 js 脚本提取出上面 cookie 的有效内容:
var c = '这里粘贴复制好的 cookie';
var value = c.match(/pt_pin=.+?;/) + c.match(/pt_key=.+?;/);
copy(value);
执行完成后此时我们的剪贴板里就已经复制好了 cookie 信息。
手机端可以通过解包 app 来提取 cookie,具体可以参考我的 js 脚本:https://github.com/nie11kun/config-rules/blob/master/Shadowrocket/get_jdcookie.js
]]>一些 components 并不直接知道他们的 children 具体是什么。在 Sidebar 或 Dialog 中可以体现,它们只是一个 box 容器,他的内容可能是变化的。
这一类的 components 推荐直接使用 children props 来直接表示 parent 传递给他们的 elements:
const FancyBorder = props => {
return (
<div className={`FancyBorder FancyBorder-${props.color}`}>
{props.children}
</div>
);
}
props.children
表示所有在调用此 component 时放在其元素中的内容。
然后我们创建一个 component 来调用上面的 FancyBorder:
const WelcomeDialog = () => {
return (
<FancyBorder color='red'>
<h1 className='Dialog-title'>Welcome</h1>
<p className='Dialog-message'>
thank you for check this page.
</p>
</FancyBorder>
)
}
任何在 FancyBorder 标签中的内容都会作为 children prop 传入 FancyBorder 中,然后通过 props.children 进行渲染。
但通常情况下我们的 compenent 可能会有多个 “入口”,此时我们就需要定义自己的 convention 声明来替代 children:
const React = require('react')
const ReactDOM = require('react-dom')
const SplitPane = (props) => {
return (
<div className='SplitPane'>
<div className='Splitpane-left'>
{props.left}
</div>
<div className='Splitpane-right'>
{props.right}
</div>
</div>
)
}
const Contacts = () => {
return (
<div>
<h1>marco</h1>
</div>
)
}
const Chat = () => {
return (
<div>
<p>this is a test</p>
</div>
)
}
const App = () => {
return (
<SplitPane
left={
<Contacts />
}
right={
<Chat />
}
/>
)
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
可以看到,我们可以将 component 像其他属性一样传递,这里 Contacts 和 Chat 就作为 left 和 right 的数据传递给 SplitPane 使用。
一些场景下,我们会将某个 component 看作另一个 component 的特殊情况,例如上面示例中的 WelcomeDialog 可以看做是 Dialog 的特殊情况。在 react 中我们通常通过给一个 generic 泛用的 component 配置 props 的方式构成另一个 special component:
const React = require('react')
const ReactDOM = require('react-dom')
const FancyBorder = props => {
return (
<div className={`FancyBorder FancyBorder-${props.color}`}>
{props.children}
</div>
);
}
const Dialog = props => {
return (
<FancyBorder color='red'>
<h1 className='Dialog-title'>
{props.title}
</h1>
<p className='Dialog-message'>
{props.message}
</p>
</FancyBorder>
)
}
const WelcomeDialog = () => {
return (
<Dialog
title='Welcome'
message='welcome to this party'
/>
)
}
ReactDOM.render(
<WelcomeDialog />,
document.getElementById('root')
);
我们也可以通过 class 的方式定义 component:
const React = require('react')
const ReactDOM = require('react-dom')
const FancyBorder = props => {
return (
<div className={`FancyBorder FancyBorder-${props.color}`}>
{props.children}
</div>
);
}
const Dialog = props => {
return (
<FancyBorder color='red'>
<h1 className='Dialog-title'>
{props.title}
</h1>
<p className='Dialog-message'>
{props.message}
</p>
{props.children}
</FancyBorder>
)
}
class SignUpDialog extends React.Component {
constructor(props) {
super(props);
this.handleChangle = this.handleChangle.bind(this);
this.handleSignup = this.handleSignup.bind(this);
this.state = {login: ''};
}
handleChangle(e) {
this.setState({login: e.target.value});
}
handleSignup() {
alert(`welcome guys, ${this.state.login}`)
}
render () {
return (
<Dialog
title='Sport game'
message='welcome to this game'
>
<input value={this.state.login} onChange={this.handleChangle} />
<button onClick={this.handleSignup}>Sign me up</button>
</Dialog>
)
}
}
ReactDOM.render(
<SignUpDialog />,
document.getElementById('root')
);
这里我们使用了自定义 props 和 children props 来构建了 SignUpDialog component,根据实际场景灵活使用。
props 和 composition 提供了灵活性来自定义一个 component 的样式/行为,且更加安全和精确。需要注意的是 component 可能会接收到 arbitrary 抽象 props,包括原始二进制数据/elements/functions 等。
]]>下面创建一个 temperature calculator 温度计算器来判断在一个给定的温度下,水是否会沸腾。
首先我们创建一个 BoilingVerdict
component作为沸腾裁决器,它接受 celsius 摄氏温度作为一个 prop,然后输出是否足够使水沸腾:
const BoilingVerdict = (props) => {
if (props.celsius >= 100) {
return <p>The Water would boil.</p>
}
return <p>The water would not boil.</p>
}
然后我们创建一个 Calculator
compenent,它会渲染一个 input 元素用来输入温度数据,且将数据存储在 this.state.temprature
中:
const React = require('react')
const ReactDOM = require('react-dom')
const BoilingVerdict = (props) => {
if (props.celsius >= 100) {
return <p>The Water would boil.</p>
}
return <p>The water would not boil.</p>
}
class Calculator extends React.Component {
constructor(props) {
super(props);
this.state = {temperature: ''};
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
this.setState({temperature: e.target.value});
}
render() {
const temperature = this.state.temperature;
return (
<fieldset>
<legend>Enter temperature in celsius</legend>
<input
value={temperature}
onChange={this.handleChange}
/>
<BoilingVerdict celsius={parseFloat(temperature)} />
</fieldset>
);
}
}
ReactDOM.render(
<Calculator />,
document.getElementById('root')
);
上面的示例中使用了一个 fieldset 来定义一个块,legend 定义了这个块的抬头信息,效果如下:
上一章节中我们详细介绍了如何构建一个 controlled component 可以参考。
现在我们有一个新的需求,在原有 celsius 摄氏输入的同时增加一个 Fahrenheit 华氏输入栏,同时让它们两者保持数据同步。
首先我们从 Calculator
中拆解出一个 TempratureInput
component,将会给其增加一个 scale props,它可以是 "c" 或 "f" 来区分摄氏和华氏:
const scaleNames = {
c: 'Celsius',
f: 'Fahrenheit'
}
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.state = {temperature: ''};
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
this.setState({temperature: e.target.value});
}
render() {
const temperature = this.state.temperature;
const scale = this.props.scale;
return (
<fieldset>
<legend>Enter temperature in {scaleNames[scale]}:</legend>
<input
value={temperature}
onChange={this.handleChange}
/>
</fieldset>
);
}
}
class Calculator extends React.Component {
render() {
return (
<div>
<TemperatureInput scale='c' />
<TemperatureInput scale='f' />
</div>
);
}
}
修改后,我们有了两个 fieldset,分别用来输入摄氏和华氏温度值,我们建立了一个 scaleNames object 用来简化 scale 属性的定义,只需要通过调用 object props 的方式即可引用我们想要的文本全称,这里使用了 ES6 的 Computed Property Names 预定义属性名,通过 scaleNames[scale]
动态调用其属性。此时两个 input 是相互隔离的,它们的数据不能相互访问。
下面我们实现两个 component 之间的数据互通。
首先我们建立两个 function 用来实现 celsius 和 Fahrenheit 之前的互相转换:
const toCelsius = fahrenheit => (fahrenheit - 32) * 5 / 9;
const toFahrenheit = celsius => (celsius * 9 / 5) + 32;
这里我使用了 ES6 的简化写法,省略了 return 等符号。
下面我们编写另一个 function 接收两个数据,一个是 string 字符串和一个 function,用来将输入的 temperature 数据转换并返回转换好的字符串,当输入的 string 不是无法转换为数字时会返回一个空字符串,可以转换时将精度设置为 3 位小数:
const tryConvert = (temperature, convert) => {
const input = parseFloat(temperature);
if (Number.isNaN(input)) {
return '';
}
const output = convert(input);
const rounded = Math.round(output * 1000) / 1000;
return rounded.toString();
}
使用上面的 function 如果执行 tryConvert('abc', toCelsius)
会返回空字符串,如果执行 tryConvert('10.22', toFahrenheit)
会返回 "50.396"
。
下面我们将 TempratureInput 中的 state 提升到 parent component。
当前的代码中,每个 TempratureInput component 各自控制它们的 state 且相互隔离,但是我们希望这两个 inputs 能够共享数据且同步更新,例如当我修改了 celsius input 后 Fahrenheit input 会自动更改为对应 celsius input 的结果。
在 react 中,共享 state 通过将其移动到这些 components 最近层级中的 parent component 中,叫做 lifting state up,下面我们将 TempratureInput 中的本地 state 移动到 Calculator 中。如果 Calculator 含有 shared state,它就成为了其下级 components 的 source of truth 可信来源,它可以管理他的下级 components 保持数据一致性,因此两个 TempratureInput 的 props 都来自于同一个 Calculator,所以他们的 inputs 将会保持一致同步.
下面我们逐步实现这个过程,首先替换 TemperatureInput component 中的 this.state.temperature
为 this.props.temperature
,稍后我们将在 Calculator 中定义它:
render() {
// Before: const temperature = this.state.temperature;
const temperature = this.props.temperature;
// ...
我们知道 props 是只读的,在之前我们的 temperature 存储在本地的 state 中,然后通过 setState 来修改它,现在 temperature 来自 parent component,所以 TemperatureInput 无法直接控制它。
在 react 中,通常的解决方法是使 component 为 controlled,就像 <input>
元素可以接受 value 和 onChange props 属性,我们可以自定义使 TempratureInput 接受 temperature 和 onTemperatureChange 属性。从而当它需要更新他的 temperature 数据时,就可以通过调用 this.props.onTemperatureChange
:
handleChange(e) {
this.props.onTemperatureChange(e.target.value);
}
注意这里的命名时自定义的,我们可以定义任意的名称作为 component 的 props 属性名称。
onTemperatureChange 属性将会在 Calculator 中通 temperature 相关联起来,它将会修改 Calculator 中的 state 同时重新渲染两个 inputs 元素.
下面我们专注于 Calculator component,我们需要存储当前 input 的 temperate 和 scale 数据到其 state 中,这里的 state 来自于之前从 TemperatureInput 中 lifting up 的,同时它将会同时作为两个 inputs 的可信来源,这两个 state 可以同时提供足够的数据来同时 render 两个 inputs,例如我们在 celsius input 中输入了 37,则 Calculator 的 state 应该是这样的:
{
temperature: '37',
scale: 'c'
}
如果我们在 Fahrenheit input 中输入了 212,则 state 应该是这样的:
{
temperature: '212',
scale: 'f'
}
我们不需要同时单独存储两个 input 的数据,只需要存储最后一个 input 的数据即可,scale 中记录了具体是哪个 input 的来源。最后两个 inputs 中的数据是同步的,因为他们的值都是来自于同一个 state:
class Calculator extends React.Component {
constructor(props) {
super(props);
this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this);
this.state = {temperature: '', scale: 'c'};
}
handleCelsiusChange(temperature) {
this.setState({temperature: temperature, scale: 'c'});
}
handleFahrenheitChange(temperature) {
this.setState({temperature: temperature, scale: 'f'});
}
render() {
const scale = this.state.scale;
const temperature = this.state.temperature;
const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;
const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;
return (
<div>
<TemperatureInput
scale='c'
temperature={celsius}
onTemperatureChange={this.handleCelsiusChange} />
<TemperatureInput
scale='f'
temperature={fahrenheit}
onTemperatureChange={this.handleFahrenheitChange} />
<BoilingVerdict celsius={parseFloat(celsius)} />
</div>
);
}
}
此时无论我们在 celsius 或 Fahrenheit 中输入数据都会同时更新两个 input 的值。
此示例完整代码如下:
const React = require('react')
const ReactDOM = require('react-dom')
const BoilingVerdict = (props) => {
if (props.celsius >= 100) {
return <p>The Water would boil.</p>
}
return <p>The water would not boil.</p>
}
const scaleNames = {
c: 'Celsius',
f: 'Fahrenheit'
}
const toCelsius = fahrenheit => (fahrenheit - 32) * 5 / 9;
const toFahrenheit = celsius => (celsius * 9 / 5) + 32;
const tryConvert = (temperature, convert) => {
const input = parseFloat(temperature);
if (Number.isNaN(input)) {
return '';
}
const output = convert(input);
const rounded = Math.round(output * 1000) / 1000;
return rounded.toString();
}
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
this.props.onTemperatureChange(e.target.value);
}
render() {
const temperature = this.props.temperature;
const scale = this.props.scale;
return (
<fieldset>
<legend>Enter temperature in {scaleNames[scale]}:</legend>
<input
value={temperature}
onChange={this.handleChange}
/>
</fieldset>
);
}
}
class Calculator extends React.Component {
constructor(props) {
super(props);
this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this);
this.state = {temperature: '', scale: 'c'};
}
handleCelsiusChange(temperature) {
this.setState({temperature: temperature, scale: 'c'});
}
handleFahrenheitChange(temperature) {
this.setState({temperature: temperature, scale: 'f'});
}
render() {
const scale = this.state.scale;
const temperature = this.state.temperature;
const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;
const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;
return (
<div>
<TemperatureInput
scale='c'
temperature={celsius}
onTemperatureChange={this.handleCelsiusChange} />
<TemperatureInput
scale='f'
temperature={fahrenheit}
onTemperatureChange={this.handleFahrenheitChange} />
<BoilingVerdict celsius={parseFloat(celsius)} />
</div>
);
}
}
ReactDOM.render(
<Calculator />,
document.getElementById('root')
);
处理流程为:
返回内容如下:
{
"current_user_url": "https://api.github.com/user",
"current_user_authorizations_html_url": "https://github.com/settings/connections/applications{/client_id}",
"authorizations_url": "https://api.github.com/authorizations",
"code_search_url": "https://api.github.com/search/code?q={query}{&page,per_page,sort,order}",
"commit_search_url": "https://api.github.com/search/commits?q={query}{&page,per_page,sort,order}",
"emails_url": "https://api.github.com/user/emails",
"emojis_url": "https://api.github.com/emojis",
"events_url": "https://api.github.com/events",
"feeds_url": "https://api.github.com/feeds",
"followers_url": "https://api.github.com/user/followers",
"following_url": "https://api.github.com/user/following{/target}",
"gists_url": "https://api.github.com/gists{/gist_id}",
"hub_url": "https://api.github.com/hub",
"issue_search_url": "https://api.github.com/search/issues?q={query}{&page,per_page,sort,order}",
"issues_url": "https://api.github.com/issues",
"keys_url": "https://api.github.com/user/keys",
"label_search_url": "https://api.github.com/search/labels?q={query}&repository_id={repository_id}{&page,per_page}",
"notifications_url": "https://api.github.com/notifications",
"organization_url": "https://api.github.com/orgs/{org}",
"organization_repositories_url": "https://api.github.com/orgs/{org}/repos{?type,page,per_page,sort}",
"organization_teams_url": "https://api.github.com/orgs/{org}/teams",
"public_gists_url": "https://api.github.com/gists/public",
"rate_limit_url": "https://api.github.com/rate_limit",
"repository_url": "https://api.github.com/repos/{owner}/{repo}",
"repository_search_url": "https://api.github.com/search/repositories?q={query}{&page,per_page,sort,order}",
"current_user_repositories_url": "https://api.github.com/user/repos{?type,page,per_page,sort}",
"starred_url": "https://api.github.com/user/starred{/owner}{/repo}",
"starred_gists_url": "https://api.github.com/gists/starred",
"user_url": "https://api.github.com/users/{user}",
"user_organizations_url": "https://api.github.com/user/orgs",
"user_repositories_url": "https://api.github.com/users/{user}/repos{?type,page,per_page,sort}",
"user_search_url": "https://api.github.com/search/users?q={query}{&page,per_page,sort,order}"
}
可以通过它里面给的使用方法来获取到相应的内容,例如可以通过:https://api.github.com/users/{user} 获取到某个用户相关数据,如下是我的个人相关数据:
{
"login": "nie11kun",
"id": 11830603,
"node_id": "MDQ6VXNlcjExODMwNjAz",
"avatar_url": "https://avatars.githubusercontent.com/u/11830603?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/nie11kun",
"html_url": "https://github.com/nie11kun",
"followers_url": "https://api.github.com/users/nie11kun/followers",
"following_url": "https://api.github.com/users/nie11kun/following{/other_user}",
"gists_url": "https://api.github.com/users/nie11kun/gists{/gist_id}",
"starred_url": "https://api.github.com/users/nie11kun/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/nie11kun/subscriptions",
"organizations_url": "https://api.github.com/users/nie11kun/orgs",
"repos_url": "https://api.github.com/users/nie11kun/repos",
"events_url": "https://api.github.com/users/nie11kun/events{/privacy}",
"received_events_url": "https://api.github.com/users/nie11kun/received_events",
"type": "User",
"site_admin": false,
"name": "Marco Nie",
"company": "HJMT",
"blog": "https://niekun.net",
"location": "China",
"email": null,
"hireable": null,
"bio": "You are the company you keep.",
"twitter_username": null,
"public_repos": 88,
"public_gists": 2,
"followers": 4,
"following": 15,
"created_at": "2015-04-07T06:53:16Z",
"updated_at": "2021-03-15T08:45:12Z"
}
更多使用方法可以自己研究下。
]]>在 JavaScript 中我们通常使用 map method 来对一个 list 的每个元素进行操作:
const numbers = [1, 2, 3, 4, 5];
const double = numbers.map((number) => { return number * 2});
console.log(double)
//output:
//[ 2, 4, 6, 8, 10 ]
在 React 中对一个 list 的元素进行操作方法类似。
我们可以在 JSX 中通过大括号{}
来建立一个 elements 的集合,下面示例中我们将 map 的返回定义为 <li>
元素并赋值给 listItems:
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) => <li>{number}</li>)
ReactDOM.render(
<ul>{listItems}</ul>,
document.getElementById('root')
);
注意在 render 中我们将 listItems 放在 <ul>
元素中。
通常情况下我们将 lists 放在一个 component 中:
const NumberList = (props) => {
const numbers = props.numbers;
const listItems = numbers.map((number) => <li>{number}</li>)
return (
<ul>{listItems}</ul>
)
}
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
<NumberList numbers={numbers}/>,
document.getElementById('root')
);
当运行以上代码时,在浏览器终端会有一个 warning 警告信息:Each child in a list should have a unique "key" prop.:
Key 是一个特殊的 string 字符串属性需要给创建的 list element 添加的。它可以用来定位 list 中的每个元素。
下面我们给 list item 添加 Key 字符串属性:
const NumberList = (props) => {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
<li key={number.toString()}>
{number}
</li>);
return (
<ul>{listItems}</ul>
);
}
添加后报警就会消除。
Key 可以帮助 React 识别哪个 item 修改过,被删除,被添加。以上示例中,我们在 map 中创建 item 时给其 key 属性,这样每个 item 可以有确切的属性值。
每个 list item 最好设置一个特殊的标识 key string 来区别于其他 items。最常用的就是使用数据中的 ID 作为 key:
const TodoItems = (props) => {
const todos = props.todos;
const listItems = todos.map((todo) =>
<li key={todo.id}>
{todo.text}
</li>
)
return (
<ul>{listItems}</ul>
);
}
const todos = [
{id: 1, text: '123'},
{id: 2, text: '456'}
];
ReactDOM.render(
<TodoItems todos={todos} />,
document.getElementById('root')
);
当没有特定的 ID 来作为标识时,作为最后的选择,可以使用 item 的 index 作为 key:
const TodoItems = (props) => {
const todos = props.todos;
const listItems = todos.map((todo, index) =>
<li key={index}>
{todo.text}
</li>
)
return (
<ul>{listItems}</ul>
);
}
如果 items 的顺序可能会发生变化的话,不推荐使用 index 作为 key 使用,因为可能对性能产生影响并且对 component 的 state 造成问题。如果没有定义确切的 key 给 items,React 默认会使用 index 作为 keys。
keys 是对应与一个数组的内容而言的,它并不能单独存在。例如我们要拆解上面的 NumberList,提取出 ListItem,则需要将 key 定义在 <ListItem />
元素中而不是 ListItem component 内部的 <li>
中:
const ListItem = (props) => {
return (
<li>{props.value}</li>
);
}
const NumberList = (props) => {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
<ListItem key={number.toString()} value={number}/>);
return (
<ul>{listItems}</ul>
);
}
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
<NumberList numbers={numbers} />,
document.getElementById('root')
);
如果写成下面模式就是错误的:
function ListItem(props) {
const value = props.value;
return (
<li key={value.toString()}>
{value}
</li>
);
}
数组中每个 items 使用的 key 必须是互相独立且不相同的,但并不需要在全局下互相独立。在两个单独的数组中可以,其元素可以使用相同的 key:
const React = require('react')
const ReactDOM = require('react-dom')
const Blog = (props) => {
const sideBar = (
<ul>
{props.posts.map((post) =>
<li key={post.id}>{post.title}</li>
)}
</ul>
);
const content = props.posts.map((post) =>
<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.content}</p>
</div>
);
return (
<div>
{sideBar}
<hr/>
{content}
</div>
);
}
const posts = [
{id: 1, title: 'Hello World', content: 'Welcome to learning React!'},
{id: 2, title: 'Installation', content: 'You can install React from npm.'}
];
ReactDOM.render(
<Blog posts={posts} />,
document.getElementById('root')
);
上面示例中,我们在 Blog component 中定义了两个 JSX,都创建了 list elements,每个元素的 key 使用了对应的 id 属性。在每个 list 内部 key 是互相独立的。可以看到不只是 <li>
元素可以加 key,只要通过 map 定义了一个 array 数组,就可以给每个元素加上 key 属性来互相独立识别。
key 是为了给 React 识别用的。它本身并不作为一个普通 prop 传给 components,也就是在 component 内部并不能使用这个 key 数据,如果想要在 component 中使用这个数据则需要单独定义一个其他 prop 来传入 key 数据:
const Post = (props) => {
return (
<li>
{props.id}: {props.title}
</li>
)
}
const Blog = (props) => {
const sideBar = (
<ul>
{props.posts.map((post) =>
<Post key={post.id} id={post.id} title={post.title} />
)}
</ul>
);
...
...
...
}
上面示例中,Post component 无法直接访问 key 的数据,所以我们在调用 Post 时单独定义一个 id 属性并赋值为 key 相同的数据,这样就间接的可以在 Post component 中通过 id 来读取 key 的数据。
在之前的 ListItem 示例中,我们声明了一个单独的 listItems 变量并在后续返回中将其放在 <ul>
中:
const NumberList = (props) => {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
<ListItem key={number.toString()} value={number}/>);
return (
<ul>{listItems}</ul>
);
}
JSX 支持嵌入任何的 JavaScript 表达式,只需要使用大括号包围即可,所以上面的代码可以修改为以下模式:
return (
<ul>
{numbers.map((number) =>
<ListItem key={number.toString()} value={number} />);}
</ul>
);
使用哪种方式来定义 JSX 取决于对应的使用场景,总的原则是要方便与代码阅读,逻辑清晰。需要注意的是如果 map()
method 中层级太复杂,可以考虑将其拆分为多个 components。
HTML 的 form element 和其他 DOM elements 有点区别,因为 form element 包含有一些内部 state 数据,例如下面的 html 示例包含一个 from 表格:
<form>
<label>
Name:
<input type="text" name="name">
</label>
<input type="submit" value="Submit">
</form>
以上示例中的 form 表格会有一个默认的 behavior 动作,那就是当用户点击 submit 按钮时会打开一个新页面。如果你不需要这个默认行为,同时需要提取 input 的信息时,需要在 submit event 事件发生时对其使用 preventDefault()
method,标准的实现方法是通过 controlled components 可控构件 来处理。
在 html 中,form 的元素如:<input>
, <textarea>
, 和 <select>
都有他们自己的 state 且随着用户输入信息而自动更新。在 react 中,可变的 state 存储在 component 的 state property 中且只能通过 setState()
更新。
我们可以将 from 元素的 state 和 component 的 state 合并起来作为唯一的数据来源,这样 component 既可以渲染 form 也可以控制 form 中的输入信息。一个 input 输入信息受 react component 控制的 form element 叫做 controlled component。
如下示例中,我们构建一个 controlled component 来记录用户 input 的内容:
const React = require('react')
const ReactDOM = require('react-dom')
class NameForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: ''};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
};
handleChange(event) {
this.setState({value: event.target.value});
}
handleSubmit(event) {
alert(`a name has been submited: ${this.state.value}`);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input type="text" value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
}
ReactDOM.render(
<NameForm />,
document.getElementById('root')
);
我们将 input 的 value 属性定义为 this.state.value
的值,这里显示的永远是当前 state value 的值。当 handleChange 被触发时,会将当前用户输入的内容更新到 state value 中,然后触发 render 更新 form。
通过 controlled component 可以使 form 中 input 的内容受控于 state,这样我们就可以操作输入的数据用于其他任何地方了。
在 html 中我们使用 textarea 来定义一段文本区域:
<textarea>
Hello there, this is some text in a text area
</textarea>
在 react 中,类似于上面的 input 标签,我们将文本内容放在 value 属性中,如下示例:
const React = require('react')
const ReactDOM = require('react-dom')
class NameForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: 'please write some words to discribe yourself'};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
};
handleChange(event) {
this.setState({value: event.target.value});
}
handleSubmit(event) {
alert(`a discribe has been submited: ${this.state.value}`);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
TextArea:
<textarea type="text" value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
}
ReactDOM.render(
<NameForm />,
document.getElementById('root')
);
注意我们在构造器中给他 state value 定义了初始值,这样在第一次访问页面时就会有一段默认文字了。
在 html 中 select 标签可以创建一个下拉菜单控件:
<select>
<option value="grapefruit">Grapefruit</option>
<option value="lime">Lime</option>
<option selected value="coconut">Coconut</option>
<option value="mango">Mango</option>
</select>
注意上面的示例中,Coconut 选项会默认选中,因为其定义了 selected 属性。在 react 中我们可以在 select 根标签中直接定义 value 属性来定义当前选中的是哪一个 option。这在 controlled component 中可以很方便的管理及更新 select element 的 value:
const React = require('react')
const ReactDOM = require('react-dom')
class NameForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: 'sports'};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
};
handleChange(event) {
this.setState({value: event.target.value});
}
handleSubmit(event) {
alert(`you favorite is: ${this.state.value}`);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
your favorite:
<select value={this.state.value} onChange={this.handleChange}>
<option value="sleep">Sleep</option>
<option value="sports">Sports</option>
<option value="takePhoto">Take Photo</option>
<option value="work">Work</option>
</select>
</label>
<input type="submit" value="Submit" />
</form>
);
}
}
ReactDOM.render(
<NameForm />,
document.getElementById('root')
);
以上示例中,我们将 state 的 value 赋值给 select 的 value 这样 select 当前选中项总是 state 中的值,在 handleChange 触发时会更新 state 中的 value 并 render 页面。
注意我们可以给 value 赋值一个数组,这样就可以同时选中多个 option:
<select multiple={true} value={['B', 'C']}>
以上几种 form 控件起始基本结构都类似,他们都核心概念就是将元素的 value 属性和 state 挂钩,从而使 controlled component 生效。
当我们需要在 component 中同时处理多个 input 元素时,可以给每个 input 添加 name 属性,然后再对应的 handle function 中通过 event.target.name 来区分他们:
const React = require('react')
const ReactDOM = require('react-dom')
class NameForm extends React.Component {
constructor(props) {
super(props);
this.state = {
isGoing: true,
numberOfGuests: 2
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
};
handleChange(event) {
const target = event.target;
const value = target.type === 'checkbox' ? target.checked : target.value;
const name = target.name;
this.setState({[name]: value});
}
handleSubmit(event) {
alert(`Is Going: ${this.state.isGoing}, Number Of Guests: ${this.state.numberOfGuests}`);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Is Going:
<input
name="isGoing"
type="checkbox"
checked={this.state.isGoing}
onChange={this.handleChange} />
</label>
<br />
<label>
Number of Guests:
<input
name="numberOfGuests"
type="number"
value={this.state.numberOfGuests}
onChange={this.handleChange} />
</label>
<br />
<input type="submit" value="Submit" />
</form>
);
}
}
ReactDOM.render(
<NameForm />,
document.getElementById('root')
);
以上示例中我们使用了 ES6 中新加入的特性: this.setState({[name]: value});
再 object 中使用方括号 []
来调用变量。setState()
会自动合并更新到 state 中。
我们可以给一个 input 元素定义初值,默认情况下在页面加载完成后,input 框就可以立刻被用户进行编辑,有时候我们不希望再一开始就让用户修改 input 中的数据,此时可以临时给 input 的 value 属性赋值为 null 或 undefined 就可以了:
const React = require('react')
const ReactDOM = require('react-dom')
ReactDOM.render(
<input value="hi" />,
document.getElementById('root')
);
setTimeout(() => {
ReactDOM.render(
<input value={null} />,
document.getElementById('root')
);
}, 2000);
以上示例中,在页面刚加载的前 2 秒,用户无法修改默认的 hi
字符串。
和 JavaScript 的相同,React 中也可以使用 conditions 语法来选择性的渲染内容。如使用 if 或 conditional operator 来根据不同 state 状态创建不同 elements 然后让 React 更新 UI 来匹配 DOM。
考虑下面两个 components:
const UserGreeting = (props) => {
return <h1>welcome back</h1>
}
const GuestGreeting = (props) => {
return <h1>please sign up</h1>
}
然后我们创建一个 Greeting component 来根据是否有用户登陆来显示以上两个中的一个:
const Greeting = (props) => {
let isLoggedIn = props.isLoggedIn;
if (isLoggedIn)
return <UserGreeting />;
else
return <GuestGreeting />;
}
ReactDOM.render(
<Greeting isLoggedIn={true} />,
document.getElementById('root')
);
以上示例会根据 isLoggedIn 属性的值来渲染不同的内容。
可以使用变量存储 elements,这样可以方便的根据情况 render 部分的 component 而不需要改变输出的指令内容。
考虑下面两个 component 表示 login 和 logout:
const LoginButton = (props) => {
return (
<button onClick={props.onClick}>
login
</button>
);
}
const LogoutButton = (props) => {
return (
<button onClick={props.onClick}>
logout
</button>
);
}
然后我们创建 LoginControl component,它将根据当前情况渲染 login 或 logout button 以及之前创建的 Greeting element:
class LoginControl extends React.Component {
constructor(props) {
super(props);
this.handleLoginClick = this.handleLoginClick.bind(this);
this.handleLogoutClick = this.handleLogoutClick.bind(this);
this.state = {isLoggedIn: false};
}
handleLoginClick() {
this.setState({isLoggedIn: true});
}
handleLogoutClick() {
this.setState({isLoggedIn: false});
}
render() {
let isLoggedIn = this.state.isLoggedIn;
let button;
if (isLoggedIn)
button = <LogoutButton onClick={this.handleLogoutClick} />
else
button = <LoginButton onClick={this.handleLoginClick} />
return (
<div>
<Greeting isLoggedIn={isLoggedIn} />
{button}
</div>
);
}
}
ReactDOM.render(
<LoginControl />,
document.getElementById('root')
);
使用 element 变量以及使用 if 语句根据条件渲染 component 是一种很好的方法。但是有时候可以使用简化语法。下面接收几种 inline condition 语法。
在 JSX 可以通过使用大括号{}
来嵌入 JavaScript 表达式,包括逻辑符号:&&
,在根据条件判断是否包含一个 element 时很有用。
请看下面示例:
const InlineCom = (props) => {
return(
<div>
<h1>hello world</h1>
{props.count > 10 &&
<h2>count is: {props.count}</h2>
}
</div>
);
}
ReactDOM.render(
<InlineCom count={20} />,
document.getElementById('root')
);
如果 props.count > 10
满足条件则后面的 element 就会成为 component 一部分。
在 JavaScript 中,true && expression
将会评估为 expression,而 false && expression
将会评估为 false。因此当 condition 为 true 时,&&
后的 element 将会输出,否则 React 将会忽略它。
另一种根据情况通过 inline 单行判断来渲染 element 就是使用 JavaScript conditional operator:condition ? true : false
。
给 LoginControl 的返回添加如下:
return (
<div>
the user is <b>{isLoggedIn ? 'currenty' : 'not'}</b> logged in.
<Greeting isLoggedIn={isLoggedIn} />
{button}
</div>
);
通过 inline conditional operator 来输出不同的信息。
也可以在较长的表达式中使用,例如可以将示例中 button 部分在 render 中这样处理:
return (
<div>
the user is <b>{isLoggedIn ? 'currenty' : 'not'}</b> logged in.
<Greeting isLoggedIn={isLoggedIn} />
{button}
{isLoggedIn
? <LogoutButton onClick={this.handleLogoutClick} />
: <LoginButton onClick={this.handleLoginClick} />
}
</div>
);
使用中根据实际情况选择最合适的方式处理 condition,最终目的是为了使结构更加清晰,代码易读。注意如果判断过复杂就需要考虑拆解 component 为多个个体了。
某些情况下我们可能需要将一个 component 隐藏起来,即使它在别的 component 中已经渲染了。可以通过 return null 来代替它的输出。
下面示例中 WarningBanner 会根据 warn 属性的值来选择性渲染:
const React = require('react')
const ReactDOM = require('react-dom')
const WarningBanner = (props) => {
if (!props.warn) {
return null;
}
return (
<div className='warning'>
warning!
</div>
);
}
class Page extends React.Component {
constructor(props) {
super(props);
this.state = {showWarning: true};
this.handelToggleClick = this.handelToggleClick.bind(this);
}
handelToggleClick() {
this.setState({showWarning: !this.state.showWarning});
}
render() {
return (
<div>
<WarningBanner warn={this.state.showWarning} />
<button onClick={this.handelToggleClick}>
{this.state.showWarning ? 'hide' : 'show'}
</button>
</div>
);
}
}
ReactDOM.render(
<Page />,
document.getElementById('root')
);
在 render method 中 return null 不会影响到 component 的 lifecycle method。例如每次更新 componentDidUpdate 依然会被自动调用。
]]>处理 React elements events 和处理 DOM elements 很相似,但有一些语法区别:
HMTL 中处理 events 示例如下:
<button onclick="activateLasers()">
Activate Lasers
</button>
React 中示例如下:
<button onClick={activateLasers}>
Activate Lasers
</button>
注意它们的区别之处一个是 event 名称,一个是 handler 定义方式。
另一个区别是在 React 中不能通过 return false 的方式防止 events 的默认行为,需要明确的调用 preventDefault method 来实现。
例如在一个 html 页面中定义一个 a tag 并取消其默认打开新页面的行为,实现如下:
<a href="#" onclick="console.log('The link was clicked.'); return false">
Click me
</a>
React 中实现同样功能代码如下:
class Link extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick(e) {
e.preventDefault();
console.log('clicked me');
}
render() {
return (
<a href='#' onClick={this.handleClick}>click me</a>
)
}
}
e 表示 synthetic 综合的 event,当前哪个 event 触发了 e 就表示哪一个。使用 bind 绑定 的 method 在调用时会自动将 e 传入 method。下面会对 bind 是什么作出解释。
React events 同原生的 events 不完全相同,查看所有可用的 events 查看官方介绍:https://reactjs.org/docs/events.html
在 React 中一般情况下不需要通过调用 addEventListener 来给 element 添加 event listener。直接在 element 初始化时为其设置 event listener 即可。
当通过 class 来定义 component 时 event handler 一般是一个 class method,例如上面示例的 handleClick。
下面的示例我们构建一个 Toggle component 可以让用户通过一个 button 来切换 ON/OFF 状态:
const React = require('react')
const ReactDOM = require('react-dom')
class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = { isToggleOn: true };
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState({ isToggleOn: !this.state.isToggleOn });
}
render() {
return (<button onClick={this.handleClick}> { this.state.isToggleOn ? 'ON' : 'OFF'} </button>);
}
}
ReactDOM.render(<
Toggle />,
document.getElementById('root')
);
在 JSX callback 中使用 this 需要特别注意,JavaScript class 的 methods 默认是相互隔离的,如果没有主动 bind 捆绑 method 到 this,在另一个 method 中使用 this.method 会报错 undefined。
如果调用 method 时不写括号()
例如:onClick={this.handleClick}
则需要提前 bind 这个 method 到 this 中,如上面的示例,bind 语法如下:
this.handleClick = this.handleClick.bind(this);
一般将其放在 constructor 中,这样初始化中就会自动执行,当然也可以在调用时直接定义:
<button onClick={this.handleClick.bind(this)}> { this.state.isToggleOn ? 'ON' : 'OFF'} </button>
如果不想使用 bind 语法来处理,那么还有两种方式来处理 class 中 methods 互相隔离这个问题。
第一种叫做 class fields syntax 语法,通过使用 arrow function 的模式定义 method,这样就可以通过通过 this.method 的方法调用 method:
constructor(props) {
super(props);
this.state = { isToggleOn: true };
}
handleClick = () => {
this.setState({ isToggleOn: !this.state.isToggleOn });
}
render() {
return (<button onClick={this.handleClick} > { this.state.isToggleOn ? 'ON' : 'OFF'} </button>);
}
这样就不需要在 constructor 中定义 bind 同时可以在 callback 中直接调用 this.handleClick。
但是需要注意目前这只是 React 实验性的语法,不一定保证以后会一直可用。
第二种是在 callback 中通过 arrow function 的模式调用 method:
handleClick() {
this.setState({ isToggleOn: !this.state.isToggleOn });
}
render() {
return (<button onClick={() => this.handleClick()} > { this.state.isToggleOn ? 'ON' : 'OFF'} </button>);
}
注意这种方法需要在 method 名称后加括号()
。
这种方法的缺点是当每次重新 render 渲染时都会创建新的 callback。当这个 callback 包含传给其 child component 的 props 时,可能会导致 child 重新被渲染。通常情况下推荐使用 constructor 定义 bind 或者使用 class fields syntax 语法来避免这些性能问题。
有时候需要给 event handler 传入附加的参数,如下面示例 button 点击时输出一个输入数据到终端:
handleClick(a, e) {
this.setState({ isToggleOn: !this.state.isToggleOn });
console.log(e._reactName);
console.log(a);
}
render() {
return (<button onClick={this.handleClick.bind(this, 'aaa')} > { this.state.isToggleOn ? 'ON' : 'OFF'} </button>);
}
上面的示例将字符串 aaa 作为 handleClick 的传入参数,并将 bind 绑定过程直接放在 callback 中,这样就不需要在 constructor 中进行 bind 定义了。
通过 bind 绑定后会自动将 e:synthetic 综合的 event 作为第二个参数传入 function,e._reactName 返回 event 名称。
上面的示例也可以通过 arrow function 在 callback 中定义实现:
handleClick(a, e) {
this.setState({ isToggleOn: !this.state.isToggleOn });
console.log(e._reactName);
console.log(a);
}
render() {
return (<button onClick={(e) => this.handleClick('aaa', e)} > { this.state.isToggleOn ? 'ON' : 'OFF'} </button>);
}
上面的示例中 e 依然表示 synthetic event。两种方法都会将 e 作为第二个参数传入。在 arrow function 中我们可以清晰地看到数据的位置,但是通过 bind 的方式会将有些参数自动转发过去。
]]>一个 element 表示我们想要显示在屏幕上的内容:
const element = <h1>Hello, world</h1>;
不同于浏览器 DOM 中的 elements,React elements 是简单的 objects 且可以很方便的创建,React DOM 会严格的刷新 DOM 并匹配对应的 React elements。
容易混淆的概念是 component 和 element,区别是 component 是用来创建 element 的。在后续章节会介绍。
我们的 html 页面中定义了一个 div 容器:
<div id="root"></div>
我们将其称作 root DOM 节点,因为它所有的内容都是被 React DOM 管理的。
通常情况下使用 React 创建的程序只有一个 root DOM 节点。如果你是将 React 整合到现有网站中,你可以有任意个独立的 root DOM 节点。
将 React elements 渲染到 root DOM 节点,需要通过调用 ReactDOM.render()
,并将 React element 和 root DOM 节点作为传入参数:
const element = <h1>Hello, world</h1>;
ReactDOM.render(
element,
document.getElementById('root')
);
此时页面会显示 hello world。
React element 是 immutable 不可改变的,当创建了一个 element 后不可以修改其 children 或 attributes,一个 element 就好像一个视频的一帧,它表示了某一时间点的 UI。
从我们目前学到的知识,唯一刷新 UI 的方法就是重新创建新的 elements 然后调用 ReactDOM.render()
,通过设置 setInterval 来定时刷新:
const tick = () => {
const element = <h1>{new Date().toLocaleTimeString()}</h1>;
ReactDOM.render(
element,
document.getElementById('root')
);
}
setInterval(tick, 1000);
这样就会每秒钟创建一个新的 element 并通过 ReactDOM.render()
渲染到界面。
通常情况下大多数 React app 只会调用 ReactDOM.render()
一次。下一章节会介绍如何将封装到 component 中。
React DOM 会比较其当前和上一个状态的,然后只对有了变化的部分进行更新来达到最终期望的状态。
我们打开上面示例的运行页面,通过chrome 的开发工具查看 elements 情况,可以看到只有时间元素每秒在刷新:
![2021-02-24T07:44:17.png][
即使我们每秒钟都新建并渲染 element,但是只有时间文本 node 是一直通过 React DOM 在刷新的。通过以上的实验,思考我们的 UI 在某个时间点应该是什么样的,而不是只想这着去修改它。
components 将 UI 元素分割为独立的,可复用的片段,每个片段都是单独存在的。这一章节介绍 component 的概念,更多细节参考:React.Component
components 类似于 JavaScript 的 functions,它可以接受抽象的输入数据(props),然后返回 React elements 用来在界面上显示。
最简单的定义 component 方式就是定义一个 JavaScript function:
const Welcome = (props) => {
return <h1> hello, {props.name}</h1>
}
上面的 function 是一个有效的 React component,因为它接受一个单参数 props object 作为传入数据并返回一个 React element。我们称这种 component 为 function component。
也可以使用 ES6 的 class 定义 component:
class Welcome extends React.Component {
render() {
return <h1>hello, {this.props.name}</h1>
}
}
以上两种定义方式是一致的。
需要注意的是 components 名称必须是以大写字母开头,因为 React 会见以小写字母开头的 components 作为 DOM tags 标签,如:<div />
表示一个 html div 标签。
上面的介绍中,我们只遇到了 DOM tags 标签类型的 React elements,例如:
const element = <div />;
elements 也可以表示用户自定义的 components:
const element = <Welcome name='marco' />
当 React 检测到使用了用户自定义的 components 它会将此 JSX 内的 attributes 或 children 作为一个 object 传入 component,这个 object 叫做 props。
下面的示例会输出 hello, marco:
const Welcome = (props) => {
return <h1> hello, {props.name}</h1>;
}
const element = <Welcome name='marco' />;
ReactDOM.render(
element,
document.getElementById('root')
);
以上示例过程如下:
ReactDOM.render()
渲染 <Welcome name="Sara" />
元素.<h1>Hello, Sara</h1>
元素.<h1>Hello, Sara</h1>
结果.component 可以在其输出中引入关联其他 components。这可以让我们在一个 component 内抽象出一个多层的结构。一个 button,一个 form,一个 dialog 或者一个 screen,在 React app 中他们都统称为 components。
例如我们可以创建一个 App component 来渲染多个 Welcome component:
const Welcome = (props) => {
return <h1> hello, {props.name}</h1>;
}
const App = () => {
return (
<div>
<Welcome name='marco' />
<Welcome name='tim' />
<Welcome name='jone' />
</div>
)
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
以上示例中,没有定义 App 的 props,因为不需要给其传入数据,也是可以的。
一般情况下,新建的 React app 只有一个顶层的 App component。
不要害怕将一个 component 拆解为多个小 components。例如下面这个 Comment component:
function Comment(props) {
return (
<div className="Comment">
<div className="UserInfo">
<img className="Avatar"
src={props.author.avatarUrl}
alt={props.author.name}
/>
<div className="UserInfo-name">
{props.author.name}
</div>
</div>
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}
它的 props 包含一个 author object,一个 text,一个 data,描述了一个社交网站上一个 commit 的内容。
修改这个 component 有点困难,因为它有很多的嵌套,同时也难以复用它的内部组件。下面我们尝试拆解这个 component。
首先我们拆解出 Avatar:
const Avatar = (props) => {
return (
<img className="Avatar"
src={props.user.avatarUrl}
alt={props.user.name}
/>
);
}
Avatar 并不需要知道它被用于 commit 中,因此我们修改其 prop 名称为一个更加通用的:user。推荐从 component 本身为出发点命名 props,而不是考虑什么地方使用它。
现在我们可以简化 Commit component:
function Comment(props) {
return (
<div className="Comment">
<div className="UserInfo">
<Avatar user={props.author} />
<div className="UserInfo-name">
{props.author.name}
</div>
</div>
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}
我们将 props.author 作为 user 数据传入 Avatar component 中。
下面我们拆解 UserInfo,其中包含一个 Avatar component:
const UserInfo = (props) => {
return (
<div className="UserInfo">
<Avatar user={props.user} />
<div className="UserInfo-name">
{props.user.name}
</div>
</div>
);
}
然后进一步简化 Commit:
function Comment(props) {
return (
<div className="Comment">
<UserInfo user={props.user} />
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}
拆解 component 在开始看起来使工作量变大了,但是在稍微复杂写的 app 中我们就能够利用这些可复用的 components。一条基本准则是:如果 UI 中的某一部分被多次使用,如 button,panel,Avatar等,或者其自身结构比较复杂,如:App, FeedStory, Comment 等,将他们拆解为独立 components 是一个好的选项。
当把一个 component 定义为 function 或 class 时,需要注意的是不可以修改 props 的值。
考虑下面的 function:
function sum(a, b) {
return a + b;
}
以上的 function 被称作 pure 纯粹的,以为它没有尝试修改输入数据。
作为对比,下面的就是 impure 不纯粹的 function,因为会尝试修改它的输入数据:
function sum(a, b) {
a = b;
}
React 程序有一条限制条件:所有的 React components 都需要是 pure function 来对待 props 数据。
当然应用程序的 UI 是随时间动态变化的。下一节我们会介绍 state 的概念。通过 state 可以使 React components 在运行期间修改它们的输出 elements 来响应用户动作,网络响应等。同时不违反上面的那条规则。
这一节介绍 React components 中 state 和 lifecycle 的概念。
在前一章的示例中,我们通过一个 tick function 在指定时间间隔通过创建新 element 并渲染的方式刷新 UI:
const tick = () => {
const element = (
<div>
<h1>hello world</h1>
<h2>{new Date().toLocaleTimeString()}</h2>
</div>
);
ReactDOM.render(
element,
document.getElementById('root')
);
}
setInterval(tick, 1000);
下面我们介绍通过创建一个封装好的 Clock component,设置定时器并更新其自身。
首先我们根据上面的示例创建 Clock component:
const Clock = (props) => {
<div>
<h1>hello world</h1>
<h2>{props.date.toLocaleTimeString()}</h2>
</div>
}
const tick = () => {
ReactDOM.render(
<Clock date={new Date()} />,
document.getElementById('root')
);
}
setInterval(tick, 1000);
在 tick 调用 Clock component 并定义 date prop 的数据供 Clock 使用。但是上面的实现缺乏一个基本需求,那就是 Clock 应该在其自身中定义定时器并每秒刷新数据的。
我们想要在渲染时达到如下效果调用 Clock:
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
为了实现上述功能,需要为 Clock 添加 state。state 类似于 props 但是它是由 component 私有且完全控制的。
首先我们需要将 component 转换为 class 模式,转换过程如下:
render()
function,将原 component function 的返回元素放入其返回值中render()
中用 this.props 代替 propsclass Clock extends React.Component {
render() {
return (
<div>
<h1>hello world</h1>
<h2>{this.props.date.toLocaleTimeString()}</h2>
</div>
);
}
}
当 update 更新发生时会自动调用 render function。但当我们将 <Clock />
放入 DOM 后,将只会有一个 Clock object 实例被使用,这就让我们可以使用 state 或 lifecycle 等功能。
下面我们将 date 数据直接放入 CLock component 中。
首先将 render 中的 this.props.date 修改为 this.state.date:
class Clock extends React.Component {
render() {
return (
<div>
<h1>hello world</h1>
<h2>{this.state.date.toLocaleTimeString()}</h2>
</div>
);
}
}
然后添加 class constructor 构造器给 this.state 赋初值:
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()}
}
render() {
return (
<div>
<h1>hello world</h1>
<h2>{this.state.date.toLocaleTimeString()}</h2>
</div>
);
}
}
注意 class component 总是应该使用 constructor 且初始化参数为 props。child class 有 constructor 时需要调用 super 来初始化 parent class,具体语法参考我的 JavaScript 教程:https://blog.niekun.net/archives/2011.html
然后删除渲染到 DOM 中 Clock 的 date prop,以及我们设置的 setInterval 定时器:
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
修改完成后的完整代码如下:
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()}
}
render() {
return (
<div>
<h1>hello world</h1>
<h2>{this.state.date.toLocaleTimeString()}</h2>
</div>
);
}
}
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
下面我们实现 Clock 设置自己的定时器并每秒更新。
对于包含很多 components 的程序,但某个 component 不再需要是需要及时释放其占用的资源。
我们需要 Clock 第一次在 DOM 中渲染时设置一个 timer 定时器,在 React 中叫做 mounting 载入。同时我们需要当 Clock 在 DOM 中被删除时清除这个定时器,在 React 中叫做 unmounting 卸载。
我们可以在 components 载入或载出时通过定义特殊的 method 来运行特定指令:
componentDidMount() {
}
componentWillUnmount() {
}
这些 methods 叫做 lifecycle methods。
componentDidMount method 会在 component 第一次输出到 DOM 后被自动调用,我们可以将定时器定义在这里:
componentDidMount() {
this.timerID = setInterval(() => this.tick(), 1000);
}
这样当 Clock 渲染到 UI 后会自动启动这个定时器。注意使用 this 定义的参数可以在 class 中任意地方被调用。
注意 setInterval 中定义的响应动作需要写在 callback 内 () => {}
中,不要直接写:setInterval(this.tick, 1000)
。因为如果要在 callback 调用 method 需要在 constructor 中做如下定义:
this.tick = this.tick.bind(this);
componentWillUnmount method 会在 component 将要被删除时自动调用,我们将定时器在这里取消:
componentWillUnmount() {
clearInterval(this.timerID);
}
接下来我们定义每秒都会自动运行的 tick method,通过 this.setState()
来更新本地 state 中的设置:
tick() {
this.setState({ date: new Date() });
}
最终的完整代码如下:
const React = require('react')
const ReactDOM = require('react-dom')
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = { date: new Date() }
}
componentDidMount() {
this.timerID = setInterval(() => this.tick(), 1000);
}
componentWillUnmount() {
clearInterval(this.timerID);
}
tick() {
this.setState({ date: new Date() });
}
render() {
return (
<div>
<h1>hello world</h1>
<h2>{this.state.date.toLocaleTimeString()}</h2>
</div>
);
}
}
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
现在整个处理流程如下:
ReactDOM.render()
传入 <Clock />
后,React 调用 Clock 构造器初始化 state 为一个包含 date 的 objectrender()
查询到需要显示的 UI 元素,然后更新 DOM 以匹配 Clock 的输出tick()
tick()
,这里面 Clock component 通过 setState()
配置了其 UI 更新任务,通过 setState()
React 就知道了 state 发生了变化并再次调用 render()
监测需要显示的内容,此时的 this.state.date
和上一次的发生了变化,React 就会更新 DOM 到最新的状态。关于 state 的使用需要如下的几点要求。
第一点,不要直接修改 state。
下面的语法不会触发重新 render 渲染 component:
this.state.comment = 'Hello';
正确的语法为使用 setState():
this.setState({comment: 'Hello'});
唯一可以对 state 赋值的是在 constructor 构造器中。
第二点,state 更新是异步的。
React 为了性能可能会在一次 component update 更新中捆绑多个 setState() 调用,由于 this.props 和 this.state 可能会被异步更新,所以不要依赖他们的数据来计算后续的 state。
如下示例可能会错误的更新 counter:
this.setState({
counter: this.state.counter + this.props.increment,
});
为了实现是这个需求,使用 setState 的另一种格式:传入一个 function,第一个参数为当前 state,第二个参数为 props 然后内部计算 state 更新:
this.setState(function (state, props) {
return {
counter: state.counter + props.increment
};
});
第三点,state 更新会合并。
当调用 setState()
后,会合并设置的 object 到当前 state 中。
如下示例,state 可能包含多个独立的变量:
constructor(props) {
super(props);
this.state = {
posts: [],
comments: []
};
}
然后我们可以单独调用 setState() 来分别更新它们:
componentDidMount() {
fetchPosts().then(response => {
this.setState({
posts: response.posts
});
});
fetchComments().then(response => {
this.setState({
comments: response.comments
});
});
}
合并过程是自动完成的,所以通过 setState 修改 comments 只会更新 comments 而不会改变 posts。
一个 component 的 child 或 parent 都不会知道当前 component 是包含 state 还是不包含,且不关心是通过 function 还是 class 方式构建的 component。所以 state 被认为是封装的不能够被外界所访问。
component 的 state 可以作为 props 向它的 child component 传递:
<FormattedDate date={this.state.date} />
如上所示 FormattedDate 可以接受 date prop,它并不知道数据来自 parent 的 state 还是 props:
function FormattedDate(props) {
return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}
通常这叫做 top-down 或 unidirectional 数据流。任何 state 都被某个特定 component 所有,state 的数据只能在其 component 的 child 中传递出去。
我们通过建立 App component 并构建三个 Clock component 来展示 component 之间是互相独立的:
function App() {
return (
<div>
<Clock />
<Clock />
<Clock />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
每个 Clock 都有个各自的定时器并独立更新。
在 React 中,components 定义为 stateful 还是 stateless 的取决于其在运行中可能的变化,可以在 stateful 的 component 中使用 stateless 的 component,反过来亦可。
]]>const element = <h1>Hello, world!</h1>;
它既不是一个 string 也不是 html。它叫做 JSX,是一种对 JavaScript 语法的扩展。推荐在 React 中使用 JSX 来描述 UI,它用来创建 React 类型的 elements 然后将其在 DOM 中渲染。
下面对 JSX 做一些基本介绍。
React 认同这种观点:渲染逻辑结构和 UI 逻辑结构是原生相互耦合的,events 的处理,状态的变化,数据何时显示等。不同于人为的将 markup 部分和 logic 逻辑部分放在单独的文件,React 的耦合单元叫做 components 可以同时包含 markup 和 logic 部分。
React 并不强制要求使用 JSX,但是大部分开发者认为它可以帮助在 JavaScript 中处理 UI 的问题。它也可以帮助 React 显示更多的 error 或 warning 信息。
下面的示例中,我们声明一个变量并在 JSX 中通过大括号{}
调用变量:
const name = 'marco nie';
const element = <h1>hello, {name}</h1>
ReactDOM.render(
element,
document.getElementById('root')
);
我们可以将任意的 JavaScript 表达式通过大括号{}
放如 JSX 中。例如:1 + 1
,user.name
都是有效的 JavaScript 表达式:
const city = 'han zhong';
const user = {
name: 'marco',
age: 20
}
const element = <h1>hello, {user.name}, from {city}, time: {1 + 20}</h1>
可以将 JSX 分割成多行以提高代码可读性,需要使用小括号包围起来,防止编译器错误的自动添加分行符:
const element = (
<h1>
hello, {user.name}, from {city}, time: {1 + 20}
</h1>
);
编译后 JSX 表达式会转换为通用的 JavaScript function 或 objects。这就意味着我们可以在 if 或 for 表达式中,传入数据中或者 返回值中使用 JSX:
const testFuc = () => {
return <h1>hello, {user.name}</h1>
}
可以使用引号 ""
将一个字符串作为属性:
const el1 = <div tabIndex="0"></div>
可以以通过大括号{}
将一个 JavaScript 表达式嵌入属性:
const el2 = <img src={user.name}></img>
当使用 JavaScript 嵌入属性时不能在大括号中使用引号,同一个属性中只能使用引号嵌入字符串或者大括号嵌入 JavaScript 表达式中的一种。
由于 JSX 更加接近于 JavaScript 而不是 html,所以 React 中的 DOM 使用 camelCase 属性命名规范代替 html 中的属性名称,例如 html 元素的 class 属性在 JSX 中定义为 className,tabindex 在 JSX 中为 tabIndex。
如果一个 tag 元素内容是空的则可以使用 />
立刻结束定义:
const el3 = <img src=''/>
JSX tag 标签内也可以有子元素:
const el4 = (
<div>
<h1>hello,</h1>
<h2>i am your friend</h2>
</div>
);
注意只能在一个元素内定义子元素,不能直接定义两个同级的元素否则会报错。
在 JSX 中嵌入用户输入是安全的:
const title = response.potentiallyMaliciousInput;
// This is safe:
const element = <h1>{title}</h1>;
默认情况下 React 在渲染前会将脱开所有嵌入 JSX 中的数据,因此可以确保不会注入任何没有明确定义在应用中的数据。渲染前所有的所有内容都会转换为 string 字符串形式。则能够防止 XSS (cross-site-scripting) 攻击。
Babel 会将 JSX 向下编译为对 React.createElement() 的调用,以下两种定义方法是相同的:
const el5 = (
<h1 className='test'>
hello world
</h1>
);
const el6 = React.createElement(
'h1',
{className: 'test'},
'hello world'
)
通过 React.createElement()
创建元素会额外做一些语法检查来防止错误代码。但通常我们使用下面语法创建一个 object:
const el7 = {
type: 'h1',
props: {
className: 'test',
children: 'hello world'
}
}
以上方式创建的 object 叫做 React elements。React 使用这些 objects 来构建 DOM 并及时更新。
推荐代码编辑器使用 Babel 语法定义环境,这样可以同时支持 ES6 和 JSX 语法结构。设置方法参考:https://babeljs.io/docs/en/editors
]]>