2023年1月17日 星期二

AdonisJs第五天Middleware

前言

middleware是軟體設計的一種模式,可以用於特定事件的“前置”處理或“後置”處理(參見圖片),在AdonisJs裡面又可以把middleware設定為全域或指定兩種,算是我覺得非常簡單又好用的工具。



全域註冊

在./start/kernel.ts檔案裡面可以見到middleware的註冊,

Server.middleware.register([
() => import('@ioc:Adonis/Core/BodyParser'),
])

在這段程式碼中註冊了全域的middleware:BodyParser,BodyParser的作用為解析controller返回的內容,可以把view或是json或是model直接轉譯為前端應該接收的類型,以昨天的程式碼為例

export default class HellosController {
say() {
return {
say: "hello"
}
}
}

這時如果打api前端就可以拿到{ say: "hello" }的物件,不需要再使用ctx.response.json({ say: "hello" })來回傳,有效減少程式碼的長度及重複度。


指定註冊

正常新增專案的情況下,這時是不會有任何註冊的middleware,沒關係我們一步一步來,先在./start/kernel.ts註冊如下

Server.middleware.registerNamed({
api: () => import('App/Middleware/ApiFormat'),
})

接著手動新增檔案./app/Middleware/ApiFormat.ts,主要我希望只要有註冊該middleware,回傳的內容我希望是我指定的格式,程式碼如下

import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'

export default class ApiFormatMiddleware {
public async handle({ response }: HttpContextContract, next: () => Promise<void>) {
    
const start: number = Date.now()
    // 進入主程式前
await next()
// 進入主程式後
const resBody = {
code: [0],
data: response.lazyBody?.[0], // 取得主程式返回的內容
}

resBody['time'] = Date.now() - start + ' ms'
response.send(resBody)
}
}

然後再把route稍微修改一下

import Route from "@ioc:Adonis/Core/Route";

Route.get("/", "HellosController.say").middleware(["api"]);

middleware裡面可以直接塞入字串,或是用陣列註冊多個middleware

這樣就大功告成了。我們再試著打一下昨天的api結果會如下

{"code":[0],"data":{"say":"hello"},"time":"2 ms"}

可以完整輸出我希望輸出的格式還有程式執行的時間


小結

其實middleware也不是AdonisJs特有的功能,有興趣的小夥伴可以再去深究他實現的原理,並更有效的運用在自己生活中,他實踐起來並不困難,但卻能為程式碼帶來大大的便利性,算是我非常喜歡的一個功能。

2023年1月16日 星期一

AdonisJs第四天Controller

前言

一個不小心就休了十天,打這系列文章讓我真心佩服那些挑戰it鐵人賽的人,每天一篇文張看來真的不是開完笑的累。


建立控制器

可以使用指令`node ace make:controller [controller_name]`來新增controller,也可以手動新增檔案。

方法中第一個參數預設會帶入context,內容幾乎涵蓋網站開發的重要物件

  • request: 封裝了請求對象,包含了請求頭,請求參數,請求體等信息。

  • response: 封裝了回應對象,用於提供回應的方法,如返回 JSON 數據或重定向。

  • params: 封裝了路徑參數,可以訪問路徑中的參數。

  • view: 封裝了渲染視圖的方法,用於渲染模板並返回 HTML。

  • auth: 封裝了認證和授權的方法,用於確認用戶是否已經登錄。

  • antl: 封裝了國際化的方法,用於翻譯文本。

  • session: 封裝了 session 的方法,用於在請求之間存儲數據。

import type { HttpContextContract } from "@ioc:Adonis/Core/HttpContext";

export default class HellosController {
say(ctx: HttpContextContract) {
ctx.response.send("hello")
}
}

小提醒:特定物件需再安裝完套件之後才會有,例如auth, session, view, antl,這系列文章中因為主要focus在api的開發上,所以只會提到auth,對此有興趣的觀眾可以自行到官網上查詢。

小結

controller雖然不是個特別難的章節,但卻是在開發上非常重要的環節,有效使用controller好處是可以把route映射在controller的方法上,有效分離程式碼落實關注點分離,避免route做太多不關他的工作。

總而言之,AdonisJS 控制器是一個非常強大且易於使用的組件,能夠有效地組織應用程序的邏輯,並提供了豐富的功能供開發者使用。


2023年1月6日 星期五

AdonisJs第三天,Route

前言

相信大家接著看一定很疑惑為什麼第三天不是前一天講的Auth,沒錯,小編就是這麼不按照牌理出牌!沒有拉,那是考量到講Auth一定會講到Route跟Controller和Context,所以調整了一下順序,不浪費時間我們開始吧。


