TL;DR
Tweetの文字数をカウントできる拡張機能を開発しました。
背景
弊社で運営しているFashion Tech Newsでは、記事公開のタイミングに合わせてZapierからTweetを実行しています。Tweetの一部の文章は、記事の管理も行っているmicroCMSに登録し、RSSを利用してZapierに渡しています。Tweetの文字数がオーバーした場合、Zapierでエラーが出て中断されます。そのためmicroCMSに文章を登録する際に、文字数がオーバーしないように気を付ける必要があり、従来は手動で様々な項目をTwitterのウェブアプリにコピペして文字数の確認を行っていました。
そこで、記事を管理しているページを開くと自動的にTweetの文字数を表示する拡張機能を作成しました。
Tweetの例:
【Fashion Tech News】
— Fashion Tech News (@zt_ftn) December 2, 2022
映画公開で人気が再燃! 「SLAM DUNK」と「エア ジョーダン6」の蜜月関係
映画「THE FIRST SLAM DUNK」が12/3公開!!#THEFIRSTSLAMDUNK #SLAMDUNK #スラムダンク #ジョーダン6https://t.co/BQr3tY0tGT
ポイント
- SPAの画面遷移に対応する
- 文字数のカウント方法はTwitterのルールをそのまま活用する(全角の換算方法、URLなどの換算方法など)
- CMS内の複数項目をTweetに含めて、変更ごとにカウントをし直して表示する
手順・実装
スタートガイドなどで紹介される、基本的なcontent scriptを利用すると、SPAの画面遷移を検知できません。そのため今回はbackground scriptを利用して画面遷移を検知し、content scriptで画面への描画を行うという方法を採用しました。
フォルダ構造は以下です。
今回Twitterの文字数カウントをそのまま活用するために、twitter-text
というNPMパッケージを利用しました。そのためBrowserifyを利用し、src/
内のファイルをBuildしてscripts/
に生成するようにしました。
├── scripts/
├── images/
├── node_modules/
├── scripts/
├── src
│ ├── background.js
│ └── content.js
├── README.md
├── manifest.json
├── package-lock.json
└── package.json
manifest.jsonは以下で、background scriptの読み込みを行っています。その後許可するホストと、許可する行為を指定しています。
{
"manifest_version": 3,
"name": "Tweet Counter Extension",
"description": "Displays the number of tweet characters",
"version": "1.0",
"icons": {
"16": "images/icon-16.png",
"32": "images/icon-32.png",
"48": "images/icon-48.png",
"128": "images/icon-128.png"
},
"background": {
"service_worker": "scripts/background.js"
},
"host_permissions": ["https://CMSのURL/*"],
"permissions": ["tabs", "scripting"]
}
参考までにpackage.jsonも載せておきます。
{
"name": "tweet-counter-extension",
"version": "1.0.0",
"description": "Displays the number of tweet characters",
"scripts": {
"build": "browserify src/content.js -o scripts/content.js && browserify src/background.js -o scripts/background.js"
},
"license": "ISC",
"devDependencies": {
"browserify": "^17.0.0",
"twitter-text": "^3.1.0"
}
}
background scriptはbackground.js
としました。chrome.tabs.onUpdated
で画面遷移を検知し、Tweetの文字数を表示させたい、記事を管理するページの場合のみ、content.js
を実行するというものです。こうすることでSPAの画面遷移でも、ページごとにcontent.jsを実行することができます。
chrome.tabs.onUpdated.addListener(function (tabId, info, tab) {
const urlConditions = tab.url.indexOf("https://記事を管理するページ/") !== -1
if (info.status === 'complete' && urlConditions) {
chrome.scripting.executeScript({
target: { tabId: tabId },
files: ["scripts/content.js"]
},);
}
});
実行されるcontent scriptのcontent.js
は以下です。
①twitter-text
のパッケージを読み込みます。
②Zapierで毎回追加される、【Fashion Tech News】
の見出しやURLの一例を設定します。
③Tweetに追加される、タイトル・本文をクラス名の前方一致で取得します。(前方一致で取得する理由は、ツールなどによりBuildごとに異なる文字列などが付加されるためです。)
④Tweetの文字数を表示する場所に隣接する要素を取得すると共に、その新たに追加する要素に設定するid
を決めます。
⑤updateBadge
の関数では、tweet
の変数に実際のTweetの内容を定義し文字数のカウントを行います。文字数がオーバーしている場合には警告の意味を込めて赤背景・白抜き文字で表示します。
⑥SPAの場合読み込みに少し時間がかかることがあるため、上記のupdateBadge
などの処理は目的の要素が読み込まれてから実行します。
⑦読み込まれるまでsetInteval
を活用して1秒ごとに実行を試みて、読み込まれた後にclearInteval
で繰り返し実行をキャンセルするようにします。
⑧初期実行が終わった後は、フォームなどの変更に応じて描画し直せるように、Event Listner追加します。
const main = () => {
const jsLoaded = () => {
// ①パッケージの読み込み
const twitter = require("twitter-text")
// ②Tweetに毎回記載されている部分を定義
const tweetHeading = "【Fashion Tech News】"
const exampleUrl = "https://fashiontechnews.zozo.com/" // URLs are shortened and converted to 12 characters in full-width
// ③Tweetに追加される要素を取得
const articleTitle = document.querySelector("[class^='タイトルのフォームのクラス名一部']")
const tweetForm = document.querySelector("[class^='Tweet本文のフォームのクラス名の一部']")
// ④文字数を表示する要素に隣接する要素の取得と、idの設定
const insertingArea = document.querySelector("[class^='contentActions_buttons']")
const badgeId = "tweet-count"
// ⑤文字数のカウントと描画を担当する関数
const updateBadge = () => {
const tweet = `${tweetHeading}\n${articleTitle.value}\n\n${tweetForm.value}\n${exampleUrl}`
const parseData = twitter.parseTweet(tweet)
const characterCountInFullWidth = Math.ceil(parseData.weightedLength / 2)
const isOvered = characterCountInFullWidth > 140
const updatingBadge = document.querySelector(`#${badgeId}`)
updatingBadge.textContent = `Tweet: ${characterCountInFullWidth}文字`
updatingBadge.style.color = isOvered ? "#fff" : "#21213b"
updatingBadge.style.backgroundColor = isOvered ? "#c00" : "#fff"
}
// ⑥目的の要素が読み込まれてから実行
if (insertingArea != null && articleTitle != null && tweetForm != null) {
// ⑦目的の要素が読み込まれた後は繰り返し実行をキャンセル
clearInterval(jsInitCheckTimer)
const badge = document.createElement("p")
badge.id = badgeId
badge.style.padding = "8px"
badge.style.marginRight = "16px"
badge.style.textAlign = "right"
badge.style.fontWeight = "700"
insertingArea.insertAdjacentElement("afterbegin", badge)
updateBadge()
// ⑧フォームにEvent Listnerを追加し変更に応じて文字数の描画し直す
articleTitle.addEventListener("input", updateBadge)
tweetForm.addEventListener("input", updateBadge)
}
}
// Iterate until the target DOM is ready
const jsInitCheckTimer = setInterval(jsLoaded, 1000)
}
main()
注意点
画面遷移を検知するのがbackground.js、実際に描画するのがcontent.jsという実装を行いました。その結果、上記⑦のclearInterval
が実行される前に画面遷移が行われると、content.js
ではそれを検知できず、updateBadge
が複数実行され、文字数の表示が複数になってしまう問題があります。今回は社内ツールということもあり、問題が発生するページを除外するという対応策にとどめました。
参考
- Chrome Extension getting started guides:公式のもので、デモコードもあり分かりやすかったです。
- 【Chrome拡張】SPAやSSRのWebページでページ遷移を検知する:background scriptについて詳しく解説がありました。
- 動的なページの読み込みが完了してからChrome拡張機能を実行する方法