WordPress 外掛開發日記 (一) – 架構

一年前的這個時候,從早到晚都在跟 WordPress 的佈景主題打交道,除了忙案子以外,空檔時就在研究哪些前端框架可以更快速的與 WordPress 做整合,想不到十二個月過去了,現在每天都改成在寫外掛,離前端程式開發越來越遠,停下腳步回頭來看,感覺是該來好好整理一下這陣子的開發心得了。

為了方便閱讀我拆成以下幾個章節來紀錄:

一、WordPress 外掛架構 – 紀錄曾經使用過的架構以及目前作法
二、WordPress 開發實用 Composer 套件 – 介紹在寫外掛的時候可以加速開發的套件
三、載入佈景主題範本 – 如何在外掛裡面使用範本檔修改前端介面並整合前端開發流程
四、整合 PHPUnit 單元測試 – 避免改東牆壞西牆的問題再次發生
五、自動化部署 – 讓 WordPress 搭上 DevOps 的列車?
六、WordPress 外掛更新 – 自行管理未上架 wordpress.org 的外掛版本維護

如果是對 WordPress 外掛開發完全沒概念的朋友,我建議先閱讀 Plugin Developer Handbook 會比較完整,或是買這本書來看在學習上會比較有脈絡與系統性。而這系列文是我的實務經驗,所以很多地方可能還是會有不足之處,希望能遇到大大再幫我指點迷津了 ?

第一篇文章主要紀錄了我在組織外掛結構的過程,並透過不斷的打掉重練來找到自己最舒服的方式,讓我們開始吧~

雖然不知道出來混為何一定要還,但還是手癢開始把這一年多來改寫外掛的日常紀錄一下XD,從開始入坑 Composer 到學著寫下第一行單元測試,再到外掛的自管更新以及自動化部署,寫到一半發現餅華太大,只好拆成多篇來寫 > < 第一篇介紹目前我慣用的 Starter Plugin ( 好像沒這種說法XD ) 與工作流程,以及隨著時間演進的組織方法,所以萬一明天點開來看發現跟今天看到的內容不一樣還請見諒,文章也是需要重構的XD 文中如有不足之處還煩請大大指點迷津了 ?

WordPress 外掛架構

以佈景主題來說,有很多公司推出初始佈景主題 ( Starter Theme ) 可以入門,相較之下外掛就少很多,原因可能是外掛的組織方式太自由了,基本上我可以把所有的程式碼全都塞進到帶有標頭的 PHP 檔案即可,不需要拆分任何檔案與資料夾,但如果是做功能複雜或是大型外掛的擴充,這樣做會被未來的自己罵到臭頭XD

剛開始實作的時候我是採用「 WordPress Plugin Boilerplate Generator」,不管要做的功能是大還是小,我覺得遵循這套架構可以讓檔案很有邏輯與系統,它的特色是在於把前後台的資料夾切分開來,然後透過一層又一層的類別來做載入。

── LICENSE.txt
├── README.txt
├── admin
│   ├── class-my-plugin-admin.php
│   ├── css
│   │    └── my-plugin-admin.css
│   ├── index.php
│   ├── js
│   │   └── my-plugin-admin.js
│   └── partials
│       └── my-plugin-admin-display.php
├── includes
│   ├── class-my-plugin-activator.php
│   ├── class-my-plugin-deactivator.php
│   ├── class-my-plugin-i18n.php
│   ├── class-my-plugin-loader.php
── LICENSE.txt
├── README.txt
├── admin
│   ├── class-my-plugin-admin.php
│   ├── css
│   │   └── my-plugin-admin.css
│   ├── index.php
│   ├── js
│   │   └── my-plugin-admin.js
│   └── partials
│       └── my-plugin-admin-display.php
├── includes
│   ├── class-my-plugin-activator.php
│   ├── class-my-plugin-deactivator.php
│   ├── class-my-plugin-i18n.php
│   ├── class-my-plugin-loader.php
│   ├── class-my-plugin.php
│   └── index.php
├── index.php
├── languages
│   └── my-plugin.pot
├── my-plugin.php
├── public
│   ├── class-my-plugin-public.php
│   ├── css
│   │   └── my-plugin-public.css
│   ├── index.php
│   ├── js
│   │   └── my-plugin-public.js
│   └── partials
│       └── my-plugin-public-display.php
└── uninstall.php