一切的起始

/.adonisrc.json檔案,很驚訝不是routes.ts吧,別緊張聽我娓娓道來,.adonisrc.json可以把這個檔案視為整個專案的配置檔,這裡面有配置起始必須載入的檔案,因此./start/routes就是在這個地方註冊的,不過/start/routes.ts是初始設定,不用調整,只是提一下讓你知道。至於./start/kernel是什麼東西,日後有機會我們再回來看。

"preloads": [
"./start/routes",
"./start/kernel"
],

接著來說明一下路由,簡單來說就設定是打api的uri,藉由不同的uri去呼叫不同的function處理不同的邏輯,大概就是這麼回事。

那我們來看一下routes.ts目前有什麼東西吧


Route.get('/', async () => {    
    return { hello: 'world' }
})

大家還記得前天把專案run起來,進入根目錄看到的內容嗎?沒錯就是上面這段{hello: 'world'},因此這裡可以解釋為,由根目錄進入且走GET的會執行上述方法,該方法回傳一個物件。

當然大家也可以依照專案需求,不一定要把所有路由都寫在同一個routes.ts中,可以藉由引入的方式來分類不同的路由

// /start/routes.ts
import '/start/User/routes'
import '/start/Cart/routes'
import '/start/Product/routes'

Route有內建一些實用的方法,以下小編會依照常用的一一做介紹

Methods

那既然有GET那必然也有POST, PATCH, PUT, DELETE甚至是any或多個methods

Route.post('posts', async () => {})
Route.any('report', async () => {})
Route.route('/', ['POST', 'GET'], async () => {})

Prefix

prefix就是把網址加上前綴,讓同類別功能的api更有一至性

Middleware

這觀念過兩天會有更詳細的解釋跟範例,邊簡單跟大家可以簡單理解為進入方法前的前置作業及後置作業。

Namespace

一般我們不會真的把邏輯全部寫在routes.ts裡面,這樣檔案會過於龐大且混亂,通常我們會make一個新的controller,再藉由route的第二個參數,可以指定controller的method,例如

Route.get('/api/users', 'UsersController.getList')

這意思就是指,只要符合路由,就會呼叫UsersController底下的getList方法

然而controller預設的路徑是在/App/Controllers/http底下,如果因為專案規劃希望是不同的路徑,就需要在route增加namespace告訴系統檔案在什麼地方。

Group

Group的參數為一個匿名函數,可以理解為在函數中的所有route都附加該group的效果

Route.group(() => {
Route.get('/users', 'UsersController.index')
Route.get('/posts', 'PostsController.index')
}).prefix('/api')

這樣就會宣告兩個同時擁有/api前綴的接口:GET /api/users, GET /api/posts

Params

有時會會把primary key或是參數放在網址裡面,當然這參數也可以是選填,甚至可以限制輸入的條件(where),一旦符合才會進入route

// 需填入id才會符合路由
Route.get('/posts/:id', async ({ params }) => {
return `${params.id}`
})
// id為選填
Route.get('/posts/:id?', async ({ params }) => {
if (params.id) {
return `${params.id}`
}
return 'Viewing all posts'
})

// id須為數字組成才符合路由
Route
.get('/posts/:id', async ({ params }) => {
return `${params.id}`
})
.where('id', /^[0-9]+$/)

// 也可以全域限制id的類型,且不受group限制
Route.where('id', /^[0-9]+$/)


後記

AdonisJs5開始支援typescript,很多api其實不用像以前弱型別時需要特別去記它,只要 . 下去,大部分聰明的ide就會列出可以使用的api提供選擇可以說非常方便。其實我寫出來的內容並非完整的,如果需要完整的資訊,其實官網的文件寫得我覺得算很友善了,只要你不排斥看英文,這些網誌我只是希望分享我的開發經驗與心得,分享自己較常用到的功能、基礎知識及做個紀錄。

2023年1月4日 星期三

AdonisJs第二天,Migration

前言

其實我猶豫很久第二天要講Route還是Migration,思慮再三還是決定從資料庫開始講起,畢竟這也是後端的根本,不過既然提到資料庫,那至少要準備一個資料庫,可以使用免錢的MariaDB,準備好那我們就開始吧。

AdonisJs中的Migration底層是用knex去實現的,可以理解為程式碼去設計資料庫的架構,並用指令及迭代去完成資料表的創建及修改。那既然提到資料庫,就不能不提及代表資料的最小單位Model,而AdonisJs的Model是用lucid去實現的,所以在Migration開始教學之前,還需要把資料庫相關的套件裝起來。


