# Файловая структура Startupjs приложения (application entry point, back-end, standalone apps)

После того как отработает `npx startupjs init myapp`, вы получите папку с проектом `myapp`. Если открыть ее, то вы увидите определенную файловую структуру, которую следует рассмотреть подробней.

## Именование файлов

Так startupjs в своем ядре имеет `react-native`, то изначально каждый файл интерпритируется как файл для компиляции под мобильное устройство и для веб версии. Но есть возможность разделить файлы основываясь на платформе. Для этого используйте следующий принцип:

  * `*.web.js` для файлов, которые предназначены только для веб версии.
  * `*.js`: для файлов, которые предназначены для мобильных устройств

Если нет файла с именем `*.web.js `, то файл `*.js` компилируется под веб и под мобильную версию.

## Зачем это нужно?

1. **Есть модуль только под одну из платформ**

  Встречается ситуация, когда кроссплатформенный модуль значительно уступает в качестве модулю под одну из платформ. Предположим есть модуль для работы с изображениями предназначенный исключительно для веб версии. Тогда в файле `*.web.js` мы будем использовать этот модуль, а для мобильного приложения подыщем альтернативу предназначенную для `react-native`.

1. **Значительные различия в веб версии и мобильном приложении**

  Такое поведение не приветствуется в формате кроссплатформенной разработки, но, как это всегда бывает, есть исключения. Допустим в вашем приложении есть админ панель и вам, по каким-то причинам, хочется чтобы веб приложение имело значительно больший функционал. Тогда имеет смысл сформировать два файла `*.web.js ` и `*.js`.

## Точка входа в приложение

Каждое приложение имеет свою точку входа. То есть, по сути, это место с которого начинается ваше приложение.

В структуре startupjs это папка `Root`. Здесь происходит импорт компонента `App` из sturtupjs, который инициализирует все приложение. Так выглядит стандартный `Root/index.js`:

```jsx
import { BASE_URL } from '@env'
import init from 'startupjs/init'
import orm from '../model'
import React from 'react'
import App from 'startupjs/app'
import { observer, model } from 'startupjs'
import { Platform } from 'react-native'

// Frontend micro-services
import * as main from '../main'

if (Platform.OS === 'web') window.model = model

// Init startupjs connection to server and the ORM.
// baseUrl option is required for the native to work - it's used
// to init the websocket connection and axios.
// Initialization must start before doing any subscribes to data.
init({ baseUrl: BASE_URL, orm })

export default observer(() => {
  return pug`
    App(
      apps={main}
    )
  `
})
```

Давайте подробней разберем, что этот код делает. Здесь есть несколько интересных моментов.

1. `import * as main from '../main'`: импорт приложения main. startupjs  позволяет создавать несколько приложений в рамках одного проекта. Подробней об этом ниже.

1. `init({ baseUrl: BASE_URL, orm })`: инициализирует текущий базовый url для проекта, который необходим для `react-native` (например, для связи приложения с сервером) и подключает orm. Собственно это и сказанно в коментарии.

1. `App(apps={main})`: собственно инициализация всего проекта, включая все приложения. Здесь также есть несколько дополнительных пропсов:

    * `criticalVersion`: указывает на текущую версию приложения и если текущая версия приложения устарела, то появится требование его обновить.

    * `useGlobalInit`: коллбек который выполнится после инициализации. По умолчанию инициализация заключается в добавлении ссылки на `user` в `_session.user`. Добавить ссылку означает создать такую связь, которая будет постоянно содержать актульную информацию о `user` в `_session.user` и наоборот, при изменении `_session.user` изменения сразу попадут в `user`. Но вы можете передать в `useGlobalInit` свой коллбек, который будет выполнен сразу после дефолтного поведения. Важный момент заключается в том, что переданный коллбек должен возвращать `true` при успешном выполнение, так как в противном случае выполнение кода не перейдет к рендеру приложения, так как посчитает, что на моменте инициализации возникла ошибка.

## ORM

В корне проекта есть папка `models`. В ней содержатся и подключаются все модели в проекте. В файле `models/index.js` происходит подключение каждой из моделей. Он имеет следующий вид:

```js
import TodosModel from './TodosModel'
import TodoModel from './TodoModel'

export default function (racer) {
  racer.orm('todos', TodosModel)
  racer.orm('todos.*', TodoModel)
}
```

