一光年

一光年 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织 www.a-lightyear.com 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

一光年 发布了文章 · 2019-03-21

Spring笔记1——极简入门教程

环境准备

  • 系统:MacOS
  • 开发:IntelliJ IDEA
  • 语言:Java8
  • 其它:Mysql、Redis

脚手架代码

Spring提供了一个创建项目脚手架的官网,在这里可以直接定制项目的框架代码。例如:

图片描述

注意:在Dependencies中,添加了Web依赖。

点击【Generate Project】,即可下载项目框架代码。

创建工程

将框架代码包解压后放到工作目录。打开IDEA,点击【File] -> 【Open】,打开对应目录。

启动工程

找到com.spring.demo.demo下的DemoApplication,右键点击运行后,console中即可显示Spring启动的信息。
图片描述

Controller

在传统MVC架构中,Controller负责接收Http请求并返回相应的应答,这个应答可以是一个页面,也可以是一个JSON对象。方便起见,本教程使用RestfulAPI为例。

添加RestController

创建一个UserController,负责响应User相关的业务请求。对于纯数据的API接口,使用@RestControll进行标注,这样每一个API接口的返回值就会被转化为JSON结构的数据。

package com.spring.demo.demo.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    @RequestMapping("/user")
    public String find() {
        return "UserA";
    }
}

重启应用,在浏览器中请求http://localhost:8080/user,即可看到请求的返回内容:UserA。

路径参数

对于带路径参数的请求,可以通过@PathVariable 标注来获取参数值。如:

package com.spring.demo.demo.controller;

import org.springframework.web.bind.annotation.PathVariable;
...

@RestController
public class UserController {

    @RequestMapping("/user/{id}")
    public String find(@PathVariable int id) {
        return "用户ID[" + id + "]";
    }
}

POST请求

对于POST请求,可以使用@PostMapping 来标注方法,接收的JSON数据需要使用@RequestBody来标注。

    ...
    @PostMapping("/user")
    public void create(@RequestBody UserCreateRequest user) {
        System.out.println(user.getName());
    }
    ...

这里的UserCreateRequest,用来接收并转换JSON数据。

package com.spring.demo.demo.dto;

public class UserCreateRequest {

    private String name;
    private boolean gender;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public boolean isGender() {
        return gender;
    }

    public void setGender(boolean gender) {
        this.gender = gender;
    }
}

在IDEA的Terminal中,使用curl来进行测试。

curl -XPOST 'http://127.0.0.1:8080/user' -H 'Content-Type: application/json' -d'{"name":"用户A","gender":0}'

参数校验

有时我们需要对传入参数进行非空或有效值的校验,这个处理应该在正式进入controller前进行。

1. 添加@Validated标注

在SpringMVC中,对输入参数进行校验通常使用@Validated标注。

    ...
    @PostMapping("/user")
    public void create(@RequestBody @Validated UserCreateRequest user) {
        System.out.println(user.getName());
    }
    ...

2. 添加校验规则标注@

在对应的字段上,加上对应的校验规则标注。

package com.spring.demo.demo.dto;

import javax.validation.constraints.NotNull;

public class UserCreateRequest {

    @NotNull
    private String name;

    private boolean gender;

    // Getters & Setters
    ...
}

添加后重启服务,发送不含name字段的POST请求,结果如下:
图片描述

Service

MVC框架中,Controller负责接收请求和相应,Service则负责具体的业务处理,即Model层。

Service在定义是需要使用@Service标注,SpringBoot在启动中将会注册该Service并在Controller通过DI来实例化并使用该Service。

一般来说,我们会创建一个Service的接口类和一个对应的实现类。

IUserService

package com.spring.demo.demo.service;

public interface IUserService {
    public String findUser(int id);
}

UserService

package com.spring.demo.demo.service;

import org.springframework.stereotype.Service;

@Service
public class UserService implements  IUserService {

    public String findUser(int id) {
        return "用户" + id;
    }
}

在调用时,Controller会直接应用接口类,并添加@Autowired标签。这里的调用原理,即是Sping最著名的DI(依赖注入)和IoC(控制反转)。

package com.spring.demo.demo.controller;

import com.spring.demo.demo.dto.UserCreateRequest;
...

@RestController
public class UserController {

    @Autowired
    IUserService userService;

    @RequestMapping("/user/{id}")
    public String find(@PathVariable int id) {
        return userService.findUser(id);
    }

    @PostMapping("/user")
    public void create(@RequestBody @Validated UserCreateRequest user) {
        System.out.println(user.getName());
    }
}

此时重启Spring,并使用curl进行请求,可以得到结果:

$ curl -XGET 'http://127.0.0.1:8080/user/100' 
$ 用户100

Repository

Spring本身集成了Spring Data JPA,用来作为访问数据库的ORM工具,它采用了Hibernate实现。在本教程,我们将实现User的增和查的工作。

添加依赖

修改pom.xml,添加对mysql和jpa的支持。

  ...
  <dependencies>
    ...
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
  </dependencies>

服务配置

修改/resources/application.properties,添加相关的设置。

...
spring.datasource.url=jdbc:mysql://localhost:3306/demo?useSSL=false
spring.datasource.username=root
spring.datasource.password=123456
...
spring.jpa.database-platform=org.hibernate.dialect.MySQL5Dialect
spring.jpa.show-sql=true

添加Repository和Entity

Repository在DDD中是一个非常重要的概念,字面意思来讲它就是一个仓库。它屏蔽了SQL和数据库操作的细节,使得业务代码无需再考虑更细节的数据处理。它和Mybatis可以说是两个完全相反的流派。

Spring Data JPA中,默认实现了Crud操作的Repository,可以直接继承并使用这个框架进行快速的CRUD实现。

package com.spring.demo.demo.repo.repository;

import com.spring.demo.demo.repo.entity.User;
...

@Repository
public interface UserRepository extends CrudRepository<User, Integer> { }

对应数据库中的数据表,会有一个实体Entity来定义它的表结构。

package com.spring.demo.demo.repo.entity;

import javax.persistence.*;

@Entity
@Table(name="t_user")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String name;
    private Integer gender;

    // Getter & Setter
    ...
}

在Service调用中,即可简单的使用save方法来保存新的User数据。

package com.spring.demo.demo.service;

import com.spring.demo.demo.repo.entity.User;
...

@Service
public class UserService implements  IUserService {

    @Autowired
    UserRepository userRepo;

    public String findUser(int id) {
        return "用户" + id;
    }

    public User createUser(User user) {
        return userRepo.save(user);
    }
}

最后在Controller中,修改Service方法并创建User。

package com.spring.demo.demo.controller;

import com.spring.demo.demo.dto.UserCreateRequest;
...

@RestController
public class UserController {

    @Autowired
    IUserService userService;

    @RequestMapping("/user/{id}")
    public String find(@PathVariable int id) {
        return userService.findUser(id);
    }

    @PostMapping("/user")
    public void create(@RequestBody @Validated UserCreateRequest userReq) {

        User user = new User();
        user.setName(userReq.getName());
        user.setGender(userReq.getGender());

        user = userService.createUser(user);
        System.out.println("创建用户 ID=" + user.getId() + " 用户名=" + user.getName());
    }
}

重启Spring并提交创建请求。

$ curl -XPOST 'http://127.0.0.1:8080/user' -H 'Content-Type: application/json' -d'{"name":"用户A","gender":0}'

可以看到,在console中有创建成功的提示信息。

Hibernate: insert into t_user (gender, name) values (?, ?)

创建用户 ID=1 用户名=用户A

缓存

对于不常改变的数据,常常需要进行缓存以提高系统性能和增加系统吞吐量。对此,Spring Cache提供了缓存的基本实现。

添加依赖

修改pom.xml,添加对cache的支持。

  ...
  <dependencies>
    ...
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-cache</artifactId>
      <optional>true</optional>
    </dependency>
  </dependencies>

服务配置

修改/resources/application.properties,添加相关的设置。

...
spring.cache.type=simple

打开开关

需要在Application中,打开Cache开关。

package com.spring.demo.demo;

import org.springframework.boot.SpringApplication;
...

@SpringBootApplication
@EnableCaching
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}

使用Cacheable标注

对于Cache内容,需要添加一个key名来保存内容。

package com.spring.demo.demo.service;

import com.spring.demo.demo.repo.entity.User;
...

@Service
public class UserService implements  IUserService {

    @Autowired
    UserRepository userRepo;

    @Cacheable(cacheNames = "user")
    public User findUser(Integer id) {
        System.out.println("取得用户操作 ID=" + id);
        return userRepo.findById(id).get();
    }

    public User createUser(User user) {
        return userRepo.save(user);
    }
}

重启Spring,发送GET请求后可以看到,只有第一次执行了SQL操作,说明缓存处理已经完成。

$ curl -XGET 'http://127.0.0.1:8080/user/1' 
$ 用户A
$ 用户A
> 取得用户操作 ID=1=
> Hibernate: select user0_.id as id1_0_0_, user0_.gender as gender2_0_0_, user0_.name as name3_0_0_ from t_user user0_ where user0_.id=?

