Вот простой пример разработки базового приложения для блога с использованием NextJs 9 и Serverless Framework и его развертывания на AWS lambda, API Gateway и Zeit. Я использовал Amazon Dynamo DB в качестве базы данных для выполнения операций CRUD.
Вы можете найти образец репозитория здесь. И попробуйте это проверить.
И если вы хотите практиковаться и иметь представление о разработке приложения в целом. Ниже приведены шаги, которые вам необходимо выполнить:
Используемый стек:
Разработка REST API в бессерверной среде и развертывание в AWS lambda и API Gateway
- Установите бессерверный
npm install -g serverless
- Создать каталог проекта
serverless create -t aws-nodejs -p <app-name>
- создать package.json
npm init
- Настройте учетные данные AWS для развертывания
serverless config credentials --provider aws --key {your access key id} --secret {your secret access key}
- Настройте Dynamo DB в serverless.yml
- Добавьте следующий фрагмент, чтобы создать таблицу BLOG_TABLE в Dynamodb при первом развертывании:
provider: name: aws runtime: nodejs10.x region: ap-south-1 environment: BLOG_TABLE: ${self:service}-${opt:stage, self:provider.stage} iamRoleStatements: - Effect: Allow Action: - dynamodb:Query - dynamodb:PutItem - dynamodb:GetItem - dynamodb:DeleteItem Resource: "arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.BLOG_TABLE}" resources: Resources: BlogsTable: Type: AWS::DynamoDB::Table Properties: TableName: ${self:provider.environment.BLOG_TABLE} AttributeDefinitions: - AttributeName: user_id AttributeType: S - AttributeName: timestamp AttributeType: N - AttributeName: blog_id AttributeType: S KeySchema: - AttributeName: user_id KeyType: HASH - AttributeName: blog_id KeyType: RANGE ProvisionedThroughput: ReadCapacityUnits: 1 WriteCapacityUnits: 1 GlobalSecondaryIndexes: - IndexName: timestamp-index KeySchema: - AttributeName: timestamp KeyType: HASH Projection: ProjectionType: ALL ProvisionedThroughput: ReadCapacityUnits: 1 WriteCapacityUnits: 1
- Добавьте файл конфигурации БД для приложения «config / db_config.js»:
const AWS = require('aws-sdk'); AWS.config.update({ region: 'ap-south-1' }); const dynamodb = new AWS.DynamoDB.DocumentClient(); const tableName = process.env.BLOG_TABLE; module.exports = { dynamodb, tableName };
- Добавьте некоторые общие служебные функции для проверки с использованием заголовков в файл: ‘util / headers.js’
const getUserId = (headers) => { return headers.app_user_id; } const getUserName = (headers) => { return headers.app_user_name; } const getResponseHeaders = () => { return { "Access-Control-Allow-Origin": "*" } } module.exports = { getUserId, getUserName, getResponseHeaders };
- Настройте разрешенные заголовки для запросов в serverless.yml
custom: allowedHeaders: - Accept - Content-Type - Authorization - app_user_id - app_user_name
- Установите несколько пакетов утилит npm: momentJs, uuid
npm install --save moment uuid
- Создайте обработчики лямбда (лямбда-функции CRUD) в handler.js
'use strict'; const db = require('./config/db_config.js'); const util = require('./utils/headers.js'); const moment = require('moment'); const uuid = require('uuid/v4'); module.exports.getBlogs = async event => { try { let user_id = util.getUserId(event.headers); let params = { TableName: db.tableName, KeyConditionExpression: "user_id = :uid", ExpressionAttributeValues: { ":uid": user_id }, ScanIndexForward: false } let data = await db.dynamodb.query(params).promise(); return { statusCode: 200, headers: util.getResponseHeaders(), body: JSON.stringify(data) } } catch (err) { console.log("Error", err); return { statusCode: err.statusCode ? err.statusCode : 500, body: JSON.stringify({ error: err.name ? err.name : "Exception", message: err.message ? err.message : "Unknown Error" }) }; } }; module.exports.getBlog = async event => { try { let blog_id = event.pathParameters.blog_id; let params = { TableName: db.tableName, Key: { user_id: util.getUserId(event.headers), blog_id: blog_id } } let data = await db.dynamodb.get(params).promise(); return { statusCode: 200, headers: util.getResponseHeaders(), body: JSON.stringify(data) } } catch (err) { console.log("Error", err); return { statusCode: err.statusCode ? err.statusCode : 500, body: JSON.stringify({ error: err.name ? err.name : "Exception", message: err.message ? err.message : "Unknown Error" }) }; } }; module.exports.addBlog = async event => { try { let item = JSON.parse(event.body).Item; item.user_id = util.getUserId(event.headers); item.user_name = util.getUserName(event.headers); item.blog_id = item.user_id + ':' + uuid() item.timestamp = moment().unix() let data = await db.dynamodb.put({ TableName: db.tableName, Item: item }).promise(); return { statusCode: 200, headers: util.getResponseHeaders(), body: JSON.stringify(item) } } catch (err) { console.log("Error", err); return { statusCode: err.statusCode ? err.statusCode : 500, body: JSON.stringify({ error: err.name ? err.name : "Exception", message: err.message ? err.message : "Unknown Error" }) }; } }; module.exports.updateBlog = async event => { try { let item = JSON.parse(event.body).Item; item.user_id = util.getUserId(event.headers); item.user_name = util.getUserName(event.headers); let data = await db.dynamodb.put({ TableName: db.tableName, Item: item, ConditionExpression: '#t = :t', ExpressionAttributeNames: { '#t': 'timestamp' }, ExpressionAttributeValues: { ':t': item.timestamp } }).promise(); return { statusCode: 200, headers: util.getResponseHeaders(), body: JSON.stringify(item) } } catch (err) { console.log("Error", err); return { statusCode: err.statusCode ? err.statusCode : 500, body: JSON.stringify({ error: err.name ? err.name : "Exception", message: err.message ? err.message : "Unknown Error" }) }; } }; module.exports.deleteBlog = async event => { try { let blog_id = event.pathParameters.blog_id; let params = { TableName: db.tableName, Key: { user_id: util.getUserId(event.headers), blog_id: blog_id } } let data = await db.dynamodb.delete(params).promise(); return { statusCode: 200, headers: util.getResponseHeaders(), body: JSON.stringify(data) } } catch (err) { console.log("Error", err); return { statusCode: err.statusCode ? err.statusCode : 500, body: JSON.stringify({ error: err.name ? err.name : "Exception", message: err.message ? err.message : "Unknown Error" }) }; } };
- Отображение обработчиков лямбда
- Добавьте следующий фрагмент кода, чтобы сопоставить каждый обработчик лямбда-выражения с типом метода, чтобы во время развертывания он мог создавать маршруты для каждого обработчика лямбда-выражений в шлюзе API.
functions: addBlog: handler: handler.addBlog events: - http: path: blogs method: post cors: origin: '*' headers: ${self:custom.allowedHeaders} getBlog: handler: handler.getBlog events: - http: path: blogs/{blog_id} method: get cors: origin: '*' headers: ${self:custom.allowedHeaders} getBlogs: handler: handler.getBlogs events: - http: path: blogs/ method: get cors: origin: '*' headers: ${self:custom.allowedHeaders} updateBlog: handler: handler.updateBlog events: - http: path: blogs/ method: put cors: origin: '*' headers: ${self:custom.allowedHeaders} deleteBlog: handler: handler.deleteBlog events: - http: path: blogs/{blog_id} method: delete cors: origin: '*' headers: ${self:custom.allowedHeaders}
- Чтобы протестировать API локально, вам необходимо установить пакет serverless-offline.
npm run serverless-offline
а затем запустить
serverless offline
он разместит ваш apis на http: // localhost: 3000 / ‹path›
- Развертывайте API в AWS с помощью
serverless deploy
Разработка FRONT-END с использованием NextJS 9 и его развертывание в Zeit.
- Установите NextJS, React и React DOM
npm install --save next react react-dom
- Установить axios
npm install --save axios
- Создайте каталог в корневой папке проекта как «компоненты» и добавьте компонент заголовка навигации и базовый компонент макета приложения:
header.js
import Link from 'next/link'; const linkStyle = { marginRight: 15 }; const Header = () => ( <div> <Link href="/"> <a style={linkStyle}>Home</a> </Link> <Link href="blogs/create"> <a style={linkStyle}>Create</a> </Link> </div> ); export default Header;
basicLayout.js
import Header from './header.js'; const layoutStyle = { margin: 20, padding: 20, border: '1px solid #DDD' }; const Layout = props => ( <div style={layoutStyle}> <Header /> {props.children} </div> ); export default Layout;
- Создайте каталог в корневой папке проекта как «страницы» и добавьте index.js, который в основном является домашней страницей, со следующим фрагментом, который будет извлекать все блоги и отображаться в виде списка на домашней странице.
Убедитесь, что вы заменили ‹маршрут API› развернутым маршрутом API для получения всех блогов.
import Layout from '../components/basicLayout'; import Link from 'next/link'; import axios from 'axios'; const Index = props => ( <Layout> <h1>Blogs</h1> <ul> {props.blogs.map(blog => ( <li key={blog.blog_id}> <Link href="/blogs/[blog_id]" as={`/blogs/${blog.blog_id}`}> <a>{blog.title}</a> </Link> </li> ))} </ul> </Layout> ); Index.getInitialProps = async function() { const res = await axios.get(<APIroute>, {headers: {"app_user_id":"test", "app_user_name": "test"}}); return { blogs: res.data.Items }; }; export default Index;
- Создать форму блога
import Layout from '../../components/basicLayout'; import { Component } from 'react'; import Router from 'next/router' import axios from 'axios'; class BlogForm extends Component { constructor(props) { super(props); this.state = { title: '', content: '' }; this.submitForm = this.submitForm.bind(this); this.handleChange = this.handleChange.bind(this); } handleChange(key) { return function (e) { var state = {}; state[key] = e.target.value; this.setState(state); }.bind(this); } submitForm(event) { event.preventDefault(); let payload = { "Item": this.state }; axios.post( '<API route>', payload, {headers: {"app_user_id":"test", "app_user_name": "test"}} ).then(response => { Router.push('/'); }); } render() { return ( <Layout> <h1>Create Blog</h1> <form onSubmit={this.submitForm}> <label> Title: <input type="text" value={this.state.title} onChange={this.handleChange('title')}/> </label> <label> Content: <input type="text" value={this.state.content} onChange={this.handleChange('content')}/> </label> <input type="submit" value="Submit" /> </form> </Layout> ); } } export default BlogForm;
- Отдельная страница просмотра блога
import Layout from '../../components/basicLayout'; import axios from 'axios'; import Router from 'next/router'; const Blog = props => ( <Layout> <h1>{props.blog.title}</h1> <p>{props.blog.content}</p> <button onClick={deletePost(props.blog.blog_id)}>Delete</button> </Layout> ); Blog.getInitialProps = async function(context) { const { id } = context.query; const res = await axios.get('<API route>'+ id , {headers: {"app_user_id":"test", "app_user_name": "test"}}); const blog = await res.data.Item; return { blog }; }; const deletePost = function(id) { return function () { axios.delete('<API route>'+ id,{headers: {"app_user_id":"test", "app_user_name": "test"}}) .then(response => { Router.push('/'); }); } } export default Blog;
- Протестируйте представления локально, запустив `npm run dev`, он будет размещать представления на http: // localhost: 3000
- Разверните Views с помощью Zeit прямо сейчас
- Установить сейчас с помощью `npm install -g now`
- Разверните, запустив команду `now` в корневом каталоге