string(1) "5"
1. 使用 Python 内置的 HTTP 服务器 (通常最快,无需额外安装)
如果你的电脑上安装了 Python 3 (现在大多数操作系统都自带或容易安装),这是最快的方法之一,因为它不需要安装任何额外的库。
步骤:
cd
命令导航到你的网站文件所在的根目录(也就是包含 index.html
文件的那个文件夹)。运行以下命令:
python -m http.server
如果你使用的是较旧的 Python 2 版本 (尽量避免使用),命令是:
python -m SimpleHTTPServer
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
的信息。http://localhost:8000
或者 http://127.0.0.1:8000
。index.html
,它会列出目录中的文件。Ctrl + C
。指定端口: 如果 8000 端口被占用,你可以指定其他端口:
python -m http.server 8080
2. 使用 Node.js 的 http-server
或 live-server
(功能更强,需要 Node.js)
如果你安装了 Node.js 和 npm (Node Package Manager),可以使用一些非常方便的包。
a) http-server
(基础服务器)
首次安装 (全局安装,只需一次):
npm install -g http-server
启动服务器:
cd
到你的网站根目录。运行:
http-server
http://127.0.0.1:8080
。Ctrl + C
停止。b) live-server
(带自动刷新功能)
live-server
会在你修改并保存文件后自动刷新浏览器,非常适合开发调试。
首次安装 (全局安装,只需一次):
npm install -g live-server
启动服务器:
cd
到你的网站根目录。运行:
live-server
http://127.0.0.1:8080
或类似地址)。当你修改并保存 HTML/CSS/JS 文件时,浏览器会自动刷新。Ctrl + C
停止。live-server
的自动刷新),Node.js 在 Web 开发中很常用。3. 使用 VS Code 扩展 "Live Server" (集成在编辑器中,非常方便)
如果你使用 Visual Studio Code 编辑器,这是最方便的方法之一。
步骤:
index.html
文件上右键单击,选择 "Open with Live Server"。4. 使用 PHP 内置服务器 (如果已安装 PHP)
如果你的系统碰巧安装了 PHP (通常用于后端开发,但也可用于快速启动静态服务器)。
步骤:
cd
到你的网站根目录。运行:
php -S localhost:8000
8000
替换为其他端口号。http://localhost:8000
。Ctrl + C
停止。哪个最快?
python -m http.server
通常是最快的,因为它很可能已经安装好了。live-server
或 http-server
也非常快,并且 live-server
提供了非常有用的自动刷新功能。选择哪种取决于你电脑上已有的环境和你个人的偏好。对于纯粹的“快速启动一个能看本地网页的服务器”,Python 的内置服务器通常拔得头筹。
]]>Honkit 是一个现代化的静态书籍生成工具,它是基于旧版 GitBook 开发的,支持 Markdown 文件的编写和多种输出格式,如 HTML 和 PDF。
创建项目目录:
mkdir my-honkit-project
cd my-honkit-project
初始化 Honkit 项目:
npx honkit init
这会生成以下两个文件:
README.md
:书籍的主页面。SUMMARY.md
:目录结构的定义。安装 Honkit:
npm install honkit --save-dev
在项目目录中运行以下命令启动本地预览:
npx honkit serve
默认情况下,书籍会在 http://localhost:4000 上运行。
当书籍准备好后,运行以下命令生成静态 HTML 文件:
npx honkit build
生成的文件会存储在 _book
目录中。
Honkit 支持将书籍生成 PDF 文件,但需要预先安装 Calibre 电子书管理软件。
下载 Calibre:
确保 Calibre 路径正确:
检查环境变量:
PATH
中。PATH
环境变量中。例如,Windows 系统下,路径可能为:
C:\Program Files\Calibre2
Linux 和 macOS 用户可通过以下命令验证:
which ebook-convert
如果返回路径为空,说明需要手动添加。
运行以下命令生成 PDF 文件:
npx honkit pdf . output.pdf
.
表示当前目录。output.pdf
是生成的 PDF 文件名。my-honkit-book
)。将本地项目与远程仓库关联:
git init
git remote add origin https://github.com/<username>/my-honkit-book.git
git add .
git commit -m "Initial commit"
git branch -M main
git push -u origin main
创建 GitHub Actions 配置文件:
在项目的根目录下创建 .github/workflows/deploy.yml
文件,内容如下:
name: Deploy Honkit to GitHub Pages
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install Dependencies
run: npm install
- name: Build Honkit
run: npx honkit build
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./_book
提交配置文件:
提交并推送配置文件到 GitHub:
git add .github/workflows/deploy.yml
git commit -m "Add GitHub Actions for deployment"
git push origin main
启用 GitHub Pages:
gh-pages
分支,点击保存。www.example.com
)。CNAME
文件。登录您的域名注册商控制台,添加以下 DNS 记录:
绑定子域名(例如 www.example.com
):
添加 CNAME 记录:
www CNAME <username>.github.io
注意:将 <username>
替换为您的 GitHub 用户名。
当每次推送代码后,自定义域名设置消失,通常是由于 CNAME
文件被覆盖或删除。GitHub Pages 需要根目录的 CNAME
文件来保存自定义域名信息。如果该文件丢失或被覆盖,GitHub 就无法识别您的自定义域名。
手动将 CNAME
文件添加到源码目录
创建 CNAME
文件:
在项目根目录(如 main
分支)下创建一个 CNAME
文件,并在文件中输入您的自定义域名,例如:
www.example.com
提交更改:
提交并推送 CNAME
文件到远程仓库:
git add CNAME
git commit -m "Add CNAME file"
git push origin main
CNAME
文件会自动被包含在 gh-pages
分支中,不会被覆盖。检查 CNAME
文件:
gh-pages
分支,确认根目录下有 CNAME
文件。文件内容应为您的自定义域名,例如:
www.example.com
检查 GitHub Pages 设置:
npx honkit pdf
报错 Command failed: ebook-convert not found
原因:
Calibre 未正确安装,或 ebook-convert
工具未添加到系统的 PATH
环境变量中。
解决方法:
确保 Calibre 已安装,并重新确认安装路径:
C:\Program Files\Calibre2
。which ebook-convert
检查路径。手动将 Calibre 的安装路径添加到系统环境变量中:
Windows:
PATH
。C:\Program Files\Calibre2
。Linux/macOS:
编辑 ~/.bashrc
或 ~/.zshrc
文件,添加以下内容:
export PATH="/path/to/calibre:$PATH"
替换 /path/to/calibre
为 Calibre 的实际路径。
source ~/.bashrc
或 source ~/.zshrc
使更改生效。npx honkit pdf
。Permission denied
原因:
GitHub Actions 无法向 gh-pages
分支推送更改,通常是因为缺少权限。
解决方法:
确保 GitHub 仓库的 Settings > Actions > General 中已启用 Read and write permissions:
确认 GitHub Actions 配置文件中使用的是 github_token
:
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
原因:
DNS 记录配置错误或未生效。
解决方法:
登录域名管理平台,检查 DNS 配置:
CNAME
文件,内容是否正确(应与自定义域名一致)。验证 DNS 配置是否正确:
启用 HTTPS:
原因:
SUMMARY.md
的目录结构未正确链接。解决方法:
检查 SUMMARY.md
中的链接是否与文件路径一致:
* [章节标题](path/to/file.md)
清理并重新构建书籍:
npx honkit build
原因:
未指定自定义样式文件或样式文件路径错误。
解决方法:
custom.css
),自定义 PDF 的排版样式。使用以下命令生成 PDF 并指定样式文件:
npx honkit pdf --css custom.css . output.pdf
如果您遇到未列出的问题或有其他疑问,请参考以下支持途径:
完成这些步骤后,您可以成功使用 Honkit 创建、部署和管理文档,解决常见问题,并生成符合您需求的 HTML 和 PDF 文档!
]]>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 的方式会将有些参数自动转发过去。
]]>