Redis

Spring的默认Cache处理并不使用Redis,要使用Redis作为缓存应用,需要添加Redis的框架支持。

添加依赖

修改pom.xml,添加对cache的支持。

  ...
  <dependencies>
    ...
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
  </dependencies>

服务配置

修改/resources/application.properties,添加相关的设置。

...
spring.redis.host=127.0.0.1
spring.redis.password=123456
spring.redis.port=6379
spring.redis.jedis.pool.max-active=8

序列化对象

由于要保存到redis中,保存的实体对象需要进行序列化。

package com.spring.demo.demo.repo.entity;

import java.io.Serializable;
...

@Entity
@Table(name="t_user")
public class User implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String name;
    private Integer gender;

    // Getter & Setter
    ...
}

重启Spring,再试一遍上节的操作,仍然是一样的结果,但缓存已经换为redis。

$ curl -XGET 'http://127.0.0.1:8080/user/1' 
$ 用户A
$ 用户A
> 取得用户操作 ID=1
> Hibernate: select user0_.id as id1_0_0_, user0_.gender as gender2_0_0_, user0_.name as name3_0_0_ from t_user user0_ where user0_.id=?
查看原文

赞 0 收藏 0 评论 0

一光年 关注了用户 · 2019-02-27

前端小智 @minnanitkong

我不是什么大牛,我其实想做的就是一个传播者。内容可能过于基础,但对于刚入门的人来说或许是一个窗口,一个解惑之窗。我要先坚持分享20年,大家来一起见证吧。

关注 9153

一光年 发布了文章 · 2019-02-22

Ghost配置6——首页太阳系动画效果

最近在逛知乎时,意外发现了一组CSS效果,其中一个太阳系运行的动画吸引了我。于是我决定把这个效果加到个人博客的首页头部中来。
图片描述

修改首页

首页对应的文件是index.hbs,找到其中的header内容,并修改为:

<header class="site-header outer">
    <div class='solar-syst'>
        <div class='sun'></div>
        <div class='mercury'></div>
        <div class='venus'></div>
        <div class='earth'></div>
        <div class='mars'></div>
        <div class='jupiter'></div>
        <div class='saturn'></div>
        <div class='uranus'></div>
        <div class='neptune'></div>
        <div class='pluto'></div>
        <div class='asteroids-belt'></div>
    </div>
    <div class="inner">
        {{> "site-nav"}}
    </div>
</header>

编写CSS

css代码在作者的codepen上有说明,注意选择编译后的css进行查看。我个人写了一个solar.css保存其内容。
图片描述

.solar-syst以上的css代码都可以删除,并且在该类中加入背景属性:

