65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 TypeScript 烹饪 angular.js

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2018年12月26日

CPOL

4分钟阅读

viewsIcon

8405

本文展示了如何将您旧的 angular.js 项目迁移到使用 TypeScript。

引言

TypeScript 因其静态类型提供的优势而越来越受欢迎。但是,一些负责维护 angular.js 项目的开发人员可能会因为社区缺乏将 angular.js 与 TypeScript 一起使用的示例而陷入困境。本文将尝试填补这一空白。

我们的策略是让产品在开发过程的每个阶段都能正常工作。因此,在实际生活中,向 TypeScript 的过渡可以逐步进行,从而不损害开发团队需要实现业务目标。

本文将包含一些参考代码片段,但如果您想更深入地学习该主题,我建议您关注 GitHub 项目,这是我已将其转换为 TypeScript 的现有项目的分支。

环境设置

首先,我们需要安装以下依赖项:

  • typescript
  • gulp-typescript - 以执行相应的 gulp 任务,以及
  • @types/angular - 将为 angular.js 内部添加强类型。

接下来,我们在项目根目录下创建 tsconfig.json 文件,内容如下:

{
  "compilerOptions": {
    "allowJs": true,
    "module": "none",
    "target": "es5",
    "types": [
      "angular"
    ]
  },
  "include": [
    "./src/**/*.ts"
  ]
}

我们将模块系统指定为“none”,因为我们让 angular.js 负责解析模块依赖项,而不是像 webpack 这样的模块解析器。

另外,请注意 types 部分,我们在其中指定了我们的类型定义,例如 @types/angular

Target es5 使我们不必创建涉及 babel.js 的复杂的转译管道。

现在,让我们将一个 gulp 任务添加到现有文件中。

var ts = require('gulp-typescript');
var tsProject = ts.createProject("tsconfig.json");

//Compile all typescript files into javascript
gulp.task('ts-build', function() {
  return gulp.src(['src/**/*.ts'])
  .pipe(tsProject())
  .pipe(gulp.dest("src/"));
});

现在我们可以调用我们的任务,使其与现有任务一起执行。

