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

2022年12月18日 星期日

把Object轉換為FormData

 

網路上找不到寫好的,或大部分只有一層,所以花了點時間,用遞回自己寫一個,做一下紀錄。


const objectToFormData = (object: RequestParams = {}, formData: FormData = new FormData(), parentKey?: string) => {
Object.keys(object).forEach((key) => {
const currentKey = parentKey ? `${parentKey}[${key}]` : key
if ([Array, Object].includes(object[key].constructor)) {
objectToFormData(object[key], formData, currentKey)
} else {
formData.append(currentKey, object[key])
}
})
return formData
}

// use
for (const pair of objectToFormData({
path: "health_tip",
a: {
a: "a",
b: [1],
},
b: [1, 2, 3],
file,
}).entries()) {
console.log(pair);
}

// result


2022年4月28日 星期四

檢查點擊element是否在某個範圍

 時常有dropdown(or modal)需求,在點擊空白要消失,但在dropdown範圍點擊(或移動)則保持顯示,這時不用用一堆if else去判斷滑鼠位置,只要用簡單的contains就可以判斷(以下用vue3示範)

// <template lang="pug">
div(
ref="dropdownlist",
:class="!showDropdown ? 'invisible' : ''"
)
    span test

// <script lang="ts">
// 顯示開關
const showDropdown = ref(false)
// 指定範圍物件
const dropdownlist = ref<Node>()

onMounted(() => {
  window.addEventListener(
"click",
(e: MouseEvent) => {
     // 判斷點擊物件是否在指定物件內
if (showDropdown.value && !dropdownlist.value?.contains(e.target as Node)) {
showDropdown.value = ! showDropdown.value
}
},
true // 冒泡捕捉
)
})

小結:這樣就可以輕鬆達到目的,這是我目前看到最簡單的辦法,剩下就看你怎麼應用啦


2022年3月5日 星期六

vue-cli打包出gzip, brotli檔,讓網頁讀取更加快速,順便說說chunk好了

 如果你的伺服器是用nginx架設,那你可以跳過本教學

就我理解到的差異,nginx伺服器可以線上轉譯,所以在伺服器上只需提供原始檔,在讀取檔案時就可以及時把壓縮檔演算出來

想像情境為
browser 訪問 nginx
nginx 找到 file.js
file.js 透過 nginx 轉譯為file.js.gz
file.js.gz 回傳 nginx
nginx 回傳給 browser

大概會是這麼個流程(當然是很粗略的),但是線上即時轉譯吃的是伺服器的CPU,同時壓縮比越高,佔用的效能也越大,例如brotli雖然壓縮率較gzip高,但卻相當吃效能,所以大多還是採用gzip,線上轉譯固然方便,但流量大就需要考量到效能問題

因為一些原因,絕對不是因為我懶得去學nginx配置,我使用http-server架設伺服器,打個參數-g(gzip) -b(brotli)伺服器就會該靜態檔案找尋同名壓縮檔,http-server不會幫你轉譯為壓縮格式,所以gz, br檔要自行準備,官方說br會較優先使用,其次才是gz

那要如何產生呢,這就是本篇的中點(其中也包含套件分割打包chunk的配置)

// vue.config.js 設定如下

