只是抱着尝试的心态对项目进行了迁移,体验了一番typeScript的强大,当然,习惯了JavaScript的灵活,弱类型,刚用上typeScript时会很不适应,犹如懒散惯了的人被突然箍上各种枷锁,约束。但是,从长远来看,尤其是多人协作的项目,还是很有必要的。
typescript的优点
- 静态代码检查
可以规避一些容易被忽视,隐晦的逻辑或语法错误,帮助我们写更加健壮,安全的代码,如下所示
function getDefaultValue (key, emphasis) {
let ret;
if (key === 'name') {
ret = 'GuangWong';
} else if(key=== 'gender') {
ret = 'Man';
} else if (key === 'age') {
ret = 23;
} else {
throw new Error('Unkown key ');
}
if (emphasis) {
ret = ret.toUpperCase();
}
return ret;
}
getDefaultValue('name'); // GuangWong
getDefaultValue('gender', true) // MAN
getDefaultValue('age', true)
这是一个简单的函数,第一个参数 key 用来获得一个默认值。第二参数 emphasis 为了某些场景下要大写强调,只需要传入 true 即可自动将结果转成大写。
但是如果不小心将 age 的值写成了数字字面量,如果我调用 getDefaultValue('age', true) 就会在运行时报错。这个有可能是业务上线了之后才发生,直接导致业务不可用。
- 提高效率,错误在编写代码时报错,而非编译阶段
如有一种场景,在代码重构迁移模块目录时,一些模块依赖引用路径变更,或者是引用的模块还没安装,不存在时,配合vscode, 及时指出错误,不用等跑一遍编译
这种情况也适用于引用非定义变量等错误
- 增强代码的可读性,可以做到代码即文档。
虽然代码有注释,但是并不是每个人都有良好的习惯
react 组件设计
export interface CouponProps {
coupons: CouponItemModel[];
}
export interface couponState {
page: number,
size: number
}
class CouponContainer extends React.Component<CouponProps, couponState> {
render() {
return (
<div>
{
this.props.coupons.map((item: CouponItemModel) => item.title)
}
</div>
)
}
}
使用 JS 写的 Component,Props 和 State表现的并不明显。使用 Typescript 编写 React 组件,需要为组件定义好 Props 和 State。而这也被证明是个好的编码方式。其可以帮助你构建更健壮的组件,别人经手自己的代码时可以很清楚知道一个组件需要传入哪些参数
- 增强设计
相关实践
实践是消弭困惑最好的方式,抱着好奇,排斥的心态还是对对项目进行了迁徙
- 项目目录介绍
--
如上图所示,项目中所有源码都放在src目录中,src/client
为客户端的源码,src/server
为服务器端的代码,dist目录是编译后的目录
2. typescript In node
2.1.准备阶段
使用npm安装:npm install -g typescript,当前项目使用了是v2.8.3
2.2 tsconfig.json
在项目的根目录下新建立tsconfig.json文件,并编辑相关配置项
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"noImplicitAny": true,
"sourceMap": true,
"lib": ["es6", "dom"],
"outDir": "dist",
"baseUrl": ".",
"jsx": "react",
"paths": {
"*": [
"node_modules/*",
"src/types/*"
]
}
},
"include": [
"src/**/*"
]
}
相关配置解析可参考tsconfig.json
2.3 结合gulp
var gulp = require('gulp');
var pump = require('pump');
var webpack = require('webpack');
var gutil = require('gulp-util');
var webpackDevConfig = require(__dirname + '/webpack.config.dev.js');
var ts = require('gulp-typescript');
var livereload = require('gulp-livereload');
var tsProject = ts.createProject("tsconfig.json");
gulp.task('compile:tsc:server', function () {
return gulp.src('src/server/**/*.ts')
.pipe(tsProject())
.pipe(gulp.dest('dist/server'));
});
gulp.task('compile:tsc:client', function(callback){
webpack(webpackDevConfig, function(err, stats){
if(err) throw new gutil.PluginError("webpack:build-js", err);
gutil.log("[webpack:build-js]", stats.toString({
colors: true
}));
callback();
});
});
//将任务同步执行
var gulpSequence = require('gulp-sequence');
gulp.task('copy:html', function() {
return pump([
gulp.src('./src/views/**/*'),
gulp.dest('./dist/server/views')
])
});
gulp.task('compile', gulpSequence(
'compile:tsc:server',
'compile:tsc:client',
'copy:html'
))
gulp.task('watch', ['compile'], function() {
livereload.listen();
gulp.watch(['./src/server/**/*.ts'], ['compile:tsc:server']);
gulp.watch(['./src/client/**/*.ts'], ['compile:tsc:client']);
gulp.watch(['./src/views/**/*.html'], ['copy:html']);
})
2.4 测试
在src/server/app.ts
下编写代码
/// <reference path="../types/custom.d.ts" />
import * as express from "express";
import * as compression from "compression"; // compresses requests
import * as cookieParser from "cookie-parser";
import * as bodyParser from "body-parser";
import * as path from "path";
import * as favicon from "serve-favicon";
import * as fs from "fs";
global.APP_PATH = __dirname;
const programConfig = require(path.join(global.APP_PATH + "../../../config/index"));
global.config = programConfig.get("config");
const mainRouters = require("./routers/main");
const underscore = require("underscore");
global._ = underscore._;
const app = express();
// parse application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: false }));
// parse application/json
app.use(bodyParser.json());
// protocal
app.use(function(req: express.Request, res: express.Response, next: express.NextFunction) {
app.locals.protocol = req.protocol;
app.locals.host = req.headers.host;
next();
});
// view engine setup
app.set("views", path.join(__dirname, "./views"));
app.set("view engine", "jade");
app.engine("html", require("ejs-mate"));
app.use(compression());
app.use(cookieParser());
// resources
const cacheOptions = {
maxAge : "1d",
};
app.use("/test", express.static(path.join(__dirname, "../public"), cacheOptions));
app.use("/", mainRouters);
const port = process.env.PORT || programConfig.get("config").port;
const server = app.listen(port, function() {
console.log("App is runing");
console.log("server is listening on " + port);
});
module.exports = app;
2.5 遇到的问题
- 动态地为global添加属性
由于js灵活的风格,我们经常动态地为某一对象添加属性,但是typeScript是编译型语言,基本原则是先定义再使用,所以当我们像下面这么引用
global.testName = '哈哈';
便会出现这样的错误
类型“Global”上不存在属性“testName”
解决方法
(1)将global强制转化为any类型
(<any>global).testName = '哈哈'
(2)扩展原有的对象
global.prototy.testName = '哈哈哈'
(3)使用.d.ts文件
declare namespace NodeJS {
export interface Global {
testName: string;
}
}
网上很多方法是直接添加一个.d.ts文件即可,但是亲测无效,需要在引用文件引入该文件,如本项目在app.ts文件中引入了
/// <reference path="../types/custom.d.ts" />
集成单元测试
项目用的测试框架是 jest + enzyme
- 安装 jest
npm i -D jest @types/jest
npm i -D ts-jest
- 安装 enzyme
npm i -D enzyme @types/enzyme
- 配置 jest.config.js 文件
module.exports = {
collectCoverage: true,
moduleFileExtensions: [
'ts',
'js',
'tsx'
],
transform: {
"^.+\\.tsx?$": "ts-jest",
},
testMatch: [
'**/test/**/*.test.(ts|js|tsx)'
],
testEnvironment: 'node'
};
4.编写测试用例 coupon.test.tsx
import * as React from 'react';
import { shallow, configure } from 'enzyme';
import * as Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter()})
test('Jest-React-TypeScript 尝试运行', () => {
const renderer = shallow(<div>hello world</div>)
expect(renderer.text()).toEqual('hello world')
})