苹苹吖 發表於 2019-12-4 20:08:00

node.js+react全栈实践-开篇

<p>利用业余时间写了个简单的项目,使用react+node.js做的一个全栈实践项目,前端参考了(https://github.com/veryStarters/react-admin-starter)这个项目,这个项目的自动配置路由,自动页面骨架的思路很新颖。后端是node.js+express提供接口访问,最主要的内容是mysql.js的使用和使用nginx反向代理来跨域。</p>
<h1>1.前端parttime</h1>
<p>前端基于框架React-Admin-Starter基本没有改动。这是一个后台管理系统,最常用的功能也就是增删改查,这里做了一些自己的调整。</p>
<h2>1.1.统一的字段名</h2>
<p>开发PC端这种后台项目,产品经理经常会提一些临时需求。比如原型上一个表格字段“编辑时间”,做到一般快结尾了或者已经快上线了,说要改成“更新时间”。这个时候就比较蛋疼了,当然最直接的办法就是Ctrl+H全局查找,一个一个替换,但是遇到新手连编辑器都不是很熟的小伙伴就要捉急了(我见过一些刚入门的小伙子,用的是vscode,还真不知道全局查找,快速跳转这些快捷键)。<br>前端项目中使用的是ant.design for react,table有两个地方需要注意,数据源和显示列名:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 数据源</span>
const dataSource =<span style="color: rgba(0, 0, 0, 1)"> [
{ key: </span>'1', name: '胡彦斌', age: 32, address: '西湖区湖底公园1号'<span style="color: rgba(0, 0, 0, 1)"> },
{ key: </span>'2', name: '胡彦祖', age: 42, address: '西湖区湖底公园1号'<span style="color: rgba(0, 0, 0, 1)"> }
];

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 显示列</span>
const columns =<span style="color: rgba(0, 0, 0, 1)"> [
{ title: </span>'姓名', dataIndex: 'name', key: 'name'<span style="color: rgba(0, 0, 0, 1)"> },
{ title: </span>'年龄', dataIndex: 'age', key: 'age'<span style="color: rgba(0, 0, 0, 1)"> },
{ title: </span>'住址', dataIndex: 'address', key: 'address'<span style="color: rgba(0, 0, 0, 1)"> }
]</span></pre>
</div>
<p>这里可以把所有字段单独写在一个文件里面,从同一个地方引用这个字段,这样只修改这一个字段所有的名字都改过来了。如下,columns.js 定义字段:</p>
<div class="cnblogs_code">
<pre>const id = { title: 'ID', dataIndex: 'id', key: 'id', type: 'input'<span style="color: rgba(0, 0, 0, 1)"> }
const name </span>= { title: '姓名', dataIndex: 'name', key: 'name', type: 'input'<span style="color: rgba(0, 0, 0, 1)"> }
const mobile </span>= { title: '手机号', dataIndex: 'mobile', key: 'mobile', type: 'input'<span style="color: rgba(0, 0, 0, 1)"> }
const email </span>= { title: '邮箱', dataIndex: 'email', key: 'email', type: 'input'<span style="color: rgba(0, 0, 0, 1)"> }
const thumb </span>= { title: '头像', dataIndex: 'thumb', key: 'thumb', render: src =&gt; &lt;img alt='' src={ src }/&gt; }
const user =<span style="color: rgba(0, 0, 0, 1)">
export {
user
}</span></pre>
</div>
<p>user/list/index.js使用字段:</p>
<div class="cnblogs_code">
<pre>import { user } from './../../../columns'

&lt;<span style="color: rgba(0, 0, 0, 1)">Table
    dataSource</span>=<span style="color: rgba(0, 0, 0, 1)">{userList}
    pagination</span>=<span style="color: rgba(0, 0, 0, 1)">{paginationProps}
    columns</span>=<span style="color: rgba(0, 0, 0, 1)">{user})}
    rowKey</span>='id'<span style="color: rgba(0, 0, 0, 1)">
    size</span>="middle"<span style="color: rgba(0, 0, 0, 1)">
    bordered</span>/&gt;</pre>
