string(1) "5" Marco Nie - web https://blog.niekun.net/category/web/ zh-CN Thu, 24 Apr 2025 09:04:00 +0800 Thu, 24 Apr 2025 09:04:00 +0800 建立本地 Web 服务器 https://blog.niekun.net/archives/3024.html https://blog.niekun.net/archives/3024.html Thu, 24 Apr 2025 09:04:00 +0800 admin 建立一个本地 Web 服务器来快速查看本地网站(HTML, CSS, JavaScript 文件)有几种非常快速的方法,通常只需要一行命令。这里介绍几种最常用的:

1. 使用 Python 内置的 HTTP 服务器 (通常最快,无需额外安装)

如果你的电脑上安装了 Python 3 (现在大多数操作系统都自带或容易安装),这是最快的方法之一,因为它不需要安装任何额外的库。

  • 步骤:

    1. 打开你的终端或命令提示符 (Terminal / Command Prompt / PowerShell)。
    2. 使用 cd 命令导航到你的网站文件所在的根目录(也就是包含 index.html 文件的那个文件夹)。
    3. 运行以下命令:

      python -m http.server
      • 如果你使用的是较旧的 Python 2 版本 (尽量避免使用),命令是:

        python -m SimpleHTTPServer
    4. 终端会显示类似 Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ... 的信息。
    5. 打开你的网页浏览器,访问 http://localhost:8000 或者 http://127.0.0.1:8000
    6. 如果你的根目录下没有 index.html,它会列出目录中的文件。
    7. 要停止服务器,回到终端按 Ctrl + C
  • 优点: 极快启动,通常无需安装额外软件。
  • 指定端口: 如果 8000 端口被占用,你可以指定其他端口:

    python -m http.server 8080

2. 使用 Node.js 的 http-serverlive-server (功能更强,需要 Node.js)

如果你安装了 Node.js 和 npm (Node Package Manager),可以使用一些非常方便的包。

  • a) http-server (基础服务器)

    1. 首次安装 (全局安装,只需一次):

      npm install -g http-server
    2. 启动服务器:

      • 在终端中,cd 到你的网站根目录。
      • 运行:

        http-server
    3. 终端会显示可用的访问地址,通常是 http://127.0.0.1:8080
    4. 在浏览器中打开该地址。
    5. Ctrl + C 停止。
  • b) live-server (带自动刷新功能)
    live-server 会在你修改并保存文件后自动刷新浏览器,非常适合开发调试。

    1. 首次安装 (全局安装,只需一次):

      npm install -g live-server
    2. 启动服务器:

      • 在终端中,cd 到你的网站根目录。
      • 运行:

        live-server
    3. 它通常会自动在你的默认浏览器中打开网站 (http://127.0.0.1:8080 或类似地址)。当你修改并保存 HTML/CSS/JS 文件时,浏览器会自动刷新。
    4. Ctrl + C 停止。
  • 优点: 功能更强(特别是 live-server 的自动刷新),Node.js 在 Web 开发中很常用。
  • 缺点: 需要先安装 Node.js 和对应的 npm 包。

3. 使用 VS Code 扩展 "Live Server" (集成在编辑器中,非常方便)

如果你使用 Visual Studio Code 编辑器,这是最方便的方法之一。

  • 步骤:

    1. 打开 VS Code。
    2. 转到扩展视图 (Extensions view, 图标通常在左侧边栏)。
    3. 搜索 "Live Server" (作者通常是 Ritwick Dey)。
    4. 点击 "Install" 安装。
    5. 安装完成后,打开你的网站项目文件夹。
    6. 在你的 index.html 文件上右键单击,选择 "Open with Live Server"。
    7. 或者,点击 VS Code 底部状态栏右下角的 "Go Live" 按钮。
    8. 服务器会启动,并在浏览器中自动打开页面,同样支持自动刷新。
    9. 要停止,可以再次点击状态栏的端口号按钮 (显示为 "Port: 5500" 或类似)。
  • 优点: 无需命令行操作,与编辑器紧密集成,带自动刷新。
  • 缺点: 需要使用 VS Code 编辑器。

4. 使用 PHP 内置服务器 (如果已安装 PHP)

如果你的系统碰巧安装了 PHP (通常用于后端开发,但也可用于快速启动静态服务器)。

  • 步骤:

    1. 打开终端或命令提示符。
    2. cd 到你的网站根目录。
    3. 运行:

      php -S localhost:8000
      • 你可以将 8000 替换为其他端口号。
    4. 在浏览器中访问 http://localhost:8000
    5. Ctrl + C 停止。
  • 优点: 如果已安装 PHP,则命令简单。
  • 缺点: 不如 Python 那么普遍预装,也不如 Node.js 工具或 VS Code 扩展功能丰富(如自动刷新)。

哪个最快?

  • 如果你有 Python: python -m http.server 通常是最快的,因为它很可能已经安装好了。
  • 如果你用 VS Code: 安装并使用 "Live Server" 扩展几乎是最方便、启动最快的方式(点击按钮即可)。
  • 如果你常用 Node.js: live-serverhttp-server 也非常快,并且 live-server 提供了非常有用的自动刷新功能。

选择哪种取决于你电脑上已有的环境和你个人的偏好。对于纯粹的“快速启动一个能看本地网页的服务器”,Python 的内置服务器通常拔得头筹。

]]>
0 https://blog.niekun.net/archives/3024.html#comments https://blog.niekun.net/feed/category/web/archives/3024.html
honkit 使用教程及部署github page https://blog.niekun.net/archives/3021.html https://blog.niekun.net/archives/3021.html Fri, 17 Jan 2025 17:14:00 +0800 admin 1. 什么是 Honkit?

