ZB-003-git和github

git 软件的版本控制工具

fork操作-如何给别人修改代码

  • 别人的仓库如https://github.com/slTrust/aaa你是没有修改权限的,所以你要fork
  • fork 就是分叉的意思,就是把别人的项目fork一份到你的github自己的工作空间内 https://github.com/your_git/aaa 此时两个空间(slTrust和 your_git)的代码完全相同
  • 然后修改在你的工作空间下改善你要修改的地方
    • commit 然后 push代码
  • 此时你就可以在 https://github.com/slTrust/aaa 的工作空间里提交一个请求 pull request
  • 别人就会去 review 你的代码,如何发现合适就把你的代码合并到他的代码上去。

这就是完整的项目合作流程

github 一个代码托管平台

GitHub团队协作入门







别人通过fork 给你的项目提交了pr

  • 你打开这个 pr
  • 你在review时候,在代码变更行可以 点击 “加号” ,发表评论
    • 写完评论,下面有很多按钮,如果你不清楚不要乱按
      • start a review ,写完评论,提交代码的人看不到任何评论,因为它有一个 pending 状态,意思是还在进行中,没有发出去
        • 你可以发起多个 start a review
        • 通过点击 Finish your review ,然后选择 comment ,点击 submit review 才能将之前的所有 评论 一次性发出去
        • start a review 只是提交了一个 评论的草稿
        • 为啥不能点了就发出去,因为一个复杂的项目可能有多个文件有评论,如果你review 一个小时,写一个意见就发出去,对方会不停的被你骚扰,这样的好处是,你把所有评论攒一起,对方可以一次性看完,避免骚扰
      • Add single comment 立刻把你的评论发给对方
    • 吊打别人的鬼畜操作:你除了 review提出建议还能告诉别人如何修改代码
      • 点击 加号 的时候 下面的输入框上方 又一个 “加减号按钮” 悬停的时候显示 "insert a suggestion <cmd-g>"
      • 这样你提交你的建议修改,别人就会看到一个状态 “suggested change”

markdown

标题

1
2
3
# 一级标题
。。。
###### 六级标题

引用

1
> 引用

列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
无序列表
- a
- b
- c

有序列表

1. a
2. b
3. c

带复选框的列表

- []a
- []b
- [x] 选中的

加粗

1
**加粗的文字**

倾斜体

1
__倾斜的文字__

代码块

1
前后三个反引号包裹的内容

行内代码 java

1
`Java`

链接

1
[百度](www.baidu.com)

~删除线~

1
~删除~

图片

1
![图片描述](图片地址)

表格

1
2
3
姓名|年龄|班级
---|---|---
a | 22|1

ZB-002-开撸准备工作

基本概念

  • 什么是JDK java development kit
  • Git 版本控制软件
  • IDEA java集成开发环境

安装java

搜索 oracle jdk

不要下载最新版java12 建议下载 java8,因为如果你是个初学者,下载最新版会遇到当前阶段你解决不了的问题。白白浪费时间。

因为下载java8需要登录,你可以搜索 oracle jdk 账号 登录下载即可

mac 配置多个版本jdk

编辑 ~/.bash_profile

1
2
3
4
5
6
7
8
9
export JAVA_8_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_202.jdk/Contents/Home
export JAVA_9_HOME=/Library/Java/JavaVirtualMachines/jdk-9.0.1.jdk/Contents/Home
export JAVA_10_HOME=/Library/Java/JavaVirtualMachines/jdk-11.jdk/Contents/Home
export JAVA_13_HOME=/Library/Java/JavaVirtualMachines/jdk-13.jdk/Contents/Home

alias jdk8="export JAVA_HOME=$JAVA_8_HOME"
alias jdk9="export JAVA_HOME=$JAVA_9_HOME"
alias jdk11="export JAVA_HOME=$JAVA_11_HOME"
alias jdk13="export JAVA_HOME=$JAVA_13_HOME"

然后 source ~/.bash_profile

术语解释

  • JRE(java runtime environment) 运行
    • java运行环境
  • JDK(java development kit) 开发 + 运行
    • java开发工具包
1
2
# 粗略理解就是
JDK = JRE + javac
  • JAVA_HOME 就是 java的安装目录

java收费问题

  • 对于开发测试来说 jdk 都是免费的
  • 在生产环境里使用 jdk 是收费的,这个不该你来担心,应该由法务部操心

java版本详解

  • java对向后兼容有着谜一样的执着和坚守

java虽然现在到了12但是java的诉求就是向后兼容

1
2
3
就是java1.1的代码在 java12上跑基本没什么问题。

而python2/ python3 就有很大的差异了。

举个例子:你盖了到20层的时候,发现中间某层有个螺丝钉有问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
95年 java 1.1--> 1.2 -->1.6

2014年 java 1.8 引入了很多的特性。跨时代的成就。
乃至2019年 全国乃至全世界都是业界最广泛使用的东西


1.8之后 JEP223提议。把版本号变成了9从此之后 只有 9 / 10 / 11 /12

java9 引入了 JPMS java模块系统 还引入了 一个破坏性的改变 对向后兼容造成了一定的破坏

所以 1.8 和 9 是 java的一道鸿沟

9/10/11/12 是一脉相承的

LTS是什么 长期支持版。 意思是提供长久的功能改进和更新 目前只有 java1.8 和 11

如果是生产环境推荐使用 8

9 和 10 任何情况下都不推荐在生产环境里使用

vendor的JDK

vender(供应商)

来个例子

1
2
3
4
5
农夫山泉
怡宝
康师傅

三大矿泉水供应商

java的供应商

  • oracle jdk 官方的
  • open jdk 一份源代码(和oracle jdk 只有细微差别)
  • adopt jdk 社区维护的jdk
  • 其他公司开发的jdk

如上各种不同的jdk 被叫做 vendor

不同供应商提供的不同jdk实现

插播一个为什么想学java的原因

如果一件事情需要15年才能看到结果,此前完全没有回报,你还会去做吗?

互联网公司都采用敏捷开发,每个迭代大约是2周~6周。比三个迭代更遥远的事情,大家一般都不会去想。但是,人生是一个长期准备的过程,20岁的时候就需要定下35岁的目标,然后苦苦积累,不计得失,只为了无比遥远的多年以后,能够看到成果。互联网是一个短期行为的行业,但是人生不是。

ZB-001-java概述

java过时了吗?

java8新特性已经是五年前的事了(2014年发布)

编程语言排行一直位列前三。

为什么C一直那么高,因为C是无可替代的,系统底层/操作系统/硬件/驱动只有C能写

  • 没有!
  • 当之无愧的世界第一编程语言
  • 海量的需求
  • 未来会过时吗?

Java的行业前景和钱景

  • 伪初级:找不到工作/勉强糊口的工作
  • 初级:对标阿里的P5(10-15K,别人带着能干活)
  • 中极:对标阿里的P6(15-25K,自己能干活)
  • 高级:阿里P7(25K~ ∞ ,自己能干活,也能帮团队干活)
1
2
3
4
5
6
6/3/1的比例分配年中

3.5/3.75/3.25
满足预期/超出预期/不足预期
B/A/C
3x/6x/0x 年终

java的好处就是蛋糕足够大!你跑的不用特别快就能吃蛋糕

但是java的求职者是海量的,xx年河南的高考人数是98万人,考之前,班主任指着窗外的操场说,你多考一分,就能超过一操场的人。反应到java的需求和要求,竞争者就是以操场为单位的。

即想赚大钱又不努力能不能吃蛋糕——答案是 洗洗睡吧!

如何跳出弱鸡循环

java和node的区别

  • java适合多人协作,适合一个长期维护的项目
  • node能快速怼出原型快速修改,但是比较难维护。

ts的写项目很香证明了什么?

证明了大项目多人协作需要静态编译

  • 大项目
  • 工程化
  • 团队协作

这些Java天生具备

