网站首页 > 文章精选 正文
学习视频教程到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}`
}
...
- 上一篇: 5、谈谈你对BFC的理解?
- 下一篇: 「炫丽」从0开始做一个WPF+Blazor对话小程序
猜你喜欢
- 2025-01-04 「炫丽」从0开始做一个WPF+Blazor对话小程序
- 2025-01-04 5、谈谈你对BFC的理解?
- 2025-01-04 前端 BFC、IFC、GFC 和 FFC,这些你都知道吗?
- 2025-01-04 Wijmo5 Flexgrid基础教程:自定义编辑器
- 2025-01-04 H5小游戏开发教程之页面基础布局的开发
- 2025-01-04 web前端:CSS的常用属性速查表
- 2025-01-04 用网页做个ppt- 定时全屏切换图片
- 2025-01-04 HTML翻牌器:用CSS和HTML元素创造动态数字展示
- 2025-01-04 「网络安全」安全设备篇(漏洞扫描器-流量监控-安全审计产品)
- 2025-01-04 语音版计算器:开源代码分享
- 最近发表
- 标签列表
-
- newcoder (56)
- 字符串的长度是指 (45)
- drawcontours()参数说明 (60)
- unsignedshortint (59)
- postman并发请求 (47)
- python列表删除 (50)
- 左程云什么水平 (56)
- 计算机网络的拓扑结构是指() (45)
- 稳压管的稳压区是工作在什么区 (45)
- 编程题 (64)
- postgresql默认端口 (66)
- 数据库的概念模型独立于 (48)
- 产生系统死锁的原因可能是由于 (51)
- 数据库中只存放视图的 (62)
- 在vi中退出不保存的命令是 (53)
- 哪个命令可以将普通用户转换成超级用户 (49)
- noscript标签的作用 (48)
- 联合利华网申 (49)
- swagger和postman (46)
- 结构化程序设计主要强调 (53)
- 172.1 (57)
- apipostwebsocket (47)
- 唯品会后台 (61)
- 简历助手 (56)
- offshow (61)