有没有用dvareactjs前端框架dva做的react的demo

看 React 看烦了?来看看真正做事的人在思考什么。
10:03:54 +08:00 · 6580 次点击
喜欢请点 star ,想看最新更新请点 watch 。点 fork 不如 pr。
(蚂蚁金服)
(蚂蚁金服)
(百姓网)
(去哪儿)
( Teambition )
(陆金所)
其 GitHub 博客最近一次更新是在八个月前,考虑删掉
(苏宁云商)
( ThoughtWorks )
54 回复 &| &直到
14:29:17 +08:00
& & 10:17:51 +08:00
看这文章有个感想,现在也就前端还在坚持首屏性能了,移动端做的是越来越狗屎,
我现在用的 QQ 音乐, QQ 没有一个能在首次打开后 10 秒钟内能正常操作的
& & 10:18:21 +08:00
@ 补充一下我用的是米 4 ,性能不差吧?怎么也是 1000 多的机型
& & 10:21:58 +08:00 via Android
@ 打开淘宝首页试试……
& & 10:23:22 +08:00
@ 现在安卓软件确实感受不到什么优化了,配置都是越来越牛逼,软件也是无所谓耗不耗资源了。
& & 10:24:13 +08:00
弄个标题引人进来, 真的大丈夫...
& & 10:24:46 +08:00
@ 我的标题有什么问题么……
& & 10:25:07 +08:00
@ 淘宝首页我只要他的搜索框而已,用了 chrome 感觉没什么太大影响,下面乱七八糟的东西我也不会去看
常用的功能,充话费点卡也在第一屏不滚动就出来
& & 10:29:21 +08:00
阮一峰 感觉是我一直在学习的人!
& & 10:30:24 +08:00
题叶现在不在 Teambition ,民工也不在苏宁了。
& & 10:31:57 +08:00
@ 然而我查不到他们在哪……
& & 10:32:09 +08:00
@ 来个 Pull Request 吧
& & 10:34:03 +08:00
@ 这标题真挺好的,很多文章发人深思,就像现在疯狂去 jQuery ,有几个人能给出 jQuery 公正的评价,为什么要用 jQuery ,为什么你敢现在放弃 jQuery ,为什么还要坚守 jQuery ?
现在前端届就是盲目跟风,去年 angular 今年 react vue 明年不知道又是什么,结果跑在浏览器里还是最原始的 ecma5 的子集
一个学弟,很简单的题,就是一个渲染+弹框,随便一个模板引擎,手写个 dialog ,绑定事件就搞定的 demo 页面,上来就用 vue ,只知道框架,连最基本的理解需求都不会
我前端和 java 都写, java 给我的感受是如果你需要一个功能,他不是在 xml 里打开,就是一定在 apache commons 里,从来不会想造轮子的事,专心开发就可以,人家的框架历史比你某些语言都老(对,比如 spring 第一版 2002 年就有了,某些语言都是在 2009 年前后才出现的)
再看前端,谁都想造个轮子,各个框架版本号跳的比西方记者还快,优秀的框架是什么, angular 都现在才 2 , jquery 用了这么久也是 2 , spring 现在用的是 4,mybatis3 , lts 版本很重要,除非你是那种捞一笔就跑的项目,否则考虑整个团队,考虑维护成本
反观某 r 框架,没几年版本号都跳到 15 了。。
不过还说一句,你可以自己不用新技术,但是不能排斥、拒绝新技术,也不能不知道这个时代在干嘛,就算是盲从
& & 10:44:51 +08:00
@ 知道了,民工去了 Teambition ,谢谢提醒
& & 10:48:28 +08:00 via Android
@ 题叶现在在饿了吗,根据 sofish 的微博
& & 10:52:47 +08:00
太啰嗦了。。。
& & 10:53:36 +08:00
@ 有点人肉的意味了,我就写前 Teambition 员工吧,等他的所有帐号更新了公司信息再说吧,目前他的个人博客和微博都写的是 Teambition 。
而且公司信息对于这个 repo 并不重要。
& & 10:53:46 +08:00
@ 赞同,一上来就用框架真的大丈夫?框架虽然解决了一部分问题,但是同时又带来了一些新的问题,而且是你不可控的问题。理解运行机制要比无脑使用框架更加重要。
& & 10:53:51 +08:00
@ Thank you all the same.
& & 10:54:37 +08:00
@ 换句话说,讲得很细……
& & 11:01:32 +08:00
不 fork 怎么 pr ?
& & 11:15:48 +08:00
1.ag 2 和 ag 1 不兼容,这个通过版本号看毫无意义。
2.ag 自带 jQuery.react 和 jquery 兼容。这个和 jquery 的关系和冲突都不大。 ag/react/underscore/knockout 之类的框架解决的问题都和 jquery 关系不大。 jQuery 只是个工具库,不是框架。
3.前端天然比后台进化快。运行的环境复杂性,需求,变化的都更多。
仅仅是个人见解。
& & 11:24:50 +08:00
前端目前也就是追求一下性能了,不然还能追求啥……
& & 11:28:16 +08:00
前端进化的比后台进化快,除了移动端一大堆国产手机(这个兼容性我理解更多是 CSS 的层面),桌面端还是主流 IE8+兼容吧,除非小公司,好像还没见哪个大公司敢直接扔 IE8 的,最多是给 IE8 一个老页面用也没说直接跳不兼容
我怎么感觉是脑洞比需求进化的还快
至于 jquery 是框架还是类库,如果对于 ie8-的浏览器说是一个小框架也无所谓,就是差了个模板么,名字问题不用介意
& & 11:31:25 +08:00
@ 再追性能不如想办法让用户升级到 IE10+或者 chrome 你说是不是
现在一个个网站做的比门户功能都多, app 也是,这不是程序员的锅吧?我要一个自行车你给我天宫一号的功能,这性能怎么上的去
如果你做的出那种王道产品能让再笨的用户再麻烦升级浏览器也要用,比自己在那优化效果好的多
& & 11:32:44 +08:00 via Android
1 后台除了性能还有啥
2 其实都一样,追求运行效率和开发效率
& & 11:49:43 +08:00
& 某些语言 09 年前后才出现的
这里指 nodejs ?钩有点直。
& 反观某 r 框架,没几年版本号都跳到 15 了。。
& Today we're announcing that we're switching to major revisions for React. The current version is 0.14.7. The next release will be: 15.0.0
只不过是从 0.14.7 跳到 15 而已。
另外, chrome 开发版现在已经到 52 了,不知楼主有何感想?
& & 12:09:53 +08:00
这堆人里我看也就云龙和民工是“真正在做事”的了……
& & 12:20:47 +08:00
@ 是么?我没把 nodejs 当语言,我说的只是 golang 而已
不喜欢框架没事跳大版本号只是我个人的观点而已。。这个不必当真
至于应用,他怎么命名谁在乎,就跟大家只知道 win7 win8 win10 又有几个人会在意 release version
& & 12:22:21 +08:00 via Android
@ 那你就看他俩
& & 12:26:51 +08:00
既然是 golang 那就是我想多了,本来还想打脸说 nodejs 不是语言呢,哈哈
其实我也不喜欢跳大版本号, nodejs 一年前还在 0.10.x , 0.12.x ,现在 6.x 都快 LTS 了
& & 12:28:09 +08:00
对于你这种有质量,有品质的骗 star 的,我表示已 star
& & 13:21:37 +08:00
@ 同问,不 fork 怎么 pr ?
& & 13:25:11 +08:00
@ 直接编辑,自动帮你 fork + PR 的呀
& & 13:30:23 +08:00
张云龙不在 UC 了
许飞也不在苏宁了
& & 13:32:01 +08:00
徐飞 /民工精髓(苏宁云商)现在在 teambition
& & 13:44:02 +08:00
@ React 版本号没什么好挑了,只不过以前一直用 minor 当 major
& & 13:47:15 +08:00
云龙好像在某个做直播的,忘了是哪家了
& & 13:52:26 +08:00
@ 不,啰嗦可不等于讲的细,张鑫旭的文章废话很多很多,而且条理性较差
每次看见一大坨就不想往下看了
& & 14:10:48 +08:00
公司不重要……
& & 14:12:11 +08:00
另外你们说不在哪里有啥用,说在哪里就行了。
& & 15:35:25 +08:00
单纯吐槽 react 是没什么意思的, 至于跟风哪个领域都不会例外。 前端容易浮躁,新东西,大环境如此吗? 你提框架的时候是否漏了一个叫 backbone 的, 现在国内用 backbone 的人应该不太属于目前前端主流了。但是并不能说 backbone 死了, 只是它成了养分,滋养出了其他的框架。这其实也是前端变化快带来的利与弊。 假设 react 过段时间走远了,难道就白白?带了,什么都没有留下?
& & 15:54:02 +08:00
@ 你还真别说,我们以前一个项目用过 backbone ,当然这项目立项的时候我还没来,结果就是因为团队水平有限浪费了这个框架的特性
所以如果让我选型,我们这种企业应用偏重的肯定选 angular 这种大而全的框架,而且这种渗透 java 思想的东西大家都好接受
& & 16:03:15 +08:00
& & 17:58:01 +08:00
@ 还是看生态的吧, ng 大而全, jquery 不大,但是配套的东西也非常多呀。 只是很多时候是没累积到那个生态就已然 out 或者?带了。
渗透 java 思想对于我这种野生前端, 实在是有点接受无能。
& & 20:15:23 +08:00 via Android
原生 js 已经让我感到很复杂了
& & 20:25:29 +08:00
为什么 lisp 如此先进,却很少见到有大公司使用,招不到人?,难以合作?lisp 太难学?
& & 22:12:34 +08:00 via Android
@ 学校不教
& & 22:13:56 +08:00
不错的内容。 LZ 都是怎么发现的?
& & 22:29:16 +08:00
@ 平时看的哇
& & 22:30:38 +08:00
为啥有个 90 后
& & 23:32:17 +08:00
@ 已经更新了,不知道你说的是哪个。
& & 10:06:24 +08:00
@ 赞!新技能 get !
& & 10:31:57 +08:00
其实你这话基本就是年初那时的一场骂战的论点了。说抛弃 jq 的理由跟你指责前端盲目跟风的理由一样
只知道框架 /工具,连最基本的理解需求都不会, jq 同样会造成这个问题,从这个角度上来说。 jq 与更新的一些轮子并没有区别,上手就 jq ,脱离 jq 就不会 js 了,到底是 js 程序员还是 jq 程序员?
另外就是基于对移动端的性能考量, jq 完全引入大小将近 90k 这在移动端难以接受,特别是一些单页下,有可能 jq 大小就等于整个页面的大小。
而且 jq 为了在 pc 端解决兼容性而多出来的复杂度在移动端意义不大,因为移动端浏览器兼容性更好了。
这个时候就这样的呼声了,为什么不放弃 jq 呢,选取一套更精简方案?
我只是需要一些简单得功能,把整个 jq 引入有必要吗?
jq 一些常用的方法的要自己实现一套够用真的很难吗?
比如 zepto 提供 jquery 风格的接口但是不到 10k ,所以弃用 jquery 就显得并不是那么不讲道理了。
另外,关于你学弟,如果你事先没有硬性要求,他用一个 vue 能实现功能又如何呢?
讲道理,为了实现这样一个简单的功能引入 jq 就比引入 vue 更“简单”了?用 jq 实现就比用 vue 实现更能理解需求了?
& & 14:29:17 +08:00
@ jQ gzip 之后不到 30k
& · & 430 人在线 & 最高记录 3541 & · &
创意工作者们的社区
World is powered by solitude
VERSION: 3.9.8.0 · 72ms · UTC 20:13 · PVG 04:13 · LAX 13:13 · JFK 16:13? Do have faith in what you're doing.标签:至少1个,最多5个
react在中国大地火热已经有2年多了,积累了一大批粉丝,还有很多后来者或许在犹豫要不要学react,网上到处都是react、vue、angular的对比,还有某些大公司开源的组件化前端框架。
然而,最近阿当大师(曾经的前端大牛),是的,曾经,我没有活在他那个权威的时代,相信你们之中很多人也是,如果不是因为前端界的撕逼大战,新人前端几乎都不认识他,我特意问了下公司的其他技术同事,个个都懵逼,这人是谁啊?
阿当的观点中,是明确反对前端学习者从框架入手学习,尤其是react这么“笨重”的框架,当然,还有很多工程化的框架都被他损了一遍。大师提了一个观点:要从基础入手,学习html、css、js,把这些基础打牢固。
其实他的说法无可厚非,在他那个前端地位没那么高的年代,前端没有站在历史的前台,没有那么多开源框架可以用,所以靠自己的双手造了很多轮子,这样就需要很扎实的基本功。
那么,为什么react闲谈会说到阿当呢?如果你看过他的2016年度前端总结的文章,就知道react被喷的有多惨。
好了,撕逼的事情交给高层去解决,当你看了这些反对react的观点之后还有学习react的激情的话,那么,欢迎继续往下看。
学习react需要什么基础?如何学习react?自学多久才能上手项目?
新手一般都有这样的3个疑问,包括我自己也是,在没开始学习react之前,总是看到社区的人说react好难啊,要配置这个,要配置那个,github上找的demo又看不懂,这是Facebook大神开发出来给大神用的框架吧?
1、关于基础:
新手一般都是基础很差,可能你现在只会写几个html和css,做一些简单的静态页面,可能你有jQuery基础,会用基本的jq函数,再好一点基础的就是js的基本常识懂一些,这应该是大部分react入门者的水平。
这其实说的就是我自己,在我知道有react这个单词之前,我就是只懂上面列举的一些前端技术,你要是那时候问我ajax怎么写,我会百度搜了告诉你。对,那时候我只会用百度。。
除非你的基础比那时候的我还差很多,可以考虑先学学基本功,否则,就可以直接学习react了。你可能会激动的反驳我:“还有一堆react的技术栈要学,没有基本功我学毛线啊!”
别急,下面会告诉你react开发是多么容易。
2、关于学习方式:
有的同学会看网上的一些react培训视频,或者去看看w3c的react教程,大部分的人会去react社区看教程,下载demo来研究。这些做法都很好,不过我都没有去认真看过。视频是绝对没兴趣看的了,在遇到bug的时候可能会去社区找找答案。
反对者:“你不看教程写毛线react代码啊!你会写吗?不是只会吹水吧?”
我学习react的方式可能跟绝大部分前端不一样,那是由我架构的第一个react项目,完全零基础,接手项目的第一天,连react都不会拼写(这个一点也不好笑)。第一天逛了下react官网,没看懂一句话,就知道有个jsx的鬼东西。到了研究的第二天,再次发现不对劲啊,怎么多了那么多与react相关的技术,flux、redux(单向数据流的概念)、对了,还需要啥打包工具。。。还有很多。
第三天,我思考了很多,然后找了一个切入点,从redux入手,为什么选择redux,而不是react本身呢?这里主要考虑的是前端工程化的问题,网上都在吹redux是react最好用的脚手架,那么我干脆先用redux搭建好框架,然后再去添加react组件。一个项目做出了最终是要进行维护和迭代更新的,如果一开始不架构好前端工程,那么后期重构的工作量就太大了,我虽然基础不好,但是这点常识还是了解一些,当然也离不开CTO的指点。
redux框架从哪里来呢?redux教程网站并没有告诉大家该如何搭建redux-react开发框架,我们有万能的github,上去找找吧,但不是star越多越好,因为对于新手来说,并不需要太强大的功能,只需要能够提供redux数据流的管理方案即可。这个框架也不是我去找的,而是CTO找好给我的,所以可以看出,又一个大牛在身边指点是多么快速学习。
redux并不比react简单啊,单向数据流的概念什么鬼,乱七八糟的,多了这么多概念,action、reducer、store、component等。即使网上有现成的redux-react框架,自己下载下来仍旧是不知道从哪个文件开始入手啊,可是设计师已经把一堆图丢过来了。
含着泪开始动手:“老大,这个。。该怎么写,你能写几行给我参考吗?”
老大:“框架不是有几个demo吗?自己看看去。”
之后,我学啊,学啊,硬是模仿着demo写出来了一个在现在看来乱七八糟的组件,还有action、reducer,虽然看不懂action是什么、reducer又是什么,但是只知道照着其他组件的数据管理方式照抄就对了。说到这里,又一个痛点就是,通篇都是用ES6的语法来写,然后我又懵逼了。
“老大,这里一个箭头表示啥意思,是C语言里面的指针吗?map函数怎么用啊?react生命周期又要怎么写啊?。。。”
然后老大就给我发了几条网址,其中就有著名的阮老师的ES6教程,还有react的官网教程。
转眼就过了2个月,view层组件竟然被我写出来了,怎么写出来的?2个月就能把这么多技术栈学完然后应用到项目上?扯淡吧。
然而我并没有学懂这些所谓的技术栈,比如最基本的箭头函数,我只会用,但是不理解为什么。
好了,关于从这个项目学习react的步骤就跳过了,后面的交互都是老大实现的,写的比我溜多了。
这个时候我算是半吊子的架构师了吧,比初级还初级的那种,转眼就毕业了,感谢老大的指导,理想和现实,我选择了理想,去了远方。
有句话说的好,一入react深似海,我发现除了react,我其他啥都不会了,咋办,还是找家用react技术的公司去锻炼吧。。故事讲完了,也就到了现在的我,抽时间写写react学习心得。
我的个人建议是,react是前端工程化思想的结晶,学习react,不能只从简单的demo学起,而是一开始就要培养工程化构建项目的思维。有的学习者缺少工程化的思维,这也就是为什么很多学习react的人会觉得react好难学啊,一个简单的弹窗组件都要搞那么复杂的数据流,jQuery一个show(),hide()不就搞定了。
现在很多公司都在推崇敏捷开发,因为市场竞争压力很大,开发一款产品的周期时间越来越短,迭代更新的速度也在加快,这也就是为什么现今的前端界那么多框架盛行的原因,因为框架可以帮助开发者节省很多时间。
3、自学react多久才能上手项目?
我觉得直接上手项目,这个过程中去自学,会有更大的体验和收获,首先一个项目可以给你带来压力,就会逼迫自己去主动学习,当然过程中放弃的人也很多。其次实践能让react概念更加刻骨铭心。
学渣们出来喷:“我就一个学渣,什么js概念都不会,react更看不懂了,不让我先学好基础,直接看react不是浪费时间吗?”
对此,分享几碗鸡汤“没有尝试过,怎么知道自己不行”“人总要有点梦想,万一实现了呢”
来,大家干了这碗鸡汤,要相信未来的路可以走的更远。
0 收藏&&|&&5
你可能感兴趣的文章
233 收藏,12.1k
14 收藏,840
7 收藏,563
本作品采用 署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可
哈哈我看完了,结果发现还是和我一样,只会点htmlcssJquery的前端菜鸟,看了react
看了demo还是不会,感觉自己能写东西的时候吧,结果拿到项目不晓得怎么入手……
继续研究……
哈哈我看完了,结果发现还是和我一样,只会点html\css\Jquery的前端菜鸟,看了react
看了demo还是不会,感觉自己能写东西的时候吧,结果拿到项目不晓得怎么入手……
继续研究……
我已经有3个企业级react项目在2家公司运营着了。。继续加油!
我已经有3个企业级react项目在2家公司运营着了。。继续加油!
前辈能否分享下react项目的源码给我们这些晚辈瞧瞧,谢谢,我q:
前辈能否分享下react项目的源码给我们这些晚辈瞧瞧,谢谢,我q:
@似水流逝 我是晚辈,谢谢,别把我说的那么老。
@似水流逝 我是晚辈,谢谢,别把我说的那么老。
求带,大神
求带,大神
技术不用很牛啊,前端再厉害也就是把psd做成网页显示出来而已,相信你也达到这个水平了。
技术不用很牛啊,前端再厉害也就是把psd做成网页显示出来而已,相信你也达到这个水平了。
那不一样社,最原始的技术也是可以的,但是现在不是那回事了啊,大神求带啊
那不一样社,最原始的技术也是可以的,但是现在不是那回事了啊,大神求带啊
我也想找个大神带
我也想找个大神带
我找你,你找大神,哈哈
我找你,你找大神,哈哈
我只是个会html和css的菜鸟,连jQuery都不会。
我只是个会html和css的菜鸟,连jQuery都不会。
不好评论的评论
不好评论的评论
分享到微博?
技术专栏,帮你记录编程中的点滴,提升你对技术的理解收藏感兴趣的文章,丰富自己的知识库
明天提醒我
我要该,理由是:React作为时下最热的前端框架,各位有什么经验分享下吗? - 知乎1312被浏览42280分享邀请回答var Aquarium = ({species}) =& (
{getFish(species)}
有没有发现被传统的 createClass 方法精简了很多?当然这样写组件也有很多局限,比如不能声明各种生命周期方法等等,但是在常见的前端业务场景中,纯 render 的组件不在少数。在这样的语法推出后,我们就能把这些组件更方便的抽出来复用了。此外,拥抱 ES 6 还有很多的好处,比如在加载依赖的时候不用先 var xxx = require('xxx'); 再 var yyy = xxx. 而是可以直接 import {yyy} from 'xxx'; 简洁明了。4. 生态环境仍然在成长中,坑不少其中首先要口诛笔伐一下的就是 react-router,我们从 v0.10 开始用,到现在 v1.0。你知道为了升级这玩意儿我们改了多少次业务代码么?每次升级 API 都要变,无力吐槽。当年好不容易搞懂了 v0.11,在博客里写了篇技术文章分享,结果后面的日子就是各种被催更……一个月前抽空就 0.13 版又重写了一遍教程,这不 1.0 版又出了,API 基本全都不一样了!!不一样了!!一样了!!样了!当然除了坑也有不少高质量的生态环境产品,比如蚂蚁的 ant design。5. Server 端渲染很美,至今没看见哪个规模级的产品用到可能是我孤陋寡闻吧,欢迎评论中跟进。自己摸索着写过一个最简单的 server 端渲染,但是这套逻辑如果套到我们现在的业务逻辑中,几乎可以直接枪毙。为了实现 server 端渲染需要做出的 trade off 太多。6. React 很简单,也很难简单是因为 React 的 API 真的很少,官网的各种文档花一个下午也能看个七七八八(此时此刻再看看 Angular……)。但是当你以为你真的搞懂 React 的时候,看看这篇文章开头提的问题,有多少人能不假思索的答对呢?(顺便安利一下,我们团队的知乎专栏,目前处于死磕 React 的状态)当你真正在业务项目中使用 React 的时候,你会发现它的生命周期比你想象的复杂;它的 API 背后的逻辑比你以为的麻烦。当然,首先你要踩进这个坑。7. 对于楼上某位仁兄表示这本书太难的回答,作为译者之一表示对不起你。作为补偿,所有购买本书的同学均可凭拍照私信我咨询 React 相关的问题。最后再次安利一下我们团队的专栏: 8. 梦中惊醒补充一点,截止目前为止,Babel 6 是个大坑,业务中使用的时候一定要注意其与 Babel 5 的巨大区别。(关于补充的这一点,再额外的阐述一下,应该理解成 Babel 5 为了让大家愉快的用上 ES 6 做了一些非标准的转换,这些转换在 Babel 6 中都去掉了。所以你在 Babel 5 下面编译正常的代码在 Babel 6 编译时可能就会出错。这个问题不能确切的算作 Babel 的坑,但是会影响你的开发进度)20325 条评论分享收藏感谢收起5821 条评论分享收藏感谢收起查看更多回答1 个回答被折叠()00:43:59 UTC
这是一个比较简单的demo,适合对React有一定了解,但是在项目中应用还有些困惑的同学。阅读前最好已经了解了React的基本语法,和一些es6的语法。
本文假定的需求是写一个图书管理的后台。稍微有点长,大概分下面几个部分。
环境和依赖、编译
dev server
react-hot-loader
react-router
CSS Module
react-ui组件库
在弹出层中编辑
现在有挺多start-kit,还有create-react-app这样的工具,这里不打算用这些,而是从空项目入手,让大家可以多了解一些配置是做什么用途的。从我的项目经验来看,前端现在的发展速度,想做一个通用的start-kit,一劳永逸是很难的。一般一个项目周期3-6个月左右,这个start-kit里面的大部分依赖包可能都升级了,很多配置可能就不可用了,比如babel5升到babel6。
整个demo里可能会有一些“私货”,比如自己写的组件,一些开发习惯等等。
,这里贴出来的代码有些可能并不完整,每一段结束我都打了一个tag,可以checkout出来执行。如果有报错,先看下nodejs的版本是否大于 7.6.0,是否有依赖没有安装。
环境和依赖、编译
安装nodejs
这里需要node v7.6.0以上版本,因为后面会使用koa2。可以npm npmjs和aws国内访问比较慢,可以换淘宝的镜像源,这里在项目根目录建一个.npmrc文件
phantomjs_cdnurl=http://cnpmjs.org/downloads
sass_binary_site=https://npm.taobao.org/mirrors/node-sass/
registry=https://registry.npm.taobao.org
也可以使用 ,很方便。
$ npm install -g nrm
* npm -----
https://registry.npmjs.org/
http://r.cnpmjs.org/
https://registry.npm.taobao.org/
rednpm -- http://registry.mirror.
skimdb -- /registry
$ nrm use taobao
创建一个项目
首先创建一个空的文件夹,在下面执行
$ npm init
安装依赖包
$ npm install react react-dom prop-types --save
$ npm install webpack babel-core babel-loader babel-plugin-react-require babel-plugin-transform-object-rest-spread babel-preset-es2015 babel-preset-react autoprefixer css-loader less-loader postcss-loader sass-loader style-loader url-loader file-loader less node-sass --save-dev
babel-core
babel-loader:babel的webpack插件。
babel-plugin-react-require:如果出现 “React is not defined” 的问题,可以安装这个插件。原因是把React打包到项目里,而某些组件没有import React导致。建议不要把React打包到项目里。
babel-plugin-transform-object-rest-spread:es6解构赋值插件。
babel-preset-es2015:es6语法转换
babel-preset-react:jsx语法转换
autoprefixer:PostCSS插件,自动添加各种前缀
css-loader:读取js文件中引入的css文件,内置了CSS Module的功能
file-loader:处理js引入的文件
less-loader:less语法转换为css
postcss-loader:PostCSS转换器
sass-loader:sass语法转换为css
style-loader:把转换的css文件转为js模块
url-loader:file-loader的封装,提供一些额外功能
less:less-loader的依赖包
node-sass:sass-loader的依赖包
使用eslint (可选)
推荐使用eslint做代码检查,安装依赖包,这里用了airbnb的代码规范
$ npm install babel-eslint eslint eslint-config-airbnb eslint-plugin-react eslint-plugin-jsx-a11y eslint-plugin-import eslint-import-resolver-webpack --save-dev
在项目根目录建一个.eslintrc文件,因为我是无分号党,所以semi设置了"never"
"extends": ["airbnb"],
"parserOptions": {
"ecmaVersion": 2016,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
"browser": true,
"es6": true,
"node": true
"settings": {
"import/parser": "babel-eslint",
"import/resolver": {
"webpack": {
// webpack 文件路径
"config": "webpack.config.js"
"rules": {
// 允许js后缀
"react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
"react/forbid-prop-types": 0,
// 强制无分号
"semi": [2, "never"]
配置webpack config
建一个webpack.config.js文件。这里是一个比较常用的配置,有些细节的配置可以参考相关文档。
const path = require('path')
const webpack = require('webpack')
const autoprefixer = require('autoprefixer')
module.exports = {
// 需要编译的入口文件
app: './src/index.js'
path: path.join(__dirname, '/build'),
// 输出文件名称规则,这里会生成 'app.js'
filename: '[name].js'
// 引用但不打包的文件
externals: { 'react': 'React', 'react-dom': 'ReactDOM' },
plugins: [
// webpack2 需要设置 LoaderOptionsPlugin 开启代码压缩
new webpack.LoaderOptionsPlugin({
minimize: true,
debug: false
// Uglify的配置
new webpack.optimize.UglifyJsPlugin({
beautify: false,
comments: false,
compress: {
warnings: false,
drop_console: true,
collapse_vars: true
resolve: {
// 给src目录一个路径,避免出现'../../'这样的引入
alias: { _: path.resolve(__dirname, 'src') }
test: /\.jsx?$/,
loader: 'babel-loader',
// 可以在这里配置babelrc,也可以在项目根目录加.babelrc文件
options: {
// false是不使用.babelrc文件
babelrc: false,
// webpack2 需要设置modules 为false
presets: [
['es2015', { 'modules': false }],
// babel的插件
plugins: [
'react-require',
'transform-object-rest-spread'
// 这是sass的配置,less配置和sass一样,把sass-loader换成less-loader即可
// webpack2 使用use来配置loader,并且不支持字符串形式的参数,x需要使用options
// loader的加载顺序是从后向前的,这里是 sass -& postcss -& css -& style
test: /\.scss$/,
{ loader: 'style-loader' },
loader: 'css-loader',
// 开启了CSS Module功能,避免类名冲突问题
options: {
modules: true,
localIdentName: '[name]-[local]'
loader: 'postcss-loader',
options: {
plugins: function () {
autoprefixer
loader: 'sass-loader'
// 当图片文件大于10KB时,复制文件到指定目录,小于10KB转为base64编码
test: /\.(png|jpg|jpeg|gif)$/,
loader: 'url-loader',
options: {
limit: 10000,
name: './images/[name].[ext]'
写一个Hello world
src/index.js
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import '_/styles/index.scss'
class App extends Component {
render() {
&div&Hello world.&/div&
ReactDOM.render(&App /&, document.getElementById('root'))
src/styles/index.scss
font-size: 14
console下执行
$ node_module/.bin/webpack
Chunk Names
[0] ./src/styles/index.scss 1.24 kB {0} [built]
[3] ./~/base64-js/index.js 3.5 kB {0} [built]
[4] ./~/buffer/index.js 48.8 kB {0} [built]
[5] ./~/buffer/~/isarray/index.js 131 bytes {0} [built]
[6] ./~/css-loader/lib/css-base.js 2.19 kB {0} [built]
[7] ./~/ieee754/index.js 2.08 kB {0} [built]
[8] ./~/style-loader/fixUrls.js 3 kB {0} [built]
[9] (webpack)/buildin/global.js 808 bytes {0} [built]
[10] ./src/index.js 2.14 kB {0} [built]
[11] ./~/css-loader?{"modules":true,"localIdentName":"[name]-[local]"}!./~/postcss-loader?{}!./~/sass-loader/lib/loader.js!./src/styles/index.scss 186 bytes {0} [built]
[12] ./~/style-loader/addStyles.js 8.51 kB {0} [built]
+ 2 hidden modules
这里可能会有一个“DeprecationWarning: loaderUtils.parseQuery()”,babel-loader的问题,下个版本应该会修复
$ git checkout step-1
建一个server
我们这里使用koa2来做开发服务器,首先,安装koa
$ npm install koa koa-router koa-send http-proxy save-dev
在demo文件夹下建一个index.html
&!doctype html&
&title&React Example&/title&
&div id='root'&&/div&
&script src="/react.min.js"&&/script&
&script src="/react-dom.min.js"&&/script&
&script src="/app.js"&&/script&
在根目录下建一个server.js
const Koa = require('koa')
const send = require('koa-send')
const Router = require('koa-router')
const app = new Koa()
const router = new Router()
router.get('/', async function (ctx) {
await send(ctx, 'demo/index.html')
router.get('/app.js', async function (ctx) {
await send(ctx, 'build/app.js')
// 线上会使用压缩版本的React,而在开发的时候,我们需要使用react-with-addons的版本来查看错误信息
// 所以这里我通常会把React和ReactDOM代理到本地未压缩的文件
router.get('**/react.min.js', async function (ctx) {
await send(ctx, 'demo/react-with-addons.js')
router.get('**/react-dom.min.js', async function (ctx) {
await send(ctx, 'demo/react-dom.js')
app.use(router.routes())
app.listen(3000, function () {
console.log('server running on http://localhost:3000')
在终端执行
$ node server.js
打开浏览器,输入 localhost:3000 ,就可以看到 “Hello world” 了。
$ git checkout step-2
加入react-hot-loader
hot loader有两个方案,一个方案是使用 webpack-dev-middleware和webpack-hot-middleware,优点是可以和开发服务器共用一个server,缺点是配置比较繁琐。另一个方案是用react-hot-loader,优点是配置比较简单,缺点是要另外启动一个server来代理资源。因为react-hot-loader 3现在还是beta版,所以需要加 @next 安装
npm install --save react-hot-loader@next webpack-dev-server --save-dev
在项目根目录添加一个 webpack.dev.config.js 文件,和webpack.config.js稍有不同,去除了代码压缩的配置,增加了react-hot-loader的插件配置
const path = require('path')
const webpack = require('webpack')
const autoprefixer = require('autoprefixer')
module.exports = {
devtool: 'cheap-module-source-map',
// 需要编译的入口文件,增加了react-hot-loader的配置
'react-hot-loader/patch',
'webpack-dev-server/client?http://localhost:3001',
'webpack/hot/only-dev-server',
'./src/index.js',
// 输出文件名称规则,这里会生成 'app.js'
filename: '[name].js',
publicPath: '/',
// 引用但不打包的文件
externals: { react: 'React', 'react-dom': 'ReactDOM' },
plugins: [
new webpack.HotModuleReplacementPlugin(),
resolve: {
// 给src目录一个路径,避免出现'../../'这样的引入
alias: { _: path.resolve(__dirname, 'src') },
test: /\.jsx?$/,
loader: 'babel-loader',
// 可以在这里配置babelrc,也可以在项目根目录加.babelrc文件
options: {
// false是不使用.babelrc文件
babelrc: false,
// webpack2 需要设置modules 为false
presets: [
['es2015', { modules: false }],
// babel的插件
plugins: [
'react-hot-loader/babel',
'react-require',
'transform-object-rest-spread',
// 这是sass的配置,less配置和sass一样,把sass-loader换成less-loader即可
// webpack2 使用use来配置loader,并且不支持字符串形式的参数了,必须使用options
// loader的加载顺序是从后向前的,这里是 sass -& postcss -& css -& style
test: /\.scss$/,
{ loader: 'style-loader' },
loader: 'css-loader',
// 开启了CSS Module功能,避免类名冲突问题
options: {
modules: true,
localIdentName: '[name]-[local]',
loader: 'postcss-loader',
options: {
plugins() {
autoprefixer,
loader: 'sass-loader',
// 当图片文件大于10KB时,复制文件到指定目录,小于10KB转为base64编码
test: /\.(png|jpg|jpeg|gif)$/,
loader: 'url-loader',
options: {
limit: 10000,
name: './images/[name].[ext]',
在 server.js 里加入代码,启动hot loader server
const webpack = require('webpack')
const WebpackDevServer = require('webpack-dev-server')
const config = require('./webpack.dev.config')
const DEVPORT = 3001
new WebpackDevServer(webpack(config), {
publicPath: config.output.publicPath,
hot: true,
quiet: false,
noInfo: true,
colors: true
}).listen(DEVPORT, 'localhost', function (err, result) {
if (err) {
return console.log(err)
把 app.js 重定向到 webpack-dev-server
/* 删掉这一段
router.get('/app.js', async function (ctx) {
await send(ctx, 'build/app.js')
// 如果请求出现跨域问题的话,参考master分支下的代码,把这里改成http-proxy转发
router.get('**/*.js(on)?', async function (ctx) {
ctx.redirect(`http://localhost:${DEVPORT}/${ctx.path}`)
这时执行 node server 访问 localhost:3000 会出现一个 “React Hot Loader: App in ..../index.js will not hot reload correctly because index.js uses
during module definition. For hot reloading to work, move App into a separate file and import it from index.js.” 警告,我们需要拆分 src/index.js 文件
src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
ReactDOM.render(&App /&, document.getElementById('root'))
// 注意,要增加这句
module.hot && module.hot.accept()
src/App.js
import React, { Component } from 'react'
import '_/styles/index.scss'
class App extends Component {
render() {
&div&Hello world.&/div&
export default App
重启服务,修改App.js代码试试看吧。
$ git checkout step-3
加入react-router 4.0
$ npm install react-router-dom --save
这里使用hashRouter,修改App.js代码(暂时忽略样式)
import React from 'react'
import { HashRouter as Router, Route, Link } from 'react-router-dom'
import Author from '_/components/author'
import Category from '_/components/category'
import Book from '_/components/book'
import '_/styles/index.scss'
function App() {
&Link to="/author"&作者&/Link&
&Link to="/category"&分类&/Link&
&Link to="/book"&书籍&/Link&
&Route path="/author" component={Author} /&
&Route path="/category" component={Category} /&
&Route path="/book" component={Book} /&
export default App
在 components 下面建3个文件夹author/category/book,每个放入一个index.js文件,先简单render一个div
export default function () {
&div&书籍列表&/div&
启动服务,点击链接,看下url变化
$ git checkout step-4
CSS Module
css-loader 提供了CSS Module的功能,在开发SPA应用的时候,可以减少css类名冲突带来的问题之前webpack已经配置过了,现在可以直接使用
options: {
modules: true,
// 这个的配置是 "文件名-类名",比较简单,实际项目中,可以加入hash,例如'[local]-[hash:base64:5]'
localIdentName: '[name]-[local]',
在styles文件夹下面加两个文件src/styles/header.scss
.container {
width: 100%;
height: 50
src/styles/menu.scss
.container {
width: 200
修改App.js
import React from 'react'
import { HashRouter as Router, Route, Link } from 'react-router-dom'
import Author from '_/components/author'
import Category from '_/components/category'
import Book from '_/components/book'
import '_/styles/index.scss'
// 和引入js文件一样
import _header from '_/styles/header.scss'
import _menu from '_/styles/menu.scss'
function App() {
{/* 和使用对象一样使用类名 */}
&div className={_header.container}&
React Example
&div className={_menu['container']}&
&Link to="/author"&作者&/Link&
&Link to="/category"&分类&/Link&
&Link to="/book"&书籍&/Link&
&Route path="/author" component={Author} /&
&Route path="/category" component={Category} /&
&Route path="/book" component={Book} /&
export default App
render后的代码,可以看到两个组件使用了两个相同的类名,但是在两个不同的文件里,生成的类名也不同
&div class="header-container"&...&/div&
&div class="menu-container"&...&/div&
完整代码checkout step-5
$ git checkout step-5
使用ui组件库
组件库这里加点私货,使用了。。
$ npm install rctui classnames query-string refetch --save
在src/components/author下面加一个List.js文件,这是一个比较常见的使用state的流程,组件加载后获取数据,重新设置数据,再渲染
import React, { Component } from 'react'
import { Table, Card } from 'rctui'
import fetch from 'refetch'
class List extends Component {
constructor(props) {
super(props)
this.state = {
componentWillMount() {
fetch.get('/authorlist.json').then((res) =& {
// 实际项目中,这里最好判断一下组件是否已经unmounted
this.setState({ data: res.data })
render() {
// 这里可以根据data的状态,返回其它内容,例如
// if (!this.state.data) return &Loading /&
&Card.Header&作者列表&/Card.Header&
data={this.state.data.list}
columns={[
{ name: 'id', header: 'ID' },
{ name: 'name', header: '姓名' },
{ name: 'nationality', header: '国籍' },
{ name: 'birthday', header: '生日' },
export default List
修改src/components/author/index.js,增加一条Route
render() {
const { match } = this.props
path={`${match.url}`}
{/* 这里可以直接用 component={List},不过我们后面要对这里做一些修改 */}
render={() =& &List /&}
在demo下加了一个authorlist.json
"total": 2,
"page": 1,
"size": 10,
"name": "乔治.R.R.马丁",
"birthday": "",
"nationality": "美国"
"name": "托尔金",
"birthday": "",
"nationality": "英国"
$ git checkout step-6
在上面的示例中,通过[ fetch -& setState -& render ] 这样一个流程来处理数据。一个项目中,可能有很多地方会有类似的场景和使用方式。可以通过高阶组件的方式来抽取这个流程,使它可以在更多的地方使用。在项目中新建一个文件 src/hoc/fetch.js
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import refetch from 'refetch'
import { Mask, Spin } from 'rctui'
const PENDING = 0
const SUCCESS = 1
const FAILURE = 2
export default function (Origin) {
class Fetch extends Component {
constructor(props) {
super(props)
this.state = {
data: null,
status: props.fetch ? PENDING : SUCCESS,
this.fetchData = this.fetchData.bind(this)
componentWillMount() {
if (this.props.fetch) this.fetchData()
this.isUnmounted = false
componentWillUnmount() {
this.isUnmounted = true
fetchData() {
let { fetch } = this.props
if (typeof fetch === 'string') fetch = { url: fetch }
// 设置状态为加载中
this.setState({ data: null, status: PENDING })
refetch.get(fetch.url, fetch.data).then((res) =& {
// 如果组件已经卸载,不处理返回数据
if (this.isUnmounted) return
// demo数据格式统一为,成功返回data,失败返回error
if (res.data) {
this.setState({ status: SUCCESS, data: res.data })
this.setState({ status: FAILURE, message: res.error })
}).catch((e) =& {
if (this.isUnmounted) return
this.setState({ status: FAILURE, message: e.message })
render() {
const { status, data } = this.state
// 状态为成功,返回组件,并且传入data
if (status === SUCCESS) {
return &Origin {...this.props} data={data} fetchData={this.fetchData} /&
// 加载中,返回一个动态的加载中
if (status === PENDING) {
&div style={{ position: 'relative' }}&
&Spin size={40} type="simple-circle" /&
// 处理失败信息
if (status === FAILURE) {
return &div&{this.state.message}&/div&
return null
Fetch.propTypes = {
fetch: PropTypes.oneOfType([
PropTypes.string,
PropTypes.object,
Fetch.defaultProps = {
fetch: null
return Fetch
修改之前的src/components/author/List.js,移除了state相关的代码,变成了一个纯粹展示的组件,所以直接写成一个函数
import React from 'react'
import PropTypes from 'prop-types'
import { Table, Card } from 'rctui'
import fetch from '_/hoc/fetch'
function List(props) {
const { data } = props
&Card.Header&作者列表&/Card.Header&
data={data.list}
columns={[
{ name: 'id', header: 'ID' },
{ name: 'name', header: '姓名', sort: true },
{ name: 'nationality', header: '国籍' },
{ name: 'birthday', header: '生日', sort: true },
List.propTypes = {
data: PropTypes.object.isRequired,
export default fetch(List)
src/components/author/index.js 也要稍作修改
render() {
const { match } = this.props
path={`${match.url}`}
{/* 这里加了fetch的属性 */}
render={() =& &List fetch={{ url: '/authorlist.json' }} /&}
$ git checkout step-7
加入mock数据
前面用了一个json文件来模拟数据,通常可以使用mock.js或者faker.js来模拟数据。这里再加一点私货,用一个我之前写的系统qenya,。暂时还有一些功能待补全,文档也还没有写,不过这里可以拿来mock数据。
首先,安装一下
$ npm install qenya --save
在server下面加入启动代码
const qenya = require('qenya')
// qenya 会启动两个服务,一个是数据管理平台,可以设置数据表和api
// 另一个是api服务,通过在数据管理平台配置的api访问
appPort: 3002,
apiPort: 3003,
render: function (res) {
if (res.data) {
return res.data
error: res.errors[0].message
// api请求转发
const proxy = new httpProxy.createProxyServer({
target: 'http://localhost:3003/',
changeOrigin: true
const methods = ['get', 'post', 'put', 'delete']
methods.forEach(m =&
router[m]('/api/*', function (ctx) {
proxy.web(ctx.req, ctx.res)
ctx.body = ctx.res
qenya会在项目下面创建一个data文件夹,数据会保存在里面。这里暂时忽略这些配置,只要知道有接口就好。如果感兴趣,可以checkout代码,访问localhost:3002 看下api配置,后面我会慢慢完善文档。
$ git checkout step-8
checkout代码,已经在后台配置好了四个数据接口,用来模拟服务端
/api/authorlist
获取列表数据
/api/author/:id
根据id获取单条记录
/api/author
添加或编辑数据
/api/author
删除一条数据
新增 src/components/author/Edit.js 文件
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { Card, Form, FormControl, Message, Button } from 'rctui'
import refetch from 'refetch'
import fetch from '_/hoc/fetch'
class Edit extends Component {
constructor(props) {
super(props)
this.handleSubmit = this.handleSubmit.bind(this)
this.handleCancel = this.handleCancel.bind(this)
handleSubmit(data) {
refetch.post('/api/author', data).then((res) =& {
if (res.data) {
this.props.history.push('/author')
Message.success('保存成功')
Message.error(res.error)
handleCancel() {
this.props.history.goBack()
render() {
const { data } = this.props
&Card.Header&作者编辑&/Card.Header&
&div style={{ padding: 20 }}&
&Form data={data} onSubmit={this.handleSubmit} &
&FormControl label="姓名" name="name" grid={1 / 3} type="text" required min={2} max={20} /&
&FormControl label="生日" name="birthday" type="date" required /&
&FormControl label="国籍" name="nationality" type="text" /&
&FormControl&
&Button type="submit" status="primary"&提交&/Button&
&Button onClick={this.handleCancel}&取消&/Button&
&/FormControl&
Edit.propTypes = {
data: PropTypes.object,
history: PropTypes.object.isRequired,
Edit.defaultProps = {
// 使用之前的高阶组件fetch来获取数据
export default fetch(Edit)
修改 src/components/author/index.js 文件,加入路由
import React from 'react'
import PropTypes from 'prop-types'
import { Route, Switch } from 'react-router-dom'
import List from './List'
import Edit from './Edit'
function Author(props) {
const { url } = props.match
{/* 新增作者,不需要fetch data */}
&Route path={`${url}/new`} component={Edit} /&
{/* 编辑作者,使用fetch获取数据 */}
path={`${url}/edit/:id`}
({ history, match }) =& &Edit history={history} fetch={{ url: `/api/author/${match.params.id}` }} /&
{/* 列表,因为加入了分页,数据处理放到了List里面 */}
&Route path={`${url}`} component={List} /&
Author.propTypes = {
match: PropTypes.object.isRequired,
export default Author
修改 src/components/author/List.js 文件,因为加入分页功能,拆分了这个页面
import React from 'react'
import PropTypes from 'prop-types'
import { Card, Button } from 'rctui'
import queryString from 'query-string'
import TableList from './TableList'
function List(props) {
const { history } = props
// 从queryString中获取分页信息,格式为 ?page=x&size=x
const query = queryString.parse(history.location.search)
// 每页数据数量
if (!query.size) query.size = 10
&Card.Header&作者列表&/Card.Header&
&div style={{ padding: 12 }}&
&Button status="success" onClick={() =& history.push('/author/new')}&添加作者&/Button&
&TableList
history={history}
fetch={{ url: '/api/authorlist', data: query }}
List.propTypes = {
history: PropTypes.object.isRequired,
export default List
src/components/author/TableList.js 代码
import React from 'react'
import PropTypes from 'prop-types'
import { Link } from 'react-router-dom'
import { Table, Pagination } from 'rctui'
import fetch from '_/hoc/fetch'
import DelButton from './DelButton'
function TableList(props) {
const { data, history, fetchData } = props
data={data.list}
columns={[
{ name: 'id', width: '60px', header: 'ID' },
{ name: 'name', header: '姓名' },
{ name: 'nationality', header: '国籍' },
{ name: 'birthday', header: '生日' },
width: '120px',
content: d =& (
&Link to={`/author/edit/${d.id}`}&编辑&/Link&
&DelButton onSuccess={fetchData} data={d} /&
&div style={{ textAlign: 'center' }}&
&Pagination
page={data.page} size={data.size} total={data.total}
onChange={page =& history.push(`/author?page=${page}`)}
TableList.propTypes = {
data: PropTypes.object.isRequired,
fetchData: PropTypes.func.isRequired,
history: PropTypes.object.isRequired,
export default fetch(TableList)
通过react-router和高阶组件fetch,我们把author下面的所有组件(列表、分页、编辑)都变成了无状态的组件。每个组件只关心route提供了什么参数,应该怎样去展示,当需要变化的时候,history.push到相应的route就行了。
$ git checkout step-9
00:44:29 UTC
接下来写分类管理,这次我们使用redux来处理。首先,仍然是安装包
$ npm install redux react-redux redux-thunk --save
数据结构非常简单
"id": "8",
"name": "科学幻想",
"desc": "简称科幻,是虚构作品的一种类型,描述诸如未来科技、时间旅行、超光速旅行、平行宇宙、外星生命、人工智能、错置历史等有关科学的想象性内容。"
有3个后端接口
/api/genres
获取列表数据
/api/genre
添加或编辑数据
/api/genre
删除一条数据
之前随手写了一个category占位,这里统一改成genre。
先在src下面建一个文件夹 src/actions,用来存放 redux 的 actions。这里做了一些简化,一次从服务端拉取所有数据存在store中,没有考虑分页的问题。也没有单条数据的请求,编辑时直接从list里面获取了。
src/actions/genre.js
import { Message } from 'rctui'
import refetch from 'refetch'
export const GENRE_LIST = 'GENRE_LIST'
function handleList(status, data, message) {
type: GENRE_LIST,
// 从服务端获取数据
function fetchList() {
return (dispatch) =& {
dispatch(handleList(0))
refetch.get('/api/genres', { size: 999 }).then((res) =& {
if (res.data) {
dispatch(handleList(1, res.data.list))
dispatch(handleList(2, null, res.error))
}).catch((err) =& {
dispatch(handleList(2, null, err.message))
// 对外获取列表的接口
export function getGenreList() {
return (dispatch, getState) =& {
const { data, status } = getState().genre
// 如果数据已存在,直接返回
if (status === 1 && data && data.length & 0) {
return Promise.resolve()
return dispatch(fetchList())
// 保存数据接口
export function saveGenre(body, onSuccess) {
return (dispatch, getState) =& {
refetch.post('/api/genre', body, { dataType: 'json' }).then((res) =& {
if (res.data) {
onSuccess()
// 如果是修改,从数组里把原数据剔除
const data = getState().genre.data.filter(d =& d.id !== res.data.id)
data.unshift(res.data)
dispatch(handleList(1, data))
Message.success('保存成功')
Message.error(res.error)
}).catch((err) =& {
Message.error(err.message)
// 删除数据接口
export function removeGenre(id) {
return (dispatch, getState) =& {
refetch.delete('/api/genre', { id }).then((res) =& {
if (res.data === 1) {
Message.success('删除成功')
// 删除直接从store的列表里剔除数据,不再发请求到服务端
const data = getState().genre.data.filter(d =& d.id !== id)
dispatch(handleList(1, data))
}).catch((err) =& {
Message.error(err.message)
接下来增加一个 src/reducers/genre.js,这个比较简单,只有一个 action type
import { GENRE_LIST } from '_/actions/genre'
export default function (state = {
status: 0,
data: undefined,
}, action) {
switch (action.type) {
case GENRE_LIST:
return Object.assign({}, state, {
status: action.status,
data: action.data,
message: action.message,
return state
虽然只有一个reducer,为了演示结构,还是建一个 src/reducers/index.js 文件
import { combineReducers } from 'redux'
import genre from './genre'
export default combineReducers({
接下来是 src/store.js,这里使用 redux-thunk 来处理异步数据
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import reducer from './reducers'
const createStoreWithMiddleware = applyMiddleware(thunk)(createStore)
const store = createStoreWithMiddleware(reducer)
export default store
最后,把 store 注入到 App,修改 src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import App from './App'
import store from './store'
ReactDOM.render(
&Provider store={store}&
&/Provider&,
document.getElementById('root'))
module.hot && module.hot.accept()
现在可以开始写 genre 的代码了
src/components/genre/index.js
import React, { Component } from 'react'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import { getGenreList } from '_/actions/genre'
import { Route, Switch } from 'react-router-dom'
import Loading from '_/components/comm/Loading'
import List from './List'
import Edit from './Edit'
class Genre extends Component {
constructor(props) {
super(props)
this.state = {}
this.renderEdit = this.renderEdit.bind(this)
componentDidMount() {
this.props.dispatch(getGenreList())
renderEdit({ history, match }) {
const { genre } = this.props
// 这里没有从服务端获取,而是从list里面获取的单条数据
const data = genre.data.find(d =& d.id === match.params.id)
return &Edit history={history} data={data} /&
render() {
const { genre, history, match } = this.props
const { url } = match
// 当没有数据的时候展示一个 Loading
if (genre.status === 0) {
return &Loading height={300} /&
if (genre.status === 2) {
return &div&{genre.message}&/div&
// 和 author 一样,都是三条路由,只是数据已经从props里拿到,这里直接传入
&Route path={`${url}/new`} component={Edit} /&
&Route path={`${url}/edit/:id`} render={this.renderEdit} /&
path={`${url}`}
render={() =& &List history={history} data={genre.data} /&}
Genre.propTypes = {
dispatch: PropTypes.func.isRequired,
genre: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
match: PropTypes.object.isRequired,
const mapStateToProps = (state) =& {
const { genre } = state
return { genre }
export default connect(mapStateToProps)(Genre)
src/components/genre/List.js
import React from 'react'
import PropTypes from 'prop-types'
import { Link } from 'react-router-dom'
import { Card, Table, Button } from 'rctui'
import DelButton from './DelButton'
function List(props) {
const { data, history } = props
&Card.Header&类型列表&/Card.Header&
&div style={{ padding: 12 }}&
&Button status="success" onClick={() =& history.push('/genre/new')}&添加类型&/Button&
data={data}
columns={[
name: 'id',
width: 100,
header: 'ID',
(a, b) =& parseInt(a.id, 10) & parseInt(b.id, 10) ? 1 : -1,
(a, b) =& parseInt(a.id, 10) & parseInt(b.id, 10) ? 1 : -1,
{ name: 'name', width: 160, header: '名称', sort: true },
{ name: 'desc', header: '简介' },
width: '120px',
content: d =& (
&Link to={`/genre/edit/${d.id}`}&编辑&/Link&
&DelButton data={d} /&
{/* 因为拿到的是全部的数据,这里使用了Table内置的分页 */}
pagination={{ size: 10, position: 'center' }}
List.propTypes = {
data: PropTypes.array.isRequired,
history: PropTypes.object.isRequired,
export default List
src/components/genre/Edit.js
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { Card, Form, FormControl, Button } from 'rctui'
import { saveGenre } from '_/actions/genre'
class Edit extends Component {
constructor(props) {
super(props)
this.handleSubmit = this.handleSubmit.bind(this)
this.handleCancel = this.handleCancel.bind(this)
handleSubmit(data) {
// 这里调用了 actions 里的方法
this.props.dispatch(saveGenre(data, this.props.history.goBack))
handleCancel() {
this.props.history.goBack()
render() {
const { data } = this.props
&Card.Header&类型编辑&/Card.Header&
&div style={{ padding: 20 }}&
&Form data={data} style={{ width: 700 }} onSubmit={this.handleSubmit} &
&FormControl label="名称" name="name" grid={1 / 3} type="text" required min={2} max={20} /&
&FormControl label="简介" name="desc" type="textarea" max={200} /&
&FormControl&
&Button type="submit" status="primary"&提交&/Button&
&Button onClick={this.handleCancel}&取消&/Button&
&/FormControl&
Edit.propTypes = {
data: PropTypes.object,
dispatch: PropTypes.func.isRequired,
history: PropTypes.object.isRequired,
Edit.defaultProps = {
export default connect()(Edit)
对比一下author的示例,使用redux之后,实际是要复杂很多的,如果加上分页,会更加复杂一些。修改代码的时候,很可能需要在action,reducer,component的代码里找一圈。并且要时刻关心store里面的数据,比如一条数据更新或者删除了,列表里的数据也要及时更新,后期的维护成本会比较高一些。当然,优点也是显而易见的,代码结构比较清晰,任意的跨组件通信,服务端请求次数减少。
个人认为,除了一些全局的数据,比如用户登陆信息,权限等等,可以放在redux里维护之外,和业务相关的大部分列表页,详情页等数据,都可以使用state维护,用完就丢,可以减少很多维护成本。
$ git checkout step-10
在弹出层中编辑
书籍这里,换一个不一样的交互方式吧,列表改为card,编辑改为弹出层。
"id": "17",
"title": "沉默的大多数",
"author": "1",
"genres": "1,2",
"publishAt": "1997-01",
"cover": "/lpic/s1447349.jpg",
"desc": ""
src/components/book/index.js,采用弹出层的设计,所以这里不再需要子路由
import React from 'react'
import PropTypes from 'prop-types'
import { Card } from 'rctui'
import queryString from 'query-string'
import List from './List'
function Book(props) {
const { history } = props
const query = queryString.parse(history.location.search)
if (!query.size) query.size = 12
&Card.Header&书籍管理&/Card.Header&
&List history={history} fetch={{ url: '/api/booklist', data: query }} /&
Book.propTypes = {
history: PropTypes.object.isRequired,
export default Book
src/components/book/List.js
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { Media, Image, Button, Modal, Pagination } from 'rctui'
import { getGenreList } from '_/actions/genre'
import fetch from '_/hoc/fetch'
import Edit from './Edit'
class List extends Component {
componentDidMount() {
this.props.dispatch(getGenreList())
handleEdit(book) {
// 这里从store里获取类别,传递给Edit使用
const { genres } = this.props
const fc = book ? { url: `/api/book/${book.id}` } : undefined
const mid = Modal.open({
header: '书籍编辑',
width: 800,
content: (
genres={genres}
fetch={fc}
onSuccess={() =& {
this.props.fetchData()
Modal.close(mid)
buttons: {
提交: 'submit',
取消: true,
render() {
const { data, history } = this.props
&div style={{ padding: 20 }}&
&Button status="success" onClick={() =& this.handleEdit(null)}&添加书籍&/Button&
{data.list.map(d =& (
&div key={d.id}&
&Media.Left&
&Image src={d.cover} width={100} height={150} type="fill" /&
&/Media.Left&
&Media.Body style={{ fontSize: 12, paddingLeft: 10, color: '#666' }}&
&h4 style={{ fontSize: 18, marginBottom: 16 }}&{d.title}&/h4&
&div&作者:{d.author}&/div&
&div&出版时间:{d.publishAt}&/div&
&div&类型:{d.genres}&/div&
style={{ position: 'absolute', right: 0, bottom: 0, fontSize: 12 }}
status="link"
onClick={() =& this.handleEdit(d)}
&编辑&/Button&
&/Media.Body&
&div style={{ textAlign: 'center' }}&
&Pagination
page={data.page} size={data.size} total={data.total}
onChange={page =& history.push(`/book?page=${page}`)}
List.propTypes = {
data: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
fetchData: PropTypes.func.isRequired,
genres: PropTypes.array,
history: PropTypes.object.isRequired,
List.defaultProps = {
genres: [],
const mapStateToProps = (state) =& {
const { genre } = state
return { genres: genre.data }
export default fetch(connect(mapStateToProps)(List))
src/components/book/Edit.js
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { Form, FormControl, Message } from 'rctui'
import fetch from '_/hoc/fetch'
import refetch from 'refetch'
class Edit extends Component {
constructor(props) {
super(props)
this.handleSubmit = this.handleSubmit.bind(this)
handleSubmit(data) {
refetch.post('/api/book', data).then((res) =& {
if (res.data) {
this.props.onSuccess()
Message.success('保存成功')
Message.error(res.error)
render() {
const { data, genres } = this.props
&Form data={data} onSubmit={this.handleSubmit}&
&FormControl label="书名" name="title" grid={1 / 2} type="text" required min={2} max={20} /&
&FormControl
label="作者" name="author" type="select" required grid={1 / 3}
fetch={{ url: '/api/authorlist?size=999', then: res =& res.data.list }}
valueTpl="{id}" optionTpl="{name}"
&FormControl label="出版时间" type="text" name="publishAt" grid={1 / 2} /&
&FormControl label="封面图片" type="text" name="cover" grid={7 / 8} /&
&FormControl
label="类别" type="checkbox-group" name="genres"
data={genres} valueTpl="{id}" textTpl="{name}"
&FormControl label="简介" type="textarea" rows={3} name="desc" grid={7 / 8} /&
Edit.propTypes = {
data: PropTypes.object,
genres: PropTypes.array.isRequired,
onSuccess: PropTypes.func.isRequired,
Edit.defaultProps = {
export default fetch(Edit)
$ git checkout step-11
最后说下项目结构吧,可能不是最好的,不过算是我做了一些项目下来比较顺手的。
生产代码,发布到cdn的
放一些本地开发需要用到的文件,html,第三方库等等
源代码目录
|- actions/
redux actions 目录
|- components/
对于SPA应用,个人习惯把所有组件都放在这个目录下
高阶组件目录
|- reducers/
redux reducer 目录
|- styles/
个人习惯把样式文件统一起来管理,因为可能会有一些全局的变量文件,
实际上文件并不多,如果不是复用的样式或者是伪类,都直接写在组件上
一些工具类的文件
项目框架文件
|- index.js
项目入口文件
|- store.js
|- .eslintrc
eslint 配置文件
这个文件可以放在全局,不过我的机器上有些项目在内网,所以习惯放在项目下面单独管理
|- server.js
开发服务器
|- webpack.dev.config.js
开发时用的webpack配置
|- webpack.config.js
发布时用的配置
最后,再贴一下项目地址,有什么问题,可以在这里回复,或者给我提issue。当然,如果有什么不足的地方,欢迎指正。
02:21:46 UTC
占楼,刚开始学习react,感谢楼主
03:06:46 UTC
~(≧▽≦)/~,围观学习
08:30:08 UTC
学习,谢谢
10:16:43 UTC
非常感谢楼主写的这个教程。
有个地方看不大懂
在src/components/author/index.js
path={`${match.url}`}
{/* 这里可以直接用 component={List},不过我们后面要对这里做一些修改 */}
render={() =& }
path={${match.url}}
这个地方 为什么不直接使用match.url 而是使用 ${ match.url
}包裹起来。其中 ${ } 是做什么用的了?
10:28:18 UTC
呃,这个地方,一开始后面是加了个字符串的,后来字符串删了,忘了改了,应该是写成 path={match.url} 更合理一些。
06:06:31 UTC
这是ES6的模版字符串 ——————`我是
${match.url}`
08:23:27 UTC
module.hot && module.hot.accept()
这个为什么要放在index.js 里面,虽然知道是热替换的意思,但是为什么这么用不是很清楚
08:39:35 UTC
Webpack HMR 需要对指定的模块启用HMR,我的这个写法简单粗暴了一些。可以参考Dan Abramov的这篇文章,或者
Updates to nested components work because of how Webpack HMR API is designed. If a module does not specify how to handle updates to itself, the modules that imported it also get included in the hot update bundle, and the updates “bubble up” until they are all accepted by all the modules importing them. If some of the modules don’t end up being accepted, the hot update fails and a warning is printed. To “accept” a dependency, you just call module.hot.accept(‘./name’, callback) which Webpack parses at compile time.
官方的例子是这样的
if (module.hot) {
module.hot.accept('./components/App', () =& {
render(App)
02:32:35 UTC
谢谢,thanks
11:20:34 UTC
qenya 怎么用,能简单介绍一下吗?
02:00:21 UTC
qenya还有一些地方在完善,应该最近会写一篇尽量详细的文档
05:29:15 UTC
$ npm install koa koa-router koa-send http-proxy save-dev 此句save丢了个 -- ,应该是--save
实际项目中怎么用react?比如本项目完成后不考虑后台代码部署时前端怎么用?猜测:把react react-dom压缩版本和编译后的app.js这三个直接放在index.html就行了吗?
08:41:34 UTC
实际项目里面,一般都是html文件是后端模板输出,因为要考虑跨域的问题,所以前端仓库一般没有html文件。js和css,image这些静态文件都是发布到cdn,所以这个示例里面是把js文件代理到本地文件。正式发布的时候,只发布build下面的文件。
07:58:16 UTC
非常感谢楼主的教程,楼主能出一个登录跳转,登录状态保持,路由跳转时权限验证的方案吗?最近在解决这个问题,感觉有一些乱……
13:32:24 UTC
之前在一个项目里面是用一个组件来处理的,大概是这样
// 这里权限点是一个array,可以后端直接输出到页面上,用一个全局变量存储,也可以异步获取,放在redux里
let privileges = ['aaa','bbb']
function Auth(props) {
// 实际项目里面,props.privilege 也用了一个数组,这里简化一点
const checked = privileges.indexOf(props.privilege) &= 0
if (checked) return props.children
return &noscript /&
// 使用的时候
&Auth privilege="aaa" &&div&aaa&/div&&/Auth&
&Auth privilege="ccc" &&div&ccc&/div&&/Auth&
还可以做一个高阶组件
let privileges = ['aaa','bbb']
function authHoc(privilege, Component) {
return function(props) {
const checked = privileges.indexOf(privilege) &= 0
if (checked) return &Component {...props} /&
return &div&没有访问权限&/div&
需要验证权限的组件
function AAA(props) { ... }
export default authHoc('aaa', AAA)

我要回帖

更多关于 react dva 教程 的文章

 

随机推荐