Honkit 是一个现代化的静态书籍生成工具,它是基于旧版 GitBook 开发的,支持 Markdown 文件的编写和多种输出格式,如 HTML 和 PDF。

2. 初始化 Honkit 项目

2.1 创建 Honkit 项目

  1. 创建项目目录

    mkdir my-honkit-project
    cd my-honkit-project
  2. 初始化 Honkit 项目

    npx honkit init

    这会生成以下两个文件:

    • README.md:书籍的主页面。
    • SUMMARY.md:目录结构的定义。
  3. 安装 Honkit

    npm install honkit --save-dev

3. 本地构建书籍

3.1 运行开发服务器

在项目目录中运行以下命令启动本地预览:

npx honkit serve

默认情况下,书籍会在 http://localhost:4000 上运行。

3.2 生成静态文件

当书籍准备好后,运行以下命令生成静态 HTML 文件:

npx honkit build

生成的文件会存储在 _book 目录中。


4. 生成 PDF 文件

Honkit 支持将书籍生成 PDF 文件,但需要预先安装 Calibre 电子书管理软件。

4.1 安装 Calibre

  1. 下载 Calibre

  2. 确保 Calibre 路径正确

    • 检查环境变量

      • 通常,Calibre 安装会自动将路径添加到系统的环境变量 PATH 中。
      • 如果未自动添加,手动将 Calibre 的安装目录路径添加到 PATH 环境变量中。
    • 例如,Windows 系统下,路径可能为:

      C:\Program Files\Calibre2
    • Linux 和 macOS 用户可通过以下命令验证:

      which ebook-convert

      如果返回路径为空,说明需要手动添加。

  3. 重新打开终端
    添加路径后,确保关闭并重新打开终端。

4.2 生成 PDF

运行以下命令生成 PDF 文件:

npx honkit pdf . output.pdf
  • . 表示当前目录。
  • output.pdf 是生成的 PDF 文件名。

5. 部署到 GitHub Pages

5.1 创建 GitHub 仓库

  1. 前往 GitHub 创建一个新仓库(例如 my-honkit-book)。
  2. 将本地项目与远程仓库关联:

    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

5.2 配置 GitHub Actions 自动部署

  1. 创建 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
  2. 提交配置文件
    提交并推送配置文件到 GitHub:

    git add .github/workflows/deploy.yml
    git commit -m "Add GitHub Actions for deployment"
    git push origin main
  3. 启用 GitHub Pages

    • 进入仓库的 Settings > Pages
    • Source 下选择 gh-pages 分支,点击保存。

6. 绑定自定义域名

6.1 配置 GitHub Pages

  1. 进入仓库的 Settings > Pages
  2. Custom domain 中输入您的自定义域名(例如 www.example.com)。
  3. 点击 Save。GitHub 会自动生成一个 CNAME 文件。

