将 Node.js 项目从普通 ES6 迁移到 TypeScript

Posted

技术标签:

【中文标题】将 Node.js 项目从普通 ES6 迁移到 TypeScript【英文标题】:Migrate Node.js project to TypeScript from plain ES6 【发布时间】:2019-06-05 20:05:33 【问题描述】:

开始。

我做了什么:

npm install -g typescript
npm install @types/node --save-dev

设置tsconfig.json:


     "compilerOptions": 
         "emitDecoratorMetadata": true,
         "experimentalDecorators": true,
         "moduleResolution": "node",
         "module": "commonjs",
         "target": "es6",
         "sourceMap": true,
         "outDir": "dist",
         "allowJs": true,
         "forceConsistentCasingInFileNames": true
     ,
     "exclude": [
         "node_modules",
         "dist",
         "docs"
     ]

将所有文件扩展名从.js 更改为.tsnode_modules 除外):

find . -not \( -path node_modules -prune \) -iname "*.js" -exec bash -c 'mv "$1" "$1%.js".ts' - '' \;

现在运行 tsc 会导致大量错误,例如:

server.ts:13:7 - error TS2451: Cannot redeclare block-scoped variable 'async'.

13 const async = require('async');
     ~~~~~

或者这些:

bootstrap/index.ts:8:7
8 const async = require('async');
        ~~~~~
'async' was also declared here.

更新:

retry 和其他 npm 包也会发生同样的情况:

const retry = require('retry');

require 语句更改为 ES6 import 语句主要解决了这些问题,但一次迁移数千个文件是不可行的,所以我需要一种方法来坚持使用 require 一段时间。这可能吗?

【问题讨论】:

我猜是因为 async/await 都是 typescript 中的两个关键字,所以您不能将变量命名为 async。顺便说一句,在相关说明中,您可能希望完全停用异步库,并将 async/await 与基于 Promise 的构造一起使用。 是的,async/await 绝对是一个目标,但首先我需要在没有错误的情况下编译所有内容,并且必须尽可能少地进行更改。 我更新了 OP:名称 async 本身并不是问题的原因,retry 包和所有其他包也会发生同样的情况,即使是本地模块也是如此。 【参考方案1】:

这是可能的,但您仍然需要编辑这些文件。

这些方法中的任何一种都足够了。

    const ... = require() 替换为import ... = require()

    import async = require('async');
    ...
    

    在文件顶部添加export

    export ;
    const async = require('async');
    ...
    

最初问题的原因是在 TS 中不同的文件是 not 模块,除非它们明确声明为模块,因此它们在相同的全局范围内编译/执行,这就是为什么 tsc 是向您报告 async 变量无法重新声明

来自documentation:

在 TypeScript 中,就像在 ECMAScript 2015 中一样,任何包含*** importexport 的文件都被视为一个模块。相反,没有任何*** importexport 声明的文件被视为其内容在全局范围内可用的脚本(因此也可用于模块)。

【讨论】:

当一个文件有一个import/export 语句时,它被认为是一个模块(而不是一个脚本)。脚本的处理方式与模块不同。在您的情况下,您需要模块。 const async = require('async'); 不认为是导入语句,所以必须添加真正的导入语句。 为什么现有的module.exports 在这里不起作用? @AlexanderZeitler 为什么module.exports 应该在 TypeScript 中工作 @AlexanderZeitler 对不起,我以为你知道。我已经更新了我的答案。【参考方案2】:

这和this one是一样的问题。

为了被视为 ES 模块,文件应包含 importexport 语句,否则 TypeScript 编译器认为变量在全局范围内声明(即使在运行时不是这样) .

解决方法与链接问题相同,添加虚拟export 。这可以通过正则表达式替换批量完成,但如果 CommonJS 、module.exports = ... 导出已在使用中,它们之间可能存在冲突。

使用 CommonJS require() 导入会导致无类型代码。所有主要的库都已经拥有 @types/... 或内置类型。现有的 NPM 包可以与代码库中的正则表达式匹配,以便批量安装相关的@types/... 包,const async = require('async') 等导入可以用import async from 'async' 批量替换。这需要设置esModuleInteropallowSyntheticDefaultImports 选项。

【讨论】:

【参考方案3】:

async 是受保护的关键字。当您使用 async/await 时,您可能会跳过“异步”包。如果您使用 ECMAScript 模块 (ESM) 正确地制作了 ES6+,那么您还将所有文件重命名为 *.mjs,例如 index.mjs。如果您有文件名 index.js,则通常假定它不是 ESM。您必须为所有 ES6 代码添加类型/接口,因此根据您的情况,一次制作所有代码可能不可行,这就是我以 ES2015+ ESM 表示法给出示例的原因。

对于 TypeScript,您应该能够使用 ESM,因为我猜您想要更多最新的符号。为了在顶层使用异步,异步函数就是为此而存在的。 index.mjs 的示例代码,其中包括 ES2015+ 从 ES5/CommonJS *.js 导入,带有 module.exports 和 ESM 导入/导出,最后是动态导入:

import  createRequireFromPath  from 'module'; // ESM import
import  fileURLToPath  from 'url';
const require = createRequireFromPath(fileURLToPath(import.meta.url));
// const untypedAsync = require('async');

class Index 

  constructor() 
    this._server = null;
    this.host = `localhost`;
    this.port = 8080;
  

  set server(value)  this._server = value; 
  get server()  return this._server; 

  async start() 
    const http = await import(`http`); // dynamic import
    this.server = http.createServer(this.handleRequest);
    this.server.on(`error`, (err) => 
        console.error(`start error:`, err);
    );
    this.server.on(`clientError`, (err, socket) => 
        console.error(`start clientError:`, err);
        if (socket.writable) 
            return socket.end(`HTTP/1.1 400 Bad Request\r\n\r\n`);
        
        socket.destroy();
    );
    this.server.on(`connection`, (socket) => 
      const arrival = new Date().toJSON();
      const ip = socket.remoteAddress;
      const port = socket.localPort;
      console.log(`Request from IP-Address $ip and source port $port at $arrival`);
    );
    this.server.listen(this.port, this.host, () => 
      console.log(`http server listening at $this.host:$this.port`);
    );
  

  handleRequest(req, res) 
    console.log(`url:`, req.url);
    res.setHeader(`Content-Type`, `application/json`);
    res.writeHead(200);
    res.end(JSON.stringify( url: req.url ));
  


export default Index; // ESM export
export const randomName = new Index(); // Usage: import  randomName  from './index.mjs';

async function main() 
  const index = new Index();
  const cjs = require(`./otherfile.js`); // ES5/CommonJS import
  console.log(`otherfile:`, cjs);
  // 'async' can be used by using: cjs.untypedAsync
  await index.start();


main();

// in otherfile.js
const untypedAsync = require('async');
const test = 
  url: "url test",
  title: "title test",
;
module.exports =  test, untypedAsync ; // ES5/CommonJS export.

但是,目前将 .mjs 与 typescript 一起使用存在一些问题。请查看仍然开放的相关打字稿问题:.mjs input files 和.mjs output files。您至少应该将 .ts 转换为 .mjs 以解决您的问题。脚本可能看起来像 (es6 to ts source):

// in package.json
"files": [ "dist" ],
"main": "dist/index",
"types": "dist/index.d.ts",
"scripts": 
    "mjs": "tsc -d && mv dist/index.js dist/index.mjs",
    "cjs": "tsc -m commonjs",
    "start": "node --no-warnings --experimental-modules ./dist/index.mjs"
    "build": "npm run mjs && npm run cjs"
,
"devDependencies": 
    "typescript": "^3.2.2"


// in tsconfig.json
"compilerOptions": 
    "module": "es2015",
    "target": "ES2017",
    "rootDir": "src",
    "outDir": "dist",
    "sourceMap": false,
    "strict": true

【讨论】:

【参考方案4】:

由于您要将大型项目迁移到 TypeScript,因此我建议您使用像这个包 (https://www.npmjs.com/package/javascript-to-typescript) 这样的工具,它可以自动完成一些工作。

您可以编写一个脚本来打开项目中的每个文件,并按照@Styx 在他的回答中的建议在顶部添加export

【讨论】:

以上是关于将 Node.js 项目从普通 ES6 迁移到 TypeScript的主要内容,如果未能解决你的问题,请参考以下文章

ES6模块,Node.js和Michael Jackson Solution

将 Node JS 代码迁移到 Apollo 服务器

Node JS - json 到 jsongraph 转换器

KoaHub.js:使用ES6/7特性开发Node.js框架

将 React ES6 迁移到 TypeScript:import 语句不起作用

Node.js资讯 | ES6 下的函数式:递归模式;JavaScript 深拷贝性能分析