</div>
<p>问题来了,如果有编辑,删除字段怎么办呢?这个时候就需要和引用它的地方交互了。这里可以使用给子组件传递函数的方法来实现:</p>
<div class="cnblogs_code">
<pre>const action = props =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
let { handleDelete, handleEdit } </span>=<span style="color: rgba(0, 0, 0, 1)"> props
</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> {
    title: </span>'操作'<span style="color: rgba(0, 0, 0, 1)">,
    key: </span>'action'<span style="color: rgba(0, 0, 0, 1)">,
    render: (text, record) </span>=&gt; &lt;span&gt;
      &lt;Popconfirm title='确定删除?' onConfirm={() =&gt; handleDelete(record)} okText="确定" cancelText="取消"&gt;
      &lt;Icon type="delete" className={style.deleteLink}/&gt;
      &lt;/Popconfirm&gt;
      &lt;Divider type="vertical"/&gt;
      &lt;Icon type="edit" onClick={() =&gt; handleEdit(record)}/&gt;
    &lt;/span&gt;
<span style="color: rgba(0, 0, 0, 1)">}
}
const user </span>= { column: props =&gt; }&nbsp;</pre>
</div>
<p>在使用这个字段的时候就可以调用一个函数:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">handleDelete(record) {
    api.user.deleteUser({ id: record.id }).then(res </span>=&gt;<span style="color: rgba(0, 0, 0, 1)"> {
      </span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (res.success) {
            </span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.search()
      }
    })
}
   