6.2 配置 DNS 记录

登录您的域名注册商控制台,添加以下 DNS 记录:

  • 绑定子域名(例如 www.example.com):
    添加 CNAME 记录

    www  CNAME  <username>.github.io
注意:将 <username> 替换为您的 GitHub 用户名。

6.3 启用 HTTPS

  1. 返回 GitHub 仓库的 Settings > Pages
  2. 勾选 Enforce HTTPS,以启用 HTTPS 访问。

自定义域名每次推送后消失的解决方法

当每次推送代码后,自定义域名设置消失,通常是由于 CNAME 文件被覆盖或删除。GitHub Pages 需要根目录的 CNAME 文件来保存自定义域名信息。如果该文件丢失或被覆盖,GitHub 就无法识别您的自定义域名。


解决方法

手动将 CNAME 文件添加到源码目录

  1. 创建 CNAME 文件
    在项目根目录(如 main 分支)下创建一个 CNAME 文件,并在文件中输入您的自定义域名,例如:

    www.example.com
  2. 提交更改
    提交并推送 CNAME 文件到远程仓库:

    git add CNAME
    git commit -m "Add CNAME file"
    git push origin main
  3. 验证
    每次部署后,CNAME 文件会自动被包含在 gh-pages 分支中,不会被覆盖。

验证步骤

  1. 检查 CNAME 文件

    • 进入 GitHub 仓库的 gh-pages 分支,确认根目录下有 CNAME 文件。
    • 文件内容应为您的自定义域名,例如:

      www.example.com
  2. 检查 GitHub Pages 设置

    • 转到 Settings > Pages
    • 确认自定义域名已正确显示,并启用 HTTPS
  3. 测试访问
    使用您的自定义域名访问页面,确认是否能够正确加载。

7. 常见问题及解决方法

问题 1:npx honkit pdf 报错 Command failed: ebook-convert not found

原因
Calibre 未正确安装,或 ebook-convert 工具未添加到系统的 PATH 环境变量中。

解决方法

  1. 确保 Calibre 已安装,并重新确认安装路径:

    • Windows 默认路径:C:\Program Files\Calibre2
    • macOS/Linux 使用 which ebook-convert 检查路径。
  2. 手动将 Calibre 的安装路径添加到系统环境变量中:

    • Windows

      • 打开“环境变量”设置,找到 PATH
      • 添加 C:\Program Files\Calibre2
    • Linux/macOS

      • 编辑 ~/.bashrc~/.zshrc 文件,添加以下内容:

        export PATH="/path/to/calibre:$PATH"

        替换 /path/to/calibre 为 Calibre 的实际路径。

      • 保存后运行 source ~/.bashrcsource ~/.zshrc 使更改生效。
  3. 重新运行 npx honkit pdf

问题 2:GitHub Actions 部署失败,提示 Permission denied

原因
GitHub Actions 无法向 gh-pages 分支推送更改,通常是因为缺少权限。

解决方法

  1. 确保 GitHub 仓库的 Settings > Actions > General 中已启用 Read and write permissions

    • 打开 Workflow permissions,选择 Read and write permissions
    • 勾选 Allow GitHub Actions to create and approve pull requests
  2. 确认 GitHub Actions 配置文件中使用的是 github_token

    with:
      github_token: ${{ secrets.GITHUB_TOKEN }}

问题 3:GitHub Pages 部署成功,但自定义域名未生效

原因
DNS 记录配置错误或未生效。

解决方法

  1. 登录域名管理平台,检查 DNS 配置:

    • 确认 A 记录或 CNAME 记录配置正确。
    • 等待 DNS 记录生效(可能需要 5-48 小时)。
  2. 检查仓库根目录下是否有 CNAME 文件,内容是否正确(应与自定义域名一致)。
  3. 验证 DNS 配置是否正确:

  4. 启用 HTTPS:

    • 进入 Settings > Pages,勾选 Enforce HTTPS

问题 4:生成的书籍内容显示错乱或样式缺失

原因

  • Markdown 文件中格式不正确。
  • SUMMARY.md 的目录结构未正确链接。

解决方法

  1. 确认 Markdown 文件的格式是否符合规范。
  2. 检查 SUMMARY.md 中的链接是否与文件路径一致:

    * [章节标题](path/to/file.md)
  3. 清理并重新构建书籍:

    npx honkit build