.solar-syst {
  background: radial-gradient(ellipse at bottom, #1C2837 0%, #050608 100%);
  margin: 0 auto;
  width: 100%;
  height: 600px;
  position: relative;
}
...

添加CSS

部署css

编辑好的solar.css文件,放置在ghost/content/themes/casper/assets/css下面。

引入css

修改default.hbs,在header中加入css引用。

...
<head>
    ...
    <link rel="stylesheet" type="text/css" href="{{asset "css/solar.css"}}" />
    ...
</head>
<body ...>
...
</body>

以上工作完成后,重启Ghost即可查看博客的新动画效果。

查看原文

赞 0 收藏 0 评论 0

一光年 发布了文章 · 2019-01-31

Flutter尝鲜3——动画处理<并行和串行>

图片描述

本例的代码参考这里

并行动画

当多个动画定义同时指向某个组件,并使用动画控制器启动时,就产生了并行动画(Parallel Animation)。例如我们可以让一个组件:

  • 移动的同时改变大小
  • 旋转的同时边界颜色闪烁
  • 圆形图片模糊的同时形状越来越方

总之,掌握了动画原理以后我们知道,只要能将一个动画抽象值与一个组件的某个外观属性值联系起来,那么就能在动画中展现出连续平滑的外观变化。这一点,任何平台(Web、Android)的原理都是一致的。

例子

接前一篇的例子,我们让一个移动的正方形在位移过程中逐渐变为圆形。

在已有的animation基础上,再添加一个新的animation用以控制动画组件的边角半径。

class ParallelDemoState extends State<ParallelDemo> with SingleTickerProviderStateMixin {
    ...
    Tween<double> slideTween = Tween(begin: 0.0, end: 200.0);
    Tween<double> borderTween = Tween(begin: 0.0, end: 40.0);  // 添加边角半径变动范围
    Animation<double> slideAnimation;
    Animation<double> borderAnimation; // 添加边角半径动画定义
    
    @override
    void initState() {
        ...
        controller = AnimationController(duration: Duration(milliseconds: 2000), vsync: this);
        slideAnimation = slideTween.animate(CurvedAnimation(parent: controller, curve: Curves.linear));
        borderAnimation = borderTween.animate(CurvedAnimation(parent: controller, curve: Curves.linear)); // 定义边角半径动画
    }
    
    ...
        
    @override
    Widget build(BuildContext context) {
        return Container(
            width: 200,
            alignment: Alignment.centerLeft,
            child: Container(
                margin: EdgeInsets.only(left: slideAnimation.value),
                decoration: BoxDecoration(
                    borderRadius: BorderRadius.circular(borderAnimation.value), // 边角半径的属性上添加动画
                    color: Colors.blue,
                ),
                width: 80,
                height: 80,
            ),
        );
    }
}

串行动画

串行动画(Sequential Animation)顾名思义,多个动画像肉串一样一个接一个的发生。但这只是从现象上观察出的结果,实际的运行方式和并行动画差别不大。串行动画的关键之处在于,它为每个动画的发生设定了一个计时器,只有到特定时间点时,特定的动画效果才会发生。

例如设计一个3秒钟的动画:

  • 移动动画从0秒开始,持续1秒
  • 旋转动画从1秒开始,持续1.5秒
  • 缩放动画从2秒开始,持续0.7秒

那么,最后的动画效果便是:

  1. 0~1秒,动画元素在移动
  2. 1~2秒,动画元素在旋转
  3. 2~2.5秒,动画既在旋转又在缩放
  4. 2.5~2.7秒,动画在缩放
  5. 2.7~3秒,动画静止不动

例子

在串行动画例子的基础上,我们加上计时器Interval的处理。Interval有三个参数,前两个参数指示了动画的开始和结束时间。这两个参数都是以动画控制器的Duration时长的比例来计算的。例如:

  • Slide动画分别为0.0和0.5,表示动画从0秒(2000ms 0.0)这个时间点开始,至1秒(2000ms 0.5)这个时间点结束
  • Border动画分别为0.5和1.0,表示动画从1秒(2000ms 0.5)这个时间点开始,至2秒(2000ms 1.0)这个时间点结束
class SequentialDemoState extends State<ParallelDemo> with SingleTickerProviderStateMixin {
    ...
    
    @override
    void initState() {
        ...
        controller = AnimationController(duration: Duration(milliseconds: 2000), vsync: this);
        // slideAnimation = slideTween.animate(CurvedAnimation(parent: controller, curve: Curves.linear));
        // borderAnimation = borderTween.animate(CurvedAnimation(parent: controller, curve: Curves.linear)); // 定义边角半径动画
        
        // 换一种写法,加入Interval
        slideAnimation = slideTween.animate(CurveTween(curve: Interval(0.0, 0.5, curve: Curves.linear)).animate(controller));
        borderAnimation = borderTween.animate(CurveTween(curve: Interval(0.5, 1.0, curve: Curves.linear)).animate(controller));
    }
    
    ...
        
    @override
    Widget build(BuildContext context) {
        return Container(
            width: 200,
            alignment: Alignment.centerLeft,
            child: Container(
                margin: EdgeInsets.only(left: slideAnimation.value),
                decoration: BoxDecoration(
                    borderRadius: BorderRadius.circular(borderAnimation.value), // 边角半径的属性上添加动画
                    color: Colors.blue,
                ),
                width: 80,
                height: 80,
            ),
        );
    }
}
查看原文

赞 2 收藏 2 评论 0

一光年 发布了文章 · 2019-01-25

Flutter尝鲜2——动画处理<基础>

本例的代码参考这里

概述

动画处理的基本原理是,对组件(widget)的某个或某组属性设置一组连续变化的值,这些值在一定时间间隔内不断被应用到该属性上,使得组件的外观看上去在进行平滑而连续的变动。

例如2秒内每隔0.1s将一个组件的x轴坐标加1,那么该组件看上去就是从左至右移动了2秒共20个单位。
图片描述

处理组成部分

具体到Flutter,动画处理主要分为三个部分:

  • 动画控制器(AnimationController),控制整个动画运行,包括开始结束和动画时长等。
  • 动画抽象(Animation),描述了动画运动的速率,例如组件是加速还是匀速,或者其它变化。
  • 变动范围(Tween),定义了动画组件属性值的变化范围,例如从坐标(0, 0)移动到(20, 0)

处理流程

上述三大组件,控制了整个动画的运行。用文字描述,其流程主要包括:

  1. 初始化动画控制器,设定动画的时长,初始值等(如上例:2秒时长)
  2. 初始化变动范围(如上例:Offset从[0, 0]到[20, 0])
  3. 初始化动画抽象,定义它的运动速率(如上例:匀速变动)
  4. 将动画描述的值,赋值到动画组件的对应属性上
  5. 开始执行动画(调用动画控制器的开始方法)
  6. 动画执行结束

AnimationController定义

AnimationController是一个特殊的Animation对象。创建一个AnimationController时,需要传递一个vsync参数。设置此参数的目的,是希望屏幕每一帧画面变化时能够被通知到。也就是说,屏幕刷新的每一帧,AnimationController都会生成一个新的值(同样也意味着,如果在屏幕外那么就不被触发)。这样动画组件就能够完成一个连续平滑的动画动作。

Tickers can be used by any object that wants to be notified whenever a frame triggers。

AnimationControler通常是在一个StatefulWidget中被声明,并且附带一个叫做SingleTickerProviderStateMixin的Mixin(原因就在上面说的,要设置vsync参数)。

class AnimationDemo extends StatefulWidget {
    AnimationDemoState createState() => AnimationDemoState();
}

class AnimationDemoState extends State<AnimationDemo> with SingleTickerProviderStateMixin {
    
    AnimationController controller;
    
    @override
    void initState() {
        super.initState();
        
        controller = AnimationController(duration: Duration(milliseconds: 2000), vsync: this);
        ...
    }
    
    @override
    void dispose() {
        controller.dispose();  // 离开时需要销毁controller
        super.dispose();
    }
    ...
}

当Animation和Tween的设置完成后,简单调用controller.forward()即可开始动画。

Tween定义

Tween就是要改变的属性值的变动范围。它可以是任意的属性类如Offset或者Color,最常见的是double。

...
    AnimationController controller;
    Tween<double> slideTween = Tween(begin: 0.0, end: 20.0);
...

Animation定义

Animation对象本身可以看做是动画中所有变化值的一个集合。它包含了变化区间内的所有可取值,并返回给动画组件当前的变动值。

Animation在使用中要设置的,是他的变动速率,如Curves.linear(线性变化)。

...
    AnimationController controller;
    Tween<double> slideTween = Tween(begin: 0.0, end: 20.0);
    Animation<double> animation;
    
    @override
    void initState() {
        super.initState();
        
        ...
        animation = slideTween.animate(CurvedAnimation(parent: controller, curve: Curves.linear));
    }
...

动画组件定义

为了说明简单,在build方法中嵌套两个Container组件,外部容器Container的paddingLeft跟随动画变动,达到移动内部Container的目的。

class AnimationDemoState extends State<AnimationDemo> with SingleTickerProviderStateMixin {
    ...
    
    @override
    Widget build(BuildContext context) {
        return Container(
            width: 200,
            alignment: Alignment.centerLeft,
            padding: EdgeInsets.only(left: animation.value),
            child: Container(
              color: Colors.blue,
              width: 80,
              height: 80,
            ),
        );
    }
}

启动动画

在启动动画之前有一个Flutter的基本概念要说明。做过React的同学很清楚,要想render方法重新执行,要么props有更新要么state有更新。在Flutter也同样如此,build方法同样依赖于state的更新才能重新执行。

在AnimationController的说明中,我们知道因为设置了vsync所以屏幕刷新的每一帧都会更新它的值。所以可以在Controller上加上一个listener,每次有update都调用一下setState,以此达到重新渲染UI的目的。

...
    @override
    void initState() {
        ...
        animation.addListener(() => this.setState(() {}));

        controller.repeat(); // 动画重复执行
    }

调用controller.repeat()方法,动画会被反复执行。如果想只执行一次,那么可以使用controller.forward();

查看原文

赞 0 收藏 0 评论 0

一光年 发布了文章 · 2019-01-22

Flutter尝鲜1——3步骤使用自定义Icon

官方Icon

Flutter本身自带了MaterialDesign的图标集,在pubspec.yaml中有如下配置

...
flutter:
  users-material-design: true
...

通过以上配置,就可以在代码中引用任何MD的官方图标(需翻墙)。这些图片都定义在了IconDatas中。

Icon(Icons.favorite)

第三方Icon

第三方图标库和MD的图片库在使用上没有区别,但需要手动引入和配置路径。为了方便复用,我们可以把图标制作为一个第三方库来调用。例如:

...
import 'package:my_icon/my_icon.dart';

...
Icon icon = Icon(MyIcon.zhihu);   # 知乎LOGO

制作Icon库

1.制作ttf文件

一般我们会在iconfont.cn上去寻找合适的图标集或自行绘制,完成后打包下载,压缩包里有制作好的ttf文件。
图片描述

2.编写配置文件

作为示例,在/lib目录下创建一个名为my_font的文件夹,文件夹中的pubspec.yaml内容如下:

name: my_font
description: The font for my application
author: Lynx <lynx86@126.com>
homepage: http://www.a-lightyear.com/
version: 1.0.0

environment:
  sdk: ">=2.0.0-dev.28.0 <3.0.0"

dependencies:
  flutter:
    sdk: flutter

dev_dependencies:
  recase: "^2.0.0+1"

flutter:
  fonts:
    - family: MyIcon
      fonts:
        - asset: lib/fonts/iconfont.ttf
          weight: 400

从配置文件看出,iconfont下载的ttf文件放在/lib/my_font/lib/fonts/下面,该路径可以自行设置。

3.编写库文件

library font_social_flutter;
    
import 'package:flutter/widgets.dart';

class MyIcon {
  static const IconData zhihu = const _MyIconData(0xe6a2);
  static const IconData wechat = const _MyIconData(0xe697);
  static const IconData alipay = const _MyIconData(0xe698);
  static const IconData weibo = const _MyIconData(0xe6ab);
  static const IconData wechat_friends = const _MyIconData(0xe6ae);
  static const IconData qq = const _MyIconData(0xe6ac);
}

class _MyIconData extends IconData {
  const _MyIconData(int codePoint)
      : super(
    codePoint,
    fontFamily: 'MyIcon',
    fontPackage: 'my_icon',
  );
}

这里的0xe6a2即为每个Icon的unicode字符。在iconfont下载包里有一个html文件,打开后可以看到每个图片的unicode值。
图片描述

使用Icon

引入Icon库

在使用之前,需要把该库引入到当前flutter工程中。编辑flutter项目的pubspec.yaml,添加如下内容:

...
dependencies:
  flutter:
    sdk: flutter
  ...
  my_icon:
      path: lib/my_icon/   # 在这里引入第三方icon库
  ...
...

使用Icon

如开篇所述,在做好以上准备工作后,即可以如MD图标一般方便的引入自制的图标集。

...
import 'package:my_icon/my_icon.dart';

...
Icon icon = Icon(MyIcon.zhihu);   # 知乎LOGO

图片描述

查看原文

赞 7 收藏 5 评论 1

一光年 发布了文章 · 2019-01-11

运维记录1——解决在Nginx下部署CRA项目,二级目录不能访问的问题

如果从头开始搭建React项目,create-react-app通常是开发者的首选。毕竟不是谁都有精力去了解WebPack的复杂配置,而CRA将配置隐藏开箱即用的特性必然会受到普遍欢迎。

根目录访问

到了部署阶段,我通常使用nginx作为web容器,将项目部署到一个根目录下访问。如

# nginx配置
server {
  listen 80;
  server_name my.website.com;
  
  ...
  
  location / {
    alias /data/www/react-project/dist;
    index index.html
  }
}

那么只要我们将项目文件放到对应的目录下,重启nginx即可开始访问web页面。

二级目录访问

有时我们有多个web项目,多个项目不可能同时挂在根目录下,所以我们会划分二级目录来分别访问各个web项目。如

问题1:CSS等资源加载失败

此时,如果简单将nginx配置的location改为/project1,则会出现网页无法访问的错误。

# nginx配置
server {
  listen 80;
  server_name my.website.com;
  
  ...
  
  # location / {
  location /project1 {
    alias /data/www/react-project/dist;
    index index.html
  }
}

现象

从dev工具可以看出,html文件有取得,但css、js等资源引用失败。css和js的文件路径都是http://my.website.com/static/...(或css)。

分析

CRA(create-react-app)的项目配置默认是跑在根目录下的。如果查看dist目录下的html会发现,所有的css或js文件的引用路径都是/开头的绝对路径。

解决

将打包路径从绝对路径改为相对路径:

# package.json
{
  ...
  "homepage": ".",   // 添加homepage属性,将路径改为当前目录
  ...
}

重新编译后看到,所有的资源文件路径都改过来了。

问题2:加载成功,网页空白

重新上传到服务器,更新dist目录下的文件,重启nginx后访问网页。

现象

结果发现,网页仍然是空白一片。查看html的渲染结果,发现似乎js并没有执行。

分析

在react-router-dom的例子中,通常使用的是BrowserRouter。这种类型的Router在向服务器发送请求时,如果相对于二级目录的路由去指向对应的页面路由,就会找不到资源,因此也就不会渲染。

解决

BrowserRouter有一个属性叫做basename,就是用于解决此类问题。

...
import { Route, BrowserRouter as Router, Switch, Redirect } from 'react-router-dom';
...
...
  <Router basename='/project1'>
    <Switch>
      <Redirect exact key='index' path='/' to='/home' />
      { routes }
    </Switch>
  </Router>
...

问题3:访问成功,刷新后404

修改以上配置并编译部署,重启nginx后可正常访问网页。但刷新后,网页变为一片空白。

现象

网页显示,在请求页面路由如http://my.website.com/project... 时,该路由的请求状态为404。

分析

还是因为BrowserRouter的问题,之前能正常访问时因为路由中设置了Redirect,所以能访问到根目录并自动跳转到/home。但直接访问则会访问失败。

解决

在nginx配置中加入try_files命令

  location /project1 {
    alias /data/www/react-project/dist;
    # index index.html
    try_files $uri /project1/index.html
  }

这样,在请求$uri时如果找不到对应的资源,会fallback回去加载index.html。

问题解决。

查看原文

赞 2 收藏 1 评论 1

一光年 发布了文章 · 2018-12-26

JS使用技巧2——momentjs太重了吗?试试dayjs和miment吧

关于时间的操作,一直在使用momentjs这个库。方便灵巧,功能强大。唯一的缺点是,对于前端HTML来讲,它的包太太太太太大了。

我是momentjs的重度用户,但它的大小时刻都在折磨人。虽然方便高效,可这动辄200K的大小,对于首页加载速度来讲简直就是一场灾难。所以,开源社区有了一些精简的方案。如dayjsmiment

dayjs

图片描述

dayjs本身就是对标momentjs进行开发的,看作者的官方介绍:

Day.js is a minimalist JavaScript library that parses, validates, manipulates, and displays dates and times for modern browsers with a largely Moment.js-compatible API. If you use Moment.js, you already know how to use Day.js.

它的用法非常简单。

dayjs().startOf('month').add(1, 'day').set('year', 2018).format('YYYY-MM-DD HH:mm:ss');

是不是很momentjs很相似?不,其实它们就是一模一样的。dayjs的API和moment几乎一模一样,所以如果想要替换到现有的momentjs代码,直接替换为dayjs即可,调用语句绝大部分情况下可以一字不改。

dayjs的大小有多少呢?2KB。再想想momentjs的大小。

miment

miment同样也是一个极简的时间处理库,压缩后的代码甚至达到了1KB左右,比dayjs还小。

与包大小相应的,作者团队只保留了momentjs中核心方法,但其实这些方法在普通场景下已经足够。

miment的使用方法,也和momentjs基本一致。例如:

miment().add(1, 'YYYY').add(2, 'MM').add(-3, 'DD') // 增加 1 年 2 个月又减回 3 天

miment().isBetween('2000-01-01','2020-01-01') // true

miment().isBefore('2000-01-01') // false

miment().format('YYYY年MM月DD日 星期ww') // 2018年04月09日 星期1 *周日对应星期0*

想要取得单独的年月日,更简单

miment().format('YYYY') // 2018
miment().format('MM') // 04
miment().format('DD') // 09
miment().format('hh') // 23
miment().format('mm') // 57
miment().format('ss') // 16
miment().format('SSS') // 063
miment().format('ww') // 1
miment().format('WW') // 一

结语

对于momentjs,大部分开发者都是又爱又恨,又或者大觉不爱。其实对于绝大部分的时间操作场景,dayjs和miment更符合使用要求。尤其对于非SSR的场合,想想那精简近200KB的首屏渲染速度,真的是非常有吸引力。

查看原文

赞 3 收藏 2 评论 0

一光年 赞了文章 · 2018-12-25

Apollo GraphQL 在 webapp 中应用的思考

Apollo GraphQL 在 webapp 中应用的思考

原文发表在: https://github.com/kuitos/kui...

简介

熟悉 Apollo GraphQL 的同学可直接跳过这一章,从 实践 一章看起。

GraphQL 作为 FaceBook 2015年推出的 API 定义/查询 语言,在历经了两年的发展之后,社区已相对发达和完善。对于 GraphQL 的一些基础概念,本文不再一一赘述,目前社区相关的文章已经很多,有兴趣的同学可以去 google,或者直接看GraphQL 官方教程Apollo GraphQL Server 官方文档

Apollo GraphQL 作为目前社区最流行的 GraphQL 解决方案提供商,提供了从 client 到 server 的一整套完整的工具链。在这里我也准备以 Apollo 为例,通过一步步搭建 Apollo GraphQL Server 的方式,来给大家展示 GraphQL 的特点,以及我的一些思考(主要是我的思考?)。

setup

创建基于 express 的 GraphQL server

// server.js
import express from 'express';
import { graphiqlExpress, graphqlExpress } from 'apollo-server-express';
import schema from './models';

const PORT = 8080;
const app = express();

...
app.use('/graphql', graphqlExpress({ schema }));
app.use('/graphiql', graphiqlExpress({
    endpointURL: '/graphql'
}));

if (process.env.NODE_ENV === 'development') {
    glob(path.resolve(__dirname, './mock/**/*.js'), {}, (er, modules) => modules.forEach(module => require(module).default(app)));
}

app.listen(PORT, () => console.log(`> Listening at port ${PORT}`));

执行 node server.js,这样我们就能启动一个 GraphQL server 了。

注意我们这里使用了 apollo-server-express 提供的 graphiqlExpress 插件,graphiql 是一个用于浏览器端调试 graphql 接口的 GUI 工具。服务启动后,我们在浏览器打开 http://localhost:8080/graphiql就可以看到这样一个页面

定义 API schema

我们在 server.js 中定义了这样一个 endpoint : app.use('/graphql', graphqlExpress({ schema }));

这里传入的 schema 是什么呢?它大概长这样:

import { makeExecutableSchema } from 'graphql-tools';
// The GraphQL schema in string form
const typeDefs = `
  type User { 
    id: ID!
    name: String
    age: Int
  }
  type Query { user(id: ID!): User }
  schema { query: Query }
`;

// The resolvers
const resolvers = {
  Query: { user({id}) { return http.get(`/users/${id}`)}}
};

// Put together a schema
const schema = makeExecutableSchema({
  typeDefs,
  resolvers
});

app.use('/graphql', graphqlExpress({ schema }));

这里的关键是用了 graphql-tools 这个库提供的 makeExecutableSchema 组合了 schema 定义和对应的 resolver。resolver 是 Apollo GraphQL 工具链中提出的一个概念,什么用呢?就是在我们客户端请求过来的 schema 中的 field 如果在 GraphQL Server 中有对应的 resolver,那么在返回数据时候,这些 field 就由对应的 resolver 的执行结果填充(支持返回 promise)。

客户端请求

这里借助 graphiql 面板的功能来发送请求:

看一下 http request payload 信息:

响应体:

也就是说,无论你是用你熟悉的 http lib 还是社区的 apollo client,只要按照 GraphQL Server 要求的既定格式发请求就 ok 了。

这里我们使用了 GraphQL 中的 variable 语法,事实上在这种需要传参的动态查询场景下,我们应该总是使用这种方式发送请求:即一个 static query + variable 的方式,而不是在运行时动态的生成 query string。这也是官方建议的最佳实践。

更复杂的嵌套查询场景

假设我们有这样一个场景,即我们需要取到 User Entity 下的 nick 字段,而 nick 数据并不来自于 user 接口,而是需要根据 userId 调用另一个接口取得。这时候我们服务端的代码需要这样写。

// schema
type User {
  id: ID!
  name: String
  age: Int
  nick: String
}
// resolver
User: {
  nick({ id }) {
    return getUserNick(id);
  }
}

resolver 的参数列表中包含了当前所在 Entity 已有的数据,所以这里可以直接在函数的入参里取到已查询出来的 userId。

看下效果:

服务端的请求:

可以看到,这里多出了查询 nick 的请求。也就是说,GraphQL Server 只有在客户端提交了包含相应字段的 query 时,才会真正去发送相应的请求。更多 resolver 说明可以看这里

其他

在真实的生产环境中,我们通常会有更多更复杂的场景,比如接口的权限认证、分页、缓存、批量提交、schema 模块化等需求,好在社区都有相对应的一些解决方案,这不是本文的重点所以不在这里一一介绍了,有兴趣的可以去看下我之前写的 graphql-server-startkit,或者官方的 demo

实践

如果你真实的使用过 Apollo GraphQL,你会经历如下过程:

  1. 定义一个 schema 用于描述查询入口

    // schema.graphql
    type User {
        id: ID!
        name: String
        nick: String
        age: Int
        gender: String
    }
    type Query {
        user(id: ID!): User
    }
    schema {
        query: Query
    }
  2. 编写 resolver 解析对应类型

    const resolvers = {
        Query: {
            user(root, { id }) {
                return getUser(id);
            }
        },
        User: {
            nick({ id }) {
                return getUserNick(id);
            }
        }
    };
  3. 编写客户端请求代码调用 GraphQL 接口,通常我们会封装一个 get 方法

    function getUser(id) {
      // 以 axios 为例
      return axios.post('/graphql', { query: 'query userQuery($id: ID!) {↵    user(id: $id) {↵    id↵    name↵    nick↵  }↵}', operationName: "userQuery", variables: {id}});
    }

    如果你的项目中加入了静态类型系统,那么你的代码可能就会变成这样:

    // 以 ts 为例
    interface User {
      id: number
      name: string
      nick: string
      age: number
      gender: string
    }
    function getUser(id: number): User {
      return axios.post('/graphql', { query: 'query userQuery($id: ID!) {↵    user(id: $id) {↵    id↵    name↵    nick↵  }↵}', operationName: "userQuery", variables: {id}});
    }

写到这里你可能已经发现,不仅是 entity 类型定义,就连接口的封装,我们在服务端和客户端都重复了一遍(虽然一个用的 GraphQL Type Language 一个用的 TS)… 这还是最简单的场景,如果业务模型复杂起来,你在两端需要重复的代码会更多(比如类型的嵌套定义和 resolve)。这时候你可能会想起 DRY 原则,然后开始思考有没有什么方式可以使得类型及接口定义能两端复用,或者根据一端的定义自动生成另一端的代码?甚至你开始怀疑,到底有没有引入 GraphQL 的必要?

思考

GraphQL 作为一个标准化并自带类型系统的 API Layer,其工程价值我也不再过多广告了。只是在实践过程中,既然我们无法完全避免服务端与客户端的实体与接口定义重复(使用 apollo-codegen 可以避免一部分),而且对于大部分小团队而言,运维一个 productive nodejs system 实际上都是力有未逮。那么我们是不是可以考虑在纯客户端构建一个类 GraphQL 的 API Layer 呢?这样既可以有效的避免编码重复,也能大大的降低对团队的要求,可操作的空间也比增加一个 nodejs 中间层大得多。

我们可以回忆一下,通常对于一个前端而言,促使我们需要一个 API Layer 的原因是什么:

  1. 后端接口设计不够 restful,命名垃圾,用的时候看见那个*一样的 url 就难受。
  2. 后端同学只愿意写 microservice,提供聚合服务的 web api 被认为没有技术含量,不愿意写。你需要一个数据,他告诉你需要调 a、b、c 三个接口,然后根据 id 组装合并。
  3. 接口返回的数据格式各种嵌套及不合理,不是前端想要的结构。
  4. 接口返回的数据字段命名随意或者风格不统一,我有强迫症用这种接口会发疯。
  5. 后端返回的 数据格式/字段名 一旦变了,前端视图绑定部分的代码需要修改。

通常情况下,碰到这些问题,你可能去跟后端同学据理力争,要求他们提供调用体验更良好设计更优雅的接口。没错这很好,毕竟为了追求完美去跟各种人撕(跟后端撕、跟产品撕、跟UI撕)是一个前端工程师基本的职业素养。但是如果你每天都被撕逼弄得心力交瘁,甚至是你根本找不到撕的对象(比如数据来源接口来着几个不同部门,甚至是一些祖传的没人敢动的接口),这些时候大概就是你迫切希望有一个 API Layer 的时候了。

如何在客户端实现一个 API Layer

其实很简单,你只需要在客户端把 Apollo Server 中要写的 resolvers 写一遍,然后配上一些性能提升手段(如缓存等),你的 API Layer 就完成了。

比如我们在src下新建一个 loaders/apis 目录,所有的数据拉取接口都放在这里。比如这样:

// UserLoader.ts
export interface User {
  id: number
  name: string
  nick: string
}

export default class UserLoader {
  
  async getUser(id: number): User {
    const base = await Promise.all([http.get('//xxx.com/users/${id}'), this.getUserNick(id)]);
    const user = base.reduce((acc, info) => ({...acc, ...info}), {});
    return user;
  }
  
  getUserNick(id: number): string {
    return http.get(`//xxx.com/nicks/${id}`);
  }
}

然后在你业务需要的地方注入相应 loader 调用接口即可,如:

import { inject } from 'mmlpx';
import UserLoader from './UserLoader';
// Controller.ts
export default class Controller {
  
  @inject(UserLoader)
  userLoader = null;
  
  async doSomething() {
    // ...
    const user = await this.userLoader.getUser(this.id);
    // ...
  }
}

如果你不喜欢依赖注入的方式,loaders/apis 层直接 export function getUser 也可以。

如果你碰到了上面描述的第 3、4 、5 三种问题,你可能还需要在这一层做一下数据格式化。比如这样:

async getUser(id: number): User {
  const base = await Promise.all([http.get('//xxx.com/users/${id}'), this.getUserNick(id)]);
  const user = base.reduce((acc, info) => ({...acc, ...info}), {});
  
  return {
    id: user.id,
    name: user.user_name, // 重命名字段
    nick: user.nick.userNick  // 剔除原始数据中无意义的层次结构
  };
}

经过这一层的数据处理,我们就能确保我们的应用运行在前端自己定义的数据模型之下。这样之后后端接口不论是数据结构还是字段名的变更,我们只需要在这一层做简单调整即可,而不会影响到我们上层的业务及视图。相应的,我们的业务层逻辑不再会直接对接接口 url,而是将其隐藏在 API Layer 下,这样不仅能提升业务代码的可读性,也能做到眼不见为净。。。

总结

熟悉 GraphQL 的同学可能会很快意识到,我这不过是在客户端做了一个简单的 API 封装嘛,并不能解决在 GraphQL 出现之前的 lots of roundtrips 及 overfetching 问题。但事实上是 roundtrip 的问题我们可以通过客户端缓存来缓解(如果你用的是 axios 你可能需要 axios-extensions ),而且 roundtrip 的问题其实本质上我们不过是将客户端的 http 开销转移到服务端了而已。在客户端与服务端均不考虑缓存的情况,客户端反而会少一个请求。。。overfetching 问题则取决于 backend service 的粒度,如果 endpoint 不够 micro,即便是 GraphQL,也会出现接口数据冗余问题,毕竟 GraphQL 不生产数据,它只是数据的搬运工。。。而如果 endpoint 粒度足够小,那么我在客户端 API 层多开几个接口(换成 Apollo 也要多写几个 resolver),一样可以按需取数据。服务端 API Layer 只有一个不可替代的优势就是,如果我们的数据源接口是不支持跨域或者仅内网可见的,那么就只能在服务端开个口子做代理了。另外一个优势就是,GraphQL Server 的 http 开销是可控的,毕竟机器是我们自己控制,而客户端的环境则不可控(http 开销受终端设备及网络环境影响,比如低版本浏览器或者低速网络,均会导致 http 开销的性能权重增大)。

可能有同学会说,服务端 API Layer 部署一次任何系统都可以共享其服务,而客户端 API Layer 的作用域只在某一项目。其实,如果我们把某一项目需要共享的 API Layer 打成一个 npm 包发布出去,不也能达到同样的效果吗,很多平台的 js sdk 不都是这个思路么(这里只讨论 web 开发范畴)。

在我看来,不论你是否会搭建一个服务端的 API Layer,我们其实都需要有一个客户端 API Layer 从数据源头来保证客户端数据的模型统一及一致性,从而有足够的能力应对接口的变迁。如果你考虑的再远一点,在 API Layer 服务的业务模型层,我们同样需要有一套独立的 Service/Model Layer 来应对视图框架的变迁。这个暂且按下不表,后面会再写篇文字来详细说一下我的思路。

事实上,对于大部分团队而言,客户端 API Layer 已经够用了,增加一层 GraphQL 并不是那么必要。而且如果没有很好的支持将客户端接口转换成 GraphQL Schema 和 resolver 的工具时,我们并不能很愉快的 coding,毕竟两端重复的工作还是有点多。

查看原文

赞 12 收藏 10 评论 1

一光年 赞了文章 · 2018-12-23

全面了解 React 新功能: Suspense 和 Hooks

悄悄的, React v16.7 发布了。 React v16.7: No, This Is Not The One With Hooks.

clipboard.png

最近我也一直在关注这两个功能,也听了程墨大佬的React讲座,十分受用,就花些时间就整理了一下, 在此分享给大家, 希望对大家有所帮助。


引子

为什么不推荐在 componentwillmount 里最获取数据的操作呢?

这个问题被过问很多遍了, 前几天又讨论到这个问题, 就以这个作为切入点吧。

有些朋友可能会想, 数据早点获取回来,页面就能快点渲染出来呀, 提升用户体验, 何乐而为不为?

这个问题, 简单回答起来就是, 因为是可能会调用多次

要深入回答这个问题, 就不得不提到一个React 的核心概念: React Fiber.

一些必须要先了解的背景

React Fiber

React Fiber 是在 v16 的时候引入的一个全新架构, 旨在解决异步渲染问题。

新的架构使得使得 React 用异步渲染成为可能,但要注意,这个改变只是让异步渲染成为可能

但是React 却并没有在 v16 发布的时候立刻开启,也就是说,React 在 v16 发布之后依然使用的是同步渲染

不过,虽然异步渲染没有立刻采用,Fiber 架构还是打开了通向新世界的大门,React v16 一系列新功能几乎都是基于 Fiber 架构。

说到这, 也要说一下 同步渲染异步渲染.

同步渲染 和 异步渲染

同步渲染

我们都知道React 是facebook 推出的, 他们内部也在大量使用这个框架,(个人感觉是很良心了, 内部推动, 而不是丢出去拿用户当小白鼠), 然后就发现了很多问题, 比较突出的就是渲染问题

他们的应用是比较复杂的, 组件树也是非常庞大, 假设有一千个组件要渲染, 每个耗费1ms, 一千个就是1000ms, 由于javascript 是单线程的, 这 1000ms 里 CPU 都在努力的干活, 一旦开始,中间就不会停。 如果这时候用户去操作, 比如输入, 点击按钮, 此时页面是没有响应的。 等更新完了, 你之前的那些输入就会啪啪啪一下子出来了。

这就是我们说的页面卡顿, 用起来很不爽, 体验不好。

这个问题和设备性能没有多大关系, 归根结底还是同步渲染机制的问题。

目前的React 版本(v16.7), 当组件树很大的时候,也会出现这个问题, 逐层渲染, 逐渐深入,不更新完就不会停

函数调用栈如图所示:

clipboard.png

因为JavaScript单线程的特点,每个同步任务不能耗时太长,不然就会让程序不会对其他输入作出相应,React的更新过程就是犯了这个禁忌,而React Fiber就是要改变现状。

异步渲染

Fiber 的做法是:分片。

把一个很耗时的任务分成很多小片,每一个小片的运行时间很短,虽然总时间依然很长,但是在每个小片执行完之后,都给其他任务一个执行的机会,这样唯一的线程就不会被独占,其他任务依然有运行的机会。 而维护每一个分片的数据结构, 就是Fiber

用一张图来展示Fiber 的碎片化更新过程:

clipboard.png

中间每一个波谷代表深入某个分片的执行过程,每个波峰就是一个分片执行结束交还控制权的时机。

更详细的信息可以看: Lin Clark - A Cartoon Intro to Fiber - React Conf 2017

在React Fiber中,一次更新过程会分成多个分片完成,所以完全有可能一个更新任务还没有完成,就被另一个更高优先级的更新过程打断,这时候,优先级高的更新任务会优先处理完,而低优先级更新任务所做的工作则会完全作废,然后等待机会重头再来

因为一个更新过程可能被打断,所以React Fiber一个更新过程被分为两个阶段: render phase and commit phase.

两个重要概念: render phase and commit phase

有了Fiber 之后, react 的渲染过程不再是一旦开始就不能终止的模式了, 而是划分成为了两个过程: 第一阶段和第二阶段, 也就是官网所谓的 render phase and commit phase

在 Render phase 中, React Fiber会找出需要更新哪些DOM,这个阶段是可以被打断的, 而到了第二阶段commit phase, 就一鼓作气把DOM更新完,绝不会被打断。

两个阶段的分界点

这两个阶段, 分界点是什么呢?

其实是 render 函数。 而且, render 函数 也是属于 第一阶段 render phase 的

那这两个 phase 包含的的生命周期函数有哪些呢?

render phase:

  • componentWillMount
  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate

commit phase:

  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

clipboard.png

因为第一阶段的过程会被打断而且“重头再来”,就会造成意想不到的情况。

比如说,一个低优先级的任务A正在执行,已经调用了某个组件的componentWillUpdate函数,接下来发现自己的时间分片已经用完了,于是冒出水面,看看有没有紧急任务,哎呀,真的有一个紧急任务B,接下来React Fiber就会去执行这个紧急任务B,任务A虽然进行了一半,但是没办法,只能完全放弃,等到任务B全搞定之后,任务A重头来一遍,注意,是重头来一遍,不是从刚才中段的部分开始,也就是说,componentWillUpdate函数会被再调用一次。

在现有的React中,每个生命周期函数在一个加载或者更新过程中绝对只会被调用一次;在React Fiber中,不再是这样了,第一阶段中的生命周期函数在一次加载和更新过程中可能会被多次调用!

这里也可以回答文行开头的那个问题了, 当然, 在异步渲染模式没有开启之前, 你可以在 willMount 里做ajax (不建议)。 首先,一个组件的 componentWillMount 比 componentDidMount 也早调用不了几微秒,性能没啥提高,而且如果开启了异步渲染, 这就难受了。 React 官方也意识到了这个问题,觉得有必要去劝告(威胁, 阻止)开发者不要在render phase 里写有副作用的代码了(副作用:简单说就是做本函数之外的事情,比如改一个全局变量, ajax之类)。

static getDerivedStateFromProps(nextProps, prevState) {
  //根据nextProps和prevState计算出预期的状态改变,返回结果会被送给setState
}

新的静态方法

为了减少(避免?)一些开发者的骚操作,React v16.3,干脆引入了一个新的生命周期函数 getDerivedStateFromProps, 这个函数是一个 static 函数,也是一个纯函数,里面不能通过 this 访问到当前组件(强制避免一些有副作用的操作),输入只能通过参数,对组件渲染的影响只能通过返回值。目的大概也是让开发者逐步去适应异步渲染。

我们再看一下 React v16.3 之前的的生命周期函数 示意图:

clipboard.png

再看看16.3的示意图:

clipboard.png

上图中并包含全部React生命周期函数,另外在React v16发布时,还增加了一个componentDidCatch,当异常发生时,一个可以捕捉到异常的componentDidCatch就排上用场了。不过,很快React觉着这还不够,在v16.6.0又推出了一个新的捕捉异常的生命周期函数getDerivedStateFromError

如果异常发生在render阶段,React就会调用getDerivedStateFromError,如果异常发生在第commit阶段,React会调用componentDidCatch。 这个异常可以是任何类型的异常, 捕捉到这个异常之后呢, 可以做一些补救之类的事情。

componentDidCatchgetDerivedStateFromError 的 区别

componentDidCatch 和 getDerivedStateFromError 都是能捕捉异常的,那他们有什么区别呢?

我们之前说了两个阶段, render phasecommit phase.

render phase 里产生异常的时候, 会调用 getDerivedStateFromError;

在 commit phase 里产生异常大的时候, 会调用 componentDidCatch

严格来说, 其实还有一点区别:

componentDidCatch 是不会在服务器端渲染的时候被调用的 而 getDerivedStateFromError 会。

背景小结

啰里八嗦一大堆, 关于背景的东西就说到这, 大家只需要了解什么是Fiber: ‘ 哦, 这个这个东西是支持异步渲染的, 虽然这个东西还没开启’。

然后就是渲染的两个阶段:renderphasecommit phase.

  • render phase 可以被打断, 大家不要在此阶段做一些有副作用的操作,可以放心在commit phase 里做。
  • 然后就是生命周期的调整, react 把你有可能在render phase 里做的有副作用的函数都改成了static 函数, 强迫开发者做一些纯函数的操作。

现在我们进入正题: SuspenseHooks

正题


suspense

Suspense要解决的两个问题:

  1. 代码分片;
  2. 异步获取数据。

刚开始的时候, React 觉得自己只是管视图的, 代码打包的事不归我管, 怎么拿数据也不归我管。 代码都打到一起, 比如十几M, 下载就要半天,体验显然不会好到哪里去。

可是后来呢,这两个事情越来越重要, React 又觉得, 嗯,还是要掺和一下,是时候站出来展现真正的技术了。

Suspense 在v16.6的时候 已经解决了代码分片的问题,异步获取数据还没有正式发布。

先看一个简单的例子:

import React from "react";
import moment from "moment";
 
const Clock = () => <h1>{moment().format("MMMM Do YYYY, h:mm:ss a")}</h1>;

export default Clock;

假设我们有一个组件, 是看当前时间的, 它用了一个很大的第三方插件, 而我想只在用的时候再加载资源,不打在总包里。

再看一段代码:

// Usage of Clock
const Clock = React.lazy(() => {
  console.log("start importing Clock");
  return import("./Clock");
});

这里我们使用了React.lazy, 这样就能实现代码的懒加载。 React.lazy 的参数是一个function, 返回的是一个promise. 这里返回的是一个import 函数, webpack build 的时候, 看到这个东西, 就知道这是个分界点。 import 里面的东西可以打包到另外一个包里。

真正要用的话, 代码大概是这个样子的:

<Suspense fallback={<Loading />}>
  { showClock ? <Clock/> : null}
</Suspense>

showClock 为 true, 就尝试render clock, 这时候, 就触发另一个事件: 去加载clock.js 和它里面的 lib momment。

看到这你可能觉得奇怪, 怎么还需要用个<Suspense> 包起来, 有啥用, 不包行不行。

哎嗨, 不包还真是不行。 为什么呢?

前面我们说到, 目前react 的渲染模式还是同步的, 一口气走到黑, 那我现在画到clock 这里, 但是这clock 在另外一个文件里, 服务器就需要去下载, 什么时候能下载完呢, 不知道。 假设你要花十分钟去下载, 那这十分钟你让react 去干啥, 总不能一直等你吧。 Suspens 就是来解决这个问题的, 你要画clock, 现在没有,那就会抛一个异常出来,我们之前说
componentDidCatch 和 getDerivedStateFromError, 这两个函数就是来抓子组件 或者 子子组件抛出的异常的。

子组件有异常的时候就会往上抛,直到某个组件的 getDerivedStateFromError 抓住这个异常,抓住之后干嘛呢, 还能干嘛呀, 忍着。

下载资源的时候会抛出一个promise, 会有地方(这里是suspense)捕捉这个promise, suspense 实现了getDerivedStateFromError,捕获到异常的时候, 一看, 哎, 小老弟,你来啦,还是个promise, 然后就等这个promise resolve, 完成之后,它会尝试重新画一下子组件。

这时候资源已经到本地了,也就能画成功了。

用伪代码 大致实现一下:

getDerivedStateFromError(error) {
   if (isPromise(error)) {
      error.then(reRender);
   }
}

以上大概就是Suspense 的原理, 其实也不是很复杂,就是利用了 componentDidCatch 和 getDerivedStateFromError, 其实刚开始在v16的时候, 是要用componentDidCatch 的, 但它毕竟是commit phase 里的东西, 还是分出来吧, 所以又加了个getDerivedStateFromError来实现 Suspense 的功能。

这里需要注意的是 reRender 会渲染suspense 下面的所有子组件。

异步渲染什么时候开启呢, 根据介绍说是在19年的第二个季度随着一个小版本的升级开启, 让我们提前做好准备。

做些什么准备呢?

  • render 函数之前的代码都检查一边, 避免一些有副作用的操作

到这, 我们说完了Suspense 的一半功能, 还有另一半: 异步获取数据。

目前这一部分功能还没正式发布。 那我们获取数据还是只能在commit phase 做, 也就是在componentDidMount 里 或者 didUpdate 里做。

就目前来说, 如果一个组件要自己获取数据, 就必须实现为一个类组件, 而且会画两次, 第一次没有数据, 是空的, 你可以画个loading, didMount 之后发请求, 数据回来之后, 把数据setState 到组件里, 这时候有数据了, 再画一次,就画出来了。

虽然是一个很简答的功能, 我就想请求个数据, 还要写一堆东西, 很麻烦, 但在目前的正式版里, 不得不这么做。

但以后这种情况会得到改善, 看一段示例:

import {unstable_createResource as createResource} from 'react-cache';

const resource = createResource(fetchDataApi);

const Foo = () => {
  const result = resource.read();
  return (
    <div>{result}</div>
  );

// ...

<Suspense>
   <Foo />
</Suskpense>};

代码里我们看不到任何譬如 async await 之类的操作, 看起来完全是同步的操作, 这是什么原理呢。

上面的例子里, 有个 resource.read(), 这里就会调api, 返回一个promise, 上面会有suspense 抓住, 等resolve 的时候,再画一下, 就达到目的了。

到这,细心的同学可能就发现了一个问题, resource.read(); 明显是一个有副作用的操作, 而且 render 函数又属于render phase, 之前又说, 不建议在 render phase 里做有副作用的操作, 这么矛盾, 不是自己打脸了吗。

这里也能看出来React 团队现在还没完全想好, 目前放出来测试api 也是以unstable_开头的, 不用用意还是跟明显的: 让大家不要写class的组件,Suspense 能很好的支持函数式组件。

hooks

React v16.7.0-alpha 中第一次引入了 Hooks 的概念, 为什么要引入这个东西呢?

有两个原因:

  1. React 官方觉得 class组件太难以理解,OO(面向对象)太难懂了
  2. React 官方觉得 , React 生命周期太难理解。

最终目的就是, 开发者不用去理解class, 也不用操心生命周期方法。

但是React 官方又说, Hooks的目的并不是消灭类组件。此处应手动滑稽。

回归正题, 我们继续看Hooks, 首先看一下官方的API

clipboard.png

乍一看还是挺多的, 其实有很多的Hook 还处在实验阶段,很可能有一部分要被砍掉, 目前大家只需要熟悉的, 三个就够了:

  • useState
  • useEffect
  • useContext

useState

举个例子来看下, 一个简单的counter :

// 有状态类组件
class Counter extends React.Component {
   state = {
      count: 0
   }
   
   increment = () => {
       this.setState({count: this.state.count + 1});
   }
   
   minus = () => {
       this.setState({count: this.state.count - 1});
   }
   
   render() {
       return (
           <div>
               <h1>{this.state.count}</h1>
               <button onClick={this.increment}>+</button>
               <button onClick={this.minus}>-</button>
           </div>
       );
   }
}
// 使用useState Hook
const Counter = () => {
  const [count, setCount] = useState(0);
  
  const increment = () => setCount(count + 1);
  
  return (
    <div>
        <h1>{count}</h1>
        <button onClick={increment}>+</button>
    </div>
  );
};

这里的Counter 不是一个类了, 而是一个函数。

进去就调用了useState, 传入 0,对state 进行初始化,此时count 就是0, 返回一个数组, 第一个元素就是 state 的值,第二个元素是更新 state 的函数。

// 下面代码等同于: const [count, setCount] = useState(0);
  const result = useState(0);
  const count = result[0];
  const setCount = result[1];

利用 count 可以读取到这个 state,利用 setCount 可以更新这个 state,而且我们完全可以控制这两个变量的命名,只要高兴,你完全可以这么写:

 const [theCount, updateCount] = useState(0);

因为 useState 在 Counter 这个函数体中,每次 Counter 被渲染的时候,这个 useState 调用都会被执行,useState 自己肯定不是一个纯函数,因为它要区分第一次调用(组件被 mount 时)和后续调用(重复渲染时),只有第一次才用得上参数的初始值,而后续的调用就返回“记住”的 state 值。

读者看到这里,心里可能会有这样的疑问:如果组件中多次使用 useState 怎么办?React 如何“记住”哪个状态对应哪个变量?

React 是完全根据 useState 的调用顺序来“记住”状态归属的,假设组件代码如下:

const Counter = () => {
  const [count, setCount] = useState(0);
  const [foo, updateFoo] = useState('foo');
  
  // ...
}

每一次 Counter 被渲染,都是第一次 useState 调用获得 count 和 setCount,第二次 useState 调用获得 foo 和 updateFoo(这里我故意让命名不用 set 前缀,可见函数名可以随意)。

React 是渲染过程中的“上帝”,每一次渲染 Counter 都要由 React 发起,所以它有机会准备好一个内存记录,当开始执行的时候,每一次 useState 调用对应内存记录上一个位置,而且是按照顺序来记录的。React 不知道你把 useState 等 Hooks API 返回的结果赋值给什么变量,但是它也不需要知道,它只需要按照 useState 调用顺序记录就好了。

你可以理解为会有一个槽去记录状态。

正因为这个原因,Hooks,千万不要在 if 语句或者 for 循环语句中使用!

像下面的代码,肯定会出乱子的:

const Counter = () => {
    const [count, setCount] = useState(0);
    if (count % 2 === 0) {
        const [foo, updateFoo] = useState('foo');
    }
    const [bar, updateBar] = useState('bar');
 // ...
}

因为条件判断,让每次渲染中 useState 的调用次序不一致了,于是 React 就错乱了。

useEffect

除了 useState,React 还提供 useEffect,用于支持组件中增加副作用的支持。

在 React 组件生命周期中如果要做有副作用的操作,代码放在哪里?

当然是放在 componentDidMount 或者 componentDidUpdate 里,但是这意味着组件必须是一个 class。

在 Counter 组件,如果我们想要在用户点击“+”或者“-”按钮之后把计数值体现在网页标题上,这就是一个修改 DOM 的副作用操作,所以必须把 Counter 写成 class,而且添加下面的代码:

componentDidMount() {
  document.title = `Count: ${this.state.count}`;
}

componentDidUpdate() {
  document.title = `Count: ${this.state.count}`;
}

而有了 useEffect,我们就不用写一个 class 了,对应代码如下:

import { useState, useEffect } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    document.title = `Count: ${this.state.count}`;
  });

  return (
    <div>
       <div>{count}</div>
       <button onClick={() => setCount(count + 1)}>+</button>
       <button onClick={() => setCount(count - 1)}>-</button>
    </div>
  );
};

