本篇為 [MTR04] 第十三週 - 帶著做留言版 plugin 的學習筆記。如有錯誤歡迎指正。
前置作業 需安裝好 webpack 以及其他套件,可跟著官方教學步驟 Getting Started 進行安裝:
1 2 3 4 mkdir webpack-demo cd webpack-demo npm init -y npm install webpack webpack-cli --save-dev
安裝使用 jquery 1 npm install jquery webpack-cli --save-dev
安裝 babel-loader 1 npm install -D babel-loader @babel/core @babel/preset-env
目標:建立留言版 plugin 也就是修改 week12 實作的留言版,將 index.html 中的內容全部以 JavaScript 形式匯入。
步驟一:init() 初始化 1 2 3 4 5 commentPlugin.init ({ apiURL : '' , siteKey : '' , containerSelector : '#comments' })
步驟二:動態新增頁面 將 UI 介面以及 CSS 樣式,同樣使用 JavaScript 來動態新增:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 let siteKey = '' ;let apiUrl = '' ;let containerElement = null ;let commentDOM = null ;let lastId = null ; let isEnd = false ; const css = '.add-comment-form { margin-bottom: 10px; } .card { margin-bottom: 10px; } .card-title { word-wrap:break-word; } .load-more { margin-bottom: 10px; }' const loadMoreButtonHTML = '<button class="load-more btn btn-dark">載入更多</button>' ;const formTemplate = ` <div> <form class="add-comment-form"> <div class="form-group"> <label for="form-nickname">暱稱</label> <input name="nickname" type="text" class="form-control" id="form-nickname" > </div> <div class="form-group"> <label for="content-textarea">留言內容</label> <textarea name="content" class="form-control" id="exampleFormControlTextarea1" rows="3"></textarea> </div> <button type="submit" class="btn btn-dark">送出</button> </form> <div class="comments"></div> </div> ` function init (options ) { siteKey = options.siteKey ; apiUrl = options.apiUrl ; containerElement = $(options.containerSelector ); containerElement.append (formTemplate); const styleElement = document .createElement ('style' ); styleElement.type = 'text/css' ; styleElement.appendChild (document .createTextNode (css)); document .head .appendChild (styleElement) commentDOM = $('.comments' ); getComments (); $('.comments' ).on ('click' , '.load-more' , () => { getComments (); }); $('.add-comment-form' ).submit (e => { e.preventDefault (); const newCommentData = { 'site_key' : siteKey, 'nickname' : $('input[name=nickname]' ).val (), 'content' : $('textarea[name=content]' ).val () } $.ajax ({ type : 'POST' , url : `${apiUrl} /api_add_comments.php` , data : newCommentData }).done (function (data ) { if (!data.ok ) { alert (data.message ); return ; } $('input[name=nickname]' ).val ('' ); $('textarea[name=content]' ).val ('' ); appendCommentToDOM (commentDOM, newCommentData, true ); }); }); } $(document ).ready (() => { init ({ siteKey : 'heidi' , apiUrl : 'http://localhost/heidi/week13_local/hw2' , containerSelector : '.comment-area' }); });
如此即可將 <script> 區塊的程式碼全部放到 src\index.js,再利用 webpack 進行下列步驟。
步驟三:進行模組化 將不同功能模組化來重構程式碼,以便後續管理。
在 src 資料夾建立 api.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import $ from 'jquery' ;export function getComments (apiUrl, siteKey, before, cb ) { let showURL = `${apiUrl} /api_comments.php?site_key=${siteKey} ` ; if (before) { showURL += '&before=' + before; } $.ajax ({ url : showURL }).done (function (data ) { cb (data); }); } export function addComments (apiUrl, siteKey, before, cb ) { $.ajax ({ type : 'POST' , url : `${apiUrl} /api_add_comments.php` , data }).done (function (data ) { cb (data) }); }
建立 template.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 export const cssTemplate = '.add-comment-form {margin - bottom: 10px; } .card {margin - bottom: 10px; } .card-title {word - wrap:break-word; } .load-more {margin - bottom: 10px; }' export const loadMoreButtonHTML = '<button class="load-more btn btn-dark">載入更多</button>' ;export const formTemplate = ` <div> <form class="add-comment-form"> <div class="form-group"> <label for="form-nickname">暱稱</label> <input name="nickname" type="text" class="form-control" id="form-nickname" > </div> <div class="form-group"> <label for="content-textarea">留言內容</label> <textarea name="content" class="form-control" id="exampleFormControlTextarea1" rows="3"></textarea> </div> <button type="submit" class="btn btn-dark">送出</button> </form> <div class="comments"></div> </div> `
建立 utils.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 export function escape (toOutput ) { return toOutput .replace (/&/g , '&' ) .replace (/</g , '<' ) .replace (/>/g , '>' ) .replace (/"/g , '"' ) .replace (/'/g , ''' ); } export function appendCommentToDOM (container, comment, isPrepend ) { const html = ` <div class="card"> <div class="card-body"> <h5 class="card-title">${escape (comment.nickname)} </h5> <p class="card-text">${escape (comment.content)} </p> </div> </div> ` ; if (isPrepend) { container.prepend (html); } else { container.append (html); } }
並在 index.js 引入上述檔案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 import { getComments, addComments } from './api' ;import { appendCommentToDOM } from './utils' ;import { cssTemplate, loadMoreButtonHTML, formTemplate } from './template' ;import $ from 'jquery' ;let siteKey = '' ;let apiUrl = '' ;let containerElement = null ;let commentDOM = null ;let lastId = null ; let isEnd = false ; $(document ).ready (() => { init ({ siteKey : 'heidi' , apiUrl : 'http://localhost/heidi/week13_local/hw2' , containerSelector : '.comment-area' }); }); function init (options ) { siteKey = options.siteKey ; apiUrl = options.apiUrl ; containerElement = $(options.containerSelector ); containerElement.append (formTemplate); const styleElement = document .createElement ('style' ); styleElement.type = 'text/css' ; styleElement.appendChild (document .createTextNode (cssTemplate)); document .head .appendChild (styleElement) commentDOM = $('.comments' ); getNewComments (); $('.comments' ).on ('click' , '.load-more' , () => { getNewComments (); }); $('.add-comment-form' ).submit (e => { e.preventDefault (); const newCommentData = { 'site_key' : siteKey, 'nickname' : $('input[name=nickname]' ).val (), 'content' : $('textarea[name=content]' ).val () } addComments (apiUrl, siteKey, newCommentData, data => { if (!data.ok ) { alert (data.message ); return ; } $('input[name=nickname]' ).val ('' ); $('textarea[name=content]' ).val ('' ); appendCommentToDOM (commentDOM, newCommentData, true ); }); }); } function getNewComments ( ) {const commentDOM = $('.comments' );$('.load-more' ).hide (); if (isEnd) { return ; } getComments (apiUrl, siteKey, lastId, data => { if (!data.ok ) { alert (data.message ); return ; } const comments = data.discussions ; for (let comment of comments) { appendCommentToDOM (commentDOM, comment); } let length = comments.length ; if (length < 5 ) { return ; } if (length === 0 ) { isEnd = true ; $('.load-more' ).hide (); } else { lastId = comments[length - 1 ].id ; $('.comments' ).append (loadMoreButtonHTML); } }); }
步驟四:使用 webpack 打包 將上述檔案進行打包,會在 dist 資料夾建立 main.js,接著回到 index.html 引入該檔案:
1 <script src="./dist/main.js" ></script>
步驟五:引入 library
可參考官方文件:https://webpack.js.org/guides/author-libraries/
將 index.js 的 init() 改為 export:
1 2 3 export function init (options ) { ... }
改在 index.html 引入 library:
1 2 3 4 5 6 7 8 9 <script> $(document ).ready (() => { commentPlugin.init ({ siteKey : 'heidi' , apiUrl : 'http://localhost/heidi/week13_local/hw2' , containerSelector : '.comment-area' }); }); </script>
並在設定檔 webpack.config.js 的 output 加上library: 'commentPlugin',
步驟六:執行 webpack 打包 利用 webpack 將檔案打包成一個 module,即可在 index.html 引入 library。在瀏覽器上開啟頁面,會發現多出全域變數 commentPlugin:
優化程式碼 接著要優化先前寫的程式碼,例如修改 plugin 的 class 名稱,避免不同使用者(siteKey)發生衝突。
修改 template.js 中的 classname
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 export function getLoadMoreButton (classname ) { return `<button class="${classname} load-more btn btn-dark">載入更多</button>` ; } export function getForm (formClassName, commentsClassName ) { return ` <div> <form class=${formClassName} > <div class="form-group"> <label>暱稱</label> <input name="nickname" type="text" class="form-control"> </div> <div class="form-group"> <label>留言內容</label> <textarea name="content" class="form-control" rows="3"></textarea> </div> <button type="submit" class="btn btn-dark">送出</button> </form> <div class="${commentsClassName} "></div> </div> ` }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 import { getComments, addComments } from './api' ;import { appendCommentToDOM, appendStyle } from './utils' ;import { cssTemplate, getLoadMoreButton, getForm } from './template' ;import $ from 'jquery' ;export function init (options ) { let siteKey = '' ; let apiUrl = '' ; let containerElement = null ; let commentDOM = null ; let lastId = null ; let isEnd = false ; let loadMoreClassName; let loadMoreSelector; let commentsClassName; let commentsSelector; let formClassName; let formSelector; siteKey = options.siteKey ; apiUrl = options.apiUrl ; loadMoreClassName = `${siteKey} -load-more` ; commentsClassName = `${siteKey} -comments` ; formClassName = `${siteKey} -add-comment-form` ; loadMoreSelector = '.' + loadMoreClassName; commentsSelector = '.' + commentsClassName; formSelector = '.' + formClassName; containerElement = $(options.containerSelector ); containerElement.append (getForm (formClassName, commentsClassName)); appendStyle (cssTemplate) commentDOM = $(commentsSelector); getNewComments (); $(commentsSelector).on ('click' , loadMoreSelector, () => { getNewComments (); }); $(formSelector).submit (e => { e.preventDefault (); const nicknameDOM = $(`${formSelector} input[name=nickname]` ); const contentDOM = $(`${formSelector} textarea[name=content]` ); const newCommentData = { site_key : siteKey, nickname : nicknameDOM.val (), content : contentDOM.val () } console .log (formSelector) console .log (nicknameDOM, nicknameDOM.val ()) console .log (contentDOM, contentDOM.val ()) addComments (apiUrl, siteKey, newCommentData, data => { if (!data.ok ) { alert (data.message ); return ; } nicknameDOM.val ('' ); contentDOM.val ('' ); appendCommentToDOM (commentDOM, newCommentData, true ); }); }); function getNewComments ( ) { const commentDOM = $(commentsSelector); $(loadMoreSelector).hide (); if (isEnd) { return ; } getComments (apiUrl, siteKey, lastId, data => { if (!data.ok ) { alert (data.message ); return ; } const comments = data.discussions ; for (let comment of comments) { appendCommentToDOM (commentDOM, comment); } let length = comments.length ; if (length < 5 ) { return ; } if (length === 0 ) { isEnd = true ; $(loadMoreSelector).hide (); } else { lastId = comments[length - 1 ].id ; const loadMoreButtonHTML = getLoadMoreButton (loadMoreClassName); $(commentsSelector).append (loadMoreButtonHTML); } }); } }