</span>&lt;<span style="color: rgba(0, 0, 0, 1)">Table
    dataSource</span>=<span style="color: rgba(0, 0, 0, 1)">{userList}
    pagination</span>=<span style="color: rgba(0, 0, 0, 1)">{paginationProps}
    columns</span>={user.column({ handleDelete: <span style="color: rgba(0, 0, 255, 1)">this</span>.handleDelete.bind(<span style="color: rgba(0, 0, 255, 1)">this</span>), handleEdit: <span style="color: rgba(0, 0, 255, 1)">this</span>.handleEdit.bind(<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">) })}
    rowKey</span>='id'<span style="color: rgba(0, 0, 0, 1)">
    size</span>="middle"<span style="color: rgba(0, 0, 0, 1)">
    bordered</span>/&gt;&nbsp;</pre>
</div>
<p>这里给Table的columns属性赋的是一个函数,函数参数是一个也是一个函数,这样子组件就可以调用到这个函数,有点拗口,你懂就好。columns.js中的action字段只是一个桥梁作用,根据具体逻辑传递进去的函数执行不同的操作,不同场合执行的操作不同,但是操作是类似的,基本都是删除,和编辑两个逻辑。</p>
<p>分页也有类似的问题,比如那天产品经理说:“分页样式统一起来,每个地方可选的每页个数都是20, 30, 50, 100”。我们也可以把这个定义在同一个地方,方便修改。这里仍然定义在columns.js中</p>
<div class="cnblogs_code">
<pre>const pageSet = { current: 1, pageSize: 2, total: 0, showQuickJumper: <span style="color: rgba(0, 0, 255, 1)">true</span>, showSizeChanger: <span style="color: rgba(0, 0, 255, 1)">true</span>, pageSizeOptions: ['20', '30', '50', '100'] }</pre>
</div>
<p>使用的,如果我们要需要某些场合需要覆盖掉部分信息,可以在state中使用...扩展运算符,然后后面跟上同名属性来覆盖,例如:</p>
<div class="cnblogs_code">
<pre>import { user, pageSet } from './../../../columns'<span style="color: rgba(0, 0, 0, 1)">
constructor(props) {
    super(props)
    </span><span style="color: rgba(0, 0, 255, 1)">this</span>.state =<span style="color: rgba(0, 0, 0, 1)"> {
      showAdd: </span><span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">,
      pageSet: { ...pageSet, pageSizeOptions: [</span>'2', '10'<span style="color: rgba(0, 0, 0, 1)">] }
    }
}</span>&nbsp;</pre>
</div>
<p>这样就不需要在每个业务逻辑里都去定义列名,只需要在columns.js中去定义,组合,导出字段就好了。这样可能也会有不妥的地方,理论上这里应该包含这个系统中所有要显示的列名,大一点的系统如果有成千上万个字段,这里就多起来了。不过话说回来这总比在每个界面自己定义字段写的代码要少。</p>
<h2>1.2&nbsp;使用同一个新增弹框</h2>
<p>新增数据,无非是一个弹出框,一个Form加上两个按钮,没有必要为每一个界面写一个,如果能给这个弹框传入属性,包含要新增的字段,点击确定的时候调用父组件中的新增方法。这样这个弹出框被公用起来,只起到收集数据,验证数据的作用。</p>
<p>传入要新增的字段,一样在columns.js这个文件里做文章,一般要新增的字段和显示在表里的字段是类似的,二般不一样就难办了,这样最好还是区分开来,顶多是组合字段而已。再者,如果新增的字段时间类型,下拉框选择,上传的文件,图片怎么办呢? 可以在这个字段里加上一个type字段,表示控件类型,如下:</p>
<div class="cnblogs_code">
<pre>const email = { title: '邮箱', dataIndex: 'email', key: 'email', type: 'input'<span style="color: rgba(0, 0, 0, 1)"> }
const createTime </span>= { title: '创建时间', dataIndex: 'createTime', key: 'createTime', type: 'time'<span style="color: rgba(0, 0, 0, 1)"> }
const user </span>= { column: props =&gt; , field: }&nbsp;</pre>
</div>
<p>引入field,传递给新增组件</p>
<div class="cnblogs_code">
<pre>import { user, pageSet } from './../../../columns'
&lt;<span style="color: rgba(0, 0, 0, 1)">AddComp
    field</span>=<span style="color: rgba(0, 0, 0, 1)">{user.field}
    showAdd</span>=<span style="color: rgba(0, 0, 0, 1)">{showAdd}
    onAddData</span>={<span style="color: rgba(0, 0, 255, 1)">this</span>.addUser.bind(<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">)}
    title</span>={route.title}/&gt;&nbsp;</pre>
</div>
<p>在AddComp组件中使用传入的字段:</p>
<div class="cnblogs_code">
<pre>import React, { Component } from 'react'<span style="color: rgba(0, 0, 0, 1)">
import { Form, Modal, Input, message } from </span>'antd'<span style="color: rgba(0, 0, 0, 1)">

class AddDataComp extends Component {
constructor(props) {
    super(props)
    </span><span style="color: rgba(0, 0, 255, 1)">this</span>.state =<span style="color: rgba(0, 0, 0, 1)"> {
    }
}
componentWillReceiveProps(nextProps, nextContext) {
    </span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.setState({ showAdd: nextProps.showAdd })
}
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 取消,关闭,调用父组件关闭弹框</span>
<span style="color: rgba(0, 0, 0, 1)">hideModel() {
    </span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.props.onClose()
}
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 确认,调用父组件,添加数据</span>
<span style="color: rgba(0, 0, 0, 1)">confirmForm() {
    </span><span style="color: rgba(0, 0, 255, 1)">this</span>.props.form.validateFields((err, values) =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
      </span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (err) {
      message.error(err)
      }
      </span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.props.onAddData(values)
    })
}
render() {
    let { showAdd } </span>= <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.state
    let { field, title } </span>= <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.props
    let { getFieldDecorator } </span>= <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.props.form
    const formItemLayout </span>= { labelCol: { span: 6 }, wrapperCol: { span: 18<span style="color: rgba(0, 0, 0, 1)"> }}
    </span><span style="color: rgba(0, 0, 255, 1)">return</span> &lt;<span style="color: rgba(0, 0, 0, 1)">Modal
      visible</span>=<span style="color: rgba(0, 0, 0, 1)">{showAdd}
      title</span>={'添加' +<span style="color: rgba(0, 0, 0, 1)"> title}
      centered
      onCancel</span>={<span style="color: rgba(0, 0, 255, 1)">this</span>.hideModel.bind(<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">)}
      onOk</span>={<span style="color: rgba(0, 0, 255, 1)">this</span>.confirmForm.bind(<span style="color: rgba(0, 0, 255, 1)">this</span>)}&gt;
      &lt;Form {...formItemLayout}&gt;<span style="color: rgba(0, 0, 0, 1)">
      {field.map((f, index) </span>=&gt; &lt;Form.Item key={f.key} label={f.title}&gt;<span style="color: rgba(0, 0, 0, 1)">
          {getFieldDecorator(f.key, {
            validateTrigger: [</span>'onChange', 'onBlur'<span style="color: rgba(0, 0, 0, 1)">],
            rules: [
            { required: </span><span style="color: rgba(0, 0, 255, 1)">true</span>, whitespace: <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">, message: `${f.title}不能为空` },
            ],
          })(</span>&lt;Input placeholder={'请输入' + f.title}/&gt;)}
      &lt;/Form.Item&gt;)}
      &lt;/Form&gt;
    &lt;/Modal&gt;
<span style="color: rgba(0, 0, 0, 1)">}
}
const AddComp </span>= Form.create({ name: 'add_comp'<span style="color: rgba(0, 0, 0, 1)"> })(AddDataComp)
export </span><span style="color: rgba(0, 0, 255, 1)">default</span> AddComp</pre>
</div>
<p>未解决问题:</p>
<ol>
<li>验证,不同的字段验证不同,可以在字段中传入一个RegExp来验证,复杂的验证比如密码比较,字段之间有关联的验证如何通过字段来验证,目前本人没有想到好办法</li>
<li>复杂字段,比如文件上传,传入file或者img字段可以明确表示需要上传的字段类型,这种一般是上传文件后得到一个链接,返回这个链接并写入到数据库中,暂时没有实现。</li>
</ol>
<h2>1.3&nbsp;使用同一个搜索组件</h2>
<p>同样,搜索也是根据几个字段来查询信息,这里我们可以把搜索分成两种类型:</p>
<ol>
<li>简单搜索,按照更新时间来搜索,比如昨天,今天,当月,上月,名称搜索,其中昨天,今天,当月,上月做成tab的形式,名称直接输入框,并且回车搜索。这个能满足最普遍的搜索功能。</li>
<li>复杂搜索,简单搜索的基础上加上要搜索的字段。</li>
</ol>
<p><img src="https://img2018.cnblogs.com/blog/72678/201912/72678-20191204195619736-2006459064.png" alt=""></p>
<p>简单搜索&nbsp;</p>
<p><img src="https://img2018.cnblogs.com/blog/72678/201912/72678-20191204195637877-1812110514.png" alt=""></p>
<p>复杂搜索</p>
<p>复杂搜索中要搜索的字段照样放在common.js中,如下:</p>
<div class="cnblogs_code">
<pre>const user = { column: props =&gt; , field: , searchField: }&nbsp;</pre>
</div>
<p>引用并使用:</p>
<div class="cnblogs_code">
<pre>import { user, pageSet } from './../../../columns'
&lt;<span style="color: rgba(0, 0, 0, 1)">AddComp
field</span>=<span style="color: rgba(0, 0, 0, 1)">{user.field}
showAdd</span>=<span style="color: rgba(0, 0, 0, 1)">{showAdd}
onAddData</span>={<span style="color: rgba(0, 0, 255, 1)">this</span>.addUser.bind(<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">)}
title</span>={route.title}/&gt;&nbsp;</pre>
</div>
<p>SearchComp组件:</p>
<div class="cnblogs_code">
<pre>import React, { Component } from 'react'<span style="color: rgba(0, 0, 0, 1)">
import { Tabs, Input, Button, DatePicker } from </span>'antd'<span style="color: rgba(0, 0, 0, 1)">
const { TabPane } </span>=<span style="color: rgba(0, 0, 0, 1)"> Tabs
const { Search } </span>=<span style="color: rgba(0, 0, 0, 1)"> Input
const { RangePicker } </span>=<span style="color: rgba(0, 0, 0, 1)"> DatePicker
import style from </span>'./../static/css/index.pcss'<span style="color: rgba(0, 0, 0, 1)">
import { Type } from </span>'utils'<span style="color: rgba(0, 0, 0, 1)">

class SearchComp extends Component {
constructor(props) {
    super(props)
    </span><span style="color: rgba(0, 0, 255, 1)">this</span>.state =<span style="color: rgba(0, 0, 0, 1)"> {
      moreSearch: </span><span style="color: rgba(0, 0, 255, 1)">true</span>, <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 显示更多搜索</span>
      timeSpan: [{ name: 'today', title: '今天'<span style="color: rgba(0, 0, 0, 1)"> },
      { name: </span>'yesterday', title: '昨天'<span style="color: rgba(0, 0, 0, 1)"> },
      { name: </span>'currentMonth', title: '本月'<span style="color: rgba(0, 0, 0, 1)"> },
      { name: </span>'lastMonth', title: '上月'<span style="color: rgba(0, 0, 0, 1)"> }],
      searchObj: {}
    }
}
componentDidMount() {
}
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 搜索条件</span>
<span style="color: rgba(0, 0, 0, 1)">setSearchState(event, column) {
    let { searchObj } </span>= <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.state
    </span><span style="color: rgba(0, 0, 255, 1)">if</span> (event.type === 'time'<span style="color: rgba(0, 0, 0, 1)">) {
      </span><span style="color: rgba(0, 0, 255, 1)">if</span> (column) {
      searchObj[`${event.dataIndex}Start`] </span>= column.format('YYYY-MM-DD hh:mm'<span style="color: rgba(0, 0, 0, 1)">)
      } </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
      </span><span style="color: rgba(0, 0, 255, 1)">delete</span><span style="color: rgba(0, 0, 0, 1)"> searchObj[`${event.dataIndex}Start`]
      }
      </span><span style="color: rgba(0, 0, 255, 1)">if</span> (column) {
      searchObj[`${event.dataIndex}End`] </span>= column.format('YYYY-MM-DD hh:mm'<span style="color: rgba(0, 0, 0, 1)">)
      } </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
      </span><span style="color: rgba(0, 0, 255, 1)">delete</span><span style="color: rgba(0, 0, 0, 1)"> searchObj[`${event.dataIndex}End`]
      }
    } </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
      </span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (event.target.value) {
      searchObj </span>=<span style="color: rgba(0, 0, 0, 1)"> event.target.value
      } </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
      </span><span style="color: rgba(0, 0, 255, 1)">delete</span><span style="color: rgba(0, 0, 0, 1)"> searchObj
      }
    }
    </span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.setState(searchObj)
}
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 简单搜索,默认搜索第一个字段</span>
<span style="color: rgba(0, 0, 0, 1)">searchKeyword(value) {
    let searchObj </span>=<span style="color: rgba(0, 0, 0, 1)"> {}
    let { searchField } </span>= <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.props
    </span><span style="color: rgba(0, 0, 255, 1)">if</span> (searchField.length &gt; 0<span style="color: rgba(0, 0, 0, 1)">) {
      searchObj.key] =<span style="color: rgba(0, 0, 0, 1)"> value
      </span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.onSearch(searchObj)
    }
}
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 回车搜索</span>
<span style="color: rgba(0, 0, 0, 1)">searchEnterKeyword(e) {
    </span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (e.target.value) {
      let searchObj </span>=<span style="color: rgba(0, 0, 0, 1)"> {}
      let { searchField } </span>= <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.props
      </span><span style="color: rgba(0, 0, 255, 1)">if</span> (searchField.length &gt; 0<span style="color: rgba(0, 0, 0, 1)">) {
      searchObj.key] =<span style="color: rgba(0, 0, 0, 1)"> e.target.value
      </span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.onSearch(searchObj)
      }
    }
}
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 条件搜索</span>
<span style="color: rgba(0, 0, 0, 1)">searchClick() {
    let { searchObj } </span>= <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.state
    </span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.onSearch(searchObj)
}
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 触发父组件搜索</span>
<span style="color: rgba(0, 0, 0, 1)">onSearch(searchObj) {
    </span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.props.onSearch(searchObj)
}
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 添加,触发父组件,弹出添加框</span>
<span style="color: rgba(0, 0, 0, 1)">popUpAdd() {
    </span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.props.onAdd()
}
getSearchItem </span>= () =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
    let { searchField } </span>= <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.props
    </span><span style="color: rgba(0, 0, 255, 1)">return</span> (&lt;div className={style.searchItem}&gt;<span style="color: rgba(0, 0, 0, 1)">
      {searchField.map((s, index) </span>=&gt;<span style="color: rgba(0, 0, 0, 1)"> {
      </span><span style="color: rgba(0, 0, 255, 1)">if</span> (s.type === 'input') { <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 文本框</span>
          <span style="color: rgba(0, 0, 255, 1)">return</span> &lt;div key={s.key}&gt;
            &lt;label htmlFor={s.key}&gt;{s.title}&lt;/label&gt;
            &lt;Input name={s.key} id={s.key} allowClear placeholder={s.title} onChange={<span style="color: rgba(0, 0, 255, 1)">this</span>.setSearchState.bind(<span style="color: rgba(0, 0, 255, 1)">this</span>)} className={style.itemInput}/&gt;
          &lt;/div&gt;
      } <span style="color: rgba(0, 0, 255, 1)">else</span> <span style="color: rgba(0, 0, 255, 1)">if</span> (s.type === 'time') { <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 时间搜索</span>
          <span style="color: rgba(0, 0, 255, 1)">return</span> &lt;div key={s.key}&gt;
            &lt;label htmlFor={s.key}&gt;{s.title}&lt;/label&gt;
            &lt;RangePicker name={s.key} id={s.key} allowClear onChange={ <span style="color: rgba(0, 0, 255, 1)">this</span>.setSearchState.bind(<span style="color: rgba(0, 0, 255, 1)">this</span>, s) } className={style.itemInput}/&gt;
          &lt;/div&gt;
      } <span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
          </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">
      }
      })}
      </span>&lt;div key='submit-button'&gt;
      &lt;Button&gt;重置&lt;/Button&gt;
      &lt;Button type="primary" className={style.commonMarginLeft} onClick={<span style="color: rgba(0, 0, 255, 1)">this</span>.searchClick.bind(<span style="color: rgba(0, 0, 255, 1)">this</span>)}&gt;搜索&lt;/Button&gt;
      &lt;/div&gt;
    &lt;/div&gt;)
<span style="color: rgba(0, 0, 0, 1)">}
render() {
    let { timeSpan, moreSearch } </span>= <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.state
    let { onAdd } </span>= <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.props
    </span><span style="color: rgba(0, 0, 255, 1)">return</span> (&lt;div&gt;
      &lt;div className={style.search}&gt;
      &lt;Tabs&gt;{ timeSpan.map((t, i) =&gt; &lt;TabPane tab={t.title} key={i}/&gt;) }&lt;/Tabs&gt;
      &lt;div className={style.searchBox}&gt;
          &lt;<span style="color: rgba(0, 0, 0, 1)">Search
            allowClear
            className</span>=<span style="color: rgba(0, 0, 0, 1)">{style.itemInput}
            placeholder</span>="请输入关键字"<span style="color: rgba(0, 0, 0, 1)">
            onPressEnter</span>={<span style="color: rgba(0, 0, 255, 1)">this</span>.searchEnterKeyword.bind(<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">)}
            onSearch</span>={<span style="color: rgba(0, 0, 255, 1)">this</span>.searchKeyword.bind(<span style="color: rgba(0, 0, 255, 1)">this</span>)}/&gt;
          &lt;<span style="color: rgba(0, 0, 0, 1)">Button
            onClick</span>={() =&gt; <span style="color: rgba(0, 0, 255, 1)">this</span>.setState({ moreSearch: !<span style="color: rgba(0, 0, 0, 1)">moreSearch })}
            icon</span>="search"<span style="color: rgba(0, 0, 0, 1)">
            className</span>={style.commonMarginLeft}/&gt;
          {Type.isFunction(onAdd) ? &lt;<span style="color: rgba(0, 0, 0, 1)">Button
            onClick</span>={<span style="color: rgba(0, 0, 255, 1)">this</span>.popUpAdd.bind(<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">)}
            className</span>=<span style="color: rgba(0, 0, 0, 1)">{style.commonMarginLeft}
            type</span>="primary"<span style="color: rgba(0, 0, 0, 1)">
            icon</span>="plus"/&gt; : null}
      &lt;/div&gt;
      &lt;/div&gt;
      {moreSearch ? <span style="color: rgba(0, 0, 255, 1)">this</span>.getSearchItem() : <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">}
    </span>&lt;/div&gt;)
<span style="color: rgba(0, 0, 0, 1)">}
}
export </span><span style="color: rgba(0, 0, 255, 1)">default</span> SearchComp</pre>
</div>
<p>这里使用onChange方法来收集搜索数据,原理是给Input组件设置name,值是key,也就是字段名,onChange方法中,使用event.target.name获取字段名字,使用event.target.value获取Input的输入值,这样组成搜索数据searchObj,最后把searchObj返回给父组件。</p>
<p>未解决问题:</p>
<ol>
<li>时间搜索一般是一个时间段,这个暂时没有实现。</li>
<li>如果搜索条件是一个下拉框选择出来的,这个要给条件渲染成下拉框,这个暂时没有实现。</li>
</ol>
<h2>1.4&nbsp;mock数据和代理跨域</h2>
<p>原框架提供自动生成mock文件的功能,项目启动后使用express启用了http应用(parttime\scripts\addone\mock-server.js),端口是10086,专门监听mock请求,在fetch(parttime\src\common\utils\fetch.js),proxyTable(parttime\src\rasConfig.js)中代理。如果不想走mock,就修改代理的target。不过上项目之后很少使用mock,增加了工作量不是?再说已经全栈开发了还要mock个啥呢?</p>
<h1>2.后端parttimeApp</h1>
<p>后端开发采用的express,mysql.js,pug实现的,注意这里主要写接口,pug模板基本上没有用到。这个子项目基本上是按照官方文档来写的。<br>使用express-generator来生成项目骨架,express的模板引擎好多,也不知道那个好,就按照官方文档中的例子给个pug来生成项目。项目中有个www文件,是启动文件,可以直接运行这个文件启动。</p>
<h2>2.1&nbsp;数据库访问</h2>
<p>要访问接口要添加中间件body-parser,因为post,put,patch三种请求中包含请求提,node.js原生的http模块中,请求提是基于流的方式来接受,body-parser可以解析JSON,Raw,文本,URL-encoded格式的请求体。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">var</span> bodyParser = require('body-parser'<span style="color: rgba(0, 0, 0, 1)">);
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">解析 application/json</span>
<span style="color: rgba(0, 0, 0, 1)">app.use(bodyParser.json());
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">解析 application/x-www-form-urlencoded</span>
app.use(bodyParser.urlencoded({ extended: <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)"> }));
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">转发api/base请求</span>
app.use('/api/base'<span style="color: rgba(0, 0, 0, 1)">, indexRouter);
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">转发api/user请求</span>
app.use('/api/user', usersRouter);</pre>
</div>
<p>在usersRouter就是具体的接口请求了,如下:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">var</span> express = require('express'<span style="color: rgba(0, 0, 0, 1)">);
</span><span style="color: rgba(0, 0, 255, 1)">var</span> router =<span style="color: rgba(0, 0, 0, 1)"> express.Router();
</span><span style="color: rgba(0, 0, 255, 1)">var</span> config = require('./../conf/index'<span style="color: rgba(0, 0, 0, 1)">)

</span><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)"> GET users listing. </span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">
router.get(</span>'/', <span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> (req, res, next) {
    res.send(</span>'respond with a resource'<span style="color: rgba(0, 0, 0, 1)">);
});</span></pre>
</div>
<p>这里简单的分了个层,和java,.net代码一样有router层(相当于业务逻辑层),dao层(数据访问层)。dao层里使用mysql.js访问mysql数据库。<br>这个地方说一下分页的逻辑,分页查询使用的是limit offset,pageSize方式,但是有个重要的信息要返回,就是数据行数,所以需要执行两次请求,这就意味这要使用回调嵌套了,这就不是很爽了,代码会成一坨。所幸mysql.js生成连接池的时候有个选项multipleStatements,把它设置成true,就可以一次执行两个sql语句,有点类似存储过程。</p>
<p>查询接口一般是select column1,column2 ... from table where column1=value1 and column2=value2 ... order by updateTime desc limit offset, pageSize,这样的,为了避免每次都拼接sql语句,这里写了一个统一处理函数,另外还使用current,pageSize生成offSet。<br>接口请求中出列current,pageSize,current字段之外的字段默认都是需要查询的字段,使用for...of方法轮询查询对象,生成where后缀。方法如下:</p>
<div class="cnblogs_code">
<pre>    paging: (sql, param) =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
      </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 如果请求中有pageSize,使用current,pageSize生成offSet</span>
      <span style="color: rgba(0, 0, 255, 1)">if</span> (param.hasOwnProperty('pageSize'<span style="color: rgba(0, 0, 0, 1)">)) {
            param.pageSize </span>=<span style="color: rgba(0, 0, 0, 1)"> parseInt(param.pageSize)
            param.offSet </span>= param.current &lt;= 1 ? 0 : (param.current - 1) *<span style="color: rgba(0, 0, 0, 1)"> param.pageSize
      }
      </span><span style="color: rgba(0, 0, 255, 1)">for</span>(let key <span style="color: rgba(0, 0, 255, 1)">in</span><span style="color: rgba(0, 0, 0, 1)"> param) {
            </span><span style="color: rgba(0, 0, 255, 1)">if</span>(!['pageSize', 'current', 'offSet'<span style="color: rgba(0, 0, 0, 1)">].includes(key)) {
                sql[</span>0]+= ` AND ${key}=<span style="color: rgba(0, 0, 0, 1)">:${key}`
                sql[</span>1]+= ` AND ${key}=<span style="color: rgba(0, 0, 0, 1)">:${key}`
            }
      }
      sql[</span>0] += ' ORDER BY updateTime DESC LIMIT :offSet, :pageSize;'<span style="color: rgba(0, 0, 0, 1)">
      sql[</span>1] += ' ORDER BY updateTime DESC;'
      <span style="color: rgba(0, 0, 255, 1)">return</span> {sql: sql.join(''<span style="color: rgba(0, 0, 0, 1)">), param: param}
    }</span>&nbsp;</pre>
</div>
<h2>2.2&nbsp;转义</h2>
<p>默认情况下使用?转义,但是我觉得这种情况有点怪,例如select * from t_user where name=? and age=? and sex=?;这样要传入的参数是一个数组,并且要时刻注意数组的顺序和sql语句中?的顺序保持一致,这是不是反人类?所幸mysql.js有提供一个配置queryFormat,自定义转义,代码如下:</p>
<div class="cnblogs_code">
<pre>queryFormat: <span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> (sqlString, values) {
    </span><span style="color: rgba(0, 0, 255, 1)">if</span> (!values) <span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> sqlString;
    </span><span style="color: rgba(0, 0, 255, 1)">return</span> sqlString.replace(/\:(\w+)/g, <span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> (txt, key) {
    </span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (values.hasOwnProperty(key)) {
      </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.escape(values);
    }
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> txt;
    }.bind(</span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">))
}</span>&nbsp;</pre>
</div>
<p>这个函数的原理是使用字符串的replace方法将sql语句中的:columnname替换成转义后的请求值,这样写sql语句就方便多了,select * from t_user where name=:name and age=:age and sex=:sex; 还有传入参数的时候就可以直接传入一个对象就好,例如{name: '张三', age: 18, sex: 'man'},见名知义,岂不是很爽?</p>
<p>未解决问题:</p>
<ol>
<li>暂时没有考虑like,between,&gt;,&lt;等情况。</li>
<li>这里默认接口请求传入的字段名字和数据库中表的字段名字一致,这是不安全的。</li>
<li>使用multipleStatements设置一次执行多条语句,也不是很安全,会有sql注入危险。</li>
</ol>
<h1>3.&nbsp;部署上线</h1>
<p>部署上线首先要有域名和空间,这没啥好说的,就是买买买,不过域名不是必须的。<br>服务器我用的是阿里云的Ubuntu,要在里面安装nginx,node.js,npm,mysql,pm2或者forever。<br>mysql装好之后命令可以连接,查看,但是这不是影响工作效率,所有要用客户端连接,我用的是navicat for mysql。首先要在阿里云服务器里当前实例的安全组里配置端口访问规则,mysql使用的是3306,截图如下:<br><img src="https://img2018.cnblogs.com/blog/72678/201912/72678-20191204200346648-1755662925.png" alt="" width="536" height="588"><br>还要允许root用户从外网登陆,要修改mysql里的user表,这里不再赘述。</p>
<p>使用pm2启动node.js项目,防止因出错造成自动退出。pm2工具的使用就不再赘述。</p>
<p>最后前端使用proxyTable代理解决跨域问题的那一套,部署在服务器上就不管用了,这里没有在后端修改服务器响应头Access-Control-Allow-Origin,而是使用nginx代理,具体做法是使用vhost,将来自localhost:3332/api/路径的请求代理到本地127.0.0.1:3333。具体做法是在nginx的vhost目录下新建一个parttime.conf,内容如下:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">server {
      listen </span>3332<span style="color: rgba(0, 0, 0, 1)">;                                    # 端口
      server_name www.hzyayun.net hzyayun.net;      # 域名
      root </span>/usr/local/app/<span style="color: rgba(0, 0, 0, 1)">parttime;                   # 站点根目录
      index index.html;                               # 默认首页
      location </span>/api/<span style="color: rgba(0, 0, 0, 1)"> {
                proxy_pass http:</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">127.0.0.1:3333;       # 请求转发的地址</span>
                proxy_connect_timeout 6000<span style="color: rgba(0, 0, 0, 1)">;             # 连接超时设置
                proxy_read_timeout </span>6000<span style="color: rgba(0, 0, 0, 1)">;
                proxy_redirect off;                     # 不修改请求url
      }
}</span>&nbsp;</pre>
</div>
<p>在nginx的配置文件ngxin.conf内修改http对象,在http配置的最后一行跟上include /etc/nginx/vhost/*.conf; 然后重启nginx。最后还要开放3332,3333两个端口。如下:</p>
<p><img src="https://img2018.cnblogs.com/blog/72678/201912/72678-20191204200456362-1717316419.png" alt="" width="513" height="561"></p>
<p><img src="https://img2018.cnblogs.com/blog/72678/201912/72678-20191204200513155-449452384.png" alt="" width="512" height="559"></p>
<p>最后如果想用域名访问,需要在阿里云上解析域名,需要备案,太麻烦我就没有弄,直接使用域名访问:http://120.27.214.189:3332/</p>
<p>git地址:https://github.com/tylerdong/parttimejob</p>

</div>
<div id="MySignature" role="contentinfo">
    <p style="background-position: 1% 50%; border-top: #e0e0e0 1px dashed; border-right: #e0e0e0 1px dashed; border-bottom: #e0e0e0 1px dashed; border-left: #e0e0e0 1px dashed; padding-top: 10px; padding-right: 10px; padding-bottom: 10px; padding-left: 60px; background: #969696 url(&quot;https://images.cnblogs.com/cnblogs_com/lloydsheng/239039/o_copyright.gif&quot;) no-repeat 1% 50%; font-family: 微软雅黑; font-size: 12px; color: #FFFFFF">
            作者:<b><span style="font-size: 12px; color: red">Tyler Ning</span></b>
            <br>
            出处:http://www.cnblogs.com/tylerdonet/
            <br>
            本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,如有问题,请微信联系<strong >冬天里的一把火
            </strong>
<div class="van-overlay" style="display: none;z-index: 1;position: fixed;top: 0;left: 0;z-index: 1;width: 100%;height: 100%;background-color: rgba(0, 0, 0, 0.7);"></div>
<imgid="pop-contect-wechat" style="display:none;width:20em;position: fixed;max-height: 100%;overflow-y: auto;transition: transform 0.3s;" src="https://files-cdn.cnblogs.com/files/tylerdonet/shouwangzhe059187.bmp"/>
      </p><br><br>
来源:https://www.cnblogs.com/tylerdonet/p/11867772.html
頁: [1]
查看完整版本: node.js+react全栈实践-开篇