Здесь остановимся, чтобы подробней рассмотреть каждый момент. Обратите особое внимание на имена импортируемых моделей. Вы видите, что у нас есть модель во множественном числе `TodosModel` и в единственном `TodoModel`. Модель в множественном числе предназначенна для коллекции. Множественное значение названия происходит из того, что коллекция содержит много однотипных обьектов (например, здесь коллекция содержит много `todo` обьектов). Модель в единственном числе отражает один ОПРЕДЕЛЕННЫЙ элемент коллекции. Давайте на примере, чтобы было проще понять. У вас есть большая комната с котами. Это коллекция котов и ее описывает `CatsModel`, в ней их много - рыжие, черные и даже пятнистые. А вот конкретный кот Борис серого цвета с светлыми пятнышками будет описан моделью `CatModel`. Очевидно, что эти модели будут иметь разный функционал. Так в комнату можно добавить нового кота, забрать уже существующего. А вот с конкретным котом можно уже сделать совершенно другие вещи, например добавить ему бантик.

Теперь давайте посмотрим как мы привязываем конкретную модель к конкретной сущности. Происходит это с помощью команды `racer.orm(path, model)`.

Здесь `path` это путь к конкретной сущности (чаще всего это коллекция или элемент коллекции). Соответственно чаще всего он имеет вид:
  * `collectionName`: имя коллекции, в листинге выше это `todos`
  * `collectionName.*`: любой конкретный элемент коллекции, в листинге выше это каждый `todo` в коллекции `todos`.

---

Теперь разберемся в том, что собой представляет сам файл модели, который мы подключали выше. Давайте опять разбирать на примере. Вот так выглядит модель, которая имеет только стандартные методы модели:

```js
import { BaseModel } from 'startupjs/orm'

export default class TodosModel extends BaseModel {

}
```

Как раз таки модуль `BaseModel`, потомком которого является `TodosModel`, предоставляет стандартные методы. Файл модели для конкретного элемента будет выглядить так же.

```js
import { BaseModel } from 'startupjs/orm'

export default class TodosModel extends BaseModel {

}
```

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

Давайте расширим `TodosModel`. Определим метод `addTodo`, который будет добавлять дату создания задачи к записываемым данным.

```js
import { BaseModel } from 'startupjs/orm'

export default class TodosModel extends BaseModel {

async addTodo() {
    let id = this.id()
    const createdAt = Date.now()
    await this.add({
      id,
      createdAt,
      ...data
    })

    return id
}

}
```

Также важный момент: `this` в данном обьекте содержит модель. То есть с `this` можно работать так же как с `model`.
Теперь можно вызывать метод `addTodo` для модели коллекции `todos`. Например:
```js
  const $todos = useModel('todos')

  const [title, setTitle] = useState('')

  function addTodo() {
    $todos.addTodo({
      title,
      complited: false
    })
  }

  return pug`
    Div.root
      TextInput(value=title onChange=setTitle)
      Button(onPress=addTodo)
```

Точно также можно расширять модель `TodoModel`. Главное не путать модель коллекции и конкретного документа.

Важно! Для того чтобы модель заработала в проекте, ее необходимо инициализировать в `models/index.js`

## apps

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

## Что такое отдельное приложение?

Зайдите в папку `main` и посмотрите на структуру.

Давайте по порядку, здесь есть файл `routes.js`, он выглядит следующим образом:

```js
export default (components = {}) => [
  {
    path: '/',
    exact: true,
    component: components.PHome
  },
  {
    path: '/about',
    exact: true,
    component: components.PAbout
  }
]
```

Здесь мы видим функцию которая возвращает массив обьектов. Каждый из объектов представляет собой правило какой компонент необходимо отобразить по заданному роуту. Параметр `exact` указывает на точное совпадение маршрута. То есть, например, компонент `PAbout` будет отображен только в случае перехода по маршруту `/about`.

Теперь давайте посмотрим на файл `index.js`:

```js
import * as pages from './pages'
import getRoutes from './routes'

