SXX19950910

SXX19950910 查看完整档案

深圳编辑武汉理工大学  |  嵌入式系统工程 编辑wensi  |  WEB前端工程师 编辑 shixiaoxi.cn 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

SXX19950910 提出了问题 · 2019-07-02

解决如何利用正则截取指定字符串

例如现在有个字符串的方法

function test () {
    return(
     <div>测试</div>
    )
}

如何利用js 正则将 (<div>测试</div) 截取出来

关注 3 回答 3

SXX19950910 赞了文章 · 2019-03-20

Flutter 、React Native 、 Ionic、 NativeScript 、小程序和PWA哪个值得学?

彬哥,我看今年前端分了俩方向啊,一个是Flutter和React Native这种,还有就是小程序,淘宝小程序这种微应用,我们公司让我做React Native这块儿,这是不是对我未来发展好些?以前技术栈还是Vue,我最近转React了,刚开始写第一个项目,我还看了看uni-app,学习成本更低一些,国内好像更吃香?到底第该学哪个?

图片描述

前几天有个学生问我上面的问题,因为前端技术领域最不缺的就是新技术,前端开发者比较头疼的不是没有技术可用,而是可用的技术太多了,学不过来,也不知道选哪个。

其实类似的问题争论在PC时代就存在过,太阳底下没有什么新鲜事。甚至在我们生活中都有类似的选择问题。技术学习者要"看技术是技术,然后看技术不是技术"。如果你打算理解一项技术的产生和优缺点,就去追问他的应用场景,或者生活中的应用场景。如果一项技术你找不到生活中的对应,很可能你根本就不理解这项技术,而只是官方demo或者别人示例代码的搬运工。

今天我就说说Flutter 、React Native 、 Ionic、 NativeScript 、小程序和PWA哪个值得学?

话说long long ago,有一个土豪,他有两个儿子大明(IOS)和小明(安卓),土豪琢磨孩子年龄大了该准备婚事了,那就得先有房子,另外的家里的门房和厢房都该翻盖了,所以他想好好的规划一番,对了还要做好防盗,不能让闲杂人等进来影响安全。

他找老大谈话征求老大对房子的意见,老大说我的婚房要豪华气派,要高级装修,要住起来非常的舒服,闭路电视、宽带,能通的就通上,反正也是折腾一回,盖房子就大事,盖一次争取就是村里最好的房子要住很多年呢。

他又找老二谈话征求老二对房子的意见,老二说我又不着急结婚,房子就是个住的地儿,我呢又在上学,反正怎么快怎么来吧,这样周末和暑假还可以过来玩。房子老了,到时候结婚了再翻盖也不迟,现在盖了将来也旧了,对了要方便一些,能让我的朋友们跟我随便造。

土豪一听都觉得有道理,他觉得都挺有道理,于是发了一个公告,寻找各路包工队帮盖房子。

图片描述
于是发了一个招标公告:

本人土豪,家资殷实,诚招各路豪杰帮我家盖房要求:

1.目前有主房两套,其余厢房、门房若干,要求尽可能一次性充分利用材料,一次采购和加工建筑材料,能够多个地方能用。比如搅拌机搅拌一次搅机开一次就能把各个房间用的水泥搅拌出来,能够做出各个房子能用的砖块和柱子。(维护一套代码,能够在跨平台运营,write once,run any where)

2.房子的各项水电煤功能齐全。(能够充分利用设备的功能)

3.房子后期维护方便,最好能让家人或者我的工人们也能改造房子。(学习容易)

4.盖房成本要尽可能低。(开发和维护都省钱)

虽然土豪也觉得条件有些苛刻(跨平台的思路都是受质疑的),但是他想我有钱啊,我想试试(技术是在尝试和质疑中完善进步的)。

还别说,才招标就来了5路人马。每一路人马都身怀绝技。

他们分别是:

html5 派: 以html5 为首,可以说包队历史最老了,其中的PWA团队是新星,受人关注。

native派:java、kotlin两个团队瞄着小明的需求去的,Object-C和Swift团队瞄着老大的需求去的,他们可以说最懂各自瞄准的需求了。

HybridApp派:号称融合了h5派和native派的优势,有很多成熟的住宅建设经验,目前风头正劲。以前有phonegap ,后来有cordova,最近ionic表现也很抢眼。

纯真派:后起之秀,被认为是建筑行业的未来,可以说综合其它各个派系的优点,一出手变表现惊艳,但是初出茅庐,经验上欠缺些。比如 NativeScript 、React Native、Flutter.

小程序派:Taro、WePY 、uni-app 、 mpvue 、 chameleon 这些团队在专业的领域比如旅馆建筑很有建树,实力不容小觑。

公开招标会开始了,招标主人人开始主持:

1.各位都是建筑业的精英,我们实行集中陈述,互相答辩的方式。

第一个问题:

1.目前有主房两套,其余厢房、门房若干,要求尽可能一次性充分利用材料,一次采购和加工建筑材料,能够多个地方能用。比如搅拌机搅拌一次搅机开一次就能把各个房间用的水泥搅拌出来,能够做出各个房子能用的砖块和柱子。(维护一套代码,能够在跨平台运营,write once,run any where)

这个需求各路豪杰谁想说说?

html5 派的PWA直接站起来,因为这个就是他的强项啊!

PWA开口了,大家都知道H5作为资历最老的建筑团队,从一开始就主张所有平台通用,目前我们所有的建筑技术都是可以通用的,不管是住宅,商用还是,写字楼,相比native派只能在具体的领域很厉害,我们还是优势很明显的,而是我们有望成为下一代的通用标准。还记得我们当初我们公司的杰作ajax吗?不就成了数据交互事实标准了吗?

没想到还没到答辩环节,PWA一开口就怼人了,但是这句话确实怼的native派没话说。

HybridApp派,看PWA说完了,说到,大家看看淘宝以及各大主流平台都采用了我们的建筑方式,所以我不想多说,我们的建筑经验第一,相信Flutter你也认同吧。

虽然Flutter被人戳中了软肋,很不爽,但是还是点了点头。对于前辈还是要尊重的。

纯真派见HybridApp派坐下了,站起来,说到,我们这一派都是精英中的精英,比如react native、nativeScrtip,它没好意思说自己,我们在建筑业的表现大家也有目共睹了。

小程序派听着他们的阐述,因为看到土豪的那么多需求感觉自己实现不了,所以心虚想看看再说,所以暂时这一轮弃权。

好,主持人又开始说话了,我们看第二个问题:

2.房子的各项水电煤功能齐全。(能够充分利用设备的功能)

刚才被怼的native派一下子站了起来,我就问一句话,说到能够把房子的功能盖到极致,我们派说第二,哪个团队敢说第一?H5你们敢吗?

因为这个问题native派派优势性很大,所以过。

主持人继续,我们看第三个问题:

3.房子后期维护方便,最好能让家人或者我的工人们也能改造房子。(学习容易)

h5又站起来了,你就说我们干活快不快吧?native?

native无话,

HybridApp派,我们也不慢。

h5很仔细的说,比我呢?

纯真派虽然维护挺快,但是因为自己用了dart所以让人去学还是存在一定困难的。

这个问题又过了,能让家人或者我的工人们也能改造房子。这点心虚,所以不说话。

主持人继续,我们看第四个问题:

4.盖房成本要尽可能低。(开发和维护都省钱)

h5又站起来了,你们就说我盖房子成本低不低吧?native。

native确实贵,被H5怼的真忍不住反驳,你那个也要叫房子?四处漏风,水电都不通。

主持人说一会答辩,土豪皱了皱眉。

HybridApp派和纯真派说,我们也不慢啊。

主持人一看这要杠上,所以说,这样吧我们开始答辩,大家说说你们打算怎么干,依次发言。

html5说到,我打算搭一个木头架子(html),然后外面贴上铁皮,一刷油漆完事(css),里面可以安个电风扇(js)。

native说到,你这个不就是活动板房吗?煤电燃气怎么解决?

PWA说我们打算逐步解决这个问题,比如大明家可以安装空调,因为他家房子架子可以用钢筋的,门房因为比较老旧了,装空调费劲,但是我们可以装空调。

native,笑道,你是在盖猪圈呢吗?native起来发言,我呢打算先挖坑,然后里面水泥浇筑,然后用压路机轧上200趟,然后我去山里拉石头,把石头磨得方方正正,然后垒起来,保证一百年不倒,重点是里面各种高级装修,闭路电视、监控只要是老大想装的,我们全能装。

HybridApp笑了,你上次给村长儿子结婚盖的房子,人家儿子都上小学了,你完工了吗?

主持人说,你打算怎么干?

我呢主结构采用native的那套保证兼顾,装修和墙壁使用h5的,又快有时会。

纯真派问道,那前辈,活动板墙壁冬天取暖很费空调费啊,保暖性能也不好,怎么解决呢?

HybridApp,无话。因为纯真派知道这次小程序就是来陪标的,所以根本不放在眼里,所以阐述自己的想法。

我能打地基的时候采用native的方式,而主体建筑我采用混凝土钢筋结构,虽然没有把石头打磨平整一整块石头那种方式坚固,但是也是绝对坚固能够满足建筑的坚固需求的。同时混凝土浇筑速度快,同时能够解决内部精装和HybridApp的建筑方式保暖、通风问题。

土豪心里似乎明朗了一些,基本上小程序打酱油、PWA不能立马满足所有需求,H5因为功能缺失出局,集中点就在HybridApp和纯真派上。HybridApp虽然成熟,但是确实性能不好,功能上也不如纯真派来的直接,但是又不想重复村长儿子的悲剧。但是一想到纯真派的经验不足,就想多问一些。

