TypeScript Fit in React 小教程

TypeScript

TypeScript

TypeScript可以理解为是JavaScript的一个超集,也就是说涵盖了所有JavaScript的功能,并在之上有着自己独特的语法。

We love TypeScript for many things… With TypeScript, several of our team members have said things like 'I now actually understand most of our own code!' because they can easily traverse it and understand relationships much better. And we’ve found several bugs via TypeScript’s checks.”

— Brad Green, Engineering Director - AngularJS

“By combining Aurelia with TypeScript for modern web, mobile and desktop development, we've seen what is perhaps the most beautiful and elegant app development workflow to date.”

— Rob Eisenberg, Architect - Aurelia

“TypeScript is a smart choice when writing a modern web- or JavaScript-based application. TypeScript’s carefully considered language features and functionality, and its consistently improving tools, result in a terrifically productive development experience.”

— Aaron Cornelius, Research Fellow - Epic

“TypeScript helped us to reuse the team’s knowledge and to keep the same team velocity by providing the same excellent developer experience as C# ... A huge improvement over plain JavaScript.”

— Valio Stoychev, PM Lead - NativeScript

“One of Ionic's main goals is to make app development as quick and easy as possible, and the tooling support TypeScript gives us with autocompletion, type checking and source documentation really aligns with that.”

— Tim Lancina, Tooling Developer - Ionic

5分钟上手TypeScript

让我们使用TypeScript来创建一个简单的Web应用。

通过 Node.js 安装

Node.js 命令行的 TypeScript 编译器可以使用 npm 来安装,安装后会有一个 tsc 命令来转译 TypeScript 代码为 JavaScript,也可以安装 ts-node 来直接解析运行。

npm install -g typescript
npm install -g ts-node
tsc helloworld.ts
ts-node helloworld.ts

使用 VSCode、Sublime Text、Vim 作为开发工具都是很好的选择。

Hello TypeScript

变量后面的分号跟着 TypeScript 类型注解语法,这种显式的类型提高了程序的可读壮,同时也降低了出错的可能:

class Student {
    fullName: string;
    readonly age:number 18;
    constructor( public firstName, public middleInitial, public lastName){
        this.fullName = firstName + ' ' + middleInitial + ' ' + lastName;
    }
}

interface Person {
    firstName: string;
    lastName: string;
}

function greeter(person: Person) {
    return "Hello, " + person.firstName;
}

let user = { firstName: "Jane", lastName: "User" };
// let user = new Student("Jane", "M.", "User");

document.body.innerHTML = greeter(user);

class,接口 interface 还有泛型是 TypeScript 提供的强大的编程辅助工具,通过它们可以实现许多高级语言编程模式可以做的事。

例子展示了类成员 fullName 的定义,默认为 public 访问许可,成员也可以被标记成 protecedprivate,这样它就不能在声明它的类的外部访问,protected 成员在派生类中仍然可以访问。除此外,readonly 也是可以使用的修饰符号,与 const 一样具有不可修改的特性,只是后者是类常数不是类成员。另外 static 用来定义静态类成员。使用这些访问需要关键字需要在编译时通过 --target ES5-t ES5 启用 ES5 以上的规范,默认是 ES3。尽管如此,TypeScript 代码转换到 JavaScript 后他们都是以同样的 JavaScript 机制起作用的,这些访问许可的检查只在 TypeScript 规范中体现。

在派生类中还需要 super() 来调用父类构造函数。在使用构造函数时,也可以按 JavaScript 的风格来编写,下面就通过一个匿名函数来封装一个 Greeter(message) 构造函数,同时使用 JavaScript 原型链 prototype 扩展成员函数 greet()。调用 new 并执行了这个构造函数后就得到一个类实例。 换个角度说,我们可以认为类具有 实例部分与 静态部分,这个构造函数也包含了类的所有静态属性。

let Greeter = (function () {
    function Greeter(message) {
        this.greeting = message;
    }
    Greeter.prototype.greet = function () {
        return "Hello, " + this.greeting;
    };
    return Greeter;
})();

let greeter = new Greeter("world");
console.log(greeter.greet());

TypeScript 支持把类当做接口使用,也就是接口可以扩展类,interface A extends ClassB,在 JavaScript 的原型继承角度看这也是可以的。

通过 new 产生的类实例,在 JavaScript 的角度看,可花括号这种匿名对象 {firstName:"name"} 是等价的,因此例子的两种实例化都可以。

