:::

跟AdonisJs與Vue一起玩的那些事兒 / Diary about AdonisJs and Vue

17-AdonisJs_Vue_Diary_about_AdonisJs.png

繼上次的「AdonisJs聊天室」之後,AdonisJs框架 + Vue.js + Webpack組合看起來雖然很好,但實際使用還是有很多細節不盡人意。這邊繼續記錄一下開發系統的一些瑣碎事情。


關於AdonisJs /  About AdonisJs

2019-1010-212545-JETS-Quick-Start-Guides-Recipes.png

(網頁截圖:AdonisJs)

我們先來談談AdonisJs的部分。

令人困惑的資料庫操作 / Confusing database management

我在前一篇有講到AdonisJs對於控制資料庫的ORM有不錯的包裝。但實際使用之後,才發現它有很多細節並不相同。

首先,在AdonisJs裡面與資料庫相關的類別有三種:

  1. Model模型物件是指LUCID,它的原始碼跟可用的方法請看adonis-lucid/src/Lucid/Model/index.js
  2. Model可用的指令大部分,特別是query(),皆會回傳查詢物件Query Builder,它的原始碼則是adonis-lucid/src/Lucid/QueryBuilder/index.js
  3. 查詢物件Query Builder,搭配await 執行fetch()查詢之後會回傳結果物件Serializer。它的原始碼是adonis-lucid/src/Lucid/Serializers/Vanilla.js。而結果物件中的屬性rows陣列中,才會包含Models。

這三種不同類別可用的方法都不相同,這造成實務開發的時候時常令人混亂。我現在操作的變數到底是Model?還是QueryBuilder?還是Serializer?

有時候QueryBuilder忘記加上fetch()時,AdonisJs的防呆會提示你要做這個動作。但如果fetch()忘記搭配await的時候,它回傳就是Promise物件,並不是我們預期中的Serializer查詢結果物件。這些小細節讓人在撰寫的時候時常碰壁。

要怎麼從LUCID模型結合多個資料表呢? / How to join multiple tables from LUCID model?

HasManyThrough_dcr86k.png

(圖片來源:AdonisJs Relationships)

不過最令我困擾的,大概就是三個以上資料表串接的情境了。

LUCID模型有提供Many Through的方法。如上圖所示,我們可以從Country模型 (資料表為countries)開始,透過User模型(資料表為users)來查詢Post模型(資料表為posts)。這個方法叫做Many Through,它的原始碼在HasManyThrough.js裡面。

我們可以用Many Through從上層模型查到下層模型,那如果我們要從下層模型找到它的上層模型,甚至是要找到上層模型其他的下層模型時,那得怎麼做才行呢?從LUCID Model的原始碼來看,它似乎是沒有Many Through之外的Through方法了。

當然,我們可以轉而使用QueryBuilder的Joins方法,但這時候回傳的就不是Model,似乎是普通的JSON物件,這就喪失了Model的眾多功能。我們也是可以用id再去用靜態方法find(id)來找到Model,但這樣會造成大量的SQL查詢,拖慢效能。

Lucid Models: How to access linked models?這篇中,發問者就有提到它想用以下方法來取得Door底下的logs,但以下程式碼是不能運作的。

Route.get('test', async () => {
return await Door.find(1).logs()
})

這是為什麼呢?因為在find(1)的時候會回傳的是查詢物件QueryBuilder。而QueryBuilder要搭配await跟fetch()方法才能回傳查詢結果Serializer。再來Serializer還要用first()或是rows[0]才能找到Door Model,然後才能用Door Model的logs()方法。

該篇下面rohitdalal67有提出一個解決方法,就是從DoorLog Model使用QueryBuilder,用join跟where來取得的方式。這個思維可以解決多資料表之間查詢的問題,但是程式碼非常難以閱讀,讓大家見識見識:

const report = await Report.query().select('*')
    .with('report_categories')
    .scope('report_categories', (builder) => {
        builder.select('report_categories.*','categories.image as category_image');
        builder.where('deleted', '0');
        builder.join('categories','categories.id','report_categories.category_id');
    })
    .where('id','=',data.report_id)
    .first();

嗯,我可以寫SQL就好嗎?

大家對於資料庫的需求千變萬化,AdonisJs並非一個專門處理資料庫的框架,我可以理解它的不足之處。它的LUCID Model已經能夠處理80%常見的資料庫問題,這點就讓我十分感謝了。

剩下20%,需要處理多資料表的時候。我再像想辦法吧。

WebSockets可以CORS嗎? / Could I use WebSockets between cross domains?

