由于自惭(自残)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; //四边分别表明:上右下左 }
项目整体的实现没有难度,都是最基础的东西。能帮助初学者(例如我)学到和巩固知识点才是最重要的!有任何疑问或者任何错误,欢迎留言啦!谢谢小伙伴们的耐心阅读~~