- Typescript+React开发环境搭建
- 配置ESLint和Git Hooks代码检查
- 使用assert、chai和mocha进行单元测试
- react、redux、react-router和connected-react-router的类型声明和实战开发
-
1、Ts工程化开发专业级React项目
前端工程化就是通过流程规范化、标准化提升团队协作效率
- 通过组件化、模块化提升代码质量
- 使用构建工具、自动化工具提升开发效率
编译->打包(合并)->压缩->代码检查->测试->持续集成
2、项目初始化
mkdir rock_typescript_developmentcd rock_typescript_developmentnpm initpackage name: (rock_typescript_development)version: (1.0.0)description: TypeScript工程化开发entry point: (index.js)test command:git repository: https://gitee.com/s2265681/rock_typescript_developmentkeywords: typescript,reactauthor: rockshanglicense: (ISC) MIT
3、git规范和changelog
3-1、良好的git commit 好处
可以加快code review 流程
- 可以根据git commit 的元数据生成change log
- 可以让其他开发者知道修改的原因
3-2、良好的commit
- commitizen是一个格式化commit message的工具
- validate-commit-msg 用于检查项目的 Commit message 是否符号格式
conventional-changelog-cli 可以从git metadata 生成变更日志
统一团队的git commit 标准
- 可以使用 angular 和 git commit 日志作为基本规范
- 提交的类型限制为 feat、fix、docs、style、refactor、pref、test、chore、revert等
提交信息分为两部分,标题(首字母不打些,末尾不加标点)、主体内容(描述修改内容)
日志提交友好的类型选择提示 使用commitize工具
- 不符合要求格式的日志拒绝提交的保障机制,需要使用validate-commit-msg工具
- 统一changelog文档信息生成,使用 conventional-changelog-cli 工具
cnpm i commitizen validate-commit-msg conventional-changelog-cli -Dcommitizen init cz-conventional-changelog --save --save-exactgit cz
3-3、gitignore
node_modules.vscodedist
3-4、提交的格式
<type>(<scope>):<subject/><BLANK LINE><body><BLANK LINE><footer>
- 代表某次提交的类型,比如是修复bug还是增加feature
- 表示作用域,比如一个页面或一个组件
- 主题 ,概述本次提交的内容
- 详细的影响内容
- 修复的bug和issue链接
| 类型 | 说明 |
|---|---|
| feat | 新增feature |
| fix | 修复bug |
| docs | 仅仅修改文档如readme合changelog等 |
| style | 仅仅修改空格缩进逗号等 |
| refactor | 代码重构没有加新功能或者修复bug |
| perf | 优化相关性能体验 |
| test | 测试用例单元测试集成测试等 |
| chore | 改变构建流程增加依赖库工具等 |
| revert | 回滚到上一个版本等 |
| ci | CI配置,脚本文件等更新 |
3-5、husky
- validate-commit-msg 可以来检查我们的commit规范
- husky可以把validate-commit-msg 作为一个githook 来验证提交消息
安装
cnpm i husky validate-commit-msg --save-dev
package.json
"husky": {"hooks": {"commit-msg": "validate-commit-msg"}}
3-6、生成CHANGELOG.md
- conventional-changelog-cli 默认推荐的 commit 标准是来自angular项目
- 参数-i CHANGELOG.md 表示从 CHANGELOG.md 读取 changelog
- 参数 -s 表示读写 CHANGELOG.md 为同一文件
- 参数 -r 表示生成 changelog 所需要使用的 release 版本数量,默认为1,全部则是0
安装
cnpm i conventional-changelog-cli -D
package.json中增加以下脚本
"scripts":{"commitmsg":"validata-commit-msg","changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0"},"config": {"commitizen": {"path": "cz-conventional-changelog"}},
git cz 命令替换 git commit
然后一顿骚操作,就可以按cz的格式提交代码了,
最后 npm run version 就可以生成changelog了
3-7、 git commit 、CHANGELOG 和版本发布的标准自动化文档
https://zhuanlan.zhihu.com/p/51894196
4、支持Typescript
基本参数
| 参数 | 解释 |
|---|---|
| target | 用于指定编译之后的版本目标 |
| module | 生成的模块形式:none、commonjs、amd、system、umd、es6、 es2015 或 esnext 只有 amd 和 system 能和 outFile 一起使用 target 为 es5 或更低时可用 es6 和 es2015 |
| lib | 编译时引入的 ES 功能库,包括:es5 、es6、es7、dom 等。如果未设 置,则默认为: target 为 es5 时: [“dom”, “es5”, “scripthost”] target 为 es6 时: [“dom”, “es6”, “dom.iterable”, “scripthost”] |
| allowJs | 是否允许编译JS文件,默认是false,即不编译JS文件 |
| checkJs | 是否检查和报告JS文件中的错误,默认是false jsx |
| jsx | 指定jsx代码用于的开发环境 preserve 指保留JSX语法,扩展名 为.jsx ,react-native是指保留jsx语法,扩展名js,react指会编译成ES5语 法 详解 |
| declaration | 是否在编译的时候生成相应的.d.ts 声明文件 |
| declarationDir | 生成的 .d.ts 文件存放路径,默认与 .ts 文件相同 |
| declarationMap | 是否为声明文件.d.ts生成map文件 |
| sourceMap | 编译时是否生成.map 文件 |
| outFile | 是否将输出文件合并为一个文件,值是一个文件路径名,只有设置module 的值为amd 和system 模块时才支持这个配置 |
| outDir | outDir 指定输出文件夹 |
| rootDir | 编译文件的根目录,编译器会在根目录查找入口文件 |
| composite | 是否编译构建引用项目 |
| removeComments | 是否将编译后的文件中的注释删掉 |
| noEmit | 不生成编译文件 |
| importHelpers | 是否引入tslib 里的辅助工具函数 |
| downlevelIteration | 当target为ES5 或ES3 时,为for-of 、spread 和destructuring 中的 迭代器提供完全支持 |
| isolatedModules | 指定是否将每个文件作为单独的模块,默认为true |
5、支持React
5-1、安装
cnpm i react react-dom @types/react @types/react-dom react-router-dom@types/react-router-dom -Scnpm i webpack webpack-cli webpack-dev-server html-webpack-plugin hoist-nonreact-statics -Dcnpm i typescript ts-loader source-map-loader -Dcnpm i redux react-redux @types/react-redux redux-thunk redux-logger@types/redux-logger -Scnpm i connected-react-router -S