于是主持人问到,纯真派你们能详细说说吗?

这句话无疑就相当于说,我觉得纯真派有戏,那么竞争就变成派系内部了,也就是三大主角。

react native、flutter、NativeScript .

react native先发言,

我们隶属于fackbook公司,依托于react,语法上沿用了react,我们使用javascript,可以很好的维护。

不用HybridApp的Webview,彻底摆脱了Webview让人不爽的交互和性能问题,有较强的扩展性,这是因为Native端提供的是基本控件,JS可以自由组合使用,可以直接使用Native原生的动画。

rn说了一堆专业术语显得自己很专业听的土豪一头雾水,但是感觉很厉害。

flutter打断道,说的好像你盖完老大的房子的东西就直接能用到老二家似的,况且你看看你建筑的那些组件的bug还用我说吗?

rn说,我们确实存在一些问题,但是我们在尝试解决。

flutter落井下石,尝试解决?就是一堆坑没解决呗。

rn反击道,就算是没解决也比你还没遇到好吧?再说了你那个破dart,你觉得非专业人员能学会吗?

NativeScript 一听这么说,心虚了,自己的东西比他们两个都复杂,赶紧闭嘴。

看来剩下就是flutter和rn的论战了,不过也没有什么新意,flutter总是强调自己的砖瓦石灰各种模块多丰富,盖房子多快,性能多好,然后rn总强调自己的学习不用学其它语言就js就行。

想到这里,主持人说答辩结束,我总结一下,各方观点:

1.h5开发快修改成本低于,能够跨平台,但是因为标准更新缓慢,所以功能支持不完善。

2.HybridApp,方案成熟,应用很多,性能不是很好(相比flutter和rn)

3.native 功能全,但是不能跨平台,开发慢。

4.rn 学习成本低(比HybridApp高,比flutter低),坑比较多。

5.flutter 优势明显,但是需要学dart,但是组建丰富。

是这样吗?

大家点头。

土豪考虑,综合一听其实王道还是h5,奈何它太慢了,感觉不是别人做的太好了,而是它做的太差了。小程序不考虑了,native不考虑成本太高,开发慢,不跨平台。因为大儿子着急结婚(主营业务、核心需求),所以采纳HybridApp比较稳妥,二儿子不着急结婚所以让flutter去盖(次要业务)。rn就不考虑了,不过好像因为之前的厢房是react盖的,所以react native接受翻盖会容易些,所以这块给他们吧。

所以最终宣布:

大儿子的房子HybridApp中标,

二儿子的房子flutter中标,

厢房和门房因为之前就是react弄的,所以reaact native中标!

3月23号,前端职业规划师Maxwell,在线live为大家讲解跳槽、面试相关的实战攻略。

查看原文

赞 22 收藏 12 评论 3

SXX19950910 赞了文章 · 2019-03-19

CSS尺寸单位介绍

前端开发过程中,尺寸单位是我们必须用到的,下面我们对css中常见的几种尺寸单位px,em,rem,rpx进行逐一介绍
在这之前,需要先对几个概念进行普及介绍

基本概念

(以下概念读起来可能有些晦涩,如果看不懂也没关系)

像素

它不是自然界的物理长度,指基本原色素及其灰度的基本编码。

css中的像素只是一个抽象的单位,在不同的设备或不同的环境中,css中的1px所代表的设备物理像素是不同的。
在为桌面浏览器设计的网页中,我们无需对这个津津计较,但在移动设备上,必须弄明白这点。
在早先的移动设备中,屏幕像素密度都比较低,如iphone3,它的分辨率为320x480,在iphone3上,一个css像素确实是等于一个屏幕物理像素的。
后来随着技术的发展,移动设备的屏幕像素密度越来越高,从iphone4开始,苹果公司便推出了所谓的Retina屏,分辨率提高了一倍,变成640x960,但屏幕尺寸却没变化,这就意味着同样大小的屏幕上,像素却多了一倍,这时,一个css像素是等于两个物理像素的。其他品牌的移动设备也是这个道理。

物理像素

它是显示器(电脑、手机屏幕)最小的物理显示单位,物理像素指的是显示器上最小的点。物理像素的大小取决于屏幕。是一个无法改变的属性。

设备独立像素

我上一张图,你就会理解什么是设备独立像素


就是我们开发过程中使用的css中的px

设备像素比(device pixel radio)

设备像素比 = 物理像素 / 设备独立像素,单位是dpr!(device pixel radio)

Retina屏幕

所谓“Retina”是一种显示标准,是把更多的像素点压缩至一块屏幕里,从而达到更高的分辨率并提高屏幕显示的细腻程度。也被称为视网膜显示屏 ——百度百科

因为Retina屏幕的出现,在pc端默认情况下,css中的1px等于1物理像素,但在移动端1px不一定等于1物理像素,比如说iPhone的设备独立像素是375 667,因为它使用了Retina屏幕,他的dpr是2,所以iPhone 6 的物理像素为 750 1334

在不同的屏幕上(普通屏幕 vs retina屏幕),css中1px所呈现的大小(物理尺寸)是一致的,不同的是1px所对应的物理像素个数是不一致的。

在普通屏幕下,1px 对应 1个物理像素(1:1)。 在Retina屏幕下(dpr=2),1px对应 2x2个物理像素(1:4)。

你会发现,在移动端开发中使用了图片(img标签),2倍图要比1倍图清晰,就是这个缘故

px

默认情况下像素px是相对于屏幕分辨率而言,比如说我们的屏幕分辨率是1440 X 900,说的就是像素1440px X 900px;

这里会遇到另一种情况

浏览器缩放

缩放是缩放CSS像素(缩放比例为1时,一个CSS像素等于一个屏幕像素),就是在屏幕分辨率不变的情况下,用户对浏览进行了缩放

强调一点,用户的缩放行为是对浏览器进行的,缩放的是css像素,而非分辨率,分辨率是屏幕的分辨率,不论你怎么缩放当前页面,屏幕分辨率都不会改变

我们知道在移动端可以在一定程度上控制用户的缩放行为,也可以禁止用户缩放

<meta name="viewport" content="width=device-width,initial-scale=1.0">

content属性值 :

  1. width:可视区域的宽度,值可为数字或关键词device-width
  2. height:可视区域的高度,值可为数字或关键词device-height
  3. intial-scale:页面首次被显示是可视区域的缩放级别,取值1.0则页面按实际尺寸显示,无任何缩放
  4. maximum-scale=1.0, minimum-scale=1.0;可视区域的缩放级别,
  5. maximum-scale用户可将页面放大的程序,1.0将禁止用户放大到实际尺寸之上。
  6. user-scalable:是否可对页面进行缩放,no 禁止缩放

但是在pc端就麻烦了
windows:

  • ctrl + +/-
  • ctrl + 滚轮
  • 浏览器菜单栏

mac:

  • cammond + +/-
  • 浏览器菜单栏

由于浏览器菜单栏属于系统软件权限,没发控制,我们可以通过js控制ctrl/cammond + +/- 或 Windows下ctrl + 滚轮 缩放页面的情况

em

本人在实际开发过程中并没有使用过em单位,但是后面要说的rem是基于em的,所以,对em进行简单介绍

em 是相对长度单位。相对于当前对象内文本的字体尺寸。如当前对行内文本的字体尺寸未被人为设置,则相对于浏览器的默认字体尺寸。

  • em的值并不是固定的;
  • em会继承父级元素的字体大小;
  • 任意浏览器的默认字体高都是16px。所有未经调整的浏览器都符合: 1em=16px。body选择器中声明Font-size=62.5%<==>1em=10px。

看个栗子吧

<body>
  <style>
    html {
      font-size: 50px;
    }

    .my-div {
      width: 100%;
      height: 500px;
      margin-top: 50px;
      background-color: gray;
      font-size: 40px;
    }

    .my-div .parent-font {
      font-size: 30px;
    }

    .my-div .parent-font .child-font {
      font-size: 0.5em;
    }
  </style>
  <div class="my-div">
    <p class="parent-font">
      我是父级文字
      <span class="child-font">我是子级文字</span>
    </p>
  </div>
</body>

html代码中,

第一级,html的 font-size: 50px;

第二级,my-div 的 font-size: 40px;

第三级,parent-font 的 font-size:30px;

第四级,child-font 的 font-size: 0.5em;

我们通过浏览器查看,发现第四级的fong-size为15px;

我们取消第三级parent-font 的字体大小

我们通过浏览器查看,发现第四级的fong-size为20px;

当我们取消第三级font-size后,第三级的字体大小为40px;

所以我们说em的字体大小不是固定的,em的大小取决于父级的字体大小

当父级的字体大小为20px,子级的1em就是20px

当父级的字体大小为30px,子级的1em就是30px

那么说font-size存在着继承父级的特点

我们在第一级html中设置font-size,第二级继承第一级,第三级继承第二级,第四级继承第三级,以此类推

每一级都继承自它的父级,也就是说每一级的em所代表的px大小都不是固定的,因为他们的父级不是同一个,所以em的应用场景并不多。

那么如果是em的都继承自同一个地方,是不是可以解决很多问题呢?

这时候rem出现了

rem

rem 是CSS3的一个相对单位(root em,根em)

使用rem为元素设定字体大小时,仍然是相对大小,但相对的只是HTML根元素

只要html的font-size大小不变,1rem所代表的font-size大小就不会变,rem只取决于html的font-size

rem解决了哪些问题

移动设备的宽度是各种各样的,每个设备的dpr也不同,换句话说就是不同设备每一行的物理像素数不同,能显示的css的px数也不同,