如何评价go

  • 主要是为了替代C的,而且大部分功能和java重合,而且语言还在发展中
  • 你的go和大公司的go是两种概念

在公司使用不同语言如何?

  • 首先,公司要考虑语言切换的开销,出了bug能否 hold住
  • 其次是,团队协调合作的问题

面对大部分公司清退35以上的程序员

  • 首先法制不够健全,导致这种现象
  • 其次35以后没法像年轻人一样一把梭就是干
  • 所以你应该成为不可替代的人

ReactWheels06-02测试覆盖率和持续集成下

每次自己发代码是不是太麻烦

能不能 push 代码 ci运行test 成功之后自动发布代码

可以

修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#https://github.com/revolunet/create-react-app-circleci/blob/master/.circleci/config.yml
defaults: &defaults
docker:
# 我们使用 node8来测试
- image: circleci/node:8
# circleci的 version
version: 2
jobs:
# 准备阶段
prepare:
<<: *defaults
steps:
# 迁出代码
- checkout
# 根据当前的package.json 创建一个 md5 把它当作key 创建一个缓存,加速之后代码的运行,只要package.json里面没有变化
# 就可以使用上次的缓存,不用从头安装
- restore_cache:
keys:
- v2-dependencies-{{ checksum "package.json" }}
- run: yarn install
- save_cache:
paths:
- node_modules
key: v2-dependencies-{{ checksum "package.json" }}
- persist_to_workspace:
root: .
paths:
- node_modules
build:
<<: *defaults
steps:
- checkout
- attach_workspace:
at: .
- run: yarn build
# 持久化,意思就是 dist目录不删除
- persist_to_workspace:
root: .
paths:
- dist
# 这三个文件必须有才能发布
- package.json
- LICENSE
- README.md
test:
<<: *defaults
steps:
- checkout
- attach_workspace:
at: .
- run: yarn ci
- store_test_results:
path: test-results
publish:
<<: *defaults
steps:
# 把 build产生的目录 放在当前目录
- attach_workspace:
at: .
- run: npm publish

workflows:
version: 2
build_accept_deploy:
jobs:
- prepare
- build:
requires:
- test
- test:
requires:
- prepare
# 新增 pubish的依赖 就是先打包
- publish:
requires:
- build

提交代码

  1. https://circleci.com/dashboard 看你的项目 publish失败了, 因为没法登录 npm啊
  2. 无论如何不要泄漏你的密码
  3. 无论如何不要泄漏你的密码
  4. 无论如何不要泄漏你的密码

npm token

  1. 登录你的npm
  2. 右上角你头像点击 tokens
  3. Create New Token 然后选择 read and publish > create token
  4. 复制你的那一串token 形如 add5b0f1-1111-4036-dd11-3118e4bfd279 我这个是假的
  5. 到你的 circle ci 点击 左侧 jobs 选中你的项目,右边小齿轮(settings)
  6. 选择 Environment Variables 环境变量
  7. 点击 Add Variable

    1
    2
    3
    4
    5
    name: NPM_TOKEN
    value: add5b0f1-1111-4036-dd11-3118e4bfd279

    点击 Add Variable
    添加之后它就作为脚本的环境变量,你可以读取到
  8. 回去继续发布我们的代码,还是失败。

  9. 因为你没告诉脚本环境变量 token 在哪里

    1
    - run: npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN

    完整版

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    #https://github.com/revolunet/create-react-app-circleci/blob/master/.circleci/config.yml
    defaults: &defaults
    docker:
    # 我们使用 node8来测试
    - image: circleci/node:8
    # circleci的 version
    version: 2
    jobs:
    # 准备阶段
    prepare:
    <<: *defaults
    steps:
    # 迁出代码
    - checkout
    # 根据当前的package.json 创建一个 md5 把它当作key 创建一个缓存,加速之后代码的运行,只要package.json里面没有变化
    # 就可以使用上次的缓存,不用从头安装
    - restore_cache:
    keys:
    - v2-dependencies-{{ checksum "package.json" }}
    - run: yarn install
    - save_cache:
    paths:
    - node_modules
    key: v2-dependencies-{{ checksum "package.json" }}
    - persist_to_workspace:
    root: .
    paths:
    - node_modules
    build:
    <<: *defaults
    steps:
    - checkout
    - attach_workspace:
    at: .
    - run: yarn build
    # 持久化,意思就是 dist目录不删除
    - persist_to_workspace:
    root: .
    paths:
    - dist
    # 这三个文件必须有才能发布
    - package.json
    - LICENSE
    - README.md
    test:
    <<: *defaults
    steps:
    - checkout
    - attach_workspace:
    at: .
    - run: yarn ci
    - store_test_results:
    path: test-results
    publish:
    <<: *defaults
    steps:
    # 把 build产生的目录 放在当前目录
    - attach_workspace:
    at: .
    - run: npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN
    - run: npm publish

    workflows:
    version: 2
    build_accept_deploy:
    jobs:
    - prepare
    - build:
    requires:
    - test
    - test:
    requires:
    - prepare
    - publish:
    requires:
    - build
  10. 记得修改在修改一下版本好 在 push 代码

此时我们做到了 push之后,自动测试 自动发布

但是你每次改还要更改版本号,能不能自动改版本号?

  • 可以
1
2
3
4
5
6
7
8
# 自动改你版本的最后一位
npm version patch

# 自动改你版本的倒数第二位
npm version minor

# 自动改你版本的第一位
npm version major

任何一个包都有这样一个版本

1
2
3
4
5
6
7
8
A.B.C

分别对应
major.minor.patch

patch 代表补丁,没有API的变化
minor 代表 API有变化,但不影响现有代码
major 代表 API变化很大,影响现有代码

我们的项目根目录 新建 deploy.sh

1
2
3
#!/bin/env bash
npm version patch
git push

分配执行权限

1
chmod 777 ./deploy.sh

本地运行

1
2
3
4
./deploy.sh

# 如果运行不了你就
sh ./deploy.sh

我们想动态指定发布的版本而不是写死 patch

1
2
3
#!/bin/env bash
npm version $1
git push

这样你就可以传参数来指定发布的版本了

1
./deploy.sh patch

但是有时候你改了代码 然后在./deploy.sh 就会失败,因为你必须先提交代码才能执行它

所以我们应该修改下 deploy.sh

1
2
#!/bin/env bash
npm version $1 && git push

再次运行

1
./deploy.sh patch

假如我正常提交代码后 git push 然后CI通过后自动帮我们 publish,因为只要 build 之后就会 publish 这样会出错的

根据 tag 来发布

搜索 circle tag filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
workflows:
version: 2
build_accept_deploy:
jobs:
- prepare
- build:
requires:
- test
- test:
requires:
- prepare
- publish:
requires:
- build
filters:
tags:
only: /v[0-9]+(\.[0-9]+)*/
# 忽略所有分支,我只看 tag
branches:
ignore: /.*/

如何给我们的项目加上小标志

  • circleci 小图标circleci badge
    • 在你的 circleci 选择 Status Badges
    • 选择默认分支
    • 他会给你个markdown 的代码 复制到你项目的 readme.md
  • npm 小图标npm badge
    • 搜索你的 UI库 如我的 package.json 里的name 是fui_t888 得到一个 markdown内容复制到你的 readme.md

js jsx ts tsx的区别

  • jsx 是 js 的扩展,支持 用 js 的方式写 标签 是xml标准

    1
    2
    3
    return (<div></div>)
    就不用这样
    React.createElement('div')
  • js 和 ts 的区别就是 ts = Type + JavasSript 也是对js的扩展

    1
    2
    3
    4
    5
    // js
    const a = 1

    // ts
    const a:number = 1
  • tsx 是对 ts 的扩展,支持 写标签 支持 Type

所以 tsx 是最厉害的