问题 5:自定义 PDF 样式未生效

原因
未指定自定义样式文件或样式文件路径错误。

解决方法

  1. 创建一个 CSS 样式文件(如 custom.css),自定义 PDF 的排版样式。
  2. 使用以下命令生成 PDF 并指定样式文件:

    npx honkit pdf --css custom.css . output.pdf

    8. 支持与反馈

如果您遇到未列出的问题或有其他疑问,请参考以下支持途径:

完成这些步骤后,您可以成功使用 Honkit 创建、部署和管理文档,解决常见问题,并生成符合您需求的 HTML 和 PDF 文档!

]]>
0 https://blog.niekun.net/archives/3021.html#comments https://blog.niekun.net/feed/category/web/archives/3021.html
Miniflux 搭建自己的 RSS 服务系统 https://blog.niekun.net/archives/2749.html https://blog.niekun.net/archives/2749.html Fri, 12 Aug 2022 14:50:00 +0800 admin 一直在使用 feedly 作为 rss 订阅器阅读文章,基本可以满足我的需求,但是部分站点只能预览摘要,想要阅读全文还打开文章链接。最近发现一个开源免费的 rss 系统,他的特点就是轻量无多于内容,致力于阅读体验。同时我发现他的一些独有功能,可以在文章只显示摘要时,有一个下载全文的选项,这样就实现了大部分文章在 rss 阅读器中就可以阅读全文的需求了。

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:

其中需要自行根据实际情况修改一些内容:

  • port 端口的主机映射,这里我使用了 18080 映射容器内的 8080 端口
  • BASE_URL 设置需要访问 miniflux 服务的域名地址,后面需要配置反向代理
  • ADMIN_USERNAME 设置管理员用户名
  • ADMIN_PASSWORD 设置管理员用户密码

敏感的环境变量值可以单独放在同配置文件路径下的 .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 配置文件中定义的管理员账户及密码:
2022-08-12T06:47:50.png

使用

如果某一篇文章不显示全文,只需要点击文章顶部的 download 按钮即可加载全文:
2022-08-12T06:47:12.png

miniflux 提供了丰富的 api 接口可供二次开发使用,通过简单的请求就可以获取到文章的各种信息,返回数据为 json 格式。

官方 api 文档参考:https://miniflux.app/docs/api.html

]]>
0 https://blog.niekun.net/archives/2749.html#comments https://blog.niekun.net/feed/category/web/archives/2749.html
获取 JD cookie 方法 https://blog.niekun.net/archives/2388.html https://blog.niekun.net/archives/2388.html Sun, 09 Jan 2022 18:53:00 +0800 admin 在运行自动化脚本的时候需要获取到对应网站的登录 cookie,这里介绍通过 chrome 浏览器获取到需要的信息。

这里提供的方法仅供用于前端开发测试。

首先登陆移动端网站:https://m.jd.com

右键点击 inspect 进入开发者界面,然后点击网页右上角的登录键根据提示需要输入账户信息登录。

登录成功后在开发栏切换到 network 选项,并且在 filter 中输入 log.gif 过滤出需要的信息:
1.jpg

选中第一个 log 文件,点击 header 栏,在其中找到 cookie 项:
2.jpg

复制其中的内容到剪贴板。

找到开发页面下方的 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

]]>
0 https://blog.niekun.net/archives/2388.html#comments https://blog.niekun.net/feed/category/web/archives/2388.html
React 入门教程之九 -- composition 模块化 和 inheritance 继承 https://blog.niekun.net/archives/2329.html https://blog.niekun.net/archives/2329.html Sun, 29 Aug 2021 13:35:29 +0800 admin react 有一套完善了 composition 构造模型,推荐使用 composition 代替 inheritance 来在 components 之间复用代码。下面介绍在开发的具体场景中常常需要用到 inheritance 的地方如何用 composition 解决。

containment 包含

一些 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 使用。

specialization 特殊化