但經過幾個專案後發現不太順手,問題在於它把勾點 ( Hook ) 跟要跑在勾點上的函式切開來,常常在找這支函式是跑在哪個勾點上時會花上一些時間,更不用說在寫勾點的時候還需要切換檔案很容易混亂。

其次是很多功能很難界定它到底是屬於前台還後台,像是在做 REST API 我有時候會放在 public,有時候忘記又會放在 admin,要找的時候自己都會搞混。

然後最麻煩的地方是檔案的載入,WPPB 是使用 require 的方式來引入,所以如果新增檔案或是修改檔名的時候都會需要手動修改,最常發生的情況就是類別寫完後一直跑不動,追查後才發現根本忘記引入了 ?

所以一直在找尋著有沒有更好的架構可以來開發外掛,找了老半天都沒有適合自己的,索性就來發展一套自己的邏輯吧~

剛好當時在看「現代 PHP 」這本書,對於 Composer 很有興趣,就開始試著把 Composer 整合進 WordPress 的開發流程之中。

Composer 可以簡單的理解為是一套可以安裝 PHP 外掛的工具,可以把別人寫好的 PHP 類別透過 Composer 安裝後進行使用。

除此之外,它還可以幫助開發者做自動載入、套件更新管理,甚至還可以安裝 WP 的外掛,透過它來管理 WP 網站的外掛,你也可以上傳自己寫的套件,把一些常用的類別打包好後上傳,下次要用時直接透過 Composer 進行安裝。

更多 Composer 的實作細節會隨著下文逐一帶到,回到 WordPress 外掛開發架構的需求,我想要的是可以依照專案規模自行增加資料夾結構以及檔案命名,尤其是外掛開發的初期功能還很陽春,可能幾個檔案就搞定,等到外掛發展的越來越大,我可以隨時調整檔案結構以符合商業邏輯的變動。

參考了很多免費與付費外掛的結構後,目前已發展出一套自己的 SOP,大概的順序如下

1.建立基本架構

建立外掛的第一步就是在 wp-content/plugins 底下建立資料夾,然後新增主檔案與 src (source) 資料夾放主程式,這樣就完成最基本的結構了。

my-plugin
├── my-plugin.php
└── src

如果專案有前端的部分需要處理,我會開 assets 資料夾,裡面會放圖檔、CSS 與 JavaScript 等靜態檔案:

my-plugin
└── assets
    ├── css
    │   ├── admin.css
    │   └── public.css
    ├── img
    │   └── img1.jpg
    └── js
        ├── admin.js
        ├── ajax.js
        └── public.js

前後台的靜態檔就用檔名來區分,前台就是 public 後台是 admin,如果有需要做 Ajax 的 JS 檔也是會放在這邊。如果要進一步的整合 Node.js 來做前端的檔案編譯,assets 下面會再區分 src 跟 dist,然後才放 img、css 與 js 資料夾,之前有實作過整合最近非常紅的 CSS 框架 Tailwindcss,這部分會在之後的文章提到。

再來如果這個外掛有需要處理範本的話,我會再另外開一個 views 資料夾,這資料夾就像是佈景主題一樣,專門放範本檔:

my-plugin
└── views
    ├── loops
    │   └── post.php
    ├── page-about.php
    ├── page-contact.php
    ├── partials
    │   └── pagination.php
    ├── woocommerce
    │   └── checkout
    ├── template-2col.php
    └── template-blank.php

views 除了放對應 slug 的範本檔之外,也放了頁面範本,loops 資料夾放的是需要跑在 WP_Query 裡面的列表類內容,而 partials 放的是所有頁面會共用到的元件,像是分頁導覽 pagination。如果是做 WooCommerce 的 Extension 我會再多開一個資料放 WC 的範本檔,這樣就能從外掛來修改 WC 相關頁面的內容。

再來是 src 裡面的結構,嘗試了很多命名方法,最喜歡的應該是 MVC 模式:

my-plugin
└── src
    ├── controllers
    │   └── cpt.php
    └── models
        └── cpt.php

View 的部分我把它獨立在 src 的資料夾外面,原因是 src 的資料夾會透過 PSR-4 的 Autoload 載入,但 WP 的範本檔並不支援這樣的機制,所以把 views 獨立在 src 以外避免被自動載入。

