├── .babelrc ├── README.md ├── app ├── addImage.js ├── index.js └── sum.js ├── build ├── bundle.js ├── css │ └── main.0c219f8baaf2304c8f9f.css └── images │ └── big.d364620e5312f4427a916c11c6065311.jpeg ├── images ├── big.jpeg └── small.jpeg ├── index.html ├── package.json ├── project ├── .babelrc ├── database │ ├── constants.js │ ├── queries │ │ ├── CreateArtist.js │ │ ├── DeleteArtist.js │ │ ├── EditArtist.js │ │ ├── FindArtist.js │ │ ├── GetAgeRange.js │ │ ├── GetYearsActiveRange.js │ │ ├── SearchArtists.js │ │ ├── SetNotRetired.js │ │ ├── SetRetired.js │ │ └── db.js │ └── seeds │ │ └── artist.js ├── dist │ └── bundle.js ├── index.html ├── package.json ├── src │ ├── actions │ │ ├── index.js │ │ └── types.js │ ├── components │ │ ├── Header.js │ │ ├── Home.js │ │ ├── artists │ │ │ ├── ArtistCreate.js │ │ │ ├── ArtistDetail.js │ │ │ ├── ArtistEdit.js │ │ │ ├── ArtistFilter.js │ │ │ ├── ArtistIndex.js │ │ │ ├── ArtistMain.js │ │ │ └── Paginator.js │ │ └── filters │ │ │ ├── Picker.js │ │ │ ├── Range.js │ │ │ ├── Switch.js │ │ │ └── index.js │ ├── index.js │ ├── reducers │ │ ├── ArtistsReducer.js │ │ ├── ErrorReducer.js │ │ ├── FilterCriteriaReducer.js │ │ ├── SelectionReducer.js │ │ └── index.js │ └── router.js ├── style │ ├── materialize.css │ ├── react-range.css │ └── style.css └── webpack.config.js ├── styles └── addImage.css └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["babel-preset-env"] 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webpack:从入门到真实项目配置 2 | > 该文使用的 Webpack 版本为 3.6.0,本文分两部分。第一步是简单的使用 webpack,第二部分通过一个真实项目来配置 webpack,没有使用任何的 CLI,都是一步步配置直到完成生产代码的打包。[这是本项目对应的仓库](https://github.com/KieSun/webpack-demo),每个小节基本都对应了一次 commit。 3 | 4 | 这是本文的大纲,如果觉得有兴趣你就可以往下看了 5 | ![](https://user-gold-cdn.xitu.io/2017/9/17/aee41c18f38e99efaa09c7691ac7da4f) 6 | 7 | ## Webpack 到底是什么 8 | 9 | 自从出现模块化以后,大家可以将原本一坨代码分离到个个模块中,但是由此引发了一个问题。每个 JS 文件都需要从服务器去拿,由此会导致加载速度变慢。Webpack 最主要的目的就是为了解决这个问题,将所有小文件打包成一个或多个大文件,官网的图片很好的诠释了这个事情,除此之外,Webpack 也是一个能让你使用各种前端新技术的工具。 10 | 11 | ![](https://user-gold-cdn.xitu.io/2017/9/15/92a95decddcecb9e25304f3b6a9dfb9c) 12 | 13 | ## 简单使用 14 | 15 | #### 安装 16 | 17 | 在命令行中依次输入 18 | ```js 19 | mkdir webpack-demo 20 | cd webpack-demo 21 | // 创建 package.json,这里会问一些问题,直接回车跳过就行 22 | npm init 23 | // 推荐这个安装方式,当然你也安装在全局环境下 24 | // 这种安装方式会将 webpack 放入 devDependencies 依赖中 25 | npm install --save-dev webpack 26 | ``` 27 | 28 | 然后按照下图创建文件 29 | ![](https://user-gold-cdn.xitu.io/2017/9/15/5c88d60a69942526609db63c7919ddaf) 30 | 31 | 在以下文件写入代码 32 | ```js 33 | // sum.js 34 | // 这个模块化写法是 node 环境独有的,浏览器原生不支持使用 35 | module.exports = function(a, b) { 36 | return a + b 37 | } 38 | // index.js 39 | var sum = require('./sum') 40 | console.log(sum(1, 2)) 41 | ``` 42 | ```html 43 | 44 | 45 | 46 | Document 47 | 48 | 49 |
50 | 51 | 52 | 53 | ``` 54 | 55 | 现在我们开始配置最简单的 webpack,首先创建 `webpack.config.js` 文件,然后写入如下代码 56 | 57 | ```js 58 | // 自带的库 59 | const path = require('path') 60 | module.exports = { 61 | entry: './app/index.js', // 入口文件 62 | output: { 63 | path: path.resolve(__dirname, 'build'), // 必须使用绝对地址,输出文件夹 64 | filename: "bundle.js" // 打包后输出文件的文件名 65 | } 66 | } 67 | ``` 68 | 69 | 现在我们可以开始使用 webpack 了,在命令行中输入 70 | ``` 71 | node_modules/.bin/webpack 72 | ``` 73 | 74 | 没问题的话你应该可以看到类似的样子 75 | ![](https://user-gold-cdn.xitu.io/2017/9/15/a0c2767b86d0b34f3bf66225377d4663) 76 | 77 | 可以发现原本两个 JS 文件只有 100B,但是打包后却增长到 2.66KB,这之中 webpack 肯定做了什么事情,我们去 `bundle.js` 文件中看看。 78 | 79 | 把代码简化以后,核心思路是这样的 80 | 81 | ```js 82 | var array = [(function () { 83 | var sum = array[1] 84 | console.log(sum(1, 2)) 85 | }), 86 | (function (a,b) { 87 | return a + b 88 | }) 89 | ] 90 | array[0]() // -> 3 91 | ``` 92 | 93 | 因为 `module.export` 浏览器是不支持的,所以 webpack 将代码改成浏览器能识别的样子。现在将 `index.html` 文件在浏览器中打开,应该也可以看到正确的 log。 94 | 95 | 我们之前是在文件夹中安装的 webpack,每次要输入 `node_modules/.bin/webpack` 过于繁琐,可以在 `package.json` 如下修改 96 | 97 | ```js 98 | "scripts": { 99 | "start": "webpack" 100 | }, 101 | ``` 102 | 103 | 然后再次执行 `npm run start`,可以发现和之前的效果是相同的。简单的使用到此为止,接下来我们来探索 webpack 更多的功能。 104 | 105 | ## Loader 106 | 107 | Loader 是 webpack 一个很强大功能,这个功能可以让你使用很多新的技术。 108 | 109 | #### Babel 110 | 111 | Babel 可以让你使用 ES2015/16/17 写代码而不用顾忌浏览器的问题,Babel 可以帮你转换代码。首先安装必要的几个 Babel 库 112 | 113 | ``` 114 | npm i --save-dev babel-loader babel-core babel-preset-env 115 | ``` 116 | 117 | 先介绍下我们安装的三个库 118 | - babel-loader 用于让 webpack 知道如何运行 babel 119 | - babel-core 可以看做编译器,这个库知道如何解析代码 120 | - babel-preset-env 这个库可以根据环境的不同转换代码 121 | 122 | 接下来更改 `webpack-config.js` 中的代码 123 | 124 | ```js 125 | module.exports = { 126 | // ...... 127 | module: { 128 | rules: [ 129 | { 130 | // js 文件才使用 babel 131 | test: /\.js$/, 132 | // 使用哪个 loader 133 | use: 'babel-loader', 134 | // 不包括路径 135 | exclude: /node_modules/ 136 | } 137 | ] 138 | } 139 | } 140 | ``` 141 | 142 | 配置 Babel 有很多方式,这里推荐使用 .babelrc 文件管理。 143 | 144 | ```js 145 | // ..babelrc 146 | { 147 | "presets": ["babel-preset-env"] 148 | } 149 | ``` 150 | 151 | 现在将之前 JS 的代码改成 ES6 的写法 152 | 153 | ```js 154 | // sum.js 155 | export default (a, b) => { 156 | return a + b 157 | } 158 | // index.js 159 | import sum from './sum' 160 | console.log(sum(1, 2)) 161 | ``` 162 | 163 | 执行 `npm run start`,再观察 `bundle.js` 中的代码,可以发现代码被转换过了,并且同样可以正常 输出3。 164 | 165 | 当然 Babel 远不止这些功能,有兴趣的可以前往官网自己探索。 166 | 167 | #### 处理图片 168 | 169 | 这一小节我们将使用 `url-loader` 和 `file-loader`,这两个库不仅可以处理图片,还有其他的功能,有兴趣的可以自行学习。 170 | 171 | 先安装库 172 | ``` 173 | npm i --save-dev url-loader file-loader 174 | ``` 175 | 176 | 创建一个 `images` 文件夹,放入两张图片,并且在 `app` 文件夹下创建一个 js 文件处理图片 177 | ,目前的文件夹结构如图 178 | ![](https://user-gold-cdn.xitu.io/2017/9/15/6e945ab1dc6b1881bf49fe172d4b63cf) 179 | 180 | ```js 181 | // addImage.js 182 | let smallImg = document.createElement('img') 183 | // 必须 require 进来 184 | smallImg.src = require('../images/small.jpeg') 185 | document.body.appendChild(smallImg) 186 | 187 | let bigImg = document.createElement('img') 188 | bigImg.src = require('../images/big.jpeg') 189 | document.body.appendChild(bigImg) 190 | ``` 191 | 192 | 接下来修改 `webpack.config.js` 代码 193 | 194 | ```js 195 | module.exports = { 196 | // ... 197 | module: { 198 | rules: [ 199 | // ... 200 | { 201 | // 图片格式正则 202 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 203 | use: [ 204 | { 205 | loader: 'url-loader', 206 | // 配置 url-loader 的可选项 207 | options: { 208 | // 限制 图片大小 10000B,小于限制会将图片转换为 base64格式 209 | limit: 10000, 210 | // 超出限制,创建的文件格式 211 | // build/images/[图片名].[hash].[图片格式] 212 | name: 'images/[name].[hash].[ext]' 213 | } 214 | } 215 | ] 216 | } 217 | ] 218 | } 219 | } 220 | ``` 221 | 222 | 运行 `npm run start`,打包成功如下图 223 | ![](https://user-gold-cdn.xitu.io/2017/9/15/b460ba75c92052ffd2df037b76af7ddb) 224 | 225 | 可以发现大的图片被单独提取了出来,小的图片打包进了 `bundle.js` 中。 226 | 227 | 在浏览器中打开 HTML 文件,发现小图确实显示出来了,但是却没有看到大图,打开开发者工具栏,可以发现我们大图的图片路径是有问题的,所以我们又要修改 `webpack.config.js` 代码了。 228 | 229 | ```js 230 | module.exports = { 231 | entry: './app/index.js', // 入口文件 232 | output: { 233 | path: path.resolve(__dirname, 'build'), // 必须使用绝对地址,输出文件夹 234 | filename: "bundle.js", // 打包后输出文件的文件名 235 | publicPath: 'build/' // 知道如何寻找资源 236 | } 237 | // ... 238 | } 239 | ``` 240 | 241 | 最后运行下 `npm run start`,编译成功了,再次刷新下页面,可以发现这次大图被正确的显示了。下一小节我们将介绍如何处理 CSS 文件。 242 | 243 | #### 处理 CSS 文件 244 | 245 | 添加 `styles` 文件夹,新增 `addImage.css` 文件,然后在该文件中新增代码 246 | 247 | ```css 248 | img { 249 | border: 5px black solid; 250 | } 251 | .test {border: 5px black solid;} 252 | ``` 253 | 254 | 这一小节我们先使用 `css-loader` 和 `style-loader` 库。前者可以让 CSS 文件也支持 `impost`,并且会解析 CSS 文件,后者可以将解析出来的 CSS 通过标签的形式插入到 HTML 中,所以后面依赖前者。 255 | 256 | ``` 257 | npm i --save-dev css-loader style-loader 258 | ``` 259 | 260 | 首先修改 `addImage.js` 文件 261 | 262 | ```js 263 | import '../styles/addImage.css' 264 | 265 | let smallImg = document.createElement('img') 266 | smallImg.src = require('../images/small.jpeg') 267 | document.body.appendChild(smallImg) 268 | 269 | // let bigImg = document.createElement('img') 270 | // bigImg.src = require('../images/big.jpeg') 271 | // document.body.appendChild(bigImg) 272 | ``` 273 | 274 | 然后修改 `webpack.config.js` 代码 275 | 276 | ```js 277 | module.exports = { 278 | // ... 279 | module: { 280 | rules: [ 281 | { 282 | test: /\.css$/, 283 | use: ['style-loader', 284 | { 285 | loader: 'css-loader', 286 | options: { 287 | modules: true 288 | } 289 | } 290 | ] 291 | }, 292 | ] 293 | } 294 | } 295 | ``` 296 | 297 | 运行下 `npm run start`,然后刷新页面,可以发现图片被正确的加上了边框,现在我们来看一下 HTML 的文件结构 298 | 299 | ![](https://user-gold-cdn.xitu.io/2017/9/15/aa11101b8e22e4dd6b8c6432cfa26e03) 300 | 301 | 从上图可以看到,我们在 `addImage.css` 文件中写的代码被加入到了 `style` 标签中,并且因为我们开启了 CSS 模块化的选项,所以 `.test` 被转成了唯一的哈希值,这样就解决了 CSS 的变量名重复问题。 302 | 303 | 但是将 CSS 代码整合进 JS 文件也是有弊端的,大量的 CSS 代码会造成 JS 文件的大小变大,操作 DOM 也会造成性能上的问题,所以接下来我们将使用 `extract-text-webpack-plugin` 插件将 CSS 文件打包为一个单独文件 304 | 305 | 首先安装 `npm i --save-dev extract-text-webpack-plugin` 306 | 307 | 然后修改 `webpack.config.js` 代码 308 | 309 | ```js 310 | const ExtractTextPlugin = require("extract-text-webpack-plugin") 311 | 312 | module.exports = { 313 | // .... 314 | module: { 315 | rules: [ 316 | { 317 | test: /\.css$/, 318 | // 写法和之前基本一致 319 | loader: ExtractTextPlugin.extract({ 320 | // 必须这样写,否则会报错 321 | fallback: 'style-loader', 322 | use: [{ 323 | loader: 'css-loader', 324 | options: { 325 | modules: true 326 | } 327 | }] 328 | }) 329 | ] 330 | } 331 | ] 332 | }, 333 | // 插件列表 334 | plugins: [ 335 | // 输出的文件路径 336 | new ExtractTextPlugin("css/[name].[hash].css") 337 | ] 338 | } 339 | ``` 340 | 341 | 运行下 `npm run start`,可以发现 CSS 文件被单独打包出来了 342 | ![](https://user-gold-cdn.xitu.io/2017/9/15/343a4f4e729dc87f0ba67a652128dea3) 343 | 344 | 但是这时候刷新页面会发现图片的边框消失了,那是因为我们的 HTML 文件没有引用新的 CSS 文件,所以这里需要我们手动引入下,在下面的章节我们会通过插件的方式自动引入新的文件。 345 | 346 | 接下来,会用一个项目来继续我们的 webpack 学习,在这之前,先 clone 一下项目。该项目原地址是 [这里](https://github.com/StephenGrider/WebpackProject),因为使用的 webpack 版本太低,并且依赖的库也有点问题,故我将项目拷贝了过来并修改了几个库的版本号。 347 | 348 | 请依次按照以下代码操作 349 | 350 | ``` 351 | git clone https://github.com/KieSun/webpack-demo.git 352 | cd webpack-demo/project 353 | // 切换到 0.1 标签上并创建一个新分支 354 | git checkout -b demo 0.1 355 | // 查看分支是否为 demo,没问题的话就可以进行下一步 356 | cd project && npm i 357 | ``` 358 | 359 | ## 如何在项目中使用 webpack 360 | 361 | 项目中已经配置了很简单的 babel 和 webpack,直接运行 `npm run build` 即可 362 | 363 | ![](https://user-gold-cdn.xitu.io/2017/9/15/95957c4402c35a8458ed848d1d193513) 364 | 365 | 这时候你会发现这个 bundle.js 居然有这么大,这肯定是不能接受的,所以接下来章节的主要目的就是将单个文件拆分为多个文件,优化项目。 366 | 367 | #### 分离代码 368 | 369 | 先让我们考虑下缓存机制。对于代码中依赖的库很少会去主动升级版本,但是我们自己的代码却每时每刻都在变更,所以我们可以考虑将依赖的库和自己的代码分割开来,这样用户在下一次使用应用时就可以尽量避免重复下载没有变更的代码,那么既然要将依赖代码提取出来,我们需要变更下入口和出口的部分代码。 370 | 371 | ```js 372 | // 这是 packet.json 中 dependencies 下的 373 | const VENOR = ["faker", 374 | "lodash", 375 | "react", 376 | "react-dom", 377 | "react-input-range", 378 | "react-redux", 379 | "redux", 380 | "redux-form", 381 | "redux-thunk" 382 | ] 383 | 384 | module.exports = { 385 | // 之前我们都是使用了单文件入口 386 | // entry 同时也支持多文件入口,现在我们有两个入口 387 | // 一个是我们自己的代码,一个是依赖库的代码 388 | entry: { 389 | // bundle 和 vendor 都是自己随便取名的,会映射到 [name] 中 390 | bundle: './src/index.js', 391 | vendor: VENOR 392 | }, 393 | output: { 394 | path: path.join(__dirname, 'dist'), 395 | filename: '[name].js' 396 | }, 397 | // ... 398 | } 399 | ``` 400 | 401 | 现在我们 build 一下,看看是否有惊喜出现 402 | ![](https://user-gold-cdn.xitu.io/2017/9/16/370471eb63feeaa72e86415e396141e3) 403 | ![](https://user-gold-cdn.xitu.io/2017/9/16/48be1794942ddaf2f030bbb3a09de61a) 404 | 405 | 真的有惊喜。。为什么 bundle 文件大小压根没变。这是因为 bundle 中也引入了依赖库的代码,刚才的步骤并没有抽取 bundle 中引入的代码,接下来让我们学习如何将共同的代码抽取出来。 406 | 407 | #### 抽取共同代码 408 | 409 | 在这小节我们使用 webpack 自带的插件 `CommonsChunkPlugin`。 410 | 411 | ```js 412 | module.exports = { 413 | //... 414 | output: { 415 | path: path.join(__dirname, 'dist'), 416 | // 既然我们希望缓存生效,就应该每次在更改代码以后修改文件名 417 | // [chunkhash]会自动根据文件是否更改而更换哈希 418 | filename: '[name].[chunkhash].js' 419 | }, 420 | plugins: [ 421 | new webpack.optimize.CommonsChunkPlugin({ 422 | // vendor 的意义和之前相同 423 | // manifest文件是将每次打包都会更改的东西单独提取出来,保证没有更改的代码无需重新打包,这样可以加快打包速度 424 | names: ['vendor', 'manifest'], 425 | // 配合 manifest 文件使用 426 | minChunks: Infinity 427 | }) 428 | ] 429 | }; 430 | ``` 431 | 432 | 当我们重新 build 以后,会发现 bundle 文件很明显的减小了体积 433 | ![](https://user-gold-cdn.xitu.io/2017/9/16/0a0617d1f53173638cf9da06769bea27) 434 | 435 | 但是我们使用哈希来保证缓存的同时会发现每次 build 都会生成不一样的文件,这时候我们引入另一个插件来帮助我们删除不需要的文件。 436 | 437 | ``` 438 | npm install --save-dev clean-webpack-plugin 439 | ``` 440 | 441 | 然后修改配置文件 442 | ```js 443 | module.exports = { 444 | //... 445 | plugins: [ 446 | // 只删除 dist 文件夹下的 bundle 和 manifest 文件 447 | new CleanWebpackPlugin(['dist/bundle.*.js','dist/manifest.*.js'], { 448 | // 打印 log 449 | verbose: true, 450 | // 删除文件 451 | dry: false 452 | }), 453 | ] 454 | }; 455 | ``` 456 | 457 | 然后 build 的时候会发现以上文件被删除了。 458 | 459 | 因为我们现在将文件已经打包成三个 JS 了,以后也许会更多,每次新增 JS 文件我们都需要手动在 HTML 中新增标签,现在我们可以通过一个插件来自动完成这个功能。 460 | 461 | ``` 462 | npm install html-webpack-plugin --save-dev 463 | ``` 464 | 465 | 然后修改配置文件 466 | ```js 467 | module.exports = { 468 | //... 469 | plugins: [ 470 | // 我们这里将之前的 HTML 文件当做模板 471 | // 注意在之前 HTML 文件中请务必删除之前引入的 JS 文件 472 | new HtmlWebpackPlugin({ 473 | template: 'index.html' 474 | }) 475 | ] 476 | }; 477 | ``` 478 | 479 | 执行 build 操作会发现同时生成了 HTML 文件,并且已经自动引入了 JS 文件 480 | ![](https://user-gold-cdn.xitu.io/2017/9/17/f8a5991239c5aa45f845d1d9d4afe05d) 481 | 482 | #### 按需加载代码 483 | 484 | 在这一小节我们将学习如何按需加载代码,在这之前的 vendor 入口我发现忘记加入 router 这个库了,大家可以加入这个库并且重新 build 下,会发现 bundle 只有不到 300KB 了。 485 | 486 | 现在我们的 bundle 文件包含了我们全部的自己代码。但是当用户访问我们的首页时,其实我们根本无需让用户加载除了首页以外的代码,这个优化我们可以通过路由的异步加载来完成。 487 | 488 | 现在修改 `src/router.js` 489 | 490 | ```js 491 | // 注意在最新版的 V4路由版本中,更改了按需加载的方式,如果安装了 V4版,可以自行前往官网学习 492 | import React from 'react'; 493 | import { Router, Route, IndexRoute, hashHistory } from 'react-router'; 494 | 495 | import Home from './components/Home'; 496 | import ArtistMain from './components/artists/ArtistMain'; 497 | 498 | const rootRoute = { 499 | component: Home, 500 | path: '/', 501 | indexRoute: { component: ArtistMain }, 502 | childRoutes: [ 503 | { 504 | path: 'artists/new', 505 | getComponent(location, cb) { 506 | System.import('./components/artists/ArtistCreate') 507 | .then(module => cb(null, module.default)) 508 | } 509 | }, 510 | { 511 | path: 'artists/:id/edit', 512 | getComponent(location, cb) { 513 | System.import('./components/artists/ArtistEdit') 514 | .then(module => cb(null, module.default)) 515 | } 516 | }, 517 | { 518 | path: 'artists/:id', 519 | getComponent(location, cb) { 520 | System.import('./components/artists/ArtistDetail') 521 | .then(module => cb(null, module.default)) 522 | } 523 | } 524 | ] 525 | } 526 | 527 | const Routes = () => { 528 | return ( 529 | 530 | ); 531 | }; 532 | 533 | export default Routes; 534 | ``` 535 | 536 | 然后执行 build 命令,可以发现我们的 bundle 文件又瘦身了,并且新增了几个文件 537 | 538 | ![](https://user-gold-cdn.xitu.io/2017/9/17/8e48f667bb4313db100105b7dc34cdb2) 539 | 540 | 将 HTML 文件在浏览器中打开,当点击路由跳转时,可以在开发者工具中的 Network 一栏中看到加载了一个 JS 文件。 541 | 542 | 首页 543 | ![](https://user-gold-cdn.xitu.io/2017/9/17/a5a9c677fc770d8a99bbed9e79fdd730) 544 | 545 | 点击右上角 Random Artist 以后 546 | ![](https://user-gold-cdn.xitu.io/2017/9/17/93e7c9d86eeb7076b6f5e7434b0012e9) 547 | 548 | #### 自动刷新 549 | 550 | 每次更新代码都需要执行依次 build,并且还要等上一会很麻烦,这一小节介绍如何使用自动刷新的功能。 551 | 552 | 首先安装插件 553 | ``` 554 | npm i --save-dev webpack-dev-server 555 | ``` 556 | 557 | 然后修改 packet.json 文件 558 | 559 | ```js 560 | "scripts": { 561 | "build": "webpack", 562 | "dev": "webpack-dev-server --open" 563 | }, 564 | ``` 565 | 566 | 现在直接执行 `npm run dev` 可以发现浏览器自动打开了一个空的页面,并且在命令行中也多了新的输出 567 | ![](https://user-gold-cdn.xitu.io/2017/9/17/43f5b70bd82152ce21379120dfab8d71) 568 | 569 | 等待编译完成以后,修改 JS 或者 CSS 文件,可以发现 webpack 自动帮我们完成了编译,并且只更新了需要更新的代码 570 | ![](https://user-gold-cdn.xitu.io/2017/9/17/7a5769363d0782abc59902d370c52472) 571 | 572 | 但是每次重新刷新页面对于 debug 来说很不友好,这时候就需要用到模块热替换了。但是因为项目中使用了 React,并且 Vue 或者其他框架都有自己的一套 hot-loader,所以这里就略过了,有兴趣的可以自己学习下。 573 | 574 | #### 生成生产环境代码 575 | 576 | 现在我们可以将之前所学和一些新加的插件整合在一起,build 生产环境代码。 577 | 578 | ``` 579 | npm i --save-dev url-loader optimize-css-assets-webpack-plugin file-loader extract-text-webpack-plugin 580 | ``` 581 | 582 | 修改 webpack 配置 583 | 584 | ```js 585 | var webpack = require('webpack'); 586 | var path = require('path'); 587 | var HtmlWebpackPlugin = require('html-webpack-plugin') 588 | var CleanWebpackPlugin = require('clean-webpack-plugin') 589 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 590 | var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 591 | 592 | const VENOR = ["faker", 593 | "lodash", 594 | "react", 595 | "react-dom", 596 | "react-input-range", 597 | "react-redux", 598 | "redux", 599 | "redux-form", 600 | "redux-thunk", 601 | "react-router" 602 | ] 603 | 604 | module.exports = { 605 | entry: { 606 | bundle: './src/index.js', 607 | vendor: VENOR 608 | }, 609 | // 如果想修改 webpack-dev-server 配置,在这个对象里面修改 610 | devServer: { 611 | port: 8081 612 | }, 613 | output: { 614 | path: path.join(__dirname, 'dist'), 615 | filename: '[name].[chunkhash].js' 616 | }, 617 | module: { 618 | rules: [{ 619 | test: /\.js$/, 620 | use: 'babel-loader' 621 | }, 622 | { 623 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 624 | use: [{ 625 | loader: 'url-loader', 626 | options: { 627 | limit: 10000, 628 | name: 'images/[name].[hash:7].[ext]' 629 | } 630 | }] 631 | }, 632 | { 633 | test: /\.css$/, 634 | loader: ExtractTextPlugin.extract({ 635 | fallback: 'style-loader', 636 | use: [{ 637 | // 这边其实还可以使用 postcss 先处理下 CSS 代码 638 | loader: 'css-loader' 639 | }] 640 | }) 641 | }, 642 | ] 643 | }, 644 | plugins: [ 645 | new webpack.optimize.CommonsChunkPlugin({ 646 | name: ['vendor', 'manifest'], 647 | minChunks: Infinity 648 | }), 649 | new CleanWebpackPlugin(['dist/*.js'], { 650 | verbose: true, 651 | dry: false 652 | }), 653 | new HtmlWebpackPlugin({ 654 | template: 'index.html' 655 | }), 656 | // 生成全局变量 657 | new webpack.DefinePlugin({ 658 | "process.env.NODE_ENV": JSON.stringify("process.env.NODE_ENV") 659 | }), 660 | // 分离 CSS 代码 661 | new ExtractTextPlugin("css/[name].[contenthash].css"), 662 | // 压缩提取出的 CSS,并解决ExtractTextPlugin分离出的 JS 重复问题 663 | new OptimizeCSSPlugin({ 664 | cssProcessorOptions: { 665 | safe: true 666 | } 667 | }), 668 | // 压缩 JS 代码 669 | new webpack.optimize.UglifyJsPlugin({ 670 | compress: { 671 | warnings: false 672 | } 673 | }) 674 | ] 675 | }; 676 | 677 | ``` 678 | 679 | 修改 packet.json 文件 680 | 681 | ```js 682 | "scripts": { 683 | "build": "NODE_ENV=production webpack -p", 684 | "dev": "webpack-dev-server --open" 685 | } 686 | ``` 687 | 688 | 执行 `npm run build` 689 | ![](https://user-gold-cdn.xitu.io/2017/9/17/ff8acf374946ee6db118117f22ec48f5) 690 | 691 | 可以看到我们在经历了这么多步以后,将 bundle 缩小到了只有 27.1KB,像 vendor 这种常用的库我们一般可以使用 CDN 的方式外链进来。 692 | 693 | ## 补充 694 | 695 | webpack 配置上有些实用的小点在上文没有提到,统一在这里提一下。 696 | 697 | ```js 698 | module.exports = { 699 | resolve: { 700 | // 文件扩展名,写明以后就不需要每个文件写后缀 701 | extensions: ['.js', '.css', '.json'], 702 | // 路径别名,比如这里可以使用 css 指向 static/css 路径 703 | alias: { 704 | '@': resolve('src'), 705 | 'css': resolve('static/css') 706 | } 707 | }, 708 | // 生成 source-map,用于打断点,这里有好几个选项 709 | devtool: '#cheap-module-eval-source-map', 710 | } 711 | ``` 712 | 713 | ## 后记 714 | 715 | 如果你是跟着本文一个个步骤敲下来的,那么大部分的 webpack 配置你应该都是可以看懂了,并且自己应该也知道如何去配置。谢谢大家看到这里,[这是本项目对应的仓库](https://github.com/KieSun/webpack-demo),每个小节基本都对应了一次 commit。 716 | 717 | 文章较长,有错误也难免,如果你发现了任何问题或者我有任何表述的不明白的地方,都可以留言给我。 718 | -------------------------------------------------------------------------------- /app/addImage.js: -------------------------------------------------------------------------------- 1 | import '../styles/addImage.css' 2 | 3 | let smallImg = document.createElement('img') 4 | smallImg.src = require('../images/small.jpeg') 5 | document.body.appendChild(smallImg) 6 | 7 | // let bigImg = document.createElement('img') 8 | // bigImg.src = require('../images/big.jpeg') 9 | // document.body.appendChild(bigImg) -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | import sum from './sum' 2 | import './addImage' 3 | 4 | console.log(sum(1, 2)) -------------------------------------------------------------------------------- /app/sum.js: -------------------------------------------------------------------------------- 1 | export default (a, b) => { 2 | return a + b 3 | } -------------------------------------------------------------------------------- /build/bundle.js: -------------------------------------------------------------------------------- 1 | /******/ (function(modules) { // webpackBootstrap 2 | /******/ // The module cache 3 | /******/ var installedModules = {}; 4 | /******/ 5 | /******/ // The require function 6 | /******/ function __webpack_require__(moduleId) { 7 | /******/ 8 | /******/ // Check if module is in cache 9 | /******/ if(installedModules[moduleId]) { 10 | /******/ return installedModules[moduleId].exports; 11 | /******/ } 12 | /******/ // Create a new module (and put it into the cache) 13 | /******/ var module = installedModules[moduleId] = { 14 | /******/ i: moduleId, 15 | /******/ l: false, 16 | /******/ exports: {} 17 | /******/ }; 18 | /******/ 19 | /******/ // Execute the module function 20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 21 | /******/ 22 | /******/ // Flag the module as loaded 23 | /******/ module.l = true; 24 | /******/ 25 | /******/ // Return the exports of the module 26 | /******/ return module.exports; 27 | /******/ } 28 | /******/ 29 | /******/ 30 | /******/ // expose the modules object (__webpack_modules__) 31 | /******/ __webpack_require__.m = modules; 32 | /******/ 33 | /******/ // expose the module cache 34 | /******/ __webpack_require__.c = installedModules; 35 | /******/ 36 | /******/ // define getter function for harmony exports 37 | /******/ __webpack_require__.d = function(exports, name, getter) { 38 | /******/ if(!__webpack_require__.o(exports, name)) { 39 | /******/ Object.defineProperty(exports, name, { 40 | /******/ configurable: false, 41 | /******/ enumerable: true, 42 | /******/ get: getter 43 | /******/ }); 44 | /******/ } 45 | /******/ }; 46 | /******/ 47 | /******/ // getDefaultExport function for compatibility with non-harmony modules 48 | /******/ __webpack_require__.n = function(module) { 49 | /******/ var getter = module && module.__esModule ? 50 | /******/ function getDefault() { return module['default']; } : 51 | /******/ function getModuleExports() { return module; }; 52 | /******/ __webpack_require__.d(getter, 'a', getter); 53 | /******/ return getter; 54 | /******/ }; 55 | /******/ 56 | /******/ // Object.prototype.hasOwnProperty.call 57 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 58 | /******/ 59 | /******/ // __webpack_public_path__ 60 | /******/ __webpack_require__.p = "build/"; 61 | /******/ 62 | /******/ // Load entry module and return exports 63 | /******/ return __webpack_require__(__webpack_require__.s = 0); 64 | /******/ }) 65 | /************************************************************************/ 66 | /******/ ([ 67 | /* 0 */ 68 | /***/ (function(module, exports, __webpack_require__) { 69 | 70 | "use strict"; 71 | 72 | 73 | var _sum = __webpack_require__(1); 74 | 75 | var _sum2 = _interopRequireDefault(_sum); 76 | 77 | __webpack_require__(2); 78 | 79 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 80 | 81 | console.log((0, _sum2.default)(1, 2)); 82 | 83 | /***/ }), 84 | /* 1 */ 85 | /***/ (function(module, exports, __webpack_require__) { 86 | 87 | "use strict"; 88 | 89 | 90 | Object.defineProperty(exports, "__esModule", { 91 | value: true 92 | }); 93 | 94 | exports.default = function (a, b) { 95 | return a + b; 96 | }; 97 | 98 | /***/ }), 99 | /* 2 */ 100 | /***/ (function(module, exports, __webpack_require__) { 101 | 102 | "use strict"; 103 | 104 | 105 | __webpack_require__(3); 106 | 107 | var smallImg = document.createElement('img'); 108 | smallImg.src = __webpack_require__(4); 109 | document.body.appendChild(smallImg); 110 | 111 | // let bigImg = document.createElement('img') 112 | // bigImg.src = require('../images/big.jpeg') 113 | // document.body.appendChild(bigImg) 114 | 115 | /***/ }), 116 | /* 3 */ 117 | /***/ (function(module, exports) { 118 | 119 | // removed by extract-text-webpack-plugin 120 | module.exports = {"test":"_2-AxcSaQBPXGeJRLE8Ocue"}; 121 | 122 | /***/ }), 123 | /* 4 */ 124 | /***/ (function(module, exports) { 125 | 126 | module.exports = "data:image/jpeg;base64,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" 127 | 128 | /***/ }) 129 | /******/ ]); -------------------------------------------------------------------------------- /build/css/main.0c219f8baaf2304c8f9f.css: -------------------------------------------------------------------------------- 1 | img { 2 | border: 5px black solid; 3 | } 4 | ._2-AxcSaQBPXGeJRLE8Ocue { 5 | border: 5px black solid; 6 | } -------------------------------------------------------------------------------- /build/images/big.d364620e5312f4427a916c11c6065311.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KieSun/webpack-demo/7cc49a6bc6a7f308d5a08d8f37383de793f712f5/build/images/big.d364620e5312f4427a916c11c6065311.jpeg -------------------------------------------------------------------------------- /images/big.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KieSun/webpack-demo/7cc49a6bc6a7f308d5a08d8f37383de793f712f5/images/big.jpeg -------------------------------------------------------------------------------- /images/small.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KieSun/webpack-demo/7cc49a6bc6a7f308d5a08d8f37383de793f712f5/images/small.jpeg -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Document 5 | 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-demo", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "dependencies": { 7 | "webpack": "^3.5.6" 8 | }, 9 | "devDependencies": { 10 | "babel-core": "^6.26.0", 11 | "babel-loader": "^7.1.2", 12 | "babel-preset-env": "^1.6.0", 13 | "css-loader": "^0.28.7", 14 | "extract-text-webpack-plugin": "^3.0.0", 15 | "file-loader": "^0.11.2", 16 | "style-loader": "^0.18.2", 17 | "url-loader": "^0.5.9", 18 | "webpack": "^3.5.6" 19 | }, 20 | "scripts": { 21 | "start": "webpack" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/KieSun/webpack-demo.git" 26 | }, 27 | "author": "", 28 | "license": "ISC", 29 | "bugs": { 30 | "url": "https://github.com/KieSun/webpack-demo/issues" 31 | }, 32 | "homepage": "https://github.com/KieSun/webpack-demo#readme" 33 | } 34 | -------------------------------------------------------------------------------- /project/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["babel-preset-env", "react"] 3 | } -------------------------------------------------------------------------------- /project/database/constants.js: -------------------------------------------------------------------------------- 1 | export const GENRES = [ 2 | 'Acceptable Country', 3 | 'Acceptable Emo', 4 | 'Acceptable Pop', 5 | 'Acceptable Pop-Punk', 6 | 'Alt-Country', 7 | 'Alt-Rap', 8 | 'Bloghaus', 9 | 'Blog Rap', 10 | 'Blog Rock', 11 | 'Cold Wave', 12 | 'Cool Jazz', 13 | 'Digital Punk', 14 | 'Doom Metal', 15 | 'Freak Folk', 16 | 'Garage Rock', 17 | 'Hypnagogic Pop', 18 | 'Noise Pop', 19 | 'Power Electronics', 20 | 'Serialism', 21 | 'Witch House', 22 | 'Ye Olde Timey Rock And Roll Music of Indeterminate Hipster Variety' 23 | ]; 24 | -------------------------------------------------------------------------------- /project/database/queries/CreateArtist.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const Artist = require('../seeds/artist'); 3 | const db = require('./db'); 4 | 5 | /** 6 | * Create a single artist in the artist collection. 7 | * @param {object} artistProps - Object containing a name, age, yearsActive, and genre 8 | * @return {promise} A promise that resolves with the Artist that was created 9 | */ 10 | module.exports = (artistProps) => { 11 | const artist = _.extend({}, 12 | artistProps, 13 | { 14 | _id: _.uniqueId(), 15 | age: parseInt(artistProps.age) || 20, 16 | yearsActive: parseInt(artistProps.yearsActive) || 5 17 | } 18 | ); 19 | db.push(artist); 20 | 21 | return new Promise((resolve, reject) => { 22 | resolve(artist); 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /project/database/queries/DeleteArtist.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const Artist = require('../seeds/artist'); 3 | const db = require('./db'); 4 | 5 | /** 6 | * Deletes a single artist from the Artists collection 7 | * @param {string} _id - The ID of the artist to delete. 8 | * @return {promise} A promise that resolves when the record is deleted 9 | */ 10 | module.exports = (_id) => { 11 | _.each(db, (artist, index) => { 12 | if(artist && artist._id === _id) { 13 | db.splice(index, 1); 14 | } 15 | }); 16 | 17 | return new Promise((resolve, reject) => resolve()); 18 | }; 19 | -------------------------------------------------------------------------------- /project/database/queries/EditArtist.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const Artist = require('../seeds/artist'); 3 | const db = require('./db'); 4 | 5 | /** 6 | * Edits a single artist in the Artists collection 7 | * @param {string} _id - The ID of the artist to edit. 8 | * @param {object} artistProps - An object with a name, age, yearsActive, and genre 9 | * @return {promise} A promise that resolves when the record is edited 10 | */ 11 | module.exports = (_id, artistProps) => { 12 | const artist = _.find(db, a => a._id === _id); 13 | _.extend(artist, artistProps); 14 | 15 | return new Promise((resolve, reject) => { 16 | resolve(); 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /project/database/queries/FindArtist.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const Artist = require('../seeds/artist'); 3 | const db = require('./db'); 4 | 5 | /** 6 | * Finds a single artist in the artist collection. 7 | * @param {string} _id - The ID of the record to find. 8 | * @return {promise} A promise that resolves with the Artist that matches the id 9 | */ 10 | module.exports = (_id) => { 11 | const artist = _.find(db, a => a._id === _id); 12 | 13 | return new Promise((resolve, reject) => { 14 | resolve(artist); 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /project/database/queries/GetAgeRange.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const Artist = require('../seeds/artist'); 3 | const db = require('./db'); 4 | 5 | /** 6 | * Finds the lowest and highest age of artists in the Artist collection 7 | * @return {promise} A promise that resolves with an object 8 | * containing the min and max ages, like { min: 16, max: 45 }. 9 | */ 10 | module.exports = () => { 11 | return new Promise((resolve, reject) => { 12 | const range = { 13 | max: _.maxBy(db, a => a.age).age, 14 | min: _.minBy(db, a => a.age).age, 15 | }; 16 | 17 | resolve(range); 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /project/database/queries/GetYearsActiveRange.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const Artist = require('../seeds/artist'); 3 | const db = require('./db'); 4 | 5 | /** 6 | * Finds the lowest and highest yearsActive of artists in the Artist collection 7 | * @return {promise} A promise that resolves with an object 8 | * containing the min and max yearsActive, like { min: 0, max: 14 }. 9 | */ 10 | module.exports = () => { 11 | return new Promise((resolve, reject) => { 12 | const range = { 13 | max: _.maxBy(db, a => a.yearsActive).yearsActive, 14 | min: _.minBy(db, a => a.yearsActive).yearsActive, 15 | }; 16 | 17 | resolve(range); 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /project/database/queries/SearchArtists.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const Artist = require('../seeds/artist'); 3 | const db = require('./db'); 4 | 5 | /** 6 | * Searches through the Artist collection 7 | * @param {object} criteria An object with a name, age, and yearsActive 8 | * @param {string} sortProperty The property to sort the results by 9 | * @param {integer} offset How many records to skip in the result set 10 | * @param {integer} limit How many records to return in the result set 11 | * @return {promise} A promise that resolves with the artists, count, offset, and limit 12 | */ 13 | module.exports = (_criteria, sortProperty, offset = 0, limit = 20) => { 14 | const criteria = _.extend({ 15 | age: { min: 0, max: 100 }, 16 | yearsActive: { min: 0, max: 100 }, 17 | name: '' 18 | }, _criteria); 19 | 20 | const artists = _.chain(db) 21 | .filter(a => _.includes(_.lowerCase(a.name), _.lowerCase(criteria.name))) 22 | .filter(a => a.age > criteria.age.min && a.age < criteria.age.max) 23 | .filter(a => a.yearsActive > criteria.yearsActive.min && a.yearsActive < criteria.yearsActive.max) 24 | .sortBy(a => a[sortProperty]) 25 | .value() 26 | 27 | return new Promise((resolve, reject) => { 28 | resolve(artists); 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /project/database/queries/SetNotRetired.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const Artist = require('../seeds/artist'); 3 | const db = require('./db'); 4 | 5 | /** 6 | * Sets a group of Artists as not retired 7 | * @param {array} _ids - An array of the _id's of of artists to update 8 | * @return {promise} A promise that resolves after the update 9 | */ 10 | module.exports = (_ids) => { 11 | return new Promise((resolve, reject) => { 12 | const artists = _.chain(_ids) 13 | .map(_id => _.find(db, a => a._id === _id)) 14 | .tap(ids => console.log(ids)) 15 | .compact() 16 | .each(a => a.retired = false) 17 | .value(); 18 | 19 | resolve(); 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /project/database/queries/SetRetired.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const Artist = require('../seeds/artist'); 3 | const db = require('./db'); 4 | 5 | /** 6 | * Sets a group of Artists as retired 7 | * @param {array} _ids - An array of the _id's of of artists to update 8 | * @return {promise} A promise that resolves after the update 9 | */ 10 | module.exports = (_ids) => { 11 | return new Promise((resolve, reject) => { 12 | const artists = _.chain(_ids) 13 | .map(_id => _.find(db, a => a._id === _id)) 14 | .compact() 15 | .each(a => a.retired = true) 16 | .value(); 17 | 18 | resolve(artists); 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /project/database/queries/db.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const Artist = require('../seeds/artist'); 3 | 4 | const artists = _.times(20, () => Artist()); 5 | 6 | module.exports = artists; 7 | -------------------------------------------------------------------------------- /project/database/seeds/artist.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import faker from 'faker'; 3 | import { GENRES } from '../constants'; 4 | 5 | module.exports = function() { 6 | return { 7 | _id: _.uniqueId(), 8 | name: faker.name.findName(), 9 | age: randomBetween(15, 45), 10 | yearsActive: randomBetween(0, 15), 11 | image: faker.image.avatar(), 12 | genre: getGenre(), 13 | website: faker.internet.url(), 14 | netWorth: randomBetween(0, 5000000), 15 | labelName: faker.company.companyName(), 16 | retired: faker.random.boolean(), 17 | albums: getAlbums() 18 | }; 19 | } 20 | 21 | function getAlbums() { 22 | return _.times(randomBetween(0, 5), () => { 23 | const copiesSold = randomBetween(0, 1000000); 24 | 25 | return { 26 | title: _.capitalize(faker.random.words()), 27 | date: faker.date.past(), 28 | copiesSold, 29 | numberTracks: randomBetween(1, 20), 30 | image: getAlbumImage(), 31 | revenue: copiesSold * 12.99 32 | }; 33 | }); 34 | } 35 | 36 | function getAlbumImage() { 37 | const types = _.keys(faker.image); 38 | const method = randomEntry(types); 39 | 40 | return faker.image[method](); 41 | } 42 | 43 | function getGenre() { 44 | return randomEntry(GENRES); 45 | } 46 | 47 | function randomEntry(array) { 48 | return array[~~(Math.random() * array.length)]; 49 | } 50 | 51 | function randomBetween(min, max) { 52 | return ~~(Math.random() * (max-min)) + min; 53 | } 54 | -------------------------------------------------------------------------------- /project/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Upstar Music 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /project/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "upstar_music", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "NODE_ENV=production webpack -p", 8 | "dev": "webpack-dev-server --open" 9 | }, 10 | "repository": "https://github.com/StephenGrider/WebpackProject", 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "faker": "^4.1.0", 15 | "lodash": "^4.17.2", 16 | "react": "15.4.1", 17 | "react-dom": "15.4.1", 18 | "react-input-range": "^0.9.2", 19 | "react-redux": "^4.4.6", 20 | "react-router": "^3.0.0", 21 | "redux": "^3.6.0", 22 | "redux-form": "^6.3.2", 23 | "redux-thunk": "^2.1.0" 24 | }, 25 | "devDependencies": { 26 | "babel-core": "^6.26.0", 27 | "babel-loader": "^7.1.2", 28 | "babel-preset-env": "^1.6.0", 29 | "babel-preset-react": "^6.16.0", 30 | "clean-webpack-plugin": "^0.1.16", 31 | "css-loader": "^0.28.7", 32 | "extract-text-webpack-plugin": "^3.0.0", 33 | "file-loader": "^0.11.2", 34 | "html-webpack-plugin": "^2.30.1", 35 | "optimize-css-assets-webpack-plugin": "^3.2.0", 36 | "style-loader": "^0.18.2", 37 | "url-loader": "^0.5.9", 38 | "webpack": "^3.5.6", 39 | "webpack-dev-server": "^2.8.2" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /project/src/actions/index.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { hashHistory } from 'react-router'; 3 | import { 4 | SET_AGE_RANGE, 5 | SET_YEARS_ACTIVE_RANGE, 6 | SEARCH_ARTISTS, 7 | FIND_ARTIST, 8 | RESET_ARTIST, 9 | CREATE_ERROR, 10 | CLEAR_ERROR, 11 | DESELECT_ARTIST, 12 | SELECT_ARTIST, 13 | RESET_SELECTION 14 | } from './types'; 15 | 16 | import GetAgeRange from '../../database/queries/GetAgeRange'; 17 | import GetYearsActiveRange from '../../database/queries/GetYearsActiveRange'; 18 | import SearchArtists from '../../database/queries/SearchArtists'; 19 | import FindArtist from '../../database/queries/FindArtist'; 20 | import CreateArtist from '../../database/queries/CreateArtist'; 21 | import EditArtist from '../../database/queries/EditArtist'; 22 | import DeleteArtist from '../../database/queries/DeleteArtist'; 23 | import SetRetired from '../../database/queries/SetRetired'; 24 | import SetNotRetired from '../../database/queries/SetNotRetired'; 25 | 26 | export const resetArtist = () => { 27 | return { type: RESET_ARTIST }; 28 | }; 29 | 30 | export const clearError = () => { 31 | return { type: CLEAR_ERROR }; 32 | }; 33 | 34 | export const selectArtist = id => { 35 | return { type: SELECT_ARTIST, payload: id }; 36 | }; 37 | 38 | export const deselectArtist = id => { 39 | return { type: DESELECT_ARTIST, payload: id }; 40 | }; 41 | 42 | export const setRetired = ids => (dispatch, getState) => 43 | SetRetiredProxy(ids.map(id => id.toString())) 44 | .then(() => dispatch({ type: RESET_SELECTION })) 45 | .then(() => refreshSearch(dispatch, getState)); 46 | 47 | export const setNotRetired = ids => (dispatch, getState) => 48 | SetNotRetiredProxy(ids.map(id => id.toString())) 49 | .then(() => dispatch({ type: RESET_SELECTION })) 50 | .then(() => refreshSearch(dispatch, getState)); 51 | 52 | export const setAgeRange = () => dispatch => 53 | GetAgeRangeProxy() 54 | .then(result => 55 | dispatch({ type: SET_AGE_RANGE, payload: result }) 56 | ); 57 | 58 | export const setYearsActiveRange = () => dispatch => 59 | GetYearsActiveRangeProxy() 60 | .then(result => 61 | dispatch({ type: SET_YEARS_ACTIVE_RANGE, payload: result }) 62 | ); 63 | 64 | export const searchArtists = (...criteria) => dispatch => 65 | SearchArtistsProxy(...criteria) 66 | .then((result = []) => 67 | dispatch({ type: SEARCH_ARTISTS, payload: result }) 68 | ); 69 | 70 | export const findArtist = id => dispatch => 71 | FindArtistProxy(id) 72 | .then(artist => 73 | dispatch({ type: FIND_ARTIST, payload: artist }) 74 | ); 75 | 76 | export const createArtist = props => dispatch => 77 | CreateArtistProxy(props) 78 | .then(artist => { 79 | hashHistory.push(`artists/${artist._id}`); 80 | }) 81 | .catch(error => { 82 | console.log(error); 83 | dispatch({ type: CREATE_ERROR, payload: error }); 84 | }); 85 | 86 | export const editArtist = (id, props) => dispatch => 87 | EditArtistProxy(id, props) 88 | .then(() => hashHistory.push(`artists/${id}`)) 89 | .catch(error => { 90 | console.log(error); 91 | dispatch({ type: CREATE_ERROR, payload: error }); 92 | }); 93 | 94 | export const deleteArtist = (id) => dispatch => 95 | DeleteArtistProxy(id) 96 | .then(() => hashHistory.push('/')) 97 | .catch(error => { 98 | console.log(error); 99 | dispatch({ type: CREATE_ERROR, payload: error }); 100 | }); 101 | 102 | 103 | // 104 | // Faux Proxies 105 | 106 | const GetAgeRangeProxy = (...args) => { 107 | const result = GetAgeRange(...args); 108 | if (!result || !result.then) { 109 | return new Promise(() => {}); 110 | } 111 | return result; 112 | }; 113 | 114 | const GetYearsActiveRangeProxy = (...args) => { 115 | const result = GetYearsActiveRange(...args); 116 | if (!result || !result.then) { 117 | return new Promise(() => {}); 118 | } 119 | return result; 120 | }; 121 | 122 | const SearchArtistsProxy = (criteria, offset, limit) => { 123 | const result = SearchArtists(_.omit(criteria, 'sort'), criteria.sort, offset, limit); 124 | if (!result || !result.then) { 125 | return new Promise(() => {}); 126 | } 127 | return result; 128 | }; 129 | 130 | const FindArtistProxy = (...args) => { 131 | const result = FindArtist(...args); 132 | if (!result || !result.then) { 133 | return new Promise(() => {}); 134 | } 135 | return result; 136 | }; 137 | 138 | const CreateArtistProxy = (...args) => { 139 | const result = CreateArtist(...args); 140 | if (!result || !result.then) { 141 | return new Promise(() => {}); 142 | } 143 | return result; 144 | }; 145 | 146 | const EditArtistProxy = (...args) => { 147 | const result = EditArtist(...args); 148 | if (!result || !result.then) { 149 | return new Promise(() => {}); 150 | } 151 | return result; 152 | }; 153 | 154 | const DeleteArtistProxy = (...args) => { 155 | const result = DeleteArtist(...args); 156 | if (!result || !result.then) { 157 | return new Promise(() => {}); 158 | } 159 | return result; 160 | }; 161 | 162 | const SetRetiredProxy = (_ids) => { 163 | const result = SetRetired(_ids); 164 | if (!result || !result.then) { 165 | return new Promise(() => {}); 166 | } 167 | return result; 168 | }; 169 | 170 | const SetNotRetiredProxy = (_ids) => { 171 | const result = SetNotRetired(_ids); 172 | if (!result || !result.then) { 173 | return new Promise(() => {}); 174 | } 175 | return result; 176 | }; 177 | 178 | // 179 | // Helpers 180 | 181 | const refreshSearch = (dispatch, getState) => { 182 | const { artists: { offset, limit } } = getState(); 183 | const criteria = getState().form.filters.values; 184 | 185 | dispatch(searchArtists(_.extend({}, { name: '' }, criteria), offset, limit)); 186 | }; 187 | -------------------------------------------------------------------------------- /project/src/actions/types.js: -------------------------------------------------------------------------------- 1 | export const SET_AGE_RANGE = 'set_age_range'; 2 | export const SET_YEARS_ACTIVE_RANGE = 'SET_YEARS_ACTIVE_RANGE'; 3 | export const SEARCH_ARTISTS = 'SEARCH_ARTISTS'; 4 | export const FIND_ARTIST = 'FIND_ARTIST'; 5 | export const RESET_ARTIST = 'RESET_ARTIST'; 6 | export const CREATE_ERROR = 'CREATE_ERROR'; 7 | export const CLEAR_ERROR = 'CLEAR_ERROR'; 8 | export const SELECT_ARTIST = 'SELECT_ARTIST'; 9 | export const DESELECT_ARTIST = 'DESELECT_ARTIST'; 10 | export const RESET_SELECTION = 'RESET_SELECTION'; 11 | -------------------------------------------------------------------------------- /project/src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router'; 3 | const db = require('../../database/queries/db'); 4 | 5 | class Header extends Component { 6 | constructor(props) { 7 | super(props); 8 | 9 | this.state = { id: null }; 10 | } 11 | 12 | componentWillMount() { 13 | this.setLink(); 14 | } 15 | 16 | setLink() { 17 | const index = _.random(0, db.length); 18 | this.setState({ id: index }); 19 | } 20 | 21 | render() { 22 | return ( 23 |
24 | 46 |
47 | ); 48 | } 49 | }; 50 | 51 | export default Header; 52 | -------------------------------------------------------------------------------- /project/src/components/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Header from './Header'; 3 | 4 | const Home = ({ children }) => { 5 | return ( 6 |
7 |
8 | {children} 9 |
10 | ); 11 | }; 12 | 13 | export default Home; 14 | -------------------------------------------------------------------------------- /project/src/components/artists/ArtistCreate.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { reduxForm, Field } from 'redux-form'; 4 | import * as actions from '../../actions'; 5 | 6 | class ArtistCreate extends Component { 7 | componentWillUnmount() { 8 | this.props.clearError(); 9 | } 10 | 11 | onSubmit(formProps) { 12 | this.props.createArtist(formProps); 13 | } 14 | 15 | render() { 16 | const { handleSubmit } = this.props; 17 | 18 | return ( 19 |
20 |
21 | 22 |
23 |
24 | 25 |
26 |
27 | 28 |
29 |
30 | 31 |
32 |
33 | {this.props.errorMessage} 34 |
35 | 36 |
37 | ); 38 | } 39 | } 40 | 41 | const mapStateToProps = (state) => { 42 | return { 43 | errorMessage: state.errors 44 | }; 45 | }; 46 | 47 | export default connect(mapStateToProps, actions)(reduxForm({ 48 | form: 'create' 49 | })(ArtistCreate)); 50 | -------------------------------------------------------------------------------- /project/src/components/artists/ArtistDetail.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Link } from 'react-router'; 4 | import * as actions from '../../actions'; 5 | 6 | class ArtistDetail extends Component { 7 | componentWillMount() { 8 | this.props.findArtist(this.props.params.id); 9 | } 10 | 11 | componentWillReceiveProps(nextProps) { 12 | if (nextProps.params.id !== this.props.params.id) { 13 | this.props.findArtist(nextProps.params.id); 14 | } 15 | } 16 | 17 | componentWillUnmount() { 18 | this.props.resetArtist(); 19 | } 20 | 21 | onDeleteClick() { 22 | this.props.deleteArtist(this.props.params.id); 23 | } 24 | 25 | renderAlbums() { 26 | const { albums } = this.props.artist; 27 | 28 | if (!albums || !albums.map) { return; } 29 | 30 | return albums.map(album => { 31 | return ( 32 |
33 |
34 | 35 | 36 |

{album.title}

37 |
38 |
39 |
40 |
41 |
{album.copiesSold}
42 | copies sold 43 |
44 |
45 |
{album.numberTracks}
46 | tracks 47 |
48 |
49 |
50 | ); 51 | }); 52 | } 53 | 54 | render() { 55 | if (!this.props.artist) { return
Todo: implement "FindArtist" query
; } 56 | 57 | const { artist: { name, age, genre, image, yearsActive, netWorth, labelName, _id } } = this.props; 58 | 59 | return ( 60 |
61 |
62 | Back 63 | Edit 64 | Delete 65 |
66 | 94 |
95 | ); 96 | } 97 | } 98 | 99 | const mapStateToProps = ({ artists }) => { 100 | return { artist: artists.artist }; 101 | }; 102 | 103 | export default connect(mapStateToProps, actions)(ArtistDetail); 104 | -------------------------------------------------------------------------------- /project/src/components/artists/ArtistEdit.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import * as actions from '../../actions'; 4 | 5 | class ArtistEdit extends Component { 6 | constructor(props) { 7 | super(props); 8 | 9 | this.state = {}; 10 | } 11 | 12 | componentWillMount() { 13 | this.props.findArtist(this.props.params.id); 14 | } 15 | 16 | componentWillReceiveProps({ artist }) { 17 | if (artist) { 18 | const { name, age, yearsActive, genre } = artist; 19 | 20 | this.setState({ name, age, yearsActive, genre }); 21 | } 22 | } 23 | 24 | componentWillUpdate(nextProps) { 25 | if (nextProps.params.id !== this.props.params.id) { 26 | this.props.findArtist(nextProps.params.id); 27 | } 28 | } 29 | 30 | componentWillUnmount() { 31 | this.props.clearError(); 32 | } 33 | 34 | onSubmit(event) { 35 | event.preventDefault(); 36 | event.stopPropagation(); 37 | 38 | this.props.editArtist(this.props.params.id, this.state); 39 | } 40 | 41 | render() { 42 | return ( 43 |
44 |
45 | this.setState({ name: e.target.value })} 48 | placeholder="Name" 49 | /> 50 |
51 |
52 | this.setState({ age: e.target.value })} 55 | placeholder="Age" 56 | /> 57 |
58 |
59 | this.setState({ yearsActive: e.target.value })} 62 | placeholder="Years Active" 63 | /> 64 |
65 |
66 | this.setState({ genre: e.target.value })} 69 | placeholder="Genre" 70 | /> 71 |
72 |
73 | {this.props.errorMessage} 74 |
75 | 76 |
77 | ); 78 | } 79 | } 80 | 81 | const mapStateToProps = (state) => { 82 | return { 83 | artist: state.artists.artist, 84 | errorMessage: state.errors 85 | }; 86 | }; 87 | 88 | export default connect(mapStateToProps, actions)(ArtistEdit); 89 | -------------------------------------------------------------------------------- /project/src/components/artists/ArtistFilter.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import React, { Component } from 'react'; 3 | import { Field, reduxForm } from 'redux-form'; 4 | import { connect } from 'react-redux'; 5 | import { Range } from '../filters'; 6 | import * as actions from '../../actions'; 7 | 8 | const TEXT_FIELDS = [ 9 | { label: 'Name', prop: 'name' } 10 | ]; 11 | 12 | class ArtistFilter extends Component { 13 | componentWillMount() { 14 | if (this.props.filters) { 15 | const criteria = _.extend({}, { name: '' }, this.props.filters); 16 | this.props.searchArtists(criteria); 17 | } else { 18 | this.props.searchArtists({ 19 | name: '', 20 | sort: 'name' 21 | }); 22 | } 23 | } 24 | 25 | componentDidMount() { 26 | this.props.setAgeRange(); 27 | this.props.setYearsActiveRange(); 28 | } 29 | 30 | handleSubmit(formProps) { 31 | const criteria = _.extend({ name: '' }, formProps); 32 | this.props.searchArtists(criteria); 33 | } 34 | 35 | renderInputs() { 36 | return TEXT_FIELDS.map(({ label, prop }) => 37 |
38 | 45 |
46 | ); 47 | } 48 | 49 | render() { 50 | const { handleSubmit } = this.props; 51 | 52 | return ( 53 |
54 |
55 |
56 |
57 | Search 58 |
59 | 60 | {this.renderInputs()} 61 | 62 |
63 | 71 |
72 | 73 |
74 | 82 |
83 | 84 |
85 | 86 | 87 | 88 | 89 | 90 | 91 |
92 | 93 |
94 | 95 |
96 |
97 |
98 |
99 | ); 100 | } 101 | } 102 | 103 | const mapStateToProps = (state) => { 104 | const { filterCriteria } = state; 105 | 106 | return { 107 | yearsActive: filterCriteria.yearsActive, 108 | ageRange: filterCriteria.age, 109 | filters: state.form.filters && state.form.filters.values 110 | }; 111 | }; 112 | 113 | export default connect(mapStateToProps, actions)(reduxForm({ 114 | destroyOnUnmount: false, 115 | form: 'filters', 116 | initialValues: { sort: 'name' } 117 | })(ArtistFilter)); 118 | -------------------------------------------------------------------------------- /project/src/components/artists/ArtistIndex.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import React, { Component } from 'react'; 3 | import { Link } from 'react-router'; 4 | import { connect } from 'react-redux'; 5 | import Paginator from './Paginator'; 6 | import * as actions from '../../actions'; 7 | 8 | class ArtistIndex extends Component { 9 | onChange(_id) { 10 | if (_.includes(this.props.selection, _id)) { 11 | this.props.deselectArtist(_id); 12 | } else { 13 | this.props.selectArtist(_id); 14 | } 15 | } 16 | 17 | renderList(artist) { 18 | const { _id } = artist; 19 | const classes = `collection-item avatar ${artist.retired && 'retired'}`; 20 | 21 | return ( 22 |
  • 23 |
    24 | this.onChange(_id)} 29 | /> 30 |
    32 | 33 |
    34 | 35 | {artist.name} 36 | 37 |

    38 | {artist.age} years old 39 |
    40 | {artist.albums ? artist.albums.length : 0} albums released 41 |

    42 |
    43 | 44 | > 45 | 46 |
  • 47 | ); 48 | } 49 | 50 | renderPaginator() { 51 | if (this.props.artists.all.length) { 52 | return ; 53 | } 54 | } 55 | 56 | renderEmptyCollection() { 57 | if (this.props.artists.all.length) { return; } 58 | 59 | return ( 60 |
    61 |
    No records found!
    62 |
    Try searching again
    63 |
    64 | ); 65 | } 66 | 67 | renderRetire() { 68 | if (this.props.selection.length) { 69 | return ( 70 |
    71 | 77 | 83 |
    84 | ); 85 | } 86 | } 87 | 88 | render() { 89 | return ( 90 |
    91 | {this.renderRetire()} 92 |
      93 | {this.props.artists.all.map(this.renderList.bind(this))} 94 | {this.renderEmptyCollection()} 95 |
    96 | 97 | {this.renderPaginator()} 98 |
    99 | ); 100 | } 101 | } 102 | 103 | const mapStateToProps = ({ artists, selection }) => ({ artists, selection }); 104 | 105 | export default connect(mapStateToProps, actions)(ArtistIndex); 106 | -------------------------------------------------------------------------------- /project/src/components/artists/ArtistMain.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ArtistFilter from './ArtistFilter'; 3 | import ArtistIndex from './ArtistIndex'; 4 | 5 | class ArtistMain extends Component { 6 | render() { 7 | return ( 8 |
    9 |
    10 | 11 |
    12 |
    13 | 14 |
    15 |
    16 | ); 17 | } 18 | } 19 | 20 | export default ArtistMain; 21 | -------------------------------------------------------------------------------- /project/src/components/artists/Paginator.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import * as actions from '../../actions'; 4 | 5 | class Paginator extends Component { 6 | back() { 7 | const { offset, limit, form: { filters: { values } } } = this.props; 8 | 9 | if (offset === 0 ) { return; } 10 | 11 | this.props.searchArtists(values, offset - 10, limit); 12 | } 13 | 14 | advance() { 15 | const { offset, limit, count, form: { filters: { values } } } = this.props; 16 | 17 | if ((offset + limit) > count) { return; } 18 | 19 | this.props.searchArtists(values, offset + 10, limit); 20 | } 21 | 22 | left() { 23 | return ( 24 |
  • 25 | 26 | chevron_left 27 | 28 |
  • 29 | ); 30 | } 31 | 32 | right() { 33 | const { offset, limit, count } = this.props; 34 | 35 | const end = ((offset + limit) >= count) ? true : false; 36 | 37 | return ( 38 |
  • 39 | 40 | chevron_right 41 | 42 |
  • 43 | ); 44 | } 45 | 46 | render() { 47 | return ( 48 |
    49 | 54 | {this.props.count} Records Found 55 |
    56 | ); 57 | } 58 | } 59 | 60 | const mapStateToProps = ({ artists, form }) => { 61 | const { limit, offset, count } = artists; 62 | 63 | return { limit, offset, count, form}; 64 | }; 65 | 66 | export default connect(mapStateToProps, actions)(Paginator); 67 | -------------------------------------------------------------------------------- /project/src/components/filters/Picker.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Picker = () => { 4 | return ( 5 |
    6 | Picker 7 |
    8 | ); 9 | }; 10 | 11 | export { Picker }; 12 | -------------------------------------------------------------------------------- /project/src/components/filters/Range.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Slider from 'react-input-range'; 3 | 4 | class Range extends Component { 5 | onChange(component, values) { 6 | const { input: { onChange } } = this.props; 7 | 8 | onChange(values); 9 | } 10 | 11 | render() { 12 | const { input: { value } } = this.props; 13 | 14 | return ( 15 |
    16 | 17 | 23 |
    24 | ); 25 | } 26 | }; 27 | 28 | Range.defaultProps = { 29 | range: { min: 0, max: 100 } 30 | }; 31 | 32 | export { Range }; 33 | -------------------------------------------------------------------------------- /project/src/components/filters/Switch.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Switch = () => { 4 | return ( 5 |
    6 | Switch 7 |
    8 | ); 9 | }; 10 | 11 | export { Switch }; 12 | -------------------------------------------------------------------------------- /project/src/components/filters/index.js: -------------------------------------------------------------------------------- 1 | export * from './Picker'; 2 | export * from './Range'; 3 | export * from './Switch'; 4 | -------------------------------------------------------------------------------- /project/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { createStore, applyMiddleware } from 'redux'; 4 | import { Provider } from 'react-redux'; 5 | import ReduxThunk from 'redux-thunk'; 6 | import reducers from './reducers'; 7 | import Routes from './router'; 8 | import '../style/materialize.css'; 9 | import '../style/react-range.css'; 10 | import '../style/style.css'; 11 | 12 | const App = () => { 13 | const store = createStore(reducers, {}, applyMiddleware(ReduxThunk)); 14 | return ( 15 | 16 | 17 | 18 | ); 19 | }; 20 | 21 | ReactDOM.render(, document.getElementById('root')); 22 | 23 | -------------------------------------------------------------------------------- /project/src/reducers/ArtistsReducer.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { 3 | SEARCH_ARTISTS, 4 | FIND_ARTIST, 5 | RESET_ARTIST 6 | } from '../actions/types'; 7 | 8 | const INITIAL_STATE = { 9 | all: [], 10 | offset: 0, 11 | limit: 20, 12 | count: 0 13 | }; 14 | 15 | export default (state = INITIAL_STATE, action) => { 16 | switch (action.type) { 17 | case SEARCH_ARTISTS: 18 | return _.extend({}, state, { 19 | count: action.payload.length, 20 | all: action.payload 21 | }) 22 | case FIND_ARTIST: 23 | return _.extend({}, state, { artist: action.payload }); 24 | case RESET_ARTIST: 25 | return _.extend({}, state, { artist: null }); 26 | default: 27 | return state; 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /project/src/reducers/ErrorReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | CREATE_ERROR, 3 | CLEAR_ERROR 4 | } from '../actions/types'; 5 | 6 | export default (state = '', action) => { 7 | switch (action.type) { 8 | case CREATE_ERROR: 9 | return 'There was an error inserting this record'; 10 | case CLEAR_ERROR: 11 | return ''; 12 | default: 13 | return state; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /project/src/reducers/FilterCriteriaReducer.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { 3 | SET_AGE_RANGE, 4 | SET_YEARS_ACTIVE_RANGE 5 | } from '../actions/types'; 6 | 7 | const INITIAL_STATE = { 8 | age: { min: 0, max: 100 } 9 | }; 10 | 11 | export default (state = INITIAL_STATE, action) => { 12 | switch (action.type) { 13 | case SET_AGE_RANGE: 14 | return _.extend({}, state, { age: action.payload }); 15 | case SET_YEARS_ACTIVE_RANGE: 16 | return _.extend({}, state, { yearsActive: action.payload }); 17 | default: 18 | return state; 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /project/src/reducers/SelectionReducer.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { 3 | SELECT_ARTIST, 4 | DESELECT_ARTIST, 5 | RESET_SELECTION 6 | } from '../actions/types'; 7 | 8 | export default (state = [], action) => { 9 | switch (action.type) { 10 | case SELECT_ARTIST: 11 | return [...state, action.payload]; 12 | case DESELECT_ARTIST: 13 | return _.without(state, action.payload); 14 | case RESET_SELECTION: 15 | return []; 16 | default: 17 | return state; 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /project/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { reducer as formReducer } from 'redux-form'; 3 | import filterCriteriaReducer from './FilterCriteriaReducer'; 4 | import ArtistsReducer from './ArtistsReducer'; 5 | import ErrorReducer from './ErrorReducer'; 6 | import SelectionReducer from './SelectionReducer'; 7 | 8 | export default combineReducers({ 9 | form: formReducer, 10 | filterCriteria: filterCriteriaReducer, 11 | artists: ArtistsReducer, 12 | errors: ErrorReducer, 13 | selection: SelectionReducer 14 | }); 15 | -------------------------------------------------------------------------------- /project/src/router.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Router, Route, IndexRoute, hashHistory } from 'react-router'; 3 | 4 | import Home from './components/Home'; 5 | import ArtistMain from './components/artists/ArtistMain'; 6 | 7 | const rootRoute = { 8 | component: Home, 9 | path: '/', 10 | indexRoute: { component: ArtistMain }, 11 | childRoutes: [ 12 | { 13 | path: 'artists/new', 14 | getComponent(location, cb) { 15 | System.import('./components/artists/ArtistCreate') 16 | .then(module => cb(null, module.default)) 17 | } 18 | }, 19 | { 20 | path: 'artists/:id/edit', 21 | getComponent(location, cb) { 22 | System.import('./components/artists/ArtistEdit') 23 | .then(module => cb(null, module.default)) 24 | } 25 | }, 26 | { 27 | path: 'artists/:id', 28 | getComponent(location, cb) { 29 | System.import('./components/artists/ArtistDetail') 30 | .then(module => cb(null, module.default)) 31 | } 32 | } 33 | ] 34 | } 35 | 36 | const Routes = () => { 37 | return ( 38 | 39 | ); 40 | }; 41 | 42 | export default Routes; 43 | -------------------------------------------------------------------------------- /project/style/react-range.css: -------------------------------------------------------------------------------- 1 | .InputRange-slider { 2 | -webkit-appearance: none; 3 | -moz-appearance: none; 4 | appearance: none; 5 | background: #3f51b5; 6 | border: 1px solid #3f51b5; 7 | border-radius: 100%; 8 | cursor: pointer; 9 | display: block; 10 | height: 1rem; 11 | margin-left: -0.5rem; 12 | margin-top: -0.65rem; 13 | outline: none; 14 | position: absolute; 15 | top: 50%; 16 | transition: -webkit-transform 0.3s ease-out, box-shadow 0.3s ease-out; 17 | transition: transform 0.3s ease-out, box-shadow 0.3s ease-out; 18 | width: 1rem; } 19 | .InputRange-slider:active { 20 | -webkit-transform: scale(1.3); 21 | transform: scale(1.3); } 22 | .InputRange-slider:focus { 23 | box-shadow: 0 0 0 5px rgba(63, 81, 181, 0.2); } 24 | .InputRange.is-disabled .InputRange-slider { 25 | background: #cccccc; 26 | border: 1px solid #cccccc; 27 | box-shadow: none; 28 | -webkit-transform: none; 29 | transform: none; } 30 | 31 | .InputRange-sliderContainer { 32 | transition: left 0.3s ease-out; } 33 | 34 | .InputRange-label { 35 | color: #aaaaaa; 36 | font-family: "Helvetica Neue", san-serif; 37 | font-size: 0.8rem; 38 | white-space: nowrap; } 39 | 40 | .InputRange-label--min, 41 | .InputRange-label--max { 42 | bottom: -1.4rem; 43 | position: absolute; } 44 | 45 | .InputRange-label--min { 46 | left: 0; } 47 | 48 | .InputRange-label--max { 49 | right: 0; } 50 | 51 | .InputRange-label--value { 52 | position: absolute; 53 | top: -1.8rem; } 54 | 55 | .InputRange-labelContainer { 56 | left: -50%; 57 | position: relative; } 58 | .InputRange-label--max .InputRange-labelContainer { 59 | left: 50%; } 60 | 61 | .InputRange-track { 62 | background: #eeeeee; 63 | border-radius: 0.3rem; 64 | cursor: pointer; 65 | display: block; 66 | height: 0.3rem; 67 | position: relative; 68 | transition: left 0.3s ease-out, width 0.3s ease-out; } 69 | .InputRange.is-disabled .InputRange-track { 70 | background: #eeeeee; } 71 | 72 | .InputRange-track--container { 73 | left: 0; 74 | margin-top: -0.15rem; 75 | position: absolute; 76 | right: 0; 77 | top: 50%; } 78 | 79 | .InputRange-track--active { 80 | background: #3f51b5; } 81 | 82 | .InputRange { 83 | height: 1rem; 84 | position: relative; 85 | width: 100%; } 86 | -------------------------------------------------------------------------------- /project/style/style.css: -------------------------------------------------------------------------------- 1 | .input-field label { 2 | left: 0; 3 | } 4 | 5 | 6 | .range-slider { 7 | margin: 65px 0 30px; 8 | } 9 | 10 | .range-slider label { 11 | top: -37px; 12 | } 13 | 14 | label { 15 | color: white !important; 16 | } 17 | 18 | .pagination li > a { 19 | cursor: pointer; 20 | } 21 | 22 | .pagination { 23 | margin-bottom: 5px; 24 | } 25 | 26 | a.secondary-content { 27 | position: relative !important; 28 | flex: 1; 29 | text-align: right; 30 | top: 0px !important; 31 | right: 0px !important; 32 | } 33 | 34 | .secondary-content .material-icons { 35 | font-size: 36px; 36 | } 37 | 38 | .artist-detail .header { 39 | display: flex; 40 | justify-content: space-between; 41 | } 42 | 43 | .flex { 44 | display: flex; 45 | justify-content: space-around; 46 | } 47 | 48 | .wrap { 49 | flex-wrap: wrap; 50 | } 51 | 52 | .album img { 53 | width: 250px !important; 54 | } 55 | 56 | .has-error { 57 | color: red; 58 | } 59 | 60 | .spacer a { 61 | margin: 0px 10px; 62 | cursor: pointer; 63 | } 64 | 65 | li.collection-item.avatar { 66 | display: flex; 67 | align-items: center; 68 | padding-left: 10px !important; 69 | } 70 | 71 | li.collection-item.avatar img { 72 | position: relative !important; 73 | left: 0px !important; 74 | margin: 0px 10px !important; 75 | } 76 | 77 | .retired { 78 | background-color: #ddd !important; 79 | } 80 | 81 | .select { 82 | font-size: 1rem; 83 | } 84 | 85 | select { 86 | display: block !important; 87 | margin-bottom: 10px; 88 | height: 30px; 89 | color: black; 90 | } 91 | -------------------------------------------------------------------------------- /project/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require('path'); 3 | var HtmlWebpackPlugin = require('html-webpack-plugin') 4 | var CleanWebpackPlugin = require('clean-webpack-plugin') 5 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 6 | var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 7 | 8 | const VENOR = ["faker", 9 | "lodash", 10 | "react", 11 | "react-dom", 12 | "react-input-range", 13 | "react-redux", 14 | "redux", 15 | "redux-form", 16 | "redux-thunk", 17 | "react-router" 18 | ] 19 | 20 | module.exports = { 21 | entry: { 22 | bundle: './src/index.js', 23 | vendor: VENOR 24 | }, 25 | // 如果想修改 webpack-dev-server 配置,在这个对象里面修改 26 | devServer: { 27 | port: 8081 28 | }, 29 | output: { 30 | path: path.join(__dirname, 'dist'), 31 | filename: '[name].[chunkhash].js' 32 | }, 33 | module: { 34 | rules: [{ 35 | test: /\.js$/, 36 | use: 'babel-loader' 37 | }, 38 | { 39 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 40 | use: [{ 41 | loader: 'url-loader', 42 | options: { 43 | limit: 10000, 44 | name: 'images/[name].[hash:7].[ext]' 45 | } 46 | }] 47 | }, 48 | { 49 | test: /\.css$/, 50 | loader: ExtractTextPlugin.extract({ 51 | fallback: 'style-loader', 52 | use: [{ 53 | loader: 'css-loader' 54 | }] 55 | }) 56 | }, 57 | ] 58 | }, 59 | plugins: [ 60 | new webpack.optimize.CommonsChunkPlugin({ 61 | name: ['vendor', 'manifest'], 62 | minChunks: Infinity 63 | }), 64 | new CleanWebpackPlugin(['dist/*.js'], { 65 | verbose: true, 66 | dry: false 67 | }), 68 | new HtmlWebpackPlugin({ 69 | template: 'index.html' 70 | }), 71 | new webpack.DefinePlugin({ 72 | "process.env.NODE_ENV": JSON.stringify("process.env.NODE_ENV") 73 | }), 74 | new ExtractTextPlugin("css/[name].[contenthash].css"), 75 | new OptimizeCSSPlugin({ 76 | cssProcessorOptions: { 77 | safe: true 78 | } 79 | }), 80 | new webpack.optimize.UglifyJsPlugin({ 81 | compress: { 82 | warnings: false 83 | } 84 | }) 85 | ] 86 | }; 87 | -------------------------------------------------------------------------------- /styles/addImage.css: -------------------------------------------------------------------------------- 1 | img { 2 | border: 5px black solid; 3 | } 4 | .test { 5 | border: 5px black solid; 6 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const ExtractTextPlugin = require("extract-text-webpack-plugin") 3 | 4 | module.exports = { 5 | entry: './app/index.js', // 入口文件 6 | output: { 7 | path: path.resolve(__dirname, 'build'), // 必须使用绝对地址,输出文件夹 8 | filename: "bundle.js", // 打包后输出文件的文件名 9 | publicPath: 'build/' // 打包后的文件夹 10 | }, 11 | module: { 12 | rules: [{ 13 | test: /\.js$/, 14 | use: 'babel-loader', 15 | exclude: /node_modules/ 16 | }, 17 | { 18 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 19 | use: [{ 20 | loader: 'url-loader', 21 | options: { 22 | limit: 10000, 23 | name: 'images/[name].[hash].[ext]' 24 | } 25 | }] 26 | }, 27 | { 28 | test: /\.css$/, 29 | loader: ExtractTextPlugin.extract({ 30 | fallback: 'style-loader', 31 | use: [{ 32 | loader: 'css-loader', 33 | options: { 34 | modules: true 35 | } 36 | }] 37 | }) 38 | }, 39 | ] 40 | }, 41 | plugins: [ 42 | new ExtractTextPlugin("css/[name].[hash].css") 43 | ] 44 | } --------------------------------------------------------------------------------