反饋系統(feedback system)是具備閉環信息通道的系統。前端
定義: 將系統的後果或輸出信息採集、處理,而後送回輸入端並據此調整系統行爲的系統。因爲信息流通構成閉合環路, 它也稱爲閉環系統。 反饋做用經常用於檢測信號誤差以及對象特性的變化,並以此來控制系統行爲以及消除偏差。 它又被稱爲反饋控制或者按照偏差控制的系統。node
主要能夠分爲自建的反饋系統和使用第三方工具做爲反饋系統:react
郵件形式:webpack
如Tower,咱們在 https://tower.im/help 的底部能夠看到下面的界面:es6
進入幫助頁面,若是咱們沒有找到答案,就能夠在底部發現這樣的一個反饋系統,郵箱是網站默認填充的, 而後咱們能夠在內容框中填寫文字、圖片等, 開發、客服人員收到郵件以後會經過郵件的方式發送給用戶。 web
顯然,這種方式是封閉的,只有發問的用戶能夠收到解決郵件,而其餘用戶看不到更多用戶的問題與回答。 mongodb
幾分鐘以後,我就接受到了這樣的一封反饋郵件,很好地解決了個人問題, 能夠看到,Tower的入口也是很是方便地。typescript
對話形式:數據庫
叮噹網站就是這種對話形式的反饋,在右下角有一個留言的圖片,點擊這個圖片以後就會進入一個對話的頁面,這個頁面還可使用一個新的iframe來顯示,以下所示:npm
這種方式最大的好處就是直觀、方便追問、具備及時性, 可是不太方便跟蹤員工處理的進度狀況。
工單形式:
在阿里雲的網站上,咱們能夠添加建議並提交,提交以後咱們能夠看到顯示以下:
能夠看到提交以後,就會造成一個工單,實時的反映當前狀況, 即 ‘已提交’、‘預審經過’、‘已採納’、‘已實現’ 幾個步驟。 對於僅僅是提交的建議,咱們還能夠進行再次編輯,另外,全部的建議其餘用戶也都是能夠看到的,全部人(固然包括管理員)能夠進行反饋、評論、投票等功能。
在此網站能夠查看:https://connect.aliyun.com/suggestion/5293?spm=5176.8409797.user.1.38c60aa8v2lqt7
需求池形式:
https://www.mockplus.cn/
這是一個作原型的工具,更爲高效、簡單。
從反饋系統的基本框架來看,此係統在在前端至少須要兩個頁面(或者說是網站) --- 用戶反饋網站 以及 後臺管理網站。
第一部分:
即須要在網站的的主頁面或者幫助頁面處提供一個入口,由此進入反饋界面,在反饋界面中的須要提供一個表單用於提交反饋,內容以下:
輸入完成以後提示已提交至後臺便可。
另外,在反饋界面,咱們還能夠列出一些常見的問題和回答,這樣,用戶也許就不須要去提問了。
第二部分:
固然,除了一個提交反饋的表單仍是不夠的,還須要一個「個人反饋」界面。
即在第一部分中,咱們反饋進行了提交,在「個人反饋」界面就須要展現對於個人全部反饋的詳情了。
既然有個人反饋界面,這也就是說每次你進入這個網站的時候仍是須要惟一標識的,那麼就須要使用登陸和註冊功能,這樣才能在你下一次登陸的時候將你的相關的建議展現出來。
後臺管理界面會稍微複雜一些。
重要: 能夠作成推送平臺的js插件!
作成插件以後,咱們就能夠在多個產品的主頁上使用了。 這種場景每每是用於一家較大的公司,公司旗下有不少的產品,每一個產品可能都會單獨建站,在每個網站上,咱們能夠添加一個特定產品的js插件,然後臺管理頁面是保持基本不變的。
登陸、註冊、請求反饋界面、請求後臺管理界面、添加產品、請求全部產品、請求某個產品下的全部反饋(每條反饋是一個文檔,每一個文檔中包含了反饋的具體信息,題目,信息,評論(評論須要包括評論人、評論時間等),狀態、所屬用戶等等,鍵值對不是簡單的string,設計的有規律一些便可。), 請求某個用戶的全部建議(在某個產品下去查找便可,那麼保存用戶信息時還須要保存用戶所使用到的產品類別,這樣在搜索產品時會比較快),
最終的項目架構:
這個項目分紅了兩個文件來作,一個做爲主服務器,另外一個做爲輔助服務器,那個輔助服務器在發送請求時是代理到主要的服務器上的。前端採用基本的分層方式,後端採用的是MVC的架構方式。
build是服務器相關文件。 model是數據庫的相關文件。 node_modules是存放的一些包。 router是存放的路由文件。 src是react的相關文件(src中components存放一些基本的組件,pages存放整個的頁面, redux存放的是項目狀態管理的相關文件,index.tsx是react項目的渲染頁面)。 www是服務器上存放的靜態文件。 最後就是基本的 package.json、settings.js(數據庫配置)、tsconfig.json(typescript的相關配置文件)、webpack.config.js(webpack相關文件)。
一、應該創建幾個文件,即開啓幾個服務器(項目)?
第一種方式: 開啓一個服務器。
即認爲只有一個服務器,在用戶點擊按鈕時,觸發一個請求,從後端請求到反饋系統的頁面,這樣就能夠進行簡單的操做了。
對於管理員,也能夠請求到服務器端的一個管理員的頁面。
這樣在技術上是能夠作到的,可是在前端處理上會出現問題。 好比使用webpack打包的時候,就會把全部的js打包,可是實際上在用戶和管理員處所使用的js並非全部的,這樣就會形成浪費。
第二種方式: 開啓兩個服務器
這樣的方式最簡單、清楚、明瞭。
結果: 實際上,咱們應該當作兩個項目來作,也就是說,須要同時進行兩個項目來作,一個項目用於寫後天管理系統(較多主要是數據庫的處理),另外一個項目主要是寫前端反饋系統。
二、 這個任務的實際使用場景是這樣的嗎?
即管理員的界面始終只有一個,而用戶反饋界面的界面是會隨着不一樣產品的改變而改變的,會有不一樣的網站來進行請求。
結果:
三、 使用react、 ant.design能夠嗎?
結果: 能夠嘗試使用 ant.design , 由於 ant.design 是螞蟻的一套前端框架,適合react項目的使用,可能會遇到一些坑,可是仍是能夠嘗試考慮使用的。
四、 這裏所說的插件究竟是什麼?怎麼去理解? 可配置是說添加一個配置文件、配置對象嗎?
插件實際上就是一段js代碼,經過這個js代碼,咱們能夠將這個js插件暴露出來的方法進行 init 而後適用在某個產品上。
五、什麼是堆樓模式? 這裏所使用的反饋系統是工單形式的嗎?
即評論是一層一層的,而不是縮進的形式。 反饋系統能夠看做是工單形式的。
六、總體的思路應該是怎樣的?
對於後臺管理界面是直接使用node做爲後臺,而後使用react來寫前臺管理界面,這個是通用的。 另外,這個後臺除了提供管理界面以外,也應該提供反饋平臺所須要的接口。 另外,還須要一個通用的普通端,這裏能夠本身選產品的類型等。
七、數據庫的設計應該是怎麼樣的?
在數據庫中,咱們須要存儲的信息包括管理員信息、用戶信息、建議信息、評論信息、產品信息等等, 對於這些信息,咱們應當儘可能採用扁平化的風格進行存儲。使用鏈接的方式。
(1)產品存儲
(2)建議存儲
(3)評論存儲
(4)管理員存儲
(5)用戶存儲
即在數據庫中須要創建5個集合,這樣咱們就能夠進行簡單的系統操做了。能夠看到,上面的數據庫的設計儘可能是扁平化的,而沒有進行扎推。
而且咱們應當提升可重用性,不要在不一樣的表中創建了太多相同的內容,這是不合適的。
八、 ant.design究竟應該怎麼用?
咱們在使用的時候應該儘量多地去考慮其源碼,這樣,咱們才能更理解,也能有所學習。
沒錯,用戶的權限很小,用戶只能提交以後,看到其答案已經提交了,後續的工做就徹底取決於管理員了。
因此說,對於建議的管理,主動權徹底在管理端,這樣纔會比較好控制數據,不至於混亂,就像redux的單向數據流的方式是同樣的。
十、登陸、註冊這部分應該怎麼作? 管理員、用戶、產品、建議的ID怎麼設置能夠保證沒有大的問題? 怎麼保證不會重複? 表的設計是否有問題。
一、 對於惟一ID,咱們可使用 uuid (在項目中咱們直接 npm install uuid --dev 便可),這個工具能夠幫助咱們快速解決問題,而且ID是不會重複的。
二、 對於登陸、註冊的問題,咱們能夠把這個反饋的網站看作一個黑盒子,而後只須要對之有肯定的輸入, 就能保證其輸出,因此,咱們能夠將其i做爲插件來想, 對於登陸、註冊的事情由不一樣的網站來解決便可。 而咱們須要作的就是在後臺處理好便可。 而在管理端仍是比較容易理解的,就是必須登陸註冊才能查看產品等等。
十一、 uuid介紹。
參考文章: https://www.npmjs.com/package/uuid
http://www.jianshu.com/p/d553318498ad
UUID是128位的全局惟一標識符,一般由32字節的字符串表示。它能夠保證時間和空間的惟一性,也稱爲GUID,全稱爲:UUID ―― Universally Unique IDentifier,Python 中叫 UUID。
十二、 如何實現代理服務器呢?
很簡單,咱們打開了兩個項目,可是咱們只但願在一個項目的服務器上處理各類服務器、數據庫的數據, 這樣,咱們直接將次服務器直接代理到主要的服務器便可。以下所示:
第一步:
npm install http-proxy-middleware --save-dev
第二步:
在dev-server.js中,須要引入 http-proxy-middleware,而後:
var proxyTable = { '/api': { target: 'http://127.0.0.1:8000/', // 本地node服務器 changeOrigin: true, pathRewrite: { '^/api': '/' } } };
上面就是咱們的基本設置,固然,在proxyTable中能夠代理多個服務器,但這裏咱們只須要一個。 思路就是對api代理,而後發出的時候再重寫。
// proxy api requests Object.keys(proxyTable).forEach(function (context) { var options = proxyTable[context] if (typeof options === 'string') { options = { target: options } } app.use(proxyMiddleware(options.filter || context, options)) })
ok! 就是這麼簡單了,這樣就能夠完成服務器的代理了。
下面咱們就能夠發出一個請求了:
componentWillMount () { fetch("/api/getAllProduct", { method: "GET" }).then(function(res) { console.log('進入'); return res.json(); }).then(function (data) { console.log(data); if (data.code == 200) { console.log('獲取到全部產品' ,data.data); } else { console.log(data.message); } }) }
即: 這裏咱們在 localhost: 3000 的服務器下發出了指向 localhost:8000 服務器的資源。
1三、屢次看到服務器崩潰! 爲何呢?
即如上所示,乍一看彷佛並無什麼解決的辦法,都是不知道的文件,可是,若是咱們仔細看,仍是能夠發現致使問題的地方的, 好比這裏咱們能夠看到 router 下的 index.js 問題, 接着鎖定 product.js 的問題, 再去追究,能夠發現,咱們在出錯的時候,沒有及時關閉mongodb數據庫,這樣,就會報錯,修復了這個問題以後,就能夠正常獲取數據了。
14、在使用redux的過程當中,遇到了一個問題。 即在一個問題列表頁,點擊每一條鏈接以後,能夠進入這個列表的詳情頁,那麼如何在詳情頁獲取到數據呢? 目前的方法是這樣的, 即在列表頁的Link進行路由跳轉的時候,將這條列表的suggestionId傳遞到詳情頁中去,而後在 componentWillMount() 這個鉤子函數中使用下面的方法:
componentWillMount () { const suggestionId = this.props.location.query.suggestionId; this.props.filterSug(suggestionId); console.log(suggestionId); }
即獲取到當前的 suggestionId, 而後經過一個 action 篩選 store 中的這條建議。
function handleSuggestions (state = {allSuggestions: [], filteredSug: []}, action) { switch (action.type) { case 'ADD_ALL_SUGGESTIONS': const newSug = Object.assign([], action.data); return { allSuggestions: newSug } case 'FILTER_SUGGESTION': return { allSuggestions: state.allSuggestions, filteredSug: state.allSuggestions.filter(function (item, index) { return item.suggestionId == action.id; }) } default: return state; } }
接着,咱們在再從store中獲取這條建議。以下:
function mapDispatchToProps (dispatch) { return { filterSug: (id) => dispatch( filterSuggestion(id) ) } } function mapStateToProps (state) { return { filteredSug: state.handleSuggestions.filteredSug } }
接着,咱們就能夠在render函數中使用這個 prop 了,即:
const {filteredSug} = this.props;
可是這樣致使的一個問題就是: 經過 filteredSug.map 調用時,發現會報錯,即 沒法讀取 map 所在的值,他是 undefined 的。
而若是咱們使用下面的方法:
const {filteredSug = [] } = this.props;
即便用es6中的默認值的方法,這樣就不會報錯了,而且能夠正常顯示這條建議。 這是爲何? 我以前的想法是: 由於在 store 中存儲時,就會有一個默認值,因此,就算是直接獲取應該也是一個空的數組,而不是undefined啊,爲何這裏還須要提供一個默認值呢?
首先能夠肯定的是,在store中的state發生變化的時候,就會及時的經過頁面進行最新的渲染,這樣頁面就會及時的變化,即最開始 filteredSug 是 [], 而後當reducer處理完了 action 以後,就會改變state,這樣 filteredSug 就成了有一個對象元素的數組了,這樣咱們就能夠進行map了。
之因此頁面會隨着數據發生變化,是由於頁面對數據有了一個訂閱,來監聽變化。getState函數能夠獲取當前的state。
下面,咱們須要測試的就是在 const {} = this.props;和後面的 mapStateToProps 獲取的速度問題(即誰先誰後),測試以下:
在 componentWillMount中添加下面的語句:
console.log('在componentWillMount中的時間', new Date().getTime());
在render函數下添加下面的語句:
const {filteredSug} = this.props; console.log('render函數時間', new Date().getTime());
在 mapStateToProps中添加下面的語句:
function mapStateToProps (state) { console.log('獲取store中的state的時間', new Date().getTime(), state); return { filteredSug: state.handleSuggestions.filteredSug } }
而後,咱們開始測試,加載這個頁面,結果以下:
這裏的總體思路很是簡單,就是首先進入頁面,而後第一步就獲取到當前的state, 這樣的好處在於,第一步獲取到以後就能夠在後面的各個鉤子函數、render函數中使用了,可是咱們能夠發現一個問題,就是在handleSuggestions這個reducer裏只有allSuggestions可是並無filteredSug。 第二步進入了componentWillMount鉤子函數中,這樣就能夠直接出發filter咱們想要的suggestion的action了。 第三步就是開始render。 因爲在第一步的過程當中就沒有獲取到filterSug,因此在render的時候,就能夠發現map的是一個undefined值。 第四步就比較有意思了,就是在咱們以前觸發了一個action,因此又在render以後,從新接收到了新的store,這樣,就又會從新渲染出來新的render。 咱們能夠發現,這個 fiterdSug 是存在的,可是以前若是沒有賦默認值,那麼就在前面報錯了,也就沒有後面的步驟了。
問題緣由:
其實如今就比較好理解了,問題就是處在 reducer 那裏,咱們在獲取到全部的建議的時候,並無把當前 state 的全部值返回到一個新的state了,因此就致使了在進入 detail 頁面的時候,接收不到 filteredSug,解決問題的方式很簡單,以下:
function handleSuggestions (state = {allSuggestions: [], filteredSug: []}, action) { switch (action.type) { case 'ADD_ALL_SUGGESTIONS': const newSug = Object.assign([], action.data); return { allSuggestions: newSug, filteredSug: [] } case 'FILTER_SUGGESTION': return { allSuggestions: state.allSuggestions, filteredSug: state.allSuggestions.filter(function (item, index) { return item.suggestionId == action.id; }) } default: return state; } }
這樣,咱們就能夠在render中使用filterSug的時候不須要使用默認值,而後就map了,由於開始map的時候,什麼都沒有,因此就不會渲染,而後store接收到action以後,觸發了新的state,這樣就可使得咱們的filterSug成爲了一個新的值,頁面就會渲染出來了。
那麼 connect 的這個源碼是怎樣的呢? 爲何在進入頁面的時候,能夠保證在 componentWillMount 鉤子函數以前就能夠已經獲取到了 store 中的state呢?
猜測一: 因爲在react中的組件裏,constrctor鉤子函數式最先被調用的,因此這裏獲取store的步驟多是在 constructor 時調用的。 由於connect是react-redux的方法, 而react-redux是另一個庫,因此只能利用react的現有的api。
測試驗證
方法: 在constructor鉤子函數中打印一下時間,再在 connect 的相關函數中打印一下時間, 若是說 connect 中的時間在後,那麼就是對的。
這個結果很意外,爲何能夠首先得到 state 呢? 不是應該首先得到conscructor的嗎? 而後利用這個鉤子函數獲取到state?
猜測二: 既然state是最早獲取到的, 那麼就是說它在原來的組件的基礎上包裝了一層。
這個確實很簡單了,在最開始學習redux的過程當中,就已經學習到了下面的概念:組件包括UI組件和容器組件,前者的做用徹底是用於展現的, 而容器組件的做用就是使用狀態管理工具如redux來命名的,即在使用redux時,就須要先將全部的數據以props的形式傳遞到各個容器組件中(實際上並非這樣的,在react中,有一個context的概念,就是傳遞數據,不須要使用props層層傳遞,而只須要使用一個 context 就能夠以最快的速度把想要傳遞的數據給到各個子組件中了), 而後容器組件再將之做爲props傳遞給UI組件, 而且咱們在使用redux時,不管是發送dispatch,仍是接受store中的數據,都須要經過這一層。
到這裏這個問題就很清楚了,就是首先,connect的容器組件首先將獲取到的數據傳進來放在props中,而後再開始建立內部的UI組件,而後內部須要相應的props時,直接從這個中間層來獲取就能夠了。因此constructor的創造時間是晚於state傳遞進來的時間的。