如果我们写一个div,宽度是375px,375px在这个屏幕(iPhone6)上是刚刚满屏,因为这个屏幕宽度刚刚是375px( 设备独立像素),

当我们换另一个宽度是414px的设备(iPhone6Plus)时,这个宽度375px的div就无法铺满这个屏幕,同样的当换一个iPhone5(320px),又会出现滚动条,安卓机的宽度更是五花八门,使用media媒体查询不靠谱,因为它不能覆盖所有的机型宽度

我们之前说rem的大小是相对于html的font-size的,如果html的font-size根据不同设备的宽度做动态计算,问题就会得到解决

我们写页面都是根据UI设计稿来做的,我们假设UI设计稿的宽度是750px(750px是常规宽度,当然也可以是640px或是其他宽度,但是整个项目,宽度必须统一),唯一不变就是就屏幕宽度,我们的html的font-size(rem)只取决于设备宽度

于是

document.documentElement.style.fontSize = 100 * ( document.documentElement.clientWidth / 750) + 'px'

html的font-size:document.documentElement.style.fontSize

设备的宽度:document.documentElement.clientWidth

750:UI设计稿的宽度

为了方便计算我们将font-size x 100,方便计算(乘100不是必须的,我接触过一些项目就不是乘以100,但是UI设计稿中使用了sketch做了动态计算,但我还是建议乘100,不然遇到psd的设计图就很麻烦了)

对上面的js做些完善

const fontFun = function () {
  let docEl = document.documentElement
  let resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize'
  const recalc = function () {
    let clientWidth = docEl.clientWidth
    if (!clientWidth) return
    docEl.style.fontSize = 100 * (clientWidth / 750) + 'px'
  }
  if (!document.addEventListener) return
  window.addEventListener(resizeEvt, recalc, false)
  window.addEventListener('pageshow', recalc, false)
  document.addEventListener('DOMContentLoaded', recalc, false)
}
export {
  fontFun
}

对以上代码不做过多解释

也可以这样写

(function(doc, win) {
  var docEl = doc.documentElement,
    resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize',
    recalc = function() {
      var clientWidth = docEl.clientWidth
      if (!clientWidth) return
      docEl.style.fontSize = 100 * (clientWidth / 750) + 'px'
    }
  if (!doc.addEventListener) return
  win.addEventListener(resizeEvt, recalc, false)
  win.addEventListener('pageshow', recalc, false)
  doc.addEventListener('DOMContentLoaded', recalc, false)
})(document, window)

iPhone5(320px)下html的font-size:42.6667px,1rem=42.6667px

iPhone6(375px)下html的font-size:50px,1rem=50px

iPhone6Plus(414px)下html的font-size:55.2px,1rem=55.2px

rem是继承自html的font-size,但是小程序中没有html,那怎么办呢?

rpx

我不基于html的font-size了,我基于一个别的值就行了,你也不需要计算这个值,我给你计算了,这就是rpx。
最终的效果就是,你开发时在iphon6的设计稿上量了多少px,就写多少rpx就行了,完美适配,perfect!

更多前端资源请关注微信公众号“前端陌上寒”

原文链接

参考链接

【微信小程序】——rpx、px、rem等尺寸间关系浅析

px、物理像素、rem、rpx的关系

阻止pc端浏览器缩放js代码

viewport解释
CSS中大小单位px,em,rem 以及微信小程序的rpx

查看原文

赞 78 收藏 62 评论 0

SXX19950910 赞了文章 · 2018-07-09

手摸手,带你用vue撸后台 系列一(基础篇)

完整项目地址:vue-element-admin
系类文章二:手摸手,带你用vue撸后台 系列二(登录权限篇)
系类文章三:手摸手,带你用vue撸后台 系列三(实战篇)
系类文章四:手摸手,带你用vue撸后台 系列四(vueAdmin 一个极简的后台基础模板)
系类文章:手摸手,带你优雅的使用 icon
系类文章:手摸手,带你封装一个vue component

前言

说好的教程终于来了,第一篇文章主要来说一说在开始写实际业务代码之前的一些准备工作吧,但这里不会教你 webpack 的基础配置,热更新原理是什么,webpack速度优化等等,有需求的请自行 google,相关文章已经很多了。

目录结构

├── build                      // 构建相关  
├── config                     // 配置相关
├── src                        // 源代码
│   ├── api                    // 所有请求
│   ├── assets                 // 主题 字体等静态资源
│   ├── components             // 全局公用组件
│   ├── directive              // 全局指令
│   ├── filtres                // 全局 filter
│   ├── icons                  // 项目所有 svg icons
│   ├── lang                   // 国际化 language
│   ├── mock                   // 项目mock 模拟数据
│   ├── router                 // 路由
│   ├── store                  // 全局 store管理
│   ├── styles                 // 全局样式
│   ├── utils                  // 全局公用方法
│   ├── vendor                 // 公用vendor
│   ├── views                   // view
│   ├── App.vue                // 入口页面
│   ├── main.js                // 入口 加载组件 初始化等
│   └── permission.js          // 权限管理
├── static                     // 第三方不打包资源
│   └── Tinymce                // 富文本
├── .babelrc                   // babel-loader 配置
├── eslintrc.js                // eslint 配置项
├── .gitignore                 // git 忽略项
├── favicon.ico                // favicon图标
├── index.html                 // html模板
└── package.json               // package.json

这里来简单讲一下src文件

api 和 views

简单截取一下公司后台项目,现在后台大概有四五十个 api 模块

如图可见模块有很多,而且随着业务的迭代,模块还会会越来越多。
所以这里建议根据业务模块来划分 views,并且 将views 和 api 两个模块一一对应,从而方便维护。如下图:

如 article 模块下放的都是文章相关的 api,这样不管项目怎么累加,api和views的维护还是清晰的,当然也有一些全区公用的api模块,如七牛upload,remoteSearch等等,这些单独放置就行。

components

这里的 components 放置的都是全局公用的一些组件,如上传组件,富文本等等。一些页面级的组件建议还是放在各自views文件下,方便管理。如图:

store

这里我个人建议不要为了用 vuex 而用 vuex。就拿我司的后台项目来说,它虽然比较庞大,几十个业务模块,几十种权限,但业务之间的耦合度是很低的,文章模块和评论模块几乎是俩个独立的东西,所以根本没有必要使用 vuex 来存储data,每个页面里存放自己的 data 就行。当然有些数据还是需要用 vuex 来统一管理的,如登录token,用户信息,或者是一些全局个人偏好设置等,还是用vuex管理更加的方便,具体当然还是要结合自己的业务场景的。总之还是那句话,不要为了用vuex而用vuex!


webpack

这里是用 vue-cliwebpack-template 为基础模板构建的,如果你对这个有什么疑惑请自行google,相关的配置绍其它的文章已经介详细了,这里就不再展开了。简单说一些需要注意到地方。

jquery (本项目已移除)

管理后台不同于前台项目,会经常用到一些第三方插件,但有些插件是不得不依赖 jquery 的,如市面很多富文本基都是依赖 jquery 的,所以干脆就直接引入到项目中省事(gzip之后只有34kb,而且常年from cache,不要考虑那些吹毛求疵的大小问题,这几kb和提高的开发效率根本不能比)。但是如果第三方库的代码中出现$.xxx或jQuery.xxx或window.jQuery或window.$则会直接报错。要达到类似的效果,则需要使用 webpack 内置的 ProvidePlugin 插件,配置很简单,只需要

new webpack.ProvidePlugin({
  $: 'jquery' ,
  'jQuery': 'jquery'
})

这样当 webpack 碰到 require 的第三方库中出现全局的$、jQeury和window.jQuery 时,就会使用 node_module 下 jquery 包 export 出来的东西了。

alias

当项目逐渐变大之后,文件与文件直接的引用关系会很复杂,这时候就需要使用alias 了。
有的人喜欢alias 指向src目录下,再使用相对路径找文件

resolve: {
  alias: {
    '~': resolve(__dirname, 'src')
  }
}

//使用
import stickTop from '~/components/stickTop'

或者也可以

alias: {
  'src': path.resolve(__dirname, '../src'),
  'components': path.resolve(__dirname, '../src/components'),
  'api': path.resolve(__dirname, '../src/api'),
  'utils': path.resolve(__dirname, '../src/utils'),
  'store': path.resolve(__dirname, '../src/store'),
  'router': path.resolve(__dirname, '../src/router')
}

//使用
import stickTop from 'components/stickTop'
import getArticle from 'api/article'

没有好与坏对与错,纯看个人喜好和团队规范。


ESLint

不管是多人合作还是个人项目,代码规范是很重要的。这样做不仅可以很大程度地避免基本语法错误,也保证了代码的可读性。这所谓工欲善其事,必先利其器,个人推荐 eslint+vscode 来写 vue,绝对有种飞一般的感觉。效果如图:
eslintGif.gif
每次保存,vscode就能标红不符合eslint规则的地方,同时还会做一些简单的自我修正。安装步骤如下:

首先安装eslint插件
eslint1.png

安装并配置完成 ESLint 后,我们继续回到 VSCode 进行扩展设置,依次点击 文件 > 首选项 > 设置 打开 VSCode 配置文件,添加如下配置


    "files.autoSave":"off",
    "eslint.validate": [
       "javascript",
       "javascriptreact",
       "html",
       { "language": "vue", "autoFix": true }
     ],
     "eslint.options": {
        "plugins": ["html"]
     }