const baseConfig = {
    ...setting
}
// 正是打包才需要壓縮,不然平常開發會爆慢
if (isProd) {
const zopfli = require("@gfx/zopfli")
var zlib = require("zlib")
baseConfig.configureWebpack = {
...baseConfig.configureWebpack,
...{
      // 把套件分割打包,可以縮小每個檔案的體積,增加讀取速度
optimization: {
runtimeChunk: "single",
splitChunks: {
chunks: "all",
maxInitialRequests: Infinity,
minSize: 0,
maxSize: 4000000,
cacheGroups: {
common: {
name: "chunk-common",
chunks: "initial",
minChunks: 2,
maxInitialRequests: 5,
minSize: 0,
priority: 1,
reuseExistingChunk: true,
enforce: true,
},
vendors: {
name: "chunk-vendors",
test: /[\\/]node_modules[\\/]/,
chunks: "initial",
priority: 2,
reuseExistingChunk: true,
enforce: true,
},
lodash: {
name: "chunk-lodash",
test: /[\\/]node_modules[\\/]lodash[\\/]/,
chunks: "all",
priority: 3,
reuseExistingChunk: true,
enforce: true,
},
styles: {
name: "styles",
test: /\.s?[ac]ss$/,
chunks: "all",
minChunks: 1,
reuseExistingChunk: true,
enforce: true,
},
},
},
},
},
}
baseConfig.pluginOptions = {
compression: {
brotli: {
filename: "[file].br[query]",
algorithm: "brotliCompress",
include: /\.(js|css|html|svg|json)(\?.*)?$/i,
compressionOptions: {
params: {
[zlib.constants.BROTLI_PARAM_QUALITY]: 11,
},
},
minRatio: 0.8,
},
gzip: {
filename: "[file].gz[query]",
algorithm: "gzip",
include: /\.(js|css|html|svg|json)(\?.*)?$/i,
minRatio: 0.8,
},
      // 目前壓縮率最高的演算法
zopfli: {
compressionOptions: {
numiterations: 15,
},
algorithm(input, compressionOptions, callback) {
return zopfli.gzip(input, compressionOptions, callback)
},
},
},
}
}
module.exports = baseConfig

最後就會產出
dist/service-worker.js
dist/service-worker.js.gz
dist/service-worker.js.br
......
雖然br, gz選一個就好了,我只是示範給大家看,絕對不是我懶得拿掉!!

小結:
當初研究這個是因為萬惡的IE載入速度太慢(謎之音:那些該死的公家機關還讓ie可以用到2029年),加上一個vendor.js有近20MB,光是要看到畫面得花上近半分鐘,無奈之下不得不研究一下加速,現在打包完,大多剩下幾byte到幾百K,事實上這也是很多人認為一般工程師與資深工程師的分水嶺,雖然我不這麼認同就是了,畢竟這些東西花上半天一天上網查就會了,比起經年累月累積起來的“實力”,我覺得後者更值得參考,不過學起來薪水多一點,何樂而不為呢?

vue cli使用pwa,所以你需要ssl憑證?

首先必須說,第一次接觸pwa對這功能有點抽象,只知道他是一個可以讓你在手機或軟體run起來的技術,這過程要分兩個階段,第一個階段要讓網站可以安裝,有個前提是網站必須是https協定,所以我們要申請ssl憑證,

小弟也不是專業維運,也不想花錢,所以參考這個網站https://kkplay3c.net/ssl-for-free/

這時你還需要一個domain,這也不難,上godaddy買一個就好了,但因為一些原因,所以我用了一個正在使用的domain,進到管理dns後,用A新加了一個subdomain,然後使用CNAME來進行dns認證,設定完大概就如下圖,CNAME TTL時間有限制3600以下


由於的前端伺服器是用http-server架,所以走https很快,下個參數就好了,指定一下cert就搞定了

再來就是manifest.json跟service-worker.js了,還有一個registerServiceWorker.js(不過這個通常cli當初會自動幫你產生,不過重新寫一個也沒這麼難拉)

要由webpack專案產出前兩個檔案(沒錯你不用從頭到尾自己寫,後面我會說為什麼不能自己寫),現在你需要的是@vue/cli-plugin-pwa,裝完你就可以在vue.config.js裡面加上pwa後開始撰寫,寫完大致上如下

pwa: {
appleMobileWebAppCapable: "yes",
appleMobileWebAppStatusBarStyle: "black",
    // 自己寫service-worker.js所以用InjectManifest,自動生成可以用GenerateSW
workboxPluginMode: "InjectManifest",
workboxOptions: { // 其餘globPatten...都已經預設好了
swSrc: "src/service-worker.js", // 指定路徑
},
manifestOptions: {
name: "hellpPWA",
description: "Hello world",
},
},

但這邊說明一下,本地測試是讀不到自定義service-worker.js的,打開chrome devtools,可以看到service-worker.js有註冊,但點進去內容卻不是自己所寫的內容


這時不用慌裝,build完,在嘗試把server run起來就會看到了,連cache也產生了

