├── .gitignore ├── README.md ├── clean-blog ├── LICENSE ├── README.md ├── about.html ├── contact.html ├── css │ ├── bootstrap.css │ ├── bootstrap.min.css │ ├── clean-blog.css │ └── clean-blog.min.css ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 ├── img │ ├── about-bg.jpg │ ├── contact-bg.jpg │ ├── home-bg.jpg │ ├── post-bg.jpg │ └── post-sample-image.jpg ├── index.html ├── js │ ├── bootstrap.js │ ├── bootstrap.min.js │ ├── clean-blog.js │ ├── clean-blog.min.js │ ├── jquery.js │ └── jquery.min.js ├── less │ ├── clean-blog.less │ ├── mixins.less │ └── variables.less ├── mail │ └── contact_me.php └── post.html ├── react_tuts ├── 1.helloworld.html ├── 1.nested components.html ├── 2.states.html ├── 2.states2.html ├── 3.props.html ├── 4.events 1.html ├── 4.events 2.html ├── 5.two way data flow.html ├── 6.component lifecycle-updating.html ├── 6.component lifecycle.html ├── 7.mixin.html ├── index-static.html └── reactQa │ ├── app │ └── js │ │ ├── components │ │ ├── QuestionApp.js │ │ ├── QuestionForm.js │ │ ├── QuestionItem.js │ │ ├── QuestionList.js │ │ └── ShowAddButton.js │ │ └── main.js │ ├── bower.json │ ├── gulpfile.js │ ├── index-static.html │ ├── index.html │ └── package.json ├── redux-chat ├── .gitinore ├── package.json ├── public │ └── css │ │ └── style.css ├── reduxtuts.sublime-project ├── reduxtuts.sublime-workspace ├── src │ ├── client │ │ ├── actionCreators.js │ │ ├── components │ │ │ ├── App.jsx │ │ │ ├── InputBox.jsx │ │ │ ├── Message.jsx │ │ │ ├── MessageList.jsx │ │ │ └── RoomList.jsx │ │ ├── index.js │ │ ├── io.js │ │ ├── middleware.js │ │ ├── reducer.js │ │ └── store.js │ ├── server │ │ ├── actionCreator.js │ │ ├── controller.js │ │ ├── core.js │ │ ├── io.js │ │ ├── reducer.js │ │ ├── server.js │ │ ├── store.js │ │ └── views │ │ │ └── index.ejs │ └── shared │ │ └── utils │ │ └── dateTime.js ├── template │ └── index.ejs ├── test │ ├── client │ │ ├── InputBox_spec.js │ │ ├── MessageList_spec.js │ │ ├── RoomList_spect.js │ │ └── reducer_spec.js │ ├── clientTesthelper.js │ ├── server │ │ ├── core_spec.js │ │ ├── reducer_spec.js │ │ └── store_spec.js │ └── serverTestHelper.js └── webpack.config.js ├── redux-chat基本功能 ├── package.json ├── public │ └── css │ │ └── style.css ├── src │ ├── client │ │ ├── actionCreators.js │ │ ├── components │ │ │ ├── App.jsx │ │ │ ├── InputBox.jsx │ │ │ ├── Message.jsx │ │ │ ├── MessageList.jsx │ │ │ └── RoomList.jsx │ │ ├── index.js │ │ ├── io.js │ │ ├── middleware.js │ │ ├── reducer.js │ │ └── store.js │ ├── server │ │ ├── actionCreator.js │ │ ├── core.js │ │ ├── io.js │ │ ├── reducer.js │ │ ├── server.js │ │ ├── store.js │ │ └── views │ │ │ └── index.ejs │ └── shared │ │ └── utils │ │ └── dateTime.js ├── template │ └── index.ejs ├── test │ ├── client │ │ ├── InputBox_spec.js │ │ ├── MessageList_spec.js │ │ ├── RoomList_spect.js │ │ └── reducer_spec.js │ ├── clientTesthelper.js │ ├── server │ │ ├── core_spec.js │ │ ├── reducer_spec.js │ │ └── store_spec.js │ └── serverTestHelper.js └── webpack.config.js └── redux-chat服务器端渲染 ├── package.json ├── public └── css │ └── style.css ├── src ├── client │ ├── actionCreators.js │ ├── components │ │ ├── App.jsx │ │ ├── InputBox.jsx │ │ ├── Message.jsx │ │ ├── MessageList.jsx │ │ └── RoomList.jsx │ ├── index.js │ ├── io.js │ ├── middleware.js │ ├── reducer.js │ └── store.js ├── server │ ├── actionCreator.js │ ├── controller.js │ ├── core.js │ ├── io.js │ ├── reducer.js │ ├── server.js │ ├── store.js │ └── views │ │ └── index.ejs └── shared │ └── utils │ └── dateTime.js ├── test ├── client │ ├── InputBox_spec.js │ ├── MessageList_spec.js │ ├── RoomList_spect.js │ └── reducer_spec.js ├── clientTesthelper.js ├── server │ ├── core_spec.js │ ├── reducer_spec.js │ └── store_spec.js └── serverTestHelper.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | lib-cov 3 | *.seed 4 | *.log 5 | *.csv 6 | *.dat 7 | *.out 8 | *.pid 9 | *.gz 10 | 11 | pids 12 | logs 13 | results 14 | 15 | npm-debug.log 16 | node_modules 17 | bower_components 18 | dist 19 | 20 | .DS_Store 21 | .nodemonignore 22 | .sass-cache/ 23 | .bower-*/ 24 | .idea/ 25 | build 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## [zexeo.com](http://zexeo.com) 教程相关文件 -------------------------------------------------------------------------------- /clean-blog/README.md: -------------------------------------------------------------------------------- 1 | # [Start Bootstrap](http://startbootstrap.com/) - [Clean Blog](http://startbootstrap.com/template-overviews/clean-blog/) 2 | 3 | [Clean Blog](http://startbootstrap.com/template-overviews/clean-blog/) is a stylish, responsive blog theme for [Bootstrap](http://getbootstrap.com/) created by [Start Bootstrap](http://startbootstrap.com/). This theme features a blog homepage, about page, contact page, and an example post page along with a working PHP contact form. 4 | 5 | ## Getting Started 6 | 7 | To use this theme, choose one of the following options to get started: 8 | * Download the latest release on Start Bootstrap 9 | * Fork this repository on GitHub 10 | 11 | ## Bugs and Issues 12 | 13 | Have a bug or an issue with this theme? [Open a new issue](https://github.com/IronSummitMedia/startbootstrap-clean-blog/issues) here on GitHub or leave a comment on the [template overview page at Start Bootstrap](http://startbootstrap.com/template-overviews/clean-blog/). 14 | 15 | ## Creator 16 | 17 | Start Bootstrap was created by and is maintained by **David Miller**, Managing Parter at [Iron Summit Media Strategies](http://www.ironsummitmedia.com/). 18 | 19 | * https://twitter.com/davidmillerskt 20 | * https://github.com/davidtmiller 21 | 22 | Start Bootstrap is based on the [Bootstrap](http://getbootstrap.com/) framework created by [Mark Otto](https://twitter.com/mdo) and [Jacob Thorton](https://twitter.com/fat). 23 | 24 | ## Copyright and License 25 | 26 | Copyright 2013-2015 Iron Summit Media Strategies, LLC. Code released under the [Apache 2.0](https://github.com/IronSummitMedia/startbootstrap-clean-blog/blob/gh-pages/LICENSE) license. -------------------------------------------------------------------------------- /clean-blog/about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Clean Blog - About 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 71 | 72 | 73 | 74 |
75 |
76 |
77 |
78 |
79 |

About Me

80 |
81 | This is what I do. 82 |
83 |
84 |
85 |
86 |
87 | 88 | 89 |
90 |
91 |
92 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Saepe nostrum ullam eveniet pariatur voluptates odit, fuga atque ea nobis sit soluta odio, adipisci quas excepturi maxime quae totam ducimus consectetur?

93 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Eius praesentium recusandae illo eaque architecto error, repellendus iusto reprehenderit, doloribus, minus sunt. Numquam at quae voluptatum in officia voluptas voluptatibus, minus!

94 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Nostrum molestiae debitis nobis, quod sapiente qui voluptatum, placeat magni repudiandae accusantium fugit quas labore non rerum possimus, corrupti enim modi! Et.

95 |
96 |
97 |
98 | 99 |
100 | 101 | 102 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /clean-blog/contact.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Clean Blog - Contact 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 71 | 72 | 73 | 74 |
75 |
76 |
77 |
78 |
79 |

Contact Me

80 |
81 | Have questions? I have answers (maybe). 82 |
83 |
84 |
85 |
86 |
87 | 88 | 89 |
90 |
91 |
92 |

Want to get in touch with me? Fill out the form below to send me a message and I will try to get back to you within 24 hours!

93 | 94 | 95 | 96 |
97 |
98 |
99 | 100 | 101 |

102 |
103 |
104 |
105 |
106 | 107 | 108 |

109 |
110 |
111 |
112 |
113 | 114 | 115 |

116 |
117 |
118 |
119 |
120 | 121 | 122 |