useEffect 的参数是一个函数,组件每次渲染之后,都会调用这个函数参数,这样就达到了 componentDidMount 和 componentDidUpdate 一样的效果。

虽然本质上,依然是 componentDidMountcomponentDidUpdate 两个生命周期被调用,但是现在我们关心的不是 mount 或者 update 过程,而是“after render”事件,useEffect 就是告诉组件在“渲染完”之后做点什么事。

读者可能会问,现在把 componentDidMountcomponentDidUpdate 混在了一起,那假如某个场景下我只在 mount 时做事但 update 不做事,用 useEffect 不就不行了吗?

其实,用一点小技巧就可以解决。useEffect 还支持第二个可选参数,只有同一 useEffect 的两次调用第二个参数不同时,第一个函数参数才会被调用. 所以,如果想模拟 componentDidMount,只需要这样写:

  useEffect(() => {
    // 这里只有mount时才被调用,相当于componentDidMount
  }, [123]);

在上面的代码中,useEffect 的第二个参数是 [123],其实也可以是任何一个常数,因为它永远不变,所以 useEffect 只在 mount 时调用第一个函数参数一次,达到了 componentDidMount 一样的效果。

useContext

在前面介绍“提供者模式”章节我们介绍过 React 新的 Context API,这个 API 不是完美的,在多个 Context 嵌套的时候尤其麻烦。

