2020.08.31更新:原始碼部份新增二欄的功能。在最後一段原始碼下載中可以下載。
本篇要解決的問題
之前常遇到臨時要出一份EDM,雖然就是個簡單的步驟,不花什麼時間,但就是會打斷其他正在寫的案子,或是要下班前才接到電話阻止人下班,為了解決這種零碎的問題,這幾天開發了一個EDM生成器。
開發的過程中,只能說踩坑又踩坑,這邊是紀錄踩了坑後,最後解決的辦法,重點整理如下:
- 預覽區的 HTML、實際產出的 HTML 要分開,不然 Vue.js 的 render 會刪掉註解,意思就是寫給 outlook 用的 hack 會全部被刪掉。
- 因為寬度是動態改變,所以 outlook hack 的部份要改用替代的。
- 下載成 .html 時,
<!DOCTYPE html><html>
這個會被刪掉,要補上。 - css 的部份,選擇器第一層要是編輯器的 id,以免 css 影響到 edm。
- 包成 zip 用套件:JSZip、FileSaver.js。
- 為了能包進 zip 裡,抓圖檔的部份要用 FileReader,才能抓到 base64。
- 用 JSZip 抓圖檔時,FileReader 抓出來的開頭要刪掉,才能設成
base64: true
。
如果需要文字版的,也有用 Netlyfy CMS 製作一個版本,請看:
預覽、產出的HTML,要分開
寫的 HTML 架構如下:
<div id="app"> <div class="row"> <!-- 編輯區 --> <div id="lego-editor"></div> <!-- 預覽區 --> <div id="preview"></div> </div> </div> <!-- 實際產出 --> <main id="output"></main>
為了不被 Vue.js 刪除掉 hack,產出區不能包在 #app
裡,而且為了讓使用者看不到產出區,要加上 display: none
。
用 replace 替代 hack,補上 width、html
下一個是要改變 hack 裡的 width,因為 width 的值是抓 v-model
的值,因此產出成 HTML 時要另外再把 hack 補進去,這邊用的是 replace。
在產出成 HTML 時,因為抓的是某個 ID 裡的 HTML,所以頁面開頭的宣告跟 html tag 都會被刪掉,在下載前也要補上,這邊一樣用 replace 補。
replace 的部份最後寫的如下:
document.getElementById('output') .outerHTML.replace('<main id="output">', '<!DOCTYPE html><html>') .replace('</main>', '</html>') .replace(/<div class="js-width-start"><\/div>/g, '<!--[if (gte mso 9)|(IE)]><table width="' + this.maxWidth + '" data-width cellpadding="0" cellspacing="0" border="0" align="center"><tr><td><![endif]-->');
css 選擇器,編輯、預覽的 id 要是第一層
因為 outlook 不支援 <style>
的 tag,因此所有樣式都是寫在行內。
為了不讓其它的 css 影響了 EDM 的樣式,css 選擇器上的第一層要是編輯區、預覽區的 id 或 class。像這樣:
#lego-editor &, & * box-sizing: border-box position: fixed h1, h2 font-weight: bold color: #000 #preview padding-top: 15px margin-left: auto
檔案包成 zip 的套件:JSZip、FileSaver.js
原本還在煩惱前端要怎麼把 HTML 跟一堆圖檔打包成 zip 讓使用者下載的,google 一下後竟然就發現有神人開發出了套件。
打包 zip – JSZip:https://stuk.github.io/jszip/
下載 zip – FileSaver:https://github.com/eligrey/FileSaver.js/
JSZip 整體來說使用蠻簡單的,像這樣:
const zip = new JSZip(); // 抓HTML zip.file(檔名, 檔案內容); // 抓圖檔 zip.file(圖檔檔名, 圖檔的base64, { base64: true }); // 自動下載 zip.generateAsync({type:"blob"}).then(content => { saveAs(content, 'zip的檔名.zip'); });
抓圖檔要用 FileReader
原本抓圖檔用的是 URL.createObjectURL,因為出來的網址簡單。
但,一直找不到把 URL.createObjectURL 轉成 base64 的方法,造成 JSZip 要存圖檔時一直存不到,只好換成 FileReader。
客製 input file 的部份,可以參考這篇:File API 客製上傳檔案按鈕(input file)
這邊寫用 FileReader 的部份。
// 以下為 Vue.js 的 methods previewImg(event) { var file = event.target.files[0]; var reader = new FileReader(); reader.addEventListener('load', () => { console.log(reader.result); }, false); if(file) reader.readAsDataURL(file); }
JSZip 抓圖檔,要刪掉 base64 的開頭
當我們用 FileReader 抓出圖片的 base64 後,會看到以下:
data:image/png;base64,xxxxxxxxxxxxxxx………
當 JSZip 在抓圖檔時,如果原封不動的給,就會看見報錯,報說格式不對。
這個問題很簡單,就把開頭的「data:image/png;base64,」給去掉就行。
這邊直接用 split,取第一個半形逗號來分,如下:
zip.file(t.name, t.img.split(',')[1], { base64: true })
demo 及原始檔下載
原始檔整理在 Github 上,也用了 Github Pages 生成 demo 頁面。
2020.08.31更新:原始碼新增了可以編輯二欄的功能,因此程式碼部份分成只有一欄的,跟今日新增可以有二欄的。
demo:https://letswritetw.github.io/letswrite-image-edm-build/
原始檔(一欄):https://github.com/letswritetw/letswrite-image-edm-build/releases/tag/v1.0.0
原始檔(二欄):https://github.com/letswritetw/letswrite-image-edm-build

