TS + React 工程化实践

1. TypeScript工程化开发

  • 前端工程化就是通过流程规范化、标准化提升团队协作效率
  • 通过组件化、模块化提升代码质量
  • 使用构建工具、自动化工具提升开发效率
  • 编译 => 打包(合并) => 压缩 => 代码检查 => 测试 => 持续集成

2.初始化项目

mkdir zhufeng_typescript_development
cd zhufeng_typescript_development
npm init
package name: (zhufeng_typescript_development)
version: (1.0.0)
description: TypeScript工程化开发
entry point: (index.js)
test command:
git repository: https://gitee.com/zhufengpeixun/zhufeng_typescript_development
keywords: typescript,react
author: zhangrenyang
license: (ISC) MIT

3. git规范和changelog

3.1 良好的git commit好处

  • 可以加快code review 的流程
  • 可以根据git commit 的元数据生成changelog
  • 可以让其它开发者知道修改的原因

3.2 良好的commit

  • commitizen是一个格式化commit message的工具

  • validate-commit-msg 用于检查项目的 Commit message 是否符合格式

  • conventional-changelog-cli可以从git metadata生成变更日志

  • 统一团队的git commit 标准

  • 可以使用angulargit commit日志作为基本规范

    • 提交的类型限制为 feat、fix、docs、style、refactor、perf、test、chore、revert等
    • 提交信息分为两部分,标题(首字母不大写,末尾不要加标点)、主体内容(描述修改内容)
  • 日志提交友好的类型选择提示 使用commitize工具

  • 不符合要求格式的日志拒绝提交 的保障机制

    • 需要使用validate-commit-msg工具
  • 统一changelog文档信息生成

    • 使用conventional-changelog-cli工具
cnpm i commitizen  validate-commit-msg conventional-changelog-cli -D
commitizen init cz-conventional-changelog --save --save-exact
git cz

3.3 提交的格式

<type>(<scope>):<subject/>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>
  • <type>代表某次提交的类型,比如是修复bug还是增加feature
  • <scope>表示作用域,比如一个页面或一个组件
  • <subject> 主题 ,概述本次提交的内容
  • <body> 详细的影响内容
  • <footer> 修复的bug和issue链接
类型 含义
feat 新增feature
fix 修复bug
docs 仅仅修改了文档,比如README、CHANGELOG、CONTRIBUTE等
style 仅仅修改了空格、格式缩进、偏好等信息,不改变代码逻辑
refactor 代码重构,没有新增功能或修复bug
perf 优化相关,提升了性能和体验
test 测试用例,包括单元测试和集成测试
chore 改变构建流程,或者添加了依赖库和工具
revert 回滚到上一个版本
ci CI 配置,脚本文件等更新

3.4 husky

  • validate-commit-msg可以来检查我们的commit规范
  • husky可以把validate-commit-msg作为一个githook来验证提交消息
cnpm i husky  validate-commit-msg --save-dev
  "husky": {
    "hooks": {
      "commit-msg": "validate-commit-msg"
    }
  }

3.5 生成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
"scripts": {
    "changelogs": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0"
}

4. 支持Typescript

tsc --init

基本参数

参数 解释
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代码用于的开发环境 preserve指保留JSX语法,扩展名为.jsx,react-native是指保留jsx语法,扩展名js,react指会编译成ES5语法 详解
declaration 是否在编译的时候生成相应的.d.ts声明文件
declarationDir 生成的 .d.ts 文件存放路径,默认与 .ts 文件相同
declarationMap 是否为声明文件.d.ts生成map文件
sourceMap 编译时是否生成.map文件
outFile 是否将输出文件合并为一个文件,值是一个文件路径名,只有设置module的值为amdsystem模块时才支持这个配置
outDir 指定输出文件夹
rootDir 编译文件的根目录,编译器会在根目录查找入口文件
composite 是否编译构建引用项目
removeComments 是否将编译后的文件中的注释删掉
noEmit 不生成编译文件
importHelpers 是否引入tslib里的辅助工具函数
downlevelIteration 当target为ES5ES3时,为for-ofspreaddestructuring中的迭代器提供完全支持
isolatedModules 指定是否将每个文件作为单独的模块,默认为true