这样每次保存的时候就可以根据根目录下.eslintrc.js你配置的eslint规则来检查和做一些简单的fix。这里提供了一份我平时的eslint规则地址,都简单写上了注释。每个人和团队都有自己的代码规范,统一就好了,去打造一份属于自己的eslint 规则上传到npm吧,如饿了么团队的 config,vue的 config

vscode 插件和配置推荐


封装 axios

我们经常遇到一些线上 的bug,但测试环境很难模拟。其实可以通过简单的配置就可以在本地调试线上环境。
这里结合业务封装了axios ,线上代码

import axios from 'axios'
import { Message } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'

// 创建axios实例
const service = axios.create({
  baseURL: process.env.BASE_API, // api的base_url
  timeout: 5000 // 请求超时时间
})

// request拦截器
service.interceptors.request.use(config => {
  // Do something before request is sent
  if (store.getters.token) {
    config.headers['X-Token'] = getToken() // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改
  }
  return config
}, error => {
  // Do something with request error
  console.log(error) // for debug
  Promise.reject(error)
})

// respone拦截器
service.interceptors.response.use(
  response => response,
  /**
  * 下面的注释为通过response自定义code来标示请求状态,当code返回如下情况为权限有问题,登出并返回到登录页
  * 如通过xmlhttprequest 状态码标识 逻辑可写在下面error中
  */
  //  const res = response.data;
  //     if (res.code !== 20000) {
  //       Message({
  //         message: res.message,
  //         type: 'error',
  //         duration: 5 * 1000
  //       });
  //       // 50008:非法的token; 50012:其他客户端登录了;  50014:Token 过期了;
  //       if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
  //         MessageBox.confirm('你已被登出,可以取消继续留在该页面,或者重新登录', '确定登出', {
  //           confirmButtonText: '重新登录',
  //           cancelButtonText: '取消',
  //           type: 'warning'
  //         }).then(() => {
  //           store.dispatch('FedLogOut').then(() => {
  //             location.reload();// 为了重新实例化vue-router对象 避免bug
  //           });
  //         })
  //       }
  //       return Promise.reject('error');
  //     } else {
  //       return response.data;
  //     }
  error => {
    console.log('err' + error)// for debug
    Message({
      message: error.message,
      type: 'error',
      duration: 5 * 1000
    })
    return Promise.reject(error)
  })

export default service
import request from '@/utils/request'

//使用
export function getInfo(params) {
  return request({
    url: '/user/info',
    method: 'get',
    params
  });
}

比如后台项目,每一个请求都是要带 token 来验证权限的,这样封装以下的话我们就不用每个请求都手动来塞 token,或者来做一些统一的异常处理,一劳永逸。
而且因为我们的 api 是根据 env 环境变量动态切换的,如果以后线上出现了bug,我们只需配置一下 @/config/dev.env.js 再重启一下服务,就能在本地模拟线上的环境了。

module.exports = {
    NODE_ENV: '"development"',
    BASE_API: '"https://api-dev"', //修改为'"https://api-prod"'就行了
    APP_ORIGIN: '"https://wallstreetcn.com"' //为公司打个广告 pc站为vue+ssr
}

妈妈再也不用担心我调试线上bug了。
当然这里只是简单举了个例子,axios还可以执行多个并发请求,拦截器什么的,大家自行去研究吧。


多环境

vue-cli 默认只提供了devprod两种环境。但其实正真的开发流程可能还会多一个sit或者stage环境,就是所谓的测试环境和预发布环境。所以我们就要简单的修改一下代码。其实很简单就是设置不同的环境变量

"build:prod": "NODE_ENV=production node build/build.js",
"build:sit": "NODE_ENV=sit node build/build.js",

之后在代码里自行判断,想干就干啥

var env = process.env.NODE_ENV === 'production' ? config.build.prodEnv : config.build.sitEnv

新版的 vue-cli 也内置了 webpack-bundle-analyzer 一个模块分析的东西,相当的好用。使用方法也很简单,和之前一样封装一个 npm script 就可以。

//package.json
 "build:sit-preview": "cross-env NODE_ENV=production env_config=sit npm_config_preview=true  npm_config_report=true node build/build.js"

//之后通过process.env.npm_config_report来判断是否来启用webpack-bundle-analyzer

var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
webpackConfig.plugins.push(new BundleAnalyzerPlugin())

效果图
analyzer.png
webpack-bundle-analyzer这个插件还是很有用的,对后期的代码优化什么的,最重要的是它够装逼~


前后端交互

每个公司都有自己一套的开发流程,没有绝对的好与坏。这里我来讲讲我司的前后端交互流程。

跨域问题

首先前后端交互不可避免的就会遇到跨域问题,我司现在全是用 cors来解决的,如果你司后端嫌麻烦不肯配置的话,dev环境也可以通过
webpack-dev-serverproxy来解决,开发环境用nginx反代理一下就好了,具体配置这里就不展开了。

前后端的交互问题

其实大家也知道,平时的开发中交流成本占据了我们很大一部分时间,但前后端如果有一个好的协作方式的话能解决很多时间。我司开发流程都是前后端和产品一起开会讨论项目,之后后端根据需求,首先定义数据格式和api,然后 mock api 生成好文档,我们前端才是对接接口的。这里推荐一个文档生成器 swagger
swagger是一个REST APIs文档生成工具,可以在许多不同的平台上从代码注释中自动生成,开源,支持大部分语言,社区好,总之就是一个强大,如下图的api 文档(swagger自动生成,ui忽略)


api 地址,需要传是没参数,需要的传参类型,返回的数据格式什么都一清二楚了。

前端自行mock

如果后端不肯来帮你 mock 数据的话,前端自己来 mock 也是很简单的。你可以使用mock server 或者使用 mockjs + rap 也是很方便的。
不久前出的 easy-mock也相当的不错,还能结合 swagger。
我们大前端终于不用再看后端的脸色了~

iconfont

element-ui 默认的icon不是很多,这里要安利一波阿里的iconfont简直是神器,不管是公司项目还是个人项目都在使用。它提供了png,ai,svg三种格式,同时使用也支持unicode,font-class,symbol三种方式。由于是管理后台对兼容性要求不高,楼主平时都喜欢用symbol,晒一波我司后台的图标(都是楼主自己发挥的)。
iconfont.png
详细具体的使用可以见文章 手摸手,带你优雅的使用 icon


router-view

different router the same component vue。真实的业务场景中,这种情况很多。比如router-view.png
我创建和编辑的页面使用的是同一个component,默认情况下当这两个页面切换时并不会触发vue的created或者mounted钩子,官方说你可以通过watch $route的变化来做处理,但其实说真的还是蛮麻烦的。后来发现其实可以简单的在 router-view上加上一个唯一的key,来保证路由切换时都会重新渲染触发钩子了。这样简单的多了。

<router-view :key="key"></router-view>

computed: {
    key() {
        return this.$route.name !== undefined? this.$route.name + +new Date(): this.$route + +new Date()
    }
 }

优化

有些人会觉得现在构建是不是有点慢,我司现在技术栈是容器服务,后台项目会把dist文件夹里的东西都会打包成一个docker镜像,基本步骤为

npm install
npm run build:prod
加打包镜像,一共是耗时如下

Paste_Image.png

还是属于能接受时间的范围。
主站PC站基于nodejs、Vue实现服务端渲染,所以不仅需要依赖nodejs,而且需要利用pm2进行nodejs生命周期的管理。为了加速线上镜像构建的速度,我们利用taobao源 https://registry.npm.taobao.org 进行加速, 并且将一些常见的npm依赖打入了基础镜像,避免每次都需要重新下载。
这里注意下 建议不要使用cnpm install或者update 它的包都是一个link,反正会有各种诡异的bug,这里建议这样使用

npm install --registry=https://registry.npm.taobao.org

如果你觉得慢还是有可优化的空间如使用webpack dll 或者把那些第三方vendor单独打包 external出去,或者我司现在用的是http2 可以使用AggressiveSplittingPlugin等等,这里有需求的可以自行优化。


占坑

常规占坑,这里是手摸手,带你用vue撸后台系列。
完整项目地址:vue-element-admin
系类文章二:手摸手,带你用vue撸后台 系列二(登录权限篇)
系类文章三:手摸手,带你用vue撸后台 系列三(实战篇)
系类文章四:手摸手,带你用vue撸后台 系列四(vueAdmin 一个极简的后台基础模板)
系类文章:手摸手,带你优雅的使用 icon
系类文章:手摸手,带你封装一个vue component
楼主个人免费圈子

查看原文

赞 497 收藏 978 评论 55

SXX19950910 赞了文章 · 2018-07-08

React + Ramda: 函数式编程尝鲜

原文:Functional Components with React stateless functions and Ramda

阅读本文需要的知识储备:

  • 函数式编程基本概念(组合、柯里化、透镜)
  • React 基本知识(组件、状态、属性、JSX)
  • ES6 基本知识(class、箭头函数)

React 无状态函数

React 组件最常见的定义方法:

const  List = React.createClass({
  render: function() {
    return (<ul>{this.props.children}</ul>);
  }
});

或者使用 ES6 类语法:

class List extends React.Component {
  render() {
    return (<ul>{this.props.children}</ul>);
  }
}

又或者使用普通的 JS 函数:

// 无状态函数语法
const List = function(children) {
  return (<ul>{children}</ul>);
};

//ES6 箭头函数语法
const List = (children) => (<ul>{children}</ul>);

React 官方文档对这种组件做了以下说明:

这种简化的组件 API 适用于仅依赖属性的纯函数组件。这些组件不允许拥有内部状态,不会生成组件实例,也没有组件的生命周期方法。它们只对输入进行纯函数转换。不过开发者仍然可以为它们指定 .propTypes.defaultProps,只需要设置为函数的属性就可以了,就跟在 ES6 类上设置一样。

