一、前面bb两句

因为自惭(自残)webpack配置还不够熟悉,想折腾着做一个小实例熟悉。想着七夕快到了,做一个聊天室自己和自己聊天吧哈哈。好了,可以停止bb了,说一下干货。

二、 这个项目能学到啥?

为了减少秒关文章的冲动。我得把好话放在前头。做了这个项目,我学会了....(对于我).

  1. Webpack的配置以及各个参数概念都有一定的熟悉。
  2. React+Webpack+Express的配合使用
  3. 熟悉React的JSX语法、生命周期等的熟悉
  4. Socket.io(入门)
  5. localStorage(入门)
  6. less(入门)

以上的都或多或少地涉及了(大神请别见笑)。不知道有没有和我一样的小伙伴以前看到socket、localStroage之类的都只懂个概念,真正使用还真没个数。没有吗?好吧。其实这几个东西写起来真的不难,和他高大上的概念并不成比例。

例如socket.io只需要20行代码就能完成基本功能

localStroage也需要创建一个对象,一个方法即可完成。

所以无需惧怕!继续看下去

三、 项目涉及的技术及地位

  1. Webpack 地位:★ ★ ★ ★ ★

    • 原因: 因为项目最初构建目的是一步步熟悉Webpack的配置,以及和React、node的搭配,所以不给满星Webpack怕是会闹别扭。

    • 内容: 基础知识的配置(入口文件等等),loader的配置(react加载器等),配置热更新,打包后自动生成html文件...

    • 扩展: 如果想要先熟悉了解webpack的一些基础知识,可以参考《入门及配置Webpack》

  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)机制以及其他通信方式封装成通用接口,解决了浏览器的兼容性

四、摩拳擦掌:准备项目前期

  1. 我们先来看一下项目部分截图:





    想看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的话恭喜你!闯过第一关!

注意:启动命令也可以用npm run start 启动,因为package.json的script里面已经默认设置了npm run start指代 node ./bin/www命令。两个使用其中一个都可以启动项目! 如果遇到端口占用情况,进入bin/www文件修改端口即可。

3.就知道这点难不倒你。开始动手写项目前我把最终目录写一下,方便后面参考使用。(可跳过)

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

拷贝到本地之后

npm install     //安装依赖
npm run build //打包
npm run start //启动服务 浏览器访问localhost:8000,测试聊天可开多一个窗口

五、开战!编写项目

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文件如下:

好了,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只是简化了父层的写法

一些点击、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; //四边分别代表:上右下左
}

六、马后话(总结)

1. 总结

项目总体的实现没有难度,都是最基础的东西。能帮助初学者(例如我)学到和巩固知识点才是最重要的!有任何疑问或者任何错误,欢迎留言啦!谢谢小伙伴们的耐心阅读~~

2. 最后给大家呈上一张最终效果的gif图

3.再一次附上:源码

真刀实战地搭建React+Webpack+Express搭建一个简易聊天室的更多相关文章

  1. React + webpack 快速搭建开发环境

    因网上大多React + webpack快速搭建的运行不起来,便自行写了一个.在搭建开发环境的前需安装nodejs,npm. 新建一个工作目录,比如叫reactdome,在reactdome目录中运行 ...

  2. Express+Socket.IO 实现简易聊天室

    代码地址如下:http://www.demodashi.com/demo/12477.html 闲暇之余研究了一下 Socket.io,搭建了一个简易版的聊天室,如有不对之处还望指正,先上效果图: 首 ...

  3. React第一篇: 搭建React + nodejs + express框架

    前提: 需要安装Node.js (>6)版本 1.cmd进到本地某个目录, 逐行输入以下指令(以下括号为注释) npm install -g create-react-app   (全局安装cr ...

  4. 利用yeoman快速搭建React+webpack+es6脚手架

    自从前后端开始分离之后,前端项目工程化也显得越来越重要了,之前写过一篇搭建基于Angular+Requirejs+Grunt的前端项目教程,有兴趣的可以点这里去看 但是有些项目可以使用这种方式,但有些 ...

  5. webpack 搭建React(手动搭建)

    前言 最近真的都是在瞎学,看到自己不是很明白的东西,都喜欢自己手动去敲1到3遍(晚上下班的时候咯), 瞧,React  基于webpack 搭建,react 官方有一套手脚架工具,我自己也搭建过确实挺 ...

  6. php+websocket搭建简易聊天室实践

    1.前言 公司游戏里面有个简单的聊天室,了解了之后才知道是node+websocket做的,想想php也来做个简单的聊天室.于是搜集各种资料看文档.找实例自己也写了个简单的聊天室. http连接分为短 ...

  7. 使用Html5下WebSocket搭建简易聊天室

    一.Html5WebSocket介绍 WebSocket protocol 是HTML5一种新的协议(protocol).它是实现了浏览器与服务器全双工通信(full-duplex). 现在,很多网站 ...

  8. 使用socket搭建一个网络聊天室

    #服务器端import socket import threading #创建一个TCP端 sock = socket.socket(socket.AF_INET, socket.SOCK_STREA ...

  9. 【react】---手动封装一个简易版的redux

    export let createStore = (reducer)=>{ //定义默认的state let state; //定义默认的action let actionTypes = &qu ...

随机推荐

  1. JustOJ1500: 蛇行矩阵

    题目链接:https://oj.ismdeep.com/problem?id=1500 题目描述 蛇形矩阵是由1开始的自然数依次排列成的一个矩阵上三角形. 输入 本题有多组数据,每组数据由一个正整数N ...

  2. JS笔记—02

    1.String截取:substr:截几位, substring:截到哪. 2.String的操作,例如变大写,小写,本身不会变,只是在栈里交换引用似的 var str = "hello w ...

  3. 知乎上一个比较好的学习QT的公众号<<跟小豆君学Qt>>

    公众号网址:https://zhuanlan.zhihu.com/p/28472916

  4. HTML5 canvas游戏工作原理

    HTML5已经不是一个新名词.它看上去很cool,有很多feature,大多数人普遍看好它的发展.对于我来说,最感兴趣的是它的canvas标签,可以结合Javascript来绘制游戏画面. 我们可以在 ...

  5. python通过sftp远程传输文件

    python提供了一个第三方模块paramiko,通过这个模块可以实现两台机器之间的网络连接,sftp是paramiko的一个方法,使用sftp可以在两台机器之间互相传输 拷贝文件.然而paramik ...

  6. Prometheus监控学习笔记之PromQL简单示例

    0x00 简单的时间序列选择 返回度量指标 http_requests_total 的所有时间序列样本数据: http_requests_total 返回度量指标名称为 http_requests_t ...

  7. Docker学习笔记之运行和管理容器

    0x00 概述 容器是基于容器技术所建立和运行的轻量级应用运行环境,它是 Docker 封装和管理应用程序或微服务的“集装箱”.在 Docker 中,容器算是最核心的部分了,掌握容器的操作也是 Doc ...

  8. Web视频播放之video.js

    h5这么火是有它的理由的,支持原生视频播放,但是目前兼容性还不是很好,因此使用js框架支持网站视频播放是比较不错的选择. 下载video.js,我们可以通过官网去下,但是官网在国外,我尝试了几次均下载 ...

  9. Codeforces 839A Arya and Bran

    Bran and his older sister Arya are from the same house. Bran like candies so much, so Arya is going ...

  10. Duilib嵌入CEF出现窗口显示不正常

    参考资料:https://www.aliyun.com/zixun/wenji/1247250.html 转载:https://www.cnblogs.com/gongxijun/p/4857977. ...