比如,一段 JSX 如果既依赖于 ThemeContext 又依赖于 LanguageContext,那么按照 React Context API 应该这么写:

<ThemeContext.Consumer>
    {
        theme => (
            <LanguageContext.Cosumer>
                language => {
                    //可以使用theme和lanugage了
                }
            </LanguageContext.Cosumer>
        )
    }
</ThemeContext.Consumer>

因为 Context API 要用 render props,所以用两个 Context 就要用两次 render props,也就用了两个函数嵌套,这样的缩格看起来也的确过分了一点点。

使用 Hooks 的 useContext,上面的代码可以缩略为下面这样:

const theme = useContext(ThemeContext);
const language = useContext(LanguageContext);
// 这里就可以用theme和language了

这个useContext把一个需要很费劲才能理解的 Context API 使用大大简化,不需要理解render props,直接一个函数调用就搞定。

但是,useContext也并不是完美的,它会造成意想不到的重新渲染,我们看一个完整的使用useContext的组件。

const ThemedPage = () => {
    const theme = useContext(ThemeContext);
    
    return (
       <div>
            <Header color={theme.color} />
            <Content color={theme.color}/>
            <Footer color={theme.color}/>
       </div>
    );
};

因为这个组件ThemedPage使用了useContext,它很自然成为了Context的一个消费者,所以,只要Context的值发生了变化,ThemedPage就会被重新渲染,这很自然,因为不重新渲染也就没办法重新获得theme值,但现在有一个大问题,对于ThemedPage来说,实际上只依赖于theme中的color属性,如果只是theme中的size发生了变化但是color属性没有变化,ThemedPage依然会被重新渲染,当然,我们通过给Header、Content和Footer这些组件添加shouldComponentUpdate实现可以减少没有必要的重新渲染,但是上一层的ThemedPage中的JSX重新渲染是躲不过去了。

