程序员求职经验分享与学习资料整理平台

网站首页 > 文章精选 正文

adonisjs的模板以及路由

balukai 2025-01-04 17:02:22 文章精选 7 ℃

学习视频教程到52课了(Let's Learn AdonisJS 6: Generating A Unique Movie Slug With Model Hooks - Adocasts),学而时习之,温故而知新,学了还需要反复去看一下。

adonisjs的模板,组件及通用导航等部件,都是放在views文件夹里,与css和js同在resources根目录下。

主模板:

/resources/views/components/layout/index.edge

<!DOCTYPE html>
<html lang="en">

  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>
      {{ title || "首页" }} - 中心
    </title>
    @if($slots.meta)
      {{{ await $slots.meta() }}}
    @endif
    @vite(['resources/js/app.js'])
  </head>

  <body>
    <div class="max-w-3xl mx-auto mt-6">
      @include('partials/nav')

      {{{ await $slots.main() }}}
    </div>
  </body>
</html>

导航菜单放在partisals文件夹里:

/resources/views/partials/nav.edge

<nav>
  <a href="{{ route('home') }}">首页</a>
</nav>

在主模板里加上js引用,css则是写在js里:

/resources/js/app.js

import '../css/app.css'

/resources/css/app.css

@tailwind base;
@tailwind components;
@tailwind utilities;

* {
  margin: 0;
  padding: 0;
}

html,
body {
  height: 100%;
  width: 100%;
}

h1{
  @apply text-3xl mb-3 mt-6;
}

使用3行,就可以把tailwindcss的样式引用到项目里了。

页面模板都在views/pages文件夹下:

/resources/views/pages/home.edge

@layout()
  @slot('meta')
    <meta name="description" content="Our awesome movies list" />
  @endslot
  
  <h2>
    Recently Released
  </h2>

  <ul class="flex flex-col gap-4">
    @each((movie, index) in recentlyReleased)
      @let(isFirst = index === 0)
      <li
        {{ html.attrs({              
            id:movie.slug,
            class: [
              'p-4 border',
              {
               'border-blue-500':isFirst
              }
            ] 
          }) }}
      >
        <a href="{{ route('movies.show',{slug:movie.slug}) }}" class="font-bold">{{ movie.title }}</a>
        <p class="text-slate-600">
          {{ truncate(movie.summary,50,{completeWords:true}) }}
        </p>
      </li>
    @end
  </ul>

  <h2>
    ComingSoon Released
  </h2>
  <div class="flex flex-wrap -mx-3 mt-3">
    @each(movie in comingSoon)
      <div class="w-full lg:w-1/3 px-3">
        @!movie.card({ movie, class:'w-full' })

      </div>
    @end
  </div>

  <h2>
    Recently Released
  </h2>
  <div class="flex flex-wrap -mx-3 mt-3">
    @each(movie in recentlyReleased)
      <div class="w-full lg:w-1/3 px-3">
        @!movie.card({ movie, class:'w-full' })

      </div>
    @end
  </div>

  <div class="fixed bottom-0 right-3 flex gap-3">
    <form action="{{ route('redis.flush',{},{qs:{_method:'DELETE' }}) }}" method="POST">
      {{ csrfField() }}
      @button({type:'submit',class:'rounded-b-none'})
        {{ svg('tabler:trash',{class:'w-5 h-5 mr-2'}) }} Flush Redis Db
      @end
    </form>
  </div>

@endlayout

@!movie.card({ movie, class:'w-full' }) 这个就是使用了一个card组件,前面加感叹号的意思,就是自关闭,没有感叹号的话,需要在后面加上结束的指令@end,例如下面部分的button按钮,就是没有加感叹号,需要有结束指令@end,位置在components里:

/resources/views/components/movie/card.edge

@let(className = 'rounded-lg overflow-hidden border border-slate-200/60 bg-white text-slate-700 shadow-sm')
<div {{ $props.merge({class:[className]}).except(['movie']).toAttrs() }}>
  <div class="relative">
    <img src="https://picsum.photos/450/200?v={{ movie.slug }}" class="w-full h-auto" />
  </div>
  <div class="p-7">
    <h2 class="mb-2 text-lg font-bold leading-none tracking-tight">
      {{ movie.title }}
    </h2>
    <p class="mb-5 text-slate-500">
      {{ movie.summary }}
    </p>
    @button({href:route('movies.show',{slug:movie.slug})})
      View Movie
    @end
  </div>
</div>

movie详情页面:

/resources/views/pages/movies/show.edge

@layout({title:movie.title})

  @slot('meta')
    <meta name="description" content="{{ movie.summary }}" />
  @endslot
    
  <h1>
    {{ movie.title }}
  </h1>

  @if(movie.abstract)
    <div class="my-8 bg-sky-100 rounded-xl p-8">
      {{{ movie.abstract }}}
    </div>
  @endif
    
@endlayout

路由是在start文件夹里:

/start/routes.ts

const RedisController = () => import('#controllers/redis_controller')
import router from '@adonisjs/core/services/router'

const MoviesController = () => import('#controllers/movies_controller')

router.get('/', [MoviesController, 'index']).as('home')

/*
router.on('/').render('pages/home').as('home')
router.get('/movies', () => {}).as('movies.index')

router.get('/movies/my-awesome-movie', () => {}).as('movies.show')

router.get('movies/create', () => {}).as('movies.create')

router.post('/movies', () => {}).as('movies.store')

router.get('/movies/my-awesome-movie/edit', () => {}).as('movies.edit')

router.put('/movies/my-awesome-movie', () => {}).as('movies.update')

router.delete('/movies/my-awesome-movie', () => {}).as('movies.destroy')
*/

router
  .get('movies/:slug', [MoviesController, 'show'])
  .as('movies.show')
  .where('slug', router.matchers.slug())

router.delete('/redis/flush', [RedisController, 'flush']).as('redis.flush')
router.delete('/redis/:slut', [RedisController, 'destroy']).as('redis.destroy')

控制器放在app文件夹下:

/app/controllers/movies_controller.ts

import type { HttpContext } from '@adonisjs/core/http'
import MovieVM from '#view_models/movie'
import Movie from '#models/movie'

export default class MoviesController {
  async index({ view }: HttpContext) {
    // const movies = await MovieVM.all()

    const comingSoon = await Movie.query()
      .apply((scope) => scope.notReleased())
      .whereNotNull('releasedAt')
      .orderBy('releasedAt', 'asc')
      .limit(3)

    const recentlyReleased = await Movie.query()
      .apply((scope) => scope.released())
      .orderBy('releasedAt', 'desc')
      .limit(9)

    return view.render('pages/home', { comingSoon, recentlyReleased })
  }

  async show({ view, params }: HttpContext) {
    const movie = await MovieVM.findByOrFail('slug', params.slug)

    return view.render('pages/movies/show', { movie })
  }
}

可以使用ace指令生成model、controller和factory(用于生成模拟数据):

node ace make:model profile -mcf

λ node ace make:model test -mcf
DONE:    create app/models/profile.ts
(node:30408) ExperimentalWarning: Importing JSON modules is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
DONE:    create database/migrations/1735200082035_create_profiles_table.ts
DONE:    create app/controllers/profiles_controller.ts
DONE:    create database/factories/profile_factory.ts

/app/models/profile.ts

import { DateTime } from 'luxon'
import { BaseModel, column } from '@adonisjs/lucid/orm'

export default class Profile extends BaseModel {
  @column({ isPrimary: true })
  declare id: number

  @column()
  declare userId: number

  @column()
  declare description: string | null

  @column.dateTime({ autoCreate: true })
  declare createdAt: DateTime

  @column.dateTime({ autoCreate: true, autoUpdate: true })
  declare updatedAt: DateTime
}

/database/migrations/1735200082035_create_profiles_table.ts

import { BaseSchema } from '@adonisjs/lucid/schema'

export default class extends BaseSchema {
  protected tableName = 'profiles'

  async up() {
    this.schema.createTable(this.tableName, (table) => {
      table.increments('id').notNullable()

      table.integer('user_id').unsigned().references('id').inTable('users').notNullable()
      table.text('description')

      table.timestamp('created_at').notNullable()
      table.timestamp('updated_at').notNullable()
    })
  }

  async down() {
    this.schema.dropTable(this.tableName)
  }
}

/app/controllers/redis_controller.ts

import cache from '#services/cache_service'
import type { HttpContext } from '@adonisjs/core/http'

export default class RedisController {
  async destroy({ response, params }: HttpContext) {
    await cache.delete(params.slug)
    return response.redirect().back()
  }

  async flush({ response }: HttpContext) {
    console.log('Flushing redis')
    await cache.flusDb()
    return response.redirect().back()
  }
}

/database/factories/profile_factory.ts

import factory from '@adonisjs/lucid/factories'
import Profile from '#models/profile'

export const ProfileFactory = factory
  .define(Profile, async ({ faker }) => {
    return {
      description: faker.lorem.sentence(),
      userId: faker.number.int({ min: 1, max: 10 }),
    }
  })
  .build()

生成模拟数据放在seeders文件夹下:

/database/seeders/01.start_seeder.ts

import Role from '#models/role'
import { BaseSeeder } from '@adonisjs/lucid/seeders'
import Roles from '#enums/roles'
import MovieStatus from '#models/movie_status'
import MovieStatuses from '#enums/movie_statuses'

export default class extends BaseSeeder {
  async run() {
    // Write your database queries inside the run method
    await Role.createMany([
      { id: Roles.ADMIN, name: 'Administrator' },
      { id: Roles.EDITOR, name: 'Editor' },
      { id: Roles.USER, name: 'User' },
    ])

    await MovieStatus.createMany([
      { id: MovieStatuses.WRITING, name: 'Writing' },
      { id: MovieStatuses.PRODUCTION, name: 'Production' },
      { id: MovieStatuses.RELEASED, name: 'Released' },
      { id: MovieStatuses.POST_PRODUCTION, name: 'Post Production' },
      { id: MovieStatuses.CASTING, name: 'Casting' },
    ])
  }
}

01.start_seeder.ts 放在前面,用于先生成角色表数据和电影状态的数据,其他的表有关联到这两个表,所以这两个表需要先生成数据,不然其他表的模拟数据关联到这个表的id时,如果该表id不存在,就报错了。

/database/seeders/02.fake_seeder.ts

import { movies } from '#database/data/movies'
import { CineastFactory } from '#database/factories/cineast_factory'
import { MovieFactory } from '#database/factories/movie_factory'
import { UserFactory } from '#database/factories/user_factory'
import MovieStatuses from '#enums/movie_statuses'
import { BaseSeeder } from '@adonisjs/lucid/seeders'
import { DateTime } from 'luxon'

export default class extends BaseSeeder {
  static environment = ['development']
  async run() {
    // Write your database queries inside the run method
    await CineastFactory.createMany(10)

    // await MovieFactory.merge({
    //   statusId: MovieStatuses.RELEASED,
    //   releasedAt: DateTime.now().minus({ month: 1 }),
    // }).createMany(5)

    await UserFactory.createMany(10)

    await this.#createMovies()
  }

  async #createMovies() {
    let index = 0
    await MovieFactory.tap((row, { faker }) => {
      const movie = movies[index]
      const released = DateTime.now().set({ year: movie.releaseYear })

      row.statusId = MovieStatuses.RELEASED
      row.title = movie.title

      row.releasedAt = DateTime.fromJSDate(
        faker.date.between({
          from: released.startOf('year').toJSDate(),
          to: released.endOf('year').toJSDate(),
        })
      )
      index++
    }).createMany(movies.length)

    await MovieFactory.createMany(3)

    await MovieFactory.apply('released').createMany(2)
    await MovieFactory.apply('releasingSoon').createMany(2)
    await MovieFactory.apply('postProduction').createMany(2)
  }
}