同时也说到:

理想情况下,大部分的组件都应该是无状态的函数,因为在未来我们可能会针对这类组件做性能优化,避免不必要的检查和内存分配。所以推荐大家尽可能的使用这种模式来开发。

是不是觉得挺有趣的?

React 社区似乎更加关注通过 classcreateClass 方式来创建组件,今天让我们来尝鲜一下无状态组件。

App 容器

首先让我们来创建一个函数式 App 容器组件,它接受一个表示应用状态的对象作为参数:

import React from 'react';
import ReactDOM from 'react-dom';

const App = appState => (<div className="container">
  <h1>App name</h1>
  <p>Some children here...</p>
</div>);

然后,定义一个 render 方法,作为 App 函数的属性:

import React from 'react';
import ReactDOM from 'react-dom';
import R from 'ramda';

const App = appState => (<div className="container">
  <h1>App name</h1>
  <p>Some children here...</p>
</div>);

App.render = R.curry((node, props) => ReactDOM.render(<App {...props}/>, node));

export default App;

等等!有点看不明白了!
为什么我们需要一个柯里化的渲染函数?又为什么渲染函数的参数顺序反过来了?
别急别急,这里唯一要说明的是,由于我们使用的是无状态组件,所以状态必须由其它地方来维护。也就是说,状态必须由外部维护,然后通过属性的方式传递给组件。
让我们来看一个具体的计时器例子。

无状态计时器组件

一个简单的计时器组件只接受一个属性 secondsElapsed

import React from 'react';

export default ({ secondsElapsed }) => (<div className="well">
  Seconds Elapsed: {secondsElapsed}
</div>);

把它添加到 App 中:

import React from 'react';
import ReactDOM from 'react-dom';
import R from 'ramda';
import Timer from './timer';

const App = appState => (<div className="container">
  <h1>App name</h1>
  <Timer secondsElapsed={appState.secondsElapsed} />
</div>);

App.render = R.curry((node, props) => ReactDOM.render(<App {...props}/>, node));

export default App;

最后,创建 main.js 来渲染 App

import App from './components/app';
const render = App.render(document.getElementById('app'));

let appState = {
  secondsElapsed: 0
};

//first render
render(appState);

setInterval(() => {
  appState.secondsElapsed++;
  render(appState);
}, 1000);

在进一步说明之前,我想说,appState.secondElapsed++ 这种修改状态的方式让我觉得非常不爽,不过稍后我们会使用更好的方式来实现。

这里我们可以看出,render 其实就是用新属性来重新渲染组件的语法糖。下面这行代码:

const render = App.render(document.getElementById(‘app’));

会返回一个具有 (props) => ReactDOM.render(...) 函数签名的函数。
这里并没有什么太难理解的内容。每当 secondsElapsed 的值改变后,我们只需要重新调用 render 方法即可:

setInterval(() => {
  appState.secondsElapsed++;
  render(appState);
}, 1000);

现在,让我们来实现一个类似 Redux 风格的归约函数,以不断的递增 secondsElapsed。归约函数是不允许修改当前状态的,所有最简单的实现方式就是 currentState -> newState

这里我们使用 Ramda 的透镜(Lens)来实现 incSecondsElapsed 函数:

const secondsElapsedLens = R.lensProp('secondsElapsed');
const incSecondsElapsed = R.over(secondsElapsedLens, R.inc);

setInterval(() => {
  appState = incSecondsElapsed(appState);
  render(appState);
}, 1000);

第一行代码中,我们创建了一个透镜:

const secondsElapsedLens = R.lensProp('secondsElapsed');

简单来说,透镜是一种专注于给定属性的方式,而不关心该属性到底是在哪个对象上,这种方式便于代码复用。当我们需要把透镜应用于对象上时,可以有以下操作:

  • View
R.view(secondsElapsedLens, { secondsElapsed: 10 });  //=> 10
  • Set
R.set(secondsElapsedLens, 11, { secondsElapsed: 10 });  //=> 11
  • 以给定函数来设置
R.over(secondsElapsedLens, R.inc, { secondsElapsed: 10 });  //=> 11

我们实现的 incSecondsElapsed 就是对 R.over 进行局部应用的结果。

const incSecondsElapsed = R.over(secondsElapsedLens, R.inc);

该行代码会返回一个新函数,一旦调用时传入 appState,就会把 R.inc 应用在 secondsElapsed 属性上。

需要注意的是,Ramda 从来都不会修改对象,所以我们需要自己来处理脏活:

appState = incSecondsElapsed(appState);

如果想支持 undo/redo ,只需要维护一个历史数组记录下每一次状态即可,或者使用 Redux 。

目前为止,我们已经品尝了柯里化和透镜,下面让我们继续品尝组合

组合 React 无状态组件

当我第一次读到 React 无状态组件时,我就在想能否使用 R.compose 来组合这些函数呢?答案很明显,当然是 YES 啦:)

让我们从一个 TodoList 组件开始:

const TodoList = React.createClass({
  render: function() {
    const createItem = function(item) {
      return (<li key={item.id}>{item.text}</li>);
    };

    return (<div className="panel panel-default">
      <div className="panel-body">
        <ul>
          {this.props.items.map(createItem)}
        </ul>
      </div>
    </div>);
  }
});

现在问题来了,TodoList 能否通过组合更小的、可复用的组件来实现呢?当然,我们可以把它分割成 3 个小组件:

  • 容器
const Container = children => (<div className="panel panel-default">
  <div className="panel-body">
    {children}
  </div>
</div>);
  • 列表
const List = children => (<ul>
  {children}
</ul>);
  • 列表项
const ListItem = ({ id, text }) => (<li key={id}>
  <span>{text}</span>
</li>);

现在,我们来一步一步看,请一定要在理解了每一步之后才往下看:

Container(<h1>Hello World!</h1>);

/**
 *  <div className="panel panel-default">
 *    <div className="panel-body">
 *      <h1>Hello World!</h1>
 *    </div>
 *  </div>
 */

Container(List(<li>Hello World!</li>));

/**
 *  <div className="panel panel-default">
 *    <div className="panel-body">
 *      <ul>
 *        <li>Hello World!</li>
 *      </ul>
 *    </div>
 *  </div>
 */

const TodoItem = {
  id: 123,
  text: 'Buy milk'
};
Container(List(ListItem(TodoItem)));

/**
 *  <div className="panel panel-default">
 *    <div className="panel-body">
 *      <ul>
 *        <li>
 *          <span>Buy milk</span>
 *        </li>
 *      </ul>
 *    </div>
 *  </div>
 */

没有什么太特别的,只不过是一步一步的传参调用。

接着,让我们来做一些组合的练习:

R.compose(Container, List)(<li>Hello World!</li>);

/**
 *  <div className="panel panel-default">
 *    <div className="panel-body">
 *      <ul>
 *        <li>Hello World!</li>
 *      </ul>
 *    </div>
 *  </div>
 */

const ContainerWithList = R.compose(Container, List);
R.compose(ContainerWithList, ListItem)({id: 123, text: 'Buy milk'});

/**
 *  <div className="panel panel-default">
 *    <div className="panel-body">
 *      <ul>
 *        <li>
 *          <span>Buy milk</span>
 *        </li>
 *      </ul>
 *    </div>
 *  </div>
 */

const TodoItem = {
  id: 123,
  text: 'Buy milk'
};
const TodoList = R.compose(Container, List, ListItem);
TodoList(TodoItem);

/**
 *  <div className="panel panel-default">
 *    <div className="panel-body">
 *      <ul>
 *        <li>
 *          <span>Buy milk</span>
 *        </li>
 *      </ul>
 *    </div>
 *  </div>
 */

发现了没!TodoList 组件已经被表示成了 ContainerListListItem 的组合了:

const TodoList = R.compose(Container, List, ListItem);

等等!TodoList 这个组件只接受一个 todo 对象,但是我们需要的是映射整个 todos 数组:

const mapTodos = function(todos) {
  return todos.map(function(todo) {
    return ListItem(todo);
  });
};
const TodoList = R.compose(Container, List, mapTodos);
const mock = [
  {id: 1, text: 'One'},
  {id: 1, text: 'Two'},
  {id: 1, text: 'Three'}
];
TodoList(mock);

/**
 *  <div className="panel panel-default">
 *    <div className="panel-body">
 *      <ul>
 *        <li>
 *          <span>One</span>
 *        </li>
 *        <li>
 *          <span>Two</span>
 *        </li>
 *        <li>
 *          <span>Three</span>
 *        </li>
 *      </ul>
 *    </div>
 *  </div>
 */

能否以更函数式的方式简化 mapTodos 函数?

// 下面的代码
return todos.map(function(todo) {
  return ListItem(todo);
});

// 等效于
return todos.map(ListItem);

// 所以变成了
const mapTodos = function(todos) {
  return todos.map(ListItem);
};

// 等效于使用 Ramda 的方式
const mapTodos = function(todos) {
  return R.map(ListItem, todos);
};

// 注意 Ramda 的两个特点:
// - Ramda 函数默认都支持柯里化
// - 为了便于柯里化,Ramda 函数的参数进行了特定排列,
//   待处理的数据通常放在最后
// 因此:
const mapTodos = R.map(ListItem);

//此时就不再需要 mapTodos 了:
const TodoList = R.compose(Container, List, R.map(ListItem));

哒哒哒!完整的 TodoList 实现代码如下:

import React from 'React';
import R from 'ramda';