说到底,useContext 需要一种表达方式告诉React:“我没有改变,重用上次内容好了。”

希望Hooks正式发布的时候能够弥补这一缺陷。

Hooks 带来的代码模式改变

上面我们介绍了 useStateuseEffectuseContext 三个最基本的 Hooks,可以感受到,Hooks 将大大简化使用 React 的代码。

首先我们可能不再需要 class了,虽然 React 官方表示 class 类型的组件将继续支持,但是,业界已经普遍表示会迁移到 Hooks 写法上,也就是放弃 class,只用函数形式来编写组件。

对于 useContext,它并没有为消除 class 做贡献,却为消除 render props 模式做了贡献。很长一段时间,高阶组件和 render props 是组件之间共享逻辑的两个武器,但如同我前面章节介绍的那样,这两个武器都不是十全十美的,现在 Hooks 的出现,也预示着高阶组件和 render props 可能要被逐步取代。

但读者朋友,不要觉得之前学习高阶组件和 render props 是浪费时间,相反,你只有明白 React 的使用历史,才能更好地理解 Hooks 的意义。

可以预测,在 Hooks 兴起之后,共享代码之间逻辑会用函数形式,而且这些函数会以 use- 前缀为约定,重用这些逻辑的方式,就是在函数形式组件中调用这些 useXXX 函数。