这个教程一共113节课,内容很丰富,也源于adonisjs对标最流行的框架laravel,所以adonisjs的功能也相当强大,各种命令生成文件也是很便捷。其中还有不少的代码需要吃透,容易一看就懵了。

用于把title生成slug标签,使用横杆-代替空格,并检查,如果重复则在尾部加上序号:

/app/models/movie.ts

...
  @beforeCreate()
  static async slugify(movie: Movie) {
    if (movie.slug) return
    const slug = string.slug(movie.title, {
      replacement: '-',
      lower: true,
      strict: true,
    })
    const rows = await Movie.query()
      .select('slug')
      .whereRaw('lower(??)=?', ['slug', slug])
      .orWhereRaw('lower(??) like ?', ['slug', `slug-%`])

    if (!rows.length) {
      movie.slug = slug
      return
    }

    const incrementors = rows.reduce<number[]>((result, row) => {
      const tokens = row.slug.toLowerCase().split(`${slug}-`)
      if (tokens.length < 2) {
        return result
      }
      const increment = Number(tokens.at(1))
      if (!Number.isNaN(increment)) {
        result.push(increment)
      }
      return result
    }, [])

    const increment = incrementors.length ? Math.max(...incrementors) + 1 : 1

    movie.slug = increment === 1 ? slug : `${slug}-${increment}`
  }
...

Tags:

最近发表
标签列表