const Container = children => (<div className="panel panel-default">
  <div className="panel-body">
    {children}
  </div>
</div>);

const List = children => (<ul>
  {children}
</ul>);

const ListItem = ({ id, text }) => (<li key={id}>
  <span>{text}</span>
</li>);

const TodoList = R.compose(Container, List, R.map(ListItem));

export default TodoList;

其实,还少了一样东西,不过马上就会加上。在那之前让我们先来做些准备:

  • 添加测试数据到应用状态
let appState = {
  secondsElapsed: 0,
  todos: [
    {id: 1, text: 'Buy milk'},
    {id: 2, text: 'Go running'},
    {id: 3, text: 'Rest'}
  ]
};
  • 添加 TodoListApp
import TodoList from './todo-list';
const App = appState => (<div className="container">
  <h1>App name</h1>
  <Timer secondsElapsed={appState.secondsElapsed} />
  <TodoList todos={appState.todos} />
</div>);

TodoList 接受的是一个 todos 数组,但是这里却是:

<TodoList todos={appState.todos} />

我们把列表传递作为一个属性,所以等效于:

TodoList({todos: appState.todos});

因此,我们必须修改 TodoList,以便让它接受一个对象并且取出 todos 属性:

const TodoList = R.compose(Container, List, R.map(ListItem), R.prop('todos'));

这里并没有什么高深技术。仅仅是从右到左的组合,R.prop('todos') 会返回一个函数,调用该函数会返回其作为的参数对象的 todos 属性,接着把该属性值传递给 R.map(ListItem),如此往复:)

以上就是本文的尝鲜内容。希望能对大家有所帮助,这仅仅是我基于 React 和 Ramda 做的一部分实验。未来,我会努力尝试覆盖高阶组件和使用 Transducer 来转换无状态函数。

完整源码线上演示代码(译者新增)。

查看原文

赞 10 收藏 12 评论 0

SXX19950910 赞了文章 · 2018-07-08

React + Ramda: 函数式编程尝鲜

原文:Functional Components with React stateless functions and Ramda

阅读本文需要的知识储备:

  • 函数式编程基本概念(组合、柯里化、透镜)
  • React 基本知识(组件、状态、属性、JSX)
  • ES6 基本知识(class、箭头函数)

React 无状态函数

React 组件最常见的定义方法:

const  List = React.createClass({
  render: function() {
    return (<ul>{this.props.children}</ul>);
  }
});

或者使用 ES6 类语法:

class List extends React.Component {
  render() {
    return (<ul>{this.props.children}</ul>);
  }
}

又或者使用普通的 JS 函数:

// 无状态函数语法
const List = function(children) {
  return (<ul>{children}</ul>);
};

//ES6 箭头函数语法
const List = (children) => (<ul>{children}</ul>);

React 官方文档对这种组件做了以下说明:

这种简化的组件 API 适用于仅依赖属性的纯函数组件。这些组件不允许拥有内部状态,不会生成组件实例,也没有组件的生命周期方法。它们只对输入进行纯函数转换。不过开发者仍然可以为它们指定 .propTypes.defaultProps,只需要设置为函数的属性就可以了,就跟在 ES6 类上设置一样。

同时也说到:

理想情况下,大部分的组件都应该是无状态的函数,因为在未来我们可能会针对这类组件做性能优化,避免不必要的检查和内存分配。所以推荐大家尽可能的使用这种模式来开发。

是不是觉得挺有趣的?

React 社区似乎更加关注通过 classcreateClass 方式来创建组件,今天让我们来尝鲜一下无状态组件。

App 容器

首先让我们来创建一个函数式 App 容器组件,它接受一个表示应用状态的对象作为参数:

import React from 'react';
import ReactDOM from 'react-dom';

const App = appState => (<div className="container">
  <h1>App name</h1>
  <p>Some children here...</p>
</div>);

然后,定义一个 render 方法,作为 App 函数的属性:

import React from 'react';
import ReactDOM from 'react-dom';
import R from 'ramda';

const App = appState => (<div className="container">
  <h1>App name</h1>
  <p>Some children here...</p>
</div>);

App.render = R.curry((node, props) => ReactDOM.render(<App {...props}/>, node));

export default App;

等等!有点看不明白了!
为什么我们需要一个柯里化的渲染函数?又为什么渲染函数的参数顺序反过来了?
别急别急,这里唯一要说明的是,由于我们使用的是无状态组件,所以状态必须由其它地方来维护。也就是说,状态必须由外部维护,然后通过属性的方式传递给组件。
让我们来看一个具体的计时器例子。

无状态计时器组件

一个简单的计时器组件只接受一个属性 secondsElapsed

import React from 'react';

export default ({ secondsElapsed }) => (<div className="well">
  Seconds Elapsed: {secondsElapsed}
</div>);

把它添加到 App 中:

import React from 'react';
import ReactDOM from 'react-dom';
import R from 'ramda';
import Timer from './timer';

const App = appState => (<div className="container">
  <h1>App name</h1>
  <Timer secondsElapsed={appState.secondsElapsed} />
</div>);

App.render = R.curry((node, props) => ReactDOM.render(<App {...props}/>, node));

export default App;

最后,创建 main.js 来渲染 App

import App from './components/app';
const render = App.render(document.getElementById('app'));

let appState = {
  secondsElapsed: 0
};

//first render
render(appState);

setInterval(() => {
  appState.secondsElapsed++;
  render(appState);
}, 1000);

在进一步说明之前,我想说,appState.secondElapsed++ 这种修改状态的方式让我觉得非常不爽,不过稍后我们会使用更好的方式来实现。

这里我们可以看出,render 其实就是用新属性来重新渲染组件的语法糖。下面这行代码:

const render = App.render(document.getElementById(‘app’));

会返回一个具有 (props) => ReactDOM.render(...) 函数签名的函数。
这里并没有什么太难理解的内容。每当 secondsElapsed 的值改变后,我们只需要重新调用 render 方法即可:

setInterval(() => {
  appState.secondsElapsed++;
  render(appState);
}, 1000);

现在,让我们来实现一个类似 Redux 风格的归约函数,以不断的递增 secondsElapsed。归约函数是不允许修改当前状态的,所有最简单的实现方式就是 currentState -> newState

这里我们使用 Ramda 的透镜(Lens)来实现 incSecondsElapsed 函数:

const secondsElapsedLens = R.lensProp('secondsElapsed');
const incSecondsElapsed = R.over(secondsElapsedLens, R.inc);

setInterval(() => {
  appState = incSecondsElapsed(appState);
  render(appState);
}, 1000);

第一行代码中,我们创建了一个透镜:

const secondsElapsedLens = R.lensProp('secondsElapsed');

简单来说,透镜是一种专注于给定属性的方式,而不关心该属性到底是在哪个对象上,这种方式便于代码复用。当我们需要把透镜应用于对象上时,可以有以下操作:

  • View
R.view(secondsElapsedLens, { secondsElapsed: 10 });  //=> 10
  • Set
R.set(secondsElapsedLens, 11, { secondsElapsed: 10 });  //=> 11
  • 以给定函数来设置
R.over(secondsElapsedLens, R.inc, { secondsElapsed: 10 });  //=> 11

我们实现的 incSecondsElapsed 就是对 R.over 进行局部应用的结果。

const incSecondsElapsed = R.over(secondsElapsedLens, R.inc);

该行代码会返回一个新函数,一旦调用时传入 appState,就会把 R.inc 应用在 secondsElapsed 属性上。

需要注意的是,Ramda 从来都不会修改对象,所以我们需要自己来处理脏活:

appState = incSecondsElapsed(appState);

如果想支持 undo/redo ,只需要维护一个历史数组记录下每一次状态即可,或者使用 Redux 。

目前为止,我们已经品尝了柯里化和透镜,下面让我们继续品尝组合

组合 React 无状态组件

当我第一次读到 React 无状态组件时,我就在想能否使用 R.compose 来组合这些函数呢?答案很明显,当然是 YES 啦:)

让我们从一个 TodoList 组件开始:

const TodoList = React.createClass({
  render: function() {
    const createItem = function(item) {
      return (<li key={item.id}>{item.text}</li>);
    };

    return (<div className="panel panel-default">
      <div className="panel-body">
        <ul>
          {this.props.items.map(createItem)}
        </ul>
      </div>
    </div>);
  }
});

现在问题来了,TodoList 能否通过组合更小的、可复用的组件来实现呢?当然,我们可以把它分割成 3 个小组件:

  • 容器
const Container = children => (<div className="panel panel-default">
  <div className="panel-body">
    {children}
  </div>
</div>);
  • 列表
const List = children => (<ul>
  {children}
</ul>);
  • 列表项
const ListItem = ({ id, text }) => (<li key={id}>
  <span>{text}</span>
</li>);

现在,我们来一步一步看,请一定要在理解了每一步之后才往下看:

Container(<h1>Hello World!</h1>);

/**
 *  <div className="panel panel-default">
 *    <div className="panel-body">
 *      <h1>Hello World!</h1>
 *    </div>
 *  </div>
 */

Container(List(<li>Hello World!</li>));

/**
 *  <div className="panel panel-default">
 *    <div className="panel-body">
 *      <ul>
 *        <li>Hello World!</li>
 *      </ul>
 *    </div>
 *  </div>
 */

const TodoItem = {
  id: 123,
  text: 'Buy milk'
};
Container(List(ListItem(TodoItem)));

/**
 *  <div className="panel panel-default">
 *    <div className="panel-body">
 *      <ul>
 *        <li>
 *          <span>Buy milk</span>
 *        </li>
 *      </ul>
 *    </div>
 *  </div>
 */