123 |
124 |
125 |
126 |
127 |
128 |
129 | 130 |
131 |
132 |
133 |
134 |
135 |
136 | 137 |
138 | 139 | 140 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | -------------------------------------------------------------------------------- /clean-blog/css/clean-blog.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Clean Blog v1.0.0 (http://startbootstrap.com) 3 | * Copyright 2014 Start Bootstrap 4 | * Licensed under Apache 2.0 (https://github.com/IronSummitMedia/startbootstrap/blob/gh-pages/LICENSE) 5 | */ 6 | 7 | body{font-family:Lora,'Times New Roman',serif;font-size:20px;color:#404040}p{line-height:1.5;margin:30px 0}p a{text-decoration:underline}h1,h2,h3,h4,h5,h6{font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;font-weight:800}a{color:#404040}a:hover,a:focus{color:#0085a1}a img:hover,a img:focus{cursor:zoom-in}blockquote{color:gray;font-style:italic}hr.small{max-width:100px;margin:15px auto;border-width:4px;border-color:#fff}.navbar-custom{position:absolute;top:0;left:0;width:100%;z-index:3;font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif}.navbar-custom .navbar-brand{font-weight:800}.navbar-custom .nav li a{text-transform:uppercase;font-size:12px;font-weight:800;letter-spacing:1px}@media only screen and (min-width:768px){.navbar-custom{background:0 0;border-bottom:1px solid transparent}.navbar-custom .navbar-brand{color:#fff;padding:20px}.navbar-custom .navbar-brand:hover,.navbar-custom .navbar-brand:focus{color:rgba(255,255,255,.8)}.navbar-custom .nav li a{color:#fff;padding:20px}.navbar-custom .nav li a:hover,.navbar-custom .nav li a:focus{color:rgba(255,255,255,.8)}}@media only screen and (min-width:1170px){.navbar-custom{-webkit-transition:background-color .3s;-moz-transition:background-color .3s;transition:background-color .3s;-webkit-transform:translate3d(0,0,0);-moz-transform:translate3d(0,0,0);-ms-transform:translate3d(0,0,0);-o-transform:translate3d(0,0,0);transform:translate3d(0,0,0);-webkit-backface-visibility:hidden;backface-visibility:hidden}.navbar-custom.is-fixed{position:fixed;top:-61px;background-color:rgba(255,255,255,.9);border-bottom:1px solid #f2f2f2;-webkit-transition:-webkit-transform .3s;-moz-transition:-moz-transform .3s;transition:transform .3s}.navbar-custom.is-fixed .navbar-brand{color:#404040}.navbar-custom.is-fixed .navbar-brand:hover,.navbar-custom.is-fixed .navbar-brand:focus{color:#0085a1}.navbar-custom.is-fixed .nav li a{color:#404040}.navbar-custom.is-fixed .nav li a:hover,.navbar-custom.is-fixed .nav li a:focus{color:#0085a1}.navbar-custom.is-visible{-webkit-transform:translate3d(0,100%,0);-moz-transform:translate3d(0,100%,0);-ms-transform:translate3d(0,100%,0);-o-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}.intro-header{background-color:gray;background:no-repeat center center;background-attachment:scroll;-webkit-background-size:cover;-moz-background-size:cover;background-size:cover;-o-background-size:cover;margin-bottom:50px}.intro-header .site-heading,.intro-header .post-heading,.intro-header .page-heading{padding:100px 0 50px;color:#fff}@media only screen and (min-width:768px){.intro-header .site-heading,.intro-header .post-heading,.intro-header .page-heading{padding:150px 0}}.intro-header .site-heading,.intro-header .page-heading{text-align:center}.intro-header .site-heading h1,.intro-header .page-heading h1{margin-top:0;font-size:50px}.intro-header .site-heading .subheading,.intro-header .page-heading .subheading{font-size:24px;line-height:1.1;display:block;font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;font-weight:300;margin:10px 0 0}@media only screen and (min-width:768px){.intro-header .site-heading h1,.intro-header .page-heading h1{font-size:80px}}.intro-header .post-heading h1{font-size:35px}.intro-header .post-heading .subheading,.intro-header .post-heading .meta{line-height:1.1;display:block}.intro-header .post-heading .subheading{font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;font-size:24px;margin:10px 0 30px;font-weight:600}.intro-header .post-heading .meta{font-family:Lora,'Times New Roman',serif;font-style:italic;font-weight:300;font-size:20px}.intro-header .post-heading .meta a{color:#fff}@media only screen and (min-width:768px){.intro-header .post-heading h1{font-size:55px}.intro-header .post-heading .subheading{font-size:30px}}.post-preview>a{color:#404040}.post-preview>a:hover,.post-preview>a:focus{text-decoration:none;color:#0085a1}.post-preview>a>.post-title{font-size:30px;margin-top:30px;margin-bottom:10px}.post-preview>a>.post-subtitle{margin:0;font-weight:300;margin-bottom:10px}.post-preview>.post-meta{color:gray;font-size:18px;font-style:italic;margin-top:0}.post-preview>.post-meta>a{text-decoration:none;color:#404040}.post-preview>.post-meta>a:hover,.post-preview>.post-meta>a:focus{color:#0085a1;text-decoration:underline}@media only screen and (min-width:768px){.post-preview>a>.post-title{font-size:36px}}.section-heading{font-size:36px;margin-top:60px;font-weight:700}.caption{text-align:center;font-size:14px;padding:10px;font-style:italic;margin:0;display:block;border-bottom-right-radius:5px;border-bottom-left-radius:5px}footer{padding:50px 0 65px}footer .list-inline{margin:0;padding:0}footer .copyright{font-size:14px;text-align:center;margin-bottom:0}.floating-label-form-group{font-size:14px;position:relative;margin-bottom:0;padding-bottom:.5em;border-bottom:1px solid #eee}.floating-label-form-group input,.floating-label-form-group textarea{z-index:1;position:relative;padding-right:0;padding-left:0;border:none;border-radius:0;font-size:1.5em;background:0 0;box-shadow:none!important;resize:none}.floating-label-form-group label{display:block;z-index:0;position:relative;top:2em;margin:0;font-size:.85em;line-height:1.764705882em;vertical-align:middle;vertical-align:baseline;opacity:0;-webkit-transition:top .3s ease,opacity .3s ease;-moz-transition:top .3s ease,opacity .3s ease;-ms-transition:top .3s ease,opacity .3s ease;transition:top .3s ease,opacity .3s ease}.floating-label-form-group::not(:first-child){padding-left:14px;border-left:1px solid #eee}.floating-label-form-group-with-value label{top:0;opacity:1}.floating-label-form-group-with-focus label{color:#0085a1}form .row:first-child .floating-label-form-group{border-top:1px solid #eee}.btn{font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;text-transform:uppercase;font-size:14px;font-weight:800;letter-spacing:1px;border-radius:0;padding:15px 25px}.btn-lg{font-size:16px;padding:25px 35px}.btn-default:hover,.btn-default:focus{background-color:#0085a1;border:1px solid #0085a1;color:#fff}.pager{margin:20px 0 0}.pager li>a,.pager li>span{font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;text-transform:uppercase;font-size:14px;font-weight:800;letter-spacing:1px;padding:15px 25px;background-color:#fff;border-radius:0}.pager li>a:hover,.pager li>a:focus{color:#fff;background-color:#0085a1;border:1px solid #0085a1}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:gray;background-color:#404040;cursor:not-allowed}::-moz-selection{color:#fff;text-shadow:none;background:#0085a1}::selection{color:#fff;text-shadow:none;background:#0085a1}img::selection{color:#fff;background:0 0}img::-moz-selection{color:#fff;background:0 0}body{webkit-tap-highlight-color:#0085a1} 8 | -------------------------------------------------------------------------------- /clean-blog/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eisneim/zexeo_tutorials/51c2e883290cc7ff9d1942fd023a7a21c61c4250/clean-blog/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /clean-blog/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eisneim/zexeo_tutorials/51c2e883290cc7ff9d1942fd023a7a21c61c4250/clean-blog/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /clean-blog/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eisneim/zexeo_tutorials/51c2e883290cc7ff9d1942fd023a7a21c61c4250/clean-blog/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /clean-blog/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eisneim/zexeo_tutorials/51c2e883290cc7ff9d1942fd023a7a21c61c4250/clean-blog/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /clean-blog/img/about-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eisneim/zexeo_tutorials/51c2e883290cc7ff9d1942fd023a7a21c61c4250/clean-blog/img/about-bg.jpg -------------------------------------------------------------------------------- /clean-blog/img/contact-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eisneim/zexeo_tutorials/51c2e883290cc7ff9d1942fd023a7a21c61c4250/clean-blog/img/contact-bg.jpg -------------------------------------------------------------------------------- /clean-blog/img/home-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eisneim/zexeo_tutorials/51c2e883290cc7ff9d1942fd023a7a21c61c4250/clean-blog/img/home-bg.jpg -------------------------------------------------------------------------------- /clean-blog/img/post-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eisneim/zexeo_tutorials/51c2e883290cc7ff9d1942fd023a7a21c61c4250/clean-blog/img/post-bg.jpg -------------------------------------------------------------------------------- /clean-blog/img/post-sample-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eisneim/zexeo_tutorials/51c2e883290cc7ff9d1942fd023a7a21c61c4250/clean-blog/img/post-sample-image.jpg -------------------------------------------------------------------------------- /clean-blog/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Clean Blog 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 71 | 72 | 73 | 74 |
75 |
76 |
77 |
78 |
79 |

Clean Blog

80 |
81 | A Clean Blog Theme by Start Bootstrap 82 |
83 |
84 |
85 |
86 |
87 | 88 | 89 |
90 |
91 | 144 |
145 |
146 | 147 |
148 | 149 | 150 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | -------------------------------------------------------------------------------- /clean-blog/less/clean-blog.less: -------------------------------------------------------------------------------- 1 | @import "variables.less"; 2 | @import "mixins.less"; 3 | 4 | // Global Components 5 | 6 | body { 7 | .serif; 8 | font-size: 20px; 9 | color: @gray-dark; 10 | } 11 | 12 | // -- Typography 13 | 14 | p { 15 | line-height: 1.5; 16 | margin: 30px 0; 17 | a { 18 | text-decoration: underline; 19 | } 20 | } 21 | 22 | h1, 23 | h2, 24 | h3, 25 | h4, 26 | h5, 27 | h6 { 28 | .sans-serif; 29 | font-weight: 800; 30 | } 31 | 32 | a { 33 | color: @gray-dark; 34 | &:hover, 35 | &:focus { 36 | color: @brand-primary; 37 | } 38 | } 39 | 40 | a img { 41 | &:hover, 42 | &:focus { 43 | cursor: zoom-in; 44 | } 45 | } 46 | 47 | blockquote { 48 | color: @gray; 49 | font-style: italic; 50 | } 51 | 52 | hr.small { 53 | max-width: 100px; 54 | margin: 15px auto; 55 | border-width: 4px; 56 | border-color: white; 57 | } 58 | 59 | // Navigation 60 | 61 | .navbar-custom { 62 | position: absolute; 63 | top: 0; 64 | left: 0; 65 | width: 100%; 66 | z-index: 3; 67 | .sans-serif; 68 | .navbar-brand { 69 | font-weight: 800; 70 | } 71 | .nav { 72 | li { 73 | a { 74 | text-transform: uppercase; 75 | font-size: 12px; 76 | font-weight: 800; 77 | letter-spacing: 1px; 78 | } 79 | } 80 | } 81 | @media only screen and (min-width: 768px) { 82 | background: transparent; 83 | border-bottom: 1px solid transparent; 84 | .navbar-brand { 85 | color: white; 86 | padding: 20px; 87 | &:hover, 88 | &:focus { 89 | color: @white-faded; 90 | } 91 | } 92 | .nav { 93 | li { 94 | a { 95 | color: white; 96 | padding: 20px; 97 | &:hover, 98 | &:focus { 99 | color: @white-faded; 100 | } 101 | } 102 | } 103 | } 104 | } 105 | @media only screen and (min-width: 1170px) { 106 | -webkit-transition: background-color 0.3s; 107 | -moz-transition: background-color 0.3s; 108 | transition: background-color 0.3s; 109 | /* Force Hardware Acceleration in WebKit */ 110 | -webkit-transform: translate3d(0, 0, 0); 111 | -moz-transform: translate3d(0, 0, 0); 112 | -ms-transform: translate3d(0, 0, 0); 113 | -o-transform: translate3d(0, 0, 0); 114 | transform: translate3d(0, 0, 0); 115 | -webkit-backface-visibility: hidden; 116 | backface-visibility: hidden; 117 | &.is-fixed { 118 | /* when the user scrolls down, we hide the header right above the viewport */ 119 | position: fixed; 120 | top: -61px; 121 | background-color: fade(white, 90%); 122 | border-bottom: 1px solid darken(white, 5%); 123 | -webkit-transition: -webkit-transform 0.3s; 124 | -moz-transition: -moz-transform 0.3s; 125 | transition: transform 0.3s; 126 | .navbar-brand { 127 | color: @gray-dark; 128 | &:hover, 129 | &:focus { 130 | color: @brand-primary; 131 | } 132 | } 133 | .nav { 134 | li { 135 | a { 136 | color: @gray-dark; 137 | &:hover, 138 | &:focus { 139 | color: @brand-primary; 140 | } 141 | } 142 | } 143 | } 144 | } 145 | &.is-visible { 146 | /* if the user changes the scrolling direction, we show the header */ 147 | -webkit-transform: translate3d(0, 100%, 0); 148 | -moz-transform: translate3d(0, 100%, 0); 149 | -ms-transform: translate3d(0, 100%, 0); 150 | -o-transform: translate3d(0, 100%, 0); 151 | transform: translate3d(0, 100%, 0); 152 | } 153 | } 154 | } 155 | 156 | // Header 157 | 158 | .intro-header { 159 | background-color: @gray; 160 | background: no-repeat center center; 161 | background-attachment: scroll; 162 | .background-cover; 163 | // NOTE: Background images are set within the HTML using inline CSS! 164 | margin-bottom: 50px; 165 | .site-heading, 166 | .post-heading, 167 | .page-heading { 168 | padding: 100px 0 50px; 169 | color: white; 170 | @media only screen and (min-width: 768px) { 171 | padding: 150px 0; 172 | } 173 | } 174 | .site-heading, 175 | .page-heading { 176 | text-align: center; 177 | h1 { 178 | margin-top: 0; 179 | font-size: 50px; 180 | } 181 | .subheading { 182 | font-size: 24px; 183 | line-height: 1.1; 184 | display: block; 185 | .sans-serif; 186 | font-weight: 300; 187 | margin: 10px 0 0; 188 | } 189 | @media only screen and (min-width: 768px) { 190 | h1 { 191 | font-size: 80px; 192 | } 193 | } 194 | } 195 | .post-heading { 196 | h1 { 197 | font-size: 35px; 198 | } 199 | .subheading, 200 | .meta { 201 | line-height: 1.1; 202 | display: block; 203 | } 204 | .subheading { 205 | .sans-serif; 206 | font-size: 24px; 207 | margin: 10px 0 30px; 208 | font-weight: 600; 209 | } 210 | .meta { 211 | .serif; 212 | font-style: italic; 213 | font-weight: 300; 214 | font-size: 20px; 215 | a { 216 | color: white; 217 | } 218 | } 219 | @media only screen and (min-width: 768px) { 220 | h1 { 221 | font-size: 55px; 222 | } 223 | .subheading { 224 | font-size: 30px; 225 | } 226 | } 227 | } 228 | } 229 | 230 | // Post Preview Pages 231 | 232 | .post-preview { 233 | > a { 234 | color: @gray-dark; 235 | &:hover, 236 | &:focus { 237 | text-decoration: none; 238 | color: @brand-primary; 239 | } 240 | > .post-title { 241 | font-size: 30px; 242 | margin-top: 30px; 243 | margin-bottom: 10px; 244 | } 245 | > .post-subtitle { 246 | margin: 0; 247 | font-weight: 300; 248 | margin-bottom: 10px; 249 | } 250 | } 251 | > .post-meta { 252 | color: @gray; 253 | font-size: 18px; 254 | font-style: italic; 255 | margin-top: 0; 256 | > a { 257 | text-decoration: none; 258 | color: @gray-dark; 259 | &:hover, 260 | &:focus { 261 | color: @brand-primary; 262 | text-decoration: underline; 263 | } 264 | } 265 | } 266 | @media only screen and (min-width: 768px) { 267 | > a { 268 | > .post-title { 269 | font-size: 36px; 270 | } 271 | } 272 | } 273 | } 274 | 275 | // Sections 276 | 277 | .section-heading { 278 | font-size: 36px; 279 | margin-top: 60px; 280 | font-weight: 700; 281 | } 282 | 283 | .caption { 284 | text-align: center; 285 | font-size: 14px; 286 | padding: 10px; 287 | font-style: italic; 288 | margin: 0; 289 | display: block; 290 | border-bottom-right-radius: 5px; 291 | border-bottom-left-radius: 5px; 292 | } 293 | 294 | footer { 295 | padding: 50px 0 65px; 296 | .list-inline { 297 | margin: 0; 298 | padding: 0; 299 | } 300 | .copyright { 301 | font-size: 14px; 302 | text-align: center; 303 | margin-bottom: 0; 304 | } 305 | } 306 | 307 | // Contact Form Styles 308 | 309 | .floating-label-form-group { 310 | font-size: 14px; 311 | position: relative; 312 | margin-bottom: 0; 313 | padding-bottom: 0.5em; 314 | border-bottom: 1px solid @gray-light; 315 | input, 316 | textarea { 317 | z-index: 1; 318 | position: relative; 319 | padding-right: 0; 320 | padding-left: 0; 321 | border: none; 322 | border-radius: 0; 323 | font-size: 1.5em; 324 | background: none; 325 | box-shadow: none !important; 326 | resize: none; 327 | } 328 | label { 329 | display: block; 330 | z-index: 0; 331 | position: relative; 332 | top: 2em; 333 | margin: 0; 334 | font-size: 0.85em; 335 | line-height: 1.764705882em; 336 | vertical-align: middle; 337 | vertical-align: baseline; 338 | opacity: 0; 339 | -webkit-transition: top 0.3s ease,opacity 0.3s ease; 340 | -moz-transition: top 0.3s ease,opacity 0.3s ease; 341 | -ms-transition: top 0.3s ease,opacity 0.3s ease; 342 | transition: top 0.3s ease,opacity 0.3s ease; 343 | } 344 | &::not(:first-child) { 345 | padding-left: 14px; 346 | border-left: 1px solid @gray-light; 347 | } 348 | } 349 | 350 | .floating-label-form-group-with-value { 351 | label { 352 | top: 0; 353 | opacity: 1; 354 | } 355 | } 356 | 357 | .floating-label-form-group-with-focus { 358 | label { 359 | color: @brand-primary; 360 | } 361 | } 362 | 363 | form .row:first-child .floating-label-form-group { 364 | border-top: 1px solid @gray-light; 365 | } 366 | 367 | // Button Styles 368 | 369 | .btn { 370 | .sans-serif; 371 | text-transform: uppercase; 372 | font-size: 14px; 373 | font-weight: 800; 374 | letter-spacing: 1px; 375 | border-radius: 0; 376 | padding: 15px 25px; 377 | } 378 | 379 | .btn-lg { 380 | font-size: 16px; 381 | padding: 25px 35px; 382 | } 383 | 384 | .btn-default { 385 | &:hover, 386 | &:focus { 387 | background-color: @brand-primary; 388 | border: 1px solid @brand-primary; 389 | color: white; 390 | } 391 | } 392 | 393 | // Pager Styling 394 | 395 | .pager { 396 | 397 | margin: 20px 0 0; 398 | 399 | li { 400 | > a, 401 | > span { 402 | .sans-serif; 403 | text-transform: uppercase; 404 | font-size: 14px; 405 | font-weight: 800; 406 | letter-spacing: 1px; 407 | padding: 15px 25px; 408 | background-color: white; 409 | border-radius: 0; 410 | } 411 | 412 | > a:hover, 413 | > a:focus { 414 | color: white; 415 | background-color: @brand-primary; 416 | border: 1px solid @brand-primary; 417 | } 418 | } 419 | 420 | .disabled { 421 | > a, 422 | > a:hover, 423 | > a:focus, 424 | > span { 425 | color: @gray; 426 | background-color: @gray-dark; 427 | cursor: not-allowed; 428 | } 429 | } 430 | } 431 | 432 | // -- Highlight Color Customization 433 | 434 | ::-moz-selection { 435 | color: white; 436 | text-shadow: none; 437 | background: @brand-primary; 438 | } 439 | 440 | ::selection { 441 | color: white; 442 | text-shadow: none; 443 | background: @brand-primary; 444 | } 445 | 446 | img::selection { 447 | color: white; 448 | background: transparent; 449 | } 450 | 451 | img::-moz-selection { 452 | color: white; 453 | background: transparent; 454 | } 455 | 456 | body { 457 | webkit-tap-highlight-color: @brand-primary; 458 | } -------------------------------------------------------------------------------- /clean-blog/less/mixins.less: -------------------------------------------------------------------------------- 1 | // Mixins 2 | 3 | .transition-all() { 4 | -webkit-transition: all 0.5s; 5 | -moz-transition: all 0.5s; 6 | transition: all 0.5s; 7 | } 8 | 9 | .background-cover() { 10 | -webkit-background-size: cover; 11 | -moz-background-size: cover; 12 | background-size: cover; 13 | -o-background-size: cover; 14 | } 15 | 16 | .serif() { 17 | font-family: 'Lora', 'Times New Roman', serif; 18 | } 19 | 20 | .sans-serif () { 21 | font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 22 | } -------------------------------------------------------------------------------- /clean-blog/less/variables.less: -------------------------------------------------------------------------------- 1 | // Variables 2 | 3 | @brand-primary: #0085A1; 4 | @gray-dark: lighten(black, 25%); 5 | @gray: lighten(black, 50%); 6 | @white-faded: fade(white, 80%); 7 | @gray-light: #eee; -------------------------------------------------------------------------------- /clean-blog/mail/contact_me.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /react_tuts/1.helloworld.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 学习React!! 6 | 7 | 8 | 9 |
10 | 11 | 12 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /react_tuts/1.nested components.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 学习React!! 6 | 7 | 8 | 9 |
10 | 11 | 12 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /react_tuts/2.states.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 学习React!! 6 | 7 | 8 | 9 |
10 | 11 | 12 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /react_tuts/2.states2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 学习React!! 6 | 7 | 8 | 9 |
10 | 11 | 12 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /react_tuts/3.props.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 学习React!! 6 | 7 | 8 | 9 |
10 | 11 | 12 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /react_tuts/4.events 1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 学习React!! 6 | 7 | 8 | 9 |
10 | 11 | 12 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /react_tuts/4.events 2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 学习React!! 6 | 7 | 8 | 9 |
10 | 11 | 12 | 127 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /react_tuts/5.two way data flow.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 学习React!! 6 | 7 | 8 | 9 |
10 | 11 | 12 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /react_tuts/6.component lifecycle-updating.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 学习React!! 6 | 7 | 8 | 9 |
10 | 11 | 12 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /react_tuts/6.component lifecycle.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 学习React!! 6 | 7 | 8 | 9 |
10 | 11 | 12 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /react_tuts/7.mixin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 学习React!! 6 | 7 | 8 | 9 |
10 | 11 | 12 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /react_tuts/index-static.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React问答 app - by 1ke.co & zexeo.com 5 | 6 | 34 | 35 | 36 |
37 |
38 |
39 |

React问答

40 | 41 |
42 |
43 |
44 |
45 |
46 | 47 | 48 |
49 | 50 | 51 | 52 |
53 |
54 |
55 |
56 | 60 | 63 |
64 |
65 |

产品经理与程序员矛盾的本质是什么?

66 |

理性探讨,请勿撕逼。产品经理的主要工作职责是产品设计。接受来自其他部门的需求,经过设计后交付研发。但这里有好些职责不清楚的地方。

67 |
68 |
69 | 70 |
71 |
72 | 76 | 79 |
80 |
81 |

热爱编程是一种怎样的体验?

82 |

别人对玩游戏感兴趣,我对写代码、看技术文章感兴趣;把泡github、stackoverflow、v2ex、reddit、csdn当做是兴趣爱好;遇到重复的工作,总想着能不能通过程序实现自动化;喝酒的时候把写代码当下酒菜,边喝边想边敲;不给工资我也会来加班;做梦都在写代码。

83 |
84 |
85 | 86 |
87 | 88 |
89 |
90 | 91 | 93 | 94 | -------------------------------------------------------------------------------- /react_tuts/reactQa/app/js/components/QuestionApp.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var ShowAddButton = require('./ShowAddButton.js'); 3 | var QuestionForm = require('./QuestionForm.js'); 4 | var QuestionList = require('./QuestionList.js'); 5 | var _ = require('lodash'); 6 | 7 | module.exports = React.createClass({ 8 | getInitialState:function(){ 9 | var questions = [ 10 | { 11 | key: 1, 12 | title:'产品经理与程序员矛盾的本质是什么?', 13 | description:'理性探讨,请勿撕逼。产品经理的主要工作职责是产品设计。接受来自其他部门的需求,经过设计后交付研发。但这里有好些职责不清楚的地方。', 14 | voteCount: 10, 15 | }, 16 | { 17 | key: 2, 18 | title:'热爱编程是一种怎样的体验?', 19 | description:'别人对玩游戏感兴趣,我对写代码、看技术文章感兴趣;把泡github、stackoverflow、v2ex、reddit、csdn当做是兴趣爱好;遇到重复的工作,总想着能不能通过程序实现自动化;喝酒的时候把写代码当下酒菜,边喝边想边敲;不给工资我也会来加班;做梦都在写代码。', 20 | voteCount: 8, 21 | }, 22 | ]; 23 | 24 | return { 25 | questions: questions, 26 | formDisplayed: false, 27 | } 28 | }, 29 | onToggleForm:function(){ 30 | this.setState({ 31 | formDisplayed: !this.state.formDisplayed, 32 | }) 33 | }, 34 | onNewQuestion:function( newQuestion ){ 35 | newQuestion.key = this.state.questions.length + 1; 36 | 37 | var newQuestions = this.state.questions.concat( newQuestion ); 38 | 39 | newQuestions = this.sortQuestion( newQuestions ); 40 | 41 | this.setState({ 42 | questions: newQuestions, 43 | }) 44 | }, 45 | sortQuestion:function(questions){ 46 | questions.sort(function(a,b){ 47 | return b.voteCount - a.voteCount; 48 | }); 49 | 50 | return questions; 51 | 52 | }, 53 | onVote:function(key,newCount){ 54 | var questions = _.uniq( this.state.questions ); 55 | var index = _.findIndex( questions, function(qst){ 56 | return qst.key == key; 57 | } ) 58 | 59 | questions[index].voteCount = newCount; 60 | 61 | questions = this.sortQuestion(questions); 62 | 63 | this.setState({ 64 | questions: questions 65 | }) 66 | }, 67 | render:function(){ 68 | return ( 69 |
70 |
71 |
72 |

React 问答

73 | 74 |
75 |
76 |
77 | 81 | 82 | 83 | 84 |
85 |
86 | ) 87 | } 88 | }) -------------------------------------------------------------------------------- /react_tuts/reactQa/app/js/components/QuestionForm.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | module.exports = React.createClass({ 4 | handleForm:function(e){ 5 | e.preventDefault(); 6 | if(!this.refs.title.getDOMNode().value) return; 7 | 8 | var newQuestion = { 9 | title: this.refs.title.getDOMNode().value, 10 | description: this.refs.description.getDOMNode().value, 11 | voteCount: 0, 12 | } 13 | 14 | this.refs.addQuestionForm.getDOMNode().reset(); 15 | 16 | this.props.onNewQuestion( newQuestion ); 17 | }, 18 | render:function(){ 19 | var styleObj={ 20 | display: this.props.formDisplayed ? 'block': 'none', 21 | } 22 | 23 | return ( 24 |
25 |
26 | 27 | 28 |
29 | 30 | 31 | 取消 32 |
33 | ) 34 | } 35 | }) -------------------------------------------------------------------------------- /react_tuts/reactQa/app/js/components/QuestionItem.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | module.exports = React.createClass({ 4 | voteUp:function(e){ 5 | 6 | var newCount = parseInt(this.props.voteCount ,10) + 1; 7 | this.props.onVote( this.props.questionKey, newCount ) 8 | }, 9 | voteDown:function(e){ 10 | var newCount = parseInt(this.props.voteCount ,10) - 1; 11 | this.props.onVote( this.props.questionKey, newCount ) 12 | }, 13 | render:function(){ 14 | return ( 15 |
16 |
17 | 21 | 24 |
25 |
26 |

{this.props.title}

27 |

{this.props.description}

28 |
29 |
30 | ) 31 | } 32 | }) -------------------------------------------------------------------------------- /react_tuts/reactQa/app/js/components/QuestionList.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var QuestionItem = require('./QuestionItem.js'); 3 | 4 | 5 | module.exports = React.createClass({ 6 | render:function(){ 7 | var questions = this.props.questions; 8 | if( !Array.isArray(questions) ) throw new Error('this.props.questions问题必须是数组'); 9 | var questionComps = questions.map(function(qst){ 10 | return 16 | }.bind(this) ); 17 | 18 | return ( 19 |
20 | {questionComps} 21 |
22 | ) 23 | } 24 | }) -------------------------------------------------------------------------------- /react_tuts/reactQa/app/js/components/ShowAddButton.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | module.exports = React.createClass({ 4 | render:function(){ 5 | return ( 6 | 7 | ) 8 | } 9 | }) -------------------------------------------------------------------------------- /react_tuts/reactQa/app/js/main.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var QuestionApp = require('./components/QuestionApp.js'); 3 | 4 | var mainCom = React.render( 5 | , 6 | document.getElementById('app') 7 | ) 8 | -------------------------------------------------------------------------------- /react_tuts/reactQa/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactQa", 3 | "version": "0.0.0", 4 | "authors": [ 5 | "eisneim " 6 | ], 7 | "license": "MIT", 8 | "ignore": [ 9 | "**/.*", 10 | "node_modules", 11 | "bower_components", 12 | "test", 13 | "tests" 14 | ], 15 | "dependencies": { 16 | "bootstrap": "~3.3.4" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /react_tuts/reactQa/gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | connect = require('gulp-connect'), 3 | browserify = require('gulp-browserify'), 4 | concat = require('gulp-concat'), 5 | port = process.env.port || 5000; 6 | 7 | gulp.task('browserify',function(){ 8 | gulp.src('./app/js/main.js') 9 | .pipe(browserify({ 10 | transform: 'reactify', 11 | })) 12 | .pipe(gulp.dest('./dist/js')) 13 | }); 14 | 15 | // live reload 16 | gulp.task('connect',function(){ 17 | connect.server({ 18 | // root:'./', 19 | port: port, 20 | livereload: true, 21 | }) 22 | }) 23 | 24 | // reload Js 25 | gulp.task('js',function(){ 26 | gulp.src('./dist/**/*.js') 27 | .pipe( connect.reload() ) 28 | }) 29 | 30 | gulp.task('html',function(){ 31 | gulp.src('./app/**/*.html') 32 | .pipe( connect.reload() ) 33 | }); 34 | 35 | gulp.task('watch',function(){ 36 | gulp.watch('./dist/**/*.js',['js']); 37 | gulp.watch('./app/**/*.html',['html']); 38 | gulp.watch('./app/js/**/*.js',['browserify']); 39 | }) 40 | 41 | gulp.task('default',['browserify']); 42 | 43 | gulp.task('serve',['browserify','connect','watch']); 44 | -------------------------------------------------------------------------------- /react_tuts/reactQa/index-static.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React问答 app - by 1ke.co & zexeo.com 5 | 6 | 34 | 35 | 36 |
37 |
38 |
39 |

React问答

40 | 41 |
42 |
43 |
44 |
45 |
46 | 47 | 48 |
49 | 50 | 51 | 52 |
53 |
54 |
55 |
56 | 60 | 63 |
64 |
65 |

产品经理与程序员矛盾的本质是什么?

66 |

理性探讨,请勿撕逼。产品经理的主要工作职责是产品设计。接受来自其他部门的需求,经过设计后交付研发。但这里有好些职责不清楚的地方。

67 |
68 |
69 | 70 |
71 |
72 | 76 | 79 |
80 |
81 |

热爱编程是一种怎样的体验?

82 |

别人对玩游戏感兴趣,我对写代码、看技术文章感兴趣;把泡github、stackoverflow、v2ex、reddit、csdn当做是兴趣爱好;遇到重复的工作,总想着能不能通过程序实现自动化;喝酒的时候把写代码当下酒菜,边喝边想边敲;不给工资我也会来加班;做梦都在写代码。

83 |
84 |
85 | 86 |
87 | 88 |
89 |
90 | 91 | 93 | 94 | -------------------------------------------------------------------------------- /react_tuts/reactQa/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React问答 app - by 1ke.co & zexeo.com 5 | 6 | 34 | 35 | 36 |
37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /react_tuts/reactQa/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactQa", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "react": "^0.13.1" 13 | }, 14 | "devDependencies": { 15 | "gulp": "^3.8.11", 16 | "gulp-browserify": "^0.5.1", 17 | "gulp-concat": "^2.5.2", 18 | "gulp-connect": "^2.2.0", 19 | "gulp-react": "^3.0.1", 20 | "lodash": "^3.6.0", 21 | "reactify": "^1.1.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /redux-chat/.gitinore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules 3 | .DS_Store 4 | 5 | dist/ 6 | public/vendor/ 7 | public/build/ 8 | 9 | -------------------------------------------------------------------------------- /redux-chat/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-chat", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "testServer": "mocha --compilers js:babel-core/register --require ./test/serverTestHelper.js ./test/server --recursive", 8 | "testServer:watch": "npm run testServer -- --watch", 9 | "testClient": "mocha --compilers js:babel-core/register --require ./test/ClientTestHelper.js ./test/client --recursive", 10 | "testClient:watch": "npm run testClient -- --watch" 11 | }, 12 | "babel": { 13 | "presets": [ 14 | "es2015", 15 | "react" 16 | ] 17 | }, 18 | "keywords": [], 19 | "author": "", 20 | "license": "ISC", 21 | "dependencies": { 22 | "ejs": "^2.3.4", 23 | "express": "^4.13.3", 24 | "immutable": "^3.7.5", 25 | "mocha": "^2.3.4", 26 | "react": "^0.14.3", 27 | "react-addons-pure-render-mixin": "^0.14.3", 28 | "react-dom": "^0.14.3", 29 | "react-hot-loader": "^1.3.0", 30 | "react-mixin": "^3.0.3", 31 | "react-redux": "^4.0.0", 32 | "redux": "^3.0.4", 33 | "socket.io": "^1.3.7", 34 | "socket.io-client": "^1.3.7", 35 | "uuid": "^2.0.1" 36 | }, 37 | "devDependencies": { 38 | "babel-cli": "^6.2.0", 39 | "babel-core": "^6.2.1", 40 | "babel-loader": "^6.2.0", 41 | "babel-preset-es2015": "^6.1.18", 42 | "babel-preset-react": "^6.1.18", 43 | "chai": "^3.4.1", 44 | "chai-immutable": "^1.5.3", 45 | "webpack": "^1.12.9", 46 | "webpack-dev-server": "^1.14.0", 47 | "jsdom": "^5.6.1", 48 | "react-addons-test-utils": "^0.14.3" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /redux-chat/public/css/style.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | *, *:before, *:after { 6 | -webkit-box-sizing: border-box; 7 | -moz-box-sizing: border-box; 8 | box-sizing: border-box; 9 | } 10 | 11 | article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block;}audio,canvas,video{display:inline-block;}audio:not([controls]){display:none;height:0;}[hidden]{display:none;}html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;}body{margin:0;}a:focus{outline:thin dotted;}a:active,a:hover{outline:0;}h1{font-size:2em;margin:0.67em 0;}abbr[title]{border-bottom:1px dotted;}b,strong{font-weight:bold;}dfn{font-style:italic;}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0;}mark{background:#ff0;color:#000;}code,kbd,pre,samp{font-family:monospace,serif;font-size:1em;}pre{white-space:pre-wrap;}q{quotes:"\201C" "\201D" "\2018" "\2019";}small{font-size:80%;}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline;}sup{top:-0.5em;}sub{bottom:-0.25em;}img{border:0;}svg:not(:root){overflow:hidden;}figure{margin:0;}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em;}legend{border:0;padding:0;}button,input,select,textarea{font-family:inherit;font-size:100%;margin:0;}button,input{line-height:normal;}button,select{text-transform:none;}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer;}button[disabled],html input[disabled]{cursor:default;}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0;}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box;}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none;}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0;}textarea{overflow:auto;vertical-align:top;}table{border-collapse:collapse;border-spacing:0;} 12 | h1,h2,h3,h4,h5{font-weight: normal;} 13 | 14 | .clearfix:before, 15 | .clearfix:after { 16 | content: " "; /* 1 */ 17 | display: table; /* 2 */ 18 | } 19 | .clearfix:after { 20 | clear: both; 21 | } 22 | 23 | .center-block { 24 | display: block; 25 | margin-left: auto; 26 | margin-right: auto; 27 | } 28 | .pull-right { 29 | float: right !important; 30 | } 31 | .pull-left { 32 | float: left !important; 33 | } 34 | .text-left{text-align: left;} 35 | .text-center{text-align: center;} 36 | .text-right{text-align: right;} 37 | 38 | .flex-row{ 39 | display: flex; 40 | flex-direction: row; 41 | /*// align-items: center;*/ 42 | } 43 | .flex-column{ 44 | flex-direction: column; 45 | display: flex; 46 | justify-content: center; 47 | } 48 | .flex-center{ 49 | display: flex; 50 | align-items: center; 51 | justify-content: center; 52 | } 53 | .flex{ 54 | flex:1; 55 | align-self:center; 56 | /*align-self: auto | flex-start | flex-end | center | baseline | stretch;*/ 57 | } 58 | 59 | 60 | body{ 61 | font-family: Helvetica Neue,"Hiragino Sans GB","Microsoft YaHei","微软雅黑","STHeiti","WenQuanYi Micro Hei",SimSun,sans-serif,Arial; 62 | background-color: #fafafa; 63 | height: 100%; 64 | } 65 | html,body,#app,#app>.flex-row,#chat-main{ 66 | height: 100%; 67 | } 68 | 69 | 70 | #chat-nav{ 71 | width: 250px; 72 | border-right: solid 1px #ddd; 73 | padding:20px; 74 | } 75 | .chat-room-list a{ 76 | display: block; 77 | padding-left: 15px; 78 | color: #27AE60; 79 | margin-bottom:10px; 80 | } 81 | 82 | .chat-room-list a.active{ 83 | background-color: #ddd; 84 | padding-top:8px;padding-bottom:8px; 85 | text-decoration: none; 86 | } 87 | 88 | #chat-main{ 89 | /*overflow-y: auto;*/ 90 | position: relative; 91 | } 92 | #chat-main header{ 93 | border-bottom: solid 1px #ddd; 94 | background-color: #fff; 95 | padding: 15px 10px; 96 | } 97 | #chat-main header h3{ 98 | margin: 0; 99 | } 100 | 101 | 102 | .chat-messages{ 103 | display: block; 104 | list-style: none; 105 | padding:10px; 106 | margin: 0; 107 | height: 80%; 108 | height: calc( 100% - 80px ); 109 | overflow-y:auto; 110 | padding-bottom: 100px; 111 | } 112 | .chat-messages >li{ 113 | } 114 | .message-inner{ 115 | background-color: #2ECC71; 116 | display: inline-block; 117 | width:auto; 118 | padding:8px 12px; 119 | margin:5px 0; 120 | border-radius: 6px; 121 | color: #fff; 122 | outline: none; 123 | min-width: 300px; 124 | } 125 | .message-inner p{ 126 | line-height: 1; 127 | margin:6px 0; 128 | } 129 | 130 | .message-self .message-inner{ 131 | background-color: #eee; 132 | color:#333; 133 | float: right; 134 | } 135 | .chat-username{ 136 | font-size: 1.2em; 137 | } 138 | .chat-username small{ 139 | margin-left: 10px; color:#ccc; 140 | } 141 | 142 | #chat-inputbox{ 143 | width:100%; 144 | position: absolute; 145 | padding: 10px; 146 | bottom:0; 147 | left:0; 148 | border-top: solid 1px #ddd; 149 | background-color: #fff; 150 | } 151 | #chat-inputbox textarea{ 152 | width:100%; 153 | min-height: 60px 154 | margin:0; 155 | } 156 | 157 | /*------------------------------*/ 158 | 159 | .btn { 160 | display: inline-block; 161 | padding: 8px 16px; 162 | border: 2px solid #ddd; 163 | color: #ddd; 164 | text-decoration: none; 165 | margin: 0px 10px 0 0; 166 | transition: all .6s; 167 | min-width: 100px; 168 | text-align: center; 169 | background-color: transparent; 170 | } 171 | .btn:hover { 172 | background: #ddd; 173 | color: #fff; 174 | } 175 | .btn.sm{ 176 | padding: 3px 8px; 177 | font-size:14px; 178 | } 179 | .btn.md{ 180 | padding: 12px 20px 181 | } 182 | /* Big Size */ 183 | .btn.lg{ 184 | padding: 20px 30px; 185 | } 186 | /* Border Radius */ 187 | .btn.style-1{ 188 | border-radius: 10px; 189 | } 190 | .btn.style-2{ 191 | border-radius: 40px; 192 | } 193 | 194 | /* Color 1 #16A085 */ 195 | .btn.color-1 {border-color: #16A085;color: #16A085;} 196 | .btn.color-1:hover, .color-1.active {background: #16A085;color: #fff;} 197 | 198 | /* Color 2 #27AE60 */ 199 | .btn.color-2 {border-color: #27AE60;color: #27AE60;} 200 | .btn.color-2:hover,.color-2.active {background: #27AE60;color: #fff;} 201 | 202 | /* Color 3 #2980B9 */ 203 | .btn.color-3 {border-color: #2980B9;color: #2980B9;} 204 | .btn.color-3:hover,.color-3.active {background: #2980B9;color: #fff;} 205 | 206 | /* Color 4 #8E44AD */ 207 | .btn.color-4 {border-color: #8E44AD;color: #8E44AD;} 208 | .btn.color-4:hover,.color-4.active {background: #8E44AD;color: #fff;} 209 | 210 | /* Color 5 #2C3E50 */ 211 | .btn.color-5 {border-color: #2C3E50;color: #2C3E50;} 212 | .btn.color-5:hover,.color-5.active {background: #2C3E50;color: #fff;} 213 | -------------------------------------------------------------------------------- /redux-chat/reduxtuts.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": 3 | [ 4 | { 5 | "path": "/Users/eisneim/www/learn/es6" 6 | }, 7 | { 8 | "path": "redux-chat" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /redux-chat/src/client/actionCreators.js: -------------------------------------------------------------------------------- 1 | import {fromJS,Map} from "immutable" 2 | import {yymmddhhmm} from "../shared/utils/dateTime" 3 | 4 | export function setState(state){ 5 | return { 6 | type:"SET_STATE", 7 | state: Map.isMap(state) ? state : fromJS(state) 8 | } 9 | } 10 | 11 | export function setUsername(username){ 12 | return { 13 | type: "SET_USERNAME", username 14 | } 15 | } 16 | 17 | export function switchRoom(roomId){ 18 | return { 19 | type:"SWITCH_ROOM", roomId, 20 | meta:{ remote:true }, 21 | } 22 | } 23 | 24 | export function newMessage({roomId,content,user,time}, isFromServer ){ 25 | return { 26 | type:"NEW_MESSAGE", 27 | meta:{ remote: !isFromServer }, 28 | message: { 29 | roomId, content:content||"", user, 30 | time: yymmddhhmm() 31 | } 32 | } 33 | } 34 | 35 | export function addRoom( room ){ 36 | if( !room || !room.owner) throw new Error("addRoom() room.owner is required") 37 | 38 | return { 39 | type:"ADD_ROOM", room, 40 | meta:{ remote:true }, 41 | } 42 | } 43 | 44 | export function removeRoom( id, user ){ 45 | return { 46 | type:"REMOVE_ROOM", 47 | payload:{ id, user }, 48 | meta:{ remote:true }, 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /redux-chat/src/client/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React,{ Component } from "react" 2 | 3 | import MessageList from "./MessageList" 4 | import InputBox from "./InputBox" 5 | import RoomList from "./RoomList" 6 | import { newMessage, switchRoom, addRoom,removeRoom } from "../actionCreators" 7 | 8 | class App extends Component { 9 | getCurrentRoomName(){ 10 | if(!this.props.currentRoom ) return "无" 11 | const room = this.props.rooms.find(r=>r.get("id") === this.props.currentRoom ) 12 | return ( room && room.get ) ? room.get("name") : room 13 | } 14 | 15 | isOwner( ){ 16 | if(!this.props.currentRoom || !this.props.username ) return false 17 | const room = this.props.rooms.find(r=>r.get("id") === this.props.currentRoom ) 18 | if(!room) return false; 19 | return room.get("owner") == this.props.username 20 | } 21 | 22 | getMessages(){ 23 | return this.props.messages ? 24 | this.props.messages.get(this.props.currentRoom) : [] 25 | } 26 | 27 | addRoom(){ 28 | var name = prompt("房间名称") 29 | if(!name) return alert("不能没有房间名称") 30 | 31 | this.props.dispatch( addRoom({ 32 | name, owner: this.props.username 33 | }) ) 34 | } 35 | 36 | removeRoom(){ 37 | this.props.dispatch(switchRoom( )) 38 | 39 | this.props.dispatch( removeRoom( 40 | this.props.currentRoom, this.props.username 41 | ) ) 42 | } 43 | 44 | sendMessage(message){ 45 | this.props.dispatch( newMessage({ 46 | roomId: this.props.currentRoom, 47 | user: this.props.username, 48 | content: message 49 | }) ) 50 | } 51 | 52 | render(){ 53 | const { currentRoom, rooms, username, dispatch } = this.props 54 | 55 | return ( 56 |
57 | 66 | { !currentRoom ?

请选择一个聊天室

: 67 |
68 |
69 |

当前聊天室:{ this.getCurrentRoomName() }

70 | 71 | { !this.isOwner() ? "": 72 | 74 | } 75 |
76 | 77 | 78 |
79 | } 80 |
81 | ) 82 | } 83 | } 84 | 85 | 86 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 87 | import reactMixin from "react-mixin"; 88 | reactMixin.onClass(App, PureRenderMixin ) 89 | 90 | import { connect } from 'react-redux' 91 | function mapStateToProps ( state ){ 92 | return { 93 | rooms: state.get("rooms"), 94 | currentRoom: state.get("currentRoom"), 95 | username: state.get("username"), 96 | messages: state.get("messages") 97 | } 98 | } 99 | 100 | export const ConnectedApp = connect( mapStateToProps )(App) 101 | 102 | export default App 103 | -------------------------------------------------------------------------------- /redux-chat/src/client/components/InputBox.jsx: -------------------------------------------------------------------------------- 1 | import React,{ Component } from "react" 2 | import ReactDOM from "react-dom" 3 | 4 | class InputBox extends Component { 5 | 6 | handleSubmit(e){ 7 | e.preventDefault() 8 | var $textarea = ReactDOM.findDOMNode( this.refs.textarea ) 9 | if( typeof this.props.sendMessage === "function" ){ 10 | this.props.sendMessage( $textarea.value ) 11 | $textarea.value = "" 12 | }else{ 13 | console.log("props.sendMessage not defined!!") 14 | } 15 | 16 | } 17 | 18 | render(){ 19 | return ( 20 |
21 |
22 |
23 | 24 |
25 |
26 | 27 |
28 |
29 |
30 | ) 31 | } 32 | } 33 | 34 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 35 | import reactMixin from "react-mixin"; 36 | reactMixin.onClass(InputBox, PureRenderMixin ) 37 | 38 | 39 | export default InputBox -------------------------------------------------------------------------------- /redux-chat/src/client/components/Message.jsx: -------------------------------------------------------------------------------- 1 | import React,{ Component } from "react" 2 | 3 | class Message extends Component { 4 | render(){ 5 | const { message, isSelf } = this.props 6 | const className = isSelf ? "message-self":"" 7 | 8 | return ( 9 |
  • 10 |
    11 |

    {message.get("user")} 12 | {message.get("time")} 13 |

    14 |

    {message.get("content")}

    15 |
    16 |
  • 17 | ) 18 | } 19 | } 20 | 21 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 22 | import reactMixin from "react-mixin"; 23 | reactMixin.onClass(Message, PureRenderMixin ) 24 | 25 | export default Message 26 | -------------------------------------------------------------------------------- /redux-chat/src/client/components/MessageList.jsx: -------------------------------------------------------------------------------- 1 | import React,{ Component, PropTypes } from "react" 2 | import Message from "./Message" 3 | 4 | class MessageList extends Component { 5 | isSelf( message ){ 6 | return this.props.username === message.get("user") 7 | } 8 | 9 | $getMessages( messages ){ 10 | if(!messages || messages.size == 0) 11 | return

    还没有信息

    12 | 13 | return messages.map((message,index)=>{ 14 | return 17 | }) 18 | } 19 | 20 | render(){ 21 | return ( 22 |
      23 | { 24 | this.$getMessages( this.props.messages ) 25 | } 26 |
    27 | ) 28 | } 29 | 30 | } 31 | 32 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 33 | import reactMixin from "react-mixin"; 34 | reactMixin.onClass(MessageList, PureRenderMixin ) 35 | 36 | export default MessageList 37 | -------------------------------------------------------------------------------- /redux-chat/src/client/components/RoomList.jsx: -------------------------------------------------------------------------------- 1 | import React,{ Component} from "react" 2 | 3 | class RoomList extends Component { 4 | 5 | isActive( room, currentRoom ){ 6 | return room.get("id") === currentRoom 7 | } 8 | 9 | render(){ 10 | const {rooms,currentRoom} = this.props 11 | 12 | return ( 13 |
    14 | { 15 | rooms.map((room,index)=>{ 16 | return ( 17 | this.props.switchRoom(room.get("id")) } 19 | key={index} href="#"> 20 | {room.get("name")} 21 | 22 | ) 23 | }) 24 | } 25 |
    26 | ) 27 | } 28 | } 29 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 30 | import reactMixin from "react-mixin"; 31 | reactMixin.onClass( RoomList, PureRenderMixin ) 32 | 33 | export default RoomList -------------------------------------------------------------------------------- /redux-chat/src/client/index.js: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom" 2 | import React from "react" 3 | import {ConnectedApp} from "./components/App" 4 | 5 | import {Provider} from "react-redux" 6 | 7 | import {createStore, applyMiddleware } from 'redux' 8 | import { logger, socketMiddleware } from "./middleware" 9 | 10 | import rootReducer from "./reducer" 11 | import { setState, newMessage } from "./actionCreators" 12 | import { getInitialState, saveToStorage } from "./store.js" 13 | 14 | import {socket} from "./io" 15 | 16 | const createStoreWithMiddleware = applyMiddleware( 17 | logger, 18 | socketMiddleware(socket) 19 | )( createStore ) 20 | const store = createStoreWithMiddleware( rootReducer, getInitialState() ) 21 | 22 | socket.on("state",state => { 23 | store.dispatch(setState(state)) 24 | }) 25 | 26 | socket.on("message",message => { 27 | console.log("get message from server") 28 | store.dispatch( newMessage(message, true ) ) 29 | }) 30 | 31 | 32 | // --------------------------- 33 | 34 | var $app = document.getElementById("app") 35 | 36 | function render(){ 37 | // const store = store.getState() 38 | 39 | ReactDOM.render( 40 | 41 | 42 | , 43 | $app 44 | ) 45 | } 46 | 47 | render() 48 | 49 | store.subscribe(()=>{ 50 | saveToStorage( store.getState() ) 51 | }) 52 | 53 | -------------------------------------------------------------------------------- /redux-chat/src/client/io.js: -------------------------------------------------------------------------------- 1 | import IO from "socket.io-client" 2 | 3 | export const socket = IO("http://localhost:3000") 4 | 5 | socket.on('disconnect', ()=>{ 6 | console.log('user disconnected'); 7 | }); -------------------------------------------------------------------------------- /redux-chat/src/client/middleware.js: -------------------------------------------------------------------------------- 1 | 2 | export const socketMiddleware = socket => store => next => action => { 3 | if (action.meta && action.meta.remote) { 4 | socket.emit('action', action); 5 | } 6 | 7 | return next(action) 8 | } 9 | 10 | /** 11 | * 记录所有被发起的 action 以及产生的新的 state。 12 | */ 13 | export const logger = store => next => action => { 14 | console.group(action.type) 15 | console.info('dispatching', action) 16 | let result = next(action) 17 | const nextState = store.getState() 18 | console.log('next state', nextState.toJS? nextState.toJS() : nextState ) 19 | console.groupEnd(action.type) 20 | return result 21 | } 22 | 23 | /** 24 | * 让你可以发起一个函数来替代 action。 25 | * 这个函数接收 `dispatch` 和 `getState` 作为参数。 26 | * 27 | * 对于(根据 `getState()` 的情况)提前退出,或者异步控制流( `dispatch()` 一些其他东西)来说,这非常有用。 28 | * 29 | * `dispatch` 会返回被发起函数的返回值。 30 | */ 31 | export const thunk = store => next => action => 32 | typeof action === 'function' ? 33 | action(store.dispatch, store.getState) : 34 | next(action) 35 | 36 | /* 37 | onclick={dispatch( (dispatch, getState)=>{ 38 | const state = getState() 39 | if(something wrong) return 40 | 41 | doSomeApiRequest( state.apiurl ) 42 | .then(data=>{ 43 | dispatch( requestSomeApiSuccess(data) ) 44 | }, error => dispatch(requestSomeApiFail(e)) ) 45 | })} 46 | */ 47 | 48 | /** 49 | * 用 { meta: { delay: N } } 来让 action 延迟 N 毫秒。 50 | * 在这个案例中,让 `dispatch` 返回一个取消 timeout 的函数。 51 | */ 52 | export const timeoutScheduler = store => next => action => { 53 | if (!action.meta || !action.meta.delay) { 54 | return next(action) 55 | } 56 | 57 | let timeoutId = setTimeout( 58 | () => next(action), 59 | action.meta.delay 60 | ) 61 | 62 | return function cancel() { 63 | clearTimeout(timeoutId) 64 | } 65 | } 66 | 67 | 68 | /** 69 | * 使你除了 action 之外还可以发起 promise。 70 | * 如果这个 promise 被 resolved,他的结果将被作为 action 发起。 71 | * 这个 promise 会被 `dispatch` 返回,因此调用者可以处理 rejection。 72 | */ 73 | const vanillaPromise = store => next => action => { 74 | if (typeof action.then !== 'function') { 75 | return next(action) 76 | } 77 | 78 | return Promise.resolve(action).then(store.dispatch) 79 | } 80 | 81 | /** 82 | * 让你可以发起带有一个 { promise } 属性的特殊 action。 83 | * 84 | * 这个 middleware 会在开始时发起一个 action,并在这个 `promise` resolve 时发起另一个成功(或失败)的 action。 85 | * 86 | * 为了方便起见,`dispatch` 会返回这个 promise 让调用者可以等待。 87 | */ 88 | const readyStatePromise = store => next => action => { 89 | if (!action.promise) { 90 | return next(action) 91 | } 92 | 93 | function makeAction(ready, data) { 94 | let newAction = Object.assign({}, action, { ready }, data) 95 | delete newAction.promise 96 | return newAction 97 | } 98 | 99 | next(makeAction(false)) 100 | return action.promise.then( 101 | result => next(makeAction(true, { result })), 102 | error => next(makeAction(true, { error })) 103 | ) 104 | } 105 | -------------------------------------------------------------------------------- /redux-chat/src/client/reducer.js: -------------------------------------------------------------------------------- 1 | import {fromJS,Map,List} from "immutable" 2 | 3 | export default function rootReducer(state=Map(),action){ 4 | switch ( action.type ){ 5 | case "SET_STATE": return state.merge(Map( action.state )) 6 | 7 | case "SET_USERNAME": 8 | return state.set("username", action.username ) 9 | 10 | case "SWITCH_ROOM": 11 | return state.set("currentRoom", action.roomId ) 12 | 13 | case "NEW_MESSAGE": 14 | if(!action.message||!action.message.roomId) 15 | return state 16 | 17 | if( state.get("messages").has( action.message.roomId ) ){ 18 | return state.updateIn( 19 | ["messages",action.message.roomId], 20 | array => array.push( Map(action.message) ) 21 | ) 22 | } else{ 23 | return state.setIn( 24 | ["messages",action.message.roomId], 25 | List.of( Map(action.message)) 26 | ) 27 | } 28 | 29 | default: return state 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /redux-chat/src/client/store.js: -------------------------------------------------------------------------------- 1 | import {Map,fromJS} from "immutable" 2 | 3 | const STATE_KEY = "CHAT_APP_STATE" 4 | 5 | export function saveToStorage( state ){ 6 | var data = JSON.stringify(state.toJS ? state.toJS() : state) 7 | localStorage.setItem(STATE_KEY, data ) 8 | } 9 | 10 | export function getInitialState( ){ 11 | var stateString = localStorage.getItem( STATE_KEY ) 12 | if( !stateString ) { 13 | return fromJS({ 14 | rooms:[],messages:{}, 15 | username: prompt("用户名") 16 | }) 17 | } 18 | 19 | return fromJS(JSON.parse( stateString )) 20 | } 21 | -------------------------------------------------------------------------------- /redux-chat/src/server/actionCreator.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export function addRoom( room ){ 4 | return { 5 | type:"ADD_ROOM",room: room 6 | } 7 | } 8 | 9 | export function removeRoom( payload ){ 10 | return { 11 | type:"REMOVE_ROOM",payload: payload 12 | } 13 | } -------------------------------------------------------------------------------- /redux-chat/src/server/controller.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import {renderToString} from "react-dom/server" 3 | 4 | import {Provider} from "react-redux" 5 | import {ConnectedApp} from "../client/components/App" 6 | 7 | export const indexCtrl = store => ( req,res ) =>{ 8 | var appString = renderToString( 9 | 10 | 11 | 12 | ) 13 | 14 | const HTML = ` 15 | 16 | 17 | 18 | 19 | Redux socket.io Chat app 20 | 21 | 22 | 23 |
    ${appString}
    24 | 25 | 26 | 27 | `; 28 | res.end(HTML) 29 | } 30 | 31 | -------------------------------------------------------------------------------- /redux-chat/src/server/core.js: -------------------------------------------------------------------------------- 1 | import {Map,List,fromJS} from "immutable" 2 | import {v1} from "uuid" 3 | 4 | export const INITIAL_STATE = fromJS({ 5 | rooms:[], 6 | }) 7 | 8 | export function addRoom( state=INITIAL_STATE, room ){ 9 | if( !room || !room.owner) return state 10 | 11 | return state.update("rooms", rooms => rooms.push(Map( { 12 | id: room.id || v1(), 13 | name: room.name || "no name", 14 | owner: room.owner, 15 | } )) ) 16 | } 17 | 18 | export function removeRoom( state, {id,user}){ 19 | const rooms = state.get("rooms") 20 | var index = rooms.findIndex( r => r.get("id") === id ) 21 | if(index == -1 || rooms.getIn([index,"owner"])!== user ) { 22 | // console.log("非房间创建者,不能删除该房间",index) 23 | return state 24 | } 25 | return state.update("rooms",rooms => rooms.splice(index,1) ) 26 | } 27 | 28 | 29 | -------------------------------------------------------------------------------- /redux-chat/src/server/io.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_ROOM = "0" 2 | 3 | export default function listenWebSocket( io, store ){ 4 | io.on("connection", socket=>{ 5 | console.log("one client connected") 6 | 7 | socket.emit("state", store.getState() ) 8 | // join this to the default room 9 | socket.join( DEFAULT_ROOM ) 10 | // add/remove room logic goes here 11 | socket.on("action",action => { 12 | console.log("client action:", action ) 13 | switch( action.type ){ 14 | case "SWITCH_ROOM": 15 | return switchRoom( socket, action.roomId || DEFAULT_ROOM ) 16 | 17 | // send this message back 18 | case "NEW_MESSAGE": 19 | if( socket.rooms && socket.rooms.length>0 ){ 20 | socket.rooms.forEach(id=>{ 21 | socket.to( id ).emit("message", action.message) 22 | }) 23 | }else{ 24 | socket.emit( "message", action.message ) 25 | } 26 | return 27 | } 28 | 29 | store.dispatch(action) 30 | // now send back new state 31 | socket.emit("state", store.getState() ) 32 | if( ["ADD_ROOM","REMOVE_ROOM"].indexOf(action.type) > -1){ 33 | socket.broadcast.emit("state", store.getState() ) 34 | } 35 | }) 36 | 37 | 38 | socket.on('disconnect', () => { 39 | console.log('user disconnected'); 40 | }); 41 | }) 42 | } 43 | 44 | function switchRoom(socket,roomId){ 45 | socket.rooms.forEach( (room,index)=>{ 46 | console.log("should leave room, skip first one") 47 | if( index > 0 ){ 48 | socket.leave( room ) 49 | } 50 | }) 51 | 52 | setTimeout(()=>{ 53 | socket.join( roomId ) 54 | console.log( "roomId:",roomId, "socket.rooms:",socket.rooms ) 55 | },200) 56 | } -------------------------------------------------------------------------------- /redux-chat/src/server/reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | addRoom, 3 | removeRoom 4 | } from "./core.js" 5 | 6 | 7 | export default function reducer(state, action ){ 8 | switch (action.type){ 9 | case "ADD_ROOM": 10 | return addRoom(state, action.room ) 11 | case "REMOVE_ROOM": 12 | return removeRoom(state, action.payload ) 13 | } 14 | return state 15 | } -------------------------------------------------------------------------------- /redux-chat/src/server/server.js: -------------------------------------------------------------------------------- 1 | import express from "express" 2 | import {Server} from "http" 3 | 4 | var app = express() 5 | var http = Server( app ) 6 | 7 | // configs 8 | var rootPath = require('path').normalize(__dirname + '/../..'); 9 | app.set('views', __dirname +'/views') 10 | app.set('view engine', 'ejs') 11 | app.use(express.static( rootPath + "/public" )); 12 | 13 | var io = require('socket.io')(http); 14 | import {makeStore} from "./store" 15 | import listenWebSocket from "./io.js" 16 | 17 | const store = makeStore() 18 | listenWebSocket( io, store ) 19 | 20 | 21 | import { indexCtrl } from "./controller" 22 | app.use(indexCtrl(store ) ) 23 | 24 | http.listen(3000,()=>{ 25 | console.log("listening on port 3000") 26 | }) 27 | -------------------------------------------------------------------------------- /redux-chat/src/server/store.js: -------------------------------------------------------------------------------- 1 | import { createStore } from "redux" 2 | import coreReducer from "./reducer" 3 | 4 | import {fromJS} from "immutable" 5 | 6 | export const DEFAULT_STATE = fromJS({ 7 | rooms:[{ 8 | name:"公开房间", id:"0" 9 | }], 10 | }) 11 | 12 | export function makeStore( state=DEFAULT_STATE ){ 13 | return createStore( coreReducer, state ) 14 | } -------------------------------------------------------------------------------- /redux-chat/src/server/views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Redux socket.io Chat app 6 | 7 | 8 | 9 |
    10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /redux-chat/src/shared/utils/dateTime.js: -------------------------------------------------------------------------------- 1 | Date.prototype.yyyymmdd = function( isChinese, delimiter) { 2 | delimiter = delimiter || '-'; 3 | var yyyy = this.getFullYear().toString(); 4 | var mm = (this.getMonth()+1).toString(); // getMonth() is zero-based 5 | var dd = this.getDate().toString(); 6 | if(isChinese){ 7 | var date = yyyy + '年' + (mm[1]?mm:"0"+mm[0]) + '月' + (dd[1]?dd:"0"+dd[0])+'日'; 8 | }else{ 9 | var date = yyyy + delimiter + (mm[1]?mm:"0"+mm[0]) + delimiter + (dd[1]?dd:"0"+dd[0]); 10 | } 11 | return date 12 | }; 13 | // 只要年月,用于创建文件夹,将图片以 年月来分成不同的文件夹以便于管理 14 | Date.prototype.yyyymm = function(delimiter) { 15 | delimiter = delimiter || '-'; 16 | var yyyy = this.getFullYear().toString(); 17 | var mm = (this.getMonth()+1).toString(); 18 | return yyyy + delimiter + (mm[1]?mm:"0"+mm[0]); 19 | }; 20 | // chat time stamp: 14-9-22 12:20 21 | Date.prototype.yymmddhhmm = function(isChinese,delimiter) { 22 | delimiter = delimiter || '-'; 23 | var yy = this.getFullYear().toString(); 24 | var mm = (this.getMonth()+1).toString(); // getMonth() is zero-based 25 | var dd = this.getDate().toString(); 26 | var hh = this.getHours().toString(); 27 | var MM = this.getMinutes().toString(); 28 | if(isChinese){ 29 | return yy + '年' + (mm[1]?mm:"0"+mm[0]) + '月' + (dd[1]?dd:"0"+dd[0])+'日 '+ (hh[1]?hh:"0"+hh[0])+':'+(MM[1]?MM:"0"+MM[0]); 30 | } 31 | return yy + delimiter + (mm[1]?mm:"0"+mm[0]) + delimiter + (dd[1]?dd:"0"+dd[0])+' '+ (hh[1]?hh:"0"+hh[0])+':'+(MM[1]?MM:"0"+MM[0]); 32 | }; 33 | 34 | 35 | export function yyyymmdd(d,isChinese,delimiter){ 36 | return d? d.yyyymmdd(isChinese,delimiter) : new Date().yyyymmdd(isChinese,delimiter); 37 | }; 38 | 39 | export function yyyymm(d){ 40 | return d? d.yyyymm() : new Date().yyyymm(); 41 | }; 42 | 43 | export function yymmddhhmm(d,isChinese,delimiter){ 44 | return d? d.yymmddhhmm(isChinese,delimiter) : new Date().yymmddhhmm(isChinese,delimiter); 45 | }; -------------------------------------------------------------------------------- /redux-chat/template/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Redux socket.io Chat app 6 | 7 | 8 | 9 |
    10 |
    11 | 20 |
    21 |
    22 |

    当前聊天室:room2

    23 | 24 | 25 |
    26 |
      27 |
    • 28 |
      29 |

      Julia14:22

      30 |

      some message goes here

      31 |
      32 |
    • 33 |
    • 34 |
      35 |

      Julia14:24

      36 |

      tempor incididunt ut labore et tempor incididunt ut labore et

      37 |
      38 |
    • 39 |
    • 40 |
      41 |

      Eisneim14:32

      42 |

      Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam

      43 |
      44 |
    • 45 |
    • 46 |
      47 |

      Julia14:24

      48 |

      tempor incididunt ut labore et tempor incididunt ut labore et

      49 |
      50 |
    • 51 |
    52 |
    53 |
    54 |
    55 | 56 |
    57 |
    58 | 59 |
    60 |
    61 |
    62 |
    63 |
    64 |
    65 | 66 | 67 | -------------------------------------------------------------------------------- /redux-chat/test/client/InputBox_spec.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom" 3 | import {fromJS,Map,List} from "immutable" 4 | import { expect } from "chai" 5 | import InputBox from "../../src/client/components/InputBox" 6 | 7 | import { 8 | Simulate, 9 | renderIntoDocument, 10 | findRenderedDOMComponentWithTag, 11 | scryRenderedDOMComponentsWithClass, 12 | } from "react-addons-test-utils" 13 | 14 | 15 | describe("InputBox",()=>{ 16 | it("send message",()=>{ 17 | var message 18 | function sendMessage(msg){ 19 | message = msg 20 | } 21 | const instance = renderIntoDocument( 22 | 23 | ) 24 | const $textarea = findRenderedDOMComponentWithTag(instance,"textarea") 25 | expect( $textarea ).to.be.ok 26 | // set value of textare 27 | $textarea.value = "some message" 28 | const $form = findRenderedDOMComponentWithTag(instance,"form") 29 | Simulate.submit( $form ) 30 | 31 | expect(message ).to.equal( "some message" ) 32 | }) 33 | 34 | }) 35 | 36 | -------------------------------------------------------------------------------- /redux-chat/test/client/MessageList_spec.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import {fromJS,Map,List} from "immutable" 3 | import { expect } from "chai" 4 | import MessageList from "../../src/client/components/MessageList" 5 | 6 | import { 7 | Simulate, 8 | renderIntoDocument, 9 | scryRenderedDOMComponentsWithTag, 10 | scryRenderedDOMComponentsWithClass, 11 | } from "react-addons-test-utils" 12 | 13 | describe("MessageList",()=>{ 14 | it("render messages and my messages",()=>{ 15 | const messages = fromJS([ 16 | {user:"eisneim",content:"some message",time:"23:33"}, 17 | {user:"terry",content:"ss message",time:"12:33"}, 18 | ]) 19 | 20 | const component = renderIntoDocument( 21 | 22 | ) 23 | const $messages = scryRenderedDOMComponentsWithTag(component,"li") 24 | const $myMessages = scryRenderedDOMComponentsWithClass(component,"message-self") 25 | 26 | expect( $messages.length ).to.equal(2) 27 | expect( $myMessages.length ).to.equal(1) 28 | }) 29 | 30 | }) 31 | 32 | 33 | -------------------------------------------------------------------------------- /redux-chat/test/client/RoomList_spect.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom" 3 | import {fromJS,Map,List} from "immutable" 4 | import { expect } from "chai" 5 | 6 | import RoomList from "../../src/client/components/RoomList" 7 | 8 | import TestUtil,{ 9 | Simulate, 10 | renderIntoDocument, 11 | isCompositeComponentWithType, 12 | scryRenderedDOMComponentsWithTag, 13 | scryRenderedDOMComponentsWithClass, 14 | } from "react-addons-test-utils" 15 | 16 | describe("RoomList组件",()=>{ 17 | 18 | it("render roomlist ",()=>{ 19 | const rooms = fromJS([ 20 | {id:"0", name:"room",owner:"eisneim"}, 21 | {id:"1", name:"room2",owner:"terry"}, 22 | ]) 23 | 24 | const component = renderIntoDocument( 25 | 26 | ) 27 | const $rooms = scryRenderedDOMComponentsWithTag(component,"a") 28 | expect( $rooms.length ).to.equal(2) 29 | const $active = scryRenderedDOMComponentsWithClass(component,"active") 30 | expect( $active.length ).to.equal(1) 31 | }) 32 | 33 | it("能够切换房间",()=>{ 34 | 35 | const rooms = fromJS([ 36 | {id:"0", name:"room",owner:"eisneim"}, 37 | {id:"1", name:"room2",owner:"terry"}, 38 | ]) 39 | var currentRoom = "0" 40 | function switchRoom(id){ 41 | console.log("change id:",id) 42 | currentRoom=id 43 | } 44 | const RoomListElm = ( 45 | 49 | ) 50 | const component = renderIntoDocument( RoomListElm ) 51 | const $rooms = scryRenderedDOMComponentsWithTag(component,"a") 52 | Simulate.click( ReactDOM.findDOMNode($rooms[1]) ) 53 | expect( currentRoom ).to.equal("1") 54 | 55 | expect( isCompositeComponentWithType( component,RoomList ) ).to.be.true 56 | // console.log("isElement",TestUtil.isElement(RoomListElm)) 57 | // console.log("isElementOfType",TestUtil.isElementOfType(RoomListElm,RoomList)) 58 | // console.log("TestUtil.isDOMComponent",TestUtil.isDOMComponent( component )) 59 | // console.log("isCompositeComponent",TestUtil.isCompositeComponent(component)) 60 | 61 | }) 62 | 63 | 64 | }) -------------------------------------------------------------------------------- /redux-chat/test/client/reducer_spec.js: -------------------------------------------------------------------------------- 1 | import {fromJS} from "immutable" 2 | import {expect} from "chai" 3 | import rootReducer from "../../src/client/reducer" 4 | 5 | import { 6 | newMessage, setState, switchRoom, setUsername 7 | } from "../../src/client/actionCreators" 8 | 9 | const fakeState = fromJS({ 10 | rooms:[ 11 | {id:"0", name:"room",owner:"eisneim"}, 12 | {id:"1", name:"room2",owner:"terry"}, 13 | ], 14 | currentRoom: "1", 15 | username:"eisneim", 16 | messages: { 17 | "1":[ 18 | {user:"eisneim",content:"some message",time:"23:33"}, 19 | {user:"terry",content:"ss message",time:"12:33"}, 20 | ] 21 | } 22 | }) 23 | 24 | describe("client Root reducer",()=>{ 25 | it("set state",()=>{ 26 | const nextState = rootReducer(fakeState, 27 | setState(fromJS({username:"Joan",currentRoom:"0"})) 28 | ) 29 | expect(nextState.get("username")).to.equal("Joan") 30 | expect(nextState.get("rooms").size).to.equal(2) 31 | }) 32 | 33 | it("setusername",()=>{ 34 | const nextState = rootReducer(fakeState,setUsername("Terry")) 35 | expect( nextState.get("username") ).to.equal("Terry") 36 | }) 37 | 38 | it("switch chat room",()=>{ 39 | const nextState = rootReducer(fakeState, switchRoom("0")) 40 | expect( nextState.get("currentRoom") ).to.equal("0") 41 | }) 42 | 43 | it("send new message",()=>{ 44 | const action = newMessage({ 45 | roomId: "0", user:"eisneim",content:"some message" 46 | }) 47 | expect(action.message.time).to.be.ok 48 | const nextState = rootReducer(fakeState, action ) 49 | 50 | expect( nextState.getIn(["messages","0"]).size ).to.equal(1) 51 | const nextNextState = rootReducer(fakeState,{ 52 | type:"NEW_MESSAGE",message:{ 53 | roomId: "1", user:"terry",time:"12:00",content:"some message" 54 | } 55 | }) 56 | expect( nextNextState.getIn(["messages","1"]).size ).to.equal(3) 57 | }) 58 | 59 | }) 60 | 61 | -------------------------------------------------------------------------------- /redux-chat/test/clientTesthelper.js: -------------------------------------------------------------------------------- 1 | import jsdom from "jsdom" 2 | import chai from 'chai'; 3 | import chaiImmutable from 'chai-immutable'; 4 | 5 | const doc = jsdom.jsdom("") 6 | const win = doc.defaultView 7 | 8 | global.document = doc 9 | global.window = win 10 | 11 | Object.keys(window).forEach((key) => { 12 | if (!(key in global)) { 13 | global[key] = window[key]; 14 | } 15 | }); 16 | 17 | chai.use(chaiImmutable); -------------------------------------------------------------------------------- /redux-chat/test/server/core_spec.js: -------------------------------------------------------------------------------- 1 | import {expect} from "chai" 2 | import {v1} from "uuid" 3 | import {fromJS,Map,List} from "immutable" 4 | 5 | import { 6 | addRoom, 7 | removeRoom 8 | } from "../../src/server/core.js" 9 | 10 | describe("rooms", ()=>{ 11 | it("能够添加房间:addRoom",()=>{ 12 | var firstRoom = {name:"first room",id:v1(), owner: "eisneim" } 13 | const nextState = addRoom( undefined, firstRoom ) 14 | const rooms = nextState.get("rooms") 15 | expect( rooms ).to.be.ok 16 | expect( rooms.get(0) ).to.equal(Map(firstRoom)) 17 | 18 | const nextNextState = addRoom(nextState,{ 19 | name:"second room",owner:"terry" 20 | }) 21 | expect(nextNextState.getIn(["rooms",1,"name"])).to.equal("second room") 22 | }) 23 | 24 | const mockState = fromJS({ 25 | rooms: [{name:"first room",id:v1(), owner: "eisneim" }] 26 | }) 27 | 28 | it("能被创建者删除",()=>{ 29 | const state = removeRoom( mockState, { 30 | id: mockState.getIn(["rooms",0,"id"]), 31 | user: "eisneim" 32 | }) 33 | 34 | expect( state.get("rooms").size ).to.equal(0) 35 | }) 36 | 37 | it("不能被其他人删除",()=>{ 38 | const state = removeRoom( mockState, { 39 | id: mockState.getIn(["rooms",0,"id"]), 40 | user: "terry" 41 | }) 42 | 43 | expect( state.get("rooms").size ).to.equal(1) 44 | 45 | }) 46 | 47 | }) 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /redux-chat/test/server/reducer_spec.js: -------------------------------------------------------------------------------- 1 | import {expect} from "chai" 2 | import {fromJS,Map,List} from "immutable" 3 | import {v1} from "uuid" 4 | 5 | import coreReducer from "../../src/server/reducer" 6 | import {addRoom,removeRoom} from "../../src/server/actionCreator.js" 7 | 8 | describe("server端核心Reducer",()=>{ 9 | 10 | it("可以当做一个reducer",()=>{ 11 | var id = v1() 12 | var actions = [ 13 | {type:"ADD_ROOM",room:{id,name:"1",owner:"eisneim"}}, 14 | {type:"ADD_ROOM",room:{name:"2",owner:"terry"}}, 15 | {type:"ADD_ROOM",room:{name:"3",owner:"eisneim"}}, 16 | {type:"REMOVE_ROOM",payload:{id:id,user:"eisneim"}}, 17 | ] 18 | const finalState = actions.reduce( coreReducer, undefined ) 19 | console.log(finalState) 20 | expect(finalState.get("rooms").size).to.equal(2) 21 | expect(finalState.getIn(["rooms",0,"owner"])).to.equal("terry") 22 | }) 23 | 24 | it("使用actionCreator",()=>{ 25 | var id = v1() 26 | var actions = [ 27 | addRoom({id,name:"1",owner:"eisneim"}), 28 | addRoom({name:"2",owner:"terry"}), 29 | addRoom({name:"3",owner:"eisneim"}), 30 | removeRoom({id:id,user:"eisneim"}), 31 | ] 32 | const finalState = actions.reduce( coreReducer, undefined ) 33 | console.log(finalState) 34 | expect(finalState.get("rooms").size).to.equal(2) 35 | expect(finalState.getIn(["rooms",0,"owner"])).to.equal("terry") 36 | 37 | }) 38 | 39 | }) -------------------------------------------------------------------------------- /redux-chat/test/server/store_spec.js: -------------------------------------------------------------------------------- 1 | import {fromJS} from "immutable" 2 | import {expect} from "chai" 3 | 4 | import {addRoom} from "../../src/server/actionCreator.js" 5 | import {makeStore} from "../../src/server/store.js" 6 | 7 | describe("server store",()=>{ 8 | 9 | it("dispatch actions", ( done )=>{ 10 | const mockState = fromJS({ 11 | rooms:[] 12 | }) 13 | const store = makeStore( mockState ) 14 | 15 | store.subscribe(()=>{ 16 | const state = store.getState() 17 | expect( state.get("rooms").size ).to.equal(1) 18 | done() 19 | }) 20 | 21 | store.dispatch( addRoom({ 22 | name:"聊天室",owner:"terry" 23 | }) ) 24 | 25 | }) 26 | 27 | }) -------------------------------------------------------------------------------- /redux-chat/test/serverTestHelper.js: -------------------------------------------------------------------------------- 1 | import chai from "chai" 2 | import chaiImmutable from "chai-immutable" 3 | 4 | chai.use( chaiImmutable ) -------------------------------------------------------------------------------- /redux-chat/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require("path") 3 | 4 | module.exports = { 5 | entry:[ 6 | // for hot loader: WebpackDevServer host and port 7 | "webpack-dev-server/client?http://localhost:8080", 8 | // for hot loader: "only" prevents reload on syntax errors 9 | "webpack/hot/only-dev-server", 10 | // our appʼs entry point 11 | "./src/client/index.js" 12 | ], 13 | module:{ 14 | loaders:[{ 15 | test:/\.jsx?$/, 16 | include: path.join(__dirname,"src"), 17 | loaders: ["react-hot","babel"], 18 | }] 19 | }, 20 | resolve:{ 21 | extensions:["",".js",".jsx"] 22 | }, 23 | output:{ 24 | path: __dirname + "/public/build", 25 | filename:"boundle.js", 26 | publicPath:"http://localhost:8080/build", 27 | }, 28 | devServer: { 29 | contentBase: "./public", 30 | hot: true, 31 | host:"localhost", 32 | proxy:{ 33 | "*": "http://localhost:"+3000 34 | } 35 | }, 36 | plugins:[ 37 | new webpack.HotModuleReplacementPlugin() 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /redux-chat基本功能/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-chat", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "testServer": "mocha --compilers js:babel-core/register --require ./test/serverTestHelper.js ./test/server --recursive", 8 | "testServer:watch": "npm run testServer -- --watch", 9 | "testClient": "mocha --compilers js:babel-core/register --require ./test/ClientTestHelper.js ./test/client --recursive", 10 | "testClient:watch": "npm run testClient -- --watch" 11 | }, 12 | "babel": { 13 | "presets": [ 14 | "es2015", 15 | "react" 16 | ] 17 | }, 18 | "keywords": [], 19 | "author": "", 20 | "license": "ISC", 21 | "dependencies": { 22 | "ejs": "^2.3.4", 23 | "express": "^4.13.3", 24 | "immutable": "^3.7.5", 25 | "mocha": "^2.3.4", 26 | "react": "^0.14.3", 27 | "react-addons-pure-render-mixin": "^0.14.3", 28 | "react-dom": "^0.14.3", 29 | "react-hot-loader": "^1.3.0", 30 | "react-mixin": "^3.0.3", 31 | "react-redux": "^4.0.0", 32 | "redux": "^3.0.4", 33 | "socket.io": "^1.3.7", 34 | "socket.io-client": "^1.3.7", 35 | "uuid": "^2.0.1" 36 | }, 37 | "devDependencies": { 38 | "babel-cli": "^6.2.0", 39 | "babel-core": "^6.2.1", 40 | "babel-loader": "^6.2.0", 41 | "babel-preset-es2015": "^6.1.18", 42 | "babel-preset-react": "^6.1.18", 43 | "chai": "^3.4.1", 44 | "chai-immutable": "^1.5.3", 45 | "webpack": "^1.12.9", 46 | "webpack-dev-server": "^1.14.0", 47 | "jsdom": "^5.6.1", 48 | "react-addons-test-utils": "^0.14.3" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /redux-chat基本功能/public/css/style.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | *, *:before, *:after { 6 | -webkit-box-sizing: border-box; 7 | -moz-box-sizing: border-box; 8 | box-sizing: border-box; 9 | } 10 | 11 | article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block;}audio,canvas,video{display:inline-block;}audio:not([controls]){display:none;height:0;}[hidden]{display:none;}html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;}body{margin:0;}a:focus{outline:thin dotted;}a:active,a:hover{outline:0;}h1{font-size:2em;margin:0.67em 0;}abbr[title]{border-bottom:1px dotted;}b,strong{font-weight:bold;}dfn{font-style:italic;}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0;}mark{background:#ff0;color:#000;}code,kbd,pre,samp{font-family:monospace,serif;font-size:1em;}pre{white-space:pre-wrap;}q{quotes:"\201C" "\201D" "\2018" "\2019";}small{font-size:80%;}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline;}sup{top:-0.5em;}sub{bottom:-0.25em;}img{border:0;}svg:not(:root){overflow:hidden;}figure{margin:0;}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em;}legend{border:0;padding:0;}button,input,select,textarea{font-family:inherit;font-size:100%;margin:0;}button,input{line-height:normal;}button,select{text-transform:none;}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer;}button[disabled],html input[disabled]{cursor:default;}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0;}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box;}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none;}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0;}textarea{overflow:auto;vertical-align:top;}table{border-collapse:collapse;border-spacing:0;} 12 | h1,h2,h3,h4,h5{font-weight: normal;} 13 | 14 | .clearfix:before, 15 | .clearfix:after { 16 | content: " "; /* 1 */ 17 | display: table; /* 2 */ 18 | } 19 | .clearfix:after { 20 | clear: both; 21 | } 22 | 23 | .center-block { 24 | display: block; 25 | margin-left: auto; 26 | margin-right: auto; 27 | } 28 | .pull-right { 29 | float: right !important; 30 | } 31 | .pull-left { 32 | float: left !important; 33 | } 34 | .text-left{text-align: left;} 35 | .text-center{text-align: center;} 36 | .text-right{text-align: right;} 37 | 38 | .flex-row{ 39 | display: flex; 40 | flex-direction: row; 41 | /*// align-items: center;*/ 42 | } 43 | .flex-column{ 44 | flex-direction: column; 45 | display: flex; 46 | justify-content: center; 47 | } 48 | .flex-center{ 49 | display: flex; 50 | align-items: center; 51 | justify-content: center; 52 | } 53 | .flex{ 54 | flex:1; 55 | align-self:center; 56 | /*align-self: auto | flex-start | flex-end | center | baseline | stretch;*/ 57 | } 58 | 59 | 60 | body{ 61 | font-family: Helvetica Neue,"Hiragino Sans GB","Microsoft YaHei","微软雅黑","STHeiti","WenQuanYi Micro Hei",SimSun,sans-serif,Arial; 62 | background-color: #fafafa; 63 | height: 100%; 64 | } 65 | html,body,#app,#app>.flex-row,#chat-main{ 66 | height: 100%; 67 | } 68 | 69 | 70 | #chat-nav{ 71 | width: 250px; 72 | border-right: solid 1px #ddd; 73 | padding:20px; 74 | } 75 | .chat-room-list a{ 76 | display: block; 77 | padding-left: 15px; 78 | color: #27AE60; 79 | margin-bottom:10px; 80 | } 81 | 82 | .chat-room-list a.active{ 83 | background-color: #ddd; 84 | padding-top:8px;padding-bottom:8px; 85 | text-decoration: none; 86 | } 87 | 88 | #chat-main{ 89 | /*overflow-y: auto;*/ 90 | position: relative; 91 | } 92 | #chat-main header{ 93 | border-bottom: solid 1px #ddd; 94 | background-color: #fff; 95 | padding: 15px 10px; 96 | } 97 | #chat-main header h3{ 98 | margin: 0; 99 | } 100 | 101 | 102 | .chat-messages{ 103 | display: block; 104 | list-style: none; 105 | padding:10px; 106 | margin: 0; 107 | height: 80%; 108 | height: calc( 100% - 80px ); 109 | overflow-y:auto; 110 | padding-bottom: 100px; 111 | } 112 | .chat-messages >li{ 113 | } 114 | .message-inner{ 115 | background-color: #2ECC71; 116 | display: inline-block; 117 | width:auto; 118 | padding:8px 12px; 119 | margin:5px 0; 120 | border-radius: 6px; 121 | color: #fff; 122 | outline: none; 123 | min-width: 300px; 124 | } 125 | .message-inner p{ 126 | line-height: 1; 127 | margin:6px 0; 128 | } 129 | 130 | .message-self .message-inner{ 131 | background-color: #eee; 132 | color:#333; 133 | float: right; 134 | } 135 | .chat-username{ 136 | font-size: 1.2em; 137 | } 138 | .chat-username small{ 139 | margin-left: 10px; color:#ccc; 140 | } 141 | 142 | #chat-inputbox{ 143 | width:100%; 144 | position: absolute; 145 | padding: 10px; 146 | bottom:0; 147 | left:0; 148 | border-top: solid 1px #ddd; 149 | background-color: #fff; 150 | } 151 | #chat-inputbox textarea{ 152 | width:100%; 153 | min-height: 60px 154 | margin:0; 155 | } 156 | 157 | /*------------------------------*/ 158 | 159 | .btn { 160 | display: inline-block; 161 | padding: 8px 16px; 162 | border: 2px solid #ddd; 163 | color: #ddd; 164 | text-decoration: none; 165 | margin: 0px 10px 0 0; 166 | transition: all .6s; 167 | min-width: 100px; 168 | text-align: center; 169 | background-color: transparent; 170 | } 171 | .btn:hover { 172 | background: #ddd; 173 | color: #fff; 174 | } 175 | .btn.sm{ 176 | padding: 3px 8px; 177 | font-size:14px; 178 | } 179 | .btn.md{ 180 | padding: 12px 20px 181 | } 182 | /* Big Size */ 183 | .btn.lg{ 184 | padding: 20px 30px; 185 | } 186 | /* Border Radius */ 187 | .btn.style-1{ 188 | border-radius: 10px; 189 | } 190 | .btn.style-2{ 191 | border-radius: 40px; 192 | } 193 | 194 | /* Color 1 #16A085 */ 195 | .btn.color-1 {border-color: #16A085;color: #16A085;} 196 | .btn.color-1:hover, .color-1.active {background: #16A085;color: #fff;} 197 | 198 | /* Color 2 #27AE60 */ 199 | .btn.color-2 {border-color: #27AE60;color: #27AE60;} 200 | .btn.color-2:hover,.color-2.active {background: #27AE60;color: #fff;} 201 | 202 | /* Color 3 #2980B9 */ 203 | .btn.color-3 {border-color: #2980B9;color: #2980B9;} 204 | .btn.color-3:hover,.color-3.active {background: #2980B9;color: #fff;} 205 | 206 | /* Color 4 #8E44AD */ 207 | .btn.color-4 {border-color: #8E44AD;color: #8E44AD;} 208 | .btn.color-4:hover,.color-4.active {background: #8E44AD;color: #fff;} 209 | 210 | /* Color 5 #2C3E50 */ 211 | .btn.color-5 {border-color: #2C3E50;color: #2C3E50;} 212 | .btn.color-5:hover,.color-5.active {background: #2C3E50;color: #fff;} 213 | -------------------------------------------------------------------------------- /redux-chat基本功能/src/client/actionCreators.js: -------------------------------------------------------------------------------- 1 | import {fromJS,Map} from "immutable" 2 | import {yymmddhhmm} from "../shared/utils/dateTime" 3 | 4 | export function setState(state){ 5 | return { 6 | type:"SET_STATE", 7 | state: Map.isMap(state) ? state : fromJS(state) 8 | } 9 | } 10 | 11 | export function setUsername(username){ 12 | return { 13 | type: "SET_USERNAME", username 14 | } 15 | } 16 | 17 | export function switchRoom(roomId){ 18 | return { 19 | type:"SWITCH_ROOM", roomId, 20 | meta:{ remote:true }, 21 | } 22 | } 23 | 24 | export function newMessage({roomId,content,user,time}, isFromServer ){ 25 | return { 26 | type:"NEW_MESSAGE", 27 | meta:{ remote: !isFromServer }, 28 | message: { 29 | roomId, content:content||"", user, 30 | time: yymmddhhmm() 31 | } 32 | } 33 | } 34 | 35 | export function addRoom( room ){ 36 | if( !room || !room.owner) throw new Error("addRoom() room.owner is required") 37 | 38 | return { 39 | type:"ADD_ROOM", room, 40 | meta:{ remote:true }, 41 | } 42 | } 43 | 44 | export function removeRoom( id, user ){ 45 | return { 46 | type:"REMOVE_ROOM", 47 | payload:{ id, user }, 48 | meta:{ remote:true }, 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /redux-chat基本功能/src/client/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React,{ Component } from "react" 2 | 3 | import MessageList from "./MessageList" 4 | import InputBox from "./InputBox" 5 | import RoomList from "./RoomList" 6 | import { newMessage, switchRoom, addRoom,removeRoom } from "../actionCreators" 7 | 8 | class App extends Component { 9 | getCurrentRoomName(){ 10 | if(!this.props.currentRoom ) return "无" 11 | const room = this.props.rooms.find(r=>r.get("id") === this.props.currentRoom ) 12 | return ( room && room.get ) ? room.get("name") : room 13 | } 14 | 15 | isOwner( ){ 16 | if(!this.props.currentRoom || !this.props.username ) return false 17 | const room = this.props.rooms.find(r=>r.get("id") === this.props.currentRoom ) 18 | if(!room) return false; 19 | return room.get("owner") == this.props.username 20 | } 21 | 22 | getMessages(){ 23 | return this.props.messages ? 24 | this.props.messages.get(this.props.currentRoom) : [] 25 | } 26 | 27 | addRoom(){ 28 | var name = prompt("房间名称") 29 | if(!name) return alert("不能没有房间名称") 30 | 31 | this.props.dispatch( addRoom({ 32 | name, owner: this.props.username 33 | }) ) 34 | } 35 | 36 | removeRoom(){ 37 | this.props.dispatch(switchRoom( )) 38 | 39 | this.props.dispatch( removeRoom( 40 | this.props.currentRoom, this.props.username 41 | ) ) 42 | } 43 | 44 | sendMessage(message){ 45 | this.props.dispatch( newMessage({ 46 | roomId: this.props.currentRoom, 47 | user: this.props.username, 48 | content: message 49 | }) ) 50 | } 51 | 52 | render(){ 53 | const { currentRoom, rooms, username, dispatch } = this.props 54 | 55 | return ( 56 |
    57 | 66 | { !currentRoom ?

    请选择一个聊天室

    : 67 |
    68 |
    69 |

    当前聊天室:{ this.getCurrentRoomName() }

    70 | 71 | { !this.isOwner() ? "": 72 | 74 | } 75 |
    76 | 77 | 78 |
    79 | } 80 |
    81 | ) 82 | } 83 | } 84 | 85 | 86 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 87 | import reactMixin from "react-mixin"; 88 | reactMixin.onClass(App, PureRenderMixin ) 89 | 90 | import { connect } from 'react-redux' 91 | function mapStateToProps ( state ){ 92 | return { 93 | rooms: state.get("rooms"), 94 | currentRoom: state.get("currentRoom"), 95 | username: state.get("username"), 96 | messages: state.get("messages") 97 | } 98 | } 99 | 100 | export const ConnectedApp = connect( mapStateToProps )(App) 101 | 102 | export default App 103 | -------------------------------------------------------------------------------- /redux-chat基本功能/src/client/components/InputBox.jsx: -------------------------------------------------------------------------------- 1 | import React,{ Component } from "react" 2 | import ReactDOM from "react-dom" 3 | 4 | class InputBox extends Component { 5 | 6 | handleSubmit(e){ 7 | e.preventDefault() 8 | var $textarea = ReactDOM.findDOMNode( this.refs.textarea ) 9 | if( typeof this.props.sendMessage === "function" ){ 10 | this.props.sendMessage( $textarea.value ) 11 | $textarea.value = "" 12 | }else{ 13 | console.log("props.sendMessage not defined!!") 14 | } 15 | 16 | } 17 | 18 | render(){ 19 | return ( 20 |
    21 |
    22 |
    23 | 24 |
    25 |
    26 | 27 |
    28 |
    29 |
    30 | ) 31 | } 32 | } 33 | 34 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 35 | import reactMixin from "react-mixin"; 36 | reactMixin.onClass(InputBox, PureRenderMixin ) 37 | 38 | 39 | export default InputBox -------------------------------------------------------------------------------- /redux-chat基本功能/src/client/components/Message.jsx: -------------------------------------------------------------------------------- 1 | import React,{ Component } from "react" 2 | 3 | class Message extends Component { 4 | render(){ 5 | const { message, isSelf } = this.props 6 | const className = isSelf ? "message-self":"" 7 | 8 | return ( 9 |
  • 10 |
    11 |

    {message.get("user")} 12 | {message.get("time")} 13 |

    14 |

    {message.get("content")}

    15 |
    16 |
  • 17 | ) 18 | } 19 | } 20 | 21 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 22 | import reactMixin from "react-mixin"; 23 | reactMixin.onClass(Message, PureRenderMixin ) 24 | 25 | export default Message 26 | -------------------------------------------------------------------------------- /redux-chat基本功能/src/client/components/MessageList.jsx: -------------------------------------------------------------------------------- 1 | import React,{ Component, PropTypes } from "react" 2 | import Message from "./Message" 3 | 4 | class MessageList extends Component { 5 | isSelf( message ){ 6 | return this.props.username === message.get("user") 7 | } 8 | 9 | $getMessages( messages ){ 10 | if(!messages || messages.size == 0) 11 | return

    还没有信息

    12 | 13 | return messages.map((message,index)=>{ 14 | return 17 | }) 18 | } 19 | 20 | render(){ 21 | return ( 22 |
      23 | { 24 | this.$getMessages( this.props.messages ) 25 | } 26 |
    27 | ) 28 | } 29 | 30 | } 31 | 32 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 33 | import reactMixin from "react-mixin"; 34 | reactMixin.onClass(MessageList, PureRenderMixin ) 35 | 36 | export default MessageList 37 | -------------------------------------------------------------------------------- /redux-chat基本功能/src/client/components/RoomList.jsx: -------------------------------------------------------------------------------- 1 | import React,{ Component} from "react" 2 | 3 | class RoomList extends Component { 4 | 5 | isActive( room, currentRoom ){ 6 | return room.get("id") === currentRoom 7 | } 8 | 9 | render(){ 10 | const {rooms,currentRoom} = this.props 11 | 12 | return ( 13 |
    14 | { 15 | rooms.map((room,index)=>{ 16 | return ( 17 | this.props.switchRoom(room.get("id")) } 19 | key={index} href="#"> 20 | {room.get("name")} 21 | 22 | ) 23 | }) 24 | } 25 |
    26 | ) 27 | } 28 | } 29 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 30 | import reactMixin from "react-mixin"; 31 | reactMixin.onClass( RoomList, PureRenderMixin ) 32 | 33 | export default RoomList -------------------------------------------------------------------------------- /redux-chat基本功能/src/client/index.js: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom" 2 | import React from "react" 3 | import {ConnectedApp} from "./components/App" 4 | 5 | import {Provider} from "react-redux" 6 | 7 | import {createStore, applyMiddleware } from 'redux' 8 | import { logger, socketMiddleware } from "./middleware" 9 | 10 | import rootReducer from "./reducer" 11 | import { setState, newMessage } from "./actionCreators" 12 | import { getInitialState, saveToStorage } from "./store.js" 13 | 14 | import {socket} from "./io" 15 | 16 | const createStoreWithMiddleware = applyMiddleware( 17 | logger, 18 | socketMiddleware(socket) 19 | )( createStore ) 20 | const store = createStoreWithMiddleware( rootReducer, getInitialState() ) 21 | 22 | socket.on("state",state => { 23 | store.dispatch(setState(state)) 24 | }) 25 | 26 | socket.on("message",message => { 27 | console.log("get message from server") 28 | store.dispatch( newMessage(message, true ) ) 29 | }) 30 | 31 | 32 | // --------------------------- 33 | 34 | var $app = document.getElementById("app") 35 | 36 | function render(){ 37 | // const store = store.getState() 38 | 39 | ReactDOM.render( 40 | 41 | 42 | , 43 | $app 44 | ) 45 | } 46 | 47 | render() 48 | 49 | store.subscribe(()=>{ 50 | saveToStorage( store.getState() ) 51 | }) 52 | 53 | -------------------------------------------------------------------------------- /redux-chat基本功能/src/client/io.js: -------------------------------------------------------------------------------- 1 | import IO from "socket.io-client" 2 | 3 | export const socket = IO("http://localhost:3000") 4 | 5 | socket.on('disconnect', ()=>{ 6 | console.log('user disconnected'); 7 | }); -------------------------------------------------------------------------------- /redux-chat基本功能/src/client/middleware.js: -------------------------------------------------------------------------------- 1 | 2 | export const socketMiddleware = socket => store => next => action => { 3 | if (action.meta && action.meta.remote) { 4 | socket.emit('action', action); 5 | } 6 | 7 | return next(action) 8 | } 9 | 10 | /** 11 | * 记录所有被发起的 action 以及产生的新的 state。 12 | */ 13 | export const logger = store => next => action => { 14 | console.group(action.type) 15 | console.info('dispatching', action) 16 | let result = next(action) 17 | const nextState = store.getState() 18 | console.log('next state', nextState.toJS? nextState.toJS() : nextState ) 19 | console.groupEnd(action.type) 20 | return result 21 | } 22 | 23 | /** 24 | * 让你可以发起一个函数来替代 action。 25 | * 这个函数接收 `dispatch` 和 `getState` 作为参数。 26 | * 27 | * 对于(根据 `getState()` 的情况)提前退出,或者异步控制流( `dispatch()` 一些其他东西)来说,这非常有用。 28 | * 29 | * `dispatch` 会返回被发起函数的返回值。 30 | */ 31 | export const thunk = store => next => action => 32 | typeof action === 'function' ? 33 | action(store.dispatch, store.getState) : 34 | next(action) 35 | 36 | /* 37 | onclick={dispatch( (dispatch, getState)=>{ 38 | const state = getState() 39 | if(something wrong) return 40 | 41 | doSomeApiRequest( state.apiurl ) 42 | .then(data=>{ 43 | dispatch( requestSomeApiSuccess(data) ) 44 | }, error => dispatch(requestSomeApiFail(e)) ) 45 | })} 46 | */ 47 | 48 | /** 49 | * 用 { meta: { delay: N } } 来让 action 延迟 N 毫秒。 50 | * 在这个案例中,让 `dispatch` 返回一个取消 timeout 的函数。 51 | */ 52 | export const timeoutScheduler = store => next => action => { 53 | if (!action.meta || !action.meta.delay) { 54 | return next(action) 55 | } 56 | 57 | let timeoutId = setTimeout( 58 | () => next(action), 59 | action.meta.delay 60 | ) 61 | 62 | return function cancel() { 63 | clearTimeout(timeoutId) 64 | } 65 | } 66 | 67 | 68 | /** 69 | * 使你除了 action 之外还可以发起 promise。 70 | * 如果这个 promise 被 resolved,他的结果将被作为 action 发起。 71 | * 这个 promise 会被 `dispatch` 返回,因此调用者可以处理 rejection。 72 | */ 73 | const vanillaPromise = store => next => action => { 74 | if (typeof action.then !== 'function') { 75 | return next(action) 76 | } 77 | 78 | return Promise.resolve(action).then(store.dispatch) 79 | } 80 | 81 | /** 82 | * 让你可以发起带有一个 { promise } 属性的特殊 action。 83 | * 84 | * 这个 middleware 会在开始时发起一个 action,并在这个 `promise` resolve 时发起另一个成功(或失败)的 action。 85 | * 86 | * 为了方便起见,`dispatch` 会返回这个 promise 让调用者可以等待。 87 | */ 88 | const readyStatePromise = store => next => action => { 89 | if (!action.promise) { 90 | return next(action) 91 | } 92 | 93 | function makeAction(ready, data) { 94 | let newAction = Object.assign({}, action, { ready }, data) 95 | delete newAction.promise 96 | return newAction 97 | } 98 | 99 | next(makeAction(false)) 100 | return action.promise.then( 101 | result => next(makeAction(true, { result })), 102 | error => next(makeAction(true, { error })) 103 | ) 104 | } 105 | -------------------------------------------------------------------------------- /redux-chat基本功能/src/client/reducer.js: -------------------------------------------------------------------------------- 1 | import {fromJS,Map,List} from "immutable" 2 | 3 | export default function rootReducer(state=Map(),action){ 4 | switch ( action.type ){ 5 | case "SET_STATE": return state.merge(Map( action.state )) 6 | 7 | case "SET_USERNAME": 8 | return state.set("username", action.username ) 9 | 10 | case "SWITCH_ROOM": 11 | return state.set("currentRoom", action.roomId ) 12 | 13 | case "NEW_MESSAGE": 14 | if(!action.message||!action.message.roomId) 15 | return state 16 | 17 | if( state.get("messages").has( action.message.roomId ) ){ 18 | return state.updateIn( 19 | ["messages",action.message.roomId], 20 | array => array.push( Map(action.message) ) 21 | ) 22 | } else{ 23 | return state.setIn( 24 | ["messages",action.message.roomId], 25 | List.of( Map(action.message)) 26 | ) 27 | } 28 | 29 | default: return state 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /redux-chat基本功能/src/client/store.js: -------------------------------------------------------------------------------- 1 | import {Map,fromJS} from "immutable" 2 | 3 | const STATE_KEY = "CHAT_APP_STATE" 4 | 5 | export function saveToStorage( state ){ 6 | var data = JSON.stringify(state.toJS ? state.toJS() : state) 7 | localStorage.setItem(STATE_KEY, data ) 8 | } 9 | 10 | export function getInitialState( ){ 11 | var stateString = localStorage.getItem( STATE_KEY ) 12 | if( !stateString ) { 13 | return fromJS({ 14 | rooms:[],messages:{}, 15 | username: prompt("用户名") 16 | }) 17 | } 18 | 19 | return fromJS(JSON.parse( stateString )) 20 | } 21 | -------------------------------------------------------------------------------- /redux-chat基本功能/src/server/actionCreator.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export function addRoom( room ){ 4 | return { 5 | type:"ADD_ROOM",room: room 6 | } 7 | } 8 | 9 | export function removeRoom( payload ){ 10 | return { 11 | type:"REMOVE_ROOM",payload: payload 12 | } 13 | } -------------------------------------------------------------------------------- /redux-chat基本功能/src/server/core.js: -------------------------------------------------------------------------------- 1 | import {Map,List,fromJS} from "immutable" 2 | import {v1} from "uuid" 3 | 4 | export const INITIAL_STATE = fromJS({ 5 | rooms:[], 6 | }) 7 | 8 | export function addRoom( state=INITIAL_STATE, room ){ 9 | if( !room || !room.owner) return state 10 | 11 | return state.update("rooms", rooms => rooms.push(Map( { 12 | id: room.id || v1(), 13 | name: room.name || "no name", 14 | owner: room.owner, 15 | } )) ) 16 | } 17 | 18 | export function removeRoom( state, {id,user}){ 19 | const rooms = state.get("rooms") 20 | var index = rooms.findIndex( r => r.get("id") === id ) 21 | if(index == -1 || rooms.getIn([index,"owner"])!== user ) { 22 | // console.log("非房间创建者,不能删除该房间",index) 23 | return state 24 | } 25 | return state.update("rooms",rooms => rooms.splice(index,1) ) 26 | } 27 | 28 | 29 | -------------------------------------------------------------------------------- /redux-chat基本功能/src/server/io.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_ROOM = "0" 2 | 3 | export default function listenWebSocket( io, store ){ 4 | io.on("connection", socket=>{ 5 | console.log("one client connected") 6 | 7 | socket.emit("state", store.getState() ) 8 | // join this to the default room 9 | socket.join( DEFAULT_ROOM ) 10 | // add/remove room logic goes here 11 | socket.on("action",action => { 12 | console.log("client action:", action ) 13 | switch( action.type ){ 14 | case "SWITCH_ROOM": 15 | return switchRoom( socket, action.roomId || DEFAULT_ROOM ) 16 | 17 | // send this message back 18 | case "NEW_MESSAGE": 19 | if( socket.rooms && socket.rooms.length>0 ){ 20 | socket.rooms.forEach(id=>{ 21 | socket.to( id ).emit("message", action.message) 22 | }) 23 | }else{ 24 | socket.emit( "message", action.message ) 25 | } 26 | return 27 | } 28 | 29 | store.dispatch(action) 30 | // now send back new state 31 | socket.emit("state", store.getState() ) 32 | if( ["ADD_ROOM","REMOVE_ROOM"].indexOf(action.type) > -1){ 33 | socket.broadcast.emit("state", store.getState() ) 34 | } 35 | }) 36 | 37 | 38 | socket.on('disconnect', () => { 39 | console.log('user disconnected'); 40 | }); 41 | }) 42 | } 43 | 44 | function switchRoom(socket,roomId){ 45 | socket.rooms.forEach( (room,index)=>{ 46 | console.log("should leave room, skip first one") 47 | if( index > 0 ){ 48 | socket.leave( room ) 49 | } 50 | }) 51 | 52 | setTimeout(()=>{ 53 | socket.join( roomId ) 54 | console.log( "roomId:",roomId, "socket.rooms:",socket.rooms ) 55 | },200) 56 | } -------------------------------------------------------------------------------- /redux-chat基本功能/src/server/reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | addRoom, 3 | removeRoom 4 | } from "./core.js" 5 | 6 | 7 | export default function reducer(state, action ){ 8 | switch (action.type){ 9 | case "ADD_ROOM": 10 | return addRoom(state, action.room ) 11 | case "REMOVE_ROOM": 12 | return removeRoom(state, action.payload ) 13 | } 14 | return state 15 | } -------------------------------------------------------------------------------- /redux-chat基本功能/src/server/server.js: -------------------------------------------------------------------------------- 1 | import express from "express" 2 | import {Server} from "http" 3 | 4 | var app = express() 5 | var http = Server( app ) 6 | 7 | // configs 8 | var rootPath = require('path').normalize(__dirname + '/../..'); 9 | app.set('views', __dirname +'/views') 10 | app.set('view engine', 'ejs') 11 | app.use(express.static( rootPath + "/public" )); 12 | 13 | var io = require('socket.io')(http); 14 | import {makeStore} from "./store" 15 | import listenWebSocket from "./io.js" 16 | 17 | const store = makeStore() 18 | listenWebSocket( io, store ) 19 | 20 | 21 | 22 | app.get("/",(req,res)=>{ 23 | res.render("index") 24 | }) 25 | 26 | http.listen(3000,()=>{ 27 | console.log("listening on port 3000") 28 | }) 29 | -------------------------------------------------------------------------------- /redux-chat基本功能/src/server/store.js: -------------------------------------------------------------------------------- 1 | import { createStore } from "redux" 2 | import coreReducer from "./reducer" 3 | 4 | import {fromJS} from "immutable" 5 | 6 | export const DEFAULT_STATE = fromJS({ 7 | rooms:[{ 8 | name:"公开房间", id:"0" 9 | }], 10 | }) 11 | 12 | export function makeStore( state=DEFAULT_STATE ){ 13 | return createStore( coreReducer, state ) 14 | } -------------------------------------------------------------------------------- /redux-chat基本功能/src/server/views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Redux socket.io Chat app 6 | 7 | 8 | 9 |
    10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /redux-chat基本功能/src/shared/utils/dateTime.js: -------------------------------------------------------------------------------- 1 | Date.prototype.yyyymmdd = function( isChinese, delimiter) { 2 | delimiter = delimiter || '-'; 3 | var yyyy = this.getFullYear().toString(); 4 | var mm = (this.getMonth()+1).toString(); // getMonth() is zero-based 5 | var dd = this.getDate().toString(); 6 | if(isChinese){ 7 | var date = yyyy + '年' + (mm[1]?mm:"0"+mm[0]) + '月' + (dd[1]?dd:"0"+dd[0])+'日'; 8 | }else{ 9 | var date = yyyy + delimiter + (mm[1]?mm:"0"+mm[0]) + delimiter + (dd[1]?dd:"0"+dd[0]); 10 | } 11 | return date 12 | }; 13 | // 只要年月,用于创建文件夹,将图片以 年月来分成不同的文件夹以便于管理 14 | Date.prototype.yyyymm = function(delimiter) { 15 | delimiter = delimiter || '-'; 16 | var yyyy = this.getFullYear().toString(); 17 | var mm = (this.getMonth()+1).toString(); 18 | return yyyy + delimiter + (mm[1]?mm:"0"+mm[0]); 19 | }; 20 | // chat time stamp: 14-9-22 12:20 21 | Date.prototype.yymmddhhmm = function(isChinese,delimiter) { 22 | delimiter = delimiter || '-'; 23 | var yy = this.getFullYear().toString(); 24 | var mm = (this.getMonth()+1).toString(); // getMonth() is zero-based 25 | var dd = this.getDate().toString(); 26 | var hh = this.getHours().toString(); 27 | var MM = this.getMinutes().toString(); 28 | if(isChinese){ 29 | return yy + '年' + (mm[1]?mm:"0"+mm[0]) + '月' + (dd[1]?dd:"0"+dd[0])+'日 '+ (hh[1]?hh:"0"+hh[0])+':'+(MM[1]?MM:"0"+MM[0]); 30 | } 31 | return yy + delimiter + (mm[1]?mm:"0"+mm[0]) + delimiter + (dd[1]?dd:"0"+dd[0])+' '+ (hh[1]?hh:"0"+hh[0])+':'+(MM[1]?MM:"0"+MM[0]); 32 | }; 33 | 34 | 35 | export function yyyymmdd(d,isChinese,delimiter){ 36 | return d? d.yyyymmdd(isChinese,delimiter) : new Date().yyyymmdd(isChinese,delimiter); 37 | }; 38 | 39 | export function yyyymm(d){ 40 | return d? d.yyyymm() : new Date().yyyymm(); 41 | }; 42 | 43 | export function yymmddhhmm(d,isChinese,delimiter){ 44 | return d? d.yymmddhhmm(isChinese,delimiter) : new Date().yymmddhhmm(isChinese,delimiter); 45 | }; -------------------------------------------------------------------------------- /redux-chat基本功能/template/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Redux socket.io Chat app 6 | 7 | 8 | 9 |
    10 |
    11 | 20 |
    21 |
    22 |

    当前聊天室:room2

    23 | 24 | 25 |
    26 |
      27 |
    • 28 |
      29 |

      Julia14:22

      30 |

      some message goes here

      31 |
      32 |
    • 33 |
    • 34 |
      35 |

      Julia14:24

      36 |

      tempor incididunt ut labore et tempor incididunt ut labore et

      37 |
      38 |
    • 39 |
    • 40 |
      41 |

      Eisneim14:32

      42 |

      Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam

      43 |
      44 |
    • 45 |
    • 46 |
      47 |

      Julia14:24

      48 |

      tempor incididunt ut labore et tempor incididunt ut labore et

      49 |
      50 |
    • 51 |
    52 |
    53 |
    54 |
    55 | 56 |
    57 |
    58 | 59 |
    60 |
    61 |
    62 |
    63 |
    64 |
    65 | 66 | 67 | -------------------------------------------------------------------------------- /redux-chat基本功能/test/client/InputBox_spec.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom" 3 | import {fromJS,Map,List} from "immutable" 4 | import { expect } from "chai" 5 | import InputBox from "../../src/client/components/InputBox" 6 | 7 | import { 8 | Simulate, 9 | renderIntoDocument, 10 | findRenderedDOMComponentWithTag, 11 | scryRenderedDOMComponentsWithClass, 12 | } from "react-addons-test-utils" 13 | 14 | 15 | describe("InputBox",()=>{ 16 | it("send message",()=>{ 17 | var message 18 | function sendMessage(msg){ 19 | message = msg 20 | } 21 | const instance = renderIntoDocument( 22 | 23 | ) 24 | const $textarea = findRenderedDOMComponentWithTag(instance,"textarea") 25 | expect( $textarea ).to.be.ok 26 | // set value of textare 27 | $textarea.value = "some message" 28 | const $form = findRenderedDOMComponentWithTag(instance,"form") 29 | Simulate.submit( $form ) 30 | 31 | expect(message ).to.equal( "some message" ) 32 | }) 33 | 34 | }) 35 | 36 | -------------------------------------------------------------------------------- /redux-chat基本功能/test/client/MessageList_spec.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import {fromJS,Map,List} from "immutable" 3 | import { expect } from "chai" 4 | import MessageList from "../../src/client/components/MessageList" 5 | 6 | import { 7 | Simulate, 8 | renderIntoDocument, 9 | scryRenderedDOMComponentsWithTag, 10 | scryRenderedDOMComponentsWithClass, 11 | } from "react-addons-test-utils" 12 | 13 | describe("MessageList",()=>{ 14 | it("render messages and my messages",()=>{ 15 | const messages = fromJS([ 16 | {user:"eisneim",content:"some message",time:"23:33"}, 17 | {user:"terry",content:"ss message",time:"12:33"}, 18 | ]) 19 | 20 | const component = renderIntoDocument( 21 | 22 | ) 23 | const $messages = scryRenderedDOMComponentsWithTag(component,"li") 24 | const $myMessages = scryRenderedDOMComponentsWithClass(component,"message-self") 25 | 26 | expect( $messages.length ).to.equal(2) 27 | expect( $myMessages.length ).to.equal(1) 28 | }) 29 | 30 | }) 31 | 32 | 33 | -------------------------------------------------------------------------------- /redux-chat基本功能/test/client/RoomList_spect.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom" 3 | import {fromJS,Map,List} from "immutable" 4 | import { expect } from "chai" 5 | 6 | import RoomList from "../../src/client/components/RoomList" 7 | 8 | import TestUtil,{ 9 | Simulate, 10 | renderIntoDocument, 11 | isCompositeComponentWithType, 12 | scryRenderedDOMComponentsWithTag, 13 | scryRenderedDOMComponentsWithClass, 14 | } from "react-addons-test-utils" 15 | 16 | describe("RoomList组件",()=>{ 17 | 18 | it("render roomlist ",()=>{ 19 | const rooms = fromJS([ 20 | {id:"0", name:"room",owner:"eisneim"}, 21 | {id:"1", name:"room2",owner:"terry"}, 22 | ]) 23 | 24 | const component = renderIntoDocument( 25 | 26 | ) 27 | const $rooms = scryRenderedDOMComponentsWithTag(component,"a") 28 | expect( $rooms.length ).to.equal(2) 29 | const $active = scryRenderedDOMComponentsWithClass(component,"active") 30 | expect( $active.length ).to.equal(1) 31 | }) 32 | 33 | it("能够切换房间",()=>{ 34 | 35 | const rooms = fromJS([ 36 | {id:"0", name:"room",owner:"eisneim"}, 37 | {id:"1", name:"room2",owner:"terry"}, 38 | ]) 39 | var currentRoom = "0" 40 | function switchRoom(id){ 41 | console.log("change id:",id) 42 | currentRoom=id 43 | } 44 | const RoomListElm = ( 45 | 49 | ) 50 | const component = renderIntoDocument( RoomListElm ) 51 | const $rooms = scryRenderedDOMComponentsWithTag(component,"a") 52 | Simulate.click( ReactDOM.findDOMNode($rooms[1]) ) 53 | expect( currentRoom ).to.equal("1") 54 | 55 | expect( isCompositeComponentWithType( component,RoomList ) ).to.be.true 56 | // console.log("isElement",TestUtil.isElement(RoomListElm)) 57 | // console.log("isElementOfType",TestUtil.isElementOfType(RoomListElm,RoomList)) 58 | // console.log("TestUtil.isDOMComponent",TestUtil.isDOMComponent( component )) 59 | // console.log("isCompositeComponent",TestUtil.isCompositeComponent(component)) 60 | 61 | }) 62 | 63 | 64 | }) -------------------------------------------------------------------------------- /redux-chat基本功能/test/client/reducer_spec.js: -------------------------------------------------------------------------------- 1 | import {fromJS} from "immutable" 2 | import {expect} from "chai" 3 | import rootReducer from "../../src/client/reducer" 4 | 5 | import { 6 | newMessage, setState, switchRoom, setUsername 7 | } from "../../src/client/actionCreators" 8 | 9 | const fakeState = fromJS({ 10 | rooms:[ 11 | {id:"0", name:"room",owner:"eisneim"}, 12 | {id:"1", name:"room2",owner:"terry"}, 13 | ], 14 | currentRoom: "1", 15 | username:"eisneim", 16 | messages: { 17 | "1":[ 18 | {user:"eisneim",content:"some message",time:"23:33"}, 19 | {user:"terry",content:"ss message",time:"12:33"}, 20 | ] 21 | } 22 | }) 23 | 24 | describe("client Root reducer",()=>{ 25 | it("set state",()=>{ 26 | const nextState = rootReducer(fakeState, 27 | setState(fromJS({username:"Joan",currentRoom:"0"})) 28 | ) 29 | expect(nextState.get("username")).to.equal("Joan") 30 | expect(nextState.get("rooms").size).to.equal(2) 31 | }) 32 | 33 | it("setusername",()=>{ 34 | const nextState = rootReducer(fakeState,setUsername("Terry")) 35 | expect( nextState.get("username") ).to.equal("Terry") 36 | }) 37 | 38 | it("switch chat room",()=>{ 39 | const nextState = rootReducer(fakeState, switchRoom("0")) 40 | expect( nextState.get("currentRoom") ).to.equal("0") 41 | }) 42 | 43 | it("send new message",()=>{ 44 | const action = newMessage({ 45 | roomId: "0", user:"eisneim",content:"some message" 46 | }) 47 | expect(action.message.time).to.be.ok 48 | const nextState = rootReducer(fakeState, action ) 49 | 50 | expect( nextState.getIn(["messages","0"]).size ).to.equal(1) 51 | const nextNextState = rootReducer(fakeState,{ 52 | type:"NEW_MESSAGE",message:{ 53 | roomId: "1", user:"terry",time:"12:00",content:"some message" 54 | } 55 | }) 56 | expect( nextNextState.getIn(["messages","1"]).size ).to.equal(3) 57 | }) 58 | 59 | }) 60 | 61 | -------------------------------------------------------------------------------- /redux-chat基本功能/test/clientTesthelper.js: -------------------------------------------------------------------------------- 1 | import jsdom from "jsdom" 2 | import chai from 'chai'; 3 | import chaiImmutable from 'chai-immutable'; 4 | 5 | const doc = jsdom.jsdom("") 6 | const win = doc.defaultView 7 | 8 | global.document = doc 9 | global.window = win 10 | 11 | Object.keys(window).forEach((key) => { 12 | if (!(key in global)) { 13 | global[key] = window[key]; 14 | } 15 | }); 16 | 17 | chai.use(chaiImmutable); -------------------------------------------------------------------------------- /redux-chat基本功能/test/server/core_spec.js: -------------------------------------------------------------------------------- 1 | import {expect} from "chai" 2 | import {v1} from "uuid" 3 | import {fromJS,Map,List} from "immutable" 4 | 5 | import { 6 | addRoom, 7 | removeRoom 8 | } from "../../src/server/core.js" 9 | 10 | describe("rooms", ()=>{ 11 | it("能够添加房间:addRoom",()=>{ 12 | var firstRoom = {name:"first room",id:v1(), owner: "eisneim" } 13 | const nextState = addRoom( undefined, firstRoom ) 14 | const rooms = nextState.get("rooms") 15 | expect( rooms ).to.be.ok 16 | expect( rooms.get(0) ).to.equal(Map(firstRoom)) 17 | 18 | const nextNextState = addRoom(nextState,{ 19 | name:"second room",owner:"terry" 20 | }) 21 | expect(nextNextState.getIn(["rooms",1,"name"])).to.equal("second room") 22 | }) 23 | 24 | const mockState = fromJS({ 25 | rooms: [{name:"first room",id:v1(), owner: "eisneim" }] 26 | }) 27 | 28 | it("能被创建者删除",()=>{ 29 | const state = removeRoom( mockState, { 30 | id: mockState.getIn(["rooms",0,"id"]), 31 | user: "eisneim" 32 | }) 33 | 34 | expect( state.get("rooms").size ).to.equal(0) 35 | }) 36 | 37 | it("不能被其他人删除",()=>{ 38 | const state = removeRoom( mockState, { 39 | id: mockState.getIn(["rooms",0,"id"]), 40 | user: "terry" 41 | }) 42 | 43 | expect( state.get("rooms").size ).to.equal(1) 44 | 45 | }) 46 | 47 | }) 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /redux-chat基本功能/test/server/reducer_spec.js: -------------------------------------------------------------------------------- 1 | import {expect} from "chai" 2 | import {fromJS,Map,List} from "immutable" 3 | import {v1} from "uuid" 4 | 5 | import coreReducer from "../../src/server/reducer" 6 | import {addRoom,removeRoom} from "../../src/server/actionCreator.js" 7 | 8 | describe("server端核心Reducer",()=>{ 9 | 10 | it("可以当做一个reducer",()=>{ 11 | var id = v1() 12 | var actions = [ 13 | {type:"ADD_ROOM",room:{id,name:"1",owner:"eisneim"}}, 14 | {type:"ADD_ROOM",room:{name:"2",owner:"terry"}}, 15 | {type:"ADD_ROOM",room:{name:"3",owner:"eisneim"}}, 16 | {type:"REMOVE_ROOM",payload:{id:id,user:"eisneim"}}, 17 | ] 18 | const finalState = actions.reduce( coreReducer, undefined ) 19 | console.log(finalState) 20 | expect(finalState.get("rooms").size).to.equal(2) 21 | expect(finalState.getIn(["rooms",0,"owner"])).to.equal("terry") 22 | }) 23 | 24 | it("使用actionCreator",()=>{ 25 | var id = v1() 26 | var actions = [ 27 | addRoom({id,name:"1",owner:"eisneim"}), 28 | addRoom({name:"2",owner:"terry"}), 29 | addRoom({name:"3",owner:"eisneim"}), 30 | removeRoom({id:id,user:"eisneim"}), 31 | ] 32 | const finalState = actions.reduce( coreReducer, undefined ) 33 | console.log(finalState) 34 | expect(finalState.get("rooms").size).to.equal(2) 35 | expect(finalState.getIn(["rooms",0,"owner"])).to.equal("terry") 36 | 37 | }) 38 | 39 | }) -------------------------------------------------------------------------------- /redux-chat基本功能/test/server/store_spec.js: -------------------------------------------------------------------------------- 1 | import {fromJS} from "immutable" 2 | import {expect} from "chai" 3 | 4 | import {addRoom} from "../../src/server/actionCreator.js" 5 | import {makeStore} from "../../src/server/store.js" 6 | 7 | describe("server store",()=>{ 8 | 9 | it("dispatch actions", ( done )=>{ 10 | const mockState = fromJS({ 11 | rooms:[] 12 | }) 13 | const store = makeStore( mockState ) 14 | 15 | store.subscribe(()=>{ 16 | const state = store.getState() 17 | expect( state.get("rooms").size ).to.equal(1) 18 | done() 19 | }) 20 | 21 | store.dispatch( addRoom({ 22 | name:"聊天室",owner:"terry" 23 | }) ) 24 | 25 | }) 26 | 27 | }) -------------------------------------------------------------------------------- /redux-chat基本功能/test/serverTestHelper.js: -------------------------------------------------------------------------------- 1 | import chai from "chai" 2 | import chaiImmutable from "chai-immutable" 3 | 4 | chai.use( chaiImmutable ) -------------------------------------------------------------------------------- /redux-chat基本功能/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require("path") 3 | 4 | module.exports = { 5 | entry:[ 6 | // for hot loader: WebpackDevServer host and port 7 | "webpack-dev-server/client?http://localhost:8080", 8 | // for hot loader: "only" prevents reload on syntax errors 9 | "webpack/hot/only-dev-server", 10 | // our appʼs entry point 11 | "./src/client/index.js" 12 | ], 13 | module:{ 14 | loaders:[{ 15 | test:/\.jsx?$/, 16 | include: path.join(__dirname,"src"), 17 | loaders: ["react-hot","babel"], 18 | }] 19 | }, 20 | resolve:{ 21 | extensions:["",".js",".jsx"] 22 | }, 23 | output:{ 24 | path: __dirname + "/public/build", 25 | filename:"boundle.js", 26 | publicPath:"http://localhost:8080/build", 27 | }, 28 | devServer: { 29 | contentBase: "./public", 30 | hot: true, 31 | host:"localhost", 32 | proxy:{ 33 | "*": "http://localhost:"+3000 34 | } 35 | }, 36 | plugins:[ 37 | new webpack.HotModuleReplacementPlugin() 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /redux-chat服务器端渲染/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-chat", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "testServer": "mocha --compilers js:babel-core/register --require ./test/serverTestHelper.js ./test/server --recursive", 8 | "testServer:watch": "npm run testServer -- --watch", 9 | "testClient": "mocha --compilers js:babel-core/register --require ./test/ClientTestHelper.js ./test/client --recursive", 10 | "testClient:watch": "npm run testClient -- --watch" 11 | }, 12 | "babel": { 13 | "presets": [ 14 | "es2015", 15 | "react" 16 | ] 17 | }, 18 | "keywords": [], 19 | "author": "", 20 | "license": "ISC", 21 | "dependencies": { 22 | "ejs": "^2.3.4", 23 | "express": "^4.13.3", 24 | "immutable": "^3.7.5", 25 | "mocha": "^2.3.4", 26 | "react": "^0.14.3", 27 | "react-addons-pure-render-mixin": "^0.14.3", 28 | "react-dom": "^0.14.3", 29 | "react-hot-loader": "^1.3.0", 30 | "react-mixin": "^3.0.3", 31 | "react-redux": "^4.0.0", 32 | "redux": "^3.0.4", 33 | "socket.io": "^1.3.7", 34 | "socket.io-client": "^1.3.7", 35 | "uuid": "^2.0.1" 36 | }, 37 | "devDependencies": { 38 | "babel-cli": "^6.2.0", 39 | "babel-core": "^6.2.1", 40 | "babel-loader": "^6.2.0", 41 | "babel-preset-es2015": "^6.1.18", 42 | "babel-preset-react": "^6.1.18", 43 | "chai": "^3.4.1", 44 | "chai-immutable": "^1.5.3", 45 | "webpack": "^1.12.9", 46 | "webpack-dev-server": "^1.14.0", 47 | "jsdom": "^5.6.1", 48 | "react-addons-test-utils": "^0.14.3" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /redux-chat服务器端渲染/public/css/style.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | *, *:before, *:after { 6 | -webkit-box-sizing: border-box; 7 | -moz-box-sizing: border-box; 8 | box-sizing: border-box; 9 | } 10 | 11 | article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block;}audio,canvas,video{display:inline-block;}audio:not([controls]){display:none;height:0;}[hidden]{display:none;}html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;}body{margin:0;}a:focus{outline:thin dotted;}a:active,a:hover{outline:0;}h1{font-size:2em;margin:0.67em 0;}abbr[title]{border-bottom:1px dotted;}b,strong{font-weight:bold;}dfn{font-style:italic;}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0;}mark{background:#ff0;color:#000;}code,kbd,pre,samp{font-family:monospace,serif;font-size:1em;}pre{white-space:pre-wrap;}q{quotes:"\201C" "\201D" "\2018" "\2019";}small{font-size:80%;}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline;}sup{top:-0.5em;}sub{bottom:-0.25em;}img{border:0;}svg:not(:root){overflow:hidden;}figure{margin:0;}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em;}legend{border:0;padding:0;}button,input,select,textarea{font-family:inherit;font-size:100%;margin:0;}button,input{line-height:normal;}button,select{text-transform:none;}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer;}button[disabled],html input[disabled]{cursor:default;}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0;}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box;}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none;}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0;}textarea{overflow:auto;vertical-align:top;}table{border-collapse:collapse;border-spacing:0;} 12 | h1,h2,h3,h4,h5{font-weight: normal;} 13 | 14 | .clearfix:before, 15 | .clearfix:after { 16 | content: " "; /* 1 */ 17 | display: table; /* 2 */ 18 | } 19 | .clearfix:after { 20 | clear: both; 21 | } 22 | 23 | .center-block { 24 | display: block; 25 | margin-left: auto; 26 | margin-right: auto; 27 | } 28 | .pull-right { 29 | float: right !important; 30 | } 31 | .pull-left { 32 | float: left !important; 33 | } 34 | .text-left{text-align: left;} 35 | .text-center{text-align: center;} 36 | .text-right{text-align: right;} 37 | 38 | .flex-row{ 39 | display: flex; 40 | flex-direction: row; 41 | /*// align-items: center;*/ 42 | } 43 | .flex-column{ 44 | flex-direction: column; 45 | display: flex; 46 | justify-content: center; 47 | } 48 | .flex-center{ 49 | display: flex; 50 | align-items: center; 51 | justify-content: center; 52 | } 53 | .flex{ 54 | flex:1; 55 | align-self:center; 56 | /*align-self: auto | flex-start | flex-end | center | baseline | stretch;*/ 57 | } 58 | 59 | 60 | body{ 61 | font-family: Helvetica Neue,"Hiragino Sans GB","Microsoft YaHei","微软雅黑","STHeiti","WenQuanYi Micro Hei",SimSun,sans-serif,Arial; 62 | background-color: #fafafa; 63 | height: 100%; 64 | } 65 | html,body,#app,#app>.flex-row,#chat-main{ 66 | height: 100%; 67 | } 68 | 69 | 70 | #chat-nav{ 71 | width: 250px; 72 | border-right: solid 1px #ddd; 73 | padding:20px; 74 | } 75 | .chat-room-list a{ 76 | display: block; 77 | padding-left: 15px; 78 | color: #27AE60; 79 | margin-bottom:10px; 80 | } 81 | 82 | .chat-room-list a.active{ 83 | background-color: #ddd; 84 | padding-top:8px;padding-bottom:8px; 85 | text-decoration: none; 86 | } 87 | 88 | #chat-main{ 89 | /*overflow-y: auto;*/ 90 | position: relative; 91 | } 92 | #chat-main header{ 93 | border-bottom: solid 1px #ddd; 94 | background-color: #fff; 95 | padding: 15px 10px; 96 | } 97 | #chat-main header h3{ 98 | margin: 0; 99 | } 100 | 101 | 102 | .chat-messages{ 103 | display: block; 104 | list-style: none; 105 | padding:10px; 106 | margin: 0; 107 | height: 80%; 108 | height: calc( 100% - 80px ); 109 | overflow-y:auto; 110 | padding-bottom: 100px; 111 | } 112 | .chat-messages >li{ 113 | } 114 | .message-inner{ 115 | background-color: #2ECC71; 116 | display: inline-block; 117 | width:auto; 118 | padding:8px 12px; 119 | margin:5px 0; 120 | border-radius: 6px; 121 | color: #fff; 122 | outline: none; 123 | min-width: 300px; 124 | } 125 | .message-inner p{ 126 | line-height: 1; 127 | margin:6px 0; 128 | } 129 | 130 | .message-self .message-inner{ 131 | background-color: #eee; 132 | color:#333; 133 | float: right; 134 | } 135 | .chat-username{ 136 | font-size: 1.2em; 137 | } 138 | .chat-username small{ 139 | margin-left: 10px; color:#ccc; 140 | } 141 | 142 | #chat-inputbox{ 143 | width:100%; 144 | position: absolute; 145 | padding: 10px; 146 | bottom:0; 147 | left:0; 148 | border-top: solid 1px #ddd; 149 | background-color: #fff; 150 | } 151 | #chat-inputbox textarea{ 152 | width:100%; 153 | min-height: 60px 154 | margin:0; 155 | } 156 | 157 | /*------------------------------*/ 158 | 159 | .btn { 160 | display: inline-block; 161 | padding: 8px 16px; 162 | border: 2px solid #ddd; 163 | color: #ddd; 164 | text-decoration: none; 165 | margin: 0px 10px 0 0; 166 | transition: all .6s; 167 | min-width: 100px; 168 | text-align: center; 169 | background-color: transparent; 170 | } 171 | .btn:hover { 172 | background: #ddd; 173 | color: #fff; 174 | } 175 | .btn.sm{ 176 | padding: 3px 8px; 177 | font-size:14px; 178 | } 179 | .btn.md{ 180 | padding: 12px 20px 181 | } 182 | /* Big Size */ 183 | .btn.lg{ 184 | padding: 20px 30px; 185 | } 186 | /* Border Radius */ 187 | .btn.style-1{ 188 | border-radius: 10px; 189 | } 190 | .btn.style-2{ 191 | border-radius: 40px; 192 | } 193 | 194 | /* Color 1 #16A085 */ 195 | .btn.color-1 {border-color: #16A085;color: #16A085;} 196 | .btn.color-1:hover, .color-1.active {background: #16A085;color: #fff;} 197 | 198 | /* Color 2 #27AE60 */ 199 | .btn.color-2 {border-color: #27AE60;color: #27AE60;} 200 | .btn.color-2:hover,.color-2.active {background: #27AE60;color: #fff;} 201 | 202 | /* Color 3 #2980B9 */ 203 | .btn.color-3 {border-color: #2980B9;color: #2980B9;} 204 | .btn.color-3:hover,.color-3.active {background: #2980B9;color: #fff;} 205 | 206 | /* Color 4 #8E44AD */ 207 | .btn.color-4 {border-color: #8E44AD;color: #8E44AD;} 208 | .btn.color-4:hover,.color-4.active {background: #8E44AD;color: #fff;} 209 | 210 | /* Color 5 #2C3E50 */ 211 | .btn.color-5 {border-color: #2C3E50;color: #2C3E50;} 212 | .btn.color-5:hover,.color-5.active {background: #2C3E50;color: #fff;} 213 | -------------------------------------------------------------------------------- /redux-chat服务器端渲染/src/client/actionCreators.js: -------------------------------------------------------------------------------- 1 | import {fromJS,Map} from "immutable" 2 | import {yymmddhhmm} from "../shared/utils/dateTime" 3 | 4 | export function setState(state){ 5 | return { 6 | type:"SET_STATE", 7 | state: Map.isMap(state) ? state : fromJS(state) 8 | } 9 | } 10 | 11 | export function setUsername(username){ 12 | return { 13 | type: "SET_USERNAME", username 14 | } 15 | } 16 | 17 | export function switchRoom(roomId){ 18 | return { 19 | type:"SWITCH_ROOM", roomId, 20 | meta:{ remote:true }, 21 | } 22 | } 23 | 24 | export function newMessage({roomId,content,user,time}, isFromServer ){ 25 | return { 26 | type:"NEW_MESSAGE", 27 | meta:{ remote: !isFromServer }, 28 | message: { 29 | roomId, content:content||"", user, 30 | time: yymmddhhmm() 31 | } 32 | } 33 | } 34 | 35 | export function addRoom( room ){ 36 | if( !room || !room.owner) throw new Error("addRoom() room.owner is required") 37 | 38 | return { 39 | type:"ADD_ROOM", room, 40 | meta:{ remote:true }, 41 | } 42 | } 43 | 44 | export function removeRoom( id, user ){ 45 | return { 46 | type:"REMOVE_ROOM", 47 | payload:{ id, user }, 48 | meta:{ remote:true }, 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /redux-chat服务器端渲染/src/client/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React,{ Component } from "react" 2 | 3 | import MessageList from "./MessageList" 4 | import InputBox from "./InputBox" 5 | import RoomList from "./RoomList" 6 | import { newMessage, switchRoom, addRoom,removeRoom } from "../actionCreators" 7 | 8 | class App extends Component { 9 | getCurrentRoomName(){ 10 | if(!this.props.currentRoom ) return "无" 11 | const room = this.props.rooms.find(r=>r.get("id") === this.props.currentRoom ) 12 | return ( room && room.get ) ? room.get("name") : room 13 | } 14 | 15 | isOwner( ){ 16 | if(!this.props.currentRoom || !this.props.username ) return false 17 | const room = this.props.rooms.find(r=>r.get("id") === this.props.currentRoom ) 18 | if(!room) return false; 19 | return room.get("owner") == this.props.username 20 | } 21 | 22 | getMessages(){ 23 | return this.props.messages ? 24 | this.props.messages.get(this.props.currentRoom) : [] 25 | } 26 | 27 | addRoom(){ 28 | var name = prompt("房间名称") 29 | if(!name) return alert("不能没有房间名称") 30 | 31 | this.props.dispatch( addRoom({ 32 | name, owner: this.props.username 33 | }) ) 34 | } 35 | 36 | removeRoom(){ 37 | this.props.dispatch(switchRoom( )) 38 | 39 | this.props.dispatch( removeRoom( 40 | this.props.currentRoom, this.props.username 41 | ) ) 42 | } 43 | 44 | sendMessage(message){ 45 | this.props.dispatch( newMessage({ 46 | roomId: this.props.currentRoom, 47 | user: this.props.username, 48 | content: message 49 | }) ) 50 | } 51 | 52 | render(){ 53 | const { currentRoom, rooms, username, dispatch } = this.props 54 | 55 | return ( 56 |
    57 | 66 | { !currentRoom ?

    请选择一个聊天室

    : 67 |
    68 |
    69 |

    当前聊天室:{ this.getCurrentRoomName() }

    70 | 71 | { !this.isOwner() ? "": 72 | 74 | } 75 |
    76 | 77 | 78 |
    79 | } 80 |
    81 | ) 82 | } 83 | } 84 | 85 | 86 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 87 | import reactMixin from "react-mixin"; 88 | reactMixin.onClass(App, PureRenderMixin ) 89 | 90 | import { connect } from 'react-redux' 91 | function mapStateToProps ( state ){ 92 | return { 93 | rooms: state.get("rooms"), 94 | currentRoom: state.get("currentRoom"), 95 | username: state.get("username"), 96 | messages: state.get("messages") 97 | } 98 | } 99 | 100 | export const ConnectedApp = connect( mapStateToProps )(App) 101 | 102 | export default App 103 | -------------------------------------------------------------------------------- /redux-chat服务器端渲染/src/client/components/InputBox.jsx: -------------------------------------------------------------------------------- 1 | import React,{ Component } from "react" 2 | import ReactDOM from "react-dom" 3 | 4 | class InputBox extends Component { 5 | 6 | handleSubmit(e){ 7 | e.preventDefault() 8 | var $textarea = ReactDOM.findDOMNode( this.refs.textarea ) 9 | if( typeof this.props.sendMessage === "function" ){ 10 | this.props.sendMessage( $textarea.value ) 11 | $textarea.value = "" 12 | }else{ 13 | console.log("props.sendMessage not defined!!") 14 | } 15 | 16 | } 17 | 18 | render(){ 19 | return ( 20 |
    21 |
    22 |
    23 | 24 |
    25 |
    26 | 27 |
    28 |
    29 |
    30 | ) 31 | } 32 | } 33 | 34 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 35 | import reactMixin from "react-mixin"; 36 | reactMixin.onClass(InputBox, PureRenderMixin ) 37 | 38 | 39 | export default InputBox -------------------------------------------------------------------------------- /redux-chat服务器端渲染/src/client/components/Message.jsx: -------------------------------------------------------------------------------- 1 | import React,{ Component } from "react" 2 | 3 | class Message extends Component { 4 | render(){ 5 | const { message, isSelf } = this.props 6 | const className = isSelf ? "message-self":"" 7 | 8 | return ( 9 |
  • 10 |
    11 |

    {message.get("user")} 12 | {message.get("time")} 13 |

    14 |

    {message.get("content")}

    15 |
    16 |
  • 17 | ) 18 | } 19 | } 20 | 21 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 22 | import reactMixin from "react-mixin"; 23 | reactMixin.onClass(Message, PureRenderMixin ) 24 | 25 | export default Message 26 | -------------------------------------------------------------------------------- /redux-chat服务器端渲染/src/client/components/MessageList.jsx: -------------------------------------------------------------------------------- 1 | import React,{ Component, PropTypes } from "react" 2 | import Message from "./Message" 3 | 4 | class MessageList extends Component { 5 | isSelf( message ){ 6 | return this.props.username === message.get("user") 7 | } 8 | 9 | $getMessages( messages ){ 10 | if(!messages || messages.size == 0) 11 | return

    还没有信息

    12 | 13 | return messages.map((message,index)=>{ 14 | return 17 | }) 18 | } 19 | 20 | render(){ 21 | return ( 22 |
      23 | { 24 | this.$getMessages( this.props.messages ) 25 | } 26 |
    27 | ) 28 | } 29 | 30 | } 31 | 32 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 33 | import reactMixin from "react-mixin"; 34 | reactMixin.onClass(MessageList, PureRenderMixin ) 35 | 36 | export default MessageList 37 | -------------------------------------------------------------------------------- /redux-chat服务器端渲染/src/client/components/RoomList.jsx: -------------------------------------------------------------------------------- 1 | import React,{ Component} from "react" 2 | 3 | class RoomList extends Component { 4 | 5 | isActive( room, currentRoom ){ 6 | return room.get("id") === currentRoom 7 | } 8 | 9 | render(){ 10 | const {rooms,currentRoom} = this.props 11 | 12 | return ( 13 |
    14 | { 15 | rooms.map((room,index)=>{ 16 | return ( 17 | this.props.switchRoom(room.get("id")) } 19 | key={index} href="#"> 20 | {room.get("name")} 21 | 22 | ) 23 | }) 24 | } 25 |
    26 | ) 27 | } 28 | } 29 | import PureRenderMixin from 'react-addons-pure-render-mixin'; 30 | import reactMixin from "react-mixin"; 31 | reactMixin.onClass( RoomList, PureRenderMixin ) 32 | 33 | export default RoomList -------------------------------------------------------------------------------- /redux-chat服务器端渲染/src/client/index.js: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom" 2 | import React from "react" 3 | import {ConnectedApp} from "./components/App" 4 | 5 | import {Provider} from "react-redux" 6 | 7 | import {createStore, applyMiddleware } from 'redux' 8 | import { logger, socketMiddleware } from "./middleware" 9 | 10 | import rootReducer from "./reducer" 11 | import { setState, newMessage } from "./actionCreators" 12 | import { getInitialState, saveToStorage } from "./store.js" 13 | 14 | import {socket} from "./io" 15 | 16 | const createStoreWithMiddleware = applyMiddleware( 17 | logger, 18 | socketMiddleware(socket) 19 | )( createStore ) 20 | const store = createStoreWithMiddleware( rootReducer, getInitialState() ) 21 | 22 | socket.on("state",state => { 23 | store.dispatch(setState(state)) 24 | }) 25 | 26 | socket.on("message",message => { 27 | console.log("get message from server") 28 | store.dispatch( newMessage(message, true ) ) 29 | }) 30 | 31 | 32 | // --------------------------- 33 | 34 | var $app = document.getElementById("app") 35 | 36 | function render(){ 37 | // const store = store.getState() 38 | 39 | ReactDOM.render( 40 | 41 | 42 | , 43 | $app 44 | ) 45 | } 46 | 47 | render() 48 | 49 | store.subscribe(()=>{ 50 | saveToStorage( store.getState() ) 51 | }) 52 | 53 | -------------------------------------------------------------------------------- /redux-chat服务器端渲染/src/client/io.js: -------------------------------------------------------------------------------- 1 | import IO from "socket.io-client" 2 | 3 | export const socket = IO("http://localhost:3000") 4 | 5 | socket.on('disconnect', ()=>{ 6 | console.log('user disconnected'); 7 | }); -------------------------------------------------------------------------------- /redux-chat服务器端渲染/src/client/middleware.js: -------------------------------------------------------------------------------- 1 | 2 | export const socketMiddleware = socket => store => next => action => { 3 | if (action.meta && action.meta.remote) { 4 | socket.emit('action', action); 5 | } 6 | 7 | return next(action) 8 | } 9 | 10 | /** 11 | * 记录所有被发起的 action 以及产生的新的 state。 12 | */ 13 | export const logger = store => next => action => { 14 | console.group(action.type) 15 | console.info('dispatching', action) 16 | let result = next(action) 17 | const nextState = store.getState() 18 | console.log('next state', nextState.toJS? nextState.toJS() : nextState ) 19 | console.groupEnd(action.type) 20 | return result 21 | } 22 | 23 | /** 24 | * 让你可以发起一个函数来替代 action。 25 | * 这个函数接收 `dispatch` 和 `getState` 作为参数。 26 | * 27 | * 对于(根据 `getState()` 的情况)提前退出,或者异步控制流( `dispatch()` 一些其他东西)来说,这非常有用。 28 | * 29 | * `dispatch` 会返回被发起函数的返回值。 30 | */ 31 | export const thunk = store => next => action => 32 | typeof action === 'function' ? 33 | action(store.dispatch, store.getState) : 34 | next(action) 35 | 36 | /* 37 | onclick={dispatch( (dispatch, getState)=>{ 38 | const state = getState() 39 | if(something wrong) return 40 | 41 | doSomeApiRequest( state.apiurl ) 42 | .then(data=>{ 43 | dispatch( requestSomeApiSuccess(data) ) 44 | }, error => dispatch(requestSomeApiFail(e)) ) 45 | })} 46 | */ 47 | 48 | /** 49 | * 用 { meta: { delay: N } } 来让 action 延迟 N 毫秒。 50 | * 在这个案例中,让 `dispatch` 返回一个取消 timeout 的函数。 51 | */ 52 | export const timeoutScheduler = store => next => action => { 53 | if (!action.meta || !action.meta.delay) { 54 | return next(action) 55 | } 56 | 57 | let timeoutId = setTimeout( 58 | () => next(action), 59 | action.meta.delay 60 | ) 61 | 62 | return function cancel() { 63 | clearTimeout(timeoutId) 64 | } 65 | } 66 | 67 | 68 | /** 69 | * 使你除了 action 之外还可以发起 promise。 70 | * 如果这个 promise 被 resolved,他的结果将被作为 action 发起。 71 | * 这个 promise 会被 `dispatch` 返回,因此调用者可以处理 rejection。 72 | */ 73 | const vanillaPromise = store => next => action => { 74 | if (typeof action.then !== 'function') { 75 | return next(action) 76 | } 77 | 78 | return Promise.resolve(action).then(store.dispatch) 79 | } 80 | 81 | /** 82 | * 让你可以发起带有一个 { promise } 属性的特殊 action。 83 | * 84 | * 这个 middleware 会在开始时发起一个 action,并在这个 `promise` resolve 时发起另一个成功(或失败)的 action。 85 | * 86 | * 为了方便起见,`dispatch` 会返回这个 promise 让调用者可以等待。 87 | */ 88 | const readyStatePromise = store => next => action => { 89 | if (!action.promise) { 90 | return next(action) 91 | } 92 | 93 | function makeAction(ready, data) { 94 | let newAction = Object.assign({}, action, { ready }, data) 95 | delete newAction.promise 96 | return newAction 97 | } 98 | 99 | next(makeAction(false)) 100 | return action.promise.then( 101 | result => next(makeAction(true, { result })), 102 | error => next(makeAction(true, { error })) 103 | ) 104 | } 105 | -------------------------------------------------------------------------------- /redux-chat服务器端渲染/src/client/reducer.js: -------------------------------------------------------------------------------- 1 | import {fromJS,Map,List} from "immutable" 2 | 3 | export default function rootReducer(state=Map(),action){ 4 | switch ( action.type ){ 5 | case "SET_STATE": return state.merge(Map( action.state )) 6 | 7 | case "SET_USERNAME": 8 | return state.set("username", action.username ) 9 | 10 | case "SWITCH_ROOM": 11 | return state.set("currentRoom", action.roomId ) 12 | 13 | case "NEW_MESSAGE": 14 | if(!action.message||!action.message.roomId) 15 | return state 16 | 17 | if( state.get("messages").has( action.message.roomId ) ){ 18 | return state.updateIn( 19 | ["messages",action.message.roomId], 20 | array => array.push( Map(action.message) ) 21 | ) 22 | } else{ 23 | return state.setIn( 24 | ["messages",action.message.roomId], 25 | List.of( Map(action.message)) 26 | ) 27 | } 28 | 29 | default: return state 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /redux-chat服务器端渲染/src/client/store.js: -------------------------------------------------------------------------------- 1 | import {Map,fromJS} from "immutable" 2 | 3 | const STATE_KEY = "CHAT_APP_STATE" 4 | 5 | export function saveToStorage( state ){ 6 | var data = JSON.stringify(state.toJS ? state.toJS() : state) 7 | localStorage.setItem(STATE_KEY, data ) 8 | } 9 | 10 | export function getInitialState( ){ 11 | var stateString = localStorage.getItem( STATE_KEY ) 12 | if( !stateString ) { 13 | return fromJS({ 14 | rooms:[],messages:{}, 15 | username: prompt("用户名") 16 | }) 17 | } 18 | 19 | return fromJS(JSON.parse( stateString )) 20 | } 21 | -------------------------------------------------------------------------------- /redux-chat服务器端渲染/src/server/actionCreator.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export function addRoom( room ){ 4 | return { 5 | type:"ADD_ROOM",room: room 6 | } 7 | } 8 | 9 | export function removeRoom( payload ){ 10 | return { 11 | type:"REMOVE_ROOM",payload: payload 12 | } 13 | } -------------------------------------------------------------------------------- /redux-chat服务器端渲染/src/server/controller.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import {renderToString} from "react-dom/server" 3 | 4 | import {Provider} from "react-redux" 5 | import {ConnectedApp} from "../client/components/App" 6 | 7 | export const indexCtrl = store => ( req,res ) =>{ 8 | var appString = renderToString( 9 | 10 | 11 | 12 | ) 13 | 14 | const HTML = ` 15 | 16 | 17 | 18 | 19 | Redux socket.io Chat app 20 | 21 | 22 | 23 |
    ${appString}
    24 | 25 | 26 | 27 | `; 28 | res.end(HTML) 29 | } 30 | 31 | -------------------------------------------------------------------------------- /redux-chat服务器端渲染/src/server/core.js: -------------------------------------------------------------------------------- 1 | import {Map,List,fromJS} from "immutable" 2 | import {v1} from "uuid" 3 | 4 | export const INITIAL_STATE = fromJS({ 5 | rooms:[], 6 | }) 7 | 8 | export function addRoom( state=INITIAL_STATE, room ){ 9 | if( !room || !room.owner) return state 10 | 11 | return state.update("rooms", rooms => rooms.push(Map( { 12 | id: room.id || v1(), 13 | name: room.name || "no name", 14 | owner: room.owner, 15 | } )) ) 16 | } 17 | 18 | export function removeRoom( state, {id,user}){ 19 | const rooms = state.get("rooms") 20 | var index = rooms.findIndex( r => r.get("id") === id ) 21 | if(index == -1 || rooms.getIn([index,"owner"])!== user ) { 22 | // console.log("非房间创建者,不能删除该房间",index) 23 | return state 24 | } 25 | return state.update("rooms",rooms => rooms.splice(index,1) ) 26 | } 27 | 28 | 29 | -------------------------------------------------------------------------------- /redux-chat服务器端渲染/src/server/io.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_ROOM = "0" 2 | 3 | export default function listenWebSocket( io, store ){ 4 | io.on("connection", socket=>{ 5 | console.log("one client connected") 6 | 7 | socket.emit("state", store.getState() ) 8 | // join this to the default room 9 | socket.join( DEFAULT_ROOM ) 10 | // add/remove room logic goes here 11 | socket.on("action",action => { 12 | console.log("client action:", action ) 13 | switch( action.type ){ 14 | case "SWITCH_ROOM": 15 | return switchRoom( socket, action.roomId || DEFAULT_ROOM ) 16 | 17 | // send this message back 18 | case "NEW_MESSAGE": 19 | if( socket.rooms && socket.rooms.length>0 ){ 20 | socket.rooms.forEach(id=>{ 21 | socket.to( id ).emit("message", action.message) 22 | }) 23 | }else{ 24 | socket.emit( "message", action.message ) 25 | } 26 | return 27 | } 28 | 29 | store.dispatch(action) 30 | // now send back new state 31 | socket.emit("state", store.getState() ) 32 | if( ["ADD_ROOM","REMOVE_ROOM"].indexOf(action.type) > -1){ 33 | socket.broadcast.emit("state", store.getState() ) 34 | } 35 | }) 36 | 37 | 38 | socket.on('disconnect', () => { 39 | console.log('user disconnected'); 40 | }); 41 | }) 42 | } 43 | 44 | function switchRoom(socket,roomId){ 45 | socket.rooms.forEach( (room,index)=>{ 46 | console.log("should leave room, skip first one") 47 | if( index > 0 ){ 48 | socket.leave( room ) 49 | } 50 | }) 51 | 52 | setTimeout(()=>{ 53 | socket.join( roomId ) 54 | console.log( "roomId:",roomId, "socket.rooms:",socket.rooms ) 55 | },200) 56 | } -------------------------------------------------------------------------------- /redux-chat服务器端渲染/src/server/reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | addRoom, 3 | removeRoom 4 | } from "./core.js" 5 | 6 | 7 | export default function reducer(state, action ){ 8 | switch (action.type){ 9 | case "ADD_ROOM": 10 | return addRoom(state, action.room ) 11 | case "REMOVE_ROOM": 12 | return removeRoom(state, action.payload ) 13 | } 14 | return state 15 | } -------------------------------------------------------------------------------- /redux-chat服务器端渲染/src/server/server.js: -------------------------------------------------------------------------------- 1 | import express from "express" 2 | import {Server} from "http" 3 | 4 | var app = express() 5 | var http = Server( app ) 6 | 7 | // configs 8 | var rootPath = require('path').normalize(__dirname + '/../..'); 9 | app.set('views', __dirname +'/views') 10 | app.set('view engine', 'ejs') 11 | app.use(express.static( rootPath + "/public" )); 12 | 13 | var io = require('socket.io')(http); 14 | import {makeStore} from "./store" 15 | import listenWebSocket from "./io.js" 16 | 17 | const store = makeStore() 18 | listenWebSocket( io, store ) 19 | 20 | 21 | import { indexCtrl } from "./controller" 22 | app.use(indexCtrl(store ) ) 23 | 24 | http.listen(3000,()=>{ 25 | console.log("listening on port 3000") 26 | }) 27 | -------------------------------------------------------------------------------- /redux-chat服务器端渲染/src/server/store.js: -------------------------------------------------------------------------------- 1 | import { createStore } from "redux" 2 | import coreReducer from "./reducer" 3 | 4 | import {fromJS} from "immutable" 5 | 6 | export const DEFAULT_STATE = fromJS({ 7 | rooms:[{ 8 | name:"公开房间", id:"0" 9 | }], 10 | }) 11 | 12 | export function makeStore( state=DEFAULT_STATE ){ 13 | return createStore( coreReducer, state ) 14 | } -------------------------------------------------------------------------------- /redux-chat服务器端渲染/src/server/views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Redux socket.io Chat app 6 | 7 | 8 | 9 |
    10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /redux-chat服务器端渲染/src/shared/utils/dateTime.js: -------------------------------------------------------------------------------- 1 | Date.prototype.yyyymmdd = function( isChinese, delimiter) { 2 | delimiter = delimiter || '-'; 3 | var yyyy = this.getFullYear().toString(); 4 | var mm = (this.getMonth()+1).toString(); // getMonth() is zero-based 5 | var dd = this.getDate().toString(); 6 | if(isChinese){ 7 | var date = yyyy + '年' + (mm[1]?mm:"0"+mm[0]) + '月' + (dd[1]?dd:"0"+dd[0])+'日'; 8 | }else{ 9 | var date = yyyy + delimiter + (mm[1]?mm:"0"+mm[0]) + delimiter + (dd[1]?dd:"0"+dd[0]); 10 | } 11 | return date 12 | }; 13 | // 只要年月,用于创建文件夹,将图片以 年月来分成不同的文件夹以便于管理 14 | Date.prototype.yyyymm = function(delimiter) { 15 | delimiter = delimiter || '-'; 16 | var yyyy = this.getFullYear().toString(); 17 | var mm = (this.getMonth()+1).toString(); 18 | return yyyy + delimiter + (mm[1]?mm:"0"+mm[0]); 19 | }; 20 | // chat time stamp: 14-9-22 12:20 21 | Date.prototype.yymmddhhmm = function(isChinese,delimiter) { 22 | delimiter = delimiter || '-'; 23 | var yy = this.getFullYear().toString(); 24 | var mm = (this.getMonth()+1).toString(); // getMonth() is zero-based 25 | var dd = this.getDate().toString(); 26 | var hh = this.getHours().toString(); 27 | var MM = this.getMinutes().toString(); 28 | if(isChinese){ 29 | return yy + '年' + (mm[1]?mm:"0"+mm[0]) + '月' + (dd[1]?dd:"0"+dd[0])+'日 '+ (hh[1]?hh:"0"+hh[0])+':'+(MM[1]?MM:"0"+MM[0]); 30 | } 31 | return yy + delimiter + (mm[1]?mm:"0"+mm[0]) + delimiter + (dd[1]?dd:"0"+dd[0])+' '+ (hh[1]?hh:"0"+hh[0])+':'+(MM[1]?MM:"0"+MM[0]); 32 | }; 33 | 34 | 35 | export function yyyymmdd(d,isChinese,delimiter){ 36 | return d? d.yyyymmdd(isChinese,delimiter) : new Date().yyyymmdd(isChinese,delimiter); 37 | }; 38 | 39 | export function yyyymm(d){ 40 | return d? d.yyyymm() : new Date().yyyymm(); 41 | }; 42 | 43 | export function yymmddhhmm(d,isChinese,delimiter){ 44 | return d? d.yymmddhhmm(isChinese,delimiter) : new Date().yymmddhhmm(isChinese,delimiter); 45 | }; -------------------------------------------------------------------------------- /redux-chat服务器端渲染/test/client/InputBox_spec.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom" 3 | import {fromJS,Map,List} from "immutable" 4 | import { expect } from "chai" 5 | import InputBox from "../../src/client/components/InputBox" 6 | 7 | import { 8 | Simulate, 9 | renderIntoDocument, 10 | findRenderedDOMComponentWithTag, 11 | scryRenderedDOMComponentsWithClass, 12 | } from "react-addons-test-utils" 13 | 14 | 15 | describe("InputBox",()=>{ 16 | it("send message",()=>{ 17 | var message 18 | function sendMessage(msg){ 19 | message = msg 20 | } 21 | const instance = renderIntoDocument( 22 | 23 | ) 24 | const $textarea = findRenderedDOMComponentWithTag(instance,"textarea") 25 | expect( $textarea ).to.be.ok 26 | // set value of textare 27 | $textarea.value = "some message" 28 | const $form = findRenderedDOMComponentWithTag(instance,"form") 29 | Simulate.submit( $form ) 30 | 31 | expect(message ).to.equal( "some message" ) 32 | }) 33 | 34 | }) 35 | 36 | -------------------------------------------------------------------------------- /redux-chat服务器端渲染/test/client/MessageList_spec.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import {fromJS,Map,List} from "immutable" 3 | import { expect } from "chai" 4 | import MessageList from "../../src/client/components/MessageList" 5 | 6 | import { 7 | Simulate, 8 | renderIntoDocument, 9 | scryRenderedDOMComponentsWithTag, 10 | scryRenderedDOMComponentsWithClass, 11 | } from "react-addons-test-utils" 12 | 13 | describe("MessageList",()=>{ 14 | it("render messages and my messages",()=>{ 15 | const messages = fromJS([ 16 | {user:"eisneim",content:"some message",time:"23:33"}, 17 | {user:"terry",content:"ss message",time:"12:33"}, 18 | ]) 19 | 20 | const component = renderIntoDocument( 21 | 22 | ) 23 | const $messages = scryRenderedDOMComponentsWithTag(component,"li") 24 | const $myMessages = scryRenderedDOMComponentsWithClass(component,"message-self") 25 | 26 | expect( $messages.length ).to.equal(2) 27 | expect( $myMessages.length ).to.equal(1) 28 | }) 29 | 30 | }) 31 | 32 | 33 | -------------------------------------------------------------------------------- /redux-chat服务器端渲染/test/client/RoomList_spect.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom" 3 | import {fromJS,Map,List} from "immutable" 4 | import { expect } from "chai" 5 | 6 | import RoomList from "../../src/client/components/RoomList" 7 | 8 | import TestUtil,{ 9 | Simulate, 10 | renderIntoDocument, 11 | isCompositeComponentWithType, 12 | scryRenderedDOMComponentsWithTag, 13 | scryRenderedDOMComponentsWithClass, 14 | } from "react-addons-test-utils" 15 | 16 | describe("RoomList组件",()=>{ 17 | 18 | it("render roomlist ",()=>{ 19 | const rooms = fromJS([ 20 | {id:"0", name:"room",owner:"eisneim"}, 21 | {id:"1", name:"room2",owner:"terry"}, 22 | ]) 23 | 24 | const component = renderIntoDocument( 25 | 26 | ) 27 | const $rooms = scryRenderedDOMComponentsWithTag(component,"a") 28 | expect( $rooms.length ).to.equal(2) 29 | const $active = scryRenderedDOMComponentsWithClass(component,"active") 30 | expect( $active.length ).to.equal(1) 31 | }) 32 | 33 | it("能够切换房间",()=>{ 34 | 35 | const rooms = fromJS([ 36 | {id:"0", name:"room",owner:"eisneim"}, 37 | {id:"1", name:"room2",owner:"terry"}, 38 | ]) 39 | var currentRoom = "0" 40 | function switchRoom(id){ 41 | console.log("change id:",id) 42 | currentRoom=id 43 | } 44 | const RoomListElm = ( 45 | 49 | ) 50 | const component = renderIntoDocument( RoomListElm ) 51 | const $rooms = scryRenderedDOMComponentsWithTag(component,"a") 52 | Simulate.click( ReactDOM.findDOMNode($rooms[1]) ) 53 | expect( currentRoom ).to.equal("1") 54 | 55 | expect( isCompositeComponentWithType( component,RoomList ) ).to.be.true 56 | // console.log("isElement",TestUtil.isElement(RoomListElm)) 57 | // console.log("isElementOfType",TestUtil.isElementOfType(RoomListElm,RoomList)) 58 | // console.log("TestUtil.isDOMComponent",TestUtil.isDOMComponent( component )) 59 | // console.log("isCompositeComponent",TestUtil.isCompositeComponent(component)) 60 | 61 | }) 62 | 63 | 64 | }) -------------------------------------------------------------------------------- /redux-chat服务器端渲染/test/client/reducer_spec.js: -------------------------------------------------------------------------------- 1 | import {fromJS} from "immutable" 2 | import {expect} from "chai" 3 | import rootReducer from "../../src/client/reducer" 4 | 5 | import { 6 | newMessage, setState, switchRoom, setUsername 7 | } from "../../src/client/actionCreators" 8 | 9 | const fakeState = fromJS({ 10 | rooms:[ 11 | {id:"0", name:"room",owner:"eisneim"}, 12 | {id:"1", name:"room2",owner:"terry"}, 13 | ], 14 | currentRoom: "1", 15 | username:"eisneim", 16 | messages: { 17 | "1":[ 18 | {user:"eisneim",content:"some message",time:"23:33"}, 19 | {user:"terry",content:"ss message",time:"12:33"}, 20 | ] 21 | } 22 | }) 23 | 24 | describe("client Root reducer",()=>{ 25 | it("set state",()=>{ 26 | const nextState = rootReducer(fakeState, 27 | setState(fromJS({username:"Joan",currentRoom:"0"})) 28 | ) 29 | expect(nextState.get("username")).to.equal("Joan") 30 | expect(nextState.get("rooms").size).to.equal(2) 31 | }) 32 | 33 | it("setusername",()=>{ 34 | const nextState = rootReducer(fakeState,setUsername("Terry")) 35 | expect( nextState.get("username") ).to.equal("Terry") 36 | }) 37 | 38 | it("switch chat room",()=>{ 39 | const nextState = rootReducer(fakeState, switchRoom("0")) 40 | expect( nextState.get("currentRoom") ).to.equal("0") 41 | }) 42 | 43 | it("send new message",()=>{ 44 | const action = newMessage({ 45 | roomId: "0", user:"eisneim",content:"some message" 46 | }) 47 | expect(action.message.time).to.be.ok 48 | const nextState = rootReducer(fakeState, action ) 49 | 50 | expect( nextState.getIn(["messages","0"]).size ).to.equal(1) 51 | const nextNextState = rootReducer(fakeState,{ 52 | type:"NEW_MESSAGE",message:{ 53 | roomId: "1", user:"terry",time:"12:00",content:"some message" 54 | } 55 | }) 56 | expect( nextNextState.getIn(["messages","1"]).size ).to.equal(3) 57 | }) 58 | 59 | }) 60 | 61 | -------------------------------------------------------------------------------- /redux-chat服务器端渲染/test/clientTesthelper.js: -------------------------------------------------------------------------------- 1 | import jsdom from "jsdom" 2 | import chai from 'chai'; 3 | import chaiImmutable from 'chai-immutable'; 4 | 5 | const doc = jsdom.jsdom("") 6 | const win = doc.defaultView 7 | 8 | global.document = doc 9 | global.window = win 10 | 11 | Object.keys(window).forEach((key) => { 12 | if (!(key in global)) { 13 | global[key] = window[key]; 14 | } 15 | }); 16 | 17 | chai.use(chaiImmutable); -------------------------------------------------------------------------------- /redux-chat服务器端渲染/test/server/core_spec.js: -------------------------------------------------------------------------------- 1 | import {expect} from "chai" 2 | import {v1} from "uuid" 3 | import {fromJS,Map,List} from "immutable" 4 | 5 | import { 6 | addRoom, 7 | removeRoom 8 | } from "../../src/server/core.js" 9 | 10 | describe("rooms", ()=>{ 11 | it("能够添加房间:addRoom",()=>{ 12 | var firstRoom = {name:"first room",id:v1(), owner: "eisneim" } 13 | const nextState = addRoom( undefined, firstRoom ) 14 | const rooms = nextState.get("rooms") 15 | expect( rooms ).to.be.ok 16 | expect( rooms.get(0) ).to.equal(Map(firstRoom)) 17 | 18 | const nextNextState = addRoom(nextState,{ 19 | name:"second room",owner:"terry" 20 | }) 21 | expect(nextNextState.getIn(["rooms",1,"name"])).to.equal("second room") 22 | }) 23 | 24 | const mockState = fromJS({ 25 | rooms: [{name:"first room",id:v1(), owner: "eisneim" }] 26 | }) 27 | 28 | it("能被创建者删除",()=>{ 29 | const state = removeRoom( mockState, { 30 | id: mockState.getIn(["rooms",0,"id"]), 31 | user: "eisneim" 32 | }) 33 | 34 | expect( state.get("rooms").size ).to.equal(0) 35 | }) 36 | 37 | it("不能被其他人删除",()=>{ 38 | const state = removeRoom( mockState, { 39 | id: mockState.getIn(["rooms",0,"id"]), 40 | user: "terry" 41 | }) 42 | 43 | expect( state.get("rooms").size ).to.equal(1) 44 | 45 | }) 46 | 47 | }) 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /redux-chat服务器端渲染/test/server/reducer_spec.js: -------------------------------------------------------------------------------- 1 | import {expect} from "chai" 2 | import {fromJS,Map,List} from "immutable" 3 | import {v1} from "uuid" 4 | 5 | import coreReducer from "../../src/server/reducer" 6 | import {addRoom,removeRoom} from "../../src/server/actionCreator.js" 7 | 8 | describe("server端核心Reducer",()=>{ 9 | 10 | it("可以当做一个reducer",()=>{ 11 | var id = v1() 12 | var actions = [ 13 | {type:"ADD_ROOM",room:{id,name:"1",owner:"eisneim"}}, 14 | {type:"ADD_ROOM",room:{name:"2",owner:"terry"}}, 15 | {type:"ADD_ROOM",room:{name:"3",owner:"eisneim"}}, 16 | {type:"REMOVE_ROOM",payload:{id:id,user:"eisneim"}}, 17 | ] 18 | const finalState = actions.reduce( coreReducer, undefined ) 19 | console.log(finalState) 20 | expect(finalState.get("rooms").size).to.equal(2) 21 | expect(finalState.getIn(["rooms",0,"owner"])).to.equal("terry") 22 | }) 23 | 24 | it("使用actionCreator",()=>{ 25 | var id = v1() 26 | var actions = [ 27 | addRoom({id,name:"1",owner:"eisneim"}), 28 | addRoom({name:"2",owner:"terry"}), 29 | addRoom({name:"3",owner:"eisneim"}), 30 | removeRoom({id:id,user:"eisneim"}), 31 | ] 32 | const finalState = actions.reduce( coreReducer, undefined ) 33 | console.log(finalState) 34 | expect(finalState.get("rooms").size).to.equal(2) 35 | expect(finalState.getIn(["rooms",0,"owner"])).to.equal("terry") 36 | 37 | }) 38 | 39 | }) -------------------------------------------------------------------------------- /redux-chat服务器端渲染/test/server/store_spec.js: -------------------------------------------------------------------------------- 1 | import {fromJS} from "immutable" 2 | import {expect} from "chai" 3 | 4 | import {addRoom} from "../../src/server/actionCreator.js" 5 | import {makeStore} from "../../src/server/store.js" 6 | 7 | describe("server store",()=>{ 8 | 9 | it("dispatch actions", ( done )=>{ 10 | const mockState = fromJS({ 11 | rooms:[] 12 | }) 13 | const store = makeStore( mockState ) 14 | 15 | store.subscribe(()=>{ 16 | const state = store.getState() 17 | expect( state.get("rooms").size ).to.equal(1) 18 | done() 19 | }) 20 | 21 | store.dispatch( addRoom({ 22 | name:"聊天室",owner:"terry" 23 | }) ) 24 | 25 | }) 26 | 27 | }) -------------------------------------------------------------------------------- /redux-chat服务器端渲染/test/serverTestHelper.js: -------------------------------------------------------------------------------- 1 | import chai from "chai" 2 | import chaiImmutable from "chai-immutable" 3 | 4 | chai.use( chaiImmutable ) -------------------------------------------------------------------------------- /redux-chat服务器端渲染/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require("path") 3 | 4 | module.exports = { 5 | entry:[ 6 | // for hot loader: WebpackDevServer host and port 7 | "webpack-dev-server/client?http://localhost:8080", 8 | // for hot loader: "only" prevents reload on syntax errors 9 | "webpack/hot/only-dev-server", 10 | // our appʼs entry point 11 | "./src/client/index.js" 12 | ], 13 | module:{ 14 | loaders:[{ 15 | test:/\.jsx?$/, 16 | include: path.join(__dirname,"src"), 17 | loaders: ["react-hot","babel"], 18 | }] 19 | }, 20 | resolve:{ 21 | extensions:["",".js",".jsx"] 22 | }, 23 | output:{ 24 | path: __dirname + "/public/build", 25 | filename:"boundle.js", 26 | publicPath:"http://localhost:8080/build", 27 | }, 28 | devServer: { 29 | contentBase: "./public", 30 | hot: true, 31 | host:"localhost", 32 | proxy:{ 33 | "*": "http://localhost:"+3000 34 | } 35 | }, 36 | plugins:[ 37 | new webpack.HotModuleReplacementPlugin() 38 | ] 39 | } 40 | --------------------------------------------------------------------------------