例如,我们可以写这样一个共享 Hook useMountLog,用于在 mount 时记录一个日志,代码如下:

const useMountLog = (name) => {
    useEffect(() => {
        console.log(`${name} mounted`);    
    }, [123]);
}

任何一个函数形式组件都可以直接调用这个 useMountLog 获得这个功能,如下:

const Counter = () => {
    useMountLog('Counter');
    
    ...
}

对了,所有的 Hooks API 都只能在函数类型组件中调用,class 类型的组件不能用,从这点看,很显然,class 类型组件将会走向消亡。

如何用Hooks 模拟旧版本的生命周期函数

Hooks 未来正式发布后, 我们自然而然的会遇到这个问题, 如何把写在旧生命周期内的逻辑迁移到Hooks里面来。下面我们就简单说一下,

模拟整个生命周期中只运行一次的方法

useMemo(() => {
  // execute only once
}, []);

我们可以看到useMemo 接收两个参数, 第一个参数是一个函数, 第二个参数是一个数组。

这里有个地方要注意, 就是, 第二个参数的数组里的元素和上一次执行useMemo的第二个参数的数组的元素 完全一样的话,那就表示没有变化, 就不用执行第一个参数里的函数了。 如果有不同, 说明有变化, 就执行。

上面的例子里, 我们只传入了一个空数组, 不会有变化, 也就是只会执行一次。