實作階段

安裝很簡單,依序下指令就好

npm i @adonisjs/lucid

node ace configure @adonisjs/lucid

這時會詢問你要使用哪一種DB,此時我們選擇我們準備好的 MySql/MariaDB (請依自行狀況調整)。

當下完上述兩個指令,db相關的cli就會跳出來了,但此時記得先到環境變數檔裡去設定資料庫連線的ip及帳號密碼,不然到時實行可是會因為找不到資料庫而報錯的!

設定完接著我們就可以開始設計資料表了

node ace make:migration [table_name]

這時在database/migrations 底下就會產生一個附有時間戳的檔案。這時間戳很重要,是電腦判斷順序的依據,有時順序錯誤就會報出錯誤。因此建議大部分時候請用指令來產生migration檔案

在AdonisJs裡面,大多數用指令產生的檔案,會依類型自動幫你複數化,假設是port => posts,唯獨model不會,產生檔案如下。


這時我們來看一下產生出來的內容

import BaseSchema from '@ioc:Adonis/Lucid/Schema'

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

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

/**
* Uses timestamptz for PostgreSQL and DATETIME2 for MSSQL
*/
table.timestamp('created_at', { useTz: true })
table.timestamp('updated_at', { useTz: true })
})
}

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



tableName這應該很好懂,就是到時會產生的資料表的名子

至於up跟down這裡需稍加說明migration的執行流程,跟幾個重要指令

node ace migration:run 以下簡稱run

node ace migration:rollback 以下簡稱rollback

想像建構一個資料庫像在蓋大樓,每跑一次run就代表上一個樓層(level),所以會跑up function,一次rollback就會下一個level,所以會跑down,又因為up跟down互為相反,所以內容必須完全相反才行,例如up在新增資料表,down就必須是刪除資料表,up是修改欄位名稱,down就必須改回原本名稱。

然後每一次run系統都會把migrations資料夾底下所有檔案讀取出來,扣除掉早於上次跑run的時間點(依時間戳判斷),執行剩餘的migration。

其餘的cli都是這兩個指令的延伸,為了符合更多不同的場景,大家有空可以玩一下這裡就不贅述。

以下舉個完整的例子

今天我make:migration post,然後跑一次run,此時建立post資料表,level為1,這時我在make:migration comment,再跑一次run,此時建立comment資料表,level為2,但後來我覺得comment設計得不太好,想要刪掉重來,此時下rollback就會回到level1,comment就會被drop掉。如下圖,順序由左至右



了解整個流程,我們就可以開始設計資料表了,依照表的功能進行設計,例如post代表po文,自然有po文的人(user_id),po的標題(title),po的內容(content),也會紀錄po文的時間(created_at)或修改的時間(updated_at),也給這個po文自己的unique id (id),其他的還沒想到我們可以先不想,反正之後還可以調整,那設計出來的migration大概會是這個樣子

import BaseSchema from "@ioc:Adonis/Lucid/Schema";

export default class extends BaseSchema {
protected tableName = "posts";

public async up() {
this.schema.createTable(this.tableName, (table) => {
table.increments("id");
table.integer("user_id");
table.string("title");
table.text("content");
table.timestamp("created_at", { useTz: true });
table.timestamp("updated_at", { useTz: true });
});
}

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



接著執行run,如果沒意外的話就恭喜你完成自己的第一張資料表。


後記

其實Migration跟Auth我也是很猶豫哪個要先做,但還是想說先把概念講一下,再開始實作可能觀眾會比較有概念,那接著Auth我們就明天見啦。(結果Route越排越後面....)

我直接在學習Migration時其實有想過,現在都什麼年代了,在功能越做越多,工程師越來越懶的情況下,為什麼不學NestJs或golang的gorm一樣,把model跟db schema同步就好,可以比較省事。

但實際自己在摸那兩套框架時測試,只要遇到欄位有時且無法轉換的情況下,其實還是會發生些不可預期的問題,要不是欄位無法轉成功,不然就是發生欄位被清空,例如原本是字串的欄位,突然被改成布林,這消失是必然的,當然這屬於比較極端的狀況,但如果發生沒有異動的狀況變成爛帳,會造成日後測試的困擾。

所以經過思考覺得還是資料庫這麼嚴謹的東西還是一步一腳印照著流程走最保險,這部分就別想偷懶了吧。


3/28更新一下

在上版到release甚至是prod之後,之後如果遇到要修改資料表就沒辦法再新建,而是要用alter的方式去修改,假設我要把title改成數字,並且新增一個欄位叫detail

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

public async up() {
this.schema.table(this.tableName, (table) => {
table.integer('title').alter()
table.string('detail')
})
}

public async down() {
this.schema.table(this.tableName, (table) => {
table.string('title').alter()
      table.dropColumns('detail')
})
}
}

