由於自慚(自殘)webpack配置還不夠熟悉,想折騰着作一個小實例熟悉。想着七夕快到了,作一個聊天室本身和本身聊天吧哈哈。好了,能夠中止bb了,說一下乾貨。css
爲了減小秒關文章的衝動。我得把好話放在前頭。作了這個項目,我學會了....(對於我).html
以上的都或多或少地涉及了(大神請別見笑)。不知道有沒有和我同樣的小夥伴之前看到socket、localStroage之類的都只懂個概念,真正使用還真沒個數。沒有嗎?好吧。其實這幾個東西寫起來真的不難,和他高大上的概念並不成比例。
例如socket.io只須要20行代碼就能完成基本功能
localStroage也須要建立一個對象,一個方法便可完成。
因此無需害怕!繼續看下去前端
緣由: 由於項目最初構建目的是一步步熟悉Webpack的配置,以及和React、node的搭配,因此不給滿星Webpack怕是會鬧彆扭。node
內容: 基礎知識的配置(入口文件等等),loader的配置(react加載器等),配置熱更新,打包後自動生成html文件...react
擴展: 若是想要先熟悉瞭解webpack的一些基礎知識,能夠參考《入門及配置Webpack》jquery
express SoyChat //建立express項目,名字我的喜歡 cd SoyChat //進入目錄 npm install //安裝依賴 node bin/www //啓動項目
訪問localhost:3000 看到Welcome to Express的話恭喜你!闖過第一關!webpack
注意:啓動命令也能夠用npm run start 啓動,由於package.json的script裏面已經默認設置了npm run start指代 node ./bin/www命令。兩個使用其中一個均可以啓動項目! 若是遇到端口占用狀況,進入bin/www文件修改端口便可。git
3.就知道這點難不倒你。開始動手寫項目前我把最終目錄寫一下,方便後面參考使用。(可跳過)github
SoyChat / bin/ www //默認生成文件,服務啓動文件 client/ //客戶端,編寫代碼的地方 components //公共組件 dist //打包後存放位置 modules //主要的邏輯組件 r_routes //react組件路由 views //模板文件、React渲染文件 index.html node_modules/ public/ //存放圖片等靜態資源 routes/ //默認生成文件,express設置路由文件 index.js app.js //默認生成文件,服務啓動配置 package.json webpack.config.js //webpack配置文件
4.完整項目的github地址:小語1.0
拷貝到本地以後web
npm install //安裝依賴 npm run build //打包 npm run start //啓動服務 瀏覽器訪問localhost:8000,測試聊天可開多一個窗口
刪除routes/文件下的user.js 去掉app.js引入的userRouter、app.use('/users',userRouter)
更改視圖渲染文件的類型:jade => html
var ejs = require('ejs'); //須要安裝ejs模塊:npm install ejs --save app.engine('html', ejs.renderFile); app.set('views', path.join(__dirname, './client/dist')); //html文件加載路徑 app.set('view engine', 'html'); app.use(express.static(path.join(__dirname, './client/dist'))); //css.js...之類文件加載路徑
可能會疑惑./client/dist是個什麼東西?
其實這個文件是咱們打包後存放的位置,咱們不直接訪問React渲染的html頁面,而是指向webpack打包後生成的html;
例如,這個項目最終打包好後的dist文件以下:
好了,node服務咱們配置到這就完事了.啥?真的就這麼簡單。
安裝Webpack依賴:npm install webpack --save-dev
這裏的--save-dev是把依賴加載到package.json的devDependencies中,--save是安裝到dependencies中。前者是開發所須要用到的,後者是生產環境須要用到的。這裏不作具體介紹,可看《入門及配置Webpack》
呼~終於安裝好了。接着新建一個webpack.config.js文件吧。
//webpack.config.js var webpack = require('webpack'); var path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: __dirname + '/client/r_routes/index', //入口文件 output: { path:path.join(__dirname + '/client/dist'), //打包後存放位置 filename:'bundle.js', //打包後的文件名 }, module :{ loaders : [{ test :/(\.jsx|\.js)$/, exclude : /node_modules/, loader :'babel-loader', options:{ presets:[ "env", "react" ] } }, { test : /\.css$/, loader:'style-loader!css-loader' }, { test: /\.less/, loader: 'style-loader!css-loader!less-loader' }, { test: /\.(png|jpg)$/, loader: 'url-loader?limit=8192'// limit 字段表明圖片打包限制 } ] }, plugins: [ //根據index.html做爲模板,打包的時候自動生成html並引入打包的js文件 new HtmlWebpackPlugin({ template: __dirname + "/client/views/index.html" }), //引入全局webpack new webpack.ProvidePlugin({ $:"jquery", jQuery:"jquery" }) ], }
接下來介紹裏面的幾個參數:
//r-routes/index import React from 'react'; import ReactDOM from 'react-dom'; import ReactApp from '../modules/r_app'//根組件 ReactDOM.render(<ReactApp />,document.getElementById('app'));
可能會疑惑,如何知道把ReactApp組件render(渲染)到哪一個html的id=app上呢?
原來這個和webpack的plugins(插件)的new HtmlWebpackPlugin有關。這個對象會讀取一個目錄下的html文件爲模版,而後通過處理後再去output指定的目錄輸入一個新的html文件。由於在這裏指定了/client/views/index.html文件爲模板,因此react的全部都會渲染到這個html文件中。
output:配置打包輸出位置以及輸出文件名字。(html的生成是經過new HtmlWebpackPlugin方法)
module:裏面是各類loader加載器;webpack理論上只能加載js文件,可是經過各類loader它能夠加載圖片、css等等文件。
項目用到的loader:style-loader、css-loader、file-loader...詳見package.json
要使webpack打包支持react和ES6語法還須要安裝babel等依賴
npm install --save-dev react react-dom babelify babel-preset-react npm install --save babel-preset-es2015 //支持ES6語法 //loader配置參考上面的便可
plugins:各類插件配置
例如上面全局jquery的配置(記得安裝jquery依賴包npm install jquery --save)
注意我這裏沒有配置熱更新,由於熱更新有本身的服務,但我想使用node啓動服務,不用webpack-dev-server的服務,因此就沒配置(網上應該有解決方法,給node服務添加熱更新,可是我沒找到,因此項目只有自動打包,但仍是須要手動刷新瀏覽器)
npm install webpack-dev-server --save-dev //熱更新安裝
至此,webpack.config.js配置完成。接下來咱們看看package.json
//package.json { "name": "app", "version": "0.0.0", "private": true, "scripts": { "start": "node ./bin/www", "build": "webpack --progress --watch" }, "dependencies": { "cookie-parser": "~1.4.3", "debug": "~2.6.9", "ejs": "^2.6.1", "express": "~4.16.0", "http-errors": "~1.6.2", "jade": "~1.11.0", "jquery": "^3.3.1", "less": "^3.8.1", "morgan": "~1.9.0" }, "devDependencies": { "babel-core": "^6.26.3", "babel-loader": "^7.1.5", "babel-preset-env": "^1.7.0", "babel-preset-es2015": "^6.24.1", "babel-preset-react": "^6.24.1", "css-loader": "^1.0.0", "file-loader": "^1.1.11", "html-webpack-plugin": "^3.2.0", "http-proxy-middleware": "^0.18.0", "less-loader": "^4.1.0", "react": "^16.4.2", "react-dom": "^16.4.2", "react-router-dom": "^4.3.1", "socket.io": "^2.1.1", "socket.io-client": "^2.1.1", "style-loader": "^0.22.1", "url-loader": "^1.0.1", "webpack": "^3.0.0", "webpack-cli": "^3.1.0", "webpack-dev-middleware": "^3.1.3", "webpack-dev-server": "^2.9.7" } }
安裝的依賴包我就不具體介紹,重點介紹scripts的參數
... "scripts": { "start": "node ./bin/www", "build": "webpack --progress --watch" }, ...
這裏是可根據狀況配置一些指代命令。
原本項目啓動須要node ./bin/www,可是經過配置,我終端輸入npm run start(npm run + 指令)也能達到同樣的效果。
同理,我利用npm run build 代替了webpack的打包命令,並附帶了一些參數命令。
--progress //顯示進度條 --watch //監聽變更並自動打包 -p //壓縮腳本
大吉大利!枯燥的項目配置到此結束!
//r-routes/index.js import ReactApp from '../modules/r_app' //根組件
咱們能夠看到r-routes/index.js引用了一個根組件r_app,r_app再由組件AppHead、AppContent、AppFoot 構成。
1. localStroage的使用
值得注意的是剛進入頁面的時候,輸入信息框會根據localStroage是否含有用戶信息來決定是否出現
//r_app.js //引入組件 import AppHead from './head/index' import AppContent from './content/index' import AppFoot from './foot/index' import UserInfoModal from '../component/userInfoModal/index' import './r_app.less' ... const storage = window.localStorage; storage.removeItem('userInfo'); //進入頁面時清除localStroage if(!storage){ // console.log("瀏覽器不支持localstorage"); return false; }else{ // console.log("瀏覽器支持localstorage"); //判斷是否存在localStroage if(storage['userInfo']){ //已經存在localStroage.隱藏輸入信息框 this.setState({ userInfo:JSON.parse(storage['userInfo']), //把StringObject轉換成Object userInfoState:true, }) } } } ... render (){ // console.log(this.state.userInfo) return ( <div className="appWried" > { this.state.userInfoState ? '' : <UserInfoModal onSubmitData={this.onGetData} /> } { <div style={{height:'100%',width:'100%'}} className={this.state.userInfoState ? '' : 'unClick'} > <AppHead /> <AppContent userInfo={this.state.userInfo}/> <AppFoot userInfo={this.state.userInfo} /> </div> } </div> ); }
能夠發現,r_app.js只是作了localStroage的讀取和判斷,可是並無寫入任何,localStroage字段。而且永遠不會進入if(storage['userInfo'])語句,由於每次在最前面都會把信息remove。因此信息輸入框每次刷新頁面都依然會彈出來.
耍我呢?localStroage出來秀逗的?
= =localStroage在這裏確實有點大材小用,由於一開始想持續性保存用戶的信息以及聊天記錄,可是發現這樣測試難以進行。我就一部電腦,讀取的localStroage['userInfo']不就如出一轍麼。
說回正題,那添加localStroage的操做在哪執行?
答案就在<UserInfoModal onSubmitData={this.onGetData}/>
子組件裏,當用戶在<UserInfoModal />
提交信息後,存儲到localStroage而且把數據傳回r_app,而後r_app再執行對應操做
//UserInfoModal.js ... <button className="submitBtn" onClick={this.submitFn.bind(this)}>提交</button> submitFn(){ let userName = $('#userName').val(); if(!userName){ alert('名字還未輸入哦') return; } let headImg = this.state.choseImg; let userId = "indexCode" + Math.round(Math.random() * 100000); //隨機建立id,用來判斷是自身信息仍是別人信息 //數據傳回父組件r_app.js this.props.onSubmitData({ userName, headImg, userId, }) } ... //r_app.js ... //子級返回數據 onGetData (e){ // console.log(e);得到子組件傳遞的數據,包括userName和headImg let userInfo = {}; userInfo.userName = e.userName; userInfo.headImg = e.headImg; userInfo.userId = e.userId; this.setState({ userInfo, userInfoState:true,//隱藏輸入信息框 },function(){ const storage = window.localStorage; storage['userInfo'] = JSON.stringify(this.state.userInfo);//localStorage只能存儲String類型,需將對象轉換成string // console.log(storage['userInfo']) }) } ...
注意:localStroage只能存儲String類型的數據,若是須要存儲對象,須要經過JSON.Stringify()轉換。取數據的時候經過JSON.parse()便可
2. Socket.io的使用
實現效果:底部input發送的數據傳遞到content組件並展現,而且要求全部客戶端都能收到。
實現思路:利用socket.io實現實時通訊,先把發送信息的客戶端的用戶我的信息以及發送內容中轉到服務器,服務器再分派給全部訂閱了這個socket事件的客戶端。接收到消息的<AppContent />
把信息顯示到內容上。
npm install socket.io --save-dev //安裝服務器端的socket.io npm install socket.io-client --save-dev //安裝客戶端的socket.io-client
// bin/www //新增socket.io模塊 var io = require('socket.io')(server); io.on('connection', function(socket){ //接受客戶端傳送的sendMessage命令 socket.on('sendMessage', function(ioUserInfo,msg){ console.log(ioUserInfo); //用戶ioUserInfo console.log(msg); //接收用戶的發送信息 //經過接受sendMessage這個action的數據再廣播給全部'訂閱的人'(即on了這個事件的) socket.broadcast.emit('getMessage', ioUserInfo, msg); //socket.emit()發送信息給所有人,只要訂閱了getMessage的人都會收到變量ioUserInfo和msg //socket.broadcast是發送除本身外的人 }); })
引入socket.io模塊,當處於connection的時候便可進行接收、發送信息。上面服務器接收(on)到某個用戶傳來的信息以後再廣播(emit)給你們
on和emit能夠這麼理解,接收信息是on事件,發送信息是emit事件
//發送標誌爲message信息,信息內容爲:test socket.emit('message','test') //訂閱了標誌爲message的信息的客戶端將會接收到這條test信息 socket.on('message',function(data){ console.log(data);//test })
const socket = require('socket.io-client')('http://localhost:8000'); socket.on().... socket.emit()...
//footComponent.js ... componentDidMount(){ document.addEventListener("keydown",this.handleEnterKey);//綁定一個鍵盤按下的方法 } //點擊按鈕發送信息 clickBtn(){ const { message } = this.state; //獲取input輸入的內容 const { userInfo } = this.props; // console.log('發送' + this.state.message) //觸發發送內容的函數 this.sendMessage(userInfo,message); } //回車後發送信息 handleEnterKey(e){ let that = this; const { message } = this.state; //獲取input輸入的內容 const { userInfo } = this.props; if(e.keyCode === 13){ //回車keyCode==13 //是否發送內容 this.sendMessage(userInfo,message); } } //發送內容函數 sendMessage(ioUserInfo,message){ if(message){ this.sendSocketIO(ioUserInfo,message);//發送websocket的函數 this.setState({ message:'', //清空input內容 }) } } sendSocketIO(ioUserInfo,message){ socket.emit('sendMessage',ioUserInfo,message) //客戶端發送 } let disabled = Object.keys(this.props.userInfo).length ? '' : 'disabled'; //未填用戶信息的時候禁止input輸入內容 return ( <div className="footDiv"> <input disabled={disabled} className="footIpt" placeholder="請輸入..." onChange={this.dataChange} value={this.state.message}/> <button className={`footBtn ${this.state.hasCont}`} onClick={this.clickBtn}>發送</button> </div> )
//content/index.js socket.on('getMessage', function(ioUserInfo,msg){ console.log(ioUserInfo); //ioUserInfo爲發送msg的用戶信息 console.log(msg) //用戶發送的內容 }
componentWillReceiveProps(nextProps){ const { userInfo } = nextProps; //獲取父級傳遞過來的userInfo,裏面攜帶自身的userId socket.on('getMessage', function(ioUserInfo,msg){ console.log(ioUserInfo); // 若是socket傳回ioUserInfo.userId和自身相同,則判斷爲自身發送的信息 let appendLi = '' if(ioUserInfo.userId == userInfo.userId){ appendLi = `<li class="contLi contLiMy"> <div class="contLiMy"> <div class="headImg"> <img src=${userInfo.headImg} /> </div> <div class="chatContent"> <div class="chatName"> <span>${userInfo.userName}</span> </div> <div class="chatBg"> <span>${msg}</span> </div> </div> </div> </li>` } else{ appendLi = `<li class="contLi contLiOther"> <div class="contLiOther"> <div class="headImg"> <img src=${ioUserInfo.headImg} /> </div> <div class="chatContent"> <div class="chatName"> <span>${ioUserInfo.userName}</span> </div> <div class="chatBg"> <span>${msg}</span> </div> </div> </div> </li> ` } $('.contUl').append(appendLi); }); } return( <div className="content"> <ul className="contUl"> <div className="contTop">歡迎你:{this.props.userInfo.userName}</div> </ul> </div> )
這裏有一個小技巧,若是內容超出高度出現滾動條的時候,須要保持顯示底部的內容.在填充內容後加上一行代碼
... $('.contUl').append(appendLi); $('.contUl').scrollTop($('.contUl')[0].scrollHeight);//保持顯示滾動條高度的位置(即底部) ...
至此項目的主要功能都已經完成啦。未介紹的less其實和css寫法差很少,這裏less只是簡化了父層的寫法
一些點擊、hover功能都是用最基礎的js、jq實現的。
例如:聊天框的實現(利用僞類:after製做三角形)
//己方聊天框 .chatBg{ font-size:15px; padding:3px 10px; border-radius: 3px; color: #EFEFEF; background-color: #8c628d; margin-right:8px; position: relative; } //聊天框三角形的製做 .chatBg:before{ right:-12px; border-color:transparent transparent transparent #8c628d; //四邊分別表明:上右下左 }
項目整體的實現沒有難度,都是最基礎的東西。能幫助初學者(例如我)學到和鞏固知識點纔是最重要的!有任何疑問或者任何錯誤,歡迎留言啦!謝謝小夥伴們的耐心閱讀~~