websockets.jpg

(圖片來源:一文讓你搞懂WebSocket原理)

之前使用的Feathers框架,它預設的前後端溝通方式是用WebSockets。用WebSockets建構的聊天室,運作起來非常輕快、流暢。

接下來我在研究Feathers能不能支援讓客戶端從不同網站上跟伺服器交換資訊,也就是跨網域資源共用(Cross-Origin Resource Sharining, CORS)的時候,發現Feathers以WebSockets客戶端Socket.io並不能支援CORS,但它的REST Client在調整後則是可以使用。

因為這件事情,讓我以為WebSockets只能用在同網域上。後來看到一篇文章在討論WebSockets時提到安全性問題中對於跨網域的處理方式時,我才發現WebSockets似乎實際上並沒有這方面的限制。當時可能還是Feathers框架內部的設定與資安限制,才導致CORS的時候不能使用。

這就讓我想要試試看AdonisJs的WebSocket功能。教學裡面有提到怎麽用WebSocket建立聊天室,我先試著把文件提到的檔案建立起來,不過在用指令「adonis install @adonisjs/websocket」安裝WebSocket套件時,我的其他套件就出現了各種問題。

裝套件裝到壞掉 / "Module not found" error

我在研究AdonisJs的File Storage跟WebSocket的時候,都需要用「adonis install」指令安裝套件。但這些套件安裝下來,不知為何都會導致我專案中其他的套件發生「Module not found」錯誤。

最麻煩的是,裝到最後連「adonis」指令都不能使用,發生了以下錯誤:

TypeError: Class extends value undefined is not a constructor or null

class BaseCommand extends ace.Command」這串討論中,最後提出的解決方法是重新安裝@adonisjs/ace套件,但要注意的是,因為我們一開始的@adonisjs/cli是安裝在全域目錄,所以這時候也要用「npm -g i @adonisjs/ace」在全域目錄安裝才行。不過實際上我並不確定這是不是最佳的解法,我嘗試了裝在專案裡面、用npm-link連接等等各種方法,最後不知為何又能繼續運作了。

對於其他套件中遇到「Module not found」錯誤的處理方式,我就整理成另一篇「Node.js找不到模組?在npm-link底下的處理方法」說明。

Node.js仰賴大量套件,套件又分成適用於專案內部的區域套件、安裝在全域目錄的全域套件、從專案內部連接到全域目錄的套件。大部分套件可以安裝在專案內的區域目錄,不過也有很多套件必須以全域目錄安裝,這時候又會跟npm-link連結的套件相互衝突。當這些套件出現了相依問題時,往往令人覺得十分棘手啊。


聊聊Vue.js / About Vue.js

Vue.JS_Logo_transparent_PNG.png

(圖片來源:StickPNG)

講完了後端的AdonisJs框架,再來講講前端的Vue.js。

組件的插槽 / Slot of component

Vue.js能夠用大量組件(component)所組成。而組件有許多可供客製化調整的設定,包括屬性(prop)、事件(event)、甚至是組件的內容,也就是插槽(slot)。以下我就直接用slot來講插槽這件事情。

slot中最好用的是帶有名稱的slot (Named Slots),可以視為組件模板中部分填空位置。在組件樣板中可以這樣寫:

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
</div>

紅字的<slot name="header" />標籤,表示可以供人客製化內容的區塊。

接下來,要用該組件的時候,我們可以在組件設定中指定<slot name="header"/>要顯示的內容:

<base-layout>
  <template v-slot:header>
    <h1>Here might be a page title</h1>
  </template>
</base-layout>

<template>標籤的屬性「v-slot:header」的意思,就是要將<template>標籤的內容放到組件內<slot name="header">裡面。

inline-template屬性 / <template> with "inline-template"

原本帶有名字的slot使用起來應該很直覺,不過實際使用的時候我卻遭遇了一些問題。奇怪的是,現在這個問題又消失了。我還是在這邊記錄一下我遇到的狀況吧。

如果我們要在<template>裡面做資料綁定的話,直接用需要編譯的語法,就會跳出錯誤訊息。舉例來說,我們的VM中有title資料。在原本的樣板中寫法是:

<h1>{{ title }}</h1>

但如果在組件的slot中用{{ title }}的時候,這樣寫會發生錯誤:

<base-layout>
  <template v-slot:header>
    <h1>{{ title }}</h1>
  </template>
</base-layout>

