真刀實戰地搭建React+Webpack+Express搭建一個簡易聊天室

1、前面bb兩句

由於自慚(自殘)webpack配置還不夠熟悉,想折騰着作一個小實例熟悉。想着七夕快到了,作一個聊天室本身和本身聊天吧哈哈。好了,能夠中止bb了,說一下乾貨。css

2、 這個項目能學到啥?

爲了減小秒關文章的衝動。我得把好話放在前頭。作了這個項目,我學會了....(對於我).html

  1. Webpack的配置以及各個參數概念都有必定的熟悉。
  2. React+Webpack+Express的配合使用
  3. 熟悉React的JSX語法、生命週期等的熟悉
  4. Socket.io(入門)
  5. localStorage(入門)
  6. less(入門)

以上的都或多或少地涉及了(大神請別見笑)。不知道有沒有和我同樣的小夥伴之前看到socket、localStroage之類的都只懂個概念,真正使用還真沒個數。沒有嗎?好吧。其實這幾個東西寫起來真的不難,和他高大上的概念並不成比例。
例如socket.io只須要20行代碼就能完成基本功能
localStroage也須要建立一個對象,一個方法便可完成。
因此無需害怕!繼續看下去前端

3、 項目涉及的技術及地位

  1. Webpack 地位:★ ★ ★ ★ ★
    • 緣由: 由於項目最初構建目的是一步步熟悉Webpack的配置,以及和React、node的搭配,因此不給滿星Webpack怕是會鬧彆扭。node

    • 內容: 基礎知識的配置(入口文件等等),loader的配置(react加載器等),配置熱更新,打包後自動生成html文件...react

    • 擴展: 若是想要先熟悉瞭解webpack的一些基礎知識,能夠參考《入門及配置Webpack》jquery

  2. Express(node) 地位:★ ★ ★ ★ ☆
    • 緣由: 雖然一樣是不可或缺的地位,沒開啓服務怎麼訪問呀!可是之因此低Webpack一等(僅僅指在這個項目),是由於對node的配置很少,大部分都是經過express自動生成的。在此項目,更改的就只有app.js渲染的文件類型(默認是jade,更改成html)還有指向文件。
    • 內容: 渲染文件類型、更改指向目錄、更改端口...
    • 擴展:確保安裝了express,而後經過$ express myappName初始化構建項目便可
  3. React 地位:★ ★ ★ ★ ☆
    • 緣由: 你說不用react也能夠構建聊天室?固然能夠,可是咱們項目畢竟是React+Webpack,不用react的話...挺尷尬的?因此項目也要求你要懂一些react的語法啦。掌握一些基礎知識便可:自定義組件、父子傳值之類...
    • 內容: 頁面內容的呈現、邏輯的處理(其實就是普通html、js)
    • 擴展: 基礎.沒...沒啥好擴展的啦(項目一開始用到了react-redux,可是後面發現沒什麼必要就去掉了)
  4. socket.io、localStroage、Less 地位:★ ★ ★ ☆ ☆
    • 緣由:把這三類歸在一塊兒,一來是由於我對三類都不太熟悉(因此跟我同樣的不用怕!不會很複雜)
    • 內容: socket.io負責接收某位客戶端傳來的信息,並廣播到全部客戶端上。
      localStroage的加入有點勉強,我只是順便想熟悉一下它,並嘗試保存聊天記錄。具體做用是經過localStroage獲取用戶信息,若是沒有則添加。可是我在最開始會清除掉localStroage,因此每次刷新頁面的時候都須要從新填寫,因此項目localStroage存在做用不大,只是代替了模擬數據。
      項目使用的Less也比較基礎,只是簡化了層級關係的寫法(這一點確實比css方便不少)
    • 擴展: socket.io用法可看:《socket.io中文文檔》
      localStroage用法可看:《localStroage入門》
    • Socket.io將Websocket和輪詢(polling)機制以及其餘通訊方式封裝成通用接口,解決了瀏覽器的兼容性

4、摩拳擦掌:準備項目前期

  1. 咱們先來看一下項目部分截圖:
    項目截圖1 項目截圖2
    項目截圖3 項目截圖4
    想看gif動圖的能夠直接跳下去
    是否是很想親自作一個出來?別急,咱們這就開始。打開VSCode,打開音樂!!
  2. 由於項目是經過edxpress初始化的,因此須要安裝express,可經過express --version檢查本身的版本確保安裝(個人版本是4.16.0)。若是未安裝,可執行:$ npm install express -g
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,測試聊天可開多一個窗口

5、開戰!編寫項目

1.更改服務啓動相關配置(app.js)

刪除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文件以下:
dist文件夾

好了,node服務咱們配置到這就完事了.啥?真的就這麼簡單。

2.高能預警:Webpack的配置(敲桌子!)

安裝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"
        })
    ],
}

接下來介紹裏面的幾個參數:

  • entry:打包的入口文件.這邊指向react的根路由文件r-routes/index。打包的時候會從該文件入口,一層層獲取全部組件
//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          //壓縮腳本

大吉大利!枯燥的項目配置到此結束!

3. 熟悉的前端味道:編寫React組件

//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 />把信息顯示到內容上。

  • 安裝socket.io
npm install socket.io --save-dev    //安裝服務器端的socket.io
npm install socket.io-client --save-dev     //安裝客戶端的socket.io-client
  • 服務器端使用socket.io
// 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  
})
  • 客戶端端使用socket.io-client
const socket = require('socket.io-client')('http://localhost:8000');  
socket.on()....
socket.emit()...
  • 客戶端發送io.socket信息
//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)    //用戶發送的內容
}
  • 判斷是己方信息仍是對方信息
    經過一開始分配的userId來區分信息類型,若是是己方信息,應用向右浮動樣式(.contLiMy);若是是對方信息,應用向左浮動樣式(.contLiOther)
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只是簡化了父層的寫法
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;  //四邊分別表明:上右下左    
}

6、馬後話(總結)

1. 總結

項目整體的實現沒有難度,都是最基礎的東西。能幫助初學者(例如我)學到和鞏固知識點纔是最重要的!有任何疑問或者任何錯誤,歡迎留言啦!謝謝小夥伴們的耐心閱讀~~

2. 最後給你們呈上一張最終效果的gif圖

項目預覽gif

3.再一次附上:源碼

相關文章
相關標籤/搜索