export { default as Layout } from './Layout'
export const routes = getRoutes(pages)
```
Данный файл, обычно, остается в таком виде на протяжении всего времени разработки. Его главная задача - експортировать список правил из `routes.js` и макета.

Папка `Layout` не представляет собой ничего сложного. Простыми словами это компонент который будет оберткой для любой страницы в данном приложении. Следовательно здесь имеет смысл описывать `Header`, `Footer` или `Sidebar`.

С `pages` такая же ситуация, здесь в папках расположены страницы, которые потом будут рендериться по обращению к заданным роутам. Когда создадите новую страницу не забудьте експортировать ее в файле `pages/index.js`. Здесь принято следующее правило именования компонентов: префикс P - указывающий что это страница, и далее `uppercase` название страницы например `PTodosList`.

## Когда имеет смысл создавать новое приложение?

Предпосылкой к этому является значительное различие макетов и функционала. Например, есть приложение и необходимо реализовать админ панель для него. Здесь имеет смысл инициализировать новое приложение `admin`. Так же в будующм это будет проще поддерживать, потому что часть административной панели и часть самого приложения являются по сути разными сущностями в концептуальном плане. Плюс это все помогает со разделением кода.

## Как подключить новое приложение?

Давайте добавим приложение `admin` в базовый проект.

1. Создайте папку с именем вашего нового приложения. В нашем случае это будет папка `admin`.
1. Реализуйте базовую структуру, по примеру `main`. То есть доюавте `index.js`, `routes.js` и папки `Layout`, `pages`.
1. Теперь необходимо подключить его в `Root`. В файле `index.js` где происходит инициализация приложения, в пропс компонента `App` добавьте приложение `admin`. Получится такая структура:

```js
...

// Frontend micro-services
import * as main from '../main'
import * as admin from '../admin'

...

export default observer(() => {
  return pug`
    App(
      apps={main, admin}
    )
  `
})
```

## Server

startupjs предоставляет сервер основанный на Express. После выполнения `npx startupjs init myapp`, вы получаете вполне работоспособный сервер, к которому уже подключена orm и базовое приложение `main`. На этой основе уже можно строить бизнес логику вашего приложения. Сервер можно найти по пути `server/index.js`. Давайте рассмотрим стандартный вид сервера в startupjs:

```js
import init from 'startupjs/init'
import orm from '../model'
import startupjsServer from 'startupjs/server'
import api from './api'
import initPermissions from '@dmapper/permissions/server'
import { initApp } from 'startupjs/app/server'

// Init startupjs ORM.
init({ orm })

// Check '@startupjs/server' readme for the full API
startupjsServer({
  getHead
}, (ee, options) => {
  initApp(ee)

  ee.on('routes', expressApp => {
    expressApp.use('/api', api)
  })
})

function getHead () {
  return `
    <title>HelloWorld</title>
    <!-- Put vendor JS and CSS here -->
  `
}

export default function run () {}
```

На что здесь нужно обратить внимание:

  * `init({ orm })`: инициализация orm на серверной части
  * `startupjsServer(option, callback)`: функция инициализации сервера, по сути это основная функция. Она имеет два параметра:
    * `option`: объект опций. Здесь подключаются роуты каждого из приложений, задается шаблон для блока head в выходном html файле. Также здесь подключаются различные расширения startupjs (например shardb-access, sharedb-schema и др.)
    * `callback(ee, option)`: здесь `ee` является инстансом `EventEmitter`, следовательно с его помощью можно осуществлять подписку на различные `events`. `option` - является модифицированной копией первого аргумента `startupjsServer`, которые генерирует `startupjsServer`.
  * `initApp(ee)`: необходима для инициализации сессии.
  * `ee.on('routes', callback)`:  здесь в `callback` происходит подключение middleware Express. Например, в данном примере происходит подключение `api` на роут `/api`, импорт которого можно увидеть выше.

## Force Compile Modules

По умолчанию вебпак не проходит файлы в папке `node_modules`, так как обычно там находится уже скомпилированный код. Но бывают такие случаи, когда необходимо преобразовать код в модуле с помощью `babel`. В таком случае нужно добавить имя пакета в `forceCompileModules` в файле конфигураций `webpack`. Обратите внимание на то, что существует два таких файла:

  * `webpack.server.config.cjs`: для сборки сервера

  * `webpack.web.config.cjs`: для сборки клиентской части

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

Например, есть модуль `react-native-animatable`, который нуждается в предварительной компиляции. Так как он используется в клиентской части проекта, то мы добавляем его в `forceCompileModules` в `webpack.web.config.cjs`:

```js
const getConfig = require('startupjs/bundler.cjs').webpackWebConfig

module.exports = getConfig(undefined, {
  forceCompileModules: [
    'react-native-animatable',
  ],
  alias: {},
  mode: 'react-native'
})
```
