NodeJS 14.x - 原生 AWS Lambda 导入/导出支持。

30

我想利用ES6原生的import/export

我正在使用AWS Lambda内的无服务器容器。

我的Dockerfile看起来像这样:

FROM public.ecr.aws/lambda/nodejs:14

COPY app ./

RUN npm install

CMD [ "app.handler" ]

接下来,我有一个app目录,里面是我的应用程序代码。 app.js的代码如下:

import { success } from './utils/log';

exports.handler = async () => {
  success('lambda invoked');
  const response = 'Hello World';
  return {
    statusCode: 200,
    body: JSON.stringify(response),
    isBase64Encoded: false,
  };
};

从这一行中可以看出,import { success } from './utils/log'; 我正在使用原生的导入。

在我的 package.json 文件中,我指定了以下内容:

  "type": "module"

我需要告诉我的应用程序这是一个模块,我希望能够原生地导入它。如果我不指定这个,我会得到以下结果:

{
    "errorType": "Runtime.UserCodeSyntaxError",
    "errorMessage": "SyntaxError: Cannot use import statement outside a module",
    "stack": [
        "Runtime.UserCodeSyntaxError: SyntaxError: Cannot use import statement outside a module",
        "    at _loadUserApp (/var/runtime/UserFunction.js:98:13)",
        "    at Object.module.exports.load (/var/runtime/UserFunction.js:140:17)",
        "    at Object.<anonymous> (/var/runtime/index.js:43:30)",
        "    at Module._compile (internal/modules/cjs/loader.js:1063:30)",
        "    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)",
        "    at Module.load (internal/modules/cjs/loader.js:928:32)",
        "    at Function.Module._load (internal/modules/cjs/loader.js:769:14)",
        "    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:72:12)",
        "    at internal/main/run_main_module.js:17:47"
    ]
}
所以,我明确指定它,告诉Lambda这是一个模块。但是,尽管我想不出任何原因,我无法让它工作,我看到了这个错误:
{
    "errorType": "Error",
    "errorMessage": "Must use import to load ES Module: /var/task/app.js\nrequire() of ES modules is not supported.\nrequire() of /var/task/app.js from /var/runtime/UserFunction.js is an ES module file as it is a .js file whose nearest parent package.json contains \"type\": \"module\" which defines all .js files in that package scope as ES modules.\nInstead rename app.js to end in .cjs, change the requiring code to use import(), or remove \"type\": \"module\" from /var/task/package.json.\n",
    "code": "ERR_REQUIRE_ESM",
    "stack": [
        "Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: /var/task/app.js",
        "require() of ES modules is not supported.",
        "require() of /var/task/app.js from /var/runtime/UserFunction.js is an ES module file as it is a .js file whose nearest parent package.json contains \"type\": \"module\" which defines all .js files in that package scope as ES modules.",
        "Instead rename app.js to end in .cjs, change the requiring code to use import(), or remove \"type\": \"module\" from /var/task/package.json.",
        "",
        "    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1080:13)",
        "    at Module.load (internal/modules/cjs/loader.js:928:32)",
        "    at Function.Module._load (internal/modules/cjs/loader.js:769:14)",
        "    at Module.require (internal/modules/cjs/loader.js:952:19)",
        "    at require (internal/modules/cjs/helpers.js:88:18)",
        "    at _tryRequire (/var/runtime/UserFunction.js:75:12)",
        "    at _loadUserApp (/var/runtime/UserFunction.js:95:12)",
        "    at Object.module.exports.load (/var/runtime/UserFunction.js:140:17)",
        "    at Object.<anonymous> (/var/runtime/index.js:43:30)",
        "    at Module._compile (internal/modules/cjs/loader.js:1063:30)"
    ]
}

看起来像 /var/runtime/UserFunction.js 正在将我的应用程序处理程序作为一个 require 和一个 module 进行调用。然而,我对 /var/runtime/UserFunction.js 没有任何控制权(我不这么认为?)。在我的 Dockerfile 中,我已经指定了 Node14。我不太清楚我错在了哪里?

我想要做的是运行最新最好的 Node14(例如导入),但没有使用 Babel/转译器“膨胀”我的代码。如果有人能指点我方向,将不胜感激。

4个回答

66
如果有人遇到相同问题,请查看以下来自AWS官方技术支持的内容: “您使用package.json { "type": "module" }的指令是正确的,但当前Lambda Node.js 14运行时不支持ECMAScript模块。” 如果有更多支持信息,我会在此帖子中发布更新。我将保留此问题,以防其他人遇到相同的问题。

1
我也卡在这个点上了。有什么解决方法吗? - sridharraman
1
这个问题没有变通方法,你必须使用TypeScript或Polyfill之类的工具。没有内置的模块使用方式。 - user3180997
2
你可以通过将所有处理程序入口点放在自己的文件夹中,并使用一个“空”的package.json来解决这个问题。 如果你想要在与处理程序相同的文件夹中使用ES模块,你可以使用.mjs扩展名。有一个工作示例,请参见https://github.com/makenew/serverless-nodejs/blob/fa148a9dd210c21736c84014c32f6da151176aba/handlers/todo.js - Evan Sosenko
2
ECMAScript模块现在在Lambda Node.js 14中得到了支持!https://aws.amazon.com/blogs/compute/using-node-js-es-modules-and-top-level-await-in-aws-lambda/ - will Farrell
3
根据 https://twitter.com/coderbyheart/status/1486470882008645634,使用 ESM 时不支持从层中导入模块。 - pfried
显示剩余2条评论