收尾工作

  • 比如我们 yarn test 的时候,它帮我们生成了 测试覆盖,但是这个过程会很慢,我们一般来说不需要知道这个测试覆盖率。只有 CI 生成了才去看

新建 jest.config.ci.js

1
2
3
4
5
6
7
8
9
10
11
const base = require('./jest.config')
module.exports = Object.assign({},base,{
reporters: ["jest-junit"],
collectCoverage: true,
// "lib/**/*.{ts,tsx}" lib里的文件都要测,除了__tests__里的,jest的默认规则是排除有__tests__目录的文件
// "!**/node_modules/**" 任意包含node_modules的目录不测
collectCoverageFrom: ["lib/**/*.{ts,tsx}", "!**/node_modules/**"],
// 生成的报告放在那里
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov'],
})

修改 jest.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// https://jestjs.io/docs/en/configuration.html

module.exports = {
verbose: true,
clearMocks: false,
reporters: ["default"],

collectCoverage: false,
// // "lib/**/*.{ts,tsx}" lib里的文件都要测,除了__tests__里的,jest的默认规则是排除有__tests__目录的文件
// // "!**/node_modules/**" 任意包含node_modules的目录不测
// collectCoverageFrom: ["lib/**/*.{ts,tsx}", "!**/node_modules/**"],
// // 生成的报告放在那里
// coverageDirectory: 'coverage',
// coverageReporters: ['text', 'lcov'],

moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx'],
moduleDirectories: ['node_modules'],
globals: {
'ts-jest': {
tsConfig: 'tsconfig.test.json',
},
},
moduleNameMapper: {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/test/__mocks__/file-mock.js",
"\\.(css|less|sass|scss)$": "<rootDir>/test/__mocks__/object-mock.js",
},
testMatch: ['<rootDir>/**/__tests__/**/*.unit.(js|jsx|ts|tsx)'],
transform: {
"^.+unit\\.(js|jsx)$": "babel-jest",
'^.+\\.(ts|tsx)$': 'ts-jest',
},
setupFilesAfterEnv: ["<rootDir>test/setupTests.js"]
}

修改 package.json

1
2
3
4
针对 ci 才使用 覆盖率测试 ,我们的测试 使用默认的 jest.config.js
"scripts": {
"ci": "cross-env NODE_ENV=test JEST_JUNIT_OUTPUT=./test-results/jest/results.xml jest --config=jest.config.ci.js"
},

制作我们的简陋官网,来鼠标点点点

修改 webpack.config.dev.js 里的入口文件,我们用example.tsx覆盖默认的入口。因为这是 dev 环境

1
2
3
4
5
6
7
8
9
10
11
12
13
const base = require('./webpack.config')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = Object.assign({},base,{
mode:'development',
entry:{
example:'./example.tsx'
},
plugins:[
new HtmlWebpackPlugin({
template:'example.html'
})
]
})

修改 index.html 为 example.html

跟路径新建 example.tsx

1
2


启动我们项目

1
yarn start

运行成功。 我们需要一个左侧菜单右侧显示组件的面板

此时我们需要 router

1
2
3
yarn add react-router-dom@5.0.0

yarn add --dev @types/react-router-dom@4.3.1

修改我们的 example.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import React from 'react';
import ReactDOM from 'react-dom';
import {HashRouter as Router, Route, NavLink} from 'react-router-dom';

import IconExample from './lib/icon/icon.example';
import ButtonExample from './lib/button/button.example';

ReactDOM.render(
<Router>
<div>
<header>
<div className="logo">
FUI
</div>
</header>
<div>
<aside>
<h2>组件</h2>
<ul>
<li>
<NavLink to="/icon">Icon</NavLink>
</li>
<li>
<NavLink to="/button">Button</NavLink>
</li>
</ul>
</aside>
<main>
<Route path="/icon" component={IconExample}/>
<Route path="/button" component={ButtonExample}/>
</main>
</div>
</div>
</Router>
,document.querySelector('#root'));

新建 lib/icon/icon.example.tsx

1
2
3
4
5
6
7
8
9
10
11
import React from 'react';
import Icon from './icon'
const IconExample:React.FunctionComponent = ()=>{
return (
<div>
<Icon name="alipay"/>
</div>
)
}

export default IconExample;

新建 lib/button/button.example.tsx

1
2
3
4
5
6
import React from 'react';
const ButtonExample:React.FunctionComponent = ()=>{
return (<div>hi!button</div>)
}

export default ButtonExample;

此时 我们的项目虽然没样式,但是点击对应的组件,就显示对应的页面了

代码仓库

ReactWheels06-01测试覆盖率和持续集成上

什么是工程化

table 小作坊 工程化
代码管理 qq传 git/svn
部署代码 登录机器手动操作 一键部署
代码质量 靠感觉 工具 + review
性能优化 靠感觉 数据上报
需求管理 下周上线 任务管理(排期)

工程化的核心:

  1. 自动化——能用机器做的,绝不给人做(人会犯错)
  2. 工业化——铁打的硬盘,流水的兵(没有任何人是独一无二的,所有人都是可替代的)
    • 为什么有些代码只能一个人改?因为没文档!这是不对的

测试覆盖率

代表着代码质量

  1. 配置
  2. 代码覆盖率 vs 函数覆盖率 vs 分支覆盖率

继续我们的造轮子 先 yarn test 跑通测试

修改 jest.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 修改为 true
collectCoverage:true
# 然后运行 yarn test,运行生成一个表格

-----------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
-----------------|----------|----------|----------|----------|-------------------|
All files | 95 | 100 | 75 | 100 | |
lib | 100 | 100 | 100 | 100 | |
button.tsx | 100 | 100 | 100 | 100 | |
lib/helpers | 100 | 100 | 100 | 100 | |
classes.tsx | 100 | 100 | 100 | 100 | |
lib/icon | 92.86 | 100 | 50 | 100 | |
icon.tsx | 100 | 100 | 100 | 100 | |
importIcons.js | 75 | 100 | 0 | 100 | |
-----------------|----------|----------|----------|----------|-------------------|
  • stmts 语句覆盖率 100代表所有语句都测试了, 75代表只有75%的语句测试了
  • branch 分支覆盖率
  • funcs 函数覆盖率
  • Lines 行数覆盖率,但这个数字对我们没有实际意义。我们需要再加一个配置,就是告诉我哪一行有问题

再次修改 jest.config.js

1
2
3
4
5
6
7
8
9
10
collectCoverage: true,
reporters: ["default","jest-junit"]


# 运行 yarn test,报错了
Error: Could not resolve a module for a custom reporter.
Module name: jest-junit

# 安装依赖
yarn add --dev jest-junit@5.2.0

再次运行 yarn test

此时我们项目里多了一个文件 ===> junit.xml

内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<testsuites name="jest tests" tests="9" failures="0" time="3.321">
<testsuite name="icon" errors="0" failures="0" skipped="0" timestamp="2019-06-26T14:32:19" time="1.803" tests="2">
<testcase classname="icon render successfully" name="icon render successfully" time="0.025">
</testcase>
<testcase classname="icon onClick" name="icon onClick" time="0.057">
</testcase>
</testsuite>
<testsuite name="button" errors="0" failures="0" skipped="0" timestamp="2019-06-26T14:32:21" time="0.185" tests="1">
<testcase classname="button 是个 div" name="button 是个 div" time="0.002">
</testcase>
</testsuite>
<testsuite name="我的第一个测试用例" errors="0" failures="0" skipped="0" timestamp="2019-06-26T14:32:21" time="0.135" tests="1">
<testcase classname="我的第一个测试用例 1 等于 1" name="我的第一个测试用例 1 等于 1" time="0.002">
</testcase>
</testsuite>
<testsuite name="classes" errors="0" failures="0" skipped="0" timestamp="2019-06-26T14:32:22" time="0.159" tests="5">
<testcase classname="classes 接受 1 个 className" name="classes 接受 1 个 className" time="0.001">
</testcase>
<testcase classname="classes 接受 2 个 className" name="classes 接受 2 个 className" time="0.001">
</testcase>
<testcase classname="classes 接受 undefined 结果不会出现 undefined" name="classes 接受 undefined 结果不会出现 undefined" time="0">
</testcase>
<testcase classname="classes 接受各种奇怪值" name="classes 接受各种奇怪值" time="0.007">
</testcase>
<testcase classname="classes 接受 0 个参数" name="classes 接受 0 个参数" time="0.001">
</testcase>
</testsuite>
</testsuites>

