Category: Backend

27
Ноя
2022

Как вывести значение по id из базы данных django

мне нужно, чтобы при нажатии на кнопку найти у меня открывался пдф файл, по id из бд(id вводится по input) попробовал сделать через js но не работает, подскажите как сделать?

let a
btn.onclick = function(e) {
e.preventDefault();

27
Ноя
2022

Django – отображать количество пользователей смотрящих трансляцию на данный момент

Я разрабатываю сайт на Django для сервиса с трансляциями спортивных игр. Нужно выводить количество пользователей которые на данный момент смотрят трансляцию. Просматривать трансляцию можно только после авторизации.
Кто-то может подсказать …

27
Ноя
2022

Стриминг видео на сайте

Нужно сделать классический совместный просмотр видео на сайте (видео готовое, хранится на сервере).
Нужна синхронизация по времени, пауза, перемотка.
Интересует с помощью каких технологий это делается (пока что моя идея это отправлять гото…

24
Ноя
2022

⚡ Как занять первое место в поисковой выдаче: добавляем SSR в Vue 3 + Vite приложение

При разработке на реактивных фреймворках многие забывают о том, что итоговое приложение – это что-то ближе к SPA, а не классический сайт как «на Wordpress». И когда дело доходит до SEO-продвижения, многие хватаются за голову, потому что поисковые системы плохо работают с такими ресурсами. Поэтому давайте сегодня познакомимся с технологией SSR, которая решит данную проблему.

Что такое SSR

SSR – (с англ. Server Side Rendering) технология, позволяющая выполнять на сервере JavaScript код для достижения каких-либо целей.

Зачем нужен SSR и что такое SEO

SSR в первую очередь необходим для продвижения сайта в интернете. Есть такое направление в маркетинге как SEO. И чаще всего, SSR необходим именно для этого.

SEO – (с англ. Search Engine Optimization) это оптимизация сайта под нужды поисковой системы. Само по себе, SEO продвижение – это целое самостоятельное направление в маркетинге с большой концентрацией капитала бизнеса, поэтому для многих это очень важная тема. Особенно если бизнес генерирует деньги в интернете.

Видите ли, когда поисковый робот делает запрос к сайту, сделанному на реактивных фреймворках по типу: Vue, React, Angular, то он видит примерно это:


Никакого контента, только полупустой HTML. Хотя, если зайти на сайт с точки зрения обычного человека, мы увидим много текста и картинки.

А вот та же самая страница, но с уже включенным SSR:


Как видите, тут контент есть. Что вообще происходит?

Все просто – сайты, сделанные на JavaScript, обычно, инициализируются в реальном времени на стороне клиента. То есть, в браузере у пользователя. Поэтому поисковая система просто не может грамотно считать контент сайта, и значит, дать ему корректное место в поисковой выдаче. Тут вряд ли можно рассчитывать на первое место.

Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека фронтендера»

Автор не прав, поисковые системы индексируют JavaScript сайты…

Раздел для тех, кто где-то видел или читал какие-то новости на этому тему. Да, вы в целом правы. Но есть огромное но

Поисковые системы это делают крайне неохотно, в том же Google можно ждать индексации сайта неделями, а то и месяцами. SEO-специалисты, как представители бизнеса, просто затюкают бедного программиста разными вопросами. Ведь им нужно быстро, здесь и сейчас.

Дьявол кроется в деталях. Чтобы поисковой системе проиндексировать JavaScript-сайт, ей нужны большие мощности. Сначала нужно сделать запрос к сайту, понять, что тут нет контента и это JavaScript-сайт. После этого надо выкачать сайт, куда-то сложить, запустить исполнительную среду JavaScript и только потом считать контент.

А теперь представьте классический сайт на PHP, C#, Python. Сделал запрос – получил контент. Все.

Как рендерить JavaScript на сервере

С помощью Node.js. Не любите Node.js? Извините, других способов у нас для вас нет.

Хотя внутри Node.js за исполнение JavaScript отвечает движок V8, можете его скачать с GitHub и засунуть в свой проект. Только учтите: V8 написан на С++. Как вы свяжите между собой кучу инструментов, мы представляем лишь примерно, но точно можем сказать что вам будет очень «весело».

Технически, возможно добавить SSR и в Laravel + Vue проект (помним, Laravel это PHP), но это будет выглядеть как-то так. Сомнительный монолит получится. Да и вам все равно потребуется Node.js, как ни крути. Так что, будем работать с Node.js.

Добавляем SSR во Vue приложение

Перед тем как начать, мы с вами сейчас создадим простое двухстраничное Vue-приложение. Это нужно лишь для того, чтобы вы поняли принцип рендеринга контента. Можете взять свое, но лучше давайте начнем вместе с простой базы, так вы сделаете меньше ошибок и будет понятно, что за что отвечает. А иначе, вопросов будет просто миллион.

Создаем Vue приложение

Примечание
Автор использует Node.js 16.15.0.

Инициализируем Vue приложение с помощью команды:

        npm init [email protected]
    

Далее нам зададут некоторые вопросы, отвечаем на них:

  1. Project name. Пишите любое.
  2. Add TypeScript? Нет.
  3. Add JSX Support? Нет.
  4. Add Vue Router for Single Page Application development? Обязательно да.
  5. Add Pinia for state management? Нет, если надо, позже сами добавите.
  6. Add Vitest for Unit Testing? Нет.
  7. Add an End-to-End Testing Solution? Нет.
  8. Add ESLint for code quality? Как хотите, автор использует всегда.
  9. Add Prettier for code formatting? Как хотите.

Теперь переходим в папку с проектом, устанавливаем пакеты и запускаем приложение в режиме разработки (команды вводите по порядку):

        cd “ваше название”
npm install
npm run dev
    

У нас с вами появился такой проект, который нужен:


Мы имеем App.vue как шаблон и несколько страниц добавленных через router/index.js: HomeView.vue и AboutView.vue.


Если мы сейчас нажмем в браузере «Посмотреть код страницы», то не увидим никакого текста в нашем базовом приложении:


Хотя в компонентах текст есть:

Компонент TheWelcome.vue, он вызывается внутри HomeView.vue.
Компонент TheWelcome.vue, он вызывается внутри HomeView.vue.

Создаем сервер для рендеринга JavaScript

Для начала идем в package.json и добавляем туда строчку:

        "type": "module",
    

Должно получится как-то так:


Это для того чтобы в Node.js файлах использовать конструкцию import.

Теперь надо создать сервер. Пусть будет Express:

        npm install express
    

В папке src создайте файл server.js со следующим содержимым:

server.js
        // Node.js utility
import path from 'path'
import fs from 'fs'
import { fileURLToPath } from 'url'

// Vite
import { createServer } from 'vite'

// Express
import express from 'express'

// Helpers
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const resolve = (p) => path.resolve(__dirname, p)

const getIndexHTML = async () => {
  const indexHTML = resolve('../index.html')
  const html = await fs.promises.readFile(indexHTML, 'utf-8')
  return html
}

async function start() {
  const manifest = null
  const ssrServer = resolve('./main-server.js')

  const app = express()
  const router = express.Router()

  const vite = await createServer({
    server: { middlewareMode: true },
    appType: 'custom'
  })

  app.use(vite.middlewares)

  // Ловим все запросы, а вообще можно продублировать тут
  // логику из src/router.js
  router.get('/*', async (req, res, next) => {
    try {
      const url = req.url
      let template = await getIndexHTML()
      template = await vite.transformIndexHtml(url, template)

      let render = (await vite.ssrLoadModule(ssrServer)).render
      
      const [appHtml, preloadLinks] = await render(url, manifest)
      const html = template
        .replace(`<!--preload-links-->`, preloadLinks)
        .replace('<!--app-html-->', appHtml)

      res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
    } catch (e) {
      vite.ssrFixStacktrace(e)
      next(e)
    }
  })

  // Routes
  app.use('/', router)

  app.listen(3000, () => {
    console.log('Сервер запущен')
  })
}

start()
    

Давайте обсудим, что же здесь написано. Это очень важно.

На 22 строке функция start() запускает Express-сервер, предварительно запуская внутри себя Vite-сервер на 29 строке. Сам Vite-сервер – это некое дополнительное приложение, которое умеет компилировать Vue-файлы.

По идее, если через Node.js вызвать файл:

        node index.js
    

в котором будем import файла с расширением .vue, то произойдет ошибка, так как нам нужно заранее предсобрать наше приложение особым способом через Vite (что мы и делаем).

Вообще, вся магия происходит с 40 по 51 строчку. В первую очередь, с помощью функции getIndexHTML(), которую мы чуть выше реализовали. Мы берем наш index.html из корня проекта, для того чтобы через регулярные выражения в нужное место установить отрендеренный контент. Да, нам нужно немного модернизировать index.html. Для этого вставьте под тег title конструкцию:

        <!--preload-links-->
    

И между <div id="app"></div> конструкцию:

        <!--app-html-->
    

Должно выйти так:

index.html
index.html

В preload-links полетят стили и еще всякие полезные ссылки, собираемые Vite. А в app-html, собранное с помощью SSR, – приложение.

Кого-то может смутить пустая переменная manifest. Все так и должно быть. Это не конечный вид файла, и чтобы вас не запутать, мы даем информацию постепенно.

За сам рендер JavaScript отвечает функция vite.ssrLoadModule(). В нее мы передаем путь до нашей специальной версии приложения – entry point для SSR. Да, мы сейчас говорим про файл main-server.js, которого у вас еще нету.

В папке src создайте еще один файл main-server.js с таким содержимым:

 main-server.js
        // Node.js
import { basename } from 'node:path'

// Vue SSR
import { createSSRApp } from 'vue'
import { renderToString } from 'vue/server-renderer'

// App
import App from './App.vue'
import router from './router/index.js'

export async function render(url, manifest = null) {
  const app = createSSRApp(App)
  app.use(router)

  await router.push(url)
  await router.isReady()

  // ctx - context. Плагин @vitejs/plugin-vue
  // https://vitejs.dev/guide/ssr.html#generating-preload-directives
  const ctx = {
    modules: []
  }
  const html = await renderToString(app)
  let preloadLinks = ''
  if (manifest) {
    renderPreloadLinks(ctx.modules, manifest)
  }

  return [html, preloadLinks]
}

function renderPreloadLinks(modules, manifest) {
  let links = ''
  const seen = new Set()
  modules.forEach((id) => {
    const files = manifest[id]
    if (files) {
      files.forEach((file) => {
        if (!seen.has(file)) {
          seen.add(file)
          const filename = basename(file)
          if (manifest[filename]) {
            for (const depFile of manifest[filename]) {
              links += renderPreloadLink(depFile)
              seen.add(depFile)
            }
          }
          links += renderPreloadLink(file)
        }
      })
    }
  })
  return links
}

function renderPreloadLink(file) {
  if (file.endsWith('.js')) {
    return `<link rel="modulepreload" crossorigin href="${file}">`
  } else if (file.endsWith('.css')) {
    return `<link rel="stylesheet" href="${file}">`
  } else if (file.endsWith('.woff')) {
    return ` <link rel="preload" href="${file}" as="font" type="font/woff" crossorigin>`
  } else if (file.endsWith('.woff2')) {
    return ` <link rel="preload" href="${file}" as="font" type="font/woff2" crossorigin>`
  } else if (file.endsWith('.gif')) {
    return ` <link rel="preload" href="${file}" as="image" type="image/gif">`
  } else if (file.endsWith('.jpg') || file.endsWith('.jpeg')) {
    return ` <link rel="preload" href="${file}" as="image" type="image/jpeg">`
  } else if (file.endsWith('.png')) {
    return ` <link rel="preload" href="${file}" as="image" type="image/png">`
  } else {
    return ''
  }
}
    

По документации Vite, функция vite.ssrLoadModule() возвращает другие экспортируемые функции из передаваемого файла. Поэтому внутри main-server.js мы объявляем функцию render() и в ней напишем классический SSR сервер из документации Vue.js.

Сама функция render() будет вызываться из файла server.js.

Внутри main-server.js у многих могут вызывать вопросы две функции: renderPreloadLinks() и renderPreloadLink(). Хотя они и выглядят страшно, но на самом деле выполняют простую роль: они помогают нам и подготавливают ссылки на .css файлы. Все ссылки на чанки стилей будут находиться в манифесте. Мы его просто тут читаем. Понимаем, вопросов много, но пока у нас нет манифеста, мы его сделаем чуть позже, и все станет сразу в разы понятней.

К сожалению, это еще не все (хотя уже финишная прямая). Даже если мы сейчас попытаемся запустить сервер, то ничего хорошего не произойдет. Нам надо еще перенастроить наш router/index.js. Для этого откройте этот файл.

Смотрите на 5 строку, раздел history. Тут используется функция createWebHistory(). Под капотом у этой функции есть использование глобальных переменных document и window. Только вот беда: когда мы будем собирать наше приложение через SSR-мод с помощью Node.js, мы не сможем обратиться к этим переменным. Просто потому, что в Node.js нет их. Вместо window в Node.js есть global и process, но там совсем другое содержимое. А document вообще является DOM API, которого тем более там нет… это же не браузер.

Поэтому мы должны поменять createWebHistory() на сreateMemoryHistory(), но только для SSR, дабы в обычном режиме приложение не сломалось. Поэтому модернизируйте файл router/index.js таким способом:

router/index.js
        import { createRouter, createWebHistory, createMemoryHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'

const baseUrl = import.meta.env.BASE_URL
const history = import.meta.env.SSR ? createMemoryHistory(baseUrl) : createWebHistory(baseUrl)

const router = createRouter({
  history,
  routes: [
    {
      path: '/',
      name: 'home',
      component: HomeView
    },
    {
      path: '/about',
      name: 'about',
      component: () => import('../views/AboutView.vue')
    }
  ]
})

export default router
    

Теперь можете запустить наше творение командой:

        node ./src/server.js
    

Во-первых, сервер запустился под адресом localhost:3000, и если вы перейдете на него и откроете исходный код, то увидите результат своего труда:

Можете проскролить вправо и там будет еще контент из компонентов
Можете проскролить вправо и там будет еще контент из компонентов

Первый заход на сайт будет отдавать контент компонента, на который мы попали. На клиентской стороне реактивность сохраняется за счет гидрации.

Во-вторых, если нажимать F5, то как-то некрасиво встают стили. Мы это исправим за счет манифеста. В режиме разработки мы поработаем и так, а для production сделаем все чуть красивее.

В-третьих, если вы меняете файлы, Vite подхватывает изменения и делает Hot Reload. Ну, кроме файла server.js… тут, если хотите, то же самое – надо поставить nodemon и запускать server.js уже через него. Как-то так:

        npm install -g nodemon
nodemon ./src/server.js
    

P.S: Это по желанию.

Финал: сборка для production

Замените содержимое server.js на новое:

server.js
        // Node.js utility
import path from 'path'
import fs from 'fs'
import { fileURLToPath } from 'url'

// Vite
import { createServer } from 'vite'

// Express
import express from 'express'

// eslint-disable-next-line no-undef
const isProd = process.env.NODE_ENV === 'production'

// Helpers
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const resolve = (p) => path.resolve(__dirname, p)

const getIndexHTML = async () => {
  const indexHTML = isProd
    ? resolve('../dist/client/index.html')
    : resolve('../index.html')
  const html = await fs.promises.readFile(indexHTML, 'utf-8')
  return html
}

async function start() {
  const manifest = isProd 
    ? JSON.parse(fs.readFileSync(resolve('../dist/client/ssr-manifest.json'), 'utf-8'))
    : null

  const ssrServer = isProd
    ? resolve('../dist/server/main-server.js')
    : resolve('./main-server.js')

  const app = express()
  const router = express.Router()

  const vite = await createServer({
    // eslint-disable-next-line no-undef
    root: process.cwd(),
    server: { middlewareMode: true },
    appType: 'custom'
  })

  if (isProd) {
    app.use(express.static('dist/client', { index: false }))
  }

  app.use(vite.middlewares)

  // Ловим все запросы, а вообще можно продублировать тут
  // логику из src/router.js
  router.get('/*', async (req, res, next) => {
    try {
      const url = req.url
      let template = await getIndexHTML()
      template = await vite.transformIndexHtml(url, template)

      let render = (await vite.ssrLoadModule(ssrServer)).render
      
      const [appHtml, preloadLinks] = await render(url, manifest)
      const html = template
        .replace(`<!--preload-links-->`, preloadLinks)
        .replace('<!--app-html-->', appHtml)

      res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
    } catch (e) {
      vite.ssrFixStacktrace(e)
      next(e)
    }
  })

  // Routes
  app.use('/', router)

  app.listen(3000, () => {
    console.log('Сервер запущен')
  })
}

start()
    

Тут не так много правок, как может показаться. В самом вверху мы добавили переменную isProd.

С помощью этой переменной мы будем понимать в каком режиме мы сейчас функционируем. По-хорошему, для production нужно заранее собрать наше приложение через Vite. После сборки оно помещается в папку dist и мы будем просто тянуть файлы оттуда. Посмотрите на строки 20, 28 и 32. Тут как раз у нас еще и манифест появился.

Теперь давайте соберем наше приложение в боевом режиме. Давайте внесем корректировки в package.json:

Добавить в  package.json
        "dev": "node ./src/server.js",
"serve": "NODE_ENV=production node ./src/server.js",
"build": "npm run build:client && npm run build:server",
"build:client": "vite build --ssrManifest --outDir dist/client",
"build:server": "vite build --ssr src/main-server.js --outDir dist/server",

    

Должно получится так:

package.json
package.json

Теперь вместо:

        node ./src/server.js
    

Можно использовать:

        npm run dev
    

А для production есть команда:

        npm run serve
    

Но только перед тем как ее запустить, выполните команду:

        npm run build
    

Ибо без сборки нечего «обслуживать».

В целом на этом все, можем поздравить вас с реализацией своего SSR без всяких фреймворков.

Бонус: альтернативные способы внедрения SSR

Вообще, если лень проходить по этому туториалу и все кажется слишком сложным, то можно рассмотреть готовые инструменты для внедрения SSR.

Например, в рамках Vue 3 существуют такие инструменты как Nuxt и Quasar. Данные инструменты позволяют не создавать всякие Express сервера, а просто работать с Vue, как привыкли. Минусы такого подхода лишь в том, что не вы сами настраиваете Express сервер, а разработчик фреймворка. Поэтому, вы, как программист, придерживаетесь чужой логики (но это не всегда плохо).

***

Итог

Надеемся, этот душный туториал не прошел зря, и вы научились магии SSR в JavaScript. Вот ссылка готового проекта на GitHub. Если будут вопросы, пишите в комментариях, автор постарается помочь.

Материалы по теме

22
Ноя
2022

Связать имя альбома и фотку, которая принадлежит альбому. REACT JS, VK

Есть задание:
В первом табе отображается список фотографий и id (под фоткой случайное число от 1 до 500). Также указывается наименование альбома.
Есть два бэка:

https://jsonplaceholder.typicode.com/albums
https://jsonplaceholder.typicode….

22
Ноя
2022

⛓️ Пошаговая инструкция: создаем свой токен на базе блокчейна TRON

Наверное, многие из вас слышали о таких вещах как USDT, BUSD, WETH. Возможно, кто-то из вас даже покупал эти монеты на криптобиржах. Сегодня мы предлагаем всем желающим окунуться в мир блокчейна и разработки криптоактивов: мы сделаем свою монету, которую можно будет добавить к себе в криптовалютный кошелек.

Начнем издалека… Это необходимо чтобы все понимали, о чем будет идти речь. Рекомендуем читать по порядку иначе, вы можете потерять смысл происходящего.

Что такое блокчейн

Блокчейн – это цепочка блоков. В каждом блоке хранится какая-то информация. Каждый блок – это некое хранилище, которое может содержать в себе какое-то ограниченное количество информации: обычно это транзакции (история операций криптовалюты).

Также в каждом блоке, кроме транзакций криптовалюты, может быть еще дополнительная информация: время создания блока, идентификатор майнера (кто создал блок) и транзакции токенов.

Токены (монеты) есть не во всех блокчейнах, например, в Bitcoin и Litecoin нельзя создавать свои токены. Токены можно создавать только в тех блокчейнах, которые реализовали идею смарт-контрактов (об этом через минуту). Например, Ethereum и TRON.

Что такое криптовалюта, майнинг и стейкинг

Криптовалюта – это внутренняя валюта, некое вознаграждение определенного человека за полезные действия в рамках блокчейна.

Например, когда майнер «майнит» внутри какого-то блокчейна, допустим, в Bitcoin, он просто создает новые блоки в цепочке блоков (блокчейне). То есть, он расширяет сеть. За это он получает вознаграждение в виде криптовалюты. До недавнего времени в Ethereum тоже была такая система, но сейчас криптовалюту дают за другие действия, и майнинг там ушел в небытие. В TRON криптовалюту тоже не майнят, а выдают за стейкинг.

Суть стейкинга в том, чтобы блокировать определенную часть монет в кошельке для получения права напрямую, или через посредников, участвовать в поддержке работоспособности блокчейн платформы. Грубо говоря, вы даете в пользование свою криптовалюту на время, а вам за это вернут больше, чем взяли.

И да, для тех, кто еще не понял, у каждой криптовалюты своя блокчейн-сеть и своя валюта. У Bitcoin – BTC, Ethereum – ETH, TRON – TRX.

Что такое смарт-контракт

Все очень просто. Смарт-контракт – это обычный алгоритм, код. Его часто называют «цифровой договор», поэтому многих людей это вводит в огромное заблуждение.

Термин смарт-контракт был придуман еще в конце 20 века программистом Ником Сабо. В качестве примера можно рассмотреть торговые автоматы. Вы купили кофе в автомате, машина отдала напиток после того, как в нее попали монеты. Под «капотом» у машины есть какой-то код, который проверяет сумму, валидность монет и после ряда успешных проверок отдает напиток. Это и есть смарт-контракт, тот самый «цифровой договор», который обеспечивает выполнение обязательств между человеком и машиной.

Смарт-контракты впервые были добавлены Виталием Бутыриным в блокчейн Ethereum. После этого некоторые современные блокчейны последовали его примеру, например: TRON, Smart Chain, Solana.

Особенность написания смарт-контрактов в том, что сами «цифровые договоры» программируются на языке программирования Solidity. После публикации смарт-контракта изменить его исходный код нельзя. Поэтому его нужно проверять очень дотошно, иначе его могут взломать. Если вносятся изменения в смарт-контракт, то нужно заново публиковать «договор» и заново просить людей «вписаться» в него. За публикацию смарт-контракта блокчейн берет комиссию в криптовалюте. Смарт-контракт публикуется только в одной сети, хотя ничто не мешает вам опубликовать его копию и в другой.

Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека программиста»

Примеры смарт-контрактов

Как первый пример смарт-контракта можно рассмотреть популярный токен USDT. Да, для многих будет сейчас шок, но USDT – это не криптовалюта, а именно токен, созданный на базе блокчейна TRON. Если упрощенно: смысл этого смарт-контракта в том, чтобы выпустить N-ое количество монет после публикации смарт-контракта в блокчейне (мы чуть позже чем-то подобным займемся). А дальше эти монеты попадают на биржи и в кошельки людей, а как… это уже тема для другой статьи.

Если вы пользовались Binance и пытались с него вывести USDT на свой личный кошелек, то, возможно, обратили внимание, что нас просят выбрать сеть для вывода:

Пример выбора сети при переводе средств в другой кошелёк.
Пример выбора сети при переводе средств в другой кошелёк.

Если на данный момент ничего не изменилось, то мы можем выбрать либо сеть Ethereum, либо TRON. А все потому, что копии этого смарт-контракта были опубликованы в обеих сетях. Вот ссылка на смарт-контракт USDT для Ethereum, а вот для TRON. Сайт etherscan и tronscan позволяют просмотреть существующий смарт-контракт и информацию о нем.

Все это позволяет бирже легко конвертировать Tether (USDT) внутри кошелька с одной сети на другую без лишних проблем. В итоге, пользователю с этим получается очень удобно работать.

Ну и если развивать эту тему дальше, можно сказать, что BUSD, Dogecoin, WETH – это все тоже не криптовалюта, это все смарт-контракты. Вообще, половина монет на криптобиржах – это все токены, порожденные смарт-контрактами.

Как второй пример, можно рассмотреть проекты, которые чеканят NFT. Например, тут можно создать свой NFT (в правом верхнем углу, если авторизовались).

Пример маркетплейса с возможностью создать свой NFT токен.
Пример маркетплейса с возможностью создать свой NFT токен.
NFT – это единичный токен, который используют для подтверждения правом владения на какую-то цифровую вещь: картинку, звук и даже код. NFT создают с помощью смарт-контрактов. И да, не во всех блокчейнах, где есть смарт-контракты есть поддержка NFT… так бывает.

Работает все очень просто: вы заходите на сайт, связываете свой кошек и сайт, запускаете смарт-контракт, который создает вам NFT. И естественно, он предварительно забирает у вас за это какую-то комиссию в криптовалюте.

Стоп, стоп, стоп. Какой сайт? Какой запуск смарт-контракта через сайт? Это как? Мы надеемся, что в голове у вас появились именно такие вопросы. И это нормально. Чтобы это понять, пора переходить к Solidity.

Что такое Solidity

Зарываемся еще глубже. Надеемся, пока вам нравится получать новые знания.

Solidity – это полноценный язык программирования, с помощью которого пишется смарт-контракт. Смарт-контракты пишутся в объектно-ориентированном стиле, на классах.

Программист (как юрист) составляет контракт, определяет какие-то условия и критерии для выполнения обязательств с помощью Solidity. Далее смарт-контракт загружается за какую-то комиссию в криптовалюте в блокчейн и начинает автономно существовать в блокчейне.

Принцип работы смарт-контракта легко представить бэкенд-разработчику. Загруженный смарт-контракт в блокчейн – это целое миниприложение, это бекенд, живущий своей жизнью. При написании своего кода на Solidity (уже скоро будем это делать), никто программисту не запрещает объявлять свои функции и переменные. А потом их можно вызывать даже из своего сайта. Посмотрите на скриншот ниже:

Пример связи смарт-контракта на Solidity и Node.js на сервере.
Пример связи смарт-контракта на Solidity и Node.js на сервере.

Программист объявил функцию name() в смарт-контракте. Функция возвращает имя контракта, которое он указал в другом месте. Далее, этот код был загружен в блокчейн. А на картинке справа, с помощью Node.js и специальной библиотеки от разработчиков блокчейна TRON – tronweb, программист соединяется с блокчейном и вызывает функцию name() из смарт-контракта. И да, он получает результат в свой Node.js код прямо из блокчейна. К слову, есть версия и для Python, правда, написанная энтузиастом.

Для справки
Многие блокчейны имеют подобные библиотеки, в основном на Node.js, например, у Ethereum есть библиотека web3. Да, можно найти версии web3 для PHP и Python тоже, просто они появились заметно позже и написаны кем-то на чистом энтузиазме.

Как видите, с помощью таких вещей можно соединять свой смарт-контракт и сайт. В предыдущей главе, где мы рассказывали про примеры смарт-контрактов и обмолвились про NFT, мы как раз про это и говорили.

Никто не мешает сделать сайт, куда пользователь будет загружать свою картинку. Картинка будет превращаться в base64-строку. Этот код мы будем (условно) отправлять в смарт-контракт в блокчейне, передавая аргументом код картинки в base64. В итоге получаем NFT. Монетизацию прикручивайте по желанию – никто вам не мешает в процессе получить за это свою комиссию в криптовалюте. Прямо в Solidity есть нужные для этого методы, просто надо их вызвать в правильном порядке.

Создание своего смарт-контракта и выпуск токена

Наконец-то переходим к самому вкусному. Но перед началом нам все равно нужно кое-что сделать.

Подготовка

Вам нужен браузер, который поддерживает плагины Google Chrome. Еще потребуется обычный офисный компьютер, любой удобный редактор кода и криптокошелек. Хотелось бы сказать любой… но нет, нам потребуется особенный, который поддерживает тестовую сеть – Tronlink. Скачайте его, он нужен лишь для разработки.

Всю работу мы будем проводить в тестовой сети, чтобы не терять реальную криптовалюту, а значит, и реальные деньги. Если что, деплой смарт-контракта в боевую среду почти ничем не отличается от деплоя в тестовую. Разница лишь в финансовых рисках, ну и нет возни с криптокошельком. Там можно будет уже использовать любой, главное, чтобы он поддерживал блокчейн TRON.

Создаем кошелек и получаем криптовалюту

В скачанном и установленном Tronlink нужно создать новый кошелек. Надеемся, тут вы справитесь сами. Как все будет готово, переключите кошелек на работу в тестовой сети Nile:


После этого зайдите на этот сайт и попросите немного тестовой криптовалюты:


Тут хотят, чтобы вы ввели адрес кошелька, его можно скопировать из TronLink (рядом с балансом). Помните, мы находимся в тестовой сети, поэтому все так просто. Криптовалюта тут почти ничего не стоит.

Если по каким-то причинам вы не получили валюту, попробуйте обновить данные в кошельке. Кнопка обновления рядом с шестеренками, напротив изменения сети кошелька.

Стандарты TRC20, TRC721

Через пару мгновений мы зайдем на GitHub и скачаем оттуда «базу» для создания токена. Сразу хотелось бы сказать, что мы будем создавать с вами токен на базе стандарта TRC20. Кратко хотелось бы ввести вас в курс дела, чтобы вы понимали разницу между стандартами.

TRC20 – это чеканка монет на базе самописного смарт-контракта. Что мы начали с вами делать.

TRC721 – это стандарт для создания своего NFT. То есть, если хотите сделать смарт-контракт, который создает NFT, то вам сюда.

Программируем смарт-контракт

Скачиваем базу для создания токена отсюда.

Скачиваем базу смарт-контракта стандарта TRC20.
Скачиваем базу смарт-контракта стандарта TRC20.

Открываем проект в любимом редакторе кода и идем в файл Token.sol. Тут меняем стандартные значения на свои:


  1. Имя токена;
  2. Сокращение, например: BTC – Bitcoin, LTC – Litecoin, TRX – TRON;
  3. Максимальное число после точки с запятой. В нашем случае – 4. Это значит нашу монету можно будет дробить до 0.0001.
  4. Общее количество токенов, которое мы выпустим.

Публикуем смарт-контракт

TRON публикует смарт-контракты через сайт – это его основное отличие от Ethereum. В Ethereum публикация происходит через командную строку. Предварительно вам нужно синхронизировать свой компьютер с блокчейном, скачав килотонны данных о транзакциях: а это, на момент написания статьи, 520 ГБ.

По этой ссылке можно опубликовать свой смарт-контракт, но в боевую сеть. Вам это пока не надо. Поэтому идем сюда, тут располагается тестовая сеть Nile.

Нам надо авторизоваться, а потом загрузить наш код со смарт-контрактом. После нажатия на кнопку Upload Contract File(s), выберите все наши файлы (их 5) и загрузите:


Далее, нажимайте на зеленую кнопку Compile, выбирайте версию Solidity 0.5.10, потом Confirm:


Надо подождать какое-то время.

Если все собралось без ошибок и светится зеленым – можете продолжать. Нажмите Deploy:


У вас должно открыться модальное окно, в котором надо сделать еще некоторые действия:


В выпадающем списке Contract name нужно выбрать главный файл, в нашем случае это Token.sol (тут просто Token). Затем нажмите Confirm.

Через какое-то время вас попросят подтвердить операцию в кошельке, сделайте это. Также с вас возьмут комиссию. На момент написания статьи 4 TRX в тестовой сети.

Когда все будет готово, перейдите в свой профиль:


А там в раздел Record a Token, после этого найдите свой смарт-контракт и нажмите Update Token Information:


У вас откроется страница, где нужно заполнить информацию о токене максимально подробно, иначе у него будет статус Scum и его заблокируют:


На странице можно указать ссылку на Github смарт-контракта, ссылки на сайт и загрузить иконку токена. Когда все сделайте, сохраняйте информацию.

Добавляем токен в кошелек

Теперь вам должны предложить перейти на страницу с выпущенными токенами.

К слову, в кошельке у вас должно появиться уведомление в разделе Asset, рядом с кнопкой плюс. Вы можете нажать туда и добавить свой токен. Если этого не произошло, посмотрите на позапрошлый скриншот и вернитесь на страницу Record a Token. Теперь нажмите на имя токена, и вас перебросит на его страницу со смарт-контрактом.

Тут надо нажать на кнопку, которая автоматически добавит токен в кошелек:


Этой кнопки может не быть, если токен пометили как Scum. Если все получилось, то поздравляем, у вас есть свой токен в кошельке:


В рамках тестовой сети Nile можете отправлять его кому угодно и вообще делайте что хотите. Если загруженная картинка не появилась, не беспокойтесь. Тут просто надо подождать.

***

Итог

Надеемся, это была очень полезная статья и вы узнали много нового о блокчейне и криптовалюте. Узнали, что такое смарт-контракты, поняли их потенциал. Если применить знания, полученные тут в боевой сети TRON, то вы получите уже самую настоящую монету, которую сможете отправить в любой кошелек, который поддерживает сеть TRON.

Естественно, вашу монету никто не допустит до криптобиржи, но не потому, что она сделана не так или просто плохая, а потому что любой монете надо пройти процесс листинга на конкретной бирже. А как вы догадались, процесс этот не из бесплатных и очень дорогой. Но если у вас есть деньги, почему бы нет?

Материалы по теме

19
Ноя
2022

Как задать ForeignKey во view

Как в views.py привязать объявление к полю ForeignKey?
Я пытаюсь это сделать так, но не получается:
AdvertisementImages.objects.create(image=i, advertisement=form.save())

Я перепробовал кучу вариантов и перерыл весь гугл, но ничего не пом…

17
Ноя
2022

Как правильно спроектировать базу данных mySQL?

Как правильно спроектировать базу данных mySQL? Нужно загрузить данный блок
Я собираюсь создать 2 таблицы, в одной заголовок и текст под ним, в другой список. И связать их по id. Будет ли это правильным решением? Или можно как то в одну т…

16
Ноя
2022

Как добавить несколько фото в форме Django?

Мне нужно добавить возможность загрузить несколько фотографий к объявлению.
Я должен был получить форму, где после загрузки изображения, сразу появляется возможность загрузить ещё одно.
По итогу я получаю форму, где я могу загрузить только…

13
Ноя
2022

Микросервисы. Event drive. Получение данных из другого сервиса

Есть микросервис А, он генерит событие которое содержит поля (id,name,age), микросервис B получает это событие и забирает себе только id, остальные данные в контексте сервиса B ненужны. Затем, сервис B генерит событие которое содержит толь…

30
Окт
2022

Laravel модель при выгрузке на фронтенд заменить id на строку с этим id

Метод getDirBySphereId выгружает из бд все данные, где ‘sphere_id’ равен входящему значению. После чего все выгруженные записи должны выводиться во фронтенд. Для удобства вывода во фронтенд хочу сделать, чтоб поле id было не просто числом,…

16
Окт
2022

Проблема в реализации регистрации через почту Django

При реализации регистрации у меня появилась проблема, есть представление для самой регистрации в которой пользователь вводит свои данные и так далее, в этом же представлении отправляется письмо на почту с кодом, потом я возвращаю другое пр…

12
Окт
2022

🐘🗂️ Гибкая ORM для Node.js – Sequelize

Когда начинаешь делать очередной проект, появляется желание упростить себе жизнь и лишний раз не писать SQL-запросы. В таком случае было бы неплохо познакомиться с технологией ORM.

Что такое ORM

ORM – (с англ. ​​Object-Relational Mapping, объектно-реляционное отображение) технология в программировании, которая связывает ваши объекты с базой данных, тем самым создавая виртуальную базу данных. К виртуальной базе данных можно обращаться, извлекая или записывая информацию без написания SQL-запросов.

Что такое Sequelize

Sequelize – это Node.js ORM на базе промисов, которая может работать в связке Postgres, MySQL, MariaDB, SQLite, Microsoft SQL Server, Amazon Redshift.

Sequelize может помочь закрыть 90% нужных задач без написания SQL-запросов. Внутри есть поддержка создания, обновления, удаления сущностей. Есть поддержка вложенных сортировок, сложных условий, LEFT JOIN, лимитов, подзапросов, кастомных запросов, а также есть защита от SQL-инъекций и отмена транзакций.

Установка и настройка

Как базу данных мы будем использовать PostgreSQL, поэтому пример интеграции Sequelize в проект будем показывать на ней. С вас готовый Node.js-сервер (можно с express) и развернутая база данных.

Для начала, установим Sequelize командой:

        npm install sequelize
    

После этого устанавливаем «драйверы» для ORM:

        npm install pg pg-hstore
    

Если вы пожелаете использовать MySQL вместо Postgres, то вам надо установить другие пакеты:

        npm install --save mysql2
    

Подробней про это можно почитать тут.

Подключайте базу данных в основном файле проекта (это может быть app.js).

app.js
        const db = require('./db.js')
db.authenticate()
  .catch(error => console.error(error))
    
db.js
        const Sequilize = require('sequelize')

module.exports = new Sequilize('proglib', 'postgres', 'secret', {
  host: 'localhost',
  dialect: 'postgres',
  operatorsAliases: 0,
  pool: {
    max: 5,
    min: 0,
    acquire: 3000,
    idle: 10000
  }
})
    

После запуска вы должны увидеть в консоли SELECT 1+1 AS result. Это значит, что подключение прошло успешно:


Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека фронтендера»

Методы

У Sequelize есть множество методов, которые позволяют удобно взаимодействовать с базой данных. Но перед тем как начать их рассматривать, нам нужно сделать некоторые подготовительные работы.

Создание таблицы в базе и ORM класса в проекте

Для начала давайте создадим таблицу users в базе данных, а после ORM класс в Node.js для взаимодействия с ней:


Теперь нам нужно создать нужный класс модели в нашем проекте. Для этого создайте папку models и добавьте там файл users.js. Добавьте в файл этот код:

        // Db
const { DataTypes } = require('sequelize')
const db = require('../db.js')

const Users = db.define('users',
  // Описание таблиц
  {
    user_id: {
      type: DataTypes.INTEGER,
      primaryKey: true,
      autoIncrement: true,
      allowNull: false
    },
    firstname: {
      type: DataTypes.STRING,
      allowNull: false
    },
    lastname: {
      type: DataTypes.STRING,
      allowNull: false
    },
    comment: {
      type: DataTypes.TEXT,
      allowNull: true
    },
    order_by: {
      type: DataTypes.INTEGER,
      allowNull: false
    },
    file_id: {
      type: DataTypes.INTEGER,
      allowNull: true
    }
  },
  // Опции
  {
    timestamps: false
  }
)

module.exports = Users
    

Теперь импортируйте в нужное место и используйте по назначению.

Для примера.
Для примера.

Создание элемента

        const Users = require('./models/users.js')

await Users.create({
  firstname: 'Иван',
  lastname: 'Иванов',
  comment: 'Классный парень',
  order_by: 10
})
    

Если какие-то поля в описании модели имеют allowNull: false, и вы попытаетесь создать сущность без них, то фреймворк выдаст ошибку.

Обновление элемента

        const Users = require('./models/users.js')

await Users.update({
  firstname: 'Сергей'
}, {
  where: {
    user_id: 1
  }
})
    

Удаление элемента

        const Users = require('./models/users.js')

await Users.destroy({
  where: {
    user_id: 1
  }
})
    

Найти один элемент

        const Users = require('./models/users.js')

const user = await Users.findOne({
  where: {
    user_id: 1
  }
})
    

Найти много элементов

        const Users = require('./models/users.js')

const user = await Users.findAll({
  where: {
    order_by: 10
  }
})
    

Если хотите указать лимит, то можно добавить атрибуты offset и limit к аргументам объекта:

        const Users = require('./models/users.js')

const user = await Users.findAll({
  where: {
    order_by: 10
  },
  offset: 0,
  limit: 10
})
    

А если хотите получить какие-то конкретные поля (а не все), то достаточно указать аргумент attributes и передать туда массив с нужными полями:

        const Users = require('./models/users.js')

const user = await Users.findAll({
  attributes: ['firstname', 'lastname', 'order_by'],
  where: {
    order_by: 10
  },
  offset: 0,
  limit: 10
})
    

Если хотите все отсортировать, достаточно указать атрибут order и указать, какую сортировку будем делать и по какому полю:

        const Users = require('./models/users.js')

const user = await Users.findAll({
  attributes: ['firstname', 'lastname', 'order_by'],
  where: {
    order_by: 10
  },
  offset: 0,
  limit: 10,
  order: [
    ['order_by', 'ASC']
  ]
})
    

Кроме ASC (по возрастанию) можно указать DESC (по убыванию).

Сложные условия

Для сложных условий существует оператор Op. Он поддерживает множество конструкций, например: and, or, not in, in, like, between, not between, регулярные выражения. Давайте продемонстрируем парочку примеров.

Или order_by равно 10 или user_id равно 1:

        const { Op } = require('sequelize')

const Users = require('./models/users.js')

const user = await Users.findAll({
  where: {
    [Op.or]: {
      order_by: 10,
      user_id: 1
    }
  }
})
    

Все. Но лишь бы не user_id под номером 1:

        const { Op } = require('sequelize')

const Users = require('./models/users.js')

const user = await Users.findAll({
  where: {
    user_id: {
      [Op.notIn]: [1]
    }
  }
})
    

Поиск через iLike:

        const { Op } = require('sequelize')

const Users = require('./models/users.js')

const user = await Users.findAll({
  where: {
    name: {
      [Op.iLike]: `%Иван%`
    }
  }
})
    

В MySQL нет оператора iLike, надо использовать like. Разница лишь в поиске с учетом регистра и без.

Инкремент и декремент

Прибавить 1 к полю order_by:

        const Users = require('./models/users.js')

await Users.increment('order_by', {
  by: 1,
  where: {
    user_id: 1
  }
})
    

Убавить 1 от поля order_by:

        const Users = require('./models/users.js')

await Users.decrement('order_by', {
  by: 1,
  where: {
    user_id: 1
  }
})
    

Кастомные запросы

        const db = require('./db.js')

await db.query('SELECT * FROM users')
    

Можно связать с моделью, сделав свой собственный метод и вызывать его через model.myMethod(). Для этого нужно просто добавить метод в модель таким способом:


Связи

Чтобы делать LEFT JOIN и тянуть данные из связанных таблиц полезно сделать связь. Для этого давайте создадим таблицу files в базе данных с полями file_id, path. И забьем ее данными:

Таблица files в базе данных.
Таблица files в базе данных.

И не забудем добавить file_id к нужным пользователям в таблице users:


Теперь надо добавить связь в ORM класс:


        Users.hasOne(Files, { foreignKey: 'file_id', sourceKey: 'file_id', as: 'file_info' })
Files.belongsTo(Users, { foreignKey: 'file_id', targetKey: 'file_id', as: 'file_info' })
    

Главное – выше не забудьте внутрь одной модели импортировать другую модель.

После этого в нужном месте делаете так:

        const Users = require('./models/users.js')
const Files = require('./models/files.js')

const result = await Users.findOne({
  include: [
    {
      model: Files,
      as: 'file_info'
    }
  ],
  where: {
    user_id: 1
  }
})
    

Результат вас должен приятно удивить:

У автора стоит плагин для Google Chrome JSON Viewer
У автора стоит плагин для Google Chrome JSON Viewer

Можно делать include с «обратной» стороны, если вы сделали belongsTo. Это значит, что можно делать include не только из основного класса, но и дополнительного (с кем связались через belongsTo). В нашем случае из класса Files. Также, кроме hasOne есть еще hasMany для «подгрузки» множества элементов.

Отмена транзакции

        const Users = require('./models/users.js')

const transaction = await Users.sequelize.transaction()

const result = await Users.create({
  firstname: 'Иван',
  lastname: 'Иванов',
  comment: 'Классный парень',
  order_by: 10
}, {
  transaction
})

if (result.user_id > 25) {
  await transaction.rollback()
} else {
  await transaction.commit()
}
    

Показанным способом вы можете отменять транзакции в базе данных. Главное – не забывайте использовать commit() для подтверждения транзакции и rollback() для ее отмены.

***

В этой статье мы рассмотрели потрясающую ORM для Node.js – Sequelize. Мы научились:

  • извлекать данные с различными условиями;
  • устанавливать лимиты;
  • сортировать результат;
  • обновлять и удалять данные;
  • писать свои запросы;
  • отменять транзакции.

Материалы по теме

12
Окт
2022

Конвертер валют JavaScript

Мне нужно сделать конвертер валют, только не с помощью селектора как это делают все другие, а с помощью ul, li.
<div class="convert_block" id="selectsetid">
<input type="number" placeholder="…

12
Окт
2022

Конвертер валют JavaScript

Мне нужно сделать конвертер валют, только не с помощью селектора как это делают все другие, а с помощью ul, li.
<div class="convert_block" id="selectsetid">
<input type="number" placeholder="…

11
Окт
2022

Как сделать автоматическое умножение на нескольких разных <li>

Имею вот такую страницу:

Выбор того что Отправляет и Получает так-же работает:

Я хочу сделать так, что-бы при выборе BTC и RUB то что я вводил в поле BTC умножалось допустим на 10, что-бы при выборе USDT TRC-20 и UAH то что я вводил в по…

07
Окт
2022

🐘 Функции и хранимые процедуры в SQL: зачем нужны и как применять в реальных примерах

Как устроены функции и хранимые процедуры и как их применять для повторного использования запросов.

SQL, как и любой другой язык программирования, предоставляет функции и хранимые процедуры. В этой статье мы рассмотрим функции и хранимые процедуры в PostgreSQL, а также будут освещены следующие моменты:

  • что такое функции и хранимые процедуры;
  • разница между функциями и хранимыми процедурами;
  • создание функций и хранимых процедур;
  • применение функций и хранимых процедур в реальных примерах.

Функции и хранимые процедуры в SQL, как и в любом другом языке программирования, обеспечивают возможность повторного использования и гибкость. Функции и хранимые процедуры представляют собой блок кода или запросов, хранящихся в базе данных, которые можно использовать снова и снова. Вместо того чтобы писать одни и те же запросы, удобнее сгруппировать все запросы и сохранить их, чтобы можно было использовать их много раз. Что касается гибкости, то всякий раз, когда происходит изменение логики запросов, можно передавать новый параметр функциям и хранимым процедурам.

Между функциями и хранимыми процедурами в PostgreSQL есть несколько различий. Они показаны в таблице ниже.

Функции Хранимые процедуры
Функция имеет возвращаемый тип и возвращает значение Хранимая процедура не имеет возвращаемого типа, но имеет выходные аргументы
Использование DML (insert, update, delete) запросов внутри функции невозможно. В функциях разрешены только SELECT-запросы Использование DML-запросов (insert, update, delete) возможно в хранимой процедуре.
Функция не имеет выходных аргументов Хранимая процедура имеет и входные, и выходные аргументы
Вызов хранимой процедуры из функции невозможно Использование или же управление транзакциями возможно в хранимой процедуре
Вызов функции внутри SELECT запросов возможен Вызов хранимой процедуры из SELECT запросов невозможно

Давайте рассмотрим создание функции в PostgreSQL. Следующий блок кода иллюстрирует, как создавать функцию.

        CREATE [or REPLACE] FUNCTION function_name(param_list)
   RETURNS return_type 
   LANGUAGE plpgsql
  as
$$
DECLATE 
-- variable declaration
BEGIN
 -- logic
END;
$$
    

Оператор:

  • create [or replace] function имя_функции — создает или заменяет функцию, если она существует, с заданным именем и параметрами;
  • returns return_type — тип данных, который возвращает функция;
  • язык plpgsql — указывает на процедурное расширение PostgreSQL;
  • внутри знака $ является телом функции;
  • declare — показывает, как объявляются или инициализируются переменные;
  • блок кода [begin — end] — содержит всю логику функции;
  • begin — указывает на начало запросов;
  • end — указывает конец функции.

В следующем блоке кода показано создание простой функции, которая вычисляет стоимость самой дорогой покупки определенного пользователя.

        CREATE OR REPLACE FUNCTION findMostExpensivePurchase(customer_id int)
    RETURNS numeric(10, 2)
    LANGUAGE plpgsql
AS
$$
DECLARE
    itemCost numeric(10, 2);
begin
    SELECT MAX(cost)
    INTO itemCost
    FROM purchases
    WHERE user_id = customer_id;
    RETURN itemCost;
end;
$$;

    

Важно отметить:

  • Declare itemCost integer — объявляем локальную переменную;
  • SELECT max(cost) INTO itemCost — как мы инициализируем переменную itemCost;
  • Возвращает itemCost — возвращает значение функции.

Создание хранимой процедуры, как показано в блоке кода ниже, почти такое же, как создание функции с небольшим отличием — в ней нет return. Остальное почти идентично.

        CREATE [OR REPLACE] PROCEDURE procedure_name(parameter_list)
LANGUAGE language_name
AS $
    stored_procedure_body;
$;
    

В приведенном ниже блоке кода показано создание процедуры — transfer(), которая принимает три параметра. Сразу после имени процедуры передаются аргументы с соответствующими типами данных — sourceAccountId, destinationAccountId, сумма. Процедура вычитает переданную сумму из одного account и добавляет ее к другому account.

        CREATE OR REPLACE PROCEDURE transfer(sourceAccountId bigInt, destinationAccountId bigInt, amount Integer)
language plpgsql
as $$
begin
    update accounts
    set balance = accounts.balance - amount
    where id = sourceAccountId;

    update accounts
    set balance = balance + amount
    where id = destinationAccountId;

    commit;
end;
$$;
    

Наконец, давайте применим все это на реальных примерах. Чтобы запустить весь код, который будет показан ниже, потребуется установить PostgreSQL (версия 13.2) на локальный компьютер или запустить PostgreSQL с помощью Docker-контейнера. Чтобы запустить PostgreSQL в Docker контейнере, необходимо запустить файл docker-compose.yaml, указанный ниже.

        version: "3.8"

services:

  postgresqldb:
    container_name: database
    image: postgres:13.2-alpine
    ports:
      - "5432:5432"
    environment:
      - POSTGRES_DB=customer-service
      - POSTGRES_USER=customer-dev
      - POSTGRES_PASSWORD=1awer321!qwQ
    volumes:
    - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:
    
        > docker-compose up
    

Давайте создадим таблицы users и purchases и заполним их. Для простоты в таблице users есть три столбца — id, name и profession; таблица profession состоит из четырех столбцов — id, name, cost и user_id.

        CREATE table users
(
    id         serial primary key,
    name       varchar(255),
    profession varchar(255)
);

insert into users(name, profession)
values
('Bob', 'QA'),
('Camilo', 'Front End developer'),
('Billy', 'Backend Developer'),
('Alice', 'Mobile Developer'),
('Kate', 'QA'),
('Wayne', 'DevOps'),
('Tim', 'Mobile Developer'),
('Amigos', 'QA');

CREATE TABLE purchases
(
    id      serial primary key ,
    name    varchar(255),
    cost    numeric(10, 2),
    user_id int,
    foreign key (user_id)
        references users (id)
);

insert into purchases(name, cost, user_id)
values
('M1 MacBook Air', 1300.99, 1),
('Iphone 14', 1200.00 , 2),
('Iphon 10', 700.00, 3),
('Iphone 13', 800.00, 1),
('Intel Core i5', 500.00, 4),
('M1 MacBook Pro', 1500, 5),
('IMAC',2500 , 7),
('ASUS VIVOBOOK', 899.99, 6),
('Lenovo', 1232.99, 1),
('Galaxy S21', 999.99, 2),
('XIAMI REDMIBOOK 14', 742.99, 4),
('M1 MacBook Air', 1299.99 , 8),
('ACER', 799.99, 7);
    

После выполнения запросов мы можем проверить нашу базу данных

Получение записей из таблицы <code class="inline-code">users</code>
Получение записей из таблицы users
Получение записей из таблицы purchases
Получение записей из таблицы purchases

Допустим, мы хотим запросить имя пользователя и его самые дорогие покупки. Можно написать несколько сложных запросов, которые вернут желаемый результат, однако мы можем использовать нашу функцию, определенную выше.

Во-первых, нам нужно выполнить сам запрос для создания нашей функции.

        CREATE OR REPLACE FUNCTION findMostExpensivePurchase(customer_id int)
    RETURNS numeric(10, 2)
    LANGUAGE plpgsql
AS
$$
DECLARE
    itemCost numeric(10, 2);
begin
    SELECT MAX(cost)
    INTO itemCost
    FROM purchases
    WHERE user_id = customer_id;
    RETURN itemCost;
end;
$$;
    

Чтобы вызвать функцию — выполните следующую команду:

        SELECT findMostExpensivePurchase(1) as mostExpensivePurchase;
    
Получение самой дорогой покупки пользователя по id = 1
Получение самой дорогой покупки пользователя по id = 1

Чтобы в полной мере воспользоваться функциями, их можно использовать во многих случаях, например, во внутренних запросах select. С функциями запросы становятся намного короче и точнее, это уменьшает шаблонный код и делает запросы лаконичными и простыми.

Получение списка имён пользователей и их самых дорогих покупок
Получение списка имён пользователей и их самых дорогих покупок

Как мы обсуждали ранее, хранимые процедуры немного отличаются от функций. Функции позволяют выполнять только Select-запросы, а хранимые процедуры позволяют выполнять Insert, Update, Delete операции. Хранимые процедуры очень удобны при работе со случаями, когда необходимы операции insert, update или delete.

Рассмотрим банковскую операцию — перевод. При выполнении какой-либо банковской операции деньги переводятся с одного счета на другой. Чтобы реализовать эту хранимую процедуру — transfer(), давайте создадим таблицу accounts и заполним ее.

        create table accounts
(
    id      SERIAL primary key,
    balance  BIGINT,
    user_id INT unique ,
    FOREIGN KEY (user_id)
        references users (id)
);

INSERT INTO accounts(balance, user_id)
values
(1500, 1),
(1100, 2),
(2300, 3),
(7500, 5),
(6500, 4);
    

После выполнения приведенных выше запросов мы получим следующий вывод.

Получение записей из таблицы accounts
Получение записей из таблицы accounts

Для вызова хранимой процедуры используется — call procedure_name().

Сделаем условно перевод денег – 500 единиц с account.id = 3 на account.id = 4 и проверим результат.

Вызов функции transfer() и с последующим выводом записей из таблицы accounts
Вызов функции transfer() и с последующим выводом записей из таблицы accounts

Заключение

В этой статье мы рассмотрели:

  • что такое функции и хранимые процедуры, а также их разницу;
  • создание функции и процедуры;
  • применение функций и процедур на реальных примерах;
***

Материалы по теме

Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека программиста»

07
Окт
2022

🐘 Функции и хранимые процедуры в SQL: зачем нужны и как применять в реальных примерах

Как устроены функции и хранимые процедуры и как их применять для повторного использования запросов.

SQL, как и любой другой язык программирования, предоставляет функции и хранимые процедуры. В этой статье мы рассмотрим функции и хранимые процедуры в PostgreSQL, а также будут освещены следующие моменты:

  • что такое функции и хранимые процедуры;
  • разница между функциями и хранимыми процедурами;
  • создание функций и хранимых процедур;
  • применение функций и хранимых процедур в реальных примерах.

Функции и хранимые процедуры в SQL, как и в любом другом языке программирования, обеспечивают возможность повторного использования и гибкость. Функции и хранимые процедуры представляют собой блок кода или запросов, хранящихся в базе данных, которые можно использовать снова и снова. Вместо того чтобы писать одни и те же запросы, удобнее сгруппировать все запросы и сохранить их, чтобы можно было использовать их много раз. Что касается гибкости, то всякий раз, когда происходит изменение логики запросов, можно передавать новый параметр функциям и хранимым процедурам.

Между функциями и хранимыми процедурами в PostgreSQL есть несколько различий. Они показаны в таблице ниже.

Функции Хранимые процедуры
Функция имеет возвращаемый тип и возвращает значение Хранимая процедура не имеет возвращаемого типа, но имеет выходные аргументы
Использование DML (insert, update, delete) запросов внутри функции невозможно. В функциях разрешены только SELECT-запросы Использование DML-запросов (insert, update, delete) возможно в хранимой процедуре.
Функция не имеет выходных аргументов Хранимая процедура имеет и входные, и выходные аргументы
Вызов хранимой процедуры из функции невозможно Использование или же управление транзакциями возможно в хранимой процедуре
Вызов функции внутри SELECT запросов возможен Вызов хранимой процедуры из SELECT запросов невозможно

Давайте рассмотрим создание функции в PostgreSQL. Следующий блок кода иллюстрирует, как создавать функцию.

        CREATE [or REPLACE] FUNCTION function_name(param_list)
   RETURNS return_type 
   LANGUAGE plpgsql
  as
$$
DECLATE 
-- variable declaration
BEGIN
 -- logic
END;
$$
    

Оператор:

  • create [or replace] function имя_функции — создает или заменяет функцию, если она существует, с заданным именем и параметрами;
  • returns return_type — тип данных, который возвращает функция;
  • язык plpgsql — указывает на процедурное расширение PostgreSQL;
  • внутри знака $ является телом функции;
  • declare — показывает, как объявляются или инициализируются переменные;
  • блок кода [begin — end] — содержит всю логику функции;
  • begin — указывает на начало запросов;
  • end — указывает конец функции.

В следующем блоке кода показано создание простой функции, которая вычисляет стоимость самой дорогой покупки определенного пользователя.

        CREATE OR REPLACE FUNCTION findMostExpensivePurchase(customer_id int)
    RETURNS numeric(10, 2)
    LANGUAGE plpgsql
AS
$$
DECLARE
    itemCost numeric(10, 2);
begin
    SELECT MAX(cost)
    INTO itemCost
    FROM purchases
    WHERE user_id = customer_id;
    RETURN itemCost;
end;
$$;

    

Важно отметить:

  • Declare itemCost integer — объявляем локальную переменную;
  • SELECT max(cost) INTO itemCost — как мы инициализируем переменную itemCost;
  • Возвращает itemCost — возвращает значение функции.

Создание хранимой процедуры, как показано в блоке кода ниже, почти такое же, как создание функции с небольшим отличием — в ней нет return. Остальное почти идентично.

        CREATE [OR REPLACE] PROCEDURE procedure_name(parameter_list)
LANGUAGE language_name
AS $
    stored_procedure_body;
$;
    

В приведенном ниже блоке кода показано создание процедуры — transfer(), которая принимает три параметра. Сразу после имени процедуры передаются аргументы с соответствующими типами данных — sourceAccountId, destinationAccountId, сумма. Процедура вычитает переданную сумму из одного account и добавляет ее к другому account.

        CREATE OR REPLACE PROCEDURE transfer(sourceAccountId bigInt, destinationAccountId bigInt, amount Integer)
language plpgsql
as $$
begin
    update accounts
    set balance = accounts.balance - amount
    where id = sourceAccountId;

    update accounts
    set balance = balance + amount
    where id = destinationAccountId;

    commit;
end;
$$;
    

Наконец, давайте применим все это на реальных примерах. Чтобы запустить весь код, который будет показан ниже, потребуется установить PostgreSQL (версия 13.2) на локальный компьютер или запустить PostgreSQL с помощью Docker-контейнера. Чтобы запустить PostgreSQL в Docker контейнере, необходимо запустить файл docker-compose.yaml, указанный ниже.

        version: "3.8"

services:

  postgresqldb:
    container_name: database
    image: postgres:13.2-alpine
    ports:
      - "5432:5432"
    environment:
      - POSTGRES_DB=customer-service
      - POSTGRES_USER=customer-dev
      - POSTGRES_PASSWORD=1awer321!qwQ
    volumes:
    - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:
    
        > docker-compose up
    

Давайте создадим таблицы users и purchases и заполним их. Для простоты в таблице users есть три столбца — id, name и profession; таблица profession состоит из четырех столбцов — id, name, cost и user_id.

        CREATE table users
(
    id         serial primary key,
    name       varchar(255),
    profession varchar(255)
);

insert into users(name, profession)
values
('Bob', 'QA'),
('Camilo', 'Front End developer'),
('Billy', 'Backend Developer'),
('Alice', 'Mobile Developer'),
('Kate', 'QA'),
('Wayne', 'DevOps'),
('Tim', 'Mobile Developer'),
('Amigos', 'QA');

CREATE TABLE purchases
(
    id      serial primary key ,
    name    varchar(255),
    cost    numeric(10, 2),
    user_id int,
    foreign key (user_id)
        references users (id)
);

insert into purchases(name, cost, user_id)
values
('M1 MacBook Air', 1300.99, 1),
('Iphone 14', 1200.00 , 2),
('Iphon 10', 700.00, 3),
('Iphone 13', 800.00, 1),
('Intel Core i5', 500.00, 4),
('M1 MacBook Pro', 1500, 5),
('IMAC',2500 , 7),
('ASUS VIVOBOOK', 899.99, 6),
('Lenovo', 1232.99, 1),
('Galaxy S21', 999.99, 2),
('XIAMI REDMIBOOK 14', 742.99, 4),
('M1 MacBook Air', 1299.99 , 8),
('ACER', 799.99, 7);
    

После выполнения запросов мы можем проверить нашу базу данных

Получение записей из таблицы <code class="inline-code">users</code>
Получение записей из таблицы users
Получение записей из таблицы purchases
Получение записей из таблицы purchases

Допустим, мы хотим запросить имя пользователя и его самые дорогие покупки. Можно написать несколько сложных запросов, которые вернут желаемый результат, однако мы можем использовать нашу функцию, определенную выше.

Во-первых, нам нужно выполнить сам запрос для создания нашей функции.

        CREATE OR REPLACE FUNCTION findMostExpensivePurchase(customer_id int)
    RETURNS numeric(10, 2)
    LANGUAGE plpgsql
AS
$$
DECLARE
    itemCost numeric(10, 2);
begin
    SELECT MAX(cost)
    INTO itemCost
    FROM purchases
    WHERE user_id = customer_id;
    RETURN itemCost;
end;
$$;
    

Чтобы вызвать функцию — выполните следующую команду:

        SELECT findMostExpensivePurchase(1) as mostExpensivePurchase;
    
Получение самой дорогой покупки пользователя по id = 1
Получение самой дорогой покупки пользователя по id = 1

Чтобы в полной мере воспользоваться функциями, их можно использовать во многих случаях, например, во внутренних запросах select. С функциями запросы становятся намного короче и точнее, это уменьшает шаблонный код и делает запросы лаконичными и простыми.

Получение списка имён пользователей и их самых дорогих покупок
Получение списка имён пользователей и их самых дорогих покупок

Как мы обсуждали ранее, хранимые процедуры немного отличаются от функций. Функции позволяют выполнять только Select-запросы, а хранимые процедуры позволяют выполнять Insert, Update, Delete операции. Хранимые процедуры очень удобны при работе со случаями, когда необходимы операции insert, update или delete.

Рассмотрим банковскую операцию — перевод. При выполнении какой-либо банковской операции деньги переводятся с одного счета на другой. Чтобы реализовать эту хранимую процедуру — transfer(), давайте создадим таблицу accounts и заполним ее.

        create table accounts
(
    id      SERIAL primary key,
    balance  BIGINT,
    user_id INT unique ,
    FOREIGN KEY (user_id)
        references users (id)
);

INSERT INTO accounts(balance, user_id)
values
(1500, 1),
(1100, 2),
(2300, 3),
(7500, 5),
(6500, 4);
    

После выполнения приведенных выше запросов мы получим следующий вывод.

Получение записей из таблицы accounts
Получение записей из таблицы accounts

Для вызова хранимой процедуры используется — call procedure_name().

Сделаем условно перевод денег – 500 единиц с account.id = 3 на account.id = 4 и проверим результат.

Вызов функции transfer() и с последующим выводом записей из таблицы accounts
Вызов функции transfer() и с последующим выводом записей из таблицы accounts

Заключение

В этой статье мы рассмотрели:

  • что такое функции и хранимые процедуры, а также их разницу;
  • создание функции и процедуры;
  • применение функций и процедур на реальных примерах;
***

Материалы по теме

Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека программиста»

05
Окт
2022

🐳🐘 Прочный фундамент для API: Docker + Node.js + Nginx + Postgres

Как бы часто мы ни начинали новые проекты, каждый раз заложить фундамент трудней всего. Поэтому сегодня мы продемонстрируем универсальную сборку для бэкенда на Node.js c Postgres в Docker. И, конечно, обязательно будем отдавать статику через Nginx.

Файловая архитектура и инструменты

Используем Visual Code Studio в качестве редактора кода. Операционная система любая. Обязательно установите Docker.

Для начала создадим вот такую файловую архитектуру:


По мере чтения статьи файлов у нас прибавится. Что мы создали: .env будет содержать переменные среды окружения, в папке app будет контейнер с Node.js, в папке static будет статика, а в Nginx — Nginx-конфигурация. Для начала этого хватит.

Docker и docker-compose.yml

У нас будет два docker-compose.yml: dev и production. Начнем с dev-версии:

docker-compose.dev.yml
        version: '3'

services:
  # Контейнер с Node.js
  app:
    build:
      context: ./app
      target: dev
    tty: true
    working_dir: /opt/server
    volumes:
      - ./app:/opt/server
      - ./static:/opt/static
    env_file:
      - .env
    expose:
      - '3000'
    depends_on:
      - db

  # Контейнер с базой данных
  db:
    image: postgres:12-alpine
    environment:
      - POSTGRES_USER=${DB_USER}
      - POSTGRES_PASSWORD=${DB_PASSWORD}
    volumes:
      - ./postgres:/var/lib/postgresql/data
    expose:
      - '5432'
    restart: always

  # Контейнер с nginx
  nginx:
    container_name: proxy_nginx
    depends_on:
      - app
      - db
      - pgadmin
    image: nginx:latest
    ports:
      - '80:80'
    volumes:
      - ./nginx:/etc/nginx/conf.d
      - ./static:/var/www/static
    restart: always

  # Контейнер с pgadmin
  pgadmin:
    container_name: pgadmin
    depends_on:
      - db
    image: dpage/pgadmin4
    environment:
      PGADMIN_DEFAULT_EMAIL: [email protected]
      PGADMIN_DEFAULT_PASSWORD: qwertyuiop
    expose:
      - '80'
    restart: always
    

Здесь стоит обратить внимание на четыре вещи:

  1. Контейнеры app и nginx связаны со статикой. Для Node.js папка со статикой будет ниже уровнем от app.js (главным файлом, чуть позже создадим), на одном уровне с папкой app.
  2. Контейнер с базой данных содержит ${DB_USER} и ${DB_PASSWORD}. Это переменные из .env-файла, мы его начнем заполнять спустя пару мгновений.
  3. Контейнер с Pgadmin содержит такие строки: PGADMIN_DEFAULT_EMAIL и PGADMIN_DEFAULT_PASSWORD. Вы можете там указать свою почту и свой пароль, он будет использоваться для входа в Pgadmin.
  4. В контейнере с Node.js есть раздел build. Там есть target dev. Просто пока обратите внимание.

Теперь перейдем к production-версии:

docker-compose.production.yml
        version: '3'

services:
  # Контейнер с Node.js
  app:
    build:
      context: ./app
      target: production
    tty: true
    working_dir: /opt/server
    volumes:
      - ./app:/opt/server
      - ./static:/opt/static
      - /opt/server/node_modules/
    env_file:
      - .env
    expose:
      - '3000'
    depends_on:
      - db
    command: npm run start

  # Контейнер с базой данных
  db:
    image: postgres:12-alpine
    environment:
      - POSTGRES_USER=${DB_USER}
      - POSTGRES_PASSWORD=${DB_PASSWORD}
    volumes:
      - ./postgres:/var/lib/postgresql/data
    expose:
      - '5432'
    restart: always

  # Контейнер с nginx
  nginx:
    container_name: proxy_nginx
    depends_on:
      - app
      - db
      - pgadmin
    image: nginx:latest
    ports:
      - '80:80'
    volumes:
      - ./nginx:/etc/nginx/conf.d
      - ./static:/var/www/static
    restart: always

  # Контейнер с pgadmin
  pgadmin:
    container_name: pgadmin
    depends_on:
      - db
    image: dpage/pgadmin4
    environment:
      PGADMIN_DEFAULT_EMAIL: [email protected]
      PGADMIN_DEFAULT_PASSWORD: qwertyuiop
    expose:
      - '80'
    restart: always
    

Здесь стоит отметить, что файлы отличаются между собой только настройками для app контейнера.

Во-первых, мы монтируем папку node_modules внутри контейнера специально, чтобы не было проблем между dev и production версией.

Во-вторых, мы исполняем команду npm run start, которая будет запускать наше приложение (позже ее напишем).

В-третьих, в разделе build у нас другой targetproduction. И вот сейчас мы плавно переходим дальше…

Как Docker поймет, какой файл запускать

Идем в .env файл и вставляем следующее содержимое:

.env
        # dev or production
NODE_ENV=dev

DB_NAME=api
DB_USER=postgres
DB_PASSWORD=secret007
DB_HOST=db

COMPOSE_FILE=docker-compose.${NODE_ENV}.yml
    

В самом начале в переменную NODE_ENV мы записываем в каком режиме мы будем сейчас работать: dev или production. В самом низу мы объединяем COMPOSE_FILE и NODE_ENV. Особо внимательные догадались, что будет происходить в зависимости от содержимого переменной NODE_ENV при команде:

        docker-compose up
    

Будет использоваться тот или иной файл. То есть, если в NODE_ENV указана строка dev, мы активируем файл docker-compose.dev.yml. Если указана строка production, то мы активируем файл docker-compose.production.yml.

Также в файле есть другие переменные для базы данных. Можете поменять их содержимое, если хотите. Главное — помните: DB_HOST должен содержать в себе название контейнера с базой данных из docker-compose.yml.

Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека фронтендера»

Контейнер с Node.js

Пора создавать наше приложение на Node.js. Если подумать, мы провели много подготовительной работы, но это только лишь ⅓ из всего того, что нам еще нужно сделать.

Для начала перейдем в папку app и создадим там файл Dockerfile с таким содержимым:

Dockerfile
        # dev
FROM node:16.10.0-alpine AS dev
RUN apk add --no-cache tzdata
ENV TZ Europe/Moscow
ENV NODE_PATH /opt/server/node_modules

WORKDIR /opt/server/

CMD [ "node" ]

# production
FROM node:16.10.0-alpine AS production
RUN apk add --no-cache tzdata
ENV TZ Europe/Moscow
ENV NODE_PATH /opt/server/node_modules

WORKDIR /opt/server/

COPY /*.json ./
RUN npm i

CMD ["sh", "-c", "npm run start"]
    

Чтобы класс new Date() в Node.js возвращал корректное для вас время, можно поменять таймзону контейнера. Например, если хотите установить уральское время, вместо Europe/Moscow напишите Asia/Yekaterinburg.

Контейнер с Nginx

Прежде чем продолжить, надо еще добавить в папку nginx конфиг. Для этого внутри папки nginx создайте файл nginx.conf со следующим содержимым:

nginx.conf
        server {
  root /var/www;
  listen 80;
  gzip on;
  gzip_types text/plain application/xml text/css application/javascript;
  gzip_min_length 1000;
  # Проверку можно будет добавить в Express
  client_max_body_size 0;

  # C любовью
  add_header X-Created-By "Proglib";

  location / {
    # Ищем файл в папке static (ее Docker собрал слизав у Node)
    # Если ничего не нашли выбрасываем прокси
    try_files /static/$uri $uri @nodeproxy;
  }

  location @nodeproxy {
    proxy_redirect off;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_read_timeout 1m;
    proxy_connect_timeout 1m;
    # app это алиас для контейнера с Node.js
    proxy_pass http://app:3000;
  }

  # А по этому маршруту проксируем все в Pgadmin
  location /pgadmin {
    proxy_set_header X-Script-Name /pgadmin;

    proxy_pass http://pgadmin;
    proxy_intercept_errors on;
    error_page 503 = @nodeproxy;
    error_page 502 = @nodeproxy;
  }
}
    

Запускаем сборку

Вот теперь можно все запустить. Из основной папки стартуем нашу сборку командой:

        docker-compose up --build -d
    

После того как все будет готово, и в терминале появятся четыре заветных зеленых done можно продолжать. Теперь-то уже можно начать непосредственно работу с Node.js.

Работаем с Node.js в контейнере

Давайте перейдем в контейнер с Node.js командой:

        docker-compose exec app sh
    

Если вы все сделали правильно, в терминале появится у строки ввода приписка opt/server.


В этом режиме вы можете выполнять команды внутри контейнера. Чтобы выйти из контейнера, напишите команду:

        exit
    

Теперь, давайте напишем немного Node.js кода (войдите в контейнер, если вышли). Сначала надо инициализировать проект командой:

        npm init
    

Далее, давайте создадим файл app.js внутри папки app. Создаем его самым обычным способом (можете через терминал, как хотите). Когда файл появится в VS Code, вы можете проверить, появился ли он в контейнере, для этого достаточно написать команду:

        ls
    

У нас должно быть сейчас 3 файла в контейнере: Dockerfile, app.js, package.json.

После установим нужные нам пакеты командой:

        npm install express nodemon

    

Поместим следующий код в app/app.js:

app.js
        // Express
const express = require('express')
const app = express()

// Router
const router = express.Router()

// Главная
router.get('/', (_req, res) => {
  res.status(200).json({
    message: 'Hello World',
  })
})

// Обработка всего остального
router.get('/*', (_req, res) => {
  res.status(400).json({
    error: 'Запрос не может быть обработан, маршрут не найден'
  })
})

// Routes
app.use('/', router)

app.listen(3000, () => {
  console.log('Сервер запущен')
})
    

И создадим команду start в package.json:


Команда start:

        nodemon ./app.js
    

Теперь можно запустить внутри контейнера команду:

        npm run start
    

В консоль вы получите сообщение «Сервер запущен». Если перейдете в своем браузере по адресу http://localhost/, то увидите что-то подобное:

У автора стоит плагин для Google Chrome JSON Viewer
У автора стоит плагин для Google Chrome JSON Viewer

Если создадите файл внутри папки app с окончание js или jsonnodemon подхватит изменения и перезапустит проект. Аналогично, если вы просто поменяете содержимое какого-нибудь файла с данными расширениями.

Если отключите nodemon командой ctrl + c, то по прошлому адресу можно увидеть сообщение от Nginx: 502 Bad Gateway. Если поместите в папку static любой файл, он будет доступен по указанному маршруту. Например, static/file.txt => http://localhost/file.txt. Даже если nodemon не работает. Помним, за работу со статикой у нас отвечает Nginx.

Устанавливаем Linter для JavaScript

Ну а как без этого? Для начала, перейдите в VS Code в раздел с плагинами, скачайте и активируйте плагин Eslint:


Вот теперь можно установить сам Eslint в проект командой:

        npm init @eslint/config
    

Сначала выбираем To check syntax and find problems, после Common JS, на вопрос о фреймворке выбираем None of these, на вопрос про TypeScript отвечаем No. Платформу выбираем Node, галочку снимаем с Browser (все через пробел). Настройки сохранять будем в JSON, соглашаемся с установкой последней версии eslint и выбираем в конце npm.

Если у вас все получилось сделать правильно, будет работать подсветка:

Возможно, придется перезапустить VS Code.
Возможно, придется перезапустить VS Code.

Это все благодаря тому, что мы монтируем всю папку app (вместе с node_modules) в dev-режиме. Осталось добавить команду для тестового (dev) запуска nodemon, который сначала будет парсить проект с помощью Eslit. Для этого добавьте еще две команды в package.json:


dev:

        nodemon ./app.js --exec \"npm run lint && node\"

    

lint:

        eslint .
    

Сохраните и запустите внутри контейнера команду:

        npm run dev
    

Допустите какую-нибудь ошибку в коде и сохраните файл:


Если получили ошибку, то вы все настроили верно. Поздравляю, теперь ваш код будет чище.

Подключаемся к базе данных

Выходим на финишную прямую: осталось лишь подключить Node.js к базе данных.

В контейнере устанавливаем пакет для соединения с базой данных:

        npm install pg
    

Добавляем код в app/app.js:

        // DB
const { Pool } = require('pg')
const pool = new Pool({
  user: process.env.DB_USER,
  host: process.env.DB_HOST,
  database: process.env.DB_NAME,
  password: process.env.DB_PASSWORD,
  port: 5432,
})

pool.query('SELECT NOW()', (err, res) => {
  console.log(err, res)
  pool.end()
})
    

Должно получиться как-то так:


Если вы сейчас сохраните и запустите проект, nodemon выбросит ошибку. А все потому, что у нас нет базы данных под названием api (если вы не меняли название). Для этого переходите по адресу http://localhost/pgadmin, и вводите данные из docker-compose.yml для входа. Если вдруг получили ошибку от Nginx после авторизации, то просто нажмите F5.

Нам нужно создать новый сервер:


Во вкладке General введите любое название. Во вкладке Connection введите наши данные для подключения:


Базы api еще нет, поэтому вместо нее введите postgres. Создайте новую базу под названием api:


Теперь запускайте nodemon. Поздравляем, вы подключились к базе данных:


Финальная сборка

Так как мы написали все нужные команды, теперь можем поменять NODE_ENV в .env на production и запустить наш проект в другом режиме командой:

        docker-compose up --build -d
    

В данный момент production сборка от dev отличается тем, что node_modules контейнер создает свои, и они больше не прокинуты через volumes между контейнером и ПК. Поэтому команда npm i (не забываем про разные таргеты) из Dockerfile установит node_modules только в контейнер. То есть, если вы удалите свои node_modules, то теперь на контейнер это не повлияет.

Поэтому эту команду можно использовать для того, чтобы быстро тянуть файлы с GitHub и запускать проект на сервере. Мы же не храним node_modules в git-репозиториях.

Как вернуться в dev

Для работы в dev-режиме вам просто потребуется вернуть NODE_ENV в dev режим, перезапустить проект через композ:

        docker-compose up --build -d
    

Зайти в контейнер командой:

        docker-compose exec app sh
    

Установить пакеты:

        npm install
    

Запустить внутри команду:

        npm run dev
    

Это обеспечит вам гибкость при разработке и разделит production от dev, позволит работать с линтером и подсветкой в VS Code. А также, разработка внутри контейнера обезопасит ваш компьютер от всяких вредоносных валварей.

***

Напоследок

Для работы с базой данных автор рекомендует Sequelize. Плюс, разделенные docker-compose.yml позволят вам создавать разные Nginx конфиги для production и dev. Например, в боевую сборку можно добавить образ gordonchan/auto-letsencrypt и открыть 443 порт в nginx для https.

И вот ссылка на репозиторий GitHub. Там можно найти весь код проекта.

Материалы по теме

04
Окт
2022

📰 Weekly #20: новости, подкасты, отборные статьи и обучающие материалы по Python, Data Science, Go, C#, C/C++ и мобильной разработке

В этом выпуске: Taichi и 100-кратное ускорение Python-кода; важно ли DS-аналитику знать про software development; разбор четырех распространенных ловушек, которые ведут к неточностям тестирования в Go; о пользе async/await в ASP.NET Core пр…

28
Сен
2022

Где и как лучше формировать JSON?

ситуация следующая. Есть определенный сайт, с которого я паршу данные. Парсер на nodejs (cheerio+axios).
Эти данные я сортирую по необходимому мне порядку, после чего ручками собираю json. Фактически, вариант данных, которые должны появлят…

20
Сен
2022

Как сделать запрос из реакта с помощью axios на локальное бэкэнд приложение на питоне?

Я запустил локальное бэкэнд приложение на питоне на 8000 порту и пытаюсь сделать на него запрос из реакта чтобы получить json с данными, но постоянно выдает ошибку, хотя запросы на внешние сервера проходят успешно

20
Сен
2022

📰 Weekly #18: новости, подкасты, отборные статьи и обучающие материалы по Python, Data Science, Go, C#, C/C++ и мобильной разработке

В этом выпуске: ИИ-самоучка демонстрирует сходство с тем, как работает мозг; почему размер вашего веб-сайта не должен превышать 14 Кб; почему команда Google Cloud любит Go; руководство по тестированию безопасности iOS-приложений.

19
Сен
2022

🐍🚀 Пишем гибридное приложение для хранения заметок на Django, Django Ninja REST Framework и Alpine.js

Изучаем основные возможности Django Ninja, Alpine.js и Axios в процессе создания веб-приложения для хранения заметок.

Рано или поздно любой начинающий Django-разработчик сталкивается с проектом, для которого нужно четкое разделение приложения на бэкенд и фронтенд: в этом случае серверную часть пишут на Django REST Framework (DRF) или FastAPI, а клиентскую – на React, Angular или Vue. Если речь идет о высоконагруженном сайте со множеством интерактивных элементов на стороне клиента – такой подход неизбежен. При этом значительную часть функциональности, которую Django предоставляет по умолчанию, придется реализовать на стороне фронтенда – и это будет гораздо сложнее.

Но если нагрузка на приложение будет умеренной, а идея разделения на фронтенд и бэкенд возникла из-за необходимости реализовать взаимодействие с базой данных без постоянных обновлений страниц – то такой проект целесообразнее воплотить в виде гибридного приложения. Гибридный подход позволяет:

  • максимально использовать «батарейки» Django – в том числе формы, систему аутентификации и авторизации;
  • реализовать асинхронную передачу данных и CRUD без перезагрузки страниц;
  • включить в шаблоны Джанго любые интерактивные JS-элементы.

Обзор проекта

Главная страница Notes – категории заметок
Главная страница Notes – категории заметок

Мы создадим гибридное приложение Notes, которое опирается на базовую функциональность Django. Помимо Джанго, приложение будет использовать:

  • Фреймворк Django Ninja для API и CRUD.
  • Библиотеку Axios – для HTTP-запросов к бэкенду.
  • Ультралегкий JS-фреймворк Alpine.js и CSS-фреймворк Bootstrap – для фронтенда.
Заметки в отдельной категории
Заметки в отдельной категории

Приложение использует один базовый и два гибридных шаблона Django/Alpine.js:

  • base.html – подключает библиотеку Axios, а также фреймворки Alpine.js и Bootstrap.
  • index.html – выводит все категории заметок. Карточки категорий добавляются и удаляются без перезагрузки страницы.
  • detail.html – отображает все заметки в определенной категории. Карточки заметок добавляются и удаляются без перезагрузки, таким же образом происходит обновление статуса В процессе на Сделано.

Весь код для проекта находится в репозитории.

Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека питониста»

Бэкенд и API

Для разработки API мы воспользуемся новым фреймворком Django Ninja. Это отличная альтернатива Django REST Framework и FastAPI, причем по синтаксису Django Ninja ближе к последнему. Django Ninja гораздо проще DRF и намного производительнее.

<a href="https://github.com/vitalik/django-ninja-benchmarks" target="_blank" rel="noopener noreferrer nofollow">Тест производительности Django Ninja</a>
Тест производительности Django Ninja

Единственный недостаток Django Ninja – пока что фреймворк не поддерживает представления на основе классов, и код получается довольно объемным (по сравнению с DRF), но в ближайшее время разработчики обещают решить эту задачу.

Начнем работу с создания нового проекта и приложения:

        python -m venv Notes\venv
cd notes
venv\scripts\activate
pip install django
pip install django-ninja
django-admin startproject config .
manage.py startapp notes

    

Создадим базу данных и учетную запись админа:

        manage.py migrate
manage.py createsuperuser

    

Сделаем нужные настройки в файле config/settings.py:

        import os
…
INSTALLED_APPS = [
…
	'notes',
]
…
STATIC_URL = '/static/'
STATIC_ROOT='/'
STATICFILES_DIRS = (os.path.join(BASE_DIR, 'static'),)

    

Создадим файл notes/urls.py с содержимым:

notes/urls.py
        from django.urls import path
from . import views
app_name = 'notes'
urlpatterns = [
	path('', views.home, name='home'),
    path('category/<category_id>/', views.category_detail, name='detail'),
]

    

Добавим нужные маршруты в config/urls.py:

config/urls.py
        from django.contrib import admin
from django.urls import path, include
from notes.api import api
from django.conf import settings
from django.conf.urls.static import static
 
urlpatterns = [
	path('admin/', admin.site.urls),
	path("api/", api.urls),
	path('', include('notes.urls', namespace="notes")),
]
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

    

Создадим модели для заметок и категорий:

notes/models.py
        from django.db import models

class Category(models.Model):
    title = models.CharField(max_length=100)
    description = models.CharField(max_length=300)
    created = models.DateTimeField(auto_now_add=True)
   
    class Meta:
    	verbose_name_plural = 'Категории'
    	ordering = ['created']

    def __str__(self):
        return self.title

class Note(models.Model):
    title = models.CharField(max_length=250)
    category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name='notes')
    created = models.DateTimeField(auto_now_add=True)
    completed = models.BooleanField(default=False, blank=True)
    
    class Meta:
    	verbose_name_plural = 'Заметки'
    	ordering = ['-created']  

    def __str__(self):
        return self.title        
    

Зарегистрируем модели в admin.py:

admin.py
        from django.contrib import admin
from .models import Category, Note
 
admin.site.register(Category)
admin.site.register(Note)

    

Файл notes/views.py должен выглядеть так:

notes/views.py
        from django.shortcuts import render, get_object_or_404

from .models import Category, Note

def home(request):
    return render(request, 'index.html', {
        'categories': Category.objects.all()
    })


def category_detail(request, category_id):
    category = get_object_or_404(Category, id=category_id)
    return render(request, 'detail.html', {
        'category': category
    })

    

Теперь нужно подготовить и выполнить миграции:

        manage.py makemigrations
manage.py migrate

    

После выполнения миграций можно приступать к разработке API и CRUD-операций. Для этого нужно создать два файла – notes/schemas.py и notes/api.py. Сначала займемся схемами – они в Django Ninja выполняют те же самые функции, что и сериализатор в Django REST Framework, то есть определяют, какие именно данные поступают в базу и какие запрашиваются. Обратите внимание на разницу в наборах данных между схемами NoteIn, NoteOut, NoteUpd, CategoryIn, CategoryOut:

notes/schemas.py
        from ninja import Schema, ModelSchema
from datetime import date
from .models import Note


class CategoryIn(Schema):
    title: str
    description: str

class CategoryOut(Schema):
    id: int
    title: str
    description: str    
    created: date
  

class NoteIn(ModelSchema):
    class Config:
        model = Note
        model_fields = ['title', 'category']

class NoteUpd(ModelSchema):
    class Config:
        model = Note
        model_fields = ['id', 'completed']

class NoteOut(ModelSchema):
    class Config:
        model = Note
        model_fields = ['id','title', 'category', 'created', 'completed']

    

Вся функциональность API описана в одном файле – notes/api.py:

notes/api.py
        from datetime import date
from typing import List
from ninja import NinjaAPI, Schema
from django.shortcuts import get_object_or_404
from .models import Note, Category
from .schemas import NoteIn, NoteOut, NoteUpd, CategoryIn, CategoryOut


api = NinjaAPI()


@api.post("/notes", tags=['Заметки'])
def create_note(request, payload: NoteIn):
    data = payload.dict()
    category = Category.objects.get(id=data['category'])
    del data['category']
    note = Note.objects.create(category=category, **data)
    return {"id": note.id}

@api.post("/category", tags=['Категории'])
def create_category(request, payload: CategoryIn):
    category = Category.objects.create(**payload.dict())
    return {"id":category.id}    


@api.get("/notes/{note_id}", response=NoteOut, tags=['Заметки'])
def get_note(request, note_id: int):
    note = get_object_or_404(Note, id=note_id)
    return note

@api.get("/category/{category_id}", response=CategoryOut, tags=['Категории'])
def get_category(request, category_id: int):
    category = get_object_or_404(Category, id=category_id)
    return category

@api.get("/category", response=List[CategoryOut], tags=['Категории'])
def list_categories(request):
    categories = Category.objects.all()
    return categories   

@api.get("/notes", response=List[NoteOut], tags=['Заметки'])
def list_notes(request):
    notes = Note.objects.all()
    return notes


@api.patch("/notes/{note_id}", tags=['Заметки'])
def update_note(request, note_id: int, payload: NoteUpd):
    note = get_object_or_404(Note, id=note_id)
    for attr, value in payload.dict().items():
        setattr(note, attr, value)
    note.save()
    return {"success": True}

@api.put("/category/{category_id}", tags=['Категории'])
def update_category(request, category_id: int, payload: CategoryIn):
    note = get_object_or_404(Category, id=category_id)
    for attr, value in payload.dict().items():
        setattr(note, attr, value)
    category.save()
    return {"success": True}

@api.delete("/notes/{note_id}", tags=['Заметки'])
def delete_note(request, note_id: int):
    note = get_object_or_404(Note, id=note_id)
    note.delete()
    return {"success": True}

@api.delete("/category/{category_id}", tags=['Категории'])
def delete_category(request, category_id: int):
    category = get_object_or_404(Category, id=category_id)
    category.delete()
    return {"success": True}

    

Теперь можно запустить сервер и протестировать работу API:

        manage.py runserver
    

Перейдите по ссылке http://localhost:8000/api/docs – это адрес Django Ninja API:

Веб-интерфейс Django Ninja API
Веб-интерфейс Django Ninja API

Создавать новые категории и заметки можно прямо на этой странице. Выберите операцию POST в разделе Категории, нажмите кнопку Try it out, введите название и описание категории:

Добавление в базу новой категории
Добавление в базу новой категории

Кликните на Execute – готово, первая категория добавлена в базу данных:

Сервер сообщает об успешном добавлении категории
Сервер сообщает об успешном добавлении категории

HTTP-запросы к бэкенду

За обработку запросов к бэкенду отвечает библиотека Axios, подключенная в шаблоне base.html. Axios – это альтернатива fetch с более дружественным синтаксисом. Код HTTP-запросов расположен в конце шаблонов index.html и detail.html. Создайте папку notes/templates и поместите туда все три шаблона. Кроме того, создайте папку static на одном уровне с notes и config, и сохраните в ней файл CSS-стилей.

Перейдите на главную страницу приложения http://localhost:8000/ и протестируйте работу API и Axios – теперь карточки категорий и заметки можно добавлять с фронтенда:

Добавление карточек происходит без перезагрузки страницы
Добавление карточек происходит без перезагрузки страницы
Статус заметки изменяется одним кликом без перезагрузки
Статус заметки изменяется одним кликом без перезагрузки

Фронтенд

Помимо API и Axios в добавлении элементов без перезагрузки страницы участвует фреймворк Alpine.js. Синтаксис Alpine.js очень похож на Vue.js – но, в отличие от Vue, Alpine не конфликтует с тегами Django и не требует заключения кода в теги {% verbatim %} {% endverbatim %}. По функциональности Alpine максимально близок к jQuery, поэтому фреймворк уже окрестили «современным jQuery».

Синтаксис Alpine.js во многом напоминает синтаксис стандартного шаблонизатора Django, и разобраться в нем (в отличие от ванильного JavaScript) не составит никакого труда. Еще одно огромное преимущество Alpine.js заключается в том, что его не нужно никуда устанавливать и запускать на отдельном локальном сервере (и, следовательно, не придется использовать CORS).

Более того, Alpine.js без проблем может получить данные от шаблонизатора Django. Обратите внимание на эти фрагменты в index.html, где в шаблоне мирно уживаются запрос Alpine и получение данных из бэкенда Django:

        <div x-data="getCategories()">
   <h3 class="text-center mt-5" style="color:#777">все категории заметок пользователя <span class="fw-bold">{{ request.user.username }}</span></h3>
    <form id="category-form">
    	{% csrf_token %}
	</form>
...
const getCategories = () => {
	return {
    	newCategory: '',
    	newDescription: '',
    	categories: [
        	{% for category in categories %}
        	{ 'title': '{{ category.title }}', 'id': '{{ category.id }}', 'description': '{{ category.description }}' },
        	{% endfor %}
        ]
    }
};

    

В конце шаблонов index.html и detail.html библиотека Axios обеспечивает обработку запросов к Django Ninja API. При создании новой категории Axios принимает от Alpine название и описание (title, description) и передает API запрос POST:

        const addCategory = async (title, description) => {
	try {
	const res = await axios.post('/api/category',
    	{ title, description },
    	{ headers: { 'X-CSRFToken': csrftoken }}
    	);
	location.reload();
  	} catch (e) {
    	console.error(e);
  	}
};

    

Для удаления категории Axios передает бэкенду соответствующий ID – categoryId:

        const removeCategory = async categoryId => {
	try {
	const res = await axios.delete('/api/category/' + categoryId,
    	{ headers: { 'X-CSRFToken': csrftoken }}
    	);
	location.reload();
  	} catch (e) {
    	console.error(e);
  	}
};

    

Подведем итоги

Фреймворк Django Ninja и библиотека Alpine.js появились совсем недавно, но уже успели произвести фурор среди разработчиков: скорость, гибкость, простота синтаксиса и бесшовная интеграция делают их идеальным выбором для гибридных Django-проектов с умеренной нагрузкой. Приложение Notes, несмотря на простоту, позволяет быстро изучить все основные возможности Django Ninja и Alpine.js, разобраться в механизме взаимодействия API и Axios, и приступить к разработке более сложных проектов. Напоминаем, что весь код для Notes можно взять в репозитории.

***

Материалы по теме

06
Сен
2022

golang get запрос возвращает блок javascript кода вместо готовой страницы, как получить данные страницы которую я вижу в браузере?

Я хочу распарсить сайт dns чтобы из кода страницы взять названия, цены и информацию по наличию видеокарт.
Чтобы получить данные с других сайтов достаточно было прочитать body с помощью кода ниже и дальше искать конкретные элементы по назва…

06
Сен
2022

golang get запрос возвращает блок javascript кода вместо готовой страницы, как получить данные страницы которую я вижу в браузере?

Я хочу распарсить сайт dns чтобы из кода страницы взять названия, цены и информацию по наличию видеокарт.
Чтобы получить данные с других сайтов достаточно было прочитать body с помощью кода ниже и дальше искать конкретные элементы по назва…

06
Сен
2022

📰 Weekly #16: новости, подкасты, отборные статьи и обучающие материалы по Python, Data Science, Go, C#, C/C++ и мобильной разработке

В этом выпуске: мифы и легенды современного Python; как построить GPT-3 для науки; собеседование Go-разработчика глазами нанимателя; простой гайд по разработке VR-проекта на Unity; что будет с iOS-разработкой в России?

06
Сен
2022

📰 Weekly #16: новости, подкасты, отборные статьи и обучающие материалы по Python, Data Science, Go, C#, C/C++ и мобильной разработке

В этом выпуске: мифы и легенды современного Python; как построить GPT-3 для науки; собеседование Go-разработчика глазами нанимателя; простой гайд по разработке VR-проекта на Unity; что будет с iOS-разработкой в России?