严格检查

参数 解释
strict 是否启动所有类型检查
noImplicitAny 不允许默认any类型
strictNullChecks 当设为true时,null和undefined值不能赋值给非这两种类型的值
strictFunctionTypes 是否使用函数参数双向协变检查
strictBindCallApply 是否对bind、call和apply绑定的方法的参数的检测是严格检测的
strictPropertyInitialization 检查类的非undefined属性是否已经在构造函数里初始化
noImplicitThis 不允许this表达式的值为any类型的时候
alwaysStrict 指定始终以严格模式检查每个模块

额外检查

参数 解释
noUnusedLocals 检查是否有定义了但是没有使用的变量
noUnusedParameters 检查是否有在函数体中没有使用的参数
noImplicitReturns 检查函数是否有返回值
noFallthroughCasesInSwitch 检查switch中是否有case没有使用break跳出

模块解析检查

参数 解释
moduleResolution 选择模块解析策略,有nodeclassic两种类型,详细说明
baseUrl 解析非相对模块名称的基本目录
paths 设置模块名到基于baseUrl的路径映射
rootDirs 可以指定一个路径列表,在构建时编译器会将这个路径列表中的路径中的内容都放到一个文件夹中
typeRoots 指定声明文件或文件夹的路径列表
types 用来指定需要包含的模块
allowSyntheticDefaultImports 允许从没有默认导出的模块中默认导入
esModuleInterop 为导入内容创建命名空间,实现CommonJS和ES模块之间的互相访问
preserveSymlinks 不把符号链接解析为其真实路径

sourcemap检查

参数 解释
sourceRoot 调试器应该找到TypeScript文件而不是源文件位置
mapRoot 调试器找到映射文件而非生成文件的位置,指定map文件的根路径
inlineSourceMap 指定是否将map文件的内容和js文件编译在一个同一个js文件中
inlineSources 是否进一步将.ts文件的内容也包含到输出文件中

试验选项

参数 解释
experimentalDecorators 是否启用实验性的装饰器特性
emitDecoratorMetadata 是否为装饰器提供元数据支持

试验选项

参数 解释
files 配置一个数组列表,里面包含指定文件的相对或绝对路径,编译器在编译的时候只会编译包含在files中列出的文件
include include也可以指定要编译的路径列表,但是和files的区别在于,这里的路径可以是文件夹,也可以是文件
exclude exclude表示要排除的、不编译的文件,他也可以指定一个列表
extends extends可以通过指定一个其他的tsconfig.json文件路径,来继承这个配置文件里的配置
compileOnSave 在我们编辑了项目中文件保存的时候,编辑器会根据tsconfig.json的配置重新生成文件
references 一个对象数组,指定要引用的项目

5. 支持React

5.1 安装

cnpm i typescript webpack webpack-cli webpack-dev-server ts-loader cross-env webpack-merge clean-webpack-plugin html-webpack-plugin -D
cnpm i babel-loader @babel/core @babel/cli @babel/plugin-proposal-class-properties @babel/plugin-proposal-object-rest-spread @babel/preset-env @babel/preset-typescript -D

5.2 webpack.config.js

webpack.config.js

const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");
module.exports = {
  mode: "development",
  devtool:false,
  entry: "./src/index.tsx",
  output: {
    filename: "[name].[hash].js",
    path: path.join(__dirname, "dist"),
  },
  devServer: {
    hot: true,
    contentBase: path.join(__dirname, "dist"),
    historyApiFallback: {
      index: "./index.html",
    },
  },
  resolve: {
    extensions: [".ts", ".tsx", ".js", ".json"],
    alias: {
      "@": path.resolve("src"), // 这样配置后 @ 可以指向 src 目录
    },
  },

  module: {
    rules: [
      {
        test: /\.tsx?$/,
        loader: "ts-loader"
      }
    ],
  },

  plugins: [
    new HtmlWebpackPlugin({
      template: "./src/index.html"
    }),
    new webpack.HotModuleReplacementPlugin()
  ],
};