还是不够详细啊!!!

修改 package.json里的 scripts

1
2
3
4
5
6
7
// 先别管那个路径是什么,后面会说
scripts:{
"xxx": "cross-env NODE_ENV=test JEST_JUNIT_OUTPUT=./test-results/jest/results.xml jest --config=jest.config.js"
}

// 运行 yarn xxx
// 此时生成了 test-results/jest/results.xml 的文件内容根上次差不多,还是不够详细

更详细的测试覆盖率

1
2
3
4
5
6
7
collectCoverage: true,
// "lib/**/*.{ts,tsx}" lib里的文件都要测,除了__tests__里的,jest的默认规则是排除有__tests__目录的文件
// "!**/node_modules/**" 任意包含node_modules的目录不测
collectCoverageFrom: ["lib/**/*.{ts,tsx}", "!**/node_modules/**"],
// 生成的报告放在那里
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov'],

完整版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// https://jestjs.io/docs/en/configuration.html

module.exports = {
verbose: true,
clearMocks: false,
reporters: ["default"],

collectCoverage: true,
collectCoverageFrom: ["lib/**/*.{ts,tsx}", "!**/node_modules/**"],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov'],

moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx'],
moduleDirectories: ['node_modules'],
globals: {
'ts-jest': {
tsConfig: 'tsconfig.test.json',
},
},
moduleNameMapper: {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/test/__mocks__/file-mock.js",
"\\.(css|less|sass|scss)$": "<rootDir>/test/__mocks__/object-mock.js",
},
testMatch: ['<rootDir>/**/__tests__/**/*.unit.(js|jsx|ts|tsx)'],
transform: {
"^.+unit\\.(js|jsx)$": "babel-jest",
'^.+\\.(ts|tsx)$': 'ts-jest',
},
setupFilesAfterEnv: ["<rootDir>test/setupTests.js"]
}

运行 yarn xxx,此时多了一个 coverage 目录

点开 coverage/lcov-report/index.html 双击在浏览器打开

就能看到各种详细信息

我们主要看什么

  • 语句覆盖率 (如果很低说明很多代码没测)
  • 分支覆盖率

有的时候一行有多个语句;
有的时候多行组成一个语句;
所以语句覆盖率和行数覆盖率是不同的

持续集成

  1. 比较流行的工具 google 搜 free ci tools
  2. 配置Travis CI
  3. 如何给自己的项目增加小图标

新建一个仓库 react-gulu-test-4

  • 在上次代码的终端里输入
1
2
git remote set-url origin https://github.com/slTrust/react-gulu-test-4.git
git push -u origin master

我们这次使用 circleci

  1. google 搜 circleci
  2. 点击 登录
  3. log in with github
  4. 左侧菜单点击 ADD PROJECTS
  5. 选择linux
  6. 选择你的仓库 我的是 react-gulu-test-4 点击 Set Up Project
  7. 创建一个目录 Add .circleci/config.yml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    #https://github.com/revolunet/create-react-app-circleci/blob/master/.circleci/config.yml
    defaults: &defaults
    docker:
    # 我们使用 node8来测试
    - image: circleci/node:8
    # circleci的 version
    version: 2
    jobs:
    # 准备阶段
    prepare:
    <<: *defaults
    steps:
    # 迁出代码
    - checkout
    # 根据当前的package.json 创建一个 md5 把它当作key 创建一个缓存,加速之后代码的运行,只要package.json里面没有变化
    # 就可以使用上次的缓存,不用从头安装
    - restore_cache:
    keys:
    - v2-dependencies-{{ checksum "package.json" }}
    - run: yarn install
    - save_cache:
    paths:
    - node_modules
    key: v2-dependencies-{{ checksum "package.json" }}
    - persist_to_workspace:
    root: .
    paths:
    - node_modules
    build:
    <<: *defaults
    steps:
    - checkout
    - attach_workspace:
    at: .
    - run: yarn build
    - persist_to_workspace:
    root: .
    paths:
    - dist
    test:
    <<: *defaults
    steps:
    - checkout
    - attach_workspace:
    at: .
    - run: yarn ci
    - store_test_results:
    path: test-results

    workflows:
    version: 2
    build_accept_deploy:
    jobs:
    - prepare
    - build:
    requires:
    - test
    - test:
    requires:
    - prepare
  8. push 你本地的代码

  9. 点击一下 Start Building
  10. test的过程失败了。 因为 不知道 yarn ci是什么
  11. 修改 package里的 script里的 xxx 改为 ci 再次提交
  12. 测试成功了! 但是无法看到测试覆盖率的详情!
  13. 因为我们少了配置 jest.config.js里
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 添加 jest-junit
    reporters: ["default","jest-junit"],


    // 安装依赖
    yarn add --dev jest-junit@6.3.0

    // 运行 yarn ci
    此时多了一个文件 test-results/jest/results.xml
    这个文件主要是给 ci 看的 不是给人看的
    // 所以它不该被提交 添加在 .gitignore 添加忽略

    再次 push代码

发布代码

修改 package.json

1
2
# 添加如下内容
"files":["/dist"],

如果提示 “没权限”,你就登录一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 记得先切换为默认源
npm adduser xxx
Username: xxx
Password: xxx
Email: (this IS public) xxx
Logged in as almost_00 on https://registry.npmjs.org/.
// 显示登录成功


npm publish
# 如果 还提示没权限 请修改 package.json里的name 和version


# 401 没登录
# 403 没权限

yarn 发布代码

1
2
3
# 只要你登录了
可以直接
yarn publish

测试我们的包

1
2
3
4
5
6
7
8
9
cd ~/Desktop
mkdir aa
cd aa
npm init -y
# npm i 你的包名
npm i fui_t888

发现 node_modules 你的包 dist里是空的 ,原因是 yarn publish 没发布全, 我们package.json 里的 files写的有问题
而我们重新用 npm publish 后代码是好的

修改 package.json

1
"files": ["/dist/**/*"]

再次发布代码

1
2
3
yarn publish

npm publish

ReactWheels05-单元测试

单元测试

测试我们的classes.tsx

1
# lib/helpers目录下 新建 __test__/classes.unit.jsx

classes.unit.jsx

1
2
3
4
5
6
7
import classes from '../classes'
describe('classes', () => {
it('接受 1 个 className', () => {
const result = classes('a')
expect(result).toEqual('a')
})
})

运行 yarn test 突然报错了,说缺少模块 babel-preset-react-app

1
2
3
4
5
6
7
8
9
# 我的是这个版本
yarn add babel-preset-react-app@7.0.2 --dev

# 继续 运行测试
yarn test

# 每次改都运行 yarn test 很麻烦 怎么办

yarn test --watch
查看 npm包版本信息
1
npm view jquery versions

完整测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import classes from '../classes'
describe('classes', () => {
it('接受 1 个 className', () => {
const result = classes('a')
expect(result).toEqual('a')
})
it('接受 2 个 className', ()=>{
const result = classes('a', 'b')
expect(result).toEqual('a b')
})
it('接受 undefined 结果不会出现 undefined', ()=>{
const result = classes('a', undefined)
expect(result).toEqual('a')
})
it('接受各种奇怪值', ()=>{
const result = classes(
'a', undefined, '中文', false , null
// 'a', undefined, '中文'
)
console.log(result)
expect(result).toEqual('a 中文')
})
it('接受 0 个参数', ()=>{
const result = classes()
expect(result).toEqual('')
})
})

