瀏覽器bfcache

bfcache是“back/foward cache”的縮寫。當頁面「符合某些條件」的時候,當使用者按了瀏覽器的上下頁,瀏覽器可以快速回復剛剛瀏覽的頁面。且因為bfcache做cache的方式是對整個頁面做快照(包含了Javascript Heap(用來儲存變數、函式的地方)),因此會包含剛剛所做的改變,例如變更過的DOM內容、Javascript的執行結果等等。同時,因為不需要再次載入資源,所以速度超快。

因為Javascript也被緩存的關係,假設我們是用vue.js開發前端頁面,當離開頁面後再返回時,就勢必不會重新觸發fetch, data, asyncData, mounted, created…等等的Methods與Lifecycle Hooks。

可快取條件

目前我測起來,各個瀏覽器可bfcache的條件似乎不太一樣。例如在Chrome中點擊了File Input就會無法cache,但在ios的Safari中並不會有問題。以下把查到的以及自己測試出來的整理出來。

  1. 不要使用unload事件:可以改用pagehide替代。
  2. 有條件地使用beforeunload事件:不使用匿名函數,且使用完後立刻removeEventListener是可以的。
  3. 標頭不要有Cache-Control: no-store
  4. 不要使用window.open()開啟新視窗或分頁,改用a標籤並指定target是_blank並加上rel=”noopener”
  5. 不要使用File Input, WebSocket, WebRTC, WebAuthetication API, IndexedDB, Web Locks API

測試方式

因為WebSocket不能用,因此若有使用Hot Module Replacement的開發環境,例如Vue, React,在開發模式時無法使用bfcache也是正常的。因此測試時最好以相同於正式環境的方式來測試。

打開Chrome的console,切換到Application頁籤,左邊找到Background Services,點選Back/foward cache。針對要測試的頁面點選Test back/foward cache按鈕。點選後頁面會快速地來回chrome://terms/與當前頁面,並回應是否成功。若失敗的話則會在下方顯示錯誤的原因。

用瀏覽器console批次移除Jenkins建置紀錄

某天公司Jenkins的容量爆了,而那天有權限進那台機器的同事剛好請假。唯一能做的只能從前端一筆一筆刪掉。但這樣刪,不知道要刪到民國幾年。於是看看他刪除是怎麼打的,然後用一個回圈批次把一個branch中的所有舊建置都刪掉。

沒權限進主機以外,懶得進主機,也可以用這個方式來刪除。原始碼如下:

const jenkinsCrumb = 'your jenkins crumb'

async function remove (domain, project, branch, from, to) {
  for (let i = from; i < to; i++) {
    const url = `https://${domain}/job/${project}/job/${branch}/${i}/doDelete`
    const form = new FormData()
    form.set('Jenkins-Crumb', jenkinsCrumb)
    const body = new URLSearchParams(form)
    await fetch(url, {body, method: 'POST', headers: {
      'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
    }})
  }
  alert('刪除完畢')
}

remove('your.jenkins.domain', 'my-repo', 'development', 1, 300)

先將程式碼複製下來,打開Jenkins頁面,打開瀏覽器console,找到head標籤的data-crumb-value把這段token(黃色處)複製起來。貼到const jenkinsCrumb的引號中。

這段token每次當我們登入Jenkins時都會變,所以你可以每次要執行之前都先更新一下,避免後面執行失敗。接著,我們開始修改 remove(‘your.ci.domain’, ‘my-repo’, ‘development’, 1, 300) 這行。

首先把你Jenkins的domain複製起來,替換掉’your.ci.domain’。接著進入要刪除的專案與branch,複製專案名稱與branch名稱,替換掉’my-repo’與’development’。

接著,看看畫面左下的建置號碼。目前最新的是85,假設我想刪掉1~84,我就把後面的1, 300替換成1, 85

接下來,打開瀏覽器console,把這段改好的code貼入console按下Enter即可。fetch的地方我用非同步的方式,是因為想讓一筆刪完後,在刪除下一筆。雖然會比較花時間,但數量一多才不會灌爆Jenkins。接下來只要等待刪完時彈出的「刪除完畢」視窗就大功告成了!

Line Bot 串接 OpenAI API