5.3 src\index.tsx

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.4 src\index.html

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>

5.5 package.json

{
  "scripts": {
+    "start": "cross-env NODE_ENV=development webpack serve --config ./config/webpack.dev.js",
+    "build": "cross-env NODE_ENV=production npm run eslint && webpack --config ./config/webpack.prod.js",
    "eslint": "eslint src --ext .ts",
    "eslint:fix": "eslint src --ext .ts --fix",
    "changelogs": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
    "test": "mocha --require ts-node/register test/**/*"
  }
}

6. 代码规范

  • 规范的代码可以促进团队合作
  • 规范的代码可以降低维护成本
  • 规范的代码有助于 code review(代码审查)

6.1 常见的代码规范文档

6.2 代码检查

  • Eslint 是一款插件化的 JavaScript 静态代码检查工具,ESLint 通过规则来描述具体的检查行为
6.2.1 模块安装
cnpm i eslint typescript @typescript-eslint/parser @typescript-eslint/eslint-plugin --save-dev
6.2.2 eslintrc配置文件

.eslintrc.js

module.exports = {
    "parser":"@typescript-eslint/parser",
    "plugins":["@typescript-eslint"],
    "rules":{
        "no-var":"error",
        "no-extra-semi":"error",
        "@typescript-eslint/indent":["error",2]
    },
    "parserOptions": {
        "ecmaVersion": 6,
        "sourceType": "module",
        "ecmaFeatures": {
          "modules": true
        }
    }
}
6.2.3 代码检查

package.json

"scripts": {
    "start": "webpack",
    "build": "tsc",
    "eslint": "eslint src --ext .ts",
    "eslint:fix": "eslint src --ext .ts --fix"
  }

src/1.ts

var name2 = 'zhufeng';;;
if(true){
    let a = 10;
}

执行命令

npm run eslint
1:1   error  Unexpected var, use let or const instead      no-var
1:23  error  Unnecessary semicolon                         no-extra-semi
1:24  error  Unnecessary semicolon                         no-extra-semi
3:1   error  Expected indentation of 2 spaces but found 4  @typescript-eslint/indent
6.2.4 配置自动修复
  • 安装vscode的eslint插件
  • 配置自动修复参数

.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 -D
npx ts-jest config:init

7.2 src\calculator.tsx

src\calculator.tsx

function sum(a: number, b: number) {
    return a + b;
}
function minus(a: number, b: number) {
    return a - b;
}
module.exports = {
    sum,
    minus
}

7.3 tests\calculator.spec.tsx

tests\calculator.spec.tsx

let math = require('../src/calculator');
test('1+1=2', () => {
    expect(math.sum(1, 1)).toBe(2);
});
test('1-1=0', () => {
    expect(math.minus(1, 1)).toBe(0);
});

7.4 package.json

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 就会去找这个文件,执行里面的命令
  • 这个文件采用 YAML 格式。下面是一个最简单的 Node 项目的.travis.yml文件
language: node_js
node_js:
  - "11"
install: npm install
script:  npm test  

8.3 实战

8.3.1 生成项目并上传github

npx create-react-app zhufeng-typescript-development

8.3.2 同步仓库

8.3.3 设置仓库环境变量

变量名 含义
GH_TOKEN 用户生成的令牌
GH_REF 仓库地址 github.com/zhufengnodejs/zhufeng_typescript_development.git

8.3.4 Github生成访问令牌 (即添加授权)

  • 访问令牌的作用就是授权仓库操作权限
  • Github>settings>Personal access tokens> Generate new token > Generate token> Copy Token

8.3.5 .travis.yml

language: node_js
node_js: 
    - '11'
install:
  - npm install
script:
  - hexo g
after_script:
  - cd ./public
  - git init
  - git config user.name "${USERNAME}"
  - git config user.email "${UESREMAIL}"
  - git add -A
  - git commit -m "Update documents"
  - git push --force  "https://${GH_TOKEN}@${GH_REF}" "master:${GH_BRANCH}"
branches:
  only:
    - master

9.React元素

DetailedReactHTMLElements.jpg

8.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;

8.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;

8.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;

参考

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容