测试我们的 Icon

1
2
3
4
5
6
7
8
9
10
11
# 新建lib/icon/__tests__/icon.unit.jsx
import * as renderer from 'react-test-renderer'
import React from 'react'
import Icon from '../icon'

describe('icon', () => {
it(' xxx ', () => {
const json = renderer.create(<Icon/>).toJSON()
expect(json).toMatchSnapshot()
})
})

运行 yarn test,报错了,意思是测试的文件里引入了 sass文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Test suite failed to run

Configuration error:

Could not locate module ./icon.scss mapped as:
/Users/huangjiaxi/Desktop/react-gulu-test-3/test/__mocks__/object-mock.js.

Please check your configuration for these entries:
{
"moduleNameMapper": {
"/\.(css|less|sass|scss)$/": "/Users/huangjiaxi/Desktop/react-gulu-test-3/test/__mocks__/object-mock.js"
},
"resolver": null
}

1 | import React from 'react';
2 | import './importIcons';
> 3 | import './icon.scss';
| ^
4 | import classes from '../helpers/classes';

解决方案就是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 根目录 新建test/__mocks__/file-mock.js
//内容如下
module.exports = 'test-file-stub';


// 如果是 scss 我们要用另外的mock
// 新建test/__mocks__/object-mock.js
//内容如下
module.exports = {}

// 配置 jest

moduleNameMapper: {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/test/__mocks__/file-mock.js",
"\\.(css|less|sass|scss)$": "<rootDir>/test/__mocks__/object-mock.js",
},

继续 yarn test,又报错了

1
2
3
4
5
6
7
8
9
10
11
12
13
TypeError: require.context is not a function
at Object.<anonymous> (/Users/huangjiaxi/Desktop/react-gulu-test-3/lib/icon/importIcons.js:3:23)


# 因为 importIcons.js里 有个 try catch ,catch里的 console.log(error)
# 删除 console.log(error) 即可

let importAll = (requireContext) => requireContext.keys().forEach(requireContext)
try {
importAll(require.context('../icons/', true, /\.svg$/))
} catch (error) {
console.log(error)
}

继续, yarn test 成功

解释测试代码里的toMatchSnapshot是啥意思

  • 运行文件产生的快照
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
运行 yarn test之后
# 目录 __tests__/__snapshots__/icon.unit.jsx.snap
文件内容如下

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`icon xxx 1`] = `
<svg
className="fui-icon"
>
<use
xlinkHref="#undefined"
/>
</svg>
`;

仔细发现我们没有传递 name

修改 icon.unit.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
describe('icon', () => {
it(' xxx ', () => {
const json = renderer.create(<Icon name="alipay"/>).toJSON()
expect(json).toMatchSnapshot()
})
})

// 再次运行 yarn test ,又报错了,代表这次修改后和上次快照的对比,我们发现这次是正确的
// 区别就是 undefined 变成了 alipay

FAIL lib/icon/__tests__/icon.unit.jsx
icon
✕ xxx (13ms)

● icon › xxx

expect(received).toMatchSnapshot()

Snapshot name: `icon xxx 1`

- Snapshot
+ Received

<svg
className="fui-icon"
>
<use
- xlinkHref="#undefined"
+ xlinkHref="#alipay"
/>
</svg>

6 | it(' xxx ', () => {
7 | const json = renderer.create(<Icon name="alipay"/>).toJSON()
> 8 | expect(json).toMatchSnapshot()
| ^
9 | })
10 | })
11 |

at Object.toMatchSnapshot (lib/icon/__tests__/icon.unit.jsx:8:18)

› 1 snapshot failed.
Snapshot Summary
› 1 snapshot failed from 1 test suite. Inspect your code changes or run `yarn test -u` to update the
m.

最后一句说了,如果你想更新快照运行 yarn test -u

运行 yarn test -u 代表我们把正确的快照保存了下来

此时快照内容如下:

1
2
3
4
5
6
7
8
9
10
11
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`icon xxx 1`] = `
<svg
className="fui-icon"
>
<use
xlinkHref="#alipay"
/>
</svg>
`;

好处是什么,如果你以后手误把 icon.tsx里name的“#”号删除,你运行 yarn test的时候就能得到上次运行成功的快照和本次的对比

  1. 你保存了一个 001版本的运行成功的快照
  2. 下次增加需求的时候,测试时,保证功能还是正确的。
  3. 001版本不通过只有两种可能,一个是版本太老了,另一个是你改出bug了

这就是Snapshot的工作原理

用 jest.fn() 测试点击事件

  • 使用enzyme库
    1
    yarn add --dev enzyme

测试点击

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import * as renderer from 'react-test-renderer'
import React from 'react'
import Icon from '../icon'
import {mount} from 'enzyme'

describe('icon', () => {
it('render successfully', () => {
const json = renderer.create(<Icon name="alipay"/>).toJSON()
expect(json).toMatchSnapshot()
})
it('onClick', () => {
let n = 1;
const fn = ()=>{
n = 2;
}
const component = mount(<Icon name="alipay" onClick={fn}/>)
component.find('svg').simulate('click');
expect(n).toEqual(2);
})
})

再次运行 yarn test ,又报错了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
✕ onClick (2ms)

● icon › onClick


Enzyme Internal Error: Enzyme expects an adapter to be configured, but found none.
To configure an adapter, you should call `Enzyme.configure({ adapter: new Adapter() })`
before using any of Enzyme's top level APIs, where `Adapter` is the adapter
corresponding to the library currently being tested. For example:

import Adapter from 'enzyme-adapter-react-15';

To find out more about this, see http://airbnb.io/enzyme/docs/installation/index.html

14 | n = 2;
15 | }
> 16 | const component = mount(<Icon name="alipay" onClick={fn}/>)
| ^
17 | component.find('svg').simulate('click');
18 | expect(n).toEqual(2);
19 | // const fn = jest.fn()



// 意思你该配置一下 Enzyme.configure({ adapter: new Adapter() })

修改 test/setupTests.js

1
2
3
4
const enzyme = require('enzyme')
const Adapter = require('enzyme-adapter-react-16')

enzyme.configure({adapter: new Adapter()})

再次运行 yarn test ,又报错了

1
2
3
4
5
6
7
8
9
10
11
12
# 意思你没有 enzyme-adapter-react-16 这个模块
FAIL lib/__tests__/hello.unit.tsx
● Test suite failed to run

Cannot find module 'enzyme-adapter-react-16' from 'setupTests.js'

1 | const enzyme = require('enzyme')
> 2 | const Adapter = require('enzyme-adapter-react-16')

# 安装它

yarn add --dev enzyme-adapter-react-16@1.11.2

再次运行 yarn test ,成功!!

如果你的测试第一次成功了,不代表成功,你一定要先失败一次,在成功才是真的成功!!!

先改成 333 然后失败,再改成 2 成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
it('onClick', () => {
let n = 1;
const fn = ()=>{
n = 2;
}
const component = mount(<Icon name="alipay" onClick={fn}/>)
component.find('svg').simulate('click');
expect(n).toEqual(333);
})

// 测试失败了

Expected: 333
Received: 2

// 此时修改为正确的,然后继续测试 yarn test
it('onClick', () => {
let n = 1;
const fn = ()=>{
n = 2;
}
const component = mount(<Icon name="alipay" onClick={fn}/>)
component.find('svg').simulate('click');
expect(n).toEqual(2);
})

// 此时成功了

但是,上面的方式有点傻!因为每次都要声明个 “n”

使用jest.fn()来模拟函数测试

1
2
3
4
5
6
it('onClick', () => {
const fn = jest.fn()
const component = mount(<Icon name="alipay" onClick={fn}/>)
component.find('svg').simulate('click')
expect(fn).toBeCalled()
})