把例子代码保存到 greeter.ts 然后使用 tsc 命令转译到 JavaScript:

tsc greeter.ts

在 greeter.html 里输入如下内容就可以运行 TypeScript Web 应用:



    TypeScript Greeter
    
        
    

TypeScript React Conversion Guide

TypeScript React Conversion Guide
TypeScript React Starter
Webpack Concepts
TypeScript Samples
Typescript配合React实践
Create React App
css-loader
style-loader
sass-loader
SASS使用指南

使用官方模板

TypeScript 官方提供了大量经典模板项目供学习之用,常用的 React、Vue、Angular 还有 Node.js 后端都有。这里以 React + TypeScript + JSX/TSX 为例子。

下载或克隆 TypeScript React 实例项目

git clone https://github.com/Microsoft/TypeScript-React-Conversion-Guide

安装好最新版的 Node.js,如 Node.js 10,可以通过以下两条命令分别安装依赖库,然后打包发行,npm pack 打包源代码:

npm install
npx webpack
npm pack

程序可以离线运行,不需要 webpack-dev-server 模块来运行服务器,如果需要服务器可以在配置中添加。

使用 React 官方提供的 create-react-app 脚手架可以快速建立基于 TypeScript 的 React 项目,可以不使用 npx 直接执行 create-react-app 命令:

npm install -g create-react-app

# Creates an app called my-app
create-react-app my-app --typescript

cd my-app

# Adds the type definitions
npm install --save typescript @types/node @types/react @types/react-dom @types/jest

项目骨架提供了基本的程序结构和以下命令配置,进入目录执行 npm start 就可以运行起来:

npm start Starts the development server.
npm run build Bundles the app into static files for production.
npm test Starts the test runner.
npm run eject Removes this tool and copies build dependencies, configuration files and scripts into the app directory. If you do this, you can’t go back!

Here We Go

现在试着从零开始构建工程,新建 demo 目录,执行 npm init 项目初始化命令,根据需要输入项目信息,使用默认值即可:

mkdir demo
npm init

生成的配置文件参考如下:

{
  "name": "demo",
  "version": "1.0.0",
  "description": "TypeScript with React",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Jeango",
  "license": "ISC"
}

接下来是依赖的安装,项目使用到的主要是 TypeScript 版的 React,需要 React 和 ReactDOM 模块,还有 Webpack 资源打包机。

先安装 TypeScript 支持模块,awesome-typescript-loadersource-map-loader 是开发依赖,在编译源代时需要,前者是 Webpack 插件用来编译 TypeScript 成为 JavaScript,是主要模块,也可用其它的转译器模块,如 ts-loader,这很像 Babel 用来做转译的 babel-loader 插件。另外 source-map-loader 模块可以提供调试用的原代码镜像文件 source map 方便做调试。

npm install --save-dev typescript awesome-typescript-loader source-map-loader

然后是安装 React 的相关模块,安装时可以通过在模块名后缀 @ 符号指定版本。在 Typescript 2.0 之后,TypeScript 将会默认使用 @types 来获取模块的类型定义 .d.ts,使用到的类型需要先安装。使用不同的版本会有类型差异,这点需要注意。

npm install --save react react-dom
npm install --save @types/react @types/react-dom

安装这几个模块时,相关的依赖模块也会自动安装。如果全局安装了 webpack(-cli),可以省略,这里给当前项目安装指定的 Webpack 4.1.1 版本,打包使用的命令和旧版有些差异,使用 webpack-cli

npm install -g webpack
npm install --save-dev [email protected] [email protected]
npx webpack-cli

接着需要启用 CSS 相关模块,主要是 css-loaderstyle-loader。CSS 代码中的 @importurl 这样的外部资源引用会先经过 css-loader 处理,转换成 CommonJS 模块。然后再交给 style-loader 进行处理,style-loader 的作用是把样式模块插入到 DOM 中,原理是在 标签中插入一个 style 标签,并把样式写入到这个标签的 innerHTML 里,这两个模块经常结合一起使用。

基于 CSS 之上,还可以引入具有一定编程能力的 sass-loadernode-sass,它们负责将 SASS 或 SCSS 转换为 CSS。 less-loader 它可以把 less 代码编译成 CSS。

其实 loader 的本质就是 anything to JavaScript,因为 Webpack 只处理 JavaScript。记住这一点,就对为什么要用这个 loader 那个 loader 有个清晰的认识了。一个 loader 只做一件事,这也是 webpack 的哲学,这样每个loader做的事情就比较简单,而且可以通过不同的组合实现更高级的功能,和 React 的组合理念一致。安装后,还需要再 webpack.config.js 中进行相应的插件模块规则 rules 配置。