最後來說一下service-worker.js內容
話說中間一度找到google有推出workbox-cli可以透過配置產出service-worker,但vue-cli打包已經會幫忙整理出靜態檔了,再加上service-worker有些內容需要客製化,所以這個方法作罷

//下面這行在build後會自動被注入,內容包含worker class,靜態資料陣列
importScripts("/precache-manifest.f3a4893d64f1a024f6a4c41422ee032a.js", "https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js");

// cache前綴
workbox.core.setCacheNameDetails({ prefix: "CSD" })

// 取得cache name
const cacheVersion = workbox.core.cacheNames.precache

self.__precacheManifest = [].concat(self.__precacheManifest || [])
// 把靜態資料檔寫入快取
workbox.precaching.precacheAndRoute(self.__precacheManifest, {})

// service-worker安裝
self.addEventListener("install", (event) => {
console.log("[ServiceWorker] Install")
self.skipWaiting()
  // 把靜態資料寫入快取(手動)
// event.waitUntil(
// caches.open(cacheVersion).then((cache) => {
// console.log("[ServiceWorker] Caching app shell")
// return cache.addAll(self.__precacheManifest)
// })
// )
})

// service-worker啟動
self.addEventListener("activate", (event) => {
console.log("[ServiceWorker] Activate")
  // 移除其他cache
event.waitUntil(
caches.keys().then((keyList) => {
return Promise.all(
keyList.map((key) => {
if (key !== cacheVersion) {
return caches.delete(key)
}
})
)
})
)
})

// ajax時觸發
self.addEventListener("fetch", (event) => {
// console.log("[ServiceWorker] fetch")
  // GET時調用
if (event.request.method == "GET") {
// console.log(event.request)
const isOnline = navigator.onLine
event.respondWith(
      // 快取比對
caches.match(event.request).then(function (resp) {
return isOnline
          // 在線優先使用api
? fetch(event.request).then(function (response) {
return caches.open(cacheVersion).then(function (cache) {
cache.put(event.request, response.clone())
return response
})
})
          // 離線使用快取資料
: resp
})
)
}
})


以上都完成就可以在網址列看到下載按鈕拉



小結:
整套寫完下來,service-worker感覺更像是一種攔截機制,其大多數重點還是在於快取,雖然一般情況使用自動生成的service-worker已經很夠用,但我遇到這個案子比較特殊,需要再無網路環境下運行,所以有些細節要自己寫,這也讓我有機會稍稍深入研究,無論是api的快取或是靜態檔案的快取(即便有網路速度也會更快),感覺有點像是本地cdn,或許還有更多可能性我沒有用到或想到,總之也是個很有趣的經驗,不僅學會pwa也第一次申請了ssl憑證,以此作一下紀錄。

備註:
網路上有說pwa只能運行在https或localhost環境,https確定是可以,但我在試localhost常會莫名失效,最後build完用http-server run起來,又可以了,好吧~早點睡吧~

2022年2月24日 星期四

在DB裡存放emoji符號

在文字編輯器裡時時常會遇到需要輸入表情的需求(圖1)


然後送出一班會傳來後端報錯的訊息,sql無法寫入
錯誤碼:ER_TRUNCATED_WRONG_VALUE_FOR_FIELD

然後上網尋找答案原因是一個表情符號就佔用了四個字元(實際看起來會長這樣\xF0\x9F\xA4\x94)(圖2)

所以😀不單純如我想像他是個^_^組合文字
那問題來了,舊版DB charset utf8只能存放三個位元的字,所以會報錯,解決方式有兩種,第一就是轉成utf8Array,然後再轉成文字存入,取出再decode,但這個方法較為麻煩,已很容易忘記decode就會看到一串物件

所以我選用從根本解決方式,就是把db charset改成新的utf8mb4(圖3),這樣就可以直接存字個字元的文字了,程式碼也不用做什麼調整,唯一容易疏忽的就是,不光是DB要改,程式碼的連線資訊也要改(這很容易忽略)(圖4)



最終結果(圖5),打完收工!

參考網站
https://community.retool.com/t/cannot-send-emojis-through-forms/3688
https://www.jianshu.com/p/980b243fa2c3