甚至是遇到constants的資料,要再新增完表之後直接把固定資料寫入,官方有提供一個非常方便的功能可以在migration:run完之後執行,就是defer

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

public async up() {
this.schema.createTable(this.tableName, (table) => {
table.increments('id')
table.string('code')
table.timestamps()
})

this.defer(async () => {
await Task.createMany([
         { id: 1, code: 'Test' },
         { id: 2, code: 'Code' },
     ])
})
}

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

不過小編也遇過defer沒有跑,或是沒有跑完全的狀況(兩個檔案只跑了一個),目前原因不明,就還在觀察中。在此做個補充

2023年1月3日 星期二

AdonisJs第一天:Node.js版的Laravel

AdonisJs第一天,創建專案,夢想啟航

前情提要

約莫在Laravel6時期,小編還是php的小白,當時連laravel的中文書在市場上數量一隻手都數得完,剛踏入社會想著什麼都要自幹來磨練心法,後來終於意識到自己這樣慢慢磨練何時才是個頭,人生何其短,當有一定基礎後當然要站在巨人的肩膀上才看得遠,於是把市面上所有的Laravel中文書掃回家後啃了個遍。只是都還沒機會用到實際專案,便因為職涯規劃轉戰了前端工程,只有偶爾工作需要兼寫一下後端。此時我便開始思考,既然都要寫js那後端也選用Node.js好了,不僅自帶server,效能也較好(當時php7剛出)。寫了一陣子之後,發現Node.js雖然套件很多,但缺乏整合,多數時候還是要自己造輪子。於是突發奇想,會不會這麼剛好有大神開發了Node.js版的laravel框架呢?於是便開始AdonisJs這段旅程,至於為什麼身為一位前端工程師卻要來分享後端框架,那又是另外一個故事了。


勸退

Adonisjs的受歡迎程度在老實說不是名列前茅(2022年約第十名左右),甚至上網查連中文介紹都沒有(所意味著你出問題也只能看英文,但不至於你遇到的問題別人都沒遇到,至少我目前還不至於),但卻是我用起來覺得開發體驗最舒服的,特別是在我體驗過近幾年最火的框架,跟最多人用的框架後(不能說名字會被黑==),這也是我為什麼要花這麼多時間寫網誌來介紹這個框架(有看我前幾篇網誌的人應該知道,看心情更新,小編是很懶的人)。如果你跟小編一樣在意開發體驗,不在意是否社群是前幾名受歡迎,歡迎你一起加入這班車。


前置作業

請先安裝好Node.js, npm

Node.js version latest14


創建初始專案

npm init adonis-ts-app@latest [project-name]

依照你的需求可以選擇

api:不安裝view相關套件

slim:除了核心套件其他都不安裝

web:一般前後端開發

AdonisJs的前端可以使用Edge或pug語法來開發,就好像Laravel用blade語法來開發畫面一樣,不過通常我只拿來開發api,所以我們選擇api

剩下的專案名稱,是否要用eslint,就一個人開發習慣使用


cli(指令)

cli可以說是AdonisJs非常重要的一環,大部分的行為都可以,甚至是必須要用cli來執行,所以請好好的學習。

AdonisJs5之後把所有指令封裝載ace檔案裡面,指令如下,node ace,就好像Laravel用php artisan一樣,實際有的指令也會在套件安裝後增加,目前因為專案剛建立只有幾個比較基本的,執行結果如下


看到很多看不懂的指令沒關係,我們先了解幾個基本的

generate:key:產生新的app key,key會放在.env裡,初始會自動產生,參與資料的加解密

serve:開發模式

build:打包

這時我們下指令 node ace serve --watch 或 npm run dev就可以把專案在本機run起來了

這時開啟瀏覽器http://127.0.0.1:3333就可以看到初始的api

3333的port號如果被佔用或想要修改,可以藉由修改.env的PORT進行調整


小記

以上就建好一個初始專案並在本地run起來了,距離百萬年薪只差一小步了是不是很興奮啊!

剩下我們明天待續...吧


官網:https://docs.adonisjs.com/guides/introduction