期待fn被调用,这样虽然成功了,!但我们需要失败一次,才代表真的成功了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
it('onClick', () => {
const fn = jest.fn()
const fn2 = jest.fn()
const component = mount(<Icon name="alipay" onClick={fn}/>)
component.find('svg').simulate('click')
expect(fn2).toBeCalled()
})
// 期待 fn2被调用,明显会失败
// 报错如下

✕ onClick (37ms)

● icon › onClick

expect(jest.fn()).toBeCalled()

Expected mock function to have been called, but it was not called.

14 | const component = mount(<Icon name="alipay" onClick={fn}/>)
15 | component.find('svg').simulate('click')
> 16 | expect(fn2).toBeCalled()



然后在改回去,运行 yarn test 成功则真的成功了

it('onClick', () => {
const fn = jest.fn()
const component = mount(<Icon name="alipay" onClick={fn}/>)
component.find('svg').simulate('click')
expect(fn).toBeCalled()
})

一个小细节 为什么我点击了之后,fn马上就调用,会不会是异步的呢?

  • 基础知识,在html里事件是同步执行的没有异步,所以你点击之后fn应该马上调用没有异步的问题

IDE 提示找不到 describe 和 it 怎么办?

解决办法:

  1. yarn add -D @types/jest
  2. 在文件开头加一句 import ‘jest’

这是因为 describe 和 it 的定于位于 jest 的类型声明文件中,不信你可以按住 ctrl 并点击 jest 查看。

如果还不行,你需要在 WebStorm 里设置对 jest 的引用:

这是因为 typescript 默认排除了 node_modules 里的类型声明。

代码链接

ReactWheels04-03Icon组件下

让Icon支持sass

icon.scss

1
2
3
4
5
.fui-icon {
width: 1.4em;
height: 1.4em;
display: inline-block;
}

在 icon.tsx 里 import ‘./icon.scss’

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import './importIcons';
import './icon.scss'

interface IconProps {
name: string;
}

const Icon:React.FunctionComponent<IconProps> = (props)=> {
return (
<svg className="fui-icon">
<use xlinkHref={`#${props.name}`}/>
</svg>
)
}

export default Icon;

运行 yarn start ,报错了

1
2
3
4
5
6
7
8
9