没有什么太特别的,只不过是一步一步的传参调用。

接着,让我们来做一些组合的练习:

R.compose(Container, List)(<li>Hello World!</li>);

/**
 *  <div className="panel panel-default">
 *    <div className="panel-body">
 *      <ul>
 *        <li>Hello World!</li>
 *      </ul>
 *    </div>
 *  </div>
 */

const ContainerWithList = R.compose(Container, List);
R.compose(ContainerWithList, ListItem)({id: 123, text: 'Buy milk'});

/**
 *  <div className="panel panel-default">
 *    <div className="panel-body">
 *      <ul>
 *        <li>
 *          <span>Buy milk</span>
 *        </li>
 *      </ul>
 *    </div>
 *  </div>
 */

const TodoItem = {
  id: 123,
  text: 'Buy milk'
};
const TodoList = R.compose(Container, List, ListItem);
TodoList(TodoItem);

/**
 *  <div className="panel panel-default">
 *    <div className="panel-body">
 *      <ul>
 *        <li>
 *          <span>Buy milk</span>
 *        </li>
 *      </ul>
 *    </div>
 *  </div>
 */

发现了没!TodoList 组件已经被表示成了 ContainerListListItem 的组合了:

const TodoList = R.compose(Container, List, ListItem);

等等!TodoList 这个组件只接受一个 todo 对象,但是我们需要的是映射整个 todos 数组:

const mapTodos = function(todos) {
  return todos.map(function(todo) {
    return ListItem(todo);
  });
};
const TodoList = R.compose(Container, List, mapTodos);
const mock = [
  {id: 1, text: 'One'},
  {id: 1, text: 'Two'},
  {id: 1, text: 'Three'}
];
TodoList(mock);

/**
 *  <div className="panel panel-default">
 *    <div className="panel-body">
 *      <ul>
 *        <li>
 *          <span>One</span>
 *        </li>
 *        <li>
 *          <span>Two</span>
 *        </li>
 *        <li>
 *          <span>Three</span>
 *        </li>
 *      </ul>
 *    </div>
 *  </div>
 */

能否以更函数式的方式简化 mapTodos 函数?

// 下面的代码
return todos.map(function(todo) {
  return ListItem(todo);
});

// 等效于
return todos.map(ListItem);

// 所以变成了
const mapTodos = function(todos) {
  return todos.map(ListItem);
};

// 等效于使用 Ramda 的方式
const mapTodos = function(todos) {
  return R.map(ListItem, todos);
};

// 注意 Ramda 的两个特点:
// - Ramda 函数默认都支持柯里化
// - 为了便于柯里化,Ramda 函数的参数进行了特定排列,
//   待处理的数据通常放在最后
// 因此:
const mapTodos = R.map(ListItem);

//此时就不再需要 mapTodos 了:
const TodoList = R.compose(Container, List, R.map(ListItem));

哒哒哒!完整的 TodoList 实现代码如下:

import React from 'React';
import R from 'ramda';

const Container = children => (<div className="panel panel-default">
  <div className="panel-body">
    {children}
  </div>
</div>);

const List = children => (<ul>
  {children}
</ul>);

const ListItem = ({ id, text }) => (<li key={id}>
  <span>{text}</span>
</li>);

const TodoList = R.compose(Container, List, R.map(ListItem));

export default TodoList;

其实,还少了一样东西,不过马上就会加上。在那之前让我们先来做些准备:

  • 添加测试数据到应用状态
let appState = {
  secondsElapsed: 0,
  todos: [
    {id: 1, text: 'Buy milk'},
    {id: 2, text: 'Go running'},
    {id: 3, text: 'Rest'}
  ]
};
  • 添加 TodoListApp
import TodoList from './todo-list';
const App = appState => (<div className="container">
  <h1>App name</h1>
  <Timer secondsElapsed={appState.secondsElapsed} />
  <TodoList todos={appState.todos} />
</div>);

TodoList 接受的是一个 todos 数组,但是这里却是:

<TodoList todos={appState.todos} />

我们把列表传递作为一个属性,所以等效于:

TodoList({todos: appState.todos});

因此,我们必须修改 TodoList,以便让它接受一个对象并且取出 todos 属性:

const TodoList = R.compose(Container, List, R.map(ListItem), R.prop('todos'));

这里并没有什么高深技术。仅仅是从右到左的组合,R.prop('todos') 会返回一个函数,调用该函数会返回其作为的参数对象的 todos 属性,接着把该属性值传递给 R.map(ListItem),如此往复:)

以上就是本文的尝鲜内容。希望能对大家有所帮助,这仅仅是我基于 React 和 Ramda 做的一部分实验。未来,我会努力尝试覆盖高阶组件和使用 Transducer 来转换无状态函数。

完整源码线上演示代码(译者新增)。

查看原文

赞 10 收藏 12 评论 0

SXX19950910 赞了文章 · 2018-07-08

React + Ramda: 函数式编程尝鲜

原文:Functional Components with React stateless functions and Ramda

阅读本文需要的知识储备:

  • 函数式编程基本概念(组合、柯里化、透镜)
  • React 基本知识(组件、状态、属性、JSX)
  • ES6 基本知识(class、箭头函数)

React 无状态函数

React 组件最常见的定义方法:

const  List = React.createClass({
  render: function() {
    return (<ul>{this.props.children}</ul>);
  }
});

或者使用 ES6 类语法:

class List extends React.Component {
  render() {
    return (<ul>{this.props.children}</ul>);
  }
}

又或者使用普通的 JS 函数:

// 无状态函数语法
const List = function(children) {
  return (<ul>{children}</ul>);
};

//ES6 箭头函数语法
const List = (children) => (<ul>{children}</ul>);

React 官方文档对这种组件做了以下说明:

这种简化的组件 API 适用于仅依赖属性的纯函数组件。这些组件不允许拥有内部状态,不会生成组件实例,也没有组件的生命周期方法。它们只对输入进行纯函数转换。不过开发者仍然可以为它们指定 .propTypes.defaultProps,只需要设置为函数的属性就可以了,就跟在 ES6 类上设置一样。

同时也说到:

理想情况下,大部分的组件都应该是无状态的函数,因为在未来我们可能会针对这类组件做性能优化,避免不必要的检查和内存分配。所以推荐大家尽可能的使用这种模式来开发。

是不是觉得挺有趣的?

React 社区似乎更加关注通过 classcreateClass 方式来创建组件,今天让我们来尝鲜一下无状态组件。

App 容器

首先让我们来创建一个函数式 App 容器组件,它接受一个表示应用状态的对象作为参数:

import React from 'react';
import ReactDOM from 'react-dom';

const App = appState => (<div className="container">
  <h1>App name</h1>
  <p>Some children here...</p>
</div>);

然后,定义一个 render 方法,作为 App 函数的属性:

import React from 'react';
import ReactDOM from 'react-dom';
import R from 'ramda';

const App = appState => (<div className="container">
  <h1>App name</h1>
  <p>Some children here...</p>
</div>);

App.render = R.curry((node, props) => ReactDOM.render(<App {...props}/>, node));

export default App;

等等!有点看不明白了!
为什么我们需要一个柯里化的渲染函数?又为什么渲染函数的参数顺序反过来了?
别急别急,这里唯一要说明的是,由于我们使用的是无状态组件,所以状态必须由其它地方来维护。也就是说,状态必须由外部维护,然后通过属性的方式传递给组件。
让我们来看一个具体的计时器例子。

无状态计时器组件

一个简单的计时器组件只接受一个属性 secondsElapsed

import React from 'react';

export default ({ secondsElapsed }) => (<div className="well">
  Seconds Elapsed: {secondsElapsed}
</div>);

把它添加到 App 中:

import React from 'react';
import ReactDOM from 'react-dom';
import R from 'ramda';
import Timer from './timer';

const App = appState => (<div className="container">
  <h1>App name</h1>
  <Timer secondsElapsed={appState.secondsElapsed} />
</div>);

App.render = R.curry((node, props) => ReactDOM.render(<App {...props}/>, node));

export default App;

最后,创建 main.js 来渲染 App

import App from './components/app';
const render = App.render(document.getElementById('app'));

let appState = {
  secondsElapsed: 0
};

//first render
render(appState);

setInterval(() => {
  appState.secondsElapsed++;
  render(appState);
}, 1000);

在进一步说明之前,我想说,appState.secondElapsed++ 这种修改状态的方式让我觉得非常不爽,不过稍后我们会使用更好的方式来实现。

这里我们可以看出,render 其实就是用新属性来重新渲染组件的语法糖。下面这行代码:

const render = App.render(document.getElementById(‘app’));

会返回一个具有 (props) => ReactDOM.render(...) 函数签名的函数。
这里并没有什么太难理解的内容。每当 secondsElapsed 的值改变后,我们只需要重新调用 render 方法即可:

setInterval(() => {
  appState.secondsElapsed++;
  render(appState);
}, 1000);

现在,让我们来实现一个类似 Redux 风格的归约函数,以不断的递增 secondsElapsed。归约函数是不允许修改当前状态的,所有最简单的实现方式就是 currentState -> newState

这里我们使用 Ramda 的透镜(Lens)来实现 incSecondsElapsed 函数:

const secondsElapsedLens = R.lensProp('secondsElapsed');
const incSecondsElapsed = R.over(secondsElapsedLens, R.inc);

setInterval(() => {
  appState = incSecondsElapsed(appState);
  render(appState);
}, 1000);

第一行代码中,我们创建了一个透镜:

const secondsElapsedLens = R.lensProp('secondsElapsed');

