javascript 迁移 typescript 实践

只是抱着尝试的心态对项目进行了迁移,体验了一番typeScript的强大,固然,习惯了JavaScript的灵活,弱类型,刚用上typeScript时会很不适应,犹如懒散惯了的人被忽然箍上各类枷锁,约束。可是,从长远来看,尤为是多人协做的项目,仍是颇有必要的。html

typescript的优势

  • 静态代码检查

能够规避一些容易被忽视,隐晦的逻辑或语法错误,帮助咱们写更加健壮,安全的代码,以下所示node

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 便可自动将结果转成大写。react

可是若是不当心将 age 的值写成了数字字面量,若是我调用 getDefaultValue('age', true) 就会在运行时报错。这个有多是业务上线了以后才发生,直接致使业务不可用。webpack

  • 提升效率,错误在编写代码时报错,而非编译阶段

若有一种场景,在代码重构迁移模块目录时,一些模块依赖引用路径变动,或者是引用的模块还没安装,不存在时,配合vscode, 及时指出错误,不用等跑一遍编译es6

clipboard.png

这种状况也适用于引用非定义变量等错误web

- 加强代码的可读性,能够作到代码即文档。typescript

虽然代码有注释,可是并非每一个人都有良好的习惯express

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。而这也被证实是个好的编码方式。其能够帮助你构建更健壮的组件,别人经手本身的代码时能够很清楚知道一个组件须要传入哪些参数

- 加强设计npm

相关实践

实践是消弭困惑最好的方式,抱着好奇,排斥的心态仍是对对项目进行了迁徙json

  1. 项目目录介绍

--

clipboard.png

如上图所示,项目中全部源码都放在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

  1. 安装 jest

npm i -D jest @types/jest
npm i -D ts-jest

  1. 安装 enzyme

npm i -D enzyme @types/enzyme

  1. 配置 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')
})