最近很紅的ChatGPT,如果也能在Line上面使用的話,是不是感覺更方便呢?我們就來串接ChatGPT的開發公司OpenAI提供的API到Line Bot上吧!

這個範例會用Express起一個web server,建立一個method POST且path為/webhook的route作為line的webhook,用來接收line傳來的events

首先你需要先到 Line developers console 點選「Create a new channel」建立一個新的Channel,類型選擇「Messaging API」。填寫完成送出後進入Channel設定頁,複製最下方的「Channel secret」。回到上方選擇「Messaging API」頁籤,複製最下方的「Channel access token」。

用NPM安裝 openai, @line/bot-sdk, express,require進檔案中

const { Configuration, OpenAIApi } = require('openai')
const line = require('@line/bot-sdk')
const express = require('express')

建立Line API需要的Config,將剛剛Line Channel的Token跟Secret填入,範例以環境變數帶入。

// line config
const lineConfig = {
  channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN,
  channelSecret: process.env.CHANNEL_SECRET
}

建立express,設定line bot所需的webhook路由以及訊息事件的handler。將程式碼上到主機上後,就可以繼續完成Line Bot的設定了。

const app = express()

// line webhook
app.post('/webhook', line.middleware(lineConfig), (req, res) => {
  Promise
    .all(req.body.events.map(lineEventHandler))
    .then((result) => res.json(result))
    .catch((err) => {
      console.error(err)
      res.status(500).end()
    })
})

// line message handler
function lineEventHandler (event) {
  const lineClient = new line.Client(lineConfig)

  return lineClient.replyMessage(event.replyToken, {
    type: 'text',
    text: 'Bot收到訊息囉'
  })
}

app.listen(3000)

Line developers console 選取剛剛新增的Channel,點選「Messaging API」頁簽,在下方的「Webhook URL」點選「edit」,填入你的Webhook網址並按下「Update」,完成後按下「Verify」,就可以檢查webhook有沒有正常運作了。

接下來首先要到OpenAI 註冊你的帳號,註冊完後,到API Keys頁面,點選「Create new secret key」,複製API Key,回到程式碼,建立OpenAI API需要的Config填入剛剛的API Key,範例以環境變數帶入。

const openAiConfig = new Configuration({
  apiKey: process.env.OPENAI_API_KEY,
})

新增一個function負責傳遞訊息給OpenAI API,並等待回傳的訊息。

async function askOpenAI (question) {
  const openai = new OpenAIApi(openAiConfig)

  // request
  try {
    // 使用text-davinci-003 model
    // const completion = await openai.createCompletion({
    //   model: 'text-davinci-003', // 看你要使用哪個model: https://platform.openai.com/docs/models
    //   max_tokens: 100,
    //   prompt: question,
    //   temperature: 0.1 // 這是一個 0 - 1 的數值,越靠近1回答會更多樣性,越靠近0回答會更可靠
    // })
    // return completion.data.choices[0].text.replace(/^\n+/g, '')

    // 使用gpt-3.5-turbo model (這個比較強)
    const completion = await openai.createChatCompletion({
      model: 'gpt-3.5-turbo',
      messages: [{ role: 'user', content: question }]
    })
    return completion.data.choices[0].message.content.replace(/^\n+/g, '')
  } catch (err) {
    console.log(err)
    return '抱歉我不知道'
  }
}

將剛剛的Handler function改成這樣,注意function的前面有加上了 async

async function lineEventHandler (event) {
  const lineClient = new line.Client(lineConfig)

  if (event.type !== 'message' || event.message.type !== 'text') {
    return Promise.resolve(null)
  }

  const result = await askOpenAI(event.message.text)
  // 將OpenAI回的訊息傳給使用者
  return lineClient.replyMessage(event.replyToken, {
    type: 'text',
    text: result
  })
}

最後,因為我不想讓他每次都回我,尤其是如果將他加入群聊的時候,可能會很吵,因此我在Handler中再加入了一個判斷

  if (event.type !== 'message' || event.message.type !== 'text') {
    return Promise.resolve(null)
  }

// 改為

  if (event.type !== 'message' || event.message.type !== 'text' || !event.message.text.includes('村長')) {
    return Promise.resolve(null)
  }

這樣子的話,就只有訊息中包含了村長兩個字時,才會將訊息傳給OpenAI等回應。