Models 資料夾我放的是需要新增或修改資料庫結構的類別,最常見的就是新增 Custom Post Type,或是需要修改預設的文章或頁面 post type、新增 options、widget,我都會放在 Models 資料夾下面。

Controllers 則是負責處理 Models 裡面各種 post type 或是 option 的 CRUD,以及各種 WP 內建機制的處理,像是 REST API、WP Cron、Shortcode 等等,基本上這個資料夾會放所有的邏輯處理類別,需要呼叫類別的程式也是放在這邊。

如果有多個 CPT 需要處理多種操作,我會再新增新的資料夾來做區分:

my-plugin
└── src
    ├── controllers
    │   ├── cpt.php
    │   ├── cpt1
    │   │   ├── Api.php
    │   │   ├── Router.php
    │   │   ├── Post.php    
    │   │   └── Shortcode.php
    │   └── cpt2
    │       ├── Post.php
    │       └── Widget.php
    └── models
        └── cpt.php

雖然從 MVC 的角度下去分類看似很清楚,但實作後常發現自己會混淆 M 跟 C 的部分,我曾經在一個專案中把 Shortcode 放在 Controllers,另一個專案卻放在 Models 裡面,或是要註冊 Rest API 我就常會舉棋不定,這到底該放在哪裡好?

於是索性就不分 Models 跟 Controllers 了,直接以 WordPress 資料表結構來區分資料夾:

my-plugin
└── src
    ├── posts
    │   ├── cpt1
    │   │   ├── Ajax.php
    │   │   ├── Field.php
    │   │   └── Cpt1.php
    │   └── cpt2
    │       ├── Api.php
    │       ├── Cpt2.php
    │       └── Shortcode.php
    ├── terms
    │   ├── taxonomy1
    │   │   └── Field.php
    │   └── taxonomy2
    │       └── Api.php
    ├── options
    │   ├── Field.php
    │   └── Menu.php
    └── user
        └── Field.php

最常處理的是 wp_posts,所以 posts 資料夾下面會放各個 CPT 的資料夾,options 是做設定頁面的,對應的資料表是 wp_options,user 是使用者相關,對應的資料表是 wp_users,至於 Taxonomy 我也會跟 Post 拆開獨立放在 terms 的資料夾底下,在結構上會更清楚些。

然後每個 cpt 的資料夾底下,會有一支跟 CPT slug 同名的檔案,裡面放的會是註冊 CPT 的功能,下一篇文章會示範如何使用 Composer 套件來註冊 CPT,其他的 Field.php 、Ajax.php、Api.php 就是管理該 CPT 的相對應功能。

所以整個資料夾的邏輯就是以資料類型 > 功能來區分,對我來說可以再也不用判斷這功能到底是屬於 Models 還是 Controllers。

最後則是單元測試的資料夾:

my-plugin
└── tests
    └── cpt
        └── cptTest.php

測試的部分比較複雜,之後會再以獨立篇幅來進行說明。

另外如果是要做 i18n 本地化,就需要再新增一個 languages 資料夾,翻譯用的 .pot 檔可以透過 WP CLI 產生。

$ wp i18n make-pot . languages/my-plugin.pot

資料夾結構會是這樣:

my-plugin
└── languages
    └── my-plugin.pot

最後視專案的需求,資料夾結構可以是這樣:

my-plugin
├── my-plugin.php
└── src
    └── cpt1
        └── Cpt1.php

也可以逐漸發展成這樣:

my-plugin
├── assets
│   ├── css
│   │   ├── admin.css
│   │   └── public.css
│   ├── img
│   │   └── img1.jpg
│   └── js
│       ├── admin.js
│       ├── ajax.js
│       └── public.js
├── my-plugin.php
├── src
│   ├── posts
│   │   ├── cpt1
│   │   │   ├── Ajax.php
│   │   │   ├── Field.php
│   │   │   └── Cpt1.php
│   │   └── cpt2
│   │       ├── Api.php
│   │       ├── Cpt2.php
│   │       └── Shortcode.php
│   ├── terms
│   │   ├── taxonomy1
│   │   │   └── Field.php
│   │   └── taxonomy2
│   │       └── Api.php
│   ├── options
│   │   ├── Field.php
│   │   └── Menu.php
│   └── user
│       └── Field.php
├── tests
│   └── cpt
│       └── cptTest.php
└── views
    ├── loops
    │   └── post.php
    ├── page-about.php
    ├── page-contact.php
    ├── partials
    │   └── pagination.php
    ├── template-2col.php
    ├── template-blank.php
    └── woocommerce