- ts-loader可以让Webpack使用TypeScript的标准配置文件tsconfig.json编译TypeScript代码。
- source-map-loader使用任意来自Typescript的sourcemap输出,以此通知webpack何时生成自
己的sourcemaps,这让你在调试最终生成的文件时就好像在调试TypeScript源码一样。
5-2、tsconfig.json
{"compilerOptions": {"outDir": "./dist","sourceMap": true,"strict": true,"noImplicitAny": true,"strictNullChecks": true,"module": "commonjs","target": "es5","jsx": "react","baseUrl": ".","esModuleInterop": true,"paths": {"@/*": ["src/*"]}},"include": ["./src/**/*"]}

5-3、webpack.config.js
webpack.config.js
const webpack = require('webpack');const path = require('path');const HtmlWebpackPlugin = require('html-webpack-plugin');module.exports = {mode:'development',devtool:false,entry:'./src/index.tsx',output:{path:path.join(__dirname,'dist'),filename:'[name].[hash:5].js'},devServer:{hot:true,contentBase:path.join(__dirname,'dist'),historyApiFallback:{//browserHash 刷新重定向到index.htmlindex:'./index.html'}},resolve:{extensions:[".ts",".tsx",".js",".json"],alias:{"@":path.resolve("src")}},module:{rules:[{test:/\.tsx?$/,loader:'ts-loader'}]},plugins:[new HtmlWebpackPlugin({template:'./src/index.html',filename:'index.[hash:5].html'}),new webpack.HotModuleReplacementPlugin()]}
5-4、src/index.tsx
import * as React from 'react';import * as ReactDOM from 'react-dom';let root = document.getElementById('root');let props = { className: 'title' };let element= React.createElement('div', props, 'hello');ReactDOM.render(element, root);
5-5、 src/index.html
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>typescript</title></head><body><div id="root"></div></body></html>
6、代码规范
- 规范的代码可以促进团队合作
- 规范的代码可以降低维护成本
- 规范的代码有助于code review(代码审查)
6-1、常见的代码规范文档
6-2、代码检查
- Eslint 是一款插件化的Javascript静态代码检查工具,可以通过Vscode安装插件
- 也可以模块安装
cnpm i eslint typescript @typescript-eslint/parser @typescript-eslint/eslintplugin--save-dev
6-3、eslintrc配置文件
.eslintrc.js
module.exports = {"parser":"@typescript-eslint/parser","plugins":["@typescript-eslint"],"rules":{"no-var":"error","no-extra-semi":"error","@typescript-eslint/indent":["error",4]},"parserOptions": {"ecmaVersion": 6,"sourceType": "module","ecmaFeatures": {"modules": true}}}
6-4、代码检查
package.json
{"name": "zhufeng_react_typescript","version": "1.0.0","description": "react+ts+webpack5x","main": "index.js","scripts": {"start": "webpack serve","build": "webpack","lint": "eslint src --ext .tsx","lint:fix": "eslint src --ext .tsx --fix","test":"jest"},"repository": {"type": "git","url": "git+https://github.com/s2265681/rock_react_typescript.git"},"keywords": ["typescript","react"],"husky": {"hooks": {"commit-msg": "validate-commit-msg"}},"author": "rockshang","license": "MIT","bugs": {"url": "https://github.com/s2265681/rock_react_typescript/issues"},"homepage": "https://github.com/s2265681/rock_react_typescript#readme","devDependencies": {"@typescript-eslint/eslint-plugin": "^4.8.0","@typescript-eslint/parser": "^4.8.0","commitizen": "^4.2.2","conventional-changelog-cli": "^2.1.1","eslint": "^7.13.0","hoist-non-react-statics": "^3.3.2","html-webpack-plugin": "^4.5.0","husky": "^4.3.0","source-map-loader": "^1.1.2","ts-loader": "^8.0.11","typescript": "^4.0.5","validate-commit-msg": "^2.14.0","webpack": "^5.4.0","webpack-cli": "^4.2.0","webpack-dev-server": "^3.11.0"},"dependencies": {"@types/react": "^16.9.56","@types/react-dom": "^16.9.9","@types/react-redux": "^7.1.11","@types/react-router-dom": "^5.1.6","@types/redux-logger": "^3.0.8","connected-react-router": "^6.8.0","react": "^17.0.1","react-dom": "^17.0.1","react-redux": "^7.2.2","react-router-dom": "^5.2.0","redux": "^4.0.5","redux-logger": "^3.0.6","redux-thunk": "^2.3.0"}}
6-5、VScode 配置自动修复
.vscode/settings.json
{"eslint.validate": ["javascript","javascriptreact","typescript","typescriptreact"],"editor.codeActionsOnSave": {"source.fixAll.eslint": true}}
7、单元测试
7-1、安装配置
cnpm i jest @types/jest ts-jest -Dnpx ts-jest config:init
7-2、src/unit.tsx
src/unit.tsx
function sum(a:number,b:number){return a+b}module.exports = {sum}
7-3、test/unit.test.tsx
test/unit.test.tsx
let unit = require('../src/unit');test('1+1=2',()=>{expect(unit.sum(1,1)).toBe(2);});test('1+2=3',()=>{expect(unit.sum(1,2)).toBe(3);});
7-4、package.json
"scripts":{"test":"jest"}
8、持续集成
- Travis CI 提供的是持续集成服务(Continuous Integration,简称CI), 它绑定Github上面的项目,只要有新的代码,就会自动抓取,然后,提供一个运行环境,执行测试,完成构建,还能部署到服务器
- 持续集成的是只要有代码变更,就自动运行构建和测试,反馈运行结果,确保符合羽骑以后,在将新代码集成到主干
- 持续集成的好处在于,每次代码的小幅变更,就能看到运行结果,从而不断累计小的变更,而不是在开发周期结束时,一下子合并大块代码
8-1、 登陆并创建项目
- Travis CI 只支持Github,所有必须拥有github
- 该账号下面有一个项目,里面有可以运行的代码,包含构建或测试脚本
- 你需要激活了一个仓库, Travis 会监听这个仓库的所有变化
8-2、.travis.yml
- Travis 要求项目的根目录下面,必须有一个 .travis.yml 文件,这是配置文件,指定Travis的行为
- 该文件必须保存在Github仓库里面,一旦代码仓库有新的 Commit, Travis就会去这个文件,执行里面的命令
- language阻断指定了默认运行环境
.travis.yml
language: node_jsnode_js:- '11'install: npm installscript: npm test
8-4、 实战
生成项目并上传gitbub
npx create-react-app rock-typescript-development
同步仓库
登陆 travis-ci cdn 选择同步仓库
设置仓库环境变量
| 变量名 | 含义 |
|---|---|
| GH_TOKEN | 用户生成的令牌 |
| GH_REF | 仓库地址 |
Github生成访问令牌 - 添加授权
- 访问令牌的作用是授权仓库操作权限
- https://github.com/settings/tokens
- Github->Developer settings -> Persional access tokens->Generate new token -> Generate token -> copy Token
.travis.yml
language: node_jsnode_js:- '11'cache:directories:- node_modulesinstall:- npm installscript:- npm run buildafter_script:- cd ./dist- git init- git config user.name "s2265681"- git config user.email "s2265681@163.com"- git add .- git commit -m "react project"- git push --force --quiet "https://${GH_TOKEN}@github.com/s2265681/rock_react_typescript.git" master:gh-pagesbranches:only:- master
9、React元素

9-1、原生组件
src/index.tsx
import * as React from 'react';
import * as ReactDOM from 'react-dom';
let root: HTMLElement | null = document.getElementById('root');
interface Props {
className: string
}
let props: Props = { className: 'title' };
let element: React.DetailedReactHTMLElement<Props, HTMLDivElement> = (
React.createElement<Props, HTMLDivElement>('div', props, 'hello')
)
ReactDOM.render(element, root);
src\typings.tsx
export interface DOMAttributes {
children?: ReactNode;
}
export interface HTMLAttributes extends DOMAttributes {
className?: string;
}
export interface ReactElement<P = any,T extends string> {
type: T;
props: P;
}
export interface DOMElement extends ReactElement{}
export interface ReactHTML { div: HTMLDivElement }
export interface DetailedReactHTMLElement extends DOMElement{
type: keyof ReactHTML;
}
export type ReactText = string | number;
export type ReactChild = ReactElement | ReactText;
export type ReactNode = ReactChild | boolean | null | undefined;
export declare function createElement<P extends {}>(
type: string,
props?: P,
...children: ReactNode[]): ReactElement;
9-2、函数组件
src\index.tsx
import * as React from 'react';
import * as ReactDOM from 'react-dom';
let root: HTMLElement | null = document.getElementById('root');
interface Props {
className: string
}
let props: Props = { className: 'title' };
function Welcome(props: Props):React.DetailedReactHTMLElement<Props,
HTMLDivElement> {
return React.createElement<Props, HTMLDivElement>('div', props, 'hello');
}
let element: React.FunctionComponentElement<Props> = (
React.createElement<Props>(Welcome, props)
)
ReactDOM.render(element, root);
src\typings.tsx
export interface DOMAttributes {
children?: ReactNode;
}
export interface HTMLAttributes extends DOMAttributes {
className?: string;
}
+export type JSXElementConstructor<P> = ((props: P) => ReactElement | null)
+export interface ReactElement<P = any, T extends string |
JSXElementConstructor<any> = string> {
type: T;
props: P;
}
export interface DOMElement extends ReactElement{}
export interface ReactHTML { div: HTMLDivElement }
export interface DetailedReactHTMLElement extends DOMElement{
type: keyof ReactHTML;
}
export type ReactText = string | number;
export type ReactChild = ReactElement | ReactText;
export type ReactNode = ReactChild | boolean | null | undefined;
+type PropsWithChildren<P> = P & { children?: ReactNode };
+interface FunctionComponent<P = {}> {
+ (props: PropsWithChildren<P>): ReactElement | null;
+}
+interface FunctionComponentElement<P> extends ReactElement<P,
FunctionComponent<P>> {}
+export declare function createElement<P extends {}>(
+ type: FunctionComponent<P>,
+ props?: P,
+ ...children: ReactNode[]): FunctionComponentElement<P>;
export declare function createElement<P extends {}>(
type: string,
props?: P,
...children: ReactNode[]): ReactElement;
9-3、类组件
src\index.tsx
import * as React from 'react';
import * as ReactDOM from 'react-dom';
let root: HTMLElement | null = document.getElementById('root');
interface Props {
className: string
}
interface State {
count:number
}
class Welcome extends React.Component<Props, State> {
state = { count: 0 }
render():React.DetailedReactHTMLElement<Props, HTMLDivElement> {
return React.createElement<Props, HTMLDivElement>('div', this.props,
this.state.count);
}
}
let props: Props = { className: 'title' };
let element = (
React.createElement<Props>(Welcome, props)
)
ReactDOM.render(element, root);
src/typings.tsx
export interface DOMAttributes {
children?: ReactNode;
}
export interface HTMLAttributes extends DOMAttributes {
className?: string;
}
export type JSXElementConstructor<P> =
| ((props: P) => ReactElement | null)
+| (new (props: P) => Component<P, any>);
export interface ReactElement<P = any, T extends string |
JSXElementConstructor<any> = string> {
type: T;
props: P;
}
export interface DOMElement extends ReactElement{}
export interface ReactHTML { div: HTMLDivElement }
export interface DetailedReactHTMLElement extends DOMElement{
type: keyof ReactHTML;
}
export type ReactText = string | number;
export type ReactChild = ReactElement | ReactText;
export type ReactNode = ReactChild | boolean | null | undefined;
type PropsWithChildren<P> = P & { children?: ReactNode };
interface FunctionComponent<P = {}> {
(props: PropsWithChildren<P>): ReactElement | null;
}
interface FunctionComponentElement<P> extends ReactElement<P,
FunctionComponent<P>> {}
+type ComponentState = any;
+declare class Component<P, S> {
+ setState(state: any): void;
+ render(): ReactNode;
+}
+interface ComponentClass<P = {}, S = ComponentState> {
+ new(props: P): Component<P, S>;
+}
+interface ComponentElement<P> extends ReactElement<P, ComponentClass<P>> {}
+export declare function createElement<P extends {}>(
+ type: ComponentClass<P>,
+ props?: P,
+ ...children: ReactNode[]): ComponentElement<P>;
export declare function createElement<P extends {}>(
type: FunctionComponent<P>,
props?: P,
...children: ReactNode[]): FunctionComponentElement<P>;
export declare function createElement<P extends {}>(
type: string,
props?: P,
...children: ReactNode[]): ReactElement;
10、创建组件
10-1、Counter.tsx
src\components\Counter.tsx
import * as React from 'react';
export interface Props {
number: number
}
export default class Counter extends React.Component<Props>{
render() {
const { number } = this.props;
return (
<div>
<p>{number}</p>
</div>
)
}
}
10-2、 types.tsx
src\components\Todos\types.tsx
export type Todo = {
id:number;
text:string
}
10-3、TodoItem.tsx
src\components\Todos\TodoItem.tsx
import * as React from "react";
import { Todo } from './types';
const todoItemStyle: React.CSSProperties = {
color: "red",
backgroundColor: "green",
};
interface Props {
todo: Todo;
}
const TodoItem: React.FC<Props> = (props: Props) => (
<li style={todoItemStyle}>{props.todo.text}</li>
);
TodoItem.defaultProps;
export default TodoItem;
10-4、TodoInput.tsx
src\components\Todos\TodoInput.tsx
import * as React from "react";
import { Todo } from './types';
interface Props {
addTodo:(todo:Todo)=>void
}
interface State {
text:string
}
let id=0;
export default class TodoInput extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
text:"",
};
}
public render() {
const { text } = this.state;
const { handleChange, handleSubmit } = this;
return (
<form onSubmit={handleSubmit}>
<input type="text" value={text} onChange={this.handleChange} />
<button type="submit">添加</button>
</form>
);
}
handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ text: e.target.value });
}
handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
let text = this.state.text.trim();
if (!text) {
return;
}
this.props.addTodo({id:id++,text});
this.setState({ text: "" });
}
}
10-5、Todos\index.tsx
src\components\Todos\index.tsx
import * as React from 'react';
import TodoInput from './TodoInput';
import TodoItem from './TodoItem';
import { Todo} from './types';
const ulStyle: React.CSSProperties = {
width: "100px"
};
export interface Props {
title:string
}
export interface State {
todos: Todo[]
}
export default class Todos extends React.Component<Props,State>{
state = {todos:new Array<Todo>()};
addTodo = (todo:Todo) =>{
this.setState({todos:[...this.state.todos,todo]});
}
render() {
return (
<div>
<h1>{this.props.title}</h1>
<TodoInput addTodo={this.addTodo}/>
<ul style={ulStyle}>
{
this.state.todos.map(todo=><TodoItem key={todo.id} todo={todo}/>)
}
</ul>
</div>
)
}
}
10-6、 src\index.tsx
src/index.tsx
import * as React from "react";
import * as ReactDOM from "react-dom";
import Counter from "./components/Counter";
import Todos from "./components/Todos";
ReactDOM.render(<><Counter number={100} /><Todos title="待办事项"/></>,
document.getElementById("root"));
12、高阶组件
13、集成redux
14、使用路由
15、文档
更多 见文档
react+typescript.pdf