MySQL 筆記

新增使用者

mysql> CREATE USER 'user_name'@'localhost' IDENTIFIED WITH mysql_native_password BY 'your_password';

移除使用者

mysql> DROP USER 'user_name'@'localhost'

修改使用者密碼

mysql> ALTER USER 'user_name'@'localhost' IDENTIFIED WITH mysql_native_password BY 'your_password';

查看使用者權限

mysql> SHOW grants for 'user_name'@'localhost';

授權使用者權限

#針對某資料庫開部分權限
maysql> GRANT SELECT,INSERT,UPDATE,DELETE,CREATE ON db_name.*(or table_name) TO 'user_name'@'localhost';

#針對某資料庫開所有權限
maysql> GRANT ALL PRIVILEGES ON db_name.*(or table_name) TO 'user_name'@'localhost';

#針對所有資料庫開所有權限
maysql> GRANT ALL PRIVILEGES ON *.* TO 'user_name'@'localhost';

重整關於權限的資料表

mysql> flush privileges;

Ubuntu ssh 安全性設定

起一台新的機器後,為了確保機器基本上有一定的安全性,我們可以在ssh登入這個環節加入一些門檻,增加登入的難度。

預計要做的設定有:更改ssh port號 -> 更改登入帳號 -> 關閉root登入 -> 關閉密碼登入

更改ssh port號

就像改門牌地址一樣,當對方不知道門牌地址時,就要一個一個門鈴去按按看,才知道要找的人住在哪間。當不知到ssh port號,要暴力登入時,就必須要一個一個port去試。

打開並修改ssh config:sudo vim /etc/ssh/sshd_config

找到 #Port 22,移除開頭的「#」號,把後面22改成想要的port號即可(列出使用中的port號 ss -tulpn | grep LISTEN 沒有列出的都可以用)。另外還可以新增 Protocol 2,使用更安全的ver2協定。修改完成後要重啟ssh服務:sudo systemctl restart ssh

重要:如果ufw防火牆有開的話(可以用 sudo ufw status 來檢查是否有開)記得要加上新的rule:sudo ufw allow <new ssh port>

更改登入帳號

首先先用 adduser <user name> 新增使用者名稱。room number, work phone, home phone, other 為選填。接著允許使用者使用sudo指令 usermod -aG sudo <user name>。再次打開剛剛ssh config文件 sudo vim /etc/ssh/sshd_config,新增一行 AllowUsers <user name>,修改完成後要重啟ssh服務:sudo systemctl restart ssh

關閉root登入

一樣打開ssh config sudo vim /etc/ssh/sshd_config,找到PermitRootLogin yes,改成no,修改完成後要重啟ssh服務:sudo systemctl restart ssh

關閉ssh密碼登入

這一步要介紹如何關閉密碼登入,只使用ssh key登入。只使用ssh key登入的好處是不會被try密碼,也省去打密碼的不便。不過,當不是使用自己的裝置登入,或是需要新增一台裝置時,稍微有點麻煩。

但這個步驟要小心一點,未測試完成前,請勿關閉console。若不慎無法登入時,可以試試看vps管理介面中的console功能來進入主機。

首先為剛剛新建立的新使用者,新增 /home/<user name>/.ssh 目錄,在這個目錄中新增 authorized_keys 檔案。authorized_keys中要填入要登入的機器的ssh key,長得像這樣:ssh-rsa AAAC4SDFasdfsdfsdfAAAA/xfvdfevvf.......sdfsdf= xxx@macbook.office

在要登入的電腦上用command line輸入 ssh-keygen,來產生ssh key。完成後輸入 cat ~/.ssh/id_rsa.pub 顯示key並將他複製起來。打開剛剛主機上的authorized_keys檔案,貼上key,多筆則用斷行來分開。

一樣打開ssh config sudo vim /etc/ssh/sshd_config,修改 PasswordAuthentication yesno,然後再重啟ssh服務:sudo systemctl restart ssh

更多保護措施

上面提到的ufw如果沒開的話,建議可以開一下,指令也相當簡單直覺。記得新增所需要的rules。另外,建議可以安裝fail2ban,若有人一直重複在嘗試登入,就會被暫時擋起來。