简单来说,透镜是一种专注于给定属性的方式,而不关心该属性到底是在哪个对象上,这种方式便于代码复用。当我们需要把透镜应用于对象上时,可以有以下操作:

  • View
R.view(secondsElapsedLens, { secondsElapsed: 10 });  //=> 10
  • Set
R.set(secondsElapsedLens, 11, { secondsElapsed: 10 });  //=> 11
  • 以给定函数来设置
R.over(secondsElapsedLens, R.inc, { secondsElapsed: 10 });  //=> 11

我们实现的 incSecondsElapsed 就是对 R.over 进行局部应用的结果。

const incSecondsElapsed = R.over(secondsElapsedLens, R.inc);

该行代码会返回一个新函数,一旦调用时传入 appState,就会把 R.inc 应用在 secondsElapsed 属性上。

需要注意的是,Ramda 从来都不会修改对象,所以我们需要自己来处理脏活:

appState = incSecondsElapsed(appState);

如果想支持 undo/redo ,只需要维护一个历史数组记录下每一次状态即可,或者使用 Redux 。

目前为止,我们已经品尝了柯里化和透镜,下面让我们继续品尝组合

组合 React 无状态组件

当我第一次读到 React 无状态组件时,我就在想能否使用 R.compose 来组合这些函数呢?答案很明显,当然是 YES 啦:)

让我们从一个 TodoList 组件开始:

const TodoList = React.createClass({
  render: function() {
    const createItem = function(item) {
      return (<li key={item.id}>{item.text}</li>);
    };

    return (<div className="panel panel-default">
      <div className="panel-body">
        <ul>
          {this.props.items.map(createItem)}
        </ul>
      </div>
    </div>);
  }
});

现在问题来了,TodoList 能否通过组合更小的、可复用的组件来实现呢?当然,我们可以把它分割成 3 个小组件:

  • 容器
const Container = children => (<div className="panel panel-default">
  <div className="panel-body">
    {children}
  </div>
</div>);
  • 列表
const List = children => (<ul>
  {children}
</ul>);
  • 列表项
const ListItem = ({ id, text }) => (<li key={id}>
  <span>{text}</span>
</li>);

现在,我们来一步一步看,请一定要在理解了每一步之后才往下看:

Container(<h1>Hello World!</h1>);

/**
 *  <div className="panel panel-default">
 *    <div className="panel-body">
 *      <h1>Hello World!</h1>
 *    </div>
 *  </div>
 */

Container(List(<li>Hello World!</li>));

/**
 *  <div className="panel panel-default">
 *    <div className="panel-body">
 *      <ul>
 *        <li>Hello World!</li>
 *      </ul>
 *    </div>
 *  </div>
 */

const TodoItem = {
  id: 123,
  text: 'Buy milk'
};
Container(List(ListItem(TodoItem)));

/**
 *  <div className="panel panel-default">
 *    <div className="panel-body">
 *      <ul>
 *        <li>
 *          <span>Buy milk</span>
 *        </li>
 *      </ul>
 *    </div>
 *  </div>
 */

没有什么太特别的,只不过是一步一步的传参调用。

接着,让我们来做一些组合的练习:

R.compose(Container, List)(<li>Hello World!</li>);

/**
 *  <div className="panel panel-default">
 *    <div className="panel-body">
 *      <ul>
 *        <li>Hello World!</li>
 *      </ul>
 *    </div>
 *  </div>
 */

const ContainerWithList = R.compose(Container, List);
R.compose(ContainerWithList, ListItem)({id: 123, text: 'Buy milk'});

/**
 *  <div className="panel panel-default">
 *    <div className="panel-body">
 *      <ul>
 *        <li>
 *          <span>Buy milk</span>
 *        </li>
 *      </ul>
 *    </div>
 *  </div>
 */

const TodoItem = {
  id: 123,
  text: 'Buy milk'
};
const TodoList = R.compose(Container, List, ListItem);
TodoList(TodoItem);

/**
 *  <div className="panel panel-default">
 *    <div className="panel-body">
 *      <ul>
 *        <li>
 *          <span>Buy milk</span>
 *        </li>
 *      </ul>
 *    </div>
 *  </div>
 */

发现了没!TodoList 组件已经被表示成了 ContainerListListItem 的组合了:

const TodoList = R.compose(Container, List, ListItem);

等等!TodoList 这个组件只接受一个 todo 对象,但是我们需要的是映射整个 todos 数组:

const mapTodos = function(todos) {
  return todos.map(function(todo) {
    return ListItem(todo);
  });
};
const TodoList = R.compose(Container, List, mapTodos);
const mock = [
  {id: 1, text: 'One'},
  {id: 1, text: 'Two'},
  {id: 1, text: 'Three'}
];
TodoList(mock);

/**
 *  <div className="panel panel-default">
 *    <div className="panel-body">
 *      <ul>
 *        <li>
 *          <span>One</span>
 *        </li>
 *        <li>
 *          <span>Two</span>
 *        </li>
 *        <li>
 *          <span>Three</span>
 *        </li>
 *      </ul>
 *    </div>
 *  </div>
 */

能否以更函数式的方式简化 mapTodos 函数?

// 下面的代码
return todos.map(function(todo) {
  return ListItem(todo);
});

// 等效于
return todos.map(ListItem);

// 所以变成了
const mapTodos = function(todos) {
  return todos.map(ListItem);
};

// 等效于使用 Ramda 的方式
const mapTodos = function(todos) {
  return R.map(ListItem, todos);
};

// 注意 Ramda 的两个特点:
// - Ramda 函数默认都支持柯里化
// - 为了便于柯里化,Ramda 函数的参数进行了特定排列,
//   待处理的数据通常放在最后
// 因此:
const mapTodos = R.map(ListItem);

//此时就不再需要 mapTodos 了:
const TodoList = R.compose(Container, List, R.map(ListItem));

哒哒哒!完整的 TodoList 实现代码如下:

import React from 'React';
import R from 'ramda';

const Container = children => (<div className="panel panel-default">
  <div className="panel-body">
    {children}
  </div>
</div>);

const List = children => (<ul>
  {children}
</ul>);

const ListItem = ({ id, text }) => (<li key={id}>
  <span>{text}</span>
</li>);

const TodoList = R.compose(Container, List, R.map(ListItem));

export default TodoList;

其实,还少了一样东西,不过马上就会加上。在那之前让我们先来做些准备:

  • 添加测试数据到应用状态
let appState = {
  secondsElapsed: 0,
  todos: [
    {id: 1, text: 'Buy milk'},
    {id: 2, text: 'Go running'},
    {id: 3, text: 'Rest'}
  ]
};
  • 添加 TodoListApp
import TodoList from './todo-list';
const App = appState => (<div className="container">
  <h1>App name</h1>
  <Timer secondsElapsed={appState.secondsElapsed} />
  <TodoList todos={appState.todos} />
</div>);

TodoList 接受的是一个 todos 数组,但是这里却是:

<TodoList todos={appState.todos} />

我们把列表传递作为一个属性,所以等效于:

TodoList({todos: appState.todos});

因此,我们必须修改 TodoList,以便让它接受一个对象并且取出 todos 属性:

const TodoList = R.compose(Container, List, R.map(ListItem), R.prop('todos'));

这里并没有什么高深技术。仅仅是从右到左的组合,R.prop('todos') 会返回一个函数,调用该函数会返回其作为的参数对象的 todos 属性,接着把该属性值传递给 R.map(ListItem),如此往复:)

以上就是本文的尝鲜内容。希望能对大家有所帮助,这仅仅是我基于 React 和 Ramda 做的一部分实验。未来,我会努力尝试覆盖高阶组件和使用 Transducer 来转换无状态函数。

完整源码线上演示代码(译者新增)。

查看原文

赞 10 收藏 12 评论 0

SXX19950910 提出了问题 · 2018-06-05

formidable配合express无法返回重命名数据

使用formidable接受到文件并重命名保存后无法返回重命名的数据

clipboard.png
最后返回的picPath一直为空,后来发现是异步操作的 然后我写在from.on("end")的回调中也无效。

请问我该怎么做才可以返回文件名称给前端?

关注 1 回答 0

SXX19950910 提出了问题 · 2018-04-25

使用clipboard.js如何在调用接口调用完成后执行复制功能?

1.项目有一个需求是,点击某个按钮执行复制的功能,但是需要复制的值是通过接口得到的,所以就不能第一时间获取这个值,所以第一次点击按钮的时候就不会成功,必须得第二次点击才会执行复制的功能。
2.如果要一次性点击按钮调完接口并执行复制功能我该怎么做呢?

关注 5 回答 4

SXX19950910 提出了问题 · 2018-04-04

解决react报错求解!

Warning: Encountered two children with the same key, `.$4`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.
    in ul (created by DOMWrap)
    in DOMWrap (created by Menu)
    in Provider (created by Menu)
    in Menu (created by Menu)
    in Menu (created by Left)
    in div (created by Sider)
    in div (created by Sider)
    in Sider (created by Left)
    in Left (created by Main)
    in div (created by BasicLayout)
    in BasicLayout (created by Adapter)
    in Adapter (created by Main)
    in Main (created by Connect(Main))
    in Connect(Main) (created by Route)
    in Route
    in Switch
    in Router
    in Provider
    in Unknown
    
    
    以上为react错误代码,求解!    
    
    
   问题以解决  把Key的值修改下就可以了    

关注 4 回答 4

认证与成就

  • 获得 3 次点赞
  • 获得 37 枚徽章 获得 1 枚金徽章, 获得 8 枚银徽章, 获得 28 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-07-11
个人主页被 596 人浏览