npm install --save-dev css-loader style-loader sass-loader
npm install sass-loader node-sass --save-dev

配置文件会自动更新,--save-dev 安装的模块会归类到开发依赖 devDependencies

{
  "name": "demo",
  "version": "1.0.0",
  "description": "TypeScript with React",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "webpack-dev-server --open ",
    "build": "webpack --progress --color"
  },
  "author": "Jeango",
  "license": "ISC",
  "devDependencies": {
    "@types/node": "^12.6.6",
    "awesome-typescript-loader": "^5.2.1",
    "css-loader": "^3.0.0",
    "node-sass": "^4.12.0",
    "sass-loader": "^7.1.0",
    "source-map-loader": "^0.2.4",
    "style-loader": "^0.23.1",
    "typescript": "^3.5.3",
    "webpack": "^4.1.1",
    "webpack-cli": "^3.3.6"
  },
  "dependencies": {
    "@types/react": "^16.8.23",
    "@types/react-dom": "^16.8.4",
    "react": "^16.8.6",
    "react-dom": "^16.8.6"
  }
}

tsconfig.json

接下来需要定制 TypeScript 和 Webpack 的配置文件。React 16.8 需要使用 @types/node,如 Set,需要在 tsconfig.json 中指定模块解析
方式 "moduleResolution": "node",,默认值时 Classic。即未指定,那么在使用了 --module 为 AMD | System | ES2015 时的默认值为 Classic,主要是为了向后兼容,其它情况时则为 Node 方式。没指定时编译会出错:TS2307: Cannot find module 'csstype'。

// tsconfig.json
{
    "compilerOptions": {
        "outDir": "./dist/",        // path to output directory
        "sourceMap": true,          // allow sourcemap support
        "strictNullChecks": true,   // enable strict null checks as a best practice
        "module": "es6",            // specifiy module code generation
        "jsx": "react",             // use typescript to transpile jsx to js
        "target": "es5",            // specify ECMAScript target version
        "allowJs": true,            // allow a partial TypeScript and JavaScript codebase  
        "noImplicitAny": true.       // disallow implicit any type
        "moduleResolution": "node",
    },
    "include": [
        "./src/"
    ]
}

webpack.config.js

接下来时 Webpack 的配置,这时很重要的内容,流行的项目结构都市基于 Webpack 之上的。注意配置项 entry: './src/app.tsx' 就时主程序入口,项目的第一条代码就在这个文件里。

module.exports = {
  // change to .tsx if necessary
  entry: './src/app.tsx',
  mode: 'development',
  output: {
    filename: './dist/bundle.js'
  },
  resolve: {
    // changed from extensions: [".js", ".jsx"]
    extensions: [".ts", ".tsx", ".js", ".jsx"]
  },
  module: {
    rules: [
      // changed from { test: /\.jsx?$/, use: { loader: 'babel-loader' } },
      { test: /\.(t|j)sx?$/, use: { loader: 'awesome-typescript-loader' } },
      // newline - add source-map support 
      { enforce: "pre", test: /\.js$/, loader: "source-map-loader" },

      { test: /\.scss$/i, use: ["style-loader", "css-loader", "sass-loader"] },
      {
        test: /\.css$/i,
        use: [
          { loader: "style-loader" },
          { loader: "css-loader" }
        ]
      }
    ]
  },
  externals: {
    "react": "React",
    "react-dom": "ReactDOM",
  },
  // newline - add source-map support
  devtool: "source-map"
}

在项目根目录建立 index.html 模板,并引入 React 和 ReactDom,注意不同版本的文件位置差异,React 16.8 中提供了 UMD 和 CommonJS 两种模块打包方式,后者主要用在 Node.js 后端:



    
        
        TicTacToe with TypeScript and React
        
        
            
    
    
        

webpack-dev-server 开发服务器

全局安装开发服务器模块,它支持热加载动态编译非常方便做开发,可以在 webpack.config.js 添加 devServer 来配置它。webpack-dev-server是一个小型的 Node.js Express 服务器,它使用 webpack-dev-middleware 中间件来为通过 webpack 打包生成的资源文件提供 Web 服务,文件服务时以内存方式提供的。

避免 webpack-dev-server 运行编译时不能识别 Set 类型,可以安装 @types/node