模拟shouldComponentUpdate

const areEqual = (prevProps, nextProps) => {
   // 返回结果和shouldComponentUpdate正好相反
   // 访问不了state
}; 
React.memo(Foo, areEqual);

模拟componentDidMount

useEffect(() => {
    // 这里在mount时执行一次
}, []);

模拟componentDidUpdate

const mounted = useRef();
useEffect(() => {
  if (!mounted.current) {
    mounted.current = true;
  } else {
    // 这里只在update是执行
  }
});

模拟componentDidUnmount

useEffect(() => {
    // 这里在mount时执行一次
    return () => {
       // 这里在unmount时执行一次
    }
}, []);

未来的代码形势

Hooks 未来发布之后, 我们的代码会写成什么样子呢? 简单设想一下:

// Hooks之后的组件逻辑重用形态

const XXXX = () => {
  const [xx, xxx, xxxx] = useX();
  
  useY();
  
  const {a, b} = useZ();
  

  return (
    <>
     //JSX
    </>
  );
};

内部可能用各种Hooks, 也可能包含第三方的Hooks。 分享Hooks 就是实现代码重用的一种形势。 其实现在已经有人在做这方面的工作了: useHooks.com, 有兴趣的朋友可以去看下。

Suspense 和 Hooks 带来的改变

Suspense 和 Hooks 发布后, 会带来什么样的改变呢? 毫无疑问, 未来的组件, 更多的将会是函数式组件。

原因很简单, 以后大家分享出来的都是Hooks,这东西只能在函数组件里用啊, 其他地方用不了,后面就会自然而然的发生了。

但函数式组件和函数式编程还不是同一个概念。 函数式编程必须是纯的, 没有副作用的, 函数式组件里, 不能保证, 比如那个resource.read(), 明显是有副作用的。

关于好坏

既然这两个东西是趋势, 那这两个东西到底好不好呢 ?

个人理解, 任何东西都不是十全十美。 既然大势所趋, 我们就努力去了解它,学会它, 努力用它好的地方, 避免用不好的地方。

React 发布路线图

最新的消息: https://reactjs.org/blog/2018...

  • React 16.6 with Suspense for Code Splitting (already shipped)
  • A minor 16.x release with React Hooks (~Q1 2019)
  • A minor 16.x release with Concurrent Mode (~Q2 2019)
  • A minor 16.x release with Suspense for Data Fetching (~mid 2019)

明显能够看到资源在往 Suspense 和 Hooks 倾斜。

结语

看到这, 相信大家都Suspense 和 Hooks 都有了一个大概的了解了。

收集各种资料花费了挺长时间,大概用了两三天写出来,中间参考了很多资料, 一部分是摘录到了上面的内容里。

在这里整理分享一下, 希望对大家有所帮助。

才疏学浅, 难免会有纰漏, 欢迎指正:)。

最后

觉得内容有帮助可以关注下我的公众号 「 前端e进阶 」,一起学习成长

clipboard.png

参考资料

查看原文

赞 161 收藏 94 评论 7

认证与成就

  • 获得 39 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2018-04-20
个人主页被 507 人浏览