15

1
我真的很难让它工作。有人成功了吗,或者看到了任何完整的代码示例?特别是当我尝试导入AWS SDK v3包时,总是会出现“找不到包”错误,例如import { SNSClient, PublishCommand } from '@aws-sdk/client-sns'; - Farski
@farski 我也遇到了同样的问题,你是否将依赖项部署为层?如果是的话,那可能就是问题所在 https://twitter.com/coderbyheart/status/1486470882008645634(请参见上面 @pfried 的评论) - Marces Engel
是的!我昨天刚刚弄明白了,一直想回来更新我的评论。 - Farski
请注意,当您的处理程序有一个“shadow”文件夹时,可能会出现错误:https://repost.aws/questions/QUG3mryU5bRJ6ieTR-IiowCg/bug-user-function-js-uses-exists-sync - vincent

3
这对我在Lambda Node 14.x上的工作有效 -
在app.js中
exports.lambdaHandler = async (event, context) => {
  const { App } = await import('./lib/app.mjs');
  return new App(event, context);
}

然后在 lib/app.mjs 文件中 -

class App {

  constructor(event, context) {
    return {
      'statusCode': 200,
      'body': JSON.stringify({
        'message': 'hello world'
      })
    }
  }
} 

export {App}

lib/app.mjs 是从哪里来的?我在我的项目中找不到它。 - crthompson
1
你需要创建它。上面的代码是最基本的版本。 - Dan Kantor

2
AWS Lambda不正式支持ESM,但通过以下解决方法可以顺利工作。
本答案是根据Evan SosenkoDan Kantor的回答/评论以及我自己的一些额外想法总结的。这包括对TypeScript项目的更好处理,但其中的某些部分也适用于纯JavaScript项目。
我假设以下内容:
  1. 使用Node.js版本14:FROM public.ecr.aws/lambda/nodejs:14
  2. 使用Serverless容器:FROM public.ecr.aws/lambda/nodejs:14
  3. 本地导入应该无需文件扩展名:import { success } from './utils/log';
  4. 从库中导入应使用ESM语法:import AWS from 'aws-sdk';
  5. lambda函数使用TypeScript编写:(.ts是文件后缀)。
(我还提供了纯.js而不是TypeScript的信息)

Dockerfile

FROM public.ecr.aws/lambda/nodejs:14

# copy only package.json + package-lock.json 
COPY package*.json ./

# install all npm dependencies including dev dependencies
RUN npm install

# copy all files not excluded by .dockerignore of current directory to docker container
COPY .  ./

# build typescript
RUN tsc

# remove npm dev dependencies as they are not needed anymore
RUN npm prune --production

# remove typescript sources
# RUN rm -r src

# rename all .js files to .mjs except for handler.js
RUN find ./dist -type f -name -and -not -name "handler.js" "*.js" -exec sh -c 'mv "$0" "${0%.js}.mjs"' {} \;

# allow local imports without file ending - see: https://nodejs.org/api/esm.html
ENV NODE_OPTIONS="--experimental-specifier-resolution=node"

# set handler function
CMD ["dist/lambda/handler.handler"]

解释

由于AWS Lambda仅支持commonJS,Lambda入口点是一个commonJS文件。这由一个空的package.json指定,该文件覆盖了项目根目录下的package.json。由于此文件为空,因此不包含:"type":"module",并将该文件夹及其子文件夹中的所有文件默认为commonJS。如果commonJS文件具有.mjs扩展名,则可以访问ESM文件,但由于typescript编译为.js,因此我使用一些unix命令在调用tsc后重命名所有匹配".*js"的文件。handler.js必须保持为".js",因此我将其从.mjs重命名回来。
更多关于".js/.mjs"的信息

文件夹结构

- src
- - lambda
- - - handler.ts (commonJS)
- - - package.json (contains only: `{}`)
- - app.ts (ESM)
- - services
- - - (other .ts files ESM)
- package.json (contains `{"type": "module"}`, but also other settings)
- Dockerfile
- tsconfig.json

handler.ts

exports.handler = async (event) => {
    const {App} = await import('../app.mjs');
    const app = new App();
    return await app.run(event);
};

app.ts

// example import library
import AWS from 'aws-sdk';
// example import local file
import {FileService} from './services/file.service';

class App {
  constructor() {
  }

  async run(event) {
     // write your logic here
     return {
       'statusCode': 200,
       'body': JSON.stringify({'message': 'hello world'})
     }
  }
}
export {App};

tsconfig.json

{
  "compilerOptions": {
    "module": "ESNext",
    "moduleResolution": "Node",
    "esModuleInterop": true,
    "declaration": true,
    "removeComments": true,
    "allowSyntheticDefaultImports": true,
    "target": "es6",
    "sourceMap": true,
    "outDir": "./dist",
    "baseUrl": "./src",
    "lib": [
      "es6",
      "dom"
    ]
  },
  "include": [
    "src/**/*"
  ]
}

如果不使用TypeScript的差异

假设您没有使用TypeScript,而是像问题中提到的那样使用JavaScript,那么请进行以下更改:

  • 不要在Dockerfile中使用tsc命令
  • 没有tsconfig.json文件
  • 所有文件的名称应为*.js,而不是*.ts
  • 显然不使用任何TypeScript类型(在我的示例中没有)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接