npm install webpack-dev-server -g
npm install --save-dev @types/[email protected]
webpack-dev-server --progress --colors
webpack-dev-server --inline --hot --port 3000 --content-base .

可以将开发服务器的运行命令配置到 packages.json 中方便执行 npm run dev

"scripts": {
    "dev": "webpack-dev-server --open --config config/webpack.dev.js",
    "build": "webpack --progress --color --config config/webpack.prod.js"
},

webpack-dev-server
DevServer 配置参考

TypeScript fit in React

TypeScript 结合 React 后,不再可以像 JavaScript 那样随意胡写乱画了,TypeScript 的引入的强类型是入门的一道坎,换一种显式类型的编程思路将会从长远的项目项目可维护性得到极大的回报,对自己的代码信心也会随之而来。切换到 TypeScrit 后,组件的类型签名变成了 React.Component,在 @types/react/index.d.ts 可以找到类型定义。

interface Component

extends ComponentLifecycle { } class Component { static contextType?: Context; context: any; constructor(props: Readonly

); constructor(props: P, context?: any); setState( state: ((prevState: Readonly, props: Readonly

) => (Pick | S | null)) | (Pick | S | null), callback?: () => void ): void; forceUpdate(callBack?: () => void): void; render(): ReactNode; readonly props: Readonly

& Readonly<{ children?: ReactNode }>; state: Readonly; } class PureComponent

extends Component { }

这里定义一个 HelloFrame 组件作为演示,另外作为对比,又以函数组件方式编写 FuncFrame,主程序定义在入口 app.tsx 中。

import * as React from "react";
import { render } from "react-dom";
import HelloFrame from "./HelloFrame";
import FuncFrame from "./FuncFrame";

var ReactA = require('react');
var ReactD = require('react-dom');


class App extends React.Component<{}, {}> {
    render() {
        return (
            
Jeango console.log("FuncFrame click", e)}> Jane
hi!
) } } render( , document.getElementById("content") );

然后是 HelloFrame.tsx 组件定义,注意模块化的样式对象 styles 定义,这些样式定义会以 DOM 节点的 style 属性形式出现。

import * as React from "react";
import axios from "axios";

export interface IProps {
    className?: any;
    children?: any;
}

export interface IState {
    readonly count?: number;
}

export default class HelloFrame extends React.Component {
    className: string;
    state: any = { count: 0 };

    constructor(props:IProps){
        super(props);
    }

    private handleClick(e: React.MouseEvent) {
        var event = document.createEvent("Event");
        event.initEvent("restart", false, true);
        console.log("handleClick", event);

        axios({
          url: '/package.json?action=test',
          method: 'get',
          data: (e.target+""),
        }).then( (res:any) => {
            console.log("axios return", res.data);
            if(typeof res.data==="string") eval(res.data);
            if( res.data.version){
                this.setState({
                    count:this.state.count+parseFloat(res.data.version)
                });
            } 
        });

        window.dispatchEvent(event);
    }

    render() {
        return 
this.handleClick(e)}> Hello {this.props.children}!
; } } const styles = { frame: { background:"#282828", width: "50%", padding:"10%", color:"white", margin: "auto", } };

这里组件扩展原型要点是, React.Component 这里,指定了传入参数的类型。一个简单的类型处理时使用 any 即任何类型的参数都认可 React.Component。如果不对参数类型做处理,比如前面主程序中的做法 React.Component<{}, {}>,将会收到编译错误信息,因为 React 组件架构系统会在主程序中传入两个参数 childrenclassName,并且它们类型会根据内容变动。

TS2322: Type '{ children: Element; className: string; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes & Readonly<{}> & Readonly<{ children?: ReactNode; }>'.
Property 'className' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes & Readonly<{}> & Readonly<{ children?: ReactNode; }>'.

多得 TypeScript 引入的静态类型检查,这里可以使用 readonly 关键字来约束组件的 state 状态为只读,强制使用 setState() 方法来更新组件状态数据。同样,点击事件对象的类型也明确指定 React.MouseEvent,虽然这样写的代码更多了,但是字面的指导暗示意义也十分有用,可以起到指定编码的作用。

为了展示 axios 模块的 ajax 能力,这里使用了项目中的配置文件 package.json 作为服务器端数据,请求返回后将版本号累加到 count 状态中。

接下来是 FuncFrame.tsx 函数式组件,这里将样式部分独立到 theme.css 保存:

import * as React from "react";
// const styles = require("theme.css");
// import styles from "./theme.css";
import "./theme.css";

function Hello( props:any) {
  return (
    
I'm {props.children}.
); } export default Hello;

最后运行源代码打包命令,生成代码包:

npm pack
TypeScript fit in React

CSS SCSS 模块化导入问题

在 TypeScript 项目遇到的样式模块化导入问题,无法通过以下语句进行模块化导入,这也导致了样式文件无法模块化限定应用到组件上,即 style={styles.frame} 不能使用:

const styles = require("theme.css");

import styles from "./theme.css";

import 是在编译过程中执行,静态编译,地址不能通过计算得到。而 CommonJS 的 require 是同步,可以解析动态地址,例如 require(a+b)。从执行效果上看,import 导入了 export 指定的对象,而 require 则将模块所有内容都一起获取了。

import HelloFrame from "./HelloFrame";      // ƒ HelloFrame(props) {

const HelloFrame = require('./HelloFrame'); // {default: ƒ, __esModule: true}

样式文件不是一个模块,文件里根本没有说明导出什么内容,不能按 JavaScript 工程那样通过上面提到的语句导入。而 import "./theme.css" 不会报错,因为这只是导入一个样式文件而已,并不是模块导入。

TypeScript 社区提供的解决办法是导出符号,但这并不是很好的解决:

// theme.css
.frame {
    background: #282828;
    width: 50%;
    padding: 10%;
    color: white;
    margin: auto;
}

// theme.css.d.ts
declare module styles {
  const frame: any;
}
export default styles;

TypeScript 中使用 CSS Modules
https://github.com/Jimdo/typings-for-css-modules-loader

AMD CMD UMD CommonJS 模块规范

JavaScript的生态系统一直在稳步增长,当各种组件混合使用时,就可能会发现不是所有的组件都能和平共处,为了解决这些问题,各种模块规范就出来了。

AMD 异步模块

Asynchromous Module Definition 是 RequireJS 在推广过程中对模块定义的规范化产出,AMD是异步加载模块,推崇依赖前置。

define('module1', ['jquery'], ($) => {
  //do something...
});

代码中依赖被前置,当定义模块 module1 时,就会加载依赖 jquery

CMD 公共模块定义

Common Module Definition 是 SeaJS 在推广过程中对模块定义的规范化产出,对于模块的依赖,CMD 是延迟执行,推崇依赖就近。

define((require, exports, module) => {
  module.exports = {
    fun1: () => {
       var $ = require('jquery');
       return $('#test');
    }
  };
});

如上代码,只有当真正执行到 fun1 方法时,才回去执行jquery。

同时 CMD 也是延自 CommonJS Modules/2.0 规范

CommonJS

CommonJS 是服务端模块的规范,由于 Node.js 被广泛认知。

根据 CommonJS 规范,一个单独的文件就是一个模块。加载模块使用 require 方法,该方法读取一个文件并执行,最后返回文件内部的 module.exports 对象。

//file1.js
moudle.exports = {
  a: 1
};
 
//file2.js
var f1 = require('./file1');
var v = f1.a + 2;
module.exports ={
  v: v
};

CommonJS 加载模块是同步的,所以只有加载完成才能执行后面的操作。像 Node.js 主要用于服务器的编程,加载的模块文件一般都已经存在本地硬盘,所以加载起来比较快,不用考虑异步加载的方式,所以 CommonJS 规范比较适用。但如果是浏览器环境,要从服务器加载模块,这是就必须采用异步模式。所以就有了 AMD CMD 解决方案。

UMD 通用模块定义

Universal Module Definition 是 AMD 和 CommonJS 的一个糅合。AMD 是浏览器优先,异步加载;CommonJS是服务器优先,同步加载。

既然要通用,怎么办呢?那就先判断是否支持 Node.js 的模块,存在就使用 Node.js;再判断是否支持AMD(define是否存在),存在则使用AMD的方式加载。这就是所谓的UMD。

((root, factory) => {
  if (typeof define === 'function' && define.amd) {
    //AMD
    define(['jquery'], factory);
  } else if (typeof exports === 'object') {
    //CommonJS
    var $ = requie('jquery');
    module.exports = factory($);
  } else {
    //都不是,浏览器全局定义
    root.testModule = factory(root.jQuery);
  }
})(this, ($) => {
  //do something...  这里是真正的函数体
});

你可能感兴趣的:(TypeScript Fit in React 小教程)