一些场景下,我们会将某个 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 等。

]]>
0 https://blog.niekun.net/archives/2329.html#comments https://blog.niekun.net/feed/category/web/archives/2329.html
React 入门教程之八 -- Lifting State Up 提升 state 层级 https://blog.niekun.net/archives/2324.html https://blog.niekun.net/archives/2324.html Sat, 28 Aug 2021 23:38:00 +0800 admin 大多数情况下,不同的 components 之间需要对同一个变化着的 data 进行响应。推荐将这些 shared state 共享的数据提升到它们最近的 parent component 中,下面详细介绍如何实现这一 function。

下面创建一个 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 定义了这个块的抬头信息,效果如下:

2021-08-28T13:36:45.png

上一章节中我们详细介绍了如何构建一个 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.temperaturethis.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> 元素可以接受 valueonChange 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 的 temperatescale 数据到其 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')
);

2021-08-28T15:30:35.png

处理流程为:

  • 用户输入在任意一个 input 中输入数值后,触发 onChange 并调用 handleChange
  • 然后触发了 Calculator 的 onTemperatureChange 并将 input 的数据作为传入参数
  • 根据不同的 input 最终触发了 Calculator 中的 handleChange function 并对 state 进行了修改
  • state 改变后会触发 render,并计算最新的 celsius 和 Fahrenheit 数值
  • 最后根据计算的结果重新渲染 Temperature 的 input value
]]>
0 https://blog.niekun.net/archives/2324.html#comments https://blog.niekun.net/feed/category/web/archives/2324.html
通过 GitHub API 获取数据 https://blog.niekun.net/archives/2290.html https://blog.niekun.net/archives/2290.html Thu, 08 Apr 2021 09:44:00 +0800 admin 最近发现通过 GitHub API 可以获取到很多有用的数据,它返回一个 json 格式数据,可以后期解析得到需要的内容。

主链接:https://api.github.com/

返回内容如下:

{
  "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"
}

更多使用方法可以自己研究下。

]]>
0 https://blog.niekun.net/archives/2290.html#comments https://blog.niekun.net/feed/category/web/archives/2290.html
React 入门教程之七 -- List 和 Form https://blog.niekun.net/archives/2203.html https://blog.niekun.net/archives/2203.html Fri, 05 Mar 2021 16:13:00 +0800 admin list 列表和 key

在 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.
2021-03-05T06:50:19.png

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>
    );
}

添加后报警就会消除。

Keys

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。

拆解 component 时 key 的处理

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>
  );
}

每个 item 的 key 必须是特定的

数组中每个 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。

Forms 表格

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 可控构件 来处理。

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,这样我们就可以操作输入的数据用于其他任何地方了。

textarea 标签

在 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 定义了初始值,这样在第一次访问页面时就会有一段默认文字了。

select 标签

在 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 生效。

处理多个 input 输入源

当我们需要在 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 Null value

我们可以给一个 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 字符串。

]]>
0 https://blog.niekun.net/archives/2203.html#comments https://blog.niekun.net/feed/category/web/archives/2203.html
React 入门教程之六 -- Conditional Rendering https://blog.niekun.net/archives/2195.html https://blog.niekun.net/archives/2195.html Tue, 02 Mar 2021 23:10:22 +0800 admin React 中,我们可以创建独立的 component 来封装特定的功能。因此,可以根据不同的程序的 state 选择性的做部分渲染。

和 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 变量

可以使用变量存储 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')
);

inline condition 单语句判断

使用 element 变量以及使用 if 语句根据条件渲染 component 是一种很好的方法。但是有时候可以使用简化语法。下面接收几种 inline condition 语法。

inline if with && operator

在 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 condition operator

另一种根据情况通过 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 隐藏起来,即使它在别的 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 依然会被自动调用。

]]>
0 https://blog.niekun.net/archives/2195.html#comments https://blog.niekun.net/feed/category/web/archives/2195.html
React 入门教程之五 -- Event https://blog.niekun.net/archives/2189.html https://blog.niekun.net/archives/2189.html Thu, 25 Feb 2021 17:23:00 +0800 admin events 处理

处理 React elements events 和处理 DOM elements 很相似,但有一些语法区别:

  • React events 命名使用 camelCase 规则,而不是 lowercase
  • 使用 JSX 传入 function 作为 events handler,而不是 string 字符串

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 传入数据

有时候需要给 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 的方式会将有些参数自动转发过去。

]]>
0 https://blog.niekun.net/archives/2189.html#comments https://blog.niekun.net/feed/category/web/archives/2189.html