在實務中我最常變動的部分是 src 資料夾,除了需求的新增外,做程式碼重構也都會常常修改這邊,以往只要資料夾名稱或是檔案有新增刪除,透過手動引入的方式就必須要全部修改,有沒有更方便且更彈性的作法呢?有!答案就是 Composer。

2.設定 Composer 環境

安裝 Composer 的教學網路非常多,這邊主要示範給 WordPress 外掛開發者的環境設定。第一步先開啟終端機然後把目錄切到正在開發的外掛底下:

$ cd wp-content\plugins\my-plugin

然後執行 composer init

$ composer init

composer 會用對話式的介面來協助我們進行設定,依序要回答六個問題:

This command will guide you through creating your composer.json config.

Package name (<vendor>/<name>) [oberonlai/my-plugin]: 
Description []: 
Author [Oberon Lai <m615926@gmail.com>, n to skip]: 
Minimum Stability []: stable
Package Type (e.g. library, project, metapackage, composer-plugin) []: project
License []: propreitary

基本上因為這不是要上傳到 Composer 套件庫 Packagist 的套件,我習慣都直接採用預設值,如果有特殊需求可以再個別調整,第七題是關鍵,它會問你是否要安裝相依套件:

Define your dependencies.

Would you like to define your dependencies (require) interactively [yes]? 
Search for a package: a7/autoload
Enter the version constraint to require (or leave blank to use the latest version): 

輸入套件名稱 a7/autoload 按下 Enter 後 Composer 就會開始自動搜尋 Packagist 上面的所有 PHP 套件並且指定安裝的版本,如果要安裝最新版就直接 Enter 下一步即可,a7/autoload 這個套件可以把指定資料夾裡面的檔案全部自動載入,如果沒有它就需要一個一個手動指定要自動載入的資料夾。

完成後接下來又會再問一次是否要搜尋其他套件,這邊我們什麼都不輸入直接下 Enter 即可。

接下來它會詢問是否要安裝只有在開發時才需要的套件,像是做單元測試 PHPUnit 這一類只會在開發環境中需要的套件就可以在這邊安裝,但目前我們先跳過,之後講解單元測試的文章再另行說明:

Would you like to define your dev dependencies (require-dev) interactively [yes]? no

接下來就會看到整個 Composer 設定檔的草稿,它會請你確認是否正確,輸入 Enter 即可,這樣就完成 composer.json 設定檔了。

{
    "name": "oberonlai/my-plugin",
    "type": "project",
    "require": {
        "a7/autoload": "^2.1"
    },
    "license": "propreitary",
    "authors": [
        {
            "name": "Oberon Lai",
            "email": "m615926@gmail.com"
        }
    ],
    "minimum-stability": "stable"
}

Do you confirm generation [yes]? yes

最後它會問你是否要安裝設定檔中的套件,按下 Enter 即可

Would you like to install dependencies now [yes]? yes

最後就會出現安裝完成的訊息,這就代表我們已經把 Composer 已經所需套件準備完成:

No lock file found. Updating dependencies instead of installing from lock file. Use composer update over composer install if you do not have a lock file.
Loading composer repositories with package information
Updating dependencies
Lock file operations: 1 install, 0 updates, 0 removals
  - Locking a7/autoload (2.1)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
  - Installing a7/autoload (2.1): Extracting archive
Generating autoload files

觀察我們外掛的目錄,會發現多了 composer.json、composer.lock 以及 vendor 資料夾,未來我們任何從 Composer 安裝的套件都會放在 vendor 這個資料夾底下,至於該如何引入這些套件以及讓 src 資料夾的檔案可以自動載入呢?讓我們開始寫 code 吧~

3.加入 Autoload 機制

打開外掛的主檔案,我把一些常用的寫法存成了 VSCode 的 Snippet,內容如下:

<?php

 /**
 * @link              https://oberonlai.blog
 * @since             1.0.0
 * @package           ODS
 *
 * @wordpress-plugin
 * Plugin Name:       Oberon Plugin
 * Plugin URI:        https://oberonlai.blog
 * Description:       Oberon Lai Blog Demo Plugins
 * Version:           1.0.0
 * Author:            Oberon Lai
 * Author URI:        https://oberonlai.blog
 * License:           GPL-2.0+
 * License URI:       http://www.gnu.org/licenses/gpl-2.0.txt
 * Text Domain:       ods
 * Domain Path:       /languages
 */


if ( ! defined( 'ABSPATH' ) ) {
  exit;
}

define('ODS_VERSION', '1.0.0');
define('ODS_PLUGIN_URL', plugin_dir_url(__FILE__));
define('ODS_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('ODS_PLUGIN_BASENAME', plugin_basename(__FILE__));

/**
 * Autoload
 */
require_once( ODS_PLUGIN_DIR . 'vendor/autoload.php' );
\A7\autoload( ODS_PLUGIN_DIR . 'src' );

外掛 Header 頭幾行的註解是必要的,具體內容可以參考 Handbook,define 的常數定義我會先把幾個常用的路徑存起來,因為 plugin_dir_xxx 在不同層級的檔案下會對應到不同的路徑,直接在主檔案存起來才能正確對應到外掛的根目錄。

再往下看就是最關鍵的 Autoload,第一個 require_once 載入的是 vendor/autoload.php,也就是透過 Composer 安裝的套件只需要這一行就可以全部自動引入,使用時只需要透過 Namespace 的 use 關鍵字即可使用,

而下一行的 \A7\autoload 則是指定自動載入 src 目錄底下所有的檔案,不管有幾層目錄、多少檔案,透過它就可以自動完整載入,縱使修改了檔名或是資料夾名稱,依舊都能正常運作。

a7\autoload 是由 Aaron Holbrook 所開發的,觀察他的程式碼,他使用了迴圈去掃描指定目錄的所有檔案,然後再透過 autoload 機制去逐一載入,雖然該套件已經有兩年多沒更新了,但用到目前為止都非常的穩定沒出過任何問題,因此它成為我每個專案的必備套件。

到這邊自動載入就完成了,日後如果我新增或修改任何 src 資料夾裡面的任何檔案名稱,就再也不用重新調整載入的路徑了,一勞永逸 ?

4. Git 設定

為了要能做到版本控管與自動部署,Git 版控也是我每個必用的工具,早期看到朋友的作法是直接把整個 wp-content 資料夾都納入版控,好處是所有外掛的更新都可以從 git 來管理,於是我也採用這樣的作法,但經過幾個專案後發現有些問題:

首先是必須要把客戶整個站的 wp-content 都 clone 下來,遇到比較大型的站這對我 128GB 的 MBP 負擔很大…

其次在設定 .gitignore 時要處理比較多的部分,有時候也會不小心把需要 ignore 的東西給 commit 進去。另外如果要提供給客戶下載正在開發中的外掛時,直接 download 就會抓回一大包,客戶還要從一大包之中去找到正確的外掛。

所以我現在都是一支外掛做一個版控,好處是本機開發環境只要安裝可能需要的外掛即可,不用把整包 wp-content 跟資料庫載到自己的電腦,然後 gitignore 也比較好設定,主要我會把測試相關以及 vendor 資料夾忽略掉,

測試主要會在本機以及測試機執行,所以不宜把所有的套件都上傳到正式機,這部分需要透過自動化部署工具來決定哪些套件該裝哪些不用裝,這部分會在自動部署一文中提及。

另外的好處是在 Github 頁面上可以設定 Releases 功能來讓客戶下載指定版本,也能看到每個版本更新的項目。

但如果一個專案裡面需要開發多支外掛,或是有改過一些既有的外掛需要納入版控時,數量一多就會不好管理,於是我採用 Github 的 Organization 來進行分類。把同一個專案不同外掛的存放庫都集中管理。

在本機的部分就使用 VSCode 的工作區功能,把需要處理到的外掛都加進到同一個工作區,然後在 commit 的時候透過內建的 Git 工具各自處理。

5.WordPress Coding Standard

到這邊就已經完成了開發環境的基本設定,但在真正開始 coding 之前,有件事雖然不是強制但我還是逼著自己習慣,那就是 WordPress 程式碼規範。

PHP 有一套自己的程式碼規範,叫做 PHP Standards Recommendations ( PSR ),雖然 WordPress 是用 PHP 寫的,但在規範上有許多地方不一樣,雖然自己本身從來沒在照什麼規範寫程式,但自從知道有這東西後就還是覺得要嘗試看看,來建立自己寫程式的習慣。

因為我都是在寫 WordPress,所以就理所當然的照著 WP 的規範走,一開始很不習慣,到現在老實說也還是很不自在,常常光是要整理格式就打斷了程式邏輯的思考,所以整理規範這件事我都是放在重構的時候來做,第一次在寫的時候就不管它了XD

後來透過 Terry Lin 大大的推薦知道 VSCode 有個好工具叫做 PHP Sniffer,可以選擇要遵循的規範並且在編輯器中即時顯示不符合規範之處,另外還可以搭配快速鍵進行修正,可以搞定我最討厭的變數對齊:

自動修正可以修到七八成,剩下的只要補上 PHPDoc 的說明即可。

安裝 PHP Sniffer 記得要在它的設定裡面將 PHPCS Standard 改成 WordPress,這樣就會以 WordPress 的程式碼規範來檢查,更多這部分的說明可以參考使用 PHP CodeSniffer 幫助熟悉 WordPress 程式碼風格

安裝與設定完成後,就會看到有不符合規範的地方會在下面出現紅色波浪線,滑鼠停留在上面一下下就會出現規範說明,然後根據說明進行修改即可。

這邊補充一下,有時候會遇到無法符合規範的狀況,像是要使用其他外掛的變數名稱,而這外掛並沒有依照規範來寫,另外一種狀況是 WordPress 規範中類別的檔名要用 class-xxx.php,有 template tag 的要用 xxx-template.php 這樣,

但在研究 PHPUnit 單元測試的時候發現這樣無法運作,原因是 PHPUnit 測試檔要載入被測試的類別時,必須要用 PSR-4 的機制來載入,然後 PSR-4 規定類別叫什麼檔名就要叫什麼,

如果類別叫做 User,則檔名就一定要叫做 User.php,寫 class-User.php 會無法載入,我測試過用 require 或 include 也無法正確載入,最後只能忽略某些 WordPress 程式碼規範。

忽略的常用方法有兩種,分為忽略當行或多行,單行的話直接在該行後面加入註解,並把規則名稱寫在註解中即可

$AbCd = 'bbb'; //phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase

WordPress 程式碼規範規定變數名稱必須要用底線而非駝峰式寫法,這時候直接在後面接上 //phpcs:ignore 加規則名稱,規則名稱可以在 PHP Sniffer 跳出的提示視窗中進行複製

如果一次要忽略多行就更簡單了,只要用兩個註解把要忽略的部分包住即可:

// @codingStandardsIgnoreStart
$AbCd = 'bbb';
$ACaaa = 'abcdefg';
// @codingStandardsIgnoreEnd

小結

在開發 WordPress 外掛時整合 Composer 可以很方便的安裝一些實用的套件以及自由組織資料夾內容,這樣在程式重構上能達到最大的彈性,另一方面,透過 Coding Standard 養成程式撰寫的習慣可以幫未來的自己或是團隊成員理解程式碼,接下來我們繼續挖掘 Composer 有什麼好物可以用吧~

文章標籤composer

目錄

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料

賴俊吾 / Oberon Lai
賴俊吾 / Oberon Lai

現為全職 WordPress 工程師,網站開發經歷 11 年,專攻前端工程與 WordPress 佈景主題、外掛客製化開發

訂閱電子報

Hi,我是 Oberon,我會固定在每週五早上發送接案心得以及與 WordPress 相關的電子報,同時也會分享一些實用的開發知識,讓你在 WordPress 的接案路上不孤單!

專注於分享 WordPress 開發、接案技巧、專案管理等自由工作者必備知識與心得

© 2024 想點創意科技有限公司

想點創意科技有限公司 | 統一編號 90516823
Designed by Hend Design | 隱私權政策

訂閱電子報

Hi,我是 Oberon,我會固定在每週五早上發送接案心得以及與 WordPress 相關的電子報,同時也會分享一些實用的開發知識,讓你在 WordPress 的接案路上不孤單!