ERROR in ./lib/icon/icon.scss 1:0
Module parse failed: Unexpected token (1:0)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
> .fui-icon {
| width: 1.4em;
| height: 1.4em;
@ ./lib/icon/icon.tsx 4:0-21
@ ./lib/index.tsx

安装loader,修改webpack配置

1
2
3
4
5
6
7
8
# 安装
yarn add style-loader css-loader sass-loader node-sass --dev

# use 里的数组意思是 从右往左
{
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader']
}
  1. sass-loader 将你的 icon.scss读取到内存(字符串) 把 sass语法变成 icon.css
  2. css-loader 将 icon.css 变成一个对象
  3. style-loader 将 对象 变成 一个 style标签 把对象里的内容放到 style.innerHTML里

让Icon支持 click

于是你去 index.tsx 里

1
2
3
4
ReactDOM.render(<div>
<Icon name="qq" onClick={() => console.log(1)}
/>
</div>, document.querySelector('#root'));

竟然直接报错了

1
2
3
client:159 [at-loader] ./lib/index.tsx:24:6 
TS2322: Type '{ name: string; onClick: () => void; }' is not assignable to type 'IntrinsicAttributes & IconProps & { children?: ReactNode; }'.
Property 'onClick' does not exist on type 'IntrinsicAttributes & IconProps & { children?: ReactNode; }'.

意思就是 IconProps 接口里没有onClick 属性

添加 onClick ,但是它的类型是什么

此时就体现你是js程序员和ts程序员的区别了

  • js只管它叫什么从来不管它是什么
  • ts一定要问你它到底是什么
1
2
3
4
5
6
// 最终答案
interface IconProps {
name: string;
// svg元素的鼠标处理函数
onClick:React.MouseEventHandler<SVGAElement>
}

以推敲的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// icon.tsx
interface IconProps {
name: string;
onClick:()=>undefined
}

// index.tsx
const fn = ()=>{
console.log(1);
}
ReactDOM.render(<div>
<Icon name="qq" onClick={fn}
/>
</div>, document.querySelector('#root'));


// 又报错了
TS2322: Type '() => void' is not assignable to type '() => undefined'.
Type 'void' is not assignable to type 'undefined'.

意思是你声明的函数 fn返回的是 void,而你要的函数返回的是undefined

所以你不能写 undefined,所以应该改写 void

interface IconProps {
name: string;
onClick:()=> void
}

const Icon:React.FunctionComponent<IconProps> = (props)=> {
return (
<svg className="fui-icon" onClick={props.onClick}>
<use xlinkHref={`#${props.name}`}/>
</svg>
)
}

我想把点击的元素打印出来怎么办?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//index.tsx
const fn = (e)=>{
console.log(e);
}

此时 ide 提示 不知道这个e是什么 你需要在IconProps里定义函数的参数的类型

// 修改 index.tsx
const fn = (e:React.MouseEvent)=>{
console.log(e);
console.log(e.target);
}

// icon.tsx
interface IconProps {
name: string;
onClick:(e:React.MouseEvent) => void
}

此时我想打印 e.target.style

  • style被标红了,意思是 我怎么知道 target里面有没有 style属性呢?
  • 因为这样是存在潜在bug的,因为你不知道target到底是什么 可能是div也可能是 img 你没法保证它一定有 width属性
    1
    2
    3
    4
    5
    6
    7
    // 修改index.tsx,此时 

    const fn = (e:React.MouseEvent)=>{
    console.log(e);
    console.log(e.target);
    console.log(e.target.style);
    }

答案是 React.MouseEvent 是可以接受参数的

1
2
3
4
5
6
7
8
9
// 如果你要明确 target的类型 就要在 MouseEvent 上加参数
const fn = (e:React.MouseEvent<SVGElement | SVGUseElement>)=>{
console.log(e);
console.log(e.target);
console.log((e.target as SVGUseElement).href);
}

// target as xxxElement 意思把这个target当作 xxx元素用
// 它是一种断言,有可能是错的

标准写法(清爽写法)

1
2
3
4
5
6
7
8
9
10
// icon.tsx
interface IconProps {
name: string;
onClick:React.MouseEventHandler<SVGElement>
}

// index.tsx
const fn:React.MouseEventHandler = (e)=>{
console.log(e.target);
}

如何接受所有事件, mouseover/doubleclick/mouseleave 这样写那么多事件要猴年马月啊

  • IconProps 里一个一个写(太笨了)
  • 是否记得 打印 target的时候有很多属性 如name/width/href 是从那里来的 是继承来的
  • 所以我们的IconProps 应该继承 一个东西
  • 编辑器里点击 import React from 'react' 搜索 onClick 看到 DOMAttributes
1
2
3
4
5
6
7
8
9
10
interface IconProps extends React.SVGAttributes{
name: string;
}
// 这样写会报错,然后提示你还需要一个参数,意思就是它里面的内容如 onClick 要接受一个 target是什么类型


// 最终答案
interface IconProps extends React.SVGAttributes<SVGElement>{
name: string;
}

修改 index.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
import ReactDOM from 'react-dom';
import React from 'react';
import Icon from './icon/icon';
const fn:React.MouseEventHandler = (e)=>{
console.log(e.target);
}
ReactDOM.render(<div>
<Icon name="qq"
onClick={fn}
onMouseEnter={()=>{console.log('enter')}}
onMouseLeave={()=>{console.log('leave')}}
/>
</div>, document.querySelector('#root'));

修改 icon.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface IconProps extends React.SVGAttributes<SVGElement>{
name: string;
}

const Icon:React.FunctionComponent<IconProps> = (props)=> {
return (
<svg className="fui-icon"
onClick={props.onClick}
onMouseEnter={props.onMouseEnter}
onMouseLeave={props.onMouseLeave}
>
<use xlinkHref={`#${props.name}`}/>
</svg>
)
}

虽然完成了我们的需求,但是如果N多个事件难道要在组件里一个一个的写吗?

优化

修改 icon.tsx

1
2
3
4
5
6
7
8
9
10
11
12
//{...props}  react 里规定 写js要用 "{}" 包起来
//所以 {...props} 别误以为是对象

const Icon:React.FunctionComponent<IconProps> = (props)=> {
return (
<svg className="fui-icon"
{...props}
>
<use xlinkHref={`#${props.name}`}/>
</svg>
)
}

这样写没问题吗?此时有bug

  • bug就是如果外面的人也传递了 className呢?
1
2
3
4
5
6
7
8
<Icon name="qq" 
className="qq"
onClick={fn}
onMouseEnter={()=>{console.log('enter')}}
onMouseLeave={()=>{console.log('leave')}}
/>

这样导致 我们的Icon组件 class="qq" 而不是 "fui-icon qq"

这就是 react和vue的理念冲突,如果你用vue不用操心它帮你处理,而react则需要你自己处理

再一次优化

1
2
3
4
5
6
7
8
9
10
const Icon:React.FunctionComponent<IconProps> = (props)=> {
const {className,...restProps} = props;
return (
<svg className={`fui-icon ${className}`}
{...restProps}
>
<use xlinkHref={`#${props.name}`}/>
</svg>
)
}

此时还有bug 如果用户不传递 className呢? 就导致 class=”fui-icon undefined”

classnames库,它就是把多个 class合并的库,但是很明显我们不会额外引入其他的库

新建 lib/helpers/classes.tsx

1
2
3
4
5
6
// 将所有参数放到names数组里,然后声明names是一个字符串数组
function classes(...names: string[]) {
return names.join(' ');
}

export default classes;

修改 icon.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React from 'react';
import './importIcons';
import './icon.scss'
import classes from '../helpers/classes';

interface IconProps extends React.SVGAttributes<SVGElement>{
name: string;
}

const Icon:React.FunctionComponent<IconProps> = (props)=> {
const {className,...restProps} = props;
return (
<svg className={classes('fui-icon',className)}
{...restProps}
>
<use xlinkHref={`#${props.name}`}/>
</svg>
)
}

export default Icon;
// 此时className={classes('fui-icon',className)} 标红了 意思这里的className可能不是 string

再次修改 classnames.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function classes(...names: (string|undefined)[]) {
return names.join(' ');
}

export default classes;

// 但是这样 ["1",undefined] ===> "1 " 多了一个空格,强迫症导致必须改了

function classes(...names: (string | undefined)[]) {
return names.filter(Boolean).join(' ');
}

export default classes;


// Boolean(xxx) 的好处就是接受一个值返回 true/false

再次优化我们的 Icon(解构赋值)

  • svg里面有name属性 ,所以name也要单独提取出来
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

import React from 'react';
import './importIcons';
import './icon.scss'
import classes from '../helpers/classes';

interface IconProps extends React.SVGAttributes<SVGElement>{
name: string;
}

const Icon:React.FunctionComponent<IconProps> = (props)=> {
const {className,name,...restProps} = props;
return (
<svg className={classes('fui-icon',className)}
{...restProps}
>
<use xlinkHref={`#${name}`}/>
</svg>
)
}

export default Icon;

我们可以直接把解构赋值放在参数里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React from 'react';
import './importIcons';
import './icon.scss';
import classes from '../helpers/classes';

interface IconProps extends React.SVGAttributes<SVGElement> {
name: string;
onClick: React.MouseEventHandler<SVGElement>
}

const Icon: React.FunctionComponent<IconProps> =
({ className, name, ...restProps }) => {
return (
<svg className={classes('fui-icon', className)}
{...restProps}
>
<use xlinkHref={`#${name}`} />
</svg>
);
};

export default Icon;

代码链接

ReactWheels04-02Icon组件上

Icon组件

icon.tsx

1
2
3
4
5
6
7
8
9
import React from 'react';

function Icon(){
return (
<span>icon</span>
)
}

export default Icon;

index.tsx

1
2
3
4
5
6
7
import ReactDOM from 'react-dom';
import React from 'react';
import Icon from './icon/icon';

ReactDOM.render(<div>
<Icon />
</div>, document.body);

为 Icon 添加 name属性

  • React.FunctionComponent 代表这是一个函数式组件
    1
    React.FunctionComponent<IconProps> 代表这个组件必须遵循 IconProps 里的属性也就是必须有name属性

icon.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
import React from 'react';

interface IconProps {
name: string;
}

const Icon:React.FunctionComponent<IconProps> = (props)=> {
return (
<span>{props.name}</span>
)
}

export default Icon;

index.tsx

1
2
3
4
5
6
7
import ReactDOM from 'react-dom';
import React from 'react';
import Icon from './icon/icon';

ReactDOM.render(<div>
<Icon name="qq"/>
</div>, document.body);

消除IDE的警告

1
2
3
4
5
6
# 为什么这样有警告
<Icon name="qq"></Icon>
# 上面的写法是 xml

# 而这里推荐的是这样,如果没有子内容就自闭和
<Icon name="qq"/>

如何现实 微信的 图标

  1. 打开 iconfont.cn 随便选个图标,账号登录,最后点svg下载
  2. 如何在一个组件里显示svg
    • 要改loader,因为webpack不认识svg
    • svg-sprite-loader
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      # 安装依赖
      yarn add svg-sprite-loader --dev

      # 修改webpack.config.js
      module:{
      rules:[
      {
      test: /\.svg$/,
      loader: 'svg-sprite-loader',
      }
      ]
      }

      # icon.tsx里
      import wechar from '../icons/wechat.svg'

      # 重新运行 yarn start
      失败了 因为 js认识 svg, ts不认识 svg

      # 重新声明 svg的类型让 ts认识
      新建 lib/types/custom.d.ts
      // 内容如下
      declare module '*.svg' {
      const content: any;
      export default content;
      }

      # 此时 ide里还是报错 ,修改 tsconfig.json

      "include": [
      "lib/**/*",
      "types/**/*"
      ]
      # 此时不报错了

路径里的 星星什么意思

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"lib/**/*",

如有如下目录
|a
|--1.js
|--2.js
|--b
|--|--3.js

如果写 a/* 就不会匹配 3.js

如果写 a/b/* 就只会匹配3.js而不会匹配 1.js 2.js

所以写 a/**/* 就是所有目录的 js

yarn start 页面警告了

1
2
3
4
5
6
7
8
9
10
react-dom.development.js:506 Warning: render(): Rendering components directly into document.body is discouraged, since its children are often manipulated by third-party scripts and browser extensions. This may lead to subtle reconciliation issues. Try rendering into a container element created for your app.


# 意思是 不能直接渲染在 document.body上
# 在 index.html里 写一个 <div id="root"></div>

# 修改 index.tsx
ReactDOM.render(<div>
<Icon name="qq"/>
</div>, document.querySelector('#root'));

如何渲染 svg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React from 'react';
import wechat from './icons/wechat.svg';

interface IconProps {
name: string;
}

const Icon:React.FunctionComponent<IconProps> = (props)=> {
return (
<svg>
<use xlinkHref={`#${props.name}`}/>
</svg>
)
}

export default Icon;

custom.d.ts有什么用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/types/custom.d.ts 内容如下
declare module '*.svg' {
const content: any;
export default content;
}

# 把 custom.d.ts 内容清空

# 我们如果引入 svg 以这样的方式 ,依然可以正常显示
import './icons/wechat.svg';
import './icons/qq.svg';
import './icons/alipay.svg';

# 但是修改成这样

import wechat from './icons/wechat.svg';

就报错了, 因为 一个 export default xxx 对应一个 import xxx from 'x'
就报错了, 因为 一个 export default xxx 对应一个 import xxx from 'x'
就报错了, 因为 一个 export default xxx 对应一个 import xxx from 'x'

# 而 custom.d.ts 里的这些 意思就是所有的.svg文件 默认导出一个东西
declare module '*.svg' {
const content: any;
export default content;
}

有一个现实问题 ,如果有100个svg,难道要写100个 import ‘xxx’;

importIcons.js

1
2
3
4
5
6
let importAll = (requireContext) => requireContext.keys().forEach(requireContext)
try {
importAll(require.context('./icons/', true, /\.svg$/))
} catch (error) {
console.log(error)
}

修改icon.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React from 'react';
import './importIcons';

interface IconProps {
name: string;
}

const Icon:React.FunctionComponent<IconProps> = (props)=> {
return (
<svg>
<use xlinkHref={`#${props.name}`}/>
</svg>
)
}

export default Icon;

tree-shaking是什么

1
2
3
4
5
6
7
8
9
10
11
12
# 到底我们应该
import A from 'a'
import B from 'b'
import C from 'c'

# 还是应该像刚才一样
importAll('./x')


第一个叫做静态引入(方便我们 tree-shaking)

第二个叫做非静态加载(写的时候方便)

比如你项目里依赖一个模块A,A里面有多个功能 a1/a2/a3 而你只用到了 a1 那么 a2/a3 则属于没有被依赖,

而 tree-shaking 则是把没有用到的依赖从打包里删掉。只留下真正用到的依赖。

tree-shaking的基础就是 静态引入

  • 非静态引入虽然方便,但是无法在后续使用 tree-shaking 来优化

代码链接

https://github.com/slTrust/react-gulu-test-3/tree/6c1bbb7b957d0090081215f9e9508279924ec8d1

ReactWheels04-01为什么造轮子

为什么造轮子

不要重复造轮子,但是要有造轮子的能力

  • 现在的前端有一个代码检查工具 tslint (lint 细碎的线头) 就是把ts里细碎的不必要的东西帮你删掉
  • tslint就是帮你检查代码的问题; 空格/回车/分号

    1. jslint 严格
    2. jshint 不那么严格
    3. jscs —— JS code style
    4. standard JS

      工具很多,造成选择困难。于是有了这句:不要重复造轮子。

  • 对于使用者:不要重复造轮子
  • 对于开发者:要造轮子。
  1. 为了不求人
    假设你用了某UI框架,发现一个bug。于是你去github反馈给开发者,开发者说两周后修复,而你的项目要求一周后上线,你怎么办?

    比如著名的前端框架React 是FaceBook 开发的。而Google会使用它吗? 不会,因为react留了一个巨大的坑,就是license里 任何一个公司如果你使用我司的react框架,如果你的产品被认为和我司的产品存在利益冲突,则收回你react的使用权

  2. 为了不流于平庸
    大家都是写CRUD,你跟别人比有什么优势?
    你如果说一句“我公司都用我写的UI框架” 是不是很NB?
    造UI轮子会遇到很多技术层面而非业务的知识,比如一些算法。
  3. 为了创造
    你为别人做了这么久的事情,有没有为自己做过什么?
  4. 为什么是UI轮子,不是其他方面的轮子
    比如,为什么不自己写个React框架,要写React UI框架呢?
    • 风口,你写一个操作系统别人会用吗?我有苹果有安卓
    • 你写一个vue /react有人用吗? 市场都占了。你能给公司带来什么?
    • 每个公司都需要UI框架

造轮子步骤

  1. SVG使用
  2. React的api
  3. TS用法
  4. Jest用法
  5. 测试覆盖率
  6. Travis CI用法
  7. 前端自动化

为什么手机上的UI框架那么不好用

  1. 功能不符合要求
  2. 很久不更新
  3. 手机交互很少
  4. 很多东西展示不了如 table
  • PC上最重要的就是 table
  • 手机上最重要的就是 下拉更新

Java-030-cookie和session

Session和Cookie

HTTP是无状态协议

之前我们学习HTTP协议的时候, 我们提到过, HTTP协议是无状态的!, 这个无状态是什么意思呢? 那首先我们来看一下有状态是什么样的.

有状态连接

  • 客户端第一次给服务端发送请求1, 然后连接建立起来了
  • 客户端第二次给服务端发送请求2, 这时候, 服务端自然知道这个请求是来自客户端的
    这个状态就是说, 客户端和服务端是可继续延续之前的请求, 然后继续.

无状态

就是说, 默认情况下, 每个HTTP请求都是独立的, 和之前的请求和之后的请求都没有关系.

那问题来了, 如何基于无状态的HTTP协议, 实现有状态的Web应用功能呢? 例如:

  • 登录状态
  • 浏览记录

Session (会话)

我们引入一个抽象概念, Session. 首先, 清空自己对这个概念的主观认识. Session不是Web开发的概念, 和Cookie也没有必然关系. 这是一个独立的概念!

Session — 指的就是一种状态, 客户端和服务端连接中的会话状态! 以便让每次请求能够延续之前请求.

回到Web开发, 我们需要基于无状态的HTTP协议, 实现有状态的Web应用功能, 本质上就是要实现一个Session的机制, 维护上一次请求的状态 (已完成或未完成). 为了维护这个状态, 则需要存储:

客户端

第一种选择是在客户端存储和维护Session, 每次给服务端发送请求的时候, 在请求中夹带Session信息.

服务端

第二种选择是在服务端存储和维护Session, 然后每次处理客户端的请求时候, 从存储中找出与与这个客户端对应的Session信息.

但是这里有个问题, 服务端需要把客户端发来的请求和服务端存储的Session信息对应上, 则客户端发来的请求还是需要夹带一个状态, 让服务端能够把这个请求和对应的客户端对应上.

比如:

  • 客户端可以在请求中的authentication字段夹带一个SessionId或Session密钥.
  • 客户端可以在请求中的cookie字段夹带一个SessionId或Session密钥.
  • 客户端可以在请求中的RequestBody夹带一个SessionId或Session密钥.
  • 等等

当服务端接收到请求,, 然后解析出请求中的这个Session密钥, 则可以把这个请求和对应的Session信息对应起来, 恢复出上一个请求的状态信息.

Cookie就是HTTP协议提供的一个机制, 能够在请求中夹带信息, 用于记录状态. 基本的操作流程是:

  1. 客户端给服务端发送请求.
  2. 服务端在HTTP响应中, 添加Cookie信息, 一起返回给客户端.
  3. 客户端接收到请求的响应. 把Cookie信息存储在本地, 之后发送给服务端的请求中, 都会夹带这个Cookie信息(浏览器默认功能).
  4. 服务端接收到这个Cookie信息, 就能够恢复出之前状态信息.

比如我们使用Cookie实现Session:

  1. 客户端给服务端发送请求.
  2. 服务端接收到请求之后, 在存储中创建这个这个客户端的Session信息, 并且把这个Session的Id添加到Cookie中, 返回给客户端.
  3. 客户端接受到请求响应之后, 把Cookie信息存储在本地, 之后发送给服务端的请求中, 都会夹带这个Cookie信息(浏览器默认功能).
  4. 服务端再接收到这个客户端的请求, 解析出其中的Cookie信息 (包含SessionId), 通过这个SessionId提取出响应的Session信息, 恢复出之前和这个客户端之前请求的状态.

安全性

Cookie盗用

我们很容易感觉到, Cookie里存储的SessionId是用来和服务端存储的Session有一一对应关系. 如果存储在客户端的Cookie信息被盗之后, 黑客可以使用这个SessionId, 伪造请求发送给服务端, 从而可以恢复出在服务端的Session信息, 从而可以利用.

威胁

本地存储

  • 本地加密
  • 防止跨站攻击

传输

加密存储HTTPS (HTTP + SSL), 也就是使用可以加密的请求.

服务端存储

有效期, Session有有效期, 需要重新提供认证信息.

代码链接

相关链接