gulp.task('usemin', ['inject-templates', 'ts-build'], function() {

现在我们已经设置好了环境,可以开始工作了。此外,一切仍然正常工作,随时可以发布。

将指令转换为惯用的 TypeScript

策略是从独立的单元开始翻译,然后根据您已翻译的项目继续翻译其他单元,这样您就可以获得静态类型的好处。您也可以在任意点开始迁移,将所有未翻译的依赖项类型指定为 any,但我认为这会削弱强类型的好处,我建议从作为 angular.js 应用程序基础的指令和服务开始。

对于指令,您只需要将 .js 扩展名重命名为 .ts 即可,但您仍然可以利用 angular.js 类型定义和您定义的类型系统,如下面的指令所示。

class NgEnterDirective implements ng.IDirective {
    public link = (scope : any, element : JQLite, attrs : ng.IAttributes) => {
        element.bind("keydown keypress", (event) => {
            if(event.which === 13) {
                scope.$apply(function(){
                    scope.$eval(attrs.ngEnter);
                });
                event.preventDefault();
            }
        });
    }

    public static Factory(): ng.IDirectiveFactory {
        return () => new NgEnterDirective();
    }
}

angular
    .module('app.core')
    .directive('ngEnter', NgEnterDirective.Factory());

翻译服务

让我们来看一下我们案例研究应用中的 ShowService

class Actor {
    name: string
    character: string
}

class Show {
    id: number
    original_name: string
    cast: Actor[]
    genres: string[]
}

class TvServiceResponse {
    results: Show[]
}

/*
 * Contains a service to communicate with the TRACK TV API
 */
class ShowService {
    static $inject = ["$http", "$log", "moment"]

    constructor(private $http : ng.IHttpService,
        private $log : ng.ILogService,
        private moment : any) {
            return this;
        }

    private API_KEY : string = '87de9079e74c828116acce677f6f255b'
    private BASE_URL : string = 'http://api.themoviedb.org/3'

    private makeRequest = (url : string, params : any) : any => {
        let requestUrl = `${this.BASE_URL}/${url}?api_key=${this.API_KEY}`;
        angular.forEach(params, function(value, key){
            requestUrl = `${requestUrl}&${key}=${value}`;
        });
        return this.$http({
            'url': requestUrl,
            'method': 'GET',
            'headers': {
                'Content-Type': 'application/json'
            },
            'cache': true
        }).then((response) => {
            return response.data;
        }).catch(this.dataServiceError);
    }
    getPremieres = () => {
        //Get first day of the current month
        let date = new Date();
        date.setDate(1);
        return this.makeRequest('discover/tv', 
        {'first_air_date.gte': this.moment(date), append_to_response: 'genres'}).then
          ((data : TvServiceResponse) => {
            return data.results;
        });
    }
    get = (id : number) => {
        return this.makeRequest(`tv/${id}`, {});
    }
    getCast = (id : number) => {
        return this.makeRequest(`tv/${id}/credits`, {});
    }
    search = (query : string) => {
        return this.makeRequest('search/tv', {query: query}).then((data : TvServiceResponse) => {
            return data.results;
        });
    }
    getPopular = () => {
        return this.makeRequest('tv/popular', {}).then((data : TvServiceResponse) => {
            return data.results;
        });
    }

    private dataServiceError = (errorResponse : string) => {
        this.$log.error('XHR Failed for ShowService');
        this.$log.error(errorResponse);
        return errorResponse;
    }
}

angular
    .module('app.services')
    .factory('ShowService', ShowService);

此时,值得一提的不仅是我们如何使用 DTO 来确保我们的程序正常工作,还包括我们如何利用 ES6 的特性,例如 箭头函数字符串模板

这里的技巧是我们已经在 tsconfig.json 中指定的,TypeScript 会将其转译为 ES5。

翻译值提供程序

另一个独立部分的翻译看起来非常简单。

class PageValues {        
    title : string
    description : string
    loading : boolean    
}

angular
    .module('app.core')
    .value('PageValues', PageValues);

翻译控制器

在迁移的这个阶段,我们可以将我们的强类型依赖注入到控制器中,并对它们进行翻译。

这是示例。

class SearchController {
    query: string;
    shows: any[];
    loading: boolean;

    setSearch = () => {
        const query = encodeURI(this.query);
        this.$location.path(`/search/${query}`);
    }
    performSearch = (query : string) => {
        this.loading = true;
        this.ShowService.search(query).then((response : Show[]) => {
            this.shows = response;
            this.loading = false;
        });
    };

    constructor(private $location : ng.ILocationService,
        private $routeParams: any,
        private ShowService: ShowService) {
            PageValues.instance.title = "SEARCH";
            PageValues.instance.description = "Search for your favorite TV shows.";

            this.query = '';
            this.shows = [];
            this.loading = false;

            if (typeof $routeParams.query != "undefined") {
                this.performSearch($routeParams.query);
                this.query = decodeURI($routeParams.query);
            }
        }
}

'use strict';
angular
    .module('app.core')
    .controller('SearchController', SearchController);

使 tsconfig.json 更严格

当应用程序中已经完全使用 TypeScript 时,我们可以使我们的 tsconfig.json 更严格。这样,我们可以应用更高级别的代码正确性检查。

让我们检查一些我们可以添加的有用选项。

{    
    "compilerOptions": {
        "allowJs": true,
        "alwaysStrict": true,                
        "module": "none",
        "noImplicitAny": true,
        "noImplicitThis": true,
        "strictNullChecks": true,
        "strictFunctionTypes": true,
        "target": "es5",
        "types": [
            "angular"
        ]
    },
    "include": [
        "./src/**/*.ts"
    ]
}

离开 angular.js 边界

另一件值得一提的事情是,使用 TypeScript 允许我们构建应用程序逻辑而不依赖于 angular.js 的构造。如果我们想构建一些会被 angular.js 限制的业务逻辑,这可能会很有用,例如,我们想使用 动态多态,但内置的 angular.js 依赖注入限制大于促进。

对于我们的示例,让我们回到值提供程序,它非常简单,但同样可以给您一个关于如何不局限于 angular.js 构造的总体印象。

class PageValues {
    title : string
    description : string
    loading : boolean

    static instance : PageValues = new PageValues();
}

注意我们现在如何使用单例模式和静态实例,并且摆脱了 angular.js 模块的绑定。

现在我们可以从 angular.js 应用程序的任何部分按以下方式调用它。

PageValues.instance.title = "VIEW";
PageValues.instance.description = `Overview, seasons & info for '${show.original_name}'.`;

结论

前端社区被认为是变化最快的社区。这可能会导致应用程序的客户端需要不断地使用最新的、有主见的框架进行重写,以便开发团队仍然可以享受获得前端社区支持的好处。然而,并非所有开发团队,尤其是在大型企业中,都能负担得起这种奢侈,因为需要追逐业务目标。

我的文章旨在帮助这些团队连接到一些现代社区解决方案,而不会大幅牺牲他们的业务目标。

我文章的最新部分还展示了如果您想为前端应用程序架构增加灵活性,您可以多么轻松地摆脱框架的固有观念。

© . All rights reserved.