Vue.js官方的Slots說明中有講到插槽作用域slot-scope等一些細節,但看起來這似乎是已經要捨棄的特色,我看不太懂怎麼用。最後我是在「VueJS 元件載入模板 (template) 的幾種方式」這篇中找到了inline-template的用法。

為<template>加上inline-template之後,<template>的內容就由上層的VM來管理,因此就能夠使用{{ title }}綁定資料了。改寫後的語法如下:

<base-layout>
  <template v-slot:header inline-template>
    <h1>{{ title }}</h1>
  </template>
</base-layout>

雖然是這樣說,到昨天為止,我的確需要加上inline-template屬性才能正常執行。但今天拿掉inline-template來測試看看,居然也沒發生什麼問題,執行起來很正常。難道是我更新Vue.js版本了嗎?不太確定發生了什麼事情。

錯誤處理 / Error handle

2019-1018-222153-Domaian-Management-Database-Error.png

這段期間我做得最有成就感的功能,大概就是這個錯誤處理了。

開發初期不可避免的就是發生錯誤。那發生錯誤的時候,我們需要知道錯誤內容是什麼、發生錯誤的檔案在那裡、發生錯誤時的情境為何。以往我們必須仰賴瀏覽器開發者工具的Console檢視錯誤,或是用Network檢視伺服器端的錯誤訊息。這些頻繁的操作都會讓偵錯、修正變得令人十分煩躁,所以我決定來做個比較容易觀看的錯誤處理功能。

首先,在系統中的錯誤可能來自三個地方:

  1. 客戶端Vue.js之內發生的錯誤。
  2. 客戶端Vue.js以外發生的錯誤。
  3. 伺服器端的錯誤。這邊是指我用AdonisJs框架提供的服務。

這三者處理的方式不大相同。我的目標是取得不同地方發生的錯誤,把它儲存在error資料中,再放到專門顯示錯誤的Vue組件中顯示內容。

前兩者在客戶端發生的錯誤,我參考了「Handling Errors in Vue.js」這篇的做法,對於第一個情況:在Vue.js之內的錯誤,可以用以下方式處理:

Vue.config.errorHandler  = function(err, vm, info) {
  VueController.data.error = err
}

第二個情況,在Vue.js以外發生的錯誤,則可以用以下方式處理:

window.onerror = function(message, source, lineno, colno, error) {
  VueController.data.error = error
}

最後第三個情況,因為我們跟伺服器溝通所使用的工具是axios,所以我們要在axios之外,使用try catch來捕捉錯誤。寫法例如:

try {
  let result = await axios.get(path, options)
  return result.data
}
catch (error) {
  VueController.data.error = error
}

2019-1018-223345-Database-Error-atDomain-ist-app.png

最後就是在顯示錯誤的組件中剖析錯誤訊息error物件,一一將訊息顯示在畫面上。對於遠端伺服器的錯誤訊息方面,使用AdonisJs的HttpException類別可以將錯誤訊息以JSON的方式回傳,這時候error.response.data.error中就可以找到它回傳的訊息以及發生錯誤的檔案。再來客戶端的錯誤則可以從error.message和error.stack當中取得。如果是伺服器端的錯誤,我還做了個重複發送http請求的功能,當伺服器端修正之後就可以再次發送相同的資訊,看看能不能順利執行。

不過錯誤處理的功能還不算完成。目前還有兩個問題有待解決。

2019-1018-224224-DR-emmg-Cons-Network-Sources-el.png

第一個問題是我用前述三種方法捕捉到的錯誤,裡面的資訊顯然還是沒有開發者工具的console豐富。我用來顯示錯誤的組件雖然有列出錯誤發生在那個檔案,但也只有三行,而且缺乏source map的支援,沒辦法分辨這個錯誤到底是發生在編譯前的那個檔案裡面。右邊console的資訊顯然就準確許多。

如果要準確偵錯的話,開發者工具還是不可或缺的吧。

2019-1018-224641-ton-WebpageListhtml-Webpagejs-WebpageListjs.png

另一個問題是,目前我在AdonisJs框架裡面得用HttpException建立錯誤,不能直接用throw把錯誤丟出去。還要多引用一個HttpException有點麻煩。我有注意到AdonisJs框架裡面可以自訂錯誤處理Error Handling的方法,昨天試了一下,可惜並沒有成功。


結語 / In closing

總之,目前還有很多問題有待克服。不過程式寫到一定進度,我就會覺得有必要先做個記錄,這樣我才能安心繼續處理其它有待克服的問題。

這篇就這樣記錄一下遭遇的問題以及處理方法,我就繼續跟AdonisJs與Vue.js一起奮鬥吧。